diff --git a/code/game/machinery/status_display.dm b/code/game/machinery/status_display.dm index 2ce0dc641a6..b925baf894f 100644 --- a/code/game/machinery/status_display.dm +++ b/code/game/machinery/status_display.dm @@ -592,7 +592,8 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/status_display/evac, 32) LAZYOR(active_displays, new_display) set_display_with_priority(SD_GREENSCREEN, DISPLAY_PRIORITY_MESSAGE, force_override = TRUE) if("friendcomputer") - friendc = !friendc + // Friend Computer should only affect AI displays, not evac displays + return update() /obj/machinery/status_display/evac/vv_edit_var(vname, vval) @@ -733,32 +734,67 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/status_display/evac, 32) desc = "A small screen which the AI can use to present itself." current_mode = SD_PICTURE var/emotion = AI_DISPLAY_DONT_GLOW + var/frequency = FREQ_STATUS_DISPLAYS + var/friendc = FALSE // track if Friend Computer mode + var/last_picture // For when Friend Computer mode is undone MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/status_display/ai, 32) +/obj/machinery/status_display/ai/Initialize(mapload) + . = ..() + // register for radio system to receive AI emote signals + SSradio.add_object(src, frequency) + find_and_hang_on_wall() + +/obj/machinery/status_display/ai/Destroy() + SSradio.remove_object(src, frequency) + return ..() + /obj/machinery/status_display/ai/attack_ai(mob/living/silicon/ai/user) if(!isAI(user)) return - var/list/choices = list() - for(var/emotion_const in GLOB.ai_status_display_emotes) - var/icon_state = GLOB.ai_status_display_emotes[emotion_const] - choices[emotion_const] = image(icon = 'icons/obj/machines/status_display.dmi', icon_state = icon_state) - var/emotion_result = show_radial_menu(user, src, choices, tooltips = TRUE) - for(var/_emote in typesof(/datum/emote/ai/emotion_display)) - var/datum/emote/ai/emotion_display/emote = _emote - if(initial(emote.emotion) == emotion_result) - user.emote(initial(emote.key)) - break + // Open the TGUI interface for selecting status display options + if(!user.status_display_picker) + user.status_display_picker = new(user, src) + user.status_display_picker.ui_interact(user) /obj/machinery/status_display/ai/process() if(machine_stat & NOPOWER) update_appearance() return PROCESS_KILL - set_picture(GLOB.ai_status_display_emotes[emotion]) + if(!length(GLOB.ai_status_display_all_options)) + init_ai_status_display_options() + + var/icon_state + // First try the combined list + if(emotion in GLOB.ai_status_display_all_options) + icon_state = GLOB.ai_status_display_all_options[emotion] + // Then try the original emotes list + else if(emotion in GLOB.ai_status_display_emotes) + icon_state = GLOB.ai_status_display_emotes[emotion] + // Default fallback + else + icon_state = "ai_neutral" + + set_picture(icon_state) return PROCESS_KILL +/obj/machinery/status_display/ai/receive_signal(datum/signal/signal) + switch(signal.data["command"]) + if("friendcomputer") + friendc = !friendc + if(friendc) + last_picture = emotion // Save current display + emotion = AI_EMOTION_FRIEND_COMPUTER + else + if(last_picture) + emotion = last_picture // Restore previous display + else + emotion = AI_DISPLAY_DONT_GLOW + update_appearance() + /obj/item/circuit_component/status_display display_name = "Status Display" desc = "Output text and pictures to a status display." diff --git a/code/modules/client/preferences/ai_emote_display.dm b/code/modules/client/preferences/ai_emote_display.dm index fc70e284583..685887e4e49 100644 --- a/code/modules/client/preferences/ai_emote_display.dm +++ b/code/modules/client/preferences/ai_emote_display.dm @@ -6,13 +6,17 @@ should_generate_icons = TRUE /datum/preference/choiced/ai_emote_display/init_possible_values() - return assoc_to_keys(GLOB.ai_status_display_emotes) + if(!length(GLOB.ai_status_display_all_options)) + init_ai_status_display_options() + return assoc_to_keys(GLOB.ai_status_display_all_options) /datum/preference/choiced/ai_emote_display/icon_for(value) if (value == "Random") return uni_icon('icons/mob/silicon/ai.dmi', "questionmark") else - return uni_icon('icons/obj/machines/status_display.dmi', GLOB.ai_status_display_emotes[value]) + if(!length(GLOB.ai_status_display_all_options)) + init_ai_status_display_options() + return uni_icon('icons/obj/machines/status_display.dmi', GLOB.ai_status_display_all_options[value]) /datum/preference/choiced/ai_emote_display/is_accessible(datum/preferences/preferences) if (!..(preferences)) diff --git a/code/modules/mob/living/silicon/ai/_preferences.dm b/code/modules/mob/living/silicon/ai/_preferences.dm index 34125aa7cc5..d790c7d6511 100644 --- a/code/modules/mob/living/silicon/ai/_preferences.dm +++ b/code/modules/mob/living/silicon/ai/_preferences.dm @@ -1,5 +1,6 @@ // A mapping between AI_EMOTION_* string constants, which also double as user readable descriptions, and the name of the iconfile. (used for /obj/machinery/status_display/ai ) GLOBAL_LIST_INIT(ai_status_display_emotes, list( + // Original AI emotion states AI_EMOTION_AWESOME = "ai_awesome", AI_EMOTION_BLANK = AI_DISPLAY_DONT_GLOW, AI_EMOTION_BLUE_GLOW = "ai_sal", @@ -18,6 +19,60 @@ GLOBAL_LIST_INIT(ai_status_display_emotes, list( AI_EMOTION_VERY_HAPPY = "ai_veryhappy", )) +// Mapping from AI core display options to new status display icon states +// This allows AI status displays to show the same choice as AI core displays +GLOBAL_LIST_INIT(ai_core_to_status_display_mapping, list( + "Alien" = "ai_status_alien", + "Angel" = "ai_status_angel", + "Banned" = "ai_status_banned", + "Bliss" = "ai_status_bliss", + "Blue" = "ai_status_blue", + "Clown" = "ai_status_clown", + "Database" = "ai_status_database", + "Dorf" = "ai_status_dorf", + "Firewall" = "ai_status_firewall", + "Fuzzy" = "ai_status_fuzzy", + "Gentoo" = "ai_status_gentoo", + "Glitchman" = "ai_status_glitchman", + "Gondola" = "ai_status_gondola", + "Goon" = "ai_status_goon", + "Hades" = "ai_status_hades", + "HAL 9000" = "ai_status_hal9000", + "Heartline" = "ai_status_heartline", + "Helios" = "ai_status_helios", + "House" = "ai_status_house", + "Matrix" = "ai_status_matrix", + "Monochrome" = "ai_status_monochrome", + "Murica" = "ai_status_murica", + "Nanotrasen" = "ai_status_nanotrasen", + "Not Malf" = "ai_status_not_malf", + "President" = "ai_status_president", + "Rainbow" = "ai_status_rainbow", + "Red October" = "ai_status_red_october", + "Red" = "ai_status_red", + "Static" = "ai_status_static", + "Syndicat Meow" = "ai_status_syndicat_meow", + "Text" = "ai_status_text", + "Too Deep" = "ai_status_too_deep", + "Triumvirate" = "ai_status_triumvirate", + "Weird" = "ai_status_weird", +)) + +// Combined list for AI status display preferences, including both emotion states and AI core display options +GLOBAL_LIST_INIT(ai_status_display_all_options, list()) + +// Initialize the combined list at runtime +/proc/init_ai_status_display_options() + if(length(GLOB.ai_status_display_all_options)) // Already initialized + return + + // Start with original emotes + GLOB.ai_status_display_all_options = GLOB.ai_status_display_emotes.Copy() + + // Add AI core display mappings + for(var/core_option in GLOB.ai_core_to_status_display_mapping) + GLOB.ai_status_display_all_options[core_option] = GLOB.ai_core_to_status_display_mapping[core_option] + GLOBAL_LIST_INIT(ai_hologram_category_options, list( AI_HOLOGRAM_CATEGORY_ANIMAL = list( AI_HOLOGRAM_BEAR, diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm index d2ac0acd742..9ad600b0f88 100644 --- a/code/modules/mob/living/silicon/ai/ai.dm +++ b/code/modules/mob/living/silicon/ai/ai.dm @@ -144,6 +144,7 @@ QDEL_NULL(malf_picker) QDEL_NULL(doomsday_device) QDEL_NULL(robot_control) + QDEL_NULL(status_display_picker) QDEL_NULL(aiMulti) QDEL_NULL(alert_control) QDEL_NULL(ai_tracking_tool) @@ -195,7 +196,9 @@ var/emote_choice = player_client.prefs.read_preference(/datum/preference/choiced/ai_emote_display) if(emote_choice == "Random") - emote_choice = pick(GLOB.ai_status_display_emotes) + if(!length(GLOB.ai_status_display_all_options)) + init_ai_status_display_options() + emote_choice = pick(GLOB.ai_status_display_all_options) apply_emote_display(emote_choice) @@ -208,29 +211,28 @@ /mob/living/silicon/ai/verb/pick_icon() set category = "AI Commands" set name = "Set AI Core Display" + set desc = "Choose what appears on your AI core display" + if(incapacitated) - return - icon = initial(icon) - icon_state = "ai" - cut_overlays() - var/list/iconstates = GLOB.ai_core_display_screens - for(var/option in iconstates) - if(option == "Random") - iconstates[option] = image(icon = src.icon, icon_state = "ai-random") - continue - if(option == "Portrait") - iconstates[option] = image(icon = src.icon, icon_state = "ai-portrait") - continue - iconstates[option] = image(icon = src.icon, icon_state = resolve_ai_icon(option)) - - view_core() - var/ai_core_icon = show_radial_menu(src, src , iconstates, radius = 42) - - if(!ai_core_icon || incapacitated) + to_chat(src, span_warning("You cannot access the core display controls in your current state.")) return - display_icon_override = ai_core_icon - set_core_display_icon(ai_core_icon) + if(!core_display_picker) + core_display_picker = new(src) + core_display_picker.ui_interact(src) + +/mob/living/silicon/ai/verb/pick_status_display() + set category = "AI Commands" + set name = "Set AI Status Display" + set desc = "Choose what appears on status displays around the station" + + if(incapacitated) + to_chat(src, span_warning("You cannot access the status display controls in your current state.")) + return + + if(!status_display_picker) + status_display_picker = new(src) + status_display_picker.ui_interact(src) /mob/living/silicon/ai/get_status_tab_items() . = ..() diff --git a/code/modules/mob/living/silicon/ai/ai_core_display_picker.dm b/code/modules/mob/living/silicon/ai/ai_core_display_picker.dm new file mode 100644 index 00000000000..311f0d064ab --- /dev/null +++ b/code/modules/mob/living/silicon/ai/ai_core_display_picker.dm @@ -0,0 +1,79 @@ +/** + * AI Core Display Picker TGUI + * Allows AIs to select core display options with search functionality + */ +/datum/ai_core_display_picker + var/mob/living/silicon/ai/ai_user + +/datum/ai_core_display_picker/New(mob/living/silicon/ai/user) + ai_user = user + +/datum/ai_core_display_picker/ui_status(mob/user, datum/ui_state/state) + if(!ai_user || user != ai_user || ai_user.incapacitated) + return UI_CLOSE + return ..() + +/datum/ai_core_display_picker/ui_state(mob/user) + return GLOB.always_state + +/datum/ai_core_display_picker/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "AiCoreDisplayPicker") + ui.open() + +/datum/ai_core_display_picker/ui_close(mob/user) + if(ai_user) + ai_user.core_display_picker = null + +/datum/ai_core_display_picker/ui_data(mob/user) + var/list/data = list() + + // If no override is set, find the actual current display from the AI's icon state + var/current_display = ai_user.display_icon_override + if(!current_display) + // Default to "Blue" if no override + current_display = "Blue" + // Try to identify current display + if(ai_user.icon_state) + for(var/display_name in GLOB.ai_core_display_screens) + if("ai-[LOWER_TEXT(display_name)]" == ai_user.icon_state) + current_display = display_name + break + + data["current_display"] = current_display + + // Get icon for current display + var/current_icon_state = resolve_ai_icon_sync(current_display) + data["current_icon"] = list( + "icon" = 'icons/mob/silicon/ai.dmi', + "icon_state" = current_icon_state + ) + + var/list/options = list() + + for(var/option_name in GLOB.ai_core_display_screens) + var/icon_state = resolve_ai_icon_sync(option_name) + var/list/option_data = list( + "name" = option_name, + "icon_state" = icon_state, + "icon" = 'icons/mob/silicon/ai.dmi' + ) + options += list(option_data) + + data["options"] = options + + return data + +/datum/ai_core_display_picker/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + + switch(action) + if("select_option") + var/chosen_option = params["option"] + if(chosen_option in GLOB.ai_core_display_screens) + ai_user.display_icon_override = chosen_option + ai_user.set_core_display_icon(chosen_option) + return TRUE diff --git a/code/modules/mob/living/silicon/ai/ai_defines.dm b/code/modules/mob/living/silicon/ai/ai_defines.dm index e1399a1ddb1..de631e7b867 100644 --- a/code/modules/mob/living/silicon/ai/ai_defines.dm +++ b/code/modules/mob/living/silicon/ai/ai_defines.dm @@ -163,6 +163,10 @@ /* UI */ /// UI for station alerts VAR_FINAL/datum/station_alert/alert_control + /// UI for AI status display picker + VAR_FINAL/datum/ai_status_display_picker/status_display_picker + /// UI for AI core display picker + VAR_FINAL/datum/ai_core_display_picker/core_display_picker /* I'M DUMB AND CAN'T SORT */ /// Used as a fake multitool in tcomms machinery diff --git a/code/modules/mob/living/silicon/ai/ai_status_display_picker.dm b/code/modules/mob/living/silicon/ai/ai_status_display_picker.dm new file mode 100644 index 00000000000..e88fc60c92a --- /dev/null +++ b/code/modules/mob/living/silicon/ai/ai_status_display_picker.dm @@ -0,0 +1,123 @@ +/** + * AI Status Display Picker TGUI + * Allows AIs to select status display options with search functionality + */ + +/datum/ai_status_display_picker + /// The AI user who opened this interface + var/mob/living/silicon/ai/ai_user + /// The status display that was clicked + var/obj/machinery/status_display/ai/target_display + +/datum/ai_status_display_picker/New(mob/living/silicon/ai/user) + ai_user = user + +/datum/ai_status_display_picker/ui_status(mob/user, datum/ui_state/state) + if(!ai_user || user != ai_user || ai_user.incapacitated) + return UI_CLOSE + return ..() + +/datum/ai_status_display_picker/ui_state(mob/user) + return GLOB.always_state + +/datum/ai_status_display_picker/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "AiStatusDisplayPicker") + ui.open() + +/datum/ai_status_display_picker/ui_close(mob/user) + if(ai_user) + ai_user.status_display_picker = null + +// No assets needed for DMIcon system + +/datum/ai_status_display_picker/ui_static_data(mob/user) + // Always ensure initialization + init_ai_status_display_options() + + var/list/data = list() + var/list/options = list() + + for(var/option_name in GLOB.ai_status_display_emotes) + var/icon_state = GLOB.ai_status_display_emotes[option_name] + var/list/option_data = list( + "name" = option_name, + "icon_state" = icon_state, + "is_original" = TRUE, + "icon" = 'icons/obj/machines/status_display.dmi' + ) + options += list(option_data) + + for(var/option_name in GLOB.ai_core_to_status_display_mapping) + var/icon_state = GLOB.ai_core_to_status_display_mapping[option_name] + var/list/option_data = list( + "name" = option_name, + "icon_state" = icon_state, + "is_original" = FALSE, + "icon" = 'icons/obj/machines/status_display.dmi' + ) + options += list(option_data) + + data["options"] = options + + return data + +/datum/ai_status_display_picker/ui_data(mob/user) + var/list/data = list() + + // Find current display emotion from first available AI status display + var/current_emotion = "None" + var/current_icon_state + var/obj/machinery/status_display/ai/first_display = locate() in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/status_display/ai) + if(first_display?.emotion) + current_emotion = first_display.emotion + if(current_emotion in GLOB.ai_status_display_emotes) + current_icon_state = GLOB.ai_status_display_emotes[current_emotion] + else if(current_emotion in GLOB.ai_core_to_status_display_mapping) + current_icon_state = GLOB.ai_core_to_status_display_mapping[current_emotion] + else + current_icon_state = "ai_neutral" + else + current_icon_state = "ai_neutral" + + data["current_emotion"] = current_emotion + data["current_icon"] = list( + "icon" = 'icons/obj/machines/status_display.dmi', + "icon_state" = current_icon_state + ) + + return data + +/datum/ai_status_display_picker/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + + if(!ai_user) + return FALSE + + switch(action) + if("select_option") + var/selected_option = params["option"] + if(!selected_option || !(selected_option in GLOB.ai_status_display_all_options)) + return FALSE + + // Check if this is an original AI emotion (has a corresponding emote) + var/found_emote = FALSE + for(var/_emote in typesof(/datum/emote/ai/emotion_display)) + var/datum/emote/ai/emotion_display/emote = _emote + if(initial(emote.emotion) == selected_option) + ai_user.emote(initial(emote.key)) + found_emote = TRUE + break + + // If no emote found (i.e., it's a new AI core display option), directly apply it + if(!found_emote) + ai_user.apply_emote_display(selected_option) + + return TRUE + + return FALSE + + diff --git a/icons/obj/machines/status_display.dmi b/icons/obj/machines/status_display.dmi index 98f78627823..2fa55c8436b 100644 Binary files a/icons/obj/machines/status_display.dmi and b/icons/obj/machines/status_display.dmi differ diff --git a/tgstation.dme b/tgstation.dme index 864f10efcb0..5e7b7ef98dc 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -5497,10 +5497,12 @@ #include "code\modules\mob\living\silicon\silicon_say.dm" #include "code\modules\mob\living\silicon\ai\_preferences.dm" #include "code\modules\mob\living\silicon\ai\ai.dm" +#include "code\modules\mob\living\silicon\ai\ai_core_display_picker.dm" #include "code\modules\mob\living\silicon\ai\ai_defense.dm" #include "code\modules\mob\living\silicon\ai\ai_defines.dm" #include "code\modules\mob\living\silicon\ai\ai_portrait_picker.dm" #include "code\modules\mob\living\silicon\ai\ai_say.dm" +#include "code\modules\mob\living\silicon\ai\ai_status_display_picker.dm" #include "code\modules\mob\living\silicon\ai\death.dm" #include "code\modules\mob\living\silicon\ai\emote.dm" #include "code\modules\mob\living\silicon\ai\examine.dm" diff --git a/tgui/packages/tgui/interfaces/AiCoreDisplayPicker.tsx b/tgui/packages/tgui/interfaces/AiCoreDisplayPicker.tsx new file mode 100644 index 00000000000..a248373d490 --- /dev/null +++ b/tgui/packages/tgui/interfaces/AiCoreDisplayPicker.tsx @@ -0,0 +1,146 @@ +import { useState } from 'react'; +import { + Box, + Button, + Flex, + ImageButton, + Input, + Section, + Stack, + Table, + DmIcon, +} from 'tgui-core/components'; +import { classes } from 'tgui-core/react'; +import { useBackend } from '../backend'; +import { Window } from '../layouts'; + +type CoreDisplayOption = { + name: string; + icon_state: string; + icon: string; +}; + +type CurrentIcon = { + icon: string; + icon_state: string; +}; + +type Data = { + current_display: string; + current_icon: CurrentIcon | null; + options: CoreDisplayOption[]; +}; + +export const AiCoreDisplayPicker = () => { + return ( + + + + + + ); +}; + +const AiCoreDisplayPickerContent = () => { + const { act, data } = useBackend(); + const { current_display, current_icon, options = [] } = data; + + const [searchTerm, setSearchTerm] = useState(''); + const [tiledView, setTiledView] = useState(false); + + // Filter options based on search term + const filteredOptions = options.filter((option) => + option.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + return ( + + +
+ + {current_icon && ( + + + + + + )} + + + {current_display} + + + +
+
+ + + setSearchTerm(value)} + fluid + /> + + + + + +
+ +
+
+ + {filteredOptions.length === 0 && ( + + + No options found matching "{searchTerm}" + + + )} +
+
+ + +
+ ); +}; + +const OptionsList = ({ + options, +}: { + options: CoreDisplayOption[]; +}) => { + const { act } = useBackend(); + + return ( + <> + {options.map((option) => ( + act('select_option', { option: option.name })} + imageSize={64} + tooltip={option.name} + > + {option.name} + + ))} + + ); +}; diff --git a/tgui/packages/tgui/interfaces/AiStatusDisplayPicker.tsx b/tgui/packages/tgui/interfaces/AiStatusDisplayPicker.tsx new file mode 100644 index 00000000000..1adc55bb4be --- /dev/null +++ b/tgui/packages/tgui/interfaces/AiStatusDisplayPicker.tsx @@ -0,0 +1,164 @@ +import { useState } from 'react'; +import type { BooleanLike } from 'tgui-core/react'; +import { + Box, + Button, + Flex, + ImageButton, + Input, + Section, + Stack, + DmIcon, +} from 'tgui-core/components'; +import { useBackend } from '../backend'; +import { Window } from '../layouts'; + +type StatusDisplayOption = { + name: string; + icon_state: string; + is_original: BooleanLike; + dmi: string; + icon: string; +}; + +type CurrentIcon = { + icon: string; + icon_state: string; +}; + +type Data = { + current_emotion: string; + current_icon: CurrentIcon | null; + options: StatusDisplayOption[]; +}; + +export const AiStatusDisplayPicker = () => { + return ( + + + + + + ); +}; + +const AiStatusDisplayPickerContent = () => { + const { act, data } = useBackend(); + const { current_emotion, current_icon, options = [] } = data; + + const [searchTerm, setSearchTerm] = useState(''); + + // Filter options based on search term + const filteredOptions = options.filter((option) => + option.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + // Separate original emotions and new AI core options + const originalOptions = filteredOptions.filter((option) => option.is_original); + const newOptions = filteredOptions.filter((option) => !option.is_original); + + return ( + + +
+ + {current_icon && ( + + + + + + )} + + + {current_emotion || 'No Selection'} + + + +
+
+ + +
+ setSearchTerm(value)} + /> +
+
+ + + + {originalOptions.length > 0 && ( + +
+ +
+
+ )} + + {newOptions.length > 0 && ( + +
+ +
+
+ )} + + {filteredOptions.length === 0 && ( + + + No options found matching "{searchTerm}" + + + )} +
+
+ + +
+ ); +}; + +const OptionsList = ({ + options +}: { + options: StatusDisplayOption[]; +}) => { + const { act } = useBackend(); + + return ( + <> + {options.map((option) => ( + act('select_option', { option: option.name })} + imageSize={64} + tooltip={option.name} + > + {option.name} + + ))} + + ); +};