Cleans Up WIP Event System + New Grid Check For It

Separates the 'count and assess everything' stuff to it's own datum, called the metric datum, which I plan to add on to in the future to make counting and metrics easier.
Makes decision process a bit more weight-based, will probably continue tweaking later.
Makes the admin debug UI have links to change settings easily.
Adds replacement for grid check event, which works similar to the old one, but is now based on a physical machine in the game world, that Engineering can hack to make the event end faster, if so desired.  Note that the machine is not mapped in, and won't be mapped in until the event system is ready for launch.
Adds grid_check variables to SMESes and APCs to make them stop doing work without draining the battery.
Grid checks in the new system are caused by a "power spike" which originates from the engine and will cause bad things, should no grid checker machine be connected to the power-net.  These power spikes occur when the GM decides that a grid check is a good event to have.
The grid checker can be built and deconstructed using the standard machine construction methods.
This commit is contained in:
Neerti
2016-10-05 21:40:07 -04:00
parent 01328c5a03
commit 337ef499a1
23 changed files with 564 additions and 186 deletions

View File

@@ -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"

View File

@@ -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"].<br>"
. += "The red light is [G.wire_locked_out ? "on" : "off"].<br>"
. += "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)

View File

@@ -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)

View File

@@ -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.

View File

@@ -4,3 +4,6 @@
name = "communications blackout"
departments = list(ROLE_ENGINEERING, ROLE_EVERYONE)
chaotic = 35
/datum/gm_action/comms_blackout/get_weight()
return 50 + (metric.count_people_in_department(ROLE_ENGINEERING) * 40)

View File

@@ -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
/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.

View File

@@ -4,3 +4,6 @@
name = "waste disposal"
departments = list(ROLE_CARGO)
chaotic = 0
/datum/gm_action/waste_disposal/get_weight()
return metric.count_people_in_department(ROLE_CARGO) * 50

View File

@@ -11,30 +11,70 @@
var/HTML = "<html><head><title>Game Master AI</title></head><body>"
HTML += "Staleness: [staleness]<br>"
HTML += "Danger: [danger]<br><br>"
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)])<br>"
HTML += "[action.name] ([english_list(action.departments)]) (weight: [action.get_weight()])<br>"
HTML += "<br>"
HTML += "All living mobs activity: [assess_all_living_mobs()]<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 departments)
var/number_of_people = count_people_in_department(department)
HTML += " [department] : [assess_department(department)] / [number_of_people * 100]<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] : [assess_player_activity(player)]<br>"
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.

View File

@@ -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"
#define EVENT_BASELINE_WEIGHT 200

View File

@@ -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.

View File

@@ -7,66 +7,3 @@
/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

View File

@@ -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)

View File

@@ -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

View File

@@ -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
)

View File

@@ -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

View File

@@ -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.
..()

View File

@@ -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()
*/

View File

@@ -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
//////////////////////////////////////////

View File

@@ -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"

View File

@@ -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)

View File

@@ -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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -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"