diff --git a/code/datums/records/record.dm b/code/datums/records/record.dm index 5441c8a7df0..07482a21911 100644 --- a/code/datums/records/record.dm +++ b/code/datums/records/record.dm @@ -177,6 +177,7 @@ species_type = locked_dna.species.type GLOB.manifest.locked += src + GLOB.name_to_appearance[name] = character_appearance // NOVA EDIT ADDITION - Cache these for Character Directory /datum/record/locked/Destroy() GLOB.manifest.locked -= src diff --git a/code/modules/mob/living/carbon/human/examine.dm b/code/modules/mob/living/carbon/human/examine.dm index ea468aa473f..37f152573aa 100644 --- a/code/modules/mob/living/carbon/human/examine.dm +++ b/code/modules/mob/living/carbon/human/examine.dm @@ -499,6 +499,8 @@ flavor_text_link = span_notice("\[Examine closely...\]") if (flavor_text_link) . += flavor_text_link + if (!face_obscured && !HAS_TRAIT(src, TRAIT_UNKNOWN) && client?.prefs.read_preference(/datum/preference/text/character_ad)) + . += span_notice("[t_He] [t_has] an ad in the character directory... \[Open directory?\]") //Temporary flavor text addition: if(temporary_flavor_text) diff --git a/modular_nova/master_files/code/modules/client/preferences/headshot.dm b/modular_nova/master_files/code/modules/client/preferences/headshot.dm index 877fa3de4cd..7861b3906cb 100644 --- a/modular_nova/master_files/code/modules/client/preferences/headshot.dm +++ b/modular_nova/master_files/code/modules/client/preferences/headshot.dm @@ -36,7 +36,7 @@ find_index = findtext(value, link_regex) if(find_index != 9) - to_chat(usr, span_warning("The image must be hosted on one of the following sites: 'Gyazo, Byond, Imgbox'")) + to_chat(usr, span_warning("The image must be hosted on one of the following sites: 'Gyazo (i.gyazo.com), Byond (files.byondhome.com), Imgbox (images2.imgbox.com)'")) return apply_headshot(value) diff --git a/modular_nova/master_files/code/modules/mob/dead/new_player/new_player.dm b/modular_nova/master_files/code/modules/mob/dead/new_player/new_player.dm new file mode 100644 index 00000000000..12ce2d72d55 --- /dev/null +++ b/modular_nova/master_files/code/modules/mob/dead/new_player/new_player.dm @@ -0,0 +1,5 @@ +/mob/dead/new_player/transfer_character() + if(iscyborg(new_character)) + var/mutable_appearance/character_appearance = new(new_character.appearance) + GLOB.name_to_appearance[new_character.real_name] = character_appearance // Cache this for Character Directory + return ..() diff --git a/modular_nova/master_files/code/modules/mob/living/sillicon/robot.dm b/modular_nova/master_files/code/modules/mob/living/sillicon/robot.dm index 147b7f38285..e892749ca5e 100644 --- a/modular_nova/master_files/code/modules/mob/living/sillicon/robot.dm +++ b/modular_nova/master_files/code/modules/mob/living/sillicon/robot.dm @@ -93,3 +93,10 @@ color = "#ffffffc2" pixel_y = -8 layer = ABOVE_MOB_LAYER + +// Update the borg's model appearance when they change models +/obj/item/robot_model/do_transform_animation() + . = ..() + var/mob/living/silicon/robot/cyborg = loc + var/mutable_appearance/character_appearance = new(cyborg.appearance) + GLOB.name_to_appearance[cyborg.real_name] = character_appearance // Cache this for Character Directory diff --git a/modular_nova/master_files/code/modules/mob_spawn/mob_spawn.dm b/modular_nova/master_files/code/modules/mob_spawn/mob_spawn.dm index 14350ff99ad..57b19421eef 100644 --- a/modular_nova/master_files/code/modules/mob_spawn/mob_spawn.dm +++ b/modular_nova/master_files/code/modules/mob_spawn/mob_spawn.dm @@ -41,6 +41,9 @@ else if (!isnull(spawned_human)) equip(spawned_human) + var/mutable_appearance/character_appearance = new(spawned_human.appearance) + GLOB.name_to_appearance[spawned_human.real_name] = character_appearance // Cache this for Character Directory + return spawned_mob /// This edit would cause somewhat ugly diffs, so I'm just replacing it. diff --git a/modular_nova/modules/character_directory/code/character_directory.dm b/modular_nova/modules/character_directory/code/character_directory.dm new file mode 100644 index 00000000000..c02c7dfc7f2 --- /dev/null +++ b/modular_nova/modules/character_directory/code/character_directory.dm @@ -0,0 +1,295 @@ +GLOBAL_DATUM(character_directory, /datum/character_directory) +GLOBAL_LIST_EMPTY(name_to_appearance) +#define READ_PREFS(target, pref) (target.client?.prefs?.read_preference(/datum/preference/pref)) + +// We want players to be able to decide whether they show up in the directory or not +/datum/preference/toggle/show_in_directory + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + default_value = TRUE + savefile_key = "show_in_directory" + savefile_identifier = PREFERENCE_PLAYER + +// The advertisement that you show to people looking through the directory +/datum/preference/text/character_ad + savefile_key = "character_ad" + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_identifier = PREFERENCE_CHARACTER + maximum_value_length = MAX_FLAVOR_LEN + +// TGUI gets angry if you don't define a default on text preferences +/datum/preference/text/character_ad/create_default_value() + return "" + +// Any text preference needs this for some reason +/datum/preference/text/character_ad/apply_to_human(mob/living/carbon/human/target, value, datum/preferences/preferences) + return FALSE + +/datum/preference/choiced/attraction + savefile_key = "attraction" + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_identifier = PREFERENCE_CHARACTER + +/datum/preference/choiced/attraction/init_possible_values() + return list("Gay", "Lesbian", "Straight", "Skolio", "Bi", "Pan", "Poly", "Omni", "Ace", "Aro", "Aro/Ace", "Unset", "Check OOC") + +/datum/preference/choiced/attraction/create_default_value() + return "Unset" + +/datum/preference/choiced/attraction/apply_to_human(mob/living/carbon/human/target, value, datum/preferences/preferences) + return FALSE + +/datum/preference/choiced/display_gender + savefile_key = "display_gender" + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_identifier = PREFERENCE_CHARACTER + +/datum/preference/choiced/display_gender/init_possible_values() + return list("Male", "Female", "Null", "Plural", "Nonbinary", "Omni", "Trans", "Andro", "Gyno", "Fluid", "Unset", "Check OOC") + +/datum/preference/choiced/display_gender/create_default_value() + return "Unset" + +/datum/preference/choiced/display_gender/apply_to_human(mob/living/carbon/human/target, value, datum/preferences/preferences) + return FALSE + +// Add a cooldown for the character directory to the client, primarily to stop server lag from refresh spam +/client + COOLDOWN_DECLARE(char_directory_cooldown) + +/// Opens character directory UI for a specific user +/client/verb/show_character_directory(specific_ad as text|null) + set name = "Character Directory" + set category = "OOC" + set desc = "Shows a listing of all active characters, along with their associated OOC notes, flavor text, and more." + + if(is_character_directory_on_cooldown()) + return + + // Check if there's not already a character directory open; open a new one if one is not present + if(!GLOB.character_directory) + GLOB.character_directory = new + + // So we start opening their page right away. There really isn't any other good way to pass this to tgui unfortunately... + if(specific_ad) + var/sanitized_name = trim(specific_ad, MAX_NAME_LEN) + GLOB.character_directory.start_viewing_ad[ckey] = sanitized_name + + GLOB.character_directory.ui_interact(mob) + +/// Returns TRUE if it's on cooldown, FALSE otherwise. This is primarily to stop malicious users from trying to lag the server by spamming this verb +/client/proc/is_character_directory_on_cooldown() + // This is primarily to stop malicious users from trying to lag the server by spamming this verb + if(!COOLDOWN_FINISHED(src, char_directory_cooldown)) + to_chat(src, span_alert("Hold your horses! It's still refreshing!")) + return TRUE + COOLDOWN_START(src, char_directory_cooldown, 10) + return FALSE + +// This is a global singleton. Keep in mind that all operations should occur on user, not src. +/datum/character_directory + /// The character preview views for the UI. + var/list/atom/movable/screen/map_view/char_preview/character_preview_views = list() + /// For when a character starts off viewing a specific character's ad + var/list/start_viewing_ad = list() + +/datum/character_directory/Destroy(force) + for(var/ckey in character_preview_views) + var/atom/movable/screen/map_view/char_preview/preview = character_preview_views[ckey] + var/mob/user = get_mob_by_ckey(ckey) + if(user) + user.client?.screen_maps -= preview + qdel(preview) + return ..() + +/// Makes a managed character preview view for a specific user +/datum/character_directory/proc/create_character_preview_view(mob/user) + var/assigned_view = "preview_[user.ckey]_[REF(src)]_directory" + + // sometimes--e.g. if you have a ui open and you observe--you can end up with a stuck map_view, which leads to subsequent previews not rendering. + // let's clear those out, we always want a new one when calling this proc anyway. + var/old_view = user.client?.screen_maps[assigned_view] + if(old_view) + character_preview_views -= old_view + user.client.screen_maps -= old_view + qdel(old_view) + + var/atom/movable/screen/map_view/char_preview/new_view = new(null) + new_view.generate_view(assigned_view) + new_view.display_to(user) + return new_view + +/// Takes a record and updates the character preview view to match it. +/datum/character_directory/proc/update_preview(mob/user, assigned_view, mutable_appearance/appearance) + var/mutable_appearance/preview = new(appearance) + preview.transform = matrix() // This is so scaled mobs aren't just getting cut off for being too big + + var/atom/movable/screen/map_view/char_preview/old_view = user.client?.screen_maps[assigned_view]?[1] + if(!old_view) + return + + old_view.appearance = preview.appearance + +/datum/character_directory/ui_state(mob/user) + return GLOB.always_state + +/datum/character_directory/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + character_preview_views[user.ckey] = create_character_preview_view(user) + ui = new(user, src, "NovaCharacterDirectory", "Character Directory") + ui.set_autoupdate(FALSE) + ui.open() + +/datum/character_directory/ui_close(mob/user) + var/atom/movable/screen/map_view/char_preview/old_preview = character_preview_views[user.ckey] + user.client?.screen_maps -= old_preview + character_preview_views -= user.ckey + qdel(old_preview) + +// We want this information to update any time the player updates their preferences, not just when the panel is refreshed +/datum/character_directory/ui_data(mob/user) + . = ..() + var/list/data = . + + // Collect the user's own preferences for the top of the UI + if (user?.client?.prefs) + data["personalVisibility"] = READ_PREFS(user, toggle/show_in_directory) + data["personalAttraction"] = READ_PREFS(user, choiced/attraction) + data["personalGender"] = READ_PREFS(user, choiced/display_gender) + data["personalErpTag"] = READ_PREFS(user, choiced/erp_status) + data["personalVoreTag"] = READ_PREFS(user, choiced/erp_status_v) + data["personalNonconTag"] = READ_PREFS(user, choiced/erp_status_nc) + data["personalHypnoTag"] = READ_PREFS(user, choiced/erp_status_hypno) + data["prefsOnly"] = TRUE + + data["assignedView"] = "preview_[user.ckey]_[REF(src)]_directory" + data["canOrbit"] = isobserver(user) + // for when we want to start off with a search term filled in automatically + var/autofill_search_term = start_viewing_ad[user.ckey] + if(autofill_search_term) + data["startViewing"] = autofill_search_term + start_viewing_ad -= user.ckey + + return data + +/datum/character_directory/ui_static_data(mob/user) + . = ..() + var/list/data = . + + // These are the variables we're trying to display in the directory + var/list/directory_mobs = list() + var/name + var/species + var/ooc_notes + var/flavor_text + var/attraction + var/gender + var/erp + var/vore + var/noncon + var/hypno + var/character_ad + var/headshot + var/ref + + // We want the directory to display only alive players, not observers or people in the lobby + for(var/mob/mob in GLOB.alive_player_list) + // Skip people who are opted out + if(!READ_PREFS(mob, toggle/show_in_directory)) + continue + // Just in case ? + if(QDELETED(mob)) + continue + + ref = REF(mob) + + // Different approach for humans and silicons + if(ishuman(mob)) + var/mob/living/carbon/human/human = mob + //If someone is obscured without flavor text visible, we don't want them on the Directory. + if((human.wear_mask && (human.wear_mask.flags_inv & HIDEFACE)) || (human.head && (human.head.flags_inv & HIDEFACE)) || (HAS_TRAIT(human, TRAIT_UNKNOWN))) + continue + //Display custom species, otherwise show base species instead + species = (READ_PREFS(human, text/custom_species)) || "Unset" + if(species == "Unset") + species = "[human.dna.species.name]" + //Load standard flavor text preference + flavor_text = READ_PREFS(human, text/flavor_text) || "" + headshot = human.dna.features["headshot"] || "" + else if(issilicon(mob)) + var/mob/living/silicon/silicon = mob + //If the target is a silicon, we want it to show its brain as its species + species = READ_PREFS(silicon, choiced/brain_type) + //Load silicon flavor text in place of normal flavor text + flavor_text = READ_PREFS(silicon, text/silicon_flavor_text) || "" + headshot = READ_PREFS(silicon, text/headshot) || "" + // Don't show if they are not a human or a silicon + else + continue + + // List of all the shown ERP preferences in the Directory. If there is none, return "Unset" + attraction = READ_PREFS(mob, choiced/attraction) || "Unspecified" + gender = READ_PREFS(mob, choiced/display_gender) || "Unset" + if(gender == "Unset") + gender = capitalize(mob.gender) + erp = READ_PREFS(mob, choiced/erp_status) || "Ask" + vore = READ_PREFS(mob, choiced/erp_status_v) || "Ask" + noncon = READ_PREFS(mob, choiced/erp_status_nc) || "Ask" + hypno = READ_PREFS(mob, choiced/erp_status_hypno) || "Ask" + character_ad = READ_PREFS(mob, text/character_ad) || "" + ooc_notes = READ_PREFS(mob, text/ooc_notes) || "" + // And finally, we want to get the mob's name, taking into account disguised names. + name = mob.real_name ? mob.name : mob.real_name + + directory_mobs.Add(list(list( + "name" = name, + "appearance_name" = mob.real_name, + "species" = species, + "ooc_notes" = ooc_notes, + "attraction" = attraction, + "gender" = gender, + "erp" = erp, + "vore" = vore, + "noncon" = noncon, + "hypno" = hypno, + "character_ad" = character_ad, + "flavor_text" = flavor_text, + "headshot" = headshot, + "ref" = ref + ))) + + data["directory"] = directory_mobs + + return data + +/datum/character_directory/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + + if(.) + return + + var/mob/user = usr + if(!user) + return + + switch(action) + if("refresh") + // This is primarily to stop malicious users from trying to lag the server by spamming this verb + if(!COOLDOWN_FINISHED(user.client, char_directory_cooldown)) + to_chat(user, "Please wait before refreshing the directory again.") + return + COOLDOWN_START(user.client, char_directory_cooldown, 10) + update_static_data(user, ui) + return TRUE + if("orbit") + var/ref = params["ref"] + var/mob/dead/observer/ghost = user + var/atom/movable/poi = (locate(ref) in GLOB.mob_list) + if (poi == null) + return TRUE + ghost.ManualFollow(poi) + ghost.reset_perspective(null) + return TRUE + if("view_character") + update_preview(usr, params["assigned_view"], GLOB.name_to_appearance[params["name"]]) + return TRUE diff --git a/modular_nova/modules/customization/modules/mob/living/carbon/human/human.dm b/modular_nova/modules/customization/modules/mob/living/carbon/human/human.dm index 44ca0cd3e60..ca3c208e12b 100644 --- a/modular_nova/modules/customization/modules/mob/living/carbon/human/human.dm +++ b/modular_nova/modules/customization/modules/mob/living/carbon/human/human.dm @@ -24,6 +24,8 @@ to_chat(usr, span_notice("[jointext(line, "\n")]")) if("open_examine_panel") mob_examine_panel.ui_interact(usr) //datum has a examine_panel component, here we open the window + if("open_character_ad") + usr.client?.show_character_directory(specific_ad = real_name) /mob/living/carbon/human/species/vox race = /datum/species/vox diff --git a/modular_nova/modules/customization/modules/mob/living/silicon/examine.dm b/modular_nova/modules/customization/modules/mob/living/silicon/examine.dm index f83c5843116..e6e317ec1ad 100644 --- a/modular_nova/modules/customization/modules/mob/living/silicon/examine.dm +++ b/modular_nova/modules/customization/modules/mob/living/silicon/examine.dm @@ -13,6 +13,9 @@ if (flavor_text_link) . += flavor_text_link + if (client?.prefs.read_preference(/datum/preference/text/character_ad)) + . += span_notice("They have an ad in the character directory... \[Open directory?\]") + if(client) var/erp_status_pref = client.prefs.read_preference(/datum/preference/choiced/erp_status) if(erp_status_pref && !CONFIG_GET(flag/disable_erp_preferences)) diff --git a/modular_nova/modules/customization/modules/mob/living/silicon/topic.dm b/modular_nova/modules/customization/modules/mob/living/silicon/topic.dm index d20650f478d..2acec6401f1 100644 --- a/modular_nova/modules/customization/modules/mob/living/silicon/topic.dm +++ b/modular_nova/modules/customization/modules/mob/living/silicon/topic.dm @@ -4,3 +4,5 @@ mob_examine_panel.ui_interact(usr) //datum has a examine_panel datum, here we open the window if(href_list["temporary_flavor"]) // we need this here because tg code doesnt call parent in /mob/living/silicon/Topic() show_temp_ftext(usr) + if(href_list["lookup_info"] == "open_character_ad") + usr.client?.show_character_directory(specific_ad = name) diff --git a/modular_nova/modules/title_screen/code/new_player.dm b/modular_nova/modules/title_screen/code/new_player.dm index ef571bc88fd..679eb527739 100644 --- a/modular_nova/modules/title_screen/code/new_player.dm +++ b/modular_nova/modules/title_screen/code/new_player.dm @@ -27,6 +27,11 @@ ViewManifest() return + if(href_list["view_directory"]) + play_lobby_button_sound() + client?.show_character_directory() + return + if(href_list["toggle_antag"]) play_lobby_button_sound() var/datum/preferences/preferences = client.prefs diff --git a/modular_nova/modules/title_screen/code/title_screen_html.dm b/modular_nova/modules/title_screen/code/title_screen_html.dm index 30f3f2949e2..a6171c6a549 100644 --- a/modular_nova/modules/title_screen/code/title_screen_html.dm +++ b/modular_nova/modules/title_screen/code/title_screen_html.dm @@ -105,6 +105,7 @@ GLOBAL_LIST_EMPTY(startup_messages) dat += {" JOIN GAME CREW MANIFEST + CHARACTER DIRECTORY "} dat += {"OBSERVE"} diff --git a/tgstation.dme b/tgstation.dme index 547059e5b16..81c18557eba 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -6669,6 +6669,7 @@ #include "modular_nova\master_files\code\modules\mining\equipment\explorer_gear.dm" #include "modular_nova\master_files\code\modules\mob\login.dm" #include "modular_nova\master_files\code\modules\mob\dead\new_player\latejoin_menu.dm" +#include "modular_nova\master_files\code\modules\mob\dead\new_player\new_player.dm" #include "modular_nova\master_files\code\modules\mob\dead\new_player\preferences_setup.dm" #include "modular_nova\master_files\code\modules\mob\living\blood.dm" #include "modular_nova\master_files\code\modules\mob\living\emote_popup.dm" @@ -7010,6 +7011,7 @@ #include "modular_nova\modules\central_command_module\code\obj\wall.dm" #include "modular_nova\modules\chadian\code\chadian.dm" #include "modular_nova\modules\chaplain\code\mortis.dm" +#include "modular_nova\modules\character_directory\code\character_directory.dm" #include "modular_nova\modules\chat_colors\code\chat_color.dm" #include "modular_nova\modules\clock_cult\code\antagonist.dm" #include "modular_nova\modules\clock_cult\code\area.dm" diff --git a/tgui/packages/tgui/interfaces/NovaCharacterDirectory.jsx b/tgui/packages/tgui/interfaces/NovaCharacterDirectory.jsx new file mode 100644 index 00000000000..c35f04c11e7 --- /dev/null +++ b/tgui/packages/tgui/interfaces/NovaCharacterDirectory.jsx @@ -0,0 +1,511 @@ +// THIS IS A NOVA SECTOR UI FILE +import { useState } from 'react'; + +import { resolveAsset } from '../assets'; +import { useBackend } from '../backend'; +import { + Button, + Divider, + Icon, + Input, + LabeledList, + NoticeBox, + Section, + Stack, + Table, + Tooltip, +} from '../components'; +import { Window } from '../layouts'; +import { CharacterPreview } from './common/CharacterPreview'; + +const formatURLs = (text) => { + if (!text) return; + const parts = []; + let regex = /https?:\/\/[^\s/$.?#].[^\s]*/gi; + let lastIndex = 0; + + text.replace(regex, (url, index) => { + parts.push(text.substring(lastIndex, index)); + parts.push( + + {url} + , + ); + lastIndex = index + url.length; + return url; + }); + + parts.push(text.substring(lastIndex)); + + return
{parts}
; +}; +const erpTagColor = { + Unset: '#000000', + 'Top - Dom': '#410308', + 'Top - Switch': '#410308', + 'Top - Sub': '#410308', + 'Verse-Top - Dom': '#3d003b', + 'Verse-Top - Switch': '#3d003b', + 'Verse-Top - Sub': '#3d003b', + 'Verse - Dom': '#310042', + 'Verse - Switch': '#310042', + 'Verse - Sub': '#310042', + 'Verse-Bottom - Dom': '#29084b', + 'Verse-Bottom - Switch': '#29084b', + 'Verse-Bottom - Sub': '#29084b', + 'Bottom - Dom': '#002f51', + 'Bottom - Switch': '#002f51', + 'Bottom - Sub': '#002f51', + 'Check OOC Notes': '#333333', + 'Ask (L)OOC': '#333333', + No: '#131313', + Yes: '#002901', +}; + +export const NovaCharacterDirectory = (props) => { + const { data } = useBackend(); + + const { + personalVisibility, + personalAttraction, + personalGender, + personalErpTag, + personalVoreTag, + personalNonconTag, + personalHypnoTag, + assignedView, + startViewing, + } = data; + + const [overlay, setOverlay] = useState(null); + const updateOverlay = (character) => { + setOverlay(character); + }; + + const [searchTerm, setSearchTerm] = useState(startViewing || ''); + const updateSearchTerm = (character) => { + setSearchTerm(character); + }; + + const [sortId, setSortId] = useState('name'); + const updateSortId = (character) => { + setSortId(character); + }; + const [sortOrder, setSortOrder] = useState('asc'); + const updateSortOrder = (character) => { + setSortOrder(character); + }; + + const [colorCodeEnabled, setColorCodeEnabled] = useState(''); + const updateColorCodeEnabled = (character) => { + setColorCodeEnabled(character); + }; + + return ( + + + {(overlay && ( + + )) || ( + <> +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + )} +
+
+ ); +}; + +const ViewCharacter = (props) => { + const { overlay, updateOverlay, assignedView } = props; + + return ( + + +
+ +
+
+ +
+
+ + + +
+ {formatURLs(overlay.flavor_text)} +
+
+ + + +
+ {overlay.ideal_antag_optin_status && ( + + Current Antag Opt-In Status:{' '} + + {overlay.current_antag_optin_status} + + {'\n'} + Antag Opt-In Status {'(Preferences)'}:{' '} + + {overlay.ideal_antag_optin_status} + + {'\n\n'} + + )} + + + {overlay.attraction} + + + {overlay.gender} + + + {overlay.erp} + + + {overlay.vore} + + + {overlay.hypno} + + + {overlay.noncon} + + +   {formatURLs(overlay.ooc_notes)} +
+
+ +
+ {overlay.character_ad} +
+ + + +
+
+
+
+
+
+ ); +}; + +const CharacterDirectoryList = (props) => { + const { act, data } = useBackend(); + const { + updateOverlay, + searchTerm, + updateSearchTerm, + sortId, + updateSortId, + sortOrder, + updateSortOrder, + colorCodeEnabled, + updateColorCodeEnabled, + } = props; + + const { directory, canOrbit, assignedView } = data; + + const handleSort = (id) => { + if (sortId === id) { + updateSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); + } else { + updateSortId(id); + updateSortOrder('asc'); + } + }; + + const handleRandomView = () => { + if (directory.length > 0) { + const randomIndex = Math.floor(Math.random() * directory.length); + const randomCharacter = directory[randomIndex]; + updateOverlay(randomCharacter); + act('view_character', { + assigned_view: assignedView, + name: randomCharacter.appearance_name, + }); + } + }; + + const filteredDirectory = directory.filter((character) => + character.name.toLowerCase().includes(searchTerm.toLowerCase()), + ); + + const sortedDirectory = filteredDirectory.slice().sort((a, b) => { + const sortOrderValue = sortOrder === 'asc' ? 1 : -1; + return sortOrderValue * a[sortId].localeCompare(b[sortId]); + }); + + return ( +
+ + + + + + } + > + + + { + updateSearchTerm(value); + }} + value={searchTerm} + mb={2} + /> + + + + + + + Name + + + Species + + + Attraction + + + Gender + + + ERP + + + Vore + + + Hypno + + + Noncon + + + Advert + + + {sortedDirectory.map((character, i) => ( + + + {canOrbit ? ( + + ) : ( + character.name + )} + + {character.species} + {character.attraction} + {character.gender} + {character.erp} + {character.vore} + {character.hypno} + {character.noncon} + + + + + ))} +
+
+ ); +}; + +const SortButton = ({ id, sortId, sortOrder, onClick, children }) => ( + + + +); diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/nova/species_features.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/nova/species_features.tsx index ed05b78f038..ebc16811345 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/nova/species_features.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/nova/species_features.tsx @@ -51,6 +51,27 @@ export const ooc_notes: Feature = { component: FeatureTextInput, }; +export const character_ad: Feature = { + name: 'Character Advert', + description: + 'An advertisement for your character. Give information on how to approach for those interested, for either regular and erotic roleplay.', + component: FeatureTextInput, +}; + +export const attraction: FeatureChoiced = { + name: 'Character Attraction', + description: + 'What your character is attracted to. This is displayed in the Directory.', + component: FeatureDropdownInput, +}; + +export const display_gender: FeatureChoiced = { + name: 'Character Gender', + description: + 'What classifies as the gender for your character. This is displayed in the Directory.', + component: FeatureDropdownInput, +}; + export const custom_species: Feature = { name: 'Custom Species Name', description: diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/nova/show_in_directory.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/nova/show_in_directory.tsx new file mode 100644 index 00000000000..b18f4b29553 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/nova/show_in_directory.tsx @@ -0,0 +1,8 @@ +import { CheckboxInput, FeatureToggle } from '../../base'; + +export const show_in_directory: FeatureToggle = { + name: 'Show in Directory', + category: 'ERP', + description: 'When enabled, character will be shown in Directory', + component: CheckboxInput, +};