From 36def0fc7e15fcb6315ffa39858950ad51b334aa Mon Sep 17 00:00:00 2001
From: Sun-Soaked <45698967+Sun-Soaked@users.noreply.github.com>
Date: Sun, 26 May 2024 23:48:15 -0400
Subject: [PATCH] Movable Physics Subsystem (#2880)
Ports some mojave code (originally written on Daedalus by Spyroshark)
hat allows you to fling ingame objects with a simple, bouncing physics
quality.
This is currently implemented on bullet casings.
Technical issues currently:
- [x] Makes the bouncing less dramatic
- [x] Physics must have a "low grav" version for tiles without gravity,
since mojave didn't think gravity was real
- [x] The code handling the angle physics objects travel at is fucky and
needs to be fixed up
the hit feature from 2003, coming to a shiptest near you
:cl: Spyroshark, Sun-Soaked
add: A movable physics subsystem, deployed using a component.
add: Bullet casings now drop using movable physics
code: ports NO_PIXEL_RANDOM_DROP from TG.
/:cl:
---------
Co-authored-by: Sun-Soaked <45698967+MemedHams@users.noreply.github.com>
---
code/__DEFINES/obj_flags.dm | 1 +
code/__DEFINES/subsystems.dm | 1 +
code/_globalvars/bitfields.dm | 1 +
.../subsystem/processing/movable_physics.dm | 24 +++
code/datums/components/movable_physics.dm | 151 ++++++++++++++++++
code/game/objects/items.dm | 2 +-
code/game/objects/items/devices/powersink.dm | 1 +
code/modules/mob/inventory.dm | 2 +-
.../projectiles/ammunition/_ammunition.dm | 10 +-
.../ammunition/caseless/_caseless.dm | 2 +-
code/modules/projectiles/gun.dm | 6 +-
code/modules/projectiles/guns/ballistic.dm | 13 +-
.../projectiles/guns/ballistic/revolver.dm | 11 +-
.../projectiles/guns/ballistic/rifle.dm | 4 +-
.../projectiles/guns/ballistic/shotgun.dm | 4 +-
.../modules/projectiles/guns/ballistic/toy.dm | 4 +-
code/modules/projectiles/guns/energy.dm | 2 +-
.../projectiles/guns/energy/special.dm | 2 +-
code/modules/projectiles/guns/magic.dm | 2 +-
.../modules/projectiles/guns/misc/chem_gun.dm | 2 +-
.../projectiles/guns/misc/syringe_gun.dm | 2 +-
shiptest.dme | 2 +
22 files changed, 222 insertions(+), 27 deletions(-)
create mode 100644 code/controllers/subsystem/processing/movable_physics.dm
create mode 100644 code/datums/components/movable_physics.dm
diff --git a/code/__DEFINES/obj_flags.dm b/code/__DEFINES/obj_flags.dm
index 865470774039..d9ca63008c1d 100644
--- a/code/__DEFINES/obj_flags.dm
+++ b/code/__DEFINES/obj_flags.dm
@@ -33,6 +33,7 @@
#define IN_STORAGE (1<<11) //is this item in the storage item, such as backpack? used for tooltips
#define SURGICAL_TOOL (1<<12) //Tool commonly used for surgery: won't attack targets in an active surgical operation on help intent (in case of mistakes)
#define EYE_STAB (1<<13) /// Item can be used to eyestab
+#define NO_PIXEL_RANDOM_DROP (1<<14) //if dropped, it wont have a randomized pixel_x/pixel_y
// Flags for the clothing_flags var on /obj/item/clothing
diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index 629755487849..36aa57e48625 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -174,6 +174,7 @@
#define FIRE_PRIORITY_PARALLAX 65
#define FIRE_PRIORITY_INSTRUMENTS 80
#define FIRE_PRIORITY_MOBS 100
+#define FIRE_PRIORITY_MOVABLE_PHYSICS 105
#define FIRE_PRIORITY_TGUI 110
#define FIRE_PRIORITY_TICKER 200
#define FIRE_PRIORITY_ATMOS_ADJACENCY 300
diff --git a/code/_globalvars/bitfields.dm b/code/_globalvars/bitfields.dm
index ca2520fc1b23..be5713d4ad0c 100644
--- a/code/_globalvars/bitfields.dm
+++ b/code/_globalvars/bitfields.dm
@@ -152,6 +152,7 @@ DEFINE_BITFIELD(item_flags, list(
"NOBLUDGEON" = NOBLUDGEON,
"NO_MAT_REDEMPTION" = NO_MAT_REDEMPTION,
"SLOWS_WHILE_IN_HAND" = SLOWS_WHILE_IN_HAND,
+ "NO_PIXEL_RANDOM_DROP" = NO_PIXEL_RANDOM_DROP,
))
DEFINE_BITFIELD(machine_stat, list(
diff --git a/code/controllers/subsystem/processing/movable_physics.dm b/code/controllers/subsystem/processing/movable_physics.dm
new file mode 100644
index 000000000000..65015edbd668
--- /dev/null
+++ b/code/controllers/subsystem/processing/movable_physics.dm
@@ -0,0 +1,24 @@
+///Real fast ticking subsystem for moving movables via modifying pixel_x/y/z
+PROCESSING_SUBSYSTEM_DEF(movablephysics)
+ name = "Movable Physics"
+ wait = 0.05 SECONDS
+ stat_tag = "MP"
+ priority = FIRE_PRIORITY_MOVABLE_PHYSICS
+
+/datum/controller/subsystem/processing/movablephysics/fire(resumed = FALSE)
+ if (!resumed)
+ currentrun = processing.Copy()
+ //cache for sanic speed (lists are references anyways)
+ var/list/current_run = currentrun
+
+ while(current_run.len)
+ var/datum/component/thing = current_run[current_run.len]
+ current_run.len--
+ if(QDELETED(thing))
+ processing -= thing
+ else
+ if(thing.process(wait * 0.1) == PROCESS_KILL)
+ // fully stop so that a future START_PROCESSING will work
+ STOP_PROCESSING(src, thing)
+ if (MC_TICK_CHECK)
+ return
diff --git a/code/datums/components/movable_physics.dm b/code/datums/components/movable_physics.dm
new file mode 100644
index 000000000000..55686bb1e259
--- /dev/null
+++ b/code/datums/components/movable_physics.dm
@@ -0,0 +1,151 @@
+#define PHYSICS_GRAV_STANDARD 9.80665
+
+///Remove the component as soon as there's zero velocity, useful for movables that will no longer move after being initially moved (blood splatters)
+#define QDEL_WHEN_NO_MOVEMENT (1<<0)
+
+///Stores information related to the movable's physics and keeping track of relevant signals to trigger movement
+/datum/component/movable_physics
+ ///Modifies the pixel_x/pixel_y of an object every process()
+ var/horizontal_velocity
+ ///Modifies the pixel_z of an object every process(), movables aren't Move()'d into another turf if pixel_z exceeds 16, so try not to supply a super high vertical value if you don't want the movable to clip through multiple turfs
+ var/vertical_velocity
+ ///The horizontal_velocity is reduced by this every process(), this doesn't take into account the object being in the air vs gravity pushing it against the ground
+ var/horizontal_friction
+ ///The vertical_velocity is reduced by this every process()
+ var/z_gravity
+ ///The pixel_z that the object will no longer be influenced by gravity for a 32x32 turf, keep this value between -16 to 0 so it's visuals matches up with it physically being in the turf
+ var/z_floor
+ ///The angle of the path the object takes on the x/y plane
+ var/angle_of_movement
+ ///Flags for turning on certain physic properties, see the top of the file for more information on flags
+ var/physic_flags
+ ///The cached animate_movement of the parent; any kind of gliding when doing Move() makes the physics look derpy, so we'll just make Move() be instant
+ var/cached_animate_movement
+ ///The sound effect to play when bouncing off of something
+ var/bounce_sound
+
+ var/numbounce = 1
+
+/datum/component/movable_physics/Initialize(_horizontal_velocity = 0, _vertical_velocity = 0, _horizontal_friction = 0, _z_gravity = 0, _z_floor = 0, _angle_of_movement = 0, _physic_flags = 0, _bounce_sound)
+ . = ..()
+ if(!ismovable(parent))
+ return COMPONENT_INCOMPATIBLE
+ RegisterSignal(parent, COMSIG_MOVABLE_IMPACT, PROC_REF(throw_impact_ricochet), override = TRUE)
+ horizontal_velocity = _horizontal_velocity
+ vertical_velocity = _vertical_velocity
+ horizontal_friction = _horizontal_friction
+ z_gravity = _z_gravity
+ z_floor = _z_floor
+ angle_of_movement = _angle_of_movement
+ physic_flags = _physic_flags
+ bounce_sound = _bounce_sound
+ if(vertical_velocity || horizontal_velocity)
+ start_movement()
+
+///Let's get moving
+/datum/component/movable_physics/proc/start_movement()
+ var/atom/movable/moving_atom = parent
+ cached_animate_movement = moving_atom.animate_movement
+ moving_atom.animate_movement = NO_STEPS
+ START_PROCESSING(SSmovablephysics, src)
+ moving_atom.SpinAnimation(speed = 1 SECONDS, loops = 1)
+
+///Alright it's time to stop
+/datum/component/movable_physics/proc/stop_movement()
+ var/atom/movable/moving_atom = parent
+ moving_atom.animate_movement = cached_animate_movement
+ STOP_PROCESSING(SSmovablephysics, src)
+ if(physic_flags & QDEL_WHEN_NO_MOVEMENT)
+ qdel(src)
+
+/datum/component/movable_physics/UnregisterFromParent()
+ UnregisterSignal(parent, COMSIG_MOVABLE_IMPACT)
+
+/datum/component/movable_physics/proc/throw_impact_ricochet(datum/source, atom/hit_atom, datum/thrownthing/throwingdatum)
+ SIGNAL_HANDLER
+ var/atom/movable/atom_source = source
+ ricochet(atom_source, Get_Angle(atom_source, throwingdatum.target_turf))
+
+/datum/component/movable_physics/proc/z_floor_bounce(atom/movable/moving_atom)
+ angle_of_movement += rand(-3000, 3000) / 100
+ var/turf/a_turf = get_turf(moving_atom)
+ if(istype(moving_atom, /obj/item/ammo_casing))
+ playsound(moving_atom, a_turf.bullet_bounce_sound, 50, TRUE)
+ else
+ playsound(moving_atom, bounce_sound, 50, TRUE)
+ moving_atom.SpinAnimation(speed = 1 SECONDS / numbounce, loops = 1)
+ moving_atom.pixel_z = z_floor
+ horizontal_velocity = max(0, horizontal_velocity + (vertical_velocity * -0.8))
+ vertical_velocity = max(0, ((vertical_velocity * -0.8) - 0.2))
+ numbounce += 0.5
+
+/datum/component/movable_physics/proc/ricochet(atom/movable/moving_atom, bounce_angle)
+ angle_of_movement = ((180 - bounce_angle) - angle_of_movement)
+ if(angle_of_movement < 0)
+ angle_of_movement += 360
+ //var/turf/a_turf = get_turf(moving_atom)
+ //playsound(src, a_turf.bullet_bounce_sound, 50, TRUE)
+
+/datum/component/movable_physics/proc/fix_angle(angle, atom/moving_atom)//fixes an angle below 0 or above 360
+ if(!(angle_of_movement > 360) && !(angle_of_movement < 0))
+ return angle //early return if it doesn't need to change
+ var/new_angle
+ if(angle_of_movement > 360)
+ new_angle = angle_of_movement - 360
+ if(angle_of_movement < 0)
+ new_angle = angle_of_movement + 360
+ return new_angle
+
+/datum/component/movable_physics/process(delta_time)
+ var/atom/movable/moving_atom = parent
+ var/turf/location = get_turf(moving_atom)
+
+ angle_of_movement = fix_angle(angle_of_movement, moving_atom)
+ if(horizontal_velocity <= 0 && moving_atom.pixel_z == 0)
+ horizontal_velocity = 0
+ stop_movement()
+ return
+
+ moving_atom.pixel_x += (horizontal_velocity * (sin(angle_of_movement)))
+ moving_atom.pixel_y += (horizontal_velocity * (cos(angle_of_movement)))
+
+ horizontal_velocity = max(0, horizontal_velocity - horizontal_friction)
+
+ moving_atom.pixel_z = max(z_floor, moving_atom.pixel_z + vertical_velocity)
+ if(moving_atom.pixel_z > z_floor)
+ vertical_velocity -= (z_gravity * 0.05)
+
+ if(moving_atom.pixel_z <= z_floor && (vertical_velocity != 0) && moving_atom.has_gravity(location)) //z bounce
+ z_floor_bounce(moving_atom)
+
+ if(moving_atom.pixel_x > 16)
+ if(moving_atom.Move(get_step(moving_atom, EAST)))
+ moving_atom.pixel_x = -16
+ else
+ moving_atom.pixel_x = 16
+ ricochet(moving_atom, 0)
+ return
+
+ if(moving_atom.pixel_x < -16)
+ if(moving_atom.Move(get_step(moving_atom, WEST)))
+ moving_atom.pixel_x = 16
+ else
+ moving_atom.pixel_x = -16
+ ricochet(moving_atom, 0)
+ return
+
+ if(moving_atom.pixel_y > 16)
+ if(moving_atom.Move(get_step(moving_atom, NORTH)))
+ moving_atom.pixel_y = -16
+ else
+ moving_atom.pixel_y = 16
+ ricochet(moving_atom, 180)
+ return
+
+ if(moving_atom.pixel_y < -16)
+ if(moving_atom.Move(get_step(moving_atom, SOUTH)))
+ moving_atom.pixel_y = 16
+ else
+ moving_atom.pixel_y = -16
+ ricochet(moving_atom, 180)
+
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index 00943573a12e..75cc7c0556fb 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -694,7 +694,7 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb
if (callback) //call the original callback
. = callback.Invoke()
item_flags &= ~IN_INVENTORY
- if(!pixel_y && !pixel_x)
+ if(!pixel_y && !pixel_x && !(item_flags & NO_PIXEL_RANDOM_DROP))
pixel_x = rand(-8,8)
pixel_y = rand(-8,8)
diff --git a/code/game/objects/items/devices/powersink.dm b/code/game/objects/items/devices/powersink.dm
index 3a6ba2f73950..5b69cab9cc7b 100644
--- a/code/game/objects/items/devices/powersink.dm
+++ b/code/game/objects/items/devices/powersink.dm
@@ -10,6 +10,7 @@
righthand_file = 'icons/mob/inhands/misc/devices_righthand.dmi'
w_class = WEIGHT_CLASS_BULKY
flags_1 = CONDUCT_1
+ item_flags = NO_PIXEL_RANDOM_DROP
throwforce = 5
throw_speed = 1
throw_range = 2
diff --git a/code/modules/mob/inventory.dm b/code/modules/mob/inventory.dm
index de07b3d4f0fd..0d9dab7a035f 100644
--- a/code/modules/mob/inventory.dm
+++ b/code/modules/mob/inventory.dm
@@ -275,7 +275,7 @@
*/
/mob/proc/dropItemToGround(obj/item/I, force = FALSE, silent = FALSE)
. = doUnEquip(I, force, drop_location(), FALSE, silent = silent)
- if(. && I) //ensure the item exists and that it was dropped properly.
+ if(. && I && !(I.item_flags & NO_PIXEL_RANDOM_DROP)) //ensure the item exists and that it was dropped properly.
I.pixel_x = rand(-6,6)
I.pixel_y = rand(-6,6)
diff --git a/code/modules/projectiles/ammunition/_ammunition.dm b/code/modules/projectiles/ammunition/_ammunition.dm
index 5120bf761f9e..c9697aa4e80f 100644
--- a/code/modules/projectiles/ammunition/_ammunition.dm
+++ b/code/modules/projectiles/ammunition/_ammunition.dm
@@ -51,6 +51,7 @@
BB = new projectile_type(src)
pixel_x = base_pixel_x + rand(-10, 10)
pixel_y = base_pixel_y + rand(-10, 10)
+ item_flags |= NO_PIXEL_RANDOM_DROP
if(auto_rotate)
transform = transform.Turn(pick(0, 90, 180, 270))
update_appearance()
@@ -107,9 +108,14 @@
bounce_away(FALSE, NONE)
. = ..()
-/obj/item/ammo_casing/proc/on_eject()
+/obj/item/ammo_casing/proc/on_eject(atom/shooter)
forceMove(drop_location()) //Eject casing onto ground.
- bounce_away(TRUE)
+ pixel_x = rand(-4, 4)
+ pixel_y = rand(-4, 4)
+ pixel_z = 8 //bounce time
+ var/angle_of_movement = !isnull(shooter) ? (rand(-3000, 3000) / 100) + dir2angle(turn(shooter.dir, 180)) : rand(-3000, 3000) / 100
+ AddComponent(/datum/component/movable_physics, _horizontal_velocity = rand(400, 450) / 100, _vertical_velocity = rand(400, 450) / 100, _horizontal_friction = rand(20, 24) / 100, _z_gravity = PHYSICS_GRAV_STANDARD, _z_floor = 0, _angle_of_movement = angle_of_movement)
+
/obj/item/ammo_casing/proc/bounce_away(still_warm = FALSE, bounce_delay = 3)
if(!heavy_metal)
diff --git a/code/modules/projectiles/ammunition/caseless/_caseless.dm b/code/modules/projectiles/ammunition/caseless/_caseless.dm
index 78525277f28c..230f5f9a9969 100644
--- a/code/modules/projectiles/ammunition/caseless/_caseless.dm
+++ b/code/modules/projectiles/ammunition/caseless/_caseless.dm
@@ -3,7 +3,7 @@
firing_effect_type = null
heavy_metal = FALSE
-/obj/item/ammo_casing/caseless/on_eject()
+/obj/item/ammo_casing/caseless/on_eject(atom/shooter)
// [CELADON-EDIT] - CELADON_FIXES
// qdel(src) // CELADON-EDIT - ORIGINAL
if(BB) // Проверяем, что гильза не пустая
diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm
index 5cd55858537a..ca4d0217b380 100644
--- a/code/modules/projectiles/gun.dm
+++ b/code/modules/projectiles/gun.dm
@@ -209,7 +209,7 @@
zoom(user, user.dir, FALSE) //we can only stay zoomed in if it's in our hands //yeah and we only unzoom if we're actually zoomed using the gun!!
//called after the gun has successfully fired its chambered ammo.
-/obj/item/gun/proc/process_chamber()
+/obj/item/gun/proc/process_chamber(atom/shooter)
SEND_SIGNAL(src, COMSIG_GUN_CHAMBER_PROCESSED)
return FALSE
@@ -357,7 +357,7 @@
shoot_with_empty_chamber(user)
firing_burst = FALSE
return FALSE
- process_chamber()
+ process_chamber(shooter = user)
update_appearance()
return TRUE
@@ -408,7 +408,7 @@
else
shoot_with_empty_chamber(user)
return
- process_chamber()
+ process_chamber(shooter = user)
update_appearance()
if(fire_delay)
semicd = TRUE
diff --git a/code/modules/projectiles/guns/ballistic.dm b/code/modules/projectiles/guns/ballistic.dm
index 227abceffa01..4f3b80bf478a 100644
--- a/code/modules/projectiles/guns/ballistic.dm
+++ b/code/modules/projectiles/guns/ballistic.dm
@@ -144,13 +144,13 @@
if(!chambered && empty_indicator)
. += "[icon_state]_empty"
-/obj/item/gun/ballistic/process_chamber(empty_chamber = TRUE, from_firing = TRUE, chamber_next_round = TRUE)
+/obj/item/gun/ballistic/process_chamber(empty_chamber = TRUE, from_firing = TRUE, chamber_next_round = TRUE, atom/shooter)
if(!semi_auto && from_firing)
return
var/obj/item/ammo_casing/casing = chambered //Find chambered round
if(istype(casing)) //there's a chambered round
if(casing_ejector || !from_firing)
- casing.on_eject()
+ casing.on_eject(shooter)
chambered = null
else if(empty_chamber)
chambered = null
@@ -179,7 +179,7 @@
bolt_locked = FALSE
if (user)
to_chat(user, "You rack the [bolt_wording] of \the [src].")
- process_chamber(!chambered, FALSE)
+ process_chamber(!chambered, FALSE, shooter = user)
if (bolt_type == BOLT_TYPE_LOCKING && !chambered)
bolt_locked = TRUE
playsound(src, lock_back_sound, lock_back_sound_volume, lock_back_sound_vary)
@@ -267,7 +267,7 @@
if (istype(A, /obj/item/ammo_casing) || istype(A, /obj/item/ammo_box))
if (bolt_type == BOLT_TYPE_NO_BOLT || internal_magazine)
if (chambered && !chambered.BB)
- chambered.on_eject()
+ chambered.on_eject(shooter = user)
chambered = null
var/num_loaded = magazine.attackby(A, user, params)
if (num_loaded)
@@ -364,7 +364,10 @@
var/num_unloaded = 0
for(var/obj/item/ammo_casing/CB in get_ammo_list(FALSE, TRUE))
CB.forceMove(drop_location())
- CB.bounce_away(FALSE, NONE)
+
+ var/angle_of_movement =(rand(-3000, 3000) / 100) + dir2angle(turn(user.dir, 180))
+ CB.AddComponent(/datum/component/movable_physics, _horizontal_velocity = rand(350, 450) / 100, _vertical_velocity = rand(400, 450) / 100, _horizontal_friction = rand(20, 24) / 100, _z_gravity = PHYSICS_GRAV_STANDARD, _z_floor = 0, _angle_of_movement = angle_of_movement)
+
num_unloaded++
SSblackbox.record_feedback("tally", "station_mess_created", 1, CB.name)
if (num_unloaded)
diff --git a/code/modules/projectiles/guns/ballistic/revolver.dm b/code/modules/projectiles/guns/ballistic/revolver.dm
index b08e156927fa..4f76267752a1 100644
--- a/code/modules/projectiles/guns/ballistic/revolver.dm
+++ b/code/modules/projectiles/guns/ballistic/revolver.dm
@@ -57,7 +57,7 @@
. += "[base_icon_state || initial(icon_state)][safety ? "_hammer_up" : "_hammer_down"]"
-/obj/item/gun/ballistic/revolver/process_chamber(empty_chamber = TRUE, from_firing = TRUE, chamber_next_round = TRUE)
+/obj/item/gun/ballistic/revolver/process_chamber(empty_chamber = TRUE, from_firing = TRUE, chamber_next_round = TRUE, atom/shooter)
SEND_SIGNAL(src, COMSIG_UPDATE_AMMO_HUD)
return ..()
@@ -84,7 +84,9 @@
if(!casing_to_eject)
continue
casing_to_eject.forceMove(drop_location())
- casing_to_eject.bounce_away(FALSE, NONE)
+ var/angle_of_movement =(rand(-3000, 3000) / 100) + dir2angle(turn(user.dir, 180))
+ casing_to_eject.AddComponent(/datum/component/movable_physics, _horizontal_velocity = rand(450, 550) / 100, _vertical_velocity = rand(400, 450) / 100, _horizontal_friction = rand(20, 24) / 100, _z_gravity = PHYSICS_GRAV_STANDARD, _z_floor = 0, _angle_of_movement = angle_of_movement)
+
num_unloaded++
SSblackbox.record_feedback("tally", "station_mess_created", 1, casing_to_eject.name)
chamber_round(FALSE)
@@ -120,8 +122,9 @@
to_chat(user, "There's nothing in the gate to eject from [src]!")
return FALSE
playsound(src, eject_sound, eject_sound_volume, eject_sound_vary)
- casing_to_eject.forceMove(drop_location())
- casing_to_eject.bounce_away(FALSE, NONE)
+ var/angle_of_movement =(rand(-3000, 3000) / 100) + dir2angle(turn(user.dir, 180))
+ casing_to_eject.AddComponent(/datum/component/movable_physics, _horizontal_velocity = rand(350, 450) / 100, _vertical_velocity = rand(400, 450) / 100, _horizontal_friction = rand(20, 24) / 100, _z_gravity = PHYSICS_GRAV_STANDARD, _z_floor = 0, _angle_of_movement = angle_of_movement)
+
SSblackbox.record_feedback("tally", "station_mess_created", 1, casing_to_eject.name)
if(!gate_loaded)
magazine.stored_ammo[casing_index] = null
diff --git a/code/modules/projectiles/guns/ballistic/rifle.dm b/code/modules/projectiles/guns/ballistic/rifle.dm
index 1632e175fd8e..24cf3323048a 100644
--- a/code/modules/projectiles/guns/ballistic/rifle.dm
+++ b/code/modules/projectiles/guns/ballistic/rifle.dm
@@ -33,11 +33,11 @@
. = ..()
. += "[icon_state]_bolt[bolt_locked ? "_locked" : ""]"
-/obj/item/gun/ballistic/rifle/rack(mob/user = null)
+/obj/item/gun/ballistic/rifle/rack(mob/living/user)
if (bolt_locked == FALSE)
to_chat(user, "You open the bolt of \the [src].")
playsound(src, rack_sound, rack_sound_volume, rack_sound_vary)
- process_chamber(FALSE, FALSE, FALSE)
+ process_chamber(FALSE, FALSE, FALSE, shooter = user)
bolt_locked = TRUE
update_appearance()
if (magazine && !magazine?.ammo_count() && empty_autoeject && !internal_magazine)
diff --git a/code/modules/projectiles/guns/ballistic/shotgun.dm b/code/modules/projectiles/guns/ballistic/shotgun.dm
index 0b7e526c8a2b..4502f7fe9b2c 100644
--- a/code/modules/projectiles/guns/ballistic/shotgun.dm
+++ b/code/modules/projectiles/guns/ballistic/shotgun.dm
@@ -303,7 +303,9 @@
var/num_unloaded = 0
for(var/obj/item/ammo_casing/casing_bullet in get_ammo_list(FALSE, TRUE))
casing_bullet.forceMove(drop_location())
- casing_bullet.bounce_away(FALSE, NONE)
+ var/angle_of_movement =(rand(-3000, 3000) / 100) + dir2angle(turn(user.dir, 180))
+ casing_bullet.AddComponent(/datum/component/movable_physics, _horizontal_velocity = rand(450, 550) / 100, _vertical_velocity = rand(400, 450) / 100, _horizontal_friction = rand(20, 24) / 100, _z_gravity = PHYSICS_GRAV_STANDARD, _z_floor = 0, _angle_of_movement = angle_of_movement)
+
num_unloaded++
SSblackbox.record_feedback("tally", "station_mess_created", 1, casing_bullet.name)
if (num_unloaded)
diff --git a/code/modules/projectiles/guns/ballistic/toy.dm b/code/modules/projectiles/guns/ballistic/toy.dm
index 95911c9269bc..5f62b8c7ca78 100644
--- a/code/modules/projectiles/guns/ballistic/toy.dm
+++ b/code/modules/projectiles/guns/ballistic/toy.dm
@@ -59,8 +59,8 @@
. = ..()
. += "[icon_state]_toy"
-/obj/item/gun/ballistic/shotgun/toy/process_chamber(empty_chamber = 0)
- ..()
+/obj/item/gun/ballistic/shotgun/toy/process_chamber(empty_chamber = 0, from_firing = TRUE, chamber_next_round = TRUE, atom/shooter)
+ . = ..()
if(chambered && !chambered.BB)
qdel(chambered)
diff --git a/code/modules/projectiles/guns/energy.dm b/code/modules/projectiles/guns/energy.dm
index a6e424901d5e..705789b3484a 100644
--- a/code/modules/projectiles/guns/energy.dm
+++ b/code/modules/projectiles/guns/energy.dm
@@ -204,7 +204,7 @@
if(!chambered.BB)
chambered.newshot()
-/obj/item/gun/energy/process_chamber()
+/obj/item/gun/energy/process_chamber(atom/shooter)
if(chambered && !chambered.BB) //if BB is null, i.e the shot has been fired...
var/obj/item/ammo_casing/energy/shot = chambered
cell.use(shot.e_cost)//... drain the cell cell
diff --git a/code/modules/projectiles/guns/energy/special.dm b/code/modules/projectiles/guns/energy/special.dm
index 27b7b65b622d..d84655fb5813 100644
--- a/code/modules/projectiles/guns/energy/special.dm
+++ b/code/modules/projectiles/guns/energy/special.dm
@@ -221,7 +221,7 @@
if(istype(WH))
WH.gun = WEAKREF(src)
-/obj/item/gun/energy/wormhole_projector/process_chamber()
+/obj/item/gun/energy/wormhole_projector/process_chamber(atom/shooter)
..()
select_fire()
diff --git a/code/modules/projectiles/guns/magic.dm b/code/modules/projectiles/guns/magic.dm
index 9360c24be499..1f8be937b645 100644
--- a/code/modules/projectiles/guns/magic.dm
+++ b/code/modules/projectiles/guns/magic.dm
@@ -41,7 +41,7 @@
if (charges && chambered && !chambered.BB)
chambered.newshot()
-/obj/item/gun/magic/process_chamber()
+/obj/item/gun/magic/process_chamber(atom/shooter)
if(chambered && !chambered.BB) //if BB is null, i.e the shot has been fired...
charges--//... drain a charge
recharge_newshot()
diff --git a/code/modules/projectiles/guns/misc/chem_gun.dm b/code/modules/projectiles/guns/misc/chem_gun.dm
index fef47121af5f..7c99b7156000 100644
--- a/code/modules/projectiles/guns/misc/chem_gun.dm
+++ b/code/modules/projectiles/guns/misc/chem_gun.dm
@@ -29,7 +29,7 @@
/obj/item/gun/chem/can_shoot()
return syringes_left
-/obj/item/gun/chem/process_chamber()
+/obj/item/gun/chem/process_chamber(atom/shooter)
if(chambered && !chambered.BB && syringes_left)
chambered.newshot()
diff --git a/code/modules/projectiles/guns/misc/syringe_gun.dm b/code/modules/projectiles/guns/misc/syringe_gun.dm
index 96927eb91afc..84d00b226371 100644
--- a/code/modules/projectiles/guns/misc/syringe_gun.dm
+++ b/code/modules/projectiles/guns/misc/syringe_gun.dm
@@ -29,7 +29,7 @@
/obj/item/gun/syringe/can_shoot()
return syringes.len
-/obj/item/gun/syringe/process_chamber()
+/obj/item/gun/syringe/process_chamber(atom/shooter)
if(chambered && !chambered.BB) //we just fired
recharge_newshot()
diff --git a/shiptest.dme b/shiptest.dme
index 80206e3f3c61..9f044e024f08 100644
--- a/shiptest.dme
+++ b/shiptest.dme
@@ -378,6 +378,7 @@
#include "code\controllers\subsystem\processing\fastprocess.dm"
#include "code\controllers\subsystem\processing\fluids.dm"
#include "code\controllers\subsystem\processing\instruments.dm"
+#include "code\controllers\subsystem\processing\movable_physics.dm"
#include "code\controllers\subsystem\processing\nanites.dm"
#include "code\controllers\subsystem\processing\networks.dm"
#include "code\controllers\subsystem\processing\obj.dm"
@@ -502,6 +503,7 @@
#include "code\datums\components\material_container.dm"
#include "code\datums\components\mirv.dm"
#include "code\datums\components\mood.dm"
+#include "code\datums\components\movable_physics.dm"
#include "code\datums\components\nanites.dm"
#include "code\datums\components\ntnet_interface.dm"
#include "code\datums\components\orbiter.dm"