From dd2edef1967df4559d35665c23fcc26c6c6882ed Mon Sep 17 00:00:00 2001 From: Neerti Date: Sun, 22 Mar 2020 03:25:44 -0400 Subject: [PATCH] Saving more work. --- code/_helpers/turfs.dm | 16 +- code/controllers/subsystems/events2.dm | 2 + code/controllers/subsystems/game_master.dm | 27 ++- code/modules/blob2/blobs/base_blob.dm | 12 +- code/modules/blob2/blobs/core.dm | 13 +- code/modules/blob2/blobs/resource.dm | 4 +- code/modules/blob2/overmind/overmind.dm | 8 +- code/modules/blob2/overmind/powers.dm | 19 +- code/modules/gamemaster/event2/event.dm | 52 +++++ .../event2/events/cargo/shipping_error.dm | 2 +- .../event2/events/engineering/blob.dm | 203 +++++++++++++----- .../events/engineering/carp_migration.dm | 142 ++++++++++++ .../event2/events/engineering/dust.dm | 19 ++ .../event2/events/engineering/gas_leak.dm | 47 ++++ .../event2/events/engineering/grid_check.dm | 6 +- .../events/engineering/meteor_defense.dm | 83 +++++++ .../event2/events/everyone/comms_blackout.dm | 6 +- .../events/everyone/electrical_fault.dm | 111 ++++++++++ .../event2/events/everyone/solar_storm.dm | 4 +- .../event2/events/synthetic/ion_storm.dm | 4 +- code/modules/gamemaster/event2/meta.dm | 14 +- code/modules/metric/department.dm | 90 +++++--- polaris.dme | 8 +- sound/effects/light_flicker.ogg | Bin 0 -> 22700 bytes 24 files changed, 770 insertions(+), 122 deletions(-) create mode 100644 code/modules/gamemaster/event2/events/engineering/carp_migration.dm create mode 100644 code/modules/gamemaster/event2/events/engineering/dust.dm create mode 100644 code/modules/gamemaster/event2/events/engineering/gas_leak.dm create mode 100644 code/modules/gamemaster/event2/events/engineering/meteor_defense.dm create mode 100644 code/modules/gamemaster/event2/events/everyone/electrical_fault.dm create mode 100644 sound/effects/light_flicker.ogg 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 0000000000000000000000000000000000000000..58be2c6ebda53e0d19ed4307aaf9c7948deac650 GIT binary patch literal 22700 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 literal 0 HcmV?d00001