[READY FOR MERGE] Storyteller 2.2 (#2116)

## About The Pull Request

This one is bound to come with mostly admin and code facing changes,
players are bound to expect one less ms of runtime and ~~maybe a
different antag cap system~~ (Later).

### Storyteller admin UI

Creates a fresh new ui for the storyteller admin side, making it
significantly easier to use than before, and adds some neat elements
that visualize what's going on.


![image](https://github.com/user-attachments/assets/550c5b03-9f6f-4393-9797-8bd105be3945)

## Why It's Good For The Game

The code sucks less, the ui sucks less

## Proof Of Testing

The TM has not been met with any major issues from the admins.

## Changelog

🆑
admin: New storyteller admin panel
admin: More logging to the storyteller panel actions and to it spawning
things
/🆑

---------

Co-authored-by: LT3 <83487515+lessthnthree@users.noreply.github.com>
Co-authored-by: The Sharkening <95130227+StrangeWeirdKitten@users.noreply.github.com>
Co-authored-by: Arturlang <24881678+Arturlang@users.noreply.github.com>
This commit is contained in:
Waterpig
2024-11-29 15:56:18 +01:00
committed by GitHub
parent ef7a90a2c6
commit f997a464a5
9 changed files with 743 additions and 337 deletions

View File

@@ -114,7 +114,7 @@
if(!check_rights(R_ADMIN))
return
//SSdynamic.admin_panel() // BUBBER EDIT - STORYTELLER
SSgamemode.admin_panel(usr) // BUBBER EDIT - STORYTELLER
SSgamemode.ui_interact(usr) // BUBBER EDIT - STORYTELLER
else if(href_list["call_shuttle"])
if(!check_rights(R_ADMIN))
return

View File

@@ -0,0 +1,15 @@
/datum/round_event_control/proc/generate_ui_data()
return list(
"name" = name,
"desc" = description,
"tags" = tags,
"occurences" = get_occurences(),
"occurences_shared" = !isnull(shared_occurence_type),
"min_pop" = min_players,
"start" = (earliest_start / 600),
"can_run" = can_spawn_event(),
"weight" = calculated_weight,
"weight_raw" = weight,
"track" = track,
"roundstart" = roundstart,
)

View File

@@ -1,7 +1,7 @@
#define INIT_ORDER_GAMEMODE 70
SUBSYSTEM_DEF(gamemode)
name = "Gamemode"
name = "Storyteller"
init_order = INIT_ORDER_GAMEMODE
runlevels = RUNLEVEL_GAME
flags = SS_BACKGROUND | SS_KEEP_TIMING
@@ -119,7 +119,7 @@ SUBSYSTEM_DEF(gamemode)
var/roundstart_event_view = TRUE
/// Whether the storyteller has been halted
var/halted_storyteller = FALSE
var/storyteller_halted = FALSE
/// Ready players for roundstart events.
var/ready_players = 0
@@ -129,6 +129,9 @@ SUBSYSTEM_DEF(gamemode)
var/sec_crew = 0
var/med_crew = 0
/// Whether we looked up pop info in this process tick
var/pop_data_cached = FALSE
var/wizardmode = FALSE
var/storyteller_voted = FALSE
@@ -175,11 +178,13 @@ SUBSYSTEM_DEF(gamemode)
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.
if(!storyteller_halted && 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)
// Reset the cache value to false
pop_data_cached = FALSE
//cache for sanic speed (lists are references anyways)
var/list/currentrun = src.currentrun
@@ -277,10 +282,12 @@ SUBSYSTEM_DEF(gamemode)
/// 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()
if(!pop_data_cached)
update_crew_infos()
return active_players
else
calculate_ready_players()
if(!pop_data_cached)
calculate_ready_players()
return ready_players
/// Refunds and removes a scheduled event.
@@ -305,10 +312,11 @@ SUBSYSTEM_DEF(gamemode)
for(var/mob/dead/new_player/player as anything in GLOB.new_player_list)
if(player.ready == PLAYER_READY_TO_PLAY)
ready_players++
pop_data_cached = TRUE
/// 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)
if(storyteller.disable_distribution || storyteller_halted)
return
/// Distribute points
for(var/track in event_track_points)
@@ -343,7 +351,7 @@ SUBSYSTEM_DEF(gamemode)
/datum/controller/subsystem/gamemode/proc/handle_pre_setup_roundstart_events()
if(storyteller.disable_distribution)
return
if(halted_storyteller)
if(storyteller_halted)
message_admins("WARNING: Didn't roll roundstart events (including antagonists) due to the storyteller being halted.")
return
while(TRUE)
@@ -397,6 +405,7 @@ SUBSYSTEM_DEF(gamemode)
med_crew++
if(player_role.departments_bitflags & DEPARTMENT_BITFLAG_SECURITY)
sec_crew++
pop_data_cached = TRUE
/datum/controller/subsystem/gamemode/proc/TriggerEvent(datum/round_event_control/event)
. = event.preRunEvent()
@@ -511,6 +520,7 @@ SUBSYSTEM_DEF(gamemode)
addtimer(CALLBACK(src, PROC_REF(send_trait_report)), rand(1 MINUTES, 5 MINUTES))
handle_post_setup_roundstart_events()
roundstart_event_view = FALSE
pop_data_cached = FALSE // Uncache it because we'd still wrongly consider it cached from lobby pops
return TRUE
@@ -680,16 +690,24 @@ SUBSYSTEM_DEF(gamemode)
break
/datum/controller/subsystem/gamemode/proc/init_storyteller()
if(storyteller) // If this is true, then an admin bussed one, don't overwrite it
log_dynamic("Roundstart storyteller has been set by admins to [storyteller.name], the vote was not considered.")
return
var/datum/storyteller/storyteller_pick
if(!voted_storyteller)
storyteller_pick = pick(storytellers)
message_admins("We picked [storyteller_pick] for this rounds storyteller, randomly.")
log_dynamic("Roundstart picked storyteller [storyteller.name] randomly due to no vote result.")
voted_storyteller = storyteller_pick
if(storyteller) // If this is true, then an admin bussed one, don't overwrite it
return
set_storyteller(voted_storyteller)
/datum/controller/subsystem/gamemode/proc/set_storyteller(passed_type)
/**
* set_storyteller
*
* Always call this to set the storyteller
* Called by the storyteller system on roundstart and after a vote finishes.
* When forced via game panel, forced = TRUE, and force_ckey contains the ckey of the admin who forced it
*/
/datum/controller/subsystem/gamemode/proc/set_storyteller(passed_type, forced, force_ckey)
if(!storytellers[passed_type])
message_admins("Attempted to set an invalid storyteller type: [passed_type].")
CRASH("Attempted to set an invalid storyteller type: [passed_type].")
@@ -704,300 +722,56 @@ SUBSYSTEM_DEF(gamemode)
to_chat(world, span_notice("<b>Storyteller is [storyteller.name]!</b>"))
to_chat(world, span_notice("[storyteller.welcome_text]"))
log_admin_private("Storyteller switched to [storyteller.name]. [forced ? "Forced by admin ckey [force_ckey]" : ""]")
/// Panel containing information, variables and controls about the gamemode and scheduled event
/datum/controller/subsystem/gamemode/proc/admin_panel(mob/user)
update_crew_infos()
var/round_started = SSticker.HasRoundStarted()
var/list/dat = list()
var/active_pop = get_correct_popcount()
dat += "Storyteller: [storyteller ? "[storyteller.name]" : "None"] "
dat += " <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>"
dat += "<BR><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_pop] (Head: [head_crew], Sec: [sec_crew], Eng: [eng_crew], Med: [med_crew]) - Antag Cap: [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>"
/**
* halt_storyteller
*
* Used to halt/unhalt and properly log storyteller
*/
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>Minimum Population for Tracks:</b>"
dat += "<BR><font color='#888888'><i>This are the minimum population caps for events to be able to run.</i></font>"
for(var/track in event_tracks)
dat += "<BR>[track]: <a href='?src=[REF(src)];panel=main;action=vars;var=min_pop;track=[track]'>[min_pop_thresholds[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 * 40 / 6) / 10
dat += "<tr style='vertical-align:top; background-color: [background_cl];'>"
dat += "<td>[track]</td>" //Track
dat += "<td>[percent]% ([lower]/[upper])</td>" //Progress
dat += "<td>~[next] m.</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
dat += "</tr>"
dat += "</table>"
var/datum/browser/popup = new(user, "gamemode_admin_panel", "Gamemode Panel", 670, 650)
popup.set_content(dat.Join())
popup.open()
/// Panel containing information and actions regarding events
/datum/controller/subsystem/gamemode/proc/event_panel(mob/user)
var/list/dat = list()
if(storyteller)
dat += "Storyteller: [storyteller.name]"
dat += "<BR>Repetition penalty multiplier: [storyteller.event_repetition_multiplier]"
dat += "<BR>Cost variance: [storyteller.cost_variance]"
if(storyteller.tag_multipliers)
dat += "<BR>Tag multipliers:"
for(var/tag in storyteller.tag_multipliers)
dat += "[tag]:[storyteller.tag_multipliers[tag]] | "
storyteller.calculate_weights(statistics_track_page)
else
dat += "Storyteller: None<BR>Weight and chance statistics will be inaccurate due to the present lack of a storyteller."
dat += "<BR><a href='?src=[REF(src)];panel=stats;action=set_roundstart'[roundstart_event_view ? "class='linkOn'" : ""]>Roundstart Events</a> Forced Roundstart events will use rolled points, and are guaranteed to trigger (even if the used points are not enough)"
dat += "<BR>Avg. event intervals: "
for(var/track in event_tracks)
if(last_point_gains[track])
var/est_time = round(point_thresholds[track] / last_point_gains[track] / STORYTELLER_WAIT_TIME * 40 / 6) / 10
dat += "[track]: ~[est_time] m. | "
dat += "<HR>"
for(var/track in EVENT_PANEL_TRACKS)
dat += "<a href='?src=[REF(src)];panel=stats;action=set_cat;cat=[track]'[(statistics_track_page == track) ? "class='linkOn'" : ""]>[track]</a>"
dat += "<HR>"
/// Create event info and stats table
dat += "<table align='center'; width='100%'; height='100%'; style='background-color:#13171C'>"
dat += "<tr style='vertical-align:top'>"
dat += "<td width=17%><b>Name</b></td>"
dat += "<td width=16%><b>Tags</b></td>"
dat += "<td width=8%><b>Occurences</b></td>"
dat += "<td width=5%><b>M.Pop</b></td>"
dat += "<td width=5%><b>M.Time</b></td>"
dat += "<td width=7%><b>Can Occur</b></td>"
dat += "<td width=16%><b>Weight</b></td>"
dat += "<td width=26%><b>Actions</b></td>"
dat += "</tr>"
var/even = TRUE
var/total_weight = 0
var/list/event_lookup
switch(statistics_track_page)
if(ALL_EVENTS)
event_lookup = control
if(UNCATEGORIZED_EVENTS)
event_lookup = uncategorized
else
event_lookup = event_pools[statistics_track_page]
var/list/assoc_spawn_weight = list()
var/active_pop = get_correct_popcount()
for(var/datum/round_event_control/event as anything in event_lookup)
if(event.roundstart != roundstart_event_view)
continue
if(event.can_spawn_event(active_pop))
total_weight += event.calculated_weight
assoc_spawn_weight[event] = event.calculated_weight
else
assoc_spawn_weight[event] = 0
sortTim(assoc_spawn_weight, cmp=/proc/cmp_numeric_dsc, associative = TRUE)
for(var/datum/round_event_control/event as anything in assoc_spawn_weight)
even = !even
var/background_cl = even ? "#17191C" : "#23273C"
dat += "<tr style='vertical-align:top; background-color: [background_cl];'>"
dat += "<td>[event.name]</td>" //Name
dat += "<td>" //Tags
for(var/tag in event.tags)
dat += "[tag] "
dat += "</td>"
var/occurence_string = "[event.occurrences]"
if(event.shared_occurence_type)
occurence_string += " (shared: [event.get_occurences()])"
dat += "<td>[occurence_string]</td>" //Occurences
dat += "<td>[event.min_players]</td>" //Minimum pop
dat += "<td>[event.earliest_start / (1 MINUTES)] m.</td>" //Minimum time
dat += "<td>[assoc_spawn_weight[event] ? "Yes" : "No"]</td>" //Can happen?
var/weight_string = "([event.calculated_weight] /raw.[event.weight])"
if(assoc_spawn_weight[event])
var/percent = round((event.calculated_weight / total_weight) * 100)
weight_string = "[percent]% - [weight_string]"
dat += "<td>[weight_string]</td>" //Weight
dat += "<td>[event.get_href_actions()]</td>" //Actions
dat += "</tr>"
dat += "</table>"
var/datum/browser/popup = new(user, "gamemode_event_panel", "Event Panel", 1000, 600)
popup.set_content(dat.Join())
popup.open()
/datum/controller/subsystem/gamemode/Topic(href, href_list)
. = ..()
var/mob/user = usr
if(!check_rights(R_ADMIN))
/datum/controller/subsystem/gamemode/proc/halt_storyteller(mob/user)
storyteller_halted = !storyteller_halted
if(isnull(user))
return
switch(href_list["panel"])
if("main")
switch(href_list["action"])
if("set_storyteller")
message_admins("[key_name_admin(usr)] is picking a new Storyteller.")
var/list/name_list = list()
for(var/storyteller_type in storytellers)
var/datum/storyteller/storyboy = storytellers[storyteller_type]
name_list[storyboy.name] = storyboy.type
var/new_storyteller_name = input(usr, "Choose new storyteller (circumvents voted one):", "Storyteller") as null|anything in name_list
if(!new_storyteller_name)
message_admins("[key_name_admin(usr)] has cancelled picking a Storyteller.")
return
message_admins("[key_name_admin(usr)] has chosen [new_storyteller_name] as the new Storyteller.")
var/new_storyteller_type = name_list[new_storyteller_name]
set_storyteller(new_storyteller_type)
if("halt_storyteller")
halted_storyteller = !halted_storyteller
message_admins("[key_name_admin(usr)] has [halted_storyteller ? "HALTED" : "un-halted"] the Storyteller.")
if("vars")
var/track = href_list["track"]
switch(href_list["var"])
if("pts_multiplier")
var/new_value = input(usr, "New value:", "Set new value") as num|null
if(isnull(new_value) || new_value < 0)
return
message_admins("[key_name_admin(usr)] set point gain multiplier for [track] track to [new_value].")
point_gain_multipliers[track] = new_value
if("roundstart_pts")
var/new_value = input(usr, "New value:", "Set new value") as num|null
if(isnull(new_value) || new_value < 0)
return
message_admins("[key_name_admin(usr)] set roundstart pts multiplier for [track] track to [new_value].")
roundstart_point_multipliers[track] = new_value
if("min_pop")
var/new_value = input(usr, "New value:", "Set new value") as num|null
if(isnull(new_value) || new_value < 0)
return
message_admins("[key_name_admin(usr)] set minimum population for [track] track to [new_value].")
min_pop_thresholds[track] = new_value
if("pts_threshold")
var/new_value = input(usr, "New value:", "Set new value") as num|null
if(isnull(new_value) || new_value < 0)
return
message_admins("[key_name_admin(usr)] set point threshold of [track] track to [new_value].")
point_thresholds[track] = new_value
if("reload_config_vars")
message_admins("[key_name_admin(usr)] reloaded gamemode config vars.")
load_config_vars()
if("tab")
var/tab = href_list["tab"]
panel_page = tab
if("open_stats")
event_panel(user)
return
if("track_action")
var/track = href_list["track"]
if(!(track in event_tracks))
return
switch(href_list["track_action"])
if("remove_forced")
if(forced_next_events[track])
var/datum/round_event_control/event = forced_next_events[track]
message_admins("[key_name_admin(usr)] removed forced event [event.name] from track [track].")
forced_next_events -= track
if("set_pts")
var/set_pts = input(usr, "New point amount ([point_thresholds[track]]+ invokes event):", "Set points for [track]") as num|null
if(isnull(set_pts))
return
event_track_points[track] = set_pts
message_admins("[key_name_admin(usr)] set points of [track] track to [set_pts].")
log_admin_private("[key_name(usr)] set points of [track] track to [set_pts].")
if("next_event")
message_admins("[key_name_admin(usr)] invoked next event for [track] track.")
log_admin_private("[key_name(usr)] invoked next event for [track] track.")
event_track_points[track] = point_thresholds[track]
if(storyteller)
storyteller.handle_tracks()
admin_panel(user)
if("stats")
switch(href_list["action"])
if("set_roundstart")
roundstart_event_view = !roundstart_event_view
if("set_cat")
var/new_category = href_list["cat"]
if(new_category in EVENT_PANEL_TRACKS)
statistics_track_page = new_category
event_panel(user)
message_admins("[key_name_admin(user)] has [storyteller_halted ? "HALTED" : "un-halted"] the Storyteller.")
log_dynamic("Storyteller [storyteller_halted ? "halted" : "un-halted"] by admin [user.ckey].")
/**
* force_next_event
*
* Forces next event scheduling/firing for `track`
*
*/
/datum/controller/subsystem/gamemode/proc/force_next_event(track, mob/user)
if(isnull(user))
return
if(isnull(storyteller))
return
event_track_points[track] = point_thresholds[track]
storyteller.handle_tracks()
message_admins("[key_name_admin(user)] has forced an event for the [track] track.")
log_admin_private("Storyteller track [track] forced to run an event by [user.ckey]")
/**
* get_scheduled_by_event_type
*
* Returns the scheduled event, if any, by the event's type.
* Returns null if no such event exists.
*/
/datum/controller/subsystem/gamemode/proc/get_scheduled_by_event_type(type)
for(var/datum/scheduled_event/scheduled_event as anything in scheduled_events)
if(scheduled_event.event.type == text2path(type))
return scheduled_event
return null
/datum/controller/subsystem/gamemode/proc/get_event_by_track_and_type(track, type)
if(isnull(track) || isnull(type))
return
var/list/track_events = event_pools[track]
if(isnull(track_events))
return
for(var/datum/round_event_control/event as anything in track_events)
if(event.type == text2path(type))
return event

View File

@@ -0,0 +1,157 @@
// Open the ui, nothing special here
// Maybe make it update static data?
/datum/controller/subsystem/gamemode/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "ZubbersStoryteller", "Storyteller control panel")
ui.open()
/datum/controller/subsystem/gamemode/ui_data(mob/user)
var/list/data = list()
data["storyteller_name"] = storyteller ? storyteller.name : "None"
data["storyteller_halt"] = storyteller_halted
data["antag_count"] = GLOB.current_living_antags.len // Switch this up if the calculation for the cap changes (It probably will)
data["antag_cap"] = get_antag_cap()
data["pop_data"] = get_ui_pop_data()
data["tracks_data"] = get_ui_track_data()
data["scheduled_data"] = get_ui_scheduled_data()
return data
/datum/controller/subsystem/gamemode/proc/get_ui_pop_data()
var/list/pop_data = list(
"active" = get_correct_popcount(),
"head" = head_crew,
"sec" = sec_crew,
"eng" = eng_crew,
"med" = med_crew,
)
return pop_data
/datum/controller/subsystem/gamemode/proc/get_ui_track_data()
var/list/track_data = list()
for(var/track in event_tracks)
var/last_points = last_point_gains[track]
var/lower = event_track_points[track]
var/upper = point_thresholds[track]
var/next = last_points ? round((upper - lower) / last_points / STORYTELLER_WAIT_TIME * 40 / 6) / 10 : 0
var/datum/round_event_control/forced = forced_next_events[track]
track_data[track] = list(
"name" = "[track]",
"current" = lower,
"max" = upper,
"next" = next,
"forced" = forced ? forced.generate_ui_data() : null
)
return track_data
/datum/controller/subsystem/gamemode/proc/get_ui_scheduled_data()
var/list/scheduled_data = list()
var/list/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)
for(var/datum/scheduled_event/scheduled as anything in sorted_scheduled)
var/time = (scheduled.event.roundstart) ? null : ((scheduled.start_time - world.time) / (1 SECONDS))
scheduled_data[scheduled.event.name] = list(
"track" = scheduled.event.track,
"time" = time,
"event_type" = scheduled.event.type,
)
return scheduled_data
// God has abandoned us
/datum/controller/subsystem/gamemode/ui_static_data(mob/user)
var/list/static_data = list()
// Events are static because we don't need to update them as often, only on storyteller ticks
static_data["events"] = list()
for(var/event_category as anything in event_pools)
var/list/event_list = event_pools[event_category]
static_data["events"][event_category] = list("name" = event_category, "events" = list())
for(var/datum/round_event_control/event as anything in event_list)
static_data["events"][event_category]["events"][event.type] = event.generate_ui_data()
// Uncategorized shit
static_data["events"]["Uncategorized"] = list("name" = "Uncategorized", "events" = list())
for(var/datum/round_event_control/event as anything in uncategorized)
static_data["events"]["Uncategorized"]["events"][event.type] = event.generate_ui_data()
return static_data
/datum/controller/subsystem/gamemode/ui_state(mob/user)
return GLOB.admin_state
/datum/controller/subsystem/gamemode/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
switch(action)
if("set_storyteller")
// Todo: Replace with tgui_input
var/list/name_list = list()
for(var/storyteller_type in storytellers)
var/datum/storyteller/storyboy = storytellers[storyteller_type]
name_list[storyboy.name] = storyboy.type
var/new_storyteller_name = input(usr, "Choose new storyteller (circumvents voted one):", "Storyteller") as null|anything in name_list
if(!new_storyteller_name)
return
message_admins("[key_name_admin(usr)] has changed the Storyteller to [new_storyteller_name].")
var/new_storyteller_type = name_list[new_storyteller_name]
set_storyteller(new_storyteller_type, TRUE, usr.ckey)
if("halt_storyteller")
halt_storyteller(usr)
if("track_action")
switch(params["action"])
if("set_pnts")
var/track_to_adjust = params["track"]
var/num = tgui_input_number(\
usr, \
"Set [track_to_adjust] track points",
title = "Track points", \
default = event_track_points[track_to_adjust], \
max_value = point_thresholds[track_to_adjust]*5, \
)
if(isnull(num))
return
event_track_points[track_to_adjust] = num
if("force_next")
var/forced_track = params["track"]
force_next_event(forced_track, usr)
if("event_action")
var/datum/scheduled_event/sch_event = get_scheduled_by_event_type(params["type"])
if(isnull(sch_event))
return
switch(params["action"])
if("cancel")
message_admins("[key_name_admin(usr)] cancelled scheduled event [sch_event.event.name].")
log_admin_private("[key_name(usr)] cancelled scheduled event [sch_event.event.name].")
SSgamemode.remove_scheduled_event(sch_event)
if("refund")
message_admins("[key_name_admin(usr)] refunded scheduled event [sch_event.event.name].")
log_admin_private("[key_name(usr)] refunded scheduled event [sch_event.event.name].")
SSgamemode.refund_scheduled_event(sch_event)
if("reschedule")
var/new_schedule = tgui_input_number(usr, "Set time in seconds in which to fire event", "Rescheduling event", 0, 3600, 0)
if(isnull(new_schedule) || QDELETED(sch_event))
return
sch_event.start_time = world.time + (new_schedule SECONDS)
message_admins("[key_name_admin(usr)] rescheduled event [sch_event.event.name] to [new_schedule] seconds.")
log_admin_private("[key_name(usr)] rescheduled event [sch_event.event.name] to [new_schedule] seconds.")
if("fire")
if(!SSticker.HasRoundStarted())
return
message_admins("[key_name_admin(usr)] has fired scheduled event [sch_event.event.name].")
log_admin_private("[key_name(usr)] has fired scheduled event [sch_event.event.name].")
sch_event.try_fire()
if("panel_action")
var/datum/round_event_control/event = get_event_by_track_and_type(params["track"], params["type"])
if(isnull(event))
return
switch(params["action"])
if("fire")
message_admins("[key_name_admin(usr)] has fired event [src.name].")
log_admin_private("[key_name(usr)] has fired event [src.name].")
SSgamemode.TriggerEvent(event)
if("force_next")
message_admins("[key_name_admin(usr)] has forced scheduled event [src.name].")
log_admin_private("[key_name(usr)] has forced scheduled event [src.name].")
SSgamemode.force_event(event)
if("panel_update")
if(storyteller)
storyteller.calculate_weights_all()

