From 79289304354f9ef839ca51a652f99e3287f8b2cf Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sat, 4 Oct 2025 13:38:20 +0200 Subject: [PATCH 01/11] Move Morrowind mechanics to data-mw --- docs/source/luadoc_data_paths.sh | 2 +- .../lua-scripting/interface_combat.rst | 2 +- files/data-mw/CMakeLists.txt | 14 + files/data-mw/builtin.omwscripts | 50 ++ .../scripts/omw/cellhandlers.lua | 0 .../scripts/omw/combat/common.lua | 0 .../scripts/omw/combat/global.lua | 0 files/data-mw/scripts/omw/combat/local.lua | 37 ++ .../scripts/omw/combat/menu.lua | 0 .../omw/interfaces/combatfunctions.lua} | 440 ++++++------------ .../scripts/omw/interfaces/skillfunctions.lua | 66 +++ .../scripts/omw/music/helpers.lua | 0 .../scripts/omw/music/music.lua | 0 .../scripts/omw/music/settings.lua | 0 .../scripts/omw/playerskillhandlers.lua | 126 +++++ files/data/CMakeLists.txt | 11 +- files/data/builtin.omwscripts | 9 +- files/data/scripts/omw/interfaces/combat.lua | 145 ++++++ .../omw/interfaces/combatfunctions.lua | 11 + .../scripts/omw/interfaces/skillfunctions.lua | 4 + .../omw/mechanics/playercontroller.lua | 117 ----- files/data/scripts/omw/skillhandlers.lua | 61 +-- files/lua_api/openmw/interfaces.lua | 2 +- 23 files changed, 605 insertions(+), 492 deletions(-) create mode 100644 files/data-mw/builtin.omwscripts rename files/{data => data-mw}/scripts/omw/cellhandlers.lua (100%) rename files/{data => data-mw}/scripts/omw/combat/common.lua (100%) rename files/{data => data-mw}/scripts/omw/combat/global.lua (100%) create mode 100644 files/data-mw/scripts/omw/combat/local.lua rename files/{data => data-mw}/scripts/omw/combat/menu.lua (100%) rename files/{data/scripts/omw/combat/local.lua => data-mw/scripts/omw/interfaces/combatfunctions.lua} (51%) create mode 100644 files/data-mw/scripts/omw/interfaces/skillfunctions.lua rename files/{data => data-mw}/scripts/omw/music/helpers.lua (100%) mode change 100755 => 100644 rename files/{data => data-mw}/scripts/omw/music/music.lua (100%) mode change 100755 => 100644 rename files/{data => data-mw}/scripts/omw/music/settings.lua (100%) mode change 100755 => 100644 create mode 100644 files/data-mw/scripts/omw/playerskillhandlers.lua create mode 100644 files/data/scripts/omw/interfaces/combat.lua create mode 100644 files/data/scripts/omw/interfaces/combatfunctions.lua create mode 100644 files/data/scripts/omw/interfaces/skillfunctions.lua diff --git a/docs/source/luadoc_data_paths.sh b/docs/source/luadoc_data_paths.sh index 6ad2ec90cc..025485e5cf 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/interfaces/combat.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..5f8271d12c 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_interfaces_combat.html diff --git a/files/data-mw/CMakeLists.txt b/files/data-mw/CMakeLists.txt index 48e818cfb1..60c8bb32aa 100644 --- a/files/data-mw/CMakeLists.txt +++ b/files/data-mw/CMakeLists.txt @@ -20,6 +20,20 @@ set(BUILTIN_DATA_MW_FILES # Game-specific settings for calendar.lua openmw_aux/calendarconfig.lua + + builtin.omwscripts + + 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/interfaces/combatfunctions.lua + scripts/omw/interfaces/skillfunctions.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}) diff --git a/files/data-mw/builtin.omwscripts b/files/data-mw/builtin.omwscripts new file mode 100644 index 0000000000..de6c79fe8a --- /dev/null +++ b/files/data-mw/builtin.omwscripts @@ -0,0 +1,50 @@ +# NB: This file is a superset of the one in data! + +# UI framework +MENU,PLAYER: scripts/omw/mwui/init.lua + +# Settings framework +MENU: scripts/omw/settings/menu.lua +PLAYER: scripts/omw/settings/player.lua +GLOBAL: scripts/omw/settings/global.lua + +# Mechanics +GLOBAL: scripts/omw/activationhandlers.lua +GLOBAL: scripts/omw/usehandlers.lua +GLOBAL: scripts/omw/worldeventhandlers.lua +GLOBAL: scripts/omw/crimes.lua +CREATURE, NPC, PLAYER: scripts/omw/mechanics/animationcontroller.lua +PLAYER: scripts/omw/skillhandlers.lua +PLAYER: scripts/omw/mechanics/playercontroller.lua +MENU: scripts/omw/camera/settings.lua +MENU: scripts/omw/input/settings.lua +PLAYER: scripts/omw/input/playercontrols.lua +PLAYER: scripts/omw/camera/camera.lua +PLAYER: scripts/omw/input/actionbindings.lua +PLAYER: scripts/omw/input/smoothmovement.lua +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 +NPC,CREATURE,PLAYER: scripts/omw/interfaces/combat.lua + +# User interface +PLAYER: scripts/omw/ui.lua + +# Lua console +MENU: scripts/omw/console/menu.lua +PLAYER: scripts/omw/console/player.lua +GLOBAL: scripts/omw/console/global.lua +CUSTOM: scripts/omw/console/local.lua + +# Music system +NPC,CREATURE: scripts/omw/music/actor.lua + +# Game specific +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-mw/scripts/omw/combat/local.lua b/files/data-mw/scripts/omw/combat/local.lua new file mode 100644 index 0000000000..6c90d73b5a --- /dev/null +++ b/files/data-mw/scripts/omw/combat/local.lua @@ -0,0 +1,37 @@ +local core = require('openmw.core') +local I = require('openmw.interfaces') +local self = require('openmw.self') +local types = require('openmw.types') +local Actor = types.Actor +local Player = types.Player +local isPlayer = Player.objectIsInstance(self) + +local godMode = function() return false end +if isPlayer then + -- openmw.debug is only allowed on player scripts + godMode = function() return require('openmw.debug').isGodMode() end +end + +local function getDamage(attack, what) + if attack.damage then + return attack.damage[what] or 0 + end +end + +local function onHit(data) + 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 + I.Combat.spawnBloodEffect(data.hitPos) + end + end + elseif data.attacker and Player.objectIsInstance(data.attacker) then + core.sound.playSound3d('miss', self) + end + Actor._onHit(self, data) +end + +I.Combat.addOnHitHandler(onHit) 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/combat/local.lua b/files/data-mw/scripts/omw/interfaces/combatfunctions.lua similarity index 51% rename from files/data/scripts/omw/combat/local.lua rename to files/data-mw/scripts/omw/interfaces/combatfunctions.lua index a046614986..398c510bf4 100644 --- a/files/data/scripts/omw/combat/local.lua +++ b/files/data-mw/scripts/omw/interfaces/combatfunctions.lua @@ -1,27 +1,14 @@ -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 isPlayer = Player.objectIsInstance(self) -local godMode = function() return false end -if isPlayer then - -- openmw.debug is only allowed on player scripts - godMode = function() return require('openmw.debug').isGodMode() end -end - -local onHitHandlers = {} - local settings = storage.globalSection('SettingsOMWCombat') local function getSkill(actor, skillId) @@ -60,39 +47,82 @@ local armorSlots = { Actor.EQUIPMENT_SLOT.CarriedLeft, } -local function getArmorSkill(item) - if not item or not Armor.objectIsInstance(item) then - return 'unarmored' - end - local record = Armor.record(item) - local weightGmst = armorTypeGmst[record.type] - local epsilon = 0.0005 - if record.weight <= weightGmst * core.getGMST('fLightMaxMod') + epsilon then - return 'lightarmor' - elseif record.weight <= weightGmst * core.getGMST('fMedMaxMod') + epsilon then - return 'mediumarmor' - else - return 'heavyarmor' +local function getDamage(attack, what) + if attack.damage then + return attack.damage[what] or 0 end end -local function getSkillAdjustedArmorRating(item, actor) - local record = Armor.record(item) - local skillid = I.Combat.getArmorSkill(item) - local skill = getSkill(actor, skillid) - if record.weight == 0 then - return record.baseArmor - end - return record.baseArmor * skill / core.getGMST('iBaseArmorSkill') +local function setDamage(attack, what, damage) + attack.damage = attack.damage or {} + attack.damage[what] = damage end -local function getEffectiveArmorRating(item, actor) - local record = Armor.record(item) - local rating = getSkillAdjustedArmorRating(item, actor) - if record.health and record.health ~= 0 then - rating = rating * (types.Item.itemData(item).condition / record.health) +local function adjustDamageForArmor(damage, actor) + local armor = I.Combat.getArmorRating(actor) + local x = damage / (damage + armor) + return damage * math.max(x, core.getGMST('fCombatArmorMinMult')) +end + +local function adjustDamageForDifficulty(attack, defendant) + local attackerIsPlayer = attack.attacker and Player.objectIsInstance(attack.attacker) + -- The interface guarantees defendant is never nil + local defendantIsPlayer = Player.objectIsInstance(defendant) + -- If both characters are NPCs or both characters are players then + -- difficulty settings do not apply + if attackerIsPlayer == defendantIsPlayer then return end + + local fDifficultyMult = core.getGMST('fDifficultyMult') + local difficultyTerm = core.getGameDifficulty() * 0.01 + local x = 0 + + if defendantIsPlayer then + -- Defending actor is a player + if difficultyTerm > 0 then + x = difficultyTerm * fDifficultyMult + else + x = difficultyTerm / fDifficultyMult + end + elseif attackerIsPlayer then + -- Attacking actor is a player + if difficultyTerm > 0 then + x = -difficultyTerm / fDifficultyMult + else + x = -difficultyTerm * fDifficultyMult + end + end + + setDamage(attack, 'health', getDamage(attack, 'health') * (1 + x)) +end + +local function applyArmor(attack) + local healthDamage = getDamage(attack, 'health') + if healthDamage > 0 then + local healthDamageAdjusted = I.Combat.adjustDamageForArmor(healthDamage) + local diff = math.floor(healthDamageAdjusted - healthDamage) + setDamage(attack, 'health', math.max(healthDamageAdjusted, 1)) + local item = I.Combat.pickRandomArmor() + local skillid = I.Combat.getArmorSkill(item) + if I.SkillProgression then + I.SkillProgression.skillUsed(skillid, {useType = I.SkillProgression.SKILL_USE_TYPES.Armor_HitByOpponent}) + end + if item and Armor.objectIsInstance(item) then + local attackerIsUnarmedCreature = attack.attacker and not attack.weapon and not attack.ammo and Creature.objectIsInstance(attack.attacker) + if settings:get('unarmedCreatureAttacksDamageArmor') or not attackerIsUnarmedCreature then + core.sendGlobalEvent('ModifyItemCondition', { actor = self, item = item, amount = diff }) + end + + if skillid == 'lightarmor' then + core.sound.playSound3d('Light Armor Hit', self) + elseif skillid == 'mediumarmor' then + core.sound.playSound3d('Medium Armor Hit', self) + elseif skillid == 'heavyarmor' then + core.sound.playSound3d('Heavy Armor Hit', self) + else + core.sound.playSound3d('Hand To Hand Hit', self) + end + end end - return rating end local function getArmorRating(actor) @@ -129,10 +159,67 @@ local function getArmorRating(actor) + magicShield end -local function adjustDamageForArmor(damage, actor) - local armor = I.Combat.getArmorRating(actor) - local x = damage / (damage + armor) - return damage * math.max(x, core.getGMST('fCombatArmorMinMult')) +local function getArmorSkill(item) + if not item or not Armor.objectIsInstance(item) then + return 'unarmored' + end + local record = Armor.record(item) + local weightGmst = armorTypeGmst[record.type] + local epsilon = 0.0005 + if record.weight <= weightGmst * core.getGMST('fLightMaxMod') + epsilon then + return 'lightarmor' + elseif record.weight <= weightGmst * core.getGMST('fMedMaxMod') + epsilon then + return 'mediumarmor' + else + return 'heavyarmor' + end +end + +local function getSkillAdjustedArmorRating(item, actor) + local record = Armor.record(item) + local skillid = I.Combat.getArmorSkill(item) + local skill = getSkill(actor, skillid) + if record.weight == 0 then + return record.baseArmor + end + return record.baseArmor * skill / core.getGMST('iBaseArmorSkill') +end + +local function getEffectiveArmorRating(item, actor) + local record = Armor.record(item) + local rating = getSkillAdjustedArmorRating(item, actor) + if record.health and record.health ~= 0 then + rating = rating * (types.Item.itemData(item).condition / record.health) + end + return rating +end + +local function spawnBloodEffect(position) + if isPlayer and not settings:get('spawnBloodEffectsOnPlayer') then + return + end + + local bloodEffectModel = string.format('Blood_Model_%d', math.random(0, 2)) -- randIntUniformClosed(0, 2) + + -- TODO: implement a Misc::correctMeshPath equivalent instead? + -- All it ever does it append 'meshes\\' though + bloodEffectModel = 'meshes/'..core.getGMST(bloodEffectModel) + + local record = self.object.type.record(self.object) + local bloodTexture = string.format('Blood_Texture_%d', record.bloodType) + bloodTexture = core.getGMST(bloodTexture) + if not bloodTexture or bloodTexture == '' then + bloodTexture = core.getGMST('Blood_Texture_0') + end + core.sendGlobalEvent('SpawnVfx', { + model = bloodEffectModel, + position = position, + options = { + mwMagicVfx = false, + particleTextureOverride = bloodTexture, + useAmbientLight = false, + }, + }) end local function pickRandomArmor(actor) @@ -170,261 +257,14 @@ local function pickRandomArmor(actor) return Actor.getEquipment(actor, slot) end -local function getDamage(attack, what) - if attack.damage then - return attack.damage[what] or 0 - end -end - -local function setDamage(attack, what, damage) - attack.damage = attack.damage or {} - attack.damage[what] = damage -end - -local function applyArmor(attack) - local healthDamage = getDamage(attack, 'health') - if healthDamage > 0 then - local healthDamageAdjusted = I.Combat.adjustDamageForArmor(healthDamage) - local diff = math.floor(healthDamageAdjusted - healthDamage) - setDamage(attack, 'health', math.max(healthDamageAdjusted, 1)) - local item = I.Combat.pickRandomArmor() - local skillid = I.Combat.getArmorSkill(item) - if I.SkillProgression then - I.SkillProgression.skillUsed(skillid, {useType = I.SkillProgression.SKILL_USE_TYPES.Armor_HitByOpponent}) - end - if item and Armor.objectIsInstance(item) then - local attackerIsUnarmedCreature = attack.attacker and not attack.weapon and not attack.ammo and Creature.objectIsInstance(attack.attacker) - if settings:get('unarmedCreatureAttacksDamageArmor') or not attackerIsUnarmedCreature then - core.sendGlobalEvent('ModifyItemCondition', { actor = self, item = item, amount = diff }) - end - - if skillid == 'lightarmor' then - core.sound.playSound3d('Light Armor Hit', self) - elseif skillid == 'mediumarmor' then - core.sound.playSound3d('Medium Armor Hit', self) - elseif skillid == 'heavyarmor' then - core.sound.playSound3d('Heavy Armor Hit', self) - else - core.sound.playSound3d('Hand To Hand Hit', self) - end - end - end -end - -local function adjustDamageForDifficulty(attack, defendant) - local attackerIsPlayer = attack.attacker and Player.objectIsInstance(attack.attacker) - -- The interface guarantees defendant is never nil - local defendantIsPlayer = Player.objectIsInstance(defendant) - -- If both characters are NPCs or both characters are players then - -- difficulty settings do not apply - if attackerIsPlayer == defendantIsPlayer then return end - - local fDifficultyMult = core.getGMST('fDifficultyMult') - local difficultyTerm = core.getGameDifficulty() * 0.01 - local x = 0 - - if defendantIsPlayer then - -- Defending actor is a player - if difficultyTerm > 0 then - x = difficultyTerm * fDifficultyMult - else - x = difficultyTerm / fDifficultyMult - end - elseif attackerIsPlayer then - -- Attacking actor is a player - if difficultyTerm > 0 then - x = -difficultyTerm / fDifficultyMult - else - x = -difficultyTerm * fDifficultyMult - end - end - - setDamage(attack, 'health', getDamage(attack, 'health') * (1 + x)) -end - -local function spawnBloodEffect(position) - if isPlayer and not settings:get('spawnBloodEffectsOnPlayer') then - return - end - - local bloodEffectModel = string.format('Blood_Model_%d', math.random(0, 2)) -- randIntUniformClosed(0, 2) - - -- TODO: implement a Misc::correctMeshPath equivalent instead? - -- All it ever does it append 'meshes\\' though - bloodEffectModel = 'meshes/'..core.getGMST(bloodEffectModel) - - local record = self.object.type.record(self.object) - local bloodTexture = string.format('Blood_Texture_%d', record.bloodType) - bloodTexture = core.getGMST(bloodTexture) - if not bloodTexture or bloodTexture == '' then - bloodTexture = core.getGMST('Blood_Texture_0') - end - core.sendGlobalEvent('SpawnVfx', { - model = bloodEffectModel, - position = position, - options = { - mwMagicVfx = false, - particleTextureOverride = bloodTexture, - useAmbientLight = false, - }, - }) -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) - end - end - elseif data.attacker and Player.objectIsInstance(data.attacker) then - core.sound.playSound3d('miss', self) - end - 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 - ---- --- @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, - }, + adjustDamageForArmor = function(damage, actor) return adjustDamageForArmor(damage, actor or self) end, + adjustDamageForDifficulty = function(attack, defendant) return adjustDamageForDifficulty(attack, defendant or self) end, + applyArmor = applyArmor, + getArmorRating = function(actor) return getArmorRating(actor or self) end, + getArmorSkill = getArmorSkill, + getSkillAdjustedArmorRating = function(item, actor) return getSkillAdjustedArmorRating(item, actor or self) end, + getEffectiveArmorRating = function(item, actor) return getEffectiveArmorRating(item, actor or self) end, + spawnBloodEffect = spawnBloodEffect, + pickRandomArmor = function(actor) return pickRandomArmor(actor or self) end } diff --git a/files/data-mw/scripts/omw/interfaces/skillfunctions.lua b/files/data-mw/scripts/omw/interfaces/skillfunctions.lua new file mode 100644 index 0000000000..0cb671d1bf --- /dev/null +++ b/files/data-mw/scripts/omw/interfaces/skillfunctions.lua @@ -0,0 +1,66 @@ +local self = require('openmw.self') +local core = require('openmw.core') +local NPC = require('openmw.types').NPC +local Skill = core.stats.Skill + +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 + +return { + getSkillProgressRequirement = getSkillProgressRequirement, + getSkillLevelUpOptions = getSkillLevelUpOptions +} 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..b6485bba8e --- /dev/null +++ b/files/data-mw/scripts/omw/playerskillhandlers.lua @@ -0,0 +1,126 @@ +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 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) + +return { + engineHandlers = { + _onJailTimeServed = jailTimeServed, + } +} diff --git a/files/data/CMakeLists.txt b/files/data/CMakeLists.txt index f9cc1df16e..cb17f23426 100644 --- a/files/data/CMakeLists.txt +++ b/files/data/CMakeLists.txt @@ -125,30 +125,25 @@ 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/interfaces/combat.lua + scripts/omw/interfaces/combatfunctions.lua + scripts/omw/interfaces/skillfunctions.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..682536ec39 100644 --- a/files/data/builtin.omwscripts +++ b/files/data/builtin.omwscripts @@ -1,3 +1,5 @@ +# NB: data-mw overrides this file! + # UI framework MENU,PLAYER: scripts/omw/mwui/init.lua @@ -8,7 +10,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 +26,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/interfaces/combat.lua # User interface PLAYER: scripts/omw/ui.lua @@ -39,6 +38,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/scripts/omw/interfaces/combat.lua b/files/data/scripts/omw/interfaces/combat.lua new file mode 100644 index 0000000000..f3330dcd25 --- /dev/null +++ b/files/data/scripts/omw/interfaces/combat.lua @@ -0,0 +1,145 @@ +local I = require('openmw.interfaces') +local auxUtil = require('openmw_aux.util') +local functions = require('scripts.omw.interfaces.combatfunctions') + +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 stat name (health, fatigue, or magicka) to 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 = functions.adjustDamageForArmor, + + --- Calculates a difficulty multiplier based on 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 = functions.adjustDamageForDifficulty, + + --- 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 = functions.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 = functions.getArmorRating, + + --- 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 = functions.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 = functions.getSkillAdjustedArmorRating, + + --- 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 = functions.getEffectiveArmorRating, + + --- Spawns a random blood effect at the given position + -- @function [parent=#Combat] spawnBloodEffect + -- @param openmw.util#Vector3 position + spawnBloodEffect = functions.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 = 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 = functions.pickRandomArmor, + + --- @{#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, + }, +} \ No newline at end of file diff --git a/files/data/scripts/omw/interfaces/combatfunctions.lua b/files/data/scripts/omw/interfaces/combatfunctions.lua new file mode 100644 index 0000000000..eb2c6d229e --- /dev/null +++ b/files/data/scripts/omw/interfaces/combatfunctions.lua @@ -0,0 +1,11 @@ +return { + adjustDamageForArmor = function(damage, actor) return damage end, + adjustDamageForDifficulty = function(attack, defendant) end, + applyArmor = function(attack) end, + getArmorRating = function(actor) return 0 end, + getArmorSkill = function(item) return nil end, + getSkillAdjustedArmorRating = function(item, actor) return 0 end, + getEffectiveArmorRating = function(item, actor) return 0 end, + spawnBloodEffect = function(position) end, + pickRandomArmor = function(actor) return nil end +} diff --git a/files/data/scripts/omw/interfaces/skillfunctions.lua b/files/data/scripts/omw/interfaces/skillfunctions.lua new file mode 100644 index 0000000000..93ea1836b6 --- /dev/null +++ b/files/data/scripts/omw/interfaces/skillfunctions.lua @@ -0,0 +1,4 @@ +return { + getSkillProgressRequirement = function(skillid) return 1 end, + getSkillLevelUpOptions = function(skillid, source) return {} end +} 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..4ce211b31a 100644 --- a/files/data/scripts/omw/skillhandlers.lua +++ b/files/data/scripts/omw/skillhandlers.lua @@ -1,10 +1,10 @@ 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 local Skill = core.stats.Skill +local functions = require('scripts.omw.interfaces.skillfunctions') --- -- Table of skill use types defined by Morrowind. @@ -47,40 +47,12 @@ 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 @@ -113,34 +85,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 = functions.getSkillLevelUpOptions(skillid, source) auxUtil.callEventHandlers(skillLevelUpHandlers, skillid, source, options) end @@ -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 = functions.getSkillProgressRequirement }, 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..cd7139cbab 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.interfaces.combat#scripts.omw.interfaces.combat Combat --- -- @field [parent=#interfaces] scripts.omw.mwui.init#scripts.omw.mwui.init MWUI From e978c230dc754ddf0733ecaf21c63312520a7159 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sun, 5 Oct 2025 17:52:46 +0200 Subject: [PATCH 02/11] Override functions by shallow copying the interface instead of overriding files --- docs/source/luadoc_data_paths.sh | 2 +- .../lua-scripting/interface_combat.rst | 2 +- files/data-mw/CMakeLists.txt | 2 - files/data-mw/builtin.omwscripts | 2 +- files/data-mw/scripts/omw/combat/local.lua | 262 +++++++++++++++++ .../omw/interfaces/combatfunctions.lua | 270 ------------------ .../scripts/omw/interfaces/skillfunctions.lua | 66 ----- .../scripts/omw/playerskillhandlers.lua | 66 ++++- files/data/CMakeLists.txt | 4 +- files/data/builtin.omwscripts | 2 +- files/data/openmw_aux/util.lua | 13 + .../combat.lua => combat/interface.lua} | 21 +- .../omw/interfaces/combatfunctions.lua | 11 - .../scripts/omw/interfaces/skillfunctions.lua | 4 - files/data/scripts/omw/skillhandlers.lua | 22 +- files/lua_api/openmw/interfaces.lua | 2 +- 16 files changed, 367 insertions(+), 384 deletions(-) delete mode 100644 files/data-mw/scripts/omw/interfaces/combatfunctions.lua delete mode 100644 files/data-mw/scripts/omw/interfaces/skillfunctions.lua rename files/data/scripts/omw/{interfaces/combat.lua => combat/interface.lua} (92%) delete mode 100644 files/data/scripts/omw/interfaces/combatfunctions.lua delete mode 100644 files/data/scripts/omw/interfaces/skillfunctions.lua diff --git a/docs/source/luadoc_data_paths.sh b/docs/source/luadoc_data_paths.sh index 025485e5cf..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/interfaces/combat.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 5f8271d12c..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_interfaces_combat.html + :file: generated_html/scripts_omw_combat_interface.html diff --git a/files/data-mw/CMakeLists.txt b/files/data-mw/CMakeLists.txt index 60c8bb32aa..75ffe703d3 100644 --- a/files/data-mw/CMakeLists.txt +++ b/files/data-mw/CMakeLists.txt @@ -28,8 +28,6 @@ set(BUILTIN_DATA_MW_FILES scripts/omw/combat/global.lua scripts/omw/combat/local.lua scripts/omw/combat/menu.lua - scripts/omw/interfaces/combatfunctions.lua - scripts/omw/interfaces/skillfunctions.lua scripts/omw/music/helpers.lua scripts/omw/music/music.lua scripts/omw/music/settings.lua diff --git a/files/data-mw/builtin.omwscripts b/files/data-mw/builtin.omwscripts index de6c79fe8a..217bac5509 100644 --- a/files/data-mw/builtin.omwscripts +++ b/files/data-mw/builtin.omwscripts @@ -26,7 +26,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 -NPC,CREATURE,PLAYER: scripts/omw/interfaces/combat.lua +NPC,CREATURE,PLAYER: scripts/omw/combat/interface.lua # User interface PLAYER: scripts/omw/ui.lua diff --git a/files/data-mw/scripts/omw/combat/local.lua b/files/data-mw/scripts/omw/combat/local.lua index 6c90d73b5a..8db8d87301 100644 --- a/files/data-mw/scripts/omw/combat/local.lua +++ b/files/data-mw/scripts/omw/combat/local.lua @@ -1,9 +1,13 @@ 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 Actor = types.Actor 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 @@ -12,12 +16,254 @@ if isPlayer then godMode = function() return require('openmw.debug').isGodMode() end end +local settings = storage.globalSection('SettingsOMWCombat') + +local function getSkill(actor, skillId) + if Creature.objectIsInstance(actor) then + local specialization = core.stats.Skill.record(skillId).specialization + local creatureRecord = Creature.record(actor) + return creatureRecord[specialization..'Skill'] + else + return types.NPC.stats.skills[skillId](actor).modified + end +end + +local armorTypeGmst = { + [Armor.TYPE.Boots] = core.getGMST('iBootsWeight'), + [Armor.TYPE.Cuirass] = core.getGMST('iCuirassWeight'), + [Armor.TYPE.Greaves] = core.getGMST('iGreavesWeight'), + [Armor.TYPE.Helmet] = core.getGMST('iHelmWeight'), + [Armor.TYPE.LBracer] = core.getGMST('iGauntletWeight'), + [Armor.TYPE.LGauntlet] = core.getGMST('iGauntletWeight'), + [Armor.TYPE.LPauldron] = core.getGMST('iPauldronWeight'), + [Armor.TYPE.RBracer] = core.getGMST('iGauntletWeight'), + [Armor.TYPE.RGauntlet] = core.getGMST('iGauntletWeight'), + [Armor.TYPE.RPauldron] = core.getGMST('iPauldronWeight'), + [Armor.TYPE.Shield] = core.getGMST('iShieldWeight'), +} + +local armorSlots = { + Actor.EQUIPMENT_SLOT.Boots, + Actor.EQUIPMENT_SLOT.Cuirass, + Actor.EQUIPMENT_SLOT.Greaves, + Actor.EQUIPMENT_SLOT.Helmet, + Actor.EQUIPMENT_SLOT.LeftGauntlet, + Actor.EQUIPMENT_SLOT.LeftPauldron, + Actor.EQUIPMENT_SLOT.RightGauntlet, + Actor.EQUIPMENT_SLOT.RightPauldron, + Actor.EQUIPMENT_SLOT.CarriedLeft, +} + local function getDamage(attack, what) if attack.damage then return attack.damage[what] or 0 end end +local function setDamage(attack, what, damage) + attack.damage = attack.damage or {} + attack.damage[what] = damage +end + +local function adjustDamageForArmor(damage, actor) + local armor = I.Combat.getArmorRating(actor) + local x = damage / (damage + armor) + return damage * math.max(x, core.getGMST('fCombatArmorMinMult')) +end + +local function adjustDamageForDifficulty(attack, defendant) + local attackerIsPlayer = attack.attacker and Player.objectIsInstance(attack.attacker) + -- The interface guarantees defendant is never nil + local defendantIsPlayer = Player.objectIsInstance(defendant) + -- If both characters are NPCs or both characters are players then + -- difficulty settings do not apply + if attackerIsPlayer == defendantIsPlayer then return end + + local fDifficultyMult = core.getGMST('fDifficultyMult') + local difficultyTerm = core.getGameDifficulty() * 0.01 + local x = 0 + + if defendantIsPlayer then + -- Defending actor is a player + if difficultyTerm > 0 then + x = difficultyTerm * fDifficultyMult + else + x = difficultyTerm / fDifficultyMult + end + elseif attackerIsPlayer then + -- Attacking actor is a player + if difficultyTerm > 0 then + x = -difficultyTerm / fDifficultyMult + else + x = -difficultyTerm * fDifficultyMult + end + end + + setDamage(attack, 'health', getDamage(attack, 'health') * (1 + x)) +end + +local function applyArmor(attack) + local healthDamage = getDamage(attack, 'health') + if healthDamage > 0 then + local healthDamageAdjusted = I.Combat.adjustDamageForArmor(healthDamage) + local diff = math.floor(healthDamageAdjusted - healthDamage) + setDamage(attack, 'health', math.max(healthDamageAdjusted, 1)) + local item = I.Combat.pickRandomArmor() + local skillid = I.Combat.getArmorSkill(item) + if I.SkillProgression then + I.SkillProgression.skillUsed(skillid, {useType = I.SkillProgression.SKILL_USE_TYPES.Armor_HitByOpponent}) + end + if item and Armor.objectIsInstance(item) then + local attackerIsUnarmedCreature = attack.attacker and not attack.weapon and not attack.ammo and Creature.objectIsInstance(attack.attacker) + if settings:get('unarmedCreatureAttacksDamageArmor') or not attackerIsUnarmedCreature then + core.sendGlobalEvent('ModifyItemCondition', { actor = self, item = item, amount = diff }) + end + + if skillid == 'lightarmor' then + core.sound.playSound3d('Light Armor Hit', self) + elseif skillid == 'mediumarmor' then + core.sound.playSound3d('Medium Armor Hit', self) + elseif skillid == 'heavyarmor' then + core.sound.playSound3d('Heavy Armor Hit', self) + else + core.sound.playSound3d('Hand To Hand Hit', self) + end + end + end +end + +local function getArmorRating(actor) + local magicShield = Actor.activeEffects(actor):getEffect(core.magic.EFFECT_TYPE.Shield).magnitude + + if Creature.objectIsInstance(actor) then + return magicShield + end + + local equipment = Actor.getEquipment(actor) + local ratings = {} + local unarmored = getSkill(actor, 'unarmored') + local fUnarmoredBase1 = core.getGMST('fUnarmoredBase1') + local fUnarmoredBase2 = core.getGMST('fUnarmoredBase2') + + for _, v in pairs(armorSlots) do + if equipment[v] and Armor.objectIsInstance(equipment[v]) then + ratings[v] = I.Combat.getEffectiveArmorRating(equipment[v], actor) + else + -- Unarmored + ratings[v] = (fUnarmoredBase1 * unarmored) * (fUnarmoredBase2 * unarmored) + end + end + + return ratings[Actor.EQUIPMENT_SLOT.Cuirass] * 0.3 + + ratings[Actor.EQUIPMENT_SLOT.CarriedLeft] * 0.1 + + ratings[Actor.EQUIPMENT_SLOT.Helmet] * 0.1 + + ratings[Actor.EQUIPMENT_SLOT.Greaves] * 0.1 + + ratings[Actor.EQUIPMENT_SLOT.Boots] * 0.1 + + ratings[Actor.EQUIPMENT_SLOT.LeftPauldron] * 0.1 + + ratings[Actor.EQUIPMENT_SLOT.RightPauldron] * 0.1 + + ratings[Actor.EQUIPMENT_SLOT.LeftGauntlet] * 0.05 + + ratings[Actor.EQUIPMENT_SLOT.RightGauntlet] * 0.05 + + magicShield +end + +local function getArmorSkill(item) + if not item or not Armor.objectIsInstance(item) then + return 'unarmored' + end + local record = Armor.record(item) + local weightGmst = armorTypeGmst[record.type] + local epsilon = 0.0005 + if record.weight <= weightGmst * core.getGMST('fLightMaxMod') + epsilon then + return 'lightarmor' + elseif record.weight <= weightGmst * core.getGMST('fMedMaxMod') + epsilon then + return 'mediumarmor' + else + return 'heavyarmor' + end +end + +local function getSkillAdjustedArmorRating(item, actor) + local record = Armor.record(item) + local skillid = I.Combat.getArmorSkill(item) + local skill = getSkill(actor, skillid) + if record.weight == 0 then + return record.baseArmor + end + return record.baseArmor * skill / core.getGMST('iBaseArmorSkill') +end + +local function getEffectiveArmorRating(item, actor) + local record = Armor.record(item) + local rating = getSkillAdjustedArmorRating(item, actor) + if record.health and record.health ~= 0 then + rating = rating * (types.Item.itemData(item).condition / record.health) + end + return rating +end + +local function spawnBloodEffect(position) + if isPlayer and not settings:get('spawnBloodEffectsOnPlayer') then + return + end + + local bloodEffectModel = string.format('Blood_Model_%d', math.random(0, 2)) -- randIntUniformClosed(0, 2) + + -- TODO: implement a Misc::correctMeshPath equivalent instead? + -- All it ever does it append 'meshes\\' though + bloodEffectModel = 'meshes/'..core.getGMST(bloodEffectModel) + + local record = self.object.type.record(self.object) + local bloodTexture = string.format('Blood_Texture_%d', record.bloodType) + bloodTexture = core.getGMST(bloodTexture) + if not bloodTexture or bloodTexture == '' then + bloodTexture = core.getGMST('Blood_Texture_0') + end + core.sendGlobalEvent('SpawnVfx', { + model = bloodEffectModel, + position = position, + options = { + mwMagicVfx = false, + particleTextureOverride = bloodTexture, + useAmbientLight = false, + }, + }) +end + +local function pickRandomArmor(actor) + local slot = nil + local roll = math.random(0, 99) -- randIntUniform(0, 100) + if roll >= 90 then + slot = Actor.EQUIPMENT_SLOT.CarriedLeft + local item = Actor.getEquipment(actor, slot) + local haveShield = item and Armor.objectIsInstance(item) + if settings:get('redistributeShieldHitsWhenNotWearingShield') and not haveShield then + if roll >= 95 then + slot = Actor.EQUIPMENT_SLOT.Cuirass + else + slot = Actor.EQUIPMENT_SLOT.LeftPauldron + end + end + elseif roll >= 85 then + slot = Actor.EQUIPMENT_SLOT.RightGauntlet + elseif roll >= 80 then + slot = Actor.EQUIPMENT_SLOT.LeftGauntlet + elseif roll >= 70 then + slot = Actor.EQUIPMENT_SLOT.RightPauldron + elseif roll >= 60 then + slot = Actor.EQUIPMENT_SLOT.LeftPauldron + elseif roll >= 50 then + slot = Actor.EQUIPMENT_SLOT.Boots + elseif roll >= 40 then + slot = Actor.EQUIPMENT_SLOT.Greaves + elseif roll >= 30 then + slot = Actor.EQUIPMENT_SLOT.Helmet + else + slot = Actor.EQUIPMENT_SLOT.Cuirass + end + + return Actor.getEquipment(actor, slot) +end + local function onHit(data) if data.successful and not godMode() then I.Combat.applyArmor(data) @@ -35,3 +281,19 @@ local function onHit(data) end 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 + +return { + interfaceName = 'Combat', + interface = interface +} diff --git a/files/data-mw/scripts/omw/interfaces/combatfunctions.lua b/files/data-mw/scripts/omw/interfaces/combatfunctions.lua deleted file mode 100644 index 398c510bf4..0000000000 --- a/files/data-mw/scripts/omw/interfaces/combatfunctions.lua +++ /dev/null @@ -1,270 +0,0 @@ -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 Actor = types.Actor -local Player = types.Player -local Creature = types.Creature -local Armor = types.Armor -local isPlayer = Player.objectIsInstance(self) - -local settings = storage.globalSection('SettingsOMWCombat') - -local function getSkill(actor, skillId) - if Creature.objectIsInstance(actor) then - local specialization = core.stats.Skill.record(skillId).specialization - local creatureRecord = Creature.record(actor) - return creatureRecord[specialization..'Skill'] - else - return types.NPC.stats.skills[skillId](actor).modified - end -end - -local armorTypeGmst = { - [Armor.TYPE.Boots] = core.getGMST('iBootsWeight'), - [Armor.TYPE.Cuirass] = core.getGMST('iCuirassWeight'), - [Armor.TYPE.Greaves] = core.getGMST('iGreavesWeight'), - [Armor.TYPE.Helmet] = core.getGMST('iHelmWeight'), - [Armor.TYPE.LBracer] = core.getGMST('iGauntletWeight'), - [Armor.TYPE.LGauntlet] = core.getGMST('iGauntletWeight'), - [Armor.TYPE.LPauldron] = core.getGMST('iPauldronWeight'), - [Armor.TYPE.RBracer] = core.getGMST('iGauntletWeight'), - [Armor.TYPE.RGauntlet] = core.getGMST('iGauntletWeight'), - [Armor.TYPE.RPauldron] = core.getGMST('iPauldronWeight'), - [Armor.TYPE.Shield] = core.getGMST('iShieldWeight'), -} - -local armorSlots = { - Actor.EQUIPMENT_SLOT.Boots, - Actor.EQUIPMENT_SLOT.Cuirass, - Actor.EQUIPMENT_SLOT.Greaves, - Actor.EQUIPMENT_SLOT.Helmet, - Actor.EQUIPMENT_SLOT.LeftGauntlet, - Actor.EQUIPMENT_SLOT.LeftPauldron, - Actor.EQUIPMENT_SLOT.RightGauntlet, - Actor.EQUIPMENT_SLOT.RightPauldron, - Actor.EQUIPMENT_SLOT.CarriedLeft, -} - -local function getDamage(attack, what) - if attack.damage then - return attack.damage[what] or 0 - end -end - -local function setDamage(attack, what, damage) - attack.damage = attack.damage or {} - attack.damage[what] = damage -end - -local function adjustDamageForArmor(damage, actor) - local armor = I.Combat.getArmorRating(actor) - local x = damage / (damage + armor) - return damage * math.max(x, core.getGMST('fCombatArmorMinMult')) -end - -local function adjustDamageForDifficulty(attack, defendant) - local attackerIsPlayer = attack.attacker and Player.objectIsInstance(attack.attacker) - -- The interface guarantees defendant is never nil - local defendantIsPlayer = Player.objectIsInstance(defendant) - -- If both characters are NPCs or both characters are players then - -- difficulty settings do not apply - if attackerIsPlayer == defendantIsPlayer then return end - - local fDifficultyMult = core.getGMST('fDifficultyMult') - local difficultyTerm = core.getGameDifficulty() * 0.01 - local x = 0 - - if defendantIsPlayer then - -- Defending actor is a player - if difficultyTerm > 0 then - x = difficultyTerm * fDifficultyMult - else - x = difficultyTerm / fDifficultyMult - end - elseif attackerIsPlayer then - -- Attacking actor is a player - if difficultyTerm > 0 then - x = -difficultyTerm / fDifficultyMult - else - x = -difficultyTerm * fDifficultyMult - end - end - - setDamage(attack, 'health', getDamage(attack, 'health') * (1 + x)) -end - -local function applyArmor(attack) - local healthDamage = getDamage(attack, 'health') - if healthDamage > 0 then - local healthDamageAdjusted = I.Combat.adjustDamageForArmor(healthDamage) - local diff = math.floor(healthDamageAdjusted - healthDamage) - setDamage(attack, 'health', math.max(healthDamageAdjusted, 1)) - local item = I.Combat.pickRandomArmor() - local skillid = I.Combat.getArmorSkill(item) - if I.SkillProgression then - I.SkillProgression.skillUsed(skillid, {useType = I.SkillProgression.SKILL_USE_TYPES.Armor_HitByOpponent}) - end - if item and Armor.objectIsInstance(item) then - local attackerIsUnarmedCreature = attack.attacker and not attack.weapon and not attack.ammo and Creature.objectIsInstance(attack.attacker) - if settings:get('unarmedCreatureAttacksDamageArmor') or not attackerIsUnarmedCreature then - core.sendGlobalEvent('ModifyItemCondition', { actor = self, item = item, amount = diff }) - end - - if skillid == 'lightarmor' then - core.sound.playSound3d('Light Armor Hit', self) - elseif skillid == 'mediumarmor' then - core.sound.playSound3d('Medium Armor Hit', self) - elseif skillid == 'heavyarmor' then - core.sound.playSound3d('Heavy Armor Hit', self) - else - core.sound.playSound3d('Hand To Hand Hit', self) - end - end - end -end - -local function getArmorRating(actor) - local magicShield = Actor.activeEffects(actor):getEffect(core.magic.EFFECT_TYPE.Shield).magnitude - - if Creature.objectIsInstance(actor) then - return magicShield - end - - local equipment = Actor.getEquipment(actor) - local ratings = {} - local unarmored = getSkill(actor, 'unarmored') - local fUnarmoredBase1 = core.getGMST('fUnarmoredBase1') - local fUnarmoredBase2 = core.getGMST('fUnarmoredBase2') - - for _, v in pairs(armorSlots) do - if equipment[v] and Armor.objectIsInstance(equipment[v]) then - ratings[v] = I.Combat.getEffectiveArmorRating(equipment[v], actor) - else - -- Unarmored - ratings[v] = (fUnarmoredBase1 * unarmored) * (fUnarmoredBase2 * unarmored) - end - end - - return ratings[Actor.EQUIPMENT_SLOT.Cuirass] * 0.3 - + ratings[Actor.EQUIPMENT_SLOT.CarriedLeft] * 0.1 - + ratings[Actor.EQUIPMENT_SLOT.Helmet] * 0.1 - + ratings[Actor.EQUIPMENT_SLOT.Greaves] * 0.1 - + ratings[Actor.EQUIPMENT_SLOT.Boots] * 0.1 - + ratings[Actor.EQUIPMENT_SLOT.LeftPauldron] * 0.1 - + ratings[Actor.EQUIPMENT_SLOT.RightPauldron] * 0.1 - + ratings[Actor.EQUIPMENT_SLOT.LeftGauntlet] * 0.05 - + ratings[Actor.EQUIPMENT_SLOT.RightGauntlet] * 0.05 - + magicShield -end - -local function getArmorSkill(item) - if not item or not Armor.objectIsInstance(item) then - return 'unarmored' - end - local record = Armor.record(item) - local weightGmst = armorTypeGmst[record.type] - local epsilon = 0.0005 - if record.weight <= weightGmst * core.getGMST('fLightMaxMod') + epsilon then - return 'lightarmor' - elseif record.weight <= weightGmst * core.getGMST('fMedMaxMod') + epsilon then - return 'mediumarmor' - else - return 'heavyarmor' - end -end - -local function getSkillAdjustedArmorRating(item, actor) - local record = Armor.record(item) - local skillid = I.Combat.getArmorSkill(item) - local skill = getSkill(actor, skillid) - if record.weight == 0 then - return record.baseArmor - end - return record.baseArmor * skill / core.getGMST('iBaseArmorSkill') -end - -local function getEffectiveArmorRating(item, actor) - local record = Armor.record(item) - local rating = getSkillAdjustedArmorRating(item, actor) - if record.health and record.health ~= 0 then - rating = rating * (types.Item.itemData(item).condition / record.health) - end - return rating -end - -local function spawnBloodEffect(position) - if isPlayer and not settings:get('spawnBloodEffectsOnPlayer') then - return - end - - local bloodEffectModel = string.format('Blood_Model_%d', math.random(0, 2)) -- randIntUniformClosed(0, 2) - - -- TODO: implement a Misc::correctMeshPath equivalent instead? - -- All it ever does it append 'meshes\\' though - bloodEffectModel = 'meshes/'..core.getGMST(bloodEffectModel) - - local record = self.object.type.record(self.object) - local bloodTexture = string.format('Blood_Texture_%d', record.bloodType) - bloodTexture = core.getGMST(bloodTexture) - if not bloodTexture or bloodTexture == '' then - bloodTexture = core.getGMST('Blood_Texture_0') - end - core.sendGlobalEvent('SpawnVfx', { - model = bloodEffectModel, - position = position, - options = { - mwMagicVfx = false, - particleTextureOverride = bloodTexture, - useAmbientLight = false, - }, - }) -end - -local function pickRandomArmor(actor) - local slot = nil - local roll = math.random(0, 99) -- randIntUniform(0, 100) - if roll >= 90 then - slot = Actor.EQUIPMENT_SLOT.CarriedLeft - local item = Actor.getEquipment(actor, slot) - local haveShield = item and Armor.objectIsInstance(item) - if settings:get('redistributeShieldHitsWhenNotWearingShield') and not haveShield then - if roll >= 95 then - slot = Actor.EQUIPMENT_SLOT.Cuirass - else - slot = Actor.EQUIPMENT_SLOT.LeftPauldron - end - end - elseif roll >= 85 then - slot = Actor.EQUIPMENT_SLOT.RightGauntlet - elseif roll >= 80 then - slot = Actor.EQUIPMENT_SLOT.LeftGauntlet - elseif roll >= 70 then - slot = Actor.EQUIPMENT_SLOT.RightPauldron - elseif roll >= 60 then - slot = Actor.EQUIPMENT_SLOT.LeftPauldron - elseif roll >= 50 then - slot = Actor.EQUIPMENT_SLOT.Boots - elseif roll >= 40 then - slot = Actor.EQUIPMENT_SLOT.Greaves - elseif roll >= 30 then - slot = Actor.EQUIPMENT_SLOT.Helmet - else - slot = Actor.EQUIPMENT_SLOT.Cuirass - end - - return Actor.getEquipment(actor, slot) -end - -return { - adjustDamageForArmor = function(damage, actor) return adjustDamageForArmor(damage, actor or self) end, - adjustDamageForDifficulty = function(attack, defendant) return adjustDamageForDifficulty(attack, defendant or self) end, - applyArmor = applyArmor, - getArmorRating = function(actor) return getArmorRating(actor or self) end, - getArmorSkill = getArmorSkill, - getSkillAdjustedArmorRating = function(item, actor) return getSkillAdjustedArmorRating(item, actor or self) end, - getEffectiveArmorRating = function(item, actor) return getEffectiveArmorRating(item, actor or self) end, - spawnBloodEffect = spawnBloodEffect, - pickRandomArmor = function(actor) return pickRandomArmor(actor or self) end -} diff --git a/files/data-mw/scripts/omw/interfaces/skillfunctions.lua b/files/data-mw/scripts/omw/interfaces/skillfunctions.lua deleted file mode 100644 index 0cb671d1bf..0000000000 --- a/files/data-mw/scripts/omw/interfaces/skillfunctions.lua +++ /dev/null @@ -1,66 +0,0 @@ -local self = require('openmw.self') -local core = require('openmw.core') -local NPC = require('openmw.types').NPC -local Skill = core.stats.Skill - -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 - -return { - getSkillProgressRequirement = getSkillProgressRequirement, - getSkillLevelUpOptions = getSkillLevelUpOptions -} diff --git a/files/data-mw/scripts/omw/playerskillhandlers.lua b/files/data-mw/scripts/omw/playerskillhandlers.lua index b6485bba8e..1146e26551 100644 --- a/files/data-mw/scripts/omw/playerskillhandlers.lua +++ b/files/data-mw/scripts/omw/playerskillhandlers.lua @@ -7,6 +7,64 @@ 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) @@ -119,8 +177,14 @@ 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 cb17f23426..4b1b9e01ca 100644 --- a/files/data/CMakeLists.txt +++ b/files/data/CMakeLists.txt @@ -135,9 +135,7 @@ set(BUILTIN_DATA_FILES scripts/omw/console/local.lua scripts/omw/console/player.lua scripts/omw/console/menu.lua - scripts/omw/interfaces/combat.lua - scripts/omw/interfaces/combatfunctions.lua - scripts/omw/interfaces/skillfunctions.lua + scripts/omw/combat/interface.lua scripts/omw/mechanics/actorcontroller.lua scripts/omw/mechanics/animationcontroller.lua scripts/omw/mechanics/globalcontroller.lua diff --git a/files/data/builtin.omwscripts b/files/data/builtin.omwscripts index 682536ec39..f61182c033 100644 --- a/files/data/builtin.omwscripts +++ b/files/data/builtin.omwscripts @@ -26,7 +26,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 -NPC,CREATURE,PLAYER: scripts/omw/interfaces/combat.lua +NPC,CREATURE,PLAYER: scripts/omw/combat/interface.lua # User interface PLAYER: scripts/omw/ui.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/interfaces/combat.lua b/files/data/scripts/omw/combat/interface.lua similarity index 92% rename from files/data/scripts/omw/interfaces/combat.lua rename to files/data/scripts/omw/combat/interface.lua index f3330dcd25..e96c60ee2b 100644 --- a/files/data/scripts/omw/interfaces/combat.lua +++ b/files/data/scripts/omw/combat/interface.lua @@ -1,6 +1,5 @@ local I = require('openmw.interfaces') local auxUtil = require('openmw_aux.util') -local functions = require('scripts.omw.interfaces.combatfunctions') local onHitHandlers = {} @@ -26,7 +25,7 @@ local onHitHandlers = {} return { --- Basic combat interface -- @module Combat - -- @usage require('openmw.interfaces').Combat + -- @usage local I = require('openmw.interfaces') -- --I.Combat.addOnHitHandler(function(attack) -- -- Adds fatigue loss when hit by draining fatigue when taking health damage @@ -62,7 +61,7 @@ return { -- @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 = functions.adjustDamageForArmor, + adjustDamageForArmor = function(damage, actor) return damage end, --- Calculates a difficulty multiplier based on current difficulty settings -- and adjusts damage accordingly. Has no effect if both this actor and the @@ -70,14 +69,14 @@ return { -- @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 = functions.adjustDamageForDifficulty, + 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 = functions.applyArmor, + applyArmor = function(attack) end, --- Computes this character's armor rating. -- Note that this interface function is read by the engine to update the UI. @@ -85,7 +84,7 @@ return { -- @function [parent=#Combat] getArmorRating -- @param openmw.core#GameObject actor (Optional) The actor to calculate the armor rating for. Defaults to self. -- @return #number - getArmorRating = functions.getArmorRating, + 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). @@ -94,7 +93,7 @@ return { -- @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 = functions.getArmorSkill, + 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. @@ -103,19 +102,19 @@ return { -- @param openmw.core#GameObject item The item -- @param openmw.core#GameObject actor (Optional) The actor, defaults to self -- @return #number - getSkillAdjustedArmorRating = functions.getSkillAdjustedArmorRating, + 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 = functions.getEffectiveArmorRating, + 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 = functions.spawnBloodEffect, + spawnBloodEffect = function(position) end, --- Hit this actor. Normally called as Hit event from the attacking actor, with the same parameters. -- @function [parent=#Combat] onHit @@ -127,7 +126,7 @@ return { -- @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 = functions.pickRandomArmor, + pickRandomArmor = function(actor) return nil end, --- @{#AttackSourceType} -- @field [parent=#Combat] #AttackSourceType ATTACK_SOURCE_TYPES Available attack source types diff --git a/files/data/scripts/omw/interfaces/combatfunctions.lua b/files/data/scripts/omw/interfaces/combatfunctions.lua deleted file mode 100644 index eb2c6d229e..0000000000 --- a/files/data/scripts/omw/interfaces/combatfunctions.lua +++ /dev/null @@ -1,11 +0,0 @@ -return { - adjustDamageForArmor = function(damage, actor) return damage end, - adjustDamageForDifficulty = function(attack, defendant) end, - applyArmor = function(attack) end, - getArmorRating = function(actor) return 0 end, - getArmorSkill = function(item) return nil end, - getSkillAdjustedArmorRating = function(item, actor) return 0 end, - getEffectiveArmorRating = function(item, actor) return 0 end, - spawnBloodEffect = function(position) end, - pickRandomArmor = function(actor) return nil end -} diff --git a/files/data/scripts/omw/interfaces/skillfunctions.lua b/files/data/scripts/omw/interfaces/skillfunctions.lua deleted file mode 100644 index 93ea1836b6..0000000000 --- a/files/data/scripts/omw/interfaces/skillfunctions.lua +++ /dev/null @@ -1,4 +0,0 @@ -return { - getSkillProgressRequirement = function(skillid) return 1 end, - getSkillLevelUpOptions = function(skillid, source) return {} end -} diff --git a/files/data/scripts/omw/skillhandlers.lua b/files/data/scripts/omw/skillhandlers.lua index 4ce211b31a..2e19e8cfeb 100644 --- a/files/data/scripts/omw/skillhandlers.lua +++ b/files/data/scripts/omw/skillhandlers.lua @@ -4,7 +4,6 @@ local core = require('openmw.core') local auxUtil = require('openmw_aux.util') local NPC = require('openmw.types').NPC local Skill = core.stats.Skill -local functions = require('scripts.omw.interfaces.skillfunctions') --- -- Table of skill use types defined by Morrowind. @@ -47,12 +46,6 @@ local functions = require('scripts.omw.interfaces.skillfunctions') local skillUsedHandlers = {} local skillLevelUpHandlers = {} -local function shallowCopy(t1) - local t2 = {} - for key, value in pairs(t1) do t2[key] = value end - return t2 -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 @@ -60,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 @@ -85,7 +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 options = functions.getSkillLevelUpOptions(skillid, source) + local options = I.SkillProgression.getSkillLevelUpOptions(skillid, source) auxUtil.callEventHandlers(skillLevelUpHandlers, skillid, source, options) end @@ -126,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. @@ -213,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 @@ -226,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 = functions.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 cd7139cbab..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.interfaces.combat#scripts.omw.interfaces.combat 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 From 189eff7a49b89ec388f5adfd6e6f8350a21c2603 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Mon, 6 Oct 2025 19:19:43 +0200 Subject: [PATCH 03/11] Use cmake to create vfs-mw/builtin.omwscripts --- files/data-mw/CMakeLists.txt | 12 +++++-- files/data-mw/builtin-extra.omwscripts | 8 +++++ files/data-mw/builtin.omwscripts | 50 -------------------------- files/data/builtin.omwscripts | 2 -- 4 files changed, 18 insertions(+), 54 deletions(-) create mode 100644 files/data-mw/builtin-extra.omwscripts delete mode 100644 files/data-mw/builtin.omwscripts diff --git a/files/data-mw/CMakeLists.txt b/files/data-mw/CMakeLists.txt index 75ffe703d3..2c0a589bd6 100644 --- a/files/data-mw/CMakeLists.txt +++ b/files/data-mw/CMakeLists.txt @@ -21,8 +21,6 @@ set(BUILTIN_DATA_MW_FILES # Game-specific settings for calendar.lua openmw_aux/calendarconfig.lua - builtin.omwscripts - scripts/omw/cellhandlers.lua scripts/omw/combat/common.lua scripts/omw/combat/global.lua @@ -37,3 +35,13 @@ set(BUILTIN_DATA_MW_FILES 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-extra.omwscripts to create vfs-mw/builtin.omwscripts +set(builtinBase "${CMAKE_CURRENT_SOURCE_DIR}/../data/builtin.omwscripts") +set(builtinExtra "${CMAKE_CURRENT_SOURCE_DIR}/builtin-extra.omwscripts") +set(builtinTemp "${CMAKE_BINARY_DIR}/tmp") +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${builtinBase}" "${builtinExtra}") +file(COPY "${builtinBase}" DESTINATION "${builtinTemp}") +file(READ "${builtinExtra}" builtinContents) +file(APPEND "${builtinTemp}/builtin.omwscripts" "${builtinContents}") +copy_resource_file("${builtinTemp}/builtin.omwscripts" "${OPENMW_RESOURCES_ROOT}" "resources/vfs-mw/builtin.omwscripts") diff --git a/files/data-mw/builtin-extra.omwscripts b/files/data-mw/builtin-extra.omwscripts new file mode 100644 index 0000000000..0d63fd1dde --- /dev/null +++ b/files/data-mw/builtin-extra.omwscripts @@ -0,0 +1,8 @@ +# 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-mw/builtin.omwscripts b/files/data-mw/builtin.omwscripts deleted file mode 100644 index 217bac5509..0000000000 --- a/files/data-mw/builtin.omwscripts +++ /dev/null @@ -1,50 +0,0 @@ -# NB: This file is a superset of the one in data! - -# UI framework -MENU,PLAYER: scripts/omw/mwui/init.lua - -# Settings framework -MENU: scripts/omw/settings/menu.lua -PLAYER: scripts/omw/settings/player.lua -GLOBAL: scripts/omw/settings/global.lua - -# Mechanics -GLOBAL: scripts/omw/activationhandlers.lua -GLOBAL: scripts/omw/usehandlers.lua -GLOBAL: scripts/omw/worldeventhandlers.lua -GLOBAL: scripts/omw/crimes.lua -CREATURE, NPC, PLAYER: scripts/omw/mechanics/animationcontroller.lua -PLAYER: scripts/omw/skillhandlers.lua -PLAYER: scripts/omw/mechanics/playercontroller.lua -MENU: scripts/omw/camera/settings.lua -MENU: scripts/omw/input/settings.lua -PLAYER: scripts/omw/input/playercontrols.lua -PLAYER: scripts/omw/camera/camera.lua -PLAYER: scripts/omw/input/actionbindings.lua -PLAYER: scripts/omw/input/smoothmovement.lua -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 -NPC,CREATURE,PLAYER: scripts/omw/combat/interface.lua - -# User interface -PLAYER: scripts/omw/ui.lua - -# Lua console -MENU: scripts/omw/console/menu.lua -PLAYER: scripts/omw/console/player.lua -GLOBAL: scripts/omw/console/global.lua -CUSTOM: scripts/omw/console/local.lua - -# Music system -NPC,CREATURE: scripts/omw/music/actor.lua - -# Game specific -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/builtin.omwscripts b/files/data/builtin.omwscripts index f61182c033..6a3899accd 100644 --- a/files/data/builtin.omwscripts +++ b/files/data/builtin.omwscripts @@ -1,5 +1,3 @@ -# NB: data-mw overrides this file! - # UI framework MENU,PLAYER: scripts/omw/mwui/init.lua From c6c515f7705080f66ab7819117e7907a00aba269 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Mon, 6 Oct 2025 19:21:29 +0200 Subject: [PATCH 04/11] Make ATTACK_SOURCE_TYPES read only --- files/data/scripts/omw/combat/interface.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/files/data/scripts/omw/combat/interface.lua b/files/data/scripts/omw/combat/interface.lua index e96c60ee2b..1e18f58dc4 100644 --- a/files/data/scripts/omw/combat/interface.lua +++ b/files/data/scripts/omw/combat/interface.lua @@ -1,4 +1,5 @@ local I = require('openmw.interfaces') +local util = require('openmw.util') local auxUtil = require('openmw_aux.util') local onHitHandlers = {} @@ -130,12 +131,12 @@ return { --- @{#AttackSourceType} -- @field [parent=#Combat] #AttackSourceType ATTACK_SOURCE_TYPES Available attack source types - ATTACK_SOURCE_TYPES = { + ATTACK_SOURCE_TYPES = util.makeStrictReadOnly({ Magic = 'magic', Melee = 'melee', Ranged = 'ranged', Unspecified = 'unspecified', - }, + }), }, eventHandlers = { From 10c96ff68ddac4ed0b8231efef8f7a82b57de5c2 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Mon, 6 Oct 2025 20:23:36 +0200 Subject: [PATCH 05/11] Use configure_file substitution --- files/data-mw/CMakeLists.txt | 10 +++------- files/data-mw/builtin-extra.omwscripts | 2 ++ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/files/data-mw/CMakeLists.txt b/files/data-mw/CMakeLists.txt index 2c0a589bd6..620a40bc02 100644 --- a/files/data-mw/CMakeLists.txt +++ b/files/data-mw/CMakeLists.txt @@ -38,10 +38,6 @@ endforeach (f) # Concat data/builtin.omwscripts and data-mw/builtin-extra.omwscripts to create vfs-mw/builtin.omwscripts set(builtinBase "${CMAKE_CURRENT_SOURCE_DIR}/../data/builtin.omwscripts") -set(builtinExtra "${CMAKE_CURRENT_SOURCE_DIR}/builtin-extra.omwscripts") -set(builtinTemp "${CMAKE_BINARY_DIR}/tmp") -set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${builtinBase}" "${builtinExtra}") -file(COPY "${builtinBase}" DESTINATION "${builtinTemp}") -file(READ "${builtinExtra}" builtinContents) -file(APPEND "${builtinTemp}/builtin.omwscripts" "${builtinContents}") -copy_resource_file("${builtinTemp}/builtin.omwscripts" "${OPENMW_RESOURCES_ROOT}" "resources/vfs-mw/builtin.omwscripts") +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${builtinBase}") +file(READ "${builtinBase}" BUILTIN_SCRIPTS) +configure_resource_file("${CMAKE_CURRENT_SOURCE_DIR}/builtin-extra.omwscripts" "${OPENMW_RESOURCES_ROOT}" "resources/vfs-mw/builtin.omwscripts") diff --git a/files/data-mw/builtin-extra.omwscripts b/files/data-mw/builtin-extra.omwscripts index 0d63fd1dde..ffcadbf69f 100644 --- a/files/data-mw/builtin-extra.omwscripts +++ b/files/data-mw/builtin-extra.omwscripts @@ -1,3 +1,5 @@ +@BUILTIN_SCRIPTS@ + # Game specific scripts to append to builtin.omwscripts GLOBAL: scripts/omw/cellhandlers.lua GLOBAL: scripts/omw/combat/global.lua From e66d43c083b7ad60b0d399fa555df15bec73abc3 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Tue, 7 Oct 2025 19:20:58 +0200 Subject: [PATCH 06/11] Reduce the diff --- files/data-mw/scripts/omw/combat/local.lua | 260 ++++++++++----------- 1 file changed, 130 insertions(+), 130 deletions(-) diff --git a/files/data-mw/scripts/omw/combat/local.lua b/files/data-mw/scripts/omw/combat/local.lua index 8db8d87301..77f43e2901 100644 --- a/files/data-mw/scripts/omw/combat/local.lua +++ b/files/data-mw/scripts/omw/combat/local.lua @@ -54,82 +54,39 @@ local armorSlots = { Actor.EQUIPMENT_SLOT.CarriedLeft, } -local function getDamage(attack, what) - if attack.damage then - return attack.damage[what] or 0 +local function getArmorSkill(item) + if not item or not Armor.objectIsInstance(item) then + return 'unarmored' + end + local record = Armor.record(item) + local weightGmst = armorTypeGmst[record.type] + local epsilon = 0.0005 + if record.weight <= weightGmst * core.getGMST('fLightMaxMod') + epsilon then + return 'lightarmor' + elseif record.weight <= weightGmst * core.getGMST('fMedMaxMod') + epsilon then + return 'mediumarmor' + else + return 'heavyarmor' end end -local function setDamage(attack, what, damage) - attack.damage = attack.damage or {} - attack.damage[what] = damage -end - -local function adjustDamageForArmor(damage, actor) - local armor = I.Combat.getArmorRating(actor) - local x = damage / (damage + armor) - return damage * math.max(x, core.getGMST('fCombatArmorMinMult')) -end - -local function adjustDamageForDifficulty(attack, defendant) - local attackerIsPlayer = attack.attacker and Player.objectIsInstance(attack.attacker) - -- The interface guarantees defendant is never nil - local defendantIsPlayer = Player.objectIsInstance(defendant) - -- If both characters are NPCs or both characters are players then - -- difficulty settings do not apply - if attackerIsPlayer == defendantIsPlayer then return end - - local fDifficultyMult = core.getGMST('fDifficultyMult') - local difficultyTerm = core.getGameDifficulty() * 0.01 - local x = 0 - - if defendantIsPlayer then - -- Defending actor is a player - if difficultyTerm > 0 then - x = difficultyTerm * fDifficultyMult - else - x = difficultyTerm / fDifficultyMult - end - elseif attackerIsPlayer then - -- Attacking actor is a player - if difficultyTerm > 0 then - x = -difficultyTerm / fDifficultyMult - else - x = -difficultyTerm * fDifficultyMult - end +local function getSkillAdjustedArmorRating(item, actor) + local record = Armor.record(item) + local skillid = I.Combat.getArmorSkill(item) + local skill = getSkill(actor, skillid) + if record.weight == 0 then + return record.baseArmor end - - setDamage(attack, 'health', getDamage(attack, 'health') * (1 + x)) + return record.baseArmor * skill / core.getGMST('iBaseArmorSkill') end -local function applyArmor(attack) - local healthDamage = getDamage(attack, 'health') - if healthDamage > 0 then - local healthDamageAdjusted = I.Combat.adjustDamageForArmor(healthDamage) - local diff = math.floor(healthDamageAdjusted - healthDamage) - setDamage(attack, 'health', math.max(healthDamageAdjusted, 1)) - local item = I.Combat.pickRandomArmor() - local skillid = I.Combat.getArmorSkill(item) - if I.SkillProgression then - I.SkillProgression.skillUsed(skillid, {useType = I.SkillProgression.SKILL_USE_TYPES.Armor_HitByOpponent}) - end - if item and Armor.objectIsInstance(item) then - local attackerIsUnarmedCreature = attack.attacker and not attack.weapon and not attack.ammo and Creature.objectIsInstance(attack.attacker) - if settings:get('unarmedCreatureAttacksDamageArmor') or not attackerIsUnarmedCreature then - core.sendGlobalEvent('ModifyItemCondition', { actor = self, item = item, amount = diff }) - end - - if skillid == 'lightarmor' then - core.sound.playSound3d('Light Armor Hit', self) - elseif skillid == 'mediumarmor' then - core.sound.playSound3d('Medium Armor Hit', self) - elseif skillid == 'heavyarmor' then - core.sound.playSound3d('Heavy Armor Hit', self) - else - core.sound.playSound3d('Hand To Hand Hit', self) - end - end +local function getEffectiveArmorRating(item, actor) + local record = Armor.record(item) + local rating = getSkillAdjustedArmorRating(item, actor) + if record.health and record.health ~= 0 then + rating = rating * (types.Item.itemData(item).condition / record.health) end + return rating end local function getArmorRating(actor) @@ -166,67 +123,10 @@ local function getArmorRating(actor) + magicShield end -local function getArmorSkill(item) - if not item or not Armor.objectIsInstance(item) then - return 'unarmored' - end - local record = Armor.record(item) - local weightGmst = armorTypeGmst[record.type] - local epsilon = 0.0005 - if record.weight <= weightGmst * core.getGMST('fLightMaxMod') + epsilon then - return 'lightarmor' - elseif record.weight <= weightGmst * core.getGMST('fMedMaxMod') + epsilon then - return 'mediumarmor' - else - return 'heavyarmor' - end -end - -local function getSkillAdjustedArmorRating(item, actor) - local record = Armor.record(item) - local skillid = I.Combat.getArmorSkill(item) - local skill = getSkill(actor, skillid) - if record.weight == 0 then - return record.baseArmor - end - return record.baseArmor * skill / core.getGMST('iBaseArmorSkill') -end - -local function getEffectiveArmorRating(item, actor) - local record = Armor.record(item) - local rating = getSkillAdjustedArmorRating(item, actor) - if record.health and record.health ~= 0 then - rating = rating * (types.Item.itemData(item).condition / record.health) - end - return rating -end - -local function spawnBloodEffect(position) - if isPlayer and not settings:get('spawnBloodEffectsOnPlayer') then - return - end - - local bloodEffectModel = string.format('Blood_Model_%d', math.random(0, 2)) -- randIntUniformClosed(0, 2) - - -- TODO: implement a Misc::correctMeshPath equivalent instead? - -- All it ever does it append 'meshes\\' though - bloodEffectModel = 'meshes/'..core.getGMST(bloodEffectModel) - - local record = self.object.type.record(self.object) - local bloodTexture = string.format('Blood_Texture_%d', record.bloodType) - bloodTexture = core.getGMST(bloodTexture) - if not bloodTexture or bloodTexture == '' then - bloodTexture = core.getGMST('Blood_Texture_0') - end - core.sendGlobalEvent('SpawnVfx', { - model = bloodEffectModel, - position = position, - options = { - mwMagicVfx = false, - particleTextureOverride = bloodTexture, - useAmbientLight = false, - }, - }) +local function adjustDamageForArmor(damage, actor) + local armor = I.Combat.getArmorRating(actor) + local x = damage / (damage + armor) + return damage * math.max(x, core.getGMST('fCombatArmorMinMult')) end local function pickRandomArmor(actor) @@ -264,6 +164,106 @@ local function pickRandomArmor(actor) return Actor.getEquipment(actor, slot) end +local function getDamage(attack, what) + if attack.damage then + return attack.damage[what] or 0 + end +end + +local function setDamage(attack, what, damage) + attack.damage = attack.damage or {} + attack.damage[what] = damage +end + +local function applyArmor(attack) + local healthDamage = getDamage(attack, 'health') + if healthDamage > 0 then + local healthDamageAdjusted = I.Combat.adjustDamageForArmor(healthDamage) + local diff = math.floor(healthDamageAdjusted - healthDamage) + setDamage(attack, 'health', math.max(healthDamageAdjusted, 1)) + local item = I.Combat.pickRandomArmor() + local skillid = I.Combat.getArmorSkill(item) + if I.SkillProgression then + I.SkillProgression.skillUsed(skillid, {useType = I.SkillProgression.SKILL_USE_TYPES.Armor_HitByOpponent}) + end + if item and Armor.objectIsInstance(item) then + local attackerIsUnarmedCreature = attack.attacker and not attack.weapon and not attack.ammo and Creature.objectIsInstance(attack.attacker) + if settings:get('unarmedCreatureAttacksDamageArmor') or not attackerIsUnarmedCreature then + core.sendGlobalEvent('ModifyItemCondition', { actor = self, item = item, amount = diff }) + end + + if skillid == 'lightarmor' then + core.sound.playSound3d('Light Armor Hit', self) + elseif skillid == 'mediumarmor' then + core.sound.playSound3d('Medium Armor Hit', self) + elseif skillid == 'heavyarmor' then + core.sound.playSound3d('Heavy Armor Hit', self) + else + core.sound.playSound3d('Hand To Hand Hit', self) + end + end + end +end + +local function adjustDamageForDifficulty(attack, defendant) + local attackerIsPlayer = attack.attacker and Player.objectIsInstance(attack.attacker) + -- The interface guarantees defendant is never nil + local defendantIsPlayer = Player.objectIsInstance(defendant) + -- If both characters are NPCs or both characters are players then + -- difficulty settings do not apply + if attackerIsPlayer == defendantIsPlayer then return end + + local fDifficultyMult = core.getGMST('fDifficultyMult') + local difficultyTerm = core.getGameDifficulty() * 0.01 + local x = 0 + + if defendantIsPlayer then + -- Defending actor is a player + if difficultyTerm > 0 then + x = difficultyTerm * fDifficultyMult + else + x = difficultyTerm / fDifficultyMult + end + elseif attackerIsPlayer then + -- Attacking actor is a player + if difficultyTerm > 0 then + x = -difficultyTerm / fDifficultyMult + else + x = -difficultyTerm * fDifficultyMult + end + end + + setDamage(attack, 'health', getDamage(attack, 'health') * (1 + x)) +end + +local function spawnBloodEffect(position) + if isPlayer and not settings:get('spawnBloodEffectsOnPlayer') then + return + end + + local bloodEffectModel = string.format('Blood_Model_%d', math.random(0, 2)) -- randIntUniformClosed(0, 2) + + -- TODO: implement a Misc::correctMeshPath equivalent instead? + -- All it ever does it append 'meshes\\' though + bloodEffectModel = 'meshes/'..core.getGMST(bloodEffectModel) + + local record = self.object.type.record(self.object) + local bloodTexture = string.format('Blood_Texture_%d', record.bloodType) + bloodTexture = core.getGMST(bloodTexture) + if not bloodTexture or bloodTexture == '' then + bloodTexture = core.getGMST('Blood_Texture_0') + end + core.sendGlobalEvent('SpawnVfx', { + model = bloodEffectModel, + position = position, + options = { + mwMagicVfx = false, + particleTextureOverride = bloodTexture, + useAmbientLight = false, + }, + }) +end + local function onHit(data) if data.successful and not godMode() then I.Combat.applyArmor(data) From a501e1ad1ca70b1db864ae3fb452df1b896506ea Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Tue, 7 Oct 2025 19:21:09 +0200 Subject: [PATCH 07/11] Workaround a cmake/ninja issue --- files/data-mw/CMakeLists.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/files/data-mw/CMakeLists.txt b/files/data-mw/CMakeLists.txt index 620a40bc02..fdb510b284 100644 --- a/files/data-mw/CMakeLists.txt +++ b/files/data-mw/CMakeLists.txt @@ -38,6 +38,11 @@ endforeach (f) # Concat data/builtin.omwscripts and data-mw/builtin-extra.omwscripts to create vfs-mw/builtin.omwscripts set(builtinBase "${CMAKE_CURRENT_SOURCE_DIR}/../data/builtin.omwscripts") -set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${builtinBase}") + +# 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-extra.omwscripts" "${OPENMW_RESOURCES_ROOT}" "resources/vfs-mw/builtin.omwscripts") From f8ef80c52291c77a52d71ad8c5a20123559bf5b8 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Fri, 17 Oct 2025 17:33:15 +0200 Subject: [PATCH 08/11] Rename builtin template --- files/data-mw/CMakeLists.txt | 4 ++-- .../{builtin-extra.omwscripts => builtin.omwscripts.in} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename files/data-mw/{builtin-extra.omwscripts => builtin.omwscripts.in} (100%) diff --git a/files/data-mw/CMakeLists.txt b/files/data-mw/CMakeLists.txt index fdb510b284..fd36d6d314 100644 --- a/files/data-mw/CMakeLists.txt +++ b/files/data-mw/CMakeLists.txt @@ -36,7 +36,7 @@ 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-extra.omwscripts to create vfs-mw/builtin.omwscripts +# 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 @@ -45,4 +45,4 @@ if (NOT CMAKE_GENERATOR MATCHES "Ninja") endif() file(READ "${builtinBase}" BUILTIN_SCRIPTS) -configure_resource_file("${CMAKE_CURRENT_SOURCE_DIR}/builtin-extra.omwscripts" "${OPENMW_RESOURCES_ROOT}" "resources/vfs-mw/builtin.omwscripts") +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-extra.omwscripts b/files/data-mw/builtin.omwscripts.in similarity index 100% rename from files/data-mw/builtin-extra.omwscripts rename to files/data-mw/builtin.omwscripts.in From dcde01dee9067e3d878c603da8765f92b3c6d388 Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Sat, 8 Nov 2025 14:13:56 +0100 Subject: [PATCH 09/11] Note that getArmorSkill can return nil --- files/data/scripts/omw/combat/interface.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/data/scripts/omw/combat/interface.lua b/files/data/scripts/omw/combat/interface.lua index 1e18f58dc4..fe90007e03 100644 --- a/files/data/scripts/omw/combat/interface.lua +++ b/files/data/scripts/omw/combat/interface.lua @@ -93,7 +93,7 @@ return { -- 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} + -- @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 From b4018b1962214e651dc084812f306d5370ec69ea Mon Sep 17 00:00:00 2001 From: Evil Eye Date: Mon, 24 Nov 2025 17:59:50 +0100 Subject: [PATCH 10/11] Absorb changes from !4996 --- .../scripts/omw/playerskillhandlers.lua | 18 +++++++++--------- files/data/scripts/omw/combat/interface.lua | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/files/data-mw/scripts/omw/playerskillhandlers.lua b/files/data-mw/scripts/omw/playerskillhandlers.lua index 1146e26551..d070dd0311 100644 --- a/files/data-mw/scripts/omw/playerskillhandlers.lua +++ b/files/data-mw/scripts/omw/playerskillhandlers.lua @@ -68,15 +68,15 @@ 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 + 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 @@ -106,11 +106,11 @@ local function skillLevelUpHandler(skillid, source, params) 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 @@ -119,14 +119,14 @@ 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)] @@ -151,7 +151,7 @@ local function jailTimeServed(days) message = message..'\n'..string.format(skillMsg, skillRecord.name, skillStat(self).base) end end - + I.UI.showInteractiveMessage(message) end diff --git a/files/data/scripts/omw/combat/interface.lua b/files/data/scripts/omw/combat/interface.lua index fe90007e03..b6471997ff 100644 --- a/files/data/scripts/omw/combat/interface.lua +++ b/files/data/scripts/omw/combat/interface.lua @@ -14,7 +14,7 @@ local onHitHandlers = {} --- -- @type AttackInfo --- @field [parent=#AttackInfo] #table damage A table mapping stat name (health, fatigue, or magicka) to 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] #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. @@ -48,7 +48,7 @@ return { --- 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 + -- 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) @@ -64,7 +64,7 @@ return { -- @return #number Damage adjusted for armor adjustDamageForArmor = function(damage, actor) return damage end, - --- Calculates a difficulty multiplier based on current difficulty settings + --- 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 From f0b7fec7bb59377f953e0c837ae0d426b2c9050c Mon Sep 17 00:00:00 2001 From: Alexei Kotov Date: Mon, 24 Nov 2025 21:22:40 +0300 Subject: [PATCH 11/11] Bump Lua API revision --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 "")