diff --git a/code/__DEFINES/weapon_stats.dm b/code/__DEFINES/weapon_stats.dm
index 1c3c09e9b28d..7313ebf80562 100644
--- a/code/__DEFINES/weapon_stats.dm
+++ b/code/__DEFINES/weapon_stats.dm
@@ -66,7 +66,7 @@ It DOES NOT control where your bullets go, that's scatter and projectile varianc
////SCATTER////
*/
-#define SCATTER_AMOUNT_NEURO 60
+#define SCATTER_AMOUNT_NEURO 45
#define SCATTER_AMOUNT_TIER_1 15
#define SCATTER_AMOUNT_TIER_2 10
#define SCATTER_AMOUNT_TIER_3 8
diff --git a/code/datums/ammo/bullet/special_ammo.dm b/code/datums/ammo/bullet/special_ammo.dm
index 10eb5abe0e75..ae5d63c79569 100644
--- a/code/datums/ammo/bullet/special_ammo.dm
+++ b/code/datums/ammo/bullet/special_ammo.dm
@@ -20,6 +20,7 @@
name = "armor-piercing smartgun bullet"
icon_state = "bullet"
+ damage_falloff = DAMAGE_FALLOFF_TIER_8
accurate_range = 12
accuracy = HIT_ACCURACY_TIER_2
damage = 20
diff --git a/code/datums/ammo/xeno.dm b/code/datums/ammo/xeno.dm
index a6e5f9206009..1ac1f96f3cae 100644
--- a/code/datums/ammo/xeno.dm
+++ b/code/datums/ammo/xeno.dm
@@ -61,7 +61,7 @@
return
if(ishuman(M))
- M.apply_effect(2.5, SUPERSLOW)
+ M.apply_effect(4, SUPERSLOW)
M.visible_message(SPAN_DANGER("[M]'s movements are slowed."))
var/no_clothes_neuro = FALSE
diff --git a/code/datums/bug_report.dm b/code/datums/bug_report.dm
index fd82d4950b91..4025ce38718f 100644
--- a/code/datums/bug_report.dm
+++ b/code/datums/bug_report.dm
@@ -56,7 +56,7 @@
/datum/tgui_bug_report_form/proc/sanitize_payload(list/params)
for(var/param in params)
- params[param] = sanitize(params[param], list("\t"=" ","�"=" "))
+ params[param] = sanitize(params[param], list("\t"=" ","�"=" ","<"=" ",">"=" ","&"=" "))
return params
diff --git a/code/datums/entities/player_sticky_ban.dm b/code/datums/entities/player_sticky_ban.dm
index 70715d1ce2f0..bfe6342ec774 100644
--- a/code/datums/entities/player_sticky_ban.dm
+++ b/code/datums/entities/player_sticky_ban.dm
@@ -131,3 +131,127 @@ BSQL_PROTECT_DATUM(/datum/entity/stickyban)
"ip",
"linked_stickyban",
)
+
+/// Returns either a list containing the primary CKEYs this alt is connected to,
+/// or FALSE.
+/proc/get_player_is_alt(ckey)
+ var/list/datum/view_record/known_alt/alts = DB_VIEW(/datum/view_record/known_alt, DB_COMP("ckey", DB_EQUALS, ckey))
+ if(!length(alts))
+ return FALSE
+
+ var/ckeys = list()
+ for(var/datum/view_record/known_alt/alt as anything in alts)
+ ckeys += alt.player_ckey
+
+ return ckeys
+
+/client/proc/add_known_alt()
+ set name = "Add Known Alt"
+ set category = "Admin.Alt"
+
+ var/player_ckey = ckey(tgui_input_text(src, "What is the player's primary Ckey?", "Player Ckey"))
+ if(!player_ckey)
+ return
+
+ var/datum/entity/player/player = get_player_from_key(player_ckey)
+ if(!istype(player))
+ return
+
+ var/existing_alts = get_player_is_alt(player_ckey)
+ if(existing_alts)
+ var/confirm = tgui_alert(src, "Primary Ckey [player_ckey] is already an alt for [english_list(existing_alts)].", "Primary Ckey", list("Confirm", "Cancel"))
+
+ if(confirm != "Confirm")
+ return
+
+ var/whitelist_to_add = ckey(tgui_input_text(src, "What is the Ckey that should be added to known alts?", "Alt Ckey"))
+ if(!whitelist_to_add)
+ return
+
+ var/alts_existing_primaries = get_player_is_alt(whitelist_to_add)
+ if(alts_existing_primaries)
+ if(player_ckey in alts_existing_primaries)
+ to_chat(src, SPAN_WARNING("The alt '[whitelist_to_add]' is already set as an alt Ckey for '[player_ckey]'."))
+ return
+
+ var/confirm = tgui_alert(src, "Alt is already an alt for [english_list(alts_existing_primaries)].", "Alt Ckey", list("Confirm", "Cancel"))
+
+ if(confirm != "Confirm")
+ return
+
+ var/datum/entity/known_alt/alt = DB_ENTITY(/datum/entity/known_alt)
+ alt.player_id = player.id
+ alt.player_ckey = player.ckey
+ alt.ckey = whitelist_to_add
+
+ alt.save()
+
+ to_chat(src, SPAN_NOTICE("[alt.ckey] added to the known alts of [player.ckey]."))
+
+/client/proc/remove_known_alt()
+ set name = "Remove Known Alt"
+ set category = "Admin.Alt"
+
+ var/player_ckey = ckey(tgui_input_text(src, "What is the player's primary Ckey?", "Player Ckey"))
+ if(!player_ckey)
+ return
+
+ var/datum/entity/player/player = get_player_from_key(player_ckey)
+ if(!istype(player))
+ return
+
+ var/existing_alts = get_player_is_alt(player_ckey)
+ if(existing_alts)
+ var/confirm = tgui_alert(src, "Primary Ckey [player_ckey] is already an alt for [english_list(existing_alts)].", "Primary Ckey", list("Confirm", "Cancel"))
+
+ if(confirm != "Confirm")
+ return
+
+ var/list/datum/view_record/known_alt/alts = DB_VIEW(/datum/view_record/known_alt, DB_COMP("player_id", DB_EQUALS, player.id))
+ if(!length(alts))
+ to_chat(src, SPAN_WARNING("User has no alts on record."))
+ return
+
+ var/options = list()
+ for(var/datum/view_record/known_alt/alt in alts)
+ options[alt.ckey] = alt.id
+
+ var/picked = tgui_input_list(src, "Which known alt should be removed?", "Alt Removal", options)
+ if(!picked)
+ return
+
+ var/picked_id = options[picked]
+ var/datum/entity/known_alt/to_delete = DB_ENTITY(/datum/entity/known_alt, picked_id)
+ to_delete.delete()
+
+ to_chat(src, SPAN_NOTICE("[picked] removed from the known alts of [player.ckey]."))
+
+/datum/entity/known_alt
+ var/player_id
+ var/player_ckey
+ var/ckey
+
+/datum/entity_meta/known_alt
+ entity_type = /datum/entity/known_alt
+ table_name = "known_alts"
+ field_types = list(
+ "player_id" = DB_FIELDTYPE_BIGINT,
+ "player_ckey" = DB_FIELDTYPE_STRING_LARGE,
+ "ckey" = DB_FIELDTYPE_STRING_LARGE,
+ )
+
+/datum/view_record/known_alt
+ var/id
+ var/player_id
+ var/player_ckey
+ var/ckey
+
+/datum/entity_view_meta/known_alt
+ root_record_type = /datum/entity/known_alt
+ destination_entity = /datum/view_record/known_alt
+ fields = list(
+ "id",
+ "player_id",
+ "player_ckey",
+ "ckey",
+ )
diff --git a/code/game/objects/items/storage/smartpack.dm b/code/game/objects/items/storage/smartpack.dm
index 928388173cc8..afdfbdfa2246 100644
--- a/code/game/objects/items/storage/smartpack.dm
+++ b/code/game/objects/items/storage/smartpack.dm
@@ -176,7 +176,8 @@
update_icon(user)
/obj/item/storage/backpack/marine/smartpack/proc/protective_form(mob/living/carbon/human/user)
- if(!istype(user) || activated_form || immobile_form)
+ if(!istype(user) || activated_form || immobile_form || user.stat == DEAD)
+ to_chat(user, SPAN_WARNING("You cannot use the S-V42 prototype smartpack right now."))
return
if(battery_charge < PROTECTIVE_COST)
@@ -224,7 +225,8 @@
/obj/item/storage/backpack/marine/smartpack/proc/immobile_form(mob/living/user)
- if(activated_form)
+ if(activated_form || user.stat == DEAD)
+ to_chat(user, SPAN_WARNING("You cannot use the S-V42 prototype smartpack right now."))
return
if(battery_charge < IMMOBILE_COST && !immobile_form)
@@ -263,7 +265,8 @@
/obj/item/storage/backpack/marine/smartpack/proc/repair_form(mob/user)
- if(!ishuman(user) || activated_form || repairing)
+ if(!ishuman(user) || activated_form || repairing || user.stat == DEAD)
+ to_chat(user, SPAN_WARNING("You cannot use the S-V42 prototype smartpack right now."))
return
if(battery_charge < REPAIR_COST)
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 5f5135bd0905..9e7482714a3d 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -74,6 +74,8 @@ GLOBAL_LIST_INIT(admin_verbs_default, list(
/client/proc/cmd_admin_tacmaps_panel,
/client/proc/other_records,
/client/proc/toggle_admin_afk_safety,
+ /client/proc/add_known_alt,
+ /client/proc/remove_known_alt,
/client/proc/toogle_door_control,
))
diff --git a/code/modules/client/color_picker.dm b/code/modules/client/color_picker.dm
new file mode 100644
index 000000000000..f3eda08af544
--- /dev/null
+++ b/code/modules/client/color_picker.dm
@@ -0,0 +1,74 @@
+/datum/body_picker/ui_static_data(mob/user)
+ . = ..()
+
+ .["icon"] = /datum/species::icobase
+
+ .["body_types"] = list()
+ for(var/key in GLOB.body_type_list)
+ var/datum/body_type/type = GLOB.body_type_list[key]
+ .["body_types"] += list(
+ list("name" = type.name, "icon" = type.icon_name)
+ )
+
+ .["skin_colors"] = list()
+ for(var/key in GLOB.skin_color_list)
+ var/datum/skin_color/color = GLOB.skin_color_list[key]
+ .["skin_colors"] += list(
+ list("name" = color.name, "icon" = color.icon_name, "color" = color.color)
+ )
+
+ .["body_sizes"] = list()
+ for(var/key in GLOB.body_size_list)
+ var/datum/body_size/size = GLOB.body_size_list[key]
+ .["body_sizes"] += list(
+ list("name" = size.name, "icon" = size.icon_name)
+ )
+
+/datum/body_picker/ui_data(mob/user)
+ . = ..()
+
+ .["body_type"] = GLOB.body_type_list[user.client.prefs.body_type].icon_name
+ .["skin_color"] = GLOB.skin_color_list[user.client.prefs.skin_color].icon_name
+ .["body_size"] = GLOB.body_size_list[user.client.prefs.body_size].icon_name
+
+/datum/body_picker/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+
+ var/datum/preferences/prefs = ui.user.client.prefs
+
+ switch(action)
+ if("type")
+ if(!GLOB.body_type_list[params["name"]])
+ return
+
+ prefs.body_type = params["name"]
+
+ if("size")
+ if(!GLOB.body_size_list[params["name"]])
+ return
+
+ prefs.body_size = params["name"]
+
+ if("color")
+ if(!GLOB.skin_color_list[params["name"]])
+ return
+
+ prefs.skin_color = params["name"]
+
+ prefs.ShowChoices(ui.user)
+ return TRUE
+
+/datum/body_picker/tgui_interact(mob/user, datum/tgui/ui)
+ . = ..()
+
+ ui = SStgui.try_update_ui(user, src, ui)
+
+ if(!ui)
+ ui = new(user, src, "BodyPicker", "Body Picker")
+ ui.open()
+ ui.set_autoupdate(FALSE)
+
+ winset(user, ui.window.id, "focus=true")
+
+/datum/body_picker/ui_state(mob/user)
+ return GLOB.always_state
diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm
index 49c766c33b78..f2be7a90b5f4 100644
--- a/code/modules/client/preferences.dm
+++ b/code/modules/client/preferences.dm
@@ -30,6 +30,8 @@ GLOBAL_LIST_INIT(bgstate_options, list(
var/atom/movable/screen/rotate/alt/rotate_left
var/atom/movable/screen/rotate/rotate_right
+ var/static/datum/body_picker/picker = new
+
//doohickeys for savefiles
var/path
var/default_slot = 1 //Holder so it doesn't default to slot 1, rather the last one used
@@ -341,10 +343,13 @@ GLOBAL_LIST_INIT(bgstate_options, list(
dat += "
Physical Information:"
dat += "®
"
dat += "Age: [age]
"
- dat += "Gender: [gender == MALE ? "Male" : "Female"]
"
- dat += "Skin Color: [skin_color]
"
- dat += "Body Size: [body_size]
"
- dat += "Body Muscularity: [body_type]
"
+ dat += "Gender: [gender == MALE ? "Male" : "Female"]
"
+
+ dat += "Skin Color: [skin_color]
"
+ dat += "Body Size: [body_size]
"
+ dat += "Body Muscularity: [body_type]
"
+ dat += "Edit Body: Picker
"
+
dat += "Traits: Character Traits"
dat += "
"
@@ -1579,23 +1584,9 @@ GLOBAL_LIST_INIT(bgstate_options, list(
if(new_h_gradient_style)
grad_style = new_h_gradient_style
- if ("skin_color")
- var/new_skin_color = tgui_input_list(user, "Choose your character's skin color:", "Character Preferences", GLOB.skin_color_list)
-
- if (new_skin_color)
- skin_color = new_skin_color
-
- if ("body_size")
- var/new_body_size = tgui_input_list(user, "Choose your character's body size:", "Character Preferences", GLOB.body_size_list)
-
- if (new_body_size)
- body_size = new_body_size
-
- if ("body_type")
- var/new_body_type = tgui_input_list(user, "Choose your character's body type:", "Character Preferences", GLOB.body_type_list)
-
- if (new_body_type)
- body_type = new_body_type
+ if ("body")
+ picker.tgui_interact(user)
+ return
if("facial")
var/new_facial = input(user, "Choose your character's facial-hair color:", "Character Preference", rgb(r_facial, g_facial, b_facial)) as color|null
diff --git a/code/modules/clothing/under/ties.dm b/code/modules/clothing/under/ties.dm
index c98e254cef2c..f8b4bf55c461 100644
--- a/code/modules/clothing/under/ties.dm
+++ b/code/modules/clothing/under/ties.dm
@@ -106,56 +106,60 @@
icon_state = "stethoscope"
/obj/item/clothing/accessory/stethoscope/attack(mob/living/carbon/human/being, mob/living/user)
- if(ishuman(being) && isliving(user))
- if(user.a_intent == INTENT_HELP)
- var/body_part = parse_zone(user.zone_selected)
- if(body_part)
- var/sound = null
- if(being.stat == DEAD || (being.status_flags&FAKEDEATH))
- sound = "can't hear anything at all, they must have kicked the bucket"
- else
- switch(body_part)
- if("chest")
- if(skillcheck(user, SKILL_MEDICAL, SKILL_MEDICAL_MEDIC)) // only medical personnel can take advantage of it
- if(!ishuman(being))
- return // not a human; only humans have the variable internal_organs_by_name // "cast" it a human type since we confirmed it is one
- if(isnull(being.internal_organs_by_name))
- return // they have no organs somehow
- var/datum/internal_organ/heart/heart = being.internal_organs_by_name["heart"]
- if(heart)
- switch(heart.organ_status)
- if(ORGAN_LITTLE_BRUISED)
- sound = "hear small murmurs with each heart beat, it is possible that [being.p_their()] heart is subtly damaged"
- if(ORGAN_BRUISED)
- sound = "hear deviant heart beating patterns, result of probable heart damage"
- if(ORGAN_BROKEN)
- sound = "hear irregular and additional heart beating patterns, probably caused by impaired blood pumping, [being.p_their()] heart is certainly failing"
- else
- sound = "hear normal heart beating patterns, [being.p_their()] heart is surely healthy"
- var/datum/internal_organ/lungs/lungs = being.internal_organs_by_name["lungs"]
- if(lungs)
- if(sound)
- sound += ". You also "
- switch(lungs.organ_status)
- if(ORGAN_LITTLE_BRUISED)
- sound += "hear some crackles when [being.p_they()] breath, [being.p_they()] is possibly suffering from a small damage to the lungs"
- if(ORGAN_BRUISED)
- sound += "hear unusual respiration sounds and noticeable difficulty to breath, possibly signalling ruptured lungs"
- if(ORGAN_BROKEN)
- sound += "barely hear any respiration sounds and a lot of difficulty to breath, [being.p_their()] lungs are heavily failing"
- else
- sound += "hear normal respiration sounds aswell, that means [being.p_their()] lungs are healthy, probably"
- else
- sound = "can't hear. Really, anything at all, how weird"
- else
- sound = "hear a lot of sounds... it's quite hard to distinguish, really"
- if("eyes","mouth")
- sound = "can't hear anything. Maybe that isn't the smartest idea"
+ if(!ishuman(being) || !isliving(user))
+ return
+
+ var/body_part = parse_zone(user.zone_selected)
+ if(!body_part)
+ return
+
+ var/sound = null
+ if(being.stat == DEAD || (being.status_flags & FAKEDEATH))
+ sound = "can't hear anything at all, they must have kicked the bucket"
+ user.visible_message("[user] places [src] against [being]'s [body_part] and listens attentively.", "You place [src] against [being.p_their()] [body_part] and... you [sound].")
+ return
+
+ switch(body_part)
+ if("chest")
+ if(skillcheck(user, SKILL_MEDICAL, SKILL_MEDICAL_MEDIC)) // only medical personnel can take advantage of it
+ if(!ishuman(being))
+ return // not a human; only humans have the variable internal_organs_by_name // "cast" it a human type since we confirmed it is one
+ if(isnull(being.internal_organs_by_name))
+ return // they have no organs somehow
+ var/datum/internal_organ/heart/heart = being.internal_organs_by_name["heart"]
+ if(heart)
+ switch(heart.organ_status)
+ if(ORGAN_LITTLE_BRUISED)
+ sound = "hear small murmurs with each heart beat, it is possible that [being.p_their()] heart is subtly damaged"
+ if(ORGAN_BRUISED)
+ sound = "hear deviant heart beating patterns, result of probable heart damage"
+ if(ORGAN_BROKEN)
+ sound = "hear irregular and additional heart beating patterns, probably caused by impaired blood pumping, [being.p_their()] heart is certainly failing"
else
- sound = "hear a sound here and there, but none of them give you any good information"
- user.visible_message("[user] places [src] against [being]'s [body_part] and listens attentively.", "You place [src] against [being.p_their()] [body_part] and... you [sound].")
- return
- return ..(being,user)
+ sound = "hear normal heart beating patterns, [being.p_their()] heart is surely healthy"
+ var/datum/internal_organ/lungs/lungs = being.internal_organs_by_name["lungs"]
+ if(lungs)
+ if(sound)
+ sound += ". You also "
+ switch(lungs.organ_status)
+ if(ORGAN_LITTLE_BRUISED)
+ sound += "hear some crackles when [being.p_they()] breath, [being.p_they()] is possibly suffering from a small damage to the lungs"
+ if(ORGAN_BRUISED)
+ sound += "hear unusual respiration sounds and noticeable difficulty to breath, possibly signalling ruptured lungs"
+ if(ORGAN_BROKEN)
+ sound += "barely hear any respiration sounds and a lot of difficulty to breath, [being.p_their()] lungs are heavily failing"
+ else
+ sound += "hear normal respiration sounds aswell, that means [being.p_their()] lungs are healthy, probably"
+ else
+ sound = "can't hear. Really, anything at all, how weird"
+ else
+ sound = "hear a lot of sounds... it's quite hard to distinguish, really"
+ if("eyes","mouth")
+ sound = "can't hear anything. Maybe that isn't the smartest idea"
+ else
+ sound = "hear a sound here and there, but none of them give you any good information"
+ user.visible_message("[user] places [src] against [being]'s [body_part] and listens attentively.", "You place [src] against [being.p_their()] [body_part] and... you [sound].")
+
//Medals
/obj/item/clothing/accessory/medal
diff --git a/code/modules/defenses/sentry.dm b/code/modules/defenses/sentry.dm
index 6c9c5ad68fea..02bccde9fa37 100644
--- a/code/modules/defenses/sentry.dm
+++ b/code/modules/defenses/sentry.dm
@@ -313,7 +313,7 @@
new_projectile.damage *= damage_mult
new_projectile.accuracy *= accuracy_mult
GIVE_BULLET_TRAIT(new_projectile, /datum/element/bullet_trait_iff, faction_group)
- new_projectile.fire_at(target, src, owner_mob, new_projectile.ammo.max_range, new_projectile.ammo.shell_speed, null, FALSE)
+ new_projectile.fire_at(target, owner_mob, src, new_projectile.ammo.max_range, new_projectile.ammo.shell_speed, null, FALSE)
muzzle_flash(Get_Angle(get_turf(src), target))
ammo.current_rounds--
track_shot()
diff --git a/code/modules/defenses/sentry_flamer.dm b/code/modules/defenses/sentry_flamer.dm
index 2c5e9ae62677..fcdd54708609 100644
--- a/code/modules/defenses/sentry_flamer.dm
+++ b/code/modules/defenses/sentry_flamer.dm
@@ -28,11 +28,11 @@
accuracy_mult = 0.1
fire_delay = 0.5
-/obj/structure/machinery/defenses/sentry/flamer/actual_fire(atom/A)
- var/obj/projectile/P = new(create_cause_data(initial(name), owner_mob))
- P.generate_bullet(new ammo.default_ammo)
- GIVE_BULLET_TRAIT(P, /datum/element/bullet_trait_iff, faction_group)
- P.fire_at(A, src, owner_mob, P.ammo.max_range, P.ammo.shell_speed, null)
+/obj/structure/machinery/defenses/sentry/flamer/actual_fire(atom/target)
+ var/obj/projectile/new_projectile = new(src, create_cause_data(initial(name), owner_mob, src))
+ new_projectile.generate_bullet(new ammo.default_ammo)
+ GIVE_BULLET_TRAIT(new_projectile, /datum/element/bullet_trait_iff, faction_group)
+ new_projectile.fire_at(target, owner_mob, src, new_projectile.ammo.max_range, new_projectile.ammo.shell_speed, null, FALSE)
ammo.current_rounds--
track_shot()
if(ammo.current_rounds == 0)
diff --git a/code/modules/mob/living/carbon/human/examine.dm b/code/modules/mob/living/carbon/human/examine.dm
index c8820ec3b97d..fb7a12342978 100644
--- a/code/modules/mob/living/carbon/human/examine.dm
+++ b/code/modules/mob/living/carbon/human/examine.dm
@@ -497,6 +497,12 @@
if(assigned_squad == H.assigned_squad) //same squad
msg += "\[Manage Fireteams.\]\n"
+ if(user.Adjacent(src) && ishuman(user))
+ var/mob/living/carbon/human/human_user = user
+ var/temp_msg = "\[Check Status\]"
+ if(skillcheck(user, SKILL_MEDICAL, SKILL_MEDICAL_MEDIC) && locate(/obj/item/clothing/accessory/stethoscope) in human_user.w_uniform)
+ temp_msg += " \[Use Stethoscope\]"
+ msg += "\nMedical actions: [temp_msg]\n"
if(print_flavor_text())
msg += "[print_flavor_text()]\n"
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index a7c082df6b46..88d7305bc0a3 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -772,6 +772,21 @@
if(R.fields["last_scan_time"] && R.fields["last_scan_result"])
tgui_interact(usr)
break
+
+ if(href_list["check_status"])
+ if(!usr.Adjacent(src))
+ return
+ var/mob/living/carbon/human/user = usr
+ user.check_status(src)
+
+ if(href_list["use_stethoscope"])
+ var/mob/living/carbon/human/user = usr
+ var/obj/item/clothing/accessory/stethoscope/stethoscope = locate() in user.w_uniform
+ if(!stethoscope || !user.Adjacent(src))
+ return
+
+ stethoscope.attack(src, user)
+
..()
return
@@ -990,45 +1005,44 @@
if(prob(30)) // Spam chat less
to_chat(src, SPAN_HIGHDANGER("Your movement jostles [W] in your [organ.display_name] painfully."))
-/mob/living/carbon/human/verb/check_status()
- set category = "Object"
- set name = "Check Status"
- set src in view(1)
- var/self = (usr == src)
- var/msg = ""
-
-
- if(usr.stat > 0 || usr.is_mob_restrained() || !ishuman(usr)) return
+/mob/living/carbon/human/proc/check_status(mob/living/carbon/human/target)
+ if(is_dead() || is_mob_restrained())
+ return
+ ///Final message detailing injuries on the target.
+ var/msg
+ ///Is the target the user or somebody else?
+ var/self = (target == src)
+ to_chat(usr,SPAN_NOTICE("You [self ? "take a moment to analyze yourself." : "start analyzing [src]."]"))
if(self)
- var/list/L = get_broken_limbs() - list("chest","head","groin")
- if(length(L) > 0)
- msg += "Your [english_list(L)] [length(L) > 1 ? "are" : "is"] broken\n"
- to_chat(usr,SPAN_NOTICE("You [self ? "take a moment to analyze yourself":"start analyzing [src]"]"))
- if(toxloss > 20)
- msg += "[self ? "Your" : "Their"] skin is slightly green\n"
- if(is_bleeding())
- msg += "[self ? "You" : "They"] have bleeding wounds on [self ? "your" : "their"] body\n"
+ var/list/broken_limbs = target.get_broken_limbs() - list("chest","head","groin")
+ if(length(broken_limbs))
+ msg += "Your [english_list(broken_limbs)] [length(broken_limbs) > 1 ? "are" : "is"] broken.\n"
+ if(target.toxloss > 20)
+ msg += "[self ? "Your" : "Their"] skin is slightly green.\n"
+
+ if(target.is_bleeding())
+ msg += "[self ? "You" : "They"] have bleeding wounds on [self ? "your" : "their"] body.\n"
if(!self && skillcheck(usr, SKILL_SURGERY, SKILL_SURGERY_NOVICE))
- for(var/datum/effects/bleeding/internal/internal_bleed in effects_list)
- msg += "They have bloating and discoloration on their [internal_bleed.limb.display_name]\n"
-
- if(stat == UNCONSCIOUS)
- msg += "They seem to be unconscious\n"
- else if(stat == DEAD)
- if(src.check_tod() && is_revivable())
- msg += "They're not breathing"
- else
- if(has_limb("head"))
- msg += "Their eyes have gone blank, there are no signs of life"
- else
- msg += "They are definitely dead"
- else
- msg += "[self ? "You're":"They're"] alive and breathing"
+ for(var/datum/effects/bleeding/internal/internal_bleed in target.effects_list)
+ msg += "They have bloating and discoloration on their [internal_bleed.limb.display_name].\n"
+ switch(target.stat)
+ if(DEAD)
+ if(target.check_tod() && target.is_revivable())
+ msg += "They're not breathing."
+ else
+ if(has_limb("head"))
+ msg += "Their eyes have gone blank, there are no signs of life."
+ else
+ msg += "They are definitely dead."
+ if(UNCONSCIOUS)
+ msg += "They seem to be unconscious.\n"
+ if(CONSCIOUS)
+ msg += "[self ? "You're" : "They're"] alive and breathing."
- to_chat(usr,SPAN_WARNING(msg))
+ to_chat(src, SPAN_WARNING(msg))
/mob/living/carbon/human/verb/view_manifest()
diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/general_powers.dm b/code/modules/mob/living/carbon/xenomorph/abilities/general_powers.dm
index 6e710ecf2d53..7c6a99499d1e 100644
--- a/code/modules/mob/living/carbon/xenomorph/abilities/general_powers.dm
+++ b/code/modules/mob/living/carbon/xenomorph/abilities/general_powers.dm
@@ -241,7 +241,7 @@
switch(X.build_resin(A, thick, make_message, plasma_cost != 0, build_speed_mod))
if(SECRETE_RESIN_INTERRUPT)
if(xeno_cooldown)
- apply_cooldown_override(xeno_cooldown * 2)
+ apply_cooldown_override(xeno_cooldown * 3)
return FALSE
if(SECRETE_RESIN_FAIL)
if(xeno_cooldown)
diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/queen/queen_abilities.dm b/code/modules/mob/living/carbon/xenomorph/abilities/queen/queen_abilities.dm
index ebcd29ded29d..b93d6d848b44 100644
--- a/code/modules/mob/living/carbon/xenomorph/abilities/queen/queen_abilities.dm
+++ b/code/modules/mob/living/carbon/xenomorph/abilities/queen/queen_abilities.dm
@@ -50,11 +50,11 @@
action_icon_state = "secrete_resin"
ability_name = "projected resin"
plasma_cost = 100
- xeno_cooldown = 2 SECONDS
+ xeno_cooldown = 4 SECONDS
ability_primacy = XENO_PRIMARY_ACTION_5
care_about_adjacency = FALSE
- build_speed_mod = 1
+ build_speed_mod = 1.5
var/boosted = FALSE
@@ -75,11 +75,12 @@
boosted = TRUE
xeno_cooldown = 0
plasma_cost = 0
+ build_speed_mod = 1
RegisterSignal(owner, COMSIG_XENO_THICK_RESIN_BYPASS, PROC_REF(override_secrete_thick_resin))
addtimer(CALLBACK(src, PROC_REF(disable_boost)), boost_duration)
/datum/action/xeno_action/activable/secrete_resin/remote/queen/proc/disable_boost()
- xeno_cooldown = 2 SECONDS
+ xeno_cooldown = 4 SECONDS
plasma_cost = 100
boosted = FALSE
UnregisterSignal(owner, COMSIG_XENO_THICK_RESIN_BYPASS)
diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/sentinel/sentinel_abilities.dm b/code/modules/mob/living/carbon/xenomorph/abilities/sentinel/sentinel_abilities.dm
index fdd1164c1479..0aaf6f1270c0 100644
--- a/code/modules/mob/living/carbon/xenomorph/abilities/sentinel/sentinel_abilities.dm
+++ b/code/modules/mob/living/carbon/xenomorph/abilities/sentinel/sentinel_abilities.dm
@@ -6,7 +6,7 @@
macro_path = /datum/action/xeno_action/verb/verb_slowing_spit
action_type = XENO_ACTION_CLICK
ability_primacy = XENO_PRIMARY_ACTION_1
- xeno_cooldown = 1.5 SECONDS
+ xeno_cooldown = 2 SECONDS
plasma_cost = 20
// Scatterspit
@@ -17,7 +17,7 @@
macro_path = /datum/action/xeno_action/verb/verb_scattered_spit
action_type = XENO_ACTION_CLICK
ability_primacy = XENO_PRIMARY_ACTION_2
- xeno_cooldown = 8 SECONDS
+ xeno_cooldown = 6 SECONDS
plasma_cost = 30
// Paralyzing slash
diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Sentinel.dm b/code/modules/mob/living/carbon/xenomorph/castes/Sentinel.dm
index b39cbbb15e7b..3693b2174f12 100644
--- a/code/modules/mob/living/carbon/xenomorph/castes/Sentinel.dm
+++ b/code/modules/mob/living/carbon/xenomorph/castes/Sentinel.dm
@@ -68,7 +68,7 @@
// State
var/next_slash_buffed = FALSE
-#define NEURO_TOUCH_DELAY 4 SECONDS
+#define NEURO_TOUCH_DELAY 3 SECONDS
/datum/behavior_delegate/sentinel_base/melee_attack_modify_damage(original_damage, mob/living/carbon/carbon_target)
if (!next_slash_buffed)
@@ -112,6 +112,6 @@
return INTENT_HARM
/datum/behavior_delegate/sentinel_base/proc/paralyzing_slash(mob/living/carbon/human/human_target)
- human_target.KnockDown(2)
- human_target.Stun(2)
+ human_target.KnockDown(2.5)
+ human_target.Stun(2.5)
to_chat(human_target, SPAN_XENOHIGHDANGER("You fall over, paralyzed by the toxin!"))
diff --git a/code/modules/mob/living/carbon/xenomorph/strains/castes/hivelord/resin_whisperer.dm b/code/modules/mob/living/carbon/xenomorph/strains/castes/hivelord/resin_whisperer.dm
index 2d3429df4050..11d9e7312b55 100644
--- a/code/modules/mob/living/carbon/xenomorph/strains/castes/hivelord/resin_whisperer.dm
+++ b/code/modules/mob/living/carbon/xenomorph/strains/castes/hivelord/resin_whisperer.dm
@@ -44,13 +44,13 @@
name = "Coerce Resin (100)"
action_icon_state = "secrete_resin"
ability_name = "coerce resin"
- xeno_cooldown = 1 SECONDS
+ xeno_cooldown = 2.5 SECONDS
thick = FALSE
make_message = FALSE
no_cooldown_msg = TRUE
- build_speed_mod = 2 // the actual building part takes twice as long
+ build_speed_mod = 2.5 // the actual building part takes twice as long
macro_path = /datum/action/xeno_action/verb/verb_coerce_resin
action_type = XENO_ACTION_CLICK
@@ -91,8 +91,10 @@
if(care_about_adjacency)
if(owner.Adjacent(target_turf))
build_speed_mod = 1
+ xeno_cooldown = 1 SECONDS
else
build_speed_mod = initial(build_speed_mod)
+ xeno_cooldown = initial(xeno_cooldown)
var/mob/living/carbon/xenomorph/hivelord = owner
if(!..())
diff --git a/code/modules/mob/new_player/skin_color.dm b/code/modules/mob/new_player/skin_color.dm
index f3158613c38c..166cb22eb302 100644
--- a/code/modules/mob/new_player/skin_color.dm
+++ b/code/modules/mob/new_player/skin_color.dm
@@ -2,6 +2,14 @@
var/name
var/icon_name
+ var/color
+
+/datum/skin_color/New()
+ . = ..()
+
+ var/icon/icon_to_use = icon(/datum/species::icobase, "[icon_name]_torso_[/datum/body_size/thin::icon_name]_[/datum/body_type/twig::icon_name]")
+ color = icon_to_use.GetPixel(icon_to_use.Width() / 2, icon_to_use.Height() / 2)
+
/datum/skin_color/cmplayer
name = "Extra Pale"
icon_name = "cmp1"
diff --git a/code/modules/projectiles/gun_attachables.dm b/code/modules/projectiles/gun_attachables.dm
index dc97d3452b86..2847021c4775 100644
--- a/code/modules/projectiles/gun_attachables.dm
+++ b/code/modules/projectiles/gun_attachables.dm
@@ -2996,9 +2996,16 @@ Defined in conflicts.dm of the #defines folder.
to_chat(user, SPAN_WARNING("[src] is full."))
return
- var/datum/reagent/to_remove
- if(length(fuel_holder.reagents.reagent_list))
- to_remove = fuel_holder.reagents.reagent_list[1]
+ if(!fuel_holder.reagents || length(fuel_holder.reagents.reagent_list) < 1)
+ to_chat(user, SPAN_WARNING("[fuel_holder] is empty!"))
+ return
+
+ var/datum/reagent/to_remove = fuel_holder.reagents.reagent_list[1]
+
+ var/flamer_chem = "utnapthal"
+ if(!istype(to_remove) || flamer_chem != to_remove.id || length(fuel_holder.reagents.reagent_list) > 1)
+ to_chat(user, SPAN_WARNING("You can't mix fuel mixtures!"))
+ return
var/fuel_amt
if(to_remove)
diff --git a/code/modules/tgui_panel/telemetry.dm b/code/modules/tgui_panel/telemetry.dm
index bd49596aa19a..951a82736b58 100644
--- a/code/modules/tgui_panel/telemetry.dm
+++ b/code/modules/tgui_panel/telemetry.dm
@@ -65,16 +65,9 @@
if (!ckey)
return
-/*
- var/list/all_known_alts = GLOB.known_alts.load_known_alts()
- var/list/our_known_alts = list()
-
- for (var/known_alt in all_known_alts)
- if (known_alt[1] == ckey)
- our_known_alts += known_alt[2]
- else if (known_alt[2] == ckey)
- our_known_alts += known_alt[1]
-*/
+ var/list/known_alts = list()
+ for(var/datum/view_record/known_alt/alts in DB_VIEW(/datum/view_record/known_alt, DB_COMP("player_ckey", DB_EQUALS, ckey)))
+ known_alts += alts.ckey
var/list/found
@@ -98,10 +91,11 @@
"address" = row["address"],
"computer_id" = row["computer_id"],
))
+ */
- if (row["ckey"] in our_known_alts)
+ if (row["ckey"] in known_alts)
continue
- */
+
if (world.IsBanned(row["ckey"], row["address"], row["computer_id"], real_bans_only = TRUE, is_telemetry = TRUE))
found = row
diff --git a/colonialmarines.dme b/colonialmarines.dme
index 41d12a5b184d..cf374e546262 100644
--- a/colonialmarines.dme
+++ b/colonialmarines.dme
@@ -1561,6 +1561,7 @@
#include "code\modules\clans\ship.dm"
#include "code\modules\client\client_defines.dm"
#include "code\modules\client\client_procs.dm"
+#include "code\modules\client\color_picker.dm"
#include "code\modules\client\country_flags.dm"
#include "code\modules\client\player_details.dm"
#include "code\modules\client\preferences.dm"
diff --git a/html/changelogs/AutoChangeLog-pr-7336.yml b/html/changelogs/AutoChangeLog-pr-7336.yml
new file mode 100644
index 000000000000..681f96f35f03
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-7336.yml
@@ -0,0 +1,4 @@
+author: "harryob"
+delete-after: True
+changes:
+ - admin: "you can add \"known alts\" to players, now"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-7423.yml b/html/changelogs/AutoChangeLog-pr-7423.yml
deleted file mode 100644
index 0b0c3349f563..000000000000
--- a/html/changelogs/AutoChangeLog-pr-7423.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Sporticusmge"
-delete-after: True
-changes:
- - balance: "Night Vision Optic now works 2 times longer, and recharge 2 times longer. Squad Leader NVO prices changed, from 20 to 25."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-7426.yml b/html/changelogs/AutoChangeLog-pr-7426.yml
deleted file mode 100644
index 47b9ab544abc..000000000000
--- a/html/changelogs/AutoChangeLog-pr-7426.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "deathrobotpunch"
-delete-after: True
-changes:
- - balance: "The Tactical compact nail gun is now available in the Chief Engineers vendor."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-7428.yml b/html/changelogs/AutoChangeLog-pr-7428.yml
deleted file mode 100644
index d2038d3e452e..000000000000
--- a/html/changelogs/AutoChangeLog-pr-7428.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-author: "Red-byte3D"
-delete-after: True
-changes:
- - balance: "sg effective range nerfed to 5"
- - balance: "sg falloff increased"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-7433.yml b/html/changelogs/AutoChangeLog-pr-7433.yml
deleted file mode 100644
index 80451ecdb9d8..000000000000
--- a/html/changelogs/AutoChangeLog-pr-7433.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "private-tristan"
-delete-after: True
-changes:
- - balance: "Forecon M39 spawn now has its spawn ammo in belt, instead of in satchel."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-7438.yml b/html/changelogs/AutoChangeLog-pr-7438.yml
deleted file mode 100644
index 7f6d7f79b717..000000000000
--- a/html/changelogs/AutoChangeLog-pr-7438.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-author: "Drathek"
-delete-after: True
-changes:
- - ui: "The ghost observer tacmap will now scroll both horizontally and vertically as needed to display a large map."
- - ui: "The TGUI panels for tacmaps and drawings have been polished to utilize more minimap space fully."
- - refactor: "Refactored minimap generation code to calculate min and max faster, to more aggressively trim minimaps, and to support larger minimaps (was 480px now 512px)."
- - bugfix: "Fixes an issue where predator tacmap would take over the ghost tacmap preventing it from opening for observers"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-7451.yml b/html/changelogs/AutoChangeLog-pr-7451.yml
deleted file mode 100644
index 3c3ac036d300..000000000000
--- a/html/changelogs/AutoChangeLog-pr-7451.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Drathek"
-delete-after: True
-changes:
- - bugfix: "Fix hive core placement restriction"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-7455.yml b/html/changelogs/AutoChangeLog-pr-7455.yml
deleted file mode 100644
index d65e2c461ce1..000000000000
--- a/html/changelogs/AutoChangeLog-pr-7455.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Zenith00000"
-delete-after: True
-changes:
- - bugfix: "fixed some un-needed code and added missing tiles"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-7463.yml b/html/changelogs/AutoChangeLog-pr-7463.yml
new file mode 100644
index 000000000000..09e6bb6ac3a2
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-7463.yml
@@ -0,0 +1,4 @@
+author: "VileBeggar"
+delete-after: True
+changes:
+ - qol: "You can now use the 'check status' verb and any stethoscope you might have attached to your uniform directly by examining an adjacent human."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-7470.yml b/html/changelogs/AutoChangeLog-pr-7470.yml
new file mode 100644
index 000000000000..92856ca59c25
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-7470.yml
@@ -0,0 +1,4 @@
+author: "vero5123"
+delete-after: True
+changes:
+ - bugfix: "Special characters are now properly sanitized and removed in bug reports."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-7471.yml b/html/changelogs/AutoChangeLog-pr-7471.yml
new file mode 100644
index 000000000000..763cc85fbcfc
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-7471.yml
@@ -0,0 +1,5 @@
+author: "Drathek"
+delete-after: True
+changes:
+ - bugfix: "Fixes additional projectiles being fired from humans that placed a shotgun sentry instead of the sentry."
+ - bugfix: "Fixes cause_data for flamer sentries so now kills they make are explained in the message and attributed to the kill counter for the sentry."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-7472.yml b/html/changelogs/AutoChangeLog-pr-7472.yml
new file mode 100644
index 000000000000..8671ee9ea13b
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-7472.yml
@@ -0,0 +1,4 @@
+author: "Drathek"
+delete-after: True
+changes:
+ - bugfix: "Fix reloading under barrel flamers with any reagent from a flamer tank"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-7473.yml b/html/changelogs/AutoChangeLog-pr-7473.yml
new file mode 100644
index 000000000000..6913f3a83aa0
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-7473.yml
@@ -0,0 +1,4 @@
+author: "Drathek"
+delete-after: True
+changes:
+ - balance: "Buffed SG armor penetration ammo damage falloff from 5 to 3."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-7474.yml b/html/changelogs/AutoChangeLog-pr-7474.yml
new file mode 100644
index 000000000000..6df2dd6c584d
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-7474.yml
@@ -0,0 +1,4 @@
+author: "Drathek"
+delete-after: True
+changes:
+ - ui: "Fixed the ping relay buttons not displaying anything when clicked on the first time. Now Button.Confirm.confirmContent will only replace children if it is truthy."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-7475.yml b/html/changelogs/AutoChangeLog-pr-7475.yml
new file mode 100644
index 000000000000..44bf126e92de
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-7475.yml
@@ -0,0 +1,4 @@
+author: "harryob"
+delete-after: True
+changes:
+ - rscadd: "a new tgui with better previews for the different skin colors / body types / body sizes"
\ No newline at end of file
diff --git a/html/changelogs/archive/2024-11.yml b/html/changelogs/archive/2024-11.yml
index 89c4cfdf53ac..71293c36ac00 100644
--- a/html/changelogs/archive/2024-11.yml
+++ b/html/changelogs/archive/2024-11.yml
@@ -40,3 +40,43 @@
with "COBALT_BASE_API" and "COBALT_API_KEY"
realforest2001:
- bugfix: No store cryo pods now work as intended.
+2024-11-02:
+ Drathek:
+ - ui: The ghost observer tacmap will now scroll both horizontally and vertically
+ as needed to display a large map.
+ - ui: The TGUI panels for tacmaps and drawings have been polished to utilize more
+ minimap space fully.
+ - refactor: Refactored minimap generation code to calculate min and max faster,
+ to more aggressively trim minimaps, and to support larger minimaps (was 480px
+ now 512px).
+ - bugfix: Fixes an issue where predator tacmap would take over the ghost tacmap
+ preventing it from opening for observers
+ - bugfix: Fix hive core placement restriction
+ Red-byte3D:
+ - balance: sg effective range nerfed to 5
+ - balance: sg falloff increased
+ Sporticusmge:
+ - balance: Night Vision Optic now works 2 times longer, and recharge 2 times longer.
+ Squad Leader NVO prices changed, from 20 to 25.
+ Zenith00000:
+ - bugfix: fixed some un-needed code and added missing tiles
+ deathrobotpunch:
+ - balance: The Tactical compact nail gun is now available in the Chief Engineers
+ vendor.
+ private-tristan:
+ - balance: Forecon M39 spawn now has its spawn ammo in belt, instead of in satchel.
+2024-11-03:
+ Ansekishoku:
+ - bugfix: Smartpack abilities no longer function if the user is dead.
+ GoldenDarkness55:
+ - balance: Cooldown multiplier for interrupting remote building raised from 2 to
+ 3
+ - balance: Queen remote building modifier raised from 1 to 1.5 (slower, around 2.3-2.5s),
+ cooldown raised from 2s to 4s
+ - balance: Resin Whisperer remote building modifier raised from 2 to 2.5 (slower,
+ around 2.5s), cooldown raised from 1s to 2.5s
+ - balance: Sentinel neuro spit cooldown raised 1.5 to 2s, superslow 2.5 to 4s
+ - balance: Sentinel scatter spit scatter lowered 60 to 45, cooldown lowered 8s to
+ 6s
+ - balance: Sentinel paralyzing slash stuns after 3 seconds instead of 4 and lasts
+ 2.5 seconds from 2.
diff --git a/tgui/packages/tgui/components/Button.tsx b/tgui/packages/tgui/components/Button.tsx
index 76ff79a60c75..c6e39264fd32 100644
--- a/tgui/packages/tgui/components/Button.tsx
+++ b/tgui/packages/tgui/components/Button.tsx
@@ -256,7 +256,7 @@ const ButtonConfirm = (props: ConfirmProps) => {
}}
{...rest}
>
- {clickedOnce ? confirmContent : children}
+ {clickedOnce && confirmContent ? confirmContent : children}
);
};
diff --git a/tgui/packages/tgui/interfaces/BodyPicker.tsx b/tgui/packages/tgui/interfaces/BodyPicker.tsx
new file mode 100644
index 000000000000..4ca0c03acdf7
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/BodyPicker.tsx
@@ -0,0 +1,183 @@
+import { useState } from 'react';
+
+import { useBackend } from '../backend';
+import {
+ Box,
+ Button,
+ ColorBox,
+ DmIcon,
+ Modal,
+ Stack,
+ Tooltip,
+} from '../components';
+import { Window } from '../layouts';
+
+type PickerData = {
+ icon: string;
+ body_types: { name: string; icon: string }[];
+ skin_colors: { name: string; icon: string; color: string }[];
+ body_sizes: { name: string; icon: string }[];
+
+ body_type: string;
+ skin_color: string;
+ body_size: string;
+};
+
+export const BodyPicker = () => {
+ const { data } = useBackend();
+
+ const { icon, body_size, body_type, skin_color, body_types, body_sizes } =
+ data;
+
+ const [picker, setPicker] = useState<'type' | 'size' | undefined>();
+
+ const unselectedBodyType = body_types.filter(
+ (val) => val.icon !== body_type,
+ )[0];
+
+ const unselectedBodySize = body_sizes.filter(
+ (val) => val.icon !== body_size,
+ )[0];
+
+ return (
+
+
+ {picker && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const TypePicker = (props: {
+ readonly picker: (_) => void;
+ readonly toUse: 'type' | 'size';
+}) => {
+ const { data, act } = useBackend();
+
+ const { picker, toUse } = props;
+
+ const { body_type, body_types, skin_color, body_size, body_sizes, icon } =
+ data;
+
+ const toIterate = toUse === 'type' ? body_types : body_sizes;
+
+ const active = toUse === 'type' ? body_type : body_size;
+
+ return (
+
+ {toIterate.map((type) => (
+
+
+ {
+ picker(undefined);
+ act(toUse, { name: type.name });
+ }}
+ position="relative"
+ className={`typePicker ${active === type.icon ? 'active' : ''}`}
+ >
+
+
+
+
+ ))}
+
+ );
+};
+
+const ColorOptions = () => {
+ const { data, act } = useBackend();
+
+ const { skin_color, skin_colors } = data;
+
+ return (
+
+ {skin_colors.map((color) => (
+
+ act('color', { name: color.name })}
+ className={`colorPicker ${skin_color === color.icon ? 'active' : ''}`}
+ />
+
+ ))}
+
+ );
+};
diff --git a/tgui/packages/tgui/styles/interfaces/BodyPicker.scss b/tgui/packages/tgui/styles/interfaces/BodyPicker.scss
new file mode 100644
index 000000000000..d60cd2112493
--- /dev/null
+++ b/tgui/packages/tgui/styles/interfaces/BodyPicker.scss
@@ -0,0 +1,19 @@
+.theme-crtblue {
+ .BodyPicker {
+ .Stack--horizontal > .colorPickerContainer:first-of-type {
+ margin-left: 6px;
+ }
+
+ .typePicker {
+ border: 1px dotted #8ac8ff;
+ }
+
+ .typePicker.active {
+ border: 1px solid #8ac8ff;
+ }
+
+ .colorPicker.active {
+ outline: solid 2px #8ac8ff;
+ }
+ }
+}
diff --git a/tgui/packages/tgui/styles/main.scss b/tgui/packages/tgui/styles/main.scss
index 2f9bf90463d7..3803da7a9237 100644
--- a/tgui/packages/tgui/styles/main.scss
+++ b/tgui/packages/tgui/styles/main.scss
@@ -48,6 +48,7 @@
@include meta.load-css('./components/Tooltip.scss');
// Interfaces
+@include meta.load-css('./interfaces/BodyPicker.scss');
@include meta.load-css('./interfaces/Changelog.scss');
@include meta.load-css('./interfaces/ListInput.scss');
@include meta.load-css('./interfaces/CasSim.scss');
diff --git a/tgui/packages/tgui/styles/themes/crt.scss b/tgui/packages/tgui/styles/themes/crt.scss
index 9f9436f275a3..87c0bd58ad14 100644
--- a/tgui/packages/tgui/styles/themes/crt.scss
+++ b/tgui/packages/tgui/styles/themes/crt.scss
@@ -99,6 +99,11 @@ $background-radial-opacity: 0.2 !default;
)
);
+ @include meta.load-css(
+ '../components/Modal.scss',
+ $with: ('background-color': base.$color-bg)
+ );
+
.Layout__content {
background-image: none;
background: radial-gradient(