illarion-content/magic/arcane/castSpell.lua

481 lines
17 KiB
Lua
Raw Permalink Blame History

--[[
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 mana = require("magic.arcane.mana")
local range = require("magic.arcane.castingRange")
local castTime = require("magic.arcane.castingSpeed")
local magicGFXSFX = require("magic.arcane.magicGFXSFX")
local targeting = require("magic.arcane.targeting")
local runes = require("magic.arcane.runes")
local teaching = require("magic.arcane.teaching")
local delayedAttack = require("magic.arcane.delayedAttack")
local incantation = require("magic.arcane.incantation")
local diminishingReturns = require("magic.arcane.diminishingReturns")
local skilling = require("magic.arcane.skilling")
local common = require("base.common")
local magic = require("base.magic")
local testing = require("base.testing")
local shared = require("craft.base.shared")
local effectScaling = require("magic.arcane.effectScaling")
local character = require("base.character")
local M = {}
local function statReqMet(user, spell)
local statReq = 0
for _ , rune in pairs(runes.runes) do
if runes.checkSpellForRune(rune.id, spell) then
local newStat = runes.getStatRequirementOfRune(rune.id)
if newStat > statReq then
statReq = newStat
end
end
end
local userStats = user:increaseAttrib("willpower", 0) + user:increaseAttrib("essence", 0) + user:increaseAttrib("intelligence", 0)
if userStats >= statReq then
return true
end
return false
end
local myTexts = {
range = {english = "The target is too far away.", german = "Dein Ziel ist zu weit weg."},
secret = {english = "You secretly begin casting a wind spell.", german = "Du beginnst heimlich einen Windzauber zu wirken."},
mana = {english = "You do not have enough mana.", german = "Du hast nicht genug Mana."},
sight = {english = "Line of sight is not clear.", german = "Du kannst dein Ziel nicht richtig sehen."},
stats = {english = "As you attempt to cast the spell, you feel an abrupt headache prevent you from proceeding. Did something happen to your ability to cast magic?", german = "Als du versuchst den Zauber zu Sprechen, versp<73>rst du pl<70>tzlich heftige Kopfschmerzen die dich daran hindern. Ist etwas mit deiner F<>higkeit zu Zaubern passiert? "}
}
local function skillCheckPassed(user, spell)
local name = skilling.getMagicSkillSpellBelongsTo(spell)
local skillName = user:getSkillName(Character[name])
local skillLevel = user:getSkill(Character[name])
local listOfRunesInSpell = {}
for runeNumber = 1, #runes.runes do
local rune = runes.runeNumberToName(runeNumber)
if runes.checkSpellForRuneByName(rune, spell) then
table.insert(listOfRunesInSpell, runeNumber)
end
end
for _, rune in pairs(listOfRunesInSpell) do
local runeName = runes.runeNumberToName(rune)
local requiredLevel = runes.getLevelRequirementOfRune(rune)
if requiredLevel > skillLevel then
return false, requiredLevel, skillName, runeName
end
end
return true
end
local function checkForWand(user)
local wand = common.getItemInHand(user, magic.wandIds)
if not wand then return true, false end -- Even without a wand you can cast rune spells, but they won't get the multiple advantages using a wand brings to your spells such a targeting, more power and more range
if common.isBroken(wand) then --You dont get the wand bonuses without an intact wand so a warning instead
user:inform("Dein Zauberstab ist zerbrochen. Du solltest ihn reparieren lassen, bevor du versuchst, ihn zu benutzen.", "Your wand is broken. You should see to its repairs before trying to use it.")
return false, false
end
return true, wand
end
local function checksPassed(user, spell, element, thePosition, startedInAttackMode)
local wandCheckPassed = checkForWand(user)
if not wandCheckPassed then
return false
end
if not statReqMet(user, spell) then
--If stats are lowered below the threshhold to learn a rune, whether through a trainer, a potion or something else
user:inform(myTexts.stats.german, myTexts.stats.english)
return false
end
if not range.isTargetInRange(user, spell, element, thePosition) then
user:inform(myTexts.range.german, myTexts.range.english)
return false
end
local passed, level, skillName, runeName = skillCheckPassed(user, spell)
if not passed then
user:inform("Du erf<72>llst nicht die Stufenanforderung, um "..runeName.." mit "..skillName.." zu wirken. Du ben<65>tigst mindestens Stufe "..level..".", "You do not meet the level requirement for casting "..runeName.." using "..skillName..". You need a minimum level of "..level..".")
return false
end
local PEN = runes.checkSpellForRuneByName("PEN", spell)
if not PEN and range.checkForObstacles(user, spell, thePosition) then
user:inform(myTexts.sight.german, myTexts.sight.english)
return false
end
if not mana.checkIfEnoughMana(user, spell, thePosition, startedInAttackMode) then
user:inform(myTexts.mana.german, myTexts.mana.english)
return false
end
return true
end
local function interrupted(player)
local health = character.GetHP(player)
local max_chance = 25 --max percentage chance of interrupting a cast
local healthRoof = 7500 -- chance to interrupt begin at 7500 health
local healthFloor = 2500 -- chance to interrupt reaches max_chance at 2500 health
local willpower = player:increaseAttrib("willpower", 0)
local attribBonus = common.GetAttributeBonusHigh(willpower)
max_chance = math.floor(max_chance / (1+attribBonus)) -- At max attrib bonus for willpower, the max chance becomes 12.5%
local healthRange = healthRoof-healthFloor
local percent = healthRange/100
local actual_chance = max_chance - math.floor(((health-healthFloor)/percent)/(100/max_chance))
if health > healthRoof then
return false
end
if health < healthFloor then
actual_chance = max_chance
end
actual_chance = actual_chance/100
local chance = math.random()
if chance <= actual_chance then
player:inform("Deine Verletzungen machen es dir schwer dich zu konzentrieren. Du machst einen Fehler beim Zaubern!", "Your wounds made it hard for you to concentrate, causing you to make a mistake in your casting.")
return true
end
return false
end
M.interrupted = interrupted
local function overWriteSlot(user, savedTargets, index, newTarget, slotLimit)
local callback = function(dialog)
if not dialog:getSuccess() then
return
end
local input = dialog:getInput()
if not common.IsNilOrEmpty(input) then
local newString = ""
for i = 1, slotLimit do
if i ~= 1 then
newString = newString..","
end
for ti, target in ipairs(savedTargets) do
if ti ~= index and ti == i then
newString = newString..target.name..","..target.id
end
end
if i == index then
newString = newString..input..","..newTarget.id
end
end
ScriptVars:set("heptTargets"..tostring(user.id), newString)
ScriptVars:save()
end
end
local dialog = InputDialog(common.GetNLS(user, "Ziel benennen", "Name Target"), common.GetNLS(user, "Gib den Namen ein, den der Slot f<>r das aktuelle Ziel haben soll.", "Write in what name you want the slot for the current target to have."), false, 255, callback)
user:requestInputDialog(dialog)
end
M.getTargets = targeting.getFriendlistTargets
local function getSlotLimit(user, spell)
local baseSlot = 1
local scaling = effectScaling.getEffectScaling(user, nil, spell)/2
local level = user:getSkill(Character["spiritMagic"])
local levelBonus = math.floor(level/25) --max 4 bonus slots from level
local slots = baseSlot + levelBonus
slots = math.min(10, slots*scaling) --At most 10 slots
slots = math.max(baseSlot, slots) --In case of terrible scaling, at least 1 slot always
return slots
end
M.getSlotLimit = getSlotLimit
local function storeHeptTaurTarget(user, spell, positionsAndTargets)
local PEN = runes.checkSpellForRuneByName("PEN", spell)
local HEPT = runes.checkSpellForRuneByName("HEPT", spell)
local TAUR = runes.checkSpellForRuneByName("TAUR", spell)
if PEN and HEPT then
if not _G.storedPenLevTargets then
_G.storedPenLevTargets = {}
end
_G.storedPenLevTargets[user.id] = nil
end
local newTarget = positionsAndTargets.targets[1]
if not newTarget or not isValidChar(newTarget) or newTarget:getType() ~= Character.player then
return
end
if not (PEN and HEPT and TAUR) then
return
end
local savedTargets = M.getTargets(user) --The list of any targets already saved
local slotLimit = getSlotLimit(user, spell)
local callback = function(dialog)
local success = dialog:getSuccess()
if not success then
return
end
local index = dialog:getSelectedIndex()+1
if index <= slotLimit then
overWriteSlot(user, savedTargets, index, newTarget, slotLimit)
end
end
local dialog = SelectionDialog(common.GetNLS(user,"Gespeicherte Ziele","Stored Targets") , common.GetNLS(user,"W<EFBFBD>hle einen Slot zum <20>berschreiben aus, um dein aktuelles Ziel f<>r die zuk<75>nftige Verwendung von PEN-LEV-Magie zu speichern. Die gespeicherte Liste kann <20>ber dein Grimoire aufgerufen werden.","Select a slot to overwrite to store your current target in for future PEN LEV magic use. The stored list can be accessed via your grimoire.") , callback)
for index, target in ipairs(savedTargets) do
if index <= slotLimit then
dialog:addOption(0, target.name)
end
end
if #savedTargets < slotLimit then
for i = 1, slotLimit-#savedTargets do
dialog:addOption(0, common.GetNLS(user, "Leerer Platz", "Empty Slot"))
end
end
user:requestSelectionDialog(dialog)
end
function M.castSpell(user, spell, actionState, oralCast)
if not oralCast then
incantation.speakIncantation(user, spell)
return
end
if interrupted(user) then
return
end
if not M[user.id] then
M[user.id] = {}
end
local skill = skilling.getMagicSkillSpellBelongsTo(spell)
local level = user:getSkill(Character[skill])
local element = runes.fetchElement(spell)
local CUN = runes.checkSpellForRuneByName("CUN", spell)
local RA = runes.checkSpellForRuneByName("RA", spell)
local SUL = runes.checkSpellForRuneByName("SUL", spell)
local JUS = runes.checkSpellForRuneByName("JUS", spell)
local MES = runes.checkSpellForRuneByName("MES", spell)
local ORL = runes.checkSpellForRuneByName("ORL", spell)
local castSFX = 0
local castSFXDuration = 10
local castGFXDuration = 10
local castGFX = magicGFXSFX.getUserGFXSFX(spell)
if actionState == Action.none then
if _G.stopAutoCast and _G.stopAutoCast[user.id] then
_G.stopAutoCast[user.id] = false
end
local thePosition, targetToTeach, startedInAttackMode = targeting.initPosition(user, spell)
if not thePosition then --rarely happens if you try to cast immediately after an !fr, also happens if you cast a PEN LEV spell after expiry
return
end
M[user.id].thePosition = thePosition
M[user.id].targetToTeach = targetToTeach
M[user.id].startedInAttackMode = startedInAttackMode
common.TurnTo(user, M[user.id].thePosition)
if not checksPassed(user, spell, element, M[user.id].thePosition, startedInAttackMode) then
return
end
if castGFX == 0 then
user:inform(myTexts.secret.german, myTexts.secret.english)
end
local castDuration = castTime.arcaneSpellCastSpeed(user, spell)
M[user.id].storedDuration = castDuration
local foundEffect, paralysis = user.effects:find(23)
if foundEffect then
local foundTime, timeLeft = paralysis:findValue("timeLeft")
if foundTime and timeLeft >= 1 then
M[user.id].extendBy = timeLeft
else
M[user.id].extendBy = 0
end
else
M[user.id].extendBy = 0
end
if not _G.extendedCastingTimeForParalysis then
_G.extendedCastingTimeForParalysis = {}
end
_G.extendedCastingTimeForParalysis[user.id] = 0
user:startAction(castDuration, castGFX, castGFXDuration, castSFX, castSFXDuration)
elseif actionState == Action.abort then
return
elseif actionState == Action.success then
--if paralysed then we extend the cast timer by the time paralysed
if not _G.extendedCastingTimeForParalysis then
_G.extendedCastingTimeForParalysis = {}
end
if M[user.id].extendBy > 0 then
if testing.active then
user:talk(Character.say, "#me's cast is extended by "..(M[user.id].extendBy/10).." seconds.")
end
user:startAction(M[user.id].extendBy, castGFX, castGFXDuration, castSFX, castSFXDuration)
return
elseif _G.extendedCastingTimeForParalysis[user.id] and _G.extendedCastingTimeForParalysis[user.id] > 0 then
if testing.active then
user:talk(Character.say, "#me's cast is extended by "..(_G.extendedCastingTimeForParalysis[user.id]/10).." seconds.")
end
user:startAction(_G.extendedCastingTimeForParalysis[user.id], castGFX, castGFXDuration, castSFX, castSFXDuration)
_G.extendedCastingTimeForParalysis[user.id] = 0
return
end
M[user.id].positionsAndTargets = targeting.getPositionsAndTargets(user, spell, false, false, M[user.id].startedInAttackMode, M[user.id].targetToTeach, M[user.id].thePosition)
if not checksPassed(user, spell, element, M[user.id].thePosition, M[user.id].startedInAttackMode) then
return
end
if not range.isTargetInRange(user, spell, element, M[user.id].thePosition) then
user:inform(myTexts.range.german, myTexts.range.english)
return
end
M[user.id].positionsAndTargets = targeting.addTargets(user, spell, M[user.id].positionsAndTargets)
if not runes.checkSpellForRuneByName("BHONA", spell) then
mana.removedUsedMana(user, spell, M[user.id].thePosition, M[user.id].startedInAttackMode)
local castDuration = M[user.id].storedDuration
skilling.increaseExperience(user, spell, castDuration, M[user.id].positionsAndTargets)
castTime.resetTan(user)
storeHeptTaurTarget(user, spell, M[user.id].positionsAndTargets)
if JUS and ORL then
diminishingReturns.applyOrl(user, M[user.id].thePosition, spell, level)
end
if (SUL and (RA or CUN)) or (JUS and MES) then
delayedAttack.applyDelay(user, M[user.id].thePosition, spell, false, level, M[user.id].positionsAndTargets.startedInAttackMode)
else
delayedAttack.spellEffects(user, M[user.id].positionsAndTargets, spell, element, false, level, castDuration, M[user.id].thePosition)
end
local _, wand = checkForWand(user)
if wand then
shared.toolBreaks(user, wand, castDuration)
end
if not _G.storedPenLevTargets then
_G.storedPenLevTargets = {}
end
_G.storedPenLevTargets[user.id] = nil
if user.attackmode and runes.isSpellAutoCast(spell, wand) and checksPassed(user, spell, element, M[user.id].thePosition, M[user.id].startedInAttackMode) then
-- To mimic wand magic so that the fire magic replacement does not feel like a downgrade, we allow auto casting of some spells
if _G.stopAutoCast and _G.stopAutoCast[user.id] then
_G.stopAutoCast[user.id] = false
return
end
castDuration = castTime.arcaneSpellCastSpeed(user, spell)
M[user.id].storedDuration = castDuration
if interrupted(user) then
return
end
user:startAction(castDuration, castGFX, castGFXDuration, castSFX, castSFXDuration)
end
else
teaching.teachRune(user, M[user.id].positionsAndTargets, spell)
end
end
end
return M