1025 lines
28 KiB
Plaintext
1025 lines
28 KiB
Plaintext
|
local ini_parts = itms_manager.ini_parts
|
||
|
parts_list = a_wpo_parts.parts_list
|
||
|
get_config = a_arti_jamming_mcm.get_config
|
||
|
gc = game.translate_string
|
||
|
math_random = math.random
|
||
|
math_floor = math.floor
|
||
|
local jammin = sound_object("wpo\\jammin")
|
||
|
|
||
|
-- Enum for different types of jams
|
||
|
JamType = {
|
||
|
OK = 0,
|
||
|
Misfire = 1, -- No discharge
|
||
|
FailureToEject = 2, -- Discharge
|
||
|
DoubleFeed = 3, -- Discharge, Need to remove magazine
|
||
|
Classic = 4, -- Just reload
|
||
|
}
|
||
|
|
||
|
-- associate jammed guns to type of jam
|
||
|
local jammed_guns = {}
|
||
|
--[[
|
||
|
properties:
|
||
|
id: id of current weapon, if applicable
|
||
|
has_parts: if weapon has parts or not, determines if rest is filled in
|
||
|
parts: map of part_name -> condition, same as item_parts, minus barrel
|
||
|
part_names: list of part names, these are the mechanism parts only. we will take the other parts as needed
|
||
|
barrel_name: name of barrel in weapon, for barrel specific calculation
|
||
|
barrel_con: cond of barrel in weapon
|
||
|
trigger_name: name of trigger
|
||
|
]]
|
||
|
local cgd = {}
|
||
|
|
||
|
local ini_weapon = ini_file("wpo\\weapons\\importer.ltx")
|
||
|
|
||
|
local guarantee_not_jam = 0
|
||
|
-- use to block inputs while animations / unjam logic fires
|
||
|
local unjam_in_progress = false
|
||
|
|
||
|
|
||
|
-------------------
|
||
|
-- SECTION utils --
|
||
|
-------------------
|
||
|
|
||
|
function print_dbg(txt, ...)
|
||
|
local enable_debug = get_config("debug") or false
|
||
|
if enable_debug then
|
||
|
printf("arti_jamming | %s | " .. txt, time_global(), ...)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function send_msg(msg)
|
||
|
if get_config("verbosity") then
|
||
|
actor_menu.set_msg(1, gc(msg),3)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- export functions
|
||
|
function get_jam_status(id)
|
||
|
return jammed_guns[id]
|
||
|
end
|
||
|
|
||
|
function set_jam_status(id, status)
|
||
|
print_dbg("Id %s set to %s", id, status)
|
||
|
jammed_guns[id] = status
|
||
|
end
|
||
|
|
||
|
function get_jammed(id)
|
||
|
return jammed_guns[id] and jammed_guns[id] > 0
|
||
|
end
|
||
|
|
||
|
local can_swear = true
|
||
|
|
||
|
--gun jam play swearing sound
|
||
|
function jam_swearing()
|
||
|
local timeout = get_config("profanity_timeout")
|
||
|
if timeout and not can_swear then return end
|
||
|
if get_config("profanity") and math_random(1,100) > 50 then
|
||
|
local lang = get_config("profanity_language") or "rus"
|
||
|
utils_obj.play_sound("cutscenes\\"..lang.."\\gun_jam_"..math_random(1,7), 1 )
|
||
|
if timeout then
|
||
|
can_swear = false
|
||
|
CreateTimeEvent("wpo", "reset_swear", 2, reset_swear)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function reset_swear()
|
||
|
can_swear = true
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
function start_jammin(chance)
|
||
|
local fun_level = get_config("fun")
|
||
|
fun_level = tonumber(fun_level)
|
||
|
print_dbg("fun level: %s", fun_level)
|
||
|
if fun_level == 0 then
|
||
|
return
|
||
|
end
|
||
|
if fun_level == 2 then
|
||
|
chance = chance * 2
|
||
|
end
|
||
|
if not jammin:playing() and math.random(100) <= chance then
|
||
|
jammin.volume = 1
|
||
|
jammin:play(db.actor, 0, sound_object.s2d)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function stop_jammin()
|
||
|
if jammin:playing() then
|
||
|
jammin:stop()
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function is_part(part)
|
||
|
return parts_list[part]
|
||
|
end
|
||
|
|
||
|
function is_barrel(part)
|
||
|
return is_part(part) and string.find(part, "barrel")
|
||
|
end
|
||
|
function is_trigger(part)
|
||
|
return is_part(part) and string.find(part, "trigger")
|
||
|
end
|
||
|
|
||
|
local function parent_section(sec)
|
||
|
return SYS_GetParam(0, sec,"parent_section") or sec
|
||
|
end
|
||
|
|
||
|
function get_parts_list(wpn)
|
||
|
if wpn and IsWeapon(wpn) and (not IsMelee(wpn)) and not IsItem("fake_ammo_wpn",sec) then
|
||
|
local sec = parent_section(wpn:section())
|
||
|
return ini_parts:r_string_ex("con_parts_list", sec)
|
||
|
else
|
||
|
return nil
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- true if item is a firearm with parts
|
||
|
function has_parts(wpn)
|
||
|
return get_parts_list(wpn) ~= nil
|
||
|
end
|
||
|
|
||
|
function sec_has_parts(sec)
|
||
|
sec = parent_section(sec)
|
||
|
if sec then
|
||
|
return string.find(sec, "wpn_") and ini_parts:r_string_ex("con_parts_list", sec)
|
||
|
else return false
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function missing_parts(wpn, check_broken)
|
||
|
local comp = check_broken and -1 or 0
|
||
|
if wpn:id() == cgd.id then
|
||
|
if not cgd.has_parts then return false end
|
||
|
if cgd.barrel_con < comp then return true end
|
||
|
for k,v in pairs(cgd.parts) do
|
||
|
if v < comp then return true end
|
||
|
end
|
||
|
else
|
||
|
local parts = item_parts.get_parts_con(wpn, nil, true)
|
||
|
if not parts or is_empty(parts) then return false end
|
||
|
for k,v in pairs(parts) do
|
||
|
if is_part(k) and v < comp then return true end
|
||
|
end
|
||
|
end
|
||
|
return false
|
||
|
end
|
||
|
|
||
|
|
||
|
function play_anim(weapon, anim, sound)
|
||
|
local length = weapon:play_hud_motion(anim, true, 0, 1, 0)
|
||
|
print_dbg("playing animation %s for %s time", anim, length)
|
||
|
if sound then
|
||
|
utils_obj.play_sound(sound)
|
||
|
end
|
||
|
return length
|
||
|
end
|
||
|
|
||
|
|
||
|
function persist_current_weapon()
|
||
|
if cgd.id and cgd.has_parts then
|
||
|
local saved_parts = se_load_var(cgd.id, nil, "parts")
|
||
|
local copy_parts = {}
|
||
|
copy_table(saved_parts, copy_parts)
|
||
|
for k,v in pairs(cgd.parts) do
|
||
|
copy_parts[k] = v
|
||
|
end
|
||
|
copy_parts[cgd.barrel_name] = cgd.barrel_con
|
||
|
item_parts.set_parts_con(cgd.id, copy_parts)
|
||
|
print_dbg("Persisted parts for weapon %s", cgd.id)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- current weapon access/update
|
||
|
|
||
|
function update_part(part, cond)
|
||
|
if cgd.barrel_name == part then
|
||
|
cgd.barrel_con = cond
|
||
|
elseif cgd.parts[part] then
|
||
|
cgd.parts[part] = cond
|
||
|
end
|
||
|
persist_current_weapon()
|
||
|
end
|
||
|
|
||
|
function remove_part(part)
|
||
|
update_part(part, -1)
|
||
|
end
|
||
|
|
||
|
function get_parts()
|
||
|
local parts = {}
|
||
|
copy_table(cgd.parts, parts)
|
||
|
parts[cgd.barrel_name] = cgd.barrel_con
|
||
|
return parts
|
||
|
end
|
||
|
|
||
|
function current_id()
|
||
|
return cgd.id
|
||
|
end
|
||
|
|
||
|
function reset_cgd()
|
||
|
clear_cgd()
|
||
|
update_current()
|
||
|
end
|
||
|
----------------------------
|
||
|
-- SECTION main functions --
|
||
|
----------------------------
|
||
|
function actor_on_first_update()
|
||
|
update_current()
|
||
|
end
|
||
|
|
||
|
function clear_cgd()
|
||
|
empty_table(cgd.parts)
|
||
|
empty_table(cgd.data)
|
||
|
empty_table(cgd)
|
||
|
cgd.id = 0
|
||
|
cgd.has_parts = false
|
||
|
end
|
||
|
|
||
|
-- update current gun to be gun in hand
|
||
|
function update_current()
|
||
|
|
||
|
local wpn = db.actor:active_item()
|
||
|
if not wpn then return end
|
||
|
local id = wpn:id()
|
||
|
if id == cgd.id then return end
|
||
|
|
||
|
local sec = wpn:section()
|
||
|
if not has_parts(wpn) then
|
||
|
clear_cgd()
|
||
|
cgd.id = id
|
||
|
print_dbg("Current wpn (no parts) changed to %s (%s)", cgd.id, sec)
|
||
|
else
|
||
|
cgd.id = id
|
||
|
cgd.has_parts = true
|
||
|
print_dbg("Current wpn changed to %s (%s)", cgd.id, sec)
|
||
|
cgd.current_wpn_shot_dec = wpn and SYS_GetParam(2, sec,"condition_shot_dec") or 0.0005
|
||
|
|
||
|
-- consider upgrades
|
||
|
local upgrades = utils_item.get_upgrades_installed(obj)
|
||
|
for _, upgrade in pairs(upgrades) do
|
||
|
local section = ini_sys:r_string_ex(upgrade, "section")
|
||
|
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))
|
||
|
cgd.current_wpn_shot_dec = op == "+" and cgd.current_wpn_shot_dec + val or cgd.current_wpn_shot_dec - val
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- ingest parts
|
||
|
cgd.parts = {}
|
||
|
local saved_parts = item_parts.get_parts_con(wpn, nil, true)
|
||
|
-- fix mistakes - if weapon should have parts but were deleted, delete and restore
|
||
|
for k, v in pairs(saved_parts) do
|
||
|
print_dbg("Part: %s, condition of part: %s", k, v)
|
||
|
if is_trigger(k) then
|
||
|
cgd.trigger_name = k
|
||
|
cgd.parts[k] = v
|
||
|
elseif is_barrel(k) then
|
||
|
cgd.barrel_name = k
|
||
|
cgd.barrel_con = v
|
||
|
elseif is_part(k) then
|
||
|
cgd.parts[k] = v
|
||
|
end
|
||
|
end
|
||
|
-- manage family data
|
||
|
cgd.data = {}
|
||
|
local parent = parent_section(sec)
|
||
|
if not ini_weapon:section_exist(parent) then parent = "base" end
|
||
|
cgd.data.jam_chance = larp.str_explode_num(ini_weapon:r_string_ex(parent, "jam_chance") or "0.001, 0.03, 0.09, 0.27, 0.81", ",")
|
||
|
cgd.data.max_jam_chance = ini_weapon:r_float_ex(parent, "max_jam_chance") or 1
|
||
|
cgd.data.misfire_chance = larp.str_explode_num(ini_weapon:r_string_ex(parent, "misfire_chance"), ",")
|
||
|
cgd.data.superjam_mult = ini_weapon:r_float_ex(parent, "superjam_mult") or 1
|
||
|
cgd.data.damage_chance_mult = ini_weapon:r_float_ex(parent, "damage_chance_mult") or 1
|
||
|
cgd.data.barrel_wear = ini_weapon:r_float_ex(parent, "barrel_wear") or 1
|
||
|
cgd.ammo_data = utils_item.get_ammo(nil, wpn:id())
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function get_ammo_impair(wpn)
|
||
|
if wpn:id() ~= cgd.id or not get_config("oldammo") then return 1 end
|
||
|
local ammo = cgd.ammo_data[wpn:get_ammo_type()+1]
|
||
|
return ammo and SYS_GetParam(2, ammo, "impair") or 1
|
||
|
end
|
||
|
|
||
|
-- returns true if using ammo with impair > 1
|
||
|
function using_old_ammo(wpn)
|
||
|
return get_ammo_impair(wpn) > 1
|
||
|
end
|
||
|
|
||
|
-- individually calculate jam based on weapon parts, and jam the weapon if applicable
|
||
|
-- Average jam chance based on damage level of each component.
|
||
|
local function calculate_jam(wpn)
|
||
|
if guarantee_not_jam > 0 then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
jam_chance = calculate_jam_chance(wpn)
|
||
|
|
||
|
print_dbg("Final jam chance: %s", jam_chance)
|
||
|
|
||
|
if (math_random() < jam_chance) then
|
||
|
if wpn:get_ammo_in_magazine() > 0 and get_config("superjam") and (math_random() < (cgd.data.superjam_mult * jam_chance/2)) then
|
||
|
set_jam_status(wpn:id(), JamType.DoubleFeed)
|
||
|
print_dbg("Severe jam")
|
||
|
else
|
||
|
set_jam_status(wpn:id(), JamType.FailureToEject)
|
||
|
print_dbg("Normal jam")
|
||
|
end
|
||
|
start_jammin(2)
|
||
|
CreateTimeEvent("arti_jamming", "jamsound", 0.1,
|
||
|
function(sec, key)
|
||
|
play_sound(nil, 1, sec, key)
|
||
|
return true
|
||
|
end
|
||
|
, wpn:section(), "jam_sound")
|
||
|
send_msg("ui_st_jam")
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- assumes weapon being used is current
|
||
|
function calculate_jam_chance(wpn, extra)
|
||
|
-- compute base chance from quality of weapon parts
|
||
|
local count = 0
|
||
|
local mechanism_cond = #cgd.parts
|
||
|
for k, v in pairs(cgd.parts) do -- skip barrel
|
||
|
local severity = 5 - clamp(math_floor(v / 20), 0, 4)
|
||
|
mechanism_cond = mechanism_cond + cgd.data.jam_chance[severity]
|
||
|
count = count + 1
|
||
|
end
|
||
|
jam_chance = mechanism_cond/count
|
||
|
if not extra then return jam_chance end
|
||
|
-- apply ammo, mcm and condition enhancements
|
||
|
local ammo_impair = get_ammo_impair(wpn)
|
||
|
if ammo_impair > 0 then
|
||
|
if jam_chance == 0 then jam_chance = clamp(0.1 * (ammo_impair - 1), 0, 0.05)
|
||
|
else
|
||
|
jam_chance = jam_chance * ammo_impair
|
||
|
end
|
||
|
end
|
||
|
-- enhancing jam chance
|
||
|
jam_chance = jam_chance * get_config("jamchance")
|
||
|
-- condition can increase jam chance by up to 3x
|
||
|
local cond = wpn:condition()
|
||
|
local ratio = cond < 0.9 and 1 + (0.9-cond)/0.45 or 1
|
||
|
jam_chance = jam_chance * ratio
|
||
|
|
||
|
jam_chance = clamp(jam_chance, 0, cgd.data.max_jam_chance)
|
||
|
|
||
|
return jam_chance
|
||
|
end
|
||
|
|
||
|
-- Randomly damage 1 part of the weapon by DAMAGE, skipping barrels
|
||
|
-- wear reduces damage and happens more frequently
|
||
|
local function damage_part(wpn, wear)
|
||
|
local to_damage = random_key_table(cgd.parts)
|
||
|
local part_con = cgd.parts[to_damage]
|
||
|
local amt = wear and 1 or math_random(5, 12)
|
||
|
cgd.parts[to_damage] = clamp(part_con - amt, 0, 99)
|
||
|
|
||
|
print_dbg("%s damaged %s amt to %s condition", to_damage, amt, cgd.parts[to_damage])
|
||
|
if get_config("verbosity") then
|
||
|
news_manager.send_tip(db.actor, gc("ui_st_parts_damage"), nil, "swiss_knife", 6000)
|
||
|
end
|
||
|
if not wear then
|
||
|
CreateTimeEvent("arti_jamming", "damsound", 0.1,
|
||
|
function()
|
||
|
play_sound(nil, 0.5, wpn:section(), "damage_sound")
|
||
|
return true
|
||
|
end)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- Calculate chance to damage based on weapon condition
|
||
|
local function calculate_damage(wpn)
|
||
|
local cond = wpn and wpn:condition() or 1
|
||
|
local damaged = false
|
||
|
local roll = math_random()
|
||
|
local damage_chance = calculate_damage_chance(wpn)
|
||
|
|
||
|
|
||
|
if (roll < damage_chance) then
|
||
|
damage_part(wpn)
|
||
|
damaged = true
|
||
|
else
|
||
|
-- wear is mostly an omnipresent threat
|
||
|
wear_chance = clamp(damage_chance * 1.5, 0, 1)
|
||
|
if math_random() < wear_chance then
|
||
|
damage_part(wpn, true)
|
||
|
damaged = true
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- calculate random condition damage
|
||
|
if wpn:condition() > 0.2 then
|
||
|
local chance = 2 * calculate_jam_chance(wpn)
|
||
|
if math_random() < chance then
|
||
|
local degrade = math_random(3)/100
|
||
|
wpn:set_condition(wpn:condition() - degrade, 0, 1)
|
||
|
damaged = true
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local barrel_wear = calculate_barrel_wear(wpn, old_ammo)
|
||
|
|
||
|
print_dbg("Calculating damage: Old ammo %s, barrel wear %s, Final: %s", old_ammo, barrel_wear, damage_chance)
|
||
|
|
||
|
if (roll < barrel_wear) then
|
||
|
print_dbg("Damaged barrel")
|
||
|
cgd.barrel_con = cgd.barrel_con > 0 and cgd.barrel_con - 1 or 0
|
||
|
damaged = true
|
||
|
end
|
||
|
|
||
|
if damaged then
|
||
|
persist_current_weapon()
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- damage chance is 0-1
|
||
|
function calculate_damage_chance(wpn)
|
||
|
local cond = wpn and wpn:condition() or 1
|
||
|
local threshold = (get_config("threshold") or 85)/100
|
||
|
local damage_chance = 0
|
||
|
old_ammo = get_ammo_impair(wpn)
|
||
|
if (cond < threshold) then
|
||
|
-- 200 x (threshold - condition) * per shot decrease, should result in pretty small chances
|
||
|
damage_chance = (threshold - cond) * cgd.current_wpn_shot_dec * 200 * (cgd.data.damage_chance_mult or 1)
|
||
|
if old_ammo > 1 then
|
||
|
damage_chance = damage_chance * old_ammo
|
||
|
end
|
||
|
else
|
||
|
if old_ammo > 1 then damage_chance = clamp(0.1 * (old_ammo - 1), 0, 0.05) end
|
||
|
end
|
||
|
return damage_chance
|
||
|
end
|
||
|
|
||
|
function calculate_barrel_wear(wpn)
|
||
|
old_ammo = using_old_ammo(wpn) and 2 or 1
|
||
|
return old_ammo * cgd.data.barrel_wear
|
||
|
end
|
||
|
|
||
|
-- calculate misfire change of held weapon
|
||
|
function calculate_misfire()
|
||
|
local trigger_con = cgd.parts[cgd.trigger_name] or 99
|
||
|
local severity = 5 - clamp(math_floor(trigger_con / 20), 0, 4)
|
||
|
local misfire_chance = cgd.data.misfire_chance[severity]
|
||
|
misfire_chance = misfire_chance * get_config("jamchance")
|
||
|
return misfire_chance
|
||
|
end
|
||
|
|
||
|
local last_wpn_snd_empty = {nil, "$no_sound", 1}
|
||
|
local tg_snd_empty = 0
|
||
|
local delay_snd_empty = 300 -- ms
|
||
|
-- before fire - misfire, block shot if jammed with same clicky sound for zeroed weapons
|
||
|
function actor_on_weapon_before_fire(flags)
|
||
|
|
||
|
local wpn = db.actor:active_item()
|
||
|
local id = wpn and wpn:id() or 0
|
||
|
|
||
|
if wpn and id ~= cgd.id then
|
||
|
-- populate current gun
|
||
|
update_current()
|
||
|
end
|
||
|
|
||
|
if not wpn or id ~= cgd.id or wpn:get_ammo_in_magazine() == 0 or not cgd.has_parts or wpn:weapon_in_grenade_mode() then return end
|
||
|
|
||
|
-- if missing parts do not roll for misfire
|
||
|
if missing_parts(wpn, true) then
|
||
|
send_msg("ui_st_missing")
|
||
|
flags.ret_value = false
|
||
|
return
|
||
|
end
|
||
|
-- if guarantee not jam, don't even calculate this
|
||
|
if guarantee_not_jam > 0 then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
-- misfire the weapon
|
||
|
local misfire_chance = calculate_misfire()
|
||
|
print_dbg("Misfire chance: "..misfire_chance)
|
||
|
-- soft misfire
|
||
|
-- if math_random() < clamp(misfire_chance * 3, 0, 1) then
|
||
|
-- flags.ret_value = false
|
||
|
-- return
|
||
|
-- end
|
||
|
-- FTE/Double feed
|
||
|
if not get_jammed(id) then
|
||
|
calculate_jam(wpn)
|
||
|
if get_jammed(id) then
|
||
|
return
|
||
|
end
|
||
|
end
|
||
|
-- hard misfire
|
||
|
if not get_jammed(id) and math_random() < misfire_chance then
|
||
|
set_jam_status(id, JamType.Misfire)
|
||
|
end
|
||
|
|
||
|
local str = ""
|
||
|
-- check for malfunction
|
||
|
if get_jammed(id) then
|
||
|
local sec = wpn:section()
|
||
|
-- Cache
|
||
|
if (sec ~= last_wpn_snd_empty[1]) then
|
||
|
-- Reset
|
||
|
last_wpn_snd_empty[1] = sec
|
||
|
last_wpn_snd_empty[2] = "$no_sound"
|
||
|
last_wpn_snd_empty[3] = 1
|
||
|
|
||
|
-- Get empty sound
|
||
|
local snd = ini_sys:r_string_ex(sec,"snd_empty")
|
||
|
if snd and (snd ~= "") then
|
||
|
snd = str_explode(snd,",")
|
||
|
last_wpn_snd_empty[2] = snd[1]
|
||
|
last_wpn_snd_empty[3] = snd[2] or 1
|
||
|
end
|
||
|
end
|
||
|
-- Play empty clip sound
|
||
|
local tg = time_global()
|
||
|
if (last_wpn_snd_empty[2] ~= "$no_sound") and (tg > tg_snd_empty) then
|
||
|
utils_obj.play_sound( last_wpn_snd_empty[2], last_wpn_snd_empty[3] or 1 )
|
||
|
tg_snd_empty = tg + delay_snd_empty
|
||
|
end
|
||
|
--ADDED jam_swearing()
|
||
|
jam_swearing()
|
||
|
start_jammin(2)
|
||
|
if (get_jam_status(id) == JamType.DoubleFeed) then
|
||
|
str = "ui_st_superjam"
|
||
|
else
|
||
|
str = "ui_st_jam"
|
||
|
end
|
||
|
-- Don't shoot
|
||
|
flags.ret_value = false
|
||
|
send_msg(str)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- after fire - chance of FTE, double feed
|
||
|
-- if shoot success, calculate damage
|
||
|
function actor_on_weapon_fired(obj, wpn, ammo_elapsed, grenade_elapsed, ammo_type, grenade_type)
|
||
|
if (obj:id() ~= AC_ID) then
|
||
|
return
|
||
|
end
|
||
|
local id = wpn:id()
|
||
|
if id ~= cgd.id or not cgd.has_parts or wpn:get_ammo_in_magazine() == 0 or missing_parts(wpn) or wpn:weapon_in_grenade_mode() then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
-- calc and apply jam
|
||
|
wpn = utils_item.item_is_fa(wpn) and wpn
|
||
|
if not wpn then return end
|
||
|
|
||
|
if get_jammed(id) then
|
||
|
level.release_action(bind_to_dik(key_bindings.kWPN_FIRE))
|
||
|
end
|
||
|
if not cgd.parts or is_empty(cgd.parts) then return
|
||
|
elseif (not get_jammed(id)
|
||
|
or get_jam_status(id) == JamType.FailureToEject
|
||
|
or get_jam_status(id) == JamType.DoubleFeed) then
|
||
|
calculate_damage(wpn)
|
||
|
end
|
||
|
|
||
|
if guarantee_not_jam > 0 then
|
||
|
guarantee_not_jam = guarantee_not_jam - 1
|
||
|
end
|
||
|
|
||
|
-- calc and apply extra degradation in rain
|
||
|
local degradation_factor = 1
|
||
|
if level.rain_factor() > 0.3 and not GetEvent("current_safe_cover") then
|
||
|
degradation_factor = degradation_factor * (get_config("degradation") or 1.5)
|
||
|
end
|
||
|
|
||
|
if degradation_factor > 1 then
|
||
|
degradation_factor = degradation_factor - 1
|
||
|
local degradation = degradation_factor * cgd.current_wpn_shot_dec
|
||
|
print_dbg("Degrading by additional %s", degradation)
|
||
|
wpn:set_condition(clamp(wpn:condition() - degradation, 0, 0.999))
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function check_unjam(wpn)
|
||
|
return get_jammed(wpn:id())
|
||
|
end
|
||
|
|
||
|
function string_unjam()
|
||
|
return gc("st_unjam")
|
||
|
end
|
||
|
|
||
|
function get_sound(sec, key)
|
||
|
print_dbg("Get sound %s %s", sec, key)
|
||
|
key = key or "unjam_sound"
|
||
|
sec = sec and parent_section(sec) or "base"
|
||
|
sec = ini_weapon:section_exist(sec) and sec or "base"
|
||
|
return "wpo\\"..ini_weapon:r_string_ex(sec, key)
|
||
|
end
|
||
|
|
||
|
function play_sound(snd, vol, sec, key)
|
||
|
print_dbg("Play sound %s %s %s %s", snd, vol, sec, key)
|
||
|
if not snd then
|
||
|
snd = get_sound(sec, key)
|
||
|
end
|
||
|
print_dbg("Playing %s", snd)
|
||
|
utils_obj.play_sound(snd, vol)
|
||
|
end
|
||
|
|
||
|
function toggle_slot(slot)
|
||
|
db.actor:activate_slot(slot)
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
-- do an unjam, optionally playing sounds and such
|
||
|
function apply_unjam(id, sound, message)
|
||
|
if not unjam_in_progress then
|
||
|
print_dbg("Unjam interrupted for %s", id)
|
||
|
return true
|
||
|
end
|
||
|
unjam_in_progress = false
|
||
|
if sound and sound ~= "" then
|
||
|
play_sound(sound)
|
||
|
end
|
||
|
if message then
|
||
|
send_msg(message)
|
||
|
end
|
||
|
if id and id ~= 0 then
|
||
|
set_jam_status(id, nil)
|
||
|
guarantee_not_jam = 1
|
||
|
end
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
function check_simple(weapon)
|
||
|
local str = gc("ui_st_functional")
|
||
|
send_msg(str)
|
||
|
guarantee_not_jam = 1
|
||
|
end
|
||
|
|
||
|
function check_anim(weapon)
|
||
|
check_simple(weapon)
|
||
|
local sec = weapon:section()
|
||
|
local idle_section = utils_item.addon_attached(weapon ,"gl") and (weapon:weapon_in_grenade_mode() and "anm_bore_g" or "anm_bore_w_gl") or "anm_bore"
|
||
|
local hud = ini_sys:r_string_ex(sec, "hud")
|
||
|
print_dbg("searching for "..idle_section.. " for weapon section "..hud)
|
||
|
|
||
|
local bored_anim = hud and ini_sys:r_string_ex(hud, idle_section)
|
||
|
if bored_anim then
|
||
|
weapon:switch_state(4)
|
||
|
guarantee_not_jam = 1
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- put down and bring back up
|
||
|
function unjam_simple(weapon)
|
||
|
stop_jammin()
|
||
|
local str = gc("ui_st_unjam")
|
||
|
local id = weapon:id()
|
||
|
local sec = parent_section(weapon:section())
|
||
|
local sound = get_sound(sec)
|
||
|
local active_slot = db.actor:active_slot()
|
||
|
toggle_slot(0)
|
||
|
|
||
|
unjam_in_progress = true
|
||
|
CreateTimeEvent("arti_jamming", "unjam"..id, 0.1,
|
||
|
unjam_simple_timed,
|
||
|
active_slot, id, sound, str)
|
||
|
end
|
||
|
|
||
|
function unjam_simple_timed(slot, id, sound, message)
|
||
|
if not unjam_in_progress then
|
||
|
print_dbg("unjam_replace_simple interrupted")
|
||
|
toggle_slot(slot)
|
||
|
return true
|
||
|
end
|
||
|
local current_weapon = db.actor:active_item()
|
||
|
if current_weapon then return false end
|
||
|
|
||
|
toggle_slot(slot)
|
||
|
-- premature interrupt
|
||
|
apply_unjam(id, sound, message)
|
||
|
|
||
|
return true
|
||
|
end
|
||
|
-- unjam held weapon with animation
|
||
|
function unjam_anim(weapon)
|
||
|
-- don't unjam if weapon is still in Fire state
|
||
|
if weapon:get_state() == 5 then
|
||
|
return
|
||
|
end
|
||
|
stop_jammin()
|
||
|
local str = gc("ui_st_unjam")
|
||
|
local id = weapon:id()
|
||
|
local sec = weapon:section()
|
||
|
-- blindside compat, check for unjaminations and cache anim name and sound
|
||
|
local unjam_anims = get_unjam_animation(weapon)
|
||
|
if unjam_anims ~= nil then
|
||
|
unjam_in_progress = true
|
||
|
local to_search = utils_item.addon_attached(weapon ,"gl") and "anm_reload_misfire_w_gl" or "anm_reload_misfire"
|
||
|
local unjam_sound = ini_sys:r_string_ex(sec, "snd_reload_misfire") or ini_sys:r_string_ex(sec, "snd_reload_1") or "$no_sound"
|
||
|
-- shorten time so that jam is cleared before the next animation plays
|
||
|
local length = play_anim(weapon, to_search, unjam_sound) - 10
|
||
|
CreateTimeEvent("arti_jamming", "restore", length/1000, apply_unjam, id)
|
||
|
end
|
||
|
|
||
|
end
|
||
|
|
||
|
function unjam_super_simple(weapon)
|
||
|
if weapon:get_ammo_in_magazine() > 1 then
|
||
|
start_jammin(50)
|
||
|
jam_swearing()
|
||
|
send_msg(gc("ui_st_unjam_fail"))
|
||
|
weapon:switch_state(2)
|
||
|
else
|
||
|
unjam_simple(weapon)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function unjam_super_anim(weapon)
|
||
|
if weapon:get_ammo_in_magazine() > 1 then
|
||
|
start_jammin(50)
|
||
|
jam_swearing()
|
||
|
send_msg(gc("ui_st_unjam_fail"))
|
||
|
weapon:switch_state(2)
|
||
|
else
|
||
|
unjam_anim(weapon)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
|
||
|
function determine_slot(weapon)
|
||
|
for i=1,5 do
|
||
|
local item = db.actor:item_in_slot(i)
|
||
|
local item_id = item and item:id() or 0
|
||
|
if weapon:id() == item_id then return i end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- clear the entry, no qs asked
|
||
|
function inv_unjam(weapon)
|
||
|
if get_jam_status(weapon:id()) > 1 then return end
|
||
|
unjam_in_progress = true
|
||
|
apply_unjam(weapon:id(), get_sound(weapon:section()))
|
||
|
end
|
||
|
|
||
|
function unjam_and_clone(weapon, slot, sound)
|
||
|
if not unjam_in_progress then
|
||
|
print_dbg("unjam_replace_simple interrupted")
|
||
|
toggle_slot(slot)
|
||
|
return true
|
||
|
end
|
||
|
local current_weapon = db.actor:active_item()
|
||
|
if current_weapon then return false end
|
||
|
|
||
|
apply_unjam(weapon:id(), sound)
|
||
|
local old_weapon = alife_object(weapon:id())
|
||
|
local new_weapon = alife_clone_weapon(old_weapon)
|
||
|
print_dbg("cloned weapon, doing restoration")
|
||
|
CreateTimeEvent("arti_jamming", "move_to_slot", 0.1,
|
||
|
function(weapon, slot)
|
||
|
print_dbg("unjam_replace_simple - restoring weapon to slot %s", slot)
|
||
|
db.actor:move_to_slot(level.object_by_id(weapon), slot)
|
||
|
toggle_slot(slot)
|
||
|
return true
|
||
|
end,
|
||
|
new_weapon.id, slot)
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
-- hide, clone, move
|
||
|
function unjam_replace_simple(weapon)
|
||
|
if not weapon then return end
|
||
|
local slot = determine_slot(weapon)
|
||
|
local sound = get_sound(weapon:section())
|
||
|
print_dbg("slot determined is %s", slot)
|
||
|
unjam_in_progress = true
|
||
|
local active_slot = db.actor:active_slot()
|
||
|
toggle_slot(0)
|
||
|
print_dbg("replacing weapon %s", weapon:id())
|
||
|
CreateTimeEvent("arti_jamming", "move "..weapon:id(), 0.1, unjam_and_clone, weapon, slot, sound)
|
||
|
print_dbg("unjam replace returning")
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
-- just change the state to reloading and check when reloading is done
|
||
|
function unjam_replace_anim(weapon)
|
||
|
weapon:switch_state(7)
|
||
|
unjam_in_progress = true
|
||
|
apply_unjam(weapon:id())
|
||
|
end
|
||
|
|
||
|
function inv_unjam_replace(weapon)
|
||
|
unjam_in_progress = true
|
||
|
apply_unjam(weapon:id(), get_sound(weapon:section()))
|
||
|
local slot = determine_slot(weapon)
|
||
|
local old_weapon = alife_object(weapon:id())
|
||
|
local new_weapon = alife_clone_weapon(old_weapon)
|
||
|
if slot then
|
||
|
CreateTimeEvent("arti_jamming", "move_to_slot", 0.1,
|
||
|
function(weapon, slot)
|
||
|
print_dbg("unjam_replace_simple - restoring weapon to slot %s", slot)
|
||
|
db.actor:move_to_slot(level.object_by_id(weapon), slot)
|
||
|
toggle_slot(slot)
|
||
|
return true
|
||
|
end,
|
||
|
new_weapon.id, slot)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- conditions:
|
||
|
-- unjam type? ok (0), misfire/feed (just 1), failure to eject(2), doublefeed(3), classic (4)
|
||
|
-- currently held?
|
||
|
-- has animation?
|
||
|
-- these functions will be completely responsible for what happens
|
||
|
local unjam_router = {
|
||
|
-- held or not
|
||
|
held = {
|
||
|
[JamType.OK] = {
|
||
|
anim = check_anim,
|
||
|
no_anim = check_simple
|
||
|
},
|
||
|
[JamType.Misfire] = {
|
||
|
anim = unjam_anim,
|
||
|
no_anim = unjam_simple
|
||
|
},
|
||
|
[JamType.FailureToEject] = {
|
||
|
anim = unjam_anim,
|
||
|
no_anim = unjam_simple
|
||
|
},
|
||
|
[JamType.DoubleFeed] = {
|
||
|
anim = unjam_super_anim,
|
||
|
no_anim = unjam_super_simple
|
||
|
},
|
||
|
[JamType.Classic] = {
|
||
|
anim = unjam_replace_anim,
|
||
|
no_anim = unjam_replace_simple
|
||
|
}
|
||
|
},
|
||
|
inventory = {
|
||
|
[JamType.Misfire] = {
|
||
|
anim = inv_unjam,
|
||
|
no_anim = inv_unjam
|
||
|
},
|
||
|
[JamType.FailureToEject] = {
|
||
|
anim = inv_unjam,
|
||
|
no_anim = inv_unjam
|
||
|
},
|
||
|
[JamType.DoubleFeed] = {
|
||
|
anim = inv_unjam,
|
||
|
no_anim = inv_unjam
|
||
|
},
|
||
|
[JamType.Classic] = {
|
||
|
anim = inv_unjam_replace,
|
||
|
no_anim = inv_unjam_replace
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
-- Unjam works by clearing weapon ID from the jam table, if conditions are met.
|
||
|
function unjam(wpn)
|
||
|
local weapon = wpn or db.actor:active_item()
|
||
|
if not weapon then return end
|
||
|
if unjam_in_progress then
|
||
|
print_dbg("weapon already being unjammed")
|
||
|
return
|
||
|
end
|
||
|
local id = weapon:id()
|
||
|
local active_id = db.actor:active_item() and db.actor:active_item():id() or 0
|
||
|
local sec = weapon:section()
|
||
|
|
||
|
if not weapon then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
if missing_parts(weapon) then
|
||
|
send_msg("ui_st_missing")
|
||
|
return
|
||
|
end
|
||
|
|
||
|
local is_current = id == active_id and "held" or "inventory"
|
||
|
|
||
|
local jam_level = get_jam_status(id) or 0
|
||
|
|
||
|
local has_anim = get_unjam_animation(weapon) and "anim" or "no_anim"
|
||
|
|
||
|
print_dbg("unjam current: %s jam level: %s has_anims: %s. has function? %s", is_current, jam_level, has_anim, unjam_router[is_current][jam_level][has_anim] ~= nil)
|
||
|
|
||
|
if unjam_router[is_current][jam_level][has_anim] then
|
||
|
unjam_router[is_current][jam_level][has_anim](weapon)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
|
||
|
function get_unjam_animation(weapon)
|
||
|
local sec = weapon:section()
|
||
|
local to_search = utils_item.addon_attached(weapon ,"gl") and "anm_reload_misfire_w_gl" or "anm_reload_misfire"
|
||
|
local hud = ini_sys:r_string_ex(sec, "hud")
|
||
|
print_dbg("searching for "..to_search.. " for weapon section "..hud)
|
||
|
local unjam_anims = hud and ini_sys:r_string_ex(hud,to_search)
|
||
|
return unjam_anims
|
||
|
end
|
||
|
|
||
|
-- SECTION callbacks --
|
||
|
|
||
|
|
||
|
local disallowed = {
|
||
|
[key_bindings.kINVENTORY] = true,
|
||
|
[key_bindings.kWPN_RELOAD] = true,
|
||
|
[key_bindings.kNIGHT_VISION] = true,
|
||
|
[key_bindings.kWPN_NEXT] = true,
|
||
|
[key_bindings.kWPN_1] = true,
|
||
|
[key_bindings.kWPN_2] = true,
|
||
|
[key_bindings.kWPN_3] = true,
|
||
|
[key_bindings.kWPN_4] = true,
|
||
|
[key_bindings.kWPN_5] = true,
|
||
|
[key_bindings.kWPN_6] = true,
|
||
|
|
||
|
}
|
||
|
function on_key_press(key)
|
||
|
-- certain actions interrupt any unjams in progress
|
||
|
local bind = dik_to_bind(key)
|
||
|
if unjam_in_progress and disallowed[bind] then
|
||
|
print_dbg("unjam interrupted by action")
|
||
|
unjam_in_progress = false
|
||
|
end
|
||
|
end
|
||
|
|
||
|
|
||
|
function actor_on_weapon_jammed(actor)
|
||
|
local wpn = db.actor:active_item()
|
||
|
if not get_jammed(wpn:id()) then
|
||
|
if not pcall(function(wpn)
|
||
|
set_jam_status(wpn:id(), JamType.FailureToEject)
|
||
|
end, wpn) then
|
||
|
set_jam_status(wpn:id(), JamType.Classic)
|
||
|
end
|
||
|
end
|
||
|
wpn:cast_Weapon():SetMisfire(false)
|
||
|
jam_swearing()
|
||
|
play_sound(nil, 2, wpn:section(), "jam_sound")
|
||
|
end
|
||
|
|
||
|
function actor_on_weapon_reload(wpn)
|
||
|
local id = wpn:id()
|
||
|
local jam_lv = get_jam_status(id)
|
||
|
if jam_lv then
|
||
|
if magazines and magazines.do_interrupt_reload then magazines.do_interrupt_reload() end
|
||
|
-- drop mag if simplejam enabled and superjammed
|
||
|
if get_config("simplejam") and jam_lv == JamType.DoubleFeed then
|
||
|
-- on simplejam, unload the magazine
|
||
|
mag_support.eject_mag(wpn)
|
||
|
end
|
||
|
CreateTimeEvent("arti_jamming", "cancel_reload", 0.1,
|
||
|
function(wpn)
|
||
|
wpn:switch_state(2)
|
||
|
return true
|
||
|
end
|
||
|
, wpn)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- Reduces player outgoing damage based on barrel quality, up to 50% --
|
||
|
local function reduce_damage(npc, shit, bone_id, flags)
|
||
|
-- skip if it wasnt a hit by the player
|
||
|
if not (shit.draftsman and shit.draftsman:id() == 0) then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
-- skip if player has no weapon out, or weapon has no parts
|
||
|
local weapon = db.actor:active_item()
|
||
|
if not weapon or barrel_con == nil then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
if barrel_con > 80 then
|
||
|
return
|
||
|
else
|
||
|
local ratio = (80 - barrel_con)/80
|
||
|
shit.power = shit.power * (1 - (0.5 * ratio))
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function npc_on_before_hit(npc,shit,bone_id, flags)
|
||
|
reduce_damage(npc, shit, bone_id, flags)
|
||
|
end
|
||
|
function monster_on_before_hit(monster,shit,bone_id, flags)
|
||
|
reduce_damage(monster, shit, bone_id, flags)
|
||
|
end
|
||
|
|
||
|
-- Clear jam mid animation with motion marks
|
||
|
function actor_on_hud_animation_mark(state, mark)
|
||
|
if mark == "clear_jam" then
|
||
|
ResetTimeEvent("arti_jamming", "restore", 0)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function on_game_start()
|
||
|
|
||
|
-- add custom functors
|
||
|
custom_functor_autoinject.add_functor("arti_unjam", check_unjam, string_unjam, nil, unjam, false)
|
||
|
RegisterScriptCallback("on_key_press",on_key_press)
|
||
|
RegisterScriptCallback("actor_on_weapon_jammed",actor_on_weapon_jammed)
|
||
|
RegisterScriptCallback("actor_on_weapon_before_fire",actor_on_weapon_before_fire)
|
||
|
RegisterScriptCallback("actor_on_weapon_fired",actor_on_weapon_fired)
|
||
|
RegisterScriptCallback("actor_on_weapon_reload",actor_on_weapon_reload)
|
||
|
RegisterScriptCallback("actor_on_first_update",actor_on_first_update)
|
||
|
RegisterScriptCallback("npc_on_before_hit",npc_on_before_hit)
|
||
|
RegisterScriptCallback("monster_on_before_hit",monster_on_before_hit)
|
||
|
RegisterScriptCallback("actor_on_hud_animation_mark",actor_on_hud_animation_mark)
|
||
|
end
|