diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm index 31e4fe70116..a3b9f39b03c 100644 --- a/code/__DEFINES/misc.dm +++ b/code/__DEFINES/misc.dm @@ -15,42 +15,48 @@ #define ISDIAGONALDIR(d) (d&(d-1)) //Human Overlays Indexes///////// -#define JOYBRINGER_LAYER 55 -#define BLACK_ROT_LAYER 54 -#define POTENCE_LAYER 53 -#define MUTATIONS_LAYER 52 //mutations. Tk headglows, cold resistance glow, etc -#define CLOAK_BEHIND_LAYER 51 -#define HANDS_BEHIND_LAYER 50 -#define BELT_BEHIND_LAYER 49 -#define BACK_BEHIND_LAYER 48 -#define BODY_BEHIND_LAYER 47 //certain mutantrace features (tail when looking south) that must appear behind the body parts -#define BODY_UNDER_LAYER 46 // Things under the bodyparts but above the "behind body" layer -#define BODYPARTS_LAYER 45 //Initially "AUGMENTS", this was repurposed to be a catch-all bodyparts flag -#define BODY_ADJ_LAYER 44 //certain mutantrace features (snout, body markings) that must appear above the body parts -#define BODY_LAYER 43 //underwear, undershirts, socks, eyes, lips(makeup) -#define FRONT_MUTATIONS_LAYER 42 //mutations that should appear above body, body_adj and bodyparts layer (e.g. laser eyes) -#define DAMAGE_LAYER 41 //damage indicators (cuts and burns) -#define LEG_PART_LAYER 40 -#define LEGWEAR_LAYER 39 -#define PANTS_LAYER 38 -#define LEG_DAMAGE_LAYER 37 -#define LEGSLEEVE_LAYER 36 -#define SHOES_LAYER 35 -#define SHOESLEEVE_LAYER 34 -#define SHIRT_LAYER 33 -#define WRISTS_LAYER 32 -#define ARMOR_LAYER 31 -#define TABARD_LAYER 30 -#define BELT_LAYER 29 //only when looking south -#define UNDER_CLOAK_LAYER 28 -#define HANDS_PART_LAYER 27 -#define GLOVES_LAYER 26 -#define ARM_DAMAGE_LAYER 25 -#define SHIRTSLEEVE_LAYER 24 -#define WRISTSLEEVE_LAYER 23 -#define ARMORSLEEVE_LAYER 22 -#define GLOVESLEEVE_LAYER 21 -#define RING_LAYER 20 +//Caustic Edit - I give up, I tried to avoid adding new layers for genitals and everything, but I think I have to at this point aaaaaa. +#define JOYBRINGER_LAYER 60 +#define BLACK_ROT_LAYER 59 +#define POTENCE_LAYER 58 +#define MUTATIONS_LAYER 57 //mutations. Tk headglows, cold resistance glow, etc +#define CLOAK_BEHIND_LAYER 56 +#define HANDS_BEHIND_LAYER 55 +#define BELT_BEHIND_LAYER 54 +#define BACK_BEHIND_LAYER 53 +#define BODY_BEHIND_LAYER 52 //certain mutantrace features (tail when looking south) that must appear behind the body parts +#define BODY_UNDER_LAYER 51 // Things under the bodyparts but above the "behind body" layer +#define BODYPARTS_LAYER 50 //Initially "AUGMENTS", this was repurposed to be a catch-all bodyparts flag +#define BODY_ADJ_LAYER 49 //certain mutantrace features (snout, body markings) that must appear above the body parts +#define BODY_LAYER 48 //underwear, undershirts, socks, eyes, lips(makeup) +#define FRONT_MUTATIONS_LAYER 47 //mutations that should appear above body, body_adj and bodyparts layer (e.g. laser eyes) +#define DAMAGE_LAYER 46 //damage indicators (cuts and burns) +#define LEG_PART_LAYER 45 +#define LEGWEAR_LAYER 44 +#define PANTS_LAYER 43 +#define LEG_DAMAGE_LAYER 42 +#define LEGSLEEVE_LAYER 41 +#define SHOES_LAYER 40 +#define SHOESLEEVE_LAYER 39 +#define ASS_LAYER 38 //Caustic Added +#define TESTICLES_LAYER 37 //Caustic Added +#define CROTCH_LAYER 36 //Caustic Added +#define SHIRT_LAYER 35 +#define WRISTS_LAYER 34 +#define ARMOR_LAYER 33 +#define TABARD_LAYER 32 +#define BELT_LAYER 31 //only when looking south +#define UNDER_CLOAK_LAYER 30 +#define HANDS_PART_LAYER 29 +#define GLOVES_LAYER 28 +#define ARM_DAMAGE_LAYER 27 +#define SHIRTSLEEVE_LAYER 26 +#define WRISTSLEEVE_LAYER 25 +#define ARMORSLEEVE_LAYER 24 +#define GLOVESLEEVE_LAYER 23 +#define RING_LAYER 22 +#define BELLY_LAYER 21 //Caustic Added +#define BREASTS_LAYER 20 //Caustic Added #define GLASSES_LAYER 19 #define NECK_LAYER 18 #define CLOAK_LAYER 17 //only when looking north or west/east @@ -70,7 +76,8 @@ #define SUNDER_LAYER 3 #define FIRE_LAYER 2 //If you're on fire #define TURF_LAYER 1 //If you're on fire -#define TOTAL_LAYERS 55 //KEEP THIS UP-TO-DATE OR SHIT WILL BREAK ;_; +#define TOTAL_LAYERS 60 //KEEP THIS UP-TO-DATE OR SHIT WILL BREAK ;_; +//Caustic Edit End #define BACK_CLOAK_SOUTH_LAYER (BODY_BEHIND_LAYER+1) @@ -83,8 +90,6 @@ //AND -1 MEANS "ABOVE", OK?, OK!?! #define ABOVE_SHOES_LAYER (SHOES_LAYER-1) //Caustic Cove edit, just puts this on top of all the other new layers. Also defining in seperate file doesn't work, likely because it doesn't see the above in a seperate file. -#define BODY_ASS_LAYER (LEG_PART_LAYER-1) -#define BODY_NOTSOFRONT_LAYER (BODY_FRONT_LAYER+1) #define BODY_FRONTER_LAYER (BODY_FRONT_LAYER-1) // Makes mini-layers on your layers without having to add any more actual layers! Used for proper organ layers #define BODY_FRONTEST_LAYER (BODY_FRONT_LAYER-2) #define ABOVE_BODY_FRONT_LAYER (BODY_FRONT_LAYER-3) diff --git a/code/datums/combat_music.dm b/code/datums/combat_music.dm index b00eab47b0d..c38606ccc13 100644 --- a/code/datums/combat_music.dm +++ b/code/datums/combat_music.dm @@ -82,6 +82,15 @@ GLOBAL_LIST_EMPTY(cmode_tracks_by_name) credits = "T-87 SULFURHEAD - Snicker Snacker (https://www.youtube.com/@T87-Sulfurhead)" musicpath = list('sound/music/cmode/adventurer/combat_outlander4.ogg') +//Caustic Edit - Added special Archivist music :3 +/datum/combat_music/archivist + name = "Archivist" + desc = "May you find your book in this place." + shortname = "Archivist" + credits = "Project Moon - Library of Ruina (Netzach Battle 1 and 2) - Compiled together and uploaded by Sanu (https://www.youtube.com/watch?v=_KFlB_bMNCY)" + musicpath = list('sound/music/cmode/towner/combat_archivist.ogg') +//Caustic Edit End + /datum/combat_music/ascended name = "Ascended" desc = "No mortal could ever comprehend the heights to which I've risen." diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index eb7a7ef0b5a..1db37221216 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -221,7 +221,10 @@ GLOBAL_PROTECT(admin_verbs_debug) /client/proc/set_tod_override, /client/proc/stresstest_chat, /client/proc/performance_stress_test, // Uncomment these if you tick the performance stress test .dm file - /client/proc/cleanup_stress_test_mobs + /client/proc/cleanup_stress_test_mobs, + //CC Edit + /client/proc/allow_broser_inspect + //CC Edit End ) GLOBAL_LIST_INIT(admin_verbs_possess, list(/proc/possess, GLOBAL_PROC_REF(release))) GLOBAL_PROTECT(admin_verbs_possess) diff --git a/code/modules/admin/verbs/debug.dm b/code/modules/admin/verbs/debug.dm index 002990ce2b0..2f511572ea7 100644 --- a/code/modules/admin/verbs/debug.dm +++ b/code/modules/admin/verbs/debug.dm @@ -1509,3 +1509,14 @@ GLOBAL_LIST_EMPTY(loadout_selected_advclasses) return if(alert(usr, "Are you absolutely sure you want to reload the configuration from the default path on the disk, wiping any in-round modificatoins?", "Really reset?", "No", "Yes") == "Yes") config.admin_reload() + + +//CC Edit: Heck TGUI, seriously, why is this missing! +/client/proc/allow_broser_inspect() + set category = "Debug" + set name = "Allow Browser Inspect" + set desc = "" + if(!check_rights(R_DEBUG)) + return + to_chat(src, span_notice("You can now right click to use inspect on browsers.")) + winset(src, null, list("browser-options" = "+devtools")) diff --git a/code/modules/clothing/clothing.dm b/code/modules/clothing/clothing.dm index b301ca97759..eec6e2af4a8 100644 --- a/code/modules/clothing/clothing.dm +++ b/code/modules/clothing/clothing.dm @@ -602,9 +602,11 @@ BLIND // can't see anything if(showcrits) if(!prevent_crits) + str += "
" str += "" str += "CRIT SUSCEPTIBLE!" else if(prevent_crits == PREVENT_CRITS_ALL) + str += "
" str += "" str += "PICK RESISTANT" diff --git a/code/modules/jobs/job_types/roguetown/courtier/archivist.dm b/code/modules/jobs/job_types/roguetown/courtier/archivist.dm index bfad84de62b..a2480611820 100644 --- a/code/modules/jobs/job_types/roguetown/courtier/archivist.dm +++ b/code/modules/jobs/job_types/roguetown/courtier/archivist.dm @@ -10,7 +10,7 @@ vice_restrictions = list(/datum/charflaw/unintelligible) allowed_races = ACCEPTED_RACES allowed_ages = ALL_AGES_LIST - cmode_music = 'sound/music/cmode/towner/combat_towner3.ogg' + cmode_music = 'sound/music/cmode/towner/combat_archivist.ogg' //Caustic Edit - Added Archivist-unique music! outfit = /datum/outfit/job/roguetown/archivist display_order = JDO_ARCHIVIST diff --git a/code/modules/mob/dead/new_player/sprite_accessory/_sprite_accessory.dm b/code/modules/mob/dead/new_player/sprite_accessory/_sprite_accessory.dm index 6719b9abfea..15d5a9206f7 100644 --- a/code/modules/mob/dead/new_player/sprite_accessory/_sprite_accessory.dm +++ b/code/modules/mob/dead/new_player/sprite_accessory/_sprite_accessory.dm @@ -178,14 +178,18 @@ if(BODY_FRONT_LAYER) return "FRONT" //Caustic Edit - if(BODY_ASS_LAYER) + if(ASS_LAYER) //This one is the only one that is currently correct :< return "ASS" - if(BODY_NOTSOFRONT_LAYER) + if(TESTICLES_LAYER) //PLEASE PLEASE PLEASE if anyone sees this, and wants to fix the names of the Iconstates in the various DMI files... I would love it. - Jon return "NSFRONT" - if(BODY_FRONTER_LAYER) + if(BELLY_LAYER) //Fix me :< return "FRONT" - if(BODY_FRONTEST_LAYER) + if(BREASTS_LAYER) //Me too :< return "FRONT" + if(CROTCH_LAYER) //aAaaAAAaaa I hate the layering system + return "FRONT" + if(GLASSES_LAYER) + return "ADJ" //Caustic End if(BODY_FRONT_FRONT_LAYER) return "FFRONT" diff --git a/code/modules/mob/dead/new_player/sprite_accessory/genitals.dm b/code/modules/mob/dead/new_player/sprite_accessory/genitals.dm index ae47968d8d4..d2fa96930f1 100644 --- a/code/modules/mob/dead/new_player/sprite_accessory/genitals.dm +++ b/code/modules/mob/dead/new_player/sprite_accessory/genitals.dm @@ -1,10 +1,11 @@ /datum/sprite_accessory/penis icon = 'icons/mob/sprite_accessory/genitals/pintle.dmi' -//Caustic Edit, adds dynamic state changes -- Also upgrade to SPLURTS icons - Jon + //Caustic Edit, adds dynamic state changes -- Also upgrade to SPLURTS icons - Jon //color_keys = 2 color_key_name = "Member" color_key_names = "Member" //list("Member", "Skin") - relevant_layers = list(/*BODY_BEHIND_LAYER, */BODY_FRONT_LAYER) //Vrell - Yes I know this is hacky but it works for now + relevant_layers = list(/*BODY_BEHIND_LAYER, */CROTCH_LAYER) //Giving these their own unique layers now. PLEASE PLEASE PLEASE if anyone sees this, and wants to fix the names of the Iconstates in the various DMI files... I would love it. Check _sprite_accessory.dm as well to change what string is appended to the state. I'm leaving it as "FRONT" for now so I don't have to rename them all over AGAIN... - Jon //Vrell - Yes I know this is hacky but it works for now + //Caustic Edit End /datum/sprite_accessory/penis/adjust_appearance_list(list/appearance_list, obj/item/organ/organ, obj/item/bodypart/bodypart, mob/living/carbon/owner) generic_gender_feature_adjust(appearance_list, organ, bodypart, owner, OFFSET_BELT, OFFSET_BELT_F) @@ -107,7 +108,7 @@ /datum/sprite_accessory/testicles icon = 'icons/mob/sprite_accessory/genitals/gonads.dmi' color_key_name = "Sack" - relevant_layers = list(BODY_BEHIND_LAYER, BODY_NOTSOFRONT_LAYER) + relevant_layers = list(BODY_BEHIND_LAYER, TESTICLES_LAYER) //Caustic Edit - Giving these their own unique layers now. PLEASE PLEASE PLEASE if anyone sees this, and wants to fix the names of the Iconstates in the various DMI files... I would love it. Check _sprite_accessory.dm as well to change what string is appended to the state. I'm leaving it as "NSFRONT" for now so I don't have to rename them all over AGAIN... - Jon /datum/sprite_accessory/testicles/adjust_appearance_list(list/appearance_list, obj/item/organ/organ, obj/item/bodypart/bodypart, mob/living/carbon/owner) generic_gender_feature_adjust(appearance_list, organ, bodypart, owner, OFFSET_BELT, OFFSET_BELT_F) @@ -142,7 +143,7 @@ //color_key_name = "Breasts" color_keys = 2 color_key_names = list("Breasts", "Nipples") - relevant_layers = list(BODY_BEHIND_LAYER, BODY_FRONTEST_LAYER) + relevant_layers = list(BODY_BEHIND_LAYER, BREASTS_LAYER) //Giving these their own unique layers now. PLEASE PLEASE PLEASE if anyone sees this, and wants to fix the names of the Iconstates in the various DMI files... I would love it. Check _sprite_accessory.dm as well to change what string is appended to the state. I'm leaving it as "FRONT" for now so I don't have to rename them all over AGAIN... - Jon //Caustic Edit end /datum/sprite_accessory/breasts/get_icon_state(obj/item/organ/organ, obj/item/bodypart/bodypart, mob/living/carbon/owner) @@ -175,7 +176,7 @@ /datum/sprite_accessory/vagina icon = 'icons/mob/sprite_accessory/genitals/nethers.dmi' color_key_name = "Nethers" - relevant_layers = list(BODY_FRONT_LAYER) + relevant_layers = list(CROTCH_LAYER) //Caustic Edit - Giving these their own unique layers now. PLEASE PLEASE PLEASE if anyone sees this, and wants to fix the names of the Iconstates in the various DMI files... I would love it. Check _sprite_accessory.dm as well to change what string is appended to the state. I'm leaving it as "FRONT" for now so I don't have to rename them all over AGAIN... - Jon /datum/sprite_accessory/vagina/adjust_appearance_list(list/appearance_list, obj/item/organ/organ, obj/item/bodypart/bodypart, mob/living/carbon/owner) generic_gender_feature_adjust(appearance_list, organ, bodypart, owner, OFFSET_BELT, OFFSET_BELT_F) diff --git a/code/modules/mob/dead/new_player/sprite_accessory/neck_features.dm b/code/modules/mob/dead/new_player/sprite_accessory/neck_features.dm index 01380840a1a..b55a136ab24 100644 --- a/code/modules/mob/dead/new_player/sprite_accessory/neck_features.dm +++ b/code/modules/mob/dead/new_player/sprite_accessory/neck_features.dm @@ -1,6 +1,6 @@ /datum/sprite_accessory/neck_feature abstract_type = /datum/sprite_accessory/neck_feature - relevant_layers = list(BODY_ADJ_LAYER) + relevant_layers = list(GLASSES_LAYER) //Caustic Edit - Moving layers from BODY_ADJ_LAYER to GLASSES_LAYER, this puts it above breasts and belly but still under neck /datum/sprite_accessory/neck_feature/adjust_appearance_list(list/appearance_list, obj/item/organ/organ, obj/item/bodypart/bodypart, mob/living/carbon/owner) generic_gender_feature_adjust(appearance_list, organ, bodypart, owner, OFFSET_NECK, OFFSET_NECK_F) diff --git a/code/modules/mob/living/carbon/human/examine.dm b/code/modules/mob/living/carbon/human/examine.dm index a5ff62a7fdf..26ad05127a2 100644 --- a/code/modules/mob/living/carbon/human/examine.dm +++ b/code/modules/mob/living/carbon/human/examine.dm @@ -593,6 +593,20 @@ //Gets encapsulated with a warning span var/list/msg = list() + //Caustic Edit - Add in the missing Virgo/Chomp code related examine strings! + var/list/vorestrings = list() + //vorestrings += examine_weight() //Nothing currently modifies this at all so... Just commented out for now until we actually add things for it. + vorestrings += examine_nutrition() + vorestrings += formatted_vore_examine() + vorestrings += examine_pickup_size() + vorestrings += examine_step_size() + vorestrings += examine_body_writing() + for(var/entry in vorestrings) + if(entry == "" || entry == null) + vorestrings -= entry + msg += vorestrings + //Caustic Edit End + var/appears_dead = FALSE if(stat == DEAD || (HAS_TRAIT(src, TRAIT_FAKEDEATH))) appears_dead = TRUE diff --git a/code/modules/tgchat/to_chat.dm b/code/modules/tgchat/to_chat.dm index 827c7c14843..d295ea3b4b1 100644 --- a/code/modules/tgchat/to_chat.dm +++ b/code/modules/tgchat/to_chat.dm @@ -30,6 +30,9 @@ if(target == world) target = GLOB.clients + if(islist(target) && !LAZYLEN(target)) + return + // Build a message var/message = list() if(type) message["type"] = type @@ -76,6 +79,9 @@ if(target == world) target = GLOB.clients + if(islist(target) && !LAZYLEN(target)) + return + // Build a message var/message = list() if(type) message["type"] = type diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm index 187f566a3d5..27e4b8c908d 100644 --- a/code/modules/tgui/tgui.dm +++ b/code/modules/tgui/tgui.dm @@ -215,11 +215,11 @@ /datum/tgui/proc/send_full_update(custom_data, force) if(!user.client || !initialized || closing) return - if(!COOLDOWN_FINISHED(src, refresh_cooldown)) - refreshing = TRUE - addtimer(CALLBACK(src, PROC_REF(send_full_update), custom_data, force), COOLDOWN_TIMELEFT(src, refresh_cooldown), TIMER_UNIQUE) - return - refreshing = FALSE + //if(!COOLDOWN_FINISHED(src, refresh_cooldown)) + //refreshing = TRUE + //addtimer(CALLBACK(src, PROC_REF(send_full_update), custom_data, force), COOLDOWN_TIMELEFT(src, refresh_cooldown), TIMER_UNIQUE) + //return + //refreshing = FALSE var/should_update_data = force || status >= UI_UPDATE window.send_message("update", get_payload( custom_data, diff --git a/code/modules/tgui_input/text.dm b/code/modules/tgui_input/text.dm index fb10390db3e..669c0f88a6c 100644 --- a/code/modules/tgui_input/text.dm +++ b/code/modules/tgui_input/text.dm @@ -76,11 +76,11 @@ /// The title of the TGUI window var/title // Whether to use a big modal variant for very large text input - var/bigmodal + //var/bigmodal /// The TGUI UI state that will be returned in ui_state(). Default: always_state var/datum/ui_state/state -/datum/tgui_input_text/New(mob/user, message, title, default, max_length, multiline, encode, timeout, ui_state, bigmodal) +/datum/tgui_input_text/New(mob/user, message, title, default, max_length, multiline, encode, timeout, ui_state/*, bigmodal*/) src.default = default src.encode = encode src.max_length = max_length @@ -88,7 +88,7 @@ src.multiline = multiline src.title = title src.state = ui_state - src.bigmodal = bigmodal + //src.bigmodal = bigmodal if (timeout) src.timeout = timeout start_time = world.time @@ -130,7 +130,7 @@ data["swapped_buttons"] = FALSE // !user.read_preference(/datum/preference/toggle/tgui_swapped_buttons) data["title"] = title data["spellcheck"] = FALSE // user.read_preference(/datum/preference/toggle/tgui_use_spellcheck) - data["bigmodal"] = bigmodal + //data["bigmodal"] = bigmodal return data /datum/tgui_input_text/ui_data(mob/user) diff --git a/modular_causticcove/code/modules/merporgans/belly.dm b/modular_causticcove/code/modules/merporgans/belly.dm index d977ed1e053..b2081625b48 100644 --- a/modular_causticcove/code/modules/merporgans/belly.dm +++ b/modular_causticcove/code/modules/merporgans/belly.dm @@ -48,4 +48,4 @@ GLOBAL_LIST_INIT(named_belly_sizes, list( icon_state = "pair" name = "Belly" color_key_defaults = list(KEY_CHEST_COLOR) - relevant_layers = list(BODY_BEHIND_LAYER, BODY_FRONTER_LAYER) + relevant_layers = list(BODY_BEHIND_LAYER, BELLY_LAYER) //Giving these their own unique layers now. PLEASE PLEASE PLEASE if anyone sees this, and wants to fix the names of the Iconstates in the various DMI files... I would love it. Check _sprite_accessory.dm as well to change what string is appended to the state. I'm leaving it as "FRONT" for now so I don't have to rename them all over AGAIN... - Jon diff --git a/modular_causticcove/code/modules/merporgans/butt.dm b/modular_causticcove/code/modules/merporgans/butt.dm index 77aca28073a..67c44e5e178 100644 --- a/modular_causticcove/code/modules/merporgans/butt.dm +++ b/modular_causticcove/code/modules/merporgans/butt.dm @@ -33,7 +33,7 @@ GLOBAL_LIST_INIT(named_butt_sizes, list( /datum/sprite_accessory/butt icon = 'modular_causticcove/icons/mob/merp_organs/butt.dmi' color_key_name = "Butt" - relevant_layers = list(BODY_ASS_LAYER/*, BODY_FRONT_LAYER*/) + relevant_layers = list(ASS_LAYER/*, BODY_FRONT_LAYER*/) /datum/sprite_accessory/butt/adjust_appearance_list(list/appearance_list, obj/item/organ/organ, obj/item/bodypart/bodypart, mob/living/carbon/owner) if(!isdwarf(owner) && !isgoblinp(owner) && !iskobold(owner) && !isvermin(owner)) diff --git a/modular_causticcove/code/modules/mob/dead/new_player/sprite_accessory/genitals.dm b/modular_causticcove/code/modules/mob/dead/new_player/sprite_accessory/genitals.dm index 09e7edde6ad..6538261dfa1 100644 --- a/modular_causticcove/code/modules/mob/dead/new_player/sprite_accessory/genitals.dm +++ b/modular_causticcove/code/modules/mob/dead/new_player/sprite_accessory/genitals.dm @@ -16,7 +16,7 @@ icon_state = "pair" name = "Belly" color_key_defaults = list(KEY_CHEST_COLOR) - relevant_layers = list(BODY_BEHIND_LAYER, BODY_FRONTER_LAYER) + relevant_layers = list(BODY_BEHIND_LAYER, BELLY_LAYER) //Caustic Edit - Giving these their own unique layers now. PLEASE PLEASE PLEASE if anyone sees this, and wants to fix the names of the Iconstates in the various DMI files... I would love it. Check _sprite_accessory.dm as well to change what string is appended to the state. I'm leaving it as "FRONT" for now so I don't have to rename them all over AGAIN... - Jon /datum/sprite_accessory/butt icon = 'modular_causticcove/icons/mob/merp_organs/butt.dmi' diff --git a/modular_causticcove/code/modules/size_scaling/resizing/resize_organs.dm b/modular_causticcove/code/modules/size_scaling/resizing/resize_organs.dm new file mode 100644 index 00000000000..759985e87f1 --- /dev/null +++ b/modular_causticcove/code/modules/size_scaling/resizing/resize_organs.dm @@ -0,0 +1,49 @@ +/proc/resize_breasts(var/size, var/mob/living/carbon/human/target) + if(!target || !size) + return + + var/obj/item/organ/breasts/booba = target.internal_organs_slot[ORGAN_SLOT_BREASTS] + if(!booba) + return + + booba.breast_size = size + +/proc/resize_belly(var/size, var/mob/living/carbon/human/target) + if(!target || !size) + return + + var/obj/item/organ/belly/tum = target.internal_organs_slot[ORGAN_SLOT_BELLY] + if(!tum) + return + + tum.belly_size = size + +/proc/resize_penis(var/size, var/mob/living/carbon/human/target) + if(!target || !size) + return + + var/obj/item/organ/penis/dong = target.internal_organs_slot[ORGAN_SLOT_PENIS] + if(!dong) + return + + dong.penis_size = size + +/proc/resize_testicles(var/size, var/mob/living/carbon/human/target) + if(!target || !size) + return + + var/obj/item/organ/testicles/balls = target.internal_organs_slot[ORGAN_SLOT_TESTICLES] + if(!balls) + return + + balls.ball_size = size + +/proc/resize_butt(var/size, var/mob/living/carbon/human/target) + if(!target || !size) + return + + var/obj/item/organ/butt/ass = target.internal_organs_slot[ORGAN_SLOT_BUTT] + if(!ass) + return + + ass.organ_size = size diff --git a/modular_causticcove/code/modules/vore/eating/belly_import.dm b/modular_causticcove/code/modules/vore/eating/belly_import.dm index f8b410c8487..c9b11fd59d5 100644 --- a/modular_causticcove/code/modules/vore/eating/belly_import.dm +++ b/modular_causticcove/code/modules/vore/eating/belly_import.dm @@ -505,7 +505,7 @@ if(isnum(belly_data["shrink_grow_size"])) var/new_shrink_grow_size = belly_data["shrink_grow_size"] - new_belly.shrink_grow_size = CLAMP(new_shrink_grow_size, 0.25, 2) + new_belly.shrink_grow_size = CLAMP(new_shrink_grow_size, RESIZE_MINIMUM, RESIZE_MAXIMUM) if(isnum(belly_data["vorespawn_blacklist"])) var/new_vorespawn_blacklist = belly_data["vorespawn_blacklist"] @@ -789,25 +789,25 @@ new_belly.tail_extra_overlay2 = new_tail_extra_overlay2 */ if(istext(belly_data["belly_fullscreen_color"])) - var/new_belly_fullscreen_color = sanitize_hexcolor(belly_data["belly_fullscreen_color"],default = new_belly.belly_fullscreen_color) + var/new_belly_fullscreen_color = sanitize_hexcolor(belly_data["belly_fullscreen_color"], include_crunch = TRUE, default = new_belly.belly_fullscreen_color) new_belly.belly_fullscreen_color = new_belly_fullscreen_color if(istext(belly_data["belly_fullscreen_color2"])) - var/new_belly_fullscreen_color2 = sanitize_hexcolor(belly_data["belly_fullscreen_color2"],default = new_belly.belly_fullscreen_color2) + var/new_belly_fullscreen_color2 = sanitize_hexcolor(belly_data["belly_fullscreen_color2"], include_crunch = TRUE, default = new_belly.belly_fullscreen_color2) new_belly.belly_fullscreen_color2 = new_belly_fullscreen_color2 else if(istext(belly_data["belly_fullscreen_color_secondary"])) // Inter server support between virgo and chomp! - var/new_belly_fullscreen_color2 = sanitize_hexcolor(belly_data["belly_fullscreen_color_secondary"],default = new_belly.belly_fullscreen_color2) + var/new_belly_fullscreen_color2 = sanitize_hexcolor(belly_data["belly_fullscreen_color_secondary"], include_crunch = TRUE, default = new_belly.belly_fullscreen_color2) new_belly.belly_fullscreen_color2 = new_belly_fullscreen_color2 if(istext(belly_data["belly_fullscreen_color3"])) - var/new_belly_fullscreen_color3 = sanitize_hexcolor(belly_data["belly_fullscreen_color3"],default = new_belly.belly_fullscreen_color3) + var/new_belly_fullscreen_color3 = sanitize_hexcolor(belly_data["belly_fullscreen_color3"], include_crunch = TRUE, default = new_belly.belly_fullscreen_color3) new_belly.belly_fullscreen_color3 = new_belly_fullscreen_color3 else if(istext(belly_data["belly_fullscreen_color_trinary"])) // Inter server support between virgo and chomp! - var/new_belly_fullscreen_color3 = sanitize_hexcolor(belly_data["belly_fullscreen_color_trinary"],default = new_belly.belly_fullscreen_color3) + var/new_belly_fullscreen_color3 = sanitize_hexcolor(belly_data["belly_fullscreen_color_trinary"], include_crunch = TRUE, default = new_belly.belly_fullscreen_color3) new_belly.belly_fullscreen_color3 = new_belly_fullscreen_color3 if(istext(belly_data["belly_fullscreen_color4"])) - var/new_belly_fullscreen_color4 = sanitize_hexcolor(belly_data["belly_fullscreen_color4"],default = new_belly.belly_fullscreen_color4) + var/new_belly_fullscreen_color4 = sanitize_hexcolor(belly_data["belly_fullscreen_color4"], include_crunch = TRUE, default = new_belly.belly_fullscreen_color4) new_belly.belly_fullscreen_color4 = new_belly_fullscreen_color4 if(istext(belly_data["belly_fullscreen_alpha"])) @@ -1073,11 +1073,11 @@ new_belly.reagent_mode_flags += new_belly.reagent_mode_flag_list[reagent_flag] if(istext(belly_data["custom_reagentcolor"])) - var/custom_reagentcolor = sanitize_hexcolor(belly_data["custom_reagentcolor"],default = new_belly.custom_reagentcolor) + var/custom_reagentcolor = sanitize_hexcolor(belly_data["custom_reagentcolor"], include_crunch = TRUE, default = new_belly.custom_reagentcolor) new_belly.custom_reagentcolor = custom_reagentcolor if(istext(belly_data["mush_color"])) - var/mush_color = sanitize_hexcolor(belly_data["mush_color"],default = new_belly.mush_color) + var/mush_color = sanitize_hexcolor(belly_data["mush_color"], include_crunch = TRUE, default = new_belly.mush_color) new_belly.mush_color = mush_color if(istext(belly_data["mush_alpha"])) diff --git a/modular_causticcove/code/modules/vore/eating/bellymodes_vr.dm b/modular_causticcove/code/modules/vore/eating/bellymodes_vr.dm index e6dae21a704..3a587f77732 100644 --- a/modular_causticcove/code/modules/vore/eating/bellymodes_vr.dm +++ b/modular_causticcove/code/modules/vore/eating/bellymodes_vr.dm @@ -210,7 +210,7 @@ var/mob/living/carbon/human/H = L //Numbing flag - if(mode_flags & DM_FLAG_NUMBING) + if(mode_flags & DM_FLAG_NUMBING) //Caustic - I don't think we have this in actually. Might need to make a reagent with an effect? if(H.reagents.get_reagent_amount(REAGENT_ID_NUMBENZYME) < 2) H.reagents.add_reagent(REAGENT_ID_NUMBENZYME,4) diff --git a/modular_causticcove/code/modules/vore/eating/human.dm b/modular_causticcove/code/modules/vore/eating/human.dm new file mode 100644 index 00000000000..f3fe84d8e26 --- /dev/null +++ b/modular_causticcove/code/modules/vore/eating/human.dm @@ -0,0 +1,108 @@ +/mob/living/carbon/human/proc/examine_weight() + if(!show_pudge() || !weight_message_visible) //Some clothing or equipment can hide this. + return "" + var/message = "" + var/weight_examine = round(weight) + switch(weight_examine) + if(0 to 74) + message = weight_messages[1] + if(75 to 99) + message = weight_messages[2] + if(100 to 124) + message = weight_messages[3] + if(125 to 174) + message = weight_messages[4] + if(175 to 224) + message = weight_messages[5] + if(225 to 274) + message = weight_messages[6] + if(275 to 325) + message = weight_messages[7] + if(325 to 374) + message = weight_messages[8] + if(375 to 474) + message = weight_messages[9] + else + message = weight_messages[10] + if(message) + message = span_notice("[message]") + return message //Credit to Aronai for helping me actually get this working! + +/mob/living/carbon/human/proc/examine_nutrition() + if(!show_pudge() || !nutrition_message_visible) //Some clothing or equipment can hide this. + return "" + //if(nutrition_hidden) // Chomp Edit + // return "" + var/message = "" + var/nutrition_examine = round(nutrition) + switch(nutrition_examine) + if(0 to 49) + message = nutrition_messages[1] + if(50 to 99) + message = nutrition_messages[2] + if(100 to 499) + message = nutrition_messages[3] + if(500 to 999) // Fat. + message = nutrition_messages[4] + if(1000 to 1399) + message = nutrition_messages[5] + if(1400 to 1934) // One person fully digested. + message = nutrition_messages[6] + if(1935 to 3004) // Two people. + message = nutrition_messages[7] + if(3005 to 4074) // Three people. + message = nutrition_messages[8] + if(4075 to 5124) // Four people. + message = nutrition_messages[9] + if(5125 to INFINITY) // More. + message = nutrition_messages[10] + if(message) + message = span_notice("[message]") + return message + +/mob/living/carbon/human/proc/examine_pickup_size(mob/living/H) + var/message = "" + if(istype(H) && (H.get_effective_size(FALSE) - src.get_effective_size(TRUE)) >= 0.50) + message = span_blue("They are small enough that you could easily pick them up!") + return message + +/mob/living/carbon/human/proc/examine_step_size(mob/living/H) + var/message = "" + if(istype(H) && (H.get_effective_size(FALSE) - src.get_effective_size(TRUE)) >= 0.75) + message = span_red("They are small enough that you could easily trample them!") + return message + +/mob/living/carbon/human/proc/examine_body_writing() + . = list() + + for(var/obj/item/bodypart/bodypart in bodyparts) + var/writing = LAZYACCESS(body_writing, bodypart) + if(writing) + var/visible = FALSE + switch(bodypart.body_zone) + if(BODY_ZONE_CHEST) + if(!wear_shirt && !wear_armor && !cloak) + visible = TRUE + if(BODY_ZONE_HEAD) + if(!wear_mask && !head) + visible = TRUE + if(BODY_ZONE_L_ARM) + if(!wear_shirt && !wear_armor && !wear_wrists) + visible = TRUE + if(BODY_ZONE_R_ARM) + if(!wear_shirt && !wear_armor && !wear_wrists) + visible = TRUE + if(BODY_ZONE_L_LEG) + if(!wear_pants) + visible = TRUE + if(BODY_ZONE_R_LEG) + if(!wear_pants) + visible = TRUE + if(BODY_ZONE_TAUR) + if(!wear_pants) + visible = TRUE + + if(!visible) + continue + + . += span_notice("[p_they(TRUE)] [p_have()] \"[writing]\" written on [p_their()] [parse_zone(bodypart)].") diff --git a/modular_causticcove/code/modules/vore/eating/panel_databackend/vorepanel_set_attribute.dm b/modular_causticcove/code/modules/vore/eating/panel_databackend/vorepanel_set_attribute.dm index 8a8b0cde964..37074ef0464 100644 --- a/modular_causticcove/code/modules/vore/eating/panel_databackend/vorepanel_set_attribute.dm +++ b/modular_causticcove/code/modules/vore/eating/panel_databackend/vorepanel_set_attribute.dm @@ -824,25 +824,25 @@ host.vore_selected.clear_preview(host) //Clears the stomach overlay. This is a failsafe but shouldn't occur. . = TRUE if("b_fullscreen_color") - var/newcolor = sanitize_hexcolor(lowertext(params["val"])) + var/newcolor = sanitize_hexcolor(lowertext(params["val"]), include_crunch = TRUE) if(newcolor) host.vore_selected.belly_fullscreen_color = newcolor host.vore_selected.update_internal_overlay() . = TRUE if("b_fullscreen_color2") - var/newcolor2 = sanitize_hexcolor(lowertext(params["val"])) + var/newcolor2 = sanitize_hexcolor(lowertext(params["val"]), include_crunch = TRUE) if(newcolor2) host.vore_selected.belly_fullscreen_color2 = newcolor2 host.vore_selected.update_internal_overlay() . = TRUE if("b_fullscreen_color3") - var/newcolor3 = sanitize_hexcolor(lowertext(params["val"])) + var/newcolor3 = sanitize_hexcolor(lowertext(params["val"]), include_crunch = TRUE) if(newcolor3) host.vore_selected.belly_fullscreen_color3 = newcolor3 host.vore_selected.update_internal_overlay() . = TRUE if("b_fullscreen_color4") - var/newcolor4 = sanitize_hexcolor(lowertext(params["val"])) + var/newcolor4 = sanitize_hexcolor(lowertext(params["val"]), include_crunch = TRUE) if(newcolor4) host.vore_selected.belly_fullscreen_color4 = newcolor4 host.vore_selected.update_internal_overlay() diff --git a/modular_causticcove/code/modules/vore/eating/panel_databackend/vorepanel_set_liqattribute.dm b/modular_causticcove/code/modules/vore/eating/panel_databackend/vorepanel_set_liqattribute.dm index 8c3f4984c27..27a916e9aa9 100644 --- a/modular_causticcove/code/modules/vore/eating/panel_databackend/vorepanel_set_liqattribute.dm +++ b/modular_causticcove/code/modules/vore/eating/panel_databackend/vorepanel_set_liqattribute.dm @@ -108,7 +108,7 @@ host.vore_selected.update_internal_overlay() . = TRUE if("b_custom_reagentcolor") - var/newcolor = sanitize_hexcolor(lowertext(params["val"])) + var/newcolor = sanitize_hexcolor(lowertext(params["val"]), include_crunch = TRUE) if(newcolor) host.vore_selected.custom_reagentcolor = newcolor else @@ -143,7 +143,7 @@ host.vore_selected.update_internal_overlay() . = TRUE if("b_mush_color") - var/newcolor = sanitize_hexcolor(lowertext(params["val"])) + var/newcolor = sanitize_hexcolor(lowertext(params["val"]), include_crunch = TRUE) if(newcolor) host.vore_selected.mush_color = newcolor host.vore_selected.update_internal_overlay() @@ -200,7 +200,7 @@ host.vore_selected.update_internal_overlay() . = TRUE if("b_custom_ingested_color") - var/newcolor = sanitize_hexcolor(lowertext(params["val"])) + var/newcolor = sanitize_hexcolor(lowertext(params["val"]), include_crunch = TRUE) if(newcolor) host.vore_selected.custom_ingested_color = newcolor else diff --git a/modular_causticcove/code/modules/vore/eating/vorepanel_vr.dm b/modular_causticcove/code/modules/vore/eating/vorepanel_vr.dm index 7af92b597c8..b8fe5a182a1 100644 --- a/modular_causticcove/code/modules/vore/eating/vorepanel_vr.dm +++ b/modular_causticcove/code/modules/vore/eating/vorepanel_vr.dm @@ -875,8 +875,8 @@ available_options += "Transform" available_options += "Health Check" // Add Reforming - /*if(isobserver(target)) - available_options += "Reform"*/ + if(isobserver(target)) + available_options += "Reform" if(isliving(target)) var/mob/living/datarget = target diff --git a/modular_causticcove/icons/mob/merp_organs/butt.dmi b/modular_causticcove/icons/mob/merp_organs/butt.dmi index f44f2f4b61e..bc0bb1d8264 100644 Binary files a/modular_causticcove/icons/mob/merp_organs/butt.dmi and b/modular_causticcove/icons/mob/merp_organs/butt.dmi differ diff --git a/roguetown.dme b/roguetown.dme index a137e7f2637..e3484ec5864 100644 --- a/roguetown.dme +++ b/roguetown.dme @@ -3137,6 +3137,7 @@ #include "modular_causticcove\code\modules\size_scaling\sizecat_types.dm" #include "modular_causticcove\code\modules\size_scaling\sizecats.dm" #include "modular_causticcove\code\modules\size_scaling\resizing\holder_micro_vr.dm" +#include "modular_causticcove\code\modules\size_scaling\resizing\resize_organs.dm" #include "modular_causticcove\code\modules\size_scaling\resizing\resize_vr.dm" #include "modular_causticcove\code\modules\size_scaling\size_verbs\standardized_sprite_verb.dm" #include "modular_causticcove\code\modules\size_scaling\spells\shrink.dm" @@ -3186,6 +3187,7 @@ #include "modular_causticcove\code\modules\vore\eating\digest_act_vr.dm" #include "modular_causticcove\code\modules\vore\eating\dumbass_globals.dm" #include "modular_causticcove\code\modules\vore\eating\exportpanel_vr.dm" +#include "modular_causticcove\code\modules\vore\eating\human.dm" #include "modular_causticcove\code\modules\vore\eating\inbelly_spawn.dm" #include "modular_causticcove\code\modules\vore\eating\living_bellies.dm" #include "modular_causticcove\code\modules\vore\eating\living_vr.dm" diff --git a/sound/music/cmode/towner/combat_archivist.ogg b/sound/music/cmode/towner/combat_archivist.ogg new file mode 100644 index 00000000000..bb1a160bee8 Binary files /dev/null and b/sound/music/cmode/towner/combat_archivist.ogg differ diff --git a/tgui/bun.lock b/tgui/bun.lock index 95342159f30..3f681de5c38 100644 --- a/tgui/bun.lock +++ b/tgui/bun.lock @@ -22,8 +22,8 @@ "devDependencies": { "@happy-dom/global-registrator": "^17.4.7", "@types/bun": "^1.2.14", - "@types/react": "^19.1.5", - "@types/react-dom": "^19.1.5", + "@types/react": "^19.2.9", + "@types/react-dom": "^19.2.3", "@types/webpack-env": "^1.18.8", "@types/wicg-file-system-access": "^2023.10.6", }, @@ -49,17 +49,18 @@ "dependencies": { "common": "workspace:*", "dateformat": "^5.0.3", - "dompurify": "^3.2.5", - "es-toolkit": "^1.39.3", + "dompurify": "^3.3.1", + "es-toolkit": "^1.44.0", "highlight.js": "^11.11.1", - "jotai": "^2.12.4", - "js-yaml": "^4.1.0", - "marked": "^15.0.11", - "marked-base-url": "^1.1.6", - "marked-smartypants": "^1.1.9", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "tgui-core": "^4.2.3", + "jotai": "^2.16.2", + "js-yaml": "^4.1.1", + "marked": "^17.0.1", + "marked-base-url": "^1.1.8", + "marked-smartypants": "^1.1.11", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "react-json-tree": "^0.20.0", + "tgui-core": "^5.9.0", "tgui-dev-server": "workspace:*", }, }, @@ -77,13 +78,15 @@ "version": "6.0.0", "dependencies": { "common": "workspace:*", - "dompurify": "^3.2.5", - "es-toolkit": "^1.39.3", - "react": "^19.1.0", - "react-dom": "^19.1.0", + "dompurify": "^3.3.1", + "es-toolkit": "^1.44.0", + "jotai": "^2.16.2", + "react": "^19.2.3", + "react-dom": "^19.2.3", "tgui": "workspace:*", - "tgui-core": "^4.2.3", + "tgui-core": "^5.9.0", "tgui-dev-server": "workspace:*", + "zod": "^4.3.6", }, }, "packages/tgui-setup": { @@ -114,15 +117,15 @@ "@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="], - "@floating-ui/core": ["@floating-ui/core@1.7.2", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw=="], + "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], - "@floating-ui/dom": ["@floating-ui/dom@1.7.2", "", { "dependencies": { "@floating-ui/core": "^1.7.2", "@floating-ui/utils": "^0.2.10" } }, "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA=="], + "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], - "@floating-ui/react": ["@floating-ui/react@0.27.13", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.4", "@floating-ui/utils": "^0.2.10", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-Qmj6t9TjgWAvbygNEu1hj4dbHI9CY0ziCMIJrmYoDIn9TUAH5lRmiIeZmRd4c6QEZkzdoH7jNnoNyoY1AIESiA=="], + "@floating-ui/react": ["@floating-ui/react@0.27.19", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog=="], - "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.4", "", { "dependencies": { "@floating-ui/dom": "^1.7.2" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw=="], + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="], - "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], "@happy-dom/global-registrator": ["@happy-dom/global-registrator@17.6.3", "", { "dependencies": { "happy-dom": "^17.6.3" } }, "sha512-SE8Mu6bdkgKENemVg20yMrKdYeAvVesQrLPVfNBKnhicCM3ylHV9oIDzDMxSFj3qYCrHrIWOZsItOXSafrM4Tw=="], @@ -268,6 +271,8 @@ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/lodash": ["@types/lodash@4.17.24", "", {}, "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ=="], + "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], "@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="], @@ -278,9 +283,9 @@ "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], - "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], - "@types/react-dom": ["@types/react-dom@19.1.6", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="], + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], "@types/retry": ["@types/retry@0.12.2", "", {}, "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow=="], @@ -442,10 +447,14 @@ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], "colorjs.io": ["colorjs.io@0.5.2", "", {}, "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw=="], @@ -490,7 +499,7 @@ "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="], - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "cubic2quad": ["cubic2quad@1.2.1", "", {}, "sha512-wT5Y7mO8abrV16gnssKdmIhIbA9wSkeMzhh27jAguKrV82i24wER0vL5TGhUJ9dbJNDcigoRZ0IAHFEEEI4THQ=="], @@ -540,7 +549,7 @@ "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], - "dompurify": ["dompurify@3.2.6", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ=="], + "dompurify": ["dompurify@3.3.3", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA=="], "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], @@ -582,7 +591,7 @@ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], - "es-toolkit": ["es-toolkit@1.39.6", "", {}, "sha512-uiVjnLem6kkfXumlwUEWEKnwUN5QbSEB0DHy2rNJt0nkYcob5K0TXJ7oJRzhAcvx+SRmz4TahKyN5V9cly/IPA=="], + "es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -784,6 +793,8 @@ "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], + "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], + "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], @@ -860,11 +871,11 @@ "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], - "jotai": ["jotai@2.12.5", "", { "peerDependencies": { "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-G8m32HW3lSmcz/4mbqx0hgJIQ0ekndKWiYP7kWVKi0p6saLXdSoye+FZiOFyonnd7Q482LCzm8sMDl7Ar1NWDw=="], + "jotai": ["jotai@2.18.1", "", { "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/template": ">=7.0.0", "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@babel/core", "@babel/template", "@types/react", "react"] }, "sha512-e0NOzK+yRFwHo7DOp0DS0Ycq74KMEAObDWFGmfEL28PD9nLqBTt3/Ug7jf9ca72x0gC9LQZG9zH+0ISICmy3iA=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], "jsbn": ["jsbn@1.1.0", "", {}, "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="], @@ -892,6 +903,8 @@ "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lodash-es": ["lodash-es@4.17.23", "", {}, "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg=="], + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], @@ -900,11 +913,11 @@ "make-fetch-happen": ["make-fetch-happen@13.0.1", "", { "dependencies": { "@npmcli/agent": "^2.0.0", "cacache": "^18.0.0", "http-cache-semantics": "^4.1.1", "is-lambda": "^1.0.1", "minipass": "^7.0.2", "minipass-fetch": "^3.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.3", "proc-log": "^4.2.0", "promise-retry": "^2.0.1", "ssri": "^10.0.0" } }, "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA=="], - "marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], + "marked": ["marked@17.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ=="], - "marked-base-url": ["marked-base-url@1.1.7", "", { "peerDependencies": { "marked": ">= 4 < 17" } }, "sha512-CJOfpG2/XOEp8UuI5H0tbELxuS1v8Ud705jamEIpWBQDdkda1i+LrafxLn41rlxhGEeJqo27b/hBFVYHWOYccw=="], + "marked-base-url": ["marked-base-url@1.1.8", "", { "peerDependencies": { "marked": ">= 4 < 18" } }, "sha512-RA80m/VTq82jAnpusyK7B4M45TZ3LMB1ymrdAJI3oaSG4+8MzBc8FFlVKIAhkBrfRM7CxeTHTGUVVK59/eiQyA=="], - "marked-smartypants": ["marked-smartypants@1.1.10", "", { "dependencies": { "smartypants": "^0.2.2" }, "peerDependencies": { "marked": ">=4 <17" } }, "sha512-XGK59M2nhy3Jpa0kdWSXQuKn908VkKbqK1IqF8Rk5QV619OWBs2/rkcg/PVhpKkADlRKJSYe6XqDMZMkZywT4g=="], + "marked-smartypants": ["marked-smartypants@1.1.11", "", { "dependencies": { "smartypants": "^0.2.2" }, "peerDependencies": { "marked": ">=4 <18" } }, "sha512-Jt0eq/6rf9oXDfEKPzQ0z7UzVWcEAK3L6QBBQzbwV8bT304OvPVLTqpH3yvkSung9foOM4s120TMHEHP76Metg=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -1098,12 +1111,16 @@ "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], - "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], - "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], + "react-base16-styling": ["react-base16-styling@0.10.0", "", { "dependencies": { "@types/lodash": "^4.17.0", "color": "^4.2.3", "csstype": "^3.1.3", "lodash-es": "^4.17.21" } }, "sha512-H1k2eFB6M45OaiRru3PBXkuCcn2qNmx+gzLb4a9IPMR7tMH8oBRXU5jGbPDYG1Hz+82d88ED0vjR8BmqU3pQdg=="], + + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "react-json-tree": ["react-json-tree@0.20.0", "", { "dependencies": { "@types/lodash": "^4.17.15", "react-base16-styling": "^0.10.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-h+f9fUNAxzBx1rbrgUF7+zSWKGHDtt2VPYLErIuB0JyKGnWgFMM21ksqQyb3EXwXNnoMW2rdE5kuAaubgGOx2Q=="], + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -1186,7 +1203,7 @@ "sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="], - "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "schema-utils": ["schema-utils@4.3.2", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ=="], @@ -1226,6 +1243,8 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], + "sirv": ["sirv@2.0.4", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ=="], "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], @@ -1314,7 +1333,7 @@ "tgui": ["tgui@workspace:packages/tgui"], - "tgui-core": ["tgui-core@4.3.3", "", { "dependencies": { "@floating-ui/react": "^0.27.13", "@nozbe/microfuzz": "^1.0.0" }, "peerDependencies": { "react": "^19.1.0", "react-dom": "^19.1.0" } }, "sha512-+eVCmmwbwH6X/SMSXBVGQwsPzIKI8Ho5+tFuIU82cxmWIlWyaJzlCb86+5yBGeeJy4+SCPo6Pv1WrCeHNQeJuw=="], + "tgui-core": ["tgui-core@5.9.0", "", { "dependencies": { "@floating-ui/react": "^0.27.16", "@nozbe/microfuzz": "^1.0.0" }, "peerDependencies": { "react": "^19.1.0", "react-dom": "^19.1.0" } }, "sha512-2GxhHuDaK56xck48bSOOvVLUDGFxyOgUkaS5xatp40LwXtOygmZT4bD7Pt7cQ2rY2j3WNfDeIaXipIbxL3Xqow=="], "tgui-dev-server": ["tgui-dev-server@workspace:packages/tgui-dev-server"], @@ -1452,6 +1471,10 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "@eslint/eslintrc/js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], @@ -1492,6 +1515,8 @@ "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], + "eslint/js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + "eslint-plugin-react/doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], diff --git a/tgui/package.json b/tgui/package.json index a49df5357fa..565359dea7b 100644 --- a/tgui/package.json +++ b/tgui/package.json @@ -36,8 +36,8 @@ "devDependencies": { "@happy-dom/global-registrator": "^17.4.7", "@types/bun": "^1.2.14", - "@types/react": "^19.1.5", - "@types/react-dom": "^19.1.5", + "@types/react": "^19.2.9", + "@types/react-dom": "^19.2.3", "@types/webpack-env": "^1.18.8", "@types/wicg-file-system-access": "^2023.10.6" } diff --git a/tgui/packages/common/assets.ts b/tgui/packages/common/assets.ts new file mode 100644 index 00000000000..958d472ca4e --- /dev/null +++ b/tgui/packages/common/assets.ts @@ -0,0 +1,30 @@ +const EXCLUDED_PATTERNS = [/v4shim/i]; + +export function loadStyleSheet(payload: string): void { + Byond.loadCss(payload); +} + +export function loadMappings( + payload: Record, + /** Lets you insert your own independent asset map. */ + map: Record, +): void { + for (const name in payload) { + // Skip anything that matches excluded patterns + if (EXCLUDED_PATTERNS.some((regex) => regex.test(name))) { + continue; + } + + const url = payload[name]; + const ext = name.split('.').pop(); + + map[name] = url; + + if (ext === 'css') { + Byond.loadCss(url); + } + if (ext === 'js') { + Byond.loadJs(url); + } + } +} diff --git a/tgui/packages/common/colorpicker.ts b/tgui/packages/common/colorpicker.ts index 95449c4c83c..f51d3ec02e1 100644 --- a/tgui/packages/common/colorpicker.ts +++ b/tgui/packages/common/colorpicker.ts @@ -1,252 +1,3 @@ -/* - * MIT License - * https://github.com/omgovich/react-colorful/ - * - * Copyright (c) 2020 Vlad Shilov - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -const round = (number: number, digits = 0, base = 10 ** digits): number => { - return Math.round(base * number) / base; -}; -export interface RgbColor { - r: number; - g: number; - b: number; -} -export interface RgbaColor extends RgbColor { - a: number; -} -export interface HslColor { - h: number; - s: number; - l: number; -} -export interface HslaColor extends HslColor { - a: number; -} -export interface HsvColor { - h: number; - s: number; - v: number; -} -export interface HsvaColor extends HsvColor { - a: number; -} -export type ObjectColor = - | RgbColor - | HslColor - | HsvColor - | RgbaColor - | HslaColor - | HsvaColor; -export type AnyColor = string | ObjectColor; -/** - * Valid CSS units. - * https://developer.mozilla.org/en-US/docs/Web/CSS/angle - */ -const angleUnits: Record = { - grad: 360 / 400, - turn: 360, - rad: 360 / (Math.PI * 2), -}; -export const hexToHsva = (hex: string): HsvaColor => rgbaToHsva(hexToRgba(hex)); -export const hexToRgba = (hex: string): RgbaColor => { - if (hex[0] === '#') hex = hex.substring(1); - if (hex.length < 6) { - return { - r: parseInt(hex[0] + hex[0], 16), - g: parseInt(hex[1] + hex[1], 16), - b: parseInt(hex[2] + hex[2], 16), - a: hex.length === 4 ? round(parseInt(hex[3] + hex[3], 16) / 255, 2) : 1, - }; - } - return { - r: parseInt(hex.substring(0, 2), 16), - g: parseInt(hex.substring(2, 4), 16), - b: parseInt(hex.substring(4, 6), 16), - a: hex.length === 8 ? round(parseInt(hex.substring(6, 8), 16) / 255, 2) : 1, - }; -}; -export const parseHue = (value: string, unit = 'deg'): number => { - return Number(value) * (angleUnits[unit] || 1); -}; -export const hslaStringToHsva = (hslString: string): HsvaColor => { - const matcher = - /hsla?\(?\s*(-?\d*\.?\d+)(deg|rad|grad|turn)?[,\s]+(-?\d*\.?\d+)%?[,\s]+(-?\d*\.?\d+)%?,?\s*[/\s]*(-?\d*\.?\d+)?(%)?\s*\)?/i; - const match = matcher.exec(hslString); - if (!match) return { h: 0, s: 0, v: 0, a: 1 }; - return hslaToHsva({ - h: parseHue(match[1], match[2]), - s: Number(match[3]), - l: Number(match[4]), - a: match[5] === undefined ? 1 : Number(match[5]) / (match[6] ? 100 : 1), - }); -}; -export const hslStringToHsva = hslaStringToHsva; -export const hslaToHsva = ({ h, s, l, a }: HslaColor): HsvaColor => { - s *= (l < 50 ? l : 100 - l) / 100; - return { - h: h, - s: s > 0 ? ((2 * s) / (l + s)) * 100 : 0, - v: l + s, - a, - }; -}; -export const hsvaToHex = (hsva: HsvaColor): string => - rgbaToHex(hsvaToRgba(hsva)); -export const hsvaToHsla = ({ h, s, v, a }: HsvaColor): HslaColor => { - const hh = ((200 - s) * v) / 100; - return { - h: round(h), - s: round( - hh > 0 && hh < 200 - ? ((s * v) / 100 / (hh <= 100 ? hh : 200 - hh)) * 100 - : 0, - ), - l: round(hh / 2), - a: round(a, 2), - }; -}; -export const hsvaToHslString = (hsva: HsvaColor): string => { - const { h, s, l } = hsvaToHsla(hsva); - return `hsl(${h}, ${s}%, ${l}%)`; -}; -export const hsvaToHsvString = (hsva: HsvaColor): string => { - const { h, s, v } = roundHsva(hsva); - return `hsv(${h}, ${s}%, ${v}%)`; -}; -export const hsvaToHsvaString = (hsva: HsvaColor): string => { - const { h, s, v, a } = roundHsva(hsva); - return `hsva(${h}, ${s}%, ${v}%, ${a})`; -}; -export const hsvaToHslaString = (hsva: HsvaColor): string => { - const { h, s, l, a } = hsvaToHsla(hsva); - return `hsla(${h}, ${s}%, ${l}%, ${a})`; -}; -export const hsvaToRgba = ({ h, s, v, a }: HsvaColor): RgbaColor => { - h = (h / 360) * 6; - s = s / 100; - v = v / 100; - const hh = Math.floor(h), - b = v * (1 - s), - c = v * (1 - (h - hh) * s), - d = v * (1 - (1 - h + hh) * s), - module = hh % 6; - return { - r: [v, c, b, b, d, v][module] * 255, - g: [d, v, v, c, b, b][module] * 255, - b: [b, b, d, v, v, c][module] * 255, - a: round(a, 2), - }; -}; -export const hsvaToRgbString = (hsva: HsvaColor): string => { - const { r, g, b } = hsvaToRgba(hsva); - return `rgb(${round(r)}, ${round(g)}, ${round(b)})`; -}; -export const hsvaToRgbaString = (hsva: HsvaColor): string => { - const { r, g, b, a } = hsvaToRgba(hsva); - return `rgba(${round(r)}, ${round(g)}, ${round(b)}, ${round(a, 2)})`; -}; -export const hsvaStringToHsva = (hsvString: string): HsvaColor => { - const matcher = - /hsva?\(?\s*(-?\d*\.?\d+)(deg|rad|grad|turn)?[,\s]+(-?\d*\.?\d+)%?[,\s]+(-?\d*\.?\d+)%?,?\s*[/\s]*(-?\d*\.?\d+)?(%)?\s*\)?/i; - const match = matcher.exec(hsvString); - if (!match) return { h: 0, s: 0, v: 0, a: 1 }; - return roundHsva({ - h: parseHue(match[1], match[2]), - s: Number(match[3]), - v: Number(match[4]), - a: match[5] === undefined ? 1 : Number(match[5]) / (match[6] ? 100 : 1), - }); -}; -export const hsvStringToHsva = hsvaStringToHsva; -export const rgbaStringToHsva = (rgbaString: string): HsvaColor => { - const matcher = - /rgba?\(?\s*(-?\d*\.?\d+)(%)?[,\s]+(-?\d*\.?\d+)(%)?[,\s]+(-?\d*\.?\d+)(%)?,?\s*[/\s]*(-?\d*\.?\d+)?(%)?\s*\)?/i; - const match = matcher.exec(rgbaString); - if (!match) return { h: 0, s: 0, v: 0, a: 1 }; - return rgbaToHsva({ - r: Number(match[1]) / (match[2] ? 100 / 255 : 1), - g: Number(match[3]) / (match[4] ? 100 / 255 : 1), - b: Number(match[5]) / (match[6] ? 100 / 255 : 1), - a: match[7] === undefined ? 1 : Number(match[7]) / (match[8] ? 100 : 1), - }); -}; -export const rgbStringToHsva = rgbaStringToHsva; -const format = (number: number) => { - const hex = number.toString(16); - return hex.length < 2 ? `0${hex}` : hex; -}; -export const rgbaToHex = ({ r, g, b, a }: RgbaColor): string => { - const alphaHex = a < 1 ? format(round(a * 255)) : ''; - return `#${format(round(r))}${format(round(g))}${format(round(b))}${alphaHex}`; -}; -export const rgbaToHsva = ({ r, g, b, a }: RgbaColor): HsvaColor => { - const max = Math.max(r, g, b); - const delta = max - Math.min(r, g, b); - // prettier-ignore - const hh = delta - ? max === r - ? (g - b) / delta - : max === g - ? 2 + (b - r) / delta - : 4 + (r - g) / delta - : 0; - return { - h: 60 * (hh < 0 ? hh + 6 : hh), - s: max ? (delta / max) * 100 : 0, - v: (max / 255) * 100, - a, - }; -}; -export const roundHsva = (hsva: HsvaColor): HsvaColor => ({ - h: round(hsva.h), - s: round(hsva.s), - v: round(hsva.v), - a: round(hsva.a, 2), -}); -export const rgbaToRgb = ({ r, g, b }: RgbaColor): RgbColor => ({ r, g, b }); -export const hslaToHsl = ({ h, s, l }: HslaColor): HslColor => ({ h, s, l }); -export const hsvaToHsv = (hsva: HsvaColor): HsvColor => { - const { h, s, v } = roundHsva(hsva); - return { h, s, v }; -}; -const hexMatcher = /^#?([0-9A-F]{3,8})$/i; -export const validHex = (value: string, alpha?: boolean): boolean => { - const match = hexMatcher.exec(value); - const length = match ? match[1].length : 0; - return ( - length === 3 || // '#rgb' format - length === 6 || // '#rrggbb' format - (!!alpha && length === 4) || // '#rgba' format - (!!alpha && length === 8) // '#rrggbbaa' format - ); -}; -// Source for the following luminance and contrast calculation code: https://blog.cristiana.tech/calculating-color-contrast-in-typescript-using-web-content-accessibility-guidelines-wcag -export const luminance = (rgb: RgbColor): number => { - const [r, g, b] = [rgb.r, rgb.g, rgb.b].map((v) => { - v /= 255; - return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; - }); - return r * 0.2126 + g * 0.7152 + b * 0.0722; -}; -export const contrast = ( - foreground: RgbColor, - background: RgbColor, -): number => { - const foreground_luminance = luminance(foreground); - const background_luminance = luminance(background); - return background_luminance < foreground_luminance - ? (background_luminance + 0.05) / (foreground_luminance + 0.05) - : (foreground_luminance + 0.05) / (background_luminance + 0.05); -}; - export const colorList = [ ['003366', '336699', '3366CC', '003399', '000099', '0000CC', '000066'], [ diff --git a/tgui/packages/common/package.json b/tgui/packages/common/package.json index f40c3b79237..a5d2ae076eb 100644 --- a/tgui/packages/common/package.json +++ b/tgui/packages/common/package.json @@ -2,7 +2,8 @@ "name": "common", "version": "4.3.1", "dependencies": { - "es-toolkit": "^1.39.3" + "es-toolkit": "^1.44.0", + "zod": "^4.3.6" }, "private": true } diff --git a/tgui/packages/common/storage.ts b/tgui/packages/common/storage.ts index 084ac3b55cd..f149c6ff548 100644 --- a/tgui/packages/common/storage.ts +++ b/tgui/packages/common/storage.ts @@ -6,12 +6,13 @@ * @license MIT */ -export const IMPL_MEMORY = 0; export const IMPL_HUB_STORAGE = 1; +export const IMPL_IFRAME_INDEXED_DB = 2; -type StorageImplementation = typeof IMPL_MEMORY | typeof IMPL_HUB_STORAGE; - -const KEY_NAME = 'azure'; +const KEY_NAME = 'chomp'; // CHOMPEdit - CHOMPStation Localstore +type StorageImplementation = + | typeof IMPL_HUB_STORAGE + | typeof IMPL_IFRAME_INDEXED_DB; type StorageBackend = { impl: StorageImplementation; @@ -33,57 +34,105 @@ const testHubStorage = testGeneric( () => window.hubStorage && !!window.hubStorage.getItem, ); -class MemoryBackend implements StorageBackend { - private store: Record; +class HubStorageBackend implements StorageBackend { public impl: StorageImplementation; constructor() { - this.impl = IMPL_MEMORY; - this.store = {}; + this.impl = IMPL_HUB_STORAGE; } async get(key: string): Promise { - return this.store[key]; + const value = await window.hubStorage.getItem(`${KEY_NAME}-${key}`); + if (typeof value === 'string') { + return JSON.parse(value); + } + return undefined; } async set(key: string, value: any): Promise { - this.store[key] = value; + window.hubStorage.setItem(`${KEY_NAME}-${key}`, JSON.stringify(value)); } async remove(key: string): Promise { - this.store[key] = undefined; + window.hubStorage.removeItem(`${KEY_NAME}-${key}`); } async clear(): Promise { - this.store = {}; + window.hubStorage.clear(); } } -class HubStorageBackend implements StorageBackend { +class IFrameIndexedDbBackend implements StorageBackend { public impl: StorageImplementation; + private documentElement: HTMLIFrameElement; + private iframeWindow: Window; + constructor() { - this.impl = IMPL_HUB_STORAGE; + this.impl = IMPL_IFRAME_INDEXED_DB; + } + + async ready(): Promise { + const iframe = document.createElement('iframe'); + const iframeStore = `${Byond.storageCdn}?store=${KEY_NAME}`; + iframe.style.display = 'none'; + this.documentElement = document.body.appendChild(iframe); + iframe.src = iframeStore; + + const completePromise: Promise = new Promise((resolve) => { + fetch(iframeStore, { method: 'HEAD' }) + .then((response) => { + if (response.status !== 200) { + resolve(false); + } + }) + .catch(() => { + resolve(false); + }); + + window.addEventListener('message', (message) => { + if (message.data === 'ready') { + resolve(true); + } + }); + }); + + if (!this.documentElement.contentWindow) { + return new Promise((res) => res(false)); + } + + this.iframeWindow = this.documentElement.contentWindow; + + return completePromise; } async get(key: string): Promise { - const value = await window.hubStorage.getItem(`${KEY_NAME}-${key}`); - if (typeof value === 'string') { - return JSON.parse(value); - } - return undefined; + const promise = new Promise((resolve) => { + window.addEventListener('message', (message) => { + if (message.data.key && message.data.key === key) { + resolve(message.data.value); + } + }); + }); + + this.iframeWindow.postMessage({ type: 'get', key: key }, '*'); + return promise; } async set(key: string, value: any): Promise { - window.hubStorage.setItem(`${KEY_NAME}-${key}`, JSON.stringify(value)); + this.iframeWindow.postMessage({ type: 'set', key: key, value: value }, '*'); } async remove(key: string): Promise { - window.hubStorage.removeItem(`${KEY_NAME}-${key}`); + this.iframeWindow.postMessage({ type: 'remove', key: key }, '*'); } async clear(): Promise { - window.hubStorage.clear(); + this.iframeWindow.postMessage({ type: 'clear' }, '*'); + } + + async destroy(): Promise { + document.body.removeChild(this.documentElement); } } @@ -93,19 +142,73 @@ class HubStorageBackend implements StorageBackend { */ class StorageProxy implements StorageBackend { private backendPromise: Promise; - public impl: StorageImplementation = IMPL_MEMORY; + public impl: StorageImplementation = IMPL_IFRAME_INDEXED_DB; constructor() { this.backendPromise = (async () => { - if (testHubStorage()) { - return new HubStorageBackend(); + // If we have not enabled byondstorage yet, we need to check + // if we can use the IFrame, or if we need to enable byondstorage + console.log(`testHubStorage ${testHubStorage()}`); + if (!testHubStorage()) { + // If we have an IFrame URL we can use, and we haven't already enabled + // byondstorage, we should use the IFrame backend + console.log(`storageCdn: ${Byond.storageCdn}`); + if (Byond.storageCdn) { + const iframe = new IFrameIndexedDbBackend(); + + if ((await iframe.ready()) === true) { + if (await iframe.get('byondstorage-migrated')) return iframe; + + Byond.winset(null, 'browser-options', '+byondstorage'); + + await new Promise((resolve) => { + document.addEventListener('byondstorageupdated', async () => { + setTimeout(() => { + const hub = new HubStorageBackend(); + + // Migrate these existing settings from byondstorage to the IFrame + for (const setting of [ + 'panel-settings', + 'chat-state', + 'chat-messages', + ]) { + hub + .get(setting) + .then((settings) => iframe.set(setting, settings)); + } + + iframe.set('byondstorage-migrated', true); + Byond.winset(null, 'browser-options', '-byondstorage'); + + resolve(); + }, 1); + }); + }); + + return iframe; + } + + iframe.destroy(); + } + + // IFrame hasn't worked out for us, we'll need to enable byondstorage + Byond.winset(null, 'browser-options', '+byondstorage'); + + return new Promise((resolve) => { + const listener = () => { + document.removeEventListener('byondstorageupdated', listener); + + // This event is emitted *before* byondstorage is actually created + // so we have to wait a little bit before we can use it + setTimeout(() => resolve(new HubStorageBackend()), 1); + }; + + document.addEventListener('byondstorageupdated', listener); + }); } - console.warn( - 'No supported storage backend found. Using in-memory storage.', - ); - - return new MemoryBackend(); + // byondstorage is already enabled, we can use it straight away + return new HubStorageBackend(); })(); } diff --git a/tgui/packages/common/type-safety.test.ts b/tgui/packages/common/type-safety.test.ts new file mode 100644 index 00000000000..f572a19781d --- /dev/null +++ b/tgui/packages/common/type-safety.test.ts @@ -0,0 +1,53 @@ +import { describe, it } from 'bun:test'; +import assert from 'node:assert/strict'; +import * as z from 'zod'; +import { smoothMerge } from './type-safety'; + +describe('smoothMerge', () => { + it('merges valid fields from source into target', () => { + const schema = z.object({ + a: z.string(), + b: z.number(), + }); + + const source = { a: 'hello', b: 'not a number', c: true }; + const target = { a: 'default', b: 42 }; + + const result = smoothMerge({ schema, source, target }); + assert.deepEqual(result, { a: 'hello', b: 42 }); + }); + + it('returns target if source is empty', () => { + const schema = z.object({ + a: z.string(), + }); + + const source = {}; + const target = { a: 'default' }; + const result = smoothMerge({ schema, source, target }); + assert.deepEqual(result, target); + }); + + it('completely ignores an object if its not in the schema', () => { + const schema = z.object({ + a: z.string(), + b: z.number(), + }); + + const source = { + c: 1, + d: [1, 2, 3], + }; + + const target = { + a: 'default', + b: 42, + }; + + const result = smoothMerge({ schema, source, target }); + assert.deepEqual(result, { + a: 'default', + b: 42, + }); + }); +}); diff --git a/tgui/packages/common/type-safety.ts b/tgui/packages/common/type-safety.ts new file mode 100644 index 00000000000..71e1c6aa568 --- /dev/null +++ b/tgui/packages/common/type-safety.ts @@ -0,0 +1,56 @@ +import type { ZodObject } from 'zod'; + +type MergeInput = { + /** + * A zod object. + * @see Writing a Zod Schema: https://zod.dev/basics + */ + schema: ZodObject; + /** The input getting merged */ + source: Record; + /** The defaults, which is the shape of the output */ + target: TObj; +}; + +/** + * Merges two objects together while validating the output against a zod schema. + * Different than just parsing - it does not throw errors, it simply discards + * invalid fields and invalid value types. + * + * @example + * + * ```ts + * const schema = z.object({ + * a: z.string(), + * b: z.number(), + * }); + * + * const source = { a: 'hello', b: 'not a number', c: true }; + * const target = { a: 'default', b: 42 }; + * + * const result = smoothMerge({ schema, source, target }); + * // result is { a: 'hello', b: 42 } + * ``` + */ +export function smoothMerge>( + input: MergeInput, +): TObj { + if (Object.keys(input.source).length === 0) return input.target; + + const validated = {}; + + for (const [key, value] of Object.entries(input.source)) { + // Skip keys that are not in the schema + if (!(key in input.schema.shape)) continue; + + const fieldSchema = input.schema.shape[key]; + const result = fieldSchema.safeParse(value); + + // Only assign fields which pass validation + if (result.success) { + validated[key] = result.data; + } + } + + return { ...input.target, ...validated }; +} diff --git a/tgui/packages/tgui-panel/Notifications.tsx b/tgui/packages/tgui-panel/Notifications.tsx index e46e6f98fa0..063b065e1f6 100644 --- a/tgui/packages/tgui-panel/Notifications.tsx +++ b/tgui/packages/tgui-panel/Notifications.tsx @@ -4,25 +4,25 @@ * @license MIT */ -import { Flex } from 'tgui-core/components'; +import { Stack } from 'tgui-core/components'; -export const Notifications = (props) => { +export function Notifications(props) { const { children } = props; return
{children}
; -}; +} -const NotificationsItem = (props) => { +function NotificationsItem(props) { const { rightSlot, children } = props; return ( - - + + {children} - + {rightSlot && ( - {rightSlot} + {rightSlot} )} - + ); -}; +} Notifications.Item = NotificationsItem; diff --git a/tgui/packages/tgui-panel/Panel.tsx b/tgui/packages/tgui-panel/Panel.tsx index 022516ab50b..44ddc763189 100644 --- a/tgui/packages/tgui-panel/Panel.tsx +++ b/tgui/packages/tgui-panel/Panel.tsx @@ -4,36 +4,40 @@ * @license MIT */ +import { useAtom, useAtomValue } from 'jotai'; +import { useState } from 'react'; import { Pane } from 'tgui/layouts'; import { Button, Section, Stack } from 'tgui-core/components'; - -import { NowPlayingWidget, useAudio } from './audio'; -import { ChatPanel, ChatTabs } from './chat'; -import { useGame } from './game'; +import { visibleAtom } from './audio/atoms'; +import { NowPlayingWidget } from './audio/NowPlayingWidget'; +import { ChatPanel } from './chat/ChatPanel'; +import { ChatTabs } from './chat/ChatTabs'; +import { useChatPersistence } from './chat/use-chat-persistence'; +import { gameAtom } from './game/atoms'; +import { useKeepAlive } from './game/use-keep-alive'; import { Notifications } from './Notifications'; -import { PingIndicator } from './ping'; +import { PingIndicator } from './ping/PingIndicator'; import { ReconnectButton } from './reconnect'; -import { SettingsPanel, useSettings } from './settings'; +import { settingsVisibleAtom } from './settings/atoms'; +import { SettingsPanel } from './settings/SettingsPanel'; +import { useSettings } from './settings/use-settings'; -export const Panel = (props) => { - const audio = useAudio(); - const settings = useSettings(); - const game = useGame(); - if (process.env.NODE_ENV !== 'production') { - const { useDebug, KitchenSink } = require('tgui/debug'); - const debug = useDebug(); - if (debug.kitchenSink) { - return ; - } - } +export function Panel(props) { + const [audioVisible, setAudioVisible] = useAtom(visibleAtom); + const game = useAtomValue(gameAtom); + const { settings } = useSettings(); + const [settingsVisible, setSettingsVisible] = useAtom(settingsVisibleAtom); + const [dismissedWarning, setDismissedWarning] = useState(false); + useChatPersistence(); + useKeepAlive(setDismissedWarning); return ( - + - +
- + @@ -42,52 +46,60 @@ export const Panel = (props) => {
- {audio.visible && ( - + {audioVisible && ( +
)} - {settings.visible && ( - + {settingsVisible && ( + )}
- + - {game.connectionLostAt && ( - }> - You are either AFK, experiencing lag or the connection has - closed. - - )} - {game.roundRestartedAt && ( + {settings.showReconnectWarning && + game.connectionLostAt && + !dismissedWarning && ( + + } + > + You are either AFK, experiencing lag or the connection has + closed. + + )} + {settings.showReconnectWarning && game.roundRestartedAt && ( The connection has been closed because the server is restarting. Please wait while you automatically reconnect. @@ -99,4 +111,4 @@ export const Panel = (props) => { ); -}; +} diff --git a/tgui/packages/tgui-panel/app.tsx b/tgui/packages/tgui-panel/app.tsx new file mode 100644 index 00000000000..efe198ae3a7 --- /dev/null +++ b/tgui/packages/tgui-panel/app.tsx @@ -0,0 +1,12 @@ +import { Provider } from 'jotai'; +import { store } from './events/store'; +import { Panel } from './Panel'; + +/** Just an expandable wrapper for setup shenanigans and providers */ +export function App() { + return ( + + + + ); +} diff --git a/tgui/packages/tgui-panel/audio/NowPlayingWidget.jsx b/tgui/packages/tgui-panel/audio/NowPlayingWidget.jsx deleted file mode 100644 index bad386cc326..00000000000 --- a/tgui/packages/tgui-panel/audio/NowPlayingWidget.jsx +++ /dev/null @@ -1,112 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -import { useDispatch, useSelector } from 'tgui/backend'; -import { Button, Collapsible, Flex, Knob, Section } from 'tgui-core/components'; -import { toFixed } from 'tgui-core/math'; - -import { useSettings } from '../settings'; -import { selectAudio } from './selectors'; - -export const NowPlayingWidget = (props) => { - const audio = useSelector(selectAudio), - dispatch = useDispatch(), - settings = useSettings(), - title = audio.meta?.title, - URL = audio.meta?.link, - Artist = audio.meta?.artist || 'Unknown Artist', - upload_date = audio.meta?.upload_date || 'Unknown Date', - album = audio.meta?.album || 'Unknown Album', - duration = audio.meta?.duration, - date = !isNaN(upload_date) - ? upload_date?.substring(0, 4) + - '-' + - upload_date?.substring(4, 6) + - '-' + - upload_date?.substring(6, 8) - : upload_date; - - return ( - - {(audio.playing && ( - - { - -
- {URL !== 'Song Link Hidden' && ( - - URL: {URL} - - )} - {duration !== 'Song Duration Hidden' && ( - - Duration: {duration} - )} - {Artist !== 'Song Artist Hidden' && - Artist !== 'Unknown Artist' && ( - - Artist: {Artist} - - )} - {album !== 'Song Album Hidden' && album !== 'Unknown Album' && ( - - Album: {album} - - )} - {upload_date !== 'Song Upload Date Hidden' && - upload_date !== 'Unknown Date' && ( - - Uploaded: {date} - - )} -
-
- } -
- )) || ( - - Nothing to play. - - )} - {audio.playing && ( - - + ) : ( + '' )} @@ -120,14 +91,7 @@ export function ChatPageSettings(props) { - dispatch( - toggleAcceptedType({ - pageId: page.id, - type: typeDef.type, - }), - ) - } + onClick={() => toggleAcceptedType(typeDef.type)} > {typeDef.name} @@ -139,14 +103,7 @@ export function ChatPageSettings(props) { - dispatch( - toggleAcceptedType({ - pageId: page.id, - type: typeDef.type, - }), - ) - } + onClick={() => toggleAcceptedType(typeDef.type)} > {typeDef.name} @@ -155,4 +112,4 @@ export function ChatPageSettings(props) {
); -} +}; diff --git a/tgui/packages/tgui-panel/chat/ChatPanel.tsx b/tgui/packages/tgui-panel/chat/ChatPanel.tsx index b42086ccd56..8bba15c42fa 100644 --- a/tgui/packages/tgui-panel/chat/ChatPanel.tsx +++ b/tgui/packages/tgui-panel/chat/ChatPanel.tsx @@ -4,84 +4,73 @@ * @license MIT */ -import { Component, createRef } from 'react'; +import { useAtom, useAtomValue } from 'jotai'; +import { useEffect, useRef } from 'react'; import { Button } from 'tgui-core/components'; -import { shallowDiffers } from 'tgui-core/react'; - +import { + chatPagesRecordAtom, + currentPageIdAtom, + scrollTrackingAtom, +} from './atoms'; import { chatRenderer } from './renderer'; +import type { Page } from './types'; type Props = { fontSize?: string; - lineHeight: string; -}; - -type State = { - scrollTracking: boolean; + lineHeight: string | number; }; -export class ChatPanel extends Component { - ref: React.RefObject; - handleScrollTrackingChange: (value: boolean) => void; - - constructor(props) { - super(props); - this.ref = createRef(); - this.state = { - scrollTracking: true, - }; - this.handleScrollTrackingChange = (value) => - this.setState({ - scrollTracking: value, - }); - } +export function ChatPanel(props: Props) { + const ref = useRef(null); + const scrollTracking = useAtomValue(scrollTrackingAtom); + // Page stuff + const currentPageId = useAtomValue(currentPageIdAtom); + const [pagesRecord, setPagesRecord] = useAtom(chatPagesRecordAtom); - componentDidMount() { - chatRenderer.mount(this.ref.current); - chatRenderer.events.on( - 'scrollTrackingChanged', - this.handleScrollTrackingChange, - ); - this.componentDidUpdate(null); - } + /** Mounts the renderer */ + useEffect(() => { + if (ref.current) { + chatRenderer.mount(ref.current); + } + }, []); - componentWillUnmount() { - chatRenderer.events.off( - 'scrollTrackingChanged', - this.handleScrollTrackingChange, - ); - } + /** Resets unread count when scroll tracking is enabled */ + useEffect(() => { + if (scrollTracking) { + const draft: Page = { + ...pagesRecord[currentPageId], + unreadCount: 0, + }; - componentDidUpdate(prevProps) { - requestAnimationFrame(() => { - chatRenderer.ensureScrollTracking(); - }); - const shouldUpdateStyle = - !prevProps || shallowDiffers(this.props, prevProps); - if (shouldUpdateStyle) { - chatRenderer.assignStyle({ - width: '100%', - 'white-space': 'pre-wrap', - 'font-size': this.props.fontSize, - 'line-height': this.props.lineHeight, + setPagesRecord({ + ...pagesRecord, + [currentPageId]: draft, }); } - } + }, [scrollTracking]); + + /** Updates the style of the chat panel */ + useEffect(() => { + chatRenderer.assignStyle({ + width: '100%', + 'white-space': 'pre-wrap', + 'font-size': props.fontSize, + 'line-height': props.lineHeight, + }); + }, [props.fontSize, props.lineHeight]); - render() { - const { scrollTracking } = this.state; - return ( - <> -
- {!scrollTracking && ( - - )} - - ); - } + return ( + <> +
+ {!scrollTracking && ( + + )} + + ); } diff --git a/tgui/packages/tgui-panel/chat/ChatTabs.tsx b/tgui/packages/tgui-panel/chat/ChatTabs.tsx index cfa8e8e385a..58ff89b9b66 100644 --- a/tgui/packages/tgui-panel/chat/ChatTabs.tsx +++ b/tgui/packages/tgui-panel/chat/ChatTabs.tsx @@ -4,53 +4,54 @@ * @license MIT */ -import { useDispatch, useSelector } from 'tgui/backend'; +import { useAtom } from 'jotai'; import { Box, Button, Stack, Tabs } from 'tgui-core/components'; +import { settingsVisibleAtom } from '../settings/atoms'; +import { useChatPages } from './use-chat-pages'; -import { openChatSettings } from '../settings/actions'; -import { addChatPage, changeChatPage } from './actions'; -import { selectChatPages, selectCurrentChatPage } from './selectors'; +type UnreadCountWidgetProps = { + value: number; +}; -function UnreadCountWidget({ value }: { value: number }) { +function UnreadCountWidget(props: UnreadCountWidgetProps) { + const { value } = props; return {Math.min(value, 99)}; } export function ChatTabs(props) { - const pages = useSelector(selectChatPages); - const currentPage = useSelector(selectCurrentChatPage); - const dispatch = useDispatch(); + const { addChatPage, changeChatPage, pages, pagesRecord, currentPageId } = + useChatPages(); + + const [, setSettingsVisible] = useAtom(settingsVisibleAtom); return ( - {pages.map((page) => ( - - dispatch( - changeChatPage({ - pageId: page.id, - }), - ) - } - > - {page.name} - {!page.hideUnreadCount && page.unreadCount > 0 && ( - - )} - - ))} + {pages.map((page) => { + const actual = pagesRecord[page]; + return ( + changeChatPage(actual)} + > + {actual.name} + {!actual.hideUnreadCount && actual.unreadCount > 0 && ( + + )} + + ); + })} - + - + + + + + ); -}; +} diff --git a/tgui/packages/tgui-panel/settings/SettingTabs/AdminSettings.tsx b/tgui/packages/tgui-panel/settings/SettingTabs/AdminSettings.tsx new file mode 100644 index 00000000000..1ee1f0ee57d --- /dev/null +++ b/tgui/packages/tgui-panel/settings/SettingTabs/AdminSettings.tsx @@ -0,0 +1,25 @@ +import { Button, LabeledList, Section } from 'tgui-core/components'; + +import { useSettings } from '../use-settings'; + +export const AdminSettings = (props) => { + const { settings, updateSettings } = useSettings(); + return ( +
+ + + + updateSettings({ + hideImportantInAdminTab: !settings.hideImportantInAdminTab, + }) + } + /> + + +
+ ); +}; diff --git a/tgui/packages/tgui-panel/settings/SettingTabs/ExportTab.tsx b/tgui/packages/tgui-panel/settings/SettingTabs/ExportTab.tsx new file mode 100644 index 00000000000..f4bb87ab3dc --- /dev/null +++ b/tgui/packages/tgui-panel/settings/SettingTabs/ExportTab.tsx @@ -0,0 +1,324 @@ +import { useAtom, useAtomValue } from 'jotai'; +import { useState } from 'react'; +import { + Box, + Button, + Collapsible, + Divider, + Dropdown, + LabeledList, + NumberInput, + Section, + Stack, +} from 'tgui-core/components'; +import { + exportEndAtom, + exportStartAtom, + storedLinesAtom, + storedRoundsAtom, +} from '../../chat/atoms'; +import { MESSAGE_TYPES } from '../../chat/constants'; +import { purgeMessageArchive } from '../../chat/helpers'; +import { chatRenderer } from '../../chat/renderer'; +import { gameAtom } from '../../game/atoms'; +import { exportChatSettings, importChatSettings } from '../settingsImExport'; +import { useSettings } from '../use-settings'; + +export const ExportTab = (props) => { + const game = useAtomValue(gameAtom); + const { settings, updateSettings, toggleInObject } = useSettings(); + const [purgeButtonText, setPurgeButtonText] = useState( + 'Purge message archive', + ); + const storedLines = useAtomValue(storedLinesAtom); + const storedRounds = useAtomValue(storedRoundsAtom); + const [exportStart, setExportStart] = useAtom(exportStartAtom); + const [exportEnd, setExportEnd] = useAtom(exportEndAtom); + return ( +
+ + {!game.databaseBackendEnabled && + (settings.logEnable ? ( + + + updateSettings({ + logEnable: false, + }) + } + > + Disable logging + + + ) : ( + + + + ))} + Round ID: + + {game.roundId ? game.roundId : 'ERROR'} + + DB Chatlogging: + + {game.databaseBackendEnabled ? 'Enabled' : 'Disabled'} + + + {settings.logEnable && !game.databaseBackendEnabled && ( + <> + + + value.toFixed()} + onChange={(value) => + updateSettings({ + logRetainRounds: value, + }) + } + /> +   + {settings.logRetainRounds > 3 && ( + + Warning, might crash! + + )} + + + + value.toFixed()} + onChange={(value) => + updateSettings({ + logLimit: value, + }) + } + + /> +   + {settings.logLimit > 0 && ( + 10000 ? 'red' : 'label'} + > + {settings.logLimit > 15000 + ? 'Warning, might crash! Takes priority above round retention.' + : 'Takes priority above round retention.'} + + )} + + + +
+ + {MESSAGE_TYPES.map((typeDef) => ( + + updateSettings({ + storedTypes: toggleInObject( + settings.storedTypes, + typeDef.type, + ), + }) + } + > + {typeDef.name} + + ))} + +
+ + )} + + + + {game.databaseBackendEnabled ? ( + <> + + setExportStart(value)} + options={game.databaseStoredRounds} + selected={exportStart.toString()} + /> + + + setExportEnd(value)} + options={game.databaseStoredRounds} + selected={exportEnd.toString()} + /> + + + ) : ( + <> + + value.toFixed()} + onChange={(value) => setExportStart(value)} + /> + + + value.toFixed()} + onChange={(value) => setExportEnd(value)} + /> + + + )} + + +  Stored Rounds:  + + + + + {game.databaseBackendEnabled + ? game.databaseStoredRounds.length - 1 + : storedRounds} + + + + + + value.toFixed()} + onChange={(value) => + updateSettings({ + logLineCount: value, + }) + } + /> + + {!game.databaseBackendEnabled && ( + + {chatRenderer.getStoredMessages()} + + )} + + + + + + + + + Import settings + + + + + + + chatRenderer.clearChat()} + > + Clear chat + + + {!game.databaseBackendEnabled && ( + + { + purgeMessageArchive(); + setPurgeButtonText('Purged!'); + setTimeout(() => { + setPurgeButtonText('Purge message archive'); + }, 1000); + }} + > + {purgeButtonText} + + + )} + +
+ ); +}; diff --git a/tgui/packages/tgui-panel/settings/SettingTabs/MessageLimits.tsx b/tgui/packages/tgui-panel/settings/SettingTabs/MessageLimits.tsx new file mode 100644 index 00000000000..7414d6d0619 --- /dev/null +++ b/tgui/packages/tgui-panel/settings/SettingTabs/MessageLimits.tsx @@ -0,0 +1,122 @@ +import { useAtomValue } from 'jotai'; +import { Box, LabeledList, NumberInput, Section } from 'tgui-core/components'; +import { gameAtom } from '../../game/atoms'; +import { useSettings } from '../use-settings'; + +export const MessageLimits = (props) => { + const game = useAtomValue(gameAtom); + const { settings, updateSettings } = useSettings(); + return ( +
+ + + value.toFixed()} + onChange={(value) => + updateSettings({ + visibleMessageLimit: value, + }) + } + /> +   + {settings.visibleMessageLimit >= 5000 && ( + + Impacts performance! + + )} + + + value.toFixed()} + onChange={(value) => + updateSettings({ + persistentMessageLimit: value, + }) + } + /> +   + {settings.persistentMessageLimit >= 2500 && ( + + Delays initialization! + + )} + + + value.toFixed()} + onChange={(value) => + updateSettings({ + combineMessageLimit: value, + }) + } + /> + + + value.toFixed()} + onChange={(value) => + updateSettings({ + combineIntervalLimit: value, + }) + } + /> + + {!game.databaseBackendEnabled && ( + + value.toFixed()} + onChange={(value) => + updateSettings({ + saveInterval: value, + }) + } + /> +   + {settings.saveInterval <= 3 && ( + + Warning, experimental! Might crash! + + )} + + )} + +
+ ); +}; diff --git a/tgui/packages/tgui-panel/settings/SettingTabs/SettingsGeneral.tsx b/tgui/packages/tgui-panel/settings/SettingTabs/SettingsGeneral.tsx new file mode 100644 index 00000000000..aac4c37af25 --- /dev/null +++ b/tgui/packages/tgui-panel/settings/SettingTabs/SettingsGeneral.tsx @@ -0,0 +1,225 @@ +import { useState } from 'react'; +import { + Box, + Button, + Collapsible, + ColorBox, + Input, + LabeledList, + Section, + Slider, + Stack, +} from 'tgui-core/components'; +import { capitalize } from 'tgui-core/string'; +import { chatRenderer } from '../../chat/renderer'; +import { FONTS, THEMES } from '../constants'; +import { resetPaneSplitters, setEditPaneSplitters } from '../scaling'; +import { useSettings } from '../use-settings'; + +export function SettingsGeneral(props) { + const { settings, updateSettings } = useSettings(); + const [freeFont, setFreeFont] = useState(false); + + const [editingPanes, setEditingPanes] = useState(false); + + return ( +
+ + + {THEMES.map((THEME) => ( + + ))} + + + + + + + + + + + + + + {!freeFont ? ( + { + setFreeFont(!freeFont); + }} + > + Custom font + + } + > + {FONTS.map((FONT) => ( + + ))} + + ) : ( + + + updateSettings({ + fontFamily: value, + }) + } + /> + + + )} + + + + + + value.toFixed()} + onChange={(e, value) => updateSettings({ fontSize: value })} + /> + + + + + value.toFixed(2)} + onChange={(e, value) => + updateSettings({ + lineHeight: value, + }) + } + /> + {' '} + + + updateSettings({ + showReconnectWarning: !settings.showReconnectWarning, + }) + } + /> + + + + updateSettings({ + interleave: !settings.interleave, + }) + } + /> + + + + updateSettings({ + interleaveColor: value, + }) + } + /> + + + + + updateSettings({ + prependTimestamps: !settings.prependTimestamps, + }) + } + /> + + + + Can freeze the chat for a while. + + + + +
+ ); +} diff --git a/tgui/packages/tgui-panel/settings/SettingsStatPanel.tsx b/tgui/packages/tgui-panel/settings/SettingTabs/SettingsStatPanel.tsx similarity index 63% rename from tgui/packages/tgui-panel/settings/SettingsStatPanel.tsx rename to tgui/packages/tgui-panel/settings/SettingTabs/SettingsStatPanel.tsx index 4502c52ae90..a6b254f8017 100644 --- a/tgui/packages/tgui-panel/settings/SettingsStatPanel.tsx +++ b/tgui/packages/tgui-panel/settings/SettingTabs/SettingsStatPanel.tsx @@ -1,4 +1,3 @@ -import { useDispatch, useSelector } from 'tgui/backend'; import { Button, LabeledList, @@ -7,42 +6,37 @@ import { Slider, Stack, } from 'tgui-core/components'; -import { toFixed } from 'tgui-core/math'; import { capitalize } from 'tgui-core/string'; +import { useSettings } from '../use-settings'; -import { updateSettings } from './actions'; -import { selectSettings } from './selectors'; +const tabViews = ['default', 'classic', 'scrollable']; -const TabsViews = ['default', 'classic', 'scrollable']; -const LinkedToChat = () => ( - Unlink Stat Panel from chat! -); +function LinkedToChat() { + return Unlink Stat Panel from chat!; +} export function SettingsStatPanel(props) { - const { statLinked, statFontSize, statTabsStyle } = - useSelector(selectSettings); - const dispatch = useDispatch(); + const { settings, updateSettings } = useSettings(); + const { statLinked, statFontSize, statTabsStyle } = settings; return (
- - {TabsViews.map((view) => ( + + {tabViews.map((view) => ( ))} - + {statLinked ? ( @@ -55,9 +49,9 @@ export function SettingsStatPanel(props) { maxValue={32} value={statFontSize} unit="px" - format={(value) => toFixed(value)} + format={(value) => value.toFixed()} onChange={(e, value) => - dispatch(updateSettings({ statFontSize: value })) + updateSettings({ statFontSize: value }) } /> )} @@ -71,9 +65,7 @@ export function SettingsStatPanel(props) { fluid icon={statLinked ? 'unlink' : 'link'} color={statLinked ? 'bad' : 'good'} - onClick={() => - dispatch(updateSettings({ statLinked: !statLinked })) - } + onClick={() => updateSettings({ statLinked: !statLinked })} > {statLinked ? 'Unlink from chat' : 'Link to chat'} diff --git a/tgui/packages/tgui-panel/settings/SettingTabs/TTSSettings.tsx b/tgui/packages/tgui-panel/settings/SettingTabs/TTSSettings.tsx new file mode 100644 index 00000000000..276eed21d0a --- /dev/null +++ b/tgui/packages/tgui-panel/settings/SettingTabs/TTSSettings.tsx @@ -0,0 +1,74 @@ +import { + Button, + Collapsible, + Dropdown, + LabeledList, + Section, +} from 'tgui-core/components'; + +import { MESSAGE_TYPES } from '../../chat/constants'; +import { useSettings } from '../use-settings'; + +export const TTSSettings = (props) => { + const { settings, updateSettings, toggleInObject } = useSettings(); + + const voices = window.speechSynthesis.getVoices(); + + return ( + <> +
+ + + voice.name)} + onSelected={(option) => { + updateSettings({ + ttsVoice: option, + }); + }} + selected={settings.ttsVoice} + /> + + +
+
+ {MESSAGE_TYPES.filter((typeDef) => !typeDef.admin).map((typeDef) => ( + + updateSettings({ + ttsCategories: toggleInObject( + settings.ttsCategories, + typeDef.type, + ), + }) + } + > + {typeDef.name} + + ))} + + {MESSAGE_TYPES.filter( + (typeDef) => !typeDef.important && typeDef.admin, + ).map((typeDef) => ( + + updateSettings({ + ttsCategories: toggleInObject( + settings.ttsCategories, + typeDef.type, + ), + }) + } + > + {typeDef.name} + + ))} + +
+ + ); +}; diff --git a/tgui/packages/tgui-panel/settings/SettingTabs/TextHighlightSettings.tsx b/tgui/packages/tgui-panel/settings/SettingTabs/TextHighlightSettings.tsx new file mode 100644 index 00000000000..b155bce8b61 --- /dev/null +++ b/tgui/packages/tgui-panel/settings/SettingTabs/TextHighlightSettings.tsx @@ -0,0 +1,211 @@ +import { useAtomValue } from 'jotai'; +import { + Box, + Button, + ColorBox, + Divider, + Input, + Section, + Stack, + TextArea, +} from 'tgui-core/components'; +import { chatRenderer } from '../../chat/renderer'; +import { settingsAtom } from '../atoms'; +import { MAX_HIGHLIGHT_SETTINGS } from '../constants'; +import { useHighlights } from '../use-highlights'; + +export const TextHighlightSettings = (props) => { + const { + highlights: { highlightSettings }, + addHighlight, + } = useHighlights(); + const settings = useAtomValue(settingsAtom); + + return ( +
+
+ + {highlightSettings.map((id, i) => ( + + ))} + {highlightSettings.length < MAX_HIGHLIGHT_SETTINGS && ( + + + + )} + +
+ + + + + Can freeze the chat for a while. + + +
+ ); +}; + +const TextHighlightSetting = (props) => { + const { id, ...rest } = props; + const { + highlights: { highlightSettingById }, + updateHighlight, + removeHighlight, + } = useHighlights(); + const { + highlightColor, + highlightText, + blacklistText, + highlightWholeMessage, + highlightBlacklist, + matchWord, + matchCase, + } = highlightSettingById[id]; + return ( + + + + updateHighlight({ + id, + highlightText: '', + blacklistText: '', + }) + } + > + Reset + + {id !== 'default' && ( + + removeHighlight(id)} + > + Delete + + + )} + + + + updateHighlight({ + id, + highlightBlacklist: !highlightBlacklist, + }) + } + > + Highlight Blacklist + + + + + updateHighlight({ + id, + highlightWholeMessage: !highlightWholeMessage, + }) + } + > + Whole Message + + + + + updateHighlight({ + id, + matchWord: !matchWord, + }) + } + > + Exact + + + + + updateHighlight({ + id, + matchCase: !matchCase, + }) + } + > + Case + + + + + + updateHighlight({ + id, + highlightColor: value, + }) + } + /> + + +