-- Rip of the enhanced recoil effects with wildcarding support and ammo support -- get_config = ballistics_mcm.get_config -- print_dbg = ballistics_mcm.print_dbg local ini_eff = ini_file("plugins\\recoil\\importer.ltx") -- track and upgrade recoil local wpn_data = { id = 0, recoil = 1, mult_table = {0, 0, 0, 0, 0, 0} } function actor_on_weapon_before_fire(flags) local current_wpn = db.actor:active_item() if not current_wpn or not (IsWeapon(current_wpn) and not IsItem("fake_ammo_wpn",sec)) then wpn_data = { id = 0, recoil = 1, mult_table = {0, 0, 0, 0, 0, 0} } return end -- cache recoil (determined by cam_step_angle_horz) local id = current_wpn:id() if wpn_data.id == id then return end local sec = current_wpn:section() local sec_p = ini_sys:r_string_ex(sec,"parent_section") or sec local base_recoil = SYS_GetParam(2, sec, "cam_step_angle_horz") local recoil_mod = 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 recoil = ini_sys:r_string_ex(section,"cam_step_angle_horz") if recoil then local op = string.sub(recoil, 1, 1) local val = tonumber(string.sub(recoil, 2)) recoil_mod = op == "+" and recoil_mod + val or recoil_mod - val end end -- current_wpn:cast_Weapon():GetCurrentFireMode() wpn_data.id = id wpn_data.recoil = 1 + (recoil_mod/base_recoil) wpn_data.mult_table = ini_eff:r_list(sec_p, "kick_recoil", "0, 0, 0, 0, 0, 0") end local wpn_fx = {} local function parse_anims(sec) local sec_p = ini_sys:r_string_ex(sec,"parent_section") or sec if wpn_fx[sec_p] and (sec ~= sec_p) then wpn_fx[sec] = wpn_fx[sec_p] elseif ini_eff:section_exist(sec_p) then local tbl = {} -- parse strength tbl.s = {} local s = ini_eff:r_string_ex(sec_p,"s") local _s = str_explode(s,",") for i=1,#_s do tbl.s[#tbl.s + 1] = tonumber(_s[i]) end -- parse strength randomizer tbl.r = ini_eff:r_float_ex(sec_p,"r") -- parse anm tbl.e = {} local e = ini_eff:r_string_ex(sec_p,"e") local _e = str_explode(e,",") for i=1,#_e do local __e = str_explode(_e[i],":") for j=1,#__e do __e[j] = tonumber(__e[j]) end tbl.e[#tbl.e + 1] = __e end -- parse ppe tbl.p = {} local p = ini_eff:r_string_ex(sec_p,"p") if p and p ~= "" then for k,v in string.gmatch(p, "([%w_%-%s%.]+)=([%w_%-%s%.]+)") do tbl.p[k] = tonumber(v) end end if (not wpn_fx[sec_p]) and (sec ~= sec_p) then wpn_fx[sec_p] = tbl end wpn_fx[sec] = tbl else wpn_fx[sec] = true end end local is_looking = false local shotc = 1 -- Counts the number of shots in a consecutive burst local shott = 0 -- Used to determin how far aparts shots can be to be considered part of the same burst local kick_t = 0 -- Time when kick stops local fov_r = 1 function shoot_effect(obj, sect) if shott and (shott < time_global()) then shotc = 1 shott = nil end local recoil_base = wpn_data.recoil or 1 -- update based on actor stance if get_config("stance") then if IsMoveState("mcAnyMove") then recoil_base = recoil_base * 1.5 elseif IsMoveState("mcCrouch") then recoil_base = recoil_base * (IsMoveState("mcAccel") and 0.25 or 0.5) end end local s = wpn_fx[sect].s local sc = #s if sc > 1 then if wpn_fx[sect].r == 1 then s = s[math.random(#s)] elseif wpn_fx[sect].r == 2 then if shotc == 1 then s = s[1] else s = s[math.random(2,#s)] end else sc = shotc <= sc and shotc or (sc > 2 and math.random(sc-1,sc)) or sc s = s[sc] end else s = s[1] end recoil_base = recoil_base * recoil_multiplier(obj, shotc) recoil_anm(sect, recoil_base, s) recoil_camera(obj, recoil_base, s) shotc = shotc+1 shott = time_global()+200 -- adjust based on gun rpm / handling? end -- plays weapon animation, strength depends on recoil function recoil_anm(sect, recoil_base, s) local anims = {} for i,v in pairs(wpn_fx[sect].e) do local cnt,r = math.modf(v[1]*fov_r) if r ~= 0 and cnt > 4 then --fmb cnt = cnt-cnt*math.random(0,r*100)/100 --fmb cnt,r = math.modf(cnt) end if r ~= 0 and math.random() < r then cnt = cnt+1 end if cnt > 0 then table.insert(anims,{e = i, d = v[2] or math.random(0,1), c = cnt}) end end for i,a in ipairs(anims) do local n = string.format([[shoot\s%s_e%s_%s.anm]], s, 2, 0) for ii=1,a.c do -- printf("N: " .. n) level.add_cam_effector(n,math.random(5000, 8000),false,"", 0, false, recoil_base*0.1) end -- printf("----") end for k,v in pairs(wpn_fx[sect].p) do local eid = math.random(5000,8000) level.add_pp_effector(string.format([[shoot\%s_s%s.ppe]],k,s), eid, false) if v > 0 then level.set_pp_effector_factor(eid,v) end end end function camera_effect(obj, recoil_base, h_recoil, strength) -- Assumes h_recoil ranges from -1 to 1 local n = string.format([[shoot\s%s_e%s_%s.anm]], strength, 0, h_recoil < 0 and 0 or 1) level.add_cam_effector(n,math.random(5000, 8000),false,"", 0, false, recoil_base*math.abs(h_recoil)) end -- Invoked whenever a shot is fired and enhanced recoil applies. -- Patch this to modify the magnitude based on the weapon and burst length function recoil_multiplier(wpn, shotc) return 1 end local v_factor = 0.5 local v_recoil = 0 local h_factor = 0.4 local h_recoil = 0 -- consider stance, wpn weight function recoil_camera(obj, recoil_base, s) -- never execute if the functions are not defined -- do not needlessly recompute if this is already firing - wait for time event to reset flag if not (db.actor.actor_look_at_point and db.actor.actor_stop_look_at_point) or recoil_base < 1 or not get_config("camera") then return end local sec = obj:section() local sec_p = ini_sys:r_string_ex(sec,"parent_section") or sec local mult_table = wpn_data.mult_table -- after enough shots climbing stops, math.random will provide a little variance local mult = tonumber(mult_table[shotc]) or (math.random()-0.5)/20 -- printf("Recoil mult is %s, shots fired %s", mult, shotc) local actor_position = device().cam_pos local actor_direction = device().cam_dir local is_zoomed = obj:cast_Weapon():IsZoomed() -- Horizontal -- Base Horizontal Recoil local horizontal_base = is_zoomed == 1 and SYS_GetParam(2, sec, "zoom_cam_step_angle_horz") or SYS_GetParam(2, sec, "cam_step_angle_horz") horizontal_base = horizontal_base * (recoil_base) -- Randomness local h_recoil_rnd = (math.random()-0.5) -- local h_recoil_rnd = math.random(-1, 1) h_recoil = h_recoil_rnd * horizontal_base * h_factor -- Rotate camera left-right actor_direction = vector_rotate_y(actor_direction, h_recoil) camera_effect(obj, recoil_base, h_recoil_rnd*2, s) -- Vertical local vertical_base = is_zoomed == 1 and SYS_GetParam(2, sec, "zoom_cam_dispersion") or SYS_GetParam(2, sec, "cam_dispersion") vertical_base = vertical_base * (recoil_base) local zoom_factor = obj:cast_Weapon():GetZoomFactor() local zoom_kick_factor = is_zoomed and zoom_factor ~= 0 and 5*(1/zoom_factor) or 1 v_recoil = vertical_base * zoom_kick_factor * mult * v_factor actor_direction.y = clamp(actor_direction.y + (v_recoil), -0.999, 0.999) -- printf("recoil_base: " .. recoil_base) kick_t = time_global()+(math.max(50, 100*recoil_base)) -- need to adjust for when scoped in -- update the variables used below, don't reset if already firing if is_looking then return end -- force actor to look at new point of aim db.actor:actor_look_at_point(vector():set(actor_position):add(actor_direction:mul(10000))) is_looking = true demonized_time_events.CreateTimeEvent("updatelook", 1, 0, function() local active_item = db.actor:active_item() if not active_item then return true end -- if died for w/e reason stop immediately if not is_looking then db.actor:actor_stop_look_at_point() return true end if active_item:get_state() == 5 then db.actor:actor_stop_look_at_point() if kick_t < time_global() then is_looking = false return true end actor_direction = device().cam_dir actor_direction.y = clamp(actor_direction.y + (v_recoil), -0.999, 0.999) actor_direction = vector_rotate_y(actor_direction, h_recoil) db.actor:actor_look_at_point(vector():set(device().cam_pos):add(actor_direction:mul(10000))) return false end is_looking = false db.actor:actor_stop_look_at_point() return true end) end function Update_Shooting(obj, wpn, ammo_elapsed, grenade_elapsed, ammo_type, grenade_type) if (obj:id() ~= AC_ID) then return end local wpn_obj = utils_item.item_is_fa(wpn) and wpn if wpn_obj then local sec = wpn_obj:section() if (not wpn_fx[sec]) then parse_anims(sec) end if (wpn_fx[sec] == true) then return end shoot_effect(wpn, sec) end end OriginalShoot = actor_effects.shoot_effect function actor_effects.shoot_effect(sec) if get_config("enabled") then return end OriginalShoot(sec) end function update_settings() if get_config("enabled") then RegisterScriptCallback("actor_on_weapon_fired",Update_Shooting) RegisterScriptCallback("actor_on_weapon_before_fire", actor_on_weapon_before_fire) else UnregisterScriptCallback("actor_on_weapon_fired",Update_Shooting) UnregisterScriptCallback("actor_on_weapon_before_fire", actor_on_weapon_before_fire) end end -- If you don't use MCM, change your defaults from here. local defaults = { ["enabled"] = true, ["camera"] = true, ["stance"] = true, } function get_config(key) if ui_mcm then return ui_mcm.get("recoil/"..key) else return defaults[key] end end function on_mcm_load() op = { id= "recoil",sh=true ,gr={ { id= "title",type= "slide",link= "ui_options_slider_player",text="ui_mcm_recoil_title",size= {512,50},spacing= 20 }, {id = "enabled", type = "check", val = 1, def=true}, {id = "camera", type = "check", val = 1, def=true}, {id = "stance", type = "check", val = 1, def=true}, } } return op end function on_game_start() RegisterScriptCallback("on_option_change",update_settings) update_settings() end