Files
Aurora.3/code/controllers/subsystems/processing/odyssey.dm
smellie bfb076eea5 Uncharted Space (#20937)
Adds 3 Crescent Expanse regions due to how large a region it is:
- Crescent Expanse (West): Nralakk and Solarian heavy region.
- Crescent Expanse (East): Coalition and Solarian heavy region.
- Crescent Expanse (Uncharted): Deep Crescent expanse, independents and
certain surveyors only.

Also adds a Lemurian Sea (Uncharted) sector if we ever go there.

Adds a `ccia_link` variable for sectors. Only usage is to block outbound
faxes to Central Command. For uncharted sectors. EBS remains an option
due to necessary CCIA functions (eg. scuttling), per Bear's wishes.

Adds `low_supply` variants of most vending machines and dispensers. For
later use, none mapped in.

Removes the `TEMPLATE_FLAG_SPAWN_GUARANTEED` flag from the sensors
array, because it meant it wasn't affected by `sectors_blacklist`. The
sensors array isn't so important it needs to be in every sector anyway.

Removes Burzsia from the `ALL_DANGEROUS_SECTORS` list, as the crisis has
passed and humanitarian relief efforts completed.

Adds the `ALL_EVENT_ONLY_SECTORS` for limited usage during event
arcs/similar where canon odysseys or certain thirdparties may infringe
upon an arc's narrative. Allows selective enabling/disabling of canon
odysseys or third parties for said arcs. Not intended for liberal
application.

### Asset Licenses
The following assets that **have not** been created by myself are
included in this PR:
| Path | Original Author | License |
| --- | --- | --- |
sound/music/lobby/crescent_expanse/crescent_expanse_1.ogg | "Lüüü -
Weedance" by Lüüü, Obtained from
https://myprivateunderground.bandcamp.com/track/lu-u-u-weedance | CC
BY-NC-SA 3.0.
sound/music/lobby/crescent_expanse/crescent_expanse_2.ogg | "Little
Bradley - Sunset Drive" by Little Bradley, Obtained from
https://myprivateunderground.bandcamp.com/track/little-bradley-sunset-drive
| CC BY-NC 3.0
sound/music/lobby/dangerous_space/dangerous_space_1.ogg | "You Can't
Kill The Boogeyman" by Karl Casey. Obtained from
https://karlcasey.bandcamp.com/track/you-can-t-kill-the-boogeyman | CC
BY 3.0
sound/music/lobby/dangerous_space_2.ogg | "Prison Planet" by Karl Casey,
Obtained from https://karlcasey.bandcamp.com/track/prison-planet | CC BY
3.0
sound/music/lobby/lights_edge/lights_edge_1.ogg | "Is Anyone Left?" by
Karl Casey. Obtained from
https://karlcasey.bandcamp.com/track/is-anyone-left | CC BY 3.0
sound/music/lobby/lights_edge/lights_edge_2.ogg | "Running From The
Wendigo" by Karl Casey. Obtained from
https://karlcasey.bandcamp.com/track/running-from-the-wendigo | CC BY
3.0
2025-07-12 12:12:40 +00:00

264 lines
10 KiB
Plaintext

SUBSYSTEM_DEF(odyssey)
name = "Odyssey"
init_order = INIT_ORDER_ODYSSEY
runlevels = RUNLEVELS_PLAYING
can_fire = FALSE //We process only if we are running an odyssey scenario, this is set to TRUE by `pick_odyssey()`
/// 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()
var/list/possible_station_levels = SSmapping.levels_by_all_traits(list(ZTRAIT_STATION))
main_map = GLOB.map_sectors["[pick(possible_station_levels)]"]
// Now that we actually have an odyssey, the subsystem can fire!
can_fire = TRUE
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
//Setting the scenario_type variable for use here in UI info and chat notices.
if(!length(scenario.possible_scenario_types))
scenario.scenario_type = SCENARIO_TYPE_NONCANON
else if(SSatlas.current_sector in ALL_EVENT_ONLY_SECTORS) // If we are in an exclusive event area for an arc (EG. The Horizon finds itself isolated and alone), we may not want canon odysseys spawning.
scenario.scenario_type = SCENARIO_TYPE_NONCANON // Noncanon odysseys are fine though!
else
scenario.scenario_type = pick(scenario.possible_scenario_types)
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