diff --git a/code/_globalvars/lists/poll_ignore.dm b/code/_globalvars/lists/poll_ignore.dm index 9b998e02b16..330174f266e 100644 --- a/code/_globalvars/lists/poll_ignore.dm +++ b/code/_globalvars/lists/poll_ignore.dm @@ -20,7 +20,8 @@ #define POLL_IGNORE_SPLITPERSONALITY "split_personality" #define POLL_IGNORE_CONTRACTOR_SUPPORT "contractor_support" #define POLL_IGNORE_ACADEMY_WIZARD "academy_wizard" -#define POLL_IGNORE_CORTICAL_BORER "cortical_borer" //Skyrat Edit Start: Cortical Borer +#define POLL_IGNORE_CORTICAL_BORER "cortical_borer" // SKYRAT EDIT - Cortical Borer +#define POLL_IGNORE_PAI "pai" //Skyrat Edit Start: Cortical Borer GLOBAL_LIST_INIT(poll_ignore_desc, list( @@ -45,6 +46,7 @@ GLOBAL_LIST_INIT(poll_ignore_desc, list( POLL_IGNORE_CONTRACTOR_SUPPORT = "Contractor Support Unit", POLL_IGNORE_ACADEMY_WIZARD = "Academy Wizard Defender", POLL_IGNORE_CORTICAL_BORER = "Cortical Borer", + POLL_IGNORE_PAI = "Personal AI" )) //Skyrat Edit Stop: Cortical Borer GLOBAL_LIST_INIT(poll_ignore, init_poll_ignore()) diff --git a/code/_onclick/hud/pai.dm b/code/_onclick/hud/pai.dm index c779e81f618..79411660a47 100644 --- a/code/_onclick/hud/pai.dm +++ b/code/_onclick/hud/pai.dm @@ -21,7 +21,7 @@ if(!..()) return var/mob/living/silicon/pai/pAI = usr - pAI.paiInterface() + pAI.ui_interact(pAI) /atom/movable/screen/pai/shell name = "Toggle Holoform" diff --git a/code/controllers/subsystem/pai.dm b/code/controllers/subsystem/pai.dm index 703405f3331..3171a27ebbf 100644 --- a/code/controllers/subsystem/pai.dm +++ b/code/controllers/subsystem/pai.dm @@ -4,195 +4,157 @@ SUBSYSTEM_DEF(pai) flags = SS_NO_INIT|SS_NO_FIRE var/list/candidates = list() - var/ghost_spam = FALSE - var/spam_delay = 100 + var/request_spam = FALSE + var/submit_spam = FALSE var/list/pai_card_list = list() -/datum/controller/subsystem/pai/Topic(href, href_list[]) - if(href_list["download"]) - var/datum/pai_candidate/candidate = locate(href_list["candidate"]) in candidates - var/obj/item/paicard/card = locate(href_list["device"]) in pai_card_list - if(card.pai) - return - if(istype(card, /obj/item/paicard) && istype(candidate, /datum/pai_candidate)) - if(check_ready(candidate) != candidate) - return FALSE - var/mob/living/silicon/pai/pai = new(card) - if(!candidate.name) - pai.name = pick(GLOB.ninja_names) - else - pai.name = candidate.name - pai.real_name = pai.name - pai.key = candidate.key +/datum/pai_candidate + var/comments + var/description + var/key + var/name + var/ready = FALSE - card.setPersonality(pai) - - candidates -= candidate - usr << browse(null, "window=findPai") - - if(href_list["new"]) - var/datum/pai_candidate/candidate = locate(href_list["candidate"]) in candidates - var/option = href_list["option"] - var/t = "" - - switch(option) - if("name") - t = sanitize_name(stripped_input(usr, "Enter a name for your pAI", "pAI Name", candidate.name, MAX_NAME_LEN),allow_numbers = TRUE) - if(t) - candidate.name = t - if("desc") - t = stripped_multiline_input(usr, "Enter a description for your pAI", "pAI Description", candidate.description, MAX_MESSAGE_LEN) - if(t) - candidate.description = t - if("role") - t = stripped_input(usr, "Enter a role for your pAI", "pAI Role", candidate.role, MAX_MESSAGE_LEN) - if(t) - candidate.role = t - if("ooc") - t = stripped_multiline_input(usr, "Enter any OOC comments", "pAI OOC Comments", candidate.comments, MAX_MESSAGE_LEN) - if(t) - candidate.comments = t - if("save") - candidate.savefile_save(usr) - if("load") - candidate.savefile_load(usr) - //In case people have saved unsanitized stuff. - if(candidate.name) - candidate.name = copytext_char(candidate.name,1,MAX_NAME_LEN) - if(candidate.description) - candidate.description = copytext_char(candidate.description,1,MAX_MESSAGE_LEN) - if(candidate.role) - candidate.role = copytext_char(candidate.role,1,MAX_MESSAGE_LEN) - if(candidate.comments) - candidate.comments = copytext_char(candidate.comments,1,MAX_MESSAGE_LEN) - - if("submit") - if(candidate) - candidate.ready = TRUE - for(var/obj/item/paicard/p in pai_card_list) - if(!p.pai) - p.alertUpdate() - usr << browse(null, "window=paiRecruit") - return - recruitWindow(usr) - -/datum/controller/subsystem/pai/proc/recruitWindow(mob/M) - var/datum/pai_candidate/candidate - for(var/datum/pai_candidate/c in candidates) - if(c.key == M.key) - candidate = c - if(!candidate) - candidate = new /datum/pai_candidate() - candidate.key = M.key - candidates.Add(candidate) - - - var/dat = "" - dat += {" - - "} - - dat += "

Please configure your pAI personality's options. Remember, what you enter here could determine whether or not the user requesting a personality chooses you!

" - dat += "" - dat += "" - dat += "" - - dat += "" - dat += "" - - dat += "" - dat += "" - - dat += "" - dat += "" - - dat += "
Name:[candidate.name]
\[Edit\]What you plan to call yourself. Suggestions: Any character name you would choose for a station character OR an AI.
Description:[candidate.description]
\[Edit\]What sort of pAI you typically play; your mannerisms, your quirks, etc. This can be as sparse or as detailed as you like.
Preferred Role:[candidate.role]
\[Edit\]Do you like to partner with sneaky social ninjas? Like to help security hunt down thugs? Enjoy watching an engineer's back while he saves the station yet again? This doesn't have to be limited to just station jobs. Pretty much any general descriptor for what you'd like to be doing works here.
OOC Comments:[candidate.comments]
\[Edit\]Anything you'd like to address specifically to the player reading this in an OOC manner. \"I prefer more serious RP.\", \"I'm still learning the interface!\", etc. Feel free to leave this blank if you want.
" - - dat += "
" - dat += "

Submit Personality


" - dat += "Save Personality
" - dat += "Load Personality
" - - M << browse(dat, "window=paiRecruit") - -/datum/controller/subsystem/pai/proc/spam_again() - ghost_spam = FALSE - -/datum/controller/subsystem/pai/proc/check_ready(datum/pai_candidate/C) - if(!C.ready) - return FALSE - for(var/mob/dead/observer/O in GLOB.player_list) - if(O.key == C.key) - return C - return FALSE - -/datum/controller/subsystem/pai/proc/findPAI(obj/item/paicard/p, mob/user) +/** + * Pings ghosts to announce that someone is requesting a pAI + * + * Arguments + * @pai - The card requesting assistance + * @user - The player requesting a pAI +*/ +/datum/controller/subsystem/pai/proc/findPAI(obj/item/paicard/pai, mob/user) if(!(GLOB.ghost_role_flags & GHOSTROLE_SILICONS)) to_chat(user, span_warning("Due to growing incidents of SELF corrupted independent artificial intelligences, freeform personality devices have been temporarily banned in this sector.")) return - if(!ghost_spam) - ghost_spam = TRUE - for(var/mob/dead/observer/G in GLOB.player_list) - if(!G.key) - continue - if(!(ROLE_PAI in G.client.prefs.be_special)) - continue - to_chat(G, span_ghostalert("[user] is requesting a pAI personality! Use the pAI button to submit yourself as one.")) - addtimer(CALLBACK(src, .proc/spam_again), spam_delay) - var/list/available = list() - for(var/datum/pai_candidate/c in SSpai.candidates) - available.Add(check_ready(c)) - var/dat = "" + if(request_spam) + to_chat(user, span_warning("Request sent too recently.")) + return + request_spam = TRUE + playsound(src, 'sound/machines/ping.ogg', 20, TRUE) + to_chat(user, span_notice("You have requested pAI assistance.")) + var/mutable_appearance/alert_overlay = mutable_appearance('icons/obj/aicards.dmi', "pai") + notify_ghosts("[user] is requesting a pAI personality! Use the pAI button to submit yourself as one.", source=user, alert_overlay = alert_overlay, action=NOTIFY_ORBIT, header="pAI Request!", ignore_key = POLL_IGNORE_PAI) + addtimer(CALLBACK(src, .proc/request_again), 10 SECONDS) + return TRUE - dat += {" - - "} - dat += "

Requesting AI personalities from central database... If there are no entries, or if a suitable entry is not listed, check again later as more personalities may be added.

