mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-07-17 11:05:16 +01:00
Reworks AI Status Display and AI Core Display UX (#93196)
## About The Pull Request Ready for merge post review No gameplay balance changes, just better AI expression options and polished interfaces for doing so. - Adds 32 AI Core animation sprites to the AI status screens as a player choice - Gives two new menus to change status displays or core display with TGUI - Fixes Friend Computer emote incorrectly affecting evacuation displays - Special mention to MrMelbert who gave a few very useful points as I embraced TGUI suffering :) <img width="1500" height="750" alt="image" src="https://github.com/user-attachments/assets/fbae0ea9-5220-4cf9-9fa0-c5f0385f9e52" /> <img width="1746" height="821" alt="image" src="https://github.com/user-attachments/assets/f1c08fdf-8dc8-44eb-b7cd-cd86f9b8f903" /> There is also a new verb under "AI Commands" <img width="433" height="127" alt="image" src="https://github.com/user-attachments/assets/2cd69b71-7a0c-4199-959e-acd4de9d5dad" /> <img width="364" height="528" alt="image" src="https://github.com/user-attachments/assets/e083784e-8251-4a55-a9e2-e27e53dfa780" /> ## Why It's Good For The Game - Allows AI players to express themselves more in round / stand out more - Now it is a TGUI menu, Much easier to see what is available and change mid round as needed. - We had talented Spriters make these and now more players can see them ## Changelog 🆑 qol: New verb in "AI commands" to update AI display status fix: Stopped "Friend Computer" emotion affecting evac displays instead of just AI displays code: Replaced radial wheel for AI status or AI core display with TGUI menus image: AIs can now update their status displays with way more graphics to match core display options onto the TVs. /🆑 --------- Co-authored-by: loganuk <falseemail@aol.com>
This commit is contained in:
@@ -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."
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
. = ..()
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 169 KiB |
@@ -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"
|
||||
|
||||
@@ -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 (
|
||||
<Window width={500} height={600} title="AI Core Display Options">
|
||||
<Window.Content scrollable>
|
||||
<AiCoreDisplayPickerContent />
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
const AiCoreDisplayPickerContent = () => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
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 (
|
||||
<Stack fill vertical>
|
||||
<Stack.Item>
|
||||
<Section title="Current AI Core Display">
|
||||
<Flex align="center" justify="center" direction="column">
|
||||
{current_icon && (
|
||||
<Flex.Item mb={2}>
|
||||
<Box
|
||||
style={{
|
||||
border: '2px solid #4a9eff',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#1a1a1a',
|
||||
padding: '8px'
|
||||
}}
|
||||
>
|
||||
<DmIcon
|
||||
icon={current_icon.icon}
|
||||
icon_state={current_icon.icon_state}
|
||||
width="64px"
|
||||
height="64px"
|
||||
/>
|
||||
</Box>
|
||||
</Flex.Item>
|
||||
)}
|
||||
<Flex.Item>
|
||||
<Box fontSize="1.2em" textAlign="center" bold color="good">
|
||||
{current_display}
|
||||
</Box>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
</Section>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<Input
|
||||
placeholder="Search display options..."
|
||||
value={searchTerm}
|
||||
onChange={(value) => setSearchTerm(value)}
|
||||
fluid
|
||||
/>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item grow>
|
||||
<Stack fill vertical>
|
||||
<Stack.Item>
|
||||
<Section title="AI Core Display Options">
|
||||
<OptionsList
|
||||
options={filteredOptions}
|
||||
/>
|
||||
</Section>
|
||||
</Stack.Item>
|
||||
|
||||
{filteredOptions.length === 0 && (
|
||||
<Stack.Item>
|
||||
<Box textAlign="center" color="average" mt={4}>
|
||||
No options found matching "{searchTerm}"
|
||||
</Box>
|
||||
</Stack.Item>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
|
||||
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const OptionsList = ({
|
||||
options,
|
||||
}: {
|
||||
options: CoreDisplayOption[];
|
||||
}) => {
|
||||
const { act } = useBackend<Data>();
|
||||
|
||||
return (
|
||||
<>
|
||||
{options.map((option) => (
|
||||
<ImageButton
|
||||
key={option.name}
|
||||
dmIcon={option.icon}
|
||||
dmIconState={option.icon_state}
|
||||
onClick={() => act('select_option', { option: option.name })}
|
||||
imageSize={64}
|
||||
tooltip={option.name}
|
||||
>
|
||||
{option.name}
|
||||
</ImageButton>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<Window width={500} height={600} title="AI Status Display Options">
|
||||
<Window.Content scrollable>
|
||||
<AiStatusDisplayPickerContent />
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
const AiStatusDisplayPickerContent = () => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
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 (
|
||||
<Stack fill vertical>
|
||||
<Stack.Item>
|
||||
<Section title="Current Display">
|
||||
<Flex align="center" justify="center" direction="column">
|
||||
{current_icon && (
|
||||
<Flex.Item mb={2}>
|
||||
<Box
|
||||
style={{
|
||||
border: '2px solid #4a9eff',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#1a1a1a',
|
||||
padding: '8px'
|
||||
}}
|
||||
>
|
||||
<DmIcon
|
||||
icon={current_icon.icon}
|
||||
icon_state={current_icon.icon_state}
|
||||
width="64px"
|
||||
height="64px"
|
||||
/>
|
||||
</Box>
|
||||
</Flex.Item>
|
||||
)}
|
||||
<Flex.Item mb={1}>
|
||||
<Box fontSize="1.4em" textAlign="center" bold color="good">
|
||||
{current_emotion || 'No Selection'}
|
||||
</Box>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
</Section>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<Section>
|
||||
<Input
|
||||
fluid
|
||||
placeholder="Search display options..."
|
||||
value={searchTerm}
|
||||
onChange={(value) => setSearchTerm(value)}
|
||||
/>
|
||||
</Section>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item grow>
|
||||
<Stack fill vertical>
|
||||
{originalOptions.length > 0 && (
|
||||
<Stack.Item>
|
||||
<Section title="AI Emotions">
|
||||
<OptionsList
|
||||
options={originalOptions}
|
||||
/>
|
||||
</Section>
|
||||
</Stack.Item>
|
||||
)}
|
||||
|
||||
{newOptions.length > 0 && (
|
||||
<Stack.Item>
|
||||
<Section title="Additional Status Display Options">
|
||||
<OptionsList
|
||||
options={newOptions}
|
||||
/>
|
||||
</Section>
|
||||
</Stack.Item>
|
||||
)}
|
||||
|
||||
{filteredOptions.length === 0 && (
|
||||
<Stack.Item>
|
||||
<Box textAlign="center" color="average" mt={4}>
|
||||
No options found matching "{searchTerm}"
|
||||
</Box>
|
||||
</Stack.Item>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
|
||||
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const OptionsList = ({
|
||||
options
|
||||
}: {
|
||||
options: StatusDisplayOption[];
|
||||
}) => {
|
||||
const { act } = useBackend<Data>();
|
||||
|
||||
return (
|
||||
<>
|
||||
{options.map((option) => (
|
||||
<ImageButton
|
||||
key={option.name}
|
||||
dmIcon={option.icon}
|
||||
dmIconState={option.icon_state}
|
||||
onClick={() => act('select_option', { option: option.name })}
|
||||
imageSize={64}
|
||||
tooltip={option.name}
|
||||
>
|
||||
{option.name}
|
||||
</ImageButton>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user