diff --git a/_maps/map_files/BoxStation/BoxStation.dmm b/_maps/map_files/BoxStation/BoxStation.dmm
index 722e7e2660..5fcdd2c6d6 100644
--- a/_maps/map_files/BoxStation/BoxStation.dmm
+++ b/_maps/map_files/BoxStation/BoxStation.dmm
@@ -10447,7 +10447,7 @@
/area/maintenance/port/fore)
"avV" = (
/obj/structure/table/wood,
-/obj/item/reagent_containers/food/snacks/burger/ghost,
+/obj/item/ectoplasm,
/turf/open/floor/wood,
/area/maintenance/port/fore)
"avW" = (
diff --git a/code/__DEFINES/cargo.dm b/code/__DEFINES/cargo.dm
index 40a50fe1e6..85e5e9d2ac 100644
--- a/code/__DEFINES/cargo.dm
+++ b/code/__DEFINES/cargo.dm
@@ -60,6 +60,5 @@ GLOBAL_LIST_INIT(podstyles, list(\
))
//cit
-#define PACK_GOODY_NONE 0
-#define PACK_GOODY_PUBLIC 1 //can be bought by both privates and cargo
-#define PACK_GOODY_PRIVATE 2 //can be bought only by privates
+#define PACK_GOODY_NONE 0 // can be bought by cargo and privates
+#define PACK_GOODY_PRIVATE 1 // can be bought only by privates
diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index 7933fab83f..88f9759940 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -266,6 +266,7 @@
#define COMSIG_MOB_GET_VISIBLE_MESSAGE "mob_get_visible_message" //from base of atom/visible_message(): (atom/A, msg, range, ignored_mobs)
#define COMPONENT_NO_VISIBLE_MESSAGE 1 //exactly what's said on the tin.
#define COMSIG_MOB_ANTAG_ON_GAIN "mob_antag_on_gain" //from base of /datum/antagonist/on_gain(): (antag_datum)
+#define COMSIG_MOB_APPLY_DAMAGE "mob_apply_damage" //from base of /mob/living/proc/apply_damage(): (damage, damagetype, def_zone, wound_bonus, bare_wound_bonus, sharpness)
#define COMSIG_MOB_SPELL_CAN_CAST "mob_spell_can_cast" //from base of /obj/effect/proc_holder/spell/can_cast(): (spell)
#define COMSIG_MOB_SWAP_HANDS "mob_swap_hands" //from base of mob/swap_hand(): (obj/item)
@@ -303,6 +304,10 @@
#define COMSIG_LIVING_ACTIVE_PARRY_START "active_parry_start" //from base of mob/living/initiate_parry_sequence(): (parrying_method, datum/parrying_item_mob_or_art, list/backup_items, list/override)
#define COMPONENT_PREVENT_PARRY_START 1
+#define COMSIG_LIVING_ATTACKER_SET "living_attacker_set" // from base of /mob/living/set_last_attacker(): (attacker)
+
+#define COMSIG_LIVING_SET_AS_ATTACKER "living_set_as_attacker" // from base of /mob/living/set_last_attacker(): (target)
+
//ALL OF THESE DO NOT TAKE INTO ACCOUNT WHETHER AMOUNT IS 0 OR LOWER AND ARE SENT REGARDLESS!
#define COMSIG_LIVING_STATUS_STUN "living_stun" //from base of mob/living/Stun() (amount, update, ignore)
#define COMSIG_LIVING_STATUS_KNOCKDOWN "living_knockdown" //from base of mob/living/Knockdown() (amount, update, ignore)
@@ -357,7 +362,6 @@
// /obj/item signals
#define COMSIG_ITEM_ATTACK "item_attack" //from base of obj/item/attack(): (/mob/living/target, /mob/living/user)
-#define COMSIG_MOB_APPLY_DAMGE "mob_apply_damage" //from base of /mob/living/proc/apply_damage(): (damage, damagetype, def_zone)
#define COMSIG_ITEM_ATTACK_SELF "item_attack_self" //from base of obj/item/attack_self(): (/mob)
#define COMPONENT_NO_INTERACT 1
#define COMSIG_ITEM_ATTACK_OBJ "item_attack_obj" //from base of obj/item/attack_obj(): (/obj, /mob)
@@ -455,6 +459,10 @@
// /datum/mutation signals
#define COMSIG_HUMAN_MUTATION_LOSS "human_mutation_loss" //from datum/mutation/human/on_losing(): (datum/mutation/human/lost_mutation)
+///from base of mob/living/death(): (gibbed)
+// Sent before any of the other death code has run, mob is still alive.
+#define COMSIG_LIVING_PREDEATH "living_predeath"
+
/*******Component Specific Signals*******/
//Janitor
#define COMSIG_TURF_IS_WET "check_turf_wet" //(): Returns bitflags of wet values.
diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm
index 69e2c234d6..01a0bc5124 100644
--- a/code/__DEFINES/misc.dm
+++ b/code/__DEFINES/misc.dm
@@ -166,7 +166,7 @@ GLOBAL_LIST_EMPTY(bloody_footprints_cache)
#define BLOOD_COLOR_SLIME "#00ff90"
#define BLOOD_COLOR_LIZARD "#db004D"
#define BLOOD_COLOR_UNIVERSAL "#db3300"
-#define BLOOD_COLOR_BUG "#a37c0f"
+#define BLOOD_COLOR_BUG "#ffc933"
#define BLOOD_COLOR_PLANT "#3d610e"
diff --git a/code/_globalvars/bitfields.dm b/code/_globalvars/bitfields.dm
index b9cbfb7e35..d44a4b3898 100644
--- a/code/_globalvars/bitfields.dm
+++ b/code/_globalvars/bitfields.dm
@@ -11,6 +11,23 @@ GLOBAL_LIST_INIT(bitfields, list(
"TILE_BOUND" = TILE_BOUND,
"PIXEL_SCALE" = PIXEL_SCALE
),
+ "area_flags" = list(
+ "ABDUCTOR_PROOF" = ABDUCTOR_PROOF,
+ "BLOBS_ALLOWED" = BLOBS_ALLOWED,
+ "BLOCK_SUICIDE" = BLOCK_SUICIDE,
+ // "CULT_PERMITTED" = CULT_PERMITTED,
+ "FLORA_ALLOWED" = FLORA_ALLOWED,
+ "HIDDEN_AREA" = HIDDEN_AREA,
+ "MEGAFAUNA_SPAWN_ALLOWED" = MEGAFAUNA_SPAWN_ALLOWED,
+ "MOB_SPAWN_ALLOWED" = MOB_SPAWN_ALLOWED,
+ "NO_ALERTS" = NO_ALERTS,
+ "NOTELEPORT" = NOTELEPORT,
+ "CAVES_ALLOWED" = CAVES_ALLOWED,
+ "UNIQUE_AREA" = UNIQUE_AREA,
+ "VALID_TERRITORY" = VALID_TERRITORY,
+ "XENOBIOLOGY_COMPATIBLE" = XENOBIOLOGY_COMPATIBLE,
+ "NO_ALERTS" = NO_ALERTS,
+ ) ,
"sight" = list(
"SEE_INFRA" = SEE_INFRA,
"SEE_SELF" = SEE_SELF,
diff --git a/code/_onclick/item_attack.dm b/code/_onclick/item_attack.dm
index f63e594675..faf911e55b 100644
--- a/code/_onclick/item_attack.dm
+++ b/code/_onclick/item_attack.dm
@@ -94,8 +94,7 @@
else if(hitsound)
playsound(loc, hitsound, get_clamped_volume(), 1, -1)
- M.lastattacker = user.real_name
- M.lastattackerckey = user.ckey
+ M.set_last_attacker(user)
if(force && M == user && user.client)
user.client.give_award(/datum/award/achievement/misc/selfouch, user)
diff --git a/code/datums/components/activity.dm b/code/datums/components/activity.dm
new file mode 100644
index 0000000000..ae18ca01a2
--- /dev/null
+++ b/code/datums/components/activity.dm
@@ -0,0 +1,63 @@
+/datum/component/activity
+ var/activity_level = 0
+ var/not_moved_counter = 0
+ var/list/historical_activity_levels = list()
+
+/datum/component/activity/Initialize()
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+ var/mob/living/L = parent
+
+ RegisterSignal(L, COMSIG_LIVING_SET_AS_ATTACKER, .proc/on_set_as_attacker)
+ RegisterSignal(L, COMSIG_LIVING_ATTACKER_SET, .proc/on_attacker_set)
+ RegisterSignal(L, COMSIG_MOB_DEATH, .proc/on_death)
+ RegisterSignal(L, COMSIG_EXIT_AREA, .proc/on_exit_area)
+ RegisterSignal(L, COMSIG_LIVING_LIFE, .proc/on_life)
+ RegisterSignal(L, list(COMSIG_MOB_ITEM_ATTACK, COMSIG_MOB_ATTACK_RANGED, COMSIG_HUMAN_MELEE_UNARMED_ATTACK, COMSIG_MOB_ATTACK_HAND, COMSIG_MOB_THROW, COMSIG_MOVABLE_TELEPORTED, COMSIG_LIVING_GUN_PROCESS_FIRE, COMSIG_MOB_APPLY_DAMAGE), .proc/minor_activity)
+
+/datum/component/activity/proc/log_activity()
+ historical_activity_levels[world.time] = activity_level
+
+/datum/component/activity/proc/minor_activity(datum/source)
+ activity_level += 1
+
+/datum/component/activity/proc/on_attacker_set(datum/source, mob/attacker)
+ activity_level += 10
+ if(attacker?.mind)
+ activity_level += 10
+ log_activity()
+
+/datum/component/activity/proc/on_set_as_attacker(datum/source, mob/target)
+ activity_level += 10
+ if(target?.mind)
+ activity_level += 20
+ log_activity()
+
+/datum/component/activity/proc/on_death(datum/source)
+ activity_level += 100 // dying means you're doing SOMETHING
+ log_activity()
+
+/datum/component/activity/proc/on_exit_area(datum/source)
+ activity_level += 1
+ not_moved_counter = 0
+
+/datum/component/activity/proc/on_life(datum/source, seconds, times_fired)
+ var/mob/living/L = source
+ if(L.stat >= UNCONSCIOUS) // can't expect the unconscious to move
+ return
+ not_moved_counter += seconds
+ var/should_log = FALSE
+ switch(not_moved_counter)
+ if(60 to 120)
+ activity_level -= 1
+ if(120 to 600)
+ activity_level -= 5
+ if(600 to 1200)
+ activity_level -= 10
+ should_log = TRUE
+ if(1200 to INFINITY)
+ activity_level -= 20
+ should_log = TRUE
+ activity_level = max(activity_level, 0)
+ if(should_log)
+ log_activity()
diff --git a/code/datums/components/explodable.dm b/code/datums/components/explodable.dm
index f5c8bf1642..1e63f2d051 100644
--- a/code/datums/components/explodable.dm
+++ b/code/datums/components/explodable.dm
@@ -56,10 +56,10 @@
detonate()
/datum/component/explodable/proc/on_equip(datum/source, mob/equipper, slot)
- RegisterSignal(equipper, COMSIG_MOB_APPLY_DAMGE, .proc/explodable_attack_zone, TRUE)
+ RegisterSignal(equipper, COMSIG_MOB_APPLY_DAMAGE, .proc/explodable_attack_zone, TRUE)
/datum/component/explodable/proc/on_drop(datum/source, mob/user)
- UnregisterSignal(user, COMSIG_MOB_APPLY_DAMGE)
+ UnregisterSignal(user, COMSIG_MOB_APPLY_DAMAGE)
/// Checks if we're hitting the zone this component is covering
/datum/component/explodable/proc/is_hitting_zone(def_zone)
diff --git a/code/datums/elements/wuv.dm b/code/datums/elements/wuv.dm
index 6476a204cf..d4f1955afc 100644
--- a/code/datums/elements/wuv.dm
+++ b/code/datums/elements/wuv.dm
@@ -34,13 +34,13 @@
. = ..()
UnregisterSignal(source, COMSIG_MOB_ATTACK_HAND)
-/datum/element/wuv/proc/on_attack_hand(datum/source, mob/user)
+/datum/element/wuv/proc/on_attack_hand(datum/source, mob/user, act_intent)
var/mob/living/L = source
if(L.stat == DEAD)
return
//we want to delay the effect to be displayed after the mob is petted, not before.
- switch(user.a_intent)
+ switch(act_intent)
if(INTENT_HARM)
addtimer(CALLBACK(src, .proc/kick_the_dog, source, user), 1)
if(INTENT_HELP)
diff --git a/code/game/gamemodes/objective.dm b/code/game/gamemodes/objective.dm
index 674787fbd4..fcfddb2788 100644
--- a/code/game/gamemodes/objective.dm
+++ b/code/game/gamemodes/objective.dm
@@ -412,6 +412,24 @@ If not set, defaults to check_completion instead. Set it. It's used by cryo.
counter++
return counter >= 8
+/datum/objective/freedom
+ name = "freedom"
+ explanation_text = "Don't get captured by nanotrasen."
+ team_explanation_text = "Have all members of your team free of nanotrasen custody."
+
+/datum/objective/freedom/check_completion()
+ var/list/datum/mind/owners = get_owners()
+ for(var/m in owners)
+ var/datum/mind/M = m
+ if(!considered_alive(M))
+ return FALSE
+ if(SSshuttle.emergency.mode != SHUTTLE_ENDGAME)
+ return FALSE
+ var/turf/location = get_turf(M.current)
+ if(!location || istype(location, /turf/open/floor/plasteel/shuttle/red) || istype(location, /turf/open/floor/mineral/plastitanium/red/brig)) // Fails if they are in the shuttle brig
+ return FALSE
+ return TRUE
+
/datum/objective/escape
name = "escape"
explanation_text = "Escape on the shuttle or an escape pod alive and without being in custody."
diff --git a/code/game/machinery/pipe/construction.dm b/code/game/machinery/pipe/construction.dm
index 141afa53e7..5b30105409 100644
--- a/code/game/machinery/pipe/construction.dm
+++ b/code/game/machinery/pipe/construction.dm
@@ -21,6 +21,7 @@ Buildable meters
level = 2
var/piping_layer = PIPING_LAYER_DEFAULT
var/RPD_type
+ var/disposable = TRUE
/obj/item/pipe/directional
RPD_type = PIPE_UNARY
@@ -236,3 +237,22 @@ Buildable meters
/obj/item/pipe_meter/proc/setAttachLayer(new_layer = PIPING_LAYER_DEFAULT)
piping_layer = new_layer
PIPING_LAYER_DOUBLE_SHIFT(src, piping_layer)
+
+/obj/item/pipe/bluespace
+ pipe_type = /obj/machinery/atmospherics/pipe/bluespace
+ var/bluespace_network_name = "default"
+ icon_state = "bluespace"
+ disposable = FALSE
+
+/obj/item/pipe/bluespace/attack_self(mob/user)
+ var/new_name = input(user, "Enter identifier for bluespace pipe network", "bluespace pipe", bluespace_network_name) as text|null
+ if(!isnull(new_name))
+ bluespace_network_name = new_name
+
+/obj/item/pipe/bluespace/make_from_existing(obj/machinery/atmospherics/pipe/bluespace/make_from)
+ bluespace_network_name = make_from.bluespace_network_name
+ return ..()
+
+/obj/item/pipe/bluespace/build_pipe(obj/machinery/atmospherics/pipe/bluespace/A)
+ A.bluespace_network_name = bluespace_network_name
+ return ..()
diff --git a/code/game/objects/items/RPD.dm b/code/game/objects/items/RPD.dm
index 5b125247d9..cf2bb06552 100644
--- a/code/game/objects/items/RPD.dm
+++ b/code/game/objects/items/RPD.dm
@@ -383,12 +383,14 @@ GLOBAL_LIST_INIT(fluid_duct_recipes, list(
. = TRUE
if((mode & DESTROY_MODE) && istype(A, /obj/item/pipe) || istype(A, /obj/structure/disposalconstruct) || istype(A, /obj/structure/c_transit_tube) || istype(A, /obj/structure/c_transit_tube_pod) || istype(A, /obj/item/pipe_meter))
- to_chat(user, "You start destroying a pipe...")
- playsound(get_turf(src), 'sound/machines/click.ogg', 50, 1)
- if(do_after(user, destroy_speed, target = A))
- activate()
- qdel(A)
- return
+ var/obj/item/pipe/P = A
+ if(!istype(P) || P.disposable)
+ to_chat(user, "You start destroying a pipe...")
+ playsound(get_turf(src), 'sound/machines/click.ogg', 50, 1)
+ if(do_after(user, destroy_speed, target = A))
+ activate()
+ qdel(A)
+ return
if((mode & PAINT_MODE))
if(istype(A, /obj/machinery/atmospherics/pipe) && !istype(A, /obj/machinery/atmospherics/pipe/layer_manifold))
diff --git a/code/game/objects/items/electrostaff.dm b/code/game/objects/items/electrostaff.dm
index 31aaff12b5..a1c06f8d23 100644
--- a/code/game/objects/items/electrostaff.dm
+++ b/code/game/objects/items/electrostaff.dm
@@ -210,8 +210,7 @@
target.apply_effect(EFFECT_STUTTER, stunforce)
SEND_SIGNAL(target, COMSIG_LIVING_MINOR_SHOCK)
if(user)
- target.lastattacker = user.real_name
- target.lastattackerckey = user.ckey
+ target.set_last_attacker(user)
target.visible_message("[user] has shocked [target] with [src]!", \
"[user] has shocked you with [src]!")
log_combat(user, target, "stunned with an electrostaff")
@@ -237,8 +236,7 @@
target.adjustFireLoss(lethal_force) //good against ointment spam
SEND_SIGNAL(target, COMSIG_LIVING_MINOR_SHOCK)
if(user)
- target.lastattacker = user.real_name
- target.lastattackerckey = user.ckey
+ target.set_last_attacker(user)
target.visible_message("[user] has seared [target] with [src]!", \
"[user] has seared you with [src]!")
log_combat(user, target, "burned with an electrostaff")
diff --git a/code/game/objects/items/handcuffs.dm b/code/game/objects/items/handcuffs.dm
index 59b6d56b51..9ae8fb80db 100644
--- a/code/game/objects/items/handcuffs.dm
+++ b/code/game/objects/items/handcuffs.dm
@@ -312,7 +312,7 @@
trap_damage = 0
item_flags = DROPDEL
flags_1 = NONE
- breakouttime = 50
+ breakouttime = 25
/obj/item/restraints/legcuffs/beartrap/energy/New()
..()
diff --git a/code/game/objects/items/implants/implant_deathrattle.dm b/code/game/objects/items/implants/implant_deathrattle.dm
new file mode 100644
index 0000000000..826dc71843
--- /dev/null
+++ b/code/game/objects/items/implants/implant_deathrattle.dm
@@ -0,0 +1,74 @@
+/datum/deathrattle_group
+ var/name
+ var/list/datum/weakref/implant_refs = list()
+
+/datum/deathrattle_group/New()
+ // Give the group a unique name for debugging, and possible future
+ // use for making custom linked groups.
+ name = "[rand(100,999)] [pick(GLOB.phonetic_alphabet)]"
+
+/datum/deathrattle_group/proc/rattle(obj/item/implant/deathrattle/origin, mob/living/owner)
+ var/name = owner.mind ? owner.mind.name : owner.real_name
+ var/area = get_area_name(get_turf(owner))
+
+ for(var/r in implant_refs)
+ var/datum/weakref/R = r
+
+ var/obj/item/implant/deathrattle/implant = R.resolve()
+ if(!implant || implant == origin)
+ continue
+
+ // Not all the implants may be actually implanted in people.
+ if(!implant.imp_in)
+ continue
+
+ // Deliberately the same message framing as nanite message + ghost deathrattle
+ var/mob/living/recipient = implant.imp_in
+ to_chat(recipient, "You hear a strange, robotic voice in your head... \"[name] has died at [area].\"")
+ SEND_SOUND(recipient, pick(
+ 'sound/items/knell1.ogg',
+ 'sound/items/knell2.ogg',
+ 'sound/items/knell3.ogg',
+ 'sound/items/knell4.ogg',
+ ))
+
+/datum/deathrattle_group/proc/register(obj/item/implant/deathrattle/implant)
+ implant.group = src
+ implant_refs += WEAKREF(implant)
+
+
+/obj/item/implant/deathrattle
+ name = "deathrattle implant"
+ desc = "Hope no one else dies, prepare for when they do."
+
+ activated = FALSE
+
+ var/datum/deathrattle_group/group = null
+
+/obj/item/implant/deathrattle/Destroy()
+ group = null
+ return ..()
+
+/obj/item/implant/deathrattle/can_be_implanted_in(mob/living/target)
+ // Can be implanted in anything that's a mob. Syndicate cyborgs, talking fish, humans...
+ return TRUE
+
+/obj/item/implant/deathrattle/proc/on_predeath(datum/source, gibbed)
+ SIGNAL_HANDLER
+
+ if(group)
+ group.rattle(origin = src, owner = source)
+
+/obj/item/implant/deathrattle/implant(mob/living/target, mob/user, silent = FALSE, force = FALSE)
+ . = ..()
+ if(.)
+ RegisterSignal(target, COMSIG_LIVING_PREDEATH, .proc/on_predeath)
+
+ if(!group)
+ to_chat(target, "You hear a strange, robotic voice in your head... \"Warning: No other linked implants detected.\"")
+
+
+/obj/item/implantcase/deathrattle
+ name = "implant case - 'Deathrattle'"
+ desc = "A glass case containing a deathrattle implant."
+ imp_type = /obj/item/implant/deathrattle
diff --git a/code/game/objects/items/stacks/sheets/glass.dm b/code/game/objects/items/stacks/sheets/glass.dm
index 026b73ca15..6b1220743f 100644
--- a/code/game/objects/items/stacks/sheets/glass.dm
+++ b/code/game/objects/items/stacks/sheets/glass.dm
@@ -272,7 +272,7 @@ GLOBAL_LIST_INIT(plastitaniumglass_recipes, list(
. = ..()
. += GLOB.plastitaniumglass_recipes
-/obj/item/stack/sheet/titaniumglass/on_solar_construction(obj/machinery/power/solar/S)
+/obj/item/stack/sheet/plastitaniumglass/on_solar_construction(obj/machinery/power/solar/S)
S.max_integrity *= 2
S.efficiency *= 2
diff --git a/code/game/objects/items/storage/fancy.dm b/code/game/objects/items/storage/fancy.dm
index d72f46e175..de7374379d 100644
--- a/code/game/objects/items/storage/fancy.dm
+++ b/code/game/objects/items/storage/fancy.dm
@@ -351,6 +351,75 @@
if(!contents.len)
. += "[icon_state]_empty"
+//Derringer "Cigarettes"//
+/obj/item/storage/fancy/cigarettes/derringer
+ name = "\improper Robust packet"
+ desc = "Smoked by the robust."
+ icon_state = "robust"
+ spawn_type = /obj/item/gun/ballistic/derringer/traitor
+
+/obj/item/storage/fancy/cigarettes/derringer/ComponentInitialize()
+ . = ..()
+ var/datum/component/storage/STR = GetComponent(/datum/component/storage)
+ STR.max_items = 6
+ STR.can_hold = typecacheof(list(/obj/item/clothing/mask/cigarette, /obj/item/lighter, /obj/item/gun/ballistic/derringer, /obj/item/ammo_casing/c38, /obj/item/ammo_casing/a357, /obj/item/ammo_casing/g4570))
+
+/obj/item/storage/fancy/cigarettes/derringer/AltClick(mob/living/carbon/user)
+ if(!istype(user) || !user.canUseTopic(src, BE_CLOSE, ismonkey(user)))
+ return
+ var/obj/item/W = (locate(/obj/item/ammo_casing/a357) in contents) || (locate(/obj/item/clothing/mask/cigarette) in contents) || locate(/obj/item/ammo_casing/g4570) //Easy access smokes and bullets
+ if(W && contents.len > 0)
+ SEND_SIGNAL(src, COMSIG_TRY_STORAGE_TAKE, W, user)
+ user.put_in_hands(W)
+ contents -= W
+ to_chat(user, "You take \a [W] out of the pack.")
+ else
+ to_chat(user, "There are no items left in the pack.")
+
+/obj/item/storage/fancy/cigarettes/derringer/PopulateContents()
+ new spawn_type(src)
+ new /obj/item/ammo_casing/a357(src)
+ new /obj/item/ammo_casing/a357(src)
+ new /obj/item/ammo_casing/a357(src)
+ new /obj/item/ammo_casing/a357(src)
+ new /obj/item/clothing/mask/cigarette/syndicate(src)
+
+//For traitors with luck/class
+/obj/item/storage/fancy/cigarettes/derringer/gold
+ name = "\improper Robust Gold packet"
+ desc = "Smoked by the truly robust."
+ icon_state = "robustg"
+ spawn_type = /obj/item/gun/ballistic/derringer/gold
+
+//For operatives, bound in a ka-tet.
+/obj/item/storage/fancy/cigarettes/derringer/midworld
+ name = "\improper Midworld's Lime Bend"
+ desc = "The wheel of Ka turns, Gunslinger."
+ icon_state = "slime"
+ spawn_type = /obj/item/gun/ballistic/derringer/nukeop
+
+/obj/item/storage/fancy/cigarettes/derringer/midworld/PopulateContents()
+ new spawn_type(src)
+ new /obj/item/ammo_casing/g4570(src)
+ new /obj/item/ammo_casing/g4570(src)
+ new /obj/item/ammo_casing/g4570(src)
+ new /obj/item/ammo_casing/g4570(src)
+ new /obj/item/clothing/mask/cigarette/xeno(src)
+
+//For Cargomen, looking for a good deal on arms, with no quarrels as to where they're from.
+/obj/item/storage/fancy/cigarettes/derringer/smuggled
+ name = "\improper Shady Jim's Super Slims packet"
+ desc = "If you get caught with this, we don't know you, capiche?"
+ icon_state = "shadyjim"
+ spawn_type = /obj/item/gun/ballistic/derringer
+
+/obj/item/storage/fancy/cigarettes/derringer/smuggled/PopulateContents()
+ new spawn_type(src)
+ new /obj/item/ammo_casing/c38/lethal(src)
+ new /obj/item/ammo_casing/c38/lethal(src)
+ new /obj/item/ammo_casing/c38/lethal(src)
+ new /obj/item/ammo_casing/c38/lethal(src)
+ new /obj/item/clothing/mask/cigarette/shadyjims (src)
/////////////
//CIGAR BOX//
/////////////
diff --git a/code/game/objects/items/storage/uplink_kits.dm b/code/game/objects/items/storage/uplink_kits.dm
index 941ba869bb..f25f6f3a51 100644
--- a/code/game/objects/items/storage/uplink_kits.dm
+++ b/code/game/objects/items/storage/uplink_kits.dm
@@ -546,3 +546,21 @@
. = ..()
new /obj/item/cardpack/syndicate(src)
new /obj/item/cardpack/syndicate(src)
+
+/obj/item/storage/box/syndie_kit/imp_deathrattle
+ name = "deathrattle implant box"
+ desc = "Contains eight linked deathrattle implants."
+
+/obj/item/storage/box/syndie_kit/imp_deathrattle/PopulateContents()
+ new /obj/item/implanter(src)
+
+ var/datum/deathrattle_group/group = new
+
+ var/implants = list()
+ for(var/j in 1 to 8)
+ var/obj/item/implantcase/deathrattle/case = new (src)
+ implants += case.imp
+
+ for(var/i in implants)
+ group.register(i)
+ desc += " The implants are registered to the \"[group.name]\" group."
diff --git a/code/game/objects/items/storage/wallets.dm b/code/game/objects/items/storage/wallets.dm
index 0b4b6f54f3..34fdb8e77d 100644
--- a/code/game/objects/items/storage/wallets.dm
+++ b/code/game/objects/items/storage/wallets.dm
@@ -46,7 +46,8 @@
/obj/item/instrument/harmonica,
/obj/item/mining_voucher,
/obj/item/suit_voucher,
- /obj/item/reagent_containers/pill))
+ /obj/item/reagent_containers/pill,
+ /obj/item/gun/ballistic/derringer))
/obj/item/storage/wallet/Exited(atom/movable/AM)
. = ..()
diff --git a/code/game/objects/items/stunbaton.dm b/code/game/objects/items/stunbaton.dm
index cc01e6aaee..88146e4b4d 100644
--- a/code/game/objects/items/stunbaton.dm
+++ b/code/game/objects/items/stunbaton.dm
@@ -209,8 +209,7 @@
L.apply_effect(EFFECT_STUTTER, stamforce)
SEND_SIGNAL(L, COMSIG_LIVING_MINOR_SHOCK)
if(user)
- L.lastattacker = user.real_name
- L.lastattackerckey = user.ckey
+ L.set_last_attacker(user)
L.visible_message("[user] has [disarming? "disarmed" : "stunned"] [L] with [src]!", \
"[user] has [disarming? "disarmed" : "stunned"] you with [src]!")
log_combat(user, L, disarming? "disarmed" : "stunned")
diff --git a/code/game/objects/structures/crates_lockers/closets/secure/secure_closets.dm b/code/game/objects/structures/crates_lockers/closets/secure/secure_closets.dm
index 0c314d988c..5b78ca323c 100644
--- a/code/game/objects/structures/crates_lockers/closets/secure/secure_closets.dm
+++ b/code/game/objects/structures/crates_lockers/closets/secure/secure_closets.dm
@@ -19,3 +19,50 @@
desc = "A sturdier card-locked storage unit used for bulky shipments."
max_integrity = 500 // Same as crates.
melee_min_damage = 25 // Idem.
+
+/obj/structure/closet/secure_closet/goodies/owned
+ name = "private locker"
+ desc = "A locker designed to only open for who purchased its contents."
+ ///Account of the person buying the crate if private purchasing.
+ var/datum/bank_account/buyer_account
+ ///Department of the person buying the crate if buying via the NIRN app.
+ var/datum/bank_account/department/department_account
+ ///Is the secure crate opened or closed?
+ var/privacy_lock = TRUE
+ ///Is the crate being bought by a person, or a budget card?
+ var/department_purchase = FALSE
+
+/obj/structure/closet/secure_closet/goodies/owned/examine(mob/user)
+ . = ..()
+ . += "It's locked with a privacy lock, and can only be unlocked by the buyer's ID."
+
+/obj/structure/closet/secure_closet/goodies/owned/Initialize(mapload, datum/bank_account/_buyer_account)
+ . = ..()
+ buyer_account = _buyer_account
+ if(istype(buyer_account, /datum/bank_account/department))
+ department_purchase = TRUE
+ department_account = buyer_account
+
+/obj/structure/closet/secure_closet/goodies/owned/togglelock(mob/living/user, silent)
+ if(privacy_lock)
+ if(!broken)
+ var/obj/item/card/id/id_card = user.get_idcard(TRUE)
+ if(id_card)
+ if(id_card.registered_account)
+ if(id_card.registered_account == buyer_account || (department_purchase && (id_card.registered_account?.account_job?.paycheck_department) == (department_account.department_id)))
+ if(iscarbon(user))
+ add_fingerprint(user)
+ locked = !locked
+ user.visible_message("[user] unlocks [src]'s privacy lock.",
+ "You unlock [src]'s privacy lock.")
+ privacy_lock = FALSE
+ update_icon()
+ else if(!silent)
+ to_chat(user, "Bank account does not match with buyer!")
+ else if(!silent)
+ to_chat(user, "No linked bank account detected!")
+ else if(!silent)
+ to_chat(user, "No ID detected!")
+ else if(!silent)
+ to_chat(user, "[src] is broken!")
+ else ..()
diff --git a/code/game/objects/structures/ghost_role_spawners.dm b/code/game/objects/structures/ghost_role_spawners.dm
index 306868daba..f5746d6ae7 100644
--- a/code/game/objects/structures/ghost_role_spawners.dm
+++ b/code/game/objects/structures/ghost_role_spawners.dm
@@ -590,7 +590,7 @@
/obj/effect/mob_spawn/human/pirate
name = "space pirate sleeper"
- desc = "A cryo sleeper smelling faintly of rum."
+ desc = "A cryo sleeper smelling faintly of rum. The sleeper looks unstable. Perhaps the pirate within can be killed with the right tools..."
job_description = "Space Pirate"
random = TRUE
icon = 'icons/obj/machines/sleeper.dmi'
@@ -608,6 +608,54 @@
assignedrole = "Space Pirate"
var/rank = "Mate"
+/obj/effect/mob_spawn/human/pirate/on_attack_hand(mob/living/user, act_intent = user.a_intent, unarmed_attack_flags)
+ . = ..()
+ if(.)
+ return
+ if(user.mind.has_antag_datum(/datum/antagonist/pirate))
+ to_chat(user, "Your shipmate sails within their dreams for now. Perhaps they may wake up eventually.")
+ else
+ to_chat(user, "If you want to kill the pirate off, something to pry open the sleeper might be the best way to do it.")
+
+
+/obj/effect/mob_spawn/human/pirate/attackby(obj/item/W, mob/user, params)
+ if(W.tool_behaviour == TOOL_CROWBAR && user.a_intent != INTENT_HARM)
+ if(user.mind.has_antag_datum(/datum/antagonist/pirate))
+ to_chat(user,"Why would you want to do that to your shipmate? That'd kill them.")
+ return
+ user.visible_message("[user] start to pry open [src]...",
+ "You start to pry open [src]...",
+ "You hear prying...")
+ W.play_tool_sound(src)
+ if(do_after(user, 100*W.toolspeed, target = src))
+ user.visible_message("[user] pries open [src], disrupting the sleep of the pirate within and killing them.",
+ "You pry open [src], disrupting the sleep of the pirate within and killing them.",
+ "You hear prying, followed by the death rattling of bones.")
+ log_game("[key_name(user)] has successfully pried open [src] and disabled a space pirate spawner.")
+ W.play_tool_sound(src)
+ playsound(src.loc, 'modular_citadel/sound/voice/scream_skeleton.ogg', 50, 1, 4, 1.2)
+ if(rank == "Captain")
+ new /obj/effect/mob_spawn/human/pirate/corpse/captain(get_turf(src))
+ else
+ new /obj/effect/mob_spawn/human/pirate/corpse(get_turf(src))
+ qdel(src)
+ else
+ ..()
+
+/obj/effect/mob_spawn/human/pirate/corpse //occurs when someone pries a pirate out of their sleeper.
+ mob_name = "Dead Space Pirate"
+ death = TRUE
+ instant = TRUE
+ random = FALSE
+
+/obj/effect/mob_spawn/human/pirate/corpse/Destroy()
+ return ..()
+
+/obj/effect/mob_spawn/human/pirate/corpse/captain
+ rank = "Captain"
+ mob_name = "Dead Space Pirate Captain"
+ outfit = /datum/outfit/pirate/space/captain
+
/obj/effect/mob_spawn/human/pirate/special(mob/living/new_spawn)
new_spawn.fully_replace_character_name(new_spawn.real_name,generate_pirate_name())
new_spawn.mind.add_antag_datum(/datum/antagonist/pirate)
diff --git a/code/game/objects/structures/manned_turret.dm b/code/game/objects/structures/manned_turret.dm
index d89606417f..3d0b28d2f3 100644
--- a/code/game/objects/structures/manned_turret.dm
+++ b/code/game/objects/structures/manned_turret.dm
@@ -199,8 +199,7 @@
return TRUE
/obj/item/gun_control/attack(mob/living/M, mob/living/user)
- M.lastattacker = user.real_name
- M.lastattackerckey = user.ckey
+ M.set_last_attacker(user)
M.attacked_by(src, user)
add_fingerprint(user)
diff --git a/code/modules/admin/DB_ban/functions.dm b/code/modules/admin/DB_ban/functions.dm
index 4c00e8f010..b8d2452874 100644
--- a/code/modules/admin/DB_ban/functions.dm
+++ b/code/modules/admin/DB_ban/functions.dm
@@ -428,7 +428,7 @@
output += ""
for(var/j in GLOB.nonhuman_positions)
output += ""
- for(var/j in list(ROLE_TRAITOR, ROLE_CHANGELING, ROLE_OPERATIVE, ROLE_REV, ROLE_CULTIST, ROLE_WIZARD))
+ for(var/j in list(ROLE_TRAITOR, ROLE_CHANGELING, ROLE_OPERATIVE, ROLE_REV, ROLE_CULTIST, ROLE_WIZARD, ROLE_HERETIC))
output += ""
output += ""
output += "Reason:
"
diff --git a/code/modules/admin/antag_panel.dm b/code/modules/admin/antag_panel.dm
index 88aab2c4ce..84071d76f4 100644
--- a/code/modules/admin/antag_panel.dm
+++ b/code/modules/admin/antag_panel.dm
@@ -101,6 +101,10 @@ GLOBAL_VAR(antag_prototypes)
out += "Mind currently owned by key: [key] [active?"(synced)":"(not synced)"]
"
out += "Assigned role: [assigned_role]. Edit
"
out += "Faction and special role: [special_role]
"
+ var/datum/component/activity/activity = current.GetComponent(/datum/component/activity)
+ if(activity)
+ out += "Activity level: [activity.activity_level]
"
+ out += "Hasn't changed areas in approximately [activity.not_moved_counter] seconds"
var/special_statuses = get_special_statuses()
if(length(special_statuses))
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index cd29da186c..953835bff0 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -870,6 +870,12 @@
else
dat += "