2023-07-07 15:12:32 +02:00
|
|
|
|
--[[
|
|
|
|
|
|
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 restTime = {}
|
|
|
|
|
|
local storedMessage = {}
|
|
|
|
|
|
local storedPosition = {}
|
|
|
|
|
|
local M = {}
|
|
|
|
|
|
|
|
|
|
|
|
-- Called by the server when user:learn(...) is issued by a script
|
|
|
|
|
|
|
|
|
|
|
|
--[[Call: Character:learn(skill, actionPoints, learnLimit);
|
|
|
|
|
|
|
|
|
|
|
|
skill: Name of the skill as skill key, e.g. Character.mining.
|
|
|
|
|
|
actionPoints: The amount of actionPoints or time (1/10s), required by the action, as integer.
|
|
|
|
|
|
Do NOT fill in 0, every action relevant for skillgain HAS TO take some time.
|
|
|
|
|
|
learnLimit: Maximum skill level the user can reach with the triggering action.]]
|
|
|
|
|
|
|
|
|
|
|
|
--Learning speed - Change here if you're unhappy with the learning speed. Skillgain scales in a linear way.
|
|
|
|
|
|
local scalingFactor = 500 --Here, you can mod the learning speed. Higher value=faster.
|
|
|
|
|
|
--It is the minor skill increase for an action with skill = 0 and all other parameters nominal as per definitions below.
|
|
|
|
|
|
--10000 minor skill points make a major skill point.
|
|
|
|
|
|
|
|
|
|
|
|
--Constants - Do not change unless you know exactly what you're doing!
|
|
|
|
|
|
local amplification = 100 --An 'abritrary' value that governs the 'resolution' of the MC function.
|
|
|
|
|
|
local damping = 0.0001 --This value determines how fast the MC system reacts on changed player behaviour.
|
|
|
|
|
|
local lowerBorder = 2.5 * amplification / damping --below 2.5 % of time spent online, no additional bonus is granted.
|
|
|
|
|
|
local normalMC = 4 * lowerBorder --A 'normal' player invests 4x the time (=10 %) into skill related actions.
|
|
|
|
|
|
local normalAP = 50 --How many actionPoints does a 'normal' action take? Default=50.
|
|
|
|
|
|
--Constants - end
|
|
|
|
|
|
|
2025-05-19 22:20:38 +02:00
|
|
|
|
local multiSkilling = {}
|
|
|
|
|
|
|
|
|
|
|
|
local function accountForMultiSkills(user, skill, actionPoints)
|
|
|
|
|
|
|
|
|
|
|
|
if not multiSkilling[user.id] then
|
|
|
|
|
|
multiSkilling[user.id] = {}
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
-- The purpose of this script is to replace the former division of actionPoints by 3 in fighting that accounted for how you
|
|
|
|
|
|
-- might be skilling parry ,armour and weapon at the same time, with a dynamic scaling and to include how you can now also
|
|
|
|
|
|
-- be skilling magicResist at the same time
|
|
|
|
|
|
|
|
|
|
|
|
local armour = {Character.heavyArmour, Character.mediumArmour, Character.lightArmour}
|
|
|
|
|
|
|
|
|
|
|
|
local parry = Character.parry
|
|
|
|
|
|
|
|
|
|
|
|
local offensive = {Character.distanceWeapons, Character.slashingWeapons, Character.wrestling, Character.concussionWeapons, Character.punctureWeapons, Character.fireMagic, Character.spiritMagic, Character.windMagic, Character.earthMagic, Character.waterMagic}
|
|
|
|
|
|
|
|
|
|
|
|
local magicResist = Character.magicResistance
|
|
|
|
|
|
|
|
|
|
|
|
local correctSkill = false
|
|
|
|
|
|
|
2025-08-30 14:52:55 +02:00
|
|
|
|
local resetTimer = 5
|
|
|
|
|
|
|
2025-05-19 22:20:38 +02:00
|
|
|
|
for _, armourSkill in pairs(armour) do
|
|
|
|
|
|
if skill == armourSkill then
|
|
|
|
|
|
correctSkill = true
|
2025-08-30 14:52:55 +02:00
|
|
|
|
multiSkilling[user.id]["armour"] = world:getTime("unix") + resetTimer
|
2025-05-19 22:20:38 +02:00
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
if skill == parry then
|
|
|
|
|
|
correctSkill = true
|
2025-08-30 14:52:55 +02:00
|
|
|
|
multiSkilling[user.id]["parry"] = world:getTime("unix") + resetTimer
|
2025-05-19 22:20:38 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
if skill == magicResist then
|
|
|
|
|
|
correctSkill = true
|
2025-08-30 14:52:55 +02:00
|
|
|
|
multiSkilling[user.id]["magicResist"] = world:getTime("unix") + resetTimer
|
2025-05-19 22:20:38 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
for _, offensiveSkill in pairs(offensive) do
|
|
|
|
|
|
if skill == offensiveSkill then
|
|
|
|
|
|
correctSkill = true
|
2025-08-30 14:52:55 +02:00
|
|
|
|
multiSkilling[user.id]["offensive"] = world:getTime("unix") + resetTimer
|
2025-05-19 22:20:38 +02:00
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
if not correctSkill then
|
|
|
|
|
|
return actionPoints
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
local divideBy = 0
|
|
|
|
|
|
|
|
|
|
|
|
local timesToCheck = {"offensive", "magicResist", "parry", "armour"}
|
|
|
|
|
|
|
|
|
|
|
|
for _, timeCheck in pairs(timesToCheck) do
|
|
|
|
|
|
local time = multiSkilling[user.id][timeCheck]
|
|
|
|
|
|
|
|
|
|
|
|
if not common.IsNilOrEmpty(time) then
|
2025-08-30 14:52:55 +02:00
|
|
|
|
if time > world:getTime("unix") then --This skill category was learnt within that last 5 seconds
|
2025-05-19 22:20:38 +02:00
|
|
|
|
divideBy = divideBy + 1
|
|
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
return actionPoints/divideBy
|
|
|
|
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
2026-03-09 21:05:14 +01:00
|
|
|
|
local accumulatedExperience = {}
|
2025-05-19 22:20:38 +02:00
|
|
|
|
|
2023-07-07 15:12:32 +02:00
|
|
|
|
function M.learn(user, skill, actionPoints, learnLimit)
|
|
|
|
|
|
|
|
|
|
|
|
local leadAttrib = common.GetLeadAttrib(user, skill) --reading the lead attribute.
|
|
|
|
|
|
local skillValue = user:getSkill(skill) --reading the skill points.
|
|
|
|
|
|
local minorSkill = user:getMinorSkill(skill) --reading the minor skill points; 10000=1 skill point.
|
|
|
|
|
|
local MCvalue = math.max(lowerBorder, user:getMentalCapacity())
|
|
|
|
|
|
--below 2.5% of time spent online, no additional bonus is granted.
|
|
|
|
|
|
|
|
|
|
|
|
M.resetRestTime(user)
|
|
|
|
|
|
|
|
|
|
|
|
if skillValue < learnLimit and skillValue < 100 then
|
2025-11-09 12:18:41 +01:00
|
|
|
|
actionPoints = accountForMultiSkills(user, skill, actionPoints) --After the check to ensure level 100 skills dont add to it
|
2023-07-07 15:12:32 +02:00
|
|
|
|
--you only learn when your skill is lower than the skill of the learnLimit and your skill is <100.
|
|
|
|
|
|
|
|
|
|
|
|
local skillFactor = (100 - skillValue)/100
|
|
|
|
|
|
local MCfactor = normalMC / math.max(MCvalue, 1)
|
|
|
|
|
|
--10 % of time spent online is considered "normal" -> MCfactor is 1.
|
|
|
|
|
|
local attributeBonus = common.GetAttributeBonusHigh(leadAttrib)
|
|
|
|
|
|
--+/-0.5 depending on attribute, limited to 1.
|
|
|
|
|
|
local intelligenceBonus = common.GetAttributeBonusLow(user:increaseAttrib("intelligence", 0))
|
|
|
|
|
|
--+/-0.1, depending on attribute, limited to 0.2.
|
|
|
|
|
|
local actionpointFactor = actionPoints / normalAP
|
|
|
|
|
|
--An action with 50 AP is "normal".
|
|
|
|
|
|
|
|
|
|
|
|
local minorIncrease = scalingFactor * skillFactor * MCfactor * actionpointFactor * (1 + attributeBonus + intelligenceBonus)
|
|
|
|
|
|
|
2026-03-09 21:05:14 +01:00
|
|
|
|
if not accumulatedExperience[user.id] then
|
|
|
|
|
|
accumulatedExperience[user.id] = {}
|
2023-07-07 15:12:32 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
2026-03-09 21:05:14 +01:00
|
|
|
|
if accumulatedExperience[user.id][skill] then
|
|
|
|
|
|
minorIncrease = minorIncrease + accumulatedExperience[user.id][skill]
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
local excess = minorIncrease-math.floor(minorIncrease)
|
|
|
|
|
|
-- Skill points only work in whole integers, so we store the decimal for later instead of relying on RNG
|
|
|
|
|
|
accumulatedExperience[user.id][skill] = excess
|
|
|
|
|
|
|
|
|
|
|
|
minorIncrease = math.floor(minorIncrease) --The whole integer we will increase by
|
|
|
|
|
|
|
2023-07-07 15:12:32 +02:00
|
|
|
|
--For debugging, use the following line.
|
|
|
|
|
|
--[[user:inform("Skill="..user:getSkillName(skill)..", actionPoints="..actionPoints..", skillFactor="
|
|
|
|
|
|
..skillFactor..", MCfactor="..MCfactor..", attributeBonus="..attributeBonus..", intelligenceBonus="
|
|
|
|
|
|
..intelligenceBonus..", actionpointFactor="..actionpointFactor..", minorIncrease="..minorIncrease..".")
|
|
|
|
|
|
]]
|
|
|
|
|
|
|
|
|
|
|
|
while minorIncrease > 0 do --for the rare case that an action results in two level ups, we have this loop.
|
|
|
|
|
|
|
|
|
|
|
|
local realIncrease = math.min(minorIncrease, 10000) --to prevent overflow.
|
|
|
|
|
|
|
|
|
|
|
|
if minorSkill + realIncrease < 10000 then
|
|
|
|
|
|
user:increaseMinorSkill(skill, realIncrease)
|
|
|
|
|
|
|
|
|
|
|
|
else --Level up!
|
|
|
|
|
|
|
|
|
|
|
|
user:increaseMinorSkill(skill, realIncrease) --Increase the skill.
|
|
|
|
|
|
local skillstring = user:getSkillName(skill)
|
|
|
|
|
|
|
|
|
|
|
|
if user:getType() == 0 and user:getQuestProgress(154) == 0 and user:getQuestProgress(325) == 1 then --Only players get an inform once.
|
|
|
|
|
|
|
|
|
|
|
|
local callbackSkill = function() end --empty callback.
|
|
|
|
|
|
local dialogTitle = common.GetNLS(user,"Levelaufstieg", "Level up")
|
|
|
|
|
|
local dialogText = common.GetNLS(user,
|
|
|
|
|
|
"Deine Fertigkeit '"..skillstring.."' hat sich soeben erh<72>ht. Dr<44>cke 'C' um deine \z
|
|
|
|
|
|
Fertigkeiten anzeigen zu lassen. Das F<EFBFBD>higkeitensystem Illarions basiert auf \z
|
|
|
|
|
|
'Learning by doing'. Dein Charakter wird also besser in dem, was er tut. Dein Charakter \z
|
|
|
|
|
|
kann mit der Zeit von einem Anf<EFBFBD>nger im Handwerk zu einem Meister werden, wodurch er dann \z
|
|
|
|
|
|
eine gr<EFBFBD><EFBFBD>ere Vielfalt von Gegenst<EFBFBD>nden besserer Qualit<EFBFBD>t herstellen kann.\n\nJe mehr \z
|
|
|
|
|
|
Handlungen du pro Zeit ausf<EFBFBD>hrst desto weniger lernst du von jeder Handlung. Unter dem \z
|
|
|
|
|
|
Strich lernt aber jeder Charakter gleich viel <EFBFBD>ber die Zeit. Du kannst also so viel Zeit in \z
|
|
|
|
|
|
Rollenspiel oder Training stecken wie du m<EFBFBD>chtest, ohne dass eine Art Illarion zu spielen \z
|
|
|
|
|
|
bevorzugt wird.",
|
|
|
|
|
|
"Your skill '"..skillstring.."' just increased. Hit 'C' to review your skills. Illarion's \z
|
|
|
|
|
|
skill system is based on 'learning by doing'. As your character practices certain actions, \z
|
|
|
|
|
|
they begin to improve. Over time your character may develop from an apprentice crafter to a \z
|
|
|
|
|
|
master, allowing him/her to create a larger variety of items at a higher quality.\n\nThe more \z
|
|
|
|
|
|
actions you do over time, the less you learn from each action. This way, it is on you how \z
|
|
|
|
|
|
much time you want to invest on training your skills, the result after a given time will be \z
|
|
|
|
|
|
the same. So you can roleplay or train as much as you want without one style of playing being \z
|
|
|
|
|
|
favoured by the game.")
|
|
|
|
|
|
local dialogSkill = MessageDialog(dialogTitle, dialogText, callbackSkill)
|
|
|
|
|
|
user:requestMessageDialog(dialogSkill)
|
|
|
|
|
|
user:setQuestProgress(154, 1) --Remember that we already informed the player.
|
|
|
|
|
|
|
|
|
|
|
|
elseif user:getType() == 0 then
|
|
|
|
|
|
|
|
|
|
|
|
common.TempInformNLS(user, skillstring.." +1", skillstring.." +1")
|
|
|
|
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
minorIncrease = minorIncrease - 10000
|
|
|
|
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
user:increaseMentalCapacity(amplification * actionPoints)
|
|
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
-- Invoked every 10 s on every user; to be used to reduce MC on a regular basis.
|
|
|
|
|
|
-- user:idleTime() can be used to get the number of seconds a user has been idle to check for inactivity.
|
|
|
|
|
|
|
|
|
|
|
|
function M.reduceMC(user)
|
|
|
|
|
|
|
|
|
|
|
|
M.checkRest(user)
|
|
|
|
|
|
|
|
|
|
|
|
if user:idleTime() < 300 and M.getRestTime(user) < 300 then
|
|
|
|
|
|
--Has the user done any action within the last five minutes?
|
|
|
|
|
|
|
|
|
|
|
|
user:increaseMentalCapacity(-1 * math.floor(user:getMentalCapacity() * damping + 0.5))
|
|
|
|
|
|
--reduce MC-points by 0.01 %, rounded correctly.
|
|
|
|
|
|
local currentMC = user:getMentalCapacity()
|
|
|
|
|
|
|
|
|
|
|
|
if currentMC < ((0.5/damping)-1) then --Mental Capacity cannot drop below 4999 -> Bugged player or cheater.
|
|
|
|
|
|
user:inform("Invalid mental capacity value found: "..currentMC..". Please inform a developer.")
|
|
|
|
|
|
user:increaseMentalCapacity(normalMC-currentMC) --Reset to default value.
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
--For debugging, use the following line.
|
|
|
|
|
|
--[[user:inform("MC="..user:getMentalCapacity()..", idleTime="..user:idleTime()..", MCfactor="..normalMC /
|
|
|
|
|
|
math.max(lowerBorder, user:getMentalCapacity())..", Counter="..M.getRestTime(user)..".")]]
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function M.getRestTime(user)
|
|
|
|
|
|
|
|
|
|
|
|
if not restTime[user.id] then
|
|
|
|
|
|
restTime[user.id] = 0
|
|
|
|
|
|
end
|
|
|
|
|
|
return restTime[user.id]
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function M.increaseRestTime(user)
|
|
|
|
|
|
if not restTime[user.id] then
|
|
|
|
|
|
restTime[user.id] = 1
|
|
|
|
|
|
else
|
|
|
|
|
|
restTime[user.id] = restTime[user.id] + 1
|
|
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function M.resetRestTime(user)
|
|
|
|
|
|
restTime[user.id] = 0
|
|
|
|
|
|
|
|
|
|
|
|
local activityTrackerEffect = 84
|
|
|
|
|
|
local foundEffect, myEffect = user.effects:find(activityTrackerEffect)
|
|
|
|
|
|
|
|
|
|
|
|
if foundEffect then
|
|
|
|
|
|
myEffect:addValue("lastResetRestTime", world:getTime("unix"))
|
|
|
|
|
|
end
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function M.checkRest(user)
|
|
|
|
|
|
|
|
|
|
|
|
if not storedMessage[user.id] then
|
|
|
|
|
|
storedMessage[user.id] = user.lastSpokenText
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
if not storedPosition[user.id] then
|
|
|
|
|
|
storedPosition[user.id] = position(user.pos.x,user.pos.y,user.pos.z)
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
if storedMessage[user.id] == user.lastSpokenText and common.isInRect(user.pos, storedPosition[user.id], 3) and user:idleTime() < 300 then
|
|
|
|
|
|
|
|
|
|
|
|
M.increaseRestTime(user)
|
|
|
|
|
|
|
|
|
|
|
|
elseif storedMessage[user.id] ~= user.lastSpokenText or not common.isInRect(user.pos, storedPosition[user.id], 3) then
|
|
|
|
|
|
|
|
|
|
|
|
M.resetRestTime(user)
|
|
|
|
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
storedMessage[user.id] = user.lastSpokenText
|
|
|
|
|
|
storedPosition[user.id] = position(user.pos.x,user.pos.y,user.pos.z)
|
|
|
|
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
return M
|