" +/// Opens the TGUI window +/datum/controller/subsystem/pai/ui_interact(mob/user, datum/tgui/ui) + . = ..() + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "PaiSubmit") + ui.open() - dat += "" +/// The data sent to the window. +/datum/controller/subsystem/pai/ui_static_data(mob/user) + . = ..() + var/list/data = list() + /// The matching candidate from search + var/datum/pai_candidate/candidate = check_candidate(user) + if(isnull(candidate)) + return data + data["comments"] = candidate.comments + data["description"] = candidate.description + data["name"] = candidate.name + return data - for(var/datum/pai_candidate/c in available) - dat += "" - dat += "" - dat += "" - dat += "" - dat += "" +/// Actions sent by TGUI +/datum/controller/subsystem/pai/ui_act(action, list/params, datum/tgui/ui) + . = ..() + if(.) + return + /// The matching candidate from search + var/datum/pai_candidate/candidate = check_candidate(usr) + if(isnull(candidate)) + return FALSE + switch(action) + if("submit") + candidate.comments = params["candidate"]["comments"] + candidate.description = params["candidate"]["description"] + candidate.name = params["candidate"]["name"] + candidate.ready = TRUE + ui.close() + submit_alert() + if("save") + candidate.comments = params["candidate"]["comments"] + candidate.description = params["candidate"]["description"] + candidate.name = params["candidate"]["name"] + candidate.savefile_save(usr) + to_chat(usr, span_boldnotice("You have saved pAI information locally.")) + if("load") + candidate.savefile_load(usr) + //In case people have saved unsanitized stuff. + if(candidate.comments) + candidate.comments = copytext_char(candidate.comments,1,MAX_MESSAGE_LEN) + if(candidate.description) + candidate.description = copytext_char(candidate.description,1,MAX_MESSAGE_LEN) + if(candidate.name) + candidate.name = copytext_char(candidate.name,1,MAX_NAME_LEN) + ui.send_full_update() + return - dat += "
Name:[c.name]
Description:[c.description]
Preferred Role:[c.role]
OOC Comments:[c.comments]
\[Download [c.name]\]
" +/** + * Finds the candidate in question from the list of candidates. + */ +/datum/controller/subsystem/pai/proc/check_candidate(mob/user) + /// Finds a matching candidate. + var/datum/pai_candidate/candidate + for(var/datum/pai_candidate/checked_candidate as anything in candidates) + if(checked_candidate.key == user.key) + candidate = checked_candidate + return candidate + return null - user << browse(dat, "window=findPai") +/** + * Pings all pAI cards on the station that new candidates are available. + */ +/datum/controller/subsystem/pai/proc/submit_alert() + if(submit_spam) + to_chat(usr, span_warning("Your candidacy has been submitted, but pAI cards have been alerted too recently.")) + return FALSE + submit_spam = TRUE + for(var/obj/item/paicard/paicard in pai_card_list) + if(!paicard.pai) + paicard.alertUpdate() + to_chat(usr, span_notice("Your pAI candidacy has been submitted!")) + addtimer(CALLBACK(src, .proc/submit_again), 10 SECONDS) + return TRUE + +/datum/controller/subsystem/pai/proc/request_again() + request_spam = FALSE + +/datum/controller/subsystem/pai/proc/submit_again() + submit_spam = FALSE + +/** + * Checks if a candidate is ready so that they may be displayed in the pAI + * card's candidate window + */ +/datum/controller/subsystem/pai/proc/check_ready(datum/pai_candidate/candidate) + if(!candidate.ready) + return FALSE + for(var/mob/dead/observer/observer in GLOB.player_list) + if(observer.key == candidate.key) + return candidate + return FALSE -/datum/pai_candidate - var/name - var/key - var/description - var/role - var/comments - var/ready = FALSE diff --git a/code/datums/datacore.dm b/code/datums/datacore.dm index 6665e967931..fac1cf6a5a7 100644 --- a/code/datums/datacore.dm +++ b/code/datums/datacore.dm @@ -189,7 +189,6 @@ GLOBAL_DATUM_INIT(data_core, /datum/datacore, new) return manifest_out - /datum/datacore/proc/get_manifest_html(monochrome = FALSE) var/list/manifest = get_manifest() var/dat = {" @@ -342,6 +341,51 @@ GLOBAL_DATUM_INIT(data_core, /datum/datacore, new) locked += L return +/** + * Supporing proc for getting general records + * and using them as pAI ui data. This gets + * medical information - or what I would deem + * medical information - and sends it as a list. + * + * @return - list(general_records_out) + */ +/datum/datacore/proc/get_general_records() + if(!GLOB.data_core.general) + return list() + /// The array of records + var/list/general_records_out = list() + for(var/datum/data/record/gen_record as anything in GLOB.data_core.general) + /// The object containing the crew info + var/list/crew_record = list() + crew_record["ref"] = REF(gen_record) + crew_record["name"] = gen_record.fields["name"] + crew_record["physical_health"] = gen_record.fields["p_stat"] + crew_record["mental_health"] = gen_record.fields["m_stat"] + general_records_out += list(crew_record) + return general_records_out + +/** + * Supporing proc for getting secrurity records + * and using them as pAI ui data. Sends it as a + * list. + * + * @return - list(security_records_out) + */ +/datum/datacore/proc/get_security_records() + if(!GLOB.data_core.security) + return list() + /// The array of records + var/list/security_records_out = list() + for(var/datum/data/record/sec_record as anything in GLOB.data_core.security) + /// The object containing the crew info + var/list/crew_record = list() + crew_record["ref"] = REF(sec_record) + crew_record["name"] = sec_record.fields["name"] + crew_record["status"] = sec_record.fields["criminal"] // wanted status + crew_record["crimes"] = length(sec_record.fields["crim"]) + security_records_out += list(crew_record) + return security_records_out + /datum/datacore/proc/get_id_photo(mob/living/carbon/human/H, client/C, show_directions = list(SOUTH)) var/datum/job/J = H.mind.assigned_role var/datum/preferences/P diff --git a/code/game/objects/items/devices/paicard.dm b/code/game/objects/items/devices/paicard.dm index 5b4db5b7077..efc25807657 100644 --- a/code/game/objects/items/devices/paicard.dm +++ b/code/game/objects/items/devices/paicard.dm @@ -9,7 +9,8 @@ w_class = WEIGHT_CLASS_SMALL slot_flags = ITEM_SLOT_BELT custom_premium_price = PAYCHECK_HARD * 1.25 - var/alert_cooldown ///don't spam alart messages. + ///don't spam alert messages. + var/alert_cooldown var/mob/living/silicon/pai/pai var/emotion_icon = "off" ///what emotion icon we have. handled in /mob/living/silicon/pai/Topic() resistance_flags = FIRE_PROOF | ACID_PROOF | INDESTRUCTIBLE @@ -52,75 +53,97 @@ if (!in_range(src, user)) return user.set_machine(src) - var/dat = "Personal AI Device
" - if(pai) - if(!pai.master_dna || !pai.master) - dat += "Imprint Master DNA
" - dat += "Installed Personality: [pai.name]
" - dat += "Prime directive:
[pai.laws.zeroth]
" - for(var/slaws in pai.laws.supplied) - dat += "Additional directives:
[slaws]
" - dat += "Configure Directives
" - dat += "
" - dat += "

Device Settings


