diff --git a/code/__HELPERS/chat.dm b/code/__HELPERS/chat.dm
index 57824b6286..a45bbe23d5 100644
--- a/code/__HELPERS/chat.dm
+++ b/code/__HELPERS/chat.dm
@@ -18,7 +18,7 @@ For example if you have the following channels in tgs4 set up
and you make the call:
-send2chat("I sniff butts", CONFIG_GET(string/where_to_send_sniff_butts))
+send2chat(new /datum/tgs_message_content("I sniff butts"), CONFIG_GET(string/where_to_send_sniff_butts))
and the config option is set like:
@@ -40,23 +40,22 @@ In TGS3 it will always be sent to all connected designated game chats.
*
* message - The message to send.
* channel_tag - Required. If "", the message with be sent to all connected (Game-type for TGS3) channels. Otherwise, it will be sent to TGS4 channels with that tag (Delimited by ','s).
+ * admin_only - Determines if this communication can only be sent to admin only channels.
*/
-/proc/send2chat(message, channel_tag)
+/proc/send2chat(message, channel_tag, admin_only = FALSE)
if(channel_tag == null || !world.TgsAvailable())
return
var/datum/tgs_version/version = world.TgsVersion()
if(channel_tag == "" || version.suite == 3)
- world.TgsTargetedChatBroadcast(message, FALSE)
+ world.TgsTargetedChatBroadcast(message, admin_only)
return
-
var/list/channels_to_use = list()
for(var/I in world.TgsChatChannelInfo())
var/datum/tgs_chat_channel/channel = I
var/list/applicable_tags = splittext(channel.custom_tag, ",")
- if(channel_tag in applicable_tags)
+ if((!admin_only || channel.is_admin_channel) && (channel_tag in applicable_tags))
channels_to_use += channel
-
if(channels_to_use.len)
world.TgsChatBroadcast(message, channels_to_use)
@@ -69,6 +68,9 @@ In TGS3 it will always be sent to all connected designated game chats.
/proc/send2adminchat(category, message, embed_links = FALSE)
category = replacetext(replacetext(category, "\proper", ""), "\improper", "")
message = replacetext(replacetext(message, "\proper", ""), "\improper", "")
- // if(!embed_links)
- // message = GLOB.has_discord_embeddable_links.Replace(replacetext(message, "`", ""), " ```$1``` ")
+ if(!embed_links)
+ message = GLOB.has_discord_embeddable_links.Replace(replacetext(message, "`", ""), " ```$1``` ")
world.TgsTargetedChatBroadcast("[category] | [message]", TRUE)
+
+/// Handles text formatting for item use hints in examine text
+#define EXAMINE_HINT(text) ("" + text + " ")
diff --git a/code/__HELPERS/roundend.dm b/code/__HELPERS/roundend.dm
index 142e3b44ab..325233b7be 100644
--- a/code/__HELPERS/roundend.dm
+++ b/code/__HELPERS/roundend.dm
@@ -275,7 +275,7 @@
if(CONFIG_GET(string/chat_reboot_role))
broadcastmessage += "\n\n<@&[CONFIG_GET(string/chat_reboot_role)]>, the server will reboot shortly!"
- send2chat(broadcastmessage, CONFIG_GET(string/chat_roundend_notice_tag))
+ send2chat(new /datum/tgs_message_content(broadcastmessage), CONFIG_GET(string/chat_roundend_notice_tag))
CHECK_TICK
diff --git a/code/_globalvars/regexes.dm b/code/_globalvars/regexes.dm
index bd252b68ce..934296fe1b 100644
--- a/code/_globalvars/regexes.dm
+++ b/code/_globalvars/regexes.dm
@@ -5,3 +5,19 @@ GLOBAL_DATUM_INIT(is_website, /regex, regex("http|www.|\[a-z0-9_-]+.(com|org|net
GLOBAL_DATUM_INIT(is_email, /regex, regex("\[a-z0-9_-]+@\[a-z0-9_-]+.\[a-z0-9_-]+", "i"))
GLOBAL_DATUM_INIT(is_alphanumeric, /regex, regex("\[a-z0-9]+", "i"))
GLOBAL_DATUM_INIT(is_punctuation, /regex, regex("\[.!?]+", "i"))
+GLOBAL_DATUM_INIT(is_color, /regex, regex("^#\[0-9a-fA-F]{6}$"))
+GLOBAL_DATUM_INIT(is_alpha_color, /regex, regex("^#\[0-9a-fA-F]{8}$"))
+
+//finds text strings recognized as links on discord. Mainly used to stop embedding.
+GLOBAL_DATUM_INIT(has_discord_embeddable_links, /regex, regex("(https?://\[^\\s|<\]{2,})"))
+
+//All < and > characters
+GLOBAL_DATUM_INIT(angular_brackets, /regex, regex(@"[<>]", "g"))
+
+//All characters between < a > inclusive of the bracket
+GLOBAL_DATUM_INIT(html_tags, /regex, regex(@"<.*?>", "g"))
+
+//All characters forbidden by filenames: ", \, \n, \t, /, ?, %, *, :, |, <, >, ..
+GLOBAL_DATUM_INIT(filename_forbidden_chars, /regex, regex(@{""|[\\\n\t/?%*:|<>]|\.\."}, "g"))
+GLOBAL_PROTECT(filename_forbidden_chars)
+// had to use the OR operator for quotes instead of putting them in the character class because it breaks the syntax highlighting otherwise.
diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm
index 2417210f8f..fb889dd0cb 100644
--- a/code/controllers/subsystem/ticker.dm
+++ b/code/controllers/subsystem/ticker.dm
@@ -163,7 +163,7 @@ SUBSYSTEM_DEF(ticker)
for(var/client/C in GLOB.clients)
window_flash(C, ignorepref = TRUE) //let them know lobby has opened up.
to_chat(world, "Welcome to [station_name()]! ")
- send2chat("New round starting on [SSmapping.config.map_name]!", CONFIG_GET(string/chat_announce_new_game))
+ send2chat(new /datum/tgs_message_content("New round starting on [SSmapping.config.map_name]!"), CONFIG_GET(string/chat_announce_new_game))
current_state = GAME_STATE_PREGAME
//SPLURT EDIT - Bring back old panel
//Everyone who wants to be an observer is now spawned
@@ -348,7 +348,7 @@ SUBSYSTEM_DEF(ticker)
var/list/allmins = adm["present"]
send2adminchat("Server", "Round [GLOB.round_id ? "#[GLOB.round_id]:" : "of"] [hide_mode ? "secret":"[mode.name]"] has started[allmins.len ? ".":" with no active admins online!"]")
if(CONFIG_GET(string/new_round_ping))
- send2chat("<@&[CONFIG_GET(string/new_round_ping)]> | A new round has started on [SSmapping.config.map_name]!", CONFIG_GET(string/chat_announce_new_game))
+ send2chat(new /datum/tgs_message_content("<@&[CONFIG_GET(string/new_round_ping)]> | A new round has started on [SSmapping.config.map_name]!"), CONFIG_GET(string/chat_announce_new_game))
setup_done = TRUE
for(var/i in GLOB.start_landmarks_list)
diff --git a/code/modules/admin/chat_commands.dm b/code/modules/admin/chat_commands.dm
index afe7367517..ae19984f15 100644
--- a/code/modules/admin/chat_commands.dm
+++ b/code/modules/admin/chat_commands.dm
@@ -15,22 +15,8 @@
var/list/allmins = adm["total"]
var/status = "Admins: [allmins.len] (Active: [english_list(adm["present"])] AFK: [english_list(adm["afk"])] Stealth: [english_list(adm["stealth"])] Skipped: [english_list(adm["noflags"])]). "
status += "Players: [GLOB.clients.len] (Active: [get_active_player_count(0,1,0)]). Mode: [SSticker.mode ? SSticker.mode.name : "Not started"]."
- return status
+ return new /datum/tgs_message_content(status)
-/datum/tgs_chat_command/irccheck
- name = "check"
- help_text = "Gets the playercount, round info, and address of the server"
- var/last_irc_check = 0
-
-/datum/tgs_chat_command/irccheck/Run(datum/tgs_chat_user/sender, params)
- var/rtod = REALTIMEOFDAY
- if(rtod - last_irc_check < IRC_STATUS_THROTTLE)
- return
- last_irc_check = rtod
- var/server = CONFIG_GET(string/server)
- return "[GLOB.round_id ? "Round #[GLOB.round_id]: " : ""][GLOB.clients.len] players on [SSmapping.config.map_name]; Round [SSticker.HasRoundStarted() ? (SSticker.IsRoundInProgress() ? "Active for [DisplayTimeText(world.time - SSticker.round_start_time, 1)]" : "Finishing") : "Starting"] -- [server ? server : ""
- //CIT CHANGE obfuscates the gamemode for TGS bot commands on discord by removing Mode:[GLOB.master_mode]
- //sandstorm change, says how long the round has been on for
/datum/tgs_chat_command/ahelp
name = "ahelp"
help_text = " |list>>"
@@ -39,7 +25,7 @@
/datum/tgs_chat_command/ahelp/Run(datum/tgs_chat_user/sender, params)
var/list/all_params = splittext(params, " ")
if(all_params.len < 2)
- return "Insufficient parameters"
+ return new /datum/tgs_message_content("Insufficient parameters")
var/target = all_params[1]
all_params.Cut(1, 2)
var/id = text2num(target)
@@ -48,10 +34,10 @@
if(AH)
target = AH.initiator_ckey
else
- return "Ticket #[id] not found!"
+ return new /datum/tgs_message_content("Ticket #[id] not found!")
var/res = IrcPm(target, all_params.Join(" "), sender.friendly_name)
if(res != "Message Successful")
- return res
+ return new /datum/tgs_message_content(res)
/datum/tgs_chat_command/namecheck
name = "namecheck"
@@ -64,7 +50,7 @@
return "Insufficient parameters"
log_admin("Chat Name Check: [sender.friendly_name] on [params]")
message_admins("Name checking [params] from [sender.friendly_name]")
- return keywords_lookup(params, 1)
+ return new /datum/tgs_message_content(keywords_lookup(params, 1))
/datum/tgs_chat_command/adminwho
name = "adminwho"
@@ -72,7 +58,7 @@
admin_only = TRUE
/datum/tgs_chat_command/adminwho/Run(datum/tgs_chat_user/sender, params)
- return ircadminwho()
+ return new /datum/tgs_message_content(ircadminwho())
GLOBAL_LIST(round_end_notifiees)
@@ -83,10 +69,10 @@ GLOBAL_LIST(round_end_notifiees)
/datum/tgs_chat_command/notify/Run(datum/tgs_chat_user/sender, params)
if(!SSticker.IsRoundInProgress() && SSticker.HasRoundStarted())
- return "[sender.mention], the round has already ended!"
+ return new /datum/tgs_message_content("[sender.mention], the round has already ended!")
LAZYINITLIST(GLOB.round_end_notifiees)
GLOB.round_end_notifiees[sender.mention] = TRUE
- return "I will notify [sender.mention] when the round ends."
+ return new /datum/tgs_message_content("I will notify [sender.mention] when the round ends.")
/datum/tgs_chat_command/sdql
name = "sdql"
@@ -95,12 +81,12 @@ GLOBAL_LIST(round_end_notifiees)
/datum/tgs_chat_command/sdql/Run(datum/tgs_chat_user/sender, params)
if(GLOB.AdminProcCaller)
- return "Unable to run query, another admin proc call is in progress. Try again later."
+ return new /datum/tgs_message_content("Unable to run query, another admin proc call is in progress. Try again later.")
GLOB.AdminProcCaller = "CHAT_[sender.friendly_name]" //_ won't show up in ckeys so it'll never match with a real admin
var/list/results = world.SDQL2_query(params, GLOB.AdminProcCaller, GLOB.AdminProcCaller)
GLOB.AdminProcCaller = null
if(!results)
- return "Query produced no output"
+ return new /datum/tgs_message_content("Query produced no output")
var/list/text_res = results.Copy(1, 3)
var/list/refs = results.len > 3 ? results.Copy(4) : null
if(refs)
@@ -112,7 +98,7 @@ GLOBAL_LIST(round_end_notifiees)
else
L += "[ref]"
refs = L
- . = "[text_res.Join("\n")][refs ? "\nRefs: [refs.Join(" ")]" : ""]"
+ . = new /datum/tgs_message_content("[text_res.Join("\n")][refs ? "\nRefs: [refs.Join(" ")]" : ""]")
/datum/tgs_chat_command/reload_admins
name = "reload_admins"
@@ -122,7 +108,7 @@ GLOBAL_LIST(round_end_notifiees)
/datum/tgs_chat_command/reload_admins/Run(datum/tgs_chat_user/sender, params)
ReloadAsync()
log_admin("[sender.friendly_name] reloaded admins via chat command.")
- return "Admins reloaded."
+ return new /datum/tgs_message_content("Admins reloaded.")
/datum/tgs_chat_command/reload_admins/proc/ReloadAsync()
set waitfor = FALSE
@@ -135,14 +121,14 @@ GLOBAL_LIST(round_end_notifiees)
/datum/tgs_chat_command/addbunkerbypass/Run(datum/tgs_chat_user/sender, params)
if(!CONFIG_GET(flag/sql_enabled))
- return "The Database is not enabled!"
+ return new /datum/tgs_message_content("The Database is not enabled!")
GLOB.bunker_passthrough |= ckey(params)
GLOB.bunker_passthrough[ckey(params)] = world.realtime
SSpersistence.SavePanicBunker() //we can do this every time, it's okay
log_admin("[sender.friendly_name] has added [params] to the current round's bunker bypass list.")
message_admins("[sender.friendly_name] has added [params] to the current round's bunker bypass list.")
- return "[params] has been added to the current round's bunker bypass list."
+ return new /datum/tgs_message_content("[params] has been added to the current round's bunker bypass list.")
// More (silly) chat commands citadel added.
/datum/tgs_chat_command/wheelofsalt
@@ -159,21 +145,21 @@ GLOBAL_LIST(round_end_notifiees)
saltresult += " `\[REDACTED\]` gets some salt this time too"
else
saltresult += "[saltprimarysubject] [saltsecondarysubject]"
- return "[saltresult]!"
+ return new /datum/tgs_message_content("[saltresult]!")
/datum/tgs_chat_command/valentine
name = "valentine"
help_text = "Get a random flirt line."
/datum/tgs_chat_command/valentine/Run(datum/tgs_chat_user/sender, params)
- return "[pick(GLOB.flirts)]"
+ return new /datum/tgs_message_content("[pick(GLOB.flirts)]")
/datum/tgs_chat_command/despacito
name = "despacito" //someone please high effort this sometime and make it a full on ytdl search
help_text = "This is so sad."
/datum/tgs_chat_command/despacito/Run()
- return "https://www.youtube.com/watch?v=kJQP7kiw5Fk"
+ return new /datum/tgs_message_content("https://www.youtube.com/watch?v=kJQP7kiw5Fk")
/datum/tgs_chat_command/polly
name = "polly"
@@ -187,7 +173,7 @@ GLOBAL_LIST(round_end_notifiees)
else
var/json_file = file("data/npc_saves/Poly.json")
if(!fexists(json_file))
- return "**BAWWWWWK!** LEAVE THE HEADSET! ***BAWKKKKK!!***"
+ return new /datum/tgs_message_content("**BAWWWWWK!** LEAVE THE HEADSET! ***BAWKKKKK!!***")
var/list/json = json_decode(file2text(json_file))
speech_buffer = json["phrases"]
- return "[pick(speech_buffer)]"
+ return new /datum/tgs_message_content("[pick(speech_buffer)]")
diff --git a/code/modules/discord/tgs_commands.dm b/code/modules/discord/tgs_commands.dm
new file mode 100644
index 0000000000..49b26eb005
--- /dev/null
+++ b/code/modules/discord/tgs_commands.dm
@@ -0,0 +1,33 @@
+/datum/tgs_chat_command/tgscheck
+ name = "check"
+ help_text = "Gets the playercount, gamemode, and address of the server"
+
+/datum/tgs_chat_command/tgscheck/Run(datum/tgs_chat_user/sender, params)
+ var/server = CONFIG_GET(string/server)
+ return new /datum/tgs_message_content("[GLOB.round_id ? "Round #[GLOB.round_id]: " : ""][GLOB.clients.len] players on [SSmapping.config.map_name]; Round [SSticker.HasRoundStarted() ? (SSticker.IsRoundInProgress() ? "Active" : "Finishing") : "Starting"] -- [server ? server : "[world.internet_address]:[world.port]"]")
+
+/datum/tgs_chat_command/gameversion
+ name = "gameversion"
+ help_text = "Gets the version details from the show-server-revision verb, basically"
+
+/datum/tgs_chat_command/gameversion/Run(datum/tgs_chat_user/sender, params)
+ var/list/msg = list("")
+ msg += "BYOND Server Version: [world.byond_version].[world.byond_build] (Compiled with: [DM_VERSION].[DM_BUILD])\n"
+
+ if (!GLOB.revdata)
+ msg += "No revision information found."
+ else
+ msg += "Revision [copytext_char(GLOB.revdata.commit, 1, 9)]"
+ if (GLOB.revdata.date)
+ msg += " compiled on '[GLOB.revdata.date]'"
+
+ if(GLOB.revdata.originmastercommit)
+ msg += ", from origin commit: <[CONFIG_GET(string/githuburl)]/commit/[GLOB.revdata.originmastercommit]>"
+
+ if(GLOB.revdata.testmerge.len)
+ msg += "\n"
+ for(var/datum/tgs_revision_information/test_merge/PR as anything in GLOB.revdata.testmerge)
+ msg += "PR #[PR.number] at [copytext_char(PR.head_commit, 1, 9)] [PR.title].\n"
+ if (PR.url)
+ msg += "<[PR.url]>\n"
+ return new /datum/tgs_message_content(msg.Join(""))
diff --git a/code/modules/mob/dead/observer/respawn.dm b/code/modules/mob/dead/observer/respawn.dm
index e191525dd5..bf7ed9658f 100644
--- a/code/modules/mob/dead/observer/respawn.dm
+++ b/code/modules/mob/dead/observer/respawn.dm
@@ -40,8 +40,8 @@
message_admins("[key_name_admin(src)] gave [key_name_admin(O)] a full respawn and sent them back to the lobby.")
log_admin("[key_name(src)] gave [key_name(O)] a full respawn and sent them back to the lobby.")
to_chat(O, "You have been given a full respawn. ")
- O.do_respawn(FALSE)
- O.client.prefs.dnr_triggered = FALSE
+ if(O.do_respawn(FALSE))
+ player.prefs.dnr_triggered = FALSE
else if(istype(M, /mob/dead/new_player))
var/mob/dead/new_player/NP = M
var/confirm = alert(src, "Remove [NP]'s respawn restrictions?", "Remove Restrictions", "Yes", "No")
@@ -160,7 +160,7 @@
*/
/mob/dead/observer/proc/do_respawn(penalize)
if(!client)
- return
+ return FALSE
if(isnull(penalize))
penalize = client.prefs.respawn_restrictions_active
client.prefs.respawn_restrictions_active = penalize
@@ -172,6 +172,7 @@
to_chat(N, "You have been respawned to the lobby. \
Remember to take heed of rules regarding round knowledge - notably, that ALL past lives are forgotten. \
Any character you join as has NO knowledge of round events unless specified otherwise by an admin. ")
+ return TRUE
/**
* Actual proc that removes us and puts us back on lobby
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index b0cea6783f..be33694b37 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -826,7 +826,7 @@
/mob/living/carbon/human/proc/piggyback(mob/living/carbon/target)
if(can_piggyback(target))
visible_message("[target] starts to climb onto [src]... ")
- if(do_after(target, 1.5 SECONDS, src, extra_checks = CALLBACK(src, PROC_REF(can_piggyback), target)))
+ if(do_after(target, 1.5 SECONDS, src, IGNORE_INCAPACITATED, extra_checks = CALLBACK(src, PROC_REF(can_piggyback), target)))
if(can_piggyback(target))
if(target.incapacitated(FALSE, TRUE) || incapacitated(FALSE, TRUE))
target.visible_message("[target] can't hang onto [src]! ")
diff --git a/code/modules/mob/living/simple_animal/parrot.dm b/code/modules/mob/living/simple_animal/parrot.dm
index 0c24dd36fa..516ffa1085 100644
--- a/code/modules/mob/living/simple_animal/parrot.dm
+++ b/code/modules/mob/living/simple_animal/parrot.dm
@@ -932,7 +932,7 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list(
/mob/living/simple_animal/parrot/Polly/say(message, bubble_type,var/list/spans = list(), sanitize = TRUE, datum/language/language = null, ignore_spam = FALSE, forced = null)
. = ..()
if(. && !client && prob(1) && prob(1) && CONFIG_GET(string/chat_squawk_tag)) //Only the one true bird may speak across dimensions.
- send2chat("A stray squawk is heard... \"[message]\"", CONFIG_GET(string/chat_squawk_tag))
+ send2chat(new /datum/tgs_message_content("A stray squawk is heard... \"[message]\""), CONFIG_GET(string/chat_squawk_tag))
/mob/living/simple_animal/parrot/Polly/BiologicalLife(delta_time, times_fired)
if(!(. = ..()))
diff --git a/modular_sand/code/modules/tgs/chat_commands.dm b/modular_sand/code/modules/tgs/chat_commands.dm
index c87de31a3d..6c8611306f 100644
--- a/modular_sand/code/modules/tgs/chat_commands.dm
+++ b/modular_sand/code/modules/tgs/chat_commands.dm
@@ -3,7 +3,7 @@
help_text = "Lists everyone currently on the server"
/datum/tgs_chat_command/who/Run(datum/tgs_chat_user/sender, params)
- return tgswho()
+ return new /datum/tgs_message_content(tgswho())
/datum/tgs_chat_command/awho
name = "awho"
@@ -11,7 +11,7 @@
admin_only = TRUE
/datum/tgs_chat_command/awho/Run(datum/tgs_chat_user/sender, params)
- return tgsadminwho()
+ return new /datum/tgs_message_content(tgsadminwho())
/datum/tgs_chat_command/restart
name = "restart"
@@ -19,7 +19,7 @@
admin_only = TRUE
/datum/tgs_chat_command/restart/Run(datum/tgs_chat_user/sender)
- . = "Restarting."
+ . = new /datum/tgs_message_content("Restarting.")
to_chat(world, span_boldwarning("Server restart - Initialized by [sender.friendly_name] on Discord."))
send2adminchat("Server", "[sender.friendly_name] forced a restart.")
addtimer(CALLBACK(src, world.TgsEndProcess()), 1 SECONDS)
diff --git a/tgstation.dme b/tgstation.dme
index 96976371fc..3fd3374b9b 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -2190,6 +2190,7 @@
#include "code\modules\detectivework\detective_work.dm"
#include "code\modules\detectivework\evidence.dm"
#include "code\modules\detectivework\scanner.dm"
+#include "code\modules\discord\tgs_commands.dm"
#include "code\modules\economy\_economy.dm"
#include "code\modules\economy\account.dm"
#include "code\modules\economy\paystand.dm"
diff --git a/tgui/packages/tgui/interfaces/MobInteraction.tsx b/tgui/packages/tgui/interfaces/MobInteraction.tsx
index f37d550dbe..f48a90f1a2 100644
--- a/tgui/packages/tgui/interfaces/MobInteraction.tsx
+++ b/tgui/packages/tgui/interfaces/MobInteraction.tsx
@@ -1,5 +1,6 @@
import { filter, map, sortBy } from 'common/collections';
import { flow } from 'common/fp';
+import { clamp } from 'common/math';
import { createSearch } from 'common/string';
import { useBackend, useLocalState } from '../backend';
import { BlockQuote, Button, Flex, LabeledList, Icon, Input, Section, Table, Tabs, Stack, ProgressBar, Divider } from '../components';
@@ -91,6 +92,10 @@ export const MobInteraction = (props, context) => {
theirLust,
theirMaxLust,
} = data;
+ const [
+ searchText,
+ setSearchText,
+ ] = useLocalState(context, 'searchText', '');
const [tabIndex, setTabIndex] = useLocalState(context, 'tabIndex', 0);
return (
@@ -98,46 +103,52 @@ export const MobInteraction = (props, context) => {
width={430}
height={700}
resizable>
-
-
+
+
-
-
-
- You...
- {selfAttributes.map(attribute => (
-
- {attribute}
-
- ))}
-
-
- {!isTargetSelf ? (
-
+
+
+
- They...
- {theirAttributes.map(attribute => (
+ You...
+ {selfAttributes.map(attribute => (
{attribute}
))}
- ) : (null)}
-
-
-
-
-
- {(!isTargetSelf && !isNaN(theirLust)) ? (
-
-
+ {!isTargetSelf ? (
+
+
+ They...
+ {theirAttributes.map(attribute => (
+
+ {attribute}
+
+ ))}
+
+
+ ) : (null)}
+
+
+
+
+
+
- ) : (null)}
-
+ {(!isTargetSelf && !isNaN(theirLust)) ? (
+
+
+
+ ) : (null)}
+
+
-
+
setTabIndex(0)}>
Interactions
@@ -152,6 +163,19 @@ export const MobInteraction = (props, context) => {
Preferences
+
+
+
+
+
+ setSearchText(value)} />
+
+
{tabIndex === 0 && (
) || tabIndex === 1 && (
@@ -175,63 +199,51 @@ const InteractionsTab = (props, context) => {
] = useLocalState(context, 'searchText', '');
const interactions = sortInteractions(data.interactions, searchText) || [];
return (
-
+
+
+ {
+ interactions.length ? (
+ interactions.map((interaction) => (
+
+ act('interact', {
+ interaction: interaction.key,
+ })}>
+ {interaction.additionalDetails && (
+ interaction.additionalDetails.map(detail => (
+
+
+
+ )))}
+
+
+ ))
+ ) : (
+
+ {
+ searchText ? (
+ "No matching results."
+ ) : (
+ "No interactions available."
+ )
+ }
+
+ )
+ }
+
+
);
};
@@ -250,6 +262,20 @@ export const sortInteractions = (interactions, searchText = '') => {
])(interactions);
};
+/**
+ * Genital sorter!
+ */
+export const sortGenitals = (genitals, searchText = '') => {
+ const testSearch = createSearch(searchText,
+ genital => genital.name);
+ return flow([
+ // Optional search term
+ searchText && filter(testSearch),
+ // Slightly expensive, but way better than sorting in BYOND
+ sortBy(genital => genital.name),
+ ])(genitals);
+};
+
// Self explanatory
const ModeToIcon = {
"Always visible": "eye",
@@ -271,65 +297,77 @@ const ModeToIcon = {
*/
const GenitalTab = (props, context) => {
const { act, data } = useBackend(context);
- const genitals = data.genitals || [];
+ const [
+ searchText,
+ setSearchText,
+ ] = useLocalState(context, 'searchText', '');
+ const genitals = sortGenitals(data.genitals, searchText) || [];
return (
- genitals.length ? (
-
- {genitals.map(genital => (
-
-
-
- Visibility
- {genital.possible_choices.map(choice => (
+
+ {genitals.length ? (
+
+ {genitals.map(genital => (
+
+
+
+ Visibility
+ {genital.possible_choices.map(choice => (
+ act('genital', {
+ genital: genital.key,
+ visibility: choice,
+ })} />
+ ))}
+
+
+ Actions
act('genital', {
genital: genital.key,
- visibility: choice,
+ set_arousal: !genital.arousal_state,
})} />
- ))}
-
-
- Actions
- act('genital', {
- genital: genital.key,
- set_arousal: !genital.arousal_state,
- })} />
- act('genital', {
- genital: genital.key,
- set_accessibility: true,
- })} />
-
-
-
- ))}
-
- ) : (
-
- You don't seem to have any genitals...
- Or any that you could modify.
-
- )
+ act('genital', {
+ genital: genital.key,
+ set_accessibility: true,
+ })} />
+
+
+
+ ))}
+
+ ) : (
+
+ {
+ searchText ? (
+ "No matching results."
+ ) : (
+ "You don't seem to have any genitals...\
+ Or any that you could modify."
+ )
+ }
+
+ )}
+
);
};
@@ -344,143 +382,145 @@ const CharacterPrefsTab = (props, context) => {
extreme_harm,
} = data;
return (
-
-
-
- act('char_pref', {
- char_pref: 'erp_pref',
- value: 1,
- })} />
- act('char_pref', {
- char_pref: 'erp_pref',
- value: 2,
- })} />
- act('char_pref', {
- char_pref: 'erp_pref',
- value: 0,
- })} />
-
-
- act('char_pref', {
- char_pref: 'noncon_pref',
- value: 1,
- })} />
- act('char_pref', {
- char_pref: 'noncon_pref',
- value: 2,
- })} />
- act('char_pref', {
- char_pref: 'noncon_pref',
- value: 0,
- })} />
-
-
- act('char_pref', {
- char_pref: 'vore_pref',
- value: 1,
- })} />
- act('char_pref', {
- char_pref: 'vore_pref',
- value: 2,
- })} />
- act('char_pref', {
- char_pref: 'vore_pref',
- value: 0,
- })} />
-
-
- act('char_pref', {
- char_pref: 'unholy_pref',
- value: 1,
- })} />
- act('char_pref', {
- char_pref: 'unholy_pref',
- value: 2,
- })} />
- act('char_pref', {
- char_pref: 'unholy_pref',
- value: 0,
- })} />
-
-
- act('char_pref', {
- char_pref: 'extreme_pref',
- value: 1,
- })} />
- act('char_pref', {
- char_pref: 'extreme_pref',
- value: 2,
- })} />
- act('char_pref', {
- char_pref: 'extreme_pref',
- value: 0,
- })} />
-
- {extreme_pref ? (
-
+
+
+
+
act('char_pref', {
- char_pref: 'extreme_harm',
+ char_pref: 'erp_pref',
value: 1,
})} />
act('char_pref', {
- char_pref: 'extreme_harm',
+ char_pref: 'erp_pref',
+ value: 2,
+ })} />
+ act('char_pref', {
+ char_pref: 'erp_pref',
value: 0,
})} />
- ) : (null)}
-
-
+
+ act('char_pref', {
+ char_pref: 'noncon_pref',
+ value: 1,
+ })} />
+ act('char_pref', {
+ char_pref: 'noncon_pref',
+ value: 2,
+ })} />
+ act('char_pref', {
+ char_pref: 'noncon_pref',
+ value: 0,
+ })} />
+
+
+ act('char_pref', {
+ char_pref: 'vore_pref',
+ value: 1,
+ })} />
+ act('char_pref', {
+ char_pref: 'vore_pref',
+ value: 2,
+ })} />
+ act('char_pref', {
+ char_pref: 'vore_pref',
+ value: 0,
+ })} />
+
+
+ act('char_pref', {
+ char_pref: 'unholy_pref',
+ value: 1,
+ })} />
+ act('char_pref', {
+ char_pref: 'unholy_pref',
+ value: 2,
+ })} />
+ act('char_pref', {
+ char_pref: 'unholy_pref',
+ value: 0,
+ })} />
+
+
+ act('char_pref', {
+ char_pref: 'extreme_pref',
+ value: 1,
+ })} />
+ act('char_pref', {
+ char_pref: 'extreme_pref',
+ value: 2,
+ })} />
+ act('char_pref', {
+ char_pref: 'extreme_pref',
+ value: 0,
+ })} />
+
+ {extreme_pref ? (
+
+ act('char_pref', {
+ char_pref: 'extreme_harm',
+ value: 1,
+ })} />
+ act('char_pref', {
+ char_pref: 'extreme_harm',
+ value: 0,
+ })} />
+
+ ) : (null)}
+
+
+
);
};
@@ -514,307 +554,309 @@ const ContentPreferencesTab = (props, context) => {
cum_onto_pref,
} = data;
return (
-
-
- act('pref', {
- pref: 'verb_consent',
- })}
- />
-
-
- act('pref', {
- pref: 'lewd_verb_sounds',
- })}
- />
-
-
- act('pref', {
- pref: 'arousable',
- })}
- />
-
-
- act('pref', {
- pref: 'genital_examine',
- })}
- />
-
-
- act('pref', {
- pref: 'vore_examine',
- })}
- />
-
-
- act('pref', {
- pref: 'medihound_sleeper',
- })}
- />
-
-
- act('pref', {
- pref: 'eating_noises',
- })}
- />
-
-
- act('pref', {
- pref: 'digestion_noises',
- })}
- />
-
-
- act('pref', {
- pref: 'trash_forcefeed',
- })}
- />
-
-
- act('pref', {
- pref: 'forced_fem',
- })}
- />
-
-
- act('pref', {
- pref: 'forced_masc',
- })}
- />
-
-
- act('pref', {
- pref: 'hypno',
- })}
- />
-
-
- act('pref', {
- pref: 'bimbofication',
- })}
- />
-
-
- act('pref', {
- pref: 'breast_enlargement',
- })}
- />
-
-
- act('pref', {
- pref: 'penis_enlargement',
- })}
- />
-
-
- act('pref', {
- pref: 'butt_enlargement',
- })}
- />
-
-
- act('pref', {
- pref: 'belly_inflation',
- })}
- />
-
-
- act('pref', {
- pref: 'never_hypno',
- })}
- />
-
-
- act('pref', {
- pref: 'no_aphro',
- })}
- />
-
-
- act('pref', {
- pref: 'no_ass_slap',
- })}
- />
-
-
- act('pref', {
- pref: 'no_auto_wag',
- })}
- />
-
-
- act('pref', {
- pref: 'chastity_pref',
- })}
- />
-
-
- act('pref', {
- pref: 'stimulation_pref',
- })}
- />
-
-
- act('pref', {
- pref: 'edging_pref',
- })}
- />
-
-
- act('pref', {
- pref: 'cum_onto_pref',
- })}
- />
-
-
+
+
+
+ act('pref', {
+ pref: 'verb_consent',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'lewd_verb_sounds',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'arousable',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'genital_examine',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'vore_examine',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'medihound_sleeper',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'eating_noises',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'digestion_noises',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'trash_forcefeed',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'forced_fem',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'forced_masc',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'hypno',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'bimbofication',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'breast_enlargement',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'penis_enlargement',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'butt_enlargement',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'belly_inflation',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'never_hypno',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'no_aphro',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'no_ass_slap',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'no_auto_wag',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'chastity_pref',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'stimulation_pref',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'edging_pref',
+ })}
+ />
+
+
+ act('pref', {
+ pref: 'cum_onto_pref',
+ })}
+ />
+
+
+
);
};