From 52c8da7ea49ef566c9a997a4b7cfc5d3d2a59178 Mon Sep 17 00:00:00 2001 From: Jacquerel Date: Sat, 15 Jul 2023 02:10:34 +0100 Subject: [PATCH] PAI Holochassis are now leashed to an area around their card (#76763) ## About The Pull Request This change restricts PAI holograms to an area around their assigned PAI card. If you leave this area, you are teleported back to the card's location (but not automatically put back into the card). https://www.youtube.com/watch?v=L2ThEVa4nx8 This setting can be configured from the PAI menu, it's set pretty short in the video because otherwise it wouldn't teleport when I threw the card and I like doing that. ![image](https://github.com/tgstation/tgstation/assets/7483112/faf0fa0b-e9d6-4990-8d8c-f30def2892f1) To accomodate this I set up a component to deal with a reasonably common problem I have had, "what if I want to track the movement of something in a box in a bag in someone's inventory". Please tell me if the solution I came up with is stupid and we already have a better one that I forgot about. Also now you can put pAIs inside bots again, by popular request. ## Why It's Good For The Game Personal AIs are intended to be personal assistants to their owner. rather than fully independent entities which can pick up their own card and leave as soon as they spawn. As "aimless wanderer" players can now possess station bots, pAIs can be limited to an area around the bearer of their card. Because the holoform now doesn't contain the card, this also means that a PAI cannot run off and then be impossible to retrieve. You are always in control of where it can go. Also it's funny to throw the card and watch the chicken get teleported to it. ## Changelog :cl: add: Personal AI holograms are now limited to an area around their PAI card. The size of this are can be configured via the PAI card. add: pAI cards can now be placed inside bots in order to grant them control of the bot. /:cl: --- .../signals_atom/signals_atom_movable.dm | 3 + code/__DEFINES/pai.dm | 5 + code/__DEFINES/robots.dm | 2 +- .../components/track_hierarchical_movement.dm | 43 +++++ .../mob/living/simple_animal/bot/bot.dm | 154 +++++++++++++++--- .../mob/living/simple_animal/bot/cleanbot.dm | 2 +- .../mob/living/simple_animal/bot/honkbot.dm | 2 +- .../living/simple_animal/bot/hygienebot.dm | 2 +- .../mob/living/simple_animal/bot/medbot.dm | 2 +- .../mob/living/simple_animal/bot/mulebot.dm | 3 +- .../mob/living/simple_animal/bot/secbot.dm | 8 +- code/modules/pai/card.dm | 13 ++ code/modules/pai/pai.dm | 38 ++++- code/modules/pai/shell.dm | 29 +--- tgstation.dme | 1 + tgui/packages/tgui/interfaces/Mule.js | 24 ++- tgui/packages/tgui/interfaces/PaiCard.tsx | 36 +++- tgui/packages/tgui/interfaces/SimpleBot.tsx | 30 ++++ 18 files changed, 332 insertions(+), 65 deletions(-) create mode 100644 code/datums/components/track_hierarchical_movement.dm 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);