Paicontroller subsystem port (#19165)

* to subsystem

* wip subsystem pain

* some big recoding

* seperated

* oop

* fucking lua

* working on tgui foundation

* compile fixes

* nice and clean

* type

* inviting works

* unit testing

* agony

* test good

* typo

* that should be the basics

* Complete recode of pai saving

* restore fluff text

* migration

* pai notification automation

* swap

* length

* more fixes

* pai sprite datums

* type 13 uses all four directions

* eye handling

* use accessors

* working on icontests

* fix

* hologram eye test

* oop

* wait that doesn't work at all

* text

* eye colors and holo eyes

* lots of fixes

* fix those up

* organized

* belly cap

* that too

* death sprites

* resting eyes

* resting flag

* bad check here

* fixing icon files

* proper file

* garble

* fix some states

* pai death

* collapse pai files into single file

* allow some restricted pai interactions

* lamp

* block invites button

* collapse in remaining vr file

* this is actually needed

* documenting and organizing

* update to TGprefs

* remove gun toggle verb on pai

* forbid default name joins

* pai cable retraction

* various pai fixes

* icon forge pai

* pai eyes

* type 13 iconstate

* directionless

* small fix

* pai dir fix

* missing icon

* no fresh pais

* oops

* fixed pai join

* fixed change chassis

* pai UI

* .

* .

* reuse

* .

* .

* .

* push that

* Horro rmasks

* pai invite data change

* splitting

* use refs

* initial

* with PAI

* soem more

* .

* .

* .

* .

* ugh

* next

* pai invite

* only preview

* that

* use from char pref

* fixed pai invite not working

* small tgui fixes

* remove temp code

* .

* pai comments properly sent

* ooc

* horror

* need to use this for now

* .

* fix

* lol

* raw mob spawn fix

* pai software

* push

---------

Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com>
This commit is contained in:
Will
2026-03-03 23:59:27 +01:00
committed by GitHub
co-authored by Kashargul
parent f5240f6c38
commit 7c84aaee4b
95 changed files with 4051 additions and 2442 deletions
+1 -5
View File
@@ -150,13 +150,9 @@
pai.key = choice.key
card.setPersonality(pai)
if(tgui_alert(pai, "Do you want to load your pAI data?", "Load", list("Yes", "No")) == "Yes")
pai.savefile_load(pai)
pai.apply_preferences(pai.client)
else
pai.name = sanitizeSafe(tgui_input_text(pai, "Enter your pAI name:", "pAI Name", "Personal AI", encode = FALSE))
card.setPersonality(pai)
for(var/datum/paiCandidate/candidate in paiController.pai_candidates)
if(candidate.key == choice.key)
paiController.pai_candidates.Remove(candidate)
log_admin("made a pAI with key=[pai.key] at ([T.x],[T.y],[T.z])")
feedback_add_details("admin_verb","MPAI") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
@@ -0,0 +1,64 @@
/datum/asset/spritesheet_batched/pai_icons
name = "pai_icons"
/datum/asset/spritesheet_batched/pai_icons/create_spritesheets()
for(var/name, current_sprite in SSpai.get_chassis_list())
var/datum/pai_sprite/sprite = current_sprite
if(!sprite.name || !sprite.sprite_icon_state)
continue
var/datum/universal_icon/I_N = uni_icon(sprite.sprite_icon, sprite.sprite_icon_state, NORTH)
var/datum/universal_icon/I_S = uni_icon(sprite.sprite_icon, sprite.sprite_icon_state, SOUTH)
var/datum/universal_icon/I_W = uni_icon(sprite.sprite_icon, sprite.sprite_icon_state, WEST)
var/datum/universal_icon/I_E = uni_icon(sprite.sprite_icon, sprite.sprite_icon_state, EAST)
if(sprite.icon_x > sprite.icon_y)
var/buffer = (sprite.icon_x - sprite.icon_y) / 2
I_N.crop(0, -buffer, sprite.icon_x, sprite.icon_y + buffer)
I_S.crop(0, -buffer, sprite.icon_x, sprite.icon_y + buffer)
I_W.crop(0, -buffer, sprite.icon_x, sprite.icon_y + buffer)
I_E.crop(0, -buffer, sprite.icon_x, sprite.icon_y + buffer)
else if (sprite.icon_x < sprite.icon_y)
var/buffer = (sprite.icon_y - sprite.icon_x) / 2
I_N.crop(-buffer, 0, sprite.icon_x + buffer, sprite.icon_y)
I_S.crop(-buffer, 0, sprite.icon_x + buffer, sprite.icon_y)
I_W.crop(-buffer, 0, sprite.icon_x + buffer, sprite.icon_y)
I_E.crop(-buffer, 0, sprite.icon_x + buffer, sprite.icon_y)
I_N.scale(120, 120)
I_S.scale(120, 120)
I_W.scale(120, 120)
I_E.scale(120, 120)
var/imgid = sanitize_css_class_name("[sprite.type]")
insert_icon(imgid + "N", I_N)
insert_icon(imgid + "S", I_S)
insert_icon(imgid + "W", I_W)
insert_icon(imgid + "E", I_E)
if(sprite.has_eye_sprites)
var/datum/universal_icon/I_NE = uni_icon(sprite.sprite_icon, "[sprite.sprite_icon_state]-eyes", NORTH)
var/datum/universal_icon/I_SE = uni_icon(sprite.sprite_icon, "[sprite.sprite_icon_state]-eyes", SOUTH)
var/datum/universal_icon/I_WE = uni_icon(sprite.sprite_icon, "[sprite.sprite_icon_state]-eyes", WEST)
var/datum/universal_icon/I_EE = uni_icon(sprite.sprite_icon, "[sprite.sprite_icon_state]-eyes", EAST)
if(sprite.icon_x > sprite.icon_y)
var/buffer = (sprite.icon_x - sprite.icon_y) / 2
I_NE.crop(0, -buffer, sprite.icon_x, sprite.icon_y + buffer)
I_SE.crop(0, -buffer, sprite.icon_x, sprite.icon_y + buffer)
I_WE.crop(0, -buffer, sprite.icon_x, sprite.icon_y + buffer)
I_EE.crop(0, -buffer, sprite.icon_x, sprite.icon_y + buffer)
else if (sprite.icon_x < sprite.icon_y)
var/buffer = (sprite.icon_y - sprite.icon_x) / 2
I_NE.crop(-buffer, 0, sprite.icon_x + buffer, sprite.icon_y)
I_SE.crop(-buffer, 0, sprite.icon_x + buffer, sprite.icon_y)
I_WE.crop(-buffer, 0, sprite.icon_x + buffer, sprite.icon_y)
I_EE.crop(-buffer, 0, sprite.icon_x + buffer, sprite.icon_y)
I_NE.scale(120, 120)
I_SE.scale(120, 120)
I_WE.scale(120, 120)
I_EE.scale(120, 120)
insert_icon(imgid + "NE", I_NE)
insert_icon(imgid + "SE", I_SE)
insert_icon(imgid + "WE", I_WE)
insert_icon(imgid + "EE", I_EE)
@@ -103,20 +103,19 @@ GLOBAL_LIST_EMPTY(robot_sprite_sheets)
var/datum/universal_icon/I_NE = uni_icon(S.sprite_icon, "[S.sprite_icon_state]-eyes", NORTH)
if(I_NE)
I_N.blend_icon(I_NE, ICON_OVERLAY)
if(S.has_eye_sprites)
var/datum/universal_icon/I_SE = uni_icon(S.sprite_icon, "[S.sprite_icon_state]-eyes", SOUTH)
if(I_SE)
I_S.blend_icon(I_SE, ICON_OVERLAY)
if(S.has_eye_sprites)
var/datum/universal_icon/I_WE = uni_icon(S.sprite_icon, "[S.sprite_icon_state]-eyes", WEST)
if(I_WE)
I_W.blend_icon(I_WE, ICON_OVERLAY)
if(S.has_eye_sprites)
var/datum/universal_icon/I_EE = uni_icon(S.sprite_icon, "[S.sprite_icon_state]-eyes", EAST)
if(I_EE)
I_E.blend_icon(I_EE, ICON_OVERLAY)
var/imgid = sanitize_css_class_name("[S.type]")
if(S.icon_x > S.icon_y)
var/buffer = (S.icon_x - S.icon_y) / 2
I_N.crop(0, -buffer, S.icon_x, S.icon_y + buffer)
@@ -134,6 +133,7 @@ GLOBAL_LIST_EMPTY(robot_sprite_sheets)
I_W.scale(120, 120)
I_E.scale(120, 120)
var/imgid = sanitize_css_class_name("[S.type]")
insert_icon(imgid + "N", I_N)
insert_icon(imgid + "S", I_S)
insert_icon(imgid + "W", I_W)
@@ -2,12 +2,6 @@
name = "Basic"
sort_order = 7
var/datum/paiCandidate/candidate
/datum/category_item/player_setup_item/general/basic_antagonism/New()
. = ..()
candidate = new()
/datum/category_item/player_setup_item/general/basic_antagonism/load_character(list/save_data)
pref.exploit_record = save_data["exploit_record"]
pref.antag_faction = save_data["antag_faction"]
@@ -19,29 +13,18 @@
save_data["antag_vis"] = pref.antag_vis
/datum/category_item/player_setup_item/general/basic_antagonism/load_preferences(datum/json_savefile/savefile)
if(!candidate)
candidate = new()
var/preference_mob = preference_mob()
if(!preference_mob)// No preference mob - this happens when we're called from client/New() before it calls ..() (via datum/preferences/New())
spawn()
preference_mob = preference_mob()
if(!preference_mob)
return
candidate.savefile_load(preference_mob)
return
candidate.savefile_load(preference_mob)
/datum/category_item/player_setup_item/general/basic_antagonism/save_preferences(datum/json_savefile/savefile)
if(!candidate)
return
if(!preference_mob())
return
candidate.savefile_save(preference_mob())
/datum/category_item/player_setup_item/general/basic_antagonism/sanitize_character()
if(!pref.antag_faction) pref.antag_faction = "None"
if(!pref.antag_vis) pref.antag_vis = "Hidden"
@@ -62,13 +45,23 @@
if(!jobban_isbanned(user, "Records"))
data["exploitable_record"] = TextPreview(pref.exploit_record, 40)
if(!candidate)
CRASH("[user] pAI prefs have a null candidate var.")
data["pai_name"] = pref.read_preference(/datum/preference/text/pai_name)
data["pai_desc"] = pref.read_preference(/datum/preference/text/pai_description)
data["pai_role"] = pref.read_preference(/datum/preference/text/pai_role)
data["pai_ad"] = pref.read_preference(/datum/preference/text/pai_ad)
data["pai_comments"] = pref.read_preference(/datum/preference/text/pai_comments)
data["pai_eyecolor"] = pref.read_preference(/datum/preference/color/pai_eye_color)
var/pai_chassis = pref.read_preference(/datum/preference/text/pai_chassis)
data["pai_chassis"] = pai_chassis
data["pai_chassises"] = SSpai.get_chassis_list()
data["pai_emotion"] = pref.read_preference(/datum/preference/text/pai_emotion)
data["pai_emotions"] = GLOB.pai_emotions
data["pai_name"] = candidate.name ? candidate.name : "None Set"
data["pai_desc"] = candidate.description ? TextPreview(candidate.description, 40) : "None Set"
data["pai_role"] = candidate.role ? TextPreview(candidate.role, 40) : "None Set"
data["pai_comments"] = candidate.comments ? TextPreview(candidate.comments, 40) : "None Set"
var/datum/pai_sprite/sprite_datum = SSpai.chassis_data(pai_chassis)
if(sprite_datum)
var/datum/asset/spritesheet_batched/pai_icons/spritesheet = get_asset_datum(/datum/asset/spritesheet_batched/pai_icons)
data["pai_sprite_datum_class"] = sanitize_css_class_name("[sprite_datum.type]")
data["pai_sprite_datum_size"] = spritesheet.icon_size_id(data["pai_sprite_datum_class"] + "S") // just get the south icon's size, the rest will be the same
return data
@@ -111,23 +104,39 @@
pref.antag_vis = choice
return TOPIC_REFRESH
if("option")
if("pai_option")
var/t
switch(params["option"])
switch(params["pai_option"])
if("name")
t = sanitizeName(tgui_input_text(user, "Enter a name for your pAI", "Global Preference", candidate.name, MAX_NAME_LEN), MAX_NAME_LEN, 1)
t = sanitizeName(params["name"], MAX_NAME_LEN, TRUE)
if(t && CanUseTopic(user))
candidate.name = t
pref.update_preference_by_type(/datum/preference/text/pai_name, t)
if("desc")
t = tgui_input_text(user, "Enter a description for your pAI", "Global Preference", html_decode(candidate.description), multiline = TRUE, prevent_enter = TRUE)
t = sanitize(params["desc"], MAX_MESSAGE_LEN * 4)
if(!isnull(t) && CanUseTopic(user))
candidate.description = sanitize(t)
pref.update_preference_by_type(/datum/preference/text/pai_description, sanitize(t))
if("ad")
t = sanitize(params["ad"])
if(!isnull(t) && CanUseTopic(user))
pref.update_preference_by_type(/datum/preference/text/pai_ad, sanitize(t))
if("role")
t = tgui_input_text(user, "Enter a role for your pAI", "Global Preference", html_decode(candidate.role))
t = sanitize(params["role"])
if(!isnull(t) && CanUseTopic(user))
candidate.role = sanitize(t)
pref.update_preference_by_type(/datum/preference/text/pai_role, sanitize(t))
if("ooc")
t = tgui_input_text(user, "Enter any OOC comments", "Global Preference", html_decode(candidate.comments), multiline = TRUE, prevent_enter = TRUE)
t = sanitize(params["ooc"])
if(!isnull(t) && CanUseTopic(user))
candidate.comments = sanitize(t)
pref.update_preference_by_type(/datum/preference/text/pai_comments, sanitize(t))
if("color")
var/new_color = sanitize_hexcolor(params["color"])
if(new_color && CanUseTopic(user))
pref.update_preference_by_type(/datum/preference/color/pai_eye_color, new_color)
if("chassis")
var/new_chassis = params["chassis"]
if(new_chassis && CanUseTopic(user) && (new_chassis in SSpai.get_chassis_list()))
pref.update_preference_by_type(/datum/preference/text/pai_chassis, new_chassis)
if("emotion")
var/new_emotion = params["emotion"]
if(new_emotion && CanUseTopic(user) && (new_emotion in GLOB.pai_emotions))
pref.update_preference_by_type(/datum/preference/text/pai_emotion, new_emotion)
return TOPIC_REFRESH
@@ -0,0 +1,57 @@
/// Moves pai save file to tg pref in current file slot
/datum/preferences/proc/migration_19_paifile(datum/json_savefile/S)
//Not everyone has a pai savefile.
var/pai_path = "data/player_saves/[copytext(client_ckey, 1, 2)]/[client_ckey]/pai.sav"
if(!fexists(pai_path))
return
var/savefile/F = new /savefile(pai_path)
if(!F)
return
// Should never happen, but if someone has a wonked out file then lets avoid it too
var/version = null
F["version"] >> version
if(isnull(version) || version != 1)
fdel(pai_path)
return
// Put the pai in slot 0
var/list/prefs = S.get_entry("character1", null)
if(!islist(prefs))
return
// Get data
var/pai_name
var/pai_description
var/pai_role
var/pai_comments
var/pai_eye_color
var/pai_chassis
var/pai_ouremotion
F["name"] >> pai_name
F["description"] >> pai_description
F["role"] >> pai_role
F["comments"] >> pai_comments
F["eyecolor"] >> pai_eye_color
F["chassis"] >> pai_chassis
F["emotion"] >> pai_ouremotion
// F["gender"] >> pai_gender // We use the character slot's now
if(pai_name)
prefs["Pai_Name"] = pai_name
if(pai_role)
prefs["Pai_Role"] = pai_role
if(pai_description)
prefs["Pai_Desc"] = pai_description
if(pai_comments)
prefs["Pai_Comments"] = pai_comments
if(pai_eye_color)
prefs["Pai_EyeColor"] = pai_eye_color
if(pai_chassis)
prefs["Pai_Chassis"] = pai_chassis
if(pai_ouremotion)
prefs["Pai_Emotion"] = pai_ouremotion
fdel(pai_path)
S.save()
@@ -0,0 +1,116 @@
/datum/preference/text/pai_name
category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
savefile_identifier = PREFERENCE_CHARACTER
savefile_key = "Pai_Name"
maximum_value_length = MAX_NAME_LEN
can_randomize = FALSE
/datum/preference/text/pai_name/create_default_value()
return PAI_UNSET
/datum/preference/text/pai_name/apply_pref_to(mob/living, value)
return
/datum/preference/text/pai_description
category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
savefile_identifier = PREFERENCE_CHARACTER
savefile_key = "Pai_Desc"
maximum_value_length = MAX_MESSAGE_LEN * 4
can_randomize = FALSE
/datum/preference/text/pai_description/create_default_value()
return PAI_UNSET
/datum/preference/text/pai_description/apply_pref_to(mob/living, value)
return
/datum/preference/text/pai_role
category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
savefile_identifier = PREFERENCE_CHARACTER
savefile_key = "Pai_Role"
maximum_value_length = MAX_MESSAGE_LEN
can_randomize = FALSE
/datum/preference/text/pai_role/create_default_value()
return PAI_UNSET
/datum/preference/text/pai_role/apply_pref_to(mob/living, value)
return
/datum/preference/text/pai_ad
category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
savefile_identifier = PREFERENCE_CHARACTER
savefile_key = "Pai_Ad"
maximum_value_length = MAX_MESSAGE_LEN
can_randomize = FALSE
/datum/preference/text/pai_ad/create_default_value()
return PAI_UNSET
/datum/preference/text/pai_ad/apply_pref_to(mob/living, value)
return
/datum/preference/text/pai_comments
category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
savefile_identifier = PREFERENCE_CHARACTER
savefile_key = "Pai_Comments"
maximum_value_length = MAX_MESSAGE_LEN
can_randomize = FALSE
/datum/preference/text/pai_comments/create_default_value()
return PAI_UNSET
/datum/preference/text/pai_comments/apply_pref_to(mob/living, value)
return
/datum/preference/color/pai_eye_color
category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
savefile_identifier = PREFERENCE_CHARACTER
savefile_key = "Pai_EyeColor"
can_randomize = TRUE
/datum/preference/color/pai_eye_color/apply_pref_to(mob/living, value)
return
/datum/preference/text/pai_chassis
category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
savefile_identifier = PREFERENCE_CHARACTER
savefile_key = "Pai_Chassis"
maximum_value_length = MAX_NAME_LEN
can_randomize = FALSE
/datum/preference/text/pai_chassis/create_default_value()
return PAI_DEFAULT_CHASSIS
/datum/preference/text/pai_chassis/is_valid(value)
if(!(value in SSpai.get_chassis_list()))
return FALSE
. = ..()
/datum/preference/text/pai_chassis/apply_pref_to(mob/living, value)
return
/datum/preference/text/pai_emotion
category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
savefile_identifier = PREFERENCE_CHARACTER
savefile_key = "Pai_Emotion"
maximum_value_length = MAX_NAME_LEN
can_randomize = FALSE
/datum/preference/text/pai_emotion/create_default_value()
return GLOB.pai_emotions[1]
/datum/preference/text/pai_emotion/is_valid(value)
if(!(value in GLOB.pai_emotions))
return FALSE
. = ..()
/datum/preference/text/pai_emotion/apply_pref_to(mob/living, value)
return
+11 -1
View File
@@ -1,5 +1,5 @@
#define SAVEFILE_VERSION_MIN 8
#define SAVEFILE_VERSION_MAX 18
#define SAVEFILE_VERSION_MAX 19
/*
SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Carn
@@ -98,6 +98,16 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
log_world("[client_ckey] preferences successfully migrated from [current_version] to v18.")
to_chat(client, span_danger("v18 savefile migration complete."))
// Migration for pai to tg pregs
if(current_version < 19)
log_world("[client_ckey] preferences migrating from [current_version] to v19....")
to_chat(client, span_danger("Migrating savefile from version [current_version] to v19..."))
migration_19_paifile(S)
log_world("[client_ckey] preferences successfully migrated from [current_version] to v19.")
to_chat(client, span_danger("v19 savefile migration complete."))
/datum/preferences/proc/update_character(current_version, list/save_data)
// Migration from BYOND savefiles to JSON: Important milemark.
if(current_version == -3)
+1
View File
@@ -24,6 +24,7 @@
get_asset_datum(/datum/asset/simple/preferences),
get_asset_datum(/datum/asset/spritesheet/preferences),
get_asset_datum(/datum/asset/json/preferences),
get_asset_datum(/datum/asset/spritesheet_batched/pai_icons),
)
for (var/datum/preference_middleware/preference_middleware as anything in middleware)
@@ -175,7 +175,7 @@
emote_sound = pick(bigsound)
else if(ispAI(user))
var/mob/living/silicon/pai/me = user
if(me.chassis == "teppi")
if(istype(SSpai.chassis_data(me.chassis_name), /datum/pai_sprite/large))
emote_sound = pick(bigsound)
else if(user.size_multiplier >= 1.5)
emote_sound = pick(bigsound)
@@ -202,7 +202,7 @@
emote_sound = pick(bigsound)
else if(ispAI(user))
var/mob/living/silicon/pai/me = user
if(me.chassis == "teppi")
if(istype(SSpai.chassis_data(me.chassis_name), /datum/pai_sprite/large))
emote_sound = pick(bigsound)
else if(user.size_multiplier >= 1.5)
emote_sound = pick(bigsound)
+3
View File
@@ -40,6 +40,9 @@
/obj/structure/musician/tgui_interact(mob/user)
return song.tgui_interact(user)
/obj/structure/musician/allow_pai_interaction(proximity_flag)
return proximity_flag
/* FIXME
/obj/structure/musician/wrench_act(mob/living/user, obj/item/tool)
. = ..()
+1 -1
View File
@@ -16,7 +16,7 @@
/datum/map_template/shelter/proc/check_deploy(turf/deploy_location, var/is_ship)
var/affected = get_affected_turfs(deploy_location, centered=TRUE)
for(var/turf/T in affected)
var/shelter_status = get_turf_deployability(T)
var/shelter_status = get_turf_deployability(T, is_ship)
if(shelter_status != SHELTER_DEPLOY_ALLOWED)
return shelter_status
return SHELTER_DEPLOY_ALLOWED
+22 -18
View File
@@ -965,24 +965,28 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
to_chat(src,span_warning("You cannot alert pAI cards when you are banned from playing as a pAI."))
return
if(src.client.prefs?.be_special & BE_PAI)
var/choice = tgui_alert(src, "Would you like to submit yourself to the recruitment list too?", "Confirmation", list("No", "Yes"))
if(!choice)
return
if(choice == "Yes")
paiController.recruitWindow(src)
var/count = 0
for(var/obj/item/paicard/p in GLOB.all_pai_cards)
var/obj/item/paicard/PP = p
if(PP.pai == null)
count++
PP.add_overlay("pai-ghostalert")
PP.alertUpdate()
spawn(54)
PP.cut_overlays()
to_chat(src,span_notice("Flashing the displays of [count] unoccupied PAIs."))
else
to_chat(src,span_warning("You have 'Be pAI' disabled in your character prefs, so we can't help you."))
if(!(src.client.prefs?.be_special & BE_PAI))
to_chat(src,span_warning("You have 'Be pAI' disabled in your character prefs."))
return
var/choice = tgui_alert(src, "Would you like to submit yourself to the recruitment list too?", "Confirmation", list("No", "Yes"))
if(!choice || choice != "Yes")
return
to_chat(src,span_notice("Flashing the displays of [pai_card_ping()] unoccupied PAIs."))
/mob/observer/dead/proc/pai_card_ping()
var/count = 0
for(var/obj/item/paicard/p in GLOB.all_pai_cards)
var/obj/item/paicard/PP = p
if(PP.pai)
continue
count++
PP.cut_overlays()
PP.add_overlay("pai-ghostalert")
PP.alertUpdate()
addtimer(CALLBACK(PP, TYPE_PROC_REF(/obj/item/paicard, clear_invite_overlay)), 1 MINUTE, TIMER_DELETE_ME)
return count
/mob/observer/dead/speech_bubble_appearance()
return "ghost"
+1 -1
View File
@@ -37,7 +37,7 @@
add_verb(src,/mob/proc/nme_vore_ch)
add_verb(src,/mob/proc/enter_soulcatcher)
if(!voice_sounds_list.len || !voice_sounds_list)
if(!length(voice_sounds_list))
if(client.prefs.voice_sound)
var/prefsound = client.prefs.voice_sound
voice_sounds_list = get_talk_sound(prefsound)
@@ -0,0 +1,69 @@
// Pai chassis selection
/datum/tgui_module/pai_chassis
name = "PAI Chassis Configurator"
tgui_id = "PaiChoose"
var/selected_chassis
var/selected_color
/datum/tgui_module/pai_chassis/tgui_state(mob/user)
return GLOB.tgui_self_state
/datum/tgui_module/pai_chassis/ui_assets(mob/user)
return list(
get_asset_datum(/datum/asset/spritesheet_batched/pai_icons),
)
/datum/tgui_module/pai_chassis/tgui_static_data()
var/list/data = ..()
var/list/available_sprites = list()
for(var/key, value in SSpai.get_chassis_list())
var/datum/pai_sprite/current_sprite = value
var/model_type = "def"
if(istype(current_sprite, /datum/pai_sprite/large))
model_type = "big"
UNTYPED_LIST_ADD(available_sprites, list("sprite" = current_sprite.name, "belly" = current_sprite.belly_states, "type" = model_type))
data["pai_chassises"] = available_sprites
return data
/datum/tgui_module/pai_chassis/tgui_data()
var/list/data = ..()
var/mob/living/silicon/pai/pai_host = host
data["pai_color"] = selected_color ? selected_color : pai_host.eye_color
var/datum/pai_sprite/sprite_datum = SSpai.chassis_data(selected_chassis || pai_host.chassis_name)
if(sprite_datum)
var/datum/asset/spritesheet_batched/pai_icons/spritesheet = get_asset_datum(/datum/asset/spritesheet_batched/pai_icons)
data["pai_chassis"] = sprite_datum.name
data["selected_chassis"] = selected_chassis
data["sprite_datum_class"] = sanitize_css_class_name("[sprite_datum.type]")
data["sprite_datum_size"] = spritesheet.icon_size_id(data["sprite_datum_class"] + "S") // just get the south icon's size, the rest will be the same
return data
/datum/tgui_module/pai_chassis/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state)
. = ..()
if(.)
return
switch(action)
if("pick_icon")
var/new_chassis = params["value"]
if(new_chassis && (new_chassis in SSpai.get_chassis_list()))
selected_chassis = new_chassis
return TRUE
if("confirm")
if(!selected_chassis)
return FALSE
var/mob/living/silicon/pai/pai_host = host
if(selected_color)
pai_host.eye_color = selected_color
pai_host.change_chassis(selected_chassis)
return TRUE
if("change_color")
var/new_color = sanitize_hexcolor(params["color"])
if(!new_color)
return FALSE
selected_color = new_color
return TRUE
+2 -1
View File
@@ -19,7 +19,8 @@
card.damage_random_component()
if(gibbed)
qdel(card)
if(!QDELETED(card)) // Either the pai or card could be deleted first, prevent a loop
qdel(card)
..(gibbed)
else
card.add_overlay("pai-dead")
+2 -11
View File
@@ -1,16 +1,7 @@
/mob/living/silicon/pai/Life()
check_retract_cable()
if(src.cable)
if(get_dist(src, src.cable) > 1)
var/turf/T = get_turf_or_move(src.loc)
for (var/mob/M in viewers(T))
M.show_message(span_red("The data cable rapidly retracts back into its spool."), 3, span_red("You hear a click and the sound of wire spooling rapidly."), 2)
playsound(src, 'sound/machines/click.ogg', 50, 1)
qdel(src.cable)
src.cable = null
if (src.stat == DEAD)
if(stat == DEAD)
return
if(card.cell != PP_FUNCTIONAL|| card.processor != PP_FUNCTIONAL || card.board != PP_FUNCTIONAL || card.capacitor != PP_FUNCTIONAL)
+440 -317
View File
@@ -6,6 +6,7 @@
emote_type = 2 // pAIs emotes are heard, not seen, so they can be seen through a container (eg. person)
pass_flags = 1
mob_size = MOB_SMALL
softfall = TRUE
holder_type = /obj/item/holder/pai
@@ -25,7 +26,9 @@
var/obj/item/radio/borg/pai/radio // Our primary radio
var/obj/item/communicator/integrated/communicator // Our integrated communicator.
var/chassis = "pai-repairbot" // A record of your chosen chassis.
var/atom/movable/screen/pai/pai_fold_display = null
var/chassis_name = PAI_DEFAULT_CHASSIS // A record of your chosen chassis.
var/obj/item/pai_cable/cable // The cable we produce and use when door or camera jacking
@@ -68,15 +71,47 @@
var/our_icon_rotation = 0
var/eye_glow = TRUE
var/hide_glow = FALSE
var/image/eye_layer = null // Holds the eye overlay.
var/eye_color = "#00ff0d"
var/icon/holo_icon_south
var/icon/holo_icon_north
var/icon/holo_icon_east
var/icon/holo_icon_west
var/holo_icon_dimension_X = 32
var/holo_icon_dimension_Y = 32
//These vars keep track of whether you have the related software, used for easily updating the UI
var/soft_ut = FALSE //universal translator
var/soft_mr = FALSE //medical records
var/soft_sr = FALSE //security records
var/soft_dj = FALSE //door jack
var/soft_as = FALSE //atmosphere sensor
var/soft_si = FALSE //signaler
var/soft_ar = FALSE //ar hud
var/soft_da = FALSE //death alarm
var/datum/tgui_module/pai_chassis/pai_ui_chassis
vore_capacity = 1
vore_capacity_ex = list("stomach" = 1)
//////////////////////////////////////////////////////////////////////////////////////////////////
// Init and destroy
//////////////////////////////////////////////////////////////////////////////////////////////////
/mob/living/silicon/pai/Initialize(mapload)
. = ..()
card = loc
if(!istype(card))
return INITIALIZE_HINT_QDEL
card = new(src)
card.pai = src
sradio = new(src)
communicator = new(src)
pai_ui_chassis = new(src)
if(card)
if(!card.radio)
card.radio = new /obj/item/radio/borg/pai(src.card)
@@ -92,6 +127,12 @@
add_verb(src, /mob/living/silicon/pai/proc/choose_chassis)
add_verb(src, /mob/living/silicon/pai/proc/choose_verbs)
add_verb(src, /mob/proc/dominate_predator)
add_verb(src, /mob/living/proc/dominate_prey)
add_verb(src, /mob/living/proc/set_size)
add_verb(src, /mob/living/proc/shred_limb)
remove_verb(src, /mob/verb/toggle_gun_mode) // Pai doesn't have support for this and shouldn't be able to use guns anyway
//PDA
pda = new(src)
@@ -104,7 +145,11 @@
M.toff = FALSE
/mob/living/silicon/pai/Login()
..()
. = ..()
if(!holo_icon_south)
last_special = world.time + 100 //Let's give get_character_icon time to work
get_character_icon()
// Vorestation Edit: Meta Info for pAI
if (client.prefs)
ooc_notes = client.prefs.read_preference(/datum/preference/text/living/ooc_notes)
@@ -117,19 +162,233 @@
src << sound('sound/effects/pai_login.ogg', volume = 75) //VOREStation Add
// this function shows the information about being silenced as a pAI in the Status panel
/mob/living/silicon/pai/proc/show_silenced()
. = ""
if(src.silence_time)
var/timeleft = round((silence_time - world.timeofday)/10 ,1)
. += "Communications system reboot in -[(timeleft / 60) % 60]:[add_zero(num2text(timeleft % 60), 2)]"
/// Load pref save data from client and apply it to the pai.
/mob/living/silicon/pai/proc/apply_preferences(client/cli, var/silent = 1)
if(!cli?.prefs)
return FALSE
var/datum/preferences/pref = cli.prefs
SetName(pref.read_preference(/datum/preference/text/pai_name))
flavor_text = pref.read_preference(/datum/preference/text/pai_description)
change_chassis(pref.read_preference(/datum/preference/text/pai_chassis))
gender = pref.read_preference(/datum/preference/choiced/gender/biological) // Cannot use identifying yet due to byond limits
eye_color = pref.read_preference(/datum/preference/color/pai_eye_color)
card.screen_color = eye_color
card.setEmotion(GLOB.pai_emotions[pref.read_preference(/datum/preference/text/pai_emotion)])
update_icon()
return TRUE
/mob/living/silicon/pai/Destroy()
release_vore_contents()
check_retract_cable()
if(!QDELETED(card))
QDEL_NULL(card)
QDEL_NULL(pai_ui_chassis)
QDEL_NULL(sradio)
QDEL_NULL(communicator)
QDEL_NULL(pda)
if(pai_fold_display)
QDEL_NULL(pai_fold_display)
securityActive1 = null
securityActive2 = null
hackdoor = null
current = null
if(ckey)
GLOB.paikeys -= ckey
return ..()
/mob/living/silicon/pai/clear_client()
if(ckey)
GLOB.paikeys -= ckey
return ..()
// No binary for pAIs.
/mob/living/silicon/pai/binarycheck()
return 0
/// Verb used to select a chassis from the list of available chassis
/mob/living/silicon/pai/proc/choose_chassis()
set category = "Abilities.pAI Commands"
set name = "Choose Chassis"
pai_ui_chassis.tgui_interact(src)
/// Change pai sprite and offsets based upon the selected chassis id
/mob/living/silicon/pai/proc/change_chassis(new_chassis)
if(!(new_chassis in SSpai.get_chassis_list()))
new_chassis = PAI_DEFAULT_CHASSIS
chassis_name = new_chassis
//We resize ourselves to normal here for a moment to let the vis_height get reset
var/oursize = size_multiplier
resize(1, FALSE, TRUE, TRUE, FALSE)
// Get icon data setup
var/datum/pai_sprite/chassis_data = SSpai.chassis_data(chassis_name)
if(chassis_data.holo_projector)
// Rebuild holosprite from character
if(!holo_icon_south)
get_character_icon()
else
// Get data from our sprite datum
icon = chassis_data.sprite_icon
pixel_x = chassis_data.pixel_x
default_pixel_x = pixel_x
pixel_y = chassis_data.pixel_y
default_pixel_y = pixel_y
vis_height = chassis_data.vis_height
// Drops you if you change to a non-flying chassis
if(chassis_data.flying)
hovering = TRUE
else
hovering = FALSE
if(isopenspace(loc))
fall()
// Set vore size.
vore_capacity = max(1, chassis_data.belly_states) // Minimum of 1
vore_capacity_ex = list("stomach" = vore_capacity)
// Emergency eject if you change to a smaller belly
if(vore_fullness > vore_capacity && vore_selected)
vore_selected.release_all_contents(TRUE)
update_icon()
resize(oursize, FALSE, TRUE, TRUE, FALSE) //And then back again now that we're sure the vis_height is correct.
//////////////////////////////////////////////////////////////////////////////////////////////////
// Click interactions
//////////////////////////////////////////////////////////////////////////////////////////////////
/mob/living/silicon/pai/attackby(obj/item/W as obj, mob/user as mob)
var/obj/item/card/id/ID = W.GetID()
if(ID)
if (idaccessible == 1)
switch(tgui_alert(user, "Do you wish to add access to [src] or remove access from [src]?","Access Modify",list("Add Access","Remove Access", "Cancel")))
if("Add Access")
idcard.access |= ID.GetAccess()
to_chat(user, span_notice("You add the access from the [W] to [src]."))
to_chat(src, span_notice("\The [user] swipes the [W] over you. You copy the access codes."))
if(radio)
radio.recalculateChannels()
return
if("Remove Access")
idcard.access = list()
to_chat(user, span_notice("You remove the access from [src]."))
to_chat(src, span_warning("\The [user] swipes the [W] over you, removing access codes from you."))
if(radio)
radio.recalculateChannels()
return
if("Cancel", null)
return
else if (istype(W, /obj/item/card/id) && idaccessible == 0)
to_chat(user, span_notice("[src] is not accepting access modifcations at this time."))
return
//Overriding this will stop a number of headaches down the track.
/mob/living/silicon/pai/attackby(obj/item/W as obj, mob/user as mob)
if(W.force)
visible_message(span_danger("[user.name] attacks [src] with [W]!"))
src.adjustBruteLoss(W.force)
src.updatehealth()
else
visible_message(span_warning("[user.name] bonks [src] harmlessly with [W]."))
spawn(1)
if(stat != DEAD) close_up()
return
/mob/living/silicon/pai/attack_hand(mob/user as mob)
if(user.a_intent == I_HELP)
visible_message(span_notice("[user.name] pats [src]."))
else
visible_message(span_danger("[user.name] boops [src] on the head."))
close_up()
/mob/living/silicon/pai/UnarmedAttack(atom/A, proximity_flag)
. = ..()
// Some restricted objects to interact with
var/obj/O = A
if(istype(O) && O.allow_pai_interaction(proximity_flag))
O.attack_hand(src)
return
// Zmovement already allows these to be used with the verbs anyway
if(istype(A,/obj/structure/ladder))
var/obj/structure/ladder/L = A
L.attack_hand(src)
return
// We don't want to pick these up, just toggle them
if(istype(A,/obj/item/flashlight/lamp))
var/obj/item/flashlight/lamp/L = A
L.toggle_light()
return
// All other computers explain why it's not accessible by showing a firewall warning
if(istype(A,/obj/machinery/computer))
to_chat(src,span_warning("A firewall prevents you from interfacing with this device!"))
return
if(!ismob(A) || A == src)
return
switch(a_intent)
if(I_HELP)
if(isliving(A))
hug(src, A)
if(I_GRAB)
pai_nom(A)
// Allow card inhabited machines to be interacted with
// This has to override ClickOn because of storage depth nonsense with how pAIs are in cards in GLOB.machines
/mob/living/silicon/pai/ClickOn(var/atom/A, var/params)
if(istype(A, /obj/machinery))
var/obj/machinery/M = A
if(M.paicard == card)
M.attack_ai(src)
return
return ..()
// Handle being picked up.
/mob/living/silicon/pai/get_scooped(var/mob/living/carbon/grabber, var/self_drop)
var/obj/item/holder/H = ..(grabber, self_drop)
if(!istype(H))
return
H.icon_state = SSpai.chassis_data(chassis_name).sprite_icon_state
grabber.update_inv_l_hand()
grabber.update_inv_r_hand()
return H
/mob/living/silicon/pai/Moved(atom/oldloc, direct, forced, movetime)
. = ..()
check_retract_cable()
//////////////////////////////////////////////////////////////////////////////////////////////////
// Status and damage
//////////////////////////////////////////////////////////////////////////////////////////////////
/mob/living/silicon/pai/get_status_tab_items()
. = ..()
. += ""
. += show_silenced()
/mob/living/silicon/pai/adjustBruteLoss(amount, include_robo)
. = ..()
if(amount > 0 && health <= 90) //Something's probably attacking us!
if(prob(amount)) //The more damage it is doing, the more likely it is to damage something important!
card.damage_random_component()
/mob/living/silicon/pai/adjustFireLoss(amount, include_robo)
. = ..()
if(amount > 0 && health <= 90)
if(prob(amount))
card.damage_random_component()
/mob/living/silicon/pai/restrained()
if(istype(src.loc,/obj/item/paicard))
return 0
@@ -169,145 +428,36 @@
if(3)
to_chat(src, span_infoplain(span_green("You feel an electric surge run through your circuitry and become acutely aware at how lucky you are that you can still feel at all.")))
/mob/living/silicon/pai/proc/switchCamera(var/obj/machinery/camera/C)
if (!C)
src.reset_perspective()
return 0
if (stat == 2 || !C.status || !(src.network in C.network)) return 0
/// this function shows the information about being silenced as a pAI in the Status panel
/mob/living/silicon/pai/proc/show_silenced()
. = ""
if(src.silence_time)
var/timeleft = round((silence_time - world.timeofday)/10 ,1)
. += "Communications system reboot in -[(timeleft / 60) % 60]:[add_zero(num2text(timeleft % 60), 2)]"
// ok, we're alive, camera is good and in our network...
src.current = C
src.AddComponent(/datum/component/remote_view, focused_on = C, viewsize = null, vconfig_path = /datum/remote_view_config/camera_standard)
return 1
/// Fully heals a pai, used when a pai is repaired
/mob/living/silicon/pai/proc/full_restore()
adjustBruteLoss(- bruteloss)
adjustFireLoss(- fireloss)
addtimer(CALLBACK(src, PROC_REF(restore_delay_start)), 5 SECONDS, TIMER_DELETE_ME)
/mob/living/silicon/pai/verb/reset_record_view()
set category = "Abilities.pAI Commands"
set name = "Reset Records Software"
/mob/living/silicon/pai/proc/restore_delay_start()
SHOULD_NOT_OVERRIDE(TRUE)
PRIVATE_PROC(TRUE)
card.setEmotion(16)
stat = CONSCIOUS
addtimer(CALLBACK(src, PROC_REF(restore_delay_end)), 1 SECONDS, TIMER_DELETE_ME)
securityActive1 = null
securityActive2 = null
security_cannotfind = 0
medicalActive1 = null
medicalActive2 = null
medical_cannotfind = 0
SStgui.update_uis(src)
to_chat(src, span_notice("You reset your record-viewing software."))
/mob/living/silicon/pai/cancel_camera()
set category = "Abilities.pAI Commands"
set name = "Cancel Camera View"
reset_perspective()
/mob/living/silicon/pai/reset_perspective(atom/new_eye)
. = ..()
current = null
// Procs/code after this point is used to convert the stationary pai item into a
// mobile pai mob. This also includes handling some of the general shit that can occur
// to it. Really this deserves its own file, but for the moment it can sit here. ~ Z
/mob/living/silicon/pai/verb/fold_out()
set category = "Abilities.pAI Commands"
set name = "Unfold Chassis"
if(stat || sleeping || paralysis || weakened)
return
if(loc != card)
return
// Lets not trap the pai forever. These are special cases we want to escape out of when in our card
if(istype(loc.loc, /obj/item/pda))
var/obj/item/pda/ourpda = loc.loc
if(ourpda.pai == card)
ourpda.pai.forceMove(ourpda.loc)
ourpda.pai = null
visible_message(span_warning("\The [card] ejects itself from \the [ourpda]."))
return
if(istype(loc.loc, /obj/item/storage/vore_egg))
var/obj/item/storage/vore_egg/ouregg = loc.loc
to_chat(src, span_notice("You craftily use your built in rumble function to break free of \the [ouregg]'s confines!"))
ouregg.hatch(src)
return
if(is_folding_unsafe(loc.loc))
to_chat(src, span_danger("It's not safe to unfold while inside a [loc.loc]!"))
return
if(card.projector != PP_FUNCTIONAL && card.emitter != PP_FUNCTIONAL)
to_chat(src, span_warning("ERROR: System malfunction. Service required!"))
if(world.time <= last_special)
to_chat(src, span_warning("You can't unfold yet."))
return
last_special = world.time + 100
if(istype(card.loc, /obj/machinery)) // VOREStation edit, this statement allows pAIs stuck in a machine to eject themselves.
var/obj/machinery/M = card.loc
M.ejectpai()
//I'm not sure how much of this is necessary, but I would rather avoid issues.
if(istype(card.loc,/obj/item/rig_module))
to_chat(src, span_filter_notice("There is no room to unfold inside this rig module. You're good and stuck."))
return 0
else if(istype(card.loc,/mob))
var/mob/holder = card.loc
if(ishuman(holder))
var/mob/living/carbon/human/H = holder
for(var/obj/item/organ/external/affecting in H.organs)
if(card in affecting.implants)
affecting.take_damage(rand(30,50))
affecting.implants -= card
H.visible_message(span_danger("\The [src] explodes out of \the [H]'s [affecting.name] in shower of gore!"))
break
holder.drop_from_inventory(card)
else if(isbelly(card.loc)) //VOREStation edit.
to_chat(src, span_notice("There is no room to unfold in here. You're good and stuck.")) //VOREStation edit.
return 0 //VOREStation edit.
else if(istype(card.loc,/obj/item/pda))
var/obj/item/pda/holder = card.loc
holder.pai = null
src.forceMove(card.loc)
card.forceMove(src)
card.screen_loc = null
/mob/living/silicon/pai/proc/restore_delay_end()
SHOULD_NOT_OVERRIDE(TRUE)
PRIVATE_PROC(TRUE)
var/mob/observer/dead/ghost = get_ghost()
if(ghost)
ghost.notify_revive("Someone is trying to revive you. Re-enter your body if you want to be revived!", 'sound/effects/pai-restore.ogg', source = card)
canmove = TRUE
if(isturf(loc))
var/turf/T = get_turf(src)
if(istype(T)) T.visible_message(span_filter_notice(span_bold("[src]") + " folds outwards, expanding into a mobile form."))
add_verb(src, /mob/living/silicon/pai/proc/pai_nom)
add_verb(src, /mob/living/proc/vertical_nom)
update_icon()
/mob/living/silicon/pai/verb/fold_up()
set category = "Abilities.pAI Commands"
set name = "Collapse Chassis"
if(stat || sleeping || paralysis || weakened)
return
if(src.loc == card)
return
if(world.time <= last_special)
to_chat(src, span_warning("You can't fold up yet."))
return
close_up()
/mob/living/silicon/pai/proc/choose_verbs()
set category = "Abilities.pAI Commands"
set name = "Choose Speech Verbs"
var/choice = tgui_input_list(src,"What theme would you like to use for your speech verbs?","Theme Choice", GLOB.possible_say_verbs)
if(!choice) return
var/list/sayverbs = GLOB.possible_say_verbs[choice]
speak_statement = sayverbs[1]
speak_exclamation = sayverbs[(sayverbs.len>1 ? 2 : sayverbs.len)]
speak_query = sayverbs[(sayverbs.len>2 ? 3 : sayverbs.len)]
card.setEmotion(15)
playsound(card, 'sound/effects/pai-restore.ogg', 50, FALSE)
card.visible_message(span_filter_notice("\The [card] chimes."), runemessage = "chime")
/mob/living/silicon/pai/lay_down()
set name = "Rest"
@@ -320,193 +470,166 @@
if(istype(rig))
rig.force_rest(src)
return
else if(chassis == "13")
resting = !resting
//update_transform() I want this to make you ROTATE like normal HUMANS do! But! There's lots of problems and I don't know how to fix them!
else
resting = !resting
icon_state = resting ? "[chassis]_rest" : "[chassis]"
update_icon() //VOREStation edit
update_icon()
to_chat(src, span_notice("You are now [resting ? "resting" : "getting up"]."))
canmove = !resting
/*
/mob/living/silicon/pai/update_transform()
/mob/living/silicon/pai/proc/check_retract_cable()
if(!cable)
return
var/desired_scale_x = size_multiplier * icon_scale_x
var/desired_scale_y = size_multiplier * icon_scale_y
var/turf/current = get_turf(src)
var/turf/cableturf = get_turf(cable)
if(get_dist(current, cableturf) <= 1)
return
// Now for the regular stuff.
var/matrix/M = matrix()
M.Scale(desired_scale_x, desired_scale_y)
M.Translate(0, (vis_height/2)*(desired_scale_y-1))
cableturf.visible_message("The data cable rapidly retracts back into its spool.", "You hear a click and the sound of wire spooling rapidly.")
playsound(src, 'sound/machines/click.ogg', 50, 1)
QDEL_NULL(cable)
if(chassis != "13")
appearance_flags |= PIXEL_SCALE
//////////////////////////////////////////////////////////////////////////////////////////////////
// Update icons
//////////////////////////////////////////////////////////////////////////////////////////////////
var/anim_time = 3
/mob/living/silicon/pai/update_icon()
. = ..()
if(resting)
M.Turn(90)
M.Scale(desired_scale_y, desired_scale_x)
if(holo_icon_dimension_X == 64 && holo_icon_dimension_Y == 64)
M.Translate(13,-22)
else if(holo_icon_dimension_X == 32 && holo_icon_dimension_Y == 64)
M.Translate(1,-22)
else if(holo_icon_dimension_X == 64 && holo_icon_dimension_Y == 32)
M.Translate(13,-6)
var/datum/pai_sprite/chassis_data = SSpai.chassis_data(chassis_name)
if(chassis_data.holo_projector)
icon_state = null
icon = holo_icon_south
add_eyes()
return
update_fullness()
// Don't get a vore belly size if we have no belly size set!
var/belly_size = CLAMP(vore_fullness, 0, chassis_data.belly_states)
if(resting && !chassis_data.resting_belly) // check if we have a belly while resting
belly_size = 0
var/fullness_extension = ""
if(belly_size > 1) // Multibelly support
fullness_extension = "_[belly_size]"
icon_state = "[chassis_data.sprite_icon_state][resting && chassis_data.can_rest ? "_rest" : ""][belly_size ? "_full[fullness_extension]" : ""]"
add_eyes()
/// Applies the eye overlay if the chassis has it
/mob/living/silicon/pai/proc/add_eyes()
remove_eyes()
var/datum/pai_sprite/chassis_data = SSpai.chassis_data(chassis_name)
if(chassis_data.holo_projector)
// Special eyes that are based on holoprojection of your character's icon size
if(holo_icon_south.Width() > 32)
holo_icon_dimension_X = 64
pixel_x = -16
default_pixel_x = -16
// Get height too
if(holo_icon_south.Height() > 32)
holo_icon_dimension_Y = 64
vis_height = 64
// Set eyes
if(holo_icon_dimension_X == 32 && holo_icon_dimension_Y == 32)
eye_layer = image('icons/mob/pai.dmi', chassis_data.holo_eyes_icon_state)
else if(holo_icon_dimension_X == 32 && holo_icon_dimension_Y == 64)
eye_layer = image('icons/mob/pai32x64.dmi', chassis_data.holo_eyes_icon_state)
else if(holo_icon_dimension_X == 64 && holo_icon_dimension_Y == 32)
eye_layer = image('icons/mob/pai64x32.dmi', chassis_data.holo_eyes_icon_state)
else if(holo_icon_dimension_X == 64 && holo_icon_dimension_Y == 64)
eye_layer = image('icons/mob/pai64x64.dmi', chassis_data.holo_eyes_icon_state)
else if(chassis_data.has_eye_sprites)
// Default eye handling
eye_layer = image(icon, "[icon_state]-eyes")
else
// No eyes, so don't bother setting icon stuff
return
eye_layer.appearance_flags = appearance_flags
eye_layer.color = eye_color
if(eye_glow && !hide_glow)
eye_layer.plane = PLANE_LIGHTING_ABOVE
add_overlay(eye_layer)
/// Removes the eye overlay if it has one
/mob/living/silicon/pai/proc/remove_eyes()
if(!eye_layer)
return
cut_overlay(eye_layer)
qdel(eye_layer)
eye_layer = null
/// Gets icons for all four directions based on the character slot currently loaded
/mob/living/silicon/pai/proc/get_character_icon()
if(!client || !client.prefs) return FALSE
var/mob/living/carbon/human/dummy/dummy = new ()
//This doesn't include custom_items because that's ... hard.
client.prefs.dress_preview_mob(dummy)
sleep(1 SECOND) //Strange bug in preview code? Without this, certain things won't show up. Yay race conditions?
dummy.regenerate_icons()
var/icon/new_holo = getCompoundIcon(dummy)
dummy.tail_layering = TRUE
dummy.set_dir(NORTH)
var/icon/new_holo_north = getCompoundIcon(dummy)
dummy.set_dir(EAST)
var/icon/new_holo_east = getCompoundIcon(dummy)
dummy.set_dir(WEST)
var/icon/new_holo_west = getCompoundIcon(dummy)
qdel(holo_icon_south)
qdel(holo_icon_north)
qdel(holo_icon_east)
qdel(holo_icon_west)
qdel(dummy)
holo_icon_south = new_holo
holo_icon_north = new_holo_north
holo_icon_east = new_holo_east
holo_icon_west = new_holo_west
return TRUE
/mob/living/silicon/pai/set_dir(var/new_dir)
. = ..()
if(. && SSpai.chassis_data(chassis_name).holo_projector)
switch(dir)
if(SOUTH)
icon = holo_icon_south
if(NORTH)
icon = holo_icon_north
if(EAST)
icon = holo_icon_east
if(WEST)
icon = holo_icon_west
else
M.Translate(1,-6)
layer = MOB_LAYER -0.01 // Fix for a byond bug where turf entry order no longer matters
else
M.Scale(desired_scale_x, desired_scale_y)
M.Translate(0, (vis_height/2)*(desired_scale_y-1))
layer = MOB_LAYER // Fix for a byond bug where turf entry order no longer matters
animate(src, transform = M, time = anim_time)
src.transform = M
handle_status_indicators()
*/
//Overriding this will stop a number of headaches down the track.
/mob/living/silicon/pai/attackby(obj/item/W as obj, mob/user as mob)
if(W.force)
visible_message(span_danger("[user.name] attacks [src] with [W]!"))
src.adjustBruteLoss(W.force)
src.updatehealth()
else
visible_message(span_warning("[user.name] bonks [src] harmlessly with [W]."))
spawn(1)
if(stat != 2) close_up()
return
icon = holo_icon_north
/mob/living/silicon/pai/attack_hand(mob/user as mob)
if(user.a_intent == I_HELP)
visible_message(span_notice("[user.name] pats [src]."))
else
visible_message(span_danger("[user.name] boops [src] on the head."))
close_up()
/mob/living/silicon/pai/a_intent_change(input as text)
. = ..()
//I'm not sure how much of this is necessary, but I would rather avoid issues.
/mob/living/silicon/pai/proc/close_up(silent= FALSE)
switch(a_intent)
if(I_HELP)
hud_used.help_intent.icon_state = "intent_help-s"
hud_used.disarm_intent.icon_state = "intent_disarm-n"
hud_used.grab_intent.icon_state = "intent_grab-n"
hud_used.hurt_intent.icon_state = "intent_harm-n"
last_special = world.time + 100
if(I_DISARM)
hud_used.help_intent.icon_state = "intent_help-n"
hud_used.disarm_intent.icon_state = "intent_disarm-s"
hud_used.grab_intent.icon_state = "intent_grab-n"
hud_used.hurt_intent.icon_state = "intent_harm-n"
if(loc == card)
return
if(I_GRAB)
hud_used.help_intent.icon_state = "intent_help-n"
hud_used.disarm_intent.icon_state = "intent_disarm-n"
hud_used.grab_intent.icon_state = "intent_grab-s"
hud_used.hurt_intent.icon_state = "intent_harm-n"
// some snowflake locations where we really shouldn't fold up...
if(is_folding_unsafe(loc))
to_chat(src, span_danger("It's not safe to fold up while inside a [loc]!"))
return
release_vore_contents(FALSE) //VOREStation Add
var/turf/T = get_turf(src)
if(istype(T) && !silent) T.visible_message(span_filter_notice(span_bold("[src]") + " neatly folds inwards, compacting down to a rectangular card."))
stop_pulling()
//stop resting
resting = 0
// If we are being held, handle removing our holder from their inv.
var/obj/item/holder/our_holder = loc
if(istype(our_holder))
var/turf/drop_turf = get_turf(our_holder)
var/mob/living/M = our_holder.loc
if(istype(M))
M.drop_from_inventory(our_holder)
src.forceMove(card)
card.forceMove(drop_turf)
if(isbelly(loc)) //If in tumby, when fold up, card go into tumby
var/obj/belly/B = loc
src.forceMove(card)
card.forceMove(B)
if(isdisposalpacket(loc))
var/obj/structure/disposalholder/hold = loc
src.forceMove(card)
card.forceMove(hold)
else //Otherwise go on floor
card.forceMove(get_turf(src))
src.forceMove(card)
canmove = 1
resting = 0
icon_state = "[chassis]"
if(isopenspace(card.loc))
fall()
remove_verb(src, /mob/living/silicon/pai/proc/pai_nom)
remove_verb(src, /mob/living/proc/vertical_nom)
/mob/living/silicon/pai/proc/is_folding_unsafe(check_location)
return isbelly(check_location) || istype(check_location, /obj/machinery) || istype(check_location, /obj/item/storage/vore_egg || istype(check_location, /obj/item/pda))
// No binary for pAIs.
/mob/living/silicon/pai/binarycheck()
return 0
// Handle being picked up.
/mob/living/silicon/pai/get_scooped(var/mob/living/carbon/grabber, var/self_drop)
var/obj/item/holder/H = ..(grabber, self_drop)
if(!istype(H))
return
H.icon_state = "[chassis]"
grabber.update_inv_l_hand()
grabber.update_inv_r_hand()
return H
/mob/living/silicon/pai/attackby(obj/item/W as obj, mob/user as mob)
var/obj/item/card/id/ID = W.GetID()
if(ID)
if (idaccessible == 1)
switch(tgui_alert(user, "Do you wish to add access to [src] or remove access from [src]?","Access Modify",list("Add Access","Remove Access", "Cancel")))
if("Add Access")
idcard.access |= ID.GetAccess()
to_chat(user, span_notice("You add the access from the [W] to [src]."))
to_chat(src, span_notice("\The [user] swipes the [W] over you. You copy the access codes."))
if(radio)
radio.recalculateChannels()
return
if("Remove Access")
idcard.access = list()
to_chat(user, span_notice("You remove the access from [src]."))
to_chat(src, span_warning("\The [user] swipes the [W] over you, removing access codes from you."))
if(radio)
radio.recalculateChannels()
return
if("Cancel", null)
return
else if (istype(W, /obj/item/card/id) && idaccessible == 0)
to_chat(user, span_notice("[src] is not accepting access modifcations at this time."))
return
/mob/living/silicon/pai/verb/allowmodification()
set name = "Change Access Modifcation Permission"
set category = "Abilities.pAI Commands"
set desc = "Allows people to modify your access or block people from modifying your access."
if(idaccessible == 0)
idaccessible = 1
visible_message(span_notice("\The [src] clicks as their access modification slot opens."),span_notice("You allow access modifications."), runemessage = "click")
else
idaccessible = 0
visible_message(span_notice("\The [src] clicks as their access modification slot closes."),span_notice("You block access modfications."), runemessage = "click")
/mob/living/silicon/pai/verb/wipe_software()
set name = "Enter Storage"
set category = "Abilities.pAI Commands"
set desc = "Upload your personality to the cloud and wipe your software from the card. This is functionally equivalent to cryo or robotic storage, freeing up your job slot."
// Make sure people don't kill themselves accidentally
if(tgui_alert(src, "WARNING: This will immediately wipe your software and ghost you, removing your character from the round permanently (similar to cryo and robotic storage). Are you entirely sure you want to do this?", "Wipe Software", list("No", "Yes")) != "Yes")
return
close_up()
visible_message(span_filter_notice(span_bold("[src]") + " fades away from the screen, the pAI device goes silent."))
card.removePersonality()
clear_client()
if(I_HURT)
hud_used.help_intent.icon_state = "intent_help-n"
hud_used.disarm_intent.icon_state = "intent_disarm-n"
hud_used.grab_intent.icon_state = "intent_grab-n"
hud_used.hurt_intent.icon_state = "intent_harm-s"
@@ -0,0 +1,148 @@
/mob/living/silicon/pai/verb/fold_out()
set category = "Abilities.pAI Commands"
set name = "Unfold Chassis"
if(stat || sleeping || paralysis || weakened)
return
if(loc != card)
return
// Lets not trap the pai forever. These are special cases we want to escape out of when in our card
if(istype(loc.loc, /obj/item/pda))
var/obj/item/pda/ourpda = loc.loc
if(ourpda.pai == card)
ourpda.pai.forceMove(ourpda.loc)
ourpda.pai = null
visible_message(span_warning("\The [card] ejects itself from \the [ourpda]."))
return
if(istype(loc.loc, /obj/item/storage/vore_egg))
var/obj/item/storage/vore_egg/ouregg = loc.loc
to_chat(src, span_notice("You craftily use your built in rumble function to break free of \the [ouregg]'s confines!"))
ouregg.hatch(src)
return
if(is_folding_unsafe(loc.loc))
to_chat(src, span_danger("It's not safe to unfold while inside a [loc.loc]!"))
return
if(card.projector != PP_FUNCTIONAL && card.emitter != PP_FUNCTIONAL)
to_chat(src, span_warning("ERROR: System malfunction. Service required!"))
if(world.time <= last_special)
to_chat(src, span_warning("You can't unfold yet."))
return
last_special = world.time + 100
if(istype(card.loc, /obj/machinery)) // VOREStation edit, this statement allows pAIs stuck in a machine to eject themselves.
var/obj/machinery/M = card.loc
M.ejectpai()
//I'm not sure how much of this is necessary, but I would rather avoid issues.
if(istype(card.loc,/obj/item/rig_module))
to_chat(src, span_filter_notice("There is no room to unfold inside this rig module. You're good and stuck."))
return 0
else if(istype(card.loc,/mob))
var/mob/holder = card.loc
if(ishuman(holder))
var/mob/living/carbon/human/H = holder
for(var/obj/item/organ/external/affecting in H.organs)
if(card in affecting.implants)
affecting.take_damage(rand(30,50))
affecting.implants -= card
H.visible_message(span_danger("\The [src] explodes out of \the [H]'s [affecting.name] in shower of gore!"))
break
holder.drop_from_inventory(card)
else if(isbelly(card.loc)) //VOREStation edit.
to_chat(src, span_notice("There is no room to unfold in here. You're good and stuck.")) //VOREStation edit.
return 0 //VOREStation edit.
else if(istype(card.loc,/obj/item/pda))
var/obj/item/pda/holder = card.loc
holder.pai = null
src.forceMove(card.loc)
card.forceMove(src)
card.screen_loc = null
canmove = TRUE
if(isturf(loc))
var/turf/T = get_turf(src)
if(istype(T)) T.visible_message(span_filter_notice(span_bold("[src]") + " folds outwards, expanding into a mobile form."))
add_verb(src, /mob/living/silicon/pai/proc/pai_nom)
add_verb(src, /mob/living/proc/vertical_nom)
update_icon()
/mob/living/silicon/pai/verb/fold_up()
set category = "Abilities.pAI Commands"
set name = "Collapse Chassis"
if(stat || sleeping || paralysis || weakened)
return
if(src.loc == card)
return
if(world.time <= last_special)
to_chat(src, span_warning("You can't fold up yet."))
return
close_up()
//I'm not sure how much of this is necessary, but I would rather avoid issues.
/mob/living/silicon/pai/proc/close_up(silent= FALSE)
last_special = world.time + 100
if(loc == card)
return
// some snowflake locations where we really shouldn't fold up...
if(is_folding_unsafe(loc))
to_chat(src, span_danger("It's not safe to fold up while inside a [loc]!"))
return
release_vore_contents(FALSE) //VOREStation Add
var/turf/T = get_turf(src)
if(istype(T) && !silent) T.visible_message(span_filter_notice(span_bold("[src]") + " neatly folds inwards, compacting down to a rectangular card."))
stop_pulling()
//stop resting
resting = 0
// If we are being held, handle removing our holder from their inv.
var/obj/item/holder/our_holder = loc
if(istype(our_holder))
var/turf/drop_turf = get_turf(our_holder)
var/mob/living/M = our_holder.loc
if(istype(M))
M.drop_from_inventory(our_holder)
src.forceMove(card)
card.forceMove(drop_turf)
if(isbelly(loc)) //If in tumby, when fold up, card go into tumby
var/obj/belly/B = loc
src.forceMove(card)
card.forceMove(B)
if(isdisposalpacket(loc))
var/obj/structure/disposalholder/hold = loc
src.forceMove(card)
card.forceMove(hold)
else //Otherwise go on floor
card.forceMove(get_turf(src))
src.forceMove(card)
canmove = 1
resting = 0
icon_state = SSpai.chassis_data(chassis_name).sprite_icon_state
if(isopenspace(card.loc))
fall()
remove_verb(src, /mob/living/silicon/pai/proc/pai_nom)
remove_verb(src, /mob/living/proc/vertical_nom)
/mob/living/silicon/pai/proc/is_folding_unsafe(check_location)
return isbelly(check_location) || istype(check_location, /obj/machinery) || istype(check_location, /obj/item/storage/vore_egg || istype(check_location, /obj/item/pda))
@@ -1,6 +1,3 @@
/mob/living/silicon/pai
var/atom/movable/screen/pai/pai_fold_display = null
/atom/movable/screen/pai
icon = 'icons/mob/pai_hud.dmi'
var/base_state
@@ -0,0 +1,136 @@
/mob/living/silicon/pai/proc/refresh_software_status() //This manages the pAI software status buttons icon states based on if you have them and if they are enabled
for(var/thing in software) //this only gets called when you click one of the relevent buttons, rather than all the time!
var/datum/pai_software/soft = software[thing]
if(istype(soft,/datum/pai_software/med_records))
soft_mr = TRUE
if(istype(soft,/datum/pai_software/sec_records))
soft_sr = TRUE
if(istype(soft,/datum/pai_software/door_jack))
soft_dj = TRUE
if(istype(soft,/datum/pai_software/atmosphere_sensor))
soft_as = TRUE
if(istype(soft,/datum/pai_software/pai_hud))
soft_ar = TRUE
if(istype(soft,/datum/pai_software/translator))
soft_ut = TRUE
if(istype(soft,/datum/pai_software/signaller))
soft_si = TRUE
if(istype(soft,/datum/pai_software/deathalarm))
soft_da = TRUE
for(var/atom/movable/screen/pai/button in hud_used.other)
if(button.name == "medical records")
if(soft_mr)
button.icon_state = "[button.base_state]"
else
button.icon_state = "[button.base_state]_o"
if(button.name == "security records")
if(soft_sr)
button.icon_state = "[button.base_state]"
else
button.icon_state = "[button.base_state]_o"
if(button.name == "door jack")
if(soft_dj)
button.icon_state = "[button.base_state]"
else
button.icon_state = "[button.base_state]_o"
if(button.name == "atmosphere sensor")
if(soft_as)
button.icon_state = "[button.base_state]"
else
button.icon_state = "[button.base_state]_o"
if(button.name == "remote signaler")
if(soft_si)
button.icon_state = "[button.base_state]"
else
button.icon_state = "[button.base_state]_o"
if(button.name == "universal translator")
if(soft_ut && translator_on)
button.icon_state = "[button.base_state]"
else
button.icon_state = "[button.base_state]_o"
if(button.name == "ar hud")
if(soft_ar && paiHUD)
button.icon_state = "[button.base_state]"
else
button.icon_state = "[button.base_state]_o"
if(button.name == "death alarm")
if(soft_da && paiDA)
button.icon_state = "[button.base_state]"
else
button.icon_state = "[button.base_state]_o"
/mob/living/silicon/pai/verb/wipe_software()
set name = "Enter Storage"
set category = "Abilities.pAI Commands"
set desc = "Upload your personality to the cloud and wipe your software from the card. This is functionally equivalent to cryo or robotic storage, freeing up your job slot."
// Make sure people don't kill themselves accidentally
if(tgui_alert(src, "WARNING: This will immediately wipe your software and ghost you, removing your character from the round permanently (similar to cryo and robotic storage). Are you entirely sure you want to do this?", "Wipe Software", list("No", "Yes")) != "Yes")
return
close_up()
visible_message(span_filter_notice(span_bold("[src]") + " fades away from the screen, the pAI device goes silent."))
card.removePersonality()
clear_client()
/mob/living/silicon/pai/proc/touch_window(soft_name) //This lets us touch TGUI procs and windows that may be nested behind other TGUI procs and windows
if(stat != CONSCIOUS) //so we can access our software without having to open up the software interface TGUI window
to_chat(src, span_warning("You can't do that right now."))
return
for(var/thing in software)
var/datum/pai_software/S = software[thing]
if(istype(S, /datum/pai_software) && S.name == soft_name)
if(S.toggle)
S.toggle(src)
to_chat(src, span_notice("You toggled [S.name]."))
refresh_software_status()
else
S.tgui_interact(src)
refresh_software_status()
return
for(var/thing in GLOB.pai_software_by_key)
var/datum/pai_software/our_soft = GLOB.pai_software_by_key[thing]
if(our_soft.name == soft_name)
if(!(ram >= our_soft.ram_cost))
to_chat(src, span_warning("Insufficient RAM for download. (Cost [our_soft.ram_cost] : [ram] Remaining)"))
return
if(tgui_alert(src, "Do you want to download [our_soft.name]? It costs [our_soft.ram_cost], and you have [ram] remaining.", "Download [our_soft.name]", list("Yes", "No")) == "Yes")
if(!(ram >= our_soft.ram_cost))
return
if(software[our_soft.id])
return
ram -= our_soft.ram_cost
software[our_soft.id] = our_soft
to_chat(src, span_notice("You downloaded [our_soft.name]. ([ram] RAM remaining.)"))
refresh_software_status()
//Procs for using the various UI buttons for your softwares
/mob/living/silicon/pai/proc/directives()
touch_window("Directives")
/mob/living/silicon/pai/proc/crew_manifest()
touch_window("Crew Manifest")
/mob/living/silicon/pai/proc/med_records()
touch_window("Medical Records")
/mob/living/silicon/pai/proc/sec_records()
touch_window("Security Records")
/mob/living/silicon/pai/proc/remote_signal()
touch_window("Remote Signaler")
/mob/living/silicon/pai/proc/atmos_sensor()
touch_window("Atmosphere Sensor")
/mob/living/silicon/pai/proc/translator()
touch_window("Universal Translator")
/mob/living/silicon/pai/proc/door_jack()
touch_window("Door Jack")
/mob/living/silicon/pai/proc/ar_hud()
touch_window("AR HUD")
/mob/living/silicon/pai/proc/death_alarm()
touch_window("Death Alarm")
@@ -0,0 +1,219 @@
/// Change currently viewed camera
/mob/living/silicon/pai/proc/switchCamera(var/obj/machinery/camera/C)
if (!C)
src.reset_perspective()
return 0
if (stat == 2 || !C.status || !(src.network in C.network)) return 0
// ok, we're alive, camera is good and in our network...
src.current = C
src.AddComponent(/datum/component/remote_view, focused_on = C, viewsize = null, vconfig_path = /datum/remote_view_config/camera_standard)
return 1
/mob/living/silicon/pai/cancel_camera()
set category = "Abilities.pAI Commands"
set name = "Cancel Camera View"
reset_perspective()
/mob/living/silicon/pai/reset_perspective(atom/new_eye)
. = ..()
current = null
/mob/living/silicon/pai/verb/reset_record_view()
set category = "Abilities.pAI Commands"
set name = "Reset Records Software"
securityActive1 = null
securityActive2 = null
security_cannotfind = 0
medicalActive1 = null
medicalActive2 = null
medical_cannotfind = 0
SStgui.update_uis(src)
to_chat(src, span_notice("You reset your record-viewing software."))
/mob/living/silicon/pai/proc/choose_verbs()
set category = "Abilities.pAI Commands"
set name = "Choose Speech Verbs"
var/choice = tgui_input_list(src,"What theme would you like to use for your speech verbs?","Theme Choice", GLOB.possible_say_verbs)
if(!choice) return
var/list/sayverbs = GLOB.possible_say_verbs[choice]
speak_statement = sayverbs[1]
speak_exclamation = sayverbs[(sayverbs.len>1 ? 2 : sayverbs.len)]
speak_query = sayverbs[(sayverbs.len>2 ? 3 : sayverbs.len)]
/mob/living/silicon/pai/verb/allowmodification()
set name = "Change Access Modifcation Permission"
set category = "Abilities.pAI Commands"
set desc = "Allows people to modify your access or block people from modifying your access."
if(idaccessible == 0)
idaccessible = 1
visible_message(span_notice("\The [src] clicks as their access modification slot opens."),span_notice("You allow access modifications."), runemessage = "click")
else
idaccessible = 0
visible_message(span_notice("\The [src] clicks as their access modification slot closes."),span_notice("You block access modfications."), runemessage = "click")
/mob/living/silicon/pai/verb/toggle_gender_identity_vr()
set name = "Set Gender Identity"
set desc = "Sets the pronouns when examined and performing an emote."
set category = "IC.Settings"
var/new_gender_identity = tgui_input_list(src, "Please select a gender Identity:", "Set Gender Identity", list(FEMALE, MALE, NEUTER, PLURAL, HERM))
if(!new_gender_identity)
return 0
gender = new_gender_identity
return 1
/mob/living/silicon/pai/verb/pai_hide()
set name = "Hide"
set desc = "Allows to hide beneath tables or certain items. Toggled on or off."
set category = "Abilities.pAI"
hide()
if(status_flags & HIDING)
hide_glow = TRUE
else
hide_glow = FALSE
update_icon()
/mob/living/silicon/pai/verb/screen_message(message as text|null)
set category = "Abilities.pAI Commands"
set name = "Screen Message"
set desc = "Allows you to display a message on your screen. This will show up in the chat of anyone who is holding your card."
if (src.client)
if(client.prefs.muted & MUTE_IC)
to_chat(src, span_warning("You cannot speak in IC (muted)."))
return
if(loc != card)
to_chat(src, span_warning("Your message won't be visible while unfolded!"))
if (!message)
message = tgui_input_text(src, "Enter text you would like to show on your screen.","Screen Message", encode = FALSE)
message = sanitize_or_reflect(message,src)
if (!message)
return
message = capitalize(message)
if (stat == DEAD)
return
card.screen_msg = message
var/logmsg = "(CARD SCREEN)[message]"
log_talk(logmsg, LOG_SAY)
to_chat(src, span_filter_say(span_cult("You print a message to your screen, \"[message]\"")))
if(isliving(card.loc))
var/mob/living/L = card.loc
if(L.client)
to_chat(L, span_filter_say(span_cult("[src.name]'s screen prints, \"[message]\"")))
else return
else if(isbelly(card.loc))
var/obj/belly/b = card.loc
if(b.owner.client)
to_chat(b.owner, span_filter_say(span_cult("[src.name]'s screen prints, \"[message]\"")))
else return
else if(istype(card.loc, /obj/item/pda))
var/obj/item/pda/p = card.loc
if(isliving(p.loc))
var/mob/living/L = p.loc
if(L.client)
to_chat(L, span_filter_say(span_cult("[src.name]'s screen prints, \"[message]\"")))
else return
else if(isbelly(p.loc))
var/obj/belly/b = card.loc
if(b.owner.client)
to_chat(b.owner, span_filter_say(span_cult("[src.name]'s screen prints, \"[message]\"")))
else return
else return
else return
to_chat(src, span_notice("Your message was relayed."))
for (var/mob/G in GLOB.player_list)
if (isnewplayer(G))
continue
else if(isobserver(G) && G.client?.prefs?.read_preference(/datum/preference/toggle/ghost_ears))
if((client?.prefs?.read_preference(/datum/preference/toggle/whisubtle_vis) || check_rights_for(G.client, R_HOLDER)) && \
G.client?.prefs?.read_preference(/datum/preference/toggle/ghost_see_whisubtle))
to_chat(G, span_filter_say(span_cult("[src.name]'s screen prints, \"[message]\"")))
/mob/living/silicon/pai/proc/pai_nom(var/mob/living/T in oview(1))
set name = "pAI Nom"
set category = "Abilities.pAI Commands"
set desc = "Allows you to eat someone while unfolded. Can't be used while in card form."
if (stat != CONSCIOUS)
return
return feed_grabbed_to_self(src,T)
/mob/living/silicon/pai/verb/toggle_eyeglow()
set category = "Abilities.pAI Commands"
set name = "Toggle Eye Glow"
if(!SSpai.chassis_data(chassis_name).has_eye_sprites)
to_chat(src, span_filter_notice("Your selected chassis cannot modify its eye glow!"))
return
if(eye_glow && !hide_glow)
eye_glow = FALSE
else
eye_glow = TRUE
hide_glow = FALSE
update_icon()
/mob/living/silicon/pai/verb/pick_eye_color()
set category = "Abilities.pAI Commands"
set name = "Pick Eye Color"
if(!SSpai.chassis_data(chassis_name).has_eye_sprites)
to_chat(src, span_warning("Your selected chassis eye color can not be modified. The color you pick will only apply to supporting chassis and your card screen."))
return
var/new_eye_color = tgui_color_picker(src, "Choose your character's eye color:", "Eye Color")
if(new_eye_color)
eye_color = new_eye_color
update_icon()
card.setEmotion(card.current_emotion)
/mob/living/silicon/pai/proc/hug(var/mob/living/silicon/pai/H, var/mob/living/target)
var/t_him = "them"
if(ishuman(target))
var/mob/living/carbon/human/T = target
switch(T.identifying_gender)
if(MALE)
t_him = "him"
if(FEMALE)
t_him = "her"
if(NEUTER)
t_him = "it"
if(HERM)
t_him = "hir"
else
t_him = "them"
else
switch(target.gender)
if(MALE)
t_him = "him"
if(FEMALE)
t_him = "her"
if(NEUTER)
t_him = "it"
if(HERM)
t_him = "hir"
else
t_him = "them"
if(H.zone_sel.selecting == BP_HEAD)
H.visible_message( \
span_notice("[H] pats [target] on the head."), \
span_notice("You pat [target] on the head."), )
else if(H.zone_sel.selecting == BP_R_HAND || H.zone_sel.selecting == BP_L_HAND)
H.visible_message( \
span_notice("[H] shakes [target]'s hand."), \
span_notice("You shake [target]'s hand."), )
else if(H.zone_sel.selecting == "mouth")
H.visible_message( \
span_notice("[H] boops [target]'s nose."), \
span_notice("You boop [target] on the nose."), )
else
H.visible_message(span_notice("[H] hugs [target] to make [t_him] feel better!"), \
span_notice("You hug [target] to make [t_him] feel better!"))
playsound(src, 'sound/weapons/thudswoosh.ogg', 50, 1, -1)
@@ -1,659 +0,0 @@
/mob/living/silicon/pai
icon = 'icons/mob/pai_vr.dmi'
softfall = TRUE
var/eye_glow = TRUE
var/hide_glow = FALSE
var/image/eye_layer = null // Holds the eye overlay.
var/eye_color = "#00ff0d"
var/icon/holo_icon
var/icon/holo_icon_north
var/holo_icon_dimension_X = 32
var/holo_icon_dimension_Y = 32
//These vars keep track of whether you have the related software, used for easily updating the UI
var/soft_ut = FALSE //universal translator
var/soft_mr = FALSE //medical records
var/soft_sr = FALSE //security records
var/soft_dj = FALSE //door jack
var/soft_as = FALSE //atmosphere sensor
var/soft_si = FALSE //signaler
var/soft_ar = FALSE //ar hud
var/soft_da = FALSE //death alarm
vore_capacity = 1
vore_capacity_ex = list("stomach" = 1)
/mob/living/silicon/pai/Initialize(mapload)
. = ..()
add_verb(src, /mob/proc/dominate_predator)
add_verb(src, /mob/living/proc/dominate_prey)
add_verb(src, /mob/living/proc/set_size)
add_verb(src, /mob/living/proc/shred_limb)
/mob/living/silicon/pai/Login()
. = ..()
if(!holo_icon)
last_special = world.time + 100 //Let's give get_character_icon time to work
get_character_icon()
if(stat == DEAD)
healths.icon_state = "health7"
/mob/living/silicon/pai/proc/full_restore() //This is using do_after all kinds of weird...
adjustBruteLoss(- bruteloss)
adjustFireLoss(- fireloss)
do_after(src, 1 SECONDS, target = src)
card.setEmotion(16)
stat = CONSCIOUS
do_after(src, 5 SECONDS, target = src)
var/mob/observer/dead/ghost = src.get_ghost()
if(ghost)
ghost.notify_revive("Someone is trying to revive you. Re-enter your body if you want to be revived!", 'sound/effects/pai-restore.ogg', source = card)
canmove = TRUE
card.setEmotion(15)
playsound(card, 'sound/effects/pai-restore.ogg', 50, FALSE)
card.visible_message(span_filter_notice("\The [card] chimes."), runemessage = "chime")
/mob/living/silicon/pai/proc/pai_nom(var/mob/living/T in oview(1))
set name = "pAI Nom"
set category = "Abilities.pAI Commands"
set desc = "Allows you to eat someone while unfolded. Can't be used while in card form."
if (stat != CONSCIOUS)
return
return feed_grabbed_to_self(src,T)
/mob/living/silicon/pai/update_icon() //Some functions cause this to occur, such as resting
..()
if(chassis == "13")
icon = holo_icon
add_eyes()
return
update_fullness()
//Add a check when selecting a chassis if you add in support for this, to set vore_capacity to 2 or however many states you have.
var/fullness_extension = ""
if(vore_capacity > 1 && vore_fullness > 1)
fullness_extension = "_[vore_fullness]"
if(!vore_fullness && !resting)
icon_state = "[chassis]" //Using icon_state here resulted in quite a few bugs. Chassis is much less buggy.
else if(!vore_fullness && resting)
icon_state = "[chassis]_rest"
// Unfortunately not all these states exist, ugh.
else if(vore_fullness && !resting)
if(icon_exists(icon, "[chassis]_full[fullness_extension]"))
icon_state = "[chassis]_full[fullness_extension]"
else
icon_state = "[chassis]"
else if(vore_fullness && resting)
if(icon_exists(icon, "[chassis]_rest_full[fullness_extension]"))
icon_state = "[chassis]_rest_full[fullness_extension]"
else
icon_state = "[chassis]_rest"
if(chassis in GLOB.wide_chassis)
pixel_x = -16
default_pixel_x = -16
else
pixel_x = 0
default_pixel_x = 0
add_eyes()
/mob/living/silicon/pai/update_icons() //And other functions cause this to occur, such as digesting someone.
..()
if(chassis == "13")
icon = holo_icon
add_eyes()
return
update_fullness()
//Add a check when selecting a chassis if you add in support for this, to set vore_capacity to 2 or however many states you have.
var/fullness_extension = ""
if(vore_capacity > 1 && vore_fullness > 1)
fullness_extension = "_[vore_fullness]"
if(!vore_fullness && !resting)
icon_state = "[chassis]"
else if(!vore_fullness && resting)
icon_state = "[chassis]_rest"
else if(vore_fullness && !resting)
icon_state = "[chassis]_full[fullness_extension]"
else if(vore_fullness && resting)
icon_state = "[chassis]_rest_full[fullness_extension]"
if(chassis in GLOB.wide_chassis)
pixel_x = -16
default_pixel_x = -16
else
pixel_x = 0
default_pixel_x = 0
add_eyes()
//proc override to avoid pAI players being invisible while the chassis selection window is open
/mob/living/silicon/pai/proc/choose_chassis()
set category = "Abilities.pAI Commands"
set name = "Choose Chassis"
var/choice
choice = tgui_input_list(src, "What would you like to use for your mobile chassis icon?", "Chassis Choice", GLOB.possible_chassis)
if(!choice) return
var/oursize = size_multiplier
resize(1, FALSE, TRUE, TRUE, FALSE) //We resize ourselves to normal here for a moment to let the vis_height get reset
chassis = GLOB.possible_chassis[choice]
vore_capacity = 1
vore_capacity_ex = list("stomach" = 1)
if(chassis == "13")
if(!holo_icon)
if(!get_character_icon())
return
icon_state = null
icon = holo_icon
else if(chassis in GLOB.wide_chassis)
icon = 'icons/mob/pai_vr64x64.dmi'
vis_height = 64
else
icon = 'icons/mob/pai_vr.dmi'
vis_height = 32
resize(oursize, FALSE, TRUE, TRUE, FALSE) //And then back again now that we're sure the vis_height is correct.
if(chassis in GLOB.flying_chassis)
hovering = TRUE
else
hovering = FALSE
if(isopenspace(loc))
fall()
update_icon()
/mob/living/silicon/pai/verb/toggle_eyeglow()
set category = "Abilities.pAI Commands"
set name = "Toggle Eye Glow"
if(chassis in GLOB.allows_eye_color)
if(eye_glow && !hide_glow)
eye_glow = FALSE
else
eye_glow = TRUE
hide_glow = FALSE
update_icon()
else
to_chat(src, span_filter_notice("Your selected chassis cannot modify its eye glow!"))
return
/mob/living/silicon/pai/verb/pick_eye_color()
set category = "Abilities.pAI Commands"
set name = "Pick Eye Color"
if(!(chassis in GLOB.allows_eye_color))
to_chat(src, span_warning("Your selected chassis eye color can not be modified. The color you pick will only apply to supporting chassis and your card screen."))
var/new_eye_color = tgui_color_picker(src, "Choose your character's eye color:", "Eye Color")
if(new_eye_color)
eye_color = new_eye_color
update_icon()
card.setEmotion(card.current_emotion)
// Release belly contents before being gc'd!
/mob/living/silicon/pai/Destroy()
release_vore_contents()
if(ckey)
GLOB.paikeys -= ckey
return ..()
/mob/living/silicon/pai/clear_client()
if(ckey)
GLOB.paikeys -= ckey
return ..()
/mob/living/silicon/pai/proc/add_eyes()
remove_eyes()
if(chassis == "13")
if(holo_icon.Width() > 32)
holo_icon_dimension_X = 64
pixel_x = -16
default_pixel_x = -16
if(holo_icon.Height() > 32)
holo_icon_dimension_Y = 64
if(holo_icon_dimension_X == 32 && holo_icon_dimension_Y == 32)
eye_layer = image('icons/mob/pai_vr.dmi', "type13-eyes")
else if(holo_icon_dimension_X == 32 && holo_icon_dimension_Y == 64)
eye_layer = image('icons/mob/pai_vr32x64.dmi', "type13-eyes")
else if(holo_icon_dimension_X == 64 && holo_icon_dimension_Y == 32)
eye_layer = image('icons/mob/pai_vr64x32.dmi', "type13-eyes")
else if(holo_icon_dimension_X == 64 && holo_icon_dimension_Y == 64)
eye_layer = image('icons/mob/pai_vr64x64.dmi', "type13-eyes")
else if(chassis in GLOB.allows_eye_color)
eye_layer = image(icon, "[icon_state]-eyes")
else return
eye_layer.appearance_flags = appearance_flags
eye_layer.color = eye_color
if(eye_glow && !hide_glow)
eye_layer.plane = PLANE_LIGHTING_ABOVE
add_overlay(eye_layer)
/mob/living/silicon/pai/proc/remove_eyes()
cut_overlay(eye_layer)
qdel(eye_layer)
eye_layer = null
/mob/living/silicon/pai/UnarmedAttack(atom/A, proximity_flag)
. = ..()
if(istype(A,/obj/structure/ladder))
// Zmovement already allows these to be used with the verbs anyway
var/obj/structure/ladder/L = A
L.attack_hand(src)
return
if(!ismob(A) || A == src)
return
switch(a_intent)
if(I_HELP)
if(isliving(A))
hug(src, A)
if(I_GRAB)
pai_nom(A)
// Allow card inhabited machines to be interacted with
// This has to override ClickOn because of storage depth nonsense with how pAIs are in cards in GLOB.machines
/mob/living/silicon/pai/ClickOn(var/atom/A, var/params)
if(istype(A, /obj/machinery))
var/obj/machinery/M = A
if(M.paicard == card)
M.attack_ai(src)
return
return ..()
/mob/living/silicon/pai/proc/hug(var/mob/living/silicon/pai/H, var/mob/living/target)
var/t_him = "them"
if(ishuman(target))
var/mob/living/carbon/human/T = target
switch(T.identifying_gender)
if(MALE)
t_him = "him"
if(FEMALE)
t_him = "her"
if(NEUTER)
t_him = "it"
if(HERM)
t_him = "hir"
else
t_him = "them"
else
switch(target.gender)
if(MALE)
t_him = "him"
if(FEMALE)
t_him = "her"
if(NEUTER)
t_him = "it"
if(HERM)
t_him = "hir"
else
t_him = "them"
if(H.zone_sel.selecting == BP_HEAD)
H.visible_message( \
span_notice("[H] pats [target] on the head."), \
span_notice("You pat [target] on the head."), )
else if(H.zone_sel.selecting == BP_R_HAND || H.zone_sel.selecting == BP_L_HAND)
H.visible_message( \
span_notice("[H] shakes [target]'s hand."), \
span_notice("You shake [target]'s hand."), )
else if(H.zone_sel.selecting == "mouth")
H.visible_message( \
span_notice("[H] boops [target]'s nose."), \
span_notice("You boop [target] on the nose."), )
else
H.visible_message(span_notice("[H] hugs [target] to make [t_him] feel better!"), \
span_notice("You hug [target] to make [t_him] feel better!"))
playsound(src, 'sound/weapons/thudswoosh.ogg', 50, 1, -1)
/mob/living/silicon/pai/proc/savefile_path(mob/user)
return "data/player_saves/[copytext(user.ckey, 1, 2)]/[user.ckey]/pai.sav"
/mob/living/silicon/pai/proc/savefile_save(mob/user)
if(IsGuestKey(user.key))
return 0
var/savefile/F = new /savefile(src.savefile_path(user))
F["name"] << src.name
F["description"] << src.flavor_text
F["eyecolor"] << src.eye_color
F["chassis"] << src.chassis
F["emotion"] << src.card.current_emotion
F["gender"] << src.gender
F["version"] << 1
return 1
/mob/living/silicon/pai/proc/savefile_load(mob/user, var/silent = 1)
if (IsGuestKey(user.key))
return 0
var/path = savefile_path(user)
if (!fexists(path))
return 0
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_async(user, "Your savefile was incompatible with this version and was deleted.")
return 0
var/ourname
var/ouremotion
var/ourdesc
var/oureyes
var/ourchassis
var/ourgender
F["name"] >> ourname
F["description"] >> ourdesc
F["eyecolor"] >> oureyes
F["chassis"] >> ourchassis
F["emotion"] >> ouremotion
F["gender"] >> ourgender
if(ourname)
SetName(ourname)
if(ourdesc)
flavor_text = ourdesc
if(ourchassis)
chassis = ourchassis
if(ourgender)
gender = ourgender
if(oureyes)
card.screen_color = oureyes
eye_color = oureyes
if(ouremotion)
card.setEmotion(ouremotion)
update_icon()
return 1
/mob/living/silicon/pai/verb/save_pai_to_slot()
set category = "Abilities.pAI Commands"
set name = "Save Configuration"
savefile_save(src)
to_chat(src, span_filter_notice("[name] configuration saved to global pAI settings."))
/mob/living/silicon/pai/a_intent_change(input as text)
. = ..()
switch(a_intent)
if(I_HELP)
hud_used.help_intent.icon_state = "intent_help-s"
hud_used.disarm_intent.icon_state = "intent_disarm-n"
hud_used.grab_intent.icon_state = "intent_grab-n"
hud_used.hurt_intent.icon_state = "intent_harm-n"
if(I_DISARM)
hud_used.help_intent.icon_state = "intent_help-n"
hud_used.disarm_intent.icon_state = "intent_disarm-s"
hud_used.grab_intent.icon_state = "intent_grab-n"
hud_used.hurt_intent.icon_state = "intent_harm-n"
if(I_GRAB)
hud_used.help_intent.icon_state = "intent_help-n"
hud_used.disarm_intent.icon_state = "intent_disarm-n"
hud_used.grab_intent.icon_state = "intent_grab-s"
hud_used.hurt_intent.icon_state = "intent_harm-n"
if(I_HURT)
hud_used.help_intent.icon_state = "intent_help-n"
hud_used.disarm_intent.icon_state = "intent_disarm-n"
hud_used.grab_intent.icon_state = "intent_grab-n"
hud_used.hurt_intent.icon_state = "intent_harm-s"
/mob/living/silicon/pai/verb/toggle_gender_identity_vr()
set name = "Set Gender Identity"
set desc = "Sets the pronouns when examined and performing an emote."
set category = "IC.Settings"
var/new_gender_identity = tgui_input_list(src, "Please select a gender Identity:", "Set Gender Identity", list(FEMALE, MALE, NEUTER, PLURAL, HERM))
if(!new_gender_identity)
return 0
gender = new_gender_identity
return 1
/mob/living/silicon/pai/verb/pai_hide()
set name = "Hide"
set desc = "Allows to hide beneath tables or certain items. Toggled on or off."
set category = "Abilities.pAI"
hide()
if(status_flags & HIDING)
hide_glow = TRUE
else
hide_glow = FALSE
update_icon()
/mob/living/silicon/pai/verb/screen_message(message as text|null)
set category = "Abilities.pAI Commands"
set name = "Screen Message"
set desc = "Allows you to display a message on your screen. This will show up in the chat of anyone who is holding your card."
if (src.client)
if(client.prefs.muted & MUTE_IC)
to_chat(src, span_warning("You cannot speak in IC (muted)."))
return
if(loc != card)
to_chat(src, span_warning("Your message won't be visible while unfolded!"))
if (!message)
message = tgui_input_text(src, "Enter text you would like to show on your screen.","Screen Message", encode = FALSE)
message = sanitize_or_reflect(message,src)
if (!message)
return
message = capitalize(message)
if (stat == DEAD)
return
card.screen_msg = message
var/logmsg = "(CARD SCREEN)[message]"
log_talk(logmsg, LOG_SAY)
to_chat(src, span_filter_say(span_cult("You print a message to your screen, \"[message]\"")))
if(isliving(card.loc))
var/mob/living/L = card.loc
if(L.client)
to_chat(L, span_filter_say(span_cult("[src.name]'s screen prints, \"[message]\"")))
else return
else if(isbelly(card.loc))
var/obj/belly/b = card.loc
if(b.owner.client)
to_chat(b.owner, span_filter_say(span_cult("[src.name]'s screen prints, \"[message]\"")))
else return
else if(istype(card.loc, /obj/item/pda))
var/obj/item/pda/p = card.loc
if(isliving(p.loc))
var/mob/living/L = p.loc
if(L.client)
to_chat(L, span_filter_say(span_cult("[src.name]'s screen prints, \"[message]\"")))
else return
else if(isbelly(p.loc))
var/obj/belly/b = card.loc
if(b.owner.client)
to_chat(b.owner, span_filter_say(span_cult("[src.name]'s screen prints, \"[message]\"")))
else return
else return
else return
to_chat(src, span_notice("Your message was relayed."))
for (var/mob/G in GLOB.player_list)
if (isnewplayer(G))
continue
else if(isobserver(G) && G.client?.prefs?.read_preference(/datum/preference/toggle/ghost_ears))
if((client?.prefs?.read_preference(/datum/preference/toggle/whisubtle_vis) || check_rights_for(G.client, R_HOLDER)) && \
G.client?.prefs?.read_preference(/datum/preference/toggle/ghost_see_whisubtle))
to_chat(G, span_filter_say(span_cult("[src.name]'s screen prints, \"[message]\"")))
/mob/living/silicon/pai/proc/touch_window(soft_name) //This lets us touch TGUI procs and windows that may be nested behind other TGUI procs and windows
if(stat != CONSCIOUS) //so we can access our software without having to open up the software interface TGUI window
to_chat(src, span_warning("You can't do that right now."))
return
for(var/thing in software)
var/datum/pai_software/S = software[thing]
if(istype(S, /datum/pai_software) && S.name == soft_name)
if(S.toggle)
S.toggle(src)
to_chat(src, span_notice("You toggled [S.name]."))
refresh_software_status()
else
S.tgui_interact(src)
refresh_software_status()
return
for(var/thing in GLOB.pai_software_by_key)
var/datum/pai_software/our_soft = GLOB.pai_software_by_key[thing]
if(our_soft.name == soft_name)
if(!(ram >= our_soft.ram_cost))
to_chat(src, span_warning("Insufficient RAM for download. (Cost [our_soft.ram_cost] : [ram] Remaining)"))
return
if(tgui_alert(src, "Do you want to download [our_soft.name]? It costs [our_soft.ram_cost], and you have [ram] remaining.", "Download [our_soft.name]", list("Yes", "No")) == "Yes")
if(!(ram >= our_soft.ram_cost))
return
if(software[our_soft.id])
return
ram -= our_soft.ram_cost
software[our_soft.id] = our_soft
to_chat(src, span_notice("You downloaded [our_soft.name]. ([ram] RAM remaining.)"))
refresh_software_status()
/mob/living/silicon/pai/proc/refresh_software_status() //This manages the pAI software status buttons icon states based on if you have them and if they are enabled
for(var/thing in software) //this only gets called when you click one of the relevent buttons, rather than all the time!
var/datum/pai_software/soft = software[thing]
if(istype(soft,/datum/pai_software/med_records))
soft_mr = TRUE
if(istype(soft,/datum/pai_software/sec_records))
soft_sr = TRUE
if(istype(soft,/datum/pai_software/door_jack))
soft_dj = TRUE
if(istype(soft,/datum/pai_software/atmosphere_sensor))
soft_as = TRUE
if(istype(soft,/datum/pai_software/pai_hud))
soft_ar = TRUE
if(istype(soft,/datum/pai_software/translator))
soft_ut = TRUE
if(istype(soft,/datum/pai_software/signaller))
soft_si = TRUE
if(istype(soft,/datum/pai_software/deathalarm))
soft_da = TRUE
for(var/atom/movable/screen/pai/button in hud_used.other)
if(button.name == "medical records")
if(soft_mr)
button.icon_state = "[button.base_state]"
else
button.icon_state = "[button.base_state]_o"
if(button.name == "security records")
if(soft_sr)
button.icon_state = "[button.base_state]"
else
button.icon_state = "[button.base_state]_o"
if(button.name == "door jack")
if(soft_dj)
button.icon_state = "[button.base_state]"
else
button.icon_state = "[button.base_state]_o"
if(button.name == "atmosphere sensor")
if(soft_as)
button.icon_state = "[button.base_state]"
else
button.icon_state = "[button.base_state]_o"
if(button.name == "remote signaler")
if(soft_si)
button.icon_state = "[button.base_state]"
else
button.icon_state = "[button.base_state]_o"
if(button.name == "universal translator")
if(soft_ut && translator_on)
button.icon_state = "[button.base_state]"
else
button.icon_state = "[button.base_state]_o"
if(button.name == "ar hud")
if(soft_ar && paiHUD)
button.icon_state = "[button.base_state]"
else
button.icon_state = "[button.base_state]_o"
if(button.name == "death alarm")
if(soft_da && paiDA)
button.icon_state = "[button.base_state]"
else
button.icon_state = "[button.base_state]_o"
//Procs for using the various UI buttons for your softwares
/mob/living/silicon/pai/proc/directives()
touch_window("Directives")
/mob/living/silicon/pai/proc/crew_manifest()
touch_window("Crew Manifest")
/mob/living/silicon/pai/proc/med_records()
touch_window("Medical Records")
/mob/living/silicon/pai/proc/sec_records()
touch_window("Security Records")
/mob/living/silicon/pai/proc/remote_signal()
touch_window("Remote Signaler")
/mob/living/silicon/pai/proc/atmos_sensor()
touch_window("Atmosphere Sensor")
/mob/living/silicon/pai/proc/translator()
touch_window("Universal Translator")
/mob/living/silicon/pai/proc/door_jack()
touch_window("Door Jack")
/mob/living/silicon/pai/proc/ar_hud()
touch_window("AR HUD")
/mob/living/silicon/pai/proc/death_alarm()
touch_window("Death Alarm")
/mob/living/silicon/pai/proc/get_character_icon()
if(!client || !client.prefs) return FALSE
var/mob/living/carbon/human/dummy/dummy = new ()
//This doesn't include custom_items because that's ... hard.
client.prefs.dress_preview_mob(dummy)
sleep(1 SECOND) //Strange bug in preview code? Without this, certain things won't show up. Yay race conditions?
dummy.regenerate_icons()
var/icon/new_holo = getCompoundIcon(dummy)
dummy.tail_layering = TRUE
dummy.set_dir(NORTH)
var/icon/new_holo_north = getCompoundIcon(dummy)
qdel(holo_icon)
qdel(holo_icon_north)
qdel(dummy)
holo_icon = new_holo
holo_icon_north = new_holo_north
return TRUE
/mob/living/silicon/pai/set_dir(var/new_dir)
. = ..()
if(. && (chassis == "13"))
switch(dir)
if(SOUTH)
icon = holo_icon
else
icon = holo_icon_north
/mob/living/silicon/pai/adjustBruteLoss(amount, include_robo)
. = ..()
if(amount > 0 && health <= 90) //Something's probably attacking us!
if(prob(amount)) //The more damage it is doing, the more likely it is to damage something important!
card.damage_random_component()
/mob/living/silicon/pai/adjustFireLoss(amount, include_robo)
. = ..()
if(amount > 0 && health <= 90)
if(prob(amount))
card.damage_random_component()
@@ -1,65 +0,0 @@
/*
name
key
description
role
comments
ready = 0
*/
/datum/paiCandidate/proc/savefile_path(mob/user)
return "data/player_saves/[copytext(user.ckey, 1, 2)]/[user.ckey]/pai.sav"
/datum/paiCandidate/proc/savefile_save(mob/user)
if(IsGuestKey(user.key))
return 0
var/savefile/F = new /savefile(src.savefile_path(user))
F["name"] << src.name
F["description"] << src.description
F["role"] << src.role
F["comments"] << src.comments
F["version"] << 1
return 1
// loads the savefile corresponding to the mob's ckey
// if silent=true, report incompatible savefiles
// returns 1 if loaded (or file was incompatible)
// returns 0 if savefile did not exist
/datum/paiCandidate/proc/savefile_load(mob/user, var/silent = 1)
if (IsGuestKey(user.key))
return 0
var/path = savefile_path(user)
if (!fexists(path))
return 0
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_async(user, "Your savefile was incompatible with this version and was deleted.")
return 0
F["name"] >> src.name
F["description"] >> src.description
F["role"] >> src.role
F["comments"] >> src.comments
F["eyecolor"] >> src.eye_color
F["chassis"] >> src.chassis
F["emotion"] >> src.ouremotion
F["gender"] >> src.gender
return 1
@@ -1,398 +0,0 @@
//This file was auto-corrected by findeclaration.exe on 25.5.2012 20:42:33
// Recruiting observers to play as pAIs
var/datum/paiController/paiController // Global handler for pAI candidates
/datum/paiCandidate
var/name
var/key
var/description
var/role
var/comments
var/ready = 0
var/chassis
var/ouremotion
var/eye_color
var/gender
/hook/startup/proc/paiControllerSetup()
paiController = new /datum/paiController()
return 1
/datum/paiController
var/inquirer = null
var/list/pai_candidates = list()
var/list/asked = list()
var/askDelay = 10 * 60 * 1 // One minute [ms * sec * min]
/datum/paiController/Topic(href, href_list[])
if(href_list["download"])
var/datum/paiCandidate/candidate = locate(href_list["candidate"])
var/obj/item/paicard/card = locate(href_list["device"])
if(card.pai)
return
if(istype(card,/obj/item/paicard) && istype(candidate,/datum/paiCandidate))
var/mob/living/silicon/pai/pai = new(card)
pai.key = candidate.key
GLOB.paikeys |= pai.ckey
card.setPersonality(pai)
if(!candidate.name)
pai.SetName(pick(GLOB.ninja_names))
else
pai.SetName(candidate.name)
if(candidate.description)
pai.flavor_text = candidate.description
if(candidate.eye_color)
pai.eye_color = candidate.eye_color
card.screen_color = pai.eye_color
if(candidate.chassis)
pai.chassis = candidate.chassis
if(candidate.ouremotion)
card.setEmotion(candidate.ouremotion)
if(candidate.gender)
pai.gender = candidate.gender
pai.update_icon()
pai.real_name = pai.name
card.looking_for_personality = 0
if(pai.mind) update_antag_icons(pai.mind)
pai_candidates -= candidate
usr << browse(null, "window=findPai")
if(href_list["new"])
var/datum/paiCandidate/candidate = locate(href_list["candidate"])
var/option = href_list["option"]
var/t = ""
switch(option)
if("name")
t = sanitizeSafe(tgui_input_text(usr, "Enter a name for your pAI", "pAI Name", candidate.name, MAX_NAME_LEN, encode = FALSE), MAX_NAME_LEN)
if(t)
candidate.name = t
if("desc")
t = tgui_input_text(usr, "Enter a description for your pAI", "pAI Description", candidate.description, multiline = TRUE, prevent_enter = TRUE)
if(t)
candidate.description = sanitize(t, encode = FALSE)
if("role")
t = tgui_input_text(usr, "Enter a role for your pAI", "pAI Role", candidate.role)
if(t)
candidate.role = sanitize(t, encode = FALSE)
if("ooc")
t = tgui_input_text(usr, "Enter any OOC comments", "pAI OOC Comments", candidate.comments, multiline = TRUE, prevent_enter = TRUE)
if(t)
candidate.comments = sanitize(t, encode = FALSE)
if("save")
candidate.savefile_save(usr)
if("load")
candidate.savefile_load(usr)
//In case people have saved unsanitized stuff.
if(candidate.name)
candidate.name = sanitizeSafe(candidate.name, MAX_NAME_LEN)
if(candidate.description)
candidate.description = sanitize(candidate.description)
if(candidate.role)
candidate.role = sanitize(candidate.role)
if(candidate.comments)
candidate.comments = sanitize(candidate.comments)
if("submit")
if(candidate)
candidate.ready = 1
for(var/obj/item/paicard/p in GLOB.all_pai_cards)
if(p.looking_for_personality == 1)
p.alertUpdate()
usr << browse(null, "window=paiRecruit")
return
recruitWindow(usr, href_list["allow_submit"] != "0")
/datum/paiController/proc/recruitWindow(var/mob/M as mob, allowSubmit = 1)
var/datum/paiCandidate/candidate
for(var/datum/paiCandidate/c in pai_candidates)
if(!istype(c) || !istype(M))
break
if(c.key == M.key)
candidate = c
if(!candidate)
candidate = new /datum/paiCandidate()
candidate.key = M.key
pai_candidates.Add(candidate)
var/dat = ""
dat += {"
<style type="text/css">
body {
margin-top:5px;
font-family:Verdana;
color:white;
font-size:13px;
background-image:url('uiBackground.png');
background-repeat:repeat-x;
background-color:#272727;
background-position:center top;
}
table {
border-collapse:collapse;
font-size:13px;
}
th, td {
border: 1px solid #333333;
}
p.top {
background-color: none;
color: white;
}
tr.d0 td {
background-color: #c0c0c0;
color: black;
border:0px;
border: 1px solid #333333;
}
tr.d0 th {
background-color: none;
color: #4477E0;
text-align:right;
vertical-align:top;
width:120px;
border:0px;
}
tr.d1 td {
background-color: #555555;
color: white;
}
td.button {
border: 1px solid #161616;
background-color: #40628a;
}
td.desc {
font-weight:bold;
}
a {
color:#4477E0;
}
a.button {
color:white;
text-decoration: none;
}
</style>
"}
dat += {"
<body>
<b><font size="3px">pAI Personality Configuration</font></b>
<p class="top">Please configure your pAI personality's options. Remember, what you enter here could determine whether or not the user requesting a personality chooses you!</p>
<table>
<tr class="d0">
<th rowspan="2"><a href='byond://?src=\ref[src];option=name;new=1;allow_submit=[allowSubmit];candidate=\ref[candidate]'>Name</a>:</th>
<td class="desc">[candidate.name]&nbsp;</td>
</tr>
<tr class="d1">
<td>What you plan to call yourself. Suggestions: Any character name you would choose for a station character OR an AI.</td>
</tr>
<tr class="d0">
<th rowspan="2"><a href='byond://?src=\ref[src];option=desc;new=1;allow_submit=[allowSubmit];candidate=\ref[candidate]'>Description</a>:</th>
<td class="desc">[candidate.description]&nbsp;</td>
</tr>
<tr class="d1">
<td>What sort of pAI you typically play; your mannerisms, your quirks, etc. This can be as sparse or as detailed as you like.</td>
</tr>
<tr class="d0">
<th rowspan="2"><a href='byond://?src=\ref[src];option=role;new=1;allow_submit=[allowSubmit];candidate=\ref[candidate]'>Preferred Role</a>:</th>
<td class="desc">[candidate.role]&nbsp;</td>
</tr>
<tr class="d1">
<td>Do you like to partner with sneaky social ninjas? Like to help security hunt down thugs? Enjoy watching an engineer's back while he saves the station yet again? This doesn't have to be limited to just station jobs. Pretty much any general descriptor for what you'd like to be doing works here.</td>
</tr>
<tr class="d0">
<th rowspan="2"><a href='byond://?src=\ref[src];option=ooc;new=1;allow_submit=[allowSubmit];candidate=\ref[candidate]'>OOC Comments</a>:</th>
<td class="desc">[candidate.comments]&nbsp;</td>
</tr>
<tr class="d1">
<td>Anything you'd like to address specifically to the player reading this in an OOC manner. \"I prefer more serious RP.\", \"I'm still learning the interface!\", etc. Feel free to leave this blank if you want.</td>
</tr>
</table>
<br>
<table>
<tr>
<td class="button">
<a href='byond://?src=\ref[src];option=save;new=1;allow_submit=[allowSubmit];candidate=\ref[candidate]' class="button">Save Personality</a>
</td>
</tr>
<tr>
<td class="button">
<a href='byond://?src=\ref[src];option=load;new=1;allow_submit=[allowSubmit];candidate=\ref[candidate]' class="button"><b><font size="3px">Load Personality</font></b></a>
</td>
</tr>
</table><br>
"}
if(allowSubmit)
dat += {"
<table>
<td class="button"><a href='byond://?src=\ref[src];option=submit;new=1;candidate=\ref[candidate]' class="button"><b><font size="4px">Submit Personality</font></b></a></td>
</table><br>
"}
dat += {"
</body>
"}
M << browse("<html>[dat]</html>", "window=paiRecruit;size=580x580;")
/datum/paiController/proc/findPAI(var/obj/item/paicard/p, var/mob/user)
requestRecruits(user)
var/list/available = list()
for(var/datum/paiCandidate/c in paiController.pai_candidates)
if(c.ready)
var/found = 0
for(var/mob/observer/dead/o in GLOB.player_list)
if(o.key == c.key && o.MayRespawn())
found = 1
if(found)
available.Add(c)
var/dat = ""
dat += {"
<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">
<html>
<head>
<style>
body {
margin-top:5px;
font-family:Verdana;
color:white;
font-size:13px;
background-image:url('uiBackground.png');
background-repeat:repeat-x;
background-color:#272727;
background-position:center top;
}
table {
font-size:13px;
}
table.desc {
border-collapse:collapse;
font-size:13px;
border: 1px solid #161616;
width:100%;
}
table.download {
border-collapse:collapse;
font-size:13px;
border: 1px solid #161616;
width:100%;
}
tr.d0 td, tr.d0 th {
background-color: #506070;
color: white;
}
tr.d1 td, tr.d1 th {
background-color: #708090;
color: white;
}
tr.d2 td {
background-color: #00FF00;
color: white;
text-align:center;
}
td.button {
border: 1px solid #161616;
background-color: #40628a;
text-align: center;
}
td.download {
border: 1px solid #161616;
background-color: #40628a;
text-align: center;
}
th {
text-align:left;
width:125px;
vertical-align:top;
}
a.button {
color:white;
text-decoration: none;
}
</style>
</head>
<body>
<b><font size='3px'>pAI Availability List</font></b><br><br>
"}
dat += "<p>Displaying available AI personalities from central database... If there are no entries, or if a suitable entry is not listed, check again later as more personalities may be added.</p>"
for(var/datum/paiCandidate/c in available)
dat += {"
<table class="desc">
<tr class="d0">
<th>Name:</th>
<td>[c.name]</td>
</tr>
<tr class="d1">
<th>Description:</th>
<td>[c.description]</td>
</tr>
<tr class="d0">
<th>Preferred Role:</th>
<td>[c.role]</td>
</tr>
<tr class="d1">
<th>OOC Comments:</th>
<td>[c.comments]</td>
</tr>
</table>
<table class="download">
<td class="download"><a href='byond://?src=\ref[src];download=1;candidate=\ref[c];device=\ref[p]' class="button"><b>Download [c.name]</b></a>
</td>
</table>
<br>
"}
dat += {"
</body>
</html>
"}
user << browse(dat, "window=findPai")
/datum/paiController/proc/requestRecruits(var/mob/user)
inquirer = user
for(var/mob/observer/dead/O in GLOB.player_list)
if(!O.MayRespawn())
continue
if(jobban_isbanned(O, "pAI"))
continue
if(asked.Find(O.key))
if(world.time < asked[O.key] + askDelay)
continue
else
asked.Remove(O.key)
if(O.client)
if(O.client.prefs.be_special & BE_PAI)
question(O.client)
/datum/paiController/proc/question(var/client/C)
spawn(0)
if(!C) return
asked.Add(C.key)
asked[C.key] = world.time
var/mob/ourmob = C.mob
if(ourmob)
var/time_till_respawn = ourmob.time_till_respawn()
if(time_till_respawn == -1 || time_till_respawn)
return
for(var/ourkey in GLOB.paikeys)
if(ourkey == ourmob.ckey)
return
var/response = tgui_alert(C, "[inquirer] is requesting a pAI personality. Would you like to play as a personal AI?", "pAI Request", list("Yes", "No", "Never for this round"))
if(!C) return //handle logouts that happen whilst the alert is waiting for a response.
if(response == "Yes")
recruitWindow(C.mob)
else if (response == "Never for this round")
C.prefs.be_special ^= BE_PAI
@@ -1,39 +1,3 @@
var/list/pai_emotions = list(
"Neutral" = 1,
"What" = 2,
"Happy" = 3,
"Cat" = 4,
"Extremely Happy" = 5,
"Face" = 6,
"Laugh" = 7,
"Sad" = 8,
"Angry" = 9,
"Silly" = 10,
"Nose" = 11,
"Smirk" = 12,
"Exclamation Points" = 13,
"Question Mark" = 14,
"Blank" = 15,
"Off" = 16
)
GLOBAL_LIST_EMPTY(pai_software_by_key)
GLOBAL_LIST_EMPTY(default_pai_software)
/hook/startup/proc/populate_pai_software_list()
var/r = 1 // I would use ., but it'd sacrifice runtime detection
for(var/type in subtypesof(/datum/pai_software))
var/datum/pai_software/P = new type()
if(GLOB.pai_software_by_key[P.id])
var/datum/pai_software/O = GLOB.pai_software_by_key[P.id]
to_chat(world, span_warning("pAI software module [P.name] has the same key as [O.name]!"))
r = 0
continue
GLOB.pai_software_by_key[P.id] = P
if(P.default)
GLOB.default_pai_software[P.id] = P
return r
/mob/living/silicon/pai/Initialize(mapload)
. = ..()
software = GLOB.default_pai_software.Copy()
@@ -79,11 +43,12 @@ GLOBAL_LIST_EMPTY(default_pai_software)
// Emotions
var/list/emotions = list()
for(var/name in pai_emotions)
var/list/emote = list()
emote["name"] = name
emote["id"] = pai_emotions[name]
emotions.Add(list(emote))
for(var/name in GLOB.pai_emotions)
var/list/emote = list(
"displayText" = name,
"value" = GLOB.pai_emotions[name]
)
UNTYPED_LIST_ADD(emotions, emote)
data["emotions"] = emotions
data["current_emotion"] = card.current_emotion
@@ -114,6 +79,6 @@ GLOBAL_LIST_EMPTY(default_pai_software)
if("image")
var/img = text2num(params["image"])
if(1 <= img && img <= (pai_emotions.len))
if(1 <= img && img <= (GLOB.pai_emotions.len))
card.setEmotion(img)
return TRUE
@@ -0,0 +1,209 @@
/datum/pai_sprite
var/name = DEVELOPER_WARNING_NAME
var/hidden = FALSE
var/sprite_icon = null
var/sprite_icon_state = null
var/has_eye_sprites = TRUE
var/flying = FALSE
var/belly_states = 1
var/can_rest = TRUE // We really shouldn't be permitting this... eventually. Remove when all sprite can rest.
var/resting_belly = TRUE
var/holo_projector = FALSE // Special AI hologram-like handling
var/holo_eyes_icon_state = null
var/pixel_x = 0
var/pixel_y = 0
var/icon_x = 32
var/icon_y = 32
var/vis_height = 32
// PAI sprites
/datum/pai_sprite/drone
name = PAI_DEFAULT_CHASSIS
sprite_icon = 'icons/mob/pai.dmi'
sprite_icon_state = "pai-repairbot"
/datum/pai_sprite/cat
name = "Cat"
sprite_icon = 'icons/mob/pai.dmi'
sprite_icon_state = "pai-cat"
/datum/pai_sprite/mouse
name = "Mouse"
sprite_icon = 'icons/mob/pai.dmi'
sprite_icon_state = "pai-mouse"
/datum/pai_sprite/monkey
name = "Monkey"
sprite_icon = 'icons/mob/pai.dmi'
sprite_icon_state = "pai-monkey"
/datum/pai_sprite/borgi
name = "Borgi"
sprite_icon = 'icons/mob/pai.dmi'
sprite_icon_state = "pai-borgi"
has_eye_sprites = FALSE
/datum/pai_sprite/fox
name = "Fox"
sprite_icon = 'icons/mob/pai.dmi'
sprite_icon_state = "pai-fox"
has_eye_sprites = FALSE
/datum/pai_sprite/parrot
name = "Parrot"
sprite_icon = 'icons/mob/pai.dmi'
sprite_icon_state = "pai-parrot"
has_eye_sprites = FALSE
flying = TRUE
/datum/pai_sprite/rabbit
name = "Rabbit"
sprite_icon = 'icons/mob/pai.dmi'
sprite_icon_state = "pai-rabbit"
has_eye_sprites = FALSE
/datum/pai_sprite/bear
name = "Bear"
sprite_icon = 'icons/mob/pai.dmi'
sprite_icon_state = "pai-bear"
/datum/pai_sprite/fennec
name = "Fennec"
sprite_icon = 'icons/mob/pai.dmi'
sprite_icon_state = "pai-fen"
belly_states = 0
/datum/pai_sprite/type_zero
name = "Type Zero"
sprite_icon = 'icons/mob/pai.dmi'
sprite_icon_state = "pai-typezero"
/datum/pai_sprite/raccoon
name = "Raccoon"
sprite_icon = 'icons/mob/pai.dmi'
sprite_icon_state = "pai-raccoon"
/datum/pai_sprite/raptor
name = "Raptor"
sprite_icon = 'icons/mob/pai.dmi'
sprite_icon_state = "pai-raptor"
/datum/pai_sprite/corgi
name = "Corgi"
sprite_icon = 'icons/mob/pai.dmi'
sprite_icon_state = "pai-corgi"
has_eye_sprites = FALSE
/datum/pai_sprite/bat
name = "Bat"
sprite_icon = 'icons/mob/pai.dmi'
sprite_icon_state = "pai-bat"
flying = TRUE
/datum/pai_sprite/butterfly
name = "Butterfly"
sprite_icon = 'icons/mob/pai.dmi'
sprite_icon_state = "pai-butterfly"
flying = TRUE
belly_states = 0
/datum/pai_sprite/hawk
name = "Hawk"
sprite_icon = 'icons/mob/pai.dmi'
sprite_icon_state = "pai-hawk"
has_eye_sprites = FALSE
flying = TRUE
belly_states = 0
/datum/pai_sprite/duffel
name = "Duffel"
sprite_icon = 'icons/mob/pai.dmi'
sprite_icon_state = "pai-duffel"
has_eye_sprites = FALSE
/datum/pai_sprite/cyber_elf
name = "Cyber Elf"
sprite_icon = 'icons/mob/pai.dmi'
sprite_icon_state = "cyberelf"
flying = TRUE
resting_belly = FALSE
/datum/pai_sprite/catslug
name = "Catslug"
sprite_icon = 'icons/mob/pai.dmi'
sprite_icon_state = "catslug"
resting_belly = FALSE
/datum/pai_sprite/car
name = "Car"
sprite_icon = 'icons/mob/pai.dmi'
sprite_icon_state = "car"
/datum/pai_sprite/type_one
name = "Type One"
sprite_icon = 'icons/mob/pai.dmi'
sprite_icon_state = "typeone"
/datum/pai_sprite/type_thirteen
name = "Type Thirteen"
sprite_icon = 'icons/mob/pai.dmi'
holo_projector = TRUE
sprite_icon_state = "type13"
holo_eyes_icon_state = "type13-eyes"
// Large pai sprites
/datum/pai_sprite/large
pixel_x = -16
icon_x = 64
icon_y = 64
vis_height = 64
/datum/pai_sprite/large/dire_wolf
name = "Dire wolf"
sprite_icon = 'icons/mob/pai64x64.dmi'
sprite_icon_state = "pai-diredog"
/datum/pai_sprite/large/horse_lune
name = "Horse (Lune)"
sprite_icon = 'icons/mob/pai64x64.dmi'
sprite_icon_state = "pai-horse_lune"
can_rest = FALSE // FIX ME!
resting_belly = FALSE
/datum/pai_sprite/large/horse_soleil
name = "Horse (Soleil)"
sprite_icon = 'icons/mob/pai64x64.dmi'
sprite_icon_state = "pai-horse_soleil"
can_rest = FALSE // FIX ME!
resting_belly = FALSE
/datum/pai_sprite/large/dragon
name = "Dragon"
sprite_icon = 'icons/mob/pai64x64.dmi'
sprite_icon_state = "pai-pdragon"
can_rest = FALSE // FIX ME!
resting_belly = FALSE
/datum/pai_sprite/large/rat
name = "Rat"
sprite_icon = 'icons/mob/pai64x64.dmi'
sprite_icon_state = "rat"
/datum/pai_sprite/large/panther
name = "Panther"
sprite_icon = 'icons/mob/pai64x64.dmi'
sprite_icon_state = "panther"
/datum/pai_sprite/large/teppi
name = "Teppi"
sprite_icon = 'icons/mob/pai64x64.dmi'
sprite_icon_state = "teppi"
/datum/pai_sprite/large/protogen_dog
name = "Protogen Dog"
sprite_icon = 'icons/mob/pai64x64.dmi'
sprite_icon_state = "pai-protodog"
@@ -84,7 +84,7 @@
model_type = "wide"
if(istype(S, /datum/robot_sprite/dogborg/tall))
model_type = "tall"
available_sprites += list(list("sprite" = S.name, "belly" = S.has_vore_belly_sprites, "type" = model_type))
UNTYPED_LIST_ADD(available_sprites, list("sprite" = S.name, "belly" = S.has_vore_belly_sprites, "type" = model_type))
data["possible_sprites"] = available_sprites
data["sprite_datum"] = sprite_datum
@@ -141,6 +141,11 @@
QDEL_NULL(mind)
qdel(src)
// pAI notify if we have be pAI invite on
SSpai.clear_pai_block_delay(REF(observer)) // Reset invite cooldown if we cancelled all invites for the round
if(SSpai.invite_valid(observer))
observer.pai_card_ping()
return TRUE
if("shownews")
handle_server_news()
+3 -3
View File
@@ -363,8 +363,8 @@
//Eye color/icon
var/should_have_eyes = owner.should_have_organ(O_EYES)
var/has_eye_color = owner.species.appearance_flags & HAS_EYE_COLOR
if((should_have_eyes || has_eye_color) && eye_icon)
var/has_eye_sprites = owner.species.appearance_flags & HAS_EYE_COLOR
if((should_have_eyes || has_eye_sprites) && eye_icon)
var/obj/item/organ/internal/eyes/eyes = owner.internal_organs_by_name[O_EYES]
var/icon/eyes_icon = new/icon(eye_icon_location, eye_icon)
//Do we have a special eye icon with its own coloration? Remove
@@ -373,7 +373,7 @@
if(should_have_eyes)
//And we have them
if(eyes)
if(has_eye_color)
if(has_eye_sprites)
eyes_icon.Blend(rgb(eyes.eye_colour[1], eyes.eye_colour[2], eyes.eye_colour[3]), ICON_ADD)
//They're gone!
else
@@ -49,6 +49,9 @@
return
tgui_interact(user)
/obj/machinery/computer/power_monitor/allow_pai_interaction(proximity_flag)
return proximity_flag
// Uses dark magic to operate the NanoUI of this computer.
/obj/machinery/computer/power_monitor/tgui_interact(mob/user, var/datum/tgui/ui = null)
power_monitor.tgui_interact(user, ui)
+3
View File
@@ -342,6 +342,9 @@
to_chat(user, "You adjust the speed of the conveyor switch")
return
/obj/machinery/conveyor_switch/allow_pai_interaction(proximity_flag)
return proximity_flag
/obj/machinery/conveyor_switch/oneway
oneway = 1
@@ -170,6 +170,9 @@
user.visible_message("[user] places \the [I] into the [src].", "You place \the [I] into the [src].","Ca-Clunk")
update_icon()
/obj/machinery/disposal/allow_pai_interaction(proximity_flag)
return proximity_flag
// Transform into next machine type
/obj/machinery/disposal/proc/alter_bin_type(mob/user)
if(contents.len > 0)
+2 -2
View File
@@ -124,10 +124,10 @@
// Emotions
var/list/emotions = list()
for(var/name in pai_emotions)
for(var/name in GLOB.pai_emotions)
var/list/emote = list()
emote["name"] = name
emote["id"] = pai_emotions[name]
emote["id"] = GLOB.pai_emotions[name]
emotions.Add(list(emote))
data["emotions"] = emotions
+5 -1
View File
@@ -72,7 +72,11 @@ GLOBAL_DATUM_INIT(tgui_default_state, /datum/tgui_state/default, new)
. = min(., shared_living_tgui_distance(src_object)) //simple animals can only use things they're near.
/mob/living/silicon/pai/default_can_use_tgui_topic(src_object)
// pAIs can only use themselves and the owner's radio.
// Allows few objects...
var/obj/check_obj = src_object
if(!stat && istype(check_obj) && check_obj.allow_pai_interaction(get_dist(check_obj, src) <= 1))
return STATUS_INTERACTIVE
// ...otherwise pAIs can only use themselves and the owner's radio.
if((src_object == src || src_object == radio || src_object == communicator) && !stat)
return STATUS_INTERACTIVE
else
@@ -97,6 +97,9 @@
return
lift.queue_move_to(floor)
/obj/structure/lift/button/allow_pai_interaction(proximity_flag)
return proximity_flag
/obj/structure/lift/button/proc/light_up()
light_up = TRUE
update_icon()
@@ -140,6 +143,9 @@
/obj/structure/lift/panel/attack_ghost(var/mob/user)
return interact(user)
/obj/structure/lift/panel/allow_pai_interaction(proximity_flag)
return proximity_flag
/obj/structure/lift/panel/interact(var/mob/user)
if(!..())
return
+2
View File
@@ -106,7 +106,9 @@
#include "mapping.dm"
#include "material_tests.dm"
// #include "nuke_cinematic.dm" // TODO: This is probably fixed later on
#include "pai_tests.dm"
#include "poster_tests.dm"
#include "special_roles.dm"
// #include "preferences.dm" // This unit test is missing some other stuff
#include "reagent_tests.dm"
#include "recipe_tests.dm"
+95
View File
@@ -0,0 +1,95 @@
/datum/unit_test/pai_software_shall_have_unique_id
/datum/unit_test/pai_software_shall_have_unique_id/Run()
for(var/datum/pai_software/P as anything in subtypesof(/datum/pai_software))
// Compare against all other pai softwares
for(var/datum/pai_software/O as anything in subtypesof(/datum/pai_software))
if(P == O) // Don't test against ourselves
continue
if(initial(P.id) == initial(O.id))
TEST_FAIL("pAI software module [P] has the same id as [O]!")
/datum/unit_test/pai_sprites_shall_be_valid
/datum/unit_test/pai_sprites_shall_be_valid/Run()
var/failed = FALSE
var/list/used_names = list()
for(var/sprite_key in SSpai.get_chassis_list())
var/datum/pai_sprite/sprite = SSpai.chassis_data(sprite_key)
if(sprite.sprite_icon == null)
continue
if(sprite.name in used_names)
TEST_NOTICE(src, "[sprite.type] - Pai sprite name is not unique: \"[sprite.name]\"")
failed = TRUE
used_names.Add(sprite.name)
if(!sprite.name || sprite.name == DEVELOPER_WARNING_NAME)
TEST_NOTICE(src, "[sprite.type] - Pai sprite had invalid name: \"[sprite.name]\"")
failed = TRUE
if(!sprite.holo_projector)
// check if we have valid icons
if(!sprite.sprite_icon_state)
TEST_NOTICE(src, "[sprite.type] - Pai sprite had no iconstate assigned")
failed = TRUE
if(!icon_exists(sprite.sprite_icon, sprite.sprite_icon_state))
TEST_NOTICE(src, "[sprite.type] - Pai sprite state \"[sprite.sprite_icon_state]\" did not exist in the dmi set \"[sprite.sprite_icon]\"")
failed = TRUE
if(sprite.can_rest)
if(!icon_exists(sprite.sprite_icon, "[sprite.sprite_icon_state]_rest"))
TEST_NOTICE(src, "[sprite.type] - Pai sprite state \"[sprite.sprite_icon_state]_rest\" did not exist in the dmi set \"[sprite.sprite_icon]\"")
failed = TRUE
// Check for vore stuff
if(sprite.belly_states > 0)
for(var/i = 1 to sprite.belly_states)
var/fullness_extension = ""
if(i > 1)
fullness_extension = "_[i]"
// standing bellies
if(!icon_exists(sprite.sprite_icon, "[sprite.sprite_icon_state]_full[fullness_extension]"))
TEST_NOTICE(src, "[sprite.type] - Pai sprite state \"[sprite.sprite_icon_state]_full[fullness_extension]\" did not exist in the dmi set \"[sprite.sprite_icon]\"")
failed = TRUE
if(sprite.can_rest && sprite.resting_belly)
// resting bellies
if(!icon_exists(sprite.sprite_icon, "[sprite.sprite_icon_state]_rest_full[fullness_extension]"))
TEST_NOTICE(src, "[sprite.type] - Pai sprite state \"[sprite.sprite_icon_state]_rest_full[fullness_extension]\" did not exist in the dmi set \"[sprite.sprite_icon]\"")
failed = TRUE
// Does it have colorable eyes?
if(sprite.has_eye_sprites)
if(!icon_exists(sprite.sprite_icon, "[sprite.sprite_icon_state]-eyes"))
TEST_NOTICE(src, "[sprite.type] - Pai sprite state \"[sprite.sprite_icon_state]-eyes\" did not exist in the dmi set \"[sprite.sprite_icon]\"")
failed = TRUE
if(sprite.can_rest)
if(!icon_exists(sprite.sprite_icon, "[sprite.sprite_icon_state]_rest-eyes"))
TEST_NOTICE(src, "[sprite.type] - Pai sprite state \"[sprite.sprite_icon_state]_rest-eyes\" did not exist in the dmi set \"[sprite.sprite_icon]\"")
failed = TRUE
// Death
if(!icon_exists(sprite.sprite_icon, "[sprite.sprite_icon_state]_dead"))
TEST_NOTICE(src, "[sprite.type] - Pai sprite state \"[sprite.sprite_icon_state]_dead\" did not exist in the dmi set \"[sprite.sprite_icon]\"")
failed = TRUE
else // Holoprojector uses ai holo instead of a sprite
// Holoprojectil eyes need to exist at all sizes for the state provided
if(sprite.holo_eyes_icon_state)
if(!icon_exists('icons/mob/pai.dmi', sprite.holo_eyes_icon_state))
TEST_NOTICE(src, "[sprite.type] - Pai holoprojection eyes state \"[sprite.holo_eyes_icon_state]\" did not exist in the dmi set \"['icons/mob/pai.dmi']\"")
failed = TRUE
if(!icon_exists('icons/mob/pai32x64.dmi', sprite.holo_eyes_icon_state))
TEST_NOTICE(src, "[sprite.type] - Pai holoprojection eyes state \"[sprite.holo_eyes_icon_state]\" did not exist in the dmi set \"['icons/mob/pai32x64.dmi']\"")
failed = TRUE
if(!icon_exists('icons/mob/pai64x32.dmi', sprite.holo_eyes_icon_state))
TEST_NOTICE(src, "[sprite.type] - Pai holoprojection eyes state \"[sprite.holo_eyes_icon_state]\" did not exist in the dmi set \"['icons/mob/pai64x32.dmi']\"")
failed = TRUE
if(!icon_exists('icons/mob/pai64x64.dmi', sprite.holo_eyes_icon_state))
TEST_NOTICE(src, "[sprite.type] - Pai holoprojection eyes state \"[sprite.holo_eyes_icon_state]\" did not exist in the dmi set \"['icons/mob/pai64x64.dmi']\"")
failed = TRUE
if(failed)
TEST_FAIL("PAI sprites had invalid entries.")
+13
View File
@@ -0,0 +1,13 @@
/datum/unit_test/special_role_flags_syncronized
/datum/unit_test/special_role_flags_syncronized/Run()
if(GLOB.special_roles.len != GLOB.be_special_flags.len)
TEST_FAIL("GLOB.special_roles and GLOB.be_special_flags lengths do not match. [GLOB.special_roles] vs [GLOB.be_special_flags].")
// These get out of order way too often.
var/index = 1
for(var/key in GLOB.special_roles)
var/check_key = GLOB.be_special_flags[index]
if(lowertext(check_key) != lowertext(key))
TEST_FAIL("Special role flag misalignment index: [index], be_special_flags: [check_key], special_roles: [key]. Keys must also match when caste to lowercase.")
index++