diff --git a/CMakeLists.txt b/CMakeLists.txt index 34c8a2f3de..b5af12d187 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,7 +82,7 @@ message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) set(OPENMW_VERSION_MINOR 51) set(OPENMW_VERSION_RELEASE 0) -set(OPENMW_LUA_API_REVISION 102) +set(OPENMW_LUA_API_REVISION 103) set(OPENMW_POSTPROCESSING_API_REVISION 3) set(OPENMW_VERSION_COMMITHASH "") diff --git a/docs/source/luadoc_data_paths.sh b/docs/source/luadoc_data_paths.sh index 6ad2ec90cc..765ccf274a 100755 --- a/docs/source/luadoc_data_paths.sh +++ b/docs/source/luadoc_data_paths.sh @@ -2,7 +2,7 @@ paths=( openmw_aux/*lua scripts/omw/activationhandlers.lua scripts/omw/ai.lua - scripts/omw/combat/local.lua + scripts/omw/combat/interface.lua scripts/omw/input/playercontrols.lua scripts/omw/mechanics/animationcontroller.lua scripts/omw/input/gamepadcontrols.lua diff --git a/docs/source/reference/lua-scripting/interface_combat.rst b/docs/source/reference/lua-scripting/interface_combat.rst index e3175ea27c..617cd9dbdf 100644 --- a/docs/source/reference/lua-scripting/interface_combat.rst +++ b/docs/source/reference/lua-scripting/interface_combat.rst @@ -4,5 +4,5 @@ Interface Combat .. include:: version.rst .. raw:: html - :file: generated_html/scripts_omw_combat_local.html + :file: generated_html/scripts_omw_combat_interface.html diff --git a/files/data-mw/CMakeLists.txt b/files/data-mw/CMakeLists.txt index 48e818cfb1..fd36d6d314 100644 --- a/files/data-mw/CMakeLists.txt +++ b/files/data-mw/CMakeLists.txt @@ -20,8 +20,29 @@ set(BUILTIN_DATA_MW_FILES # Game-specific settings for calendar.lua openmw_aux/calendarconfig.lua + + scripts/omw/cellhandlers.lua + scripts/omw/combat/common.lua + scripts/omw/combat/global.lua + scripts/omw/combat/local.lua + scripts/omw/combat/menu.lua + scripts/omw/music/helpers.lua + scripts/omw/music/music.lua + scripts/omw/music/settings.lua + scripts/omw/playerskillhandlers.lua ) foreach (f ${BUILTIN_DATA_MW_FILES}) copy_resource_file("${CMAKE_CURRENT_SOURCE_DIR}/${f}" "${OPENMW_RESOURCES_ROOT}" "resources/vfs-mw/${f}") endforeach (f) + +# Concat data/builtin.omwscripts and data-mw/builtin.omwscripts.in to create vfs-mw/builtin.omwscripts +set(builtinBase "${CMAKE_CURRENT_SOURCE_DIR}/../data/builtin.omwscripts") + +# https://gitlab.kitware.com/cmake/cmake/-/issues/20181 +if (NOT CMAKE_GENERATOR MATCHES "Ninja") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${builtinBase}") +endif() + +file(READ "${builtinBase}" BUILTIN_SCRIPTS) +configure_resource_file("${CMAKE_CURRENT_SOURCE_DIR}/builtin.omwscripts.in" "${OPENMW_RESOURCES_ROOT}" "resources/vfs-mw/builtin.omwscripts") diff --git a/files/data-mw/builtin.omwscripts.in b/files/data-mw/builtin.omwscripts.in new file mode 100644 index 0000000000..ffcadbf69f --- /dev/null +++ b/files/data-mw/builtin.omwscripts.in @@ -0,0 +1,10 @@ +@BUILTIN_SCRIPTS@ + +# Game specific scripts to append to builtin.omwscripts +GLOBAL: scripts/omw/cellhandlers.lua +GLOBAL: scripts/omw/combat/global.lua +MENU: scripts/omw/combat/menu.lua +NPC,CREATURE,PLAYER: scripts/omw/combat/local.lua +PLAYER: scripts/omw/music/music.lua +MENU: scripts/omw/music/settings.lua +PLAYER: scripts/omw/playerskillhandlers.lua diff --git a/files/data/scripts/omw/cellhandlers.lua b/files/data-mw/scripts/omw/cellhandlers.lua similarity index 100% rename from files/data/scripts/omw/cellhandlers.lua rename to files/data-mw/scripts/omw/cellhandlers.lua diff --git a/files/data/scripts/omw/combat/common.lua b/files/data-mw/scripts/omw/combat/common.lua similarity index 100% rename from files/data/scripts/omw/combat/common.lua rename to files/data-mw/scripts/omw/combat/common.lua diff --git a/files/data/scripts/omw/combat/global.lua b/files/data-mw/scripts/omw/combat/global.lua similarity index 100% rename from files/data/scripts/omw/combat/global.lua rename to files/data-mw/scripts/omw/combat/global.lua diff --git a/files/data/scripts/omw/combat/local.lua b/files/data-mw/scripts/omw/combat/local.lua similarity index 55% rename from files/data/scripts/omw/combat/local.lua rename to files/data-mw/scripts/omw/combat/local.lua index a046614986..77f43e2901 100644 --- a/files/data/scripts/omw/combat/local.lua +++ b/files/data-mw/scripts/omw/combat/local.lua @@ -1,17 +1,13 @@ -local animation = require('openmw.animation') -local async = require('openmw.async') local core = require('openmw.core') local I = require('openmw.interfaces') local self = require('openmw.self') local storage = require('openmw.storage') local types = require('openmw.types') -local util = require('openmw.util') -local auxUtil = require('openmw_aux.util') local Actor = types.Actor -local Weapon = types.Weapon local Player = types.Player local Creature = types.Creature local Armor = types.Armor +local auxUtil = require('openmw_aux.util') local isPlayer = Player.objectIsInstance(self) local godMode = function() return false end @@ -20,8 +16,6 @@ if isPlayer then godMode = function() return require('openmw.debug').isGodMode() end end -local onHitHandlers = {} - local settings = storage.globalSection('SettingsOMWCombat') local function getSkill(actor, skillId) @@ -271,16 +265,13 @@ local function spawnBloodEffect(position) end local function onHit(data) - if auxUtil.callEventHandlers(onHitHandlers, data) then - return - end if data.successful and not godMode() then I.Combat.applyArmor(data) I.Combat.adjustDamageForDifficulty(data) if getDamage(data, 'health') > 0 then core.sound.playSound3d('Health Damage', self) if data.hitPos then - spawnBloodEffect(data.hitPos) + I.Combat.spawnBloodEffect(data.hitPos) end end elseif data.attacker and Player.objectIsInstance(data.attacker) then @@ -289,142 +280,20 @@ local function onHit(data) Actor._onHit(self, data) end ---- --- Table of possible attack source types --- @type AttackSourceType --- @field #string Magic --- @field #string Melee --- @field #string Ranged --- @field #string Unspecified +I.Combat.addOnHitHandler(onHit) + +local interface = auxUtil.shallowCopy(I.Combat) +interface.adjustDamageForArmor = function(damage, actor) return adjustDamageForArmor(damage, actor or self) end +interface.adjustDamageForDifficulty = function(attack, defendant) return adjustDamageForDifficulty(attack, defendant or self) end +interface.applyArmor = applyArmor +interface.getArmorRating = function(actor) return getArmorRating(actor or self) end +interface.getArmorSkill = getArmorSkill +interface.getSkillAdjustedArmorRating = function(item, actor) return getSkillAdjustedArmorRating(item, actor or self) end +interface.getEffectiveArmorRating = function(item, actor) return getEffectiveArmorRating(item, actor or self) end +interface.spawnBloodEffect = spawnBloodEffect +interface.pickRandomArmor = function(actor) return pickRandomArmor(actor or self) end ---- --- @type AttackInfo --- @field [parent=#AttackInfo] #table damage A table mapping a stat name (health, fatigue, or magicka) to a number. For example, {health = 50, fatigue = 10} will cause 50 damage to health and 10 to fatigue (before adjusting for armor and difficulty). This field is ignored for failed attacks. --- @field [parent=#AttackInfo] #number strength A number between 0 and 1 representing the attack strength. This field is ignored for failed attacks. --- @field [parent=#AttackInfo] #boolean successful Whether the attack was successful or not. --- @field [parent=#AttackInfo] #AttackSourceType sourceType What class of attack this is. --- @field [parent=#AttackInfo] openmw.self#ATTACK_TYPE type (Optional) Attack variant if applicable. For melee attacks this represents chop vs thrust vs slash. For unarmed creatures this implies which of its 3 possible attacks were used. For other attacks this field can be ignored. --- @field [parent=#AttackInfo] openmw.types#Actor attacker (Optional) Attacking actor --- @field [parent=#AttackInfo] openmw.types#Weapon weapon (Optional) Attacking weapon --- @field [parent=#AttackInfo] #string ammo (Optional) Ammo record ID --- @field [parent=#AttackInfo] openmw.util#Vector3 hitPos (Optional) Where on the victim the attack is landing. Used to spawn blood effects. Blood effects are skipped if nil. return { - --- Basic combat interface - -- @module Combat - -- @usage require('openmw.interfaces').Combat - -- - --I.Combat.addOnHitHandler(function(attack) - -- -- Adds fatigue loss when hit by draining fatigue when taking health damage - -- if attack.damage.health and not attack.damage.fatigue then - -- local strengthFactor = Actor.stats.attributes.strength(self).modified / 100 * 0.66 - -- local enduranceFactor = Actor.stats.attributes.endurance(self).modified / 100 * 0.34 - -- local factor = 1 - math.min(strengthFactor + enduranceFactor, 1) - -- if factor > 0 then - -- attack.damage.fatigue = attack.damage.health * factor - -- end - -- end - --end) - interfaceName = 'Combat', - interface = { - --- Interface version - -- @field [parent=#Combat] #number version - version = 1, - - --- Add new onHit handler for this actor - -- If `handler(attack)` returns false, other handlers for - -- the call will be skipped. Where attack is the same @{#AttackInfo} passed to #Combat.onHit - -- @function [parent=#Combat] addOnHitHandler - -- @param #function handler The handler. - addOnHitHandler = function(handler) - onHitHandlers[#onHitHandlers + 1] = handler - end, - - --- Calculates the character's armor rating and adjusts damage accordingly. - -- Note that this function only adjusts the number, use #Combat.applyArmor - -- to include other side effects. - -- @function [parent=#Combat] adjustDamageForArmor - -- @param #number Damage The numeric damage to adjust - -- @param openmw.core#GameObject actor (Optional) The actor to calculate the armor rating for. Defaults to self. - -- @return #number Damage adjusted for armor - adjustDamageForArmor = function(damage, actor) return adjustDamageForArmor(damage, actor or self) end, - - --- Calculates a difficulty multiplier based on the current difficulty settings - -- and adjusts damage accordingly. Has no effect if both this actor and the - -- attacker are NPCs, or if both are Players. - -- @function [parent=#Combat] adjustDamageForDifficulty - -- @param #Attack attack The attack to adjust - -- @param openmw.core#GameObject defendant (Optional) The defendant to make the difficulty adjustment for. Defaults to self. - adjustDamageForDifficulty = function(attack, defendant) return adjustDamageForDifficulty(attack, defendant or self) end, - - --- Applies this character's armor to the attack. Adjusts damage, reduces item - -- condition accordingly, progresses armor skill, and plays the armor appropriate - -- hit sound. - -- @function [parent=#Combat] applyArmor - -- @param #Attack attack - applyArmor = applyArmor, - - --- Computes this character's armor rating. - -- Note that this interface function is read by the engine to update the UI. - -- This function can still be overridden same as any other interface, but must not call any functions or interfaces that modify anything. - -- @function [parent=#Combat] getArmorRating - -- @param openmw.core#GameObject actor (Optional) The actor to calculate the armor rating for. Defaults to self. - -- @return #number - getArmorRating = function(actor) return getArmorRating(actor or self) end, - - --- Computes this character's armor rating. - -- You can override this to return any skill you wish (including non-armor skills, if you so wish). - -- Note that this interface function is read by the engine to update the UI. - -- This function can still be overridden same as any other interface, but must not call any functions or interfaces that modify anything. - -- @function [parent=#Combat] getArmorSkill - -- @param openmw.core#GameObject item The item - -- @return #string The armor skill identifier, or unarmored if the item was nil or not an instace of @{openmw.types#Armor} - getArmorSkill = getArmorSkill, - - --- Computes the armor rating of a single piece of @{openmw.types#Armor}, adjusted for skill - -- Note that this interface function is read by the engine to update the UI. - -- This function can still be overridden same as any other interface, but must not call any functions or interfaces that modify anything. - -- @function [parent=#Combat] getSkillAdjustedArmorRating - -- @param openmw.core#GameObject item The item - -- @param openmw.core#GameObject actor (Optional) The actor, defaults to self - -- @return #number - getSkillAdjustedArmorRating = function(item, actor) return getSkillAdjustedArmorRating(item, actor or self) end, - - --- Computes the effective armor rating of a single piece of @{openmw.types#Armor}, adjusted for skill and item condition - -- @function [parent=#Combat] getEffectiveArmorRating - -- @param openmw.core#GameObject item The item - -- @param openmw.core#GameObject actor (Optional) The actor, defaults to self - -- @return #number - getEffectiveArmorRating = function(item, actor) return getEffectiveArmorRating(item, actor or self) end, - - --- Spawns a random blood effect at the given position - -- @function [parent=#Combat] spawnBloodEffect - -- @param openmw.util#Vector3 position - spawnBloodEffect = spawnBloodEffect, - - --- Hit this actor. Normally called as Hit event from the attacking actor, with the same parameters. - -- @function [parent=#Combat] onHit - -- @param #AttackInfo attackInfo - onHit = onHit, - - --- Picks a random armor slot and returns the item equipped in that slot. - -- Used to pick which armor to damage / skill to increase when hit during combat. - -- @function [parent=#Combat] pickRandomArmor - -- @param openmw.core#GameObject actor (Optional) The actor to pick armor from, defaults to self - -- @return openmw.core#GameObject The armor equipped in the chosen slot. nil if nothing was equipped in that slot. - pickRandomArmor = function(actor) return pickRandomArmor(actor or self) end, - - --- @{#AttackSourceType} - -- @field [parent=#Combat] #AttackSourceType ATTACK_SOURCE_TYPES Available attack source types - ATTACK_SOURCE_TYPES = { - Magic = 'magic', - Melee = 'melee', - Ranged = 'ranged', - Unspecified = 'unspecified', - }, - }, - - eventHandlers = { - Hit = function(data) I.Combat.onHit(data) end, - }, + interface = interface } diff --git a/files/data/scripts/omw/combat/menu.lua b/files/data-mw/scripts/omw/combat/menu.lua similarity index 100% rename from files/data/scripts/omw/combat/menu.lua rename to files/data-mw/scripts/omw/combat/menu.lua diff --git a/files/data/scripts/omw/music/helpers.lua b/files/data-mw/scripts/omw/music/helpers.lua old mode 100755 new mode 100644 similarity index 100% rename from files/data/scripts/omw/music/helpers.lua rename to files/data-mw/scripts/omw/music/helpers.lua diff --git a/files/data/scripts/omw/music/music.lua b/files/data-mw/scripts/omw/music/music.lua old mode 100755 new mode 100644 similarity index 100% rename from files/data/scripts/omw/music/music.lua rename to files/data-mw/scripts/omw/music/music.lua diff --git a/files/data/scripts/omw/music/settings.lua b/files/data-mw/scripts/omw/music/settings.lua old mode 100755 new mode 100644 similarity index 100% rename from files/data/scripts/omw/music/settings.lua rename to files/data-mw/scripts/omw/music/settings.lua diff --git a/files/data-mw/scripts/omw/playerskillhandlers.lua b/files/data-mw/scripts/omw/playerskillhandlers.lua new file mode 100644 index 0000000000..d070dd0311 --- /dev/null +++ b/files/data-mw/scripts/omw/playerskillhandlers.lua @@ -0,0 +1,190 @@ +local ambient = require('openmw.ambient') +local core = require('openmw.core') +local Skill = core.stats.Skill +local I = require('openmw.interfaces') +local self = require('openmw.self') +local types = require('openmw.types') +local NPC = types.NPC +local Actor = types.Actor +local ui = require('openmw.ui') +local auxUtil = require('openmw_aux.util') + +local function tableHasValue(table, value) + for _, v in pairs(table) do + if v == value then return true end + end + return false +end + +local function getSkillProgressRequirement(skillid) + local npcRecord = NPC.record(self) + local class = NPC.classes.record(npcRecord.class) + local skillStat = NPC.stats.skills[skillid](self) + local skillRecord = Skill.record(skillid) + + local factor = core.getGMST('fMiscSkillBonus') + if tableHasValue(class.majorSkills, skillid) then + factor = core.getGMST('fMajorSkillBonus') + elseif tableHasValue(class.minorSkills, skillid) then + factor = core.getGMST('fMinorSkillBonus') + end + + if skillRecord.specialization == class.specialization then + factor = factor * core.getGMST('fSpecialSkillBonus') + end + + return (skillStat.base + 1) * factor +end + +local function getSkillLevelUpOptions(skillid, source) + local skillRecord = Skill.record(skillid) + local npcRecord = NPC.record(self) + local class = NPC.classes.record(npcRecord.class) + + local levelUpProgress = 0 + local levelUpAttributeIncreaseValue = core.getGMST('iLevelupMiscMultAttriubte') + + if tableHasValue(class.minorSkills, skillid) then + levelUpProgress = core.getGMST('iLevelUpMinorMult') + levelUpAttributeIncreaseValue = core.getGMST('iLevelUpMinorMultAttribute') + elseif tableHasValue(class.majorSkills, skillid) then + levelUpProgress = core.getGMST('iLevelUpMajorMult') + levelUpAttributeIncreaseValue = core.getGMST('iLevelUpMajorMultAttribute') + end + + local options = {} + if source == 'jail' and not (skillid == 'security' or skillid == 'sneak') then + options.skillIncreaseValue = -1 + else + options.skillIncreaseValue = 1 + options.levelUpProgress = levelUpProgress + options.levelUpAttribute = skillRecord.attribute + options.levelUpAttributeIncreaseValue = levelUpAttributeIncreaseValue + options.levelUpSpecialization = skillRecord.specialization + options.levelUpSpecializationIncreaseValue = core.getGMST('iLevelupSpecialization') + end + return options +end + +local function skillLevelUpHandler(skillid, source, params) + local skillStat = NPC.stats.skills[skillid](self) + if (skillStat.base >= 100 and params.skillIncreaseValue > 0) or + (skillStat.base <= 0 and params.skillIncreaseValue < 0) then + return false + end + + if params.skillIncreaseValue then + skillStat.base = skillStat.base + params.skillIncreaseValue + end + + local levelStat = Actor.stats.level(self) + if params.levelUpProgress then + levelStat.progress = levelStat.progress + params.levelUpProgress + end + + if params.levelUpAttribute and params.levelUpAttributeIncreaseValue then + levelStat.skillIncreasesForAttribute[params.levelUpAttribute] + = levelStat.skillIncreasesForAttribute[params.levelUpAttribute] + params.levelUpAttributeIncreaseValue + end + + if params.levelUpSpecialization and params.levelUpSpecializationIncreaseValue then + levelStat.skillIncreasesForSpecialization[params.levelUpSpecialization] + = levelStat.skillIncreasesForSpecialization[params.levelUpSpecialization] + params.levelUpSpecializationIncreaseValue; + end + + if source ~= 'jail' then + local skillRecord = Skill.record(skillid) + local npcRecord = NPC.record(self) + local class = NPC.classes.record(npcRecord.class) + + ambient.playSound("skillraise") + + local message = string.format(core.getGMST('sNotifyMessage39'),skillRecord.name,skillStat.base) + + if source == I.SkillProgression.SKILL_INCREASE_SOURCES.Book then + message = '#{sBookSkillMessage}\n'..message + end + + ui.showMessage(message, { showInDialogue = false }) + + if levelStat.progress >= core.getGMST('iLevelUpTotal') then + ui.showMessage('#{sLevelUpMsg}', { showInDialogue = false }) + end + + if not source or source == I.SkillProgression.SKILL_INCREASE_SOURCES.Usage then skillStat.progress = 0 end + end +end + +local function jailTimeServed(days) + if not days or days <= 0 then + return + end + + local oldSkillLevels = {} + local skillByNumber = {} + for skillid, skillStat in pairs(NPC.stats.skills) do + oldSkillLevels[skillid] = skillStat(self).base + skillByNumber[#skillByNumber+1] = skillid + end + + math.randomseed(core.getSimulationTime()) + for day=1,days do + local skillid = skillByNumber[math.random(#skillByNumber)] + -- skillLevelUp() handles skill-based increase/decrease + I.SkillProgression.skillLevelUp(skillid, I.SkillProgression.SKILL_INCREASE_SOURCES.Jail) + end + + local message = '' + if days == 1 then + message = string.format(core.getGMST('sNotifyMessage42'), days) + else + message = string.format(core.getGMST('sNotifyMessage43'), days) + end + for skillid, skillStat in pairs(NPC.stats.skills) do + local diff = skillStat(self).base - oldSkillLevels[skillid] + if diff ~= 0 then + local skillMsg = core.getGMST('sNotifyMessage39') + if diff < 0 then + skillMsg = core.getGMST('sNotifyMessage44') + end + local skillRecord = Skill.record(skillid) + message = message..'\n'..string.format(skillMsg, skillRecord.name, skillStat(self).base) + end + end + + I.UI.showInteractiveMessage(message) +end + +local function skillUsedHandler(skillid, params) + if NPC.isWerewolf(self) then + return false + end + + local skillStat = NPC.stats.skills[skillid](self) + + if (skillStat.base >= 100 and params.skillGain > 0) or + (skillStat.base <= 0 and params.skillGain < 0) then + return false + end + + skillStat.progress = skillStat.progress + params.skillGain / I.SkillProgression.getSkillProgressRequirement(skillid) + + if skillStat.progress >= 1 then + I.SkillProgression.skillLevelUp(skillid, I.SkillProgression.SKILL_INCREASE_SOURCES.Usage) + end +end + +I.SkillProgression.addSkillUsedHandler(skillUsedHandler) +I.SkillProgression.addSkillLevelUpHandler(skillLevelUpHandler) + +local interface = auxUtil.shallowCopy(I.SkillProgression) +interface.getSkillProgressRequirement = getSkillProgressRequirement +interface.getSkillLevelUpOptions = getSkillLevelUpOptions + +return { + engineHandlers = { + _onJailTimeServed = jailTimeServed, + }, + interfaceName = 'SkillProgression', + interface = interface +} diff --git a/files/data/CMakeLists.txt b/files/data/CMakeLists.txt index f9cc1df16e..4b1b9e01ca 100644 --- a/files/data/CMakeLists.txt +++ b/files/data/CMakeLists.txt @@ -125,30 +125,23 @@ set(BUILTIN_DATA_FILES scripts/omw/activationhandlers.lua scripts/omw/ai.lua - scripts/omw/cellhandlers.lua scripts/omw/camera/camera.lua scripts/omw/camera/head_bobbing.lua scripts/omw/camera/third_person.lua scripts/omw/camera/settings.lua scripts/omw/camera/move360.lua scripts/omw/camera/first_person_auto_switch.lua - scripts/omw/combat/common.lua - scripts/omw/combat/global.lua - scripts/omw/combat/local.lua - scripts/omw/combat/menu.lua scripts/omw/console/global.lua scripts/omw/console/local.lua scripts/omw/console/player.lua scripts/omw/console/menu.lua + scripts/omw/combat/interface.lua scripts/omw/mechanics/actorcontroller.lua scripts/omw/mechanics/animationcontroller.lua scripts/omw/mechanics/globalcontroller.lua scripts/omw/mechanics/playercontroller.lua scripts/omw/settings/menu.lua scripts/omw/music/actor.lua - scripts/omw/music/helpers.lua - scripts/omw/music/music.lua - scripts/omw/music/settings.lua scripts/omw/settings/player.lua scripts/omw/settings/global.lua scripts/omw/settings/common.lua diff --git a/files/data/builtin.omwscripts b/files/data/builtin.omwscripts index 0d0d2eb8a7..6a3899accd 100644 --- a/files/data/builtin.omwscripts +++ b/files/data/builtin.omwscripts @@ -8,7 +8,6 @@ GLOBAL: scripts/omw/settings/global.lua # Mechanics GLOBAL: scripts/omw/activationhandlers.lua -GLOBAL: scripts/omw/cellhandlers.lua GLOBAL: scripts/omw/usehandlers.lua GLOBAL: scripts/omw/worldeventhandlers.lua GLOBAL: scripts/omw/crimes.lua @@ -25,9 +24,7 @@ PLAYER: scripts/omw/input/gamepadcontrols.lua NPC,CREATURE: scripts/omw/ai.lua GLOBAL: scripts/omw/mechanics/globalcontroller.lua CREATURE, NPC, PLAYER: scripts/omw/mechanics/actorcontroller.lua -GLOBAL: scripts/omw/combat/global.lua -MENU: scripts/omw/combat/menu.lua -NPC,CREATURE,PLAYER: scripts/omw/combat/local.lua +NPC,CREATURE,PLAYER: scripts/omw/combat/interface.lua # User interface PLAYER: scripts/omw/ui.lua @@ -39,6 +36,4 @@ GLOBAL: scripts/omw/console/global.lua CUSTOM: scripts/omw/console/local.lua # Music system -PLAYER: scripts/omw/music/music.lua -MENU: scripts/omw/music/settings.lua NPC,CREATURE: scripts/omw/music/actor.lua diff --git a/files/data/openmw_aux/util.lua b/files/data/openmw_aux/util.lua index 00dd974427..9535c979c8 100644 --- a/files/data/openmw_aux/util.lua +++ b/files/data/openmw_aux/util.lua @@ -143,5 +143,18 @@ function aux_util.callMultipleEventHandlers(handlers, ...) return false end +--- +-- Copies all key-value pairs from the input table to a new table. +-- @function [parent=#util] shallowCopy +-- @param #table table The table to copy +-- @return #table A shallow copy of the input table +function aux_util.shallowCopy(table) + local copy = {} + for key, value in pairs(table) do + copy[key] = value + end + return copy +end + return aux_util diff --git a/files/data/scripts/omw/combat/interface.lua b/files/data/scripts/omw/combat/interface.lua new file mode 100644 index 0000000000..b6471997ff --- /dev/null +++ b/files/data/scripts/omw/combat/interface.lua @@ -0,0 +1,145 @@ +local I = require('openmw.interfaces') +local util = require('openmw.util') +local auxUtil = require('openmw_aux.util') + +local onHitHandlers = {} + +--- +-- Table of possible attack source types +-- @type AttackSourceType +-- @field #string Magic +-- @field #string Melee +-- @field #string Ranged +-- @field #string Unspecified + +--- +-- @type AttackInfo +-- @field [parent=#AttackInfo] #table damage A table mapping a stat name (health, fatigue, or magicka) to a number. For example, {health = 50, fatigue = 10} will cause 50 damage to health and 10 to fatigue (before adjusting for armor and difficulty). This field is ignored for failed attacks. +-- @field [parent=#AttackInfo] #number strength A number between 0 and 1 representing the attack strength. This field is ignored for failed attacks. +-- @field [parent=#AttackInfo] #boolean successful Whether the attack was successful or not. +-- @field [parent=#AttackInfo] #AttackSourceType sourceType What class of attack this is. +-- @field [parent=#AttackInfo] openmw.self#ATTACK_TYPE type (Optional) Attack variant if applicable. For melee attacks this represents chop vs thrust vs slash. For unarmed creatures this implies which of its 3 possible attacks were used. For other attacks this field can be ignored. +-- @field [parent=#AttackInfo] openmw.types#Actor attacker (Optional) Attacking actor +-- @field [parent=#AttackInfo] openmw.types#Weapon weapon (Optional) Attacking weapon +-- @field [parent=#AttackInfo] #string ammo (Optional) Ammo record ID +-- @field [parent=#AttackInfo] openmw.util#Vector3 hitPos (Optional) Where on the victim the attack is landing. Used to spawn blood effects. Blood effects are skipped if nil. +return { + --- Basic combat interface + -- @module Combat + -- @usage local I = require('openmw.interfaces') + -- + --I.Combat.addOnHitHandler(function(attack) + -- -- Adds fatigue loss when hit by draining fatigue when taking health damage + -- if attack.damage.health and not attack.damage.fatigue then + -- local strengthFactor = Actor.stats.attributes.strength(self).modified / 100 * 0.66 + -- local enduranceFactor = Actor.stats.attributes.endurance(self).modified / 100 * 0.34 + -- local factor = 1 - math.min(strengthFactor + enduranceFactor, 1) + -- if factor > 0 then + -- attack.damage.fatigue = attack.damage.health * factor + -- end + -- end + --end) + + interfaceName = 'Combat', + interface = { + --- Interface version + -- @field [parent=#Combat] #number version + version = 1, + + --- Add new onHit handler for this actor + -- If `handler(attack)` returns false, other handlers for + -- the call will be skipped. Where attack is the same @{#AttackInfo} passed to #Combat.onHit + -- @function [parent=#Combat] addOnHitHandler + -- @param #function handler The handler. + addOnHitHandler = function(handler) + onHitHandlers[#onHitHandlers + 1] = handler + end, + + --- Calculates the character's armor rating and adjusts damage accordingly. + -- Note that this function only adjusts the number, use #Combat.applyArmor + -- to include other side effects. + -- @function [parent=#Combat] adjustDamageForArmor + -- @param #number Damage The numeric damage to adjust + -- @param openmw.core#GameObject actor (Optional) The actor to calculate the armor rating for. Defaults to self. + -- @return #number Damage adjusted for armor + adjustDamageForArmor = function(damage, actor) return damage end, + + --- Calculates a difficulty multiplier based on the current difficulty settings + -- and adjusts damage accordingly. Has no effect if both this actor and the + -- attacker are NPCs, or if both are Players. + -- @function [parent=#Combat] adjustDamageForDifficulty + -- @param #Attack attack The attack to adjust + -- @param openmw.core#GameObject defendant (Optional) The defendant to make the difficulty adjustment for. Defaults to self. + adjustDamageForDifficulty = function(attack, defendant) end, + + --- Applies this character's armor to the attack. Adjusts damage, reduces item + -- condition accordingly, progresses armor skill, and plays the armor appropriate + -- hit sound. + -- @function [parent=#Combat] applyArmor + -- @param #Attack attack + applyArmor = function(attack) end, + + --- Computes this character's armor rating. + -- Note that this interface function is read by the engine to update the UI. + -- This function can still be overridden same as any other interface, but must not call any functions or interfaces that modify anything. + -- @function [parent=#Combat] getArmorRating + -- @param openmw.core#GameObject actor (Optional) The actor to calculate the armor rating for. Defaults to self. + -- @return #number + getArmorRating = function(actor) return 0 end, + + --- Computes this character's armor rating. + -- You can override this to return any skill you wish (including non-armor skills, if you so wish). + -- Note that this interface function is read by the engine to update the UI. + -- This function can still be overridden same as any other interface, but must not call any functions or interfaces that modify anything. + -- @function [parent=#Combat] getArmorSkill + -- @param openmw.core#GameObject item The item + -- @return #string The armor skill identifier, or unarmored if the item was nil or not an instance of @{openmw.types#Armor}. Can return nil if unimplemented. + getArmorSkill = function(item) return nil end, + + --- Computes the armor rating of a single piece of @{openmw.types#Armor}, adjusted for skill + -- Note that this interface function is read by the engine to update the UI. + -- This function can still be overridden same as any other interface, but must not call any functions or interfaces that modify anything. + -- @function [parent=#Combat] getSkillAdjustedArmorRating + -- @param openmw.core#GameObject item The item + -- @param openmw.core#GameObject actor (Optional) The actor, defaults to self + -- @return #number + getSkillAdjustedArmorRating = function(item, actor) return 0 end, + + --- Computes the effective armor rating of a single piece of @{openmw.types#Armor}, adjusted for skill and item condition + -- @function [parent=#Combat] getEffectiveArmorRating + -- @param openmw.core#GameObject item The item + -- @param openmw.core#GameObject actor (Optional) The actor, defaults to self + -- @return #number + getEffectiveArmorRating = function(item, actor) return 0 end, + + --- Spawns a random blood effect at the given position + -- @function [parent=#Combat] spawnBloodEffect + -- @param openmw.util#Vector3 position + spawnBloodEffect = function(position) end, + + --- Hit this actor. Normally called as Hit event from the attacking actor, with the same parameters. + -- @function [parent=#Combat] onHit + -- @param #AttackInfo attackInfo + onHit = function(attackInfo) auxUtil.callEventHandlers(onHitHandlers, attackInfo) end, + + --- Picks a random armor slot and returns the item equipped in that slot. + -- Used to pick which armor to damage / skill to increase when hit during combat. + -- @function [parent=#Combat] pickRandomArmor + -- @param openmw.core#GameObject actor (Optional) The actor to pick armor from, defaults to self + -- @return openmw.core#GameObject The armor equipped in the chosen slot. nil if nothing was equipped in that slot. + pickRandomArmor = function(actor) return nil end, + + --- @{#AttackSourceType} + -- @field [parent=#Combat] #AttackSourceType ATTACK_SOURCE_TYPES Available attack source types + ATTACK_SOURCE_TYPES = util.makeStrictReadOnly({ + Magic = 'magic', + Melee = 'melee', + Ranged = 'ranged', + Unspecified = 'unspecified', + }), + }, + + eventHandlers = { + Hit = function(data) I.Combat.onHit(data) end, + }, +} \ No newline at end of file diff --git a/files/data/scripts/omw/mechanics/playercontroller.lua b/files/data/scripts/omw/mechanics/playercontroller.lua index 8345ed2166..a8963f86d1 100644 --- a/files/data/scripts/omw/mechanics/playercontroller.lua +++ b/files/data/scripts/omw/mechanics/playercontroller.lua @@ -1,12 +1,7 @@ -local ambient = require('openmw.ambient') local core = require('openmw.core') -local Skill = core.stats.Skill -local I = require('openmw.interfaces') local nearby = require('openmw.nearby') local self = require('openmw.self') local types = require('openmw.types') -local NPC = types.NPC -local Actor = types.Actor local ui = require('openmw.ui') local cell = nil @@ -37,114 +32,6 @@ local function processAutomaticDoors() end end -local function skillLevelUpHandler(skillid, source, params) - local skillStat = NPC.stats.skills[skillid](self) - if (skillStat.base >= 100 and params.skillIncreaseValue > 0) or - (skillStat.base <= 0 and params.skillIncreaseValue < 0) then - return false - end - - if params.skillIncreaseValue then - skillStat.base = skillStat.base + params.skillIncreaseValue - end - - local levelStat = Actor.stats.level(self) - if params.levelUpProgress then - levelStat.progress = levelStat.progress + params.levelUpProgress - end - - if params.levelUpAttribute and params.levelUpAttributeIncreaseValue then - levelStat.skillIncreasesForAttribute[params.levelUpAttribute] - = levelStat.skillIncreasesForAttribute[params.levelUpAttribute] + params.levelUpAttributeIncreaseValue - end - - if params.levelUpSpecialization and params.levelUpSpecializationIncreaseValue then - levelStat.skillIncreasesForSpecialization[params.levelUpSpecialization] - = levelStat.skillIncreasesForSpecialization[params.levelUpSpecialization] + params.levelUpSpecializationIncreaseValue; - end - - if source ~= 'jail' then - local skillRecord = Skill.record(skillid) - local npcRecord = NPC.record(self) - local class = NPC.classes.record(npcRecord.class) - - ambient.playSound("skillraise") - - local message = string.format(core.getGMST('sNotifyMessage39'),skillRecord.name,skillStat.base) - - if source == I.SkillProgression.SKILL_INCREASE_SOURCES.Book then - message = '#{sBookSkillMessage}\n'..message - end - - ui.showMessage(message, { showInDialogue = false }) - - if levelStat.progress >= core.getGMST('iLevelUpTotal') then - ui.showMessage('#{sLevelUpMsg}', { showInDialogue = false }) - end - - if not source or source == I.SkillProgression.SKILL_INCREASE_SOURCES.Usage then skillStat.progress = 0 end - end -end - -local function jailTimeServed(days) - if not days or days <= 0 then - return - end - - local oldSkillLevels = {} - local skillByNumber = {} - for skillid, skillStat in pairs(NPC.stats.skills) do - oldSkillLevels[skillid] = skillStat(self).base - skillByNumber[#skillByNumber+1] = skillid - end - - math.randomseed(core.getSimulationTime()) - for day=1,days do - local skillid = skillByNumber[math.random(#skillByNumber)] - -- skillLevelUp() handles skill-based increase/decrease - I.SkillProgression.skillLevelUp(skillid, I.SkillProgression.SKILL_INCREASE_SOURCES.Jail) - end - - local message = '' - if days == 1 then - message = string.format(core.getGMST('sNotifyMessage42'), days) - else - message = string.format(core.getGMST('sNotifyMessage43'), days) - end - for skillid, skillStat in pairs(NPC.stats.skills) do - local diff = skillStat(self).base - oldSkillLevels[skillid] - if diff ~= 0 then - local skillMsg = core.getGMST('sNotifyMessage39') - if diff < 0 then - skillMsg = core.getGMST('sNotifyMessage44') - end - local skillRecord = Skill.record(skillid) - message = message..'\n'..string.format(skillMsg, skillRecord.name, skillStat(self).base) - end - end - - I.UI.showInteractiveMessage(message) -end - -local function skillUsedHandler(skillid, params) - if NPC.isWerewolf(self) then - return false - end - - local skillStat = NPC.stats.skills[skillid](self) - - if (skillStat.base >= 100 and params.skillGain > 0) or - (skillStat.base <= 0 and params.skillGain < 0) then - return false - end - - skillStat.progress = skillStat.progress + params.skillGain / I.SkillProgression.getSkillProgressRequirement(skillid) - - if skillStat.progress >= 1 then - I.SkillProgression.skillLevelUp(skillid, I.SkillProgression.SKILL_INCREASE_SOURCES.Usage) - end -end - local function onUpdate(dt) if dt <= 0 then return @@ -157,13 +44,9 @@ local function onUpdate(dt) processAutomaticDoors() end -I.SkillProgression.addSkillUsedHandler(skillUsedHandler) -I.SkillProgression.addSkillLevelUpHandler(skillLevelUpHandler) - return { engineHandlers = { onUpdate = onUpdate, - _onJailTimeServed = jailTimeServed, }, eventHandlers = { diff --git a/files/data/scripts/omw/skillhandlers.lua b/files/data/scripts/omw/skillhandlers.lua index 70036db857..2e19e8cfeb 100644 --- a/files/data/scripts/omw/skillhandlers.lua +++ b/files/data/scripts/omw/skillhandlers.lua @@ -1,6 +1,5 @@ local self = require('openmw.self') local I = require('openmw.interfaces') -local types = require('openmw.types') local core = require('openmw.core') local auxUtil = require('openmw_aux.util') local NPC = require('openmw.types').NPC @@ -47,40 +46,6 @@ local Skill = core.stats.Skill local skillUsedHandlers = {} local skillLevelUpHandlers = {} -local function tableHasValue(table, value) - for _, v in pairs(table) do - if v == value then return true end - end - return false -end - -local function shallowCopy(t1) - local t2 = {} - for key, value in pairs(t1) do t2[key] = value end - return t2 -end - -local function getSkillProgressRequirement(skillid) - local npcRecord = NPC.record(self) - local class = NPC.classes.record(npcRecord.class) - local skillStat = NPC.stats.skills[skillid](self) - local skillRecord = Skill.record(skillid) - - local factor = core.getGMST('fMiscSkillBonus') - if tableHasValue(class.majorSkills, skillid) then - factor = core.getGMST('fMajorSkillBonus') - elseif tableHasValue(class.minorSkills, skillid) then - factor = core.getGMST('fMinorSkillBonus') - end - - if skillRecord.specialization == class.specialization then - factor = factor * core.getGMST('fSpecialSkillBonus') - end - - return (skillStat.base + 1) * factor -end - - local function skillUsed(skillid, options) if #skillUsedHandlers == 0 then -- If there are no handlers, then there won't be any effect, so skip calculations @@ -88,7 +53,7 @@ local function skillUsed(skillid, options) end -- Make a copy so we don't change the caller's table - options = shallowCopy(options) + options = auxUtil.shallowCopy(options) -- Compute use value if it was not supplied directly if not options.skillGain then @@ -113,34 +78,7 @@ local function skillLevelUp(skillid, source) -- If there are no handlers, then there won't be any effect, so skip calculations return end - - local skillRecord = Skill.record(skillid) - local npcRecord = NPC.record(self) - local class = NPC.classes.record(npcRecord.class) - - local levelUpProgress = 0 - local levelUpAttributeIncreaseValue = core.getGMST('iLevelupMiscMultAttriubte') - - if tableHasValue(class.minorSkills, skillid) then - levelUpProgress = core.getGMST('iLevelUpMinorMult') - levelUpAttributeIncreaseValue = core.getGMST('iLevelUpMinorMultAttribute') - elseif tableHasValue(class.majorSkills, skillid) then - levelUpProgress = core.getGMST('iLevelUpMajorMult') - levelUpAttributeIncreaseValue = core.getGMST('iLevelUpMajorMultAttribute') - end - - local options = {} - if source == 'jail' and not (skillid == 'security' or skillid == 'sneak') then - options.skillIncreaseValue = -1 - else - options.skillIncreaseValue = 1 - options.levelUpProgress = levelUpProgress - options.levelUpAttribute = skillRecord.attribute - options.levelUpAttributeIncreaseValue = levelUpAttributeIncreaseValue - options.levelUpSpecialization = skillRecord.specialization - options.levelUpSpecializationIncreaseValue = core.getGMST('iLevelupSpecialization') - end - + local options = I.SkillProgression.getSkillLevelUpOptions(skillid, source) auxUtil.callEventHandlers(skillLevelUpHandlers, skillid, source, options) end @@ -181,7 +119,7 @@ return { interface = { --- Interface version -- @field [parent=#SkillProgression] #number version - version = 1, + version = 2, --- Add new skill level up handler for this actor. -- For load order consistency, handlers should be added in the body if your script. @@ -268,6 +206,13 @@ return { -- @param #string skillid The id of the skill to level up. -- @param #SkillLevelUpSource source The source of the skill increase. Note that passing a value of @{#SkillLevelUpSource.Jail} will cause a skill decrease for all skills except sneak and security. skillLevelUp = skillLevelUp, + + --- Construct a table of skill level up options + -- @function [parent=#SkillProgression] getSkillLevelUpOptions + -- @param #string skillid The id of the skill to level up + -- @param #SkillLevelUpSource source The source of the skill increase + -- @return #table The options to pass to the skill level up handlers + getSkillLevelUpOptions = function(skillid, source) return {} end, --- @{#SkillLevelUpSource} -- @field [parent=#SkillProgression] #SkillLevelUpSource SKILL_INCREASE_SOURCES @@ -281,7 +226,7 @@ return { --- Compute the total skill gain required to level up a skill based on its current level, and other modifying factors such as major skills and specialization. -- @function [parent=#SkillProgression] getSkillProgressRequirement -- @param #string skillid The id of the skill to compute skill progress requirement for - getSkillProgressRequirement = getSkillProgressRequirement + getSkillProgressRequirement = function(skillid) return 1 end }, engineHandlers = { -- Use the interface in these handlers so any overrides will receive the calls. diff --git a/files/lua_api/openmw/interfaces.lua b/files/lua_api/openmw/interfaces.lua index cfdf4728d1..140b484382 100644 --- a/files/lua_api/openmw/interfaces.lua +++ b/files/lua_api/openmw/interfaces.lua @@ -16,7 +16,7 @@ -- @field [parent=#interfaces] scripts.omw.camera.camera#scripts.omw.camera.camera Camera --- --- @field [parent=#interfaces] scripts.omw.combat.local#scripts.omw.combat.local Combat +-- @field [parent=#interfaces] scripts.omw.combat.interface#scripts.omw.combat.interface Combat --- -- @field [parent=#interfaces] scripts.omw.mwui.init#scripts.omw.mwui.init MWUI