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
+58
View File
@@ -0,0 +1,58 @@
/datum/action/innate/pai
name = "PAI Action"
icon_icon = 'icons/mob/actions/actions_silicon.dmi'
var/mob/living/silicon/pai/pai_owner
/datum/action/innate/pai/Trigger(trigger_flags)
if(!ispAI(owner))
return FALSE
pai_owner = owner
/datum/action/innate/pai/software
name = "Software Interface"
button_icon_state = "pai"
background_icon_state = "bg_tech"
/datum/action/innate/pai/software/Trigger(trigger_flags)
..()
pai_owner.ui_act()
/datum/action/innate/pai/shell
name = "Toggle Holoform"
button_icon_state = "pai_holoform"
background_icon_state = "bg_tech"
/datum/action/innate/pai/shell/Trigger(trigger_flags)
..()
if(pai_owner.holoform)
pai_owner.fold_in(0)
else
pai_owner.fold_out()
/datum/action/innate/pai/chassis
name = "Holochassis Appearance Composite"
button_icon_state = "pai_chassis"
background_icon_state = "bg_tech"
/datum/action/innate/pai/chassis/Trigger(trigger_flags)
..()
pai_owner.choose_chassis()
/datum/action/innate/pai/rest
name = "Rest"
button_icon_state = "pai_rest"
background_icon_state = "bg_tech"
/datum/action/innate/pai/rest/Trigger(trigger_flags)
..()
pai_owner.toggle_resting()
/datum/action/innate/pai/light
name = "Toggle Integrated Lights"
icon_icon = 'icons/mob/actions/actions_spells.dmi'
button_icon_state = "emp"
background_icon_state = "bg_tech"
/datum/action/innate/pai/light/Trigger(trigger_flags)
..()
pai_owner.toggle_integrated_light()
+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)
+91
View File
@@ -0,0 +1,91 @@
/mob/living/silicon/pai/blob_act(obj/structure/blob/B)
return FALSE
/mob/living/silicon/pai/emp_act(severity)
. = ..()
if(. & EMP_PROTECT_SELF)
return
take_holo_damage(50 / severity)
Stun(400 / severity)
if(holoform)
fold_in(force = TRUE)
//Need more effects that aren't instadeath or permanent law corruption.
//Ask and you shall receive
switch(rand(1, 3))
if(1)
adjust_timed_status_effect(1 MINUTES / severity, /datum/status_effect/speech/stutter)
to_chat(src, span_danger("Warning: Feedback loop detected in speech module."))
if(2)
adjust_timed_status_effect(INFINITY, /datum/status_effect/speech/slurring/drunk)
to_chat(src, span_danger("Warning: Audio synthesizer CPU stuck."))
if(3)
adjust_timed_status_effect(INFINITY, /datum/status_effect/speech/stutter/derpspeech)
to_chat(src, span_danger("Warning: Vocabulary databank corrupted."))
if(prob(40))
mind.language_holder.selected_language = get_random_spoken_language()
/mob/living/silicon/pai/ex_act(severity, target)
take_holo_damage(50 * severity)
switch(severity)
if(EXPLODE_DEVASTATE) //RIP
qdel(card)
qdel(src)
if(EXPLODE_HEAVY)
fold_in(force = 1)
Paralyze(400)
if(EXPLODE_LIGHT)
fold_in(force = 1)
Paralyze(200)
/mob/living/silicon/pai/attack_hand(mob/living/carbon/human/user, list/modifiers)
if(!user.combat_mode)
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_name)
visible_message(span_danger("[user] stomps on [src]!."))
take_holo_damage(2)
return
visible_message(span_notice("Responding to its master's touch, [src] disengages its holochassis emitter, rapidly losing coherence."))
if(!do_after(user, 1 SECONDS, TRUE, src))
return
fold_in()
if(user.put_in_hands(card))
user.visible_message(span_notice("[user] promptly scoops up [user.p_their()] pAI's card."))
/mob/living/silicon/pai/bullet_act(obj/projectile/Proj)
if(Proj.stun)
fold_in(force = TRUE)
src.visible_message(span_warning("The electrically-charged projectile disrupts [src]'s holomatrix, forcing [src] to fold in!"))
. = ..(Proj)
/mob/living/silicon/pai/ignite_mob()
return FALSE
/mob/living/silicon/pai/proc/take_holo_damage(amount)
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!"))
return amount
/mob/living/silicon/pai/adjustBruteLoss(amount, updating_health = TRUE, forced = FALSE)
return take_holo_damage(amount)
/mob/living/silicon/pai/adjustFireLoss(amount, updating_health = TRUE, forced = FALSE)
return take_holo_damage(amount)
/mob/living/silicon/pai/adjustStaminaLoss(amount, updating_health, forced = FALSE)
if(forced)
take_holo_damage(amount)
else
take_holo_damage(amount * 0.25)
/mob/living/silicon/pai/getBruteLoss()
return HOLOCHASSIS_MAX_HEALTH - holochassis_health
/mob/living/silicon/pai/getFireLoss()
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
+273
View File
@@ -0,0 +1,273 @@
#define PAI_MISSING_SOFTWARE_MESSAGE span_warning("You must download the required software to use this.")
/atom/movable/screen/pai
icon = 'icons/hud/screen_pai.dmi'
var/required_software
/atom/movable/screen/pai/Click()
if(isobserver(usr) || usr.incapacitated())
return FALSE
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
/atom/movable/screen/pai/software
name = "Software Interface"
icon_state = "pai"
/atom/movable/screen/pai/software/Click()
if(!..())
return
var/mob/living/silicon/pai/pAI = usr
pAI.ui_interact(pAI)
/atom/movable/screen/pai/shell
name = "Toggle Holoform"
icon_state = "pai_holoform"
/atom/movable/screen/pai/shell/Click()
if(!..())
return
var/mob/living/silicon/pai/pAI = usr
if(pAI.holoform)
pAI.fold_in(0)
else
pAI.fold_out()
/atom/movable/screen/pai/chassis
name = "Holochassis Appearance Composite"
icon_state = "pai_chassis"
/atom/movable/screen/pai/chassis/Click()
if(!..())
return
var/mob/living/silicon/pai/pAI = usr
pAI.choose_chassis()
/atom/movable/screen/pai/rest
name = "Rest"
icon_state = "pai_rest"
/atom/movable/screen/pai/rest/Click()
if(!..())
return
var/mob/living/silicon/pai/pAI = usr
pAI.toggle_resting()
/atom/movable/screen/pai/light
name = "Toggle Integrated Lights"
icon_state = "light"
/atom/movable/screen/pai/light/Click()
if(!..())
return
var/mob/living/silicon/pai/pAI = usr
pAI.toggle_integrated_light()
/atom/movable/screen/pai/newscaster
name = "pAI Newscaster"
icon_state = "newscaster"
required_software = "Newscaster"
/atom/movable/screen/pai/newscaster/Click()
if(!..())
return
var/mob/living/silicon/pai/pAI = usr
pAI.newscaster.ui_interact(usr)
/atom/movable/screen/pai/host_monitor
name = "Host Health Scan"
icon_state = "host_monitor"
required_software = "Host Scan"
/atom/movable/screen/pai/host_monitor/Click(location, control, params)
. = ..()
if(!.)
return
var/mob/living/silicon/pai/pAI = usr
var/list/modifiers = params2list(params)
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"
required_software = "crew manifest"
/atom/movable/screen/pai/crew_manifest/Click()
if(!..())
return
var/mob/living/silicon/pai/pAI = usr
pAI.ai_roster()
/atom/movable/screen/pai/state_laws
name = "State Laws"
icon_state = "state_laws"
/atom/movable/screen/pai/state_laws/Click()
if(!..())
return
var/mob/living/silicon/pai/pAI = usr
pAI.checklaws()
/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()
. = ..()
if(!.) // this works for some reason.
return
pAI.modularInterface?.interact(pAI)
/atom/movable/screen/pai/internal_gps
name = "Internal GPS"
icon_state = "internal_gps"
required_software = "Internal GPS"
/atom/movable/screen/pai/internal_gps/Click()
. = ..()
if(!.)
return
var/mob/living/silicon/pai/pAI = usr
if(!pAI.internal_gps)
pAI.internal_gps = new(pAI)
pAI.internal_gps.attack_self(pAI)
/atom/movable/screen/pai/image_take
name = "Take Image"
icon_state = "take_picture"
required_software = "Photography Module"
/atom/movable/screen/pai/image_take/Click()
if(!..())
return
var/mob/living/silicon/pai/pAI = usr
pAI.camera.toggle_camera_mode(usr)
/atom/movable/screen/pai/image_view
name = "View Images"
icon_state = "view_images"
required_software = "Photography Module"
/atom/movable/screen/pai/image_view/Click()
if(!..())
return
var/mob/living/silicon/pai/pAI = usr
pAI.camera.viewpictures(usr)
/atom/movable/screen/pai/radio
name = "radio"
icon = 'icons/hud/screen_cyborg.dmi'
icon_state = "radio"
/atom/movable/screen/pai/radio/Click()
if(!..())
return
var/mob/living/silicon/pai/pAI = usr
pAI.radio.interact(usr)
/datum/hud/pai/New(mob/living/silicon/pai/owner)
..()
var/atom/movable/screen/using
var/mob/living/silicon/pai/mypai = mymob
// Software menu
using = new /atom/movable/screen/pai/software
using.screen_loc = ui_pai_software
static_inventory += using
// Holoform
using = new /atom/movable/screen/pai/shell
using.screen_loc = ui_pai_shell
static_inventory += using
// Chassis Select Menu
using = new /atom/movable/screen/pai/chassis
using.screen_loc = ui_pai_chassis
static_inventory += using
// Rest
using = new /atom/movable/screen/pai/rest
using.screen_loc = ui_pai_rest
static_inventory += using
// Integrated Light
using = new /atom/movable/screen/pai/light
using.screen_loc = ui_pai_light
static_inventory += using
// Newscaster
using = new /atom/movable/screen/pai/newscaster
using.screen_loc = ui_pai_newscaster
static_inventory += using
// Language menu
using = new /atom/movable/screen/language_menu
using.screen_loc = ui_pai_language_menu
static_inventory += using
// Navigation
using = new /atom/movable/screen/navigate
using.screen_loc = ui_pai_navigate_menu
static_inventory += using
// Host Monitor
using = new /atom/movable/screen/pai/host_monitor()
using.screen_loc = ui_pai_host_monitor
static_inventory += using
// Crew Manifest
using = new /atom/movable/screen/pai/crew_manifest()
using.screen_loc = ui_pai_crew_manifest
static_inventory += using
// Laws
using = new /atom/movable/screen/pai/state_laws()
using.screen_loc = ui_pai_state_laws
static_inventory += using
// Modular Interface
using = new /atom/movable/screen/pai/modpc()
using.screen_loc = ui_pai_mod_int
static_inventory += using
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()
using.screen_loc = ui_pai_internal_gps
static_inventory += using
// Take image
using = new /atom/movable/screen/pai/image_take()
using.screen_loc = ui_pai_take_picture
static_inventory += using
// View images
using = new /atom/movable/screen/pai/image_view()
using.screen_loc = ui_pai_view_images
static_inventory += using
// Radio
using = new /atom/movable/screen/pai/radio()
using.screen_loc = ui_pai_radio
static_inventory += using
update_software_buttons()
/datum/hud/pai/proc/update_software_buttons()
var/mob/living/silicon/pai/owner = mymob
for(var/atom/movable/screen/pai/button in static_inventory)
if(button.required_software)
button.color = owner.installed_software.Find(button.required_software) ? null : "#808080"
#undef PAI_MISSING_SOFTWARE_MESSAGE
+10
View File
@@ -0,0 +1,10 @@
/mob/living/silicon/pai/Login()
. = ..()
if(!. || !client)
return FALSE
client.perspective = EYE_PERSPECTIVE
if(holoform)
client.eye = src
else
client.eye = card
+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
+56
View File
@@ -0,0 +1,56 @@
/**
* name
* key
* description
* role
* comments
* ready = TRUE
*/
/datum/pai_candidate/proc/savefile_path(mob/user)
return "data/player_saves/[user.ckey[1]]/[user.ckey]/pai.sav"
/datum/pai_candidate/proc/savefile_save(mob/user)
if(is_guest_key(user.key))
to_chat(usr, span_warning("You cannot save pAI information as a guest."))
return FALSE
var/savefile/F = new /savefile(src.savefile_path(user))
WRITE_FILE(F["name"], name)
WRITE_FILE(F["description"], description)
WRITE_FILE(F["comments"], comments)
WRITE_FILE(F["version"], 1)
to_chat(usr, span_boldnotice("You have saved pAI information locally."))
return TRUE
// loads the savefile corresponding to the mob's ckey
// if silent=true, report incompatible savefiles
// returns TRUE if loaded (or file was incompatible)
// returns FALSE if savefile did not exist
/datum/pai_candidate/proc/savefile_load(mob/user, silent = TRUE)
if (is_guest_key(user.key))
return FALSE
var/path = savefile_path(user)
if (!fexists(path))
return FALSE
var/savefile/F = new /savefile(path)
if(!F)
return //Not everyone has a pai savefile.
var/version = null
F["version"] >> version
if (isnull(version) || version != 1)
fdel(path)
if (!silent)
tgui_alert(user, "Your savefile was incompatible with this version and was deleted.")
return FALSE
F["name"] >> src.name
F["description"] >> src.description
F["comments"] >> src.comments
return TRUE
+2
View File
@@ -0,0 +1,2 @@
/mob/living/silicon/pai/binarycheck()
return radio?.translate_binary
+174
View File
@@ -0,0 +1,174 @@
/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 ..()
/mob/living/silicon/pai/start_pulling(atom/movable/thing, state, force = move_force, supress_message = FALSE)
return FALSE
/mob/living/silicon/pai/update_resting()
. = ..()
if(resting)
icon_state = "[chassis]_rest"
else
icon_state = "[chassis]"
if(loc != card)
visible_message(span_notice("[src] [resting? "lays down for a moment..." : "perks up from the ground."]"))
/mob/living/silicon/pai/wabbajack()
if(length(possible_chassis) < 2)
return FALSE
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