Adds the Odyssey gamemode. (#18972)

https://forums.aurorastation.org/topic/20198-mission-briefing-auroras-gamemode-revolution

To-do:

- [x] Finish storyteller verbs.
- [x] Storyteller landmarks.
- [x] Proper storyteller spawning. Right now the gamemode system is
happy with just picking one storyteller and no actors.
- [x] Antagonist whitelists code.
- [x] Adding the Storyteller whitelist.
- [x] Mission map loading code.
- [x] Map in a bunch of missions.
- [ ] Storyteller adminhelps.

---------

Co-authored-by: Matt Atlas <liermattia@gmail.com>
Co-authored-by: DreamySkrell <>
This commit is contained in:
Matt Atlas
2024-11-26 20:18:43 +00:00
committed by GitHub
co-authored by Matt Atlas DreamySkrell <>
parent e1f5de6298
commit 1da20ad33f
194 changed files with 242505 additions and 1904 deletions
+2 -2
View File
@@ -197,7 +197,7 @@ SUBSYSTEM_DEF(ghostroles)
if("jump_to")
var/spawner_id = params["spawner_id"]
var/datum/ghostspawner/human/spawner = spawners[spawner_id]
var/mob/abstract/observer/observer = usr
var/mob/abstract/ghost/observer/observer = usr
if(spawner && istype(observer) && spawner.can_jump_to(observer))
var/atom/turf = spawner.select_spawnlocation(FALSE)
if(isturf(turf))
@@ -208,7 +208,7 @@ SUBSYSTEM_DEF(ghostroles)
var/spawner_id = params["spawner_id"]
var/spawned_mob_name = params["spawned_mob_name"]
var/datum/ghostspawner/human/spawner = spawners[spawner_id]
var/mob/abstract/observer/observer = usr
var/mob/abstract/ghost/observer/observer = usr
if(istype(observer) && spawner.can_jump_to(observer) && spawner && LAZYLEN(spawner.spawned_mobs))
for(var/datum/weakref/mob_ref in spawner.spawned_mobs)
var/mob/spawned_mob = mob_ref.resolve()
@@ -37,6 +37,11 @@ SUBSYSTEM_DEF(misc_late)
if (GLOB.config.use_forumuser_api)
update_admins_from_api(TRUE)
// Load outfits here so that the verb isn't laggy as balls.
for(var/outfit_type in subtypesof(/obj/outfit))
var/obj/outfit/new_outfit = new outfit_type()
GLOB.outfit_cache[new_outfit.name] = new_outfit
return SS_INIT_SUCCESS
/proc/sorted_add_area(area/A)
+2 -2
View File
@@ -181,7 +181,7 @@ SUBSYSTEM_DEF(pai)
for(var/datum/paiCandidate/c in SSpai.pai_candidates)
if(c.ready)
var/found = 0
for(var/mob/abstract/observer/o in GLOB.player_list)
for(var/mob/abstract/ghost/observer/o in GLOB.player_list)
if(o.key == c.key && o.MayRespawn())
found = 1
if(found)
@@ -293,7 +293,7 @@ SUBSYSTEM_DEF(pai)
/datum/controller/subsystem/pai/proc/requestRecruits(mob/user)
inquirer = user
for(var/mob/abstract/observer/O in GLOB.player_list)
for(var/mob/abstract/ghost/observer/O in GLOB.player_list)
if(!O.MayRespawn())
continue
if(jobban_isbanned(O, "pAI"))
@@ -0,0 +1,250 @@
SUBSYSTEM_DEF(odyssey)
name = "Odyssey"
init_order = INIT_ORDER_ODYSSEY
runlevels = RUNLEVELS_PLAYING
/// The selected scenario singleton.
var/singleton/scenario/scenario
/// The z-levels that the odyssey takes place in.
var/list/scenario_zlevels = list()
/// This is the site the odyssey takes place on. If null, then the mission takes place on a non-site zlevel.
/// Should only be changed through set_scenario_site().
var/datum/map_template/ruin/away_site/scenario_site
/// A list of currently spawned actors for easy access.
var/list/mob/living/carbon/human/actors
/// A list of currently spawned storytellers for easy access.
var/list/mob/abstract/ghost/storyteller/storytellers
/// Whether or not we've sent the odyssey's roundstart report yet.
var/has_sent_roundstart_announcement = FALSE
/// The current station map's overmap object. We keep it here for convenience.
var/obj/effect/overmap/visitable/ship/main_map
/// If ships can dock on the Odyssey site. True by default, edited by the scenario.
var/site_landing_restricted = TRUE
/datum/controller/subsystem/odyssey/Initialize()
return SS_INIT_SUCCESS
/datum/controller/subsystem/odyssey/Recover()
scenario = SSodyssey.scenario
scenario_site = SSodyssey.scenario_site
scenario_zlevels = SSodyssey.scenario_zlevels
actors = SSodyssey.actors
storytellers = SSodyssey.storytellers
/datum/controller/subsystem/odyssey/fire()
if(!has_sent_roundstart_announcement)
// First of all, notify the Horizon.
addtimer(CALLBACK(scenario, TYPE_PROC_REF(/singleton/scenario, send_main_map_message), main_map), rand(4 MINUTES, 6 MINUTES))
addtimer(CALLBACK(scenario, TYPE_PROC_REF(/singleton/scenario, unrestrict_landing_and_message_horizon), main_map), 40 MINUTES)
var/obj/effect/overmap/odyssey_site = get_odyssey_overmap_effect()
if(odyssey_site)
// Next, notify the offships - these announcements can happen earlier to potentially give them a bit of an edge in reaching the objective area.
addtimer(CALLBACK(scenario, TYPE_PROC_REF(/singleton/scenario, notify_offships), odyssey_site), rand(20 MINUTES, 50 MINUTES))
has_sent_roundstart_announcement = TRUE
/**
* Returns the current scenario's overmap effect. Returns null if there isn't any.
*/
/datum/controller/subsystem/odyssey/proc/get_odyssey_overmap_effect()
var/obj/effect/overmap/odyssey_site
for(var/z in scenario_zlevels)
odyssey_site = GLOB.map_sectors["[z]"]
if(!istype(odyssey_site))
continue
return odyssey_site
/**
* Picks a random odyssey while keeping in mind sector requirements.
* If successful, makes the SS start firing.
*/
/datum/controller/subsystem/odyssey/proc/pick_odyssey()
var/list/singleton/scenario/all_scenarios = GET_SINGLETON_SUBTYPE_LIST(/singleton/scenario)
var/list/possible_scenarios = list()
for(var/singleton/scenario/S as anything in all_scenarios)
if((SSatlas.current_sector.name in S.sector_whitelist) || !length(S.sector_whitelist))
possible_scenarios[S] = S.weight
if(!length(possible_scenarios))
log_subsystem_odyssey("CRITICAL ERROR: No available odyssey for sector [SSatlas.current_sector.name]!")
log_and_message_admins(SPAN_DANGER(FONT_HUGE("CRITICAL ERROR: NO SITUATIONS ARE AVAILABLE FOR THIS SECTOR!")))
return FALSE
scenario = pickweight(possible_scenarios)
setup_scenario_variables()
// Now that we actually have an odyssey, the subsystem can fire!
var/list/possible_station_levels = SSmapping.levels_by_all_traits(list(ZTRAIT_STATION))
main_map = GLOB.map_sectors["[pick(possible_station_levels)]"]
return TRUE
/**
* This proc overrides the gamemode variables for the amount of actors and takes care of overriding any other variables the scenario might set on external things.
* Note that Storytellers spawn through a ghost role.
*/
/datum/controller/subsystem/odyssey/proc/setup_scenario_variables()
var/datum/game_mode/odyssey/ody_gamemode = GLOB.gamemode_cache["odyssey"]
if(scenario)
ody_gamemode.required_players = scenario.min_player_amount
ody_gamemode.required_enemies = scenario.min_actor_amount
site_landing_restricted = scenario.site_landing_restricted
/**
* This is the proc that should be used to set the odyssey away site.
*/
/datum/controller/subsystem/odyssey/proc/set_scenario_site(datum/map_template/ruin/away_site/site)
if(!istype(site))
return
scenario_site = site
/**
* Adds an actor to the subsystem actor list.
*/
/datum/controller/subsystem/odyssey/proc/add_actor(mob/living/carbon/human/H)
LAZYDISTINCTADD(actors, H)
/**
* Adds a storyteller to the subsystem storyteller list.
*/
/datum/controller/subsystem/odyssey/proc/add_storyteller(mob/abstract/ghost/storyteller/S)
LAZYDISTINCTADD(storytellers, S)
/**
* Removes an actor from the subsystem actor list.
*/
/datum/controller/subsystem/odyssey/proc/remove_actor(mob/living/carbon/human/H)
LAZYREMOVE(actors, H)
/**
* Removes a storyteller from the subsystem storyteller list.
*/
/datum/controller/subsystem/odyssey/proc/remove_storyteller(mob/abstract/ghost/storyteller/S)
LAZYREMOVE(storytellers, S)
/datum/controller/subsystem/odyssey/ui_state()
return GLOB.always_state
/datum/controller/subsystem/odyssey/ui_interact(mob/user, datum/tgui/ui)
. = ..()
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "OdysseyPanel", "Odyssey Panel")
ui.open()
/datum/controller/subsystem/odyssey/ui_data(mob/user)
var/list/data = list()
if(scenario)
data["scenario_name"] = SSodyssey.scenario.name
data["scenario_desc"] = SSodyssey.scenario.desc
data["scenario_canonicity"] = SSodyssey.scenario.scenario_type == SCENARIO_TYPE_CANON ? "Canon" : "Non-Canon"
data["is_storyteller"] = isstoryteller(user) || check_rights(R_ADMIN, FALSE, user)
if(length(scenario.roles))
data["scenario_roles"] = list()
for(var/role_singleton in scenario.roles)
var/singleton/role/scenario_role = GET_SINGLETON(role_singleton)
data["scenario_roles"] += list(
list(
"name" = scenario_role.name,
"desc" = scenario_role.desc,
"outfit" = "[scenario_role.outfit]",
"type" = scenario_role.type
)
)
return data
/datum/controller/subsystem/odyssey/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
var/mob/odyssey_user = ui.user
if(!ismob(odyssey_user))
return
var/is_admin = check_rights(R_ADMIN, FALSE, odyssey_user)
switch(action)
if("equip_outfit")
if(!ishuman(odyssey_user))
return
var/mob/living/carbon/human/player = odyssey_user
if(player.incapacitated())
return
if(!((player.z in SSodyssey.scenario_zlevels) || (isAdminLevel(player.z))))
to_chat(player, SPAN_WARNING("You can only equip outfits on the odyssey scenario z-level, or the actor prep area!"))
return
var/outfit_type = text2path(params["outfit_type"])
if(ispath(outfit_type, /obj/outfit))
player.delete_inventory(TRUE)
player.preEquipOutfit(outfit_type, FALSE)
player.equipOutfit(outfit_type, FALSE)
return TRUE
if("edit_scenario_name")
if(!isstoryteller(odyssey_user) && !is_admin)
return
var/new_scenario_name = tgui_input_text(usr, "Insert the new name for your scenario. Remember that this will be visible for anyone in the Stat Panel.", "Odyssey Panel", max_length = MAX_NAME_LEN)
if(!new_scenario_name)
return
log_and_message_admins("has changed the scenario name from [SSodyssey.scenario.name] to [new_scenario_name]", odyssey_user)
SSodyssey.scenario.name = new_scenario_name
return TRUE
if("edit_scenario_desc")
if(!isstoryteller(odyssey_user) && !is_admin)
return
var/new_scenario_desc = tgui_input_text(odyssey_user, "Insert the new description for your scenario. This is visible only in the Odyssey Panel.", "Odyssey Panel", max_length = MAX_MESSAGE_LEN)
if(!new_scenario_desc)
return
log_and_message_admins("has changed the scenario description", odyssey_user)
SSodyssey.scenario.desc = new_scenario_desc
return TRUE
if("edit_role")
if(!isstoryteller(odyssey_user) && !is_admin)
return
var/role_path = text2path(params["role_type"])
if(!role_path || !ispath(role_path, /singleton/role))
to_chat(odyssey_user, SPAN_WARNING("Invalid or inexisting role!"))
return
var/singleton/role/role_to_edit = GET_SINGLETON(role_path)
var/editing_name = params["new_name"]
var/editing_desc = params["new_desc"]
var/editing_outfit = params["edit_outfit"]
if(editing_name)
var/new_name = tgui_input_text(odyssey_user, "Insert the new name for this role.", "Odyssey Panel", max_length = MAX_NAME_LEN)
if(new_name)
log_and_message_admins("has changed the [role_to_edit.name] role's name to [new_name]", odyssey_user)
role_to_edit.name = new_name
return TRUE
if(editing_desc)
var/new_desc = tgui_input_text(odyssey_user, "Insert the new description for this role.", "Odyssey Panel", max_length = MAX_MESSAGE_LEN)
if(new_desc)
log_and_message_admins("has changed the [role_to_edit.name] role's description", odyssey_user)
role_to_edit.desc = new_desc
return TRUE
if(editing_outfit)
var/chosen_outfit = tgui_input_list(odyssey_user, "Select the new outfit for this role.", "Odyssey Panel", GLOB.outfit_cache)
if(chosen_outfit)
var/obj/outfit/new_outfit = GLOB.outfit_cache[chosen_outfit]
if(new_outfit)
log_and_message_admins("has changed the [role_to_edit.name] role's outfit from [role_to_edit.outfit] to [new_outfit]", odyssey_user)
role_to_edit.outfit = new_outfit.type
return TRUE
+3 -3
View File
@@ -203,7 +203,7 @@ SUBSYSTEM_DEF(records)
return GLOB.always_state
/datum/controller/subsystem/records/ui_status(mob/user, datum/ui_state/state)
return (isnewplayer(user) || isobserver(user) || issilicon(user)) ? UI_INTERACTIVE : UI_CLOSE
return (isnewplayer(user) || isghost(user) || issilicon(user)) ? UI_INTERACTIVE : UI_CLOSE
/datum/controller/subsystem/records/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
@@ -211,7 +211,7 @@ SUBSYSTEM_DEF(records)
return
if(action == "follow")
var/mob/abstract/observer/O = usr
var/mob/abstract/ghost/O = usr
if(istype(O))
for(var/mob/living/M in GLOB.human_mob_list)
if(istype(M) && M.real_name == params["name"])
@@ -222,7 +222,7 @@ SUBSYSTEM_DEF(records)
/datum/controller/subsystem/records/ui_static_data(mob/user)
var/list/data = list()
data["manifest"] = SSrecords.get_manifest_list()
data["allow_follow"] = isobserver(user)
data["allow_follow"] = isghost(user)
return data
/datum/controller/subsystem/records/proc/open_manifest_tgui(mob/user, datum/tgui/ui)
+4
View File
@@ -41,6 +41,10 @@ SUBSYSTEM_DEF(statpanels)
if(eta_status)
global_data += eta_status
if(SSodyssey && SSodyssey.scenario)
global_data += "Odyssey: [SSodyssey.scenario.name]"
global_data += "Storytellers: [length(SSodyssey.storytellers)]"
src.currentrun = GLOB.clients.Copy()
mc_data = null
+14 -2
View File
@@ -64,6 +64,9 @@ var/datum/controller/subsystem/ticker/SSticker
var/list/roundstart_callbacks
/// Used to prevent players from readying or unreadying, such as for Odyssey's setup.
var/prevent_unready = FALSE
// Pre-game ready menu handling
var/total_players = 0
var/total_players_ready = 0
@@ -262,8 +265,8 @@ var/datum/controller/subsystem/ticker/SSticker
else
to_chat(Player, SPAN_NOTICE(SPAN_BOLD("You missed the crew transfer after the events on [station_name()] as [Player.real_name].")))
else
if(istype(Player,/mob/abstract/observer))
var/mob/abstract/observer/O = Player
if(isobserver(Player))
var/mob/abstract/ghost/observer/O = Player
if(!O.started_as_observer)
to_chat(Player, SPAN_WARNING(SPAN_BOLD("You did not survive the events on [station_name()]...")))
else
@@ -533,6 +536,15 @@ var/datum/controller/subsystem/ticker/SSticker
to_world("<span class='danger'>Serious error in mode setup!</span> Reverting to pre-game lobby.")
return SETUP_REVOTE
// This proc must return TRUE on success.
if(!mode.pre_game_setup())
current_state = GAME_STATE_PREGAME
to_world("<span class='danger'>Round start pre-game setup failed! Reverting to pre-game lobby.")
prevent_unready = FALSE
return SETUP_REVOTE
prevent_unready = FALSE
SSjobs.ResetOccupations()
src.mode.create_antagonists()
src.mode.pre_setup()