mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-28 07:38:05 +01:00
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:
co-authored by
Mothblocks
AnturK
iamgoofball
Aleksej Komarov
KubeRoot
Kapu1178
Iamgoofball
DomitiusKnack
LemonInTheDark
Wallem
parent
5b77b9e44e
commit
acfa5e4fdd
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
|
||||
@@ -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]")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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")])"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 |
@@ -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
@@ -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"
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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),
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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:*"
|
||||
}
|
||||
}
|
||||
@@ -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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
+7
@@ -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,
|
||||
};
|
||||
|
||||
+8
@@ -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..."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:."
|
||||
|
||||
@@ -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'),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user