diff --git a/_maps/map_files/BoxStation/BoxStation.dmm b/_maps/map_files/BoxStation/BoxStation.dmm
index f6d73c48c5..9ace5cf95b 100644
--- a/_maps/map_files/BoxStation/BoxStation.dmm
+++ b/_maps/map_files/BoxStation/BoxStation.dmm
@@ -10444,7 +10444,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/_maps/shuttles/emergency_airless.dmm b/_maps/shuttles/emergency_construction.dmm
similarity index 100%
rename from _maps/shuttles/emergency_airless.dmm
rename to _maps/shuttles/emergency_construction.dmm
diff --git a/code/__DEFINES/_flags/item_flags.dm b/code/__DEFINES/_flags/item_flags.dm
index 7da71e22cb..529499127d 100644
--- a/code/__DEFINES/_flags/item_flags.dm
+++ b/code/__DEFINES/_flags/item_flags.dm
@@ -51,3 +51,4 @@
#define ORGAN_NO_SPOIL (1<<5) //Do not spoil under any circumstances
#define ORGAN_NO_DISMEMBERMENT (1<<6) //Immune to disembowelment.
#define ORGAN_EDIBLE (1<<7) //is a snack? :D
+#define ORGAN_SYNTHETIC_EMP (1<<6) //Synthetic organ affected by an EMP. Deteriorates over time.
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/combat.dm b/code/__DEFINES/combat.dm
index d12fe77448..d219736abe 100644
--- a/code/__DEFINES/combat.dm
+++ b/code/__DEFINES/combat.dm
@@ -159,9 +159,6 @@
#define SHOVE_STAGGER_DURATION 35
/// how long they're off balance for
#define SHOVE_OFFBALANCE_DURATION 30
-//Shove disarming item list
-GLOBAL_LIST_INIT(shove_disarming_types, typecacheof(list(
- /obj/item/gun)))
//Embedded objects
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 fe139fff10..32d961d77d 100644
--- a/code/__DEFINES/misc.dm
+++ b/code/__DEFINES/misc.dm
@@ -159,7 +159,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/__DEFINES/traits.dm b/code/__DEFINES/traits.dm
index e27c42ff75..ded183ba0b 100644
--- a/code/__DEFINES/traits.dm
+++ b/code/__DEFINES/traits.dm
@@ -215,6 +215,7 @@
#define TRAIT_NO_STAMINA_BUFFER_REGENERATION "block_stamina_buffer_regen" /// Prevents stamina buffer regeneration
#define TRAIT_NO_STAMINA_REGENERATION "block_stamina_regen" /// Prevents stamina regeneration
#define TRAIT_ARMOR_BROKEN "armor_broken" //acts as if you are wearing no clothing when taking damage, does not affect non-clothing sources of protection
+#define TRAIT_IWASBATONED "iwasbatoned" //some dastardly fellow has struck you with a baton and thought to use another to strike you again, the rogue
/// forces update_density to make us not dense
#define TRAIT_LIVING_NO_DENSITY "living_no_density"
/// forces us to not render our overlays
diff --git a/code/__DEFINES/vv.dm b/code/__DEFINES/vv.dm
index 99a2e9d0ab..a04f02bd6a 100644
--- a/code/__DEFINES/vv.dm
+++ b/code/__DEFINES/vv.dm
@@ -136,3 +136,6 @@
// paintings
#define VV_HK_REMOVE_PAINTING "remove_painting"
+
+//outfits
+#define VV_HK_TO_OUTFIT_EDITOR "outfit_editor"
diff --git a/code/__HELPERS/global_lists.dm b/code/__HELPERS/global_lists.dm
index b8c009ff4f..9719157d4d 100644
--- a/code/__HELPERS/global_lists.dm
+++ b/code/__HELPERS/global_lists.dm
@@ -68,6 +68,7 @@
for(var/spath in subtypesof(/datum/species))
var/datum/species/S = new spath()
GLOB.species_list[S.id] = spath
+ GLOB.species_datums[S.id] = S
//Surgeries
for(var/path in subtypesof(/datum/surgery))
diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm
index 1d37f639bf..fabe70c929 100644
--- a/code/__HELPERS/icons.dm
+++ b/code/__HELPERS/icons.dm
@@ -1051,7 +1051,7 @@ GLOBAL_LIST_EMPTY(friendly_animal_types)
return 0
//For creating consistent icons for human looking simple animals
-/proc/get_flat_human_icon(icon_id, datum/job/J, datum/preferences/prefs, dummy_key, showDirs = GLOB.cardinals, outfit_override = null)
+/proc/get_flat_human_icon(icon_id, datum/job/J, datum/preferences/prefs, dummy_key, showDirs = GLOB.cardinals, outfit_override = null, no_anim = FALSE)
var/static/list/humanoid_icon_cache = list()
if(!icon_id || !humanoid_icon_cache[icon_id])
var/mob/living/carbon/human/dummy/body = generate_or_wait_for_human_dummy(dummy_key)
@@ -1065,10 +1065,9 @@ GLOBAL_LIST_EMPTY(friendly_animal_types)
var/icon/out_icon = icon('icons/effects/effects.dmi', "nothing")
+ COMPILE_OVERLAYS(body)
for(var/D in showDirs)
- body.setDir(D)
- COMPILE_OVERLAYS(body)
- var/icon/partial = getFlatIcon(body)
+ var/icon/partial = getFlatIcon(body, defdir = D, no_anim = no_anim)
out_icon.Insert(partial,dir=D)
humanoid_icon_cache[icon_id] = out_icon
diff --git a/code/__HELPERS/mobs.dm b/code/__HELPERS/mobs.dm
index 513437e57e..92cf7050e0 100644
--- a/code/__HELPERS/mobs.dm
+++ b/code/__HELPERS/mobs.dm
@@ -320,6 +320,8 @@ GLOBAL_LIST_INIT(nonstandard_skin_tones, list("orange"))
GLOBAL_LIST_EMPTY(species_list)
+GLOBAL_LIST_EMPTY(species_datums)
+
/proc/age2agedescription(age)
switch(age)
if(0 to 1)
diff --git a/code/__HELPERS/reagents.dm b/code/__HELPERS/reagents.dm
index de225b3b53..fa655efce4 100644
--- a/code/__HELPERS/reagents.dm
+++ b/code/__HELPERS/reagents.dm
@@ -95,3 +95,9 @@
if("I'm feeling lucky")
chosen_id = pick(subtypesof(/datum/reagent))
return chosen_id
+
+/proc/find_reagent_object_from_type(input)
+ if(GLOB.chemical_reagents_list[input]) //prefer IDs!
+ return GLOB.chemical_reagents_list[input]
+ else
+ return null
diff --git a/code/_globalvars/bitfields.dm b/code/_globalvars/bitfields.dm
index b9cbfb7e35..9c6f16e724 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,
@@ -32,7 +49,12 @@ GLOBAL_LIST_INIT(bitfields, list(
"UNIQUE_RENAME" = UNIQUE_RENAME,
"USES_TGUI" = USES_TGUI,
"FROZEN" = FROZEN,
- "SHOVABLE_ONTO" = SHOVABLE_ONTO
+ "SHOVABLE_ONTO" = SHOVABLE_ONTO,
+ "BLOCK_Z_OUT_DOWN" = BLOCK_Z_OUT_DOWN,
+ "BLOCK_Z_OUT_UP" = BLOCK_Z_OUT_UP,
+ "BLOCK_Z_IN_DOWN" = BLOCK_Z_IN_DOWN,
+ "BLOCK_Z_IN_UP" = BLOCK_Z_IN_UP,
+ "EXAMINE_SKIP" = EXAMINE_SKIP
),
"datum_flags" = list(
"DF_USE_TAG" = DF_USE_TAG,
diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm
index ac6ea4e25c..26e0197deb 100644
--- a/code/_globalvars/traits.dm
+++ b/code/_globalvars/traits.dm
@@ -123,7 +123,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_HIGH_BLOOD" = TRAIT_HIGH_BLOOD,
"TRAIT_EMPATH" = TRAIT_EMPATH,
"TRAIT_FRIENDLY" = TRAIT_FRIENDLY,
- "TRAIT_NICE_SHOT" = TRAIT_NICE_SHOT
+ "TRAIT_IWASBATONED" = TRAIT_IWASBATONED
),
/obj/item/bodypart = list(
"TRAIT_PARALYSIS" = TRAIT_PARALYSIS
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/controllers/subsystem/activity.dm b/code/controllers/subsystem/activity.dm
index 24cd1802f0..0a8d248e58 100644
--- a/code/controllers/subsystem/activity.dm
+++ b/code/controllers/subsystem/activity.dm
@@ -61,14 +61,14 @@ SUBSYSTEM_DEF(activity)
for(var/threat in threat_history)
. = max(threat_history[threat], .)
-/datum/controller/subsystem/activity/proc/on_explosion(atom/epicenter, devastation_range, heavy_impact_range, light_impact_range, took, orig_dev_range, orig_heavy_range, orig_light_range)
+/datum/controller/subsystem/activity/proc/on_explosion(datum/source, atom/epicenter, devastation_range, heavy_impact_range, light_impact_range, took, orig_dev_range, orig_heavy_range, orig_light_range)
if(!("explosions" in deferred_threats))
deferred_threats["explosions"] = 0
var/area/A = get_area(epicenter)
if(is_station_level(epicenter.z) && (A.area_flags & BLOBS_ALLOWED) && !istype(A, /area/asteroid))
deferred_threats["explosions"] += devastation_range**2 + heavy_impact_range**2 / 4 + light_impact_range**2 / 8 // 75 for a maxcap
-/datum/controller/subsystem/activity/proc/on_death(mob/M, gibbed)
+/datum/controller/subsystem/activity/proc/on_death(datum/source, mob/M, gibbed)
if(!("crew_deaths" in deferred_threats))
deferred_threats["crew_deaths"] = 0
if(M?.mind && SSjob.GetJob(M.mind.assigned_role))
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 2e5834c025..6e4be8497d 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/components/radioactive.dm b/code/datums/components/radioactive.dm
index 8afa4353b2..4c418bb8dc 100644
--- a/code/datums/components/radioactive.dm
+++ b/code/datums/components/radioactive.dm
@@ -52,7 +52,7 @@
return
strength -= strength / hl3_release_date
if(strength <= RAD_BACKGROUND_RADIATION)
- return PROCESS_KILL
+ qdel(src)
/datum/component/radioactive/proc/glow_loop(atom/movable/master)
diff --git a/code/datums/dna.dm b/code/datums/dna.dm
index b6b93bdddc..248b669ab1 100644
--- a/code/datums/dna.dm
+++ b/code/datums/dna.dm
@@ -329,12 +329,13 @@
uni_identity = generate_uni_identity()
unique_enzymes = generate_unique_enzymes()
-/datum/dna/proc/initialize_dna(newblood_type)
+/datum/dna/proc/initialize_dna(newblood_type, skip_index = FALSE)
if(newblood_type)
blood_type = newblood_type
unique_enzymes = generate_unique_enzymes()
uni_identity = generate_uni_identity()
- generate_dna_blocks()
+ if(!skip_index) //I hate this
+ generate_dna_blocks()
features = random_features(species?.id, holder?.gender)
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/datums/mutations/space_adaptation.dm b/code/datums/mutations/space_adaptation.dm
index a3a2f10f2f..8b2263c2f2 100644
--- a/code/datums/mutations/space_adaptation.dm
+++ b/code/datums/mutations/space_adaptation.dm
@@ -11,7 +11,7 @@
/datum/mutation/human/space_adaptation/New(class_ = MUT_OTHER, timer, datum/mutation/human/copymut)
..()
if(!(type in visual_indicators))
- visual_indicators[type] = list(mutable_appearance('icons/effects/genetics.dmi', "fire", -MUTATIONS_LAYER))
+ visual_indicators[type] = list(mutable_appearance('icons/effects/genetics.dmi', "space_adapt", -MUTATIONS_LAYER))
/datum/mutation/human/space_adaptation/get_visual_indicator()
return visual_indicators[type][1]
diff --git a/code/datums/outfit.dm b/code/datums/outfit.dm
index da379b9851..0b46629365 100755
--- a/code/datums/outfit.dm
+++ b/code/datums/outfit.dm
@@ -1,71 +1,185 @@
+/**
+ * # Outfit datums
+ *
+ * This is a clean system of applying outfits to mobs, if you need to equip someone in a uniform
+ * this is the way to do it cleanly and properly.
+ *
+ * You can also specify an outfit datum on a job to have it auto equipped to the mob on join
+ *
+ * /mob/living/carbon/human/proc/equipOutfit(outfit) is the mob level proc to equip an outfit
+ * and you pass it the relevant datum outfit
+ *
+ * outfits can also be saved as json blobs downloadable by a client and then can be uploaded
+ * by that user to recreate the outfit, this is used by admins to allow for custom event outfits
+ * that can be restored at a later date
+ */
/datum/outfit
+ ///Name of the outfit (shows up in the equip admin verb)
var/name = "Naked"
- var/uniform = null
- var/suit = null
- var/toggle_helmet = TRUE
- var/back = null
- var/belt = null
- var/gloves = null
- var/shoes = null
- var/head = null
- var/mask = null
- var/neck = null
- var/ears = null
- var/glasses = null
+ /// Type path of item to go in the idcard slot
var/id = null
- var/l_pocket = null
- var/r_pocket = null
+
+ /// Type path of item to go in uniform slot
+ var/uniform = null
+
+ /// Type path of item to go in suit slot
+ var/suit = null
+
+ /**
+ * Type path of item to go in suit storage slot
+ *
+ * (make sure it's valid for that suit)
+ */
var/suit_store = null
- var/r_hand = null
+
+ /// Type path of item to go in back slot
+ var/back = null
+
+ /**
+ * list of items that should go in the backpack of the user
+ *
+ * Format of this list should be: list(path=count,otherpath=count)
+ */
+ var/list/backpack_contents = null
+
+ /// Type path of item to go in belt slot
+ var/belt = null
+
+ /// Type path of item to go in ears slot
+ var/ears = null
+
+ /// Type path of item to go in the glasses slot
+ var/glasses = null
+
+ /// Type path of item to go in gloves slot
+ var/gloves = null
+
+ /// Type path of item to go in head slot
+ var/head = null
+
+ /// Type path of item to go in mask slot
+ var/mask = null
+
+ /// Type path of item to go in neck slot
+ var/neck = null
+
+ /// Type path of item to go in shoes slot
+ var/shoes = null
+
+ /// Type path of item for left pocket slot
+ var/l_pocket = null
+
+ /// Type path of item for right pocket slot
+ var/r_pocket = null
+
+ ///Type path of item to go in the right hand
var/l_hand = null
- var/internals_slot = null //ID of slot containing a gas tank
- var/list/backpack_contents = null // In the list(path=count,otherpath=count) format
- var/box // Internals box. Will be inserted at the start of backpack_contents
- var/list/implants = null
+
+ //Type path of item to go in left hand
+ var/r_hand = null
+
+ /// Any clothing accessory item
var/accessory = null
- var/can_be_admin_equipped = TRUE // Set to FALSE if your outfit requires runtime parameters
- var/list/chameleon_extras //extra types for chameleon outfit changes, mostly guns
+ /// Internals box. Will be inserted at the start of backpack_contents
+ var/box
+ /**
+ * extra types for chameleon outfit changes, mostly guns
+ *
+ * Format of this list is (typepath, typepath, typepath)
+ *
+ * These are all added and returns in the list for get_chamelon_diguise_info proc
+ */
+ var/list/chameleon_extras
+
+ /**
+ * Any implants the mob should start implanted with
+ *
+ * Format of this list is (typepath, typepath, typepath)
+ */
+ var/list/implants = null
+
+ ///ID of the slot containing a gas tank
+ var/internals_slot = null
+
+ /// Should the toggle helmet proc be called on the helmet during equip
+ var/toggle_helmet = TRUE
+
+ /// Any undershirt. While on humans it is a string, here we use paths to stay consistent with the rest of the equips.
+ var/datum/sprite_accessory/undershirt = null
+
+/**
+ * Called at the start of the equip proc
+ *
+ * Override to change the value of the slots depending on client prefs, species and
+ * other such sources of change
+ *
+ * Extra Arguments
+ * * visualsOnly true if this is only for display (in the character setup screen)
+ *
+ * If visualsOnly is true, you can omit any work that doesn't visually appear on the character sprite
+ */
/datum/outfit/proc/pre_equip(mob/living/carbon/human/H, visualsOnly = FALSE, client/preference_source)
//to be overridden for customization depending on client prefs,species etc
return
+/**
+ * Called after the equip proc has finished
+ *
+ * All items are on the mob at this point, use this proc to toggle internals
+ * fiddle with id bindings and accesses etc
+ *
+ * Extra Arguments
+ * * visualsOnly true if this is only for display (in the character setup screen)
+ *
+ * If visualsOnly is true, you can omit any work that doesn't visually appear on the character sprite
+ */
/datum/outfit/proc/post_equip(mob/living/carbon/human/H, visualsOnly = FALSE, client/preference_source)
//to be overridden for toggling internals, id binding, access etc
return
+/**
+ * Equips all defined types and paths to the mob passed in
+ *
+ * Extra Arguments
+ * * visualsOnly true if this is only for display (in the character setup screen)
+ *
+ * If visualsOnly is true, you can omit any work that doesn't visually appear on the character sprite
+ */
/datum/outfit/proc/equip(mob/living/carbon/human/H, visualsOnly = FALSE, client/preference_source)
pre_equip(H, visualsOnly, preference_source)
//Start with uniform,suit,backpack for additional slots
if(uniform)
- H.equip_to_slot_or_del(new uniform(H),SLOT_W_UNIFORM)
+ H.equip_to_slot_or_del(new uniform(H), SLOT_W_UNIFORM, TRUE)
if(suit)
- H.equip_to_slot_or_del(new suit(H),SLOT_WEAR_SUIT)
+ H.equip_to_slot_or_del(new suit(H), SLOT_WEAR_SUIT, TRUE)
if(back)
- H.equip_to_slot_or_del(new back(H),SLOT_BACK)
+ H.equip_to_slot_or_del(new back(H), SLOT_BACK, TRUE)
if(belt)
- H.equip_to_slot_or_del(new belt(H),SLOT_BELT)
+ H.equip_to_slot_or_del(new belt(H), SLOT_BELT, TRUE)
if(gloves)
- H.equip_to_slot_or_del(new gloves(H),SLOT_GLOVES)
+ H.equip_to_slot_or_del(new gloves(H), SLOT_GLOVES, TRUE)
if(shoes)
- H.equip_to_slot_or_del(new shoes(H),SLOT_SHOES)
+ H.equip_to_slot_or_del(new shoes(H), SLOT_SHOES, TRUE)
if(head)
- H.equip_to_slot_or_del(new head(H),SLOT_HEAD)
+ H.equip_to_slot_or_del(new head(H), SLOT_HEAD, TRUE)
if(mask)
- H.equip_to_slot_or_del(new mask(H),SLOT_WEAR_MASK)
+ H.equip_to_slot_or_del(new mask(H), SLOT_WEAR_MASK, TRUE)
if(neck)
- H.equip_to_slot_or_del(new neck(H),SLOT_NECK)
+ H.equip_to_slot_or_del(new neck(H), SLOT_NECK, TRUE)
if(ears)
- H.equip_to_slot_or_del(new ears(H),SLOT_EARS)
+ H.equip_to_slot_or_del(new ears(H), SLOT_EARS, TRUE)
if(glasses)
- H.equip_to_slot_or_del(new glasses(H),SLOT_GLASSES)
+ H.equip_to_slot_or_del(new glasses(H), SLOT_GLASSES, TRUE)
if(id)
- H.equip_to_slot_or_del(new id(H),SLOT_WEAR_ID)
+ H.equip_to_slot_or_del(new id(H), SLOT_WEAR_ID, TRUE)
if(suit_store)
- H.equip_to_slot_or_del(new suit_store(H),SLOT_S_STORE)
+ H.equip_to_slot_or_del(new suit_store(H), SLOT_S_STORE, TRUE)
+ if(undershirt)
+ H.undershirt = initial(undershirt.name)
if(accessory)
var/obj/item/clothing/under/U = H.w_uniform
@@ -81,9 +195,9 @@
if(!visualsOnly) // Items in pockets or backpack don't show up on mob's icon.
if(l_pocket)
- H.equip_to_slot_or_del(new l_pocket(H),SLOT_L_STORE)
+ H.equip_to_slot_or_del(new l_pocket(H), SLOT_L_STORE, TRUE)
if(r_pocket)
- H.equip_to_slot_or_del(new r_pocket(H),SLOT_R_STORE)
+ H.equip_to_slot_or_del(new r_pocket(H), SLOT_R_STORE, TRUE)
if(box)
if(!backpack_contents)
@@ -97,7 +211,7 @@
if(!isnum(number))//Default to 1
number = 1
for(var/i in 1 to number)
- H.equip_to_slot_or_del(new path(H),SLOT_IN_BACKPACK)
+ H.equip_to_slot_or_del(new path(H), SLOT_IN_BACKPACK, TRUE)
if(!H.head && toggle_helmet && istype(H.wear_suit, /obj/item/clothing/suit/space/hardsuit))
var/obj/item/clothing/suit/space/hardsuit/HS = H.wear_suit
@@ -112,55 +226,178 @@
H.update_action_buttons_icon()
if(implants)
for(var/implant_type in implants)
- var/obj/item/implant/I = new implant_type
+ var/obj/item/implant/I = new implant_type(H)
I.implant(H, null, TRUE)
H.update_body()
return TRUE
+/**
+ * Apply a fingerprint from the passed in human to all items in the outfit
+ *
+ * Used for forensics setup when the mob is first equipped at roundstart
+ * essentially calls add_fingerprint to every defined item on the human
+ *
+ */
/datum/outfit/proc/apply_fingerprints(mob/living/carbon/human/H)
if(!istype(H))
return
if(H.back)
- H.back.add_fingerprint(H,1) //The 1 sets a flag to ignore gloves
+ H.back.add_fingerprint(H, ignoregloves = TRUE)
for(var/obj/item/I in H.back.contents)
- I.add_fingerprint(H,1)
+ I.add_fingerprint(H, ignoregloves = TRUE)
if(H.wear_id)
- H.wear_id.add_fingerprint(H,1)
+ H.wear_id.add_fingerprint(H, ignoregloves = TRUE)
if(H.w_uniform)
- H.w_uniform.add_fingerprint(H,1)
+ H.w_uniform.add_fingerprint(H, ignoregloves = TRUE)
if(H.wear_suit)
- H.wear_suit.add_fingerprint(H,1)
+ H.wear_suit.add_fingerprint(H, ignoregloves = TRUE)
if(H.wear_mask)
- H.wear_mask.add_fingerprint(H,1)
+ H.wear_mask.add_fingerprint(H, ignoregloves = TRUE)
if(H.wear_neck)
- H.wear_neck.add_fingerprint(H,1)
+ H.wear_neck.add_fingerprint(H, ignoregloves = TRUE)
if(H.head)
- H.head.add_fingerprint(H,1)
+ H.head.add_fingerprint(H, ignoregloves = TRUE)
if(H.shoes)
- H.shoes.add_fingerprint(H,1)
+ H.shoes.add_fingerprint(H, ignoregloves = TRUE)
if(H.gloves)
- H.gloves.add_fingerprint(H,1)
+ H.gloves.add_fingerprint(H, ignoregloves = TRUE)
if(H.ears)
- H.ears.add_fingerprint(H,1)
+ H.ears.add_fingerprint(H, ignoregloves = TRUE)
if(H.glasses)
- H.glasses.add_fingerprint(H,1)
+ H.glasses.add_fingerprint(H, ignoregloves = TRUE)
if(H.belt)
- H.belt.add_fingerprint(H,1)
+ H.belt.add_fingerprint(H, ignoregloves = TRUE)
for(var/obj/item/I in H.belt.contents)
- I.add_fingerprint(H,1)
+ I.add_fingerprint(H, ignoregloves = TRUE)
if(H.s_store)
- H.s_store.add_fingerprint(H,1)
+ H.s_store.add_fingerprint(H, ignoregloves = TRUE)
if(H.l_store)
- H.l_store.add_fingerprint(H,1)
+ H.l_store.add_fingerprint(H, ignoregloves = TRUE)
if(H.r_store)
- H.r_store.add_fingerprint(H,1)
+ H.r_store.add_fingerprint(H, ignoregloves = TRUE)
for(var/obj/item/I in H.held_items)
- I.add_fingerprint(H,1)
- return 1
+ I.add_fingerprint(H, ignoregloves = TRUE)
+ return TRUE
+/// Return a list of all the types that are required to disguise as this outfit type
/datum/outfit/proc/get_chameleon_disguise_info()
var/list/types = list(uniform, suit, back, belt, gloves, shoes, head, mask, neck, ears, glasses, id, l_pocket, r_pocket, suit_store, r_hand, l_hand)
types += chameleon_extras
listclearnulls(types)
return types
+
+/// Return a json list of this outfit
+/datum/outfit/proc/get_json_data()
+ . = list()
+ .["outfit_type"] = type
+ .["name"] = name
+ .["uniform"] = uniform
+ .["suit"] = suit
+ .["toggle_helmet"] = toggle_helmet
+ .["back"] = back
+ .["belt"] = belt
+ .["gloves"] = gloves
+ .["shoes"] = shoes
+ .["head"] = head
+ .["mask"] = mask
+ .["neck"] = neck
+ .["ears"] = ears
+ .["glasses"] = glasses
+ .["id"] = id
+ .["l_pocket"] = l_pocket
+ .["r_pocket"] = r_pocket
+ .["suit_store"] = suit_store
+ .["r_hand"] = r_hand
+ .["l_hand"] = l_hand
+ .["internals_slot"] = internals_slot
+ .["backpack_contents"] = backpack_contents
+ .["box"] = box
+ .["implants"] = implants
+ .["accessory"] = accessory
+
+/// Copy most vars from another outfit to this one
+/datum/outfit/proc/copy_from(datum/outfit/target)
+ name = target.name
+ uniform = target.uniform
+ suit = target.suit
+ toggle_helmet = target.toggle_helmet
+ back = target.back
+ belt = target.belt
+ gloves = target.gloves
+ shoes = target.shoes
+ head = target.head
+ mask = target.mask
+ neck = target.neck
+ ears = target.ears
+ glasses = target.glasses
+ id = target.id
+ l_pocket = target.l_pocket
+ r_pocket = target.r_pocket
+ suit_store = target.suit_store
+ r_hand = target.r_hand
+ l_hand = target.l_hand
+ internals_slot = target.internals_slot
+ backpack_contents = target.backpack_contents
+ box = target.box
+ implants = target.implants
+ accessory = target.accessory
+
+/// Prompt the passed in mob client to download this outfit as a json blob
+/datum/outfit/proc/save_to_file(mob/admin)
+ var/stored_data = get_json_data()
+ var/json = json_encode(stored_data)
+ //Kinda annoying but as far as i can tell you need to make actual file.
+ var/f = file("data/TempOutfitUpload")
+ fdel(f)
+ WRITE_FILE(f,json)
+ admin << ftp(f,"[name].json")
+
+/// Create an outfit datum from a list of json data
+/datum/outfit/proc/load_from(list/outfit_data)
+ //This could probably use more strict validation
+ name = outfit_data["name"]
+ uniform = text2path(outfit_data["uniform"])
+ suit = text2path(outfit_data["suit"])
+ toggle_helmet = outfit_data["toggle_helmet"]
+ back = text2path(outfit_data["back"])
+ belt = text2path(outfit_data["belt"])
+ gloves = text2path(outfit_data["gloves"])
+ shoes = text2path(outfit_data["shoes"])
+ head = text2path(outfit_data["head"])
+ mask = text2path(outfit_data["mask"])
+ neck = text2path(outfit_data["neck"])
+ ears = text2path(outfit_data["ears"])
+ glasses = text2path(outfit_data["glasses"])
+ id = text2path(outfit_data["id"])
+ l_pocket = text2path(outfit_data["l_pocket"])
+ r_pocket = text2path(outfit_data["r_pocket"])
+ suit_store = text2path(outfit_data["suit_store"])
+ r_hand = text2path(outfit_data["r_hand"])
+ l_hand = text2path(outfit_data["l_hand"])
+ internals_slot = outfit_data["internals_slot"]
+ var/list/backpack = outfit_data["backpack_contents"]
+ backpack_contents = list()
+ for(var/item in backpack)
+ var/itype = text2path(item)
+ if(itype)
+ backpack_contents[itype] = backpack[item]
+ box = text2path(outfit_data["box"])
+ var/list/impl = outfit_data["implants"]
+ implants = list()
+ for(var/I in impl)
+ var/imptype = text2path(I)
+ if(imptype)
+ implants += imptype
+ accessory = text2path(outfit_data["accessory"])
+ return TRUE
+
+/datum/outfit/vv_get_dropdown()
+ . = ..()
+ VV_DROPDOWN_OPTION("", "---")
+ VV_DROPDOWN_OPTION(VV_HK_TO_OUTFIT_EDITOR, "Outfit Editor")
+
+/datum/outfit/vv_do_topic(list/href_list)
+ . = ..()
+ if(href_list[VV_HK_TO_OUTFIT_EDITOR])
+ usr.client.open_outfit_editor(src)
diff --git a/code/datums/status_effects/debuffs.dm b/code/datums/status_effects/debuffs.dm
index a958d20276..1c49f61996 100644
--- a/code/datums/status_effects/debuffs.dm
+++ b/code/datums/status_effects/debuffs.dm
@@ -108,7 +108,7 @@
/datum/status_effect/off_balance/on_remove()
var/active_item = owner.get_active_held_item()
- if(is_type_in_typecache(active_item, GLOB.shove_disarming_types))
+ if(active_item)
owner.visible_message("[owner.name] regains their grip on \the [active_item]!", "You regain your grip on \the [active_item]", null, COMBAT_MESSAGE_RANGE)
return ..()
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/limbgrower.dm b/code/game/machinery/limbgrower.dm
index aa1d05884e..dc87322b57 100644
--- a/code/game/machinery/limbgrower.dm
+++ b/code/game/machinery/limbgrower.dm
@@ -1,9 +1,5 @@
-#define LIMBGROWER_MAIN_MENU 1
-#define LIMBGROWER_CATEGORY_MENU 2
-#define LIMBGROWER_CHEMICAL_MENU 3
-//use these for the menu system
-
-
+/// The limbgrower. Makes organd and limbs with synthflesh and chems.
+/// See [limbgrower_designs.dm] for everything we can make.
/obj/machinery/limbgrower
name = "limb grower"
desc = "It grows new limbs using Synthflesh."
@@ -15,161 +11,235 @@
active_power_usage = 100
circuit = /obj/item/circuitboard/machine/limbgrower
- var/operating = FALSE
- var/disabled = FALSE
+ /// The category of limbs we're browing in our UI.
+ var/selected_category = "human"
+ /// If we're currently printing something.
var/busy = FALSE
- var/prod_coeff = 1
+ /// How efficient our machine is. Better parts = less chemicals used and less power used. Range of 1 to 0.25.
+ var/production_coefficient = 1
+ /// How long it takes for us to print a limb. Affected by production_coefficient.
+ var/production_speed = 3 SECONDS
+ /// The design we're printing currently.
var/datum/design/being_built
+ /// Our internal techweb for limbgrower designs.
var/datum/techweb/stored_research
- var/selected_category
- var/screen = 1
+ /// All the categories of organs we can print.
var/list/categories = list(
- "human" = /datum/species/human,
- "lizard" = /datum/species/lizard,
- "mammal" = /datum/species/mammal,
- "insect" = /datum/species/insect,
- "fly" = /datum/species/fly,
- "plasmaman" = /datum/species/plasmaman,
- "xeno" = /datum/species/xeno,
- "other" = /datum/species,
- )
- var/list/stored_species = list()
+ "human",
+ "lizard",
+ "mammal",
+ "insect",
+ "fly",
+ "plasmaman",
+ "xeno",
+ "other",
+ )
var/obj/item/disk/data/dna_disk
/obj/machinery/limbgrower/Initialize()
create_reagents(100, OPENCONTAINER)
stored_research = new /datum/techweb/specialized/autounlocking/limbgrower
- for(var/i in categories)
- var/species = categories[i]
- stored_species[i] = new species()
. = ..()
+ AddComponent(/datum/component/plumbing/simple_demand)
+ AddComponent(/datum/component/simple_rotation, ROTATION_WRENCH | ROTATION_CLOCKWISE, null, CALLBACK(src, .proc/can_be_rotated))
-/obj/machinery/limbgrower/ui_interact(mob/user)
+/obj/machinery/limbgrower/ui_interact(mob/user, datum/tgui/ui)
. = ..()
- if(!is_operational())
- return
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "Limbgrower", src)
+ ui.open()
- var/dat = main_win(user)
+/obj/machinery/limbgrower/ui_data(mob/user)
+ var/list/data = list()
- switch(screen)
- if(LIMBGROWER_MAIN_MENU)
- dat = main_win(user)
- if(LIMBGROWER_CATEGORY_MENU)
- dat = category_win(user,selected_category)
- if(LIMBGROWER_CHEMICAL_MENU)
- dat = chemical_win(user)
+ for(var/datum/reagent/reagent_id in reagents.reagent_list)
+ var/list/reagent_data = list(
+ reagent_name = reagent_id.name,
+ reagent_amount = reagent_id.volume,
+ reagent_type = reagent_id.type
+ )
+ data["reagents"] += list(reagent_data)
- var/datum/browser/popup = new(user, "Limb Grower", name, 400, 500)
- popup.set_content(dat)
- popup.open()
+ data["total_reagents"] = reagents.total_volume
+ data["max_reagents"] = reagents.maximum_volume
+ data["busy"] = busy
+ var/list/disk_data = list()
+ disk_data["disk"] = dna_disk //Do i, the machine, have a disk?
+ disk_data["name"] = dna_disk?.fields["name"] //Name for the human saved if there is one
+ data["disk"] = disk_data
+
+ return data
+
+/obj/machinery/limbgrower/ui_static_data(mob/user)
+ var/list/data = list()
+ data["categories"] = list()
+
+ var/species_categories = categories.Copy()
+ for(var/species in species_categories)
+ species_categories[species] = list()
+ for(var/design_id in stored_research.researched_designs)
+ var/datum/design/limb_design = SSresearch.techweb_design_by_id(design_id)
+ for(var/found_category in species_categories)
+ if(found_category in limb_design.category)
+ species_categories[found_category] += limb_design
+
+ for(var/category in species_categories)
+ var/list/category_data = list(
+ name = category,
+ designs = list(),
+ )
+ for(var/datum/design/found_design in species_categories[category])
+ var/list/all_reagents = list()
+ for(var/reagent_typepath in found_design.reagents_list)
+ var/datum/reagent/reagent_id = find_reagent_object_from_type(reagent_typepath)
+ var/list/reagent_data = list(
+ name = reagent_id.name,
+ amount = (found_design.reagents_list[reagent_typepath] * production_coefficient),
+ )
+ all_reagents += list(reagent_data)
+
+ category_data["designs"] += list(list(
+ parent_category = category,
+ name = found_design.name,
+ id = found_design.id,
+ needed_reagents = all_reagents,
+ ))
+
+ data["categories"] += list(category_data)
+
+ return data
/obj/machinery/limbgrower/on_deconstruction()
- for(var/obj/item/reagent_containers/glass/G in component_parts)
- reagents.trans_to(G, G.reagents.maximum_volume)
+ for(var/obj/item/reagent_containers/glass/our_beaker in component_parts)
+ reagents.trans_to(our_beaker, our_beaker.reagents.maximum_volume)
..()
-/obj/machinery/limbgrower/attackby(obj/item/O, mob/user, params)
- if(busy)
+/obj/machinery/limbgrower/attackby(obj/item/user_item, mob/living/user, params)
+ if (busy)
to_chat(user, "\The [src] is busy. Please wait for completion of previous operation.")
return
- if(default_deconstruction_screwdriver(user, "limbgrower_panelopen", "limbgrower_idleoff", O))
- updateUsrDialog()
+ if(default_deconstruction_screwdriver(user, "limbgrower_panelopen", "limbgrower_idleoff", user_item))
+ ui_close(user)
return
- if(panel_open && default_deconstruction_crowbar(O))
- return
-
- if(user.a_intent == INTENT_HARM) //so we can hit the machine
+ if(user_item.tool_behaviour == TOOL_WRENCH && panel_open)
return ..()
- if(istype(O, /obj/item/disk))
+ if(panel_open && default_deconstruction_crowbar(user_item))
+ return
+
+ if(istype(user_item, /obj/item/disk))
if(dna_disk)
to_chat(user, "\The [src] already has a dna disk, take it out first!")
return
else
- O.forceMove(src)
- dna_disk = O
- to_chat(user, "You insert \the [O] into \the [src].")
+ user_item.forceMove(src)
+ dna_disk = user_item
+ to_chat(user, "You insert \the [user_item] into \the [src].")
+ playsound(src, 'sound/machines/terminal_insert_disc.ogg', 50, 0)
return
-/obj/machinery/limbgrower/Topic(href, href_list)
- if(..())
+ if(user.a_intent != INTENT_HELP)
+ return ..()
+
+/obj/machinery/limbgrower/proc/can_be_rotated()
+ if(panel_open)
+ return TRUE
+ return FALSE
+
+/obj/machinery/limbgrower/ui_act(action, list/params)
+ . = ..()
+ if(.)
return
- if (!busy)
- if(href_list["menu"])
- screen = text2num(href_list["menu"])
- if(href_list["category"])
- selected_category = href_list["category"]
+ if (busy)
+ to_chat(usr, "\The [src] is busy. Please wait for completion of previous operation.")
+ return
- if(href_list["disposeI"]) //Get rid of a reagent incase you add the wrong one by mistake
- reagents.del_reagent(text2path(href_list["disposeI"]))
+ switch(action)
- if(href_list["make"])
+ if("empty_reagent")
+ reagents.del_reagent(text2path(params["reagent_type"]))
+ . = TRUE
- /////////////////
- //href protection
- being_built = stored_research.isDesignResearchedID(href_list["make"]) //check if it's a valid design
+ if("eject_disk")
+ eject_disk(usr)
+
+ if("make_limb")
+ being_built = stored_research.isDesignResearchedID(params["design_id"])
if(!being_built)
- return
+ CRASH("[src] was passed an invalid design id!")
+ /// All the reagents we're using to make our organ.
+ var/list/consumed_reagents_list = being_built.reagents_list.Copy()
+ /// The amount of power we're going to use, based on how much reagent we use.
+ var/power = 0
- var/synth_cost = being_built.reagents_list[/datum/reagent/medicine/synthflesh]*prod_coeff
- var/power = max(2000, synth_cost/5)
+ for(var/reagent_id in consumed_reagents_list)
+ consumed_reagents_list[reagent_id] *= production_coefficient
+ if(!reagents.has_reagent(reagent_id, consumed_reagents_list[reagent_id]))
+ audible_message("\The [src] buzzes.")
+ playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
+ return
- if(reagents.has_reagent(/datum/reagent/medicine/synthflesh, being_built.reagents_list[/datum/reagent/medicine/synthflesh]*prod_coeff))
- busy = TRUE
- use_power(power)
- flick("limbgrower_fill",src)
- icon_state = "limbgrower_idleon"
- addtimer(CALLBACK(src, .proc/build_item),32*prod_coeff)
+ power = max(2000, (power + consumed_reagents_list[reagent_id]))
- if(href_list["dna_disk"])
- var/mob/living/carbon/user = usr
- if(istype(user))
- if(!dna_disk)
- var/obj/item/disk/diskette = user.get_active_held_item()
- if(istype(diskette))
- diskette.forceMove(src)
- dna_disk = diskette
- to_chat(user, "You insert \the [diskette] into \the [src].")
- else
- dna_disk.forceMove(src.loc)
- user.put_in_active_hand(dna_disk)
- to_chat(user, "You remove \the [dna_disk] from \the [src].")
- dna_disk = null
- else
- to_chat(user, "You are unable to grasp \the [dna_disk] disk from \the [src].")
- else
- to_chat(usr, "\The [src] is busy. Please wait for completion of previous operation.")
+ busy = TRUE
+ use_power(power)
+ flick("limbgrower_fill",src)
+ icon_state = "limbgrower_idleon"
+ selected_category = params["active_tab"]
+ addtimer(CALLBACK(src, .proc/build_item, consumed_reagents_list), production_speed * production_coefficient)
+ . = TRUE
- updateUsrDialog()
return
-/obj/machinery/limbgrower/proc/build_item()
- if(reagents.has_reagent(/datum/reagent/medicine/synthflesh, being_built.reagents_list[/datum/reagent/medicine/synthflesh]*prod_coeff)) //sanity check, if this happens we are in big trouble
- reagents.remove_reagent(/datum/reagent/medicine/synthflesh, being_built.reagents_list[/datum/reagent/medicine/synthflesh]*prod_coeff)
- var/buildpath = being_built.build_path
- if(ispath(buildpath, /obj/item/bodypart)) //This feels like spaghetti code, but i need to initiliaze a limb somehow
- build_limb(buildpath)
- else if(ispath(buildpath, /obj/item/organ/genital)) //genitals are uhh... customizable
- build_genital(buildpath)
- else
- //Just build whatever it is
- new buildpath(loc)
- else
- src.visible_message(" Something went very wrong and there isnt enough synthflesh anymore!")
- busy = FALSE
- flick("limbgrower_unfill",src)
- icon_state = "limbgrower_idleoff"
- updateUsrDialog()
+/*
+ * The process of beginning to build a limb or organ.
+ * Goes through and sanity checks that we actually have enough reagent to build our item.
+ * Then, remove those reagents from our reagents datum.
+ *
+ * After the reagents are handled, we can proceede with making the limb or organ. (Limbs are handled in a separate proc)
+ *
+ * modified_consumed_reagents_list - the list of reagents we will consume on build, modified by the production coefficient.
+ */
+/obj/machinery/limbgrower/proc/build_item(list/modified_consumed_reagents_list)
+ for(var/reagent_id in modified_consumed_reagents_list)
+ if(!reagents.has_reagent(reagent_id, modified_consumed_reagents_list[reagent_id]))
+ audible_message("\The [src] buzzes.")
+ playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
+ break
-/obj/machinery/limbgrower/proc/build_limb(buildpath)
+ reagents.remove_reagent(reagent_id, modified_consumed_reagents_list[reagent_id])
+
+ var/built_typepath = being_built.build_path
+ // If we have a bodypart, we need to initialize the limb on its own. Otherwise we can build it here.
+ if(ispath(built_typepath, /obj/item/bodypart))
+ build_limb(built_typepath)
+ else if(ispath(built_typepath, /obj/item/organ/genital)) //genitals are uhh... customizable
+ build_genital(built_typepath)
+ else
+ new built_typepath(loc)
+
+ busy = FALSE
+ flick("limbgrower_unfill", src)
+ icon_state = "limbgrower_idleoff"
+
+/*
+ * The process of putting together a limb.
+ * This is called from after we remove the reagents, so this proc is just initializing the limb type.
+ *
+ * This proc handles skin / mutant color, greyscaling, names and descriptions, and various other limb creation steps.
+ *
+ * built_typepath - the path of the bodypart we're building.
+ */
+/obj/machinery/limbgrower/proc/build_limb(built_typepath)
//i need to create a body part manually using a set icon (otherwise it doesnt appear)
var/obj/item/bodypart/limb
- var/datum/species/selected = stored_species[selected_category]
- limb = new buildpath(loc)
+ var/datum/species/selected = GLOB.species_datums[selected_category]
+ limb = new built_typepath(loc)
limb.base_bp_icon = selected.icon_limbs || DEFAULT_BODYPART_ICON_ORGANIC
limb.species_id = selected.limbs_id
limb.color_src = (MUTCOLORS in selected.species_traits ? MUTCOLORS : (selected.use_skintones ? SKINTONE : FALSE))
@@ -189,135 +259,103 @@
BP.name = "\improper synthetic [lowertext(selected.name)] [limb.name]"
BP.desc = "A synthetic [selected_category] limb that will morph on its first use in surgery. This one is for the [parse_zone(limb.body_zone)]."
-/obj/machinery/limbgrower/proc/build_genital(buildpath)
+/*
+ * Builds genitals, modifies to be the same
+ * as the person's cloning data on the data disk
+ */
+/obj/machinery/limbgrower/proc/build_genital(built_typepath)
//i needed to create a way to customize gene tools using dna
var/list/features = dna_disk?.fields["features"]
if(length(features))
- switch(buildpath)
+ switch(built_typepath)
if(/obj/item/organ/genital/penis)
var/obj/item/organ/genital/penis/penis = new(loc)
if(features["has_cock"])
penis.shape = features["cock_shape"]
penis.length = features["cock_shape"]
penis.diameter_ratio = features["cock_diameter_ratio"]
- penis.color = sanitize_hexcolor(features["cock_color"], 6)
- penis.update_icon()
+ penis.color = sanitize_hexcolor(features["cock_color"], 6, TRUE)
+ penis.update()
if(/obj/item/organ/genital/testicles)
var/obj/item/organ/genital/testicles/balls = new(loc)
if(features["has_balls"])
- balls.color = sanitize_hexcolor(features["balls_color"], 6)
+ balls.color = sanitize_hexcolor(features["balls_color"], 6, TRUE)
balls.shape = features["balls_shape"]
balls.size = features["balls_size"]
balls.fluid_rate = features["balls_cum_rate"]
balls.fluid_mult = features["balls_cum_mult"]
balls.fluid_efficiency = features["balls_efficiency"]
+ balls.update()
if(/obj/item/organ/genital/vagina)
var/obj/item/organ/genital/vagina/vegana = new(loc)
- if(features["has_vagina"])
- vegana.color = sanitize_hexcolor(features["vag_color"], 6)
+ if(features["has_vag"])
+ vegana.color = sanitize_hexcolor(features["vag_color"], 6, TRUE)
vegana.shape = features["vag_shape"]
+ vegana.update()
if(/obj/item/organ/genital/breasts)
var/obj/item/organ/genital/breasts/boobs = new(loc)
if(features["has_breasts"])
- boobs.color = sanitize_hexcolor(features["breasts_color"], 6)
+ boobs.color = sanitize_hexcolor(features["breasts_color"], 6, TRUE)
boobs.size = features["breasts_size"]
boobs.shape = features["breasts_shape"]
if(!features["breasts_producing"])
boobs.genital_flags &= ~(GENITAL_FUID_PRODUCTION|CAN_CLIMAX_WITH|CAN_MASTURBATE_WITH)
+ boobs.update()
else
- new buildpath(loc)
+ new built_typepath(loc)
else
- new buildpath(loc)
+ new built_typepath(loc)
/obj/machinery/limbgrower/RefreshParts()
reagents.maximum_volume = 0
- for(var/obj/item/reagent_containers/glass/G in component_parts)
- reagents.maximum_volume += G.volume
- G.reagents.trans_to(src, G.reagents.total_volume)
- var/T=1.2
- for(var/obj/item/stock_parts/manipulator/M in component_parts)
- T -= M.rating*0.2
- prod_coeff = min(1,max(0,T)) // Coeff going 1 -> 0,8 -> 0,6 -> 0,4
+ for(var/obj/item/reagent_containers/glass/our_beaker in component_parts)
+ reagents.maximum_volume += our_beaker.volume
+ our_beaker.reagents.trans_to(src, our_beaker.reagents.total_volume)
+ production_coefficient = 1.2
+ for(var/obj/item/stock_parts/manipulator/our_manipulator in component_parts)
+ production_coefficient -= our_manipulator.rating * 0.2
+ production_coefficient = clamp(production_coefficient, 0, 1) // coefficient goes from 1 -> 0.8 -> 0.6 -> 0.4
/obj/machinery/limbgrower/examine(mob/user)
. = ..()
+ if(!panel_open)
+ . += "It looks like as if the panel were open you could rotate it with a wrench."
+ else
+ . += "The panel is open."
if(in_range(user, src) || isobserver(user))
- . += "The status display reads: Storing up to [reagents.maximum_volume]u of synthflesh.
Synthflesh consumption at [prod_coeff*100]%."
+ . += "The status display reads: Storing up to [reagents.maximum_volume]u of reagents.
Reagent consumption rate at [production_coefficient * 100]%."
-/obj/machinery/limbgrower/proc/main_win(mob/user)
- var/dat = "[src] Menu:
"
- dat += "[dna_disk ? "Remove" : "Insert"] cloning data disk"
- dat += "
"
- dat += "Chemical Storage"
- dat += materials_printout()
- dat += ""
-
- for(var/C in categories)
- dat += " [C] "
- dat += ""
- //one category per line
-
- dat += " Browsing [selected_category]:
"
- dat += materials_printout()
-
- for(var/v in stored_research.researched_designs)
- var/datum/design/D = SSresearch.techweb_design_by_id(v)
- if(!(selected_category in D.category))
- continue
- if(disabled || !can_build(D))
- dat += "[D.name]"
- else
- dat += "[D.name]"
- dat += "[get_design_cost(D)]
"
-
- dat += "Browsing Chemical Storage:
"
- dat += materials_printout()
-
- for(var/datum/reagent/R in reagents.reagent_list)
- dat += "[R.name]: [R.volume]"
- dat += "Purge
"
-
- dat += "
"
- return dat
-
-/obj/machinery/limbgrower/proc/can_build(datum/design/D)
- return (reagents.has_reagent(/datum/reagent/medicine/synthflesh, D.reagents_list[/datum/reagent/medicine/synthflesh]*prod_coeff)) //Return whether the machine has enough synthflesh to produce the design
-
-/obj/machinery/limbgrower/proc/get_design_cost(datum/design/D)
- var/dat
- if(D.reagents_list[/datum/reagent/medicine/synthflesh])
- dat += "[D.reagents_list[/datum/reagent/medicine/synthflesh] * prod_coeff] Synthetic flesh "
- return dat
+/*
+ * Checks our reagent list to see if a design can be built.
+ *
+ * limb_design - the design we're checking for buildability.
+ *
+ * returns TRUE if we have enough reagent to build it. Returns FALSE if we do not.
+ */
+/obj/machinery/limbgrower/proc/can_build(datum/design/limb_design)
+ for(var/datum/reagent/reagent_id in limb_design.reagents_list)
+ if(!reagents.has_reagent(reagent_id, limb_design.reagents_list[reagent_id] * production_coefficient))
+ return FALSE
+ return TRUE
+/// Emagging a limbgrower allows you to build synthetic armblades.
/obj/machinery/limbgrower/emag_act(mob/user)
. = ..()
if(obj_flags & EMAGGED)
return
- for(var/id in SSresearch.techweb_designs)
- var/datum/design/D = SSresearch.techweb_design_by_id(id)
- if((D.build_type & LIMBGROWER) && ("emagged" in D.category))
- stored_research.add_design(D)
+ for(var/design_id in SSresearch.techweb_designs)
+ var/datum/design/found_design = SSresearch.techweb_design_by_id(design_id)
+ if((found_design.build_type & LIMBGROWER) && ("emagged" in found_design.category))
+ stored_research.add_design(found_design)
to_chat(user, "A warning flashes onto the screen, stating that safety overrides have been deactivated!")
obj_flags |= EMAGGED
- return TRUE
+ update_static_data(user)
/obj/machinery/limbgrower/AltClick(mob/living/user)
. = ..()
+ eject_disk(user)
+
+/obj/machinery/limbgrower/proc/eject_disk(mob/user)
if(istype(user) && user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
if(busy)
to_chat(user, "\The [src] is busy. Please wait for completion of previous operation.")
@@ -326,6 +364,7 @@
dna_disk.forceMove(src.loc)
user.put_in_active_hand(dna_disk)
to_chat(user, "You remove \the [dna_disk] from \the [src].")
+ playsound(src, 'sound/machines/terminal_insert_disc.ogg', 50, 0)
dna_disk = null
else
to_chat(user, "\The [src] has doesn't have a disk on it!")
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 70e4441c66..635ea97902 100644
--- a/code/game/objects/items/RPD.dm
+++ b/code/game/objects/items/RPD.dm
@@ -375,12 +375,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/airlock_painter.dm b/code/game/objects/items/airlock_painter.dm
index b28de437ed..8edd1c2a76 100644
--- a/code/game/objects/items/airlock_painter.dm
+++ b/code/game/objects/items/airlock_painter.dm
@@ -157,7 +157,7 @@
to_chat(user, "You need to get closer!")
return
if(use_paint(user) && isturf(F))
- F.AddElement(/datum/element/decal, 'icons/turf/decals.dmi', stored_decal_total, turn(stored_dir, -dir2angle(F.dir)), CLEAN_STRONG, color, null, null, alpha)
+ F.AddElement(/datum/element/decal, 'icons/turf/decals.dmi', stored_decal_total, stored_dir, CLEAN_STRONG, color, null, null, alpha)
/obj/item/airlock_painter/decal/attack_self(mob/user)
if((ink) && (ink.charges >= 1))
@@ -180,6 +180,11 @@
stored_decal_total = "[stored_decal][yellow_fix][stored_color]"
return
+/obj/item/airlock_painter/decal/ui_assets(mob/user)
+ return list(
+ get_asset_datum(/datum/asset/spritesheet/decals)
+ )
+
/obj/item/airlock_painter/decal/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
@@ -189,6 +194,7 @@
/obj/item/airlock_painter/decal/ui_data(mob/user)
var/list/data = list()
data["decal_direction"] = stored_dir
+ data["decal_dir_text"] = dir2text(stored_dir)
data["decal_color"] = stored_color
data["decal_style"] = stored_decal
data["decal_list"] = list()
diff --git a/code/game/objects/items/chromosome.dm b/code/game/objects/items/chromosome.dm
index 3acf3cfe5c..f5b693879b 100644
--- a/code/game/objects/items/chromosome.dm
+++ b/code/game/objects/items/chromosome.dm
@@ -75,18 +75,3 @@
desc = "A chromosome that reduces action based mutation cooldowns by by 50%."
icon_state = "energy"
energy_coeff = 0.5
-
-/obj/item/chromosome/reinforcer
- name = "reinforcement chromosome"
- desc = "A chromosome that renders mutations immune to mutadone."
- icon_state = "reinforcer"
- weight = 3
-
-/obj/item/chromosome/reinforcer/can_apply(datum/mutation/human/HM)
- if(!HM || !(HM.can_chromosome == CHROMOSOME_NONE))
- return FALSE
- return !HM.mutadone_proof
-
-/obj/item/chromosome/reinforcer/apply(datum/mutation/human/HM)
- HM.mutadone_proof = TRUE
- ..()
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/miscellaneous.dm b/code/game/objects/items/miscellaneous.dm
index 224c4ffb9b..80466832a6 100644
--- a/code/game/objects/items/miscellaneous.dm
+++ b/code/game/objects/items/miscellaneous.dm
@@ -135,8 +135,8 @@
/obj/item/organ/cyberimp/arm/toolset,
/obj/item/organ/cyberimp/arm/surgery,
/obj/item/organ/cyberimp/chest/thrusters,
- /obj/item/organ/lungs/cybernetic,
- /obj/item/organ/liver/cybernetic) //cyberimplants range from a nice bonus to fucking broken bullshit so no subtypesof
+ /obj/item/organ/lungs/cybernetic/tier3,
+ /obj/item/organ/liver/cybernetic/tier3) //cyberimplants range from a nice bonus to fucking broken bullshit so no subtypesof
for(var/V in templist)
var/atom/A = V
augment_list[initial(A.name)] = A
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 db55b0414c..e73097bf18 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..7721de42ad 100644
--- a/code/game/objects/items/stunbaton.dm
+++ b/code/game/objects/items/stunbaton.dm
@@ -16,17 +16,21 @@
armor = list("melee" = 0, "bullet" = 0, "laser" = 0, "energy" = 0, "bomb" = 50, "bio" = 0, "rad" = 0, "fire" = 80, "acid" = 80)
attack_speed = CLICK_CD_MELEE
- var/stamforce = 35
+ var/stamina_loss_amount = 35
var/turned_on = FALSE
var/knockdown = TRUE
var/obj/item/stock_parts/cell/cell
var/hitcost = 750
var/throw_hit_chance = 35
var/preload_cell_type //if not empty the baton starts with this type of cell
+ var/cooldown_duration = 5 SECONDS //How long our baton rightclick goes on cooldown for after applying a knockdown
+ var/status_duration = 5 SECONDS //how long our status effects last for otherwise
+ COOLDOWN_DECLARE(shove_cooldown)
/obj/item/melee/baton/examine(mob/user)
. = ..()
- . += "Right click attack while in combat mode to disarm instead of stun."
+ . += "Right click attack while in combat mode to knockdown, but only once per [cooldown_duration / 10] seconds."
+ . += "This knockdown will also put them off balance for [status_duration / 20] seconds, allowing you to shove a weapon out of their hand with a right click in Disarm intent."
/obj/item/melee/baton/get_cell()
. = cell
@@ -53,8 +57,8 @@
/obj/item/melee/baton/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum)
..()
//Only mob/living types have stun handling
- if(turned_on && prob(throw_hit_chance) && iscarbon(hit_atom))
- baton_stun(hit_atom)
+ if(turned_on && prob(throw_hit_chance) && iscarbon(hit_atom) && thrownby)
+ baton_stun(hit_atom, thrownby, shoving = TRUE)
/obj/item/melee/baton/loaded //this one starts with a cell pre-installed.
preload_cell_type = /obj/item/stock_parts/cell/high/plus
@@ -74,6 +78,16 @@
//we're below minimum, turn off
switch_status(FALSE)
+///Check for our cell to determine how much penetration our weapon does.
+/obj/item/melee/baton/proc/get_cell_zap_pen()
+ var/obj/item/stock_parts/cell/copper_top = get_cell()
+ if(copper_top)
+ var/chargepower = copper_top.maxcharge
+ var/zap_penetration = (chargepower/1000) //This is our effective penetration. Every 1000 max charge, we get 1 pen power. A high capacity cell is equal to 10 armor pen, as an example.
+ return zap_penetration
+ else
+ return 0
+
/obj/item/melee/baton/proc/switch_status(new_status = FALSE, silent = FALSE)
if(turned_on != new_status)
turned_on = new_status
@@ -101,6 +115,7 @@
var/obj/item/stock_parts/cell/copper_top = get_cell()
if(copper_top)
. += "\The [src] is [round(copper_top.percent())]% charged."
+ . += "\The [src] currently can penetrate [round(copper_top.maxcharge/1000)]% of enemy armor thanks to it's loaded cell."
else
. += "\The [src] does not have a power source installed."
@@ -150,10 +165,10 @@
/obj/item/melee/baton/alt_pre_attack(atom/A, mob/living/user, params)
if(!user.CheckActionCooldown(CLICK_CD_MELEE))
return
- . = common_baton_melee(A, user, TRUE) //return true (attackchain interrupt) if this also returns true. no harm-disarming.
+ . = common_baton_melee(A, user, TRUE) //return true (attackchain interrupt) if this also returns true. no harm-shoving.
//return TRUE to interrupt attack chain.
-/obj/item/melee/baton/proc/common_baton_melee(mob/M, mob/living/user, disarming = FALSE)
+/obj/item/melee/baton/proc/common_baton_melee(mob/M, mob/living/user, shoving = FALSE)
if(iscyborg(M) || !isliving(M)) //can't baton cyborgs
return FALSE
if(turned_on && HAS_TRAIT(user, TRAIT_CLUMSY) && prob(50))
@@ -167,21 +182,26 @@
if(check_martial_counter(L, user))
return TRUE
if(turned_on)
- if(baton_stun(M, user, disarming))
+ if(baton_stun(M, user, shoving))
user.do_attack_animation(M)
else if(user.a_intent != INTENT_HARM) //they'll try to bash in the last proc.
M.visible_message("[user] has prodded [M] with [src]. Luckily it was off.", \
"[user] has prodded you with [src]. Luckily it was off")
- return disarming || (user.a_intent != INTENT_HARM)
+ return shoving || (user.a_intent != INTENT_HARM)
-/obj/item/melee/baton/proc/baton_stun(mob/living/L, mob/living/user, disarming = FALSE)
+/obj/item/melee/baton/proc/baton_stun(mob/living/L, mob/living/user, shoving = FALSE)
var/list/return_list = list()
if(L.mob_run_block(src, 0, "[user]'s [name]", ATTACK_TYPE_MELEE, 0, user, null, return_list) & BLOCK_SUCCESS) //No message; check_shields() handles that
playsound(L, 'sound/weapons/genhit.ogg', 50, 1)
return FALSE
- var/stunpwr = stamforce
- stunpwr = block_calculate_resultant_damage(stunpwr, return_list)
+ var/final_stamina_loss_amount = stamina_loss_amount //Our stunning power for the baton
+ var/shoved = FALSE //Did we succeed on knocking our target over?
+ var/zap_penetration = get_cell_zap_pen() //Find out what kind of cell we have, and calculating the resultant armor pen we get from it
+ var/zap_block = L.run_armor_check(BODY_ZONE_CHEST, "melee", null, null, zap_penetration) //armor check, including calculation for armor penetration, for our attack
+ final_stamina_loss_amount = block_calculate_resultant_damage(final_stamina_loss_amount, return_list)
+
var/obj/item/stock_parts/cell/our_cell = get_cell()
+
if(!our_cell)
switch_status(FALSE)
return FALSE
@@ -194,26 +214,31 @@
L.visible_message("[user] has prodded [L] with [src]. Luckily it was out of charge.", \
"[user] has prodded you with [src]. Luckily it was out of charge.")
return FALSE
- stunpwr *= round(stuncharge/hitcost, 0.1)
+ final_stamina_loss_amount *= round(stuncharge/hitcost, 0.1)
if(user && !user.UseStaminaBuffer(getweight(user, STAM_COST_BATON_MOB_MULT), warn = TRUE))
return FALSE
- if(!disarming)
- if(knockdown)
- L.DefaultCombatKnockdown(50, override_stamdmg = 0) //knockdown
- L.adjustStaminaLoss(stunpwr)
- else
- L.drop_all_held_items() //no knockdown/stamina damage, instead disarm.
+ if(shoving && COOLDOWN_FINISHED(src, shove_cooldown) && !HAS_TRAIT(L, TRAIT_IWASBATONED)) //Rightclicking applies a knockdown, but only once every couple of seconds, based on the cooldown_duration var. If they were recently knocked down, they can't be knocked down again by a baton.
+ L.DefaultCombatKnockdown(50, override_stamdmg = 0)
+ L.apply_status_effect(STATUS_EFFECT_TASED_WEAK, status_duration) //Even if they shove themselves up, they're still slowed.
+ L.apply_status_effect(STATUS_EFFECT_OFF_BALANCE, status_duration) //They're very likely to drop items if shoved briefly after a knockdown.
+ shoved = TRUE
+ COOLDOWN_START(src, shove_cooldown, cooldown_duration)
+ ADD_TRAIT(L, TRAIT_IWASBATONED, STATUS_EFFECT_TRAIT) //Prevents swapping to a new baton to avoid the cooldown by just acquiring more batons
+ addtimer(TRAIT_CALLBACK_REMOVE(L, TRAIT_IWASBATONED, STATUS_EFFECT_TRAIT), cooldown_duration)
+ playsound(loc, 'sound/weapons/zapbang.ogg', 50, 1, -1)
+ else //If we cannot/don't knock down the target, we apply a stagger, which keeps them from just running off
+ L.apply_status_effect(STATUS_EFFECT_STAGGERED, status_duration)
- L.apply_effect(EFFECT_STUTTER, stamforce)
+ L.apply_damage (final_stamina_loss_amount, STAMINA, BODY_ZONE_CHEST, zap_block)
+ L.apply_effect(EFFECT_STUTTER, stamina_loss_amount)
SEND_SIGNAL(L, COMSIG_LIVING_MINOR_SHOCK)
if(user)
- L.lastattacker = user.real_name
- L.lastattackerckey = user.ckey
- 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")
+ L.set_last_attacker(user)
+ L.visible_message("[user] has [shoved ? "brutally stunned" : "stunned"] [L] with [src]!", \
+ "[user] has [shoved ? "brutally stunnned" : "stunned"] you with [src]!")
+ log_combat(user, L, shoved ? "stunned and attempted knockdown" : "stunned")
playsound(loc, 'sound/weapons/egloves.ogg', 50, 1, -1)
@@ -228,7 +253,7 @@
user.visible_message("[user] accidentally hits [user.p_them()]self with [src]!", \
"You accidentally hit yourself with [src]!")
SEND_SIGNAL(user, COMSIG_LIVING_MINOR_SHOCK)
- user.DefaultCombatKnockdown(stamforce*6)
+ user.DefaultCombatKnockdown(stamina_loss_amount*6)
playsound(loc, 'sound/weapons/egloves.ogg', 50, 1, -1)
deductcharge(hitcost)
@@ -306,18 +331,20 @@
w_class = WEIGHT_CLASS_BULKY
force = 3
throwforce = 5
- stamforce = 25
+ stamina_loss_amount = 25
hitcost = 1000
throw_hit_chance = 10
slot_flags = ITEM_SLOT_BACK
+ cooldown_duration = 7 SECONDS //It's a little on the weak side
+ status_duration = 3 //Slows someone for a tiny bit
var/obj/item/assembly/igniter/sparkler
/obj/item/melee/baton/cattleprod/Initialize()
. = ..()
sparkler = new (src)
- sparkler.activate_cooldown = 5
+ sparkler.activate_cooldown = 7 //Helps visualize the knockdown
-/obj/item/melee/baton/cattleprod/baton_stun()
+/obj/item/melee/baton/cattleprod/baton_stun(mob/living/L, mob/living/carbon/user, shoving = FALSE)
sparkler?.activate()
. = ..()
@@ -344,8 +371,8 @@
/obj/item/melee/baton/boomerang/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum)
if(turned_on)
var/caught = hit_atom.hitby(src, FALSE, FALSE, throwingdatum=throwingdatum)
- if(ishuman(hit_atom) && !caught && prob(throw_hit_chance))//if they are a carbon and they didn't catch it
- baton_stun(hit_atom)
+ if(ishuman(hit_atom) && !caught && prob(throw_hit_chance) && thrownby)//if they are a carbon and they didn't catch it
+ baton_stun(hit_atom, thrownby, shoving = TRUE)
if(thrownby && !caught)
sleep(1)
if(!QDELETED(src))
diff --git a/code/game/objects/items/teleprod.dm b/code/game/objects/items/teleprod.dm
index 63bde36976..94eb6fc85b 100644
--- a/code/game/objects/items/teleprod.dm
+++ b/code/game/objects/items/teleprod.dm
@@ -6,7 +6,7 @@
item_state = "teleprod"
slot_flags = null
-/obj/item/melee/baton/cattleprod/teleprod/baton_stun(mob/living/L, mob/living/carbon/user)//handles making things teleport when hit
+/obj/item/melee/baton/cattleprod/teleprod/baton_stun(mob/living/L, mob/living/carbon/user, shoving = FALSE)//handles making things teleport when hit
. = ..()
if(!. || L.anchored)
return
@@ -16,7 +16,7 @@
user.visible_message("[user] accidentally hits [user.p_them()]self with [src]!", \
"You accidentally hit yourself with [src]!")
SEND_SIGNAL(user, COMSIG_LIVING_MINOR_SHOCK)
- user.DefaultCombatKnockdown(stamforce * 6)
+ user.DefaultCombatKnockdown(stamina_loss_amount * 6)
playsound(loc, 'sound/weapons/egloves.ogg', 50, 1, -1)
if(do_teleport(user, get_turf(user), 50, channel = TELEPORT_CHANNEL_BLUESPACE))
deductcharge(hitcost)
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/game/objects/structures/watercloset.dm b/code/game/objects/structures/watercloset.dm
index dba8d5de45..01c62a7901 100644
--- a/code/game/objects/structures/watercloset.dm
+++ b/code/game/objects/structures/watercloset.dm
@@ -554,7 +554,7 @@
if(B.cell)
if(B.cell.charge > 0 && B.turned_on)
flick("baton_active", src)
- var/stunforce = B.stamforce
+ var/stunforce = B.stamina_loss_amount
user.DefaultCombatKnockdown(stunforce * 2)
user.stuttering = stunforce/20
B.deductcharge(B.hitcost)
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/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index f22ceb6a2d..96eb4e4a0f 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -93,7 +93,7 @@ GLOBAL_PROTECT(admin_verbs_ban)
GLOBAL_LIST_INIT(admin_verbs_sounds, list(/client/proc/play_local_sound, /client/proc/play_sound, /client/proc/manual_play_web_sound, /client/proc/set_round_end_sound))
GLOBAL_PROTECT(admin_verbs_sounds)
GLOBAL_LIST_INIT(admin_verbs_fun, list(
- /client/proc/cmd_admin_dress,
+ /client/proc/cmd_select_equipment,
/client/proc/cmd_admin_gib_self,
/client/proc/drop_bomb,
/client/proc/set_dynex_scale,
@@ -232,7 +232,7 @@ GLOBAL_LIST_INIT(admin_verbs_hideable, list(
/client/proc/play_local_sound,
/client/proc/play_sound,
/client/proc/set_round_end_sound,
- /client/proc/cmd_admin_dress,
+ /client/proc/cmd_select_equipment,
/client/proc/cmd_admin_gib_self,
/client/proc/drop_bomb,
/client/proc/drop_dynex_bomb,
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/outfit_editor.dm b/code/modules/admin/outfit_editor.dm
new file mode 100644
index 0000000000..9a99d8b20e
--- /dev/null
+++ b/code/modules/admin/outfit_editor.dm
@@ -0,0 +1,196 @@
+
+/client/proc/open_outfit_editor(datum/outfit/target)
+ var/datum/outfit_editor/ui = new(usr, target)
+ ui.ui_interact(usr)
+
+#define OUTFIT_EDITOR_NAME "Outfit-O-Tron 9000"
+/datum/outfit_editor
+ var/client/owner
+
+ var/dummy_key
+
+ var/datum/outfit/drip
+
+/datum/outfit_editor/New(user, datum/outfit/target)
+ owner = CLIENT_FROM_VAR(user)
+
+ if(ispath(target))
+ drip = new /datum/outfit
+ drip.copy_from(new target)
+ else if(istype(target))
+ drip = target
+ else
+ drip = new /datum/outfit
+ drip.name = "New Outfit"
+
+/datum/outfit_editor/ui_state(mob/user)
+ return GLOB.admin_state
+
+/datum/outfit_editor/ui_status(mob/user, datum/ui_state/state)
+ if(QDELETED(drip))
+ return UI_CLOSE
+ return ..()
+
+/datum/outfit_editor/ui_close(mob/user)
+ clear_human_dummy(dummy_key)
+ qdel(src)
+
+/datum/outfit_editor/proc/init_dummy()
+ dummy_key = "outfit_editor_[owner]"
+ generate_dummy_lookalike(dummy_key, owner.mob)
+ unset_busy_human_dummy(dummy_key)
+
+/datum/outfit_editor/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "OutfitEditor", OUTFIT_EDITOR_NAME)
+ ui.open()
+ ui.set_autoupdate(FALSE)
+
+/datum/outfit_editor/proc/entry(data)
+ if(ispath(data, /obj/item))
+ var/obj/item/item = data
+ return list(
+ "path" = item,
+ "name" = initial(item.name),
+ "desc" = initial(item.desc),
+ // at this point initializing the item is probably faster tbh
+ "sprite" = icon2base64(icon(initial(item.icon), initial(item.icon_state))),
+ )
+
+ return data
+
+/datum/outfit_editor/proc/serialize_outfit()
+ var/list/outfit_slots = drip.get_json_data()
+ . = list()
+ for(var/key in outfit_slots)
+ var/val = outfit_slots[key]
+ . += list("[key]" = entry(val))
+
+/datum/outfit_editor/ui_data(mob/user)
+ var/list/data = list()
+
+ data["outfit"] = serialize_outfit()
+ data["saveable"] = !GLOB.custom_outfits.Find(drip)
+
+ if(!dummy_key)
+ init_dummy()
+ var/icon/dummysprite = get_flat_human_icon(null,
+ dummy_key = dummy_key,
+ showDirs = list(SOUTH),
+ outfit_override = drip)
+ data["dummy64"] = icon2base64(dummysprite)
+
+ return data
+
+
+/datum/outfit_editor/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ if(..())
+ return
+ . = TRUE
+
+ var/slot = params["slot"]
+ switch(action)
+ if("click")
+ choose_item(slot)
+ if("ctrlClick")
+ choose_any_item(slot)
+ if("clear")
+ if(drip.vars.Find(slot))
+ drip.vars[slot] = null
+
+ if("rename")
+ var/newname = stripped_input(owner, "What do you want to name this outfit?", OUTFIT_EDITOR_NAME)
+ if(newname)
+ drip.name = newname
+ if("save")
+ GLOB.custom_outfits |= drip
+ SStgui.update_user_uis(owner.mob)
+ if("delete")
+ GLOB.custom_outfits -= drip
+ SStgui.update_user_uis(owner.mob)
+ if("vv")
+ owner.debug_variables(drip)
+
+
+/datum/outfit_editor/proc/set_item(slot, obj/item/choice)
+ if(!choice)
+ return
+ if(!ispath(choice))
+ alert(owner, "Invalid item", OUTFIT_EDITOR_NAME, "oh no")
+ return
+ if(initial(choice.icon_state) == null) //hacky check copied from experimentor code
+ var/msg = "Warning: This item's icon_state is null, indicating it is very probably not actually a usable item."
+ if(alert(owner, msg, OUTFIT_EDITOR_NAME, "Use it anyway", "Cancel") != "Use it anyway")
+ return
+
+ if(drip.vars.Find(slot))
+ drip.vars[slot] = choice
+
+/datum/outfit_editor/proc/choose_any_item(slot)
+ var/obj/item/choice = pick_closest_path(FALSE)
+
+ if(!choice)
+ return
+
+ set_item(slot, choice)
+
+//this proc will try to give a good selection of items that the user can choose from
+//it does *not* give a selection of all items that can fit in a slot because lag;
+//most notably the hand and pocket slots because they accept pretty much anything
+//also stuff that fits in the belt and back slots are scattered pretty much all over the place
+/datum/outfit_editor/proc/choose_item(slot)
+ var/list/options = list()
+
+ switch(slot)
+ if("head")
+ options = typesof(/obj/item/clothing/head)
+ if("glasses")
+ options = typesof(/obj/item/clothing/glasses)
+ if("ears")
+ options = typesof(/obj/item/radio/headset)
+
+ if("neck")
+ options = typesof(/obj/item/clothing/neck)
+ if("mask")
+ options = typesof(/obj/item/clothing/mask)
+
+ if("uniform")
+ options = typesof(/obj/item/clothing/under)
+ if("suit")
+ options = typesof(/obj/item/clothing/suit)
+ if("gloves")
+ options = typesof(/obj/item/clothing/gloves)
+
+ if("suit_store")
+ var/obj/item/clothing/suit/suit = drip.suit
+ if(suit)
+ suit = new suit //initial() doesn't like lists
+ options = suit.allowed
+ if(!options.len) //nothing will happen, but don't let the user think it's broken
+ to_chat(owner, "No options available for the current suit.")
+
+ if("belt")
+ options = typesof(/obj/item/storage/belt)
+ if("id")
+ options = typesof(/obj/item/card/id)
+
+ if("l_hand")
+ choose_any_item(slot)
+ if("back")
+ options = typesof(/obj/item/storage/backpack)
+ if("r_hand")
+ choose_any_item(slot)
+
+ if("l_pocket")
+ choose_any_item(slot)
+ if("shoes")
+ options = typesof(/obj/item/clothing/shoes)
+ if("r_pocket")
+ choose_any_item(slot)
+
+ if(length(options))
+ set_item(slot, tgui_input_list(owner, "Choose an item", OUTFIT_EDITOR_NAME, options))
+
+
+#undef OUTFIT_EDITOR_NAME
diff --git a/code/modules/admin/outfit_manager.dm b/code/modules/admin/outfit_manager.dm
new file mode 100644
index 0000000000..9d20b64547
--- /dev/null
+++ b/code/modules/admin/outfit_manager.dm
@@ -0,0 +1,73 @@
+/client/proc/outfit_manager()
+ set category = "Debug"
+ set name = "Outfit Manager"
+
+ if(!check_rights(R_DEBUG))
+ return
+ var/datum/outfit_manager/ui = new(usr)
+ ui.ui_interact(usr)
+
+
+/datum/outfit_manager
+ var/client/owner
+
+/datum/outfit_manager/New(user)
+ owner = CLIENT_FROM_VAR(user)
+
+/datum/outfit_manager/ui_state(mob/user)
+ return GLOB.admin_state
+
+/datum/outfit_manager/ui_close(mob/user)
+ qdel(src)
+
+/datum/outfit_manager/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "OutfitManager")
+ ui.open()
+
+/datum/outfit_manager/proc/entry(datum/outfit/outfit)
+ var/vv = FALSE
+ var/datum/outfit/varedit/varoutfit = outfit
+ if(istype(varoutfit))
+ vv = length(varoutfit.vv_values)
+ return list(
+ "name" = "[outfit.name] [vv ? "(VV)" : ""]",
+ "ref" = REF(outfit),
+ )
+
+/datum/outfit_manager/ui_data(mob/user)
+ var/list/data = list()
+
+ var/list/outfits = list()
+ for(var/datum/outfit/custom_outfit in GLOB.custom_outfits)
+ outfits += list(entry(custom_outfit))
+ data["outfits"] = outfits
+
+ return data
+
+/datum/outfit_manager/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ if(..())
+ return
+ . = TRUE
+
+ switch(action)
+ if("new")
+ owner.open_outfit_editor(new /datum/outfit)
+ if("load")
+ owner.holder.load_outfit(owner.mob)
+ if("copy")
+ var/datum/outfit/outfit = tgui_input_list(owner, "Pick an outfit to copy from", "Outfit Manager", subtypesof(/datum/outfit))
+ if(ispath(outfit))
+ owner.open_outfit_editor(new outfit)
+
+ var/datum/outfit/target_outfit = locate(params["outfit"])
+ if(!istype(target_outfit))
+ return
+ switch(action) //wow we're switching through action again this is horrible optimization smh
+ if("edit")
+ owner.open_outfit_editor(target_outfit)
+ if("save")
+ owner.holder.save_outfit(owner.mob, target_outfit)
+ if("delete")
+ owner.holder.delete_outfit(owner.mob, target_outfit)
diff --git a/code/modules/admin/outfits.dm b/code/modules/admin/outfits.dm
new file mode 100644
index 0000000000..1b615e3d62
--- /dev/null
+++ b/code/modules/admin/outfits.dm
@@ -0,0 +1,32 @@
+GLOBAL_LIST_EMPTY(custom_outfits) //Admin created outfits
+
+/datum/admins/proc/save_outfit(mob/admin, datum/outfit/O)
+ O.save_to_file(admin)
+ SStgui.update_user_uis(admin)
+
+/datum/admins/proc/delete_outfit(mob/admin, datum/outfit/O)
+ GLOB.custom_outfits -= O
+ qdel(O)
+ to_chat(admin,"Outfit deleted.")
+ SStgui.update_user_uis(admin)
+
+/datum/admins/proc/load_outfit(mob/admin)
+ var/outfit_file = input("Pick outfit json file:", "File") as null|file
+ if(!outfit_file)
+ return
+ var/filedata = file2text(outfit_file)
+ var/json = json_decode(filedata)
+ if(!json)
+ to_chat(admin,"JSON decode error.")
+ return
+ var/otype = text2path(json["outfit_type"])
+ if(!ispath(otype,/datum/outfit))
+ to_chat(admin,"Malformed/Outdated file.")
+ return
+ var/datum/outfit/O = new otype
+ if(!O.load_from(json))
+ to_chat(admin,"Malformed/Outdated file.")
+ return
+ GLOB.custom_outfits += O
+ SStgui.update_user_uis(admin)
+
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 += "