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"