diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm
index 0bfccb6b9113..628fd38d2e5e 100644
--- a/code/modules/projectiles/gun.dm
+++ b/code/modules/projectiles/gun.dm
@@ -109,6 +109,12 @@
///Color of the muzzle flash effect.
var/muzzle_flash_color = COLOR_VERY_SOFT_YELLOW
+ //gun saftey
+ ///Does this gun have a saftey and thus can toggle it?
+ var/has_safety = FALSE
+ ///If the saftey on? If so, we can't fire the weapon
+ var/safety = FALSE
+
/obj/item/gun/Initialize()
. = ..()
RegisterSignal(src, COMSIG_TWOHANDED_WIELD, PROC_REF(on_wield))
@@ -198,6 +204,9 @@
else if(can_bayonet)
. += "It has a bayonet lug on it."
+ if(has_safety)
+ . += "The safety is [safety ? "ON" : "OFF"]. Ctrl-Click to toggle the safety."
+
/obj/item/gun/equipped(mob/living/user, slot)
. = ..()
if(zoomed && user.get_active_held_item() != src)
@@ -210,11 +219,16 @@
//check if there's enough ammo/energy/whatever to shoot one time
//i.e if clicking would make it shoot
/obj/item/gun/proc/can_shoot()
+ if(safety)
+ return FALSE
return TRUE
/obj/item/gun/proc/shoot_with_empty_chamber(mob/living/user as mob|obj)
- to_chat(user, "*[dry_fire_text]*") //WS Edit - Dry firing
- playsound(src, dry_fire_sound, 30, TRUE)
+ if(!safety)
+ to_chat(user, "*[dry_fire_text]*")
+ playsound(src, dry_fire_sound, 30, TRUE)
+ return
+ to_chat(user, "Safeties are active on the [src]! Turn them off to fire!")
/obj/item/gun/proc/shoot_live_shot(mob/living/user, pointblank = 0, atom/pbtarget = null, message = 1)
@@ -484,6 +498,25 @@
else
return ..()
+/obj/item/gun/CtrlClick(mob/user)
+ . = ..()
+ if(!has_safety)
+ return
+
+ if(src == !user.get_active_held_item())
+ return
+
+ playsound(user, 'sound/weapons/gun/general/selector.ogg', 100, TRUE)
+ safety = !safety
+
+ user.visible_message(
+ span_notice("[user] turns the safety on [src] [safety ? "ON" : "OFF"]."),
+ span_notice("You turn the safety on [src] [safety ? "ON" : "OFF"]."),
+ vision_distance = COMBAT_MESSAGE_RANGE
+ )
+ update_appearance()
+
+
/obj/item/gun/screwdriver_act(mob/living/user, obj/item/I)
. = ..()
if(.)
@@ -634,13 +667,19 @@
var/datum/action/A = X
A.UpdateButtonIcon()
+/obj/item/gun/attack_hand(mob/user)
+ . = ..()
+ update_appearance()
+
/obj/item/gun/pickup(mob/user)
- ..()
+ . = ..()
+ update_appearance()
if(azoom)
azoom.Grant(user)
/obj/item/gun/dropped(mob/user)
. = ..()
+ update_appearance()
if(azoom)
azoom.Remove(user)
if(zoomed)
@@ -669,6 +708,15 @@
knife_overlay.pixel_y = knife_y_offset
. += knife_overlay
+ if(ismob(loc) && has_safety)
+ var/mutable_appearance/safety_overlay
+ safety_overlay = mutable_appearance('icons/obj/guns/safety.dmi')
+ if(safety)
+ safety_overlay.icon_state = "safety-on"
+ else
+ safety_overlay.icon_state = "safety-off"
+ . += safety_overlay
+
/obj/item/gun/proc/handle_suicide(mob/living/carbon/human/user, mob/living/carbon/human/target, params, bypass_timer)
if(!ishuman(user) || !ishuman(target))
return
diff --git a/code/modules/projectiles/guns/ballistic.dm b/code/modules/projectiles/guns/ballistic.dm
index 90bcf0c73508..a8e2a201c81a 100644
--- a/code/modules/projectiles/guns/ballistic.dm
+++ b/code/modules/projectiles/guns/ballistic.dm
@@ -6,6 +6,9 @@
icon_state = "pistol"
w_class = WEIGHT_CLASS_NORMAL
+ has_safety = TRUE
+ safety = TRUE
+
///sound when inserting magazine
var/load_sound = 'sound/weapons/gun/general/magazine_insert_full.ogg'
///sound when inserting an empty magazine
@@ -88,7 +91,7 @@
///Whether the gun can be tacloaded by slapping a fresh magazine directly on it
var/tac_reloads = TRUE //Snowflake mechanic no more.
///If we have the 'snowflake mechanic,' how long should it take to reload?
- var/tactical_reload_delay = 1.2 SECONDS
+ var/tactical_reload_delay = 1 SECONDS
/obj/item/gun/ballistic/Initialize()
. = ..()
@@ -242,6 +245,8 @@
update_appearance()
/obj/item/gun/ballistic/can_shoot()
+ if(safety)
+ return FALSE
return chambered
/obj/item/gun/ballistic/attackby(obj/item/A, mob/user, params)
diff --git a/code/modules/projectiles/guns/ballistic/pistol.dm b/code/modules/projectiles/guns/ballistic/pistol.dm
index b466f2811dd4..591bc7eafa3d 100644
--- a/code/modules/projectiles/guns/ballistic/pistol.dm
+++ b/code/modules/projectiles/guns/ballistic/pistol.dm
@@ -237,6 +237,9 @@
can_suppress = FALSE
var/random_icon = TRUE
+ has_safety = FALSE //thing barely costs anything, why would it have a safety?
+ safety = FALSE
+
/obj/item/gun/ballistic/automatic/pistol/disposable/Initialize()
. = ..()
var/picked = pick("none","red","purple","yellow","green","dark")
diff --git a/code/modules/projectiles/guns/ballistic/revolver.dm b/code/modules/projectiles/guns/ballistic/revolver.dm
index 1e922d26aed0..74c7f95d0db2 100644
--- a/code/modules/projectiles/guns/ballistic/revolver.dm
+++ b/code/modules/projectiles/guns/ballistic/revolver.dm
@@ -24,6 +24,9 @@
bolt_wording = "hammer"
wield_slowdown = 0.3
+ has_safety = FALSE //irl revolvers dont have safetys. i think. maybe
+ safety = FALSE
+
/obj/item/gun/ballistic/revolver/examine(mob/user)
. = ..()
. += "You can use the revolver with your other empty hand to empty the cylinder."
diff --git a/code/modules/projectiles/guns/energy.dm b/code/modules/projectiles/guns/energy.dm
index 2a16164c6119..681d79338a52 100644
--- a/code/modules/projectiles/guns/energy.dm
+++ b/code/modules/projectiles/guns/energy.dm
@@ -7,6 +7,9 @@
muzzleflash_iconstate = "muzzle_flash_laser"
muzzle_flash_color = COLOR_SOFT_RED
+ has_safety = TRUE
+ safety = TRUE
+
var/obj/item/stock_parts/cell/gun/cell //What type of power cell this uses
var/cell_type = /obj/item/stock_parts/cell/gun
var/modifystate = 0
@@ -147,7 +150,9 @@
eject_cell(user)
return ..()
-/obj/item/gun/energy/can_shoot()
+/obj/item/gun/energy/can_shoot(visuals)
+ if(safety && !visuals)
+ return FALSE
var/obj/item/ammo_casing/energy/shot = ammo_type[select]
return !QDELETED(cell) ? (cell.charge >= shot.e_cost) : FALSE
@@ -252,7 +257,7 @@
///Used by update_icon_state() and update_overlays()
/obj/item/gun/energy/proc/get_charge_ratio()
- return can_shoot() ? CEILING(clamp(cell.charge / cell.maxcharge, 0, 1) * charge_sections, 1) : 0
+ return can_shoot(visuals = TRUE) ? CEILING(clamp(cell.charge / cell.maxcharge, 0, 1) * charge_sections, 1) : 0
// Sets the ratio to 0 if the gun doesn't have enough charge to fire, or if its power cell is removed.
/obj/item/gun/energy/vv_edit_var(var_name, var_value)
diff --git a/code/modules/projectiles/guns/energy/special.dm b/code/modules/projectiles/guns/energy/special.dm
index 9226a587e4aa..2037e77fbed0 100644
--- a/code/modules/projectiles/guns/energy/special.dm
+++ b/code/modules/projectiles/guns/energy/special.dm
@@ -384,7 +384,7 @@
return
return ..()
-/obj/item/gun/energy/gravity_gun/can_shoot()
+/obj/item/gun/energy/gravity_gun/can_shoot(visuals)
if(!firing_core)
return FALSE
return ..()
diff --git a/code/modules/unit_tests/projectiles.dm b/code/modules/unit_tests/projectiles.dm
index 4950be10c1a6..e93d20910af0 100644
--- a/code/modules/unit_tests/projectiles.dm
+++ b/code/modules/unit_tests/projectiles.dm
@@ -19,6 +19,7 @@
gunner.put_in_hands(test_gun, forced=TRUE)
var/expected_damage = loaded_bullet.damage
loaded_bullet.def_zone = BODY_ZONE_CHEST
+ test_gun.safety = FALSE //So we can shoot the gun
var/did_we_shoot = test_gun.afterattack(victim, gunner)
TEST_ASSERT(did_we_shoot, "Gun does not appeared to have successfully fired.")
TEST_ASSERT_EQUAL(victim.getBruteLoss(), expected_damage, "Victim took incorrect amount of damage, expected [expected_damage], got [victim.getBruteLoss()].")
diff --git a/icons/obj/guns/safety.dmi b/icons/obj/guns/safety.dmi
new file mode 100644
index 000000000000..072a483fa795
Binary files /dev/null and b/icons/obj/guns/safety.dmi differ