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'} + +
+ )}