diff --git a/citadel.dme b/citadel.dme
index 52eaac21819..52e2c18fa62 100644
--- a/citadel.dme
+++ b/citadel.dme
@@ -3579,10 +3579,12 @@
#include "code\modules\mob\living\carbon\human\descriptors\descriptors_generic.dm"
#include "code\modules\mob\living\carbon\human\descriptors\descriptors_skrell.dm"
#include "code\modules\mob\living\carbon\human\descriptors\descriptors_vox.dm"
+#include "code\modules\mob\living\carbon\human\traits\_trait_group.dm"
#include "code\modules\mob\living\carbon\human\traits\_trait.dm"
#include "code\modules\mob\living\carbon\human\traits\negative.dm"
#include "code\modules\mob\living\carbon\human\traits\neutral.dm"
#include "code\modules\mob\living\carbon\human\traits\positive.dm"
+#include "code\modules\mob\living\carbon\human\traits\trait_groups.dm"
#include "code\modules\mob\living\carbon\human\traits\weaver_objs.dm"
#include "code\modules\mob\living\carbon\human\traits\weaver_recipies.dm"
#include "code\modules\mob\living\silicon\damage_procs.dm"
diff --git a/code/__HELPERS/global_lists.dm b/code/__HELPERS/global_lists.dm
index 9a9827e78cf..37a8a8f916d 100644
--- a/code/__HELPERS/global_lists.dm
+++ b/code/__HELPERS/global_lists.dm
@@ -168,10 +168,10 @@ var/global/list/hexNums = list("0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
var/global/list/negative_traits = list()
/// Neutral custom species traits, indexed by path.
var/global/list/neutral_traits = list()
-/// Neutral traits available to all species, indexed by path.
-var/global/list/everyone_traits = list()
/// Positive custom species traits, indexed by path.
var/global/list/positive_traits = list()
+/// Trait groups, indexed by path
+var/global/list/all_trait_groups = list()
/// Just path = cost list, saves time in char setup.
var/global/list/traits_costs = list()
/// All of 'em at once. (same instances)
@@ -601,8 +601,6 @@ var/global/list/remainless_species = list(SPECIES_ID_PROMETHEAN,
var/cost = instance.cost
traits_costs[path] = cost
all_traits[path] = instance
- if(!instance.custom_only && instance.cost <= 0)
- everyone_traits[path] = instance
switch(cost)
if(-INFINITY to -0.1)
negative_traits[path] = instance
@@ -611,6 +609,14 @@ var/global/list/remainless_species = list(SPECIES_ID_PROMETHEAN,
if(0.1 to INFINITY)
positive_traits[path] = instance
+ // Trait groups
+ paths = typesof(/datum/trait_group) - /datum/trait_group
+ for(var/path in paths)
+ var/datum/trait_group/instance = new path()
+ if(!instance.name)
+ continue // Should never happen but worth checking for
+ all_trait_groups[path] = instance
+
// Weaver recipe stuff
paths = subtypesof(/datum/weaver_recipe/structure)
for(var/path in paths)
diff --git a/code/modules/mob/living/carbon/human/traits/_trait.dm b/code/modules/mob/living/carbon/human/traits/_trait.dm
index 9e709eb0c28..ac19f4490e3 100644
--- a/code/modules/mob/living/carbon/human/traits/_trait.dm
+++ b/code/modules/mob/living/carbon/human/traits/_trait.dm
@@ -2,6 +2,20 @@
var/name
var/desc = "Contact a developer if you see this trait."
+ /// Path of the group this trait is affiliated with in TGUI
+ /// If unspecified, create a group named after this trait
+ /// where this trait is the only member
+ var/group = null
+
+ /// If this trait is affiliated with a group, use a shorter name for it in the group UI
+ /// Name must still be set (it's used on the overall trait summary page)
+ /// For instance, Autohiss (Tajaran) becomes Tajaran
+ var/group_short_name = null
+
+ /// String key for sorting this trait in the UI
+ /// If this trait creates its own group (group = null), then this is the sort key of
+ /// the created group.
+ var/sort_key
/// Extra IC information about this trait that gets placed within the confidential flap of ID cards
var/extra_id_info
/// Whether or not this trait can have extra info opted out of
@@ -18,6 +32,10 @@
/// Trait only available for custom species.
var/custom_only = TRUE
+ /// If TRUE, show this trait even if it is forbidden.
+ /// We use this to blacklist species-level customization that most users would have genuinely no reason to care about.
+ var/show_when_forbidden = TRUE
+
/// list of TRAIT_*'s to apply, using QUIRK_TRAIT
var/list/traits
diff --git a/code/modules/mob/living/carbon/human/traits/_trait_group.dm b/code/modules/mob/living/carbon/human/traits/_trait_group.dm
new file mode 100644
index 00000000000..e6f5137d691
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/traits/_trait_group.dm
@@ -0,0 +1,6 @@
+/datum/trait_group
+ var/name
+ var/desc = "Contact a developer if you see this description."
+
+ /// String key for sorting this trait group in the UI
+ var/sort_key
diff --git a/code/modules/mob/living/carbon/human/traits/negative.dm b/code/modules/mob/living/carbon/human/traits/negative.dm
index 44e033c2d3f..54c3fa4abbb 100644
--- a/code/modules/mob/living/carbon/human/traits/negative.dm
+++ b/code/modules/mob/living/carbon/human/traits/negative.dm
@@ -1,126 +1,198 @@
/datum/trait/negative/speed_slow
name = "Slowdown"
- desc = "Allows you to move slower on average than baseline."
+ desc = "Slower."
cost = -2
var_changes = list("slowdown" = 0.5)
+ group = /datum/trait_group/speed
+ group_short_name = "Slowdown"
+ sort_key = "2-Slowdown"
+
/datum/trait/negative/speed_slow_plus
name = "Major Slowdown"
- desc = "Allows you to move MUCH slower on average than baseline."
+ desc = "MUCH slower."
cost = -3
var_changes = list("slowdown" = 1.0)
+ group = /datum/trait_group/speed
+ group_short_name = "Major Slowdown"
+ sort_key = "1-Major Slowdown"
+
/datum/trait/negative/endurance_low
name = "Low Endurance"
- desc = "Reduces your maximum total hitpoints to 75."
+ desc = "75 hitpoints."
cost = -2
extra_id_info = "Employee is unusually susceptible to all forms of harm."
var_changes = list("total_health" = 75)
+ group = /datum/trait_group/health
+ group_short_name = "Low"
+ sort_key = "2-Low"
+
/datum/trait/negative/endurance_low/apply(datum/species/S, mob/living/carbon/human/H)
..(S,H)
H.setMaxHealth(S.total_health)
/datum/trait/negative/endurance_very_low
name = "Extremely Low Endurance"
- desc = "Reduces your maximum total hitpoints to 50."
+ desc = "50 hitpoints."
cost = -3 //Teshari HP. This makes the person a lot more suseptable to getting stunned, killed, etc.
extra_id_info = "Employee is extremely susceptible to all forms of harm."
var_changes = list("total_health" = 50)
+ group = /datum/trait_group/health
+ group_short_name = "Extremely Low"
+ sort_key = "2-Extremely Low"
+
/datum/trait/negative/endurance_very_low/apply(datum/species/S, mob/living/carbon/human/H)
..(S,H)
H.setMaxHealth(S.total_health)
/datum/trait/negative/minor_brute_weak
name = "Minor Brute Weakness"
- desc = "You take 15% more brute damage"
+ desc = "15% more."
cost = -1
var_changes = list("brute_mod" = 1.15)
+ group = /datum/trait_group/brute
+ group_short_name = "Minor Weakness"
+ sort_key = "3-Minor Weakness"
+
/datum/trait/negative/brute_weak
name = "Brute Weakness"
- desc = "You take 25% more brute damage"
+ desc = "25% more."
cost = -2
var_changes = list("brute_mod" = 1.25)
+ group = /datum/trait_group/brute
+ group_short_name = "Weakness"
+ sort_key = "2-Weakness"
+
/datum/trait/negative/brute_weak_plus
name = "Major Brute Weakness"
- desc = "You take 50% more brute damage"
+ desc = "50% more."
cost = -3
extra_id_info = "Employee is unusually susceptible to blunt trauma."
var_changes = list("brute_mod" = 1.5)
+ group = /datum/trait_group/brute
+ group_short_name = "Major Weakness"
+ sort_key = "1-Major Weakness"
+
/datum/trait/negative/minor_burn_weak
name = "Minor Burn Weakness"
- desc = "You take 15% more burn damage"
+ desc = "15% more."
cost = -1
var_changes = list("burn_mod" = 1.15)
+ group = /datum/trait_group/burn
+ group_short_name = "Minor Weakness"
+ sort_key = "3-Minor Weakness"
+
/datum/trait/negative/burn_weak
name = "Burn Weakness"
- desc = "You take 25% more burn damage"
+ desc = "25% more."
cost = -2
var_changes = list("burn_mod" = 1.25)
+ group = /datum/trait_group/burn
+ group_short_name = "Weakness"
+ sort_key = "2-Weakness"
+
/datum/trait/negative/burn_weak_plus
name = "Major Burn Weakness"
- desc = "You take 50% more burn damage"
+ desc = "50% more."
cost = -3
extra_id_info = "Employee is unusually sensitive to heat."
var_changes = list("burn_mod" = 1.5)
+ group = /datum/trait_group/burn
+ group_short_name = "Major Weakness"
+ sort_key = "1-Major Weakness"
+
/datum/trait/negative/toxin_weak
name = "Toxin Weakness"
- desc = "You take 25% more toxin damage"
+ desc = "25% more."
cost = -1
var_changes = list("toxins_mod" = 1.25)
+ group = /datum/trait_group/toxin
+ group_short_name = "Weakness"
+ sort_key = "2-Weakness"
+
/datum/trait/negative/toxin_weak_plus
- name = "Major Toxin Weaness"
- desc = "You take 50% more toxin damage"
+ name = "Major Toxin Weakness"
+ desc = "50% more."
cost = -2
extra_id_info = "Employee's organs are ineffective at filtering toxins."
var_changes = list("toxins_mod" = 1.5)
+ group = /datum/trait_group/toxin
+ group_short_name = "Major Weakness"
+ sort_key = "1-Major Weakness"
+
/datum/trait/negative/oxy_weak
name = "Breathe Weakness"
- desc = "You take 25% more breathe damage and require 25% more air (20kpa minimum). Make sure to adjust your emergency EVA tanks."
+ desc = "25% more damage, 25% more air. (20kpa min)"
cost = -1
extra_id_info = "Employee requires a minimum atmospheric pressure of 20kPa to breathe."
var_changes = list("minimum_breath_pressure" = 20, "oxy_mod" = 1.25)
+ group = /datum/trait_group/oxy
+ group_short_name = "Weakness"
+ sort_key = "2-Weakness"
+
/datum/trait/negative/rad_weak
name = "Radiation Weakness"
- desc = "You take 25% more radition damage"
+ desc = "25% more."
cost = -1
var_changes = list("radiation_mod" = 1.25)
+ group = /datum/trait_group/rad
+ group_short_name = "Weakness"
+ sort_key = "2-Weakness"
+
/datum/trait/negative/rad_weak_plus
name = "Major Radiation Weakness"
- desc = "You take 50% more radition damage"
+ desc = "50% more."
cost = -2
extra_id_info = "Employee is extremely susceptible to radiation."
var_changes = list("radiation_mod" = 1.50)
+ group = /datum/trait_group/rad
+ group_short_name = "Major Weakness"
+ sort_key = "1-Major Weakness"
+
/datum/trait/negative/conductive
name = "Conductive"
- desc = "Increases your susceptibility to electric shocks by 50%"
+ desc = "50% more susceptible."
cost = -1
var_changes = list("siemens_coefficient" = 1.5) //This makes you a lot weaker to tasers.
+ group = /datum/trait_group/electro
+ group_short_name = "Conductive"
+ sort_key = "2-Conductive"
+
/datum/trait/negative/conductive_plus
name = "Major Conductive"
- desc = "Increases your susceptibility to electric shocks by 100%"
+ desc = "100% more susceptible."
cost = -2
extra_id_info = "Employee is exceptionally conductive."
var_changes = list("siemens_coefficient" = 2.0) //This makes you extremely weak to tasers.
+ group = /datum/trait_group/electro
+ group_short_name = "Major Conductive"
+ sort_key = "1-Major Conductive"
+
/datum/trait/negative/hollow
name = "Weak Bones/Aluminum Alloy"
- desc = "Your bones and robot limbs are easier to break."
+ desc = "Easier to break."
cost = -2 //I feel like this should be higher, but let's see where it goes
+ group = /datum/trait_group/bones
+ group_short_name = "Weak/Aluminum"
+ sort_key = "2-Weak/Aluminum"
+
/datum/trait/negative/hollow/apply(var/datum/species/S,var/mob/living/carbon/human/H)
..(S,H)
for(var/obj/item/organ/external/O in H.organs)
@@ -129,10 +201,14 @@
/datum/trait/negative/hollow_plus
name = "Hollow Bones/Brittle Alloy"
- desc = "Your bones and robot limbs are significantly easier to break."
+ desc = "Significantly easier to break."
cost = -4 //I feel like this should be higher, but let's see where it goes
extra_id_info = "Employee's bones are unusually fragile."
+ group = /datum/trait_group/bones
+ group_short_name = "Hollow/Brittle"
+ sort_key = "1-Hollow/Brittle"
+
/datum/trait/negative/hollow_plus/apply(var/datum/species/S,var/mob/living/carbon/human/H)
..(S,H)
for(var/obj/item/organ/external/O in H.organs)
@@ -147,31 +223,43 @@
/datum/trait/negative/colorblind/mono
name = "Colorblindness (Monochromancy)"
- desc = "You simply can't see colors at all, period. You are 100% colorblind."
+ desc = "No colors. 100% colorblind."
cost = -1
custom_only = FALSE
extra_id_info = "Employee is only capable of perceiving luminance, and cannot perceive hues or saturation."
+ group = /datum/trait_group/colorblindness
+ group_short_name = "Monochromancy"
+ sort_key = "1-Monochromancy"
+
/datum/trait/negative/colorblind/mono/apply(var/datum/species/S,var/mob/living/carbon/human/H)
..(S,H)
H.add_modifier(/datum/modifier/trait/colorblind_monochrome)
/datum/trait/negative/colorblind/para_vulp
name = "Colorblindness (Para Vulp)"
- desc = "You have a severe issue with green colors and have difficulty recognizing them from red colors."
+ desc = "Severe red/green difficulty."
cost = -1
extra_id_info = "Employee has a form of red/green colorblindness."
+ group = /datum/trait_group/colorblindness
+ group_short_name = "Para Vulp"
+ sort_key = "2-Para Vulp"
+
/datum/trait/negative/colorblind/para_vulp/apply(var/datum/species/S,var/mob/living/carbon/human/H)
..(S,H)
H.add_modifier(/datum/modifier/trait/colorblind_vulp)
/datum/trait/negative/colorblind/para_taj
name = "Colorblindness (Para Taj)"
- desc = "You have a minor issue with blue colors and have difficulty recognizing them from red colors."
+ desc = "Minor red/blue difficulty."
cost = -1
extra_id_info = "Employee has a form of blue/red colorblindness."
+ group = /datum/trait_group/colorblindness
+ group_short_name = "Para Taj"
+ sort_key = "2-Para Taj"
+
/datum/trait/negative/colorblind/para_taj/apply(var/datum/species/S,var/mob/living/carbon/human/H)
..(S,H)
H.add_modifier(/datum/modifier/trait/colorblind_taj)
@@ -180,9 +268,13 @@
name = "Photosensitive"
desc = "You are incredibly vulnerable to bright lights. You are blinded for longer and your skin burns under extreme light."
cost = -1
+ var_changes = list("flash_mod" = 2, "flash_burn" = 5)
+
+ group = /datum/trait_group/photosensitivity
+ group_short_name = "Photosensitive"
+ sort_key = "4-Photosensitive"
+
extra_id_info = "Employee is exceptionally sensitive to bright lights."
- var_changes = list("flash_mod" = 2)
- var_changes = list("flash_burn" = 5)
/datum/trait/negative/hemophilia
name = "Hemophilia"
@@ -191,6 +283,10 @@
extra_id_info = "Employee is exceptionally prone to bleeding."
var_changes = list("bloodloss_rate" = 2)
+ group = /datum/trait_group/blood
+ group_short_name = "Hemophilia"
+ sort_key = "4-Hemophilia"
+
// todo: use it as a disability? kinda silly this applies forever
/datum/trait/negative/blind
name = "Blind"
@@ -202,6 +298,9 @@
/datum/trait/negative/deaf
)
+ group = /datum/trait_group/disability
+ group_short_name = "Blind"
+
/datum/trait/negative/blind/apply(var/datum/species/S,var/mob/living/carbon/human/H)
.=..()
H.add_blindness_source(TRAIT_BLINDNESS_NEGATIV)
@@ -224,6 +323,9 @@
/datum/trait/negative/blind
)
+ group = /datum/trait_group/disability
+ group_short_name = "Deaf"
+
// todo: organ disability? better way to have mutual exclusion from having all 3
/datum/trait/negative/mute
name = "Mute"
@@ -234,3 +336,6 @@
traits = list(
TRAIT_MUTE
)
+
+ group = /datum/trait_group/disability
+ group_short_name = "Mute"
diff --git a/code/modules/mob/living/carbon/human/traits/neutral.dm b/code/modules/mob/living/carbon/human/traits/neutral.dm
index c7ea33c9fac..e21c9b786a5 100644
--- a/code/modules/mob/living/carbon/human/traits/neutral.dm
+++ b/code/modules/mob/living/carbon/human/traits/neutral.dm
@@ -6,6 +6,10 @@
excludes = list(/datum/trait/neutral/metabolism_down, /datum/trait/neutral/metabolism_apex)
extra_id_info = "Employee has a faster-than-average metabolism."
+ group = /datum/trait_group/metabolism
+ group_short_name = "Fast"
+ sort_key = "5-Fast"
+
/datum/trait/neutral/metabolism_down
name = "Slow Metabolism"
desc = "You process ingested and injected reagents slower, but get hungry slower."
@@ -14,6 +18,10 @@
excludes = list(/datum/trait/neutral/metabolism_up, /datum/trait/neutral/metabolism_apex)
extra_id_info = "Employee has a slower-than-average metabolism."
+ group = /datum/trait_group/metabolism
+ group_short_name = "Slow"
+ sort_key = "4-Slow"
+
/datum/trait/neutral/metabolism_apex
name = "Apex Metabolism"
desc = "Finally a proper excuse for your predatory actions. Essentially doubles the fast trait rates. Good for characters with big appetites."
@@ -22,6 +30,10 @@
excludes = list(/datum/trait/neutral/metabolism_up, /datum/trait/neutral/metabolism_down)
extra_id_info = "Employee has an unusually fast metabolism."
+ group = /datum/trait_group/metabolism
+ group_short_name = "Apex"
+ sort_key = "6-Apex"
+
/datum/trait/neutral/cold_discomfort
name = "Hot-Blooded"
desc = "You are too hot at the standard 20C. 18C is more suitable. Rolling down your jumpsuit or being unclothed helps."
@@ -30,6 +42,9 @@
excludes = list(/datum/trait/neutral/hot_discomfort)
extra_id_info = "Employee is acclimated to colder temperatures."
+ group = /datum/trait_group/temperature
+ group_short_name = "Hot-Blooded"
+
/datum/trait/neutral/hot_discomfort
name = "Cold-Blooded"
desc = "You are too cold at the standard 20C. 22C is more suitable. Wearing clothing that covers your legs and torso helps."
@@ -38,9 +53,12 @@
excludes = list(/datum/trait/neutral/cold_discomfort)
extra_id_info = "Employee is acclimated to warmer temperatures."
+ group = /datum/trait_group/temperature
+ group_short_name = "Cold-Blooded"
+
/datum/trait/neutral/autohiss_unathi
name = "Autohiss (Unathi)"
- desc = "You roll your S's and x's"
+ desc = "Rolls your S's and X's."
cost = 0
custom_only = FALSE
var_changes = list(
@@ -54,9 +72,12 @@
excludes = list(/datum/trait/neutral/autohiss_tajaran)
+ group = /datum/trait_group/autohiss
+ group_short_name = "Unathi"
+
/datum/trait/neutral/autohiss_tajaran
name = "Autohiss (Tajaran)"
- desc = "You roll your R's."
+ desc = "Rolls your R's."
cost = 0
custom_only = FALSE
var_changes = list(
@@ -66,14 +87,21 @@
autohiss_exempt = list("Siik"))
excludes = list(/datum/trait/neutral/autohiss_unathi)
+ group = /datum/trait_group/autohiss
+ group_short_name = "Tajaran"
+
/datum/trait/neutral/bloodsucker
name = "Bloodsucker"
- desc = "Makes you unable to gain nutrition from anything but blood. To compenstate, you get fangs that can be used to drain blood from prey."
+ desc = "Only blood provides nutrition. Sharp fangs included. No other features."
cost = 0
var_changes = list("is_vampire" = TRUE) //The verb is given in human.dm
custom_only = FALSE
extra_id_info = "Employee's diet is exclusively blood. Employee has tested negative for vetalism."
+ group = /datum/trait_group/vampirism
+ group_short_name = "Lite"
+ sort_key = "2-Lite"
+
/datum/trait/neutral/bloodsucker/apply(datum/species/S, mob/living/carbon/human/H)
..(S,H)
add_verb(H, /mob/living/carbon/human/proc/bloodsuck)
@@ -91,7 +119,7 @@
/datum/trait/neutral/vampire
name = "Vetalan / Vampiric"
- desc = "Vampires, officially known as the Vetalan, are weaker to burns, bright lights, and must consume blood to survive. To this end, they can see near-perfectly in the darkness, possess sharp, numbing fangs, and anti-septic saliva."
+ desc = "Standard Vetalan features."
cost = 0
extra_id_info = "Employee is a carrier of Vetalism, and needs to consume blood to survive. Additionally, employee's saliva carries antiseptic properties."
custom_only = FALSE
@@ -103,6 +131,10 @@
"burn_mod" = 1.25,
"unarmed_types" = list(/datum/unarmed_attack/stomp, /datum/unarmed_attack/kick, /datum/unarmed_attack/claws, /datum/unarmed_attack/bite/sharp, /datum/unarmed_attack/bite/sharp/numbing))
+ group = /datum/trait_group/vampirism
+ group_short_name = "Standard"
+ sort_key = "1-Standard"
+
/datum/trait/neutral/vampire/apply(datum/species/S, mob/living/carbon/human/H)
..(S,H)
H.add_vision_modifier(/datum/vision/augmenting/vetalan)
@@ -137,12 +169,20 @@
custom_only = FALSE
var_changes = list("has_glowing_eyes" = 1)
+ group = /datum/trait_group/bioluminescence
+ group_short_name = "Eyes"
+ sort_key = "1-Eyes"
+
/datum/trait/neutral/glowing_body
name = "Glowing Body"
desc = "Your body glows about as much as a PDA light! Settable color and toggle in Abilities tab ingame."
cost = 0
custom_only = FALSE
+ group = /datum/trait_group/bioluminescence
+ group_short_name = "Body"
+ sort_key = "1-Body"
+
/datum/trait/neutral/glowing_body/apply(datum/species/S, mob/living/carbon/human/H)
..(S,H)
add_verb(H, /mob/living/proc/glow_toggle)
@@ -151,96 +191,128 @@
//! ## Body shape traits
/datum/trait/neutral/taller
name = "Taller"
- desc = "Your body is taller than average."
+ desc = "Even taller."
cost = 0
custom_only = FALSE
var_changes = list("icon_scale_y" = 1.09)
excludes = list(/datum/trait/neutral/tall, /datum/trait/neutral/short, /datum/trait/neutral/shorter)
+ group = /datum/trait_group/height
+ group_short_name = "Taller"
+ sort_key = "6-Taller"
+
/datum/trait/neutral/taller/apply(datum/species/S, mob/living/carbon/human/H)
..(S,H)
H.update_transform()
/datum/trait/neutral/tall
name = "Tall"
- desc = "Your body is a bit taller than average."
+ desc = "A bit taller than average."
cost = 0
custom_only = FALSE
var_changes = list("icon_scale_y" = 1.05)
excludes = list(/datum/trait/neutral/taller, /datum/trait/neutral/short, /datum/trait/neutral/shorter)
+ group = /datum/trait_group/height
+ group_short_name = "Tall"
+ sort_key = "5-Tall"
+
/datum/trait/neutral/tall/apply(datum/species/S, mob/living/carbon/human/H)
..(S,H)
H.update_transform()
/datum/trait/neutral/short
name = "Short"
- desc = "Your body is a bit shorter than average."
+ desc = "A bit shorter than average."
cost = 0
custom_only = FALSE
var_changes = list("icon_scale_y" = 0.95)
excludes = list(/datum/trait/neutral/taller, /datum/trait/neutral/tall, /datum/trait/neutral/shorter)
+ group = /datum/trait_group/height
+ group_short_name = "Short"
+ sort_key = "4-Short"
+
/datum/trait/neutral/short/apply(datum/species/S, mob/living/carbon/human/H)
..(S,H)
H.update_transform()
/datum/trait/neutral/shorter
name = "Shorter"
- desc = "You are shorter than average."
+ desc = "Short."
cost = 0
custom_only = FALSE
var_changes = list("icon_scale_y" = 0.915)
excludes = list(/datum/trait/neutral/taller, /datum/trait/neutral/tall, /datum/trait/neutral/short)
+ group = /datum/trait_group/height
+ group_short_name = "Shorter"
+ sort_key = "3-Short"
+
/datum/trait/neutral/shorter/apply(datum/species/S, mob/living/carbon/human/H)
..(S,H)
H.update_transform()
/datum/trait/neutral/fat
name = "Overweight"
- desc = "You are heavier than average."
+ desc = "Heavier than average."
cost = 0
custom_only = FALSE
var_changes = list("icon_scale_x" = 1.054)
excludes = list(/datum/trait/neutral/obese, /datum/trait/neutral/thin, /datum/trait/neutral/thinner)
+ group = /datum/trait_group/weight
+ group_short_name = "Overweight"
+ sort_key = "5-Overweight"
+
/datum/trait/neutral/fat/apply(datum/species/S, mob/living/carbon/human/H)
..(S,H)
H.update_transform()
/datum/trait/neutral/obese
name = "Obese"
- desc = "You are much heavier than average."
+ desc = "Even heavier."
cost = 0
custom_only = FALSE
var_changes = list("icon_scale_x" = 1.095)
excludes = list(/datum/trait/neutral/fat, /datum/trait/neutral/thin, /datum/trait/neutral/thinner)
+ group = /datum/trait_group/weight
+ group_short_name = "Obese"
+ sort_key = "6-Obese"
+
/datum/trait/neutral/obese/apply(datum/species/S, mob/living/carbon/human/H)
..(S,H)
H.update_transform()
/datum/trait/neutral/thin
name = "Thin"
- desc = "You are skinnier than average."
+ desc = "Skinnier than average."
cost = 0
custom_only = FALSE
var_changes = list("icon_scale_x" = 0.945)
excludes = list(/datum/trait/neutral/fat, /datum/trait/neutral/obese, /datum/trait/neutral/thinner)
+ group = /datum/trait_group/weight
+ group_short_name = "Thin"
+ sort_key = "4-Thin"
+
/datum/trait/neutral/thin/apply(datum/species/S, mob/living/carbon/human/H)
..(S,H)
H.update_transform()
/datum/trait/neutral/thinner
name = "Very Thin"
- desc = "You are much skinnier than average."
+ desc = "Very skinny."
cost = 0
custom_only = FALSE
var_changes = list("icon_scale_x" = 0.905)
excludes = list(/datum/trait/neutral/fat, /datum/trait/neutral/obese, /datum/trait/neutral/thin)
+ group = /datum/trait_group/weight
+ group_short_name = "Very Thin"
+ sort_key = "3-Very Thin"
+
/datum/trait/neutral/thinner/apply(datum/species/S, mob/living/carbon/human/H)
..(S,H)
H.update_transform()
@@ -252,6 +324,10 @@
custom_only = FALSE
extra_id_info = "Employee's saliva carries antiseptic properties."
+ group = /datum/trait_group/vampirism
+ group_short_name = "Saliva"
+ sort_key = "8-Saliva"
+
/datum/trait/neutral/antiseptic_saliva/apply(datum/species/S, mob/living/carbon/human/H)
..(S,H)
add_verb(H, /mob/living/carbon/human/proc/lick_wounds)
diff --git a/code/modules/mob/living/carbon/human/traits/positive.dm b/code/modules/mob/living/carbon/human/traits/positive.dm
index b429282bb8f..a419edb7377 100644
--- a/code/modules/mob/living/carbon/human/traits/positive.dm
+++ b/code/modules/mob/living/carbon/human/traits/positive.dm
@@ -1,135 +1,211 @@
/datum/trait/positive/speed_fast
name = "Haste"
- desc = "Allows you to move faster on average than baseline."
+ desc = "Faster than average."
cost = 2
var_changes = list("slowdown" = -0.2)
+ group = /datum/trait_group/speed
+ group_short_name = "Haste"
+ sort_key = "3-Haste"
+
/datum/trait/positive/endurance_plus
name = "Better Endurance"
- desc = "Increases your maximum total hitpoints to 110"
+ desc = "110 hitpoints."
cost = 3
var_changes = list("total_health" = 110)
+ group = /datum/trait_group/health
+ group_short_name = "Better"
+ sort_key = "4-Better"
+
/datum/trait/positive/endurance_high
name = "High Endurance"
- desc = "Increases your maximum total hitpoints to 125"
+ desc = "125 hitpoints."
cost = 4
var_changes = list("total_health" = 125)
+ group = /datum/trait_group/health
+ group_short_name = "High"
+ sort_key = "5-High"
+
/datum/trait/positive/endurance_high/apply(datum/species/S, mob/living/carbon/human/H)
..(S,H)
H.setMaxHealth(S.total_health)
/datum/trait/positive/nonconductive
name = "Non-Conductive"
- desc = "Decreases your susceptibility to electric shocks by a 25% amount."
- cost = 2 //This effects tasers!
+ desc = "25% less."
+ cost = 2 //This affects tasers!
var_changes = list("siemens_coefficient" = 0.75)
+ group = /datum/trait_group/electro
+ group_short_name = "Non-Conductive"
+ sort_key = "5-Non-Conductive"
+
/datum/trait/positive/nonconductive_plus
name = "Major Non-Conductive"
- desc = "Decreases your susceptibility to electric shocks by a 50% amount."
- cost = 3 //Let us not forget this effects tasers!
+ desc = "50% less."
+ cost = 3 //Let us not forget this affects tasers!
var_changes = list("siemens_coefficient" = 0.5)
+ group = /datum/trait_group/electro
+ group_short_name = "Major Non-Conductive"
+ sort_key = "6-Major Non-Conductive"
+
/datum/trait/positive/melee_attack
name = "Sharp Melee"
desc = "Provides sharp melee attacks that do more damage."
cost = 1
var_changes = list("unarmed_types" = list(/datum/unarmed_attack/stomp, /datum/unarmed_attack/kick, /datum/unarmed_attack/claws/good, /datum/unarmed_attack/bite/sharp/good))
+ group = /datum/trait_group/bite_and_claw
+ group_short_name = "Sharp"
+ sort_key = "6-Sharp"
+
/datum/trait/positive/melee_attack/apply(var/datum/species/S,var/mob/living/carbon/human/H)
..(S,H)
S.update_attack_types()
/datum/trait/positive/melee_attack_fangs
name = "Sharp Melee & Venomous Fangs"
- desc = "Provides sharp melee attacks that do more damage, along with venomous fangs."
+ desc = "That plus venomous fangs."
cost = 2
var_changes = list("unarmed_types" = list(/datum/unarmed_attack/stomp, /datum/unarmed_attack/kick, /datum/unarmed_attack/claws/good/venom, /datum/unarmed_attack/bite/sharp/good/venom))
+ group = /datum/trait_group/bite_and_claw
+ group_short_name = "Sharp, Venomous"
+ sort_key = "7-Sharp, Venomous"
+
/datum/trait/positive/melee_attack_fangs/apply(var/datum/species/S,var/mob/living/carbon/human/H)
..(S,H)
S.update_attack_types()
/datum/trait/positive/minor_brute_resist
name = "Minor Brute Resist"
- desc = "Adds 15% resistance to brute damage"
+ desc = "15% less."
cost = 2
var_changes = list("brute_mod" = 0.85)
+ group = /datum/trait_group/brute
+ group_short_name = "Minor Resist"
+ sort_key = "5-Minor Resist"
+
/datum/trait/positive/brute_resist
name = "Brute Resist"
- desc = "Adds 25% resistance to brute damage"
+ desc = "25% less."
cost = 3
var_changes = list("brute_mod" = 0.75)
excludes = list(/datum/trait/positive/minor_burn_resist,/datum/trait/positive/burn_resist)
+ group = /datum/trait_group/brute
+ group_short_name = "Resist"
+ sort_key = "6-Resist"
+
/datum/trait/positive/minor_burn_resist
name = "Minor Burn Resist"
- desc = "Adds 15% resistance to burn damage sources."
+ desc = "15% less."
cost = 2
var_changes = list("burn_mod" = 0.85)
+ group = /datum/trait_group/burn
+ group_short_name = "Minor Resist"
+ sort_key = "5-Minor Resist"
+
/datum/trait/positive/burn_resist
name = "Burn Resist"
- desc = "Adds 25% resistance to burn damage sources."
+ desc = "25% less."
cost = 3
var_changes = list("burn_mod" = 0.75)
excludes = list(/datum/trait/positive/minor_brute_resist,/datum/trait/positive/brute_resist)
+ group = /datum/trait_group/burn
+ group_short_name = "Resist"
+ sort_key = "6-Resist"
+
/datum/trait/positive/toxin_resist
name = "Minor Toxin Resist"
- desc = "Adds 15% resistance to toxin damage sources."
+ desc = "15% less."
cost = 2
var_changes = list("toxins_mod" = 0.85)
+ group = /datum/trait_group/toxin
+ group_short_name = "Minor Resist"
+ sort_key = "5-Minor Resist"
+
/datum/trait/positive/toxin_resist_plus
name = "Toxin Resist"
- desc = "Adds 25% resistance to toxin damage sources."
+ desc = "25% less."
cost = 3
var_changes = list("toxins_mod" = 0.75)
excludes = list(/datum/trait/positive/toxin_resist,/datum/trait/positive/toxin_resist_plus)
+ group = /datum/trait_group/toxin
+ group_short_name = "Resist"
+ sort_key = "6-Resist"
+
/datum/trait/positive/oxy_resist
name = "Minor Breathe Resist"
- desc = "You take 15% less oxygen damge and require 12.5% less air (14kpa minimum)."
+ desc = "15% less damage, 12.5% less air. (14kpa min)"
cost = 2
var_changes = list("minimum_breath_pressure" = 14, "oxy_mod" = 0.85)
extra_id_info = "Employee only requires an atmospheric pressure of 14kPa to breathe."
+ group = /datum/trait_group/oxy
+ group_short_name = "Minor Resist"
+ sort_key = "5-Minor Resist"
+
/datum/trait/positive/oxy_resist_plus
name = "Breathe Resist"
- desc = "You take 25% less oxygen damge and require 25% less air (12kpa minimum)."
+ desc = "25% less damage, 25% less air. (12kpa min)"
cost = 3
var_changes = list("minimum_breath_pressure" = 12, "oxy_mod" = 0.75)
excludes = list(/datum/trait/positive/oxy_resist,/datum/trait/positive/oxy_resist_plus)
extra_id_info = "Employee only requires an atmospheric pressure of 12kPa to breathe."
+ group = /datum/trait_group/oxy
+ group_short_name = "Resist"
+ sort_key = "6-Resist"
+
/datum/trait/positive/rad_resist
name = "Minor Radiation Resist"
- desc = "You take 15% less radition damage"
+ desc = "15% less."
cost = 1
var_changes = list("radiation_mod" = 0.85)
+ group = /datum/trait_group/rad
+ group_short_name = "Minor Resist"
+ sort_key = "5-Minor Resist"
+
/datum/trait/positive/rad_resist_plus
name = "Radiation Resist"
- desc = "You take 25% less radition damage"
+ desc = "25% less."
cost = 2
var_changes = list("radiation_mod" = 0.75)
excludes = list(/datum/trait/positive/rad_resist,/datum/trait/positive/rad_resist_plus)
+ group = /datum/trait_group/rad
+ group_short_name = "Resist"
+ sort_key = "6-Resist"
+
/datum/trait/positive/photoresistant
name = "Photoresistant"
desc = "Decreases stun duration from flashes and other light-based stuns and disabilities by 50%"
cost = 1
var_changes = list("flash_mod" = 0.5)
+ group = /datum/trait_group/photosensitivity
+ group_short_name = "Photoresistant"
+ sort_key = "6-Photoresistant"
+
/datum/trait/positive/reinforced
name = "Reinforced Skeleton"
- desc = "Your body either by science or nature has been reinforced and is harder to break."
+ desc = "Harder to break."
cost = 4 //Strong Trait, high cost.
+ group = /datum/trait_group/bones
+ group_short_name = "Reinforced"
+ sort_key = "6-Reinforced"
+
/datum/trait/positive/reinforced/apply(var/datum/species/S,var/mob/living/carbon/human/H)
..(S,H)
for(var/obj/item/organ/external/O in H.organs)
@@ -156,10 +232,15 @@
/datum/trait/positive/antiseptic_saliva
name = "Antiseptic Saliva"
- desc = "Your saliva has especially strong antiseptic properties that can be used to heal small wounds."
+ desc = "Does the same thing, costs more. Weird."
cost = 1
extra_id_info = "Employee's saliva carries antiseptic properties."
+ group = /datum/trait_group/vampirism
+ group_short_name = "Saliva"
+ sort_key = "9-Saliva"
+
+
/datum/trait/positive/antiseptic_saliva/apply(var/datum/species/S,var/mob/living/carbon/human/H)
..()
add_verb(H, /mob/living/carbon/human/proc/lick_wounds)
@@ -170,6 +251,10 @@
cost = 1
var_changes = list("bloodloss_rate" = 0.75)
+ group = /datum/trait_group/blood
+ group_short_name = "Thick Blood"
+ sort_key = "6-Thick Blood"
+
/datum/trait/positive/positive/weaver
name = "Weaver"
desc = "You can produce silk and create various articles of clothing and objects."
diff --git a/code/modules/mob/living/carbon/human/traits/trait_groups.dm b/code/modules/mob/living/carbon/human/traits/trait_groups.dm
new file mode 100644
index 00000000000..38b4dba2e9e
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/traits/trait_groups.dm
@@ -0,0 +1,90 @@
+
+/datum/trait_group/height
+ name = "Height"
+ desc = "Adjusts your height."
+
+/datum/trait_group/weight
+ name = "Weight"
+ desc = "Adjusts your weight."
+
+/datum/trait_group/speed
+ name = "Speed"
+ desc = "Adjusts your speed relative to baseline."
+
+/datum/trait_group/health
+ name = "Health"
+ desc = "Adjusts your maximum total hitpoints."
+
+/datum/trait_group/brute
+ name = "Brute"
+ desc = "Adjusts how much brute damage you take."
+
+/datum/trait_group/burn
+ name = "Burn"
+ desc = "Adjusts how much burn damage you take."
+
+/datum/trait_group/toxin
+ name = "Toxin"
+ desc = "Adjusts how much toxin damage you take."
+
+/datum/trait_group/oxy
+ name = "Breathe"
+ desc = "Adjusts how much breathe damage you take and how much air you require. Make sure to adjust your emergency EVA tanks."
+
+/datum/trait_group/rad
+ name = "Radiation"
+ desc = "Adjusts how much radiation damage you take."
+
+/datum/trait_group/electro
+ name = "Electricity"
+ desc = "Adjusts susceptibility to electric shocks."
+
+/datum/trait_group/bones
+ name = "Bones"
+ desc = "Adjusts the fragility of your bones."
+
+/datum/trait_group/blood
+ name = "Blood"
+ desc = "Adjusts the viscosity of your blood."
+
+/datum/trait_group/photosensitivity
+ name = "Photosensitivity"
+ desc = "Adjusts your response to light."
+
+/datum/trait_group/autohiss
+ name = "Autohiss"
+ desc = "Your messages are phonetically transformed."
+
+/datum/trait_group/bite_and_claw
+ name = "Bite and Claw"
+ desc = "Adjusts the characteristics of your sharp bits."
+
+/datum/trait_group/bioluminescence
+ name = "Bioluminescence"
+ desc = "Glow in the dark! Scare the neighbors."
+
+/datum/trait_group/vampirism
+ name = "Vetalism / Vampirism"
+ desc = "Vampires, officially known as the Vetalan, are weaker to burns, bright lights, and must consume blood to survive. To this end, they can see near-perfectly in the darkness, possess sharp, numbing fangs, and antiseptic saliva."
+
+/datum/trait_group/colorblindness
+ name = "Colorblindness"
+ desc = "Alters your perception of color."
+
+/datum/trait_group/disability
+ name = "Disability"
+ desc = "These traits correspond to serious, game-affecting disabilities."
+
+/datum/trait_group/shadekin
+ name = "Shadekin"
+ desc = "Choose your shadekin's adaptations!"
+
+ sort_key = "000_Shadekin"
+
+/datum/trait_group/temperature
+ name = "Body Temperature"
+ desc = "Adjusts your body's preferred temperature."
+
+/datum/trait_group/metabolism
+ name = "Metabolism"
+ desc = "Adjusts the rate at which you process injested and injected reagents"
diff --git a/code/modules/preferences/preference_setup/vore/08_traits.dm b/code/modules/preferences/preference_setup/vore/08_traits.dm
index 250b388b226..a932b7822b2 100644
--- a/code/modules/preferences/preference_setup/vore/08_traits.dm
+++ b/code/modules/preferences/preference_setup/vore/08_traits.dm
@@ -23,6 +23,18 @@
var/starting_trait_points = STARTING_SPECIES_POINTS
var/max_traits = MAX_SPECIES_TRAITS
+/datum/traits_available_trait
+ var/internal_name
+ var/datum/trait/real_record
+ var/cost
+ var/forbidden_reason
+ var/list/exclusive_with
+
+/datum/traits_constraints
+ var/max_traits
+ var/max_points
+
+
// Definition of the stuff for Ears
/datum/category_item/player_setup_item/vore/traits
name = "Traits"
@@ -76,27 +88,11 @@
pref.starting_trait_points = STARTING_SPECIES_POINTS
pref.max_traits = MAX_SPECIES_TRAITS
- if(pref.real_species_id() != SPECIES_ID_CUSTOM)
- pref.pos_traits.Cut()
-
- // Clean up positive traits
- for(var/path in pref.pos_traits)
- if(!(path in positive_traits))
- pref.pos_traits -= path
- //Neutral traits
- for(var/path in pref.neu_traits)
- if(!(path in neutral_traits))
- pref.neu_traits -= path
- continue
- if((pref.real_species_id() != SPECIES_ID_CUSTOM) && !(path in everyone_traits))
- pref.neu_traits -= path
- //Negative traits
- for(var/path in pref.neg_traits)
- if(!(path in negative_traits))
- pref.neg_traits -= path
- continue
- if((pref.real_species_id() != SPECIES_ID_CUSTOM) && !(path in everyone_traits))
- pref.neg_traits -= path
+ // sanitize traits
+ var/available_traits = compute_available_traits()
+ var/constraints = compute_constraints()
+
+ apply_traits(TRUE, pref.pos_traits.Copy() + pref.neu_traits.Copy() + pref.neg_traits.Copy(), available_traits, constraints)
for(var/path in pref.id_hidden_traits)
var/datum/trait/T = all_traits[path]
@@ -153,14 +149,13 @@
. += "[pref.custom_base ? pref.custom_base : SPECIES_HUMAN]
"
var/traits_left = pref.max_traits - length(pref.pos_traits) - length(pref.neg_traits)
- . += "Traits Left: [traits_left > 0? traits_left : "[traits_left]"]
"
+ . += "Traits Left: [traits_left >= 0? traits_left : "[traits_left]"]
"
if(pref.real_species_id() == SPECIES_ID_CUSTOM)
var/points_left = pref.starting_trait_points
for(var/T in pref.pos_traits + pref.neg_traits)
points_left -= traits_costs[T]
- traits_left--
- . += "Points Left: [points_left]
"
+ . += "Points Left: [points_left >= 0 ? points_left : "[points_left]"]
"
if(points_left < 0 || traits_left < 0 || !pref.custom_species)
. += "^ Fix things! ^
"
@@ -324,107 +319,297 @@
else if(href_list["add_trait"])
var/mode = text2num(href_list["add_trait"])
- var/list/picklist
- var/list/mylist
- switch(mode)
- if(POSITIVE_MODE)
- picklist = positive_traits.Copy() - pref.pos_traits
- mylist = pref.pos_traits
- if(NEUTRAL_MODE)
- if(pref.real_species_id() == SPECIES_ID_CUSTOM)
- picklist = neutral_traits.Copy() - pref.neu_traits
- mylist = pref.neu_traits
- else
- picklist = everyone_traits.Copy() - pref.neu_traits
- mylist = pref.neu_traits
- if(NEGATIVE_MODE)
- picklist = negative_traits.Copy() - pref.neg_traits
- mylist = pref.neg_traits
- if(ALL_MODE)
- picklist = everyone_traits.Copy() - pref.neu_traits - pref.neg_traits
- mylist = pref.neg_traits.Copy() + pref.neu_traits.Copy()
-
- if(isnull(picklist))
- return PREFERENCES_REFRESH
- if(isnull(mylist))
- return PREFERENCES_REFRESH
+ var/available_traits = compute_available_traits()
+ var/constraints = compute_constraints()
+ var/tgui_data = compute_tgui_data(mode, available_traits, constraints)
+ var/traits_submission = tgui_trait_select(user, tgui_data)
+ if (traits_submission != null)
+ apply_traits(FALSE, traits_submission, available_traits, constraints)
- var/list/nicelist = list()
- var/species = pref.real_species_name()
- for(var/P in picklist)
- var/datum/trait/T = picklist[P]
- if(LAZYLEN(T.allowed_species) && !(species in T.allowed_species))
- picklist -= P
- continue
- nicelist[T.name] = P
+ return PREFERENCES_REFRESH
- var/points_left = pref.starting_trait_points
- for(var/T in pref.pos_traits + pref.neu_traits + pref.neg_traits)
- points_left -= traits_costs[T]
+ return ..()
- var/traits_left = pref.max_traits - (pref.pos_traits.len + pref.neg_traits.len)
-
- var/trait_choice
- var/done = FALSE
- while(!done)
- var/message = "\[Remaining: [points_left] points, [traits_left] traits\] Select a trait to read the description and see the cost."
- trait_choice = tgui_input_list(user, message,"Pick a trait", nicelist)
- if(!trait_choice)
- done = TRUE
- if(trait_choice in nicelist)
- var/datum/trait/path = nicelist[trait_choice]
- var/choice = alert("\[Cost:[initial(path.cost)]\] [initial(path.desc)]",initial(path.name),"Take Trait","Cancel","Go Back")
- if(choice == "Cancel")
- trait_choice = null
- if(choice != "Go Back")
- done = TRUE
-
- if(!trait_choice)
- return PREFERENCES_REFRESH
- else if(trait_choice in nicelist)
- var/datum/trait/path = nicelist[trait_choice]
- var/datum/trait/instance = all_traits[path]
-
- var/conflict = FALSE
-
- // if(pref.species in instance.banned_species)
- // tgui_alert_async(usr, "The trait you've selected cannot be taken by the species you've chosen!", "Error")
- // return PREFERENCES_REFRESH
-
- if( LAZYLEN(instance.allowed_species) && !(pref.real_species_name() in instance.allowed_species))
- tgui_alert_async(usr, "The trait you've selected cannot be taken by the species you've chosen!", "Error")
- return PREFERENCES_REFRESH
- if(trait_choice in pref.pos_traits + pref.neu_traits + pref.neg_traits)
- conflict = instance.name
-
- varconflict:
- for(var/P in pref.pos_traits + pref.neu_traits + pref.neg_traits)
- var/datum/trait/instance_test = all_traits[P]
- if(path in instance_test.excludes)
- conflict = instance_test.name
- break varconflict
-
- for(var/V in instance.var_changes)
- if(V in instance_test.var_changes)
- conflict = instance_test.name
- break varconflict
-
- if(conflict)
- alert("You cannot take this trait and [conflict] at the same time. \
- Please remove that trait, or pick another trait to add.","Error")
- return PREFERENCES_REFRESH
-
- if(mode == ALL_MODE)
- if(instance.cost < 0)
- mylist = pref.neg_traits
- else
- mylist = pref.neu_traits
-
- mylist += path
- return PREFERENCES_REFRESH
+/datum/category_item/player_setup_item/vore/traits/proc/trait_exclusions(var/possible_traits)
+ // NOTE: This should ideally be cached
+ var/list/var_exclude_groups = list()
+ for (var/trait_path in possible_traits)
+ var/datum/trait/trait = possible_traits[trait_path]
+ for(var/v in trait.var_changes)
+ if (!var_exclude_groups[v])
+ var_exclude_groups[v] = list()
+ var_exclude_groups[v] += trait_path
- return ..()
+ var/list/explicit_excludes = list()
+ for (var/trait_path in possible_traits)
+ explicit_excludes[trait_path] = list()
+
+ for (var/trait_path in possible_traits)
+ var/datum/trait/trait = possible_traits[trait_path]
+ for(var/other_path in trait.excludes)
+ var other = possible_traits[other_path]
+
+ if (other)
+ explicit_excludes[trait_path] += list(other_path)
+ explicit_excludes[other_path] += list(trait_path)
+
+ var/list/total_excludes = list()
+ for (var/trait_path in possible_traits)
+ var/datum/trait/trait = possible_traits[trait_path]
+ total_excludes[trait_path] = list()
+
+ for (var/other_path in explicit_excludes[trait_path])
+ total_excludes[trait_path][other_path] = TRUE
+
+ for (var/v in trait.var_changes)
+ for (var/other_path in var_exclude_groups[v])
+ if (other_path == trait_path)
+ continue
+
+ total_excludes[trait_path][other_path] = TRUE
+
+ return total_excludes
+
+/datum/category_item/player_setup_item/vore/traits/proc/compute_available_traits()
+ var/species = pref.real_species_name()
+ var/species_id = pref.real_species_id()
+ var/possible_traits = positive_traits.Copy() + neutral_traits.Copy() + negative_traits.Copy()
+
+ var/list/available_traits = list()
+
+ var/exclusions = trait_exclusions(possible_traits)
+
+ for (var/trait_path in possible_traits)
+ var/datum/trait/trait = possible_traits[trait_path]
+ var/datum/traits_available_trait/available_trait = new
+
+ available_trait.internal_name = trait_path
+ available_trait.real_record = trait
+ available_trait.cost = trait.cost
+
+ if (LAZYLEN(trait.allowed_species) && !(species in trait.allowed_species))
+ available_trait.forbidden_reason = "This trait is not allowed for your species."
+
+ // NOTE: For some reason, this is only actually used for neutral traits??? Weird.
+ if (species_id != SPECIES_ID_CUSTOM)
+ if (trait_path in positive_traits || (trait_path in neutral_traits && trait.custom_only))
+ available_trait.forbidden_reason = "This trait is only allowed for custom species."
+
+ available_trait.exclusive_with = exclusions[trait_path]
+
+ available_traits[trait_path] = available_trait
+
+ return available_traits
+
+/datum/category_item/player_setup_item/vore/traits/proc/compute_constraints()
+ var/datum/traits_constraints/constraints = new
+
+ constraints.max_traits = pref.max_traits
+ constraints.max_points = pref.starting_trait_points
+
+ return constraints
+
+/datum/category_item/player_setup_item/vore/traits/proc/compute_tgui_data(mode, list/available_traits, datum/traits_constraints/constraints)
+ var/initial_traits_json = list()
+ var/list/trait_groups_json = list()
+ var/list/available_traits_json = list()
+ var/constraints_json = list()
+
+ initial_traits_json = pref.neg_traits.Copy() + pref.neu_traits.Copy() + pref.pos_traits.Copy()
+
+ for (var/trait_group_path in all_trait_groups)
+ var/datum/trait_group/trait_group = all_trait_groups[trait_group_path]
+ var/list/trait_group_json = list()
+
+ trait_group_json["internal_name"] = trait_group_path
+ trait_group_json["name"] = trait_group.name
+ trait_group_json["description"] = trait_group.desc
+ trait_group_json["sort_key"] = trait_group.sort_key
+
+ trait_groups_json[trait_group_path] = trait_group_json
+
+ for (var/trait_path in available_traits)
+ var/datum/traits_available_trait/trait = available_traits[trait_path]
+ var/list/available_trait_json = list()
+
+ available_trait_json["internal_name"] = trait.internal_name
+ available_trait_json["name"] = trait.real_record.name
+ available_trait_json["group"] = trait.real_record.group
+ available_trait_json["group_short_name"] = trait.real_record.group_short_name
+ available_trait_json["sort_key"] = trait.real_record.sort_key
+ available_trait_json["description"] = trait.real_record.desc
+ available_trait_json["cost"] = trait.cost
+ available_trait_json["forbidden_reason"] = trait.forbidden_reason
+ available_trait_json["show_when_forbidden"] = trait.real_record.show_when_forbidden
+ available_trait_json["exclusive_with"] = trait.exclusive_with
+
+ available_traits_json[trait_path] = available_trait_json
+
+ // NOTE: Confusingly, only positive or negative traits are counted towards pref.max_traits
+ constraints_json["max_traits"] = constraints.max_traits
+ constraints_json["max_points"] = constraints.max_points
+
+ . = list()
+ .["initial_traits"] = initial_traits_json
+ .["trait_groups"] = trait_groups_json
+ .["available_traits"] = available_traits_json
+ .["constraints"] = constraints_json
+
+/datum/category_item/player_setup_item/vore/traits/proc/apply_traits(allow_invalid, new_trait_paths, available_traits, datum/traits_constraints/constraints)
+ // allow_invalid: used by sanitize_character()
+ // if true, try as hard as possible to fix the traits rather than rejecting the update if it's bad
+
+ // dedupe traits and deal with exclusion, forbiddenness, and so on
+ // new traits
+ var/list/traits_to_apply = list()
+ var/list/excluded = list()
+
+ // set to true if the input is invalid
+ var/input_was_invalid = FALSE
+
+ for (var/new_trait_path in new_trait_paths)
+ var/datum/traits_available_trait/new_trait_record = available_traits[new_trait_path]
+ if (!new_trait_record)
+ input_was_invalid = TRUE
+ continue
+
+ if(new_trait_record.forbidden_reason)
+ // skip forbidden traits
+ input_was_invalid = TRUE
+ continue
+
+ if(excluded[new_trait_path])
+ // and excluded traits
+ input_was_invalid = TRUE
+ continue
+
+ // each trait excludes itself (to deduplicate)
+ excluded[new_trait_path] = TRUE
+ for(var/i in new_trait_record.exclusive_with)
+ excluded[i] = TRUE
+
+ traits_to_apply += list(new_trait_path)
+
+ var/n_traits = 0
+ var/total_cost = 0
+ for (var/new_trait_path in traits_to_apply)
+ // neutral traits don't count towards number
+ if (new_trait_path in neutral_traits)
+ continue
+
+ n_traits += 1
+
+ for (var/new_trait_path in traits_to_apply)
+ var/datum/traits_available_trait/new_trait_record = available_traits[new_trait_path]
+ total_cost += new_trait_record.cost
+
+ if (n_traits > constraints.max_traits)
+ input_was_invalid = TRUE
+
+ if (total_cost > constraints.max_points)
+ input_was_invalid = TRUE
+
+ if (input_was_invalid)
+ if (!allow_invalid)
+ return
+
+ pref.pos_traits = list()
+ pref.neu_traits = list()
+ pref.neg_traits = list()
+ for (var/trait in traits_to_apply)
+ if (positive_traits[trait])
+ pref.pos_traits += trait
+ else if (neutral_traits[trait])
+ pref.neu_traits += trait
+ else if (negative_traits[trait])
+ pref.neg_traits += trait
+ else
+ // ???: Should this alert somehow?
+
+/datum/category_item/player_setup_item/vore/traits/proc/tgui_trait_select(mob/user, trait_data)
+ var/datum/tgui_trait_selector/selector = new(user, trait_data)
+
+ selector.ui_interact(user)
+ selector.wait()
+ if (selector)
+ . = selector.submission
+ qdel(selector)
+
+
+/datum/tgui_trait_selector
+ /// The selector input data
+ var/list/input_data
+ /// The user's submitted trait choices
+ var/submission
+ /// Boolean field describing if the tgui_trait-selector was closed by the user.
+ var/closed
+
+/datum/tgui_trait_selector/New(mob/user, input_data)
+ src.input_data = input_data
+
+/datum/tgui_trait_selector/Destroy()
+ SStgui.close_uis(src)
+ . = ..()
+
+/datum/tgui_trait_selector/proc/wait()
+ while (!submission && !closed)
+ stoplag(1)
+
+/datum/tgui_trait_selector/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if (!ui)
+ ui = new (user, src, "TraitSelectorModal")
+ ui.open()
+
+/datum/tgui_trait_selector/on_ui_close(mob/user, datum/tgui/ui, embedded)
+ . = ..()
+ closed = TRUE
+
+/datum/tgui_trait_selector/ui_state()
+ return GLOB.always_state
+
+/datum/tgui_trait_selector/ui_static_data(mob/user, datum/tgui/ui)
+ . = src.input_data
+
+/datum/tgui_trait_selector/ui_act(action, list/params, datum/tgui/ui)
+ . = ..()
+ if (.)
+ return
+ switch(action)
+ if("submit")
+ if (!validate_and_use_submission(params["entry"]))
+ return
+ closed = TRUE
+ SStgui.close_uis(src)
+ return TRUE
+ if("cancel")
+ closed = TRUE
+ SStgui.close_uis(src)
+ return TRUE
+
+/datum/tgui_trait_selector/proc/validate_and_use_submission(submission_text)
+ // Validate basic format: actual validity of choices is up to our host
+ var/possible_submission = json_decode(submission_text)
+ if (!islist(possible_submission))
+ return
+
+ var/traits = possible_submission["traits"]
+ if (traits == null) // distinguish null from empty list
+ return
+
+ var/trait_paths = list()
+ for (var/trait_text in traits) // list must be all text
+ if (!istext(trait_text))
+ return
+
+ var/trait_path = text2path(trait_text)
+ if (!trait_path)
+ return
+ trait_paths += trait_path
+
+ submission = trait_paths
+ return TRUE
#undef POSITIVE_MODE
#undef NEUTRAL_MODE
diff --git a/code/modules/species/shadekin/shadekin_traits.dm b/code/modules/species/shadekin/shadekin_traits.dm
index 4fcd0acdac3..392f0a98c96 100644
--- a/code/modules/species/shadekin/shadekin_traits.dm
+++ b/code/modules/species/shadekin/shadekin_traits.dm
@@ -2,7 +2,7 @@
allowed_species = list(SPECIES_SHADEKIN)
var/color = BLUE_EYES
name = "Shadekin Blue Adaptation"
- desc = "Makes your shadekin adapted as a Blue eyed kin! This gives you good energy regeneration in darkness, decreased regeneration in the light and unchanged health!"
+ desc = "Good energy regeneration in darkness, decreased regeneration in the light and unchanged health!"
cost = 0
custom_only = FALSE
var_changes = list(
@@ -18,10 +18,14 @@
)
)
+ group = /datum/trait_group/shadekin
+ group_short_name = "Blue-Eyed"
+ show_when_forbidden = FALSE
+
/datum/trait/kintype/red
name = "Shadekin Red Adaptation"
color = RED_EYES
- desc = "Makes your shadekin adapted as a Red eyed kin! This gives you minimal energy regeneration in darkness, good regeneration in the light and increased health!"
+ desc = "Minimal energy regeneration in darkness, good regeneration in the light and increased health!"
var_changes = list(
"total_health" = 200,
"energy_light" = 1,
@@ -35,10 +39,14 @@
)
)
+ group = /datum/trait_group/shadekin
+ group_short_name = "Red-Eyed"
+ show_when_forbidden = FALSE
+
/datum/trait/kintype/purple
name = "Shadekin Purple Adaptation"
color = PURPLE_EYES
- desc = "Makes your shadekin adapted as a Purple eyed kin! This gives you very good energy regeneration in darkness, minor degeneration in the light and increased health!"
+ desc = "Very good energy regeneration in darkness, minor degeneration in the light and increased health!"
var_changes = list(
"total_health" = 150,
"energy_light" = -0.5,
@@ -52,10 +60,14 @@
)
)
+ group = /datum/trait_group/shadekin
+ group_short_name = "Purple-Eyed"
+ show_when_forbidden = FALSE
+
/datum/trait/kintype/yellow
name = "Shadekin Yellow Adaptation"
color = YELLOW_EYES
- desc = "Makes your shadekin adapted as a Yellow eyed kin! This gives you the highest energy regeneration in darkness, high degeneration in the light and unchanged health!"
+ desc = "Highest energy regeneration in darkness, high degeneration in the light and unchanged health!"
var_changes = list(
"total_health" = 100,
"energy_light" = -1,
@@ -69,10 +81,14 @@
)
)
+ group = /datum/trait_group/shadekin
+ group_short_name = "Yellow-Eyed"
+ show_when_forbidden = FALSE
+
/datum/trait/kintype/green
name = "Shadekin Green Adaptation"
color = GREEN_EYES
- desc = "Makes your shadekin adapted as a Green eyed kin! This gives you high energy regeneration in darkness, minor regeneration in the light and unchanged health!"
+ desc = "High energy regeneration in darkness, minor regeneration in the light and unchanged health!"
var_changes = list(
"total_health" = 100,
"energy_light" = 0.25,
@@ -86,10 +102,14 @@
)
)
+ group = /datum/trait_group/shadekin
+ group_short_name = "Green-Eyed"
+ show_when_forbidden = FALSE
+
/datum/trait/kintype/orange
name = "Shadekin Orange Adaptation"
color = ORANGE_EYES
- desc = "Makes your shadekin adapted as a Orange eyed kin! This gives you good energy regeneration in darkness, small degeneration in the light and increased health!"
+ desc = "Good energy regeneration in darkness, small degeneration in the light and increased health!"
var_changes = list(
"total_health" = 175,
"energy_light" = -0.5,
@@ -103,6 +123,10 @@
)
)
+ group = /datum/trait_group/shadekin
+ group_short_name = "Orange-Eyed"
+ show_when_forbidden = FALSE
+
/datum/trait/kintype/apply(datum/species/shadekin/S, mob/living/carbon/human/H)
if (istype(S))
..(S,H)
diff --git a/tgui/packages/tgui/interfaces/TraitSelectorModal.tsx b/tgui/packages/tgui/interfaces/TraitSelectorModal.tsx
new file mode 100644
index 00000000000..37dc8b36fa5
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/TraitSelectorModal.tsx
@@ -0,0 +1,303 @@
+import { InputButtons } from './common/InputButtons';
+import { Box, Button, Input, LabeledList, Section, Stack, Table } from '../components';
+import { useBackend, useLocalState } from '../backend';
+import { Window } from '../layouts';
+
+type TraitSelectorInputData = {
+ initial_traits: string[],
+ trait_groups: Record,
+ available_traits: Record,
+ constraints: ConstraintsData,
+};
+
+type TraitSelectorSubmissionData = {
+ traits: string[],
+};
+
+type ConstraintsData = {
+ max_traits: number,
+ max_points: number
+};
+
+type TraitGroupData = {
+ internal_name: string
+ name: string
+ description: string,
+
+ sort_key?: string,
+
+ // populated in later logic
+ items?: AvailableTraitData[]
+}
+
+type AvailableTraitData = {
+ internal_name: string,
+ name: string,
+ group?: string,
+ group_short_name?: string,
+ sort_key?: string,
+ description: string,
+ cost: number,
+ forbidden_reason?: string,
+ show_when_forbidden: number,
+
+ // list of traits that this trait can't occur with
+ exclusive_with: Record,
+
+ // populated in later logic
+ display_name: string,
+ show_name?: boolean,
+};
+
+export const TraitSelectorModal = (_, context) => {
+ const { act, data } = useBackend(context);
+
+ const containsLoosely = function (needle: string, haystack: string): boolean {
+ if (needle === "") { return true; }
+
+ return haystack.toLowerCase().indexOf(needle.toLowerCase().trim()) !== -1;
+ };
+
+ const [submission, setSubmission] = useLocalState(
+ context, "submission", { traits: data.initial_traits }
+ );
+
+ const [searchQuery, setSearchQuery] = useLocalState(
+ context, "searchQuery", ""
+ );
+
+ const stringSubmission = JSON.stringify(submission);
+
+ // -- build add/remove callback --
+ const addRemover = (trait: string) => {
+ return () => {
+ let newTraits = [...submission.traits];
+ let ixExisting = newTraits.indexOf(trait);
+ if (ixExisting !== -1) {
+ newTraits.splice(ixExisting, 1);
+ } else {
+ newTraits.push(trait);
+ }
+ setSubmission({ traits: newTraits });
+ };
+ };
+
+
+ const generateTraitCards = () => {
+ // == Build groups ==
+ let groups = {};
+ for (let traitPath in data.available_traits) {
+ let trait = data.available_traits[traitPath];
+
+ // -- is the trait even showable? --
+ if (trait.forbidden_reason && !trait.show_when_forbidden) {
+ continue;
+ }
+
+ // -- it is: find or make its group --
+ let desired_group = trait.group ? data.trait_groups[trait.group] : undefined;
+ if (desired_group) {
+ let existing = groups[desired_group.internal_name];
+ if (!existing) {
+ existing = (groups[desired_group.internal_name] = {
+ name: desired_group.name,
+ description: desired_group.description,
+ sort_key: desired_group.sort_key ?? desired_group.name,
+ items: [],
+ });
+ }
+ existing.items.push({
+ ...trait,
+ display_name: trait.group_short_name ?? trait.name,
+ sort_key: trait.sort_key ?? trait.name,
+ show_name: true,
+ });
+ } else {
+ groups[trait.internal_name] = {
+ name: trait.name,
+ description: "",
+ sort_key: trait.sort_key ?? trait.name,
+ items: [{
+ ...trait,
+ display_name: trait.name,
+ sort_key: trait.sort_key ?? trait.name,
+ show_name: false,
+ }],
+ };
+ }
+ }
+
+ // == Sort the items in every group ==
+ for (let groupName in groups) {
+ groups[groupName].items.sort(
+ (x: AvailableTraitData, y: AvailableTraitData) => (x.sort_key ?? "").localeCompare(y.sort_key ?? "")
+ );
+ }
+
+ // == Sort the groups ==
+ let orderedGroups: TraitGroupData[] = [];
+ for (let groupName in groups) {
+ orderedGroups.push(groups[groupName]);
+ }
+ orderedGroups.sort(
+ (x, y) => (x.sort_key ?? "").localeCompare(y.sort_key ?? "")
+ );
+
+ // == Build cards from groups ==
+ let groupCards: any[] = [];
+ for (let group of orderedGroups) {
+ // -- can this group be shown? --
+ let canBeShown = (() => {
+ if (containsLoosely(searchQuery, group.name)) {
+ return true;
+ }
+
+ for (let i of group.items ?? []) {
+ if (containsLoosely(searchQuery, i.name)) {
+ return true;
+
+ }
+ if (i.group_short_name && containsLoosely(searchQuery, i.group_short_name)) {
+ return true;
+ }
+ }
+ return false;
+ })();
+
+ if (!canBeShown) {
+ continue;
+ }
+
+ // -- OK, it can be shown --
+ let itemCards: any[] = [];
+
+ for (let item of group.items ?? []) {
+ // -- our full description --
+ let description = (
+ <>
+ {item.show_name && {item.display_name}:} {" "}
+ {item.description}
+ >
+ );
+
+ // -- whether we're selected --
+ let isSelected = submission.traits.indexOf(item.internal_name) !== -1;
+
+ // -- whether we're disabled --
+ let disabledReason = item.forbidden_reason;
+ if (!disabledReason) {
+ for (let selectedTrait of submission.traits) {
+ let rec = data.available_traits[selectedTrait];
+ if (rec.exclusive_with[item.internal_name]) {
+ disabledReason = "This trait is exclusive with " + rec.name + ".";
+ break;
+ }
+ }
+ }
+
+ // -- we can always drop a trait --
+ let isDisabled = !!disabledReason;
+ if (isSelected) {
+ isDisabled = false;
+ disabledReason = undefined;
+ }
+
+ // -- build the actual button. whew! --
+ let selectButton = (
+
+ );
+
+ itemCards.push(
+
+
+ {description}
+
+
+ {item.cost.toString()}
+
+
+ {selectButton}
+
+
+ );
+ }
+
+ let groupCard = (
+
+ {group.description && {group.description}}
+
+
+ );
+ groupCards.push(groupCard);
+ }
+ return groupCards;
+ };
+
+ let n_traits = 0;
+ let n_points = 0;
+ for (let t of submission.traits) {
+ let rec = data.available_traits[t];
+ if (rec.cost !== 0) { n_traits += 1; }
+ n_points += rec.cost;
+ }
+
+ let max_traits = data.constraints.max_traits;
+ let max_points = data.constraints.max_points;
+
+ let traitsSatisfactory = n_traits <= max_traits;
+ let pointsSatisfactory = n_points <= max_points;
+ let satisfactory = traitsSatisfactory && pointsSatisfactory;
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/common/InputButtons.tsx b/tgui/packages/tgui/interfaces/common/InputButtons.tsx
index c5c55fadbc5..008f68722e8 100644
--- a/tgui/packages/tgui/interfaces/common/InputButtons.tsx
+++ b/tgui/packages/tgui/interfaces/common/InputButtons.tsx
@@ -9,12 +9,13 @@ type InputButtonsData = {
type InputButtonsProps = {
readonly input: string | number;
readonly message?: string;
+ readonly goodDisabled?: boolean;
};
export const InputButtons = (props: InputButtonsProps, context) => {
const { act, data } = useBackend(context);
const { large_buttons, swapped_buttons } = data;
- const { input, message } = props;
+ const { input, message, goodDisabled } = props;
const submitButton = (
);
diff --git a/tgui/packages/tgui/styles/components/Button.scss b/tgui/packages/tgui/styles/components/Button.scss
index f48ea8fa94b..6c5c5fb1c2d 100644
--- a/tgui/packages/tgui/styles/components/Button.scss
+++ b/tgui/packages/tgui/styles/components/Button.scss
@@ -127,6 +127,18 @@ $bg-map: colors.$bg-map !default;
color: $color-transparent-text;
}
+ .Button--color--transparent-with-disabling {
+ @include button-color(base.$color-bg);
+ background-color: rgba(base.$color-bg, 0);
+ }
+
+.Button--disabled.Button--color--transparent-with-disabling {
+ /* the base behavior for disabled buttons is to be grayed out, _even when transparent_ */
+ /* the transparent-with-disabling class handles this correctly */
+ background-color: rgba(base.$color-bg, 0) !important;
+ color: $color-transparent-text !important;
+}
+
.Button--disabled {
background-color: $color-disabled !important;
}