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.
 
 
 

415 lines
8.3 KiB

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