tgui: Vote Panel (#57775)

This upgrades the voting panel to TGUI which makes it much more user friendly and adds some new functionality:

- Users can now change votes
- The screen no longer scrolls to top when someone else votes
- Admins can see ckeys of users with the voting window open
- Fancy icons and vote tallies in a much better format
- Map list is now shuffled.
- All the old functionality remains for custom votes, etc.

Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com>
Co-authored-by: Mothblocks <35135081+Mothblocks@users.noreply.github.com>
Co-authored-by: Aleksej Komarov <stylemistake@gmail.com>
This commit is contained in:
Jeremiah
2021-03-22 01:30:41 +02:00
committed by GitHub
co-authored by LemonInTheDark Mothblocks Aleksej Komarov
parent f2e5285535
commit 90ed4672a6
2 changed files with 310 additions and 145 deletions
+116 -145
View File
@@ -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, "<span style='boldannounce'>Notice:Restart vote will not restart the server automatically because there are active admins on.</span>")
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, "<span class='warning'>Cannot start vote, server is not done initializing.</span>")
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, "<span class='warning'>There is already a vote in progress! please wait for it to finish.</span>")
return FALSE
if(next_allowed_time > world.time && !admin)
if(next_allowed_time > world.time && !lower_admin)
to_chat(usr, "<span class='warning'>A vote was initiated recently, you must wait [DisplayTimeText(next_allowed_time-world.time)] before a new vote can be started!</span>")
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, "<span class='warning'>The next map has already been selected.</span>")
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<font color='purple'><b>[text]</b>\nType <b>vote</b> or click <a href='?src=[REF(src)]'>here</a> to place your votes.\nYou have [DisplayTimeText(vp)] to vote.</font>")
to_chat(world, "\n<font color='purple'><b>[text]</b>\nType <b>vote</b> or click <a href='byond://winset?command=vote'>here</a> to place your votes.\nYou have [DisplayTimeText(vp)] to vote.</font>")
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)
. += "<h2>Vote: '[question]'</h2>"
else
. += "<h2>Vote: [capitalize(mode)]</h2>"
. += "Time Left: [time_remaining] s<hr><ul>"
for(var/i=1,i<=choices.len,i++)
var/votes = choices[choices[i]]
if(!votes)
votes = 0
. += "<li><a href='?src=[REF(src)];vote=[i]'>[choices[i]]</a> ([votes] votes)</li>"
. += "</ul><hr>"
if(admin)
. += "(<a href='?src=[REF(src)];vote=cancel'>Cancel Vote</a>) "
else
. += "<h2>Start a vote:</h2><hr><ul><li>"
//restart
var/avr = CONFIG_GET(flag/allow_vote_restart)
if(trialmin || avr)
. += "<a href='?src=[REF(src)];vote=restart'>Restart</a>"
else
. += "<font color='grey'>Restart (Disallowed)</font>"
if(trialmin)
. += "\t(<a href='?src=[REF(src)];vote=toggle_restart'>[avr ? "Allowed" : "Disallowed"]</a>)"
. += "</li><li>"
//gamemode
var/avm = CONFIG_GET(flag/allow_vote_mode)
if(trialmin || avm)
. += "<a href='?src=[REF(src)];vote=gamemode'>GameMode</a>"
else
. += "<font color='grey'>GameMode (Disallowed)</font>"
if(trialmin)
. += "\t(<a href='?src=[REF(src)];vote=toggle_gamemode'>[avm ? "Allowed" : "Disallowed"]</a>)"
. += "</li>"
//map
var/avmap = CONFIG_GET(flag/allow_vote_map)
if(trialmin || avmap)
. += "<a href='?src=[REF(src)];vote=map'>Map</a>"
else
. += "<font color='grey'>Map (Disallowed)</font>"
if(trialmin)
. += "\t(<a href='?src=[REF(src)];vote=toggle_map'>[avmap ? "Allowed" : "Disallowed"]</a>)"
. += "</li>"
//custom
if(trialmin)
. += "<li><a href='?src=[REF(src)];vote=custom'>Custom</a></li>"
. += "</ul><hr>"
. += "<a href='?src=[REF(src)];vote=close' style='position:absolute;right:50px'>Close</a>"
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!"
+194
View File
@@ -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 (
<Window resizable title={windowTitle} width={400} height={500}>
<Window.Content>
<Stack fill vertical>
{!!lower_admin && (
<Section title="Admin Options">
<VoteOptions />
<VotersList />
</Section>
)}
<ChoicesPanel />
<TimePanel />
</Stack>
</Window.Content>
</Window>
);
};
// 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 (
<Stack.Item>
<Collapsible title="Start a Vote">
<Stack justify="space-between">
<Stack.Item>
<Stack vertical>
<Stack.Item>
{!!upper_admin && (
<Button.Checkbox
mr={!allow_vote_map ? 1 : 1.6}
color="red"
checked={!!allow_vote_map}
onClick={() => act("toggle_map")}>
{allow_vote_map ? "Enabled" : "Disabled"}
</Button.Checkbox>
)}
<Button
disabled={!upper_admin || !allow_vote_map}
onClick={() => act("map")}>
Map
</Button>
</Stack.Item>
<Stack.Item>
{!!upper_admin && (
<Button.Checkbox
mr={!allow_vote_restart ? 1 : 1.6}
color="red"
checked={!!allow_vote_restart}
onClick={() => act("toggle_restart")}>
{allow_vote_restart ? "Enabled" : "Disabled"}
</Button.Checkbox>
)}
<Button
disabled={!upper_admin || !allow_vote_restart}
onClick={() => act("restart")}>
Restart
</Button>
</Stack.Item>
<Stack.Item>
{!!upper_admin && (
<Button.Checkbox
mr={!allow_vote_mode ? 1 : 1.6}
color="red"
checked={!!allow_vote_mode}
onClick={() => act("toggle_gamemode")}>
{allow_vote_mode ? "Enabled" : "Disabled"}
</Button.Checkbox>
)}
<Button
disabled={!upper_admin || !allow_vote_mode}
onClick={() => act("gamemode")}>
Gamemode
</Button>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item>
<Button disabled={!upper_admin} onClick={() => act("custom")}>
Create Custom Vote
</Button>
</Stack.Item>
</Stack>
</Collapsible>
</Stack.Item>
);
};
// Table to view voters by ckey
const VotersList = (props, context) => {
const { data } = useBackend(context);
const { voting } = data;
return (
<Stack.Item>
<Collapsible title={`View Voters: ${voting.length}`}>
<Section height={8} fill scrollable>
{voting.map(voter => {
return <Box key={voter}>{voter}</Box>;
})}
</Section>
</Collapsible>
</Stack.Item>
);
};
// Display choices
const ChoicesPanel = (props, context) => {
const { act, data } = useBackend(context);
const { choices, selected_choice } = data;
return (
<Stack.Item grow>
<Section fill scrollable title="Choices">
{choices.length !== 0 ? (
<LabeledList>
{choices.map((choice, i) => (
<Box key={choice.id}>
<LabeledList.Item
label={choice.name.replace(/^\w/, c => c.toUpperCase())}
textAlign="right"
buttons={
<Button
disabled={i === selected_choice - 1}
onClick={() => {
act("vote", { index: i + 1 });
}}>
Vote
</Button>
}>
{i === selected_choice - 1 && (
<Icon
alignSelf="right"
mr={2}
color="green"
name="vote-yea" />
)}
{choice.votes} Votes
</LabeledList.Item>
<LabeledList.Divider />
</Box>
))}
</LabeledList>
) : (
<NoticeBox>No choices available!</NoticeBox>
)}
</Section>
</Stack.Item>
);
};
// 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 (
<Stack.Item mt={1}>
<Section>
<Stack justify="space-between">
<Box fontSize={1.5}>
Time Remaining: {time_remaining || 0}s
</Box>
{!!upper_admin && (
<Button color="red" onClick={() => act('cancel')}>
Cancel Vote
</Button>
)}
</Stack>
</Section>
</Stack.Item>
);
};