diff --git a/code/__DEFINES/~nova_defines/antag_optin_defines.dm b/code/__DEFINES/~nova_defines/antag_optin_defines.dm new file mode 100644 index 00000000000..50e750053b9 --- /dev/null +++ b/code/__DEFINES/~nova_defines/antag_optin_defines.dm @@ -0,0 +1,45 @@ +//defines for antag opt in objective checking +//objectives check for all players with a value equal or greater than the 'threat' level of an objective then pick from that list +//command + sec roles are always opted in regardless of opt in status + +/// For temporary or otherwise 'inconvenient' objectives like kidnapping or theft +#define OPT_IN_YES_TEMP 1 +/// Cool with being killed or otherwise occupied but not removed from the round +#define OPT_IN_YES_KILL 2 +/// Fine with being round removed. +#define OPT_IN_YES_ROUND_REMOVE 3 + +#define OPT_IN_YES_TEMP_STRING "Yes - Temporary/Inconvenience" +#define OPT_IN_YES_KILL_STRING "Yes - Kill" +#define OPT_IN_YES_ROUND_REMOVE_STRING "Yes - Round Remove" +#define OPT_IN_NOT_TARGET_STRING "No" + +/// Assoc list of stringified opt_in_## define to the front-end string to show users as a representation of the setting. +GLOBAL_LIST_INIT(antag_opt_in_strings, list( + "0" = OPT_IN_NOT_TARGET_STRING, + "1" = OPT_IN_YES_TEMP_STRING, + "2" = OPT_IN_YES_KILL_STRING, + "3" = OPT_IN_YES_ROUND_REMOVE_STRING, +)) + +/// Assoc list of stringified opt_in_## define to the color associated with it. +GLOBAL_LIST_INIT(antag_opt_in_colors, list( + OPT_IN_NOT_TARGET_STRING = COLOR_GRAY, + OPT_IN_YES_TEMP_STRING = COLOR_EMERALD, + OPT_IN_YES_KILL_STRING = COLOR_ORANGE, + OPT_IN_YES_ROUND_REMOVE_STRING = COLOR_RED +)) + +/// Prefers not to be a target. Will still be a potential target if playing sec or command. +#define OPT_IN_NOT_TARGET 0 + +/// The minimum opt-in level for people playing sec. +#define SECURITY_OPT_IN_LEVEL OPT_IN_YES_KILL +/// The minimum opt-in level for people playing command. +#define COMMAND_OPT_IN_LEVEL OPT_IN_YES_KILL + +/// The default opt in level for preferences and mindless mobs. +#define OPT_IN_DEFAULT_LEVEL OPT_IN_NOT_TARGET + +/// If the player has any non-ghost role antags enabled, they are forced to use a minimum of this. +#define OPT_IN_ANTAG_ENABLED_LEVEL OPT_IN_YES_TEMP diff --git a/code/controllers/subsystem/dynamic/dynamic_rulesets_midround.dm b/code/controllers/subsystem/dynamic/dynamic_rulesets_midround.dm index 036236b0a91..79e47cb3b3d 100644 --- a/code/controllers/subsystem/dynamic/dynamic_rulesets_midround.dm +++ b/code/controllers/subsystem/dynamic/dynamic_rulesets_midround.dm @@ -942,12 +942,16 @@ /datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/proc/find_original() var/list/possible_targets = list() + var/opt_in_disabled = CONFIG_GET(flag/disable_antag_opt_in_preferences) // NOVA EDIT ADDITION - ANTAG OPT-IN for(var/mob/living/carbon/human/player in GLOB.player_list) if(!player.client || !player.mind || player.stat) continue if(!(player.mind.assigned_role.job_flags & JOB_CREW_MEMBER)) continue - possible_targets += player + // NOVA EDIT ADDITION START - Players in the interlink can't be obsession targets + Antag Optin + if (!opt_in_disabled && player.mind?.get_effective_opt_in_level() < OPT_IN_YES_ROUND_REMOVE) + continue + // NOVA EDIT ADDITION END if(possible_targets.len) return pick(possible_targets) diff --git a/code/datums/brain_damage/creepy_trauma.dm b/code/datums/brain_damage/creepy_trauma.dm index b056ddfb8de..11283588543 100644 --- a/code/datums/brain_damage/creepy_trauma.dm +++ b/code/datums/brain_damage/creepy_trauma.dm @@ -131,15 +131,18 @@ var/list/special_pool = list() //The special list, for quirk-based var/chosen_victim //The obsession target + var/opt_in_disabled = CONFIG_GET(flag/disable_antag_opt_in_preferences) // NOVA EDIT ADDITION - ANTAG OPT-IN for(var/mob/player as anything in GLOB.player_list)//prevents crew members falling in love with nuke ops they never met, and other annoying hijinks if(!player.client || !player.mind || isnewplayer(player) || player.stat == DEAD || isbrain(player) || player == owner) continue if(!(player.mind.assigned_role.job_flags & JOB_CREW_MEMBER)) continue - // NOVA EDIT ADDITION START - Players in the interlink can't be obsession targets + // NOVA EDIT ADDITION START - Players in the interlink can't be obsession targets + Antag Optin if(SSticker.IsRoundInProgress() && istype(get_area(player), /area/centcom/interlink)) continue - // NOVA EDIT END + if (!opt_in_disabled && player.mind?.get_effective_opt_in_level() < OPT_IN_YES_KILL) + continue + // NOVA EDIT ADDITION END viable_minds += player.mind for(var/datum/mind/possible_target as anything in viable_minds) if(possible_target != owner && ishuman(possible_target.current)) diff --git a/code/game/gamemodes/objective.dm b/code/game/gamemodes/objective.dm index f8a2cbede01..7c7deb058f2 100644 --- a/code/game/gamemodes/objective.dm +++ b/code/game/gamemodes/objective.dm @@ -157,6 +157,7 @@ GLOBAL_LIST_EMPTY(objectives) //NOVA EDIT ADDITION var/datum/mind/O = I if(O.late_joiner) try_target_late_joiners = TRUE + var/opt_in_disabled = CONFIG_GET(flag/disable_antag_opt_in_preferences) // NOVA EDIT ADDITION - ANTAG OPT-IN for(var/datum/mind/possible_target in get_crewmember_minds()) if(possible_target in owners) continue @@ -166,6 +167,10 @@ GLOBAL_LIST_EMPTY(objectives) //NOVA EDIT ADDITION continue if(!is_valid_target(possible_target)) continue + // NOVA EDIT ADDITION START - Antag Opt In + if (!opt_in_disabled && !opt_in_valid(possible_target)) + continue + // NOVA EDIT ADDITION END possible_targets += possible_target if(try_target_late_joiners) var/list/all_possible_targets = possible_targets.Copy() @@ -871,7 +876,15 @@ GLOBAL_LIST_EMPTY(possible_items) /datum/objective/destroy/find_target(dupe_search_range, list/blacklist) var/list/possible_targets = active_ais(TRUE) possible_targets -= blacklist - var/mob/living/silicon/ai/target_ai = pick(possible_targets) + //var/mob/living/silicon/ai/target_ai = pick(possible_targets) // NOVA EDIT REMOVAL - Uses the below loop + // NOVA EDIT ADDITION BEGIN - ANTAG OPTIN + var/mob/living/silicon/ai/target_ai + var/opt_in_disabled = CONFIG_GET(flag/disable_antag_opt_in_preferences) // NOVA EDIT ADDITION - ANTAG OPT-IN + for (var/mob/living/silicon/ai/possible_target as anything in shuffle(possible_targets)) + if (!opt_in_disabled && !opt_in_valid(possible_target)) + continue + target_ai = possible_target + // NOVA EDIT ADDITION END target = target_ai.mind update_explanation_text() return target diff --git a/code/modules/antagonists/cult/cult_objectives.dm b/code/modules/antagonists/cult/cult_objectives.dm index 7da5d095661..42312eea2dd 100644 --- a/code/modules/antagonists/cult/cult_objectives.dm +++ b/code/modules/antagonists/cult/cult_objectives.dm @@ -19,19 +19,24 @@ return var/datum/team/cult/cult = team var/list/target_candidates = list() + var/opt_in_disabled = CONFIG_GET(flag/disable_antag_opt_in_preferences) // NOVA EDIT ADDITION - ANTAG OPT-IN for(var/mob/living/carbon/human/player in GLOB.player_list) - // NOVA EDIT ADDITION START - Players in the interlink can't be obsession targets + // NOVA EDIT ADDITION START - Players in the interlink can't be obsession targets + Antag Optin if(SSticker.IsRoundInProgress() && istype(get_area(player), /area/centcom/interlink)) continue + if (!opt_in_disabled && !opt_in_valid(player)) + continue // NOVA EDIT END if(player.mind && !player.mind.has_antag_datum(/datum/antagonist/cult) && !is_convertable_to_cult(player) && player.stat != DEAD) target_candidates += player.mind if(target_candidates.len == 0) message_admins("Cult Sacrifice: Could not find unconvertible target, checking for convertible target.") for(var/mob/living/carbon/human/player in GLOB.player_list) - // NOVA EDIT ADDITION START - Players in the interlink can't be obsession targets + // NOVA EDIT ADDITION START - Players in the interlink can't be obsession targets + Antag Optin if(SSticker.IsRoundInProgress() && istype(get_area(player), /area/centcom/interlink)) continue + if (!opt_in_disabled && !opt_in_valid(player)) + continue // NOVA EDIT END if(player.mind && !player.mind.has_antag_datum(/datum/antagonist/cult) && player.stat != DEAD) target_candidates += player.mind diff --git a/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_knowledge.dm b/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_knowledge.dm index 797d754ea0b..f515b3e25bd 100644 --- a/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_knowledge.dm +++ b/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_knowledge.dm @@ -113,6 +113,10 @@ continue if(possible_target.current.stat == DEAD) continue + // NOVA EDIT ADDITION BEGIN - Antag opt-in (Only security and command can be targetted) + if (!possible_target.assigned_role?.heretic_sac_target) + continue + // NOVA EDIT ADDITION END valid_targets += possible_target @@ -142,12 +146,14 @@ valid_targets -= sec_mind break + /* NOVA EDIT REMOVAL -- Antag Opt In (Only sec and command may be targetted) // Third target, someone in their department. for(var/datum/mind/department_mind as anything in shuffle(valid_targets)) if(department_mind.assigned_role?.departments_bitflags & user.mind.assigned_role?.departments_bitflags) final_targets += department_mind valid_targets -= department_mind break + */ // NOVA EDIT REMOVAL END // Now grab completely random targets until we'll full var/target_sanity = 0 diff --git a/code/modules/jobs/job_types/_job.dm b/code/modules/jobs/job_types/_job.dm index c0a56dc7a67..70bc9113150 100644 --- a/code/modules/jobs/job_types/_job.dm +++ b/code/modules/jobs/job_types/_job.dm @@ -312,6 +312,15 @@ info += span_boldnotice("As this station was initially staffed with a \ [CONFIG_GET(flag/jobs_have_minimal_access) ? "full crew, only your job's necessities" : "skeleton crew, additional access may"] \ have been added to your ID card.") + //NOVA EDIT ADDITION BEGIN - ANTAG OPT IN + if (!CONFIG_GET(flag/disable_antag_opt_in_preferences)) + if (isnum(minimum_opt_in_level) && minimum_opt_in_level > OPT_IN_NOT_TARGET) + info += span_bolddanger("This job forces a minimum opt-in setting of [GLOB.antag_opt_in_strings["[minimum_opt_in_level]"]].") + if (heretic_sac_target) + info += span_bolddanger("This job can be sacrificed by heretics.") + if (contractable) + info += span_bolddanger("This job can be targeted by contractors.") + //NOVA EDIT ADDITION END //NOVA EDIT ADDITION START - ALTERNATIVE_JOB_TITLES if(alt_title != title) info += span_warning("Remember that alternate titles are purely for flavor and roleplay.") diff --git a/code/modules/mob/living/carbon/human/examine.dm b/code/modules/mob/living/carbon/human/examine.dm index 735d6f83cc1..c628b9ddcb8 100644 --- a/code/modules/mob/living/carbon/human/examine.dm +++ b/code/modules/mob/living/carbon/human/examine.dm @@ -505,6 +505,12 @@ if(erp_status_pref && !CONFIG_GET(flag/disable_erp_preferences)) . += span_notice("ERP STATUS: [erp_status_pref]") + if (!CONFIG_GET(flag/disable_antag_opt_in_preferences)) + var/opt_in_status = mind?.get_effective_opt_in_level() + if (!isnull(opt_in_status)) + var/stringified_optin = GLOB.antag_opt_in_strings["[opt_in_status]"] + . += span_notice("Antag Opt-in Status: [stringified_optin]") + //Temporary flavor text addition: if(temporary_flavor_text) if(length_char(temporary_flavor_text) < TEMPORARY_FLAVOR_PREVIEW_LIMIT) diff --git a/config/nova/config_nova.txt b/config/nova/config_nova.txt index deb7f925ea8..31f620888d9 100644 --- a/config/nova/config_nova.txt +++ b/config/nova/config_nova.txt @@ -146,3 +146,6 @@ VETERAN_LEGACY_SYSTEM ## How much time arrivals shuttle should stay at station after its engines recharged before returning to interlink. In deciseconds. 150 - 15 seconds. 0 - disables autoreturn. ARRIVALS_WAIT 150 + +## Uncomment to completely disable the opt-in system, which is a system that forces objectives to only roll on individuals who consent to it. +#DISABLE_ANTAG_OPT_IN_PREFERENCES diff --git a/modular_nova/master_files/code/modules/mob/living/examine_tgui.dm b/modular_nova/master_files/code/modules/mob/living/examine_tgui.dm index 5648dda48d3..de181c91ffc 100644 --- a/modular_nova/master_files/code/modules/mob/living/examine_tgui.dm +++ b/modular_nova/master_files/code/modules/mob/living/examine_tgui.dm @@ -54,19 +54,30 @@ var/custom_species_lore var/obscured var/ooc_notes = "" + var/ideal_antag_optin_status + var/current_antag_optin_status var/headshot = "" // Handle OOC notes first - if(preferences && preferences.read_preference(/datum/preference/toggle/master_erp_preferences)) - var/e_prefs = preferences.read_preference(/datum/preference/choiced/erp_status) - var/e_prefs_nc = preferences.read_preference(/datum/preference/choiced/erp_status_nc) - var/e_prefs_v = preferences.read_preference(/datum/preference/choiced/erp_status_v) - var/e_prefs_mechanical = preferences.read_preference(/datum/preference/choiced/erp_status_mechanics) - ooc_notes += "ERP: [e_prefs]\n" - ooc_notes += "Non-Con: [e_prefs_nc]\n" - ooc_notes += "Vore: [e_prefs_v]\n" - ooc_notes += "ERP Mechanics: [e_prefs_mechanical]\n" - ooc_notes += "\n" + if(preferences) + if(preferences.read_preference(/datum/preference/toggle/master_erp_preferences)) + var/e_prefs = preferences.read_preference(/datum/preference/choiced/erp_status) + var/e_prefs_nc = preferences.read_preference(/datum/preference/choiced/erp_status_nc) + var/e_prefs_v = preferences.read_preference(/datum/preference/choiced/erp_status_v) + var/e_prefs_mechanical = preferences.read_preference(/datum/preference/choiced/erp_status_mechanics) + ooc_notes += "ERP: [e_prefs]\n" + ooc_notes += "Non-Con: [e_prefs_nc]\n" + ooc_notes += "Vore: [e_prefs_v]\n" + ooc_notes += "ERP Mechanics: [e_prefs_mechanical]\n" + ooc_notes += "\n" + + if(!CONFIG_GET(flag/disable_antag_opt_in_preferences)) + var/antag_prefs = holder.mind?.ideal_opt_in_level + var/effective_opt_in_level = holder.mind?.get_effective_opt_in_level() + if(isnull(antag_prefs)) + antag_prefs = preferences.read_preference(/datum/preference/choiced/antag_opt_in_status) + current_antag_optin_status = GLOB.antag_opt_in_strings[num2text(effective_opt_in_level)] + ideal_antag_optin_status = GLOB.antag_opt_in_strings[num2text(antag_prefs)] // Now we handle silicon and/or human, order doesn't really matter // If other variants of mob/living need to be handled at some point, put them here @@ -97,4 +108,14 @@ data["custom_species"] = custom_species data["custom_species_lore"] = custom_species_lore data["headshot"] = headshot + + data["ideal_antag_optin_status"] = ideal_antag_optin_status + data["current_antag_optin_status"] = current_antag_optin_status + return data + +/datum/examine_panel/ui_static_data(mob/user) + var/list/data = list() + + data["opt_in_colors"] = GLOB.antag_opt_in_colors + return data diff --git a/modular_nova/modules/antag_opt_in/code/antag_optin_config.dm b/modular_nova/modules/antag_opt_in/code/antag_optin_config.dm new file mode 100644 index 00000000000..bff8c325131 --- /dev/null +++ b/modular_nova/modules/antag_opt_in/code/antag_optin_config.dm @@ -0,0 +1,2 @@ +/datum/config_entry/flag/disable_antag_opt_in_preferences + default = FALSE diff --git a/modular_nova/modules/antag_opt_in/code/antag_optin_preferences.dm b/modular_nova/modules/antag_opt_in/code/antag_optin_preferences.dm new file mode 100644 index 00000000000..eb6038e62e8 --- /dev/null +++ b/modular_nova/modules/antag_opt_in/code/antag_optin_preferences.dm @@ -0,0 +1,33 @@ +/datum/preference/choiced/antag_opt_in_status + category = PREFERENCE_CATEGORY_NON_CONTEXTUAL + savefile_identifier = PREFERENCE_CHARACTER + savefile_key = "antag_opt_in_status_pref" + +/datum/preference/choiced/antag_opt_in_status/init_possible_values() + return list(OPT_IN_YES_TEMP, OPT_IN_YES_KILL, OPT_IN_YES_ROUND_REMOVE, OPT_IN_NOT_TARGET) + +/datum/preference/choiced/antag_opt_in_status/create_default_value() + return OPT_IN_DEFAULT_LEVEL + +/datum/preference/choiced/antag_opt_in_status/is_accessible(datum/preferences/preferences) + if (!..(preferences)) + return FALSE + + return !(CONFIG_GET(flag/disable_antag_opt_in_preferences)) + +/datum/preference/choiced/antag_opt_in_status/deserialize(input, datum/preferences/preferences) + if(CONFIG_GET(flag/disable_antag_opt_in_preferences)) + return OPT_IN_DEFAULT_LEVEL + + return ..() + +/datum/preference/choiced/antag_opt_in_status/apply_to_human(mob/living/carbon/human/target, value, datum/preferences/preferences) + return FALSE + +/datum/preference/choiced/antag_opt_in_status/compile_constant_data() + var/list/data = ..() + + // An assoc list of values to display names so we don't show players numbers in their settings! + data[CHOICED_PREFERENCE_DISPLAY_NAMES] = GLOB.antag_opt_in_strings + + return data diff --git a/modular_nova/modules/antag_opt_in/code/job.dm b/modular_nova/modules/antag_opt_in/code/job.dm new file mode 100644 index 00000000000..1eb819d5c99 --- /dev/null +++ b/modular_nova/modules/antag_opt_in/code/job.dm @@ -0,0 +1,72 @@ +/datum/job + /// The minimum antag opt-in any holder of this job must use. If null, will defer to the mind's opt in level. + var/minimum_opt_in_level + /// Can this job be targetted as a heretic sacrifice target? + var/heretic_sac_target + /// Is this job targetable by contractors? + var/contractable + +/// Updates [minimum_opt_in_level] [heretic_sac_target] and [contractable]. +/datum/job/proc/update_opt_in_vars() + if(CONFIG_GET(flag/disable_antag_opt_in_preferences)) + return + + if(isnull(minimum_opt_in_level)) + minimum_opt_in_level = get_initial_opt_in_level() + if(isnull(heretic_sac_target)) + heretic_sac_target = initialize_heretic_target_status() + if(isnull(contractable)) + contractable = initialize_contractable_status() + + update_opt_in_desc_suffix() + +/// Returns this job's initial opt in level, taking into account departmental bitflags. +/datum/job/proc/get_initial_opt_in_level() + if (departments_bitflags & (DEPARTMENT_BITFLAG_SECURITY)) + return SECURITY_OPT_IN_LEVEL + if (departments_bitflags & (DEPARTMENT_BITFLAG_COMMAND)) + return COMMAND_OPT_IN_LEVEL + +/// Determines if this job should be sacrificable by heretics. +/datum/job/proc/initialize_heretic_target_status() + if (departments_bitflags & (DEPARTMENT_BITFLAG_SECURITY | DEPARTMENT_BITFLAG_COMMAND)) + return TRUE + + return FALSE + +/// Determines if this job should be targetable by contractors. +/datum/job/proc/initialize_contractable_status() + if (departments_bitflags & (DEPARTMENT_BITFLAG_SECURITY | DEPARTMENT_BITFLAG_COMMAND)) + return TRUE + + return FALSE + +/// Generates and sets a suffix appended to our description detailing our opt-in variables. +/datum/job/proc/update_opt_in_desc_suffix() + var/list/suffixes = list() + + if (minimum_opt_in_level) + suffixes += " Forces a minimum of [GLOB.antag_opt_in_strings["[minimum_opt_in_level]"]] antag opt-in." + if (contractable) + suffixes += " Targetable by contractors." + if (heretic_sac_target) + suffixes += " Targetable by heretics." + if (length(suffixes)) + var/suffix = jointext(suffixes, "") + set_opt_in_desc_suffix(suffix) + +/// Setter for [new_suffix]. Resets desc then appends the new suffix. +/datum/job/proc/set_opt_in_desc_suffix(new_suffix) + description = initial(description) + + if (new_suffix) + description += new_suffix + +/datum/controller/subsystem/job/SetupOccupations() + . = ..() + + if(CONFIG_GET(flag/disable_antag_opt_in_preferences)) + return + + for(var/datum/job/job as anything in all_occupations) + job.update_opt_in_vars() diff --git a/modular_nova/modules/antag_opt_in/code/mind.dm b/modular_nova/modules/antag_opt_in/code/mind.dm new file mode 100644 index 00000000000..91224664bf5 --- /dev/null +++ b/modular_nova/modules/antag_opt_in/code/mind.dm @@ -0,0 +1,92 @@ +/// If a player has any of these enabled, they are forced to use a minimum of OPT_IN_ANTAG_ENABLED_LEVEL antag optin. Dynamic - checked on the fly, not cached. +GLOBAL_LIST_INIT(optin_forcing_midround_antag_categories, list( + ROLE_CHANGELING_MIDROUND, + ROLE_MALF_MIDROUND, + ROLE_OBSESSED, + ROLE_SLEEPER_AGENT, +)) + +/// If a player has any of these enabled ON SPAWN, they are forced to use a minimum of OPT_IN_ANTAG_ENABLED_LEVEL antag optin for the rest of the round. +GLOBAL_LIST_INIT(optin_forcing_on_spawn_antag_categories, list( + ROLE_BROTHER, + ROLE_CHANGELING, + ROLE_CULTIST, + ROLE_HERETIC, + ROLE_MALF, + ROLE_OPERATIVE, + ROLE_TRAITOR, + ROLE_WIZARD, + ROLE_ASSAULT_OPERATIVE, + ROLE_CLOWN_OPERATIVE, + ROLE_NUCLEAR_OPERATIVE, + ROLE_HERETIC_SMUGGLER, + ROLE_PROVOCATEUR, + ROLE_STOWAWAY_CHANGELING, + ROLE_SYNDICATE_INFILTRATOR, +)) + +/datum/mind + /// The optin level set by preferences. + var/ideal_opt_in_level = OPT_IN_DEFAULT_LEVEL + /// Set on the FIRST mob login. Set by on-spawn antags (e.g. if you have traitor on and spawn, this will be set to OPT_IN_ANTAG_ENABLED_LEVEL and cannot change) + var/on_spawn_antag_opt_in_level = OPT_IN_NOT_TARGET + /// Set to TRUE on a successful transfer_mind() call. If TRUE, transfer_mind() will not refresh opt in. + var/opt_in_initialized + +/mob/living/Login() + . = ..() + + if (isnull(mind)) + return + if (isnull(client?.prefs)) + return + if (!mind.opt_in_initialized) + mind.update_opt_in(client.prefs) + mind.send_antag_optin_reminder() + mind.opt_in_initialized = TRUE + +/// Refreshes our ideal/on spawn antag opt in level by accessing preferences. +/datum/mind/proc/update_opt_in(datum/preferences/preference_instance = GLOB.preferences_datums[lowertext(key)]) + if (isnull(preference_instance)) + return + + ideal_opt_in_level = preference_instance.read_preference(/datum/preference/choiced/antag_opt_in_status) + + if (preference_instance.read_preference(/datum/preference/toggle/be_antag)) + for (var/antag_category in GLOB.optin_forcing_on_spawn_antag_categories) + if (antag_category in preference_instance.be_special) + on_spawn_antag_opt_in_level = OPT_IN_ANTAG_ENABLED_LEVEL + break + +/// Sends a bold message to our holder, telling them if their optin setting has been set to a minimum due to their antag preferences. +/datum/mind/proc/send_antag_optin_reminder() + var/datum/preferences/preference_instance = GLOB.preferences_datums[lowertext(key)] + var/client/our_client = preference_instance?.parent // that moment when /mind doesnt have a ref to client :) + if (our_client) + var/antag_level = get_antag_opt_in_level() + if (antag_level <= OPT_IN_NOT_TARGET) + return + var/stringified_level = GLOB.antag_opt_in_strings["[antag_level]"] + to_chat(our_client, span_boldnotice("Due to your antag preferences, your antag-optin status has been set to a minimum of [stringified_level].")) + +/// Gets the actual opt-in level used for determining targets. +/datum/mind/proc/get_effective_opt_in_level() + var/step_1 = max(ideal_opt_in_level, get_job_opt_in_level()) + var/step_2 = max(step_1, get_antag_opt_in_level()) + return step_2 + +/// Returns the opt in level of our job. +/datum/mind/proc/get_job_opt_in_level() + return assigned_role?.minimum_opt_in_level || OPT_IN_NOT_TARGET + +/// If we have any antags enabled in GLOB.optin_forcing_midround_antag_categories, returns OPT_IN_ANTAG_ENABLED_LEVEL. OPT_IN_NOT_TARGET otherwise. +/datum/mind/proc/get_antag_opt_in_level() + if (on_spawn_antag_opt_in_level > OPT_IN_NOT_TARGET) + return on_spawn_antag_opt_in_level + + var/datum/preferences/preference_instance = GLOB.preferences_datums[lowertext(key)] + if (!isnull(preference_instance) && preference_instance.read_preference(/datum/preference/toggle/be_antag)) + for (var/antag_category in GLOB.optin_forcing_midround_antag_categories) + if (antag_category in preference_instance.be_special) + return OPT_IN_ANTAG_ENABLED_LEVEL + return OPT_IN_NOT_TARGET diff --git a/modular_nova/modules/antag_opt_in/code/objective.dm b/modular_nova/modules/antag_opt_in/code/objective.dm new file mode 100644 index 00000000000..1c806b04957 --- /dev/null +++ b/modular_nova/modules/antag_opt_in/code/objective.dm @@ -0,0 +1,74 @@ +/datum/objective + /// The default opt in level of this objective. Only targets with opt in above or at this will be considered for this objective. + var/default_opt_in_level = OPT_IN_YES_KILL + +/// Simple getter for [default_opt_in_level]. Use for custom behavior. +/datum/objective/proc/get_opt_in_level(datum/mind/target_mind) + return default_opt_in_level + +/// Returns whether or not our opt in levels/variables are correct for the target. If true, they can be picked as a target. +/datum/objective/proc/opt_in_valid(datum/mind/target_mind) + return (get_opt_in_level(target_mind) <= target_mind.get_effective_opt_in_level()) + +// ROUND REMOVE +/datum/objective/maroon + default_opt_in_level = OPT_IN_YES_ROUND_REMOVE + +/datum/objective/assassinate/paradox_clone + default_opt_in_level = OPT_IN_YES_ROUND_REMOVE + +/datum/objective/capture + default_opt_in_level = OPT_IN_YES_ROUND_REMOVE + +/datum/objective/absorb + default_opt_in_level = OPT_IN_YES_ROUND_REMOVE + +/datum/objective/absorb_changeling + default_opt_in_level = OPT_IN_YES_ROUND_REMOVE + +/datum/objective/sacrifice + default_opt_in_level = OPT_IN_YES_ROUND_REMOVE + +/datum/objective/debrain + default_opt_in_level = OPT_IN_YES_ROUND_REMOVE + +// KILL + +/datum/objective/assassinate + default_opt_in_level = OPT_IN_YES_KILL + +/datum/objective/destroy + default_opt_in_level = OPT_IN_YES_KILL + +/datum/objective/mutiny + default_opt_in_level = OPT_IN_YES_KILL + +// TEMP + +/datum/objective/protect + default_opt_in_level = OPT_IN_YES_TEMP + +/datum/objective/protect/nonhuman + default_opt_in_level = OPT_IN_YES_TEMP + +/datum/objective/steal_n_of_type + default_opt_in_level = OPT_IN_YES_TEMP + +/datum/objective/steal + default_opt_in_level = OPT_IN_YES_TEMP + +/datum/objective/escape/escape_with_identity + default_opt_in_level = OPT_IN_YES_TEMP + +/datum/objective/jailbreak + default_opt_in_level = OPT_IN_YES_TEMP + +/datum/objective/contract + default_opt_in_level = OPT_IN_YES_TEMP + +/datum/objective/contract/opt_in_valid(datum/mind/target_mind) + var/datum/job/target_job = target_mind.assigned_role + if (!target_job?.contractable) + return FALSE + + return ..() diff --git a/modular_nova/modules/antag_opt_in/code/objective_item.dm b/modular_nova/modules/antag_opt_in/code/objective_item.dm new file mode 100644 index 00000000000..32c89aa9238 --- /dev/null +++ b/modular_nova/modules/antag_opt_in/code/objective_item.dm @@ -0,0 +1,20 @@ +/datum/objective_item + /// The opt in level all owners of the item must meet for this to be eligible as an objective target. + var/opt_in_level = OPT_IN_YES_TEMP + +/// Returns TRUE if we have no owners, or all owners's effective opt in level is above [opt_in_level]. FALSE otherwise. +/datum/objective_item/proc/owner_opted_in() + if (!length(item_owner)) + return TRUE + for (var/mob/living/player as anything in GLOB.player_list) + if ((player.mind?.assigned_role.title in item_owner) && player.stat != DEAD && !is_centcom_level(player.z)) // is an owner, copypasted from objective_items.dm owner_exists() + if (player.mind.get_effective_opt_in_level() < opt_in_level) + return FALSE + return TRUE + +/datum/objective_item/valid_objective_for(list/potential_thieves, require_owner) + var/opt_in_disabled = CONFIG_GET(flag/disable_antag_opt_in_preferences) + if (!opt_in_disabled && require_owner && !owner_opted_in()) + return FALSE + + return ..() diff --git a/modular_nova/modules/antag_opt_in/readme.md b/modular_nova/modules/antag_opt_in/readme.md new file mode 100644 index 00000000000..de41e00958b --- /dev/null +++ b/modular_nova/modules/antag_opt_in/readme.md @@ -0,0 +1,33 @@ +https://github.com/NovaSector/NovaSector/pull/121 + +## \ + +Module ID: ANTAG_OPTIN + +### Description: + + +Adds functionality to allow players to 'opt-in' to being an antagonist's mechanical target, with three different levels of involvement - being temporarily inconvenienced, killed, and round removed. Command roles & security are automatically opted-in to at least 'KILL' level. Additionally, contractor & heretic have their objectives adjusted to only have command staff & security as their targets + +### TG Proc/File Changes: + +- Changes in several antag files (will list later) +- examine_tgui.dm (Adds opt in info to OOC examine info) +- objective.dm (target selection stuff) + +### Modular Overrides: + +- N/A + +### Defines: + +- antag_opt_in - lives in ~skyrat_defines located in __DEFINES folder. Defines named OPT_IN_YES_KILL, OPT_IN_YES_TEMP, OPT_IN_YES_ROUND_REMOVE, and OPT_IN_OPT_IN_NOT_TARGET - used for managing opt in stuff. + +### Included files that are not contained in this module: + +- tgui\packages\tgui\interfaces\PreferencesMenu\preferences\features\character_preferences\skyrat\antag_optin.tsx + +### Credits: + +- niko - for doing stuff and taking over +- plum - the original author diff --git a/modular_nova/modules/customization/modules/mob/living/silicon/examine.dm b/modular_nova/modules/customization/modules/mob/living/silicon/examine.dm index 3eb789b6a28..f83c5843116 100644 --- a/modular_nova/modules/customization/modules/mob/living/silicon/examine.dm +++ b/modular_nova/modules/customization/modules/mob/living/silicon/examine.dm @@ -17,6 +17,13 @@ var/erp_status_pref = client.prefs.read_preference(/datum/preference/choiced/erp_status) if(erp_status_pref && !CONFIG_GET(flag/disable_erp_preferences)) . += span_notice("ERP STATUS: [erp_status_pref]") + + if (!CONFIG_GET(flag/disable_antag_opt_in_preferences)) + var/opt_in_status = mind?.get_effective_opt_in_level() + if (!isnull(opt_in_status)) + var/stringified_optin = GLOB.antag_opt_in_strings["[opt_in_status]"] + . += span_notice("Antag Opt-in Status: <b><font color='[GLOB.antag_opt_in_colors[stringified_optin]]'>[stringified_optin]</font></b>") + if(temporary_flavor_text) if(length_char(temporary_flavor_text) <= 40) . += span_notice("<b>They look different than usual:</b> [temporary_flavor_text]") diff --git a/modular_nova/modules/opposing_force/code/opposing_force_datum.dm b/modular_nova/modules/opposing_force/code/opposing_force_datum.dm index 3f688332cb5..e6f3571a10a 100644 --- a/modular_nova/modules/opposing_force/code/opposing_force_datum.dm +++ b/modular_nova/modules/opposing_force/code/opposing_force_datum.dm @@ -216,6 +216,16 @@ ) data["selected_equipment"] += list(equipment_data) + data["current_crew"] = generate_optin_crew_list() + + return data + +/datum/opposing_force/ui_static_data(mob/user) + var/list/data = list() + + data["opt_in_colors"] = GLOB.antag_opt_in_colors + data["opt_in_enabled"] = (!CONFIG_GET(flag/disable_antag_opt_in_preferences)) + return data /datum/opposing_force/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) @@ -1021,3 +1031,25 @@ return opfor.ui_interact(usr) + +/proc/generate_optin_crew_list() + var/list/output = list() + + for (var/datum/record/locked/iterated_record as anything in GLOB.manifest.locked) + var/datum/mind/mind_datum = iterated_record.mind_ref.resolve() + if (!istype(mind_datum)) + continue + var/name = iterated_record.name + var/rank = iterated_record.rank + + var/opt_in_status = mind_datum.get_effective_opt_in_level() + var/ideal_opt_in_status = mind_datum.ideal_opt_in_level + + output += list(list( + "name" = name, + "rank" = rank, + "opt_in_status" = GLOB.antag_opt_in_strings["[opt_in_status]"], + "ideal_opt_in_status" = GLOB.antag_opt_in_strings["[ideal_opt_in_status]"] + )) + + return output diff --git a/tgstation.dme b/tgstation.dme index 8e80cbb8e44..725b5a9dca8 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -399,6 +399,7 @@ #include "code\__DEFINES\~nova_defines\admin.dm" #include "code\__DEFINES\~nova_defines\airlock.dm" #include "code\__DEFINES\~nova_defines\ammo_defines.dm" +#include "code\__DEFINES\~nova_defines\antag_optin_defines.dm" #include "code\__DEFINES\~nova_defines\antagonists.dm" #include "code\__DEFINES\~nova_defines\apc_defines.dm" #include "code\__DEFINES\~nova_defines\armaments.dm" @@ -6646,6 +6647,12 @@ #include "modular_nova\modules\alternative_job_titles\code\job.dm" #include "modular_nova\modules\ammo_workbench\code\ammo_workbench.dm" #include "modular_nova\modules\ammo_workbench\code\design_disks.dm" +#include "modular_nova\modules\antag_opt_in\code\antag_optin_config.dm" +#include "modular_nova\modules\antag_opt_in\code\antag_optin_preferences.dm" +#include "modular_nova\modules\antag_opt_in\code\job.dm" +#include "modular_nova\modules\antag_opt_in\code\mind.dm" +#include "modular_nova\modules\antag_opt_in\code\objective.dm" +#include "modular_nova\modules\antag_opt_in\code\objective_item.dm" #include "modular_nova\modules\apc_arcing\apc.dm" #include "modular_nova\modules\armaments\code\armament_component.dm" #include "modular_nova\modules\armaments\code\armament_entries.dm" diff --git a/tgui/packages/tgui/interfaces/ExaminePanel.jsx b/tgui/packages/tgui/interfaces/ExaminePanel.jsx index 7ed8129e4ba..b8807db8c24 100644 --- a/tgui/packages/tgui/interfaces/ExaminePanel.jsx +++ b/tgui/packages/tgui/interfaces/ExaminePanel.jsx @@ -43,6 +43,9 @@ export const ExaminePanel = (props) => { custom_species, custom_species_lore, headshot, + ideal_antag_optin_status, + current_antag_optin_status, + opt_in_colors = { optin, color }, } = data; return ( <Window title="Examine Panel" width={900} height={670} theme="admin"> @@ -105,6 +108,29 @@ export const ExaminePanel = (props) => { title="OOC Notes" preserveWhitespace > + {ideal_antag_optin_status && ( + <Stack.Item> + Current Antag Opt-In Status:{' '} + <span + style={{ + fontWeight: 'bold', + color: opt_in_colors[current_antag_optin_status], + }} + > + {current_antag_optin_status} + </span> + {'\n'} + Antag Opt-In Status {'(Preferences)'}:{' '} + <span + style={{ + color: opt_in_colors[ideal_antag_optin_status], + }} + > + {ideal_antag_optin_status} + </span> + {'\n\n'} + </Stack.Item> + )} {formatURLs(ooc_notes)} </Section> </Stack.Item> diff --git a/tgui/packages/tgui/interfaces/OpposingForcePanel.jsx b/tgui/packages/tgui/interfaces/OpposingForcePanel.jsx index 76d2f11b470..7f25e09847d 100644 --- a/tgui/packages/tgui/interfaces/OpposingForcePanel.jsx +++ b/tgui/packages/tgui/interfaces/OpposingForcePanel.jsx @@ -22,7 +22,7 @@ import { Window } from '../layouts'; export const OpposingForcePanel = (props) => { const [tab, setTab] = useState(1); const { act, data } = useBackend(); - const { admin_mode, creator_ckey, owner_antag } = data; + const { admin_mode, creator_ckey, owner_antag, opt_in_enabled } = data; return ( <Window title={'Opposing Force: ' + creator_ckey} @@ -74,6 +74,15 @@ export const OpposingForcePanel = (props) => { > Admin Chat </Tabs.Tab> + {!!opt_in_enabled && ( + <Tabs.Tab + width="100%" + selected={tab === 4} + onClick={() => setTab(4)} + > + Target List + </Tabs.Tab> + )} </> )} </Tabs> @@ -89,6 +98,7 @@ export const OpposingForcePanel = (props) => { {tab === 1 && <OpposingForceTab />} {tab === 2 && <EquipmentTab />} {tab === 3 && <AdminChatTab />} + {tab === 4 && <TargetTab />} </> )} </Window.Content> @@ -910,3 +920,39 @@ export const AdminTab = (props) => { </Stack> ); }; + +export const TargetTab = (props) => { + const { act, data } = useBackend(); + const { current_crew = [], opt_in_colors = { optin, color } } = data; + return ( + <Stack vertical fill> + <Stack.Item grow={10}> + <Section title="Currently active crew"> + {current_crew.map((crew) => ( + <Stack vertical={false} key={crew.name} pb="10px"> + <Stack.Item> + <span style={{ textDecoration: 'underline' }}>{crew.name}</span> + {': '} + {crew.rank}, Current Opt-In status:{' '} + <span + style={{ + fontWeight: 'bold', + color: opt_in_colors[crew.opt_in_status], + }} + > + {crew.opt_in_status} + </span> + , Ideal Opt-in status:{' '} + <span + style={{ color: opt_in_colors[crew.ideal_opt_in_status] }} + > + {crew.ideal_opt_in_status} + </span> + </Stack.Item> + </Stack> + ))} + </Section> + </Stack.Item> + </Stack> + ); +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/nova/antag_optin.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/nova/antag_optin.tsx new file mode 100644 index 00000000000..8e3032932a2 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/nova/antag_optin.tsx @@ -0,0 +1,13 @@ +// THIS IS A NOVA SECTOR UI FILE +import { FeatureChoiced, FeatureDropdownInput } from '../../base'; + +export const antag_opt_in_status_pref: FeatureChoiced = { + name: 'Be Antagonist Target', + description: + 'This is for objective targetting and OOC consent.\ + By extension, picking "Round Remove" will allow you to be round removed in applicable situations, even by non-antagonists. \ + Enabling any non-ghost antags \ + (revenant, abductor contractor, etc.) will force your opt-in to be, \ + at minimum, "Temporarily Inconvenience".', + component: FeatureDropdownInput, +};