mirror of
https://github.com/ParadiseSS13/Paradise.git
synced 2026-08-29 06:57:42 +01:00
Implements dynamic latespawns (#31005)
* first pass * Latespawning now works for implied rulesets * Prevent team antags from getting latespawns. * calculate_budget() formula no longer gives half-points * fix mindflayer ruleset not updating banned_jobs * update dynamic budget unit test for new formula * tweak latespawn budget threshold * remove autotraitor ruleset * use the right logging proc this time * oops * Logging tweaks. log_dynamic can now output to log_debug. * Logging tweaks. Don't log crew join/cryo when latespawns are disabled. * Fix rare double antag spawn with implied ruleset trigger. * add heretic ruleset * prevent dynamic antag_budget from going negative. * implement add/remove_owner_to_gamemode() for changeling datum
This commit is contained in:
@@ -51,6 +51,15 @@ GLOBAL_PROTECT(log_end)
|
||||
if(GLOB.configuration.logging.vote_logging)
|
||||
rustlibs_log_write(GLOB.world_game_log, "VOTE: [text][GLOB.log_end]")
|
||||
|
||||
/proc/log_dynamic(text, important = FALSE)
|
||||
log_game("Dynamic: [text]")
|
||||
var/datum/game_mode/dynamic/dynamic = SSticker.mode
|
||||
if(!istype(dynamic))
|
||||
return
|
||||
if(important)
|
||||
log_debug("\[Dynamic\] [text]")
|
||||
dynamic.dynamic_log += text
|
||||
|
||||
/proc/log_if_mismatch(mob/who, message, automatic = FALSE)
|
||||
if(istype(usr, /mob) && istype(who) && usr.last_known_ckey != who.last_known_ckey)
|
||||
if(automatic)
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
|
||||
/// Rulesets that cannot be rolled while this ruleset is active. Used to prevent traitors from rolling while theres cultists, etc.
|
||||
var/list/banned_mutual_rulesets = list(
|
||||
/datum/ruleset/traitor/autotraitor,
|
||||
/datum/ruleset/team/cult,
|
||||
)
|
||||
|
||||
@@ -58,8 +57,10 @@
|
||||
var/antag_amount = 0
|
||||
/// All of the minds that we will make into our antagonist type
|
||||
var/list/datum/mind/pre_antags = list()
|
||||
/// If non-zero, how long from the start of the game should a latespawn for this role occur?
|
||||
var/latespawn_time
|
||||
/// If non-zero, how long from the start of the game should this role enable latespawns?
|
||||
var/latespawn_time = 0
|
||||
/// Should this ruleset roll latespawns?
|
||||
var/latespawns_enabled = TRUE
|
||||
|
||||
/datum/ruleset/Destroy(force, ...)
|
||||
stack_trace("[src] ([type]) was destroyed.")
|
||||
@@ -139,45 +140,60 @@
|
||||
|
||||
// Assemble a list of active players without jobbans.
|
||||
for(var/mob/living/carbon/human/player in GLOB.player_list)
|
||||
// Has a mind
|
||||
if(!player.mind)
|
||||
continue
|
||||
// Connected and not AFK
|
||||
if(!player.client || (locate(player) in SSafk.afk_players))
|
||||
continue
|
||||
// Not antag-banned and not specific antag banned
|
||||
if(jobban_isbanned(player, ROLE_SYNDICATE) || jobban_isbanned(player, antagonist_type::job_rank))
|
||||
continue
|
||||
// Make sure they want to play antag, and that they're not already something (off station or antag)
|
||||
if(player.client.persistent.skip_antag || player.mind.offstation_role || player.mind.special_role)
|
||||
continue
|
||||
// Make sure they actually want to be this antagonist
|
||||
if(!(antagonist_type::job_rank in player.client.prefs.be_special))
|
||||
continue
|
||||
// Make sure their species CAN be this antagonist
|
||||
if(EXCLUSIVE_OR(player.dna.species.name in banned_species, banned_species_only))
|
||||
continue
|
||||
// Make sure they're not in a banned job
|
||||
if(player.mind.assigned_role in banned_jobs)
|
||||
continue
|
||||
|
||||
candidates += player.mind
|
||||
if(is_valid_candidate(player))
|
||||
// We can't return the mind since we need to check the candidate's species while latespawning.
|
||||
candidates += player
|
||||
|
||||
return shuffle(candidates)
|
||||
|
||||
/datum/ruleset/proc/latespawn(datum/game_mode/dynamic/dynamic)
|
||||
// latespawning is only used by traitors at this point, so we're just going to be naive and allocate all budget when this proc is called.
|
||||
var/late_antag_amount = floor(dynamic.antag_budget / antag_cost)
|
||||
dynamic.antag_budget -= (late_antag_amount * antag_cost)
|
||||
/datum/ruleset/proc/is_valid_candidate(mob/living/carbon/human/player)
|
||||
// Has a mind
|
||||
if(!player.mind)
|
||||
return FALSE
|
||||
// Connected and not AFK
|
||||
if(!player.client || (locate(player) in SSafk.afk_players))
|
||||
return FALSE
|
||||
// Not antag-banned and not specific antag banned
|
||||
if(jobban_isbanned(player, ROLE_SYNDICATE) || jobban_isbanned(player, antagonist_type::job_rank))
|
||||
return FALSE
|
||||
// Make sure they want to play antag, and that they're not already something (off station or antag)
|
||||
if(player.client.persistent.skip_antag || player.mind.offstation_role || player.mind.special_role)
|
||||
return FALSE
|
||||
// Make sure they actually want to be this antagonist
|
||||
if(!(antagonist_type::job_rank in player.client.prefs.be_special))
|
||||
return FALSE
|
||||
// Make sure they're not in a banned job
|
||||
if(player.mind.assigned_role in banned_jobs)
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
var/list/datum/mind/possible_antags = get_latejoin_players()
|
||||
for(var/i in 1 to late_antag_amount)
|
||||
var/datum/mind/antag = pick_n_take(possible_antags)
|
||||
antag.add_antag_datum(antagonist_type)
|
||||
/datum/ruleset/proc/enable_latespawns()
|
||||
latespawns_enabled = TRUE
|
||||
|
||||
/datum/ruleset/proc/latespawn(datum/game_mode/dynamic/dynamic)
|
||||
var/list/mob/living/carbon/human/possible_antags = get_latejoin_players()
|
||||
|
||||
if(length(possible_antags) < antag_amount)
|
||||
var/wasted_budget = (antag_amount - length(possible_antags)) * antag_cost
|
||||
dynamic.antag_budget += wasted_budget
|
||||
log_dynamic("Tried to roll [antag_amount] [name], but there were only [length(possible_antags)] candidates! Refunding [wasted_budget] budget.", TRUE)
|
||||
antag_amount = length(possible_antags)
|
||||
|
||||
var/antags_rolled = 0
|
||||
for(var/mob/living/carbon/human/player as anything in possible_antags)
|
||||
if(antag_amount <= 0)
|
||||
break
|
||||
// This check needs to be here so that we can signal the implied ruleset. Yes, this is jank.
|
||||
if(EXCLUSIVE_OR(player.dna.species.name in banned_species, banned_species_only))
|
||||
SEND_SIGNAL(src, COMSIG_RULESET_FAILED_SPECIES)
|
||||
break
|
||||
player.mind.add_antag_datum(antagonist_type)
|
||||
antag_amount -= 1
|
||||
antags_rolled += 1
|
||||
SSblackbox.record_feedback("nested tally", "dynamic_selections", 1, list("latespawn", "[antagonist_type]"))
|
||||
|
||||
log_dynamic("Latespawned [late_antag_amount] [name]\s.")
|
||||
message_admins("Dynamic latespawned [late_antag_amount] [name]\s.")
|
||||
log_dynamic("Latespawned [antags_rolled] [name]\s from [length(possible_antags)] candidates.", TRUE)
|
||||
message_admins("Dynamic latespawned [antags_rolled] [name]\s.")
|
||||
|
||||
/datum/ruleset/proc/automatic_deduct(budget)
|
||||
. = antag_cost * antag_amount
|
||||
@@ -204,22 +220,6 @@
|
||||
antag.add_antag_datum(traitor_datum)
|
||||
SSblackbox.record_feedback("nested tally", "dynamic_selections", 1, list("roundstart", "[antagonist_type]"))
|
||||
|
||||
/datum/ruleset/traitor/autotraitor
|
||||
name = "Autotraitor"
|
||||
ruleset_weight = 2
|
||||
antag_cost = 10
|
||||
banned_mutual_rulesets = list(
|
||||
/datum/ruleset/traitor,
|
||||
/datum/ruleset/vampire,
|
||||
/datum/ruleset/changeling,
|
||||
/datum/ruleset/team/cult
|
||||
)
|
||||
|
||||
/datum/ruleset/traitor/autotraitor/roundstart_post_setup(datum/game_mode/dynamic)
|
||||
. = ..()
|
||||
latespawn_time = null
|
||||
addtimer(CALLBACK(src, PROC_REF(latespawn), dynamic), 5 MINUTES, TIMER_DELETE_ME|TIMER_LOOP)
|
||||
|
||||
/datum/ruleset/heretic
|
||||
name = "Heretic"
|
||||
ruleset_weight = 10
|
||||
@@ -228,7 +228,6 @@
|
||||
|
||||
banned_jobs = list("Cyborg", "AI")
|
||||
|
||||
|
||||
/datum/ruleset/vampire
|
||||
name = "Vampire"
|
||||
ruleset_weight = 12
|
||||
@@ -275,6 +274,11 @@
|
||||
/datum/ruleset/implied/proc/on_implied(datum/antagonist/implier)
|
||||
stack_trace("[type]/on_implied() not implemented!")
|
||||
|
||||
/datum/ruleset/implied/roundstart_pre_setup()
|
||||
// antag_amount is always 0 when this proc is called, so we need to update banned_jobs manually
|
||||
if(GLOB.configuration.gamemode.prevent_mindshield_antags)
|
||||
banned_jobs += protected_jobs
|
||||
|
||||
/datum/ruleset/implied/mindflayer
|
||||
name = "Mindflayer"
|
||||
antagonist_type = /datum/antagonist/mindflayer
|
||||
@@ -294,6 +298,7 @@
|
||||
/datum/ruleset/team
|
||||
name = "BASE TEAM RULESET"
|
||||
ruleset_weight = 0
|
||||
latespawns_enabled = FALSE
|
||||
/// Whether there should only be one of this kind of team. This could be used for blood-brothers if false.
|
||||
var/unique_team = TRUE
|
||||
/// How many players on a team.
|
||||
@@ -326,7 +331,6 @@
|
||||
antagonist_type = /datum/antagonist/cultist
|
||||
banned_mutual_rulesets = list(
|
||||
/datum/ruleset/traitor,
|
||||
/datum/ruleset/traitor/autotraitor,
|
||||
/datum/ruleset/vampire,
|
||||
/datum/ruleset/changeling
|
||||
)
|
||||
|
||||
@@ -14,6 +14,9 @@ GLOBAL_LIST_EMPTY(dynamic_forced_rulesets)
|
||||
/// Log for what happens in a dynamic round
|
||||
var/list/dynamic_log = list()
|
||||
|
||||
/// Minimum amount of budget required to start considering latespawn antags. 0 disables latespawn antags.
|
||||
var/min_latespawn_budget = 0
|
||||
|
||||
/datum/game_mode/dynamic/announce()
|
||||
to_chat(world, "<b>The current game mode is - Dynamic</b>")
|
||||
var/list/possible_rulesets = list()
|
||||
@@ -34,11 +37,11 @@ GLOBAL_LIST_EMPTY(dynamic_forced_rulesets)
|
||||
if(5 to 20)
|
||||
// +0.5 budget each for players 5-20
|
||||
// Cumulative total at 20 players: 15
|
||||
return 7 + 0.5 * (players - 4)
|
||||
return 7 + ceil(0.5 * (players - 4))
|
||||
if(21 to 30)
|
||||
// +1.5 budget each for players 21 to 30
|
||||
// Cumulative total at 30 players: 30
|
||||
return 15 + 1.5 * (players - 20)
|
||||
return 15 + ceil(1.5 * (players - 20))
|
||||
else
|
||||
// +1 budget each for players 31+, so just player count.
|
||||
return players
|
||||
@@ -113,36 +116,62 @@ GLOBAL_LIST_EMPTY(dynamic_forced_rulesets)
|
||||
if(!length(rulesets))
|
||||
log_dynamic("No rulesets in play.")
|
||||
return
|
||||
log_dynamic("Allocated antagonist budget: [antag_budget].")
|
||||
log_dynamic("Allocated antagonist budget: [antag_budget].", TRUE)
|
||||
|
||||
for(var/datum/ruleset/ruleset in rulesets)
|
||||
ruleset.antag_amount = 1
|
||||
antag_budget -= ruleset.automatic_deduct(antag_budget)
|
||||
|
||||
log_dynamic("Rulesets in play: [english_list((rulesets + implied_rulesets))]")
|
||||
log_dynamic("Rulesets in play: [english_list((rulesets + implied_rulesets))]", TRUE)
|
||||
|
||||
apply_antag_budget()
|
||||
|
||||
/datum/game_mode/dynamic/proc/apply_antag_budget() // todo, can be called later in the game to apply more budget. That also means there has to be shit done for latejoins.
|
||||
/datum/game_mode/dynamic/proc/apply_antag_budget(consider_latespawns = FALSE)
|
||||
var/list/temp_rulesets = rulesets.Copy()
|
||||
var/antags_rolled = 0
|
||||
while(antag_budget >= 0)
|
||||
var/datum/ruleset/ruleset = pickweight(temp_rulesets)
|
||||
if(!ruleset)
|
||||
log_dynamic("No rulesets remaining. Remaining budget: [antag_budget].")
|
||||
return
|
||||
if(!ruleset.antagonist_possible(antag_budget))
|
||||
log_dynamic("No rulesets remaining. Remaining budget: [antag_budget]. Antagonists rolled: [antags_rolled]")
|
||||
return antags_rolled
|
||||
if((consider_latespawns && !ruleset.latespawns_enabled) || !ruleset.antagonist_possible(antag_budget))
|
||||
log_dynamic("Rolled [ruleset.name]: failed, removing [ruleset.name] ruleset.")
|
||||
temp_rulesets -= ruleset
|
||||
continue
|
||||
ruleset.antag_amount++
|
||||
ruleset.antag_amount += 1
|
||||
antags_rolled += 1
|
||||
antag_budget -= ruleset.antag_cost
|
||||
log_dynamic("Rolled [ruleset.name]: success, +1 [ruleset.name]. Remaining budget: [antag_budget].")
|
||||
log_dynamic("No more antagonist budget remaining.")
|
||||
log_dynamic("No more antagonist budget remaining. Antagonists rolled: [antags_rolled]")
|
||||
return antags_rolled
|
||||
|
||||
/datum/game_mode/dynamic/proc/set_latespawn_budget()
|
||||
// Disable latespawns if there's no rulesets to roll from
|
||||
if(length(rulesets) <= 0)
|
||||
log_dynamic("Found zero rulesets. Disabling latespawns.", TRUE)
|
||||
return
|
||||
if(length(rulesets) == 1)
|
||||
var/datum/ruleset/ruleset = rulesets[1]
|
||||
if(!ruleset.latespawns_enabled)
|
||||
min_latespawn_budget = 0
|
||||
log_dynamic("The only ruleset, [ruleset.name], does not support latespawns. Disabling latespawns.", TRUE)
|
||||
return
|
||||
min_latespawn_budget = ruleset.antag_cost
|
||||
else
|
||||
var/max_cost = 0
|
||||
for(var/datum/ruleset/ruleset as anything in rulesets)
|
||||
if(!ruleset.latespawns_enabled)
|
||||
continue
|
||||
if(ruleset.antag_cost > max_cost)
|
||||
max_cost = ruleset.antag_cost
|
||||
min_latespawn_budget = max_cost
|
||||
log_dynamic("Latespawn budget threshold set to [min_latespawn_budget] from [length(rulesets)] ruleset(s).", TRUE)
|
||||
|
||||
/datum/game_mode/dynamic/pre_setup()
|
||||
var/watch = start_watch()
|
||||
log_dynamic("Starting dynamic setup.")
|
||||
allocate_ruleset_budget()
|
||||
set_latespawn_budget()
|
||||
log_dynamic("-=-=-=-=-=-=-=-=-=-=-=-=-")
|
||||
allocate_antagonist_budget()
|
||||
log_dynamic("=-=-=-=-=-=-=-=-=-=-=-=-=")
|
||||
@@ -150,7 +179,8 @@ GLOBAL_LIST_EMPTY(dynamic_forced_rulesets)
|
||||
for(var/datum/ruleset/ruleset in (rulesets + implied_rulesets)) // rulesets first, then implied rulesets
|
||||
log_dynamic("Applying [ruleset.antag_amount] [ruleset.name]\s.")
|
||||
antag_budget += ruleset.roundstart_pre_setup()
|
||||
|
||||
antag_budget = max(0, antag_budget)
|
||||
|
||||
log_dynamic("Budget overflow: [antag_budget].")
|
||||
// for the future, maybe try readding antagonists with apply_antag_budget(antag_budget)
|
||||
log_dynamic("Finished dynamic setup in [stop_watch(watch)]s.")
|
||||
@@ -162,26 +192,42 @@ GLOBAL_LIST_EMPTY(dynamic_forced_rulesets)
|
||||
log_dynamic("Making antag datums for [ruleset.name] ruleset.")
|
||||
ruleset.roundstart_post_setup(src)
|
||||
if(ruleset.latespawn_time)
|
||||
addtimer(CALLBACK(ruleset, TYPE_PROC_REF(/datum/ruleset, latespawn), src), ruleset.latespawn_time)
|
||||
log_dynamic("[ruleset]s will latespawn at [ruleset.latespawn_time / 600].")
|
||||
ruleset.latespawns_enabled = FALSE
|
||||
addtimer(CALLBACK(ruleset, TYPE_PROC_REF(/datum/ruleset, enable_latespawns)), ruleset.latespawn_time)
|
||||
log_dynamic("[ruleset]s will latespawn at [ruleset.latespawn_time / 600].", TRUE)
|
||||
..()
|
||||
|
||||
/datum/game_mode/dynamic/latespawn(mob)
|
||||
. = ..()
|
||||
antag_budget++
|
||||
if(min_latespawn_budget <= 0)
|
||||
return
|
||||
antag_budget += 1
|
||||
log_dynamic("Crew joined. New budget: [antag_budget] (latespawn in [min_latespawn_budget - antag_budget])", TRUE)
|
||||
if(antag_budget >= min_latespawn_budget)
|
||||
log_dynamic("Budget at latespawn threshold ([min_latespawn_budget]), attempting to buy antagonists.", TRUE)
|
||||
if(apply_antag_budget(TRUE))
|
||||
for(var/datum/ruleset/ruleset as anything in (rulesets + implied_rulesets))
|
||||
if(ruleset.antag_amount <= 0)
|
||||
continue
|
||||
ruleset.latespawn(src)
|
||||
else
|
||||
log_dynamic("No antagonists were rolled.", TRUE)
|
||||
|
||||
/datum/game_mode/dynamic/on_mob_cryo(mob/sleepy_mob, obj/machinery/cryopod/cryopod)
|
||||
if(min_latespawn_budget <= 0)
|
||||
return
|
||||
var/turf/T = get_turf(cryopod)
|
||||
if(!T || is_admin_level(T.z))
|
||||
return
|
||||
antag_budget--
|
||||
antag_budget = max(0, antag_budget - 1)
|
||||
if(!sleepy_mob.mind || !length(sleepy_mob.mind.antag_datums))
|
||||
log_dynamic("Crew cryo. New budget: [antag_budget]", TRUE)
|
||||
return
|
||||
for(var/datum/antagonist/antag in sleepy_mob.mind.antag_datums)
|
||||
for(var/datum/ruleset/possible_ruleset as anything in subtypesof(/datum/ruleset))
|
||||
if(istype(antag, possible_ruleset.antagonist_type))
|
||||
antag_budget += possible_ruleset.antag_cost
|
||||
log_dynamic("[possible_ruleset] cryo. +[possible_ruleset.antag_cost] budget.")
|
||||
antag_budget += possible_ruleset.antag_cost + 1
|
||||
log_dynamic("[possible_ruleset] cryo, refunded [possible_ruleset.antag_cost] budget. New budget: [antag_budget]", TRUE)
|
||||
|
||||
/datum/game_mode/dynamic/get_webhook_name()
|
||||
var/list/implied_and_used = list()
|
||||
@@ -195,9 +241,3 @@ GLOBAL_LIST_EMPTY(dynamic_forced_rulesets)
|
||||
ruleset.declare_completion()
|
||||
. = ..()
|
||||
|
||||
/proc/log_dynamic(text)
|
||||
log_game("Dynamic: [text]")
|
||||
var/datum/game_mode/dynamic/dynamic = SSticker.mode
|
||||
if(!istype(dynamic))
|
||||
return
|
||||
dynamic.dynamic_log += text
|
||||
|
||||
@@ -60,7 +60,6 @@ RESTRICT_TYPE(/datum/antagonist/changeling)
|
||||
purchaseable_powers = get_powers_of_type(CHANGELING_PURCHASABLE_POWER)
|
||||
|
||||
/datum/antagonist/changeling/on_gain()
|
||||
SSticker.mode.changelings |= owner
|
||||
var/honorific = owner.current.gender == FEMALE ? "Ms." : "Mr."
|
||||
if(length(GLOB.possible_changeling_IDs))
|
||||
changelingID = pick(GLOB.possible_changeling_IDs)
|
||||
@@ -79,12 +78,17 @@ RESTRICT_TYPE(/datum/antagonist/changeling)
|
||||
..()
|
||||
|
||||
/datum/antagonist/changeling/Destroy()
|
||||
SSticker.mode.changelings -= owner
|
||||
QDEL_LIST_CONTENTS(acquired_powers)
|
||||
STOP_PROCESSING(SSobj, src)
|
||||
chosen_sting = null
|
||||
return ..()
|
||||
|
||||
/datum/antagonist/changeling/add_owner_to_gamemode()
|
||||
SSticker.mode.changelings |= owner
|
||||
|
||||
/datum/antagonist/changeling/remove_owner_from_gamemode()
|
||||
SSticker.mode.changelings -= owner
|
||||
|
||||
/datum/antagonist/changeling/greet()
|
||||
. = ..()
|
||||
SEND_SOUND(owner.current, sound('sound/ambience/antag/ling_alert.ogg'))
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
TEST_ASSERT_EQUAL(dynamic.calculate_budget(0), 7, "Flat budget incorrect.")
|
||||
TEST_ASSERT_EQUAL(dynamic.calculate_budget(4), 7, "Flat budget incorrect.")
|
||||
|
||||
TEST_ASSERT_EQUAL(dynamic.calculate_budget(5), 7.5, "Lowpop budget incorrect.")
|
||||
TEST_ASSERT_EQUAL(dynamic.calculate_budget(5), 8, "Lowpop budget incorrect.")
|
||||
TEST_ASSERT_EQUAL(dynamic.calculate_budget(20), 15, "Lowpop budget incorrect.")
|
||||
|
||||
TEST_ASSERT_EQUAL(dynamic.calculate_budget(21), 16.5, "Midpop budget incorrect.")
|
||||
TEST_ASSERT_EQUAL(dynamic.calculate_budget(21), 17, "Midpop budget incorrect.")
|
||||
TEST_ASSERT_EQUAL(dynamic.calculate_budget(30), 30, "Midpop budget incorrect.")
|
||||
|
||||
TEST_ASSERT_EQUAL(dynamic.calculate_budget(31), 31, "Highpop budget incorrect.")
|
||||
|
||||
Reference in New Issue
Block a user