TGUI Say: Upgrades chat input with modern features (#67116)

Co-authored-by: Mothblocks <35135081+Mothblocks@users.noreply.github.com>
Co-authored-by: AnturK <AnturK@users.noreply.github.com>
Co-authored-by: iamgoofball <iamgoofball@gmail.com>
Co-authored-by: Aleksej Komarov <stylemistake@gmail.com>
Co-authored-by: KubeRoot <6917698+KubeRoot@users.noreply.github.com>
Co-authored-by: Kapu1178 <75460809+Kapu1178@users.noreply.github.com>
Co-authored-by: Iamgoofball <4081722+Iamgoofball@users.noreply.github.com>
Co-authored-by: DomitiusKnack <56321744+DomitiusKnack@users.noreply.github.com>
Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com>
Co-authored-by: Wallem <66052067+Wallemations@users.noreply.github.com>
This commit is contained in:
Jeremiah
2022-06-16 17:21:21 -07:00
committed by GitHub
co-authored by Mothblocks AnturK iamgoofball Aleksej Komarov KubeRoot Kapu1178 Iamgoofball DomitiusKnack LemonInTheDark Wallem
parent 5b77b9e44e
commit acfa5e4fdd
68 changed files with 1990 additions and 622 deletions
+3 -1
View File
@@ -29,9 +29,11 @@
#define COMSIG_KB_CLIENT_MINIMALHUD_DOWN "keybinding_client_minimalhud_down"
//Communication
#define COMSIG_KB_CLIENT_OOC_DOWN "keybinding_client_ooc_down"
#define COMSIG_KB_CLIENT_SAY_DOWN "keybinding_client_say_down"
#define COMSIG_KB_CLIENT_RADIO_DOWN "keybinding_client_radio_down"
#define COMSIG_KB_CLIENT_ME_DOWN "keybinding_client_me_down"
#define COMSIG_KB_CLIENT_OOC_DOWN "keybinding_client_ooc_down"
//Human
#define COMSIG_KB_HUMAN_QUICKEQUIP_DOWN "keybinding_human_quickequip_down"
+2
View File
@@ -186,6 +186,8 @@
#define RUNECHAT_PLANE 501
/// Plane for balloon text (text that fades up)
#define BALLOON_CHAT_PLANE 502
/// Bubble for typing indicators
#define TYPING_LAYER 500
//-------------------- HUD ---------------------
//HUD layer defines
+3 -2
View File
@@ -38,15 +38,16 @@
#define LOG_ECON (1 << 18)
#define LOG_VICTIM (1 << 19)
#define LOG_RADIO_EMOTE (1 << 20)
#define LOG_SPEECH_INDICATORS (1 << 21)
//Individual logging panel pages
#define INDIVIDUAL_ATTACK_LOG (LOG_ATTACK | LOG_VICTIM)
#define INDIVIDUAL_SAY_LOG (LOG_SAY | LOG_WHISPER | LOG_DSAY)
#define INDIVIDUAL_SAY_LOG (LOG_SAY | LOG_WHISPER | LOG_DSAY | LOG_SPEECH_INDICATORS)
#define INDIVIDUAL_EMOTE_LOG (LOG_EMOTE | LOG_RADIO_EMOTE)
#define INDIVIDUAL_COMMS_LOG (LOG_PDA | LOG_CHAT | LOG_COMMENT | LOG_TELECOMMS)
#define INDIVIDUAL_OOC_LOG (LOG_OOC | LOG_ADMIN)
#define INDIVIDUAL_OWNERSHIP_LOG (LOG_OWNERSHIP)
#define INDIVIDUAL_SHOW_ALL_LOG (LOG_ATTACK | LOG_SAY | LOG_WHISPER | LOG_EMOTE | LOG_RADIO_EMOTE | LOG_DSAY | LOG_PDA | LOG_CHAT | LOG_COMMENT | LOG_TELECOMMS | LOG_OOC | LOG_ADMIN | LOG_OWNERSHIP | LOG_GAME | LOG_ADMIN_PRIVATE | LOG_ASAY | LOG_MECHA | LOG_VIRUS | LOG_SHUTTLE | LOG_ECON | LOG_VICTIM)
#define INDIVIDUAL_SHOW_ALL_LOG (LOG_ATTACK | LOG_SAY | LOG_WHISPER | LOG_EMOTE | LOG_RADIO_EMOTE | LOG_DSAY | LOG_PDA | LOG_CHAT | LOG_COMMENT | LOG_TELECOMMS | LOG_OOC | LOG_ADMIN | LOG_OWNERSHIP | LOG_GAME | LOG_ADMIN_PRIVATE | LOG_ASAY | LOG_MECHA | LOG_VIRUS | LOG_SHUTTLE | LOG_ECON | LOG_VICTIM | LOG_SPEECH_INDICATORS)
#define LOGSRC_CKEY "Ckey"
#define LOGSRC_MOB "Mob"
+5
View File
@@ -0,0 +1,5 @@
// Used to direct channels to speak into.
#define SAY_CHANNEL "Say"
#define RADIO_CHANNEL "Radio"
#define ME_CHANNEL "Me"
#define OOC_CHANNEL "OOC"
+2
View File
@@ -114,6 +114,8 @@ GLOBAL_LIST_INIT(testing_global_profiler, list("_PROFILE_NAME" = "Global"))
log_mecha(log_text)
if(LOG_SHUTTLE)
log_shuttle(log_text)
if(LOG_SPEECH_INDICATORS)
log_speech_indicators(log_text)
else
stack_trace("Invalid individual logging type: [message_type]. Defaulting to [LOG_GAME] (LOG_GAME).")
log_game(log_text)
+5
View File
@@ -40,3 +40,8 @@
/proc/log_telecomms(text)
if (CONFIG_GET(flag/log_telecomms))
WRITE_LOG(GLOB.world_telecomms_log, "TCOMMS: [text]")
/// Logging for speech indicators.
/proc/log_speech_indicators(text)
if (CONFIG_GET(flag/log_speech_indicators))
WRITE_LOG(GLOB.world_speech_indicators_log, "SPEECH INDICATOR: [text]")
+2
View File
@@ -33,6 +33,8 @@ GLOBAL_VAR(world_uplink_log)
GLOBAL_PROTECT(world_uplink_log)
GLOBAL_VAR(world_telecomms_log)
GLOBAL_PROTECT(world_telecomms_log)
GLOBAL_VAR(world_speech_indicators_log)
GLOBAL_PROTECT(world_speech_indicators_log)
GLOBAL_VAR(world_manifest_log)
GLOBAL_PROTECT(world_manifest_log)
GLOBAL_VAR(query_debug_log)
@@ -131,6 +131,9 @@
/// log telecomms messages
/datum/config_entry/flag/log_telecomms
/// log speech indicators(started/stopped speaking)
/datum/config_entry/flag/log_speech_indicators
/// log certain expliotable parrots and other such fun things in a JSON file of twitter valid phrases.
/datum/config_entry/flag/log_twitter
+9 -3
View File
@@ -3,18 +3,24 @@
/datum/keybinding/client/communication/say
hotkey_keys = list("T")
name = "Say"
name = SAY_CHANNEL
full_name = "IC Say"
keybind_signal = COMSIG_KB_CLIENT_SAY_DOWN
/datum/keybinding/client/communication/radio
hotkey_keys = list("Y")
name = RADIO_CHANNEL
full_name = "IC Radio (;)"
keybind_signal = COMSIG_KB_CLIENT_RADIO_DOWN
/datum/keybinding/client/communication/ooc
hotkey_keys = list("O")
name = "OOC"
name = OOC_CHANNEL
full_name = "Out Of Character Say (OOC)"
keybind_signal = COMSIG_KB_CLIENT_OOC_DOWN
/datum/keybinding/client/communication/me
hotkey_keys = list("M")
name = "Me"
name = ME_CHANNEL
full_name = "Custom Emote (/Me)"
keybind_signal = COMSIG_KB_CLIENT_ME_DOWN
+1
View File
@@ -142,6 +142,7 @@ GLOBAL_VAR(restart_counter)
GLOB.world_pda_log = "[GLOB.log_directory]/pda.log"
GLOB.world_uplink_log = "[GLOB.log_directory]/uplink.log"
GLOB.world_telecomms_log = "[GLOB.log_directory]/telecomms.log"
GLOB.world_speech_indicators_log = "[GLOB.log_directory]/speech_indicators.log"
GLOB.world_manifest_log = "[GLOB.log_directory]/manifest.log"
GLOB.world_href_log = "[GLOB.log_directory]/hrefs.log"
GLOB.world_mob_tag_log = "[GLOB.log_directory]/mob_tags.log"
+2 -3
View File
@@ -99,8 +99,6 @@
custom_name = FALSE
command_name = params["updated_name"]
if("update_report_contents")
command_report_content = params["updated_contents"]
if("set_report_sound")
played_sound = params["picked_sound"]
if("toggle_announce")
@@ -109,9 +107,10 @@
if(!command_name)
to_chat(ui_user, span_danger("You can't send a report with no command name."))
return
if(!command_report_content)
if(!params["report"])
to_chat(ui_user, span_danger("You can't send a report with no contents."))
return
command_report_content = params["report"]
send_announcement()
return TRUE
+3
View File
@@ -251,3 +251,6 @@
/// If this client has been fully initialized or not
var/fully_created = FALSE
/// Does this client have typing indicators enabled?
var/typing_indicators = FALSE
+16 -6
View File
@@ -219,6 +219,8 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
// Instantiate tgui panel
tgui_panel = new(src, "browseroutput")
tgui_say = new(src, "tgui_say")
set_right_click_menu_mode(TRUE)
GLOB.ahelp_tickets.ClientLogin(src)
@@ -353,6 +355,8 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
// Initialize tgui panel
tgui_panel.initialize()
tgui_say.initialize()
if(alert_mob_dupe_login && !holder)
var/dupe_login_message = "Your ComputerID has already logged in with another key this round, please log out of this one NOW or risk being banned!"
if (alert_admin_multikey)
@@ -1031,12 +1035,18 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
movement_keys[key] = WEST
if("South")
movement_keys[key] = SOUTH
if("Say")
winset(src, "default-[REF(key)]", "parent=default;name=[key];command=say")
if("OOC")
winset(src, "default-[REF(key)]", "parent=default;name=[key];command=ooc")
if("Me")
winset(src, "default-[REF(key)]", "parent=default;name=[key];command=me")
if(SAY_CHANNEL)
var/say = tgui_say_create_open_command(SAY_CHANNEL)
winset(src, "default-[REF(key)]", "parent=default;name=[key];command=[say]")
if(RADIO_CHANNEL)
var/radio = tgui_say_create_open_command(RADIO_CHANNEL)
winset(src, "default-[REF(key)]", "parent=default;name=[key];command=[radio]")
if(ME_CHANNEL)
var/me = tgui_say_create_open_command(ME_CHANNEL)
winset(src, "default-[REF(key)]", "parent=default;name=[key];command=[me]")
if(OOC_CHANNEL)
var/ooc = tgui_say_create_open_command(OOC_CHANNEL)
winset(src, "default-[REF(key)]", "parent=default;name=[key];command=[ooc]")
/client/proc/change_view(new_size)
if (isnull(new_size))
+10
View File
@@ -47,3 +47,13 @@
for (var/datum/tgui/tgui as anything in client.mob?.tgui_open_uis)
// Force it to reload either way
tgui.update_static_data(client.mob)
/// Light mode for tgui say
/datum/preference/toggle/tgui_say_light_mode
category = PREFERENCE_CATEGORY_GAME_PREFERENCES
savefile_key = "tgui_say_light_mode"
savefile_identifier = PREFERENCE_PLAYER
default_value = FALSE
/datum/preference/toggle/tgui_say_light_mode/apply_to_client(client/client)
client.tgui_say?.load()
@@ -84,4 +84,4 @@
/mob/living/carbon/human/get_alt_name()
if(name != GetVoice())
return " (as [get_id_name("Unknown")])"\
return " (as [get_id_name("Unknown")])"
+95 -84
View File
@@ -1120,10 +1120,14 @@ GLOBAL_LIST_EMPTY(features_by_species)
var/attack_direction = get_dir(user, target)
if(atk_effect == ATTACK_EFFECT_KICK)//kicks deal 1.5x raw damage
target.apply_damage(damage*1.5, user.dna.species.attack_type, affecting, armor_block, attack_direction = attack_direction)
if((damage * 1.5) >= 9)
target.force_say()
log_combat(user, target, "kicked")
else//other attacks deal full raw damage + 1.5x in stamina damage
target.apply_damage(damage, user.dna.species.attack_type, affecting, armor_block, attack_direction = attack_direction)
target.apply_damage(damage*1.5, STAMINA, affecting, armor_block)
if(damage >= 9)
target.force_say()
log_combat(user, target, "punched")
if((target.stat != DEAD) && damage >= user.dna.species.punchstunthreshold)
@@ -1157,126 +1161,133 @@ GLOBAL_LIST_EMPTY(features_by_species)
/datum/species/proc/spec_hitby(atom/movable/AM, mob/living/carbon/human/H)
return
/datum/species/proc/spec_attack_hand(mob/living/carbon/human/M, mob/living/carbon/human/H, datum/martial_art/attacker_style, modifiers)
if(!istype(M))
/datum/species/proc/spec_attack_hand(mob/living/carbon/human/owner, mob/living/carbon/human/target, datum/martial_art/attacker_style, modifiers)
if(!istype(owner))
return
CHECK_DNA_AND_SPECIES(M)
CHECK_DNA_AND_SPECIES(H)
CHECK_DNA_AND_SPECIES(owner)
CHECK_DNA_AND_SPECIES(target)
if(!istype(M)) //sanity check for drones.
if(!istype(owner)) //sanity check for drones.
return
if(M.mind)
attacker_style = M.mind.martial_art
if((M != H) && M.combat_mode && H.check_shields(M, 0, M.name, attack_type = UNARMED_ATTACK))
log_combat(M, H, "attempted to touch")
H.visible_message(span_warning("[M] attempts to touch [H]!"), \
span_danger("[M] attempts to touch you!"), span_hear("You hear a swoosh!"), COMBAT_MESSAGE_RANGE, M)
to_chat(M, span_warning("You attempt to touch [H]!"))
if(owner.mind)
attacker_style = owner.mind.martial_art
if((owner != target) && owner.combat_mode && target.check_shields(owner, 0, owner.name, attack_type = UNARMED_ATTACK))
log_combat(owner, target, "attempted to touch")
target.visible_message(span_warning("[owner] attempts to touch [target]!"), \
span_danger("[owner] attempts to touch you!"), span_hear("You hear a swoosh!"), COMBAT_MESSAGE_RANGE, owner)
to_chat(owner, span_warning("You attempt to touch [target]!"))
return
SEND_SIGNAL(M, COMSIG_MOB_ATTACK_HAND, M, H, attacker_style)
SEND_SIGNAL(owner, COMSIG_MOB_ATTACK_HAND, owner, target, attacker_style)
if(LAZYACCESS(modifiers, RIGHT_CLICK))
disarm(M, H, attacker_style)
disarm(owner, target, attacker_style)
return // dont attack after
if(M.combat_mode)
harm(M, H, attacker_style)
if(owner.combat_mode)
harm(owner, target, attacker_style)
else
help(M, H, attacker_style)
help(owner, target, attacker_style)
/datum/species/proc/spec_attacked_by(obj/item/I, mob/living/user, obj/item/bodypart/affecting, mob/living/carbon/human/H)
/datum/species/proc/spec_attacked_by(obj/item/weapon, mob/living/user, obj/item/bodypart/affecting, mob/living/carbon/human/human)
// Allows you to put in item-specific reactions based on species
if(user != H)
if(H.check_shields(I, I.force, "the [I.name]", MELEE_ATTACK, I.armour_penetration))
if(user != human)
if(human.check_shields(weapon, weapon.force, "the [weapon.name]", MELEE_ATTACK, weapon.armour_penetration))
return FALSE
if(H.check_block())
H.visible_message(span_warning("[H] blocks [I]!"), \
span_userdanger("You block [I]!"))
if(human.check_block())
human.visible_message(span_warning("[human] blocks [weapon]!"), \
span_userdanger("You block [weapon]!"))
return FALSE
var/hit_area
if(!affecting) //Something went wrong. Maybe the limb is missing?
affecting = H.bodyparts[1]
affecting = human.bodyparts[1]
hit_area = affecting.plaintext_zone
var/def_zone = affecting.body_zone
var/armor_block = H.run_armor_check(affecting, MELEE, span_notice("Your armor has protected your [hit_area]!"), span_warning("Your armor has softened a hit to your [hit_area]!"),I.armour_penetration, weak_against_armour = I.weak_against_armour)
var/armor_block = human.run_armor_check(affecting, MELEE, span_notice("Your armor has protected your [hit_area]!"), span_warning("Your armor has softened a hit to your [hit_area]!"),weapon.armour_penetration, weak_against_armour = weapon.weak_against_armour)
armor_block = min(ARMOR_MAX_BLOCK, armor_block) //cap damage reduction at 90%
var/Iwound_bonus = I.wound_bonus
var/Iwound_bonus = weapon.wound_bonus
// this way, you can't wound with a surgical tool on help intent if they have a surgery active and are lying down, so a misclick with a circular saw on the wrong limb doesn't bleed them dry (they still get hit tho)
if((I.item_flags & SURGICAL_TOOL) && !user.combat_mode && H.body_position == LYING_DOWN && (LAZYLEN(H.surgeries) > 0))
if((weapon.item_flags & SURGICAL_TOOL) && !user.combat_mode && human.body_position == LYING_DOWN && (LAZYLEN(human.surgeries) > 0))
Iwound_bonus = CANT_WOUND
var/weakness = check_species_weakness(I, user)
var/weakness = check_species_weakness(weapon, user)
H.send_item_attack_message(I, user, hit_area, affecting)
human.send_item_attack_message(weapon, user, hit_area, affecting)
var/attack_direction = get_dir(user, H)
apply_damage(I.force * weakness, I.damtype, def_zone, armor_block, H, wound_bonus = Iwound_bonus, bare_wound_bonus = I.bare_wound_bonus, sharpness = I.get_sharpness(), attack_direction = attack_direction)
var/attack_direction = get_dir(user, human)
apply_damage(weapon.force * weakness, weapon.damtype, def_zone, armor_block, human, wound_bonus = Iwound_bonus, bare_wound_bonus = weapon.bare_wound_bonus, sharpness = weapon.get_sharpness(), attack_direction = attack_direction)
if(!I.force)
if(!weapon.force)
return FALSE //item force is zero
var/bloody = FALSE
if(((I.damtype == BRUTE) && I.force && prob(25 + (I.force * 2))))
if(IS_ORGANIC_LIMB(affecting))
I.add_mob_blood(H) //Make the weapon bloody, not the person.
if(prob(I.force * 2)) //blood spatter!
bloody = TRUE
var/turf/location = H.loc
if(istype(location))
H.add_splatter_floor(location)
if(get_dist(user, H) <= 1) //people with TK won't get smeared with blood
user.add_mob_blood(H)
if(weapon.damtype != BRUTE)
return TRUE
if(!(prob(25 + (weapon.force * 2))))
return TRUE
switch(hit_area)
if(BODY_ZONE_HEAD)
if(!I.get_sharpness() && armor_block < 50)
if(prob(I.force))
H.adjustOrganLoss(ORGAN_SLOT_BRAIN, 20)
if(H.stat == CONSCIOUS)
H.visible_message(span_danger("[H] is knocked senseless!"), \
span_userdanger("You're knocked senseless!"))
H.set_timed_status_effect(20 SECONDS, /datum/status_effect/confusion, only_if_higher = TRUE)
H.adjust_blurriness(10)
if(prob(10))
H.gain_trauma(/datum/brain_trauma/mild/concussion)
else
H.adjustOrganLoss(ORGAN_SLOT_BRAIN, I.force * 0.2)
if(IS_ORGANIC_LIMB(affecting))
weapon.add_mob_blood(human) //Make the weapon bloody, not the person.
if(prob(weapon.force * 2)) //blood spatter!
bloody = TRUE
var/turf/location = human.loc
if(istype(location))
human.add_splatter_floor(location)
if(get_dist(user, human) <= 1) //people with TK won't get smeared with blood
user.add_mob_blood(human)
if(H.mind && H.stat == CONSCIOUS && H != user && prob(I.force + ((100 - H.health) * 0.5))) // rev deconversion through blunt trauma.
var/datum/antagonist/rev/rev = H.mind.has_antag_datum(/datum/antagonist/rev)
if(rev)
rev.remove_revolutionary(FALSE, user)
switch(hit_area)
if(BODY_ZONE_HEAD)
if(!weapon.get_sharpness() && armor_block < 50)
if(prob(weapon.force))
human.adjustOrganLoss(ORGAN_SLOT_BRAIN, 20)
if(human.stat == CONSCIOUS)
human.visible_message(span_danger("[human] is knocked senseless!"), \
span_userdanger("You're knocked senseless!"))
human.set_timed_status_effect(20 SECONDS, /datum/status_effect/confusion, only_if_higher = TRUE)
human.adjust_blurriness(10)
if(prob(10))
human.gain_trauma(/datum/brain_trauma/mild/concussion)
else
human.adjustOrganLoss(ORGAN_SLOT_BRAIN, weapon.force * 0.2)
if(bloody) //Apply blood
if(H.wear_mask)
H.wear_mask.add_mob_blood(H)
H.update_inv_wear_mask()
if(H.head)
H.head.add_mob_blood(H)
H.update_inv_head()
if(H.glasses && prob(33))
H.glasses.add_mob_blood(H)
H.update_inv_glasses()
if(human.mind && human.stat == CONSCIOUS && human != user && prob(weapon.force + ((100 - human.health) * 0.5))) // rev deconversion through blunt trauma.
var/datum/antagonist/rev/rev = human.mind.has_antag_datum(/datum/antagonist/rev)
if(rev)
rev.remove_revolutionary(FALSE, user)
if(BODY_ZONE_CHEST)
if(H.stat == CONSCIOUS && !I.get_sharpness() && armor_block < 50)
if(prob(I.force))
H.visible_message(span_danger("[H] is knocked down!"), \
span_userdanger("You're knocked down!"))
H.apply_effect(60, EFFECT_KNOCKDOWN, armor_block)
if(bloody) //Apply blood
if(human.wear_mask)
human.wear_mask.add_mob_blood(human)
human.update_inv_wear_mask()
if(human.head)
human.head.add_mob_blood(human)
human.update_inv_head()
if(human.glasses && prob(33))
human.glasses.add_mob_blood(human)
human.update_inv_glasses()
if(bloody)
if(H.wear_suit)
H.wear_suit.add_mob_blood(H)
H.update_inv_wear_suit()
if(H.w_uniform)
H.w_uniform.add_mob_blood(H)
H.update_inv_w_uniform()
if(BODY_ZONE_CHEST)
if(human.stat == CONSCIOUS && !weapon.get_sharpness() && armor_block < 50)
if(prob(weapon.force))
human.visible_message(span_danger("[human] is knocked down!"), \
span_userdanger("You're knocked down!"))
human.apply_effect(60, EFFECT_KNOCKDOWN, armor_block)
if(bloody)
if(human.wear_suit)
human.wear_suit.add_mob_blood(human)
human.update_inv_wear_suit()
if(human.w_uniform)
human.w_uniform.add_mob_blood(human)
human.update_inv_w_uniform()
/// Triggers force say events
if(weapon.force > 10 || weapon.force >= 5 && prob(33))
human.force_say(user)
return TRUE
+7
View File
@@ -234,3 +234,10 @@
var/datum/client_interface/mock_client
var/interaction_range = 0 //how far a mob has to be to interact with something without caring about obsctruction, defaulted to 0 tiles
/// Typing indicator - mob is typing into a input
var/typing_indicator = FALSE
/// Thinking indicator - mob has input window open
var/thinking_indicator = FALSE
/// User is thinking in character. Used to revert to thinking state after stop_typing
var/thinking_IC = FALSE
-183
View File
@@ -1,183 +0,0 @@
/**
* Creates a TGUI alert window and returns the user's response.
*
* This proc should be used to create alerts that the caller will wait for a response from.
* Arguments:
* * user - The user to show the alert to.
* * message - The content of the alert, shown in the body of the TGUI window.
* * title - The of the alert modal, shown on the top of the TGUI window.
* * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
* * timeout - The timeout of the alert, after which the modal will close and qdel itself. Set to zero for no timeout.
* * autofocus - The bool that controls if this alert should grab window focus.
*/
/proc/tgui_alert(mob/user, message = "", title, list/buttons = list("Ok"), timeout = 0, autofocus = TRUE)
if (!user)
user = usr
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return
// Client does NOT have tgui_input on: Returns regular input
if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
if(length(buttons) == 2)
return alert(user, message, title, buttons[1], buttons[2])
if(length(buttons) == 3)
return alert(user, message, title, buttons[1], buttons[2], buttons[3])
var/datum/tgui_modal/alert = new(user, message, title, buttons, timeout, autofocus)
alert.ui_interact(user)
alert.wait()
if (alert)
. = alert.choice
qdel(alert)
/**
* Creates an asynchronous TGUI alert window with an associated callback.
*
* This proc should be used to create alerts that invoke a callback with the user's chosen option.
* Arguments:
* * user - The user to show the alert to.
* * message - The content of the alert, shown in the body of the TGUI window.
* * title - The of the alert modal, shown on the top of the TGUI window.
* * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
* * callback - The callback to be invoked when a choice is made.
* * timeout - The timeout of the alert, after which the modal will close and qdel itself. Disabled by default, can be set to seconds otherwise.
* * autofocus - The bool that controls if this alert should grab window focus.
*/
/proc/tgui_alert_async(mob/user, message = "", title, list/buttons = list("Ok"), datum/callback/callback, timeout = 0, autofocus = TRUE)
if (!user)
user = usr
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return
// Client does NOT have tgui_input on: Returns regular input
if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
if(length(buttons) == 2)
return alert(user, message, title, buttons[1], buttons[2])
if(length(buttons) == 3)
return alert(user, message, title, buttons[1], buttons[2], buttons[3])
var/datum/tgui_modal/async/alert = new(user, message, title, buttons, callback, timeout, autofocus)
alert.ui_interact(user)
/**
* # tgui_modal
*
* Datum used for instantiating and using a TGUI-controlled modal that prompts the user with
* a message and has buttons for responses.
*/
/datum/tgui_modal
/// The title of the TGUI window
var/title
/// The textual body of the TGUI window
var/message
/// The list of buttons (responses) provided on the TGUI window
var/list/buttons
/// The button that the user has pressed, null if no selection has been made
var/choice
/// The time at which the tgui_modal was created, for displaying timeout progress.
var/start_time
/// The lifespan of the tgui_modal, after which the window will close and delete itself.
var/timeout
/// The bool that controls if this modal should grab window focus
var/autofocus
/// Boolean field describing if the tgui_modal was closed by the user.
var/closed
/datum/tgui_modal/New(mob/user, message, title, list/buttons, timeout, autofocus)
src.autofocus = autofocus
src.buttons = buttons.Copy()
src.message = message
src.title = title
if (timeout)
src.timeout = timeout
start_time = world.time
QDEL_IN(src, timeout)
/datum/tgui_modal/Destroy(force, ...)
SStgui.close_uis(src)
QDEL_NULL(buttons)
. = ..()
/**
* Waits for a user's response to the tgui_modal's prompt before returning. Returns early if
* the window was closed by the user.
*/
/datum/tgui_modal/proc/wait()
while (!choice && !closed && !QDELETED(src))
stoplag(1)
/datum/tgui_modal/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "AlertModal")
ui.open()
/datum/tgui_modal/ui_close(mob/user)
. = ..()
closed = TRUE
/datum/tgui_modal/ui_state(mob/user)
return GLOB.always_state
/datum/tgui_modal/ui_static_data(mob/user)
. = list()
.["autofocus"] = autofocus
.["buttons"] = buttons
.["message"] = message
.["large_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large)
.["swapped_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped)
.["title"] = title
/datum/tgui_modal/ui_data(mob/user)
. = list()
if(timeout)
.["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
/datum/tgui_modal/ui_act(action, list/params)
. = ..()
if (.)
return
switch(action)
if("choose")
if (!(params["choice"] in buttons))
CRASH("[usr] entered a non-existent button choice: [params["choice"]]")
set_choice(params["choice"])
closed = TRUE
SStgui.close_uis(src)
return TRUE
if("cancel")
closed = TRUE
SStgui.close_uis(src)
return TRUE
/datum/tgui_modal/proc/set_choice(choice)
src.choice = choice
/**
* # async tgui_modal
*
* An asynchronous version of tgui_modal to be used with callbacks instead of waiting on user responses.
*/
/datum/tgui_modal/async
/// The callback to be invoked by the tgui_modal upon having a choice made.
var/datum/callback/callback
/datum/tgui_modal/async/New(mob/user, message, title, list/buttons, callback, timeout, autofocus)
..(user, message, title, buttons, timeout, autofocus)
src.callback = callback
/datum/tgui_modal/async/Destroy(force, ...)
QDEL_NULL(callback)
. = ..()
/datum/tgui_modal/async/set_choice(choice)
. = ..()
if(!isnull(src.choice))
callback?.InvokeAsync(src.choice)
/datum/tgui_modal/async/wait()
return
+133
View File
@@ -0,0 +1,133 @@
/**
* Creates a TGUI alert window and returns the user's response.
*
* This proc should be used to create alerts that the caller will wait for a response from.
* Arguments:
* * user - The user to show the alert to.
* * message - The content of the alert, shown in the body of the TGUI window.
* * title - The of the alert modal, shown on the top of the TGUI window.
* * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
* * timeout - The timeout of the alert, after which the modal will close and qdel itself. Set to zero for no timeout.
* * autofocus - The bool that controls if this alert should grab window focus.
*/
/proc/tgui_alert(mob/user, message = "", title, list/buttons = list("Ok"), timeout = 0, autofocus = TRUE)
if (!user)
user = usr
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return
// A gentle nudge - you should not be using TGUI alert for anything other than a simple message.
if(length(buttons) > 3)
log_tgui(user, "Error: TGUI Alert initiated with too many buttons. Use a list.", "TguiAlert")
return tgui_input_list(user, message, title, buttons, timeout, autofocus)
// Client does NOT have tgui_input on: Returns regular input
if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
if(length(buttons) == 2)
return alert(user, message, title, buttons[1], buttons[2])
if(length(buttons) == 3)
return alert(user, message, title, buttons[1], buttons[2], buttons[3])
var/datum/tgui_alert/alert = new(user, message, title, buttons, timeout, autofocus)
alert.ui_interact(user)
alert.wait()
if (alert)
. = alert.choice
qdel(alert)
/**
* # tgui_alert
*
* Datum used for instantiating and using a TGUI-controlled modal that prompts the user with
* a message and has buttons for responses.
*/
/datum/tgui_alert
/// The title of the TGUI window
var/title
/// The textual body of the TGUI window
var/message
/// The list of buttons (responses) provided on the TGUI window
var/list/buttons
/// The button that the user has pressed, null if no selection has been made
var/choice
/// The time at which the tgui_alert was created, for displaying timeout progress.
var/start_time
/// The lifespan of the tgui_alert, after which the window will close and delete itself.
var/timeout
/// The bool that controls if this modal should grab window focus
var/autofocus
/// Boolean field describing if the tgui_alert was closed by the user.
var/closed
/datum/tgui_alert/New(mob/user, message, title, list/buttons, timeout, autofocus)
src.autofocus = autofocus
src.buttons = buttons.Copy()
src.message = message
src.title = title
if (timeout)
src.timeout = timeout
start_time = world.time
QDEL_IN(src, timeout)
/datum/tgui_alert/Destroy(force, ...)
SStgui.close_uis(src)
QDEL_NULL(buttons)
return ..()
/**
* Waits for a user's response to the tgui_alert's prompt before returning. Returns early if
* the window was closed by the user.
*/
/datum/tgui_alert/proc/wait()
while (!choice && !closed && !QDELETED(src))
stoplag(1)
/datum/tgui_alert/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "AlertModal")
ui.open()
/datum/tgui_alert/ui_close(mob/user)
. = ..()
closed = TRUE
/datum/tgui_alert/ui_state(mob/user)
return GLOB.always_state
/datum/tgui_alert/ui_static_data(mob/user)
var/list/data = list()
data["autofocus"] = autofocus
data["buttons"] = buttons
data["message"] = message
data["large_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large)
data["swapped_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped)
data["title"] = title
return data
/datum/tgui_alert/ui_data(mob/user)
var/list/data = list()
if(timeout)
data["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
return data
/datum/tgui_alert/ui_act(action, list/params)
. = ..()
if (.)
return
switch(action)
if("choose")
if (!(params["choice"] in buttons))
CRASH("[usr] entered a non-existent button choice: [params["choice"]]")
set_choice(params["choice"])
closed = TRUE
SStgui.close_uis(src)
return TRUE
if("cancel")
closed = TRUE
SStgui.close_uis(src)
return TRUE
/datum/tgui_alert/proc/set_choice(choice)
src.choice = choice
@@ -31,36 +31,6 @@
. = input.choice
qdel(input)
/**
* Creates an asynchronous TGUI input list window with an associated callback.
*
* This proc should be used to create inputs that invoke a callback with the user's chosen option.
* Arguments:
* * user - The user to show the input box to.
* * message - The content of the input box, shown in the body of the TGUI window.
* * title - The title of the input box, shown on the top of the TGUI window.
* * items - The options that can be chosen by the user, each string is assigned a button on the UI.
* * default - If an option is already preselected on the UI. Current values, etc.
* * callback - The callback to be invoked when a choice is made.
* * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
*/
/proc/tgui_input_list_async(mob/user, message, title = "Select", list/items, default, datum/callback/callback, timeout = 60 SECONDS)
if (!user)
user = usr
if(!length(items))
return
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return
/// Client does NOT have tgui_input on: Returns regular input
if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
return input(user, message, title) as null|anything in items
var/datum/tgui_list_input/async/input = new(user, message, title, items, default, callback, timeout)
input.ui_interact(user)
/**
* # tgui_list_input
*
@@ -94,22 +64,16 @@
src.items_map = list()
src.default = default
var/list/repeat_items = list()
// Gets rid of illegal characters
var/static/regex/whitelistedWords = regex(@{"([^\u0020-\u8000]+)"})
for(var/i in items)
if(!i)
continue
var/string_key = whitelistedWords.Replace("[i]", "")
//avoids duplicated keys E.g: when areas have the same name
string_key = avoid_assoc_duplicate_keys(string_key, repeat_items)
src.items += string_key
src.items_map[string_key] = i
if (timeout)
src.timeout = timeout
start_time = world.time
@@ -118,7 +82,7 @@
/datum/tgui_list_input/Destroy(force, ...)
SStgui.close_uis(src)
QDEL_NULL(items)
. = ..()
return ..()
/**
* Waits for a user's response to the tgui_list_input's prompt before returning. Returns early if
@@ -142,18 +106,20 @@
return GLOB.always_state
/datum/tgui_list_input/ui_static_data(mob/user)
. = list()
.["init_value"] = default || items[1]
.["items"] = items
.["large_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large)
.["message"] = message
.["swapped_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped)
.["title"] = title
var/list/data = list()
data["init_value"] = default || items[1]
data["items"] = items
data["large_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large)
data["message"] = message
data["swapped_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped)
data["title"] = title
return data
/datum/tgui_list_input/ui_data(mob/user)
. = list()
var/list/data = list()
if(timeout)
.["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1)
data["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1)
return data
/datum/tgui_list_input/ui_act(action, list/params)
. = ..()
@@ -174,28 +140,3 @@
/datum/tgui_list_input/proc/set_choice(choice)
src.choice = choice
/**
* # async tgui_list_input
*
* An asynchronous version of tgui_list_input to be used with callbacks instead of waiting on user responses.
*/
/datum/tgui_list_input/async
/// The callback to be invoked by the tgui_list_input upon having a choice made.
var/datum/callback/callback
/datum/tgui_list_input/async/New(mob/user, message, title, list/items, default, callback, timeout)
..(user, message, title, items, default, timeout)
src.callback = callback
/datum/tgui_list_input/async/Destroy(force, ...)
QDEL_NULL(callback)
. = ..()
/datum/tgui_list_input/async/set_choice(choice)
. = ..()
if(!isnull(src.choice))
callback?.InvokeAsync(src.choice)
/datum/tgui_list_input/async/wait()
return
@@ -35,38 +35,6 @@
. = number_input.entry
qdel(number_input)
/**
* Creates an asynchronous TGUI number input window with an associated callback.
*
* This proc should be used to create number inputs that invoke a callback with the user's entry.
*
* Arguments:
* * user - The user to show the number input to.
* * message - The content of the number input, shown in the body of the TGUI window.
* * title - The title of the number input modal, shown on the top of the TGUI window.
* * default - The default (or current) value, shown as a placeholder. Users can press refresh with this.
* * max_value - Specifies a maximum value. If none is set, any number can be entered. Pressing "max" defaults to 1000.
* * min_value - Specifies a minimum value. Often 0.
* * callback - The callback to be invoked when a choice is made.
* * timeout - The timeout of the number input, after which the modal will close and qdel itself. Set to zero for no timeout.
* * round_value - whether the inputted number is rounded down into an integer.
*/
/proc/tgui_input_number_async(mob/user, message, title = "Number Input", default = 0, max_value = 10000, min_value = 0, datum/callback/callback, timeout = 60 SECONDS, round_value = TRUE)
if (!user)
user = usr
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return
// Client does NOT have tgui_input on: Returns regular input
if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
var/input_number = input(user, message, title, default) as null|num
return clamp(round_value ? round(input_number) : input_number, min_value, max_value)
var/datum/tgui_input_number/async/number_input = new(user, message, title, default, max_value, min_value, callback, timeout, round_value)
number_input.ui_interact(user)
/**
* # tgui_input_number
*
@@ -86,15 +54,14 @@
var/message
/// The minimum value that can be entered.
var/min_value
/// Whether the submitted number is rounded down into an integer.
var/round_value
/// The time at which the number input was created, for displaying timeout progress.
var/start_time
/// The lifespan of the number input, after which the window will close and delete itself.
var/timeout
/// The title of the TGUI window
var/title
/// Whether the submitted number is rounded down into an integer.
var/round_value
/datum/tgui_input_number/New(mob/user, message, title, default, max_value, min_value, timeout, round_value)
src.default = default
@@ -120,7 +87,7 @@
/datum/tgui_input_number/Destroy(force, ...)
SStgui.close_uis(src)
. = ..()
return ..()
/**
* Waits for a user's response to the tgui_input_number's prompt before returning. Returns early if
@@ -144,19 +111,21 @@
return GLOB.always_state
/datum/tgui_input_number/ui_static_data(mob/user)
. = list()
.["init_value"] = default // Default is a reserved keyword
.["large_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large)
.["max_value"] = max_value
.["message"] = message
.["min_value"] = min_value
.["swapped_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped)
.["title"] = title
var/list/data = list()
data["init_value"] = default // Default is a reserved keyword
data["large_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large)
data["max_value"] = max_value
data["message"] = message
data["min_value"] = min_value
data["swapped_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped)
data["title"] = title
return data
/datum/tgui_input_number/ui_data(mob/user)
. = list()
var/list/data = list()
if(timeout)
.["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
data["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
return data
/datum/tgui_input_number/ui_act(action, list/params)
. = ..()
@@ -181,29 +150,4 @@
return TRUE
/datum/tgui_input_number/proc/set_entry(entry)
src.entry = entry
/**
* # async tgui_input_number
*
* An asynchronous version of tgui_input_number to be used with callbacks instead of waiting on user responses.
*/
/datum/tgui_input_number/async
/// The callback to be invoked by the tgui_input_number upon having a choice made.
var/datum/callback/callback
/datum/tgui_input_number/async/New(mob/user, message, title, default, max_value, min_value, callback, timeout)
..(user, message, title, default, max_value, min_value, timeout)
src.callback = callback
/datum/tgui_input_number/async/Destroy(force, ...)
QDEL_NULL(callback)
. = ..()
/datum/tgui_input_number/async/set_entry(entry)
. = ..()
if(!isnull(src.entry))
callback?.InvokeAsync(src.entry)
/datum/tgui_input_number/async/wait()
return
src.entry = entry
+133
View File
@@ -0,0 +1,133 @@
/** 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)
window.subscribe(src, .proc/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
winshow(client, "tgui_say", FALSE)
window.send_message("props", list(
lightMode = client.prefs?.read_preference(/datum/preference/toggle/tgui_say_light_mode),
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)
start_thinking()
if(client.typing_indicators)
log_speech_indicators("[key_name(client)] started typing at [loc_name(client.mob)], indicators enabled.")
else
log_speech_indicators("[key_name(client)] started typing at [loc_name(client.mob)], indicators DISABLED.")
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()
if(client.typing_indicators)
log_speech_indicators("[key_name(client)] stopped typing at [loc_name(client.mob)], indicators enabled.")
else
log_speech_indicators("[key_name(client)] stopped typing at [loc_name(client.mob)], indicators DISABLED.")
/**
* 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["mode"] == TRUE)
start_thinking()
return TRUE
if(payload["mode"] == FALSE)
stop_thinking()
return TRUE
return FALSE
if (type == "typing")
start_typing()
return TRUE
if (type == "entry" || type == "force")
handle_entry(type, payload)
return TRUE
return FALSE
@@ -0,0 +1,92 @@
/**
* 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)
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)
if(channel == OOC_CHANNEL)
client.ooc(entry)
return TRUE
switch(channel)
if(RADIO_CHANNEL)
client.mob.say_verb(";" + entry)
return TRUE
if(ME_CHANNEL)
client.mob.me_verb(entry)
return TRUE
if(SAY_CHANNEL)
client.mob.say_verb(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.")
/**
* 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 = SAY_CHANNEL // No ooc leaks
delegate_speech(alter_entry(payload), target_channel)
return TRUE
return FALSE
+111
View File
@@ -0,0 +1,111 @@
/// Thinking
GLOBAL_DATUM_INIT(thinking_indicator, /mutable_appearance, mutable_appearance('icons/mob/talk.dmi', "default3", TYPING_LAYER))
/// Typing
GLOBAL_DATUM_INIT(typing_indicator, /mutable_appearance, mutable_appearance('icons/mob/talk.dmi', "default0", TYPING_LAYER))
/** Creates a thinking indicator over the mob. */
/mob/proc/create_thinking_indicator()
return
/** Removes the thinking indicator over the mob. */
/mob/proc/remove_thinking_indicator()
return
/** Creates a typing indicator over the mob. */
/mob/proc/create_typing_indicator()
return
/** Removes the typing indicator over the mob. */
/mob/proc/remove_typing_indicator()
return
/** Removes any indicators and marks the mob as not speaking IC. */
/mob/proc/remove_all_indicators()
return
/mob/set_stat(new_stat)
. = ..()
if(.)
remove_all_indicators()
/mob/Logout()
remove_all_indicators()
return ..()
/// Whether or not to show a typing indicator when speaking. Defaults to on.
/datum/preference/toggle/typing_indicator
category = PREFERENCE_CATEGORY_GAME_PREFERENCES
savefile_key = "typingIndicator"
savefile_identifier = PREFERENCE_PLAYER
/datum/preference/toggle/typing_indicator/apply_to_client(client/client, value)
client?.typing_indicators = value
/** Sets the mob as "thinking" - with indicator and variable thinking_IC */
/datum/tgui_say/proc/start_thinking()
if(!window_open || !client.typing_indicators)
return FALSE
/// Special exemptions
if(isabductor(client.mob))
return FALSE
client.mob.thinking_IC = TRUE
client.mob.create_thinking_indicator()
/** Removes typing/thinking indicators and flags the mob as not thinking */
/datum/tgui_say/proc/stop_thinking()
client.mob.remove_all_indicators()
/**
* 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()
client.mob.remove_thinking_indicator()
if(!window_open || !client.typing_indicators || !client.mob.thinking_IC)
return FALSE
client.mob.create_typing_indicator()
addtimer(CALLBACK(src, .proc/stop_typing), 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.
*/
/datum/tgui_say/proc/stop_typing()
if(!client?.mob)
return FALSE
client.mob.remove_typing_indicator()
if(!window_open || !client.typing_indicators || !client.mob.thinking_IC)
return FALSE
client.mob.create_thinking_indicator()
/// Overrides for overlay creation
/mob/living/create_thinking_indicator()
if(thinking_indicator || typing_indicator || !thinking_IC || stat != CONSCIOUS )
return FALSE
add_overlay(GLOB.thinking_indicator)
thinking_indicator = TRUE
/mob/living/remove_thinking_indicator()
if(!thinking_indicator)
return FALSE
cut_overlay(GLOB.thinking_indicator)
thinking_indicator = FALSE
/mob/living/create_typing_indicator()
if(typing_indicator || thinking_indicator || !thinking_IC || stat != CONSCIOUS)
return FALSE
add_overlay(GLOB.typing_indicator)
typing_indicator = TRUE
/mob/living/remove_typing_indicator()
if(!typing_indicator)
return FALSE
cut_overlay(GLOB.typing_indicator)
typing_indicator = FALSE
/mob/living/remove_all_indicators()
thinking_IC = FALSE
remove_thinking_indicator()
remove_typing_indicator()
@@ -44,45 +44,7 @@
qdel(text_input)
/**
* Creates an asynchronous TGUI text input window with an associated callback.
*
* This proc should be used to create text inputs that invoke a callback with the user's entry.
* Arguments:
* * user - The user to show the text input to.
* * message - The content of the text input, shown in the body of the TGUI window.
* * title - The title of the text input modal, shown on the top of the TGUI window.
* * default - The default (or current) value, shown as a placeholder.
* * max_length - Specifies a max length for input.
* * multiline - Bool that determines if the input box is much larger. Good for large messages, laws, etc.
* * encode - If toggled, input is filtered via html_encode. Setting this to FALSE gives raw input.
* * callback - The callback to be invoked when a choice is made.
*/
/proc/tgui_input_text_async(mob/user, message = "", title = "Text Input", default, max_length = MAX_MESSAGE_LEN, multiline = FALSE, encode = TRUE, datum/callback/callback, timeout = 60 SECONDS)
if (!user)
user = usr
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return
// Client does NOT have tgui_input on: Returns regular input
if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
if(encode)
if(multiline)
return stripped_multiline_input(user, message, title, default, max_length)
else
return stripped_input(user, message, title, default, max_length)
else
if(multiline)
return input(user, message, title, default) as message|null
else
return input(user, message, title, default) as text|null
var/datum/tgui_input_text/async/text_input = new(user, message, title, default, max_length, multiline, encode, callback, timeout)
text_input.ui_interact(user)
/**
* # tgui_input_text
* tgui_input_text
*
* Datum used for instantiating and using a TGUI-controlled text input that prompts the user with
* a message and has an input for text entry.
@@ -109,7 +71,6 @@
/// The title of the TGUI window
var/title
/datum/tgui_input_text/New(mob/user, message, title, default, max_length, multiline, encode, timeout)
src.default = default
src.encode = encode
@@ -124,7 +85,7 @@
/datum/tgui_input_text/Destroy(force, ...)
SStgui.close_uis(src)
. = ..()
return ..()
/**
* Waits for a user's response to the tgui_input_text's prompt before returning. Returns early if
@@ -148,19 +109,21 @@
return GLOB.always_state
/datum/tgui_input_text/ui_static_data(mob/user)
. = list()
.["large_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large)
.["max_length"] = max_length
.["message"] = message
.["multiline"] = multiline
.["placeholder"] = default // Default is a reserved keyword
.["swapped_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped)
.["title"] = title
var/list/data = list()
data["large_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large)
data["max_length"] = max_length
data["message"] = message
data["multiline"] = multiline
data["placeholder"] = default // Default is a reserved keyword
data["swapped_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped)
data["title"] = title
return data
/datum/tgui_input_text/ui_data(mob/user)
. = list()
var/list/data = list()
if(timeout)
.["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
data["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
return data
/datum/tgui_input_text/ui_act(action, list/params)
. = ..()
@@ -172,7 +135,7 @@
if(length(params["entry"]) > max_length)
CRASH("[usr] typed a text string longer than the max length")
if(encode && (length(html_encode(params["entry"])) > max_length))
to_chat(usr, span_notice("Input uses special characters, thus reducing the maximum length."))
to_chat(usr, span_notice("Your message was clipped due to special character usage."))
set_entry(params["entry"])
closed = TRUE
SStgui.close_uis(src)
@@ -182,32 +145,13 @@
SStgui.close_uis(src)
return TRUE
/**
* Sets the return value for the tgui text proc.
* If html encoding is enabled, the text will be encoded.
* This can sometimes result in a string that is longer than the max length.
* If the string is longer than the max length, it will be clipped.
*/
/datum/tgui_input_text/proc/set_entry(entry)
if(!isnull(entry))
var/converted_entry = encode ? html_encode(entry) : entry
src.entry = trim(converted_entry, max_length)
/**
* # async tgui_input_text
*
* An asynchronous version of tgui_input_text to be used with callbacks instead of waiting on user responses.
*/
/datum/tgui_input_text/async
// The callback to be invoked by the tgui_input_text upon having a choice made.
var/datum/callback/callback
/datum/tgui_input_text/async/New(mob/user, message, title, default, max_length, multiline, encode, callback, timeout)
..(user, message, title, default, max_length, multiline, encode, timeout)
src.callback = callback
/datum/tgui_input_text/async/Destroy(force, ...)
QDEL_NULL(callback)
. = ..()
/datum/tgui_input_text/async/set_entry(entry)
. = ..()
if(!isnull(src.entry))
callback?.InvokeAsync(src.entry)
/datum/tgui_input_text/async/wait()
return
+3
View File
@@ -166,6 +166,9 @@ LOG_TOOLS
## Log all timers on timer auto reset
# LOG_TIMERS_ON_BUCKET_RESET
## log speech indicators
LOG_SPEECH_INDICATORS
##Log camera pictures - Must have picture logging enabled
PICTURE_LOGGING_CAMERA
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

+19
View File
@@ -339,3 +339,22 @@ window "statwindow"
is-visible = false
saved-params = ""
window "tgui_say"
elem "tgui_say"
type = MAIN
pos = 848,500
size = 231x30
anchor1 = 50,50
anchor2 = 50,50
is-visible = false
saved-params = ""
statusbar = false
can-minimize = false
elem "browser"
type = BROWSER
pos = 0,0
size = 231x30
anchor1 = 0,0
anchor2 = 0,0
saved-params = ""
+8 -4
View File
@@ -164,6 +164,7 @@
#include "code\__DEFINES\span.dm"
#include "code\__DEFINES\spatial_gridmap.dm"
#include "code\__DEFINES\species_clothing_paths.dm"
#include "code\__DEFINES\speech_channels.dm"
#include "code\__DEFINES\speech_controller.dm"
#include "code\__DEFINES\stat.dm"
#include "code\__DEFINES\stat_tracking.dm"
@@ -4280,10 +4281,6 @@
#include "code\modules\tgui\states.dm"
#include "code\modules\tgui\status_composers.dm"
#include "code\modules\tgui\tgui.dm"
#include "code\modules\tgui\tgui_alert.dm"
#include "code\modules\tgui\tgui_input_list.dm"
#include "code\modules\tgui\tgui_input_number.dm"
#include "code\modules\tgui\tgui_input_text.dm"
#include "code\modules\tgui\tgui_window.dm"
#include "code\modules\tgui\states\admin.dm"
#include "code\modules\tgui\states\always.dm"
@@ -4306,6 +4303,13 @@
#include "code\modules\tgui\states\reverse_contained.dm"
#include "code\modules\tgui\states\self.dm"
#include "code\modules\tgui\states\zlevel.dm"
#include "code\modules\tgui_input\alert.dm"
#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"
+23 -3
View File
@@ -28,11 +28,31 @@ export const debounce = (fn, time, immediate = false) => {
};
};
/**
* Returns a function, that, when invoked, will only be triggered at most once
* during a given window of time.
*/
export const throttle = (fn, time) => {
let previouslyRun, queuedToRun;
return function invokeFn(...args) {
const now = Date.now();
queuedToRun = clearTimeout(queuedToRun);
if (!previouslyRun || now - previouslyRun >= time) {
fn.apply(null, args);
previouslyRun = now;
} else {
queuedToRun = setTimeout(
invokeFn.bind(null, ...args),
time - (now - previouslyRun)
);
}
};
};
/**
* Suspends an asynchronous function for N milliseconds.
*
* @param {number} time
*/
export const sleep = time => (
new Promise(resolve => setTimeout(resolve, time))
);
export const sleep = (time) =>
new Promise((resolve) => setTimeout(resolve, time));
@@ -0,0 +1,20 @@
import { dragStartHandler } from 'tgui/drag';
import { DragzoneProps } from '../types';
/** Creates a draggable edge. Props Req: Location */
export const Dragzone = (props: Partial<DragzoneProps>) => {
const { theme } = props;
if (!theme) return null;
const direction
= (props.top && 'top')
|| (props.right && 'right')
|| (props.bottom && 'bottom')
|| (props.left && 'left');
return (
<div
className={`dragzone-${direction}-${theme}`}
onmousedown={dragStartHandler}
/>
);
};
@@ -0,0 +1,73 @@
/** Radio channels */
export const CHANNELS = ['Say', 'Radio', 'Me', 'OOC'] as const;
/** Window sizes in pixels */
export const WINDOW_SIZES = {
small: 30,
medium: 50,
large: 70,
width: 231,
} as const;
/** Line lengths for autoexpand */
export const LINE_LENGTHS = {
small: 20,
medium: 35,
} as const;
/**
* Radio prefixes.
* Contains the properties:
* id - string. css class identifier.
* label - string. button label.
*/
export const RADIO_PREFIXES = {
':a ': {
id: 'hive',
label: 'Hive',
},
':b ': {
id: 'binary',
label: '0101',
},
':c ': {
id: 'command',
label: 'Cmd',
},
':e ': {
id: 'engi',
label: 'Engi',
},
':m ': {
id: 'medical',
label: 'Med',
},
':n ': {
id: 'science',
label: 'Sci',
},
':o ': {
id: 'ai',
label: 'AI',
},
':s ': {
id: 'security',
label: 'Sec',
},
':t ': {
id: 'syndicate',
label: 'Syndi',
},
':u ': {
id: 'supply',
label: 'Supp',
},
':v ': {
id: 'service',
label: 'Svc',
},
':y ': {
id: 'centcom',
label: 'CCom',
},
} as const;
@@ -0,0 +1,15 @@
import { KEY_DOWN, KEY_UP } from 'common/keycodes';
import { getHistoryLength } from '../helpers';
import { Modal } from '../types';
/** Increments the chat history counter, looping through entries */
export const handleArrowKeys = function (this: Modal, direction: number) {
const { historyCounter } = this.fields;
if (direction === KEY_UP && historyCounter < getHistoryLength()) {
this.fields.historyCounter++;
this.events.onViewHistory();
} else if (direction === KEY_DOWN && historyCounter > 0) {
this.fields.historyCounter--;
this.events.onViewHistory();
}
};
@@ -0,0 +1,22 @@
import { CHANNELS } from '../constants';
import { Modal } from '../types';
/**
* 1. Resets history if editing a message
* 2. Backspacing while empty resets any radio subchannels
* 3. Ensures backspace and delete calculate window size
*/
export const handleBackspaceDelete = function (this: Modal) {
const { buttonContent, channel } = this.state;
const { radioPrefix, value } = this.fields;
// User is on a chat history message
if (typeof buttonContent === 'number') {
this.fields.historyCounter = 0;
this.setState({ buttonContent: CHANNELS[channel] });
}
if (!value.length && radioPrefix) {
this.fields.radioPrefix = '';
this.setState({ buttonContent: CHANNELS[channel] });
}
this.events.onSetSize(value.length);
};
@@ -0,0 +1,9 @@
import { Modal } from '../types';
/**
* User clicks the channel button.
* Simulates the tab key.
*/
export const handleClick = function (this: Modal) {
this.events.onIncrementChannel();
};
@@ -0,0 +1,23 @@
import { CHANNELS } from '../constants';
import { windowLoad, windowOpen } from '../helpers';
import { Modal } from '../types';
/** Attach listeners, sets window size just in case */
export const handleComponentMount = function (this: Modal) {
Byond.subscribeTo('props', (data) => {
this.fields.maxLength = data.maxLength;
this.fields.lightMode = !!data.lightMode;
});
Byond.subscribeTo('force', () => {
this.events.onForce();
});
Byond.subscribeTo('open', (data) => {
const channel = CHANNELS.indexOf(data.channel) || 0;
this.setState({ buttonContent: CHANNELS[channel], channel });
setTimeout(() => {
this.fields.innerRef.current?.focus();
}, 1);
windowOpen(CHANNELS[channel]);
});
windowLoad();
};
@@ -0,0 +1,6 @@
import { Modal } from '../types';
/** After updating the input value, sets back to false */
export const handleComponentUpdate = function (this: Modal) {
this.setState({ edited: false });
};
+23
View File
@@ -0,0 +1,23 @@
import { CHANNELS } from '../constants';
import { storeChat, windowClose } from '../helpers';
import { Modal } from '../types';
/** User presses enter. Closes if no value. */
export const handleEnter = function (
this: Modal,
event: KeyboardEvent,
value: string
) {
const { channel } = this.state;
const { maxLength, radioPrefix } = this.fields;
event.preventDefault();
if (value && value.length < maxLength) {
storeChat(value);
Byond.sendMessage('entry', {
channel: CHANNELS[channel],
entry: channel === 0 ? radioPrefix + value : value,
});
}
this.events.onReset();
windowClose();
};
@@ -0,0 +1,8 @@
import { windowClose } from '../helpers';
import { Modal } from '../types';
/** User presses escape, closes the window */
export const handleEscape = function (this: Modal) {
this.events.onReset();
windowClose();
};
+19
View File
@@ -0,0 +1,19 @@
import { CHANNELS, WINDOW_SIZES } from '../constants';
import { windowSet } from '../helpers';
import { Modal } from '../types';
/** Sends the current input to byond and purges it */
export const handleForce = function (this: Modal) {
const { channel, size } = this.state;
const { radioPrefix, value } = this.fields;
if (value && channel < 2) {
this.timers.forceDebounce({
channel: CHANNELS[channel],
entry: channel === 0 ? radioPrefix + value : value,
});
this.events.onReset(channel);
if (size !== WINDOW_SIZES.small) {
windowSet();
}
}
};
@@ -0,0 +1,32 @@
import { CHANNELS } from '../constants';
import { Modal } from '../types';
/**
* Increments the channel or resets to the beginning of the list.
* If the user switches between IC/OOC, messages Byond to toggle thinking
* indicators.
*/
export const handleIncrementChannel = function (this: Modal) {
const { channel } = this.state;
const { radioPrefix } = this.fields;
if (radioPrefix === ':b ') {
this.timers.channelDebounce({ mode: true });
}
this.fields.radioPrefix = '';
if (channel === CHANNELS.length - 1) {
this.timers.channelDebounce({ mode: true });
this.setState({
buttonContent: CHANNELS[0],
channel: 0,
});
} else {
if (channel === 2) {
// Disables thinking indicator for OOC channel
this.timers.channelDebounce({ mode: false });
}
this.setState({
buttonContent: CHANNELS[channel + 1],
channel: channel + 1,
});
}
};
+41
View File
@@ -0,0 +1,41 @@
import { handleArrowKeys } from './arrowKeys';
import { handleBackspaceDelete } from './backspaceDelete';
import { handleComponentMount } from './componentMount';
import { handleComponentUpdate } from './componentUpdate';
import { handleClick } from './click';
import { handleEnter } from './enter';
import { handleEscape } from './escape';
import { handleForce } from './force';
import { handleIncrementChannel } from './incrementChannel';
import { handleInput } from './input';
import { handleKeyDown } from './keyDown';
import { handleRadioPrefix } from './radioPrefix';
import { handleReset } from './reset';
import { handleSetSize } from './setSize';
import { handleViewHistory } from './viewHistory';
import { Modal } from '../types';
/**
* Maps all TGUI say events with their associated handlers.
*
* return -- object: events
*/
export const eventHandlerMap = (parent: Modal): Modal["events"] => {
return {
onArrowKeys: handleArrowKeys.bind(parent),
onBackspaceDelete: handleBackspaceDelete.bind(parent),
onClick: handleClick.bind(parent),
onComponentMount: handleComponentMount.bind(parent),
onComponentUpdate: handleComponentUpdate.bind(parent),
onEnter: handleEnter.bind(parent),
onEscape: handleEscape.bind(parent),
onForce: handleForce.bind(parent),
onIncrementChannel: handleIncrementChannel.bind(parent),
onInput: handleInput.bind(parent),
onKeyDown: handleKeyDown.bind(parent),
onRadioPrefix: handleRadioPrefix.bind(parent),
onReset: handleReset.bind(parent),
onSetSize: handleSetSize.bind(parent),
onViewHistory: handleViewHistory.bind(parent),
};
};
+11
View File
@@ -0,0 +1,11 @@
import { Modal } from '../types';
/**
* Grabs input and sets size, force values etc.
* Input value only triggers a rerender on setEdited.
*/
export const handleInput = function (this: Modal, _, value: string) {
this.fields.value = value;
this.events.onRadioPrefix();
this.events.onSetSize(value.length);
};
@@ -0,0 +1,35 @@
import { KEY_BACKSPACE, KEY_DELETE, KEY_DOWN, KEY_TAB, KEY_UP } from 'common/keycodes';
import { isAlphanumeric, getHistoryLength } from '../helpers';
import { Modal } from '../types';
/**
* Handles other key events.
* TAB - Changes channels.
* UP/DOWN - Sets history counter and input value.
* BKSP/DEL - Resets history counter and checks window size.
* TYPING - When users key, it tells byond that it's typing.
*/
export const handleKeyDown = function (this: Modal, event: KeyboardEvent) {
const { channel } = this.state;
const { radioPrefix } = this.fields;
if (!event.keyCode) {
return; // Really doubt it, but...
}
if (isAlphanumeric(event.keyCode)) {
if (channel !== 3 && radioPrefix !== ':b ') {
this.timers.typingThrottle();
}
}
if (event.keyCode === KEY_UP || event.keyCode === KEY_DOWN) {
if (getHistoryLength()) {
this.events.onArrowKeys(event.keyCode);
}
}
if (event.keyCode === KEY_DELETE || event.keyCode === KEY_BACKSPACE) {
this.events.onBackspaceDelete();
}
if (event.keyCode === KEY_TAB) {
this.events.onIncrementChannel();
event.preventDefault();
}
};
@@ -0,0 +1,34 @@
import { RADIO_PREFIXES } from '../constants';
import { Modal } from '../types';
/**
* Gets any channel prefixes from the chat bar
* and changes to the corresponding radio subchannel.
*
* Exemptions: Channel is OOC, value is too short,
* Not a valid radio pref, or value is already the radio pref.
*/
export const handleRadioPrefix = function (this: Modal) {
const { channel } = this.state;
const { radioPrefix, value } = this.fields;
if (channel > 1 || value.length < 3) {
return;
}
const nextPrefix = value.slice(0, 3)?.toLowerCase();
if (!RADIO_PREFIXES[nextPrefix] || radioPrefix === nextPrefix) {
return;
}
this.fields.value = value.slice(3);
// Binary is a "secret" channel
if (nextPrefix === ':b ') {
Byond.sendMessage('thinking', { mode: false });
} else if (radioPrefix === ':b ' && nextPrefix !== ':b ') {
Byond.sendMessage('thinking', { mode: true });
}
this.fields.radioPrefix = nextPrefix;
this.setState({
buttonContent: RADIO_PREFIXES[nextPrefix]?.label,
channel: 0,
edited: true,
});
};
+21
View File
@@ -0,0 +1,21 @@
import { CHANNELS, WINDOW_SIZES } from '../constants';
import { valueExists } from '../helpers';
import { Modal } from '../types';
/**
* Resets window to default parameters.
*
* Parameters:
* channel - Optional. Sets the channel and thus the color scheme.
*/
export const handleReset = function (this: Modal, channel?: number) {
this.fields.historyCounter = 0;
this.fields.radioPrefix = '';
this.fields.value = '';
this.setState({
buttonContent: valueExists(channel) ? CHANNELS[channel!] : '',
channel: valueExists(channel) ? channel! : -1,
edited: true,
size: WINDOW_SIZES.small,
});
};
@@ -0,0 +1,22 @@
import { LINE_LENGTHS, WINDOW_SIZES } from '../constants';
import { windowSet } from '../helpers';
import { Modal } from '../types';
/** Adjusts window sized based on event.target.value */
export const handleSetSize = function (this: Modal, value: number) {
const { size } = this.state;
if (value > LINE_LENGTHS.medium && size !== WINDOW_SIZES.large) {
this.setState({ size: WINDOW_SIZES.large });
windowSet(WINDOW_SIZES.large);
} else if (
value <= LINE_LENGTHS.medium
&& value > LINE_LENGTHS.small
&& size !== WINDOW_SIZES.medium
) {
this.setState({ size: WINDOW_SIZES.medium });
windowSet(WINDOW_SIZES.medium);
} else if (value <= LINE_LENGTHS.small && size !== WINDOW_SIZES.small) {
this.setState({ size: WINDOW_SIZES.small });
windowSet(WINDOW_SIZES.small);
}
};
@@ -0,0 +1,24 @@
import { CHANNELS } from '../constants';
import { getHistoryAt, getHistoryLength } from '../helpers';
import { Modal } from '../types';
/** Sets the input value to chat history at index historyCounter. */
export const handleViewHistory = function (this: Modal) {
const { channel } = this.state;
const { historyCounter } = this.fields;
if (historyCounter > 0 && getHistoryLength()) {
this.fields.value = getHistoryAt(historyCounter);
if (channel < 2) {
this.timers.typingThrottle();
}
this.setState({ buttonContent: historyCounter, edited: true });
this.events.onSetSize(0);
} else {
this.fields.value = '';
this.setState({
buttonContent: CHANNELS[channel],
edited: true,
});
this.events.onSetSize(0);
}
};
+157
View File
@@ -0,0 +1,157 @@
import { CHANNELS, RADIO_PREFIXES, WINDOW_SIZES } from '../constants';
import { KEY_0, KEY_Z } from 'common/keycodes';
import { classes } from 'common/react';
import { debounce, throttle } from 'common/timer';
/**
* Window functions
*/
/**
* Once byond signals this via keystroke, it
* ensures window size, visibility, and focus.
*/
export const windowOpen = (channel: string) => {
setOpen();
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 = () => {
setClosed();
Byond.sendMessage('close');
};
/** Some QoL to hide the window on load. Doesn't log this event */
export const windowLoad = () => {
Byond.winset('tgui_say', {
pos: '848,500',
});
setClosed();
};
/**
* Modifies the window size.
*
* Parameters:
* size - The size of the window in pixels. Optional.
*/
export const windowSet = (size: number = WINDOW_SIZES.small) => {
Byond.winset('tgui_say', { size: `${WINDOW_SIZES.width}x${size}` });
Byond.winset('tgui_say.browser', { size: `${WINDOW_SIZES.width}x${size}` });
};
/** Private functions */
/** Sets the skin props as opened. Focus might be a placebo here. */
const setOpen = () => {
Byond.winset('tgui_say', {
'is-visible': true,
size: `${WINDOW_SIZES.width}x${WINDOW_SIZES.small}`,
});
Byond.winset('tgui_say.browser', {
'is-visible': true,
size: `${WINDOW_SIZES.width}x${WINDOW_SIZES.small}`,
});
};
/** Sets the skin props as closed. */
const setClosed = () => {
Byond.winset('tgui_say', {
'is-visible': false,
size: `${WINDOW_SIZES.width}x${WINDOW_SIZES.small}`,
});
Byond.winset('map', {
focus: true,
});
};
/**
* Chat history functions
*/
/** Stores a list of chat messages entered as values */
let savedMessages: string[] = [];
/** Returns the chat history at specified index */
export const getHistoryAt = (index: number): string =>
savedMessages[savedMessages.length - index];
/**
* The length of chat history.
* I am absolutely being excessive, but whatever
*/
export const getHistoryLength = (): number => savedMessages.length;
/**
* Stores entries in the chat history.
* Deletes old entries if the list is too long.
*/
export const storeChat = (message: string): void => {
if (savedMessages.length === 5) {
savedMessages.shift();
}
savedMessages.push(message);
};
/** Miscellaneous */
/**
* Returns modular css classes.
*
* Parameters:
* element - required string. The element selector.
* theme - optional string. The theme to apply.
* options - optional string | number. Adds another css selector.
*/
export const getCss = (
element: string,
theme?: string,
options?: string | number
): string =>
classes([
element,
valueExists(theme) && `${element}-${theme}`,
valueExists(options) && `${element}-${options}`,
]);
/**
* Returns a string that represents the css selector to use.
* Light mode takes precedence over radioPrefixes,
* radioPrefixes takes precedence over channel.
*
* Parameters:
* lightMode - boolean. If true, returns the light mode selector.
* radioPrefix - string. If not empty, returns the radio prefix selector.
* channel - number. The channel to use.
*/
export const getTheme = (
lightMode: boolean,
radioPrefix: string,
channel: number
): string => {
return (
(lightMode && 'lightMode')
|| RADIO_PREFIXES[radioPrefix]?.id
|| CHANNELS[channel]?.toLowerCase()
);
};
/** Checks keycodes for alpha/numeric characters */
export const isAlphanumeric = (keyCode: number): boolean =>
keyCode >= KEY_0 && keyCode <= KEY_Z;
/** Timers: Prevents overloading the server, throttles messages */
export const timers = {
channelDebounce: debounce((mode) => Byond.sendMessage('thinking', mode), 400),
forceDebounce: debounce(
(entry) => Byond.sendMessage('force', entry),
1000,
true
),
typingThrottle: throttle(() => Byond.sendMessage('typing'), 4000),
};
/** Checks if a parameter is null or undefined. Returns bool */
export const valueExists = (param: any): boolean =>
param !== null && param !== undefined;
+19
View File
@@ -0,0 +1,19 @@
import './styles/main.scss';
import { createRenderer } from 'tgui/renderer';
import { TguiSay } from './interfaces/TguiSay';
const renderApp = createRenderer(() => {
return <TguiSay />;
});
const setupApp = () => {
// Delay setup
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupApp);
return;
}
renderApp();
};
setupApp();
@@ -0,0 +1,75 @@
import { TextArea } from 'tgui/components';
import { WINDOW_SIZES } from '../constants';
import { Dragzone } from '../components/dragzone';
import { eventHandlerMap } from '../handlers';
import { getCss, getTheme, timers } from '../helpers';
import { Component, createRef } from 'inferno';
import { Modal, State } from '../types';
/** Primary class for the TGUI say modal. */
export class TguiSay extends Component<{}, State> {
events: Modal['events'] = eventHandlerMap(this);
fields: Modal['fields'] = {
historyCounter: 0,
innerRef: createRef(),
lightMode: false,
maxLength: 1024,
radioPrefix: '',
value: '',
};
state: Modal['state'] = {
buttonContent: '',
channel: -1,
edited: false,
size: WINDOW_SIZES.small,
};
timers: Modal['timers'] = timers;
componentDidMount() {
this.events.onComponentMount();
}
componentDidUpdate() {
if (this.state.edited) {
this.events.onComponentUpdate();
}
}
render() {
const { onClick, onEnter, onEscape, onKeyDown, onInput } = this.events;
const { innerRef, lightMode, maxLength, radioPrefix, value } = this.fields;
const { buttonContent, channel, edited, size } = this.state;
const theme = getTheme(lightMode, radioPrefix, channel);
return (
<div className={getCss('modal', theme, size)} $HasKeyedChildren>
<Dragzone theme={theme} top />
<div className="modal__content" $HasKeyedChildren>
<Dragzone theme={theme} left />
{!!theme && (
<button
className={getCss('button', theme)}
onclick={onClick}
type="submit">
{buttonContent}
</button>
)}
<TextArea
className={getCss('textarea', theme)}
dontUseTabForIndent
innerRef={innerRef}
maxLength={maxLength}
onEnter={onEnter}
onEscape={onEscape}
onInput={onInput}
onKeyDown={onKeyDown}
selfClear
value={edited && value}
/>
<Dragzone theme={theme} right />
</div>
<Dragzone theme={theme} bottom />
</div>
);
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"private": true,
"name": "tgui-say",
"version": "1.0.0",
"dependencies": {
"common": "workspace:*",
"dompurify": "^2.3.1",
"inferno": "^7.4.8",
"tgui": "workspace:*",
"tgui-dev-server": "workspace:*",
"tgui-polyfill": "workspace:*"
}
}
+47
View File
@@ -0,0 +1,47 @@
@use 'sass:color';
@use 'sass:selector';
@use './colors.scss';
.button {
background-color: colors.$button;
border: thin solid;
border-radius: 2px;
color: colors.$background;
font-family: 'Consolas', monospace;
font-weight: bold;
font-size: 0.9rem;
height: 100%;
padding: 0;
text-align: center;
vertical-align: middle;
width: 4rem;
&:hover {
background-color: lighten(colors.$button, 10%);
}
}
.button-lightMode {
@extend .button;
background-color: colors.$lightBorder;
border: none;
color: black;
&:hover {
background-color: colors.$lightHover;
}
}
/** Creates a button for each channel */
@each $channel,
$color in colors.$channel-map {
.button-#{$channel} {
color: $color;
border-color: darken($color, 10%);
&:hover {
border-color: lighten($color, 10%);
color: lighten($color, 5%);
}
}
}
+60
View File
@@ -0,0 +1,60 @@
@use 'sass:color';
@use 'sass:map';
$background: #131313;
$button: #1f1f1f;
$lightMode: #FFFFFF;
$lightBorder: #BBBBBB;
$lightHover: #EAEAEA;
////////////////////////////////////////////////
// Normal chat colors
$say: #a4bad6;
$radio: #1ecc43;
$me: #5975da;
$ooc: #cca300;
////////////////////////////////////////////////
// Subchannel chat colors
$ai: #d65d95;
$binary: #1e90ff;
$centcom: #2681a5;
$command: #fcdf03;
$engi: #f37746;
$hive: #855d85;
$medical: #57b8f0;
$science: #c68cfa;
$security: #dd3535;
$syndicate: #8f4a4b;
$service: #6ca729;
$supply: #b88646;
$_channel_map: (
'say': $say,
'radio': $radio,
'me': $me,
'ooc': $ooc,
'ai': $ai,
'binary': $binary,
'centcom': $centcom,
'command': $command,
'engi': $engi,
'hive': $hive,
'medical': $medical,
'science': $science,
'security': $security,
'syndicate': $syndicate,
'service': $service,
'supply': $supply,
);
$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),
));
}
@@ -0,0 +1,73 @@
@use 'sass:color';
@use './colors.scss';
.dragzone-horizontal {
color: transparent;
height: 5px;
width: 100%;
}
.dragzone-vertical {
color: transparent;
height: 100%;
position: absolute;
top: 0;
width: 5px;
}
@each $channel,
$color in colors.$channel-map {
$darkened: darken($color, 20%);
.dragzone-bottom-#{$channel} {
@extend .dragzone-horizontal;
border-left: 2px solid $darkened;
border-right: 2px solid $darkened;
}
.dragzone-left-#{$channel} {
@extend .dragzone-vertical;
left: 0;
border-left: 2px solid $darkened;
}
.dragzone-right-#{$channel} {
@extend .dragzone-vertical;
right: 0;
border-right: 2px solid $darkened;
}
.dragzone-top-#{$channel} {
@extend .dragzone-horizontal;
border-left: 2px solid $darkened;
border-right: 2px solid $darkened;
border-top: 2px solid $darkened;
}
}
/** Lightmode static theme */
.dragzone-bottom-lightMode {
@extend .dragzone-horizontal;
border-left: 2px solid colors.$lightBorder;
border-right: 2px solid colors.$lightBorder;
border-bottom: 2px solid colors.$lightBorder;
}
.dragzone-left-lightMode {
@extend .dragzone-vertical;
left: 0;
border-left: 2px solid colors.$lightBorder;
}
.dragzone-right-lightMode {
@extend .dragzone-vertical;
right: 0;
border-right: 2px solid colors.$lightBorder;
}
.dragzone-top-lightMode {
@extend .dragzone-horizontal;
border-left: 2px solid colors.$lightBorder;
border-right: 2px solid colors.$lightBorder;
border-top: 2px solid colors.$lightBorder;
}
+13
View File
@@ -0,0 +1,13 @@
@use 'sass:meta';
// 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('./dragzone.scss');
@include meta.load-css('./modal.scss');
@include meta.load-css('./textarea.scss');
+70
View File
@@ -0,0 +1,70 @@
@use 'sass:color';
@use './colors.scss';
@keyframes gradient {
0% {
background-position: 0 0;
}
100% {
background-position: 100% 0;
}
}
.modal {
background-color: colors.$background;
display: flex;
flex-direction: column;
max-width: 231px;
width: 100%;
}
.modal-lightMode {
@extend .modal;
background-color: colors.$lightMode;
}
.modal__content {
display: flex;
flex-direction: row;
height: 100%;
padding: 1px 0 1px 5px;
}
/** Window sizes */
.modal-30 {
height: 30px;
}
.modal-50 {
height: 50px;
}
.modal-70 {
height: 70px;
}
/** Creates an animated border for each channel */
@each $channel,
$color in colors.$channel-map {
.modal-#{$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;
}
}
}
@@ -0,0 +1,24 @@
@use './colors.scss';
.textarea {
background-color: transparent;
border: none;
color: colors.$background;
flex-grow: 1;
font-family: "Lucida Console", monospace;
font-size: 1.1rem;
margin-top: 2px;
}
.textarea-lightMode {
@extend .textarea;
color: black;
}
/** Creates an input for each channel */
@each $channel,
$color in colors.$channel-map {
.textarea-#{$channel} {
color: $color;
}
}
+57
View File
@@ -0,0 +1,57 @@
import { RefObject } from 'inferno';
export type Modal = {
events: Events;
fields: Fields;
setState: (state: {}) => void;
state: State;
timers: Timers;
};
type Events = {
onArrowKeys: (direction: number) => void;
onBackspaceDelete: () => void;
onClick: () => void;
onEscape: () => void;
onEnter: (event: KeyboardEvent, value: string) => void;
onForce: () => void;
onKeyDown: (event: KeyboardEvent) => void;
onIncrementChannel: () => void;
onInput: (event: InputEvent, value: string) => void;
onComponentMount: () => void;
onComponentUpdate: () => void;
onRadioPrefix: () => void;
onReset: (channel?: number) => void;
onSetSize: (size: number) => void;
onViewHistory: () => void;
};
type Fields = {
historyCounter: number;
innerRef: RefObject<HTMLInputElement>;
lightMode: boolean;
maxLength: number;
radioPrefix: string;
value: string;
};
export type State = {
buttonContent: string | number;
channel: number;
edited: boolean;
size: number;
};
type Timers = {
channelDebounce: ({ mode: boolean }) => void;
forceDebounce: ({ channel: number, entry: string }) => void;
typingThrottle: () => void;
};
export type DragzoneProps = {
theme: string;
top: boolean;
right: boolean;
bottom: boolean;
left: boolean;
};
+36 -13
View File
@@ -9,12 +9,12 @@ import { classes } from 'common/react';
import { Component, createRef } from 'inferno';
import { Box } from './Box';
import { toInputValue } from './Input';
import { KEY_ESCAPE } from 'common/keycodes';
import { KEY_ENTER, KEY_ESCAPE, KEY_TAB } from 'common/keycodes';
export class TextArea extends Component {
constructor(props, context) {
super(props, context);
this.textareaRef = createRef();
this.textareaRef = props.innerRef || createRef();
this.fillerRef = createRef();
this.state = {
editing: false,
@@ -54,19 +54,47 @@ export class TextArea extends Component {
};
this.handleKeyDown = e => {
const { editing } = this.state;
const { onKeyDown } = this.props;
if (e.keyCode === KEY_ESCAPE) {
const { onChange, onInput, onEnter, onKeyDown } = this.props;
if (e.keyCode === KEY_ENTER) {
this.setEditing(false);
e.target.value = toInputValue(this.props.value);
e.target.blur();
if (onChange) {
onChange(e, e.target.value);
}
if (onInput) {
onInput(e, e.target.value);
}
if (onEnter) {
onEnter(e, e.target.value);
}
if (this.props.selfClear) {
e.target.value = '';
e.target.blur();
}
return;
}
if (e.keyCode === KEY_ESCAPE) {
if (this.props.onEscape) {
this.props.onEscape(e);
}
this.setEditing(false);
if (this.props.selfClear) {
e.target.value = '';
} else {
e.target.value = toInputValue(this.props.value);
e.target.blur();
}
return;
}
if (!editing) {
this.setEditing(true);
}
if (onKeyDown) {
onKeyDown(e, e.target.value);
}
if (!dontUseTabForIndent) {
const keyCode = e.keyCode || e.which;
if (keyCode === 9) {
if (keyCode === KEY_TAB) {
e.preventDefault();
const { value, selectionStart, selectionEnd } = e.target;
e.target.value = (
@@ -76,9 +104,6 @@ export class TextArea extends Component {
e.target.selectionEnd = selectionStart + 1;
}
}
if (onKeyDown) {
onKeyDown(e, e.target.value);
}
};
this.handleFocus = e => {
const { editing } = this.state;
@@ -104,7 +129,6 @@ export class TextArea extends Component {
if (input) {
input.value = toInputValue(nextValue);
}
if (this.props.autoFocus || this.props.autoSelect) {
setTimeout(() => {
input.focus();
@@ -117,11 +141,10 @@ export class TextArea extends Component {
}
componentDidUpdate(prevProps, prevState) {
const { editing } = this.state;
const prevValue = prevProps.value;
const nextValue = this.props.value;
const input = this.textareaRef.current;
if (input && !editing && prevValue !== nextValue) {
if (input && typeof nextValue === "string" && prevValue !== nextValue) {
input.value = toInputValue(nextValue);
}
}
@@ -1,90 +0,0 @@
import { useBackend } from '../backend';
import { Button, Dropdown, Input, Section, Stack, TextArea } from '../components';
import { Window } from '../layouts';
export const CommandReport = (props, context) => {
const { act, data } = useBackend(context);
const {
command_name,
custom_name,
command_name_presets = [],
command_report_content,
played_sound,
announcer_sounds = [],
announce_contents,
} = data;
return (
<Window
title="Create Command Report"
width={325}
height={525}
theme="admin">
<Window.Content>
<Stack vertical>
<Stack.Item>
<Section title="Set Central Command name:" textAlign="center">
<Dropdown
width="100%"
selected={command_name}
options={command_name_presets}
onSelected={value => act('update_command_name', {
updated_name: value,
})} />
{!!custom_name && (
<Input
width="100%"
mt={1}
value={command_name}
placeholder={command_name}
onChange={(e, value) => act("update_command_name", {
updated_name: value,
})} />
)}
</Section>
</Stack.Item>
<Stack.Item>
<Section title="Set announcement sound:" textAlign="center">
<Dropdown
width="100%"
displayText={played_sound}
options={announcer_sounds}
onSelected={value => act('set_report_sound', {
picked_sound: value,
})} />
</Section>
</Stack.Item>
<Stack.Item>
<Section title="Set report text:" textAlign="center">
<TextArea
height="200px"
mb={1}
value={command_report_content}
onChange={(e, value) => act("update_report_contents", {
updated_contents: value,
})} />
<Stack vertical>
<Stack.Item>
<Button.Checkbox
fluid
checked={announce_contents}
onClick={() => act("toggle_announce")}>
Announce Contents
</Button.Checkbox>
</Stack.Item>
<Stack.Item>
<Button.Confirm
fluid
icon="check"
color="good"
textAlign="center"
content="Submit Report"
onClick={() => act("submit_report")} />
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
@@ -0,0 +1,130 @@
import { useBackend, useLocalState } from '../backend';
import { Button, Dropdown, Input, Section, Stack, TextArea } from '../components';
import { Window } from '../layouts';
type Data = {
announce_contents: string;
announcer_sounds: string[];
command_name: string;
command_name_presets: string[];
command_report_content: string;
custom_name: string;
played_sound: string;
};
export const CommandReport = () => {
return (
<Window
title="Create Command Report"
width={325}
height={525}
theme="admin">
<Window.Content>
<Stack fill vertical>
<Stack.Item>
<CentComName />
</Stack.Item>
<Stack.Item>
<AnnouncementSound />
</Stack.Item>
<Stack.Item>
<ReportText />
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
/** Allows the user to set the "sender" of the message via dropdown */
const CentComName = (_, context) => {
const { act, data } = useBackend<Data>(context);
const { command_name, command_name_presets = [], custom_name } = data;
return (
<Section title="Set Central Command name" textAlign="center">
<Dropdown
width="100%"
selected={command_name}
options={command_name_presets}
onSelected={(value) =>
act('update_command_name', {
updated_name: value,
})}
/>
{!!custom_name && (
<Input
width="100%"
mt={1}
value={command_name}
placeholder={command_name}
onChange={(_, value) =>
act('update_command_name', {
updated_name: value,
})}
/>
)}
</Section>
);
};
/** Features a section with dropdown for sounds. */
const AnnouncementSound = (_, context) => {
const { act, data } = useBackend<Data>(context);
const { announcer_sounds = [], played_sound } = data;
return (
<Section title="Set announcement sound" textAlign="center">
<Dropdown
width="100%"
displayText={played_sound}
options={announcer_sounds}
onSelected={(value) =>
act('set_report_sound', {
picked_sound: value,
})}
/>
</Section>
);
};
/** Creates the report textarea with a submit button. */
const ReportText = (_, context) => {
const { act, data } = useBackend<Data>(context);
const { announce_contents, command_report_content } = data;
const [commandReport, setCommandReport] = useLocalState<string>(
context,
'textArea',
command_report_content
);
return (
<Section title="Set report text" textAlign="center">
<TextArea
height="200px"
mb={1}
onInput={(_, value) => setCommandReport(value)}
value={commandReport}
/>
<Stack vertical>
<Stack.Item>
<Button.Checkbox
fluid
checked={announce_contents}
onClick={() => act('toggle_announce')}>
Announce Contents
</Button.Checkbox>
</Stack.Item>
<Stack.Item>
<Button.Confirm
fluid
icon="check"
textAlign="center"
content="Submit Report"
onClick={() => act('submit_report', { report: commandReport })}
/>
</Stack.Item>
</Stack>
</Section>
);
};
@@ -34,3 +34,10 @@ export const tgui_lock: FeatureToggle = {
description: 'Locks TGUI windows to your main monitor.',
component: CheckboxInput,
};
export const tgui_say_light_mode: FeatureToggle = {
name: 'Say: Light mode',
category: 'UI',
description: 'Sets TGUI Say to use a light mode.',
component: CheckboxInput,
};
@@ -0,0 +1,8 @@
import { CheckboxInput, FeatureToggle } from '../base';
export const typingIndicator: FeatureToggle = {
name: 'Enable typing indicators for self',
category: 'GAMEPLAY',
description: "Enable typing indicators that show you're typing a message.",
component: CheckboxInput,
};
@@ -90,15 +90,10 @@ const InputArea = (props, context) => {
autoSelect
height={multiline || input.length >= 30 ? '100%' : '1.8rem'}
maxLength={max_length}
onKeyDown={(event) => {
const keyCode = window.event ? event.which : event.keyCode;
if (keyCode === KEY_ENTER) {
act('submit', { entry: input });
event.preventDefault();
}
if (keyCode === KEY_ESCAPE) {
act('cancel');
}
onEscape={() => act('cancel')}
onEnter={(event) => {
act('submit', { entry: input });
event.preventDefault();
}}
onInput={(_, value) => onType(value)}
placeholder="Type something..."
+4
View File
@@ -40,6 +40,10 @@ module.exports = (env = {}, argv) => {
'./packages/tgui-polyfill',
'./packages/tgui-panel',
],
'tgui-say': [
'./packages/tgui-polyfill',
'./packages/tgui-say',
],
},
output: {
path: argv.useTmpFolder
+13
View File
@@ -8191,6 +8191,19 @@ resolve@^2.0.0-next.3:
languageName: unknown
linkType: soft
"tgui-say@workspace:packages/tgui-say":
version: 0.0.0-use.local
resolution: "tgui-say@workspace:packages/tgui-say"
dependencies:
common: "workspace:*"
dompurify: ^2.3.1
inferno: ^7.4.8
tgui: "workspace:*"
tgui-dev-server: "workspace:*"
tgui-polyfill: "workspace:*"
languageName: unknown
linkType: soft
"tgui-workspace@workspace:.":
version: 0.0.0-use.local
resolution: "tgui-workspace@workspace:."
+2
View File
@@ -183,6 +183,8 @@ export const TguiTarget = new Juke.Target({
'tgui/public/tgui.bundle.js',
'tgui/public/tgui-panel.bundle.css',
'tgui/public/tgui-panel.bundle.js',
"tgui/public/tgui-say.bundle.css",
"tgui/public/tgui-say.bundle.js",
],
executes: () => yarn('tgui:build'),
});