diff --git a/code/__HELPERS/global_lists.dm b/code/__HELPERS/global_lists.dm index 9ce17b7530c..ba215f55130 100644 --- a/code/__HELPERS/global_lists.dm +++ b/code/__HELPERS/global_lists.dm @@ -172,6 +172,12 @@ if(initial(D.name)) GLOB.keybindings += new path() + for(var/path in subtypesof(/datum/objective)) + var/datum/objective/O = path + if(isnull(initial(O.name))) + continue // These are not valid objectives to add. + GLOB.admin_objective_list[initial(O.name)] = path + /* // Uncomment to debug chemical reaction list. /client/verb/debug_chemical_list() diff --git a/code/game/gamemodes/miniantags/abduction/abduction.dm b/code/game/gamemodes/miniantags/abduction/abduction.dm index 04f9738745d..bdf8449fa6b 100644 --- a/code/game/gamemodes/miniantags/abduction/abduction.dm +++ b/code/game/gamemodes/miniantags/abduction/abduction.dm @@ -50,7 +50,7 @@ team_names[team_number] = "Mothership [pick(GLOB.possible_changeling_IDs)]" //TODO Ensure unique and actual alieny names //Team Objective var/datum/objective/experiment/team_objective = new - team_objective.abductor_team = team_number + team_objective.abductor_team_number = team_number team_objectives[team_number] = team_objective //Team Members @@ -238,7 +238,8 @@ // OBJECTIVES /datum/objective/experiment target_amount = 6 - var/abductor_team + /// Which abductor team number does this belong to. + var/abductor_team_number /datum/objective/stay_hidden @@ -251,7 +252,7 @@ explanation_text = "Experiment on [target_amount] humans." /datum/objective/experiment/check_completion() - var/ab_team = abductor_team + var/ab_team = abductor_team_number var/list/owners = get_owners() for(var/datum/mind/M in owners) if(!M.current || !ishuman(M.current) || !isabductor(M.current)) diff --git a/code/game/gamemodes/objective.dm b/code/game/gamemodes/objective.dm index 2d6b8ac9c63..723ae80033a 100644 --- a/code/game/gamemodes/objective.dm +++ b/code/game/gamemodes/objective.dm @@ -1,8 +1,17 @@ +/// Stores a reference to every [objective][/datum/objective] which currently exists. GLOBAL_LIST_EMPTY(all_objectives) +// Used in admin procs to give them a pretty list to look at, and to also have sane reusable code. +/// Stores objective [names][/datum/objective/var/name] as list keys, and their corresponding typepaths as list values. +GLOBAL_LIST_EMPTY(admin_objective_list) GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective) - /datum/theft_objective/steal - /datum/theft_objective/number - /datum/theft_objective/unique)) /datum/objective + /** + * Proper name of the objective. Not player facing, only shown to admins when adding objectives. + * Leave as null (or override to null) if you don't want admins to see that objective as a viable one to add (such as the mindslave objective). + */ + var/name /** * Owner of the objective. * Note that it's fine to set this directly, but when needing to check completion of the objective or otherwise check conditions on the owner of the objective, @@ -76,13 +85,13 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective) return ..() -/datum/objective/proc/find_target() +/datum/objective/proc/find_target(list/target_blacklist) if(!needs_target) return var/list/possible_targets = list() for(var/datum/mind/possible_target in SSticker.minds) - if(is_invalid_target(possible_target)) + if(is_invalid_target(possible_target) || (possible_target in target_blacklist)) continue possible_targets += possible_target @@ -117,9 +126,10 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective) M.announce_objectives() /datum/objective/assassinate + name = "Assassinate" martyr_compatible = 1 -/datum/objective/assassinate/find_target() +/datum/objective/assassinate/find_target(list/target_blacklist) ..() if(target && target.current) explanation_text = "Assassinate [target.current.real_name], the [target.assigned_role]." @@ -140,9 +150,10 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective) /datum/objective/mutiny + name = "Mutiny" martyr_compatible = 1 -/datum/objective/mutiny/find_target() +/datum/objective/mutiny/find_target(list/target_blacklist) ..() if(target && target.current) explanation_text = "Assassinate [target.current.real_name], the [target.assigned_role]." @@ -166,9 +177,10 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective) qdel(src) /datum/objective/maroon + name = "Maroon" martyr_compatible = 1 -/datum/objective/maroon/find_target() +/datum/objective/maroon/find_target(list/target_blacklist) ..() if(target && target.current) explanation_text = "Prevent [target.current.real_name], the [target.assigned_role] from escaping alive." @@ -194,9 +206,10 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective) /datum/objective/debrain //I want braaaainssss + name = "Debrain" martyr_compatible = 0 -/datum/objective/debrain/find_target() +/datum/objective/debrain/find_target(list/target_blacklist) ..() if(target && target.current) explanation_text = "Steal the brain of [target.current.real_name] the [target.assigned_role]." @@ -219,9 +232,10 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective) /datum/objective/protect //The opposite of killing a dude. + name = "Protect" martyr_compatible = 1 -/datum/objective/protect/find_target() +/datum/objective/protect/find_target(list/target_blacklist) ..() if(target && target.current) explanation_text = "Protect [target.current.real_name], the [target.assigned_role]." @@ -256,6 +270,7 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective) qdel(src) /datum/objective/hijack + name = "Hijack" martyr_compatible = 0 //Technically you won't get both anyway. explanation_text = "Hijack the shuttle by escaping on it with no loyalist Nanotrasen crew on board and free. \ Syndicate agents, other enemies of Nanotrasen, cyborgs, pets, and cuffed/restrained hostages may be allowed on the shuttle alive. \ @@ -271,6 +286,7 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective) return SSshuttle.emergency.is_hijacked() /datum/objective/hijackclone + name = "Hijack (with clones)" explanation_text = "Hijack the shuttle by ensuring only you (or your copies) escape." martyr_compatible = 0 needs_target = FALSE @@ -303,6 +319,7 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective) return FALSE /datum/objective/block + name = "Silicon hijack" explanation_text = "Hijack the shuttle with no loyalist Nanotrasen crew on board and free. \ Syndicate agents, other enemies of Nanotrasen, cyborgs, pets, and cuffed/restrained hostages may be allowed on the shuttle alive. \ Using the doomsday device successfully is also an option." @@ -322,6 +339,7 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective) return TRUE /datum/objective/escape + name = "Escape" explanation_text = "Escape on the shuttle or an escape pod alive and free." needs_target = FALSE @@ -350,6 +368,7 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective) /datum/objective/escape/escape_with_identity + name = null /// Stored because the target's `[mob/var/real_name]` can change over the course of the round. var/target_real_name /// If the objective has an assassinate objective tied to it. @@ -371,7 +390,7 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective) // If the target is geneless, then it's an invalid target. return HAS_TRAIT(possible_target.current, TRAIT_GENELESS) -/datum/objective/escape/escape_with_identity/find_target() +/datum/objective/escape/escape_with_identity/find_target(list/target_blacklist) ..() if(target && target.current) target_real_name = target.current.real_name @@ -413,6 +432,7 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective) return FALSE /datum/objective/die + name = "Die a glorious death" explanation_text = "Die a glorious death." needs_target = FALSE @@ -428,6 +448,7 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective) /datum/objective/survive + name = "Survive" explanation_text = "Stay alive until the end." needs_target = FALSE @@ -440,11 +461,13 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective) return TRUE /datum/objective/nuclear + name = "Nuke station" explanation_text = "Destroy the station with a nuclear device." martyr_compatible = 1 needs_target = FALSE /datum/objective/steal + name = "Steal Item" var/datum/theft_objective/steal_target martyr_compatible = 0 var/theft_area @@ -452,7 +475,7 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective) /datum/objective/steal/proc/get_location() return steal_target.location_override || "an unknown area" -/datum/objective/steal/find_target() +/datum/objective/steal/find_target(list/target_blacklist) var/potential = GLOB.potential_theft_objectives.Copy() while(!steal_target && length(potential)) var/thefttype = pick_n_take(potential) @@ -531,6 +554,7 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective) /datum/objective/absorb + name = "Absorb DNA" needs_target = FALSE /datum/objective/absorb/proc/gen_amount_goal(lowbound = 4, highbound = 6) @@ -562,10 +586,11 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective) return FALSE /datum/objective/destroy + name = "Destroy AI" martyr_compatible = 1 var/target_real_name -/datum/objective/destroy/find_target() +/datum/objective/destroy/find_target(list/target_blacklist) var/list/possible_targets = active_ais(1) var/mob/living/silicon/ai/target_ai = pick(possible_targets) target = target_ai.mind @@ -584,6 +609,7 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective) return TRUE /datum/objective/steal_five_of_type + name = "Steal Five Items" explanation_text = "Steal at least five items!" needs_target = FALSE var/list/wanted_items = list() @@ -606,10 +632,12 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective) return stolen_count >= 5 /datum/objective/steal_five_of_type/summon_guns + name = "Steal Five Guns" explanation_text = "Steal at least five guns!" wanted_items = list(/obj/item/gun) /datum/objective/steal_five_of_type/summon_magic + name = "Steal Five Artefacts" explanation_text = "Steal at least five magical artefacts!" wanted_items = list() @@ -639,6 +667,7 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective) return stolen_count >= 5 /datum/objective/blood + name = "Spread blood" needs_target = FALSE /datum/objective/blood/New() diff --git a/code/modules/admin/player_panel.dm b/code/modules/admin/player_panel.dm index 46ed68bf5b6..33cf379e115 100644 --- a/code/modules/admin/player_panel.dm +++ b/code/modules/admin/player_panel.dm @@ -363,6 +363,8 @@ dat += "ETA: [(timeleft / 60) % 60]:[add_zero(num2text(timeleft % 60), 2)]
" dat += "[SSticker.delay_end ? "End Round Normally" : "Delay Round End"]
" + dat += "
Antagonist Teams
" + dat += "View Teams
" if(SSticker.mode.syndicates.len) dat += "
" for(var/datum/mind/N in SSticker.mode.syndicates) diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index 5d4a7be0e42..352f24886e1 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -1609,6 +1609,49 @@ else if(href_list["check_antagonist"]) check_antagonists() + else if(href_list["check_teams"]) + if(!check_rights(R_ADMIN)) + return + check_teams() + + else if(href_list["team_command"]) + if(!check_rights(R_ADMIN)) + return + var/datum/team/team + var/datum/mind/member + if(href_list["team"]) + team = locateUID(href_list["team"]) + if(QDELETED(team)) + to_chat(usr, "This team doesn't exist anymore!") + return + if(href_list["member"]) + member = locateUID(href_list["member"]) + if(QDELETED(member)) + to_chat(usr, "This team member doesn't exist anymore!") + return + switch(href_list["team_command"]) + if("communicate") + team.admin_communicate(usr) + if("delete_team") + message_admins("[key_name_admin(usr)] has deleted the '[team.name]' team.") + log_admin("[key_name_admin(usr)] has deleted the '[team.name]' team.") + qdel(team) + if("rename_team") + team.admin_rename_team(usr) + if("admin_add_member") + team.admin_add_member(usr) + if("remove_member") + team.admin_remove_member(usr, member) + if("view_member") + show_player_panel(member.current) + if("add_objective") + team.admin_add_objective(usr) + if("remove_objective") + var/datum/objective/O = locateUID(href_list["objective"]) + if(O) + team.admin_remove_objective(usr, O) + check_teams() + else if(href_list["take_question"]) var/index = text2num(href_list["take_question"]) diff --git a/code/modules/antagonists/_common/antag_datum.dm b/code/modules/antagonists/_common/antag_datum.dm index 2c7503f4a4b..81053f72146 100644 --- a/code/modules/antagonists/_common/antag_datum.dm +++ b/code/modules/antagonists/_common/antag_datum.dm @@ -44,7 +44,10 @@ GLOBAL_LIST_EMPTY(antagonists) assigned_targets = list() /datum/antagonist/Destroy(force, ...) - QDEL_LIST(objectives) + for(var/datum/objective/O as anything in objectives) + objectives -= O + if(!O.team) + qdel(O) remove_owner_from_gamemode() GLOB.antagonists -= src if(!silent) diff --git a/code/modules/antagonists/_common/antag_helpers.dm b/code/modules/antagonists/_common/antag_helpers.dm index af00cbf3190..42b84ddeec6 100644 --- a/code/modules/antagonists/_common/antag_helpers.dm +++ b/code/modules/antagonists/_common/antag_helpers.dm @@ -6,14 +6,3 @@ continue if(!antag_type || !specific && istype(A, antag_type) || specific && A.type == antag_type) . += A.owner - -//Get all teams [of type team_type] -/proc/get_all_teams(team_type) - . = list() - for(var/V in GLOB.antagonists) - var/datum/antagonist/A = V - if(!A.owner) - continue - var/datum/team/T = A.get_team() - if(!team_type || istype(T, team_type)) - . |= T diff --git a/code/modules/antagonists/_common/antag_team.dm b/code/modules/antagonists/_common/antag_team.dm index 9dcec5894ee..1b4b0a47cf7 100644 --- a/code/modules/antagonists/_common/antag_team.dm +++ b/code/modules/antagonists/_common/antag_team.dm @@ -1,24 +1,217 @@ -//A barebones antagonist team. +GLOBAL_LIST_EMPTY(antagonist_teams) + +/** + * # Antagonist Team + * + * Datum used by team antagonists to track it's members and objectives the team needs to complete. + */ /datum/team - var/list/datum/mind/members = list() - var/name = "team" - var/member_name = "member" - var/list/objectives = list() //common objectives, these won't be added or removed automatically, subtypes handle this, this is here for bookkeeping purposes. + /// The name of the team. + var/name = "Generic Team Name" + /// A list of [minds][/datum/mind] who belong to this team. + var/list/members + /// A list of objectives which all team members share. + var/list/objectives + /// Type of antag datum members of this team have. Also given to new members added by admins. + var/antag_datum_type -/datum/team/New(starting_members) - . = ..() - if(starting_members) - if(islist(starting_members)) - for(var/datum/mind/M in starting_members) - add_member(M) - else - add_member(starting_members) +/datum/team/New(list/starting_members) + ..() + members = list() + objectives = list() + if(starting_members && !islist(starting_members)) + starting_members = list(starting_members) + for(var/datum/mind/M as anything in starting_members) + add_member(M) + GLOB.antagonist_teams += src -/datum/team/proc/is_solo() - return members.len == 1 +/datum/team/Destroy(force = FALSE, ...) + for(var/datum/mind/member as anything in members) + remove_member(member) + QDEL_LIST(objectives) + members.Cut() + GLOB.antagonist_teams -= src + return ..() +/** + * Adds `new_member` to this team. + * + * Generally this should ONLY be called by `add_antag_datum()` to ensure proper order of operations. + */ /datum/team/proc/add_member(datum/mind/new_member) + SHOULD_CALL_PARENT(TRUE) + var/datum/antagonist/team_antag = get_antag_datum_from_member(new_member) members |= new_member + team_antag.objectives |= objectives +/** + * Removes `member` from this team. + */ /datum/team/proc/remove_member(datum/mind/member) + SHOULD_CALL_PARENT(TRUE) + var/datum/antagonist/A = get_antag_datum_from_member(member) members -= member + A.objectives -= objectives + +/** + * Adds a new member to this team from a list of players in the round. + */ +/datum/team/proc/admin_add_member(mob/user) + var/list/valid_minds = list() + for(var/mob/living/carbon/human/H in GLOB.player_list) + if(!H.mind || (H.mind in members)) + continue + valid_minds[H.real_name] = H.mind + + var/name = input(user, "Choose a player to add to this team", "Add Team Member") as null|anything in valid_minds + if(!name) + to_chat(user, "No suitable humanoid targets found!") + return + + var/datum/mind/new_member = valid_minds[name] + add_member(new_member) + +/** + * Adds a team objective to each member's matching antag datum. + */ +/datum/team/proc/add_objective_to_members(datum/objective/O) + for(var/datum/mind/M as anything in members) + var/datum/antagonist/A = get_antag_datum_from_member(M) + A.objectives |= O + +/** + * Remove a team objective from each member's matching antag datum. + */ +/datum/team/proc/remove_objective_from_members(datum/objective/O) + for(var/datum/mind/M as anything in members) + var/datum/antagonist/A = get_antag_datum_from_member(M) + A.objectives -= O + objectives -= O + qdel(O) + +/** + * Return an antag datum from a member which is linked with this team. + */ +/datum/team/proc/get_antag_datum_from_member(datum/mind/member) + for(var/datum/antagonist/A as anything in member.antag_datums) + if(A.get_team() != src) + continue + return A + // If no matching antag datum was found, give them one. + if(antag_datum_type) + member.add_antag_datum(antag_datum_type, src) + +/** + * Allows admins to send a message to all members of this team. + */ +/datum/team/proc/admin_communicate(mob/user) + var/message = input(user, "Enter a message to send to the team:", "Team Message") as text|null + if(!message) + return + + for(var/datum/mind/M as anything in members) + to_chat(M.current, "Admin Team Message ([user.key]): [message]") + + message_admins("Team Message: [key_name(user)] -> '[name]' team. Message: [message]") + log_admin("Team Message: [key_name(user)] -> '[name]' team. Message: [message]") + +/** + * Allows admins to add a team objective. + */ +/datum/team/proc/admin_add_objective(mob/user) + var/selected = input("Select an objective type:", "Objective Type") as null|anything in GLOB.admin_objective_list + if(!selected) + return + + var/objective_type = GLOB.admin_objective_list[selected] + var/datum/objective/O = new objective_type(team_to_join = src) + O.find_target(members) // Blacklist any team members from being the target. + objectives |= O + add_objective_to_members(O) + + message_admins("[key_name_admin(user)] added objective [O.type] to the team '[name]'.") + log_admin("[key_name(user)] added objective [O.type] to the team '[name]'.") + +/** + * Allows admins to remove a team objective. + */ +/datum/team/proc/admin_remove_objective(mob/user, datum/objective/O) + remove_objective_from_members(O) + message_admins("[key_name_admin(user)] removed objective [O.type] from the team '[name]'.") + log_admin("[key_name(user)] removed objective [O.type] from the team '[name]'.") + +/** + * Allows admins to rename the team. + */ +/datum/team/proc/admin_rename_team(mob/user) + var/team_name = stripped_input(user, "Enter a new name for the team:", "Rename Team") + if(!team_name) + return + + message_admins("[key_name_admin(user)] renamed the '[name]' team to '[team_name]'.") + log_admin("[key_name(user)] renamed the '[name]' team to '[team_name]'.") + name = team_name + +/** + * Allows admins to remove a team member. + */ +/datum/team/proc/admin_remove_member(mob/user, datum/mind/M) + message_admins("[key_name_admin(user)] removed [key_name_admin(M)] from the team '[name]'.") + log_admin("[key_name(user)] removed [key_name(M)] from the team '[name]'.") + remove_member(M) + +// Used for running team specific admin commands. +/datum/team/Topic(href, href_list) + if(!check_rights(R_ADMIN)) + return + + var/commands = get_admin_commands() + for(var/admin_command in commands) + if(href_list["command"] == admin_command) + var/datum/callback/C = commands[admin_command] + C.Invoke(usr) + return + +/** + * A list of team-specific admin commands for this team. Should be in the form of `"command" = CALLBACK(x, PROC_REF(some_proc))`. + */ +/datum/team/proc/get_admin_commands() + return list() + +/** + * Opens a window which lists the teams for the round. + */ +/datum/admins/proc/check_teams() + if(!SSticker.HasRoundStarted()) + to_chat(usr, "The game hasn't started yet!") + return + + var/datum/browser/popup = new(usr, "teams", "Team List", 500, 500) + popup.set_content(list_teams()) + popup.open() + +/** + * Returns HTML content for the "check teams" window. + */ +/datum/admins/proc/list_teams() + var/list/content = list() + if(!length(GLOB.antagonist_teams)) + content += "There are currently no antag teams." + for(var/datum/team/T as anything in GLOB.antagonist_teams) + content += "

[T.name] - [T.type]

" + content += "Rename Team" + content += "Delete Team" + content += "Message Team" + for(var/command in T.get_admin_commands()) + // _src_ is T.UID() so it points to `/datum/team/Topic` instead of `/datum/admins/Topic`. + content += "[command]" + content += "

Objectives:
    " + for(var/datum/objective/O as anything in T.objectives) + content += "
  1. [O.explanation_text] - Remove
  2. " + content += "
Add Objective

" + content += "Members:
    " + for(var/datum/mind/M as anything in T.members) + content += "
  1. [M.name] - Show Player Panel" + content += "Remove Member
  2. " + content += "
Add Member
" + return content.Join()
Syndicates