diff --git a/code/__DEFINES/cooldowns.dm b/code/__DEFINES/cooldowns.dm
index 0cb559b0e8f..0ff525dac5a 100644
--- a/code/__DEFINES/cooldowns.dm
+++ b/code/__DEFINES/cooldowns.dm
@@ -66,6 +66,7 @@
#define MOB_SHARED_COOLDOWN_1 (1<<0)
#define MOB_SHARED_COOLDOWN_2 (1<<1)
#define MOB_SHARED_COOLDOWN_3 (1<<2)
+#define MOB_SHARED_COOLDOWN_BOT_ANNOUNCMENT (1<<3)
//TIMER COOLDOWN MACROS
diff --git a/code/game/communications.dm b/code/game/communications.dm
index 0be3ccf17bc..6d26a977993 100644
--- a/code/game/communications.dm
+++ b/code/game/communications.dm
@@ -129,6 +129,24 @@ GLOBAL_LIST_INIT(reverseradiochannels, list(
"[FREQ_CTF_YELLOW]" = RADIO_CHANNEL_CTF_YELLOW
))
+GLOBAL_LIST_INIT(radiocolors, list(
+ RADIO_CHANNEL_COMMON = "#008000",
+ RADIO_CHANNEL_SCIENCE = "#993399",
+ RADIO_CHANNEL_COMMAND = "#948f02",
+ RADIO_CHANNEL_MEDICAL = "#337296",
+ RADIO_CHANNEL_ENGINEERING = "#fb5613",
+ RADIO_CHANNEL_SECURITY = "#a30000",
+ RADIO_CHANNEL_CENTCOM = "#686868",
+ RADIO_CHANNEL_SYNDICATE = "#6d3f40",
+ RADIO_CHANNEL_SUPPLY = "#a8732b",
+ RADIO_CHANNEL_SERVICE = "#6eaa2c",
+ RADIO_CHANNEL_AI_PRIVATE = "#ff00ff",
+ RADIO_CHANNEL_CTF_RED = "#ff0000",
+ RADIO_CHANNEL_CTF_BLUE = "#0000ff",
+ RADIO_CHANNEL_CTF_GREEN = "#00ff00",
+ RADIO_CHANNEL_CTF_YELLOW = "#d1ba22"
+))
+
/datum/radio_frequency
/// The frequency of this radio frequency. Of course.
var/frequency
diff --git a/code/modules/mob/living/simple_animal/bot/bot_announcement.dm b/code/modules/mob/living/simple_animal/bot/bot_announcement.dm
index eade5a291c4..061e6375088 100644
--- a/code/modules/mob/living/simple_animal/bot/bot_announcement.dm
+++ b/code/modules/mob/living/simple_animal/bot/bot_announcement.dm
@@ -6,10 +6,15 @@
overlay_icon_state = "bg_tech_blue_border"
button_icon = 'icons/obj/machines/wallmounts.dmi'
button_icon_state = "intercom"
- cooldown_time = 10 SECONDS
+ cooldown_time = 5 SECONDS
+ shared_cooldown = MOB_SHARED_COOLDOWN_BOT_ANNOUNCMENT
melee_cooldown_time = 0 SECONDS
/// List of strings to sound effects corresponding to automated messages we can play
var/list/automated_announcements
+ /// Maximum amount of buttons this can have
+ var/max_buttons = 10
+ /// List of buttons that automatically correspond to an announcement and channel
+ var/list/buttons = list()
/datum/action/cooldown/bot_announcement/New(Target, original, list/automated_announcements)
src.automated_announcements = automated_announcements
@@ -29,15 +34,118 @@
return FALSE
return TRUE
-/datum/action/cooldown/bot_announcement/Activate(trigger_flags, atom/target)
- var/picked
- if (length(automated_announcements) > 1)
- picked = tgui_input_list(owner, message = "Choose announcement to make.", title = "Select announcement", items = automated_announcements)
+/datum/action/cooldown/bot_announcement/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "BotAnnouncement", "Select announcement")
+ ui.set_autoupdate(TRUE)
+ ui.open()
+
+/datum/action/cooldown/bot_announcement/ui_state(mob/user)
+ return GLOB.conscious_state
+
+/datum/action/cooldown/bot_announcement/ui_status(mob/user, datum/ui_state/state)
+ . = ..()
+ if(user != owner)
+ return UI_CLOSE
+
+/datum/action/cooldown/bot_announcement/ui_data(mob/user)
+ var/list/data = list()
+
+ var/mob/living/simple_animal/bot/bot_owner = owner
+ if(istype(bot_owner))
+ var/list/channels = list()
+ for(var/channel in bot_owner.internal_radio.channels)
+ channels += channel
+ data["channels"] = channels
else
- picked = pick(automated_announcements)
- if (isnull(picked))
+ data["channels"] = list()
+ var/list/lines = list()
+ for(var/line in automated_announcements)
+ lines += line
+ data["lines"] = lines
+ var/list/button_data = list()
+ for(var/datum/action/cooldown/bot_announcement_shortcut/button as anything in buttons)
+ button_data += list(list(
+ "name" = button.name,
+ "channel" = button.channel
+ ))
+ data["button_data"] = button_data
+ data["cooldown_left"] = next_use_time - world.time
+ return data
+
+/datum/action/cooldown/bot_announcement/Destroy()
+ QDEL_LIST(buttons)
+ return ..()
+
+/datum/action/cooldown/bot_announcement/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
return
- announce(picked)
+
+ switch(action)
+ if("announce")
+ var/picked = params["picked"]
+ var/channel = params["channel"]
+ if(!(picked in automated_announcements))
+ return
+ announce(picked, channel)
+ StartCooldown()
+ return TRUE
+ if("set_button")
+ if(length(buttons) >= max_buttons)
+ return
+ var/picked = params["picked"]
+ var/channel = params["channel"]
+ if(!(picked in automated_announcements))
+ return
+ create_shortcut(picked, channel)
+ return TRUE
+ if("remove_button")
+ var/index = text2num(params["index"])
+ if(!index)
+ return
+ if(index < 1 || index > length(buttons))
+ return
+ var/datum/action/button = buttons[index]
+ qdel(button)
+ return TRUE
+
+/datum/action/cooldown/bot_announcement/Activate(trigger_flags, atom/target)
+ if (length(automated_announcements) > 1)
+ ui_interact(owner)
+ return
+ else if(length(automated_announcements) == 1)
+ announce(automated_announcements[1])
+ return ..()
+
+/datum/action/cooldown/bot_announcement/proc/create_shortcut(line, channel)
+ var/datum/action/cooldown/bot_announcement_shortcut/shortcut = new(src)
+
+ shortcut.prefix = copytext(line, 1, 4)
+ var/color = GLOB.radiocolors["[channel]"]
+ if(color)
+ shortcut.prefix_color = color
+ shortcut.name = line
+ shortcut.message = line
+ shortcut.channel = channel
+ shortcut.linked = src
+ shortcut.Grant(owner)
+ RegisterSignal(shortcut, COMSIG_QDELETING, PROC_REF(on_shortcut_deleted))
+ buttons += shortcut
+
+/datum/action/cooldown/bot_announcement/proc/on_shortcut_deleted(datum/shortcut)
+ SIGNAL_HANDLER
+ buttons -= shortcut
+
+/datum/action/cooldown/bot_announcement/Grant(mob/granted_to)
+ . = ..()
+ for(var/datum/action/action as anything in buttons)
+ action.Grant(granted_to)
+
+/datum/action/cooldown/bot_announcement/Remove(mob/removed_from)
+ for(var/datum/action/action as anything in buttons)
+ action.Remove(removed_from)
return ..()
/// Speak the provided line on the provided radio channel
@@ -46,10 +154,9 @@
if (!(bot_owner.bot_mode_flags & BOT_MODE_ON))
return
+ bot_owner.say(line)
if (channel && bot_owner.internal_radio.channels[channel])
bot_owner.internal_radio.talk_into(bot_owner, message = line, channel = channel)
- else
- bot_owner.say(line)
if (length(automated_announcements) && !isnull(automated_announcements[line]))
playsound(bot_owner, automated_announcements[line], vol = 50, vary = FALSE)
@@ -62,3 +169,49 @@
if(!(bot_owner.medical_mode_flags & MEDBOT_SPEAK_MODE))
return
return ..()
+
+/datum/action/cooldown/bot_announcement_shortcut
+ desc = "Play a prerecorded message for the benefit of those around you."
+ shared_cooldown = MOB_SHARED_COOLDOWN_BOT_ANNOUNCMENT
+ melee_cooldown_time = 0 SECONDS
+ background_icon_state = "bg_tech_blue"
+ overlay_icon_state = "bg_tech_blue_border"
+ button_icon = 'icons/obj/machines/wallmounts.dmi'
+ button_icon_state = "intercom"
+ /// The prefix that appears on this button
+ var/prefix
+ /// The color of the prefix that appears on the button
+ var/prefix_color = "#ffffff"
+ /// The prefix icon that's rendered on the button
+ var/mutable_appearance/prefix_icon
+ /// The message to send when this button is clicked
+ var/message
+ /// The channel to send this to when clicked
+ var/channel
+
+ /// The linked bot_announcement ability
+ var/datum/action/cooldown/bot_announcement/linked
+
+/datum/action/cooldown/bot_announcement_shortcut/Destroy()
+ linked = null
+ return ..()
+
+/datum/action/cooldown/bot_announcement_shortcut/apply_button_overlay(atom/movable/screen/movable/action_button/current_button, force)
+ . = ..()
+ if(prefix_icon)
+ current_button.cut_overlay(prefix_icon)
+ if(!prefix)
+ return
+ if(!prefix_icon)
+ prefix_icon = mutable_appearance()
+ prefix_icon.maptext = MAPTEXT_SPESSFONT("[prefix]")
+ prefix_icon.maptext_x = -4
+ prefix_icon.maptext_y = 8
+ current_button.add_overlay(prefix_icon)
+
+/datum/action/cooldown/bot_announcement_shortcut/Activate(atom/target)
+ if(!message || !linked)
+ return
+ cooldown_time = linked.cooldown_time
+ linked.announce(message, channel)
+ return ..()
diff --git a/tgui/packages/tgui/interfaces/BotAnnouncement.tsx b/tgui/packages/tgui/interfaces/BotAnnouncement.tsx
new file mode 100644
index 00000000000..3292e81a0e7
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/BotAnnouncement.tsx
@@ -0,0 +1,270 @@
+import { createSearch } from 'common/string';
+import { useState } from 'react';
+
+import { useBackend } from '../backend';
+import {
+ Box,
+ Button,
+ Dropdown,
+ Icon,
+ Input,
+ Section,
+ Stack,
+ Tabs,
+} from '../components';
+import { RADIO_CHANNELS } from '../constants';
+import { Window } from '../layouts';
+
+type ButtonData = {
+ name: string;
+ channel: string;
+};
+
+type ButtonDataWithId = {
+ button: ButtonData;
+ index: number;
+};
+
+type StringWithId = {
+ string: string;
+ index: number;
+};
+
+type BotAnnouncementData = {
+ channels: string[];
+ lines: string[];
+ button_data: ButtonData[];
+ cooldown_left: number;
+};
+
+enum TAB {
+ Announcements,
+ Shortcuts,
+}
+
+export const BotAnnouncement = (props) => {
+ const { act, data } = useBackend();
+ const { channels, lines, button_data, cooldown_left } = data;
+
+ const [tab, setTab] = useState(TAB.Announcements);
+ const [selectedChannel, setSelectedChannel] = useState(null);
+ const [selectedLine, setSelectedLine] = useState(null);
+ const [selectedButton, setSelectedButton] = useState(null);
+ const [search, setSearch] = useState('');
+
+ let filteredLines: StringWithId[] = lines.map((val, index) => ({
+ string: val,
+ index,
+ }));
+ let filteredShortcuts: ButtonDataWithId[] = button_data.map((val, index) => ({
+ button: val,
+ index,
+ }));
+
+ if (search !== '') {
+ if (tab === TAB.Announcements) {
+ const lineSearch = createSearch(
+ search,
+ (item: StringWithId) => item.string,
+ );
+ filteredLines = filteredLines.filter(lineSearch);
+ } else {
+ const buttonSearch = createSearch(
+ search,
+ (item: ButtonDataWithId) => item.button.name,
+ );
+ filteredShortcuts = filteredShortcuts.filter(buttonSearch);
+ }
+ }
+
+ return (
+
+
+
+
+ {
+ setSearch('');
+ setTab(TAB.Announcements);
+ }}
+ >
+ Announcements
+
+ {
+ setSearch('');
+ setTab(TAB.Shortcuts);
+ }}
+ >
+ Shortcuts
+
+
+
+
+ {tab === TAB.Announcements && (
+
+ {filteredLines.map((val) => (
+
+
+
+ ))}
+
+ )}
+ {tab === TAB.Shortcuts && (
+
+ {filteredShortcuts.map((val) => (
+
+
+
+ ))}
+
+ )}
+
+
+
+
+ setSearch(newValue)}
+ fluid
+ autoFocus
+ placeholder="Search..."
+ />
+
+
+
+ {tab === TAB.Announcements ? (
+
+ {
+ if (value === 'No radio channel') {
+ setSelectedChannel(null);
+ } else {
+ setSelectedChannel(value);
+ }
+ }}
+ />
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};