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.
 
 
 

374 lines
8.1 KiB

  1. let abs = Math.abs.bind(Math);
  2. let rnd = Math.random.bind(Math);
  3. let max = Math.max.bind(Math);
  4. const canPathHome = require('./mob/canPathHome');
  5. const teleportHome = (physics, obj, mob) => {
  6. physics.removeObject(obj, obj.x, obj.y);
  7. obj.x = mob.originX;
  8. obj.y = mob.originY;
  9. const syncer = obj.syncer;
  10. syncer.o.x = obj.x;
  11. syncer.o.y = obj.y;
  12. physics.addObject(obj, obj.x, obj.y);
  13. obj.aggro.clearIgnoreList();
  14. obj.aggro.move();
  15. };
  16. const performPatrolAction = ({ obj }, node) => {
  17. const { action } = node;
  18. const { chatter, syncer, instance: { scheduler } } = obj;
  19. if (action === 'chat') {
  20. if (!chatter)
  21. obj.addComponent('chatter');
  22. syncer.set(false, 'chatter', 'msg', node.msg);
  23. return true;
  24. }
  25. if (action === 'wait') {
  26. if (node.cron) {
  27. const isActive = scheduler.isActive(node);
  28. return isActive;
  29. } else if (node.ttl === undefined) {
  30. node.ttl = node.duration;
  31. return false;
  32. }
  33. node.ttl--;
  34. if (!node.ttl) {
  35. delete node .ttl;
  36. return true;
  37. }
  38. }
  39. };
  40. const getNextPatrolTarget = mob => {
  41. const { patrol, obj: { x, y } } = mob;
  42. let toX, toY;
  43. do {
  44. const toNode = patrol[mob.patrolTargetNode];
  45. if (toNode.action) {
  46. const nodeDone = performPatrolAction(mob, toNode);
  47. if (!nodeDone)
  48. return true;
  49. mob.patrolTargetNode++;
  50. if (mob.patrolTargetNode >= patrol.length)
  51. mob.patrolTargetNode = 0;
  52. continue;
  53. }
  54. toX = toNode[0];
  55. toY = toNode[1];
  56. if ((toX - x === 0) && (toY - y === 0)) {
  57. mob.patrolTargetNode++;
  58. if (mob.patrolTargetNode >= patrol.length)
  59. mob.patrolTargetNode = 0;
  60. } else
  61. break;
  62. } while (toX - x !== 0 || toY - y !== 0);
  63. return [ toX, toY ];
  64. };
  65. module.exports = {
  66. type: 'mob',
  67. target: null,
  68. physics: null,
  69. originX: 0,
  70. originY: 0,
  71. walkDistance: 1,
  72. maxChaseDistance: 25,
  73. goHome: false,
  74. patrol: null,
  75. patrolTargetNode: 0,
  76. needLos: null,
  77. init: function (blueprint) {
  78. this.physics = this.obj.instance.physics;
  79. this.originX = this.obj.x;
  80. this.originY = this.obj.y;
  81. if (blueprint.patrol)
  82. this.patrol = blueprint.patrol;
  83. if (blueprint.maxChaseDistance)
  84. this.maxChaseDistance = blueprint.maxChaseDistance;
  85. },
  86. /* eslint-disable-next-line max-lines-per-function */
  87. update: function () {
  88. let obj = this.obj;
  89. let target = null;
  90. if (obj.aggro)
  91. target = obj.aggro.getHighest();
  92. //Have we reached home?
  93. if (this.goHome) {
  94. let distanceFromHome = Math.max(abs(this.originX - obj.x), abs(this.originY - obj.y));
  95. if (!distanceFromHome) {
  96. this.goHome = false;
  97. if (!obj.spellbook) {
  98. /* eslint-disable-next-line no-console */
  99. console.log('MOB HAS NO SPELLBOOK BUT WANTS TO RESET ROTATION');
  100. /* eslint-disable-next-line no-console */
  101. console.log(obj.name, obj.zone, obj.zoneName, obj.x, obj.y, obj.components.map(c => c.type).join(','));
  102. }
  103. obj.spellbook.resetRotation();
  104. }
  105. }
  106. if (!this.goHome) {
  107. //Are we chasing a target too far from home?
  108. if (!obj.follower && target) {
  109. if (!this.canChase(target)) {
  110. obj.clearQueue();
  111. obj.aggro.unAggro(target);
  112. target = obj.aggro.getHighest();
  113. }
  114. }
  115. //Are we too far from home?
  116. let distanceFromHome = Math.max(abs(this.originX - obj.x), abs(this.originY - obj.y));
  117. if (distanceFromHome > this.maxChaseDistance)
  118. this.goHome = true;
  119. else if ((target) && (target !== obj) && ((!obj.follower) || (obj.follower.master !== target))) {
  120. //If we just started attacking, patrols need to know where home is
  121. if (!this.target && this.patrol) {
  122. this.originX = obj.x;
  123. this.originY = obj.y;
  124. }
  125. //Are we in fight mode?
  126. this.fight(target);
  127. return;
  128. } else if (!target && this.target) {
  129. //Is fight mode over?
  130. this.target = null;
  131. obj.clearQueue();
  132. obj.spellbook.resetRotation();
  133. if (canPathHome(this))
  134. this.goHome = true;
  135. else
  136. teleportHome(this.physics, obj, this);
  137. }
  138. }
  139. //If we're already going somewhere, don't calculate a new path
  140. if (obj.actionQueue.length > 0)
  141. return;
  142. //Unless we're going home, don't always move
  143. if (!this.goHome && rnd() < 0.85 && !this.patrol)
  144. return;
  145. //Don't move around if we're not allowed to, unless we're going home
  146. let walkDistance = this.walkDistance;
  147. if ((!this.goHome) && (walkDistance <= 0))
  148. return;
  149. let toX, toY;
  150. //Patrol mobs should not pick random locations unless they're going home
  151. if (this.goHome || !this.patrol) {
  152. toX = this.originX + ~~(rnd() * (walkDistance * 2)) - walkDistance;
  153. toY = this.originY + ~~(rnd() * (walkDistance * 2)) - walkDistance;
  154. } else if (this.patrol) {
  155. const patrolResult = getNextPatrolTarget(this);
  156. //When an action is performed, we will only get a boolean value back
  157. if (!patrolResult.push)
  158. return;
  159. [toX, toY] = patrolResult;
  160. }
  161. //We use goHome to force followers to follow us around but they should never stay in that state
  162. // since it messes with combat
  163. if (obj.follower)
  164. this.goHome = false;
  165. const dx = abs(obj.x - toX);
  166. const dy = abs(obj.y - toY);
  167. if (dx + dy === 0)
  168. return;
  169. if (dx <= 1 && dy <= 1) {
  170. obj.queue({
  171. action: 'move',
  172. data: {
  173. x: toX,
  174. y: toY
  175. }
  176. });
  177. return;
  178. }
  179. const path = this.physics.getPath({
  180. x: obj.x,
  181. y: obj.y
  182. }, {
  183. x: toX,
  184. y: toY
  185. }, false);
  186. const pLen = path.length;
  187. for (let i = 0; i < pLen; i++) {
  188. let p = path[i];
  189. obj.queue({
  190. action: 'move',
  191. data: {
  192. x: p.x,
  193. y: p.y
  194. }
  195. });
  196. }
  197. },
  198. fight: function (target) {
  199. let obj = this.obj;
  200. if (this.target !== target) {
  201. obj.clearQueue();
  202. this.target = target;
  203. }
  204. //If the target is true, it means we can't reach the target and should wait for a new one
  205. if (this.target === true)
  206. return;
  207. else if (obj.spellbook.isCasting())
  208. return;
  209. let x = obj.x;
  210. let y = obj.y;
  211. let tx = ~~target.x;
  212. let ty = ~~target.y;
  213. let distance = max(abs(x - tx), abs(y - ty));
  214. let furthestAttackRange = obj.spellbook.getFurthestRange(target, true);
  215. let furthestStayRange = obj.spellbook.getFurthestRange(target, false);
  216. let doesCollide = null;
  217. let hasLos = null;
  218. if (distance <= furthestAttackRange) {
  219. doesCollide = this.physics.mobsCollide(x, y, obj, target);
  220. if (!doesCollide) {
  221. hasLos = this.physics.hasLos(x, y, tx, ty);
  222. //Maybe we don't care if the mob has LoS
  223. if (hasLos || this.needLos === false) {
  224. let spell = obj.spellbook.getSpellToCast(target);
  225. if (!spell)
  226. return;
  227. let success = obj.spellbook.cast({
  228. spell: spell.id,
  229. target
  230. });
  231. //null means we don't have LoS
  232. if (success !== null)
  233. return;
  234. hasLos = false;
  235. }
  236. }
  237. } else if (furthestAttackRange === 0) {
  238. if (distance <= obj.spellbook.closestRange && !this.physics.mobsCollide(x, y, obj, target))
  239. return;
  240. }
  241. let targetPos = this.physics.getClosestPos(x, y, tx, ty, target, obj);
  242. if (!targetPos) {
  243. //Find a new target
  244. obj.aggro.ignore(target);
  245. //TODO: Don't skip a turn
  246. return;
  247. }
  248. let newDistance = max(abs(targetPos.x - tx), abs(targetPos.y - ty));
  249. if (newDistance >= distance && newDistance > furthestStayRange) {
  250. obj.clearQueue();
  251. obj.aggro.ignore(target);
  252. if (!obj.aggro.getHighest()) {
  253. //Nobody left to attack so reset our aggro table
  254. obj.aggro.die();
  255. this.goHome = true;
  256. }
  257. return;
  258. }
  259. if (abs(x - targetPos.x) <= 1 && abs(y - targetPos.y) <= 1) {
  260. obj.queue({
  261. action: 'move',
  262. data: {
  263. x: targetPos.x,
  264. y: targetPos.y
  265. }
  266. });
  267. } else {
  268. let path = this.physics.getPath({
  269. x: x,
  270. y: y
  271. }, {
  272. x: targetPos.x,
  273. y: targetPos.y
  274. });
  275. if (path.length === 0) {
  276. obj.aggro.ignore(target);
  277. //TODO: Don't skip a turn
  278. return;
  279. }
  280. let p = path[0];
  281. obj.queue({
  282. action: 'move',
  283. data: {
  284. x: p.x,
  285. y: p.y
  286. }
  287. });
  288. }
  289. },
  290. canChase: function (obj) {
  291. //Patrol mobs can always chase if they don't have a target yet (since they don't have a home yet)
  292. if (this.patrol && !this.target && !this.goHome)
  293. return true;
  294. let distanceFromHome = Math.max(abs(this.originX - obj.x), abs(this.originY - obj.y));
  295. return ((!this.goHome) && (distanceFromHome <= this.maxChaseDistance));
  296. },
  297. events: {
  298. beforeTakeDamage: function ({ damage }) {
  299. if (this.goHome)
  300. damage.failed = true;
  301. }
  302. }
  303. };