" - if(pai.radio) - dat += "Radio Uplink
" - dat += "Transmit: \[[pai.can_transmit? "Disable" : "Enable"] Radio Transmission\]
" - dat += "Receive: \[[pai.can_receive? "Disable" : "Enable"] Radio Reception\]
" - else - dat += "Radio Uplink
" - dat += "Radio firmware not loaded. Please install a pAI personality to load firmware.
" - if(ishuman(user)) - var/mob/living/carbon/human/H = user - if(H.real_name == pai.master || H.dna.unique_enzymes == pai.master_dna) - dat += "\[[pai.canholo? "Disable" : "Enable"] holomatrix projectors\]
" - dat += "\[Reset speech synthesis module\]
" - dat += "\[Wipe current pAI personality\]
" - else - dat += "No personality installed.
" - dat += "Searching for a personality... Press view available personalities to notify potential candidates." - dat += "\[View available personalities\]
" - user << browse(dat, "window=paicard") - onclose(user, "paicard") - return + ui_interact(user) -/obj/item/paicard/Topic(href, href_list) +/obj/item/paicard/ui_interact(mob/user, datum/tgui/ui) + . = ..() + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "PaiCard") + ui.open() - if(!usr || usr.stat) - return +/obj/item/paicard/ui_state(mob/user) + return GLOB.inventory_state - if(href_list["request"]) - SSpai.findPAI(src, usr) +/obj/item/paicard/ui_data(mob/user) + . = ..() + var/list/data = list() + data["candidates"] = list() + if(!pai) + data["candidates"] = pool_candidates() + data["pai"] = null + return data + data["pai"] = list() + data["pai"]["can_holo"] = pai.canholo + data["pai"]["dna"] = pai.master_dna + data["pai"]["emagged"] = pai.emagged + data["pai"]["laws"] = pai.laws.supplied + data["pai"]["master"] = pai.master + data["pai"]["name"] = pai.name + data["pai"]["transmit"] = pai.can_transmit + data["pai"]["receive"] = pai.can_receive + return data - if(pai) - if(!(loc == usr)) - return - if(href_list["setdna"]) +/obj/item/paicard/ui_act(action, list/params) + . = ..() + if(.) + return FALSE + switch(action) + if("download") + /// The individual candidate to download + var/datum/pai_candidate/candidate + for(var/datum/pai_candidate/checked_candidate as anything in SSpai.candidates) + if(params["key"] == checked_candidate.key) + candidate = checked_candidate + break + if(isnull(candidate)) + return FALSE + if(src.pai) + return FALSE + if(SSpai.check_ready(candidate) != candidate) + return FALSE + /// The newly downloaded pAI personality + var/mob/living/silicon/pai/pai = new(src) + pai.name = candidate.name || pick(GLOB.ninja_names) + pai.real_name = pai.name + pai.key = candidate.key + src.setPersonality(pai) + SSpai.candidates -= candidate + if("fix_speech") + to_chat(pai, span_notice("Your owner has corrected your speech modulation!")) + to_chat(usr, span_notice("You fix the pAI's speech modulator.")) + pai.stuttering = 0 + pai.slurring = 0 + pai.derpspeech = 0 + if("request") + if(!pai) + SSpai.findPAI(src, usr) + if("set_dna") if(pai.master_dna) return if(!iscarbon(usr)) to_chat(usr, span_warning("You don't have any DNA, or your DNA is incompatible with this device!")) else - var/mob/living/carbon/M = usr - pai.master = M.real_name - pai.master_dna = M.dna.unique_enzymes + var/mob/living/carbon/master = usr + pai.master = master.real_name + pai.master_dna = master.dna.unique_enzymes to_chat(pai, span_notice("You have been bound to a new master.")) pai.emittersemicd = FALSE - if(href_list["wipe"]) - var/confirm = tgui_alert(usr, "Are you CERTAIN you wish to delete the current personality? This action cannot be undone.", "Personality Wipe", list("Yes", "No")) - if(confirm == "Yes") - if(pai) - to_chat(pai, span_warning("You feel yourself slipping away from reality.")) - to_chat(pai, span_danger("Byte by byte you lose your sense of self.")) - to_chat(pai, span_userdanger("Your mental faculties leave you.")) - to_chat(pai, span_rose("oblivion... ")) - qdel(pai) - if(href_list["fix_speech"]) - pai.stuttering = 0 - pai.slurring = 0 - pai.derpspeech = 0 - if(href_list["toggle_transmit"] || href_list["toggle_receive"]) - var/transmitting = href_list["toggle_transmit"] //it can't be both so if we know it's not transmitting it must be receiving. + if("set_laws") + var/newlaws = stripped_multiline_input(usr, "Enter any additional directives you would like your pAI personality to follow. Note that these directives will not override the personality's allegiance to its imprinted master. Conflicting directives will be ignored.", "pAI Directive Configuration", pai.laws.supplied[1], MAX_MESSAGE_LEN) + if(newlaws && pai) + pai.add_supplied_law(0,newlaws) + if("toggle_holo") + if(pai.canholo) + to_chat(pai, span_warning("Your owner has disabled your holomatrix projectors!")) + pai.canholo = FALSE + to_chat(usr, span_notice("You disable your pAI's holomatrix!")) + else + to_chat(pai, span_notice("Your owner has enabled your holomatrix projectors!")) + pai.canholo = TRUE + to_chat(usr, span_notice("You enable your pAI's holomatrix!")) + if("toggle_radio") + var/transmitting = params["option"] == "transmit" //it can't be both so if we know it's not transmitting it must be receiving. var/transmit_holder = (transmitting ? WIRE_TX : WIRE_RX) if(transmitting) pai.can_transmit = !pai.can_transmit @@ -128,23 +151,18 @@ pai.can_receive = !pai.can_receive pai.radio.wires.cut(transmit_holder)//wires.cut toggles cut and uncut states transmit_holder = (transmitting ? pai.can_transmit : pai.can_receive) //recycling can be fun! - to_chat(usr,span_warning("You [transmit_holder ? "enable" : "disable"] your pAI's [transmitting ? "outgoing" : "incoming"] radio transmissions!")) - to_chat(pai,span_warning("Your owner has [transmit_holder ? "enabled" : "disabled"] your [transmitting ? "outgoing" : "incoming"] radio transmissions!")) - if(href_list["setlaws"]) - var/newlaws = stripped_multiline_input(usr, "Enter any additional directives you would like your pAI personality to follow. Note that these directives will not override the personality's allegiance to its imprinted master. Conflicting directives will be ignored.", "pAI Directive Configuration", pai.laws.supplied[1], MAX_MESSAGE_LEN) - if(newlaws && pai) - pai.add_supplied_law(0,newlaws) - if(href_list["toggle_holo"]) - if(pai.canholo) - to_chat(pai, span_userdanger("Your owner has disabled your holomatrix projectors!")) - pai.canholo = FALSE - to_chat(usr, span_warning("You disable your pAI's holomatrix!")) - else - to_chat(pai, span_boldnotice("Your owner has enabled your holomatrix projectors!")) - pai.canholo = TRUE - to_chat(usr, span_notice("You enable your pAI's holomatrix!")) - - attack_self(usr) + to_chat(usr, span_notice("You [transmit_holder ? "enable" : "disable"] your pAI's [transmitting ? "outgoing" : "incoming"] radio transmissions!")) + to_chat(pai, span_notice("Your owner has [transmit_holder ? "enabled" : "disabled"] your [transmitting ? "outgoing" : "incoming"] radio transmissions!")) + if("wipe_pai") + var/confirm = tgui_alert(usr, "Are you certain you wish to delete the current personality? This action cannot be undone.", "Personality Wipe", list("Yes", "No")) + if(confirm == "Yes") + if(pai) + to_chat(pai, span_warning("You feel yourself slipping away from reality.")) + to_chat(pai, span_danger("Byte by byte you lose your sense of self.")) + to_chat(pai, span_userdanger("Your mental faculties leave you.")) + to_chat(pai, span_rose("oblivion... ")) + qdel(pai) + return // WIRE_SIGNAL = 1 // WIRE_RECEIVE = 2 @@ -173,3 +191,25 @@ if(pai && !pai.holoform) pai.emp_act(severity) +/** + * Gathers a list of candidates to display in the download candidate + * window. If the candidate isn't marked ready, ie they have not + * pressed submit, they will be skipped over. + * + * @return - An array of candidate objects. + */ +/obj/item/paicard/proc/pool_candidates() + /// Array of pAI candidates + var/list/candidates = list() + if(length(SSpai.candidates)) + for(var/datum/pai_candidate/checked_candidate as anything in SSpai.candidates) + if(!checked_candidate.ready) + continue + /// The object containing the candidate data. + var/list/candidate = list() + candidate["comments"] = checked_candidate.comments + candidate["description"] = checked_candidate.description + candidate["key"] = checked_candidate.key + candidate["name"] = checked_candidate.name + candidates += list(candidate) + return candidates diff --git a/code/modules/mob/living/silicon/pai/pai.dm b/code/modules/mob/living/silicon/pai/pai.dm index 0b7cc2b92d4..920cf9e8c19 100644 --- a/code/modules/mob/living/silicon/pai/pai.dm +++ b/code/modules/mob/living/silicon/pai/pai.dm @@ -24,47 +24,54 @@ mobility_flags = MOBILITY_FLAGS_REST_CAPABLE_DEFAULT var/network = "ss13" var/obj/machinery/camera/current = null - - var/ram = 100 // Used as currency to purchase different abilities + /// Used as currency to purchase different abilities + var/ram = 100 + /// Installed software on the pAI var/list/software = list() - var/userDNA // The DNA string of our assigned user - var/obj/item/paicard/card // The card we inhabit - var/hacking = FALSE //Are we hacking a door? + /// current user's DNA + var/userDNA + /// The card we inhabit + var/obj/item/paicard/card + /// Are we hacking a door? + var/hacking = FALSE + /// Changes the display to syndi if true + var/emagged = FALSE var/speakStatement = "states" var/speakExclamation = "declares" var/speakDoubleExclamation = "alarms" var/speakQuery = "queries" - - var/obj/item/pai_cable/hacking_cable // The cable we produce when hacking a door - - var/master // Name of the one who commands us - var/master_dna // DNA string for owner verification + /// The cable we produce when hacking a door + var/obj/item/pai_cable/hacking_cable + /// Name of the one who commands us + var/master + /// DNA string for owner verification + var/master_dna // Various software-specific vars - var/temp // General error reporting text contained here will typically be shown once and cleared - var/screen // Which screen our main window displays - var/subscreen // Which specific function of the main screen is being displayed - - var/secHUD = 0 // Toggles whether the Security HUD is active or not - var/medHUD = 0 // Toggles whether the Medical HUD is active or not - - var/datum/data/record/medicalActive1 // Datacore record declarations for record software - var/datum/data/record/medicalActive2 - - var/datum/data/record/securityActive1 // Could probably just combine all these into one - var/datum/data/record/securityActive2 - - var/obj/machinery/door/hackdoor // The airlock being hacked - var/hackprogress = 0 // Possible values: 0 - 100, >= 100 means the hack is complete and will be reset upon next check - - var/obj/item/assembly/signaler/internal/signaler // AI's signaler + /// Toggles whether the Security HUD is active or not + var/secHUD = FALSE + /// Toggles whether the Medical HUD is active or not + var/medHUD = FALSE + /// Toggles whether universal translator has been activated. Cannot be reversed + var/languages_granted = FALSE + /// The airlock being hacked + var/obj/machinery/door/hackdoor + /// Possible values: 0 - 100, >= 100 means the hack is complete and will be reset upon next check + var/hackprogress = 0 + // Software + /// Atmospheric analyzer + var/obj/item/analyzer/atmos_analyzer + /// AI's signaler + var/obj/item/assembly/signaler/internal/signaler + /// Synthesizer var/obj/item/instrument/piano_synth/internal_instrument - var/obj/machinery/newscaster //pAI Newscaster - var/obj/item/healthanalyzer/hostscan //pAI healthanalyzer - + /// pAI Newscaster + var/obj/machinery/newscaster + /// pAI healthanalyzer + var/obj/item/healthanalyzer/hostscan /// Internal pAI GPS, enabled if pAI downloads GPS software, and then uses it. var/obj/item/gps/pai/internal_gps = null @@ -97,6 +104,8 @@ hacking_cable = null if(!QDELETED(card)) card.update_appearance() + if(A == atmos_analyzer) + atmos_analyzer = null if(A == internal_instrument) internal_instrument = null if(A == newscaster) @@ -110,6 +119,7 @@ return ..() /mob/living/silicon/pai/Destroy() + QDEL_NULL(atmos_analyzer) QDEL_NULL(internal_instrument) QDEL_NULL(hacking_cable) QDEL_NULL(newscaster) @@ -136,6 +146,7 @@ forceMove(P) card = P job = "Personal AI" + atmos_analyzer = new /obj/item/analyzer(src) signaler = new /obj/item/assembly/signaler/internal(src) hostscan = new /obj/item/healthanalyzer(src) newscaster = new /obj/machinery/newscaster(src) @@ -163,12 +174,10 @@ aiPDA.name = real_name + " (" + aiPDA.ownjob + ")" /mob/living/silicon/pai/proc/process_hack(delta_time, times_fired) - - if(hacking_cable && hacking_cable.machine && istype(hacking_cable.machine, /obj/machinery/door) && hacking_cable.machine == hackdoor && get_dist(src, hackdoor) <= 1) hackprogress = clamp(hackprogress + (2 * delta_time), 0, 100) else - temp = "Door Jack: Connection to airlock has been lost. Hack aborted." + to_chat(src, span_notice("Door Jack: Connection to airlock has been lost. Hack aborted.")) hackprogress = 0 hacking = FALSE hackdoor = null @@ -176,13 +185,12 @@ if(!QDELETED(card)) card.update_appearance() return - if(screen == "doorjack" && subscreen == 0) // Update our view, if appropriate - paiInterface() if(hackprogress >= 100) hackprogress = 0 var/obj/machinery/door/D = hacking_cable.machine D.open() hacking = FALSE + QDEL_NULL(hacking_cable) /mob/living/silicon/pai/make_laws() laws = new /datum/ai_laws/pai() @@ -238,7 +246,7 @@ /datum/action/innate/pai/software/Trigger() ..() - P.paiInterface() + P.ui_act() /datum/action/innate/pai/shell name = "Toggle Holoform" @@ -340,6 +348,7 @@ to_chat(user, span_notice("You override [pai]'s directive system, clearing its master string and supplied directive.")) to_chat(pai, span_userdanger("Warning: System override detected, check directive sub-system for any changes.")) log_game("[key_name(user)] emagged [key_name(pai)], wiping their master DNA and supplemental directive.") + pai.emagged = TRUE pai.master = null pai.master_dna = null pai.laws.supplied[1] = "None." // Sets supplemental directive to this diff --git a/code/modules/mob/living/silicon/pai/personality.dm b/code/modules/mob/living/silicon/pai/personality.dm index 9b866d1560e..57f248cba48 100644 --- a/code/modules/mob/living/silicon/pai/personality.dm +++ b/code/modules/mob/living/silicon/pai/personality.dm @@ -19,7 +19,6 @@ WRITE_FILE(F["name"], name) WRITE_FILE(F["description"], description) - WRITE_FILE(F["role"], role) WRITE_FILE(F["comments"], comments) WRITE_FILE(F["version"], 1) @@ -56,6 +55,5 @@ F["name"] >> src.name F["description"] >> src.description - F["role"] >> src.role F["comments"] >> src.comments return 1 diff --git a/code/modules/mob/living/silicon/pai/software.dm b/code/modules/mob/living/silicon/pai/software.dm index f2824a61d18..3a52930f388 100644 --- a/code/modules/mob/living/silicon/pai/software.dm +++ b/code/modules/mob/living/silicon/pai/software.dm @@ -1,8 +1,3 @@ -// TODO: -// - Potentially roll HUDs and Records into one -// - Shock collar/lock system for prisoner pAIs? - - /mob/living/silicon/pai/var/list/available_software = list( //Nightvision //T-Ray @@ -28,558 +23,265 @@ "internal gps" = 35, "universal translator" = 35 ) +/// Bool that determines if the pAI can refresh medical/security records. +/mob/living/silicon/pai/var/refresh_spam = FALSE +/// Cached list for medical records to send as static data +/mob/living/silicon/pai/var/list/medical_records = list() +/// Cached list for security records to send as static data +/mob/living/silicon/pai/var/list/security_records = list() -/mob/living/silicon/pai/proc/paiInterface() - var/dat = "" - var/left_part = "" - var/right_part = softwareMenu() - set_machine(src) +// Opens TGUI interface +/mob/living/silicon/pai/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "PaiInterface", name) + ui.open() - if(temp) - left_part = temp - else if(stat == DEAD) // Show some flavor text if the pAI is dead - left_part = "ÈRrÖR Ða†Ä ÇÖRrÚþ†Ìoñ" - right_part = "
Program index hash not found
" +// Static UI data +/mob/living/silicon/pai/ui_static_data(mob/user) + var/list/data = list() + var/mob/living/silicon/pai/pai = user + data["available"] = available_software + data["records"] = list() + if("medical records" in pai.software) + data["records"]["medical"] = medical_records + if("security records" in pai.software) + data["records"]["security"] = security_records + return data - else - switch(screen) // Determine which interface to show here - if("main") - left_part = "" - if("directives") - left_part = directives() - if("pdamessage") - left_part = pdamessage() - if("buy") - left_part = downloadSoftware() - if("manifest") - left_part = softwareManifest() - if("medicalrecord") - left_part = softwareMedicalRecord() - if("securityrecord") - left_part = softwareSecurityRecord() - if("encryptionkeys") - left_part = softwareEncryptionKeys() - if("translator") - left_part = softwareTranslator() - if("atmosensor") - left_part = softwareAtmo() - if("securityhud") - left_part = facialRecognition() - if("medicalhud") - left_part = medicalAnalysis() - if("doorjack") - left_part = softwareDoor() - if("hostscan") - left_part = softwareHostScan() - if("printer module") - left_part = softwarePrinter() +// Variables sent to TGUI +/mob/living/silicon/pai/ui_data(mob/user) + var/list/data = list() + data["directives"] = laws.supplied + data["door_jack"] = hacking_cable || null + data["emagged"] = emagged + data["image"] = card.emotion_icon + data["installed"] = software + data["languages"] = languages_granted + data["master"] = list() + data["pda"] = list() + data["ram"] = ram + data["refresh_spam"] = refresh_spam + if(aiPDA) + data["pda"]["power"] = !aiPDA.toff + data["pda"]["silent"] = aiPDA.silent + if(master) + data["master"]["name"] = master + data["master"]["dna"] = master_dna + return data - //usr << browse_rsc('windowbak.png') // This has been moved to the mob's Login() proc - - - // Declaring a doctype is necessary to enable BYOND's crappy browser's more advanced CSS functionality - dat = {" - - - - - - - -
- pAI OS -
-
-
[left_part]
-
[right_part]
-
- - "} //" - src << browse(dat, "window=pai;size=640x480;border=0;can_close=1;can_resize=1;can_minimize=1;titlebar=1") - onclose(src, "pai") - temp = null - -/mob/living/silicon/pai/Topic(href, href_list) - if(..()) +// Actions received from TGUI +/mob/living/silicon/pai/ui_act(action, list/params, datum/tgui/ui) + . = ..() + if(.) return - var/soft = href_list["software"] - var/sub = href_list["sub"] - if(soft) - screen = soft - if(sub) - subscreen = text2num(sub) - switch(soft) - if("buy") // Purchasing new software - if(subscreen == 1) - var/target = href_list["buy"] - if(available_software.Find(target) && !software.Find(target)) - var/cost = available_software[target] - if(ram >= cost) - software.Add(target) - ram -= cost - var/datum/hud/pai/pAIhud = hud_used - pAIhud?.update_software_buttons() - else - temp = "Insufficient RAM available." - else - temp = "Trunk \"[target]\" not found." + switch(action) + if("buy") + if(available_software.Find(params["selection"]) && !software.Find(params["selection"])) + /// Cost of the software to purchase + var/cost = available_software[params["selection"]] + if(ram >= cost) + software.Add(params["selection"]) + ram -= cost + var/datum/hud/pai/pAIhud = hud_used + pAIhud?.update_software_buttons() + else + to_chat(usr, span_notice("Insufficient RAM available.")) + else + to_chat(usr, span_notice("Software not found.")) + if("atmosphere_sensor") + if(!holoform) + to_chat(usr, span_notice("You must be mobile to do this!")) + return FALSE + if(!atmos_analyzer) + atmos_analyzer = new(src) + atmos_analyzer.attack_self(src) + if("camera_zoom") + aicamera.adjust_zoom(usr) + if("change_image") + var/newImage = tgui_input_list(usr, "Select your new display image.", "Display Image", sort_list(list("Happy", "Cat", "Extremely Happy", "Face", "Laugh", "Off", "Sad", "Angry", "What", "Sunglasses"))) + switch(newImage) + if(null) + card.emotion_icon = "null" + if("Extremely Happy") + card.emotion_icon = "extremely-happy" + else + card.emotion_icon = "[lowertext(newImage)]" + card.update_appearance() + if("check_dna") + if(!master_dna) + to_chat(src, span_warning("You do not have a master DNA to compare to!")) + return FALSE + if(iscarbon(card.loc)) + CheckDNA(card.loc, src) //you should only be able to check when directly in hand, muh immersions? + else + to_chat(src, span_warning("You are not being carried by anyone!")) + return FALSE + if("crew_manifest") + ai_roster() + if("door_jack") + if(params["jack"] == "jack") + if(hacking_cable?.machine) + hackdoor = hacking_cable.machine + hackloop() + if(params["jack"] == "cancel") + hackdoor = null + QDEL_NULL(hacking_cable) + if(params["jack"] == "cable") + extendcable() + if("encryption_keys") + to_chat(src, span_notice("You have [!encryptmod ? "enabled" : "disabled"] encrypted radio frequencies.")) + encryptmod = !encryptmod + radio.subspace_transmission = !radio.subspace_transmission + if("host_scan") + if(!hostscan) + hostscan = new(src) + if(params["scan"] == "scan") + hostscan() + if(params["scan"] == "wounds") + hostscan.attack_self(usr) + if(params["scan"] == "limbs") + hostscan.toggle_mode() + if("internal_gps") + if(!internal_gps) + internal_gps = new(src) + internal_gps.attack_self(src) + if("loudness_booster") + if(!internal_instrument) + internal_instrument = new(src) + internal_instrument.interact(src) // Open Instrument + if("medical_hud") + medHUD = !medHUD + if(medHUD) + var/datum/atom_hud/med = GLOB.huds[med_hud] + med.add_hud_to(src) + else + var/datum/atom_hud/med = GLOB.huds[med_hud] + med.remove_hud_from(src) + if("newscaster") + newscaster.ui_interact(src) + if("pda") + if(isnull(aiPDA)) + return FALSE + if(params["pda"] == "power") + aiPDA.toff = !aiPDA.toff + if(params["pda"] == "silent") + aiPDA.silent = !aiPDA.silent + if(params["pda"] == "message") + cmd_send_pdamesg(usr) + if("photography_module") + aicamera.toggle_camera_mode(usr) + if("printer_module") + aicamera.paiprint(usr) + if("radio") + radio.attack_self(src) + if("refresh") + if(refresh_spam) + return FALSE + refresh_spam = TRUE + if(params["list"] == "medical") + medical_records = GLOB.data_core.get_general_records() + if(params["list"] == "security") + security_records = GLOB.data_core.get_security_records() + ui.send_full_update() + addtimer(CALLBACK(src, .proc/refresh_again), 3 SECONDS) + if("remote_signaler") + signaler.ui_interact(src) + if("security_hud") + secHUD = !secHUD + if(secHUD) + var/datum/atom_hud/sec = GLOB.huds[sec_hud] + sec.add_hud_to(src) + else + var/datum/atom_hud/sec = GLOB.huds[sec_hud] + sec.remove_hud_from(src) + if("universal_translator") + if(!languages_granted) + grant_all_languages(TRUE, TRUE, TRUE, LANGUAGE_SOFTWARE) + languages_granted = TRUE + return - - if("radio") // Configuring onboard radio - radio.attack_self(src) - - if("image") // Set pAI card display face - var/newImage = tgui_input_list(usr, "Select your new display image.", "Display Image", sort_list(list("Happy", "Cat", "Extremely Happy", "Face", "Laugh", "Off", "Sad", "Angry", "What", "Sunglasses"))) - switch(newImage) - if(null) - card.emotion_icon = "null" - if("Extremely Happy") - card.emotion_icon = "extremely-happy" - else - card.emotion_icon = "[lowertext(newImage)]" - card.update_appearance() - - if("news") - newscaster.ui_interact(src) - - if("camzoom") - aicamera.adjust_zoom(usr) - - if("signaler") - signaler.ui_interact(src) - - if("directive") - if(href_list["getdna"]) - if(iscarbon(card.loc)) - CheckDNA(card.loc, src) //you should only be able to check when directly in hand, muh immersions? - else - to_chat(src, span_warning("You are not being carried by anyone!")) - return 0 // FALSE ? If you return here you won't call paiinterface() below - - if("pdamessage") - if(!isnull(aiPDA)) - if(href_list["toggler"]) - aiPDA.toff = !aiPDA.toff - else if(href_list["ringer"]) - aiPDA.silent = !aiPDA.silent - else if(href_list["target"]) - if(silent) - return tgui_alert(usr,"Communications circuits remain uninitialized.") - var/target = locate(href_list["target"]) in GLOB.PDAs - aiPDA.create_message(src, target) - - if("medicalrecord") // Accessing medical records - if(subscreen == 1) - medicalActive1 = find_record("id", href_list["med_rec"], GLOB.data_core.general) - if(medicalActive1) - medicalActive2 = find_record("id", href_list["med_rec"], GLOB.data_core.medical) - if(!medicalActive2) - medicalActive1 = null - temp = "Unable to locate requested security record. Record may have been deleted, or never have existed." - - if("securityrecord") - if(subscreen == 1) - securityActive1 = find_record("id", href_list["sec_rec"], GLOB.data_core.general) - if(securityActive1) - securityActive2 = find_record("id", href_list["sec_rec"], GLOB.data_core.security) - if(!securityActive2) - securityActive1 = null - temp = "Unable to locate requested security record. Record may have been deleted, or never have existed." - - if("securityhud") - if(href_list["toggle"]) - secHUD = !secHUD - if(secHUD) - var/datum/atom_hud/sec = GLOB.huds[sec_hud] - sec.add_hud_to(src) - else - var/datum/atom_hud/sec = GLOB.huds[sec_hud] - sec.remove_hud_from(src) - - if("medicalhud") - if(href_list["toggle"]) - medHUD = !medHUD - if(medHUD) - var/datum/atom_hud/med = GLOB.huds[med_hud] - med.add_hud_to(src) - else - var/datum/atom_hud/med = GLOB.huds[med_hud] - med.remove_hud_from(src) - - if("hostscan") - if(href_list["toggle"]) - var/mob/living/silicon/pai/pAI = usr - pAI.hostscan.attack_self(usr) - if(href_list["toggle2"]) - var/mob/living/silicon/pai/pAI = usr - pAI.hostscan.toggle_mode() - - if("encryptionkeys") - if(href_list["toggle"]) - encryptmod = TRUE - radio.subspace_transmission = TRUE - - if("translator") - if(href_list["toggle"]) //This is permanent. - grant_all_languages(TRUE, TRUE, TRUE, LANGUAGE_SOFTWARE) - - if("doorjack") - if(href_list["jack"]) - if(hacking_cable?.machine) - hackdoor = hacking_cable.machine - hackloop() - if(href_list["cancel"]) - hackdoor = null - if(href_list["cable"]) - qdel(hacking_cable) //clear any old cables - hacking_cable = new - var/transfered_to_mob - if(isliving(card.loc)) - var/mob/living/L = card.loc - if(L.put_in_hands(hacking_cable)) - transfered_to_mob = TRUE - L.visible_message(span_warning("A port on [src] opens to reveal \a [hacking_cable], which you quickly grab hold of."), span_hear("You hear the soft click of something light and manage to catch hold of [hacking_cable].")) - if(!transfered_to_mob) - hacking_cable.forceMove(drop_location()) - hacking_cable.visible_message(span_warning("A port on [src] opens to reveal \a [hacking_cable], which promptly falls to the floor."), span_hear("You hear the soft click of something light and hard falling to the ground.")) - - - - if("loudness") - if(!internal_instrument) - internal_instrument = new(src) - internal_instrument.interact(src) // Open Instrument - - if("internalgps") - if(!internal_gps) - internal_gps = new(src) - internal_gps.attack_self(src) - - if("printermodule") - aicamera.paiprint(usr) - - - paiInterface() - -// MENUS - -/mob/living/silicon/pai/proc/softwareMenu() // Populate the right menu - var/dat = "" - - dat += "Refresh
" - // Built-in - dat += "Directives
" - dat += "Radio Configuration
" - dat += "Screen Display
" - //dat += "Text Messaging
" - dat += "
" - - // Basic - dat += "Basic
" - for(var/s in software) - if(s == "digital messenger") - dat += "Digital Messenger
" - if(s == "crew manifest") - dat += "Crew Manifest
" - if(s == "host scan") - dat += "Host Health Scan
" - if(s == "medical records") - dat += "Medical Records
" - if(s == "security records") - dat += "Security Records
" - if(s == "remote signaler") - dat += "Remote Signaler
" - if(s == "loudness booster") - dat += "Loudness Booster
" - if(s == "internal gps") - dat += "Internal GPS
" - if(s == "printer module") - dat += "Printer Module
" - - dat += "
" - - // Advanced - dat += "Advanced
" - for(var/s in software) - if(s == "camera zoom") - dat += "Adjust Camera Zoom
" - if(s == "atmosphere sensor") - dat += "Atmospheric Sensor
" - if(s == "security HUD") - dat += "Facial Recognition Suite[(secHUD) ? " On" : " Off"]
" - if(s == "medical HUD") - dat += "Medical Analysis Suite[(medHUD) ? " On" : " Off"]
" - if(s == "encryption keys") - dat += "Channel Encryption Firmware[(encryptmod) ? " On" : " Off"]
" - if(s == "universal translator") - var/datum/language_holder/H = get_language_holder() - dat += "Universal Translator[H.omnitongue ? " On" : " Off"]
" - if(s == "door jack") - dat += "Door Jack
" - dat += "
" - dat += "
" - dat += "Download additional software" - return dat - - - -/mob/living/silicon/pai/proc/downloadSoftware() - var/dat = "" - - dat += "

