474 lines
16 KiB
Plaintext
474 lines
16 KiB
Plaintext
|
-- Utility scripts to help calculate things.
|
||
|
|
||
|
get_config = ballistics_mcm.get_config
|
||
|
print_dbg = ballistics_mcm.print_dbg
|
||
|
|
||
|
ini_ammo = ini_file("ammo\\importer.ltx")
|
||
|
ini_damage = ini_file("creatures\\damages.ltx")
|
||
|
|
||
|
ini_sounds = ini_file("ammo\\sounds.ltx")
|
||
|
|
||
|
Bone_IDs = {
|
||
|
[1] = "bip01_pelvis",
|
||
|
[2] = "bip01_pelvis",
|
||
|
[3] = "bip01_l_thigh",
|
||
|
[4] = "bip01_l_calf",
|
||
|
[5] = "bip01_l_foot",
|
||
|
[6] = "bip01_l_foot",
|
||
|
[7] = "bip01_r_thigh",
|
||
|
[8] = "bip01_r_calf",
|
||
|
[9] = "bip01_r_foot",
|
||
|
[10] = "bip01_r_foot",
|
||
|
[11] = "bip01_spine",
|
||
|
[12] = "bip01_spine1",
|
||
|
[13] = "bip01_spine2",
|
||
|
-- head
|
||
|
[14] = "bip01_neck",
|
||
|
[15] = "bip01_head",
|
||
|
[16] = "eye_left",
|
||
|
[17] = "eye_right",
|
||
|
[18] = "eyelid_1",
|
||
|
[19] = "jaw_1",
|
||
|
-- endhead
|
||
|
[20] = "bip01_l_clavicle",
|
||
|
[21] = "bip01_l_upperarm",
|
||
|
[22] = "bip01_l_forearm",
|
||
|
[23] = "bip01_l_hand",
|
||
|
[24] = "bip01_l_hand",
|
||
|
[25] = "bip01_l_hand",
|
||
|
[26] = "bip01_l_hand",
|
||
|
[27] = "bip01_l_hand",
|
||
|
[28] = "bip01_l_hand",
|
||
|
[29] = "bip01_l_hand",
|
||
|
[30] = "bip01_l_hand",
|
||
|
[31] = "bip01_l_hand",
|
||
|
[32] = "bip01_l_hand",
|
||
|
[33] = "bip01_r_clavicle",
|
||
|
[34] = "bip01_r_upperarm",
|
||
|
[35] = "bip01_r_forearm",
|
||
|
[36] = "bip01_r_hand",
|
||
|
[37] = "bip01_r_hand",
|
||
|
[38] = "bip01_r_hand",
|
||
|
[39] = "bip01_r_hand",
|
||
|
[40] = "bip01_r_hand",
|
||
|
[41] = "bip01_r_hand",
|
||
|
[42] = "bip01_r_hand",
|
||
|
[43] = "bip01_r_hand",
|
||
|
[44] = "bip01_r_hand",
|
||
|
[45] = "bip01_r_hand",
|
||
|
[46] = "bip01_pelvis",
|
||
|
}
|
||
|
|
||
|
head_bones = {
|
||
|
|
||
|
[14] = "bip01_neck",
|
||
|
[15] = "bip01_head",
|
||
|
[16] = "eye_left",
|
||
|
[17] = "eye_right",
|
||
|
[18] = "eyelid_1",
|
||
|
[19] = "jaw_1",
|
||
|
}
|
||
|
|
||
|
center_mass = {
|
||
|
|
||
|
[1] = "bip01_pelvis",
|
||
|
[2] = "bip01_pelvis",
|
||
|
[11] = "bip01_spine",
|
||
|
[12] = "bip01_spine1",
|
||
|
[13] = "bip01_spine2",
|
||
|
[20] = "bip01_l_clavicle",
|
||
|
[33] = "bip01_r_clavicle",
|
||
|
[46] = "bip01_pelvis",
|
||
|
}
|
||
|
|
||
|
|
||
|
limbs = {
|
||
|
[3] = "bip01_l_thigh",
|
||
|
[4] = "bip01_l_calf",
|
||
|
[6] = "bip01_l_toe0",
|
||
|
[7] = "bip01_r_thigh",
|
||
|
[8] = "bip01_r_calf",
|
||
|
[10] = "bip01_r_toe0",
|
||
|
[22] = "bip01_l_forearm",
|
||
|
[24] = "bip01_l_finger0",
|
||
|
[25] = "bip01_l_finger01",
|
||
|
[26] = "bip01_l_finger02",
|
||
|
[27] = "bip01_l_finger1",
|
||
|
[28] = "bip01_l_finger11",
|
||
|
[29] = "bip01_l_finger12",
|
||
|
[30] = "bip01_l_finger2",
|
||
|
[31] = "bip01_l_finger21",
|
||
|
[32] = "bip01_l_finger22",
|
||
|
[35] = "bip01_r_forearm",
|
||
|
[37] = "bip01_r_finger0",
|
||
|
[38] = "bip01_r_finger01",
|
||
|
[39] = "bip01_r_finger02",
|
||
|
[40] = "bip01_r_finger1",
|
||
|
[41] = "bip01_r_finger11",
|
||
|
[42] = "bip01_r_finger12",
|
||
|
[43] = "bip01_r_finger2",
|
||
|
[44] = "bip01_r_finger21",
|
||
|
[45] = "bip01_r_finger22",
|
||
|
}
|
||
|
|
||
|
|
||
|
-- play particle at npc bone
|
||
|
function play_particle(npc, bone_id, name, duration)
|
||
|
bone_id = (not bone_id or bone_id == 65535) and 1 or bone_id
|
||
|
bone_name = Bone_IDs[bone_id] or "bip01_spine"
|
||
|
duration = duration or 1
|
||
|
local particle = particles_object(name)
|
||
|
local bone_pos = npc:bone_position(bone_name)
|
||
|
|
||
|
if particle and not particle:playing() then
|
||
|
particle:play_at_pos(bone_pos)
|
||
|
end
|
||
|
local timeout = time_global() + (duration * 1000)
|
||
|
CreateTimeEvent(npc:id(), name, 0, function(npc, bone, particle, timeout)
|
||
|
if time_global() > timeout then
|
||
|
return true
|
||
|
end
|
||
|
local bp = npc:bone_position(bone)
|
||
|
if not particle:playing() then
|
||
|
particle:play_at_pos(bp)
|
||
|
end
|
||
|
particle:move_to(bp, VEC_Z)
|
||
|
end, npc, bone_name, particle, timeout)
|
||
|
end
|
||
|
|
||
|
-- play a particle at the weapon bone firepoint. good for emulating fancier muzzle fx
|
||
|
function play_particle_firepoint(name)
|
||
|
local wpn = db.actor:active_item()
|
||
|
if not wpn then return end
|
||
|
|
||
|
last_particle = particles_object(name)
|
||
|
if last_particle and not last_particle:playing() then
|
||
|
local hud = utils_data.read_from_ini(nil,wpn:section(),"hud","string",nil)
|
||
|
local fire_bone = utils_data.read_from_ini(nil,hud,"fire_bone","string",nil) or "wpn_body"
|
||
|
local offset = utils_data.read_from_ini(nil,hud,"fire_point","string",nil) or VEC_ZERO
|
||
|
offset = offset and utils_data.string_to_vector(offset)
|
||
|
last_particle:play_at_pos( wpn:bone_position(fire_bone, true), offset )
|
||
|
CreateTimeEvent("particle_stop", math.random(30), 0.7,
|
||
|
function()
|
||
|
last_particle:stop()
|
||
|
return true
|
||
|
end)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- play some sounds that scale based on distance
|
||
|
function play_sound_on_location(snd, npc, distance)
|
||
|
if not snd or not npc or not ini_sounds:section_exist(snd) then
|
||
|
return
|
||
|
end
|
||
|
distance = distance or get_distance(db.actor, npc)
|
||
|
play_sound_distance(snd, distance)
|
||
|
end
|
||
|
|
||
|
function play_sound_distance(snd, distance)
|
||
|
to_use = distance > 10 and ini_sounds:r_string_ex(snd, "far") and "far" or "near"
|
||
|
snd_to_play = ini_sounds:r_string_ex(snd, to_use)
|
||
|
if not snd_to_play then return end
|
||
|
snd_to_play = snd_to_play .. math.random(ini_sounds:r_float_ex(snd, to_use.."_amt") or 1)
|
||
|
local s = xr_sound.get_safe_sound_object(snd_to_play)
|
||
|
if s then
|
||
|
-- print_dbg("Playing %s", snd_to_play)
|
||
|
s:play(db.actor, 0, sound_object.s2d)
|
||
|
-- snd.volume = 1
|
||
|
s.volume = (distance > 10) and 0.3 + (0.7 * (1 - (clamp(distance, 2, 100) - 2)/98)) or 1
|
||
|
return s
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- pull the bone_mult from [stalker_damage] section, cache the values to prevent excessive stringsplitting
|
||
|
local stalker_damage = {}
|
||
|
function npc_bone_mult(bone_id)
|
||
|
local damage_mult = stalker_damage[bone_id]
|
||
|
if damage_mult then return damage_mult
|
||
|
else
|
||
|
-- pull from data and cache
|
||
|
local bone_name = Bone_IDs[bone_id] or "bip01_spine"
|
||
|
local damages = SYS_GetParam(0, "stalker_damage", bone_name)
|
||
|
damages = str_explode(damages, ",")
|
||
|
stalker_damage[bone_id] = tonumber(damages[1])
|
||
|
return stalker_damage[bone_id]
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- Pull bone protection for a given NPC's bone. Can use custom bone data.
|
||
|
-- returns two values, second one indicates if this has already been modified
|
||
|
local bone_cache = {}
|
||
|
local custom_bone_data = {}
|
||
|
function npc_bone_protection(npc, bone_id)
|
||
|
local id = npc:id()
|
||
|
if id == AC_ID then return {
|
||
|
armor = actor_bone_prot(bone_id),
|
||
|
modified = false
|
||
|
} end
|
||
|
if custom_bone_data[id] and custom_bone_data[id][bone_id] then return {
|
||
|
armor = custom_bone_data[id][bone_id],
|
||
|
modified = false
|
||
|
}
|
||
|
else
|
||
|
local bone_name = Bone_IDs[bone_id] or "bip01_spine"
|
||
|
local bone_sec = read_npc_bone_sec(npc) or "stalker_hero_1"
|
||
|
if not bone_cache[bone_sec] then
|
||
|
bone_cache[bone_sec] = {}
|
||
|
end
|
||
|
if not bone_cache[bone_sec][bone_id] then
|
||
|
local bone_armor_str = SYS_GetParam(0, bone_sec, bone_name) or "0, 0.15"
|
||
|
local bone_armor = str_explode(bone_armor_str, ",")
|
||
|
bone_cache[bone_sec][bone_id] = tonumber(bone_armor[2]) or 0.15
|
||
|
end
|
||
|
return {
|
||
|
armor = bone_cache[bone_sec][bone_id],
|
||
|
modified = false
|
||
|
}
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function hit_frac(npc, bone_id)
|
||
|
local bone_sec = read_npc_bone_sec(npc) or "stalker_hero_1"
|
||
|
return SYS_GetParam(2, bone_sec, "hit_fraction_npc") or 0.5
|
||
|
end
|
||
|
|
||
|
-- compute actor bone prot based on active suit
|
||
|
function actor_bone_prot(bone_id)
|
||
|
local helmet = db.actor:item_in_slot(12)
|
||
|
if head_bones[bone_id] and helmet then
|
||
|
return helmet:cast_Helmet():GetBoneArmor(bone_id)
|
||
|
else
|
||
|
local outfit = db.actor:item_in_slot(7)
|
||
|
if not outfit then return 0
|
||
|
else return outfit:cast_CustomOutfit():GetBoneArmor(bone_id) end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function read_npc_bone_sec(npc)
|
||
|
local visual_data = game.get_visual_userdata(npc:get_visual_name())
|
||
|
if not visual_data then
|
||
|
printf("!!! Visual %s is lacking data !!!", npc:get_visual_name())
|
||
|
return "stalker_hero_1"
|
||
|
end
|
||
|
local bone_sec = visual_data:r_string_ex("bone_protection", "bones_protection_sect", "stalker_hero_1")
|
||
|
visual_data:close()
|
||
|
return bone_sec
|
||
|
end
|
||
|
|
||
|
-- Used to modify the custom values of a stalker's bone armors (to account for reductions, etc)
|
||
|
function modify_bone(npc, bone_id, new_val)
|
||
|
local npc_id = npc:id()
|
||
|
if npc_id == AC_ID then return end
|
||
|
if not custom_bone_data[npc_id] then custom_bone_data[npc_id] = {} end
|
||
|
custom_bone_data[npc_id][bone_id] = new_val
|
||
|
end
|
||
|
|
||
|
-- util to collect information about a bone - damage multiplier, id, name
|
||
|
function npc_bone_data(npc, bone_id)
|
||
|
bone_prot = npc_bone_protection(npc, bone_id)
|
||
|
local data = {
|
||
|
id = bone_id,
|
||
|
name = Bone_IDs[bone_id],
|
||
|
armor = bone_prot.armor,
|
||
|
modified = bone_prot.modified,
|
||
|
mult = npc_bone_mult(bone_id),
|
||
|
hit_fraction = hit_frac(npc, bone_id)
|
||
|
}
|
||
|
-- halve leg armor and damage multiplier
|
||
|
if get_config("legs") and limbs[bone_id] then
|
||
|
data.armor = data.armor / 2
|
||
|
data.mult = data.mult / 2
|
||
|
end
|
||
|
return data
|
||
|
end
|
||
|
|
||
|
function npc_on_death_callback(npc, who)
|
||
|
if custom_bone_data[npc:id()] then
|
||
|
custom_bone_data[npc:id()] = nil
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- zombies have an inexplicable 0.1x damage multiplier that isn't reflected anywhere, so I added it here
|
||
|
local undead_clsid = {
|
||
|
SM_IZLOM = true,
|
||
|
SM_ZOMBI = true
|
||
|
}
|
||
|
|
||
|
local mutant_hp = {
|
||
|
SM_BLOOD = 1.5,
|
||
|
SM_CONTR = 3,
|
||
|
SM_CHIMS = 3,
|
||
|
SM_IZLOM = 10,
|
||
|
SM_ZOMBI = 10,
|
||
|
}
|
||
|
|
||
|
local burer = {
|
||
|
SM_BURER = 3
|
||
|
}
|
||
|
function mutant_prot_values(mutant, id)
|
||
|
|
||
|
local sec = mutant:section()
|
||
|
local skin_armor = SYS_GetParam(2, sec, "skin_armor")
|
||
|
local clsid = SYS_GetParam(0, sec, "kind")
|
||
|
if not clsid or clsid == "" then clsid = SYS_GetParam(0, sec, "class") or "" end
|
||
|
local prot_sec = SYS_GetParam(0, sec, "protections_sect")
|
||
|
local burer_lv = get_config("burer") or 0
|
||
|
if not skin_armor then
|
||
|
if prot_sec then
|
||
|
skin_armor = SYS_GetParam(2, prot_sec, "skin_armor") or 0
|
||
|
else skin_armor = 0 end
|
||
|
end
|
||
|
local hit_frac = SYS_GetParam(2, sec, "hit_fraction_monster")
|
||
|
if not hit_frac and prot_sec then
|
||
|
hit_frac = SYS_GetParam(2, prot_sec, "hit_fraction_monster")
|
||
|
end
|
||
|
if not hit_frac then hit_frac = 0.5 end
|
||
|
local mult = mutant_hp[clsid] and (get_config("mutant") or undead_clsid[clsid]) and 1/mutant_hp[clsid] or 1
|
||
|
|
||
|
local snork = get_config("snork")
|
||
|
if snork > 0 and clsid == SM_SNORK then
|
||
|
skin_armor = skin_armor / (snork * 2)
|
||
|
mult = mult * (1 + snork)
|
||
|
end
|
||
|
|
||
|
if burer[clsid] then
|
||
|
if burer_lv > 0 then
|
||
|
skin_armor = skin_armor / 2
|
||
|
end
|
||
|
if burer_lv == 1 then
|
||
|
mult = 0.4
|
||
|
end
|
||
|
end
|
||
|
return {
|
||
|
id = id,
|
||
|
hit_fraction = hit_frac,
|
||
|
armor = skin_armor,
|
||
|
deflect = (burer[clsid] and burer_lv == 2) and true or false,
|
||
|
mult = mult
|
||
|
}
|
||
|
end
|
||
|
|
||
|
-- Utils to handle storing velocity. Before weapon fired, compute and cache velocity modifier
|
||
|
local cur_wep_data = {
|
||
|
id = 0
|
||
|
}
|
||
|
-- check and update current weapon to cache hit power, velocity and recoil (for recoil module)
|
||
|
function actor_on_weapon_before_fire(flags)
|
||
|
local current_wpn = db.actor:active_item()
|
||
|
if not current_wpn then return end
|
||
|
-- cache
|
||
|
local id = current_wpn:id()
|
||
|
local sec = current_wpn:section()
|
||
|
if not cur_wep_data.id or cur_wep_data.id ~= id then
|
||
|
if IsWeapon(current_wpn) and not IsItem("fake_ammo_wpn",sec) then
|
||
|
cur_wep_data.id = current_wpn:id()
|
||
|
base_vel = SYS_GetParam(2, sec, "bullet_speed")
|
||
|
base_degrade = SYS_GetParam(2, sec, "condition_shot_dec") or 0
|
||
|
base_recoil = SYS_GetParam(2, sec, "cam_step_angle_horz")
|
||
|
local vel_add = 0
|
||
|
local recoil_add = 0
|
||
|
local upgrades = utils_item.get_upgrades_installed(current_wpn)
|
||
|
for _, upgrade in pairs(upgrades) do
|
||
|
local section = ini_sys:r_string_ex(upgrade, "section")
|
||
|
local speed_adj = ini_sys:r_string_ex(section,"bullet_speed")
|
||
|
if speed_adj then
|
||
|
local op = string.sub(speed_adj, 1, 1)
|
||
|
local val = tonumber(string.sub(speed_adj, 2))
|
||
|
|
||
|
vel_add = op == "+" and vel_add + val or vel_add - val
|
||
|
end
|
||
|
|
||
|
local shot_dec = ini_sys:r_string_ex(section,"condition_shot_dec")
|
||
|
if shot_dec then
|
||
|
local op = string.sub(shot_dec, 1, 1)
|
||
|
local val = tonumber(string.sub(shot_dec, 2))
|
||
|
base_degrade = op == "+" and base_degrade + val or base_degrade - val
|
||
|
end
|
||
|
end
|
||
|
cur_wep_data.vel_mod = vel_add/base_vel
|
||
|
cur_wep_data.dec = base_degrade
|
||
|
-- print_dbg("Stored velocity mod %s, recoil %s, degradation %s for wpn %s", cur_wep_data.vel_mod, cur_wep_data.recoil, base_degrade, sec)
|
||
|
else
|
||
|
-- clear
|
||
|
-- print_dbg("Clearing cwd")
|
||
|
cur_wep_data.id = 0
|
||
|
cur_wep_data.vel_mod = 0
|
||
|
cur_wep_data.dec = 0
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- return the velocity modifier for the current weapon, or 1
|
||
|
function get_cur_vel()
|
||
|
return cur_wep_data.vel_mod or 1
|
||
|
end
|
||
|
|
||
|
function get_recoil()
|
||
|
return cur_wep_data.recoil or 1
|
||
|
end
|
||
|
|
||
|
-- Provide a damage modifier based on distance, adjusted by bullet velocity and flatness
|
||
|
function modify_distance(shooter, npc, ammo)
|
||
|
local dist = get_distance(shooter, npc)
|
||
|
local air_res = SYS_GetParam(2, ammo, "k_air_resistance") or 0.05
|
||
|
air_res = clamp(air_res, 0, 1.1)
|
||
|
local mod = clamp( 1 / ( 1 + (dist / 600) * air_res / ( 1.3 - air_res)), 0.02, 1)
|
||
|
-- print_dbg("applying distance modifier %s", mod)
|
||
|
return mod
|
||
|
end
|
||
|
|
||
|
function get_distance(shooter, npc)
|
||
|
local npc_pos = npc:position()
|
||
|
local shooter_pos = shooter:position()
|
||
|
return npc_pos:distance_to(shooter_pos)
|
||
|
end
|
||
|
|
||
|
-- One way to modify damage based on velocity. 20% of bonus velocity (e.g. taken from flatness upgrades) is added as hit damage.
|
||
|
-- Of course, you don't need to use this
|
||
|
function modify_velocity()
|
||
|
-- 20% of velocity bonus becomes damage
|
||
|
local velocity_mod = (1 + (get_cur_vel() / 5))
|
||
|
-- print_dbg("Applying velocity damage multiplier of %s", velocity_mod)
|
||
|
return velocity_mod
|
||
|
end
|
||
|
|
||
|
-- Modify weapon degradation based on ammo type to be correct, if needed. Leverage stored data
|
||
|
-- eg. if ammo degrade in custom config is 1.5 and is 1 in weapon_ammo, reduce weapon dura by 0.5 x condition_shot_dec
|
||
|
function actor_on_weapon_fired(obj, weapon, ammo_elapsed, grenade_elapsed, ammo_type, grenade_type)
|
||
|
if weapon:id() ~= cur_wep_data.id then return end
|
||
|
if not get_config("impair") then return end
|
||
|
local sec = weapon:section()
|
||
|
ammo_map = utils_item.get_ammo(nil, cur_wep_data.id)
|
||
|
local ammo = ammo_map[weapon:get_ammo_type() + 1]
|
||
|
local base_degrade = SYS_GetParam(2, ammo, "impair")
|
||
|
local actual_degrade = ini_ammo:r_float_ex(ammo, "impair") or base_degrade
|
||
|
if base_degrade == actual_degrade then return end
|
||
|
local diff = (actual_degrade - base_degrade) * cur_wep_data.dec
|
||
|
-- print_dbg("Adjusting weapon cond by %s", diff)
|
||
|
weapon:set_condition(clamp(weapon:condition() - diff, 0.001, 0.999))
|
||
|
end
|
||
|
|
||
|
|
||
|
function on_get_item_cost(kind, obj, profile, calculated_cost, ret)
|
||
|
if not (obj and profile) then
|
||
|
return nil
|
||
|
end
|
||
|
|
||
|
if not get_config("cost") then return end
|
||
|
|
||
|
local sec = obj:section()
|
||
|
local discount = profile.discount
|
||
|
|
||
|
local ammo = IsItem("ammo",sec)
|
||
|
if ammo and ini_ammo:section_exist(sec) and ini_ammo:r_float_ex(sec, "cost") then
|
||
|
ret.new_cost = ini_ammo:r_float_ex(sec, "cost") * discount * (obj:ammo_get_count() / ammo)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function on_game_start()
|
||
|
RegisterScriptCallback("actor_on_weapon_before_fire", actor_on_weapon_before_fire)
|
||
|
RegisterScriptCallback("actor_on_weapon_fired", actor_on_weapon_fired)
|
||
|
RegisterScriptCallback("on_get_item_cost", on_get_item_cost)
|
||
|
RegisterScriptCallback("npc_on_death_callback",npc_on_death_callback)
|
||
|
RegisterScriptCallback("monster_on_hit_callback",npc_on_death_callback)
|
||
|
end
|