View File

@@ -66,31 +66,3 @@
remove_occurence()
event = null
return ..()
/datum/scheduled_event/Topic(href, href_list)
. = ..()
if(QDELETED(src))
return
var/round_started = SSticker.HasRoundStarted()
switch(href_list["action"])
if("cancel")
message_admins("[key_name_admin(usr)] cancelled scheduled event [event.name].")
log_admin_private("[key_name(usr)] cancelled scheduled event [event.name].")
SSgamemode.remove_scheduled_event(src)
if("refund")
message_admins("[key_name_admin(usr)] refunded scheduled event [event.name].")
log_admin_private("[key_name(usr)] refunded scheduled event [event.name].")
SSgamemode.refund_scheduled_event(src)
if("reschedule")
var/new_schedule = input(usr, "New schedule time (in seconds):", "Reschedule Event") as num|null
if(isnull(new_schedule) || QDELETED(src))
return
start_time = world.time + new_schedule * 1 SECONDS
message_admins("[key_name_admin(usr)] rescheduled event [event.name] to [new_schedule] seconds.")
log_admin_private("[key_name(usr)] rescheduled event [event.name] to [new_schedule] seconds.")
if("fire")
if(!round_started)
return
message_admins("[key_name_admin(usr)] has fired scheduled event [event.name].")
log_admin_private("[key_name(usr)] has fired scheduled event [event.name].")
try_fire()