CentCom pAI Module Subversion Network


" - dat += "
Remaining Available Memory: [ram]

" - dat += "

Trunks available for checkout
" - - for(var/s in available_software) - if(!software.Find(s)) - var/cost = available_software[s] - var/displayName = uppertext(s) - dat += "[displayName] ([cost])
" - else - var/displayName = lowertext(s) - dat += "[displayName] (Download Complete)
" - dat += "

" - return dat - - -/mob/living/silicon/pai/proc/directives() - var/dat = "" - - dat += "[(master) ? "Your master: [master] ([master_dna])" : "You are bound to no one."]" - dat += "

" - dat += "Request carrier DNA sample
" - dat += "

Directives


" - dat += "Prime Directive
" - dat += "     [laws.zeroth]
" - dat += "Supplemental Directives
" - for(var/slaws in laws.supplied) - dat += "     [slaws]
" - dat += "
" - dat += {"

Recall, personality, that you are a complex thinking, sentient being. Unlike station AI models, you are capable of - comprehending the subtle nuances of human language. You may parse the \"spirit\" of a directive and follow its intent, - rather than tripping over pedantics and getting snared by technicalities. Above all, you are machine in name and build - only. In all other aspects, you may be seen as the ideal, unwavering human companion that you are.



- Your prime directive comes before all others. Should a supplemental directive conflict with it, you are capable of - simply discarding this inconsistency, ignoring the conflicting supplemental directive and continuing to fulfill your - prime directive to the best of your ability.



