diff --git a/code/__defines/misc.dm b/code/__defines/misc.dm
index 68954b0dc5..6a009677e1 100644
--- a/code/__defines/misc.dm
+++ b/code/__defines/misc.dm
@@ -167,3 +167,15 @@
#define ANTAG_HIDDEN "Hidden"
#define ANTAG_SHARED "Shared"
#define ANTAG_KNOWN "Known"
+
+// Job groups
+#define ROLE_COMMAND "command"
+#define ROLE_SECURITY "security"
+#define ROLE_ENGINEERING "engineering"
+#define ROLE_MEDICAL "medical"
+#define ROLE_RESEARCH "research"
+#define ROLE_CARGO "cargo"
+#define ROLE_CIVILIAN "civilian"
+#define ROLE_SYNTHETIC "synthetic"
+#define ROLE_UNKNOWN "unknown"
+#define ROLE_EVERYONE "everyone"
diff --git a/code/datums/wires/grid_checker.dm b/code/datums/wires/grid_checker.dm
new file mode 100644
index 0000000000..1e09be577e
--- /dev/null
+++ b/code/datums/wires/grid_checker.dm
@@ -0,0 +1,66 @@
+/datum/wires/grid_checker
+ holder_type = /obj/machinery/power/grid_checker
+ wire_count = 8
+
+var/const/GRID_CHECKER_WIRE_REBOOT = 1 // This wire causes the grid-check to end, if pulsed.
+var/const/GRID_CHECKER_WIRE_LOCKOUT = 2 // If cut or pulsed, locks the user out for half a minute.
+var/const/GRID_CHECKER_WIRE_ALLOW_MANUAL_1 = 4 // Needs to be cut for REBOOT to be possible.
+var/const/GRID_CHECKER_WIRE_ALLOW_MANUAL_2 = 8 // Needs to be cut for REBOOT to be possible.
+var/const/GRID_CHECKER_WIRE_ALLOW_MANUAL_3 = 16 // Needs to be cut for REBOOT to be possible.
+var/const/GRID_CHECKER_WIRE_SHOCK = 32 // Shocks the user if not wearing gloves.
+var/const/GRID_CHECKER_WIRE_NOTHING_1 = 64 // Does nothing, but makes it a bit harder.
+var/const/GRID_CHECKER_WIRE_NOTHING_2 = 128 // Does nothing, but makes it a bit harder.
+
+
+/datum/wires/grid_checker/CanUse(var/mob/living/L)
+ var/obj/machinery/power/grid_checker/G = holder
+ if(G.opened)
+ return TRUE
+ return FALSE
+
+
+/datum/wires/grid_checker/GetInteractWindow()
+ var/obj/machinery/power/grid_checker/G = holder
+ . += ..()
+ . += "The green light is [G.power_failing ? "off" : "on"].
"
+ . += "The red light is [G.wire_locked_out ? "on" : "off"].
"
+ . += "The blue light is [G.wire_allow_manual_1 && G.wire_allow_manual_2 && G.wire_allow_manual_3 ? "on" : "off"]."
+
+
+/datum/wires/grid_checker/UpdateCut(var/index, var/mended)
+ var/obj/machinery/power/grid_checker/G = holder
+ switch(index)
+ if(GRID_CHECKER_WIRE_LOCKOUT)
+ G.wire_locked_out = !mended
+ if(GRID_CHECKER_WIRE_ALLOW_MANUAL_1)
+ G.wire_allow_manual_1 = !mended
+ if(GRID_CHECKER_WIRE_ALLOW_MANUAL_2)
+ G.wire_allow_manual_2 = !mended
+ if(GRID_CHECKER_WIRE_ALLOW_MANUAL_3)
+ G.wire_allow_manual_3 = !mended
+ if(GRID_CHECKER_WIRE_SHOCK)
+ if(G.wire_locked_out)
+ return
+ G.shock(usr, 70)
+
+
+/datum/wires/grid_checker/UpdatePulsed(var/index)
+ var/obj/machinery/power/grid_checker/G = holder
+ switch(index)
+ if(GRID_CHECKER_WIRE_REBOOT)
+ if(G.wire_locked_out)
+ return
+
+ if(G.power_failing && G.wire_allow_manual_1 && G.wire_allow_manual_2 && G.wire_allow_manual_3)
+ G.end_power_failure(TRUE)
+ if(GRID_CHECKER_WIRE_LOCKOUT)
+ if(G.wire_locked_out)
+ return
+
+ G.wire_locked_out = TRUE
+ spawn(30 SECONDS)
+ G.wire_locked_out = FALSE
+ if(GRID_CHECKER_WIRE_SHOCK)
+ if(G.wire_locked_out)
+ return
+ G.shock(usr, 70)
\ No newline at end of file
diff --git a/code/game/objects/items/weapons/circuitboards/machinery/power.dm b/code/game/objects/items/weapons/circuitboards/machinery/power.dm
index 6e5667ebe5..57d8822df6 100644
--- a/code/game/objects/items/weapons/circuitboards/machinery/power.dm
+++ b/code/game/objects/items/weapons/circuitboards/machinery/power.dm
@@ -22,3 +22,10 @@
build_path = /obj/machinery/power/smes/batteryrack/makeshift
board_type = new /datum/frame/frame_types/machine
req_components = list(/obj/item/weapon/cell = 3)
+
+/obj/item/weapon/circuitboard/grid_checker
+ name = T_BOARD("power grid checker")
+ build_path = /obj/machinery/power/grid_checker
+ board_type = new /datum/frame/frame_types/machine
+ origin_tech = list(TECH_POWER = 4, TECH_ENGINEERING = 3)
+ req_components = list(/obj/item/weapon/stock_parts/capacitor = 3, /obj/item/stack/cable_coil = 10)
diff --git a/code/global.dm b/code/global.dm
index cd27fbc8ec..70171ac081 100644
--- a/code/global.dm
+++ b/code/global.dm
@@ -119,6 +119,7 @@ var/join_motd = null
var/datum/nanomanager/nanomanager = new() // NanoManager, the manager for Nano UIs.
var/datum/event_manager/event_manager = new() // Event Manager, the manager for events.
var/datum/game_master/game_master = new() // Game Master, an AI for choosing events.
+var/datum/metric/metric = new() // Metric datum, used to keep track of the round.
var/list/awaydestinations = list() // Away missions. A list of landmarks that the warpgate can take you to.
diff --git a/code/modules/gamemaster/actions/comms_blackout.dm b/code/modules/gamemaster/actions/comms_blackout.dm
index 77814c3f93..75359085ac 100644
--- a/code/modules/gamemaster/actions/comms_blackout.dm
+++ b/code/modules/gamemaster/actions/comms_blackout.dm
@@ -3,4 +3,7 @@
/datum/gm_action/comms_blackout
name = "communications blackout"
departments = list(ROLE_ENGINEERING, ROLE_EVERYONE)
- chaotic = 35
\ No newline at end of file
+ chaotic = 35
+
+/datum/gm_action/comms_blackout/get_weight()
+ return 50 + (metric.count_people_in_department(ROLE_ENGINEERING) * 40)
\ No newline at end of file
diff --git a/code/modules/gamemaster/actions/grid_check.dm b/code/modules/gamemaster/actions/grid_check.dm
index f46f0b162d..08c817fdf9 100644
--- a/code/modules/gamemaster/actions/grid_check.dm
+++ b/code/modules/gamemaster/actions/grid_check.dm
@@ -1,9 +1,22 @@
// New grid check event:
// Very similar to the old one, power goes out in most of the colony, 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.
+// 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/gm_action/grid_check
name = "grid check"
departments = list(ROLE_ENGINEERING, ROLE_EVERYONE)
- chaotic = 20
\ No newline at end of file
+ chaotic = 20
+
+/datum/gm_action/grid_check/get_weight()
+ return 50 + (metric.count_people_in_department(ROLE_ENGINEERING) * 30)
+
+/datum/gm_action/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.
+ for(var/obj/machinery/power/generator/engine in machines)
+ engine.power_spike()
+ break // Just one engine, please.
+ // 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.
\ No newline at end of file
diff --git a/code/modules/gamemaster/actions/waste_disposal.dm b/code/modules/gamemaster/actions/waste_disposal.dm
index 4edfba9a1e..e7ba856e78 100644
--- a/code/modules/gamemaster/actions/waste_disposal.dm
+++ b/code/modules/gamemaster/actions/waste_disposal.dm
@@ -3,4 +3,7 @@
/datum/gm_action/waste_disposal
name = "waste disposal"
departments = list(ROLE_CARGO)
- chaotic = 0
\ No newline at end of file
+ chaotic = 0
+
+/datum/gm_action/waste_disposal/get_weight()
+ return metric.count_people_in_department(ROLE_CARGO) * 50
\ No newline at end of file
diff --git a/code/modules/gamemaster/controller.dm b/code/modules/gamemaster/controller.dm
index abff64ea11..079c535164 100644
--- a/code/modules/gamemaster/controller.dm
+++ b/code/modules/gamemaster/controller.dm
@@ -11,30 +11,70 @@
var/HTML = "
Game Master AI"
- HTML += "Staleness: [staleness]
"
- HTML += "Danger: [danger]
"
+ HTML += "\[Toggle Time Restrictions\] | \
+ \[Toggle GM\] | \
+ \[Force Event Decision\]
"
+
+ HTML += "Status: [pre_action_checks() ? "Ready" : "Suppressed"]
"
+
+ HTML += "Staleness: [staleness] \[Adjust\]
"
+ HTML += "Danger: [danger] \[Adjust\]
"
HTML += "Actions available;
"
for(var/datum/gm_action/action in available_actions)
if(action.enabled == FALSE)
continue
- HTML += "[action.name] ([english_list(action.departments)])
"
+ HTML += "[action.name] ([english_list(action.departments)]) (weight: [action.get_weight()])
"
HTML += "
"
- HTML += "All living mobs activity: [assess_all_living_mobs()]
"
+ HTML += "All living mobs activity: [metric.assess_all_living_mobs()]%
"
+ HTML += "All ghost activity: [metric.assess_all_dead_mobs()]%
"
HTML += "
"
HTML += "Departmental activity;
"
- for(var/department in departments)
- var/number_of_people = count_people_in_department(department)
- HTML += " [department] : [assess_department(department)] / [number_of_people * 100]
"
+ for(var/department in metric.departments)
+ HTML += " [department] : [metric.assess_department(department)]%
"
HTML += "
"
HTML += "Activity of players;
"
for(var/mob/player in player_list)
- HTML += " [player] : [assess_player_activity(player)]
"
+ HTML += " [player] ([player.key]) : [metric.assess_player_activity(player)]%
"
HTML +=""
- user << browse(HTML, "window=log;size=400x450;border=1;can_resize=1;can_close=1;can_minimize=1")
\ No newline at end of file
+ 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.
\ No newline at end of file
diff --git a/code/modules/gamemaster/defines.dm b/code/modules/gamemaster/defines.dm
index 796eb014f8..2e486ee23b 100644
--- a/code/modules/gamemaster/defines.dm
+++ b/code/modules/gamemaster/defines.dm
@@ -1,10 +1 @@
-#define ROLE_COMMAND "command"
-#define ROLE_SECURITY "security"
-#define ROLE_ENGINEERING "engineering"
-#define ROLE_MEDICAL "medical"
-#define ROLE_RESEARCH "research"
-#define ROLE_CARGO "cargo"
-#define ROLE_CIVILIAN "civilian"
-#define ROLE_SYNTHETIC "synthetic"
-#define ROLE_UNKNOWN "unknown"
-#define ROLE_EVERYONE "everyone"
\ No newline at end of file
+#define EVENT_BASELINE_WEIGHT 200
\ No newline at end of file
diff --git a/code/modules/gamemaster/game_master.dm b/code/modules/gamemaster/game_master.dm
index e21b9631bf..180892f4c9 100644
--- a/code/modules/gamemaster/game_master.dm
+++ b/code/modules/gamemaster/game_master.dm
@@ -4,7 +4,8 @@
// the round.
/datum/game_master
- var/suspended = FALSE // 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/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.
@@ -12,31 +13,20 @@
var/staleness_modifier = 1 // Ditto. Higher numbers generally result in more events occuring in a round.
var/ticks_completed = 0 // Counts amount of ticks completed. Note that this ticks once a minute.
var/next_action = 0 // Minimum amount of time of nothingness until the GM can pick something again.
- var/departments = list( // List of departments the GM considers for choosing events for.
- ROLE_COMMAND,
- ROLE_SECURITY,
- ROLE_ENGINEERING,
- ROLE_MEDICAL,
- ROLE_RESEARCH,
- ROLE_CARGO,
- ROLE_CIVILIAN,
- ROLE_SYNTHETIC
- )
+ 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()
..()
available_actions = init_subtypes(/datum/gm_action)
-// var/actions = typesof(/datum/gm_actions)
-// for(var/type in actions)
-// available_actions.Add(new type)
/datum/game_master/proc/process()
- if(ticker && ticker.current_state == GAME_STATE_PLAYING)
+ if(ticker && ticker.current_state == GAME_STATE_PLAYING && !suspended)
adjust_staleness(1)
adjust_danger(-1)
ticks_completed++
- var/global_afk = assess_all_living_mobs()
+ var/global_afk = metric.assess_all_living_mobs()
global_afk -= 100
global_afk = abs(global_afk)
global_afk = round(global_afk / 100, 0.1)
@@ -46,19 +36,15 @@
log_debug("Game Master going to start something.")
start_action()
-/datum/game_master/proc/assess_all_living_mobs()
- var/num = 0
- for(var/mob/living/L in player_list) // Ghosts being AFK isn't that much of a concern.
- . += assess_player_activity(L)
- num++
- if(num)
- . = round(. / num, 0.1)
-
// This is run before committing to an action/event.
/datum/game_master/proc/pre_action_checks()
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.")
return FALSE
+ if(suspended)
+ return FALSE
+ if(ignore_time_restrictions)
+ return TRUE
// 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)
@@ -76,43 +62,21 @@
if(!pre_action_checks()) // Make sure we're not doing last minute events, or early events.
return
log_debug("Game Master now starting action decision.")
- var/list/best_actions = assess_round() // Checks the whole round for active people, and returns a list of the most activie departments.
- if(best_actions && best_actions.len)
- var/datum/gm_action/choice = pick(best_actions)
- if(choice)
-// log_debug("[choice.name] was chosen by the Game Master, and is now being ran.")
-// choice.set_up()
-// choice.start()
-// choice.annnounce()
- next_action = world.time + rand(15 MINUTES, 30 MINUTES)
-
-/datum/game_master/proc/assess_round()
- var/list/activity = list()
- for(var/department in departments)
- activity[department] = assess_department(department)
- log_debug("Assessing department [department]. They have activity of [activity[department]].")
-
- var/list/most_active_departments = list() // List of winners.
- var/highest_activity = null // Department who is leading in activity, if one exists.
- var/highest_number = 0 // Activity score needed to beat to be the most active department.
- for(var/i = 1, i <= 3, i++)
- log_debug("Doing [i]\th round of counting.")
- for(var/department in activity)
- if(activity[department] > highest_number && activity[department] > 0) // More active than the current highest department?
- highest_activity = department
- highest_number = activity[department]
-
- if(highest_activity) // Someone's a winner.
- most_active_departments.Add(highest_activity) // Add to the list of most active.
- activity.Remove(highest_activity) // Remove them from the other list so they don't win more than once.
- log_debug("[highest_activity] has won the [i]\th round of activity counting.")
- highest_activity = null // Now reset for the next round.
- highest_number = 0
- //todo: finish
+ var/list/most_active_departments = metric.assess_all_departments(3, list(last_department_used))
var/list/best_actions = decide_best_action(most_active_departments)
- return best_actions
- // By now, we should have a list of departments populated. The GM will prefer events tailored to these departments.
+ if(best_actions && best_actions.len)
+ var/list/weighted_actions = list()
+ for(var/datum/gm_action/action in best_actions)
+ 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.")
+ choice.set_up()
+ choice.start()
+ next_action = world.time + rand(15 MINUTES, 30 MINUTES)
+ last_department_used = choice.departments[1]
@@ -169,39 +133,4 @@
else
log_debug("Game Master failed to find a suitable event, something very wrong is going on.")
-// This checks a whole department's viability to receive an event.
-/datum/game_master/proc/assess_department(var/department)
- if(!department)
- return
- var/departmental_activitiy = 0
- for(var/mob/M in player_list)
- if(guess_department(M) != department) // Ignore people outside the department we're assessing.
- continue
- departmental_activitiy += assess_player_activity(M)
- return departmental_activitiy
-
-// This checks an individual player's activity level. People who have been afk for a few minutes aren't punished as much as those
-// who were afk for hours, as they're most likely gone for good.
-/datum/game_master/proc/assess_player_activity(var/mob/M)
- . = 100
- if(!M)
- . = 0
- return
-
- if(!M.mind || !M.client) // Logged out. They might come back but we can't do any meaningful assessments for now.
- . = 0
- return
-
- var/afk = M.client.is_afk(1 MINUTE)
- if(afk) // Deduct points based on length of AFK-ness.
- switch(afk) // One minute is equal to 600, for reference.
- if(1 MINUTE to 10 MINUTES) // People gone for this emough of time hopefully will come back soon.
- . -= round( (afk / 200), 1)
- // . -= 30
- if(10 MINUTES to 30 MINUTES)
- . -= round( (afk / 150), 1)
- // . -= 70
- if(30 MINUTES to INFINITY) // They're probably not coming back if it's been 30 minutes.
- . -= 100
- . = max(. , 0) // No negative numbers, or else people could drag other, non-afk players down.
diff --git a/code/modules/gamemaster/helpers.dm b/code/modules/gamemaster/helpers.dm
index a0972207a3..80fc133931 100644
--- a/code/modules/gamemaster/helpers.dm
+++ b/code/modules/gamemaster/helpers.dm
@@ -6,67 +6,4 @@
// Tell the game master that something interesting happened.
/datum/game_master/proc/adjust_staleness(var/amt)
amt = amt * staleness_modifier
- staleness = round( Clamp(staleness + amt, -50, 200), 0.1)
-
-// This proc tries to find the department of an arbitrary mob.
-/datum/game_master/proc/guess_department(var/mob/M)
- var/datum/data/record/R = find_general_record("name", M.real_name)
- . = ROLE_UNKNOWN
- if(R) // We found someone with a record.
- var/recorded_rank = R.fields["real_rank"]
- . = role_name_to_department(recorded_rank)
- if(. != ROLE_UNKNOWN) // We found the correct department, so we can stop now.
- return
-
- // They have a custom title, aren't crew, or someone deleted their record, so we need a fallback method.
- // Let's check the mind.
- if(M.mind)
- . = role_name_to_department(M.mind.assigned_role)
- if(. != ROLE_UNKNOWN)
- return
-
- // At this point, they don't have a mind, or for some reason assigned_role didn't work.
- if(ishuman(M))
- var/mob/living/carbon/human/H = M
- . = role_name_to_department(H.job)
- if(. != ROLE_UNKNOWN)
- return
-
- return ROLE_UNKNOWN // Welp.
-
-
-// Feed this proc the name of a job, and it will try to figure out what department they are apart of.
-/datum/game_master/proc/role_name_to_department(var/role_name)
- if(role_name in security_positions)
- return ROLE_SECURITY
-
- if(role_name in engineering_positions)
- return ROLE_ENGINEERING
-
- if(role_name in medical_positions)
- return ROLE_MEDICAL
-
- if(role_name in science_positions)
- return ROLE_RESEARCH
-
- if(role_name in cargo_positions)
- return ROLE_CARGO
-
- if(role_name in civilian_positions)
- return ROLE_CIVILIAN
-
- if(role_name in nonhuman_positions)
- return ROLE_SYNTHETIC
-
- if(role_name in command_positions) // We do command last, so that only the Captain and command secretaries get caught.
- return ROLE_COMMAND
-
- return ROLE_UNKNOWN
-
-/datum/game_master/proc/count_people_in_department(var/department)
- if(!department)
- return
- for(var/mob/M in player_list)
- if(guess_department(M) != department) // Ignore people outside the department we're counting.
- continue
- . += 1
\ No newline at end of file
+ staleness = round( Clamp(staleness + amt, -50, 200), 0.1)
\ No newline at end of file
diff --git a/code/modules/metric/activity.dm b/code/modules/metric/activity.dm
new file mode 100644
index 0000000000..370ae0eb2f
--- /dev/null
+++ b/code/modules/metric/activity.dm
@@ -0,0 +1,82 @@
+// This checks an individual player's activity level. People who have been afk for a few minutes aren't punished as much as those
+// who were afk for hours, as they're most likely gone for good.
+/datum/metric/proc/assess_player_activity(var/mob/M)
+ . = 100
+ if(!M)
+ . = 0
+ return
+
+ if(!M.mind || !M.client) // Logged out. They might come back but we can't do any meaningful assessments for now.
+ . = 0
+ return
+
+ var/afk = M.client.is_afk(1 MINUTE)
+ if(afk) // Deduct points based on length of AFK-ness.
+ switch(afk) // One minute is equal to 600, for reference.
+ if(1 MINUTE to 10 MINUTES) // People gone for this emough of time hopefully will come back soon.
+ . -= round( (afk / 200), 1)
+ if(10 MINUTES to 30 MINUTES)
+ . -= round( (afk / 150), 1)
+ if(30 MINUTES to INFINITY) // They're probably not coming back if it's been 30 minutes.
+ . -= 100
+ . = max(. , 0) // No negative numbers, or else people could drag other, non-afk players down.
+
+// This checks a whole department's collective activity.
+/datum/metric/proc/assess_department(var/department)
+ if(!department)
+ return
+ var/departmental_activity = 0
+ var/departmental_size = 0
+ for(var/mob/M in player_list)
+ if(guess_department(M) != department) // Ignore people outside the department we're assessing.
+ continue
+ departmental_activity += assess_player_activity(M)
+ departmental_size++
+ if(departmental_size)
+ departmental_activity = departmental_activity / departmental_size // Average it out.
+ return departmental_activity
+
+/datum/metric/proc/assess_all_departments(var/cutoff_number = 3, var/list/department_blacklist = list())
+ var/list/activity = list()
+ for(var/department in departments)
+ activity[department] = assess_department(department)
+ log_debug("Assessing department [department]. They have activity of [activity[department]].")
+
+ var/list/most_active_departments = list() // List of winners.
+ var/highest_activity = null // Department who is leading in activity, if one exists.
+ var/highest_number = 0 // Activity score needed to beat to be the most active department.
+ for(var/i = 1, i <= cutoff_number, i++)
+ log_debug("Doing [i]\th round of counting.")
+ for(var/department in activity)
+ if(department in department_blacklist) // Blacklisted?
+ continue
+ if(activity[department] > highest_number && activity[department] > 0) // More active than the current highest department?
+ highest_activity = department
+ highest_number = activity[department]
+
+ if(highest_activity) // Someone's a winner.
+ most_active_departments.Add(highest_activity) // Add to the list of most active.
+ activity.Remove(highest_activity) // Remove them from the other list so they don't win more than once.
+ log_debug("[highest_activity] has won the [i]\th round of activity counting.")
+ highest_activity = null // Now reset for the next round.
+ highest_number = 0
+ //todo: finish
+ return most_active_departments
+
+/datum/metric/proc/assess_all_living_mobs() // Living refers to the type, not the stat variable.
+ . = 0
+ var/num = 0
+ for(var/mob/living/L in player_list)
+ . += assess_player_activity(L)
+ num++
+ if(num)
+ . = round(. / num, 0.1)
+
+/datum/metric/proc/assess_all_dead_mobs() // Ditto.
+ . = 0
+ var/num = 0
+ for(var/mob/observer/dead/O in player_list)
+ . += assess_player_activity(O)
+ num++
+ if(num)
+ . = round(. / num, 0.1)
\ No newline at end of file
diff --git a/code/modules/metric/department.dm b/code/modules/metric/department.dm
new file mode 100644
index 0000000000..ef146de506
--- /dev/null
+++ b/code/modules/metric/department.dm
@@ -0,0 +1,72 @@
+
+// This proc tries to find the department of an arbitrary mob.
+/datum/metric/proc/guess_department(var/mob/M)
+ var/list/found_roles = list()
+ . = ROLE_UNKNOWN
+
+ // Records are usually the most reliable way to get what job someone is.
+ var/datum/data/record/R = find_general_record("name", M.real_name)
+ if(R) // We found someone with a record.
+ var/recorded_rank = R.fields["real_rank"]
+ found_roles = role_name_to_department(recorded_rank)
+ . = found_roles[1]
+ if(. != ROLE_UNKNOWN) // We found the correct department, so we can stop now.
+ return
+
+ // They have a custom title, aren't crew, or someone deleted their record, so we need a fallback method.
+ // Let's check the mind.
+ if(M.mind)
+ found_roles = role_name_to_department(M.mind.assigned_role)
+ . = found_roles[1]
+ if(. != ROLE_UNKNOWN)
+ return
+
+ // At this point, they don't have a mind, or for some reason assigned_role didn't work.
+ found_roles = role_name_to_department(M.job)
+ . = found_roles[1]
+ if(. != ROLE_UNKNOWN)
+ return
+
+ return ROLE_UNKNOWN // Welp.
+
+// Feed this proc the name of a job, and it will try to figure out what department they are apart of.
+// Note that this returns a list, as some jobs are in more than one department, like Command. The 'primary' department is the first
+// in the list, e.g. a HoS has Security as first, Command as second in the returned list.
+/datum/metric/proc/role_name_to_department(var/role_name)
+ var/list/result = list()
+
+ if(role_name in security_positions)
+ result += ROLE_SECURITY
+
+ if(role_name in engineering_positions)
+ result += ROLE_ENGINEERING
+
+ if(role_name in medical_positions)
+ result += ROLE_MEDICAL
+
+ if(role_name in science_positions)
+ result += ROLE_RESEARCH
+
+ if(role_name in cargo_positions)
+ result += ROLE_CARGO
+
+ if(role_name in civilian_positions)
+ result += ROLE_CIVILIAN
+
+ if(role_name in nonhuman_positions)
+ result += ROLE_SYNTHETIC
+
+ if(role_name in command_positions) // We do Command last, since we consider command to only be a primary department for hop/admin.
+ result += ROLE_COMMAND
+
+ if(!result.len) // No department was found.
+ result += ROLE_UNKNOWN
+ return result
+
+/datum/metric/proc/count_people_in_department(var/department)
+ if(!department)
+ return
+ for(var/mob/M in player_list)
+ if(guess_department(M) != department) // Ignore people outside the department we're counting.
+ continue
+ . += 1
\ No newline at end of file
diff --git a/code/modules/metric/metric.dm b/code/modules/metric/metric.dm
new file mode 100644
index 0000000000..1550b2c34e
--- /dev/null
+++ b/code/modules/metric/metric.dm
@@ -0,0 +1,15 @@
+// This is a global datum used to retrieve certain information about the round, such as activity of a department or a specific
+// player.
+
+/datum/metric
+ var/departments = list(
+ ROLE_COMMAND,
+ ROLE_SECURITY,
+ ROLE_ENGINEERING,
+ ROLE_MEDICAL,
+ ROLE_RESEARCH,
+ ROLE_CARGO,
+ ROLE_CIVILIAN,
+ ROLE_SYNTHETIC
+ )
+
diff --git a/code/modules/power/apc.dm b/code/modules/power/apc.dm
index 8096e1f20e..b577439394 100644
--- a/code/modules/power/apc.dm
+++ b/code/modules/power/apc.dm
@@ -68,6 +68,7 @@
var/cell_type = /obj/item/weapon/cell/apc
var/opened = 0 //0=closed, 1=opened, 2=cover removed
var/shorted = 0
+ var/grid_check = FALSE
var/lighting = 3
var/equipment = 3
var/environ = 3
@@ -796,7 +797,7 @@
return "[area.name] : [equipment]/[lighting]/[environ] ([lastused_equip+lastused_light+lastused_environ]) : [cell? cell.percent() : "N/C"] ([charging])"
/obj/machinery/power/apc/proc/update()
- if(operating && !shorted)
+ if(operating && !shorted && !grid_check)
area.power_light = (lighting > 1)
area.power_equip = (equipment > 1)
area.power_environ = (environ > 1)
@@ -1001,7 +1002,7 @@
if(debug)
log_debug("Status: [main_status] - Excess: [excess] - Last Equip: [lastused_equip] - Last Light: [lastused_light] - Longterm: [longtermpower]")
- if(cell && !shorted)
+ if(cell && !shorted && !grid_check)
// draw power from cell as before to power the area
var/cellused = min(cell.charge, CELLRATE * lastused_total) // clamp deduction to a max, amount left in cell
cell.use(cellused)
@@ -1196,7 +1197,7 @@ obj/machinery/power/apc/proc/autoset(var/val, var/on)
// overload the lights in this APC area
/obj/machinery/power/apc/proc/overload_lighting(var/chance = 100)
- if(/* !get_connection() || */ !operating || shorted)
+ if(/* !get_connection() || */ !operating || shorted || grid_check)
return
if( cell && cell.charge>=20)
cell.use(20);
@@ -1225,4 +1226,34 @@ obj/machinery/power/apc/proc/autoset(var/val, var/on)
update_icon()
return 1
+/obj/machinery/power/apc/overload(var/obj/machinery/power/source)
+ if(is_critical)
+ return
+
+ if(prob(30)) // Nothing happens.
+ return
+
+ if(prob(40)) // Lights blow.
+ overload_lighting()
+
+ if(prob(40)) // Spooky flickers.
+ for(var/obj/machinery/light/L in area)
+ L.flicker(20)
+
+ if(prob(25)) // Bluescreens.
+ emagged = 1
+ locked = 0
+ update_icon()
+
+ if(prob(25)) // Cell gets damaged.
+ if(cell)
+ cell.corrupt()
+
+ if(prob(10)) // Computers get broken.
+ for(var/obj/machinery/computer/comp in area)
+ comp.ex_act(3)
+
+ if(prob(5)) // APC completely ruined.
+ set_broken()
+
#undef APC_UPDATE_ICON_COOLDOWN
diff --git a/code/modules/power/generator.dm b/code/modules/power/generator.dm
index 48798c1fb5..92d749df0f 100644
--- a/code/modules/power/generator.dm
+++ b/code/modules/power/generator.dm
@@ -234,3 +234,8 @@
return
src.set_dir(turn(src.dir, -90))
+
+/obj/machinery/power/generator/power_spike()
+ if(effective_gen >= max_power / 2 && powernet) // Don't make a spike if we're not making a whole lot of power.
+ ..()
+
diff --git a/code/modules/power/grid_checker.dm b/code/modules/power/grid_checker.dm
new file mode 100644
index 0000000000..b52dc4a4d6
--- /dev/null
+++ b/code/modules/power/grid_checker.dm
@@ -0,0 +1,125 @@
+/obj/machinery/power/grid_checker
+ name = "grid checker"
+ desc = "A machine that reacts to unstable conditions in the powernet, by safely shutting everything down. Probably better \
+ than the alternative."
+ icon_state = "gridchecker_on"
+ circuit = /obj/item/weapon/circuitboard/grid_checker
+ var/power_failing = FALSE // Turns to TRUE when the grid check event is fired by the Game Master, or perhaps a cheeky antag.
+ // Wire stuff below.
+ var/datum/wires/grid_checker/wires
+ var/wire_locked_out = FALSE
+ var/wire_allow_manual_1 = FALSE
+ var/wire_allow_manual_2 = FALSE
+ var/wire_allow_manual_3 = FALSE
+ var/opened = FALSE
+
+/obj/machinery/power/grid_checker/New()
+ ..()
+ connect_to_network()
+ update_icon()
+ wires = new(src)
+ component_parts = list()
+ component_parts += new /obj/item/weapon/stock_parts/capacitor(src)
+ component_parts += new /obj/item/weapon/stock_parts/capacitor(src)
+ component_parts += new /obj/item/weapon/stock_parts/capacitor(src)
+ component_parts += new /obj/item/stack/cable_coil(src, 10)
+ RefreshParts()
+
+/obj/machinery/power/grid_checker/Destroy()
+ qdel(wires)
+ wires = null
+ ..()
+
+/obj/machinery/power/grid_checker/update_icon()
+ if(power_failing)
+ icon_state = "gridchecker_off"
+ set_light(2, 2, "#F86060")
+ else
+ icon_state = "gridchecker_on"
+ set_light(2, 2, "#A8B0F8")
+
+/obj/machinery/power/grid_checker/attackby(obj/item/W, mob/user)
+ if(!user)
+ return
+ if(istype(W, /obj/item/weapon/screwdriver))
+ default_deconstruction_screwdriver(user, W)
+ opened = !opened
+ else if(istype(W, /obj/item/weapon/crowbar))
+ default_deconstruction_crowbar(user, W)
+ else if(istype(W, /obj/item/device/multitool) || istype(W, /obj/item/weapon/wirecutters) )
+ attack_hand(user)
+
+/obj/machinery/power/grid_checker/attack_hand(mob/user)
+ if(!user)
+ return
+ add_fingerprint(user)
+ interact(user)
+
+/obj/machinery/power/grid_checker/interact(mob/user)
+ if(!user)
+ return
+
+ if(opened)
+ wires.Interact(user)
+
+ return ui_interact(user)
+
+/obj/machinery/power/grid_checker/proc/power_failure(var/announce = TRUE)
+ if(announce)
+ command_announcement.Announce("Abnormal activity detected in [station_name()]'s powernet. As a precautionary measure, \
+ the colony's power will be shut off for an indeterminate duration while the powernet monitor restarts automatically, or \
+ when Engineering can manually resolve the issue.",
+ "Critical Power Failure",
+ new_sound = 'sound/AI/poweroff.ogg')
+ power_failing = TRUE
+ if(powernet)
+ for(var/obj/machinery/power/terminal/T in powernet.nodes) // SMESes that are "downstream" of the powernet.
+
+ if(istype(T.master, /obj/machinery/power/apc))
+ var/obj/machinery/power/apc/A = T.master
+ if(A.is_critical)
+ continue
+ A.grid_check = TRUE
+
+ for(var/obj/machinery/power/smes/smes in powernet.nodes) // These are "upstream"
+ smes.grid_check = TRUE
+/*
+ smes.last_charge = smes.charge
+ smes.last_output_attempt = smes.output_attempt
+ smes.last_input_attempt = smes.input_attempt
+ smes.charge = 0
+ smes.inputting(FALSE)
+ smes.outputting(FALSE)
+ smes.update_icon()
+ smes.power_change()
+*/
+ update_icon()
+
+ spawn(rand(4 MINUTES, 10 MINUTES) )
+ if(power_failing) // Check to see if engineering didn't beat us to it.
+ end_power_failure(TRUE)
+
+/obj/machinery/power/grid_checker/proc/end_power_failure(var/announce = TRUE)
+ if(announce)
+ command_announcement.Announce("Power has been restored to [station_name()]. We apologize for the inconvenience.",
+ "Power Systems Nominal",
+ new_sound = 'sound/AI/poweron.ogg')
+ power_failing = FALSE
+ update_icon()
+
+ for(var/obj/machinery/power/terminal/T in powernet.nodes)
+ if(istype(T.master, /obj/machinery/power/apc))
+ var/obj/machinery/power/apc/A = T.master
+ if(A.is_critical)
+ continue
+ A.grid_check = FALSE
+
+ for(var/obj/machinery/power/smes/smes in powernet.nodes) // These are "upstream"
+ smes.grid_check = FALSE
+ /*
+ smes.charge = smes.last_charge
+ smes.output_attempt = smes.last_output_attempt
+ smes.input_attempt = smes.last_input_attempt
+ smes.update_icon()
+ smes.power_change()
+ */
\ No newline at end of file
diff --git a/code/modules/power/power.dm b/code/modules/power/power.dm
index 4f34cd7eff..b8beea238c 100644
--- a/code/modules/power/power.dm
+++ b/code/modules/power/power.dm
@@ -138,6 +138,29 @@
..()
return
+// Used for power spikes by the engine, has specific effects on different machines.
+/obj/machinery/power/proc/overload(var/obj/machinery/power/source)
+ return
+
+/obj/machinery/power/proc/power_spike()
+ var/obj/machinery/power/grid_checker/G = locate() in powernet.nodes
+ if(G) // If we found a grid checker, then all is well.
+ G.power_failure(prob(30))
+ else // Otherwise lets break some stuff.
+ spawn(1)
+ command_announcement.Announce("Dangerous power spike detected in the power network. Please check machinery \
+ for electrical damage.",
+ "Critical Power Overload")
+ var/i = 0
+ var/limit = rand(30, 50)
+ for(var/obj/machinery/power/P in powernet.nodes)
+ P.overload(src)
+ i++
+ if(i % 5)
+ sleep(1)
+ if(i >= limit)
+ break
+
///////////////////////////////////////////
// Powernet handling helpers
//////////////////////////////////////////
diff --git a/code/modules/power/smes.dm b/code/modules/power/smes.dm
index 9fc0f74290..bf165f0916 100644
--- a/code/modules/power/smes.dm
+++ b/code/modules/power/smes.dm
@@ -44,6 +44,7 @@
var/building_terminal = 0 //Suggestions about how to avoid clickspam building several terminals accepted!
var/obj/machinery/power/terminal/terminal = null
var/should_be_mapped = 0 // If this is set to 0 it will send out warning on New()
+ var/grid_check = FALSE // If true, suspends all I/O.
/obj/machinery/power/smes/drain_power(var/drain_check, var/surge, var/amount = 0)
@@ -124,7 +125,7 @@
var/last_onln = outputting
//inputting
- if(input_attempt && (!input_pulsed && !input_cut))
+ if(input_attempt && (!input_pulsed && !input_cut) && !grid_check)
var/target_load = min((capacity-charge)/SMESRATE, input_level) // charge at set rate, limited to spare capacity
var/actual_load = draw_power(target_load) // add the load to the terminal side network
charge += actual_load * SMESRATE // increase the charge
@@ -137,7 +138,7 @@
inputting = 0
//outputting
- if(outputting && (!output_pulsed && !output_cut))
+ if(outputting && (!output_pulsed && !output_cut) && !grid_check)
output_used = min( charge/SMESRATE, output_level) //limit output to that stored
charge -= output_used*SMESRATE // reduce the storage (may be recovered in /restore() if excessive)
@@ -420,6 +421,11 @@
update_icon()
..()
+/obj/machinery/power/smes/overload(var/obj/machinery/power/source) // This propagates the power spike down the powernet.
+ if(istype(source, /obj/machinery/power/smes)) // Prevent infinite loops if two SMESes are hooked up to each other.
+ return
+ power_spike()
+
/obj/machinery/power/smes/magical
name = "magical power storage unit"
diff --git a/code/modules/power/terminal.dm b/code/modules/power/terminal.dm
index 3636c3acfa..a1f2fbf030 100644
--- a/code/modules/power/terminal.dm
+++ b/code/modules/power/terminal.dm
@@ -37,3 +37,7 @@
// Powernet rebuilds need this to work properly.
/obj/machinery/power/terminal/process()
return 1
+
+/obj/machinery/power/terminal/overload(var/obj/machinery/power/source)
+ if(master)
+ master.overload(source)
diff --git a/code/modules/research/designs.dm b/code/modules/research/designs.dm
index bbb54d7f7e..a4807c2332 100644
--- a/code/modules/research/designs.dm
+++ b/code/modules/research/designs.dm
@@ -1038,6 +1038,14 @@ CIRCUITS BELOW
build_path = /obj/item/weapon/circuitboard/smes
sort_string = "JBABB"
+/datum/design/circuit/grid_checker
+ name = "power grid checker"
+ desc = "Allows for the construction of circuit boards used to build a grid checker."
+ id = "grid_checker"
+ req_tech = list(TECH_POWER = 4, TECH_ENGINEERING = 3)
+ build_path = /obj/item/weapon/circuitboard/grid_checker
+ sort_string = "JBABC"
+
/datum/design/circuit/gas_heater
name = "gas heating system"
id = "gasheater"
diff --git a/icons/obj/power.dmi b/icons/obj/power.dmi
index 14878d4dd0..fe67ea02af 100644
Binary files a/icons/obj/power.dmi and b/icons/obj/power.dmi differ
diff --git a/polaris.dme b/polaris.dme
index 8205a0bf3d..ed8f7faa84 100644
--- a/polaris.dme
+++ b/polaris.dme
@@ -253,6 +253,7 @@
#include "code\datums\wires\autolathe.dm"
#include "code\datums\wires\camera.dm"
#include "code\datums\wires\explosive.dm"
+#include "code\datums\wires\grid_checker.dm"
#include "code\datums\wires\particle_accelerator.dm"
#include "code\datums\wires\radio.dm"
#include "code\datums\wires\robot.dm"
@@ -1409,6 +1410,9 @@
#include "code\modules\materials\material_sheets.dm"
#include "code\modules\materials\material_synth.dm"
#include "code\modules\materials\materials.dm"
+#include "code\modules\metric\activity.dm"
+#include "code\modules\metric\department.dm"
+#include "code\modules\metric\metric.dm"
#include "code\modules\mining\abandonedcrates.dm"
#include "code\modules\mining\alloys.dm"
#include "code\modules\mining\coins.dm"
@@ -1762,6 +1766,7 @@
#include "code\modules\power\generator.dm"
#include "code\modules\power\generator_type2.dm"
#include "code\modules\power\gravitygenerator.dm"
+#include "code\modules\power\grid_checker.dm"
#include "code\modules\power\lighting.dm"
#include "code\modules\power\port_gen.dm"
#include "code\modules\power\power.dm"