Upgrades bot voice lines (#83847)

<!-- Write **BELOW** The Headers and **ABOVE** The comments else it may
not be viewable. -->
<!-- You can view Contributing.MD for a detailed description of the pull
request process. -->

## About The Pull Request

<!-- Describe The Pull Request. Please be sure every change is
documented or this can delay review and even discourage maintainers from
merging your PR! -->
As the title says. Upgrades the menu and allows you to create action
button shortcuts.


![image](https://github.com/tgstation/tgstation/assets/37270891/902d5e2b-de50-463e-a566-062ee24cc138)

![image](https://github.com/tgstation/tgstation/assets/37270891/0ce025e3-2f38-4013-8cde-00af12584086)

![image](https://github.com/tgstation/tgstation/assets/37270891/88ad4a30-dd65-4275-912c-b341264d77fd)

Additionally, bots can now select the radio channel if they have one.

Lowers the cooldown for voice lines.

## Why It's Good For The Game

<!-- Argue for the merits of your changes and how they benefit the game,
especially if they are controversial and/or far reaching. If you can't
actually explain WHY what you are doing will improve the game, then it
probably isn't good for the game in the first place. -->
Being able to more conveniently send a voice line by pressing a button
without needing to open up an interface and navigate to the correct line
will make using these a lot easier in relevant situations.

Voice line cooldowns are lowered a bit just to make voice lines easier
to use in relevant situations.
I.e. 'Extinguishing fire' -> 5 seconds later -> 'Fire extinguished'

Abuse should generally just be punished by the admins, the cooldown
doesn't need to be too lengthy.

## Changelog

<!-- If your PR modifies aspects of the game that can be concretely
observed by players or admins you should add a changelog. If your change
does NOT meet this description, remove this section. Be sure to properly
mark your PRs to prevent unnecessary GBP loss. You can read up on GBP
and it's effects on PRs in the tgstation guides for contributors. Please
note that maintainers freely reserve the right to remove and add tags
should they deem it appropriate. You can attempt to finagle the system
all you want, but it's best to shoot for clear communication right off
the bat. -->

🆑
qol: Bots can now bind voice lines to an action slot. Lowers cooldown
for all bot voicelines to 5 seconds.
/🆑

<!-- Both 🆑's are required for the changelog to work! You can put
your name to the right of the first 🆑 if you want to overwrite your
GitHub username as author ingame. -->
<!-- You can use multiple of the same prefix (they're only used for the
icon ingame) and delete the unneeded ones. Despite some of the tags,
changelogs should generally represent how a player might be affected by
the changes rather than a summary of the PR's contents. -->

---------

Co-authored-by: Watermelon914 <3052169-Watermelon914@users.noreply.gitlab.com>
This commit is contained in:
Watermelon914
2024-06-13 13:06:49 +00:00
committed by GitHub
co-authored by Watermelon914
parent 0eed0339b8
commit 17d39f7af0
4 changed files with 452 additions and 10 deletions
+1
View File
@@ -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
+18
View File
@@ -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
@@ -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("<span style='color: [prefix_color]; text-align: right; '>[prefix]</span>")
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 ..()
@@ -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<BotAnnouncementData>();
const { channels, lines, button_data, cooldown_left } = data;
const [tab, setTab] = useState(TAB.Announcements);
const [selectedChannel, setSelectedChannel] = useState<null | string>(null);
const [selectedLine, setSelectedLine] = useState<null | number>(null);
const [selectedButton, setSelectedButton] = useState<null | number>(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 (
<Window width={650} height={500}>
<Window.Content>
<Section fitted m={0}>
<Tabs>
<Tabs.Tab
selected={tab === TAB.Announcements}
onClick={() => {
setSearch('');
setTab(TAB.Announcements);
}}
>
Announcements
</Tabs.Tab>
<Tabs.Tab
selected={tab === TAB.Shortcuts}
onClick={() => {
setSearch('');
setTab(TAB.Shortcuts);
}}
>
Shortcuts
</Tabs.Tab>
</Tabs>
</Section>
<Section m={0} scrollable fill maxHeight="350px">
{tab === TAB.Announcements && (
<Stack vertical zebra>
{filteredLines.map((val) => (
<Stack.Item key={val.string}>
<Button
py={1}
color={selectedLine === val.index ? 'green' : 'transparent'}
fluid
onClick={() => setSelectedLine(val.index)}
minHeight="32px"
>
<Stack>
<Stack.Item>
<Icon name="volume-high" />
</Stack.Item>
<Stack.Item>{val.string}</Stack.Item>
</Stack>
</Button>
</Stack.Item>
))}
</Stack>
)}
{tab === TAB.Shortcuts && (
<Stack vertical zebra>
{filteredShortcuts.map((val) => (
<Stack.Item key={val.index}>
<Button
py={1}
color={
selectedButton === val.index ? 'green' : 'transparent'
}
fluid
onClick={() => setSelectedButton(val.index)}
minHeight="32px"
>
<Stack>
<Stack.Item>
<Icon name="volume-high" />
</Stack.Item>
<Stack.Item grow>{val.button.name}</Stack.Item>
<Stack.Item align="center">
<Box
color={
selectedButton === val.index
? 'white'
: RADIO_CHANNELS.find(
(channel) =>
channel.name === val.button.channel,
)?.color
}
>
{val.button.channel || 'No radio channel'}
</Box>
</Stack.Item>
</Stack>
</Button>
</Stack.Item>
))}
</Stack>
)}
</Section>
<Section>
<Stack vertical>
<Stack.Item>
<Input
onInput={(event, newValue) => setSearch(newValue)}
fluid
autoFocus
placeholder="Search..."
/>
</Stack.Item>
<Stack.Item>
<Stack align="center">
{tab === TAB.Announcements ? (
<Stack.Item grow>
<Dropdown
options={['No radio channel', ...channels]}
displayText={
selectedChannel === null
? 'No radio channel'
: selectedChannel
}
width="100%"
selected={selectedChannel}
onSelected={(value) => {
if (value === 'No radio channel') {
setSelectedChannel(null);
} else {
setSelectedChannel(value);
}
}}
/>
</Stack.Item>
) : (
<Stack.Item grow />
)}
<Stack.Item>
<Button
textAlign="center"
onClick={() => {
if (tab === TAB.Announcements) {
if (selectedLine === null) {
return;
}
act('set_button', {
picked: lines[selectedLine],
channel: selectedChannel,
});
} else {
if (selectedButton === null) {
return;
}
act('remove_button', {
index: selectedButton + 1,
});
}
}}
color={tab === TAB.Announcements ? 'default' : 'red'}
minWidth="96px"
>
{tab === TAB.Announcements
? 'Make Shortcut'
: 'Delete Shortcut'}
</Button>
</Stack.Item>
<Stack.Item>
<Button
textAlign="center"
onClick={() => {
if (tab === TAB.Announcements) {
if (selectedLine === null) {
return;
}
act('announce', {
picked: lines[selectedLine],
channel: selectedChannel,
});
} else {
if (selectedButton === null) {
return;
}
const button = button_data[selectedButton];
act('announce', {
picked: button.name,
channel: button.channel,
});
}
}}
disabled={cooldown_left > 0}
minWidth="96px"
>
Play{' '}
{cooldown_left > 0
? `(${Math.round(cooldown_left / 10)}s)`
: ''}
</Button>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Section>
</Window.Content>
</Window>
);
};