From d14fc362860ffdac8b7d40f80495dd26b895ff61 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Tue, 15 Apr 2025 13:00:43 -0500 Subject: [PATCH] Fixes Russian Roulette by refactoring the gun to not be an affront to god (#90426) Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com> Co-authored-by: ATH1909 <42606352+ATH1909@users.noreply.github.com> Co-authored-by: SmArtKar <44720187+SmArtKar@users.noreply.github.com> --- .../mood_events/generic_negative_events.dm | 5 + code/modules/projectiles/gun.dm | 48 ++-- .../projectiles/guns/ballistic/revolver.dm | 224 ++++++++++++------ .../projectiles/guns/energy/laser_gatling.dm | 4 +- .../guns/special/grenade_launcher.dm | 1 + .../projectiles/guns/special/medbeam.dm | 1 + .../projectiles/guns/special/syringe_gun.dm | 5 +- 7 files changed, 193 insertions(+), 95 deletions(-) diff --git a/code/datums/mood_events/generic_negative_events.dm b/code/datums/mood_events/generic_negative_events.dm index 7fa9cf37d91..3de6dd5b127 100644 --- a/code/datums/mood_events/generic_negative_events.dm +++ b/code/datums/mood_events/generic_negative_events.dm @@ -434,6 +434,11 @@ mood_change = -3 timeout = 3 MINUTES +/datum/mood_event/russian_roulette_lose_cheater + description = "I gambled and lost! Good thing I wasn't aiming for my head..." + mood_change = -10 + timeout = 10 MINUTES + /datum/mood_event/russian_roulette_lose description = "I gambled my life and lost! I guess this is the end..." mood_change = -20 diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm index 9084dbcd534..db816fb0eb7 100644 --- a/code/modules/projectiles/gun.dm +++ b/code/modules/projectiles/gun.dm @@ -52,6 +52,9 @@ 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 = FALSE // SKYRAT EDIT - DISABLED ORIGINAL: TRUE + /// If TRUE, and we aim at ourselves, it will initiate a do after to fire at ourselves. + /// If FALSE it will just try to fire at ourselves straight up. + var/doafter_self_shoot = TRUE /// Just 'slightly' snowflakey way to modify projectile damage for projectiles fired from this gun. var/projectile_damage_multiplier = 1 @@ -210,27 +213,41 @@ return FALSE if(tk_firing(user)) visible_message( - span_danger("[src] fires itself[pointblank ? " point blank at [pbtarget]!" : "!"]"), - blind_message = span_hear("You hear a gunshot!"), - vision_distance = COMBAT_MESSAGE_RANGE + span_danger("[src] fires itself[pointblank ? " point blank at [pbtarget]!" : "!"]"), + blind_message = span_hear("You hear a gunshot!"), + vision_distance = COMBAT_MESSAGE_RANGE ) else if(pointblank) - user.visible_message( + if(user == pbtarget) + user.visible_message( + span_danger("[user] fires [src] point blank at [user.p_them()]self!"), + span_userdanger("You fire [src] point blank at yourself!"), + span_hear("You hear a gunshot!"), + vision_distance = COMBAT_MESSAGE_RANGE, + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, + ) + else + user.visible_message( span_danger("[user] fires [src] point blank at [pbtarget]!"), span_danger("You fire [src] point blank at [pbtarget]!"), - span_hear("You hear a gunshot!"), COMBAT_MESSAGE_RANGE, pbtarget - ) - to_chat(pbtarget, span_userdanger("[user] fires [src] point blank at you!")) + span_hear("You hear a gunshot!"), + vision_distance = COMBAT_MESSAGE_RANGE, + ignored_mobs = pbtarget, + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, + ) + to_chat(pbtarget, span_userdanger("[user] fires [src] point blank at you!")) if(pb_knockback > 0 && ismob(pbtarget)) var/mob/PBT = pbtarget var/atom/throw_target = get_edge_target_turf(PBT, user.dir) PBT.throw_at(throw_target, pb_knockback, 2) else if(!tk_firing(user)) user.visible_message( - span_danger("[user] fires [src]!"), - blind_message = span_hear("You hear a gunshot!"), - vision_distance = COMBAT_MESSAGE_RANGE, - ignored_mobs = user + span_danger("[user] fires [src]!"), + span_danger("You fire [src]!"), + span_hear("You hear a gunshot!"), + vision_distance = COMBAT_MESSAGE_RANGE, + ignored_mobs = user, + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, ) if(chambered?.integrity_damage) @@ -339,7 +356,7 @@ return if(!ismob(target)) //melee attack return - if(target == user && user.zone_selected != BODY_ZONE_PRECISE_MOUTH) //so we can't shoot ourselves (unless mouth selected) + if(target == user && (user.zone_selected != BODY_ZONE_PRECISE_MOUTH && doafter_self_shoot)) //so we can't shoot ourselves (unless mouth selected) return if(iscarbon(target)) var/mob/living/carbon/C = target @@ -353,10 +370,9 @@ if(!can_trigger_gun(L)) return - if(flag) - if(user.zone_selected == BODY_ZONE_PRECISE_MOUTH) - handle_suicide(user, target, params) - return + if(flag && doafter_self_shoot && user.zone_selected == BODY_ZONE_PRECISE_MOUTH) + handle_suicide(user, target, params) + return if(!can_shoot()) //Just because you can pull the trigger doesn't mean it can shoot. shoot_with_empty_chamber(user) diff --git a/code/modules/projectiles/guns/ballistic/revolver.dm b/code/modules/projectiles/guns/ballistic/revolver.dm index 786d910e34a..19feca04b16 100644 --- a/code/modules/projectiles/guns/ballistic/revolver.dm +++ b/code/modules/projectiles/guns/ballistic/revolver.dm @@ -17,8 +17,9 @@ var/last_fire = 0 /obj/item/gun/ballistic/revolver/process_fire(atom/target, mob/living/user, message, params, zone_override, bonus_spread) - ..() - last_fire = world.time + . = ..() + if(.) + last_fire = world.time /obj/item/gun/ballistic/revolver/chamber_round(spin_cylinder = TRUE, replace_new_round) @@ -99,7 +100,7 @@ var/live_ammo = get_ammo(FALSE, FALSE) . += "[live_ammo ? live_ammo : "None"] of those are live rounds." if (current_skin) - . += "It can be spun with alt+click" + . += span_notice("It can be spun with [EXAMINE_HINT("alt-click")].") /obj/item/gun/ballistic/revolver/ignition_effect(atom/A, mob/user) if(last_fire && last_fire + 15 SECONDS > world.time) @@ -185,22 +186,47 @@ desc = "A Russian-made revolver for drinking games. Uses .357 ammo, and has a mechanism requiring you to spin the chamber before each trigger pull." icon_state = "russianrevolver" accepted_magazine_type = /obj/item/ammo_box/magazine/internal/cylinder/rus357 - var/spun = FALSE hidden_chambered = TRUE //Cheater. gun_flags = NOT_A_REAL_GUN + can_hold_up = FALSE // for obvious reasons + doafter_self_shoot = FALSE // snowflake + /// If we've been spun before firing + var/spun = FALSE + /// Do after for trying to fire the gun + var/aim_time = 4 SECONDS + +/obj/item/gun/ballistic/revolver/russian/examine(mob/user) + . = ..() + . += span_notice("You can change length of your pause before pulling the trigger with [EXAMINE_HINT("alt-right-click")].") + +/obj/item/gun/ballistic/revolver/russian/click_alt_secondary(mob/user) + if(loc != user) + to_chat(user, span_warning("You need to be holding the gun to determine how long you are going to pause!")) + return CLICK_ACTION_BLOCKING + var/new_aim_time = tgui_input_number(user, "How long will you pause before pulling the trigger (seconds)?", "Do you feel lucky?", (aim_time / (1 SECONDS)), 10, 0) + if(loc != user || user.incapacitated) + return CLICK_ACTION_BLOCKING + aim_time = new_aim_time * (1 SECONDS) + to_chat(user, span_warning("You're going to pause [aim_time] second\s before pulling the trigger[aim_time == 0 ? "... Good luck" : ""].")) + return CLICK_ACTION_SUCCESS + +/obj/item/gun/ballistic/revolver/russian/dropped(mob/user, silent) + . = ..() + aim_time = initial(aim_time) // next person chooses their own time /obj/item/gun/ballistic/revolver/russian/do_spin() . = ..() if(.) spun = TRUE -/obj/item/gun/ballistic/revolver/russian/attackby(obj/item/A, mob/user, params) - ..() - if(get_ammo() > 0) - spin() - update_appearance() - A.update_appearance() - return +/obj/item/gun/ballistic/revolver/russian/can_shoot() + return TRUE // we ALWAYS want to shoot. even if we don't have a chambered round, even if our chambered round has no bullet + +/obj/item/gun/ballistic/revolver/russian/load_gun(obj/item/ammo, mob/living/user) + . = ..() + if(!.) + return + do_spin() /obj/item/gun/ballistic/revolver/russian/can_trigger_gun(mob/living/user, akimbo_usage) if(akimbo_usage) @@ -210,94 +236,140 @@ /obj/item/gun/ballistic/revolver/russian/attack_self(mob/user) if(!spun) spin() - spun = TRUE - return - ..() + return TRUE + return ..() -/obj/item/gun/ballistic/revolver/russian/fire_gun(atom/target, mob/living/user, flag, params) - . = ..(null, user, flag, params) +/obj/item/gun/ballistic/revolver/russian/handle_chamber(empty_chamber = TRUE, from_firing = TRUE, chamber_next_round = TRUE) + from_firing = FALSE // never eject casings from firing the gun + return ..() - var/tk_controlled = FALSE - if(flag) - if(!(target in user.contents) && ismob(target)) - if(user.combat_mode) // Flogging action - return - else if (HAS_TRAIT_FROM_ONLY(src, TRAIT_TELEKINESIS_CONTROLLED, REF(user))) // if we're far away, you can still fire it at yourself if you have TK. - tk_controlled = TRUE - - if(isliving(user)) - if(!can_trigger_gun(user)) - return +/obj/item/gun/ballistic/revolver/russian/try_fire_gun(atom/target, mob/living/user, params) + if(user.combat_mode) + return FALSE // melee attack if(target != user) - playsound(src, dry_fire_sound, 30, TRUE) + shoot_with_empty_chamber(user) + spun = FALSE user.visible_message( - span_danger("[user.name] tries to fire \the [src] at the same time, but only succeeds at looking like an idiot."), + span_danger("[user] tries to fire \the [src] aimed at something else, but only succeeds at looking like an idiot."), span_danger("\The [src]'s anti-combat mechanism prevents you from firing it at anyone but yourself!"), ) - return + return TRUE // no melee attack + if(!spun) + to_chat(user, span_warning("You need to spin \the [src]'s chamber first!")) + return TRUE // no melee attack + if(HAS_TRAIT(user, TRAIT_CURSED)) // I cannot live, I cannot die, trapped in myself, body my holding cell. + to_chat(user, span_warning("What a horrible night... To have a curse!")) + return TRUE // no melee attack + if(loc != user) + if(tk_firing(user)) + to_chat(user, span_warning("Russian roulette is stressful enough without trying to focus on telekinesis!")) + else + to_chat(user, span_warning("You need to be holding the gun to fire it!")) + return TRUE // no melee attack - if(ishuman(user)) - var/mob/living/carbon/human/H = user - if(!spun) - to_chat(user, span_warning("You need to spin \the [src]'s chamber first!")) - return + return ..() // try to shoot the gun - spun = FALSE - - var/zone = check_zone(user.zone_selected) - var/obj/item/bodypart/affecting = H.get_bodypart(zone) - var/is_target_face = zone == BODY_ZONE_HEAD || zone == BODY_ZONE_PRECISE_EYES || zone == BODY_ZONE_PRECISE_MOUTH - var/loaded_rounds = get_ammo(FALSE, FALSE) // check before it is fired - - if(HAS_TRAIT(user, TRAIT_CURSED)) // I cannot live, I cannot die, trapped in myself, body my holding cell. - to_chat(user, span_warning("What a horrible night... To have a curse!")) - return - - if(loaded_rounds && is_target_face) - add_memory_in_range(user, 7, /datum/memory/witnessed_russian_roulette, \ - protagonist = user, \ - antagonist = src, \ - rounds_loaded = loaded_rounds, \ - aimed_at = affecting.name, \ - result = (chambered ? "lost" : "won"), \ +// Replaces clumsy check with a do after +/obj/item/gun/ballistic/revolver/russian/check_botched(mob/living/user, atom/target) + if(aim_time <= 0) + return FALSE + user.visible_message( + span_danger("[user] aims \the [src] at [user.p_their()] [parse_zone(user.zone_selected)]..."), + span_userdanger("You aim \the [src] at your [parse_zone(user.zone_selected)]..."), + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, + ) + if(prob(10) && !HAS_TRAIT(user, TRAIT_FEARLESS)) + user.adjust_jitter(aim_time) + if(!do_after(user, aim_time, target)) + if(!user.incapacitated) + user.visible_message( + span_danger("[user] loses [user.p_their()] nerve and puts \the [src] down."), + span_userdanger("You lose your nerve and put \the [src] down."), + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, ) + return TRUE + return FALSE - if(chambered) - var/obj/item/ammo_casing/AC = chambered - if(AC.fire_casing(user, user, params, distro = 0, quiet = 0, zone_override = null, spread = 0, fired_from = src)) - playsound(user, fire_sound, fire_sound_volume, vary_fire_sound) - if(is_target_face) - shoot_self(user, affecting) - else if(tk_controlled) // the consequence of you doing the telekinesis stuff - to_chat(user, span_userdanger("As your mind concentrates on the revolver, you realize that it's pointing towards your head a little too late!")) - shoot_self(user, BODY_ZONE_HEAD) - else - user.visible_message(span_danger("[user.name] cowardly fires [src] at [user.p_their()] [affecting.name]!"), span_userdanger("You cowardly fire [src] at your [affecting.name]!"), span_hear("You hear a gunshot!")) - chambered = null - user.add_mood_event("russian_roulette_lose", /datum/mood_event/russian_roulette_lose) - return +/obj/item/gun/ballistic/revolver/russian/before_firing(atom/target, mob/user) + if(target != user) + CRASH("Russian revolver somehow got to before_firing with a target that isn't the user!") + // we will definitely have a chambered round, but not always projectile + if(check_zone(user.zone_selected) == BODY_ZONE_HEAD) + chambered.loaded_projectile?.damage = 300 + chambered.loaded_projectile?.wound_bonus = 100 + else + chambered.loaded_projectile?.damage = 80 + chambered.loaded_projectile?.wound_bonus = 10 +/obj/item/gun/ballistic/revolver/russian/fire_gun(atom/target, mob/living/user, flag, params) + // . = false = no shot fired + . = ..() + spun = FALSE + var/is_target_face = check_zone(user.zone_selected) == BODY_ZONE_HEAD + var/aimed_at_readable = parse_zone(user.zone_selected) + var/loaded_rounds = get_ammo(FALSE, FALSE) // check before it is fired + if(loaded_rounds && is_target_face) + add_memory_in_range(user, 7, /datum/memory/witnessed_russian_roulette, \ + protagonist = user, \ + antagonist = src, \ + rounds_loaded = loaded_rounds, \ + aimed_at = aimed_at_readable, \ + result = (. ? "lost" : "won"), \ + ) + + if(!.) if(loaded_rounds && is_target_face) user.add_mood_event("russian_roulette_win", /datum/mood_event/russian_roulette_win, loaded_rounds) + user.visible_message( + span_danger("[user][is_target_face ? "": " cowardly"] points \the [src] at [user.p_their()] [aimed_at_readable], pulls the trigger, and... nothing happens!"), + span_danger("You[is_target_face ? "": " cowardly"] point \the [src] at your [aimed_at_readable], pull the trigger, and... nothing happens!"), + span_hear("You hear a click!"), + vision_distance = COMBAT_MESSAGE_RANGE, + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, + ) + return TRUE // so they don't hit themselves in the forehead. because returning FALSE translates to "do melee attack" for whatever reason - user.visible_message(span_danger("*click*")) - playsound(src, dry_fire_sound, 30, TRUE) + user.visible_message( + span_danger("[user][is_target_face ? "": " cowardly"] aims \the [src] at [user.p_their()] [aimed_at_readable] as it goes off!"), + span_danger("You[is_target_face ? "": " cowardly"] aim \the [src] at your [aimed_at_readable] as it goes off![user.stat >= HARD_CRIT ? " Everything suddenly goes black." : ""]"), + span_hear("You hear a grunt[user.stat == CONSCIOUS ? "" : ", followed by a thud"]!"), + vision_distance = COMBAT_MESSAGE_RANGE, + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, + ) + shoot_self(user, check_zone(user.zone_selected)) + return . +/// Called after successfully(if you can call it that) shooting ourselves /obj/item/gun/ballistic/revolver/russian/proc/shoot_self(mob/living/carbon/human/user, affecting = BODY_ZONE_HEAD) - user.apply_damage(300, BRUTE, affecting) - user.visible_message(span_danger("[user.name] fires [src] at [user.p_their()] head!"), span_userdanger("You fire [src] at your head!"), span_hear("You hear a gunshot!")) + user.add_mood_event( + "russian_roulette_lose", + affecting == BODY_ZONE_HEAD ? /datum/mood_event/russian_roulette_lose : /datum/mood_event/russian_roulette_lose_cheater, + ) /obj/item/gun/ballistic/revolver/russian/soul name = "cursed Russian revolver" desc = "To play with this revolver requires wagering your very soul." -/obj/item/gun/ballistic/revolver/russian/soul/shoot_self(mob/living/user) +/obj/item/gun/ballistic/revolver/russian/soul/shoot_self(mob/living/user, affecting = BODY_ZONE_HEAD) . = ..() - var/obj/item/soulstone/anybody/revolver/stone = new /obj/item/soulstone/anybody/revolver(get_turf(src)) - if(!stone.capture_soul(user, forced = TRUE)) //Something went wrong - qdel(stone) + if(affecting == BODY_ZONE_HEAD) + var/obj/item/soulstone/anybody/revolver/stone = new(user.drop_location()) + if(!stone.capture_soul(user, forced = TRUE)) //Something went wrong + qdel(stone) + return + user.visible_message( + span_danger("[user]'s soul is captured by \the [src]!"), + span_userdanger("You've lost the gamble! Your soul is forfeit!"), + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, + ) return - user.visible_message(span_danger("[user.name]'s soul is captured by \the [src]!"), span_userdanger("You've lost the gamble! Your soul is forfeit!")) + + user.visible_message( + span_danger("[user] is punished for trying to cheat the game!"), + span_userdanger("You've lost the gamble! Not only is your soul forfeit, but it is whisked away for attempting to cheat death!"), + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, + ) + user.dust(drop_items = TRUE) /obj/item/gun/ballistic/revolver/reverse //Fires directly at its user... unless the user is a clown, of course. clumsy_check = FALSE diff --git a/code/modules/projectiles/guns/energy/laser_gatling.dm b/code/modules/projectiles/guns/energy/laser_gatling.dm index eb2c74f3c73..d5612119c3b 100644 --- a/code/modules/projectiles/guns/energy/laser_gatling.dm +++ b/code/modules/projectiles/guns/energy/laser_gatling.dm @@ -135,7 +135,9 @@ if(ammo_pack && ammo_pack.overheat >= ammo_pack.overheat_max) to_chat(user, span_warning("The gun's heat sensor locked the trigger to prevent lens damage!")) return - ..() + . = ..() + if(!.) + return ammo_pack.overheat++ if(ammo_pack.battery) var/transferred = ammo_pack.battery.use(cell.maxcharge - cell.charge, force = TRUE) diff --git a/code/modules/projectiles/guns/special/grenade_launcher.dm b/code/modules/projectiles/guns/special/grenade_launcher.dm index e57a041e60a..dcb182782cb 100644 --- a/code/modules/projectiles/guns/special/grenade_launcher.dm +++ b/code/modules/projectiles/guns/special/grenade_launcher.dm @@ -54,3 +54,4 @@ F.icon_state = initial(F.icon_state) + "_active" playsound(user.loc, 'sound/items/weapons/armbomb.ogg', 75, TRUE, -3) addtimer(CALLBACK(F, TYPE_PROC_REF(/obj/item/grenade, detonate)), 1.5 SECONDS) + return TRUE diff --git a/code/modules/projectiles/guns/special/medbeam.dm b/code/modules/projectiles/guns/special/medbeam.dm index b317a8ddabf..375cffe14b7 100644 --- a/code/modules/projectiles/guns/special/medbeam.dm +++ b/code/modules/projectiles/guns/special/medbeam.dm @@ -68,6 +68,7 @@ START_PROCESSING(SSobj, src) SSblackbox.record_feedback("tally", "gun_fired", 1, type) + return TRUE /obj/item/gun/medbeam/process() if(!mounted && !isliving(loc)) diff --git a/code/modules/projectiles/guns/special/syringe_gun.dm b/code/modules/projectiles/guns/special/syringe_gun.dm index c2448fa7085..a20d3423cdc 100644 --- a/code/modules/projectiles/guns/special/syringe_gun.dm +++ b/code/modules/projectiles/guns/special/syringe_gun.dm @@ -230,8 +230,9 @@ trigger_guard = TRIGGER_GUARD_ALLOW_ALL /obj/item/gun/syringe/blowgun/process_fire(atom/target, mob/living/user, message = TRUE, params = null, zone_override = "", bonus_spread = 0) + . = ..() + if(!.) + return visible_message(span_danger("[user] shoots the blowgun!")) - user.adjustStaminaLoss(20, updating_stamina = FALSE) user.adjustOxyLoss(20) - return ..()