Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"Dynamic" Gamemode using rulesets #27600

Merged
merged 20 commits into from
Jan 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions code/__DEFINES/dcs/datum_signals.dm
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,8 @@
/// /datum/component/label
/// Called when a handlabeler is used on an item when off
#define COMSIG_LABEL_REMOVE "label_remove"

// /datum/ruleset

/// from base of /datum/ruleset/proc/can_apply()
#define COMSIG_RULESET_FAILED_SPECIES "failed_species"
2 changes: 2 additions & 0 deletions code/__DEFINES/directions.dm
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@
#define DIR_JUST_HORIZONTAL(dir) ((dir == EAST) || (dir == WEST))
/// returns TRUE if the direction is NORTH or SOUTH
#define DIR_JUST_VERTICAL(dir) ((dir == NORTH) || (dir == SOUTH))

#define EXCLUSIVE_OR(thing_one, thing_two) ((thing_one)^(thing_two))
11 changes: 11 additions & 0 deletions code/__DEFINES/gamemode.dm
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,14 @@
#define NUKE_SITE_OFF_STATION_ZLEVEL 2
/// The bomb's location cannot be found.
#define NUKE_SITE_INVALID 3

/**
* Dynamic Gamemode Defines
*/
#define DYNAMIC_RULESET_NORMAL "Normal"
#define DYNAMIC_RULESET_FORCED "<b>Forced</b>"
#define DYNAMIC_RULESET_BANNED "<b>Banned</b>"

#define RULESET_FAILURE_BUDGET "Not enough budget"
#define RULESET_FAILURE_NO_PLAYERS "No drafted players"
#define RULESET_FAILURE_CHANGELING_SECONDARY_RULESET "Needs a secondary ruleset in rotation"
3 changes: 3 additions & 0 deletions code/game/gamemodes/autotraitor/autotraitor.dm
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,6 @@
message_admins("New traitor roll passed. Making a new Traitor.")
log_game("New traitor roll passed. Making a new Traitor.")
character.mind.make_Traitor()

/datum/game_mode/traitor/autotraitor/on_mob_cryo(mob/sleepy_mob, obj/machinery/cryopod/cryopod)
possible_traitors.Remove(sleepy_mob)
192 changes: 192 additions & 0 deletions code/game/gamemodes/dynamic/antag_rulesets.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/**
* These are gamemode rulesets for the dynamic gamemode type. They determine what antagonists spawn during a round.
*/
/datum/ruleset
Contrabang marked this conversation as resolved.
Show resolved Hide resolved
/// What this ruleset is called
var/name = "BASE RULESET"
/// The cost to roll this ruleset
var/ruleset_cost = 1
/// The weight to roll this ruleset
var/ruleset_weight = 1
/// The cost to roll an antagonist of this ruleset
var/antag_cost = 1
/// The weight to roll an antagonist of this ruleset
var/antag_weight = 1
/// Antagonist datum to apply to users
var/datum/antagonist/antagonist_type
/// A ruleset to be added when this ruleset is selected by the gamemode
var/datum/ruleset/implied/implied_ruleset_type

/// These roles 100% cannot be this antagonist
var/list/banned_jobs = list("Cyborg")
/// These roles can't be antagonists because mindshielding (this can be disabled via config)
var/list/protected_jobs = list(
"Security Officer",
"Warden",
"Detective",
"Head of Security",
"Captain",
"Blueshield",
"Nanotrasen Representative",
"Magistrate",
"Internal Affairs Agent",
"Nanotrasen Career Trainer",
"Nanotrasen Navy Officer",
"Special Operations Officer",
"Trans-Solar Federation General"
)
/// Applies the mind roll to assigned_role, preventing them from rolling a normal job. Good for wizards and nuclear operatives.
var/assign_job_role = FALSE
/// A blacklist of species names that cannot play this antagonist
var/list/banned_species = list()
/// If true, the species blacklist is now a species whitelist
var/banned_species_only = FALSE

// var/list/banned_mutual_rulesets = list() // UNIMPLEMENTED: could be used to prevent nukies rolling while theres cultists, or wizards, etc

/* This stuff changes, all stuff above is static */
Contrabang marked this conversation as resolved.
Show resolved Hide resolved
/// How many antagonists to spawn
var/antag_amount = 0
/// All of the minds that we will make into our antagonist type
var/list/datum/mind/pre_antags = list()

/datum/ruleset/Destroy(force, ...)
stack_trace("[src] ([type]) was destroyed.")
return ..()

/datum/ruleset/proc/ruleset_possible(ruleset_budget, rulesets)
if(ruleset_budget < ruleset_cost)
return RULESET_FAILURE_BUDGET
if(!length(SSticker.mode.get_players_for_role(antagonist_type::job_rank))) // this specifically needs to be job_rank not special_rank
return RULESET_FAILURE_NO_PLAYERS

/datum/ruleset/proc/antagonist_possible(budget)
return budget >= antag_cost

/datum/ruleset/proc/pre_setup()
if(antag_amount == 0)
return
if(antag_amount < 0)
stack_trace("/datum/ruleset/proc/pre_setup() for [type] somehow had a negative antagonist amount")
return
var/list/datum/mind/possible_antags = SSticker.mode.get_players_for_role(antagonist_type::job_rank) // this specifically needs to be job_rank not special_rank
if(!length(possible_antags))
refund("No possible players for [src] ruleset.") // we allocate antag budget before we allocate players, and previous rulesets can steal our players
return antag_cost * antag_amount // shitty refund for now

