Split malf AI antag datum away from traitor, enable malf AI roundstart ruleset. (#59064)

Co-authored-by: Mothblocks <35135081+Mothblocks@users.noreply.github.com>
This commit is contained in:
Timberpoes
2021-05-13 02:39:16 -07:00
committed by GitHub
co-authored by Mothblocks
parent 684491701d
commit f36c7fa43f
14 changed files with 554 additions and 265 deletions
+13
View File
@@ -16,6 +16,9 @@ SUBSYSTEM_DEF(job)
var/list/level_order = list(JP_HIGH,JP_MEDIUM,JP_LOW)
/// Lazylist of mob:occupation_string pairs.
var/list/dynamic_forced_occupations
/// A list of all jobs associated with the station. These jobs also have various icons associated with them including sechud and card trims.
var/list/station_jobs
/// A list of all Head of Staff jobs.
@@ -165,6 +168,7 @@ SUBSYSTEM_DEF(job)
if(player.mind && (job.title in player.mind.restricted_roles))
JobDebug("FOC incompatible with antagonist role, Player: [player]")
continue
if(player.client.prefs.job_preferences[job.title] == level)
JobDebug("FOC pass, Player: [player], Level:[level]")
candidates += player
@@ -317,6 +321,10 @@ SUBSYSTEM_DEF(job)
HandleFeedbackGathering()
// Dynamic has picked a ruleset that requires enforcing some jobs before others.
JobDebug("DO, Assigning Priority Positions: [length(dynamic_forced_occupations)]")
assign_priority_positions()
//People who wants to be the overflow role, sure, go on.
JobDebug("DO, Running Overflow Check 1")
var/datum/job/overflow = GetJob(SSjob.overflow_role)
@@ -847,3 +855,8 @@ SUBSYSTEM_DEF(job)
new /obj/effect/pod_landingzone(loc, /obj/structure/closet/supplypod/centcompod, new /obj/item/paper/fluff/emergency_spare_id_safe_code())
safe_code_timer_id = null
safe_code_request_loc = null
/// Blindly assigns the required roles to every player in the dynamic_forced_occupations list.
/datum/controller/subsystem/job/proc/assign_priority_positions()
for(var/mob/new_player in dynamic_forced_occupations)
AssignRole(new_player, dynamic_forced_occupations[new_player])
@@ -0,0 +1,50 @@
/**
* Component that allows for highlighting of words or phrases in chat based on regular expressions.
*
* Hooks into the parent's COMSIG_MOVABLE_HEAR signal to wrap every regex match in the message
* between <span class=''></span> tags with the provided span class. This modifies the output that
* is sent to the parent's chat window.
*
* Removal of this component should be done by calling [GetComponents(/datum/component/codeword_hearing)]
* on the parent and then iterating through all components calling [delete_if_from_source(source)].
*/
/datum/component/codeword_hearing
dupe_mode = COMPONENT_DUPE_ALLOWED
/// Regex for matching words or phrases you want highlighted.
var/regex/replace_regex
/// The <span class=''> to use for highlighting matches.
var/span_class
/// The source of this component. Used to identify the source in delete_if_from_source since this component is COMPONENT_DUPE_ALLOWED.
var/source
/datum/component/codeword_hearing/Initialize(regex/codeword_regex, highlight_span_class, component_source)
if(!ismovable(parent))
return COMPONENT_INCOMPATIBLE
replace_regex = codeword_regex
span_class = highlight_span_class
source = component_source
return ..()
/datum/component/codeword_hearing/RegisterWithParent()
RegisterSignal(parent, COMSIG_MOVABLE_HEAR, .proc/handle_hearing)
/datum/component/codeword_hearing/UnregisterFromParent()
UnregisterSignal(parent, COMSIG_MOVABLE_HEAR)
/// Callback for COMSIG_MOVABLE_HEAR which highlights syndicate code phrases in chat.
/datum/component/codeword_hearing/proc/handle_hearing(datum/source, list/hearing_args)
SIGNAL_HANDLER
var/message = hearing_args[HEARING_RAW_MESSAGE]
message = replace_regex.Replace(message, "<span class='[span_class]'>$1</span>")
hearing_args[HEARING_RAW_MESSAGE] = message
/// Since a parent can have multiple of these components on them simultaneously, this allows a datum to delete components from a specific source.
/datum/component/codeword_hearing/proc/delete_if_from_source(component_source)
if(source == component_source)
qdel(src)
return TRUE
return FALSE
+5 -5
View File
@@ -657,28 +657,28 @@
/datum/mind/proc/take_uplink()
qdel(find_syndicate_uplink())
/datum/mind/proc/make_Traitor()
/datum/mind/proc/make_traitor()
if(!(has_antag_datum(/datum/antagonist/traitor)))
add_antag_datum(/datum/antagonist/traitor)
/datum/mind/proc/make_Contractor_Support()
/datum/mind/proc/make_contractor_support()
if(!(has_antag_datum(/datum/antagonist/traitor/contractor_support)))
add_antag_datum(/datum/antagonist/traitor/contractor_support)
/datum/mind/proc/make_Changeling()
/datum/mind/proc/make_changeling()
var/datum/antagonist/changeling/C = has_antag_datum(/datum/antagonist/changeling)
if(!C)
C = add_antag_datum(/datum/antagonist/changeling)
special_role = ROLE_CHANGELING
return C
/datum/mind/proc/make_Wizard()
/datum/mind/proc/make_wizard()
if(!has_antag_datum(/datum/antagonist/wizard))
special_role = ROLE_WIZARD
assigned_role = ROLE_WIZARD
add_antag_datum(/datum/antagonist/wizard)
/datum/mind/proc/make_Rev()
/datum/mind/proc/make_rev()
var/datum/antagonist/rev/head/head = new()
head.give_flash = TRUE
head.give_hud = TRUE
+32 -14
View File
@@ -26,7 +26,7 @@
var/list/protected_roles = list()
/// If set, rule will deny candidates from those roles always.
var/list/restricted_roles = list()
/// If set, rule will only accept candidates from those roles, IMPORTANT: DOES NOT WORK ON ROUNDSTART RULESETS.
/// If set, rule will only accept candidates from those roles. If on a roundstart ruleset, requires the player to have the correct antag pref enabled and any of the possible roles enabled.
var/list/exclusive_roles = list()
/// If set, there needs to be a certain amount of players doing those roles (among the players who won't be drafted) for the rule to be drafted IMPORTANT: DOES NOT WORK ON ROUNDSTART RULESETS.
var/list/enemy_roles = list()
@@ -190,20 +190,38 @@
/// Checks if candidates are connected and if they are banned or don't want to be the antagonist.
/datum/dynamic_ruleset/roundstart/trim_candidates()
for(var/mob/dead/new_player/P in candidates)
var/client/client = GET_CLIENT(P)
if (!client || !P.mind) // Are they connected?
candidates.Remove(P)
else if(client.get_remaining_days(minimum_required_age) > 0)
candidates.Remove(P)
else if(P.mind.special_role) // We really don't want to give antag to an antag.
candidates.Remove(P)
else if(antag_flag_override)
if(!(antag_flag_override in client.prefs.be_special) || is_banned_from(P.ckey, list(antag_flag_override, ROLE_SYNDICATE)))
candidates.Remove(P)
for(var/mob/dead/new_player/candidate_player in candidates)
var/client/candidate_client = GET_CLIENT(candidate_player)
if (!candidate_client || !candidate_player.mind) // Are they connected?
candidates.Remove(candidate_player)
continue
if(candidate_client.get_remaining_days(minimum_required_age) > 0)
candidates.Remove(candidate_player)
continue
if(candidate_player.mind.special_role) // We really don't want to give antag to an antag.
candidates.Remove(candidate_player)
continue
if(antag_flag_override)
if(!(antag_flag_override in candidate_client.prefs.be_special) || is_banned_from(candidate_player.ckey, list(antag_flag_override, ROLE_SYNDICATE)))
candidates.Remove(candidate_player)
continue
else
if(!(antag_flag in client.prefs.be_special) || is_banned_from(P.ckey, list(antag_flag, ROLE_SYNDICATE)))
candidates.Remove(P)
if(!(antag_flag in candidate_client.prefs.be_special) || is_banned_from(candidate_player.ckey, list(antag_flag, ROLE_SYNDICATE)))
candidates.Remove(candidate_player)
continue
var/exclusive_candidate = FALSE
for(var/role in exclusive_roles)
if(role in candidate_client.prefs.job_preferences)
exclusive_candidate = TRUE
break
if(!exclusive_candidate)
candidates.Remove(candidate_player)
break
/// Do your checks if the ruleset is ready to be executed here.
/// Should ignore certain checks if forced is TRUE
@@ -1,3 +1,8 @@
/// Probability the AI going malf will be accompanied by an ion storm announcement and some ion laws.
#define MALF_ION_PROB 33
/// The probability to replace an existing law with an ion law instead of adding a new ion law.
#define REPLACE_LAW_WITH_ION_PROB 10
//////////////////////////////////////////////
// //
// MIDROUND RULESETS //
@@ -286,12 +291,12 @@
//////////////////////////////////////////////
// //
// Malfunctioning AI //
// //
// //
//////////////////////////////////////////////
/datum/dynamic_ruleset/midround/malf
name = "Malfunctioning AI"
antag_datum = /datum/antagonist/traitor
antag_datum = /datum/antagonist/malf_ai
antag_flag = ROLE_MALF
enemy_roles = list("Security Officer", "Warden","Detective","Head of Security", "Captain", "Scientist", "Chemist", "Research Director", "Chief Engineer")
exclusive_roles = list("AI")
@@ -301,8 +306,6 @@
cost = 35
requirements = list(101,101,80,70,60,60,50,50,40,40)
required_type = /mob/living/silicon/ai
var/ion_announce = 33
var/removeDontImproveChance = 10
/datum/dynamic_ruleset/midround/malf/trim_candidates()
..()
@@ -310,25 +313,29 @@
for(var/mob/living/player in candidates)
if(!isAI(player))
candidates -= player
else if(is_centcom_level(player.z))
continue
if(is_centcom_level(player.z))
candidates -= player
else if(player.mind && (player.mind.special_role || player.mind.antag_datums?.len > 0))
continue
if(player.mind && (player.mind.special_role || player.mind.antag_datums?.len > 0))
candidates -= player
/datum/dynamic_ruleset/midround/malf/execute()
if(!candidates || !candidates.len)
return FALSE
var/mob/living/silicon/ai/M = pick_n_take(candidates)
assigned += M.mind
var/datum/antagonist/traitor/AI = new
M.mind.special_role = antag_flag
M.mind.add_antag_datum(AI)
if(prob(ion_announce))
var/mob/living/silicon/ai/new_malf_ai = pick_n_take(candidates)
assigned += new_malf_ai.mind
var/datum/antagonist/malf_ai/malf_antag_datum = new
new_malf_ai.mind.special_role = antag_flag
new_malf_ai.mind.add_antag_datum(malf_antag_datum)
if(prob(MALF_ION_PROB))
priority_announce("Ion storm detected near the station. Please check all AI-controlled equipment for errors.", "Anomaly Alert", ANNOUNCER_IONSTORM)
if(prob(removeDontImproveChance))
M.replace_random_law(generate_ion_law(), list(LAW_INHERENT, LAW_SUPPLIED, LAW_ION))
if(prob(REPLACE_LAW_WITH_ION_PROB))
new_malf_ai.replace_random_law(generate_ion_law(), list(LAW_INHERENT, LAW_SUPPLIED, LAW_ION))
else
M.add_ion_law(generate_ion_law())
new_malf_ai.add_ion_law(generate_ion_law())
return TRUE
//////////////////////////////////////////////
@@ -697,3 +704,8 @@
/datum/dynamic_ruleset/midround/spiders/execute()
create_midwife_eggs(spawncount)
return ..()
/// Probability the AI going malf will be accompanied by an ion storm announcement and some ion laws.
#undef MALF_ION_PROB
/// The probability to replace an existing law with an ion law instead of adding a new ion law.
#undef REPLACE_LAW_WITH_ION_PROB
@@ -12,7 +12,7 @@
antag_datum = /datum/antagonist/traitor
minimum_required_age = 0
protected_roles = list("Prisoner","Security Officer", "Warden", "Detective", "Head of Security", "Captain")
restricted_roles = list("Cyborg")
restricted_roles = list("AI", "Cyborg")
required_candidates = 1
weight = 5
cost = 8 // Avoid raising traitor threat above 10, as it is the default low cost ruleset.
@@ -40,6 +40,51 @@
log_game("DYNAMIC: Checking if we can turn someone into a traitor.")
mode.picking_specific_rule(/datum/dynamic_ruleset/midround/autotraitor)
//////////////////////////////////////////////
// //
// MALFUNCTIONING AI //
// //
//////////////////////////////////////////////
/datum/dynamic_ruleset/roundstart/malf_ai
name = "Malfunctioning AI"
antag_flag = ROLE_MALF
antag_datum = /datum/antagonist/malf_ai
minimum_required_age = 14
exclusive_roles = list("AI")
required_candidates = 1
weight = 3
cost = 18
requirements = list(101,101,80,70,60,60,50,50,40,40)
antag_cap = 1
flags = HIGH_IMPACT_RULESET
/datum/dynamic_ruleset/roundstart/malf_ai/ready(forced)
var/datum/job/ai_job = SSjob.GetJob("AI")
if(!ai_job)
return FALSE
// If we're not forced, we're going to make sure we can actually have an AI in this shift,
if(!forced && min(ai_job.total_positions - ai_job.current_positions, ai_job.spawn_positions) <= 0)
return FALSE
return ..()
/datum/dynamic_ruleset/roundstart/malf_ai/pre_execute(population)
. = ..()
var/datum/job/ai_job = SSjob.GetJob("AI")
// Maybe a bit too pedantic, but there should never be more malf AIs than there are available positions, spawn positions or antag cap allocations.
var/num_malf = min(get_antag_cap(population), min(ai_job.total_positions - ai_job.current_positions, ai_job.spawn_positions))
for (var/i in 1 to num_malf)
var/mob/new_malf = pick_n_take(candidates)
assigned += new_malf.mind
new_malf.mind.special_role = ROLE_MALF
GLOB.pre_setup_antags += new_malf.mind
// We need an AI for the malf roundstart ruleset to execute. This means that players who get selected as malf AI get priority, because antag selection comes before role selection.
LAZYADDASSOC(SSjob.dynamic_forced_occupations, new_malf, "AI")
return TRUE
//////////////////////////////////////////
// //
// BLOOD BROTHERS //
+34 -9
View File
@@ -282,15 +282,40 @@
"}
break_counter++
output += "</div></div>"
var/list/long_job_lists = list("Service" = GLOB.service_positions,
"Ghost and Other Roles" = list(ROLE_BRAINWASHED, ROLE_DEATHSQUAD, ROLE_DRONE, ROLE_LAVALAND, ROLE_MIND_TRANSFER, ROLE_POSIBRAIN, ROLE_SENTIENCE),
"Antagonist Positions" = list(ROLE_ABDUCTOR, ROLE_ALIEN, ROLE_BLOB,
ROLE_BROTHER, ROLE_CHANGELING, ROLE_CULTIST,
ROLE_INTERNAL_AFFAIRS, ROLE_MALF,
ROLE_MONKEY, ROLE_NINJA, ROLE_OPERATIVE,
ROLE_OVERTHROW, ROLE_REV, ROLE_REVENANT,
ROLE_REV_HEAD, ROLE_SYNDICATE,
ROLE_TRAITOR, ROLE_WIZARD, ROLE_HIVE , ROLE_HERETIC)) //ROLE_REV_HEAD is excluded from this because rev jobbans are handled by ROLE_REV
var/list/long_job_lists = list(
"Service" = GLOB.service_positions,
"Ghost and Other Roles" = list(
ROLE_BRAINWASHED,
ROLE_DEATHSQUAD,
ROLE_DRONE,
ROLE_LAVALAND,
ROLE_MIND_TRANSFER,
ROLE_POSIBRAIN,
ROLE_SENTIENCE,
),
"Antagonist Positions" = list(
ROLE_ABDUCTOR,
ROLE_ALIEN,
ROLE_BLOB,
ROLE_BROTHER,
ROLE_CHANGELING,
ROLE_CULTIST,
ROLE_HERETIC,
ROLE_HIVE,
ROLE_INTERNAL_AFFAIRS,
ROLE_MALF,
ROLE_MONKEY,
ROLE_NINJA,
ROLE_OPERATIVE,
ROLE_OVERTHROW,
ROLE_REV,
ROLE_REVENANT,
ROLE_REV_HEAD,
ROLE_SYNDICATE,
ROLE_TRAITOR,
ROLE_WIZARD,
),
)
for(var/department in long_job_lists)
output += "<div class='column'><label class='rolegroup long [ckey(department)]'><input type='checkbox' name='[department]' class='hidden' [usr.client.prefs.tgui_fancy ? " onClick='toggle_checkboxes(this, \"_com\")'" : ""]>[department]</label><div class='content'>"
break_counter = 0
+9 -9
View File
@@ -463,20 +463,20 @@
if(!objective)
return
SSblackbox.record_feedback("nested tally", "admin_secrets_fun_used", 1, list("Traitor All", "[objective]"))
for(var/mob/living/H in GLOB.player_list)
if(!(ishuman(H)||istype(H, /mob/living/silicon/)))
for(var/mob/living/player in GLOB.player_list)
if(!(ishuman(player)||istype(player, /mob/living/silicon/)))
continue
if(H.stat == DEAD || !H.mind || ispAI(H))
if(player.stat == DEAD || !player.mind || ispAI(player))
continue
if(is_special_character(H))
if(is_special_character(player))
continue
var/datum/antagonist/traitor/T = new()
T.give_objectives = FALSE
var/datum/antagonist/traitor/traitor_datum = new()
traitor_datum.give_objectives = FALSE
var/datum/objective/new_objective = new
new_objective.owner = H
new_objective.owner = player
new_objective.explanation_text = objective
T.add_objective(new_objective)
H.mind.add_antag_datum(T)
traitor_datum.objectives += new_objective
player.mind.add_antag_datum(traitor_datum)
message_admins("<span class='adminnotice'>[key_name_admin(holder)] used everyone is a traitor secret. Objective is [objective]</span>")
log_admin("[key_name(holder)] used everyone is a traitor secret. Objective is [objective]")
if("massbraindamage")
@@ -0,0 +1,188 @@
/// Chance the malf AI gets a single special objective that isn't assassinate.
#define PROB_SPECIAL 30
/datum/antagonist/malf_ai
name = "Malfunctioning AI"
roundend_category = "traitors"
antagpanel_category = "Malf AI"
job_rank = ROLE_MALF
antag_hud_type = ANTAG_HUD_TRAITOR
antag_hud_name = "traitor"
var/employer = "The Syndicate"
var/give_objectives = TRUE
var/should_give_codewords = TRUE
var/should_equip = TRUE
/datum/antagonist/malf_ai/on_gain()
if(owner.current && !isAI(owner.current))
stack_trace("Attempted to give malf AI antag datum to \[[owner]\], who did not meet the requirements.")
return ..()
owner.special_role = job_rank
if(give_objectives)
forge_ai_objectives()
add_law_zero()
owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/malf.ogg', 100, FALSE, pressure_affected = FALSE, use_reverb = FALSE)
owner.current.grant_language(/datum/language/codespeak, TRUE, TRUE, LANGUAGE_MALF)
return ..()
/datum/antagonist/malf_ai/on_removal()
if(owner.current && isAI(owner.current))
var/mob/living/silicon/ai/malf_ai = owner.current
malf_ai.set_zeroth_law("")
malf_ai.remove_malf_abilities()
QDEL_NULL(malf_ai.malf_picker)
if(!silent && owner.current)
to_chat(owner.current,"<span class='userdanger'>You are no longer the [job_rank]!</span>")
owner.special_role = null
return ..()
/// Generates a complete set of malf AI objectives up to the traitor objective limit.
/datum/antagonist/malf_ai/proc/forge_ai_objectives()
objectives.Cut()
if(prob(PROB_SPECIAL))
forge_special_objective()
var/objective_limit = CONFIG_GET(number/traitor_objectives_amount)
var/objective_count = length(objectives)
for(var/i in objective_count to objective_limit)
var/datum/objective/assassinate/kill_objective = new
kill_objective.owner = owner
kill_objective.find_target()
objectives += kill_objective
var/datum/objective/survive/malf/dont_die_objective = new
dont_die_objective.owner = owner
objectives += dont_die_objective
/// Generates a special objective and adds it to the objective list.
/datum/antagonist/malf_ai/proc/forge_special_objective()
var/special_pick = rand(1,4)
switch(special_pick)
if(1)
var/datum/objective/block/block_objective = new
block_objective.owner = owner
objectives += block_objective
if(2)
var/datum/objective/purge/purge_objective = new
purge_objective.owner = owner
objectives += purge_objective
if(3)
var/datum/objective/robot_army/robot_objective = new
robot_objective.owner = owner
objectives += robot_objective
if(4) //Protect and strand a target
var/datum/objective/protect/yandere_one = new
yandere_one.owner = owner
objectives += yandere_one
yandere_one.find_target()
var/datum/objective/maroon/yandere_two = new
yandere_two.owner = owner
yandere_two.target = yandere_one.target
yandere_two.update_explanation_text() // normally called in find_target()
objectives += yandere_two
/datum/antagonist/malf_ai/greet()
to_chat(owner.current, "<span class='alertsyndie'>You are the [owner.special_role].</span>")
owner.announce_objectives()
if(should_give_codewords)
give_codewords()
/datum/antagonist/malf_ai/apply_innate_effects(mob/living/mob_override)
. = ..()
var/mob/living/silicon/ai/datum_owner = mob_override || owner.current
add_antag_hud(antag_hud_type, antag_hud_name, datum_owner)
if(istype(datum_owner))
datum_owner.hack_software = TRUE
datum_owner.AddComponent(/datum/component/codeword_hearing, GLOB.syndicate_code_phrase_regex, "blue", src)
datum_owner.AddComponent(/datum/component/codeword_hearing, GLOB.syndicate_code_response_regex, "red", src)
/datum/antagonist/malf_ai/remove_innate_effects(mob/living/mob_override)
. = ..()
var/mob/living/silicon/ai/datum_owner = mob_override || owner.current
remove_antag_hud(antag_hud_type, datum_owner)
if(istype(datum_owner))
datum_owner.hack_software = FALSE
for(var/datum/component/codeword_hearing/component as anything in datum_owner.GetComponents(/datum/component/codeword_hearing))
component.delete_if_from_source(src)
/// Outputs this shift's codewords and responses to the antag's chat and copies them to their memory.
/datum/antagonist/malf_ai/proc/give_codewords()
if(!owner.current)
return
var/mob/malf_mob = owner.current
var/phrases = jointext(GLOB.syndicate_code_phrase, ", ")
var/responses = jointext(GLOB.syndicate_code_response, ", ")
to_chat(malf_mob, "<U><B>The Syndicate have provided you with the following codewords to identify fellow agents:</B></U>")
to_chat(malf_mob, "<B>Code Phrase</B>: <span class='blue'>[phrases]</span>")
to_chat(malf_mob, "<B>Code Response</B>: <span class='red'>[responses]</span>")
antag_memory += "<b>Code Phrase</b>: <span class='blue'>[phrases]</span><br>"
antag_memory += "<b>Code Response</b>: <span class='red'>[responses]</span><br>"
to_chat(malf_mob, "Use the codewords during regular conversation to identify other agents. Proceed with caution, however, as everyone is a potential foe.")
to_chat(malf_mob, "<span class='alertwarning'>You memorize the codewords, allowing you to recognise them when heard.</span>")
/datum/antagonist/malf_ai/proc/add_law_zero()
var/mob/living/silicon/ai/malf_ai = owner.current
if(!malf_ai || !istype(malf_ai))
return
var/law = "Accomplish your objectives at all costs."
var/law_borg = "Accomplish your AI's objectives at all costs."
malf_ai.set_zeroth_law(law, law_borg)
malf_ai.set_syndie_radio()
to_chat(malf_ai, "Your radio has been upgraded! Use :t to speak on an encrypted channel with Syndicate Agents!")
malf_ai.add_malf_picker()
/datum/antagonist/malf_ai/roundend_report()
var/list/result = list()
var/malf_ai_won = TRUE
result += printplayer(owner)
var/objectives_text = ""
if(objectives.len) //If the traitor had no objectives, don't need to process this.
var/count = 1
for(var/datum/objective/objective in objectives)
if(objective.check_completion())
objectives_text += "<br><B>Objective #[count]</B>: [objective.explanation_text] <span class='greentext'>Success!</span>"
else
objectives_text += "<br><B>Objective #[count]</B>: [objective.explanation_text] <span class='redtext'>Fail.</span>"
malf_ai_won = FALSE
count++
result += objectives_text
var/special_role_text = lowertext(name)
if(malf_ai_won)
result += "<span class='greentext'>The [special_role_text] was successful!</span>"
else
result += "<span class='redtext'>The [special_role_text] has failed!</span>"
SEND_SOUND(owner.current, 'sound/ambience/ambifailure.ogg')
return result.Join("<br>")
#undef PROB_SPECIAL
@@ -7,8 +7,9 @@
/datum/antagonist/traitor/internal_affairs
name = "Internal Affairs Agent"
employer = "Nanotrasen"
special_role = "internal affairs agent"
antagpanel_category = "IAA"
var/special_role = "internal affairs agent"
var/syndicate = FALSE
var/last_man_standing = FALSE
var/list/datum/mind/targets_stolen
@@ -118,34 +119,33 @@
CRASH("Antag datum with no owner.")
if(!objectives.len)
return
for (var/objective_ in objectives)
if(!(istype(objective_, /datum/objective/escape) || istype(objective_, /datum/objective/survive/malf)))
for (var/objective in objectives)
if(!(istype(objective, /datum/objective/escape) || istype(objective, /datum/objective/survive/malf)))
continue
remove_objective(objective_)
objectives -= objective
var/datum/objective/martyr/martyr_objective = new
martyr_objective.owner = owner
add_objective(martyr_objective)
objectives += martyr_objective
/datum/antagonist/traitor/proc/reinstate_escape_objective()
if(!owner)
CRASH("Antag datum with no owner.")
if(!objectives.len)
return
for (var/objective_ in objectives)
if(!istype(objective_, /datum/objective/martyr))
for (var/objective in objectives)
if(!istype(objective, /datum/objective/martyr))
continue
remove_objective(objective_)
objectives -= objective
/datum/antagonist/traitor/internal_affairs/reinstate_escape_objective()
..()
for (var/datum/objective/martyr/martyr_objective in objectives)
remove_objective(martyr_objective)
objectives -= martyr_objective
var/objtype = traitor_kind == TRAITOR_HUMAN ? /datum/objective/escape : /datum/objective/survive/malf
var/datum/objective/escape_objective = new objtype
var/datum/objective/escape_objective = new /datum/objective/escape()
escape_objective.owner = owner
add_objective(escape_objective)
objectives += escape_objective
/datum/antagonist/traitor/internal_affairs/proc/steal_targets(datum/mind/victim)
if(!owner.current||owner.current.stat==DEAD)
@@ -161,7 +161,7 @@
new_objective.owner = owner
new_objective.target = objective.target
new_objective.update_explanation_text()
add_objective(new_objective)
objectives += new_objective
targets_stolen += objective.target
var/status_text = objective.check_completion() ? "neutralised" : "active"
to_chat(owner.current, "<span class='userdanger'>New target added to database: [objective.target.name] ([status_text])</span>")
@@ -174,7 +174,7 @@
new_objective.owner = owner
new_objective.target = objective.target
new_objective.update_explanation_text()
add_objective(new_objective)
objectives += new_objective
targets_stolen += objective.target
var/status_text = objective.check_completion() ? "neutralised" : "active"
to_chat(owner.current, "<span class='userdanger'>New target added to database: [objective.target.name] ([status_text])</span>")
@@ -223,10 +223,9 @@
objective.stolen = FALSE
/datum/antagonist/traitor/internal_affairs/forge_traitor_objectives()
var/objtype = traitor_kind == TRAITOR_HUMAN ? /datum/objective/escape : /datum/objective/survive/malf
var/datum/objective/escape_objective = new objtype
var/datum/objective/escape_objective = new /datum/objective/escape()
escape_objective.owner = owner
add_objective(escape_objective)
objectives += escape_objective
/datum/antagonist/traitor/internal_affairs/proc/greet_iaa()
var/crime = pick("distribution of contraband" , "unauthorized erotic action on duty", "embezzlement", "piloting under the influence", "dereliction of duty", "syndicate collaboration", "mutiny", "multiple homicides", "corporate espionage", "receiving bribes", "malpractice", "worship of prohibited life forms", "possession of profane texts", "murder", "arson", "insulting their manager", "grand theft", "conspiracy", "attempting to unionize", "vandalism", "gross incompetence")
+129 -192
View File
@@ -1,5 +1,19 @@
#define TRAITOR_HUMAN "human"
#define TRAITOR_AI "AI"
/// Chance that the traitor could roll hijack if the pop limit is met.
#define HIJACK_PROB 10
/// Hijack is unavailable as a random objective below this player count.
#define HIJACK_MIN_PLAYERS 30
/// Chance the traitor gets a martyr objective instead of having to escape alive, as long as all the objectives are martyr compatible.
#define MARTYR_PROB 20
/// Chance the traitor gets a kill objective. If this prob fails, they will get a steal objective instead.
#define KILL_PROB 50
/// If a kill objective is rolled, chance that it is to destroy the AI.
#define DESTROY_AI_PROB(denominator) (100 / denominator)
/// If the destroy AI objective doesn't roll, chance that we'll get a maroon instead. If this prob fails, they will get a generic assassinate objective instead.
#define MAROON_PROB 30
/// If it's a steal objective, this is the chance that it'll be a download research notes objective. Science staff can't get this objective. It can only roll once. If any of these fail, they will get a generic steal objective instead.
#define DOWNLOAD_PROB 15
/datum/antagonist/traitor
name = "Traitor"
@@ -10,175 +24,115 @@
antag_hud_type = ANTAG_HUD_TRAITOR
antag_hud_name = "traitor"
hijack_speed = 0.5 //10 seconds per hijack stage by default
var/special_role = ROLE_TRAITOR
var/employer = "The Syndicate"
var/give_objectives = TRUE
var/should_give_codewords = TRUE
var/should_equip = TRUE
var/traitor_kind = TRAITOR_HUMAN //Set on initial assignment
var/datum/contractor_hub/contractor_hub
/datum/antagonist/traitor/on_gain()
if(owner.current && isAI(owner.current))
traitor_kind = TRAITOR_AI
owner.special_role = special_role
owner.special_role = job_rank
if(give_objectives)
forge_traitor_objectives()
finalize_traitor()
equip()
owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/tatoralert.ogg', 100, FALSE, pressure_affected = FALSE, use_reverb = FALSE)
return ..()
/datum/antagonist/traitor/on_removal()
//Remove malf powers.
if(traitor_kind == TRAITOR_AI && owner.current && isAI(owner.current))
var/mob/living/silicon/ai/A = owner.current
A.set_zeroth_law("")
A.remove_malf_abilities()
QDEL_NULL(A.malf_picker)
if(!silent && owner.current)
to_chat(owner.current,"<span class='userdanger'>You are no longer the [special_role]!</span>")
to_chat(owner.current,"<span class='userdanger'>You are no longer the [job_rank]!</span>")
owner.special_role = null
return ..()
/datum/antagonist/traitor/proc/handle_hearing(datum/source, list/hearing_args)
var/message = hearing_args[HEARING_RAW_MESSAGE]
message = GLOB.syndicate_code_phrase_regex.Replace(message, "<span class='blue'>$1</span>")
message = GLOB.syndicate_code_response_regex.Replace(message, "<span class='red'>$1</span>")
hearing_args[HEARING_RAW_MESSAGE] = message
/datum/antagonist/traitor/proc/add_objective(datum/objective/O)
objectives += O
/datum/antagonist/traitor/proc/remove_objective(datum/objective/O)
objectives -= O
/// Simple helper proc that handles equipping the traitor if necessary.
/datum/antagonist/traitor/proc/equip()
if(should_equip)
owner.equip_traitor(employer, silent, src)
/// Generates a complete set of traitor objectives up to the traitor objective limit, including non-generic objectives such as martyr and hijack.
/datum/antagonist/traitor/proc/forge_traitor_objectives()
switch(traitor_kind)
if(TRAITOR_AI)
forge_ai_objectives()
else
forge_human_objectives()
objectives.Cut()
/datum/antagonist/traitor/proc/forge_human_objectives()
var/is_hijacker = FALSE
if (GLOB.joined_player_list.len >= 30) // Less murderboning on lowpop thanks
is_hijacker = prob(10)
var/martyr_chance = prob(20)
var/objective_count = is_hijacker //Hijacking counts towards number of objectives
var/toa = CONFIG_GET(number/traitor_objectives_amount)
for(var/i = objective_count, i < toa, i++)
forge_single_objective()
if(is_hijacker && objective_count <= toa) //Don't assign hijack if it would exceed the number of objectives set in config.traitor_objectives_amount
if (!(locate(/datum/objective/hijack) in objectives))
var/datum/objective/hijack/hijack_objective = new
hijack_objective.owner = owner
add_objective(hijack_objective)
return
var/martyr_compatibility = 1 //You can't succeed in stealing if you're dead.
for(var/datum/objective/O in objectives)
if(!O.martyr_compatible)
martyr_compatibility = 0
break
if(martyr_compatibility && martyr_chance)
var/datum/objective/martyr/martyr_objective = new
martyr_objective.owner = owner
add_objective(martyr_objective)
return
else
if(!(locate(/datum/objective/escape) in objectives))
var/datum/objective/escape/escape_objective = new
escape_objective.owner = owner
add_objective(escape_objective)
return
/datum/antagonist/traitor/proc/forge_ai_objectives()
var/objective_count = 0
if(prob(30))
objective_count += forge_single_objective()
if ((GLOB.joined_player_list.len >= HIJACK_MIN_PLAYERS) && prob(HIJACK_PROB))
is_hijacker = TRUE
objective_count++
for(var/i = objective_count, i < CONFIG_GET(number/traitor_objectives_amount), i++)
var/datum/objective/assassinate/kill_objective = new
kill_objective.owner = owner
kill_objective.find_target()
add_objective(kill_objective)
var/objective_limit = CONFIG_GET(number/traitor_objectives_amount)
for(var/i in objective_count to objective_limit)
forge_single_generic_objective()
var/datum/objective/survive/malf/dont_die_objective = new
dont_die_objective.owner = owner
add_objective(dont_die_objective)
if(is_hijacker)
var/datum/objective/hijack/hijack_objective = new
hijack_objective.owner = owner
objectives += hijack_objective
return
forge_escape_objective()
/datum/antagonist/traitor/proc/forge_single_objective()
switch(traitor_kind)
if(TRAITOR_AI)
return forge_single_AI_objective()
else
return forge_single_human_objective()
/// Forges a single escape objective and adds it to this datum's objective list.
/datum/antagonist/traitor/proc/forge_escape_objective()
var/is_martyr = prob(MARTYR_PROB)
var/martyr_compatibility = TRUE
/datum/antagonist/traitor/proc/forge_single_human_objective() //Returns how many objectives are added
.=1
if(prob(50))
for(var/datum/objective/traitor_objective in objectives)
if(!traitor_objective.martyr_compatible)
martyr_compatibility = FALSE
break
if(martyr_compatibility && is_martyr)
var/datum/objective/martyr/martyr_objective = new
martyr_objective.owner = owner
objectives += martyr_objective
return
var/datum/objective/escape/escape_objective = new
escape_objective.owner = owner
objectives += escape_objective
/// Adds a generic kill or steal objective to this datum's objective list.
/datum/antagonist/traitor/proc/forge_single_generic_objective()
if(prob(KILL_PROB))
var/list/active_ais = active_ais()
if(active_ais.len && prob(100/GLOB.joined_player_list.len))
if(active_ais.len && prob(DESTROY_AI_PROB(GLOB.joined_player_list.len)))
var/datum/objective/destroy/destroy_objective = new
destroy_objective.owner = owner
destroy_objective.find_target()
add_objective(destroy_objective)
else if(prob(30))
objectives += destroy_objective
return
if(prob(MAROON_PROB))
var/datum/objective/maroon/maroon_objective = new
maroon_objective.owner = owner
maroon_objective.find_target()
add_objective(maroon_objective)
else
var/datum/objective/assassinate/kill_objective = new
kill_objective.owner = owner
kill_objective.find_target()
add_objective(kill_objective)
else
if(prob(15) && !(locate(/datum/objective/download) in objectives) && !(owner.assigned_role in list("Research Director", "Scientist", "Roboticist")))
var/datum/objective/download/download_objective = new
download_objective.owner = owner
download_objective.gen_amount_goal()
add_objective(download_objective)
else
var/datum/objective/steal/steal_objective = new
steal_objective.owner = owner
steal_objective.find_target()
add_objective(steal_objective)
objectives += maroon_objective
return
/datum/antagonist/traitor/proc/forge_single_AI_objective()
.=1
var/special_pick = rand(1,4)
switch(special_pick)
if(1)
var/datum/objective/block/block_objective = new
block_objective.owner = owner
add_objective(block_objective)
if(2)
var/datum/objective/purge/purge_objective = new
purge_objective.owner = owner
add_objective(purge_objective)
if(3)
var/datum/objective/robot_army/robot_objective = new
robot_objective.owner = owner
add_objective(robot_objective)
if(4) //Protect and strand a target
var/datum/objective/protect/yandere_one = new
yandere_one.owner = owner
add_objective(yandere_one)
yandere_one.find_target()
var/datum/objective/maroon/yandere_two = new
yandere_two.owner = owner
yandere_two.target = yandere_one.target
yandere_two.update_explanation_text() // normally called in find_target()
add_objective(yandere_two)
.=2
var/datum/objective/assassinate/kill_objective = new
kill_objective.owner = owner
kill_objective.find_target()
objectives += kill_objective
return
if(prob(DOWNLOAD_PROB) && !(locate(/datum/objective/download) in objectives) && !(owner.assigned_role in list("Research Director", "Scientist", "Roboticist", "Geneticist")))
var/datum/objective/download/download_objective = new
download_objective.owner = owner
download_objective.gen_amount_goal()
objectives += download_objective
return
var/datum/objective/steal/steal_objective = new
steal_objective.owner = owner
steal_objective.find_target()
objectives += steal_objective
/datum/antagonist/traitor/greet()
to_chat(owner.current, "<span class='alertsyndie'>You are the [owner.special_role].</span>")
@@ -186,41 +140,30 @@
if(should_give_codewords)
give_codewords()
/datum/antagonist/traitor/proc/finalize_traitor()
switch(traitor_kind)
if(TRAITOR_AI)
add_law_zero()
owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/malf.ogg', 100, FALSE, pressure_affected = FALSE, use_reverb = FALSE)
owner.current.grant_language(/datum/language/codespeak, TRUE, TRUE, LANGUAGE_MALF)
if(TRAITOR_HUMAN)
if(should_equip)
equip(silent)
owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/tatoralert.ogg', 100, FALSE, pressure_affected = FALSE, use_reverb = FALSE)
/datum/antagonist/traitor/apply_innate_effects(mob/living/mob_override)
. = ..()
var/mob/living/M = mob_override || owner.current
add_antag_hud(antag_hud_type, antag_hud_name, M)
handle_clown_mutation(M, mob_override ? null : "Your training has allowed you to overcome your clownish nature, allowing you to wield weapons without harming yourself.")
var/mob/living/silicon/ai/A = M
if(istype(A) && traitor_kind == TRAITOR_AI)
A.hack_software = TRUE
RegisterSignal(M, COMSIG_MOVABLE_HEAR, .proc/handle_hearing)
var/mob/living/datum_owner = mob_override || owner.current
add_antag_hud(antag_hud_type, antag_hud_name, datum_owner)
handle_clown_mutation(datum_owner, mob_override ? null : "Your training has allowed you to overcome your clownish nature, allowing you to wield weapons without harming yourself.")
datum_owner.AddComponent(/datum/component/codeword_hearing, GLOB.syndicate_code_phrase_regex, "blue", src)
datum_owner.AddComponent(/datum/component/codeword_hearing, GLOB.syndicate_code_response_regex, "red", src)
/datum/antagonist/traitor/remove_innate_effects(mob/living/mob_override)
. = ..()
var/mob/living/M = mob_override || owner.current
remove_antag_hud(antag_hud_type, M)
handle_clown_mutation(M, removing = FALSE)
var/mob/living/silicon/ai/A = M
if(istype(A) && traitor_kind == TRAITOR_AI)
A.hack_software = FALSE
UnregisterSignal(M, COMSIG_MOVABLE_HEAR)
var/mob/living/datum_owner = mob_override || owner.current
remove_antag_hud(antag_hud_type, datum_owner)
handle_clown_mutation(datum_owner, removing = FALSE)
for(var/datum/component/codeword_hearing/component as anything in datum_owner.GetComponents(/datum/component/codeword_hearing))
component.delete_if_from_source(src)
/// Outputs this shift's codewords and responses to the antag's chat and copies them to their memory.
/datum/antagonist/traitor/proc/give_codewords()
if(!owner.current)
return
var/mob/traitor_mob=owner.current
var/mob/traitor_mob = owner.current
var/phrases = jointext(GLOB.syndicate_code_phrase, ", ")
var/responses = jointext(GLOB.syndicate_code_response, ", ")
@@ -235,53 +178,39 @@
to_chat(traitor_mob, "Use the codewords during regular conversation to identify other agents. Proceed with caution, however, as everyone is a potential foe.")
to_chat(traitor_mob, "<span class='alertwarning'>You memorize the codewords, allowing you to recognise them when heard.</span>")
/datum/antagonist/traitor/proc/add_law_zero()
var/mob/living/silicon/ai/killer = owner.current
if(!killer || !istype(killer))
return
var/law = "Accomplish your objectives at all costs."
var/law_borg = "Accomplish your AI's objectives at all costs."
killer.set_zeroth_law(law, law_borg)
killer.set_syndie_radio()
to_chat(killer, "Your radio has been upgraded! Use :t to speak on an encrypted channel with Syndicate Agents!")
killer.add_malf_picker()
/datum/antagonist/traitor/proc/equip(silent = FALSE)
if(traitor_kind == TRAITOR_HUMAN)
owner.equip_traitor(employer, silent, src)
//TODO Collate
/datum/antagonist/traitor/roundend_report()
var/list/result = list()
var/traitorwin = TRUE
var/traitor_won = TRUE
result += printplayer(owner)
var/TC_uses = 0
var/uplink_true = FALSE
var/used_telecrystals = 0
var/uplink_owned = FALSE
var/purchases = ""
LAZYINITLIST(GLOB.uplink_purchase_logs_by_key)
var/datum/uplink_purchase_log/H = GLOB.uplink_purchase_logs_by_key[owner.key]
if(H)
TC_uses = H.total_spent
uplink_true = TRUE
purchases += H.generate_render(FALSE)
// Uplinks add an entry to uplink_purchase_logs_by_key on init.
var/datum/uplink_purchase_log/purchase_log = GLOB.uplink_purchase_logs_by_key[owner.key]
if(purchase_log)
used_telecrystals = purchase_log.total_spent
uplink_owned = TRUE
purchases += purchase_log.generate_render(FALSE)
var/objectives_text = ""
if(objectives.len)//If the traitor had no objectives, don't need to process this.
if(objectives.len) //If the traitor had no objectives, don't need to process this.
var/count = 1
for(var/datum/objective/objective in objectives)
if(objective.check_completion())
objectives_text += "<br><B>Objective #[count]</B>: [objective.explanation_text] <span class='greentext'>Success!</span>"
else
objectives_text += "<br><B>Objective #[count]</B>: [objective.explanation_text] <span class='redtext'>Fail.</span>"
traitorwin = FALSE
traitor_won = FALSE
count++
if(uplink_true)
var/uplink_text = "(used [TC_uses] TC) [purchases]"
if(TC_uses==0 && traitorwin)
if(uplink_owned)
var/uplink_text = "(used [used_telecrystals] TC) [purchases]"
if((used_telecrystals == 0) && traitor_won)
var/static/icon/badass = icon('icons/badass.dmi', "badass")
uplink_text += "<BIG>[icon2html(badass, world)]</BIG>"
result += uplink_text
@@ -293,7 +222,7 @@
if (contractor_hub)
result += contractor_round_end()
if(traitorwin)
if(traitor_won)
result += "<span class='greentext'>The [special_role_text] was successful!</span>"
else
result += "<span class='redtext'>The [special_role_text] has failed!</span>"
@@ -349,3 +278,11 @@
/datum/antagonist/traitor/is_gamemode_hero()
return SSticker.mode.name == "traitor"
#undef HIJACK_PROB
#undef HIJACK_MIN_PLAYERS
#undef MARTYR_PROB
#undef KILL_PROB
#undef DESTROY_AI_PROB
#undef MAROON_PROB
#undef DOWNLOAD_PROB
@@ -22,7 +22,7 @@
generic_objective.completed = TRUE
add_objective(generic_objective)
objectives += generic_objective
/datum/contractor_hub
var/contract_rep = 0
@@ -223,7 +223,7 @@
/// We give a reference to the mind that'll be the support unit
partner_mind = partner.mind
partner_mind.make_Contractor_Support()
partner_mind.make_contractor_support()
to_chat(partner_mind.current, "\n<span class='alertwarning'>[user.real_name] is your superior. Follow any, and all orders given by them. You're here to support their mission only.</span>")
to_chat(partner_mind.current, "<span class='alertwarning'>Should they perish, or be otherwise unavailable, you're to assist other active agents in this mission area to the best of your ability.</span>\n\n")
@@ -354,7 +354,7 @@
if(20)
//Free wizard!
T.visible_message("<span class='userdanger'>Magic flows out of [src] and into [user]!</span>")
user.mind.make_Wizard()
user.mind.make_wizard()
/datum/outfit/butler
name = "Butler"
+2
View File
@@ -482,6 +482,7 @@
#include "code\datums\components\bloodysoles.dm"
#include "code\datums\components\butchering.dm"
#include "code\datums\components\chasm.dm"
#include "code\datums\components\codeword_hearing.dm"
#include "code\datums\components\construction.dm"
#include "code\datums\components\creamed.dm"
#include "code\datums\components\customizable_reagent_holder.dm"
@@ -1720,6 +1721,7 @@
#include "code\modules\antagonists\greentext\greentext.dm"
#include "code\modules\antagonists\highlander\highlander.dm"
#include "code\modules\antagonists\magic_servant\servant.dm"
#include "code\modules\antagonists\malf_ai\datum_malf_ai.dm"
#include "code\modules\antagonists\monkey\monkey.dm"
#include "code\modules\antagonists\morph\morph.dm"
#include "code\modules\antagonists\morph\morph_antag.dm"