From 2ee79d7077804f4e918d6c4bdbe856570cf24a01 Mon Sep 17 00:00:00 2001 From: Jacquerel Date: Tue, 11 Jul 2023 01:55:43 +0100 Subject: [PATCH] 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 :cl: 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 /:cl: --- code/__DEFINES/robots.dm | 4 +- code/__DEFINES/role_preferences.dm | 1 + code/_globalvars/lists/objects.dm | 113 +++++---- code/_globalvars/lists/poll_ignore.dm | 2 + .../datums/components/ghost_direct_control.dm | 140 ++++++++++++ code/datums/spawners_menu.dm | 35 ++- code/datums/station_traits/neutral_traits.dm | 2 - code/modules/admin/sql_ban_system.dm | 1 + code/modules/events/ghost_role/sentience.dm | 1 - .../modules/mob/living/silicon/robot/emote.dm | 2 +- .../mob/living/simple_animal/bot/bot.dm | 214 +++++++++--------- .../mob/living/simple_animal/bot/cleanbot.dm | 3 +- .../mob/living/simple_animal/bot/firebot.dm | 1 + .../mob/living/simple_animal/bot/floorbot.dm | 1 + .../mob/living/simple_animal/bot/honkbot.dm | 3 +- .../living/simple_animal/bot/hygienebot.dm | 2 +- .../mob/living/simple_animal/bot/medbot.dm | 3 +- .../mob/living/simple_animal/bot/mulebot.dm | 25 +- .../mob/living/simple_animal/bot/secbot.dm | 9 +- .../mob/living/simple_animal/bot/vibebot.dm | 1 + .../simple_animal/hostile/gorilla/gorilla.dm | 50 +--- .../living/simple_animal/hostile/regalrat.dm | 55 ++--- tgstation.dme | 1 + tgui/packages/tgui/interfaces/Mule.js | 55 +++-- tgui/packages/tgui/interfaces/SimpleBot.tsx | 73 +++--- .../packages/tgui/interfaces/SpawnersMenu.tsx | 27 ++- 26 files changed, 497 insertions(+), 327 deletions(-) create mode 100644 code/datums/components/ghost_direct_control.dm diff --git a/code/__DEFINES/robots.dm b/code/__DEFINES/robots.dm index 5f7f7706560..20d838b80f7 100644 --- a/code/__DEFINES/robots.dm +++ b/code/__DEFINES/robots.dm @@ -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. diff --git a/code/__DEFINES/role_preferences.dm b/code/__DEFINES/role_preferences.dm index a57d9204549..c69da3a904f 100644 --- a/code/__DEFINES/role_preferences.dm +++ b/code/__DEFINES/role_preferences.dm @@ -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. diff --git a/code/_globalvars/lists/objects.dm b/code/_globalvars/lists/objects.dm index 0afdcb915f1..fb6be9e1b9c 100644 --- a/code/_globalvars/lists/objects.dm +++ b/code/_globalvars/lists/objects.dm @@ -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()) diff --git a/code/_globalvars/lists/poll_ignore.dm b/code/_globalvars/lists/poll_ignore.dm index 96d49f88a1c..f216ef0c68a 100644 --- a/code/_globalvars/lists/poll_ignore.dm +++ b/code/_globalvars/lists/poll_ignore.dm @@ -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()) diff --git a/code/datums/components/ghost_direct_control.dm b/code/datums/components/ghost_direct_control.dm new file mode 100644 index 00000000000..6ff22b4266a --- /dev/null +++ b/code/datums/components/ghost_direct_control.dm @@ -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) diff --git a/code/datums/spawners_menu.dm b/code/datums/spawners_menu.dm index 849ad8e3ef6..06b6dbc76a8 100644 --- a/code/datums/spawners_menu.dm +++ b/code/datums/spawners_menu.dm @@ -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) diff --git a/code/datums/station_traits/neutral_traits.dm b/code/datums/station_traits/neutral_traits.dm index 73d9a4e6056..49159e16755 100644 --- a/code/datums/station_traits/neutral_traits.dm +++ b/code/datums/station_traits/neutral_traits.dm @@ -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) diff --git a/code/modules/admin/sql_ban_system.dm b/code/modules/admin/sql_ban_system.dm index 146d9612f03..cd0ae40ece5 100644 --- a/code/modules/admin/sql_ban_system.dm +++ b/code/modules/admin/sql_ban_system.dm @@ -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, diff --git a/code/modules/events/ghost_role/sentience.dm b/code/modules/events/ghost_role/sentience.dm index 21d939cdaca..fbc1dfad9d5 100644 --- a/code/modules/events/ghost_role/sentience.dm +++ b/code/modules/events/ghost_role/sentience.dm @@ -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, diff --git a/code/modules/mob/living/silicon/robot/emote.dm b/code/modules/mob/living/silicon/robot/emote.dm index 81c574583a0..2fed6a4ec9c 100644 --- a/code/modules/mob/living/silicon/robot/emote.dm +++ b/code/modules/mob/living/silicon/robot/emote.dm @@ -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 diff --git a/code/modules/mob/living/simple_animal/bot/bot.dm b/code/modules/mob/living/simple_animal/bot/bot.dm index 3275cbcf87c..0e1dfab398b 100644 --- a/code/modules/mob/living/simple_animal/bot/bot.dm +++ b/code/modules/mob/living/simple_animal/bot/bot.dm @@ -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 "pAI Controlled" - else - return "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]" /** * 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 hemostat 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 diff --git a/code/modules/mob/living/simple_animal/bot/cleanbot.dm b/code/modules/mob/living/simple_animal/bot/cleanbot.dm index a73369b8457..8ff6beef757 100644 --- a/code/modules/mob/living/simple_animal/bot/cleanbot.dm +++ b/code/modules/mob/living/simple_animal/bot/cleanbot.dm @@ -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" diff --git a/code/modules/mob/living/simple_animal/bot/firebot.dm b/code/modules/mob/living/simple_animal/bot/firebot.dm index 64aafb8071b..e6c7ff0c921 100644 --- a/code/modules/mob/living/simple_animal/bot/firebot.dm +++ b/code/modules/mob/living/simple_animal/bot/firebot.dm @@ -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 diff --git a/code/modules/mob/living/simple_animal/bot/floorbot.dm b/code/modules/mob/living/simple_animal/bot/floorbot.dm index 7a1fd85f37a..a06a0398996 100644 --- a/code/modules/mob/living/simple_animal/bot/floorbot.dm +++ b/code/modules/mob/living/simple_animal/bot/floorbot.dm @@ -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 diff --git a/code/modules/mob/living/simple_animal/bot/honkbot.dm b/code/modules/mob/living/simple_animal/bot/honkbot.dm index 26c22ca1dca..1eee60357f4 100644 --- a/code/modules/mob/living/simple_animal/bot/honkbot.dm +++ b/code/modules/mob/living/simple_animal/bot/honkbot.dm @@ -9,7 +9,7 @@ radio_key = /obj/item/encryptionkey/headset_service //doesn't have security key radio_channel = RADIO_CHANNEL_SERVICE //Doesn't even use the radio anyway. bot_type = HONK_BOT - bot_mode_flags = BOT_MODE_ON | BOT_MODE_REMOTE_ENABLED | BOT_MODE_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 diff --git a/code/modules/mob/living/simple_animal/bot/hygienebot.dm b/code/modules/mob/living/simple_animal/bot/hygienebot.dm index fdd5c127f72..bdb0c8fb736 100644 --- a/code/modules/mob/living/simple_animal/bot/hygienebot.dm +++ b/code/modules/mob/living/simple_animal/bot/hygienebot.dm @@ -15,7 +15,7 @@ maints_access_required = list(ACCESS_ROBOTICS, ACCESS_JANITOR) radio_key = /obj/item/encryptionkey/headset_service radio_channel = RADIO_CHANNEL_SERVICE //Service - bot_mode_flags = ~BOT_MODE_PAI_CONTROLLABLE + bot_mode_flags = ~BOT_MODE_GHOST_CONTROLLABLE bot_type = HYGIENE_BOT hackables = "cleaning service protocols" path_image_color = "#993299" diff --git a/code/modules/mob/living/simple_animal/bot/medbot.dm b/code/modules/mob/living/simple_animal/bot/medbot.dm index 3950930bca1..4171cf1e8ac 100644 --- a/code/modules/mob/living/simple_animal/bot/medbot.dm +++ b/code/modules/mob/living/simple_animal/bot/medbot.dm @@ -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 diff --git a/code/modules/mob/living/simple_animal/bot/mulebot.dm b/code/modules/mob/living/simple_animal/bot/mulebot.dm index fcfefe5e2cc..a5669bb462b 100644 --- a/code/modules/mob/living/simple_animal/bot/mulebot.dm +++ b/code/modules/mob/living/simple_animal/bot/mulebot.dm @@ -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." diff --git a/code/modules/mob/living/simple_animal/bot/secbot.dm b/code/modules/mob/living/simple_animal/bot/secbot.dm index 2b32f69c135..5545dfcf1af 100644 --- a/code/modules/mob/living/simple_animal/bot/secbot.dm +++ b/code/modules/mob/living/simple_animal/bot/secbot.dm @@ -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 diff --git a/code/modules/mob/living/simple_animal/bot/vibebot.dm b/code/modules/mob/living/simple_animal/bot/vibebot.dm index 2a63c8571b8..5a876bd8eca 100644 --- a/code/modules/mob/living/simple_animal/bot/vibebot.dm +++ b/code/modules/mob/living/simple_animal/bot/vibebot.dm @@ -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 diff --git a/code/modules/mob/living/simple_animal/hostile/gorilla/gorilla.dm b/code/modules/mob/living/simple_animal/hostile/gorilla/gorilla.dm index d6d5a2515ce..fa86cc950e5 100644 --- a/code/modules/mob/living/simple_animal/hostile/gorilla/gorilla.dm +++ b/code/modules/mob/living/simple_animal/hostile/gorilla/gorilla.dm @@ -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 diff --git a/code/modules/mob/living/simple_animal/hostile/regalrat.dm b/code/modules/mob/living/simple_animal/hostile/regalrat.dm index abd6756e666..4e47f29337f 100644 --- a/code/modules/mob/living/simple_animal/hostile/regalrat.dm +++ b/code/modules/mob/living/simple_animal/hostile/regalrat.dm @@ -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]" diff --git a/tgstation.dme b/tgstation.dme index 5dccbd2935f..073f886c756 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -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" diff --git a/tgui/packages/tgui/interfaces/Mule.js b/tgui/packages/tgui/interfaces/Mule.js index c8a906f4a46..fa3e3d2364e 100644 --- a/tgui/packages/tgui/interfaces/Mule.js +++ b/tgui/packages/tgui/interfaces/Mule.js @@ -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 ( - +
diff --git a/tgui/packages/tgui/interfaces/SimpleBot.tsx b/tgui/packages/tgui/interfaces/SimpleBot.tsx index bdc606be73c..6ce81daeab6 100644 --- a/tgui/packages/tgui/interfaces/SimpleBot.tsx +++ b/tgui/packages/tgui/interfaces/SimpleBot.tsx @@ -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(context); - const { can_hack, has_access, locked, pai } = data; - const { allow_pai } = pai; + const { can_hack, has_access, locked } = data; return ( <> {!!can_hack && } - {!!allow_pai && } + - ); - } else { - return ( - - ); - } -}; - /** Displays the bot's standard settings: Power, patrol, etc. */ const SettingsDisplay = (props, context) => { const { act, data } = useBackend(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 ( @@ -187,6 +169,23 @@ const SettingsDisplay = (props, context) => { /> + {allow_possession && ( + + + act('toggle_personality')} + /> + + + )} ); }; diff --git a/tgui/packages/tgui/interfaces/SpawnersMenu.tsx b/tgui/packages/tgui/interfaces/SpawnersMenu.tsx index 00f9dad9a00..daa0370a116 100644 --- a/tgui/packages/tgui/interfaces/SpawnersMenu.tsx +++ b/tgui/packages/tgui/interfaces/SpawnersMenu.tsx @@ -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) => { }> - - {spawner.you_are_text || 'Unknown'} - - - {spawner.flavor_text || 'None'} - - - {spawner.important_text || 'None'} - + {spawner.desc ? ( + + {spawner.desc} + + ) : ( +
+ + {spawner.you_are_text || 'Unknown'} + + + {spawner.flavor_text || 'None'} + + + {spawner.important_text || 'None'} + +
+ )}