Event system rewrite started.

This commit is contained in:
Neerti
2020-03-19 07:31:30 -04:00
parent cef23cc2f4
commit 33657c9e11
39 changed files with 2138 additions and 313 deletions

View File

@@ -70,6 +70,7 @@ var/global/list/runlevel_flags = list(RUNLEVEL_LOBBY, RUNLEVEL_SETUP, RUNLEVEL_G
#define INIT_ORDER_CIRCUIT -21 #define INIT_ORDER_CIRCUIT -21
#define INIT_ORDER_AI -22 #define INIT_ORDER_AI -22
#define INIT_ORDER_JOB -23 #define INIT_ORDER_JOB -23
#define INIT_ORDER_GAME_MASTER -24
// Subsystem fire priority, from lowest to highest priority // Subsystem fire priority, from lowest to highest priority

View File

@@ -1,5 +1,5 @@
SUBSYSTEM_DEF(events) SUBSYSTEM_DEF(events)
name = "Events" name = "Events (Legacy)"
wait = 20 wait = 20
var/list/datum/event/active_events = list() var/list/datum/event/active_events = list()

View File

@@ -0,0 +1,35 @@
// This is a simple ticker for the new event system.
// The logic that determines what events get chosen is held inside a seperate subsystem.
SUBSYSTEM_DEF(event_ticker)
name = "Events (Ticker)"
wait = 2 SECONDS
runlevels = RUNLEVEL_GAME
// List of `/datum/event2/event`s that are currently active, and receiving process() ticks.
var/list/active_events = list()
// List of `/datum/event2/event`s that finished, and are here for showing at roundend, if that's desired.
var/list/finished_events = list()
// Process active events.
/datum/controller/subsystem/event_ticker/fire(resumed)
for(var/E in active_events)
var/datum/event2/event/event = E
event.process()
if(event.finished)
event_finished(event)
// Starts an event, independent of the GM system.
// This means it will always run, and won't affect the GM system in any way, e.g. not putting the event off limits after one use.
/datum/controller/subsystem/event_ticker/proc/start_event(event_type)
var/datum/event2/event/E = new event_type(src)
E.execute()
event_started(E)
/datum/controller/subsystem/event_ticker/proc/event_started(datum/event2/event/E)
active_events += E
/datum/controller/subsystem/event_ticker/proc/event_finished(datum/event2/event/E)
active_events -= E
finished_events += E

View File

@@ -0,0 +1,494 @@
// This is a sort of successor to the various event systems created over the years. It is designed to be just a tad smarter than the
// previous ones, checking various things like player count, department size and composition, individual player activity,
// individual player (IC) skill, and such, in order to try to choose the best events to take in order to add spice or variety to
// the round.
// This subsystem holds the logic that chooses events. Actual event processing is handled in a seperate subsystem.
SUBSYSTEM_DEF(game_master)
name = "Events (Game Master)"
wait = 1 MINUTE
runlevels = RUNLEVEL_GAME
// The GM object is what actually chooses events.
// It's held in a seperate object for better encapsulation, and allows for different 'flavors' of GMs to be made, that choose events differently.
var/datum/game_master/GM = null
var/game_master_type = /datum/game_master/default
var/list/available_events = list() // A list of meta event objects.
var/danger = 0 // The GM's best guess at how chaotic the round is. High danger makes it hold back.
var/staleness = -20 // Determines liklihood of the GM doing something, increases over time.
var/next_event = 0 // Minimum amount of time of nothingness until the GM can pick something again.
var/debug_messages = FALSE // If true, debug information is written to `log_debug()`.
/datum/controller/subsystem/game_master/Initialize()
var/list/subtypes = subtypesof(/datum/event2/meta)
for(var/T in subtypes)
var/datum/event2/meta/M = new T(src)
if(!M.name)
continue
available_events += M
GM = new game_master_type(src)
if(config && !config.enable_game_master)
can_fire = FALSE
// Remove when finished.
debug_gm()
return ..()
/datum/controller/subsystem/game_master/fire(resumed)
adjust_staleness(1)
adjust_danger(-1)
var/global_afk = metric.assess_all_living_mobs()
global_afk = abs(global_afk - 100)
global_afk = round(global_afk / 100, 0.1)
adjust_staleness(global_afk) // Staleness increases faster if more people are less active.
if(GM.ignore_time_restrictions || next_event < world.time)
if(prob(staleness) && pre_event_checks())
do_event_decision()
/datum/controller/subsystem/game_master/proc/do_event_decision()
log_game_master("Going to choose an event.")
var/datum/event2/meta/event_picked = GM.choose_event()
if(event_picked)
run_event(event_picked)
next_event = world.time + rand(GM.decision_cooldown_lower_bound, GM.decision_cooldown_upper_bound)
/datum/controller/subsystem/game_master/proc/debug_gm()
can_fire = TRUE
staleness = 100
debug_messages = TRUE
/datum/controller/subsystem/game_master/proc/run_event(datum/event2/meta/chosen_event)
var/datum/event2/event/E = chosen_event.make_event()
SSevent_ticker.event_started(E)
adjust_danger(chosen_event.chaos)
adjust_staleness(-(10 + chosen_event.chaos)) // More chaotic events reduce staleness more, e.g. a 25 chaos event will reduce it by 35.
// Tell the game master that something dangerous happened, e.g. someone dying, station explosions.
/datum/controller/subsystem/game_master/proc/adjust_danger(amount)
amount *= GM.danger_modifier
danger = round(between(0, danger + amount, 1000), 0.1)
// Tell the game master that things are getting boring if positive, or something interesting if negative..
/datum/controller/subsystem/game_master/proc/adjust_staleness(amount)
amount *= GM.staleness_modifier
staleness = round( between(-20, staleness + amount, 100), 0.1)
// These are ran before committing to an event.
// Returns TRUE if the system is allowed to procede, otherwise returns FALSE.
/datum/controller/subsystem/game_master/proc/pre_event_checks(quiet = FALSE)
if(!ticker || ticker.current_state != GAME_STATE_PLAYING)
if(!quiet)
log_game_master("Unable to start event: Ticker is nonexistant, or the game is not ongoing.")
return FALSE
if(GM.ignore_time_restrictions)
return TRUE
if(next_event > world.time) // Sanity.
if(!quiet)
log_game_master("Unable to start event: Time until next event is approximately [round((next_event - world.time) / (1 MINUTE))] minute(s)")
return FALSE
// Last minute antagging is bad for humans to do, so the GM will respect the start and end of the round.
var/mills = round_duration_in_ticks
var/mins = round((mills % 36000) / 600)
var/hours = round(mills / 36000)
// if(hours < 1 && mins <= 20) // Don't do anything for the first twenty minutes of the round.
// if(!quiet)
// log_debug("Game Master unable to start event: It is too early.")
// return FALSE
if(hours >= 2 && mins >= 40) // Don't do anything in the last twenty minutes of the round, as well.
if(!quiet)
log_game_master("Unable to start event: It is too late.")
return FALSE
return TRUE
/datum/controller/subsystem/game_master/proc/choose_game_master(mob/user)
var/list/subtypes = subtypesof(/datum/game_master)
var/new_gm_path = input(user, "What kind of Game Master do you want?", "New Game Master", /datum/game_master/default) as null|anything in subtypes
if(new_gm_path)
log_and_message_admins("has swapped the current GM ([GM.type]) for a new GM ([new_gm_path]).")
GM = new new_gm_path(src)
/datum/controller/subsystem/game_master/proc/log_game_master(message)
if(debug_messages)
log_debug("GAME MASTER: [message]")
// This object makes the actual decisions.
/datum/game_master
var/datum/controller/subsystem/game_master/ticker = null
// Multiplier for how much 'danger' is accumulated. Higer generally makes it possible for more dangerous events to be picked.
var/danger_modifier = 1.0
// Ditto. Higher numbers generally result in more events occuring in a round.
var/staleness_modifier = 1.0
var/decision_cooldown_lower_bound = 5 MINUTES // Lower bound for how long to wait until -the potential- for another event being decided.
var/decision_cooldown_upper_bound = 20 MINUTES // Same, but upper bound.
var/ignore_time_restrictions = FALSE // Useful for debugging without needing to wait 20 minutes each time.
var/ignore_round_chaos = FALSE // If true, the system will happily choose back to back intense events like meteors and blobs, Dwarf Fortress style.
/datum/game_master/New(datum/controller/subsystem/game_master/new_ticker)
ticker = new_ticker
// Push button, receive event.
// Returns a selected event datum.
/datum/game_master/proc/choose_event()
/datum/game_master/proc/log_game_master(message)
SSgame_master.log_game_master(message)
// The default game master tries to choose events with these goals in mind.
// * Don't choose an event if the crew can't take it. E.g. no meteors after half of the crew died.
// * Try to involve lots of people, particuarly in active departments.
// * Avoid giving events to the same department multiple times in a row.
/datum/game_master/default
// If an event was done for a specific department, it is written here, so it doesn't do it again.
var/last_department_used = null
/datum/game_master/default/choose_event()
log_game_master("Now starting event decision.")
var/list/most_active_departments = metric.assess_all_departments(3, list(last_department_used))
var/list/best_events = decide_best_events(most_active_departments)
if(LAZYLEN(best_events))
log_game_master("Got [best_events.len] choice\s for the next event.")
var/list/weighted_events = list()
for(var/E in best_events)
var/datum/event2/meta/event = E
weighted_events[event] = event.get_weight()
var/datum/event2/meta/choice = pickweight(weighted_events)
if(choice)
log_game_master("[choice.name] was chosen, and is now being ran.")
last_department_used = LAZYACCESS(choice.departments, 1)
return choice
/datum/game_master/default/proc/decide_best_events(list/most_active_departments)
if(!LAZYLEN(most_active_departments)) // Server's empty?
log_game_master("Game Master failed to find any active departments.")
return list()
var/list/best_events = list()
if(most_active_departments.len >= 2)
var/list/top_two = list(most_active_departments[1], most_active_departments[2])
best_events = filter_events_by_departments(top_two)
if(LAZYLEN(best_events)) // We found something for those two, let's do it.
return best_events
// Otherwise we probably couldn't find something for the second highest group, so let's ignore them.
best_events = filter_events_by_departments(most_active_departments[1])
if(LAZYLEN(best_events))
return best_events
// At this point we should expand our horizons.
best_events = filter_events_by_departments(list(DEPARTMENT_EVERYONE))
if(LAZYLEN(best_events))
return best_events
// Just give a random event if for some reason it still can't make up its mind.
best_events = filter_events_by_departments()
if(LAZYLEN(best_events))
return best_events
log_game_master("Game Master failed to find a suitable event, something very wrong is going on.")
return list()
// Filters the available events down to events for specific departments.
// Pass DEPARTMENT_EVERYONE if you want events that target the general population, like gravity failure.
// If no list is passed, all the events will be returned.
/datum/game_master/default/proc/filter_events_by_departments(list/departments)
. = list()
for(var/E in ticker.available_events)
var/datum/event2/meta/event = E
if(!event.enabled)
continue
if(event.chaotic_threshold && !ignore_round_chaos)
if(ticker.danger > event.chaotic_threshold)
continue
// An event has to involve all of these departments to pass.
var/viable = TRUE
if(LAZYLEN(departments))
for(var/department in departments)
if(!LAZYFIND(departments, department))
viable = FALSE
break
if(viable)
. += event
// The `old_like` game master tries to act like the old system, choosing events without any specific goals.
// * Has no goals, and instead operates purely off of the weights of the events it has.
// * Does not react to danger at all.
/datum/game_master/old_like/choose_event()
var/list/weighted_events = list()
for(var/E in ticker.available_events)
var/datum/event2/meta/event = E
weighted_events[event] = event.get_weight()
var/datum/event2/meta/choice = pickweight(weighted_events)
if(choice)
log_game_master("[choice.name] was chosen, and is now being ran.")
return choice
// The `super_random` game master chooses events purely at random, ignoring weights entirely.
// * Has no goals, and instead chooses randomly, ignoring weights.
// * Does not react to danger at all.
/datum/game_master/super_random/choose_event()
return pick(ticker.available_events)
// The `brutal` game master tries to run dangerous events frequently.
// * Chaotic events have their weights artifically boosted.
// * Ignores accumulated danger.
/datum/game_master/brutal
ignore_round_chaos = TRUE
/datum/game_master/brutal/choose_event()
var/list/weighted_events = list()
for(var/E in ticker.available_events)
var/datum/event2/meta/event = E
weighted_events[event] = event.get_weight() + (event.chaos * 2)
var/datum/event2/meta/choice = pickweight(weighted_events)
if(choice)
log_game_master("[choice.name] was chosen, and is now being ran.")
return choice
/client/proc/show_gm_status()
set category = "Debug"
set name = "Show GM Status"
set desc = "Shows you what the GM is thinking. If only that existed in real life..."
if(check_rights(R_ADMIN|R_EVENT|R_DEBUG))
SSgame_master.interact(usr)
else
to_chat(usr, span("warning", "You do not have sufficent rights to view the GM panel, sorry."))
/datum/controller/subsystem/game_master/proc/interact(var/client/user)
if(!user)
return
// Using lists for string tree conservation.
var/list/dat = list("<html><head><title>Automated Game Master Event System</title></head><body>")
// Makes the system turn on or off.
dat += href(src, list("toggle" = 1), "\[Toggle GM\]")
dat += " | "
// Makes the system not care about staleness or being near round-end.
dat += href(src, list("toggle_time_restrictions" = 1), "\[Toggle Time Restrictions\]")
dat += " | "
// Makes the system not care about how chaotic the round might be.
dat += href(src, list("toggle_chaos_throttle" = 1), "\[Toggle Chaos Throttling\]")
dat += " | "
// Makes the system immediately choose an event, while still bound to factors like danger, weights, and department staffing.
dat += href(src, list("force_choose_event" = 1), "\[Force Event Decision\]")
dat += "<br>"
// Swaps out the current GM for a new one with different ideas on what a good event might be.
dat += href(src, list("change_gm" = 1), "\[Change GM\]")
dat += "<br>"
dat += "Current GM Type: [GM.type]<br>"
dat += "State: [can_fire ? "Active": "Inactive"]<br>"
dat += "Status: [pre_event_checks(TRUE) ? "Ready" : "Suppressed"]<br><br>"
dat += "Staleness: [staleness] "
dat += href(src, list("set_staleness" = 1), "\[Set\]")
dat += "<br>"
dat += "<i>Staleness is an estimate of how boring the round might be, and if an event should be done. It is increased passively over time, \
and increases faster if people are AFK. It deceases when events and certain 'interesting' things happen in the round.</i><br>"
dat += "Danger: [danger] "
dat += href(src, list("set_danger" = 1), "\[Set\]")
dat += "<br>"
dat += "<i>Danger is an estimate of how chaotic the round has been so far. It is decreased passively over time, and is increased by having \
certain chaotic events be selected, or chaotic things happen in the round. A sufficently high amount of danger will make the system \
avoid using destructive events, to avoid pushing the station over the edge.</i><br>"
dat += "<h2>Player Activity:</h2>"
dat += "<table border='1' style='width:100%'>"
dat += "<tr>"
dat += "<th>Category</th>"
dat += "<th>Activity Percentage</th>"
dat += "</tr>"
dat += "<tr>"
dat += "<td>All Living Mobs</td>"
dat += "<td>[metric.assess_all_living_mobs()]%</td>"
dat += "</tr>"
dat += "<tr>"
dat += "<td>All Ghosts</td>"
dat += "<td>[metric.assess_all_dead_mobs()]%</td>"
dat += "</tr>"
dat += "<tr>"
dat += "<th colspan='2'>Departments</td>"
dat += "</tr>"
for(var/D in metric.departments)
dat += "<tr>"
dat += "<td>[D]</td>"
dat += "<td>[metric.assess_department(D)]%</td>"
dat += "</tr>"
dat += "<tr>"
dat += "<th colspan='2'>Players</td>"
dat += "</tr>"
for(var/P in player_list)
var/mob/M = P
dat += "<tr>"
dat += "<td>[M] ([M.ckey])</td>"
dat += "<td>[metric.assess_player_activity(M)]%</td>"
dat += "</tr>"
dat += "</table>"
dat += "<h2>Events available:</h2>"
dat += "<table border='1' style='width:100%'>"
dat += "<tr>"
dat += "<th>Event Name</th>"
dat += "<th>Involved Departments</th>"
dat += "<th>Chaos</th>"
dat += "<th>Current Weight</th>"
dat += "<th>Buttons</th>"
dat += "</tr>"
for(var/E in available_events)
var/datum/event2/meta/event = E
dat += "<tr>"
if(!event.enabled)
dat += "<td><strike>[event.name]</strike></td>"
else
dat += "<td>[event.name]</td>"
dat += "<td>[english_list(event.departments)]</td>"
dat += "<td>[event.chaos]</td>"
dat += "<td>[event.get_weight()]</td>"
dat += "<td>[href(event, list("force" = 1), "\[Force\]")] [href(event, list("toggle" = 1), "\[Toggle\]")]</td>"
dat += "</tr>"
dat += "</table>"
dat += "<h2>Events active:</h2>"
dat += "Current time: [world.time]"
dat += "<table border='1' style='width:100%'>"
dat += "<tr>"
dat += "<th>Event Type</th>"
dat += "<th>Time Started</th>"
dat += "<th>Time to Announce</th>"
dat += "<th>Time to End</th>"
dat += "<th>Announced</th>"
dat += "<th>Started</th>"
dat += "<th>Ended</th>"
dat += "<th>Buttons</th>"
dat += "</tr>"
for(var/E in SSevent_ticker.active_events)
var/datum/event2/event/event = E
dat += "<tr>"
dat += "<td>[event.type]</td>"
dat += "<td>[event.time_started]</td>"
dat += "<td>[event.time_to_announce ? event.time_to_announce : "NULL"]</td>"
dat += "<td>[event.time_to_end ? event.time_to_end : "NULL"]</td>"
dat += "<td>[event.announced ? "Yes" : "No"]</td>"
dat += "<td>[event.started ? "Yes" : "No"]</td>"
dat += "<td>[event.ended ? "Yes" : "No"]</td>"
dat += "<td>[href(event, list("abort" = 1), "\[Abort\]")]</td>"
dat += "</tr>"
dat += "</table>"
dat += "</body></html>"
dat += "<h2>Events completed:</h2>"
dat += "<table border='1' style='width:100%'>"
dat += "<tr>"
dat += "<th>Event Type</th>"
dat += "<th>Start Time</th>"
dat += "<th>Finish Time</th>"
dat += "</tr>"
for(var/E in SSevent_ticker.finished_events)
var/datum/event2/event/event = E
dat += "<tr>"
dat += "<td>[event.type]</td>"
dat += "<td>[event.time_started]</td>"
dat += "<td>[event.time_finished]</td>"
dat += "</tr>"
dat += "</body></html>"
var/datum/browser/popup = new(user, "game_master_debug", "Automated Game Master Event System", 800, 500, src)
popup.set_content(dat.Join())
popup.open()
/datum/controller/subsystem/game_master/Topic(href, href_list)
if(..())
return
if(href_list["close"]) // Needed or the window re-opens after closing, making it last forever.
return
if(!check_rights(R_ADMIN|R_EVENT|R_DEBUG))
message_admins("[usr] has attempted to modify the Game Master values without sufficent privilages.")
return
if(href_list["toggle"])
can_fire = !can_fire
message_admins("GM was [!can_fire ? "dis" : "en"]abled by [usr.key].")
if(href_list["toggle_time_restrictions"])
GM.ignore_time_restrictions = !GM.ignore_time_restrictions
message_admins("GM event time restrictions was [GM.ignore_time_restrictions ? "dis" : "en"]abled by [usr.key].")
if(href_list["toggle_chaos_throttle"])
GM.ignore_round_chaos = !GM.ignore_round_chaos
message_admins("GM event chaos restrictions was [GM.ignore_round_chaos ? "dis" : "en"]abled by [usr.key].")
if(href_list["force_choose_event"])
do_event_decision()
message_admins("[usr.key] forced the Game Master to choose an event immediately.")
if(href_list["change_gm"])
choose_game_master(usr)
if(href_list["set_staleness"])
var/amount = input(usr, "How much staleness should there be?", "Game Master") as null|num
if(!isnull(amount))
staleness = amount
message_admins("GM staleness was set to [amount] by [usr.key].")
if(href_list["set_danger"])
var/amount = input(usr, "How much danger should there be?", "Game Master") as null|num
if(!isnull(amount))
danger = amount
message_admins("GM danger was set to [amount] by [usr.key].")
interact(usr) // To refresh the UI.

View File

@@ -0,0 +1,402 @@
// This is a sort of successor to the various event systems created over the years. It is designed to be just a tad smarter than the
// previous ones, checking various things like player count, department size and composition, individual player activity,
// individual player (IC) skill, and such, in order to try to choose the best actions to take in order to add spice or variety to
// the round.
SUBSYSTEM_DEF(game_master_old)
name = "Events (Game Master)"
wait = 2 SECONDS
init_order = INIT_ORDER_GAME_MASTER
flags = SS_KEEP_TIMING
runlevels = RUNLEVEL_GAME
// List of `/datum/event2/event`s that are currently active, and receiving process() ticks.
var/list/active_events = list()
// List of `/datum/event2/event`s that finished, and are here for showing at roundend, if that's desired.
var/list/finished_events = list()
// List of `/datum/event2/meta`s that hold meta-information about events.
var/list/all_meta_events = list()
var/list/available_actions = list() // A list of 'actions' that the GM has access to, to spice up a round, such as events.
var/danger = 0 // The GM's best guess at how chaotic the round is. High danger makes it hold back.
var/staleness = -20 // Determines liklihood of the GM doing something, increases over time.
// Multiplier for how much 'danger' is accumulated. Higer generally makes it possible for more dangerous events to be picked.
var/danger_modifier = 1.0
// Ditto. Higher numbers generally result in more events occuring in a round.
var/staleness_modifier = 1.0
// If an event was done for a specific department, it is written here, so it doesn't do it again.
var/last_department_used = null
var/next_action = 0 // Minimum amount of time of nothingness until the GM can pick something again.
var/decision_cooldown_lower_bound = 5 MINUTES // Lower bound for how long to wait until -the potential- for another action being decided.
var/decision_cooldown_upper_bound = 20 MINUTES // Same, but upper bound.
var/ignore_time_restrictions = FALSE// Useful for debugging without needing to wait 20 minutes each time.
var/ignore_round_chaos = FALSE // If true, the system will happily choose back to back intense events like meteors and blobs, Dwarf Fortress style.
/datum/controller/subsystem/game_master/Initialize()
// available_actions = init_subtypes(/datum/gm_action)
// for(var/A in available_actions)
// var/datum/gm_action/action = A
// action.gm = src
var/list/subtypes = subtypesof(/datum/event2/meta)
for(var/T in subtypes)
var/datum/event2/meta/M = new T(src)
all_meta_events += M
if(config && !config.enable_game_master)
can_fire = FALSE
return ..()
/datum/controller/subsystem/game_master/fire(resumed)
// Process active events.
for(var/E in active_events)
var/datum/event2/event/event = E
event.process()
if(event.finished)
event_finished(event)
// Decide if a new event is a good idea, and if so, which one.
if(times_fired % (1 MINUTE / wait) == 0) // Run once a minute, even if `wait` gets changed in the future or something.
adjust_staleness(1)
adjust_danger(-1)
var/global_afk = metric.assess_all_living_mobs()
global_afk = abs(global_afk - 100)
global_afk = round(global_afk / 100, 0.1)
adjust_staleness(global_afk) // Staleness increases faster if more people are less active.
if(ignore_time_restrictions || next_action < world.time)
if(prob(staleness) && pre_action_checks())
log_debug("Game Master is going to start something.")
start_action()
/datum/controller/subsystem/game_master/proc/event_started(datum/event2/event/E)
active_events += E
/datum/controller/subsystem/game_master/proc/event_finished(datum/event2/event/E)
active_events -= E
finished_events += E
// These are ran before committing to an action or event.
// Returns TRUE if the system is allowed to procede, otherwise returns FALSE.
/datum/controller/subsystem/game_master/proc/pre_action_checks(quiet = FALSE)
if(!ticker || ticker.current_state != GAME_STATE_PLAYING)
if(!quiet)
log_debug("Game Master unable to start event: Ticker is nonexistant, or the game is not ongoing.")
return FALSE
if(ignore_time_restrictions)
return TRUE
if(next_action > world.time) // Sanity.
if(!quiet)
log_debug("Game Master unable to start event: Time until next action is approximately [round((next_action - world.time) / (1 MINUTE))] minute(s)")
return FALSE
// Last minute antagging is bad for humans to do, so the GM will respect the start and end of the round.
var/mills = round_duration_in_ticks
var/mins = round((mills % 36000) / 600)
var/hours = round(mills / 36000)
// if(hours < 1 && mins <= 20) // Don't do anything for the first twenty minutes of the round.
// if(!quiet)
// log_debug("Game Master unable to start event: It is too early.")
// return FALSE
if(hours >= 2 && mins >= 40) // Don't do anything in the last twenty minutes of the round, as well.
if(!quiet)
log_debug("Game Master unable to start event: It is too late.")
return FALSE
return TRUE
/datum/controller/subsystem/game_master/proc/start_action()
log_debug("Game Master now starting action decision.")
var/list/most_active_departments = metric.assess_all_departments(3, list(last_department_used))
var/list/best_actions = decide_best_action(most_active_departments)
if(best_actions && best_actions.len)
var/list/weighted_actions = list()
for(var/A in best_actions)
var/datum/gm_action/action = A
if(danger >= action.chaos_threshold)
continue // We skip dangerous events when bad stuff is already occuring.
weighted_actions[action] = action.get_weight()
var/datum/gm_action/choice = pickweight(weighted_actions)
if(choice)
log_debug("[choice.name] was chosen by the Game Master, and is now being ran.")
run_action(choice)
/datum/controller/subsystem/game_master/proc/run_action(datum/gm_action/action)
log_debug("[action.name] is being ran.")
action.set_up()
action.start()
action.announce()
if(action.chaotic)
danger += action.chaotic
next_action = world.time + rand(decision_cooldown_lower_bound, decision_cooldown_upper_bound)
last_department_used = LAZYACCESS(action.departments, 1)
/datum/controller/subsystem/game_master/proc/decide_best_action(list/most_active_departments)
if(!LAZYLEN(most_active_departments))// Server's empty?
log_debug("Game Master failed to find any active departments.")
return list()
var/list/best_actions = list() // List of actions which involve the most active departments.
if(most_active_departments.len >= 2)
for(var/A in available_actions)
var/datum/gm_action/action = A
if(!action.enabled)
continue
// Try to incorporate an action with the top two departments first.
if(most_active_departments[1] in action.departments && most_active_departments[2] in action.departments)
best_actions.Add(action)
log_debug("[action.name] is being considered because both most active departments are involved.")
if(best_actions.len) // We found something for those two, let's do it.
return best_actions
// Otherwise we probably couldn't find something for the second highest group, so let's ignore them.
for(var/A in available_actions)
var/datum/gm_action/action = A
if(!action.enabled)
continue
if(most_active_departments[1] in action.departments)
best_actions.Add(action)
log_debug("[action.name] is being considered because the most active department is involved.")
if(best_actions.len) // Found something for the one guy.
return best_actions
// At this point we should expand our horizons.
for(var/A in available_actions)
var/datum/gm_action/action = A
if(!action.enabled)
continue
if(DEPARTMENT_EVERYONE in action.departments)
best_actions.Add(action)
log_debug("[action.name] is being considered because it involves everyone.")
if(best_actions.len) // Finally, perhaps?
return best_actions
// Just give a random event if for some reason it still can't make up its mind.
for(var/A in available_actions)
var/datum/gm_action/action = A
if(!action.enabled)
continue
best_actions.Add(action)
log_debug("[action.name] is being considered because everything else failed.")
if(best_actions.len) // Finally, perhaps?
return best_actions
log_debug("Game Master failed to find a suitable event, something very wrong is going on.")
return list()
// Tell the game master that something dangerous happened, e.g. someone dying, station explosions.
/datum/controller/subsystem/game_master/proc/adjust_danger(amount)
amount *= danger_modifier
danger = round(between(0, danger + amount, 1000), 0.1)
// Tell the game master that things are getting boring if positive, or something interesting if negative..
/datum/controller/subsystem/game_master/proc/adjust_staleness(amount)
amount *= staleness_modifier
staleness = round( between(-20, staleness + amount, 100), 0.1)
/*
Admin UI
*/
/client/proc/show_gm_status()
set category = "Debug"
set name = "Show GM Status"
set desc = "Shows you what the GM is thinking. If only that existed in real life..."
if(check_rights(R_ADMIN|R_EVENT|R_DEBUG))
SSgame_master.interact(usr)
else
to_chat(usr, span("warning", "You do not have sufficent rights to view the GM panel, sorry."))
/datum/controller/subsystem/game_master/proc/interact(var/client/user)
if(!user)
return
// Using lists for string tree conservation.
var/list/dat = list("<html><head><title>Automated Game Master Event System</title></head><body>")
// Makes the system turn on or off.
dat += href(src, list("toggle" = 1), "\[Toggle GM\]")
dat += " | "
// Makes the system not care about staleness or being near round-end.
dat += href(src, list("toggle_time_restrictions" = 1), "\[Toggle Time Restrictions\]")
dat += " | "
// Makes the system not care about how chaotic the round might be.
dat += href(src, list("toggle_chaos_throttle" = 1), "\[Toggle Chaos Throttling\]")
dat += " | "
// Makes the system immediately choose an event, while still bound to factors like danger, weights, and department staffing.
dat += href(src, list("force_choose_event" = 1), "\[Force Event Decision\]")
dat += "<br>"
dat += "State: [can_fire ? "Active": "Inactive"]<br>"
dat += "Status: [pre_action_checks(TRUE) ? "Ready" : "Suppressed"]<br><br>"
dat += "Staleness: [staleness] "
dat += href(src, list("set_staleness" = 1), "\[Set\]")
dat += "<br>"
dat += "<i>Staleness is an estimate of how boring the round might be, and if an event should be done. It is increased passively over time, \
and increases faster if people are AFK. It deceases when events and certain 'interesting' things happen in the round.</i><br>"
dat += "Danger: [danger] "
dat += href(src, list("set_danger" = 1), "\[Set\]")
dat += "<br>"
dat += "<i>Danger is an estimate of how chaotic the round has been so far. It is decreased passively over time, and is increased by having \
certain chaotic events be selected, or chaotic things happen in the round. A sufficently high amount of danger will make the system \
avoid using destructive events, to avoid pushing the station over the edge.</i><br>"
dat += "<h2>Player Activity:</h2>"
dat += "<table border='1' style='width:100%'>"
dat += "<tr>"
dat += "<th>Category</th>"
dat += "<th>Activity Percentage</th>"
dat += "</tr>"
dat += "<tr>"
dat += "<td>All Living Mobs</td>"
dat += "<td>[metric.assess_all_living_mobs()]%</td>"
dat += "</tr>"
dat += "<tr>"
dat += "<td>All Ghosts</td>"
dat += "<td>[metric.assess_all_dead_mobs()]%</td>"
dat += "</tr>"
dat += "<tr>"
dat += "<th colspan='2'>Departments</td>"
dat += "</tr>"
for(var/D in metric.departments)
dat += "<tr>"
dat += "<td>[D]</td>"
dat += "<td>[metric.assess_department(D)]%</td>"
dat += "</tr>"
dat += "<tr>"
dat += "<th colspan='2'>Players</td>"
dat += "</tr>"
for(var/P in player_list)
var/mob/M = P
dat += "<tr>"
dat += "<td>[M] ([M.ckey])</td>"
dat += "<td>[metric.assess_player_activity(M)]%</td>"
dat += "</tr>"
dat += "</table>"
dat += "<h2>Actions available:</h2>"
dat += "<table border='1' style='width:100%'>"
dat += "<tr>"
dat += "<th>Action Name</th>"
dat += "<th>Involved Departments</th>"
dat += "<th>Chaos</th>"
dat += "<th>Current Weight</th>"
dat += "<th>Buttons</th>"
dat += "</tr>"
for(var/A in available_actions)
var/datum/gm_action/action = A
dat += "<tr>"
dat += "<td>[action.name]</td>"
dat += "<td>[english_list(action.departments)]</td>"
dat += "<td>[action.chaotic]</td>"
dat += "<td>[action.get_weight()]</td>"
dat += "<td>[href(action, list("force" = 1), "\[Force\]")] [href(action, list("toggle" = 1), "\[Toggle\]")]</td>"
dat += "</tr>"
dat += "</table>"
dat += "</body></html>"
var/datum/browser/popup = new(user, "game_master_debug", "Automated Game Master Event System", 800, 500, src)
popup.set_content(dat.Join())
popup.open()
// HTML += "Actions available;<br>"
// for(var/datum/gm_action/action in available_actions)
// if(action.enabled == FALSE)
// continue
// HTML += "[action.name] ([english_list(action.departments)]) (weight: [action.get_weight()]) <a href='?src=\ref[action];force=1'>\[Force\]</a> <br>"
// HTML += "<br>"
// HTML += "All living mobs activity: [metric.assess_all_living_mobs()]%<br>"
// HTML += "All ghost activity: [metric.assess_all_dead_mobs()]%<br>"
//
// HTML += "<br>"
// HTML += "Departmental activity;<br>"
// for(var/department in metric.departments)
// HTML += " [department] : [metric.assess_department(department)]%<br>"
//
// HTML += "<br>"
// HTML += "Activity of players;<br>"
// for(var/mob/player in player_list)
// HTML += " [player] ([player.key]) : [metric.assess_player_activity(player)]%<br>"
//
// HTML +="</body></html>"
// user << browse(HTML, "window=log;size=400x450;border=1;can_resize=1;can_close=1;can_minimize=1")
/datum/controller/subsystem/game_master/Topic(href, href_list)
if(..())
return
if(!check_rights(R_ADMIN|R_EVENT|R_DEBUG))
message_admins("[usr] has attempted to modify the Game Master values without being an admin.")
return
if(href_list["toggle"])
can_fire = !can_fire
message_admins("GM was [!can_fire ? "dis" : "en"]abled by [usr.key].")
if(href_list["toggle_time_restrictions"])
ignore_time_restrictions = !ignore_time_restrictions
message_admins("GM event time restrictions was [ignore_time_restrictions ? "dis" : "en"]abled by [usr.key].")
if(href_list["toggle_chaos_throttle"])
ignore_round_chaos = !ignore_round_chaos
message_admins("GM event chaos restrictions was [ignore_round_chaos ? "dis" : "en"]abled by [usr.key].")
if(href_list["force_choose_event"])
start_action()
message_admins("[usr.key] forced the Game Master to choose an event immediately.")
if(href_list["set_staleness"])
var/amount = input(usr, "How much staleness should there be?", "Game Master") as null|num
if(!isnull(amount))
staleness = amount
message_admins("GM staleness was set to [amount] by [usr.key].")
if(href_list["set_danger"])
var/amount = input(usr, "How much danger should there be?", "Game Master") as null|num
if(!isnull(amount))
danger = amount
message_admins("GM danger was set to [amount] by [usr.key].")
interact(usr) // To refresh the UI.

View File

@@ -496,6 +496,7 @@
M.emote("gasp") M.emote("gasp")
M.Weaken(rand(10,25)) M.Weaken(rand(10,25))
M.updatehealth() M.updatehealth()
SSgame_master.adjust_danger(-20)
apply_brain_damage(M, deadtime) apply_brain_damage(M, deadtime)
/obj/item/weapon/shockpaddles/proc/apply_brain_damage(mob/living/carbon/human/H, var/deadtime) /obj/item/weapon/shockpaddles/proc/apply_brain_damage(mob/living/carbon/human/H, var/deadtime)

View File

@@ -1,18 +0,0 @@
/proc/level_seven_blob_announcement(var/obj/structure/blob/core/B)
if(!B || !B.overmind)
return
var/datum/blob_type/blob = B.overmind.blob_type // Shortcut so we don't need to delve into three variables every time.
var/list/lines = list()
lines += "Confirmed outbreak of level [7 + blob.difficulty] biohazard aboard [station_name()]. All personnel must contain the outbreak."
if(blob.difficulty >= BLOB_DIFFICULTY_MEDIUM) // Tell them what kind of blob it is if it's tough.
lines += "The biohazard has been identified as a '[blob.name]'."
if(blob.difficulty >= BLOB_DIFFICULTY_HARD) // If it's really hard then tell them where it is so the response occurs faster.
lines += "It is suspected to have originated from \the [get_area(B)]."
if(blob.difficulty >= BLOB_DIFFICULTY_SUPERHARD)
lines += "Extreme caution is advised."
command_announcement.Announce(lines.Join("\n"), "Biohazard Alert", new_sound = 'sound/AI/outbreak7.ogg')

View File

@@ -40,7 +40,6 @@ var/list/overminds = list()
color = blob_type.complementary_color color = blob_type.complementary_color
if(blob_core) if(blob_core)
blob_core.update_icon() blob_core.update_icon()
level_seven_blob_announcement(blob_core)
..(newloc) ..(newloc)

View File

@@ -125,18 +125,20 @@
activeFor++ activeFor++
//Called when start(), announce() and end() has all been called. //Called when start(), announce() and end() has all been called.
/datum/event/proc/kill() /datum/event/proc/kill(external_use = FALSE)
// If this event was forcefully killed run end() for individual cleanup // If this event was forcefully killed run end() for individual cleanup
if(isRunning) if(isRunning)
isRunning = 0 isRunning = 0
end() end()
endedAt = world.time endedAt = world.time
if(!external_use)
SSevents.active_events -= src SSevents.active_events -= src
SSevents.event_complete(src) SSevents.event_complete(src)
/datum/event/New(var/datum/event_meta/EM) /datum/event/New(var/datum/event_meta/EM, external_use = FALSE)
// event needs to be responsible for this, as stuff like APLUs currently make their own events for curious reasons // event needs to be responsible for this, as stuff like APLUs currently make their own events for curious reasons
if(!external_use)
SSevents.active_events += src SSevents.active_events += src
event_meta = EM event_meta = EM

View File

@@ -52,7 +52,7 @@ var/list/event_last_fired = list()
possibleEvents[/datum/event/pda_spam] = max(min(25, player_list.len) * 4, 200) possibleEvents[/datum/event/pda_spam] = max(min(25, player_list.len) * 4, 200)
possibleEvents[/datum/event/money_lotto] = max(min(5, player_list.len), 50) possibleEvents[/datum/event/money_lotto] = max(min(5, player_list.len), 50)
if(account_hack_attempted) if(GLOB.account_hack_attempted)
possibleEvents[/datum/event/money_hacker] = max(min(25, player_list.len) * 4, 200) possibleEvents[/datum/event/money_hacker] = max(min(25, player_list.len) * 4, 200)

View File

@@ -1,5 +1,7 @@
//var/global/account_hack_attempted = 0 //var/global/account_hack_attempted = 0
GLOBAL_VAR_INIT(account_hack_attempted, 0)
/datum/event/money_hacker /datum/event/money_hacker
var/datum/money_account/affected_account var/datum/money_account/affected_account
endWhen = 100 endWhen = 100
@@ -10,7 +12,7 @@
if(all_money_accounts.len) if(all_money_accounts.len)
affected_account = pick(all_money_accounts) affected_account = pick(all_money_accounts)
account_hack_attempted = 1 GLOB.account_hack_attempted = 1
else else
kill() kill()

View File

@@ -3,6 +3,7 @@
var/enabled = TRUE // If not enabled, this action is never taken. var/enabled = TRUE // If not enabled, this action is never taken.
var/departments = list() // What kinds of departments are affected by this action. Multiple departments can be listed. var/departments = list() // What kinds of departments are affected by this action. Multiple departments can be listed.
var/chaotic = 0 // A number showing how chaotic the action may be. If danger is high, the GM will avoid it. var/chaotic = 0 // A number showing how chaotic the action may be. If danger is high, the GM will avoid it.
var/chaos_threshold = 0 // If the GM's danger score is at this number or higher, the event won't get picked.
var/reusable = FALSE // If true, the event does not become disabled upon being used. Should be used sparingly. var/reusable = FALSE // If true, the event does not become disabled upon being used. Should be used sparingly.
var/observers_used = FALSE // Determines if the GM should check if ghosts are available before using this. var/observers_used = FALSE // Determines if the GM should check if ghosts are available before using this.
var/length = 0 // Determines how long the event lasts, until end() is called. var/length = 0 // Determines how long the event lasts, until end() is called.
@@ -25,3 +26,24 @@
/datum/gm_action/proc/announce() /datum/gm_action/proc/announce()
return return
/datum/gm_action/proc/should_end()
return TRUE
/datum/gm_action/Topic(href, href_list)
if(..())
return
if(!is_admin(usr))
message_admins("[usr] has attempted to force an event without being an admin.")
return
if(href_list["force"])
// gm.run_action(src)
message_admins("GM event [name] was forced by [usr.key].")
if(href_list["toggle"])
enabled = !enabled
message_admins("GM event [name] was toggled [enabled ? "on" : "off"] by [usr.key].")
SSgame_master.interact(usr) // To refresh the UI.

View File

@@ -1,10 +0,0 @@
/datum/gm_action/security_drill
name = "security drills"
departments = list(DEPARTMENT_SECURITY, DEPARTMENT_EVERYONE)
/datum/gm_action/security_drill/announce()
spawn(rand(1 MINUTE, 2 MINUTES))
command_announcement.Announce("[pick("A NanoTrasen security director", "A Vir-Gov correspondant", "Local Sif authoritiy")] has advised the enactment of [pick("a rampant wildlife", "a fire", "a hostile boarding", "a nonstandard", "a bomb", "an emergent intelligence")] drill with the personnel onboard \the [station_name()].", "Security Advisement")
/datum/gm_action/security_drill/get_weight()
return max(-20, 10 + gm.staleness - (gm.danger * 2)) + (metric.count_people_in_department(DEPARTMENT_SECURITY) * 5) + (metric.count_people_in_department(DEPARTMENT_EVERYONE) * 1.5)

View File

@@ -0,0 +1,21 @@
/datum/gm_action/security_drill
name = "security drills"
departments = list(DEPARTMENT_SECURITY, DEPARTMENT_EVERYONE)
chaos_threshold = 20 // To avoid CentCom from mandating a drill while people are dying left and right.
/datum/gm_action/security_drill/announce()
command_announcement.Announce("[pick("A NanoTrasen security director", "A Vir-Gov correspondant", "Local Sif authoritiy")] \
has advised the enactment of [pick("a rampant wildlife", "a fire", "a hostile boarding", "a nonstandard", \
"a bomb", "an emergent intelligence")] drill with the personnel onboard \the [station_name()].", "Security Advisement")
/datum/gm_action/security_drill/get_weight()
var/sec = metric.count_people_in_department(DEPARTMENT_SECURITY)
var/everyone = metric.count_people_in_department(DEPARTMENT_EVERYONE)
if(!sec) // If there's no security, then there is no drill.
return 0
if(everyone - sec < 0) // If there's no non-sec, then there is no drill.
return 0
// Each security player adds +5 weight, while non-security adds +1.5.
return (sec * 5) + ((everyone - sec) * 1.5)

View File

@@ -1,7 +1,8 @@
/datum/gm_action/rogue_drone /datum/gm_action/rogue_drone
name = "rogue drones" name = "rogue drones"
departments = list(DEPARTMENT_SECURITY) departments = list(DEPARTMENT_SECURITY)
chaotic = 60 chaotic = 20
chaos_threshold = EVENT_CHAOS_THRESHOLD_MEDIUM_IMPACT
length = 20 MINUTES length = 20 MINUTES
var/list/drones_list = list() var/list/drones_list = list()
@@ -60,4 +61,6 @@
command_announcement.Announce("We're disappointed at the loss of the drones, but the survivors have been recovered.", "Rogue drone alert") command_announcement.Announce("We're disappointed at the loss of the drones, but the survivors have been recovered.", "Rogue drone alert")
/datum/gm_action/rogue_drone/get_weight() /datum/gm_action/rogue_drone/get_weight()
return 20 + (metric.count_people_in_department(DEPARTMENT_SECURITY) * 10) + (metric.count_all_space_mobs() * 30) . = 20 // Start with a base weight of 20, since this event does provide some value even if no sec is around.
. += metric.count_people_in_department(DEPARTMENT_SECURITY) * 10 // Each sec adds 10.
. += metric.count_all_space_mobs() * 30 // Each player currently EVA adds 30.

View File

@@ -2,6 +2,7 @@
name = "spider infestation" name = "spider infestation"
departments = list(DEPARTMENT_SECURITY, DEPARTMENT_MEDICAL, DEPARTMENT_EVERYONE) departments = list(DEPARTMENT_SECURITY, DEPARTMENT_MEDICAL, DEPARTMENT_EVERYONE)
chaotic = 30 chaotic = 30
chaos_threshold = EVENT_CHAOS_THRESHOLD_MEDIUM_IMPACT
severity = 1 severity = 1
@@ -26,11 +27,13 @@
spawncount = rand(4 * severity, 6 * severity) spawncount = rand(4 * severity, 6 * severity)
/datum/gm_action/spider_infestation/announce() /datum/gm_action/spider_infestation/announce()
command_announcement.Announce("Unidentified lifesigns detected coming aboard [station_name()]. Secure any exterior access, including ducting and ventilation.", "Lifesign Alert", new_sound = 'sound/AI/aliens.ogg') command_announcement.Announce("Unidentified lifesigns detected coming aboard [station_name()]. \
Secure any exterior access, including ducting and ventilation.", "Lifesign Alert", new_sound = 'sound/AI/aliens.ogg')
if(severity >= EVENT_LEVEL_MAJOR) if(severity >= EVENT_LEVEL_MAJOR)
spawn(rand(600, 3000)) spawn(rand(600, 3000))
command_announcement.Announce("Unidentified lifesigns previously detected coming aboard [station_name()] have been classified as a swarm of arachnids. Extreme caution is advised.", "Arachnid Alert") command_announcement.Announce("Unidentified lifesigns previously detected coming aboard [station_name()] \
have been classified as a swarm of arachnids. Extreme caution is advised.", "Arachnid Alert")
/datum/gm_action/spider_infestation/start() /datum/gm_action/spider_infestation/start()
..() ..()

View File

@@ -1,92 +0,0 @@
/client/proc/show_gm_status()
set category = "Debug"
set name = "Show GM Status"
set desc = "Shows you what the GM is thinking. If only that existed in real life..."
game_master.interact(usr)
/datum/game_master/proc/interact(var/client/user)
if(!user)
return
var/HTML = "<html><head><title>Game Master AI</title></head><body>"
HTML += "<a href='?src=\ref[src];toggle_time_restrictions=1'>\[Toggle Time Restrictions\]</a> | \
<a href='?src=\ref[src];suspend=1'>\[Toggle GM\]</a> | \
<a href='?src=\ref[src];force_choose_event=1'>\[Force Event Decision\]</a><br>"
HTML += "Status: [pre_action_checks() ? "Ready" : "Suppressed"]<br><br>"
HTML += "Staleness: [staleness] <a href='?src=\ref[src];adjust_staleness=1'>\[Adjust\]</a><br>"
HTML += "Danger: [danger] <a href='?src=\ref[src];adjust_danger=1'>\[Adjust\]</a><br><br>"
HTML += "Actions available;<br>"
for(var/datum/gm_action/action in available_actions)
if(action.enabled == FALSE)
continue
HTML += "[action.name] ([english_list(action.departments)]) (weight: [action.get_weight()]) <a href='?src=\ref[action];force=1'>\[Force\]</a> <br>"
HTML += "<br>"
HTML += "All living mobs activity: [metric.assess_all_living_mobs()]%<br>"
HTML += "All ghost activity: [metric.assess_all_dead_mobs()]%<br>"
HTML += "<br>"
HTML += "Departmental activity;<br>"
for(var/department in metric.departments)
HTML += " [department] : [metric.assess_department(department)]%<br>"
HTML += "<br>"
HTML += "Activity of players;<br>"
for(var/mob/player in player_list)
HTML += " [player] ([player.key]) : [metric.assess_player_activity(player)]%<br>"
HTML +="</body></html>"
user << browse(HTML, "window=log;size=400x450;border=1;can_resize=1;can_close=1;can_minimize=1")
/datum/game_master/Topic(href, href_list)
if(..())
return
if(!is_admin(usr))
message_admins("[usr] has attempted to modify the Game Master values without being an admin.")
return
if(href_list["toggle_time_restrictions"])
ignore_time_restrictions = !ignore_time_restrictions
message_admins("GM event time restrictions was [ignore_time_restrictions ? "dis" : "en"]abled by [usr.key].")
if(href_list["force_choose_event"])
start_action()
message_admins("[usr.key] forced the Game Master to choose an event immediately.")
if(href_list["suspend"])
suspended = !suspended
message_admins("GM was [suspended ? "dis" : "en"]abled by [usr.key].")
if(href_list["adjust_staleness"])
var/amount = input(usr, "How much staleness should be added or subtracted?", "Game Master") as null|num
if(amount)
adjust_staleness(amount)
message_admins("GM staleness was adjusted by [amount] by [usr.key].")
if(href_list["adjust_danger"])
var/amount = input(usr, "How much danger should be added or subtracted?", "Game Master") as null|num
if(amount)
adjust_danger(amount)
message_admins("GM danger was adjusted by [amount] by [usr.key].")
interact(usr) // To refresh the UI.
/datum/gm_action/Topic(href, href_list)
if(..())
return
if(!is_admin(usr))
message_admins("[usr] has attempted to force an event without being an admin.")
return
if(href_list["force"])
gm.run_action(src)
message_admins("GM event [name] was forced by [usr.key].")

View File

@@ -1 +1,5 @@
#define EVENT_BASELINE_WEIGHT 200 #define EVENT_BASELINE_WEIGHT 200
#define EVENT_CHAOS_THRESHOLD_HIGH_IMPACT 25
#define EVENT_CHAOS_THRESHOLD_MEDIUM_IMPACT 50
#define EVENT_CHAOS_THRESHOLD_LOW_IMPACT 100

View File

@@ -0,0 +1,185 @@
// This object holds the code that is needed to execute an event.
// Code for judging whether doing that event is a good idea or not belongs inside its meta event object.
/*
Important: DO NOT `sleep()` in any of the procs here, or the GM will get stuck. Use callbacks insead.
Also please don't use spawn(), but use callbacks instead.
Note that there is an important distinction between an event being ended, and an event being finished.
- Ended is for when the actual event is over, regardless of whether an announcement happened or not.
- Finished is for when both the event itself is over, and it was announced. The event will stop being
processed after it is finished.
For an event to finish, it must have done two things:
- Go through its entire cycle, of start() -> end(), and
- Have the event be announced.
If an event has ended, but the announcement didn't happen, the event will not be finished.
This allows for events that have their announcement happen after the end itself.
*/
//
/datum/event2/event
var/announced = FALSE // Is set to TRUE when `announce()` is called by `process()`.
var/started = FALSE // Is set to TRUE when `start()` is called by `process()`.
var/ended = FALSE // Is set to TRUE when `end()` is called by `process()`.
var/finished = FALSE // Is set to TRUE when `ended` and `announced` are TRUE.
// `world.time`s when this event started, and finished, for bookkeeping.
var/time_started = null
var/time_finished = null
// If these are set, the announcement will be delayed by a random time between the lower and upper bounds.
// If the upper bound is not defined, then it will use the lower bound instead.
// Note that this is independant of the event itself, so you can have the announcement happen long after the event ended.
// This may not work if should_announce() is overrided.
var/announce_delay_lower_bound = null
var/announce_delay_upper_bound = null
// If these are set, the event will be delayed by a random time between the lower and upper bounds.
// If the upper bound is not defined, then it will use the lower bound instead.
// This may not work if should_start() is overrided.
var/start_delay_lower_bound = null
var/start_delay_upper_bound = null
// If these are set, the event will automatically end at a random time between the lower and upper bounds.
// If the upper bound is not defined, then it will use the lower bound instead.
// This may not work if should_end() is overrided.
var/length_lower_bound = null
var/length_upper_bound = null
// Set automatically, don't touch.
var/time_to_start = null
var/time_to_announce = null
var/time_to_end = null
// Returns the name of where the event is taking place.
// In the future this might be handy for off-station events.
/datum/event2/event/proc/location_name()
return station_name()
// Returns the z-levels that are involved with the event.
// In the future this might be handy for off-station events.
/datum/event2/event/proc/get_location_z_levels()
return using_map.station_levels
// Starts the event.
/datum/event2/event/proc/execute()
time_started = world.time
if(announce_delay_lower_bound)
time_to_announce = world.time + rand(announce_delay_lower_bound, announce_delay_upper_bound ? announce_delay_upper_bound : announce_delay_lower_bound)
if(start_delay_lower_bound)
time_to_start = world.time + rand(start_delay_lower_bound, start_delay_upper_bound ? start_delay_upper_bound : start_delay_lower_bound)
if(length_lower_bound)
time_to_end = world.time + rand(length_lower_bound, length_upper_bound ? length_upper_bound : length_lower_bound)
set_up()
// Called at the very end of the event's lifecycle, or when aborted.
// Don't override this, use `end()` for cleanup instead.
/datum/event2/event/proc/finish()
finished = TRUE
time_finished = world.time
// Called by admins wanting to stop an event immediately.
/datum/event2/event/proc/abort()
if(!announced)
announce()
if(!ended) // `end()` generally has cleanup procs, so call that.
end()
finish()
// Called by the GM processer.
/datum/event2/event/process()
// Handle announcement track.
if(!announced && should_announce())
announce()
announced = TRUE
// Handle event track.
if(!started)
if(should_start())
start()
started = TRUE
else
wait_tick()
if(started && !ended)
if(should_end())
end()
ended = TRUE
else
event_tick()
// In order to be finished, the event needs to end, and be announced.
if(ended && announced)
finish()
/datum/event2/event/Topic(href, href_list)
if(..())
return
if(!check_rights(R_ADMIN|R_EVENT|R_DEBUG))
message_admins("[usr] has attempted to manipulate an event without sufficent privilages.")
return
if(href_list["abort"])
abort()
message_admins("Event '[type]' was aborted by [usr.key].")
SSgame_master.interact(usr) // To refresh the UI.
/*
* Procs to Override
*/
// Override this for code to be ran before the event is started.
/datum/event2/event/proc/set_up()
// Called every tick from the GM system, and determines if the announcement should happen.
// Override this for special logic on when it should be announced, e.g. after `ended` is set to TRUE,
// however be aware that the event cannot finish until this returns TRUE at some point.
/datum/event2/event/proc/should_announce()
if(!time_to_announce)
return TRUE
return time_to_announce <= world.time
// Override this for code that alerts the crew that the event is happening in some form, e.g. a centcom announcement or some other message.
// If you want them to not know, you can just not override it.
/datum/event2/event/proc/announce()
// Override for code that runs every few seconds, while the event is waiting for `should_start()` to return TRUE.
// Note that events that have `should_start()` return TRUE at the start will never have this proc called.
/datum/event2/event/proc/wait_tick()
// Called every tick from the GM system, and determines if the event should offically start.
// Override this for special logic on when it should start.
/datum/event2/event/proc/should_start()
if(!time_to_start)
return TRUE
return time_to_start <= world.time
// Override this for code to do the actual event.
/datum/event2/event/proc/start()
// Override for code that runs every few seconds, while the event is waiting for `should_end()` to return TRUE.
// Note that events that have `should_end()` return TRUE at the start will never have this proc called.
/datum/event2/event/proc/event_tick()
// Called every tick from the GM system, and determines if the event should end.
// If this returns TRUE at the very start, then the event ends instantly and `tick()` will never be called.
// Override this for special logic on when it should end, e.g. blob core has to die before event ends.
/datum/event2/event/proc/should_end()
if(!time_to_end)
return TRUE
return time_to_end <= world.time
// Override this for code to run when the event is over, e.g. cleanup.
/datum/event2/event/proc/end()

View File

@@ -0,0 +1,16 @@
/datum/event2/meta/shipping_error
name = "shipping error"
departments = list(DEPARTMENT_CARGO)
chaos = -10 // A helpful event.
reusable = TRUE
event_type = /datum/event2/event/shipping_error
/datum/event2/meta/shipping_error/get_weight()
return metric.count_people_in_department(DEPARTMENT_CARGO) * 40
/datum/event2/event/shipping_error/start()
var/datum/supply_order/O = new /datum/supply_order()
O.ordernum = supply_controller.ordernum
O.object = supply_controller.supply_pack[pick(supply_controller.supply_pack)]
O.ordered_by = random_name(pick(MALE,FEMALE), species = "Human")
supply_controller.shoppinglist += O

View File

@@ -0,0 +1,163 @@
/datum/event2/meta/blob
name = "blob"
departments = list(DEPARTMENT_ENGINEERING, DEPARTMENT_SECURITY, DEPARTMENT_MEDICAL)
chaos = 50
chaotic_threshold = EVENT_CHAOS_THRESHOLD_HIGH_IMPACT
event_type = /datum/event2/event/blob
/datum/event2/meta/blob/get_weight()
var/engineers = metric.count_people_in_department(DEPARTMENT_ENGINEERING)
var/security = metric.count_people_in_department(DEPARTMENT_SECURITY)
var/medical = metric.count_people_in_department(DEPARTMENT_MEDICAL)
var/assigned_staff = engineers + security
if(engineers || security) // Medical only counts if one of the other two exists, and even then they count as half.
assigned_staff += round(medical / 2)
var/weight = (max(assigned_staff - 2, 0) * 20) // An assigned staff count of 2 must be had to spawn a blob.
return weight
/datum/event2/event/blob
// This could be made into a GLOB accessible list if needed.
var/list/area/excuded = list(
/area/submap,
/area/shuttle,
/area/crew_quarters,
/area/holodeck,
/area/engineering/engine_room
)
var/turf/target_turf = null
var/area/target_area = null
var/spawn_blob_type = /obj/structure/blob/core/random_medium
var/obj/structure/blob/core/blob_core = null
/datum/event2/event/blob/set_up()
var/list/area/grand_list_of_areas = get_station_areas(excluded)
for(var/i in 1 to 10)
var/area/A = pick(grand_list_of_areas)
if(is_area_occupied(A))
log_debug("Blob infestation event: Rejected [A] because it is occupied.")
continue
var/list/turfs = list()
for(var/turf/simulated/floor/F in A)
if(turf_clear(F))
turfs += F
if(turfs.len == 0)
log_debug("Blob infestation event: Rejected [A] because it has no clear turfs.")
continue
target_area = A
target_turf = pick(turfs)
if(!target_area)
log_debug("Blob infestation event: Giving up after too many failures to pick target area")
/datum/event2/event/blob/start()
blob_core = new spawn_blob_type(target_turf)
/datum/event2/event/blob/announce()
spawn(rand(600, 3000)) // 1-5 minute leeway for the blob to go un-detected.
command_announcement.Announce("Confirmed outbreak of level 7 biohazard aboard [station_name()]. All personnel must contain the outbreak.", "Biohazard Alert", new_sound = 'sound/AI/outbreak7.ogg')
/*
/datum/gm_action/blob
name = "blob infestation"
departments = list(DEPARTMENT_ENGINEERING, DEPARTMENT_SECURITY, DEPARTMENT_MEDICAL)
chaotic = 25
var/list/area/excluded = list(
/area/submap,
/area/shuttle,
/area/crew_quarters,
/area/holodeck,
/area/engineering/engine_room
)
var/area/target_area // Chosen target area
var/turf/target_turf // Chosen target turf in target_area
var/obj/structure/blob/core/Blob
var/spawn_blob_type = /obj/structure/blob/core/random_medium
/datum/gm_action/blob/set_up()
severity = pickweight(EVENT_LEVEL_MUNDANE = 4,
EVENT_LEVEL_MODERATE = 2,
EVENT_LEVEL_MAJOR = 1
)
var/list/area/grand_list_of_areas = get_station_areas(excluded)
for(var/i in 1 to 10)
var/area/A = pick(grand_list_of_areas)
if(is_area_occupied(A))
log_debug("Blob infestation event: Rejected [A] because it is occupied.")
continue
var/list/turfs = list()
for(var/turf/simulated/floor/F in A)
if(turf_clear(F))
turfs += F
if(turfs.len == 0)
log_debug("Blob infestation event: Rejected [A] because it has no clear turfs.")
continue
target_area = A
target_turf = pick(turfs)
if(!target_area)
log_debug("Blob infestation event: Giving up after too many failures to pick target area")
/datum/gm_action/blob/start()
..()
var/turf/T
if(severity == EVENT_LEVEL_MUNDANE || !target_area || !target_turf)
T = pick(blobstart)
else if(severity == EVENT_LEVEL_MODERATE)
T = target_turf
else
T = target_turf
spawn_blob_type = /obj/structure/blob/core/random_hard
Blob = new spawn_blob_type(T)
/datum/gm_action/blob/announce()
spawn(rand(600, 3000)) // 1-5 minute leeway for the blob to go un-detected.
command_announcement.Announce("Confirmed outbreak of level 7 biohazard aboard [station_name()]. All personnel must contain the outbreak.", "Biohazard Alert", new_sound = 'sound/AI/outbreak7.ogg')
/datum/gm_action/blob/get_weight()
var/engineers = metric.count_people_in_department(DEPARTMENT_ENGINEERING)
var/security = metric.count_people_in_department(DEPARTMENT_SECURITY)
var/medical = metric.count_people_in_department(DEPARTMENT_MEDICAL)
var/assigned_staff = engineers + security
if(engineers || security) // Medical only counts if one of the other two exists, and even then they count as half.
assigned_staff += round(medical / 2)
var/weight = (max(assigned_staff - 2, 0) * 20) // An assigned staff count of 2 must be had to spawn a blob.
return weight
*/
/*
/proc/level_seven_blob_announcement(var/obj/structure/blob/core/B)
if(!B || !B.overmind)
return
var/datum/blob_type/blob = B.overmind.blob_type // Shortcut so we don't need to delve into three variables every time.
var/list/lines = list()
lines += "Confirmed outbreak of level [7 + blob.difficulty] biohazard aboard [station_name()]. All personnel must contain the outbreak."
if(blob.difficulty >= BLOB_DIFFICULTY_MEDIUM) // Tell them what kind of blob it is if it's tough.
lines += "The biohazard has been identified as a '[blob.name]'."
if(blob.difficulty >= BLOB_DIFFICULTY_HARD) // If it's really hard then tell them where it is so the response occurs faster.
lines += "It is suspected to have originated from \the [get_area(B)]."
if(blob.difficulty >= BLOB_DIFFICULTY_SUPERHARD)
lines += "Extreme caution is advised."
command_announcement.Announce(lines.Join("\n"), "Biohazard Alert", new_sound = 'sound/AI/outbreak7.ogg')
*/

View File

@@ -0,0 +1,41 @@
/datum/event2/meta/camera_damage
name = "random camera damage"
departments = list(DEPARTMENT_SYNTHETIC, DEPARTMENT_ENGINEERING)
chaos = 5
chaotic_threshold = EVENT_CHAOS_THRESHOLD_LOW_IMPACT
reusable = TRUE
event_type = /datum/event2/event/camera_damage
/datum/event2/meta/camera_damage/get_weight()
return 30 + (metric.count_people_in_department(DEPARTMENT_ENGINEERING) * 20) + (metric.count_people_in_department(DEPARTMENT_SYNTHETIC) * 40)
/datum/event2/event/camera_damage
var/camera_range = 7
/datum/event2/event/camera_damage/start()
var/obj/machinery/camera/C = acquire_random_camera()
if(!C)
return
for(var/obj/machinery/camera/cam in range(camera_range, C))
if(is_valid_camera(cam))
cam.wires.UpdateCut(CAMERA_WIRE_POWER, 0)
if(prob(25))
cam.wires.UpdateCut(CAMERA_WIRE_ALARM, 0)
/datum/event2/event/camera_damage/proc/acquire_random_camera(var/remaining_attempts = 5)
if(!cameranet.cameras.len)
return
if(!remaining_attempts)
return
var/obj/machinery/camera/C = pick(cameranet.cameras)
if(is_valid_camera(C))
return C
// It is very important to use --var and not var-- for recursive calls, as var-- will cause an infinite loop.
return acquire_random_camera(--remaining_attempts)
/datum/event2/event/camera_damage/proc/is_valid_camera(var/obj/machinery/camera/C)
// Only return a functional camera, not installed in a silicon/hardsuit/circuit/etc, and that exists somewhere players have access
var/turf/T = get_turf(C)
return T && C.can_use() && istype(C.loc, /turf) && (T.z in using_map.player_levels)

View File

@@ -0,0 +1,26 @@
//
// This event chooses a random canister on player levels and breaks it, releasing its contents!
//
/datum/event2/meta/canister_leak
name = "canister leak"
departments = list(DEPARTMENT_ENGINEERING)
chaos = 10
chaotic_threshold = EVENT_CHAOS_THRESHOLD_LOW_IMPACT
reusable = TRUE
event_type = /datum/event2/event/canister_leak
/datum/event2/meta/canister_leak/get_weight()
return metric.count_people_in_department(DEPARTMENT_ENGINEERING) * 30
/datum/event2/event/canister_leak/start()
// List of all non-destroyed canisters on station levels
var/list/all_canisters = list()
for(var/obj/machinery/portable_atmospherics/canister/C in machines)
if(!C.destroyed && (C.z in using_map.station_levels) && C.air_contents.total_moles >= MOLES_CELLSTANDARD)
all_canisters += C
var/obj/machinery/portable_atmospherics/canister/C = pick(all_canisters)
log_debug("canister_leak event: Canister [C] ([C.x],[C.y],[C.z]) destroyed.")
C.health = 0
C.healthcheck()

View File

@@ -0,0 +1,48 @@
// New grid check event:
// Very similar to the old one, power goes out in most of the station, however the new feature is the ability for engineering to
// get power back on sooner, if they are able to reach a special machine and initiate a manual reboot. If no one is able to do so,
// it will reboot itself after a few minutes, just like the old one. Bad things happen if there is no grid checker machine protecting
// the powernet when this event fires.
/datum/event2/meta/grid_check
name = "grid check"
departments = list(DEPARTMENT_ENGINEERING, DEPARTMENT_EVERYONE)
chaos = 10
chaotic_threshold = EVENT_CHAOS_THRESHOLD_LOW_IMPACT
reusable = TRUE
event_type = /datum/event2/event/grid_check
// Having the turbines be way over their rated limit makes grid checks more likely.
/datum/event2/meta/grid_check/proc/get_overpower()
var/highest_overpower = 0
for(var/T in GLOB.all_turbines)
var/obj/machinery/power/generator/turbine = T
var/overpower = max((turbine.effective_gen / turbine.max_power) - 1, 0)
if(overpower > highest_overpower)
highest_overpower = overpower
return highest_overpower
/datum/event2/meta/grid_check/get_weight()
return 50 + (metric.count_people_in_department(DEPARTMENT_ENGINEERING) * 20) + (50 * get_overpower())
/datum/event2/event/grid_check
var/obj/machinery/power/generator/engine // The turbine that will send a power spike.
/datum/event2/event/grid_check/set_up()
// Find the turbine being pushed the most.
var/obj/machinery/power/generator/most_stressed_turbine = null
for(var/T in GLOB.all_turbines)
var/obj/machinery/power/generator/turbine = T
if(!most_stressed_turbine)
most_stressed_turbine = turbine
else if(turbine.effective_gen > most_stressed_turbine.effective_gen)
most_stressed_turbine = turbine
engine = most_stressed_turbine
/datum/event2/event/grid_check/start()
// This sets off a chain of events that lead to the actual grid check (or perhaps worse).
// First, the Supermatter engine makes a power spike.
if(engine)
engine.power_spike()
// After that, the engine checks if a grid checker exists on the same powernet, and if so, it triggers a blackout.
// If not, lots of stuff breaks. See code/modules/power/generator.dm for that piece of code.

View File

@@ -0,0 +1,17 @@
/datum/event2/meta/spacevine
name = "space-vine infestation"
departments = list(DEPARTMENT_ENGINEERING)
chaos = 10 // There's a really rare chance of vines getting something awful like phoron atmosphere but thats not really controllable.
chaotic_threshold = EVENT_CHAOS_THRESHOLD_MEDIUM_IMPACT
event_type = /datum/event2/event/spacevine
/datum/event2/meta/spacevine/get_weight()
return 20 + (metric.count_people_in_department(DEPARTMENT_ENGINEERING) * 20) + (metric.count_people_in_department(DEPARTMENT_EVERYONE) * 10)
/datum/event2/event/spacevine/announce()
level_seven_announcement()
/datum/event2/event/spacevine/start()
spacevine_infestation()

View File

@@ -0,0 +1,32 @@
/datum/event2/meta/comms_blackout
name = "communications blackout"
departments = list(DEPARTMENT_EVERYONE) // It's not an engineering event because engineering can't do anything to help . . . for now.
chaos = 10
chaotic_threshold = EVENT_CHAOS_THRESHOLD_MEDIUM_IMPACT
reusable = TRUE
event_type = /datum/event2/event/comms_blackout
/datum/event2/meta/comms_blackout/get_weight()
return 50 + metric.count_people_in_department(DEPARTMENT_EVERYONE) * 5
/datum/event2/event/comms_blackout/announce()
if(prob(33))
command_announcement.Announce("Ionospheric anomalies detected. \
Temporary telecommunication failure imminent. Please contact you-BZZT", new_sound = 'sound/misc/interference.ogg')
// AIs will always know if there's a comm blackout, rogue AIs could then lie about comm blackouts in the future while they shutdown comms
for(var/mob/living/silicon/ai/A in player_list)
to_chat(A, "<br>")
to_chat(A, "<span class='warning'><b>Ionospheric anomalies detected. Temporary telecommunication failure imminent. Please contact you-BZZT</b></span>")
to_chat(A, "<br>")
/datum/event2/event/comms_blackout/start()
// One in two chance for the radios to turn i%t# t&_)#%, which can be more alarming than radio silence.
if(prob(50))
for(var/obj/machinery/telecomms/processor/P in telecomms_list)
P.emp_act(1)
// Otherwise just shut everything down.
else
for(var/obj/machinery/telecomms/T in telecomms_list)
T.emp_act(1)

View File

@@ -0,0 +1,50 @@
/datum/event2/meta/solar_storm
name = "solar storm"
reusable = TRUE
event_type = /datum/event2/event/solar_storm
/datum/event2/meta/solar_storm/get_weight()
return 20 + (metric.count_people_in_department(DEPARTMENT_ENGINEERING) * 10) + (metric.count_all_space_mobs() * 30)
/datum/event2/event/solar_storm
start_delay_lower_bound = 1 MINUTE
start_delay_upper_bound = 1 MINUTE
length_lower_bound = 2 MINUTES
length_upper_bound = 4 MINUTES
var/base_solar_gen_rate = null
/datum/event2/event/solar_storm/announce()
command_announcement.Announce("A solar storm has been detected approaching \the [station_name()]. \
Please halt all EVA activites immediately and return to the interior of the station.", "Anomaly Alert", new_sound = 'sound/AI/radiation.ogg')
adjust_solar_output(1.5)
/datum/event2/event/solar_storm/start()
command_announcement.Announce("The solar storm has reached the station. Please refain from EVA and remain inside the station until it has passed.", "Anomaly Alert")
adjust_solar_output(5)
/datum/event2/event/solar_storm/event_tick()
radiate()
/datum/event2/event/solar_storm/end()
command_announcement.Announce("The solar storm has passed the station. It is now safe to resume EVA activities. \
Please report to medbay if you experience any unusual symptoms.", "Anomaly Alert")
adjust_solar_output(1)
/datum/event2/event/solar_storm/proc/adjust_solar_output(var/mult = 1)
if(isnull(base_solar_gen_rate))
base_solar_gen_rate = GLOB.solar_gen_rate
GLOB.solar_gen_rate = mult * base_solar_gen_rate
/datum/event2/event/solar_storm/proc/radiate()
// Note: Too complicated to be worth trying to use the radiation system for this. Its only in space anyway, so we make an exception in this case.
for(var/mob/living/L in player_list)
var/turf/T = get_turf(L)
if(!T)
continue
if(!istype(T.loc,/area/space) && !istype(T,/turf/space)) //Make sure you're in a space area or on a space turf
continue
//Todo: Apply some burn damage from the heat of the sun. Until then, enjoy some moderate radiation.
L.rad_act(rand(15, 30))

View File

@@ -0,0 +1,54 @@
// This is a somewhat special type of event, that bridges to the old event datum and makes it work with the new system.
// This is possible because the new datum is mostly superset of the old one.
/datum/event2/event/legacy
var/datum/event/legacy_event = null
var/tick_count = 0 // Used to emulate legacy's `activeFor` tick counter.
/datum/event2/meta/legacy/get_weight()
return 50
/datum/event2/event/legacy/process()
..()
tick_count++
/datum/event2/event/legacy/set_up()
legacy_event = new legacy_event(null, external_use = TRUE)
legacy_event.setup()
/datum/event2/event/legacy/should_announce()
return tick_count >= legacy_event.announceWhen
/datum/event2/event/legacy/announce()
legacy_event.announce()
// Legacy events don't tick before they start, so we don't need to do `wait_tick()`.
/datum/event2/event/legacy/should_start()
return tick_count >= legacy_event.startWhen
/datum/event2/event/legacy/start()
legacy_event.start()
/datum/event2/event/legacy/event_tick()
legacy_event.tick()
/datum/event2/event/legacy/should_end()
return tick_count >= legacy_event.endWhen
/datum/event2/event/legacy/end()
legacy_event.end()
/datum/event2/event/legacy/finish()
legacy_event.kill(external_use = TRUE)
..()
// Testing.
/datum/event2/meta/legacy_gravity
name = "gravity (legacy)"
reusable = TRUE
event_type = /datum/event2/event/legacy/gravity
/datum/event2/event/legacy/gravity
legacy_event = /datum/event/gravity

View File

@@ -0,0 +1,61 @@
// This is a somewhat special type of event, that bridges to the old event datum and makes it work with the new system.
// It acts as a compatability layer between the old event, and the new GM system.
// This is possible because the new datum is mostly a superset of the old one.
/datum/event2/event/legacy
var/datum/event/legacy_event = null
// Used to emulate legacy's `activeFor` tick counter.
var/tick_count = 0
// How 'severe' the legacy event should be. This should only be used for legacy events, as severity is an outdated concept for the GM system.
var/severity = EVENT_LEVEL_MODERATE
/datum/event2/meta/legacy/get_weight()
return 50
/datum/event2/event/legacy/process()
..()
tick_count++
/datum/event2/event/legacy/set_up()
legacy_event = new legacy_event(null, external_use = TRUE)
legacy_event.severity = severity
legacy_event.setup()
/datum/event2/event/legacy/should_announce()
return tick_count >= legacy_event.announceWhen
/datum/event2/event/legacy/announce()
legacy_event.announce()
// Legacy events don't tick before they start, so we don't need to do `wait_tick()`.
/datum/event2/event/legacy/should_start()
return tick_count >= legacy_event.startWhen
/datum/event2/event/legacy/start()
legacy_event.start()
/datum/event2/event/legacy/event_tick()
legacy_event.tick()
/datum/event2/event/legacy/should_end()
return tick_count >= legacy_event.endWhen
/datum/event2/event/legacy/end()
legacy_event.end()
/datum/event2/event/legacy/finish()
legacy_event.kill(external_use = TRUE)
..()
// Testing.
/datum/event2/meta/legacy_gravity
name = "gravity (legacy)"
reusable = TRUE
event_type = /datum/event2/event/legacy/gravity
/datum/event2/event/legacy/gravity
legacy_event = /datum/event/gravity

View File

@@ -0,0 +1,22 @@
/datum/event2/meta/security_drill
name = "security drill"
departments = list(DEPARTMENT_SECURITY, DEPARTMENT_EVERYONE)
chaotic_threshold = EVENT_CHAOS_THRESHOLD_HIGH_IMPACT // Don't run if we just got hit by meteors.
event_type = /datum/event2/event/security_drill
/datum/event2/meta/security_drill/get_weight()
var/sec = metric.count_people_in_department(DEPARTMENT_SECURITY)
var/everyone = metric.count_people_in_department(DEPARTMENT_EVERYONE)
if(!sec) // If there's no security, then there is no drill.
return 0
if(everyone - sec < 0) // If there's no non-sec, then there is no drill.
return 0
// Each security player adds +5 weight, while non-security adds +1.5.
return (sec * 5) + ((everyone - sec) * 1.5)
/datum/event2/event/security_drill/announce()
command_announcement.Announce("[pick("A NanoTrasen security director", "A Vir-Gov correspondant", "Local Sif authoritiy")] \
has advised the enactment of [pick("a rampant wildlife", "a fire", "a hostile boarding", \
"a bomb", "an emergent intelligence")] drill with the personnel onboard \the [location_name()].", "Security Advisement")

View File

@@ -0,0 +1,93 @@
/datum/event2/meta/security_screening
name = "security screening"
departments = list(DEPARTMENT_SECURITY, DEPARTMENT_EVERYONE)
chaotic_threshold = EVENT_CHAOS_THRESHOLD_HIGH_IMPACT // So this won't get called in the middle of a crisis.
event_type = /datum/event2/event/security_screening
/datum/event2/meta/security_screening/get_weight()
. = 0
var/sec = metric.count_people_in_department(DEPARTMENT_SECURITY)
if(!sec < 2)
return 0 // Can't screen with no security.
. += sec * 10
. += metric.count_people_in_department(DEPARTMENT_EVERYONE) * 2
// Having ""suspecious"" people present makes this more likely to be picked.
var/suspicious_people = 0
suspicious_people += metric.count_all_of_specific_species(SPECIES_PROMETHEAN) * 20
suspicious_people += metric.count_all_of_specific_species(SPECIES_UNATHI) * 10
suspicious_people += metric.count_all_of_specific_species(SPECIES_ZADDAT) * 10
suspicious_people += metric.count_all_of_specific_species(SPECIES_SKRELL) * 5 // Not sure why skrell are so high.
suspicious_people += metric.count_all_of_specific_species(SPECIES_TAJ) * 5
suspicious_people += metric.count_all_of_specific_species(SPECIES_TESHARI) * 5
suspicious_people += metric.count_all_of_specific_species(SPECIES_HUMAN_VATBORN) * 5
suspicious_people += metric.count_all_FBPs_of_kind(FBP_DRONE) * 20
suspicious_people += metric.count_all_FBPs_of_kind(FBP_POSI) * 10
if(!suspicious_people)
return 0
. += suspicious_people
/datum/event2/event/security_screening
var/victim = null
var/list/species_weights = list(
SPECIES_SKRELL = 9,
SPECIES_UNATHI = 15,
SPECIES_HUMAN_VATBORN = 6,
SPECIES_TESHARI = 2,
SPECIES_TAJ = 3,
SPECIES_DIONA = 1,
SPECIES_ZADDAT = 25,
SPECIES_PROMETHEAN = 30
)
var/list/synth_weights = list(
FBP_CYBORG = 15,
FBP_DRONE = 30,
FBP_POSI = 25
)
/datum/event2/event/security_screening/set_up()
var/list/end_weights = list()
// First pass makes popular things more likely to get picked, e.g. 5 prommies vs 1 drone.
for(var/species_name in species_weights)
var/give_weight = 0
for(var/datum/data/record/R in data_core.general)
if(R.fields["species"] == species_name)
give_weight += species_weights[species_name]
end_weights[species_name] = give_weight
for(var/bot_type in synth_weights)
var/give_weight = 0
for(var/datum/data/record/R in data_core.general)
if(R.fields["brain_type"] == bot_type)
give_weight += synth_weights[bot_type]
end_weights[bot_type] = give_weight
// Second pass eliminates things that don't exist on the station.
// It's possible to choose something like drones when all the drones are AFK. This prevents that from happening.
while(end_weights.len) // Keep at it until we find someone or run out of possibilities.
var/victim_chosen = pickweight(end_weights)
if(victim_chosen in synth_weights)
if(metric.count_all_FBPs_of_kind(victim_chosen) > 0)
victim = victim_chosen
break
else
if(metric.count_all_of_specific_species(victim_chosen) > 0)
victim = victim_chosen
break
if(!victim)
end_weights -= victim_chosen
if(!victim)
log_debug("Security Screening event failed to find anyone to screen. Aborting.")
abort()
return
/datum/event2/event/security_screening/announce()
command_announcement.Announce("[pick("A nearby Navy vessel", "A Solar official", "A Vir-Gov official", "A NanoTrasen board director")] has \
requested the screening of [pick("every other", "every", "suspicious", "willing")] [victim] \
personnel onboard \the [location_name()].", "Security Advisement")

View File

@@ -0,0 +1,58 @@
/datum/event2/meta/ion_storm
name = "ion storm"
departments = list(DEPARTMENT_SYNTHETIC)
chaos = 40
chaotic_threshold = EVENT_CHAOS_THRESHOLD_MEDIUM_IMPACT
event_type = /datum/event2/event/ion_storm
/datum/event2/meta/ion_storm/get_weight()
var/bots = metric.count_people_in_department(DEPARTMENT_SYNTHETIC)
var/weight = 5 + (bots * 20)
return weight
/datum/event2/event/ion_storm
announce_delay_lower_bound = 7 MINUTES
announce_delay_upper_bound = 15 MINUTES
var/bot_emag_chance = 30 // This is rolled once, instead of once a second for a minute like the old version.
/datum/event2/event/ion_storm/start()
// Ion laws.
for(var/mob/living/silicon/target in silicon_mob_list)
if(target.z in get_location_z_levels())
// Don't ion law drons.
if(istype(target, /mob/living/silicon/robot/drone))
continue
// Or borgs with an AI (they'll get their AI's ion law anyways).
if(istype(target, /mob/living/silicon/robot))
var/mob/living/silicon/robot/R = target
if(R.connected_ai)
continue
// Crew member names, and excluding off station antags, are handled by `generate_ion_law()` automatically.
var/law = target.generate_ion_law()
target.add_ion_law(law)
target.show_laws()
// Emag bots.
for(var/mob/living/bot/B in mob_list)
if(B.z in get_location_z_levels())
if(prob(bot_emag_chance))
B.emag_act(1)
// Messaging server spam filters.
// This might be better served as a seperate event since it seems more like a hacker attack than a natural occurance.
if(message_servers)
for(var/obj/machinery/message_server/MS in message_servers)
if(MS.z in get_location_z_levels())
MS.spamfilter.Cut()
for (var/i = 1, i <= MS.spamfilter_limit, i++)
MS.spamfilter += pick("warble","help","almach","ai","liberty","freedom","drugs", "[using_map.station_short]", \
"admin","sol","security","meow","_","monkey","-","moron","pizza","message","spam",\
"director", "Hello", "Hi!"," ","nuke","crate","taj","xeno")
/datum/event2/event/ion_storm/announce()
if(prob(50))
command_announcement.Announce("An ion storm was detected within proximity to \the [location_name()]. \
Check all AI controlled equipment for corruption.", "Anomaly Alert", new_sound = 'sound/AI/ionstorm.ogg')

View File

@@ -0,0 +1,85 @@
// The 'meta' object contains information about its assigned 'action' object, like what departments it will affect.
// It is directly held inside the Game Master Event System.
// The code for actually executing an event should go inside the event object instead.
/datum/event2/meta
// Name used for organization, shown in the debug verb for the GM system.
// If null, the meta event will be discarded when the GM system initializes, so it is safe to use nameless subtypes for inheritence.
var/name = null
// If FALSE, the GM system won't pick this.
// Some events set this to FALSE after running, to avoid running twice.
var/enabled = TRUE
// What departments the event attached might affect.
var/list/departments = list(DEPARTMENT_EVERYONE)
// A guess on how disruptive to a round the event might be. If the action is chosen, the GM's
// 'danger' score is increased by this number.
// Negative numbers could be used to signify helpful events.
var/chaos = 0
// A threshold the GM will use alongside its 'danger' score, to determine if it should pass
// over the event associated with this object. The decision is based on
var/chaotic_threshold = null
// If true, the event won't have it's `enabled` var set to FALSE when ran by the GM system.
var/reusable = FALSE
// A reference to the system that initialized us.
var/datum/controller/subsystem/game_master/GM = null
// The type path to the event associated with this meta object.
// When the GM chooses this event, a new instance is made.
// Seperate instances allow for multiple concurrent events without sharing state, e.g. two blobs.
var/event_type = null
/datum/event2/meta/New(datum/controller/subsystem/game_master/new_gm)
GM = new_gm
/datum/event2/meta/Destroy()
GM = null
return ..()
// Called by the GM system to actually start an event,
// and makes it so events that should only be ran once are made to not be usable again.
/datum/event2/meta/proc/make_event()
var/datum/event2/event/E = new event_type(GM)
E.execute()
if(!reusable)
enabled = FALSE
return E
// Returns a TRUE or FALSE for if the GM system should be able to pick this event.
// Can be extended to check for more than just `enabled` later.
/datum/event2/meta/proc/can_pick()
return enabled
/*
* Procs to Override
*/
// Returns a number that determines how likely it is for the event to be picked over others.
// Individual events should override this for their own weights.
/datum/event2/meta/proc/get_weight()
return 0
/datum/event2/meta/Topic(href, href_list)
if(..())
return
if(!check_rights(R_ADMIN|R_EVENT|R_DEBUG))
message_admins("[usr] has attempted to manipulate an event without sufficent privilages.")
return
if(href_list["force"])
SSevent_ticker.start_event(event_type)
message_admins("Event '[name]' was forced by [usr.key].")
if(href_list["toggle"])
enabled = !enabled
message_admins("Event '[name]' was toggled [enabled ? "on" : "off"] by [usr.key].")
SSgame_master.interact(usr) // To refresh the UI.

View File

@@ -3,7 +3,7 @@
// individual player (IC) skill, and such, in order to try to choose the best actions to take in order to add spice or variety to // individual player (IC) skill, and such, in order to try to choose the best actions to take in order to add spice or variety to
// the round. // the round.
/datum/game_master /datum/game_master_old
var/suspended = TRUE // If true, it will not do anything. var/suspended = TRUE // If true, it will not do anything.
var/ignore_time_restrictions = FALSE// Useful for debugging without needing to wait 20 minutes each time. var/ignore_time_restrictions = FALSE// Useful for debugging without needing to wait 20 minutes each time.
var/list/available_actions = list() // A list of 'actions' that the GM has access to, to spice up a round, such as events. var/list/available_actions = list() // A list of 'actions' that the GM has access to, to spice up a round, such as events.
@@ -15,7 +15,7 @@
var/next_action = 0 // Minimum amount of time of nothingness until the GM can pick something again. var/next_action = 0 // Minimum amount of time of nothingness until the GM can pick something again.
var/last_department_used = null // If an event was done for a specific department, it is written here, so it doesn't do it again. var/last_department_used = null // If an event was done for a specific department, it is written here, so it doesn't do it again.
/datum/game_master/New() /datum/game_master_old/New()
..() ..()
available_actions = init_subtypes(/datum/gm_action) available_actions = init_subtypes(/datum/gm_action)
for(var/datum/gm_action/action in available_actions) for(var/datum/gm_action/action in available_actions)
@@ -32,7 +32,7 @@
else else
sleep(30 SECONDS) sleep(30 SECONDS)
/datum/game_master/process() /datum/game_master_old/process()
if(ticker && ticker.current_state == GAME_STATE_PLAYING && !suspended) if(ticker && ticker.current_state == GAME_STATE_PLAYING && !suspended)
adjust_staleness(1) adjust_staleness(1)
adjust_danger(-1) adjust_danger(-1)
@@ -49,7 +49,7 @@
start_action() start_action()
// This is run before committing to an action/event. // This is run before committing to an action/event.
/datum/game_master/proc/pre_action_checks() /datum/game_master_old/proc/pre_action_checks()
if(!ticker || ticker.current_state != GAME_STATE_PLAYING) if(!ticker || ticker.current_state != GAME_STATE_PLAYING)
log_debug("Game Master unable to start event: Ticker is nonexistant, or the game is not ongoing.") log_debug("Game Master unable to start event: Ticker is nonexistant, or the game is not ongoing.")
return FALSE return FALSE
@@ -73,7 +73,7 @@
return FALSE return FALSE
return TRUE return TRUE
/datum/game_master/proc/start_action() /datum/game_master_old/proc/start_action()
if(!pre_action_checks()) // Make sure we're not doing last minute events, or early events. if(!pre_action_checks()) // Make sure we're not doing last minute events, or early events.
return return
log_debug("Game Master now starting action decision.") log_debug("Game Master now starting action decision.")
@@ -92,7 +92,7 @@
log_debug("[choice.name] was chosen by the Game Master, and is now being ran.") log_debug("[choice.name] was chosen by the Game Master, and is now being ran.")
run_action(choice) run_action(choice)
/datum/game_master/proc/run_action(var/datum/gm_action/action) /datum/game_master_old/proc/run_action(var/datum/gm_action/action)
action.set_up() action.set_up()
action.start() action.start()
action.announce() action.announce()
@@ -105,7 +105,7 @@
last_department_used = action.departments[1] last_department_used = action.departments[1]
/datum/game_master/proc/decide_best_action(var/list/most_active_departments) /datum/game_master_old/proc/decide_best_action(var/list/most_active_departments)
if(!most_active_departments.len) // Server's empty? if(!most_active_departments.len) // Server's empty?
log_debug("Game Master failed to find any active departments.") log_debug("Game Master failed to find any active departments.")
return list() return list()

View File

@@ -1,9 +1,9 @@
// Tell the game master that something dangerous happened, e.g. someone dying. // Tell the game master that something dangerous happened, e.g. someone dying.
/datum/game_master/proc/adjust_danger(var/amt) /datum/game_master_old/proc/adjust_danger(var/amt)
amt = amt * danger_modifier amt = amt * danger_modifier
danger = round( CLAMP(danger + amt, 0, 1000), 0.1) danger = round( CLAMP(danger + amt, 0, 1000), 0.1)
// Tell the game master that something interesting happened. // Tell the game master that something interesting happened.
/datum/game_master/proc/adjust_staleness(var/amt) /datum/game_master_old/proc/adjust_staleness(var/amt)
amt = amt * staleness_modifier amt = amt * staleness_modifier
staleness = round( CLAMP(staleness + amt, -50, 200), 0.1) staleness = round( CLAMP(staleness + amt, -50, 200), 0.1)

View File

@@ -21,3 +21,36 @@
if(assess_player_activity(L) >= cutoff) if(assess_player_activity(L) >= cutoff)
num++ num++
return num return num
// Gives a count of how many human mobs of a specific species are on the station.
// Note that `ignore_synths` makes this proc ignore posibrains and drones, but NOT cyborgs, as they are still the same species in-universe.
/datum/metric/proc/count_all_of_specific_species(species_name, ignore_synths = TRUE, cutoff = 75, respect_z = TRUE)
var/num = 0
for(var/mob/living/carbon/human/H in player_list)
if(respect_z && !(H.z in using_map.station_levels))
continue
if(ignore_synths && H.isSynthetic() && H.get_FBP_type() != FBP_CYBORG)
continue
if(H.species.name == species_name)
if(assess_player_activity(H) >= cutoff)
num++
return num
// Gives a count of how many FBPs of a specific type there are on the station.
/datum/metric/proc/count_all_FBPs_of_kind(desired_FBP_class, cutoff = 75, respect_z = TRUE)
var/num = 0
for(var/mob/living/carbon/human/H in player_list)
if(respect_z && !(H.z in using_map.station_levels))
continue
if(H.get_FBP_type() != desired_FBP_class)
continue
if(assess_player_activity(H) >= cutoff)
num++
return num
// Like above, but for all FBPs.
/datum/metric/proc/count_all_FBPs(desired_FBP_class, cutoff = 75, respect_z = TRUE)
var/num = count_all_FBPs_of_kind(FBP_CYBORG, cutoff, respect_z)
num += count_all_FBPs_of_kind(FBP_POSI, cutoff, respect_z)
num += count_all_FBPs_of_kind(FBP_DRONE, cutoff, respect_z)
return num

View File

@@ -81,6 +81,7 @@
callHook("death", list(src, gibbed)) callHook("death", list(src, gibbed))
if(mind) if(mind)
SSgame_master.adjust_danger(gibbed ? 40 : 20)
for(var/mob/observer/dead/O in mob_list) for(var/mob/observer/dead/O in mob_list)
if(O.client && O.client.is_preference_enabled(/datum/client_preference/show_dsay)) if(O.client && O.client.is_preference_enabled(/datum/client_preference/show_dsay))
to_chat(O, "<span class='deadsay'><b>[src]</b> has died in <b>[get_area(src)]</b>. [ghost_follow_link(src, O)] </span>") to_chat(O, "<span class='deadsay'><b>[src]</b> has died in <b>[get_area(src)]</b>. [ghost_follow_link(src, O)] </span>")

View File

@@ -1,3 +1,5 @@
GLOBAL_LIST_EMPTY(all_turbines)
/obj/machinery/power/generator /obj/machinery/power/generator
name = "thermoelectric generator" name = "thermoelectric generator"
desc = "It's a high efficiency thermoelectric generator." desc = "It's a high efficiency thermoelectric generator."
@@ -27,12 +29,14 @@
/obj/machinery/power/generator/Initialize() /obj/machinery/power/generator/Initialize()
soundloop = new(list(src), FALSE) soundloop = new(list(src), FALSE)
desc = initial(desc) + " Rated for [round(max_power/1000)] kW." desc = initial(desc) + " Rated for [round(max_power/1000)] kW."
GLOB.all_turbines += src
spawn(1) spawn(1)
reconnect() reconnect()
return ..() return ..()
/obj/machinery/power/generator/Destroy() /obj/machinery/power/generator/Destroy()
QDEL_NULL(soundloop) QDEL_NULL(soundloop)
GLOB.all_turbines -= src
return ..() return ..()
//generators connect in dir and reverse_dir(dir) directions //generators connect in dir and reverse_dir(dir) directions

View File

@@ -203,6 +203,8 @@
#include "code\controllers\subsystems\character_setup.dm" #include "code\controllers\subsystems\character_setup.dm"
#include "code\controllers\subsystems\circuits.dm" #include "code\controllers\subsystems\circuits.dm"
#include "code\controllers\subsystems\events.dm" #include "code\controllers\subsystems\events.dm"
#include "code\controllers\subsystems\events2.dm"
#include "code\controllers\subsystems\game_master.dm"
#include "code\controllers\subsystems\garbage.dm" #include "code\controllers\subsystems\garbage.dm"
#include "code\controllers\subsystems\holomaps.dm" #include "code\controllers\subsystems\holomaps.dm"
#include "code\controllers\subsystems\inactivity.dm" #include "code\controllers\subsystems\inactivity.dm"
@@ -1726,55 +1728,20 @@
#include "code\modules\food\kitchen\cooking_machines\fryer.dm" #include "code\modules\food\kitchen\cooking_machines\fryer.dm"
#include "code\modules\food\kitchen\cooking_machines\grill.dm" #include "code\modules\food\kitchen\cooking_machines\grill.dm"
#include "code\modules\food\kitchen\cooking_machines\oven.dm" #include "code\modules\food\kitchen\cooking_machines\oven.dm"
#include "code\modules\gamemaster\controller.dm"
#include "code\modules\gamemaster\defines.dm" #include "code\modules\gamemaster\defines.dm"
#include "code\modules\gamemaster\game_master.dm" #include "code\modules\gamemaster\event2\event.dm"
#include "code\modules\gamemaster\helpers.dm" #include "code\modules\gamemaster\event2\meta.dm"
#include "code\modules\gamemaster\actions\action.dm" #include "code\modules\gamemaster\event2\events\cargo\shipping_error.dm"
#include "code\modules\gamemaster\actions\atmos_leak.dm" #include "code\modules\gamemaster\event2\events\engineering\camera_damage.dm"
#include "code\modules\gamemaster\actions\blob.dm" #include "code\modules\gamemaster\event2\events\engineering\canister_leak.dm"
#include "code\modules\gamemaster\actions\brand_intelligence.dm" #include "code\modules\gamemaster\event2\events\engineering\grid_check.dm"
#include "code\modules\gamemaster\actions\camera_damage.dm" #include "code\modules\gamemaster\event2\events\engineering\spacevine.dm"
#include "code\modules\gamemaster\actions\canister_leak.dm" #include "code\modules\gamemaster\event2\events\everyone\comms_blackout.dm"
#include "code\modules\gamemaster\actions\carp_migration.dm" #include "code\modules\gamemaster\event2\events\everyone\solar_storm.dm"
#include "code\modules\gamemaster\actions\comms_blackout.dm" #include "code\modules\gamemaster\event2\events\legacy\legacy.dm"
#include "code\modules\gamemaster\actions\drill_announcement.dm" #include "code\modules\gamemaster\event2\events\security\drill_announcement.dm"
#include "code\modules\gamemaster\actions\dust.dm" #include "code\modules\gamemaster\event2\events\security\security_advisement.dm"
#include "code\modules\gamemaster\actions\electrical_storm.dm" #include "code\modules\gamemaster\event2\events\synthetic\ion_storm.dm"
#include "code\modules\gamemaster\actions\electrified_door.dm"
#include "code\modules\gamemaster\actions\gravity.dm"
#include "code\modules\gamemaster\actions\grid_check.dm"
#include "code\modules\gamemaster\actions\infestation.dm"
#include "code\modules\gamemaster\actions\ion_storm.dm"
#include "code\modules\gamemaster\actions\manifest_malfunction.dm"
#include "code\modules\gamemaster\actions\meteor_defense.dm"
#include "code\modules\gamemaster\actions\money_hacker.dm"
#include "code\modules\gamemaster\actions\money_lotto.dm"
#include "code\modules\gamemaster\actions\money_spam.dm"
#include "code\modules\gamemaster\actions\planet_weather_change.dm"
#include "code\modules\gamemaster\actions\prison_break.dm"
#include "code\modules\gamemaster\actions\radiation_storm.dm"
#include "code\modules\gamemaster\actions\random_antagonist.dm"
#include "code\modules\gamemaster\actions\rogue_drones.dm"
#include "code\modules\gamemaster\actions\security_advisement.dm"
#include "code\modules\gamemaster\actions\shipping_error.dm"
#include "code\modules\gamemaster\actions\solar_storm.dm"
#include "code\modules\gamemaster\actions\spacevine.dm"
#include "code\modules\gamemaster\actions\spider_infestation.dm"
#include "code\modules\gamemaster\actions\spontaneous_appendicitis.dm"
#include "code\modules\gamemaster\actions\station_fundraise.dm"
#include "code\modules\gamemaster\actions\stowaway.dm"
#include "code\modules\gamemaster\actions\supply_conversion.dm"
#include "code\modules\gamemaster\actions\supplyrequest.dm"
#include "code\modules\gamemaster\actions\surprise_carp_attack.dm"
#include "code\modules\gamemaster\actions\surprise_meteor.dm"
#include "code\modules\gamemaster\actions\swarmboarder.dm"
#include "code\modules\gamemaster\actions\viral_infection.dm"
#include "code\modules\gamemaster\actions\viral_outbreak.dm"
#include "code\modules\gamemaster\actions\wallrot.dm"
#include "code\modules\gamemaster\actions\waste_disposal.dm"
#include "code\modules\gamemaster\actions\window_break.dm"
#include "code\modules\gamemaster\actions\wormholes.dm"
#include "code\modules\games\cah.dm" #include "code\modules\games\cah.dm"
#include "code\modules\games\cah_black_cards.dm" #include "code\modules\games\cah_black_cards.dm"
#include "code\modules\games\cah_white_cards.dm" #include "code\modules\games\cah_white_cards.dm"
@@ -2861,7 +2828,7 @@
#include "code\ZAS\Zone.dm" #include "code\ZAS\Zone.dm"
#include "interface\interface.dm" #include "interface\interface.dm"
#include "interface\skin.dmf" #include "interface\skin.dmf"
#include "maps\southern_cross\southern_cross.dm" #include "maps\example\example.dm"
#include "maps\submaps\space_submaps\space.dm" #include "maps\submaps\space_submaps\space.dm"
#include "maps\submaps\surface_submaps\mountains\mountains.dm" #include "maps\submaps\surface_submaps\mountains\mountains.dm"
#include "maps\submaps\surface_submaps\mountains\mountains_areas.dm" #include "maps\submaps\surface_submaps\mountains\mountains_areas.dm"