-- 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