diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm
index 07954aab2f5..ca93ee078a6 100644
--- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm
+++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm
@@ -109,3 +109,6 @@
#define COMSIG_MOVABLE_MESSAGE_GET_NAME_PART "movable_message_get_name_part"
///The index of the name part
#define NAME_PART_INDEX 1
+
+///from /datum/component/track_hierarchical_movement/on_moved() if atom or any of its containers has moved: (atom/old_loc, dir, forced, list/old_locs)
+#define COMSIG_MOVABLE_OR_CONTAINER_MOVED "movable_or_container_moved"
diff --git a/code/__DEFINES/pai.dm b/code/__DEFINES/pai.dm
index f47524d32e2..5e5e13b1867 100644
--- a/code/__DEFINES/pai.dm
+++ b/code/__DEFINES/pai.dm
@@ -11,6 +11,11 @@
/// The amount of time between spamming for pAI candidates
#define PAI_SPAM_TIME (40 SECONDS)
+/// Maximum distance you can set the holoform leash
+#define HOLOFORM_MAX_RANGE 9
+/// Minimum distance you can set the holoform leash
+#define HOLOFORM_MIN_RANGE 3
+
/// UI action to toggle huds
#define PAI_TOGGLE_MEDICAL_HUD 0
#define PAI_TOGGLE_SECURITY_HUD 1
diff --git a/code/__DEFINES/robots.dm b/code/__DEFINES/robots.dm
index 20d838b80f7..b3273da3175 100644
--- a/code/__DEFINES/robots.dm
+++ b/code/__DEFINES/robots.dm
@@ -87,7 +87,7 @@
///The Bot is currently allowed to be remote controlled by Silicon.
#define BOT_MODE_REMOTE_ENABLED (1<<2)
///The Bot is allowed to have a ghost placed in control of it.
-#define BOT_MODE_GHOST_CONTROLLABLE (1<<3)
+#define BOT_MODE_CAN_BE_SAPIENT (1<<3)
//Bot cover defines indicating the Bot's status
///The Bot's cover is open and can be modified/emagged by anyone.
diff --git a/code/datums/components/track_hierarchical_movement.dm b/code/datums/components/track_hierarchical_movement.dm
new file mode 100644
index 00000000000..ed2b5e0dc64
--- /dev/null
+++ b/code/datums/components/track_hierarchical_movement.dm
@@ -0,0 +1,43 @@
+/**
+ * Component which outputs a signal if the attached atom or any of its containers moves
+ */
+/datum/component/track_hierarchical_movement
+ /// List of things we're currently listening out for movement from
+ var/list/containers
+
+/datum/component/track_hierarchical_movement/Initialize()
+ . = ..()
+ if (!ismovable(parent))
+ return COMPONENT_INCOMPATIBLE
+
+/datum/component/track_hierarchical_movement/Destroy(force, silent)
+ LAZYCLEARLIST(containers)
+ return ..()
+
+/datum/component/track_hierarchical_movement/RegisterWithParent()
+ . = ..()
+ RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved))
+ rebuild_hierarchy()
+
+/datum/component/track_hierarchical_movement/UnregisterFromParent()
+ . = ..()
+ UnregisterSignal(parent, COMSIG_MOVABLE_MOVED)
+ for (var/container in containers)
+ UnregisterSignal(container, COMSIG_MOVABLE_MOVED)
+
+/// Something in our hierarchy moved, send signal then rebuild hierarchy
+/datum/component/track_hierarchical_movement/proc/on_moved(datum/source, old_loc, dir, forced)
+ SIGNAL_HANDLER
+ SEND_SIGNAL(parent, COMSIG_MOVABLE_OR_CONTAINER_MOVED, old_loc, dir, forced)
+ rebuild_hierarchy()
+
+/// Listen for the movement signal on every parent which isn't the floor or the void
+/datum/component/track_hierarchical_movement/proc/rebuild_hierarchy()
+ for (var/container in containers)
+ UnregisterSignal(container, COMSIG_MOVABLE_MOVED)
+ LAZYCLEARLIST(containers)
+ var/atom/checked = parent
+ while (!isnull(checked.loc) && !isturf(checked.loc))
+ RegisterSignal(checked.loc, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved))
+ checked = checked.loc
+ LAZYADD(containers, checked)
diff --git a/code/modules/mob/living/simple_animal/bot/bot.dm b/code/modules/mob/living/simple_animal/bot/bot.dm
index 0e1dfab398b..c3d36b58f01 100644
--- a/code/modules/mob/living/simple_animal/bot/bot.dm
+++ b/code/modules/mob/living/simple_animal/bot/bot.dm
@@ -37,6 +37,8 @@
var/maints_access_required = list(ACCESS_ROBOTICS)
///The Robot arm attached to this robot - has a 50% chance to drop on death.
var/robot_arm = /obj/item/bodypart/arm/right/robot
+ ///The inserted (if any) pAI in this bot.
+ var/obj/item/pai_card/paicard
///The type of bot it is, for radio control.
var/bot_type = NONE
@@ -45,8 +47,8 @@
///All initial access this bot started with.
var/list/prev_access = list()
- ///Bot-related mode flags on the Bot indicating how they will act. BOT_MODE_ON | BOT_MODE_AUTOPATROL | BOT_MODE_REMOTE_ENABLED | BOT_MODE_GHOST_CONTROLLABLE
- var/bot_mode_flags = BOT_MODE_ON | BOT_MODE_REMOTE_ENABLED | BOT_MODE_GHOST_CONTROLLABLE
+ ///Bot-related mode flags on the Bot indicating how they will act. BOT_MODE_ON | BOT_MODE_AUTOPATROL | BOT_MODE_REMOTE_ENABLED | BOT_MODE_CAN_BE_SAPIENT
+ var/bot_mode_flags = BOT_MODE_ON | BOT_MODE_REMOTE_ENABLED | BOT_MODE_CAN_BE_SAPIENT
///Bot-related cover flags on the Bot to deal with what has been done to their cover, including emagging. BOT_COVER_OPEN | BOT_COVER_LOCKED | BOT_COVER_EMAGGED | BOT_COVER_HACKED
var/bot_cover_flags = BOT_COVER_LOCKED
@@ -113,7 +115,7 @@
/mob/living/simple_animal/bot/proc/get_mode()
if(client) //Player bots do not have modes, thus the override. Also an easy way for PDA users/AI to know when a bot is a player.
- return "Autonomous"
+ return paicard ? "pAI Controlled" : "Autonomous"
if(!(bot_mode_flags & BOT_MODE_ON))
return "Inactive"
return "[mode]"
@@ -123,7 +125,7 @@
*/
/mob/living/simple_animal/bot/proc/get_mode_ui()
if(client) //Player bots do not have modes, thus the override. Also an easy way for PDA users/AI to know when a bot is a player.
- return "Autonomous"
+ return paicard ? "pAI Controlled" : "Autonomous"
if(!(bot_mode_flags & BOT_MODE_ON))
return "Inactive"
return "[mode]"
@@ -190,10 +192,12 @@
if(HAS_TRAIT(SSstation, STATION_TRAIT_BOTS_GLITCHED))
randomize_language_if_on_station()
- if(mapload && is_station_level(z) && (bot_mode_flags & BOT_MODE_GHOST_CONTROLLABLE))
- enable_possession(mapload)
+ if(mapload && is_station_level(z) && (bot_mode_flags & BOT_MODE_CAN_BE_SAPIENT))
+ enable_possession(mapload = mapload)
/mob/living/simple_animal/bot/Destroy()
+ if(paicard)
+ ejectpai()
GLOB.bots_list -= src
QDEL_NULL(personality_download)
QDEL_NULL(internal_radio)
@@ -202,7 +206,10 @@
return ..()
/// Allows this bot to be controlled by a ghost, who will become its mind
-/mob/living/simple_animal/bot/proc/enable_possession(mapload = FALSE)
+/mob/living/simple_animal/bot/proc/enable_possession(user, mapload = FALSE)
+ if (paicard)
+ balloon_alert(user, "already sapient!")
+ return
can_be_possessed = TRUE
var/can_announce = !mapload && COOLDOWN_FINISHED(src, offer_ghosts_cooldown)
personality_download = AddComponent(\
@@ -221,14 +228,15 @@
/mob/living/simple_animal/bot/proc/disable_possession(mob/user)
can_be_possessed = FALSE
QDEL_NULL(personality_download)
- if (mind)
- if (user)
- log_combat(user, src, "ejected from [initial(src.name)] control.")
- to_chat(src, span_warning("You feel yourself fade as your personality matrix is reset!"))
- ghostize(can_reenter_corpse = FALSE)
- playsound(src, 'sound/machines/ping.ogg', 30, TRUE)
- say("Personally matrix reset!", forced = "bot")
- key = null
+ if (!key)
+ return
+ if (user)
+ log_combat(user, src, "ejected from [initial(src.name)] control.")
+ to_chat(src, span_warning("You feel yourself fade as your personality matrix is reset!"))
+ ghostize(can_reenter_corpse = FALSE)
+ playsound(src, 'sound/machines/ping.ogg', 30, TRUE)
+ say("Personality matrix reset!", forced = "bot")
+ key = null
/// Returns true if this mob can be controlled
/mob/living/simple_animal/bot/proc/check_possession(mob/potential_possessor)
@@ -334,6 +342,10 @@
var/is_sillycone = issilicon(user)
if(!(bot_cover_flags & BOT_COVER_EMAGGED) && (is_sillycone || user.Adjacent(src)))
. += span_info("Alt-click [is_sillycone ? "" : "or use your ID on "]it to [bot_cover_flags & BOT_COVER_LOCKED ? "un" : ""]lock its control panel.")
+ if(paicard)
+ . += span_notice("It has a pAI device installed.")
+ if(!(bot_cover_flags & BOT_COVER_OPEN))
+ . += span_info("You can use a hemostat to remove it.")
/mob/living/simple_animal/bot/adjustHealth(amount, updating_health = TRUE, forced = FALSE)
if(amount > 0 && prob(10))
@@ -453,6 +465,19 @@
if(attacking_item.GetID())
unlock_with_id(user)
return
+ if(istype(attacking_item, /obj/item/pai_card))
+ insertpai(user, attacking_item)
+ return
+ if(attacking_item.tool_behaviour == TOOL_HEMOSTAT && paicard)
+ if(bot_cover_flags & BOT_COVER_OPEN)
+ balloon_alert(user, "open the access panel!")
+ else
+ balloon_alert(user, "removing pAI...")
+ if(!do_after(user, 3 SECONDS, target = src) || !paicard)
+ return
+ user.visible_message(span_notice("[user] uses [attacking_item] to pull [paicard] out of [initial(src.name)]!"),span_notice("You pull [paicard] out of [initial(src.name)] with [attacking_item]."))
+ ejectpai(user)
+ return
return ..()
/mob/living/simple_animal/bot/attacked_by(obj/item/I, mob/living/user)
@@ -474,6 +499,10 @@
var/was_on = bot_mode_flags & BOT_MODE_ON ? TRUE : FALSE
stat |= EMPED
new /obj/effect/temp_visual/emp(loc)
+ if(paicard)
+ paicard.emp_act(severity)
+ src.visible_message(span_notice("[paicard] is flies out of [initial(src.name)]!"), span_warning("You are forcefully ejected from [initial(src.name)]!"))
+ ejectpai()
if(prob(70/severity))
set_active_language(get_random_spoken_language())
@@ -842,6 +871,8 @@ Pass a positive integer as an argument to override a bot's default speed.
access_card.set_access(user_access + prev_access) //Adds the user's access, if any.
mode = BOT_SUMMON
speak("Responding.", radio_channel)
+ if("ejectpai")
+ ejectpairemote(user)
return
@@ -925,7 +956,8 @@ Pass a positive integer as an argument to override a bot's default speed.
data["locked"] = bot_cover_flags & BOT_COVER_LOCKED
data["settings"] = list()
if(!(bot_cover_flags & BOT_COVER_LOCKED) || issilicon(user) || isAdminGhostAI(user))
- data["settings"]["allow_possession"] = bot_mode_flags & BOT_MODE_GHOST_CONTROLLABLE
+ data["settings"]["pai_inserted"] = !!paicard
+ data["settings"]["allow_possession"] = bot_mode_flags & BOT_MODE_CAN_BE_SAPIENT
data["settings"]["possession_enabled"] = can_be_possessed
data["settings"]["airplane_mode"] = !(bot_mode_flags & BOT_MODE_REMOTE_ENABLED)
data["settings"]["maintenance_lock"] = !(bot_cover_flags & BOT_COVER_OPEN)
@@ -967,18 +999,24 @@ Pass a positive integer as an argument to override a bot's default speed.
message_admins("Safety lock of [ADMIN_LOOKUPFLW(src)] was disabled by [ADMIN_LOOKUPFLW(usr)] in [ADMIN_VERBOSEJMP(src)]")
usr.log_message("disabled safety lock of [src]", LOG_GAME)
bot_reset()
- else if(!(bot_cover_flags & BOT_COVER_HACKED))
+ return
+ if(!(bot_cover_flags & BOT_COVER_HACKED))
to_chat(usr, span_boldannounce("You fail to repair [src]'s [hackables]."))
- else
- bot_cover_flags &= ~(BOT_COVER_EMAGGED|BOT_COVER_HACKED)
- to_chat(usr, span_notice("You reset the [src]'s [hackables]."))
- usr.log_message("re-enabled safety lock of [src]", LOG_GAME)
- bot_reset()
+ return
+ bot_cover_flags &= ~(BOT_COVER_EMAGGED|BOT_COVER_HACKED)
+ to_chat(usr, span_notice("You reset the [src]'s [hackables]."))
+ usr.log_message("re-enabled safety lock of [src]", LOG_GAME)
+ bot_reset()
+ if("eject_pai")
+ if(!paicard)
+ return
+ to_chat(usr, span_notice("You eject [paicard] from [initial(src.name)]."))
+ ejectpai(usr)
if("toggle_personality")
if (can_be_possessed)
disable_possession(usr)
else
- enable_possession()
+ enable_possession(usr)
if("rename")
rename(usr)
@@ -986,7 +1024,8 @@ Pass a positive integer as an argument to override a bot's default speed.
icon_state = "[isnull(base_icon_state) ? initial(icon_state) : base_icon_state][get_bot_flag(bot_mode_flags, BOT_MODE_ON)]"
return ..()
-/mob/living/simple_animal/bot/proc/topic_denied(mob/user) //Access check proc for bot topics! Remember to place in a bot's individual Topic if desired.
+/// Access check proc for bot topics! Remember to place in a bot's individual Topic if desired.
+/mob/living/simple_animal/bot/proc/topic_denied(mob/user)
if(!user.can_perform_action(src, ALLOW_SILICON_REACH))
return TRUE
// 0 for access, 1 for denied.
@@ -997,6 +1036,72 @@ Pass a positive integer as an argument to override a bot's default speed.
return TRUE
return FALSE
+/// Places a pAI in control of this mob
+/mob/living/simple_animal/bot/proc/insertpai(mob/user, obj/item/pai_card/card)
+ if(paicard)
+ balloon_alert(user, "slot occupied!")
+ return
+ if(key)
+ balloon_alert(user, "personality already present!")
+ return
+ if(bot_cover_flags & BOT_COVER_LOCKED || !(bot_cover_flags & BOT_COVER_OPEN))
+ balloon_alert(user, "slot inaccessible!")
+ return
+ if(!(bot_mode_flags & BOT_MODE_CAN_BE_SAPIENT))
+ balloon_alert(user, "incompatible firmware!")
+ return
+ if(!card.pai || !card.pai.mind)
+ balloon_alert(user, "pAI is inactive!")
+ return
+ if(!user.transferItemToLoc(card, src))
+ return
+ paicard = card
+ disable_possession()
+ if(paicard.pai.holoform)
+ paicard.pai.fold_in()
+ user.visible_message(span_notice("[user] inserts [card] into [src]!"), span_notice("You insert [card] into [src]."))
+ paicard.pai.mind.transfer_to(src)
+ to_chat(src, span_notice("You sense your form change as you are uploaded into [src]."))
+ name = paicard.pai.name
+ faction = user.faction.Copy()
+ log_combat(user, paicard.pai, "uploaded to [initial(src.name)],")
+ return TRUE
+
+/mob/living/simple_animal/bot/ghost()
+ if(stat != DEAD) // Only ghost if we're doing this while alive, the pAI probably isn't dead yet.
+ return ..()
+ if(paicard && (!client || stat == DEAD))
+ ejectpai()
+
+/// Ejects a pAI from this bot
+/mob/living/simple_animal/bot/proc/ejectpai(mob/user = null, announce = TRUE)
+ if(!paicard)
+ return
+ if(mind && paicard.pai)
+ mind.transfer_to(paicard.pai)
+ else if(paicard.pai)
+ paicard.pai.key = key
+ else
+ ghostize(FALSE) // The pAI card that just got ejected was dead.
+ key = null
+ paicard.forceMove(loc)
+ if(user)
+ log_combat(user, paicard.pai, "ejected from [initial(src.name)],")
+ else
+ log_combat(src, paicard.pai, "ejected")
+ if(announce)
+ to_chat(paicard.pai, span_notice("You feel your control fade as [paicard] ejects from [initial(src.name)]."))
+ paicard = null
+ name = initial(src.name)
+ faction = initial(faction)
+
+/// Ejects the pAI remotely.
+/mob/living/simple_animal/bot/proc/ejectpairemote(mob/user)
+ if(!check_access(user) || !paicard)
+ return
+ speak("Ejecting personality chip.", radio_channel)
+ ejectpai(user)
+
/mob/living/simple_animal/bot/Login()
. = ..()
if(!. || !client)
@@ -1014,7 +1119,6 @@ Pass a positive integer as an argument to override a bot's default speed.
. = ..()
if(!.)
return
-
update_appearance()
/mob/living/simple_animal/bot/sentience_act()
diff --git a/code/modules/mob/living/simple_animal/bot/cleanbot.dm b/code/modules/mob/living/simple_animal/bot/cleanbot.dm
index 8ff6beef757..bd8b5f0268e 100644
--- a/code/modules/mob/living/simple_animal/bot/cleanbot.dm
+++ b/code/modules/mob/living/simple_animal/bot/cleanbot.dm
@@ -92,7 +92,7 @@
)
/mob/living/simple_animal/bot/cleanbot/autopatrol
- bot_mode_flags = BOT_MODE_ON | BOT_MODE_AUTOPATROL | BOT_MODE_REMOTE_ENABLED | BOT_MODE_GHOST_CONTROLLABLE
+ bot_mode_flags = BOT_MODE_ON | BOT_MODE_AUTOPATROL | BOT_MODE_REMOTE_ENABLED | BOT_MODE_CAN_BE_SAPIENT
/mob/living/simple_animal/bot/cleanbot/medbay
name = "Scrubs, MD"
diff --git a/code/modules/mob/living/simple_animal/bot/honkbot.dm b/code/modules/mob/living/simple_animal/bot/honkbot.dm
index 1eee60357f4..5867e88f8fe 100644
--- a/code/modules/mob/living/simple_animal/bot/honkbot.dm
+++ b/code/modules/mob/living/simple_animal/bot/honkbot.dm
@@ -9,7 +9,7 @@
radio_key = /obj/item/encryptionkey/headset_service //doesn't have security key
radio_channel = RADIO_CHANNEL_SERVICE //Doesn't even use the radio anyway.
bot_type = HONK_BOT
- bot_mode_flags = BOT_MODE_ON | BOT_MODE_REMOTE_ENABLED | BOT_MODE_GHOST_CONTROLLABLE | BOT_MODE_AUTOPATROL
+ bot_mode_flags = BOT_MODE_ON | BOT_MODE_REMOTE_ENABLED | BOT_MODE_CAN_BE_SAPIENT | BOT_MODE_AUTOPATROL
hackables = "sound control systems"
path_image_color = "#FF69B4"
data_hud_type = DATA_HUD_SECURITY_BASIC //show jobs
diff --git a/code/modules/mob/living/simple_animal/bot/hygienebot.dm b/code/modules/mob/living/simple_animal/bot/hygienebot.dm
index bdb0c8fb736..5151bf0e97d 100644
--- a/code/modules/mob/living/simple_animal/bot/hygienebot.dm
+++ b/code/modules/mob/living/simple_animal/bot/hygienebot.dm
@@ -15,7 +15,7 @@
maints_access_required = list(ACCESS_ROBOTICS, ACCESS_JANITOR)
radio_key = /obj/item/encryptionkey/headset_service
radio_channel = RADIO_CHANNEL_SERVICE //Service
- bot_mode_flags = ~BOT_MODE_GHOST_CONTROLLABLE
+ bot_mode_flags = ~BOT_MODE_CAN_BE_SAPIENT
bot_type = HYGIENE_BOT
hackables = "cleaning service protocols"
path_image_color = "#993299"
diff --git a/code/modules/mob/living/simple_animal/bot/medbot.dm b/code/modules/mob/living/simple_animal/bot/medbot.dm
index 4171cf1e8ac..0dfa631fffa 100644
--- a/code/modules/mob/living/simple_animal/bot/medbot.dm
+++ b/code/modules/mob/living/simple_animal/bot/medbot.dm
@@ -72,7 +72,7 @@
COOLDOWN_DECLARE(last_tipping_action_voice)
/mob/living/simple_animal/bot/medbot/autopatrol
- bot_mode_flags = BOT_MODE_ON | BOT_MODE_AUTOPATROL | BOT_MODE_REMOTE_ENABLED | BOT_MODE_GHOST_CONTROLLABLE
+ bot_mode_flags = BOT_MODE_ON | BOT_MODE_AUTOPATROL | BOT_MODE_REMOTE_ENABLED | BOT_MODE_CAN_BE_SAPIENT
/mob/living/simple_animal/bot/medbot/stationary
medical_mode_flags = MEDBOT_DECLARE_CRIT | MEDBOT_STATIONARY_MODE | MEDBOT_SPEAK_MODE
diff --git a/code/modules/mob/living/simple_animal/bot/mulebot.dm b/code/modules/mob/living/simple_animal/bot/mulebot.dm
index a5669bb462b..ea535a45992 100644
--- a/code/modules/mob/living/simple_animal/bot/mulebot.dm
+++ b/code/modules/mob/living/simple_animal/bot/mulebot.dm
@@ -263,8 +263,9 @@
data["autoPickup"] = auto_pickup
data["reportDelivery"] = report_delivery
data["id"] = id
- data["allow_possession"] = bot_mode_flags & BOT_MODE_GHOST_CONTROLLABLE
+ data["allow_possession"] = bot_mode_flags & BOT_MODE_CAN_BE_SAPIENT
data["possession_enabled"] = can_be_possessed
+ data["pai_inserted"] = !!paicard
return data
/mob/living/simple_animal/bot/mulebot/ui_act(action, params)
diff --git a/code/modules/mob/living/simple_animal/bot/secbot.dm b/code/modules/mob/living/simple_animal/bot/secbot.dm
index 5545dfcf1af..b45789391a4 100644
--- a/code/modules/mob/living/simple_animal/bot/secbot.dm
+++ b/code/modules/mob/living/simple_animal/bot/secbot.dm
@@ -15,7 +15,7 @@
radio_key = /obj/item/encryptionkey/secbot //AI Priv + Security
radio_channel = RADIO_CHANNEL_SECURITY //Security channel
bot_type = SEC_BOT
- bot_mode_flags = ~BOT_MODE_GHOST_CONTROLLABLE
+ bot_mode_flags = ~BOT_MODE_CAN_BE_SAPIENT
data_hud_type = DATA_HUD_SECURITY_ADVANCED
hackables = "target identification systems"
path_image_color = "#FF0000"
@@ -65,13 +65,13 @@
/mob/living/simple_animal/bot/secbot/beepsky/ofitser
name = "Prison Ofitser"
desc = "Powered by the tears and sweat of laborers."
- bot_mode_flags = ~(BOT_MODE_GHOST_CONTROLLABLE|BOT_MODE_AUTOPATROL)
+ bot_mode_flags = ~(BOT_MODE_CAN_BE_SAPIENT|BOT_MODE_AUTOPATROL)
/mob/living/simple_animal/bot/secbot/beepsky/armsky
name = "Sergeant-At-Armsky"
desc = "It's Sergeant-At-Armsky! He's a disgruntled assistant to the warden that would probably shoot you if he had hands."
health = 45
- bot_mode_flags = ~(BOT_MODE_GHOST_CONTROLLABLE|BOT_MODE_AUTOPATROL)
+ bot_mode_flags = ~(BOT_MODE_CAN_BE_SAPIENT|BOT_MODE_AUTOPATROL)
security_mode_flags = SECBOT_DECLARE_ARRESTS | SECBOT_CHECK_IDS | SECBOT_CHECK_RECORDS
/mob/living/simple_animal/bot/secbot/beepsky/jr
@@ -87,7 +87,7 @@
name = "Officer Pingsky"
desc = "It's Officer Pingsky! Delegated to satellite guard duty for harbouring anti-human sentiment."
radio_channel = RADIO_CHANNEL_AI_PRIVATE
- bot_mode_flags = ~(BOT_MODE_GHOST_CONTROLLABLE|BOT_MODE_AUTOPATROL)
+ bot_mode_flags = ~(BOT_MODE_CAN_BE_SAPIENT|BOT_MODE_AUTOPATROL)
security_mode_flags = SECBOT_DECLARE_ARRESTS | SECBOT_CHECK_IDS | SECBOT_CHECK_RECORDS
/mob/living/simple_animal/bot/secbot/genesky
diff --git a/code/modules/pai/card.dm b/code/modules/pai/card.dm
index 3b6e47df5d8..4fddc8bdc44 100644
--- a/code/modules/pai/card.dm
+++ b/code/modules/pai/card.dm
@@ -65,6 +65,7 @@
/obj/item/pai_card/Initialize(mapload)
. = ..()
+ AddComponent(/datum/component/track_hierarchical_movement)
update_appearance()
SSpai.pai_card_list += src
@@ -95,6 +96,11 @@
return UI_INTERACTIVE
return ..()
+/obj/item/pai_card/ui_static_data(mob/user)
+ . = ..()
+ .["range_max"] = HOLOFORM_MAX_RANGE
+ .["range_min"] = HOLOFORM_MIN_RANGE
+
/obj/item/pai_card/ui_data(mob/user)
. = ..()
var/list/data = list()
@@ -110,6 +116,7 @@
name = pai.name,
transmit = pai.can_transmit,
receive = pai.can_receive,
+ range = pai.leashed_distance,
)
return data
@@ -146,6 +153,12 @@
if("toggle_radio")
pai.toggle_radio(params["option"])
return TRUE
+ if("increase_range")
+ pai.increment_range(1)
+ return TRUE
+ if("decrease_range")
+ pai.increment_range(-1)
+ return TRUE
if("wipe_pai")
pai.wipe_pai(usr)
ui.close()
diff --git a/code/modules/pai/pai.dm b/code/modules/pai/pai.dm
index a97ba8a9aa6..a6bc3bb11b0 100644
--- a/code/modules/pai/pai.dm
+++ b/code/modules/pai/pai.dm
@@ -37,6 +37,8 @@
var/can_transmit = TRUE
/// The card we inhabit
var/obj/item/pai_card/card
+ /// The maximum distance we can travel away from our pai card
+ var/leashed_distance = 5
/// The current chasis that will appear when in holoform
var/chassis = "repairbot"
/// Toggles whether the pAI can hold encryption keys or not
@@ -225,14 +227,28 @@
var/newcardloc = pai_card
pai_card = new(newcardloc)
pai_card.set_personality(src)
- forceMove(pai_card)
card = pai_card
+ forceMove(pai_card)
addtimer(VARSET_WEAK_CALLBACK(src, holochassis_ready, TRUE), HOLOCHASSIS_INIT_TIME)
if(!holoform)
add_traits(list(TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED), PAI_FOLDED)
- desc = "A pAI hard-light holographics emitter. This one appears in the form of a [chassis]."
+ update_appearance(UPDATE_DESC)
RegisterSignal(src, COMSIG_LIVING_CULT_SACRIFICED, PROC_REF(on_cult_sacrificed))
+ RegisterSignal(card, COMSIG_MOVABLE_OR_CONTAINER_MOVED, PROC_REF(check_distance))
+
+/mob/living/silicon/pai/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
+ . = ..()
+ check_distance()
+
+/// Checks if we're in range of our pai card
+/mob/living/silicon/pai/proc/check_distance()
+ SIGNAL_HANDLER
+ if (get_dist(get_turf(card), get_turf(src)) <= leashed_distance)
+ return
+ to_chat(src, span_warning("You moved out of range of your holotransmitter!"))
+ new /obj/effect/temp_visual/guardian/phase/out(loc)
+ forceMove(get_turf(card))
/mob/living/silicon/pai/make_laws()
laws = new /datum/ai_laws/pai()
@@ -259,6 +275,15 @@
update_stat()
SEND_SIGNAL(src, COMSIG_LIVING_HEALTH_UPDATE)
+/mob/living/silicon/pai/update_desc(updates)
+ desc = "A hard-light holographic avatar representing a pAI. This one appears in the form of a [chassis]."
+ return ..()
+
+/mob/living/silicon/pai/update_icon_state()
+ icon_state = resting ? "[chassis]_rest" : "[chassis]"
+ held_state = "[chassis]"
+ return ..()
+
/**
* Resolves the weakref of the pai's master.
* If the master has been deleted, calls reset_software().
@@ -434,3 +459,12 @@
for(var/mob/living/cultist as anything in invokers)
to_chat(cultist, span_cultitalic("You don't think this is what Nar'Sie had in mind when She asked for blood sacrifices..."))
return STOP_SACRIFICE
+
+/// Updates the distance we can be from our pai card
+/mob/living/silicon/pai/proc/increment_range(increment_amount)
+ var/new_distance = leashed_distance + increment_amount
+ if (new_distance < HOLOFORM_MIN_RANGE || new_distance > HOLOFORM_MAX_RANGE)
+ return
+ leashed_distance = new_distance
+ if (increment_amount < 0)
+ check_distance()
diff --git a/code/modules/pai/shell.dm b/code/modules/pai/shell.dm
index 711e19ead51..4e8fe4f7343 100644
--- a/code/modules/pai/shell.dm
+++ b/code/modules/pai/shell.dm
@@ -9,10 +9,7 @@
/mob/living/silicon/pai/update_resting()
. = ..()
- if(resting)
- icon_state = "[chassis]_rest"
- else
- icon_state = "[chassis]"
+ update_appearance(UPDATE_ICON_STATE)
if(loc != card)
visible_message(span_notice("[src] [resting? "lays down for a moment..." : "perks up from the ground."]"))
@@ -77,11 +74,11 @@
addtimer(VARSET_CALLBACK(src, holochassis_ready, TRUE), HOLOCHASSIS_COOLDOWN)
else
addtimer(VARSET_CALLBACK(src, holochassis_ready, TRUE), HOLOCHASSIS_OVERLOAD_COOLDOWN)
- icon_state = "[chassis]"
+ set_resting(FALSE, silent = TRUE, instant = TRUE)
if(!holoform)
. = fold_out(force)
return FALSE
- visible_message(span_notice("[src] deactivates its holochassis emitter and folds back into a compact card!"))
+ visible_message(span_notice("[src] dematerialises!"))
stop_pulling()
if(ispickedupmob(loc))
var/obj/item/clothing/head/mob_holder/mob_head = loc
@@ -89,8 +86,8 @@
if(client)
client.perspective = EYE_PERSPECTIVE
client.set_eye(card)
- var/turf/target = drop_location()
- card.forceMove(target)
+ if (isturf(loc))
+ new /obj/effect/temp_visual/guardian/phase/out(loc)
forceMove(card)
add_traits(list(TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED), PAI_FOLDED)
set_density(FALSE)
@@ -129,20 +126,14 @@
var/obj/item/modular_computer/pc = card.loc
pc.inserted_pai = null
pc.visible_message(span_notice("[src] ejects itself from [pc]!"))
- if(isliving(card.loc))
- var/mob/living/living_holder = card.loc
- if(!living_holder.temporarilyRemoveItemFromInventory(card))
- balloon_alert(src, "unable to expand")
- return FALSE
+ card.forceMove(get_turf(pc))
forceMove(get_turf(card))
- card.forceMove(src)
if(client)
client.perspective = EYE_PERSPECTIVE
client.set_eye(src)
set_light_on(FALSE)
- icon_state = "[chassis]"
- held_state = "[chassis]"
- visible_message(span_boldnotice("[src] folds out its holochassis emitter and forms a holoshell around itself!"))
+ update_appearance(UPDATE_ICON_STATE)
+ visible_message(span_boldnotice("[src] appears in a flash of light!"))
holoform = TRUE
return TRUE
@@ -157,9 +148,7 @@
if(!choice)
return FALSE
chassis = choice
- icon_state = "[chassis]"
- held_state = "[chassis]"
- desc = "A pAI mobile hard-light holographics emitter. This one appears in the form of a [chassis]."
+ update_appearance(UPDATE_DESC | UPDATE_ICON_STATE)
return TRUE
/**
diff --git a/tgstation.dme b/tgstation.dme
index 84815561240..96d1a9a1f20 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -1047,6 +1047,7 @@
#include "code\datums\components\tippable.dm"
#include "code\datums\components\toggle_attached_clothing.dm"
#include "code\datums\components\toggle_suit.dm"
+#include "code\datums\components\track_hierarchical_movement.dm"
#include "code\datums\components\transforming.dm"
#include "code\datums\components\trapdoor.dm"
#include "code\datums\components\twohanded.dm"
diff --git a/tgui/packages/tgui/interfaces/Mule.js b/tgui/packages/tgui/interfaces/Mule.js
index fa3e3d2364e..c5ecad5ad90 100644
--- a/tgui/packages/tgui/interfaces/Mule.js
+++ b/tgui/packages/tgui/interfaces/Mule.js
@@ -20,6 +20,7 @@ export const Mule = (props, context) => {
id,
allow_possession,
possession_enabled,
+ pai_inserted,
destinations = [],
} = data;
const locked = data.locked && !data.siliconUser;
@@ -74,13 +75,22 @@ export const Mule = (props, context) => {
act('unload')}
- />
- )
+ <>
+ {!!load && (
+