diff --git a/code/controllers/subsystem/vote.dm b/code/controllers/subsystem/vote.dm index e7508654900..b7e1b028029 100644 --- a/code/controllers/subsystem/vote.dm +++ b/code/controllers/subsystem/vote.dm @@ -6,42 +6,37 @@ SUBSYSTEM_DEF(vote) runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT - var/initiator = null - var/started_time = null - var/time_remaining = 0 - var/mode = null - var/question = null var/list/choices = list() + var/list/choice_by_ckey = list() + var/list/generated_actions = list() + var/initiator + var/mode + var/question + var/started_time + var/time_remaining var/list/voted = list() var/list/voting = list() - var/list/generated_actions = list() - -/datum/controller/subsystem/vote/fire() //called by master_controller - if(mode) - time_remaining = round((started_time + CONFIG_GET(number/vote_period) - world.time)/10) - - if(time_remaining < 0) - result() - for(var/client/C in voting) - C << browse(null, "window=vote;can_close=0") - reset() - else - var/datum/browser/client_popup - for(var/client/C in voting) - client_popup = new(C, "vote", "Voting Panel") - client_popup.set_window_options("can_close=0") - client_popup.set_content(interface(C)) - client_popup.open(FALSE) +// Called by master_controller +/datum/controller/subsystem/vote/fire() + if(!mode) + return + time_remaining = round((started_time + CONFIG_GET(number/vote_period) - world.time)/10) + if(time_remaining < 0) + result() + SStgui.close_uis(src) + reset() /datum/controller/subsystem/vote/proc/reset() + choices.Cut() + choice_by_ckey.Cut() initiator = null - time_remaining = 0 mode = null question = null - choices.Cut() + time_remaining = 0 voted.Cut() voting.Cut() + remove_action_buttons() /datum/controller/subsystem/vote/proc/get_result() @@ -74,17 +69,13 @@ SUBSYSTEM_DEF(vote) else if(mode == "map") for (var/non_voter_ckey in non_voters) var/client/C = non_voters[non_voter_ckey] - if(C.prefs.preferred_map) - if(choices[C.prefs.preferred_map]) //No votes if the map isn't in the vote. - var/preferred_map = C.prefs.preferred_map - choices[preferred_map] += 1 - greatest_votes = max(greatest_votes, choices[preferred_map]) - else if(config.defaultmap) - if(choices[config.defaultmap]) //No votes if the map isn't in the vote. - var/default_map = config.defaultmap.map_name - choices[default_map] += 1 - greatest_votes = max(greatest_votes, choices[default_map]) - //get all options with that many votes and return them in a list + var/preferred_map = C.prefs.preferred_map + if(isnull(global.config.defaultmap)) + continue + if(!preferred_map) + preferred_map = global.config.defaultmap.map_name + choices[preferred_map] += 1 + greatest_votes = max(greatest_votes, choices[preferred_map]) . = list() if(greatest_votes) for(var/option in choices) @@ -141,12 +132,13 @@ SUBSYSTEM_DEF(vote) SSmapping.map_voted = TRUE if(restart) var/active_admins = FALSE - for(var/client/C in GLOB.admins) + for(var/client/C in GLOB.admins + GLOB.deadmins) if(!C.is_afk() && check_rights_for(C, R_SERVER)) active_admins = TRUE break if(!active_admins) - SSticker.Reboot("Restart vote successful.", "restart vote", 1) //no delay in case the restart is due to lag + // No delay in case the restart is due to lag + SSticker.Reboot("Restart vote successful.", "restart vote", 1) else to_chat(world, "Notice: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.") @@ -154,24 +146,30 @@ SUBSYSTEM_DEF(vote) return . /datum/controller/subsystem/vote/proc/submit_vote(vote) - if(mode) - if(CONFIG_GET(flag/no_dead_vote) && usr.stat == DEAD && !usr.client.holder) - return FALSE - if(!(usr.ckey in voted)) - if(vote && 1<=vote && vote<=choices.len) - voted += usr.ckey - choices[choices[vote]]++ //check this - return vote - return FALSE + if(!mode) + return FALSE + if(CONFIG_GET(flag/no_dead_vote) && usr.stat == DEAD && !usr.client.holder) + return FALSE + if(!vote || vote < 1 || vote > choices.len) + return FALSE + // If user has already voted, remove their specific vote + if(usr.ckey in voted) + choices[choices[choice_by_ckey[usr.ckey]]]-- + else + voted += usr.ckey + choice_by_ckey[usr.ckey] = vote + choices[choices[vote]]++ + return vote /datum/controller/subsystem/vote/proc/initiate_vote(vote_type, initiator_key) - if(!Master.current_runlevel) //Server is still intializing. + //Server is still intializing. + if(!Master.current_runlevel) to_chat(usr, "Cannot start vote, server is not done initializing.") return FALSE - var/admin = FALSE + var/lower_admin = FALSE var/ckey = ckey(initiator_key) if(GLOB.admin_datums[ckey]) - admin = TRUE + lower_admin = TRUE if(!mode) if(started_time) @@ -179,9 +177,7 @@ SUBSYSTEM_DEF(vote) if(mode) to_chat(usr, "There is already a vote in progress! please wait for it to finish.") return FALSE - - - if(next_allowed_time > world.time && !admin) + if(next_allowed_time > world.time && !lower_admin) to_chat(usr, "A vote was initiated recently, you must wait [DisplayTimeText(next_allowed_time-world.time)] before a new vote can be started!") return FALSE @@ -192,14 +188,19 @@ SUBSYSTEM_DEF(vote) if("gamemode") choices.Add(config.votable_modes) if("map") - if(!admin && SSmapping.map_voted) + if(!lower_admin && SSmapping.map_voted) to_chat(usr, "The next map has already been selected.") return FALSE - for(var/map in config.maplist) + // Randomizes the list so it isn't always METASTATION + var/list/maps = list() + for(var/map in global.config.maplist) var/datum/map_config/VM = config.maplist[map] if(!VM.votable || (VM.map_name in SSpersistence.blocked_maps)) continue - choices.Add(VM.map_name) + maps += VM.map_name + shuffle_inplace(maps) + for(var/valid_map in maps) + choices.Add(valid_map) if("custom") question = stripped_input(usr,"What is the vote for?") if(!question) @@ -214,12 +215,12 @@ SUBSYSTEM_DEF(vote) mode = vote_type initiator = initiator_key started_time = world.time - var/text = "[capitalize(mode)] vote started by [initiator]." + var/text = "[capitalize(mode)] vote started by [initiator || "CentCom"]." if(mode == "custom") text += "\n[question]" log_vote(text) var/vp = CONFIG_GET(number/vote_period) - to_chat(world, "\n[text]\nType vote or click here to place your votes.\nYou have [DisplayTimeText(vp)] to vote.") + to_chat(world, "\n[text]\nType vote or click here to place your votes.\nYou have [DisplayTimeText(vp)] to vote.") time_remaining = round(vp/10) for(var/c in GLOB.clients) var/client/C = c @@ -234,97 +235,73 @@ SUBSYSTEM_DEF(vote) return TRUE return FALSE -/datum/controller/subsystem/vote/proc/interface(client/C) - if(!C) +/mob/verb/vote() + set category = "OOC" + set name = "Vote" + SSvote.ui_interact(usr) + +/datum/controller/subsystem/vote/ui_state() + return GLOB.always_state + +/datum/controller/subsystem/vote/ui_interact(mob/user, datum/tgui/ui) + // Tracks who is voting + if(!(user.client?.ckey in voting)) + voting += user.client?.ckey + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "Vote") + ui.open() + +/datum/controller/subsystem/vote/ui_data(mob/user) + var/list/data = list( + "allow_vote_map" = CONFIG_GET(flag/allow_vote_map), + "allow_vote_mode" = CONFIG_GET(flag/allow_vote_mode), + "allow_vote_restart" = CONFIG_GET(flag/allow_vote_restart), + "choices" = list(), + "lower_admin" = !!user.client?.holder, + "mode" = mode, + "question" = question, + "selected_choice" = choice_by_ckey[user.client?.ckey], + "time_remaining" = time_remaining, + "upper_admin" = check_rights_for(user.client, R_ADMIN), + "voting" = list(), + ) + + if(!!user.client?.holder) + data["voting"] += list(voting) + + for(var/key in choices) + data["choices"] += list(list( + "name" = key, + "votes" = choices[key] || 0 + )) + + return data + +/datum/controller/subsystem/vote/ui_act(action, params) + . = ..() + if(.) return - var/admin = FALSE - var/trialmin = FALSE - if(C.holder) - admin = TRUE - if(check_rights_for(C, R_ADMIN)) - trialmin = TRUE - voting |= C - if(mode) - if(question) - . += "

