diff --git a/code/controllers/subsystem/SSghost_spawns.dm b/code/controllers/subsystem/SSghost_spawns.dm index 9815d413f56..ad1690b7610 100644 --- a/code/controllers/subsystem/SSghost_spawns.dm +++ b/code/controllers/subsystem/SSghost_spawns.dm @@ -43,7 +43,7 @@ SUBSYSTEM_DEF(ghost_spawns) * * source - The atom, atom prototype, icon or mutable appearance to display as an icon in the alert * * role_cleanname - The name override to display to clients */ -/datum/controller/subsystem/ghost_spawns/proc/poll_candidates(question = "Would you like to play a special role?", role, antag_age_check = FALSE, poll_time = 30 SECONDS, ignore_respawnability = FALSE, min_hours = 0, flash_window = TRUE, check_antaghud = TRUE, source, role_cleanname, reason) +/datum/controller/subsystem/ghost_spawns/proc/poll_candidates(question = "Would you like to play a special role?", role, antag_age_check = FALSE, poll_time = 30 SECONDS, ignore_respawnability = FALSE, min_hours = 0, flash_window = TRUE, check_antaghud = TRUE, source, role_cleanname, reason, dont_play_notice_sound = FALSE) log_debug("Polling candidates [role ? "for [role_cleanname || get_roletext(role)]" : "\"[question]\""] for [poll_time / 10] seconds") // Start firing @@ -63,8 +63,8 @@ SUBSYSTEM_DEF(ghost_spawns) for(var/mob/M in (GLOB.player_list)) if(!is_eligible(M, role, antag_age_check, role, min_hours, check_antaghud)) continue - - SEND_SOUND(M, notice_sound) + if(!dont_play_notice_sound) + SEND_SOUND(M, notice_sound) if(flash_window) window_flash(M.client) diff --git a/code/datums/components/ghost_direct_control.dm b/code/datums/components/ghost_direct_control.dm new file mode 100644 index 00000000000..06eaa3e018c --- /dev/null +++ b/code/datums/components/ghost_direct_control.dm @@ -0,0 +1,147 @@ +/** + * 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 + /// Is this an antagonist spawner, so we check ROLE_SYNDICATE + var/is_antag_spawner + +/datum/component/ghost_direct_control/Initialize( + ban_type = ROLE_SENTIENT, + role_name = null, + poll_question = null, + poll_candidates = TRUE, + poll_length = 10 SECONDS, + assumed_control_message = null, + datum/callback/extra_control_checks, + datum/callback/after_assumed_control, + is_antag_spawner = TRUE, + ) + . = ..() + 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 + src.is_antag_spawner = is_antag_spawner + + + if(poll_candidates) + INVOKE_ASYNC(src, PROC_REF(request_ghost_control), poll_question, role_name || "[parent]", poll_length) + +/datum/component/ghost_direct_control/RegisterWithParent() + . = ..() + RegisterSignal(parent, COMSIG_ATOM_ATTACK_GHOST, PROC_REF(on_ghost_clicked)) + RegisterSignal(parent, COMSIG_PARENT_EXAMINE, PROC_REF(on_examined)) + RegisterSignal(parent, COMSIG_MOB_LOGIN, PROC_REF(on_login)) + +/datum/component/ghost_direct_control/UnregisterFromParent() + UnregisterSignal(parent, list(COMSIG_ATOM_ATTACK_GHOST, COMSIG_PARENT_EXAMINE, COMSIG_MOB_LOGIN)) + return ..() + +/datum/component/ghost_direct_control/Destroy(force) + extra_control_checks = null + after_assumed_control = null + 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 //COMSIG_PARENT_EXAMINE + if(!isobserver(user)) + return + var/mob/living/our_mob = parent + if(our_mob.stat == DEAD || our_mob.key || awaiting_ghosts) + return + examine_text += "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(poll_question, role_name, poll_length) + awaiting_ghosts = TRUE + var/list/candidates = SSghost_spawns.poll_candidates( + question = poll_question, + role = ban_type, + poll_time = poll_length, + source = parent, + role_cleanname = role_name, + flash_window = FALSE, + dont_play_notice_sound = TRUE + ) + awaiting_ghosts = FALSE + var/mob/chosen_one = null + if(length(candidates)) + chosen_one = pick(candidates) + + if(isnull(chosen_one)) + return + assume_direct_control(chosen_one) + +/// 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 // COMSIG_ATOM_ATTACK_GHOST + if(our_mob.key) + qdel(src) + return COMPONENT_CANCEL_ATTACK_CHAIN + if(!hopeful_ghost.client) + return COMPONENT_CANCEL_ATTACK_CHAIN + if(awaiting_ghosts) + to_chat(hopeful_ghost, "Ghost candidate selection currently in progress!") + return COMPONENT_CANCEL_ATTACK_CHAIN + if(!SSticker.HasRoundStarted()) + to_chat(hopeful_ghost, "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, "Offer to possess creature has expired!") + return + if(jobban_isbanned(harbinger, ban_type) || jobban_isbanned(harbinger, ROLE_SENTIENT) || (is_antag_spawner && jobban_isbanned(harbinger, ROLE_SYNDICATE))) + to_chat(harbinger, "You are banned from playing as this role!") + return + var/mob/living/new_body = parent + if(new_body.stat == DEAD) + to_chat(harbinger, "This body has passed away, it is of no use!") + return + if(new_body.key) + to_chat(harbinger, "[parent] has already become sapient!") + qdel(src) + return + if(extra_control_checks && !extra_control_checks.Invoke(harbinger)) + return + + // doesn't transfer mind because that transfers antag datum as well + new_body.key = harbinger.key + + // Already qdels due to below proc but just in case + if(!QDELETED(src)) + qdel(src) + +/// When someone assumes control, get rid of our component +/datum/component/ghost_direct_control/proc/on_login(mob/harbinger) + SIGNAL_HANDLER //COMSIG_MOB_LOGIN + // This proc is called the very moment .key is set, so we need to force mind to initialize here if we want the invoke to affect the mind of the mob + if(isnull(harbinger.mind)) + harbinger.mind_initialize() + to_chat(harbinger, "[assumed_control_message]") + after_assumed_control?.Invoke(harbinger) + qdel(src) diff --git a/code/datums/components/spawner.dm b/code/datums/components/spawner.dm index b1d6e867975..eb845a9aba6 100644 --- a/code/datums/components/spawner.dm +++ b/code/datums/components/spawner.dm @@ -51,6 +51,7 @@ L.nest = src L.faction = src.faction P.visible_message("[L] [spawn_text] [P].") + P.on_mob_spawn(L) /datum/component/spawner/proc/rally_spawned_mobs(parent, mob/living/target) SIGNAL_HANDLER // COMSIG_SPAWNER_SET_TARGET diff --git a/code/game/atoms.dm b/code/game/atoms.dm index afc7519b673..2222ad926dd 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -1438,3 +1438,7 @@ GLOBAL_LIST_EMPTY(blood_splatter_icons) */ /atom/proc/relaydrive(mob/living/user, direction) return !(SEND_SIGNAL(src, COMSIG_RIDDEN_DRIVER_MOVE, user, direction) & COMPONENT_DRIVER_BLOCK_MOVE) + +/// Used with the spawner component to do something when a mob is spawned. +/atom/proc/on_mob_spawn(mob/created_mob) + return diff --git a/code/game/gamemodes/cult/cult_datums.dm b/code/game/gamemodes/cult/cult_datums.dm index 7e0ec869737..827c962e147 100644 --- a/code/game/gamemodes/cult/cult_datums.dm +++ b/code/game/gamemodes/cult/cult_datums.dm @@ -42,6 +42,10 @@ var/harvester_icon_state = "harvester" var/harvester_dead_state = "shade_dead" + //Proteon Construct + var/proteon_name = "Proteon" + var/proteon_icon_state = "proteon" + var/proteon_dead_state = "shade_dead" //Shade Spirit var/shade_name = "Shade" var/shade_icon_state = "shade2" @@ -211,6 +215,8 @@ return harvester_name if("wraith") return wraith_name + if("proteon") + return proteon_name if("shade") return shade_name @@ -230,6 +236,8 @@ return harvester_icon_state if("wraith") return wraith_icon_state + if("proteon") + return proteon_icon_state if("shade") return shade_icon_state if("forge") diff --git a/code/game/gamemodes/cult/cult_items.dm b/code/game/gamemodes/cult/cult_items.dm index a6b2e8ae567..9e36aeaf9f5 100644 --- a/code/game/gamemodes/cult/cult_items.dm +++ b/code/game/gamemodes/cult/cult_items.dm @@ -829,3 +829,88 @@ icon = 'icons/obj/biomass.dmi' icon_state = "rift" color = "red" + +#define GATEWAY_TURF_SCAN_RANGE 40 +GLOBAL_LIST_EMPTY(proteon_portals) + +/obj/item/proteon_orb + name = "summoning orb" + desc = "An eerie translucent orb that feels impossibly light. Legends say summoning orbs are created from corrupted scrying orbs. If you hold it close to your ears, you can hear the screams of the damned." + icon = 'icons/obj/wizard.dmi' + icon_state ="scrying_orb" + light_range = 3 + light_color = LIGHT_COLOR_RED + new_attack_chain = TRUE + /// A nice blood colour matrix + var/list/blood_color_matrix = list(1.25,-0.1,-0.1,0, 0,0.15,0,0, 0,0,0.15,0, 0,0,0,1, 0,0,0,0) + + +/obj/item/proteon_orb/Initialize(mapload) + . = ..() + color = blood_color_matrix + + +/obj/item/proteon_orb/examine(mob/user) + . = ..() + if(!IS_CULTIST(user) && isliving(user)) + var/mob/living/living_user = user + living_user.adjustBrainLoss(5) + . += "It hurts just to look at it. Better keep away." + else + . += "It can be used to create a gateway to Nar'Sie's domain, which will summon weak, sentient constructs over time." + +/obj/item/proteon_orb/activate_self(mob/user) + if(..()) + return + + var/list/portals_to_scan = GLOB.proteon_portals + + if(!IS_CULTIST(user)) + to_chat(user, "\"You want to enter my domain? Go ahead.\"") + portals_to_scan = null // narsie wants to have some fun and the veil wont stop her + + for(var/obj/structure/spawner/sentient/proteon_spawner/P as anything in portals_to_scan) + if(get_dist(P, src) <= 40) + to_chat(user, "There's a gateway too close nearby. The veil is not yet weak enough to allow such close rips in its fabric.") + return + to_chat(user, "You focus on [src] and direct it into the ground. It rumbles...") + + var/turf/hole_spot = get_turf(user) + if(!isfloorturf(hole_spot)) + to_chat(user, "This is not a suitable spot.") + return + + INVOKE_ASYNC(hole_spot, TYPE_PROC_REF(/turf/simulated/floor, quake_gateway), user) + qdel(src) + +/** + * Bespoke proc that happens when a proteon orb is activated, creating a gateway. + * If activated by a non-cultist, they get an unusual game over. +*/ +/turf/simulated/floor/proc/quake_gateway(mob/living/user) + ChangeTurf(/turf/simulated/floor/engine/cult) + Shake(4, 4, 5 SECONDS) + var/fucked = FALSE + if(!IS_CULTIST(user)) + fucked = TRUE + user.notransform = TRUE + user.add_atom_colour(LIGHT_COLOR_RED, TEMPORARY_COLOUR_PRIORITY) + user.visible_message("Dark tendrils appear from the ground and root [user] in place!") + sleep(5 SECONDS) // can we still use these or. i mean its async + new /obj/structure/spawner/sentient/proteon_spawner(src) + visible_message("A mysterious hole appears out of nowhere!") + if(!fucked || QDELETED(user)) + return + if(get_turf(user) != src) // they get away. for now + user.notransform = FALSE + user.visible_message("[user] is pulled into the portal through an infinitesmally minuscule hole, shredding [user.p_their()] body!") + add_attack_logs(user, user, "Killed themselfs via use of a proteon orb as a non cultist", ATKLOG_ALL) + user.gib() // total destruction + sleep(5 SECONDS) + user.visible_message("An unusually large construct appears through the portal!") + var/mob/living/simple_animal/hostile/construct/proteon/hostile/remnant = new(get_step_rand(src)) + remnant.name = "[user]" // no, they do not become it + remnant.appearance_flags += PIXEL_SCALE + remnant.transform *= 1.5 + +#undef GATEWAY_TURF_SCAN_RANGE diff --git a/code/game/objects/structures/monster_spawner.dm b/code/game/objects/structures/monster_spawner.dm index 2de2429d603..67b5d6694c1 100644 --- a/code/game/objects/structures/monster_spawner.dm +++ b/code/game/objects/structures/monster_spawner.dm @@ -24,6 +24,7 @@ return ..() + /obj/structure/spawner/syndicate name = "warp beacon" icon = 'icons/obj/device.dmi' @@ -74,3 +75,87 @@ name = "basilisk den" desc = "A den housing a nest of basilisks, bring a coat." mob_types = list(/mob/living/simple_animal/hostile/asteroid/basilisk) + +/obj/structure/spawner/sentient + var/role_name = "A sentient mob" + var/assumed_control_message = "You are a sentient mob from a badly coded spawner" + +/obj/structure/spawner/sentient/Initialize(mapload) + . = ..() + notify_ghosts("\A [name] has been created in \the [get_area(src)]!", source = src, title = "Sentient Spawner Created", flashwindow = FALSE + ) + +/obj/structure/spawner/sentient/on_mob_spawn(mob/created_mob) + created_mob.AddComponent(\ + /datum/component/ghost_direct_control,\ + role_name = src.role_name,\ + assumed_control_message = src.assumed_control_message,\ + after_assumed_control = CALLBACK(src, PROC_REF(became_player_controlled)),\ + ) + +/obj/structure/spawner/sentient/proc/became_player_controlled(mob/proteon) + return + +/obj/structure/spawner/sentient/proteon_spawner + name = "eldritch gateway" + desc = "A dizzying structure that somehow links into Nar'Sie's own domain. The screams of the damned echo continously." + icon = 'icons/obj/cult.dmi' + icon_state = "hole" + light_power = 2 + light_color = LIGHT_COLOR_RED + max_integrity = 50 + density = FALSE + max_mobs = 2 + spawn_time = 15 SECONDS + mob_types = list(/mob/living/simple_animal/hostile/construct/proteon/hostile) + spawn_text = "arises from" + faction = list("cult") + role_name = "A proteon cult construct" + assumed_control_message = null + /// The GPS inside the spawner, lets security find them, especially if someone is space basing them. + var/obj/item/gps/internal/proteon/gps + +/obj/structure/spawner/sentient/proteon_spawner/Initialize(mapload) + . = ..() + GLOB.proteon_portals += src + gps = new /obj/item/gps/internal/proteon(src) + +/obj/structure/spawner/sentient/proteon_spawner/Destroy() + GLOB.proteon_portals -= src + QDEL_NULL(gps) + return ..() + +/obj/structure/spawner/sentient/proteon_spawner/examine_status(mob/user) + if(IS_CULTIST(user) || !isliving(user)) + return "It's at [round(obj_integrity * 100 / max_integrity)]% stability." + return ..() + +/obj/structure/spawner/sentient/proteon_spawner/examine(mob/user) + . = ..() + if(!IS_CULTIST(user) && isliving(user)) + var/mob/living/living_user = user + living_user.adjustBrainLoss(5) + . += "The voices of the damned echo relentlessly in your mind, continously rebounding on the walls of your self the more you focus on [src]. Your head pounds, better keep away..." + else + . += "The gateway will create one weak proteon construct every [spawn_time * 0.1] seconds, up to a total of [max_mobs], that may be controlled by the spirits of the dead." + +/obj/structure/spawner/sentient/proteon_spawner/became_player_controlled(mob/living/simple_animal/hostile/construct/proteon/hostile/proteon) + proteon.mind.add_antag_datum(/datum/antagonist/cultist) + proteon.add_filter("awoken_proteon", 3, list("type" = "outline", "color" = LIGHT_COLOR_RED, "size" = 2)) + visible_message("[proteon] awakens, glowing an eerie red as it stirs from its stupor!") + addtimer(CALLBACK(src, PROC_REF(remove_wake_outline), proteon), 8 SECONDS) + +/obj/structure/spawner/sentient/proteon_spawner/proc/remove_wake_outline(mob/proteon) + proteon.remove_filter("awoken_proteon") + proteon.add_filter("sentient_proteon", 3, list("type" = "outline", "color" = LIGHT_COLOR_RED, "size" = 2, "alpha" = 40)) + +/obj/structure/spawner/sentient/proteon_spawner/obj_destruction(damage_flag) + playsound(src, 'sound/hallucinations/veryfar_noise.ogg', 75) + visible_message("[src] completely falls apart, the screams of the damned reaching a feverous pitch before slowly fading away into nothing.") + return ..() + +/obj/item/gps/internal/proteon + local = FALSE // Let security find the cult bases in space with it. + icon_state = null + gpstag = "Eldritch Signal" + desc = "You (can)'t just BSA a hole into hell!" diff --git a/code/modules/mob/living/simple_animal/constructs.dm b/code/modules/mob/living/simple_animal/constructs.dm index dbb791303b8..dbe50369e6f 100644 --- a/code/modules/mob/living/simple_animal/constructs.dm +++ b/code/modules/mob/living/simple_animal/constructs.dm @@ -327,6 +327,31 @@ AIStatus = AI_ON environment_smash = 1 //only token destruction, don't smash the cult wall NO STOP +/mob/living/simple_animal/hostile/construct/proteon + name = "Proteon" + real_name = "Proteon" + desc = "A weaker construct meant to scour ruins for objects of Nar'Sie's affection. Those barbed claws are no joke." + icon_state = "proteon" + icon_living = "proteon" + maxHealth = 35 + health = 35 + melee_damage_lower = 8 + melee_damage_upper = 10 + obj_damage = 20 // Bit better at breaking stuff than cult ghosts, but only *barely* + retreat_distance = 4 // AI proteons will rapidly move in and out of combat to avoid conflict, but will still target and follow you. + attacktext = "pinches" + environment_smash = ENVIRONMENT_SMASH_STRUCTURES // No you can not break down all the walls of the station + attack_sound = 'sound/weapons/punch2.ogg' + playstyle_string = "You are a Proteon. Your abilities in combat are outmatched by most combat constructs, but you are still fast and nimble. Run metal and supplies, and cooperate with your fellow cultists." + construct_type = "proteon" + +/mob/living/simple_animal/hostile/construct/proteon/Initialize(mapload) + . = ..() + add_overlay("glow_proteon_cult") + +// This is the type you will run into, spawned by the proteon spawner +/mob/living/simple_animal/hostile/construct/proteon/hostile + AIStatus = AI_ON /mob/living/simple_animal/hostile/construct/proc/make_holy() if(holy) // Already holy-fied diff --git a/code/modules/mob/living/simple_animal/hostile/terror_spiders/prince.dm b/code/modules/mob/living/simple_animal/hostile/terror_spiders/prince.dm index cd32ea73e96..fdd2f2116de 100644 --- a/code/modules/mob/living/simple_animal/hostile/terror_spiders/prince.dm +++ b/code/modules/mob/living/simple_animal/hostile/terror_spiders/prince.dm @@ -52,7 +52,9 @@ RegisterSignal(src, COMSIG_MOB_LOGIN, TYPE_PROC_REF(/mob/living/simple_animal/hostile/poison/terror_spider/prince, give_spell)) /mob/living/simple_animal/hostile/poison/terror_spider/prince/proc/give_spell() - SIGNAL_HANDLER + SIGNAL_HANDLER //COMSIG_MOB_LOGIN + if(isnull(mind)) + mind_initialize() var/datum/spell/spell = new /datum/spell/princely_charge() mind.AddSpell(spell) UnregisterSignal(src, COMSIG_MOB_LOGIN) diff --git a/code/modules/mob/living/simple_animal/hostile/terror_spiders/terror_spiders.dm b/code/modules/mob/living/simple_animal/hostile/terror_spiders/terror_spiders.dm index 35a3ad73b20..5ec143862fe 100644 --- a/code/modules/mob/living/simple_animal/hostile/terror_spiders/terror_spiders.dm +++ b/code/modules/mob/living/simple_animal/hostile/terror_spiders/terror_spiders.dm @@ -475,7 +475,3 @@ GLOBAL_LIST_EMPTY(ts_infected_list) . = ..() if(pulling && !ismob(pulling) && pulling.density) . += 6 // Drastic move speed penalty for dragging anything that is not a mob or a non dense object - -/mob/living/simple_animal/hostile/poison/terror_spider/Login() - . = ..() - SEND_SIGNAL(src, COMSIG_MOB_LOGIN) diff --git a/code/modules/mob/mob_login_base.dm b/code/modules/mob/mob_login_base.dm index 238057a92cd..326108cf97b 100644 --- a/code/modules/mob/mob_login_base.dm +++ b/code/modules/mob/mob_login_base.dm @@ -81,3 +81,4 @@ update_client_colour(0) update_morgue() client.init_verbs() + SEND_SIGNAL(src, COMSIG_MOB_LOGIN) diff --git a/icons/mob/cult.dmi b/icons/mob/cult.dmi index b4db710526e..da3566c7f74 100644 Binary files a/icons/mob/cult.dmi and b/icons/mob/cult.dmi differ diff --git a/icons/obj/cult.dmi b/icons/obj/cult.dmi index 69284a978e9..2a2fabd845b 100644 Binary files a/icons/obj/cult.dmi and b/icons/obj/cult.dmi differ diff --git a/paradise.dme b/paradise.dme index b3304b92956..85eadb7a7e3 100644 --- a/paradise.dme +++ b/paradise.dme @@ -448,6 +448,7 @@ #include "code\datums\components\emissive_blocker.dm" #include "code\datums\components\footstep.dm" #include "code\datums\components\fullauto.dm" +#include "code\datums\components\ghost_direct_control.dm" #include "code\datums\components\label.dm" #include "code\datums\components\largeobjecttransparency.dm" #include "code\datums\components\material_container.dm"