You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

412 line
8.3 KiB

  1. const configThreatCeiling = {
  2. regular: 1,
  3. rare: 0.5,
  4. champion: 0.2
  5. };
  6. module.exports = {
  7. type: 'aggro',
  8. range: 7,
  9. cascadeRange: 5,
  10. faction: null,
  11. physics: null,
  12. list: [],
  13. ignoreList: [],
  14. threatDecay: 0.9,
  15. threatCeiling: 1,
  16. //Certain summoned minions need to despawn when they lose their last target
  17. dieOnAggroClear: false,
  18. init: function (blueprint) {
  19. this.physics = this.obj.instance.physics;
  20. blueprint = blueprint || {};
  21. if (blueprint.faction)
  22. this.faction = blueprint.faction;
  23. //TODO: Why don't we move if faction is null?
  24. if (!this.has('faction'))
  25. return;
  26. if (this.physics.width > 0)
  27. this.move();
  28. },
  29. calcThreatCeiling: function (mobType) {
  30. this.threatCeiling = configThreatCeiling[mobType];
  31. },
  32. events: {
  33. beforeRezone: function () {
  34. this.die();
  35. }
  36. },
  37. simplify: function (self) {
  38. return {
  39. type: 'aggro',
  40. faction: this.faction
  41. };
  42. },
  43. //If we send through a proxy, we know it's for cascading threat
  44. move: function (proxy) {
  45. let obj = proxy || this.obj;
  46. let aggro = obj.aggro;
  47. if (obj.dead)
  48. return;
  49. let result = {
  50. success: true
  51. };
  52. obj.fireEvent('beforeAggro', result);
  53. if (!result.success)
  54. return;
  55. //If we're attacking something, don't try and look for more trouble. SAVE THE CPU!
  56. // this only counts for mobs, players can have multiple attackers
  57. let list = aggro.list;
  58. if (obj.mob) {
  59. let lLen = list.length;
  60. for (let i = 0; i < lLen; i++) {
  61. let l = list[i];
  62. let lThreat = l.obj.aggro.getHighest();
  63. if (lThreat) {
  64. l.obj.aggro.list.forEach(function (a) {
  65. a.obj.aggro.unIgnore(lThreat);
  66. });
  67. }
  68. l.obj.aggro.unIgnore(obj);
  69. if (l.threat > 0)
  70. return;
  71. }
  72. } else {
  73. let lLen = list.length;
  74. for (let i = 0; i < lLen; i++) {
  75. let targetAggro = list[i].obj.aggro;
  76. //Maybe the aggro component has been removed?
  77. if (targetAggro)
  78. targetAggro.unIgnore(obj);
  79. }
  80. }
  81. let x = this.obj.x;
  82. let y = this.obj.y;
  83. //Find mobs in range
  84. let range = proxy ? aggro.cascadeRange : aggro.range;
  85. let inRange = this.physics.getArea(x - range, y - range, x + range, y + range, c => (
  86. c.aggro &&
  87. !c.dead &&
  88. (
  89. !c.player ||
  90. !obj.player
  91. ) &&
  92. c.aggro.willAutoAttack(obj) &&
  93. !list.some(l => l.obj === c)
  94. ));
  95. if (!inRange.length)
  96. return;
  97. let iLen = inRange.length;
  98. for (let i = 0; i < iLen; i++) {
  99. let enemy = inRange[i];
  100. if (!this.physics.hasLos(x, y, enemy.x, enemy.y))
  101. continue;
  102. else if (enemy.aggro.tryEngage(obj))
  103. aggro.tryEngage(enemy, 0);
  104. }
  105. },
  106. canAttack: function (target) {
  107. let obj = this.obj;
  108. if (target === obj)
  109. return false;
  110. else if ((target.player) && (obj.player)) {
  111. let hasButcher = (obj.prophecies.hasProphecy('butcher')) && (target.prophecies.hasProphecy('butcher'));
  112. if ((!target.social.party) || (!obj.social.party))
  113. return hasButcher;
  114. else if (target.social.partyLeaderId !== obj.social.partyLeaderId)
  115. return hasButcher;
  116. return false;
  117. } else if ((target.follower) && (target.follower.master.player) && (obj.player))
  118. return false;
  119. else if (obj.player)
  120. return true;
  121. else if (target.aggro.faction !== obj.aggro.faction)
  122. return true;
  123. else if (!!target.player !== !!obj.player)
  124. return true;
  125. },
  126. willAutoAttack: function (target) {
  127. if (this.obj === target)
  128. return false;
  129. let faction = target.aggro.faction;
  130. if (!faction || !this.faction)
  131. return false;
  132. let rep = this.obj.reputation;
  133. if (!rep) {
  134. let targetRep = target.reputation;
  135. if (!targetRep)
  136. return false;
  137. return (targetRep.getTier(this.faction) < 3);
  138. }
  139. return (rep.getTier(faction) < 3);
  140. },
  141. ignore: function (obj) {
  142. this.ignoreList.spliceWhere(o => o === obj);
  143. this.ignoreList.push(obj);
  144. },
  145. unIgnore: function (obj) {
  146. this.ignoreList.spliceWhere(o => o === obj);
  147. },
  148. tryEngage: function (source, amount, threatMult) {
  149. let obj = this.obj;
  150. //Don't aggro yourself, stupid
  151. if (source === obj)
  152. return;
  153. let result = {
  154. success: true
  155. };
  156. obj.fireEvent('beforeAggro', result);
  157. if (!result.success)
  158. return false;
  159. //Mobs shouldn't aggro players that are too far from their home
  160. let mob = obj.mob || source.mob;
  161. if (mob) {
  162. let notMob = source.mob ? obj : source;
  163. if (!mob.canChase(notMob))
  164. return false;
  165. }
  166. let oId = source.id;
  167. let list = this.list;
  168. amount = (amount || 0);
  169. let threat = (amount / obj.stats.values.hpMax) * (threatMult || 1);
  170. let exists = list.find(l => l.obj.id === oId);
  171. if (!exists) {
  172. exists = {
  173. obj: source,
  174. damage: 0,
  175. threat: 0
  176. };
  177. list.push(exists);
  178. //Cascade threat
  179. if (obj.mob)
  180. this.move(source);
  181. }
  182. exists.damage += amount;
  183. exists.threat += threat;
  184. if (exists.threat > this.threatCeiling)
  185. exists.threat = this.threatCeiling;
  186. return true;
  187. },
  188. getFirstAttacker: function () {
  189. let first = this.list.find(l => ((l.obj.player) && (l.damage > 0)));
  190. if (first)
  191. return first.obj;
  192. return null;
  193. },
  194. die: function () {
  195. let list = this.list;
  196. let lLen = list.length;
  197. for (let i = 0; i < lLen; i++) {
  198. let l = list[i];
  199. if (!l) {
  200. lLen--;
  201. continue;
  202. }
  203. //Maybe the aggro component was removed?
  204. let targetAggro = l.obj.aggro;
  205. if (targetAggro) {
  206. targetAggro.unAggro(this.obj);
  207. i--;
  208. lLen--;
  209. }
  210. }
  211. this.list = [];
  212. },
  213. unAggro: function (obj, amount) {
  214. let list = this.list;
  215. let lLen = list.length;
  216. for (let i = 0; i < lLen; i++) {
  217. let l = list[i];
  218. if (l.obj !== obj)
  219. continue;
  220. if (!amount) {
  221. list.splice(i, 1);
  222. obj.aggro.unAggro(this.obj);
  223. break;
  224. } else {
  225. l.threat -= amount;
  226. if (l.threat <= 0) {
  227. list.splice(i, 1);
  228. obj.aggro.unAggro(this.obj);
  229. break;
  230. }
  231. }
  232. }
  233. if (list.length !== lLen && this.dieOnAggroClear)
  234. this.obj.destroyed = true;
  235. this.ignoreList.spliceWhere(o => o === obj);
  236. //Stuff like cocoons don't have spellbooks
  237. if (this.obj.spellbook)
  238. this.obj.spellbook.unregisterCallback(obj.id, true);
  239. if ((this.list.length === 0) && (this.obj.mob) && (!this.obj.follower))
  240. this.obj.stats.resetHp();
  241. },
  242. sortThreat: function () {
  243. this.list.sort(function (a, b) {
  244. return (b.threat - a.threat);
  245. });
  246. },
  247. getHighest: function () {
  248. if (this.list.length === 0)
  249. return null;
  250. let list = this.list;
  251. let lLen = list.length;
  252. let highest = null;
  253. let closest = 99999;
  254. let thisObj = this.obj;
  255. let x = thisObj.x;
  256. let y = thisObj.y;
  257. for (let i = 0; i < lLen; i++) {
  258. let l = list[i];
  259. let obj = l.obj;
  260. if (this.ignoreList.some(o => o === obj))
  261. continue;
  262. if (!highest || l.threat > highest.threat) {
  263. highest = l;
  264. closest = Math.max(Math.abs(x - obj.x), Math.abs(y - obj.y));
  265. } else if (l.threat === highest.threat && l.threat !== 0) {
  266. //Don't chase a closer target if both targets are at 0 threat because
  267. // this means that neither of them have attacked the target.
  268. // This stops people from griefing other players by pulling mobs to them.
  269. let distance = Math.max(Math.abs(x - obj.x), Math.abs(y - obj.y));
  270. if (distance < closest) {
  271. highest = l;
  272. closest = distance;
  273. }
  274. }
  275. }
  276. if (highest)
  277. return highest.obj;
  278. return null;
  279. },
  280. getFurthest: function () {
  281. let furthest = null;
  282. let distance = 0;
  283. let list = this.list;
  284. let lLen = list.length;
  285. let thisObj = this.obj;
  286. let x = thisObj.x;
  287. let y = thisObj.y;
  288. for (let i = 0; i < lLen; i++) {
  289. let l = list[i];
  290. let obj = l.obj;
  291. if (this.ignoreList.some(o => o === obj))
  292. continue;
  293. let oDistance = Math.max(Math.abs(x - obj.x), Math.abs(y - obj.y));
  294. if (oDistance > distance) {
  295. furthest = l;
  296. distance = oDistance;
  297. }
  298. }
  299. return furthest.obj;
  300. },
  301. getRandom: function () {
  302. let useList = this.list.filter(l => (!this.ignoreList.some(o => (o === l.obj))));
  303. return useList[~~(Math.random() * useList.length)];
  304. },
  305. hasAggroOn: function (obj) {
  306. return (
  307. this.list.find(l => l.obj === obj) &&
  308. !this.ignoreList.find(l => l === obj)
  309. );
  310. },
  311. update: function () {
  312. let list = this.list;
  313. let lLen = list.length;
  314. for (let i = 0; i < lLen; i++) {
  315. let l = list[i];
  316. if (l.obj.destroyed) {
  317. this.unAggro(l.obj);
  318. i--;
  319. lLen--;
  320. } else if (l.threat > 0)
  321. l.threat *= this.threatDecay;
  322. }
  323. },
  324. clearIgnoreList: function () {
  325. this.ignoreList = [];
  326. },
  327. isInCombat: function () {
  328. return this.list.length > 0;
  329. }
  330. };