mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-01-28 18:11:16 +00:00
* Valentines Day Rework (Better Late Than Never) (#81499) ## About The Pull Request Big changes: - Participation is Valentines day requires consent, as consent is important. - When the event triggers, all valid players are automatically signed up to get a random date. However if you're uninterested, you can opt out of getting a date. - This uses the same system as ghost role polling, so it's a non-obstrusive screen alert + chat box entry. - AIs are now given a zeroth law to protect their date. - This does not override existing zeroth laws (for malf ais). - This zeroth is law is worded in a way such that they are not effectively malf AIs. Their other laws still apply, but not for situations pertaining to their date. - Cyborgs are desynced from AIs and are given similar zeroth laws to protect their date.  Small changes: - Valentines cards are now paper. Meaning you can write on them, stamp them, or yes, burn them. - Third wheeling is more codified than before. Third wheels get their own antag datum type. - The antag panel listing in roundend takes up significantly less room for each date. Additionally, dates are now paired up with each other. - Adds implementations for getting pronouns from mind datums.  ## Why It's Good For The Game It's 2024 and our Valentines day is sooo 2012. I'm a big fan of Valentines Day, personally - it gives me the opportunity to mess around with another player that I probably would not otherwise mess around with, getting into shenanigans I would not otherwise. But as the years have gone by it's gotten pretty lackluster. Some people like it as much as I do, but others ditch it entirely and ignore the objectives. And if you get paired with someone ignoring it, well, now you're out of luck! This is something I'm aiming to rectify by making it opt-in when it triggers rather than forced. All the people participating will be guaranteed to get someone who cares about the event as much, which makes it more fun. As for the silicon changes, there's been lots of confusion around silicons and their dates, so I thought I'd fix it here as well. Also, better late than never? ## Changelog 🆑 Melbert add: Valentines Day now polls all players for candidates when it triggers rather than forcing all players to be a Valentine. Consent is important. add: Valentine silicons now gain special laws pertaining to their date. qol: Valentines Cards are now paper, so you can write on them, stamp them, or burn them. qol: Valentine's roundend report no longer takes up a massive amount of space and also no longer sound so, so weird. /🆑 * Valentines Day Rework (Better Late Than Never) --------- Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
275 lines
12 KiB
Plaintext
275 lines
12 KiB
Plaintext
SUBSYSTEM_DEF(polling)
|
|
name = "Polling"
|
|
flags = SS_BACKGROUND | SS_NO_INIT
|
|
wait = 1 SECONDS
|
|
runlevels = RUNLEVEL_GAME
|
|
/// List of polls currently ongoing, to be checked on next fire()
|
|
var/list/datum/candidate_poll/currently_polling
|
|
/// Number of polls performed since the start
|
|
var/total_polls = 0
|
|
|
|
/datum/controller/subsystem/polling/fire()
|
|
if(!currently_polling) // if polls_active is TRUE then this shouldn't happen, but still..
|
|
currently_polling = list()
|
|
|
|
for(var/datum/candidate_poll/running_poll as anything in currently_polling)
|
|
if(running_poll.time_left() <= 0)
|
|
polling_finished(running_poll)
|
|
|
|
/**
|
|
* Starts a poll.
|
|
*
|
|
* Arguments
|
|
* * question: Optional, The question to ask the candidates. If null, a default question will be used. ("Do you want to play as role?")
|
|
* * role: Optional, An antag role (IE, ROLE_TRAITOR) to pass, it won't show to any candidates who don't have it in their preferences.
|
|
* * check_jobban: Optional, What jobban role / flag to check, it won't show to any candidates who have this jobban.
|
|
* * poll_time: How long the poll will last.
|
|
* * ignore_category: Optional, A poll category. If a candidate has this category in their ignore list, they won't be polled.
|
|
* * flash_window: If TRUE, the candidate's window will flash when they're polled.
|
|
* * list/group: A list of candidates to poll.
|
|
* * pic_source: Optional, An /atom or an /image to display on the poll alert.
|
|
* * role_name_text: Optional, A string to display in logging / the (default) question. If null, the role name will be used.
|
|
* * list/custom_response_messages: Optional, A list of strings to use as responses to the poll. If null, the default responses will be used. see __DEFINES/polls.dm for valid keys to use.
|
|
* * start_signed_up: If TRUE, all candidates will start signed up for the poll, making it opt-out rather than opt-in.
|
|
*
|
|
* Returns a list of all mobs who signed up for the poll.
|
|
*/
|
|
/datum/controller/subsystem/polling/proc/poll_candidates(
|
|
question,
|
|
role,
|
|
check_jobban,
|
|
poll_time = 30 SECONDS,
|
|
ignore_category = null,
|
|
flash_window = TRUE,
|
|
list/group = null,
|
|
pic_source,
|
|
role_name_text,
|
|
list/custom_response_messages,
|
|
start_signed_up = FALSE,
|
|
)
|
|
RETURN_TYPE(/list/mob)
|
|
if(group.len == 0)
|
|
return list()
|
|
if(role && !role_name_text)
|
|
role_name_text = role
|
|
if(role_name_text && !question)
|
|
question = "Do you want to play as [full_capitalize(role_name_text)]?"
|
|
if(!question)
|
|
question = "Do you want to play as a special role?"
|
|
log_game("Polling candidates [role_name_text ? "for [role_name_text]" : "\"[question]\""] for [DisplayTimeText(poll_time)] seconds")
|
|
|
|
// Start firing
|
|
total_polls++
|
|
|
|
var/jumpable = isatom(pic_source) ? pic_source : null
|
|
|
|
var/datum/candidate_poll/new_poll = new(role_name_text, question, poll_time, ignore_category, jumpable, custom_response_messages)
|
|
LAZYADD(currently_polling, new_poll)
|
|
|
|
var/category = "[new_poll.poll_key]_poll_alert"
|
|
|
|
for(var/mob/candidate_mob as anything in group)
|
|
if(!candidate_mob.client)
|
|
continue
|
|
// Universal opt-out for all players if it's for a role.
|
|
if(role && (!candidate_mob.client.prefs.read_preference(/datum/preference/toggle/ghost_roles)))
|
|
continue
|
|
// Opt-out for admins whom are currently adminned.
|
|
if(role && (!candidate_mob.client.prefs.read_preference(/datum/preference/toggle/ghost_roles_as_admin)) && candidate_mob.client.holder)
|
|
continue
|
|
if(role && !is_eligible(candidate_mob, role, check_jobban, ignore_category))
|
|
continue
|
|
|
|
if(start_signed_up)
|
|
new_poll.sign_up(candidate_mob, TRUE)
|
|
if(flash_window)
|
|
window_flash(candidate_mob.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/atom/movable/screen/alert/poll_alert/current_alert = LAZYACCESS(candidate_mob.alerts, category)
|
|
var/alert_time = poll_time
|
|
var/datum/candidate_poll/alert_poll = new_poll
|
|
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/atom/movable/screen/alert/poll_alert/poll_alert_button = candidate_mob.throw_alert(category, /atom/movable/screen/alert/poll_alert, timeout_override = alert_time, no_anim = TRUE)
|
|
if(!poll_alert_button)
|
|
continue
|
|
|
|
new_poll.alert_buttons += poll_alert_button
|
|
new_poll.RegisterSignal(poll_alert_button, COMSIG_QDELETING, TYPE_PROC_REF(/datum/candidate_poll, clear_alert_ref))
|
|
|
|
poll_alert_button.icon = ui_style2icon(candidate_mob.client?.prefs?.read_preference(/datum/preference/choiced/ui_style))
|
|
poll_alert_button.desc = "[question]"
|
|
poll_alert_button.show_time_left = TRUE
|
|
poll_alert_button.poll = alert_poll
|
|
poll_alert_button.set_role_overlay()
|
|
poll_alert_button.update_stacks_overlay()
|
|
poll_alert_button.update_candidates_number_overlay()
|
|
poll_alert_button.update_signed_up_overlay()
|
|
|
|
|
|
// Sign up inheritance and stacking
|
|
for(var/datum/candidate_poll/other_poll as anything in currently_polling)
|
|
if(new_poll == other_poll || new_poll.poll_key != other_poll.poll_key)
|
|
continue
|
|
// 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((candidate_mob in other_poll.signed_up) && new_poll.sign_up(candidate_mob, TRUE))
|
|
break
|
|
|
|
// Image to display
|
|
var/image/poll_image
|
|
if(pic_source)
|
|
if(!ispath(pic_source))
|
|
var/atom/the_pic_source = pic_source
|
|
var/old_layer = the_pic_source.layer
|
|
var/old_plane = the_pic_source.plane
|
|
the_pic_source.plane = poll_alert_button.plane
|
|
the_pic_source.layer = FLOAT_LAYER
|
|
poll_alert_button.add_overlay(the_pic_source)
|
|
the_pic_source.layer = old_layer
|
|
the_pic_source.plane = old_plane
|
|
else
|
|
poll_image = image(pic_source, layer = FLOAT_LAYER)
|
|
else
|
|
// Just use a generic image
|
|
poll_image = image('icons/effects/effects.dmi', icon_state = "static", layer = FLOAT_LAYER)
|
|
|
|
if(poll_image)
|
|
poll_image.plane = poll_alert_button.plane
|
|
poll_alert_button.add_overlay(poll_image)
|
|
|
|
// Chat message
|
|
var/act_jump = ""
|
|
if(isatom(pic_source) && isobserver(candidate_mob))
|
|
act_jump = "<a href='?src=[REF(poll_alert_button)];jump=1'>\[Teleport\]</a>"
|
|
var/act_signup = "<a href='?src=[REF(poll_alert_button)];signup=1'>\[[start_signed_up ? "Opt out" : "Sign Up"]\]</a>"
|
|
var/act_never = ""
|
|
if(ignore_category)
|
|
act_never = "<a href='?src=[REF(poll_alert_button)];never=1'>\[Never For This Round\]</a>"
|
|
|
|
if(!duplicate_message_check(alert_poll)) //Only notify people once. They'll notice if there are multiple and we don't want to spam people.
|
|
SEND_SOUND(candidate_mob, 'sound/misc/notice2.ogg')
|
|
to_chat(candidate_mob, span_boldnotice(examine_block("Now looking for candidates [role_name_text ? "to play as \an [role_name_text]." : "\"[question]\""] [act_jump] [act_signup] [act_never]")))
|
|
|
|
// Start processing it so it updates visually the timer
|
|
START_PROCESSING(SSprocessing, poll_alert_button)
|
|
|
|
// Sleep until the time is up
|
|
UNTIL(new_poll.finished)
|
|
return new_poll.signed_up
|
|
|
|
/datum/controller/subsystem/polling/proc/poll_ghost_candidates(question, role, check_jobban, poll_time = 30 SECONDS, ignore_category = null, flashwindow = TRUE, pic_source, role_name_text)
|
|
var/list/candidates = list()
|
|
if(!(GLOB.ghost_role_flags & GHOSTROLE_STATION_SENTIENCE))
|
|
return candidates
|
|
|
|
for(var/mob/dead/observer/ghost_player in GLOB.player_list)
|
|
candidates += ghost_player
|
|
|
|
return poll_candidates(question, role, check_jobban, poll_time, ignore_category, flashwindow, candidates, pic_source, role_name_text)
|
|
|
|
/datum/controller/subsystem/polling/proc/poll_ghost_candidates_for_mob(question, role, check_jobban, poll_time = 30 SECONDS, mob/target_mob, ignore_category = null, flashwindow = TRUE, pic_source, role_name_text)
|
|
var/static/list/mob/currently_polling_mobs = list()
|
|
|
|
if(currently_polling_mobs.Find(target_mob))
|
|
return list()
|
|
|
|
currently_polling_mobs += target_mob
|
|
|
|
var/list/possible_candidates = poll_ghost_candidates(question, role, check_jobban, poll_time, ignore_category, flashwindow, pic_source, role_name_text)
|
|
|
|
currently_polling_mobs -= target_mob
|
|
if(!target_mob || QDELETED(target_mob) || !target_mob.loc)
|
|
return list()
|
|
|
|
return possible_candidates
|
|
|
|
/datum/controller/subsystem/polling/proc/poll_ghost_candidates_for_mobs(question, role, check_jobban, poll_time = 30 SECONDS, list/mobs, ignore_category = null, flashwindow = TRUE, pic_source, role_name_text)
|
|
var/list/candidate_list = poll_ghost_candidates(question, role, check_jobban, poll_time, ignore_category, flashwindow, pic_source, role_name_text)
|
|
|
|
for(var/mob/potential_mob as anything in mobs)
|
|
if(QDELETED(potential_mob) || !potential_mob.loc)
|
|
mobs -= potential_mob
|
|
|
|
if(!length(mobs))
|
|
return list()
|
|
|
|
return candidate_list
|
|
|
|
/datum/controller/subsystem/polling/proc/is_eligible(mob/potential_candidate, role, check_jobban, the_ignore_category)
|
|
if(isnull(potential_candidate.key) || isnull(potential_candidate.client))
|
|
return FALSE
|
|
if(the_ignore_category)
|
|
if(potential_candidate.ckey in GLOB.poll_ignore[the_ignore_category])
|
|
return FALSE
|
|
if(role)
|
|
if(!(role in potential_candidate.client.prefs.be_special))
|
|
return FALSE
|
|
var/required_time = GLOB.special_roles[role] || 0
|
|
if(potential_candidate.client && potential_candidate.client.get_remaining_days(required_time) > 0)
|
|
return FALSE
|
|
|
|
if(check_jobban)
|
|
if(is_banned_from(potential_candidate.ckey, list(check_jobban, ROLE_SYNDICATE)))
|
|
return FALSE
|
|
|
|
//SKYRAT EDIT ADDITION BEGIN
|
|
if(is_banned_from(potential_candidate.ckey, BAN_GHOST_TAKEOVER) || is_banned_from(potential_candidate.ckey, BAN_ANTAGONIST))
|
|
to_chat(potential_candidate, "There was a ghost prompt for: [role], unfortunately you are banned from ghost takeovers.")
|
|
return FALSE
|
|
//SKYRAT EDIT END
|
|
|
|
return TRUE
|
|
|
|
/datum/controller/subsystem/polling/proc/polling_finished(datum/candidate_poll/finishing_poll)
|
|
currently_polling -= finishing_poll
|
|
// Trim players who aren't eligible anymore
|
|
var/length_pre_trim = length(finishing_poll.signed_up)
|
|
finishing_poll.trim_candidates()
|
|
log_game("Candidate poll [finishing_poll.role ? "for [finishing_poll.role]" : "\"[finishing_poll.question]\""] finished. [length_pre_trim] players signed up, [length(finishing_poll.signed_up)] after trimming")
|
|
finishing_poll.finished = TRUE
|
|
|
|
// Take care of updating the remaining screen alerts if a similar poll is found, or deleting them.
|
|
if(length(finishing_poll.alert_buttons))
|
|
for(var/atom/movable/screen/alert/poll_alert/alert as anything in finishing_poll.alert_buttons)
|
|
if(duplicate_message_check(finishing_poll))
|
|
alert.update_stacks_overlay()
|
|
else
|
|
alert.owner.clear_alert("[finishing_poll.poll_key]_poll_alert")
|
|
|
|
//More than enough time for the the `UNTIL()` stopping loop in `poll_candidates()` to be over, and the results to be turned in.
|
|
QDEL_IN(finishing_poll, 0.5 SECONDS)
|
|
|
|
/datum/controller/subsystem/polling/stat_entry(msg)
|
|
msg += "Active: [length(currently_polling)] | Total: [total_polls]"
|
|
var/datum/candidate_poll/soonest_to_complete = get_next_poll_to_finish()
|
|
if(soonest_to_complete)
|
|
msg += " | Next: [DisplayTimeText(soonest_to_complete.time_left())] ([length(soonest_to_complete.signed_up)] candidates)"
|
|
return ..()
|
|
|
|
///Is there a multiple of the given event type running right now?
|
|
/datum/controller/subsystem/polling/proc/duplicate_message_check(datum/candidate_poll/poll_to_check)
|
|
for(var/datum/candidate_poll/running_poll as anything in currently_polling)
|
|
if((running_poll.poll_key == poll_to_check.poll_key && running_poll != poll_to_check) && running_poll.time_left() > 0)
|
|
return TRUE
|
|
return FALSE
|
|
|
|
/datum/controller/subsystem/polling/proc/get_next_poll_to_finish()
|
|
var/lowest_time_left = INFINITY
|
|
var/next_poll_to_finish
|
|
for(var/datum/candidate_poll/poll as anything in currently_polling)
|
|
var/time_left = poll.time_left()
|
|
if(time_left >= lowest_time_left)
|
|
continue
|
|
lowest_time_left = time_left
|
|
next_poll_to_finish = poll
|
|
|
|
if(isnull(next_poll_to_finish))
|
|
return FALSE
|
|
|
|
return next_poll_to_finish
|