mirror of
https://github.com/ParadiseSS13/Paradise.git
synced 2025-12-19 14:51:27 +00:00
325 lines
12 KiB
Plaintext
325 lines
12 KiB
Plaintext
SUBSYSTEM_DEF(ghost_spawns)
|
|
name = "Ghost Spawns"
|
|
flags = SS_BACKGROUND | SS_NO_INIT
|
|
wait = 1 SECONDS
|
|
runlevels = RUNLEVEL_GAME
|
|
offline_implications = "Ghosts will no longer be able to respawn as event mobs (Blob, etc..). Shuttle call recommended."
|
|
cpu_display = SS_CPUDISPLAY_LOW
|
|
|
|
/// List of polls currently ongoing, to be checked on next fire()
|
|
var/list/datum/candidate_poll/currently_polling
|
|
/// Whether there are active polls or not
|
|
var/polls_active = FALSE
|
|
/// Number of polls performed since the start
|
|
var/total_polls = 0
|
|
/// The poll that's closest to finishing
|
|
var/datum/candidate_poll/next_poll_to_finish
|
|
|
|
/datum/controller/subsystem/ghost_spawns/fire()
|
|
if(!polls_active)
|
|
return
|
|
if(!currently_polling) // if polls_active is TRUE then this shouldn't happen, but still..
|
|
currently_polling = list()
|
|
|
|
for(var/poll in currently_polling)
|
|
var/datum/candidate_poll/P = poll
|
|
if(P.time_left() <= 0)
|
|
polling_finished(P)
|
|
|
|
/**
|
|
* Polls for candidates with a question and a preview of the role
|
|
*
|
|
* This proc replaces /proc/pollCandidates.
|
|
* Should NEVER be used in a proc that has waitfor set to FALSE/0 (due to #define UNTIL)
|
|
* Arguments:
|
|
* * question - The question to ask to potential candidates
|
|
* * role - The role to poll for. Should be a ROLE_x enum. If set, potential candidates who aren't eligible will be ignored
|
|
* * antag_age_check - Whether to filter out potential candidates who don't have an old enough account
|
|
* * poll_time - How long to poll for in deciseconds
|
|
* * ignore_respawnability - Whether to ignore the player's respawnability
|
|
* * min_hours - The amount of hours needed for a potential candidate to be eligible
|
|
* * flash_window - Whether the poll should flash a potential candidate's game window
|
|
* * check_antaghud - Whether to filter out potential candidates who enabled AntagHUD
|
|
* * 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)
|
|
log_debug("Polling candidates [role ? "for [role_cleanname || get_roletext(role)]" : "\"[question]\""] for [poll_time / 10] seconds")
|
|
|
|
// Start firing
|
|
polls_active = TRUE
|
|
total_polls++
|
|
|
|
var/datum/candidate_poll/P = new(role, question, poll_time)
|
|
LAZYADD(currently_polling, P)
|
|
|
|
// We're the poll closest to completion
|
|
if(!next_poll_to_finish || poll_time < next_poll_to_finish.time_left())
|
|
next_poll_to_finish = P
|
|
|
|
var/category = "[P.hash]_notify_action"
|
|
|
|
var/notice_sound = sound('sound/effects/ghost_ping.ogg')
|
|
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(flash_window)
|
|
window_flash(M.client)
|
|
|
|
// If we somehow send two polls for the same mob type, but with a duration on the second one shorter than the time left on the first one,
|
|
// we need to keep the first one's timeout rather than use the shorter one
|
|
var/obj/screen/alert/notify_action/current_alert = LAZYACCESS(M.alerts, category)
|
|
var/alert_time = poll_time
|
|
var/alert_poll = P
|
|
if(current_alert && current_alert.timeout > (world.time + poll_time - world.tick_lag))
|
|
alert_time = current_alert.timeout - world.time + world.tick_lag
|
|
alert_poll = current_alert.poll
|
|
|
|
// Send them an on-screen alert
|
|
var/obj/screen/alert/notify_action/A = M.throw_alert(category, /obj/screen/alert/notify_action, timeout_override = alert_time, no_anim = TRUE)
|
|
if(!A)
|
|
continue
|
|
|
|
A.icon = ui_style2icon(M.client?.prefs.UI_style)
|
|
A.name = "Looking for candidates"
|
|
A.desc = "[question]\n\n(expires in [poll_time / 10] seconds)"
|
|
A.show_time_left = TRUE
|
|
A.poll = alert_poll
|
|
|
|
// Sign up inheritance and stacking
|
|
var/inherited_sign_up = FALSE
|
|
var/num_stack = 1
|
|
for(var/existing_poll in currently_polling)
|
|
var/datum/candidate_poll/P2 = existing_poll
|
|
if(P != P2 && P.hash == P2.hash)
|
|
// If there's already a poll for an identical mob type ongoing and the client is signed up for it, sign them up for this one
|
|
if(!inherited_sign_up && (M in P2.signed_up) && P.sign_up(M, TRUE))
|
|
A.update_signed_up_alert(M)
|
|
inherited_sign_up = TRUE
|
|
// This number is used to display the number of polls the alert regroups
|
|
num_stack++
|
|
if(num_stack > 1)
|
|
A.display_stacks(num_stack)
|
|
|
|
// Image to display
|
|
var/image/I
|
|
if(source)
|
|
if(!ispath(source))
|
|
var/atom/S = source
|
|
var/old_layer = S.layer
|
|
var/old_plane = S.plane
|
|
|
|
S.layer = FLOAT_LAYER
|
|
S.plane = FLOAT_PLANE
|
|
A.overlays += S
|
|
S.layer = old_layer
|
|
S.plane = old_plane
|
|
else
|
|
I = image(source, layer = FLOAT_LAYER, dir = SOUTH)
|
|
else
|
|
// Just use a generic image
|
|
I = image('icons/effects/effects.dmi', icon_state = "static", layer = FLOAT_LAYER, dir = SOUTH)
|
|
|
|
if(I)
|
|
I.layer = FLOAT_LAYER
|
|
I.plane = FLOAT_PLANE
|
|
A.overlays += I
|
|
|
|
// Chat message
|
|
var/act_jump = ""
|
|
if(isatom(source))
|
|
act_jump = "<a href='?src=[M.UID()];jump=\ref[source]'>\[Teleport]</a>"
|
|
var/act_signup = "<a href='?src=[A.UID()];signup=1'>\[Sign Up]</a>"
|
|
to_chat(M, "<big><span class='boldnotice'>Now looking for candidates [role ? "to play as \an [role_cleanname || get_roletext(role)]" : "\"[question]\""]. [act_jump] [act_signup] [reason ? "<i>\nReason: [sanitize(reason)]</i>" : ""]</span></big>")
|
|
|
|
// Start processing it so it updates visually the timer
|
|
START_PROCESSING(SSprocessing, A)
|
|
A.process()
|
|
|
|
// Sleep until the time is up
|
|
UNTIL(P.finished)
|
|
if(!ignore_respawnability)
|
|
var/list/eligable_mobs = list()
|
|
for(var/mob/signed_up in P.signed_up)
|
|
if(HAS_TRAIT(signed_up, TRAIT_RESPAWNABLE))
|
|
eligable_mobs += signed_up
|
|
return eligable_mobs
|
|
else
|
|
return P.signed_up
|
|
|
|
/**
|
|
* Returns whether an observer is eligible to be an event mob
|
|
*
|
|
* Arguments:
|
|
* * M - The mob to check eligibility
|
|
* * role - The role to check eligibility for. Checks 1. the client has enabled the role 2. the account's age for this role if antag_age_check is TRUE
|
|
* * antag_age_check - Whether to check the account's age or not for the given role.
|
|
* * role_text - The role's clean text. Used for checking job bans to determine eligibility
|
|
* * min_hours - The amount of minimum hours the client needs before being eligible
|
|
* * check_antaghud - Whether to consider a client who enabled AntagHUD ineligible or not
|
|
*/
|
|
/datum/controller/subsystem/ghost_spawns/proc/is_eligible(mob/M, role, antag_age_check, role_text, min_hours, check_antaghud, ignore_respawnability)
|
|
. = FALSE
|
|
if(!M.key || !M.client)
|
|
return
|
|
if(!ignore_respawnability && !HAS_TRAIT(M, TRAIT_RESPAWNABLE))
|
|
return
|
|
if(role)
|
|
if(!(role in M.client.prefs.be_special))
|
|
return
|
|
if(antag_age_check)
|
|
if(!player_old_enough_antag(M.client, role))
|
|
return
|
|
if(role_text)
|
|
if(jobban_isbanned(M, role_text) || jobban_isbanned(M, ROLE_SYNDICATE))
|
|
return
|
|
if(GLOB.configuration.jobs.enable_exp_restrictions && min_hours)
|
|
if(M.client.get_exp_type_num(EXP_TYPE_LIVING) < min_hours * 60)
|
|
return
|
|
if(check_antaghud && isobserver(M))
|
|
var/mob/dead/observer/O = M
|
|
if(!O.check_ahud_rejoin_eligibility())
|
|
return
|
|
|
|
return TRUE
|
|
|
|
/**
|
|
* Called by the subsystem when a poll's timer runs out
|
|
*
|
|
* Can be called manually to finish a poll prematurely
|
|
* Arguments:
|
|
* * P - The poll to finish
|
|
*/
|
|
/datum/controller/subsystem/ghost_spawns/proc/polling_finished(datum/candidate_poll/P)
|
|
// Trim players who aren't eligible anymore
|
|
var/len_pre_trim = length(P.signed_up)
|
|
P.trim_candidates()
|
|
log_debug("Candidate poll [P.role ? "for [get_roletext(P.role)]" : "\"[P.question]\""] finished. [len_pre_trim] players signed up, [length(P.signed_up)] after trimming")
|
|
|
|
P.finished = TRUE
|
|
currently_polling -= P
|
|
|
|
// Determine which is the next poll closest the completion or "disable" firing if there's none
|
|
if(!length(currently_polling))
|
|
polls_active = FALSE
|
|
next_poll_to_finish = null
|
|
else if(P == next_poll_to_finish)
|
|
next_poll_to_finish = null
|
|
for(var/poll in currently_polling)
|
|
var/datum/candidate_poll/P2 = poll
|
|
if(!next_poll_to_finish || P2.time_left() < next_poll_to_finish.time_left())
|
|
next_poll_to_finish = P2
|
|
|
|
/datum/controller/subsystem/ghost_spawns/get_stat_details()
|
|
var/list/msg = list()
|
|
msg += "Active: [length(currently_polling)] | Total: [total_polls]"
|
|
if(next_poll_to_finish)
|
|
msg += " | Next: [DisplayTimeText(next_poll_to_finish.time_left())] ([length(next_poll_to_finish.signed_up)] candidates)"
|
|
return msg.Join("")
|
|
|
|
// The datum that describes one instance of candidate polling
|
|
/datum/candidate_poll
|
|
var/role // The role the poll is for
|
|
var/question // The question asked to observers
|
|
var/duration // The duration of the poll
|
|
var/list/signed_up // The players who signed up to this poll
|
|
var/time_started // The world.time at which the poll was created
|
|
var/finished = FALSE // Whether the polling is finished
|
|
var/hash // Used to categorize in the alerts system
|
|
|
|
/datum/candidate_poll/New(polled_role, polled_question, poll_duration)
|
|
role = polled_role
|
|
question = polled_question
|
|
duration = poll_duration
|
|
signed_up = list()
|
|
time_started = world.time
|
|
hash = copytext(md5("[question]_[role ? role : "0"]"), 1, 7)
|
|
return ..()
|
|
|
|
/**
|
|
* Attempts to sign a (controlled) mob up
|
|
*
|
|
* Will fail if the mob is already signed up or the poll's timer ran out.
|
|
* Does not check for eligibility
|
|
* Arguments:
|
|
* * M - The (controlled) mob to sign up
|
|
* * silent - Whether no messages should appear or not. If not TRUE, signing up to this poll will also sign the mob up for identical polls
|
|
*/
|
|
/datum/candidate_poll/proc/sign_up(mob/M, silent = FALSE)
|
|
. = FALSE
|
|
if(!HAS_TRAIT(M, TRAIT_RESPAWNABLE) || !M.key || !M.client)
|
|
return
|
|
if(M in signed_up)
|
|
if(!silent)
|
|
to_chat(M, "<span class='warning'>You have already signed up for this!</span>")
|
|
return
|
|
|
|
if(time_left() <= 0)
|
|
if(!silent)
|
|
to_chat(M, "<span class='danger'>Sorry, you were too late for the consideration!</span>")
|
|
SEND_SOUND(M, sound('sound/machines/buzz-sigh.ogg'))
|
|
return
|
|
|
|
signed_up += M
|
|
if(!silent)
|
|
to_chat(M, "<span class='notice'>You have signed up for this role! A candidate will be picked randomly soon.</span>")
|
|
// Sign them up for any other polls with the same mob type
|
|
for(var/existing_poll in SSghost_spawns.currently_polling)
|
|
var/datum/candidate_poll/P = existing_poll
|
|
if(src != P && hash == P.hash && !(M in P.signed_up))
|
|
P.sign_up(M, TRUE)
|
|
|
|
return TRUE
|
|
|
|
/**
|
|
* Attempts to remove a signed-up mob from a poll.
|
|
*
|
|
* Arguments:
|
|
* * M - The mob to remove from the poll, if present.
|
|
* * silent - If TRUE, no messages will be sent to M about their removal.
|
|
*/
|
|
/datum/candidate_poll/proc/remove_candidate(mob/M, silent = FALSE)
|
|
. = FALSE
|
|
if(!HAS_TRAIT(M, TRAIT_RESPAWNABLE) || !M.key || !M.client)
|
|
return
|
|
if(!(M in signed_up))
|
|
if(!silent)
|
|
to_chat(M, "<span class='warning'>You aren't signed up for this!</span>")
|
|
return
|
|
|
|
if(time_left() <= 0)
|
|
if(!silent)
|
|
to_chat(M, "<span class='danger'>It's too late to unregister yourself, selection has already begun!</span>")
|
|
return
|
|
|
|
signed_up -= M
|
|
if(!silent)
|
|
to_chat(M, "<span class='notice'>You have been unregistered as a candidate for this role. You can freely sign up again before the poll ends.</span>")
|
|
|
|
for(var/existing_poll in SSghost_spawns.currently_polling)
|
|
var/datum/candidate_poll/P = existing_poll
|
|
if(src != P && hash == P.hash && (M in P.signed_up))
|
|
P.remove_candidate(M, TRUE)
|
|
return TRUE
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Deletes any candidates who may have disconnected from the list
|
|
*/
|
|
/datum/candidate_poll/proc/trim_candidates()
|
|
listclearnulls(signed_up)
|
|
for(var/mob in signed_up)
|
|
var/mob/M = mob
|
|
if(!M.key || !M.client)
|
|
signed_up -= M
|
|
|
|
/**
|
|
* Returns the time left for a poll
|
|
*/
|
|
/datum/candidate_poll/proc/time_left()
|
|
return duration - (world.time - time_started)
|