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.
 
 
 

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