diff --git a/code/datums/antagonists/antag_datum.dm b/code/datums/antagonists/antag_datum.dm
index 2464d5a4584..9d84349609f 100644
--- a/code/datums/antagonists/antag_datum.dm
+++ b/code/datums/antagonists/antag_datum.dm
@@ -1,27 +1,33 @@
-var/global/list/antagonists = list()
+GLOBAL_LIST_EMPTY(antagonists)
/datum/antagonist
var/name = "Antagonist"
+ var/roundend_category = "other antagonists" //Section of roundend report, datums with same category will be displayed together, also default header for the section
+ var/show_in_roundend = TRUE //Set to false to hide the antagonists from roundend report
var/datum/mind/owner //Mind that owns this datum
var/silent = FALSE //Silent will prevent the gain/lose texts to show
var/can_coexist_with_others = TRUE //Whether or not the person will be able to have more than one datum
var/list/typecache_datum_blacklist = list() //List of datums this type can't coexist with
var/delete_on_mind_deletion = TRUE
- var/antag_role //the antag type
+ var/job_rank
var/replace_banned = TRUE //Should replace jobbaned player with ghosts if granted.
var/list/objectives = list()
var/antag_memory = ""//These will be removed with antag datum
+ var/antag_moodlet //typepath of moodlet that the mob will gain with their status
+
+ //Antag panel properties
+ var/show_in_antagpanel = TRUE //This will hide adding this antag type in antag panel, use only for internal subtypes that shouldn't be added directly but still show if possessed by mind
+ var/antagpanel_category = "Uncategorized" //Antagpanel will display these together, REQUIRED
+ var/show_name_in_check_antagonists = FALSE //Will append antagonist name in admin listings - use for categories that share more than one antag type
-/datum/antagonist/New(datum/mind/new_owner)
- antagonists += src
+/datum/antagonist/New()
+ GLOB.antagonists += src
typecache_datum_blacklist = typecacheof(typecache_datum_blacklist)
- if(new_owner)
- owner = new_owner
/datum/antagonist/Destroy()
- antagonists -= src
+ GLOB.antagonists -= src
if(owner)
LAZYREMOVE(owner.antag_datums, src)
owner = null
@@ -37,6 +43,11 @@ var/global/list/antagonists = list()
if(is_type_in_typecache(src, A.typecache_datum_blacklist))
return FALSE
+//This will be called in add_antag_datum before owner assignment.
+//Should return antag datum without owner.
+/datum/antagonist/proc/specialization(datum/mind/new_owner)
+ return src
+
/datum/antagonist/proc/on_body_transfer(mob/living/old_body, mob/living/new_body)
remove_innate_effects(old_body)
apply_innate_effects(new_body)
@@ -50,7 +61,7 @@ var/global/list/antagonists = list()
return
//Assign default team and creates one for one of a kind team antagonists
-/datum/antagonist/proc/create_team(datum/objective_team/team)
+/datum/antagonist/proc/create_team(datum/team/team)
return
//Proc called when the datum is given to a mind.
@@ -65,27 +76,27 @@ var/global/list/antagonists = list()
/datum/antagonist/proc/is_banned(mob/M)
if(!M)
return FALSE
- . = (jobban_isbanned(M,"Syndicate") || (antag_role && jobban_isbanned(M,antag_role)))
+ . = (jobban_isbanned(M, ROLE_SYNDICATE) || (job_rank && jobban_isbanned(M,job_rank)))
/datum/antagonist/proc/replace_banned_player()
set waitfor = FALSE
- var/list/mob/dead/observer/candidates = pollCandidates("Do you want to play as a [name]?", "[name]", null, antag_role, 0, 100)
- var/mob/dead/observer/theghost = null
- if(candidates.len)
- theghost = pick(candidates)
- to_chat(owner, " Your mob has been taken over by a ghost! Appeal your role ban if you want to avoid this in the future!")
- message_admins("[key_name_admin(theghost)] has taken control of ([key_name_admin(owner.current)]) to replace a jobbaned player.")
- log_admin("[key_name_admin(theghost)] has taken control of ([key_name_admin(owner.current)]) to replace a jobbaned player.")
+ var/list/mob/dead/observer/candidates = pollCandidatesForMob("Do you want to play as a [name]?", "[name]", null, job_rank, 50, owner.current)
+ if(LAZYLEN(candidates))
+ var/mob/dead/observer/C = pick(candidates)
+ to_chat(owner, "Your mob has been taken over by a ghost! Appeal your job ban if you want to avoid this in the future!")
+ message_admins("[key_name_admin(C)] has taken control of ([key_name_admin(owner.current)]) to replace a jobbaned player.")
owner.current.ghostize(0)
- owner.current.key = theghost.key
+ owner.current.key = C.key
/datum/antagonist/proc/on_removal()
remove_innate_effects()
+ clear_antag_moodies()
if(owner)
LAZYREMOVE(owner.antag_datums, src)
if(!silent && owner.current)
farewell()
+ owner.objectives -= objectives
var/datum/team/team = get_team()
if(team)
team.remove_member(owner)
@@ -94,9 +105,136 @@ var/global/list/antagonists = list()
/datum/antagonist/proc/greet()
return
+/datum/antagonist/proc/farewell()
+ return
+
+
//Returns the team antagonist belongs to if any.
/datum/antagonist/proc/get_team()
return
-/datum/antagonist/proc/farewell()
- return
\ No newline at end of file
+//Individual roundend report
+/datum/antagonist/proc/roundend_report()
+ var/list/report = list()
+
+ if(!owner)
+ CRASH("antagonist datum without owner")
+
+ report += printplayer(owner)
+
+ var/objectives_complete = TRUE
+ if(owner.objectives.len)
+ report += printobjectives(owner)
+ for(var/datum/objective/objective in owner.objectives)
+ if(!objective.check_completion())
+ objectives_complete = FALSE
+ break
+
+ if(owner.objectives.len == 0 || objectives_complete)
+ report += "The [name] was successful!"
+ else
+ report += "The [name] has failed!"
+
+ return report.Join("
")
+
+//Displayed at the start of roundend_category section, default to roundend_category header
+/datum/antagonist/proc/roundend_report_header()
+ return "
"
+
+//Displayed at the end of roundend_category section
+/datum/antagonist/proc/roundend_report_footer()
+ return
+
+
+//ADMIN TOOLS
+
+//Called when using admin tools to give antag status
+/datum/antagonist/proc/admin_add(datum/mind/new_owner,mob/admin)
+ message_admins("[key_name_admin(admin)] made [new_owner.current] into [name].")
+ log_admin("[key_name(admin)] made [new_owner.current] into [name].")
+ new_owner.add_antag_datum(src)
+
+//Called when removing antagonist using admin tools
+/datum/antagonist/proc/admin_remove(mob/user)
+ if(!user)
+ return
+ message_admins("[key_name_admin(user)] has removed [name] antagonist status from [owner.current].")
+ log_admin("[key_name(user)] has removed [name] antagonist status from [owner.current].")
+ on_removal()
+
+//gamemode/proc/is_mode_antag(antagonist/A) => TRUE/FALSE
+
+//Additional data to display in antagonist panel section
+//nuke disk code, genome count, etc
+/datum/antagonist/proc/antag_panel_data()
+ return ""
+
+/datum/antagonist/proc/enabled_in_preferences(datum/mind/M)
+ if(job_rank)
+ if(M.current && M.current.client && (job_rank in M.current.client.prefs.be_special))
+ return TRUE
+ else
+ return FALSE
+ return TRUE
+
+// List if["Command"] = CALLBACK(), user will be appeneded to callback arguments on execution
+/datum/antagonist/proc/get_admin_commands()
+ . = list()
+
+/datum/antagonist/Topic(href,href_list)
+ if(!check_rights(R_ADMIN))
+ return
+ //Antag memory edit
+ if(href_list["memory_edit"])
+ edit_memory(usr)
+ owner.traitor_panel()
+ return
+
+ //Some commands might delete/modify this datum clearing or changing owner
+ var/datum/mind/persistent_owner = owner
+
+ 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)
+ persistent_owner.traitor_panel()
+ return
+
+/datum/antagonist/proc/edit_memory(mob/user)
+ var/new_memo = copytext(trim(input(user,"Write new memory", "Memory", antag_memory) as null|message),1,MAX_MESSAGE_LEN)
+ if(isnull(new_memo))
+ return
+ antag_memory = new_memo
+
+//This datum will autofill the name with special_role
+//Used as placeholder for minor antagonists, please create proper datums for these
+/datum/antagonist/auto_custom
+ show_in_antagpanel = FALSE
+ antagpanel_category = "Other"
+ show_name_in_check_antagonists = TRUE
+
+/datum/antagonist/auto_custom/on_gain()
+ ..()
+ name = owner.special_role
+ //Add all objectives not already owned by other datums to this one.
+ var/list/already_registered_objectives = list()
+ for(var/datum/antagonist/A in owner.antag_datums)
+ if(A == src)
+ continue
+ else
+ already_registered_objectives |= A.objectives
+ objectives = owner.objectives - already_registered_objectives
+
+//This one is created by admin tools for custom objectives
+/datum/antagonist/custom
+ antagpanel_category = "Custom"
+ show_name_in_check_antagonists = TRUE //They're all different
+
+/datum/antagonist/custom/admin_add(datum/mind/new_owner,mob/admin)
+ var/custom_name = stripped_input(admin, "Custom antagonist name:", "Custom antag", "Antagonist")
+ if(custom_name)
+ name = custom_name
+ else
+ return
+ ..()
\ No newline at end of file
diff --git a/code/datums/antagonists/antag_helpers.dm b/code/datums/antagonists/antag_helpers.dm
new file mode 100644
index 00000000000..d99920b9e21
--- /dev/null
+++ b/code/datums/antagonists/antag_helpers.dm
@@ -0,0 +1,19 @@
+//Returns MINDS of the assigned antags of given type/subtypes
+/proc/get_antag_minds(antag_type,specific = FALSE)
+ . = list()
+ for(var/datum/antagonist/A in GLOB.antagonists)
+ if(!A.owner)
+ 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
\ No newline at end of file
diff --git a/code/datums/antagonists/antag_team.dm b/code/datums/antagonists/antag_team.dm
new file mode 100644
index 00000000000..372ee26dfad
--- /dev/null
+++ b/code/datums/antagonists/antag_team.dm
@@ -0,0 +1,34 @@
+//A barebones antagonist team.
+/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.
+
+/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/proc/is_solo()
+ return members.len == 1
+
+/datum/team/proc/add_member(datum/mind/new_member)
+ members |= new_member
+
+/datum/team/proc/remove_member(datum/mind/member)
+ members -= member
+
+//Display members/victory/failure/objectives for the team
+/datum/team/proc/roundend_report()
+ var/list/report = list()
+
+ report += "[name]:"
+ report += "The [member_name]s were:"
+ report += printplayerlist(members)
+
+ return report.Join("
")
\ No newline at end of file
diff --git a/paradise.dme b/paradise.dme
index f03d37b701c..0a486bdddf9 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -228,6 +228,8 @@
#include "code\datums\vision_override.dm"
#include "code\datums\vr.dm"
#include "code\datums\antagonists\antag_datum.dm"
+#include "code\datums\antagonists\antag_helpers.dm"
+#include "code\datums\antagonists\antag_team.dm"
#include "code\datums\cache\air_alarm.dm"
#include "code\datums\cache\apc.dm"
#include "code\datums\cache\cache.dm"