Bots no longer require PAIs to become sapient (#76691)

## About The Pull Request

We were talking in the coder channel about what the role of a pAI is,
with a general conclusion that as the name would suggest they should be
_personal assistants_.
This means they should be sticking around their owner, not wandering
away as a holochassis or in the body of a bot.
The former is a matter for a future PR, the latter I am addressing here.

What we also discussed is that clearly some people _want_ to respawn as
a weird quasi-useless mob which wanders aimlessly around the station.
That seems like a fine thing to exist, but it shouldn't be a pAI.

Resultingly: pAI cards can no longer be placed inside bots.
However, you also no longer need to place pAI cards inside bots in order
for them to become sapient, it's a simple toggle on the bot control
menu. Enabling this option will poll ghosts
Toggling the "personality matrix" off while a bot is being controlled by
a ghost will ghost them again, so if they're annoying they're not that
hard to get rid of.


![image](https://github.com/tgstation/tgstation/assets/7483112/ec14c2f2-3c0f-4f03-9dfc-22abca00a477)

Mobs which couldn't have a pAI inserted don't have this option.
Specifically securitrons, ED-209, and Hygienebots (for some reason).

Perhaps most controversially, any bots which are present on the station
when the map loads will have this setting enabled by default. We will
see if players abuse this too much and need their toys taken away, I am
hoping they can be trusted.

Additionally, as part of this change, mobs you can possess now appear in
the spawners menu.

![image](https://github.com/tgstation/tgstation/assets/7483112/7c505471-43de-4e4e-89a5-877dc3086684)
Here is an unusually populated example.

Oh also in the process of doing this I turned the regal rat "click this
to become it" behaviour into a component because it seems generally
useful.

## Why It's Good For The Game

Minor stuff for dead players to do if they want to interact with living
players instead of observe.
Shift pAI back into a more intended role as a personal assistant who
hangs around with their owner, rather than just a generic respawn role.

## Changelog

🆑
add: PAIs can no longer be inserted into Bots
add: Bots can now have their sapience toggled by anyone with access to
their settings panel
add: Bots which exist on the map at the start of the round automatically
have this setting enabled
qol: Bots, Regal Rats, and Cargorilla now appear in the Spawners menu if
you are dead
qol: Bots can be renamed from their maintenance panel
/🆑
This commit is contained in:
Jacquerel
2023-07-10 17:55:43 -07:00
committed by GitHub
parent 60d798db7f
commit 2ee79d7077
26 changed files with 497 additions and 327 deletions
+2 -2
View File
@@ -86,8 +86,8 @@
#define BOT_MODE_AUTOPATROL (1<<1)
///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 pAI placed in control of it.
#define BOT_MODE_PAI_CONTROLLABLE (1<<3)
///The Bot is allowed to have a ghost placed in control of it.
#define BOT_MODE_GHOST_CONTROLLABLE (1<<3)
//Bot cover defines indicating the Bot's status
///The Bot's cover is open and can be modified/emagged by anyone.
+1
View File
@@ -99,6 +99,7 @@
#define ROLE_BATTLECRUISER_CREW "Battlecruiser Crew"
#define ROLE_BATTLECRUISER_CAPTAIN "Battlecruiser Captain"
#define ROLE_VENUSHUMANTRAP "Venus Human Trap"
#define ROLE_BOT "Bot"
/// This defines the antagonists you can operate with in the settings.
+75 -38
View File
@@ -1,31 +1,54 @@
GLOBAL_LIST_EMPTY(cable_list) //Index for all cables, so that powernets don't have to look through the entire world all the time
GLOBAL_LIST_EMPTY(portals) //list of all /obj/effect/portal
GLOBAL_LIST_EMPTY(airlocks) //list of all airlocks
GLOBAL_LIST_EMPTY(curtains) //list of all curtains
GLOBAL_LIST_EMPTY(mechas_list) //list of all mechs. Used by hostile mobs target tracking.
GLOBAL_LIST_EMPTY(shuttle_caller_list) //list of all communication consoles and AIs, for automatic shuttle calls when there are none.
GLOBAL_LIST_EMPTY(machines) //NOTE: this is a list of ALL machines now. The processing machines list is SSmachine.processing !
GLOBAL_LIST_EMPTY(navigation_computers) //list of all /obj/machinery/computer/camera_advanced/shuttle_docker
GLOBAL_LIST_EMPTY(syndicate_shuttle_boards) //important to keep track of for managing nukeops war declarations.
GLOBAL_LIST_EMPTY(navbeacons) //list of all bot nagivation beacons, used for patrolling.
GLOBAL_LIST_EMPTY(teleportbeacons) //list of all tracking beacons used by teleporters
GLOBAL_LIST_EMPTY(deliverybeacons) //list of all MULEbot delivery beacons.
GLOBAL_LIST_EMPTY(deliverybeacontags) //list of all tags associated with delivery beacons.
/// Index for all cables, so that powernets don't have to look through the entire world all the time
GLOBAL_LIST_EMPTY(cable_list)
/// list of all /obj/effect/portal
GLOBAL_LIST_EMPTY(portals)
/// list of all airlocks
GLOBAL_LIST_EMPTY(airlocks)
/// list of all curtains
GLOBAL_LIST_EMPTY(curtains)
/// list of all mechs. Used by hostile mobs target tracking.
GLOBAL_LIST_EMPTY(mechas_list)
/// list of all communication consoles and AIs, for automatic shuttle calls when there are none.
GLOBAL_LIST_EMPTY(shuttle_caller_list)
/// list of ALL machines
GLOBAL_LIST_EMPTY(machines)
/// list of all /obj/machinery/computer/camera_advanced/shuttle_docker
GLOBAL_LIST_EMPTY(navigation_computers)
/// important to keep track of for managing nukeops war declarations.
GLOBAL_LIST_EMPTY(syndicate_shuttle_boards)
/// list of all bot nagivation beacons, used for patrolling.
GLOBAL_LIST_EMPTY(navbeacons)
/// list of all tracking beacons used by teleporters
GLOBAL_LIST_EMPTY(teleportbeacons)
/// list of all MULEbot delivery beacons.
GLOBAL_LIST_EMPTY(deliverybeacons)
/// list of all tags associated with delivery beacons.
GLOBAL_LIST_EMPTY(deliverybeacontags)
GLOBAL_LIST_EMPTY(nuke_list)
GLOBAL_LIST_EMPTY(alarmdisplay) //list of all machines or programs that can display station alerts
GLOBAL_LIST_EMPTY_TYPED(singularities, /datum/component/singularity) //list of all singularities on the station
GLOBAL_LIST_EMPTY(mechpad_list) //list of all /obj/machinery/mechpad
/// list of all machines or programs that can display station alerts
GLOBAL_LIST_EMPTY(alarmdisplay)
/// list of all singularities on the station
GLOBAL_LIST_EMPTY_TYPED(singularities, /datum/component/singularity)
/// list of all /obj/machinery/mechpad
GLOBAL_LIST_EMPTY(mechpad_list)
GLOBAL_LIST(chemical_reactions_list) //list of all /datum/chemical_reaction datums indexed by their typepath. Use this for general lookup stuff
GLOBAL_LIST(chemical_reactions_list_reactant_index) //list of all /datum/chemical_reaction datums. Used during chemical reactions. Indexed by REACTANT types
GLOBAL_LIST(chemical_reactions_list_product_index) //list of all /datum/chemical_reaction datums. Used for the reaction lookup UI. Indexed by PRODUCT type
GLOBAL_LIST_INIT(chemical_reagents_list, init_chemical_reagent_list()) //list of all /datum/reagent datums indexed by reagent id. Used by chemistry stuff
/// list of all /datum/chemical_reaction datums indexed by their typepath. Use this for general lookup stuff
GLOBAL_LIST(chemical_reactions_list)
/// list of all /datum/chemical_reaction datums. Used during chemical reactions. Indexed by REACTANT types
GLOBAL_LIST(chemical_reactions_list_reactant_index)
/// list of all /datum/chemical_reaction datums. Used for the reaction lookup UI. Indexed by PRODUCT type
GLOBAL_LIST(chemical_reactions_list_product_index) /// list of all /datum/reagent datums indexed by reagent id. Used by chemistry stuff
GLOBAL_LIST_INIT(chemical_reagents_list, init_chemical_reagent_list())
/// names of reagents used by plumbing UI.
GLOBAL_LIST_INIT(chemical_name_list, init_chemical_name_list())
GLOBAL_LIST(chemical_reactions_results_lookup_list) //List of all reactions with their associated product and result ids. Used for reaction lookups
GLOBAL_LIST(fake_reagent_blacklist) //List of all reagents that are parent types used to define a bunch of children - but aren't used themselves as anything.
GLOBAL_LIST_EMPTY(tech_list) //list of all /datum/tech datums indexed by id.
GLOBAL_LIST_INIT(surgeries_list, init_surgeries()) //list of all surgeries by name, associated with their path.
/// List of all reactions with their associated product and result ids. Used for reaction lookups
GLOBAL_LIST(chemical_reactions_results_lookup_list)
/// List of all reagents that are parent types used to define a bunch of children - but aren't used themselves as anything.
GLOBAL_LIST(fake_reagent_blacklist)
/// list of all /datum/tech datums indexed by id.
GLOBAL_LIST_EMPTY(tech_list)
/// list of all surgeries by name, associated with their path.
GLOBAL_LIST_INIT(surgeries_list, init_surgeries())
/// Global list of all non-cooking related crafting recipes.
GLOBAL_LIST_EMPTY(crafting_recipes)
@@ -37,16 +60,24 @@ GLOBAL_LIST_EMPTY(cooking_recipes)
/// This is a global list of typepaths, these typepaths are atoms or reagents that are associated with cooking recipes.
/// This includes stuff like recipe components and results.
GLOBAL_LIST_EMPTY(cooking_recipes_atoms)
GLOBAL_LIST_EMPTY(rcd_list) //list of Rapid Construction Devices.
GLOBAL_LIST_EMPTY(intercoms_list) //list of wallmounted intercom radios.
GLOBAL_LIST_EMPTY(apcs_list) //list of all Area Power Controller machines, separate from machines for powernet speeeeeeed.
GLOBAL_LIST_EMPTY(tracked_implants) //list of all current implants that are tracked to work out what sort of trek everyone is on. Sadly not on lavaworld not implemented...
GLOBAL_LIST_EMPTY(tracked_chem_implants) //list of implants the prisoner console can track and send inject commands too
GLOBAL_LIST_EMPTY(pinpointer_list) //list of all pinpointers. Used to change stuff they are pointing to all at once.
GLOBAL_LIST_EMPTY(zombie_infection_list) // A list of all zombie_infection organs, for any mass "animation"
GLOBAL_LIST_EMPTY(meteor_list) // List of all meteors.
GLOBAL_LIST_EMPTY(active_jammers) // List of active radio jammers
/// list of Rapid Construction Devices.
GLOBAL_LIST_EMPTY(rcd_list)
/// list of wallmounted intercom radios.
GLOBAL_LIST_EMPTY(intercoms_list)
/// list of all Area Power Controller machines, separate from machines for powernet speeeeeeed.
GLOBAL_LIST_EMPTY(apcs_list)
/// list of all current implants that are tracked to work out what sort of trek everyone is on. Sadly not on lavaworld not implemented...
GLOBAL_LIST_EMPTY(tracked_implants)
/// list of implants the prisoner console can track and send inject commands too
GLOBAL_LIST_EMPTY(tracked_chem_implants)
/// list of all pinpointers. Used to change stuff they are pointing to all at once.
GLOBAL_LIST_EMPTY(pinpointer_list)
/// A list of all zombie_infection organs, for any mass "animation"
GLOBAL_LIST_EMPTY(zombie_infection_list)
/// List of all meteors.
GLOBAL_LIST_EMPTY(meteor_list)
/// List of active radio jammers
GLOBAL_LIST_EMPTY(active_jammers)
GLOBAL_LIST_EMPTY(ladders)
GLOBAL_LIST_EMPTY(stairs)
GLOBAL_LIST_EMPTY(janitor_devices)
@@ -60,11 +91,17 @@ GLOBAL_LIST_EMPTY(wire_name_directory)
GLOBAL_LIST_EMPTY(ai_status_displays)
GLOBAL_LIST_EMPTY(mob_spawners) // All mob_spawn objects
GLOBAL_LIST_EMPTY(alert_consoles) // Station alert consoles, /obj/machinery/computer/station_alert
/// List of all instances of /obj/effect/mob_spawn/ghost_role in the game world
GLOBAL_LIST_EMPTY(mob_spawners)
/// List of all mobs with the "ghost_direct_control" component
GLOBAL_LIST_EMPTY(joinable_mobs)
/// List of all station alert consoles, /obj/machinery/computer/station_alert
GLOBAL_LIST_EMPTY(alert_consoles)
GLOBAL_LIST_EMPTY(roundstart_station_borgcharger_areas) // List of area names of roundstart station cyborg rechargers, for the low charge/no charge cyborg screen alert tooltips.
GLOBAL_LIST_EMPTY(roundstart_station_mechcharger_areas) // List of area names of roundstart station mech rechargers, for the low charge/no charge mech screen alert tooltips.
/// List of area names of roundstart station cyborg rechargers, for the low charge/no charge cyborg screen alert tooltips.
GLOBAL_LIST_EMPTY(roundstart_station_borgcharger_areas)
/// List of area names of roundstart station mech rechargers, for the low charge/no charge mech screen alert tooltips.
GLOBAL_LIST_EMPTY(roundstart_station_mechcharger_areas)
/// Associative list of alcoholic container typepath to instances, currently used by the alcoholic quirk
GLOBAL_LIST_INIT(alcohol_containers, init_alcohol_containers())
+2
View File
@@ -26,6 +26,7 @@
#define POLL_IGNORE_MONKEY_HELMET "mind_magnified_monkey"
#define POLL_IGNORE_LAVALAND_ELITE "lavaland_elite"
#define POLL_IGNORE_SHUTTLE_DENIZENS "shuttle_denizens"
#define POLL_IGNORE_BOTS "bots"
GLOBAL_LIST_INIT(poll_ignore_desc, list(
@@ -55,6 +56,7 @@ GLOBAL_LIST_INIT(poll_ignore_desc, list(
POLL_IGNORE_MONKEY_HELMET = "Mind magnified monkey",
POLL_IGNORE_LAVALAND_ELITE = "Lavaland elite",
POLL_IGNORE_SHUTTLE_DENIZENS = "Shuttle denizens",
POLL_IGNORE_BOTS = "Bots",
))
GLOBAL_LIST_INIT(poll_ignore, init_poll_ignore())
@@ -0,0 +1,140 @@
/**
* Component which lets ghosts click on a mob to take control of it
*/
/datum/component/ghost_direct_control
/// Message to display upon successful possession
var/assumed_control_message
/// Type of ban you can get to prevent you from accepting this role
var/ban_type
/// Any extra checks which need to run before we take over
var/datum/callback/extra_control_checks
/// Callback run after someone successfully takes over the body
var/datum/callback/after_assumed_control
/// If we're currently awaiting the results of a ghost poll
var/awaiting_ghosts = FALSE
/datum/component/ghost_direct_control/Initialize(
ban_type = ROLE_SENTIENCE,
role_name = null,
poll_candidates = TRUE,
poll_length = 10 SECONDS,
poll_ignore_key = POLL_IGNORE_SENTIENCE_POTION,
assumed_control_message = null,
datum/callback/extra_control_checks,
datum/callback/after_assumed_control,
)
. = ..()
if (!isliving(parent))
return COMPONENT_INCOMPATIBLE
src.ban_type = ban_type
src.assumed_control_message = assumed_control_message || "You are [parent]!"
src.extra_control_checks = extra_control_checks
src.after_assumed_control= after_assumed_control
var/mob/mob_parent = parent
LAZYADD(GLOB.joinable_mobs[format_text("[initial(mob_parent.name)]")], mob_parent)
if (poll_candidates)
INVOKE_ASYNC(src, PROC_REF(request_ghost_control), role_name || "[parent]", poll_length, poll_ignore_key)
/datum/component/ghost_direct_control/RegisterWithParent()
. = ..()
RegisterSignal(parent, COMSIG_ATOM_ATTACK_GHOST, PROC_REF(on_ghost_clicked))
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examined))
/datum/component/ghost_direct_control/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_ATOM_ATTACK_GHOST, COMSIG_ATOM_EXAMINE))
return ..()
/datum/component/ghost_direct_control/Destroy(force, silent)
QDEL_NULL(extra_control_checks)
QDEL_NULL(after_assumed_control)
var/mob/mob_parent = parent
var/list/spawners = GLOB.joinable_mobs[format_text("[initial(mob_parent.name)]")]
LAZYREMOVE(spawners, mob_parent)
if(!LAZYLEN(spawners))
GLOB.joinable_mobs -= format_text("[initial(mob_parent.name)]")
return ..()
/// Inform ghosts that they can possess this
/datum/component/ghost_direct_control/proc/on_examined(datum/source, mob/user, list/examine_text)
SIGNAL_HANDLER
if (!isobserver(user))
return
var/mob/living/our_mob = parent
if (our_mob.stat == DEAD || our_mob.key || awaiting_ghosts)
return
examine_text += span_boldnotice("You could take control of this mob by clicking on it.")
/// Send out a request for a brain
/datum/component/ghost_direct_control/proc/request_ghost_control(role_name, poll_length, poll_ignore_key)
if (!(GLOB.ghost_role_flags & GHOSTROLE_SPAWNER))
return
awaiting_ghosts = TRUE
var/list/mob/dead/observer/candidates = poll_ghost_candidates(
question = "Do you want to play as [role_name]?",
jobban_type = ban_type,
be_special_flag = ban_type,
poll_time = poll_length,
ignore_category = poll_ignore_key,
)
awaiting_ghosts = FALSE
if (!LAZYLEN(candidates))
return
assume_direct_control(pick(candidates))
/// A ghost clicked on us, they want to get in this body
/datum/component/ghost_direct_control/proc/on_ghost_clicked(mob/our_mob, mob/dead/observer/hopeful_ghost)
SIGNAL_HANDLER
if (our_mob.key)
qdel(src)
return
if (!hopeful_ghost.client)
return
if (!(GLOB.ghost_role_flags & GHOSTROLE_SPAWNER))
to_chat(hopeful_ghost, span_warning("Ghost roles have been temporarily disabled!"))
return
if (awaiting_ghosts)
to_chat(hopeful_ghost, span_warning("Ghost candidate selection currently in progress!"))
return COMPONENT_CANCEL_ATTACK_CHAIN
if (!SSticker.HasRoundStarted())
to_chat(hopeful_ghost, span_warning("You cannot assume control of this until after the round has started!"))
return COMPONENT_CANCEL_ATTACK_CHAIN
INVOKE_ASYNC(src, PROC_REF(attempt_possession), our_mob, hopeful_ghost)
return COMPONENT_CANCEL_ATTACK_CHAIN
/// We got far enough to establish that this mob is a valid target, let's try to posssess it
/datum/component/ghost_direct_control/proc/attempt_possession(mob/our_mob, mob/dead/observer/hopeful_ghost)
var/ghost_asked = tgui_alert(usr, "Become [our_mob]?", "Are you sure?", list("Yes", "No"))
if (ghost_asked != "Yes" || QDELETED(our_mob))
return
assume_direct_control(hopeful_ghost)
/// Grant possession of our mob, component is now no longer required
/datum/component/ghost_direct_control/proc/assume_direct_control(mob/harbinger)
if (QDELETED(src))
to_chat(harbinger, span_warning("Offer to possess creature has expired!"))
return
if (is_banned_from(harbinger.ckey, list(ban_type)))
to_chat(harbinger, span_warning("You are banned from playing as this role!"))
return
if (!(GLOB.ghost_role_flags & GHOSTROLE_SPAWNER))
to_chat(harbinger, span_warning("Ghost roles have been temporarily disabled!"))
return
var/mob/living/new_body = parent
if (new_body.stat == DEAD)
to_chat(harbinger, span_warning("This body has passed away, it is of no use!"))
return
if (new_body.key)
to_chat(harbinger, span_warning("[parent] has already become sapient!"))
qdel(src)
return
if (extra_control_checks && !extra_control_checks.Invoke(harbinger))
return
harbinger.log_message("took control of [new_body].", LOG_GAME)
new_body.key = harbinger.key
to_chat(new_body, span_boldnotice(assumed_control_message))
after_assumed_control?.Invoke(harbinger)
qdel(src)
+27 -8
View File
@@ -44,6 +44,16 @@
this["amount_left"] += 1
if(this["amount_left"] > 0)
data["spawners"] += list(this)
for(var/mob_type in GLOB.joinable_mobs)
var/list/this = list()
this["name"] = mob_type
this["amount_left"] = 0
for(var/mob/joinable_mob as anything in GLOB.joinable_mobs[mob_type])
this["amount_left"] += 1
if(!this["desc"])
this["desc"] = initial(joinable_mob.desc)
if(this["amount_left"] > 0)
data["spawners"] += list(this)
return data
/datum/spawners_menu/ui_act(action, params, datum/tgui/ui)
@@ -52,16 +62,25 @@
return
var/group_name = params["name"]
if(!group_name || !(group_name in GLOB.mob_spawners))
if(!group_name)
return
var/list/spawnerlist = GLOB.mob_spawners[group_name]
for(var/obj/effect/mob_spawn/ghost_role/current_spawner as anything in spawnerlist)
if(!current_spawner.allow_spawn(usr, silent = TRUE))
spawnerlist -= current_spawner
if(!spawnerlist.len)
var/list/spawnerlist = list()
if (group_name in GLOB.mob_spawners)
spawnerlist = GLOB.mob_spawners[group_name]
if(!length(spawnerlist))
return
for(var/obj/effect/mob_spawn/ghost_role/current_spawner as anything in spawnerlist)
if(!current_spawner.allow_spawn(usr, silent = TRUE))
spawnerlist -= current_spawner
else if (group_name in GLOB.joinable_mobs)
spawnerlist = GLOB.joinable_mobs[group_name]
if(!length(spawnerlist))
return
var/obj/effect/mob_spawn/mob_spawner = pick(spawnerlist)
if(!istype(mob_spawner) || !SSpoints_of_interest.is_valid_poi(mob_spawner))
var/atom/mob_spawner = pick(spawnerlist)
if(!SSpoints_of_interest.is_valid_poi(mob_spawner))
return
switch(action)
@@ -165,9 +165,7 @@
cargorilla = new(cargo_sloth.loc)
cargorilla.name = cargo_sloth.name
// We do a poll on roundstart, don't let ghosts in early
cargorilla.being_polled_for = TRUE
INVOKE_ASYNC(src, PROC_REF(make_id_for_gorilla))
// hm our sloth looks funny today
qdel(cargo_sloth)
+1
View File
@@ -332,6 +332,7 @@
var/list/long_job_lists = list(
"Ghost and Other Roles" = list(
ROLE_PAI,
ROLE_BOT,
ROLE_BRAINWASHED,
ROLE_DEATHSQUAD,
ROLE_DRONE,
@@ -11,7 +11,6 @@ GLOBAL_LIST_INIT(high_priority_sentience, typecacheof(list(
/mob/living/basic/pig,
/mob/living/basic/rabbit,
/mob/living/basic/sheep,
/mob/living/simple_animal/bot/mulebot,
/mob/living/simple_animal/bot/secbot/beepsky,
/mob/living/simple_animal/hostile/retaliate/goat,
/mob/living/simple_animal/hostile/retaliate/goose/vomit,
@@ -1,5 +1,5 @@
/datum/emote/silicon
mob_type_allowed_typecache = list(/mob/living/silicon)
mob_type_allowed_typecache = list(/mob/living/silicon, /mob/living/simple_animal/bot)
emote_type = EMOTE_AUDIBLE
/datum/emote/silicon/boop
+103 -111
View File
@@ -37,8 +37,6 @@
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
@@ -47,13 +45,11 @@
///All initial access this bot started with.
var/list/prev_access = list()
///Bot-related mode flags on the Bot indicating how they will act.
var/bot_mode_flags = BOT_MODE_ON | BOT_MODE_REMOTE_ENABLED | BOT_MODE_PAI_CONTROLLABLE
// Selections: BOT_MODE_ON | BOT_MODE_AUTOPATROL | BOT_MODE_REMOTE_ENABLED | BOT_MODE_PAI_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_GHOST_CONTROLLABLE
var/bot_mode_flags = BOT_MODE_ON | BOT_MODE_REMOTE_ENABLED | BOT_MODE_GHOST_CONTROLLABLE
///Bot-related cover flags on the Bot to deal with what has been done to their cover, including emagging.
///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
// Selections: BOT_COVER_OPEN | BOT_COVER_LOCKED | BOT_COVER_EMAGGED | BOT_COVER_HACKED
///Small name of what the bot gets messed with when getting hacked/emagged.
var/hackables = "system circuits"
@@ -106,27 +102,31 @@
var/reset_access_timer_id
var/ignorelistcleanuptimer = 1 // This ticks up every automated action, at 300 we clean the ignore list
/// Component which allows ghosts to take over this bot
var/datum/component/ghost_direct_control/personality_download
/// If true we will allow ghosts to control this mob
var/can_be_possessed = FALSE
/// If true we will offer this
COOLDOWN_DECLARE(offer_ghosts_cooldown)
/// Message to display upon possession
var/possessed_message = "You're a generic bot. How did one of these even get made?"
/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.
if(paicard)
return "<b>pAI Controlled</b>"
else
return "<b>Autonomous</b>"
else if(!(bot_mode_flags & BOT_MODE_ON))
return "<b>Autonomous</b>"
if(!(bot_mode_flags & BOT_MODE_ON))
return "<span class='bad'>Inactive</span>"
else
return "<span class='average'>[mode]</span>"
return "<span class='average'>[mode]</span>"
/**
* Returns a status string about the bot's current status, if it's moving, manually controlled, or idle.
*/
/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 paicard ? "pAI Controlled" : "Autonomous"
else if(!(bot_mode_flags & BOT_MODE_ON))
return "Autonomous"
if(!(bot_mode_flags & BOT_MODE_ON))
return "Inactive"
else
return "[mode]"
return "[mode]"
/mob/living/simple_animal/bot/proc/turn_on()
if(stat)
@@ -190,17 +190,80 @@
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)
/mob/living/simple_animal/bot/Destroy()
if(path_hud)
QDEL_NULL(path_hud)
path_hud = null
GLOB.bots_list -= src
if(paicard)
ejectpai()
QDEL_NULL(personality_download)
QDEL_NULL(internal_radio)
QDEL_NULL(access_card)
QDEL_NULL(path_hud)
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)
can_be_possessed = TRUE
var/can_announce = !mapload && COOLDOWN_FINISHED(src, offer_ghosts_cooldown)
personality_download = AddComponent(\
/datum/component/ghost_direct_control,\
ban_type = ROLE_BOT,\
poll_candidates = can_announce,\
poll_ignore_key = POLL_IGNORE_BOTS,\
assumed_control_message = possessed_message,\
extra_control_checks = CALLBACK(src, PROC_REF(check_possession)),\
after_assumed_control = CALLBACK(src, PROC_REF(post_possession)),\
)
if (can_announce)
COOLDOWN_START(src, offer_ghosts_cooldown, 30 SECONDS)
/// Disables this bot from being possessed by ghosts
/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
/// Returns true if this mob can be controlled
/mob/living/simple_animal/bot/proc/check_possession(mob/potential_possessor)
if (!can_be_possessed)
to_chat(potential_possessor, span_warning("The bot's personality download has been disabled!"))
return can_be_possessed
/// Fired after something takes control of this mob
/mob/living/simple_animal/bot/proc/post_possession()
playsound(src, 'sound/machines/ping.ogg', 30, TRUE)
say("New personality installed successfully!", forced = "bot")
rename(src)
/// Allows renaming the bot to something else
/mob/living/simple_animal/bot/proc/rename(mob/user)
var/new_name = sanitize_name(reject_bad_text(tgui_input_text(
user = user,
message = "This machine is designated [real_name]. Would you like to update its registration?",
title = "Name change",
default = real_name,
max_length = MAX_NAME_LEN,
)))
if (isnull(new_name) || QDELETED(src))
return
if (key && user != src)
var/accepted = tgui_alert(
src,
message = "Do you wish to be renamed to [new_name]?",
title = "Name change",
buttons = list("Yes", "No"),
)
if (accepted != "Yes" || QDELETED(src))
return
fully_replace_character_name(real_name, new_name)
/mob/living/simple_animal/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.
return TRUE
@@ -271,10 +334,6 @@
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))
@@ -393,21 +452,14 @@
/mob/living/simple_animal/bot/attackby(obj/item/attacking_item, mob/living/user, params)
if(attacking_item.GetID())
unlock_with_id(user)
else if(istype(attacking_item, /obj/item/pai_card))
insertpai(user, attacking_item)
else if(attacking_item.tool_behaviour == TOOL_HEMOSTAT && paicard)
if(bot_cover_flags & BOT_COVER_OPEN)
to_chat(user, span_warning("Close the access panel before manipulating the personality slot!"))
else
to_chat(user, span_notice("You attempt to pull [paicard] free..."))
if(do_after(user, 30, target = src))
if (paicard)
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)
else
if(attacking_item.force) //if force is non-zero
do_sparks(5, TRUE, src)
..()
return
return ..()
/mob/living/simple_animal/bot/attacked_by(obj/item/I, mob/living/user)
. = ..()
if (!.)
return
do_sparks(5, TRUE, src)
/mob/living/simple_animal/bot/bullet_act(obj/projectile/Proj, def_zone, piercing_hit = FALSE)
if(Proj && (Proj.damage_type == BRUTE || Proj.damage_type == BURN))
@@ -422,10 +474,6 @@
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(0)
if(prob(70/severity))
set_active_language(get_random_spoken_language())
@@ -794,9 +842,6 @@ 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
@@ -878,11 +923,10 @@ Pass a positive integer as an argument to override a bot's default speed.
data["emagged"] = bot_cover_flags & BOT_COVER_EMAGGED
data["has_access"] = check_access(user)
data["locked"] = bot_cover_flags & BOT_COVER_LOCKED
data["pai"] = list()
data["settings"] = list()
if(!(bot_cover_flags & BOT_COVER_LOCKED) || issilicon(user) || isAdminGhostAI(user))
data["pai"]["allow_pai"] = bot_mode_flags & BOT_MODE_PAI_CONTROLLABLE
data["pai"]["card_inserted"] = paicard
data["settings"]["allow_possession"] = bot_mode_flags & BOT_MODE_GHOST_CONTROLLABLE
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)
data["settings"]["power"] = bot_mode_flags & BOT_MODE_ON
@@ -930,10 +974,13 @@ Pass a positive integer as an argument to override a bot's default speed.
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)
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()
if("rename")
rename(usr)
/mob/living/simple_animal/bot/update_icon_state()
icon_state = "[isnull(base_icon_state) ? initial(icon_state) : base_icon_state][get_bot_flag(bot_mode_flags, BOT_MODE_ON)]"
@@ -950,55 +997,6 @@ Pass a positive integer as an argument to override a bot's default speed.
return TRUE
return FALSE
/mob/living/simple_animal/bot/proc/insertpai(mob/user, obj/item/pai_card/card)
if(paicard)
to_chat(user, span_warning("A [paicard] is already inserted!"))
return
if(bot_cover_flags & BOT_COVER_LOCKED || !(bot_cover_flags & BOT_COVER_OPEN))
to_chat(user, span_warning("The personality slot is locked."))
return
if(!(bot_mode_flags & BOT_MODE_PAI_CONTROLLABLE) || key) //Not pAI controllable or is already player controlled.
to_chat(user, span_warning("[src] is not compatible with [card]!"))
return
if(!card.pai || !card.pai.mind)
to_chat(user, span_warning("[card] is inactive."))
return
if(!user.transferItemToLoc(card, src))
return
paicard = card
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/proc/ejectpai(mob/user = null, announce = TRUE)
if(paicard)
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)
/mob/living/simple_animal/bot/proc/ejectpairemote(mob/user)
if(check_access(user) && paicard)
speak("Ejecting personality chip.", radio_channel)
ejectpai(user)
/mob/living/simple_animal/bot/Login()
. = ..()
if(!. || !client)
@@ -1019,12 +1017,6 @@ Pass a positive integer as an argument to override a bot's default speed.
update_appearance()
/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(0)
/mob/living/simple_animal/bot/sentience_act()
faction -= FACTION_SILICON
@@ -19,6 +19,7 @@
hackables = "cleaning software"
path_image_color = "#993299"
greyscale_config = /datum/greyscale_config/buckets_cleanbot
possessed_message = "You are a cleanbot! Clean the station to the best of your ability!"
///the bucket used to build us.
var/obj/item/reagent_containers/cup/bucket/build_bucket
@@ -91,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_PAI_CONTROLLABLE
bot_mode_flags = BOT_MODE_ON | BOT_MODE_AUTOPATROL | BOT_MODE_REMOTE_ENABLED | BOT_MODE_GHOST_CONTROLLABLE
/mob/living/simple_animal/bot/cleanbot/medbay
name = "Scrubs, MD"
@@ -20,6 +20,7 @@
bot_type = FIRE_BOT
hackables = "fire safety protocols"
path_image_color = "#FFA500"
possessed_message = "You are a firebot! Protect the station from fires to the best of your ability!"
var/atom/target_fire
var/atom/old_target_fire
@@ -22,6 +22,7 @@
bot_type = FLOOR_BOT
hackables = "floor construction protocols"
path_image_color = "#FFA500"
possessed_message = "You are a floorbot! Repair the hull to the best of your ability!"
var/process_type //Determines what to do when process_scan() receives a target. See process_scan() for details.
var/targetdirection
@@ -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_PAI_CONTROLLABLE | BOT_MODE_AUTOPATROL
bot_mode_flags = BOT_MODE_ON | BOT_MODE_REMOTE_ENABLED | BOT_MODE_GHOST_CONTROLLABLE | BOT_MODE_AUTOPATROL
hackables = "sound control systems"
path_image_color = "#FF69B4"
data_hud_type = DATA_HUD_SECURITY_BASIC //show jobs
@@ -17,6 +17,7 @@
baton_type = /obj/item/bikehorn
cuff_type = /obj/item/restraints/handcuffs/cable/zipties/fake/used
security_mode_flags = SECBOT_CHECK_WEAPONS | SECBOT_HANDCUFF_TARGET
possessed_message = "You are a honkbot! Make sure the crew are having a great time!"
///Keeping track of how much we honk to prevent spamming it
var/limiting_spam = FALSE
@@ -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_PAI_CONTROLLABLE
bot_mode_flags = ~BOT_MODE_GHOST_CONTROLLABLE
bot_type = HYGIENE_BOT
hackables = "cleaning service protocols"
path_image_color = "#993299"
@@ -33,6 +33,7 @@
data_hud_type = DATA_HUD_MEDICAL_ADVANCED
hackables = "health processor circuits"
path_image_color = "#DDDDFF"
possessed_message = "You are a medbot! Ensure good health among the crew to the best of your ability!"
/// drop determining variable
var/healthanalyzer = /obj/item/healthanalyzer
@@ -71,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_PAI_CONTROLLABLE
bot_mode_flags = BOT_MODE_ON | BOT_MODE_AUTOPATROL | BOT_MODE_REMOTE_ENABLED | BOT_MODE_GHOST_CONTROLLABLE
/mob/living/simple_animal/bot/medbot/stationary
medical_mode_flags = MEDBOT_DECLARE_CRIT | MEDBOT_STATIONARY_MODE | MEDBOT_SPEAK_MODE
@@ -30,6 +30,7 @@
radio_channel = RADIO_CHANNEL_SUPPLY
bot_type = MULE_BOT
path_image_color = "#7F5200"
possessed_message = "You are a MULEbot! Do your best to make sure that packages get to their destination!"
/// unique identifier in case there are multiple mulebots.
var/id
@@ -135,8 +136,6 @@
/mob/living/simple_animal/bot/mulebot/proc/set_id(new_id)
id = new_id
if(!paicard)
name = "[initial(name)] ([new_id])"
/mob/living/simple_animal/bot/mulebot/bot_reset()
..()
@@ -263,8 +262,9 @@
data["autoReturn"] = auto_return
data["autoPickup"] = auto_pickup
data["reportDelivery"] = report_delivery
data["haspai"] = paicard ? TRUE : FALSE
data["id"] = id
data["allow_possession"] = bot_mode_flags & BOT_MODE_GHOST_CONTROLLABLE
data["possession_enabled"] = can_be_possessed
return data
/mob/living/simple_animal/bot/mulebot/ui_act(action, params)
@@ -276,7 +276,7 @@
if("lock")
if(usr.has_unlimited_silicon_privilege)
bot_cover_flags ^= BOT_COVER_LOCKED
. = TRUE
return TRUE
if("on")
if(bot_mode_flags & BOT_MODE_ON)
turn_off()
@@ -287,10 +287,10 @@
if(!turn_on())
to_chat(usr, span_warning("You can't switch on [src]!"))
return
. = TRUE
return TRUE
else
bot_control(action, usr, params) // Kill this later. // Kill PDAs in general please
. = TRUE
return TRUE
/mob/living/simple_animal/bot/mulebot/bot_control(command, mob/user, list/params = list(), pda = FALSE)
if(pda && wires.is_cut(WIRE_RX)) // MULE wireless is controlled by wires.
@@ -342,8 +342,6 @@
auto_pickup = !auto_pickup
if("report")
report_delivery = !report_delivery
if("ejectpai")
ejectpairemote(user)
/mob/living/simple_animal/bot/mulebot/proc/buzz(type)
switch(type)
@@ -658,7 +656,7 @@
/mob/living/simple_animal/bot/mulebot/MobBump(mob/M) // called when the bot bumps into a mob
if(paicard || !isliving(M)) //if there's a PAIcard controlling the bot, they aren't allowed to harm folks.
if(mind || !isliving(M)) //if there's a sentience controlling the bot, they aren't allowed to harm folks.
return ..()
var/mob/living/L = M
if(wires.is_cut(WIRE_AVOIDANCE)) // usually just bumps, but if the avoidance wire is cut, knocks them over.
@@ -765,11 +763,6 @@
else
return ..()
/mob/living/simple_animal/bot/mulebot/insertpai(mob/user, obj/item/pai_card/card)
. = ..()
if(.)
visible_message(span_notice("[src]'s safeties are locked on."))
/// Checks whether the bot can complete a step_towards, checking whether the bot is on and has the charge to do the move. Returns COMPONENT_MOB_BOT_CANCELSTEP if the bot should not step.
/mob/living/simple_animal/bot/mulebot/proc/check_pre_step(datum/source)
SIGNAL_HANDLER
@@ -787,6 +780,10 @@
cell?.use(cell_move_power_usage)
/mob/living/simple_animal/bot/mulebot/post_possession()
. = ..()
visible_message(span_notice("[src]'s safeties are locked on."))
/mob/living/simple_animal/bot/mulebot/paranormal//allows ghosts only unless hacked to actually be useful
name = "\improper GHOULbot"
desc = "A rather ghastly looking... Multiple Utility Load Effector bot? It only seems to accept paranormal forces, and for this reason is fucking useless."
@@ -15,10 +15,11 @@
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_PAI_CONTROLLABLE
bot_mode_flags = ~BOT_MODE_GHOST_CONTROLLABLE
data_hud_type = DATA_HUD_SECURITY_ADVANCED
hackables = "target identification systems"
path_image_color = "#FF0000"
possessed_message = "You are a securitron! Guard the station to the best of your ability!"
///The type of baton this Secbot will use
var/baton_type = /obj/item/melee/baton/security
@@ -64,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_PAI_CONTROLLABLE|BOT_MODE_AUTOPATROL)
bot_mode_flags = ~(BOT_MODE_GHOST_CONTROLLABLE|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_PAI_CONTROLLABLE|BOT_MODE_AUTOPATROL)
bot_mode_flags = ~(BOT_MODE_GHOST_CONTROLLABLE|BOT_MODE_AUTOPATROL)
security_mode_flags = SECBOT_DECLARE_ARRESTS | SECBOT_CHECK_IDS | SECBOT_CHECK_RECORDS
/mob/living/simple_animal/bot/secbot/beepsky/jr
@@ -86,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_PAI_CONTROLLABLE|BOT_MODE_AUTOPATROL)
bot_mode_flags = ~(BOT_MODE_GHOST_CONTROLLABLE|BOT_MODE_AUTOPATROL)
security_mode_flags = SECBOT_DECLARE_ARRESTS | SECBOT_CHECK_IDS | SECBOT_CHECK_RECORDS
/mob/living/simple_animal/bot/secbot/genesky
@@ -19,6 +19,7 @@
bot_type = VIBE_BOT
data_hud_type = DATA_HUD_DIAGNOSTIC_BASIC
path_image_color = "#2cac12"
possessed_message = "You are a vibebot! Maintain the station's vibes to the best of your ability!"
///The vibe ability given to vibebots, so sentient ones can still change their color.
var/datum/action/innate/vibe/vibe_ability
@@ -124,58 +124,28 @@
faction = list(FACTION_NEUTRAL, FACTION_MONKEY, FACTION_JUNGLE)
gold_core_spawnable = NO_SPAWN
unique_name = FALSE
/// Whether we're currently being polled over
var/being_polled_for = FALSE
/mob/living/simple_animal/hostile/gorilla/cargo_domestic/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_PACIFISM, INNATE_TRAIT)
AddComponent(/datum/component/crate_carrier)
/mob/living/simple_animal/hostile/gorilla/cargo_domestic/attack_ghost(mob/user)
if(being_polled_for || mind || client || (flags_1 & ADMIN_SPAWNED_1))
return ..()
if(is_banned_from(user.ckey, list(ROLE_SENTIENCE, ROLE_SYNDICATE)))
return ..()
if(!SSticker.HasRoundStarted())
return ..()
var/become_gorilla = tgui_alert(user, "Become a Cargorilla?", "Confirm", list("Yes", "No"))
if(become_gorilla != "Yes" || QDELETED(src) || QDELETED(user) || being_polled_for || mind || client)
return
enter_ghost(user)
/// Poll ghosts for control of the gorilla.
/mob/living/simple_animal/hostile/gorilla/cargo_domestic/proc/poll_for_gorilla()
being_polled_for = TRUE
var/list/mob/dead/candidates = poll_candidates_for_mob(
"Do you want to play as a Cargorilla?",
ROLE_SENTIENCE,
ROLE_SENTIENCE,
30 SECONDS,
src,
POLL_IGNORE_CARGORILLA
AddComponent(\
/datum/component/ghost_direct_control,\
poll_candidates = TRUE,\
poll_length = 30 SECONDS,\
role_name = "Cargorilla",\
assumed_control_message = "You are Cargorilla, a pacifistic friend of the station and carrier of freight.",\
poll_ignore_key = POLL_IGNORE_CARGORILLA,\
after_assumed_control = CALLBACK(src, PROC_REF(became_player_controlled)),\
)
being_polled_for = FALSE
if(QDELETED(src) || mind || client)
return
if(LAZYLEN(candidates))
enter_ghost(pick(candidates))
/// Brings in the a ghost to take control of the gorilla.
/mob/living/simple_animal/hostile/gorilla/cargo_domestic/proc/enter_ghost(mob/dead/user)
key = user.key
if(!mind)
CRASH("[type] - enter_ghost didn't end up with a mind.")
/// Called once a ghost assumes control
/mob/living/simple_animal/hostile/gorilla/cargo_domestic/proc/became_player_controlled()
mind.set_assigned_role(SSjob.GetJobType(/datum/job/cargo_technician))
mind.special_role = "Cargorilla"
to_chat(src, span_boldnotice("You are a Cargorilla, a pacifistic friend of the station and carrier of freight."))
to_chat(src, span_notice("You can pick up crates by clicking on them, and drop them by clicking on the ground."))
/obj/item/card/id/advanced/cargo_gorilla
@@ -35,50 +35,39 @@
var/datum/action/cooldown/domain/domain
///The Spell that the rat uses to recruit/convert more rats.
var/datum/action/cooldown/riot/riot
///Should we request a mind immediately upon spawning?
var/poll_ghosts = FALSE
/mob/living/simple_animal/hostile/regalrat/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT)
AddElement(/datum/element/waddling)
AddComponent(\
/datum/component/ghost_direct_control,\
poll_candidates = poll_ghosts,\
role_name = "the Regal Rat, cheesy be their crown",\
poll_ignore_key = POLL_IGNORE_REGAL_RAT,\
assumed_control_message = "You are an independent, invasive force on the station! Hoard coins, trash, cheese, and the like from the safety of darkness!",\
after_assumed_control = CALLBACK(src, PROC_REF(became_player_controlled)),\
)
domain = new(src)
riot = new(src)
domain.Grant(src)
riot.Grant(src)
AddElement(/datum/element/waddling)
ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT)
/mob/living/simple_animal/hostile/regalrat/Destroy()
QDEL_NULL(domain)
QDEL_NULL(riot)
return ..()
/mob/living/simple_animal/hostile/regalrat/proc/become_player_controlled(mob/user)
log_message("took control of [name].", LOG_GAME)
key = user.key
notify_ghosts("All rise for the rat king, ascendant to the throne in \the [get_area(src)].", source = src, action = NOTIFY_ORBIT, flashwindow = FALSE, header = "Sentient Rat Created")
to_chat(src, span_notice("You are an independent, invasive force on the station! Horde coins, trash, cheese, and the like from the safety of darkness!"))
/mob/living/simple_animal/hostile/regalrat/proc/get_player()
var/list/mob/dead/observer/candidates = poll_ghost_candidates("Do you want to play as the Regal Rat, cheesey be their crown?", ROLE_SENTIENCE, ROLE_SENTIENCE, 100, POLL_IGNORE_REGAL_RAT)
if(LAZYLEN(candidates) && !mind)
var/mob/dead/observer/candidate = pick(candidates)
become_player_controlled(candidate)
/mob/living/simple_animal/hostile/regalrat/attack_ghost(mob/user)
. = ..()
if(. || !(GLOB.ghost_role_flags & GHOSTROLE_SPAWNER))
return
if(key || stat)
return
if(!SSticker.HasRoundStarted())
to_chat(user, span_warning("You cannot assume control of this until after the round has started!"))
return
var/rat_ask = tgui_alert(usr, "Become the Royal Rat?", "Are you sure?", list("Yes", "No"))
if(rat_ask != "Yes" || QDELETED(src))
return
if(key)
to_chat(user, span_warning("Someone else already took the rat!"))
return
become_player_controlled(user)
/mob/living/simple_animal/hostile/regalrat/proc/became_player_controlled()
notify_ghosts(
"All rise for the rat king, ascendant to the throne in \the [get_area(src)].",
source = src,
action = NOTIFY_ORBIT,
flashwindow = FALSE,
header = "Sentient Rat Created",
)
/mob/living/simple_animal/hostile/regalrat/handle_automated_action()
if(prob(20))
@@ -181,9 +170,11 @@
return FALSE
opening_airlock = FALSE
/mob/living/simple_animal/hostile/regalrat/controlled
poll_ghosts = TRUE
/mob/living/simple_animal/hostile/regalrat/controlled/Initialize(mapload)
. = ..()
INVOKE_ASYNC(src, PROC_REF(get_player))
var/kingdom = pick("Plague","Miasma","Maintenance","Trash","Garbage","Rat","Vermin","Cheese")
var/title = pick("King","Lord","Prince","Emperor","Supreme","Overlord","Master","Shogun","Bojar","Tsar")
name = "[kingdom] [title]"
+1
View File
@@ -929,6 +929,7 @@
#include "code\datums\components\gags_recolorable.dm"
#include "code\datums\components\gas_leaker.dm"
#include "code\datums\components\geiger_sound.dm"
#include "code\datums\components\ghost_direct_control.dm"
#include "code\datums\components\gps.dm"
#include "code\datums\components\grillable.dm"
#include "code\datums\components\ground_sinking.dm"
+31 -24
View File
@@ -12,32 +12,40 @@ export const Mule = (props, context) => {
load,
mode,
modeStatus,
haspai,
autoReturn,
autoPickup,
reportDelivery,
destination,
home,
id,
allow_possession,
possession_enabled,
destinations = [],
} = data;
const locked = data.locked && !data.siliconUser;
return (
<Window width={350} height={425}>
<Window width={350} height={445}>
<Window.Content>
<InterfaceLockNoticeBox />
<Section
title="Status"
minHeight="110px"
buttons={
!locked && (
<>
<Button
icon={on ? 'power-off' : 'times'}
content={on ? 'On' : 'Off'}
selected={on}
onClick={() => act('on')}
icon="fa-poll-h"
content="Rename"
onClick={() => act('rename')}
/>
)
{!locked && (
<Button
icon={on ? 'power-off' : 'times'}
content={on ? 'On' : 'Off'}
selected={on}
onClick={() => act('on')}
/>
)}
</>
}>
<ProgressBar
value={cell ? cellPercent / 100 : 0}
@@ -66,22 +74,13 @@ export const Mule = (props, context) => {
<Section
title="Controls"
buttons={
<>
{!!load && (
<Button
icon="eject"
content="Unload"
onClick={() => act('unload')}
/>
)}
{!!haspai && (
<Button
icon="eject"
content="Eject PAI"
onClick={() => act('ejectpai')}
/>
)}
</>
!!load && (
<Button
icon="eject"
content="Unload"
onClick={() => act('unload')}
/>
)
}>
<LabeledList>
<LabeledList.Item label="ID">
@@ -137,6 +136,14 @@ export const Mule = (props, context) => {
content="Report Delivery"
onClick={() => act('report')}
/>
<br />
{allow_possession && (
<Button.Checkbox
checked={possession_enabled}
content="Download Personality"
onClick={() => act('toggle_personality')}
/>
)}
</LabeledList.Item>
</LabeledList>
</Section>
+36 -37
View File
@@ -8,21 +8,18 @@ type SimpleBotContext = {
locked: number;
emagged: number;
has_access: number;
pai: Pai;
settings: Settings;
custom_controls: Controls;
};
type Pai = {
allow_pai: number;
card_inserted: number;
};
type Settings = {
power: number;
airplane_mode: number;
maintenance_lock: number;
patrol_station: number;
allow_possession: number;
possession_enabled: number;
has_personality: number;
};
type Controls = {
@@ -59,13 +56,18 @@ export const SimpleBot = (props, context) => {
/** Creates a lock button at the top of the controls */
const TabDisplay = (props, context) => {
const { act, data } = useBackend<SimpleBotContext>(context);
const { can_hack, has_access, locked, pai } = data;
const { allow_pai } = pai;
const { can_hack, has_access, locked } = data;
return (
<>
{!!can_hack && <HackButton />}
{!!allow_pai && <PaiButton />}
<Button
color="transparent"
icon="fa-poll-h"
onClick={() => act('rename')}
tooltip="Update the bot's name registration.">
Rename
</Button>
<Button
color="transparent"
disabled={!has_access && !can_hack}
@@ -101,38 +103,18 @@ 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 { card_inserted } = data.pai;
if (!card_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={!card_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);
const { settings } = data;
const { airplane_mode, patrol_station, power, maintenance_lock } = settings;
const {
airplane_mode,
patrol_station,
power,
maintenance_lock,
allow_possession,
possession_enabled,
} = settings;
return (
<LabeledControls>
@@ -187,6 +169,23 @@ const SettingsDisplay = (props, context) => {
/>
</Tooltip>
</LabeledControls.Item>
{allow_possession && (
<LabeledControls.Item label="Personality">
<Tooltip
content={
possession_enabled
? 'Resets personality to factory default.'
: 'Enables download of a unique personality.'
}>
<Icon
size={2}
name="robot"
color={possession_enabled ? 'good' : 'gray'}
onClick={() => act('toggle_personality')}
/>
</Tooltip>
</LabeledControls.Item>
)}
</LabeledControls>
);
};
+18 -9
View File
@@ -10,6 +10,7 @@ type SpawnersMenuContext = {
type spawner = {
name: string;
amount_left: number;
desc?: string;
you_are_text?: string;
flavor_text?: string;
important_text?: string;
@@ -54,15 +55,23 @@ export const SpawnersMenu = (props, context) => {
</Stack>
}>
<LabeledList>
<LabeledList.Item label="Origin">
{spawner.you_are_text || 'Unknown'}
</LabeledList.Item>
<LabeledList.Item label="Directives">
{spawner.flavor_text || 'None'}
</LabeledList.Item>
<LabeledList.Item color="bad" label="Conditions">
{spawner.important_text || 'None'}
</LabeledList.Item>
{spawner.desc ? (
<LabeledList.Item label="Description">
{spawner.desc}
</LabeledList.Item>
) : (
<div>
<LabeledList.Item label="Origin">
{spawner.you_are_text || 'Unknown'}
</LabeledList.Item>
<LabeledList.Item label="Directives">
{spawner.flavor_text || 'None'}
</LabeledList.Item>
<LabeledList.Item color="bad" label="Conditions">
{spawner.important_text || 'None'}
</LabeledList.Item>
</div>
)}
</LabeledList>
</Section>
</Stack.Item>