From 0f5272e62219c907524e095d5d0b53ebd31d2783 Mon Sep 17 00:00:00 2001 From: CHOMPStation2 <58959929+CHOMPStation2@users.noreply.github.com> Date: Thu, 3 Oct 2024 16:09:08 -0700 Subject: [PATCH] [MIRROR] TGUI Vote Panel (#9131) Co-authored-by: Heroman3003 <31296024+Heroman3003@users.noreply.github.com> Co-authored-by: Kashargul --- code/__defines/misc.dm | 4 + code/controllers/autotransfer.dm | 4 +- code/controllers/subsystems/SSvote.dm | 16 ++ code/controllers/subsystems/ticker.dm | 10 +- code/controllers/subsystems/vote.dm | 2 + code/controllers/vote.dm | 2 + code/modules/mob/new_player/new_player.dm | 5 +- code/modules/vote/vote_datum.dm | 200 ++++++++++++++++++++ code/modules/vote/vote_presets.dm | 14 ++ code/modules/vote/vote_verb.dm | 56 ++++++ tgui/packages/tgui/interfaces/VotePanel.tsx | 44 +++++ vorestation.dme | 4 + 12 files changed, 352 insertions(+), 9 deletions(-) create mode 100644 code/controllers/subsystems/SSvote.dm create mode 100644 code/modules/vote/vote_datum.dm create mode 100644 code/modules/vote/vote_presets.dm create mode 100644 code/modules/vote/vote_verb.dm create mode 100644 tgui/packages/tgui/interfaces/VotePanel.tsx diff --git a/code/__defines/misc.dm b/code/__defines/misc.dm index d7eb31a0ed..b1e3c7cd60 100644 --- a/code/__defines/misc.dm +++ b/code/__defines/misc.dm @@ -491,3 +491,7 @@ GLOBAL_LIST_INIT(all_volume_channels, list( #define SPECIES_SORT_WHITELISTED 2 #define SPECIES_SORT_RESTRICTED 3 #define SPECIES_SORT_CUSTOM 4 + +// Vote Types +#define VOTE_RESULT_TYPE_MAJORITY "Majority" +#define VOTE_RESULT_TYPE_SKEWED "Seventy" diff --git a/code/controllers/autotransfer.dm b/code/controllers/autotransfer.dm index 9443f592ed..a4324a8309 100644 --- a/code/controllers/autotransfer.dm +++ b/code/controllers/autotransfer.dm @@ -19,13 +19,13 @@ var/datum/controller/transfer_controller/transfer_controller currenttick = currenttick + 1 //VOREStation Edit START if (round_duration_in_ds >= shift_last_vote - 2 MINUTES) - shift_last_vote = 1000000000000 //Setting to a stupidly high number since it'll be not used again. + shift_last_vote = 1000000000000 //Setting to a stupidly high number since it'll be not used again. //CHOMPEdit to_world("Warning: This upcoming round-extend vote will be your last chance to vote for shift extension. Wrap up your scenes in the next 60 minutes if the round is extended.") //CHOMPStation Edit if (round_duration_in_ds >= shift_hard_end - 1 MINUTE) init_shift_change(null, 1) shift_hard_end = timerbuffer + CONFIG_GET(number/vote_autotransfer_interval) //If shuttle somehow gets recalled, let's force it to call again next time a vote would occur. // CHOMPEdit timerbuffer = timerbuffer + CONFIG_GET(number/vote_autotransfer_interval) //Just to make sure a vote doesn't occur immediately afterwords. // CHOMPEdit else if (round_duration_in_ds >= timerbuffer - 1 MINUTE) - SSvote.autotransfer() + new /datum/vote/crew_transfer //VOREStation Edit END timerbuffer = timerbuffer + CONFIG_GET(number/vote_autotransfer_interval) // CHOMPEdit diff --git a/code/controllers/subsystems/SSvote.dm b/code/controllers/subsystems/SSvote.dm new file mode 100644 index 0000000000..5ccaa7c551 --- /dev/null +++ b/code/controllers/subsystems/SSvote.dm @@ -0,0 +1,16 @@ +SUBSYSTEM_DEF(vote) + name = "Vote" + wait = 10 + priority = FIRE_PRIORITY_VOTE + runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT + flags = SS_KEEP_TIMING | SS_NO_INIT + + var/datum/vote/active_vote + +/datum/controller/subsystem/vote/fire() + if(active_vote) + active_vote.tick() + +/datum/controller/subsystem/vote/proc/start_vote(datum/vote/V) + active_vote = V + active_vote.start() diff --git a/code/controllers/subsystems/ticker.dm b/code/controllers/subsystems/ticker.dm index 2a6ecd4e80..9a9670a61a 100644 --- a/code/controllers/subsystems/ticker.dm +++ b/code/controllers/subsystems/ticker.dm @@ -87,7 +87,7 @@ var/global/datum/controller/subsystem/ticker/ticker if(start_immediately) pregame_timeleft = 0 - else if(SSvote.time_remaining) + else if(SSvote.active_vote) return // vote still going, wait for it. // Time to start the game! @@ -98,8 +98,8 @@ var/global/datum/controller/subsystem/ticker/ticker fire() // Don't wait for next tick, do it now! return - if(pregame_timeleft <= CONFIG_GET(number/vote_autogamemode_timeleft) && !SSvote.gamemode_vote_called) - SSvote.autogamemode() // Start the game mode vote (if we haven't had one already) + //if(pregame_timeleft <= CONFIG_GET(number/vote_autogamemode_timeleft) && !SSvote.gamemode_vote_called) //CHOMPEdit + //SSvote.autogamemode() // Start the game mode vote (if we haven't had one already) //CHOMPEdit // Called during GAME_STATE_SETTING_UP (RUNLEVEL_SETUP) /datum/controller/subsystem/ticker/proc/setup_tick(resumed = FALSE) @@ -107,7 +107,7 @@ var/global/datum/controller/subsystem/ticker/ticker if(!setup_choose_gamemode()) // It failed, go back to lobby state and re-send the welcome message pregame_timeleft = CONFIG_GET(number/pregame_time) // CHOMPEdit - SSvote.gamemode_vote_called = FALSE // Allow another autogamemode vote + // SSvote.gamemode_vote_called = FALSE // Allow another autogamemode vote current_state = GAME_STATE_PREGAME Master.SetRunLevel(RUNLEVEL_LOBBY) pregame_welcome() @@ -229,7 +229,7 @@ var/global/datum/controller/subsystem/ticker/ticker mode.cleanup() //call a transfer shuttle vote to_world("The round has ended!") - SSvote.autotransfer() + new /datum/vote/crew_transfer // Called during GAME_STATE_FINISHED (RUNLEVEL_POSTGAME) /datum/controller/subsystem/ticker/proc/post_game_tick() diff --git a/code/controllers/subsystems/vote.dm b/code/controllers/subsystems/vote.dm index 65c2f20e61..e173fbee7b 100644 --- a/code/controllers/subsystems/vote.dm +++ b/code/controllers/subsystems/vote.dm @@ -1,3 +1,4 @@ +/* SUBSYSTEM_DEF(vote) name = "Vote" wait = 10 @@ -404,3 +405,4 @@ SUBSYSTEM_DEF(vote) if(SSvote) src << browse(SSvote.interface(src), "window=vote;size=500x[300 + SSvote.choices.len * 25]") +*/ diff --git a/code/controllers/vote.dm b/code/controllers/vote.dm index b4faf11fd0..cad156a483 100644 --- a/code/controllers/vote.dm +++ b/code/controllers/vote.dm @@ -1,3 +1,4 @@ +/* SUBSYSTEM_DEF(vote) name = "Vote" wait = 10 @@ -391,3 +392,4 @@ SUBSYSTEM_DEF(vote) if(SSvote) src << browse(SSvote.interface(src), "window=vote;size=500x[300 + SSvote.choices.len * 25]") +*/ diff --git a/code/modules/mob/new_player/new_player.dm b/code/modules/mob/new_player/new_player.dm index 514ad06310..81524fef4c 100644 --- a/code/modules/mob/new_player/new_player.dm +++ b/code/modules/mob/new_player/new_player.dm @@ -127,6 +127,7 @@ panel.set_content(output) panel.open() return + //CHOMPEdit Begin /mob/new_player/get_status_tab_items() . = ..() @@ -134,8 +135,8 @@ . += "Game Mode: [SSticker.hide_mode ? "Secret" : "[config.mode_names[master_mode]]"]" - if(SSvote.mode) - . += "Vote: [capitalize(SSvote.mode)] Time Left: [SSvote.time_remaining] s" + //if(SSvote.mode) + // . += "Vote: [capitalize(SSvote.mode)] Time Left: [SSvote.time_remaining] s" if(SSticker.current_state == GAME_STATE_INIT) . += "Time To Start: Server Initializing" diff --git a/code/modules/vote/vote_datum.dm b/code/modules/vote/vote_datum.dm new file mode 100644 index 0000000000..87bc80e99c --- /dev/null +++ b/code/modules/vote/vote_datum.dm @@ -0,0 +1,200 @@ +/datum/vote + // Person who started the vote + var/initiator = "the server" + // world.time the bote started at + var/started_time + // The question being asked + var/question + // Vote type text, for showing in UIs and stuff + var/vote_type_text = "unset" + // Do we want to show the vote count as it goes + var/show_counts = FALSE + // Vote result type. This determines how a winner is picked + var/vote_result_type = VOTE_RESULT_TYPE_MAJORITY + // Was this this vote custom started? + var/is_custom = FALSE + // Choices available in the vote + var/list/choices = list() + // Assoc list of [ckeys => choice] who have voted. We don't want to hold clients refs.___callbackvarset(list_or_datum, var_name, var_value) + var/list/voted = list() + // For how long will it be up + var/vote_time = 60 SECONDS + +/datum/vote/New(var/_initiator, var/_question, list/_choices, var/_is_custom = FALSE) + if(SSvote.active_vote) + CRASH("Attempted to start another vote with one already in progress!") + + if(_initiator) + initiator = _initiator + if(_question) + question = _question + if(_choices) + choices = _choices + + is_custom = _is_custom + + // If we have no choices, dynamically generate them + if(!length(choices)) + generate_choices() + +/datum/vote/proc/start() + var/text = "[capitalize(vote_type_text)] vote started by [initiator]." + if(is_custom) + vote_type_text = "custom" + text += "\n[question]" + if(usr) + log_admin("[capitalize(vote_type_text)] ([question]) vote started by [key_name(usr)].") + + else if(usr) + log_admin("[capitalize(vote_type_text)] vote started by [key_name(usr)].") + + log_vote(text) + started_time = world.time + announce(text) + +/datum/vote/proc/remaining() + return max(((started_time + vote_time - world.time)/10), 0) + +/datum/vote/proc/calculate_result() + if(!length(voted)) + to_chat(world, span_interface("No votes were cast. Do you all hate democracy?!")) + return null + + return calculate_vote_result(voted, choices, vote_result_type) + + +/datum/vote/proc/calculate_vote_result(var/list/voted, var/list/choices, var/vote_result_type) + var/list/results = list() + + for(var/ck in voted) + if(voted[ck] in results) + results[voted[ck]]++ + else + results[voted[ck]] = 1 + + var/maxvotes = 0 + for(var/res in results) + maxvotes = max(results[res], maxvotes) + + var/list/winning_options = list() + + for(var/res in results) + if(results[res] == maxvotes) + winning_options |= res + + for(var/res in results) + to_chat(world, span_interface("[res] - [results[res]] vote\s")) + + switch(vote_result_type) + if(VOTE_RESULT_TYPE_MAJORITY) + if(length(winning_options) == 1) + var/res = winning_options[1] + if(res in choices) + to_chat(world, span_interface("[res] won the vote!")) + return res + else + to_chat(world, span_interface("The winner of the vote ([sanitize(res)]) isn't a valid choice? What the heck?")) + stack_trace("Vote concluded with an invalid answer. Answer was [sanitize(res)], choices were [json_encode(choices)]") + return null + + to_chat(world, span_interface("No clear winner. The vote did not pass.")) + return null + + if(VOTE_RESULT_TYPE_SKEWED) + var/required_votes = ceil(length(voted) * 0.7) // 70% of total votes + if(maxvotes >= required_votes && length(winning_options) == 1) + var/res = winning_options[1] + if(res in choices) + to_chat(world, span_interface("[res] won the vote with a 70% majority!")) + return res + else + to_chat(world, span_interface("The winner of the vote ([sanitize(res)]) isn't a valid choice? What the heck?")) + stack_trace("Vote concluded with an invalid answer. Answer was [sanitize(res)], choices were [json_encode(choices)]") + return null + + to_chat(world, span_interface("No option received 70% of the votes. The vote did not pass.")) + return null + + return null + +/datum/vote/proc/announce(start_text, var/time = vote_time) + to_chat(world, span_lightpurple("Type vote or click here to place your vote. \ + You have [time] seconds to vote.")) + world << sound('sound/ambience/alarm4.ogg', repeat = 0, wait = 0, volume = 50, channel = 3) + +/datum/vote/Topic(href, list/href_list) + if(href_list["vote"] == "open") + if(src) + tgui_interact(usr) + else + to_chat(usr, "There is no active vote to participate in.") + +/datum/vote/proc/tick() + if(remaining() == 0) + var/result = calculate_result() + handle_result(result) + qdel(src) + +/datum/vote/Destroy(force) + if(SSvote.active_vote == src) + SSvote.active_vote = null + return ..() + +/datum/vote/proc/handle_result(result) + return + +/datum/vote/proc/generate_choices() + return + +/* + UI STUFFS +*/ + +/datum/vote/tgui_state(mob/user) + return GLOB.tgui_always_state + +/datum/vote/tgui_interact(mob/user, datum/tgui/ui = null) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "VotePanel", "Vote Panel") + ui.open() + +/datum/vote/tgui_data(mob/user) + var/list/data = list() + data["remaining"] = remaining() + data["user_vote"] = null + if(user.ckey in voted) + data["user_vote"] = voted[user.ckey] + + data["question"] = question + data["choices"] = choices + + if(show_counts || check_rights(R_ADMIN, FALSE, user)) + data["show_counts"] = TRUE + + var/list/counts = list() + for(var/ck in voted) + if(voted[ck] in counts) + counts[voted[ck]]++ + else + counts[voted[ck]] = 1 + + data["counts"] = counts + else + data["show_counts"] = FALSE + data["counts"] = list() + + return data + +/datum/vote/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state) + if(..()) + return + + . = TRUE + + switch(action) + if("vote") + if(params["target"] in choices) + voted[usr.ckey] = params["target"] + else + message_admins(span_warning("User [key_name_admin(usr)] spoofed a vote in the vote panel!")) diff --git a/code/modules/vote/vote_presets.dm b/code/modules/vote/vote_presets.dm new file mode 100644 index 0000000000..a7a0829838 --- /dev/null +++ b/code/modules/vote/vote_presets.dm @@ -0,0 +1,14 @@ +/datum/vote/crew_transfer + question = "End the shift" + choices = list("Initiate Crew Transfer", "Extend The Shift") + vote_type_text = "crew transfer" + vote_result_type = VOTE_RESULT_TYPE_SKEWED + +/datum/vote/crew_transfer/New() + if(SSticker.current_state < GAME_STATE_PLAYING) + CRASH("Attempted to call a shutle vote before the game starts!") + ..() + +/datum/vote/crew_transfer/handle_result(result) + if(result == "Initiate Crew Transfer") + init_shift_change(null, TRUE) diff --git a/code/modules/vote/vote_verb.dm b/code/modules/vote/vote_verb.dm new file mode 100644 index 0000000000..3198a8fabd --- /dev/null +++ b/code/modules/vote/vote_verb.dm @@ -0,0 +1,56 @@ +/client/verb/vote() + set category = "OOC" + set name = "Vote" + + if(SSvote.active_vote) + SSvote.active_vote.tgui_interact(usr) + else + to_chat(usr, "There is no active vote") + +/client/proc/start_vote() + set category = "Admin" + set name = "Start Vote" + set desc = "Start a vote on the server" + + if(!is_admin()) + return + + if(SSvote.active_vote) + to_chat(usr, "A vote is already in progress") + return + + var/vote_types = subtypesof(/datum/vote) + vote_types |= "\[CUSTOM]" + + var/list/votemap = list() + for(var/vtype in vote_types) + votemap["[vtype]"] = vtype + + var/choice = tgui_input_list(usr, "Select a vote type", "Vote", vote_types) + + if(choice == null) + return + + if(choice != "\[CUSTOM]") + var/datum/votetype = votemap["[choice]"] + SSvote.start_vote(new votetype(usr.ckey)) + return + + var/question = tgui_input_text(usr, "What is the vote for?", "Create Vote", encode = FALSE) + if(isnull(question)) + return + + var/list/choices = list() + for(var/i in 1 to 10) + var/option = tgui_input_text(usr, "Please enter an option or hit cancel to finish", "Create Vote", encode = FALSE) + if(isnull(option) || !usr.client) + break + choices |= option + + var/c2 = tgui_alert(usr, "Show counts while vote is happening?", "Counts", list("Yes", "No")) + var/c3 = input(usr, "Select a result calculation type", "Vote", VOTE_RESULT_TYPE_MAJORITY) as anything in list(VOTE_RESULT_TYPE_MAJORITY) + + var/datum/vote/V = new /datum/vote(usr.ckey, question, choices, TRUE) + V.show_counts = (c2 == "Yes") + V.vote_result_type = c3 + SSvote.start_vote(V) diff --git a/tgui/packages/tgui/interfaces/VotePanel.tsx b/tgui/packages/tgui/interfaces/VotePanel.tsx new file mode 100644 index 0000000000..f3ef4ff118 --- /dev/null +++ b/tgui/packages/tgui/interfaces/VotePanel.tsx @@ -0,0 +1,44 @@ +import { useBackend } from '../backend'; +import { Box, Button, Section } from '../components'; +import { Window } from '../layouts'; + +type Data = { + remaining: number; + question: string; + choices: string[]; + user_vote: string | null; + counts: Record; + show_counts: boolean; +}; + +export const VotePanel = (props, context) => { + const { act, data } = useBackend(); + const { remaining, question, choices, user_vote, counts, show_counts } = data; + + return ( + + +
+ + Time remaining: {remaining.toFixed(0)}s + + {choices.map((choice, i) => ( + +
+
+
+ ); +}; diff --git a/vorestation.dme b/vorestation.dme index 934572cb18..ab85913f69 100644 --- a/vorestation.dme +++ b/vorestation.dme @@ -400,6 +400,7 @@ #include "code\controllers\subsystems\sounds.dm" #include "code\controllers\subsystems\speech_controller_ch.dm" #include "code\controllers\subsystems\sqlite.dm" +#include "code\controllers\subsystems\SSvote.dm" #include "code\controllers\subsystems\statpanel_ch.dm" #include "code\controllers\subsystems\sun.dm" #include "code\controllers\subsystems\supply.dm" @@ -4554,6 +4555,9 @@ #include "code\modules\vore\resizing\sizegun_vr.dm" #include "code\modules\vore\smoleworld\smoleworld_vr.dm" #include "code\modules\vore\weight\fitness_machines_vr.dm" +#include "code\modules\vote\vote_datum.dm" +#include "code\modules\vote\vote_presets.dm" +#include "code\modules\vote\vote_verb.dm" #include "code\modules\webhooks\_webhook.dm" #include "code\modules\webhooks\webhook_ahelp2discord.dm" #include "code\modules\webhooks\webhook_custom_event.dm"