diff --git a/src/server/components/inventory.js b/src/server/components/inventory.js index 43edb19c..5a0b6e33 100644 --- a/src/server/components/inventory.js +++ b/src/server/components/inventory.js @@ -403,7 +403,44 @@ module.exports = { try { let effectModule = require('../' + effectUrl); e.events = effectModule.events; - if (effectModule.events.onGetText) + + const { rolls } = e; + + if (rolls.textTemplate) { + let text = rolls.textTemplate; + + while (text.includes('((')) { + Object.entries(rolls).forEach(([k, v]) => { + text = text.replaceAll(`((${k}))`, v); + }); + + if (rolls.applyEffect) { + Object.entries(rolls.applyEffect).forEach(([k, v]) => { + text = text.replaceAll(`((applyEffect.${k}))`, v); + }); + } + + if (rolls.castSpell) { + Object.entries(rolls.castSpell).forEach(([k, v]) => { + text = text.replaceAll(`((castSpell.${k}))`, v); + }); + } + + if (rolls.applyEffect?.scaleDamage) { + Object.entries(rolls.applyEffect.scaleDamage).forEach(([k, v]) => { + text = text.replaceAll(`((applyEffect.scaleDamage.${k}))`, v); + }); + } + + if (rolls.castSpell?.scaleDamage) { + Object.entries(rolls.castSpell.scaleDamage).forEach(([k, v]) => { + text = text.replaceAll(`((castSpell.scaleDamage.${k}))`, v); + }); + } + } + + e.text = text; + } else if (effectModule.events.onGetText) e.text = effectModule.events.onGetText(item, e); } catch (error) { _.log(`Effect not found: ${e.type}`); diff --git a/src/server/components/stats.js b/src/server/components/stats.js index c7cb86a1..28baff4c 100644 --- a/src/server/components/stats.js +++ b/src/server/components/stats.js @@ -536,7 +536,9 @@ module.exports = { target: Target object (heal.target) spell: Optional spell object that caused this event */ - getHp: function (heal, source, event) { + getHp: function (event) { + const { heal, source } = event; + let amount = heal.amount; if (amount === 0) return; @@ -548,48 +550,45 @@ module.exports = { let values = this.values; let hpMax = values.hpMax; - if (values.hp >= hpMax) - return; - - if (hpMax - values.hp < amount) - amount = hpMax - values.hp; - - values.hp += amount; - if (values.hp > hpMax) - values.hp = hpMax; - - let recipients = []; - if (this.obj.serverId) - recipients.push(this.obj.serverId); - if (source.serverId) - recipients.push(source.serverId); - if (recipients.length > 0) { - this.syncer.queue('onGetDamage', { - id: this.obj.id, - source: source.id, - heal: true, - amount: amount, - crit: heal.crit, - element: heal.element - }, recipients); - } + if (values.hp < hpMax) { + if (hpMax - values.hp < amount) + amount = hpMax - values.hp; + + values.hp += amount; + if (values.hp > hpMax) + values.hp = hpMax; + + let recipients = []; + if (this.obj.serverId) + recipients.push(this.obj.serverId); + if (source.serverId) + recipients.push(source.serverId); + if (recipients.length > 0) { + this.syncer.queue('onGetDamage', { + id: this.obj.id, + source: source.id, + heal: true, + amount: amount, + crit: heal.crit, + element: heal.element + }, recipients); + } - //Add aggro to all our attackers - let threat = amount * 0.4 * threatMult; - if (threat !== 0) { - let aggroList = this.obj.aggro.list; - let aLen = aggroList.length; - for (let i = 0; i < aLen; i++) { - let a = aggroList[i].obj; - a.aggro.tryEngage(source, threat); + //Add aggro to all our attackers + let threat = amount * 0.4 * threatMult; + if (threat !== 0) { + let aggroList = this.obj.aggro.list; + let aLen = aggroList.length; + for (let i = 0; i < aLen; i++) { + let a = aggroList[i].obj; + a.aggro.tryEngage(source, threat); + } } - } - this.obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp); + this.obj.syncer.setObject(false, 'stats', 'values', 'hp', values.hp); + } - //We want to eventually replace the first two args with the event object - // For now, only fire the event when the event object is specified. - if (!heal.noEvents && event) + if (!heal.noEvents) source.fireEvent('afterGiveHp', event); }, @@ -730,7 +729,11 @@ module.exports = { if (target === obj || !lifeOnHit) return; - this.getHp({ amount: lifeOnHit }, obj); + this.getHp({ + event: { amount: lifeOnHit }, + source: obj, + target: obj + }); } } }; diff --git a/src/server/config/effects/effectHolyVengeance.js b/src/server/config/effects/effectHolyVengeance.js index 0f68395d..01d536c5 100644 --- a/src/server/config/effects/effectHolyVengeance.js +++ b/src/server/config/effects/effectHolyVengeance.js @@ -6,7 +6,11 @@ module.exports = { events: { afterDealDamage: function ({ damage, target }) { damage.dealt *= 0.5; - this.obj.stats.getHp(damage, this.obj); + this.obj.stats.getHp({ + event: damage, + source: this.obj, + target: this.obj + }); } } }; diff --git a/src/server/config/itemEffects/castSpellOnHit.js b/src/server/config/itemEffects/castSpellOnHit.js index 636eede8..091e2841 100644 --- a/src/server/config/itemEffects/castSpellOnHit.js +++ b/src/server/config/itemEffects/castSpellOnHit.js @@ -1,37 +1,95 @@ +//Imports const spellBaseTemplate = require('../spells/spellTemplate'); -module.exports = { - events: { - onGetText: function (item) { - const { rolls: { chance, spell, damage = 1 } } = item.effects.find(e => (e.type === 'castSpellOnHit')); +//Helpers +const getItemEffect = item => { + return item.effects.find(e => (e.type === 'castSpellOnHit')); +}; - return `${chance}% chance to cast a ${damage} damage ${spell} on hit`; - }, +const doesEventMatch = (firedEvent, eventCondition) => { + if ( + !firedEvent || + ( + !eventCondition.targetNotSelf && + firedEvent.target === firedEvent.source + ) + ) + return false; + + const foundNonMatch = Object.entries(eventCondition).some(([k, v]) => { + if (v !== null && typeof(v) === 'object') { + if (!doesEventMatch(firedEvent[k], v)) + return true; + + return false; + } + + return firedEvent[k] !== v; + }); + + return !foundNonMatch; +}; + +const shouldApplyEffect = (itemEffect, firedEvent, firedEventName) => { + const { rolls: { chance, combatEvent: { [firedEventName]: eventCondition } } } = itemEffect; + + if (!eventCondition || !doesEventMatch(firedEvent, eventCondition)) + return false; + + if (chance !== undefined && Math.random() * 100 >= chance) + return false; + + return true; +}; + +const handler = (obj, item, event, firedEventName) => { + const itemEffect = getItemEffect(item); - afterDealDamage: function (item, { damage, target }) { - //Should only proc for attacks...this is kind of a hack - const { element } = damage; - if (element) - return; - - const { rolls: { chance, spell, statType = 'dex', damage: spellDamage = 1 } } = item.effects.find(e => (e.type === 'castSpellOnHit')); - - const chanceRoll = Math.random() * 100; - if (chanceRoll >= chance) - return; - - const spellName = 'spell' + spell.replace(/./, spell.toUpperCase()[0]); - const spellTemplate = require(`../spells/${spellName}`); - const builtSpell = extend({ obj: this }, spellBaseTemplate, spellTemplate, { - name: spellName, - noEvents: true, - statType, - damage: spellDamage, - duration: 5, - radius: 1 + if (!shouldApplyEffect(itemEffect, event, firedEventName)) + return; + + const { rolls: { castSpell, castTarget } } = itemEffect; + + const spellConfig = extend({}, castSpell); + spellConfig.noEvents = true; + + const scaleDamage = spellConfig.scaleDamage; + delete spellConfig.scaleDamage; + + if (scaleDamage) { + if (scaleDamage.s_useOriginal) { + scaleDamage.s_useOriginal.forEach(s => { + spellConfig[s] = event.spell[s]; }); + } + + if (scaleDamage.percentage) + spellConfig.damage *= (scaleDamage.percentage / 100); + } + + const spellName = 'spell' + spellConfig.type.replace(/./, spellConfig.type.toUpperCase()[0]); + const spellTemplate = require(`../spells/${spellName}`); + + const builtSpell = extend({ obj }, spellBaseTemplate, spellTemplate, spellConfig); + + let target = event.target; + if (castTarget === 'self') + target = obj; + else if (castTarget === 'none') + target = undefined; + + builtSpell.cast({ target }); +}; + +//Effect +module.exports = { + events: { + afterGiveHp: function (item, event) { + handler(this, item, event, 'afterGiveHp'); + }, - builtSpell.cast(); + afterDealDamage: function (item, event) { + handler(this, item, event, 'afterDealDamage'); } } }; diff --git a/src/server/config/itemEffects/gainStat.js b/src/server/config/itemEffects/gainStat.js index 666fa76a..6381f1aa 100644 --- a/src/server/config/itemEffects/gainStat.js +++ b/src/server/config/itemEffects/gainStat.js @@ -18,9 +18,13 @@ module.exports = { amount = (cpnStats.values.hpMax / 100) * ~~amount.replace('%', ''); cpnStats.getHp({ - amount: amount, - threatMult: 0 - }, item); + event: { + amount: amount, + threatMult: 0 + }, + source: cpnStats.obj, + target: cpnStats.obj + }); } else cpnStats.addStat(stat, amount); }, diff --git a/src/server/config/itemEffects/healOnCrit.js b/src/server/config/itemEffects/healOnCrit.js index 47a0f5b4..d6862cc5 100644 --- a/src/server/config/itemEffects/healOnCrit.js +++ b/src/server/config/itemEffects/healOnCrit.js @@ -34,8 +34,12 @@ module.exports = { let amount = rolls.amount || ((damage.dealt / 100) * rolls.percentage); this.stats.getHp({ - amount: amount - }, this); + event: { + amount: amount + }, + source: this, + target: this + }); } } }; diff --git a/src/server/config/spells/spellArcaneBarrier.js b/src/server/config/spells/spellArcaneBarrier.js index f69d90ae..7b665a9e 100644 --- a/src/server/config/spells/spellArcaneBarrier.js +++ b/src/server/config/spells/spellArcaneBarrier.js @@ -39,7 +39,11 @@ let cpnArcanePatch = { let c = contents[i]; let amount = this.spell.getDamage(c, true); - c.stats.getHp(amount, this.caster); + c.stats.getHp({ + event: amount, + source: this.caster, + target: c + }); } } }; diff --git a/src/server/config/spells/spellHealingCircle.js b/src/server/config/spells/spellHealingCircle.js index a9646695..2f57930b 100644 --- a/src/server/config/spells/spellHealingCircle.js +++ b/src/server/config/spells/spellHealingCircle.js @@ -9,7 +9,11 @@ let cpnHealPatch = { }, applyHeal: function (o, amount) { - o.stats.getHp(amount, this.caster); + o.stats.getHp({ + event: amount, + source: this.caster, + target: o + }); }, collisionEnter: function (o) { diff --git a/src/server/config/spells/spellSingleTargetHeal.js b/src/server/config/spells/spellSingleTargetHeal.js index 8963e108..9d0ed806 100644 --- a/src/server/config/spells/spellSingleTargetHeal.js +++ b/src/server/config/spells/spellSingleTargetHeal.js @@ -17,14 +17,18 @@ module.exports = { const target = action.target; const { x, y } = target; - const amount = this.getDamage(target, true); + const heal = this.getDamage(target, true); + heal.noEvents = this.noEvents; + const event = { - heal: amount, + heal, source: this.obj, target, + spellName: 'singleTargetHeal', spell: this }; - target.stats.getHp(amount, this.obj, event); + + target.stats.getHp(event); const effect = { x, diff --git a/src/server/config/spells/spellSummonConsumableFollower.js b/src/server/config/spells/spellSummonConsumableFollower.js index ba3b8ea1..4fcec1b9 100644 --- a/src/server/config/spells/spellSummonConsumableFollower.js +++ b/src/server/config/spells/spellSummonConsumableFollower.js @@ -96,9 +96,13 @@ module.exports = { mLen--; } else if ((Math.abs(x - m.x) <= 1) && (Math.abs(y - m.y) <= 1)) { m.destroyed = true; - this.obj.stats.getHp({ - amount: obj.stats.values.hpMax / 10 - }, obj); + obj.stats.getHp({ + event: { + amount: obj.stats.values.hpMax / 10 + }, + source: obj, + target: obj + }); obj.instance.syncer.queue('onGetObject', { x: m.x, diff --git a/src/server/fixes/fixes.js b/src/server/fixes/fixes.js index f564b5cf..c546cc3c 100644 --- a/src/server/fixes/fixes.js +++ b/src/server/fixes/fixes.js @@ -1,3 +1,5 @@ +/* eslint-disable max-lines-per-function */ + module.exports = { fixDb: async function () { await io.deleteAsync({ @@ -116,6 +118,34 @@ module.exports = { effect.properties.element = 'poison'; }); + items + .filter(i => i.name === 'Gourdhowl') + .forEach(i => { + const effect = i.effects[0]; + + if (!effect.rolls.castSpell) { + effect.rolls = { + castSpell: { + type: 'whirlwind', + damage: effect.rolls.damage, + range: 1, + statType: 'str', + statMult: 1, + isAttack: true + }, + castTarget: 'none', + chance: effect.rolls.chance, + textTemplate: 'Grants you a ((chance))% chance to cast a ((castSpell.damage)) damage whirlwind on hit', + combatEvent: { + name: 'afterDealDamage', + afterDealDamage: { + spellName: 'melee' + } + } + }; + } + }); + items .filter(f => f.effects?.[0]?.factionId === 'akarei' && !f.effects[0].properties) .forEach(function (i) { diff --git a/src/server/items/generators/effects.js b/src/server/items/generators/effects.js index 497c1a87..0e438d2c 100644 --- a/src/server/items/generators/effects.js +++ b/src/server/items/generators/effects.js @@ -16,7 +16,7 @@ const rollValues = (rollsDefinition, result) => { const isInt = (p.indexOf('i_') === 0); const fieldName = p.replace('i_', ''); - if (!entry.push) { + if (!Array.isArray(entry) || p.indexOf('s_') === 0) { result[fieldName] = range; continue; diff --git a/src/server/mods/class-necromancer/spells/spellBloodBarrier.js b/src/server/mods/class-necromancer/spells/spellBloodBarrier.js index 44573c32..9790955f 100644 --- a/src/server/mods/class-necromancer/spells/spellBloodBarrier.js +++ b/src/server/mods/class-necromancer/spells/spellBloodBarrier.js @@ -45,7 +45,11 @@ module.exports = { amount = amount * this.shieldMultiplier; const heal = { amount }; - target.stats.getHp(heal, obj); + target.stats.getHp({ + event: heal, + source: obj, + target + }); //Only reset the first spell's cooldown if it's an auto attack and not a spell const firstSpell = target.spellbook.spells[0]; diff --git a/src/server/mods/class-necromancer/spells/spellHarvestLife.js b/src/server/mods/class-necromancer/spells/spellHarvestLife.js index b52f3caa..90a5709c 100644 --- a/src/server/mods/class-necromancer/spells/spellHarvestLife.js +++ b/src/server/mods/class-necromancer/spells/spellHarvestLife.js @@ -54,7 +54,11 @@ module.exports = { let healAmount = damage.amount * (this.healPercent / 100); obj.stats.getHp({ - amount: healAmount - }, obj); + event: { + amount: healAmount + }, + source: obj, + target: obj + }); } };