Files
vgstation13/code/datums/gamemode/dynamic/dynamic_rulesets.dm

364 lines
18 KiB
Plaintext

/datum/dynamic_ruleset
var/name = ""//For admin logging, and round end scoreboard
var/persistent = 0//if set to 1, the rule won't be discarded after being executed, and /gamemode/dynamic will call process() every MC tick
var/repeatable = 0//if set to 1, dynamic mode will be able to draft this ruleset again later on. (doesn't apply for roundstart rules)
var/list/candidates = list()//list of players that are being drafted for this rule
var/list/assigned = list()//list of players that were selected for this rule
var/datum/role/role_category = /datum/role/traitor //rule will only accept candidates with "Yes" or "Always" in the preferences for this role
var/list/protected_from_jobs = list() // if set, and config.protect_roles_from_antagonist = 0, then the rule will have a much lower chance than usual to pick those roles.
var/list/restricted_from_jobs = list()//if set, rule will deny candidates from those jobs
var/list/exclusive_to_jobs = list()//if set, rule will only accept candidates from those jobs
var/list/job_priority = list() //May be used by progressive_job_search for prioritizing some jobs for a role. Order matters.
var/list/enemy_jobs = list()//if set, there needs to be a certain amount of players doing those jobs (among the players who won't be drafted) for the rule to be drafted
var/list/required_pop = list(10,10,0,0,0,0,0,0,0,0)//if enemy_jobs was set, this is the amount of population required for the ruleset to fire. enemy jobs count double
var/required_enemies = list(0,0,0,0,0,0,0,0,0,0) //If set, the ruleset requires this many enemy jobs to be filled in order to fire (per threat level)
var/required_candidates = 0//the rule needs this many candidates (post-trimming) to be executed (example: Cult need 4 players at round start)
var/weight = 5//1 -> 9, probability for this rule to be picked against other rules
var/list/weekday_rule_boost = list()
var/list/timeslot_rule_boost = list()
var/cost = 0//threat cost for this rule.
var/logo = ""//any state from /icons/logos.dmi
var/calledBy //who dunnit, for round end scoreboard
var/flags = 0
var/stillborn = FALSE//executed when the round was already about to end
//for midround polling
var/list/applicants = list()
var/searching = 0
var/list/requirements = list(40,30,20,10,10,10,10,10,10,10)
//requirements are the threat level requirements per pop range. The ranges are as follow:
//0-4, 5-9, 10-14, 15-19, 20-24, 25-29, 30-34, 35-39, 40-54, 45+
//so with the above default values, The rule will never get drafted below 10 threat level (aka: "peaceful extended"), and it requires a higher threat level at lower pops.
//for reminder: the threat level is rolled at roundstart and tends to hover around 50 https://docs.google.com/spreadsheets/d/1QLN_OBHqeL4cm9zTLEtxlnaJHHUu0IUPzPbsI-DFFmc/edit#gid=499381388
var/high_population_requirement = 10
//an alternative, static requirement used instead when "high_population_override" is set to 1 in the config
//which it should be when even low pop rounds have over 30 players and high pop rounds have 90+.
var/datum/gamemode/dynamic/mode = null
var/role_category_override = null // If a role is to be considered another for the purpose of bannig.
/datum/dynamic_ruleset/New()
..()
if (config.protect_roles_from_antagonist)
restricted_from_jobs += protected_from_jobs
if (istype(ticker.mode, /datum/gamemode/dynamic))
mode = ticker.mode
else
message_admins("A dynamic ruleset was created but server isn't on Dynamic Mode!")
qdel(src)
/datum/dynamic_ruleset/roundstart//One or more of those drafted at roundstart
/datum/dynamic_ruleset/roundstart/delayed/ // Executed with a 30 seconds delay
var/delay = 30 SECONDS
var/required_type = /mob/living/carbon/human // No ghosts, new players or silicons allowed.
var/assigned_ckeys = list()
/datum/dynamic_ruleset/latejoin//Can be drafted when a player joins the server
/datum/dynamic_ruleset/proc/acceptable(var/population=0,var/threat_level=0)
//by default, a rule is acceptable if it satisfies the threat level/population requirements.
//If your rule has extra checks, such as counting security officers, do that in ready() instead
if (!map.map_ruleset(src))
message_admins("Dynamic Mode: Skipping [name] due to map blacklist.")
log_admin("Dynamic Mode: Skipping [name] due to map blacklist")
return 0
if (player_list.len >= mode.high_pop_limit)
return (threat_level >= high_population_requirement)
else
var/indice_pop = min(10,round(population/5)+1)
return (threat_level >= requirements[indice_pop])
// -- Choosing the candidate(s) for a rule which is otherwise guarenteed to be executed.
// This generic proc works for a solo candidate.
// returns: 0 or 1 depending on success. (failure meaning something runtimed mid-code.)
/datum/dynamic_ruleset/proc/choose_candidates()
var/mob/M = pick(candidates)
if (istype(M))
assigned += M
candidates -= M
return (assigned.len > 0)
/datum/dynamic_ruleset/proc/process()
//write here your rule execution code, everything about faction/role spawning/populating.
return
/datum/dynamic_ruleset/proc/execute()
//write here your rule execution code, everything about faction/role spawning/populating.
return 1
/datum/dynamic_ruleset/proc/ready(var/forced = 0) //Here you can perform any additional checks you want. (such as checking the map, the amount of certain jobs, etc)
if (admin_disable_rulesets && !forced)
message_admins("Dynamic Mode: [name] was prevented from firing by admins.")
log_admin("Dynamic Mode: [name] was prevented from firing by admins.")
return FALSE
if (required_candidates > candidates.len) //IMPORTANT: If ready() returns 1, that means execute() should never fail!
return FALSE
return TRUE
// Returns TRUE if there is enough pop to execute this ruleset
/datum/dynamic_ruleset/proc/check_enemy_jobs(var/dead_dont_count = FALSE, var/midround = FALSE)
var/enemies_count = 0
if (dead_dont_count)
for (var/mob/M in mode.living_players)
if (M.stat == DEAD)
continue//dead players cannot count as opponents
if (M.mind && M.mind.assigned_role && (M.mind.assigned_role in enemy_jobs) && (!(M in candidates) || (M.mind.assigned_role in restricted_from_jobs)))
enemies_count++//checking for "enemies" (such as sec officers). To be counters, they must either not be candidates to that rule, or have a job that restricts them from it
else
for (var/mob/M in mode.candidates)
if (M.mind && M.mind.assigned_role && (M.mind.assigned_role in enemy_jobs) && (!(M in candidates) || (M.mind.assigned_role in restricted_from_jobs)))
enemies_count++//checking for "enemies" (such as sec officers). To be counters, they must either not be candidates to that rule, or have a job that restricts them from it
var/pop_and_enemies
if (ticker && ticker.current_state == GAME_STATE_PLAYING)
pop_and_enemies += mode.living_players.len
else
pop_and_enemies += mode.roundstart_pop_ready
pop_and_enemies += enemies_count // Enemies count twice
var/threat = 0
if(midround)
threat = mode.midround_threat_level != 100 ? round(mode.midround_threat_level/10)+1 : 10
else
threat = mode.threat_level != 100 ? round(mode.threat_level/10)+1 : 10
if (enemies_count < required_enemies[threat] && !map.ignore_enemy_requirement(src))
message_admins("Dynamic Mode: There are not enough enemy jobs ready for [name]. ([enemies_count] out of [required_enemies[threat]])")
log_admin("Dynamic Mode: There are not enough enemy jobs ready for [name]. ([enemies_count] out of [required_enemies[threat]])")
return FALSE
if (pop_and_enemies >= required_pop[threat])
return TRUE
if (!dead_dont_count)//roundstart check only
message_admins("Dynamic Mode: Despite [name] having enough candidates, there are not enough enemy jobs and pop ready ([enemies_count] and [mode.roundstart_pop_ready] out of [required_pop[threat]])")
log_admin("Dynamic Mode: Despite [name] having enough candidates, there are not enough enemy jobs and pop ready ([enemies_count] and [mode.roundstart_pop_ready] out of [required_pop[threat]])")
return FALSE
/datum/dynamic_ruleset/proc/get_weight()
var/result = weight
result *= map.ruleset_multiplier(src)
result *= weight_time_day()
for(var/datum/dynamic_ruleset/DR in mode.executed_rules)
if(DR.role_category == src.role_category) // If the same type of antag is already in this round, reduce the odds
result *= 0.5
break
result = previous_rounds_odds_reduction(result)
if (mode.highlander_rulesets_favoured && (flags & HIGHLANDER_RULESET))
result *= ADDITIONAL_RULESET_WEIGHT
message_admins("[name] had [result] weight (-[initial(weight) - result]).")
return result
/datum/dynamic_ruleset/proc/previous_rounds_odds_reduction(var/result)
for (var/previous_round in mode.previously_executed_rules)
for(var/previous_ruleset in mode.previously_executed_rules[previous_round])
var/datum/dynamic_ruleset/DR = previous_ruleset
if(initial(DR.role_category) == src.role_category)
switch (previous_round)
if ("one_round_ago")
result *= 0.4
if ("two_rounds_ago")
result *= 0.7
if ("three_rounds_ago")
result *= 0.9
return result
//Return a multiplicative weight. 1 for nothing special.
/datum/dynamic_ruleset/proc/weight_time_day()
var/weigh = 1
if(time2text(world.timeofday, "DDD") in weekday_rule_boost)
weigh *= 2
for(var/i = 1 to requirements.len)
if ((i < requirements.len) && (requirements[i+1] == 90))//let's not actually reduce the requirement on low pop.
continue
requirements[i] = clamp(requirements[i] - 20,10,90)
for(var/i = 1 to required_pop.len)
required_pop[i] = clamp(required_pop[i] - 5,0,100)
if(getTimeslot() in timeslot_rule_boost)
weigh *= 2
return weigh
/datum/dynamic_ruleset/proc/trim_candidates()
return
/datum/dynamic_ruleset/proc/send_applications(var/list/possible_volunteers = list())
if (possible_volunteers.len <= 0)//this shouldn't happen, as ready() should return 0 if there is not a single valid candidate
message_admins("Possible volunteers was 0. This shouldn't appear, because of ready(), unless you forced it!")
return
message_admins("DYNAMIC MODE: Polling [possible_volunteers.len] players to apply for the [name] ruleset.")
log_admin("DYNAMIC MODE: Polling [possible_volunteers.len] players to apply for the [name] ruleset.")
searching = 1
var/role_id = initial(role_category.id)
var/icon/logo_icon = icon('icons/logos.dmi', logo)
for(var/mob/M in possible_volunteers)
var/banned_factor = (jobban_isbanned(M, role_id) || isantagbanned(M) || (role_category_override && jobban_isbanned(M, role_category_override)))
if(!M.client || banned_factor || M.client.is_afk())
continue
to_chat(M, "[logo ? "[bicon(logo_icon)]" : ""]<span class='recruit'>The mode is looking for volunteers to become [initial(role_category.id)]. (<a href='?src=\ref[src];signup=\ref[M]'>Apply now!</a>)</span>[logo ? "[bicon(logo_icon)]" : ""]")
window_flash(M.client)
spawn(1 MINUTES)
searching = 0
for(var/mob/M in possible_volunteers)
if(!M.client || jobban_isbanned(M, role_category) || M.client.is_afk())
continue
to_chat(M, "[logo ? "[bicon(logo_icon)]" : ""]<span class='recruit'>Applications for [initial(role_category.id)] are now closed.</span>[logo ? "[bicon(logo_icon)]" : ""]")
if(!applicants || applicants.len <= 0)
log_admin("DYNAMIC MODE: [name] received no applications.")
message_admins("DYNAMIC MODE: [name] received no applications.")
mode.refund_midround_threat(cost)
mode.threat_log += "[worldtime2text()]: Rule [name] refunded [cost] (no applications)"
mode.executed_rules -= src
return
log_admin("DYNAMIC MODE: [applicants.len] players volunteered for [name].")
message_admins("DYNAMIC MODE: [applicants.len] players volunteered for [name].")
review_applications()
/datum/dynamic_ruleset/proc/review_applications()
/datum/dynamic_ruleset/Topic(var/href, var/list/href_list)
if(href_list["signup"])
var/mob/M = usr
if(!M)
return
volunteer(M)
/datum/dynamic_ruleset/proc/volunteer(var/mob/M)
if (!searching)
return
if(jobban_isbanned(M, role_category) || isantagbanned(M))
to_chat(M, "<span class='danger'>Banned from [initial(role_category.id)].</span>")
to_chat(M, "<span class='warning'>Your application has been discarded due to past conduct..</span>")
return
if(M in applicants)
to_chat(M, "<span class='notice'>Removed from the [initial(role_category.id)] registration list.</span>")
applicants -= M
return
else
to_chat(M, "<span class='notice'>Added to the [initial(role_category.id)] registration list.</span>")
applicants |= M
return
/datum/dynamic_ruleset/proc/progressive_job_search()
for(var/job in job_priority)
for(var/mob/M in candidates)
if(M.mind.assigned_role == job)
assigned += M
candidates -= M
return M
var/mob/M = pick(candidates)
assigned += M
candidates -= M
return M
////////////////////////////////////////////////////////////////////////
/datum/forced_ruleset
var/name = ""
var/ruleType
var/calledBy
//////////////////////////////////////////////
// //
// ROUNDSTART RULESETS ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
//////////////////////////////////////////////Remember that roundstart objectives are automatically forged by /datum/gamemode/proc/PostSetup()
/datum/dynamic_ruleset/roundstart/trim_candidates()
//-----------debug info---------------------------
var/cand = candidates.len
var/a = 0
var/b = 0
var/b1 = 0
var/c = 0
var/c1 = 0
var/d = 0
var/e = 0
//------------------------------------------------
var/role_id = initial(role_category.id)
var/role_pref = initial(role_category.required_pref)
for(var/mob/new_player/P in candidates)
if (!P.client || !P.mind || !P.mind.assigned_role)//are they connected?
candidates.Remove(P)
a++
continue
if (!P.client.desires_role(role_pref))//are they willing?
candidates.Remove(P)
b++
continue
else if (jobban_isbanned(P, role_id) || isantagbanned(P) || (role_category_override && jobban_isbanned(P, role_category_override)))//are they not antag-banned?
candidates.Remove(P)
b1++//we only count banned ones if they actually wanted to play the role
continue
if ((restricted_from_jobs.len > 0) && (P.mind.assigned_role && (P.mind.assigned_role in restricted_from_jobs)) || (P.mind.role_alt_title && (P.mind.role_alt_title in restricted_from_jobs)))//does their job allow for it?
candidates.Remove(P)
d++
continue
if ((exclusive_to_jobs.len > 0) && P.mind.assigned_role && !(P.mind.assigned_role in exclusive_to_jobs))//is the rule exclusive to their job?
candidates.Remove(P)
e++
continue
if ((protected_from_jobs.len > 0) && (P.mind.assigned_role && (P.mind.assigned_role in protected_from_jobs)) || (P.mind.role_alt_title && (P.mind.role_alt_title in protected_from_jobs)))
var/probability = initial(role_category.protected_traitor_prob)
if (prob(probability))
candidates.Remove(P)
c1++
c++
continue
message_admins("DYNAMIC MODE: [name] has [candidates.len] valid candidates out of [cand] players ([a ? "[a] disconnected, ":""][b ? "[b] didn't want the role, ":""][b1 ? "[b1] wanted the role but are banned from it, ":""][c1 ? "[c1] out of [c] were protected from the role, " : ""][d ? "[d] were restricted from the role, " : ""][e ? "[e] didn't pick the job necessary for the role" : ""])")
log_admin("DYNAMIC MODE: [name] has [candidates.len] valid candidates out of [cand] players ([a ? "[a] disconnected, ":""][b ? "[b] didn't want the role, ":""][b1 ? "[b1] wanted the role but are banned from it, ":""][c1 ? "[c1] out of [c] were protected from the role, " : ""][d ? "[d] were restricted from the role, " : ""][e ? "[e] didn't pick the job necessary for the role" : ""])")
/datum/dynamic_ruleset/roundstart/delayed/trim_candidates()
if (ticker && ticker.current_state < GAME_STATE_PLAYING)
return ..() // If the game didn't start, we'll use the parent's method to see if we have enough people desiring the role & what not.
var/role_id = initial(role_category.id)
for (var/mob/P in candidates)
if (!istype(P, required_type))
candidates.Remove(P) // Can be a new_player, etc.
continue
if (!P.client || !P.mind || !P.mind.assigned_role || P.mind.antag_roles.len)//are they connected? Are they an antag already?
candidates.Remove(P)
continue
if (!P.client.desires_role(role_id) || jobban_isbanned(P, role_id) || isantagbanned(P) || (role_category_override && jobban_isbanned(P, role_category_override)))//are they willing and not antag-banned?
candidates.Remove(P)
continue
if ((P.mind.assigned_role && (P.mind.assigned_role in protected_from_jobs)) || (P.mind.role_alt_title && (P.mind.role_alt_title in protected_from_jobs)))
var/probability = initial(role_category.protected_traitor_prob)
if (prob(probability))
candidates.Remove(P)
continue
if ((P.mind.assigned_role && (P.mind.assigned_role in restricted_from_jobs)) || (P.mind.role_alt_title && (P.mind.role_alt_title in restricted_from_jobs)))//does their job allow for it?
candidates.Remove(P)
continue
if ((exclusive_to_jobs.len > 0) && !(P.mind.assigned_role in exclusive_to_jobs))//is the rule exclusive to their job?
candidates.Remove(P)
continue
/datum/dynamic_ruleset/roundstart/ready(var/forced = 0)
if (!forced)
if(!check_enemy_jobs(FALSE))
return 0
return ..()
/datum/dynamic_ruleset/proc/latejoinprompt(var/mob/user, var/ruleset)
if(alert(user,"The gamemode is trying to select you for [ruleset], do you want this?",,"Yes","No") == "Yes")
return 1
return 0
/datum/dynamic_ruleset/proc/generate_ruleset_body(mob/applicant)
var/mob/living/carbon/human/new_character = makeBody(applicant)
new_character.dna.ResetSE()
return new_character