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

🆑
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.
/🆑
This commit is contained in:
Jacquerel
2023-07-14 19:10:34 -06:00
committed by GitHub
parent f2fe624ca4
commit 52c8da7ea4
18 changed files with 332 additions and 65 deletions
@@ -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"
+5
View File
@@ -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
+1 -1
View File
@@ -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.
@@ -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)
+129 -25
View File
@@ -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 "<b>Autonomous</b>"
return paicard ? "<b>pAI Controlled</b>" : "<b>Autonomous</b>"
if(!(bot_mode_flags & BOT_MODE_ON))
return "<span class='bad'>Inactive</span>"
return "<span class='average'>[mode]</span>"
@@ -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 <b>hemostat</b> 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()
@@ -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"
@@ -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
@@ -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"
@@ -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
@@ -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)
@@ -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
+13
View File
@@ -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()
+36 -2
View File
@@ -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()
+9 -20
View File
@@ -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
/**
+1
View File
@@ -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"
+17 -7
View File
@@ -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) => {
<Section
title="Controls"
buttons={
!!load && (
<Button
icon="eject"
content="Unload"
onClick={() => act('unload')}
/>
)
<>
{!!load && (
<Button
icon="eject"
content="Unload"
onClick={() => act('unload')}
/>
)}
{!!pai_inserted && (
<Button
icon="eject"
content="Eject PAI"
onClick={() => act('eject_pai')}
/>
)}
</>
}>
<LabeledList>
<LabeledList.Item label="ID">
+35 -1
View File
@@ -7,6 +7,8 @@ import { Window } from '../layouts';
type Data = {
candidates: ReadonlyArray<Candidate>;
pai: Pai;
range_max: number;
range_min: number;
};
type Candidate = Readonly<{
@@ -25,6 +27,7 @@ type Pai = {
name: string;
transmit: BooleanLike;
receive: BooleanLike;
range: number;
};
export const PaiCard = (props, context) => {
@@ -140,7 +143,19 @@ const CandidateDisplay = (
const PaiOptions = (props, context) => {
const { act, data } = useBackend<Data>(context);
const {
pai: { can_holo, dna, emagged, laws, master, name, transmit, receive },
range_max,
range_min,
pai: {
can_holo,
dna,
emagged,
laws,
master,
name,
transmit,
receive,
range,
},
} = data;
const suppliedLaws = laws[0] ? decodeHtmlEntities(laws[0]) : 'None';
@@ -170,6 +185,25 @@ const PaiOptions = (props, context) => {
Toggle
</Button>
</LabeledList.Item>
<LabeledList.Item label="Holoform Range">
<Stack>
<Stack.Item>
<Button
icon="fa-circle-minus"
onClick={() => act('decrease_range')}
disabled={range === range_min}
/>
</Stack.Item>
<Stack.Item mt={0.5}>{range}</Stack.Item>
<Stack.Item>
<Button
icon="fa-circle-plus"
onClick={() => act('increase_range')}
disabled={range === range_max}
/>
</Stack.Item>
</Stack>
</LabeledList.Item>
<LabeledList.Item label="Transmit">
<Button
icon={transmit ? 'toggle-on' : 'toggle-off'}
@@ -20,6 +20,7 @@ type Settings = {
allow_possession: number;
possession_enabled: number;
has_personality: number;
pai_inserted: boolean;
};
type Controls = {
@@ -57,10 +58,12 @@ export const SimpleBot = (props, context) => {
const TabDisplay = (props, context) => {
const { act, data } = useBackend<SimpleBotContext>(context);
const { can_hack, has_access, locked } = data;
const { allow_possession } = data.settings;
return (
<>
{!!can_hack && <HackButton />}
{!!allow_possession && <PaiButton />}
<Button
color="transparent"
icon="fa-poll-h"
@@ -103,6 +106,33 @@ const HackButton = (props, context) => {
);
};
/** Creates a button indicating PAI status and offers the eject action */
const PaiButton = (props, context) => {
const { act, data } = useBackend<SimpleBotContext>(context);
const { pai_inserted } = data.settings;
if (!pai_inserted) {
return (
<Button
color="transparent"
icon="robot"
tooltip={multiline`Insert an active PAI card to control this device.`}>
No PAI Inserted
</Button>
);
} else {
return (
<Button
disabled={!pai_inserted}
icon="eject"
onClick={() => act('eject_pai')}
tooltip={multiline`Ejects the current PAI.`}>
Eject PAI
</Button>
);
}
};
/** Displays the bot's standard settings: Power, patrol, etc. */
const SettingsDisplay = (props, context) => {
const { act, data } = useBackend<SimpleBotContext>(context);