adds the proteon orb. (heretic requirement) (#27705)

* adds the proteon orb.

* oops

* Update code/datums/components/ghost_direct_control.dm

Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com>
Signed-off-by: Qwertytoforty <52090703+Qwertytoforty@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Charlie Nolan <funnyman3595@gmail.com>
Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com>
Signed-off-by: Qwertytoforty <52090703+Qwertytoforty@users.noreply.github.com>

* revert that

* Update code/game/objects/structures/monster_spawner.dm

Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com>
Signed-off-by: Qwertytoforty <52090703+Qwertytoforty@users.noreply.github.com>

* lewcs stuff

* Update code/game/objects/structures/monster_spawner.dm

Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com>
Signed-off-by: Qwertytoforty <52090703+Qwertytoforty@users.noreply.github.com>

* Update monster_spawner.dm

---------

Signed-off-by: Qwertytoforty <52090703+Qwertytoforty@users.noreply.github.com>
Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com>
Co-authored-by: Charlie Nolan <funnyman3595@gmail.com>
Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com>
This commit is contained in:
Qwertytoforty
2025-01-10 14:11:53 -05:00
committed by GitHub
parent 678bb87de3
commit 60c00ce0d5
14 changed files with 363 additions and 8 deletions
+3 -3
View File
@@ -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)
@@ -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 += "<span class='boldnotice'>You could take control of this mob by clicking on it.</span>"
/// 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, "<span class='warning'>Ghost candidate selection currently in progress!</span>")
return COMPONENT_CANCEL_ATTACK_CHAIN
if(!SSticker.HasRoundStarted())
to_chat(hopeful_ghost, "<span class='warning'>You cannot assume control of this until after the round has started!</span>")
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 class='warning'>Offer to possess creature has expired!</span>")
return
if(jobban_isbanned(harbinger, ban_type) || jobban_isbanned(harbinger, ROLE_SENTIENT) || (is_antag_spawner && jobban_isbanned(harbinger, ROLE_SYNDICATE)))
to_chat(harbinger, "<span class='warning'>You are banned from playing as this role!</span>")
return
var/mob/living/new_body = parent
if(new_body.stat == DEAD)
to_chat(harbinger, "<span class='warning'>This body has passed away, it is of no use!</span>")
return
if(new_body.key)
to_chat(harbinger, "<span class='warning'>[parent] has already become sapient!</span>")
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, "<span class='boldnotice'>[assumed_control_message]</span>")
after_assumed_control?.Invoke(harbinger)
qdel(src)
+1
View File
@@ -51,6 +51,7 @@
L.nest = src
L.faction = src.faction
P.visible_message("<span class='danger'>[L] [spawn_text] [P].</span>")
P.on_mob_spawn(L)
/datum/component/spawner/proc/rally_spawned_mobs(parent, mob/living/target)
SIGNAL_HANDLER // COMSIG_SPAWNER_SET_TARGET
+4
View File
@@ -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
+8
View File
@@ -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")
+85
View File
@@ -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)
. += "<span class='danger'>It hurts just to look at it. Better keep away.</span>"
else
. += "<span class='cult'>It can be used to create a gateway to Nar'Sie's domain, which will summon weak, sentient constructs over time.</span>"
/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, "<span class='cultlarge'>\"You want to enter my domain? Go ahead.\"</span>")
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, "<span class='cult'><b>There's a gateway too close nearby. The veil is not yet weak enough to allow such close rips in its fabric.</b></span>")
return
to_chat(user, "<span class='cultitalic'><b>You focus on [src] and direct it into the ground. It rumbles...</b></span>")
var/turf/hole_spot = get_turf(user)
if(!isfloorturf(hole_spot))
to_chat(user, "<span class='notice'>This is not a suitable spot.</span>")
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("<span class='cult'><b>Dark tendrils appear from the ground and root [user] in place!</b></span>")
sleep(5 SECONDS) // can we still use these or. i mean its async
new /obj/structure/spawner/sentient/proteon_spawner(src)
visible_message("<span class='cult'><b>A mysterious hole appears out of nowhere!</b></span>")
if(!fucked || QDELETED(user))
return
if(get_turf(user) != src) // they get away. for now
user.notransform = FALSE
user.visible_message("<span class='cult'><b>[user] is pulled into the portal through an infinitesmally minuscule hole, shredding [user.p_their()] body!</b></span>")
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("<span class='cultitalic'>An unusually large construct appears through the portal!</span>")
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
@@ -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 "<span class='cult'>It's at <b>[round(obj_integrity * 100 / max_integrity)]%</b> stability.</span>"
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)
. += "<span class='userdanger'>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...</span>"
else
. += "<span class='cult'>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.</span>"
/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("<span class='cult'>[proteon] awakens, glowing an eerie red as it stirs from its stupor!</span>")
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("<span class='cult'><b>[src] completely falls apart, the screams of the damned reaching a feverous pitch before slowly fading away into nothing.</b></span>")
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!"
@@ -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 = "<b>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.</b>"
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
@@ -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)
@@ -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)
+1
View File
@@ -81,3 +81,4 @@
update_client_colour(0)
update_morgue()
client.init_verbs()
SEND_SIGNAL(src, COMSIG_MOB_LOGIN)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 45 KiB

+1
View File
@@ -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"