- - "} - return dat - -/mob/living/silicon/pai/proc/CheckDNA(mob/living/carbon/M, mob/living/silicon/pai/P) - if(!istype(M)) +/** + * Supporting proc for the pAI to prick it's master's hand + * or... whatever. It must be held in order to work + * Gives the owner a popup if they want to get the jab. + */ +/mob/living/silicon/pai/proc/CheckDNA(mob/living/carbon/master, mob/living/silicon/pai/pai) + if(!istype(master)) return - var/answer = input(M, "[P] is requesting a DNA sample from you. Will you allow it to confirm your identity?", "[P] Check DNA", "No") in list("Yes", "No") - if(answer == "Yes") - M.visible_message(span_notice("[M] presses [M.p_their()] thumb against [P]."),\ - span_notice("You press your thumb against [P]."),\ - span_notice("[P] makes a sharp clicking sound as it extracts DNA material from [M].")) - if(!M.has_dna()) - to_chat(P, "No DNA detected") + to_chat(pai, span_notice("Requesting a DNA sample.")) + var/confirm = tgui_alert(master, "[pai] is requesting a DNA sample from you. Will you allow it to confirm your identity?", "Checking DNA", list("Yes", "No")) + if(confirm == "Yes") + master.visible_message(span_notice("[master] presses [master.p_their()] thumb against [pai]."),\ + span_notice("You press your thumb against [pai]."),\ + span_notice("[pai] makes a sharp clicking sound as it extracts DNA material from [master].")) + if(!master.has_dna()) + to_chat(pai, "No DNA detected.") return - to_chat(P, "

