diff --git a/code/_onclick/hud/ai.dm b/code/_onclick/hud/ai.dm index f2ef2e73a2..419de492ef 100644 --- a/code/_onclick/hud/ai.dm +++ b/code/_onclick/hud/ai.dm @@ -84,7 +84,7 @@ if(..()) return var/mob/living/silicon/ai/AI = usr - AI.announcement() + AI.ai_announcement() /atom/movable/screen/ai/call_shuttle name = "Call Emergency Shuttle" diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 0413502edc..4eb836dab5 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -974,10 +974,11 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( addtimer(CALLBACK(SSassets.transport, TYPE_PROC_REF(/datum/asset_transport, send_assets_slow), src, SSassets.transport.preload), 5 SECONDS) #if (PRELOAD_RSC == 0) - for (var/name in GLOB.vox_sounds) - var/file = GLOB.vox_sounds[name] - Export("##action=load_rsc", file) - stoplag() + for (var/type in GLOB.vox_types) + for(var/word in GLOB.vox_types[type]) + var/file = GLOB.vox_types[type][word] + Export("##action=load_rsc", file) + stoplag() #endif diff --git a/code/modules/integrated_electronics/subtypes/output.dm b/code/modules/integrated_electronics/subtypes/output.dm index 9fa816e0d0..518672aed1 100644 --- a/code/modules/integrated_electronics/subtypes/output.dm +++ b/code/modules/integrated_electronics/subtypes/output.dm @@ -226,15 +226,22 @@ spawn_flags = IC_SPAWN_RESEARCH /obj/item/integrated_circuit/output/sound/vox - name = "ai vox sound circuit" + name = "Female ai vox sound circuit" desc = "Takes a sound name as an input, and will play said sound when pulsed. This circuit is often found in AI announcement systems." spawn_flags = IC_SPAWN_RESEARCH + var/voice_type = "Female" /obj/item/integrated_circuit/output/sound/vox/Initialize(mapload) - .= ..() - sounds = GLOB.vox_sounds + . = ..() + sounds = GLOB.vox_types[voice_type] extended_desc = "The first input pin determines which sound is used. It uses the AI Vox Broadcast word list. So either experiment to find words that work, or ask the AI to help in figuring them out. The second pin determines the volume of sound that is played, and the third determines if the frequency of the sound will vary with each activation." +/obj/item/integrated_circuit/output/sound/vox/male + name = "Male ai vox sound circuit" + desc = "Takes a sound name as an input, and will play said sound when pulsed. This circuit is often found in AI announcement systems." + spawn_flags = IC_SPAWN_RESEARCH + voice_type = "Male" + /obj/item/integrated_circuit/output/text_to_speech name = "text-to-speech circuit" desc = "Takes any string as an input and will make the device say the string when pulsed." diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm index 65288498ba..f5cf7e1d37 100644 --- a/code/modules/mob/living/silicon/ai/ai.dm +++ b/code/modules/mob/living/silicon/ai/ai.dm @@ -101,6 +101,10 @@ var/datum/robot_control/robot_control /// Station alert datum for showing alerts UI var/datum/station_alert/alert_control + /// Announcement UI + var/datum/ai_announcement/ai_announcement + /// Lists possible spoken words for announcements + var/datum/announcement_help/announcement_help ///remember AI's last location var/atom/lastloc interaction_range = INFINITY @@ -204,6 +208,8 @@ QDEL_NULL(doomsday_device) QDEL_NULL(robot_control) QDEL_NULL(alert_control) + QDEL_NULL(ai_announcement) + QDEL_NULL(announcement_help) QDEL_NULL(aiMulti) QDEL_NULL(aiPDA) malfhack = null diff --git a/code/modules/mob/living/silicon/ai/ai_announcement.dm b/code/modules/mob/living/silicon/ai/ai_announcement.dm new file mode 100644 index 0000000000..4165dec6bd --- /dev/null +++ b/code/modules/mob/living/silicon/ai/ai_announcement.dm @@ -0,0 +1,59 @@ +#ifdef AI_VOX +/mob/living/silicon/ai/verb/ai_announcement() + set name = "Vox Announcement" + set desc = "Make an audible announcement!" + set category = "AI Commands" + + if(incapacitated()) + return + + if(!ai_announcement) + ai_announcement = new(src) + + ai_announcement.ui_interact(src) + +/datum/ai_announcement + var/mob/living/silicon/ai/owner + +/datum/ai_announcement/New(mob/living/silicon/ai/new_owner) + if(!istype(new_owner)) + qdel(src) + owner = new_owner + +/datum/ai_announcement/ui_status(mob/living/silicon/ai/user) + if(owner == user && !owner.incapacitated()) + return ..() + return UI_CLOSE + +/datum/ai_announcement/ui_state(mob/user) + return GLOB.always_state + +/datum/ai_announcement/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "AIAnnouncement") + ui.open() + +/datum/ai_announcement/ui_data(mob/user) + var/list/data = ..() + data["last_announcement"] = owner.last_announcement + return data + +/datum/ai_announcement/ui_static_data(mob/user) + var/list/data = ..() + data["vox_types"] = GLOB.vox_types + return data + +/datum/ai_announcement/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + switch(action) + if("announce") + var/vox_type = params["vox_type"] + if(!vox_type) + return + var/to_speak = params["to_speak"] + if(!to_speak) + return + owner.announcement(vox_type, to_speak) + ui.close() +#endif diff --git a/code/modules/mob/living/silicon/ai/announcement_help.dm b/code/modules/mob/living/silicon/ai/announcement_help.dm new file mode 100644 index 0000000000..f22b3ce4cb --- /dev/null +++ b/code/modules/mob/living/silicon/ai/announcement_help.dm @@ -0,0 +1,54 @@ +#ifdef AI_VOX +/mob/living/silicon/ai/verb/announcement_help() + + set name = "Announcement Help" + set desc = "Display a list of vocal words to announce to the crew." + set category = "AI Commands" + + if(incapacitated()) + return + + if(!announcement_help) + announcement_help = new(src) + + announcement_help.ui_interact(src) + +/datum/announcement_help + var/mob/living/silicon/ai/owner + +/datum/announcement_help/New(mob/living/silicon/ai/new_owner) + if(!istype(new_owner)) + qdel(src) + owner = new_owner + +/datum/announcement_help/ui_status(mob/living/silicon/ai/user) + if(owner == user && !owner.incapacitated()) + return ..() + return UI_CLOSE + +/datum/announcement_help/ui_state(mob/user) + return GLOB.always_state + +/datum/announcement_help/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "AnnouncementHelp") + ui.open() + +/datum/announcement_help/ui_static_data(mob/user) + var/list/data = ..() + data["vox_types"] = GLOB.vox_types + return data + +/datum/announcement_help/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + switch(action) + if("say_word") + var/vox_type = params["vox_type"] + if(!vox_type) + return + var/to_speak = params["to_speak"] + if(!to_speak) + return + play_vox_word(to_speak, null, owner, vox_type) +#endif diff --git a/code/modules/mob/living/silicon/ai/say.dm b/code/modules/mob/living/silicon/ai/say.dm index e23eb842ea..2a05266587 100644 --- a/code/modules/mob/living/silicon/ai/say.dm +++ b/code/modules/mob/living/silicon/ai/say.dm @@ -79,59 +79,23 @@ // Make sure that the code compiles with AI_VOX undefined #ifdef AI_VOX -#define VOX_DELAY 600 -/mob/living/silicon/ai/verb/announcement_help() - - set name = "Announcement Help" - set desc = "Display a list of vocal words to announce to the crew." - set category = "AI Commands" - - if(incapacitated()) - return - - var/dat = {" - WARNING: Misuse of the announcement system will get you job banned.

- Here is a list of words you can type into the 'Announcement' button to create sentences to vocally announce to everyone on the same level at you.
-
- "} - - var/index = 0 - for(var/word in GLOB.vox_sounds) - index++ - dat += "[capitalize(word)]" - if(index != GLOB.vox_sounds.len) - dat += " / " - - var/datum/browser/popup = new(src, "announce_help", "Announcement Help", 500, 400) - popup.set_content(dat) - popup.open() - - -/mob/living/silicon/ai/proc/announcement() +#define VOX_DELAY 1 MINUTES +/mob/living/silicon/ai/proc/announcement(voxType = "Female", message) var/static/announcing_vox = 0 // Stores the time of the last announcement if(announcing_vox > world.time) - to_chat(src, "Please wait [DisplayTimeText(announcing_vox - world.time)].") + to_chat(src, span_notice("Please wait [DisplayTimeText(announcing_vox - world.time)].")) return - var/message = input(src, "WARNING: Misuse of this verb can result in you being job banned. More help is available in 'Announcement Help'", "Announcement", src.last_announcement) as text - last_announcement = message - var/voxType = input(src, "Male or female VOX?", "VOX-gender") in list("male", "female") - - if(!message || announcing_vox > world.time) + if(!message || !GLOB.vox_types[voxType]) return if(incapacitated()) return if(control_disabled) - to_chat(src, "Wireless interface disabled, unable to interact with announcement PA.") + to_chat(src, span_warning("Wireless interface disabled, unable to interact with announcement PA.")) return var/list/words = splittext(trim(message), " ") @@ -145,13 +109,11 @@ if(!word) words -= word continue - if(!GLOB.vox_sounds[word] && voxType == "female") - incorrect_words += word - if(!GLOB.vox_sounds_male[word] && voxType == "male") + if(!GLOB.vox_types[voxType][word]) incorrect_words += word if(incorrect_words.len) - to_chat(src, "These words are not available on the announcement system: [english_list(incorrect_words)].") + to_chat(src, span_notice("These words are not available on the announcement system: [english_list(incorrect_words)].")) return announcing_vox = world.time + VOX_DELAY @@ -162,18 +124,12 @@ play_vox_word(word, src.z, null, voxType) -/proc/play_vox_word(word, z_level, mob/only_listener, voxType = "female") +/proc/play_vox_word(word, z_level, mob/only_listener, voxType = "Female") word = lowertext(word) - if( (GLOB.vox_sounds[word] && voxType == "female") || (GLOB.vox_sounds_male[word] && voxType == "male") ) - - var/sound_file - - if(voxType == "female") - sound_file = GLOB.vox_sounds[word] - else - sound_file = GLOB.vox_sounds_male[word] + var/sound_file = LAZYACCESSASSOC(GLOB.vox_types, voxType, word) + if(sound_file) var/sound/voice = sound(sound_file, wait = 1, channel = CHANNEL_VOX) voice.status = SOUND_STREAM diff --git a/code/modules/mob/living/silicon/ai/vox_sounds.dm b/code/modules/mob/living/silicon/ai/vox_sounds.dm index 057561562b..8959d49e9a 100644 --- a/code/modules/mob/living/silicon/ai/vox_sounds.dm +++ b/code/modules/mob/living/silicon/ai/vox_sounds.dm @@ -5,9 +5,11 @@ // Regex for collecting a list of ogg files // (([a-zA-Z,.]+)\.ogg) -// For vim -// :%s/\(\(.*\)\.ogg\)/"\2" = 'sound\/vox_fem\/\1',/g -GLOBAL_LIST_INIT(vox_sounds, list("abduction" = 'sound/vox_fem/abduction.ogg', +GLOBAL_LIST_INIT(vox_types, list( + // For vim + // :%s/\(\(.*\)\.ogg\)/"\2" = 'sound\/vox_fem\/\1',/g + "Female" = list( +"abduction" = 'sound/vox_fem/abduction.ogg', "abortions" = 'sound/vox_fem/abortions.ogg', "above" = 'sound/vox_fem/above.ogg', "abstain" = 'sound/vox_fem/abstain.ogg', @@ -974,11 +976,11 @@ GLOBAL_LIST_INIT(vox_sounds, list("abduction" = 'sound/vox_fem/abduction.ogg', "z" = 'sound/vox_fem/z.ogg', "zombie" = 'sound/vox_fem/zombie.ogg', "zone" = 'sound/vox_fem/zone.ogg', -"zulu" = 'sound/vox_fem/zulu.ogg')) - -//for vim -// :%s/\(\(.*\)\.ogg\)/"\2" = 'sound\/vox\/\1',/g -GLOBAL_LIST_INIT(vox_sounds_male, list("," = 'sound/vox/_comma.ogg', +"zulu" = 'sound/vox_fem/zulu.ogg'), + //for vim + // :%s/\(\(.*\)\.ogg\)/"\2" = 'sound\/vox\/\1',/g + "Male" = list( +"," = 'sound/vox/_comma.ogg', "." = 'sound/vox/_period.ogg', "a" = 'sound/vox/a.ogg', "accelerating" = 'sound/vox/accelerating.ogg', @@ -1605,5 +1607,6 @@ GLOBAL_LIST_INIT(vox_sounds_male, list("," = 'sound/vox/_comma.ogg', "yourself" = 'sound/vox/yourself.ogg', "zero" = 'sound/vox/zero.ogg', "zone" = 'sound/vox/zone.ogg', -"zulu" = 'sound/vox/zulu.ogg',)) +"zulu" = 'sound/vox/zulu.ogg', +))) #endif diff --git a/tgstation.dme b/tgstation.dme index 9e2969580c..ab75f4b2c6 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -2833,8 +2833,10 @@ #include "code\modules\mob\living\silicon\silicon_defense.dm" #include "code\modules\mob\living\silicon\silicon_movement.dm" #include "code\modules\mob\living\silicon\ai\ai.dm" +#include "code\modules\mob\living\silicon\ai\ai_announcement.dm" #include "code\modules\mob\living\silicon\ai\ai_defense.dm" #include "code\modules\mob\living\silicon\ai\ai_portrait_picker.dm" +#include "code\modules\mob\living\silicon\ai\announcement_help.dm" #include "code\modules\mob\living\silicon\ai\death.dm" #include "code\modules\mob\living\silicon\ai\emote.dm" #include "code\modules\mob\living\silicon\ai\examine.dm" diff --git a/tgui/packages/tgui/interfaces/AIAnnouncement.js b/tgui/packages/tgui/interfaces/AIAnnouncement.js new file mode 100644 index 0000000000..9545a677d1 --- /dev/null +++ b/tgui/packages/tgui/interfaces/AIAnnouncement.js @@ -0,0 +1,88 @@ +import { filter } from 'common/collections'; +import { useBackend } from '../backend'; +import { Button, Icon, Input, Section, Stack, Tabs } from '../components'; +import { useLocalState } from '../backend'; +import { Window } from '../layouts'; + +export const AIAnnouncement = (props, context) => { + const { act, data } = useBackend(context); + const { + last_announcement, + vox_types = {}, + } = data; + + const [ + current_page, + set_page, + ] = useLocalState(context, 'current_page', 0); + + const [ + announcement_input, + set_announcement_input, + ] = useLocalState(context, 'announcement_input', last_announcement); + + // I love `Object`s!! + const words_filtered = Object.keys(vox_types[Object.keys(vox_types)[current_page]]); + + const search_split = announcement_input.split(" ").filter(element => element); + + const missing_words = search_split.map(element => words_filtered.includes(element) ? null : element).filter(element => element); + + return ( + + +
+ + + WARNING: Misuse of this verb can result in you being job banned. More help is available in 'Announcement Help' + + + + { + Object.keys(vox_types).map((vox_type, index) => ( + set_page(index)} + selected={current_page === index}> + {vox_type} + + )) + } + + + + + + set_announcement_input(value)} + onEnter={(e, value) => act("announce", { + "vox_type": Object.keys(vox_types)[current_page], + "to_speak": value, + })} + /> + + +
+
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/AnnouncementHelp.js b/tgui/packages/tgui/interfaces/AnnouncementHelp.js new file mode 100644 index 0000000000..1869a21eab --- /dev/null +++ b/tgui/packages/tgui/interfaces/AnnouncementHelp.js @@ -0,0 +1,100 @@ +import { filter } from 'common/collections'; +import { flow } from 'common/fp'; +import { createSearch } from 'common/string'; +import { useBackend } from '../backend'; +import { Button, Collapsible, Icon, Input, Section, Stack, Tabs } from '../components'; +import { useLocalState } from '../backend'; +import { Window } from '../layouts'; + +export const AnnouncementHelp = (props, context) => { + const { act, data } = useBackend(context); + const { vox_types = {} } = data; + + const [ + current_page, + set_page, + ] = useLocalState(context, 'current_page', 0); + + const [ + search_text, + set_search_text, + ] = useLocalState(context, 'search_text', ''); + + // I love `Object`s!! + const words_filtered = prepare_search(Object.keys(vox_types[Object.keys(vox_types)[current_page]]), search_text); + + return ( + + +
+ + + + WARNING: Misuse of the announcement system will get you job banned.

+ Here is a list of words you can type into the 'Announcement' button to create sentences to vocally announce to everyone on the same level at you.
+
  • You can also click on the word to PREVIEW it.
  • +
  • You can only say 30 words for every announcement.
  • +
  • Do not use punctuation as you would normally, if you want a pause you can use the full stop and comma characters by separating them with spaces, like so: 'Alpha . Test , Bravo'.
  • +
  • Numbers are in word format, e.g. eight, sixty, etc
  • +
  • Sound effects begin with an 's' before the actual word, e.g. scensor
  • +

+
+
+ + + { + Object.keys(vox_types).map((vox_type, index) => ( + set_page(index)} + selected={current_page === index}> + {vox_type} + + )) + } + + + + + + + + + set_search_text(value)} + /> + + + + + {words_filtered.map(nestedKey => ( + + ))} + +
+
+
+
+ ); +}; + +export const prepare_search = (words_filtered, search_text = '') => { + const search = createSearch(search_text, + word_soup => word_soup); + return flow([ + search_text && filter(search), + ])(words_filtered); +};