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