View File

@@ -65,7 +65,11 @@
mode.event_track_points[track] += point_gain
mode.last_point_gains[track] = point_gain
/// Goes through every track of the gamemode and checks if it passes a threshold to buy an event, if does, buys one.
/**
* Goes through every track of the gamemode and checks if it passes a threshold to buy an event, if does, buys one.
*
* Additionally updates static ui data once it's done incase event track data has changed
*/
/datum/storyteller/proc/handle_tracks()
. = FALSE //Has return value for the roundstart loop
var/datum/controller/subsystem/gamemode/mode = SSgamemode
@@ -73,6 +77,7 @@
var/points = mode.event_track_points[track]
if(points >= mode.point_thresholds[track] && find_and_buy_event_from_track(track))
. = TRUE
mode.update_static_data_for_all_viewers()
/// Find and buy a valid event from a track.
/datum/storyteller/proc/find_and_buy_event_from_track(track)
@@ -128,7 +133,9 @@
else
mode.schedule_event(bought_event, (rand(3, 4) MINUTES), total_cost)
/// Calculates the weights of the events from a passed track.
/**
* Calculates the weights of the events from a passed track.
*/
/datum/storyteller/proc/calculate_weights(track)
for(var/datum/round_event_control/event as anything in SSgamemode.event_pools[track])
var/weight_total = event.weight
@@ -144,3 +151,9 @@
weight_total -= event.reoccurence_penalty_multiplier * weight_total * (1 - (event_repetition_multiplier ** occurences))
/// Write it
event.calculated_weight = round(weight_total, 1)
/datum/storyteller/proc/calculate_weights_all()
var/datum/controller/subsystem/gamemode/mode = SSgamemode
for(var/track in mode.event_tracks)
calculate_weights(track)
mode.update_static_data_for_all_viewers()

