diff --git a/code/modules/client/preferences/accessibility.dm b/code/modules/client/preferences/accessibility.dm index 1cc413ffca5..6af31083f24 100644 --- a/code/modules/client/preferences/accessibility.dm +++ b/code/modules/client/preferences/accessibility.dm @@ -19,6 +19,16 @@ savefile_key = "remove_double_click" savefile_identifier = PREFERENCE_PLAYER +/datum/preference/numeric/min_recoil_multiplier + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + maximum = 200 + minimum = 0 + savefile_key = "min_recoil_multiplier" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/numeric/min_recoil_multiplier/create_default_value() + return 100 + /// When toggled, enables staircase indicators /datum/preference/toggle/stair_indicator category = PREFERENCE_CATEGORY_GAME_PREFERENCES diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm index b637e33eb6f..d340bc5268c 100644 --- a/code/modules/mob/mob_helpers.dm +++ b/code/modules/mob/mob_helpers.dm @@ -174,9 +174,10 @@ if(C.prefs?.read_preference(/datum/preference/toggle/screen_shake_darken)) var/type = /atom/movable/screen/fullscreen/flash/black + var/shake_dur = max(duration, 2 SECONDS) M.overlay_fullscreen("flash", type) - addtimer(CALLBACK(M, TYPE_PROC_REF(/mob, clear_fullscreen), "flash", 3 SECONDS), 3 SECONDS) + addtimer(CALLBACK(M, TYPE_PROC_REF(/mob, clear_fullscreen), "flash", 1 SECONDS), shake_dur) //How much time to allot for each pixel moved var/time_scalar = (1 / ICON_SIZE_ALL) * TILES_PER_SECOND @@ -206,6 +207,31 @@ #undef TILES_PER_SECOND +/// Helper proc for a similar function to shake_camera, expect this one kicks the camera back at an opposite angle rather then skake_camera's irratic jittering. +/proc/recoil_camera(mob/recoiled_mob, duration, backtime_duration, strength, angle) + if(!recoiled_mob || !recoiled_mob.client || duration < 1) + return + var/client/my_client = recoiled_mob.client + strength *= world.icon_size + var/client/client_to_shake = recoiled_mob.client + var/oldx = client_to_shake.pixel_x + var/oldy = client_to_shake.pixel_y + + //get pixels to move the camera in an angle + var/mpx = sin(angle) * strength + var/mpy = cos(angle) * strength + + + if(my_client.prefs?.read_preference(/datum/preference/toggle/screen_shake_darken)) + var/type = /atom/movable/screen/fullscreen/flash/black + var/shake_dur = max(duration, 2 SECONDS) + recoiled_mob.overlay_fullscreen("flash", type) + addtimer(CALLBACK(recoiled_mob, TYPE_PROC_REF(/mob, clear_fullscreen), "flash", 1 SECONDS), shake_dur) + + + animate(client_to_shake, pixel_x = oldx+mpx, pixel_y = oldy+mpy, time = duration, flags = ANIMATION_RELATIVE) + animate(pixel_x = oldx, pixel_y = oldy, time = backtime_duration, easing = BACK_EASING) + ///Find if the message has the real name of any user mob in the mob_list /proc/findname(msg) if(!istext(msg)) diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm index 06d5d1b64cc..277cadb5ab2 100644 --- a/code/modules/projectiles/gun.dm +++ b/code/modules/projectiles/gun.dm @@ -35,6 +35,7 @@ var/light_time = 0.1 SECONDS var/gun_flags = NONE + var/fire_sound = 'sound/items/weapons/gun/pistol/shot.ogg' var/vary_fire_sound = TRUE var/fire_sound_volume = 50 @@ -47,7 +48,7 @@ var/suppressed_volume = 60 /// Whether a gun can be unsuppressed. for ballistics, also determines if it generates a suppressor overlay var/can_unsuppress = TRUE - var/recoil = 0 //boom boom shake the room + var/clumsy_check = TRUE var/obj/item/ammo_casing/chambered = null trigger_guard = TRIGGER_GUARD_NORMAL //trigger guard on the weapon, hulks can't fire them with their big meaty fingers @@ -62,7 +63,6 @@ /// firing cooldown, true if this gun shouldn't be allowed to manually fire var/fire_cd = 0 var/weapon_weight = WEAPON_LIGHT - var/dual_wield_spread = 24 //additional spread when dual wielding ///Can we hold up our target with this? Default to yes var/can_hold_up = TRUE /// If TRUE, and we aim at ourselves, it will initiate a do after to fire at ourselves. @@ -85,6 +85,18 @@ var/spread = 0 //Spread induced by the gun itself. var/randomspread = 1 //Set to 0 for shotguns. This is used for weapons that don't fire all their bullets at once. + var/dual_wield_spread = 24 //additional spread when dual wielding + + ///Screen shake when the weapon is fired + var/recoil = 0 + ///a multiplier of the duration the recoil takes to go back to normal view, this is (recoil*recoil_backtime_multiplier)+1 + var/recoil_backtime_multiplier = 1.5 + ///this is how much deviation the gun recoil can have, recoil pushes the screen towards the reverse angle you shot + some deviation which this is the max. + var/recoil_deviation = 20 + /// Used as the min value when calculating recoil + /// Affected by a player's min_recoil_multiplier preference, so keep in mind it can ultimately be 0 regardless + /// Often utilized as a "purely visual" form of recoil (as it can be disabled) + var/min_recoil = 0 lefthand_file = 'icons/mob/inhands/weapons/guns_lefthand.dmi' righthand_file = 'icons/mob/inhands/weapons/guns_righthand.dmi' @@ -228,8 +240,9 @@ set_light_on(FALSE) /obj/item/gun/proc/shoot_live_shot(mob/living/user, pointblank = FALSE, atom/pbtarget = null, message = TRUE) - if(recoil && !tk_firing(user)) - shake_camera(user, recoil + 1, recoil) + if(!tk_firing(user)) + var/actual_angle = get_angle((user || get_turf(src)), pbtarget) + simulate_recoil(user, recoil, actual_angle) fire_sounds() muzzle_flash_on() if(suppressed || !message) @@ -489,6 +502,39 @@ update_appearance() return TRUE +/** + * Calculates the final recoil value applied when firing a gun. + * + * Arguments: + * * user - The living mob attempting to fire the gun. Used for preference lookups. + * * recoil_amount - The raw recoil value to be processed before clamping. + * + * Returns: + * The clamped recoil value after applying all modifiers. + */ +/obj/item/gun/proc/calculate_recoil(mob/living/user, recoil_amount = 0) + var/used_min_recoil = min_recoil + if(user.client) + used_min_recoil *= (user.client.prefs.read_preference(/datum/preference/numeric/min_recoil_multiplier) / 100) + return clamp(recoil_amount, used_min_recoil, INFINITY) + +/** + * Simulates firearm recoil and applies camera feedback when firing. + * + * Arguments: + * * user - The mob firing the gun. Used for recoil calculation and camera shake. + * * recoil_amount - The base recoil value before modifiers. + * * firing_angle - The firing direction used to determine camera kick direction. + */ +/obj/item/gun/proc/simulate_recoil(mob/living/user, recoil_amount = 0, firing_angle) + var/total_recoil = calculate_recoil(user, recoil_amount) + + var/actual_angle = firing_angle + rand(-recoil_deviation, recoil_deviation) + 180 + if(actual_angle > 360) + actual_angle -= 360 + if(total_recoil > 0) + recoil_camera(user, total_recoil + 1, (total_recoil * recoil_backtime_multiplier)+1, total_recoil, actual_angle) + ///returns true if the gun successfully fires /obj/item/gun/proc/process_fire(atom/target, mob/living/user, message = TRUE, params = null, zone_override = "", bonus_spread = 0) var/base_bonus_spread = 0 diff --git a/code/modules/projectiles/guns/ballistic.dm b/code/modules/projectiles/guns/ballistic.dm index c3a82c96b72..d9c2ce77fcc 100644 --- a/code/modules/projectiles/guns/ballistic.dm +++ b/code/modules/projectiles/guns/ballistic.dm @@ -10,6 +10,8 @@ drop_sound = 'sound/items/handling/gun/gun_drop.ogg' sound_vary = TRUE + min_recoil = 0.1 + ///sound when inserting magazine var/load_sound = 'sound/items/weapons/gun/general/magazine_insert_full.ogg' ///sound when inserting an empty magazine diff --git a/code/modules/projectiles/guns/ballistic/pistol.dm b/code/modules/projectiles/guns/ballistic/pistol.dm index 4f1d86842e6..6d3e407d6e8 100644 --- a/code/modules/projectiles/guns/ballistic/pistol.dm +++ b/code/modules/projectiles/guns/ballistic/pistol.dm @@ -25,6 +25,7 @@ bolt_wording = "slide" suppressor_x_offset = 10 suppressor_y_offset = -1 + recoil_backtime_multiplier = 1 /obj/item/gun/ballistic/automatic/pistol/no_mag spawnwithmagazine = FALSE diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/accessibility.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/accessibility.tsx index dd91fa7c1d5..1beda34b292 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/accessibility.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/accessibility.tsx @@ -1,4 +1,4 @@ -import { CheckboxInput, type FeatureToggle } from '../base'; +import { CheckboxInput, type Feature, type FeatureToggle, FeatureSliderInput } from '../base'; export const darkened_flash: FeatureToggle = { name: 'Enable darkened flashes', @@ -29,6 +29,16 @@ export const remove_double_click: FeatureToggle = { component: CheckboxInput, }; +export const min_recoil_multiplier: Feature = { + name: 'Cosmetic Recoil Strength', + category: 'ACCESSIBILITY', + description: ` + Modifies the strength of cosmetic recoil's effect on your camera. + 0 will disable cosmetic recoil entirely, though mechanical recoil will be unaffected. + `, + component: FeatureSliderInput, +}; + export const stair_indicator: FeatureToggle = { name: 'Enable stair indicator', category: 'ACCESSIBILITY',