mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-30 00:29:02 +01:00
## About The Pull Request Title. So, the proc takes instances, but we were getting typepaths from the global list. This fixes it, by getting the instances. Also makes rerolled antags always pick from the original reroll. ## Why It's Good For The Game Bugs bag ## Proof Of Testing <details> <summary>Screenshots/Videos</summary> <img width="1736" height="424" alt="image" src="https://github.com/user-attachments/assets/6ad83ee3-f453-4f43-bb32-10b2555b2654" /> </details> ## Changelog 🆑 fix: Antagonist reroll now works /🆑
896 lines
35 KiB
Plaintext
896 lines
35 KiB
Plaintext
#define INIT_ORDER_GAMEMODE 70
|
|
|
|
SUBSYSTEM_DEF(gamemode)
|
|
name = "Storyteller"
|
|
init_order = INIT_ORDER_GAMEMODE
|
|
runlevels = RUNLEVEL_GAME
|
|
ss_flags = SS_BACKGROUND | SS_KEEP_TIMING
|
|
wait = 2 SECONDS
|
|
|
|
/// List of our event tracks for fast access during for loops.
|
|
var/list/event_tracks = EVENT_TRACKS
|
|
/// Our storyteller. He progresses our trackboards and picks out events
|
|
var/datum/storyteller/storyteller
|
|
/// Result of the storyteller vote. Defaults to the guide.
|
|
var/voted_storyteller = /datum/storyteller/default
|
|
/// List of all the storytellers. Populated at init. Associative from type
|
|
var/list/storytellers = list()
|
|
/// Next process for our storyteller. The wait time is STORYTELLER_WAIT_TIME
|
|
var/next_storyteller_process = 0
|
|
/// Associative list of even track points.
|
|
var/list/event_track_points = list(
|
|
EVENT_TRACK_MUNDANE = 0,
|
|
EVENT_TRACK_MODERATE = 0,
|
|
EVENT_TRACK_MAJOR = 0,
|
|
EVENT_TRACK_CREWSET = 0,
|
|
EVENT_TRACK_GHOSTSET = 0
|
|
)
|
|
/// Last point amount gained of each track. Those are recorded for purposes of estimating how long until next event.
|
|
var/list/last_point_gains = list(
|
|
EVENT_TRACK_MUNDANE = 0,
|
|
EVENT_TRACK_MODERATE = 0,
|
|
EVENT_TRACK_MAJOR = 0,
|
|
EVENT_TRACK_CREWSET = 0,
|
|
EVENT_TRACK_GHOSTSET = 0
|
|
)
|
|
/// Point thresholds at which the events are supposed to be rolled, it is also the base cost for events.
|
|
var/list/point_thresholds = list(
|
|
EVENT_TRACK_MUNDANE = 100,
|
|
EVENT_TRACK_MODERATE = 100,
|
|
EVENT_TRACK_MAJOR = 100,
|
|
EVENT_TRACK_CREWSET = 100,
|
|
EVENT_TRACK_GHOSTSET = 100
|
|
)
|
|
|
|
/// Minimum population thresholds for the tracks to fire off events.
|
|
var/list/min_pop_thresholds = list(
|
|
EVENT_TRACK_MUNDANE = MUNDANE_MIN_POP,
|
|
EVENT_TRACK_MODERATE = MODERATE_MIN_POP,
|
|
EVENT_TRACK_MAJOR = MAJOR_MIN_POP,
|
|
EVENT_TRACK_CREWSET = CREWSET_MIN_POP,
|
|
EVENT_TRACK_GHOSTSET = GHOSTSET_MIN_POP
|
|
)
|
|
|
|
/// Configurable multipliers for point gain over time.
|
|
var/list/point_gain_multipliers = list(
|
|
EVENT_TRACK_MUNDANE = 1,
|
|
EVENT_TRACK_MODERATE = 1,
|
|
EVENT_TRACK_MAJOR = 1,
|
|
EVENT_TRACK_CREWSET = 1,
|
|
EVENT_TRACK_GHOSTSET = 1
|
|
)
|
|
/// Configurable multipliers for roundstart points.
|
|
var/list/roundstart_point_multipliers = list(
|
|
EVENT_TRACK_MUNDANE = 1,
|
|
EVENT_TRACK_MODERATE = 1,
|
|
EVENT_TRACK_MAJOR = 1,
|
|
EVENT_TRACK_CREWSET = 1,
|
|
EVENT_TRACK_GHOSTSET = 1
|
|
)
|
|
/// Whether we allow pop scaling. This is configured by config, or the storyteller UI
|
|
var/allow_pop_scaling = TRUE
|
|
|
|
/// Associative list of pop scale thresholds.
|
|
var/list/pop_scale_thresholds = list(
|
|
EVENT_TRACK_MUNDANE = MUNDANE_POP_SCALE_THRESHOLD,
|
|
EVENT_TRACK_MODERATE = MODERATE_POP_SCALE_THRESHOLD,
|
|
EVENT_TRACK_MAJOR = MAJOR_POP_SCALE_THRESHOLD,
|
|
EVENT_TRACK_CREWSET = CREWSET_POP_SCALE_THRESHOLD,
|
|
EVENT_TRACK_GHOSTSET = GHOSTSET_POP_SCALE_THRESHOLD
|
|
)
|
|
|
|
/// Associative list of pop scale penalties.
|
|
var/list/pop_scale_penalties = list(
|
|
EVENT_TRACK_MUNDANE = MUNDANE_POP_SCALE_PENALTY,
|
|
EVENT_TRACK_MODERATE = MODERATE_POP_SCALE_PENALTY,
|
|
EVENT_TRACK_MAJOR = MAJOR_POP_SCALE_PENALTY,
|
|
EVENT_TRACK_CREWSET = CREWSET_POP_SCALE_PENALTY,
|
|
EVENT_TRACK_GHOSTSET = GHOSTSET_POP_SCALE_PENALTY
|
|
)
|
|
|
|
|
|
|
|
/// Associative list of control events by their track category. Compiled in Init
|
|
var/list/event_pools = list()
|
|
|
|
/// Events that we have scheduled to run in the nearby future
|
|
var/list/scheduled_events = list()
|
|
|
|
/// Associative list of tracks to forced event controls. For admins to force events (though they can still invoke them freely outside of the track system)
|
|
var/list/forced_next_events = list()
|
|
|
|
var/list/control = list() //list of all datum/round_event_control. Used for selecting events based on weight and occurrences.
|
|
var/list/running = list() //list of all existing /datum/round_event
|
|
var/list/currentrun = list()
|
|
|
|
/// List of all uncategorized events, because they were wizard or holiday events
|
|
var/list/uncategorized = list()
|
|
|
|
var/list/holidays //List of all holidays occuring today or null if no holidays
|
|
|
|
/// Event frequency multiplier, it exists because wizard, eugh.
|
|
var/event_frequency_multiplier = 1
|
|
|
|
/// Current preview page for the statistics UI.
|
|
var/statistics_track_page = EVENT_TRACK_MUNDANE
|
|
/// Page of the UI panel.
|
|
var/panel_page = GAMEMODE_PANEL_MAIN
|
|
/// Whether we are viewing the roundstart events or not
|
|
var/roundstart_event_view = TRUE
|
|
|
|
/// Whether the storyteller has been halted
|
|
var/storyteller_halted = FALSE
|
|
|
|
/// Ready players for roundstart events.
|
|
var/ready_players = 0
|
|
var/active_players = 0
|
|
var/active_crew = 0
|
|
var/head_crew = 0
|
|
var/eng_crew = 0
|
|
var/sec_crew = 0
|
|
var/med_crew = 0
|
|
|
|
//Security Based Antag Cap
|
|
var/sec_antag_cap = 0
|
|
/// A list of event controls to re-roll antagonists
|
|
var/list/antag_rerolls
|
|
/// A assoc list of event controls by pref flag, used for accurately rerolling antags. (flag -> /datum/round_control_event)
|
|
var/list/antag_rerolls_by_pref
|
|
|
|
/// Whether we looked up pop info in this process tick
|
|
var/pop_data_cached = FALSE
|
|
|
|
var/wizardmode = FALSE
|
|
|
|
var/storyteller_voted = FALSE
|
|
var/datum/vote/storyteller/vote_datum
|
|
|
|
/datum/controller/subsystem/gamemode/Initialize(time, zlevel)
|
|
. = ..()
|
|
// Populate event pools
|
|
for(var/track in event_tracks)
|
|
event_pools[track] = list()
|
|
|
|
// Populate storytellers
|
|
for(var/type in subtypesof(/datum/storyteller))
|
|
storytellers[type] = new type()
|
|
|
|
for(var/type in typesof(/datum/round_event_control))
|
|
var/datum/round_event_control/event = new type()
|
|
if(!event.typepath || !event.name || !event.valid_for_map())
|
|
continue //don't want this one! leave it for the garbage collector
|
|
control += event //add it to the list of all events (controls)
|
|
getHoliday()
|
|
|
|
load_config_vars()
|
|
load_event_config_vars()
|
|
|
|
///Seeding events into track event pools needs to happen after event config vars are loaded
|
|
for(var/datum/round_event_control/event as anything in control)
|
|
if(event.wizardevent)
|
|
uncategorized += event
|
|
continue
|
|
|
|
if(event.holidayID)
|
|
var/holiday_categorized = FALSE
|
|
for(var/datum/holiday/current_holiday as anything in GLOB.holidays)
|
|
if(current_holiday == event.holidayID)
|
|
log_game("Storyteller enabled event [event] due to active holiday [current_holiday]")
|
|
message_admins("Storyteller enabled event [event] due to active holiday [current_holiday].")
|
|
event_pools[event.track] += event
|
|
holiday_categorized = TRUE
|
|
break
|
|
if(!holiday_categorized)
|
|
uncategorized += event
|
|
continue
|
|
|
|
var/list/event_tags = event.tags
|
|
if(LAZYLEN(event_tags))
|
|
if(LAZYFIND(event_tags, TAG_ANTAG_REROLL))
|
|
LAZYADDASSOC(antag_rerolls, event, event.weight)
|
|
if (istype(event, /datum/round_event_control/antagonist))
|
|
var/datum/round_event_control/antagonist/antag_event = event
|
|
LAZYADDASSOC(antag_rerolls_by_pref, antag_event.antag_flag, antag_event)
|
|
continue
|
|
|
|
event_pools[event.track] += event //Add it to the categorized event pools
|
|
|
|
return SS_INIT_SUCCESS
|
|
|
|
|
|
/datum/controller/subsystem/gamemode/fire(resumed = FALSE)
|
|
if(!resumed)
|
|
src.currentrun = running.Copy()
|
|
|
|
if(EMERGENCY_AT_LEAST_DOCKED)
|
|
//Don't run any events if the shuttle is docked with the station (or in transit towards central command.
|
|
return
|
|
if((SSshuttle.emergency_no_recall && !SSshuttle.admin_emergency_no_recall) && EMERGENCY_IDLE_OR_RECALLED)
|
|
//Don't run any events if the shuttle is in transit in a non-admin no-recall state.
|
|
return
|
|
|
|
if(next_storyteller_process <= world.time && storyteller)
|
|
|
|
next_storyteller_process = world.time + STORYTELLER_WAIT_TIME
|
|
|
|
// We update crew information here to adjust population scalling and event thresholds for the storyteller.
|
|
update_crew_infos()
|
|
|
|
//Process storyteller
|
|
if(!storyteller_halted)
|
|
storyteller.process(STORYTELLER_WAIT_TIME * 0.1)
|
|
|
|
///Handle scheduled events
|
|
for(var/datum/scheduled_event/sch_event in scheduled_events)
|
|
if(world.time >= sch_event.start_time)
|
|
sch_event.try_fire()
|
|
else if(!sch_event.alerted_admins && world.time >= sch_event.start_time - 1 MINUTES)
|
|
///Alert admins 1 minute before running and allow them to cancel or refund the event, once again.
|
|
sch_event.alerted_admins = TRUE
|
|
message_admins("Scheduled Event: [sch_event.event] will run in [(sch_event.start_time - world.time) / 10] seconds. (<a href='byond://?src=[REF(sch_event)];action=cancel'>CANCEL</a>) (<a href='byond://?src=[REF(sch_event)];action=refund'>REFUND</a>)")
|
|
|
|
// Reset the cache value to false
|
|
pop_data_cached = FALSE
|
|
|
|
//cache for sanic speed (lists are references anyways)
|
|
var/list/currentrun = src.currentrun
|
|
|
|
while(currentrun.len)
|
|
var/datum/thing = currentrun[currentrun.len]
|
|
currentrun.len--
|
|
if(thing)
|
|
thing.process(wait * 0.1)
|
|
else
|
|
running.Remove(thing)
|
|
if (MC_TICK_CHECK)
|
|
return
|
|
|
|
/datum/controller/subsystem/gamemode/proc/storyteller_desc(storyteller_name)
|
|
for(var/storyteller_type in storytellers)
|
|
var/datum/storyteller/storyboy = storytellers[storyteller_type]
|
|
if(storyboy.name != storyteller_name)
|
|
continue
|
|
return storyboy.desc
|
|
|
|
/// Gets the number of antagonists the antagonist injection events will stop rolling after.
|
|
/datum/controller/subsystem/gamemode/proc/get_antag_cap()
|
|
if(isnull(storyteller))
|
|
return 0
|
|
if(!storyteller.antag_divisor)
|
|
return 0
|
|
return round(max(min(get_correct_popcount() / storyteller.antag_divisor + sec_antag_cap ,sec_antag_cap * 1.5),ANTAG_CAP_FLAT))
|
|
|
|
/// Whether events can inject more antagonists into the round
|
|
/datum/controller/subsystem/gamemode/proc/can_inject_antags()
|
|
return (get_antag_cap() > length(GLOB.current_living_antags))
|
|
|
|
/// Gets candidates for antagonist roles.
|
|
|
|
/// Todo: Split into get_candidates and post_get_candidates
|
|
/datum/controller/subsystem/gamemode/proc/get_candidates(
|
|
special_role_flag,
|
|
pick_observers,
|
|
pick_roundstart_players,
|
|
no_antags = TRUE,
|
|
list/restricted_roles,
|
|
list/restricted_species,
|
|
)
|
|
|
|
|
|
var/list/candidates = list()
|
|
var/list/candidate_candidates = list() //lol
|
|
if(pick_roundstart_players)
|
|
for(var/mob/dead/new_player/player in GLOB.new_player_list)
|
|
if(player.ready == PLAYER_READY_TO_PLAY && player.mind && player.check_job_preferences())
|
|
candidate_candidates += player
|
|
else if(pick_observers)
|
|
for(var/mob/player as anything in GLOB.dead_mob_list)
|
|
candidate_candidates += player
|
|
else
|
|
for(var/datum/record/locked/manifest_log as anything in GLOB.manifest.locked)
|
|
var/datum/mind/player_mind = manifest_log.mind_ref.resolve()
|
|
var/mob/living/player = player_mind.current
|
|
if(isnull(player))
|
|
continue
|
|
candidate_candidates += player
|
|
|
|
|
|
for(var/mob/candidate as anything in candidate_candidates)
|
|
if(QDELETED(candidate) || !candidate.key || !candidate.client || !candidate.mind)
|
|
continue
|
|
if(no_antags && LAZYLEN(candidate.mind.special_roles))
|
|
continue
|
|
if(restricted_roles && (candidate.mind.assigned_role.title in restricted_roles))
|
|
continue
|
|
var/mob/living/carbon/carbon = candidate
|
|
if(istype(carbon)) // species restrictions
|
|
if(restricted_species && (carbon.dna.species.id in restricted_species))
|
|
continue
|
|
if(special_role_flag)
|
|
if(!(candidate.client.prefs) || !(special_role_flag in candidate.client.prefs.be_special))
|
|
continue
|
|
if(candidate.client.get_days_to_play_antag(special_role_flag) > 0)
|
|
continue
|
|
if(is_banned_from(candidate.ckey, list(special_role_flag, ROLE_SYNDICATE)))
|
|
continue
|
|
|
|
if(is_banned_from(candidate.client.ckey, BAN_ANTAGONIST))
|
|
continue
|
|
if(!candidate.client?.prefs?.read_preference(/datum/preference/toggle/be_antag))
|
|
continue
|
|
candidates += candidate
|
|
return candidates
|
|
|
|
/// Gets the correct popcount, returning READY people if roundstart, and active people if not.
|
|
/datum/controller/subsystem/gamemode/proc/get_correct_popcount()
|
|
if(SSticker.HasRoundStarted())
|
|
if(!pop_data_cached)
|
|
update_crew_infos()
|
|
return active_players
|
|
else
|
|
if(!pop_data_cached)
|
|
calculate_ready_players()
|
|
return ready_players
|
|
|
|
/// Refunds and removes a scheduled event.
|
|
/datum/controller/subsystem/gamemode/proc/refund_scheduled_event(datum/scheduled_event/refunded)
|
|
if(refunded.cost)
|
|
var/track_type = refunded.event.track
|
|
event_track_points[track_type] += refunded.cost
|
|
remove_scheduled_event(refunded)
|
|
|
|
/// Schedules an event.
|
|
/datum/controller/subsystem/gamemode/proc/force_event(datum/round_event_control/event)
|
|
forced_next_events[event.track] = event
|
|
|
|
/// Removes a scheduled event.
|
|
/datum/controller/subsystem/gamemode/proc/remove_scheduled_event(datum/scheduled_event/removed)
|
|
scheduled_events -= removed
|
|
qdel(removed)
|
|
|
|
/// We need to calculate ready players for the sake of roundstart events becoming eligible.
|
|
/datum/controller/subsystem/gamemode/proc/calculate_ready_players()
|
|
ready_players = 0
|
|
for(var/mob/dead/new_player/player as anything in GLOB.new_player_list)
|
|
if(player.ready == PLAYER_READY_TO_PLAY)
|
|
ready_players++
|
|
pop_data_cached = TRUE
|
|
|
|
/// We roll points to be spent for roundstart events, including antagonists.
|
|
/datum/controller/subsystem/gamemode/proc/roll_pre_setup_points()
|
|
if(storyteller.disable_distribution || storyteller_halted)
|
|
return
|
|
/// Distribute points
|
|
for(var/track in event_track_points)
|
|
var/base_amt
|
|
switch(track)
|
|
if(EVENT_TRACK_MUNDANE)
|
|
base_amt = ROUNDSTART_MUNDANE_BASE
|
|
if(EVENT_TRACK_MODERATE)
|
|
base_amt = ROUNDSTART_MODERATE_BASE
|
|
if(EVENT_TRACK_MAJOR)
|
|
base_amt = ROUNDSTART_MAJOR_BASE
|
|
if(EVENT_TRACK_CREWSET)
|
|
base_amt = ROUNDSTART_CREWSET_BASE
|
|
if(EVENT_TRACK_GHOSTSET)
|
|
base_amt = ROUNDSTART_GHOSTSET_BASE
|
|
var/calc_value = base_amt
|
|
calc_value *= roundstart_point_multipliers[track]
|
|
calc_value *= storyteller.starting_point_multipliers[track]
|
|
calc_value *= (1 + (rand(-storyteller.roundstart_points_variance, storyteller.roundstart_points_variance) / 100))
|
|
event_track_points[track] = max(0, round(calc_value))
|
|
|
|
/// If the storyteller guarantees an antagonist roll, add points to make it so.
|
|
if(storyteller.guarantees_roundstart_crewset)
|
|
event_track_points[EVENT_TRACK_CREWSET] = point_thresholds[EVENT_TRACK_CREWSET]
|
|
|
|
/// If we have any forced events, ensure we get enough points for them
|
|
for(var/track in event_tracks)
|
|
if(forced_next_events[track] && event_track_points[track] < point_thresholds[track])
|
|
event_track_points[track] = point_thresholds[track]
|
|
|
|
/// Because roundstart events need 2 steps of firing for purposes of antags, here is the first step handled, happening before occupation division.
|
|
/datum/controller/subsystem/gamemode/proc/handle_pre_setup_roundstart_events()
|
|
if(storyteller.disable_distribution)
|
|
return
|
|
if(storyteller_halted)
|
|
message_admins("WARNING: Didn't roll roundstart events (including antagonists) due to the storyteller being halted.")
|
|
return
|
|
log_dynamic("Initializing storyteller [storyteller.name] with the following multipliers! [english_list_assoc(storyteller.tag_multipliers)]")
|
|
while(TRUE)
|
|
if(!storyteller.handle_tracks())
|
|
break
|
|
|
|
/// Second step of handlind roundstart events, happening after people spawn.
|
|
/datum/controller/subsystem/gamemode/proc/handle_post_setup_roundstart_events()
|
|
/// Start all roundstart events on post_setup immediately
|
|
for(var/datum/round_event/event as anything in running)
|
|
if(!event.control.roundstart)
|
|
continue
|
|
ASYNC
|
|
event.try_start()
|
|
// INVOKE_ASYNC(event, TYPE_PROC_REF(/datum/round_event, try_start))
|
|
|
|
/// Schedules an event to run later.
|
|
/datum/controller/subsystem/gamemode/proc/schedule_event(datum/round_event_control/passed_event, passed_time, passed_cost, passed_ignore, passed_announce)
|
|
var/datum/scheduled_event/scheduled = new (passed_event, world.time + passed_time, passed_cost, passed_ignore, passed_announce)
|
|
var/round_started = SSticker.HasRoundStarted()
|
|
if(round_started)
|
|
message_admins("Event: [passed_event] has been scheduled to run in [passed_time / 10] seconds. (<a href='byond://?src=[REF(scheduled)];action=cancel'>CANCEL</a>) (<a href='byond://?src=[REF(scheduled)];action=refund'>REFUND</a>)")
|
|
else //Only roundstart events can be scheduled before round start
|
|
message_admins("Event: [passed_event] has been scheduled to run on roundstart. (<a href='byond://?src=[REF(scheduled)];action=cancel'>CANCEL</a>)")
|
|
scheduled_events += scheduled
|
|
|
|
/datum/controller/subsystem/gamemode/proc/update_crew_infos()
|
|
|
|
// Very similar logic to `get_active_player_count()`
|
|
|
|
active_crew = 0
|
|
active_players = 0
|
|
head_crew = 0
|
|
eng_crew = 0
|
|
med_crew = 0
|
|
sec_crew = 0
|
|
sec_antag_cap = 0
|
|
|
|
for(var/mob/player_mob as anything in GLOB.alive_player_list)
|
|
|
|
if(!player_mob || !player_mob.mind || !player_mob.client)
|
|
continue
|
|
|
|
if(player_mob.client.is_afk()) //If afk. Don't include.
|
|
continue
|
|
|
|
active_players++
|
|
|
|
if(!player_mob.mind.assigned_role)
|
|
continue
|
|
|
|
var/datum/job/player_role = player_mob.mind.assigned_role
|
|
|
|
//Check if they're actually a crew job (and not something like an off-station ghost role).
|
|
if(!(player_role.job_flags & JOB_CREW_MEMBER))
|
|
continue
|
|
|
|
//Check if they're actually on station and not """engaged""" in """roleplay""".like a good crew member.
|
|
//This basically checks if they're on a station z-level and they're not in dorms.
|
|
if(engaged_role_play_check(player_mob, station = TRUE, dorms = TRUE))
|
|
continue
|
|
|
|
active_crew++
|
|
|
|
//Now check each crewmember's job flags.
|
|
if(player_role.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND)
|
|
head_crew++
|
|
if(player_role.departments_bitflags & DEPARTMENT_BITFLAG_ENGINEERING)
|
|
eng_crew++
|
|
if(player_role.departments_bitflags & DEPARTMENT_BITFLAG_MEDICAL)
|
|
med_crew++
|
|
if(player_role.departments_bitflags & DEPARTMENT_BITFLAG_SECURITY)
|
|
sec_crew++
|
|
sec_antag_cap += player_role.sec_antag_cap
|
|
|
|
pop_data_cached = TRUE
|
|
|
|
/datum/controller/subsystem/gamemode/proc/TriggerEvent(datum/round_event_control/event)
|
|
. = event.preRunEvent()
|
|
if(. == EVENT_CANT_RUN)//we couldn't run this event for some reason, set its max_occurrences to 0
|
|
event.max_occurrences = 0
|
|
else if(. == EVENT_READY)
|
|
event.run_event(random = TRUE) // fallback to dynamic
|
|
|
|
///Resets frequency multiplier.
|
|
/datum/controller/subsystem/gamemode/proc/resetFrequency()
|
|
event_frequency_multiplier = 1
|
|
|
|
/* /client/proc/forceEvent()
|
|
set name = "Trigger Event"
|
|
set category = "Admin.Events"
|
|
|
|
if(!holder ||!check_rights(R_FUN))
|
|
return
|
|
|
|
holder.forceEvent(usr) */
|
|
|
|
/* /datum/admins/forceEvent(mob/user)
|
|
SSgamemode.event_panel(user) */
|
|
|
|
|
|
//////////////
|
|
// HOLIDAYS //
|
|
//////////////
|
|
//Uncommenting ALLOW_HOLIDAYS in config.txt will enable holidays
|
|
|
|
//It's easy to add stuff. Just add a holiday datum in code/modules/holiday/holidays.dm
|
|
//You can then check if it's a special day in any code in the game by doing if(SSgamemode.holidays["Groundhog Day"])
|
|
|
|
//You can also make holiday random events easily thanks to Pete/Gia's system.
|
|
//simply make a random event normally, then assign it a holidayID string which matches the holiday's name.
|
|
//Anything with a holidayID, which isn't in the holidays list, will never occur.
|
|
|
|
//Please, Don't spam stuff up with stupid stuff (key example being april-fools Pooh/ERP/etc),
|
|
//And don't forget: CHECK YOUR CODE!!!! We don't want any zero-day bugs which happen only on holidays and never get found/fixed!
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//ALSO, MOST IMPORTANTLY: Don't add stupid stuff! Discuss bonus content with Project-Heads first please!//
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
//sets up the holidays and holidays list
|
|
/datum/controller/subsystem/gamemode/proc/getHoliday()
|
|
if(!CONFIG_GET(flag/allow_holidays))
|
|
return // Holiday stuff was not enabled in the config!
|
|
for(var/H in subtypesof(/datum/holiday))
|
|
var/datum/holiday/holiday = new H()
|
|
var/delete_holiday = TRUE
|
|
for(var/timezone in holiday.timezones)
|
|
var/time_in_timezone = world.realtime + timezone HOURS
|
|
|
|
var/YYYY = text2num(time2text(time_in_timezone, "YYYY")) // get the current year
|
|
var/MM = text2num(time2text(time_in_timezone, "MM")) // get the current month
|
|
var/DD = text2num(time2text(time_in_timezone, "DD")) // get the current day
|
|
var/DDD = time2text(time_in_timezone, "DDD") // get the current weekday
|
|
|
|
if(holiday.shouldCelebrate(DD, MM, YYYY, DDD))
|
|
holiday.celebrate()
|
|
LAZYSET(holidays, holiday.name, holiday)
|
|
delete_holiday = FALSE
|
|
break
|
|
if(delete_holiday)
|
|
qdel(holiday)
|
|
|
|
if(holidays)
|
|
holidays = shuffle(holidays)
|
|
// regenerate station name because holiday prefixes.
|
|
set_station_name(new_station_name())
|
|
world.update_status()
|
|
|
|
/datum/controller/subsystem/gamemode/proc/toggleWizardmode()
|
|
wizardmode = !wizardmode //TODO: decide what to do with wiz events
|
|
message_admins("Summon Events has been [wizardmode ? "enabled, events will occur [SSgamemode.event_frequency_multiplier] times as fast" : "disabled"]!")
|
|
log_game("Summon Events was [wizardmode ? "enabled" : "disabled"]!")
|
|
|
|
///Attempts to select players for special roles the mode might have.
|
|
/datum/controller/subsystem/gamemode/proc/pre_setup()
|
|
// We need to do this to prevent some niche fuckery... and make dep. orders work. Lol
|
|
SSjob.reset_occupations()
|
|
calculate_ready_players()
|
|
roll_pre_setup_points()
|
|
handle_pre_setup_roundstart_events()
|
|
return TRUE
|
|
|
|
///Everyone should now be on the station and have their normal gear. This is the place to give the special roles extra things
|
|
/datum/controller/subsystem/gamemode/proc/post_setup(report) //Gamemodes can override the intercept report. Passing TRUE as the argument will force a report.
|
|
if(!report)
|
|
report = !CONFIG_GET(flag/no_intercept_report)
|
|
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(display_roundstart_logout_report)), 15 MINUTES)
|
|
|
|
if(SSdbcore.Connect())
|
|
var/list/to_set = list()
|
|
var/arguments = list()
|
|
if(storyteller)
|
|
to_set += "game_mode = :game_mode"
|
|
arguments["game_mode"] = storyteller.name
|
|
if(GLOB.revdata.originmastercommit)
|
|
to_set += "commit_hash = :commit_hash"
|
|
arguments["commit_hash"] = GLOB.revdata.originmastercommit
|
|
if(to_set.len)
|
|
arguments["round_id"] = GLOB.round_id
|
|
var/datum/db_query/query_round_game_mode = SSdbcore.NewQuery(
|
|
"UPDATE [format_table_name("round")] SET [to_set.Join(", ")] WHERE id = :round_id",
|
|
arguments
|
|
)
|
|
query_round_game_mode.Execute()
|
|
qdel(query_round_game_mode)
|
|
addtimer(CALLBACK(src, PROC_REF(send_trait_report)), rand(1 MINUTES, 5 MINUTES))
|
|
handle_post_setup_roundstart_events()
|
|
roundstart_event_view = FALSE
|
|
pop_data_cached = FALSE // Uncache it because we'd still wrongly consider it cached from lobby pops
|
|
return TRUE
|
|
|
|
|
|
///Handles late-join antag assignments
|
|
/datum/controller/subsystem/gamemode/proc/make_antag_chance(mob/living/carbon/human/character)
|
|
return
|
|
|
|
/datum/controller/subsystem/gamemode/proc/check_finished(force_ending) //to be called by SSticker
|
|
if(!SSticker.setup_done)
|
|
return FALSE
|
|
if(SSshuttle.emergency && (SSshuttle.emergency.mode == SHUTTLE_ENDGAME))
|
|
return TRUE
|
|
if(GLOB.station_was_nuked)
|
|
return TRUE
|
|
if(force_ending)
|
|
return TRUE
|
|
|
|
//////////////////////////
|
|
//Reports player logouts//
|
|
//////////////////////////
|
|
/proc/display_roundstart_logout_report()
|
|
var/list/msg = list("[span_boldnotice("Roundstart logout report")]\n\n")
|
|
for(var/i in GLOB.mob_living_list)
|
|
var/mob/living/L = i
|
|
var/mob/living/carbon/C = L
|
|
var/mob/living/carbon/human/H = C
|
|
if (istype(C) && !C.last_mind)
|
|
continue // never had a client
|
|
|
|
if(L.ckey && !GLOB.directory[L.ckey])
|
|
msg += "<b>[L.name]</b> ([L.key]), the [L.job] (<font color='#ffcc00'><b>Disconnected</b></font>)\n"
|
|
|
|
|
|
if(L.ckey && L.client)
|
|
var/failed = FALSE
|
|
if(L.client.inactivity >= (15 MINUTES)) //Connected, but inactive (alt+tabbed or something)
|
|
msg += "<b>[L.name]</b> ([L.key]), the [L.job] (<font color='#ffcc00'><b>Connected, Inactive</b></font>)\n"
|
|
failed = TRUE //AFK client
|
|
if(!failed && L.stat)
|
|
if(H.get_dnr()) //Suicider
|
|
msg += "<b>[L.name]</b> ([L.key]), the [L.job] ([span_boldannounce("Suicide")])\n"
|
|
failed = TRUE //Disconnected client
|
|
if(!failed && (L.stat == UNCONSCIOUS || L.stat == HARD_CRIT))
|
|
msg += "<b>[L.name]</b> ([L.key]), the [L.job] (Dying)\n"
|
|
failed = TRUE //Unconscious
|
|
if(!failed && L.stat == DEAD)
|
|
msg += "<b>[L.name]</b> ([L.key]), the [L.job] (Dead)\n"
|
|
failed = TRUE //Dead
|
|
|
|
continue //Happy connected client
|
|
for(var/mob/dead/observer/D in GLOB.dead_mob_list)
|
|
if(D.mind && D.mind.current == L)
|
|
if(L.stat == DEAD)
|
|
if(H.get_dnr()) //Suicider
|
|
msg += "<b>[L.name]</b> ([ckey(D.mind.key)]), the [L.job] ([span_boldannounce("Suicide")])\n"
|
|
continue //Disconnected client
|
|
else
|
|
msg += "<b>[L.name]</b> ([ckey(D.mind.key)]), the [L.job] (Dead)\n"
|
|
continue //Dead mob, ghost abandoned
|
|
else
|
|
if(D.can_reenter_corpse)
|
|
continue //Adminghost, or cult/wizard ghost
|
|
else
|
|
msg += "<b>[L.name]</b> ([ckey(D.mind.key)]), the [L.job] ([span_boldannounce("Ghosted")])\n"
|
|
continue //Ghosted while alive
|
|
|
|
|
|
for (var/C in GLOB.admins)
|
|
to_chat(C, msg.Join())
|
|
|
|
//Set result and news report here
|
|
/datum/controller/subsystem/gamemode/proc/set_round_result()
|
|
SSticker.mode_result = "undefined"
|
|
if(GLOB.station_was_nuked)
|
|
SSticker.news_report = STATION_DESTROYED_NUKE
|
|
if(EMERGENCY_ESCAPED_OR_ENDGAMED)
|
|
SSticker.news_report = STATION_EVACUATED
|
|
if(SSshuttle.emergency.is_hijacked())
|
|
SSticker.news_report = SHUTTLE_HIJACK
|
|
|
|
/// Loads json event config values from events.txt
|
|
/datum/controller/subsystem/gamemode/proc/load_event_config_vars()
|
|
var/json_file = file("[global.config.directory]/events.json")
|
|
if(!fexists(json_file))
|
|
return
|
|
var/list/decoded = json_decode(file2text(json_file))
|
|
for(var/event_text_path in decoded)
|
|
var/event_path = text2path(event_text_path)
|
|
var/datum/round_event_control/event
|
|
for(var/datum/round_event_control/iterated_event as anything in control)
|
|
if(iterated_event.type == event_path)
|
|
event = iterated_event
|
|
break
|
|
if(!event)
|
|
continue
|
|
var/list/var_list = decoded[event_text_path]
|
|
for(var/variable in var_list)
|
|
var/value = var_list[variable]
|
|
switch(variable)
|
|
if("weight")
|
|
event.weight = value
|
|
if("min_players")
|
|
event.min_players = value
|
|
if("max_occurrences")
|
|
event.max_occurrences = value
|
|
if("earliest_start")
|
|
event.earliest_start = value * (1 MINUTES)
|
|
if("track")
|
|
if(value in event_tracks)
|
|
event.track = value
|
|
if("cost")
|
|
event.cost = value
|
|
if("reoccurence_penalty_multiplier")
|
|
event.reoccurence_penalty_multiplier = value
|
|
if("shared_occurence_type")
|
|
if(!isnull(value))
|
|
value = text2path(value)
|
|
event.shared_occurence_type = value
|
|
|
|
/// Loads config values from game_options.txt
|
|
/datum/controller/subsystem/gamemode/proc/load_config_vars()
|
|
point_gain_multipliers[EVENT_TRACK_MUNDANE] = CONFIG_GET(number/mundane_point_gain_multiplier)
|
|
point_gain_multipliers[EVENT_TRACK_MODERATE] = CONFIG_GET(number/moderate_point_gain_multiplier)
|
|
point_gain_multipliers[EVENT_TRACK_MAJOR] = CONFIG_GET(number/major_point_gain_multiplier)
|
|
point_gain_multipliers[EVENT_TRACK_CREWSET] = CONFIG_GET(number/crewset_point_gain_multiplier)
|
|
point_gain_multipliers[EVENT_TRACK_GHOSTSET] = CONFIG_GET(number/ghostset_point_gain_multiplier)
|
|
|
|
roundstart_point_multipliers[EVENT_TRACK_MUNDANE] = CONFIG_GET(number/mundane_roundstart_point_multiplier)
|
|
roundstart_point_multipliers[EVENT_TRACK_MODERATE] = CONFIG_GET(number/moderate_roundstart_point_multiplier)
|
|
roundstart_point_multipliers[EVENT_TRACK_MAJOR] = CONFIG_GET(number/major_roundstart_point_multiplier)
|
|
roundstart_point_multipliers[EVENT_TRACK_CREWSET] = CONFIG_GET(number/crewset_roundstart_point_multiplier)
|
|
roundstart_point_multipliers[EVENT_TRACK_GHOSTSET] = CONFIG_GET(number/ghostset_roundstart_point_multiplier)
|
|
|
|
min_pop_thresholds[EVENT_TRACK_MUNDANE] = CONFIG_GET(number/mundane_min_pop)
|
|
min_pop_thresholds[EVENT_TRACK_MODERATE] = CONFIG_GET(number/moderate_min_pop)
|
|
min_pop_thresholds[EVENT_TRACK_MAJOR] = CONFIG_GET(number/major_min_pop)
|
|
min_pop_thresholds[EVENT_TRACK_CREWSET] = CONFIG_GET(number/crewset_min_pop)
|
|
min_pop_thresholds[EVENT_TRACK_GHOSTSET] = CONFIG_GET(number/ghostset_min_pop)
|
|
|
|
/datum/controller/subsystem/gamemode/proc/storyteller_vote_choices()
|
|
var/client_amount = GLOB.clients.len
|
|
var/list/choices = list()
|
|
var/list/vote_message = list()
|
|
for(var/storyteller_type in storytellers)
|
|
var/datum/storyteller/storyboy = storytellers[storyteller_type]
|
|
/// Prevent repeating storytellers
|
|
if(storyboy.storyteller_type && storyboy.storyteller_type == SSpersistence.last_storyteller_type)
|
|
continue
|
|
if(!storyboy.votable)
|
|
continue
|
|
if((storyboy.population_min && storyboy.population_min > client_amount) || (storyboy.population_max && storyboy.population_max < client_amount))
|
|
continue
|
|
choices += storyboy.name
|
|
///Because the vote subsystem is dumb and does not support any descriptions, we dump them into world.
|
|
vote_message += "<b>[storyboy.name]</b>"
|
|
vote_message += "[storyboy.desc]"
|
|
vote_message += ""
|
|
var/finalized_message = "[vote_message.Join("\n")]"
|
|
to_chat(world, vote_font(fieldset_block("Storyteller Vote", "[finalized_message]", "boxed_message purple_box")))
|
|
return choices
|
|
|
|
/datum/controller/subsystem/gamemode/proc/storyteller_vote_result(winner_name)
|
|
/// Find the winner
|
|
voted_storyteller = winner_name
|
|
if(storyteller)
|
|
return
|
|
for(var/storyteller_type in storytellers)
|
|
var/datum/storyteller/storyboy = storytellers[storyteller_type]
|
|
if(storyboy.name == winner_name)
|
|
voted_storyteller = storyteller_type
|
|
break
|
|
|
|
/datum/controller/subsystem/gamemode/proc/init_storyteller()
|
|
if(storyteller) // If this is true, then an admin bussed one, don't overwrite it
|
|
log_dynamic("Roundstart storyteller has been set by admins to [storyteller.name], the vote was not considered.")
|
|
return
|
|
var/datum/storyteller/storyteller_pick
|
|
if(!voted_storyteller)
|
|
storyteller_pick = pick(storytellers)
|
|
log_dynamic("Roundstart picked storyteller [storyteller_pick.name] randomly due to no vote result.")
|
|
voted_storyteller = storyteller_pick
|
|
|
|
set_storyteller(voted_storyteller)
|
|
if(vote_datum)
|
|
var/list/vote_results = vote_datum.elimination_results
|
|
var/serialized_vote_results = "[vote_results.Join("\n")]"
|
|
var/list/vote_result_message = list("Method: Ranked Vote\n\nElimination order:\n[serialized_vote_results]")
|
|
to_chat(world, custom_boxed_message("purple_box", vote_font("[vote_result_message.Join("\n")]")))
|
|
to_chat(world, vote_font(fieldset_block("Storyteller: [storyteller.name]", "[storyteller.welcome_text]", "boxed_message purple_box")))
|
|
|
|
if(vote_datum)
|
|
QDEL_NULL(vote_datum)
|
|
|
|
// Notify discord about the round's selected storyteller
|
|
for(var/channel_tag in CONFIG_GET(str_list/channel_announce_new_game))
|
|
send2chat(
|
|
new /datum/tgs_message_content("The storyteller selected for this round is [storyteller.name]!"),
|
|
channel_tag,
|
|
)
|
|
|
|
/**
|
|
* set_storyteller
|
|
*
|
|
* Always call this to set the storyteller
|
|
* Called by the storyteller system on roundstart and after a vote finishes.
|
|
* When forced via game panel, forced = TRUE, and force_ckey contains the ckey of the admin who forced it
|
|
*/
|
|
/datum/controller/subsystem/gamemode/proc/set_storyteller(passed_type, forced, force_ckey)
|
|
if(!storytellers[passed_type])
|
|
message_admins("Attempted to set an invalid storyteller type: [passed_type].")
|
|
CRASH("Attempted to set an invalid storyteller type: [passed_type].")
|
|
storyteller = storytellers[passed_type]
|
|
|
|
var/datum/storyteller_data/tracks/track_data = storyteller.track_data
|
|
point_thresholds[EVENT_TRACK_MUNDANE] = track_data.threshold_mundane * CONFIG_GET(number/mundane_point_threshold)
|
|
point_thresholds[EVENT_TRACK_MODERATE] = track_data.threshold_moderate * CONFIG_GET(number/moderate_point_threshold)
|
|
point_thresholds[EVENT_TRACK_MAJOR] = track_data.threshold_major * CONFIG_GET(number/major_point_threshold)
|
|
point_thresholds[EVENT_TRACK_CREWSET] = track_data.threshold_crewset * CONFIG_GET(number/crewset_point_threshold)
|
|
point_thresholds[EVENT_TRACK_GHOSTSET] = track_data.threshold_ghostset * CONFIG_GET(number/ghostset_point_threshold)
|
|
|
|
log_admin_private("Storyteller switched to [storyteller.name]. [forced ? "Forced by admin ckey [force_ckey]" : ""]")
|
|
|
|
/**
|
|
* halt_storyteller
|
|
*
|
|
* Used to halt/unhalt and properly log storyteller
|
|
*/
|
|
|
|
/datum/controller/subsystem/gamemode/proc/halt_storyteller(mob/user)
|
|
storyteller_halted = !storyteller_halted
|
|
if(isnull(user))
|
|
return
|
|
message_admins("[key_name_admin(user)] has [storyteller_halted ? "HALTED" : "un-halted"] the Storyteller.")
|
|
log_dynamic("Storyteller [storyteller_halted ? "halted" : "un-halted"] by admin [user.ckey].")
|
|
|
|
/**
|
|
* force_next_event
|
|
*
|
|
* Forces next event scheduling/firing for `track`
|
|
*
|
|
*/
|
|
/datum/controller/subsystem/gamemode/proc/force_next_event(track, mob/user)
|
|
if(isnull(user))
|
|
return
|
|
if(isnull(storyteller))
|
|
return
|
|
event_track_points[track] = point_thresholds[track]
|
|
storyteller.handle_tracks()
|
|
message_admins("[key_name_admin(user)] has forced an event for the [track] track.")
|
|
log_admin_private("Storyteller track [track] forced to run an event by [user.ckey]")
|
|
|
|
/**
|
|
* get_scheduled_by_event_type
|
|
*
|
|
* Returns the scheduled event, if any, by the event's type.
|
|
* Returns null if no such event exists.
|
|
*/
|
|
/datum/controller/subsystem/gamemode/proc/get_scheduled_by_event_type(type)
|
|
for(var/datum/scheduled_event/scheduled_event as anything in scheduled_events)
|
|
if(scheduled_event.event.type == text2path(type))
|
|
return scheduled_event
|
|
return null
|
|
|
|
/datum/controller/subsystem/gamemode/proc/get_event_by_track_and_type(track, type)
|
|
if(isnull(track) || isnull(type))
|
|
return
|
|
var/list/track_events = event_pools[track]
|
|
if(isnull(track_events))
|
|
return
|
|
|
|
for(var/datum/round_event_control/event as anything in track_events)
|
|
if(event.type == text2path(type))
|
|
return event
|
|
|
|
/datum/controller/subsystem/gamemode/proc/inject_event(datum/round_event_control/event_control)
|
|
if(!istype(event_control, /datum/round_event_control))
|
|
stack_trace("Storyteller was requested to inject event type [event_control ? event_control : "NULL"], but it's invalid!")
|
|
return
|
|
|
|
var/datum/round_event_control/event = locate(event_control) in SSevents.control
|
|
if(!event)
|
|
stack_trace("Storyteller was requested to inject event type [event_control] but could not locate it in SSevents.")
|
|
return
|
|
|
|
event.run_event(admin_forced = TRUE)
|
|
|
|
/datum/controller/subsystem/gamemode/proc/reroll_antagonist(datum/round_event_control/event_control, antag_name, datum/antagonist/existing_antag)
|
|
message_admins(span_yellowteamradio("[key_name_admin(usr)] requested a new antagonist to replace [antag_name]."))
|
|
log_admin("[key_name_admin(usr)] requested a new antagonist to replace [antag_name].")
|
|
if (isnull(event_control) && !isnull(existing_antag))
|
|
event_control = SSgamemode.antag_rerolls_by_pref[existing_antag.pref_flag]
|
|
if(isnull(event_control))
|
|
event_control = pick_weight(SSgamemode.antag_rerolls)
|
|
SSgamemode.inject_event(event_control = event_control)
|
|
|
|
ADMIN_VERB(create_antagonist, R_FUN, "Create Antagonist", "Inject a little more action into the round.", ADMIN_CATEGORY_EVENTS)
|
|
var/list/available_antags = list()
|
|
for(var/datum/round_event_control/event_control as anything in SSgamemode.antag_rerolls)
|
|
LAZYADD(available_antags, event_control)
|
|
|
|
var/datum/round_event_control/selected_event = tgui_input_list(user, "Choose a crew antagonist type to spawn.", "Create Antagonist", available_antags)
|
|
if(isnull(selected_event))
|
|
return
|
|
|
|
SSgamemode.reroll_antagonist(event_control = selected_event, antag_name = "nobody")
|
|
|
|
#undef INIT_ORDER_GAMEMODE
|