Reworks pAIs (#68241)

A pretty heavy refactor for pAIs that just spilled into a rework.

Attempts to fully document and organize backend code.
Fixes a large number of bugs left untouched for a decade.
Breaks down the frontend into subcomponents.
Rebalances their software modules.
(should) fix pAI faces get removed if you activate them during alert #68242
This commit is contained in:
Jeremiah
2022-07-24 08:18:59 -07:00
committed by GitHub
parent c529eab0e0
commit 86e801987e
63 changed files with 2304 additions and 2067 deletions
+1 -1
View File
@@ -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" = (
+1 -1
View File
@@ -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" = (
+1 -1
View File
@@ -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{
+1 -1
View File
@@ -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{
+13 -13
View File
@@ -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)
@@ -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,
+4 -4
View File
@@ -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" = (
+5 -5
View File
@@ -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" = (
+1 -1
View File
@@ -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" = (
+5 -5
View File
@@ -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
+1 -1
View File
@@ -845,7 +845,7 @@
/obj/item/food/monkeycube{
pixel_x = 5
},
/obj/item/paicard{
/obj/item/pai_card{
pixel_x = -6;
pixel_y = 1
},
+1
View File
@@ -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"
+5 -5
View File
@@ -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"
+31
View File
@@ -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
+1 -2
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
-6
View File
@@ -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
+40 -110
View File
@@ -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
-45
View File
@@ -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
-231
View File
@@ -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
+1 -1
View File
@@ -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,
-33
View File
@@ -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 != "")
+1 -1
View File
@@ -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!"))
+1 -1
View File
@@ -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, "<b>Your laws have been changed.</b>")
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)
@@ -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)
-286
View File
@@ -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
@@ -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, "<b>No DNA detected.</b>")
return
to_chat(pai, "<font color = red><h3>[master]'s UE string : [master.dna.unique_enzymes]</h3></font>")
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
@@ -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
@@ -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."))
@@ -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
@@ -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)
+58
View File
@@ -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
+35
View File
@@ -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
+261
View File
@@ -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
+12
View File
@@ -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)
+45
View File
@@ -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)
@@ -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
+134
View File
@@ -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
@@ -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
+415
View File
@@ -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
@@ -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
+242
View File
@@ -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
@@ -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, "<span class='infoplain'><B>Camera Mode deactivated</B></span>")
/obj/item/camera/siliconcam/proc/camera_mode_on(mob/user)
in_camera_mode = TRUE
to_chat(user, "<span class='infoplain'><B>Camera Mode activated</B></span>")
/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, "<span class='infoplain'><font color=red><b>No images saved</b></font></span>")
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 class='infoplain'>[span_unconscious("Image recorded")]</span>")
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 class='infoplain'>[span_unconscious("Image recorded and saved to remote database")]</span>")
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 class='infoplain'>[span_unconscious("Image recorded and saved to local storage. Upload will happen automatically if unit is lawsynced.")]</span>")
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."))
@@ -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")
-20
View File
@@ -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
+1 -1
View File
@@ -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
+16 -13
View File
@@ -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"
+114 -97
View File
@@ -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<Candidate>;
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<PaiCardData>(context);
const { data } = useBackend<Data>(context);
const { pai } = data;
return (
<Window width={400} height={400} title="pAI Options Menu">
<Window.Content>{!pai ? <PaiDownload /> : <PaiOptions />}</Window.Content>
<Window.Content scrollable>
{!pai ? <PaiDownload /> : <PaiOptions />}
</Window.Content>
</Window>
);
};
/** Gives a list of candidates as cards */
const PaiDownload = (props, context) => {
const { act, data } = useBackend<PaiCardData>(context);
const { act, data } = useBackend<Data>(context);
const { candidates = [] } = data;
const [tabInChar, setTabInChar] = useLocalState(context, 'tab', true);
const onClick = () => {
setTabInChar(!tabInChar);
};
return (
<Stack fill vertical>
<Stack.Item>
<NoticeBox info>
<Stack fill>
<Stack.Item grow fontSize="16px">
pAI Candidates
</Stack.Item>
<Stack.Item>
<Button
color="good"
icon="bell"
onClick={() => act('request')}
tooltip="Request more candidates from beyond.">
Request
</Button>
</Stack.Item>
</Stack>
</NoticeBox>
</Stack.Item>
{candidates.map((candidate, index) => {
return (
<Stack.Item key={index}>
<CandidateDisplay candidate={candidate} index={index + 1} />
</Stack.Item>
);
})}
</Stack>
);
};
/**
* Renders a custom section that displays a candidate.
*/
const CandidateDisplay = (
props: { candidate: Candidate; index: number },
context
) => {
const { act } = useBackend<Data>(context);
const {
candidate: { comments, ckey, description, name },
index,
} = props;
return (
<Section
buttons={
<>
{!!candidates.length && (
<Button
icon="info"
onClick={onClick}
tooltip="Toggles between IC and OOC information.">
{tabInChar ? 'IC' : 'OOC'}
</Button>
)}
<Button
icon="bell"
onClick={() => act('request')}
tooltip="Request candidates.">
Request
</Button>
</>
<Button icon="save" onClick={() => act('download', { ckey })}>
Download
</Button>
}
fill
scrollable
title="pAI Candidates">
{!candidates.length ? (
<NoticeBox>None found!</NoticeBox>
) : (
<Stack fill vertical>
{candidates.map((candidate, index) => {
return (
<Stack.Item key={index}>
<CandidateDisplay
candidate={candidate}
index={index + 1}
tabInChar={tabInChar}
/>
</Stack.Item>
);
})}
</Stack>
)}
overflow="hidden"
title={`Candidate ${index}`}>
<Stack vertical>
<Stack.Item>
<Box color="label" mb={1}>
Name:
</Box>
{name ? (
<Box color="green">{name}</Box>
) : (
'None provided - name will be randomized.'
)}
</Stack.Item>
{!!description && (
<>
<Stack.Divider />
<Stack.Item>
<Box color="label" mb={1}>
IC Description:
</Box>
{description}
</Stack.Item>
</>
)}
{!!comments && (
<>
<Stack.Divider />
<Stack.Item>
<Box color="label" mb={1}>
OOC Notes:
</Box>
{comments}
</Stack.Item>
</>
)}
</Stack>
</Section>
);
};
/** 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<PaiCardData>(context);
const { candidate, index, tabInChar } = props;
const { comments, description, key, name } = candidate;
return (
<Box
style={{
'background': '#111111',
'border': '1px solid #4972a1',
'border-radius': '5px',
'padding': '1rem',
}}>
<Section
buttons={
<Button
icon="download"
onClick={() => act('download', { key })}
tooltip="Accepts this pAI candidate.">
Download
</Button>
}
fill
height={12}
scrollable
title={'Candidate ' + index}>
<Box color="green" fontSize="16px">
Name: {name || 'Randomized Name'}
</Box>
{tabInChar
? `Description: ${description || 'None'}`
: `OOC Comments: ${comments || 'None'}`}
</Section>
</Box>
);
};
/** Once a pAI has been loaded, you can alter its settings here */
const PaiOptions = (props, context) => {
const { act, data } = useBackend<PaiCardData>(context);
const { pai } = data;
const { can_holo, dna, emagged, laws, master, name, transmit, receive } = pai;
const { act, data } = useBackend<Data>(context);
const {
pai: { can_holo, dna, emagged, laws, master, name, transmit, receive },
} = data;
const suppliedLaws = laws[0] ? decodeHtmlEntities(laws[0]) : 'None';
return (
<Section fill scrollable title={`Settings: ${name.toUpperCase()}`}>
@@ -147,8 +154,14 @@ const PaiOptions = (props, context) => {
</Button>
)}
</LabeledList.Item>
{!!master && <LabeledList.Item label="DNA">{dna}</LabeledList.Item>}
<LabeledList.Item label="Laws">{laws}</LabeledList.Item>
{!!master && (
<LabeledList.Item color="red" label="DNA">
{dna}
</LabeledList.Item>
)}
<LabeledList.Item label="Laws">
<BlockQuote>{suppliedLaws}</BlockQuote>
</LabeledList.Item>
<LabeledList.Item label="Holoform">
<Button
icon={can_holo ? 'toggle-on' : 'toggle-off'}
@@ -188,7 +201,11 @@ const PaiOptions = (props, context) => {
</LabeledList.Item>
</LabeledList>
{!!emagged && (
<Button color="bad" disabled icon="bug" mt={1}>
<Button
color="bad"
icon="bug"
mt={1}
onClick={() => act('reset_software')}>
Malicious Software Detected
</Button>
)}
@@ -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 (
<Window title="pAI Software Interface v2.4" width={380} height={480}>
<Window.Content>
<Stack fill vertical>
<Stack.Item grow>
{tab === Tab.System && <SystemDisplay />}
{tab === Tab.Directive && <DirectiveDisplay />}
{tab === Tab.Installed && <InstalledDisplay />}
{tab === Tab.Available && <AvailableDisplay />}
</Stack.Item>
<Stack.Item>
<TabDisplay tab={tab} onTabClick={setTabHandler} />
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
/** Tabs at bottom of screen */
const TabDisplay = (props) => {
const { tab, onTabClick } = props;
return (
<Tabs fluid>
<Tabs.Tab
icon="list"
onClick={() => onTabClick(Tab.System)}
selected={tab === Tab.System}>
System
</Tabs.Tab>
<Tabs.Tab
icon="list"
onClick={() => onTabClick(Tab.Directive)}
selected={tab === Tab.Directive}>
Directives
</Tabs.Tab>
<Tabs.Tab
icon="list"
onClick={() => onTabClick(Tab.Installed)}
selected={tab === Tab.Installed}>
Installed
</Tabs.Tab>
<Tabs.Tab
icon="list"
onClick={() => onTabClick(Tab.Available)}
selected={tab === Tab.Available}>
Download
</Tabs.Tab>
</Tabs>
);
};
const SystemDisplay = () => {
return (
<Stack fill vertical>
<Stack.Item grow={3}>
<SystemWallpaper />
</Stack.Item>
<Stack.Item grow>
<SystemInfo />
</Stack.Item>
</Stack>
);
};
/** Renders some ASCII art. Changes to red on emag. */
const SystemWallpaper = (props, context) => {
const { data } = useBackend<PaiInterfaceData>(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 (
<Section fill nowrap overflow="hidden">
<pre>
<Box color={!emagged ? 'blue' : 'crimson'}>{paiAscii}</Box>
<Box color={!emagged ? 'gold' : 'limegreen'}>{floofAscii}</Box>
</pre>
</Section>
);
};
/** Displays master info.
* You can check their DNA and change your image here.
*/
const SystemInfo = (props, context) => {
const { act, data } = useBackend<PaiInterfaceData>(context);
const { image, master } = data;
return (
<Section
buttons={
<>
<Button
disabled={!master.dna}
icon="dna"
onClick={() => act('check_dna')}
tooltip="Verifies your master's DNA. Must be carried in hand.">
Verify
</Button>
<Button
icon={ICON_MAP[image] || 'meh-blank'}
onClick={() => act('change_image')}
tooltip="Change your display image.">
Display
</Button>
</>
}
fill
scrollable
title="System Info">
<LabeledList>
<LabeledList.Item label="Master">
{master.name || 'None.'}
</LabeledList.Item>
<LabeledList.Item label="DNA">{master.dna || 'None.'}</LabeledList.Item>
</LabeledList>
</Section>
);
};
/** Shows the hardcoded PAI info along with any supplied orders. */
const DirectiveDisplay = (props, context) => {
const { data } = useBackend<PaiInterfaceData>(context);
const { directives, master } = data;
return (
<Stack fill vertical>
<Stack.Item grow={2}>
<Section fill scrollable title="Logic Core">
<Box color="label">
{DIRECTIVE_COMPREHENSION}
<br />
<br />
{DIRECTIVE_ORDER}
</Box>
</Section>
</Stack.Item>
<Stack.Item grow>
<Section fill scrollable title="Directives">
{!master.name ? (
'None.'
) : (
<LabeledList>
<LabeledList.Item label="Prime">
Serve your master.
</LabeledList.Item>
<LabeledList.Item label="Supplemental">
<Box wrap>{directives}</Box>
</LabeledList.Item>
</LabeledList>
)}
</Section>
</Stack.Item>
</Stack>
);
};
/** 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 (
<Stack fill vertical>
<Stack.Item grow>
<InstalledSoftware onInstallClick={onInstallHandler} />
</Stack.Item>
<Stack.Item grow={2}>
<InstalledInfo software={installSelected} />
</Stack.Item>
</Stack>
);
};
/** Iterates over installed software to render buttons. */
const InstalledSoftware = (props, context) => {
const { data } = useBackend<PaiInterfaceData>(context);
const { installed = [] } = data;
const { onInstallClick } = props;
return (
<Section fill scrollable title="Installed Software">
{!installed.length ? (
<NoticeBox>Nothing installed!</NoticeBox>
) : (
installed.map((software) => {
return (
<Button key={software} onClick={() => onInstallClick(software)}>
{software.replace(/(^\w{1})|(\s+\w{1})/g, (letter) =>
letter.toUpperCase()
)}
</Button>
);
})
)}
</Section>
);
};
/** Software info for buttons clicked. */
const InstalledInfo = (props) => {
const { software } = props;
/** Records get their own section here */
if (software === 'medical records') {
return <RecordsDisplay record_type="medical" />;
} else if (software === 'security records') {
return <RecordsDisplay record_type="security" />;
} else {
return (
<Section
fill
scrollable
title={
!software
? 'Select a Program'
: software.replace(/(^\w{1})|(\s+\w{1})/g, (letter) =>
letter.toUpperCase()
)
}>
{software && (
<Stack fill vertical>
<Stack.Item>{SOFTWARE_DESC[software] || ''}</Stack.Item>
<Stack.Item grow>
<SoftwareButtons software={software} />
</Stack.Item>
</Stack>
)}
</Section>
);
}
};
/** Todo: Remove this entirely when records get a TGUI interface themselves */
const RecordsDisplay = (props, context) => {
const { act, data } = useBackend<PaiInterfaceData>(context);
const { record_type } = props;
const { records = [], refresh_spam } = data;
const convertedRecords: CrewRecord[] = records[record_type];
return (
<Section
title="Name"
buttons={
<Stack>
<Stack.Item>
<Button
disabled={refresh_spam}
onClick={() => act('refresh', { list: record_type })}
tooltip="Refresh">
<Icon mr={-0.7} name="sync" spin={refresh_spam} />
</Button>
</Stack.Item>
<Stack.Item>
<RecordLabels record_type={record_type} />
</Stack.Item>
</Stack>
}
fill
scrollable>
<Table>
{convertedRecords?.map((record) => {
return <RecordRow key={record.ref} record={record} />;
})}
</Table>
</Section>
);
};
/** Renders the labels for the record viewer */
const RecordLabels = (props) => {
const { record_type } = props;
return (
<Table>
<Table.Row>
<Table.Cell>
{record_type === 'medical' ? 'Physical Health' : 'Arrest Status'}
</Table.Cell>
<Table.Cell>
{record_type === 'medical' ? 'Mental Health' : 'Total Crimes'}
</Table.Cell>
</Table.Row>
</Table>
);
};
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 (
<Table.Row className="candystripe">
{filteredRecord?.map((value) => {
return <Table.Cell key={value}>{value}</Table.Cell>;
})}
</Table.Row>
);
};
/** Once a software is selected, generates custom buttons or a default
* power toggle.
*/
const SoftwareButtons = (props, context) => {
const { act, data } = useBackend<PaiInterfaceData>(context);
const { door_jack, languages, pda } = data;
const { software } = props;
switch (software) {
case 'digital messenger':
return (
<>
<Button
icon="power-off"
onClick={() => act('pda', { pda: 'power' })}
selected={pda.power}>
Power
</Button>
<Button
icon="volume-mute"
onClick={() => act('pda', { pda: 'silent' })}
selected={pda.silent}>
Silent
</Button>
<Button
disabled={!pda.power}
icon="envelope"
onClick={() => act('pda', { pda: 'message' })}>
Message
</Button>
</>
);
case 'door jack':
return (
<>
<Button
disabled={door_jack}
icon="plug"
onClick={() => act('door_jack', { jack: 'cable' })}>
Extend Cable
</Button>
<Button
color="bad"
disabled={!door_jack}
icon="door-open"
onClick={() => act('door_jack', { jack: 'jack' })}>
Hack Door
</Button>
<Button
disabled={!door_jack}
icon="unlink"
onClick={() => act('door_jack', { jack: 'cancel' })}>
Cancel
</Button>
</>
);
case 'host scan': {
return (
<>
<Button
icon="search"
onClick={() => act('host_scan', { scan: 'scan' })}>
Host Scan
</Button>
<Button
icon="cog"
onClick={() => act('host_scan', { scan: 'wounds' })}>
Toggle Wounds
</Button>
<Button
icon="cog"
onClick={() => act('host_scan', { scan: 'limbs' })}>
Toggle Limbs
</Button>
</>
);
}
case 'universal translator':
return (
<Button
icon="download"
onClick={() => act(software.toLowerCase().replace(/ /g, '_'))}
disabled={!!languages}>
{!languages ? 'Install' : 'Installed'}
</Button>
);
default:
return (
<Button
icon="power-off"
onClick={() => act(software.toLowerCase().replace(/ /g, '_'))}
tooltip="Attempts to toggle the module's power.">
Toggle
</Button>
);
}
};
const AvailableDisplay = () => {
return (
<Section
buttons={<AvailableMemory />}
fill
scrollable
title="Available Software">
<AvailableSoftware />
</Section>
);
};
/** Displays the remaining RAM left as a progressbar. */
const AvailableMemory = (props, context) => {
const { data } = useBackend<PaiInterfaceData>(context);
const { ram } = data;
return (
<Tooltip content="Available System Memory">
<Stack>
<Stack.Item>
<Icon color="purple" mt={0.7} name="microchip" />
</Stack.Item>
<Stack.Item>
<ProgressBar
minValue={0}
maxValue={100}
ranges={{
good: [67, 100],
average: [34, 66],
bad: [0, 33],
}}
value={ram}
/>
</Stack.Item>
</Stack>
</Tooltip>
);
};
/** A list of available software.
* creates table rows for each, like a vendor.
*/
const AvailableSoftware = (props, context) => {
const { data } = useBackend<PaiInterfaceData>(context);
const { available } = data;
const convertedList: Available[] = Object.entries(available).map((key) => {
return { name: key[0], value: key[1] };
});
return (
<Table>
{convertedList?.map((software) => {
return <AvailableRow key={software.name} software={software} />;
})}
</Table>
);
};
/** A row for an individual software listing. */
const AvailableRow = (props, context) => {
const { act, data } = useBackend<PaiInterfaceData>(context);
const { ram } = data;
const { installed } = data;
const { software } = props;
const purchased = installed.includes(software.name);
return (
<Table.Row className="candystripe">
<Table.Cell collapsible>
<Box color="label">
{software.name.replace(/^\w/, (c) => c.toUpperCase())}
</Box>
</Table.Cell>
<Table.Cell collapsible>
<Box color={ram < software.value && 'bad'} textAlign="right">
{!purchased && software.value}{' '}
<Icon
color={purchased || ram >= software.value ? 'purple' : 'bad'}
name={purchased ? 'check' : 'microchip'}
/>
</Box>
</Table.Cell>
<Table.Cell collapsible>
<Button
fluid
mb={0.5}
disabled={ram < software.value || purchased}
onClick={() => act('buy', { selection: software.name })}
tooltip={SOFTWARE_DESC[software.name] || ''}>
<Icon ml={1} mr={-2} name="download" />
</Button>
</Table.Cell>
</Table.Row>
);
};
@@ -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 (
<Section
buttons={<MemoryDisplay />}
fill
scrollable
title="Available Software">
<SoftwareList />
</Section>
);
};
/** Displays the remaining RAM left as a progressbar. */
const MemoryDisplay = (props, context) => {
const { data } = useBackend<Data>(context);
const { ram } = data;
return (
<Tooltip content={`Available System Memory: ${ram}`}>
<Table>
<Table.Row>
<Table.Cell>
<Icon color="purple" mt={0.7} name="microchip" />
</Table.Cell>
<Table.Cell>
<ProgressBar
minValue={0}
maxValue={100}
ranges={{
good: [67, 100],
average: [34, 66],
bad: [0, 33],
}}
value={ram}
/>
</Table.Cell>
</Table.Row>
</Table>
</Tooltip>
);
};
/** A list of available software.
* creates table rows for each, like a vendor.
*/
const SoftwareList = (props, context) => {
const { data } = useBackend<Data>(context);
const { available } = data;
if (!available) {
return null;
}
const entries = Object.entries(available);
if (entries.length === 0) {
return null;
}
return (
<Table>
{entries?.map(([name, cost], index) => {
return <ListItem cost={cost} key={index} name={name} />;
})}
</Table>
);
};
/** A row for an individual software listing. */
const ListItem = (props, context) => {
const { act, data } = useBackend<Data>(context);
const { installed, ram } = data;
const { cost, name } = props;
const purchased = installed.includes(name);
return (
<Table.Row className="candystripe">
<Table.Cell collapsible>
<Box color="label">{name}</Box>
</Table.Cell>
<Table.Cell collapsible>
<Box color={ram < cost && 'bad'} textAlign="right">
{!purchased && cost}{' '}
<Icon
color={purchased || ram >= cost ? 'purple' : 'bad'}
name={purchased ? 'check' : 'microchip'}
/>
</Box>
</Table.Cell>
<Table.Cell collapsible>
<Button
fluid
mb={0.5}
disabled={ram < cost || purchased}
onClick={() => act('buy', { selection: name })}
tooltip={SOFTWARE_DESC[name]}
tooltipPosition="bottom-start">
<Icon ml={1} mr={-2} name="download" />
</Button>
</Table.Cell>
</Table.Row>
);
};
@@ -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<Data>(context);
const { directives = [], master_name } = data;
const displayedLaw = directives?.length
? decodeHtmlEntities(directives[0])
: 'None.';
return (
<Stack fill vertical>
<Stack.Item grow={2}>
<Section fill scrollable title="Logic Core">
<Box color="label">
{DIRECTIVE_COMPREHENSION}
<br />
<br />
{DIRECTIVE_ORDER}
</Box>
</Section>
</Stack.Item>
<Stack.Item grow>
<Section fill scrollable title="Directives">
{!master_name ? (
'None.'
) : (
<LabeledList>
<LabeledList.Item label="Prime">
Serve your master.
</LabeledList.Item>
<LabeledList.Item label="Supplemental">
<BlockQuote>{displayedLaw}</BlockQuote>
</LabeledList.Item>
</LabeledList>
)}
</Section>
</Stack.Item>
</Stack>
);
};
@@ -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 (
<Stack fill vertical>
<Stack.Item grow>
<InstalledSoftware />
</Stack.Item>
<Stack.Item grow={2}>
<InstalledInfo />
</Stack.Item>
</Stack>
);
};
/** Iterates over installed software to render buttons. */
const InstalledSoftware = (props, context) => {
const { data } = useBackend<Data>(context);
const { installed = [] } = data;
const [currentSelection, setCurrentSelection] = useLocalState(
context,
'software',
''
);
return (
<Section fill scrollable title="Installed Software">
{!installed.length ? (
<NoticeBox>Nothing installed!</NoticeBox>
) : (
installed.map((software, index) => {
return (
<Button key={index} onClick={() => setCurrentSelection(software)}>
{software}
</Button>
);
})
)}
</Section>
);
};
/** Software info for buttons clicked. */
const InstalledInfo = (props, context) => {
const [currentSelection, setCurrentSelection] = useLocalState(
context,
'software',
''
);
const title = !currentSelection ? 'Select a Program' : currentSelection;
return (
<Section fill scrollable title={title}>
{currentSelection && (
<Stack fill vertical>
<Stack.Item>{SOFTWARE_DESC[currentSelection]}</Stack.Item>
<Stack.Item grow>
<SoftwareButtons />
</Stack.Item>
</Stack>
)}
</Section>
);
};
/**
* Once a software is selected, generates custom buttons or a default
* power toggle.
*/
const SoftwareButtons = (props, context) => {
const { act, data } = useBackend<Data>(context);
const { door_jack, languages, master_name } = data;
const [currentSelection, setCurrentSelection] = useLocalState(
context,
'software',
''
);
switch (currentSelection) {
case 'Door Jack':
return (
<>
<Button
disabled={door_jack}
icon="plug"
onClick={() => act(currentSelection, { mode: DOOR_JACK.Cable })}
tooltip="Drops a cable. Insert into a compatible airlock.">
Extend Cable
</Button>
<Button
color="bad"
disabled={!door_jack}
icon="door-open"
onClick={() => act(currentSelection, { mode: DOOR_JACK.Hack })}
tooltip="Begins overriding the airlock security protocols.">
Hack Door
</Button>
<Button
disabled={!door_jack}
icon="unlink"
onClick={() => act(currentSelection, { mode: DOOR_JACK.Cancel })}>
Cancel
</Button>
</>
);
case 'Host Scan':
return (
<>
<Button
icon="hand-holding-heart"
onClick={() => act(currentSelection, { mode: HOST_SCAN.Target })}
tooltip="Must be held or scooped up to scan.">
Scan Holder
</Button>
<Button
disabled={!master_name}
icon="user-cog"
onClick={() => act(currentSelection, { mode: HOST_SCAN.Master })}
tooltip="Scans any bound masters.">
Scan Master
</Button>
</>
);
case 'Photography Module':
return (
<>
<Button
icon="camera-retro"
onClick={() => act(currentSelection, { mode: PHOTO_MODE.Camera })}
tooltip="Toggles the camera. Click an area to take a photo.">
Camera
</Button>
<Button
icon="print"
onClick={() => act(currentSelection, { mode: PHOTO_MODE.Printer })}
tooltip="Gives a list of stored photos.">
Printer
</Button>
<Button
icon="search-plus"
onClick={() => act(currentSelection, { mode: PHOTO_MODE.Zoom })}
tooltip="Adjusts zoom level on future photographs.">
Zoom
</Button>
</>
);
case 'Universal Translator':
return (
<Button
icon="download"
onClick={() => act(currentSelection)}
disabled={!!languages}>
{!languages ? 'Install' : 'Installed'}
</Button>
);
default:
return (
<Button
icon="power-off"
onClick={() => act(currentSelection)}
tooltip="Attempts to enable the module.">
Toggle
</Button>
);
}
};
@@ -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 (
<Stack fill vertical>
<Stack.Item grow={3}>
<SystemWallpaper />
</Stack.Item>
<Stack.Item grow>
<SystemInfo />
</Stack.Item>
</Stack>
);
};
/** Renders some ASCII art. Changes to red on emag. */
const SystemWallpaper = (props, context) => {
const { data } = useBackend<Data>(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 (
<Section fill nowrap overflow="hidden">
<pre>
<Box color={!emagged ? 'blue' : 'crimson'}>{paiAscii}</Box>
<Box color={!emagged ? 'gold' : 'limegreen'}>{floofAscii}</Box>
</pre>
</Section>
);
};
/** Displays master info.
* You can check their DNA and change your image here.
*/
const SystemInfo = (props, context) => {
const { act, data } = useBackend<Data>(context);
const { image, master_dna, master_name } = data;
return (
<Section
buttons={
<>
<Button
disabled={!master_dna}
icon="dna"
onClick={() => act('check dna')}
tooltip="Verifies your master's DNA. Must be carried in hand.">
Verify
</Button>
<Button
icon={ICON_MAP[image]}
onClick={() => act('change image')}
tooltip="Change your display image.">
Display
</Button>
</>
}
fill
title="System Info">
<LabeledList>
<LabeledList.Item label="Master">
{master_name || 'None.'}
</LabeledList.Item>
<LabeledList.Item color={master_dna && 'red'} label="DNA">
{master_dna || 'None.'}
</LabeledList.Item>
</LabeledList>
</Section>
);
};
@@ -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,
}
@@ -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 (
<Window title="pAI Software Interface v2.5" width={380} height={480}>
<Window.Content>
<Stack fill vertical>
<Stack.Item grow>
{tab === TAB.System && <SystemDisplay />}
{tab === TAB.Directive && <DirectiveDisplay />}
{tab === TAB.Installed && <InstalledDisplay />}
{tab === TAB.Available && <AvailableDisplay />}
</Stack.Item>
<Stack.Item>
<TabDisplay />
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
/**
* 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 (
<Tabs fluid>
<Tabs.Tab
icon="list"
onClick={() => setTab(TAB.System)}
selected={tab === TAB.System}>
System
</Tabs.Tab>
<Tabs.Tab
icon="list"
onClick={() => setTab(TAB.Directive)}
selected={tab === TAB.Directive}>
Directives
</Tabs.Tab>
<Tabs.Tab
icon="list"
onClick={() => setTab(TAB.Installed)}
selected={tab === TAB.Installed}>
Installed
</Tabs.Tab>
<Tabs.Tab
icon="list"
onClick={() => setTab(TAB.Available)}
selected={tab === TAB.Available}>
Download
</Tabs.Tab>
</Tabs>
);
};
@@ -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<string>;
languages: BooleanLike;
master_dna: string | null;
master_name: string | null;
ram: number;
};
+55 -57
View File
@@ -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<CandidateData>(context);
const [input, setInput] = useLocalState<CandidateData>(context, 'input', {
comments: data.comments || '',
description: data.description || '',
name: data.name || '',
const { data } = useBackend<Data>(context);
const { comments, description, name } = data;
const [input, setInput] = useLocalState<Data>(context, 'input', {
comments,
description,
name,
});
const onChangeHandler = (e, value) => {
setInput({ ...input, [value]: e.target.value });
};
logger.log(input);
return (
<Window width={400} height={460} title="pAI Candidacy Menu">
@@ -38,7 +36,7 @@ export const PaiSubmit = (props, context) => {
<DetailsDisplay />
</Stack.Item>
<Stack.Item>
<InputDisplay input={input} onChangeHandler={onChangeHandler} />
<InputDisplay input={input} setInput={setInput} />
</Stack.Item>
<Stack.Item>
<ButtonsDisplay input={input} />
@@ -50,7 +48,7 @@ export const PaiSubmit = (props, context) => {
};
/** Displays basic info about playing pAI */
const DetailsDisplay = () => {
const DetailsDisplay = (props, context) => {
return (
<Section fill scrollable title="Details">
<Box color="label">
@@ -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 (
<Section fill title="Input">
<Stack fill vertical>
<Stack.Item>
<Tooltip content="The name of your pAI.">
<Box bold color="label">
Name
</Box>
<Input
fluid
value={name}
onChange={(e) => onChangeHandler(e, 'name')}
/>
</Tooltip>
<Box bold color="label">
Name
</Box>
<Input
fluid
maxLength={41}
value={name}
onChange={(e, value) => setInput({ ...input, name: value })}
/>
</Stack.Item>
<Stack.Item>
<Tooltip content="This describes how you will (mis)behave in game.">
<Box bold color="label">
Description
</Box>
<Input
fluid
value={description}
onChange={(e) => onChangeHandler(e, 'description')}
/>
</Tooltip>
<Box bold color="label">
Description
</Box>
<Input
fluid
maxLength={100}
value={description}
onChange={(e, value) => setInput({ ...input, description: value })}
/>
</Stack.Item>
<Stack.Item>
<Tooltip content="Any other OOC comments about your pAI personality.">
<Box bold color="label">
OOC Comments
</Box>
<Input
fluid
value={comments}
onChange={(e) => onChangeHandler(e, 'comments')}
/>
</Tooltip>
<Box bold color="label">
OOC Comments
</Box>
<Input
fluid
maxLength={100}
value={comments}
onChange={(e, value) => setInput({ ...input, comments: value })}
/>
</Stack.Item>
</Stack>
</Section>
@@ -114,15 +109,16 @@ const InputDisplay = (props) => {
/** Gives the user a submit button */
const ButtonsDisplay = (props, context) => {
const { act } = useBackend<CandidateData>(context);
const { act } = useBackend<Data>(context);
const { input } = props;
const { comments, description, name } = input;
return (
<Section fill>
<Stack>
<Stack.Item>
<Button
onClick={() => act('save', { candidate: input })}
onClick={() => act('save', { comments, description, name })}
tooltip="Saves your candidate data locally.">
SAVE
</Button>
@@ -138,7 +134,9 @@ const ButtonsDisplay = (props, context) => {
<Button
onClick={() =>
act('submit', {
candidate: input,
comments,
description,
name,
})
}>
SUBMIT
@@ -0,0 +1,3 @@
# comment finally changed paicard to pai_card
/obj/item/paicard : /obj/item/pai_card{@OLD}