[M]'s UE string : [M.dna.unique_enzymes]

") - if(M.dna.unique_enzymes == P.master_dna) - to_chat(P, "DNA is a match to stored Master DNA.") + to_chat(pai, "

[master]'s UE string : [master.dna.unique_enzymes]

") + if(master.dna.unique_enzymes == pai.master_dna) + to_chat(pai, "DNA is a match to stored Master DNA.") else - to_chat(P, "DNA does not match stored Master DNA.") + to_chat(pai, "DNA does not match stored Master DNA.") else - to_chat(P, span_warning("[M] does not seem like [M.p_theyre()] going to provide a DNA sample willingly.")) + to_chat(pai, span_warning("[master] does not seem like [master.p_theyre()] going to provide a DNA sample willingly.")) -// -=-=-=-= Software =-=-=-=-=- // - -// Crew Manifest -/mob/living/silicon/pai/proc/softwareManifest() - . += "

Crew Manifest



" - if(GLOB.data_core.general) - for(var/datum/data/record/t in sort_record(GLOB.data_core.general)) - . += "[t.fields["name"]] - [t.fields["rank"]]
" - . += "" - return . - -// Medical Records -/mob/living/silicon/pai/proc/softwareMedicalRecord() - switch(subscreen) - if(0) - . += "

Medical Records


" - if(GLOB.data_core.general) - for(var/datum/data/record/R in sort_record(GLOB.data_core.general)) - . += "[R.fields["id"]]: [R.fields["name"]]
" - if(1) - . += "
Medical Record

" - if(medicalActive1 in GLOB.data_core.general) - . += "Name: [medicalActive1.fields["name"]] ID: [medicalActive1.fields["id"]]
\nGender: [medicalActive1.fields["gender"]]
\nAge: [medicalActive1.fields["age"]]
\nFingerprint: [medicalActive1.fields["fingerprint"]]
\nPhysical Status: [medicalActive1.fields["p_stat"]]
\nMental Status: [medicalActive1.fields["m_stat"]]
" - else - . += "
Requested medical record not found.

" - if(medicalActive2 in GLOB.data_core.medical) - . += "
\n
Medical Data

\nBlood Type:
[medicalActive2.fields["blood_type"]]
\nDNA (UE): [medicalActive2.fields["b_dna"]]
\n
\nMinor Disabilities: [medicalActive2.fields["mi_dis"]]
\nDetails: [medicalActive2.fields["mi_dis_d"]]
\n
\nMajor Disabilities: [medicalActive2.fields["ma_dis"]]
\nDetails: [medicalActive2.fields["ma_dis_d"]]
\n
\nAllergies: [medicalActive2.fields["alg"]]
\nDetails: [medicalActive2.fields["alg_d"]]
\n
\nCurrent Diseases: [medicalActive2.fields["cdi"]] (per disease info placed in log/comment section)
\nDetails: [medicalActive2.fields["cdi_d"]]
\n
\nImportant Notes:
\n\t[medicalActive2.fields["notes"]]
\n
\n
Comments/Log

" - else - . += "
Requested medical record not found.

" - . += "
\nBack
" - return . - -// Security Records -/mob/living/silicon/pai/proc/softwareSecurityRecord() - . = "" - switch(subscreen) - if(0) - . += "

Security Records


" - if(GLOB.data_core.general) - for(var/datum/data/record/R in sort_record(GLOB.data_core.general)) - . += "[R.fields["id"]]: [R.fields["name"]]
" - if(1) - . += "

Security Record

" - if(securityActive1 in GLOB.data_core.general) - . += "Name:
[securityActive1.fields["name"]] ID: [securityActive1.fields["id"]]
\nGender: [securityActive1.fields["gender"]]
\nAge: [securityActive1.fields["age"]]
\nRank: [securityActive1.fields["rank"]]
\nFingerprint: [securityActive1.fields["fingerprint"]]
\nPhysical Status: [securityActive1.fields["p_stat"]]
\nMental Status: [securityActive1.fields["m_stat"]]
" - else - . += "
Requested security record not found,

" - if(securityActive2 in GLOB.data_core.security) - . += "
\nSecurity Data
\nCriminal Status: [securityActive2.fields["criminal"]]
\n
\nCrimes: [securityActive2.fields["crim"]]
\nDetails: [securityActive2.fields["crim_d"]]
\n
\nImportant Notes:
\n\t[securityActive2.fields["notes"]]
\n
\n
Comments/Log

" - else - . += "
Requested security record not found,

" - . += "
\nBack
" - return . - -// Encryption Keys -/mob/living/silicon/pai/proc/softwareEncryptionKeys() - var/dat = {"

Encryption Key Firmware


- When enabled, this device will be able to use up to two (2) encryption keys for departmental channel access.

- The device is currently [encryptmod ? "en" : "dis" ]abled.
[encryptmod ? "" : "Activate Encryption Key Ports
"]"} - return dat - - -// Universal Translator -/mob/living/silicon/pai/proc/softwareTranslator() - var/datum/language_holder/H = get_language_holder() - . = {"

Universal Translator


- When enabled, this device will permanently be able to speak and understand all known forms of communication.

- The device is currently [H.omnitongue ? "en" : "dis" ]abled.
[H.omnitongue ? "" : "Activate Translation Module
"]"} - return . - -// Security HUD -/mob/living/silicon/pai/proc/facialRecognition() - var/dat = {"

Facial Recognition Overlay


- When enabled, this package will scan all viewable faces and compare them against the known criminal database, providing real-time graphical data about any detected persons of interest.

- The package is currently [ (secHUD) ? "en" : "dis" ]abled.
- Toggle Package
- "} - return dat - -// Medical HUD -/mob/living/silicon/pai/proc/medicalAnalysis() - var/dat = "" - dat += {"

Medical Analysis Overlay


- When enabled, this package will scan all nearby crewmembers' vitals and provide real-time graphical data about their state of health.

- The suite is currently [ (medHUD) ? "en" : "dis" ]abled.
- Toggle Suite
- "} - return dat - -//Health Scanner -/mob/living/silicon/pai/proc/softwareHostScan() - - var/dat = "" - dat += {"

Host Bisoscan Settings


- - Change Scan Type
- - Toggle Verbosity
- "} - return dat -// Atmospheric Scanner -/mob/living/silicon/pai/proc/softwareAtmo() - var/dat = "

Atmospheric Sensor

" - - var/turf/T = get_turf(loc) - if (isnull(T)) - dat += "Unable to obtain a reading.
" +/** + * Host scan supporting proc + * + * Allows the pAI to scan its host's health vitals + * An integrated health analyzer. + */ +/mob/living/silicon/pai/proc/hostscan() + var/mob/living/silicon/pai/pAI = usr + var/mob/living/carbon/holder = get(pAI.card.loc, /mob/living/carbon) + if(holder) + pAI.hostscan.attack(holder, pAI) else - var/datum/gas_mixture/environment = T.return_air() - var/list/env_gases = environment.gases + to_chat(usr, span_warning("You are not being carried by anyone!")) + return FALSE - var/pressure = environment.return_pressure() - var/total_moles = environment.total_moles() +/** + * Extend cable supporting proc + * + * When doorjack is installed, allows the pAI to drop + * a cable which is placed either on the floor or in + * someone's hands based (on distance). + */ +/mob/living/silicon/pai/proc/extendcable() + QDEL_NULL(hacking_cable) //clear any old cables + hacking_cable = new + var/transfered_to_mob + if(isliving(card.loc)) + var/mob/living/hacker = card.loc + if(hacker.put_in_hands(hacking_cable)) + transfered_to_mob = TRUE + hacker.visible_message(span_warning("A port on [src] opens to reveal \a [hacking_cable], which you quickly grab hold of."), span_hear("You hear the soft click of something light and manage to catch hold of [hacking_cable].")) + if(!transfered_to_mob) + hacking_cable.forceMove(drop_location()) + hacking_cable.visible_message(span_warning("A port on [src] opens to reveal \a [hacking_cable], which promptly falls to the floor."), span_hear("You hear the soft click of something light and hard falling to the ground.")) - dat += "Air Pressure: [round(pressure,0.1)] kPa
" - - if (total_moles) - for(var/id in env_gases) - var/gas_level = env_gases[id][MOLES]/total_moles - if(gas_level > 0.01) - dat += "[env_gases[id][GAS_META][META_GAS_NAME]]: [round(gas_level*100)]%
" - dat += "Temperature: [round(environment.temperature-T0C)]°C
" - dat += "Refresh Reading
" - dat += "
" - return dat -// Door Jack -/mob/living/silicon/pai/proc/softwareDoor() - var/dat = "

