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}} + + {itemCards} +
+
+ ); + 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 ( + + +
+ + +
+ {generateTraitCards()} +
+
+ setSearchQuery(value)} + placeholder="Search..." + value={searchQuery} + /> + + + + + {(max_traits - n_traits).toString()} + {" "} (Neutral traits are free.) + + + + {(max_points - n_points).toString()} + + + + + + +
+
+
+
+ ); +}; 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; }