illarion-content/magic/arcane/magicDamage.lua
2026-04-20 17:23:55 +02:00

366 lines
12 KiB
Lua

--[[
Illarion Server
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option) any
later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License along
with this program. If not, see <http://www.gnu.org/licenses/>.
]]
local common = require("base.common")
local magic = require("base.magic")
local MR = require("magic.arcane.magicResistance")
local MP = require("magic.arcane.magicPenetration")
local runes = require("magic.arcane.runes")
local M = {}
local damageList = {
{rune = "RA", damage = 500, element = "Fire"},
{rune = "IRA", damage = 200, element = "Fire"}, --With IRA the damage is converted to mana damage instead of health
{rune = "KAH", damage = 200, element = "Fire"}, --With IRA the damage is converted to mana damage instead of health
{rune = "PHERC", damage = 400, element = "Water"},
{rune = "PHERC", damage = 800, element = "Air"}, -- As the only damaging rune for wind spells, 800 puts the damage right below RA in terms of DPS as fire spells are still meant to be the primary source of direct damage
{rune = "PHERC", damage = 800, element = "Earth"}, -- As the only damaging rune for earth spells, 800 puts the damage right below RA in terms of DPS as fire spells are still meant to be the primary source of direct damage
{rune = "QWAN", damage = 600, element = "Fire"},
{rune = "LUK", damage = 400, element = "Fire"},
{rune = "TAUR", damage = 400, element = "Fire"},
{rune = "URA", damage = 400, element = "Fire"},
{rune = "YEG", damage = 400, element = "Fire"}
}
local damageOverTimeList = { --This damage is balanced around DoTs having 15 ticks that it is spread out over. So you would want to start with a DoT before moving onto direct damage spells when fighting high level opponents
{rune = "PHERC", damage = 500, element = "Fire"},
{rune = "IRA", damage = 375, element = "Water"}, --Damage gets converted to mana damage
{rune = "KAH", damage = 375, element = "Water"}, --Damage gets converted to stamina damage
{rune = "CUN", damage = 600, element = "Water"},
{rune = "QWAN", damage = 900, element = "Water"},
{rune = "TAUR", damage = 600, element = "Water"},
{rune = "URA", damage = 600, element = "Water"},
{rune = "YEG", damage = 600, element = "Water"}
}
M.raceList = {
{race = Character.human, typeOf = "sentient"},
{race = Character.dwarf, typeOf = "sentient"},
{race = Character.halfling, typeOf = "sentient"},
{race = Character.elf, typeOf = "sentient"},
{race = Character.orc, typeOf = "sentient"},
{race = Character.lizardman, typeOf = "sentient"},
{race = Character.forestTroll, typeOf = "sentient"},
{race = Character.mummy, typeOf = "undead"},
{race = Character.skeleton, typeOf = "undead"},
{race = Character.floatingEye, typeOf = "sentient"},
{race = Character.sheep, typeOf = "animal"},
{race = Character.spider, typeOf = "animal"},
{race = Character.pig, typeOf = "animal"},
{race = Character.wasp, typeOf = "animal"},
{race = Character.golem, typeOf = "sentient"},
{race = Character.cow, typeOf = "animal"},
{race = Character.wolf, typeOf = "animal"},
{race = Character.bear, typeOf = "animal"},
{race = Character.raptor, typeOf = "animal"},
{race = Character.zombie, typeOf = "undead"},
{race = Character.hellhound, typeOf = "animal"},
{race = Character.imp, typeOf = "sentient"},
{race = Character.ironGolem, typeOf = "sentient"},
{race = Character.ratman, typeOf = "sentient"},
{race = Character.dog, typeOf = "animal"},
{race = Character.beetle, typeOf = "animal"},
{race = Character.fox, typeOf = "animal"},
{race = Character.slime, typeOf = "animal"},
{race = Character.chicken, typeOf = "animal"},
{race = Character.boneDragon, typeOf = "undead"},
{race = Character.rat, typeOf = "animal"},
{race = Character.fleshDragon, typeOf = "animal"},
{race = Character.rabbit, typeOf = "animal"},
{race = Character.akaltut, typeOf = "sentient"},
{race = Character.fairy, typeOf = "sentient"},
{race = Character.deer, typeOf = "animal"},
{race = Character.ettin, typeOf = "sentient"}
}
local function CheckIftargetPriest(target)
if target:getMagicType() == 1 then
return true
end
if string.find(target.name, "priest") then
--it is a priest mob
return true
end
return false
end
local function CheckIftargetType(target, targetType)
if not isValidChar(target) then
return false
end
local targetRace = target:getRace()
for _, race in pairs(M.raceList) do
if race.race == targetRace then
if race.typeOf == targetType then
return true
end
end
end
return false
end
function M.checkIfRaceBonus(target, rune)
local targetType
if rune == "TAUR" then
targetType = "sentient"
elseif rune == "URA" then
targetType = "animal"
elseif rune == "YEG" then
targetType = "undead"
end
return CheckIftargetType(target, targetType)
end
local function checkForDamageRunes(target, spell, element, DoT, earthTrap)
local RA = runes.checkSpellForRuneByName("RA", spell)
local damage = 0
local list
if DoT then
list = damageOverTimeList
else
list = damageList
end
for _, rune in pairs(list) do
if rune.element == element and runes.checkSpellForRuneByName(rune.rune, spell) then
if rune.rune == "LUK" and RA then
if CheckIftargetPriest(target) then
damage = damage + rune.damage
end
elseif rune.rune == "TAUR" then
if CheckIftargetType(target, "sentient") then
damage = damage + rune.damage
end
elseif rune.rune == "URA" then
if CheckIftargetType(target, "animal") then
damage = damage + rune.damage
end
elseif rune.rune == "YEG" then
if CheckIftargetType(target, "undead") then
damage = damage + rune.damage
end
else
damage = damage + rune.damage
end
end
end
return damage
end
local function hasDamageRuneOfSize(spell, cost, element, DoT)
local list = damageList
if DoT then
list = damageOverTimeList
end
for _, rune in pairs(list) do
if rune.element == element then
for _, rune2 in pairs(runes.runes) do
if rune2.name == rune.name then
if rune.cost == cost then
return true
end
end
end
end
end
return false
end
function M.getEquipmentImpact(user, elementBonus)
return magic.getQualityBonusWand(user) + elementBonus
end
function M.intImpact(user)
local intelligence = user:increaseAttrib("intelligence", 0)
local statBonus = common.GetAttributeBonusHigh(intelligence)
return statBonus
end
function M.crit(user, value)
local perception = user:increaseAttrib("perception", 0)
local statBonus = common.GetAttributeBonusHigh(perception)
local rand = math.random()
if statBonus >= rand then
return value*1.5 --If you crit, 50% bonus
end
return value
end
function M.resistanceAndPenetrationImpact(resist, penetration, damage, spell)
if damage < 0 then
return 0
end
if resist > 0 and spell then
local SAV = runes.checkSpellForRuneByName("SAV", spell)
if SAV then
resist = resist*0.9
end
end
return math.max(0, damage*(1- resist + penetration)) -- never return negative values
end
function M.getMagicDamage(user, spell, element, target, DoT, ORL, earthTrap, castDuration)
if not isValidChar(target) then
return false
end
local baseDamage = checkForDamageRunes(target, spell, element, DoT)
local magicResist
local magicPen
local elementBonus
local wandGemBonus
local cloakGemBonus = magic.getGemBonusCloak(target)/100 -- tier 5 set is 30%, tier 10 60%
if not earthTrap then
magicPen = MP.getMagicPenetration(user, element, spell)
elementBonus = magic.checkElementBonus(user, element)
wandGemBonus = magic.getGemBonusWand(user)/100 -- tier 5 set is 30%, tier 10 60%
else
magicPen = earthTrap:getData("magicPenetration")
elementBonus = earthTrap:getData("elementBonus")
wandGemBonus = earthTrap:getData("gemBonus")
end
local damage = baseDamage*(1+M.intImpact(user)+M.getEquipmentImpact(user, elementBonus))
damage = M.crit(user, damage) -- Critical strike chance based on perception stat and rng, 50% increase in damage if crit
local finalDamage = 0
local illusion = runes.checkSpellForRuneByName("LHOR", spell)
local playerOrMonster = target:getType()
if playerOrMonster ~= Character.npc and not illusion then
magicResist = MR.getMagicResistance(target)
finalDamage = M.resistanceAndPenetrationImpact(magicResist, magicPen, damage, spell)
end
if runes.checkSpellForRuneByName("SUL",spell) then
finalDamage = finalDamage*1.5 -- 50% damage increase
end
local TAUR = runes.checkSpellForRuneByName("TAUR", spell)
local URA = runes.checkSpellForRuneByName("URA", spell)
local YEG = runes.checkSpellForRuneByName("YEG", spell)
local JUS = runes.checkSpellForRuneByName("JUS", spell)
local rune
if TAUR then
rune = "TAUR"
elseif URA then
rune = "URA"
elseif YEG then
rune = "YEG"
end
local raceBonus
if rune then
raceBonus = M.checkIfRaceBonus(target, rune)
end
if raceBonus and JUS then
finalDamage = finalDamage*2
end
if ORL and JUS then
finalDamage = finalDamage/2
end
-- Lastly we check magic gems that, just like in fighting, increase/decrease the final damage by a % after cancelling each other out
finalDamage = finalDamage * (1 + wandGemBonus-cloakGemBonus)
local maxDamage = 10000 -- A full health bar, no damage cap in PvE
local castTime = castDuration
if user and user:getType() == Character.player and target and target:getType() == Character.player then
local hasMediumDamageRune = hasDamageRuneOfSize(spell, "Medium", element, DoT)
local hasLargeDamageRune = hasDamageRuneOfSize(spell, "Large", element, DoT)
maxDamage = 3000
if hasMediumDamageRune and hasLargeDamageRune then -- The below ensures that higher level damage runes will still always have an advantage while also no chance of oneshotting players with a 7-second duration spell cast
maxDamage = 4999
elseif hasMediumDamageRune then
maxDamage = 3888
elseif hasLargeDamageRune then
maxDamage = 4444
end
-- both user and target exist and both are players, this is a pvp battle
local secondsToKillInPvP = 7 -- in testing: 5 attacks with RA(7.5 seconds cast time), 4 to make them fall down then a last to finish them off
local maxHealth = 10000
local maxDamagePerSecond = maxHealth/secondsToKillInPvP
maxDamage = math.min(maxDamage, (castTime/10*maxDamagePerSecond)) --This only impacts cases where there is an extreme power difference in pvp
end
if finalDamage > maxDamage then
finalDamage = maxDamage
end
finalDamage = math.floor(finalDamage)
finalDamage = math.max(0, finalDamage) -- just in case it ends up healing someone because the damage went into the negatives
return finalDamage
end
return M