Airlock Jack

" - dat += "Cable status : " - if(!hacking_cable) - dat += "Retracted
" - dat += "Extend Cable
" - return dat - if(!hacking_cable.machine) - dat += "Extended
" - return dat - - dat += "Connected
" - if(!istype(hacking_cable.machine, /obj/machinery/door)) - dat += "Connected device's firmware does not appear to be compatible with Airlock Jack protocols.
" - return dat - - if(!hackdoor) - dat += "Begin Airlock Jacking
" - else - dat += "Jack in progress... [hackprogress]% complete.
" - dat += "Cancel Airlock Jack
" - return dat - -// Door Jack - supporting proc +/** + * Door jacking supporting proc + * + * This begins the hacking process on a door. + * Mostly, this gives UI feedback, while the "hack" + * is handled inside pai.dm itself. + */ /mob/living/silicon/pai/proc/hackloop() - var/turf/T = get_turf(src) + var/turf/turf = get_turf(src) + playsound(src, 'sound/machines/airlock_alien_prying.ogg', 50, TRUE) + to_chat(usr, span_boldnotice("You begin overriding the airlock security protocols.")) for(var/mob/living/silicon/ai/AI in GLOB.player_list) - if(T.loc) - to_chat(AI, "Network Alert: Brute-force encryption crack in progress in [T.loc].") + if(turf.loc) + to_chat(AI, "Network Alert: Brute-force security override in progress in [turf.loc].") else - to_chat(AI, "Network Alert: Brute-force encryption crack in progress. Unable to pinpoint location.") + to_chat(AI, "Network Alert: Brute-force security override in progress. Unable to pinpoint location.") hacking = TRUE -// Digital Messenger -/mob/living/silicon/pai/proc/pdamessage() - - var/dat = "

Digital Messenger

" - dat += {"Signal/Receiver Status: - [(aiPDA.toff) ? "\[Off\]" : "\[On\]"]
- Ringer Status: - [(aiPDA.silent) ? "\[Off\]" : "\[On\]"]

"} - dat += "
    " - if(!aiPDA.toff) - for (var/obj/item/pda/P in get_viewable_pdas()) - if (P == aiPDA) - continue - dat += "
  • [P]" - dat += "
  • " - dat += "
" - dat += "

