New station trait job: Human AI (#81681)

## About The Pull Request

This PR does many things, I'll try to explain the basic/background stuff
to the main thing first:

1. Adds a new remote that allows a human to function like an AI. It
controls a fly that will fly around the station slowly, and when it
reaches a machine then the person can interact with it as if they were
an AI. This required changing a lot of silicon/AI checks with one that
also checks for this remote, and some messing with shared ui state.
2. Moves req_access from the obj and bot to ``/atom/movable`` which lets
it be shared between the two, no more copy-paste and one side lacking
features/checks/signals the other has.
3. Adds a check for AI config for AI-related station traits, which was
lacking prior

Now for the good part...
Adds a new station trait that replaces the AI with a Human.
This person is equipped with an AI headset (including Binary), an
advanced camera console, an omni door wand, the machine controller, and
their laws.
They are immune to the SAT's turrets (even if set to target borgs) and
are slow outside of the SAT, mimicing the actions of the AI.

They interact with the world through their advanced camera console,
which allows them to do most AI stuff needed, and the holopad they can
connect to without having to ring first (like Command can).

They are given a paper with the laws they must follow, but since they
are human they are able to bend it. Cyborgs that run the default lawset
are "slaved" to them via an unremovable law 0, so the Human AI can bend
the laws if they really need to (for their own survival n such), and
make the cyborgs obey their commands above laws, but in general this
shouldn't be a frequent occurrence. This does take into account the
unique AI trait, so it's not guaranteed Asimov.

When this station trait rolls, all Intellicards, AI uploads, and AI core
boards are destroyed and are unresearchable. They can be spawned by
admins in-game if necessary. Maybe in the future we can also exclude
Oldstation from this but I haven't really decided.

Extra perks:

Human AI spawns with a Robotic voicebox (unless they are a body purist)
and teleport blocking implant, so they can't use teleporters to bypass
their on-station slowdown.
They also have an infinite laser pointer that can be used to blind
through their camera console. This is unfortunately nerfed from the
recent borg balance PR that removed its stun. This was meant to be the
alternative to no longer being able to permanently lock borgs down like
AIs can (or more than one, for that matter).
They aren't affected by Roburgers, Acid, and Fuel's toxicity.
Bots salute them like they do Beepsky (which is now a trait)
They spawn with SyndEye to replace the AI's tracking ability
They do not have a bank account

### The machine remote

The machine remote has a little fly in it that flies to the machines it
is pointed to, working as the arms and legs of the Human AI. It scans
the machine and punches in the action the AI does, and is how the AI
accesses basically anything. This fly slowly moves from one machine to
the next, and can be recalled with Alt Click.
It works on machines and bots.

### Video (Low quality to fit Github)


https://github.com/tgstation/tgstation/assets/53777086/e16509f8-8bed-42b5-9fbf-7e37165a11e8

## Why It's Good For The Game

I've seen a funny screenshot one day of a person replacing the AI by
using a bunch of door remotes, camera console, crew monitoring console,
and a few other things. I've been thinking about that for a few years
and really wanted to make it official if not easier to make possible,
because it is an incredibly funny interaction.
This makes it a reality, and while they aren't as powerful as regular
AIs, I think it makes for better and funnier in-game moments. With the
same weight as Cargorilla (1), I hope this wouldn't be rolling too often
and ruin rounds, but instead show off the different capabilities that
Humans and AIs can do, to do the job of an AI. You win some you lose
some.

## Changelog

🆑 JohnFulpWillard, Tattax
add: Adds a new station trait job: The Human AI.
/🆑

---------

Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
This commit is contained in:
John Willard
2024-03-09 23:48:39 +01:00
committed by GitHub
co-authored by MrMelbert
parent c2799af007
commit 9ac81e1a64
138 changed files with 1019 additions and 472 deletions
+38 -56
View File
@@ -39,8 +39,7 @@ GLOBAL_LIST_INIT(command_strings, list(
light_range = 3
light_power = 0.9
speed = 3
///Access required to access this Bot's maintenance protocols
var/maints_access_required = list(ACCESS_ROBOTICS)
req_one_access = 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.
@@ -51,8 +50,8 @@ GLOBAL_LIST_INIT(command_strings, list(
var/list/initial_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_CAN_BE_SAPIENT | BOT_MODE_ROUNDSTART_POSSESSION
var/bot_mode_flags = BOT_MODE_ON | BOT_MODE_REMOTE_ENABLED | BOT_MODE_CAN_BE_SAPIENT | BOT_MODE_ROUNDSTART_POSSESSION
///Bot-related cover flags on the Bot to deal with what has been done to their cover, including emagging. BOT_MAINTS_PANEL_OPEN | BOT_CONTROL_PANEL_OPEN | BOT_COVER_EMAGGED | BOT_COVER_HACKED
var/bot_access_flags = NONE
///Bot-related cover flags on the Bot to deal with what has been done to their cover, including emagging. BOT_COVER_MAINTS_OPEN | BOT_COVER_LOCKED | BOT_COVER_EMAGGED | BOT_COVER_HACKED
var/bot_access_flags = BOT_COVER_LOCKED
///Small name of what the bot gets messed with when getting hacked/emagged.
var/hackables = "system circuits"
///Standardizes the vars that indicate the bot is busy with its function.
@@ -270,26 +269,10 @@ GLOBAL_LIST_INIT(command_strings, list(
return
fully_replace_character_name(real_name, new_name)
/mob/living/basic/bot/proc/check_access(mob/living/user, obj/item/card/id)
if(user.has_unlimited_silicon_privilege || isAdminGhostAI(user)) // Silicon and Admins always have access.
/mob/living/basic/bot/allowed(mob/living/user)
if(!(bot_access_flags & BOT_COVER_LOCKED)) // Unlocked.
return TRUE
if(!istype(user)) // Non-living mobs shouldn't be manipulating bots (like observes using the botkeeper UI).
return FALSE
if(!length(maints_access_required)) // No requirements to access it.
return TRUE
if(bot_access_flags & BOT_CONTROL_PANEL_OPEN) // Unlocked.
return TRUE
var/obj/item/card/id/used_id = id || user.get_idcard(TRUE)
if(!used_id || !used_id.access)
return FALSE
for(var/requested_access in maints_access_required)
if(requested_access in used_id.access)
return TRUE
return FALSE
return ..()
/mob/living/basic/bot/bee_friendly()
return TRUE
@@ -309,15 +292,15 @@ GLOBAL_LIST_INIT(command_strings, list(
/mob/living/basic/bot/emag_act(mob/user, obj/item/card/emag/emag_card)
. = ..()
if(!(bot_access_flags & BOT_CONTROL_PANEL_OPEN)) //First emag application unlocks the bot's interface. Apply a screwdriver to use the emag again.
bot_access_flags |= BOT_CONTROL_PANEL_OPEN
if(bot_access_flags & BOT_COVER_LOCKED) //First emag application unlocks the bot's interface. Apply a screwdriver to use the emag again.
bot_access_flags &= ~BOT_COVER_LOCKED
balloon_alert(user, "cover unlocked")
return TRUE
if(!(bot_access_flags & BOT_CONTROL_PANEL_OPEN) || !(bot_access_flags & BOT_MAINTS_PANEL_OPEN)) //Bot panel is unlocked by ID or emag, and the panel is screwed open. Ready for emagging.
if((bot_access_flags & BOT_COVER_LOCKED) || !(bot_access_flags & BOT_COVER_MAINTS_OPEN)) //Bot panel is unlocked by ID or emag, and the panel is screwed open. Ready for emagging.
balloon_alert(user, "open maintenance panel first!")
return FALSE
bot_access_flags |= BOT_COVER_EMAGGED
bot_access_flags &= ~BOT_CONTROL_PANEL_OPEN
bot_access_flags |= BOT_COVER_LOCKED
bot_mode_flags &= ~BOT_MODE_REMOTE_ENABLED //Manually emagging the bot also locks the AI from controlling it.
bot_reset()
turn_on() //The bot automatically turns on when emagged, unless recently hit with EMP.
@@ -337,17 +320,17 @@ GLOBAL_LIST_INIT(command_strings, list(
else
. += "[src] is in pristine condition."
. += span_notice("Its maintenance panel is [bot_access_flags & BOT_MAINTS_PANEL_OPEN ? "open" : "closed"].")
. += span_info("You can use a <b>screwdriver</b> to [bot_access_flags & BOT_MAINTS_PANEL_OPEN ? "close" : "open"] it.")
. += span_notice("Its maintenance panel is [bot_access_flags & BOT_COVER_MAINTS_OPEN ? "open" : "closed"].")
. += span_info("You can use a <b>screwdriver</b> to [bot_access_flags & BOT_COVER_MAINTS_OPEN ? "close" : "open"] it.")
if(bot_access_flags & BOT_MAINTS_PANEL_OPEN)
. += span_notice("Its control panel is [bot_access_flags & BOT_CONTROL_PANEL_OPEN ? "unlocked" : "locked"].")
if(bot_access_flags & BOT_COVER_MAINTS_OPEN)
. += span_notice("Its control panel is [bot_access_flags & BOT_COVER_LOCKED ? "locked" : "unlocked"].")
if(!(bot_access_flags & BOT_COVER_EMAGGED) && (issilicon(user) || user.Adjacent(src)))
. += span_info("Alt-click [issilicon(user) ? "" : "or use your ID on "]it to [bot_access_flags & BOT_CONTROL_PANEL_OPEN ? "" : "un"]lock its control panel.")
. += span_info("Alt-click [issilicon(user) ? "" : "or use your ID on "]it to [bot_access_flags & BOT_COVER_LOCKED ? "un" : ""]lock its control panel.")
if(isnull(paicard))
return
. += span_notice("It has a pAI device installed.")
if(!(bot_access_flags & BOT_MAINTS_PANEL_OPEN))
if(!(bot_access_flags & BOT_COVER_MAINTS_OPEN))
. += span_info("You can use a <b>hemostat</b> to remove it.")
/mob/living/basic/bot/updatehealth()
@@ -390,25 +373,25 @@ GLOBAL_LIST_INIT(command_strings, list(
if(bot_access_flags & BOT_COVER_EMAGGED)
balloon_alert(user, "error!")
return
if(bot_access_flags & BOT_MAINTS_PANEL_OPEN)
if(bot_access_flags & BOT_COVER_MAINTS_OPEN)
balloon_alert(user, "access panel must be closed!")
return
if(!check_access(user))
if(!allowed(user))
balloon_alert(user, "no access")
return
bot_access_flags ^= BOT_CONTROL_PANEL_OPEN
to_chat(user, span_notice("Controls are now [bot_access_flags & BOT_CONTROL_PANEL_OPEN ? "unlocked" : "locked"]."))
bot_access_flags ^= BOT_COVER_LOCKED
to_chat(user, span_notice("Controls are now [bot_access_flags & BOT_COVER_LOCKED ? "locked" : "unlocked"]."))
return TRUE
/mob/living/basic/bot/screwdriver_act(mob/living/user, obj/item/tool)
. = ITEM_INTERACT_SUCCESS
if(!(bot_access_flags & BOT_CONTROL_PANEL_OPEN))
if(bot_access_flags & BOT_COVER_LOCKED)
to_chat(user, span_warning("The maintenance panel is locked!"))
return
tool.play_tool_sound(src)
bot_access_flags ^= BOT_MAINTS_PANEL_OPEN
to_chat(user, span_notice("The maintenance panel is now [bot_access_flags & BOT_MAINTS_PANEL_OPEN ? "opened" : "closed"]."))
bot_access_flags ^= BOT_COVER_MAINTS_OPEN
to_chat(user, span_notice("The maintenance panel is now [bot_access_flags & BOT_COVER_MAINTS_OPEN ? "opened" : "closed"]."))
/mob/living/basic/bot/welder_act(mob/living/user, obj/item/tool)
user.changeNext_move(CLICK_CD_MELEE)
@@ -421,7 +404,7 @@ GLOBAL_LIST_INIT(command_strings, list(
user.balloon_alert(user, "no repairs needed!")
return
if(!(bot_access_flags & BOT_MAINTS_PANEL_OPEN))
if(!(bot_access_flags & BOT_COVER_MAINTS_OPEN))
user.balloon_alert(user, "maintenance panel closed!")
return
@@ -443,7 +426,7 @@ GLOBAL_LIST_INIT(command_strings, list(
if(attacking_item.tool_behaviour != TOOL_HEMOSTAT || !paicard)
return ..()
if(bot_access_flags & BOT_MAINTS_PANEL_OPEN)
if(bot_access_flags & BOT_COVER_MAINTS_OPEN)
balloon_alert(user, "open the access panel!")
return
@@ -584,18 +567,18 @@ GLOBAL_LIST_INIT(command_strings, list(
/mob/living/basic/bot/ui_data(mob/user)
var/list/data = list()
data["can_hack"] = (issilicon(user) || isAdminGhostAI(user))
data["can_hack"] = HAS_SILICON_ACCESS(user)
data["custom_controls"] = list()
data["emagged"] = bot_access_flags & BOT_COVER_EMAGGED
data["has_access"] = check_access(user)
data["locked"] = !(bot_access_flags & BOT_CONTROL_PANEL_OPEN)
data["has_access"] = allowed(user)
data["locked"] = (bot_access_flags & BOT_COVER_LOCKED)
data["settings"] = list()
if((bot_access_flags & BOT_CONTROL_PANEL_OPEN) || issilicon(user) || isAdminGhostAI(user))
if(!(bot_access_flags & BOT_COVER_LOCKED) || HAS_SILICON_ACCESS(user))
data["settings"]["pai_inserted"] = !isnull(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_access_flags & BOT_MAINTS_PANEL_OPEN)
data["settings"]["maintenance_lock"] = !(bot_access_flags & BOT_COVER_MAINTS_OPEN)
data["settings"]["power"] = bot_mode_flags & BOT_MODE_ON
data["settings"]["patrol_station"] = bot_mode_flags & BOT_MODE_AUTOPATROL
return data
@@ -606,12 +589,12 @@ GLOBAL_LIST_INIT(command_strings, list(
if(.)
return
var/mob/the_user = ui.user
if(!check_access(the_user))
if(!allowed(the_user))
balloon_alert(the_user, "access denied!")
return
if(action == "lock")
bot_access_flags ^= BOT_CONTROL_PANEL_OPEN
bot_access_flags ^= BOT_COVER_LOCKED
switch(action)
if("power")
@@ -620,18 +603,17 @@ GLOBAL_LIST_INIT(command_strings, list(
else
turn_on()
if("maintenance")
bot_access_flags ^= BOT_MAINTS_PANEL_OPEN
bot_access_flags ^= BOT_COVER_MAINTS_OPEN
if("patrol")
bot_mode_flags ^= BOT_MODE_AUTOPATROL
bot_reset()
if("airplane")
bot_mode_flags ^= BOT_MODE_REMOTE_ENABLED
if("hack")
if(!(issilicon(the_user) || isAdminGhostAI(the_user)))
if(!HAS_SILICON_ACCESS(the_user))
return
if(!(bot_access_flags & BOT_COVER_EMAGGED))
bot_access_flags |= (BOT_COVER_EMAGGED|BOT_COVER_HACKED)
bot_access_flags &= ~BOT_CONTROL_PANEL_OPEN
bot_access_flags |= (BOT_COVER_LOCKED|BOT_COVER_EMAGGED|BOT_COVER_HACKED)
to_chat(the_user, span_warning("You overload [src]'s [hackables]."))
message_admins("Safety lock of [ADMIN_LOOKUPFLW(src)] was disabled by [ADMIN_LOOKUPFLW(the_user)] in [ADMIN_VERBOSEJMP(the_user)]")
the_user.log_message("disabled safety lock of [the_user]", LOG_GAME)
@@ -674,7 +656,7 @@ GLOBAL_LIST_INIT(command_strings, list(
return FALSE
if(!(bot_access_flags & BOT_COVER_HACKED)) //Manually emagged by a human - access denied to all.
return TRUE
if(!issilicon(user) && !isAdminGhostAI(user)) //Bot is hacked, so only silicons and admins are allowed access.
if(!HAS_SILICON_ACCESS(user)) //Bot is hacked, so only silicons and admins are allowed access.
return TRUE
return FALSE
@@ -687,7 +669,7 @@ GLOBAL_LIST_INIT(command_strings, list(
if(key)
balloon_alert(user, "personality already present!")
return
if(!(bot_access_flags & BOT_COVER_OPEN))
if(!(bot_access_flags & BOT_COVER_MAINTS_OPEN))
balloon_alert(user, "slot inaccessible!")
return
if(!(bot_mode_flags & BOT_MODE_CAN_BE_SAPIENT))
@@ -744,7 +726,7 @@ GLOBAL_LIST_INIT(command_strings, list(
/// Ejects the pAI remotely.
/mob/living/basic/bot/proc/eject_pai_remote(mob/user)
if(!check_access(user) || !paicard)
if(!allowed(user) || !paicard)
return
speak("Ejecting personality chip.", radio_channel)
ejectpai(user)