mirror of
https://gitlab.com/OpenMW/openmw.git
synced 2025-09-11 21:36:40 -04:00
432 lines
18 KiB
Lua
432 lines
18 KiB
Lua
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 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)
|
|
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 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 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 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)
|
|
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 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)
|
|
for i = #onHitHandlers, 1, -1 do
|
|
if onHitHandlers[i](data) == false then
|
|
return -- skip other handlers
|
|
end
|
|
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 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 = function(damage, actor) return adjustDamageForArmor(damage, actor or self) end,
|
|
|
|
--- 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 = 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,
|
|
},
|
|
}
|