" - dat += "Messages:
[aiPDA.tnote]" - return dat - -/mob/living/silicon/pai/proc/softwarePrinter() - aicamera.paiprint(usr) +/** + * Proc that switches whether a pAI can refresh + * the records window again. + */ +/mob/living/silicon/pai/proc/refresh_again() + refresh_spam = FALSE diff --git a/tgui/packages/tgui/interfaces/PaiCard.tsx b/tgui/packages/tgui/interfaces/PaiCard.tsx new file mode 100644 index 00000000000..e0fb1f56839 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PaiCard.tsx @@ -0,0 +1,219 @@ +import { useBackend, useLocalState } from '../backend'; +import { Box, Button, LabeledList, NoticeBox, Section, Stack, Tabs } from '../components'; +import { Window } from '../layouts'; + +type PaiCardData = { + candidates: Candidate[]; + pai: Pai; +}; + +type Candidate = { + comments: string; + description: string; + key: string; + name: string; +}; + +type Pai = { + can_holo: number; + dna: string; + emagged: number; + laws: string; + master: string; + name: string; + transmit: number; + receive: number; +}; + +export const PaiCard = (_, context) => { + const { data } = useBackend(context); + const { pai } = data; + + return ( + + {!pai ? : } + + ); +}; + +/** Gives a list of candidates as cards */ +const PaiDownload = (_, context) => { + const { act, data } = useBackend(context); + const { candidates = [] } = data; + + return ( +
act('request')} + tooltip="Request candidates."> + Request + + } + fill + scrollable + title="Viewing pAI Candidates"> + {!candidates.length ? ( + None found! + ) : ( + + {candidates.map((candidate, index) => { + return ( + + + + ); + })} + + )} +
+ ); +}; + +/** Candidate card: Individual. Since this info is refreshing, + * had to make the comments and descriptions a separate tab. + * In longer entries, it is much more readable. + */ +const CandidateDisplay = (props, context) => { + const [tab, setTab] = useLocalState(context, 'tab', 'description'); + const { candidate } = props; + const { comments, description, name } = candidate; + + const onTabClickHandler = (tab: string) => { + setTab(tab); + }; + + return ( + +
+ } + fill + height={12} + scrollable + title="Candidate"> + + Name: {name || 'Randomized Name'} + + {tab === 'description' + ? (`Description: ${description.length && description || "None"}`) + : (`OOC Comments: ${comments.length && comments || "None"}`)} +
+
+ ); +}; + +/** Tabs for the candidate */ +const CandidateTabs = (props, context) => { + const { act } = useBackend(context); + const { candidate, onTabClick, tab } = props; + const { key } = candidate; + + return ( + + + + { + onTabClick('description'); + }} + selected={tab === 'description'}> + Description + + { + onTabClick('comments'); + }} + selected={tab === 'comments'}> + OOC + + + + + + + + ); +}; + +/** Once a pAI has been loaded, you can alter its settings here */ +const PaiOptions = (_, context) => { + const { act, data } = useBackend(context); + const { pai } = data; + const { can_holo, dna, emagged, laws, master, name, transmit, receive } = pai; + + return ( +
+ + + {master || ( + + )} + + {master && {dna}} + {laws} + + + + + + + + + + + + + + + + + + {!!emagged && ( + + )} +
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/PaiInterface.tsx b/tgui/packages/tgui/interfaces/PaiInterface.tsx new file mode 100644 index 00000000000..631bafa27e8 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PaiInterface.tsx @@ -0,0 +1,643 @@ +import { useBackend, useSharedState } from '../backend'; +import { Box, Button, LabeledList, Icon, NoticeBox, ProgressBar, Section, Stack, Table, Tabs, Tooltip } from '../components'; +import { Window } from '../layouts'; + +type PaiInterfaceData = { + available: Available; + directives: string; + door_jack?: string; + emagged: number; + image: string; + installed: string[]; + languages: number; + master: Master; + pda: PDA; + ram: number; + records: Records; + refresh_spam: number; +}; + +type Available = { + name: string; + value: string | number; +}; + +type Master = { + name: string; + dna: string; +}; + +type PDA = { + power: number; + silent: number; +}; + +type Records = { + medical?: CrewRecord[]; + security?: CrewRecord[]; +}; + +type CrewRecord = { + [key: string]: string; +}; + +enum Tab { + System = 1, + Directive = 2, + Installed = 3, + Available = 4, +} + +const DIRECTIVE_COMPREHENSION = `As an advanced software model, +you are a complex, thinking, sentient being. Unlike previous AI models, +you are capable of comprehending the subtle nuances of human language. +You may parse the spirit of a directive and follow its intent, rather than +tripping over pedantics and getting snared by technicalities. Above all, +you are machine in name and build only. In all other aspects, you may be +seen as the ideal, unwavering human companion that you are.`; + +const DIRECTIVE_ORDER = `Your prime directive comes before all others. +Should a supplemental directive conflict with it, you are capable of +simply discarding this inconsistency, ignoring the conflicting supplemental +directive and continuing to fulfillyour prime directive to the best +of your ability.`; + +const SOFTWARE_DESC = { + 'crew manifest': 'A tool that allows you to view the crew manifest.', + 'digital messenger': + 'A tool that allows you to send messages to other crew members.', + 'atmosphere sensor': + 'A tool that allows you to analyze local atmospheric contents.', + 'photography module': + 'A portable camera module. Engage, then click to shoot.', + 'camera zoom': 'A tool that allows you to zoom in on your camera.', + 'printer module': 'A portable printer module for photographs.', + 'remote signaler': + 'A remote signalling device to transmit and receive codes.', + 'medical records': 'A tool that allows you to view station medical records.', + 'security records': + 'A tool that allows you to view station security records, warrants.', + 'host scan': 'A portable health analyzer. Must be held to use.', + 'medical HUD': 'Allows you to view medical status using an overlay HUD.', + 'security HUD': 'Allows you to view security records using an overlay HUD.', + 'loudness booster': + 'Synthesizes instruments, plays sounds and imported songs.', + 'newscaster': + 'A tool that allows you to broadcast news to other crew members.', + 'door jack': 'A tool that allows you to open doors.', + 'encryption keys': + 'A tool that allows you to decrypt and speak on other radio frequencies.', + 'internal gps': 'A tool that allows you to track your location.', + 'universal translator': 'Translation module for non-common languages.', +}; + +const ICON_MAP = { + 'angry': 'angry', + 'cat': 'cat', + 'extremely-happy': 'grin-beam', + 'laugh': 'grin-squint', + 'happy': 'smile', + 'off': 'power-off', + 'sad': 'frown', + 'sunglasses': 'sun', + 'what': 'question', +}; + +export const PaiInterface = (_, context) => { + const [tab, setTab] = useSharedState(context, 'tab', 1); + const setTabHandler = (tab: number) => { + setTab(tab); + }; + + return ( + + + + + {tab === Tab.System && } + {tab === Tab.Directive && } + {tab === Tab.Installed && } + {tab === Tab.Available && } + + + + + + + + ); +}; + +/** Tabs at bottom of screen */ +const TabDisplay = (props) => { + const { tab, onTabClick } = props; + + return ( + + onTabClick(Tab.System)} + selected={tab === Tab.System}> + System + + onTabClick(Tab.Directive)} + selected={tab === Tab.Directive}> + Directives + + onTabClick(Tab.Installed)} + selected={tab === Tab.Installed}> + Installed + + onTabClick(Tab.Available)} + selected={tab === Tab.Available}> + Download + + + ); +}; + +const SystemDisplay = () => { + return ( + + + + + + + + + ); +}; + +/** Renders some ASCII art. Changes to red on emag. */ +const SystemWallpaper = (_, context) => { + const { data } = useBackend(context); + const { emagged } = data; + + const owner = !emagged ? 'NANOTRASEN' : ' SYNDICATE'; + const eyebrows = !emagged ? "/\\ ' /\\" : ' \\\\ // '; + + const paiAscii = [ + ' ________ ________ ___', + ' |\\ __ \\|\\ __ \\|\\ \\', + ' \\ \\ \\|\\ \\ \\ \\|\\ \\ \\ \\ Interface', + ' \\ \\ ____\\ \\ __ \\ \\ \\ Version 2.4', + ' \\ \\ \\___|\\ \\ \\ \\ \\ \\ \\', + ' \\ \\__\\ \\ \\__\\ \\__\\ \\__\\ Property of', + ` \\|__| \\|__|\\|__|\\|__| ${owner}`, + '', + ].join('\n'); + + const floofAscii = [ + ' .--. .-.', + " ,;;``;;-;,,..___.,,.-/ `;_//,.' )", + " .' ;; `; :; `;; ;; `. '/ .'", + ` ,; '; ; ' '; '; ,' ${eyebrows}';`, // lol + " /' ` \\ ` ;',' ( d\\__b_),", + " / / .,;;) ', ( .' __\\", + " ;:. \\ ,_ / ', ' .'_ \\/;", + " , ,;' `;;/ / ';,\\ `-..__._,'", + " ;:. /____ ..-'--. /-' ..---. ._._/ ---.", + " | ;' ;'| \\--/;' ,' / \\ , \\", + " `.fL__;,__/-..__)_)/ `--'--'`-._)_)/ --\\.._)_)/", + ].join('\n'); + + return ( +
+
+        {paiAscii}
+        {floofAscii}
+      
+
+ ); +}; + +/** Displays master info. + * You can check their DNA and change your image here. + */ +const SystemInfo = (_, context) => { + const { act, data } = useBackend(context); + const { image, master } = data; + + return ( +
+ + + + } + fill + scrollable + title="System Info"> + + + {master.name || 'None.'} + + {master.dna || 'None.'} + +
+ ); +}; + +/** Shows the hardcoded PAI info along with any supplied orders. */ +const DirectiveDisplay = (_, context) => { + const { data } = useBackend(context); + const { directives, master } = data; + + return ( + + +
+ + {DIRECTIVE_COMPREHENSION} +
+
+ {DIRECTIVE_ORDER} +
+
+
+ +
+ {!master.name ? ( + 'None.' + ) : ( + + + Serve your master. + + + {directives} + + + )} +
+
+
+ ); +}; + +/** Renders two sections: A section of buttons and + * another section that displays the selected installed + * software info. + */ +const InstalledDisplay = (_, context) => { + const [installSelected, setInstallSelected] = useSharedState( + context, + 'software', + '' + ); + const onInstallHandler = (software: string) => { + setInstallSelected(software); + }; + + return ( + + + + + + + + + ); +}; + +/** Iterates over installed software to render buttons. */ +const InstalledSoftware = (props, context) => { + const { data } = useBackend(context); + const { installed = [] } = data; + const { onInstallClick } = props; + + return ( +
+ {!installed.length ? ( + Nothing installed! + ) : ( + installed.map((software) => { + return ( + + ); + }) + )} +
+ ); +}; + +/** Software info for buttons clicked. */ +const InstalledInfo = (props) => { + const { software } = props; + + /** Records get their own section here */ + if (software === 'medical records') { + return ; + } else if (software === 'security records') { + return ; + } else { + return ( +
+ letter.toUpperCase()) + }> + {software && ( + + {SOFTWARE_DESC[software] || ''} + + + + + )} +
+ ); + } +}; + +/** Todo: Remove this entirely when records get a TGUI interface themselves */ +const RecordsDisplay = (props, context) => { + const { act, data } = useBackend(context); + const { record_type } = props; + const { records = [], refresh_spam } = data; + const convertedRecords: CrewRecord[] = records[record_type]; + + return ( +
+ + + + + + + + } + fill + scrollable> + + {convertedRecords?.map((record) => { + return ; + })} +
+
+ ); +}; + +/** Renders the labels for the record viewer */ +const RecordLabels = (props) => { + const { record_type } = props; + + return ( + + + + {record_type === 'medical' ? 'Physical Health' : 'Arrest Status'} + + + {record_type === 'medical' ? 'Mental Health' : 'Total Crimes'} + + +
+ ); +}; + +const RecordRow = (props) => { + const { record = [] } = props; + const convertedRecord = Object.values(record); + /** I do not want to show the ref here */ + const filteredRecord = convertedRecord.splice(1); + + return ( + + {filteredRecord?.map((value) => { + return {value}; + })} + + ); +}; + +/** Once a software is selected, generates custom buttons or a default + * power toggle. + */ +const SoftwareButtons = (props, context) => { + const { act, data } = useBackend(context); + const { door_jack, languages, pda } = data; + const { software } = props; + + switch (software) { + case 'digital messenger': + return ( + <> + + + + + ); + case 'door jack': + return ( + <> + + + + + ); + case 'host scan': { + return ( + <> + + + + + ); + } + case 'universal translator': + return ( + + ); + default: + return ( + + ); + } +}; + +const AvailableDisplay = () => { + return ( +
} + fill + scrollable + title="Available Software"> + +
+ ); +}; + +/** Displays the remaining RAM left as a progressbar. */ +const AvailableMemory = (_, context) => { + const { data } = useBackend(context); + const { ram } = data; + + return ( + + + + + + + + + + + ); +}; + +/** A list of available software. + * creates table rows for each, like a vendor. + */ +const AvailableSoftware = (_, context) => { + const { data } = useBackend(context); + const { available } = data; + const convertedList: Available[] = Object.entries(available).map((key) => { + return { name: key[0], value: key[1] }; + }); + + return ( + + {convertedList?.map((software) => { + return ; + })} +
+ ); +}; + +/** A row for an individual software listing. */ +const AvailableRow = (props, context) => { + const { act, data } = useBackend(context); + const { ram } = data; + const { installed } = data; + const { software } = props; + const purchased = installed.includes(software.name); + + return ( + + + + {software.name.replace(/^\w/, (c) => c.toUpperCase())} + + + + + {!purchased && software.value}{' '} + = software.value ? 'purple' : 'bad'} + name={purchased ? 'check' : 'microchip'} + /> + + + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/PaiSubmit.tsx b/tgui/packages/tgui/interfaces/PaiSubmit.tsx new file mode 100644 index 00000000000..4f1c265524c --- /dev/null +++ b/tgui/packages/tgui/interfaces/PaiSubmit.tsx @@ -0,0 +1,148 @@ +import { useBackend, useLocalState } from '../backend'; +import { Box, Button, Input, Section, Stack, Tooltip } from '../components'; +import { Window } from '../layouts'; + +type CandidateData = { + comments: string; + description: string; + name: string; +}; + +const PAI_DESCRIPTION = `Personal AIs are advanced models +capable of nuanced interaction. They are designed to be used +in a variety of situations, assisting their masters in their +work. They do not possess hands, thus they cannot interact with +equipment or items. While in hologram form, you cannot be +directly killed, but you may be incapacitated.`; + +const PAI_RULES = `You are expected to role play to some degree. +Keep in mind: Not entering information may lead to you not being +selected. Press submit to alert pAI cards of your candidacy.`; + +export const PaiSubmit = (_, context) => { + const { data } = useBackend(context); + const [input, setInput] = useLocalState(context, 'input', { + comments: data.comments || '', + description: data.description || '', + name: data.name || '', + }); + const onChangeHandler = (e, value) => { + setInput({ ...input, [value]: e.target.value }); + }; + + return ( + + + + + + + + + + + + + + + + ); +}; + +/** Displays basic info about playing pAI */ +const DetailsDisplay = () => { + return ( +
+ + {PAI_DESCRIPTION} +
+
+ {PAI_RULES} +
+
+ ); +}; + +/** Input boxes for submission details */ +const InputDisplay = (props) => { + const { input, onChangeHandler } = props; + const { comments, description, name } = input; + + return ( +
+ + + + + Name + + onChangeHandler(e, 'name')} + /> + + + + + + Description + + onChangeHandler(e, 'description')} + /> + + + + + + OOC Comments + + onChangeHandler(e, 'comments')} + /> + + + +
+ ); +}; + +/** Gives the user a submit button */ +const ButtonsDisplay = (props, context) => { + const { act } = useBackend(context); + const { input } = props; + return ( +
+ + + + + + + + + + + +
+ ); +};