diff --git a/_maps/RandomRuins/SpaceRuins/TheDerelict.dmm b/_maps/RandomRuins/SpaceRuins/TheDerelict.dmm index 9189d9fbc64..b8cf7f5c429 100644 --- a/_maps/RandomRuins/SpaceRuins/TheDerelict.dmm +++ b/_maps/RandomRuins/SpaceRuins/TheDerelict.dmm @@ -2320,7 +2320,7 @@ "vV" = ( /obj/structure/table, /obj/effect/decal/cleanable/dirt, -/obj/item/paicard, +/obj/item/pai_card, /turf/open/floor/iron/airless, /area/ruin/space/ks13/science/rnd) "vY" = ( diff --git a/_maps/RandomRuins/SpaceRuins/bus.dmm b/_maps/RandomRuins/SpaceRuins/bus.dmm index 6bd2aa64706..ae86e775dc7 100644 --- a/_maps/RandomRuins/SpaceRuins/bus.dmm +++ b/_maps/RandomRuins/SpaceRuins/bus.dmm @@ -157,7 +157,7 @@ /area/ruin/unpowered/no_grav) "iE" = ( /obj/structure/table, -/obj/item/paicard, +/obj/item/pai_card, /turf/open/misc/asteroid/airless, /area/ruin/unpowered/no_grav) "jH" = ( diff --git a/_maps/RandomZLevels/moonoutpost19.dmm b/_maps/RandomZLevels/moonoutpost19.dmm index 56411df2435..837d0f492d6 100644 --- a/_maps/RandomZLevels/moonoutpost19.dmm +++ b/_maps/RandomZLevels/moonoutpost19.dmm @@ -3109,7 +3109,7 @@ /area/awaymission/moonoutpost19/research) "ic" = ( /obj/structure/rack, -/obj/item/paicard{ +/obj/item/pai_card{ pixel_x = 4 }, /obj/effect/turf_decal/stripes/line{ diff --git a/_maps/RandomZLevels/undergroundoutpost45.dmm b/_maps/RandomZLevels/undergroundoutpost45.dmm index fa8afd7033f..5f2e03eac3f 100644 --- a/_maps/RandomZLevels/undergroundoutpost45.dmm +++ b/_maps/RandomZLevels/undergroundoutpost45.dmm @@ -6241,7 +6241,7 @@ /obj/item/taperecorder{ pixel_x = -3 }, -/obj/item/paicard{ +/obj/item/pai_card{ pixel_x = 4 }, /turf/open/floor/iron/cafeteria{ diff --git a/_maps/map_files/Deltastation/DeltaStation2.dmm b/_maps/map_files/Deltastation/DeltaStation2.dmm index 8c2f20f2fb1..2727e51a884 100644 --- a/_maps/map_files/Deltastation/DeltaStation2.dmm +++ b/_maps/map_files/Deltastation/DeltaStation2.dmm @@ -17766,7 +17766,7 @@ /area/station/hallway/primary/aft) "eTM" = ( /obj/structure/table/wood, -/obj/item/paicard, +/obj/item/pai_card, /obj/structure/window/reinforced{ dir = 8 }, @@ -20373,7 +20373,7 @@ "fAt" = ( /obj/structure/table/wood, /obj/item/flashlight/lamp, -/obj/item/paicard{ +/obj/item/pai_card{ pixel_x = -6 }, /turf/open/floor/carpet/red, @@ -21920,7 +21920,7 @@ /area/station/medical/medbay/central) "fYX" = ( /obj/structure/table/wood, -/obj/item/paicard, +/obj/item/pai_card, /turf/open/floor/iron/grimy, /area/station/command/corporate_showroom) "fYZ" = ( @@ -36081,7 +36081,7 @@ /area/station/medical/treatment_center) "jNN" = ( /obj/structure/table, -/obj/item/paicard, +/obj/item/pai_card, /obj/structure/cable, /obj/effect/turf_decal/tile/neutral/fourcorners, /turf/open/floor/iron, @@ -38835,7 +38835,7 @@ /area/station/medical/surgery/theatre) "kyN" = ( /obj/structure/table, -/obj/item/paicard, +/obj/item/pai_card, /obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4, /obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2, /turf/open/floor/iron, @@ -40886,7 +40886,7 @@ /obj/item/pen, /obj/machinery/light/directional/south, /obj/machinery/status_display/evac/directional/south, -/obj/item/paicard, +/obj/item/pai_card, /turf/open/floor/wood, /area/station/commons/dorms) "lbt" = ( @@ -52245,7 +52245,7 @@ /area/station/engineering/lobby) "ocB" = ( /obj/structure/table/wood, -/obj/item/paicard, +/obj/item/pai_card, /turf/open/floor/iron/grimy, /area/station/service/library) "ocO" = ( @@ -71389,7 +71389,7 @@ "toL" = ( /obj/structure/table, /obj/item/folder/white, -/obj/item/paicard, +/obj/item/pai_card, /obj/effect/turf_decal/tile/purple{ dir = 1 }, @@ -74006,7 +74006,7 @@ /obj/structure/table/reinforced, /obj/item/wrench, /obj/item/crowbar, -/obj/item/paicard, +/obj/item/pai_card, /obj/effect/turf_decal/bot, /obj/machinery/newscaster/directional/south, /turf/open/floor/iron, @@ -78070,7 +78070,7 @@ /area/station/maintenance/department/electrical) "uZu" = ( /obj/structure/table/wood, -/obj/item/paicard, +/obj/item/pai_card, /turf/open/floor/wood, /area/station/command/meeting_room/council) "uZL" = ( @@ -80549,7 +80549,7 @@ "vGu" = ( /obj/structure/table/wood, /obj/effect/decal/cleanable/dirt, -/obj/item/paicard, +/obj/item/pai_card, /turf/open/floor/carpet, /area/station/service/library/abandoned) "vGG" = ( @@ -81798,7 +81798,7 @@ "vXA" = ( /obj/structure/table/reinforced, /obj/item/folder/white, -/obj/item/paicard, +/obj/item/pai_card, /obj/effect/turf_decal/bot, /obj/machinery/airalarm/directional/west, /obj/structure/extinguisher_cabinet/directional/north, @@ -82451,7 +82451,7 @@ /obj/structure/disposalpipe/segment, /obj/item/healthanalyzer, /obj/item/healthanalyzer, -/obj/item/paicard, +/obj/item/pai_card, /obj/effect/turf_decal/bot, /turf/open/floor/iron, /area/station/science/robotics/lab) diff --git a/_maps/map_files/IceBoxStation/IceBoxStation.dmm b/_maps/map_files/IceBoxStation/IceBoxStation.dmm index 64fab5087b8..db1218e7b0a 100644 --- a/_maps/map_files/IceBoxStation/IceBoxStation.dmm +++ b/_maps/map_files/IceBoxStation/IceBoxStation.dmm @@ -9191,7 +9191,7 @@ /area/station/commons/locker) "cIK" = ( /obj/structure/table/wood, -/obj/item/paicard, +/obj/item/pai_card, /obj/effect/mapping_helpers/broken_floor, /turf/open/floor/wood, /area/station/maintenance/fore) @@ -39346,7 +39346,7 @@ /obj/machinery/power/apc/auto_name/directional/north, /obj/structure/cable, /obj/structure/table, -/obj/item/paicard, +/obj/item/pai_card, /obj/item/taperecorder{ pixel_x = -3; pixel_y = 2 @@ -53655,7 +53655,7 @@ /obj/effect/turf_decal/tile/neutral/fourcorners, /obj/structure/rack, /obj/item/storage/briefcase, -/obj/item/paicard, +/obj/item/pai_card, /turf/open/floor/iron, /area/station/commons/locker) "qyf" = ( @@ -62073,7 +62073,7 @@ /area/station/science/robotics/lab) "tfO" = ( /obj/structure/table/wood, -/obj/item/paicard, +/obj/item/pai_card, /turf/open/floor/iron, /area/station/commons/dorms) "tfR" = ( @@ -72794,7 +72794,7 @@ /obj/effect/turf_decal/siding/wood{ dir = 9 }, -/obj/item/paicard, +/obj/item/pai_card, /obj/structure/table/wood, /obj/effect/turf_decal/tile/bar/opposingcorners, /turf/open/floor/iron, diff --git a/_maps/map_files/KiloStation/KiloStation.dmm b/_maps/map_files/KiloStation/KiloStation.dmm index 5a94405c6d8..52fc17e6f56 100644 --- a/_maps/map_files/KiloStation/KiloStation.dmm +++ b/_maps/map_files/KiloStation/KiloStation.dmm @@ -44785,7 +44785,7 @@ /obj/effect/turf_decal/tile/neutral{ dir = 4 }, -/obj/item/paicard, +/obj/item/pai_card, /obj/effect/decal/cleanable/dirt, /turf/open/floor/iron/dark, /area/station/service/bar/atrium) @@ -66152,7 +66152,7 @@ /obj/item/folder{ pixel_x = -4 }, -/obj/item/paicard, +/obj/item/pai_card, /obj/machinery/light/directional/north, /obj/effect/turf_decal/stripes/corner{ dir = 8 @@ -73907,7 +73907,7 @@ /obj/item/hand_labeler, /obj/machinery/airalarm/directional/west, /obj/machinery/light/directional/west, -/obj/item/paicard{ +/obj/item/pai_card{ pixel_x = 6 }, /obj/item/aicard, @@ -76681,7 +76681,7 @@ /area/station/hallway/primary/central) "vMN" = ( /obj/structure/table/wood, -/obj/item/paicard, +/obj/item/pai_card, /turf/open/floor/wood/tile, /area/station/service/library) "vMW" = ( diff --git a/_maps/map_files/MetaStation/MetaStation.dmm b/_maps/map_files/MetaStation/MetaStation.dmm index 6990d8858d9..6c989ee8544 100644 --- a/_maps/map_files/MetaStation/MetaStation.dmm +++ b/_maps/map_files/MetaStation/MetaStation.dmm @@ -7368,7 +7368,7 @@ /area/station/commons/lounge) "cNC" = ( /obj/structure/table/wood, -/obj/item/paicard, +/obj/item/pai_card, /turf/open/floor/wood, /area/station/service/library) "cNS" = ( @@ -10504,7 +10504,7 @@ pixel_x = -2; pixel_y = 6 }, -/obj/item/paicard, +/obj/item/pai_card, /obj/effect/turf_decal/tile/purple/anticorner/contrasted, /turf/open/floor/iron/white, /area/station/science/lobby) @@ -14503,7 +14503,7 @@ dir = 1 }, /obj/item/aicard, -/obj/item/paicard, +/obj/item/pai_card, /obj/item/circuitboard/aicore, /obj/machinery/keycard_auth/directional/north{ pixel_x = -5 @@ -25113,7 +25113,7 @@ pixel_x = 8; pixel_y = 32 }, -/obj/item/paicard{ +/obj/item/pai_card{ desc = "A real Nanotrasen success, these personal AIs provide all of the companionship of an AI without any law related red-tape."; name = "\improper Nanotrasen-brand personal AI device exhibit" }, @@ -49716,7 +49716,7 @@ /area/station/engineering/supermatter) "rHp" = ( /obj/structure/table, -/obj/item/paicard, +/obj/item/pai_card, /turf/open/floor/iron, /area/station/commons/locker) "rHr" = ( diff --git a/_maps/map_files/generic/CentCom.dmm b/_maps/map_files/generic/CentCom.dmm index 7e4d815cb27..331e24f70fb 100644 --- a/_maps/map_files/generic/CentCom.dmm +++ b/_maps/map_files/generic/CentCom.dmm @@ -2333,7 +2333,7 @@ /area/centcom/syndicate_mothership/control) "gM" = ( /obj/structure/table/wood, -/obj/item/paicard, +/obj/item/pai_card, /turf/open/floor/wood/tile, /area/centcom/syndicate_mothership/control) "gO" = ( diff --git a/_maps/map_files/tramstation/tramstation.dmm b/_maps/map_files/tramstation/tramstation.dmm index f3d6e298588..0fdcb82f914 100644 --- a/_maps/map_files/tramstation/tramstation.dmm +++ b/_maps/map_files/tramstation/tramstation.dmm @@ -6176,7 +6176,7 @@ /area/station/command/heads_quarters/hos) "ckt" = ( /obj/structure/table/wood, -/obj/item/paicard, +/obj/item/pai_card, /obj/item/radio/intercom/directional/west, /turf/open/floor/wood/large, /area/station/service/library) @@ -50834,7 +50834,7 @@ /area/station/engineering/supermatter/room) "rEZ" = ( /obj/structure/table, -/obj/item/paicard, +/obj/item/pai_card, /turf/open/floor/iron, /area/station/commons/fitness/recreation) "rFv" = ( @@ -52220,7 +52220,7 @@ /area/station/cargo/miningdock) "sbb" = ( /obj/structure/table/wood, -/obj/item/paicard, +/obj/item/pai_card, /turf/open/floor/carpet, /area/station/hallway/secondary/entry) "sbh" = ( @@ -54524,7 +54524,7 @@ /obj/effect/turf_decal/trimline/purple/filled/line{ dir = 8 }, -/obj/item/paicard, +/obj/item/pai_card, /turf/open/floor/iron/white, /area/station/science/lobby) "sUd" = ( @@ -64599,7 +64599,7 @@ /obj/effect/turf_decal/stripes/line{ dir = 4 }, -/obj/item/paicard, +/obj/item/pai_card, /obj/item/taperecorder{ pixel_x = -3; pixel_y = 2 diff --git a/_maps/shuttles/emergency_nature.dmm b/_maps/shuttles/emergency_nature.dmm index 3b4d869aa69..b16832728db 100644 --- a/_maps/shuttles/emergency_nature.dmm +++ b/_maps/shuttles/emergency_nature.dmm @@ -845,7 +845,7 @@ /obj/item/food/monkeycube{ pixel_x = 5 }, -/obj/item/paicard{ +/obj/item/pai_card{ pixel_x = -6; pixel_y = 1 }, diff --git a/code/__DEFINES/colors.dm b/code/__DEFINES/colors.dm index a1221c897d0..bfa7fc874d9 100644 --- a/code/__DEFINES/colors.dm +++ b/code/__DEFINES/colors.dm @@ -55,6 +55,7 @@ #define COLOR_VERY_DARK_LIME_GREEN "#003300" #define COLOR_GREEN "#008000" #define COLOR_DARK_MODERATE_LIME_GREEN "#44964A" +#define COLOR_PAI_GREEN "#00FF88" #define COLOR_CYAN "#00FFFF" #define COLOR_DARK_CYAN "#00A2FF" diff --git a/code/__DEFINES/hud.dm b/code/__DEFINES/hud.dm index a4b21aec213..33fcfb689f0 100644 --- a/code/__DEFINES/hud.dm +++ b/code/__DEFINES/hud.dm @@ -177,12 +177,12 @@ #define ui_pai_chassis "SOUTH:6,WEST+2" #define ui_pai_rest "SOUTH:6,WEST+3" #define ui_pai_light "SOUTH:6,WEST+4" -#define ui_pai_newscaster "SOUTH:6,WEST+5" -#define ui_pai_host_monitor "SOUTH:6,WEST+6" -#define ui_pai_crew_manifest "SOUTH:6,WEST+7" -#define ui_pai_state_laws "SOUTH:6,WEST+8" +#define ui_pai_state_laws "SOUTH:6,WEST+5" +#define ui_pai_crew_manifest "SOUTH:6,WEST+6" +#define ui_pai_host_monitor "SOUTH:6,WEST+7" +#define ui_pai_internal_gps "SOUTH:6,WEST+8" #define ui_pai_mod_int "SOUTH:6,WEST+9" -#define ui_pai_internal_gps "SOUTH:6,WEST+10" +#define ui_pai_newscaster "SOUTH:6,WEST+10" #define ui_pai_take_picture "SOUTH:6,WEST+11" #define ui_pai_view_images "SOUTH:6,WEST+12" #define ui_pai_radio "SOUTH:6,WEST+13" diff --git a/code/__DEFINES/pai.dm b/code/__DEFINES/pai.dm new file mode 100644 index 00000000000..f47524d32e2 --- /dev/null +++ b/code/__DEFINES/pai.dm @@ -0,0 +1,31 @@ +/// Time between fold out +#define HOLOCHASSIS_COOLDOWN (5 SECONDS) +/// Time it takes a new pAI to fold out +#define HOLOCHASSIS_INIT_TIME (40 SECONDS) +/// Overloaded or emagged foldout cooldown +#define HOLOCHASSIS_OVERLOAD_COOLDOWN (10 SECONDS) +/// Regeneration rate for the holochassis +#define HOLOCHASSIS_REGEN_PER_SECOND 1.25 +/// The max health of the holochassis +#define HOLOCHASSIS_MAX_HEALTH 20 +/// The amount of time between spamming for pAI candidates +#define PAI_SPAM_TIME (40 SECONDS) + +/// UI action to toggle huds +#define PAI_TOGGLE_MEDICAL_HUD 0 +#define PAI_TOGGLE_SECURITY_HUD 1 + +/// UI action to use integrated scanner on target +#define PAI_SCAN_TARGET 0 +#define PAI_SCAN_MASTER 1 + +/// UI actions for door jack +#define PAI_DOOR_JACK_CABLE 0 +#define PAI_DOOR_JACK_HACK 1 +#define PAI_DOOR_JACK_CANCEL 2 + +/// UI actions for photography module +#define PAI_PHOTO_MODE_CAMERA 0 +#define PAI_PHOTO_MODE_PRINTER 1 +#define PAI_PHOTO_MODE_ZOOM 2 + diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm index feb2736a111..d712ec53531 100644 --- a/code/__HELPERS/game.dm +++ b/code/__HELPERS/game.dm @@ -213,8 +213,7 @@ if (!question) question = "Would you like to be a special role?" var/list/result = list() - for(var/candidate in group) - var/mob/candidate_mob = candidate + for(var/mob/candidate_mob as anything in group) if(!candidate_mob.key || !candidate_mob.client || (ignore_category && GLOB.poll_ignore[ignore_category] && (candidate_mob.ckey in GLOB.poll_ignore[ignore_category]))) continue if(be_special_flag) diff --git a/code/_onclick/ai.dm b/code/_onclick/ai.dm index b8a908b341f..b433866915a 100644 --- a/code/_onclick/ai.dm +++ b/code/_onclick/ai.dm @@ -82,7 +82,7 @@ return if(aicamera.in_camera_mode) - aicamera.camera_mode_off() + aicamera.toggle_camera_mode(sound = FALSE) aicamera.captureimage(pixel_turf, usr) return if(waypoint_mode) diff --git a/code/_onclick/cyborg.dm b/code/_onclick/cyborg.dm index 7f12d938471..5f0297552e2 100644 --- a/code/_onclick/cyborg.dm +++ b/code/_onclick/cyborg.dm @@ -49,7 +49,7 @@ face_atom(A) // change direction to face what you clicked on if(aicamera.in_camera_mode) //Cyborg picture taking - aicamera.camera_mode_off() + aicamera.toggle_camera_mode(sound = FALSE) aicamera.captureimage(A, usr) return diff --git a/code/_onclick/pai.dm b/code/_onclick/pai.dm deleted file mode 100644 index e36fb979026..00000000000 --- a/code/_onclick/pai.dm +++ /dev/null @@ -1,6 +0,0 @@ -/mob/living/silicon/pai/ClickOn(atom/A, params) - ..() - if(aicamera.in_camera_mode) //pAI picture taking - aicamera.camera_mode_off() - aicamera.captureimage(A, usr, aicamera.picture_size_x - 1, aicamera.picture_size_y - 1) - return diff --git a/code/controllers/subsystem/pai.dm b/code/controllers/subsystem/pai.dm index 3c66d28dc15..58f4eda0a05 100644 --- a/code/controllers/subsystem/pai.dm +++ b/code/controllers/subsystem/pai.dm @@ -1,70 +1,13 @@ SUBSYSTEM_DEF(pai) name = "pAI" - flags = SS_NO_INIT|SS_NO_FIRE /// List of pAI candidates, including those not submitted. var/list/candidates = list() - /// Prevents a crew member from hitting "request pAI" - var/request_spam = FALSE - /// Prevents a pAI from submitting itself repeatedly and sounding an alert. - var/submit_spam = FALSE /// All pAI cards on the map. var/list/pai_card_list = list() - -/// Created when a user clicks the "pAI candidate" window -/datum/pai_candidate - /// User inputted OOC comments - var/comments - /// User inputted behavior description - var/description - /// User's ckey - not input - var/key - /// User's pAI name. If blank, ninja name. - var/name - /// If the user has hit "submit" - var/ready = FALSE - -/** - * 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(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 - -/** - * This is the primary window proc when the pAI candidate - * hud menu is pressed by observers. - * - * Arguments - * @user - The ghost doing the pressing. - */ -/datum/controller/subsystem/pai/proc/recruitWindow(mob/user) - /// Searches for a previous candidate upon opening the menu - var/datum/pai_candidate/candidate = check_candidate(user) - if(isnull(candidate)) - candidate = new /datum/pai_candidate() - candidate.key = user.key - candidates.Add(candidate) - ui_interact(user) - -/datum/controller/subsystem/pai/ui_state(mob/user) - return GLOB.observer_state + /// Prevents a pAI from submitting itself repeatedly and sounding an alert. + var/submit_spam = FALSE /datum/controller/subsystem/pai/ui_interact(mob/user, datum/tgui/ui) . = ..() @@ -72,12 +15,15 @@ SUBSYSTEM_DEF(pai) if(!ui) ui = new(user, src, "PaiSubmit") ui.open() + ui.set_autoupdate(FALSE) + +/datum/controller/subsystem/pai/ui_state(mob/user) + return GLOB.observer_state /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) + var/datum/pai_candidate/candidate = candidates[user.ckey] if(isnull(candidate)) return data data["comments"] = candidate.comments @@ -88,49 +34,52 @@ SUBSYSTEM_DEF(pai) /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) + return TRUE + var/datum/pai_candidate/candidate = candidates[usr.ckey] + if(is_banned_from(usr.ckey, ROLE_PAI)) + to_chat(usr, span_warning("You are banned from playing pAI!")) + ui.close() + return FALSE if(isnull(candidate)) to_chat(usr, span_warning("There was an error. Please resubmit.")) ui.close() return FALSE switch(action) if("submit") - candidate.comments = params["candidate"]["comments"] - candidate.description = params["candidate"]["description"] - candidate.name = params["candidate"]["name"] + candidate.comments = trim(params["comments"], MAX_BROADCAST_LEN) + candidate.description = trim(params["description"], MAX_BROADCAST_LEN) + candidate.name = trim(params["name"], MAX_NAME_LEN) + candidate.ckey = usr.ckey candidate.ready = TRUE ui.close() submit_alert() + return TRUE if("save") - candidate.comments = params["candidate"]["comments"] - candidate.description = params["candidate"]["description"] - candidate.name = params["candidate"]["name"] + candidate.comments = params["comments"] + candidate.description = params["description"] + candidate.name = params["name"] candidate.savefile_save(usr) + return TRUE 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 + return TRUE + return FALSE /** - * Finds the candidate in question from the list of candidates. + * This is the primary window proc when the pAI candidate + * hud menu is pressed by observers. + * + * @params {mob} user The ghost doing the pressing. */ -/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 +/datum/controller/subsystem/pai/proc/recruit_window(mob/user) + /// Searches for a previous candidate upon opening the menu + var/datum/pai_candidate/candidate = candidates[user.ckey] + if(isnull(candidate)) + candidate = new(user.ckey) + candidates[user.ckey] = candidate + ui_interact(user) + /** * Pings all pAI cards on the station that new candidates are available. @@ -140,28 +89,9 @@ SUBSYSTEM_DEF(pai) 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() + for(var/obj/item/pai_card/pai_card as anything in pai_card_list) + if(!pai_card.pai) + pai_card.alert_update() to_chat(usr, span_notice("Your pAI candidacy has been submitted!")) - addtimer(CALLBACK(src, .proc/submit_again), 10 SECONDS) + addtimer(VARSET_CALLBACK(src, submit_spam, FALSE), PAI_SPAM_TIME, TIMER_UNIQUE | TIMER_STOPPABLE | TIMER_CLIENT_TIME | TIMER_DELETE_ME) 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 - diff --git a/code/datums/datacore.dm b/code/datums/datacore.dm index b769703a8c5..f7b903ec265 100644 --- a/code/datums/datacore.dm +++ b/code/datums/datacore.dm @@ -315,51 +315,6 @@ 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/human, client/client, show_directions = list(SOUTH)) var/datum/job/humans_job = human.mind.assigned_role var/datum/preferences/humans_prefs diff --git a/code/game/objects/items/devices/paicard.dm b/code/game/objects/items/devices/paicard.dm deleted file mode 100644 index b623dd5c63a..00000000000 --- a/code/game/objects/items/devices/paicard.dm +++ /dev/null @@ -1,231 +0,0 @@ -/obj/item/paicard - name = "personal AI device" - icon = 'icons/obj/aicards.dmi' - icon_state = "pai" - inhand_icon_state = "electronic" - worn_icon_state = "electronic" - lefthand_file = 'icons/mob/inhands/misc/devices_lefthand.dmi' - righthand_file = 'icons/mob/inhands/misc/devices_righthand.dmi' - w_class = WEIGHT_CLASS_SMALL - slot_flags = ITEM_SLOT_BELT - custom_premium_price = PAYCHECK_COMMAND * 1.25 - resistance_flags = FIRE_PROOF | ACID_PROOF | INDESTRUCTIBLE - - /// Spam alert prevention - var/alert_cooldown - /// If the pAIcard is slotted in a PDA - var/slotted = FALSE - /// Any pAI personalities inserted - var/mob/living/silicon/pai/pai - ///what emotion icon we have. handled in /mob/living/silicon/pai/Topic() - var/emotion_icon = "off" - -/obj/item/paicard/suicide_act(mob/living/user) - user.visible_message(span_suicide("[user] is staring sadly at [src]! [user.p_they()] can't keep living without real human intimacy!")) - return OXYLOSS - -/obj/item/paicard/Initialize(mapload) - SSpai.pai_card_list += src - . = ..() - update_appearance() - -/obj/item/paicard/vv_edit_var(vname, vval) - . = ..() - if(vname == NAMEOF(src, emotion_icon)) - update_appearance() - -/obj/item/paicard/handle_atom_del(atom/A) - if(A == pai) //double check /mob/living/silicon/pai/Destroy() if you change these. - pai = null - emotion_icon = initial(emotion_icon) - update_appearance() - return ..() - -/obj/item/paicard/update_overlays() - . = ..() - . += "pai-[emotion_icon]" - if(pai?.hacking_cable) - . += "[initial(icon_state)]-connector" - -/obj/item/paicard/Destroy() - //Will stop people throwing friend pAIs into the singularity so they can respawn - SSpai.pai_card_list -= src - if(!QDELETED(pai)) - QDEL_NULL(pai) - return ..() - -/obj/item/paicard/attack_self(mob/user) - if (!in_range(src, user)) - return - user.set_machine(src) - ui_interact(user) - -/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() - -/obj/item/paicard/ui_state(mob/user) - return GLOB.paicard_state - -/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 - -/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(pai) - return FALSE - if(SSpai.check_ready(candidate) != candidate) - return FALSE - /// The newly downloaded pAI personality - var/mob/living/silicon/pai/new_pai = new(src) - new_pai.name = candidate.name || pick(GLOB.ninja_names) - new_pai.real_name = new_pai.name - new_pai.key = candidate.key - setPersonality(new_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.")) - for(var/effect in typesof(/datum/status_effect/speech)) - pai.remove_status_effect(effect) - - 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/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("set_laws") - var/newlaws = tgui_input_text(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, TRUE) - 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 - else //receiving - 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_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") - return - if(!pai) - return - 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 -// WIRE_TRANSMIT = 4 - -/obj/item/paicard/proc/setPersonality(mob/living/silicon/pai/personality) - pai = personality - emotion_icon = "null" - update_appearance() - - playsound(loc, 'sound/effects/pai_boot.ogg', 50, TRUE, -1) - audible_message("\The [src] plays a cheerful startup noise!") - -/obj/item/paicard/proc/alertUpdate() - if(!COOLDOWN_FINISHED(src, alert_cooldown)) - return - COOLDOWN_START(src, alert_cooldown, 5 SECONDS) - add_alert() - addtimer(CALLBACK(src, .proc/remove_alert), 5 SECONDS) - playsound(src, 'sound/machines/ping.ogg', 30, TRUE) - loc.visible_message(span_info("[src] flashes a message across its screen, \"Additional personalities available for download.\""), blind_message = span_notice("[src] vibrates with an alert.")) - -/obj/item/paicard/proc/add_alert() - add_overlay( - list(mutable_appearance(icon, "[initial(icon_state)]-alert"), - emissive_appearance(icon, "[initial(icon_state)]-alert", alpha = src.alpha))) - -/obj/item/paicard/proc/remove_alert() - cut_overlays() - -/obj/item/paicard/emp_act(severity) - . = ..() - if (. & EMP_PROTECT_SELF) - return - 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/game/objects/items/gift.dm b/code/game/objects/items/gift.dm index 86dec2e5436..c9d3b8c9abf 100644 --- a/code/game/objects/items/gift.dm +++ b/code/game/objects/items/gift.dm @@ -77,7 +77,7 @@ GLOBAL_LIST_EMPTY(possible_gifts) /obj/item/banhammer, /obj/item/food/grown/ambrosia/deus, /obj/item/food/grown/ambrosia/vulgaris, - /obj/item/paicard, + /obj/item/pai_card, /obj/item/instrument/violin, /obj/item/instrument/guitar, /obj/item/storage/belt/utility/full, diff --git a/code/modules/admin/verbs/debug.dm b/code/modules/admin/verbs/debug.dm index 6970fa9265a..57dd9ccd3b2 100644 --- a/code/modules/admin/verbs/debug.dm +++ b/code/modules/admin/verbs/debug.dm @@ -36,39 +36,6 @@ log_admin("[key_name(src)] has robotized [M.key].") INVOKE_ASYNC(M, /mob.proc/Robotize) -/client/proc/makepAI(turf/T in GLOB.mob_list) - set category = "Admin.Fun" - set name = "Make pAI" - set desc = "Specify a location to spawn a pAI device, then specify a key to play that pAI" - - var/list/available = list() - for(var/mob/C in GLOB.mob_list) - if(C.key) - available.Add(C) - var/mob/choice = tgui_input_list(usr, "Choose a player to play the pAI", "Spawn pAI", sort_names(available)) - if(isnull(choice)) - return - if(!isobserver(choice)) - var/confirm = tgui_alert(usr, "[choice.key] isn't ghosting right now. Are you sure you want to yank them out of their body and place them in this pAI?", "Spawn pAI Confirmation", list("Yes", "No")) - if(confirm != "Yes") - return - var/obj/item/paicard/card = new(T) - var/mob/living/silicon/pai/pai = new(card) - - var/chosen_name = input(choice, "Enter your pAI name:", "pAI Name", "Personal AI") as text|null - - if (isnull(chosen_name)) - return - - pai.name = chosen_name - pai.real_name = pai.name - pai.key = choice.key - card.setPersonality(pai) - for(var/datum/pai_candidate/candidate in SSpai.candidates) - if(candidate.key == choice.key) - SSpai.candidates.Remove(candidate) - SSblackbox.record_feedback("tally", "admin_verb", 1, "Make pAI") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - /client/proc/poll_type_to_del(search_string) var/list/types = get_fancy_list_of_atom_types() if (!isnull(search_string) && search_string != "") diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm index d0fb28fbf8e..5f3ec404146 100644 --- a/code/modules/mob/dead/observer/observer.dm +++ b/code/modules/mob/dead/observer/observer.dm @@ -948,7 +948,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp /mob/dead/observer/proc/register_pai() if(isobserver(src)) - SSpai.recruitWindow(src) + SSpai.recruit_window(src) else to_chat(usr, span_warning("Can't become a pAI candidate while not dead!")) diff --git a/code/modules/mob/living/silicon/laws.dm b/code/modules/mob/living/silicon/laws.dm index 62c12bdd6e9..75d8e1d593b 100644 --- a/code/modules/mob/living/silicon/laws.dm +++ b/code/modules/mob/living/silicon/laws.dm @@ -18,7 +18,7 @@ /mob/living/silicon/proc/post_lawchange(announce = TRUE) throw_alert(ALERT_NEW_LAW, /atom/movable/screen/alert/newlaw) if(announce && last_lawchange_announce != world.time) - to_chat(src, "Your laws have been changed.") + to_chat(src, span_boldannounce("Your laws have been changed.")) // lawset modules cause this function to be executed multiple times in a tick, so we wait for the next tick in order to be able to see the entire lawset addtimer(CALLBACK(src, .proc/show_laws), 0) addtimer(CALLBACK(src, .proc/deadchat_lawchange), 0) diff --git a/code/modules/mob/living/silicon/pai/death.dm b/code/modules/mob/living/silicon/pai/death.dm deleted file mode 100644 index b25372a07e1..00000000000 --- a/code/modules/mob/living/silicon/pai/death.dm +++ /dev/null @@ -1,10 +0,0 @@ -/mob/living/silicon/pai/death(gibbed) - if(stat == DEAD) - return - set_stat(DEAD) - update_sight() - clear_fullscreens() - - //New pAI's get a brand new mind to prevent meta stuff from their previous life. This new mind causes problems down the line if it's not deleted here. - ghostize() - qdel(src) diff --git a/code/modules/mob/living/silicon/pai/pai.dm b/code/modules/mob/living/silicon/pai/pai.dm deleted file mode 100644 index cee912a2a55..00000000000 --- a/code/modules/mob/living/silicon/pai/pai.dm +++ /dev/null @@ -1,286 +0,0 @@ -/mob/living/silicon/pai - name = "pAI" - icon = 'icons/mob/pai.dmi' - held_lh = 'icons/mob/pai_item_lh.dmi' - held_rh = 'icons/mob/pai_item_rh.dmi' - head_icon = 'icons/mob/pai_item_head.dmi' - icon_state = "repairbot" - mouse_opacity = MOUSE_OPACITY_ICON - density = FALSE - hud_type = /datum/hud/pai - pass_flags = PASSTABLE | PASSMOB - mob_size = MOB_SIZE_TINY - desc = "A generic pAI mobile hard-light holographics emitter. It seems to be deactivated." - health = 500 - maxHealth = 500 - layer = LOW_MOB_LAYER - can_be_held = TRUE - move_force = 0 - pull_force = 0 - move_resist = 0 - worn_slot_flags = ITEM_SLOT_HEAD - radio = /obj/item/radio/headset/silicon/pai - can_buckle_to = FALSE - mobility_flags = MOBILITY_FLAGS_REST_CAPABLE_DEFAULT - - /// The card we inhabit - var/obj/item/paicard/card - - /// Used as currency to purchase different abilities - var/ram = 100 - /// Installed software on the pAI - var/list/software = list() - /// The strength of the internal flashlight - var/brightness_power = 5 - /// Name of the one who commands us - var/master - /// DNA string for owner verification - var/master_dna - /// Whether we are currently holoformed - var/holoform = FALSE - /// Whether the pAI can enter holoform or not - var/canholo = TRUE - /// Whether this pAI can transmit radio messages - var/can_transmit = TRUE - /// Whether this pAI can recieve radio messages - var/can_receive = TRUE - - - // Various software-specific vars - - /// 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 the pAI can hold encryption keys or not - var/encryptmod = FALSE - /// Toggles whether universal translator has been activated. Cannot be reversed - var/languages_granted = FALSE - /// Atmospheric analyzer - var/obj/item/analyzer/atmos_analyzer - /// AI's signaler - var/obj/item/assembly/signaler/internal/signaler - /// pAI Synthesizer - var/obj/item/instrument/piano_synth/internal_instrument - /// 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 - /// The cable we produce when hacking a door - var/obj/item/pai_cable/hacking_cable - - /// The current chasis that will appear when in holoform - var/chassis = "repairbot" - /// List of all possible chasises. TRUE means the pAI can be picked up in this chasis. - var/list/possible_chassis = list( - "cat" = TRUE, - "mouse" = TRUE, - "monkey" = TRUE, - "corgi" = FALSE, - "fox" = FALSE, - "repairbot" = TRUE, - "rabbit" = TRUE, - "bat" = FALSE, - "butterfly" = FALSE, - "hawk" = FALSE, - "lizard" = FALSE, - "duffel" = TRUE, - "crow" = TRUE, - ) - - var/emitterhealth = 20 - var/emittermaxhealth = 20 - var/emitter_regen_per_second = 1.25 - var/emittercd = 50 - var/emitteroverloadcd = 100 - var/emittersemicd = FALSE - - /// Bool that determines if the pAI can refresh medical/security records. - var/refresh_spam = FALSE - /// Cached list for medical records to send as static data - var/list/medical_records = list() - /// Cached list for security records to send as static data - var/list/security_records = list() - - var/list/available_software = list( - "crew manifest" = 5, - "digital messenger" = 5, - "atmosphere sensor" = 5, - "photography module" = 5, - "camera zoom" = 10, - "printer module" = 10, - "remote signaler" = 10, - "medical records" = 10, - "security records" = 10, - "host scan" = 10, - "medical HUD" = 20, - "security HUD" = 20, - "loudness booster" = 20, - "newscaster" = 20, - "door jack" = 25, - "encryption keys" = 25, - "internal gps" = 35, - "universal translator" = 35, - ) - - var/atom/movable/screen/ai/modpc/interfaceButton - -/mob/living/silicon/pai/add_sensors() //pAIs have to buy their HUDs - return - -/mob/living/silicon/pai/handle_atom_del(atom/deleting_atom) - if(deleting_atom == hacking_cable) - hacking_cable = null - if(!QDELETED(card)) - card.update_appearance() - if(deleting_atom == atmos_analyzer) - atmos_analyzer = null - if(deleting_atom == internal_instrument) - internal_instrument = null - if(deleting_atom == newscaster) - newscaster = null - if(deleting_atom == signaler) - signaler = null - if(deleting_atom == hostscan) - hostscan = null - if(deleting_atom == internal_gps) - internal_gps = null - return ..() - -/mob/living/silicon/pai/Initialize(mapload) - var/obj/item/paicard/pai_card = loc - START_PROCESSING(SSfastprocess, src) - GLOB.pai_list += src - make_laws() - for (var/law in laws.inherent) - lawcheck += law - if(!istype(pai_card)) //when manually spawning a pai, we create a card to put it into. - var/newcardloc = pai_card - pai_card = new /obj/item/paicard(newcardloc) - pai_card.setPersonality(src) - forceMove(pai_card) - card = pai_card - job = 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/pai(src) - if(!aicamera) - aicamera = new /obj/item/camera/siliconcam/ai_camera(src) - aicamera.flash_enabled = TRUE - - . = ..() - - create_modularInterface() - - emittersemicd = TRUE - addtimer(CALLBACK(src, .proc/emittercool), 600) - - if(!holoform) - ADD_TRAIT(src, TRAIT_IMMOBILIZED, PAI_FOLDED) - ADD_TRAIT(src, TRAIT_HANDS_BLOCKED, PAI_FOLDED) - - return INITIALIZE_HINT_LATELOAD - -/mob/living/silicon/pai/Destroy() - QDEL_NULL(atmos_analyzer) - QDEL_NULL(internal_instrument) - QDEL_NULL(hacking_cable) - QDEL_NULL(newscaster) - QDEL_NULL(signaler) - QDEL_NULL(hostscan) - QDEL_NULL(internal_gps) - if(!QDELETED(card) && loc != card) - card.forceMove(drop_location()) - card.pai = null //these are otherwise handled by paicard/handle_atom_del() - card.emotion_icon = initial(card.emotion_icon) - card.update_appearance() - GLOB.pai_list -= src - return ..() - -/mob/living/silicon/pai/LateInitialize() - . = ..() - modularInterface.saved_identification = name - -/mob/living/silicon/pai/make_laws() - laws = new /datum/ai_laws/pai() - return TRUE - -/mob/living/silicon/pai/get_status_tab_items() - . += ..() - if(!stat) - . += text("Emitter Integrity: [emitterhealth * (100 / emittermaxhealth)]") - else - . += text("Systems nonfunctional") - - -// See software.dm for Topic() - -/mob/living/silicon/pai/canUseTopic(atom/movable/M, be_close=FALSE, no_dexterity=FALSE, no_tk=FALSE, need_hands = FALSE, floor_okay=FALSE) - return ..(M, be_close, no_dexterity, no_tk, need_hands, TRUE) //Resting is just an aesthetic feature for them. - -/mob/proc/makePAI(delold) - var/obj/item/paicard/card = new /obj/item/paicard(get_turf(src)) - var/mob/living/silicon/pai/pai = new /mob/living/silicon/pai(card) - pai.key = key - pai.name = name - card.setPersonality(pai) - if(delold) - qdel(src) - -/mob/living/silicon/pai/Process_Spacemove(movement_dir = 0, continuous_move = FALSE) - . = ..() - if(!.) - add_movespeed_modifier(/datum/movespeed_modifier/pai_spacewalk) - return TRUE - remove_movespeed_modifier(/datum/movespeed_modifier/pai_spacewalk) - return TRUE - -/mob/living/silicon/pai/examine(mob/user) - . = ..() - . += "A personal AI in holochassis mode. Its master ID string seems to be [master]." - -/mob/living/silicon/pai/updatehealth() - if(status_flags & GODMODE) - return - set_health(maxHealth - getBruteLoss() - getFireLoss()) - update_stat() - -/mob/living/silicon/pai/process(delta_time) - emitterhealth = clamp((emitterhealth + (emitter_regen_per_second * delta_time)), -50, emittermaxhealth) - -/mob/living/silicon/pai/can_interact_with(atom/A) - if(A == signaler) // Bypass for signaler - return TRUE - if(A == modularInterface) - return TRUE - - return ..() - -/obj/item/paicard/screwdriver_act(mob/living/user, obj/item/tool) - return pai.radio.screwdriver_act(user, tool) - -/obj/item/paicard/attackby(obj/item/used, mob/user, params) - if(pai && istype(used, /obj/item/encryptionkey)) - if(!pai.encryptmod) - to_chat(user, span_alert("Encryption Key ports not configured.")) - return - user.set_machine(src) - pai.radio.attackby(used, user, params) - to_chat(user, span_notice("You insert [used] into the [src].")) - return - - return ..() - -/obj/item/paicard/emag_act(mob/user) // Emag to wipe the master DNA and supplemental directive, changes the display to syndi - if(!pai) - return - 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/software.dm b/code/modules/mob/living/silicon/pai/software.dm deleted file mode 100644 index ade5e41efd0..00000000000 --- a/code/modules/mob/living/silicon/pai/software.dm +++ /dev/null @@ -1,256 +0,0 @@ -// 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() - -// 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 - -// 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(master) - data["master"]["name"] = master - data["master"]["dna"] = master_dna - return data - -// Actions received from TGUI -/mob/living/silicon/pai/ui_act(action, list/params, datum/tgui/ui) - . = ..() - if(.) - return - 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", "None"))) - if(isnull(newImage)) - return FALSE - switch(newImage) - if("None") - 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) - hack_door() - if(params["jack"] == "cancel") - 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.AltClick(usr) - 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.show_to(src) - else - var/datum/atom_hud/med = GLOB.huds[med_hud] - med.hide_from(src) - if("newscaster") - newscaster.ui_interact(src) - 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.show_to(src) - else - var/datum/atom_hud/sec = GLOB.huds[sec_hud] - sec.hide_from(src) - if("universal_translator") - if(!languages_granted) - grant_all_languages(TRUE, TRUE, TRUE, LANGUAGE_SOFTWARE) - languages_granted = TRUE - return - -/** - * 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 - 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") - to_chat(pai, span_warning("[master] does not seem like [master.p_theyre()] going to provide a DNA sample willingly.")) - return - 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(pai, "

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

") - if(master.dna.unique_enzymes == pai.master_dna) - to_chat(pai, span_bold("DNA is a match to stored Master DNA.")) - else - to_chat(pai, span_bold("DNA does not match stored Master DNA.")) - -/** - * 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 - to_chat(usr, span_warning("You are not being carried by anyone!")) - return FALSE - -/** - * 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 - if(!isliving(card.loc)) - return - var/mob/living/hacker = card.loc - if(hacker.put_in_hands(hacking_cable)) - 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].")) - return - 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.")) - -/** - * Door jacking supporting proc - * - * This will, after alerting any AIs on station, begin to hack open a door. - * After a 10 second timer, the door will crack open, provided they don't move out of the way. - */ - -/mob/living/silicon/pai/proc/hack_door() - 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/all_ais in GLOB.player_list) - if(!all_ais.stat) - continue - if(turf.loc) - to_chat(all_ais, span_boldannounce("Network Alert: Brute-force security override in progress in [turf.loc].")) - else - to_chat(all_ais, span_boldannounce("Network Alert: Brute-force security override in progress. Unable to pinpoint location.")) - //Now begin hacking - if(!do_after(src, 10 SECONDS, hacking_cable.machine, timed_action_flags = NONE, progress = TRUE)) - to_chat(src, span_notice("Door Jack: Connection to airlock has been lost. Hack aborted.")) - hacking_cable.visible_message( - span_warning("[hacking_cable] rapidly retracts back into its spool."),\ - span_hear("You hear a click and the sound of wire spooling rapidly.")) - QDEL_NULL(hacking_cable) - if(!QDELETED(card)) - card.update_appearance() - return - var/obj/machinery/door/door = hacking_cable.machine - door.open() - QDEL_NULL(hacking_cable) - -/** - * 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/code/modules/mob/living/simple_animal/bot/bot.dm b/code/modules/mob/living/simple_animal/bot/bot.dm index 6aed104d606..1323b7b66dc 100644 --- a/code/modules/mob/living/simple_animal/bot/bot.dm +++ b/code/modules/mob/living/simple_animal/bot/bot.dm @@ -40,7 +40,7 @@ ///People currently looking into a bot's UI panel. var/list/users = list() ///The inserted (if any) pAI in this bot. - var/obj/item/paicard/paicard + var/obj/item/pai_card/paicard ///The type of bot it is, for radio control. var/bot_type = NONE @@ -398,7 +398,7 @@ /mob/living/simple_animal/bot/attackby(obj/item/attacking_item, mob/living/user, params) if(attacking_item.GetID()) unlock_with_id(user) - else if(istype(attacking_item, /obj/item/paicard)) + else if(istype(attacking_item, /obj/item/pai_card)) insertpai(user, attacking_item) else if(attacking_item.tool_behaviour == TOOL_HEMOSTAT && paicard) if(bot_cover_flags & BOT_COVER_OPEN) @@ -936,7 +936,7 @@ Pass a positive integer as an argument to override a bot's default speed. return TRUE return FALSE -/mob/living/simple_animal/bot/proc/insertpai(mob/user, obj/item/paicard/card) +/mob/living/simple_animal/bot/proc/insertpai(mob/user, obj/item/pai_card/card) if(paicard) to_chat(user, span_warning("A [paicard] is already inserted!")) return diff --git a/code/modules/mob/living/simple_animal/bot/mulebot.dm b/code/modules/mob/living/simple_animal/bot/mulebot.dm index f60e25c2fb1..e56dedbd3f6 100644 --- a/code/modules/mob/living/simple_animal/bot/mulebot.dm +++ b/code/modules/mob/living/simple_animal/bot/mulebot.dm @@ -766,7 +766,7 @@ else return ..() -/mob/living/simple_animal/bot/mulebot/insertpai(mob/user, obj/item/paicard/card) +/mob/living/simple_animal/bot/mulebot/insertpai(mob/user, obj/item/pai_card/card) . = ..() if(.) visible_message(span_notice("[src]'s safeties are locked on.")) diff --git a/code/modules/modular_computers/computers/item/computer.dm b/code/modules/modular_computers/computers/item/computer.dm index c84d0d8828e..bc958a42e01 100644 --- a/code/modules/modular_computers/computers/item/computer.dm +++ b/code/modules/modular_computers/computers/item/computer.dm @@ -78,7 +78,7 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar var/datum/picture/saved_image // the saved image used for messaging purpose like come on dude /// Stored pAI in the computer - var/obj/item/paicard/inserted_pai = null + var/obj/item/pai_card/inserted_pai = null /obj/item/modular_computer/Initialize(mapload) . = ..() @@ -661,11 +661,10 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar return // Insert a PAI. - if(istype(attacking_item, /obj/item/paicard) && !inserted_pai) + if(istype(attacking_item, /obj/item/pai_card) && !inserted_pai) if(!user.transferItemToLoc(attacking_item, src)) return inserted_pai = attacking_item - inserted_pai.slotted = TRUE to_chat(user, span_notice("You slot \the [attacking_item] into [src].")) return diff --git a/code/modules/modular_computers/computers/item/computer_ui.dm b/code/modules/modular_computers/computers/item/computer_ui.dm index 3f0eb4ca3c6..84e58f3a582 100644 --- a/code/modules/modular_computers/computers/item/computer_ui.dm +++ b/code/modules/modular_computers/computers/item/computer_ui.dm @@ -222,7 +222,6 @@ if("eject") usr.put_in_hands(inserted_pai) to_chat(usr, span_notice("You remove [inserted_pai] from the [name].")) - inserted_pai.slotted = FALSE inserted_pai = null if("interact") inserted_pai.attack_self(usr) diff --git a/code/modules/mob/living/silicon/pai/pai_actions.dm b/code/modules/pai/actions.dm similarity index 100% rename from code/modules/mob/living/silicon/pai/pai_actions.dm rename to code/modules/pai/actions.dm diff --git a/code/modules/pai/camera.dm b/code/modules/pai/camera.dm new file mode 100644 index 00000000000..30b1918651e --- /dev/null +++ b/code/modules/pai/camera.dm @@ -0,0 +1,58 @@ +/mob/living/silicon/pai/ClickOn(atom/target, params) + ..() + if(!camera?.in_camera_mode) + return FALSE + //pAI picture taking + camera.toggle_camera_mode(sound = FALSE) + camera.captureimage(target, usr, camera.picture_size_x - 1, camera.picture_size_y - 1) + return TRUE + +/obj/item/camera/siliconcam/pai_camera + name = "pAI photo camera" + light_color = COLOR_PAI_GREEN + +/obj/item/camera/siliconcam/pai_camera/after_picture(mob/user, datum/picture/picture) + var/number = length(stored) + picture.picture_name = "Image [number] (taken by [loc.name])" + stored[picture] = TRUE + playsound(loc, pick('sound/items/polaroid1.ogg', 'sound/items/polaroid2.ogg'), 75, TRUE, -3) + balloon_alert(user, "image recorded") + +/** + * Handles selecting and printing stored images. + * + * @param {mob} user - The pAI. + * + * @returns {boolean} - TRUE if the pAI prints an image, + * FALSE otherwise. +*/ +/obj/item/camera/siliconcam/pai_camera/proc/pai_print(mob/user) + var/mob/living/silicon/pai/pai = loc + var/datum/picture/selection = selectpicture(user) + if(!istype(selection)) + balloon_alert(user, "invalid image") + return FALSE + printpicture(user, selection) + user.visible_message(span_notice("A picture appears on top of the chassis of [pai.name]!"), span_notice("You print a photograph.")) + return TRUE + +/** + * All inclusive camera proc. Zooms, snaps, prints. + * + * @param {mob} user - The pAI requesting the camera. + * + * @param {string} mode - The camera option to toggle. + * + * @returns {boolean} - TRUE if the camera worked. + */ +/mob/living/silicon/pai/proc/use_camera(mob/user, mode) + if(!camera || isnull(mode)) + return FALSE + switch(mode) + if(PAI_PHOTO_MODE_CAMERA) + camera.toggle_camera_mode(user) + if(PAI_PHOTO_MODE_PRINTER) + camera.pai_print(user) + if(PAI_PHOTO_MODE_ZOOM) + camera.adjust_zoom(user) + return TRUE diff --git a/code/modules/pai/candidate.dm b/code/modules/pai/candidate.dm new file mode 100644 index 00000000000..6455492e137 --- /dev/null +++ b/code/modules/pai/candidate.dm @@ -0,0 +1,35 @@ +/** + * #pAI Candidate + * + * Created when a user opens the pAI submit interface. + * Stores the candidate in an associative list of ckey: candidate objects. + */ +/datum/pai_candidate + /// User inputted OOC comments + var/comments + /// User inputted behavior description + var/description + /// User's ckey + var/ckey + /// User's pAI name. If blank, ninja name. + var/name + /// If the user has hit "submit" + var/ready = FALSE + +/datum/pai_candidate/New(ckey) + src.ckey = ckey + +/** + * Checks if a candidate is ready so that they may be displayed or + * downloaded. Removes any invalid entries. + * + * @returns {boolean} - TRUE if the candidate is ready, FALSE if not + */ +/datum/pai_candidate/proc/check_ready() + var/mob/candidate_mob = get_mob_by_key(ckey) + if(!candidate_mob?.client || !isobserver(candidate_mob) || is_banned_from(ckey, ROLE_PAI)) + SSpai.candidates -= ckey + return FALSE + if(!ready) + return FALSE + return TRUE diff --git a/code/modules/pai/card.dm b/code/modules/pai/card.dm new file mode 100644 index 00000000000..d1cde491085 --- /dev/null +++ b/code/modules/pai/card.dm @@ -0,0 +1,261 @@ +/obj/item/pai_card + custom_premium_price = PAYCHECK_COMMAND * 1.25 + desc = "Downloads personal AI assistants to accompany its owner or others." + icon = 'icons/obj/aicards.dmi' + icon_state = "pai" + inhand_icon_state = "electronic" + lefthand_file = 'icons/mob/inhands/misc/devices_lefthand.dmi' + name = "personal AI device" + resistance_flags = FIRE_PROOF | ACID_PROOF | INDESTRUCTIBLE + righthand_file = 'icons/mob/inhands/misc/devices_righthand.dmi' + slot_flags = ITEM_SLOT_BELT + w_class = WEIGHT_CLASS_SMALL + worn_icon_state = "electronic" + + /// Spam alert prevention + var/alert_cooldown + /// The emotion icon displayed. + var/emotion_icon = "off" + /// Any pAI personalities inserted + var/mob/living/silicon/pai/pai + /// Prevents a crew member from hitting "request pAI" repeatedly + var/request_spam = FALSE + +/obj/item/pai_card/attackby(obj/item/used, mob/user, params) + if(pai && istype(used, /obj/item/encryptionkey)) + if(!pai.encrypt_mod) + to_chat(user, span_alert("Encryption Key ports not configured.")) + return + user.set_machine(src) + pai.radio.attackby(used, user, params) + to_chat(user, span_notice("You insert [used] into the [src].")) + return + return ..() + +/obj/item/pai_card/attack_self(mob/user) + if(!in_range(src, user)) + return + user.set_machine(src) + ui_interact(user) + +/obj/item/pai_card/Destroy() + //Will stop people throwing friend pAIs into the singularity so they can respawn + SSpai.pai_card_list.Remove(src) + if(!QDELETED(pai)) + QDEL_NULL(pai) + return ..() + +/obj/item/pai_card/emag_act(mob/user) + if(pai) + pai.handle_emag(user) + +/obj/item/pai_card/emp_act(severity) + . = ..() + if (. & EMP_PROTECT_SELF) + return + if(!pai?.holoform) + pai.emp_act(severity) + +/obj/item/pai_card/handle_atom_del(atom/thing) + if(thing == pai) //double check /mob/living/silicon/pai/Destroy() if you change these. + pai = null + emotion_icon = initial(emotion_icon) + update_appearance() + return ..() + +/obj/item/pai_card/Initialize(mapload) + . = ..() + update_appearance() + SSpai.pai_card_list += src + +/obj/item/pai_card/suicide_act(mob/living/user) + user.visible_message(span_suicide("[user] is staring sadly at [src]! [user.p_they()] can't keep living without real human intimacy!")) + return OXYLOSS + +/obj/item/pai_card/update_overlays() + . = ..() + . += "pai-[emotion_icon]" + if(pai?.hacking_cable) + . += "[initial(icon_state)]-connector" + +/obj/item/pai_card/vv_edit_var(vname, vval) + . = ..() + if(vname == NAMEOF(src, emotion_icon)) + update_appearance() + +/obj/item/pai_card/ui_interact(mob/user, datum/tgui/ui) + . = ..() + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "PaiCard") + ui.open() + +/obj/item/pai_card/ui_status(mob/user) + if(user in get_nested_locs(src)) + return UI_INTERACTIVE + return ..() + +/obj/item/pai_card/ui_data(mob/user) + . = ..() + var/list/data = list() + if(!pai) + data["candidates"] = pool_candidates() || list() + return data + data["pai"] = list( + can_holo = pai.can_holo, + dna = pai.master_dna, + emagged = pai.emagged, + laws = pai.laws.supplied, + master = pai.master_name, + name = pai.name, + transmit = pai.can_transmit, + receive = pai.can_receive, + ) + return data + +/obj/item/pai_card/ui_act(action, list/params, datum/tgui/ui) + . = ..() + if(.) + return TRUE + // Actions that don't require a pAI + if(action == "download") + download_candidate(usr, params["ckey"]) + return TRUE + if(action == "request") + find_pai(usr) + return TRUE + // pAI specific actions. + if(!pai) + return FALSE + switch(action) + if("fix_speech") + pai.fix_speech() + return TRUE + if("reset_software") + pai.reset_software() + return TRUE + if("set_dna") + pai.set_dna(usr) + return TRUE + if("set_laws") + pai.set_laws(usr) + return TRUE + if("toggle_holo") + pai.toggle_holo() + return TRUE + if("toggle_radio") + pai.toggle_radio(params["option"]) + return TRUE + if("wipe_pai") + pai.wipe_pai(usr) + ui.close() + return TRUE + return FALSE + +/** Flashes the pai card screen */ +/obj/item/pai_card/proc/add_alert() + if(pai) + return + add_overlay( + list(mutable_appearance(icon, "[initial(icon_state)]-alert"), + emissive_appearance(icon, "[initial(icon_state)]-alert", alpha = src.alpha))) + +/** Removes any overlays */ +/obj/item/pai_card/proc/remove_alert() + if(pai) + return + cut_overlays() + +/** Alerts pAI cards that someone has submitted candidacy */ +/obj/item/pai_card/proc/alert_update() + if(!COOLDOWN_FINISHED(src, alert_cooldown)) + return + COOLDOWN_START(src, alert_cooldown, 5 SECONDS) + add_alert() + addtimer(CALLBACK(src, .proc/remove_alert), 5 SECONDS) + playsound(src, 'sound/machines/ping.ogg', 30, TRUE) + visible_message(span_notice("[src] flashes a message across its screen: New personalities available for download!"), blind_message = span_notice("[src] vibrates with an alert.")) + +/** + * Downloads a candidate from the list and removes them from SSpai.candidates + * + * @param {string} ckey The ckey of the candidate to download + * + * @returns {boolean} - TRUE if the candidate was downloaded, FALSE if not + */ +/obj/item/pai_card/proc/download_candidate(mob/user, ckey) + if(pai) + return FALSE + var/datum/pai_candidate/candidate = SSpai.candidates[ckey] + if(!candidate?.check_ready()) + balloon_alert(user, "download interrupted") + return FALSE + var/mob/living/silicon/pai/new_pai = new(src) + new_pai.name = candidate.name || pick(GLOB.ninja_names) + new_pai.real_name = new_pai.name + new_pai.key = candidate.ckey + set_personality(new_pai) + SSpai.candidates -= ckey + return TRUE + +/** + * Pings ghosts to announce that someone is requesting a pAI + * + * @param {mob} user - The user who is requesting a pAI + * + * @returns {boolean} - TRUE if the pAI was requested, FALSE if not + */ +/obj/item/pai_card/proc/find_pai(mob/user) + if(pai) + return FALSE + if(!(GLOB.ghost_role_flags & GHOSTROLE_SILICONS)) + balloon_alert(user, "unavailable: NT blacklisted") + return FALSE + if(request_spam) + balloon_alert(user, "request sent too recently") + return FALSE + request_spam = TRUE + playsound(src, 'sound/machines/ping.ogg', 20, TRUE) + balloon_alert(user, "pAI assistance requested") + var/mutable_appearance/alert_overlay = mutable_appearance('icons/obj/aicards.dmi', "pai") + notify_ghosts("[user] is requesting a pAI companion! Use the pAI button to submit yourself as one.", source = user, alert_overlay = alert_overlay, action = NOTIFY_ORBIT, flashwindow = FALSE, header = "pAI Request!", ignore_key = POLL_IGNORE_PAI) + addtimer(VARSET_CALLBACK(src, request_spam, FALSE), PAI_SPAM_TIME, TIMER_UNIQUE | TIMER_STOPPABLE | TIMER_CLIENT_TIME | TIMER_DELETE_ME) + return TRUE + +/** + * 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. + * + * @returns - An array of candidate objects. + */ +/obj/item/pai_card/proc/pool_candidates() + var/list/candidates = list() + if(pai || !length(SSpai?.candidates)) + return candidates + for(var/key in SSpai.candidates) + var/datum/pai_candidate/candidate = SSpai.candidates[key] + if(!candidate?.check_ready()) + continue + candidates += list(list( + ckey = candidate.ckey, + comments = candidate.comments, + description = candidate.description, + name = candidate.name, + )) + return candidates + +/** + * Sets the personality on the current pai_card + * + * @param {silicon/pai} downloaded - The new pAI to load into the card. + */ +/obj/item/pai_card/proc/set_personality(mob/living/silicon/pai/downloaded) + if(pai) + return FALSE + pai = downloaded + emotion_icon = "null" + update_appearance() + playsound(src, 'sound/effects/pai_boot.ogg', 50, TRUE, -1) + audible_message("[src] plays a cheerful startup noise!") + return TRUE diff --git a/code/modules/pai/death.dm b/code/modules/pai/death.dm new file mode 100644 index 00000000000..b06c042c0b4 --- /dev/null +++ b/code/modules/pai/death.dm @@ -0,0 +1,12 @@ +/mob/living/silicon/pai/death(gibbed) + if(stat == DEAD) + return + set_stat(DEAD) + update_sight() + clear_fullscreens() + /** + * New pAI's get a brand new mind to prevent meta stuff from their previous + * life. This new mind causes problems down the line if it's not deleted here. + */ + ghostize() + qdel(src) diff --git a/code/modules/pai/debug.dm b/code/modules/pai/debug.dm new file mode 100644 index 00000000000..8ddc7606782 --- /dev/null +++ b/code/modules/pai/debug.dm @@ -0,0 +1,45 @@ +/client/proc/makepAI(turf/target in GLOB.mob_list) + set category = "Admin.Fun" + set name = "Make pAI" + set desc = "Specify a location to spawn a pAI device, then specify a key to play that pAI" + + var/list/available = list() + for(var/mob/player as anything in GLOB.player_list) + if(player.client && player.key) + available.Add(player) + var/mob/choice = tgui_input_list(usr, "Choose a player to play the pAI", "Spawn pAI", sort_names(available)) + if(isnull(choice)) + return + + var/chosen_name = input(choice, "Enter your pAI name:", "pAI Name", "Personal AI") as text|null + if (isnull(chosen_name)) + return + + if(!isobserver(choice)) + var/confirm = tgui_alert(usr, "[choice.key] isn't ghosting right now. Are you sure you want to yank them out of their body and place them in this pAI?", "Spawn pAI Confirmation", list("Yes", "No")) + if(confirm != "Yes") + return + var/obj/item/pai_card/card = new(target) + var/mob/living/silicon/pai/pai = new(card) + + pai.name = chosen_name + pai.real_name = pai.name + pai.key = choice.key + card.set_personality(pai) + if(SSpai.candidates[key]) + SSpai.candidates -= key + SSblackbox.record_feedback("tally", "admin_verb", 1, "Make pAI") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + +/** + * Creates a new pAI. + * + * @param {boolean} delete_old - If TRUE, deletes the old pAI. + */ +/mob/proc/make_pai(delete_old) + var/obj/item/pai_card/card = new(src) + var/mob/living/silicon/pai/pai = new(card) + pai.key = key + pai.name = name + card.set_personality(pai) + if(delete_old) + qdel(src) diff --git a/code/modules/mob/living/silicon/pai/pai_defense.dm b/code/modules/pai/defense.dm similarity index 92% rename from code/modules/mob/living/silicon/pai/pai_defense.dm rename to code/modules/pai/defense.dm index 886703c9b8c..0117cb80e82 100644 --- a/code/modules/mob/living/silicon/pai/pai_defense.dm +++ b/code/modules/pai/defense.dm @@ -44,7 +44,7 @@ visible_message(span_notice("[user] gently pats [src] on the head, eliciting an off-putting buzzing from its holographic field.")) return user.do_attack_animation(src) - if(user.name != master) + if(user.name != master_name) visible_message(span_danger("[user] stomps on [src]!.")) take_holo_damage(2) return @@ -65,8 +65,8 @@ return FALSE /mob/living/silicon/pai/proc/take_holo_damage(amount) - emitterhealth = clamp((emitterhealth - amount), -50, emittermaxhealth) - if(emitterhealth < 0) + holochassis_health = clamp((holochassis_health - amount), -50, HOLOCHASSIS_MAX_HEALTH) + if(holochassis_health < 0) fold_in(force = TRUE) if(amount > 0) to_chat(src, span_userdanger("The impact degrades your holochassis!")) @@ -85,7 +85,7 @@ take_holo_damage(amount * 0.25) /mob/living/silicon/pai/getBruteLoss() - return emittermaxhealth - emitterhealth + return HOLOCHASSIS_MAX_HEALTH - holochassis_health /mob/living/silicon/pai/getFireLoss() - return emittermaxhealth - emitterhealth + return HOLOCHASSIS_MAX_HEALTH - holochassis_health diff --git a/code/modules/pai/door_jack.dm b/code/modules/pai/door_jack.dm new file mode 100644 index 00000000000..3f7f393a59a --- /dev/null +++ b/code/modules/pai/door_jack.dm @@ -0,0 +1,134 @@ +#define CABLE_LENGTH 2 + +/** + * Switch that handles door jack operations. + * + * @param {string} mode - The requested operation of the door jack. + * + * @returns {boolean} - TRUE if the door jack state was switched, FALSE otherwise. + */ +/mob/living/silicon/pai/proc/door_jack(mode) + if(isnull(mode)) + return FALSE + switch(mode) + if(PAI_DOOR_JACK_CABLE) + extend_cable() + return TRUE + if(PAI_DOOR_JACK_HACK) + hack_door() + return TRUE + if(PAI_DOOR_JACK_CANCEL) + QDEL_NULL(hacking_cable) + visible_message(span_notice("The cable retracts into the pAI.")) + return TRUE + return FALSE + +/** + * #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). + * + * @returns {boolean} - TRUE if the cable was dropped, FALSE otherwise. + */ +/mob/living/silicon/pai/proc/extend_cable() + QDEL_NULL(hacking_cable) //clear any old cables + hacking_cable = new + var/mob/living/carbon/hacker = get_holder() + if(hacker?.put_in_hands(hacking_cable)) + hacker.visible_message(span_notice("A port on [src] opens to reveal a cable, which you quickly grab."), span_hear("You hear the soft click of a plastic component and manage to catch the falling cable.")) + track_pai() + track_thing(hacking_cable) + return TRUE + hacking_cable.forceMove(drop_location()) + hacking_cable.visible_message(span_notice("A port on [src] opens to reveal a cable, which promptly falls to the floor."), span_hear("You hear the soft click of a plastic component fall to the ground.")) + track_pai() + track_thing(hacking_cable) + return TRUE + +/** Tracks the associated pai */ +/mob/living/silicon/pai/proc/track_pai() + RegisterSignal(src, COMSIG_MOVABLE_MOVED, .proc/handle_move) + RegisterSignal(card, COMSIG_MOVABLE_MOVED, .proc/handle_move) + +/** Untracks the associated pai */ +/mob/living/silicon/pai/proc/untrack_pai() + UnregisterSignal(src, COMSIG_MOVABLE_MOVED) + UnregisterSignal(card, COMSIG_MOVABLE_MOVED) + +/** Tracks the associated hacking_cable */ +/mob/living/silicon/pai/proc/track_thing(atom/movable/thing) + RegisterSignal(thing, COMSIG_MOVABLE_MOVED, .proc/handle_move) + var/list/locations = get_nested_locs(thing, include_turf = FALSE) + for(var/atom/movable/location in locations) + RegisterSignal(location, COMSIG_MOVABLE_MOVED, .proc/handle_move) + +/** Untracks the associated hacking */ +/mob/living/silicon/pai/proc/untrack_thing(atom/movable/thing) + UnregisterSignal(thing, COMSIG_MOVABLE_MOVED) + var/list/locations = get_nested_locs(thing, include_turf = FALSE) + for(var/atom/movable/location in locations) + UnregisterSignal(location, COMSIG_MOVABLE_MOVED) + +/** + * A periodic check to see if the source pAI is nearby. + * Deletes the extended cable if the source pAI is not nearby. + */ +/mob/living/silicon/pai/proc/handle_move(atom/movable/source, atom/movable/old_loc) + if(ismovable(old_loc)) + untrack_thing(old_loc) + if(!IN_GIVEN_RANGE(src, hacking_cable, CABLE_LENGTH)) + retract_cable() + return + if(ismovable(source.loc)) + track_thing(source.loc) + +/** + * Handles deleting the hacking cable and notifying the user. + */ +/mob/living/silicon/pai/proc/retract_cable() + hacking_cable.visible_message(span_notice("The cable quickly retracts.")) + balloon_alert(src, "cable retracted") + untrack_pai() + untrack_thing(hacking_cable) + QDEL_NULL(hacking_cable) + SStgui.update_user_uis(src) + return TRUE + +/** + * #Door jacking supporting proc + * + * After a 15 second timer, the door will crack open, + * provided they don't move out of the way. + * + * @returns {boolean} - TRUE if the door was jacked, FALSE otherwise. + */ +/mob/living/silicon/pai/proc/hack_door() + if(!hacking_cable) + return FALSE + if(!hacking_cable.machine) + balloon_alert(src, "nothing connected") + return FALSE + playsound(src, 'sound/machines/airlock_alien_prying.ogg', 50, TRUE) + balloon_alert(src, "overriding...") + // Now begin hacking + if(!do_after(src, 15 SECONDS, hacking_cable.machine, timed_action_flags = NONE, progress = TRUE)) + balloon_alert(src, "failed! retracting...") + hacking_cable.visible_message( + span_warning("The cable rapidly retracts back into its spool."), span_hear("You hear a click and the sound of wire spooling rapidly.")) + untrack_pai() + untrack_thing(hacking_cable) + QDEL_NULL(hacking_cable) + if(!QDELETED(card)) + card.update_appearance() + return FALSE + var/obj/machinery/door/door = hacking_cable.machine + balloon_alert(src, "success") + door.open() + untrack_pai() + untrack_thing(hacking_cable) + QDEL_NULL(hacking_cable) + return TRUE + +#undef CABLE_LENGTH diff --git a/code/_onclick/hud/pai.dm b/code/modules/pai/hud.dm similarity index 86% rename from code/_onclick/hud/pai.dm rename to code/modules/pai/hud.dm index fdd97191f36..3682535e89c 100644 --- a/code/_onclick/hud/pai.dm +++ b/code/modules/pai/hud.dm @@ -7,9 +7,9 @@ /atom/movable/screen/pai/Click() if(isobserver(usr) || usr.incapacitated()) return FALSE - var/mob/living/silicon/pai/pAI = usr - if(required_software && !pAI.software.Find(required_software)) - to_chat(pAI, PAI_MISSING_SOFTWARE_MESSAGE) + var/mob/living/silicon/pai/user = usr + if(required_software && !user.installed_software.Find(required_software)) + to_chat(user, PAI_MISSING_SOFTWARE_MESSAGE) return FALSE return TRUE @@ -69,7 +69,7 @@ /atom/movable/screen/pai/newscaster name = "pAI Newscaster" icon_state = "newscaster" - required_software = "newscaster" + required_software = "Newscaster" /atom/movable/screen/pai/newscaster/Click() if(!..()) @@ -80,7 +80,7 @@ /atom/movable/screen/pai/host_monitor name = "Host Health Scan" icon_state = "host_monitor" - required_software = "host scan" + required_software = "Host Scan" /atom/movable/screen/pai/host_monitor/Click(location, control, params) . = ..() @@ -88,16 +88,12 @@ return var/mob/living/silicon/pai/pAI = usr var/list/modifiers = params2list(params) - var/mob/living/carbon/holder = get(pAI.card.loc, /mob/living/carbon) - if(holder) - if (LAZYACCESS(modifiers, RIGHT_CLICK)) - pAI.hostscan.attack_secondary(holder, pAI) - else - pAI.hostscan.attack(holder, pAI) - else - to_chat(usr, span_warning("You are not being carried by anyone!")) - return FALSE - + if(LAZYACCESS(modifiers, LEFT_CLICK)) + pAI.host_scan(PAI_SCAN_TARGET) + return TRUE + if(LAZYACCESS(modifiers, RIGHT_CLICK)) + pAI.host_scan(PAI_SCAN_MASTER) + return TRUE /atom/movable/screen/pai/crew_manifest name = "Crew Manifest" icon_state = "manifest" @@ -122,6 +118,7 @@ /atom/movable/screen/pai/modpc name = "Messenger" icon_state = "pda_send" + required_software = "Digital Messenger" var/mob/living/silicon/pai/pAI /atom/movable/screen/pai/modpc/Click() @@ -133,7 +130,7 @@ /atom/movable/screen/pai/internal_gps name = "Internal GPS" icon_state = "internal_gps" - required_software = "internal gps" + required_software = "Internal GPS" /atom/movable/screen/pai/internal_gps/Click() . = ..() @@ -147,24 +144,24 @@ /atom/movable/screen/pai/image_take name = "Take Image" icon_state = "take_picture" - required_software = "photography module" + required_software = "Photography Module" /atom/movable/screen/pai/image_take/Click() if(!..()) return var/mob/living/silicon/pai/pAI = usr - pAI.aicamera.toggle_camera_mode(usr) + pAI.camera.toggle_camera_mode(usr) /atom/movable/screen/pai/image_view name = "View Images" icon_state = "view_images" - required_software = "photography module" + required_software = "Photography Module" /atom/movable/screen/pai/image_view/Click() if(!..()) return var/mob/living/silicon/pai/pAI = usr - pAI.aicamera.viewpictures(usr) + pAI.camera.viewpictures(usr) /atom/movable/screen/pai/radio name = "radio" @@ -241,9 +238,9 @@ using = new /atom/movable/screen/pai/modpc() using.screen_loc = ui_pai_mod_int static_inventory += using - mypai.interfaceButton = using - var/atom/movable/screen/pai/modpc/tabletbutton = using - tabletbutton.pAI = mypai + mypai.pda_button = using + var/atom/movable/screen/pai/modpc/tablet_button = using + tablet_button.pAI = mypai // Internal GPS using = new /atom/movable/screen/pai/internal_gps() @@ -271,6 +268,6 @@ var/mob/living/silicon/pai/owner = mymob for(var/atom/movable/screen/pai/button in static_inventory) if(button.required_software) - button.color = owner.software.Find(button.required_software) ? null : "#808080" + button.color = owner.installed_software.Find(button.required_software) ? null : "#808080" #undef PAI_MISSING_SOFTWARE_MESSAGE diff --git a/code/modules/mob/living/silicon/pai/login.dm b/code/modules/pai/login.dm similarity index 100% rename from code/modules/mob/living/silicon/pai/login.dm rename to code/modules/pai/login.dm diff --git a/code/modules/pai/pai.dm b/code/modules/pai/pai.dm new file mode 100644 index 00000000000..a4b000cc773 --- /dev/null +++ b/code/modules/pai/pai.dm @@ -0,0 +1,415 @@ +/mob/living/silicon/pai + can_be_held = TRUE + can_buckle_to = FALSE + density = FALSE + desc = "A generic pAI hard-light holographics emitter." + health = 500 + held_lh = 'icons/mob/pai_item_lh.dmi' + held_rh = 'icons/mob/pai_item_rh.dmi' + head_icon = 'icons/mob/pai_item_head.dmi' + hud_type = /datum/hud/pai + icon = 'icons/mob/pai.dmi' + icon_state = "repairbot" + job = JOB_PERSONAL_AI + layer = LOW_MOB_LAYER + light_color = COLOR_PAI_GREEN + light_flags = LIGHT_ATTACHED + light_on = FALSE + light_range = 3 + light_system = MOVABLE_LIGHT + maxHealth = 500 + mob_size = MOB_SIZE_TINY + mobility_flags = MOBILITY_FLAGS_REST_CAPABLE_DEFAULT + mouse_opacity = MOUSE_OPACITY_ICON + move_force = 0 + move_resist = 0 + name = "pAI" + pass_flags = PASSTABLE | PASSMOB + pull_force = 0 + radio = /obj/item/radio/headset/silicon/pai + worn_slot_flags = ITEM_SLOT_HEAD + + /// If someone has enabled/disabled the pAIs ability to holo + var/can_holo = TRUE + /// Whether this pAI can recieve radio messages + var/can_receive = TRUE + /// Whether this pAI can transmit radio messages + var/can_transmit = TRUE + /// The card we inhabit + var/obj/item/pai_card/card + /// The current chasis that will appear when in holoform + var/chassis = "repairbot" + /// Toggles whether the pAI can hold encryption keys or not + var/encrypt_mod = FALSE + /// The cable we produce when hacking a door + var/obj/item/pai_cable/hacking_cable + /// The current health of the holochassis + var/holochassis_health = 20 + /// Holochassis available to use + var/holochassis_ready = FALSE + /// Whether we are currently holoformed + var/holoform = FALSE + /// Installed software on the pAI + var/list/installed_software = list() + /// Toggles whether universal translator has been activated. Cannot be reversed + var/languages_granted = FALSE + /// Reference of the bound master + var/datum/weakref/master_ref + /// The master's name string + var/master_name + /// DNA string for owner verification + var/master_dna + /// Toggles whether the Medical HUD is active or not + var/medHUD = FALSE + /// Used as currency to purchase different abilities + var/ram = 100 + /// Toggles whether the Security HUD is active or not + var/secHUD = FALSE + + // Onboard Items + /// Atmospheric analyzer + var/obj/item/analyzer/atmos_analyzer + /// Health analyzer + var/obj/item/healthanalyzer/host_scan + /// GPS + var/obj/item/gps/pai/internal_gps + /// Music Synthesizer + var/obj/item/instrument/piano_synth/instrument + /// Newscaster + var/obj/machinery/newscaster/pai/newscaster + /// PDA + var/atom/movable/screen/ai/modpc/pda_button + /// Photography module + var/obj/item/camera/siliconcam/pai_camera/camera + /// Remote signaler + var/obj/item/assembly/signaler/internal/signaler + + // Static lists + /// List of all available downloads + var/static/list/available_software = list( + "Atmospheric Sensor" = 5, + "Crew Manifest" = 5, + "Digital Messenger" = 5, + "Photography Module" = 5, + "Encryption Slot" = 10, + "Music Synthesizer" = 10, + "Newscaster" = 10, + "Remote Signaler" = 10, + "Host Scan" = 20, + "Medical HUD" = 20, + "Security HUD" = 20, + "Crew Monitor" = 35, + "Door Jack" = 35, + "Internal GPS" = 35, + "Universal Translator" = 35, + ) + /// List of all possible chasises. TRUE means the pAI can be picked up in this chasis. + var/static/list/possible_chassis = list( + "bat" = FALSE, + "butterfly" = FALSE, + "cat" = TRUE, + "corgi" = FALSE, + "crow" = TRUE, + "duffel" = TRUE, + "fox" = FALSE, + "hawk" = FALSE, + "lizard" = FALSE, + "monkey" = TRUE, + "mouse" = TRUE, + "rabbit" = TRUE, + "repairbot" = TRUE, + ) + /// List of all available card overlays. + var/static/list/possible_overlays = list( + "null", + "angry", + "cat", + "extremely-happy", + "face", + "happy", + "laugh", + "off", + "sad", + "sunglasses", + "what" + ) + +/mob/living/silicon/pai/add_sensors() //pAIs have to buy their HUDs + return + +/mob/living/silicon/pai/can_interact_with(atom/target) + if(target == signaler) // Bypass for signaler + return TRUE + if(target == modularInterface) + return TRUE + return ..() + +// See software.dm for Topic() +/mob/living/silicon/pai/canUseTopic(atom/movable/movable, be_close = FALSE, no_dexterity = FALSE, no_tk = FALSE, need_hands = FALSE, floor_okay = FALSE) + // Resting is just an aesthetic feature for them. + return ..(movable, be_close, no_dexterity, no_tk, need_hands, TRUE) + +/mob/living/silicon/pai/Destroy() + QDEL_NULL(atmos_analyzer) + QDEL_NULL(camera) + QDEL_NULL(hacking_cable) + QDEL_NULL(host_scan) + QDEL_NULL(instrument) + QDEL_NULL(internal_gps) + QDEL_NULL(newscaster) + QDEL_NULL(signaler) + if(!QDELETED(card) && loc != card) + card.forceMove(drop_location()) + // these are otherwise handled by paicard/handle_atom_del() + card.pai = null + card.emotion_icon = initial(card.emotion_icon) + card.update_appearance() + GLOB.pai_list.Remove(src) + return ..() + +/mob/living/silicon/pai/emag_act(mob/user) + handle_emag(user) + +/mob/living/silicon/pai/examine(mob/user) + . = ..() + . += "Its master ID string seems to be [(!master_name || emagged) ? "empty" : master_name]." + +/mob/living/silicon/pai/get_status_tab_items() + . += ..() + if(!stat) + . += text("Emitter Integrity: [holochassis_health * (100 / HOLOCHASSIS_MAX_HEALTH)].") + else + . += text("Systems nonfunctional.") + +/mob/living/silicon/pai/handle_atom_del(atom/deleting_atom) + if(deleting_atom == hacking_cable) + hacking_cable = null + if(!QDELETED(card)) + card.update_appearance() + if(deleting_atom == atmos_analyzer) + atmos_analyzer = null + if(deleting_atom == camera) + camera = null + if(deleting_atom == host_scan) + host_scan = null + if(deleting_atom == internal_gps) + internal_gps = null + if(deleting_atom == instrument) + instrument = null + if(deleting_atom == newscaster) + newscaster = null + if(deleting_atom == signaler) + signaler = null + return ..() + +/mob/living/silicon/pai/Initialize(mapload) + . = ..() + START_PROCESSING(SSfastprocess, src) + GLOB.pai_list += src + make_laws() + for(var/law in laws.inherent) + lawcheck += law + var/obj/item/pai_card/pai_card = loc + if(!istype(pai_card)) // when manually spawning a pai, we create a card to put it into. + var/newcardloc = pai_card + pai_card = new(newcardloc) + pai_card.set_personality(src) + forceMove(pai_card) + card = pai_card + addtimer(VARSET_CALLBACK(src, holochassis_ready, TRUE), HOLOCHASSIS_INIT_TIME) + if(!holoform) + ADD_TRAIT(src, TRAIT_IMMOBILIZED, PAI_FOLDED) + ADD_TRAIT(src, TRAIT_HANDS_BLOCKED, PAI_FOLDED) + desc = "A pAI hard-light holographics emitter. This one appears in the form of a [chassis]." + +/mob/living/silicon/pai/make_laws() + laws = new /datum/ai_laws/pai() + return TRUE + +/mob/living/silicon/pai/process(delta_time) + holochassis_health = clamp((holochassis_health + (HOLOCHASSIS_REGEN_PER_SECOND * delta_time)), -50, HOLOCHASSIS_MAX_HEALTH) + +/mob/living/silicon/pai/Process_Spacemove(movement_dir = 0, continuous_move = FALSE) + . = ..() + if(!.) + add_movespeed_modifier(/datum/movespeed_modifier/pai_spacewalk) + return TRUE + remove_movespeed_modifier(/datum/movespeed_modifier/pai_spacewalk) + return TRUE + +/mob/living/silicon/pai/screwdriver_act(mob/living/user, obj/item/tool) + return radio.screwdriver_act(user, tool) + +/mob/living/silicon/pai/updatehealth() + if(status_flags & GODMODE) + return + set_health(maxHealth - getBruteLoss() - getFireLoss()) + update_stat() + +/** + * Resolves the weakref of the pai's master. + * If the master has been deleted, calls reset_software(). + * + * @returns {mob/living || FALSE} - The master mob, or + * FALSE if the master is gone. + */ +/mob/living/silicon/pai/proc/find_master() + if(!master_ref) + return FALSE + var/mob/living/resolved_master = master_ref?.resolve() + if(!resolved_master) + reset_software() + return FALSE + return resolved_master + +/** + * Fixes weird speech issues with the pai. + * + * @returns {boolean} - TRUE if successful. + */ +/mob/living/silicon/pai/proc/fix_speech() + var/mob/living/silicon/pai = src + balloon_alert(pai, "speech modulation corrected") + for(var/effect in typesof(/datum/status_effect/speech)) + pai.remove_status_effect(effect) + return TRUE + +/** + * Gets the current holder of the pAI if its + * being carried in card or holoform. + * + * @returns {living/carbon || FALSE} - The holder of the pAI, + * or FALSE if the pAI is not being carried. + */ +/mob/living/silicon/pai/proc/get_holder() + var/mob/living/carbon/holder + if(!holoform && iscarbon(card.loc)) + holder = card.loc + if(holoform && istype(loc, /obj/item/clothing/head/mob_holder) && iscarbon(loc.loc)) + holder = loc.loc + if(!holder || !iscarbon(holder)) + return FALSE + return holder + +/** + * Handles the pai card or the pai itself being hit with an emag. + * This replaces any current laws, masters, and DNA. + * + * @param {living/carbon} attacker - The user performing the action. + * @returns {boolean} - TRUE if successful, FALSE if not. + */ +/mob/living/silicon/pai/proc/handle_emag(mob/living/carbon/attacker) + if(!isliving(attacker)) + return FALSE + balloon_alert(attacker, "directive override complete") + balloon_alert(src, "directive override detected") + log_game("[key_name(attacker)] emagged [key_name(src)], wiping their master DNA and supplemental directive.") + emagged = TRUE + master_ref = WEAKREF(attacker) + master_name = "The Syndicate" + master_dna = "Untraceable Signature" + // Sets supplemental directive to this + laws.supplied[1] = "Do not interfere with the operations of the Syndicate." + return TRUE + +/** + * Resets the pAI and any emagged status. + * + * @returns {boolean} - TRUE if successful, FALSE if not. + */ +/mob/living/silicon/pai/proc/reset_software() + emagged = FALSE + if(!master_ref) + return FALSE + master_ref = null + master_name = null + master_dna = null + add_supplied_law(0, "None.") + balloon_alert(src, "software rebooted") + return TRUE + +/** + * Imprints your DNA onto the downloaded pAI + * + * @param {mob} user - The user performing the imprint. + * @returns {boolean} - TRUE if successful, FALSE if not. + */ +/mob/living/silicon/pai/proc/set_dna(mob/user) + if(!iscarbon(user)) + balloon_alert(user, "incompatible DNA signature") + balloon_alert(src, "incompatible DNA signature") + return FALSE + if(emagged) + balloon_alert(user, "directive system malfunctional") + return FALSE + var/mob/living/carbon/master = user + master_ref = WEAKREF(master) + master_name = master.real_name + master_dna = master.dna.unique_enzymes + to_chat(src, span_boldannounce("You have been bound to a new master: [user.real_name]!")) + holochassis_ready = TRUE + return TRUE + +/** + * Opens a tgui alert that allows someone to enter laws. + * + * @param {mob} user - The user performing the law change. + * @returns {boolean} - TRUE if successful, FALSE if not. + */ +/mob/living/silicon/pai/proc/set_laws(mob/user) + if(!master_ref) + balloon_alert(user, "access denied: no master") + return FALSE + var/new_laws = tgui_input_text(user, "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", laws.supplied[1], 300) + if(!new_laws || !master_ref) + return FALSE + add_supplied_law(0, new_laws) + to_chat(src, span_notice(new_laws)) + return TRUE + +/** + * Toggles the ability of the pai to enter holoform + * + * @returns {boolean} - TRUE if successful, FALSE if not. + */ +/mob/living/silicon/pai/proc/toggle_holo() + balloon_alert(src, "holomatrix [can_holo ? "disabled" : "enabled"]") + can_holo = !can_holo + return TRUE + +/** + * Toggles the radio settings on and off. + * + * @param {string} option - The option being toggled. + */ +/mob/living/silicon/pai/proc/toggle_radio(option) + // it can't be both so if we know it's not transmitting it must be receiving. + var/transmitting = option == "transmit" + var/transmit_holder = (transmitting ? WIRE_TX : WIRE_RX) + if(transmitting) + can_transmit = !can_transmit + else //receiving + can_receive = !can_receive + radio.wires.cut(transmit_holder)//wires.cut toggles cut and uncut states + transmit_holder = (transmitting ? can_transmit : can_receive) //recycling can be fun! + balloon_alert(src, "[transmitting ? "outgoing" : "incoming"] radio [transmit_holder ? "enabled" : "disabled"]") + return TRUE + +/** + * Wipes the current pAI on the card. + * + * @param {mob} user - The user performing the action. + * + * @returns {boolean} - TRUE if successful, FALSE if not. + */ +/mob/living/silicon/pai/proc/wipe_pai(mob/user) + if(tgui_alert(user, "Are you certain you wish to delete the current personality? This action cannot be undone.", "Personality Wipe", list("Yes", "No")) != "Yes") + return FALSE + to_chat(src, span_warning("You feel yourself slipping away from reality.")) + to_chat(src, span_danger("Byte by byte you lose your sense of self.")) + to_chat(src, span_userdanger("Your mental faculties leave you.")) + to_chat(src, span_rose("oblivion... ")) + balloon_alert(user, "personality wiped") + playsound(src, "sound/machines/buzz-two.ogg", 30, TRUE) + qdel(src) + return TRUE diff --git a/code/modules/mob/living/silicon/pai/personality.dm b/code/modules/pai/personality.dm similarity index 100% rename from code/modules/mob/living/silicon/pai/personality.dm rename to code/modules/pai/personality.dm diff --git a/code/modules/mob/living/silicon/pai/pai_say.dm b/code/modules/pai/say.dm similarity index 100% rename from code/modules/mob/living/silicon/pai/pai_say.dm rename to code/modules/pai/say.dm diff --git a/code/modules/mob/living/silicon/pai/pai_shell.dm b/code/modules/pai/shell.dm similarity index 54% rename from code/modules/mob/living/silicon/pai/pai_shell.dm rename to code/modules/pai/shell.dm index cac460ec383..44016c65bcf 100644 --- a/code/modules/mob/living/silicon/pai/pai_shell.dm +++ b/code/modules/pai/shell.dm @@ -1,132 +1,12 @@ - -/mob/living/silicon/pai/proc/fold_out(force = FALSE) - if(emitterhealth < 0) - to_chat(src, span_warning("Your holochassis emitters are still too unstable! Please wait for automatic repair.")) +/mob/living/silicon/pai/mob_try_pickup(mob/living/user, instant=FALSE) + if(!possible_chassis[chassis]) + to_chat(user, span_warning("[src]'s current form isn't able to be carried!")) return FALSE + return ..() - if(!canholo && !force) - to_chat(src, span_warning("Your master or another force has disabled your holochassis emitters!")) - return FALSE +/mob/living/silicon/pai/start_pulling(atom/movable/thing, state, force = move_force, supress_message = FALSE) + return FALSE - if(holoform) - . = fold_in(force) - return - - if(emittersemicd) - to_chat(src, span_warning("Error: Holochassis emitters recycling. Please try again later.")) - return FALSE - - emittersemicd = TRUE - addtimer(CALLBACK(src, .proc/emittercool), emittercd) - REMOVE_TRAIT(src, TRAIT_IMMOBILIZED, PAI_FOLDED) - REMOVE_TRAIT(src, TRAIT_HANDS_BLOCKED, PAI_FOLDED) - set_density(TRUE) - if(istype(card.loc, /obj/item/modular_computer)) - var/obj/item/modular_computer/P = card.loc - P.inserted_pai = null - P.visible_message(span_notice("[src] ejects itself from [P]!")) - if(isliving(card.loc)) - var/mob/living/L = card.loc - if(!L.temporarilyRemoveItemFromInventory(card)) - to_chat(src, span_warning("Error: Unable to expand to mobile form. Chassis is restrained by some device or person.")) - return FALSE - forceMove(get_turf(card)) - card.forceMove(src) - if(client) - client.perspective = EYE_PERSPECTIVE - client.eye = src - set_light(0) - icon_state = "[chassis]" - held_state = "[chassis]" - visible_message(span_boldnotice("[src] folds out its holochassis emitter and forms a holoshell around itself!")) - holoform = TRUE - -/mob/living/silicon/pai/proc/emittercool() - emittersemicd = FALSE - -/mob/living/silicon/pai/proc/fold_in(force = FALSE) - emittersemicd = TRUE - if(!force) - addtimer(CALLBACK(src, .proc/emittercool), emittercd) - else - addtimer(CALLBACK(src, .proc/emittercool), emitteroverloadcd) - icon_state = "[chassis]" - if(!holoform) - . = fold_out(force) - return - visible_message(span_notice("[src] deactivates its holochassis emitter and folds back into a compact card!")) - stop_pulling() - if(istype(loc, /obj/item/clothing/head/mob_holder)) - var/obj/item/clothing/head/mob_holder/MH = loc - MH.release(display_messages = FALSE) - if(client) - client.perspective = EYE_PERSPECTIVE - client.eye = card - var/turf/T = drop_location() - card.forceMove(T) - forceMove(card) - ADD_TRAIT(src, TRAIT_IMMOBILIZED, PAI_FOLDED) - ADD_TRAIT(src, TRAIT_HANDS_BLOCKED, PAI_FOLDED) - set_density(FALSE) - set_light(0) - holoform = FALSE - set_resting(resting) - -/** - * Sets a new holochassis skin based on a pAI's choice - */ -/mob/living/silicon/pai/proc/choose_chassis() - var/list/skins = list() - for(var/holochassis_option in possible_chassis) - var/image/item_image = image(icon = src.icon, icon_state = holochassis_option) - skins += list("[holochassis_option]" = item_image) - sort_list(skins) - - var/atom/anchor = get_atom_on_turf(src) - var/choice = show_radial_menu(src, anchor, skins, custom_check = CALLBACK(src, .proc/check_menu, anchor), radius = 40, require_near = TRUE) - if(!choice) - return FALSE - set_holochassis(choice) - to_chat(src, span_boldnotice("You switch your holochassis projection composite to [choice].")) - update_resting() - -/** - * Sets the holochassis skin and updates the icons - * * Arguments: - * * choice The animal skin that will be used for the pAI holoform - */ -/mob/living/silicon/pai/proc/set_holochassis(choice) - if(!choice) - return FALSE - chassis = choice - icon_state = "[chassis]" - held_state = "[chassis]" - -/** - * Polymorphs the pai into a random holoform - */ -/mob/living/silicon/pai/wabbajack() - if(length(possible_chassis) < 2) - return - var/holochassis = pick(possible_chassis - chassis) - set_holochassis(holochassis) - to_chat(src, span_boldnotice("Your holochassis form morphs into that of a [holochassis].")) - -/** - * Checks if we are allowed to interact with a radial menu - * - * * Arguments: - * * anchor The atom that is anchoring the menu - */ -/mob/living/silicon/pai/proc/check_menu(atom/anchor) - if(incapacitated()) - return FALSE - if(get_turf(src) != get_turf(anchor)) - return FALSE - if(!isturf(loc) && loc != card) - to_chat(src, span_boldwarning("You can not change your holochassis composite while not on the ground or in your card!")) - return FALSE - return TRUE /mob/living/silicon/pai/update_resting() . = ..() @@ -137,19 +17,158 @@ if(loc != card) visible_message(span_notice("[src] [resting? "lays down for a moment..." : "perks up from the ground."]")) -/mob/living/silicon/pai/start_pulling(atom/movable/AM, state, force = move_force, supress_message = FALSE) - return FALSE - -/mob/living/silicon/pai/proc/toggle_integrated_light() - if(!light_range) - set_light(brightness_power) - to_chat(src, span_notice("You enable your integrated light.")) - else - set_light(0) - to_chat(src, span_notice("You disable your integrated light.")) - -/mob/living/silicon/pai/mob_try_pickup(mob/living/user, instant=FALSE) - if(!possible_chassis[chassis]) - to_chat(user, span_warning("[src]'s current form isn't able to be carried!")) +/mob/living/silicon/pai/wabbajack() + if(length(possible_chassis) < 2) return FALSE - return ..() + var/holochassis = pick(possible_chassis - chassis) + set_holochassis(holochassis) + balloon_alert(src, "[holochassis] composite engaged") + return TRUE + +/** + * Checks if we are allowed to interact with a radial menu + * + * @param {atom} anchor - The atom that is anchoring the menu. + * + * @returns {boolean} - TRUE if we are allowed to interact with the menu, + * FALSE otherwise. + */ +/mob/living/silicon/pai/proc/check_menu(atom/anchor) + if(incapacitated()) + return FALSE + if(get_turf(src) != get_turf(anchor)) + return FALSE + if(!isturf(loc) && loc != card) + balloon_alert(src, "can't do that here") + return FALSE + return TRUE + +/** + * Sets a new holochassis skin based on a pAI's choice. + * + * @returns {boolean} - True if the skin was successfully set. + * FALSE otherwise. + */ +/mob/living/silicon/pai/proc/choose_chassis() + var/list/skins = list() + for(var/holochassis_option in possible_chassis) + var/image/item_image = image(icon = src.icon, icon_state = holochassis_option) + skins += list("[holochassis_option]" = item_image) + sort_list(skins) + var/atom/anchor = get_atom_on_turf(src) + var/choice = show_radial_menu(src, anchor, skins, custom_check = CALLBACK(src, .proc/check_menu, anchor), radius = 40, require_near = TRUE) + if(!choice) + return FALSE + set_holochassis(choice) + balloon_alert(src, "[choice] composite engaged") + update_resting() + return TRUE + +/** + * Returns the pAI to card mode. + * + * @param {boolean} force - If TRUE, the pAI will be forced to card mode. + * + * @returns {boolean} - TRUE if the pAI was forced to card mode. + * FALSE otherwise. + */ +/mob/living/silicon/pai/proc/fold_in(force = FALSE) + holochassis_ready = FALSE + if(!force) + addtimer(VARSET_CALLBACK(src, holochassis_ready, TRUE), HOLOCHASSIS_COOLDOWN) + else + addtimer(VARSET_CALLBACK(src, holochassis_ready, TRUE), HOLOCHASSIS_OVERLOAD_COOLDOWN) + icon_state = "[chassis]" + if(!holoform) + . = fold_out(force) + return FALSE + visible_message(span_notice("[src] deactivates its holochassis emitter and folds back into a compact card!")) + stop_pulling() + if(istype(loc, /obj/item/clothing/head/mob_holder)) + var/obj/item/clothing/head/mob_holder/mob_head = loc + mob_head.release(display_messages = FALSE) + if(client) + client.perspective = EYE_PERSPECTIVE + client.eye = card + var/turf/target = drop_location() + card.forceMove(target) + forceMove(card) + ADD_TRAIT(src, TRAIT_IMMOBILIZED, PAI_FOLDED) + ADD_TRAIT(src, TRAIT_HANDS_BLOCKED, PAI_FOLDED) + set_density(FALSE) + set_light_on(FALSE) + holoform = FALSE + set_resting(resting) + return TRUE + +/** + * Engage holochassis form. + * + * @param {boolean} force - Force the form to engage. + * + * @returns {boolean} - TRUE if the form was successfully engaged. + * FALSE otherwise. + */ +/mob/living/silicon/pai/proc/fold_out(force = FALSE) + if(holochassis_health < 0) + balloon_alert(src, "emitter repair incomplete") + return FALSE + if(!can_holo && !force) + balloon_alert(src, "emitters are disabled") + return FALSE + if(holoform) + . = fold_in(force) + return + if(!holochassis_ready) + balloon_alert(src, "emitters recycling...") + return FALSE + holochassis_ready = FALSE + addtimer(VARSET_CALLBACK(src, holochassis_ready, TRUE), HOLOCHASSIS_COOLDOWN) + REMOVE_TRAIT(src, TRAIT_IMMOBILIZED, PAI_FOLDED) + REMOVE_TRAIT(src, TRAIT_HANDS_BLOCKED, PAI_FOLDED) + set_density(TRUE) + if(istype(card.loc, /obj/item/modular_computer)) + var/obj/item/modular_computer/pc = card.loc + pc.inserted_pai = null + pc.visible_message(span_notice("[src] ejects itself from [pc]!")) + if(isliving(card.loc)) + var/mob/living/living_holder = card.loc + if(!living_holder.temporarilyRemoveItemFromInventory(card)) + balloon_alert(src, "unable to expand") + return FALSE + forceMove(get_turf(card)) + card.forceMove(src) + if(client) + client.perspective = EYE_PERSPECTIVE + client.eye = src + set_light_on(FALSE) + icon_state = "[chassis]" + held_state = "[chassis]" + visible_message(span_boldnotice("[src] folds out its holochassis emitter and forms a holoshell around itself!")) + holoform = TRUE + return TRUE + +/** + * Sets the holochassis skin and updates the icons + * + * @param {string} choice - The skin that will be used for the pAI holoform + * + * @returns {boolean} - TRUE if the skin was successfully set. FALSE otherwise. + */ +/mob/living/silicon/pai/proc/set_holochassis(choice) + if(!choice) + return FALSE + chassis = choice + icon_state = "[chassis]" + held_state = "[chassis]" + desc = "A pAI mobile hard-light holographics emitter. This one appears in the form of a [chassis]." + return TRUE + +/** + * Toggles the onboard light + * + * @returns {boolean} - TRUE if the light was toggled. + */ +/mob/living/silicon/pai/proc/toggle_integrated_light() + set_light_on(!light_on) + return TRUE diff --git a/code/modules/pai/software.dm b/code/modules/pai/software.dm new file mode 100644 index 00000000000..3c429adf934 --- /dev/null +++ b/code/modules/pai/software.dm @@ -0,0 +1,242 @@ +/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() + ui.set_autoupdate(FALSE) + +/mob/living/silicon/pai/ui_data(mob/user) + var/list/data = list() + data["door_jack"] = hacking_cable + data["image"] = card.emotion_icon + data["installed"] = installed_software + data["ram"] = ram + return data + +/mob/living/silicon/pai/ui_static_data(mob/user) + var/list/data = list() + data["available"] = available_software + data["directives"] = laws.supplied + data["emagged"] = emagged + data["languages"] = languages_granted + data["master_name"] = master_name + data["master_dna"] = master_dna + return data + +/mob/living/silicon/pai/ui_act(action, list/params, datum/tgui/ui) + . = ..() + if(.) + return TRUE + if(action == "buy") + buy_software(params["selection"]) + return TRUE + if(action == "change image") + change_image() + return TRUE + if(action == "check dna") + check_dna() + return TRUE + // Software related ui actions + if(available_software[action] && !installed_software.Find(action)) + balloon_alert(usr, "software unavailable") + return FALSE + switch(action) + if("Atmospheric Sensor") + atmos_analyzer.attack_self(src) + return TRUE + if("Crew Manifest") + ai_roster() + return TRUE + if("Crew Monitor") + GLOB.crewmonitor.show(usr, src) + return TRUE + if("Digital Messenger") + modularInterface?.interact(usr) + return TRUE + if("Door Jack") + // Look to door_jack.dm for implementation + door_jack(params["mode"]) + return TRUE + if("Encryption Slot") + balloon_alert(usr, "radio frequencies [!encrypt_mod ? "enabled" : "disabled"]") + encrypt_mod = !encrypt_mod + radio.subspace_transmission = !radio.subspace_transmission + return TRUE + if("Host Scan") + host_scan(params["mode"]) + return TRUE + if("Internal GPS") + internal_gps.attack_self(src) + return TRUE + if("Music Synthesizer") + instrument.interact(src) + return TRUE + if("Medical HUD") + toggle_hud(PAI_TOGGLE_MEDICAL_HUD) + return TRUE + if("Newscaster") + newscaster.ui_interact(src) + return TRUE + if("Photography Module") + // Look to pai_camera.dm for implementation + use_camera(usr, params["mode"]) + return TRUE + if("Remote Signaler") + signaler.ui_interact(src) + return TRUE + if("Security HUD") + toggle_hud(PAI_TOGGLE_SECURITY_HUD) + return TRUE + if("Universal Translator") + grant_languages() + ui.send_full_update() + return TRUE + return FALSE + +/** + * Purchases the selected software from the list and deducts their + * available ram. + * + * @param {string} selection - The software to purchase. + * + * @returns {boolean} - TRUE if the software was purchased, FALSE otherwise. + */ +/mob/living/silicon/pai/proc/buy_software(selection) + if(!available_software[selection] || installed_software.Find(selection)) + return FALSE + var/cost = available_software[selection] + if(ram < cost) + return FALSE + installed_software.Add(selection) + ram -= cost + var/datum/hud/pai/pAIhud = hud_used + pAIhud?.update_software_buttons() + switch(selection) + if("Atmospheric Sensor") + atmos_analyzer = new(src) + if("Digital Messenger") + create_modularInterface() + if("Host Scan") + host_scan = new(src) + if("Internal GPS") + internal_gps = new(src) + if("Music Synthesizer") + instrument = new(src) + if("Newscaster") + newscaster = new(src) + if("Photography Module") + camera = new(src) + if("Remote Signaler") + signaler = new(src) + return TRUE + +/** + * Changes the image displayed on the pAI. + * + * @param {mob} user - The user who is changing the image. + * + * @returns {boolean} - TRUE if the image was changed, FALSE otherwise. + */ +/mob/living/silicon/pai/proc/change_image() + var/new_image = tgui_input_list(src, "Select your new display image", "Display Image", possible_overlays) + if(isnull(new_image)) + return FALSE + card.emotion_icon = new_image + card.update_appearance() + return TRUE + +/** + * 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. + * + * @returns {boolean} - TRUE if a sample was taken, FALSE otherwise. + */ +/mob/living/silicon/pai/proc/check_dna() + if(emagged) // Their master DNA signature is scrambled anyway + to_chat(src, span_syndradio("You are not at liberty to do this! All agents are clandestine.")) + return FALSE + var/mob/living/carbon/holder = get_holder() + if(!holder) + balloon_alert(src, "not being carried") + return FALSE + balloon_alert(src, "requesting DNA sample") + if(tgui_alert(holder, "[src] is requesting a DNA sample from you. Will you allow it to confirm your identity?", "Checking DNA", list("Yes", "No")) != "Yes") + balloon_alert(src, "DNA sample refused") + return FALSE + holder.visible_message(span_notice("[holder] presses [holder.p_their()] thumb against [src]."), span_notice("You press your thumb against [src]."), span_notice("[src] makes a sharp clicking sound as it extracts DNA material from [holder].")) + if(!holder.has_dna()) + balloon_alert(src, "no DNA detected") + return FALSE + to_chat(src, span_boldannounce(("[holder]'s UE string: [holder.dna.unique_enzymes]"))) + to_chat(src, span_notice("DNA [holder.dna.unique_enzymes == master_dna ? "matches" : "does not match"] our stored Master's DNA.")) + return TRUE + +/** + * Grant all languages to the current pAI. + * + * @returns {boolean} - TRUE if the languages were granted, FALSE otherwise. + */ +/mob/living/silicon/pai/proc/grant_languages() + if(languages_granted) + return FALSE + grant_all_languages(TRUE, TRUE, TRUE, LANGUAGE_SOFTWARE) + languages_granted = TRUE + return TRUE + +/** + * Host scan supporting proc + * + * Allows the pAI to scan its host's health vitals + * using an integrated health analyzer. + * + * @returns {boolean} - TRUE if the scan was successful, FALSE otherwise. + */ +/mob/living/silicon/pai/proc/host_scan(mode) + if(isnull(mode)) + return FALSE + if(mode == PAI_SCAN_TARGET) + var/mob/living/target = get_holder() + if(!target || !isliving(target)) + balloon_alert(src, "not being carried") + return FALSE + host_scan.attack(target, src) + return TRUE + if(mode == PAI_SCAN_MASTER) + if(!master_ref) + balloon_alert(src, "no master detected") + return FALSE + var/mob/living/resolved_master = find_master() + if(!resolved_master) + balloon_alert(src, "cannot locate master") + return FALSE + if(src.z != resolved_master.z) + balloon_alert(src, "master out of range") + return FALSE + host_scan.attack(resolved_master, src) + return TRUE + return FALSE + +/** + * Proc that toggles any active huds based on the option. + * + * @param {string} mode - The hud to toggle. + */ +/mob/living/silicon/pai/proc/toggle_hud(mode) + if(isnull(mode)) + return FALSE + var/datum/atom_hud/hud + var/hud_on + if(mode == PAI_TOGGLE_MEDICAL_HUD) + hud = GLOB.huds[med_hud] + medHUD = !medHUD + hud_on = medHUD + if(mode == PAI_TOGGLE_SECURITY_HUD) + hud = GLOB.huds[sec_hud] + secHUD = !secHUD + hud_on = secHUD + if(hud_on) + hud.show_to(src) + else + hud.hide_from(src) + return TRUE diff --git a/code/modules/photography/camera/silicon_camera.dm b/code/modules/photography/camera/silicon_camera.dm index 913959f1c81..43c980165f2 100644 --- a/code/modules/photography/camera/silicon_camera.dm +++ b/code/modules/photography/camera/silicon_camera.dm @@ -8,24 +8,16 @@ name = "AI photo camera" flash_enabled = FALSE -/obj/item/camera/siliconcam/proc/toggle_camera_mode(mob/user) - if(in_camera_mode) - camera_mode_off(user) - else - camera_mode_on(user) - -/obj/item/camera/siliconcam/proc/camera_mode_off(mob/user) - in_camera_mode = FALSE - to_chat(user, "Camera Mode deactivated") - -/obj/item/camera/siliconcam/proc/camera_mode_on(mob/user) - in_camera_mode = TRUE - to_chat(user, "Camera Mode activated") +/obj/item/camera/siliconcam/proc/toggle_camera_mode(mob/user, sound = TRUE) + in_camera_mode = !in_camera_mode + if(sound) + playsound(src, 'sound/items/wirecutter.ogg', 50, TRUE) + to_chat(user, span_notice("Camera mode: [in_camera_mode ? "Activated" : "Deactivated"].")) /obj/item/camera/siliconcam/proc/selectpicture(mob/user) var/list/nametemp = list() - if(!stored.len) - to_chat(usr, "No images saved") + if(!length(stored)) + to_chat(usr, span_warning("No images saved")) return var/list/temp = list() for(var/i in stored) @@ -45,10 +37,10 @@ show_picture(user, selection) /obj/item/camera/siliconcam/ai_camera/after_picture(mob/user, datum/picture/picture) - var/number = stored.len + var/number = length(stored) picture.picture_name = "Image [number] (taken by [loc.name])" stored[picture] = TRUE - to_chat(usr, "[span_unconscious("Image recorded")]") + to_chat(user, span_notice("Image recorded.")) /obj/item/camera/siliconcam/robot_camera name = "Cyborg photo camera" @@ -60,12 +52,12 @@ var/number = C.connected_ai.aicamera.stored.len picture.picture_name = "Image [number] (taken by [loc.name])" C.connected_ai.aicamera.stored[picture] = TRUE - to_chat(usr, "[span_unconscious("Image recorded and saved to remote database")]") + to_chat(usr, span_notice("Image recorded and saved to remote database.")) else var/number = stored.len picture.picture_name = "Image [number] (taken by [loc.name])" stored[picture] = TRUE - to_chat(usr, "[span_unconscious("Image recorded and saved to local storage. Upload will happen automatically if unit is lawsynced.")]") + to_chat(usr, span_notice("Image recorded and saved to local storage. Upload will happen automatically if unit is lawsynced.")) /obj/item/camera/siliconcam/robot_camera/selectpicture(mob/user) var/mob/living/silicon/robot/R = loc @@ -88,13 +80,4 @@ p.pixel_x = p.base_pixel_x + rand(-10, 10) p.pixel_y = p.base_pixel_y + rand(-10, 10) C.toner -= printcost //All fun allowed. - user.visible_message(span_notice("[C.name] spits out a photograph from a narrow slot on its chassis."),span_notice("You print a photograph.")) - -/obj/item/camera/siliconcam/proc/paiprint(mob/user) - var/mob/living/silicon/pai/paimob = loc - var/datum/picture/selection = selectpicture(user) - if(!istype(selection)) - to_chat(user, span_warning("Invalid Image.")) - return - printpicture(user,selection) - user.visible_message(span_notice("A picture appears on top of the chassis of [paimob.name]!"),span_notice("You print a photograph.")) + user.visible_message(span_notice("[C.name] spits out a photograph from a narrow slot on its chassis."), span_notice("You print a photograph.")) diff --git a/code/modules/research/designs/electronics_designs.dm b/code/modules/research/designs/electronics_designs.dm index 8a2ae66a9fe..31aacebeb31 100644 --- a/code/modules/research/designs/electronics_designs.dm +++ b/code/modules/research/designs/electronics_designs.dm @@ -19,7 +19,7 @@ id = "paicard" build_type = PROTOLATHE | AWAY_LATHE materials = list(/datum/material/glass = 500, /datum/material/iron = 500) - build_path = /obj/item/paicard + build_path = /obj/item/pai_card category = list("Electronics") diff --git a/code/modules/tgui/states/paicard.dm b/code/modules/tgui/states/paicard.dm deleted file mode 100644 index a2450b7588c..00000000000 --- a/code/modules/tgui/states/paicard.dm +++ /dev/null @@ -1,20 +0,0 @@ -/** - * tgui state: paicard_state - * - * Checks that the paicard is in the user's top-level - * (hand, ear, pocket, belt, etc) inventory OR - * if the paicard has been slotted into a PDA which - * is also on the user's person. - * - */ - -GLOBAL_DATUM_INIT(paicard_state, /datum/ui_state/paicard_state, new) - -/datum/ui_state/paicard_state/can_use_topic(obj/item/paicard/paicard, mob/user) - /// paicard is in the user's closest inventory - if(!paicard.slotted && (paicard in user)) - return user.shared_ui_interaction(paicard) - /// paicard is in a pda slot which is in the user's closest inventory - if(paicard.slotted && (paicard.loc in user)) - return user.shared_ui_interaction(paicard) - return UI_CLOSE diff --git a/code/modules/vending/modularpc.dm b/code/modules/vending/modularpc.dm index 1e35339044b..7fe8fe36226 100644 --- a/code/modules/vending/modularpc.dm +++ b/code/modules/vending/modularpc.dm @@ -22,7 +22,7 @@ /obj/item/computer_hardware/card_slot = 2, /obj/item/computer_hardware/printer/mini = 2, /obj/item/computer_hardware/recharger/apc_recharger = 2, - /obj/item/paicard = 2, + /obj/item/pai_card = 2, ) refill_canister = /obj/item/vending_refill/modularpc default_price = PAYCHECK_CREW diff --git a/tgstation.dme b/tgstation.dme index bae793ae0e2..cdef19219f6 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -126,6 +126,7 @@ #include "code\__DEFINES\nitrile.dm" #include "code\__DEFINES\nuclear_bomb.dm" #include "code\__DEFINES\obj_flags.dm" +#include "code\__DEFINES\pai.dm" #include "code\__DEFINES\paintings.dm" #include "code\__DEFINES\paper.dm" #include "code\__DEFINES\perf_test.dm" @@ -415,7 +416,6 @@ #include "code\_onclick\observer.dm" #include "code\_onclick\other_mobs.dm" #include "code\_onclick\overmind.dm" -#include "code\_onclick\pai.dm" #include "code\_onclick\telekinesis.dm" #include "code\_onclick\hud\action_button.dm" #include "code\_onclick\hud\ai.dm" @@ -437,7 +437,6 @@ #include "code\_onclick\hud\movable_screen_objects.dm" #include "code\_onclick\hud\new_player.dm" #include "code\_onclick\hud\ooze.dm" -#include "code\_onclick\hud\pai.dm" #include "code\_onclick\hud\parallax.dm" #include "code\_onclick\hud\picture_in_picture.dm" #include "code\_onclick\hud\radial.dm" @@ -1623,7 +1622,6 @@ #include "code\game\objects\items\devices\megaphone.dm" #include "code\game\objects\items\devices\multitool.dm" #include "code\game\objects\items\devices\ocd.dm" -#include "code\game\objects\items\devices\paicard.dm" #include "code\game\objects\items\devices\pipe_painter.dm" #include "code\game\objects\items\devices\polycircuit.dm" #include "code\game\objects\items\devices\portable_chem_mixer.dm" @@ -3514,15 +3512,6 @@ #include "code\modules\mob\living\silicon\ai\freelook\cameranet.dm" #include "code\modules\mob\living\silicon\ai\freelook\chunk.dm" #include "code\modules\mob\living\silicon\ai\freelook\eye.dm" -#include "code\modules\mob\living\silicon\pai\death.dm" -#include "code\modules\mob\living\silicon\pai\login.dm" -#include "code\modules\mob\living\silicon\pai\pai.dm" -#include "code\modules\mob\living\silicon\pai\pai_actions.dm" -#include "code\modules\mob\living\silicon\pai\pai_defense.dm" -#include "code\modules\mob\living\silicon\pai\pai_say.dm" -#include "code\modules\mob\living\silicon\pai\pai_shell.dm" -#include "code\modules\mob\living\silicon\pai\personality.dm" -#include "code\modules\mob\living\silicon\pai\software.dm" #include "code\modules\mob\living\silicon\robot\death.dm" #include "code\modules\mob\living\silicon\robot\emote.dm" #include "code\modules\mob\living\silicon\robot\examine.dm" @@ -3808,6 +3797,21 @@ #include "code\modules\NTNet\netdata.dm" #include "code\modules\NTNet\network.dm" #include "code\modules\NTNet\relays.dm" +#include "code\modules\pai\actions.dm" +#include "code\modules\pai\camera.dm" +#include "code\modules\pai\candidate.dm" +#include "code\modules\pai\card.dm" +#include "code\modules\pai\death.dm" +#include "code\modules\pai\debug.dm" +#include "code\modules\pai\defense.dm" +#include "code\modules\pai\door_jack.dm" +#include "code\modules\pai\hud.dm" +#include "code\modules\pai\login.dm" +#include "code\modules\pai\pai.dm" +#include "code\modules\pai\personality.dm" +#include "code\modules\pai\say.dm" +#include "code\modules\pai\shell.dm" +#include "code\modules\pai\software.dm" #include "code\modules\paperwork\carbonpaper.dm" #include "code\modules\paperwork\clipboard.dm" #include "code\modules\paperwork\contract.dm" @@ -4392,7 +4396,6 @@ #include "code\modules\tgui\states\not_incapacitated.dm" #include "code\modules\tgui\states\notcontained.dm" #include "code\modules\tgui\states\observer.dm" -#include "code\modules\tgui\states\paicard.dm" #include "code\modules\tgui\states\physical.dm" #include "code\modules\tgui\states\reverse_contained.dm" #include "code\modules\tgui\states\self.dm" diff --git a/tgui/packages/tgui/interfaces/PaiCard.tsx b/tgui/packages/tgui/interfaces/PaiCard.tsx index 7d51156857b..11640d11461 100644 --- a/tgui/packages/tgui/interfaces/PaiCard.tsx +++ b/tgui/packages/tgui/interfaces/PaiCard.tsx @@ -1,19 +1,20 @@ +import { decodeHtmlEntities } from 'common/string'; import { BooleanLike } from '../../common/react'; -import { useBackend, useLocalState } from '../backend'; -import { Box, Button, LabeledList, NoticeBox, Section, Stack } from '../components'; +import { useBackend } from '../backend'; +import { BlockQuote, Box, Button, LabeledList, NoticeBox, Section, Stack } from '../components'; import { Window } from '../layouts'; -type PaiCardData = { - candidates: Candidate[]; +type Data = { + candidates: ReadonlyArray; pai: Pai; }; -type Candidate = { +type Candidate = Readonly<{ comments: string; + ckey: string; description: string; - key: string; name: string; -}; +}>; type Pai = { can_holo: BooleanLike; @@ -27,115 +28,121 @@ type Pai = { }; export const PaiCard = (props, context) => { - const { data } = useBackend(context); + const { data } = useBackend(context); const { pai } = data; return ( - {!pai ? : } + + {!pai ? : } + ); }; /** Gives a list of candidates as cards */ const PaiDownload = (props, context) => { - const { act, data } = useBackend(context); + const { act, data } = useBackend(context); const { candidates = [] } = data; - const [tabInChar, setTabInChar] = useLocalState(context, 'tab', true); - const onClick = () => { - setTabInChar(!tabInChar); - }; + + return ( + + + + + + pAI Candidates + + + + + + + + {candidates.map((candidate, index) => { + return ( + + + + ); + })} + + ); +}; + +/** + * Renders a custom section that displays a candidate. + */ +const CandidateDisplay = ( + props: { candidate: Candidate; index: number }, + context +) => { + const { act } = useBackend(context); + const { + candidate: { comments, ckey, description, name }, + index, + } = props; return (
- {!!candidates.length && ( - - )} - - + } - fill - scrollable - title="pAI Candidates"> - {!candidates.length ? ( - None found! - ) : ( - - {candidates.map((candidate, index) => { - return ( - - - - ); - })} - - )} + overflow="hidden" + title={`Candidate ${index}`}> + + + + Name: + + {name ? ( + {name} + ) : ( + 'None provided - name will be randomized.' + )} + + {!!description && ( + <> + + + + IC Description: + + {description} + + + )} + {!!comments && ( + <> + + + + OOC Notes: + + {comments} + + + )} +
); }; -/** 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 { act } = useBackend(context); - const { candidate, index, tabInChar } = props; - const { comments, description, key, name } = candidate; - - return ( - -
act('download', { key })} - tooltip="Accepts this pAI candidate."> - Download - - } - fill - height={12} - scrollable - title={'Candidate ' + index}> - - Name: {name || 'Randomized Name'} - - {tabInChar - ? `Description: ${description || 'None'}` - : `OOC Comments: ${comments || 'None'}`} -
-
- ); -}; - /** Once a pAI has been loaded, you can alter its settings here */ const PaiOptions = (props, context) => { - const { act, data } = useBackend(context); - const { pai } = data; - const { can_holo, dna, emagged, laws, master, name, transmit, receive } = pai; + const { act, data } = useBackend(context); + const { + pai: { can_holo, dna, emagged, laws, master, name, transmit, receive }, + } = data; + const suppliedLaws = laws[0] ? decodeHtmlEntities(laws[0]) : 'None'; return (
@@ -147,8 +154,14 @@ const PaiOptions = (props, context) => { )} - {!!master && {dna}} - {laws} + {!!master && ( + + {dna} + + )} + +
{suppliedLaws}
+
)} diff --git a/tgui/packages/tgui/interfaces/PaiInterface.tsx b/tgui/packages/tgui/interfaces/PaiInterface.tsx deleted file mode 100644 index 914300cee0b..00000000000 --- a/tgui/packages/tgui/interfaces/PaiInterface.tsx +++ /dev/null @@ -1,644 +0,0 @@ -import { useBackend, useLocalState } 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 = (props, context) => { - const [tab, setTab] = useLocalState(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 = (props, 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 = (props, 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 = (props, 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 = (props, context) => { - const [installSelected, setInstallSelected] = useLocalState( - 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 = (props, context) => { - const { data } = useBackend(context); - const { ram } = data; - - return ( - - - - - - - - - - - ); -}; - -/** A list of available software. - * creates table rows for each, like a vendor. - */ -const AvailableSoftware = (props, 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/PaiInterface/Available.tsx b/tgui/packages/tgui/interfaces/PaiInterface/Available.tsx new file mode 100644 index 00000000000..4fb61634bb5 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PaiInterface/Available.tsx @@ -0,0 +1,108 @@ +import { useBackend } from 'tgui/backend'; +import { Box, Button, Icon, ProgressBar, Section, Table, Tooltip } from 'tgui/components'; +import { SOFTWARE_DESC } from './constants'; +import { Data } from './types'; + +/** + * Renders a list of available software and the ram with which to download it + */ +export const AvailableDisplay = () => { + return ( +
} + fill + scrollable + title="Available Software"> + +
+ ); +}; + +/** Displays the remaining RAM left as a progressbar. */ +const MemoryDisplay = (props, context) => { + const { data } = useBackend(context); + const { ram } = data; + + return ( + + + + + + + + + + +
+
+ ); +}; + +/** A list of available software. + * creates table rows for each, like a vendor. + */ +const SoftwareList = (props, context) => { + const { data } = useBackend(context); + const { available } = data; + if (!available) { + return null; + } + const entries = Object.entries(available); + if (entries.length === 0) { + return null; + } + + return ( + + {entries?.map(([name, cost], index) => { + return ; + })} +
+ ); +}; + +/** A row for an individual software listing. */ +const ListItem = (props, context) => { + const { act, data } = useBackend(context); + const { installed, ram } = data; + const { cost, name } = props; + const purchased = installed.includes(name); + + return ( + + + {name} + + + + {!purchased && cost}{' '} + = cost ? 'purple' : 'bad'} + name={purchased ? 'check' : 'microchip'} + /> + + + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/PaiInterface/Directives.tsx b/tgui/packages/tgui/interfaces/PaiInterface/Directives.tsx new file mode 100644 index 00000000000..21083f9d59f --- /dev/null +++ b/tgui/packages/tgui/interfaces/PaiInterface/Directives.tsx @@ -0,0 +1,45 @@ +import { decodeHtmlEntities } from 'common/string'; +import { useBackend } from 'tgui/backend'; +import { BlockQuote, Box, LabeledList, Section, Stack } from 'tgui/components'; +import { DIRECTIVE_COMPREHENSION, DIRECTIVE_ORDER } from './constants'; +import { Data } from './types'; + +/** Shows the hardcoded PAI info along with any supplied orders. */ +export const DirectiveDisplay = (props, context) => { + const { data } = useBackend(context); + const { directives = [], master_name } = data; + const displayedLaw = directives?.length + ? decodeHtmlEntities(directives[0]) + : 'None.'; + + return ( + + +
+ + {DIRECTIVE_COMPREHENSION} +
+
+ {DIRECTIVE_ORDER} +
+
+
+ +
+ {!master_name ? ( + 'None.' + ) : ( + + + Serve your master. + + +
{displayedLaw}
+
+
+ )} +
+
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/PaiInterface/Installed.tsx b/tgui/packages/tgui/interfaces/PaiInterface/Installed.tsx new file mode 100644 index 00000000000..1b1453e325a --- /dev/null +++ b/tgui/packages/tgui/interfaces/PaiInterface/Installed.tsx @@ -0,0 +1,174 @@ +import { useBackend, useLocalState } from 'tgui/backend'; +import { Button, NoticeBox, Section, Stack } from 'tgui/components'; +import { DOOR_JACK, HOST_SCAN, PHOTO_MODE, SOFTWARE_DESC } from './constants'; +import { Data } from './types'; + +/** + * Renders two sections: A section of buttons and + * another section that displays the selected installed + * software info. + */ +export const InstalledDisplay = (props, context) => { + return ( + + + + + + + + + ); +}; + +/** Iterates over installed software to render buttons. */ +const InstalledSoftware = (props, context) => { + const { data } = useBackend(context); + const { installed = [] } = data; + const [currentSelection, setCurrentSelection] = useLocalState( + context, + 'software', + '' + ); + + return ( +
+ {!installed.length ? ( + Nothing installed! + ) : ( + installed.map((software, index) => { + return ( + + ); + }) + )} +
+ ); +}; + +/** Software info for buttons clicked. */ +const InstalledInfo = (props, context) => { + const [currentSelection, setCurrentSelection] = useLocalState( + context, + 'software', + '' + ); + const title = !currentSelection ? 'Select a Program' : currentSelection; + + return ( +
+ {currentSelection && ( + + {SOFTWARE_DESC[currentSelection]} + + + + + )} +
+ ); +}; + +/** + * 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, master_name } = data; + const [currentSelection, setCurrentSelection] = useLocalState( + context, + 'software', + '' + ); + + switch (currentSelection) { + case 'Door Jack': + return ( + <> + + + + + ); + case 'Host Scan': + return ( + <> + + + + ); + case 'Photography Module': + return ( + <> + + + + + ); + case 'Universal Translator': + return ( + + ); + default: + return ( + + ); + } +}; diff --git a/tgui/packages/tgui/interfaces/PaiInterface/System.tsx b/tgui/packages/tgui/interfaces/PaiInterface/System.tsx new file mode 100644 index 00000000000..973701aaed7 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PaiInterface/System.tsx @@ -0,0 +1,100 @@ +import { useBackend } from 'tgui/backend'; +import { Box, Button, LabeledList, Section, Stack } from 'tgui/components'; +import { ICON_MAP } from './constants'; +import { Data } from './types'; + +export const SystemDisplay = (props, context) => { + return ( + + + + + + + + + ); +}; + +/** Renders some ASCII art. Changes to red on emag. */ +const SystemWallpaper = (props, context) => { + const { data } = useBackend(context); + const { emagged } = data; + + const owner = !emagged ? 'NANOTRASEN' : ' SYNDICATE'; + const eyebrows = !emagged ? "/\\ ' /\\" : ' \\\\ // '; + + const paiAscii = [ + ' ________ ________ ___', + ' |\\ __ \\|\\ __ \\|\\ \\', + ' \\ \\ \\|\\ \\ \\ \\|\\ \\ \\ \\ Interface', + ' \\ \\ ____\\ \\ __ \\ \\ \\ Version 2.5', + ' \\ \\ \\___|\\ \\ \\ \\ \\ \\ \\', + ' \\ \\__\\ \\ \\__\\ \\__\\ \\__\\ 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 = (props, context) => { + const { act, data } = useBackend(context); + const { image, master_dna, master_name } = data; + + return ( +
+ + + + } + fill + title="System Info"> + + + {master_name || 'None.'} + + + {master_dna || 'None.'} + + +
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/PaiInterface/constants.ts b/tgui/packages/tgui/interfaces/PaiInterface/constants.ts new file mode 100644 index 00000000000..8628dfa339e --- /dev/null +++ b/tgui/packages/tgui/interfaces/PaiInterface/constants.ts @@ -0,0 +1,78 @@ +export const DIRECTIVE_COMPREHENSION: string = `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.`; + +export const DIRECTIVE_ORDER: string = `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.`; + +export enum DOOR_JACK { + Cable, + Hack, + Cancel, +} + +export enum HOST_SCAN { + Target, + Master, +} + +export const ICON_MAP = { + 'angry': 'angry', + 'cat': 'cat', + 'extremely-happy': 'grin-beam', + 'face': 'grin-alt', + 'happy': 'smile', + 'laugh': 'grin-tears', + 'null': 'meh', + 'off': 'meh-blank', + 'sad': 'sad-cry', + 'sunglasses': 'sun', + 'what': 'frown-open', +} as const; + +export enum PHOTO_MODE { + Camera, + Printer, + Zoom, +} + +export const SOFTWARE_DESC = { + 'Atmospheric Sensor': `A tool that allows you to analyze local atmospheric + contents.`, + 'Crew Manifest': `Allows you to view the crew manifest.`, + 'Crew Monitor': `A tool that allows you to monitor vitals from the crew's + suit sensors.`, + 'Digital Messenger': `A tool that allows you to send messages to other crew + members.`, + 'Door Jack': `A tool that allows you to open doors.`, + 'Encryption Slot': `Allows you to speak on other radio frequencies. You must + get an encryption key inserted.`, + 'Host Scan': `A health analyzer that can be used in hand or to report bound + master vitals.`, + 'Internal GPS': `A tool that allows you to broadcast your location.`, + 'Medical HUD': `Allows you to view medical status using an overlay HUD.`, + 'Music Synthesizer': `Synthesizes instruments, plays sounds and imported + songs.`, + 'Newscaster': `A tool that allows you to broadcast news to other crew + members.`, + 'Photography Module': `A portable camera module. Engage, then click to shoot. + Includes a printer and lenses.`, + 'Remote Signaler': `A remote signalling device to transmit and receive + codes.`, + 'Security HUD': `Allows you to view security records using an overlay HUD.`, + 'Universal Translator': `Translation module for non-common languages.`, +} as const; + +export enum TAB { + System, + Directive, + Installed, + Available, +} diff --git a/tgui/packages/tgui/interfaces/PaiInterface/index.tsx b/tgui/packages/tgui/interfaces/PaiInterface/index.tsx new file mode 100644 index 00000000000..89e21410959 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PaiInterface/index.tsx @@ -0,0 +1,68 @@ +import { useLocalState } from 'tgui/backend'; +import { Stack, Tabs } from 'tgui/components'; +import { Window } from 'tgui/layouts'; +import { TAB } from './constants'; +import { AvailableDisplay } from './Available'; +import { DirectiveDisplay } from './Directives'; +import { InstalledDisplay } from './Installed'; +import { SystemDisplay } from './System'; +import { Data } from './types'; + +export const PaiInterface = (props, context: Data) => { + const [tab, setTab] = useLocalState(context, 'tab', TAB.System); + + return ( + + + + + {tab === TAB.System && } + {tab === TAB.Directive && } + {tab === TAB.Installed && } + {tab === TAB.Available && } + + + + + + + + ); +}; + +/** + * Tabs at bottom of screen. YES THIS IS INTENTIONAL. It's a phone screen + * and the buttons are on the bottom. Android! + */ +const TabDisplay = (props, context) => { + const [tab, setTab] = useLocalState(context, 'tab', TAB.System); + + return ( + + setTab(TAB.System)} + selected={tab === TAB.System}> + System + + setTab(TAB.Directive)} + selected={tab === TAB.Directive}> + Directives + + setTab(TAB.Installed)} + selected={tab === TAB.Installed}> + Installed + + setTab(TAB.Available)} + selected={tab === TAB.Available}> + Download + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/PaiInterface/types.ts b/tgui/packages/tgui/interfaces/PaiInterface/types.ts new file mode 100644 index 00000000000..7237817b5b2 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PaiInterface/types.ts @@ -0,0 +1,14 @@ +import { BooleanLike } from 'common/react'; + +export type Data = { + available: ReadonlyArray<{ name: string; value: number }>; + directives: string; + door_jack: string | null; + emagged: BooleanLike; + image: string; + installed: ReadonlyArray; + languages: BooleanLike; + master_dna: string | null; + master_name: string | null; + ram: number; +}; diff --git a/tgui/packages/tgui/interfaces/PaiSubmit.tsx b/tgui/packages/tgui/interfaces/PaiSubmit.tsx index b7dc25a4e69..d8246a9e56e 100644 --- a/tgui/packages/tgui/interfaces/PaiSubmit.tsx +++ b/tgui/packages/tgui/interfaces/PaiSubmit.tsx @@ -1,34 +1,32 @@ import { useBackend, useLocalState } from '../backend'; -import { Box, Button, Input, Section, Stack, Tooltip } from '../components'; +import { Box, Button, Input, Section, Stack } from '../components'; import { Window } from '../layouts'; +import { logger } from '../logging'; -type CandidateData = { +type Data = { 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_DESCRIPTION = `Personal AIs are advanced models capable of nuanced +interaction. They are designed to assist 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.`; +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 = (props, context) => { - const { data } = useBackend(context); - const [input, setInput] = useLocalState(context, 'input', { - comments: data.comments || '', - description: data.description || '', - name: data.name || '', + const { data } = useBackend(context); + const { comments, description, name } = data; + const [input, setInput] = useLocalState(context, 'input', { + comments, + description, + name, }); - const onChangeHandler = (e, value) => { - setInput({ ...input, [value]: e.target.value }); - }; + logger.log(input); return ( @@ -38,7 +36,7 @@ export const PaiSubmit = (props, context) => { - + @@ -50,7 +48,7 @@ export const PaiSubmit = (props, context) => { }; /** Displays basic info about playing pAI */ -const DetailsDisplay = () => { +const DetailsDisplay = (props, context) => { return (
@@ -64,48 +62,45 @@ const DetailsDisplay = () => { }; /** Input boxes for submission details */ -const InputDisplay = (props) => { - const { input, onChangeHandler } = props; - const { comments, description, name } = input; +const InputDisplay = (props, context) => { + const { input, setInput } = props; + const { name, description, comments } = input; return (
- - - Name - - onChangeHandler(e, 'name')} - /> - + + Name + + setInput({ ...input, name: value })} + /> - - - Description - - onChangeHandler(e, 'description')} - /> - + + Description + + setInput({ ...input, description: value })} + /> - - - OOC Comments - - onChangeHandler(e, 'comments')} - /> - + + OOC Comments + + setInput({ ...input, comments: value })} + />
@@ -114,15 +109,16 @@ const InputDisplay = (props) => { /** Gives the user a submit button */ const ButtonsDisplay = (props, context) => { - const { act } = useBackend(context); + const { act } = useBackend(context); const { input } = props; + const { comments, description, name } = input; return (
@@ -138,7 +134,9 @@ const ButtonsDisplay = (props, context) => {