diff --git a/code/_helpers/turfs.dm b/code/_helpers/turfs.dm index 1926246856..8a67d877e9 100644 --- a/code/_helpers/turfs.dm +++ b/code/_helpers/turfs.dm @@ -38,4 +38,18 @@ var/pressure = environment ? environment.return_pressure() : 0 if(pressure < SOUND_MINIMUM_PRESSURE) return TRUE - return FALSE \ No newline at end of file + return FALSE + +// Picks a turf that is clearance tiles away from the map edge given by dir, on z-level Z +/proc/pick_random_edge_turf(var/dir, var/Z, var/clearance = TRANSITIONEDGE + 1) + if(!dir) + return + switch(dir) + if(NORTH) + return locate(rand(clearance, world.maxx - clearance), world.maxy - clearance, Z) + if(SOUTH) + return locate(rand(clearance, world.maxx - clearance), clearance, Z) + if(EAST) + return locate(world.maxx - clearance, rand(clearance, world.maxy - clearance), Z) + if(WEST) + return locate(clearance, rand(clearance, world.maxy - clearance), Z) \ No newline at end of file diff --git a/code/controllers/subsystems/events2.dm b/code/controllers/subsystems/events2.dm index bb23dd6073..c7b5faabfb 100644 --- a/code/controllers/subsystems/events2.dm +++ b/code/controllers/subsystems/events2.dm @@ -28,8 +28,10 @@ SUBSYSTEM_DEF(event_ticker) event_started(E) /datum/controller/subsystem/event_ticker/proc/event_started(datum/event2/event/E) + log_debug("Event [E.type] is now being ran.") active_events += E /datum/controller/subsystem/event_ticker/proc/event_finished(datum/event2/event/E) + log_debug("Event [E.type] has finished.") active_events -= E finished_events += E \ No newline at end of file diff --git a/code/controllers/subsystems/game_master.dm b/code/controllers/subsystems/game_master.dm index d043135067..7c402e14b5 100644 --- a/code/controllers/subsystems/game_master.dm +++ b/code/controllers/subsystems/game_master.dm @@ -69,6 +69,19 @@ SUBSYSTEM_DEF(game_master) /datum/controller/subsystem/game_master/proc/run_event(datum/event2/meta/chosen_event) var/datum/event2/event/E = chosen_event.make_event() + + chosen_event.times_ran++ + + if(!chosen_event.reusable) + // Disable this event, so it only gets picked once. + chosen_event.enabled = FALSE + if(chosen_event.event_class) + // Disable similar events, too. + for(var/M in available_events) + var/datum/event2/meta/meta = M + if(meta.event_class == chosen_event.event_class) + meta.enabled = FALSE + SSevent_ticker.event_started(E) adjust_danger(chosen_event.chaos) adjust_staleness(-(10 + chosen_event.chaos)) // More chaotic events reduce staleness more, e.g. a 25 chaos event will reduce it by 35. @@ -171,7 +184,11 @@ SUBSYSTEM_DEF(game_master) for(var/E in best_events) var/datum/event2/meta/event = E - weighted_events[event] = event.get_weight() + var/weight = event.get_weight() + if(weight <= 0) + continue + weighted_events[event] = weight + log_game_master("Filtered down to [weighted_events.len] choice\s.") var/datum/event2/meta/choice = pickweight(weighted_events) @@ -237,10 +254,10 @@ SUBSYSTEM_DEF(game_master) . += event -// The `old_like` game master tries to act like the old system, choosing events without any specific goals. +// The `classic` game master tries to act like the old system, choosing events without any specific goals. // * Has no goals, and instead operates purely off of the weights of the events it has. // * Does not react to danger at all. -/datum/game_master/old_like/choose_event() +/datum/game_master/classic/choose_event() var/list/weighted_events = list() for(var/E in ticker.available_events) var/datum/event2/meta/event = E @@ -377,7 +394,8 @@ SUBSYSTEM_DEF(game_master) dat += "Event Name" dat += "Involved Departments" dat += "Chaos" - dat += "Current Weight" + dat += "Chaotic Threshold" + dat += "Weight" dat += "Buttons" dat += "" @@ -390,6 +408,7 @@ SUBSYSTEM_DEF(game_master) dat += "[event.name]" dat += "[english_list(event.departments)]" dat += "[event.chaos]" + dat += "[event.chaotic_threshold]" dat += "[event.get_weight()]" dat += "[href(event, list("force" = 1), "\[Force\]")] [href(event, list("toggle" = 1), "\[Toggle\]")]" dat += "" diff --git a/code/modules/blob2/blobs/base_blob.dm b/code/modules/blob2/blobs/base_blob.dm index 767cf4c783..02a7827825 100644 --- a/code/modules/blob2/blobs/base_blob.dm +++ b/code/modules/blob2/blobs/base_blob.dm @@ -1,4 +1,4 @@ -var/list/blobs = list() +GLOBAL_LIST_EMPTY(all_blobs) /obj/structure/blob name = "blob" @@ -18,21 +18,21 @@ var/list/blobs = list() var/mob/observer/blob/overmind = null var/base_name = "blob" // The name that gets appended along with the blob_type's name. -/obj/structure/blob/New(var/newloc, var/new_overmind) - ..(newloc) +/obj/structure/blob/Initialize(newloc, new_overmind) if(new_overmind) overmind = new_overmind update_icon() if(!integrity) integrity = max_integrity set_dir(pick(cardinal)) - blobs += src + GLOB.all_blobs += src consume_tile() + return ..() /obj/structure/blob/Destroy() playsound(src.loc, 'sound/effects/splat.ogg', 50, 1) //Expand() is no longer broken, no check necessary. - blobs -= src + GLOB.all_blobs -= src overmind = null return ..() @@ -112,7 +112,7 @@ var/list/blobs = list() if(overmind) expand_probablity *= overmind.blob_type.spread_modifier if(overmind.blob_type.slow_spread_with_size) - expand_probablity /= (blobs.len / 10) + expand_probablity /= (GLOB.all_blobs.len / 10) if(distance <= expand_range) var/can_expand = TRUE diff --git a/code/modules/blob2/blobs/core.dm b/code/modules/blob2/blobs/core.dm index f43ec7ad6f..84092ac566 100644 --- a/code/modules/blob2/blobs/core.dm +++ b/code/modules/blob2/blobs/core.dm @@ -12,6 +12,7 @@ var/list/blob_cores = list() health_regen = 0 //we regen in Life() instead of when pulsed var/datum/blob_type/desired_blob_type = null // If this is set, the core always creates an overmind possessing this blob type. var/difficulty_threshold = null // Otherwise if this is set, it picks a random blob_type that is equal or lower in difficulty. + var/difficulty_floor = null // Related to the above var, acts as a floor value to the above, inclusive. var/core_regen = 2 var/overmind_get_delay = 0 //we don't want to constantly try to find an overmind, this var tracks when we'll try to get an overmind again var/resource_delay = 0 @@ -23,14 +24,18 @@ var/list/blob_cores = list() ai_controlled = FALSE // Spawn these if you want a semi-random blob. +// Can give a random easy blob. /obj/structure/blob/core/random_easy difficulty_threshold = BLOB_DIFFICULTY_EASY +// Can give a random easy or medium blob. /obj/structure/blob/core/random_medium difficulty_threshold = BLOB_DIFFICULTY_MEDIUM +// Can give a random medium or hard blob. /obj/structure/blob/core/random_hard difficulty_threshold = BLOB_DIFFICULTY_HARD + difficulty_floor = BLOB_DIFFICULTY_MEDIUM // Spawn these if you want a specific blob. /obj/structure/blob/core/blazing_oil @@ -81,8 +86,8 @@ var/list/blob_cores = list() /obj/structure/blob/core/classic desired_blob_type = /datum/blob_type/classic -/obj/structure/blob/core/New(var/newloc, var/client/new_overmind = null, new_rate = 2, placed = 0) - ..(newloc) +/obj/structure/blob/core/Initialize(newloc, client/new_overmind = null, new_rate = 2, placed = 0) + . = ..(newloc) blob_cores += src START_PROCESSING(SSobj, src) update_icon() //so it atleast appears @@ -180,7 +185,9 @@ var/list/blob_cores = list() var/list/valid_types = list() for(var/thing in subtypesof(/datum/blob_type)) var/datum/blob_type/BT = thing - if(initial(BT.difficulty) > difficulty_threshold) + if(initial(BT.difficulty) > difficulty_threshold) // Too hard. + continue + if(initial(BT.difficulty) < difficulty_floor) // Too easy. continue valid_types += BT return pick(valid_types) \ No newline at end of file diff --git a/code/modules/blob2/blobs/resource.dm b/code/modules/blob2/blobs/resource.dm index 189abfd520..0cd3f06f10 100644 --- a/code/modules/blob2/blobs/resource.dm +++ b/code/modules/blob2/blobs/resource.dm @@ -9,10 +9,10 @@ var/resource_delay = 0 var/resource_cooldown = 4 SECONDS -/obj/structure/blob/resource/New(var/newloc, var/new_overmind) - ..(newloc, new_overmind) +/obj/structure/blob/resource/Initialize(newloc, new_overmind) if(overmind) overmind.resource_blobs += src + return ..() /obj/structure/blob/resource/Destroy() if(overmind) diff --git a/code/modules/blob2/overmind/overmind.dm b/code/modules/blob2/overmind/overmind.dm index 8a628d6dca..ffcd347dd8 100644 --- a/code/modules/blob2/overmind/overmind.dm +++ b/code/modules/blob2/overmind/overmind.dm @@ -23,7 +23,7 @@ var/list/overminds = list() var/ai_controlled = TRUE var/auto_pilot = FALSE // If true, and if a client is attached, the AI routine will continue running. -/mob/observer/blob/New(var/newloc, pre_placed = 0, starting_points = 60, desired_blob_type = null) +/mob/observer/blob/Initialize(newloc, pre_placed = 0, starting_points = 60, desired_blob_type = null) blob_points = starting_points if(pre_placed) //we already have a core! placed = 1 @@ -41,10 +41,10 @@ var/list/overminds = list() if(blob_core) blob_core.update_icon() - ..(newloc) + return ..(newloc) /mob/observer/blob/Destroy() - for(var/BL in blobs) + for(var/BL in GLOB.all_blobs) var/obj/structure/blob/B = BL if(B && B.overmind == src) B.overmind = null @@ -65,7 +65,7 @@ var/list/overminds = list() if(blob_core) stat(null, "Core Health: [blob_core.integrity]") stat(null, "Power Stored: [blob_points]/[max_blob_points]") - stat(null, "Total Blobs: [blobs.len]") + stat(null, "Total Blobs: [GLOB.all_blobs.len]") /mob/observer/blob/Move(NewLoc, Dir = 0) if(placed) diff --git a/code/modules/blob2/overmind/powers.dm b/code/modules/blob2/overmind/powers.dm index 360bfbb10b..f5727e94bf 100644 --- a/code/modules/blob2/overmind/powers.dm +++ b/code/modules/blob2/overmind/powers.dm @@ -22,6 +22,9 @@ to_chat(src, "There is no blob here!") return + if(B.overmind != src) + to_chat(src, span("warning", "This blob isn't controlled by you.")) + if(!istype(B, /obj/structure/blob/normal)) to_chat(src, "Unable to use this blob, find a normal one.") return @@ -66,7 +69,7 @@ return FALSE var/obj/structure/blob/B = null - var/list/potential_blobs = blobs.Copy() + var/list/potential_blobs = GLOB.all_blobs.Copy() while(potential_blobs.len) var/obj/structure/blob/temp = pick(potential_blobs) if(!(locate(/obj/structure/blob/node) in range(temp, BLOB_NODE_PULSE_RANGE) ) && !(locate(/obj/structure/blob/core) in range(temp, BLOB_CORE_PULSE_RANGE) )) @@ -77,6 +80,8 @@ potential_blobs -= temp // Don't take up the core's shield spot. else if(!istype(temp, /obj/structure/blob/normal)) potential_blobs -= temp // Not a normal blob. + else if(temp.overmind != src) + potential_blobs -= temp // Not our blob. else B = temp break @@ -107,7 +112,7 @@ return FALSE var/obj/structure/blob/B = null - var/list/potential_blobs = blobs.Copy() + var/list/potential_blobs = GLOB.all_blobs.Copy() while(potential_blobs.len) var/obj/structure/blob/temp = pick(potential_blobs) if(!(locate(/obj/structure/blob/node) in range(temp, BLOB_NODE_PULSE_RANGE) ) && !(locate(/obj/structure/blob/core) in range(temp, BLOB_CORE_PULSE_RANGE) )) @@ -118,6 +123,8 @@ potential_blobs -= temp // Don't take up the core's shield spot. else if(!istype(temp, /obj/structure/blob/normal)) potential_blobs -= temp // Not a normal blob. + else if(temp.overmind != src) + potential_blobs -= temp // Not our blob. else B = temp break @@ -149,7 +156,7 @@ return FALSE var/obj/structure/blob/B = null - var/list/potential_blobs = blobs.Copy() + var/list/potential_blobs = GLOB.all_blobs.Copy() while(potential_blobs.len) var/obj/structure/blob/temp = pick(potential_blobs) if(locate(/obj/structure/blob/node) in range(temp, 5) ) @@ -158,6 +165,8 @@ potential_blobs -= temp else if(!istype(temp, /obj/structure/blob/normal)) potential_blobs -= temp + else if(temp.overmind != src) + potential_blobs -= temp // Not our blob. else B = temp break @@ -184,7 +193,7 @@ other_T = get_step(T, direction) if(other_T) B = locate(/obj/structure/blob) in other_T - if(B) + if(B && B.overmind == src) break if(!B) @@ -216,7 +225,7 @@ for(var/direction in cardinal) var/turf/T = get_step(L, direction) B = locate(/obj/structure/blob) in T - if(B) + if(B && B.overmind == src) break if(!B) continue diff --git a/code/modules/gamemaster/event2/event.dm b/code/modules/gamemaster/event2/event.dm index 7fb6f54f29..3e1f8064da 100644 --- a/code/modules/gamemaster/event2/event.dm +++ b/code/modules/gamemaster/event2/event.dm @@ -65,6 +65,58 @@ This allows for events that have their announcement happen after the end itself. /datum/event2/event/proc/get_location_z_levels() return using_map.station_levels +// Returns a list of empty turfs in the same area. +/datum/event2/event/proc/find_random_turfs(minimum_free_space = 5, ignore_occupancy = FALSE) + // In the future it might be better to have areas themselves have a 'dont put event stuff here' var instead. + var/list/excluded_areas = list( + /area/submap, + /area/shuttle, + /area/crew_quarters, + /area/holodeck, + /area/engineering/engine_room + ) + var/list/area/grand_list_of_areas = get_station_areas(excluded_areas) + if(!LAZYLEN(grand_list_of_areas)) + return list() + for(var/i = 1 to 10) + var/area/A = pick(grand_list_of_areas) + if(!ignore_occupancy && is_area_occupied(A)) + continue // Occupied. + var/list/turfs = list() + for(var/turf/T in A) + if(turf_clear(T)) + turfs += T + if(turfs.len < minimum_free_space) + continue // Not enough free space. + return turfs + return list() + + +/* + var/list/area/grand_list_of_areas = get_station_areas(excluded) + if(grand_list_of_areas.len) + for(var/i in 1 to 10) + var/area/A = pick(grand_list_of_areas) + if(is_area_occupied(A)) + log_debug("Blob infestation event: Rejected [A] because it is occupied.") + continue + var/list/turfs = list() + for(var/turf/simulated/floor/F in A) + if(turf_clear(F)) + turfs += F + if(turfs.len < 5 + number_of_blobs) + log_debug("Blob infestation event: Rejected [A] because it has too little clear turfs.") + continue + open_turfs = turfs.Copy() + target_area = A + log_debug("Blob infestation event: Going to place blob at [A].") + break + + if(!open_turfs.len) + log_debug("Blob infestation event: Giving up after too many failures to pick a blob spot.") + abort() +*/ + // Starts the event. /datum/event2/event/proc/execute() time_started = world.time diff --git a/code/modules/gamemaster/event2/events/cargo/shipping_error.dm b/code/modules/gamemaster/event2/events/cargo/shipping_error.dm index 696d228f99..507270df21 100644 --- a/code/modules/gamemaster/event2/events/cargo/shipping_error.dm +++ b/code/modules/gamemaster/event2/events/cargo/shipping_error.dm @@ -6,7 +6,7 @@ event_type = /datum/event2/event/shipping_error /datum/event2/meta/shipping_error/get_weight() - return metric.count_people_in_department(DEPARTMENT_CARGO) * 40 + return (metric.count_people_with_job(/datum/job/cargo_tech) + metric.count_people_with_job(/datum/job/qm)) * 30 /datum/event2/event/shipping_error/start() var/datum/supply_order/O = new /datum/supply_order() diff --git a/code/modules/gamemaster/event2/events/engineering/blob.dm b/code/modules/gamemaster/event2/events/engineering/blob.dm index 4a431281c8..badb260114 100644 --- a/code/modules/gamemaster/event2/events/engineering/blob.dm +++ b/code/modules/gamemaster/event2/events/engineering/blob.dm @@ -1,67 +1,182 @@ /datum/event2/meta/blob name = "blob" departments = list(DEPARTMENT_ENGINEERING, DEPARTMENT_SECURITY, DEPARTMENT_MEDICAL) - chaos = 50 + chaos = 30 chaotic_threshold = EVENT_CHAOS_THRESHOLD_HIGH_IMPACT + event_class = "blob" // This makes it so there is no potential for multiple blob events of different types happening in the same round. event_type = /datum/event2/event/blob + // In the distant future, if a mechanical skill system were to come into being, these vars could be replaced with skill checks so off duty people could count. + var/required_fighters = 2 // Fighters refers to engineering OR security. + var/required_support = 1 // Support refers to doctors AND roboticists, depending on fighter composition. + +/datum/event2/meta/blob/hard + name = "harder blob" + chaos = 40 + event_type = /datum/event2/event/blob/hard_blob + required_fighters = 3 + +/datum/event2/meta/blob/multi_blob + name = "multi blob" + chaos = 60 + event_type = /datum/event2/event/blob/multi_blob + required_fighters = 4 + required_support = 2 + +// For bussing only. +/datum/event2/meta/blob/omni_blob + name = "omni blob" + chaos = 200 + event_type = /datum/event2/event/blob/omni_blob + enabled = FALSE /datum/event2/meta/blob/get_weight() - var/engineers = metric.count_people_in_department(DEPARTMENT_ENGINEERING) - var/security = metric.count_people_in_department(DEPARTMENT_SECURITY) - var/medical = metric.count_people_in_department(DEPARTMENT_MEDICAL) + // Count the 'fighters'. + var/list/engineers = metric.get_people_in_department(DEPARTMENT_ENGINEERING) + var/list/security = metric.get_people_in_department(DEPARTMENT_SECURITY) - var/assigned_staff = engineers + security - if(engineers || security) // Medical only counts if one of the other two exists, and even then they count as half. - assigned_staff += round(medical / 2) + if(engineers.len + security.len < required_fighters) + return 0 - var/weight = (max(assigned_staff - 2, 0) * 20) // An assigned staff count of 2 must be had to spawn a blob. - return weight + // Now count the 'support'. + var/list/medical = metric.get_people_in_department(DEPARTMENT_MEDICAL) + var/need_medical = FALSE + + var/list/robotics = metric.get_people_with_job(/datum/job/roboticist) + var/need_robotics = FALSE + + // Determine what kind of support might be needed. + for(var/mob/living/L in engineers|security) + if(L.isSynthetic()) + need_robotics = TRUE + else + need_medical = TRUE + + // Medical is more important than robotics, since robits tend to not suffer slow deaths if there isn't a roboticist. + if(medical.len < required_support && need_medical) + return 0 + + // Engineers can sometimes fill in as robotics. This is done in the interest of the event having a chance of not being super rare. + // In the uncertain future, a mechanical skill system check could replace this check here. + if(robotics.len + engineers.len < required_support && need_robotics) + return 0 + + var/fighter_weight = (engineers.len + security.len) * 20 + var/support_weight = (medical.len + robotics.len) * 10 // Not counting engineers as support so they don't cause 30 weight each. + var/chaos_weight = chaos / 2 // Chaos is added as a weight in order to make more chaotic variants be preferred if they are allowed to be picked. + + return fighter_weight + support_weight + chaos_weight /datum/event2/event/blob - - // This could be made into a GLOB accessible list if needed. - var/list/area/excuded = list( + announce_delay_lower_bound = 1 MINUTE + announce_delay_upper_bound = 5 MINUTES + // This could be made into a GLOB accessible list for reuse if needed. + var/list/area/excluded = list( /area/submap, /area/shuttle, /area/crew_quarters, /area/holodeck, /area/engineering/engine_room ) - var/turf/target_turf = null + var/list/open_turfs = list() var/area/target_area = null var/spawn_blob_type = /obj/structure/blob/core/random_medium - var/obj/structure/blob/core/blob_core = null + var/number_of_blobs = 1 + var/list/blobs = list() // A list containing weakrefs to blob cores created. Weakrefs mean this event won't interfere with qdel. + +/datum/event2/event/blob/hard_blob + spawn_blob_type = /obj/structure/blob/core/random_hard + +/datum/event2/event/blob/multi_blob + spawn_blob_type = /obj/structure/blob/core/random_hard // Lethargic blobs are boring. + number_of_blobs = 2 + +// For adminbus only. +/datum/event2/event/blob/omni_blob + number_of_blobs = 16 // Someday maybe we can get this to specifically spawn every blob. /datum/event2/event/blob/set_up() var/list/area/grand_list_of_areas = get_station_areas(excluded) + if(grand_list_of_areas.len) + for(var/i in 1 to 10) + var/area/A = pick(grand_list_of_areas) + if(is_area_occupied(A)) + log_debug("Blob infestation event: Rejected [A] because it is occupied.") + continue + var/list/turfs = list() + for(var/turf/simulated/floor/F in A) + if(turf_clear(F)) + turfs += F + if(turfs.len < 5 + number_of_blobs) + log_debug("Blob infestation event: Rejected [A] because it has too little clear turfs.") + continue + open_turfs = turfs.Copy() + target_area = A + log_debug("Blob infestation event: Going to place blob at [A].") + break - for(var/i in 1 to 10) - var/area/A = pick(grand_list_of_areas) - if(is_area_occupied(A)) - log_debug("Blob infestation event: Rejected [A] because it is occupied.") - continue - var/list/turfs = list() - for(var/turf/simulated/floor/F in A) - if(turf_clear(F)) - turfs += F - if(turfs.len == 0) - log_debug("Blob infestation event: Rejected [A] because it has no clear turfs.") - continue - target_area = A - target_turf = pick(turfs) - - if(!target_area) - log_debug("Blob infestation event: Giving up after too many failures to pick target area") + if(!open_turfs.len) + log_debug("Blob infestation event: Giving up after too many failures to pick a blob spot.") + abort() /datum/event2/event/blob/start() - blob_core = new spawn_blob_type(target_turf) + for(var/i = 1 to number_of_blobs) + var/turf/T = pick(open_turfs) + var/obj/structure/blob/core/new_blob = new spawn_blob_type(T) + blobs += weakref(new_blob) + open_turfs -= T // So we can't put two cores on the same tile if doing multiblob. + log_debug("Spawned [new_blob.overmind.blob_type.name] blob at [get_area(new_blob)].") + +/datum/event2/event/blob/should_end() + for(var/WR in blobs) + var/weakref/weakref = WR + if(weakref.resolve()) // If the weakref is resolvable, that means the blob hasn't been deleted yet. + return FALSE + return TRUE // Only end if all blobs die. + +// Normally this does nothing, but is useful if aborted by an admin. +/datum/event2/event/blob/end() + for(var/WR in blobs) + var/weakref/weakref = WR + var/obj/structure/blob/core/B = weakref.resolve() + if(istype(B)) + qdel(B) /datum/event2/event/blob/announce() - spawn(rand(600, 3000)) // 1-5 minute leeway for the blob to go un-detected. - command_announcement.Announce("Confirmed outbreak of level 7 biohazard aboard [station_name()]. All personnel must contain the outbreak.", "Biohazard Alert", new_sound = 'sound/AI/outbreak7.ogg') + if(!ended) // Don't announce if the blobs die early. + var/danger_level = 0 + var/list/blob_type_names = list() + var/multiblob = FALSE + for(var/WR in blobs) + var/weakref/weakref = WR + var/obj/structure/blob/core/B = weakref.resolve() + if(!istype(B)) + continue + var/datum/blob_type/blob_type = B.overmind.blob_type + blob_type_names += blob_type.name + if(danger_level > blob_type.difficulty) // The highest difficulty is used, if multiple blobs are present. + danger_level = blob_type.difficulty + + if(blob_type_names.len > 1) // More than one blob is harder. + danger_level += blob_type_names.len + multiblob = TRUE + + var/list/lines = list() + lines += "Confirmed outbreak of level [7 + danger_level] biohazard[multiblob ? "s": ""] \ + aboard [location_name()]. All personnel must contain the outbreak." + + if(danger_level >= BLOB_DIFFICULTY_MEDIUM) // Tell them what kind of blob it is if it's tough. + lines += "The biohazard[multiblob ? "s have": " has"] been identified as [english_list(blob_type_names)]." + + if(danger_level >= BLOB_DIFFICULTY_HARD) // If it's really hard then tell them where it is so the response occurs faster. + lines += "[multiblob ? "It is": "They are"] suspected to have originated from \the [target_area]." + + if(danger_level >= BLOB_DIFFICULTY_SUPERHARD) + lines += "Extreme caution is advised." + + command_announcement.Announce(lines.Join("\n"), "Biohazard Alert", new_sound = 'sound/AI/outbreak7.ogg') /* /datum/gm_action/blob @@ -141,23 +256,3 @@ */ -/* -/proc/level_seven_blob_announcement(var/obj/structure/blob/core/B) - if(!B || !B.overmind) - return - var/datum/blob_type/blob = B.overmind.blob_type // Shortcut so we don't need to delve into three variables every time. - var/list/lines = list() - - lines += "Confirmed outbreak of level [7 + blob.difficulty] biohazard aboard [station_name()]. All personnel must contain the outbreak." - - if(blob.difficulty >= BLOB_DIFFICULTY_MEDIUM) // Tell them what kind of blob it is if it's tough. - lines += "The biohazard has been identified as a '[blob.name]'." - - if(blob.difficulty >= BLOB_DIFFICULTY_HARD) // If it's really hard then tell them where it is so the response occurs faster. - lines += "It is suspected to have originated from \the [get_area(B)]." - - if(blob.difficulty >= BLOB_DIFFICULTY_SUPERHARD) - lines += "Extreme caution is advised." - - command_announcement.Announce(lines.Join("\n"), "Biohazard Alert", new_sound = 'sound/AI/outbreak7.ogg') -*/ \ No newline at end of file diff --git a/code/modules/gamemaster/event2/events/engineering/carp_migration.dm b/code/modules/gamemaster/event2/events/engineering/carp_migration.dm new file mode 100644 index 0000000000..4a4ceee6ce --- /dev/null +++ b/code/modules/gamemaster/event2/events/engineering/carp_migration.dm @@ -0,0 +1,142 @@ +/datum/event2/meta/carp_migration + name = "carp migration" + event_class = "carp" + departments = list(DEPARTMENT_SECURITY, DEPARTMENT_EVERYONE) + chaos = 30 + chaotic_threshold = EVENT_CHAOS_THRESHOLD_MEDIUM_IMPACT + event_type = /datum/event2/event/carp_migration + +/datum/event2/meta/carp_migration/get_weight() + return 10 + (metric.count_people_in_department(DEPARTMENT_SECURITY) * 20) + (metric.count_all_space_mobs() * 40) + + +/datum/event2/event/carp_migration + announce_delay_lower_bound = 1 MINUTE + announce_delay_upper_bound = 2 MINUTES + length_lower_bound = 30 SECONDS + length_upper_bound = 1 MINUTE + var/use_map_edge_with_landmarks = TRUE // Use both landmarks and spawning from the "edge" of the map. Otherise uses landmarks over map edge. + var/carp_cap = 30 // No more than this many (living) carp can exist from this event. + var/carp_smallest_group = 3 + var/carp_largest_group = 5 + var/carp_wave_cooldown = 10 SECONDS + + var/list/spawned_carp = list() + var/last_carp_wave_time = null // Last world.time we spawned a carp wave. + var/list/valid_z_levels = null + +/datum/event2/event/carp_migration/set_up() + valid_z_levels = get_location_z_levels() + valid_z_levels -= using_map.sealed_levels // Space levels only please! + +/datum/event2/event/carp_migration/announce() + var/announcement = "Unknown biological entities been detected near \the [location_name()], please stand-by." + command_announcement.Announce(announcement, "Lifesign Alert") + + +/datum/event2/event/carp_migration/event_tick() + if(last_carp_wave_time + carp_wave_cooldown > world.time) + return + last_carp_wave_time = world.time + + if(count_spawned_carps() < carp_cap) + spawn_fish(number_of_groups = rand(1, 4), min_size_of_group = carp_smallest_group, max_size_of_group = carp_largest_group) + +/datum/event2/event/carp_migration/end() + // Clean up carp that died in space for some reason. + for(var/mob/living/simple_mob/SM in spawned_carp) + if(SM.stat == DEAD) + var/turf/T = get_turf(SM) + if(istype(T, /turf/space)) + if(prob(75)) + qdel(SM) + +// Makes carp spawn, no landmarks required, but they're still used if they exist. +/datum/event2/event/carp_migration/proc/spawn_fish(number_of_groups, min_size_of_group, max_size_of_group, dir) + if(isnull(dir)) + dir = pick(GLOB.cardinal) + + // Check if any landmarks exist! + var/list/spawn_locations = list() + for(var/obj/effect/landmark/C in landmarks_list) + if(C.name == "carpspawn" && (C.z in valid_z_levels)) + spawn_locations.Add(C.loc) + + var/prioritize_landmarks = TRUE + if(use_map_edge_with_landmarks && prob(50)) + prioritize_landmarks = FALSE // One in two chance to come from the edge instead. + + if(spawn_locations.len && prioritize_landmarks) // Okay we've got landmarks, lets use those! + shuffle_inplace(spawn_locations) + number_of_groups = min(number_of_groups, spawn_locations.len) + var/i = 1 + while (i <= number_of_groups) + var/group_size = rand(min_size_of_group, max_size_of_group) + for (var/j = 0, j < group_size, j++) + spawn_one_carp(spawn_locations[i]) + i++ + return + + // Okay we did *not* have any landmarks, or we're being told to do both, so lets do our best! + var/i = 1 + while(i <= number_of_groups) + var/z_level = pick(valid_z_levels) + var/group_size = rand(min_size_of_group, max_size_of_group) + var/turf/map_center = locate(round(world.maxx/2), round(world.maxy/2), z_level) + var/turf/group_center = pick_random_edge_turf(dir, z_level, TRANSITIONEDGE + 2) + var/list/turfs = getcircle(group_center, 2) + for(var/j = 0, j < group_size, j++) + // On larger maps, BYOND gets in the way of letting simple_mobs path to the closest edge of the station. + // So instead we need to simulate the carp's travel, then spawn them somewhere still hopefully off screen. + + // Find a turf to be the edge of the map. + var/turf/edge_of_map = turfs[(i % turfs.len) + 1] + + // Now walk a straight line towards the center of the map, until we find a non-space tile. + var/turf/edge_of_station = null + + var/list/space_line = list() // This holds all space tiles on the line. Will be used a bit later. + for(var/turf/T in getline(edge_of_map, map_center)) + if(!T.is_space()) + break // We found the station! + space_line += T + edge_of_station = T + + // Now put the carp somewhere on the line, hopefully off screen. + // I wish this was higher than 8 but the BYOND internal A* algorithm gives up sometimes when using + // 16 or more. + // In the future, a new AI stance that handles long distance travel using getline() could work. + var/max_distance = 8 + var/turf/spawn_turf = null + for(var/P in space_line) + var/turf/point = P + if(get_dist(point, edge_of_station) <= max_distance) + spawn_turf = P + break + + if(spawn_turf) + // Finally, make the carp go towards the edge of the station. + var/mob/living/simple_mob/animal/M = spawn_one_carp(spawn_turf) + if(edge_of_station) + M.ai_holder?.give_destination(edge_of_station) // Ask carp to swim towards the edge of the station. + i++ + +/datum/event2/event/carp_migration/proc/spawn_one_carp(new_loc) + var/mob/living/simple_mob/animal/M = new /mob/living/simple_mob/animal/space/carp/event(new_loc) + GLOB.destroyed_event.register(M, src, .proc/on_carp_destruction) + spawned_carp += M + return M + + +// Counts living carp spawned by this event. +/datum/event2/event/carp_migration/proc/count_spawned_carps() + . = 0 + for(var/I in spawned_carp) + var/mob/living/simple_mob/animal/M = I + if(!QDELETED(M) && M.stat != DEAD) + . += 1 + +// If carp is bomphed, remove it from the list. +/datum/event2/event/carp_migration/proc/on_carp_destruction(mob/M) + spawned_carp -= M + GLOB.destroyed_event.unregister(M, src, .proc/on_carp_destruction) diff --git a/code/modules/gamemaster/event2/events/engineering/dust.dm b/code/modules/gamemaster/event2/events/engineering/dust.dm new file mode 100644 index 0000000000..1a26118fbc --- /dev/null +++ b/code/modules/gamemaster/event2/events/engineering/dust.dm @@ -0,0 +1,19 @@ +/datum/event2/meta/dust + name = "dust" + departments = list(DEPARTMENT_ENGINEERING) + chaos = 10 + chaotic_threshold = EVENT_CHAOS_THRESHOLD_LOW_IMPACT + reusable = TRUE + event_type = /datum/event2/event/dust + +/datum/event2/meta/dust/get_weight() + return metric.count_people_in_department(DEPARTMENT_ENGINEERING) * 20 + + + +/datum/event2/event/dust/announce() + if(prob(33)) + command_announcement.Announce("Dust has been detected on a collision course with \the [location_name()].") + +/datum/event2/event/dust/start() + dust_swarm("norm") diff --git a/code/modules/gamemaster/event2/events/engineering/gas_leak.dm b/code/modules/gamemaster/event2/events/engineering/gas_leak.dm new file mode 100644 index 0000000000..fb8dc41f86 --- /dev/null +++ b/code/modules/gamemaster/event2/events/engineering/gas_leak.dm @@ -0,0 +1,47 @@ +/datum/event2/meta/gas_leak + name = "gas leak" + departments = list(DEPARTMENT_ENGINEERING, DEPARTMENT_SYNTHETIC) + chaos = 10 + chaotic_threshold = EVENT_CHAOS_THRESHOLD_LOW_IMPACT + reusable = TRUE + event_type = /datum/event2/event/gas_leak + +/datum/event2/meta/gas_leak/get_weight() + // Synthetics are counted in higher value because they can wirelessly connect to alarms. + var/engineering_factor = metric.count_people_in_department(DEPARTMENT_ENGINEERING) * 10 + var/synthetic_factor = metric.count_people_in_department(DEPARTMENT_SYNTHETIC) * 30 + return (15 + engineering_factor + synthetic_factor) / (times_ran + 1) + + + +/datum/event2/event/gas_leak + var/potential_gas_choices = list("carbon_dioxide", "sleeping_agent", "phoron", "volatile_fuel") + var/chosen_gas = null + var/turf/chosen_turf = null + +/datum/event2/event/gas_leak/set_up() + chosen_gas = pick(potential_gas_choices) + + var/list/turfs = find_random_turfs() + if(!turfs.len) + log_debug("Gas Leak event failed to find any available turfs to leak into. Aborting.") + abort() + return + chosen_turf = pick(turfs) + +/datum/event2/event/gas_leak/announce() + if(chosen_turf) + command_announcement.Announce("Warning, hazardous [lowertext(gas_data.name[chosen_gas])] gas leak detected in \the [chosen_turf.loc], evacuate the area.", "Hazard Alert") + +/datum/event2/event/gas_leak/start() + // Okay, time to actually put the gas in the room! + // TODO - Would be nice to break a waste pipe perhaps? + // TODO - Maybe having it released from a single point and thus causing airflow to blow stuff around + + // Fow now just add a bunch of it to the air + + var/datum/gas_mixture/air_contents = new + air_contents.temperature = T20C + rand(-50, 50) + air_contents.gas[chosen_gas] = 10 * MOLES_CELLSTANDARD + chosen_turf.assume_air(air_contents) + playsound(chosen_turf, 'sound/effects/smoke.ogg', 75, 1) \ No newline at end of file diff --git a/code/modules/gamemaster/event2/events/engineering/grid_check.dm b/code/modules/gamemaster/event2/events/engineering/grid_check.dm index a0af41ccf7..8b081f29e2 100644 --- a/code/modules/gamemaster/event2/events/engineering/grid_check.dm +++ b/code/modules/gamemaster/event2/events/engineering/grid_check.dm @@ -23,7 +23,11 @@ return highest_overpower /datum/event2/meta/grid_check/get_weight() - return 50 + (metric.count_people_in_department(DEPARTMENT_ENGINEERING) * 20) + (50 * get_overpower()) + var/population_factor = metric.count_people_in_department(DEPARTMENT_ENGINEERING) * 10 + var/overpower_factor = 50 * get_overpower() // Will be 0 if not overloaded at all, and 50 if turbines are outputting twice as much as rated. + return (20 + population_factor + overpower_factor) / (times_ran + 1) + + /datum/event2/event/grid_check var/obj/machinery/power/generator/engine // The turbine that will send a power spike. diff --git a/code/modules/gamemaster/event2/events/engineering/meteor_defense.dm b/code/modules/gamemaster/event2/events/engineering/meteor_defense.dm new file mode 100644 index 0000000000..378c6b42fe --- /dev/null +++ b/code/modules/gamemaster/event2/events/engineering/meteor_defense.dm @@ -0,0 +1,83 @@ +// This event gives the station an advance warning about meteors, so that they can prepare in various ways. + +/datum/event2/meta/meteor_defense + name = "meteor defense" + departments = list(DEPARTMENT_ENGINEERING, DEPARTMENT_CARGO) + chaos = 50 + chaotic_threshold = EVENT_CHAOS_THRESHOLD_HIGH_IMPACT + event_class = "meteor defense" + event_type = /datum/event2/event/meteor_defense + +/datum/event2/meta/meteor_defense/get_weight() + // Engineers count as 20. + var/engineers = metric.count_people_in_department(DEPARTMENT_ENGINEERING) + if(engineers < 3) // There -must- be at least three engineers for this to be possible. + return 0 + + . = engineers * 20 + + // Cargo and AI/borgs count as 10. + var/cargo = metric.count_people_with_job(/datum/job/cargo_tech) + metric.count_people_with_job(/datum/job/qm) + var/bots = metric.count_people_in_department(DEPARTMENT_SYNTHETIC) + + . += (cargo + bots) * 10 + + +/datum/event2/event/meteor_defense + start_delay_lower_bound = 10 MINUTES + start_delay_upper_bound = 15 MINUTES + var/soon_announced = FALSE + var/direction = null // Actual dir used for which side the meteors come from. + var/dir_text = null // Direction shown in the announcement. + var/list/meteor_types = null + var/waves = null // How many times to send meteors. + var/last_wave_time = null // world.time of latest wave. + var/wave_delay = 10 SECONDS + var/wave_upper_bound = 8 // Max amount of meteors per wave. + var/wave_lower_bound = 4 // Min amount. + +/datum/event2/event/meteor_defense/proc/set_meteor_types() + meteor_types = meteors_threatening.Copy() + +/datum/event2/event/meteor_defense/set_up() + direction = pick(cardinal) // alldirs doesn't work with current meteor code unfortunately. + waves = rand(3, 6) + switch(direction) + if(NORTH) + dir_text = "aft" // For some reason this is needed. + if(SOUTH) + dir_text = "fore" + if(EAST) + dir_text = "port" + if(WEST) + dir_text = "starboard" + set_meteor_types() + +/datum/event2/event/meteor_defense/announce() + var/announcement = "Meteors are expected to approach from the [dir_text] side, in approximately [DisplayTimeText(time_to_start - world.time, 60)]." + command_announcement.Announce(announcement, "Meteor Alert", new_sound = 'sound/AI/meteors.ogg') + +/datum/event2/event/meteor_defense/wait_tick() + if(!soon_announced) + if((time_to_start - world.time) <= 5 MINUTES) + soon_announced = TRUE + var/announcement = "The incoming meteors are expected to approach from the [dir_text] side. \ + ETA to arrival is approximately [DisplayTimeText(time_to_start - world.time, 60)]." + command_announcement.Announce(announcement, "Meteor Alert - Update") + +/datum/event2/event/meteor_defense/start() + command_announcement.Announce("Incoming meteors approach from \the [dir_text] side!", "Meteor Alert - Update") + +/datum/event2/event/meteor_defense/event_tick() + if(world.time > last_wave_time + wave_delay) + last_wave_time = world.time + waves-- + message_admins("[waves] more wave\s of meteors remain.") + // Dir is reversed because the direction describes where meteors are going, not what side it's gonna hit. + spawn_meteors(rand(wave_upper_bound, wave_lower_bound), meteor_types, reverse_dir[direction]) + +/datum/event2/event/meteor_defense/should_end() + return waves <= 0 + +/datum/event2/event/meteor_defense/end() + command_announcement.Announce("\The [location_name()] will clear the incoming meteors in a moment.", "Meteor Alert - Update") diff --git a/code/modules/gamemaster/event2/events/everyone/comms_blackout.dm b/code/modules/gamemaster/event2/events/everyone/comms_blackout.dm index 1371055c84..69e91d6942 100644 --- a/code/modules/gamemaster/event2/events/everyone/comms_blackout.dm +++ b/code/modules/gamemaster/event2/events/everyone/comms_blackout.dm @@ -22,11 +22,13 @@ to_chat(A, "
") /datum/event2/event/comms_blackout/start() - // One in two chance for the radios to turn i%t# t&_)#%, which can be more alarming than radio silence. if(prob(50)) + // One in two chance for the radios to turn i%t# t&_)#%, which can be more alarming than radio silence. + log_debug("Doing partial outage of telecomms.") for(var/obj/machinery/telecomms/processor/P in telecomms_list) P.emp_act(1) - // Otherwise just shut everything down. else + // Otherwise just shut everything down, madagascar style. + log_debug("Doing complete outage of telecomms.") for(var/obj/machinery/telecomms/T in telecomms_list) T.emp_act(1) diff --git a/code/modules/gamemaster/event2/events/everyone/electrical_fault.dm b/code/modules/gamemaster/event2/events/everyone/electrical_fault.dm new file mode 100644 index 0000000000..e43d02fd21 --- /dev/null +++ b/code/modules/gamemaster/event2/events/everyone/electrical_fault.dm @@ -0,0 +1,111 @@ +// Makes a spooky electrical thing happen, that can blow the lights or make the APCs turn off for a short period of time. +// Doesn't do any permanent damage beyond the small chance to emag an APC, which just unlocks it forever. As such, this is free to occur even with no engineers. + +/datum/event2/meta/electrical_fault + name = "electrical fault" + departments = list(DEPARTMENT_EVERYONE) + chaos = 10 + chaotic_threshold = EVENT_CHAOS_THRESHOLD_LOW_IMPACT + event_type = /datum/event2/event/electrical_fault + +/datum/event2/meta/electrical_fault/get_weight() + return 10 + (metric.count_people_in_department(DEPARTMENT_EVERYONE) * 5) + + +/datum/event2/event/electrical_fault + start_delay_lower_bound = 30 SECONDS + start_delay_upper_bound = 1 MINUTE + length_lower_bound = 20 SECONDS + length_upper_bound = 40 SECONDS + var/max_apcs_per_tick = 2 + + var/list/valid_apcs = null + var/list/valid_z_levels = null + +/datum/event2/event/electrical_fault/announce() + // Trying to be vague to avoid 'space lightning storms'. + // This could be re-flavored to be a solar flare or something and have robots outside be sad. + command_announce.Announce("External conditions near \the [location_name()] are likely \ + to cause voltage spikes and other electrical issues very soon. Please secure sensitive electrical equipment until conditions improve.", "[location_name()] Sensor Array") + +/datum/event2/event/electrical_fault/set_up() + valid_z_levels = get_location_z_levels() + valid_z_levels -= using_map.sealed_levels // Space levels only please! + + valid_apcs = list() + for(var/obj/machinery/power/apc/A in global.machines) + if(A.z in valid_z_levels) + valid_apcs += A + +/datum/event2/event/electrical_fault/event_tick() + if(!valid_apcs.len) + log_debug("No valid APCs found for electrical fault event.") + return + + var/list/picked_apcs = list() + for(var/i = 1 to max_apcs_per_tick) + picked_apcs |= pick(valid_apcs) + + for(var/A in picked_apcs) + affect_apc(A) + +/datum/event2/event/electrical_fault/end() + command_announce.Announce("External conditions have returned to normal around \the [location_name()].") + +/datum/event2/event/electrical_fault/proc/affect_apc(obj/machinery/power/apc/A) + // Main breaker is turned off or is Special(tm). Consider it protected. + if((!A.operating || failure_timer > 0) || A.is_critical) + return + + playsound( + + // Decent chance to overload lighting circuit. + if(prob(6)) + A.overload_lighting() + + // Small chance to make the APC turn off for awhile. + // This will actually protect it from further damage. + if(prob(3)) + A.energy_fail(rand(20, 40)) + + // Relatively small chance to emag the apc as apc_damage event does. + if(prob(1)) + A.emagged = TRUE + A.update_icon() + +/* +/datum/event/electrical_storm/start() + ..() + valid_apcs = list() + for(var/obj/machinery/power/apc/A in global.machines) + if(A.z in affecting_z) + valid_apcs.Add(A) + endWhen = (severity * 60) + startWhen + +/datum/event/electrical_storm/tick() + ..() + // See if shields can stop it first (It would be nice to port baystation's cooler shield gens perhaps) + // TODO - We need a better shield generator system to handle this properly. + if(!valid_apcs.len) + log_debug("No valid APCs found for electrical storm event ship=[victim]!") + return + var/list/picked_apcs = list() + for(var/i=0, i< severity * 2, i++) // up to 2/4/6 APCs per tick depending on severity + picked_apcs |= pick(valid_apcs) + for(var/obj/machinery/power/apc/T in picked_apcs) + affect_apc(T) + +/datum/event/electrical_storm/proc/affect_apc(var/obj/machinery/power/apc/T) + // Main breaker is turned off. Consider this APC protected. + if(!T.operating) + return + + // Decent chance to overload lighting circuit. + if(prob(3 * severity)) + T.overload_lighting() + + // Relatively small chance to emag the apc as apc_damage event does. + if(prob(0.2 * severity)) + T.emagged = 1 + T.update_icon() +*/ diff --git a/code/modules/gamemaster/event2/events/everyone/solar_storm.dm b/code/modules/gamemaster/event2/events/everyone/solar_storm.dm index 8ab211affe..b73df66076 100644 --- a/code/modules/gamemaster/event2/events/everyone/solar_storm.dm +++ b/code/modules/gamemaster/event2/events/everyone/solar_storm.dm @@ -4,7 +4,9 @@ event_type = /datum/event2/event/solar_storm /datum/event2/meta/solar_storm/get_weight() - return 20 + (metric.count_people_in_department(DEPARTMENT_ENGINEERING) * 10) + (metric.count_all_space_mobs() * 30) + var/population_factor = metric.count_people_in_department(DEPARTMENT_ENGINEERING) * 10 + var/space_factor = metric.count_all_space_mobs() * 50 + return (20 + population_factor + space_factor) / (times_ran + 1) /datum/event2/event/solar_storm diff --git a/code/modules/gamemaster/event2/events/synthetic/ion_storm.dm b/code/modules/gamemaster/event2/events/synthetic/ion_storm.dm index a1d30f0976..e84108d608 100644 --- a/code/modules/gamemaster/event2/events/synthetic/ion_storm.dm +++ b/code/modules/gamemaster/event2/events/synthetic/ion_storm.dm @@ -7,7 +7,7 @@ /datum/event2/meta/ion_storm/get_weight() var/bots = metric.count_people_in_department(DEPARTMENT_SYNTHETIC) - var/weight = 5 + (bots * 20) + var/weight = 5 + (bots * 40) // A small chance even if no synths are on, since it can still emag beepsky. return weight @@ -29,6 +29,8 @@ var/mob/living/silicon/robot/R = target if(R.connected_ai) continue + if(R.shell) + continue // Crew member names, and excluding off station antags, are handled by `generate_ion_law()` automatically. var/law = target.generate_ion_law() diff --git a/code/modules/gamemaster/event2/meta.dm b/code/modules/gamemaster/event2/meta.dm index 95ba5c40bd..c876ce8f7d 100644 --- a/code/modules/gamemaster/event2/meta.dm +++ b/code/modules/gamemaster/event2/meta.dm @@ -26,6 +26,15 @@ // If true, the event won't have it's `enabled` var set to FALSE when ran by the GM system. var/reusable = FALSE + // A string used to identify a 'class' of similar events. + // If the event is not reusable, than all events sharing the same class are disabled. + // Useful if you only ever want one event per round while having a lot of different subtypes of the event. + var/event_class = null + + // Counter for how many times this event has been picked by the GM. + // Can be used to make event repeats discouraged but not forbidden by adjusting the weight based on it. + var/times_ran = 0 + // A reference to the system that initialized us. var/datum/controller/subsystem/game_master/GM = null @@ -42,13 +51,10 @@ return ..() -// Called by the GM system to actually start an event, -// and makes it so events that should only be ran once are made to not be usable again. +// Called by the GM system to actually start an event. /datum/event2/meta/proc/make_event() var/datum/event2/event/E = new event_type(GM) E.execute() - if(!reusable) - enabled = FALSE return E // Returns a TRUE or FALSE for if the GM system should be able to pick this event. diff --git a/code/modules/metric/department.dm b/code/modules/metric/department.dm index acb58101f1..e97e0cb17c 100644 --- a/code/modules/metric/department.dm +++ b/code/modules/metric/department.dm @@ -29,44 +29,72 @@ return DEPARTMENT_UNKNOWN // Welp. +// Similar to above, but gets the actual job. Note that it returns the job datum itself, or null. +/datum/metric/proc/guess_job(mob/M) + // Like before, records are the most reliable way. + var/datum/data/record/R = find_general_record("name", M.real_name) + if(R) // They got a record, now find the job datum. + var/datum/job/J = SSjob.get_job(R.fields["real_rank"]) + if(istype(J)) + return J + + // Try the mind. + if(M.mind) + var/datum/job/J = SSjob.get_job(M.mind.assigned_role) + if(istype(J)) + return J + + // Last ditch effort, check for job assigned to the mob itself. + var/datum/job/J = SSjob.get_job(M.job) + if(istype(J)) + return J + + return null + // 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. +// Improved with the addition of SSjob, which has departments be an actual thing and not a virtual concept. /datum/metric/proc/role_name_to_department(var/role_name) - var/list/result = list() + var/datum/job/J = SSjob.get_job(role_name) + if(istype(J)) + if(LAZYLEN(J.departments)) + return J.departments + return list(DEPARTMENT_UNKNOWN) - if(SSjob.is_job_in_department(role_name, DEPARTMENT_SECURITY)) - result += DEPARTMENT_SECURITY +/datum/metric/proc/count_people_in_department(var/department, cutoff = 75) + var/list/L = get_people_in_department(department, cutoff) + return L.len - if(SSjob.is_job_in_department(role_name, DEPARTMENT_ENGINEERING)) - result += DEPARTMENT_ENGINEERING - if(SSjob.is_job_in_department(role_name, DEPARTMENT_MEDICAL)) - result += DEPARTMENT_MEDICAL - - if(SSjob.is_job_in_department(role_name, DEPARTMENT_RESEARCH)) - result += DEPARTMENT_RESEARCH - - if(SSjob.is_job_in_department(role_name, DEPARTMENT_CARGO)) - result += DEPARTMENT_CARGO - - if(SSjob.is_job_in_department(role_name, DEPARTMENT_CIVILIAN)) - result += DEPARTMENT_CIVILIAN - - if(SSjob.is_job_in_department(role_name, DEPARTMENT_SYNTHETIC)) - result += DEPARTMENT_SYNTHETIC - - if(SSjob.is_job_in_department(role_name, DEPARTMENT_COMMAND)) // We do Command last, since we consider command to only be a primary department for hop/admin. - result += DEPARTMENT_COMMAND - - if(!result.len) // No department was found. - result += DEPARTMENT_UNKNOWN - return result - -/datum/metric/proc/count_people_in_department(var/department) +/datum/metric/proc/get_people_in_department(department, cutoff = 75) + . = list() 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 + if(assess_player_activity(M) < cutoff) + continue + . += M + +/datum/metric/proc/get_people_with_job(job_type, cutoff = 75) + . = list() + // First, get the name. + var/datum/job/J = SSjob.get_job_type(job_type) + if(!istype(J)) + return + + // Now find people with the job name. + for(var/M in player_list) + var/datum/job/their_job = guess_job(M) + if(!istype(their_job)) // No job was guessed. + continue + if(their_job.title != J.title) // Jobs don't match. + continue + if(assess_player_activity(M) < cutoff) // Too AFK. + continue + . += M + + +/datum/metric/proc/count_people_with_job(job_type, cutoff = 75) + var/list/L = get_people_with_job(job_type, cutoff) + return L.len \ No newline at end of file diff --git a/polaris.dme b/polaris.dme index d333604c80..7bfbbeb7b2 100644 --- a/polaris.dme +++ b/polaris.dme @@ -1424,7 +1424,6 @@ #include "code\modules\awaymissions\trigger.dm" #include "code\modules\awaymissions\zlevel.dm" #include "code\modules\blob2\_defines.dm" -#include "code\modules\blob2\announcement.dm" #include "code\modules\blob2\blobs\base_blob.dm" #include "code\modules\blob2\blobs\core.dm" #include "code\modules\blob2\blobs\factory.dm" @@ -1732,9 +1731,14 @@ #include "code\modules\gamemaster\event2\event.dm" #include "code\modules\gamemaster\event2\meta.dm" #include "code\modules\gamemaster\event2\events\cargo\shipping_error.dm" +#include "code\modules\gamemaster\event2\events\engineering\blob.dm" #include "code\modules\gamemaster\event2\events\engineering\camera_damage.dm" #include "code\modules\gamemaster\event2\events\engineering\canister_leak.dm" +#include "code\modules\gamemaster\event2\events\engineering\carp_migration.dm" +#include "code\modules\gamemaster\event2\events\engineering\dust.dm" +#include "code\modules\gamemaster\event2\events\engineering\gas_leak.dm" #include "code\modules\gamemaster\event2\events\engineering\grid_check.dm" +#include "code\modules\gamemaster\event2\events\engineering\meteor_defense.dm" #include "code\modules\gamemaster\event2\events\engineering\spacevine.dm" #include "code\modules\gamemaster\event2\events\everyone\comms_blackout.dm" #include "code\modules\gamemaster\event2\events\everyone\solar_storm.dm" @@ -2828,7 +2832,7 @@ #include "code\ZAS\Zone.dm" #include "interface\interface.dm" #include "interface\skin.dmf" -#include "maps\example\example.dm" +#include "maps\southern_cross\southern_cross.dm" #include "maps\submaps\space_submaps\space.dm" #include "maps\submaps\surface_submaps\mountains\mountains.dm" #include "maps\submaps\surface_submaps\mountains\mountains_areas.dm" diff --git a/sound/effects/light_flicker.ogg b/sound/effects/light_flicker.ogg new file mode 100644 index 0000000000..58be2c6ebd Binary files /dev/null and b/sound/effects/light_flicker.ogg differ