From 7c84aaee4b019252567a243d2a378633adc51432 Mon Sep 17 00:00:00 2001
From: Will <7099514+Willburd@users.noreply.github.com>
Date: Tue, 3 Mar 2026 17:59:27 -0500
Subject: [PATCH] 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>
---
code/__defines/gamemode.dm | 2 +-
code/__defines/preferences.dm | 3 +
code/_helpers/global_lists.dm | 88 +-
code/_helpers/text.dm | 2 +-
code/controllers/admin.dm | 1 -
code/controllers/subsystems/pai.dm | 224 ++++
code/game/machinery/atmo_control.dm | 3 +
code/game/machinery/buttons.dm | 3 +
code/game/machinery/computer/atmos_alert.dm | 3 +
code/game/machinery/computer/camera.dm | 3 +
code/game/machinery/computer/crew.dm | 3 +
code/game/machinery/computer/station_alert.dm | 3 +
code/game/machinery/doors/airlock_control.dm | 5 +
code/game/machinery/jukebox.dm | 3 +
code/game/machinery/lightswitch.dm | 3 +
code/game/machinery/newscaster.dm | 3 +
code/game/objects/items/devices/paicard.dm | 958 ++++++++++++------
code/game/objects/items/devices/paicard_vr.dm | 386 -------
code/game/objects/objs.dm | 4 +
.../structures/crates_lockers/__closets.dm | 3 +
code/game/objects/structures/fence.dm | 3 +
code/game/objects/structures/watercloset.dm | 3 +
code/modules/admin/verbs/debug.dm | 6 +-
.../assets/spritesheets/pai_icons.dm | 64 ++
.../assets/spritesheets/robot_icons.dm | 8 +-
.../preference_setup/general/07_antagonism.dm | 75 +-
.../preferences/migrations/19_paifile.dm | 57 ++
.../types/character/pai/01_basic.dm | 116 +++
code/modules/client/preferences_savefile.dm | 12 +-
code/modules/client/preferences_tgui.dm | 1 +
.../emotes/definitions/audible_furry_vr.dm | 4 +-
code/modules/instruments/stationary.dm | 3 +
code/modules/mining/shelters_vr.dm | 2 +-
code/modules/mob/dead/observer/observer.dm | 40 +-
code/modules/mob/living/login.dm | 2 +-
.../mob/living/silicon/pai/chassis_ui.dm | 69 ++
code/modules/mob/living/silicon/pai/death.dm | 3 +-
code/modules/mob/living/silicon/pai/life.dm | 13 +-
code/modules/mob/living/silicon/pai/pai.dm | 757 ++++++++------
.../mob/living/silicon/pai/pai_folding.dm | 148 +++
.../modules/mob/living/silicon/pai/pai_hud.dm | 3 -
.../mob/living/silicon/pai/pai_software.dm | 136 +++
.../mob/living/silicon/pai/pai_verbs.dm | 219 ++++
code/modules/mob/living/silicon/pai/pai_vr.dm | 659 ------------
.../mob/living/silicon/pai/personality.dm | 65 --
.../modules/mob/living/silicon/pai/recruit.dm | 398 --------
.../mob/living/silicon/pai/software.dm | 49 +-
.../silicon/pai/sprites/_sprite_datum.dm | 209 ++++
.../living/silicon/robot/robot_ui_module.dm | 2 +-
code/modules/mob/new_player/lobby_browser.dm | 5 +
code/modules/organs/subtypes/standard.dm | 6 +-
.../power/sensors/sensor_monitoring.dm | 3 +
code/modules/recycling/conveyor2.dm | 3 +
code/modules/recycling/disposal_machines.dm | 3 +
code/modules/resleeving/sleevecard.dm | 4 +-
code/modules/tgui/states/default.dm | 6 +-
code/modules/turbolift/turbolift_console.dm | 6 +
code/modules/unit_tests/_unit_tests.dm | 2 +
code/modules/unit_tests/pai_tests.dm | 95 ++
code/modules/unit_tests/special_roles.dm | 13 +
icons/mob/pai.dmi | Bin 60806 -> 192146 bytes
icons/mob/{pai_vr32x64.dmi => pai32x64.dmi} | Bin
icons/mob/pai64x32.dmi | Bin 0 -> 507 bytes
icons/mob/pai64x64.dmi | Bin 0 -> 103257 bytes
icons/mob/pai_vr.dmi | Bin 185050 -> 0 bytes
icons/mob/pai_vr64x32.dmi | Bin 1914 -> 0 bytes
icons/mob/pai_vr64x64.dmi | Bin 107038 -> 0 bytes
tgui/packages/tgui-panel/chat/handlers.ts | 4 +
tgui/packages/tgui-panel/events/listeners.ts | 3 +-
.../interfaces/PAICard/PAIActiveCompanion.tsx | 178 ++++
.../tgui/interfaces/PAICard/PAIDataEntry.tsx | 110 ++
.../interfaces/PAICard/PAIFindCompanion.tsx | 31 +
.../tgui/interfaces/PAICard/PaiIcon.tsx | 60 ++
.../tgui/interfaces/PAICard/constants.ts | 7 +
.../tgui/interfaces/PAICard/functions.ts | 12 +
.../tgui/interfaces/PAICard/index.tsx | 24 +
.../packages/tgui/interfaces/PAICard/types.ts | 44 +
.../tgui/interfaces/PaiChoose/IconSection.tsx | 44 +
.../tgui/interfaces/PaiChoose/PaiPreview.tsx | 135 +++
.../interfaces/PaiChoose/SpriteSection.tsx | 76 ++
.../tgui/interfaces/PaiChoose/functions.ts | 65 ++
.../tgui/interfaces/PaiChoose/index.tsx | 21 +
.../tgui/interfaces/PaiChoose/types.ts | 10 +
.../CharacterPreferenceWindow.tsx | 2 +-
.../bay_prefs/general/SubtabInfo.tsx | 6 +-
.../bay_prefs/general/SubtabPai.tsx | 248 +++++
.../bay_prefs/general/SubtabSettings.tsx | 33 +-
.../PreferencesMenu/bay_prefs/general/data.ts | 24 +-
.../bay_prefs/general/functions.ts | 11 +-
.../bay_prefs/general/index.tsx | 10 +-
.../PreferencesMenu/elements/ColorInput.tsx | 187 ++++
.../interfaces/RobotChoose/SpriteSection.tsx | 4 +-
.../VorePanelElements/VorePanelEditColor.tsx | 6 +-
.../packages/tgui/interfaces/pAIInterface.tsx | 150 ++-
vorestation.dme | 13 +-
95 files changed, 4051 insertions(+), 2442 deletions(-)
create mode 100644 code/controllers/subsystems/pai.dm
delete mode 100644 code/game/objects/items/devices/paicard_vr.dm
create mode 100644 code/modules/asset_cache/assets/spritesheets/pai_icons.dm
create mode 100644 code/modules/client/preferences/migrations/19_paifile.dm
create mode 100644 code/modules/client/preferences/types/character/pai/01_basic.dm
create mode 100644 code/modules/mob/living/silicon/pai/chassis_ui.dm
create mode 100644 code/modules/mob/living/silicon/pai/pai_folding.dm
create mode 100644 code/modules/mob/living/silicon/pai/pai_software.dm
create mode 100644 code/modules/mob/living/silicon/pai/pai_verbs.dm
delete mode 100644 code/modules/mob/living/silicon/pai/pai_vr.dm
delete mode 100644 code/modules/mob/living/silicon/pai/personality.dm
delete mode 100644 code/modules/mob/living/silicon/pai/recruit.dm
create mode 100644 code/modules/mob/living/silicon/pai/sprites/_sprite_datum.dm
create mode 100644 code/modules/unit_tests/pai_tests.dm
create mode 100644 code/modules/unit_tests/special_roles.dm
rename icons/mob/{pai_vr32x64.dmi => pai32x64.dmi} (100%)
create mode 100644 icons/mob/pai64x32.dmi
create mode 100644 icons/mob/pai64x64.dmi
delete mode 100644 icons/mob/pai_vr.dmi
delete mode 100644 icons/mob/pai_vr64x32.dmi
delete mode 100644 icons/mob/pai_vr64x64.dmi
create mode 100644 tgui/packages/tgui/interfaces/PAICard/PAIActiveCompanion.tsx
create mode 100644 tgui/packages/tgui/interfaces/PAICard/PAIDataEntry.tsx
create mode 100644 tgui/packages/tgui/interfaces/PAICard/PAIFindCompanion.tsx
create mode 100644 tgui/packages/tgui/interfaces/PAICard/PaiIcon.tsx
create mode 100644 tgui/packages/tgui/interfaces/PAICard/constants.ts
create mode 100644 tgui/packages/tgui/interfaces/PAICard/functions.ts
create mode 100644 tgui/packages/tgui/interfaces/PAICard/index.tsx
create mode 100644 tgui/packages/tgui/interfaces/PAICard/types.ts
create mode 100644 tgui/packages/tgui/interfaces/PaiChoose/IconSection.tsx
create mode 100644 tgui/packages/tgui/interfaces/PaiChoose/PaiPreview.tsx
create mode 100644 tgui/packages/tgui/interfaces/PaiChoose/SpriteSection.tsx
create mode 100644 tgui/packages/tgui/interfaces/PaiChoose/functions.ts
create mode 100644 tgui/packages/tgui/interfaces/PaiChoose/index.tsx
create mode 100644 tgui/packages/tgui/interfaces/PaiChoose/types.ts
create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/bay_prefs/general/SubtabPai.tsx
create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/elements/ColorInput.tsx
diff --git a/code/__defines/gamemode.dm b/code/__defines/gamemode.dm
index 4bafb55ace..a617b5ce75 100644
--- a/code/__defines/gamemode.dm
+++ b/code/__defines/gamemode.dm
@@ -43,7 +43,7 @@ GLOBAL_LIST_INIT(be_special_flags, list(
"Wizard" = BE_WIZARD,
"Malf AI" = BE_MALF,
"Revolutionary" = BE_REV,
- "Genaprawn" = BE_ALIEN,
+ "Alien Candidate" = BE_ALIEN,
"Positronic Brain" = BE_AI,
"Cultist" = BE_CULTIST,
"Renegade" = BE_RENEGADE,
diff --git a/code/__defines/preferences.dm b/code/__defines/preferences.dm
index 6980c7f985..eb4cf69ceb 100644
--- a/code/__defines/preferences.dm
+++ b/code/__defines/preferences.dm
@@ -72,3 +72,6 @@
#define WRITE_PREF_NORMAL 1
#define WRITE_PREF_INSTANT 2
#define WRITE_PREF_MANUAL 3
+
+#define PAI_UNSET "None Set"
+#define PAI_DEFAULT_CHASSIS "Drone"
diff --git a/code/_helpers/global_lists.dm b/code/_helpers/global_lists.dm
index 902c554c11..106053b432 100644
--- a/code/_helpers/global_lists.dm
+++ b/code/_helpers/global_lists.dm
@@ -969,16 +969,13 @@ GLOBAL_LIST_INIT(special_roles, list(
"ninja" = 0, // 10
"raider" = 0, // 11
"diona" = 0, // 12
- "mutineer" = 0, // 13
- "loyalist" = 0, // 14
- "pAI candidate" = 1, // 15
- "lost drone" = 1, // 16
- "maint pred" = 1, // 17
- "maint lurker" = 1, // 18
- "morph" = 1, // 19
- "corgi" = 1, // 20
- "cursed sword" = 1, // 21
- "Ship Survivor" = 1, // 22
+ "loyalist" = 0, // 13
+ "pAI" = 1, // 14
+ "lost drone" = 1, // 15
+ "maint critter" = 1, // 16
+ "corgi" = 1, // 17
+ "cursed sword" = 1, // 18
+ "ship survivor" = 1, // 19
))
GLOBAL_LIST_INIT(maint_mob_pred_options, list(
@@ -1095,23 +1092,27 @@ GLOBAL_LIST_INIT(breach_burn_descriptors, list(
"huge scorched area"
))
-GLOBAL_LIST_INIT(wide_chassis, list(
- "rat",
- "panther",
- "teppi",
- "pai-diredog",
- "pai-horse_lune",
- "pai-horse_soleil",
- "pai-pdragon",
- "pai-protodog"
- ))
+GLOBAL_LIST_EMPTY(paikeys)
-GLOBAL_LIST_INIT(flying_chassis, list(
- "pai-parrot",
- "pai-bat",
- "pai-butterfly",
- "pai-hawk",
- "cyberelf"
+GLOBAL_LIST_EMPTY(pai_software_by_key)
+GLOBAL_LIST_EMPTY(default_pai_software)
+GLOBAL_LIST_INIT(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
))
//Sure I could spend all day making wacky overlays for all of the different forms
@@ -1303,41 +1304,6 @@ GLOBAL_LIST_EMPTY(sparring_attack_cache)
GLOBAL_LIST_EMPTY(protean_abilities)
-//PAI stuff
-GLOBAL_LIST_INIT(possible_chassis, list(
- "Drone" = "pai-repairbot",
- "Cat" = "pai-cat",
- "Mouse" = "pai-mouse",
- "Monkey" = "pai-monkey",
- "Borgi" = "pai-borgi",
- "Fox" = "pai-fox",
- "Parrot" = "pai-parrot",
- "Rabbit" = "pai-rabbit",
- "Dire wolf" = "pai-diredog",
- "Horse (Lune)" = "pai-horse_lune",
- "Horse (Soleil)" = "pai-horse_soleil",
- "Dragon" = "pai-pdragon",
- "Bear" = "pai-bear",
- "Fennec" = "pai-fen",
- "Type Zero" = "pai-typezero",
- "Raccoon" = "pai-raccoon",
- "Raptor" = "pai-raptor",
- "Corgi" = "pai-corgi",
- "Bat" = "pai-bat",
- "Butterfly" = "pai-butterfly",
- "Hawk" = "pai-hawk",
- "Duffel" = "pai-duffel",
- "Rat" = "rat",
- "Panther" = "panther",
- "Cyber Elf" = "cyberelf",
- "Teppi" = "teppi",
- "Catslug" = "catslug",
- "Car" = "car",
- "Type One" = "typeone",
- "Type Thirteen" = "13",
- "Protogen Dog" = "pai-protodog"
- ))
-
//PAI stuff
GLOBAL_LIST_INIT(possible_say_verbs, list(
"Robotic" = list("states","declares","queries"),
diff --git a/code/_helpers/text.dm b/code/_helpers/text.dm
index ec08572ddd..a24fdd681b 100644
--- a/code/_helpers/text.dm
+++ b/code/_helpers/text.dm
@@ -353,7 +353,7 @@ GLOBAL_LIST_INIT(alphabet_upper, list("A","B","C","D","E","F","G","H","I","J","K
else
return string
else
- return "[copytext_preserve_html(string, 1, 37)]..."
+ return "[copytext_preserve_html(string, 1, len - 3)]..."
//alternative copytext() for encoded text, doesn't break html entities (" and other)
/proc/copytext_preserve_html(var/text, var/first, var/last)
diff --git a/code/controllers/admin.dm b/code/controllers/admin.dm
index a44c3b7eeb..6c5235dd48 100644
--- a/code/controllers/admin.dm
+++ b/code/controllers/admin.dm
@@ -80,7 +80,6 @@ ADMIN_VERB(debug_controller, R_DEBUG, "Debug Controller", "Debug the various per
options["LEGACY: master_controller"] = GLOB.master_controller
options["LEGACY: job_master"] = GLOB.job_master
options["LEGACY: emergency_shuttle"] = GLOB.emergency_shuttle
- options["LEGACY: paiController"] = paiController
options["LEGACY: cameranet"] = GLOB.cameranet
options["LEGACY: transfer_controller"] = GLOB.transfer_controller
diff --git a/code/controllers/subsystems/pai.dm b/code/controllers/subsystems/pai.dm
new file mode 100644
index 0000000000..2d1bdf714b
--- /dev/null
+++ b/code/controllers/subsystems/pai.dm
@@ -0,0 +1,224 @@
+#define PAI_DELAY_TIME 1 MINUTE
+
+////////////////////////////////
+//// Pai join and management subsystem
+////////////////////////////////
+SUBSYSTEM_DEF(pai)
+ name = "Pai"
+ wait = 4 SECONDS
+ dependencies = list(
+ /datum/controller/subsystem/atoms
+ )
+ VAR_PRIVATE/list/datum/pai_sprite/pai_chassis_sprites = list()
+ VAR_PRIVATE/list/current_run = list()
+ VAR_PRIVATE/list/pai_ghosts = list()
+ VAR_PRIVATE/list/asked = list()
+
+/datum/controller/subsystem/pai/Initialize()
+ // Get all software setup
+ for(var/type in subtypesof(/datum/pai_software))
+ var/datum/pai_software/P = new type()
+ GLOB.pai_software_by_key[P.id] = P
+ if(P.default)
+ GLOB.default_pai_software[P.id] = P
+
+ // Get all valid chassis types
+ for(var/datum/pai_sprite/sprite as anything in subtypesof(/datum/pai_sprite))
+ if(!initial(sprite.sprite_icon) || initial(sprite.hidden))
+ continue
+ pai_chassis_sprites[initial(sprite.name)] = new sprite()
+
+ return SS_INIT_SUCCESS
+
+/datum/controller/subsystem/pai/stat_entry(msg)
+ msg = "C:[length(pai_ghosts)]"
+ return ..()
+
+/datum/controller/subsystem/pai/fire(resumed)
+ if(!resumed)
+ pai_ghosts.Cut()
+ current_run = GLOB.observer_mob_list.Copy()
+
+ while(length(current_run))
+ if(MC_TICK_CHECK)
+ return
+
+ var/mob/observer/ghost = current_run[length(current_run)]
+ current_run.len--
+ if(!invite_valid(ghost))
+ continue
+
+ // Create candidate
+ pai_ghosts[REF(ghost)] = WEAKREF(ghost)
+
+/datum/controller/subsystem/pai/proc/get_chassis_list()
+ RETURN_TYPE(/list/datum/pai_sprite)
+ SHOULD_NOT_OVERRIDE(TRUE)
+ return pai_chassis_sprites
+
+/datum/controller/subsystem/pai/proc/chassis_data(id_name)
+ RETURN_TYPE(/datum/pai_sprite)
+ SHOULD_NOT_OVERRIDE(TRUE)
+ if(!(id_name in pai_chassis_sprites))
+ return pai_chassis_sprites[PAI_DEFAULT_CHASSIS]
+ return pai_chassis_sprites[id_name]
+
+/datum/controller/subsystem/pai/proc/invite_valid(mob/user)
+ SHOULD_NOT_OVERRIDE(TRUE)
+ if(!user.client?.prefs || !user.ckey)
+ return FALSE
+ if(!user.MayRespawn())
+ return FALSE
+ if(jobban_isbanned(user, "pAI"))
+ return FALSE
+ if(!(user.client.prefs.be_special & BE_PAI))
+ return FALSE
+ if(check_is_delayed(REF(user)))
+ return FALSE
+ if(check_is_already_pai(user.ckey))
+ return FALSE
+ if(user.client.prefs.read_preference(/datum/preference/text/pai_name) == PAI_UNSET) // Forbid unset name
+ return FALSE
+ return TRUE
+
+/datum/controller/subsystem/pai/proc/check_is_delayed(ghost_ref)
+ SHOULD_NOT_OVERRIDE(TRUE)
+ if(ghost_ref in asked)
+ if(world.time < asked[ghost_ref] + PAI_DELAY_TIME)
+ return TRUE
+ return FALSE
+
+/datum/controller/subsystem/pai/proc/check_is_already_pai(check_ckey)
+ SHOULD_NOT_OVERRIDE(TRUE)
+ return (check_ckey in GLOB.paikeys)
+
+/datum/controller/subsystem/pai/proc/get_ghost_from_ref(ghost_ref)
+ RETURN_TYPE(/mob/observer)
+ SHOULD_NOT_OVERRIDE(TRUE)
+ PRIVATE_PROC(TRUE)
+ if(!ghost_ref)
+ return null
+ var/datum/weakref/WF = pai_ghosts[ghost_ref]
+ return WF?.resolve()
+
+/datum/controller/subsystem/pai/proc/get_invite_list_data()
+ RETURN_TYPE(/list)
+ SHOULD_NOT_OVERRIDE(TRUE)
+
+ var/list/data = list()
+ for(var/ghost_ref in pai_ghosts)
+ var/mob/observer/ghost = get_ghost_from_ref(ghost_ref)
+ if(!istype(ghost) || !ghost.client?.prefs)
+ continue
+
+ var/datum/preferences/pref = ghost.client.prefs
+ var/datum/asset/spritesheet_batched/pai_icons/spritesheet = get_asset_datum(/datum/asset/spritesheet_batched/pai_icons)
+ var/chassis = pref.read_preference(/datum/preference/text/pai_chassis)
+ var/datum/pai_sprite/sprite_datum = SSpai.chassis_data(chassis)
+ var/css_class = sanitize_css_class_name("[sprite_datum.type]")
+ UNTYPED_LIST_ADD(data, list(
+ "ref" = REF(ghost),
+ "name" = pref.read_preference(/datum/preference/text/pai_name),
+ "gender" = pref.read_preference(/datum/preference/choiced/gender/biological), // Cannot use identifying yet due to byond limits
+ "role" = TextPreview(pref.read_preference(/datum/preference/text/pai_role), 152),
+ "ad" = TextPreview(pref.read_preference(/datum/preference/text/pai_ad), 244),
+ "eyecolor" = pref.read_preference(/datum/preference/color/pai_eye_color),
+ "chassis" = chassis,
+ "emotion" = pref.read_preference(/datum/preference/text/pai_emotion),
+ "sprite_datum_class" = css_class,
+ "sprite_datum_size" = spritesheet.icon_size_id(css_class + "S"), // just get the south icon's size, the rest will be the same
+ ))
+ return data
+
+/datum/controller/subsystem/pai/proc/get_detailed_invite_data(var/ghost_ref)
+ RETURN_TYPE(/list)
+ SHOULD_NOT_OVERRIDE(TRUE)
+
+ if(!(ghost_ref in pai_ghosts))
+ return null
+
+ var/datum/weakref/WF = pai_ghosts[ghost_ref]
+ var/mob/observer/ghost = WF?.resolve()
+ if(!istype(ghost) || !ghost.client?.prefs)
+ return null
+
+ var/datum/preferences/pref = ghost.client.prefs
+ var/datum/asset/spritesheet_batched/pai_icons/spritesheet = get_asset_datum(/datum/asset/spritesheet_batched/pai_icons)
+ var/chassis = pref.read_preference(/datum/preference/text/pai_chassis)
+ var/datum/pai_sprite/sprite_datum = SSpai.chassis_data(chassis)
+ var/css_class = sanitize_css_class_name("[sprite_datum.type]")
+ return list(
+ "ref" = ghost_ref,
+ "name" = pref.read_preference(/datum/preference/text/pai_name),
+ "gender" = pref.read_preference(/datum/preference/choiced/gender/biological), // Cannot use identifying yet due to byond limits
+ // Description
+ "role" = pref.read_preference(/datum/preference/text/pai_role),
+ "description" = pref.read_preference(/datum/preference/text/pai_description),
+ "ad" = pref.read_preference(/datum/preference/text/pai_ad),
+ "comments" = pref.read_preference(/datum/preference/text/pai_comments),
+ // Appearance
+ "eyecolor" = pref.read_preference(/datum/preference/color/pai_eye_color),
+ "chassis" = chassis,
+ "emotion" = pref.read_preference(/datum/preference/text/pai_emotion),
+ // Sprites
+ "sprite_datum_class" = css_class,
+ "sprite_datum_size" = spritesheet.icon_size_id(css_class + "S"), // just get the south icon's size, the rest will be the same
+ )
+
+/datum/controller/subsystem/pai/proc/invite_ghost(mob/inquirer, ghost_ref, obj/item/paicard/card)
+ SHOULD_NOT_OVERRIDE(TRUE)
+ // Is our card legal to inhabit?
+ if(QDELETED(card) || card.pai || card.is_damage_critical())
+ to_chat(inquirer, span_warning("This [card] can no longer be used to house a pAI."))
+ return
+
+ // Check if the ghost stopped existing
+ var/mob/observer/ghost = get_ghost_from_ref(ghost_ref)
+ if(!isobserver(ghost) || !ghost.client)
+ to_chat(inquirer, span_warning("This pAI has gone offline."))
+ return
+
+ // Time delay if the ghost cancels your invite.
+ if(check_is_delayed(REF(ghost)))
+ to_chat(inquirer, span_notice("This pAI is responding to a request, but may become available again shortly..."))
+ return
+ asked[REF(ghost)] = world.time
+
+ // Can't play, still respawning
+ var/time_till_respawn = ghost.time_till_respawn()
+ if(time_till_respawn == -1 || time_till_respawn)
+ to_chat(inquirer, span_warning("This pAI is still downloading..."))
+ return
+
+ // Send it!
+ to_chat(inquirer, span_info("A request has been sent!"))
+ var/client/target = ghost.client
+ var/response = tgui_alert(target, "[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(!response || !target || !isobserver(target.mob) || ghost != target.mob)
+ return // Nice try smartass
+ if(check_is_already_pai(target.ckey))
+ to_chat(inquirer, span_warning("This pAI has already been downloaded."))
+ return
+ if(QDELETED(card) || card.pai)
+ to_chat(inquirer, span_warning("This [card] can no longer be used to house a pAI."))
+ return
+
+ switch(response)
+ if("Yes")
+ var/new_pai = card.ghost_inhabit(target.mob, TRUE)
+ to_chat(inquirer, span_info("[new_pai] has accepted your pAI request!"))
+ return
+ if("Never for this round")
+ SSpai.block_pai_invites(REF(ghost))
+
+ to_chat(inquirer, span_warning("The pAI denied the request."))
+
+/datum/controller/subsystem/pai/proc/block_pai_invites(ghost_ref)
+ SHOULD_NOT_OVERRIDE(TRUE)
+ asked[ghost_ref] = world.time + 99 HOURS // We never want to be asked again
+
+/datum/controller/subsystem/pai/proc/clear_pai_block_delay(ghost_ref)
+ SHOULD_NOT_OVERRIDE(TRUE)
+ asked -= ghost_ref
+
+#undef PAI_DELAY_TIME
diff --git a/code/game/machinery/atmo_control.dm b/code/game/machinery/atmo_control.dm
index 6eb17f9088..7d61d7d6b4 100644
--- a/code/game/machinery/atmo_control.dm
+++ b/code/game/machinery/atmo_control.dm
@@ -186,6 +186,9 @@
. = ..(W, user)
+/obj/machinery/computer/general_air_control/allow_pai_interaction(proximity_flag)
+ return proximity_flag
+
/obj/machinery/computer/general_air_control/receive_signal(datum/signal/signal)
if(!signal || signal.encryption) return
diff --git a/code/game/machinery/buttons.dm b/code/game/machinery/buttons.dm
index 733f0763a1..5ee5da62df 100644
--- a/code/game/machinery/buttons.dm
+++ b/code/game/machinery/buttons.dm
@@ -16,3 +16,6 @@
/obj/machinery/button/attackby(obj/item/W, mob/user as mob)
return attack_hand(user)
+
+/obj/machinery/button/allow_pai_interaction(proximity_flag)
+ return proximity_flag
diff --git a/code/game/machinery/computer/atmos_alert.dm b/code/game/machinery/computer/atmos_alert.dm
index 54f67c1de2..4c78c2d9e8 100644
--- a/code/game/machinery/computer/atmos_alert.dm
+++ b/code/game/machinery/computer/atmos_alert.dm
@@ -17,6 +17,9 @@
/obj/machinery/computer/atmos_alert/attack_hand(mob/user)
tgui_interact(user)
+/obj/machinery/computer/atmos_alert/allow_pai_interaction(proximity_flag)
+ return proximity_flag
+
/obj/machinery/computer/atmos_alert/tgui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
diff --git a/code/game/machinery/computer/camera.dm b/code/game/machinery/computer/camera.dm
index b6b8fc1cbf..c3a7ee5043 100644
--- a/code/game/machinery/computer/camera.dm
+++ b/code/game/machinery/computer/camera.dm
@@ -68,6 +68,9 @@
density = FALSE
circuit = null
+/obj/machinery/computer/security/telescreen/allow_pai_interaction(proximity_flag)
+ return proximity_flag
+
GLOBAL_LIST_EMPTY(entertainment_screens)
/obj/machinery/computer/security/telescreen/entertainment
name = "entertainment monitor"
diff --git a/code/game/machinery/computer/crew.dm b/code/game/machinery/computer/crew.dm
index b658638700..2045154cd0 100644
--- a/code/game/machinery/computer/crew.dm
+++ b/code/game/machinery/computer/crew.dm
@@ -28,6 +28,9 @@
return
tgui_interact(user)
+/obj/machinery/computer/crew/allow_pai_interaction(proximity_flag)
+ return proximity_flag
+
/obj/machinery/computer/crew/tgui_interact(mob/user, datum/tgui/ui = null)
crew_monitor.tgui_interact(user, ui)
diff --git a/code/game/machinery/computer/station_alert.dm b/code/game/machinery/computer/station_alert.dm
index 0a5692a897..a365fa66a9 100644
--- a/code/game/machinery/computer/station_alert.dm
+++ b/code/game/machinery/computer/station_alert.dm
@@ -41,6 +41,9 @@
tgui_interact(user)
return
+/obj/machinery/computer/station_alert/allow_pai_interaction(proximity_flag)
+ return proximity_flag
+
/obj/machinery/computer/station_alert/tgui_interact(mob/user)
alarm_monitor.tgui_interact(user)
diff --git a/code/game/machinery/doors/airlock_control.dm b/code/game/machinery/doors/airlock_control.dm
index 5e70300958..3779df3582 100644
--- a/code/game/machinery/doors/airlock_control.dm
+++ b/code/game/machinery/doors/airlock_control.dm
@@ -270,6 +270,9 @@
return TRUE
return ..()
+/obj/machinery/airlock_sensor/allow_pai_interaction(proximity_flag)
+ return proximity_flag
+
/obj/machinery/airlock_sensor/airlock_interior
command = "cycle_interior"
@@ -361,6 +364,8 @@
radio_connection.post_signal(src, signal, range = AIRLOCK_CONTROL_RANGE, radio_filter = RADIO_AIRLOCK)
flick("access_button_cycle", src)
+/obj/machinery/access_button/allow_pai_interaction(proximity_flag)
+ return proximity_flag
/obj/machinery/access_button/proc/set_frequency(new_frequency)
SSradio.remove_object(src, frequency)
diff --git a/code/game/machinery/jukebox.dm b/code/game/machinery/jukebox.dm
index 5cf746aa1b..fec3d3d3d3 100644
--- a/code/game/machinery/jukebox.dm
+++ b/code/game/machinery/jukebox.dm
@@ -252,6 +252,9 @@
/obj/machinery/media/jukebox/attack_hand(var/mob/user as mob)
interact(user)
+/obj/machinery/media/jukebox/allow_pai_interaction(proximity_flag)
+ return proximity_flag
+
/obj/machinery/media/jukebox/proc/explode()
walk_to(src,0)
src.visible_message(span_danger("\The [src] blows apart!"), 1)
diff --git a/code/game/machinery/lightswitch.dm b/code/game/machinery/lightswitch.dm
index 8e62a56d31..933cac4d64 100644
--- a/code/game/machinery/lightswitch.dm
+++ b/code/game/machinery/lightswitch.dm
@@ -71,6 +71,9 @@
area.power_change()
GLOB.lights_switched_on_roundstat++
+/obj/machinery/light_switch/allow_pai_interaction(proximity_flag)
+ return proximity_flag
+
/obj/machinery/light_switch/power_change()
if(!otherarea)
diff --git a/code/game/machinery/newscaster.dm b/code/game/machinery/newscaster.dm
index 69755023bd..6d5877effa 100644
--- a/code/game/machinery/newscaster.dm
+++ b/code/game/machinery/newscaster.dm
@@ -273,6 +273,9 @@ GLOBAL_LIST_BOILERPLATE(allCasters, /obj/machinery/newscaster)
tgui_interact(user)
+/obj/machinery/newscaster/allow_pai_interaction(proximity_flag)
+ return proximity_flag
+
/**
* Sets a temporary message to display to the user
*
diff --git a/code/game/objects/items/devices/paicard.dm b/code/game/objects/items/devices/paicard.dm
index e0603d8978..236157dbb2 100644
--- a/code/game/objects/items/devices/paicard.dm
+++ b/code/game/objects/items/devices/paicard.dm
@@ -10,17 +10,28 @@
preserve_item = 1
var/obj/item/radio/borg/pai/radio
- var/looking_for_personality = 0
var/mob/living/silicon/pai/pai
var/image/screen_layer
var/screen_color = "#00ff0d"
+ var/current_emotion = 1
var/last_notify = 0
var/screen_msg
pickup_sound = 'sound/items/pickup/device.ogg'
drop_sound = 'sound/items/drop/device.ogg'
+ // Parts and upgrades
+ var/panel_open = FALSE
+ var/cell = PP_FUNCTIONAL //critical- power
+ var/processor = PP_FUNCTIONAL //critical- the thinky part
+ var/board = PP_FUNCTIONAL //critical- makes everything work
+ var/capacitor = PP_FUNCTIONAL //critical- power processing
+ var/projector = PP_FUNCTIONAL //non-critical- affects unfolding
+ var/emitter = PP_FUNCTIONAL //non-critical- affects unfolding
+ var/speech_synthesizer = PP_FUNCTIONAL //non-critical- affects speech
+
///Var for attack_self chain
var/special_handling = FALSE
+ var/selected_pai
/obj/item/paicard/relaymove(var/mob/user, var/direction)
if(user.stat || user.stunned)
@@ -31,18 +42,17 @@
/obj/item/paicard/Initialize(mapload)
. = ..()
- add_overlay("pai-off")
+ setEmotion(16)
/obj/item/paicard/Destroy()
//Will stop people throwing friend pAIs into the singularity so they can respawn
- if(!isnull(pai))
+ if(!QDELETED(pai)) // Either the pai or card could be deleted first, prevent a loop
pai.death(0)
QDEL_NULL(radio)
return ..()
-// VOREStation Edit - Allow everyone to become a pAI
/obj/item/paicard/attack_ghost(mob/user as mob)
- if(pai != null) //Have a person in them already?
+ if(pai) //Have a person in them already?
return ..()
if(is_damage_critical())
to_chat(user, span_warning("That card is too damaged to activate!"))
@@ -57,361 +67,250 @@
to_chat(user,span_warning("You cannot join a pAI card when you are banned from playing as a pAI."))
return
- for(var/ourkey in GLOB.paikeys)
- if(ourkey == user.ckey)
- to_chat(user, span_warning("You can't just rejoin any old pAI card!!! Your card still exists."))
- return
+ if(SSpai.check_is_already_pai(user.ckey))
+ to_chat(user, span_warning("You can't just rejoin any old pAI card!!! Your card still exists."))
+ return
- var/choice = tgui_alert(user, "You sure you want to inhabit this PAI, or submit yourself to being recruited?", "Confirmation", list("Inhabit", "Recruit", "Cancel"))
- if(!choice || choice == "Cancel")
- return ..()
- if(choice == "Recruit")
- paiController.recruitWindow(user)
- return ..()
- choice = tgui_alert(user, "Do you want to load your pAI data?", "Load", list("Yes", "No"))
- var/actual_pai_name
- var/turf/location = get_turf(src)
- if(choice == "No")
- var/pai_name = tgui_input_text(user, "Choose your character's name", "Character Name")
- actual_pai_name = sanitize_name(pai_name, ,1)
- if(isnull(actual_pai_name))
- return ..()
- if(istype(src , /obj/item/paicard/typeb))
- var/obj/item/paicard/typeb/card = new(location)
- var/mob/living/silicon/pai/new_pai = new(card)
- new_pai.key = user.key
- GLOB.paikeys |= new_pai.ckey
- card.setPersonality(new_pai)
- new_pai.SetName(actual_pai_name)
- else
- var/obj/item/paicard/card = new(location)
- var/mob/living/silicon/pai/new_pai = new(card)
- new_pai.key = user.key
- GLOB.paikeys |= new_pai.ckey
- card.setPersonality(new_pai)
- new_pai.SetName(actual_pai_name)
+ var/pai_name = user.client?.prefs.read_preference(/datum/preference/text/pai_name)
+ if(!pai_name || pai_name == PAI_UNSET)
+ to_chat(user, span_danger("You have no pai name set."))
+ return
- if(choice == "Yes")
- if(istype(src , /obj/item/paicard/typeb))
- var/obj/item/paicard/typeb/card = new(location)
- var/mob/living/silicon/pai/new_pai = new(card)
- new_pai.key = user.key
- GLOB.paikeys |= new_pai.ckey
- card.setPersonality(new_pai)
- if(!new_pai.savefile_load(new_pai))
- var/pai_name = tgui_input_text(new_pai, "Choose your character's name", "Character Name")
- actual_pai_name = sanitize_name(pai_name, ,1)
- if(isnull(actual_pai_name))
- return ..()
- else
- var/obj/item/paicard/card = new(location)
- var/mob/living/silicon/pai/new_pai = new(card)
- new_pai.key = user.key
- GLOB.paikeys |= new_pai.ckey
- card.setPersonality(new_pai)
- if(!new_pai.savefile_load(new_pai))
- var/pai_name = tgui_input_text(new_pai, "Choose your character's name", "Character Name")
- actual_pai_name = sanitize_name(pai_name, ,1)
- if(isnull(actual_pai_name))
- return ..()
+ var/choice = tgui_alert(user, "Do you want to inhabit this pAI using \"[pai_name]\"?", "Load pAI", list("Load pAI Data", "Cancel"))
+ if(choice == "Load pAI Data")
+ ghost_inhabit(user)
+ return
- qdel(src)
- return ..()
+/obj/item/paicard/proc/ghost_inhabit(mob/user)
+ RETURN_TYPE(/mob/living/silicon/pai)
+ var/pai_name = user.client?.prefs.read_preference(/datum/preference/text/pai_name)
+ if(!pai_name || pai_name == PAI_UNSET) // Lets avoid "None Set" pais joining
+ to_chat(user, span_danger("You have no pai name set."))
+ return
+ // Setup pai
+ var/mob/living/silicon/pai/new_pai = new(src)
+ new_pai.key = user.key
+ GLOB.paikeys |= new_pai.ckey
+ setPersonality(new_pai)
+ new_pai.apply_preferences(new_pai.client)
+ return new_pai
-// VOREStation Edit End
-
-/obj/item/paicard/proc/access_screen(mob/user)
+/obj/item/paicard/tgui_interact(mob/user, datum/tgui/ui)
if(is_damage_critical())
to_chat(user, span_warning("WARNING: CRITICAL HARDWARE FAILURE, SERVICE DEVICE IMMEDIATELY"))
return
- if (!in_range(src, user))
- return
- user.set_machine(src)
- var/dat = {"
-
-
-
-
-
-
- "}
- if(pai)
- dat += {"
- "} + span_bold("Personal AI Device") + {"
-
-
- | "} + span_bold("[pai.name]") + {" |
-
-
- | Integrity: [pai.health] |
-
-
- | Prime directive: |
- [pai.pai_law0] |
-
-
- | Additional directives: |
- [pai.pai_laws] |
-
-
-
- "}
- dat += {"
-
- "}
- if(pai && (!pai.master_dna || !pai.master))
- dat += {"
-
- "}
- dat += "
"
- if(radio)
- dat += span_bold("Radio Uplink")
- dat += {"
-
-
- "}
- else //
- dat += span_bold("Radio Uplink") + "
"
- dat += span_red(span_italics("Radio firmware not loaded. Please install a pAI personality to load firmware.")) + "
"
- /* - //A button for instantly deleting people from the game is lame, especially considering that pAIs on our server tend to activate without a master.
- dat += {"
-
- | Wipe current pAI personality
+/obj/item/paicard/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state)
+ var/list/data = list(
+ "active_pai_data" = null,
+ "selected_pai_data" = null,
+ "available_pais" = null,
+ "waiting_for_response" = in_use,
+ )
- |
-
- "}
+
+ if(pai) // Only set pai data if we have one
+ data["active_pai_data"] = get_active_data()
+ return data
+
+ if(selected_pai)
+ data["selected_pai_data"] = SSpai.get_detailed_invite_data(selected_pai)
+ // Only get the invite list if we can browse for them
+ data["available_pais"] = SSpai.get_invite_list_data()
+ return data
+
+/obj/item/paicard/proc/get_active_data()
+ var/list/radio_data
+ if(radio)
+ radio_data = list(
+ "radio_transmit" = radio.broadcasting,
+ "radio_recieve" = radio.listening,
+ )
+
+ var/datum/asset/spritesheet_batched/pai_icons/spritesheet = get_asset_datum(/datum/asset/spritesheet_batched/pai_icons)
+ var/datum/pai_sprite/sprite_datum = SSpai.chassis_data(pai.chassis_name)
+ var/css_class = sanitize_css_class_name("[sprite_datum.type]")
+ return list(
+ "name" = pai.name,
+ "color" = screen_color,
+ "chassis" = pai.chassis_name,
+ "health" = pai.health,
+ "law_zero" = pai.pai_law0,
+ "law_extra" = pai.pai_laws,
+ "master_name" = pai.master,
+ "master_dna" = pai.master_dna,
+ "screen_msg" = screen_msg,
+ "radio_data" = radio_data,
+ "sprite_datum_class" = css_class,
+ "sprite_datum_size" = spritesheet.icon_size_id(css_class + "S"), // just get the south icon's size, the rest will be the same
+ )
+
+/obj/item/paicard/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state)
+ if(is_damage_critical())
+ return FALSE
+ if(..())
+ return TRUE
+ add_fingerprint(ui.user)
+
+ switch(action)
+ if("preview")
+ if(pai)
+ return FALSE
+ if(in_use)
+ return FALSE
+ var/new_selection = params["ref"]
+ if(!istext(new_selection))
+ return FALSE
+ selected_pai = new_selection
+ return TRUE
+ if("clear_preview")
+ if(pai)
+ return FALSE
+ if(in_use)
+ return FALSE
+ selected_pai = null
+ return TRUE
+ if("setdna")
+ if(!pai)
+ return FALSE
+ if(pai.master_dna)
+ return FALSE
+
+ var/mob/M = ui.user
+ var/has_dna = FALSE
+ if(istype(M, /mob/living/carbon))
+ var/mob/living/carbon/carby = M
+ var/datum/species/spec = carby.species
+ has_dna = TRUE
+ if(spec.flags & NO_DNA)
+ has_dna = FALSE
+
+ if(has_dna)
+ var/datum/dna/dna = M.dna
+ pai.master = M.real_name
+ pai.master_dna = dna.unique_enzymes
+ to_chat(pai, span_warning(span_large("You have been bound to a new master.")))
+ return TRUE
+ to_chat(ui.user, span_notice("You don't have any DNA, or your DNA is incompatible with this device."))
+ return FALSE
+
+ if("cleardna")
+ if(!pai)
+ return FALSE
+ pai.master = null
+ pai.master_dna = null
+ return TRUE
+ /*
+ if("wipe")
+ if(!pai)
+ return FALSE
+ if(in_use)
+ return FALSE
+ in_use = TRUE
+ var/confirm = tgui_alert(usr, "Are you CERTAIN you wish to delete the current personality? This action cannot be undone.", "Personality Wipe", list("Yes", "No"))
+ in_use = FALSE
+ if(!confirm)
+ return FALSE
+ if(confirm == "Yes")
+ for(var/mob/M in src)
+ to_chat(M, span_red("You feel yourself slipping away from reality.
"))
+ to_chat(M, "Byte by byte you lose your sense of self.
")
+ to_chat(M, "Your mental faculties leave you.
")
+ to_chat(M, "oblivion...
")
+ M.death(0)
+ removePersonality()
+ return TRUE
*/
- if(screen_msg)
- dat += span_bold("Message from [pai.name]") + "
[screen_msg]"
- else
- if(looking_for_personality)
- dat += {"
- "} + span_bold("pAI Request Module") + {"
- Requesting AI personalities from central database... If there are no entries, or if a suitable entry is not listed, check again later as more personalities may be added.
-
Searching for personalities
-
- "}
- else
- dat += {"
- "} + span_bold("pAI Request Module") + {"
- No personality is installed.
-
-
- Each time this button is pressed, a request will be sent out to any available personalities. Check back often give plenty of time for personalities to respond. This process could take anywhere from 15 seconds to several minutes, depending on the available personalities' timeliness.
- "}
- dat += ""
- user << browse(dat, "window=paicard")
- onclose(user, "paicard")
- return
+ if("wires")
+ if(!pai)
+ return FALSE
+ // WIRE_SIGNAL = 1
+ // WIRE_RECEIVE = 2
+ // WIRE_TRANSMIT = 4
+ switch(text2num(params["wires"]))
+ if(4)
+ radio.ToggleBroadcast()
+ return TRUE
+ if(2)
+ radio.ToggleReception()
+ return TRUE
+ return FALSE
-/obj/item/paicard/Topic(href, href_list)
+ if("setlaws")
+ if(!pai)
+ return FALSE
+ if(in_use)
+ return FALSE
+ var/newlaws = sanitize(params["directive"], MAX_MESSAGE_LEN, FALSE, FALSE, TRUE)
+ if(newlaws)
+ pai.pai_laws = newlaws
+ show_laws(TRUE)
+ return TRUE
- if(!usr || usr.stat)
- return
+ if("clearlaws")
+ if(!pai)
+ return FALSE
+ pai.pai_laws = null
+ return TRUE
- if(href_list["setdna"])
- if(pai.master_dna)
- return
- var/mob/M = usr
- if(!istype(M, /mob/living/carbon))
- to_chat(usr, span_blue("You don't have any DNA, or your DNA is incompatible with this device."))
- else
- var/datum/dna/dna = usr.dna
- pai.master = M.real_name
- pai.master_dna = dna.unique_enzymes
- to_chat(pai, span_red("You have been bound to a new master.
"))
- if(href_list["request"])
- src.looking_for_personality = 1
- paiController.findPAI(src, usr)
- if(href_list["wipe"])
- var/confirm = tgui_alert(usr, "Are you CERTAIN you wish to delete the current personality? This action cannot be undone.", "Personality Wipe", list("Yes", "No"))
- if(confirm == "Yes")
- for(var/mob/M in src)
- to_chat(M, span_red("You feel yourself slipping away from reality.
"))
- to_chat(M, "Byte by byte you lose your sense of self.
")
- to_chat(M, "Your mental faculties leave you.
")
- to_chat(M, "oblivion...
")
- M.death(0)
- removePersonality()
- if(href_list["wires"])
- var/t1 = text2num(href_list["wires"])
- switch(t1)
- if(4)
- radio.ToggleBroadcast()
- if(2)
- radio.ToggleReception()
- if(href_list["setlaws"])
- var/newlaws = sanitize(tgui_input_text(usr, "Enter any additional directives you would like your pAI personality to follow. Note that these directives will not override the personality's allegiance to its imprinted master. Conflicting directives will be ignored.", "pAI Directive Configuration", pai.pai_laws, MAX_MESSAGE_LEN, encode = FALSE, multiline = TRUE, prevent_enter = TRUE), MAX_MESSAGE_LEN, FALSE, FALSE, TRUE)
- if(newlaws)
- pai.pai_laws = newlaws
- to_chat(pai, "Your supplemental directives have been updated. Your new directives are:")
- to_chat(pai, "Prime Directive:
[pai.pai_law0]")
- to_chat(pai, "Supplemental Directives:
[pai.pai_laws]")
- attack_self(usr)
+ if("select_pai")
+ if(pai)
+ return FALSE
+ if(in_use)
+ return FALSE
+ in_use = TRUE
+ SSpai.invite_ghost(ui.user, params["ref"], src)
+ in_use = FALSE
+ selected_pai = null
+ return TRUE
-// WIRE_SIGNAL = 1
-// WIRE_RECEIVE = 2
-// WIRE_TRANSMIT = 4
+/obj/item/paicard/proc/show_laws(updated = FALSE)
+ to_chat(pai, examine_block(span_notice((updated ? "Your supplemental directives have been updated. Your new" : "Your") + " directives are:") + "
" + "Prime Directive: [span_info(pai.pai_law0)]
Supplemental Directives: [span_info(pai.pai_laws)]"))
/obj/item/paicard/proc/setPersonality(mob/living/silicon/pai/personality)
- src.pai = personality
+ pai = personality
setEmotion(1)
/obj/item/paicard/proc/removePersonality()
- src.pai = null
- cut_overlays()
+ pai = null
setEmotion(16)
-/obj/item/paicard
- var/current_emotion = 1
/obj/item/paicard/proc/setEmotion(var/emotion)
- if(pai)
- cut_overlays()
- qdel(screen_layer)
- screen_layer = null
- switch(emotion)
- if(1) screen_layer = image(icon, "pai-neutral")
- if(2) screen_layer = image(icon, "pai-what")
- if(3) screen_layer = image(icon, "pai-happy")
- if(4) screen_layer = image(icon, "pai-cat")
- if(5) screen_layer = image(icon, "pai-extremely-happy")
- if(6) screen_layer = image(icon, "pai-face")
- if(7) screen_layer = image(icon, "pai-laugh")
- if(8) screen_layer = image(icon, "pai-sad")
- if(9) screen_layer = image(icon, "pai-angry")
- if(10) screen_layer = image(icon, "pai-silly")
- if(11) screen_layer = image(icon, "pai-nose")
- if(12) screen_layer = image(icon, "pai-smirk")
- if(13) screen_layer = image(icon, "pai-exclamation")
- if(14) screen_layer = image(icon, "pai-question")
- if(15) screen_layer = image(icon, "pai-blank")
- if(16) screen_layer = image(icon, "pai-off")
+ cut_overlays()
+ qdel(screen_layer)
+ screen_layer = null
+ switch(emotion)
+ if(1) screen_layer = image(icon, "pai-neutral")
+ if(2) screen_layer = image(icon, "pai-what")
+ if(3) screen_layer = image(icon, "pai-happy")
+ if(4) screen_layer = image(icon, "pai-cat")
+ if(5) screen_layer = image(icon, "pai-extremely-happy")
+ if(6) screen_layer = image(icon, "pai-face")
+ if(7) screen_layer = image(icon, "pai-laugh")
+ if(8) screen_layer = image(icon, "pai-sad")
+ if(9) screen_layer = image(icon, "pai-angry")
+ if(10) screen_layer = image(icon, "pai-silly")
+ if(11) screen_layer = image(icon, "pai-nose")
+ if(12) screen_layer = image(icon, "pai-smirk")
+ if(13) screen_layer = image(icon, "pai-exclamation")
+ if(14) screen_layer = image(icon, "pai-question")
+ if(15) screen_layer = image(icon, "pai-blank")
+ if(16) screen_layer = image(icon, "pai-off")
- screen_layer.color = pai.eye_color
- add_overlay(screen_layer)
- current_emotion = emotion
+ screen_layer.color = pai ? pai.eye_color : initial(screen_color)
+ add_overlay(screen_layer)
+ current_emotion = emotion
/obj/item/paicard/proc/alertUpdate()
if(pai)
@@ -442,10 +341,401 @@
pai.show_message(rendered, type)
..()
+/obj/item/paicard/proc/clear_invite_overlay()
+ SHOULD_NOT_OVERRIDE(TRUE)
+ PRIVATE_PROC(TRUE)
+ if(pai) // Don't wipe emotion if a pai was invited
+ return
+ setEmotion(16)
+/obj/item/paicard/attackby(var/obj/item/I as obj, mob/user as mob)
+ if(istype(I,/obj/item/tool/screwdriver))
+ if(panel_open)
+ panel_open = FALSE
+ user.visible_message(span_notice("\The [user] secured \the [src]'s maintenance panel."))
+ playsound(src, 'sound/items/Screwdriver.ogg', 50, 1)
+ else if(pai)
+ if(do_after(user, 3 SECONDS, target = src))
+ panel_open = TRUE
+ user.visible_message(span_warning("\The [user] opened \the [src]'s maintenance panel."))
+ playsound(src, 'sound/items/Screwdriver.ogg', 50, 1)
+ if(istype(I,/obj/item/robotanalyzer))
+ if(!panel_open)
+ to_chat(user, span_warning("The panel isn't open. You will need to unscrew it to open it."))
+ else
+ if(cell == PP_FUNCTIONAL)
+ to_chat(user,"Power cell: " + span_notice("functional"))
+ else if(cell == PP_BROKEN)
+ to_chat(user,"Power cell: " + span_warning("damaged - CRITICAL"))
+ else
+ to_chat(user,"Power cell: " + span_warning("missing - CRITICAL"))
+
+ if(processor == PP_FUNCTIONAL)
+ to_chat(user,"Processor: " + span_notice("functional"))
+ else if(processor == PP_BROKEN)
+ to_chat(user,"Processor: " + span_warning("damaged - CRITICAL"))
+ else
+ to_chat(user,"Processor: " + span_warning("missing - CRITICAL"))
+
+ if(board == PP_FUNCTIONAL)
+ to_chat(user,"Board: " + span_notice("functional"))
+ else if(board == PP_BROKEN)
+ to_chat(user,"Board: " + span_warning("damaged - CRITICAL"))
+ else
+ to_chat(user,"Board: " + span_warning("missing - CRITICAL"))
+
+ if(capacitor == PP_FUNCTIONAL)
+ to_chat(user,"Capacitors: " + span_notice("functional"))
+ else if(capacitor == PP_BROKEN)
+ to_chat(user,"Capacitors: " + span_warning("damaged - CRITICAL"))
+ else
+ to_chat(user,"Capacitors: " + span_warning("missing - CRITICAL"))
+
+ if(projector == PP_FUNCTIONAL)
+ to_chat(user,"Projectors: " + span_notice("functional"))
+ else if(projector == PP_BROKEN)
+ to_chat(user,"Projectors: " + span_warning("damaged"))
+ else
+ to_chat(user,"Projectors: " + span_warning("missing"))
+
+ if(emitter == PP_FUNCTIONAL)
+ to_chat(user,"Emitters: " + span_notice("functional"))
+ else if(emitter == PP_BROKEN)
+ to_chat(user,"Emitters: " + span_warning("damaged"))
+ else
+ to_chat(user,"Emitters: " + span_warning("missing"))
+
+ if(speech_synthesizer == PP_FUNCTIONAL)
+ to_chat(user,"Speech Synthesizer: " + span_notice("functional"))
+ else if(speech_synthesizer == PP_BROKEN)
+ to_chat(user,"Speech Synthesizer: " + span_warning("damaged"))
+ else
+ to_chat(user,"Speech Synthesizer: " + span_warning("missing"))
+
+ if(istype(I,/obj/item/multitool))
+ if(!panel_open)
+ to_chat(user, span_warning("You can't do that in this state."))
+ else
+ var/list/parts = list()
+ if(cell != PP_MISSING)
+ parts |= "cell"
+ if(processor != PP_MISSING)
+ parts |= "processor"
+ if(board != PP_MISSING)
+ parts |= "board"
+ if(capacitor != PP_MISSING)
+ parts |= "capacitor"
+ if(projector != PP_MISSING)
+ parts |= "projector"
+ if(emitter != PP_MISSING)
+ parts |= "emitter"
+ if(speech_synthesizer != PP_MISSING)
+ parts |= "speech synthesizer"
+
+ var/choice = tgui_input_list(user, "Which part would you like to check?", "Check part", parts)
+ switch(choice)
+ if("cell")
+ if(cell == PP_FUNCTIONAL)
+ to_chat(user,"Power cell: " + span_notice("functional"))
+ else if(speech_synthesizer == PP_BROKEN)
+ to_chat(user,"Power cell: " + span_warning("damaged"))
+ else
+ to_chat(user,"Power cell: " + span_warning("missing"))
+
+ if("processor")
+ if(processor == PP_FUNCTIONAL)
+ to_chat(user,"Processor: " + span_notice("functional"))
+ else if(speech_synthesizer == PP_BROKEN)
+ to_chat(user,"Processor: " + span_warning("damaged"))
+ else
+ to_chat(user,"Processor: " + span_warning("missing"))
+
+ if("board")
+ if(board == PP_FUNCTIONAL)
+ to_chat(user,"Board: " + span_notice("functional"))
+ else if(speech_synthesizer == PP_BROKEN)
+ to_chat(user,"Board: " + span_warning("damaged"))
+ else
+ to_chat(user,"Board: " + span_warning("missing"))
+
+ if("capacitor")
+ if(capacitor == PP_FUNCTIONAL)
+ to_chat(user,"Capacitors: " + span_notice("functional"))
+ else if(speech_synthesizer == PP_BROKEN)
+ to_chat(user,"Capacitors: " + span_warning("damaged"))
+ else
+ to_chat(user,"Capacitors: " + span_warning("missing"))
+
+ if("projector")
+ if(projector == PP_FUNCTIONAL)
+ to_chat(user,"Projectors: " + span_notice("functional"))
+ else if(speech_synthesizer == PP_BROKEN)
+ to_chat(user,"Projectors: " + span_warning("damaged"))
+ else
+ to_chat(user,"Projectors: " + span_warning("missing"))
+
+ if("emitter")
+ if(emitter == PP_FUNCTIONAL)
+ to_chat(user,"Emitters: " + span_notice("functional"))
+ else if(speech_synthesizer == PP_BROKEN)
+ to_chat(user,"Emitters: " + span_warning("damaged"))
+ else
+ to_chat(user,"Emitters: " + span_warning("missing"))
+
+ if("speech synthesizer")
+ if(speech_synthesizer == PP_FUNCTIONAL)
+ to_chat(user,"Speech Synthesizer: " + span_notice("functional"))
+ else if(speech_synthesizer == PP_BROKEN)
+ to_chat(user,"Speech Synthesizer: " + span_warning("damaged"))
+ else
+ to_chat(user,"Speech Synthesizer: " + span_warning("missing"))
+
+ if(istype(I,/obj/item/paiparts/cell))
+ if(cell == PP_MISSING)
+ if(do_after(user, 3 SECONDS, target = src))
+ user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src]."))
+ cell = PP_FUNCTIONAL
+ user.drop_from_inventory(I)
+ qdel(I)
+ else
+ to_chat(user, span_warning("You would need to remove the installed [I] first!"))
+ if(istype(I,/obj/item/paiparts/processor))
+ if(processor == PP_MISSING)
+ if(do_after(user, 3 SECONDS, target = src))
+ user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src]."))
+ processor = PP_FUNCTIONAL
+ user.drop_from_inventory(I)
+ qdel(I)
+ else
+ to_chat(user, span_warning("You would need to remove the installed [I] first!"))
+ if(istype(I,/obj/item/paiparts/board))
+ if(board == PP_MISSING)
+ if(do_after(user, 3 SECONDS, target = src))
+ user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src]."))
+ board = PP_FUNCTIONAL
+ user.drop_from_inventory(I)
+ qdel(I)
+ else
+ to_chat(user, span_warning("You would need to remove the installed [I] first!"))
+ if(istype(I,/obj/item/paiparts/capacitor))
+ if(capacitor == PP_MISSING)
+ if(do_after(user, 3 SECONDS, target = src))
+ user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src]."))
+ capacitor = PP_FUNCTIONAL
+ user.drop_from_inventory(I)
+ qdel(I)
+ else
+ to_chat(user, span_warning("You would need to remove the installed [I] first!"))
+ if(istype(I,/obj/item/paiparts/projector))
+ if(projector == PP_MISSING)
+ if(do_after(user, 3 SECONDS, target = src))
+ user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src]."))
+ projector = PP_FUNCTIONAL
+ user.drop_from_inventory(I)
+ qdel(I)
+ else
+ to_chat(user, span_warning("You would need to remove the installed [I] first!"))
+ if(istype(I,/obj/item/paiparts/emitter))
+ if(emitter == PP_MISSING)
+ if(do_after(user, 3 SECONDS, target = src))
+ user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src]."))
+ emitter = PP_FUNCTIONAL
+ user.drop_from_inventory(I)
+ qdel(I)
+ else
+ to_chat(user, span_warning("You would need to remove the installed [I] first!"))
+ if(istype(I,/obj/item/paiparts/speech_synthesizer))
+ if(speech_synthesizer == PP_MISSING)
+ if(do_after(user, 3 SECONDS, target = src))
+ user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src]."))
+ speech_synthesizer = PP_FUNCTIONAL
+ user.drop_from_inventory(I)
+ qdel(I)
+ else
+ to_chat(user, span_warning("You would need to remove the installed [I] first!"))
+
+/obj/item/paicard/attack_self(mob/user, callback)
+ . = ..(user)
+ if(.)
+ return TRUE
+ if(special_handling && !callback)
+ return FALSE
+ if(!panel_open)
+ tgui_interact(user)
+ return
+ var/list/parts = list()
+ if(cell != PP_MISSING)
+ parts |= "cell"
+ if(processor != PP_MISSING)
+ parts |= "processor"
+ if(board != PP_MISSING)
+ parts |= "board"
+ if(capacitor != PP_MISSING)
+ parts |= "capacitor"
+ if(projector != PP_MISSING)
+ parts |= "projector"
+ if(emitter != PP_MISSING)
+ parts |= "emitter"
+ if(speech_synthesizer != PP_MISSING)
+ parts |= "speech synthesizer"
+
+ var/choice = tgui_input_list(user, "Which part would you like to remove?", "Remove part", parts)
+ if(choice)
+ playsound(src, 'sound/items/pickup/component.ogg', vary = TRUE)
+ else
+ return
+ if(!do_after(user, 3 SECONDS, target = src))
+ return
+ switch(choice)
+ if("cell")
+ if(cell == PP_FUNCTIONAL)
+ new /obj/item/paiparts/cell(get_turf(user))
+ else
+ new /obj/item/paiparts(get_turf(user))
+ user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src]."))
+ cell = PP_MISSING
+ if("processor")
+ if(processor == PP_FUNCTIONAL)
+ new /obj/item/paiparts/processor(get_turf(user))
+ else
+ new /obj/item/paiparts(get_turf(user))
+ user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src]."))
+ processor = PP_MISSING
+ if("board")
+ board = PP_MISSING
+ if(board == PP_FUNCTIONAL)
+ new /obj/item/paiparts/board(get_turf(user))
+ else
+ new /obj/item/paiparts(get_turf(user))
+ user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src]."))
+
+ if("capacitor")
+ if(capacitor == PP_FUNCTIONAL)
+ new /obj/item/paiparts/capacitor(get_turf(user))
+ else
+ new /obj/item/paiparts(get_turf(user))
+ user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src]."))
+ capacitor = PP_MISSING
+ if("projector")
+ if(projector == PP_FUNCTIONAL)
+ new /obj/item/paiparts/projector(get_turf(user))
+ else
+ new /obj/item/paiparts(get_turf(user))
+ user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src]."))
+ projector = PP_MISSING
+ if("emitter")
+ if(emitter == PP_FUNCTIONAL)
+ new /obj/item/paiparts/emitter(get_turf(user))
+ else
+ new /obj/item/paiparts(get_turf(user))
+ user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src]."))
+ emitter = PP_MISSING
+ if("speech synthesizer")
+ if(speech_synthesizer == PP_FUNCTIONAL)
+ new /obj/item/paiparts/speech_synthesizer(get_turf(user))
+ else
+ new /obj/item/paiparts(get_turf(user))
+ user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src]."))
+ speech_synthesizer = PP_MISSING
+
+/obj/item/paicard/proc/death_damage()
+ var/number = rand(1,4)
+ while(number)
+ number --
+ switch(rand(1,4))
+ if(1)
+ cell = PP_BROKEN
+ if(2)
+ processor = PP_BROKEN
+ if(3)
+ board = PP_BROKEN
+ if(4)
+ capacitor = PP_BROKEN
+
+/obj/item/paicard/proc/damage_random_component(nonfatal = FALSE)
+ var/datum/effect/effect/system/spark_spread/s = new /datum/effect/effect/system/spark_spread
+ s.set_up(2, 1, src)
+ s.start()
+ if(prob(80) || nonfatal) //Way more likely to be non-fatal part damage
+ switch(rand(1,3))
+ if(1)
+ projector = PP_BROKEN
+ if(2)
+ emitter = PP_BROKEN
+ if(3)
+ speech_synthesizer = PP_BROKEN
+ else
+ switch(rand(1,4))
+ if(1)
+ cell = PP_BROKEN
+ if(2)
+ processor = PP_BROKEN
+ if(3)
+ board = PP_BROKEN
+ if(4)
+ capacitor = PP_BROKEN
+
+/obj/item/paicard/proc/is_damage_critical()
+ if(cell != PP_FUNCTIONAL || processor != PP_FUNCTIONAL || board != PP_FUNCTIONAL || capacitor != PP_FUNCTIONAL)
+ return TRUE
+ return FALSE
+
+///////////////////////////////
+//////////pAI Parts //////////
+///////////////////////////////
+/obj/item/paiparts
+ name = "broken pAI component"
+ desc = "It's broken scrap from a pAI card!"
+ icon = 'icons/obj/paicard.dmi'
+ icon_state = "broken"
+ pickup_sound = 'sound/items/pickup/card.ogg'
+ drop_sound = 'sound/items/drop/card.ogg'
+
+/obj/item/paiparts/Initialize(mapload)
+ . = ..()
+ pixel_x = rand(-10,10)
+ pixel_y = rand(-10,10)
+
+/obj/item/paiparts/cell
+ name = "pAI power cell"
+ desc = "It's very small and efficient! It powers the pAI!"
+ icon_state = "cell"
+
+/obj/item/paiparts/processor
+ name = "pAI processor"
+ desc = "It's the brain of your computer friend!"
+ icon_state = "processor"
+
+/obj/item/paiparts/board
+ name = "pAI board"
+ desc = "It's the thing all the other parts get attatched to!"
+ icon_state = "board"
+
+/obj/item/paiparts/capacitor
+ name = "pAI capacitor"
+ desc = "It helps regulate power flow!"
+ icon_state = "capacitor"
+
+/obj/item/paiparts/projector
+ name = "pAI projector"
+ desc = "It projects the pAI's form!"
+ icon_state = "projector"
+
+/obj/item/paiparts/emitter
+ name = "pAI emitter"
+ desc = "It emits the fields to help the pAI get around!"
+ icon_state = "emitter"
+
+/obj/item/paiparts/speech_synthesizer
+ name = "pAI speech synthesizer"
+ desc = "It's a little voice box!"
+ icon_state = "speech_synthesizer"
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// VoreEdit: Living Machine Stuff after this.
// This adds a var and proc for all machines to take a pAI. (The pAI can't control anything, it's just for RP.)
// You need to add usage of the proc to each machine to actually add support. For an example of this, see code\modules\food\kitchen\microwave.dm
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/obj/machinery
var/obj/item/paicard/paicard = null
diff --git a/code/game/objects/items/devices/paicard_vr.dm b/code/game/objects/items/devices/paicard_vr.dm
deleted file mode 100644
index e9db9a1806..0000000000
--- a/code/game/objects/items/devices/paicard_vr.dm
+++ /dev/null
@@ -1,386 +0,0 @@
-GLOBAL_LIST_EMPTY(paikeys)
-
-/obj/item/paicard
- var/panel_open = FALSE
- var/cell = PP_FUNCTIONAL //critical- power
- var/processor = PP_FUNCTIONAL //critical- the thinky part
- var/board = PP_FUNCTIONAL //critical- makes everything work
- var/capacitor = PP_FUNCTIONAL //critical- power processing
- var/projector = PP_FUNCTIONAL //non-critical- affects unfolding
- var/emitter = PP_FUNCTIONAL //non-critical- affects unfolding
- var/speech_synthesizer = PP_FUNCTIONAL //non-critical- affects speech
-
-/obj/item/paicard/attackby(var/obj/item/I as obj, mob/user as mob)
- if(istype(I,/obj/item/tool/screwdriver))
- if(panel_open)
- panel_open = FALSE
- user.visible_message(span_notice("\The [user] secured \the [src]'s maintenance panel."))
- playsound(src, 'sound/items/Screwdriver.ogg', 50, 1)
- else if(pai)
- if(do_after(user, 3 SECONDS, target = src))
- panel_open = TRUE
- user.visible_message(span_warning("\The [user] opened \the [src]'s maintenance panel."))
- playsound(src, 'sound/items/Screwdriver.ogg', 50, 1)
- if(istype(I,/obj/item/robotanalyzer))
- if(!panel_open)
- to_chat(user, span_warning("The panel isn't open. You will need to unscrew it to open it."))
- else
- if(cell == PP_FUNCTIONAL)
- to_chat(user,"Power cell: " + span_notice("functional"))
- else if(cell == PP_BROKEN)
- to_chat(user,"Power cell: " + span_warning("damaged - CRITICAL"))
- else
- to_chat(user,"Power cell: " + span_warning("missing - CRITICAL"))
-
- if(processor == PP_FUNCTIONAL)
- to_chat(user,"Processor: " + span_notice("functional"))
- else if(processor == PP_BROKEN)
- to_chat(user,"Processor: " + span_warning("damaged - CRITICAL"))
- else
- to_chat(user,"Processor: " + span_warning("missing - CRITICAL"))
-
- if(board == PP_FUNCTIONAL)
- to_chat(user,"Board: " + span_notice("functional"))
- else if(board == PP_BROKEN)
- to_chat(user,"Board: " + span_warning("damaged - CRITICAL"))
- else
- to_chat(user,"Board: " + span_warning("missing - CRITICAL"))
-
- if(capacitor == PP_FUNCTIONAL)
- to_chat(user,"Capacitors: " + span_notice("functional"))
- else if(capacitor == PP_BROKEN)
- to_chat(user,"Capacitors: " + span_warning("damaged - CRITICAL"))
- else
- to_chat(user,"Capacitors: " + span_warning("missing - CRITICAL"))
-
- if(projector == PP_FUNCTIONAL)
- to_chat(user,"Projectors: " + span_notice("functional"))
- else if(projector == PP_BROKEN)
- to_chat(user,"Projectors: " + span_warning("damaged"))
- else
- to_chat(user,"Projectors: " + span_warning("missing"))
-
- if(emitter == PP_FUNCTIONAL)
- to_chat(user,"Emitters: " + span_notice("functional"))
- else if(emitter == PP_BROKEN)
- to_chat(user,"Emitters: " + span_warning("damaged"))
- else
- to_chat(user,"Emitters: " + span_warning("missing"))
-
- if(speech_synthesizer == PP_FUNCTIONAL)
- to_chat(user,"Speech Synthesizer: " + span_notice("functional"))
- else if(speech_synthesizer == PP_BROKEN)
- to_chat(user,"Speech Synthesizer: " + span_warning("damaged"))
- else
- to_chat(user,"Speech Synthesizer: " + span_warning("missing"))
-
- if(istype(I,/obj/item/multitool))
- if(!panel_open)
- to_chat(user, span_warning("You can't do that in this state."))
- else
- var/list/parts = list()
- if(cell != PP_MISSING)
- parts |= "cell"
- if(processor != PP_MISSING)
- parts |= "processor"
- if(board != PP_MISSING)
- parts |= "board"
- if(capacitor != PP_MISSING)
- parts |= "capacitor"
- if(projector != PP_MISSING)
- parts |= "projector"
- if(emitter != PP_MISSING)
- parts |= "emitter"
- if(speech_synthesizer != PP_MISSING)
- parts |= "speech synthesizer"
-
- var/choice = tgui_input_list(user, "Which part would you like to check?", "Check part", parts)
- switch(choice)
- if("cell")
- if(cell == PP_FUNCTIONAL)
- to_chat(user,"Power cell: " + span_notice("functional"))
- else if(speech_synthesizer == PP_BROKEN)
- to_chat(user,"Power cell: " + span_warning("damaged"))
- else
- to_chat(user,"Power cell: " + span_warning("missing"))
-
- if("processor")
- if(processor == PP_FUNCTIONAL)
- to_chat(user,"Processor: " + span_notice("functional"))
- else if(speech_synthesizer == PP_BROKEN)
- to_chat(user,"Processor: " + span_warning("damaged"))
- else
- to_chat(user,"Processor: " + span_warning("missing"))
-
- if("board")
- if(board == PP_FUNCTIONAL)
- to_chat(user,"Board: " + span_notice("functional"))
- else if(speech_synthesizer == PP_BROKEN)
- to_chat(user,"Board: " + span_warning("damaged"))
- else
- to_chat(user,"Board: " + span_warning("missing"))
-
- if("capacitor")
- if(capacitor == PP_FUNCTIONAL)
- to_chat(user,"Capacitors: " + span_notice("functional"))
- else if(speech_synthesizer == PP_BROKEN)
- to_chat(user,"Capacitors: " + span_warning("damaged"))
- else
- to_chat(user,"Capacitors: " + span_warning("missing"))
-
- if("projector")
- if(projector == PP_FUNCTIONAL)
- to_chat(user,"Projectors: " + span_notice("functional"))
- else if(speech_synthesizer == PP_BROKEN)
- to_chat(user,"Projectors: " + span_warning("damaged"))
- else
- to_chat(user,"Projectors: " + span_warning("missing"))
-
- if("emitter")
- if(emitter == PP_FUNCTIONAL)
- to_chat(user,"Emitters: " + span_notice("functional"))
- else if(speech_synthesizer == PP_BROKEN)
- to_chat(user,"Emitters: " + span_warning("damaged"))
- else
- to_chat(user,"Emitters: " + span_warning("missing"))
-
- if("speech synthesizer")
- if(speech_synthesizer == PP_FUNCTIONAL)
- to_chat(user,"Speech Synthesizer: " + span_notice("functional"))
- else if(speech_synthesizer == PP_BROKEN)
- to_chat(user,"Speech Synthesizer: " + span_warning("damaged"))
- else
- to_chat(user,"Speech Synthesizer: " + span_warning("missing"))
-
- if(istype(I,/obj/item/paiparts/cell))
- if(cell == PP_MISSING)
- if(do_after(user, 3 SECONDS, target = src))
- user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src]."))
- cell = PP_FUNCTIONAL
- user.drop_from_inventory(I)
- qdel(I)
- else
- to_chat(user, span_warning("You would need to remove the installed [I] first!"))
- if(istype(I,/obj/item/paiparts/processor))
- if(processor == PP_MISSING)
- if(do_after(user, 3 SECONDS, target = src))
- user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src]."))
- processor = PP_FUNCTIONAL
- user.drop_from_inventory(I)
- qdel(I)
- else
- to_chat(user, span_warning("You would need to remove the installed [I] first!"))
- if(istype(I,/obj/item/paiparts/board))
- if(board == PP_MISSING)
- if(do_after(user, 3 SECONDS, target = src))
- user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src]."))
- board = PP_FUNCTIONAL
- user.drop_from_inventory(I)
- qdel(I)
- else
- to_chat(user, span_warning("You would need to remove the installed [I] first!"))
- if(istype(I,/obj/item/paiparts/capacitor))
- if(capacitor == PP_MISSING)
- if(do_after(user, 3 SECONDS, target = src))
- user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src]."))
- capacitor = PP_FUNCTIONAL
- user.drop_from_inventory(I)
- qdel(I)
- else
- to_chat(user, span_warning("You would need to remove the installed [I] first!"))
- if(istype(I,/obj/item/paiparts/projector))
- if(projector == PP_MISSING)
- if(do_after(user, 3 SECONDS, target = src))
- user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src]."))
- projector = PP_FUNCTIONAL
- user.drop_from_inventory(I)
- qdel(I)
- else
- to_chat(user, span_warning("You would need to remove the installed [I] first!"))
- if(istype(I,/obj/item/paiparts/emitter))
- if(emitter == PP_MISSING)
- if(do_after(user, 3 SECONDS, target = src))
- user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src]."))
- emitter = PP_FUNCTIONAL
- user.drop_from_inventory(I)
- qdel(I)
- else
- to_chat(user, span_warning("You would need to remove the installed [I] first!"))
- if(istype(I,/obj/item/paiparts/speech_synthesizer))
- if(speech_synthesizer == PP_MISSING)
- if(do_after(user, 3 SECONDS, target = src))
- user.visible_message(span_notice("\The [user] installs \the [I] into \the [src]."),span_notice("You install \the [I] into \the [src]."))
- speech_synthesizer = PP_FUNCTIONAL
- user.drop_from_inventory(I)
- qdel(I)
- else
- to_chat(user, span_warning("You would need to remove the installed [I] first!"))
-
-/obj/item/paicard/attack_self(mob/user, callback)
- . = ..(user)
- if(.)
- return TRUE
- if(special_handling && !callback)
- return FALSE
- if(!panel_open)
- access_screen(user)
- return
- var/list/parts = list()
- if(cell != PP_MISSING)
- parts |= "cell"
- if(processor != PP_MISSING)
- parts |= "processor"
- if(board != PP_MISSING)
- parts |= "board"
- if(capacitor != PP_MISSING)
- parts |= "capacitor"
- if(projector != PP_MISSING)
- parts |= "projector"
- if(emitter != PP_MISSING)
- parts |= "emitter"
- if(speech_synthesizer != PP_MISSING)
- parts |= "speech synthesizer"
-
- var/choice = tgui_input_list(user, "Which part would you like to remove?", "Remove part", parts)
- if(choice)
- playsound(src, 'sound/items/pickup/component.ogg', vary = TRUE)
- else
- return
- if(!do_after(user, 3 SECONDS, target = src))
- return
- switch(choice)
- if("cell")
- if(cell == PP_FUNCTIONAL)
- new /obj/item/paiparts/cell(get_turf(user))
- else
- new /obj/item/paiparts(get_turf(user))
- user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src]."))
- cell = PP_MISSING
- if("processor")
- if(processor == PP_FUNCTIONAL)
- new /obj/item/paiparts/processor(get_turf(user))
- else
- new /obj/item/paiparts(get_turf(user))
- user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src]."))
- processor = PP_MISSING
- if("board")
- board = PP_MISSING
- if(board == PP_FUNCTIONAL)
- new /obj/item/paiparts/board(get_turf(user))
- else
- new /obj/item/paiparts(get_turf(user))
- user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src]."))
-
- if("capacitor")
- if(capacitor == PP_FUNCTIONAL)
- new /obj/item/paiparts/capacitor(get_turf(user))
- else
- new /obj/item/paiparts(get_turf(user))
- user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src]."))
- capacitor = PP_MISSING
- if("projector")
- if(projector == PP_FUNCTIONAL)
- new /obj/item/paiparts/projector(get_turf(user))
- else
- new /obj/item/paiparts(get_turf(user))
- user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src]."))
- projector = PP_MISSING
- if("emitter")
- if(emitter == PP_FUNCTIONAL)
- new /obj/item/paiparts/emitter(get_turf(user))
- else
- new /obj/item/paiparts(get_turf(user))
- user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src]."))
- emitter = PP_MISSING
- if("speech synthesizer")
- if(speech_synthesizer == PP_FUNCTIONAL)
- new /obj/item/paiparts/speech_synthesizer(get_turf(user))
- else
- new /obj/item/paiparts(get_turf(user))
- user.visible_message(span_warning("\The [user] removes \the [choice] from \the [src]."),span_warning("You remove \the [choice] from \the [src]."))
- speech_synthesizer = PP_MISSING
-
-/obj/item/paicard/proc/death_damage()
-
- var/number = rand(1,4)
- while(number)
- number --
- switch(rand(1,4))
- if(1)
- cell = PP_BROKEN
- if(2)
- processor = PP_BROKEN
- if(3)
- board = PP_BROKEN
- if(4)
- capacitor = PP_BROKEN
-
-
-/obj/item/paicard/proc/damage_random_component(nonfatal = FALSE)
- var/datum/effect/effect/system/spark_spread/s = new /datum/effect/effect/system/spark_spread
- s.set_up(2, 1, src)
- s.start()
- if(prob(80) || nonfatal) //Way more likely to be non-fatal part damage
- switch(rand(1,3))
- if(1)
- projector = PP_BROKEN
- if(2)
- emitter = PP_BROKEN
- if(3)
- speech_synthesizer = PP_BROKEN
- else
- switch(rand(1,4))
- if(1)
- cell = PP_BROKEN
- if(2)
- processor = PP_BROKEN
- if(3)
- board = PP_BROKEN
- if(4)
- capacitor = PP_BROKEN
-
-/obj/item/paicard/proc/is_damage_critical()
- if(cell != PP_FUNCTIONAL || processor != PP_FUNCTIONAL || board != PP_FUNCTIONAL || capacitor != PP_FUNCTIONAL)
- return TRUE
- return FALSE
-
-/obj/item/paiparts
- name = "broken pAI component"
- desc = "It's broken scrap from a pAI card!"
- icon = 'icons/obj/paicard.dmi'
- icon_state = "broken"
- pickup_sound = 'sound/items/pickup/card.ogg'
- drop_sound = 'sound/items/drop/card.ogg'
-/obj/item/paiparts/Initialize(mapload)
- . = ..()
- pixel_x = rand(-10,10)
- pixel_y = rand(-10,10)
-
-/obj/item/paiparts/cell
- name = "pAI power cell"
- desc = "It's very small and efficient! It powers the pAI!"
- icon_state = "cell"
-/obj/item/paiparts/processor
- name = "pAI processor"
- desc = "It's the brain of your computer friend!"
- icon_state = "processor"
-/obj/item/paiparts/board
- name = "pAI board"
- desc = "It's the thing all the other parts get attatched to!"
- icon_state = "board"
-/obj/item/paiparts/capacitor
- name = "pAI capacitor"
- desc = "It helps regulate power flow!"
- icon_state = "capacitor"
-/obj/item/paiparts/projector
- name = "pAI projector"
- desc = "It projects the pAI's form!"
- icon_state = "projector"
-/obj/item/paiparts/emitter
- name = "pAI emitter"
- desc = "It emits the fields to help the pAI get around!"
- icon_state = "emitter"
-/obj/item/paiparts/speech_synthesizer
- name = "pAI speech synthesizer"
- desc = "It's a little voice box!"
- icon_state = "speech_synthesizer"
diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm
index c382a84b5d..1936cf9040 100644
--- a/code/game/objects/objs.dm
+++ b/code/game/objects/objs.dm
@@ -144,6 +144,10 @@
/obj/proc/container_resist(var/mob/living)
return
+// If returns true, pai can interact with the object with a click
+/obj/proc/allow_pai_interaction(proximity_flag)
+ return FALSE
+
//To be called from things that spill objects on the floor.
//Makes an object move around randomly for a couple of tiles
/obj/proc/tumble(var/dist = 2)
diff --git a/code/game/objects/structures/crates_lockers/__closets.dm b/code/game/objects/structures/crates_lockers/__closets.dm
index dc067df234..54632231e0 100644
--- a/code/game/objects/structures/crates_lockers/__closets.dm
+++ b/code/game/objects/structures/crates_lockers/__closets.dm
@@ -539,6 +539,9 @@
M.Translate(closet_appearance.door_hinge, 0)
return M
+/obj/structure/closet/allow_pai_interaction(proximity_flag)
+ return proximity_flag
+
//verb to eat people in the same closet as yourself
/obj/structure/closet/verb/hidden_vore()
diff --git a/code/game/objects/structures/fence.dm b/code/game/objects/structures/fence.dm
index 4e0dafc9ac..298e5e33eb 100644
--- a/code/game/objects/structures/fence.dm
+++ b/code/game/objects/structures/fence.dm
@@ -211,6 +211,9 @@
if(get_dist(user,src) <= 1) //not remotely though
return attack_hand(user)
+/obj/structure/fence/door/allow_pai_interaction(proximity_flag)
+ return proximity_flag
+
/obj/structure/fence/door/proc/toggle(mob/user)
switch(open)
if(FALSE)
diff --git a/code/game/objects/structures/watercloset.dm b/code/game/objects/structures/watercloset.dm
index 4f5e054119..dd511a7e6c 100644
--- a/code/game/objects/structures/watercloset.dm
+++ b/code/game/objects/structures/watercloset.dm
@@ -201,6 +201,9 @@
if(istype(I, /obj/item/analyzer)) //Lol? Why...
to_chat(user, span_notice("The water temperature seems to be [current_temperature]."))
+/obj/machinery/shower/allow_pai_interaction(proximity_flag)
+ return proximity_flag
+
/obj/machinery/shower/click_alt(mob/user)
..()
var/list/temperature_settings = list(SHOWER_NORMAL, SHOWER_BOILING, SHOWER_FREEZING)
diff --git a/code/modules/admin/verbs/debug.dm b/code/modules/admin/verbs/debug.dm
index 12699adf13..8074220965 100644
--- a/code/modules/admin/verbs/debug.dm
+++ b/code/modules/admin/verbs/debug.dm
@@ -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!
diff --git a/code/modules/asset_cache/assets/spritesheets/pai_icons.dm b/code/modules/asset_cache/assets/spritesheets/pai_icons.dm
new file mode 100644
index 0000000000..75fffd8732
--- /dev/null
+++ b/code/modules/asset_cache/assets/spritesheets/pai_icons.dm
@@ -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)
diff --git a/code/modules/asset_cache/assets/spritesheets/robot_icons.dm b/code/modules/asset_cache/assets/spritesheets/robot_icons.dm
index 429e5f3432..9194281420 100644
--- a/code/modules/asset_cache/assets/spritesheets/robot_icons.dm
+++ b/code/modules/asset_cache/assets/spritesheets/robot_icons.dm
@@ -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)
diff --git a/code/modules/client/preference_setup/general/07_antagonism.dm b/code/modules/client/preference_setup/general/07_antagonism.dm
index 9d14c3654f..48f0c4a7fa 100644
--- a/code/modules/client/preference_setup/general/07_antagonism.dm
+++ b/code/modules/client/preference_setup/general/07_antagonism.dm
@@ -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
diff --git a/code/modules/client/preferences/migrations/19_paifile.dm b/code/modules/client/preferences/migrations/19_paifile.dm
new file mode 100644
index 0000000000..8cab27586d
--- /dev/null
+++ b/code/modules/client/preferences/migrations/19_paifile.dm
@@ -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()
diff --git a/code/modules/client/preferences/types/character/pai/01_basic.dm b/code/modules/client/preferences/types/character/pai/01_basic.dm
new file mode 100644
index 0000000000..b5df4c5c04
--- /dev/null
+++ b/code/modules/client/preferences/types/character/pai/01_basic.dm
@@ -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
diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm
index 858a26269a..37534d4dc0 100644
--- a/code/modules/client/preferences_savefile.dm
+++ b/code/modules/client/preferences_savefile.dm
@@ -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)
diff --git a/code/modules/client/preferences_tgui.dm b/code/modules/client/preferences_tgui.dm
index 87e63ff698..3e1c5069ba 100644
--- a/code/modules/client/preferences_tgui.dm
+++ b/code/modules/client/preferences_tgui.dm
@@ -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)
diff --git a/code/modules/emotes/definitions/audible_furry_vr.dm b/code/modules/emotes/definitions/audible_furry_vr.dm
index 4e2aa85770..479f91cde0 100644
--- a/code/modules/emotes/definitions/audible_furry_vr.dm
+++ b/code/modules/emotes/definitions/audible_furry_vr.dm
@@ -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)
diff --git a/code/modules/instruments/stationary.dm b/code/modules/instruments/stationary.dm
index d2d83fd9a0..71f4bb930c 100644
--- a/code/modules/instruments/stationary.dm
+++ b/code/modules/instruments/stationary.dm
@@ -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)
. = ..()
diff --git a/code/modules/mining/shelters_vr.dm b/code/modules/mining/shelters_vr.dm
index 45fcac9f0f..4ae99d1af6 100644
--- a/code/modules/mining/shelters_vr.dm
+++ b/code/modules/mining/shelters_vr.dm
@@ -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
diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm
index 7a3f2efd65..7aaadfd01a 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -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"
diff --git a/code/modules/mob/living/login.dm b/code/modules/mob/living/login.dm
index 1d117d730d..56bc850d3d 100644
--- a/code/modules/mob/living/login.dm
+++ b/code/modules/mob/living/login.dm
@@ -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)
diff --git a/code/modules/mob/living/silicon/pai/chassis_ui.dm b/code/modules/mob/living/silicon/pai/chassis_ui.dm
new file mode 100644
index 0000000000..4624ed60f7
--- /dev/null
+++ b/code/modules/mob/living/silicon/pai/chassis_ui.dm
@@ -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
diff --git a/code/modules/mob/living/silicon/pai/death.dm b/code/modules/mob/living/silicon/pai/death.dm
index 5180ec1a90..30edde7839 100644
--- a/code/modules/mob/living/silicon/pai/death.dm
+++ b/code/modules/mob/living/silicon/pai/death.dm
@@ -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")
diff --git a/code/modules/mob/living/silicon/pai/life.dm b/code/modules/mob/living/silicon/pai/life.dm
index b2893fe172..c518959c65 100644
--- a/code/modules/mob/living/silicon/pai/life.dm
+++ b/code/modules/mob/living/silicon/pai/life.dm
@@ -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)
diff --git a/code/modules/mob/living/silicon/pai/pai.dm b/code/modules/mob/living/silicon/pai/pai.dm
index e02028bfb8..0a463d9d53 100644
--- a/code/modules/mob/living/silicon/pai/pai.dm
+++ b/code/modules/mob/living/silicon/pai/pai.dm
@@ -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"
diff --git a/code/modules/mob/living/silicon/pai/pai_folding.dm b/code/modules/mob/living/silicon/pai/pai_folding.dm
new file mode 100644
index 0000000000..75b1b0ebb2
--- /dev/null
+++ b/code/modules/mob/living/silicon/pai/pai_folding.dm
@@ -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))
diff --git a/code/modules/mob/living/silicon/pai/pai_hud.dm b/code/modules/mob/living/silicon/pai/pai_hud.dm
index a8e4385a77..45af2b6941 100644
--- a/code/modules/mob/living/silicon/pai/pai_hud.dm
+++ b/code/modules/mob/living/silicon/pai/pai_hud.dm
@@ -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
diff --git a/code/modules/mob/living/silicon/pai/pai_software.dm b/code/modules/mob/living/silicon/pai/pai_software.dm
new file mode 100644
index 0000000000..1240f50fff
--- /dev/null
+++ b/code/modules/mob/living/silicon/pai/pai_software.dm
@@ -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")
diff --git a/code/modules/mob/living/silicon/pai/pai_verbs.dm b/code/modules/mob/living/silicon/pai/pai_verbs.dm
new file mode 100644
index 0000000000..e5319fea38
--- /dev/null
+++ b/code/modules/mob/living/silicon/pai/pai_verbs.dm
@@ -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)
diff --git a/code/modules/mob/living/silicon/pai/pai_vr.dm b/code/modules/mob/living/silicon/pai/pai_vr.dm
deleted file mode 100644
index 7c5972b46e..0000000000
--- a/code/modules/mob/living/silicon/pai/pai_vr.dm
+++ /dev/null
@@ -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()
diff --git a/code/modules/mob/living/silicon/pai/personality.dm b/code/modules/mob/living/silicon/pai/personality.dm
deleted file mode 100644
index 1824789f55..0000000000
--- a/code/modules/mob/living/silicon/pai/personality.dm
+++ /dev/null
@@ -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
diff --git a/code/modules/mob/living/silicon/pai/recruit.dm b/code/modules/mob/living/silicon/pai/recruit.dm
deleted file mode 100644
index 1a6603c78a..0000000000
--- a/code/modules/mob/living/silicon/pai/recruit.dm
+++ /dev/null
@@ -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 += {"
-
- "}
-
- dat += {"
-
- pAI Personality Configuration
- Please configure your pAI personality's options. Remember, what you enter here could determine whether or not the user requesting a personality chooses you!
-
-
-
- | Name: |
- [candidate.name] |
-
-
- | What you plan to call yourself. Suggestions: Any character name you would choose for a station character OR an AI. |
-
-
- | Description: |
- [candidate.description] |
-
-
- | What sort of pAI you typically play; your mannerisms, your quirks, etc. This can be as sparse or as detailed as you like. |
-
-
- | Preferred Role: |
- [candidate.role] |
-
-
- | Do you like to partner with sneaky social ninjas? Like to help security hunt down thugs? Enjoy watching an engineer's back while he saves the station yet again? This doesn't have to be limited to just station jobs. Pretty much any general descriptor for what you'd like to be doing works here. |
-
-
- | OOC Comments: |
- [candidate.comments] |
-
-
- | 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. |
-
-
-
-
- "}
- if(allowSubmit)
- dat += {"
-
- "}
- dat += {"
-
- "}
-
- M << browse("[dat]", "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 += {"
-
-
-
-
-
-
- pAI Availability List
- "}
- dat += "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.
"
-
- for(var/datum/paiCandidate/c in available)
- dat += {"
-
-
- | Name: |
- [c.name] |
-
-
- | Description: |
- [c.description] |
-
-
- | Preferred Role: |
- [c.role] |
-
-
- | OOC Comments: |
- [c.comments] |
-
-
-
-
- "}
-
- dat += {"
-
-
- "}
-
- 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
diff --git a/code/modules/mob/living/silicon/pai/software.dm b/code/modules/mob/living/silicon/pai/software.dm
index 67fdc68969..59693e3684 100644
--- a/code/modules/mob/living/silicon/pai/software.dm
+++ b/code/modules/mob/living/silicon/pai/software.dm
@@ -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
diff --git a/code/modules/mob/living/silicon/pai/sprites/_sprite_datum.dm b/code/modules/mob/living/silicon/pai/sprites/_sprite_datum.dm
new file mode 100644
index 0000000000..c1425a2ba9
--- /dev/null
+++ b/code/modules/mob/living/silicon/pai/sprites/_sprite_datum.dm
@@ -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"
diff --git a/code/modules/mob/living/silicon/robot/robot_ui_module.dm b/code/modules/mob/living/silicon/robot/robot_ui_module.dm
index 2923f0c539..6548a8672f 100644
--- a/code/modules/mob/living/silicon/robot/robot_ui_module.dm
+++ b/code/modules/mob/living/silicon/robot/robot_ui_module.dm
@@ -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
diff --git a/code/modules/mob/new_player/lobby_browser.dm b/code/modules/mob/new_player/lobby_browser.dm
index 9d1391b5b1..dfd984f082 100644
--- a/code/modules/mob/new_player/lobby_browser.dm
+++ b/code/modules/mob/new_player/lobby_browser.dm
@@ -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()
diff --git a/code/modules/organs/subtypes/standard.dm b/code/modules/organs/subtypes/standard.dm
index 79fbdb6ce8..1b3aa7bdab 100644
--- a/code/modules/organs/subtypes/standard.dm
+++ b/code/modules/organs/subtypes/standard.dm
@@ -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
diff --git a/code/modules/power/sensors/sensor_monitoring.dm b/code/modules/power/sensors/sensor_monitoring.dm
index 79210ca478..2def9f56a1 100644
--- a/code/modules/power/sensors/sensor_monitoring.dm
+++ b/code/modules/power/sensors/sensor_monitoring.dm
@@ -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)
diff --git a/code/modules/recycling/conveyor2.dm b/code/modules/recycling/conveyor2.dm
index 9595d1c6d1..37321fadbd 100644
--- a/code/modules/recycling/conveyor2.dm
+++ b/code/modules/recycling/conveyor2.dm
@@ -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
diff --git a/code/modules/recycling/disposal_machines.dm b/code/modules/recycling/disposal_machines.dm
index b698194212..7a727e316c 100644
--- a/code/modules/recycling/disposal_machines.dm
+++ b/code/modules/recycling/disposal_machines.dm
@@ -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)
diff --git a/code/modules/resleeving/sleevecard.dm b/code/modules/resleeving/sleevecard.dm
index 3fa313ad6c..645d1a5f4e 100644
--- a/code/modules/resleeving/sleevecard.dm
+++ b/code/modules/resleeving/sleevecard.dm
@@ -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
diff --git a/code/modules/tgui/states/default.dm b/code/modules/tgui/states/default.dm
index 7010735609..b5737eee3d 100644
--- a/code/modules/tgui/states/default.dm
+++ b/code/modules/tgui/states/default.dm
@@ -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
diff --git a/code/modules/turbolift/turbolift_console.dm b/code/modules/turbolift/turbolift_console.dm
index 22848ceb92..9b74ee66e7 100644
--- a/code/modules/turbolift/turbolift_console.dm
+++ b/code/modules/turbolift/turbolift_console.dm
@@ -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
diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm
index 0fb8f6fb54..8dc1f82a5c 100644
--- a/code/modules/unit_tests/_unit_tests.dm
+++ b/code/modules/unit_tests/_unit_tests.dm
@@ -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"
diff --git a/code/modules/unit_tests/pai_tests.dm b/code/modules/unit_tests/pai_tests.dm
new file mode 100644
index 0000000000..1b63bde038
--- /dev/null
+++ b/code/modules/unit_tests/pai_tests.dm
@@ -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.")
diff --git a/code/modules/unit_tests/special_roles.dm b/code/modules/unit_tests/special_roles.dm
new file mode 100644
index 0000000000..ed24ed3f6a
--- /dev/null
+++ b/code/modules/unit_tests/special_roles.dm
@@ -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++
diff --git a/icons/mob/pai.dmi b/icons/mob/pai.dmi
index df6010159b3e6abf7b6cc7f94777d7c77a044903..1e377affd3d5f0de43ce9cd3f92c4051d6470f59 100644
GIT binary patch
literal 192146
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