Skip to content

Commit

Permalink
hair/facial hair character setup picker (#7493)
Browse files Browse the repository at this point in the history
video


https://github.com/user-attachments/assets/36c68a58-2ecf-4847-85db-ccf519ff42d4



🆑 harryob, drathek
ui: replaces the lists for hair/facial hair with images in a separate
picker
ui: replaces the eye color picker with a tgui color picker
code: added unit test for duplicate sprite accessories (hair)
/🆑

color picker modal is from
BeeStation/BeeStation-Hornet#9417 thank you

---------

Co-authored-by: Drathek <[email protected]>
  • Loading branch information
harryob and Drulikar authored Nov 7, 2024
1 parent 0d310a5 commit 0e79267
Show file tree
Hide file tree
Showing 19 changed files with 2,053 additions and 103 deletions.
171 changes: 171 additions & 0 deletions code/modules/client/hair_picker.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/datum/hair_picker/ui_static_data(mob/user)
. = ..()

.["hair_icon"] = /datum/sprite_accessory/hair::icon
.["facial_hair_icon"] = /datum/sprite_accessory/facial_hair::icon


/datum/hair_picker/ui_data(mob/user)
. = ..()

var/datum/preferences/prefs = user.client.prefs

.["hair_style"] = GLOB.hair_styles_list[prefs.h_style].icon_state
.["hair_color"] = rgb(prefs.r_hair, prefs.g_hair, prefs.b_hair)

.["hair_styles"] = list()
for(var/key in GLOB.hair_styles_list)
var/datum/sprite_accessory/hair/hair = GLOB.hair_styles_list[key]
if(!hair.selectable)
continue
if(!(prefs.species in hair.species_allowed))
continue

.["hair_styles"] += list(
list("name" = hair.name, "icon" = hair.icon_state)
)

.["facial_hair_style"] = GLOB.facial_hair_styles_list[prefs.f_style].icon_state
.["facial_hair_color"] = rgb(prefs.r_facial, prefs.g_facial, prefs.b_facial)

.["facial_hair_styles"] = list()
for(var/key in GLOB.facial_hair_styles_list)
var/datum/sprite_accessory/facial_hair/facial_hair = GLOB.facial_hair_styles_list[key]
if(!facial_hair.selectable)
continue
if(!(prefs.species in facial_hair.species_allowed))
continue
if(facial_hair.gender != NEUTER && prefs.gender != facial_hair.gender)
continue

.["facial_hair_styles"] += list(
list("name" = facial_hair.name, "icon" = facial_hair.icon_state)
)

.["gradient_available"] = !!(/datum/character_trait/hair_dye in prefs.traits)
.["gradient_style"] = prefs.grad_style
.["gradient_color"] = rgb(prefs.r_gradient, prefs.g_gradient, prefs.b_gradient)

.["gradient_styles"] = list()
for(var/key in GLOB.hair_gradient_list)
var/datum/sprite_accessory/hair_gradient/gradient = GLOB.hair_gradient_list[key]
if(!gradient.selectable)
continue
if(!(prefs.species in gradient.species_allowed))
continue

.["gradient_styles"] += gradient.name

/datum/hair_picker/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()

var/datum/preferences/prefs = ui.user.client.prefs

switch(action)
if("hair")
var/datum/sprite_accessory/hair/hair = GLOB.hair_styles_list[params["name"]]
if(!hair)
return

if(!hair.selectable)
return

if(!(prefs.species in hair.species_allowed))
return

prefs.h_style = params["name"]

if("hair_color")
var/param_color = params["color"]
if(!param_color)
return

var/r = hex2num(copytext(param_color, 2, 4))
var/g = hex2num(copytext(param_color, 4, 6))
var/b = hex2num(copytext(param_color, 6, 8))

if(!isnum(r) || !isnum(g) || !isnum(b))
return

prefs.r_hair = clamp(r, 0, 255)
prefs.g_hair = clamp(g, 0, 255)
prefs.b_hair = clamp(b, 0, 255)

if("facial_hair")
var/datum/sprite_accessory/facial_hair/facial_hair = GLOB.facial_hair_styles_list[params["name"]]
if(!facial_hair)
return

if(!facial_hair.selectable)
return

if(!(prefs.species in facial_hair.species_allowed))
return

if(facial_hair.gender != NEUTER && prefs.gender != facial_hair.gender)
return

prefs.f_style = params["name"]

if("facial_hair_color")
var/param_color = params["color"]
if(!param_color)
return

var/r = hex2num(copytext(param_color, 2, 4))
var/g = hex2num(copytext(param_color, 4, 6))
var/b = hex2num(copytext(param_color, 6, 8))

if(!isnum(r) || !isnum(g) || !isnum(b))
return

prefs.r_facial = clamp(r, 0, 255)
prefs.g_facial = clamp(g, 0, 255)
prefs.b_facial = clamp(b, 0, 255)

if("gradient")
var/datum/sprite_accessory/hair_gradient/gradient = GLOB.hair_gradient_list[params["name"]]
if(!gradient)
return

if(!gradient.selectable)
return

if(!(prefs.species in gradient.species_allowed))
return

prefs.grad_style = params["name"]

if("gradient_color")
var/param_color = params["color"]
if(!param_color)
return

var/r = hex2num(copytext(param_color, 2, 4))
var/g = hex2num(copytext(param_color, 4, 6))
var/b = hex2num(copytext(param_color, 6, 8))

if(!isnum(r) || !isnum(g) || !isnum(b))
return

prefs.r_gradient = clamp(r, 0, 255)
prefs.g_gradient = clamp(g, 0, 255)
prefs.b_gradient = clamp(b, 0, 255)

prefs.ShowChoices(ui.user)
return TRUE

/datum/hair_picker/tgui_interact(mob/user, datum/tgui/ui)
. = ..()

ui = SStgui.try_update_ui(user, src, ui)

if(!ui)
ui = new(user, src, "HairPicker", "Hair Picker")
ui.open()
ui.set_autoupdate(FALSE)

winset(user, ui.window.id, "focus=true")

/datum/hair_picker/ui_state(mob/user)
return GLOB.always_state
131 changes: 39 additions & 92 deletions code/modules/client/preferences.dm
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ GLOBAL_LIST_INIT(bgstate_options, list(
var/atom/movable/screen/rotate/alt/rotate_left
var/atom/movable/screen/rotate/rotate_right

var/static/datum/body_picker/picker = new
var/static/datum/hair_picker/hair_picker = new
var/static/datum/body_picker/body_picker = new

//doohickeys for savefiles
var/path
Expand Down Expand Up @@ -361,30 +362,23 @@ GLOBAL_LIST_INIT(bgstate_options, list(

dat += "<div id='column2'>"
dat += "<h2><b><u>Hair and Eyes:</u></b></h2>"
dat += "<b>Hair:</b> "
dat += "<a href='?_src_=prefs;preference=h_style;task=input'><b>[h_style]</b></a>"
dat += "<b>Hair:</b> [h_style]"
dat += " | "
dat += "<a href='?_src_=prefs;preference=hair;task=input'>"
dat += "<b>Color</b> <span class='square' style='background-color: #[num2hex(r_hair, 2)][num2hex(g_hair, 2)][num2hex(b_hair)];'></span>"
dat += "</a>"
dat += "<span class='square' style='background-color: #[num2hex(r_hair, 2)][num2hex(g_hair, 2)][num2hex(b_hair)];'></span>"
dat += "<br>"

dat += "<b>Facial Hair:</b> [f_style]"
dat += " | "
dat += "<span class='square' style='background-color: #[num2hex(r_facial, 2)][num2hex(g_facial, 2)][num2hex(b_facial)];'></span>"
dat += "<br>"

if(/datum/character_trait/hair_dye in traits)
dat += "<b>Hair Gradient:</b> "
dat += "<a href='?_src_=prefs;preference=grad_style;task=input'><b>[grad_style]</b></a>"
dat += "<b>Hair Gradient:</b> [grad_style]"
dat += " | "
dat += "<a href='?_src_=prefs;preference=grad;task=input'>"
dat += "<b>Color</b> <span class='square' style='background-color: #[num2hex(r_gradient, 2)][num2hex(g_gradient, 2)][num2hex(b_gradient)];'></span>"
dat += "</a>"
dat += "<span class='square' style='background-color: #[num2hex(r_gradient, 2)][num2hex(g_gradient, 2)][num2hex(b_gradient)];'></span>"
dat += "<br>"

dat += "<b>Facial Hair:</b> "
dat += "<a href='?_src_=prefs;preference=f_style;task=input'><b>[f_style]</b></a>"
dat += " | "
dat += "<a href='?_src_=prefs;preference=facial;task=input'>"
dat += "<b>Color</b> <span class='square' style='background-color: #[num2hex(r_facial, 2)][num2hex(g_facial, 2)][num2hex(b_facial)];'></span>"
dat += "</a>"
dat += "<br>"
dat += "<b>Edit Hair:</b> <a href='?_src_=prefs;preference=hair;task=input'><b>Picker</b></a><br><br>"

dat += "<b>Eye:</b> "
dat += "<a href='?_src_=prefs;preference=eyes;task=input'>"
Expand Down Expand Up @@ -1531,83 +1525,13 @@ GLOBAL_LIST_INIT(bgstate_options, list(
metadata = strip_html(new_metadata)

if("hair")
if(species == "Human")
var/new_hair = input(user, "Choose your character's hair color:", "Character Preference", rgb(r_hair, g_hair, b_hair)) as color|null
if(new_hair)
r_hair = hex2num(copytext(new_hair, 2, 4))
g_hair = hex2num(copytext(new_hair, 4, 6))
b_hair = hex2num(copytext(new_hair, 6, 8))

if("h_style")
var/list/valid_hairstyles = list()
for(var/hairstyle in GLOB.hair_styles_list)
var/datum/sprite_accessory/sprite_accessory = GLOB.hair_styles_list[hairstyle]
if( !(species in sprite_accessory.species_allowed))
continue
if(!sprite_accessory.selectable)
continue

valid_hairstyles[hairstyle] = GLOB.hair_styles_list[hairstyle]
valid_hairstyles = sortList(valid_hairstyles)

var/new_h_style = input(user, "Choose your character's hair style:", "Character Preference") as null|anything in valid_hairstyles
if(new_h_style)
h_style = new_h_style

if("grad")
if(species == "Human")
var/new_hair_grad = input(user, "Choose your character's hair gradient color:", "Character Preference", rgb(r_gradient, g_gradient, b_gradient)) as color|null
if(new_hair_grad)
r_gradient = hex2num(copytext(new_hair_grad, 2, 4))
g_gradient = hex2num(copytext(new_hair_grad, 4, 6))
b_gradient = hex2num(copytext(new_hair_grad, 6, 8))

if("grad_style")
var/list/valid_hair_gradients = list()
for(var/hair_gradient in GLOB.hair_gradient_list)
var/datum/sprite_accessory/sprite_accessory = GLOB.hair_gradient_list[hair_gradient]
if(!(species in sprite_accessory.species_allowed))
continue
if(!sprite_accessory.selectable)
continue
valid_hair_gradients[hair_gradient] = GLOB.hair_gradient_list[hair_gradient]
valid_hair_gradients = sortList(valid_hair_gradients)

var/new_h_gradient_style = input(user, "Choose your character's hair gradient style:", "Character Preference") as null|anything in valid_hair_gradients
if(new_h_gradient_style)
grad_style = new_h_gradient_style
hair_picker.tgui_interact(user)
return

if ("body")
picker.tgui_interact(user)
body_picker.tgui_interact(user)
return

if("facial")
var/new_facial = input(user, "Choose your character's facial-hair color:", "Character Preference", rgb(r_facial, g_facial, b_facial)) as color|null
if(new_facial)
r_facial = hex2num(copytext(new_facial, 2, 4))
g_facial = hex2num(copytext(new_facial, 4, 6))
b_facial = hex2num(copytext(new_facial, 6, 8))

if("f_style")
var/list/valid_facialhairstyles = list()
for(var/facialhairstyle in GLOB.facial_hair_styles_list)
var/datum/sprite_accessory/sprite_accessory = GLOB.facial_hair_styles_list[facialhairstyle]
if(gender == MALE && sprite_accessory.gender == FEMALE)
continue
if(gender == FEMALE && sprite_accessory.gender == MALE)
continue
if( !(species in sprite_accessory.species_allowed))
continue
if(!sprite_accessory.selectable)
continue

valid_facialhairstyles[facialhairstyle] = GLOB.facial_hair_styles_list[facialhairstyle]
valid_facialhairstyles = sortList(valid_facialhairstyles)

var/new_f_style = input(user, "Choose your character's facial-hair style:", "Character Preference") as null|anything in valid_facialhairstyles
if(new_f_style)
f_style = new_f_style

if("underwear")
var/list/underwear_options = gender == MALE ? GLOB.underwear_m : GLOB.underwear_f
var/old_gender = gender
Expand All @@ -1629,7 +1553,8 @@ GLOBAL_LIST_INIT(bgstate_options, list(
ShowChoices(user)

if("eyes")
var/new_eyes = input(user, "Choose your character's eye color:", "Character Preference", rgb(r_eyes, g_eyes, b_eyes)) as color|null
var/new_eyes = tgui_color_picker(user, "Choose your character's eye color:", "Character Preference", rgb(r_eyes, g_eyes, b_eyes))

if(new_eyes)
r_eyes = hex2num(copytext(new_eyes, 2, 4))
g_eyes = hex2num(copytext(new_eyes, 4, 6))
Expand Down Expand Up @@ -1793,6 +1718,11 @@ GLOBAL_LIST_INIT(bgstate_options, list(
underwear = sanitize_inlist(underwear, gender == MALE ? GLOB.underwear_m : GLOB.underwear_f, initial(underwear))
undershirt = sanitize_inlist(undershirt, gender == MALE ? GLOB.undershirt_m : GLOB.undershirt_f, initial(undershirt))

// Refresh hair picker
var/datum/tgui/picker_ui = SStgui.get_open_ui(user, hair_picker)
if(picker_ui)
picker_ui.send_update()

if("hear_adminhelps")
toggles_sound ^= SOUND_ADMINHELP

Expand Down Expand Up @@ -2002,6 +1932,14 @@ GLOBAL_LIST_INIT(bgstate_options, list(
load_character()
reload_cooldown = world.time + 50

// Refresh pickers
var/datum/tgui/picker_ui = SStgui.get_open_ui(user, hair_picker)
if(picker_ui)
picker_ui.send_update()
picker_ui = SStgui.get_open_ui(user, body_picker)
if(picker_ui)
picker_ui.send_update()

if("open_load_dialog")
if(!IsGuestKey(user.key))
open_load_dialog(user)
Expand All @@ -2017,6 +1955,15 @@ GLOBAL_LIST_INIT(bgstate_options, list(
var/mob/new_player/np = user
if(istype(np))
np.new_player_panel_proc()

// Refresh pickers
var/datum/tgui/picker_ui = SStgui.get_open_ui(user, hair_picker)
if(picker_ui)
picker_ui.send_update()
picker_ui = SStgui.get_open_ui(user, body_picker)
if(picker_ui)
picker_ui.send_update()

if("tgui_fancy")
tgui_fancy = !tgui_fancy
if("tgui_lock")
Expand Down
14 changes: 13 additions & 1 deletion code/modules/client/preferences_savefile.dm
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#define SAVEFILE_VERSION_MIN 8
#define SAVEFILE_VERSION_MAX 28
#define SAVEFILE_VERSION_MAX 29

//handles converting savefiles to new formats
//MAKE SURE YOU KEEP THIS UP TO DATE!
Expand Down Expand Up @@ -180,6 +180,18 @@
completed_tutorials += "marine_req_1"
S["completed_tutorials"] << tutorial_list_to_savestring()

if(savefile_version < 29)
var/hair_style = ""
S["hair_style_name"] >> hair_style

switch(hair_style)
if("Shoulder-length Hair Alt")
hair_style = "Long Fringe"
if("Long Hair Alt")
hair_style = "Longer Fringe"

S["hair_style_name"] << hair_style

savefile_version = SAVEFILE_VERSION_MAX
return 1

Expand Down
Loading

0 comments on commit 0e79267

Please sign in to comment.