diff --git a/code/modules/asset_cache/asset_list_items.dm b/code/modules/asset_cache/asset_list_items.dm index d4ab7b5648..a8b4c93764 100644 --- a/code/modules/asset_cache/asset_list_items.dm +++ b/code/modules/asset_cache/asset_list_items.dm @@ -388,3 +388,9 @@ Insert("polycrystal", 'icons/obj/telescience.dmi', "polycrystal") ..() +/datum/asset/spritesheet/mafia + name = "mafia" + +/datum/asset/spritesheet/mafia/register() + InsertAll("", 'icons/obj/mafia.dmi') + ..() diff --git a/code/modules/mafia/_defines.dm b/code/modules/mafia/_defines.dm index b862ce63c5..3833d5aefe 100644 --- a/code/modules/mafia/_defines.dm +++ b/code/modules/mafia/_defines.dm @@ -32,6 +32,8 @@ GLOBAL_LIST_EMPTY(mafia_signup) //the current global mafia game running. GLOBAL_VAR(mafia_game) +/// list of ghosts who want to play mafia that have since disconnected. They are kept in the lobby, but not counted for starting a game. +GLOBAL_LIST_EMPTY(mafia_bad_signup) GLOBAL_LIST_INIT(mafia_setups,generate_mafia_setups()) diff --git a/code/modules/mafia/controller.dm b/code/modules/mafia/controller.dm index 172f9dca50..8c4006ef90 100644 --- a/code/modules/mafia/controller.dm +++ b/code/modules/mafia/controller.dm @@ -6,6 +6,8 @@ * It is first created when the first ghost signs up to play. */ /datum/mafia_controller + ///list of observers that should get game updates. + var/list/spectators = list() ///all roles in the game, dead or alive. check their game status if you only want living or dead. var/list/all_roles = list() ///exists to speed up role retrieval, it's a dict. player_role_lookup[player ckey] will give you the role they play @@ -48,7 +50,8 @@ ///group voting on one person, like putting people to trial or choosing who to kill as mafia var/list/votes = list() - ///and these (judgement_innocent_votes and judgement_guilty_votes) are the judgement phase votes, aka people sorting themselves into guilty and innocent lists. whichever has more wins! + ///and these (judgement_innocent_votes, judgement_abstain_votes and judgement_guilty_votes) are the judgement phase votes, aka people sorting themselves into guilty and innocent, and "eh, i don't really care" lists. whichever has more inno or guilty wins! + var/list/judgement_abstain_votes = list() var/list/judgement_innocent_votes = list() var/list/judgement_guilty_votes = list() ///current role on trial for the judgement phase, will die if guilty is greater than innocent @@ -129,7 +132,7 @@ var/team_suffix = team ? "([uppertext(team)] CHAT)" : "" for(var/M in GLOB.dead_mob_list) var/mob/spectator = M - if(spectator.ckey in GLOB.mafia_signup || player_role_lookup[spectator.mind.current] != null) //was in current game, or is signed up + if(spectator.ckey in spectators) //was in current game, or spectatin' (won't send to living) var/link = FOLLOW_LINK(M, town_center_landmark) to_chat(M, "[link] MAFIA: [msg] [team_suffix]") @@ -190,8 +193,19 @@ */ /datum/mafia_controller/proc/check_trial(verbose = TRUE) var/datum/mafia_role/loser = get_vote_winner("Day")//, majority_of_town = TRUE) + var/loser_votes = get_vote_count(loser,"Day") if(loser) + // if(loser_votes > 12) + // loser.body.client?.give_award(/datum/award/achievement/mafia/universally_hated, loser.body) send_message("[loser.body.real_name] wins the day vote, Listen to their defense and vote \"INNOCENT\" or \"GUILTY\"!") + //refresh the lists + judgement_abstain_votes = list() + judgement_innocent_votes = list() + judgement_guilty_votes = list() + for(var/i in all_roles) + var/datum/mafia_role/abstainee = i + if(abstainee.game_status == MAFIA_ALIVE && abstainee != loser) + judgement_abstain_votes += abstainee on_trial = loser on_trial.body.forceMove(get_turf(town_center_landmark)) phase = MAFIA_PHASE_JUDGEMENT @@ -215,8 +229,11 @@ for(var/i in judgement_innocent_votes) var/datum/mafia_role/role = i send_message("[role.body.real_name] voted innocent.") - for(var/ii in judgement_guilty_votes) + for(var/ii in judgement_abstain_votes) var/datum/mafia_role/role = ii + send_message("[role.body.real_name] abstained.") + for(var/iii in judgement_guilty_votes) + var/datum/mafia_role/role = iii send_message("[role.body.real_name] voted guilty.") if(judgement_guilty_votes.len > judgement_innocent_votes.len) //strictly need majority guilty to lynch send_message("Guilty wins majority, [on_trial.body.real_name] has been lynched.") @@ -225,9 +242,6 @@ else send_message("Innocent wins majority, [on_trial.body.real_name] has been spared.") on_trial.body.forceMove(get_turf(on_trial.assigned_landmark)) - //by now clowns should have killed someone in guilty list, clear this out - judgement_innocent_votes = list() - judgement_guilty_votes = list() on_trial = null //day votes are already cleared, so this will skip the trial and check victory/lockdown/whatever else next_phase_timer = addtimer(CALLBACK(src, .proc/check_trial, FALSE),judgement_lynch_period,TIMER_STOPPABLE)// small pause to see the guy dead, no verbosity since we already did this @@ -320,10 +334,8 @@ /** * Cleans up the game, resetting variables back to the beginning and removing the map with the generator. */ -/datum/mafia_controller/proc/end_game() - +/datum/mafia_controller/proc/end_game( map_deleter.generate() //remove the map, it will be loaded at the start of the next one - QDEL_LIST(all_roles) turn = 0 votes = list() @@ -481,7 +493,7 @@ if(phase != MAFIA_PHASE_VOTING) return var/v = get_vote_count(player_role_lookup[source],"Day") - var/mutable_appearance/MA = mutable_appearance('icons/obj/mafia.dmi',"vote_[v]") + var/mutable_appearance/MA = mutable_appearance('icons/obj/mafia.dmi',"vote_[v > 12 ? "over_12" : v]") overlay_list += MA /** @@ -528,13 +540,26 @@ .["judgement_phase"] = FALSE var/datum/mafia_role/user_role = player_role_lookup[user] if(user_role) - .["roleinfo"] = list("role" = user_role.name,"desc" = user_role.desc, "action_log" = user_role.role_notes) + .["roleinfo"] = list("role" = user_role.name,"desc" = user_role.desc, "action_log" = user_role.role_notes, "hud_icon" = user_role.hud_icon, "revealed_icon" = user_role.revealed_icon) var/actions = list() for(var/action in user_role.actions) if(user_role.validate_action_target(src,action,null)) actions += action .["actions"] = actions .["role_theme"] = user_role.special_theme + else + var/list/lobby_data = list() + for(var/key in GLOB.mafia_signup + GLOB.mafia_bad_signup) + var/list/lobby_member = list() + lobby_member["name"] = key + lobby_member["status"] = "Ready" + if(key in GLOB.mafia_bad_signup) + lobby_member["status"] = "Disconnected" + lobby_member["spectating"] = "Ghost" + if(key in spectators) + lobby_member["spectating"] = "Spectator" + lobby_data += list(lobby_member) + .["lobbydata"] = lobby_data var/list/player_data = list() for(var/datum/mafia_role/R in all_roles) var/list/player_info = list() @@ -561,6 +586,11 @@ //Not sure on this, should this info be visible .["all_roles"] = current_setup_text +/datum/mafia_controller/ui_assets(mob/user) + return list( + get_asset_datum(/datum/asset/spritesheet/mafia), + ) + /datum/mafia_controller/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) . = ..() if(.) @@ -606,7 +636,31 @@ helper = role break helper.show_help(usr) - if(!user_role || user_role.game_status == MAFIA_DEAD)//ghosts, dead people? + if(!user_role)//just the dead + var/client/C = ui.user.client + switch(action) + if("mf_signup") + if(!SSticker.HasRoundStarted()) + to_chat(usr, "Wait for the round to start.") + return + if(GLOB.mafia_signup[C.ckey]) + GLOB.mafia_signup -= C.ckey + to_chat(usr, "You unregister from Mafia.") + return + else + GLOB.mafia_signup[C.ckey] = C + to_chat(usr, "You sign up for Mafia.") + if(phase == MAFIA_PHASE_SETUP) + check_signups() + try_autostart() + if("mf_spectate") + if(C.ckey in spectators) + to_chat(usr, "You will no longer get messages from the game.") + spectators -= C.ckey + else + to_chat(usr, "You will now get messages from the game.") + spectators += C.ckey + if(user_role.game_status == MAFIA_DEAD) return var/self_voting = user_role == on_trial ? TRUE : FALSE //used to block people from voting themselves innocent or guilty //User actions @@ -637,20 +691,29 @@ return user_role.handle_action(src,params["atype"],target) return TRUE - if("vote_innocent") - if(phase != MAFIA_PHASE_JUDGEMENT && !self_voting) - return - to_chat(user_role.body,"Your vote on [on_trial.body.real_name] submitted as INNOCENT!") - judgement_innocent_votes -= user_role//no double voting - judgement_guilty_votes -= user_role//no radical centrism - judgement_innocent_votes += user_role - if("vote_guilty") - if(phase != MAFIA_PHASE_JUDGEMENT && !self_voting) - return - to_chat(user_role.body,"Your vote on [on_trial.body.real_name] submitted as GUILTY!") - judgement_innocent_votes -= user_role//no radical centrism - judgement_guilty_votes -= user_role//no double voting - judgement_guilty_votes += user_role + if(user_role != on_trial) + switch(action) + if("vote_abstain") + if(phase != MAFIA_PHASE_JUDGEMENT || (user_role in judgement_abstain_votes)) + return + to_chat(user_role.body,"You have decided to abstain.") + judgement_innocent_votes -= user_role + judgement_guilty_votes -= user_role + judgement_abstain_votes += user_role + if("vote_innocent") + if(phase != MAFIA_PHASE_JUDGEMENT || (user_role in judgement_innocent_votes)) + return + to_chat(user_role.body,"Your vote on [on_trial.body.real_name] submitted as INNOCENT!") + judgement_abstain_votes -= user_role//no fakers, and... + judgement_guilty_votes -= user_role//no radical centrism + judgement_innocent_votes += user_role + if("vote_guilty") + if(phase != MAFIA_PHASE_JUDGEMENT || (user_role in judgement_guilty_votes)) + return + to_chat(user_role.body,"Your vote on [on_trial.body.real_name] submitted as GUILTY!") + judgement_abstain_votes -= user_role//no fakers, and... + judgement_innocent_votes -= user_role//no radical centrism + judgement_guilty_votes += user_role /datum/mafia_controller/ui_state(mob/user) return GLOB.always_state @@ -699,7 +762,7 @@ //cuts invalid players from signups (disconnected/not a ghost) var/list/possible_keys = list() for(var/key in GLOB.mafia_signup) - if(GLOB.directory[key] && GLOB.directory[key] == GLOB.mafia_signup[key]) + if(GLOB.directory[key]) var/client/C = GLOB.directory[key] if(isobserver(C.mob)) possible_keys += key @@ -739,6 +802,25 @@ if(GLOB.mafia_signup.len >= min_players)//enough people to try and make something basic_setup() +/** + * Filters inactive player into a different list until they reconnect, and removes players who are no longer ghosts. + * + * If a disconnected player gets a non-ghost mob and reconnects, they will be first put back into mafia_signup then filtered by that. + */ +/datum/mafia_controller/proc/check_signups() + for(var/bad_key in GLOB.mafia_bad_signup) + if(GLOB.directory[bad_key])//they have reconnected if we can search their key and get a client + GLOB.mafia_bad_signup -= bad_key + GLOB.mafia_signup += bad_key + for(var/key in GLOB.mafia_signup) + var/client/C = GLOB.directory[key] + if(!C)//vice versa but in a variable we use later + GLOB.mafia_signup -= key + GLOB.mafia_bad_signup += key + if(!isobserver(C.mob)) + //they are back to playing the game, remove them from the signups + GLOB.mafia_signup -= key + /datum/action/innate/mafia_panel name = "Mafia Panel" desc = "Use this to play." diff --git a/code/modules/mafia/roles.dm b/code/modules/mafia/roles.dm index 394d121005..914cbb0bd3 100644 --- a/code/modules/mafia/roles.dm +++ b/code/modules/mafia/roles.dm @@ -19,7 +19,12 @@ var/game_status = MAFIA_ALIVE - var/special_theme //set this to something cool for antagonists and their window will look different + ///icon state in the mafia dmi of the hud of the role, used in the mafia ui + var/hud_icon = "hudassistant" + ///icon state in the mafia dmi of the hud of the role, used in the mafia ui + var/revealed_icon = "assistant" + ///set this to something cool for antagonists and their window will look different + var/special_theme var/list/role_notes = list() @@ -34,6 +39,8 @@ body.death() if(lynch) reveal_role(game, verbose = TRUE) + if(!(player_key in game.spectators)) //people who played will want to see the end of the game more often than not + game.spectators += player_key return TRUE /datum/mafia_role/Destroy(force, ...) @@ -108,6 +115,9 @@ desc = "You can investigate a single person each night to learn their team." revealed_outfit = /datum/outfit/mafia/detective + hud_icon = "huddetective" + revealed_icon = "detective" + targeted_actions = list("Investigate") var/datum/mafia_role/current_investigation @@ -152,7 +162,8 @@ name = "Medical Doctor" desc = "You can protect a single person each night from killing." revealed_outfit = /datum/outfit/mafia/md // /mafia <- outfit must be readded (just make a new mafia outfits file for all of these) - + hud_icon = "hudmedicaldoctor" + revealed_icon = "medicaldoctor" targeted_actions = list("Protect") var/datum/mafia_role/current_protected @@ -194,7 +205,8 @@ name = "Chaplain" desc = "You can communicate with spirits of the dead each night to discover dead crewmember roles." revealed_outfit = /datum/outfit/mafia/chaplain - + hud_icon = "hudchaplain" + revealed_icon = "chaplain" targeted_actions = list("Pray") var/current_target @@ -222,8 +234,9 @@ /datum/mafia_role/lawyer name = "Lawyer" desc = "You can choose a person during the day to provide extensive legal advice to during the night, preventing night actions." - revealed_outfit = /datum/outfit/mafia/lawyer + hud_icon = "hudlawyer" + revealed_icon = "lawyer" targeted_actions = list("Advise") var/datum/mafia_role/current_target @@ -278,6 +291,9 @@ desc = "You can visit someone ONCE PER GAME to reveal their true role in the morning!" revealed_outfit = /datum/outfit/mafia/psychologist + hud_icon = "hudpsychologist" + revealed_icon = "psychologist" + targeted_actions = list("Reveal") var/datum/mafia_role/current_target var/can_use = TRUE @@ -313,6 +329,8 @@ desc = "You're a member of the changeling hive. Use ':j' talk prefix to talk to your fellow lings." team = MAFIA_TEAM_MAFIA revealed_outfit = /datum/outfit/mafia/changeling + hud_icon = "hudchangeling" + revealed_icon = "changeling" special_theme = "syndicate" win_condition = "become majority over the town and no solo killing role can stop them." @@ -332,7 +350,10 @@ team = MAFIA_TEAM_SOLO targeted_actions = list("Night Kill") revealed_outfit = /datum/outfit/mafia/traitor - special_theme = "syndicate" + + hud_icon = "hudtraitor" + revealed_icon = "traitor" + special_theme = "neutral" var/datum/mafia_role/current_victim @@ -384,6 +405,9 @@ var/protection_status = FUGITIVE_NOT_PRESERVING solo_counts_as_town = TRUE //should not count towards mafia victory, they should have the option to work with town revealed_outfit = /datum/outfit/mafia/fugitive + special_theme = "neutral" + hud_icon = "hudfugitive" + revealed_icon = "fugitive" /datum/mafia_role/fugitive/New(datum/mafia_controller/game) . = ..() @@ -434,7 +458,9 @@ win_condition = "lynch their obsession." team = MAFIA_TEAM_SOLO revealed_outfit = /datum/outfit/mafia/obsessed // /mafia <- outfit must be readded (just make a new mafia outfits file for all of these) - + special_theme = "neutral" + hud_icon = "hudobsessed" + revealed_icon = "obsessed" solo_counts_as_town = TRUE //after winning or whatever, can side with whoever. they've already done their objective! var/datum/mafia_role/obsession var/lynched_target = FALSE @@ -470,10 +496,14 @@ /datum/mafia_role/clown name = "Clown" - desc = "If you are lynched you take down one of your voters with you and win. HONK!" + desc = "If you are lynched you take down one of your voters (guilty or abstain) with you and win. HONK!" win_condition = "get themselves lynched!" revealed_outfit = /datum/outfit/mafia/clown + solo_counts_as_town = TRUE team = MAFIA_TEAM_SOLO + special_theme = "neutral" + hud_icon = "hudclown" + revealed_icon = "clown" /datum/mafia_role/clown/New(datum/mafia_controller/game) . = ..() @@ -481,7 +511,7 @@ /datum/mafia_role/clown/proc/prank(datum/source,datum/mafia_controller/game,lynch) if(lynch) - var/datum/mafia_role/victim = pick(game.judgement_guilty_votes) + var/datum/mafia_role/victim = pick(game.judgement_guilty_votes + game.judgement_abstain_votes) game.send_message("[body.real_name] WAS A CLOWN! HONK! They take down [victim.body.real_name] with their last prank.") game.send_message("!! CLOWN VICTORY !!") victim.kill(game,FALSE) diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm index bb39639ec1..a0df1ee938 100644 --- a/code/modules/mob/dead/observer/observer.dm +++ b/code/modules/mob/dead/observer/observer.dm @@ -905,6 +905,22 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp else to_chat(usr, "Can't become a pAI candidate while not dead!") +/mob/dead/observer/verb/mafia_game_signup() + set category = "Ghost" + set name = "Signup for Mafia" + set desc = "Sign up for a game of Mafia to pass the time while dead." + mafia_signup() +/mob/dead/observer/proc/mafia_signup() + if(!client) + return + if(!isobserver(src)) + to_chat(usr, "You must be a ghost to join mafia!") + return + var/datum/mafia_controller/game = GLOB.mafia_game //this needs to change if you want multiple mafia games up at once. + if(!game) + game = create_mafia_game("mafia") + game.ui_interact(usr) + /mob/dead/observer/CtrlShiftClick(mob/user) if(isobserver(user) && check_rights(R_SPAWN)) change_mob_type( /mob/living/carbon/human , null, null, TRUE) //always delmob, ghosts shouldn't be left lingering diff --git a/icons/obj/mafia.dmi b/icons/obj/mafia.dmi index fc0426a19f..c44b80aba1 100644 Binary files a/icons/obj/mafia.dmi and b/icons/obj/mafia.dmi differ diff --git a/tgui/packages/tgui/assets/bg-neutral.svg b/tgui/packages/tgui/assets/bg-neutral.svg new file mode 100644 index 0000000000..1c397616e8 --- /dev/null +++ b/tgui/packages/tgui/assets/bg-neutral.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/tgui/packages/tgui/index.js b/tgui/packages/tgui/index.js index e4ec6d285d..69092ce00f 100644 --- a/tgui/packages/tgui/index.js +++ b/tgui/packages/tgui/index.js @@ -22,6 +22,7 @@ import './styles/themes/abductor.scss'; import './styles/themes/cardtable.scss'; import './styles/themes/hackerman.scss'; import './styles/themes/malfunction.scss'; +import './styles/themes/neutral.scss'; import './styles/themes/ntos.scss'; import './styles/themes/paper.scss'; import './styles/themes/retro.scss'; diff --git a/tgui/packages/tgui/interfaces/MafiaPanel.js b/tgui/packages/tgui/interfaces/MafiaPanel.js index e3c2c893ec..3616781fc5 100644 --- a/tgui/packages/tgui/interfaces/MafiaPanel.js +++ b/tgui/packages/tgui/interfaces/MafiaPanel.js @@ -1,11 +1,14 @@ +import { classes } from 'common/react'; import { Fragment } from 'inferno'; +import { multiline } from 'common/string'; import { useBackend } from '../backend'; -import { Box, Button, Flex, LabeledList, Section, TimeDisplay } from '../components'; +import { Box, Button, Collapsible, Flex, NoticeBox, Section, TimeDisplay, Tooltip } from '../components'; import { Window } from '../layouts'; export const MafiaPanel = (props, context) => { const { act, data } = useBackend(context); const { + lobbydata, players, actions, phase, @@ -16,29 +19,124 @@ export const MafiaPanel = (props, context) => { timeleft, all_roles, } = data; + const playerAddedHeight = roleinfo ? players.length * 30 : 7; + const readyGhosts = lobbydata ? lobbydata.filter( + player => player.status === "Ready") : null; return ( - -
- {!!roleinfo && ( - - - + width={650} // 414 or 415 / 444 or 445 + height={293 + playerAddedHeight}> + + {!roleinfo && ( + +
+ }> + + + The lobby currently has {readyGhosts.length} + /12 valid players signed up. + + + {!!lobbydata && lobbydata.map(lobbyist => ( + + + + {lobbyist.name} + + + STATUS: + + +
+ + {lobbyist.status} {lobbyist.spectating} + +
+
+
+
+ ))} +
+
+
+ )} + {!!roleinfo && ( +
- You are a {roleinfo.role} + {!!admin_controls && ( +
+ + + + + + +
+ )} {!!actions && actions.map(action => ( @@ -49,116 +147,323 @@ export const MafiaPanel = (props, context) => { ))} - {!!admin_controls && ( + {!!roleinfo && (
- THESE ARE DEBUG, THEY WILL BREAK THE GAME, DO NOT TOUCH
- Also because an admin did it: do not gib/delete/etc - anyone! It will runtime the game to death!
- - - -
- -
- )} -
- - {!!players && players.map(player => ( - - {!player.alive && (DEAD)} - {player.votes !== undefined && !!player.alive - && (Votes : {player.votes} )} - { - !!player.actions && player.actions.map(action => { - return ( - ); }) - } - ) - )} - -
- {!!judgement_phase && ( -
+ title="Judgement" + buttons={ + - Use these buttons to vote the accused innocent or guilty! + disabled={!judgement_phase} + onClick={() => act("vote_innocent")} /> + {!judgement_phase && ( + + There is nobody on trial at the moment. + + )} + {!!judgement_phase && ( + + It is now time to vote, vote the accused innocent or guilty! + + )} + disabled={!judgement_phase} + onClick={() => act("vote_guilty")} /> + + +
)} - - -
- {!!all_roles && all_roles.map(r => ( - - - {r} - ); }) + } + + + ) + )} + +
+
+ + +
+
-
- -
- {roleinfo !== undefined && !!roleinfo.action_log - && roleinfo.action_log.map(log_line => ( - - {log_line} - - ))} -
+ + {!!roleinfo && ( + +
+ {roleinfo !== undefined && !!roleinfo.action_log + && roleinfo.action_log.map(log_line => ( + + {log_line} + + ))} +
+
+ )} +
+ + + )} + + + {!!admin_controls && ( +
+ +
+ )}
); }; + +const LobbyDisplay = (props, context) => { + const { act, data } = useBackend(context); + const { + phase, + timeleft, + admin_controls, + } = data; + return ( + + [Phase = {phase} | ]{' '} +