mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-11 18:53:06 +00:00
[MIRROR] TGUI Say (#8771)
Co-authored-by: ShadowLarkens <shadowlarkens@gmail.com> Co-authored-by: Kashargul <KashL@t-online.de>
This commit is contained in:
9
code/__defines/speech_channels.dm
Normal file
9
code/__defines/speech_channels.dm
Normal file
@@ -0,0 +1,9 @@
|
||||
// Used to direct channels to speak into.
|
||||
#define SAY_CHANNEL "Say"
|
||||
#define RADIO_CHANNEL "Radio"
|
||||
#define ME_CHANNEL "Me"
|
||||
#define OOC_CHANNEL "OOC"
|
||||
#define ADMIN_CHANNEL "Admin"
|
||||
#define LOOC_CHANNEL "LOOC"
|
||||
#define WHIS_CHANNEL "Whis"
|
||||
#define SUBTLE_CHANNEL "Subtle"
|
||||
@@ -5,59 +5,118 @@
|
||||
#define ADD_TRAIT(target, trait, source) \
|
||||
do { \
|
||||
var/list/_L; \
|
||||
if (!target.status_traits) { \
|
||||
target.status_traits = list(); \
|
||||
_L = target.status_traits; \
|
||||
if (!target._status_traits) { \
|
||||
target._status_traits = list(); \
|
||||
_L = target._status_traits; \
|
||||
_L[trait] = list(source); \
|
||||
SEND_SIGNAL(target, SIGNAL_ADDTRAIT(trait), trait); \
|
||||
} else { \
|
||||
_L = target.status_traits; \
|
||||
_L = target._status_traits; \
|
||||
if (_L[trait]) { \
|
||||
_L[trait] |= list(source); \
|
||||
} else { \
|
||||
_L[trait] = list(source); \
|
||||
SEND_SIGNAL(target, SIGNAL_ADDTRAIT(trait), trait); \
|
||||
} \
|
||||
} \
|
||||
} while (0)
|
||||
#define REMOVE_TRAIT(target, trait, sources) \
|
||||
do { \
|
||||
var/list/_L = target.status_traits; \
|
||||
var/list/_L = target._status_traits; \
|
||||
var/list/_S; \
|
||||
if (sources && !islist(sources)) { \
|
||||
_S = list(sources); \
|
||||
} else { \
|
||||
_S = sources\
|
||||
}; \
|
||||
if (_L && _L[trait]) { \
|
||||
if (_L?[trait]) { \
|
||||
for (var/_T in _L[trait]) { \
|
||||
if ((!_S && (_T != ROUNDSTART_TRAIT)) || (_T in _S)) { \
|
||||
_L[trait] -= _T \
|
||||
} \
|
||||
};\
|
||||
if (!length(_L[trait])) { \
|
||||
_L -= trait \
|
||||
_L -= trait; \
|
||||
SEND_SIGNAL(target, SIGNAL_REMOVETRAIT(trait), trait); \
|
||||
}; \
|
||||
if (!length(_L)) { \
|
||||
target.status_traits = null \
|
||||
target._status_traits = null \
|
||||
}; \
|
||||
} \
|
||||
} while (0)
|
||||
#define REMOVE_TRAIT_NOT_FROM(target, trait, sources) \
|
||||
do { \
|
||||
var/list/_traits_list = target._status_traits; \
|
||||
var/list/_sources_list; \
|
||||
if (sources && !islist(sources)) { \
|
||||
_sources_list = list(sources); \
|
||||
} else { \
|
||||
_sources_list = sources\
|
||||
}; \
|
||||
if (_traits_list?[trait]) { \
|
||||
for (var/_trait_source in _traits_list[trait]) { \
|
||||
if (!(_trait_source in _sources_list)) { \
|
||||
_traits_list[trait] -= _trait_source \
|
||||
} \
|
||||
};\
|
||||
if (!length(_traits_list[trait])) { \
|
||||
_traits_list -= trait; \
|
||||
SEND_SIGNAL(target, SIGNAL_REMOVETRAIT(trait), trait); \
|
||||
}; \
|
||||
if (!length(_traits_list)) { \
|
||||
target._status_traits = null \
|
||||
}; \
|
||||
} \
|
||||
} while (0)
|
||||
#define REMOVE_TRAITS_NOT_IN(target, sources) \
|
||||
do { \
|
||||
var/list/_L = target.status_traits; \
|
||||
var/list/_L = target._status_traits; \
|
||||
var/list/_S = sources; \
|
||||
if (_L) { \
|
||||
for (var/_T in _L) { \
|
||||
_L[_T] &= _S;\
|
||||
if (!length(_L[_T])) { \
|
||||
_L -= _T } \
|
||||
};\
|
||||
if (!length(_L)) { \
|
||||
target.status_traits = null\
|
||||
_L -= _T; \
|
||||
SEND_SIGNAL(target, SIGNAL_REMOVETRAIT(_T), _T); \
|
||||
}; \
|
||||
};\
|
||||
if (!length(_L)) { \
|
||||
target._status_traits = null\
|
||||
};\
|
||||
}\
|
||||
} while (0)
|
||||
|
||||
#define HAS_TRAIT(target, trait) (target.status_traits ? (target.status_traits[trait] ? TRUE : FALSE) : FALSE)
|
||||
#define HAS_TRAIT_FROM(target, trait, source) (target.status_traits ? (target.status_traits[trait] ? (source in target.status_traits[trait]) : FALSE) : FALSE)
|
||||
#define HAS_TRAIT_FROM_ONLY(target, trait, source) (HAS_TRAIT(target, trait) && (source in target.status_traits[trait]) && (length(target.status_traits[trait]) == 1))
|
||||
#define HAS_TRAIT_NOT_FROM(target, trait, source) (HAS_TRAIT(target, trait) && (length(target.status_traits[trait] - source) > 0))
|
||||
#define REMOVE_TRAITS_IN(target, sources) \
|
||||
do { \
|
||||
var/list/_L = target._status_traits; \
|
||||
var/list/_S = sources; \
|
||||
if (sources && !islist(sources)) { \
|
||||
_S = list(sources); \
|
||||
} else { \
|
||||
_S = sources\
|
||||
}; \
|
||||
if (_L) { \
|
||||
for (var/_T in _L) { \
|
||||
_L[_T] -= _S;\
|
||||
if (!length(_L[_T])) { \
|
||||
_L -= _T; \
|
||||
SEND_SIGNAL(target, SIGNAL_REMOVETRAIT(_T)); \
|
||||
}; \
|
||||
};\
|
||||
if (!length(_L)) { \
|
||||
target._status_traits = null\
|
||||
};\
|
||||
}\
|
||||
} while (0)
|
||||
|
||||
#define HAS_TRAIT(target, trait) (target._status_traits?[trait] ? TRUE : FALSE)
|
||||
#define HAS_TRAIT_FROM(target, trait, source) (HAS_TRAIT(target, trait) && (source in target._status_traits[trait]))
|
||||
#define HAS_TRAIT_FROM_ONLY(target, trait, source) (HAS_TRAIT(target, trait) && (source in target._status_traits[trait]) && (length(target._status_traits[trait]) == 1))
|
||||
#define HAS_TRAIT_NOT_FROM(target, trait, source) (HAS_TRAIT(target, trait) && (length(target._status_traits[trait] - source) > 0))
|
||||
/// Returns a list of trait sources for this trait. Only useful for wacko cases and internal futzing
|
||||
/// You should not be using this
|
||||
#define GET_TRAIT_SOURCES(target, trait) (target._status_traits?[trait] || list())
|
||||
/// Returns the amount of sources for a trait. useful if you don't want to have a "thing counter" stuck around all the time
|
||||
#define COUNT_TRAIT_SOURCES(target, trait) length(GET_TRAIT_SOURCES(target, trait))
|
||||
/// A simple helper for checking traits in a mob's mind
|
||||
#define HAS_MIND_TRAIT(target, trait) (HAS_TRAIT(target, trait) || (target.mind ? HAS_TRAIT(target.mind, trait) : FALSE))
|
||||
|
||||
11
code/__defines/traits/declarations.dm
Normal file
11
code/__defines/traits/declarations.dm
Normal file
@@ -0,0 +1,11 @@
|
||||
// This file contains all of the "static" define strings that tie to a trait.
|
||||
// WARNING: The sections here actually matter in this file as it's tested by CI. Please do not toy with the sections."
|
||||
|
||||
// BEGIN TRAIT DEFINES
|
||||
|
||||
/*
|
||||
Remember to update _globalvars/traits.dm if you're adding/removing/renaming traits.
|
||||
*/
|
||||
|
||||
/// Trait given to a mob that is currently thinking (giving off the "thinking" icon), used in an IC context
|
||||
#define TRAIT_THINKING_IN_CHARACTER "currently_thinking_IC"
|
||||
7
code/__defines/traits/sources.dm
Normal file
7
code/__defines/traits/sources.dm
Normal file
@@ -0,0 +1,7 @@
|
||||
// This file contains all of the trait sources, or all of the things that grant traits.
|
||||
// Several things such as `type` or `REF(src)` may be used in the ADD_TRAIT() macro as the "source", but this file contains all of the defines for immutable static strings.
|
||||
|
||||
/// cannot be removed without admin intervention
|
||||
// #define ROUNDSTART_TRAIT "roundstart" //CHOMPRemove, already exists in traits
|
||||
/// This trait comes from when a mob is currently typing.
|
||||
#define CURRENTLY_TYPING_TRAIT "currently_typing"
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
GLOBAL_LIST_INIT(traits_by_type, list(
|
||||
/mob = list(
|
||||
"TRAIT_THINKING_IN_CHARACTER" = TRAIT_THINKING_IN_CHARACTER,
|
||||
/*
|
||||
"TRAIT_BLIND" = TRAIT_BLIND,
|
||||
*/
|
||||
|
||||
14
code/_global_vars/traits/_traits.dm
Normal file
14
code/_global_vars/traits/_traits.dm
Normal file
@@ -0,0 +1,14 @@
|
||||
// This file should contain every single global trait in the game in a type-based list, as well as any additional trait-related information that's useful to have on a global basis.
|
||||
// This file is used in linting, so make sure to add everything alphabetically and what-not.
|
||||
// Do consider adding your trait entry to the similar list in `admin_tooling.dm` if you want it to be accessible to admins (which is probably the case for 75% of traits).
|
||||
|
||||
// Please do note that there is absolutely no bearing on what traits are added to what subtype of `/datum`, this is just an easily referenceable list sorted by type.
|
||||
// The only thing that truly matters about traits is the code that is built to handle the traits, and where that code is located. Nothing else.
|
||||
|
||||
/* CHOMPRemove, see traits.dm
|
||||
GLOBAL_LIST_INIT(traits_by_type, list(
|
||||
/mob = list(
|
||||
"TRAIT_THINKING_IN_CHARACTER" = TRAIT_THINKING_IN_CHARACTER,
|
||||
)
|
||||
))
|
||||
*/
|
||||
45
code/_helpers/traits.dm
Normal file
45
code/_helpers/traits.dm
Normal file
@@ -0,0 +1,45 @@
|
||||
#define TRAIT_CALLBACK_ADD(target, trait, source) CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(___TraitAdd), ##target, ##trait, ##source)
|
||||
#define TRAIT_CALLBACK_REMOVE(target, trait, source) CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(___TraitRemove), ##target, ##trait, ##source)
|
||||
|
||||
///DO NOT USE ___TraitAdd OR ___TraitRemove as a replacement for ADD_TRAIT / REMOVE_TRAIT defines. To be used explicitly for callback.
|
||||
/proc/___TraitAdd(target, trait, source)
|
||||
if(!target || !trait || !source)
|
||||
return
|
||||
|
||||
if(islist(target))
|
||||
for(var/datum/listed_target in target)
|
||||
ADD_TRAIT(listed_target, trait, source)
|
||||
return
|
||||
|
||||
ASSERT(isdatum(target), "Invalid target used in TRAIT_CALLBACK_ADD! Expected a datum reference, got [target] instead.")
|
||||
|
||||
var/datum/datum_target = target
|
||||
ADD_TRAIT(datum_target, trait, source)
|
||||
|
||||
///DO NOT USE ___TraitAdd OR ___TraitRemove as a replacement for ADD_TRAIT / REMOVE_TRAIT defines. To be used explicitly for callback.
|
||||
/proc/___TraitRemove(target, trait, source)
|
||||
if(!target || !trait || !source)
|
||||
return
|
||||
|
||||
if(islist(target))
|
||||
for(var/datum/listed_target in target)
|
||||
REMOVE_TRAIT(listed_target, trait, source)
|
||||
return
|
||||
|
||||
ASSERT(isdatum(target), "Invalid target used in TRAIT_CALLBACK_REMOVE! Expected a datum reference, got [target] instead.")
|
||||
|
||||
var/datum/datum_target = target
|
||||
REMOVE_TRAIT(datum_target, trait, source)
|
||||
|
||||
|
||||
/// Proc that handles adding multiple traits to a target via a list. Must have a common source and target.
|
||||
/datum/proc/add_traits(list/list_of_traits, source)
|
||||
ASSERT(islist(list_of_traits), "Invalid arguments passed to add_traits! Invoked on [src] with [list_of_traits], source being [source].")
|
||||
for(var/trait in list_of_traits)
|
||||
ADD_TRAIT(src, trait, source)
|
||||
|
||||
/// Proc that handles removing multiple traits from a target via a list. Must have a common source and target.
|
||||
/datum/proc/remove_traits(list/list_of_traits, source)
|
||||
ASSERT(islist(list_of_traits), "Invalid arguments passed to remove_traits! Invoked on [src] with [list_of_traits], source being [source].")
|
||||
for(var/trait in list_of_traits)
|
||||
REMOVE_TRAIT(src, trait, source)
|
||||
@@ -23,6 +23,8 @@
|
||||
|
||||
/// Active timers with this datum as the target
|
||||
var/list/_active_timers // CHOMPEdit
|
||||
/// Status traits attached to this datum. associative list of the form: list(trait name (string) = list(source1, source2, source3,...))
|
||||
var/list/_status_traits
|
||||
|
||||
/**
|
||||
* Components attached to this datum
|
||||
|
||||
28
code/modules/asset_cache/assets/icon_ref_map.dm
Normal file
28
code/modules/asset_cache/assets/icon_ref_map.dm
Normal file
@@ -0,0 +1,28 @@
|
||||
/// Maps icon names to ref values
|
||||
/datum/asset/json/icon_ref_map
|
||||
name = "icon_ref_map"
|
||||
early = TRUE
|
||||
|
||||
/datum/asset/json/icon_ref_map/generate()
|
||||
var/list/data = list() //"icons/obj/drinks.dmi" => "[0xc000020]"
|
||||
|
||||
//var/start = "0xc000000"
|
||||
var/value = 0
|
||||
|
||||
while(TRUE)
|
||||
value += 1
|
||||
var/ref = "\[0xc[num2text(value,6,16)]\]"
|
||||
var/mystery_meat = locate(ref)
|
||||
|
||||
if(isicon(mystery_meat))
|
||||
if(!isfile(mystery_meat)) // Ignore the runtime icons for now
|
||||
continue
|
||||
var/path = get_icon_dmi_path(mystery_meat) //Try to get the icon path
|
||||
if(path)
|
||||
data[path] = ref
|
||||
else if(mystery_meat)
|
||||
continue; //Some other non-icon resource, ogg/json/whatever
|
||||
else //Out of resources end this, could also try to end this earlier as soon as runtime generated icons appear but eh
|
||||
break;
|
||||
|
||||
return data
|
||||
@@ -1,12 +1,12 @@
|
||||
/datum/asset/simple/tgui
|
||||
// keep_local_name = TRUE
|
||||
keep_local_name = TRUE
|
||||
assets = list(
|
||||
"tgui.bundle.js" = file("tgui/public/tgui.bundle.js"),
|
||||
"tgui.bundle.css" = file("tgui/public/tgui.bundle.css"),
|
||||
)
|
||||
|
||||
/datum/asset/simple/tgui_panel
|
||||
// keep_local_name = TRUE
|
||||
keep_local_name = TRUE
|
||||
assets = list(
|
||||
"tgui-panel.bundle.js" = file("tgui/public/tgui-panel.bundle.js"),
|
||||
"tgui-panel.bundle.css" = file("tgui/public/tgui-panel.bundle.css"),
|
||||
|
||||
@@ -132,6 +132,9 @@
|
||||
asset_cache_preload_data(href_list["asset_cache_preload_data"])
|
||||
return
|
||||
|
||||
if(href_list["commandbar_typing"])
|
||||
handle_commandbar_typing(href_list)
|
||||
|
||||
switch(href_list["_src_"])
|
||||
if("holder") hsrc = holder
|
||||
if("mentorholder") hsrc = (check_rights(R_ADMIN, 0) ? holder : mentorholder)
|
||||
@@ -204,6 +207,8 @@
|
||||
stat_panel.subscribe(src, .proc/on_stat_panel_message)
|
||||
|
||||
// Instantiate tgui panel
|
||||
tgui_say = new(src, "tgui_say")
|
||||
initialize_commandbar_spy()
|
||||
tgui_panel = new(src, "browseroutput")
|
||||
|
||||
GLOB.tickets.ClientLogin(src) // CHOMPedit - Tickets System
|
||||
@@ -243,6 +248,7 @@
|
||||
addtimer(CALLBACK(src, PROC_REF(check_panel_loaded)), 30 SECONDS)
|
||||
|
||||
// Initialize tgui panel
|
||||
tgui_say.initialize()
|
||||
tgui_panel.initialize()
|
||||
|
||||
connection_time = world.time
|
||||
|
||||
@@ -240,7 +240,13 @@ var/list/_client_preferences_by_type
|
||||
|
||||
/datum/client_preference/show_typing_indicator/toggled(var/mob/preference_mob, var/enabled)
|
||||
if(!enabled)
|
||||
preference_mob.set_typing_indicator(FALSE)
|
||||
preference_mob.client?.stop_thinking()
|
||||
|
||||
/datum/client_preference/show_typing_indicator_subtle
|
||||
description ="Typing indicator (subtle)"
|
||||
key = "SHOW_TYPING_SUBTLE"
|
||||
enabled_description = "Show"
|
||||
disabled_description = "Hide"
|
||||
|
||||
/datum/client_preference/show_ooc
|
||||
description ="OOC chat"
|
||||
@@ -423,6 +429,19 @@ var/list/_client_preferences_by_type
|
||||
enabled_description = "Automatic"
|
||||
disabled_description = "Manual Only"
|
||||
|
||||
/datum/client_preference/tgui_say
|
||||
description = "TGUI Say: Use TGUI For Say Input"
|
||||
key = "TGUI_SAY"
|
||||
enabled_by_default = TRUE
|
||||
enabled_description = "Yes"
|
||||
disabled_description = "No"
|
||||
|
||||
/datum/client_preference/tgui_say_light
|
||||
description = "TGUI Say: Use Light Mode"
|
||||
key = "TGUI_SAY_LIGHT_MODE"
|
||||
enabled_by_default = FALSE
|
||||
enabled_description = "Yes"
|
||||
disabled_description = "No"
|
||||
|
||||
/********************
|
||||
* Staff Preferences *
|
||||
|
||||
77
code/modules/client/verbs/typing.dm
Normal file
77
code/modules/client/verbs/typing.dm
Normal file
@@ -0,0 +1,77 @@
|
||||
#define IC_VERBS list("say", "me", "whisper", "subtle")
|
||||
|
||||
/client/var/commandbar_thinking = FALSE
|
||||
/client/var/commandbar_typing = FALSE
|
||||
|
||||
/client/proc/initialize_commandbar_spy()
|
||||
src << output('html/typing_indicator.html', "commandbar_spy")
|
||||
|
||||
/client/proc/handle_commandbar_typing(href_list)
|
||||
if(!is_preference_enabled(/datum/client_preference/show_typing_indicator))
|
||||
return
|
||||
|
||||
if(length(href_list["verb"]) < 1 || !(lowertext(href_list["verb"]) in IC_VERBS) || text2num(href_list["argument_length"]) < 1)
|
||||
if(commandbar_typing)
|
||||
commandbar_typing = FALSE
|
||||
stop_typing()
|
||||
|
||||
if(commandbar_thinking)
|
||||
commandbar_thinking = FALSE
|
||||
stop_thinking()
|
||||
return
|
||||
|
||||
if(!commandbar_thinking)
|
||||
commandbar_thinking = TRUE
|
||||
start_thinking(href_list["verb"])
|
||||
|
||||
if(!commandbar_typing)
|
||||
commandbar_typing = TRUE
|
||||
start_typing(href_list["verb"])
|
||||
|
||||
|
||||
/** Sets the mob as "thinking" - with indicator and the TRAIT_THINKING_IN_CHARACTER trait */
|
||||
/client/proc/start_thinking(channel)
|
||||
if(!is_preference_enabled(/datum/client_preference/show_typing_indicator))
|
||||
return FALSE
|
||||
if(channel == "Whis" || channel == "Subtle" || channel == "whisper" || channel == "subtle")
|
||||
if(!is_preference_enabled(/datum/client_preference/show_typing_indicator_subtle))
|
||||
return FALSE
|
||||
ADD_TRAIT(mob, TRAIT_THINKING_IN_CHARACTER, CURRENTLY_TYPING_TRAIT)
|
||||
mob.create_thinking_indicator()
|
||||
|
||||
/** Removes typing/thinking indicators and flags the mob as not thinking */
|
||||
/client/proc/stop_thinking(channel)
|
||||
mob?.remove_all_indicators()
|
||||
|
||||
/**
|
||||
* Handles the user typing. After a brief period of inactivity,
|
||||
* signals the client mob to revert to the "thinking" icon.
|
||||
*/
|
||||
/client/proc/start_typing(channel)
|
||||
var/mob/client_mob = mob
|
||||
client_mob.remove_thinking_indicator()
|
||||
if(!is_preference_enabled(/datum/client_preference/show_typing_indicator) || !HAS_TRAIT(client_mob, TRAIT_THINKING_IN_CHARACTER))
|
||||
return FALSE
|
||||
if(channel == "Whis" || channel == "Subtle" || channel == "whisper" || channel == "subtle")
|
||||
if(!is_preference_enabled(/datum/client_preference/show_typing_indicator_subtle))
|
||||
return FALSE
|
||||
client_mob.create_typing_indicator()
|
||||
addtimer(CALLBACK(src, PROC_REF(stop_typing), channel), 5 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE | TIMER_STOPPABLE)
|
||||
|
||||
/**
|
||||
* Callback to remove the typing indicator after a brief period of inactivity.
|
||||
* If the user was typing IC, the thinking indicator is shown.
|
||||
*/
|
||||
/client/proc/stop_typing(channel)
|
||||
if(isnull(mob))
|
||||
return FALSE
|
||||
var/mob/client_mob = mob
|
||||
client_mob.remove_typing_indicator()
|
||||
if(!is_preference_enabled(/datum/client_preference/show_typing_indicator) || !HAS_TRAIT(client_mob, TRAIT_THINKING_IN_CHARACTER))
|
||||
return FALSE
|
||||
if(channel == "Whis" || channel == "Subtle" || channel == "whisper" || channel == "subtle")
|
||||
if(!is_preference_enabled(/datum/client_preference/show_typing_indicator_subtle))
|
||||
return FALSE
|
||||
client_mob.create_thinking_indicator()
|
||||
|
||||
#undef IC_VERBS
|
||||
@@ -53,33 +53,6 @@
|
||||
|
||||
return loc
|
||||
|
||||
/mob/living/carbon/brain/set_typing_indicator(var/state)
|
||||
if(isturf(loc))
|
||||
return ..()
|
||||
|
||||
if(!is_preference_enabled(/datum/client_preference/show_typing_indicator))
|
||||
loc.cut_overlay(typing_indicator, TRUE)
|
||||
return
|
||||
|
||||
var/cur_bubble_appearance = custom_speech_bubble
|
||||
if(!cur_bubble_appearance || cur_bubble_appearance == "default")
|
||||
cur_bubble_appearance = speech_bubble_appearance()
|
||||
if(!typing_indicator || cur_typing_indicator != cur_bubble_appearance)
|
||||
init_typing_indicator("[cur_bubble_appearance]_typing")
|
||||
|
||||
if(state && !typing)
|
||||
add_overlay(typing_indicator, TRUE)
|
||||
typing = TRUE
|
||||
typing_indicator_active = typing_indicator
|
||||
else if(typing)
|
||||
cut_overlay(typing_indicator_active, TRUE)
|
||||
typing = FALSE
|
||||
if(typing_indicator_active != typing_indicator)
|
||||
qdel(typing_indicator_active)
|
||||
typing_indicator_active = null
|
||||
|
||||
return state
|
||||
|
||||
// Vorestation edit start
|
||||
|
||||
/mob/living/carbon/brain/verb/backup_ping()
|
||||
|
||||
@@ -988,8 +988,8 @@
|
||||
return
|
||||
|
||||
cut_overlays()
|
||||
if(typing) //CHOMPAdd, needed as we don't have priority overlays anymore
|
||||
add_overlay(typing_indicator, TRUE) //CHOMPAdd, needed as we don't have priority overlays anymore
|
||||
add_overlay(active_thinking_indicator)
|
||||
add_overlay(active_typing_indicator)
|
||||
handle_status_indicators() //CHOMPAdd, needed as we don't have priority overlays anymore
|
||||
|
||||
icon = sprite_datum.sprite_icon
|
||||
|
||||
@@ -219,10 +219,6 @@
|
||||
|
||||
var/get_rig_stats = 0 //Moved from computer.dm
|
||||
|
||||
var/typing
|
||||
var/obj/effect/decal/typing_indicator
|
||||
var/obj/effect/decal/typing_indicator_active
|
||||
var/cur_typing_indicator
|
||||
var/custom_speech_bubble = "default"
|
||||
|
||||
var/low_priority = FALSE //Skip processing life() if there's just no players on this Z-level
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/mob/proc/say(var/message, var/datum/language/speaking = null, var/whispering = 0)
|
||||
return
|
||||
|
||||
/mob/verb/whisper(message as text)
|
||||
/mob/verb/whisper(message as text) //CHOMPEdit
|
||||
set name = "Whisper"
|
||||
set category = "IC.Subtle" //CHOMPEdit
|
||||
// set category = "IC.Subtle" //CHOMPEdit
|
||||
set hidden = 1
|
||||
//VOREStation Addition Start
|
||||
if(forced_psay)
|
||||
psay(message)
|
||||
@@ -12,18 +13,18 @@
|
||||
|
||||
usr.say(message,whispering=1)
|
||||
|
||||
/mob/verb/say_verb(message as text)
|
||||
/mob/verb/say_verb(message as text) //CHOMPEdit
|
||||
set name = "Say"
|
||||
set category = "IC.Chat" //CHOMPEdit
|
||||
// set category = "IC.Chat" //CHOMPEdit
|
||||
set instant = TRUE // CHOMPEdit
|
||||
|
||||
set hidden = 1
|
||||
//VOREStation Addition Start
|
||||
if(forced_psay)
|
||||
psay(message)
|
||||
return
|
||||
//VOREStation Addition End
|
||||
|
||||
set_typing_indicator(FALSE)
|
||||
client?.stop_thinking()
|
||||
// CHOMPEdit Start
|
||||
//queue this message because verbs are scheduled to process after SendMaps in the tick and speech is pretty expensive when it happens.
|
||||
//by queuing this for next tick the mc can compensate for its cost instead of having speech delay the start of the next tick
|
||||
@@ -31,9 +32,11 @@
|
||||
QUEUE_OR_CALL_VERB_FOR(VERB_CALLBACK(src, TYPE_PROC_REF(/mob, say), message), SSspeech_controller)
|
||||
// CHOMPEdit End
|
||||
|
||||
/mob/verb/me_verb(message as message)
|
||||
/mob/verb/me_verb(message as message) //CHOMPEdit
|
||||
set name = "Me"
|
||||
set category = "IC.Chat" //CHOMPEdit
|
||||
// set category = "IC.Chat" //CHOMPEdit
|
||||
set desc = "Emote to nearby people (and your pred/prey)"
|
||||
set hidden = 1
|
||||
|
||||
if(say_disabled) //This is here to try to identify lag problems
|
||||
to_chat(usr, span_red("Speech is currently admin-disabled."))
|
||||
@@ -52,7 +55,7 @@
|
||||
message = sanitize_or_reflect(message,src) //VOREStation Edit - Reflect too-long messages (within reason)
|
||||
//VOREStation Edit End
|
||||
|
||||
set_typing_indicator(FALSE)
|
||||
client?.stop_thinking()
|
||||
if(use_me)
|
||||
custom_emote(usr.emote_type, message)
|
||||
else
|
||||
|
||||
@@ -7,3 +7,85 @@
|
||||
ultimate_mob = to_check
|
||||
to_check = to_check.loc
|
||||
return ultimate_mob
|
||||
|
||||
/mob/verb/say_verb_ch()
|
||||
set name = "Say CH"
|
||||
set category = "IC.Chat"
|
||||
|
||||
client?.start_thinking()
|
||||
client?.start_typing()
|
||||
var/message = tgui_input_text(usr, "Speak to people in sight.\nType your message:", "Say")
|
||||
client?.stop_thinking()
|
||||
|
||||
if(message)
|
||||
say_verb(message)
|
||||
|
||||
/mob/verb/me_verb_ch()
|
||||
set name = "Me CH"
|
||||
set category = "IC.Chat"
|
||||
set desc = "Emote to nearby people (and your pred/prey)"
|
||||
|
||||
client?.start_thinking()
|
||||
client?.start_typing()
|
||||
var/message = tgui_input_text(usr, "Emote to people in sight (and your pred/prey).\nType your message:", "Emote", multiline = TRUE)
|
||||
client?.stop_thinking()
|
||||
|
||||
if(message)
|
||||
me_verb(message)
|
||||
|
||||
/mob/verb/whisper_ch()
|
||||
set name = "Whisper CH"
|
||||
set category = "IC.Subtle"
|
||||
|
||||
var/message = tgui_input_text(usr, "Speak to nearby people.\nType your message:", "Whisper")
|
||||
|
||||
if(message)
|
||||
whisper(message)
|
||||
|
||||
|
||||
/mob/verb/me_verb_subtle_ch()
|
||||
set name = "Subtle CH"
|
||||
set category = "IC.Subtle"
|
||||
set desc = "Emote to nearby people (and your pred/prey)"
|
||||
|
||||
var/message = tgui_input_text(usr, "Emote to nearby people (and your pred/prey).\nType your message:", "Subtle", multiline = TRUE)
|
||||
|
||||
if(message)
|
||||
me_verb_subtle(message)
|
||||
|
||||
/mob/verb/me_verb_subtle_custom_ch()
|
||||
set name = "Subtle (Custom) CH"
|
||||
set category = "IC.Subtle"
|
||||
set desc = "Emote to nearby people, with ability to choose which specific portion of people you wish to target."
|
||||
|
||||
var/message = tgui_input_text(usr, "Emote to nearby people, with ability to choose which specific portion of people you wish to target.\nType your message:", "Subtle (Custom)", multiline = TRUE)
|
||||
|
||||
if(message)
|
||||
me_verb_subtle_custom(message)
|
||||
|
||||
/mob/verb/psay_ch()
|
||||
set name = "Psay CH"
|
||||
set category = "IC.Subtle"
|
||||
|
||||
var/message = tgui_input_text(usr, "Talk to people affected by complete absorbed or dominate predator/prey.\nType your message:", "Psay")
|
||||
|
||||
if(message)
|
||||
psay(message)
|
||||
|
||||
/mob/verb/pme_ch()
|
||||
set name = "Pme CH"
|
||||
set category = "IC.Subtle"
|
||||
|
||||
var/message = tgui_input_text(usr, "Emote to people affected by complete absorbed or dominate predator/prey.\nType your message:", "Psay")
|
||||
|
||||
if(message)
|
||||
pme(message)
|
||||
|
||||
/mob/living/verb/player_narrate_ch()
|
||||
set name = "Narrate (Player) CH"
|
||||
set category = "IC.Chat"
|
||||
|
||||
var/message = tgui_input_text(usr, "Narrate an action or event! An alternative to emoting, for when your emote shouldn't start with your name!\nType your message:", "Psay")
|
||||
|
||||
if(message)
|
||||
player_narrate(message)
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
////////////////////SUBTLE COMMAND////////////////////
|
||||
//////////////////////////////////////////////////////
|
||||
|
||||
/mob/verb/me_verb_subtle(message as message) //This would normally go in say.dm
|
||||
/mob/verb/me_verb_subtle(message as message) //This would normally go in say.dm //CHOMPEdit
|
||||
set name = "Subtle"
|
||||
set category = "IC.Subtle" //CHOMPEdit
|
||||
// set category = "IC.Subtle" //CHOMPEdit
|
||||
set desc = "Emote to nearby people (and your pred/prey)"
|
||||
set hidden = 1
|
||||
|
||||
if(say_disabled) //This is here to try to identify lag problems
|
||||
to_chat(usr, "Speech is currently admin-disabled.")
|
||||
@@ -18,15 +19,15 @@
|
||||
if(!message)
|
||||
return
|
||||
|
||||
set_typing_indicator(FALSE)
|
||||
client?.stop_thinking()
|
||||
if(use_me)
|
||||
usr.emote_vr("me",4,message)
|
||||
else
|
||||
usr.emote_vr(message)
|
||||
|
||||
/mob/verb/me_verb_subtle_custom(message as message) // Literally same as above but with mode_selection set to true
|
||||
/mob/verb/me_verb_subtle_custom(message as message) // Literally same as above but with mode_selection set to true //CHOMPEdit
|
||||
set name = "Subtle (Custom)"
|
||||
set category = "IC.Subtle" //CHOMPEdit
|
||||
// set category = "IC.Subtle" //CHOMPEdit
|
||||
set desc = "Emote to nearby people, with ability to choose which specific portion of people you wish to target."
|
||||
|
||||
if(say_disabled) //This is here to try to identify lag problems
|
||||
@@ -40,7 +41,7 @@
|
||||
if(!message)
|
||||
return
|
||||
|
||||
set_typing_indicator(FALSE)
|
||||
client?.stop_thinking()
|
||||
if(use_me)
|
||||
usr.emote_vr("me",4,message,TRUE)
|
||||
else
|
||||
@@ -259,8 +260,8 @@
|
||||
|
||||
///// PSAY /////
|
||||
|
||||
/mob/verb/psay(message as text)
|
||||
set category = "IC.Subtle" //CHOMPEdit
|
||||
/mob/verb/psay(message as text) //CHOMPEdit
|
||||
// set category = "IC.Subtle" //CHOMPEdit
|
||||
set name = "Psay"
|
||||
set desc = "Talk to people affected by complete absorbed or dominate predator/prey."
|
||||
|
||||
@@ -365,8 +366,8 @@
|
||||
|
||||
///// PME /////
|
||||
|
||||
/mob/verb/pme(message as message)
|
||||
set category = "IC.Subtle" //CHOMPEdit
|
||||
/mob/verb/pme(message as message) //CHOMPEdit
|
||||
// set category = "IC.Subtle" //CHOMPEdit
|
||||
set name = "Pme"
|
||||
set desc = "Emote to people affected by complete absorbed or dominate predator/prey."
|
||||
|
||||
@@ -469,8 +470,8 @@
|
||||
M.forced_psay = FALSE
|
||||
M.me_verb(message)
|
||||
|
||||
/mob/living/verb/player_narrate(message as message)
|
||||
set category = "IC.Chat" //CHOMPEdit
|
||||
/mob/living/verb/player_narrate(message as message) //CHOMPEdit
|
||||
// set category = "IC.Chat" //CHOMPEdit
|
||||
set name = "Narrate (Player)"
|
||||
set desc = "Narrate an action or event! An alternative to emoting, for when your emote shouldn't start with your name!"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/proc/generate_speech_bubble(var/bubble_loc, var/speech_state, var/set_layer = FLOAT_LAYER)
|
||||
/proc/generate_speech_bubble(bubble_loc, speech_state, set_layer = FLOAT_LAYER)
|
||||
var/image/I = image('icons/mob/talk_vr.dmi', bubble_loc, speech_state, set_layer) //VOREStation Edit - talk_vr.dmi instead of talk.dmi for right-side icons
|
||||
I.appearance_flags |= (RESET_COLOR|PIXEL_SCALE) //VOREStation Edit
|
||||
/* //VOREStation Removal Start
|
||||
@@ -11,76 +11,69 @@
|
||||
*/ //VOREStation Removal Start
|
||||
return I
|
||||
|
||||
/mob/proc/init_typing_indicator(var/set_state = "typing")
|
||||
if(typing_indicator)
|
||||
qdel(typing_indicator)
|
||||
typing_indicator = null
|
||||
typing_indicator = new
|
||||
typing_indicator.appearance = generate_speech_bubble(null, set_state)
|
||||
typing_indicator.appearance_flags |= (RESET_COLOR|PIXEL_SCALE) //VOREStation Edit
|
||||
/mob/verb/say_wrapper()
|
||||
set name = "Say verb"
|
||||
set category = "IC.TGUI Say" //CHOMPEdit
|
||||
|
||||
/mob/proc/set_typing_indicator(var/state) //Leaving this here for mobs.
|
||||
|
||||
if(!is_preference_enabled(/datum/client_preference/show_typing_indicator))
|
||||
if(typing_indicator)
|
||||
cut_overlay(typing_indicator, TRUE)
|
||||
if(is_preference_enabled(/datum/client_preference/tgui_say))
|
||||
winset(src, null, "command=[client.tgui_say_create_open_command(SAY_CHANNEL)]")
|
||||
return
|
||||
|
||||
var/cur_bubble_appearance = custom_speech_bubble
|
||||
if(!cur_bubble_appearance || cur_bubble_appearance == "default")
|
||||
cur_bubble_appearance = speech_bubble_appearance()
|
||||
if(!typing_indicator || cur_typing_indicator != cur_bubble_appearance)
|
||||
init_typing_indicator("[cur_bubble_appearance]_typing")
|
||||
|
||||
if(state && !typing)
|
||||
add_overlay(typing_indicator, TRUE)
|
||||
typing = TRUE
|
||||
typing_indicator_active = typing_indicator
|
||||
else if(typing)
|
||||
cut_overlay(typing_indicator_active, TRUE)
|
||||
typing = FALSE
|
||||
if(typing_indicator_active != typing_indicator)
|
||||
qdel(typing_indicator_active)
|
||||
typing_indicator_active = null
|
||||
|
||||
return state
|
||||
|
||||
/mob/verb/say_wrapper()
|
||||
set name = ".Say"
|
||||
set hidden = 1
|
||||
|
||||
set_typing_indicator(TRUE)
|
||||
client?.start_thinking()
|
||||
client?.start_typing()
|
||||
var/message = tgui_input_text(usr, "Type your message:", "Say")
|
||||
set_typing_indicator(FALSE)
|
||||
client?.stop_thinking()
|
||||
|
||||
if(message)
|
||||
say_verb(message)
|
||||
|
||||
/mob/verb/me_wrapper()
|
||||
set name = ".Me"
|
||||
set hidden = 1
|
||||
set name = "Me verb"
|
||||
set category = "IC.TGUI Say" //CHOMPEdit
|
||||
|
||||
set_typing_indicator(TRUE)
|
||||
if(is_preference_enabled(/datum/client_preference/tgui_say))
|
||||
winset(src, null, "command=[client.tgui_say_create_open_command(ME_CHANNEL)]")
|
||||
return
|
||||
|
||||
client?.start_thinking()
|
||||
client?.start_typing()
|
||||
var/message = tgui_input_text(usr, "Type your message:", "Emote", multiline = TRUE)
|
||||
set_typing_indicator(FALSE)
|
||||
client?.stop_thinking()
|
||||
|
||||
if(message)
|
||||
me_verb(message)
|
||||
|
||||
// No typing indicators here, but this is the file where the wrappers are, so...
|
||||
/mob/verb/whisper_wrapper()
|
||||
set name = ".Whisper"
|
||||
set hidden = 1
|
||||
set name = "Whisper verb"
|
||||
set category = "IC.TGUI Say" //CHOMPEdit
|
||||
|
||||
if(is_preference_enabled(/datum/client_preference/tgui_say))
|
||||
winset(src, null, "command=[client.tgui_say_create_open_command(WHIS_CHANNEL)]")
|
||||
return
|
||||
|
||||
if(is_preference_enabled(/datum/client_preference/show_typing_indicator_subtle))
|
||||
client?.start_thinking()
|
||||
client?.start_typing()
|
||||
var/message = tgui_input_text(usr, "Type your message:", "Whisper")
|
||||
client?.stop_thinking()
|
||||
|
||||
if(message)
|
||||
whisper(message)
|
||||
|
||||
/mob/verb/subtle_wrapper()
|
||||
set name = ".Subtle"
|
||||
set hidden = 1
|
||||
set name = "Subtle verb"
|
||||
set category = "IC.TGUI Say" //CHOMPEdit
|
||||
set desc = "Emote to nearby people (and your pred/prey)"
|
||||
|
||||
if(is_preference_enabled(/datum/client_preference/tgui_say))
|
||||
winset(src, null, "command=[client.tgui_say_create_open_command(SUBTLE_CHANNEL)]")
|
||||
return
|
||||
|
||||
if(is_preference_enabled(/datum/client_preference/show_typing_indicator_subtle))
|
||||
client?.start_thinking()
|
||||
client?.start_typing()
|
||||
var/message = tgui_input_text(usr, "Type your message:", "Subtle", multiline = TRUE)
|
||||
client?.stop_thinking()
|
||||
|
||||
if(message)
|
||||
me_verb_subtle(message)
|
||||
|
||||
@@ -126,6 +126,8 @@
|
||||
/datum/asset/simple/namespaced/fontawesome))
|
||||
flush_queue |= window.send_asset(get_asset_datum(
|
||||
/datum/asset/simple/namespaced/tgfont))
|
||||
flush_queue |= window.send_asset(get_asset_datum(
|
||||
/datum/asset/json/icon_ref_map))
|
||||
for(var/datum/asset/asset in src_object.ui_assets(user))
|
||||
flush_queue |= window.send_asset(asset)
|
||||
if (flush_queue)
|
||||
|
||||
@@ -119,11 +119,11 @@
|
||||
html = replacetextEx(html, "<!-- tgui:inline-html -->", inline_html)
|
||||
// Inject inline JS
|
||||
if (inline_js)
|
||||
inline_js = "<script>\n'use strict';\n[inline_js]\n</script>"
|
||||
inline_js = "<script>\n'use strict';\n[isfile(inline_js) ? file2text(inline_js) : inline_js]\n</script>"
|
||||
html = replacetextEx(html, "<!-- tgui:inline-js -->", inline_js)
|
||||
// Inject inline CSS
|
||||
if (inline_css)
|
||||
inline_css = "<style>\n[inline_css]\n</style>"
|
||||
inline_css = "<style>\n[isfile(inline_css) ? file2text(inline_css) : inline_css]\n</style>"
|
||||
html = replacetextEx(html, "<!-- tgui:inline-css -->", inline_css)
|
||||
// Open the window
|
||||
client << browse(html, "window=[id];[options]")
|
||||
@@ -146,6 +146,9 @@
|
||||
inline_html = initial_inline_html,
|
||||
inline_js = initial_inline_js,
|
||||
inline_css = initial_inline_css)
|
||||
// Resend assets
|
||||
for(var/datum/asset/asset in sent_assets)
|
||||
send_asset(asset)
|
||||
|
||||
/**
|
||||
* public
|
||||
|
||||
129
code/modules/tgui_input/say_modal/modal.dm
Normal file
129
code/modules/tgui_input/say_modal/modal.dm
Normal file
@@ -0,0 +1,129 @@
|
||||
/** Assigned say modal of the client */
|
||||
/client/var/datum/tgui_say/tgui_say
|
||||
|
||||
/**
|
||||
* Creates a JSON encoded message to open TGUI say modals properly.
|
||||
*
|
||||
* Arguments:
|
||||
* channel - The channel to open the modal in.
|
||||
* Returns:
|
||||
* string - A JSON encoded message to open the modal.
|
||||
*/
|
||||
/client/proc/tgui_say_create_open_command(channel)
|
||||
var/message = TGUI_CREATE_MESSAGE("open", list(
|
||||
channel = channel,
|
||||
))
|
||||
return "\".output tgui_say.browser:update [message]\""
|
||||
|
||||
/**
|
||||
* The tgui say modal. This initializes an input window which hides until
|
||||
* the user presses one of the speech hotkeys. Once something is entered, it will
|
||||
* delegate the speech to the proper channel.
|
||||
*/
|
||||
/datum/tgui_say
|
||||
/// The user who opened the window
|
||||
var/client/client
|
||||
/// Injury phrases to blurt out
|
||||
var/list/hurt_phrases = list("GACK!", "GLORF!", "OOF!", "AUGH!", "OW!", "URGH!", "HRNK!")
|
||||
/// Max message length
|
||||
var/max_length = MAX_MESSAGE_LEN
|
||||
/// The modal window
|
||||
var/datum/tgui_window/window
|
||||
/// Boolean for whether the tgui_say was opened by the user.
|
||||
var/window_open
|
||||
|
||||
/** Creates the new input window to exist in the background. */
|
||||
/datum/tgui_say/New(client/client, id)
|
||||
src.client = client
|
||||
window = new(client, id)
|
||||
winset(client, "tgui_say", "size=1,1;is-visible=0;")
|
||||
window.subscribe(src, PROC_REF(on_message))
|
||||
window.is_browser = TRUE
|
||||
|
||||
/**
|
||||
* After a brief period, injects the scripts into
|
||||
* the window to listen for open commands.
|
||||
*/
|
||||
/datum/tgui_say/proc/initialize()
|
||||
set waitfor = FALSE
|
||||
// Sleep to defer initialization to after client constructor
|
||||
sleep(3 SECONDS)
|
||||
window.initialize(
|
||||
strict_mode = TRUE,
|
||||
fancy = TRUE,
|
||||
inline_css = file("tgui/public/tgui-say.bundle.css"),
|
||||
inline_js = file("tgui/public/tgui-say.bundle.js"),
|
||||
);
|
||||
|
||||
/**
|
||||
* Ensures nothing funny is going on window load.
|
||||
* Minimizes the window, sets max length, closes all
|
||||
* typing and thinking indicators. This is triggered
|
||||
* as soon as the window sends the "ready" message.
|
||||
*/
|
||||
/datum/tgui_say/proc/load()
|
||||
window_open = FALSE
|
||||
|
||||
winset(client, "tgui_say", "pos=410,400;size=360,30;is-visible=0;")
|
||||
|
||||
window.send_message("props", list(
|
||||
lightMode = client.is_preference_enabled(/datum/client_preference/tgui_say_light),
|
||||
maxLength = max_length,
|
||||
))
|
||||
|
||||
stop_thinking()
|
||||
return TRUE
|
||||
|
||||
/**
|
||||
* Sets the window as "opened" server side, though it is already
|
||||
* visible to the user. We do this to set local vars &
|
||||
* start typing (if enabled and in an IC channel). Logs the event.
|
||||
*
|
||||
* Arguments:
|
||||
* payload - A list containing the channel the window was opened in.
|
||||
*/
|
||||
/datum/tgui_say/proc/open(payload)
|
||||
if(!payload?["channel"])
|
||||
CRASH("No channel provided to an open TGUI-Say")
|
||||
window_open = TRUE
|
||||
if(payload["channel"] != OOC_CHANNEL && payload["channel"] != ADMIN_CHANNEL)
|
||||
start_thinking()
|
||||
return TRUE
|
||||
|
||||
/**
|
||||
* Closes the window serverside. Closes any open chat bubbles
|
||||
* regardless of preference. Logs the event.
|
||||
*/
|
||||
/datum/tgui_say/proc/close()
|
||||
window_open = FALSE
|
||||
stop_thinking()
|
||||
|
||||
/**
|
||||
* The equivalent of ui_act, this waits on messages from the window
|
||||
* and delegates actions.
|
||||
*/
|
||||
/datum/tgui_say/proc/on_message(type, payload)
|
||||
if(type == "ready")
|
||||
load()
|
||||
return TRUE
|
||||
if(type == "open")
|
||||
open(payload)
|
||||
return TRUE
|
||||
if(type == "close")
|
||||
close()
|
||||
return TRUE
|
||||
if(type == "thinking")
|
||||
if(payload["visible"] == TRUE)
|
||||
start_thinking(payload["channel"])
|
||||
return TRUE
|
||||
if(payload["visible"] == FALSE)
|
||||
stop_thinking(payload["channel"])
|
||||
return TRUE
|
||||
return FALSE
|
||||
if(type == "typing")
|
||||
start_typing(payload["channel"])
|
||||
return TRUE
|
||||
if(type == "entry" || type == "force")
|
||||
handle_entry(type, payload)
|
||||
return TRUE
|
||||
return FALSE
|
||||
106
code/modules/tgui_input/say_modal/speech.dm
Normal file
106
code/modules/tgui_input/say_modal/speech.dm
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Alters text when players are injured.
|
||||
* Adds text, trims left and right side
|
||||
*
|
||||
* Arguments:
|
||||
* payload - a string list containing entry & channel
|
||||
* Returns:
|
||||
* string - the altered entry
|
||||
*/
|
||||
/datum/tgui_say/proc/alter_entry(payload)
|
||||
var/entry = payload["entry"]
|
||||
/// No OOC leaks
|
||||
if(!entry || payload["channel"] == OOC_CHANNEL || payload["channel"] == ME_CHANNEL || payload["channel"] == LOOC_CHANNEL)
|
||||
return pick(hurt_phrases)
|
||||
/// Random trimming for larger sentences
|
||||
if(length(entry) > 50)
|
||||
entry = trim(entry, rand(40, 50))
|
||||
else
|
||||
/// Otherwise limit trim to just last letter
|
||||
if(length(entry) > 1)
|
||||
entry = trim(entry, length(entry))
|
||||
return entry + "-" + pick(hurt_phrases)
|
||||
|
||||
/**
|
||||
* Delegates the speech to the proper channel.
|
||||
*
|
||||
* Arguments:
|
||||
* entry - the text to broadcast
|
||||
* channel - the channel to broadcast in
|
||||
* Returns:
|
||||
* boolean - on success or failure
|
||||
*/
|
||||
/datum/tgui_say/proc/delegate_speech(entry, channel)
|
||||
switch(channel)
|
||||
if(SAY_CHANNEL)
|
||||
client.mob.say_verb(entry)
|
||||
return TRUE
|
||||
if(RADIO_CHANNEL)
|
||||
client.mob.say_verb(";" + entry)
|
||||
return TRUE
|
||||
if(ME_CHANNEL)
|
||||
client.mob.me_verb(entry)
|
||||
return TRUE
|
||||
if(WHIS_CHANNEL)
|
||||
client.mob.whisper(entry)
|
||||
return TRUE
|
||||
if(SUBTLE_CHANNEL)
|
||||
client.mob.me_verb_subtle(entry)
|
||||
return TRUE
|
||||
if(OOC_CHANNEL)
|
||||
client.ooc(entry)
|
||||
return TRUE
|
||||
if(LOOC_CHANNEL)
|
||||
client.looc(entry)
|
||||
return TRUE
|
||||
if(ADMIN_CHANNEL)
|
||||
if(check_rights(R_ADMIN, show_msg = FALSE))
|
||||
client.cmd_admin_say(entry)
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/**
|
||||
* Force say handler.
|
||||
* Sends a message to the say modal to send its current value.
|
||||
*/
|
||||
// /datum/tgui_say/proc/force_say()
|
||||
// window.send_message("force")
|
||||
// stop_typing()
|
||||
|
||||
/**
|
||||
* Makes the player force say what's in their current input box.
|
||||
*/
|
||||
// /mob/living/carbon/human/proc/force_say()
|
||||
// if(stat != CONSCIOUS || !client?.tgui_say?.window_open)
|
||||
// return FALSE
|
||||
// client.tgui_say.force_say()
|
||||
// if(client.typing_indicators)
|
||||
// log_speech_indicators("[key_name(client)] FORCED to stop typing, indicators enabled.")
|
||||
// else
|
||||
// log_speech_indicators("[key_name(client)] FORCED to stop typing, indicators DISABLED.")
|
||||
// SEND_SIGNAL(src, COMSIG_HUMAN_FORCESAY)
|
||||
|
||||
/**
|
||||
* Handles text entry and forced speech.
|
||||
*
|
||||
* Arguments:
|
||||
* type - a string "entry" or "force" based on how this function is called
|
||||
* payload - a string list containing entry & channel
|
||||
* Returns:
|
||||
* boolean - success or failure
|
||||
*/
|
||||
/datum/tgui_say/proc/handle_entry(type, payload)
|
||||
if(!payload?["channel"] || !payload["entry"])
|
||||
CRASH("[usr] entered in a null payload to the chat window.")
|
||||
if(length(payload["entry"]) > max_length)
|
||||
CRASH("[usr] has entered more characters than allowed into a TGUI-Say")
|
||||
if(type == "entry")
|
||||
delegate_speech(payload["entry"], payload["channel"])
|
||||
return TRUE
|
||||
if(type == "force")
|
||||
var/target_channel = payload["channel"]
|
||||
if(target_channel == ME_CHANNEL || target_channel == OOC_CHANNEL || target_channel == LOOC_CHANNEL)
|
||||
target_channel = SAY_CHANNEL // No ooc leaks - this is fine because alter_entry will override this completely
|
||||
delegate_speech(alter_entry(payload), target_channel)
|
||||
return TRUE
|
||||
return FALSE
|
||||
75
code/modules/tgui_input/say_modal/typing.dm
Normal file
75
code/modules/tgui_input/say_modal/typing.dm
Normal file
@@ -0,0 +1,75 @@
|
||||
/mob
|
||||
///the icon currently used for the typing indicator's bubble
|
||||
var/mutable_appearance/active_typing_indicator
|
||||
///the icon currently used for the thinking indicator's bubble
|
||||
var/mutable_appearance/active_thinking_indicator
|
||||
|
||||
/** Creates a thinking indicator over the mob. Note: Prefs are checked in /client/proc/start_thinking() */
|
||||
/mob/proc/create_thinking_indicator()
|
||||
if(active_thinking_indicator || active_typing_indicator || stat != CONSCIOUS || !HAS_TRAIT(src, TRAIT_THINKING_IN_CHARACTER))
|
||||
return FALSE
|
||||
var/cur_bubble_appearance = custom_speech_bubble
|
||||
if(!cur_bubble_appearance || cur_bubble_appearance == "default")
|
||||
cur_bubble_appearance = speech_bubble_appearance()
|
||||
active_thinking_indicator = mutable_appearance('icons/mob/talk_vr.dmi', "[cur_bubble_appearance]_thinking", FLOAT_LAYER)
|
||||
active_thinking_indicator.appearance_flags |= (RESET_COLOR|PIXEL_SCALE)
|
||||
add_overlay(active_thinking_indicator)
|
||||
|
||||
/** Removes the thinking indicator over the mob. */
|
||||
/mob/proc/remove_thinking_indicator()
|
||||
if(!active_thinking_indicator)
|
||||
return FALSE
|
||||
cut_overlay(active_thinking_indicator)
|
||||
active_thinking_indicator = null
|
||||
|
||||
/** Creates a typing indicator over the mob. Note: Prefs are checked in /client/proc/start_typing() */
|
||||
/mob/proc/create_typing_indicator()
|
||||
if(active_typing_indicator || active_thinking_indicator || stat != CONSCIOUS || !HAS_TRAIT(src, TRAIT_THINKING_IN_CHARACTER))
|
||||
return FALSE
|
||||
var/cur_bubble_appearance = custom_speech_bubble
|
||||
if(!cur_bubble_appearance || cur_bubble_appearance == "default")
|
||||
cur_bubble_appearance = speech_bubble_appearance()
|
||||
active_typing_indicator = mutable_appearance('icons/mob/talk_vr.dmi', "[cur_bubble_appearance]_typing", ABOVE_MOB_LAYER)
|
||||
active_typing_indicator.appearance_flags |= (RESET_COLOR|PIXEL_SCALE)
|
||||
add_overlay(active_typing_indicator)
|
||||
|
||||
/** Removes the typing indicator over the mob. */
|
||||
/mob/proc/remove_typing_indicator()
|
||||
if(!active_typing_indicator)
|
||||
return FALSE
|
||||
cut_overlay(active_typing_indicator)
|
||||
active_typing_indicator = null
|
||||
|
||||
/** Removes any indicators and marks the mob as not speaking IC. */
|
||||
/mob/proc/remove_all_indicators()
|
||||
REMOVE_TRAIT(src, TRAIT_THINKING_IN_CHARACTER, CURRENTLY_TYPING_TRAIT)
|
||||
remove_thinking_indicator()
|
||||
remove_typing_indicator()
|
||||
|
||||
/mob/set_stat(new_stat)
|
||||
. = ..()
|
||||
if(.)
|
||||
remove_all_indicators()
|
||||
|
||||
/mob/Logout()
|
||||
remove_all_indicators()
|
||||
return ..()
|
||||
|
||||
/** Sets the mob as "thinking" - with indicator and the TRAIT_THINKING_IN_CHARACTER trait */
|
||||
/datum/tgui_say/proc/start_thinking(channel)
|
||||
if(!window_open)
|
||||
return FALSE
|
||||
return client.start_thinking(channel)
|
||||
|
||||
/** Removes typing/thinking indicators and flags the mob as not thinking */
|
||||
/datum/tgui_say/proc/stop_thinking(channel)
|
||||
return client.stop_thinking(channel)
|
||||
|
||||
/**
|
||||
* Handles the user typing. After a brief period of inactivity,
|
||||
* signals the client mob to revert to the "thinking" icon.
|
||||
*/
|
||||
/datum/tgui_say/proc/start_typing(channel)
|
||||
if(!window_open)
|
||||
return FALSE
|
||||
return client.start_typing(channel)
|
||||
46
html/typing_indicator.html
Normal file
46
html/typing_indicator.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
lastseentypedtext = "";
|
||||
function getoutput() {
|
||||
setTimeout(getoutput, 1000);
|
||||
window.location = "byond://winget?callback=checkoutput&id=:Input&property=text";
|
||||
}
|
||||
function checkoutput(props) {
|
||||
if (typeof props !== 'object')
|
||||
return;
|
||||
|
||||
if (typeof props.text !== 'string' && !(props.text instanceof String))
|
||||
return;
|
||||
|
||||
var text = props.text;
|
||||
|
||||
if (text == lastseentypedtext)
|
||||
return;
|
||||
|
||||
lastseentypedtext = text;
|
||||
|
||||
var words = text.split(" ");
|
||||
var verb = words[0];
|
||||
var argument = "";
|
||||
var argument_length = -1;
|
||||
|
||||
if (words.length >= 2) {
|
||||
words.splice(0, 1)
|
||||
argument = words.join(" ");
|
||||
argument_length = argument.length;
|
||||
}
|
||||
|
||||
if (argument_length > 0 && argument[0] == "\"")
|
||||
argument_length -= 1;
|
||||
|
||||
window.location = "byond://?commandbar_typing=1&verb="+encodeURIComponent(verb)+"&argument_length="+argument_length;
|
||||
}
|
||||
setTimeout(getoutput, 2000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 15 KiB |
@@ -124,10 +124,10 @@ macro "borghotkeymode"
|
||||
command = "a-intent left"
|
||||
elem
|
||||
name = "5"
|
||||
command = ".me"
|
||||
command = "Me-verb"
|
||||
elem
|
||||
name = "6"
|
||||
command = ".Subtle"
|
||||
command = "Subtle-verb"
|
||||
elem
|
||||
name = "A"
|
||||
command = "KeyDown A"
|
||||
@@ -178,7 +178,7 @@ macro "borghotkeymode"
|
||||
command = "KeyUp S"
|
||||
elem
|
||||
name = "T"
|
||||
command = ".say"
|
||||
command = "Say-verb"
|
||||
elem "w_key"
|
||||
name = "W"
|
||||
command = "KeyDown W"
|
||||
@@ -193,10 +193,10 @@ macro "borghotkeymode"
|
||||
command = ".northeast"
|
||||
elem
|
||||
name = "Y"
|
||||
command = ".Whisper"
|
||||
command = "Whisper-verb"
|
||||
elem
|
||||
name = "CTRL+Y"
|
||||
command = ".Whisper"
|
||||
command = "Whisper-verb"
|
||||
elem
|
||||
name = "Z"
|
||||
command = "Activate-Held-Object"
|
||||
@@ -244,10 +244,10 @@ macro "borghotkeymode"
|
||||
command = ".screenshot"
|
||||
elem
|
||||
name = "F3"
|
||||
command = ".say"
|
||||
command = "Say-verb"
|
||||
elem
|
||||
name = "F4"
|
||||
command = ".me"
|
||||
command = "Me-verb"
|
||||
elem
|
||||
name = "F5"
|
||||
command = "asay"
|
||||
@@ -474,10 +474,10 @@ macro "macro"
|
||||
command = ".screenshot"
|
||||
elem
|
||||
name = "F3"
|
||||
command = ".say"
|
||||
command = "Say-verb"
|
||||
elem
|
||||
name = "F4"
|
||||
command = ".me"
|
||||
command = "Me-verb"
|
||||
elem
|
||||
name = "F5"
|
||||
command = "asay"
|
||||
@@ -626,10 +626,10 @@ macro "hotkeymode"
|
||||
command = "a-intent harm"
|
||||
elem
|
||||
name = "5"
|
||||
command = ".me"
|
||||
command = "Me-verb"
|
||||
elem
|
||||
name = "6"
|
||||
command = ".Subtle"
|
||||
command = "Subtle-verb"
|
||||
elem
|
||||
name = "A"
|
||||
command = "KeyDown A"
|
||||
@@ -692,7 +692,7 @@ macro "hotkeymode"
|
||||
command = "KeyUp S"
|
||||
elem
|
||||
name = "T"
|
||||
command = ".say"
|
||||
command = "Say-verb"
|
||||
elem "w_key"
|
||||
name = "W"
|
||||
command = "KeyDown W"
|
||||
@@ -707,10 +707,10 @@ macro "hotkeymode"
|
||||
command = ".northeast"
|
||||
elem
|
||||
name = "Y"
|
||||
command = ".Whisper"
|
||||
command = "Whisper-verb"
|
||||
elem
|
||||
name = "CTRL+Y"
|
||||
command = ".Whisper"
|
||||
command = "Whisper-verb"
|
||||
elem
|
||||
name = "Z"
|
||||
command = "Activate-Held-Object"
|
||||
@@ -767,10 +767,10 @@ macro "hotkeymode"
|
||||
command = ".screenshot"
|
||||
elem
|
||||
name = "F3"
|
||||
command = ".say"
|
||||
command = "Say-verb"
|
||||
elem
|
||||
name = "F4"
|
||||
command = ".me"
|
||||
command = "Me-verb"
|
||||
elem
|
||||
name = "F5"
|
||||
command = "asay"
|
||||
@@ -991,10 +991,10 @@ macro "borgmacro"
|
||||
command = ".screenshot"
|
||||
elem
|
||||
name = "F3"
|
||||
command = ".say"
|
||||
command = "Say-verb"
|
||||
elem
|
||||
name = "F4"
|
||||
command = ".me"
|
||||
command = "Me-verb"
|
||||
elem
|
||||
name = "F5"
|
||||
command = "asay"
|
||||
@@ -1230,6 +1230,15 @@ window "mainwindow"
|
||||
anchor2 = -1,-1
|
||||
is-visible = false
|
||||
saved-params = ""
|
||||
elem "commandbar_spy"
|
||||
type = BROWSER
|
||||
is-default = false
|
||||
pos = 0,0
|
||||
size = 200x200
|
||||
anchor1 = -1,-1
|
||||
anchor2 = -1,-1
|
||||
is-visible = false
|
||||
saved-params = ""
|
||||
|
||||
window "mapwindow"
|
||||
elem "mapwindow"
|
||||
@@ -1579,4 +1588,22 @@ window "statwindow"
|
||||
anchor1 = 0,0
|
||||
anchor2 = 100,100
|
||||
is-visible = false
|
||||
|
||||
window "tgui_say"
|
||||
elem "tgui_say"
|
||||
type = MAIN
|
||||
pos = 410,400
|
||||
size = 360x30
|
||||
anchor1 = 50,50
|
||||
anchor2 = 50,50
|
||||
is-visible = false
|
||||
saved-params = ""
|
||||
statusbar = false
|
||||
can-minimize = false
|
||||
elem "browser"
|
||||
type = BROWSER
|
||||
pos = 0,0
|
||||
size = 360x30
|
||||
anchor1 = 0,0
|
||||
anchor2 = 0,0
|
||||
saved-params = ""
|
||||
|
||||
61
tgui/packages/tgui-say/ChannelIterator.test.ts
Normal file
61
tgui/packages/tgui-say/ChannelIterator.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { ChannelIterator } from './ChannelIterator';
|
||||
|
||||
describe('ChannelIterator', () => {
|
||||
let channelIterator: ChannelIterator;
|
||||
|
||||
beforeEach(() => {
|
||||
channelIterator = new ChannelIterator();
|
||||
});
|
||||
|
||||
it('should cycle through channels properly', () => {
|
||||
expect(channelIterator.current()).toBe('Say');
|
||||
expect(channelIterator.next()).toBe('Radio');
|
||||
expect(channelIterator.next()).toBe('Me');
|
||||
expect(channelIterator.next()).toBe('Whis');
|
||||
expect(channelIterator.next()).toBe('Subtle');
|
||||
expect(channelIterator.next()).toBe('LOOC');
|
||||
expect(channelIterator.next()).toBe('OOC');
|
||||
expect(channelIterator.next()).toBe('Say'); // Admin is blacklisted so it should be skipped
|
||||
});
|
||||
|
||||
it('should cycle through channels backwards properly', () => {
|
||||
expect(channelIterator.current()).toBe('Say');
|
||||
expect(channelIterator.prev()).toBe('OOC'); // Admin is blacklisted so it should be skipped
|
||||
expect(channelIterator.prev()).toBe('LOOC');
|
||||
expect(channelIterator.prev()).toBe('Subtle');
|
||||
expect(channelIterator.prev()).toBe('Whis');
|
||||
expect(channelIterator.prev()).toBe('Me');
|
||||
expect(channelIterator.prev()).toBe('Radio');
|
||||
expect(channelIterator.prev()).toBe('Say');
|
||||
});
|
||||
|
||||
it('should set a channel properly', () => {
|
||||
channelIterator.set('OOC');
|
||||
expect(channelIterator.current()).toBe('OOC');
|
||||
});
|
||||
|
||||
it('should return true when current channel is "Say"', () => {
|
||||
channelIterator.set('Say');
|
||||
expect(channelIterator.isSay()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when current channel is not "Say"', () => {
|
||||
channelIterator.set('Radio');
|
||||
expect(channelIterator.isSay()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when current channel is visible', () => {
|
||||
channelIterator.set('Say');
|
||||
expect(channelIterator.isVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when current channel is not visible', () => {
|
||||
channelIterator.set('OOC');
|
||||
expect(channelIterator.isVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not leak a message from a blacklisted channel', () => {
|
||||
channelIterator.set('Admin');
|
||||
expect(channelIterator.next()).toBe('Admin');
|
||||
});
|
||||
});
|
||||
89
tgui/packages/tgui-say/ChannelIterator.ts
Normal file
89
tgui/packages/tgui-say/ChannelIterator.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
export type Channel =
|
||||
| 'Say'
|
||||
| 'Radio'
|
||||
| 'Me'
|
||||
| 'Whis'
|
||||
| 'Subtle'
|
||||
| 'LOOC'
|
||||
| 'OOC'
|
||||
| 'Admin';
|
||||
|
||||
/**
|
||||
* ### ChannelIterator
|
||||
* Cycles a predefined list of channels,
|
||||
* skipping over blacklisted ones,
|
||||
* and providing methods to manage and query the current channel.
|
||||
*/
|
||||
export class ChannelIterator {
|
||||
private index: number = 0;
|
||||
private readonly channels: Channel[] = [
|
||||
'Say',
|
||||
'Radio',
|
||||
'Me',
|
||||
'Whis',
|
||||
'Subtle',
|
||||
'LOOC',
|
||||
'OOC',
|
||||
'Admin',
|
||||
];
|
||||
private readonly blacklist: Channel[] = ['Admin'];
|
||||
private readonly quiet: Channel[] = ['OOC', 'LOOC', 'Admin'];
|
||||
private readonly multiline: Channel[] = ['Me', 'Subtle'];
|
||||
|
||||
public next(): Channel {
|
||||
if (this.blacklist.includes(this.channels[this.index])) {
|
||||
return this.channels[this.index];
|
||||
}
|
||||
|
||||
for (let index = 1; index <= this.channels.length; index++) {
|
||||
let nextIndex = (this.index + index) % this.channels.length;
|
||||
if (!this.blacklist.includes(this.channels[nextIndex])) {
|
||||
this.index = nextIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this.channels[this.index];
|
||||
}
|
||||
|
||||
public prev(): Channel {
|
||||
if (this.blacklist.includes(this.channels[this.index])) {
|
||||
return this.channels[this.index];
|
||||
}
|
||||
|
||||
for (let index = 1; index <= this.channels.length; index++) {
|
||||
let nextIndex =
|
||||
(this.index - index + this.channels.length) % this.channels.length;
|
||||
if (!this.blacklist.includes(this.channels[nextIndex])) {
|
||||
this.index = nextIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this.channels[this.index];
|
||||
}
|
||||
|
||||
public set(channel: Channel): void {
|
||||
this.index = this.channels.indexOf(channel) || 0;
|
||||
}
|
||||
|
||||
public current(): Channel {
|
||||
return this.channels[this.index];
|
||||
}
|
||||
|
||||
public isSay(): boolean {
|
||||
return this.channels[this.index] === 'Say';
|
||||
}
|
||||
|
||||
public isMultiline(): boolean {
|
||||
return this.multiline.includes(this.channels[this.index]);
|
||||
}
|
||||
|
||||
public isVisible(): boolean {
|
||||
return !this.quiet.includes(this.channels[this.index]);
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.index = 0;
|
||||
}
|
||||
}
|
||||
50
tgui/packages/tgui-say/ChatHistory.test.ts
Normal file
50
tgui/packages/tgui-say/ChatHistory.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { ChatHistory } from './ChatHistory';
|
||||
|
||||
describe('ChatHistory', () => {
|
||||
let chatHistory: ChatHistory;
|
||||
|
||||
beforeEach(() => {
|
||||
chatHistory = new ChatHistory();
|
||||
});
|
||||
|
||||
it('should add a message to the history', () => {
|
||||
chatHistory.add('Hello');
|
||||
expect(chatHistory.getOlderMessage()).toEqual('Hello');
|
||||
});
|
||||
|
||||
it('should retrieve older and newer messages', () => {
|
||||
chatHistory.add('Hello');
|
||||
chatHistory.add('World');
|
||||
expect(chatHistory.getOlderMessage()).toEqual('World');
|
||||
expect(chatHistory.getOlderMessage()).toEqual('Hello');
|
||||
expect(chatHistory.getNewerMessage()).toEqual('World');
|
||||
expect(chatHistory.getNewerMessage()).toBeNull();
|
||||
expect(chatHistory.getOlderMessage()).toEqual('World');
|
||||
});
|
||||
|
||||
it('should limit the history to 5 messages', () => {
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
chatHistory.add(`Message ${i}`);
|
||||
}
|
||||
|
||||
expect(chatHistory.getOlderMessage()).toEqual('Message 6');
|
||||
for (let i = 5; i >= 2; i--) {
|
||||
expect(chatHistory.getOlderMessage()).toEqual(`Message ${i}`);
|
||||
}
|
||||
expect(chatHistory.getOlderMessage()).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle temp message correctly', () => {
|
||||
chatHistory.saveTemp('Temp message');
|
||||
expect(chatHistory.getTemp()).toEqual('Temp message');
|
||||
expect(chatHistory.getTemp()).toBeNull();
|
||||
});
|
||||
|
||||
it('should reset correctly', () => {
|
||||
chatHistory.add('Hello');
|
||||
chatHistory.getOlderMessage();
|
||||
chatHistory.reset();
|
||||
expect(chatHistory.isAtLatest()).toBe(true);
|
||||
expect(chatHistory.getOlderMessage()).toEqual('Hello');
|
||||
});
|
||||
});
|
||||
59
tgui/packages/tgui-say/ChatHistory.ts
Normal file
59
tgui/packages/tgui-say/ChatHistory.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* ### ChatHistory
|
||||
* A class to manage a chat history,
|
||||
* maintaining a maximum of five messages and supporting navigation,
|
||||
* temporary message storage, and query operations.
|
||||
*/
|
||||
export class ChatHistory {
|
||||
private messages: string[] = [];
|
||||
private index: number = -1; // Initialize index at -1
|
||||
private temp: string | null = null;
|
||||
|
||||
public add(message: string): void {
|
||||
this.messages.unshift(message);
|
||||
this.index = -1; // Reset index
|
||||
if (this.messages.length > 5) {
|
||||
this.messages.pop();
|
||||
}
|
||||
}
|
||||
|
||||
public getIndex(): number {
|
||||
return this.index + 1;
|
||||
}
|
||||
|
||||
public getOlderMessage(): string | null {
|
||||
if (this.messages.length === 0 || this.index >= this.messages.length - 1) {
|
||||
return null;
|
||||
}
|
||||
this.index++;
|
||||
return this.messages[this.index];
|
||||
}
|
||||
|
||||
public getNewerMessage(): string | null {
|
||||
if (this.index <= 0) {
|
||||
this.index = -1;
|
||||
return null;
|
||||
}
|
||||
this.index--;
|
||||
return this.messages[this.index];
|
||||
}
|
||||
|
||||
public isAtLatest(): boolean {
|
||||
return this.index === -1;
|
||||
}
|
||||
|
||||
public saveTemp(message: string): void {
|
||||
this.temp = message;
|
||||
}
|
||||
|
||||
public getTemp(): string | null {
|
||||
const temp = this.temp;
|
||||
this.temp = null;
|
||||
return temp;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.index = -1;
|
||||
this.temp = null;
|
||||
}
|
||||
}
|
||||
389
tgui/packages/tgui-say/TguiSay.tsx
Normal file
389
tgui/packages/tgui-say/TguiSay.tsx
Normal file
@@ -0,0 +1,389 @@
|
||||
import { KEY } from 'common/keys';
|
||||
import { BooleanLike } from 'common/react';
|
||||
import { Component, createRef, RefObject } from 'react';
|
||||
import { dragStartHandler } from 'tgui/drag';
|
||||
import {
|
||||
removeAllSkiplines,
|
||||
sanitizeMultiline,
|
||||
} from 'tgui/interfaces/TextInputModal';
|
||||
|
||||
import { Channel, ChannelIterator } from './ChannelIterator';
|
||||
import { ChatHistory } from './ChatHistory';
|
||||
import { LINE_LENGTHS, RADIO_PREFIXES, WINDOW_SIZES } from './constants';
|
||||
import { windowClose, windowOpen, windowSet } from './helpers';
|
||||
import { byondMessages } from './timers';
|
||||
|
||||
type ByondOpen = {
|
||||
channel: Channel;
|
||||
};
|
||||
|
||||
type ByondProps = {
|
||||
maxLength: number;
|
||||
lightMode: BooleanLike;
|
||||
};
|
||||
|
||||
type State = {
|
||||
buttonContent: string | number;
|
||||
size: WINDOW_SIZES;
|
||||
};
|
||||
|
||||
const CHANNEL_REGEX = /^:\w\s|^,b\s/;
|
||||
|
||||
export class TguiSay extends Component<{}, State> {
|
||||
private channelIterator: ChannelIterator;
|
||||
private chatHistory: ChatHistory;
|
||||
private currentPrefix: keyof typeof RADIO_PREFIXES | null;
|
||||
private innerRef: RefObject<HTMLTextAreaElement>;
|
||||
private lightMode: boolean;
|
||||
private maxLength: number;
|
||||
private messages: typeof byondMessages;
|
||||
state: State;
|
||||
|
||||
constructor(props: never) {
|
||||
super(props);
|
||||
|
||||
this.channelIterator = new ChannelIterator();
|
||||
this.chatHistory = new ChatHistory();
|
||||
this.currentPrefix = null;
|
||||
this.innerRef = createRef();
|
||||
this.lightMode = false;
|
||||
this.maxLength = 1024;
|
||||
this.messages = byondMessages;
|
||||
this.state = {
|
||||
buttonContent: '',
|
||||
size: WINDOW_SIZES.small,
|
||||
};
|
||||
|
||||
this.handleArrowKeys = this.handleArrowKeys.bind(this);
|
||||
this.handleBackspaceDelete = this.handleBackspaceDelete.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
this.handleEnter = this.handleEnter.bind(this);
|
||||
this.handleForceSay = this.handleForceSay.bind(this);
|
||||
this.handleIncrementChannel = this.handleIncrementChannel.bind(this);
|
||||
this.handleInput = this.handleInput.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.handleOpen = this.handleOpen.bind(this);
|
||||
this.handleProps = this.handleProps.bind(this);
|
||||
this.reset = this.reset.bind(this);
|
||||
this.setSize = this.setSize.bind(this);
|
||||
this.setValue = this.setValue.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
Byond.subscribeTo('props', this.handleProps);
|
||||
Byond.subscribeTo('force', this.handleForceSay);
|
||||
Byond.subscribeTo('open', this.handleOpen);
|
||||
}
|
||||
|
||||
handleArrowKeys(direction: KEY.PageUp | KEY.PageDown) {
|
||||
const currentValue = this.innerRef.current?.value;
|
||||
|
||||
if (direction === KEY.PageUp) {
|
||||
if (this.chatHistory.isAtLatest() && currentValue) {
|
||||
// Save current message to temp history if at the most recent message
|
||||
this.chatHistory.saveTemp(currentValue);
|
||||
}
|
||||
// Try to get the previous message, fall back to the current value if none
|
||||
const prevMessage = this.chatHistory.getOlderMessage();
|
||||
|
||||
if (prevMessage) {
|
||||
this.setState({ buttonContent: this.chatHistory.getIndex() });
|
||||
this.setSize(prevMessage.length);
|
||||
this.setValue(prevMessage);
|
||||
}
|
||||
} else {
|
||||
const nextMessage =
|
||||
this.chatHistory.getNewerMessage() || this.chatHistory.getTemp() || '';
|
||||
|
||||
const buttonContent = this.chatHistory.isAtLatest()
|
||||
? this.channelIterator.current()
|
||||
: this.chatHistory.getIndex();
|
||||
|
||||
this.setState({ buttonContent });
|
||||
this.setSize(nextMessage.length);
|
||||
this.setValue(nextMessage);
|
||||
}
|
||||
}
|
||||
|
||||
handleBackspaceDelete() {
|
||||
const typed = this.innerRef.current?.value;
|
||||
|
||||
// User is on a chat history message
|
||||
if (!this.chatHistory.isAtLatest()) {
|
||||
this.chatHistory.reset();
|
||||
this.setState({
|
||||
buttonContent: this.currentPrefix ?? this.channelIterator.current(),
|
||||
});
|
||||
// Empty input, resets the channel
|
||||
} else if (
|
||||
!!this.currentPrefix &&
|
||||
this.channelIterator.isSay() &&
|
||||
typed?.length === 0
|
||||
) {
|
||||
this.currentPrefix = null;
|
||||
this.setState({ buttonContent: this.channelIterator.current() });
|
||||
}
|
||||
|
||||
this.setSize(typed?.length);
|
||||
}
|
||||
|
||||
handleClose() {
|
||||
const current = this.innerRef.current;
|
||||
|
||||
if (current) {
|
||||
current.blur();
|
||||
}
|
||||
|
||||
this.reset();
|
||||
this.chatHistory.reset();
|
||||
this.channelIterator.reset();
|
||||
this.currentPrefix = null;
|
||||
windowClose();
|
||||
}
|
||||
|
||||
handleEnter() {
|
||||
const prefix = this.currentPrefix ?? '';
|
||||
const value = this.innerRef.current?.value;
|
||||
|
||||
if (value?.length && value.length < this.maxLength) {
|
||||
this.chatHistory.add(value);
|
||||
|
||||
// Everything can be multiline, but only emotes get passed that way to the game
|
||||
const sanitizedValue = this.channelIterator.isMultiline()
|
||||
? sanitizeMultiline(value)
|
||||
: removeAllSkiplines(value);
|
||||
|
||||
Byond.sendMessage('entry', {
|
||||
channel: this.channelIterator.current(),
|
||||
entry: this.channelIterator.isSay()
|
||||
? prefix + sanitizedValue
|
||||
: sanitizedValue,
|
||||
});
|
||||
}
|
||||
|
||||
this.handleClose();
|
||||
}
|
||||
|
||||
handleForceSay() {
|
||||
const currentValue = this.innerRef.current?.value;
|
||||
// Only force say if we're on a visible channel and have typed something
|
||||
if (!currentValue || !this.channelIterator.isVisible()) return;
|
||||
|
||||
const prefix = this.currentPrefix ?? '';
|
||||
const grunt = this.channelIterator.isSay()
|
||||
? prefix + currentValue
|
||||
: currentValue;
|
||||
|
||||
this.messages.forceSayMsg(grunt);
|
||||
this.reset();
|
||||
}
|
||||
|
||||
handleIncrementChannel() {
|
||||
this.currentPrefix = null;
|
||||
|
||||
this.channelIterator.next();
|
||||
|
||||
// If we've looped onto a quiet channel, tell byond to hide thinking indicators
|
||||
if (!this.channelIterator.isVisible()) {
|
||||
this.messages.channelIncrementMsg(false, this.channelIterator.current());
|
||||
} else {
|
||||
this.messages.channelIncrementMsg(true, this.channelIterator.current());
|
||||
}
|
||||
|
||||
this.setState({ buttonContent: this.channelIterator.current() });
|
||||
}
|
||||
|
||||
handleDecrementChannel() {
|
||||
this.currentPrefix = null;
|
||||
|
||||
this.channelIterator.prev();
|
||||
|
||||
// If we've looped onto a quiet channel, tell byond to hide thinking indicators
|
||||
if (!this.channelIterator.isVisible()) {
|
||||
this.messages.channelIncrementMsg(false, this.channelIterator.current());
|
||||
} else {
|
||||
this.messages.channelIncrementMsg(true, this.channelIterator.current());
|
||||
}
|
||||
|
||||
this.setState({ buttonContent: this.channelIterator.current() });
|
||||
}
|
||||
|
||||
handleInput() {
|
||||
const typed = this.innerRef.current?.value;
|
||||
|
||||
// If we're typing, send the message
|
||||
if (this.channelIterator.isVisible()) {
|
||||
this.messages.typingMsg(this.channelIterator.current());
|
||||
}
|
||||
|
||||
this.setSize(typed?.length);
|
||||
|
||||
// Is there a value? Is it long enough to be a prefix?
|
||||
if (!typed || typed.length < 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CHANNEL_REGEX.test(typed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Is it a valid prefix?
|
||||
const prefix = typed
|
||||
.slice(0, 3)
|
||||
?.toLowerCase() as keyof typeof RADIO_PREFIXES;
|
||||
if (!RADIO_PREFIXES[prefix] || prefix === this.currentPrefix) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.channelIterator.set('Say');
|
||||
this.currentPrefix = prefix;
|
||||
this.setState({ buttonContent: RADIO_PREFIXES[prefix] });
|
||||
this.setValue(typed.slice(3));
|
||||
}
|
||||
|
||||
handleKeyDown(event: React.KeyboardEvent<HTMLTextAreaElement>) {
|
||||
const currentValue = this.innerRef.current?.value;
|
||||
switch (event.key) {
|
||||
case KEY.PageUp:
|
||||
case KEY.PageDown:
|
||||
// Allow moving between lines if there are newlines
|
||||
/* if (currentValue?.includes('\n')) {
|
||||
break;
|
||||
} */
|
||||
event.preventDefault();
|
||||
this.handleArrowKeys(event.key);
|
||||
break;
|
||||
|
||||
case KEY.Delete:
|
||||
case KEY.Backspace:
|
||||
this.handleBackspaceDelete();
|
||||
break;
|
||||
|
||||
case KEY.Enter:
|
||||
// Allow inputting newlines
|
||||
if (event.shiftKey) {
|
||||
break;
|
||||
}
|
||||
event.preventDefault();
|
||||
this.handleEnter();
|
||||
break;
|
||||
|
||||
case KEY.Tab:
|
||||
event.preventDefault();
|
||||
if (event.shiftKey) {
|
||||
this.handleDecrementChannel();
|
||||
} else {
|
||||
this.handleIncrementChannel();
|
||||
}
|
||||
break;
|
||||
|
||||
case KEY.Escape:
|
||||
this.handleClose();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleOpen = (data: ByondOpen) => {
|
||||
setTimeout(() => {
|
||||
this.innerRef.current?.focus();
|
||||
}, 0);
|
||||
|
||||
const { channel } = data;
|
||||
// Catches the case where the modal is already open
|
||||
if (this.channelIterator.isSay()) {
|
||||
this.channelIterator.set(channel);
|
||||
}
|
||||
this.setState({ buttonContent: this.channelIterator.current() });
|
||||
|
||||
windowOpen(this.channelIterator.current());
|
||||
};
|
||||
|
||||
handleProps = (data: ByondProps) => {
|
||||
const { maxLength, lightMode } = data;
|
||||
this.maxLength = maxLength;
|
||||
this.lightMode = !!lightMode;
|
||||
};
|
||||
|
||||
reset() {
|
||||
this.setValue('');
|
||||
this.setSize();
|
||||
this.setState({
|
||||
buttonContent: this.channelIterator.current(),
|
||||
});
|
||||
}
|
||||
|
||||
setSize(length = 0) {
|
||||
let newSize: WINDOW_SIZES;
|
||||
|
||||
const currentValue = this.innerRef.current?.value;
|
||||
|
||||
if (currentValue?.includes('\n')) {
|
||||
newSize = WINDOW_SIZES.large;
|
||||
} else if (length > LINE_LENGTHS.medium) {
|
||||
newSize = WINDOW_SIZES.large;
|
||||
} else if (length <= LINE_LENGTHS.medium && length > LINE_LENGTHS.small) {
|
||||
newSize = WINDOW_SIZES.medium;
|
||||
} else {
|
||||
newSize = WINDOW_SIZES.small;
|
||||
}
|
||||
|
||||
if (this.state.size !== newSize) {
|
||||
this.setState({ size: newSize });
|
||||
windowSet(newSize);
|
||||
}
|
||||
}
|
||||
|
||||
setValue(value: string) {
|
||||
const textArea = this.innerRef.current;
|
||||
if (textArea) {
|
||||
textArea.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme =
|
||||
(this.lightMode && 'lightMode') ||
|
||||
(this.currentPrefix && RADIO_PREFIXES[this.currentPrefix]) ||
|
||||
this.channelIterator.current();
|
||||
|
||||
return (
|
||||
<div className={`window window-${theme} window-${this.state.size}`}>
|
||||
<Dragzone position="top" theme={theme} />
|
||||
<div className="center">
|
||||
<Dragzone position="left" theme={theme} />
|
||||
<div className="input">
|
||||
<button
|
||||
className={`button button-${theme}`}
|
||||
onClick={this.handleIncrementChannel}
|
||||
type="button"
|
||||
>
|
||||
{this.state.buttonContent}
|
||||
</button>
|
||||
<textarea
|
||||
className={`textarea textarea-${theme}`}
|
||||
maxLength={this.maxLength}
|
||||
onInput={this.handleInput}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
ref={this.innerRef}
|
||||
/>
|
||||
</div>
|
||||
<Dragzone position="right" theme={theme} />
|
||||
</div>
|
||||
<Dragzone position="bottom" theme={theme} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Dragzone = ({ theme, position }: { theme: string; position: string }) => {
|
||||
// Horizontal or vertical?
|
||||
const location =
|
||||
position === 'left' || position === 'right' ? 'vertical' : 'horizontal';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`dragzone-${location} dragzone-${position} dragzone-${theme}`}
|
||||
onMouseDown={dragStartHandler}
|
||||
/>
|
||||
);
|
||||
};
|
||||
37
tgui/packages/tgui-say/constants.ts
Normal file
37
tgui/packages/tgui-say/constants.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/** Window sizes in pixels */
|
||||
export enum WINDOW_SIZES {
|
||||
small = 30,
|
||||
medium = 50,
|
||||
large = 70,
|
||||
width = 360,
|
||||
}
|
||||
|
||||
/** Line lengths for autoexpand */
|
||||
export enum LINE_LENGTHS {
|
||||
small = 22,
|
||||
medium = 45,
|
||||
}
|
||||
|
||||
/**
|
||||
* Radio prefixes.
|
||||
* Displays the name in the left button, tags a css class.
|
||||
*/
|
||||
export const RADIO_PREFIXES = {
|
||||
':a ': 'EVA',
|
||||
',b ': '010',
|
||||
':c ': 'Cmd',
|
||||
':e ': 'Eng',
|
||||
':g ': 'Cas',
|
||||
':h ': 'Dept',
|
||||
':i ': 'Int',
|
||||
':k ': 'ERT',
|
||||
':m ': 'Med',
|
||||
':n ': 'Sci',
|
||||
':p ': 'AI',
|
||||
':s ': 'Sec',
|
||||
':t ': 'Merc',
|
||||
':u ': 'Sup',
|
||||
':v ': 'Srv',
|
||||
':x ': 'Rai',
|
||||
':y ': 'ITV',
|
||||
} as const;
|
||||
BIN
tgui/packages/tgui-say/fonts/VT323-Regular.ttf
Normal file
BIN
tgui/packages/tgui-say/fonts/VT323-Regular.ttf
Normal file
Binary file not shown.
46
tgui/packages/tgui-say/helpers.ts
Normal file
46
tgui/packages/tgui-say/helpers.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Channel } from './ChannelIterator';
|
||||
import { WINDOW_SIZES } from './constants';
|
||||
|
||||
/**
|
||||
* Once byond signals this via keystroke, it
|
||||
* ensures window size, visibility, and focus.
|
||||
*/
|
||||
export const windowOpen = (channel: Channel) => {
|
||||
setWindowVisibility(true);
|
||||
Byond.sendMessage('open', { channel });
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets the state of the window and hides it from user view.
|
||||
* Sending "close" logs it server side.
|
||||
*/
|
||||
export const windowClose = () => {
|
||||
setWindowVisibility(false);
|
||||
Byond.winset('map', {
|
||||
focus: true,
|
||||
});
|
||||
Byond.sendMessage('close');
|
||||
};
|
||||
|
||||
/**
|
||||
* Modifies the window size.
|
||||
*/
|
||||
export const windowSet = (size = WINDOW_SIZES.small) => {
|
||||
let sizeStr = `${WINDOW_SIZES.width}x${size}`;
|
||||
|
||||
Byond.winset('tgui_say.browser', {
|
||||
size: sizeStr,
|
||||
});
|
||||
|
||||
Byond.winset('tgui_say', {
|
||||
size: sizeStr,
|
||||
});
|
||||
};
|
||||
|
||||
/** Helper function to set window size and visibility */
|
||||
const setWindowVisibility = (visible: boolean) => {
|
||||
Byond.winset('tgui_say', {
|
||||
'is-visible': visible,
|
||||
size: `${WINDOW_SIZES.width}x${WINDOW_SIZES.small}`,
|
||||
});
|
||||
};
|
||||
18
tgui/packages/tgui-say/index.tsx
Normal file
18
tgui/packages/tgui-say/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import './styles/main.scss';
|
||||
|
||||
import { createRoot, Root } from 'react-dom/client';
|
||||
|
||||
import { TguiSay } from './TguiSay';
|
||||
|
||||
let reactRoot: Root | null = null;
|
||||
|
||||
document.onreadystatechange = function () {
|
||||
if (document.readyState !== 'complete') return;
|
||||
|
||||
if (!reactRoot) {
|
||||
const root = document.getElementById('react-root');
|
||||
reactRoot = createRoot(root!);
|
||||
}
|
||||
|
||||
reactRoot.render(<TguiSay />);
|
||||
};
|
||||
14
tgui/packages/tgui-say/package.json
Normal file
14
tgui/packages/tgui-say/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "tgui-say",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@types/react": "^18.2.74",
|
||||
"@types/react-dom": "^18.2.24",
|
||||
"common": "workspace:*",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tgui": "workspace:*",
|
||||
"tgui-polyfill": "workspace:*"
|
||||
}
|
||||
}
|
||||
29
tgui/packages/tgui-say/styles/button.scss
Normal file
29
tgui/packages/tgui-say/styles/button.scss
Normal file
@@ -0,0 +1,29 @@
|
||||
@use 'sass:color';
|
||||
@use './colors.scss';
|
||||
|
||||
.button {
|
||||
align-items: center;
|
||||
background-color: colors.$button;
|
||||
border-radius: 0.3rem;
|
||||
border: thin solid;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
font-family: inherit;
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
width: 1rem;
|
||||
&:hover {
|
||||
background-color: lighten(colors.$button, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
.button-lightMode {
|
||||
background-color: colors.$lightBorder;
|
||||
border: none;
|
||||
color: black;
|
||||
&:hover {
|
||||
background-color: colors.$lightHover;
|
||||
}
|
||||
}
|
||||
46
tgui/packages/tgui-say/styles/colors.scss
Normal file
46
tgui/packages/tgui-say/styles/colors.scss
Normal file
@@ -0,0 +1,46 @@
|
||||
@use 'sass:map';
|
||||
|
||||
$background: #131313;
|
||||
$button: #1f1f1f;
|
||||
$lightMode: #ffffff;
|
||||
$lightBorder: #bbbbbb;
|
||||
$lightHover: #eaeaea;
|
||||
|
||||
$_channel_map: (
|
||||
// Radio (EVA, Cas, Int use default color)
|
||||
'010': #1e90ff,
|
||||
'Cmd': #57b8f0,
|
||||
'Eng': #fcdf03,
|
||||
'Dept': #1ecc43,
|
||||
'ERT': #5c5c8a,
|
||||
'Med': #57f09e,
|
||||
'Sci': #c68cfa,
|
||||
'AI': #d65d95,
|
||||
'Sec': #dd3535,
|
||||
'Merc': #8f4a4b,
|
||||
'Sup': #b88646,
|
||||
'Srv': #6ca729,
|
||||
'Rai': #8f4a4b,
|
||||
'ITV': #8a8a8a,
|
||||
// Modes
|
||||
'LOOC': #3a9696,
|
||||
'Me': #5975da,
|
||||
'OOC': #cca300,
|
||||
'Radio': #1ecc43,
|
||||
'Say': #a4bad6,
|
||||
'Whis': #7c7fd9,
|
||||
'Subtle': #6366bd
|
||||
);
|
||||
|
||||
$channel_keys: map.keys($_channel_map) !default;
|
||||
|
||||
$channel-map: ();
|
||||
|
||||
@each $channel in $channel_keys {
|
||||
$channel-map: map-merge(
|
||||
$channel-map,
|
||||
(
|
||||
$channel: map.get($_channel_map, $channel),
|
||||
)
|
||||
);
|
||||
}
|
||||
14
tgui/packages/tgui-say/styles/content.scss
Normal file
14
tgui/packages/tgui-say/styles/content.scss
Normal file
@@ -0,0 +1,14 @@
|
||||
.center {
|
||||
display: flex;
|
||||
flex: 1 1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input {
|
||||
display: flex;
|
||||
flex: 1 1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
font-family: 'Consolas', monospace;
|
||||
}
|
||||
39
tgui/packages/tgui-say/styles/dragzone.scss
Normal file
39
tgui/packages/tgui-say/styles/dragzone.scss
Normal file
@@ -0,0 +1,39 @@
|
||||
@use 'sass:color';
|
||||
@use './colors.scss';
|
||||
|
||||
$dragSize: 0.6rem;
|
||||
$borderSize: 0.2rem;
|
||||
|
||||
.dragzone-horizontal {
|
||||
border-left: $borderSize solid;
|
||||
border-right: $borderSize solid;
|
||||
color: transparent;
|
||||
width: 100%;
|
||||
height: $dragSize;
|
||||
}
|
||||
.dragzone-left {
|
||||
border-left: $borderSize solid;
|
||||
}
|
||||
|
||||
.dragzone-right {
|
||||
border-right: $borderSize solid;
|
||||
}
|
||||
|
||||
.dragzone-vertical {
|
||||
color: transparent;
|
||||
height: 100%;
|
||||
width: $dragSize;
|
||||
}
|
||||
|
||||
.dragzone-top {
|
||||
border-top: $borderSize solid;
|
||||
}
|
||||
|
||||
.dragzone-bottom {
|
||||
border-bottom: $borderSize solid;
|
||||
}
|
||||
|
||||
/** Lightmode static theme */
|
||||
.dragzone-lightMode {
|
||||
border-color: colors.$lightBorder;
|
||||
}
|
||||
70
tgui/packages/tgui-say/styles/main.scss
Normal file
70
tgui/packages/tgui-say/styles/main.scss
Normal file
@@ -0,0 +1,70 @@
|
||||
@use 'sass:meta';
|
||||
@use 'sass:color';
|
||||
@use './colors.scss';
|
||||
|
||||
// Core styles
|
||||
@include meta.load-css('~tgui/styles/reset.scss');
|
||||
// Atomic styles
|
||||
@include meta.load-css('~tgui/styles/atomic/text.scss');
|
||||
// External styles
|
||||
@include meta.load-css('~tgui/styles/components/TextArea.scss');
|
||||
// Local styles
|
||||
@include meta.load-css('./button.scss');
|
||||
@include meta.load-css('./content.scss');
|
||||
@include meta.load-css('./dragzone.scss');
|
||||
@include meta.load-css('./textarea.scss');
|
||||
@include meta.load-css('./window.scss');
|
||||
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@each $channel, $color in colors.$channel-map {
|
||||
$darkened: darken($color, 20%);
|
||||
|
||||
.button-#{$channel} {
|
||||
border-color: darken($color, 10%);
|
||||
color: $color;
|
||||
&:hover {
|
||||
border-color: lighten($color, 10%);
|
||||
color: lighten($color, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
.dragzone-#{$channel} {
|
||||
border-color: $darkened;
|
||||
}
|
||||
|
||||
.textarea-#{$channel} {
|
||||
color: $color;
|
||||
}
|
||||
|
||||
.window-#{$channel} {
|
||||
&:after {
|
||||
animation: gradient 10s linear infinite;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
darken($color, 35%),
|
||||
$color,
|
||||
lighten($color, 10%),
|
||||
$color,
|
||||
darken($color, 35%)
|
||||
);
|
||||
background-position: 0% 0%;
|
||||
background-size: 500% auto;
|
||||
bottom: 0px;
|
||||
content: '';
|
||||
height: 2px;
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
z-index: 999;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
tgui/packages/tgui-say/styles/textarea.scss
Normal file
11
tgui/packages/tgui-say/styles/textarea.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
.textarea {
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
display: flex;
|
||||
flex-grow: 5;
|
||||
font-family: inherit;
|
||||
font-size: 1.1rem;
|
||||
overflow: hidden;
|
||||
margin: 0 0 0 0.4rem;
|
||||
}
|
||||
29
tgui/packages/tgui-say/styles/window.scss
Normal file
29
tgui/packages/tgui-say/styles/window.scss
Normal file
@@ -0,0 +1,29 @@
|
||||
@use 'sass:color';
|
||||
@use './colors.scss';
|
||||
|
||||
.window {
|
||||
background-color: colors.$background;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 360px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.window-lightMode {
|
||||
background-color: colors.$lightMode;
|
||||
}
|
||||
|
||||
/** Window sizes */
|
||||
.window-30 {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.window-50 {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.window-70 {
|
||||
height: 70px;
|
||||
}
|
||||
23
tgui/packages/tgui-say/timers.ts
Normal file
23
tgui/packages/tgui-say/timers.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { debounce, throttle } from 'common/timer';
|
||||
|
||||
const SECONDS = 1000;
|
||||
|
||||
/** Timers: Prevents overloading the server, throttles messages */
|
||||
export const byondMessages = {
|
||||
// Debounce: Prevents spamming the server
|
||||
channelIncrementMsg: debounce(
|
||||
(visible: boolean, channel: string) =>
|
||||
Byond.sendMessage('thinking', { visible, channel }),
|
||||
0.4 * SECONDS,
|
||||
),
|
||||
forceSayMsg: debounce(
|
||||
(entry: string) => Byond.sendMessage('force', { entry, channel: 'Say' }),
|
||||
1 * SECONDS,
|
||||
true,
|
||||
),
|
||||
// Throttle: Prevents spamming the server
|
||||
typingMsg: throttle(
|
||||
(channel: string) => Byond.sendMessage('typing', { channel }),
|
||||
4 * SECONDS,
|
||||
),
|
||||
} as const;
|
||||
@@ -22,7 +22,7 @@ export const sanitizeMultiline = (toSanitize: string) => {
|
||||
};
|
||||
|
||||
export const removeAllSkiplines = (toSanitize: string) => {
|
||||
return toSanitize.replace(/[\r\n]+/, '');
|
||||
return toSanitize.replace(/[\r\n]+/, ' ');
|
||||
};
|
||||
|
||||
export const TextInputModal = (props) => {
|
||||
|
||||
@@ -33,6 +33,7 @@ module.exports = (env = {}, argv) => {
|
||||
entry: {
|
||||
tgui: ['./packages/tgui-polyfill', './packages/tgui'],
|
||||
'tgui-panel': ['./packages/tgui-polyfill', './packages/tgui-panel'],
|
||||
'tgui-say': ['./packages/tgui-polyfill', './packages/tgui-say'],
|
||||
},
|
||||
output: {
|
||||
path: argv.useTmpFolder
|
||||
|
||||
@@ -1453,6 +1453,25 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react-dom@npm:^18.2.24":
|
||||
version: 18.3.0
|
||||
resolution: "@types/react-dom@npm:18.3.0"
|
||||
dependencies:
|
||||
"@types/react": "npm:*"
|
||||
checksum: 10c0/6c90d2ed72c5a0e440d2c75d99287e4b5df3e7b011838cdc03ae5cd518ab52164d86990e73246b9d812eaf02ec351d74e3b4f5bd325bf341e13bf980392fd53b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react@npm:*":
|
||||
version: 18.3.3
|
||||
resolution: "@types/react@npm:18.3.3"
|
||||
dependencies:
|
||||
"@types/prop-types": "npm:*"
|
||||
csstype: "npm:^3.0.2"
|
||||
checksum: 10c0/fe455f805c5da13b89964c3d68060cebd43e73ec15001a68b34634604a78140e6fc202f3f61679b9d809dde6d7a7c2cb3ed51e0fd1462557911db09879b55114
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react@npm:^18.2.74":
|
||||
version: 18.3.2
|
||||
resolution: "@types/react@npm:18.3.2"
|
||||
@@ -8276,6 +8295,20 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"tgui-say@workspace:packages/tgui-say":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "tgui-say@workspace:packages/tgui-say"
|
||||
dependencies:
|
||||
"@types/react": "npm:^18.2.74"
|
||||
"@types/react-dom": "npm:^18.2.24"
|
||||
common: "workspace:*"
|
||||
react: "npm:^18.2.0"
|
||||
react-dom: "npm:^18.2.0"
|
||||
tgui: "workspace:*"
|
||||
tgui-polyfill: "workspace:*"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"tgui-workspace@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "tgui-workspace@workspace:."
|
||||
|
||||
@@ -138,6 +138,7 @@
|
||||
#include "code\__defines\species_languages.dm"
|
||||
#include "code\__defines\species_languages_vr.dm"
|
||||
#include "code\__defines\species_languages_YW.dm"
|
||||
#include "code\__defines\speech_channels.dm"
|
||||
#include "code\__defines\spells.dm"
|
||||
#include "code\__defines\sprite_sheets.dm"
|
||||
#include "code\__defines\sprite_sheets_ch.dm"
|
||||
@@ -174,6 +175,8 @@
|
||||
#include "code\__defines\dcs\signals_ch\signals_subsystem.dm"
|
||||
#include "code\__defines\dcs\signals_ch\signals_mob\signals_mob_main_ch.dm"
|
||||
#include "code\__defines\traits\_traits.dm"
|
||||
#include "code\__defines\traits\declarations.dm"
|
||||
#include "code\__defines\traits\sources.dm"
|
||||
#include "code\_global_vars\_regexes.dm"
|
||||
#include "code\_global_vars\bitfields.dm"
|
||||
#include "code\_global_vars\configuration_ch.dm"
|
||||
@@ -186,6 +189,7 @@
|
||||
#include "code\_global_vars\lists\mapping.dm"
|
||||
#include "code\_global_vars\lists\misc.dm"
|
||||
#include "code\_global_vars\lists\species.dm"
|
||||
#include "code\_global_vars\traits\_traits.dm"
|
||||
#include "code\_helpers\_global_objects.dm"
|
||||
#include "code\_helpers\_global_objects_vr.dm"
|
||||
#include "code\_helpers\_lists.dm"
|
||||
@@ -216,6 +220,7 @@
|
||||
#include "code\_helpers\string_lists.dm"
|
||||
#include "code\_helpers\text.dm"
|
||||
#include "code\_helpers\time.dm"
|
||||
#include "code\_helpers\traits.dm"
|
||||
#include "code\_helpers\turfs.dm"
|
||||
#include "code\_helpers\type2type.dm"
|
||||
#include "code\_helpers\typelists.dm"
|
||||
@@ -2041,6 +2046,7 @@
|
||||
#include "code\modules\asset_cache\asset_list_items.dm"
|
||||
#include "code\modules\asset_cache\assets\chat.dm"
|
||||
#include "code\modules\asset_cache\assets\fontawesome.dm"
|
||||
#include "code\modules\asset_cache\assets\icon_ref_map.dm"
|
||||
#include "code\modules\asset_cache\assets\jquery.dm"
|
||||
#include "code\modules\asset_cache\assets\tgfont.dm"
|
||||
#include "code\modules\asset_cache\assets\tgui.dm"
|
||||
@@ -2198,6 +2204,7 @@
|
||||
#include "code\modules\client\verbs\ignore.dm"
|
||||
#include "code\modules\client\verbs\ooc.dm"
|
||||
#include "code\modules\client\verbs\suicide.dm"
|
||||
#include "code\modules\client\verbs\typing.dm"
|
||||
#include "code\modules\client\verbs\who.dm"
|
||||
#include "code\modules\clothing\chameleon.dm"
|
||||
#include "code\modules\clothing\clothing.dm"
|
||||
@@ -4432,6 +4439,9 @@
|
||||
#include "code\modules\tgui_input\list.dm"
|
||||
#include "code\modules\tgui_input\number.dm"
|
||||
#include "code\modules\tgui_input\text.dm"
|
||||
#include "code\modules\tgui_input\say_modal\modal.dm"
|
||||
#include "code\modules\tgui_input\say_modal\speech.dm"
|
||||
#include "code\modules\tgui_input\say_modal\typing.dm"
|
||||
#include "code\modules\tgui_panel\audio.dm"
|
||||
#include "code\modules\tgui_panel\external.dm"
|
||||
#include "code\modules\tgui_panel\telemetry.dm"
|
||||
|
||||
Reference in New Issue
Block a user