mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-23 05:00:55 +01:00
Refactors SSvote, makes votes into datums, also makes vote ui Typescript (#66772)
Makes vote into their own singleton datums. Refactors the voting subsystem to accommodate. Refactors the vote UI from JS to TSX (probably badly).
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
|
||||
/**
|
||||
* # Vote Singleton
|
||||
*
|
||||
* A singleton datum that represents a type of vote for the voting subsystem.
|
||||
*/
|
||||
/datum/vote
|
||||
/// The name of the vote.
|
||||
var/name
|
||||
/// If supplied, an override question will be displayed instead of the name of the vote.
|
||||
var/override_question
|
||||
/// The sound effect played to everyone when this vote is initiated.
|
||||
var/vote_sound = 'sound/misc/bloop.ogg'
|
||||
/// A list of default choices we have for this vote.
|
||||
var/list/default_choices
|
||||
|
||||
// Internal values used when tracking ongoing votes.
|
||||
// Don't mess with these, change the above values / override procs for subtypes.
|
||||
/// An assoc list of [all choices] to [number of votes in the current running vote].
|
||||
var/list/choices = list()
|
||||
/// A assoc list of [ckey] to [what they voted for in the current running vote].
|
||||
var/list/choices_by_ckey = list()
|
||||
/// The world time this vote was started.
|
||||
var/started_time
|
||||
/// The time remaining in this vote's run.
|
||||
var/time_remaining
|
||||
|
||||
/**
|
||||
* Used to determine if this vote is a possible
|
||||
* vote type for the vote subsystem.
|
||||
*
|
||||
* If FALSE is returned, this vote singleton
|
||||
* will not be created when the vote subsystem initializes,
|
||||
* meaning no one will be able to hold this vote.
|
||||
*/
|
||||
/datum/vote/proc/is_accessible_vote()
|
||||
return !!length(default_choices)
|
||||
|
||||
/**
|
||||
* Resets our vote to its default state.
|
||||
*/
|
||||
/datum/vote/proc/reset()
|
||||
SHOULD_CALL_PARENT(TRUE)
|
||||
|
||||
choices.Cut()
|
||||
choices_by_ckey.Cut()
|
||||
started_time = null
|
||||
time_remaining = null
|
||||
|
||||
/**
|
||||
* If this vote has a config associated, toggles it between enabled and disabled.
|
||||
* Returns TRUE on a successful toggle, FALSE otherwise
|
||||
*/
|
||||
/datum/vote/proc/toggle_votable(mob/toggler)
|
||||
return FALSE
|
||||
|
||||
/**
|
||||
* If this vote has a config associated, returns its value (True or False, usually).
|
||||
* If it has no config, returns -1.
|
||||
*/
|
||||
/datum/vote/proc/is_config_enabled()
|
||||
return -1
|
||||
|
||||
/**
|
||||
* Checks if the passed mob can initiate this vote.
|
||||
*
|
||||
* Return TRUE if the mob can begin the vote, allowing anyone to actually vote on it.
|
||||
* Return FALSE if the mob cannot initiate the vote.
|
||||
*/
|
||||
/datum/vote/proc/can_be_initiated(mob/by_who, forced = FALSE)
|
||||
SHOULD_CALL_PARENT(TRUE)
|
||||
|
||||
if(started_time)
|
||||
var/next_allowed_time = (started_time + CONFIG_GET(number/vote_delay))
|
||||
if(next_allowed_time > world.time && !forced)
|
||||
if(by_who)
|
||||
to_chat(by_who, span_warning("A vote was initiated recently. You must wait [DisplayTimeText(next_allowed_time - world.time)] before a new vote can be started!"))
|
||||
return FALSE
|
||||
|
||||
return TRUE
|
||||
|
||||
/**
|
||||
* Called prior to the vote being initiated.
|
||||
*
|
||||
* Return FALSE to prevent the vote from being initiated.
|
||||
*/
|
||||
/datum/vote/proc/create_vote(mob/vote_creator)
|
||||
SHOULD_CALL_PARENT(TRUE)
|
||||
|
||||
for(var/key in default_choices)
|
||||
choices[key] = 0
|
||||
|
||||
return TRUE
|
||||
|
||||
/**
|
||||
* Called when this vote is actually initiated.
|
||||
*
|
||||
* Return a string - the text displayed to the world when the vote is initiated.
|
||||
*/
|
||||
/datum/vote/proc/initiate_vote(initiator, duration)
|
||||
SHOULD_CALL_PARENT(TRUE)
|
||||
|
||||
started_time = world.time
|
||||
time_remaining = round(duration / 10)
|
||||
|
||||
return "[capitalize(name)] vote started by [initiator || "Central Command"]."
|
||||
|
||||
/**
|
||||
* Gets the result of the vote.
|
||||
*
|
||||
* non_voters - a list of all ckeys who didn't vote in the vote.
|
||||
*
|
||||
* Returns a list of all options that won.
|
||||
* If there were no votes at all, the list will be length = 0, non-null.
|
||||
* If only one option one, the list will be length = 1.
|
||||
* If there was a tie, the list will be length > 1.
|
||||
*/
|
||||
/datum/vote/proc/get_vote_result(list/non_voters)
|
||||
RETURN_TYPE(/list)
|
||||
|
||||
var/list/winners = list()
|
||||
var/highest_vote = 0
|
||||
|
||||
for(var/option in choices)
|
||||
|
||||
var/vote_count = choices[option]
|
||||
// If we currently have no winners...
|
||||
if(!length(winners))
|
||||
// And the current option has any votes, it's the new highest.
|
||||
if(vote_count > 0)
|
||||
winners += option
|
||||
highest_vote = vote_count
|
||||
continue
|
||||
|
||||
// If we're greater than, and NOT equal to, the highest vote,
|
||||
// we are the new supreme winner - clear all others
|
||||
if(vote_count > highest_vote)
|
||||
winners.Cut()
|
||||
winners += option
|
||||
highest_vote = vote_count
|
||||
|
||||
// If we're equal to the highest vote, we tie for winner
|
||||
else if(vote_count == highest_vote)
|
||||
winners += option
|
||||
|
||||
return winners
|
||||
|
||||
/**
|
||||
* Gets the resulting text displayed when the vote is completed.
|
||||
*
|
||||
* all_winners - list of all options that won. Can be multiple, in the event of ties.
|
||||
* real_winner - the option that actually won.
|
||||
* non_voters - a list of all ckeys who didn't vote in the vote.
|
||||
*
|
||||
* Return a formatted string of text to be displayed to everyone.
|
||||
*/
|
||||
/datum/vote/proc/get_result_text(list/all_winners, real_winner, list/non_voters)
|
||||
if(length(all_winners) <= 0 || !real_winner)
|
||||
return span_bold("Vote Result: Inconclusive - No Votes!")
|
||||
|
||||
var/returned_text = ""
|
||||
if(override_question)
|
||||
returned_text += span_bold(override_question)
|
||||
else
|
||||
returned_text += span_bold("[capitalize(name)]")
|
||||
|
||||
for(var/option in choices)
|
||||
returned_text += "\n[span_bold(option)]: [choices[option]]"
|
||||
|
||||
returned_text += "\n"
|
||||
returned_text += get_winner_text(all_winners, real_winner, non_voters)
|
||||
|
||||
return returned_text
|
||||
|
||||
/**
|
||||
* Gets the text that displays the winning options within the result text.
|
||||
*
|
||||
* all_winners - list of all options that won. Can be multiple, in the event of ties.
|
||||
* real_winner - the option that actually won.
|
||||
* non_voters - a list of all ckeys who didn't vote in the vote.
|
||||
*
|
||||
* Return a formatted string of text to be displayed to everyone.
|
||||
*/
|
||||
/datum/vote/proc/get_winner_text(list/all_winners, real_winner, list/non_voters)
|
||||
var/returned_text = ""
|
||||
if(length(all_winners) > 1)
|
||||
returned_text += "\n[span_bold("Vote Tied Between:")]"
|
||||
for(var/a_winner in all_winners)
|
||||
returned_text += "\n\t[a_winner]"
|
||||
|
||||
returned_text += span_bold("Vote Result: [real_winner]")
|
||||
return returned_text
|
||||
|
||||
/**
|
||||
* How this vote handles a tiebreaker between multiple winners.
|
||||
*/
|
||||
/datum/vote/proc/tiebreaker(list/winners)
|
||||
return pick(winners)
|
||||
|
||||
/**
|
||||
* Called when a vote is actually all said and done.
|
||||
* Apply actual vote effects here.
|
||||
*/
|
||||
/datum/vote/proc/finalize_vote(winning_option)
|
||||
return
|
||||
@@ -0,0 +1,53 @@
|
||||
/// The max amount of options someone can have in a custom vote.
|
||||
#define MAX_CUSTOM_VOTE_OPTIONS 10
|
||||
|
||||
/datum/vote/custom_vote
|
||||
name = "Custom"
|
||||
|
||||
// Custom votes ares always accessible.
|
||||
/datum/vote/custom_vote/is_accessible_vote()
|
||||
return TRUE
|
||||
|
||||
/datum/vote/custom_vote/reset()
|
||||
default_choices = null
|
||||
override_question = null
|
||||
return ..()
|
||||
|
||||
/datum/vote/custom_vote/can_be_initiated(mob/by_who, forced = FALSE)
|
||||
. = ..()
|
||||
if(!.)
|
||||
return FALSE
|
||||
|
||||
// Custom votes can only be created if they're forced to be made.
|
||||
// (Either an admin makes it, or otherwise.)
|
||||
return forced
|
||||
|
||||
/datum/vote/custom_vote/create_vote(mob/vote_creator)
|
||||
override_question = tgui_input_text(vote_creator, "What is the vote for?", "Custom Vote")
|
||||
if(!override_question)
|
||||
return FALSE
|
||||
|
||||
default_choices = list()
|
||||
for(var/i in 1 to MAX_CUSTOM_VOTE_OPTIONS)
|
||||
var/option = tgui_input_text(vote_creator, "Please enter an option, or hit cancel to finish. [MAX_CUSTOM_VOTE_OPTIONS] max.", "Options", max_length = MAX_NAME_LEN)
|
||||
if(!vote_creator?.client)
|
||||
return FALSE
|
||||
if(!option)
|
||||
break
|
||||
|
||||
default_choices += capitalize(option)
|
||||
|
||||
if(!length(default_choices))
|
||||
return FALSE
|
||||
|
||||
return ..()
|
||||
|
||||
/datum/vote/custom_vote/initiate_vote(initiator, duration)
|
||||
. = ..()
|
||||
. += "\n[override_question]"
|
||||
|
||||
// There are no winners or losers for custom votes
|
||||
/datum/vote/custom_vote/get_winner_text(list/all_winners, real_winner, list/non_voters)
|
||||
return "[span_bold("Did not vote:")] [length(non_voters)]"
|
||||
|
||||
#undef MAX_CUSTOM_VOTE_OPTIONS
|
||||
@@ -0,0 +1,87 @@
|
||||
/datum/vote/map_vote
|
||||
name = "Map"
|
||||
|
||||
/datum/vote/map_vote/New()
|
||||
. = ..()
|
||||
|
||||
default_choices = list()
|
||||
|
||||
// Fill in our default choices with all of the maps in our map config, if they are votable and not blocked.
|
||||
var/list/maps = shuffle(global.config.maplist)
|
||||
for(var/map in maps)
|
||||
var/datum/map_config/possible_config = config.maplist[map]
|
||||
if(!possible_config.votable || (possible_config.map_name in SSpersistence.blocked_maps))
|
||||
continue
|
||||
|
||||
default_choices += possible_config.map_name
|
||||
|
||||
/datum/vote/map_vote/create_vote()
|
||||
. = ..()
|
||||
|
||||
// Before we create a vote, remove all maps from our choices that are outside of our population range.
|
||||
// Note that this can result in zero remaining choices for our vote, which is not ideal (but technically fine).
|
||||
for(var/map in choices)
|
||||
var/datum/map_config/possible_config = config.maplist[map]
|
||||
if(possible_config.config_min_users > 0 && GLOB.clients.len < possible_config.config_min_users)
|
||||
choices -= map
|
||||
|
||||
else if(possible_config.config_max_users > 0 && GLOB.clients.len > possible_config.config_max_users)
|
||||
choices -= map
|
||||
|
||||
/datum/vote/map_vote/toggle_votable(mob/toggler)
|
||||
if(!toggler)
|
||||
CRASH("[type] wasn't passed a \"toggler\" mob to toggle_votable.")
|
||||
if(!check_rights_for(toggler.client, R_ADMIN))
|
||||
return FALSE
|
||||
|
||||
CONFIG_SET(flag/allow_vote_map, !CONFIG_GET(flag/allow_vote_map))
|
||||
return TRUE
|
||||
|
||||
/datum/vote/map_vote/is_config_enabled()
|
||||
return CONFIG_GET(flag/allow_vote_map)
|
||||
|
||||
/datum/vote/map_vote/can_be_initiated(mob/by_who, forced = FALSE)
|
||||
. = ..()
|
||||
if(!.)
|
||||
return FALSE
|
||||
|
||||
if(forced)
|
||||
return TRUE
|
||||
|
||||
if(!CONFIG_GET(flag/allow_vote_map))
|
||||
if(by_who)
|
||||
to_chat(by_who, span_warning("Map voting is disabled."))
|
||||
return FALSE
|
||||
|
||||
if(SSmapping.map_voted)
|
||||
if(by_who)
|
||||
to_chat(by_who, span_warning("The next map has already been selected."))
|
||||
return FALSE
|
||||
|
||||
return TRUE
|
||||
|
||||
/datum/vote/map_vote/get_vote_result(list/non_voters)
|
||||
// Even if we have default no vote off,
|
||||
// if our default map is null for some reason, we shouldn't continue
|
||||
if(CONFIG_GET(flag/default_no_vote) || isnull(global.config.defaultmap))
|
||||
return ..()
|
||||
|
||||
for(var/non_voter_ckey in non_voters)
|
||||
var/client/non_voter_client = non_voters[non_voter_ckey]
|
||||
// Non-voters will have their preferred map voted for automatically.
|
||||
var/their_preferred_map = non_voter_client?.prefs.read_preference(/datum/preference/choiced/preferred_map)
|
||||
// If the non-voter's preferred map is null for some reason, we just use the default map.
|
||||
var/voting_for = their_preferred_map || global.config.defaultmap.map_name
|
||||
|
||||
if(voting_for in choices)
|
||||
choices[voting_for] += 1
|
||||
|
||||
return ..()
|
||||
|
||||
/datum/vote/map_vote/finalize_vote(winning_option)
|
||||
var/datum/map_config/winning_map = global.config.maplist[winning_option]
|
||||
if(!istype(winning_map))
|
||||
CRASH("[type] wasn't passed a valid winning map choice. (Got: [winning_option || "null"] - [winning_map || "null"])")
|
||||
|
||||
SSmapping.changemap(winning_map)
|
||||
SSmapping.map_voted = TRUE
|
||||
@@ -0,0 +1,61 @@
|
||||
#define CHOICE_RESTART "Restart Round"
|
||||
#define CHOICE_CONTINUE "Continue Playing"
|
||||
|
||||
/datum/vote/restart_vote
|
||||
name = "Restart"
|
||||
default_choices = list(
|
||||
CHOICE_RESTART,
|
||||
CHOICE_CONTINUE,
|
||||
)
|
||||
|
||||
/datum/vote/restart_vote/toggle_votable(mob/toggler)
|
||||
if(!toggler)
|
||||
CRASH("[type] wasn't passed a \"toggler\" mob to toggle_votable.")
|
||||
if(!check_rights_for(toggler.client, R_ADMIN))
|
||||
return FALSE
|
||||
|
||||
CONFIG_SET(flag/allow_vote_restart, !CONFIG_GET(flag/allow_vote_restart))
|
||||
return TRUE
|
||||
|
||||
/datum/vote/restart_vote/is_config_enabled()
|
||||
return CONFIG_GET(flag/allow_vote_restart)
|
||||
|
||||
/datum/vote/restart_vote/can_be_initiated(mob/by_who, forced)
|
||||
. = ..()
|
||||
if(!.)
|
||||
return FALSE
|
||||
|
||||
if(!forced && !CONFIG_GET(flag/allow_vote_restart))
|
||||
if(by_who)
|
||||
to_chat(by_who, span_warning("Restart voting is disabled."))
|
||||
return FALSE
|
||||
|
||||
return TRUE
|
||||
|
||||
/datum/vote/restart_vote/get_vote_result(list/non_voters)
|
||||
if(!CONFIG_GET(flag/default_no_vote))
|
||||
// Default no votes will add non-voters to "Continue Playing"
|
||||
choices[CHOICE_CONTINUE] += length(non_voters)
|
||||
|
||||
return ..()
|
||||
|
||||
/datum/vote/restart_vote/finalize_vote(winning_option)
|
||||
if(winning_option == CHOICE_CONTINUE)
|
||||
return
|
||||
|
||||
if(winning_option == CHOICE_RESTART)
|
||||
for(var/client/online_admin as anything in GLOB.admins | GLOB.deadmins)
|
||||
if(online_admin.is_afk() || !check_rights_for(online_admin, R_SERVER))
|
||||
continue
|
||||
|
||||
to_chat(world, span_boldannounce("Notice: A restart vote will not restart the server automatically because there are active admins on."))
|
||||
message_admins("A restart vote has passed, but there are active admins on with +SERVER, so it has been canceled. If you wish, you may restart the server.")
|
||||
return
|
||||
|
||||
SSticker.Reboot("Restart vote successful.", "restart vote", 1)
|
||||
return
|
||||
|
||||
CRASH("[type] wasn't passed a valid winning choice. (Got: [winning_option || "null"])")
|
||||
|
||||
#undef CHOICE_RESTART
|
||||
#undef CHOICE_CONTINUE
|
||||
Reference in New Issue
Block a user