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 && ( + + + + + + ); + } else { + return ( + + ); + } +}; + /** Displays the bot's standard settings: Power, patrol, etc. */ const SettingsDisplay = (props, context) => { const { act, data } = useBackend(context);