-
-
Notifications
You must be signed in to change notification settings - Fork 444
/
Copy pathgamemode_subsystem.dm
1219 lines (1092 loc) · 52.1 KB
/
gamemode_subsystem.dm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#define INIT_ORDER_GAMEMODE 70
///how many storytellers can be voted for along with always_votable ones
#define DEFAULT_STORYTELLER_VOTE_OPTIONS 4
///amount of players we can have before no longer running votes for storyteller
#define MAX_POP_FOR_STORYTELLER_VOTE 25
SUBSYSTEM_DEF(gamemode)
name = "Gamemode"
init_order = INIT_ORDER_GAMEMODE
runlevels = RUNLEVEL_GAME
flags = SS_BACKGROUND | SS_KEEP_TIMING
wait = 2 SECONDS
/// The name of the icon shown during credit roll (only set if the roundstart antag has an icon)
var/title_icon = null
/// List of our event tracks for fast access during for loops.
var/list/event_tracks = EVENT_TRACKS
/// Our storyteller. They progresses our trackboards and picks out events
var/datum/storyteller/storyteller
/// Result of the storyteller vote/pick. Defaults to the guide.
var/selected_storyteller = /datum/storyteller/guide
/// List of all the storytellers. Populated at init. Associative from type
var/list/storytellers = list()
/// Next process for our storyteller. The wait time is STORYTELLER_WAIT_TIME
var/next_storyteller_process = 0
/// Associative list of even track points.
var/list/event_track_points = list(
EVENT_TRACK_MUNDANE = 0,
EVENT_TRACK_MODERATE = 0,
EVENT_TRACK_MAJOR = 0,
EVENT_TRACK_ROLESET = 0,
EVENT_TRACK_OBJECTIVES = 0
)
/// Last point amount gained of each track. Those are recorded for purposes of estimating how long until next event.
var/list/last_point_gains = list(
EVENT_TRACK_MUNDANE = 0,
EVENT_TRACK_MODERATE = 0,
EVENT_TRACK_MAJOR = 0,
EVENT_TRACK_ROLESET = 0,
EVENT_TRACK_OBJECTIVES = 0
)
/// Point thresholds at which the events are supposed to be rolled, it is also the base cost for events.
var/list/point_thresholds = list(
EVENT_TRACK_MUNDANE = MUNDANE_POINT_THRESHOLD,
EVENT_TRACK_MODERATE = MODERATE_POINT_THRESHOLD,
EVENT_TRACK_MAJOR = MAJOR_POINT_THRESHOLD,
EVENT_TRACK_ROLESET = ROLESET_POINT_THRESHOLD,
EVENT_TRACK_OBJECTIVES = OBJECTIVES_POINT_THRESHOLD
)
/// Configurable multipliers for point gain over time.
var/list/point_gain_multipliers = list(
EVENT_TRACK_MUNDANE = 1,
EVENT_TRACK_MODERATE = 1,
EVENT_TRACK_MAJOR = 1,
EVENT_TRACK_ROLESET = 1,
EVENT_TRACK_OBJECTIVES = 1
)
/// Configurable multipliers for roundstart points.
var/list/roundstart_point_multipliers = list(
EVENT_TRACK_MUNDANE = 1,
EVENT_TRACK_MODERATE = 1,
EVENT_TRACK_MAJOR = 1,
EVENT_TRACK_ROLESET = 1,
EVENT_TRACK_OBJECTIVES = 1
)
/// Whether we allow pop scaling. This is configured by config, or the storyteller UI
var/allow_pop_scaling = TRUE
/// Associative list of pop scale thresholds.
var/list/pop_scale_thresholds = list(
EVENT_TRACK_MUNDANE = MUNDANE_POP_SCALE_THRESHOLD,
EVENT_TRACK_MODERATE = MODERATE_POP_SCALE_THRESHOLD,
EVENT_TRACK_MAJOR = MAJOR_POP_SCALE_THRESHOLD,
EVENT_TRACK_ROLESET = ROLESET_POP_SCALE_THRESHOLD,
EVENT_TRACK_OBJECTIVES = OBJECTIVES_POP_SCALE_THRESHOLD
)
/// Associative list of pop scale penalties.
var/list/pop_scale_penalties = list(
EVENT_TRACK_MUNDANE = MUNDANE_POP_SCALE_PENALTY,
EVENT_TRACK_MODERATE = MODERATE_POP_SCALE_PENALTY,
EVENT_TRACK_MAJOR = MAJOR_POP_SCALE_PENALTY,
EVENT_TRACK_ROLESET = ROLESET_POP_SCALE_PENALTY,
EVENT_TRACK_OBJECTIVES = OBJECTIVES_POP_SCALE_PENALTY
)
/// Associative list of active multipliers from pop scale penalty.
var/list/current_pop_scale_multipliers = list(
EVENT_TRACK_MUNDANE = 1,
EVENT_TRACK_MODERATE = 1,
EVENT_TRACK_MAJOR = 1,
EVENT_TRACK_ROLESET = 1,
EVENT_TRACK_OBJECTIVES = 1,
)
/// Associative list of control events by their track category. Compiled in Init
var/list/event_pools = list()
/// Events that we have scheduled to run in the nearby future
var/list/scheduled_events = list()
/// Associative list of tracks to forced event controls. For admins to force events (though they can still invoke them freely outside of the track system)
var/list/forced_next_events = list()
var/list/control = list() //list of all datum/round_event_control. Used for selecting events based on weight and occurrences.
var/list/running = list() //list of all existing /datum/round_event
var/list/round_end_data = list() //list of all reports that need to add round end reports
var/list/currentrun = list()
/// List of all uncategorized events, because they were wizard or holiday events
var/list/uncategorized = list()
var/list/holidays //List of all holidays occuring today or null if no holidays
/// Event frequency multiplier, it exists because wizard, eugh.
var/event_frequency_multiplier = 1
/// Current preview page for the statistics UI.
var/statistics_track_page = EVENT_TRACK_MUNDANE
/// Page of the UI panel.
var/panel_page = GAMEMODE_PANEL_MAIN
/// Whether we are viewing the roundstart events or not
var/roundstart_event_view = TRUE
/// Whether the storyteller has been halted
var/halted_storyteller = FALSE
/// Ready players for roundstart events.
var/ready_players = 0
var/active_players = 0
var/head_crew = 0
var/eng_crew = 0
var/sec_crew = 0
var/med_crew = 0
/// Is storyteller secret or not
var/secret_storyteller = FALSE
/// List of new player minds we currently want to give our roundstart antag to
var/list/roundstart_antag_minds = list()
var/wizardmode = FALSE //refactor this into just being a unique storyteller
/// What is our currently desired/selected roundstart event
var/datum/round_event_control/antagonist/solo/current_roundstart_event
var/list/last_round_events = list()
var/ran_roundstart = FALSE
var/list/triggered_round_events = list()
var/total_valid_antags = 0
/datum/controller/subsystem/gamemode/Initialize(time, zlevel)
// Populate event pools
for(var/track in event_tracks)
event_pools[track] = list()
// Populate storytellers
for(var/type in subtypesof(/datum/storyteller))
storytellers[type] = new type()
for(var/datum/round_event_control/event_type as anything in typesof(/datum/round_event_control))
if(!event_type::typepath || !event_type::name)
continue
var/datum/round_event_control/event = new event_type
if(!event.valid_for_map())
qdel(event)
continue // event isn't good for this map no point in trying to add it to the list
control += event //add it to the list of all events (controls)
getHoliday()
load_config_vars()
load_event_config_vars()
///Seeding events into track event pools needs to happen after event config vars are loaded
for(var/datum/round_event_control/event as anything in control)
if(event.holidayID || event.wizardevent)
uncategorized += event
continue
event_pools[event.track] += event //Add it to the categorized event pools
load_roundstart_data()
return SS_INIT_SUCCESS
/datum/controller/subsystem/gamemode/fire(resumed = FALSE)
if(!resumed)
src.currentrun = running.Copy()
///Handle scheduled events
for(var/datum/scheduled_event/sch_event in scheduled_events)
if(world.time >= sch_event.start_time)
sch_event.try_fire()
else if(!sch_event.alerted_admins && world.time >= sch_event.start_time - 1 MINUTES)
///Alert admins 1 minute before running and allow them to cancel or refund the event, once again.
sch_event.alerted_admins = TRUE
message_admins("Scheduled Event: [sch_event.event] will run in [(sch_event.start_time - world.time) / 10] seconds. (<a href='?src=[REF(sch_event)];action=cancel'>CANCEL</a>) (<a href='?src=[REF(sch_event)];action=refund'>REFUND</a>)")
if(!halted_storyteller && next_storyteller_process <= world.time && storyteller)
// We update crew information here to adjust population scalling and event thresholds for the storyteller.
update_crew_infos()
next_storyteller_process = world.time + STORYTELLER_WAIT_TIME
storyteller.process(STORYTELLER_WAIT_TIME * 0.1)
//cache for sanic speed (lists are references anyways)
var/list/currentrun = src.currentrun
while(currentrun.len)
var/datum/thing = currentrun[currentrun.len]
currentrun.len--
if(thing)
thing.process(wait * 0.1)
else
running.Remove(thing)
if (MC_TICK_CHECK)
return
/// Gets the number of antagonists the antagonist injection events will stop rolling after.
/datum/controller/subsystem/gamemode/proc/get_antag_cap()
var/total_number = get_correct_popcount() + (sec_crew * 2)
var/cap = FLOOR((total_number / ANTAG_CAP_DENOMINATOR), 1) + ANTAG_CAP_FLAT
return cap
/// Whether events can inject more antagonists into the round
/datum/controller/subsystem/gamemode/proc/can_inject_antags()
total_valid_antags = 0
for(var/datum/antagonist/antag in GLOB.antagonists)
if(!antag.count_towards_antag_cap) //some antags don't count towards the cap
continue
if(!antag.owner) //if the datum doesn't have an owner
continue
if(!antag.owner.current) //if the datum owner doesn't have a body
continue
if(antag.owner.current.stat == DEAD) //if the datum owner's body is dead
continue
total_valid_antags++
return (get_antag_cap() > total_valid_antags)
/// Gets candidates for antagonist roles.
/datum/controller/subsystem/gamemode/proc/get_candidates(be_special, job_ban, observers, ready_newplayers, living_players, required_time, inherit_required_time = TRUE, midround_antag_pref, no_antags = TRUE, list/restricted_roles, list/required_roles)
var/list/candidates = list()
var/list/candidate_candidates = list() //lol
for(var/mob/player as anything in GLOB.player_list)
if(player.mind && player?.client?.prefs?.read_preference(/datum/preference/toggle/quiet_mode)) //yogs: make sure to give them quiet mode, don't need to do anything else as the gamemode itself will handle that
player.mind.quiet_round = TRUE
if(ready_newplayers && isnewplayer(player))
var/mob/dead/new_player/new_player = player
if(new_player.ready == PLAYER_READY_TO_PLAY && new_player.mind && new_player.check_preferences())
candidate_candidates += player
else if(observers && isobserver(player))
candidate_candidates += player
else if(living_players && isliving(player))
if(!ishuman(player) && !isAI(player))
continue
// I split these checks up to make the code more readable ~Lucy
var/is_on_station = is_station_level(player.z)
var/is_late_arrival = HAS_TRAIT(SSstation, STATION_TRAIT_LATE_ARRIVALS) && istype(get_area(player), /area/shuttle/arrival)
if(!is_on_station && !is_late_arrival)
continue
candidate_candidates += player
for(var/mob/candidate as anything in candidate_candidates)
if(QDELETED(candidate) || !candidate.key || !candidate.client || (!observers && !candidate.mind))
continue
if(!observers)
if(!ready_players && !isliving(candidate))
continue
if(no_antags && !isnull(candidate.mind.antag_datums))
var/real = TRUE
for(var/datum/antagonist/antag_datum as anything in candidate.mind.antag_datums)
//if(antag_datum.count_against_dynamic_roll_chance && !(antag_datum.antag_flags & FLAG_FAKE_ANTAG))
//real = TRUE
//break
if(real)
continue
if(restricted_roles && (candidate.mind.assigned_role in restricted_roles))
continue
if(length(required_roles) && !(candidate.mind.assigned_role in required_roles))
continue
if(be_special)
if(!(candidate.client.prefs) || !(be_special in candidate.client.prefs.be_special))
continue
var/time_to_check
if(required_time)
time_to_check = required_time
else if(inherit_required_time)
var/datum/antagonist/age_check = GLOB.special_roles[be_special]
time_to_check = initial(age_check.min_account_age)
if(time_to_check && candidate.client.get_remaining_days(time_to_check) > 0)
continue
//if(midround_antag_pref)
//continue
if(job_ban && is_banned_from(candidate.ckey, list(job_ban, ROLE_ANTAG)))
continue
candidates += candidate
return candidates
/// Gets the correct popcount, returning READY people if roundstart, and active people if not.
/datum/controller/subsystem/gamemode/proc/get_correct_popcount()
if(SSticker.HasRoundStarted())
update_crew_infos()
return active_players
else
calculate_ready_players()
return ready_players
/// Refunds and removes a scheduled event.
/datum/controller/subsystem/gamemode/proc/refund_scheduled_event(datum/scheduled_event/refunded)
if(refunded.cost)
var/track_type = refunded.event.track
event_track_points[track_type] += refunded.cost
remove_scheduled_event(refunded)
/// Removes a scheduled event.
/datum/controller/subsystem/gamemode/proc/remove_scheduled_event(datum/scheduled_event/removed)
scheduled_events -= removed
qdel(removed)
/// We need to calculate ready players for the sake of roundstart events becoming eligible.
/datum/controller/subsystem/gamemode/proc/calculate_ready_players()
ready_players = 0
for(var/mob/dead/new_player/player as anything in GLOB.new_player_list)
if(player.ready == PLAYER_READY_TO_PLAY)
ready_players++
/// We roll points to be spent for roundstart events, including antagonists.
/datum/controller/subsystem/gamemode/proc/roll_pre_setup_points()
if(storyteller?.disable_distribution || halted_storyteller)
return
/// Distribute points
for(var/track in event_track_points)
var/base_amt
var/gain_amt
switch(track)
if(EVENT_TRACK_MUNDANE)
base_amt = ROUNDSTART_MUNDANE_BASE
gain_amt = ROUNDSTART_MUNDANE_GAIN
if(EVENT_TRACK_MODERATE)
base_amt = ROUNDSTART_MODERATE_BASE
gain_amt = ROUNDSTART_MODERATE_GAIN
if(EVENT_TRACK_MAJOR)
base_amt = ROUNDSTART_MAJOR_BASE
gain_amt = ROUNDSTART_MAJOR_GAIN
if(EVENT_TRACK_ROLESET)
base_amt = ROUNDSTART_ROLESET_BASE
gain_amt = ROUNDSTART_ROLESET_GAIN
if(EVENT_TRACK_OBJECTIVES)
base_amt = ROUNDSTART_OBJECTIVES_BASE
gain_amt = ROUNDSTART_OBJECTIVES_GAIN
var/calc_value = base_amt + (gain_amt * ready_players)
calc_value *= roundstart_point_multipliers[track]
calc_value *= storyteller.starting_point_multipliers[track]
calc_value *= (rand(100 - storyteller.roundstart_points_variance,100 + storyteller.roundstart_points_variance)/100)
event_track_points[track] = round(calc_value)
/// If the storyteller guarantees an antagonist roll, add points to make it so.
if(storyteller.guarantees_roundstart_roleset && event_track_points[EVENT_TRACK_ROLESET] < point_thresholds[EVENT_TRACK_ROLESET])
event_track_points[EVENT_TRACK_ROLESET] = point_thresholds[EVENT_TRACK_ROLESET]
/// If we have any forced events, ensure we get enough points for them
for(var/track in event_tracks)
if(forced_next_events[track] && event_track_points[track] < point_thresholds[track])
event_track_points[track] = point_thresholds[track]
/// At this point we've rolled roundstart events and antags and we handle leftover points here.
/datum/controller/subsystem/gamemode/proc/handle_post_setup_points()
// for(var/track in event_track_points) //Just halve the points for now.
// event_track_points[track] *= 0.5 TESTING HOW THINGS GO WITHOUT THIS HALVING OF POINTS
return
/// Because roundstart events need 2 steps of firing for purposes of antags, here is the first step handled, happening before occupation division.
/datum/controller/subsystem/gamemode/proc/handle_pre_setup_roundstart_events()
if(storyteller?.disable_distribution)
return
if(halted_storyteller)
message_admins("WARNING: Didn't roll roundstart events (including antagonists) due to the storyteller being halted.")
return
while(TRUE)
if(!storyteller.handle_tracks())
break
/// Second step of handlind roundstart events, happening after people spawn.
/datum/controller/subsystem/gamemode/proc/handle_post_setup_roundstart_events()
/// Start all roundstart events on post_setup immediately
for(var/datum/round_event/event as anything in running)
if(!event.control.roundstart)
continue
ASYNC
event.try_start()
INVOKE_ASYNC(event, TYPE_PROC_REF(/datum/round_event, try_start))
/// Schedules an event to run later.
/datum/controller/subsystem/gamemode/proc/schedule_event(datum/round_event_control/passed_event, passed_time, passed_cost, passed_ignore, passed_announce, _forced = FALSE)
if(_forced)
passed_ignore = TRUE
var/datum/scheduled_event/scheduled = new (passed_event, world.time + passed_time, passed_cost, passed_ignore, passed_announce)
var/round_started = SSticker.HasRoundStarted()
if(round_started)
message_admins("Event: [passed_event] has been scheduled to run in [passed_time / 10] seconds. (<a href='?src=[REF(scheduled)];action=cancel'>CANCEL</a>) (<a href='?src=[REF(scheduled)];action=refund'>REFUND</a>)")
else //Only roundstart events can be scheduled before round start
message_admins("Event: [passed_event] has been scheduled to run on roundstart. (<a href='?src=[REF(scheduled)];action=cancel'>CANCEL</a>)")
scheduled_events += scheduled
/datum/controller/subsystem/gamemode/proc/update_crew_infos()
// Very similar logic to `get_active_player_count()`
active_players = 0
head_crew = 0
eng_crew = 0
med_crew = 0
sec_crew = 0
for(var/mob/player_mob as anything in GLOB.player_list)
if(QDELETED(player_mob))
continue
if(!player_mob.client)
continue
if(player_mob.stat) //If they're alive
continue
if(player_mob.client.is_afk()) //If afk
continue
if(!ishuman(player_mob))
continue
active_players++
if(player_mob.mind?.assigned_role)
var/datum/job/player_role = SSjob.GetJob(player_mob.mind.assigned_role)
if(!player_role || !istype(player_role))
continue
if(player_role.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND)
head_crew++
if(player_role.departments_bitflags & DEPARTMENT_BITFLAG_ENGINEERING)
eng_crew++
if(player_role.departments_bitflags & DEPARTMENT_BITFLAG_MEDICAL)
med_crew++
if(player_role.departments_bitflags & DEPARTMENT_BITFLAG_SECURITY)
sec_crew++
update_pop_scaling()
/datum/controller/subsystem/gamemode/proc/update_pop_scaling()
for(var/track in event_tracks)
var/high_pop_bound = pop_scale_thresholds[track]
var/scale_penalty = pop_scale_penalties[track]
var/perceived_pop = min(active_players, high_pop_bound)
var/divisor = high_pop_bound
/// If the bounds are equal, we'd be dividing by zero or worse, if upper is smaller than lower, we'd be increasing the factor, just make it 1 and continue.
/// this is only a problem for bad configs
if(divisor <= 0)
current_pop_scale_multipliers[track] = 1
continue
var/scalar = perceived_pop / divisor
var/penalty = scale_penalty - (scale_penalty * scalar)
var/calculated_multiplier = 1 - (penalty / 100)
current_pop_scale_multipliers[track] = calculated_multiplier
/datum/controller/subsystem/gamemode/proc/TriggerEvent(datum/round_event_control/event, forced = FALSE)
log_storyteller("Event: [event] is being triggered.")
. = event.preRunEvent(forced)
if(. == EVENT_CANT_RUN)//we couldn't run this event for some reason, set its max_occurrences to 0
event.max_occurrences = 0
else if(. == EVENT_READY)
event.runEvent(random = TRUE)
///Resets frequency multiplier.
/datum/controller/subsystem/gamemode/proc/resetFrequency()
event_frequency_multiplier = 1
/client/proc/forceEvent()
set name = "Trigger Event"
set category = "Admin.Events"
if(!holder)
return
holder.forceEvent(usr)
/datum/admins/proc/forceEvent(mob/user)
SSgamemode.event_panel(user)
/client/proc/forceGamemode()
set name = "Open Gamemode Panel"
set category = "Admin.Events"
if(!holder)
return
holder.forceGamemode(usr)
/datum/admins/proc/forceGamemode(mob/user)
SSgamemode.admin_panel(user)
//////////////
// HOLIDAYS //
//////////////
//Uncommenting ALLOW_HOLIDAYS in config.txt will enable holidays
//It's easy to add stuff. Just add a holiday datum in code/modules/holiday/holidays.dm
//You can then check if it's a special day in any code in the game by doing if(SSgamemode.holidays["Groundhog Day"])
//You can also make holiday random events easily thanks to Pete/Gia's system.
//simply make a random event normally, then assign it a holidayID string which matches the holiday's name.
//Anything with a holidayID, which isn't in the holidays list, will never occur.
//Please, Don't spam stuff up with stupid stuff (key example being april-fools Pooh/ERP/etc),
//And don't forget: CHECK YOUR CODE!!!! We don't want any zero-day bugs which happen only on holidays and never get found/fixed!
//////////////////////////////////////////////////////////////////////////////////////////////////////////
//ALSO, MOST IMPORTANTLY: Don't add stupid stuff! Discuss bonus content with Project-Heads first please!//
//////////////////////////////////////////////////////////////////////////////////////////////////////////
/proc/check_holidays(holiday_to_find)
if(!CONFIG_GET(flag/allow_holidays))
return // Holiday stuff was not enabled in the config!
return LAZYFIND(SSgamemode.holidays, holiday_to_find)
//sets up the holidays and holidays list
/datum/controller/subsystem/gamemode/proc/getHoliday()
if(!CONFIG_GET(flag/allow_holidays))
return // Holiday stuff was not enabled in the config!
for(var/H in subtypesof(/datum/holiday))
var/datum/holiday/holiday = new H()
var/delete_holiday = TRUE
for(var/timezone in holiday.timezones)
var/time_in_timezone = world.realtime + timezone HOURS
var/YYYY = text2num(time2text(time_in_timezone, "YYYY")) // get the current year
var/MM = text2num(time2text(time_in_timezone, "MM")) // get the current month
var/DD = text2num(time2text(time_in_timezone, "DD")) // get the current day
var/DDD = time2text(time_in_timezone, "DDD") // get the current weekday
if(holiday.shouldCelebrate(DD, MM, YYYY, DDD))
holiday.celebrate()
LAZYSET(holidays, holiday.name, holiday)
delete_holiday = FALSE
break
if(delete_holiday)
qdel(holiday)
if(holidays)
holidays = shuffle(holidays)
// regenerate station name because holiday prefixes.
set_station_name(new_station_name())
world.update_status()
/datum/controller/subsystem/gamemode/proc/toggleWizardmode()
wizardmode = !wizardmode //TODO: decide what to do with wiz events
message_admins("Summon Events has been [wizardmode ? "enabled, events will occur [SSgamemode.event_frequency_multiplier] times as fast" : "disabled"]!")
log_game("Summon Events was [wizardmode ? "enabled" : "disabled"]!")
///Attempts to select players for special roles the mode might have.
/datum/controller/subsystem/gamemode/proc/pre_setup()
calculate_ready_players()
roll_pre_setup_points()
//handle_pre_setup_roundstart_events()
return TRUE
///Everyone should now be on the station and have their normal gear. This is the place to give the special roles extra things
/datum/controller/subsystem/gamemode/proc/post_setup(report) //Gamemodes can override the intercept report. Passing TRUE as the argument will force a report.
if(!report)
report = !CONFIG_GET(flag/no_intercept_report)
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(display_roundstart_logout_report)), ROUNDSTART_LOGOUT_REPORT_TIME)
if(CONFIG_GET(flag/reopen_roundstart_suicide_roles))
var/delay = CONFIG_GET(number/reopen_roundstart_suicide_roles_delay)
if(delay)
delay = (delay SECONDS)
else
delay = (4 MINUTES) //default to 4 minutes if the delay isn't defined.
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(reopen_roundstart_suicide_roles)), delay)
if(SSdbcore.Connect())
var/list/to_set = list()
var/arguments = list()
if(storyteller)
to_set += "game_mode = :game_mode"
arguments["game_mode"] = storyteller.name
if(GLOB.revdata.originmastercommit)
to_set += "commit_hash = :commit_hash"
arguments["commit_hash"] = GLOB.revdata.originmastercommit
if(to_set.len)
arguments["round_id"] = GLOB.round_id
var/datum/DBQuery/query_round_game_mode = SSdbcore.NewQuery(
"UPDATE [format_table_name("round")] SET [to_set.Join(", ")] WHERE id = :round_id",
arguments
)
query_round_game_mode.Execute()
qdel(query_round_game_mode)
if(report)
addtimer(CALLBACK(src, PROC_REF(send_intercept)), rand(600, 1800))
generate_station_goals()
handle_post_setup_roundstart_events()
handle_post_setup_points()
roundstart_event_view = FALSE
return TRUE
/datum/controller/subsystem/gamemode/proc/send_intercept()
var/intercepttext = "<b><i>Central Command Status Summary</i></b><hr>"
intercepttext += "<b>Central Command has intercepted and is attempting to decode a Syndicate transmission with vital information regarding their movements in this station's sector.</b>"
intercepttext += generate_station_goal_report()
if(CONFIG_GET(flag/auto_blue_alert))
print_command_report(intercepttext, "Central Command Status Summary", announce=FALSE)
priority_announce("A summary has been copied and printed to all communications consoles.\n\n[generate_station_trait_report()]", "Enemy communication intercepted. Security level elevated.", ANNOUNCER_INTERCEPT)
if(SSsecurity_level.get_current_level_as_number() < SEC_LEVEL_BLUE)
SSsecurity_level.set_level(SEC_LEVEL_BLUE)
else
print_command_report(intercepttext, "Central Command Status Summary")
///Handles late-join antag assignments
/datum/controller/subsystem/gamemode/proc/make_antag_chance(mob/living/carbon/human/character)
return
/datum/controller/subsystem/gamemode/proc/check_finished(force_ending) //to be called by SSticker
if(!SSticker.setup_done)
return FALSE
if(SSshuttle.emergency && (SSshuttle.emergency.mode == SHUTTLE_ENDGAME))
return TRUE
if(SSgamemode.station_was_nuked)
return TRUE
if(force_ending)
return TRUE
/*
* Generate a list of station goals available to purchase to report to the crew.
*
* Returns a formatted string all station goals that are available to the station.
*/
/datum/controller/subsystem/gamemode/proc/generate_station_goal_report()
if(!SSgamemode.station_goals.len)
return
. = "<hr><b>Special Orders for [station_name()]:</b><BR>"
for(var/datum/station_goal/station_goal as anything in SSgamemode.station_goals)
station_goal.on_report()
. += station_goal.get_report()
return
/*
* Generate a list of active station traits to report to the crew.
*
* Returns a formatted string of all station traits (that are shown) affecting the station.
*/
/datum/controller/subsystem/gamemode/proc/generate_station_trait_report()
if(!SSstation.station_traits.len)
return
. = "<hr><b>Identified shift divergencies:</b><BR>"
for(var/datum/station_trait/station_trait as anything in SSstation.station_traits)
if(!station_trait.show_in_report)
continue
. += "[station_trait.get_report()]<BR>"
return
/* /proc/reopen_roundstart_suicide_roles()
var/include_command = CONFIG_GET(flag/reopen_roundstart_suicide_roles_command_positions)
var/list/reopened_jobs = list()
for(var/mob/living/quitter in GLOB.suicided_mob_list)
var/datum/job/job = SSjob.GetJob(quitter.job)
if(!job || !(job.job_flags & JOB_REOPEN_ON_ROUNDSTART_LOSS))
continue
if(!include_command && job.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND)
continue
job.current_positions = max(job.current_positions - 1, 0)
reopened_jobs += quitter.job
if(CONFIG_GET(flag/reopen_roundstart_suicide_roles_command_report))
if(reopened_jobs.len)
var/reopened_job_report_positions
for(var/dead_dudes_job in reopened_jobs)
reopened_job_report_positions = "[reopened_job_report_positions ? "[reopened_job_report_positions]\n":""][dead_dudes_job]"
var/suicide_command_report = "<font size = 3><b>Central Command Human Resources Board</b><br>\
Notice of Personnel Change</font><hr>\
To personnel management staff aboard [station_name()]:<br><br>\
Our medical staff have detected a series of anomalies in the vital sensors \
of some of the staff aboard your station.<br><br>\
Further investigation into the situation on our end resulted in us discovering \
a series of rather... unforturnate decisions that were made on the part of said staff.<br><br>\
As such, we have taken the liberty to automatically reopen employment opportunities for the positions of the crew members \
who have decided not to partake in our research. We will be forwarding their cases to our employment review board \
to determine their eligibility for continued service with the company (and of course the \
continued storage of cloning records within the central medical backup server.)<br><br>\
<i>The following positions have been reopened on our behalf:<br><br>\
[reopened_job_report_positions]</i>"
print_command_report(suicide_command_report, "Central Command Personnel Update") */
//////////////////////////
//Reports player logouts//
//////////////////////////
/* /proc/display_roundstart_logout_report()
var/list/msg = list("[SPAN_BOLDNOTICE("Roundstart logout report")]\n\n")
for(var/i in GLOB.mob_living_list)
var/mob/living/L = i
var/mob/living/carbon/C = L
if (istype(C) && !C.last_mind)
continue // never had a client
if(L.ckey && !GLOB.directory[L.ckey])
msg += "<b>[L.name]</b> ([L.key]), the [L.job] (<font color='#ffcc00'><b>Disconnected</b></font>)\n"
if(L.ckey && L.client)
var/failed = FALSE
if(L.client.inactivity >= (ROUNDSTART_LOGOUT_REPORT_TIME / 2)) //Connected, but inactive (alt+tabbed or something)
msg += "<b>[L.name]</b> ([L.key]), the [L.job] (<font color='#ffcc00'><b>Connected, Inactive</b></font>)\n"
failed = TRUE //AFK client
if(!failed && L.stat)
if(L.suiciding) //Suicider
msg += "<b>[L.name]</b> ([L.key]), the [L.job] ([SPAN_BOLDANNOUNCE("Suicide")])\n"
failed = TRUE //Disconnected client
if(!failed && (L.stat == UNCONSCIOUS || L.stat == HARD_CRIT))
msg += "<b>[L.name]</b> ([L.key]), the [L.job] (Dying)\n"
failed = TRUE //Unconscious
if(!failed && L.stat == DEAD)
msg += "<b>[L.name]</b> ([L.key]), the [L.job] (Dead)\n"
failed = TRUE //Dead
continue //Happy connected client
for(var/mob/dead/observer/D in GLOB.dead_mob_list)
if(D.mind && D.mind.current == L)
if(L.stat == DEAD)
if(L.suiciding) //Suicider
msg += "<b>[L.name]</b> ([ckey(D.mind.key)]), the [L.job] ([SPAN_BOLDANNOUNCE("Suicide")])\n"
continue //Disconnected client
else
msg += "<b>[L.name]</b> ([ckey(D.mind.key)]), the [L.job] (Dead)\n"
continue //Dead mob, ghost abandoned
else
if(D.can_reenter_corpse)
continue //Adminghost, or cult/wizard ghost
else
msg += "<b>[L.name]</b> ([ckey(D.mind.key)]), the [L.job] ([SPAN_BOLDANNOUNCE("Ghosted")])\n"
continue //Ghosted while alive
for (var/C in GLOB.admins)
to_chat(C, msg.Join()) */
/datum/controller/subsystem/gamemode/proc/generate_station_goals()
var/list/possible = subtypesof(/datum/station_goal)
var/goal_weights = 0
while(possible.len && goal_weights < 1) // station goal budget is 1
var/datum/station_goal/picked = pick_n_take(possible)
goal_weights += initial(picked.weight)
station_goals += new picked
//Set result and news report here
/datum/controller/subsystem/gamemode/proc/set_round_result()
SSticker.mode_result = "undefined"
if(SSgamemode.station_was_nuked)
SSticker.news_report = STATION_DESTROYED_NUKE
if(EMERGENCY_ESCAPED_OR_ENDGAMED)
SSticker.news_report = STATION_EVACUATED
if(SSshuttle.emergency.is_hijacked())
SSticker.news_report = SHUTTLE_HIJACK
/// Loads json event config values from events.txt
/datum/controller/subsystem/gamemode/proc/load_event_config_vars()
var/json_file = file("[global.config.directory]/events.json")
if(!fexists(json_file))
return
var/list/decoded = json_decode(file2text(json_file))
for(var/event_text_path in decoded)
var/event_path = text2path(event_text_path)
var/datum/round_event_control/event
for(var/datum/round_event_control/iterated_event as anything in control)
if(iterated_event.type == event_path)
event = iterated_event
break
if(!event)
continue
var/list/var_list = decoded[event_text_path]
for(var/variable in var_list)
var/value = var_list[variable]
switch(variable)
if("weight")
event.weight = value
if("min_players")
event.min_players = value
if("max_occurrences")
event.max_occurrences = value
if("earliest_start")
event.earliest_start = value * (1 MINUTES)
if("track")
if(value in event_tracks)
event.track = value
if("cost")
event.cost = value
if("reoccurence_penalty_multiplier")
event.reoccurence_penalty_multiplier = value
if("shared_occurence_type")
if(!isnull(value))
value = "[value]"
event.shared_occurence_type = value
/// Loads config values from game_options.txt
/datum/controller/subsystem/gamemode/proc/load_config_vars()
point_gain_multipliers[EVENT_TRACK_MUNDANE] = CONFIG_GET(number/mundane_point_gain_multiplier)
point_gain_multipliers[EVENT_TRACK_MODERATE] = CONFIG_GET(number/moderate_point_gain_multiplier)
point_gain_multipliers[EVENT_TRACK_MAJOR] = CONFIG_GET(number/major_point_gain_multiplier)
point_gain_multipliers[EVENT_TRACK_ROLESET] = CONFIG_GET(number/roleset_point_gain_multiplier)
point_gain_multipliers[EVENT_TRACK_OBJECTIVES] = CONFIG_GET(number/objectives_point_gain_multiplier)
roundstart_point_multipliers[EVENT_TRACK_MUNDANE] = CONFIG_GET(number/mundane_roundstart_point_multiplier)
roundstart_point_multipliers[EVENT_TRACK_MODERATE] = CONFIG_GET(number/moderate_roundstart_point_multiplier)
roundstart_point_multipliers[EVENT_TRACK_MAJOR] = CONFIG_GET(number/major_roundstart_point_multiplier)
roundstart_point_multipliers[EVENT_TRACK_ROLESET] = CONFIG_GET(number/roleset_roundstart_point_multiplier)
roundstart_point_multipliers[EVENT_TRACK_OBJECTIVES] = CONFIG_GET(number/objectives_roundstart_point_multiplier)
point_thresholds[EVENT_TRACK_MUNDANE] = CONFIG_GET(number/mundane_point_threshold)
point_thresholds[EVENT_TRACK_MODERATE] = CONFIG_GET(number/moderate_point_threshold)
point_thresholds[EVENT_TRACK_MAJOR] = CONFIG_GET(number/major_point_threshold)
point_thresholds[EVENT_TRACK_ROLESET] = CONFIG_GET(number/roleset_point_threshold)
point_thresholds[EVENT_TRACK_OBJECTIVES] = CONFIG_GET(number/objectives_point_threshold)
/datum/controller/subsystem/gamemode/proc/handle_picking_storyteller()
if(length(GLOB.clients) > MAX_POP_FOR_STORYTELLER_VOTE)
secret_storyteller = TRUE
selected_storyteller = pick_weight(get_valid_storytellers(TRUE))
return
SSvote.initiate_vote(/datum/vote/storyteller, "pick round storyteller", forced = TRUE)
/datum/controller/subsystem/gamemode/proc/storyteller_vote_choices()
var/list/final_choices = list()
var/list/pick_from = list()
for(var/datum/storyteller/storyboy in get_valid_storytellers())
if(storyboy.always_votable)
final_choices[storyboy.name] = 0
else
pick_from[storyboy.name] = storyboy.weight //might be able to refactor this to be slightly better due to get_valid_storytellers returning a weighted list
var/added_storytellers = 0
while(added_storytellers < DEFAULT_STORYTELLER_VOTE_OPTIONS && length(pick_from))
added_storytellers++
var/picked_storyteller = pick_weight(pick_from)
final_choices[picked_storyteller] = 0
pick_from -= picked_storyteller
return final_choices
/datum/controller/subsystem/gamemode/proc/storyteller_desc(storyteller_name)
for(var/storyteller_type in storytellers)
var/datum/storyteller/storyboy = storytellers[storyteller_type]
if(storyboy.name != storyteller_name)
continue
return storyboy.desc
/datum/controller/subsystem/gamemode/proc/storyteller_vote_result(winner_name)
for(var/storyteller_type in storytellers)
var/datum/storyteller/storyboy = storytellers[storyteller_type]
if(storyboy.name == winner_name)
selected_storyteller = storyteller_type
break
///return a weighted list of all storytellers that are currently valid to roll, if return_types is set then we will return types instead of instances
/datum/controller/subsystem/gamemode/proc/get_valid_storytellers(return_types = FALSE)
var/client_amount = length(GLOB.clients)
var/list/valid_storytellers = list()
for(var/storyteller_type in storytellers)
var/datum/storyteller/storyboy = storytellers[storyteller_type]
if(storyboy.restricted || (storyboy.population_min && storyboy.population_min > client_amount) || (storyboy.population_max && storyboy.population_max < client_amount))
continue
valid_storytellers[return_types ? storyboy.type : storyboy] = storyboy.weight
return valid_storytellers
/datum/controller/subsystem/gamemode/proc/init_storyteller()
set_storyteller(selected_storyteller)
/datum/controller/subsystem/gamemode/proc/set_storyteller(passed_type)
if(!storytellers[passed_type])
message_admins("Attempted to set an invalid storyteller type: [passed_type], force setting to guide instead.")
storyteller = storytellers[/datum/storyteller/guide] //if we dont have any then we brick, lets not do that
CRASH("Attempted to set an invalid storyteller type: [passed_type].")
storyteller = storytellers[passed_type]
if(!secret_storyteller)
send_to_playing_players(span_notice("<b>Storyteller is [storyteller.name]!</b>"))
send_to_playing_players(span_notice("[storyteller.welcome_text]"))
else
send_to_observers(span_boldbig("<b>Storyteller is [storyteller.name]!</b>")) //observers still get to know
log_storyteller("Storyteller set: [storyteller.name]")
/// Panel containing information, variables and controls about the gamemode and scheduled event
/datum/controller/subsystem/gamemode/proc/admin_panel(mob/user)
update_crew_infos()
total_valid_antags = 0
for(var/mob/checked_mob in GLOB.mob_list)
if(!checked_mob.mind)
continue
if(!checked_mob.mind.special_role)
continue
if(checked_mob.stat == DEAD)
continue
total_valid_antags++
var/round_started = SSticker.HasRoundStarted()
var/list/dat = list()
dat += "<BR><a href='?src=[REF(src)];panel=main;action=halt_storyteller' [halted_storyteller ? "class='linkOn'" : ""]>HALT Storyteller</a> <a href='?src=[REF(src)];panel=main;action=open_stats'>Event Panel</a> <a href='?src=[REF(src)];panel=main;action=set_storyteller'>Set Storyteller</a> <a href='?src=[REF(src)];panel=main'>Refresh</a>"
if(storyteller)
dat += "<BR>Storyteller: [storyteller.name]"
dat += "<BR>Description: [storyteller.desc]"
else
dat += "<BR><b>No Storyteller Selected</b>"
dat += "<font color='#888888'><i>Storyteller determines points gained, event chances, and is the entity responsible for rolling events.</i></font>"
dat += "<BR>Active Players: [active_players] (Head: [head_crew], Sec: [sec_crew], Eng: [eng_crew], Med: [med_crew])"
dat += "<BR>Antagonist Count vs Maximum: [total_valid_antags] / [get_antag_cap()]"
dat += "<HR>"
dat += "<a href='?src=[REF(src)];panel=main;action=tab;tab=[GAMEMODE_PANEL_MAIN]' [panel_page == GAMEMODE_PANEL_MAIN ? "class='linkOn'" : ""]>Main</a>"
dat += " <a href='?src=[REF(src)];panel=main;action=tab;tab=[GAMEMODE_PANEL_VARIABLES]' [panel_page == GAMEMODE_PANEL_VARIABLES ? "class='linkOn'" : ""]>Variables</a>"
dat += "<HR>"
switch(panel_page)
if(GAMEMODE_PANEL_VARIABLES)
dat += "<a href='?src=[REF(src)];panel=main;action=reload_config_vars'>Reload Config Vars</a> <font color='#888888'><i>Configs located in game_options.txt.</i></font>"
dat += "<BR><b>Point Gains Multipliers (only over time):</b>"
dat += "<BR><font color='#888888'><i>This affects points gained over time towards scheduling new events of the tracks.</i></font>"
for(var/track in event_tracks)
dat += "<BR>[track]: <a href='?src=[REF(src)];panel=main;action=vars;var=pts_multiplier;track=[track]'>[point_gain_multipliers[track]]</a>"
dat += "<HR>"
dat += "<b>Roundstart Points Multipliers:</b>"
dat += "<BR><font color='#888888'><i>This affects points generated for roundstart events and antagonists.</i></font>"
for(var/track in event_tracks)
dat += "<BR>[track]: <a href='?src=[REF(src)];panel=main;action=vars;var=roundstart_pts;track=[track]'>[roundstart_point_multipliers[track]]</a>"
dat += "<HR>"
dat += "<b>Point Thresholds:</b>"
dat += "<BR><font color='#888888'><i>Those are thresholds the tracks require to reach with points to make an event.</i></font>"
for(var/track in event_tracks)
dat += "<BR>[track]: <a href='?src=[REF(src)];panel=main;action=vars;var=pts_threshold;track=[track]'>[point_thresholds[track]]</a>"
if(GAMEMODE_PANEL_MAIN)
var/even = TRUE
dat += "<h2>Event Tracks:</h2>"
dat += "<font color='#888888'><i>Every track represents progression towards scheduling an event of it's severity</i></font>"
dat += "<table align='center'; width='100%'; height='100%'; style='background-color:#13171C'>"
dat += "<tr style='vertical-align:top'>"
dat += "<td width=25%><b>Track</b></td>"
dat += "<td width=20%><b>Progress</b></td>"
dat += "<td width=10%><b>Next</b></td>"
dat += "<td width=10%><b>Forced</b></td>"
dat += "<td width=35%><b>Actions</b></td>"
dat += "</tr>"
for(var/track in event_tracks)
even = !even
var/background_cl = even ? "#17191C" : "#23273C"
var/lower = event_track_points[track]
var/upper = point_thresholds[track]
var/percent = round((lower/upper)*100)
var/next = 0
var/last_points = last_point_gains[track]
if(last_points)
next = round(((upper - lower) / last_points / STORYTELLER_WAIT_TIME))
dat += "<tr style='vertical-align:top; background-color: [background_cl];'>"
dat += "<td>[track] - [last_points] per process.</td>" //Track
dat += "<td>[percent]% ([lower]/[upper])</td>" //Progress
dat += "<td>~[next] seconds</td>" //Next
var/datum/round_event_control/forced_event = forced_next_events[track]
var/forced = forced_event ? "[forced_event.name] <a href='?src=[REF(src)];panel=main;action=track_action;track_action=remove_forced;track=[track]'>X</a>" : ""
dat += "<td>[forced]</td>" //Forced
dat += "<td><a href='?src=[REF(src)];panel=main;action=track_action;track_action=set_pts;track=[track]'>Set Pts.</a> <a href='?src=[REF(src)];panel=main;action=track_action;track_action=next_event;track=[track]'>Next Event</a></td>" //Actions
dat += "</tr>"
dat += "</table>"
dat += "<h2>Scheduled Events:</h2>"
dat += "<table align='center'; width='100%'; height='100%'; style='background-color:#13171C'>"
dat += "<tr style='vertical-align:top'>"
dat += "<td width=30%><b>Name</b></td>"
dat += "<td width=17%><b>Severity</b></td>"
dat += "<td width=12%><b>Time</b></td>"
dat += "<td width=41%><b>Actions</b></td>"
dat += "</tr>"
var/sorted_scheduled = list()
for(var/datum/scheduled_event/scheduled as anything in scheduled_events)
sorted_scheduled[scheduled] = scheduled.start_time
sortTim(sorted_scheduled, cmp=/proc/cmp_numeric_asc, associative = TRUE)
even = TRUE
for(var/datum/scheduled_event/scheduled as anything in sorted_scheduled)
even = !even
var/background_cl = even ? "#17191C" : "#23273C"
dat += "<tr style='vertical-align:top; background-color: [background_cl];'>"
dat += "<td>[scheduled.event.name]</td>" //Name
dat += "<td>[scheduled.event.track]</td>" //Severity
var/time = (scheduled.event.roundstart && !round_started) ? "ROUNDSTART" : "[(scheduled.start_time - world.time) / (1 SECONDS)] s."
dat += "<td>[time]</td>" //Time
dat += "<td>[scheduled.get_href_actions()]</td>" //Actions
dat += "</tr>"
dat += "</table>"
dat += "<h2>Running Events:</h2>"
dat += "<table align='center'; width='100%'; height='100%'; style='background-color:#13171C'>"
dat += "<tr style='vertical-align:top'>"
dat += "<td width=30%><b>Name</b></td>"
dat += "<td width=70%><b>Actions</b></td>"
dat += "</tr>"
even = TRUE
for(var/datum/round_event/event as anything in running)
even = !even
var/background_cl = even ? "#17191C" : "#23273C"
dat += "<tr style='vertical-align:top; background-color: [background_cl];'>"
dat += "<td>[event.control.name]</td>" //Name
dat += "<td>-TBA-</td>" //Actions