View File

@@ -0,0 +1,2 @@
ADMIN_VERB(storyteller_panel, R_ADMIN, "Storyteller Panel", "Control panel for the Storyteller.", ADMIN_CATEGORY_GAME)
SSgamemode.ui_interact(usr)

View File

@@ -9275,9 +9275,12 @@
#include "modular_zubbers\code\modules\storyteller\config.dm"
#include "modular_zubbers\code\modules\storyteller\divergency_report.dm"
#include "modular_zubbers\code\modules\storyteller\gamemode.dm"
#include "modular_zubbers\code\modules\storyteller\gamemode_ui.dm"
#include "modular_zubbers\code\modules\storyteller\scheduled_event.dm"
#include "modular_zubbers\code\modules\storyteller\storyteller_vote.dm"
#include "modular_zubbers\code\modules\storyteller\verbs.dm"
#include "modular_zubbers\code\modules\storyteller\_events\_event.dm"
#include "modular_zubbers\code\modules\storyteller\_events\_event_ui.dm"
#include "modular_zubbers\code\modules\storyteller\_events\scrubber_overflow.dm"
#include "modular_zubbers\code\modules\storyteller\event_defines\disabled_event_overrides.dm"
#include "modular_zubbers\code\modules\storyteller\event_defines\crewset\_antagonist_event.dm"

