Browse Source

feat #1872: Implemented rune rotations

tags/v0.10.6^2
Shaun 2 years ago
parent
commit
576f725813
6 changed files with 167 additions and 77 deletions
  1. +4
    -0
      src/server/components/aggro.js
  2. +17
    -13
      src/server/components/mob.js
  3. +24
    -30
      src/server/components/spellbook.js
  4. +111
    -0
      src/server/components/spellbook/rotationManager.js
  5. +9
    -19
      src/server/objects/objBase.js
  6. +2
    -15
      src/server/world/mobBuilder.js

+ 4
- 0
src/server/components/aggro.js View File

@@ -403,5 +403,9 @@ module.exports = {

clearIgnoreList: function () {
this.ignoreList = [];
},

isInCombat: function () {
return this.list.length > 0;
}
};

+ 17
- 13
src/server/components/mob.js View File

@@ -98,6 +98,8 @@ module.exports = {
patrol: null,
patrolTargetNode: 0,

needLos: null,

init: function (blueprint) {
this.physics = this.obj.instance.physics;

@@ -251,8 +253,8 @@ module.exports = {
let ty = ~~target.y;

let distance = max(abs(x - tx), abs(y - ty));
let furthestAttackRange = obj.spellbook.getFurthestRange(null, true);
let furthestStayRange = obj.spellbook.getFurthestRange(null, false);
let furthestAttackRange = obj.spellbook.getFurthestRange(target, true);
let furthestStayRange = obj.spellbook.getFurthestRange(target, false);

let doesCollide = null;
let hasLos = null;
@@ -263,18 +265,20 @@ module.exports = {
hasLos = this.physics.hasLos(x, y, tx, ty);
//Maybe we don't care if the mob has LoS
if (hasLos || this.needLos === false) {
if (((obj.follower) && (obj.follower.master.player)) || (rnd() < 0.65)) {
let spell = obj.spellbook.getRandomSpell(target);
let success = obj.spellbook.cast({
spell: spell,
target: target
});
//null means we don't have LoS
if (success !== null)
return;
hasLos = false;
} else
let spell = obj.spellbook.getSpellToCast(target);
if (!spell)
return;

let success = obj.spellbook.cast({
spell: spell.id,
target
});

//null means we don't have LoS
if (success !== null)
return;

hasLos = false;
}
}
} else if (furthestAttackRange === 0) {


+ 24
- 30
src/server/components/spellbook.js View File

@@ -3,6 +3,11 @@ let animations = require('../config/animations');
let playerSpells = require('../config/spells');
let playerSpellsConfig = require('../config/spellsConfig');

//Helpers
const rotationManager = require('./spellbook/rotationManager');

//Component

module.exports = {
type: 'spellbook',

@@ -16,6 +21,8 @@ module.exports = {

callbacks: [],

rotation: null,

init: function (blueprint) {
this.objects = this.obj.instance.objects;
this.physics = this.obj.instance.physics;
@@ -24,7 +31,21 @@ module.exports = {

(blueprint.spells || []).forEach(s => this.addSpell(s, -1));

if (blueprint.rotation) {
const { duration, spells } = blueprint.rotation;

this.rotation = {
currentTick: 0,
duration,
spells
};
}

delete blueprint.spells;

//External helpers that should form part of the component
this.getSpellToCast = rotationManager.getSpellToCast.bind(null, this);
this.getFurthestRange = rotationManager.getFurthestRange.bind(null, this);
},

transfer: function () {
@@ -243,17 +264,6 @@ module.exports = {
});
},

getRandomSpell: function (target) {
const valid = this.spells.filter(s => {
return (!s.selfCast && !s.procCast && !s.castOnDeath && s.canCast(target));
});

if (!valid.length)
return null;

return valid[~~(Math.random() * valid.length)].id;
},

getTarget: function (spell, action) {
let target = action.target;

@@ -445,25 +455,6 @@ module.exports = {
return this.closestRange;
},

getFurthestRange: function (spellNum, checkCanCast) {
if (spellNum)
return this.spells[spellNum].range;

let spells = this.spells;
let sLen = spells.length;
let furthest = 0;
for (let i = 0; i < sLen; i++) {
let spell = spells[i];
if (spell.procCast || spell.castOnDeath)
continue;

if (spell.range > furthest && (!checkCanCast || spell.canCast()))
furthest = spell.range;
}

return furthest;
},

getCooldowns: function () {
let cds = [];
this.spells.forEach(
@@ -480,6 +471,9 @@ module.exports = {
let didCast = false;
const isCasting = this.isCasting();

if (this.rotation)
rotationManager.tick(this);

this.spells.forEach(s => {
let auto = s.autoActive;
if (auto) {


+ 111
- 0
src/server/components/spellbook/rotationManager.js View File

@@ -0,0 +1,111 @@
//Mobs that define rotations (normally bosses) use this method to determine their spell choices
const getRotationSpell = (source, target) => {
const { spells, rotation: { currentTick, spells: rotationSpells } } = source;

//Find spell matching current tick
let rotationEntry = rotationSpells.find(s => s.atRotationTicks?.includes(currentTick));

//If no rotation spell found, use a default spell
//Todo: round-robin/random/weighted/whatever when there are more than one
if (!rotationEntry)
rotationEntry = rotationSpells.find(s => !s.atRotationTicks);

if (!rotationEntry)
return;

//Don't cast anything
if (rotationEntry.spellIndex === -1)
return;

const useSpell = spells[rotationEntry.spellIndex];

//Todo: We should set cdMax and manaCost to 0 of rotation spells (unless there's a mana drain mechanic)
// later and we want to allow that on bosses
useSpell.cd = 0;
useSpell.manaCost = 0;
if (!useSpell.selfCast && !useSpell.canCast(target))
return;

return useSpell;
};

//Mobs without rune rotations (normally the case) simple select any random spell that is valid
const getRandomSpell = (source, target) => {
const valid = source.spells.filter(s => {
return (!s.selfCast && !s.procCast && !s.castOnDeath && s.canCast(target));
});

if (!valid.length)
return null;

return valid[~~(Math.random() * valid.length)];
};

const getSpellToCast = (source, target) => {
if (source.rotation)
return getRotationSpell(source, target);

const { obj: { follower } } = source;

//Mobs don't cast all the time but player followers do
if (!follower?.master?.player && Math.random() >= 0.65)
return;

return getRandomSpell(source, target);
};

const tick = source => {
if (!source.obj.aggro.isInCombat())
return;

const { rotation } = source;

rotation.currentTick++;

if (rotation.currentTick === rotation.duration)
rotation.currentTick = 1;
};

//Gets the range we need to be at to cast a specific rotation spell
const getFurthestRangeRotation = (source, target, checkCanCast) => {
const spell = getRotationSpell(source, target);

if (!spell)
return 0;

return spell.range;
};

/*
This is used by mobs when in combat mode,
* When checkCanCast is true, we want to see if we can cast right now
* When checkCanCast is false, we want to see if there is a spell we could cast in the near future
-> This could be a spell that is currently on cooldown, or that the mob has insufficient mana for
---> Even though mobs don't need mana for spells at the moment
-> Ultimately, this means the mob should not move, just wait
*/
const getFurthestRange = (source, target, checkCanCast) => {
const { spells, rotation } = source;

if (rotation)
return getFurthestRangeRotation(source, target, checkCanCast);

let sLen = spells.length;
let furthest = 0;
for (let i = 0; i < sLen; i++) {
let spell = spells[i];
if (spell.procCast || spell.castOnDeath)
continue;

if (spell.range > furthest && (!checkCanCast || spell.canCast()))
furthest = spell.range;
}

return furthest;
};

module.exports = {
tick,
getSpellToCast,
getFurthestRange
};

+ 9
- 19
src/server/objects/objBase.js View File

@@ -277,7 +277,7 @@ module.exports = {
},

performMove: function (action) {
const { x: xOld, y: yOld, syncer, aggro, mob, instance: { physics } } = this;
const { x: xOld, y: yOld, syncer, aggro, instance: { physics } } = this;

const { maxDistance = 1, force, data } = action;
const { x: xNew, y: yNew } = data;
@@ -309,26 +309,16 @@ module.exports = {
return false;
}

//Don't allow mob overlap during combat
if (mob && mob.target) {
this.x = xNew;
this.y = yNew;
this.x = xNew;
this.y = yNew;

if (physics.addObject(this, xNew, yNew))
physics.removeObject(this, xOld, yOld);
else {
this.x = xOld;
this.y = yOld;

return false;
}
} else {
physics.removeObject(this, xOld, yOld, xNew, yNew);

this.x = xNew;
this.y = yNew;
if (physics.addObject(this, xNew, yNew))
physics.removeObject(this, xOld, yOld);
else {
this.x = xOld;
this.y = yOld;

physics.addObject(this, xNew, yNew, xOld, yOld);
return false;
}

//We can't use xNew and yNew because addObject could have changed the position (like entering a building interior with stairs)


+ 2
- 15
src/server/world/mobBuilder.js View File

@@ -50,6 +50,8 @@ module.exports = {
if (cpnMob.patrol)
cpnMob.walkDistance = 1;

cpnMob.needLos = blueprint.needLos;

let spells = extend([], blueprint.spells);
spells.forEach(s => {
if (!s.animation && mob.sheetName === 'mobs' && animations.mobs[mob.cell])
@@ -179,21 +181,6 @@ module.exports = {
s.dmgMult = s.name ? dmgMult / 3 : dmgMult;
s.statType = preferStat;
s.manaCost = 0;

/*if (mob.name.toLowerCase().includes('stinktooth')) {
mob.stats.values.critChance = 0;
mob.stats.values.attackCritChance = 0;
mob.stats.values.spellCritChance = 0;

const n = mob.name + '-' + s.type;
if (!track[n])
track[n] = [];

track[n].push(~~s.getDamage(mob, true).amount);
track[n].sort((a, b) => a - b);
console.log(track);
console.log('');
}*/
});

//Hack to disallow low level mobs from having any lifeOnHit


Loading…
Cancel
Save