Vote: '[question]'

" - else - . += "

Vote: [capitalize(mode)]

" - . += "Time Left: [time_remaining] s

" - if(admin) - . += "(Cancel Vote) " - else - . += "

Start a vote:



" - . += "Close" - return . - - -/datum/controller/subsystem/vote/Topic(href,href_list[],hsrc) - if(!usr || !usr.client) - return //not necessary but meh...just in-case somebody does something stupid - - var/trialmin = FALSE + var/upper_admin = FALSE if(usr.client.holder) if(check_rights_for(usr.client, R_ADMIN)) - trialmin = TRUE + upper_admin = TRUE - switch(href_list["vote"]) - if("close") - voting -= usr.client - usr << browse(null, "window=vote") - return + switch(action) if("cancel") if(usr.client.holder) usr.log_message("[key_name_admin(usr)] cancelled a vote.", LOG_ADMIN) message_admins("[key_name_admin(usr)] has cancelled the current vote.") reset() if("toggle_restart") - if(usr.client.holder && trialmin) + if(usr.client.holder && upper_admin) CONFIG_SET(flag/allow_vote_restart, !CONFIG_GET(flag/allow_vote_restart)) if("toggle_gamemode") - if(usr.client.holder && trialmin) + if(usr.client.holder && upper_admin) CONFIG_SET(flag/allow_vote_mode, !CONFIG_GET(flag/allow_vote_mode)) if("toggle_map") - if(usr.client.holder && trialmin) + if(usr.client.holder && upper_admin) CONFIG_SET(flag/allow_vote_map, !CONFIG_GET(flag/allow_vote_map)) if("restart") if(CONFIG_GET(flag/allow_vote_restart) || usr.client.holder) @@ -338,9 +315,9 @@ SUBSYSTEM_DEF(vote) if("custom") if(usr.client.holder) initiate_vote("custom",usr.key) - else - submit_vote(round(text2num(href_list["vote"]))) - usr.vote() + if("vote") + submit_vote(round(text2num(params["index"]))) + return TRUE /datum/controller/subsystem/vote/proc/remove_action_buttons() for(var/v in generated_actions) @@ -350,14 +327,8 @@ SUBSYSTEM_DEF(vote) V.Remove(V.owner) generated_actions = list() -/mob/verb/vote() - set category = "OOC" - set name = "Vote" - - var/datum/browser/popup = new(src, "vote", "Voting Panel") - popup.set_window_options("can_close=0") - popup.set_content(SSvote.interface(client)) - popup.open(FALSE) +/datum/controller/subsystem/vote/ui_close(mob/user) + voting -= user.client?.ckey /datum/action/vote name = "Vote!" diff --git a/tgui/packages/tgui/interfaces/Vote.js b/tgui/packages/tgui/interfaces/Vote.js new file mode 100644 index 00000000000..aba4911d3dd --- /dev/null +++ b/tgui/packages/tgui/interfaces/Vote.js @@ -0,0 +1,194 @@ +import { useBackend } from "../backend"; +import { Box, Icon, Stack, Button, Section, NoticeBox, LabeledList, Collapsible } from "../components"; +import { Window } from "../layouts"; + +export const Vote = (props, context) => { + const { data } = useBackend(context); + const { mode, question, lower_admin } = data; + + // Adds the voting type to title if there is an ongoing vote + let windowTitle = "Vote"; + if (mode) { + windowTitle += ": " + (question || mode).replace(/^\w/, c => c.toUpperCase()); + } + + return ( + + + + {!!lower_admin && ( +
+ + +
+ )} + + +
+
+
+ ); +}; + +// Gives access to starting votes +const VoteOptions = (props, context) => { + const { act, data } = useBackend(context); + const { + allow_vote_mode, + allow_vote_restart, + allow_vote_map, + upper_admin, + } = data; + + return ( + + + + + + + {!!upper_admin && ( + act("toggle_map")}> + {allow_vote_map ? "Enabled" : "Disabled"} + + )} + + + + {!!upper_admin && ( + act("toggle_restart")}> + {allow_vote_restart ? "Enabled" : "Disabled"} + + )} + + + + {!!upper_admin && ( + act("toggle_gamemode")}> + {allow_vote_mode ? "Enabled" : "Disabled"} + + )} + + + + + + + + + + + ); +}; + +// Table to view voters by ckey +const VotersList = (props, context) => { + const { data } = useBackend(context); + const { voting } = data; + + return ( + + +
+ {voting.map(voter => { + return {voter}; + })} +
+
+
+ ); +}; + +// Display choices +const ChoicesPanel = (props, context) => { + const { act, data } = useBackend(context); + const { choices, selected_choice } = data; + + return ( + +
+ {choices.length !== 0 ? ( + + {choices.map((choice, i) => ( + + c.toUpperCase())} + textAlign="right" + buttons={ + + }> + {i === selected_choice - 1 && ( + + )} + {choice.votes} Votes + + + + ))} + + ) : ( + No choices available! + )} +
+
+ ); +}; + +// Countdown timer at the bottom. Includes a cancel vote option for admins +const TimePanel = (props, context) => { + const { act, data } = useBackend(context); + const { upper_admin, time_remaining } = data; + + return ( + +
+ + + Time Remaining: {time_remaining || 0}s + + {!!upper_admin && ( + + )} + +
+
+ ); +};