Reduces species feature boilerplate (#93570)

## About The Pull Request

Rather than having 100 separate list variables on `SSaccessories`, have
1 list for all feature keys that are associated with sprite accessories

This way you can get a feature by doing
`SSaccessories.feature_list[key]`, instead of necessitating
`SSaccessories.ears_list`, `SSaccessories.tail_list`, etc.

This lets us cut back on a lot of boilerplate in prefs, dna, and organs

## Why It's Good For The Game

We can see the benefit in this example: This is all the code for horn
DNA, bodypart overlay, and preference
```dm
/datum/dna_block/feature/accessory/horn
	feature_key = FEATURE_HORNS
```
```dm
/datum/bodypart_overlay/mutant/horns
	layers = EXTERNAL_ADJACENT
	feature_key = FEATURE_HORNS
	dyable = TRUE

/datum/bodypart_overlay/mutant/horns/can_draw_on_bodypart(obj/item/bodypart/bodypart_owner)
	return !(bodypart_owner.owner?.obscured_slots & HIDEHAIR)
```
```dm
/datum/preference/choiced/species_feature/lizard_horns
	savefile_key = "feature_lizard_horns"
	savefile_identifier = PREFERENCE_CHARACTER
	category = PREFERENCE_CATEGORY_FEATURES
	main_feature_name = "Horns"
	should_generate_icons = TRUE
	relevant_organ = /obj/item/organ/horns

/datum/preference/choiced/species_feature/lizard_horns/icon_for(value)
	return generate_lizard_side_shot(get_accessory_for_value(value), "horns")
```

## Changelog

🆑 Melbert
refactor: Refactored species unique organs slightly, particularly how
they are set up at game start. Report any oddities, like invisible tails
or wings
/🆑

---------

Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com>
This commit is contained in:
MrMelbert
2025-10-30 03:46:32 +01:00
committed by GitHub
co-authored by Ghom
parent 340f9d5fe8
commit 585b02c325
30 changed files with 181 additions and 374 deletions
+38 -2
View File
@@ -109,7 +109,7 @@ GLOBAL_LIST_INIT(preference_entries_by_key, init_preference_entries_by_key())
/// If the selected species has this in its /datum/species/body_markings,
/// will show the feature as selectable.
var/relevant_body_markings = null
var/datum/bodypart_overlay/simple/body_marking/relevant_body_markings = null
/// If the selected species has this in its /datum/species/inherent_traits,
/// will show the feature as selectable.
@@ -117,7 +117,7 @@ GLOBAL_LIST_INIT(preference_entries_by_key, init_preference_entries_by_key())
/// If the selected species has this in its /datum/species/var/external_organs,
/// will show the feature as selectable.
var/relevant_organ = null
var/obj/item/organ/relevant_organ = null
/// If the selected species has this head_flag by default,
/// will show the feature as selectable.
@@ -443,6 +443,42 @@ GLOBAL_LIST_INIT(preference_entries_by_key, init_preference_entries_by_key())
return data
/// This subtype handles a lot of boilerplate for implementing a species preference tied to a feature key / sprite accessory
/datum/preference/choiced/species_feature
abstract_type = /datum/preference/choiced/species_feature
/// What feature key does this feature represent?
/// Does not need to be set, it will infer it from either relevant_organ or relevant_body_markings.
/// However you can set it manually if you have a more complex feature.
var/feature_key
/datum/preference/choiced/species_feature/New()
. = ..()
if(relevant_organ && relevant_organ::bodypart_overlay)
feature_key ||= relevant_organ::bodypart_overlay::feature_key
main_feature_name ||= capitalize(relevant_organ::name)
if(relevant_body_markings)
feature_key ||= relevant_body_markings::dna_feature_key
main_feature_name ||= "Body markings"
if(isnull(feature_key))
CRASH("`feature_key` was not set or inferable for [type]!")
/datum/preference/choiced/species_feature/init_possible_values()
return assoc_to_keys_features(get_accessory_list())
/datum/preference/choiced/species_feature/create_default_value()
return get_consistent_feature_entry(get_accessory_list())
/datum/preference/choiced/species_feature/apply_to_human(mob/living/carbon/human/target, value)
target.dna.features[feature_key] = value
/// Returns what acessory list to draw from
/datum/preference/choiced/species_feature/proc/get_accessory_list() as /list
return SSaccessories.feature_list[feature_key]
/// Get a specific accessory for a given value
/datum/preference/choiced/species_feature/proc/get_accessory_for_value(value)
return get_accessory_list()[value]
/// A preference that represents an RGB color of something.
/// Will give the value as 6 hex digits, without a hash.
/datum/preference/color
@@ -1,32 +1,13 @@
/datum/preference/choiced/tail_felinid
/datum/preference/choiced/species_feature/tail_felinid
savefile_key = "feature_human_tail" //savefile keys cannot be changed, blame whoever named them this way.
savefile_identifier = PREFERENCE_CHARACTER
category = PREFERENCE_CATEGORY_SECONDARY_FEATURES
can_randomize = FALSE
relevant_organ = /obj/item/organ/tail/cat
/datum/preference/choiced/tail_felinid/init_possible_values()
return assoc_to_keys_features(SSaccessories.tails_list_felinid)
/datum/preference/choiced/tail_felinid/apply_to_human(mob/living/carbon/human/target, value)
target.dna.features[FEATURE_TAIL_CAT] = value
/datum/preference/choiced/tail_felinid/create_default_value()
var/datum/sprite_accessory/tails/felinid/cat/tail = /datum/sprite_accessory/tails/felinid/cat
return initial(tail.name)
/datum/preference/choiced/felinid_ears
/datum/preference/choiced/species_feature/felinid_ears
savefile_key = "feature_human_ears" //savefile keys cannot be changed, blame whoever named them this way.
savefile_identifier = PREFERENCE_CHARACTER
category = PREFERENCE_CATEGORY_SECONDARY_FEATURES
can_randomize = FALSE
relevant_organ = /obj/item/organ/ears/cat
/datum/preference/choiced/felinid_ears/init_possible_values()
return assoc_to_keys_features(SSaccessories.ears_list)
/datum/preference/choiced/felinid_ears/apply_to_human(mob/living/carbon/human/target, value)
target.dna.features[FEATURE_EARS] = value
/datum/preference/choiced/felinid_ears/create_default_value()
return /datum/sprite_accessory/ears/cat::name
@@ -23,7 +23,7 @@
return final_icon
/datum/preference/choiced/lizard_body_markings
/datum/preference/choiced/species_feature/lizard_body_markings
savefile_key = "feature_lizard_body_markings"
savefile_identifier = PREFERENCE_CHARACTER
category = PREFERENCE_CATEGORY_FEATURES
@@ -31,11 +31,8 @@
should_generate_icons = TRUE
relevant_body_markings = /datum/bodypart_overlay/simple/body_marking/lizard
/datum/preference/choiced/lizard_body_markings/init_possible_values()
return assoc_to_keys_features(SSaccessories.lizard_markings_list)
/datum/preference/choiced/lizard_body_markings/icon_for(value)
var/datum/sprite_accessory/sprite_accessory = SSaccessories.lizard_markings_list[value]
/datum/preference/choiced/species_feature/lizard_body_markings/icon_for(value)
var/datum/sprite_accessory/sprite_accessory = get_accessory_for_value(value)
var/datum/universal_icon/final_icon = uni_icon('icons/mob/human/species/lizard/bodyparts.dmi', "lizard_chest_m")
@@ -54,10 +51,7 @@
return final_icon
/datum/preference/choiced/lizard_body_markings/apply_to_human(mob/living/carbon/human/target, value)
target.dna.features[FEATURE_LIZARD_MARKINGS] = value
/datum/preference/choiced/lizard_frills
/datum/preference/choiced/species_feature/lizard_frills
savefile_key = "feature_lizard_frills"
savefile_identifier = PREFERENCE_CHARACTER
category = PREFERENCE_CATEGORY_FEATURES
@@ -65,16 +59,10 @@
should_generate_icons = TRUE
relevant_organ = /obj/item/organ/frills
/datum/preference/choiced/lizard_frills/init_possible_values()
return assoc_to_keys_features(SSaccessories.frills_list)
/datum/preference/choiced/species_feature/lizard_frills/icon_for(value)
return generate_lizard_side_shot(get_accessory_for_value(value), "frills")
/datum/preference/choiced/lizard_frills/icon_for(value)
return generate_lizard_side_shot(SSaccessories.frills_list[value], "frills")
/datum/preference/choiced/lizard_frills/apply_to_human(mob/living/carbon/human/target, value)
target.dna.features[FEATURE_FRILLS] = value
/datum/preference/choiced/lizard_horns
/datum/preference/choiced/species_feature/lizard_horns
savefile_key = "feature_lizard_horns"
savefile_identifier = PREFERENCE_CHARACTER
category = PREFERENCE_CATEGORY_FEATURES
@@ -82,14 +70,8 @@
should_generate_icons = TRUE
relevant_organ = /obj/item/organ/horns
/datum/preference/choiced/lizard_horns/init_possible_values()
return assoc_to_keys_features(SSaccessories.horns_list)
/datum/preference/choiced/lizard_horns/icon_for(value)
return generate_lizard_side_shot(SSaccessories.horns_list[value], "horns")
/datum/preference/choiced/lizard_horns/apply_to_human(mob/living/carbon/human/target, value)
target.dna.features[FEATURE_HORNS] = value
/datum/preference/choiced/species_feature/lizard_horns/icon_for(value)
return generate_lizard_side_shot(get_accessory_for_value(value), "horns")
/datum/preference/choiced/lizard_legs
savefile_key = "feature_lizard_legs"
@@ -130,7 +112,7 @@
var/datum/species/species_type = preferences.read_preference(/datum/preference/choiced/species)
return initial(species_type.digitigrade_customization) == DIGITIGRADE_OPTIONAL
/datum/preference/choiced/lizard_snout
/datum/preference/choiced/species_feature/lizard_snout
savefile_key = "feature_lizard_snout"
savefile_identifier = PREFERENCE_CHARACTER
category = PREFERENCE_CATEGORY_FEATURES
@@ -138,38 +120,17 @@
should_generate_icons = TRUE
relevant_organ = /obj/item/organ/snout
/datum/preference/choiced/lizard_snout/init_possible_values()
return assoc_to_keys_features(SSaccessories.snouts_list)
/datum/preference/choiced/species_feature/lizard_snout/icon_for(value)
return generate_lizard_side_shot(get_accessory_for_value(value), "snout", include_snout = FALSE)
/datum/preference/choiced/lizard_snout/icon_for(value)
return generate_lizard_side_shot(SSaccessories.snouts_list[value], "snout", include_snout = FALSE)
/datum/preference/choiced/lizard_snout/apply_to_human(mob/living/carbon/human/target, value)
target.dna.features[FEATURE_SNOUT] = value
/datum/preference/choiced/lizard_spines
/datum/preference/choiced/species_feature/lizard_spines
savefile_key = "feature_lizard_spines"
savefile_identifier = PREFERENCE_CHARACTER
category = PREFERENCE_CATEGORY_SECONDARY_FEATURES
relevant_organ = /obj/item/organ/spines
/datum/preference/choiced/lizard_spines/init_possible_values()
return assoc_to_keys_features(SSaccessories.spines_list)
/datum/preference/choiced/lizard_spines/apply_to_human(mob/living/carbon/human/target, value)
target.dna.features[FEATURE_SPINES] = value
/datum/preference/choiced/lizard_tail
/datum/preference/choiced/species_feature/lizard_tail
savefile_key = "feature_lizard_tail"
savefile_identifier = PREFERENCE_CHARACTER
category = PREFERENCE_CATEGORY_SECONDARY_FEATURES
relevant_organ = /obj/item/organ/tail/lizard
/datum/preference/choiced/lizard_tail/init_possible_values()
return assoc_to_keys_features(SSaccessories.tails_list_lizard)
/datum/preference/choiced/lizard_tail/apply_to_human(mob/living/carbon/human/target, value)
target.dna.features[FEATURE_TAIL_LIZARD] = value
/datum/preference/choiced/lizard_tail/create_default_value()
return /datum/sprite_accessory/tails/lizard/smooth::name
@@ -1,15 +1,6 @@
/datum/preference/choiced/monkey_tail
/datum/preference/choiced/species_feature/monkey_tail
savefile_key = "feature_monkey_tail"
savefile_identifier = PREFERENCE_CHARACTER
category = PREFERENCE_CATEGORY_SECONDARY_FEATURES
relevant_organ = /obj/item/organ/tail/monkey
can_randomize = FALSE
/datum/preference/choiced/monkey_tail/init_possible_values()
return assoc_to_keys_features(SSaccessories.tails_list_monkey)
/datum/preference/choiced/monkey_tail/apply_to_human(mob/living/carbon/human/target, value)
target.dna.features[FEATURE_TAIL_MONKEY] = value
/datum/preference/choiced/monkey_tail/create_default_value()
return /datum/sprite_accessory/tails/monkey/default::name
@@ -1,4 +1,4 @@
/datum/preference/choiced/moth_antennae
/datum/preference/choiced/species_feature/moth_antennae
savefile_key = "feature_moth_antennae"
savefile_identifier = PREFERENCE_CHARACTER
category = PREFERENCE_CATEGORY_FEATURES
@@ -6,10 +6,7 @@
should_generate_icons = TRUE
relevant_organ = /obj/item/organ/antennae
/datum/preference/choiced/moth_antennae/init_possible_values()
return assoc_to_keys_features(SSaccessories.moth_antennae_list)
/datum/preference/choiced/moth_antennae/icon_for(value)
/datum/preference/choiced/species_feature/moth_antennae/icon_for(value)
var/static/datum/universal_icon/moth_head
if (isnull(moth_head))
@@ -17,7 +14,7 @@
moth_head.blend_icon(uni_icon('icons/mob/human/human_face.dmi', "motheyes_l"), ICON_OVERLAY)
moth_head.blend_icon(uni_icon('icons/mob/human/human_face.dmi', "motheyes_r"), ICON_OVERLAY)
var/datum/sprite_accessory/antennae = SSaccessories.moth_antennae_list[value]
var/datum/sprite_accessory/antennae = get_accessory_for_value(value)
var/datum/universal_icon/icon_with_antennae = moth_head.copy()
icon_with_antennae.blend_icon(uni_icon(antennae.icon, "m_moth_antennae_[antennae.icon_state]_FRONT"), ICON_OVERLAY)
@@ -26,10 +23,7 @@
return icon_with_antennae
/datum/preference/choiced/moth_antennae/apply_to_human(mob/living/carbon/human/target, value)
target.dna.features[FEATURE_MOTH_ANTENNAE] = value
/datum/preference/choiced/moth_markings
/datum/preference/choiced/species_feature/moth_markings
savefile_key = "feature_moth_markings"
savefile_identifier = PREFERENCE_CHARACTER
category = PREFERENCE_CATEGORY_FEATURES
@@ -37,10 +31,7 @@
should_generate_icons = TRUE
relevant_body_markings = /datum/bodypart_overlay/simple/body_marking/moth
/datum/preference/choiced/moth_markings/init_possible_values()
return assoc_to_keys_features(SSaccessories.moth_markings_list)
/datum/preference/choiced/moth_markings/icon_for(value)
/datum/preference/choiced/species_feature/moth_markings/icon_for(value)
var/static/list/body_parts = list(
/obj/item/bodypart/head/moth,
/obj/item/bodypart/chest/moth,
@@ -58,7 +49,7 @@
moth_body.blend_icon(uni_icon('icons/mob/human/human_face.dmi', "motheyes_l"), ICON_OVERLAY)
moth_body.blend_icon(uni_icon('icons/mob/human/human_face.dmi', "motheyes_r"), ICON_OVERLAY)
var/datum/sprite_accessory/markings = SSaccessories.moth_markings_list[value]
var/datum/sprite_accessory/markings = get_accessory_for_value(value)
var/datum/universal_icon/icon_with_markings = moth_body.copy()
if (value != SPRITE_ACCESSORY_NONE)
@@ -76,10 +67,7 @@
return icon_with_markings
/datum/preference/choiced/moth_markings/apply_to_human(mob/living/carbon/human/target, value)
target.dna.features[FEATURE_MOTH_MARKINGS] = value
/datum/preference/choiced/moth_wings
/datum/preference/choiced/species_feature/moth_wings
savefile_key = "feature_moth_wings"
savefile_identifier = PREFERENCE_CHARACTER
category = PREFERENCE_CATEGORY_FEATURES
@@ -87,12 +75,6 @@
should_generate_icons = TRUE
relevant_organ = /obj/item/organ/wings/moth
/datum/preference/choiced/moth_wings/init_possible_values()
return assoc_to_keys_features(SSaccessories.moth_wings_list)
/datum/preference/choiced/moth_wings/icon_for(value)
var/datum/sprite_accessory/moth_wings = SSaccessories.moth_wings_list[value]
/datum/preference/choiced/species_feature/moth_wings/icon_for(value)
var/datum/sprite_accessory/moth_wings = get_accessory_for_value(value)
return uni_icon(moth_wings.icon, "m_moth_wings_[moth_wings.icon_state]_BEHIND")
/datum/preference/choiced/moth_wings/apply_to_human(mob/living/carbon/human/target, value)
target.dna.features[FEATURE_MOTH_WINGS] = value
@@ -1,11 +1,5 @@
/datum/preference/choiced/mushroom_cap
/datum/preference/choiced/species_feature/mushroom_cap
savefile_key = "feature_mushperson_cap"
savefile_identifier = PREFERENCE_CHARACTER
category = PREFERENCE_CATEGORY_SECONDARY_FEATURES
relevant_organ = /obj/item/organ/mushroom_cap
/datum/preference/choiced/mushroom_cap/init_possible_values()
return assoc_to_keys_features(SSaccessories.caps_list)
/datum/preference/choiced/mushroom_cap/apply_to_human(mob/living/carbon/human/target, value)
target.dna.features[FEATURE_MUSH_CAP] = value
@@ -1,4 +1,4 @@
/datum/preference/choiced/pod_hair
/datum/preference/choiced/species_feature/pod_hair
savefile_key = "feature_pod_hair"
savefile_identifier = PREFERENCE_CHARACTER
category = PREFERENCE_CATEGORY_FEATURES
@@ -6,11 +6,8 @@
should_generate_icons = TRUE
relevant_organ = /obj/item/organ/pod_hair
/datum/preference/choiced/pod_hair/init_possible_values()
return assoc_to_keys_features(SSaccessories.pod_hair_list)
/datum/preference/choiced/pod_hair/icon_for(value)
var/datum/sprite_accessory/pod_hair = SSaccessories.pod_hair_list[value]
/datum/preference/choiced/species_feature/pod_hair/icon_for(value)
var/datum/sprite_accessory/pod_hair = get_accessory_for_value(value)
var/datum/universal_icon/icon_with_hair = uni_icon('icons/mob/human/bodyparts_greyscale.dmi', "pod_head_m")
@@ -23,9 +20,3 @@
icon_with_hair.blend_color(COLOR_GREEN, ICON_MULTIPLY)
return icon_with_hair
/datum/preference/choiced/pod_hair/create_default_value()
return pick(assoc_to_keys_features(SSaccessories.pod_hair_list))
/datum/preference/choiced/pod_hair/apply_to_human(mob/living/carbon/human/target, value)
target.dna.features[FEATURE_POD_HAIR] = value