if(GLOB.configuration.gamemode.prevent_mindshield_antags)
banned_jobs += protected_jobs

shuffle_inplace(possible_antags)
for(var/datum/mind/antag as anything in possible_antags)
if(antag_amount <= 0)
break
if(!can_apply(antag))
continue
pre_antags += antag
if(assign_job_role)
antag.assigned_role = antagonist_type::special_role
antag.special_role = antagonist_type::special_role
antag.restricted_roles = banned_jobs
antag_amount -= 1

if(antag_amount > 0)
refund("Missing [antag_amount] antagonists for [src] ruleset.")
return antag_cost * antag_amount // shitty refund for now

/datum/ruleset/proc/can_apply(datum/mind/antag)
if(EXCLUSIVE_OR(antag.current.client.prefs.active_character.species in banned_species, banned_species_only))
SEND_SIGNAL(src, COMSIG_RULESET_FAILED_SPECIES)
return FALSE
if(antag.special_role) // You can only have 1 antag roll at a time, sorry
return FALSE
return TRUE

/datum/ruleset/proc/post_setup(datum/game_mode/dynamic)
for(var/datum/mind/antag as anything in pre_antags)
antag.add_antag_datum(antagonist_type)

/datum/ruleset/proc/refund(info)
// not enough antagonists signed up!!! idk what to do. The only real solution is to procedurally allocate budget, which will result in 1000x more get_players_for_role() calls. Which is not cheap.
// OR we cache get_players_for_role() and then just check if they have a special_role. May be unreliable.
// log_dynamic("[info] Refunding [antag_cost * antag_amount] budget.")
// Currently unimplemented. Will be useful for a possible future PR where latejoin antagonists are factored in.
return

/datum/ruleset/traitor
name = "Traitor"
ruleset_weight = 11
antag_cost = 5
antag_weight = 2
antagonist_type = /datum/antagonist/traitor

/datum/ruleset/traitor/post_setup(datum/game_mode/dynamic)
var/random_time = rand(5 MINUTES, 15 MINUTES)
for(var/datum/mind/antag as anything in pre_antags)
var/datum/antagonist/traitor/traitor_datum = new antagonist_type()
if(ishuman(antag.current))
traitor_datum.delayed_objectives = TRUE
traitor_datum.addtimer(CALLBACK(traitor_datum, TYPE_PROC_REF(/datum/antagonist/traitor, reveal_delayed_objectives)), random_time, TIMER_DELETE_ME)
antag.add_antag_datum(traitor_datum)
addtimer(CALLBACK(dynamic, TYPE_PROC_REF(/datum/game_mode, fill_antag_slots)), random_time)

/datum/ruleset/vampire
name = "Vampire"
ruleset_weight = 12
antag_cost = 10
antagonist_type = /datum/antagonist/vampire

banned_jobs = list("Cyborg", "AI", "Chaplain")
banned_species = list("Machine")
implied_ruleset_type = /datum/ruleset/implied/mindflayer

/datum/ruleset/changeling
name = "Changeling"
ruleset_weight = 9
antag_cost = 10
antagonist_type = /datum/antagonist/changeling

banned_jobs = list("Cyborg", "AI")
banned_species = list("Machine")
implied_ruleset_type = /datum/ruleset/implied/mindflayer

/datum/ruleset/changeling/ruleset_possible(ruleset_budget, rulesets)
// Theres already a ruleset, we're good to go
if(length(rulesets))
return ..()
// We're the first ruleset, but we can afford another ruleset
if((ruleset_budget >= /datum/ruleset/traitor::ruleset_cost) || (ruleset_budget >= /datum/ruleset/vampire::ruleset_cost))
return ..()
return RULESET_FAILURE_CHANGELING_SECONDARY_RULESET

// This is the fucking worst, but its required to not change functionality with mindflayers. Cannot be rolled normally, this is applied by other methods.
/datum/ruleset/implied
// These 3 variables should never change
ruleset_cost = 0
ruleset_weight = 0
antag_weight = 0
// antag_cost is allowed to be edited to help with refunding antagonists
antag_cost = 0
/// This signal is registered on whatever (multiple) rulesets implied us. This will call on_implied.
var/target_signal
/// Set this to true if this implied ruleset was activated
var/was_triggered = FALSE

/datum/ruleset/implied/proc/on_implied(datum/antagonist/implier)
stack_trace("[type]/on_implied() not implemented!")

/datum/ruleset/implied/mindflayer
name = "Mindflayer"
antagonist_type = /datum/antagonist/mindflayer
antag_cost = 10
target_signal = COMSIG_RULESET_FAILED_SPECIES

banned_jobs = list("Cyborg", "AI")
banned_species = list("Machine")
banned_species_only = TRUE

/datum/ruleset/implied/mindflayer/on_implied(datum/ruleset/implier)
// log_dynamic("Rolled implied [name]: +1 [name], -1 [implier.name].")
implier.antag_amount -= 1
antag_amount += 1
was_triggered = TRUE
Loading
Loading