View File

@@ -0,0 +1,470 @@
import { useState } from 'react';
import { Tooltip } from 'tgui-core/components';
import { useBackend } from '../backend';
import {
Box,
Button,
LabeledList,
ProgressBar,
Section,
Stack,
Table,
} from '../components';
import { Window } from '../layouts';
export type Storyteller_Data = {
storyteller_name: string;
storyteller_halt: Boolean;
antag_count: number;
antag_cap: number;
pop_data: Record<string, number>;
tracks_data: Record<string, Storyteller_Track>;
scheduled_data: Record<string, Record<string, string>>;
events: Record<string, Storyteller_Event_Category>;
};
export type Storyteller_Track = {
name: string;
current: number;
max: number;
next: number;
forced: Storyteller_Event;
};
export type Storyteller_Event = {
name: string;
desc: string;
tags: string[];
occurences: number;
occurences_shared: Boolean;
min_pop: number;
start: number;
can_run: Boolean;
weight: number;
weight_raw: number;
track: string;
roundstart: boolean;
};
export type Storyteller_Event_Category = {
name: string;
events: Record<string, Storyteller_Event>;
};
export const ZubbersStoryteller = (props) => {
return (
<Window width={1200} height={680}>
<Window.Content height="100%">
<Stack fill vertical>
<Stack.Item grow>
<ZubbersStorytellerRoundData />
<Stack.Divider />
<ZubbersStorytellerTrackData />
</Stack.Item>
<Stack.Item grow>
<Stack fill vertical>
<Stack.Item>
<ZubbersStorytellerScheduledData />
</Stack.Item>
<Stack.Item grow>
<ZubbersStorytellerEventPanel />
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
export const ZubbersStorytellerRoundData = (props) => {
const { act, data } = useBackend<Storyteller_Data>();
const {
storyteller_name,
storyteller_halt,
pop_data,
antag_cap,
antag_count,
} = data;
return (
<Section
title="Storyteller"
buttons={
<>
<Box inline bold mr={1}>
{storyteller_name}
</Box>
<Button onClick={() => act('set_storyteller')}>
Set Storyteller
</Button>
</>
}
>
<LabeledList>
<LabeledList.Item label="Storyteller status">
<Button
color={storyteller_halt ? 'red' : 'green'}
tooltip={(storyteller_halt ? 'Unhalt' : 'Halt') + ' storyteller'}
onClick={() => act('halt_storyteller')}
textAlign="center"
>
{storyteller_halt ? 'Halted' : 'Running'}
</Button>
</LabeledList.Item>
<LabeledList.Item label="Active Players">
{pop_data['active']} {'('}Head: {pop_data['head']}, Sec:{' '}
{pop_data['sec']}, Eng: {pop_data['eng']}, Med: {pop_data['med']}
{')'}
</LabeledList.Item>
<LabeledList.Item
label="Antag Cap"
tooltip="Amount of antags / The antag cap"
>
<ProgressBar
value={antag_count}
maxValue={antag_cap}
ranges={{
good: [-Infinity, antag_cap],
bad: [antag_cap, Infinity],
}}
>
{antag_count + ' / ' + antag_cap}
</ProgressBar>
</LabeledList.Item>
</LabeledList>
</Section>
);
};
const TRACK_DATA_TRACK_WIDTH = '5%';
const TRACK_DATA_POINT_WIDTH = '22%';
const TRACK_DATA_NEXT_WIDTH = '5%';
const TRACK_DATA_FORCED_WIDTH = '20%';
const TRACK_DATA_ACTIONS_WIDTH = '20%';
export const ZubbersStorytellerTrackData = (props) => {
const { act, data } = useBackend<Storyteller_Data>();
const { tracks_data, storyteller_halt } = data;
return (
<Section title="Tracks">
<Table>
<Table.Row bold>
<Table.Cell width={TRACK_DATA_TRACK_WIDTH}>Track</Table.Cell>
<Table.Cell width={TRACK_DATA_POINT_WIDTH}>Points</Table.Cell>
<Table.Cell width={TRACK_DATA_NEXT_WIDTH} textAlign="center">
Next event
</Table.Cell>
<Table.Cell width={TRACK_DATA_FORCED_WIDTH}>
Next forced event
</Table.Cell>
<Table.Cell width={TRACK_DATA_ACTIONS_WIDTH}>Actions</Table.Cell>
</Table.Row>
<Stack.Divider />
{Object.entries(tracks_data).map(([track, track_data]) => {
const max_points = track_data.max;
const current_points = track_data.current;
const forced = track_data.forced ? track_data.forced : 0;
return (
<Table.Row key={track}>
<Table.Cell>
<Button
tooltip="Edit points"
width="100%"
textAlign="center"
onClick={() =>
act('track_action', { action: 'set_pnts', track: track })
}
>
{track}
</Button>
</Table.Cell>
<Table.Cell>
<ProgressBar
value={current_points}
maxValue={max_points}
ranges={{
good: [-Infinity, max_points],
average: [max_points, Infinity],
}}
>
{current_points + ' / ' + max_points}
{' (' +
Math.floor((current_points * 100) / max_points) +
'%) '}
</ProgressBar>
</Table.Cell>
<Table.Cell textAlign="center">
{storyteller_halt ? 'N/A' : '~' + track_data['next'] + 'min'}
</Table.Cell>
<Table.Cell>{forced ? forced.name : ''}</Table.Cell>
<Table.Cell>
<Button
onClick={() =>
act('track_action', { action: 'force_next', track: track })
}
>
Next Event
</Button>
</Table.Cell>
</Table.Row>
);
})}
</Table>
</Section>
);
};
export const ZubbersStorytellerScheduledData = (props) => {
const { act, data } = useBackend<Storyteller_Data>();
const { scheduled_data } = data;
return (
<Section title="Scheduled events">
<Table>
<Table.Row bold>
<Table.Cell>Name</Table.Cell>
<Table.Cell>Track</Table.Cell>
<Table.Cell>Time</Table.Cell>
<Table.Cell>Actions</Table.Cell>
</Table.Row>
{Object.entries(scheduled_data).map(([event_name, event_data]) => {
const timeNum = Number(event_data['time'])?.toFixed(1);
return (
<Table.Row key={event_name}>
<Table.Cell>{event_name}</Table.Cell>
<Table.Cell>{event_data['track']}</Table.Cell>
<Table.Cell>{timeNum ? timeNum + ' s' : 'Roundstart'}</Table.Cell>
<Table.Cell>
<Button
color="red"
onClick={() =>
act('event_action', {
action: 'cancel',
type: event_data['event_type'],
})
}
>
Cancel
</Button>
<Button
onClick={() =>
act('event_action', {
action: 'refund',
type: event_data['event_type'],
})
}
>
Refund
</Button>
<Button
onClick={() =>
act('event_action', {
action: 'reschedule',
type: event_data['event_type'],
})
}
>
Reschedule
</Button>
<Button
color="green"
onClick={() =>
act('event_action', {
action: 'fire',
type: event_data['event_type'],
})
}
>
Fire
</Button>
</Table.Cell>
</Table.Row>
);
})}
</Table>
</Section>
);
};
const EVENT_PANEL_NAME_WIDTH = '20%';
const EVENT_PANEL_TAGS_WIDTH = '10%';
const EVENT_PANEL_OCCURENCES_WIDTH = '6%';
const EVENT_PANEL_POP_WIDTH = '6%';
const EVENT_PANEL_MINTIME_WIDTH = '6%';
const EVENT_PANEL_CANRUN_WIDTH = '6%';
const EVENT_PANEL_WEIGHT_WIDTH = '10%';
const EVENT_PANEL_ACTIONS_WIDTH = '20%';
export const ZubbersStorytellerEventPanel = (props) => {
const { act, data } = useBackend<Storyteller_Data>();
const { events } = data;
const eventCategoryTabs = Object.values(events);
const [currentEventCategory, setCurrentEventCategory] = useState(
eventCategoryTabs[0],
);
const [showRoundstart, setRounstart] = useState(false);
const eventButtons = () => {
const event_categories = Object.values(events).map((event_category) => {
return (
<Button
key={event_category.name}
selected={event_category === currentEventCategory}
onClick={() => setCurrentEventCategory(event_category)}
>
{event_category.name}
</Button>
);
});
return (
<Stack>
<Button.Checkbox
checked={showRoundstart}
onClick={() => setRounstart((prevValue) => !prevValue)}
>
Roundstart
</Button.Checkbox>
{event_categories}
</Stack>
);
};
return (
<Section
title="Event Panel"
maxHeight="100%"
fill
scrollable
buttons={eventButtons()}
>
<Table>
<Table.Row bold>
<Table.Cell width={EVENT_PANEL_NAME_WIDTH}>Name</Table.Cell>
<Table.Cell width={EVENT_PANEL_TAGS_WIDTH}>Tags</Table.Cell>
<Table.Cell width={EVENT_PANEL_OCCURENCES_WIDTH} textAlign="center">
Occ.
</Table.Cell>
<Table.Cell width={EVENT_PANEL_POP_WIDTH} textAlign="center">
M.Pop
</Table.Cell>
<Table.Cell width={EVENT_PANEL_MINTIME_WIDTH} textAlign="center">
M.Time
</Table.Cell>
<Table.Cell width={EVENT_PANEL_CANRUN_WIDTH} textAlign="center">
Can Run
</Table.Cell>
<Table.Cell width={EVENT_PANEL_WEIGHT_WIDTH} textAlign="center">
Weight (Raw)
</Table.Cell>
<Table.Cell width={EVENT_PANEL_ACTIONS_WIDTH}>Actions</Table.Cell>
</Table.Row>
<ZubbersStorytellerEventPanelCategory
current={currentEventCategory}
roundstart={showRoundstart}
/>
</Table>
</Section>
);
};
type EventPanel_Category_Props = {
current: Storyteller_Event_Category;
roundstart: boolean;
};
export const ZubbersStorytellerEventPanelCategory = (
props: EventPanel_Category_Props,
) => {
const { current, roundstart } = props;
return (
<>
{Object.entries(current.events)
.sort((a, b) => b[1].weight - a[1].weight)
.map(([event_type, event]) => {
if (Boolean(event.roundstart) === roundstart) {
return (
<ZubbersStorytellerEvent
key={event_type}
type={event_type}
event={event}
/>
);
}
})}
</>
);
};
type Event_Props = {
type: string;
event: Storyteller_Event;
};
export const ZubbersStorytellerEvent = (props: Event_Props) => {
const { type, event } = props;
const { act } = useBackend<Storyteller_Data>();
return (
<Table.Row>
<Table.Cell>
<Tooltip content={event.desc}>{event.name}</Tooltip>
</Table.Cell>
<Table.Cell>
{Object.values(event.tags).map((tag) => {
return tag + ' ';
})}
</Table.Cell>
<Table.Cell textAlign="center">
{event.occurences}
{event.occurences_shared ? 'S' : ''}
</Table.Cell>
<Table.Cell textAlign="center">{event.min_pop}</Table.Cell>
<Table.Cell textAlign="center">
{event.start}
{'m.'}
</Table.Cell>
<Table.Cell textAlign="center">{event.can_run ? 'Yes' : 'No'}</Table.Cell>
<Table.Cell textAlign="center">
{event.weight}
{' (' + event.weight_raw + ')'}
</Table.Cell>
<Table.Cell>
<Button
onClick={() =>
act('panel_action', {
action: 'fire',
type: type,
track: event.track,
})
}
>
Fire
</Button>
<Button
onClick={() =>
act('panel_action', {
action: 'schedule',
type: type,
track: event.track,
})
}
>
Schedule
</Button>
<Button
onClick={() =>
act('panel_action', {
action: 'force_next',
type: type,
track: event.track,
})
}
>
Force Next
</Button>
</Table.Cell>
</Table.Row>
);
};