diff --git a/_maps/shuttles/emergency_luxury.dmm b/_maps/shuttles/emergency_luxury.dmm
deleted file mode 100644
index 042202d52b57..000000000000
--- a/_maps/shuttles/emergency_luxury.dmm
+++ /dev/null
@@ -1,1324 +0,0 @@
-//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE
-"aa" = (
-/turf/template_noop,
-/area/template_noop)
-"ab" = (
-/turf/closed/wall/mineral/titanium,
-/area/shuttle/escape/luxury)
-"ac" = (
-/obj/machinery/door/airlock/external/ruin{
- name = "Economy-Class"
- },
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"ad" = (
-/obj/machinery/scanner_gate/luxury_shuttle{
- layer = 2.6
- },
-/obj/machinery/door/airlock/silver{
- name = "First Class"
- },
-/turf/open/floor/carpet/blue,
-/area/shuttle/escape/luxury)
-"ae" = (
-/obj/docking_port/mobile/emergency{
- dir = 2;
- dwidth = 5;
- height = 14;
- name = "Luxurious Emergency Shuttle";
- width = 25
- },
-/obj/machinery/scanner_gate/luxury_shuttle{
- layer = 2.6
- },
-/obj/machinery/door/airlock/silver{
- name = "First Class"
- },
-/turf/open/floor/carpet/blue,
-/area/shuttle/escape/luxury)
-"af" = (
-/obj/machinery/door/airlock/security/glass{
- name = "Escape Shuttle Cell"
- },
-/obj/effect/mapping_helpers/airlock/access/all/security/brig,
-/turf/open/floor/mineral/plastitanium/red/brig,
-/area/shuttle/escape/brig)
-"ag" = (
-/turf/closed/indestructible/riveted/plastinum,
-/area/shuttle/escape/luxury)
-"ah" = (
-/obj/effect/turf_decal/delivery,
-/obj/structure/ore_box,
-/obj/effect/decal/cleanable/cobweb,
-/obj/machinery/light/small/directional/north,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"aj" = (
-/obj/effect/turf_decal/delivery,
-/obj/structure/closet/crate/large,
-/obj/effect/spawner/random/maintenance/eight,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"ak" = (
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"al" = (
-/turf/closed/wall/mineral/plastitanium/nodiagonal,
-/area/shuttle/escape/luxury)
-"ao" = (
-/turf/open/floor/carpet/blue,
-/area/shuttle/escape/luxury)
-"aq" = (
-/obj/effect/turf_decal/delivery,
-/obj/structure/reagent_dispensers/fueltank,
-/obj/structure/window{
- dir = 4
- },
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"ar" = (
-/obj/structure/shuttle/engine/propulsion{
- dir = 8
- },
-/turf/open/floor/plating/airless,
-/area/shuttle/escape/luxury)
-"as" = (
-/obj/structure/shuttle/engine/heater{
- dir = 8
- },
-/obj/structure/window/reinforced{
- dir = 4
- },
-/turf/open/floor/plating/airless,
-/area/shuttle/escape/luxury)
-"at" = (
-/turf/open/floor/mineral/plastitanium/red/brig,
-/area/shuttle/escape/brig)
-"au" = (
-/obj/effect/spawner/structure/window,
-/turf/open/floor/plating,
-/area/shuttle/escape/luxury)
-"av" = (
-/obj/machinery/door/poddoor/shutters/indestructible{
- id = "ohnopoors"
- },
-/turf/closed/indestructible/opsglass{
- desc = "A durable looking window made of an alloy of of plasma and titanium.";
- name = "plastitanium window"
- },
-/area/shuttle/escape/luxury)
-"aw" = (
-/turf/open/floor/mineral/titanium/blue,
-/area/shuttle/escape/luxury)
-"ax" = (
-/obj/effect/decal/cleanable/cobweb/cobweb2,
-/obj/machinery/light/small/directional/east,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"ay" = (
-/turf/closed/indestructible/syndicate,
-/area/shuttle/escape/luxury)
-"az" = (
-/obj/machinery/computer/communications{
- dir = 8
- },
-/turf/open/floor/mineral/diamond,
-/area/shuttle/escape/luxury)
-"aA" = (
-/obj/machinery/button/door{
- id = "ohnopoors";
- name = "window shutters";
- pixel_y = 26
- },
-/turf/open/floor/carpet/blue,
-/area/shuttle/escape/luxury)
-"aB" = (
-/turf/open/floor/carpet/cyan,
-/area/shuttle/escape/luxury)
-"aC" = (
-/obj/item/food/enchiladas{
- pixel_y = 3
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/red,
-/area/shuttle/escape/luxury)
-"aD" = (
-/obj/item/food/candiedapple{
- pixel_y = 6
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/red,
-/area/shuttle/escape/luxury)
-"aE" = (
-/obj/item/food/benedict{
- pixel_y = 1
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/red,
-/area/shuttle/escape/luxury)
-"aF" = (
-/obj/item/food/cakeslice/chocolate{
- pixel_y = 1
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/red,
-/area/shuttle/escape/luxury)
-"aG" = (
-/obj/item/food/eggplantparm{
- pixel_y = 3
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/red,
-/area/shuttle/escape/luxury)
-"aH" = (
-/obj/item/food/spaghetti/copypasta{
- pixel_y = 5
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/red,
-/area/shuttle/escape/luxury)
-"aI" = (
-/obj/item/food/spaghetti/boiledspaghetti{
- pixel_y = 5
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/red,
-/area/shuttle/escape/luxury)
-"aJ" = (
-/obj/item/food/cherrycupcake{
- pixel_y = 2
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/red,
-/area/shuttle/escape/luxury)
-"aK" = (
-/obj/item/food/spaghetti/meatballspaghetti{
- pixel_y = 5
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/red,
-/area/shuttle/escape/luxury)
-"aL" = (
-/obj/item/food/notasandwich{
- pixel_y = 11
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/red,
-/area/shuttle/escape/luxury)
-"aM" = (
-/obj/structure/chair/comfy/teal{
- dir = 4
- },
-/turf/open/floor/mineral/diamond,
-/area/shuttle/escape/luxury)
-"aN" = (
-/obj/item/food/burger/baconburger{
- pixel_y = 2
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/red,
-/area/shuttle/escape/luxury)
-"aO" = (
-/obj/item/food/melonfruitbowl{
- pixel_y = 4
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/red,
-/area/shuttle/escape/luxury)
-"aP" = (
-/obj/item/food/khachapuri{
- pixel_y = 6
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/red,
-/area/shuttle/escape/luxury)
-"aQ" = (
-/obj/item/food/bearsteak{
- pixel_y = 6
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/red,
-/area/shuttle/escape/luxury)
-"aR" = (
-/obj/structure/mirror/directional/west,
-/obj/structure/sink/greyscale{
- dir = 4;
- pixel_x = -12
- },
-/turf/open/floor/mineral/titanium/white,
-/area/shuttle/escape/luxury)
-"aS" = (
-/turf/open/floor/mineral/titanium/white,
-/area/shuttle/escape/luxury)
-"aT" = (
-/obj/item/food/spaghetti/pastatomato{
- pixel_y = 5
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/red,
-/area/shuttle/escape/luxury)
-"aU" = (
-/obj/item/food/kebab/tofu{
- pixel_y = 6
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/red,
-/area/shuttle/escape/luxury)
-"aV" = (
-/obj/item/food/honkdae{
- pixel_y = 8
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/red,
-/area/shuttle/escape/luxury)
-"aW" = (
-/obj/item/food/spaghetti/chowmein{
- pixel_y = 5
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/red,
-/area/shuttle/escape/luxury)
-"aX" = (
-/obj/item/food/grilled_cheese_sandwich{
- pixel_y = 11
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/red,
-/area/shuttle/escape/luxury)
-"aY" = (
-/obj/item/food/jelliedtoast/cherry{
- pixel_y = 6
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/red,
-/area/shuttle/escape/luxury)
-"aZ" = (
-/obj/item/food/honeybun{
- pixel_y = 1
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/red,
-/area/shuttle/escape/luxury)
-"ba" = (
-/obj/item/food/pizza/dank{
- pixel_y = 5
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/red,
-/area/shuttle/escape/luxury)
-"bb" = (
-/obj/machinery/door/airlock/silver{
- name = "Restroom"
- },
-/turf/open/floor/mineral/titanium/white,
-/area/shuttle/escape/luxury)
-"bc" = (
-/turf/closed/indestructible/opsglass{
- desc = "A durable looking window made of an alloy of of plasma and titanium.";
- name = "plastitanium window"
- },
-/area/shuttle/escape/luxury)
-"be" = (
-/obj/structure/girder,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"bf" = (
-/obj/structure/closet/crate/trashcart,
-/obj/effect/turf_decal/loading_area,
-/obj/effect/spawner/random/maintenance/six,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"bg" = (
-/obj/structure/closet/crate/trashcart,
-/obj/effect/turf_decal/loading_area,
-/obj/effect/spawner/random/maintenance/three,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"bi" = (
-/obj/machinery/computer/emergency_shuttle{
- dir = 8
- },
-/turf/open/floor/mineral/diamond,
-/area/shuttle/escape/luxury)
-"bj" = (
-/obj/structure/chair/comfy/shuttle,
-/obj/machinery/light/directional/north,
-/turf/open/floor/mineral/plastitanium/red/brig,
-/area/shuttle/escape/brig)
-"bk" = (
-/obj/machinery/light/directional/north,
-/turf/open/floor/carpet/blue,
-/area/shuttle/escape/luxury)
-"bl" = (
-/obj/machinery/computer/station_alert{
- dir = 8
- },
-/turf/open/floor/mineral/diamond,
-/area/shuttle/escape/luxury)
-"bm" = (
-/obj/structure/table/wood/fancy/black,
-/obj/item/storage/fancy/cigarettes/cigars/havana{
- pixel_y = 7
- },
-/turf/open/floor/mineral/titanium/blue,
-/area/shuttle/escape/luxury)
-"bn" = (
-/obj/effect/turf_decal/delivery,
-/obj/structure/closet/crate/engineering/electrical,
-/obj/effect/decal/cleanable/robot_debris,
-/obj/effect/spawner/random/maintenance/eight,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"bo" = (
-/obj/effect/turf_decal/delivery,
-/obj/structure/closet/crate/engineering,
-/obj/effect/spawner/random/maintenance/eight,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"bp" = (
-/obj/effect/turf_decal/delivery,
-/obj/structure/window{
- dir = 4
- },
-/obj/structure/closet/crate{
- icon_state = "crateopen"
- },
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"bq" = (
-/obj/item/stack/rods/ten,
-/turf/open/floor/plating,
-/area/shuttle/escape/luxury)
-"br" = (
-/obj/effect/decal/cleanable/robot_debris,
-/turf/open/floor/plating,
-/area/shuttle/escape/luxury)
-"bs" = (
-/turf/open/floor/plating,
-/area/shuttle/escape/luxury)
-"bt" = (
-/obj/structure/window/reinforced{
- dir = 1;
- pixel_y = 2
- },
-/obj/item/clothing/head/collectable/paper,
-/turf/open/floor/holofloor/beach/water,
-/area/shuttle/escape/luxury)
-"bu" = (
-/obj/structure/window/reinforced{
- dir = 1;
- pixel_y = 2
- },
-/turf/open/floor/holofloor/beach/water,
-/area/shuttle/escape/luxury)
-"bv" = (
-/obj/machinery/computer/crew{
- dir = 8
- },
-/turf/open/floor/mineral/diamond,
-/area/shuttle/escape/luxury)
-"bw" = (
-/obj/structure/window/reinforced{
- dir = 1;
- pixel_y = 2
- },
-/obj/structure/window/reinforced{
- dir = 4;
- layer = 2.9
- },
-/turf/open/floor/holofloor/beach/coast_t{
- dir = 8
- },
-/area/shuttle/escape/luxury)
-"bx" = (
-/obj/machinery/door/airlock/silver{
- name = "Flight Control"
- },
-/turf/open/floor/mineral/titanium/blue,
-/area/shuttle/escape/luxury)
-"by" = (
-/obj/structure/closet/crate/large,
-/obj/effect/turf_decal/delivery,
-/obj/effect/decal/cleanable/robot_debris,
-/obj/effect/spawner/random/maintenance/eight,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"bz" = (
-/obj/effect/turf_decal/delivery,
-/obj/structure/closet/crate{
- icon_state = "crateopen"
- },
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"bA" = (
-/obj/effect/turf_decal/delivery,
-/obj/structure/window{
- dir = 4
- },
-/obj/structure/closet/crate{
- icon_state = "crateopen"
- },
-/obj/effect/decal/cleanable/oil,
-/obj/effect/spawner/random/maintenance/two,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"bB" = (
-/obj/item/stack/ore/glass,
-/turf/open/floor/plating,
-/area/shuttle/escape/luxury)
-"bC" = (
-/obj/structure/chair/comfy/shuttle{
- dir = 1
- },
-/obj/effect/turf_decal/stripes/line,
-/obj/structure/window,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"bD" = (
-/obj/structure/chair/comfy/shuttle{
- dir = 1
- },
-/turf/open/floor/mineral/plastitanium/red/brig,
-/area/shuttle/escape/brig)
-"bE" = (
-/obj/machinery/light/directional/north,
-/turf/open/floor/mineral/titanium/blue,
-/area/shuttle/escape/luxury)
-"bF" = (
-/obj/item/bikehorn/rubberducky,
-/obj/item/bikehorn/rubberducky,
-/turf/open/floor/holofloor/beach/water,
-/area/shuttle/escape/luxury)
-"bG" = (
-/obj/machinery/door/window/left/directional/east,
-/turf/open/floor/holofloor/beach/coast_t{
- dir = 8
- },
-/area/shuttle/escape/luxury)
-"bH" = (
-/obj/structure/door_assembly,
-/turf/open/floor/plating,
-/area/shuttle/escape/luxury)
-"bI" = (
-/obj/item/stack/tile/iron/base,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"bJ" = (
-/obj/structure/chair/comfy/shuttle,
-/obj/effect/turf_decal/stripes/line{
- dir = 1
- },
-/obj/structure/window{
- dir = 1
- },
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"bK" = (
-/obj/structure/chair/comfy/shuttle,
-/obj/effect/turf_decal/stripes/line{
- dir = 1
- },
-/obj/structure/window{
- dir = 1
- },
-/turf/open/floor/plating,
-/area/shuttle/escape/luxury)
-"bL" = (
-/obj/structure/window/reinforced,
-/turf/open/floor/holofloor/beach/water,
-/area/shuttle/escape/luxury)
-"bM" = (
-/obj/structure/window/reinforced,
-/obj/machinery/door/window/right/directional/east,
-/turf/open/floor/holofloor/beach/coast_t{
- dir = 8
- },
-/area/shuttle/escape/luxury)
-"bN" = (
-/obj/machinery/chem_dispenser/drinks/beer{
- name = "aperitif fountain";
- pixel_y = 8
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/green,
-/area/shuttle/escape/luxury)
-"bO" = (
-/obj/machinery/chem_dispenser/drinks{
- name = "soda fountain";
- pixel_y = 8
- },
-/obj/structure/table/wood/fancy/black,
-/turf/open/floor/carpet/green,
-/area/shuttle/escape/luxury)
-"bP" = (
-/obj/structure/table/wood/fancy/black,
-/obj/machinery/recharger,
-/turf/open/floor/mineral/titanium/blue,
-/area/shuttle/escape/luxury)
-"bQ" = (
-/obj/machinery/light/directional/west,
-/turf/open/floor/holofloor/beach/water,
-/area/shuttle/escape/luxury)
-"bR" = (
-/obj/effect/decal/cleanable/glass,
-/turf/open/floor/plating,
-/area/shuttle/escape/luxury)
-"bS" = (
-/obj/item/stack/tile/iron/base,
-/turf/open/floor/plating,
-/area/shuttle/escape/luxury)
-"bT" = (
-/obj/effect/decal/cleanable/oil,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"bU" = (
-/obj/effect/decal/cleanable/generic,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"bV" = (
-/obj/structure/chair/comfy/brown,
-/turf/open/floor/carpet/orange,
-/area/shuttle/escape/luxury)
-"bW" = (
-/turf/open/floor/carpet/green,
-/area/shuttle/escape/luxury)
-"bX" = (
-/obj/structure/chair/comfy/shuttle{
- dir = 1
- },
-/obj/effect/turf_decal/stripes/line,
-/obj/structure/window,
-/turf/open/floor/plating,
-/area/shuttle/escape/luxury)
-"bY" = (
-/obj/structure/table/wood/fancy/black,
-/obj/item/reagent_containers/food/drinks/bottle/champagne{
- pixel_x = 7;
- pixel_y = 11
- },
-/obj/item/reagent_containers/food/drinks/shaker{
- pixel_x = -7;
- pixel_y = 11
- },
-/obj/item/reagent_containers/glass/rag{
- pixel_x = -6;
- pixel_y = 4
- },
-/turf/open/floor/carpet/green,
-/area/shuttle/escape/luxury)
-"bZ" = (
-/obj/machinery/vending/boozeomat,
-/turf/open/floor/carpet/green,
-/area/shuttle/escape/luxury)
-"cb" = (
-/obj/machinery/stasis,
-/turf/open/floor/carpet/cyan,
-/area/shuttle/escape/luxury)
-"cc" = (
-/obj/effect/turf_decal/delivery,
-/obj/structure/closet/crate/large,
-/obj/effect/decal/cleanable/robot_debris,
-/obj/effect/spawner/random/maintenance/eight,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"cd" = (
-/obj/machinery/door/airlock/public/glass{
- name = "Emergency Shuttle Infirmary"
- },
-/turf/open/floor/carpet/cyan,
-/area/shuttle/escape/luxury)
-"cf" = (
-/obj/effect/decal/cleanable/vomit/old,
-/obj/item/stack/ore/glass,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"cg" = (
-/obj/effect/decal/cleanable/glass,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"ch" = (
-/obj/structure/chair/comfy/brown{
- dir = 1
- },
-/turf/open/floor/carpet/orange,
-/area/shuttle/escape/luxury)
-"ci" = (
-/obj/structure/musician/piano{
- icon_state = "piano"
- },
-/turf/open/floor/carpet/green,
-/area/shuttle/escape/luxury)
-"cj" = (
-/obj/structure/closet/crate/freezer,
-/obj/item/reagent_containers/blood,
-/obj/item/reagent_containers/blood,
-/obj/item/reagent_containers/blood/a_minus,
-/obj/item/reagent_containers/blood/b_minus{
- pixel_x = -4;
- pixel_y = 4
- },
-/obj/item/reagent_containers/blood/b_plus{
- pixel_x = 1;
- pixel_y = 2
- },
-/obj/item/reagent_containers/blood/o_minus,
-/obj/item/reagent_containers/blood/o_plus{
- pixel_x = -2;
- pixel_y = -1
- },
-/obj/item/reagent_containers/blood/random,
-/obj/item/reagent_containers/blood/random,
-/obj/item/reagent_containers/blood/a_plus,
-/obj/item/reagent_containers/blood/random,
-/turf/open/floor/carpet/cyan,
-/area/shuttle/escape/luxury)
-"ck" = (
-/obj/item/stack/tile/iron/base,
-/obj/machinery/light/small/directional/west,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"cl" = (
-/mob/living/simple_animal/bot/medbot{
- name = "\improper emergency medibot";
- pixel_x = -3;
- pixel_y = 2
- },
-/turf/open/floor/carpet/cyan,
-/area/shuttle/escape/luxury)
-"cm" = (
-/obj/structure/table,
-/obj/item/scalpel{
- pixel_y = 12
- },
-/obj/item/circular_saw,
-/obj/item/retractor{
- pixel_x = 4
- },
-/obj/item/hemostat{
- pixel_x = -4
- },
-/obj/item/clothing/gloves/color/latex,
-/obj/item/clothing/mask/surgical,
-/obj/item/surgicaldrill,
-/obj/item/cautery,
-/turf/open/floor/carpet/cyan,
-/area/shuttle/escape/luxury)
-"cn" = (
-/obj/machinery/door/airlock{
- name = "Economy Medical"
- },
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"co" = (
-/obj/item/shard,
-/turf/open/floor/plating,
-/area/shuttle/escape/luxury)
-"cp" = (
-/obj/structure/closet/crate/freezer,
-/obj/effect/decal/cleanable/blood/old,
-/obj/item/bot_assembly/medbot,
-/obj/item/stack/gauze,
-/obj/item/reagent_containers/food/drinks/bottle/whiskey,
-/obj/item/reagent_containers/glass/bottle/ethanol,
-/obj/item/organ/stomach,
-/obj/item/clothing/mask/surgical,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"cq" = (
-/obj/effect/decal/cleanable/chem_pile,
-/turf/open/floor/plating,
-/area/shuttle/escape/luxury)
-"cr" = (
-/obj/structure/frame/machine,
-/obj/effect/decal/cleanable/cobweb/cobweb2,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"cs" = (
-/obj/effect/decal/cleanable/robot_debris,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"ct" = (
-/obj/machinery/shower{
- dir = 8
- },
-/turf/open/floor/mineral/titanium/white,
-/area/shuttle/escape/luxury)
-"cu" = (
-/obj/structure/toilet{
- pixel_y = 8
- },
-/obj/machinery/light/directional/east,
-/turf/open/floor/mineral/titanium/white,
-/area/shuttle/escape/luxury)
-"cv" = (
-/obj/structure/table,
-/obj/item/reagent_containers/pill/maintenance{
- pixel_x = -6;
- pixel_y = -3
- },
-/obj/item/reagent_containers/pill/maintenance{
- pixel_x = 8;
- pixel_y = -3
- },
-/obj/item/reagent_containers/pill/maintenance,
-/obj/item/healthanalyzer{
- pixel_y = 9
- },
-/turf/open/floor/plating,
-/area/shuttle/escape/luxury)
-"cw" = (
-/obj/structure/bed,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"cx" = (
-/obj/effect/decal/cleanable/vomit/old,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"cy" = (
-/obj/item/shard,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"cz" = (
-/obj/machinery/light/directional/south,
-/turf/open/floor/mineral/titanium/blue,
-/area/shuttle/escape/luxury)
-"cA" = (
-/obj/effect/decal/cleanable/oil,
-/turf/open/floor/plating,
-/area/shuttle/escape/luxury)
-"cB" = (
-/obj/structure/bed,
-/obj/effect/decal/cleanable/blood/old,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"cC" = (
-/obj/structure/grille/broken,
-/obj/effect/spawner/random/maintenance/two,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"cD" = (
-/obj/structure/frame/machine,
-/obj/effect/turf_decal/delivery,
-/obj/effect/decal/cleanable/cobweb,
-/obj/machinery/light/small/directional/west,
-/turf/open/floor/iron,
-/area/shuttle/escape/luxury)
-"cE" = (
-/obj/structure/chair/comfy/brown,
-/obj/machinery/light/directional/north,
-/turf/open/floor/carpet/orange,
-/area/shuttle/escape/luxury)
-"cF" = (
-/obj/machinery/light/directional/east,
-/turf/open/floor/carpet/green,
-/area/shuttle/escape/luxury)
-"cG" = (
-/obj/structure/table,
-/obj/item/storage/medkit/advanced,
-/obj/item/storage/medkit/regular,
-/obj/item/storage/medkit/fire,
-/obj/item/reagent_containers/glass/bottle/epinephrine{
- pixel_x = 6
- },
-/obj/item/reagent_containers/glass/bottle/dylovene,
-/obj/item/reagent_containers/glass/bottle/epinephrine{
- pixel_x = -3;
- pixel_y = 8
- },
-/obj/item/reagent_containers/glass/bottle/dylovene,
-/obj/item/reagent_containers/syringe/epinephrine{
- pixel_x = 3;
- pixel_y = -2
- },
-/obj/item/reagent_containers/syringe/epinephrine{
- pixel_x = 3;
- pixel_y = -2
- },
-/obj/item/reagent_containers/syringe/epinephrine{
- pixel_x = 3;
- pixel_y = -2
- },
-/obj/machinery/light/directional/north,
-/turf/open/floor/carpet/cyan,
-/area/shuttle/escape/luxury)
-"cH" = (
-/obj/machinery/light/directional/south,
-/obj/structure/table/optable,
-/turf/open/floor/carpet/cyan,
-/area/shuttle/escape/luxury)
-"cI" = (
-/obj/machinery/light/directional/east,
-/turf/open/floor/carpet/blue,
-/area/shuttle/escape/luxury)
-"cJ" = (
-/obj/item/stack/tile/iron/base,
-/obj/machinery/light/small/directional/west,
-/turf/open/floor/plating,
-/area/shuttle/escape/luxury)
-"cK" = (
-/obj/structure/table,
-/obj/item/wirecutters{
- pixel_y = 6
- },
-/obj/item/hatchet,
-/obj/item/knife/kitchen,
-/obj/machinery/light/small/directional/west,
-/turf/open/floor/plating,
-/area/shuttle/escape/luxury)
-"cL" = (
-/obj/structure/chair/comfy/brown{
- dir = 1
- },
-/obj/machinery/light/directional/south,
-/turf/open/floor/carpet/orange,
-/area/shuttle/escape/luxury)
-"cM" = (
-/obj/machinery/light/directional/south,
-/turf/open/floor/mineral/titanium/white,
-/area/shuttle/escape/luxury)
-"cN" = (
-/obj/structure/chair/wood{
- dir = 8
- },
-/turf/open/floor/carpet/green,
-/area/shuttle/escape/luxury)
-"qa" = (
-/obj/structure/toilet{
- pixel_y = 8
- },
-/obj/machinery/light/directional/west,
-/turf/open/floor/mineral/titanium/white,
-/area/shuttle/escape/luxury)
-"zh" = (
-/turf/open/floor/mineral/diamond,
-/area/shuttle/escape/luxury)
-"Eg" = (
-/obj/machinery/vending/wallmed/directional/north,
-/obj/machinery/iv_drip,
-/turf/open/floor/carpet/cyan,
-/area/shuttle/escape/luxury)
-"Lt" = (
-/obj/machinery/door/airlock/security/glass{
- name = "Escape Shuttle Cell"
- },
-/obj/machinery/scanner_gate/luxury_shuttle,
-/obj/effect/mapping_helpers/airlock/access/all/security/brig,
-/turf/open/floor/mineral/plastitanium/red/brig,
-/area/shuttle/escape/brig)
-
-(1,1,1) = {"
-aa
-al
-ar
-ar
-al
-ar
-ar
-ar
-ar
-al
-ar
-ar
-al
-aa
-"}
-(2,1,1) = {"
-ab
-al
-as
-as
-al
-as
-as
-as
-as
-al
-as
-as
-al
-ab
-"}
-(3,1,1) = {"
-ab
-ah
-bn
-by
-ab
-cD
-aj
-cc
-aj
-ab
-cp
-cv
-cK
-ab
-"}
-(4,1,1) = {"
-ab
-aj
-bo
-bz
-bH
-bR
-bC
-bJ
-ak
-cn
-cq
-bs
-cA
-ab
-"}
-(5,1,1) = {"
-ab
-aq
-bp
-bA
-ab
-bS
-bC
-bJ
-cf
-ab
-cr
-cw
-cB
-ab
-"}
-(6,1,1) = {"
-ab
-be
-bq
-bB
-ck
-bT
-ak
-bU
-cg
-ab
-ab
-ab
-ab
-ab
-"}
-(7,1,1) = {"
-ab
-bf
-br
-bC
-bJ
-bI
-bC
-bK
-bs
-co
-cs
-cJ
-cC
-ab
-"}
-(8,1,1) = {"
-ab
-bg
-ak
-bC
-bJ
-bU
-bC
-bJ
-bS
-bC
-bJ
-cx
-bC
-ab
-"}
-(9,1,1) = {"
-ac
-ax
-bs
-bC
-bK
-bS
-bX
-bJ
-ak
-bC
-bJ
-cy
-bC
-ab
-"}
-(10,1,1) = {"
-ag
-ag
-af
-ag
-ag
-ay
-av
-av
-av
-ay
-av
-av
-av
-ay
-"}
-(11,1,1) = {"
-af
-at
-at
-bD
-ag
-aA
-ao
-ao
-ao
-ao
-ao
-ao
-ao
-ag
-"}
-(12,1,1) = {"
-ag
-bj
-at
-bD
-ag
-cE
-aG
-aK
-ch
-bV
-aO
-aT
-cL
-ag
-"}
-(13,1,1) = {"
-ag
-at
-at
-bD
-ag
-bV
-aH
-aL
-ch
-bV
-aP
-aU
-ch
-ag
-"}
-(14,1,1) = {"
-ag
-Lt
-ag
-ag
-ag
-bV
-aC
-aE
-ch
-bV
-aX
-aZ
-ch
-ag
-"}
-(15,1,1) = {"
-ag
-ao
-bt
-bQ
-bL
-bV
-aD
-aF
-ch
-bV
-aY
-ba
-ch
-ag
-"}
-(16,1,1) = {"
-ag
-ao
-bu
-bF
-bL
-bV
-aI
-aD
-ch
-bV
-aH
-aV
-ch
-ag
-"}
-(17,1,1) = {"
-ae
-ao
-bw
-bG
-bM
-bV
-aJ
-aN
-ch
-bV
-aQ
-aW
-cL
-ag
-"}
-(18,1,1) = {"
-ag
-bk
-ao
-ao
-ao
-ao
-ao
-ao
-ao
-ao
-cI
-ao
-ao
-ag
-"}
-(19,1,1) = {"
-ad
-ao
-ao
-ao
-bN
-bW
-bY
-ao
-ci
-ag
-ag
-ag
-bb
-ag
-"}
-(20,1,1) = {"
-ag
-ao
-ao
-ao
-bO
-cF
-bZ
-ao
-cN
-ag
-aR
-aR
-aS
-ag
-"}
-(21,1,1) = {"
-ag
-au
-bx
-bx
-au
-ag
-au
-cd
-au
-ag
-ct
-ct
-cM
-ag
-"}
-(22,1,1) = {"
-ag
-bm
-aw
-aw
-bP
-ag
-cj
-aB
-cm
-ag
-ag
-ag
-aS
-ag
-"}
-(23,1,1) = {"
-ag
-bE
-aw
-aw
-cz
-ag
-cG
-aB
-cH
-ag
-cu
-bb
-aS
-ag
-"}
-(24,1,1) = {"
-ag
-aM
-aM
-aM
-zh
-ag
-Eg
-aB
-cl
-ag
-ag
-ag
-cM
-ag
-"}
-(25,1,1) = {"
-ag
-az
-bi
-bl
-bv
-ag
-cb
-cb
-cb
-ag
-qa
-bb
-aS
-ag
-"}
-(26,1,1) = {"
-ag
-bc
-bc
-bc
-bc
-ag
-ag
-ag
-ag
-ag
-ag
-ag
-ag
-ag
-"}
diff --git a/code/__DEFINES/_multiz.dm b/code/__DEFINES/_multiz.dm
index b4c9d0de5774..02dd04549d43 100644
--- a/code/__DEFINES/_multiz.dm
+++ b/code/__DEFINES/_multiz.dm
@@ -5,3 +5,9 @@
#define GetAbove(A) (HasAbove(A:z) ? get_step(A, UP) : null)
#define GetBelow(A) (HasBelow(A:z) ? get_step(A, DOWN) : null)
+
+
+/// Vertical Z movement
+#define ZMOVING_VERTICAL 1
+/// Laterial Z movement
+#define ZMOVING_LATERAL 2
diff --git a/code/__DEFINES/ai.dm b/code/__DEFINES/ai.dm
index 076efefc7d5f..ed88474afdd1 100644
--- a/code/__DEFINES/ai.dm
+++ b/code/__DEFINES/ai.dm
@@ -36,7 +36,7 @@
/// probability that the pawn should try resisting out of restraints
#define RESIST_SUBTREE_PROB 50
///macro for whether it's appropriate to resist right now, used by resist subtree
-#define SHOULD_RESIST(source) (source.on_fire || source.buckled || HAS_TRAIT(source, TRAIT_RESTRAINED) || (source.pulledby && source.pulledby.grab_state > GRAB_PASSIVE))
+#define SHOULD_RESIST(source) (source.on_fire || source.buckled || HAS_TRAIT(source, TRAIT_RESTRAINED) || HAS_TRAIT(source, TRAIT_AGGRESSIVE_GRAB))
///macro for whether the pawn can act, used generally to prevent some horrifying ai disasters
#define IS_DEAD_OR_INCAP(source) (source.incapacitated() || source.stat)
diff --git a/code/__DEFINES/bodyparts.dm b/code/__DEFINES/bodyparts.dm
index 4618a751494a..0a6f0128f943 100644
--- a/code/__DEFINES/bodyparts.dm
+++ b/code/__DEFINES/bodyparts.dm
@@ -39,15 +39,19 @@
#define BP_CUT_AWAY (1<<10)
/// Limb cannot feel pain
#define BP_NO_PAIN (1<<11)
+/// Limb can be dislocated
+#define BP_CAN_BE_DISLOCATED (1<<12)
+/// Limb is dislocated
+#define BP_DISLOCATED (1<<13)
#define HATCH_CLOSED 1
#define HATCH_UNSCREWED 2
#define HATCH_OPENED 3
-#define STOCK_BP_FLAGS_CHEST (BP_HAS_BLOOD | BP_HAS_BONES | BP_HAS_ARTERY)
-#define STOCK_BP_FLAGS_HEAD (BP_HAS_BLOOD | BP_HAS_BONES | BP_HAS_ARTERY)
-#define STOCK_BP_FLAGS_ARMS (BP_IS_GRABBY_LIMB | BP_HAS_BLOOD | BP_HAS_BONES | BP_HAS_TENDON | BP_HAS_ARTERY)
-#define STOCK_BP_FLAGS_LEGS (BP_IS_MOVEMENT_LIMB | BP_HAS_BLOOD | BP_HAS_BONES | BP_HAS_TENDON | BP_HAS_ARTERY)
+#define STOCK_BP_FLAGS_CHEST (BP_HAS_BLOOD | BP_HAS_BONES | BP_HAS_ARTERY | BP_CAN_BE_DISLOCATED)
+#define STOCK_BP_FLAGS_HEAD (BP_HAS_BLOOD | BP_HAS_BONES | BP_HAS_ARTERY | BP_CAN_BE_DISLOCATED)
+#define STOCK_BP_FLAGS_ARMS (BP_IS_GRABBY_LIMB | BP_HAS_BLOOD | BP_HAS_BONES | BP_HAS_TENDON | BP_HAS_ARTERY | BP_CAN_BE_DISLOCATED)
+#define STOCK_BP_FLAGS_LEGS (BP_IS_MOVEMENT_LIMB | BP_HAS_BLOOD | BP_HAS_BONES | BP_HAS_TENDON | BP_HAS_ARTERY| BP_CAN_BE_DISLOCATED)
//check_bones() return values
#define CHECKBONES_NONE (1<<0)
diff --git a/code/__DEFINES/combat.dm b/code/__DEFINES/combat.dm
index 9d3e5d3e8edf..7ea730b5e8ac 100644
--- a/code/__DEFINES/combat.dm
+++ b/code/__DEFINES/combat.dm
@@ -113,7 +113,7 @@
//the define for visible message range in combat
#define SAMETILE_MESSAGE_RANGE 1
-#define COMBAT_MESSAGE_RANGE 3
+#define COMBAT_MESSAGE_RANGE 4
#define DEFAULT_MESSAGE_RANGE 7
//Shove knockdown lengths (deciseconds)
diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_mouse.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_mouse.dm
index 2f8ebc836650..91ebec57f7f9 100644
--- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_mouse.dm
+++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_mouse.dm
@@ -9,7 +9,7 @@
///from base of atom/ShiftClick(): (/mob)
#define COMSIG_CLICK_SHIFT "shift_click"
#define COMPONENT_ALLOW_EXAMINATE (1<<0) //Allows the user to examinate regardless of client.eye.
-///from base of atom/CtrlClickOn(): (/mob)
+///from base of atom/CtrlClickOn(): (/mob, list/params)
#define COMSIG_CLICK_CTRL "ctrl_click"
///from base of atom/AltClick(): (/mob)
#define COMSIG_CLICK_ALT "alt_click"
diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movement.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movement.dm
index bdc082f9c759..eb3fccdbf931 100644
--- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movement.dm
+++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movement.dm
@@ -2,26 +2,23 @@
// When the signal is called: (signal arguments)
// All signals send the source datum of the signal as the first argument
-///signal sent out by an atom when it checks if it can be pulled, for additional checks
-#define COMSIG_ATOM_CAN_BE_PULLED "movable_can_be_pulled"
- #define COMSIG_ATOM_CANT_PULL (1 << 0)
+///signal sent out by an atom when it checks if it can be grabbed, for additional checks
+#define COMSIG_ATOM_CAN_BE_GRABBED "movable_can_be_grabbed"
+ #define COMSIG_ATOM_NO_GRAB (1 << 0)
///signal sent out by an atom when it is no longer being pulled by something else : (atom/puller)
-#define COMSIG_ATOM_NO_LONGER_PULLED "movable_no_longer_pulled"
-///signal sent out by an atom when it is no longer pulling something : (atom/pulling)
-#define COMSIG_ATOM_NO_LONGER_PULLING "movable_no_longer_pulling"
+#define COMSIG_ATOM_NO_LONGER_GRABBED "movable_no_longer_grabbed"
+///signal sent out by a living mob when it is no longer pulling something : (atom/pulling)
+#define COMSIG_LIVING_NO_LONGER_GRABBING "living_no_longer_grabbing"
///called for each movable in a turf contents on /turf/zImpact(): (atom/movable/A, levels)
#define COMSIG_ATOM_INTERCEPT_Z_FALL "movable_intercept_z_impact"
-///called on a movable (NOT living) when it starts pulling (atom/movable/pulled, state, force)
-#define COMSIG_ATOM_START_PULL "movable_start_pull"
-///called on /living when someone starts pulling (atom/movable/pulled, state, force)
-#define COMSIG_LIVING_START_PULL "living_start_pull"
-///called on /living when someone is pulled (mob/living/puller)
-#define COMSIG_LIVING_GET_PULLED "living_start_pulled"
-///called on /living, when pull is attempted, but before it completes, from base of [/mob/living/start_pulling]: (atom/movable/thing, force)
-#define COMSIG_LIVING_TRY_PULL "living_try_pull"
- #define COMSIG_LIVING_CANCEL_PULL (1 << 0)
-/// Called from /mob/living/update_pull_movespeed
-#define COMSIG_LIVING_UPDATING_PULL_MOVESPEED "living_updating_pull_movespeed"
+///called on a movable when it starts pulling (atom/movable/pulled, state, force)
+#define COMSIG_LIVING_START_GRAB "movable_start_grab"
+///called on a movable when it has been grabbed
+#define COMSIG_ATOM_GET_GRABBED "movable_start_grabbed"
+///called on /living, when a grab is attempted, but before it completes, from base of [/mob/living/make_grab]: (atom/movable/thing, grab_type)
+#define COMSIG_LIVING_TRY_GRAB "living_try_pull"
+ #define COMSIG_LIVING_CANCEL_GRAB (1 << 0)
+
/// Called from /mob/living/PushAM -- Called when this mob is about to push a movable, but before it moves
/// (aotm/movable/being_pushed)
#define COMSIG_LIVING_PUSHING_MOVABLE "living_pushing_movable"
diff --git a/code/__DEFINES/grab_defines.dm b/code/__DEFINES/grab_defines.dm
new file mode 100644
index 000000000000..1b3437a83073
--- /dev/null
+++ b/code/__DEFINES/grab_defines.dm
@@ -0,0 +1,9 @@
+// Trait sources for grabs
+#define AGGRESSIVE_GRAB "source_aggressive_grab"
+#define NECK_GRAB "source_neck_grab"
+#define KILL_GRAB "source_kill_grab"
+
+/// Applied to movables that are aggressively grabbed OR HIGHER
+#define TRAIT_AGGRESSIVE_GRAB "trait_aggressive_grab"
+/// Applied to movables that are being strangled
+#define TRAIT_KILL_GRAB "trait_strangle_grab"
diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm
index e4c1945cf50a..053da27efe1a 100644
--- a/code/__DEFINES/is_helpers.dm
+++ b/code/__DEFINES/is_helpers.dm
@@ -224,6 +224,8 @@ GLOBAL_LIST_INIT(turfs_without_ground, typecacheof(list(
#define isfalsewall(A) (istype(A, /obj/structure/falsewall))
+#define isgrab(A) (istype(A, /obj/item/hand_item/grab))
+
//Assemblies
#define isassembly(O) (istype(O, /obj/item/assembly))
@@ -271,3 +273,4 @@ GLOBAL_LIST_INIT(book_types, typecacheof(list(
#define is_security_officer_job(job_type) (istype(job_type, /datum/job/security_officer))
#define is_research_director_job(job_type) (istype(job_type, /datum/job/research_director))
#define is_unassigned_job(job_type) (istype(job_type, /datum/job/unassigned))
+
diff --git a/code/__DEFINES/movement.dm b/code/__DEFINES/movement.dm
index 8dd637fb8784..c0e4adc594b1 100644
--- a/code/__DEFINES/movement.dm
+++ b/code/__DEFINES/movement.dm
@@ -61,7 +61,7 @@ GLOBAL_VAR_INIT(glide_size_multiplier, 1.0)
/// Used when the grip on a pulled object shouldn't be broken.
#define FALL_RETAIN_PULL (1<<3)
-/// Runs check_pulling() by the end of [/atom/movable/proc/zMove] for every movable that's pulling something. Should be kept enabled unless you know what you are doing.
+/// Runs recheck_grabs() by the end of [/atom/movable/proc/zMove] for every movable that's pulling something. Should be kept enabled unless you know what you are doing.
#define ZMOVE_CHECK_PULLING (1<<0)
/// Checks if pulledby is nearby. if not, stop being pulled.
#define ZMOVE_CHECK_PULLEDBY (1<<1)
@@ -79,6 +79,8 @@ GLOBAL_VAR_INIT(glide_size_multiplier, 1.0)
#define ZMOVE_INCLUDE_PULLED (1<<8)
/// Skips check for whether the moving atom is anchored or not.
#define ZMOVE_ALLOW_ANCHORED (1<<9)
+/// Skip CanMoveOnto() checks
+#define ZMOVE_SKIP_CANMOVEONTO (1<<10)
#define ZMOVE_CHECK_PULLS (ZMOVE_CHECK_PULLING|ZMOVE_CHECK_PULLEDBY)
diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm
index cd8e84dc99e2..4fe9b8993eba 100644
--- a/code/__DEFINES/traits.dm
+++ b/code/__DEFINES/traits.dm
@@ -408,8 +408,10 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_SPRAY_PAINTABLE "spray_paintable"
/// This person is blushing
#define TRAIT_BLUSHING "blushing"
-
-#define TRAIT_NOBLEED "nobleed" //This carbon doesn't bleed
+/// This bodypart is being held in a grab, and reduces bleeding
+#define TRAIT_BODYPART_GRABBED "bodypart_grabbed"
+/// This carbon doesn't bleed
+#define TRAIT_NOBLEED "nobleed"
/// This atom can ignore the "is on a turf" check for simple AI datum attacks, allowing them to attack from bags or lockers as long as any other conditions are met
#define TRAIT_AI_BAGATTACK "bagattack"
/// This mobs bodyparts are invisible but still clickable.
@@ -887,7 +889,6 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define STATION_TRAIT_UNIQUE_AI "station_trait_unique_ai"
#define STATION_TRAIT_CARP_INFESTATION "station_trait_carp_infestation"
#define STATION_TRAIT_PREMIUM_INTERNALS "station_trait_premium_internals"
-#define STATION_TRAIT_RANDOM_ARRIVALS "station_trait_random_arrivals"
#define STATION_TRAIT_HANGOVER "station_trait_hangover"
#define STATION_TRAIT_FILLED_MAINT "station_trait_filled_maint"
#define STATION_TRAIT_EMPTY_MAINT "station_trait_empty_maint"
diff --git a/code/__HELPERS/global_lists.dm b/code/__HELPERS/global_lists.dm
index cfba25e00440..f81755d0a9f3 100644
--- a/code/__HELPERS/global_lists.dm
+++ b/code/__HELPERS/global_lists.dm
@@ -87,6 +87,15 @@
GLOB.emote_list = init_emote_list()
+ for(var/datum/grab/G as anything in subtypesof(/datum/grab))
+ if(isabstract(G))
+ continue
+ GLOB.all_grabstates[G] = new G
+
+ for(var/path in GLOB.all_grabstates)
+ var/datum/grab/G = GLOB.all_grabstates[path]
+ G.refresh_updown()
+
init_crafting_recipes(GLOB.crafting_recipes)
init_loadout_references()
init_augment_references()
diff --git a/code/_onclick/adjacent.dm b/code/_onclick/adjacent.dm
index 0329134a9f2d..39c5ee4ff32e 100644
--- a/code/_onclick/adjacent.dm
+++ b/code/_onclick/adjacent.dm
@@ -145,3 +145,41 @@
else if( !border_only ) // dense, not on border, cannot pass over
return FALSE
return TRUE
+
+/atom/proc/MultiZAdjacent(atom/neighbor)
+ var/turf/T = get_turf(src)
+ var/turf/N = get_turf(neighbor)
+
+ // Not on valid turfs.
+ if(QDELETED(src) || QDELETED(neighbor) || !istype(T) || !istype(N))
+ return FALSE
+
+ // On the same z-level, we don't need to care about multiz.
+ if(N.z == T.z)
+ return Adjacent(neighbor)
+
+ // More than one z-level away from each other.
+ if(abs(N.x - T.x) > 1 || abs(N.y - T.y) > 1 || abs(N.z - T.z) > 1)
+ return FALSE
+
+
+ // Are they below us?
+ if(N.z < T.z && HasBelow(T.z))
+ var/turf/B = GetBelow(T)
+ . = isopenspaceturf(T) && neighbor.Adjacent(B)
+ if(!.)
+ B = GetAbove(N)
+ . = isopenspaceturf(B) && src.Adjacent(B)
+ return
+
+
+ // Are they above us?
+ if(HasAbove(T.z))
+ var/turf/A = GetAbove(T)
+ . = isopenspaceturf(A) && neighbor.Adjacent(A)
+ if(!.)
+ A = GetBelow(N)
+ . = isopenspaceturf(N) && src.Adjacent(A)
+ return
+
+ return FALSE
diff --git a/code/_onclick/ai.dm b/code/_onclick/ai.dm
index 94399b1b8c62..e587a08ac1ce 100644
--- a/code/_onclick/ai.dm
+++ b/code/_onclick/ai.dm
@@ -123,7 +123,7 @@
A.AICtrlShiftClick(src)
/mob/living/silicon/ai/ShiftClickOn(atom/A)
A.AIShiftClick(src)
-/mob/living/silicon/ai/CtrlClickOn(atom/A)
+/mob/living/silicon/ai/CtrlClickOn(atom/A, list/params)
A.AICtrlClick(src)
/mob/living/silicon/ai/AltClickOn(atom/A)
A.AIAltClick(src)
diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm
index f80f121fbab6..58cb44fa436b 100644
--- a/code/_onclick/click.dm
+++ b/code/_onclick/click.dm
@@ -99,7 +99,7 @@
AltClickOn(A)
return
if(LAZYACCESS(modifiers, CTRL_CLICK))
- CtrlClickOn(A)
+ CtrlClickOn(A, modifiers)
return
//PARIAH EDIT ADDITION
@@ -351,11 +351,11 @@
* Ctrl click
* For most objects, pull
*/
-/mob/proc/CtrlClickOn(atom/A)
- A.CtrlClick(src)
+/mob/proc/CtrlClickOn(atom/A, list/params)
+ A.CtrlClick(src, params)
return
-/atom/proc/CtrlClick(mob/user)
+/atom/proc/CtrlClick(mob/user, list/params)
SEND_SIGNAL(src, COMSIG_CLICK_CTRL, user)
SEND_SIGNAL(user, COMSIG_MOB_CTRL_CLICKED, src)
var/mob/living/ML = user
@@ -364,7 +364,7 @@
if(!can_interact(user))
return FALSE
-/mob/living/CtrlClick(mob/user)
+/mob/living/CtrlClick(mob/user, list/params)
if(!isliving(user) || !user.CanReach(src) || user.incapacitated())
return ..()
@@ -379,7 +379,7 @@
return ..()
-/mob/living/carbon/human/CtrlClick(mob/user)
+/mob/living/carbon/human/CtrlClick(mob/user, list/params)
if(!ishuman(user) || !user.CanReach(src) || user.incapacitated())
return ..()
@@ -388,7 +388,13 @@
return FALSE
var/mob/living/carbon/human/human_user = user
- if(human_user.dna.species.grab(human_user, src, human_user.mind.martial_art))
+ // If they're wielding a grab item, do the normal click chain.
+ var/obj/item/hand_item/grab/G = user.get_active_held_item()
+ if(isgrab(G))
+ G.current_grab.hit_with_grab(G, src, params)
+ return TRUE
+
+ if(human_user.dna.species.grab(human_user, src, human_user.mind.martial_art, params))
human_user.changeNext_move(CLICK_CD_MELEE)
human_user.animate_interact(src, INTERACT_GRAB)
return TRUE
diff --git a/code/_onclick/cyborg.dm b/code/_onclick/cyborg.dm
index 7f12d938471c..774072aace91 100644
--- a/code/_onclick/cyborg.dm
+++ b/code/_onclick/cyborg.dm
@@ -99,7 +99,7 @@
A.BorgCtrlShiftClick(src)
/mob/living/silicon/robot/ShiftClickOn(atom/A)
A.BorgShiftClick(src)
-/mob/living/silicon/robot/CtrlClickOn(atom/A)
+/mob/living/silicon/robot/CtrlClickOn(atom/A, list/params)
A.BorgCtrlClick(src)
/mob/living/silicon/robot/AltClickOn(atom/A)
A.BorgAltClick(src)
diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm
index 2509fc9ec2db..5d4fda7800fb 100644
--- a/code/_onclick/hud/screen_objects.dm
+++ b/code/_onclick/hud/screen_objects.dm
@@ -395,10 +395,12 @@
/atom/movable/screen/pull/Click()
if(isobserver(usr))
return
- usr.stop_pulling()
+ if(isliving(usr) && usr == hud.mymob)
+ var/mob/living/L = usr
+ L.release_all_grabs()
/atom/movable/screen/pull/update_icon_state()
- icon_state = "[base_icon_state][hud?.mymob?.pulling ? null : 0]"
+ icon_state = "[base_icon_state][LAZYLEN(hud?.mymob?:get_active_grabs()) ? null : 0]"
return ..()
/atom/movable/screen/resist
diff --git a/code/_onclick/item_attack.dm b/code/_onclick/item_attack.dm
index dc9a43b16b2b..24e9850bbe2e 100644
--- a/code/_onclick/item_attack.dm
+++ b/code/_onclick/item_attack.dm
@@ -348,3 +348,16 @@
visible_message(span_danger("[attack_message_spectator]"), vision_distance = COMBAT_MESSAGE_RANGE)
return 1
+
+/**
+ * Interaction handler for being clicked on with a grab. This is called regardless of user intent.
+ *
+ * **Parameters**:
+ * - `grab` - The grab item being used.
+ * - `click_params` - List of click parameters.
+ *
+ * Returns boolean to indicate whether the attack call was handled or not. If `FALSE`, the next `use_*` proc in the
+ * resolve chain will be called.
+ */
+/atom/proc/attack_grab(mob/living/user, atom/movable/victim, obj/item/hand_item/grab/grab, list/params)
+ return FALSE
diff --git a/code/_onclick/other_mobs.dm b/code/_onclick/other_mobs.dm
index 7c68a2ca584a..9085ab8e8687 100644
--- a/code/_onclick/other_mobs.dm
+++ b/code/_onclick/other_mobs.dm
@@ -122,7 +122,7 @@
return TRUE
if(isturf(A) && get_dist(src,A) <= 1)
- Move_Pulled(A)
+ move_grabbed_atoms_towards(A)
return TRUE
/*
diff --git a/code/_onclick/overmind.dm b/code/_onclick/overmind.dm
index d6b8994f82f0..555ba4b87d76 100644
--- a/code/_onclick/overmind.dm
+++ b/code/_onclick/overmind.dm
@@ -25,7 +25,7 @@
if(T)
rally_spores(T)
-/mob/camera/blob/CtrlClickOn(atom/A) //Create a shield
+/mob/camera/blob/CtrlClickOn(atom/A, list/params) //Create a shield
var/turf/T = get_turf(A)
if(T)
create_shield(T)
diff --git a/code/datums/ai/generic/generic_behaviors.dm b/code/datums/ai/generic/generic_behaviors.dm
index 08f6dc16b908..d3d1a18da088 100644
--- a/code/datums/ai/generic/generic_behaviors.dm
+++ b/code/datums/ai/generic/generic_behaviors.dm
@@ -43,7 +43,7 @@
if(get_dist(batman, big_guy) >= give_up_distance)
finish_action(controller, FALSE, target_key)
- big_guy.start_pulling(batman)
+ big_guy.try_make_grab(batman)
big_guy.setDir(get_dir(big_guy, batman))
batman.visible_message(span_warning("[batman] gets a slightly too tight hug from [big_guy]!"), span_userdanger("You feel your body break as [big_guy] embraces you!"))
diff --git a/code/datums/ai/idle_behaviors/idle_dog.dm b/code/datums/ai/idle_behaviors/idle_dog.dm
index fd27bc6968e3..6e570e301143 100644
--- a/code/datums/ai/idle_behaviors/idle_dog.dm
+++ b/code/datums/ai/idle_behaviors/idle_dog.dm
@@ -1,7 +1,7 @@
///Dog specific idle behavior.
/datum/idle_behavior/idle_dog/perform_idle_behavior(delta_time, datum/ai_controller/dog/controller)
var/mob/living/living_pawn = controller.pawn
- if(!isturf(living_pawn.loc) || living_pawn.pulledby)
+ if(!isturf(living_pawn.loc) || LAZYLEN(living_pawn.grabbed_by))
return
// if we were just ordered to heel, chill out for a bit
diff --git a/code/datums/ai/idle_behaviors/idle_monkey.dm b/code/datums/ai/idle_behaviors/idle_monkey.dm
index 66b630c35e12..c1c5f2d72b70 100644
--- a/code/datums/ai/idle_behaviors/idle_monkey.dm
+++ b/code/datums/ai/idle_behaviors/idle_monkey.dm
@@ -1,7 +1,7 @@
/datum/idle_behavior/idle_monkey/perform_idle_behavior(delta_time, datum/ai_controller/controller)
var/mob/living/living_pawn = controller.pawn
- if(DT_PROB(25, delta_time) && (living_pawn.mobility_flags & MOBILITY_MOVE) && isturf(living_pawn.loc) && !living_pawn.pulledby)
+ if(DT_PROB(25, delta_time) && (living_pawn.mobility_flags & MOBILITY_MOVE) && isturf(living_pawn.loc) && !LAZYLEN(living_pawn.grabbed_by))
var/move_dir = pick(GLOB.alldirs)
living_pawn.Move(get_step(living_pawn, move_dir), move_dir)
else if(DT_PROB(5, delta_time))
diff --git a/code/datums/ai/idle_behaviors/idle_random_walk.dm b/code/datums/ai/idle_behaviors/idle_random_walk.dm
index 1187d8786c1d..8adb8b6e01a8 100644
--- a/code/datums/ai/idle_behaviors/idle_random_walk.dm
+++ b/code/datums/ai/idle_behaviors/idle_random_walk.dm
@@ -6,7 +6,7 @@
. = ..()
var/mob/living/living_pawn = controller.pawn
- if(DT_PROB(walk_chance, delta_time) && (living_pawn.mobility_flags & MOBILITY_MOVE) && isturf(living_pawn.loc) && !living_pawn.pulledby)
+ if(DT_PROB(walk_chance, delta_time) && (living_pawn.mobility_flags & MOBILITY_MOVE) && isturf(living_pawn.loc) && !LAZYLEN(living_pawn.grabbed_by))
var/move_dir = pick(GLOB.alldirs)
living_pawn.Move(get_step(living_pawn, move_dir), move_dir)
diff --git a/code/datums/ai/monkey/monkey_behaviors.dm b/code/datums/ai/monkey/monkey_behaviors.dm
index 9132106dd88a..7c8a4499d0a7 100644
--- a/code/datums/ai/monkey/monkey_behaviors.dm
+++ b/code/datums/ai/monkey/monkey_behaviors.dm
@@ -259,9 +259,15 @@
controller.current_movement_target = target
- if(target.pulledby != living_pawn && !HAS_AI_CONTROLLER_TYPE(target.pulledby, /datum/ai_controller/monkey)) //Dont steal from my fellow monkeys.
+ var/monkey_is_grabbing_target = FALSE
+ for(var/obj/item/hand_item/grab/G as anything in target.grabbed_by)
+ if(!HAS_AI_CONTROLLER_TYPE(G.assailant, /datum/ai_controller/monkey))
+ monkey_is_grabbing_target = TRUE
+ break
+
+ if(!living_pawn.is_grabbing(target) && !monkey_is_grabbing_target) //Dont steal from my fellow monkeys.
if(living_pawn.Adjacent(target) && isturf(target.loc))
- target.grabbedby(living_pawn)
+ living_pawn.try_make_grab(target)
return //Do the rest next turn
var/datum/weakref/disposal_ref = controller.blackboard[disposal_target_key]
diff --git a/code/datums/ai/monkey/monkey_controller.dm b/code/datums/ai/monkey/monkey_controller.dm
index 98566a77a36f..08a77309ca3b 100644
--- a/code/datums/ai/monkey/monkey_controller.dm
+++ b/code/datums/ai/monkey/monkey_controller.dm
@@ -54,7 +54,7 @@ have ways of interacting with a specific mob and control it.
RegisterSignal(new_pawn, COMSIG_MOB_ATTACK_ALIEN, PROC_REF(on_attack_alien))
RegisterSignal(new_pawn, COMSIG_ATOM_BULLET_ACT, PROC_REF(on_bullet_act))
RegisterSignal(new_pawn, COMSIG_ATOM_HITBY, PROC_REF(on_hitby))
- RegisterSignal(new_pawn, COMSIG_LIVING_START_PULL, PROC_REF(on_startpulling))
+ RegisterSignal(new_pawn, COMSIG_ATOM_GET_GRABBED, PROC_REF(on_grabbed))
RegisterSignal(new_pawn, COMSIG_LIVING_TRY_SYRINGE, PROC_REF(on_try_syringe))
RegisterSignal(new_pawn, COMSIG_ATOM_HULK_ATTACK, PROC_REF(on_attack_hulk))
RegisterSignal(new_pawn, COMSIG_CARBON_CUFF_ATTEMPTED, PROC_REF(on_attempt_cuff))
@@ -70,7 +70,7 @@ have ways of interacting with a specific mob and control it.
COMSIG_ATOM_ATTACK_PAW,
COMSIG_ATOM_BULLET_ACT,
COMSIG_ATOM_HITBY,
- COMSIG_LIVING_START_PULL,
+ COMSIG_ATOM_GET_GRABBED,
COMSIG_LIVING_TRY_SYRINGE,
COMSIG_ATOM_HULK_ATTACK,
COMSIG_CARBON_CUFF_ATTEMPTED,
@@ -189,11 +189,12 @@ have ways of interacting with a specific mob and control it.
in_the_way_mob.knockOver(living_pawn)
return
-/datum/ai_controller/monkey/proc/on_startpulling(datum/source, atom/movable/puller, state, force)
+/datum/ai_controller/monkey/proc/on_grabbed(datum/source, atom/movable/puller)
SIGNAL_HANDLER
+
var/mob/living/living_pawn = pawn
if(!IS_DEAD_OR_INCAP(living_pawn) && prob(MONKEY_PULL_AGGRO_PROB)) // nuh uh you don't pull me!
- retaliate(living_pawn.pulledby)
+ retaliate(puller)
return TRUE
/datum/ai_controller/monkey/proc/on_try_syringe(datum/source, mob/user)
diff --git a/code/datums/ai/movement/_ai_movement.dm b/code/datums/ai/movement/_ai_movement.dm
index fa83d64e7986..c383b15769a7 100644
--- a/code/datums/ai/movement/_ai_movement.dm
+++ b/code/datums/ai/movement/_ai_movement.dm
@@ -29,7 +29,7 @@
source.delay = controller.movement_delay
var/can_move = TRUE
- if(controller.ai_traits & STOP_MOVING_WHEN_PULLED && pawn.pulledby) //Need to store more state. Annoying.
+ if(controller.ai_traits & STOP_MOVING_WHEN_PULLED && LAZYLEN(pawn.grabbed_by)) //Need to store more state. Annoying.
can_move = FALSE
if(!isturf(pawn.loc)) //No moving if not on a turf
diff --git a/code/datums/ai/robot_customer/robot_customer_controller.dm b/code/datums/ai/robot_customer/robot_customer_controller.dm
index 901c0f2f69c6..22fc2e267074 100644
--- a/code/datums/ai/robot_customer/robot_customer_controller.dm
+++ b/code/datums/ai/robot_customer/robot_customer_controller.dm
@@ -21,12 +21,12 @@
if(!istype(new_pawn, /mob/living/simple_animal/robot_customer))
return AI_CONTROLLER_INCOMPATIBLE
RegisterSignal(new_pawn, COMSIG_PARENT_ATTACKBY, PROC_REF(on_attackby))
- RegisterSignal(new_pawn, COMSIG_LIVING_GET_PULLED, PROC_REF(on_get_pulled))
+ RegisterSignal(new_pawn, COMSIG_ATOM_GET_GRABBED, PROC_REF(on_get_pulled))
RegisterSignal(new_pawn, COMSIG_ATOM_ATTACK_HAND, PROC_REF(on_get_punched))
return ..() //Run parent at end
/datum/ai_controller/robot_customer/UnpossessPawn(destroy)
- UnregisterSignal(pawn, list(COMSIG_PARENT_ATTACKBY, COMSIG_LIVING_GET_PULLED, COMSIG_ATOM_ATTACK_HAND))
+ UnregisterSignal(pawn, list(COMSIG_PARENT_ATTACKBY, COMSIG_ATOM_GET_GRABBED, COMSIG_ATOM_ATTACK_HAND))
return ..() //Run parent at end
/datum/ai_controller/robot_customer/proc/on_attackby(datum/source, obj/item/I, mob/living/user)
diff --git a/code/datums/brain_damage/special.dm b/code/datums/brain_damage/special.dm
index 52c418bab37c..e7f3e112663c 100644
--- a/code/datums/brain_damage/special.dm
+++ b/code/datums/brain_damage/special.dm
@@ -170,18 +170,17 @@
/datum/brain_trauma/special/quantum_alignment/proc/try_entangle()
//Check for pulled mobs
- if(ismob(owner.pulling))
- entangle(owner.pulling)
+ var/list/grabs = owner.get_active_grabs()
+ if(length(grabs))
+ for(var/obj/item/hand_item/grab/G in grabs)
+ entangle(G.affecting)
return
+
//Check for adjacent mobs
for(var/mob/living/L in oview(1, owner))
if(owner.Adjacent(L))
entangle(L)
return
- //Check for pulled objects
- if(isobj(owner.pulling))
- entangle(owner.pulling)
- return
//Check main hand
var/obj/item/held_item = owner.get_active_held_item()
diff --git a/code/datums/components/butchering.dm b/code/datums/components/butchering.dm
index 7f93831b78a2..6574667a4b2b 100644
--- a/code/datums/components/butchering.dm
+++ b/code/datums/components/butchering.dm
@@ -29,6 +29,7 @@
can_be_blunt = _can_be_blunt
if(_butcher_callback)
butcher_callback = _butcher_callback
+
if(isitem(parent))
RegisterSignal(parent, COMSIG_ITEM_ATTACK, PROC_REF(onItemAttack))
@@ -40,49 +41,12 @@
INVOKE_ASYNC(src, PROC_REF(startButcher), source, M, user)
return COMPONENT_CANCEL_ATTACK_CHAIN
- if(ishuman(M) && source.force && (source.sharpness & SHARP_EDGED))
- var/mob/living/carbon/human/H = M
- if((user.pulling == H && user.grab_state >= GRAB_AGGRESSIVE) && user.zone_selected == BODY_ZONE_HEAD) // Only aggressive grabbed can be sliced.
- if(H.has_status_effect(/datum/status_effect/neck_slice))
- user.show_message(span_warning("[H]'s neck has already been already cut, you can't make the bleeding any worse!"), MSG_VISUAL, \
- span_warning("Their neck has already been already cut, you can't make the bleeding any worse!"))
- return COMPONENT_CANCEL_ATTACK_CHAIN
- INVOKE_ASYNC(src, PROC_REF(startNeckSlice), source, H, user)
- return COMPONENT_CANCEL_ATTACK_CHAIN
-
/datum/component/butchering/proc/startButcher(obj/item/source, mob/living/M, mob/living/user)
to_chat(user, span_notice("You begin to butcher [M]..."))
playsound(M.loc, butcher_sound, 50, TRUE, -1)
if(do_after(user, M, speed, DO_PUBLIC, display = parent) && M.Adjacent(source))
Butcher(user, M)
-/datum/component/butchering/proc/startNeckSlice(obj/item/source, mob/living/carbon/human/H, mob/living/user)
- if(DOING_INTERACTION_WITH_TARGET(user, H))
- to_chat(user, span_warning("You're already interacting with [H]!"))
- return
-
- user.visible_message(span_danger("[user] is slitting [H]'s throat!"), \
- span_danger("You start slicing [H]'s throat!"), \
- span_hear("You hear a cutting noise!"), ignored_mobs = H)
- H.show_message(span_userdanger("Your throat is being slit by [user]!"), MSG_VISUAL, \
- span_userdanger("Something is cutting into your neck!"), NONE)
- log_combat(user, H, "attempted throat slitting", source)
-
- playsound(H.loc, butcher_sound, 50, TRUE, -1)
- if(do_after(user, H, clamp(500 / source.force, 30, 100), DO_PUBLIC, display = parent) && H.Adjacent(source))
- if(H.has_status_effect(/datum/status_effect/neck_slice))
- user.show_message(span_warning("[H]'s neck has already been already cut, you can't make the bleeding any worse!"), MSG_VISUAL, \
- span_warning("Their neck has already been already cut, you can't make the bleeding any worse!"))
- return
-
- H.visible_message(span_danger("[user] slits [H]'s throat!"), \
- span_userdanger("[user] slits your throat..."))
- log_combat(user, H, "wounded via throat slitting", source)
- var/obj/item/bodypart/slit_throat = H.get_bodypart(BODY_ZONE_HEAD)
-
- slit_throat.create_wound_easy(/datum/wound/cut/flesh, 30)
- H.apply_status_effect(/datum/status_effect/neck_slice)
-
/**
* Handles a user butchering a target
*
diff --git a/code/datums/components/drift.dm b/code/datums/components/drift.dm
index 58bcff8acdf6..a649b3947f9c 100644
--- a/code/datums/components/drift.dm
+++ b/code/datums/components/drift.dm
@@ -92,14 +92,14 @@
// This way you can't ride two movements at once while drifting, since that'd be dumb as fuck
RegisterSignal(movable_parent, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE, PROC_REF(handle_glidesize_update))
// If you stop pulling something mid drift, I want it to retain that momentum
- RegisterSignal(movable_parent, COMSIG_ATOM_NO_LONGER_PULLING, PROC_REF(stopped_pulling))
+ RegisterSignal(movable_parent, COMSIG_LIVING_NO_LONGER_GRABBING, PROC_REF(stopped_pulling))
/datum/component/drift/proc/drifting_stop()
SIGNAL_HANDLER
var/atom/movable/movable_parent = parent
movable_parent.inertia_moving = FALSE
ignore_next_glide = FALSE
- UnregisterSignal(movable_parent, list(COMSIG_MOVABLE_MOVED, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE, COMSIG_ATOM_NO_LONGER_PULLING))
+ UnregisterSignal(movable_parent, list(COMSIG_MOVABLE_MOVED, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE, COMSIG_LIVING_NO_LONGER_GRABBING))
/datum/component/drift/proc/before_move(datum/source)
SIGNAL_HANDLER
diff --git a/code/datums/components/jetpack.dm b/code/datums/components/jetpack.dm
index 73a26b986fd3..1f9f2dda3820 100644
--- a/code/datums/components/jetpack.dm
+++ b/code/datums/components/jetpack.dm
@@ -109,7 +109,7 @@
return
if(!(user.movement_type & FLOATING) || user.buckled)//You don't want use jet in gravity or while buckled.
return
- if(user.pulledby)//You don't must use jet if someone pull you
+ if(LAZYLEN(user.grabbed_by))//You don't must use jet if someone pull you
return
if(user.throwing)//You don't must use jet if you thrown
return
diff --git a/code/datums/components/riding/riding_mob.dm b/code/datums/components/riding/riding_mob.dm
index f6945d215cc1..3a112c079bf3 100644
--- a/code/datums/components/riding/riding_mob.dm
+++ b/code/datums/components/riding/riding_mob.dm
@@ -13,7 +13,7 @@
. = ..()
var/mob/living/living_parent = parent
- living_parent.stop_pulling() // was only used on humans previously, may change some other behavior
+ living_parent.release_all_grabs() // was only used on humans previously, may change some other behavior
log_riding(living_parent, riding_mob)
riding_mob.set_glide_size(living_parent.glide_size)
handle_vehicle_offsets(living_parent.dir)
diff --git a/code/datums/components/rot.dm b/code/datums/components/rot.dm
index 9e1b186dc8e7..8ec182019770 100644
--- a/code/datums/components/rot.dm
+++ b/code/datums/components/rot.dm
@@ -43,7 +43,7 @@
RegisterSignal(parent, COMSIG_MOVABLE_BUMP, PROC_REF(rot_react))
if(isliving(parent))
RegisterSignal(parent, COMSIG_LIVING_REVIVE, PROC_REF(react_to_revive)) //mobs stop this when they come to life
- RegisterSignal(parent, COMSIG_LIVING_GET_PULLED, PROC_REF(rot_react_touch))
+ RegisterSignal(parent, COMSIG_ATOM_GET_GRABBED, PROC_REF(rot_react_touch))
if(iscarbon(parent))
var/mob/living/carbon/carbon_parent = parent
RegisterSignal(carbon_parent.reagents, list(COMSIG_REAGENTS_ADD_REAGENT,
diff --git a/code/datums/components/shy.dm b/code/datums/components/shy.dm
index 433b723e9923..b3823031a3b0 100644
--- a/code/datums/components/shy.dm
+++ b/code/datums/components/shy.dm
@@ -45,7 +45,7 @@
/datum/component/shy/RegisterWithParent()
RegisterSignal(parent, COMSIG_MOB_CLICKON, PROC_REF(on_clickon))
- RegisterSignal(parent, COMSIG_LIVING_TRY_PULL, PROC_REF(on_try_pull))
+ RegisterSignal(parent, COMSIG_LIVING_TRY_GRAB, PROC_REF(on_try_pull))
RegisterSignal(parent, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_EARLY_UNARMED_ATTACK), PROC_REF(on_unarmed_attack))
RegisterSignal(parent, COMSIG_TRY_STRIP, PROC_REF(on_try_strip))
RegisterSignal(parent, COMSIG_TRY_ALT_ACTION, PROC_REF(on_try_alt_action))
@@ -53,7 +53,7 @@
/datum/component/shy/UnregisterFromParent()
UnregisterSignal(parent, list(
COMSIG_MOB_CLICKON,
- COMSIG_LIVING_TRY_PULL,
+ COMSIG_LIVING_TRY_GRAB,
COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_EARLY_UNARMED_ATTACK,
COMSIG_TRY_STRIP,
COMSIG_TRY_ALT_ACTION,
@@ -118,7 +118,7 @@
/datum/component/shy/proc/on_try_pull(datum/source, atom/movable/target, force)
SIGNAL_HANDLER
- return is_shy(target) && COMSIG_LIVING_CANCEL_PULL
+ return is_shy(target) && COMSIG_LIVING_CANCEL_GRAB
/datum/component/shy/proc/on_unarmed_attack(datum/source, atom/target, proximity, modifiers)
SIGNAL_HANDLER
diff --git a/code/datums/components/shy_in_room.dm b/code/datums/components/shy_in_room.dm
index cf4319339630..a6a7933a0b4b 100644
--- a/code/datums/components/shy_in_room.dm
+++ b/code/datums/components/shy_in_room.dm
@@ -16,7 +16,7 @@
/datum/component/shy_in_room/RegisterWithParent()
RegisterSignal(parent, COMSIG_MOB_CLICKON, PROC_REF(on_clickon))
- RegisterSignal(parent, COMSIG_LIVING_TRY_PULL, PROC_REF(on_try_pull))
+ RegisterSignal(parent, COMSIG_LIVING_TRY_GRAB, PROC_REF(on_try_pull))
RegisterSignal(parent, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_EARLY_UNARMED_ATTACK), PROC_REF(on_unarmed_attack))
RegisterSignal(parent, COMSIG_TRY_STRIP, PROC_REF(on_try_strip))
RegisterSignal(parent, COMSIG_TRY_ALT_ACTION, PROC_REF(on_try_alt_action))
@@ -25,7 +25,7 @@
/datum/component/shy_in_room/UnregisterFromParent()
UnregisterSignal(parent, list(
COMSIG_MOB_CLICKON,
- COMSIG_LIVING_TRY_PULL,
+ COMSIG_LIVING_TRY_GRAB,
COMSIG_LIVING_UNARMED_ATTACK,
COMSIG_HUMAN_EARLY_UNARMED_ATTACK,
COMSIG_TRY_STRIP,
@@ -60,7 +60,7 @@
/datum/component/shy_in_room/proc/on_try_pull(datum/source, atom/movable/target, force)
SIGNAL_HANDLER
- return is_shy(target) && COMSIG_LIVING_CANCEL_PULL
+ return is_shy(target) && COMSIG_LIVING_CANCEL_GRAB
/datum/component/shy_in_room/proc/on_unarmed_attack(datum/source, atom/target, proximity, modifiers)
SIGNAL_HANDLER
diff --git a/code/datums/components/strong_pull.dm b/code/datums/components/strong_pull.dm
index f0cb206a650d..85e251c234ae 100644
--- a/code/datums/components/strong_pull.dm
+++ b/code/datums/components/strong_pull.dm
@@ -17,7 +17,7 @@ Basically, the items they pull cannot be pulled (except by the puller)
/datum/component/strong_pull/RegisterWithParent()
. = ..()
- RegisterSignal(parent, COMSIG_LIVING_START_PULL, PROC_REF(on_pull))
+ RegisterSignal(parent, COMSIG_LIVING_START_GRAB, PROC_REF(on_pull))
/**
* Called when the parent grabs something, adds signals to the object to reject interactions
@@ -25,8 +25,8 @@ Basically, the items they pull cannot be pulled (except by the puller)
/datum/component/strong_pull/proc/on_pull(datum/source, atom/movable/pulled, state, force)
SIGNAL_HANDLER
strongpulling = pulled
- RegisterSignal(strongpulling, COMSIG_ATOM_CAN_BE_PULLED, PROC_REF(reject_further_pulls))
- RegisterSignal(strongpulling, COMSIG_ATOM_NO_LONGER_PULLED, PROC_REF(on_no_longer_pulled))
+ RegisterSignal(strongpulling, COMSIG_ATOM_CAN_BE_GRABBED, PROC_REF(reject_further_pulls))
+ RegisterSignal(strongpulling, COMSIG_ATOM_NO_LONGER_GRABBED, PROC_REF(on_no_longer_pulled))
if(istype(strongpulling, /obj/structure/closet) && !istype(strongpulling, /obj/structure/closet/body_bag))
var/obj/structure/closet/grabbed_closet = strongpulling
grabbed_closet.strong_grab = TRUE
@@ -37,13 +37,13 @@ Basically, the items they pull cannot be pulled (except by the puller)
/datum/component/strong_pull/proc/reject_further_pulls(datum/source, mob/living/puller)
SIGNAL_HANDLER
if(puller != parent) //for increasing grabs, you need to have a valid pull. thus, parent should be able to pull the same object again
- return COMSIG_ATOM_CANT_PULL
+ return COMSIG_ATOM_NO_GRAB
/*
* Unregisters signals and stops any buffs to pulling.
*/
/datum/component/strong_pull/proc/lose_strong_grip()
- UnregisterSignal(strongpulling, list(COMSIG_ATOM_CAN_BE_PULLED, COMSIG_ATOM_NO_LONGER_PULLED))
+ UnregisterSignal(strongpulling, list(COMSIG_ATOM_CAN_BE_GRABBED, COMSIG_ATOM_NO_LONGER_GRABBED))
if(istype(strongpulling, /obj/structure/closet))
var/obj/structure/closet/ungrabbed_closet = strongpulling
ungrabbed_closet.strong_grab = FALSE
diff --git a/code/datums/components/tackle.dm b/code/datums/components/tackle.dm
index 68da2882c5d9..c665ee56bddc 100644
--- a/code/datums/components/tackle.dm
+++ b/code/datums/components/tackle.dm
@@ -74,7 +74,7 @@
if(modifiers[ALT_CLICK] || modifiers[SHIFT_CLICK] || modifiers[CTRL_CLICK] || modifiers[MIDDLE_CLICK])
return
- if(!user.throw_mode || user.get_active_held_item() || user.pulling || user.buckled || user.incapacitated())
+ if(!user.throw_mode || user.get_active_held_item() || user.get_active_grabs() || user.buckled || user.incapacitated())
return
if(!A || !(isturf(A) || isturf(A.loc)))
@@ -211,9 +211,7 @@
target.stamina.adjust(-40)
target.Paralyze(5)
target.Knockdown(30)
- if(ishuman(target) && ishuman(user))
- INVOKE_ASYNC(S.dna.species, TYPE_PROC_REF(/datum/species, grab), S, T)
- S.setGrabState(GRAB_PASSIVE)
+ S.try_make_grab(target, /datum/grab/normal/aggressive)
if(5 to INFINITY) // absolutely BODIED
user.visible_message(span_warning("[user] lands a monster [tackle_word] on [target], knocking [target.p_them()] senseless and applying an aggressive pin!"), span_userdanger("You land a monster [tackle_word] on [target], knocking [target.p_them()] senseless and applying an aggressive pin!"), ignored_mobs = target)
@@ -225,10 +223,7 @@
target.stamina.adjust(-40)
target.Paralyze(5)
target.Knockdown(30)
- if(ishuman(target) && ishuman(user))
- INVOKE_ASYNC(S.dna.species, TYPE_PROC_REF(/datum/species, grab), S, T)
- S.setGrabState(GRAB_AGGRESSIVE)
-
+ S.try_make_grab(target, /datum/grab/normal/aggressive)
return COMPONENT_MOVABLE_IMPACT_FLIP_HITPUSH
diff --git a/code/datums/elements/atmos_requirements.dm b/code/datums/elements/atmos_requirements.dm
index ff2ede8401b2..71f074978291 100644
--- a/code/datums/elements/atmos_requirements.dm
+++ b/code/datums/elements/atmos_requirements.dm
@@ -36,7 +36,7 @@
target.throw_alert(ALERT_NOT_ENOUGH_OXYGEN, /atom/movable/screen/alert/not_enough_oxy)
/datum/element/atmos_requirements/proc/is_breathable_atmos(mob/living/target)
- if(target.pulledby && target.pulledby.grab_state >= GRAB_KILL && atmos_requirements["min_oxy"])
+ if(HAS_TRAIT(target, TRAIT_KILL_GRAB) && atmos_requirements["min_oxy"])
return FALSE
if(!isopenturf(target.loc))
diff --git a/code/datums/elements/nerfed_pulling.dm b/code/datums/elements/nerfed_pulling.dm
deleted file mode 100644
index c192e04f5c2d..000000000000
--- a/code/datums/elements/nerfed_pulling.dm
+++ /dev/null
@@ -1,53 +0,0 @@
-/// This living will be slower when pulling/moving anything in the given typecache
-/datum/element/nerfed_pulling
- element_flags = ELEMENT_BESPOKE | ELEMENT_DETACH
- id_arg_index = 2
-
- /// The typecache of things that shouldn't be easily movable
- var/list/typecache
-
-/datum/element/nerfed_pulling/Attach(datum/target, list/typecache)
- . = ..()
-
- if (!isliving(target))
- return ELEMENT_INCOMPATIBLE
-
- src.typecache = typecache
-
- RegisterSignal(target, COMSIG_LIVING_PUSHING_MOVABLE, PROC_REF(on_push_movable))
- RegisterSignal(target, COMSIG_LIVING_UPDATING_PULL_MOVESPEED, PROC_REF(on_updating_pull_movespeed))
-
-/datum/element/nerfed_pulling/Detach(mob/living/source)
- source.remove_movespeed_modifier(/datum/movespeed_modifier/nerfed_bump)
- source.remove_movespeed_modifier(/datum/movespeed_modifier/nerfed_pull)
-
- UnregisterSignal(source, list(COMSIG_LIVING_PUSHING_MOVABLE, COMSIG_LIVING_UPDATING_PULL_MOVESPEED))
-
- return ..()
-
-/datum/element/nerfed_pulling/proc/on_push_movable(mob/living/source, atom/movable/being_pushed)
- SIGNAL_HANDLER
-
- if (!will_slow_down(being_pushed))
- return
-
- source.add_movespeed_modifier(/datum/movespeed_modifier/nerfed_bump)
- addtimer(CALLBACK(source, TYPE_PROC_REF(/mob, remove_movespeed_modifier), /datum/movespeed_modifier/nerfed_bump), 1 SECONDS, TIMER_OVERRIDE | TIMER_UNIQUE)
-
-/datum/element/nerfed_pulling/proc/on_updating_pull_movespeed(mob/living/source)
- SIGNAL_HANDLER
-
- if (!will_slow_down(source.pulling))
- source.remove_movespeed_modifier(/datum/movespeed_modifier/nerfed_pull)
- return
-
- source.add_movespeed_modifier(/datum/movespeed_modifier/nerfed_pull)
-
-/datum/element/nerfed_pulling/proc/will_slow_down(datum/input)
- return !isnull(input) && typecache[input.type]
-
-/datum/movespeed_modifier/nerfed_pull
- multiplicative_slowdown = 5.5
-
-/datum/movespeed_modifier/nerfed_bump
- multiplicative_slowdown = 5.5
diff --git a/code/datums/keybinding/mob.dm b/code/datums/keybinding/mob.dm
index e34142ffd72f..e3bbdc8e4f2d 100644
--- a/code/datums/keybinding/mob.dm
+++ b/code/datums/keybinding/mob.dm
@@ -12,11 +12,13 @@
. = ..()
if(.)
return
- var/mob/M = user.mob
- if(!M.pulling)
- to_chat(user, span_notice("You are not pulling anything."))
+ var/mob/living/M = user.mob
+ if(!istype(M))
+ return
+ if(!LAZYLEN(M.get_active_grabs()))
+ to_chat(user, span_notice("You are not grabbing anything."))
else
- M.stop_pulling()
+ M.release_all_grabs()
return TRUE
/datum/keybinding/mob/swap_hands
diff --git a/code/datums/martial/cqc.dm b/code/datums/martial/cqc.dm
index 262d299c4578..6b9702fc67e3 100644
--- a/code/datums/martial/cqc.dm
+++ b/code/datums/martial/cqc.dm
@@ -122,11 +122,17 @@
add_to_streak("G",D)
if(check_streak(A,D)) //if a combo is made no grab upgrade is done
return TRUE
- old_grab_state = A.grab_state
- D.grabbedby(A, 1)
+ var/obj/item/hand_item/grab/G = A.is_grabbing(D)
+ if(G)
+ old_grab_state = G?.current_grab.damage_stage
+ if(old_grab_state == GRAB_PASSIVE)
+ G.upgrade()
+
+ else
+ A.try_make_grab(D, /datum/grab/normal/aggressive)
+
if(old_grab_state == GRAB_PASSIVE)
D.drop_all_held_items()
- A.setGrabState(GRAB_AGGRESSIVE) //Instant aggressive grab if on grab intent
log_combat(A, D, "grabbed", addition="aggressively")
D.visible_message(span_warning("[A] violently grabs [D]!"), \
span_userdanger("You're grabbed violently by [A]!"), span_hear("You hear sounds of aggressive fondling!"), COMBAT_MESSAGE_RANGE, A)
@@ -191,15 +197,17 @@
to_chat(A, span_warning("You fail to disarm [D]!"))
playsound(D, 'sound/weapons/punchmiss.ogg', 25, TRUE, -1)
log_combat(A, D, "disarmed (CQC)", "[I ? " grabbing \the [I]" : ""]")
- if(restraining && A.pulling == D)
+
+ var/obj/item/hand_item/grab/G = A.is_grabbing(D)
+ if(restraining && G)
log_combat(A, D, "knocked out (Chokehold)(CQC)")
D.visible_message(span_danger("[A] puts [D] into a chokehold!"), \
span_userdanger("You're put into a chokehold by [A]!"), span_hear("You hear shuffling and a muffled groan!"), null, A)
to_chat(A, span_danger("You put [D] into a chokehold!"))
D.SetSleeping(400)
restraining = FALSE
- if(A.grab_state < GRAB_NECK && !HAS_TRAIT(A, TRAIT_PACIFISM))
- A.setGrabState(GRAB_NECK)
+ if(G.current_grab.damage_stage < GRAB_NECK && !HAS_TRAIT(A, TRAIT_PACIFISM))
+ G.upgrade()
else
restraining = FALSE
return FALSE
diff --git a/code/datums/martial/psychotic_brawl.dm b/code/datums/martial/psychotic_brawl.dm
index 329144feebb9..71cea7c1d96e 100644
--- a/code/datums/martial/psychotic_brawl.dm
+++ b/code/datums/martial/psychotic_brawl.dm
@@ -24,22 +24,24 @@
A.Stun(20)
atk_verb = "cried looking at"
if(3)
- if(A.grab_state >= GRAB_AGGRESSIVE)
- D.grabbedby(A, 1)
+ var/obj/item/hand_item/grab/G = A.is_grabbing(D)
+ if(G?.current_grab.damage_stage == GRAB_AGGRESSIVE)
+ G.upgrade(TRUE)
else
- A.start_pulling(D, supress_message = TRUE)
- if(A.pulling)
+ A.try_make_grab(D)
+ G = A.is_grabbing(D)
+ if(G)
D.drop_all_held_items()
- D.stop_pulling()
+ D.release_all_grabs()
if(grab_attack)
log_combat(A, D, "grabbed", addition="aggressively")
D.visible_message(span_warning("[A] violently grabs [D]!"), \
span_userdanger("You're violently grabbed by [A]!"), span_hear("You hear sounds of aggressive fondling!"), null, A)
to_chat(A, span_danger("You violently grab [D]!"))
- A.setGrabState(GRAB_AGGRESSIVE) //Instant aggressive grab
+ G.upgrade(TRUE)
else
log_combat(A, D, "grabbed", addition="passively")
- A.setGrabState(GRAB_PASSIVE)
+
if(4)
A.do_attack_animation(D, ATTACK_EFFECT_PUNCH)
atk_verb = "headbutt"
diff --git a/code/datums/martial/wrestling.dm b/code/datums/martial/wrestling.dm
index 0ca18ae84110..26f649ed2544 100644
--- a/code/datums/martial/wrestling.dm
+++ b/code/datums/martial/wrestling.dm
@@ -130,7 +130,7 @@ If you make a derivative work from this code, you must include this notification
/datum/martial_art/wrestling/proc/throw_wrassle(mob/living/A, mob/living/D)
if(!D)
return
- if(!A.pulling || A.pulling != D)
+ if(!A.is_grabbing(D))
to_chat(A, span_warning("You need to have [D] in a cinch!"))
return
D.forceMove(A.loc)
@@ -214,7 +214,7 @@ If you make a derivative work from this code, you must include this notification
/datum/martial_art/wrestling/proc/slam(mob/living/A, mob/living/D)
if(!D)
return
- if(!A.pulling || A.pulling != D)
+ if(!A.is_grabbing(D))
to_chat(A, span_warning("You need to have [D] in a cinch!"))
return
D.forceMove(A.loc)
@@ -452,9 +452,10 @@ If you make a derivative work from this code, you must include this notification
/datum/martial_art/wrestling/grab_act(mob/living/A, mob/living/D)
if(check_streak(A,D))
return 1
- if(A.pulling == D)
+ if(A.is_grabbing(D))
+ return 1
+ if(!A.try_make_grab(D))
return 1
- A.start_pulling(D)
D.visible_message(span_danger("[A] gets [D] in a cinch!"), \
span_userdanger("You're put into a cinch by [A]!"), span_hear("You hear aggressive shuffling!"), COMBAT_MESSAGE_RANGE, A)
to_chat(A, span_danger("You get [D] in a cinch!"))
diff --git a/code/datums/mutations/hulk.dm b/code/datums/mutations/hulk.dm
index 2c28e969a08b..32111d8c27af 100644
--- a/code/datums/mutations/hulk.dm
+++ b/code/datums/mutations/hulk.dm
@@ -102,10 +102,11 @@
return
if(!user.throw_mode || user.get_active_held_item() || user.zone_selected != BODY_ZONE_PRECISE_GROIN)
return
- if(user.grab_state < GRAB_NECK || !iscarbon(user.pulling) || user.buckled || user.incapacitated())
+ var/obj/item/hand_item/grab/G = user.get_active_grab()
+ if(G.current_grab.damage_stage < GRAB_NECK || !iscarbon(G.affecting) || user.buckled || user.incapacitated())
return
- var/mob/living/carbon/possible_throwable = user.pulling
+ var/mob/living/carbon/possible_throwable = G.affecting
if(!possible_throwable.getorganslot(ORGAN_SLOT_EXTERNAL_TAIL))
return
diff --git a/code/datums/station_traits/negative_traits.dm b/code/datums/station_traits/negative_traits.dm
index 610631947394..fce4ef1f1578 100644
--- a/code/datums/station_traits/negative_traits.dm
+++ b/code/datums/station_traits/negative_traits.dm
@@ -17,15 +17,6 @@
/datum/station_trait/distant_supply_lines/on_round_start()
SSeconomy.pack_price_modifier *= 1.2
-/datum/station_trait/random_spawns
- name = "Drive-by landing"
- trait_type = STATION_TRAIT_NEGATIVE
- weight = 2
- show_in_report = TRUE
- report_message = "Sorry for that, we missed your station by a few miles, so we just launched you towards your station in pods. Hope you don't mind!"
- trait_to_give = STATION_TRAIT_RANDOM_ARRIVALS
- blacklist = list(/datum/station_trait/hangover)
-
/datum/station_trait/hangover
name = "Hangover"
trait_type = STATION_TRAIT_NEGATIVE
@@ -33,7 +24,6 @@
show_in_report = TRUE
report_message = "Ohh....Man....That mandatory office party from last shift...God that was awesome..I woke up in some random toilet 3 sectors away..."
trait_to_give = STATION_TRAIT_HANGOVER
- blacklist = list(/datum/station_trait/random_spawns)
/datum/station_trait/hangover/New()
. = ..()
diff --git a/code/game/area/areas/shuttles.dm b/code/game/area/areas/shuttles.dm
index 551260b4a11e..b5b8bfd7eec3 100644
--- a/code/game/area/areas/shuttles.dm
+++ b/code/game/area/areas/shuttles.dm
@@ -254,9 +254,9 @@
return
var/mob/living/L = AM
- if(L.pulling && istype(L.pulling, /obj/item/bodypart/head))
+ if(istype(L.get_active_grab()?.affecting, /obj/item/bodypart/head))
to_chat(L, span_notice("Your offering is accepted. You may pass."), confidential = TRUE)
- qdel(L.pulling)
+ qdel(L.get_active_grab()?.affecting)
var/turf/LA = get_turf(pick(warp_points))
L.forceMove(LA)
L.hallucination = 0
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index da32f9f269f6..6e95c092b8b7 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -1389,11 +1389,11 @@
*
* Default behaviour is to send the [COMSIG_ATOM_EXIT]
*/
-/atom/Exit(atom/movable/leaving, direction)
+/atom/Exit(atom/movable/leaving, direction, no_side_effects)
// Don't call `..()` here, otherwise `Uncross()` gets called.
// See the doc comment on `Uncross()` to learn why this is bad.
- if(SEND_SIGNAL(src, COMSIG_ATOM_EXIT, leaving, direction) & COMPONENT_ATOM_BLOCK_EXIT)
+ if(SEND_SIGNAL(src, COMSIG_ATOM_EXIT, leaving, direction, no_side_effects) & COMPONENT_ATOM_BLOCK_EXIT)
return FALSE
return TRUE
@@ -2218,3 +2218,9 @@
//Currently only changed by Observers to be hearing through their orbit target.
/atom/proc/hear_location()
return src
+
+///Reset plane and layer values to their defaults.
+/atom/proc/reset_plane_and_layer()
+ plane = initial(plane)
+ layer = initial(layer)
+
diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm
index 004b99dbb652..86bd47b393cd 100644
--- a/code/game/atoms_movable.dm
+++ b/code/game/atoms_movable.dm
@@ -13,7 +13,6 @@
var/throw_range = 7
///Max range this atom can be thrown via telekinesis
var/tk_throw_range = 10
- var/mob/pulledby = null
var/initial_language_holder = /datum/language_holder
var/datum/language_holder/language_holder // Mindless mobs and objects need language too, some times. Mind holder takes prescedence.
var/verb_say = "says"
@@ -63,8 +62,6 @@
*/
var/movement_type = GROUND
- var/atom/movable/pulling
- var/grab_state = 0
var/throwforce = 0
var/datum/component/orbiter/orbiting
@@ -95,6 +92,9 @@
///For storing what do_after's someone has, key = string, value = amount of interactions of that type happening.
var/list/do_afters
+ ///A lazylist of grab objects gripping us
+ var/list/grabbed_by
+
/mutable_appearance/emissive_blocker
/mutable_appearance/emissive_blocker/New()
@@ -167,11 +167,6 @@
invisibility = INVISIBILITY_ABSTRACT
- if(pulledby)
- pulledby.stop_pulling()
- if(pulling)
- stop_pulling()
-
if(orbiting)
orbiting.end_orbit(src)
orbiting = null
@@ -221,297 +216,6 @@
if(emissive_block)
. += emissive_block
-/atom/movable/proc/onZImpact(turf/impacted_turf, levels, message = TRUE)
- if(message)
- visible_message(
- span_danger("[src] slams into [impacted_turf]!"),
- blind_message = span_hear("You hear something slam into the deck.")
- )
-
- INVOKE_ASYNC(src, PROC_REF(SpinAnimation), 5, 2)
- return TRUE
-
-/*
- * The core multi-z movement proc. Used to move a movable through z levels.
- * If target is null, it'll be determined by the can_z_move proc, which can potentially return null if
- * conditions aren't met (see z_move_flags defines in __DEFINES/movement.dm for info) or if dir isn't set.
- * Bear in mind you don't need to set both target and dir when calling this proc, but at least one or two.
- * This will set the currently_z_moving to CURRENTLY_Z_MOVING_GENERIC if unset, and then clear it after
- * Forcemove().
- *
- *
- * Args:
- * * dir: the direction to go, UP or DOWN, only relevant if target is null.
- * * target: The target turf to move the src to. Set by can_z_move() if null.
- * * z_move_flags: bitflags used for various checks in both this proc and can_z_move(). See __DEFINES/movement.dm.
- */
-/atom/movable/proc/zMove(dir, turf/target, z_move_flags = ZMOVE_FLIGHT_FLAGS)
- var/able_to_climb = (!currently_z_moving && (dir == UP) && isliving(src))
- if(!target)
- // We remove feedback if the mover is able to climb to prevent feedback from playing if they end up climbing.
- target = can_z_move(dir, get_turf(src), able_to_climb ? z_move_flags & ~ZMOVE_FEEDBACK : z_move_flags)
- if(!target)
- set_currently_z_moving(FALSE, TRUE)
-
- if(!target && able_to_climb)
- var/obj/climbable = check_zclimb()
- if(!climbable)
- if(z_move_flags & ZMOVE_FEEDBACK)
- // This is kind of stupid, but we do this *again* to properly get feedback
- can_z_move(dir, get_turf(src), z_move_flags)
- return FALSE
-
- return ClimbUp(climbable)
-
- if(!target)
- return FALSE
-
- var/list/moving_movs = get_z_move_affected(z_move_flags)
-
- for(var/atom/movable/movable as anything in moving_movs)
- movable.currently_z_moving = currently_z_moving || CURRENTLY_Z_MOVING_GENERIC
- movable.forceMove(target)
- movable.set_currently_z_moving(FALSE, TRUE)
-
- // This is run after ALL movables have been moved, so pulls don't get broken unless they are actually out of range.
- if(z_move_flags & ZMOVE_CHECK_PULLS)
- for(var/atom/movable/moved_mov as anything in moving_movs)
- if(z_move_flags & ZMOVE_CHECK_PULLEDBY && moved_mov.pulledby && (moved_mov.z != moved_mov.pulledby.z || get_dist(moved_mov, moved_mov.pulledby) > 1))
- moved_mov.pulledby.stop_pulling()
- if(z_move_flags & ZMOVE_CHECK_PULLING)
- moved_mov.check_pulling(TRUE)
- return TRUE
-
-/// Returns a list of movables that should also be affected when src moves through zlevels, and src.
-/atom/movable/proc/get_z_move_affected(z_move_flags)
- . = list(src)
- if(buckled_mobs)
- . |= buckled_mobs
- if(!(z_move_flags & ZMOVE_INCLUDE_PULLED))
- return
- for(var/mob/living/buckled as anything in buckled_mobs)
- if(buckled.pulling)
- . |= buckled.pulling
- if(pulling)
- . |= pulling
-
-/**
- * Checks if the destination turf is elegible for z movement from the start turf to a given direction and returns it if so.
- * Args:
- * * direction: the direction to go, UP or DOWN, only relevant if target is null.
- * * start: Each destination has a starting point on the other end. This is it. Most of the times the location of the source.
- * * z_move_flags: bitflags used for various checks. See __DEFINES/movement.dm.
- * * rider: A living mob in control of the movable. Only non-null when a mob is riding a vehicle through z-levels.
- */
-/atom/movable/proc/can_z_move(direction, turf/start, z_move_flags = ZMOVE_FLIGHT_FLAGS, mob/living/rider)
- if(!start)
- start = get_turf(src)
- if(!start)
- CRASH("Something tried to zMove from nullspace.")
-
- if(!direction)
- CRASH("can_z_move() called with no direction")
-
- if(direction != UP && direction != DOWN)
- CRASH("Tried to zMove on the same Z Level.")
-
- var/turf/destination = get_step_multiz(start, direction)
- if(!destination)
- if(z_move_flags & ZMOVE_FEEDBACK)
- to_chat(rider || src, span_notice("There is nothing of interest in this direction."))
- return FALSE
-
- // Check flight
- if(z_move_flags & ZMOVE_CAN_FLY_CHECKS)
- if(!(movement_type & (FLYING|FLOATING)) && has_gravity(start) && (direction == UP))
- if(z_move_flags & ZMOVE_FEEDBACK)
- if(rider)
- to_chat(rider, span_warning("[src] is is not capable of flight."))
- else
- to_chat(src, span_warning("You stare at [destination]."))
- return FALSE
-
- // Check CanZPass
- if(!(z_move_flags & ZMOVE_IGNORE_OBSTACLES))
- // Check exit
- if(!start.CanZPass(src, direction, z_move_flags))
- if(z_move_flags & ZMOVE_FEEDBACK)
- to_chat(rider || src, span_warning("[start] is in the way."))
- return FALSE
-
- // Check enter
- if(!destination.CanZPass(src, direction, z_move_flags))
- if(z_move_flags & ZMOVE_FEEDBACK)
- to_chat(rider || src, span_warning("You bump against [destination]."))
- return FALSE
-
- // Check destination movable CanPass
- for(var/atom/movable/A as anything in destination)
- if(!A.CanMoveOnto(src, direction))
- if(z_move_flags & ZMOVE_FEEDBACK)
- to_chat(rider || src, span_warning("You are blocked by [A]."))
- return FALSE
-
- // Check if we would fall.
- if(z_move_flags & ZMOVE_FALL_CHECKS)
- if((throwing || (movement_type & (FLYING|FLOATING)) || !has_gravity(start)))
- if(z_move_flags & ZMOVE_FEEDBACK)
- to_chat(rider || src, span_warning("You see nothing to hold onto."))
- return FALSE
-
- return destination //used by some child types checks and zMove()
-
-/// Precipitates a movable (plus whatever buckled to it) to lower z levels if possible and then calls zImpact()
-/atom/movable/proc/zFall(levels = 1, force = FALSE, falling_from_move = FALSE)
- if(QDELETED(src))
- return FALSE
-
- var/direction = DOWN
- if(has_gravity() == NEGATIVE_GRAVITY)
- direction = UP
-
- var/turf/target = direction == UP ? GetAbove(src) : GetBelow(src)
- if(!target)
- return FALSE
-
- if(!CanZFall(get_turf(src), direction))
- set_currently_z_moving(FALSE, TRUE)
- return FALSE
-
- var/isliving = isliving(src)
- if(!isliving && !isobj(src))
- return
-
- if(isliving)
- var/mob/living/falling_living = src
- //relay this mess to whatever the mob is buckled to.
- if(falling_living.buckled)
- return falling_living.buckled.zFall()
-
- if(!falling_from_move && currently_z_moving)
- return
-
- if(!force && !can_z_move(direction, get_turf(src), ZMOVE_FALL_FLAGS))
- set_currently_z_moving(FALSE, TRUE)
- return FALSE
-
- spawn(-1)
- _doZFall(target, levels, get_turf(src))
- return TRUE
-
-/atom/movable/proc/_doZFall(turf/destination, levels, turf/prev_turf)
- PRIVATE_PROC(TRUE)
-
- // So it doesn't trigger other zFall calls. Cleared on zMove.
- set_currently_z_moving(CURRENTLY_Z_FALLING)
-
- zMove(null, destination, ZMOVE_CHECK_PULLEDBY)
- destination.zImpact(src, levels, prev_turf)
-
-/atom/movable/proc/CanZFall(turf/from, direction, anchor_bypass)
- if(anchored && !anchor_bypass)
- return FALSE
-
- if(from)
- for(var/obj/O in from)
- if(O.obj_flags & BLOCK_Z_FALL)
- return FALSE
-
- var/turf/other = direction == UP ? GetAbove(from) : GetBelow(from)
- if(!from.CanZPass(from, direction) || !other?.CanZPass(from, direction)) //Kinda hacky but it does work.
- return FALSE
-
- return TRUE
-
-/// Returns an object we can climb onto
-/atom/movable/proc/check_zclimb()
- var/turf/above = GetAbove(src)
- if(!above?.CanZPass(src, UP))
- return
-
- var/list/all_turfs_above = get_adjacent_open_turfs(above)
-
- //Check directly above first
- . = get_climbable_surface(above)
- if(.)
- return
-
- //Next, try the direction the mob is facing
- . = get_climbable_surface(get_step(above, dir))
- if(.)
- return
-
- for(var/turf/T as turf in all_turfs_above)
- . = get_climbable_surface(T)
- if(.)
- return
-
-/atom/movable/proc/get_climbable_surface(turf/T)
- var/climb_target
- if(!T.Enter(src))
- return
- if(!isopenspaceturf(T) && isfloorturf(T))
- climb_target = T
- else
- for(var/obj/I in T)
- if(I.obj_flags & BLOCK_Z_FALL)
- climb_target = I
- break
-
- if(climb_target)
- return climb_target
-
-/atom/movable/proc/ClimbUp(atom/onto)
- if(!isturf(loc))
- return FALSE
-
- var/turf/above = GetAbove(src)
- var/turf/destination = get_turf(onto)
- if(!above || !above.Adjacent(destination, mover = src))
- return FALSE
-
- if(has_gravity() > 0)
- var/can_overcome
- for(var/atom/A in loc)
- if(HAS_TRAIT(A, TRAIT_CLIMBABLE))
- can_overcome = TRUE
- break;
-
- if(!can_overcome)
- var/list/objects_to_stand_on = list(
- /obj/structure/chair,
- /obj/structure/bed,
- /obj/structure/lattice
- )
- for(var/path in objects_to_stand_on)
- if(locate(path) in loc)
- can_overcome = TRUE
- break;
- if(!can_overcome)
- to_chat(src, span_warning("You cannot reach [onto] from here!"))
- return FALSE
-
- visible_message(
- span_notice("[src] starts climbing onto \the [onto]."),
- span_notice("You start climbing onto \the [onto].")
- )
-
- if(!do_after(src, time = 5 SECONDS, timed_action_flags = DO_PUBLIC, display = image('icons/hud/do_after.dmi', "help")))
- return FALSE
-
- visible_message(
- span_notice("[src] climbs onto \the [onto]."),
- span_notice("You climb onto \the [onto].")
- )
-
- var/oldloc = loc
- setDir(get_dir(above, destination))
- set_currently_z_moving(CURRENTLY_Z_ASCENDING)
- . = zMove(UP, destination, ZMOVE_CHECK_PULLS|ZMOVE_INCAPACITATED_CHECKS)
- if(.)
- playsound(oldloc, 'sound/effects/stairs_step.ogg', 50)
- playsound(destination, 'sound/effects/stairs_step.ogg', 50)
-
/atom/movable/vv_edit_var(var_name, var_value)
var/static/list/banned_edits = list(
NAMEOF_STATIC(src, step_x) = TRUE,
@@ -563,9 +267,6 @@
if(NAMEOF(src, anchored))
set_anchored(var_value)
. = TRUE
- if(NAMEOF(src, pulledby))
- set_pulledby(var_value)
- . = TRUE
if(NAMEOF(src, glide_size))
set_glide_size(var_value)
. = TRUE
@@ -576,104 +277,6 @@
return ..()
-
-/atom/movable/proc/start_pulling(atom/movable/pulled_atom, state, force = move_force, supress_message = FALSE)
- if(QDELETED(pulled_atom))
- return FALSE
- if(!(pulled_atom.can_be_pulled(src, state, force)))
- return FALSE
-
- // If we're pulling something then drop what we're currently pulling and pull this instead.
- if(pulling)
- if(state == 0)
- stop_pulling()
- return FALSE
- // Are we trying to pull something we are already pulling? Then enter grab cycle and end.
- if(pulled_atom == pulling)
- setGrabState(state)
- if(istype(pulled_atom,/mob/living))
- var/mob/living/pulled_mob = pulled_atom
- pulled_mob.grabbedby(src)
- return TRUE
- stop_pulling()
-
- if(pulled_atom.pulledby)
- log_combat(pulled_atom, pulled_atom.pulledby, "pulled from", src)
- pulled_atom.pulledby.stop_pulling() //an object can't be pulled by two mobs at once.
- pulling = pulled_atom
- pulled_atom.set_pulledby(src)
- SEND_SIGNAL(src, COMSIG_ATOM_START_PULL, pulled_atom, state, force)
- setGrabState(state)
- if(ismob(pulled_atom))
- var/mob/pulled_mob = pulled_atom
- log_combat(src, pulled_mob, "grabbed", addition="passive grab")
- if(!supress_message)
- pulled_mob.visible_message(span_warning("[src] grabs [pulled_mob] passively."), \
- span_danger("[src] grabs you passively."))
- return TRUE
-
-/atom/movable/proc/stop_pulling()
- if(!pulling)
- return
- pulling.set_pulledby(null)
- setGrabState(GRAB_PASSIVE)
- var/atom/movable/old_pulling = pulling
- pulling = null
- SEND_SIGNAL(old_pulling, COMSIG_ATOM_NO_LONGER_PULLED, src)
- SEND_SIGNAL(src, COMSIG_ATOM_NO_LONGER_PULLING, old_pulling)
-
-///Reports the event of the change in value of the pulledby variable.
-/atom/movable/proc/set_pulledby(new_pulledby)
- if(new_pulledby == pulledby)
- return FALSE //null signals there was a change, be sure to return FALSE if none happened here.
- . = pulledby
- pulledby = new_pulledby
-
-
-/atom/movable/proc/Move_Pulled(atom/moving_atom)
- if(!pulling)
- return FALSE
- if(pulling.anchored || pulling.move_resist > move_force || !pulling.Adjacent(src, src, pulling))
- stop_pulling()
- return FALSE
- if(isliving(pulling))
- var/mob/living/pulling_mob = pulling
- if(pulling_mob.buckled && pulling_mob.buckled.buckle_prevents_pull) //if they're buckled to something that disallows pulling, prevent it
- stop_pulling()
- return FALSE
- if(moving_atom == loc && pulling.density)
- return FALSE
- var/move_dir = get_dir(pulling.loc, moving_atom)
- if(!Process_Spacemove(move_dir))
- return FALSE
- pulling.Move(get_step(pulling.loc, move_dir), move_dir, glide_size)
- return TRUE
-
-/mob/living/Move_Pulled(atom/moving_atom)
- . = ..()
- if(!. || !isliving(moving_atom))
- return
- var/mob/living/pulled_mob = moving_atom
- set_pull_offsets(pulled_mob, grab_state)
-
-/**
- * Checks if the pulling and pulledby should be stopped because they're out of reach.
- * If z_allowed is TRUE, the z level of the pulling will be ignored.This is to allow things to be dragged up and down stairs.
- */
-/atom/movable/proc/check_pulling(only_pulling = FALSE, z_allowed = FALSE)
- if(pulling)
- if(get_dist(src, pulling) > 1 || (z != pulling.z && !z_allowed))
- stop_pulling()
- else if(!isturf(loc))
- stop_pulling()
- else if(pulling && !isturf(pulling.loc) && pulling.loc != loc) //to be removed once all code that changes an object's loc uses forceMove().
- log_game("DEBUG:[src]'s pull on [pulling] wasn't broken despite [pulling] being in [pulling.loc]. Pull stopped manually.")
- stop_pulling()
- else if(pulling.anchored || pulling.move_resist > move_force)
- stop_pulling()
- if(!only_pulling && pulledby && moving_diagonally != FIRST_DIAG_STEP && (get_dist(src, pulledby) > 1 || z != pulledby.z)) //separated from our puller and not in the middle of a diagonal move.
- pulledby.stop_pulling()
-
/atom/movable/proc/set_glide_size(target = 8)
SEND_SIGNAL(src, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE, target)
glide_size = target
@@ -699,7 +302,7 @@
// Here's where we rewrite how byond handles movement except slightly different
// To be removed on step_ conversion
// All this work to prevent a second bump
-/atom/movable/Move(atom/newloc, direction, glide_size_override = 0)
+/atom/movable/Move(atom/newloc, direction, glide_size_override = 0, z_movement_flags)
. = FALSE
if(!newloc || newloc == loc)
@@ -708,7 +311,7 @@
if(!direction)
direction = get_dir(src, newloc)
- if(set_dir_on_move && dir != direction)
+ if(set_dir_on_move && dir != direction && !(dir & (UP|DOWN)))
setDir(direction)
var/is_multi_tile_object = bound_width > 32 || bound_height > 32
@@ -776,16 +379,21 @@
////////////////////////////////////////
-/atom/movable/Move(atom/newloc, direct, glide_size_override = 0)
+/atom/movable/Move(atom/newloc, direct, glide_size_override = 0, z_movement_flags)
if(QDELING(src))
CRASH("Illegal Move()! on [type]")
- var/atom/movable/pullee = pulling
- var/turf/current_turf = loc
if(!moving_from_pull)
- check_pulling(z_allowed = TRUE)
+ recheck_grabs(z_allowed = TRUE)
+
if(!loc || !newloc)
return FALSE
+
+ if(direct & (UP|DOWN))
+ if(!can_z_move(direct, null, z_movement_flags))
+ return FALSE
+ set_currently_z_moving(ZMOVING_VERTICAL)
+
var/atom/oldloc = loc
//Early override for some cases like diagonal movement
if(glide_size_override && glide_size != glide_size_override)
@@ -851,27 +459,17 @@
if(!loc || (loc == oldloc && oldloc != newloc))
last_move = 0
- set_currently_z_moving(FALSE, TRUE)
+ set_currently_z_moving(FALSE)
return
- if(. && pulling && pulling == pullee && pulling != moving_from_pull) //we were pulling a thing and didn't lose it during our move.
- if(pulling.anchored)
- stop_pulling()
- else
- //puller and pullee more than one tile away or in diagonal position and whatever the pullee is pulling isn't already moving from a pull as it'll most likely result in an infinite loop a la ouroborus.
- if(!pulling.pulling?.moving_from_pull)
- var/pull_dir = get_dir(pulling, src)
- var/target_turf = current_turf
+ if(set_dir_on_move && dir != direct)
+ setDir(direct)
- // Pulling things down/up stairs. zMove() has flags for check_pulling and stop_pulling calls.
- // You may wonder why we're not just forcemoving the pulling movable and regrabbing it.
- // The answer is simple. forcemoving and regrabbing is ugly and breaks conga lines.
- if(pulling.z != z)
- target_turf = get_step(pulling, get_dir(pulling, current_turf))
+ if(. && isliving(src) && currently_z_moving != ZMOVING_LATERAL)
+ var/mob/living/L = src
+ L.handle_grabs_during_movement(oldloc, direct)
- if(target_turf != current_turf || (moving_diagonally != SECOND_DIAG_STEP && ISDIAGONALDIR(pull_dir)) || get_dist(src, pulling) > 1)
- pulling.move_from_pull(src, target_turf, glide_size)
- check_pulling()
+ recheck_grabs(only_pulled = TRUE)
//glide_size strangely enough can change mid movement animation and update correctly while the animation is playing
//This means that if you don't override it late like this, it will just be set back by the movement update that's called when you move turfs.
@@ -880,16 +478,11 @@
last_move = direct
- if(set_dir_on_move && dir != direct)
- setDir(direct)
if(. && has_buckled_mobs() && !handle_buckled_mob_movement(loc, direct, glide_size_override)) //movement failed due to buckled mob(s)
. = FALSE
if(currently_z_moving)
- if(. && loc == newloc)
- zFall(falling_from_move = TRUE)
- else
- set_currently_z_moving(FALSE, TRUE)
+ set_currently_z_moving(FALSE)
/// Called when src is being moved to a target turf because another movable (puller) is moving around.
/atom/movable/proc/move_from_pull(atom/movable/puller, turf/target_turf, glide_size_override)
@@ -1183,14 +776,12 @@
anchored = anchorvalue
SEND_SIGNAL(src, COMSIG_MOVABLE_SET_ANCHORED, anchorvalue)
-/// Sets the currently_z_moving variable to a new value. Used to allow some zMovement sources to have precedence over others.
-/atom/movable/proc/set_currently_z_moving(new_z_moving_value, forced = FALSE)
- if(forced)
- currently_z_moving = new_z_moving_value
- return TRUE
- var/old_z_moving_value = currently_z_moving
- currently_z_moving = max(currently_z_moving, new_z_moving_value)
- return currently_z_moving > old_z_moving_value
+/// Sets the currently_z_moving variable to a new value. Used to temporarily disable some Move() side effects.
+/atom/movable/proc/set_currently_z_moving(new_z_moving_value)
+ if(new_z_moving_value == currently_z_moving)
+ return FALSE
+ currently_z_moving = new_z_moving_value
+ return TRUE
/atom/movable/proc/forceMove(atom/destination)
if(QDELING(src))
@@ -1210,10 +801,6 @@
var/atom/oldloc = loc
var/is_multi_tile = bound_width > world.icon_size || bound_height > world.icon_size
if(destination)
- ///zMove already handles whether a pull from another movable should be broken.
- if(pulledby && !currently_z_moving)
- pulledby.stop_pulling()
-
var/same_loc = oldloc == destination
var/area/old_area = get_area(oldloc)
var/area/destarea = get_area(destination)
@@ -1312,8 +899,19 @@
if(SEND_SIGNAL(src, COMSIG_MOVABLE_SPACEMOVE, movement_dir, continuous_move) & COMSIG_MOVABLE_STOP_SPACEMOVE)
return TRUE
- if(pulledby && (pulledby.pulledby != src || moving_from_pull))
- return TRUE
+ // If we are being pulled by something AND (we are NOT pulling them OR we are moving from a pull), do not drift
+ if(LAZYLEN(grabbed_by))
+ var/can_drift = TRUE
+ if(isliving(src))
+ var/mob/living/L = src
+ for(var/obj/item/hand_item/grab/G in grabbed_by)
+ if(!L.is_grabbing(G.assailant))
+ can_drift = FALSE // Something is grabbing us and we're not grabbing them
+ else
+ can_drift = FALSE
+
+ if(!can_drift || moving_from_pull)
+ return TRUE
if(throwing)
return TRUE
@@ -1373,9 +971,6 @@
if(SEND_SIGNAL(src, COMSIG_MOVABLE_PRE_THROW, args) & COMPONENT_CANCEL_THROW)
return
- if (pulledby)
- pulledby.stop_pulling()
-
//They are moving! Wouldn't it be cool if we calculated their momentum and added it to the throw?
if (thrower && thrower.last_move && thrower.client && thrower.client.move_delay >= world.time + world.tick_lag*2)
var/user_momentum = thrower.cached_multiplicative_slowdown
@@ -1432,8 +1027,9 @@
thrown_thing.diagonal_error = dist_x/2 - dist_y
thrown_thing.start_time = world.time
- if(pulledby)
- pulledby.stop_pulling()
+ if(LAZYLEN(grabbed_by))
+ free_from_all_grabs()
+
if (quickstart && (throwing || SSthrowing.state == SS_RUNNING)) //Avoid stack overflow edgecases.
quickstart = FALSE
throwing = thrown_thing
@@ -1638,44 +1234,6 @@
/atom/movable/proc/get_cell()
return
-/atom/movable/proc/can_be_pulled(user, grab_state, force)
- if(src == user || !isturf(loc))
- return FALSE
- if(SEND_SIGNAL(src, COMSIG_ATOM_CAN_BE_PULLED, user) & COMSIG_ATOM_CANT_PULL)
- return FALSE
- if(anchored || throwing)
- return FALSE
- if(force < (move_resist * MOVE_FORCE_PULL_RATIO))
- return FALSE
- return TRUE
-
-/**
- * Updates the grab state of the movable
- *
- * This exists to act as a hook for behaviour
- */
-/atom/movable/proc/setGrabState(newstate)
- if(newstate == grab_state)
- return
- SEND_SIGNAL(src, COMSIG_MOVABLE_SET_GRAB_STATE, newstate)
- . = grab_state
- grab_state = newstate
- switch(grab_state) // Current state.
- if(GRAB_PASSIVE)
- REMOVE_TRAIT(pulling, TRAIT_IMMOBILIZED, CHOKEHOLD_TRAIT)
- REMOVE_TRAIT(pulling, TRAIT_HANDS_BLOCKED, CHOKEHOLD_TRAIT)
- if(. >= GRAB_NECK) // Previous state was a a neck-grab or higher.
- REMOVE_TRAIT(pulling, TRAIT_FLOORED, CHOKEHOLD_TRAIT)
- if(GRAB_AGGRESSIVE)
- if(. >= GRAB_NECK) // Grab got downgraded.
- REMOVE_TRAIT(pulling, TRAIT_FLOORED, CHOKEHOLD_TRAIT)
- else // Grab got upgraded from a passive one.
- ADD_TRAIT(pulling, TRAIT_IMMOBILIZED, CHOKEHOLD_TRAIT)
- ADD_TRAIT(pulling, TRAIT_HANDS_BLOCKED, CHOKEHOLD_TRAIT)
- if(GRAB_NECK, GRAB_KILL)
- if(. <= GRAB_AGGRESSIVE)
- ADD_TRAIT(pulling, TRAIT_FLOORED, CHOKEHOLD_TRAIT)
-
/**
* Adds the deadchat_plays component to this atom with simple movement commands.
*
@@ -1735,7 +1293,7 @@
var/current_loc = get_turf(src)
var/direction = get_dir(current_loc, destination)
if(loc)
- if(!loc.Exit(src, direction))
+ if(!loc.Exit(src, direction, TRUE))
return FALSE
return destination.Enter(src, TRUE)
diff --git a/code/game/machinery/computer/camera_advanced.dm b/code/game/machinery/computer/camera_advanced.dm
index b8b93da05fa7..052c146469d5 100644
--- a/code/game/machinery/computer/camera_advanced.dm
+++ b/code/game/machinery/computer/camera_advanced.dm
@@ -310,7 +310,7 @@
if(!owner || !isliving(owner))
return
var/mob/camera/ai_eye/remote/remote_eye = owner.remote_control
- if(remote_eye.zMove(UP))
+ if(step(remote_eye, UP))
to_chat(owner, span_notice("You move upwards."))
else
to_chat(owner, span_notice("You couldn't move upwards!"))
@@ -324,7 +324,7 @@
if(!owner || !isliving(owner))
return
var/mob/camera/ai_eye/remote/remote_eye = owner.remote_control
- if(remote_eye.zMove(DOWN))
+ if(step(remote_eye, DOWN))
to_chat(owner, span_notice("You move downwards."))
else
to_chat(owner, span_notice("You couldn't move downwards!"))
diff --git a/code/game/machinery/datanet/test_equipment/test_equipment_wired.dm b/code/game/machinery/datanet/test_equipment/test_equipment_wired.dm
index 48d4f7d4e348..137cb13d9c38 100644
--- a/code/game/machinery/datanet/test_equipment/test_equipment_wired.dm
+++ b/code/game/machinery/datanet/test_equipment/test_equipment_wired.dm
@@ -8,7 +8,7 @@
// This equipment is *incredibly* basic, and there's probably a subtype of machinery that you should use instead.
// I should probably remove this before pushing, but if not, here it is!
-/obj/machinery/test_equipment/wired/CtrlClick(mob/user)
+/obj/machinery/test_equipment/wired/CtrlClick(mob/user, list/params)
. = ..()
reconnect_dataterm()
diff --git a/code/game/machinery/washing_machine.dm b/code/game/machinery/washing_machine.dm
index 6ff1fd03d62d..c5a5c28378f0 100644
--- a/code/game/machinery/washing_machine.dm
+++ b/code/game/machinery/washing_machine.dm
@@ -369,22 +369,30 @@ GLOBAL_LIST_INIT(dye_registry, list(
to_chat(user, span_warning("[src] is busy!"))
return
- if(user.pulling && isliving(user.pulling))
- var/mob/living/L = user.pulling
- if(L.buckled || L.has_buckled_mobs())
- return
- if(state_open)
- if(istype(L, /mob/living/simple_animal/pet))
- L.forceMove(src)
- update_appearance()
- return
-
if(!state_open)
open_machine()
else
state_open = FALSE //close the door
update_appearance()
+/obj/machinery/washing_machine/attack_grab(mob/living/user, atom/movable/victim, obj/item/hand_item/grab/grab, list/params)
+ . = ..()
+ if(busy)
+ to_chat(user, span_warning("[src] is busy!"))
+ return
+
+ if(!isliving(victim))
+ return
+
+ var/mob/living/L = victim
+ if(L.buckled || L.has_buckled_mobs())
+ return
+ if(state_open)
+ if(istype(L, /mob/living/simple_animal/pet))
+ L.forceMove(src)
+ update_appearance()
+ return TRUE
+
/obj/machinery/washing_machine/attack_hand_secondary(mob/user, modifiers)
. = ..()
if(. == SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN)
diff --git a/code/game/objects/buckling.dm b/code/game/objects/buckling.dm
index 7bf7b875e938..dfff619d5f42 100644
--- a/code/game/objects/buckling.dm
+++ b/code/game/objects/buckling.dm
@@ -102,12 +102,12 @@
if(SEND_SIGNAL(src, COMSIG_MOVABLE_PREBUCKLE, M, force, buckle_mob_flags) & COMPONENT_BLOCK_BUCKLE)
return FALSE
- if(M.pulledby)
+ if(LAZYLEN(M.grabbed_by))
if(buckle_prevents_pull)
- M.pulledby.stop_pulling()
- else if(isliving(M.pulledby))
- var/mob/living/L = M.pulledby
- L.reset_pull_offsets(M, TRUE)
+ M.free_from_all_grabs()
+
+ else if(LAZYLEN(grabbed_by))
+ M.reset_pull_offsets(TRUE)
if(anchored)
ADD_TRAIT(M, TRAIT_NO_FLOATING_ANIM, BUCKLED_TRAIT)
@@ -346,7 +346,6 @@
span_notice("You unbuckle yourself from [src]."),\
span_hear("You hear metal clanking."))
add_fingerprint(user)
- if(isliving(M.pulledby))
- var/mob/living/L = M.pulledby
- L.set_pull_offsets(M, L.grab_state)
+
+ update_offsets()
return M
diff --git a/code/game/objects/effects/spiderwebs.dm b/code/game/objects/effects/spiderwebs.dm
index 709127a37596..d81e1d5086cd 100644
--- a/code/game/objects/effects/spiderwebs.dm
+++ b/code/game/objects/effects/spiderwebs.dm
@@ -67,8 +67,9 @@
if(istype(mover, /mob/living/simple_animal/hostile/giant_spider))
return TRUE
else if(isliving(mover))
- if(istype(mover.pulledby, /mob/living/simple_animal/hostile/giant_spider))
- return TRUE
+ for(var/obj/item/hand_item/grab/G in mover.grabbed_by)
+ if(istype(G.assailant, /mob/living/simple_animal/hostile/giant_spider))
+ return TRUE
if(prob(50))
to_chat(mover, span_danger("You get stuck in \the [src] for a moment."))
return FALSE
@@ -95,7 +96,7 @@
if(mover == allowed_mob)
return TRUE
else if(isliving(mover)) //we change the spider to not be able to go through here
- if(mover.pulledby == allowed_mob)
+ if(allowed_mob.is_grabbing(mover))
return TRUE
if(prob(50))
to_chat(mover, span_danger("You get stuck in \the [src] for a moment."))
diff --git a/code/game/objects/items/defib.dm b/code/game/objects/items/defib.dm
index b95c96aa1408..235c97615852 100644
--- a/code/game/objects/items/defib.dm
+++ b/code/game/objects/items/defib.dm
@@ -514,8 +514,7 @@
update_appearance()
/obj/item/shockpaddles/proc/shock_pulling(dmg, mob/H)
- if(isliving(H.pulledby)) //CLEAR!
- var/mob/living/M = H.pulledby
+ for(var/mob/living/M in H.recursively_get_all_grabbers())
if(M.electrocute_act(dmg, H))
M.visible_message(span_danger("[M] is electrocuted by [M.p_their()] contact with [H]!"))
M.emote("scream")
diff --git a/code/game/objects/items/devices/pressureplates.dm b/code/game/objects/items/devices/pressureplates.dm
index c46b42b0c58f..b40e82716182 100644
--- a/code/game/objects/items/devices/pressureplates.dm
+++ b/code/game/objects/items/devices/pressureplates.dm
@@ -74,7 +74,7 @@
sigdev = null
return ..()
-/obj/item/pressure_plate/CtrlClick(mob/user)
+/obj/item/pressure_plate/CtrlClick(mob/user, list/params)
if(protected)
to_chat(user, span_warning("You can't quite seem to turn this pressure plate off..."))
return
diff --git a/code/game/objects/items/devices/radio/radio.dm b/code/game/objects/items/devices/radio/radio.dm
index 102d93618f8a..f2ce888af395 100644
--- a/code/game/objects/items/devices/radio/radio.dm
+++ b/code/game/objects/items/devices/radio/radio.dm
@@ -228,7 +228,7 @@
if(talking_carbon.handcuffed)// If we're handcuffed, we can't press the button
to_chat(talking_carbon, span_warning("You can't use the radio while handcuffed!"))
return ITALICS | REDUCE_RANGE
- if(talking_carbon.pulledby?.grab_state)
+ if(HAS_TRAIT(talking_carbon, TRAIT_AGGRESSIVE_GRAB))
to_chat(talking_carbon, span_warning("You can't use the radio while aggressively grabbed!"))
return ITALICS | REDUCE_RANGE
diff --git a/code/game/objects/items/hand_items.dm b/code/game/objects/items/hand_items.dm
index f24aa5103144..120eb826ab88 100644
--- a/code/game/objects/items/hand_items.dm
+++ b/code/game/objects/items/hand_items.dm
@@ -123,7 +123,8 @@
to_chat(user, span_warning("You can't bring yourself to noogie [target]! You don't want to risk harming anyone..."))
return
- if(!(target?.get_bodypart(BODY_ZONE_HEAD)) || user.pulling != target || user.grab_state < GRAB_AGGRESSIVE || HAS_TRAIT(user, TRAIT_EXHAUSTED))
+ var/obj/item/hand_item/grab/G = user.is_grabbing(target)
+ if(!(target?.get_bodypart(BODY_ZONE_HEAD)) || !G || !(G.current_grab.damage_stage >= GRAB_AGGRESSIVE)|| HAS_TRAIT(user, TRAIT_EXHAUSTED))
return FALSE
// [user] gives [target] a [prefix_desc] noogie[affix_desc]!
@@ -153,7 +154,7 @@
/// The actual meat and bones of the noogie'ing
/obj/item/hand_item/noogie/proc/noogie_loop(mob/living/carbon/human/user, mob/living/carbon/target, iteration)
- if(!(target?.get_bodypart(BODY_ZONE_HEAD)) || user.pulling != target)
+ if(!(target?.get_bodypart(BODY_ZONE_HEAD)) || !user.is_grabbing(target))
return FALSE
if(HAS_TRAIT(user, TRAIT_EXHAUSTED))
diff --git a/code/game/objects/items/his_grace.dm b/code/game/objects/items/his_grace.dm
index feccb047f5d6..e2651dad4373 100644
--- a/code/game/objects/items/his_grace.dm
+++ b/code/game/objects/items/his_grace.dm
@@ -64,7 +64,7 @@
else
..()
-/obj/item/his_grace/CtrlClick(mob/user) //you can't pull his grace
+/obj/item/his_grace/CtrlClick(mob/user, list/params) //you can't pull his grace
return
/obj/item/his_grace/examine(mob/user)
diff --git a/code/game/objects/items/stacks/stack.dm b/code/game/objects/items/stacks/stack.dm
index 029f9bb9825d..aea74a3ee574 100644
--- a/code/game/objects/items/stacks/stack.dm
+++ b/code/game/objects/items/stacks/stack.dm
@@ -490,8 +490,12 @@
transfer = min(transfer, round((target_stack.source.max_energy - target_stack.source.energy) / target_stack.cost))
else
transfer = min(transfer, (limit ? limit : target_stack.max_amount) - target_stack.amount)
- if(pulledby)
- pulledby.start_pulling(target_stack)
+ if(LAZYLEN(grabbed_by))
+ for(var/obj/item/hand_item/grab/G in grabbed_by)
+ var/mob/living/grabber = G.assailant
+ qdel(G)
+ grabber.try_make_grab(target_stack)
+
target_stack.copy_evidences(src)
use(transfer, transfer = TRUE, check = FALSE)
target_stack.add(transfer)
diff --git a/code/game/objects/items/storage/bags.dm b/code/game/objects/items/storage/bags.dm
index bd8330ea4744..4b2a2bdbb5dc 100644
--- a/code/game/objects/items/storage/bags.dm
+++ b/code/game/objects/items/storage/bags.dm
@@ -141,8 +141,9 @@
if(!isturf(tile))
return
- if(istype(user.pulling, /obj/structure/ore_box))
- box = user.pulling
+ var/obj/item/hand_item/grab/G = user.get_active_held_item()
+ if(isgrab(G) && istype(G.affecting, /obj/structure/ore_box))
+ box = G.affecting
if(atom_storage)
for(var/thing in tile)
@@ -233,7 +234,7 @@
. = ..()
. += span_notice("Ctrl-click to activate seed extraction.")
-/obj/item/storage/bag/plants/portaseeder/CtrlClick(mob/user)
+/obj/item/storage/bag/plants/portaseeder/CtrlClick(mob/user, list/params)
if(user.incapacitated())
return
for(var/obj/item/plant in contents)
diff --git a/code/game/objects/items/theft_tools.dm b/code/game/objects/items/theft_tools.dm
index dfe6df5024d4..5a286cd37ca8 100644
--- a/code/game/objects/items/theft_tools.dm
+++ b/code/game/objects/items/theft_tools.dm
@@ -169,7 +169,8 @@
return
-/obj/item/nuke_core/supermatter_sliver/can_be_pulled(user) // no drag memes
+/obj/item/nuke_core/supermatter_sliver/can_be_grabbed(mob/living/grabber, target_zone, force)
+ // no drag memes
return FALSE
/obj/item/nuke_core/supermatter_sliver/attackby(obj/item/W, mob/living/user, params)
diff --git a/code/game/objects/structures.dm b/code/game/objects/structures.dm
index 5732aac3ed14..ebb02c17b238 100644
--- a/code/game/objects/structures.dm
+++ b/code/game/objects/structures.dm
@@ -96,3 +96,35 @@
L.Paralyze(10 SECONDS)
visible_message(span_warning("[src] slams into [highest] from above!"))
+
+/obj/structure/attack_grab(mob/living/user, atom/movable/victim, obj/item/hand_item/grab/grab, list/params)
+ . = ..()
+ if(!user.combat_mode)
+ return
+ if(!grab.target_zone == BODY_ZONE_HEAD)
+ return
+ if (!grab.current_grab.enable_violent_interactions)
+ to_chat(user, span_warning("You need a better grip to do that!"))
+ return TRUE
+
+ var/mob/living/affecting_mob = grab.get_affecting_mob()
+ if(!istype(affecting_mob))
+ to_chat(user, span_warning("You need to be grabbing a living creature to do that!"))
+ return TRUE
+
+ // Slam their face against the table.
+ var/blocked = affecting_mob.run_armor_check(BODY_ZONE_HEAD, MELEE)
+ if (prob(30 * ((100-blocked)/100)))
+ affecting_mob.Knockdown(10 SECONDS)
+
+ affecting_mob.apply_damage(30, BRUTE, BODY_ZONE_HEAD, blocked)
+ visible_message(span_danger("[user] slams [affecting_mob]'s face against \the [src]!"))
+ playsound(loc, 'sound/items/trayhit1.ogg', 50, 1)
+
+ take_damage(rand(1,5), BRUTE)
+ for(var/obj/item/shard/S in loc)
+ if(prob(50))
+ affecting_mob.visible_message(span_danger("\The [S] slices into [affecting_mob]'s face!"), span_danger("\The [S] slices into your face!"))
+ S.melee_attack_chain(user, victim, params)
+ qdel(grab)
+ return TRUE
diff --git a/code/game/objects/structures/crates_lockers/closets.dm b/code/game/objects/structures/crates_lockers/closets.dm
index e837fe600101..d1672fb5462a 100644
--- a/code/game/objects/structures/crates_lockers/closets.dm
+++ b/code/game/objects/structures/crates_lockers/closets.dm
@@ -212,7 +212,8 @@
if(welded || locked)
return FALSE
if(strong_grab)
- to_chat(user, span_danger("[pulledby] has an incredibly strong grip on [src], preventing it from opening."))
+ var/obj/item/hand_item/grab/G = grabbed_by[1]
+ to_chat(user, span_danger("[G.assailant] has an incredibly strong grip on [src], preventing it from opening."))
return FALSE
var/turf/T = get_turf(src)
for(var/mob/living/L in T)
@@ -309,7 +310,7 @@
for(var/mob/living/M in contents)
if(++mobs_stored >= mob_storage_capacity)
return FALSE
- L.stop_pulling()
+ L.release_all_grabs()
else if(istype(AM, /obj/structure/closet))
return FALSE
diff --git a/code/game/objects/structures/ladders.dm b/code/game/objects/structures/ladders.dm
index cd5d558f2317..12db214d0175 100644
--- a/code/game/objects/structures/ladders.dm
+++ b/code/game/objects/structures/ladders.dm
@@ -5,7 +5,7 @@
icon = 'icons/obj/structures.dmi'
icon_state = "ladder11"
anchored = TRUE
- obj_flags = CAN_BE_HIT | BLOCK_Z_OUT_DOWN
+ obj_flags = CAN_BE_HIT
var/obj/structure/ladder/down //the ladder below this one
var/obj/structure/ladder/up //the ladder above this one
var/crafted = FALSE
@@ -89,7 +89,7 @@
if(!do_after(user, src, travel_time, DO_PUBLIC))
return
- if(!user.zMove(target = target, z_move_flags = ZMOVE_CHECK_PULLEDBY|ZMOVE_ALLOW_BUCKLED|ZMOVE_INCLUDE_PULLED|ZMOVE_IGNORE_OBSTACLES))
+ if(!zstep(user, going_up ? UP : DOWN, ZMOVE_INCAPACITATED_CHECKS))
return
if(!is_ghost)
diff --git a/code/game/objects/structures/plasticflaps.dm b/code/game/objects/structures/plasticflaps.dm
index f446c0dbf474..273dc249229e 100644
--- a/code/game/objects/structures/plasticflaps.dm
+++ b/code/game/objects/structures/plasticflaps.dm
@@ -73,8 +73,12 @@
if(!ventcrawler && living_caller.mob_size != MOB_SIZE_TINY)
return FALSE
- if(caller?.pulling)
- return CanAStarPass(ID, to_dir, caller.pulling, no_id = no_id)
+ if(isliving(caller))
+ var/mob/living/L = caller
+ var/list/grabs = L.get_active_grabs()
+ for(var/obj/item/hand_item/grab/G in grabs)
+ if(!CanAStarPass(ID, to_dir, G.affecting, no_id = no_id))
+ return FALSE
return TRUE //diseases, stings, etc can pass
diff --git a/code/game/objects/structures/railings.dm b/code/game/objects/structures/railings.dm
index 4cd10b042c51..04df2e167525 100644
--- a/code/game/objects/structures/railings.dm
+++ b/code/game/objects/structures/railings.dm
@@ -117,3 +117,27 @@
/obj/structure/railing/proc/check_anchored(checked_anchored)
if(anchored == checked_anchored)
return TRUE
+
+/obj/structure/railing/attack_grab(mob/living/user, atom/movable/victim, obj/item/hand_item/grab/grab, list/params)
+ var/mob/living/L = grab.get_affecting_mob()
+ if(!grab.current_grab.enable_violent_interactions || !isliving(L))
+ return ..()
+
+ if(!Adjacent(L))
+ user.move_grabbed_atoms_towards(get_turf(src))
+ return ..()
+
+ if(user.combat_mode)
+ visible_message(span_danger("[user] slams [L]'s face against \the [src]!"))
+ playsound(loc, 'sound/effects/grillehit.ogg', 50, 1)
+ var/blocked = L.run_armor_check(BODY_ZONE_HEAD, MELEE)
+ if (prob(30 * ((100 - blocked)/100)))
+ L.Knockdown(10 SECONDS)
+ L.apply_damage(8, BRUTE, BODY_ZONE_HEAD)
+ else
+ if (get_turf(L) == get_turf(src))
+ L.forceMove(get_step(src, src.dir))
+ else
+ L.forceMove(get_turf(src))
+ L.Knockdown(10 SECONDS)
+ visible_message(span_danger("[user] throws \the [L] over \the [src]."))
diff --git a/code/game/objects/structures/stairs.dm b/code/game/objects/structures/stairs.dm
index 2d5856c23db6..5c350a2ae064 100644
--- a/code/game/objects/structures/stairs.dm
+++ b/code/game/objects/structures/stairs.dm
@@ -55,15 +55,15 @@
if(S)
S.update_appearance()
-/obj/structure/stairs/proc/on_exit(datum/source, atom/movable/leaving, direction)
+/obj/structure/stairs/proc/on_exit(datum/source, atom/movable/leaving, direction, no_side_effects)
SIGNAL_HANDLER
if(leaving == src)
return //Let's not block ourselves.
if(!isobserver(leaving) && isTerminator() && direction == dir)
- leaving.set_currently_z_moving(CURRENTLY_Z_ASCENDING)
- INVOKE_ASYNC(src, PROC_REF(stair_ascend), leaving)
+ if(!no_side_effects)
+ INVOKE_ASYNC(src, PROC_REF(stair_ascend), leaving)
leaving.Bump(src)
return COMPONENT_ATOM_BLOCK_EXIT
@@ -91,19 +91,19 @@
to_chat(climber, span_warning("Something blocks the path."))
return
- if(!target.Enter(climber))
+ if(!target.Enter(climber, FALSE))
to_chat(climber, span_warning("Something blocks the path."))
return
- climber.forceMove(target)
+ climber.forceMoveWithGroup(target, z_movement = ZMOVING_VERTICAL)
+
if(!(climber.throwing || (climber.movement_type & (VENTCRAWLING | FLYING)) || HAS_TRAIT(climber, TRAIT_IMMOBILIZED)))
playsound(my_turf, 'sound/effects/stairs_step.ogg', 50)
playsound(my_turf, 'sound/effects/stairs_step.ogg', 50)
/// Moves anything that's being dragged by src or anything buckled to it to the stairs turf.
- climber.pulling?.move_from_pull(climber, loc, climber.glide_size)
for(var/mob/living/buckled as anything in climber.buckled_mobs)
- buckled.pulling?.move_from_pull(buckled, loc, buckled.glide_size)
+ buckled.handle_grabs_during_movement(my_turf, get_dir(my_turf, target))
/obj/structure/stairs/intercept_zImpact(list/falling_movables, levels = 1)
. = ..()
diff --git a/code/game/objects/structures/transit_tubes/station.dm b/code/game/objects/structures/transit_tubes/station.dm
index 134b381f6867..fb6ea8be1733 100644
--- a/code/game/objects/structures/transit_tubes/station.dm
+++ b/code/game/objects/structures/transit_tubes/station.dm
@@ -64,38 +64,43 @@
if(.)
return
if(!pod_moving)
- if(user.pulling && isliving(user.pulling))
- if(open_status == STATION_TUBE_OPEN)
- var/mob/living/GM = user.pulling
- if(user.grab_state >= GRAB_AGGRESSIVE)
- if(GM.buckled || GM.has_buckled_mobs())
- to_chat(user, span_warning("[GM] is attached to something!"))
- return
- for(var/obj/structure/transit_tube_pod/pod in loc)
- pod.visible_message(span_warning("[user] starts putting [GM] into the [pod]!"))
- if(do_after(user, src, 15))
- if(open_status == STATION_TUBE_OPEN && GM && user.grab_state >= GRAB_AGGRESSIVE && user.pulling == GM && !GM.buckled && !GM.has_buckled_mobs())
- GM.Paralyze(100)
- src.BumpedBy(GM)
- break
- else
- for(var/obj/structure/transit_tube_pod/pod in loc)
- if(!pod.moving && (pod.dir in tube_dirs))
- if(open_status == STATION_TUBE_CLOSED)
- open_animation()
-
- else if(open_status == STATION_TUBE_OPEN)
- if(pod.contents.len && user.loc != pod)
- user.visible_message(span_notice("[user] starts emptying [pod]'s contents onto the floor."), span_notice("You start emptying [pod]'s contents onto the floor..."))
- if(do_after(user, src, 10)) //So it doesn't default to close_animation() on fail
- if(pod && pod.loc == loc)
- for(var/atom/movable/AM in pod)
- AM.forceMove(get_turf(user))
-
- else
- close_animation()
- break
+ for(var/obj/structure/transit_tube_pod/pod in loc)
+ if(!pod.moving && (pod.dir in tube_dirs))
+ if(open_status == STATION_TUBE_CLOSED)
+ open_animation()
+
+ else if(open_status == STATION_TUBE_OPEN)
+ if(pod.contents.len && user.loc != pod)
+ user.visible_message(span_notice("[user] starts emptying [pod]'s contents onto the floor."), span_notice("You start emptying [pod]'s contents onto the floor..."))
+ if(do_after(user, src, 10)) //So it doesn't default to close_animation() on fail
+ if(pod && pod.loc == loc)
+ for(var/atom/movable/AM in pod)
+ AM.forceMove(get_turf(user))
+
+ else
+ close_animation()
+ break
+
+/obj/structure/transit_tube/station/attack_grab(mob/living/user, atom/movable/victim, obj/item/hand_item/grab/grab, list/params)
+ . = ..()
+ if(!isliving(victim) || pod_moving)
+ return
+ if(!(open_status == STATION_TUBE_OPEN))
+ return
+
+ var/mob/living/GM = victim
+ if(grab.current_grab.damage_stage >= GRAB_AGGRESSIVE)
+ if(GM.buckled || GM.has_buckled_mobs())
+ to_chat(user, span_warning("[GM] is attached to something!"))
+ return
+ for(var/obj/structure/transit_tube_pod/pod in loc)
+ pod.visible_message(span_warning("[user] starts putting [GM] into the [pod]!"))
+ if(do_after(user, src, 15))
+ if(open_status == STATION_TUBE_OPEN && GM && grab.current_grab?.damage_stage >= GRAB_AGGRESSIVE && user.is_grabbing(victim) && !GM.buckled && !GM.has_buckled_mobs())
+ GM.Paralyze(100)
+ src.BumpedBy(GM)
+ break
/obj/structure/transit_tube/station/attackby(obj/item/W, mob/user, params)
if(W.tool_behaviour == TOOL_CROWBAR)
diff --git a/code/game/objects/structures/watercloset.dm b/code/game/objects/structures/watercloset.dm
index a3f945e18a62..0ff0ab0aa2e1 100644
--- a/code/game/objects/structures/watercloset.dm
+++ b/code/game/objects/structures/watercloset.dm
@@ -29,39 +29,6 @@
log_combat(user, swirlie, "swirlied (brute)")
swirlie.adjustBruteLoss(5)
- else if(user.pulling && isliving(user.pulling))
- user.changeNext_move(CLICK_CD_MELEE)
- var/mob/living/GM = user.pulling
- if(user.grab_state >= GRAB_AGGRESSIVE)
- if(GM.loc != get_turf(src))
- to_chat(user, span_warning("[GM] needs to be on [src]!"))
- return
- if(!swirlie)
- if(open)
- GM.visible_message(span_danger("[user] starts to give [GM] a swirlie!"), span_userdanger("[user] starts to give you a swirlie..."))
- swirlie = GM
- var/was_alive = (swirlie.stat != DEAD)
- if(do_after(user, src, 3 SECONDS, timed_action_flags = IGNORE_HELD_ITEM))
- GM.visible_message(span_danger("[user] gives [GM] a swirlie!"), span_userdanger("[user] gives you a swirlie!"), span_hear("You hear a toilet flushing."))
- if(iscarbon(GM))
- var/mob/living/carbon/C = GM
- if(!C.internal)
- log_combat(user, C, "swirlied (oxy)")
- C.adjustOxyLoss(5)
- else
- log_combat(user, GM, "swirlied (oxy)")
- GM.adjustOxyLoss(5)
- if(was_alive && swirlie.stat == DEAD && swirlie.client)
- swirlie.client.give_award(/datum/award/achievement/misc/swirlie, swirlie) // just like space high school all over again!
- swirlie = null
- else
- playsound(src.loc, 'sound/effects/bang.ogg', 25, TRUE)
- GM.visible_message(span_danger("[user] slams [GM.name] into [src]!"), span_userdanger("[user] slams you into [src]!"))
- log_combat(user, GM, "toilet slammed")
- GM.adjustBruteLoss(5)
- else
- to_chat(user, span_warning("You need a tighter grip!"))
-
else if(cistern && !open && user.CanReach(src))
if(!contents.len)
to_chat(user, span_notice("The cistern is empty."))
@@ -78,6 +45,46 @@
update_appearance()
+/obj/structure/toilet/attack_grab(mob/living/user, atom/movable/victim, obj/item/hand_item/grab/grab, list/params)
+ . = ..()
+ if(!isliving(victim))
+ return
+
+ user.changeNext_move(CLICK_CD_MELEE)
+ var/mob/living/GM = victim
+ if(grab.current_grab.damage_stage >= GRAB_AGGRESSIVE)
+ if(GM.loc != get_turf(src))
+ to_chat(user, span_warning("[GM] needs to be on [src]!"))
+ return
+
+ if(!swirlie)
+ if(open)
+ GM.visible_message(span_danger("[user] starts to give [GM] a swirlie!"), span_userdanger("[user] starts to give you a swirlie..."))
+ swirlie = GM
+ var/was_alive = (swirlie.stat != DEAD)
+ if(do_after(user, src, 3 SECONDS, timed_action_flags = IGNORE_HELD_ITEM))
+ GM.visible_message(span_danger("[user] gives [GM] a swirlie!"), span_userdanger("[user] gives you a swirlie!"), span_hear("You hear a toilet flushing."))
+ if(iscarbon(GM))
+ var/mob/living/carbon/C = GM
+ if(!C.internal)
+ log_combat(user, C, "swirlied (oxy)")
+ C.adjustOxyLoss(5)
+ else
+ log_combat(user, GM, "swirlied (oxy)")
+ GM.adjustOxyLoss(5)
+ if(was_alive && swirlie.stat == DEAD && swirlie.client)
+ swirlie.client.give_award(/datum/award/achievement/misc/swirlie, swirlie) // just like space high school all over again!
+ swirlie = null
+ return TRUE
+ else
+ playsound(src.loc, 'sound/effects/bang.ogg', 25, TRUE)
+ GM.visible_message(span_danger("[user] slams [GM.name] into [src]!"), span_userdanger("[user] slams you into [src]!"))
+ log_combat(user, GM, "toilet slammed")
+ GM.adjustBruteLoss(5)
+ return TRUE
+ else
+ to_chat(user, span_warning("You need a tighter grip!"))
+
/obj/structure/toilet/update_icon_state()
icon_state = "toilet[open][cistern]"
return ..()
@@ -169,19 +176,8 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/urinal, 32)
. = ..()
if(.)
return
- if(user.pulling && isliving(user.pulling))
- var/mob/living/GM = user.pulling
- if(user.grab_state >= GRAB_AGGRESSIVE)
- if(GM.loc != get_turf(src))
- to_chat(user, span_notice("[GM.name] needs to be on [src]."))
- return
- user.changeNext_move(CLICK_CD_MELEE)
- user.visible_message(span_danger("[user] slams [GM] into [src]!"), span_danger("You slam [GM] into [src]!"))
- GM.adjustBruteLoss(8)
- else
- to_chat(user, span_warning("You need a tighter grip!"))
- else if(exposed)
+ if(exposed)
if(!hiddenitem)
to_chat(user, span_warning("There is nothing in the drain holder!"))
else
@@ -194,6 +190,23 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/urinal, 32)
else
..()
+/obj/structure/urinal/attack_grab(mob/living/user, atom/movable/victim, obj/item/hand_item/grab/grab, list/params)
+ . = ..()
+ if(!isliving(victim))
+ return
+
+ var/mob/living/GM = victim
+
+ if(grab.current_grab.damage_stage >= GRAB_AGGRESSIVE)
+ if(GM.loc != get_turf(src))
+ to_chat(user, span_notice("[GM.name] needs to be on [src]."))
+ return
+ user.changeNext_move(CLICK_CD_MELEE)
+ user.visible_message(span_danger("[user] slams [GM] into [src]!"), span_danger("You slam [GM] into [src]!"))
+ GM.adjustBruteLoss(8)
+ else
+ to_chat(user, span_warning("You need a tighter grip!"))
+
/obj/structure/urinal/attackby(obj/item/I, mob/living/user, params)
if(exposed)
if (hiddenitem)
diff --git a/code/game/objects/structures/window.dm b/code/game/objects/structures/window.dm
index 48233a4d8846..12442a85f256 100644
--- a/code/game/objects/structures/window.dm
+++ b/code/game/objects/structures/window.dm
@@ -435,6 +435,59 @@
/obj/structure/window/GetExplosionBlock()
return reinf && fulltile ? real_explosion_block : 0
+/obj/structure/window/attack_grab(mob/living/user, atom/movable/victim, obj/item/hand_item/grab/grab, list/params)
+ if (!user.combat_mode || !grab.current_grab.enable_violent_interactions)
+ return ..()
+
+ var/mob/living/affecting_mob = grab.get_affecting_mob()
+ if(!istype(affecting_mob))
+ if(isitem(victim))
+ var/obj/item/I = victim
+ I.melee_attack_chain(user, src, params)
+ return TRUE
+
+
+ var/def_zone = deprecise_zone(grab.target_zone)
+ var/obj/item/bodypart/BP = affecting_mob.get_bodypart(def_zone)
+ if(!BP)
+ return
+ var/blocked = affecting_mob.run_armor_check(def_zone, MELEE)
+ if(grab.current_grab.damage_stage < GRAB_NECK)
+ affecting_mob.visible_message(span_danger("[user] bashes [affecting_mob]'s [BP.plaintext_zone] against \the [src]!"))
+ switch(def_zone)
+ if(BODY_ZONE_HEAD, BODY_ZONE_CHEST)
+ if(prob(50 * ((100 - blocked/100))))
+ affecting_mob.Knockdown(4 SECONDS)
+ if(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM)
+ if(prob(50 * ((100 - blocked/100))))
+ var/side = def_zone == BODY_ZONE_L_ARM ? LEFT_HANDS : RIGHT_HANDS
+ var/obj/item/I = affecting_mob.get_held_items_for_side(side)
+ if(I)
+ affecting_mob.dropItemToGround(I)
+ affecting_mob.apply_damage(20, BRUTE, def_zone, blocked)
+ take_damage(10)
+ qdel(grab)
+ else
+ affecting_mob.visible_message(span_danger("[user] crushes [affecting_mob]'s [BP.plaintext_zone] against \the [src]!"))
+ switch(def_zone)
+ if(BODY_ZONE_HEAD, BODY_ZONE_CHEST)
+ affecting_mob.Knockdown(10 SECONDS)
+ if(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM)
+ var/side = def_zone == BODY_ZONE_L_ARM ? LEFT_HANDS : RIGHT_HANDS
+ var/obj/item/I = affecting_mob.get_held_items_for_side(side)
+ if(I)
+ affecting_mob.dropItemToGround(I)
+
+ affecting_mob.apply_damage(20, BRUTE, def_zone, blocked)
+ take_damage(20)
+ qdel(grab)
+
+ var/obj/effect/decal/cleanable/blood/splatter/over_window/splatter = new(src)
+ splatter.transfer_mob_blood_dna(victim)
+ vis_contents += splatter
+ bloodied = TRUE
+ return TRUE
+
/obj/structure/window/spawner/east
dir = EAST
diff --git a/code/game/turfs/closed/wall/misc_walls.dm b/code/game/turfs/closed/wall/misc_walls.dm
index 4d1f430736ed..9b6d7683cb51 100644
--- a/code/game/turfs/closed/wall/misc_walls.dm
+++ b/code/game/turfs/closed/wall/misc_walls.dm
@@ -16,11 +16,11 @@
. = ..()
if(istype(gone, /mob/living/simple_animal/hostile/construct/harvester)) //harvesters can go through cult walls, dragging something with
var/mob/living/simple_animal/hostile/construct/harvester/H = gone
- var/atom/movable/stored_pulling = H.pulling
+ var/atom/movable/stored_pulling = H.get_active_grab()
if(stored_pulling)
stored_pulling.setDir(direction)
stored_pulling.forceMove(src)
- H.start_pulling(stored_pulling, supress_message = TRUE)
+ H.try_make_grab(stored_pulling)
/turf/closed/wall/mineral/cult/artificer
name = "runed stone wall"
diff --git a/code/game/turfs/open/_open.dm b/code/game/turfs/open/_open.dm
index ab15cefc3351..d190e4c0fb7b 100644
--- a/code/game/turfs/open/_open.dm
+++ b/code/game/turfs/open/_open.dm
@@ -207,7 +207,7 @@
else
slipper.Knockdown(knockdown_amount)
slipper.Paralyze(paralyze_amount)
- slipper.stop_pulling()
+ slipper.release_all_grabs()
if(buckled_obj)
buckled_obj.unbuckle_mob(slipper)
diff --git a/code/game/turfs/open/floor/reinforced_floor.dm b/code/game/turfs/open/floor/reinforced_floor.dm
index 92f717979c58..71d98b5aa58e 100644
--- a/code/game/turfs/open/floor/reinforced_floor.dm
+++ b/code/game/turfs/open/floor/reinforced_floor.dm
@@ -88,11 +88,13 @@
/turf/open/floor/engine/attack_paw(mob/user, list/modifiers)
return attack_hand(user, modifiers)
-/turf/open/floor/engine/attack_hand(mob/user, list/modifiers)
+/turf/open/floor/engine/attack_hand(mob/living/user, list/modifiers)
. = ..()
if(.)
return
- user.Move_Pulled(src)
+ if(!isliving(user))
+ return
+ user.move_grabbed_atoms_towards(src)
//air filled floors; used in atmos pressure chambers
diff --git a/code/game/turfs/open/openspace.dm b/code/game/turfs/open/openspace.dm
index 70499f599b6a..75c0ddcc2cce 100644
--- a/code/game/turfs/open/openspace.dm
+++ b/code/game/turfs/open/openspace.dm
@@ -31,28 +31,15 @@
SHOULD_CALL_PARENT(FALSE)
return below.examine(user)
-/**
- * Prepares a moving movable to be precipitated if Move() is successful.
- * This is done in Enter() and not Entered() because there's no easy way to tell
- * if the latter was called by Move() or forceMove() while the former is only called by Move().
- */
-/turf/open/openspace/Enter(atom/movable/movable, atom/oldloc)
- . = ..()
- if(.)
- //higher priority than CURRENTLY_Z_FALLING so the movable doesn't fall on Entered()
- movable.set_currently_z_moving(CURRENTLY_Z_FALLING_FROM_MOVE)
-
///Makes movables fall when forceMove()'d to this turf.
/turf/open/openspace/Entered(atom/movable/movable)
. = ..()
- if(movable.set_currently_z_moving(CURRENTLY_Z_FALLING))
- movable.zFall(falling_from_move = TRUE)
-
-/turf/open/openspace/proc/zfall_if_on_turf(atom/movable/movable)
- if(QDELETED(movable) || movable.loc != src)
- return
movable.zFall()
+/turf/open/openspace/hitby(atom/movable/hitting_atom, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum)
+ . = ..()
+ hitting_atom.zFall()
+
/turf/open/openspace/can_have_cabling()
if(locate(/obj/structure/lattice/catwalk, src))
return TRUE
diff --git a/code/game/turfs/open/space/space.dm b/code/game/turfs/open/space/space.dm
index 21ecdc3ae84f..1230ee74611f 100644
--- a/code/game/turfs/open/space/space.dm
+++ b/code/game/turfs/open/space/space.dm
@@ -123,7 +123,7 @@ GLOBAL_REAL_VAR(space_appearances) = make_space_appearances()
if(!arrived || src != arrived.loc)
return
- if(destination_z && destination_x && destination_y && !arrived.pulledby && !arrived.currently_z_moving)
+ if(destination_z && destination_x && destination_y && !LAZYLEN(arrived.grabbed_by) && !arrived.currently_z_moving)
var/tx = destination_x
var/ty = destination_y
var/turf/DT = locate(tx, ty, destination_z)
@@ -144,13 +144,13 @@ GLOBAL_REAL_VAR(space_appearances) = make_space_appearances()
if(SEND_SIGNAL(arrived, COMSIG_MOVABLE_LATERAL_Z_MOVE) & COMPONENT_BLOCK_MOVEMENT)
return
- arrived.zMove(null, DT, ZMOVE_ALLOW_BUCKLED)
- var/atom/movable/current_pull = arrived.pulling
- while (current_pull)
- var/turf/target_turf = get_step(current_pull.pulledby.loc, REVERSE_DIR(current_pull.pulledby.dir)) || current_pull.pulledby.loc
- current_pull.zMove(null, target_turf, ZMOVE_ALLOW_BUCKLED)
- current_pull = current_pull.pulling
+ arrived.forceMoveWithGroup(DT, ZMOVING_LATERAL)
+ if(isliving(arrived))
+ var/mob/living/L = arrived
+ for(var/obj/item/hand_item/grab/G as anything in L.recursively_get_conga_line())
+ var/turf/pulled_dest = get_step(G.assailant.loc, REVERSE_DIR(G.assailant.dir)) || G.assailant.loc
+ G.affecting.forceMoveWithGroup(pulled_dest, ZMOVING_LATERAL)
/turf/open/space/MakeSlippery(wet_setting, min_wet_time, wet_time_to_add, max_wet_time, permanent)
diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm
index 50207ec1bc75..d441ac2dd2b1 100755
--- a/code/game/turfs/turf.dm
+++ b/code/game/turfs/turf.dm
@@ -201,11 +201,22 @@ GLOBAL_LIST_EMPTY(station_turfs)
/turf/clear_signal_refs()
return
-/turf/attack_hand(mob/user, list/modifiers)
+/turf/attack_hand(mob/living/user, list/modifiers)
. = ..()
if(.)
return
- user.Move_Pulled(src)
+ if(!isliving(user))
+ return
+ user.move_grabbed_atoms_towards(src)
+
+/turf/attack_grab(mob/living/user, atom/movable/victim, obj/item/hand_item/grab/grab, list/params)
+ . = ..()
+ if(.)
+ return
+ if(!isliving(user))
+ return
+ user.move_grabbed_atoms_towards(src)
+
/**
* Check whether the specified turf is blocked by something dense inside it with respect to a specific atom.
@@ -260,7 +271,7 @@ GLOBAL_LIST_EMPTY(station_turfs)
else if(O.obj_flags & BLOCK_Z_OUT_DOWN)
return FALSE
- return direction == UP //can't go below
+ return direction & UP //can't go below
else
if(density) //No fuck off
return FALSE
@@ -276,8 +287,8 @@ GLOBAL_LIST_EMPTY(station_turfs)
///Called each time the target falls down a z level possibly making their trajectory come to a halt. see __DEFINES/movement.dm.
/turf/proc/zImpact(atom/movable/falling, levels = 1, turf/prev_turf)
- var/flags = NONE
- var/list/falling_movables = falling.get_z_move_affected()
+ var/flags = FALL_RETAIN_PULL
+ var/list/falling_movables = falling.get_move_group()
var/list/falling_mob_names
for(var/atom/movable/falling_mob as anything in falling_movables)
@@ -303,20 +314,26 @@ GLOBAL_LIST_EMPTY(station_turfs)
if(!(flags & FALL_INTERCEPTED) && falling.zFall(levels + 1))
return FALSE
- for(var/atom/movable/falling_mob as anything in falling_movables)
+ for(var/atom/movable/falling_movable as anything in falling_movables)
+ var/mob/living/L = falling_movable
+ if(!isliving(L))
+ L = null
if(!(flags & FALL_RETAIN_PULL))
- falling_mob.stop_pulling()
+ L?.release_all_grabs()
if(!(flags & FALL_INTERCEPTED))
- falling_mob.onZImpact(src, levels)
+ falling_movable.onZImpact(src, levels)
#ifndef ZMIMIC_MULTIZ_SPEECH //Multiz speech handles this otherwise
if(!(flags & FALL_NO_MESSAGE))
prev_turf.audible_message(span_hear("You hear something slam into the deck below."))
#endif
- if(falling_mob.pulledby && (falling_mob.z != falling_mob.pulledby.z || get_dist(falling_mob, falling_mob.pulledby) > 1))
- falling_mob.pulledby.stop_pulling()
+ if(L)
+ if(LAZYLEN(L.grabbed_by))
+ for(var/obj/item/hand_item/grab/G in L.grabbed_by)
+ if(L.z != G.assailant.z || get_dist(L, G.assailant) > 1)
+ qdel(G)
return TRUE
/turf/proc/handleRCL(obj/item/rcl/C, mob/user)
@@ -375,7 +392,7 @@ GLOBAL_LIST_EMPTY(station_turfs)
return FALSE
continue
else
- if(!firstbump || ((thing.layer > firstbump.layer || (thing.flags_1 & ON_BORDER_1|BUMP_PRIORITY_1)) && !(firstbump.flags_1 & ON_BORDER_1)))
+ if(!firstbump || ((thing.layer < firstbump.layer || (thing.flags_1 & ON_BORDER_1|BUMP_PRIORITY_1)) && !(firstbump.flags_1 & ON_BORDER_1)))
firstbump = thing
if(QDELETED(mover)) //Mover deleted from Cross/CanPass/Bump, do not proceed.
diff --git a/code/modules/antagonists/changeling/powers/absorb.dm b/code/modules/antagonists/changeling/powers/absorb.dm
index 8ce7d4eb0aa9..83efdc1e59a6 100644
--- a/code/modules/antagonists/changeling/powers/absorb.dm
+++ b/code/modules/antagonists/changeling/powers/absorb.dm
@@ -16,20 +16,21 @@
to_chat(owner, span_warning("We are already absorbing!"))
return
- if(!owner.pulling || !iscarbon(owner.pulling))
+ var/obj/item/hand_item/grab/G = owner.get_active_grab()
+ if(!G)
to_chat(owner, span_warning("We must be grabbing a creature to absorb them!"))
return
- if(owner.grab_state <= GRAB_NECK)
+ if(!G.current_grab.can_absorb)
to_chat(owner, span_warning("We must have a tighter grip to absorb this creature!"))
return
- var/mob/living/carbon/target = owner.pulling
+ var/mob/living/carbon/target = G.affecting
var/datum/antagonist/changeling/changeling = owner.mind.has_antag_datum(/datum/antagonist/changeling)
return changeling.can_absorb_dna(target)
-/datum/action/changeling/absorb_dna/sting_action(mob/owner)
+/datum/action/changeling/absorb_dna/sting_action(mob/living/owner)
var/datum/antagonist/changeling/changeling = owner.mind.has_antag_datum(/datum/antagonist/changeling)
- var/mob/living/carbon/human/target = owner.pulling
+ var/mob/living/carbon/human/target = owner.get_active_grab()?.affecting
is_absorbing = TRUE
if(!attempt_absorb(target))
diff --git a/code/modules/antagonists/changeling/powers/mutations.dm b/code/modules/antagonists/changeling/powers/mutations.dm
index b15ef9498ffd..dc120f31da77 100644
--- a/code/modules/antagonists/changeling/powers/mutations.dm
+++ b/code/modules/antagonists/changeling/powers/mutations.dm
@@ -326,8 +326,8 @@
H.swap_hand()
if(H.get_active_held_item())
return
- C.grabbedby(H)
- C.grippedby(H, instant = TRUE) //instant aggro grab
+
+ H.try_make_grab(C, /datum/grab/normal/aggressive)
/obj/projectile/tentacle/proc/tentacle_stab(mob/living/carbon/human/H, mob/living/carbon/C)
if(H.Adjacent(C))
diff --git a/code/modules/antagonists/cult/cult_items.dm b/code/modules/antagonists/cult/cult_items.dm
index d4b71b770bce..8a7845ea49f0 100644
--- a/code/modules/antagonists/cult/cult_items.dm
+++ b/code/modules/antagonists/cult/cult_items.dm
@@ -166,7 +166,7 @@ Striking a noncultist, however, will tear their flesh."}
else
. += "The sword appears to be quite lifeless."
-/obj/item/cult_bastard/can_be_pulled(user)
+/obj/item/cult_bastard/can_be_grabbed(mob/living/grabber, target_zone, force)
return FALSE
/obj/item/cult_bastard/attack_self(mob/user)
@@ -607,8 +607,9 @@ Striking a noncultist, however, will tear their flesh."}
/obj/item/cult_shift/proc/handle_teleport_grab(turf/T, mob/user)
var/mob/living/carbon/C = user
- if(C.pulling)
- var/atom/movable/pulled = C.pulling
+ var/list/obj/item/hand_item/grab/grabs = C.get_active_grabs()
+ if(length(grabs))
+ var/atom/movable/pulled = grabs[1].affecting
do_teleport(pulled, T, channel = TELEPORT_CHANNEL_CULT)
. = pulled
@@ -636,7 +637,7 @@ Striking a noncultist, however, will tear their flesh."}
var/atom/movable/pulled = handle_teleport_grab(destination, C)
if(do_teleport(C, destination, channel = TELEPORT_CHANNEL_CULT))
if(pulled)
- C.start_pulling(pulled) //forcemove resets pulls, so we need to re-pull
+ C.try_make_grab(pulled) //forcemove resets pulls, so we need to re-pull
new /obj/effect/temp_visual/dir_setting/cult/phase(destination, C.dir)
playsound(destination, 'sound/effects/phasein.ogg', 25, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
playsound(destination, SFX_SPARKS, 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
diff --git a/code/modules/antagonists/cult/runes.dm b/code/modules/antagonists/cult/runes.dm
index 892d8a408090..1cb5eeb90353 100644
--- a/code/modules/antagonists/cult/runes.dm
+++ b/code/modules/antagonists/cult/runes.dm
@@ -702,11 +702,19 @@ structure_check() searches for nearby cultist structures required for the invoca
fail_invoke()
log_game("Summon Cultist rune failed - target died")
return
- if(cultist_to_summon.pulledby || cultist_to_summon.buckled)
- to_chat(user, "[cultist_to_summon] is being held in place!")
- fail_invoke()
- log_game("Summon Cultist rune failed - target restrained")
- return
+ if(LAZYLEN(cultist_to_summon.grabbed_by) || cultist_to_summon.buckled)
+ var/grab_check = 0
+ if(!cultist_to_summon.buckled)
+ for(var/obj/item/hand_item/grab/G in cultist_to_summon.grabbed_by)
+ if(G.current_grab.stop_move)
+ grab_check++
+
+ if(grab_check == 0)
+ to_chat(user, "[cultist_to_summon] is being held in place!")
+ fail_invoke()
+ log_game("Summon Cultist rune failed - target restrained")
+ return
+
if(!IS_CULTIST(cultist_to_summon))
to_chat(user, "[cultist_to_summon] is not a follower of the Geometer!")
fail_invoke()
diff --git a/code/modules/antagonists/nukeop/equipment/nuclearbomb.dm b/code/modules/antagonists/nukeop/equipment/nuclearbomb.dm
index 27f372762cb0..e57a962d4644 100644
--- a/code/modules/antagonists/nukeop/equipment/nuclearbomb.dm
+++ b/code/modules/antagonists/nukeop/equipment/nuclearbomb.dm
@@ -740,7 +740,7 @@ This is here to make the tiles around the station mininuke change when it's arme
if (last_secured_location == get_turf(src))
return FALSE
- var/mob/holder = pulledby || get(src, /mob)
+ var/mob/holder = get(src, /mob)
if (isnull(holder?.client))
return FALSE
diff --git a/code/modules/aquarium/aquarium.dm b/code/modules/aquarium/aquarium.dm
index fff606f84db9..3f6c8f05b9b3 100644
--- a/code/modules/aquarium/aquarium.dm
+++ b/code/modules/aquarium/aquarium.dm
@@ -142,31 +142,36 @@
return NONE
/obj/structure/aquarium/interact(mob/user)
- if(!broken && user.pulling && isliving(user.pulling))
- var/mob/living/living_pulled = user.pulling
- var/datum/component/aquarium_content/content_component = living_pulled.GetComponent(/datum/component/aquarium_content)
- if(content_component && content_component.is_ready_to_insert(src))
- try_to_put_mob_in(user)
- else if(panel_open)
+ if(panel_open)
. = ..() //call base ui_interact
+/obj/structure/aquarium/attack_grab(mob/living/user, atom/movable/victim, obj/item/hand_item/grab/grab, list/params)
+ . = ..()
+ if(broken || !isliving(victim))
+ return
+
+ var/mob/living/living_pulled = victim
+ var/datum/component/aquarium_content/content_component = living_pulled.GetComponent(/datum/component/aquarium_content)
+ if(content_component && content_component.is_ready_to_insert(src))
+ try_to_put_mob_in(user, grab)
+
/// Tries to put mob pulled by the user in the aquarium after a delay
-/obj/structure/aquarium/proc/try_to_put_mob_in(mob/user)
- if(user.pulling && isliving(user.pulling))
- var/mob/living/living_pulled = user.pulling
- if(living_pulled.buckled || living_pulled.has_buckled_mobs())
- to_chat(user, span_warning("[living_pulled] is attached to something!"))
+/obj/structure/aquarium/proc/try_to_put_mob_in(mob/living/user, obj/item/hand_item/grab/G)
+ var/mob/living/living_pulled = G.affecting
+ if(living_pulled.buckled || living_pulled.has_buckled_mobs())
+ to_chat(user, span_warning("[living_pulled] is attached to something!"))
+ return
+ user.visible_message(span_danger("[user] starts to put [living_pulled] into [src]!"))
+
+ if(do_after(user, src, 10 SECONDS))
+ if(QDELETED(living_pulled) || !user.is_grabbing(living_pulled) || living_pulled.buckled || living_pulled.has_buckled_mobs())
return
- user.visible_message(span_danger("[user] starts to put [living_pulled] into [src]!"))
- if(do_after(user, src, 10 SECONDS))
- if(QDELETED(living_pulled) || user.pulling != living_pulled || living_pulled.buckled || living_pulled.has_buckled_mobs())
- return
- var/datum/component/aquarium_content/content_component = living_pulled.GetComponent(/datum/component/aquarium_content)
- if(content_component || content_component.is_ready_to_insert(src))
- return
- user.visible_message(span_danger("[user] stuffs [living_pulled] into [src]!"))
- living_pulled.forceMove(src)
- update_appearance()
+ var/datum/component/aquarium_content/content_component = living_pulled.GetComponent(/datum/component/aquarium_content)
+ if(content_component || content_component.is_ready_to_insert(src))
+ return
+ user.visible_message(span_danger("[user] stuffs [living_pulled] into [src]!"))
+ living_pulled.forceMove(src)
+ update_appearance()
/obj/structure/aquarium/ui_data(mob/user)
. = ..()
diff --git a/code/modules/atmospherics/ZAS/Airflow.dm b/code/modules/atmospherics/ZAS/Airflow.dm
index 9accdb2b38aa..b97dbf3a3798 100644
--- a/code/modules/atmospherics/ZAS/Airflow.dm
+++ b/code/modules/atmospherics/ZAS/Airflow.dm
@@ -36,8 +36,6 @@ This entire system is an absolute mess.
/mob/living/airflow_stun(delta_p)
if(stat == 2)
return FALSE
- if(pulledby || pulling)
- return FALSE
if(last_airflow_stun > world.time - zas_settings.airflow_stun_cooldown)
return FALSE
if(!(status_flags & CANSTUN) && !(status_flags & CANKNOCKDOWN))
diff --git a/code/modules/atmospherics/machinery/components/binary_devices/passive_gate.dm b/code/modules/atmospherics/machinery/components/binary_devices/passive_gate.dm
index a52c80e0f49e..4852cc3cdae1 100644
--- a/code/modules/atmospherics/machinery/components/binary_devices/passive_gate.dm
+++ b/code/modules/atmospherics/machinery/components/binary_devices/passive_gate.dm
@@ -26,7 +26,7 @@ Passive gate is similar to the regular pump except:
///Stores the radio connection
var/datum/radio_frequency/radio_connection
-/obj/machinery/atmospherics/components/binary/passive_gate/CtrlClick(mob/user)
+/obj/machinery/atmospherics/components/binary/passive_gate/CtrlClick(mob/user, list/params)
if(can_interact(user))
on = !on
investigate_log("was turned [on ? "on" : "off"] by [key_name(user)]", INVESTIGATE_ATMOS)
diff --git a/code/modules/atmospherics/machinery/components/binary_devices/pressure_valve.dm b/code/modules/atmospherics/machinery/components/binary_devices/pressure_valve.dm
index 4b8d32a708f7..e091fc3d8059 100644
--- a/code/modules/atmospherics/machinery/components/binary_devices/pressure_valve.dm
+++ b/code/modules/atmospherics/machinery/components/binary_devices/pressure_valve.dm
@@ -23,7 +23,7 @@
///Which side is the valve regulating?
var/regulate_mode = REGULATE_OUTPUT
-/obj/machinery/atmospherics/components/binary/pressure_valve/CtrlClick(mob/user)
+/obj/machinery/atmospherics/components/binary/pressure_valve/CtrlClick(mob/user, list/params)
if(can_interact(user))
on = !on
investigate_log("was turned [on ? "on" : "off"] by [key_name(user)]", INVESTIGATE_ATMOS)
diff --git a/code/modules/atmospherics/machinery/components/binary_devices/pump.dm b/code/modules/atmospherics/machinery/components/binary_devices/pump.dm
index cad775e3b114..27d95f7d7e17 100644
--- a/code/modules/atmospherics/machinery/components/binary_devices/pump.dm
+++ b/code/modules/atmospherics/machinery/components/binary_devices/pump.dm
@@ -39,7 +39,7 @@
/obj/item/circuit_component/atmos_pump,
))
-/obj/machinery/atmospherics/components/binary/pump/CtrlClick(mob/user)
+/obj/machinery/atmospherics/components/binary/pump/CtrlClick(mob/user, list/params)
if(can_interact(user))
set_on(!on)
investigate_log("was turned [on ? "on" : "off"] by [key_name(user)]", INVESTIGATE_ATMOS)
diff --git a/code/modules/atmospherics/machinery/components/binary_devices/temperature_gate.dm b/code/modules/atmospherics/machinery/components/binary_devices/temperature_gate.dm
index 98ef842b0e8e..bebadfd3c98a 100644
--- a/code/modules/atmospherics/machinery/components/binary_devices/temperature_gate.dm
+++ b/code/modules/atmospherics/machinery/components/binary_devices/temperature_gate.dm
@@ -17,7 +17,7 @@
///Check if the gas is moving from one pipenet to the other
var/is_gas_flowing = FALSE
-/obj/machinery/atmospherics/components/binary/temperature_gate/CtrlClick(mob/user)
+/obj/machinery/atmospherics/components/binary/temperature_gate/CtrlClick(mob/user, list/params)
if(can_interact(user))
on = !on
investigate_log("was turned [on ? "on" : "off"] by [key_name(user)]", INVESTIGATE_ATMOS)
diff --git a/code/modules/atmospherics/machinery/components/binary_devices/temperature_pump.dm b/code/modules/atmospherics/machinery/components/binary_devices/temperature_pump.dm
index 425ed4b25bce..dc570ffcb1fd 100644
--- a/code/modules/atmospherics/machinery/components/binary_devices/temperature_pump.dm
+++ b/code/modules/atmospherics/machinery/components/binary_devices/temperature_pump.dm
@@ -12,7 +12,7 @@
///Maximum allowed transfer percentage
var/max_heat_transfer_rate = 100
-/obj/machinery/atmospherics/components/binary/temperature_pump/CtrlClick(mob/user)
+/obj/machinery/atmospherics/components/binary/temperature_pump/CtrlClick(mob/user, list/params)
if(can_interact(user))
on = !on
investigate_log("was turned [on ? "on" : "off"] by [key_name(user)]", INVESTIGATE_ATMOS)
diff --git a/code/modules/atmospherics/machinery/components/binary_devices/volume_pump.dm b/code/modules/atmospherics/machinery/components/binary_devices/volume_pump.dm
index e0c827d10dc7..1bab358d3aeb 100644
--- a/code/modules/atmospherics/machinery/components/binary_devices/volume_pump.dm
+++ b/code/modules/atmospherics/machinery/components/binary_devices/volume_pump.dm
@@ -39,7 +39,7 @@
/obj/item/circuit_component/atmos_volume_pump,
))
-/obj/machinery/atmospherics/components/binary/volume_pump/CtrlClick(mob/user)
+/obj/machinery/atmospherics/components/binary/volume_pump/CtrlClick(mob/user, list/params)
if(can_interact(user))
set_on(!on)
investigate_log("was turned [on ? "on" : "off"] by [key_name(user)]", INVESTIGATE_ATMOS)
diff --git a/code/modules/atmospherics/machinery/components/trinary_devices/filter.dm b/code/modules/atmospherics/machinery/components/trinary_devices/filter.dm
index 73b1a8d6f765..41267ae13e75 100644
--- a/code/modules/atmospherics/machinery/components/trinary_devices/filter.dm
+++ b/code/modules/atmospherics/machinery/components/trinary_devices/filter.dm
@@ -22,7 +22,7 @@
//Last power draw, for the progress bar in the UI
var/last_power_draw = 0
-/obj/machinery/atmospherics/components/trinary/filter/CtrlClick(mob/user)
+/obj/machinery/atmospherics/components/trinary/filter/CtrlClick(mob/user, list/params)
if(can_interact(user))
on = !on
investigate_log("was turned [on ? "on" : "off"] by [key_name(user)]", INVESTIGATE_ATMOS)
diff --git a/code/modules/atmospherics/machinery/components/trinary_devices/mixer.dm b/code/modules/atmospherics/machinery/components/trinary_devices/mixer.dm
index 80fb796f509e..8ad21db4f41e 100644
--- a/code/modules/atmospherics/machinery/components/trinary_devices/mixer.dm
+++ b/code/modules/atmospherics/machinery/components/trinary_devices/mixer.dm
@@ -22,7 +22,7 @@
var/last_power_draw = 0
-/obj/machinery/atmospherics/components/trinary/mixer/CtrlClick(mob/user)
+/obj/machinery/atmospherics/components/trinary/mixer/CtrlClick(mob/user, list/params)
if(can_interact(user))
on = !on
investigate_log("was turned [on ? "on" : "off"] by [key_name(user)]", INVESTIGATE_ATMOS)
diff --git a/code/modules/atmospherics/machinery/components/unary_devices/cryo.dm b/code/modules/atmospherics/machinery/components/unary_devices/cryo.dm
index c1cc24f86f28..5c52a6a8ac5d 100644
--- a/code/modules/atmospherics/machinery/components/unary_devices/cryo.dm
+++ b/code/modules/atmospherics/machinery/components/unary_devices/cryo.dm
@@ -459,7 +459,7 @@ GLOBAL_VAR_INIT(cryo_overlay_cover_off, mutable_appearance('icons/obj/cryogenics
/obj/machinery/atmospherics/components/unary/cryo_cell/can_interact(mob/user)
return ..() && user.loc != src
-/obj/machinery/atmospherics/components/unary/cryo_cell/CtrlClick(mob/user)
+/obj/machinery/atmospherics/components/unary/cryo_cell/CtrlClick(mob/user, list/params)
if(can_interact(user) && !state_open)
set_on(!on)
return ..()
diff --git a/code/modules/atmospherics/machinery/components/unary_devices/outlet_injector.dm b/code/modules/atmospherics/machinery/components/unary_devices/outlet_injector.dm
index 64f30e857671..c2f6056f1700 100644
--- a/code/modules/atmospherics/machinery/components/unary_devices/outlet_injector.dm
+++ b/code/modules/atmospherics/machinery/components/unary_devices/outlet_injector.dm
@@ -20,7 +20,7 @@
///Rate of operation of the device
var/volume_rate = 50
-/obj/machinery/atmospherics/components/unary/outlet_injector/CtrlClick(mob/user)
+/obj/machinery/atmospherics/components/unary/outlet_injector/CtrlClick(mob/user, list/params)
if(can_interact(user))
on = !on
investigate_log("was turned [on ? "on" : "off"] by [key_name(user)]", INVESTIGATE_ATMOS)
diff --git a/code/modules/awaymissions/signpost.dm b/code/modules/awaymissions/signpost.dm
index ea0d22ac39f6..12e2e832e510 100644
--- a/code/modules/awaymissions/signpost.dm
+++ b/code/modules/awaymissions/signpost.dm
@@ -20,12 +20,19 @@
var/turf/T = find_safe_turf(zlevels=zlevels)
if(T)
- var/atom/movable/AM = user.pulling
- if(AM)
- AM.forceMove(T)
+ var/atom/movable/preserve_grab
+ if(isliving(user))
+ var/mob/living/L = user
+ var/list/grabbing= L.get_all_grabbed_movables()
+ for(var/atom/movable/AM as anything in grabbing)
+ preserve_grab ||= AM
+ AM.forceMove(T)
+
user.forceMove(T)
- if(AM)
- user.start_pulling(AM)
+ if(preserve_grab && isliving(user))
+ var/mob/living/L = user
+ L.try_make_grab(preserve_grab)
+
to_chat(user, span_notice("You blink and find yourself in [get_area_name(T)]."))
else
to_chat(user, "Nothing happens. You feel that this is a bad sign.")
diff --git a/code/modules/cargo/supplypod.dm b/code/modules/cargo/supplypod.dm
index 7d337767d90c..0404781ad769 100644
--- a/code/modules/cargo/supplypod.dm
+++ b/code/modules/cargo/supplypod.dm
@@ -369,7 +369,7 @@
var/mob/living/mob_to_insert = to_insert
if(mob_to_insert.anchored || mob_to_insert.incorporeal_move)
return FALSE
- mob_to_insert.stop_pulling()
+ mob_to_insert.release_all_grabs()
else if(isobj(to_insert))
var/obj/obj_to_insert = to_insert
diff --git a/code/modules/credits_roll/episode_name.dm b/code/modules/credits_roll/episode_name.dm
index 8e312c961cff..44023c41332f 100644
--- a/code/modules/credits_roll/episode_name.dm
+++ b/code/modules/credits_roll/episode_name.dm
@@ -318,7 +318,7 @@
var/all_braindamaged = TRUE
for(var/mob/living/carbon/human/H as anything in SSticker.popcount["human_escapees_list"])
var/obj/item/organ/brain/hbrain = H.getorganslot(ORGAN_SLOT_BRAIN)
- if(hbrain.damage < 60)
+ if(hbrain?.damage < 60)
all_braindamaged = FALSE
braindamage_total += hbrain.damage
var/average_braindamage = braindamage_total / human_escapees
diff --git a/code/modules/food_and_drinks/kitchen_machinery/deep_fryer.dm b/code/modules/food_and_drinks/kitchen_machinery/deep_fryer.dm
index d83125e7e4cc..e73f1cc40fb6 100644
--- a/code/modules/food_and_drinks/kitchen_machinery/deep_fryer.dm
+++ b/code/modules/food_and_drinks/kitchen_machinery/deep_fryer.dm
@@ -129,31 +129,42 @@ GLOBAL_LIST_INIT(oilfry_blacklisted_items, typecacheof(list(
frying_burnt = FALSE
fry_loop.stop()
return
- else if(user.pulling && iscarbon(user.pulling) && reagents.total_volume)
- if(user.grab_state < GRAB_AGGRESSIVE)
- to_chat(user, span_warning("You need a better grip to do that!"))
- return
- var/mob/living/carbon/dunking_target = user.pulling
- log_combat(user, dunking_target, "dunked", null, "into [src]")
- user.visible_message(span_danger("[user] dunks [dunking_target]'s face in [src]!"))
- reagents.expose(dunking_target, TOUCH)
- var/permeability = 1 - dunking_target.get_permeability_protection(list(HEAD))
- var/target_temp = dunking_target.bodytemperature
- var/cold_multiplier = 1
- if(target_temp < TCMB + 10) // a tiny bit of leeway
- dunking_target.visible_message(span_userdanger("[dunking_target] explodes from the entropic difference! Holy fuck!"))
- dunking_target.gib()
- log_combat(user, dunking_target, "blew up", null, "by dunking them into [src]")
- return
- else if(target_temp < T0C)
- cold_multiplier += round(target_temp * 1.5 / T0C, 0.01)
- dunking_target.apply_damage(min(30 * permeability * cold_multiplier, reagents.total_volume), BURN, BODY_ZONE_HEAD)
- if(reagents.reagent_list) //This can runtime if reagents has nothing in it.
- reagents.remove_any((reagents.total_volume/2))
- dunking_target.Paralyze(60)
- user.changeNext_move(CLICK_CD_MELEE)
return ..()
+/obj/machinery/deepfryer/attack_grab(mob/living/user, atom/movable/victim, obj/item/hand_item/grab/grab, list/params)
+ . = ..()
+ if(!iscarbon(victim) || !reagents.total_volume)
+ return
+
+ if(grab.current_grab.damage_stage < GRAB_AGGRESSIVE)
+ to_chat(user, span_warning("You need a better grip to do that!"))
+ return
+
+ var/mob/living/carbon/dunking_target = victim
+
+ log_combat(user, dunking_target, "dunked", null, "into [src]")
+ user.visible_message(span_danger("[user] dunks [dunking_target]'s face in [src]!"))
+ reagents.expose(dunking_target, TOUCH)
+
+ var/permeability = 1 - dunking_target.get_permeability_protection(list(HEAD))
+ var/target_temp = dunking_target.bodytemperature
+ var/cold_multiplier = 1
+ if(target_temp < TCMB + 10) // a tiny bit of leeway
+ dunking_target.visible_message(span_userdanger("[dunking_target] explodes from the entropic difference! Holy fuck!"))
+ dunking_target.gib()
+ log_combat(user, dunking_target, "blew up", null, "by dunking them into [src]")
+ return
+
+ else if(target_temp < T0C)
+ cold_multiplier += round(target_temp * 1.5 / T0C, 0.01)
+
+ dunking_target.apply_damage(min(30 * permeability * cold_multiplier, reagents.total_volume), BURN, BODY_ZONE_HEAD)
+ if(reagents.reagent_list) //This can runtime if reagents has nothing in it.
+ reagents.remove_any((reagents.total_volume/2))
+
+ dunking_target.Paralyze(60)
+ user.changeNext_move(CLICK_CD_MELEE)
+
/obj/machinery/deepfryer/proc/fry(obj/item/frying_item, mob/user)
to_chat(user, span_notice("You put [frying_item] into [src]."))
if(istype(frying_item, /obj/item/freeze_cube))
diff --git a/code/modules/food_and_drinks/kitchen_machinery/gibber.dm b/code/modules/food_and_drinks/kitchen_machinery/gibber.dm
index 2b7af865200a..e3448d3a25be 100644
--- a/code/modules/food_and_drinks/kitchen_machinery/gibber.dm
+++ b/code/modules/food_and_drinks/kitchen_machinery/gibber.dm
@@ -73,34 +73,39 @@
to_chat(user, span_warning("[src] cannot be used unless bolted to the ground!"))
return
- if(user.pulling && isliving(user.pulling))
- var/mob/living/L = user.pulling
- if(!iscarbon(L))
- to_chat(user, span_warning("This item is not suitable for the gibber!"))
- return
- var/mob/living/carbon/C = L
- if(C.buckled ||C.has_buckled_mobs())
- to_chat(user, span_warning("[C] is attached to something!"))
- return
-
- if(!ignore_clothing)
- for(var/obj/item/I in C.held_items + C.get_equipped_items())
- if(!HAS_TRAIT(I, TRAIT_NODROP))
- to_chat(user, span_warning("Subject may not have abiotic items on!"))
- return
-
- user.visible_message(span_danger("[user] starts to put [C] into the gibber!"))
-
- add_fingerprint(user)
-
- if(do_after(user, src, gibtime))
- if(C && user.pulling == C && !C.buckled && !C.has_buckled_mobs() && !occupant)
- user.visible_message(span_danger("[user] stuffs [C] into the gibber!"))
- C.forceMove(src)
- set_occupant(C)
- update_appearance()
- else
- startgibbing(user)
+ startgibbing(user)
+
+/obj/machinery/gibber/attack_grab(mob/living/user, atom/movable/victim, obj/item/hand_item/grab/grab, list/params)
+ . = ..()
+ if(!isliving(victim))
+ return
+
+ var/mob/living/L = victim
+ if(!iscarbon(L))
+ to_chat(user, span_warning("This item is not suitable for the gibber!"))
+ return
+
+ var/mob/living/carbon/C = L
+ if(C.buckled ||C.has_buckled_mobs())
+ to_chat(user, span_warning("[C] is attached to something!"))
+ return
+
+ if(!ignore_clothing)
+ for(var/obj/item/I in C.held_items + C.get_equipped_items())
+ if(!HAS_TRAIT(I, TRAIT_NODROP))
+ to_chat(user, span_warning("Subject may not have abiotic items on!"))
+ return
+
+ user.visible_message(span_danger("[user] starts to put [C] into the gibber!"))
+
+ add_fingerprint(user)
+
+ if(do_after(user, src, gibtime))
+ if(C && user.is_grabbing(C) && !C.buckled && !C.has_buckled_mobs() && !occupant)
+ user.visible_message(span_danger("[user] stuffs [C] into the gibber!"))
+ C.forceMove(src)
+ set_occupant(C)
+ update_appearance()
/obj/machinery/gibber/wrench_act(mob/living/user, obj/item/tool)
. = ..()
diff --git a/code/modules/food_and_drinks/kitchen_machinery/processor.dm b/code/modules/food_and_drinks/kitchen_machinery/processor.dm
index 616d4700adfa..871fa54ed9b9 100644
--- a/code/modules/food_and_drinks/kitchen_machinery/processor.dm
+++ b/code/modules/food_and_drinks/kitchen_machinery/processor.dm
@@ -118,16 +118,7 @@
if(processing)
to_chat(user, span_warning("[src] is in the process of processing!"))
return TRUE
- if(ismob(user.pulling) && PROCESSOR_SELECT_RECIPE(user.pulling))
- if(user.grab_state < GRAB_AGGRESSIVE)
- to_chat(user, span_warning("You need a better grip to do that!"))
- return
- var/mob/living/pushed_mob = user.pulling
- visible_message(span_warning("[user] stuffs [pushed_mob] into [src]!"))
- pushed_mob.forceMove(src)
- LAZYADD(processor_contents, pushed_mob)
- user.stop_pulling()
- return
+
if(!LAZYLEN(processor_contents))
to_chat(user, span_warning("[src] is empty!"))
return TRUE
@@ -157,6 +148,23 @@
processing = FALSE
visible_message(span_notice("\The [src] finishes processing."))
+/obj/machinery/processor/attack_grab(mob/living/user, atom/movable/victim, obj/item/hand_item/grab/grab, list/params)
+ . = ..()
+ if(processing)
+ to_chat(user, span_warning("[src] is in the process of processing!"))
+ return TRUE
+
+ if(ismob(victim) && PROCESSOR_SELECT_RECIPE(victim))
+ if(grab.current_grab.damage_stage < GRAB_AGGRESSIVE)
+ to_chat(user, span_warning("You need a better grip to do that!"))
+ return
+ var/mob/living/pushed_mob = victim
+ visible_message(span_warning("[user] stuffs [pushed_mob] into [src]!"))
+ qdel(grab)
+ pushed_mob.forceMove(src)
+ LAZYADD(processor_contents, pushed_mob)
+ return TRUE
+
/obj/machinery/processor/verb/eject()
set category = "Object"
set name = "Eject Contents"
diff --git a/code/modules/grab/grab_carbon.dm b/code/modules/grab/grab_carbon.dm
new file mode 100644
index 000000000000..a81f042b942b
--- /dev/null
+++ b/code/modules/grab/grab_carbon.dm
@@ -0,0 +1,8 @@
+/mob/living/carbon/get_active_grabs()
+ . = list()
+ for(var/obj/item/hand_item/grab/grab in held_items)
+ . += grab
+
+// We're changing the default grab type here.
+/mob/living/carbon/make_grab(atom/movable/target, grab_type = /datum/grab/normal/passive)
+ . = ..()
diff --git a/code/modules/grab/grab_datum.dm b/code/modules/grab/grab_datum.dm
new file mode 100644
index 000000000000..d8cda6f98a2a
--- /dev/null
+++ b/code/modules/grab/grab_datum.dm
@@ -0,0 +1,416 @@
+/// An associative list of type:instance for grab datums
+GLOBAL_LIST_EMPTY(all_grabstates)
+/datum/grab
+ abstract_type = /datum/grab
+ var/icon = 'icons/hud/screen_gen.dmi'
+ var/icon_state
+
+ /// The grab that this will upgrade to if it upgrades, null means no upgrade
+ var/datum/grab/upgrab
+ /// The grab that this will downgrade to if it downgrades, null means break grab on downgrade
+ var/datum/grab/downgrab
+
+ // Whether or not the grabbed person can move out of the grab
+ var/stop_move = FALSE
+ // Whether the assailant is facing forwards or backwards.
+ var/reverse_facing = FALSE
+ /// Whether this grab state is strong enough to, as a changeling, absorb the person you're grabbing.
+ var/can_absorb = FALSE
+ /// How much the grab increases point blank damage.
+ var/point_blank_mult = 1
+ /// Affects how much damage is being dealt using certain actions.
+ var/damage_stage = GRAB_PASSIVE
+ /// If the grabbed person and the grabbing person are on the same tile.
+ var/same_tile = FALSE
+ /// If the grabber can throw the person grabbed.
+ var/can_throw = FALSE
+ /// If the grab needs to be downgraded when the grabber does stuff.
+ var/downgrade_on_action = FALSE
+ /// If the grab needs to be downgraded when the grabber moves.
+ var/downgrade_on_move = FALSE
+ /// If the grab is strong enough to be able to force someone to do something harmful to them, like slam their head into glass.
+ var/enable_violent_interactions = FALSE
+ /// If the grab acts like cuffs and prevents action from the victim.
+ var/restrains = FALSE
+
+ var/grab_slowdown = 0
+
+ var/shift = 0
+
+ var/success_up = "You upgrade the grab."
+ var/success_down = "You downgrade the grab."
+
+ var/fail_up = "You fail to upgrade the grab."
+ var/fail_down = "You fail to downgrade the grab."
+
+ var/upgrade_cooldown = 4 SECONDS
+ var/action_cooldown = 4 SECONDS
+
+ var/can_downgrade_on_resist = TRUE
+
+ /// The baseline index of break_chance_table, before modifiers. This can be higher than the length of break_chance_table.
+ var/breakability = 2
+ var/list/break_chance_table = list(100)
+
+ var/can_grab_self = TRUE
+
+ // The names of different intents for use in attack logs
+ var/help_action = ""
+ var/disarm_action = ""
+ var/grab_action = ""
+ var/harm_action = ""
+
+/*
+ These procs shouldn't be overriden in the children unless you know what you're doing with them; they handle important core functions.
+ Even if you do override them, you should likely be using ..() if you want the behaviour to function properly. That is, of course,
+ unless you're writing your own custom handling of things.
+*/
+/// Called during world/New to setup references.
+/datum/grab/proc/refresh_updown()
+ if(upgrab)
+ upgrab = GLOB.all_grabstates[upgrab]
+
+ if(downgrab)
+ downgrab = GLOB.all_grabstates[downgrab]
+
+/// Called by the grab item's setup() proc. May return FALSE to interrupt, otherwise the grab has succeeded.
+/datum/grab/proc/setup(obj/item/hand_item/grab)
+ SHOULD_CALL_PARENT(TRUE)
+ return TRUE
+
+// This is for the strings defined as datum variables. It takes them and swaps out keywords for relevent ones from the grab
+// object involved.
+/datum/grab/proc/string_process(obj/item/hand_item/grab/G, to_write, obj/item/used_item)
+ to_write = replacetext(to_write, "rep_affecting", G.affecting)
+ to_write = replacetext(to_write, "rep_assailant", G.assailant)
+ if(used_item)
+ to_write = replacetext(to_write, "rep_item", used_item)
+ return to_write
+
+/datum/grab/proc/upgrade(obj/item/hand_item/grab/G)
+ if(!upgrab)
+ return
+
+ if (can_upgrade(G))
+ upgrade_effect(G)
+ return upgrab
+ else
+ to_chat(G.assailant, span_warning("[string_process(G, fail_up)]"))
+ return
+
+/datum/grab/proc/downgrade(obj/item/hand_item/grab/G)
+ // Starts the process of letting go if there's no downgrade grab
+ if(can_downgrade())
+ downgrade_effect(G)
+ return downgrab
+ else
+ to_chat(G.assailant, span_warning("[string_process(G, fail_down)]"))
+ return
+
+/datum/grab/proc/let_go(obj/item/hand_item/grab/G)
+ SHOULD_NOT_OVERRIDE(TRUE)
+ if(!G)
+ return
+ let_go_effect(G)
+ G.current_grab = null
+ if(!QDELETED(G))
+ qdel(G)
+
+/datum/grab/proc/on_target_change(obj/item/hand_item/grab/G, old_zone, new_zone)
+ remove_bodyzone_effects(G, old_zone, new_zone)
+ G.special_target_functional = check_special_target(G)
+ if(G.special_target_functional)
+ special_bodyzone_change(G, old_zone, new_zone)
+ special_bodyzone_effects(G)
+
+/datum/grab/proc/hit_with_grab(obj/item/hand_item/grab/G, atom/target, params)
+ if(G.is_currently_resolving_hit)
+ return FALSE
+
+ if(!COOLDOWN_FINISHED(G, action_cd))
+ to_chat(G.assailant, span_warning("You must wait [round(COOLDOWN_TIMELEFT(G, action_cd) * 0.1, 0.1)] seconds before you can perform a grab action."))
+ return FALSE
+
+ if(downgrade_on_action)
+ G.downgrade()
+
+ G.is_currently_resolving_hit = TRUE
+
+ var/combat_mode = G.assailant.combat_mode
+ if(params[RIGHT_CLICK])
+ if(on_hit_disarm(G, target))
+ . = disarm_action || TRUE
+
+ else if(params[CTRL_CLICK])
+ if(on_hit_grab(G, target))
+ . = grab_action || TRUE
+
+
+ else if(combat_mode)
+ if(on_hit_harm(G, target))
+ . = harm_action || TRUE
+ else
+ if(on_hit_help(G, target))
+ . = help_action || TRUE
+
+ if(QDELETED(G))
+ return
+
+ G.is_currently_resolving_hit = FALSE
+
+ if(!.)
+ return
+
+ G.action_used()
+ if(G.assailant)
+ G.assailant.changeNext_move(CLICK_CD_GRABBING)
+ if(istext(.) && G.affecting)
+ make_log(G, "used [.] on")
+
+ if(downgrade_on_action)
+ G.downgrade()
+
+/datum/grab/proc/make_log(obj/item/hand_item/grab/G, action)
+ log_combat(G.assailant, G.affecting, "[action] their victim")
+
+/*
+ Override these procs to set how the grab state will work. Some of them are best
+ overriden in the parent of the grab set (for example, the behaviour for on_hit_intent(var/obj/item/hand_item/grab/G)
+ procs is determined in /datum/grab/normal and then inherited by each intent).
+*/
+
+// What happens when you upgrade from one grab state to the next.
+/datum/grab/proc/upgrade_effect(obj/item/hand_item/grab/G, datum/grab/old_grab)
+ update_stage_effects(G, old_grab)
+
+// Conditions to see if upgrading is possible
+/datum/grab/proc/can_upgrade(obj/item/hand_item/grab/G)
+ if(!upgrab)
+ return FALSE
+
+ var/mob/living/L = G.get_affecting_mob()
+ if(!isliving(L))
+ return FALSE
+
+ if(!(L.status_flags & CANPUSH) || HAS_TRAIT(L, TRAIT_PUSHIMMUNE))
+ to_chat(G.assailant, span_warning("[src] can't be grabbed more aggressively!"))
+ return FALSE
+
+ if(upgrab.damage_stage >= GRAB_AGGRESSIVE && HAS_TRAIT(G.assailant, TRAIT_PACIFISM))
+ to_chat(G.assailant, span_warning("You don't want to risk hurting [src]!"))
+ return FALSE
+ return TRUE
+
+// What happens when you downgrade from one grab state to the next.
+/datum/grab/proc/downgrade_effect(obj/item/hand_item/grab/G, datum/grab/old_grab)
+ update_stage_effects(G, old_grab)
+
+// Conditions to see if downgrading is possible
+/datum/grab/proc/can_downgrade(obj/item/hand_item/grab/G)
+ return TRUE
+
+// What happens when you let go of someone by either dropping the grab
+// or by downgrading from the lowest grab state.
+/datum/grab/proc/let_go_effect(obj/item/hand_item/grab/G)
+ SEND_SIGNAL(G.affecting, COMSIG_ATOM_NO_LONGER_GRABBED, G.assailant)
+ SEND_SIGNAL(G.assailant, COMSIG_LIVING_NO_LONGER_GRABBING, G.affecting)
+
+ remove_bodyzone_effects(G, G.target_zone)
+ if(G.is_grab_unique(src))
+ remove_unique_grab_effects(G)
+ update_stage_effects(G, src, TRUE)
+
+/// Add effects that apply based on damage_stage here
+/datum/grab/proc/update_stage_effects(obj/item/hand_item/grab/G, datum/grab/old_grab, dropping_grab)
+ var/old_damage_stage = old_grab?.damage_stage || GRAB_PASSIVE
+ var/new_stage = dropping_grab ? GRAB_PASSIVE : damage_stage
+
+ switch(new_stage) // Current state.
+ if(GRAB_PASSIVE)
+ REMOVE_TRAIT(G.affecting, TRAIT_IMMOBILIZED, REF(G))
+ REMOVE_TRAIT(G.affecting, TRAIT_HANDS_BLOCKED, REF(G))
+ if(old_damage_stage >= GRAB_AGGRESSIVE)
+ REMOVE_TRAIT(G.affecting, TRAIT_AGGRESSIVE_GRAB, REF(G))
+ REMOVE_TRAIT(G.affecting, TRAIT_FLOORED, REF(G))
+
+ if(GRAB_AGGRESSIVE)
+ if(old_damage_stage >= GRAB_NECK) // Grab got downgraded.
+ REMOVE_TRAIT(G.affecting, TRAIT_FLOORED, REF(G))
+ else // Grab got upgraded from a passive one.
+ ADD_TRAIT(G.affecting, TRAIT_IMMOBILIZED, REF(G))
+ ADD_TRAIT(G.affecting, TRAIT_HANDS_BLOCKED, REF(G))
+ ADD_TRAIT(G.affecting, TRAIT_AGGRESSIVE_GRAB, REF(G))
+
+ if(GRAB_NECK, GRAB_KILL)
+ if(old_damage_stage < GRAB_AGGRESSIVE)
+ ADD_TRAIT(G.affecting, TRAIT_AGGRESSIVE_GRAB, REF(G))
+ if(old_damage_stage <= GRAB_AGGRESSIVE)
+ ADD_TRAIT(G.affecting, TRAIT_FLOORED, REF(G))
+ ADD_TRAIT(G.affecting, TRAIT_HANDS_BLOCKED, REF(G))
+ ADD_TRAIT(G.affecting, TRAIT_IMMOBILIZED, REF(G))
+
+
+/// Apply effects that should only be applied when a grab type is first used on a mob.
+/datum/grab/proc/apply_unique_grab_effects(obj/item/hand_item/grab/G)
+ SHOULD_CALL_PARENT(TRUE)
+ if(G.loc != G.assailant.loc && same_tile)
+ G.affecting.move_from_pull(G.assailant, get_turf(G.assailant))
+
+/// Remove effects added by apply_unique_grab_effects()
+/datum/grab/proc/remove_unique_grab_effects(obj/item/hand_item/grab/G)
+ SHOULD_CALL_PARENT(TRUE)
+
+/// Handles special targeting like eyes and mouth being covered.
+/// CLEAR OUT ANY EFFECTS USING remove_bodyzone_effects()
+/datum/grab/proc/special_bodyzone_effects(obj/item/hand_item/grab/G)
+ SHOULD_CALL_PARENT(TRUE)
+ var/mob/living/carbon/C = G.affecting
+ if(!istype(C))
+ return
+
+ var/obj/item/bodypart/BP = C.get_bodypart(deprecise_zone(G.target_zone))
+ if(!BP)
+ return
+
+ ADD_TRAIT(BP, TRAIT_BODYPART_GRABBED, REF(G))
+
+/// Clear out any effects from special_bodyzone_effects()
+/datum/grab/proc/remove_bodyzone_effects(obj/item/hand_item/grab/G, old_zone, new_zone)
+ SHOULD_CALL_PARENT(TRUE)
+ var/mob/living/carbon/C = G.affecting
+ if(!istype(C))
+ return
+
+ var/obj/item/bodypart/BP = C.get_bodypart(deprecise_zone(old_zone))
+ if(!BP)
+ return
+
+ REMOVE_TRAIT(BP, TRAIT_BODYPART_GRABBED, REF(G))
+
+// Handles when they change targeted areas and something is supposed to happen.
+/datum/grab/proc/special_bodyzone_change(obj/item/hand_item/grab/G, diff_zone)
+
+// Checks if the special target works on the grabbed humanoid.
+/datum/grab/proc/check_special_target(obj/item/hand_item/grab/G)
+
+// What happens when you hit the grabbed person with the grab on help intent.
+/datum/grab/proc/on_hit_help(obj/item/hand_item/grab/G)
+ return FALSE
+
+// What happens when you hit the grabbed person with the grab on disarm intent.
+/datum/grab/proc/on_hit_disarm(obj/item/hand_item/grab/G)
+ return FALSE
+
+// What happens when you hit the grabbed person with the grab on grab intent.
+/datum/grab/proc/on_hit_grab(obj/item/hand_item/grab/G)
+ return FALSE
+
+// What happens when you hit the grabbed person with the grab on harm intent.
+/datum/grab/proc/on_hit_harm(obj/item/hand_item/grab/G)
+ return FALSE
+
+// What happens when you hit the grabbed person with an open hand and you want it
+// to do some special snowflake action based on some other factor such as
+// intent.
+/datum/grab/proc/resolve_openhand_attack(obj/item/hand_item/grab/G)
+ return FALSE
+
+// Used when you want an effect to happen when the grab enters this state as an upgrade
+/// Return TRUE unless the grab state is changing during this proc (for example, calling upgrade())
+/datum/grab/proc/enter_as_up(obj/item/hand_item/grab/G, silent)
+ SHOULD_CALL_PARENT(TRUE)
+ return TRUE
+
+// Used when you want an effect to happen when the grab enters this state as a downgrade
+/// Return TRUE unless the grab state is changing during this proc (for example, calling upgrade())
+/datum/grab/proc/enter_as_down(obj/item/hand_item/grab/G, silent)
+ SHOULD_CALL_PARENT(TRUE)
+ return TRUE
+
+/datum/grab/proc/item_attack(obj/item/hand_item/grab/G, obj/item)
+
+/datum/grab/proc/resolve_item_attack(obj/item/hand_item/grab/G, mob/living/carbon/human/user, obj/item/I, target_zone)
+ return FALSE
+
+/// Handle resist actions from the affected mob. Returns TRUE if the grab was broken.
+/datum/grab/proc/handle_resist(obj/item/hand_item/grab/G)
+ var/mob/living/carbon/human/affecting = G.affecting
+ var/mob/living/carbon/human/assailant = G.assailant
+
+ var/break_strength = breakability + size_difference(affecting, assailant)
+ var/affecting_shock = affecting.getPain()
+ var/assailant_shock = assailant.getPain()
+
+ // Target modifiers
+ if(affecting.incapacitated(IGNORE_GRAB))
+ break_strength--
+ if(affecting.has_status_effect(/datum/status_effect/confusion))
+ break_strength--
+ if(affecting.eye_blind || HAS_TRAIT(affecting, TRAIT_BLIND))
+ break_strength--
+ if(affecting.eye_blurry)
+ break_strength--
+ if(affecting_shock >= 10)
+ break_strength--
+ if(affecting_shock >= 30)
+ break_strength--
+ if(affecting_shock >= 50)
+ break_strength--
+
+ // User modifiers
+ if(assailant.has_status_effect(/datum/status_effect/confusion))
+ break_strength++
+ if(assailant.eye_blind || HAS_TRAIT(assailant, TRAIT_BLIND))
+ break_strength++
+ if(assailant.eye_blurry)
+ break_strength++
+ if(assailant_shock >= 10)
+ break_strength++
+ if(assailant_shock >= 30)
+ break_strength++
+ if(assailant_shock >= 50)
+ break_strength++
+
+ if(break_strength < 1)
+ to_chat(G.affecting, span_warning("You try to break free but feel that unless something changes, you'll never escape!"))
+ return FALSE
+
+ if (assailant.incapacitated(IGNORE_GRAB))
+ let_go(G)
+ stack_trace("Someone resisted a grab while the assailant was incapacitated. This shouldn't ever happen.")
+ return TRUE
+
+ var/break_chance = break_chance_table[clamp(break_strength, 1, length(break_chance_table))]
+ if(prob(break_chance))
+ if(can_downgrade_on_resist && !prob((break_chance+100)/2))
+ affecting.visible_message(span_danger("[affecting] has loosened [assailant]'s grip!"), vision_distance = COMBAT_MESSAGE_RANGE)
+ G.downgrade()
+ return FALSE
+ else
+ affecting.visible_message(span_danger("[affecting] has broken free of [assailant]'s grip!"), vision_distance = COMBAT_MESSAGE_RANGE)
+ let_go(G)
+ return TRUE
+
+/datum/grab/proc/size_difference(mob/living/A, mob/living/B)
+ return mob_size_difference(A.mob_size, B.mob_size)
+
+/datum/grab/proc/moved_effect(obj/item/hand_item/grab/G)
+ return
+
+/// Add screentip context, user will always be assailant.
+/datum/grab/proc/add_context(list/context, obj/item/held_item, mob/living/user, atom/movable/target)
+ if(!(isgrab(held_item) && held_item:current_grab == src))
+ return
+
+ if(disarm_action)
+ context[SCREENTIP_CONTEXT_RMB] = capitalize(disarm_action)
+
+ if(grab_action)
+ context[SCREENTIP_CONTEXT_CTRL_LMB] = capitalize(grab_action)
+
+ if(user.combat_mode)
+ if(harm_action)
+ context[SCREENTIP_CONTEXT_LMB] = capitalize(harm_action)
+
+ else if(help_action)
+ context[SCREENTIP_CONTEXT_LMB] = capitalize(help_action)
diff --git a/code/modules/grab/grab_helpers.dm b/code/modules/grab/grab_helpers.dm
new file mode 100644
index 000000000000..496bfdb99c26
--- /dev/null
+++ b/code/modules/grab/grab_helpers.dm
@@ -0,0 +1,88 @@
+/mob/living/proc/get_active_grabs()
+ . = list()
+ for(var/obj/item/hand_item/grab/G in held_items)
+ . += G
+
+/// Returns TRUE if src is grabbing hold of the target
+/mob/living/proc/is_grabbing(atom/movable/AM)
+ RETURN_TYPE(/obj/item/hand_item/grab)
+ for(var/obj/item/hand_item/grab/G in get_active_grabs())
+ if(G.affecting == AM)
+ return G
+
+/// Release all grabs we have
+/mob/living/proc/release_all_grabs()
+ for(var/obj/item/hand_item/grab/G in get_active_grabs())
+ qdel(G)
+
+/// Release the given movable from a grab
+/mob/living/proc/release_grabs(atom/movable/AM)
+ for(var/obj/item/hand_item/grab/G in get_active_grabs())
+ if(G.affecting == AM)
+ qdel(G)
+
+/// Returns the currently selected grab item
+/mob/living/proc/get_active_grab()
+ RETURN_TYPE(/obj/item/hand_item/grab)
+ var/obj/item/hand_item/grab/G = locate() in src
+ return G
+
+/mob/living/carbon/get_active_grab()
+ RETURN_TYPE(/obj/item/hand_item/grab)
+ var/item = get_active_held_item()
+ if(isgrab(item))
+ return item
+ return ..()
+
+/// Releases the currently selected grab item
+/mob/living/proc/release_active_grab()
+ qdel(get_active_grab())
+
+/// Returns a list of every movable we are grabbing
+/mob/living/proc/get_all_grabbed_movables()
+ . = list()
+ for(var/obj/item/hand_item/grab/G in get_active_grabs())
+ . |= G.affecting
+
+/// Frees src from all grabs.
+/atom/movable/proc/free_from_all_grabs()
+ grabbed_by?.len = 0
+ grabbed_by = null
+
+
+/// Gets every grabber of this atom, and every grabber of those grabbers, repeat
+/atom/movable/proc/recursively_get_all_grabbers()
+ RETURN_TYPE(/list)
+ . = list()
+ for(var/obj/item/hand_item/grab/G in grabbed_by)
+ . |= G.assailant
+ . |= G.assailant.recursively_get_all_grabbers()
+
+/// Gets every grabbed atom of this mob, and every grabbed atom of that grabber, repeat
+/mob/living/proc/recursively_get_all_grabbed_movables()
+ RETURN_TYPE(/list)
+ . = list()
+ for(var/obj/item/hand_item/grab/G in get_active_grabs())
+ . |= G.affecting
+ var/mob/living/L = G.get_affecting_mob()
+ if(L)
+ . |= L.recursively_get_all_grabbed_movables()
+
+/// Gets every grab object owned by this mob, and every grabbed atom of those grabbed mobs
+/mob/living/proc/recursively_get_conga_line()
+ RETURN_TYPE(/list)
+ . = list()
+ for(var/obj/item/hand_item/grab/G in get_active_grabs())
+ . |= G
+ var/mob/living/L = G.get_affecting_mob()
+ if(L)
+ . |= L.recursively_get_conga_line()
+
+/// Get every single member of a grab chain
+/atom/movable/proc/get_all_grab_chain_members()
+ RETURN_TYPE(/list)
+ return recursively_get_all_grabbers()
+
+/mob/living/get_all_grab_chain_members()
+ . = ..()
+ . |= recursively_get_all_grabbed_movables()
diff --git a/code/modules/grab/grab_living.dm b/code/modules/grab/grab_living.dm
new file mode 100644
index 000000000000..08fec11b1379
--- /dev/null
+++ b/code/modules/grab/grab_living.dm
@@ -0,0 +1,159 @@
+/mob/living/proc/can_grab(atom/movable/target, target_zone, use_offhand)
+ if(throwing || !(mobility_flags & MOBILITY_PULL))
+ return FALSE
+ if(!ismob(target) && target.anchored)
+ to_chat(src, span_warning("\The [target] won't budge!"))
+ return FALSE
+
+ if(use_offhand)
+ if(!get_empty_held_index())
+ to_chat(src, span_warning("Your hands are full!"))
+ return FALSE
+
+ else if(get_active_held_item())
+ to_chat(src, span_warning("Your [active_hand_index % 2 ? "right" : "left"] hand is full!"))
+ return FALSE
+
+ if(LAZYLEN(grabbed_by))
+ to_chat(src, span_warning("You cannot start grappling while already being grappled!"))
+ return FALSE
+
+ for(var/obj/item/hand_item/grab/G in target.grabbed_by)
+ if(G.assailant != src)
+ continue
+ if(!target_zone || !ismob(target))
+ to_chat(src, span_warning("You already have a grip on \the [target]!"))
+ return FALSE
+ if(G.target_zone == target_zone)
+ var/obj/item/bodypart/BP = G.get_targeted_bodypart()
+ if(BP)
+ to_chat(src, span_warning("You already have a grip on \the [target]'s [BP.plaintext_zone]."))
+ return FALSE
+ return TRUE
+
+/mob/living/can_be_grabbed(mob/living/grabber, target_zone, use_offhand)
+ . = ..()
+ if(!.)
+ return
+ if((buckled?.buckle_prevents_pull))
+ return FALSE
+
+/// Attempt to create a grab, returns TRUE on success
+/mob/living/proc/try_make_grab(atom/movable/target, grab_type, use_offhand)
+ return canUseTopic(src, USE_IGNORE_TK|USE_CLOSE) && make_grab(target, grab_type, use_offhand)
+
+/mob/living/proc/make_grab(atom/movable/target, grab_type = /datum/grab/simple, use_offhand)
+ if(SEND_SIGNAL(src, COMSIG_LIVING_TRY_GRAB, target, grab_type) & COMSIG_LIVING_CANCEL_GRAB)
+ return
+
+ // Resolve to the 'topmost' atom in the buckle chain, as grabbing someone buckled to something tends to prevent further interaction.
+ var/atom/movable/original_target = target
+ var/mob/grabbing_mob = (ismob(target) && target)
+
+ while(istype(grabbing_mob) && grabbing_mob.buckled)
+ grabbing_mob = grabbing_mob.buckled
+
+ if(grabbing_mob && grabbing_mob != original_target)
+ target = grabbing_mob
+ to_chat(src, span_warning("As \the [original_target] is buckled to \the [target], you try to grab that instead!"))
+
+ if(!istype(target))
+ return
+
+ face_atom(target)
+
+ var/obj/item/hand_item/grab/grab
+ if(ispath(grab_type, /datum/grab) && can_grab(target, zone_selected, use_offhand) && target.can_be_grabbed(src, zone_selected, use_offhand))
+ grab = new /obj/item/hand_item/grab(src, target, grab_type, use_offhand)
+
+
+ if(QDELETED(grab))
+ if(original_target != src && ismob(original_target))
+ to_chat(original_target, span_warning("\The [src] tries to grab you, but fails!"))
+ return null
+
+ SEND_SIGNAL(src, COMSIG_LIVING_START_GRAB, target, grab)
+ SEND_SIGNAL(target, COMSIG_ATOM_GET_GRABBED, src, grab)
+
+ return grab
+
+/mob/living/proc/add_grab(obj/item/hand_item/grab/grab, use_offhand)
+ for(var/obj/item/hand_item/grab/other_grab in contents)
+ if(other_grab != grab)
+ return FALSE
+ grab.forceMove(src)
+ return TRUE
+
+/mob/living/recheck_grabs(only_pulling = FALSE, only_pulled = FALSE, z_allowed = FALSE)
+ if(only_pulled)
+ return ..()
+
+ for(var/obj/item/hand_item/grab/G in get_active_grabs())
+ var/atom/movable/pulling = G.affecting
+ if(!MultiZAdjacent(src, pulling))
+ qdel(G)
+ else if(!isturf(loc))
+ qdel(G)
+ else if(!isturf(pulling.loc)) //to be removed once all code that changes an object's loc uses forceMove().
+ qdel(G)
+ else if(pulling.anchored || pulling.move_resist > move_force)
+ qdel(G)
+
+ return ..()
+
+/// Called by grab objects when a grab has been released
+/mob/living/proc/after_grab_release(atom/movable/old_target)
+ animate_interact(old_target, INTERACT_UNPULL)
+ update_pull_hud_icon()
+
+/// Called during or immediately after movement. Used to move grab targets around to ensure the grabs do not break during movement.
+/mob/living/proc/handle_grabs_during_movement(turf/old_loc, direction)
+ var/list/grabs_in_grab_chain = get_active_grabs() //recursively_get_conga_line()
+ if(!LAZYLEN(grabs_in_grab_chain))
+ return
+
+ for(var/obj/item/hand_item/grab/G in grabs_in_grab_chain)
+ var/atom/movable/pulling = G.affecting
+ if(pulling == src || pulling.loc == loc)
+ continue
+ if(pulling.anchored || !isturf(loc))
+ qdel(G)
+ continue
+
+ var/pull_dir = get_dir(pulling, src)
+ var/target_turf = G.current_grab.same_tile ? loc : old_loc
+
+ // Pulling things down/up stairs. zMove() has flags for check_pulling and stop_pulling calls.
+ // You may wonder why we're not just forcemoving the pulling movable and regrabbing it.
+ // The answer is simple. forcemoving and regrabbing is ugly and breaks conga lines.
+ if(pulling.z != z)
+ target_turf = get_step(pulling, get_dir(pulling, old_loc))
+
+
+ if(target_turf != old_loc || (moving_diagonally != SECOND_DIAG_STEP && ISDIAGONALDIR(pull_dir)) || get_dist(src, pulling) > 1)
+ pulling.move_from_pull(G.assailant, get_step(pulling, get_dir(pulling, target_turf)), glide_size)
+ if(QDELETED(G))
+ continue
+
+ if(!pulling.MultiZAdjacent(src))
+ qdel(G)
+ continue
+
+ if(!QDELETED(G))
+ G.current_grab.moved_effect(G)
+ if(G.current_grab.downgrade_on_move)
+ G.downgrade()
+
+ if(moving_diagonally != FIRST_DIAG_STEP)
+ pulling.update_offsets()
+
+ var/list/my_grabs = get_active_grabs()
+ for(var/obj/item/hand_item/grab/G in my_grabs)
+ if(G.current_grab.reverse_facing || HAS_TRAIT(G.affecting, TRAIT_KEEP_DIRECTION_WHILE_PULLING))
+ if(!direction)
+ direction = get_dir(src, G.affecting)
+ if(!direction)
+ continue
+ setDir(global.reverse_dir[direction])
+
+
diff --git a/code/modules/grab/grab_movable.dm b/code/modules/grab/grab_movable.dm
new file mode 100644
index 000000000000..ab2afce93af6
--- /dev/null
+++ b/code/modules/grab/grab_movable.dm
@@ -0,0 +1,100 @@
+/atom/movable/proc/can_be_grabbed(mob/living/grabber, target_zone, use_offhand)
+ if(!istype(grabber) || !isturf(loc) || !isturf(grabber.loc))
+ return FALSE
+ if(SEND_SIGNAL(src, COMSIG_ATOM_CAN_BE_GRABBED, grabber) & COMSIG_ATOM_NO_GRAB)
+ return FALSE
+ if(!grabber.canUseTopic(src, USE_CLOSE|USE_IGNORE_TK))
+ return FALSE
+ if(!buckled_grab_check(grabber))
+ return FALSE
+ if(anchored)
+ to_chat(grabber, span_warning("\The [src] won't budge!"))
+ return FALSE
+ if(throwing)
+ return FALSE
+ if(pull_force < (move_resist * MOVE_FORCE_PULL_RATIO))
+ to_chat(grabber, span_warning("You aren't strong enough to move [src]!"))
+ return FALSE
+ return TRUE
+
+/atom/movable/proc/buckled_grab_check(mob/grabber)
+ if(grabber.buckled == src && (grabber in buckled_mobs))
+ return TRUE
+ if(grabber.anchored)
+ return FALSE
+ if(grabber.buckled)
+ return FALSE
+ return TRUE
+
+/**
+ * Checks if the pulling and pulledby should be stopped because they're out of reach.
+ * If z_allowed is TRUE, the z level of the pulling will be ignored.This is to allow things to be dragged up and down stairs.
+ */
+/atom/movable/proc/recheck_grabs(only_pulling = FALSE, only_pulled = FALSE, z_allowed = FALSE)
+ if(only_pulling)
+ return
+
+ for(var/obj/item/hand_item/grab/G in grabbed_by)
+ if(moving_diagonally != FIRST_DIAG_STEP && !MultiZAdjacent(G.assailant)) //separated from our puller and not in the middle of a diagonal move.
+ qdel(G)
+
+/// Move grabbed atoms towards a destination
+/mob/living/proc/move_grabbed_atoms_towards(atom/destination)
+ for(var/obj/item/hand_item/grab/G in get_active_grabs())
+ var/atom/movable/pulling = G.affecting
+ if(pulling.anchored || pulling.move_resist > move_force || !pulling.Adjacent(src, src, pulling))
+ qdel(G)
+ continue
+
+ if(isliving(pulling))
+ var/mob/living/pulling_mob = pulling
+ if(pulling_mob.buckled && pulling_mob.buckled.buckle_prevents_pull) //if they're buckled to something that disallows pulling, prevent it
+ qdel(G)
+ continue
+
+ if(destination == loc && pulling.density)
+ continue
+
+ var/move_dir = get_dir(pulling.loc, destination)
+ if(!Process_Spacemove(move_dir))
+ continue
+
+ // At this point the move was successful
+ pulling.Move(get_step(pulling.loc, move_dir), move_dir, glide_size)
+
+ pulling.update_offsets()
+
+
+/atom/movable/proc/update_offsets()
+ var/last_pixel_x = pixel_x
+ var/last_pixel_y = pixel_y
+
+ var/new_pixel_x = base_pixel_x
+ var/new_pixel_y = base_pixel_y
+
+ var/list/grabbed_by = list()
+
+ grabbed_by += src.grabbed_by
+ if(isliving(src))
+ var/mob/living/L = src
+ if(L.buckled)
+ grabbed_by += L.buckled.grabbed_by
+
+ if(isturf(loc))
+ if(length(grabbed_by))
+ for(var/obj/item/hand_item/grab/G in grabbed_by)
+ var/grab_dir = get_dir(G.assailant, src)
+ if(grab_dir && G.current_grab.shift != 0)
+ if(grab_dir & WEST)
+ new_pixel_x = min(new_pixel_x+G.current_grab.shift, base_pixel_x+G.current_grab.shift)
+ else if(grab_dir & EAST)
+ new_pixel_x = max(new_pixel_x-G.current_grab.shift, base_pixel_x-G.current_grab.shift)
+ if(grab_dir & NORTH)
+ new_pixel_y = max(new_pixel_y-G.current_grab.shift, base_pixel_y-G.current_grab.shift)
+ else if(grab_dir & SOUTH)
+ new_pixel_y = min(new_pixel_y+G.current_grab.shift, base_pixel_y+G.current_grab.shift)
+
+ if(last_pixel_x != new_pixel_x || last_pixel_y != new_pixel_y)
+ animate(src, pixel_x = new_pixel_x, pixel_y = new_pixel_y, 3, 1, (LINEAR_EASING|EASE_IN))
+
+ UPDATE_OO_IF_PRESENT
diff --git a/code/modules/grab/grab_object.dm b/code/modules/grab/grab_object.dm
new file mode 100644
index 000000000000..5cb1e3b6e64d
--- /dev/null
+++ b/code/modules/grab/grab_object.dm
@@ -0,0 +1,360 @@
+/obj/item/hand_item/grab
+ name = "grab"
+ item_flags = DROPDEL | ABSTRACT | HAND_ITEM | NOBLUDGEON
+
+ /// The initiator of the grab
+ var/mob/living/assailant = null
+ /// The thing being grabbed
+ var/atom/movable/affecting = null
+
+ /// The grab datum currently being used
+ var/datum/grab/current_grab
+
+ /// Cooldown for actions
+ COOLDOWN_DECLARE(action_cd)
+ /// Cooldown for upgrade times
+ COOLDOWN_DECLARE(upgrade_cd)
+ /// Indicates if the current grab has special interactions applied to the target organ (eyes and mouth at time of writing)
+ var/special_target_functional = TRUE
+ /// Used to avoid stacking interactions that sleep during /decl/grab/proc/on_hit_foo() (ie. do_after() is used)
+ var/is_currently_resolving_hit = FALSE
+ /// Records a specific bodypart that was targetted by this grab.
+ var/target_zone
+ /// Used by struggle grab datum to keep track of state.
+ var/done_struggle = FALSE
+
+/obj/item/hand_item/grab/Initialize(mapload, atom/movable/target, datum/grab/grab_type, use_offhand)
+ . = ..()
+ current_grab = GLOB.all_grabstates[grab_type]
+
+ assailant = loc
+ if(!istype(assailant))
+ assailant = null
+ return INITIALIZE_HINT_QDEL
+
+ affecting = target
+ if(!istype(assailant) || !assailant.add_grab(src, use_offhand = use_offhand))
+ return INITIALIZE_HINT_QDEL
+
+ target_zone = deprecise_zone(assailant.zone_selected)
+
+ if(!current_grab.setup(src))
+ return INITIALIZE_HINT_QDEL
+
+ /// Apply any needed updates to the assailant
+ assailant.update_pull_hud_icon()
+ LAZYADD(affecting.grabbed_by, src) // This is how we handle affecting being deleted.
+
+ /// Do flavor things like pixel offsets, animation, sound
+ adjust_position()
+ assailant.animate_interact(affecting, INTERACT_GRAB)
+
+ var/sound = 'sound/weapons/thudswoosh.ogg'
+ if(iscarbon(assailant))
+ var/mob/living/carbon/C = assailant
+ if(C.dna.species.grab_sound)
+ sound = C.dna.species.grab_sound
+
+ playsound(affecting.loc, sound, 50, 1, -1)
+
+ /// Spread diseases
+ if(isliving(affecting))
+ var/mob/living/affecting_mob = affecting
+ for(var/datum/disease/D as anything in assailant.diseases)
+ if(D.spread_flags & DISEASE_SPREAD_CONTACT_SKIN)
+ affecting_mob.ContactContractDisease(D)
+
+ for(var/datum/disease/D as anything in affecting_mob.diseases)
+ if(D.spread_flags & DISEASE_SPREAD_CONTACT_SKIN)
+ assailant.ContactContractDisease(D)
+
+ /// Setup the effects applied by grab
+ current_grab.update_stage_effects(src, null)
+ current_grab.special_bodyzone_effects(src)
+
+ var/mob/living/L = get_affecting_mob()
+ if(L && assailant.combat_mode)
+ upgrade(TRUE)
+
+ /// Update appearance
+ update_appearance(UPDATE_ICON_STATE)
+
+ /// Setup signals
+ var/obj/item/bodypart/BP = get_targeted_bodypart()
+ if(BP)
+ name = "[initial(name)] ([BP.plaintext_zone])"
+ RegisterSignal(affecting, COMSIG_CARBON_REMOVED_LIMB, PROC_REF(on_limb_loss))
+
+ RegisterSignal(assailant, COMSIG_PARENT_QDELETING, PROC_REF(target_or_owner_del))
+ if(affecting != assailant)
+ RegisterSignal(affecting, COMSIG_PARENT_QDELETING, PROC_REF(target_or_owner_del))
+ RegisterSignal(affecting, COMSIG_MOVABLE_PRE_THROW, PROC_REF(target_thrown))
+ RegisterSignal(affecting, COMSIG_ATOM_ATTACK_HAND, PROC_REF(intercept_attack_hand))
+
+ RegisterSignal(assailant, COMSIG_MOB_SELECTED_ZONE_SET, PROC_REF(on_target_change))
+
+/obj/item/hand_item/grab/Destroy()
+ if(affecting)
+ LAZYREMOVE(affecting.grabbed_by, src)
+ affecting.update_offsets()
+ if(affecting && assailant && current_grab)
+ current_grab.let_go(src)
+ if(assailant)
+ assailant.after_grab_release(affecting)
+ affecting = null
+ assailant = null
+ return ..()
+
+/obj/item/hand_item/grab/examine(mob/user)
+ . = ..()
+ var/mob/living/L = get_affecting_mob()
+ var/obj/item/bodypart/BP = get_targeted_bodypart()
+ if(L && BP)
+ to_chat(user, "A grab on \the [L]'s [BP.plaintext_zone].")
+
+/obj/item/hand_item/grab/update_icon_state()
+ . = ..()
+ icon = current_grab.icon
+ if(current_grab.icon_state)
+ icon_state = current_grab.icon_state
+
+/obj/item/hand_item/grab/attack_self(mob/user)
+ if (!assailant)
+ return
+
+ if(assailant.combat_mode)
+ upgrade()
+ else
+ downgrade()
+
+
+/obj/item/hand_item/grab/melee_attack_chain(mob/user, atom/target, params)
+ if (QDELETED(src) || !assailant || !current_grab)
+ return
+
+ if(target.attack_grab(assailant, affecting, src, params2list(params)))
+ return
+
+ current_grab.hit_with_grab(src, target, params2list(params))
+
+/*
+ This section is for newly defined useful procs.
+*/
+
+/obj/item/hand_item/grab/proc/on_target_change(datum/source, new_sel)
+ SIGNAL_HANDLER
+
+ if(src != assailant.get_active_held_item())
+ return // Note that because of this condition, there's no guarantee that target_zone = old_sel
+
+ new_sel = deprecise_zone(new_sel)
+ if(target_zone == new_sel)
+ return
+
+ var/obj/item/bodypart/BP
+
+ if(affecting == assailant && iscarbon(assailant))
+ BP = assailant.get_bodypart(new_sel)
+ var/using_slot = assailant.get_held_index_of_item(src)
+ if(assailant.has_hand_for_held_index(using_slot) == BP)
+ to_chat(assailant, span_warning("You can't grab your own [BP.plaintext_zone] with itself!"))
+ return
+
+ var/old_zone = target_zone
+ target_zone = new_sel
+ BP = get_targeted_bodypart()
+
+ if (!BP)
+ to_chat(assailant, span_warning("You fail to grab \the [affecting] there as they do not have that bodypart!"))
+ return
+
+ name = "[initial(name)] ([BP.plaintext_zone])"
+ to_chat(assailant, span_notice("You are now holding \the [affecting] by \the [BP.plaintext_zone]."))
+
+ if(!isbodypart(get_targeted_bodypart()))
+ current_grab.let_go(src)
+ return
+
+ current_grab.on_target_change(src, old_zone, target_zone)
+
+/obj/item/hand_item/grab/proc/on_limb_loss(mob/victim, obj/item/bodypart/lost)
+ SIGNAL_HANDLER
+
+ if(affecting != victim)
+ stack_trace("A grab switched affecting targets without properly re-registering for dismemberment updates.")
+ return
+ var/obj/item/bodypart/BP = get_targeted_bodypart()
+ if(!istype(BP))
+ current_grab.let_go(src)
+ return // Sanity check in case the lost organ was improperly removed elsewhere in the code.
+ if(lost != BP)
+ return
+ current_grab.let_go(src)
+
+/// Intercepts attack_hand() calls on our target.
+/obj/item/hand_item/grab/proc/intercept_attack_hand(atom/movable/source, user, list/modifiers)
+ SIGNAL_HANDLER
+ if(user != assailant)
+ return
+
+ if(current_grab.resolve_openhand_attack(src))
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+// Returns the bodypart of the grabbed person that the grabber is targeting
+/obj/item/hand_item/grab/proc/get_targeted_bodypart()
+ RETURN_TYPE(/obj/item/bodypart)
+ var/mob/living/L = get_affecting_mob()
+ return (L?.get_bodypart(deprecise_zone(target_zone)))
+
+/obj/item/hand_item/grab/proc/resolve_item_attack(mob/living/M, obj/item/I, target_zone)
+ if((M && ishuman(M)) && I)
+ return current_grab.resolve_item_attack(src, M, I, target_zone)
+ else
+ return 0
+
+/obj/item/hand_item/grab/proc/action_used()
+ COOLDOWN_START(src, action_cd, current_grab.action_cooldown)
+ leave_forensic_traces()
+
+/obj/item/hand_item/grab/proc/leave_forensic_traces()
+ if (!affecting)
+ return
+ var/mob/living/carbon/carbo = get_affecting_mob()
+ if(istype(carbo))
+ var/obj/item/clothing/C = carbo.get_item_covering_zone(target_zone)
+ if(istype(C))
+ C.add_fingerprint(assailant)
+ return
+
+ affecting.add_fingerprint(assailant) //If no clothing; add fingerprint to mob proper.
+
+/obj/item/hand_item/grab/proc/upgrade(bypass_cooldown, silent)
+ if(!COOLDOWN_FINISHED(src, upgrade_cd) && !bypass_cooldown)
+ if(!silent)
+ to_chat(assailant, span_warning("You must wait [round(COOLDOWN_TIMELEFT(src, upgrade_cd) * 0.1, 0.1)] seconds to upgrade."))
+ return
+
+ var/datum/grab/upgrab = current_grab.upgrade(src)
+ if(!upgrab)
+ return
+
+ if(is_grab_unique(current_grab))
+ current_grab.remove_unique_grab_effects(src)
+
+ current_grab = upgrab
+
+ COOLDOWN_START(src, upgrade_cd, current_grab.upgrade_cooldown)
+
+ adjust_position()
+ update_appearance()
+ leave_forensic_traces()
+
+ if(!current_grab.enter_as_up(src, silent))
+ return
+ if(is_grab_unique(current_grab))
+ current_grab.apply_unique_grab_effects(src)
+
+/obj/item/hand_item/grab/proc/downgrade(silent)
+ var/datum/grab/downgrab = current_grab.downgrade(src)
+ if(!downgrab)
+ return
+ if(is_grab_unique(current_grab))
+ current_grab.remove_unique_grab_effects(src)
+
+ current_grab = downgrab
+
+ if(!current_grab.enter_as_down(src))
+ return
+
+ if(is_grab_unique(current_grab))
+ current_grab.apply_unique_grab_effects(src)
+
+ current_grab.enter_as_down(src, silent)
+ adjust_position()
+ update_appearance()
+
+/// Used to prevent repeated effect application or early effect removal
+/obj/item/hand_item/grab/proc/is_grab_unique(datum/grab/grab_datum)
+ var/count = 0
+ for(var/obj/item/hand_item/grab/other as anything in affecting.grabbed_by)
+ if(other.current_grab == grab_datum)
+ count++
+
+ if(count >= 2)
+ return FALSE
+ return TRUE
+
+/obj/item/hand_item/grab/proc/draw_affecting_over()
+ affecting.plane = assailant.plane
+ affecting.layer = assailant.layer + 0.01
+
+/obj/item/hand_item/grab/proc/draw_affecting_under()
+ affecting.plane = assailant.plane
+ affecting.layer = assailant.layer - 0.01
+
+/obj/item/hand_item/grab/proc/handle_resist()
+ current_grab.handle_resist(src)
+
+/obj/item/hand_item/grab/proc/has_hold_on_bodypart(obj/item/bodypart/BP)
+ if (!BP)
+ return FALSE
+
+ if (get_targeted_bodypart() == BP)
+ return TRUE
+
+ return FALSE
+
+/obj/item/hand_item/grab/proc/get_affecting_mob()
+ RETURN_TYPE(/mob/living)
+ if(isobj(affecting))
+ if(length(affecting.buckled_mobs))
+ return affecting.buckled_mobs?[1]
+ return
+
+ if(isliving(affecting))
+ return affecting
+
+/// Primarily used for do_after() callbacks, checks if the grab item is still holding onto something
+/obj/item/hand_item/grab/proc/is_grabbing(atom/movable/AM)
+ return affecting == AM
+/*
+ * This section is for component signal relays/hooks
+*/
+
+/// Target deleted, ABORT
+/obj/item/hand_item/grab/proc/target_or_owner_del(datum/source)
+ SIGNAL_HANDLER
+ qdel(src)
+
+/// If something tries to throw the target.
+/obj/item/hand_item/grab/proc/target_thrown(atom/movable/source, list/arguments)
+ SIGNAL_HANDLER
+
+ if(!current_grab.stop_move)
+ return
+ if(arguments[4] == assailant && current_grab.can_throw)
+ return
+
+ return COMPONENT_CANCEL_THROW
+
+/obj/item/hand_item/grab/attackby(obj/W, mob/user)
+ if(user == assailant)
+ current_grab.item_attack(src, W)
+
+/obj/item/hand_item/grab/proc/resolve_openhand_attack()
+ return current_grab.resolve_openhand_attack(src)
+
+/obj/item/hand_item/grab/proc/adjust_position()
+ if(QDELETED(assailant) || QDELETED(affecting) || !assailant.Adjacent(affecting))
+ qdel(src)
+ return FALSE
+
+ if(assailant)
+ assailant.setDir(get_dir(assailant, affecting))
+
+ if(current_grab.same_tile)
+ affecting.move_from_pull(assailant, get_turf(assailant))
+ affecting.setDir(assailant.dir)
+
+ affecting.update_offsets()
+ affecting.reset_plane_and_layer()
diff --git a/code/modules/grab/grab_silicon.dm b/code/modules/grab/grab_silicon.dm
new file mode 100644
index 000000000000..f13f7d448607
--- /dev/null
+++ b/code/modules/grab/grab_silicon.dm
@@ -0,0 +1,2 @@
+/mob/living/silcion/try_make_grab(atom/movable/target, grab_type = /datum/grab/simple)
+ . = ..()
diff --git a/code/modules/grab/grabs/grab_aggressive.dm b/code/modules/grab/grabs/grab_aggressive.dm
new file mode 100644
index 000000000000..6ef2a8f3dc39
--- /dev/null
+++ b/code/modules/grab/grabs/grab_aggressive.dm
@@ -0,0 +1,72 @@
+/datum/grab/normal/aggressive
+ upgrab = /datum/grab/normal/neck
+ downgrab = /datum/grab/normal/passive
+
+ grab_slowdown = 0.7
+ shift = 12
+ stop_move = TRUE
+ reverse_facing = FALSE
+ point_blank_mult = 1.5
+ damage_stage = GRAB_AGGRESSIVE
+ same_tile = FALSE
+ can_throw = TRUE
+ enable_violent_interactions = TRUE
+ breakability = 3
+ icon_state = "reinforce1"
+
+ break_chance_table = list(5, 20, 40, 80, 100)
+
+/datum/grab/normal/aggressive/apply_unique_grab_effects(obj/item/hand_item/grab/G)
+ . = ..()
+ if(!isliving(G.affecting))
+ return
+
+ var/mob/living/L = G.affecting
+ RegisterSignal(G.affecting, COMSIG_LIVING_SET_BODY_POSITION, PROC_REF(target_bodyposition_change))
+
+ if(L.body_position == LYING_DOWN)
+ ADD_TRAIT(L, TRAIT_FLOORED, AGGRESSIVE_GRAB)
+
+/datum/grab/normal/aggressive/remove_unique_grab_effects(obj/item/hand_item/grab/G)
+ . = ..()
+ UnregisterSignal(G.affecting, COMSIG_LIVING_SET_BODY_POSITION)
+ REMOVE_TRAIT(G.affecting, TRAIT_FLOORED, AGGRESSIVE_GRAB)
+
+/datum/grab/normal/aggressive/proc/target_bodyposition_change(mob/living/source)
+ if(source.body_position == LYING_DOWN)
+ ADD_TRAIT(source, TRAIT_FLOORED, AGGRESSIVE_GRAB)
+
+/datum/grab/normal/aggressive/can_upgrade(obj/item/hand_item/grab/G)
+ . = ..()
+ if(.)
+ if(!ishuman(G.affecting))
+ to_chat(G.assailant, span_warning("You can only upgrade an aggressive grab when grappling a human!"))
+ return FALSE
+ if(!(G.target_zone in list(BODY_ZONE_CHEST, BODY_ZONE_HEAD)))
+ to_chat(G.assailant, span_warning("You need to be grabbing their torso or head for this!"))
+ return FALSE
+
+ var/mob/living/carbon/human/affecting_mob = G.get_affecting_mob()
+ if(istype(affecting_mob))
+ var/obj/item/clothing/C = affecting_mob.head
+ if(istype(C)) //hardsuit helmets etc
+ if((C.clothing_flags & STOPSPRESSUREDAMAGE) && C.returnArmor().getRating(MELEE) > 20)
+ to_chat(G.assailant, span_warning("\The [C] is in the way!"))
+ return FALSE
+
+/datum/grab/normal/aggressive/enter_as_up(obj/item/hand_item/grab/G, silent)
+ . = ..()
+ if(!silent)
+ G.assailant.visible_message(
+ span_danger("[G.assailant] tightens their grip on [G.affecting] (now hands)!"),
+ blind_message = span_hear("You hear aggressive shuffling."),
+ vision_distance = COMBAT_MESSAGE_RANGE,
+ )
+
+/datum/grab/normal/aggressive/enter_as_down(obj/item/hand_item/grab/G, silent)
+ . = ..()
+ if(!silent)
+ G.assailant.visible_message(
+ span_danger("[G.assailant] loosens their grip on [G.affecting], allowing them to breathe!"),
+ vision_distance = COMBAT_MESSAGE_RANGE,
+ )
diff --git a/code/modules/grab/grabs/grab_neck.dm b/code/modules/grab/grabs/grab_neck.dm
new file mode 100644
index 000000000000..8f1597cb4c39
--- /dev/null
+++ b/code/modules/grab/grabs/grab_neck.dm
@@ -0,0 +1,47 @@
+/datum/grab/normal/neck
+ upgrab = /datum/grab/normal/kill
+ downgrab = /datum/grab/normal/aggressive
+
+ grab_slowdown = 4
+
+ drop_headbutt = FALSE
+ shift = -10
+ stop_move = TRUE
+ reverse_facing = TRUE
+ point_blank_mult = 2
+ damage_stage = GRAB_NECK
+ same_tile = TRUE
+ can_throw = TRUE
+ enable_violent_interactions = TRUE
+ restrains = TRUE
+ icon_state = "kill"
+ break_chance_table = list(3, 18, 45, 100)
+
+/datum/grab/normal/neck/apply_unique_grab_effects(obj/item/hand_item/grab/G)
+ . = ..()
+ if(!isliving(G.affecting))
+ return
+
+ var/mob/living/L = G.affecting
+ ADD_TRAIT(L, TRAIT_FLOORED, NECK_GRAB)
+
+/datum/grab/normal/neck/remove_unique_grab_effects(obj/item/hand_item/grab/G)
+ . = ..()
+ REMOVE_TRAIT(G.affecting, TRAIT_FLOORED, NECK_GRAB)
+
+/datum/grab/normal/neck/enter_as_up(obj/item/hand_item/grab/G, silent)
+ . = ..()
+ if(!silent)
+ G.assailant.visible_message(
+ span_danger("[G.assailant] places their arm around [G.affecting]'s neck!"),
+ blind_message = span_hear("You hear aggressive shuffling."),
+ vision_distance = COMBAT_MESSAGE_RANGE
+ )
+
+/datum/grab/normal/neck/enter_as_down(obj/item/hand_item/grab/G, silent)
+ . = ..()
+ if(!silent)
+ G.assailant.visible_message(
+ span_danger("[G.assailant] stops strangling [G.affecting]."),
+ vision_distance = COMBAT_MESSAGE_RANGE
+ )
diff --git a/code/modules/grab/grabs/grab_normal.dm b/code/modules/grab/grabs/grab_normal.dm
new file mode 100644
index 000000000000..2f9ebd222b3e
--- /dev/null
+++ b/code/modules/grab/grabs/grab_normal.dm
@@ -0,0 +1,341 @@
+/datum/grab/normal
+ abstract_type = /datum/grab/normal
+
+ help_action = "inspect"
+ disarm_action = "pin"
+ grab_action = "jointlock"
+ harm_action = "dislocate"
+
+ var/drop_headbutt = 1
+
+/datum/grab/normal/setup(obj/item/hand_item/grab/G)
+ if(!(. = ..()))
+ return
+
+ var/obj/item/bodypart/BP = G.get_targeted_bodypart()
+ if(!BP)
+ return
+
+ if(G.affecting != G.assailant)
+ G.assailant.visible_message(span_warning("[G.assailant] has grabbed [G.affecting]'s [BP.plaintext_zone]!"))
+ else
+ G.assailant.visible_message(span_notice("[G.assailant] has grabbed [G.assailant.p_their()] [BP.plaintext_zone]!"))
+
+/datum/grab/normal/on_hit_help(obj/item/hand_item/grab/G, atom/A)
+
+ var/obj/item/bodypart/BP = G.get_targeted_bodypart()
+ if(!BP || (A && A != G.get_affecting_mob()))
+ return FALSE
+ return BP.inspect(G.assailant)
+
+/datum/grab/normal/on_hit_disarm(obj/item/hand_item/grab/G, atom/A)
+
+ var/mob/living/affecting = G.get_affecting_mob()
+ var/mob/living/assailant = G.assailant
+ if(affecting && A && A == affecting)
+ if(affecting.body_position == LYING_DOWN)
+ to_chat(assailant, span_warning("They're already on the ground."))
+ return FALSE
+
+ affecting.visible_message(span_danger("\The [assailant] is trying to pin \the [affecting] to the ground!"))
+
+ if(do_after(assailant, affecting, action_cooldown - 1, DO_PUBLIC, display = image('icons/hud/do_after.dmi', "harm")))
+ G.action_used()
+ affecting.visible_message(span_danger("\The [assailant] pins \the [affecting] to the ground!"))
+ affecting.Knockdown(1 SECOND) // This can only be performed with an aggressive grab, which ensures that once someone is knocked down, they stay down/
+ return TRUE
+
+ affecting.visible_message(span_warning("\The [assailant] fails to pin \the [affecting] to the ground."))
+ return FALSE
+
+/datum/grab/normal/on_hit_grab(obj/item/hand_item/grab/G, atom/A)
+ var/mob/living/affecting = G.get_affecting_mob()
+ if(!affecting || (A && A != affecting))
+ return FALSE
+
+ var/mob/living/assailant = G.assailant
+ if(!assailant)
+ return FALSE
+
+ var/obj/item/bodypart/BP = G.get_targeted_bodypart()
+ if(!BP)
+ to_chat(assailant, span_warning("\The [affecting] is missing that body part!"))
+ return FALSE
+
+ assailant.visible_message(span_danger("\The [assailant] begins to [pick("bend", "twist")] \the [affecting]'s [BP.plaintext_zone] into a jointlock!"))
+ if(do_after(assailant, affecting, action_cooldown - 1, DO_PUBLIC, display = image('icons/hud/do_after.dmi', "harm")))
+ G.action_used()
+ BP.jointlock(assailant)
+ assailant.visible_message(span_danger("\The [affecting]'s [BP.plaintext_zone] is twisted!"))
+ playsound(assailant.loc, 'sound/weapons/thudswoosh.ogg', 50, 1, -1)
+ return TRUE
+
+ affecting.visible_message(span_warning("\The [assailant] fails to jointlock \the [affecting]'s [BP.plaintext_zone]."))
+ return FALSE
+
+/datum/grab/normal/on_hit_harm(obj/item/hand_item/grab/G, atom/A)
+ var/mob/living/carbon/affecting = G.get_affecting_mob()
+ if(!istype(affecting) || (A && A != affecting))
+ return FALSE
+
+ var/mob/living/assailant = G.assailant
+ if(!assailant)
+ return FALSE
+
+ var/obj/item/bodypart/BP = G.get_targeted_bodypart()
+ if(!BP)
+ to_chat(assailant, span_warning("\The [affecting] is missing that body part!"))
+ return FALSE
+
+ if(BP.bodypart_flags & BP_DISLOCATED)
+ assailant.visible_message(span_notice("\The [assailant] begins to place [affecting]'s [BP.joint_name] back in it's socket."))
+ if(do_after(assailant, affecting, action_cooldown - 1, DO_PUBLIC))
+ G.action_used()
+ BP.set_dislocated(FALSE)
+ assailant.visible_message(span_warning("\The [affecting]'s [BP.joint_name] pops back into place!"))
+ affecting.pain_message("AAAHHHHAAGGHHHH", 50, TRUE)
+ return TRUE
+
+ if(BP.can_be_dislocated())
+ assailant.visible_message(span_danger("\The [assailant] begins to dislocate \the [affecting]'s [BP.joint_name]!"))
+ if(do_after(assailant, affecting, action_cooldown - 1, DO_PUBLIC))
+ G.action_used()
+ BP.set_dislocated(TRUE)
+ assailant.visible_message(span_danger("\The [affecting]'s [BP.joint_name] [pick("gives way","caves in","collapses")]!"))
+ playsound(assailant.loc, 'sound/weapons/thudswoosh.ogg', 50, 1, -1)
+ return TRUE
+
+ affecting.visible_message(span_warning("\The [assailant] fails to dislocate \the [affecting]'s [BP.joint_name]."))
+ return FALSE
+
+ if(BP.can_be_dislocated())
+ to_chat(assailant, span_warning("\The [affecting]'s [BP.joint_name] is already dislocated!"))
+ else
+ to_chat(assailant, span_warning("You can't dislocate \the [affecting]'s [BP.joint_name]!"))
+ return FALSE
+
+/datum/grab/normal/resolve_openhand_attack(obj/item/hand_item/grab/G)
+ if(!G.assailant.combat_mode)
+ return FALSE
+ if(G.target_zone == BODY_ZONE_HEAD)
+ if(G.assailant.zone_selected == BODY_ZONE_PRECISE_EYES)
+ if(attack_eye(G))
+ return TRUE
+ else
+ if(headbutt(G))
+ if(drop_headbutt)
+ let_go()
+ return TRUE
+ return FALSE
+
+/datum/grab/normal/proc/attack_eye(obj/item/hand_item/grab/G)
+ var/mob/living/carbon/human/target = G.get_affecting_mob()
+ var/mob/living/carbon/human/attacker = G.assailant
+ if(!istype(target) || !istype(attacker))
+ return
+
+ if(target.is_eyes_covered())
+ to_chat(attacker, "You're going to need to remove the eye covering first.")
+ return
+
+ var/obj/item/organ/eyes/E = target.getorganslot(ORGAN_SLOT_EYES)
+ if(E)
+ to_chat(attacker, "You cannot locate any eyes on [target]!")
+ return
+
+ log_combat(attacker, target, "attacked the eyes of (grab)")
+
+ if(E)
+ E.applyOrganDamage(rand(3,4))
+ attacker.visible_message(span_danger("\The [attacker] jams [G.p_their()] fingers into \the [target]'s [E.name]!"))
+ if(!(E.organ_flags & ORGAN_SYNTHETIC))
+ to_chat(target, span_danger("You experience immense pain as fingers are jammed into your [E.name]!"))
+ else
+ to_chat(target, span_danger("You experience fingers being jammed into your [E.name]."))
+ else
+ attacker.visible_message(span_danger("\The [attacker] attempts to press [G.p_their()] fingers into \the [target]'s [E.name], but [target.p_they()] doesn't have any!"))
+ return TRUE
+
+/datum/grab/normal/proc/headbutt(obj/item/hand_item/grab/G)
+ var/mob/living/carbon/human/target = G.get_affecting_mob()
+ var/mob/living/carbon/human/attacker = G.assailant
+ if(!istype(target) || !istype(attacker))
+ return
+
+ if(target.body_position == LYING_DOWN)
+ return
+
+ var/damage = 20
+ var/obj/item/clothing/hat = attacker.head
+ var/sharpness
+ if(istype(hat))
+ damage += hat.force * 3
+ sharpness = hat.sharpness
+
+ if(sharpness & SHARP_POINTY)
+ if(istype(hat))
+ attacker.visible_message(span_danger("\The [attacker] gores \the [target] with \the [hat]!"))
+ else
+ attacker.visible_message(span_danger("\The [attacker] gores \the [target]!"))
+ else
+ attacker.visible_message(span_danger("\The [attacker] thrusts [attacker.p_their()] head into \the [target]'s skull!"))
+
+ var/armor = target.run_armor_check(BODY_ZONE_HEAD, MELEE)
+ target.apply_damage(damage, BRUTE, BODY_ZONE_HEAD, armor, sharpness = sharpness)
+ attacker.apply_damage(10, BRUTE, BODY_ZONE_HEAD)
+
+ if(armor < 0.5 && target.can_head_trauma_ko() && prob(damage))
+ target.Unconscious(20 SECONDS)
+ target.visible_message(span_danger("\The [target] collapses, now fast asleep."))
+
+ playsound(attacker.loc, "swing_hit", 25, 1, -1)
+ log_combat(attacker, target, "headbutted")
+ return 1
+
+// Handles special targeting like eyes and mouth being covered.
+/datum/grab/normal/special_bodyzone_effects(obj/item/hand_item/grab/G)
+ . = ..()
+ var/mob/living/affecting_mob = G.get_affecting_mob()
+ if(istype(affecting_mob) && G.special_target_functional)
+ switch(G.target_zone)
+ if(BODY_ZONE_PRECISE_MOUTH)
+ ADD_TRAIT(affecting_mob, TRAIT_MUTE, REF(G))
+ if(BODY_ZONE_PRECISE_EYES)
+ ADD_TRAIT(affecting_mob, TRAIT_BLIND, REF(G))
+
+/datum/grab/normal/remove_bodyzone_effects(obj/item/hand_item/grab/G)
+ . = ..()
+ REMOVE_TRAIT(G.affecting, TRAIT_MUTE, REF(G))
+ REMOVE_TRAIT(G.affecting, TRAIT_BLIND, REF(G))
+
+// Handles when they change targeted areas and something is supposed to happen.
+/datum/grab/normal/special_bodyzone_change(obj/item/hand_item/grab/G, old_zone, new_zone)
+ old_zone = parse_zone(old_zone)
+ if((old_zone != BODY_ZONE_HEAD && old_zone != BODY_ZONE_CHEST) || !G.get_affecting_mob())
+ return
+ switch(new_zone)
+ if(BODY_ZONE_PRECISE_MOUTH)
+ G.assailant.visible_message("\The [G.assailant] covers [G.affecting]'s mouth!")
+ if(BODY_ZONE_PRECISE_EYES)
+ G.assailant.visible_message("\The [G.assailant] covers [G.affecting]'s eyes!")
+
+/datum/grab/normal/check_special_target(obj/item/hand_item/grab/G)
+ var/mob/living/affecting_mob = G.get_affecting_mob()
+ if(!istype(affecting_mob))
+ return FALSE
+ switch(G.target_zone)
+ if(BODY_ZONE_PRECISE_MOUTH)
+ if(!affecting_mob.has_mouth())
+ to_chat(G.assailant, "You cannot locate a mouth on [G.affecting]!")
+ return FALSE
+ if(BODY_ZONE_PRECISE_EYES)
+ if(!affecting_mob.getorganslot(ORGAN_SLOT_EYES))
+ to_chat(G.assailant, "You cannot locate any eyes on [G.affecting]!")
+ return FALSE
+ return TRUE
+
+/datum/grab/normal/resolve_item_attack(obj/item/hand_item/grab/G, mob/living/carbon/human/user, obj/item/I)
+ switch(G.target_zone)
+ if(BODY_ZONE_HEAD)
+ return attack_throat(G, I, user)
+ else
+ return attack_tendons(G, I, user, G.target_zone)
+
+/datum/grab/normal/proc/attack_throat(obj/item/hand_item/grab/G, obj/item/W, mob/living/user)
+ var/mob/living/carbon/affecting = G.get_affecting_mob()
+ if(!istype(affecting))
+ return
+ if(!user.combat_mode)
+ return FALSE // Not trying to hurt them.
+
+ if(!(W.sharpness & SHARP_EDGED) || !W.force || W.damtype != BRUTE)
+ return FALSE //unsuitable weapon
+
+ if(DOING_INTERACTION(user, "throat slit"))
+ return FALSE
+
+ user.visible_message("\The [user] begins to slit [affecting]'s throat with \the [W]!")
+
+ user.changeNext_move(CLICK_CD_MELEE)
+ if(!do_after(user, affecting, 2 SECONDS, DO_PUBLIC, extra_checks = CALLBACK(G, TYPE_PROC_REF(/obj/item/hand_item/grab, is_grabbing), affecting), interaction_key = "throat slit", display = W))
+ return FALSE
+
+ if(!(G && G.affecting == affecting)) //check that we still have a grab
+ return FALSE
+
+ var/armor
+
+ //presumably, if they are wearing a helmet that stops pressure effects, then it probably covers the throat as well
+ for(var/obj/item/clothing/equipped in affecting.get_equipped_items())
+ if((equipped.body_parts_covered & HEAD) && (equipped.clothing_flags & STOPSPRESSUREDAMAGE))
+ armor = affecting.run_armor_check(BODY_ZONE_HEAD, MELEE, silent = TRUE)
+ break
+
+ var/total_damage = 0
+ for(var/i in 1 to 3)
+ var/damage = min(W.force*1.5, 20)
+ affecting.apply_damage(damage, BRUTE, BODY_ZONE_HEAD, armor, sharpness = W.sharpness)
+ total_damage += damage
+
+ if(total_damage)
+ user.visible_message("\The [user] slit [affecting]'s throat open with \the [W]!")
+ affecting.apply_status_effect(/datum/status_effect/neck_slice)
+ if(W.hitsound)
+ playsound(affecting.loc, W.hitsound, 50, 1, -1)
+
+ COOLDOWN_START(G, action_cd, action_cooldown)
+
+ log_combat(user, affecting, "slit throat (grab)")
+ return 1
+
+/datum/grab/normal/proc/attack_tendons(obj/item/hand_item/grab/G, obj/item/W, mob/living/user, target_zone)
+ var/mob/living/affecting = G.get_affecting_mob()
+ if(!affecting)
+ return
+ if(!user.combat_mode)
+ return FALSE // Not trying to hurt them.
+
+ if(!(W.sharpness & SHARP_EDGED) || !W.force || W.damtype != BRUTE)
+ return FALSE //unsuitable weapon
+
+ var/obj/item/bodypart/BP = G.get_targeted_bodypart()
+ if(!BP || !(BP.check_tendon() == CHECKTENDON_OK))
+ return FALSE
+
+ if(DOING_INTERACTION(user, "slice tendon"))
+ return FALSE
+
+ user.visible_message(span_danger("\The [user] begins to cut \the [affecting]'s [BP.tendon_name] with \the [W]!"), vision_distance = COMBAT_MESSAGE_RANGE)
+ user.changeNext_move(CLICK_CD_MELEE)
+
+ if(!do_after(user, affecting, 2 SECONDS, DO_PUBLIC, extra_checks = CALLBACK(G, TYPE_PROC_REF(/obj/item/hand_item/grab, is_grabbing), affecting), interaction_key = "slice tendon", display = W))
+ return FALSE
+
+ if(!BP || !BP.set_sever_tendon(TRUE))
+ return FALSE
+
+ user.visible_message(span_danger("\The [user] cut \the [affecting]'s [BP.tendon_name] with \the [W]!"))
+
+ if(W.hitsound)
+ playsound(affecting.loc, W.hitsound, 50, 1, -1)
+
+ COOLDOWN_START(G, action_cd, action_cooldown)
+
+ log_combat(user, affecting, "hamstrung (grab)")
+ return TRUE
+
+/datum/grab/normal/enter_as_down(obj/item/hand_item/grab/G, silent)
+ . = ..()
+ G.assailant.visible_message(
+ span_warning("[G.assailant] loosens their grip on [G.affecting]."),
+ )
+
+/datum/grab/normal/add_context(list/context, obj/item/held_item, mob/living/user, atom/movable/target)
+ . = ..()
+ if(held_item?.sharpness & SHARP_EDGED)
+ var/obj/item/hand_item/grab/G = user.is_grabbing(target)
+ switch(G.target_zone)
+ if(BODY_ZONE_HEAD)
+ context[SCREENTIP_CONTEXT_LMB] = "Slice neck"
+ else
+ context[SCREENTIP_CONTEXT_LMB] = "Attack tendons"
diff --git a/code/modules/grab/grabs/grab_passive.dm b/code/modules/grab/grabs/grab_passive.dm
new file mode 100644
index 000000000000..09cca4c4e6b7
--- /dev/null
+++ b/code/modules/grab/grabs/grab_passive.dm
@@ -0,0 +1,24 @@
+/datum/grab/normal/passive
+ upgrab = /datum/grab/normal/struggle
+ shift = 8
+ stop_move = 0
+ reverse_facing = 0
+ point_blank_mult = 1.1
+ same_tile = 0
+ icon_state = "!reinforce"
+ break_chance_table = list(15, 60, 100)
+
+/datum/grab/normal/passive/on_hit_disarm(obj/item/hand_item/grab/G, atom/A)
+ to_chat(G.assailant, span_warning("Your grip isn't strong enough to pin."))
+ return FALSE
+
+/datum/grab/normal/passive/on_hit_grab(obj/item/hand_item/grab/G, atom/A)
+ to_chat(G.assailant, span_warning("Your grip isn't strong enough to jointlock."))
+ return FALSE
+
+/datum/grab/normal/passive/on_hit_harm(obj/item/hand_item/grab/G, atom/A)
+ to_chat(G.assailant, span_warning("Your grip isn't strong enough to dislocate."))
+ return FALSE
+
+/datum/grab/normal/passive/resolve_openhand_attack(obj/item/hand_item/grab/G)
+ return FALSE
diff --git a/code/modules/grab/grabs/grab_simple.dm b/code/modules/grab/grabs/grab_simple.dm
new file mode 100644
index 000000000000..147b95eabf0b
--- /dev/null
+++ b/code/modules/grab/grabs/grab_simple.dm
@@ -0,0 +1,23 @@
+/datum/grab/simple
+ shift = 8
+ stop_move = FALSE
+ reverse_facing = FALSE
+ point_blank_mult = 1
+ same_tile = FALSE
+ icon_state = "!reinforce"
+ break_chance_table = list(15, 60, 100)
+
+/datum/grab/simple/upgrade(obj/item/hand_item/grab/G)
+ return
+
+/datum/grab/simple/on_hit_disarm(obj/item/hand_item/grab/G, atom/A)
+ return FALSE
+
+/datum/grab/simple/on_hit_grab(obj/item/hand_item/grab/G, atom/A)
+ return FALSE
+
+/datum/grab/simple/on_hit_harm(obj/item/hand_item/grab/G, atom/A)
+ return FALSE
+
+/datum/grab/simple/resolve_openhand_attack(obj/item/hand_item/grab/G)
+ return FALSE
diff --git a/code/modules/grab/grabs/grab_strangle.dm b/code/modules/grab/grabs/grab_strangle.dm
new file mode 100644
index 000000000000..0de3afae24dd
--- /dev/null
+++ b/code/modules/grab/grabs/grab_strangle.dm
@@ -0,0 +1,36 @@
+/datum/grab/normal/kill
+ downgrab = /datum/grab/normal/neck
+ grab_slowdown = 1.4
+
+ shift = 0
+ stop_move = TRUE
+ reverse_facing = TRUE
+ point_blank_mult = 2
+ damage_stage = GRAB_KILL
+ same_tile = TRUE
+ enable_violent_interactions = TRUE
+ restrains = TRUE
+ downgrade_on_action = TRUE
+ downgrade_on_move = TRUE
+ icon_state = "kill1"
+ break_chance_table = list(5, 20, 40, 80, 100)
+
+/datum/grab/normal/kill/apply_unique_grab_effects(obj/item/hand_item/grab/G)
+ . = ..()
+ if(!isliving(G.affecting))
+ return
+
+ ADD_TRAIT(G.affecting, TRAIT_KILL_GRAB, REF(G))
+
+/datum/grab/normal/kill/remove_unique_grab_effects(obj/item/hand_item/grab/G)
+ . = ..()
+ REMOVE_TRAIT(G.affecting, TRAIT_KILL_GRAB, REF(G))
+
+/datum/grab/normal/kill/enter_as_up(obj/item/hand_item/grab/G, silent)
+ . = ..()
+ if(!silent)
+ G.assailant.visible_message(
+ span_danger("[G.assailant] begins to strangle [G.affecting]!"),
+ blind_message = span_hear("You hear aggressive shuffling."),
+ vision_distance = COMBAT_MESSAGE_RANGE
+ )
diff --git a/code/modules/grab/grabs/grab_struggle.dm b/code/modules/grab/grabs/grab_struggle.dm
new file mode 100644
index 000000000000..0123c3944955
--- /dev/null
+++ b/code/modules/grab/grabs/grab_struggle.dm
@@ -0,0 +1,74 @@
+/datum/grab/normal/struggle
+ upgrab = /datum/grab/normal/aggressive
+ downgrab = /datum/grab/normal/passive
+ shift = 8
+ stop_move = TRUE
+ reverse_facing = FALSE
+ same_tile = FALSE
+ breakability = 3
+ grab_slowdown = 0.7
+ upgrade_cooldown = 2 SECONDS
+ can_downgrade_on_resist = 0
+ icon_state = "reinforce"
+ break_chance_table = list(5, 20, 30, 80, 100)
+
+/datum/grab/normal/struggle/enter_as_up(obj/item/hand_item/grab/G, silent)
+ var/mob/living/affecting = G.get_affecting_mob()
+ var/mob/living/assailant = G.assailant
+ if(!affecting)
+ return TRUE
+
+ if(affecting == assailant)
+ G.done_struggle = TRUE
+ G.upgrade(TRUE)
+ return FALSE
+
+ if(!affecting.can_resist() || !affecting.combat_mode)
+ affecting.visible_message(
+ span_danger("\The [affecting] isn't prepared to fight back as [assailant] tightens [assailant.p_their()] grip!"),
+ vision_distance = COMBAT_MESSAGE_RANGE,
+ )
+ G.done_struggle = TRUE
+ G.upgrade(TRUE, FALSE)
+ return FALSE
+
+ affecting.visible_message("[affecting] struggles against [assailant]!", vision_distance = COMBAT_MESSAGE_RANGE)
+ G.done_struggle = FALSE
+ addtimer(CALLBACK(G, TYPE_PROC_REF(/obj/item/hand_item/grab, handle_resist)), 1 SECOND)
+ resolve_struggle(G)
+ return ..()
+
+/datum/grab/normal/struggle/proc/resolve_struggle(obj/item/hand_item/grab/G)
+ set waitfor = FALSE
+
+ var/datum/callback/user_incapacitated_callback = CALLBACK(src, PROC_REF(resolve_struggle_check), G)
+ if(do_after(G.assailant, G.affecting, upgrade_cooldown, DO_PUBLIC|IGNORE_USER_LOC_CHANGE|IGNORE_TARGET_LOC_CHANGE, extra_checks = user_incapacitated_callback))
+ G.done_struggle = TRUE
+ G.upgrade(TRUE)
+ else
+ if(!QDELETED(G))
+ G.downgrade()
+
+/// Callback for the above proc
+/datum/grab/normal/struggle/proc/resolve_struggle_check(obj/item/hand_item/grab/G)
+ if(QDELETED(G))
+ return FALSE
+ return TRUE
+
+/datum/grab/normal/struggle/can_upgrade(obj/item/hand_item/grab/G)
+ . = ..() && G.done_struggle
+
+/datum/grab/normal/struggle/on_hit_disarm(obj/item/hand_item/grab/G, atom/A)
+ to_chat(G.assailant, span_warning("Your grip isn't strong enough to pin."))
+ return FALSE
+
+/datum/grab/normal/struggle/on_hit_grab(obj/item/hand_item/grab/G, atom/A)
+ to_chat(G.assailant, span_warning("Your grip isn't strong enough to jointlock."))
+ return FALSE
+
+/datum/grab/normal/struggle/on_hit_harm(obj/item/hand_item/grab/G, atom/A)
+ to_chat(G.assailant, span_warning("Your grip isn't strong enough to dislocate."))
+ return FALSE
+
+/datum/grab/normal/struggle/resolve_openhand_attack(obj/item/hand_item/grab/G)
+ return FALSE
diff --git a/code/modules/grab/human_grab.dm b/code/modules/grab/human_grab.dm
new file mode 100644
index 000000000000..aa852d5419e4
--- /dev/null
+++ b/code/modules/grab/human_grab.dm
@@ -0,0 +1,36 @@
+/mob/living/carbon/human/add_grab(obj/item/hand_item/grab/grab, use_offhand = FALSE)
+ if(use_offhand)
+ . = put_in_inactive_hand(grab)
+ else
+ . = put_in_active_hand(grab)
+
+/mob/living/carbon/human/can_be_grabbed(mob/living/grabber, target_zone, use_offhand)
+ . = ..()
+ if(!.)
+ return
+
+ var/obj/item/bodypart/BP = get_bodypart(deprecise_zone(target_zone))
+ if(!istype(BP))
+ to_chat(grabber, span_warning("\The [src] is missing that body part!"))
+ return FALSE
+
+ if(grabber == src)
+ var/using_slot = use_offhand ? get_inactive_hand() : get_active_hand()
+ if(!using_slot)
+ to_chat(src, span_warning("You cannot grab yourself without a usable hand!"))
+ return FALSE
+
+ if(using_slot == BP)
+ to_chat(src, span_warning("You can't grab your own [BP.plaintext_zone] with itself!"))
+ return FALSE
+ /*
+ if(pull_damage())
+ to_chat(grabber, span_warning("Pulling \the [src] in their current condition would probably be a bad idea."))
+ */
+
+ var/obj/item/clothing/C = get_item_covering_zone(target_zone)
+ if(istype(C))
+ C.add_fingerprint(grabber)
+ else
+ add_fingerprint(grabber)
+
diff --git a/code/modules/holodeck/items.dm b/code/modules/holodeck/items.dm
index 202c7ea00139..80e4167689a1 100644
--- a/code/modules/holodeck/items.dm
+++ b/code/modules/holodeck/items.dm
@@ -93,21 +93,19 @@
if(user.transferItemToLoc(W, drop_location()))
visible_message(span_warning("[user] dunks [W] into \the [src]!"))
-/obj/structure/holohoop/attack_hand(mob/living/user, list/modifiers)
+/obj/structure/holohoop/attack_grab(mob/living/user, atom/movable/victim, obj/item/hand_item/grab/grab, list/params)
. = ..()
- if(.)
+ if(!isliving(victim))
return
- if(user.pulling && isliving(user.pulling))
- var/mob/living/L = user.pulling
- if(user.grab_state < GRAB_AGGRESSIVE)
- to_chat(user, span_warning("You need a better grip to do that!"))
- return
- L.forceMove(loc)
- L.Paralyze(100)
- visible_message(span_danger("[user] dunks [L] into \the [src]!"))
- user.stop_pulling()
- else
- ..()
+ var/mob/living/L = victim
+ if(grab.current_grab.damage_stage < 1)
+ to_chat(user, span_warning("You need a better grip to do that!"))
+ return TRUE
+ L.forceMove(loc)
+ L.Paralyze(100)
+ visible_message(span_danger("[user] dunks [L] into \the [src]!"))
+ user.release_grabs(L)
+ return TRUE
/obj/structure/holohoop/hitby(atom/movable/AM, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum)
if (isitem(AM) && !istype(AM,/obj/projectile))
diff --git a/code/modules/hydroponics/hydroponics.dm b/code/modules/hydroponics/hydroponics.dm
index 89b7d1d293d4..cffffe2e88ba 100644
--- a/code/modules/hydroponics/hydroponics.dm
+++ b/code/modules/hydroponics/hydroponics.dm
@@ -1032,7 +1032,7 @@
if(user)
user.examinate(src)
-/obj/machinery/hydroponics/CtrlClick(mob/user)
+/obj/machinery/hydroponics/CtrlClick(mob/user, list/params)
. = ..()
if(!user.canUseTopic(src, USE_CLOSE|USE_IGNORE_TK))
return
@@ -1130,7 +1130,7 @@
qdel(src)
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
-/obj/machinery/hydroponics/soil/CtrlClick(mob/user)
+/obj/machinery/hydroponics/soil/CtrlClick(mob/user, list/params)
return //Soil has no electricity.
diff --git a/code/modules/jobs/job_types/_job.dm b/code/modules/jobs/job_types/_job.dm
index 6ae574caed32..5d3081ebc631 100644
--- a/code/modules/jobs/job_types/_job.dm
+++ b/code/modules/jobs/job_types/_job.dm
@@ -436,8 +436,6 @@ GLOBAL_LIST_INIT(job_display_order, list(
/// Returns an atom where the mob should spawn in.
/datum/job/proc/get_roundstart_spawn_point()
if(random_spawns_possible)
- if(HAS_TRAIT(SSstation, STATION_TRAIT_RANDOM_ARRIVALS))
- return get_safe_random_station_turf(typesof(/area/station/hallway)) || get_latejoin_spawn_point()
if(HAS_TRAIT(SSstation, STATION_TRAIT_HANGOVER))
var/obj/effect/landmark/start/hangover_spawn_point
for(var/obj/effect/landmark/start/hangover/hangover_landmark in GLOB.start_landmarks_list)
diff --git a/code/modules/mob/camera/camera.dm b/code/modules/mob/camera/camera.dm
index 77bc82db77b9..4b7bbbff0d84 100644
--- a/code/modules/mob/camera/camera.dm
+++ b/code/modules/mob/camera/camera.dm
@@ -29,14 +29,14 @@
set name = "Move Upwards"
set category = "IC"
- if(zMove(UP, z_move_flags = ZMOVE_FEEDBACK))
+ if(zstep(src, UP, ZMOVE_FEEDBACK))
to_chat(src, span_notice("You move upwards."))
/mob/camera/down()
set name = "Move Down"
set category = "IC"
- if(zMove(DOWN, z_move_flags = ZMOVE_FEEDBACK))
+ if(zstep(src, UP, ZMOVE_FEEDBACK))
to_chat(src, span_notice("You move down."))
/mob/camera/can_z_move(direction, turf/start, z_move_flags = NONE, mob/living/rider)
diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm
index ad68a81c6c42..a9218dd53829 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -242,7 +242,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
return
ghostize(FALSE)
-/mob/dead/observer/Move(NewLoc, direct, glide_size_override = 32)
+/mob/dead/observer/Move(NewLoc, direct, glide_size_override = 32, z_movement_flags)
setDir(direct)
if(glide_size_override)
diff --git a/code/modules/mob/dead/observer/observer_movement.dm b/code/modules/mob/dead/observer/observer_movement.dm
index a0338903bfac..138f33d0f2d7 100644
--- a/code/modules/mob/dead/observer/observer_movement.dm
+++ b/code/modules/mob/dead/observer/observer_movement.dm
@@ -2,7 +2,7 @@
set name = "Move Upwards"
set category = "IC"
- if(zMove(UP, z_move_flags = ZMOVE_FEEDBACK))
+ if(zstep(src, UP, ZMOVE_FEEDBACK))
to_chat(src, "You move upwards.")
/mob/dead/observer/can_z_move(direction, turf/start, z_move_flags = NONE, mob/living/rider)
diff --git a/code/modules/mob/inventory.dm b/code/modules/mob/inventory.dm
index 0987fd3f546b..29889b3f30cf 100644
--- a/code/modules/mob/inventory.dm
+++ b/code/modules/mob/inventory.dm
@@ -47,6 +47,12 @@
return has_hand_for_held_index(active_hand_index)
+/// Returns the first available empty held index
+/mob/proc/get_empty_held_index()
+ for(var/i in 1 to length(held_items))
+ if(isnull(held_items[i]))
+ return i
+
//Finds the first available (null) index OR all available (null) indexes in held_items based on a side.
//Lefts: 1, 3, 5, 7...
//Rights:2, 4, 6, 8...
@@ -162,11 +168,14 @@
held_items[hand_index] = I
I.plane = ABOVE_HUD_PLANE
I.equipped(src, ITEM_SLOT_HANDS)
+
if(QDELETED(I)) // this is here because some ABSTRACT items like slappers and circle hands could be moved from hand to hand then delete, which meant you'd have a null in your hand until you cleared it (say, by dropping it)
held_items[hand_index] = null
return FALSE
- if(I.pulledby)
- I.pulledby.stop_pulling()
+
+ if(LAZYLEN(I.grabbed_by))
+ I.free_from_all_grabs()
+
update_held_items()
I.pixel_x = I.base_pixel_x
I.pixel_y = I.base_pixel_y
diff --git a/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm b/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm
index eed38cba076a..3469d4d706c8 100644
--- a/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm
+++ b/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm
@@ -39,10 +39,13 @@ GLOBAL_LIST_INIT(strippable_alien_humanoid_items, create_strippable_list(list(
..(I, cuff_break = INSTANT_CUFFBREAK)
/mob/living/carbon/alien/humanoid/resist_grab(moving_resist)
- if(pulledby.grab_state)
- visible_message(span_danger("[src] breaks free of [pulledby]'s grip!"), \
- span_danger("You break free of [pulledby]'s grip!"))
- pulledby.stop_pulling()
+ if(LAZYLEN(grabbed_by))
+ for(var/obj/item/hand_item/grab/G in grabbed_by)
+ visible_message(
+ span_danger("[src] breaks free of [G.assailant]'s grip!"),
+ span_danger("You break free of [G.assailant]'s grip!")
+ )
+ free_from_all_grabs()
. = 0
/mob/living/carbon/alien/humanoid/get_permeability_protection(list/target_zones)
diff --git a/code/modules/mob/living/carbon/alien/larva/larva.dm b/code/modules/mob/living/carbon/alien/larva/larva.dm
index afe80cc9a4f6..0135607736b3 100644
--- a/code/modules/mob/living/carbon/alien/larva/larva.dm
+++ b/code/modules/mob/living/carbon/alien/larva/larva.dm
@@ -69,8 +69,8 @@
/mob/living/carbon/alien/larva/toggle_throw_mode()
return
-/mob/living/carbon/alien/larva/start_pulling(atom/movable/AM, state, force = move_force, supress_message = FALSE)
- return
+/mob/living/carbon/alien/larva/can_grab(atom/movable/target, target_zone, use_offhand)
+ return FALSE
/mob/living/carbon/alien/larva/canBeHandcuffed()
return TRUE
diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm
index 0a0ac287cafe..5f64d3354a02 100644
--- a/code/modules/mob/living/carbon/carbon.dm
+++ b/code/modules/mob/living/carbon/carbon.dm
@@ -151,45 +151,59 @@
var/atom/movable/thrown_thing
var/obj/item/I = get_active_held_item()
+ if(isnull(I))
+ return
+
var/neckgrab_throw = FALSE // we can't check for if it's a neckgrab throw when totaling up power_throw since we've already stopped pulling them by then, so get it early
- if(!I)
- if(pulling && isliving(pulling) && grab_state >= GRAB_AGGRESSIVE)
- var/mob/living/throwable_mob = pulling
+ if(isgrab(I))
+ var/obj/item/hand_item/grab/G = I
+ if(isitem(G.affecting))
+ I = G.affecting
+ thrown_thing = I.on_thrown(src, target)
+ if(!thrown_thing)
+ return
+ release_grabs(I)
+ else
+ if(!G.current_grab.can_throw || !isliving(G.affecting))
+ return
+
+ var/mob/living/throwable_mob = G.affecting
if(!throwable_mob.buckled)
thrown_thing = throwable_mob
- if(grab_state >= GRAB_NECK)
+ if(G.current_grab.damage_stage >= GRAB_NECK)
neckgrab_throw = TRUE
- stop_pulling()
+ release_grabs(throwable_mob)
if(HAS_TRAIT(src, TRAIT_PACIFISM))
to_chat(src, span_notice("You gently let go of [throwable_mob]."))
return
else
thrown_thing = I.on_thrown(src, target)
- if(thrown_thing)
-
- if(isliving(thrown_thing))
- var/turf/start_T = get_turf(loc) //Get the start and target tile for the descriptors
- var/turf/end_T = get_turf(target)
- if(start_T && end_T)
- log_combat(src, thrown_thing, "thrown", addition="grab from tile in [AREACOORD(start_T)] towards tile at [AREACOORD(end_T)]")
- var/power_throw = 0
- if(HAS_TRAIT(src, TRAIT_HULK))
- power_throw++
- if(HAS_TRAIT(src, TRAIT_DWARF))
- power_throw--
- if(HAS_TRAIT(thrown_thing, TRAIT_DWARF))
- power_throw++
- if(neckgrab_throw)
- power_throw++
- do_attack_animation(target, no_effect = TRUE) //PARIAH EDIT ADDITION - AESTHETICS
- playsound(loc, 'sound/weapons/punchmiss.ogg', 50, TRUE, -1) //PARIAH EDIT ADDITION - AESTHETICS
- visible_message(span_danger("[src] throws [thrown_thing][power_throw ? " really hard!" : "."]"), \
- span_danger("You throw [thrown_thing][power_throw ? " really hard!" : "."]"))
- log_message("has thrown [thrown_thing] [power_throw ? "really hard" : ""]", LOG_ATTACK)
- newtonian_move(get_dir(target, src))
- thrown_thing.safe_throw_at(target, thrown_thing.throw_range, thrown_thing.throw_speed + power_throw, src, null, null, null, move_force)
+ if(!thrown_thing)
+ return
+
+ if(isliving(thrown_thing))
+ var/turf/start_T = get_turf(loc) //Get the start and target tile for the descriptors
+ var/turf/end_T = get_turf(target)
+ if(start_T && end_T)
+ log_combat(src, thrown_thing, "thrown", addition="grab from tile in [AREACOORD(start_T)] towards tile at [AREACOORD(end_T)]")
+ var/power_throw = 0
+ if(HAS_TRAIT(src, TRAIT_HULK))
+ power_throw++
+ if(HAS_TRAIT(src, TRAIT_DWARF))
+ power_throw--
+ if(HAS_TRAIT(thrown_thing, TRAIT_DWARF))
+ power_throw++
+ if(neckgrab_throw)
+ power_throw++
+ do_attack_animation(target, no_effect = TRUE) //PARIAH EDIT ADDITION - AESTHETICS
+ playsound(loc, 'sound/weapons/punchmiss.ogg', 50, TRUE, -1) //PARIAH EDIT ADDITION - AESTHETICS
+ visible_message(span_danger("[src] throws [thrown_thing][power_throw ? " really hard!" : "."]"), \
+ span_danger("You throw [thrown_thing][power_throw ? " really hard!" : "."]"))
+ log_message("has thrown [thrown_thing] [power_throw ? "really hard" : ""]", LOG_ATTACK)
+ newtonian_move(get_dir(target, src))
+ thrown_thing.safe_throw_at(target, thrown_thing.throw_range, thrown_thing.throw_speed + power_throw, src, null, null, null, move_force)
/mob/living/carbon/proc/canBeHandcuffed()
@@ -776,7 +790,7 @@
/mob/living/carbon/proc/update_handcuffed()
if(handcuffed)
drop_all_held_items()
- stop_pulling()
+ release_all_grabs()
throw_alert(ALERT_HANDCUFFED, /atom/movable/screen/alert/restrained/handcuffed, new_master = src.handcuffed)
else
clear_alert(ALERT_HANDCUFFED)
diff --git a/code/modules/mob/living/carbon/carbon_context.dm b/code/modules/mob/living/carbon/carbon_context.dm
index 3ea764aa043f..2577f15a55dd 100644
--- a/code/modules/mob/living/carbon/carbon_context.dm
+++ b/code/modules/mob/living/carbon/carbon_context.dm
@@ -1,7 +1,7 @@
/mob/living/carbon/add_context(atom/source, list/context, obj/item/held_item, mob/user)
. = ..()
- if (!isnull(held_item))
+ if (!isnull(held_item) && !(held_item.item_flags & (ABSTRACT|HAND_ITEM)))
context[SCREENTIP_CONTEXT_CTRL_SHIFT_LMB] = "Offer item"
return CONTEXTUAL_SCREENTIP_SET
@@ -15,11 +15,8 @@
else if (human_user == src)
context[SCREENTIP_CONTEXT_LMB] = "Check injuries"
- if (get_bodypart(human_user.zone_selected)?.get_modified_bleed_rate())
- context[SCREENTIP_CONTEXT_CTRL_LMB] = "Grab limb"
-
if (human_user != src)
- context[SCREENTIP_CONTEXT_RMB] = "Shove"
+ context[SCREENTIP_CONTEXT_RMB] = "Disarm"
if (!human_user.combat_mode)
if (body_position == STANDING_UP)
diff --git a/code/modules/mob/living/carbon/carbon_defense.dm b/code/modules/mob/living/carbon/carbon_defense.dm
index ca447b99bb32..0a9de6b65671 100644
--- a/code/modules/mob/living/carbon/carbon_defense.dm
+++ b/code/modules/mob/living/carbon/carbon_defense.dm
@@ -158,7 +158,7 @@
if(D.spread_flags & DISEASE_SPREAD_CONTACT_SKIN)
ContactContractDisease(D)
- return FALSE
+ return . || FALSE
/mob/living/carbon/attack_paw(mob/living/carbon/human/user, list/modifiers)
@@ -290,8 +290,7 @@
COMBAT_MESSAGE_RANGE,
)
log_combat(src, target, "shoved", "knocking them down")
- target.pulledby?.stop_pulling()
- target.stop_pulling()
+ target.release_all_grabs()
return
if(target.IsKnockdown()) //KICK HIM IN THE NUTS //That is harm intent.
@@ -363,10 +362,9 @@
if(undergoing_cardiac_arrest())
set_heartattack(FALSE)
var/list/shocking_queue = list()
- if(iscarbon(pulling) && source != pulling)
- shocking_queue += pulling
- if(iscarbon(pulledby) && source != pulledby)
- shocking_queue += pulledby
+ shocking_queue += get_all_grabbed_movables()
+ shocking_queue -= source
+
if(iscarbon(buckled) && source != buckled)
shocking_queue += buckled
for(var/mob/living/carbon/carried in buckled_mobs)
@@ -603,77 +601,3 @@
var/obj/item/bodypart/limb = _limb
if (!IS_ORGANIC_LIMB(limb))
. += (limb.brute_dam) + (limb.burn_dam)
-
-/mob/living/carbon/grabbedby(mob/living/carbon/user, supress_message = FALSE)
- if(user != src)
- return ..()
-
- var/obj/item/bodypart/grasped_part = get_bodypart(zone_selected)
- if(!grasped_part?.get_modified_bleed_rate())
- return
- var/starting_hand_index = active_hand_index
- if(starting_hand_index == grasped_part.held_index)
- to_chat(src, span_danger("You can't grasp your [grasped_part.name] with itself!"))
- return
-
- to_chat(src, span_warning("You try grasping at your [grasped_part.name], trying to stop the bleeding..."))
- if(!do_after(src, time = 0.75 SECONDS))
- to_chat(src, span_danger("You fail to grasp your [grasped_part.name]."))
- return
-
- var/obj/item/hand_item/self_grasp/grasp = new
- if(starting_hand_index != active_hand_index || !put_in_active_hand(grasp))
- to_chat(src, span_danger("You fail to grasp your [grasped_part.name]."))
- QDEL_NULL(grasp)
- return
- grasp.grasp_limb(grasped_part)
-
-/// an abstract item representing you holding your own limb to staunch the bleeding, see [/mob/living/carbon/proc/grabbedby] will probably need to find somewhere else to put this.
-/obj/item/hand_item/self_grasp
- name = "self-grasp"
- desc = "Sometimes all you can do is slow the bleeding."
- icon_state = "latexballon"
- inhand_icon_state = "nothing"
- slowdown = 0.5
- item_flags = DROPDEL | ABSTRACT | NOBLUDGEON | SLOWS_WHILE_IN_HAND | HAND_ITEM
- /// The bodypart we're staunching bleeding on, which also has a reference to us in [/obj/item/bodypart/var/grasped_by]
- var/obj/item/bodypart/grasped_part
- /// The carbon who owns all of this mess
- var/mob/living/carbon/user
-
-/obj/item/hand_item/self_grasp/Destroy()
- if(user)
- to_chat(user, span_warning("You stop holding onto your[grasped_part ? " [grasped_part.name]" : "self"]."))
- UnregisterSignal(user, COMSIG_PARENT_QDELETING)
- if(grasped_part)
- UnregisterSignal(grasped_part, list(COMSIG_CARBON_REMOVED_LIMB, COMSIG_PARENT_QDELETING))
- grasped_part.grasped_by = null
- grasped_part.refresh_bleed_rate()
- grasped_part = null
- user = null
- return ..()
-
-/// The limb or the whole damn person we were grasping got deleted or dismembered, so we don't care anymore
-/obj/item/hand_item/self_grasp/proc/qdel_void()
- SIGNAL_HANDLER
- qdel(src)
-
-/// We've already cleared that the bodypart in question is bleeding in [the place we create this][/mob/living/carbon/proc/grabbedby], so set up the connections
-/obj/item/hand_item/self_grasp/proc/grasp_limb(obj/item/bodypart/grasping_part)
- user = grasping_part.owner
- if(!istype(user))
- stack_trace("[src] attempted to try_grasp() with [istype(user, /datum) ? user.type : isnull(user) ? "null" : user] user")
- qdel(src)
- return
-
- grasped_part = grasping_part
- grasped_part.grasped_by = src
- grasped_part.refresh_bleed_rate()
- RegisterSignal(user, COMSIG_PARENT_QDELETING, PROC_REF(qdel_void))
- RegisterSignal(grasped_part, list(COMSIG_CARBON_REMOVED_LIMB, COMSIG_PARENT_QDELETING), PROC_REF(qdel_void))
-
- user.visible_message(span_danger("[user] grasps at [user.p_their()] [grasped_part.name], trying to stop the bleeding."), span_notice("You grab hold of your [grasped_part.name] tightly."), vision_distance=COMBAT_MESSAGE_RANGE)
- playsound(get_turf(src), 'sound/weapons/thudswoosh.ogg', 50, TRUE, -1)
- return TRUE
-
-#undef SHAKE_ANIMATION_OFFSET
diff --git a/code/modules/mob/living/carbon/carbon_movement.dm b/code/modules/mob/living/carbon/carbon_movement.dm
index 980a01980117..343c96b813a1 100644
--- a/code/modules/mob/living/carbon/carbon_movement.dm
+++ b/code/modules/mob/living/carbon/carbon_movement.dm
@@ -6,7 +6,7 @@
..()
return loc.handle_slip(src, knockdown_amount, slipped_on, lube_flags, paralyze, force_drop)
-/mob/living/carbon/Move(NewLoc, direct)
+/mob/living/carbon/Move(NewLoc, direct, glide_size_override, z_movement_flags)
. = ..()
if(!(usr == src))
return
diff --git a/code/modules/mob/living/carbon/examine.dm b/code/modules/mob/living/carbon/examine.dm
index dd7e7053b9e8..03e406ec651c 100644
--- a/code/modules/mob/living/carbon/examine.dm
+++ b/code/modules/mob/living/carbon/examine.dm
@@ -98,9 +98,6 @@
if(has_status_effect(/datum/status_effect/fire_handler/wet_stacks))
msg += "[t_He] look[p_s()] a little soaked.\n"
- if(pulledby?.grab_state)
- msg += "[t_He] [t_is] restrained by [pulledby]'s grip.\n"
-
msg += ""
. += msg.Join("")
diff --git a/code/modules/mob/living/carbon/human/examine.dm b/code/modules/mob/living/carbon/human/examine.dm
index 613d36df97ae..f84712ae80cc 100644
--- a/code/modules/mob/living/carbon/human/examine.dm
+++ b/code/modules/mob/living/carbon/human/examine.dm
@@ -208,8 +208,13 @@
msg += "[t_He] look[p_s()] a little soaked.\n"
- if(pulledby?.grab_state)
- msg += "[t_He] [t_is] restrained by [pulledby]'s grip.\n"
+ for(var/obj/item/hand_item/grab/G in grabbed_by)
+ if(G.assailant == src)
+ msg += "[t_He] [t_is] gripping [t_His] [G.get_targeted_bodypart().plaintext_zone].\n"
+ continue
+ if(!G.current_grab.stop_move)
+ continue
+ msg += "[t_He] [t_is] restrained by [G.assailant]'s grip.\n"
if(nutrition < NUTRITION_LEVEL_STARVING - 50)
msg += "[t_He] [t_is] severely malnourished.\n"
@@ -237,17 +242,6 @@
if(-INFINITY to BLOOD_VOLUME_BAD)
msg += "[span_deadsay("[t_He] resemble[p_s()] a crushed, empty juice pouch.")]\n"
- if(is_bleeding())
- var/list/obj/item/bodypart/grasped_limbs = list()
-
- for(var/obj/item/bodypart/body_part as anything in bodyparts)
- if(body_part.grasped_by)
- grasped_limbs += body_part
-
- for(var/i in grasped_limbs)
- var/obj/item/bodypart/grasped_part = i
- msg += "[t_He] [t_is] holding [t_his] [grasped_part.name] to slow the bleeding!\n"
-
if(islist(stun_absorption))
for(var/i in stun_absorption)
if(stun_absorption[i]["end_time"] > world.time && stun_absorption[i]["examine_message"])
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index 4b9f33232d49..171d99721566 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -763,7 +763,9 @@
for(var/obj/item/bodypart/BP as anything in bodyparts)
BP.set_sever_artery(FALSE)
BP.set_sever_tendon(FALSE)
+ BP.set_dislocated(FALSE)
BP.heal_bones()
+ BP.adjustPain(-INFINITY)
remove_all_embedded_objects()
set_heartattack(FALSE)
@@ -852,7 +854,8 @@
set_species(newtype)
/mob/living/carbon/human/mouse_buckle_handling(mob/living/M, mob/living/user)
- if(pulling != M || grab_state != GRAB_AGGRESSIVE || stat != CONSCIOUS)
+ var/obj/item/hand_item/grab/G = is_grabbing(M)
+ if(!G || G.current_grab.damage_stage != GRAB_AGGRESSIVE || stat != CONSCIOUS)
return FALSE
//If they dragged themselves to you and you're currently aggressively grabbing them try to piggyback
@@ -1131,3 +1134,29 @@
return FALSE
return dna.species.organs[slot]
+
+//Used by various things that knock people out by applying blunt trauma to the head.
+//Checks that the species has a "head" (brain containing organ) and that hit_zone refers to it.
+/mob/living/carbon/human/proc/can_head_trauma_ko()
+ var/obj/item/organ/brain = getorganslot(ORGAN_SLOT_BRAIN)
+ if(!brain || !needs_organ(ORGAN_SLOT_BRAIN))
+ return FALSE
+
+ //if the parent organ is significantly larger than the brain organ, then hitting it is not guaranteed
+ var/obj/item/bodypart/head = get_bodypart(BODY_ZONE_HEAD)
+ if(!head)
+ return FALSE
+ if(head.w_class > brain.w_class + 1)
+ return prob(100 / 2**(head.w_class - brain.w_class - 1))
+ return TRUE
+
+/mob/living/carbon/human/up()
+ . = ..()
+ if(.)
+ return
+ var/obj/climbable = check_zclimb()
+ if(!climbable)
+ can_z_move(UP, get_turf(src), ZMOVE_FEEDBACK|ZMOVE_FLIGHT_FLAGS)
+ return FALSE
+
+ return ClimbUp(climbable)
diff --git a/code/modules/mob/living/carbon/human/human_context.dm b/code/modules/mob/living/carbon/human/human_context.dm
index 56dc9040bbd8..eaaac631a911 100644
--- a/code/modules/mob/living/carbon/human/human_context.dm
+++ b/code/modules/mob/living/carbon/human/human_context.dm
@@ -7,17 +7,10 @@
if (user == src)
return .
- if (pulledby == user)
- switch (user.grab_state)
- if (GRAB_PASSIVE)
- context[SCREENTIP_CONTEXT_CTRL_LMB] = "Grip"
- if (GRAB_AGGRESSIVE)
- context[SCREENTIP_CONTEXT_CTRL_LMB] = "Choke"
- if (GRAB_NECK)
- context[SCREENTIP_CONTEXT_CTRL_LMB] = "Strangle"
- else
- return .
- else
- context[SCREENTIP_CONTEXT_CTRL_LMB] = "Pull"
+ context[SCREENTIP_CONTEXT_CTRL_LMB] = "Grab"
+
+ var/obj/item/hand_item/grab/G = user.is_grabbing(src)
+ if (G)
+ G.current_grab.add_context(context, held_item, user, src)
return CONTEXTUAL_SCREENTIP_SET
diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm
index 43730ea1e6b2..bf7961570ca0 100644
--- a/code/modules/mob/living/carbon/human/human_defense.dm
+++ b/code/modules/mob/living/carbon/human/human_defense.dm
@@ -163,12 +163,6 @@
return ..()
-/mob/living/carbon/human/grippedby(mob/living/user, instant = FALSE)
- if(w_uniform)
- w_uniform.add_fingerprint(user)
- ..()
-
-
/mob/living/carbon/human/attacked_by(obj/item/I, mob/living/user)
if(!I || !user)
return MOB_ATTACKEDBY_FAIL
diff --git a/code/modules/mob/living/carbon/human/human_movement.dm b/code/modules/mob/living/carbon/human/human_movement.dm
index eba4d1e9145f..4dcd5a3b3a95 100644
--- a/code/modules/mob/living/carbon/human/human_movement.dm
+++ b/code/modules/mob/living/carbon/human/human_movement.dm
@@ -25,7 +25,7 @@
if(dna.species.negates_gravity(src) || ..())
return TRUE
-/mob/living/carbon/human/Move(NewLoc, direct)
+/mob/living/carbon/human/Move(NewLoc, direct, glide_size_override, z_movement_flags)
. = ..()
if(shoes && body_position == STANDING_UP && loc == NewLoc && has_gravity(loc))
SEND_SIGNAL(shoes, COMSIG_SHOES_STEP_ACTION)
diff --git a/code/modules/mob/living/carbon/human/human_stripping.dm b/code/modules/mob/living/carbon/human/human_stripping.dm
index b43915faf779..bbbbf0835669 100644
--- a/code/modules/mob/living/carbon/human/human_stripping.dm
+++ b/code/modules/mob/living/carbon/human/human_stripping.dm
@@ -23,12 +23,13 @@ GLOBAL_LIST_INIT(strippable_human_items, create_strippable_list(list(
/datum/strippable_item/mob_item_slot/legcuffs,
)))
-/mob/living/carbon/human/should_strip(mob/user)
+/mob/living/carbon/human/should_strip(mob/living/user)
. = ..()
if (!.)
return FALSE
- if (user.grab_state != GRAB_AGGRESSIVE)
+ var/obj/item/hand_item/grab/G = user.is_grabbing(src)
+ if (G && G.current_grab.damage_stage != GRAB_AGGRESSIVE)
return TRUE
if (ishuman(user))
diff --git a/code/modules/mob/living/carbon/human/inventory.dm b/code/modules/mob/living/carbon/human/inventory.dm
index c5f0afa51615..7fc1eef67643 100644
--- a/code/modules/mob/living/carbon/human/inventory.dm
+++ b/code/modules/mob/living/carbon/human/inventory.dm
@@ -184,7 +184,7 @@
update_worn_undersuit()
if(wear_suit.breakouttime) //when equipping a straightjacket
ADD_TRAIT(src, TRAIT_RESTRAINED, SUIT_TRAIT)
- stop_pulling() //can't pull if restrained
+ release_all_grabs() //can't pull if restrained
update_mob_action_buttons() //certain action buttons will no longer be usable.
update_worn_oversuit()
if(ITEM_SLOT_ICLOTHING)
diff --git a/code/modules/mob/living/carbon/human/species.dm b/code/modules/mob/living/carbon/human/species.dm
index 0036cb65704e..28792e3b5035 100644
--- a/code/modules/mob/living/carbon/human/species.dm
+++ b/code/modules/mob/living/carbon/human/species.dm
@@ -952,7 +952,7 @@ GLOBAL_LIST_EMPTY(features_by_species)
user.do_cpr(target)
-/datum/species/proc/grab(mob/living/carbon/human/user, mob/living/carbon/human/target, datum/martial_art/attacker_style)
+/datum/species/proc/grab(mob/living/carbon/human/user, mob/living/carbon/human/target, datum/martial_art/attacker_style, list/params)
if(target.check_block())
target.visible_message(span_warning("[target] blocks [user]'s grab!"), \
span_userdanger("You block [user]'s grab!"), span_hear("You hear a swoosh!"), COMBAT_MESSAGE_RANGE, user)
@@ -961,7 +961,7 @@ GLOBAL_LIST_EMPTY(features_by_species)
if(attacker_style?.grab_act(user,target) == MARTIAL_ATTACK_SUCCESS)
return TRUE
else
- target.grabbedby(user)
+ user.try_make_grab(target, use_offhand = params?[RIGHT_CLICK])
return TRUE
///This proc handles punching damage.
diff --git a/code/modules/mob/living/carbon/human/species_types/jellypeople.dm b/code/modules/mob/living/carbon/human/species_types/jellypeople.dm
index 6e1930f90e96..da26471014dc 100644
--- a/code/modules/mob/living/carbon/human/species_types/jellypeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/jellypeople.dm
@@ -746,11 +746,13 @@
return TRUE
/datum/action/innate/link_minds/Activate()
- if(!isliving(owner.pulling) || owner.grab_state < GRAB_AGGRESSIVE)
+ var/mob/living/L = owner
+ var/obj/item/hand_item/grab/G = L.get_active_grab()
+ if(!isliving(G?.affecting) || G.current_grab.damage_stage < GRAB_AGGRESSIVE)
to_chat(owner, span_warning("You need to aggressively grab someone to link minds!"))
return
- var/mob/living/living_target = owner.pulling
+ var/mob/living/living_target = G.affecting
if(living_target.stat == DEAD)
to_chat(owner, span_warning("They're dead!"))
return
@@ -779,11 +781,9 @@
/datum/action/innate/link_minds/proc/while_link_callback(mob/living/linkee)
if(!is_species(owner, req_species))
return FALSE
- if(!owner.pulling)
- return FALSE
- if(owner.pulling != linkee)
- return FALSE
- if(owner.grab_state < GRAB_AGGRESSIVE)
+ var/mob/living/L = owner
+ var/obj/item/hand_item/grab/G = L.is_grabbing(linkee)
+ if(!G || G.current_grab.damage_stage < GRAB_AGGRESSIVE)
return FALSE
if(linkee.stat == DEAD)
return FALSE
diff --git a/code/modules/mob/living/carbon/human/species_types/vampire.dm b/code/modules/mob/living/carbon/human/species_types/vampire.dm
index 92a96b6c7e7e..650b8ea0edb7 100644
--- a/code/modules/mob/living/carbon/human/species_types/vampire.dm
+++ b/code/modules/mob/living/carbon/human/species_types/vampire.dm
@@ -176,37 +176,48 @@
if(!COOLDOWN_FINISHED(V, drain_cooldown))
to_chat(H, span_warning("You just drained blood, wait a few seconds!"))
return
- if(H.pulling && iscarbon(H.pulling))
- var/mob/living/carbon/victim = H.pulling
- if(H.blood_volume >= BLOOD_VOLUME_MAXIMUM)
- to_chat(H, span_warning("You're already full!"))
- return
- if(victim.stat == DEAD)
- to_chat(H, span_warning("You need a living victim!"))
- return
- if(!victim.blood_volume || (victim.dna && ((NOBLOOD in victim.dna.species.species_traits) || victim.dna.species.exotic_blood)))
- to_chat(H, span_warning("[victim] doesn't have blood!"))
- return
- COOLDOWN_START(V, drain_cooldown, 3 SECONDS)
- if(victim.can_block_magic(MAGIC_RESISTANCE_HOLY, charge_cost = 0))
- victim.show_message(span_warning("[H] tries to bite you, but stops before touching you!"))
- to_chat(H, span_warning("[victim] is blessed! You stop just in time to avoid catching fire."))
- return
- if(victim.has_reagent(/datum/reagent/consumable/garlic))
- victim.show_message(span_warning("[H] tries to bite you, but recoils in disgust!"))
- to_chat(H, span_warning("[victim] reeks of garlic! you can't bring yourself to drain such tainted blood."))
- return
- if(!do_after(H, victim, 3 SECONDS))
- return
- var/blood_volume_difference = BLOOD_VOLUME_MAXIMUM - H.blood_volume //How much capacity we have left to absorb blood
- var/drained_blood = min(victim.blood_volume, VAMP_DRAIN_AMOUNT, blood_volume_difference)
- victim.show_message(span_danger("[H] is draining your blood!"))
- to_chat(H, span_notice("You drain some blood!"))
- playsound(H, 'sound/items/drink.ogg', 30, TRUE, -2)
- victim.blood_volume = clamp(victim.blood_volume - drained_blood, 0, BLOOD_VOLUME_MAXIMUM)
- H.blood_volume = clamp(H.blood_volume + drained_blood, 0, BLOOD_VOLUME_MAXIMUM)
- if(!victim.blood_volume)
- to_chat(H, span_notice("You finish off [victim]'s blood supply."))
+ var/obj/item/hand_item/grab/G = H.get_active_grab()
+ if(!iscarbon(G?.affecting))
+ return
+
+ var/mob/living/carbon/victim = G.affecting
+ if(H.blood_volume >= BLOOD_VOLUME_MAXIMUM)
+ to_chat(H, span_warning("You're already full!"))
+ return
+
+ if(victim.stat == DEAD)
+ to_chat(H, span_warning("You need a living victim!"))
+ return
+
+ if(!victim.blood_volume || (victim.dna && ((NOBLOOD in victim.dna.species.species_traits) || victim.dna.species.exotic_blood)))
+ to_chat(H, span_warning("[victim] doesn't have blood!"))
+ return
+
+ COOLDOWN_START(V, drain_cooldown, 3 SECONDS)
+ if(victim.can_block_magic(MAGIC_RESISTANCE_HOLY, charge_cost = 0))
+ victim.show_message(span_warning("[H] tries to bite you, but stops before touching you!"))
+ to_chat(H, span_warning("[victim] is blessed! You stop just in time to avoid catching fire."))
+ return
+
+ if(victim.has_reagent(/datum/reagent/consumable/garlic))
+ victim.show_message(span_warning("[H] tries to bite you, but recoils in disgust!"))
+ to_chat(H, span_warning("[victim] reeks of garlic! you can't bring yourself to drain such tainted blood."))
+ return
+
+ if(!do_after(H, victim, 3 SECONDS))
+ return
+
+ var/blood_volume_difference = BLOOD_VOLUME_MAXIMUM - H.blood_volume //How much capacity we have left to absorb blood
+ var/drained_blood = min(victim.blood_volume, VAMP_DRAIN_AMOUNT, blood_volume_difference)
+
+ victim.show_message(span_danger("[H] is draining your blood!"))
+ to_chat(H, span_notice("You drain some blood!"))
+ playsound(H, 'sound/items/drink.ogg', 30, TRUE, -2)
+
+ victim.blood_volume = clamp(victim.blood_volume - drained_blood, 0, BLOOD_VOLUME_MAXIMUM)
+ H.blood_volume = clamp(H.blood_volume + drained_blood, 0, BLOOD_VOLUME_MAXIMUM)
+ if(!victim.blood_volume)
+ to_chat(H, span_notice("You finish off [victim]'s blood supply."))
/obj/item/organ/heart/vampire
name = "vampire heart"
diff --git a/code/modules/mob/living/carbon/inventory.dm b/code/modules/mob/living/carbon/inventory.dm
index 4c932b08c75f..c9b50fb3f94c 100644
--- a/code/modules/mob/living/carbon/inventory.dm
+++ b/code/modules/mob/living/carbon/inventory.dm
@@ -67,8 +67,8 @@
if(index)
held_items[index] = null
- if(I.pulledby)
- I.pulledby.stop_pulling()
+ if(LAZYLEN(I.grabbed_by))
+ I.free_from_all_grabs()
I.screen_loc = null
if(client)
@@ -299,3 +299,11 @@
covered_flags |= worn_item.body_parts_covered
return covered_flags
+
+///Returns an item that is covering a bodypart.
+/mob/living/carbon/proc/get_item_covering_bodypart(obj/item/bodypart/BP)
+ return get_item_covering_zone(body_zone2cover_flags(BP.body_zone))
+
+///Returns an item that is covering a body_zone (BODY_ZONE_CHEST, etc)
+/mob/living/carbon/proc/get_item_covering_zone(zone)
+ for(var/obj/item in get_all_worn_items())
diff --git a/code/modules/mob/living/carbon/life.dm b/code/modules/mob/living/carbon/life.dm
index 41d28aab82c1..5c0e3cf571c8 100644
--- a/code/modules/mob/living/carbon/life.dm
+++ b/code/modules/mob/living/carbon/life.dm
@@ -83,7 +83,7 @@
losebreath = max(2, losebreath + 1)
else if(!getorganslot(ORGAN_SLOT_BREATHING_TUBE))
- if((pulledby?.grab_state >= GRAB_KILL) || (lungs?.organ_flags & ORGAN_DEAD))
+ if(HAS_TRAIT(src, TRAIT_KILL_GRAB) || (lungs?.organ_flags & ORGAN_DEAD))
losebreath++ //You can't breath at all when in critical or when being choked, so you're going to miss a breath
// Recover from breath loss
diff --git a/code/modules/mob/living/death.dm b/code/modules/mob/living/death.dm
index 09423e5001a9..ba82fda8f6c1 100644
--- a/code/modules/mob/living/death.dm
+++ b/code/modules/mob/living/death.dm
@@ -88,7 +88,7 @@
update_health_hud()
med_hud_set_health()
med_hud_set_status()
- stop_pulling()
+ release_all_grabs()
set_ssd_indicator(FALSE)
set_typing_indicator(FALSE)
diff --git a/code/modules/mob/living/init_signals.dm b/code/modules/mob/living/init_signals.dm
index e832d5d770ff..ca539e53f0a1 100644
--- a/code/modules/mob/living/init_signals.dm
+++ b/code/modules/mob/living/init_signals.dm
@@ -154,8 +154,7 @@
/mob/living/proc/on_pull_blocked_trait_gain(datum/source)
SIGNAL_HANDLER
mobility_flags &= ~(MOBILITY_PULL)
- if(pulling)
- stop_pulling()
+ release_all_grabs()
/// Called when [TRAIT_PULL_BLOCKED] is removed from the mob.
/mob/living/proc/on_pull_blocked_trait_loss(datum/source)
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index cd7b49d8e5e3..9b911267e0f4 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -133,18 +133,20 @@
ContactContractDisease(D)
//Should stop you pushing a restrained person out of the way
- if(L.pulledby && L.pulledby != src && HAS_TRAIT(L, TRAIT_RESTRAINED))
+ if(LAZYLEN(L.grabbed_by) && !is_grabbing(L) && HAS_TRAIT(L, TRAIT_RESTRAINED))
if(!(world.time % 5))
to_chat(src, span_warning("[L] is restrained, you cannot push past."))
return TRUE
- if(L.pulling)
- if(ismob(L.pulling))
- var/mob/P = L.pulling
- if(HAS_TRAIT(P, TRAIT_RESTRAINED))
- if(!(world.time % 5))
- to_chat(src, span_warning("[L] is restraining [P], you cannot push past."))
- return TRUE
+ var/list/grabs = L.get_active_grabs()
+ if(length(grabs))
+ for(var/obj/item/hand_item/grab/G in grabs)
+ if(ismob(G.affecting))
+ var/mob/P = G.affecting
+ if(HAS_TRAIT(P, TRAIT_RESTRAINED))
+ if(!(world.time % 5))
+ to_chat(src, span_warning("[L] is restraining [P], you cannot push past."))
+ return TRUE
if(moving_diagonally)//no mob swap during diagonal moves.
return TRUE
@@ -157,7 +159,7 @@
mob_swap = TRUE
else
//You can swap with the person you are dragging on grab intent, and restrained people in most cases
- if(M.pulledby == src && !too_strong)
+ if(is_grabbing(M) && !too_strong)
mob_swap = TRUE
else if(
!(HAS_TRAIT(M, TRAIT_NOMOBSWAP) || HAS_TRAIT(src, TRAIT_NOMOBSWAP))&&\
@@ -167,32 +169,18 @@
mob_swap = TRUE
if(mob_swap)
//switch our position with M
- if(loc && !loc.Adjacent(M.loc))
+ if(loc && !loc.MultiZAdjacent(M.loc))
return TRUE
+
now_pushing = TRUE
+
var/oldloc = loc
var/oldMloc = M.loc
-
-
- var/M_passmob = (M.pass_flags & PASSMOB) // we give PASSMOB to both mobs to avoid bumping other mobs during swap.
- var/src_passmob = (pass_flags & PASSMOB)
- M.pass_flags |= PASSMOB
- pass_flags |= PASSMOB
-
- var/move_failed = FALSE
- if(!M.Move(oldloc) || !Move(oldMloc))
- M.forceMove(oldMloc)
- forceMove(oldloc)
- move_failed = TRUE
- if(!src_passmob)
- pass_flags &= ~PASSMOB
- if(!M_passmob)
- M.pass_flags &= ~PASSMOB
+ forceMove(oldMloc)
+ M.forceMove(oldloc)
now_pushing = FALSE
-
- if(!move_failed)
- return TRUE
+ return TRUE
//okay, so we didn't switch. but should we push?
//not if he's not CANPUSH of course
@@ -283,8 +271,9 @@
for(var/obj/structure/window/win in get_step(W, dir_to_target))
now_pushing = FALSE
return
- if(pulling == AM)
- stop_pulling()
+
+ release_grabs(AM)
+
var/current_dir
if(isliving(AM))
current_dir = AM.dir
@@ -295,128 +284,10 @@
AM.setDir(current_dir)
now_pushing = FALSE
-/mob/living/start_pulling(atom/movable/AM, state, force = pull_force, supress_message = FALSE)
- if(!AM || !src)
- return FALSE
- if(!(AM.can_be_pulled(src, state, force)))
- return FALSE
- if(throwing || !(mobility_flags & MOBILITY_PULL))
- return FALSE
- if(SEND_SIGNAL(src, COMSIG_LIVING_TRY_PULL, AM, force) & COMSIG_LIVING_CANCEL_PULL)
- return FALSE
-
- AM.add_fingerprint(src)
-
- // If we're pulling something then drop what we're currently pulling and pull this instead.
- if(pulling)
- // Are we trying to pull something we are already pulling? Then just stop here, no need to continue.
- if(AM == pulling)
- return
- stop_pulling()
-
- changeNext_move(CLICK_CD_GRABBING)
- animate_interact(AM, INTERACT_PULL)
-
- if(AM.pulledby)
- if(!supress_message)
- AM.visible_message(span_danger("[src] pulls [AM] from [AM.pulledby]'s grip."), \
- span_danger("[src] pulls you from [AM.pulledby]'s grip."), null, null, src)
- to_chat(src, span_notice("You pull [AM] from [AM.pulledby]'s grip!"))
- log_combat(AM, AM.pulledby, "pulled from", src)
- AM.pulledby.stop_pulling() //an object can't be pulled by two mobs at once.
-
- pulling = AM
- AM.set_pulledby(src)
-
- SEND_SIGNAL(src, COMSIG_LIVING_START_PULL, AM, state, force)
-
- if(!supress_message)
- var/sound_to_play = 'sound/weapons/thudswoosh.ogg'
- if(ishuman(src))
- var/mob/living/carbon/human/H = src
- if(H.dna.species.grab_sound)
- sound_to_play = H.dna.species.grab_sound
- if(HAS_TRAIT(H, TRAIT_STRONG_GRABBER))
- sound_to_play = null
- playsound(src.loc, sound_to_play, 50, TRUE, -1)
- update_pull_hud_icon()
-
- if(ismob(AM))
- var/mob/M = AM
-
- log_combat(src, M, "grabbed", addition="passive grab")
- if(!supress_message && !(iscarbon(AM) && HAS_TRAIT(src, TRAIT_STRONG_GRABBER)))
- if(ishuman(M))
- var/mob/living/carbon/human/grabbed_human = M
- var/grabbed_by_hands = (zone_selected == "l_arm" || zone_selected == "r_arm") && grabbed_human.usable_hands > 0
- M.visible_message(span_warning("[src] grabs [M] [grabbed_by_hands ? "by their hands":"passively"]!"), \
- span_warning("[src] grabs you [grabbed_by_hands ? "by your hands":"passively"]!"), null, null, src)
- to_chat(src, span_notice("You grab [M] [grabbed_by_hands ? "by their hands":"passively"]!"))
- else
- M.visible_message(span_warning("[src] grabs [M] passively!"), \
- span_warning("[src] grabs you passively!"), null, null, src)
- to_chat(src, span_notice("You grab [M] passively!"))
-
- if(!iscarbon(src))
- M.LAssailant = null
- else
- M.LAssailant = WEAKREF(usr)
- if(isliving(M))
- var/mob/living/L = M
-
- SEND_SIGNAL(M, COMSIG_LIVING_GET_PULLED, src)
- //Share diseases that are spread by touch
- for(var/thing in diseases)
- var/datum/disease/D = thing
- if(D.spread_flags & DISEASE_SPREAD_CONTACT_SKIN)
- L.ContactContractDisease(D)
-
- for(var/thing in L.diseases)
- var/datum/disease/D = thing
- if(D.spread_flags & DISEASE_SPREAD_CONTACT_SKIN)
- ContactContractDisease(D)
-
- if(iscarbon(L))
- var/mob/living/carbon/C = L
- if(HAS_TRAIT(src, TRAIT_STRONG_GRABBER))
- C.grippedby(src)
-
- update_pull_movespeed()
-
- set_pull_offsets(M, state)
-
-/mob/living/proc/set_pull_offsets(mob/living/M, grab_state = GRAB_PASSIVE)
- if(M.buckled)
- return //don't make them change direction or offset them if they're buckled into something.
- var/offset = 0
- switch(grab_state)
- if(GRAB_PASSIVE)
- offset = GRAB_PIXEL_SHIFT_PASSIVE
- if(GRAB_AGGRESSIVE)
- offset = GRAB_PIXEL_SHIFT_AGGRESSIVE
- if(GRAB_NECK)
- offset = GRAB_PIXEL_SHIFT_NECK
- if(GRAB_KILL)
- offset = GRAB_PIXEL_SHIFT_NECK
- M.setDir(get_dir(M, src))
- switch(M.dir)
- if(NORTH)
- animate(M, pixel_x = M.base_pixel_x, pixel_y = M.base_pixel_y + offset, 3)
- if(SOUTH)
- animate(M, pixel_x = M.base_pixel_x, pixel_y = M.base_pixel_y - offset, 3)
- if(EAST)
- if(M.lying_angle == 270) //update the dragged dude's direction if we've turned
- M.set_lying_angle(90)
- animate(M, pixel_x = M.base_pixel_x + offset, pixel_y = M.base_pixel_y, 3)
- if(WEST)
- if(M.lying_angle == 90)
- M.set_lying_angle(270)
- animate(M, pixel_x = M.base_pixel_x - offset, pixel_y = M.base_pixel_y, 3)
-
-/mob/living/proc/reset_pull_offsets(mob/living/M, override)
- if(!override && M.buckled)
+/mob/living/proc/reset_pull_offsets(override)
+ if(!override && buckled)
return
- animate(M, pixel_x = M.base_pixel_x, pixel_y = M.base_pixel_y, 1)
+ animate(src, pixel_x = src.base_pixel_x, pixel_y = src.base_pixel_y, 1)
//mob verbs are a lot faster than object verbs
//for more info on why this is not atom/pull, see examinate() in mob.dm
@@ -425,22 +296,12 @@
set category = "Object"
if(istype(AM) && Adjacent(AM))
- start_pulling(AM)
- else if(!combat_mode) //Don;'t cancel pulls if misclicking in combat mode.
- stop_pulling()
-
-/mob/living/stop_pulling()
- animate_interact(pulling, INTERACT_UNPULL)
- if(ismob(pulling))
- reset_pull_offsets(pulling)
- ..()
- update_pull_movespeed()
- update_pull_hud_icon()
+ try_make_grab(AM)
/mob/living/verb/stop_pulling1()
set name = "Stop Pulling"
set category = "IC"
- stop_pulling()
+ release_all_grabs()
//same as above
/mob/living/pointed(atom/A as mob|obj|turf in view(client.view, src))
@@ -487,8 +348,12 @@
if(!(flags & IGNORE_RESTRAINTS) && HAS_TRAIT(src, TRAIT_RESTRAINED))
return TRUE
- if(!(flags & IGNORE_GRAB) && pulledby && pulledby.grab_state >= GRAB_AGGRESSIVE)
- return TRUE
+
+ if(!(flags & IGNORE_GRAB))
+ for(var/obj/item/hand_item/grab/G in grabbed_by)
+ if(G.current_grab.restrains && !G.assailant == src)
+ return TRUE
+
if(!(flags & IGNORE_STASIS) && IS_IN_HARD_STASIS(src))
return TRUE
return FALSE
@@ -889,7 +754,7 @@
/mob/living/proc/update_wound_overlays()
return
-/mob/living/Move(atom/newloc, direct, glide_size_override)
+/mob/living/Move(atom/newloc, direct, glide_size_override, z_movement_flags)
if(lying_angle != 0)
lying_angle_on_movement(direct)
if (buckled && buckled.loc != newloc) //not updating position
@@ -902,15 +767,10 @@
var/old_direction = dir
var/turf/T = loc
- if(pulling)
- update_pull_movespeed()
+ update_pull_movespeed()
. = ..()
- if(moving_diagonally != FIRST_DIAG_STEP && isliving(pulledby))
- var/mob/living/L = pulledby
- L.set_pull_offsets(src, pulledby.grab_state)
-
if(active_storage && !((active_storage.parent?.resolve() in important_recursive_contents?[RECURSIVE_CONTENTS_ACTIVE_STORAGE]) || CanReach(active_storage.parent?.resolve(),view_only = TRUE)))
active_storage.hide_contents(src)
@@ -1035,8 +895,7 @@
SEND_SIGNAL(src, COMSIG_LIVING_RESIST, src)
//resisting grabs (as if it helps anyone...)
- if(!HAS_TRAIT(src, TRAIT_RESTRAINED) && pulledby)
- log_combat(src, pulledby, "resisted grab")
+ if(!HAS_TRAIT(src, TRAIT_RESTRAINED) && LAZYLEN(grabbed_by))
resist_grab()
return
@@ -1054,34 +913,24 @@
else if(last_special <= world.time)
resist_restraints() //trying to remove cuffs.
+/// Attempt to break free of grabs. Returning TRUE means the user broke free and can move.
/mob/proc/resist_grab(moving_resist)
- return 1 //returning 0 means we successfully broke free
+ return TRUE
/mob/living/resist_grab(moving_resist)
+ if(!LAZYLEN(grabbed_by))
+ return TRUE
+
+ if(!moving_resist)
+ visible_message(span_danger("\The [src] struggles to break free!"))
+
. = TRUE
- if(pulledby.grab_state || body_position == LYING_DOWN || HAS_TRAIT(src, TRAIT_GRABWEAKNESS))
- var/altered_grab_state = pulledby.grab_state
- if((body_position == LYING_DOWN || HAS_TRAIT(src, TRAIT_GRABWEAKNESS)) && pulledby.grab_state < GRAB_KILL) //If prone, resisting out of a grab is equivalent to 1 grab state higher. won't make the grab state exceed the normal max, however
- altered_grab_state++
- var/resist_chance = BASE_GRAB_RESIST_CHANCE /// see defines/combat.dm, this should be baseline 60%
- resist_chance = (resist_chance/altered_grab_state) ///Resist chance divided by the value imparted by your grab state. It isn't until you reach neckgrab that you gain a penalty to escaping a grab.
- if(prob(resist_chance))
- visible_message(span_danger("[src] breaks free of [pulledby]'s grip!"), \
- span_danger("You break free of [pulledby]'s grip!"), null, null, pulledby)
- to_chat(pulledby, span_warning("[src] breaks free of your grip!"))
- log_combat(pulledby, src, "broke grab")
- pulledby.stop_pulling()
- return FALSE
- else
- stamina.adjust(-rand(15,20)) //failure to escape still imparts a pretty serious penalty
- visible_message(span_danger("[src] struggles as they fail to break free of [pulledby]'s grip!"), \
- span_warning("You struggle as you fail to break free of [pulledby]'s grip!"), null, null, pulledby)
- to_chat(pulledby, span_danger("[src] struggles as they fail to break free of your grip!"))
- if(moving_resist && client) //we resisted by trying to move
- client.move_delay = world.time + 4 SECONDS
- else
- pulledby.stop_pulling()
- return FALSE
+ for(var/obj/item/hand_item/grab/G as anything in grabbed_by)
+ if(G.assailant == src) //Grabbing our own bodyparts
+ continue
+ log_combat(src, G.assailant, "resisted grab")
+ if(!G.handle_resist())
+ . = FALSE
/mob/living/proc/resist_buckle()
buckled.user_unbuckle_mob(src,src)
@@ -1253,7 +1102,7 @@
return
/mob/living/throw_at(atom/target, range, speed, mob/thrower, spin=1, diagonals_first = 0, datum/callback/callback, force, gentle = FALSE, quickstart = TRUE)
- stop_pulling()
+ release_all_grabs()
. = ..()
// Used in polymorph code to shapeshift mobs into other creatures
@@ -1602,23 +1451,27 @@ GLOBAL_LIST_EMPTY(fire_appearances)
"[C] leaps out of [src]'s way!")]")
C.Paralyze(40)
-/mob/living/can_be_pulled()
- return ..() && !(buckled?.buckle_prevents_pull)
-
-
/// Called when mob changes from a standing position into a prone while lacking the ability to stand up at the moment.
/mob/living/proc/on_fall()
return
/mob/living/forceMove(atom/destination)
+ var/old_loc = loc
if(!currently_z_moving)
- stop_pulling()
if(buckled && !HAS_TRAIT(src, TRAIT_CANNOT_BE_UNBUCKLED))
buckled.unbuckle_mob(src, force = TRUE)
if(has_buckled_mobs())
unbuckle_all_mobs(force = TRUE)
+
. = ..()
- if(. && client)
+ if(!.)
+ return
+
+ if(!QDELETED(src) && currently_z_moving == ZMOVING_VERTICAL) // Lateral Z movement handles this on it's own
+ handle_grabs_during_movement(old_loc, get_dir(old_loc, src))
+ recheck_grabs()
+
+ if(client)
reset_perspective()
@@ -1658,7 +1511,7 @@ GLOBAL_LIST_EMPTY(fire_appearances)
var/mob/living/U = user
if(isliving(dropping))
var/mob/living/M = dropping
- if(U.pulling == M && mob_size > M.mob_size)
+ if(U.is_grabbing(M) && !U.combat_mode && mob_size > M.mob_size)
M.mob_try_pickup(U)//blame kevinz
return//dont open the mobs inventory if you are picking them up
. = ..()
@@ -1987,31 +1840,6 @@ GLOBAL_LIST_EMPTY(fire_appearances)
if(old_buckled.buckle_lying == 0 && (resting || HAS_TRAIT(src, TRAIT_FLOORED))) // The buckle forced us to stay up (like a chair)
set_lying_down() // We want to rest or are otherwise floored, so let's drop on the ground.
-/mob/living/set_pulledby(new_pulledby)
- . = ..()
- if(. == FALSE) //null is a valid value here, we only want to return if FALSE is explicitly passed.
- return
- if(pulledby)
- if(!. && HAS_TRAIT(src, TRAIT_SOFT_CRITICAL_CONDITION))
- ADD_TRAIT(src, TRAIT_IMMOBILIZED, PULLED_WHILE_SOFTCRIT_TRAIT)
- else if(. && HAS_TRAIT(src, TRAIT_SOFT_CRITICAL_CONDITION))
- REMOVE_TRAIT(src, TRAIT_IMMOBILIZED, PULLED_WHILE_SOFTCRIT_TRAIT)
-
-
-/// Updates the grab state of the mob and updates movespeed
-/mob/living/setGrabState(newstate)
- . = ..()
- switch(grab_state)
- if(GRAB_PASSIVE)
- remove_movespeed_modifier(MOVESPEED_ID_MOB_GRAB_STATE)
- if(GRAB_AGGRESSIVE)
- add_movespeed_modifier(/datum/movespeed_modifier/grab_slowdown/aggressive)
- if(GRAB_NECK)
- add_movespeed_modifier(/datum/movespeed_modifier/grab_slowdown/neck)
- if(GRAB_KILL)
- add_movespeed_modifier(/datum/movespeed_modifier/grab_slowdown/kill)
-
-
/// Only defined for carbons who can wear masks and helmets, we just assume other mobs have visible faces
/mob/living/proc/is_face_visible()
return TRUE
diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm
index b69dd1b1860d..09cba62bf2bc 100644
--- a/code/modules/mob/living/living_defense.dm
+++ b/code/modules/mob/living/living_defense.dm
@@ -61,7 +61,14 @@
// we need a second, silent armor check to actually know how much to reduce damage taken, as opposed to
// on [/atom/proc/bullet_act] where it's just to pass it to the projectile's on_hit().
var/armor_check = check_projectile_armor(def_zone, P, is_silent = TRUE)
- apply_damage(P.damage, P.damage_type, def_zone, armor_check, sharpness = P.sharpness, attack_direction = attack_direction)
+
+ var/modifier = 1
+ if(LAZYLEN(grabbed_by))
+ for(var/obj/item/hand_item/grab/G in grabbed_by)
+ modifier = max(G.current_grab.point_blank_mult, modifier)
+ var/damage = P.damage * modifier
+
+ apply_damage(damage, P.damage_type, def_zone, armor_check, sharpness = P.sharpness, attack_direction = attack_direction)
apply_effects(P.stun, P.knockdown, P.unconscious, P.slur, P.stutter, P.eyeblur, P.drowsy, armor_check, P.stamina, P.jitter, P.paralyze, P.immobilize)
if(P.disorient_length)
var/stamina = P.disorient_damage * ((100-armor_check)/100)
@@ -137,82 +144,6 @@
adjust_fire_stacks(3)
ignite_mob()
-/mob/living/proc/grabbedby(mob/living/carbon/user, supress_message = FALSE)
- if(user == src || anchored || !isturf(user.loc))
- return FALSE
- if(!user.pulling || user.pulling != src)
- user.start_pulling(src, supress_message = supress_message)
- return
-
- if(!(status_flags & CANPUSH) || HAS_TRAIT(src, TRAIT_PUSHIMMUNE))
- to_chat(user, span_warning("[src] can't be grabbed more aggressively!"))
- return FALSE
-
- if(user.grab_state >= GRAB_AGGRESSIVE && HAS_TRAIT(user, TRAIT_PACIFISM))
- to_chat(user, span_warning("You don't want to risk hurting [src]!"))
- return FALSE
- grippedby(user)
-
-//proc to upgrade a simple pull into a more aggressive grab.
-/mob/living/proc/grippedby(mob/living/carbon/user, instant = FALSE)
- if(user.grab_state < GRAB_KILL)
- user.changeNext_move(CLICK_CD_GRABBING)
- var/sound_to_play = 'sound/weapons/thudswoosh.ogg'
- if(ishuman(user))
- var/mob/living/carbon/human/H = user
- if(H.dna.species.grab_sound)
- sound_to_play = H.dna.species.grab_sound
- playsound(src.loc, sound_to_play, 50, TRUE, -1)
-
- if(user.grab_state) //only the first upgrade is instantaneous
- var/old_grab_state = user.grab_state
- var/grab_upgrade_time = instant ? 0 : 30
- visible_message(span_danger("[user] starts to tighten [user.p_their()] grip on [src]!"), \
- span_userdanger("[user] starts to tighten [user.p_their()] grip on you!"), span_hear("You hear aggressive shuffling!"), null, user)
- to_chat(user, span_danger("You start to tighten your grip on [src]!"))
- switch(user.grab_state)
- if(GRAB_AGGRESSIVE)
- log_combat(user, src, "attempted to neck grab", addition="neck grab")
- if(GRAB_NECK)
- log_combat(user, src, "attempted to strangle", addition="kill grab")
- if(!do_after(user, src, grab_upgrade_time))
- return FALSE
- if(!user.pulling || user.pulling != src || user.grab_state != old_grab_state)
- return FALSE
- user.setGrabState(user.grab_state + 1)
- switch(user.grab_state)
- if(GRAB_AGGRESSIVE)
- var/add_log = ""
- if(HAS_TRAIT(user, TRAIT_PACIFISM))
- visible_message(span_danger("[user] firmly grips [src]!"),
- span_danger("[user] firmly grips you!"), span_hear("You hear aggressive shuffling!"), null, user)
- to_chat(user, span_danger("You firmly grip [src]!"))
- add_log = " (pacifist)"
- else
- visible_message(span_danger("[user] grabs [src] aggressively!"), \
- span_userdanger("[user] grabs you aggressively!"), span_hear("You hear aggressive shuffling!"), null, user)
- to_chat(user, span_danger("You grab [src] aggressively!"))
- drop_all_held_items()
- stop_pulling()
- log_combat(user, src, "grabbed", addition="aggressive grab[add_log]")
- if(GRAB_NECK)
- log_combat(user, src, "grabbed", addition="neck grab")
- visible_message(span_danger("[user] grabs [src] by the neck!"),\
- span_userdanger("[user] grabs you by the neck!"), span_hear("You hear aggressive shuffling!"), null, user)
- to_chat(user, span_danger("You grab [src] by the neck!"))
- if(!buckled && !density)
- Move(user.loc)
- if(GRAB_KILL)
- log_combat(user, src, "strangled", addition="kill grab")
- visible_message(span_danger("[user] is strangling [src]!"), \
- span_userdanger("[user] is strangling you!"), span_hear("You hear aggressive shuffling!"), null, user)
- to_chat(user, span_danger("You're strangling [src]!"))
- if(!buckled && !density)
- Move(user.loc)
- user.set_pull_offsets(src, grab_state)
- return TRUE
-
-
/mob/living/attack_slime(mob/living/simple_animal/slime/M)
if(!SSticker.HasRoundStarted())
to_chat(M, "You cannot attack people before the game has started.")
diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm
index b0387aad0e10..d9b29efeb9d3 100644
--- a/code/modules/mob/living/living_defines.dm
+++ b/code/modules/mob/living/living_defines.dm
@@ -147,8 +147,6 @@
var/list/diseases /// list of all diseases in a mob
var/list/disease_resistances
- var/slowed_by_drag = TRUE ///Whether the mob is slowed down when dragging another prone mob
-
/// List of changes to body temperature, used by desease symtoms like fever
var/list/body_temp_changes = list()
diff --git a/code/modules/mob/living/living_movement.dm b/code/modules/mob/living/living_movement.dm
index cfe982af22bc..b6fbadc0b781 100644
--- a/code/modules/mob/living/living_movement.dm
+++ b/code/modules/mob/living/living_movement.dm
@@ -96,47 +96,27 @@
remove_movespeed_modifier(/datum/movespeed_modifier/turf_slowdown)
current_turf_slowdown = 0
-
/mob/living/proc/update_pull_movespeed()
- SEND_SIGNAL(src, COMSIG_LIVING_UPDATING_PULL_MOVESPEED)
+ var/list/obj/item/hand_item/grab/grabs = get_active_grabs()
+ if(!length(grabs))
+ remove_movespeed_modifier(/datum/movespeed_modifier/grabbing)
+ return
- if(pulling)
+ var/slowdown_total = 0
+ for(var/obj/item/hand_item/grab/G as anything in grabs)
+ var/atom/movable/pulling = G.affecting
if(isliving(pulling))
var/mob/living/L = pulling
- if(!slowed_by_drag || L.body_position == STANDING_UP || L.buckled || grab_state >= GRAB_AGGRESSIVE)
- remove_movespeed_modifier(/datum/movespeed_modifier/bulky_drag)
- return
- add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/bulky_drag, multiplicative_slowdown = PULL_PRONE_SLOWDOWN)
- return
+ if(G.current_grab.grab_slowdown || L.body_position == LYING_DOWN)
+ slowdown_total += max(G.current_grab.grab_slowdown, PULL_PRONE_SLOWDOWN)
+
if(isobj(pulling))
var/obj/structure/S = pulling
- if(!slowed_by_drag || !S.drag_slowdown)
- remove_movespeed_modifier(/datum/movespeed_modifier/bulky_drag)
- return
- add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/bulky_drag, multiplicative_slowdown = S.drag_slowdown)
- return
- remove_movespeed_modifier(/datum/movespeed_modifier/bulky_drag)
-
-/**
- * We want to relay the zmovement to the buckled atom when possible
- * and only run what we can't have on buckled.zMove() or buckled.can_z_move() here.
- * This way we can avoid esoteric bugs, copypasta and inconsistencies.
- */
-/mob/living/zMove(dir, turf/target, z_move_flags = ZMOVE_FLIGHT_FLAGS)
- if(buckled)
- if(buckled.currently_z_moving)
- return FALSE
- if(!(z_move_flags & ZMOVE_ALLOW_BUCKLED))
- buckled.unbuckle_mob(src, force = TRUE, can_fall = FALSE)
- else
- if(!target)
- target = can_z_move(dir, get_turf(src), z_move_flags, src)
- if(!target)
- return FALSE
- return buckled.zMove(dir, target, z_move_flags) // Return value is a loc.
- return ..()
+ slowdown_total += S.drag_slowdown
+
+ add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/grabbing, multiplicative_slowdown = slowdown_total)
-/mob/living/can_z_move(direction, turf/start, z_move_flags = ZMOVE_FLIGHT_FLAGS|ZMOVE_FEEDBACK, mob/living/rider)
+/mob/living/can_z_move(direction, turf/start, z_move_flags, mob/living/rider)
// Check physical climbing ability
if((z_move_flags & ZMOVE_INCAPACITATED_CHECKS))
if(incapacitated())
@@ -171,11 +151,6 @@
to_chat(src, span_warning("Unbuckle from [buckled] first."))
return FALSE
-/mob/set_currently_z_moving(value)
- if(buckled)
- return buckled.set_currently_z_moving(value)
- return ..()
-
/mob/living/keybind_face_direction(direction)
if(stat != CONSCIOUS)
return
diff --git a/code/modules/mob/living/living_stripping.dm b/code/modules/mob/living/living_stripping.dm
index a0956eb26724..793745cf3a4c 100644
--- a/code/modules/mob/living/living_stripping.dm
+++ b/code/modules/mob/living/living_stripping.dm
@@ -1,7 +1,7 @@
-/mob/living/proc/should_strip(mob/user)
+/mob/living/proc/should_strip(mob/living/user)
. = TRUE
- if(user.pulling == src && isliving(user))
+ if(user.is_grabbing(src) && !user.combat_mode && isliving(user))
var/mob/living/living_user = user
if(mob_size < living_user.mob_size) // If we're smaller than user
return !mob_pickup_checks(user, FALSE)
diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm
index 5cd18ca61fd1..bf02f88c7c64 100644
--- a/code/modules/mob/living/silicon/ai/ai.dm
+++ b/code/modules/mob/living/silicon/ai/ai.dm
@@ -1016,14 +1016,14 @@
set name = "Move Upwards"
set category = "IC"
- if(eyeobj.zMove(UP, z_move_flags = ZMOVE_FEEDBACK))
+ if(zstep(eyeobj, UP, ZMOVE_FEEDBACK))
to_chat(src, span_notice("You move upwards."))
/mob/living/silicon/ai/down()
set name = "Move Down"
set category = "IC"
- if(eyeobj.zMove(DOWN, z_move_flags = ZMOVE_FEEDBACK))
+ if(zstep(eyeobj, DOWN, ZMOVE_FEEDBACK))
to_chat(src, span_notice("You move down."))
/// Proc to hook behavior to the changes of the value of [aiRestorePowerRoutine].
diff --git a/code/modules/mob/living/silicon/ai/freelook/eye.dm b/code/modules/mob/living/silicon/ai/freelook/eye.dm
index d0246f3b7b5b..77cb0adf204e 100644
--- a/code/modules/mob/living/silicon/ai/freelook/eye.dm
+++ b/code/modules/mob/living/silicon/ai/freelook/eye.dm
@@ -101,11 +101,6 @@
if(ai.master_multicam)
ai.master_multicam.refresh_view()
-/mob/camera/ai_eye/zMove(dir, turf/target, z_move_flags = NONE, recursions_left = 1, list/falling_movs)
- . = ..()
- if(.)
- setLoc(loc, force_update = TRUE)
-
/mob/camera/ai_eye/Move()
return
diff --git a/code/modules/mob/living/silicon/pai/pai_shell.dm b/code/modules/mob/living/silicon/pai/pai_shell.dm
index 8036bd1c0d88..deac77c2ce66 100644
--- a/code/modules/mob/living/silicon/pai/pai_shell.dm
+++ b/code/modules/mob/living/silicon/pai/pai_shell.dm
@@ -55,7 +55,9 @@
. = fold_out(force)
return
visible_message(span_notice("[src] deactivates its holochassis emitter and folds back into a compact card!"))
- stop_pulling()
+
+ release_all_grabs()
+
if(ismobholder(loc))
var/obj/item/mob_holder/MH = loc
MH.release_mob(display_messages = FALSE)
@@ -137,7 +139,7 @@
if(loc != card)
visible_message(span_notice("[src] [resting? "lays down for a moment..." : "perks up from the ground."]"))
-/mob/living/silicon/pai/start_pulling(atom/movable/AM, state, force = move_force, supress_message = FALSE)
+/mob/living/silicon/pai/try_make_grab(atom/movable/target, grab_type)
return FALSE
/mob/living/silicon/pai/proc/toggle_integrated_light()
diff --git a/code/modules/mob/living/silicon/silicon_defense.dm b/code/modules/mob/living/silicon/silicon_defense.dm
index cde9424d34f8..65145599894e 100644
--- a/code/modules/mob/living/silicon/silicon_defense.dm
+++ b/code/modules/mob/living/silicon/silicon_defense.dm
@@ -1,7 +1,3 @@
-
-/mob/living/silicon/grippedby(mob/living/user, instant = FALSE)
- return //can't upgrade a simple pull into a more aggressive grab.
-
/mob/living/silicon/get_ear_protection()//no ears
return 2
diff --git a/code/modules/mob/living/simple_animal/constructs.dm b/code/modules/mob/living/simple_animal/constructs.dm
index 4ca684cda8d5..d32c28f24a2f 100644
--- a/code/modules/mob/living/simple_animal/constructs.dm
+++ b/code/modules/mob/living/simple_animal/constructs.dm
@@ -422,13 +422,14 @@
/mob/living/simple_animal/hostile/construct/harvester/Bump(atom/AM)
. = ..()
if(istype(AM, /turf/closed/wall/mineral/cult) && AM != loc) //we can go through cult walls
- var/atom/movable/stored_pulling = pulling
+ var/atom/movable/stored_pulling = get_active_grab()?.affecting
+ var/datum/grab/stored_tier = get_active_grab()?.current_grab.type
if(stored_pulling)
stored_pulling.setDir(get_dir(stored_pulling.loc, loc))
stored_pulling.forceMove(loc)
forceMove(AM)
if(stored_pulling)
- start_pulling(stored_pulling, supress_message = TRUE) //drag anything we're pulling through the wall with us by magic
+ try_make_grab(stored_pulling, stored_tier)
/mob/living/simple_animal/hostile/construct/harvester/AttackingTarget()
if(iscarbon(target))
diff --git a/code/modules/mob/living/simple_animal/friendly/drone/inventory.dm b/code/modules/mob/living/simple_animal/friendly/drone/inventory.dm
index e32880794724..504d25e598b1 100644
--- a/code/modules/mob/living/simple_animal/friendly/drone/inventory.dm
+++ b/code/modules/mob/living/simple_animal/friendly/drone/inventory.dm
@@ -61,8 +61,8 @@
held_items[index] = null
update_held_items()
- if(I.pulledby)
- I.pulledby.stop_pulling()
+ if(LAZYLEN(I.grabbed_by))
+ I.free_from_all_grabs()
I.screen_loc = null // will get moved if inventory is visible
I.forceMove(src)
diff --git a/code/modules/mob/living/simple_animal/friendly/farm_animals.dm b/code/modules/mob/living/simple_animal/friendly/farm_animals.dm
index 5d79eca81b66..120e7061df63 100644
--- a/code/modules/mob/living/simple_animal/friendly/farm_animals.dm
+++ b/code/modules/mob/living/simple_animal/friendly/farm_animals.dm
@@ -56,7 +56,7 @@
return
eat_plants()
- if(pulledby)
+ if(LAZYLEN(grabbed_by))
return
for(var/direction in shuffle(list(1,2,4,8,5,6,9,10)))
diff --git a/code/modules/mob/living/simple_animal/guardian/guardian.dm b/code/modules/mob/living/simple_animal/guardian/guardian.dm
index ae0f67eb6b85..eb39e577788d 100644
--- a/code/modules/mob/living/simple_animal/guardian/guardian.dm
+++ b/code/modules/mob/living/simple_animal/guardian/guardian.dm
@@ -300,8 +300,8 @@ GLOBAL_LIST_EMPTY(parasites) //all currently existing/living guardians
held_items[index] = null
update_held_items()
- if(I.pulledby)
- I.pulledby.stop_pulling()
+ if(LAZYLEN(I.grabbed_by))
+ I.free_from_all_grabs()
I.screen_loc = null // will get moved if inventory is visible
I.forceMove(src)
diff --git a/code/modules/mob/living/simple_animal/heretic_monsters.dm b/code/modules/mob/living/simple_animal/heretic_monsters.dm
index 60b060679590..1cc62a77421f 100644
--- a/code/modules/mob/living/simple_animal/heretic_monsters.dm
+++ b/code/modules/mob/living/simple_animal/heretic_monsters.dm
@@ -219,7 +219,7 @@
/mob/living/simple_animal/hostile/heretic_summon/armsy/has_gravity(turf/T)
return TRUE
-/mob/living/simple_animal/hostile/heretic_summon/armsy/can_be_pulled()
+/mob/living/simple_animal/hostile/heretic_summon/armsy/can_be_grabbed(mob/living/grabber, target_zone, force)
return FALSE
/// Updates every body in the chain to force move onto a single tile.
diff --git a/code/modules/mob/living/simple_animal/hostile/giant_spider.dm b/code/modules/mob/living/simple_animal/hostile/giant_spider.dm
index 0747ca686f1e..da17fbd2fb18 100644
--- a/code/modules/mob/living/simple_animal/hostile/giant_spider.dm
+++ b/code/modules/mob/living/simple_animal/hostile/giant_spider.dm
@@ -67,7 +67,6 @@
if(poison_per_bite)
AddElement(/datum/element/venomous, poison_type, poison_per_bite)
- AddElement(/datum/element/nerfed_pulling, GLOB.typecache_general_bad_things_to_easily_move)
AddElement(/datum/element/prevent_attacking_of_types, GLOB.typecache_general_bad_hostile_attack_targets, "this tastes awful!")
/mob/living/simple_animal/hostile/giant_spider/Login()
diff --git a/code/modules/mob/living/simple_animal/hostile/hostile.dm b/code/modules/mob/living/simple_animal/hostile/hostile.dm
index 998ab16d6e65..2263216ecd8b 100644
--- a/code/modules/mob/living/simple_animal/hostile/hostile.dm
+++ b/code/modules/mob/living/simple_animal/hostile/hostile.dm
@@ -437,7 +437,7 @@
return iswallturf(T) || ismineralturf(T)
-/mob/living/simple_animal/hostile/Move(atom/newloc, dir , step_x , step_y)
+/mob/living/simple_animal/hostile/Move(atom/newloc, dir, glide_size_override, z_movement_flags)
if(dodging && approaching_target && prob(dodge_prob) && moving_diagonally == 0 && isturf(loc) && isturf(newloc))
return dodge(newloc,dir)
else
diff --git a/code/modules/mob/living/simple_animal/hostile/jungle/leaper.dm b/code/modules/mob/living/simple_animal/hostile/jungle/leaper.dm
index 31e792d02525..0ad9d727d85d 100644
--- a/code/modules/mob/living/simple_animal/hostile/jungle/leaper.dm
+++ b/code/modules/mob/living/simple_animal/hostile/jungle/leaper.dm
@@ -142,7 +142,7 @@
. = ..()
remove_verb(src, /mob/living/verb/pulled)
-/mob/living/simple_animal/hostile/jungle/leaper/CtrlClickOn(atom/A)
+/mob/living/simple_animal/hostile/jungle/leaper/CtrlClickOn(atom/A, list/params)
face_atom(A)
GiveTarget(A)
if(!isturf(loc))
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm
index c3fa6d9cf43d..fc22a1e781f6 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm
@@ -556,7 +556,7 @@ Difficulty: Hard
. = ..()
if(QDELETED(caster))
return FALSE
- if(mover == caster.pulledby)
+ if(caster.is_grabbing(mover))
return
if(istype(mover, /obj/projectile))
var/obj/projectile/P = mover
diff --git a/code/modules/mob/living/simple_animal/hostile/mimic.dm b/code/modules/mob/living/simple_animal/hostile/mimic.dm
index b0512edbe159..42a3050fb213 100644
--- a/code/modules/mob/living/simple_animal/hostile/mimic.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mimic.dm
@@ -383,7 +383,7 @@ GLOBAL_LIST_INIT(mimic_blacklist, list(/obj/structure/table, /obj/structure/cabl
for(var/mob/living/M in contents)
if(++mobs_stored >= mob_storage_capacity)
return FALSE
- L.stop_pulling()
+ L.release_all_grabs()
else if(istype(AM, /obj/structure/closet))
return FALSE
diff --git a/code/modules/mob/living/simple_animal/hostile/ooze.dm b/code/modules/mob/living/simple_animal/hostile/ooze.dm
index fb6918f2bf81..25e7c53da6ea 100644
--- a/code/modules/mob/living/simple_animal/hostile/ooze.dm
+++ b/code/modules/mob/living/simple_animal/hostile/ooze.dm
@@ -209,16 +209,17 @@
if(!.)
return
var/mob/living/simple_animal/hostile/ooze/gelatinous/ooze = owner
- if(!isliving(ooze.pulling))
- to_chat(src, span_warning("You need to be pulling a creature for this to work!"))
+ var/mob/living/target = ooze.get_active_grab()?.affecting
+ if(!isliving(target))
+ to_chat(src, span_warning("You need to be gripping a creature for this to work!"))
return FALSE
if(vored_mob)
to_chat(src, span_warning("You are already consuming another creature!"))
return FALSE
owner.visible_message(span_warning("[ooze] starts attempting to devour [target]!"), span_notice("You start attempting to devour [target]."))
- if(!do_after(ooze, ooze.pulling, 1.5 SECONDS))
+ if(!do_after(ooze, target, 1.5 SECONDS))
return FALSE
- var/mob/living/eat_target = ooze.pulling
+ var/mob/living/eat_target = target
if(!(eat_target.mob_biotypes & MOB_ORGANIC) || eat_target.stat == DEAD)
to_chat(src, span_warning("This creature isn't to my tastes!"))
@@ -416,14 +417,15 @@
///Try to put the pulled mob in a cocoon
/datum/action/cooldown/gel_cocoon/proc/gel_cocoon()
var/mob/living/simple_animal/hostile/ooze/grapes/ooze = owner
- if(!iscarbon(ooze.pulling))
+ var/mob/living/carbon/target = ooze.get_active_grab()?.affecting
+ if(!iscarbon(target))
to_chat(src, span_warning("You need to be pulling an intelligent enough creature to assist it with a cocoon!"))
return FALSE
owner.visible_message(span_nicegreen("[ooze] starts attempting to put [target] into a gel cocoon!"), span_notice("You start attempting to put [target] into a gel cocoon."))
- if(!do_after(ooze, ooze.pulling, 1.5 SECONDS))
+ if(!do_after(ooze, target, 1.5 SECONDS))
return FALSE
- put_in_cocoon(ooze.pulling)
+ put_in_cocoon(target)
ooze.adjust_ooze_nutrition(-30)
///Mob needs to have enough nutrition
diff --git a/code/modules/mob/living/simple_animal/hostile/space_dragon.dm b/code/modules/mob/living/simple_animal/hostile/space_dragon.dm
index 60fa7abe30a0..25a930ddc5a9 100644
--- a/code/modules/mob/living/simple_animal/hostile/space_dragon.dm
+++ b/code/modules/mob/living/simple_animal/hostile/space_dragon.dm
@@ -715,7 +715,6 @@
return FALSE
var/mob/living/simple_animal/hostile/carp/newcarp = new /mob/living/simple_animal/hostile/carp(loc)
- newcarp.AddElement(/datum/element/nerfed_pulling, GLOB.typecache_general_bad_things_to_easily_move)
newcarp.AddElement(/datum/element/prevent_attacking_of_types, GLOB.typecache_general_bad_hostile_attack_targets, "this tastes awful!")
if(!is_listed)
diff --git a/code/modules/mob/living/simple_animal/parrot.dm b/code/modules/mob/living/simple_animal/parrot.dm
index 1a1670c19318..c9be88c0b46b 100644
--- a/code/modules/mob/living/simple_animal/parrot.dm
+++ b/code/modules/mob/living/simple_animal/parrot.dm
@@ -377,7 +377,7 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list(
..()
//Sprite update for when a parrot gets pulled
- if(pulledby && !stat && parrot_state != PARROT_WANDER)
+ if(LAZYLEN(grabbed_by) && !stat && parrot_state != PARROT_WANDER)
if(buckled)
buckled.unbuckle_mob(src, TRUE)
buckled = null
diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm
index 69a9ef18b6a2..4cbf4746e625 100644
--- a/code/modules/mob/living/simple_animal/simple_animal.dm
+++ b/code/modules/mob/living/simple_animal/simple_animal.dm
@@ -263,7 +263,7 @@
turns_since_move++
if(turns_since_move < turns_per_move)
return TRUE
- if(stop_automated_movement_when_pulled && pulledby) //Some animals don't move when pulled
+ if(stop_automated_movement_when_pulled && LAZYLEN(grabbed_by)) //Some animals don't move when pulled
return TRUE
var/anydir = pick(GLOB.cardinals)
if(Process_Spacemove(anydir))
@@ -309,7 +309,7 @@
/mob/living/simple_animal/proc/environment_air_is_safe()
. = TRUE
- if(pulledby && pulledby.grab_state >= GRAB_KILL && atmos_requirements["min_oxy"])
+ if(HAS_TRAIT(src, TRAIT_KILL_GRAB) && atmos_requirements["min_oxy"])
. = FALSE //getting choked
if(isturf(loc) && isopenturf(loc))
@@ -648,7 +648,7 @@
stack_trace("Something attempted to set simple animals AI to an invalid state: [togglestatus]")
/mob/living/simple_animal/proc/consider_wakeup()
- if (pulledby || shouldwakeup)
+ if (LAZYLEN(grabbed_by) || shouldwakeup)
toggle_ai(AI_ON)
/mob/living/simple_animal/on_changed_z_level(turf/old_turf, turf/new_turf)
diff --git a/code/modules/mob/living/simple_animal/slime/life.dm b/code/modules/mob/living/simple_animal/slime/life.dm
index d02608454d5c..89e5f1a0e8ce 100644
--- a/code/modules/mob/living/simple_animal/slime/life.dm
+++ b/code/modules/mob/living/simple_animal/slime/life.dm
@@ -357,7 +357,7 @@
else
if(holding_still)
holding_still = max(holding_still - (0.5 * delta_time), 0)
- else if (docile && pulledby)
+ else if (docile && LAZYLEN(grabbed_by))
holding_still = 10
else if(!HAS_TRAIT(src, TRAIT_IMMOBILIZED) && isturf(loc) && prob(33))
step(src, pick(GLOB.cardinals))
diff --git a/code/modules/mob/living/simple_animal/slime/slime.dm b/code/modules/mob/living/simple_animal/slime/slime.dm
index 05c1e3fbd938..fd1e6d7d548c 100644
--- a/code/modules/mob/living/simple_animal/slime/slime.dm
+++ b/code/modules/mob/living/simple_animal/slime/slime.dm
@@ -270,7 +270,7 @@
/mob/living/simple_animal/slime/doUnEquip(obj/item/I, force, newloc, no_move, invdrop = TRUE, silent = FALSE)
return
-/mob/living/simple_animal/slime/start_pulling(atom/movable/AM, state, force = move_force, supress_message = FALSE)
+/mob/living/simple_animal/slime/try_make_grab(atom/movable/target, grab_type)
return
/mob/living/simple_animal/slime/attack_ui(slot, params)
diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm
index 6832cae61937..368bbe87ade8 100644
--- a/code/modules/mob/mob_helpers.dm
+++ b/code/modules/mob/mob_helpers.dm
@@ -80,8 +80,9 @@ GLOBAL_LIST_INIT(bodyzone_miss_chance, list(
if(target.buckled || target.body_position == LYING_DOWN)
return zone
// if your target is being grabbed aggressively by someone you cannot miss either
- if(target.pulledby)
- return zone
+ for(var/obj/item/hand_item/grab/G in target.grabbed_by)
+ if(G.current_grab.stop_move)
+ return zone
var/miss_chance = GLOB.bodyzone_miss_chance[zone]
@@ -516,3 +517,7 @@ GLOBAL_LIST_INIT(bodyzone_miss_chance, list(
/// Can this mob SMELL THE SMELLY SMELLS?
/mob/proc/can_smell(intensity)
return FALSE
+
+//returns the number of size categories between two mob_sizes, rounded. Positive means A is larger than B
+/proc/mob_size_difference(mob_size_A, mob_size_B)
+ return round(log(2, mob_size_A/mob_size_B), 1)
diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm
index 9500c1abc49b..d97407867a9a 100644
--- a/code/modules/mob/mob_movement.dm
+++ b/code/modules/mob/mob_movement.dm
@@ -104,7 +104,7 @@
if(isAI(mob))
return AIMove(new_loc,direct,mob)
- if(Process_Grab()) //are we restrained by someone's grip?
+ if(Process_Grabs()) //are we restrained by someone's grip?
return
if(mob.buckled) //if we're buckled to something, tell it we moved.
@@ -162,28 +162,20 @@
// as a result of player input and not because they were pulled or any other magic.
SEND_SIGNAL(mob, COMSIG_MOB_CLIENT_MOVED)
- var/atom/movable/P = mob.pulling
- if(P && !ismob(P) && P.density && !HAS_TRAIT(P, TRAIT_KEEP_DIRECTION_WHILE_PULLING))
- mob.setDir(turn(mob.dir, 180))
-
/**
* Checks to see if you're being grabbed and if so attempts to break it
*
* Called by client/Move()
*/
-/client/proc/Process_Grab()
- if(!mob.pulledby)
- return FALSE
- if(mob.pulledby == mob.pulling && mob.pulledby.grab_state == GRAB_PASSIVE) //Don't autoresist passive grabs if we're grabbing them too.
- return FALSE
+/client/proc/Process_Grabs()
if(HAS_TRAIT(mob, TRAIT_INCAPACITATED))
COOLDOWN_START(src, move_delay, 1 SECONDS)
- return TRUE
+ return FALSE
else if(HAS_TRAIT(mob, TRAIT_RESTRAINED))
COOLDOWN_START(src, move_delay, 1 SECONDS)
to_chat(src, span_warning("You're restrained! You can't move!"))
- return TRUE
- return mob.resist_grab(TRUE)
+ return FALSE
+ return !mob.resist_grab(TRUE)
/**
@@ -348,8 +340,16 @@
continue
if(rebound.anchored)
return rebound
- if(pulling == rebound)
- continue
+ if(isliving(rebound))
+ var/mob/living/L = rebound
+ var/_continue = FALSE
+ if(LAZYLEN(L.grabbed_by))
+ for(var/obj/item/hand_item/grab/G in L.grabbed_by)
+ if(G.assailant == src)
+ _continue = TRUE
+ break
+ if(_continue)
+ continue
return rebound
/mob/has_gravity()
@@ -539,7 +539,10 @@
var/atom/loc_atom = loc
return loc_atom.relaymove(src, UP)
- if(zMove(UP, z_move_flags = ZMOVE_FLIGHT_FLAGS|ZMOVE_FEEDBACK))
+ //Human's up() override has its own feedback
+ var/flags = ishuman(src) ? ZMOVE_FLIGHT_FLAGS : ZMOVE_FLIGHT_FLAGS|ZMOVE_FEEDBACK
+ . = zstep(src, UP, flags)
+ if(.)
to_chat(src, span_notice("You move upwards."))
///Moves a mob down a z level
@@ -551,8 +554,9 @@
var/atom/loc_atom = loc
return loc_atom.relaymove(src, DOWN)
- if(zMove(DOWN, z_move_flags = ZMOVE_FLIGHT_FLAGS|ZMOVE_FEEDBACK))
+ if(zstep(src, DOWN, ZMOVE_FLIGHT_FLAGS|ZMOVE_FEEDBACK))
to_chat(src, span_notice("You move down."))
+
return FALSE
/mob/abstract_move(atom/destination)
diff --git a/code/modules/mod/mod_ai.dm b/code/modules/mod/mod_ai.dm
index 61ffeca5e997..cd6052039021 100644
--- a/code/modules/mod/mod_ai.dm
+++ b/code/modules/mod/mod_ai.dm
@@ -74,7 +74,7 @@
#define AI_FALL_TIME 1 SECONDS
/obj/item/mod/control/relaymove(mob/user, direction)
- if((!active && wearer) || get_charge() < CHARGE_PER_STEP || user != ai || !COOLDOWN_FINISHED(src, cooldown_mod_move) || (wearer?.pulledby?.grab_state > GRAB_PASSIVE))
+ if((!active && wearer) || get_charge() < CHARGE_PER_STEP || user != ai || !COOLDOWN_FINISHED(src, cooldown_mod_move) || HAS_TRAIT(wearer, TRAIT_AGGRESSIVE_GRAB))
return FALSE
var/timemodifier = MOVE_DELAY * (ISDIAGONALDIR(direction) ? 2 : 1) * (wearer ? WEARER_DELAY : LONE_DELAY)
if(wearer && !wearer.Process_Spacemove(direction))
diff --git a/code/modules/mod/modules/modules_maint.dm b/code/modules/mod/modules/modules_maint.dm
index 418c9b49671a..dbf71dc5cdb4 100644
--- a/code/modules/mod/modules/modules_maint.dm
+++ b/code/modules/mod/modules/modules_maint.dm
@@ -268,7 +268,7 @@
qdel(mod.wearer.RemoveElement(/datum/element/forced_gravity, NEGATIVE_GRAVITY))
UnregisterSignal(mod.wearer, COMSIG_MOVABLE_MOVED)
REMOVE_TRAIT(mod.wearer, TRAIT_SILENT_FOOTSTEPS, MOD_TRAIT)
- mod.wearer.zFall(falling_from_move = TRUE)
+ mod.wearer.zFall()
/obj/item/mod/module/atrocinator/proc/check_upstairs()
SIGNAL_HANDLER
diff --git a/code/modules/mod/modules/modules_supply.dm b/code/modules/mod/modules/modules_supply.dm
index 698dbde97131..12fd3b3d0206 100644
--- a/code/modules/mod/modules/modules_supply.dm
+++ b/code/modules/mod/modules/modules_supply.dm
@@ -315,8 +315,8 @@
. = ..()
if(!.)
return
- if(istype(mod.wearer.pulling, /obj/structure/closet))
- var/obj/structure/closet/locker = mod.wearer.pulling
+ var/obj/structure/closet/locker = mod.wearer.get_active_grab()?.affecting
+ if(istype(locker, /obj/structure/closet))
playsound(locker, 'sound/effects/gravhit.ogg', 75, TRUE)
locker.forceMove(mod.wearer.loc)
locker.throw_at(target, range = 7, speed = 4, thrower = mod.wearer)
@@ -324,7 +324,7 @@
if(!istype(target, /obj/structure/closet) || !(target in view(mod.wearer)))
balloon_alert(mod.wearer, "invalid target!")
return
- var/obj/structure/closet/locker = target
+
if(locker.anchored || locker.move_resist >= MOVE_FORCE_OVERPOWERING)
balloon_alert(mod.wearer, "target anchored!")
return
@@ -337,23 +337,24 @@
. = ..()
if(!.)
return
- if(istype(mod.wearer.pulling, /obj/structure/closet))
- mod.wearer.stop_pulling()
+ for(var/obj/item/hand_item/grab/G in mod.wearer.get_active_grabs())
+ if(istype(G.affecting, /obj/structure/closet))
+ qdel(G)
/obj/item/mod/module/magnet/proc/check_locker(obj/structure/closet/locker)
if(!mod?.wearer)
return
if(!locker.Adjacent(mod.wearer) || !isturf(locker.loc) || !isturf(mod.wearer.loc))
return
- mod.wearer.start_pulling(locker)
+ mod.wearer.try_make_grab(locker)
locker.strong_grab = TRUE
- RegisterSignal(locker, COMSIG_ATOM_NO_LONGER_PULLED, PROC_REF(on_stop_pull))
+ RegisterSignal(locker, COMSIG_ATOM_NO_LONGER_GRABBED, PROC_REF(on_stop_pull))
/obj/item/mod/module/magnet/proc/on_stop_pull(obj/structure/closet/locker, atom/movable/last_puller)
SIGNAL_HANDLER
locker.strong_grab = FALSE
- UnregisterSignal(locker, COMSIG_ATOM_NO_LONGER_PULLED)
+ UnregisterSignal(locker, COMSIG_ATOM_NO_LONGER_GRABBED)
/obj/item/mod/module/ash_accretion
name = "MOD ash accretion module"
diff --git a/code/modules/modular_computers/computers/item/tablet.dm b/code/modules/modular_computers/computers/item/tablet.dm
index 6d80b4df1fbb..30273877b6bb 100644
--- a/code/modules/modular_computers/computers/item/tablet.dm
+++ b/code/modules/modular_computers/computers/item/tablet.dm
@@ -73,7 +73,7 @@
remove_pen(user)
-/obj/item/modular_computer/tablet/CtrlClick(mob/user)
+/obj/item/modular_computer/tablet/CtrlClick(mob/user, list/params)
. = ..()
if(.)
return
diff --git a/code/modules/movespeed/modifiers/mobs.dm b/code/modules/movespeed/modifiers/mobs.dm
index 38fe0bfc3833..50883711c836 100644
--- a/code/modules/movespeed/modifiers/mobs.dm
+++ b/code/modules/movespeed/modifiers/mobs.dm
@@ -83,7 +83,7 @@
blacklisted_movetypes = (FLYING|FLOATING)
variable = TRUE
-/datum/movespeed_modifier/bulky_drag
+/datum/movespeed_modifier/grabbing
variable = TRUE
/datum/movespeed_modifier/cold
diff --git a/code/modules/multiz/movement.dm b/code/modules/multiz/movement.dm
new file mode 100644
index 000000000000..2bab8a8a5634
--- /dev/null
+++ b/code/modules/multiz/movement.dm
@@ -0,0 +1,273 @@
+/// A step() variant that allows passing z_movement_flags. Normal step() is fine if you do not need special movement flags.
+/proc/zstep(atom/movable/mover, dir, z_movement_flags)
+ if(!istype(mover))
+ return
+
+ var/turf/destination = get_step_multiz(mover, dir)
+ if(!mover.can_z_move(dir, z_move_flags = z_movement_flags))
+ return FALSE
+
+ if(!destination)
+ if(z_movement_flags & ZMOVE_FEEDBACK)
+ to_chat(mover, span_warning("There is nothing of interest in that direction."))
+ return FALSE
+ return mover.Move(destination, dir, null, z_movement_flags)
+
+/atom/movable/proc/onZImpact(turf/impacted_turf, levels, message = TRUE)
+ if(message)
+ visible_message(
+ span_danger("[src] slams into [impacted_turf]!"),
+ blind_message = span_hear("You hear something slam into the deck.")
+ )
+
+ INVOKE_ASYNC(src, PROC_REF(SpinAnimation), 5, 2)
+ return TRUE
+
+/**
+ * Checks if the destination turf is elegible for z movement from the start turf to a given direction and returns it if so.
+ * Args:
+ * * direction: the direction to go, UP or DOWN, only relevant if target is null.
+ * * start: Each destination has a starting point on the other end. This is it. Most of the times the location of the source.
+ * * z_move_flags: bitflags used for various checks. See __DEFINES/movement.dm.
+ * * rider: A living mob in control of the movable. Only non-null when a mob is riding a vehicle through z-levels.
+ */
+/atom/movable/proc/can_z_move(direction, turf/start, z_move_flags = ZMOVE_FLIGHT_FLAGS, mob/living/rider)
+ if(!start)
+ start = get_turf(src)
+ if(!start)
+ CRASH("Something tried to zMove from nullspace.")
+
+ if(!(direction & (UP|DOWN)))
+ CRASH("can_z_move() received an invalid direction ([direction || "null"])")
+
+ var/turf/destination = get_step_multiz(start, direction)
+ if(!destination)
+ if(z_move_flags & ZMOVE_FEEDBACK)
+ to_chat(rider || src, span_notice("There is nothing of interest in this direction."))
+ return FALSE
+
+ // Check flight
+ if(z_move_flags & ZMOVE_CAN_FLY_CHECKS)
+ if(!(movement_type & (FLYING|FLOATING)) && has_gravity(start) && (direction == UP))
+ if(z_move_flags & ZMOVE_FEEDBACK)
+ if(rider)
+ to_chat(rider, span_warning("[src] is is not capable of flight."))
+ else
+ to_chat(src, span_warning("You stare at [destination]."))
+ return FALSE
+
+ // Check CanZPass
+ if(!(z_move_flags & ZMOVE_IGNORE_OBSTACLES))
+ // Check exit
+ if(!start.CanZPass(src, direction, z_move_flags))
+ if(z_move_flags & ZMOVE_FEEDBACK)
+ to_chat(rider || src, span_warning("[start] is in the way."))
+ return FALSE
+
+ // Check enter
+ if(!destination.CanZPass(src, direction, z_move_flags))
+ if(z_move_flags & ZMOVE_FEEDBACK)
+ to_chat(rider || src, span_warning("You bump against [destination]."))
+ return FALSE
+
+ if(!(z_move_flags & ZMOVE_SKIP_CANMOVEONTO))
+ // Check destination movable CanPass
+ for(var/atom/movable/A as anything in destination)
+ if(!A.CanMoveOnto(src, direction))
+ if(z_move_flags & ZMOVE_FEEDBACK)
+ to_chat(rider || src, span_warning("You are blocked by [A]."))
+ return FALSE
+
+ // Check if we would fall.
+ if(z_move_flags & ZMOVE_FALL_CHECKS)
+ if((throwing || (movement_type & (FLYING|FLOATING)) || !has_gravity(start)))
+ if(z_move_flags & ZMOVE_FEEDBACK)
+ to_chat(rider || src, span_warning("You see nothing to hold onto."))
+ return FALSE
+
+ return destination //used by some child types checks and zMove()
+
+/// Precipitates a movable (plus whatever buckled to it) to lower z levels if possible and then calls zImpact()
+/atom/movable/proc/zFall(levels = 1, force = FALSE)
+ if(QDELETED(src))
+ return FALSE
+
+ var/direction = DOWN
+ if(has_gravity() == NEGATIVE_GRAVITY)
+ direction = UP
+
+ var/turf/target = direction == UP ? GetAbove(src) : GetBelow(src)
+ if(!target)
+ return FALSE
+
+ var/isliving = isliving(src)
+ if(!isliving && !isobj(src))
+ return
+
+ if(isliving)
+ var/mob/living/falling_living = src
+ //relay this mess to whatever the mob is buckled to.
+ if(falling_living.buckled)
+ return falling_living.buckled.zFall(levels, force)
+
+ if(!force && !can_z_move(direction, get_turf(src), direction == DOWN ? ZMOVE_SKIP_CANMOVEONTO|ZMOVE_FALL_FLAGS : ZMOVE_FALL_FLAGS))
+ return FALSE
+
+ if(!CanZFall(get_turf(src), direction))
+ return FALSE
+
+ . = TRUE
+
+ spawn(0)
+ _doZFall(target, get_turf(src), levels)
+
+/atom/movable/proc/_doZFall(turf/destination, turf/prev_turf, levels)
+ PRIVATE_PROC(TRUE)
+ SHOULD_NOT_OVERRIDE(TRUE)
+
+ if(QDELETED(src))
+ return
+
+ forceMoveWithGroup(destination, z_movement = ZMOVING_VERTICAL)
+ destination.zImpact(src, levels, prev_turf)
+
+/atom/movable/proc/CanZFall(turf/from, direction, anchor_bypass)
+ if(anchored && !anchor_bypass)
+ return FALSE
+
+ if(from)
+ for(var/obj/O in from)
+ if(O.obj_flags & BLOCK_Z_FALL)
+ return FALSE
+
+ var/turf/other = direction == UP ? GetAbove(from) : GetBelow(from)
+ if(!from.CanZPass(from, direction) || !other?.CanZPass(from, direction)) //Kinda hacky but it does work.
+ return FALSE
+
+ return TRUE
+
+/// Returns an object we can climb onto
+/atom/movable/proc/check_zclimb()
+ var/turf/above = GetAbove(src)
+ if(!above?.CanZPass(src, UP))
+ return
+
+ var/list/all_turfs_above = get_adjacent_open_turfs(above)
+
+ //Check directly above first
+ . = get_climbable_surface(above)
+ if(.)
+ return
+
+ //Next, try the direction the mob is facing
+ . = get_climbable_surface(get_step(above, dir))
+ if(.)
+ return
+
+ for(var/turf/T as turf in all_turfs_above)
+ . = get_climbable_surface(T)
+ if(.)
+ return
+
+/atom/movable/proc/get_climbable_surface(turf/T)
+ var/climb_target
+ if(!T.Enter(src))
+ return
+ if(!isopenspaceturf(T) && isfloorturf(T))
+ climb_target = T
+ else
+ for(var/obj/I in T)
+ if(I.obj_flags & BLOCK_Z_FALL)
+ climb_target = I
+ break
+
+ if(climb_target)
+ return climb_target
+
+/atom/movable/proc/ClimbUp(atom/onto)
+ if(!isturf(loc))
+ return FALSE
+
+ var/turf/above = GetAbove(src)
+ var/turf/destination = get_turf(onto)
+ if(!above || !above.Adjacent(destination, mover = src))
+ return FALSE
+
+ if(has_gravity() > 0)
+ var/can_overcome
+ for(var/atom/A in loc)
+ if(HAS_TRAIT(A, TRAIT_CLIMBABLE))
+ can_overcome = TRUE
+ break;
+
+ if(!can_overcome)
+ var/list/objects_to_stand_on = list(
+ /obj/structure/chair,
+ /obj/structure/bed,
+ /obj/structure/lattice
+ )
+ for(var/path in objects_to_stand_on)
+ if(locate(path) in loc)
+ can_overcome = TRUE
+ break;
+ if(!can_overcome)
+ to_chat(src, span_warning("You cannot reach [onto] from here!"))
+ return FALSE
+
+ visible_message(
+ span_notice("[src] starts climbing onto \the [onto]."),
+ span_notice("You start climbing onto \the [onto].")
+ )
+
+ if(!do_after(src, time = 5 SECONDS, timed_action_flags = DO_PUBLIC, display = image('icons/hud/do_after.dmi', "help")))
+ return FALSE
+
+ visible_message(
+ span_notice("[src] climbs onto \the [onto]."),
+ span_notice("You climb onto \the [onto].")
+ )
+
+ var/oldloc = loc
+ setDir(get_dir(above, destination))
+ . = Move(destination, null, null, ZMOVE_INCAPACITATED_CHECKS)
+ if(.)
+ playsound(oldloc, 'sound/effects/stairs_step.ogg', 50)
+ playsound(destination, 'sound/effects/stairs_step.ogg', 50)
+
+/// Returns a list of movables that should also be affected when calling forceMoveWithGroup()
+/atom/movable/proc/get_move_group()
+ SHOULD_CALL_PARENT(TRUE)
+ RETURN_TYPE(/list)
+ . = list(src)
+ if(length(buckled_mobs))
+ . |= buckled_mobs
+
+/// forceMove() wrapper to include things like buckled mobs.
+/atom/movable/proc/forceMoveWithGroup(destination, z_movement)
+ var/list/movers = get_move_group()
+ for(var/atom/movable/AM as anything in movers)
+ if(z_movement)
+ AM.set_currently_z_moving(z_movement)
+ AM.forceMove(destination)
+ if(z_movement)
+ AM.set_currently_z_moving(FALSE)
+/*
+/**
+ * We want to relay the zmovement to the buckled atom when possible
+ * and only run what we can't have on buckled.zMove() or buckled.can_z_move() here.
+ * This way we can avoid esoteric bugs, copypasta and inconsistencies.
+ */
+/mob/living/zMove(dir, turf/target, z_move_flags = ZMOVE_FLIGHT_FLAGS)
+ if(buckled)
+ if(buckled.currently_z_moving)
+ return FALSE
+ if(!(z_move_flags & ZMOVE_ALLOW_BUCKLED))
+ buckled.unbuckle_mob(src, force = TRUE, can_fall = FALSE)
+ else
+ if(!target)
+ target = can_z_move(dir, get_turf(src), z_move_flags, src)
+ if(!target)
+ return FALSE
+ return buckled.zMove(dir, target, z_move_flags) // Return value is a loc.
+ return ..()
+*/
diff --git a/code/modules/projectiles/aiming.dm b/code/modules/projectiles/aiming.dm
index 8b40dc161b4c..968ca42c5552 100644
--- a/code/modules/projectiles/aiming.dm
+++ b/code/modules/projectiles/aiming.dm
@@ -34,7 +34,7 @@
RegisterSignal(user, COMSIG_PARENT_QDELETING, PROC_REF(target_del))
RegisterSignal(user, COMSIG_MOVABLE_MOVED, PROC_REF(check_sight))
RegisterSignal(user, COMSIG_MOB_FIRED_GUN, PROC_REF(user_shot))
- RegisterSignal(user, list(COMSIG_PARENT_ATTACKBY, COMSIG_LIVING_GET_PULLED, COMSIG_HUMAN_DISARM_HIT), PROC_REF(trigger))
+ RegisterSignal(user, list(COMSIG_PARENT_ATTACKBY, COMSIG_ATOM_GET_GRABBED, COMSIG_HUMAN_DISARM_HIT), PROC_REF(trigger))
RegisterSignal(
user,
list(
diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm
index bbc98258780d..ffee0e6687ea 100644
--- a/code/modules/projectiles/gun.dm
+++ b/code/modules/projectiles/gun.dm
@@ -161,32 +161,33 @@
if(recoil && !tk_firing(user))
shake_camera(user, recoil + 1, recoil)
fire_sounds()
- if(!suppressed)
- if(message)
- 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
- )
- else if(pointblank)
- 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!"))
- 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
- )
+ if(suppressed || !message)
+ return
+
+ 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
+ )
+ else if(pointblank)
+ 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!"))
+ 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
+ )
/obj/item/gun/emp_act(severity)
. = ..()
diff --git a/code/modules/reagents/chemistry/reagents/drug_reagents.dm b/code/modules/reagents/chemistry/reagents/drug_reagents.dm
index da949c73599a..195039bdd985 100644
--- a/code/modules/reagents/chemistry/reagents/drug_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/drug_reagents.dm
@@ -549,16 +549,17 @@
dancer.spin(30, 2)
if(dancer.disgust < 40)
dancer.adjust_disgust(10)
- if(!dancer.pulledby)
+ if(!LAZYLEN(dancer.grabbed_by))
return
var/dancer_turf = get_turf(dancer)
- var/atom/movable/dance_partner = dancer.pulledby
- dance_partner.visible_message(span_danger("[dance_partner] tries to hold onto [dancer], but is thrown back!"), span_danger("You try to hold onto [dancer], but you are thrown back!"), null, COMBAT_MESSAGE_RANGE)
- var/throwtarget = get_edge_target_turf(dancer_turf, get_dir(dancer_turf, get_step_away(dance_partner, dancer_turf)))
- if(overdosed)
- dance_partner.throw_at(target = throwtarget, range = 7, speed = 4)
- else
- dance_partner.throw_at(target = throwtarget, range = 4, speed = 1) //superspeed
+ for(var/obj/item/hand_item/grab/G in dancer.grabbed_by)
+ var/atom/movable/dance_partner = G.assailant
+ dance_partner.visible_message(span_danger("[dance_partner] tries to hold onto [dancer], but is thrown back!"), span_danger("You try to hold onto [dancer], but you are thrown back!"), null, COMBAT_MESSAGE_RANGE)
+ var/throwtarget = get_edge_target_turf(dancer_turf, get_dir(dancer_turf, get_step_away(dance_partner, dancer_turf)))
+ if(overdosed)
+ dance_partner.throw_at(target = throwtarget, range = 7, speed = 4)
+ else
+ dance_partner.throw_at(target = throwtarget, range = 4, speed = 1) //superspeed
/datum/reagent/drug/saturnx
name = "SaturnX"
diff --git a/code/modules/recycling/conveyor.dm b/code/modules/recycling/conveyor.dm
index b16c91286531..b87445232e99 100644
--- a/code/modules/recycling/conveyor.dm
+++ b/code/modules/recycling/conveyor.dm
@@ -295,11 +295,13 @@ GLOBAL_LIST_EMPTY(conveyors_by_id)
// attack with hand, move pulled object onto conveyor
-/obj/machinery/conveyor/attack_hand(mob/user, list/modifiers)
+/obj/machinery/conveyor/attack_hand(mob/living/user, list/modifiers)
. = ..()
if(.)
return
- user.Move_Pulled(src)
+
+ if(!istype(user))
+ user.move_grabbed_atoms_towards(src)
/obj/machinery/conveyor/power_change()
. = ..()
diff --git a/code/modules/recycling/sortingmachinery.dm b/code/modules/recycling/sortingmachinery.dm
index 511b1d05759a..f2ede34647d7 100644
--- a/code/modules/recycling/sortingmachinery.dm
+++ b/code/modules/recycling/sortingmachinery.dm
@@ -381,7 +381,7 @@
new_barcode.cut_multiplier = cut_multiplier // Also the registered percent cut.
user.put_in_hands(new_barcode)
-/obj/item/sales_tagger/CtrlClick(mob/user)
+/obj/item/sales_tagger/CtrlClick(mob/user, list/params)
. = ..()
payments_acc = null
to_chat(user, span_notice("You clear the registered account."))
diff --git a/code/modules/religion/religion_structures.dm b/code/modules/religion/religion_structures.dm
index adc4c2e53b9e..c9c41175cb3b 100644
--- a/code/modules/religion/religion_structures.dm
+++ b/code/modules/religion/religion_structures.dm
@@ -30,20 +30,21 @@
new_overlays += "convertaltarcandle"
return new_overlays
-/obj/structure/altar_of_gods/attack_hand(mob/living/user, list/modifiers)
- if(!Adjacent(user) || !user.pulling)
- return ..()
- if(!isliving(user.pulling))
- return ..()
- var/mob/living/pushed_mob = user.pulling
+/obj/structure/altar_of_gods/attack_grab(mob/living/user, atom/movable/victim, obj/item/hand_item/grab/grab, list/params)
+ . = ..()
+ if(!isliving(victim))
+ return
+
+ var/mob/living/pushed_mob = victim
if(pushed_mob.buckled)
to_chat(user, span_warning("[pushed_mob] is buckled to [pushed_mob.buckled]!"))
- return ..()
+ return TRUE
+
to_chat(user, span_notice("You try to coax [pushed_mob] onto [src]..."))
if(!do_after(user,(5 SECONDS),target = pushed_mob))
- return ..()
+ return TRUE
pushed_mob.forceMove(loc)
- return ..()
+ return TRUE
/obj/structure/altar_of_gods/examine_more(mob/user)
if(!isobserver(user))
@@ -104,7 +105,7 @@
new /obj/effect/decal/cleanable/ash(drop_location())
qdel(src)
-/obj/item/ritual_totem/can_be_pulled(user, grab_state, force)
+/obj/item/ritual_totem/can_be_grabbed(mob/living/grabber, target_zone, force)
. = ..()
return FALSE //no
diff --git a/code/modules/research/designs/wiremod_designs.dm b/code/modules/research/designs/wiremod_designs.dm
index dc5443e58006..4ae96e9a1b21 100644
--- a/code/modules/research/designs/wiremod_designs.dm
+++ b/code/modules/research/designs/wiremod_designs.dm
@@ -218,11 +218,6 @@
id = "comp_split"
build_path = /obj/item/circuit_component/split
-/datum/design/component/pull
- name = "Pull Component"
- id = "comp_pull"
- build_path = /obj/item/circuit_component/pull
-
/datum/design/component/soundemitter
name = "Sound Emitter Component"
id = "comp_soundemitter"
diff --git a/code/modules/research/xenobiology/crossbreeding/_status_effects.dm b/code/modules/research/xenobiology/crossbreeding/_status_effects.dm
index 2f0872a673e0..e59677a3674c 100644
--- a/code/modules/research/xenobiology/crossbreeding/_status_effects.dm
+++ b/code/modules/research/xenobiology/crossbreeding/_status_effects.dm
@@ -882,11 +882,11 @@
/datum/status_effect/stabilized/black/proc/on_grab(mob/living/source, new_state)
SIGNAL_HANDLER
- if(new_state < GRAB_KILL || !isliving(source.pulling))
+ if(new_state < GRAB_KILL || !isliving(source.get_active_grab()?.affecting))
draining_ref = null
return
- var/mob/living/draining = source.pulling
+ var/mob/living/draining = source.get_active_grab()?.affecting
if(draining.stat == DEAD)
return
@@ -902,7 +902,8 @@
return span_warning("[owner.p_they(TRUE)] [owner.p_are()] draining health from [draining]!")
/datum/status_effect/stabilized/black/tick()
- if(owner.grab_state < GRAB_KILL || !IS_WEAKREF_OF(owner.pulling, draining_ref))
+ var/obj/item/hand_item/grab/G = owner.get_active_grab()
+ if(G.current_grab.damage_stage < GRAB_KILL || !IS_WEAKREF_OF(G.affecting, draining_ref))
return
var/mob/living/drained = draining_ref.resolve()
diff --git a/code/modules/research/xenobiology/xenobio_camera.dm b/code/modules/research/xenobiology/xenobio_camera.dm
index e7bd35a169ce..baaa3b87ee61 100644
--- a/code/modules/research/xenobiology/xenobio_camera.dm
+++ b/code/modules/research/xenobiology/xenobio_camera.dm
@@ -322,17 +322,17 @@
..()
//scans slimes
-/mob/living/simple_animal/slime/CtrlClick(mob/user)
+/mob/living/simple_animal/slime/CtrlClick(mob/user, list/params)
SEND_SIGNAL(user, COMSIG_XENO_SLIME_CLICK_CTRL, src)
..()
//picks up dead monkies
-/mob/living/carbon/human/species/monkey/CtrlClick(mob/user)
+/mob/living/carbon/human/species/monkey/CtrlClick(mob/user, list/params)
SEND_SIGNAL(user, COMSIG_XENO_MONKEY_CLICK_CTRL, src)
..()
//places monkies
-/turf/open/CtrlClick(mob/user)
+/turf/open/CtrlClick(mob/user, list/params)
SEND_SIGNAL(user, COMSIG_XENO_TURF_CLICK_CTRL, src)
..()
diff --git a/code/modules/shuttle/on_move.dm b/code/modules/shuttle/on_move.dm
index 9e3d2289b2ea..fe5df4134cc6 100644
--- a/code/modules/shuttle/on_move.dm
+++ b/code/modules/shuttle/on_move.dm
@@ -28,9 +28,7 @@ All ShuttleMove procs go here
var/mob/living/M = thing
if(M.buckled)
M.buckled.unbuckle_mob(M, 1)
- if(M.pulledby)
- M.pulledby.stop_pulling()
- M.stop_pulling()
+ M.release_all_grabs()
M.visible_message(span_warning("[shuttle] slams into [M]!"))
SSblackbox.record_feedback("tally", "shuttle_gib", 1, M.type)
log_attack("[key_name(M)] was shuttle gibbed by [shuttle].")
diff --git a/code/modules/shuttle/special.dm b/code/modules/shuttle/special.dm
index b33cee2b2dc6..d695e9e7c42b 100644
--- a/code/modules/shuttle/special.dm
+++ b/code/modules/shuttle/special.dm
@@ -226,183 +226,6 @@
if(ID && (ACCESS_CENT_BAR in ID.access))
return TRUE
-//Luxury Shuttle Blockers
-
-/obj/machinery/scanner_gate/luxury_shuttle
- name = "luxury shuttle ticket field"
- density = FALSE //allows shuttle airlocks to close, nothing but an approved passenger gets past CanPass
- locked = TRUE
- use_power = NO_POWER_USE
- resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
- speech_span = SPAN_ROBOT
- var/threshold = 500
- var/static/list/approved_passengers = list()
- var/static/list/check_times = list()
- var/list/payees = list()
-
-/obj/machinery/scanner_gate/luxury_shuttle/CanAllowThrough(atom/movable/mover, border_dir)
- . = ..()
-
- if(mover in approved_passengers)
- set_scanline("scanning", 10)
- if(isvehicle(mover))
- var/obj/vehicle/vehicle = mover
- for(var/mob/living/rat in vehicle.occupants)
- if(!(rat in approved_passengers))
- say("Stowaway detected. Please exit the vehicle first.")
- return FALSE
- return TRUE
- if(isitem(mover))
- return TRUE
- if(isstructure(mover))
- var/obj/structure/struct = mover
- for(var/mob/living/rat in struct.contents)
- say("Stowaway detected. Please exit the structure first.")
- return FALSE
- return TRUE
-
- return FALSE
-
-/obj/machinery/scanner_gate/luxury_shuttle/auto_scan(atom/movable/AM)
- return
-
-/obj/machinery/scanner_gate/luxury_shuttle/attackby(obj/item/W, mob/user, params)
- return
-
-/obj/machinery/scanner_gate/luxury_shuttle/emag_act(mob/user)
- return
-
-#define LUXURY_MESSAGE_COOLDOWN 100
-/obj/machinery/scanner_gate/luxury_shuttle/BumpedBy(atom/movable/AM)
- ///If the atom entering the gate is a vehicle, we store it here to add to the approved list to enter/leave the scanner gate.
- var/obj/vehicle/vehicle
- ///We store the driver of vehicles separately so that we can add them to the approved list once payment is fully processed.
- var/mob/living/driver_holdout
- if(!isliving(AM) && !isvehicle(AM))
- alarm_beep()
- return ..()
-
- var/datum/bank_account/account
- if(istype(AM.pulling, /obj/item/card/id))
- var/obj/item/card/id/I = AM.pulling
- if(I.registered_account)
- account = I.registered_account
- else if(!check_times[AM] || check_times[AM] < world.time) //Let's not spam the message
- to_chat(AM, span_notice("This ID card doesn't have an owner associated with it!"))
- check_times[AM] = world.time + LUXURY_MESSAGE_COOLDOWN
- else if(isliving(AM))
- var/mob/living/L = AM
- account = L.get_bank_account()
-
- else if(isvehicle(AM))
- vehicle = AM
- for(var/passenger in vehicle.occupants)
- if(!isliving(passenger))
- continue
- var/mob/living/rider = passenger
- if(vehicle.is_driver(rider))
- driver_holdout = rider
- var/obj/item/card/id/id = rider.get_idcard(TRUE)
- account = id?.registered_account
- break
-
- if(account)
- if(account.account_balance < threshold - payees[AM])
- account.adjust_money(-account.account_balance)
- payees[AM] += account.account_balance
- else
- var/money_owed = threshold - payees[AM]
- account.adjust_money(-money_owed)
- payees[AM] += money_owed
-
- //Here is all the possible paygate payment methods.
- var/list/counted_money = list()
- for(var/obj/item/coin/C in AM.get_all_contents()) //Coins.
- if(payees[AM] >= threshold)
- break
- payees[AM] += C.value
- counted_money += C
- for(var/obj/item/stack/spacecash/S in AM.get_all_contents()) //Paper Cash
- if(payees[AM] >= threshold)
- break
- payees[AM] += S.value * S.amount
- counted_money += S
- for(var/obj/item/holochip/H in AM.get_all_contents()) //Holocredits
- if(payees[AM] >= threshold)
- break
- payees[AM] += H.credits
- counted_money += H
-
- if(payees[AM] < threshold && istype(AM.pulling, /obj/item/coin)) //Coins(Pulled).
- var/obj/item/coin/C = AM.pulling
- payees[AM] += C.value
- counted_money += C
-
- else if(payees[AM] < threshold && istype(AM.pulling, /obj/item/stack/spacecash)) //Cash(Pulled).
- var/obj/item/stack/spacecash/S = AM.pulling
- payees[AM] += S.value * S.amount
- counted_money += S
-
- else if(payees[AM] < threshold && istype(AM.pulling, /obj/item/holochip)) //Holocredits(pulled).
- var/obj/item/holochip/H = AM.pulling
- payees[AM] += H.credits
- counted_money += H
-
- if(payees[AM] < threshold) //Suggestions for those with no arms/simple animals.
- var/armless
- if(!ishuman(AM) && !istype(AM, /mob/living/simple_animal/slime))
- armless = TRUE
- else
- var/mob/living/carbon/human/H = AM
- if(!H.get_bodypart(BODY_ZONE_L_ARM) && !H.get_bodypart(BODY_ZONE_R_ARM))
- armless = TRUE
-
- if(armless)
- if(!AM.pulling || !iscash(AM.pulling) && !istype(AM.pulling, /obj/item/card/id))
- if(!check_times[AM] || check_times[AM] < world.time) //Let's not spam the message
- to_chat(AM, span_notice("Try pulling a valid ID, space cash, holochip or coin into \the [src]!"))
- check_times[AM] = world.time + LUXURY_MESSAGE_COOLDOWN
-
- if(payees[AM] >= threshold)
- for(var/obj/I in counted_money)
- qdel(I)
- payees[AM] -= threshold
-
- var/change = FALSE
- if(payees[AM] > 0)
- change = TRUE
- var/obj/item/holochip/HC = new /obj/item/holochip(AM.loc) //Change is made in holocredits exclusively.
- HC.credits = payees[AM]
- HC.name = "[HC.credits] credit holochip"
- if(istype(AM, /mob/living/carbon/human))
- var/mob/living/carbon/human/H = AM
- if(!H.put_in_hands(HC))
- AM.pulling = HC
- else
- AM.pulling = HC
- payees[AM] -= payees[AM]
-
- say("Welcome to first class, [driver_holdout ? "[driver_holdout]" : "[AM]" ]![change ? " Here is your change." : ""]")
- approved_passengers |= AM
- if(vehicle)
- approved_passengers |= vehicle
- if(driver_holdout)
- approved_passengers |= driver_holdout
-
- check_times -= AM
- return
- else if (payees[AM] > 0)
- for(var/obj/I in counted_money)
- qdel(I)
- if(!check_times[AM] || check_times[AM] < world.time) //Let's not spam the message
- to_chat(AM, span_notice("[payees[AM]] cr received. You need [threshold-payees[AM]] cr more."))
- check_times[AM] = world.time + LUXURY_MESSAGE_COOLDOWN
- alarm_beep()
- return ..()
- else
- alarm_beep()
- return ..()
-
/mob/living/simple_animal/hostile/bear/fightpit
name = "fight pit bear"
desc = "This bear's trained through ancient Russian secrets to fear the walls of its glass prison."
diff --git a/code/modules/spells/spell_types/jaunt/bloodcrawl.dm b/code/modules/spells/spell_types/jaunt/bloodcrawl.dm
index 4cbbca3d9990..0216650f04f1 100644
--- a/code/modules/spells/spell_types/jaunt/bloodcrawl.dm
+++ b/code/modules/spells/spell_types/jaunt/bloodcrawl.dm
@@ -163,7 +163,7 @@
/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/try_enter_jaunt(obj/effect/decal/cleanable/blood, mob/living/jaunter)
// Save this before the actual jaunt
- var/atom/coming_with = jaunter.pulling
+ var/atom/coming_with = jaunter.get_active_grab()?.affecting
// Does the actual jaunt
. = ..()
diff --git a/code/modules/spells/spell_types/self/charge.dm b/code/modules/spells/spell_types/self/charge.dm
index 87d7ae287d33..d8ee8db134f8 100644
--- a/code/modules/spells/spell_types/self/charge.dm
+++ b/code/modules/spells/spell_types/self/charge.dm
@@ -21,8 +21,8 @@
. = ..()
// Charge people we're pulling first and foremost
- if(isliving(cast_on.pulling))
- var/mob/living/pulled_living = cast_on.pulling
+ if(isliving(cast_on.get_active_grab()?.affecting))
+ var/mob/living/pulled_living = cast_on.get_active_grab()?.affecting
var/pulled_has_spells = FALSE
for(var/datum/action/cooldown/spell/spell in pulled_living.actions)
diff --git a/code/modules/surgery/bodyparts/_bodyparts.dm b/code/modules/surgery/bodyparts/_bodyparts.dm
index d5240011597d..99a4daed1980 100644
--- a/code/modules/surgery/bodyparts/_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/_bodyparts.dm
@@ -124,6 +124,8 @@
var/artery_name = "artery"
/// The name of the tendon this limb has
var/tendon_name = "tendon"
+ /// The name of the joint you can dislocate
+ var/joint_name = "joint"
/// The name for the amputation point of the limb
var/amputation_point
/// Surgical stage. Magic BS. Do not touch
@@ -187,8 +189,6 @@
/// How much generic bleedstacks we have on this bodypart
var/generic_bleedstacks
- /// If something is currently grasping this bodypart and trying to staunch bleeding (see [/obj/item/hand_item/self_grasp])
- var/obj/item/hand_item/self_grasp/grasped_by
/// If something is currently supporting this limb as a splint
var/obj/item/splint
/// The bandage that may-or-may-not be absorbing our blood
@@ -262,7 +262,7 @@
. = ..()
. += mob_examine()
-/obj/item/bodypart/proc/mob_examine(hallucinating, covered)
+/obj/item/bodypart/proc/mob_examine(hallucinating, covered, just_wounds_please)
. = list()
if(covered)
@@ -330,6 +330,9 @@
if(6 to INFINITY)
flavor_text += "a ton of [wound]\s"
+ if(just_wounds_please)
+ return english_list(flavor_text)
+
if(owner)
if(current_damage)
. += "[owner.p_they(TRUE)] [owner.p_have()] [english_list(flavor_text)] on [owner.p_their()] [plaintext_zone]."
@@ -1014,9 +1017,6 @@
cached_bleed_rate += round(iter_wound.damage / 40, DAMAGE_PRECISION)
bodypart_flags |= BP_BLEEDING
- if(!cached_bleed_rate)
- QDEL_NULL(grasped_by)
-
// Our bleed overlay is based directly off bleed_rate, so go aheead and update that would you?
if(cached_bleed_rate != old_bleed_rate)
update_part_wound_overlay()
@@ -1028,8 +1028,9 @@
var/bleed_rate = cached_bleed_rate
if(owner.body_position == LYING_DOWN)
bleed_rate *= 0.75
- if(grasped_by)
- bleed_rate *= 0.7
+
+ if(HAS_TRAIT(src, TRAIT_BODYPART_GRABBED))
+ bleed_rate *= 0.4
if(bandage)
bleed_rate *= bandage.absorption_rate_modifier
@@ -1303,6 +1304,9 @@
if(check_bones() & CHECKBONES_BROKEN)
. += tag ? "Fractured" : "Fractured"
+ if(bodypart_flags & BP_DISLOCATED)
+ . += tag ? "Dislocated" : "Dislocated"
+
if (length(cavity_items) || length(embedded_objects))
var/unknown_body = 0
for(var/obj/item/I in cavity_items + embedded_objects)
@@ -1371,3 +1375,42 @@
else
user.visible_message(span_notice("[user] removes [removed] from [owner]'s [plaintext_zone]."))
return
+
+/obj/item/bodypart/proc/inspect(mob/user)
+ if(is_stump)
+ to_chat(user, span_notice("[owner] is missing that bodypart."))
+ return
+
+ user.visible_message(span_notice("[user] starts inspecting [owner]'s [plaintext_zone] carefully."))
+ if(LAZYLEN(wounds))
+ to_chat(user, span_warning("You find [mob_examine(just_wounds_please = TRUE)]."))
+ var/list/stuff = list()
+ for(var/datum/wound/wound as anything in wounds)
+ if(LAZYLEN(wound.embedded_objects))
+ stuff |= wound.embedded_objects
+
+ if(length(stuff))
+ to_chat(user, span_warning("There's [english_list(stuff)] sticking out of [owner]'s [plaintext_zone]."))
+ else
+ to_chat(user, span_notice("You find no visible wounds."))
+
+ to_chat(user, span_notice("Checking skin now..."))
+
+ if(!do_after(user, owner, 1 SECOND, DO_PUBLIC))
+ return
+
+ to_chat(user, span_notice("Checking bones now..."))
+ if(!do_after(user, owner, 1 SECOND, DO_PUBLIC))
+ return
+
+ if(bodypart_flags & BP_BROKEN_BONES)
+ to_chat(user, span_warning("The [encased ? encased : "bone in the [plaintext_zone]"] moves slightly when you poke it!"))
+ owner.apply_pain(40, body_zone, "Your [plaintext_zone] hurts where it's poked.")
+ else
+ to_chat(user, span_notice("The [encased ? encased : "bones in the [plaintext_zone]"] seem to be fine."))
+
+ if(bodypart_flags & BP_TENDON_CUT)
+ to_chat(user, span_warning("The tendons in the [plaintext_zone] are severed!"))
+ if(bodypart_flags & BP_DISLOCATED)
+ to_chat(user, span_warning("The [joint_name] is dislocated!"))
+ return TRUE
diff --git a/code/modules/surgery/bodyparts/head.dm b/code/modules/surgery/bodyparts/head.dm
index 42fa7dc72fae..1d26ff3f667c 100644
--- a/code/modules/surgery/bodyparts/head.dm
+++ b/code/modules/surgery/bodyparts/head.dm
@@ -31,6 +31,7 @@
encased = "skull"
artery_name = "carotid artery"
cavity_name = "cranial"
+ joint_name = "jaw"
minimum_break_damage = 30
diff --git a/code/modules/surgery/bodyparts/injuries.dm b/code/modules/surgery/bodyparts/injuries.dm
index ad7bebc09729..e9ab90d5475c 100644
--- a/code/modules/surgery/bodyparts/injuries.dm
+++ b/code/modules/surgery/bodyparts/injuries.dm
@@ -155,6 +155,31 @@
update_disabled()
return TRUE
+/obj/item/bodypart/proc/can_be_dislocated()
+ if(!(bodypart_flags & BP_CAN_BE_DISLOCATED))
+ return FALSE
+ if(bodypart_flags & BP_DISLOCATED)
+ return FALSE
+ return TRUE
+
+/obj/item/bodypart/proc/set_dislocated(val, painless)
+ if(val)
+ if(!can_be_dislocated())
+ return FALSE
+ else
+ if(can_be_dislocated())
+ return FALSE
+
+ if(val)
+ bodypart_flags |= BP_DISLOCATED
+ if(!painless)
+ owner?.apply_pain(20, body_zone, "A surge of pain shoots through your [plaintext_zone].")
+ else
+ bodypart_flags &= BP_DISLOCATED
+
+ return TRUE
+
+
/obj/item/bodypart/proc/clamp_wounds()
for(var/datum/wound/W as anything in wounds)
. ||= !W.clamped
@@ -223,3 +248,14 @@
if(!W)
return
W.open_wound(min(W.damage * 2, W.damage_list[1] - W.damage))
+
+/obj/item/bodypart/proc/jointlock(mob/living/user)
+ if(!IS_ORGANIC_LIMB(src))
+ return
+
+ var/armor = owner.run_armor_check(body_zone, MELEE, silent = TRUE)
+ if(armor > 70)
+ return
+
+ var/max_halloss = round(owner.maxHealth * 0.8 * ((100 - armor) / 100)) //up to 80% of passing out, further reduced by armour
+ owner.apply_pain(max(30, max_halloss - owner.getPain()), body_zone, "your [plaintext_zone] is in excruciating pain")
diff --git a/code/modules/surgery/bodyparts/pain.dm b/code/modules/surgery/bodyparts/pain.dm
index 4cd293d6b0d7..2da6993e59fd 100644
--- a/code/modules/surgery/bodyparts/pain.dm
+++ b/code/modules/surgery/bodyparts/pain.dm
@@ -15,10 +15,9 @@
var/lasting_pain = 0
if(bodypart_flags & BP_BROKEN_BONES)
lasting_pain += 10
- /*
- else if(is_dislocated())
+
+ else if(bodypart_flags & BP_DISLOCATED)
lasting_pain += 5
- */
var/organ_dam = 0
for(var/obj/item/organ/O as anything in contained_organs)
diff --git a/code/modules/surgery/bodyparts/parts.dm b/code/modules/surgery/bodyparts/parts.dm
index d73430c4efaa..04be40662950 100644
--- a/code/modules/surgery/bodyparts/parts.dm
+++ b/code/modules/surgery/bodyparts/parts.dm
@@ -16,6 +16,7 @@
encased = "ribcage"
artery_name = "aorta"
cavity_name = "thoracic"
+ joint_name = "neck"
minimum_break_damage = 35
@@ -105,6 +106,7 @@
px_y = 0
bodypart_trait_source = LEFT_ARM_TRAIT
amputation_point = "left shoulder"
+ joint_name = "left elbow"
/obj/item/bodypart/arm/left/set_owner(new_owner)
@@ -188,6 +190,7 @@
bodypart_trait_source = RIGHT_ARM_TRAIT
can_be_disabled = TRUE
amputation_point = "right shoulder"
+ joint_name = "right elbow"
/obj/item/bodypart/arm/right/set_owner(new_owner)
. = ..()
@@ -297,6 +300,7 @@
can_be_disabled = TRUE
bodypart_trait_source = LEFT_LEG_TRAIT
amputation_point = "left hip"
+ joint_name = "left knee"
/obj/item/bodypart/leg/left/set_owner(new_owner)
@@ -377,6 +381,7 @@
bodypart_trait_source = RIGHT_LEG_TRAIT
can_be_disabled = TRUE
amputation_point = "right hip"
+ joint_name = "right knee"
/obj/item/bodypart/leg/right/set_owner(new_owner)
. = ..()
diff --git a/code/modules/surgery/organs/external/wings.dm b/code/modules/surgery/organs/external/wings.dm
index d07c14dffd15..4e7b89dc6fa9 100644
--- a/code/modules/surgery/organs/external/wings.dm
+++ b/code/modules/surgery/organs/external/wings.dm
@@ -106,7 +106,7 @@
var/olddir = human.dir
- human.stop_pulling()
+ human.release_all_grabs()
if(buckled_obj)
buckled_obj.unbuckle_mob(human)
step(buckled_obj, olddir)
diff --git a/code/modules/tables/tables_racks.dm b/code/modules/tables/tables_racks.dm
index a43f8b267c16..ad16f54b11c9 100644
--- a/code/modules/tables/tables_racks.dm
+++ b/code/modules/tables/tables_racks.dm
@@ -98,10 +98,6 @@
/obj/structure/table/attack_paw(mob/user, list/modifiers)
return attack_hand(user, modifiers)
-/obj/structure/table/attack_hand(mob/living/user, list/modifiers)
- try_place_pulled_onto_table(user)
- return ..()
-
/obj/structure/table/attack_tk(mob/user)
return
@@ -238,24 +234,30 @@
else
layer = TABLE_LAYER
-/obj/structure/table/proc/try_place_pulled_onto_table(mob/living/user)
- if(!Adjacent(user) || !user.pulling)
+/obj/structure/table/attack_grab(mob/living/user, atom/movable/victim, obj/item/hand_item/grab/grab, list/params)
+ try_place_pulled_onto_table(user, victim, grab)
+ return TRUE
+
+/obj/structure/table/proc/try_place_pulled_onto_table(mob/living/user, atom/movable/target, obj/item/hand_item/grab/grab)
+ if(!Adjacent(user))
return
- if(isliving(user.pulling))
- var/mob/living/pushed_mob = user.pulling
+ if(isliving(target))
+ var/mob/living/pushed_mob = target
if(pushed_mob.buckled)
to_chat(user, span_warning("[pushed_mob] is buckled to [pushed_mob.buckled]!"))
return
if(user.combat_mode)
- switch(user.grab_state)
+ switch(grab.current_grab.damage_stage)
if(GRAB_PASSIVE)
to_chat(user, span_warning("You need a better grip to do that!"))
return
- if(GRAB_AGGRESSIVE)
+
+ if(GRAB_NECK, GRAB_KILL)
tablepush(user, pushed_mob)
- if(GRAB_NECK to GRAB_KILL)
- tablelimbsmash(user, pushed_mob)
+ else
+ if(grab.target_zone == BODY_ZONE_HEAD)
+ tablelimbsmash(user, pushed_mob)
else
pushed_mob.visible_message(span_notice("[user] begins to place [pushed_mob] onto [src]..."), \
span_userdanger("[user] begins to place [pushed_mob] onto [src]..."))
@@ -263,13 +265,15 @@
tableplace(user, pushed_mob)
else
return
- user.stop_pulling()
- else if(user.pulling.pass_flags & PASSTABLE)
- user.Move_Pulled(src)
- if (user.pulling.loc == loc)
- user.visible_message(span_notice("[user] places [user.pulling] onto [src]."),
- span_notice("You place [user.pulling] onto [src]."))
- user.stop_pulling()
+ user.release_grabs(pushed_mob)
+
+ else if(target.pass_flags & PASSTABLE)
+ user.move_grabbed_atoms_towards(src)
+ if (target.loc == loc)
+ user.visible_message(span_notice("[user] places [target] onto [src]."),
+ span_notice("You place [target] onto [src]."))
+
+ user.release_grabs(target)
/obj/structure/table/proc/tableplace(mob/living/user, mob/living/pushed_mob)
pushed_mob.forceMove(loc)
@@ -305,16 +309,24 @@
log_combat(user, pushed_mob, "tabled", null, "onto [src]")
/obj/structure/table/proc/tablelimbsmash(mob/living/user, mob/living/pushed_mob)
- pushed_mob.Knockdown(30)
- var/obj/item/bodypart/banged_limb = pushed_mob.get_bodypart(user.zone_selected) || pushed_mob.get_bodypart(BODY_ZONE_HEAD)
- banged_limb?.receive_damage(30)
+ var/obj/item/bodypart/banged_limb = pushed_mob.get_bodypart(BODY_ZONE_HEAD)
+ if(!banged_limb)
+ return
+
+ var/blocked = pushed_mob.run_armor_check(BODY_ZONE_HEAD, MELEE)
+ pushed_mob.apply_damage(30, BRUTE, BODY_ZONE_HEAD, blocked)
+ if (prob(30 * ((100-blocked)/100)))
+ pushed_mob.Knockdown(10 SECONDS)
+
pushed_mob.stamina.adjust(-60)
take_damage(50)
if(user.mind?.martial_art.smashes_tables && user.mind?.martial_art.can_use(user))
deconstruct(FALSE)
- playsound(pushed_mob, 'sound/effects/bang.ogg', 90, TRUE)
- pushed_mob.visible_message(span_danger("[user] smashes [pushed_mob]'s [banged_limb.name] against \the [src]!"),
- span_userdanger("[user] smashes your [banged_limb.name] against \the [src]"))
+
+ playsound(pushed_mob, 'sound/items/trayhit1.ogg', 70, TRUE)
+ pushed_mob.visible_message(
+ span_danger("[user] smashes [pushed_mob]'s [banged_limb.plaintext_zone] against \the [src]!"),
+ )
log_combat(user, pushed_mob, "head slammed", null, "against [src]")
/obj/structure/table/screwdriver_act_secondary(mob/living/user, obj/item/tool)
diff --git a/code/modules/unit_tests/chain_pull_through_space.dm b/code/modules/unit_tests/chain_pull_through_space.dm
index b718da91c2c6..4ec1f7d612c3 100644
--- a/code/modules/unit_tests/chain_pull_through_space.dm
+++ b/code/modules/unit_tests/chain_pull_through_space.dm
@@ -42,8 +42,8 @@
/datum/unit_test/chain_pull_through_space/Run()
// Alice pulls Bob, who pulls Charlie
// Normally, when Alice moves forward, the rest follow
- alice.start_pulling(bob)
- bob.start_pulling(charlie)
+ bob.try_make_grab(charlie)
+ alice.try_make_grab(bob)
// Walk normally to the left, make sure we're still a chain
alice.Move(locate(run_loc_floor_bottom_left.x + 1, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z))
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_vox.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_vox.png
index 896167f6eaf5..1917786c90cd 100644
Binary files a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_vox.png and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_vox.png differ
diff --git a/code/modules/wiremod/components/action/pull.dm b/code/modules/wiremod/components/action/pull.dm
deleted file mode 100644
index 8c83e938b00a..000000000000
--- a/code/modules/wiremod/components/action/pull.dm
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * # Pull Component
- *
- * Tells the shell to start pulling on a designated atom. Only works on movable shells.
- */
-/obj/item/circuit_component/pull
- display_name = "Start Pulling"
- desc = "A component that can force the shell to pull entities. Only works for drone shells."
- category = "Action"
-
- /// Frequency input
- var/datum/port/input/target
- circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL
-
-/obj/item/circuit_component/pull/populate_ports()
- target = add_input_port("Target", PORT_TYPE_ATOM)
-
-/obj/item/circuit_component/pull/input_received(datum/port/input/port)
- var/atom/target_atom = target.value
- if(!target_atom)
- return
-
- var/mob/shell = parent.shell
- if(!istype(shell) || get_dist(shell, target_atom) > 1 || shell.z != target_atom.z)
- return
-
- INVOKE_ASYNC(shell, TYPE_PROC_REF(/atom/movable, start_pulling), target_atom)
diff --git a/daedalus.dme b/daedalus.dme
index 8c670b34a50d..5fb691b5850d 100644
--- a/daedalus.dme
+++ b/daedalus.dme
@@ -94,6 +94,7 @@
#include "code\__DEFINES\food.dm"
#include "code\__DEFINES\footsteps.dm"
#include "code\__DEFINES\ghost.dm"
+#include "code\__DEFINES\grab_defines.dm"
#include "code\__DEFINES\gravity.dm"
#include "code\__DEFINES\greyscale.dm"
#include "code\__DEFINES\hud.dm"
@@ -1009,7 +1010,6 @@
#include "code\datums\elements\light_eater.dm"
#include "code\datums\elements\movement_turf_changer.dm"
#include "code\datums\elements\movetype_handler.dm"
-#include "code\datums\elements\nerfed_pulling.dm"
#include "code\datums\elements\obj_regen.dm"
#include "code\datums\elements\openspace_item_click_handler.dm"
#include "code\datums\elements\pet_bonus.dm"
@@ -2979,6 +2979,21 @@
#include "code\modules\food_and_drinks\restaurant\custom_order.dm"
#include "code\modules\food_and_drinks\restaurant\generic_venues.dm"
#include "code\modules\food_and_drinks\restaurant\customers\_customer.dm"
+#include "code\modules\grab\grab_carbon.dm"
+#include "code\modules\grab\grab_datum.dm"
+#include "code\modules\grab\grab_helpers.dm"
+#include "code\modules\grab\grab_living.dm"
+#include "code\modules\grab\grab_movable.dm"
+#include "code\modules\grab\grab_object.dm"
+#include "code\modules\grab\grab_silicon.dm"
+#include "code\modules\grab\human_grab.dm"
+#include "code\modules\grab\grabs\grab_aggressive.dm"
+#include "code\modules\grab\grabs\grab_neck.dm"
+#include "code\modules\grab\grabs\grab_normal.dm"
+#include "code\modules\grab\grabs\grab_passive.dm"
+#include "code\modules\grab\grabs\grab_simple.dm"
+#include "code\modules\grab\grabs\grab_strangle.dm"
+#include "code\modules\grab\grabs\grab_struggle.dm"
#include "code\modules\holiday\easter.dm"
#include "code\modules\holiday\foreign_calendar.dm"
#include "code\modules\holiday\holidays.dm"
@@ -3824,6 +3839,7 @@
#include "code\modules\movespeed\modifiers\mobs.dm"
#include "code\modules\movespeed\modifiers\reagent.dm"
#include "code\modules\movespeed\modifiers\status_effects.dm"
+#include "code\modules\multiz\movement.dm"
#include "code\modules\ninja\__ninjaDefines.dm"
#include "code\modules\ninja\energy_katana.dm"
#include "code\modules\ninja\ninja_explosive.dm"
@@ -4508,7 +4524,6 @@
#include "code\modules\wiremod\components\action\mmi.dm"
#include "code\modules\wiremod\components\action\pathfind.dm"
#include "code\modules\wiremod\components\action\printer.dm"
-#include "code\modules\wiremod\components\action\pull.dm"
#include "code\modules\wiremod\components\action\radio.dm"
#include "code\modules\wiremod\components\action\soundemitter.dm"
#include "code\modules\wiremod\components\action\speech.dm"
diff --git a/goon/icons/items/grab.dmi b/goon/icons/items/grab.dmi
new file mode 100644
index 000000000000..eb4b9699b4c2
Binary files /dev/null and b/goon/icons/items/grab.dmi differ
diff --git a/icons/hud/screen1.dmi b/icons/hud/screen1.dmi
new file mode 100644
index 000000000000..7f8db4256da5
Binary files /dev/null and b/icons/hud/screen1.dmi differ
diff --git a/icons/hud/screen_gen.dmi b/icons/hud/screen_gen.dmi
index 22e9460f0677..c2a0e48c20d4 100644
Binary files a/icons/hud/screen_gen.dmi and b/icons/hud/screen_gen.dmi differ
diff --git a/icons/mob/species/vox/bodyparts.dmi b/icons/mob/species/vox/bodyparts.dmi
index f036392f6339..8663e45da845 100644
Binary files a/icons/mob/species/vox/bodyparts.dmi and b/icons/mob/species/vox/bodyparts.dmi differ
diff --git a/modular_pariah/modules/customization/modules/clothing/glasses/glasses.dm b/modular_pariah/modules/customization/modules/clothing/glasses/glasses.dm
index e8924f08ec1a..63fddf494caf 100644
--- a/modular_pariah/modules/customization/modules/clothing/glasses/glasses.dm
+++ b/modular_pariah/modules/customization/modules/clothing/glasses/glasses.dm
@@ -2,7 +2,7 @@
var/can_switch_eye = FALSE //Having this default to false means that its easy to make sure this doesnt apply to any pre-existing items
var/current_eye = "_R" //Added to the end of the icon_state to make this easy code-wise, L and R being the wearer's Left and Right
-/obj/item/clothing/glasses/CtrlClick(mob/user)
+/obj/item/clothing/glasses/CtrlClick(mob/user, list/params)
. = ..()
if(.)
return