Files
Bubberstation/code/datums/map_config.dm
ArcaneMusic 44f6846306 Adds structural weakpoints that can spawn on station (#94839)
## About The Pull Request

This PR was originally described to me by @AnturK and I implemented a
few elements of it to the best of my ability, then I promptly forgot
about it for the last 2 months.

This PR adds in randomly spawning structural weakpoints, using much of
the same mechanics of spawning in hidden satchels. If identified using a
T-ray scanner, they can be fixed using a welder by crew. More
excitingly, if a weakpoint is subject to an explosion, it will propagate
a crack, using a random line of turfs, then calling `ex_act()` on each
one with a small callback chain. Finally, cracks will form additional
weakpoints if caused this way, though are almost certainly going to be
noticeable, unlike before where they can spawn undertile.

With this addition, I've added a negative station trait, that will
increase the number of structural weakpoints that will spawn in a given
round. In a normal round, 3 weakpoints will spawn on the station, where
with the station trait, 4-8 weakpoints will spawn across the map.

Weakpoints come in 1 flavor practically, with a larger subtype being
spawnable by admins. The vars for weakpoints also happen to be fully
var-edit-able for admin purposes, including the explosion intensity that
can cause the weakpoint to trigger, the number of sub-cracks, if
additional weakpoints can spawn, etc.

You may also seal weakpoints using sticky tape.

## Why It's Good For The Game

Weakpoints feel like an interesting modifier to a map, where if caught
early can prevent a bad situation becoming worse. Otherwise, weakpoints
could very quickly cause chaos on the map, but despite their
description, aren't capable of causing **too** much destruction that
they can cause a shuttle call unless they're being manipulated by
traitors or antagonists. Though low odds, the idea of a player
welder-bombing a door or an IED going off related to the current
situation in a round, being exacerbated by pure chance due to being near
an IED seems really, REALLY funny.

## Changelog

🆑
add: Stations can now spawn with Structural weakpoints. If they're
subject to an explosion, they'll crack open a tear in the station's
turfs. If you spot one, make sure to weld it or seal it with sticky tape
to fix one. Or not!
add: Adds a negative station trait that greatly increases the number of
weakpoints that can spawn on station.
/🆑

---------

Co-authored-by: san7890 <the@san7890.com>
Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
2026-02-06 16:12:31 -05:00

284 lines
9.0 KiB
Plaintext

//This file is used to contain unique properties of every map, and how we wish to alter them on a per-map basis.
//Use JSON files that match the datum layout and you should be set from there.
//Right now, we default to MetaStation to ensure something does indeed load by default.
// -san7890 (with regards to Cyberboss)
/datum/map_config
// Metadata
var/config_filename = "_maps/metastation.json"
var/defaulted = TRUE // set to FALSE by LoadConfig() succeeding
// Config from maps.txt
var/config_max_users = 0
var/config_min_users = 0
var/voteweight = 1
var/votable = FALSE
///A URL linking to a place for people to send feedback about this map.
var/feedback_link
/// The URL given by config directing you to the webmap.
var/mapping_url
// Config actually from the JSON - should default to Meta
var/map_name = "MetaStation"
var/map_path = "map_files/MetaStation"
var/map_file = "MetaStation.dmm"
var/traits = null
var/space_ruin_levels = DEFAULT_SPACE_RUIN_LEVELS
var/space_empty_levels = DEFAULT_SPACE_EMPTY_LEVELS
/// Boolean that tells us if this is a planetary station. (like IceBoxStation)
var/planetary = FALSE
/// How many z's to generate around a planetary station
var/wilderness_levels = 0
/// Directory to the wilderness area we can spawn in
var/wilderness_directory
/// Index of map names (inside wilderness_directory) with the amount to spawn. ("ice_planes" = 1) for one ice spawn
var/list/maps_to_spawn = list()
///The type of mining Z-level that should be loaded.
var/minetype = MINETYPE_LAVALAND
///If no minetype is set, this will be the blacklist file used
var/blacklist_file
var/allow_custom_shuttles = TRUE
var/shuttles = list(
"cargo" = "cargo_box",
"ferry" = "ferry_fancy",
"whiteship" = "whiteship_meta",
"emergency" = "emergency_meta",
)
/// Dictionary of job sub-typepath to template changes dictionary
var/job_changes = list()
/// List of additional areas that count as a part of the library
var/library_areas = list()
/// Boolean - if TRUE, the "Up" and "Down" traits are automatically distributed to the map's z-levels. If FALSE; they're set via JSON.
var/height_autosetup = TRUE
/// Boolean - if TRUE, players spawn with grappling hooks in their bags
var/give_players_hooks = FALSE
/// List of unit tests that are skipped when running this map
var/list/skipped_tests
/// Boolean that tells SSmapping to load all away missions in the codebase.
var/load_all_away_missions = FALSE
/// Number of additional weakpoints to spawn for SSminor_mapping
var/bonus_weakpoints = 0
/**
* Proc that simply loads the default map config, which should always be functional.
*/
/proc/load_default_map_config()
return new /datum/map_config
/**
* Proc handling the loading of map configs. Will return the default map config using [/proc/load_default_map_config] if the loading of said file fails for any reason whatsoever, so we always have a working map for the server to run.
* Arguments:
* * filename - Name of the config file for the map we want to load. The .json file extension is added during the proc, so do not specify filenames with the extension.
* * directory - Name of the directory containing our .json - Must be in MAP_DIRECTORY_WHITELIST. We default this to MAP_DIRECTORY_MAPS as it will likely be the most common usecase. If no filename is set, we ignore this.
* * error_if_missing - Bool that says whether failing to load the config for the map will be logged in log_world or not as it's passed to LoadConfig().
*
* Returns the config for the map to load.
*/
/proc/load_map_config(filename = null, directory = null, error_if_missing = TRUE)
var/datum/map_config/configuring_map = load_default_map_config()
if(filename) // If none is specified, then go to look for next_map.json, for map rotation purposes.
//Default to MAP_DIRECTORY_MAPS if no directory is passed
if(directory)
if(!(directory in MAP_DIRECTORY_WHITELIST))
log_world("map directory not in whitelist: [directory] for map [filename]")
return configuring_map
else
directory = MAP_DIRECTORY_MAPS
filename = "[directory]/[filename].json"
else
filename = PATH_TO_NEXT_MAP_JSON
if (!configuring_map.LoadConfig(filename, error_if_missing))
qdel(configuring_map)
return load_default_map_config()
return configuring_map
#define CHECK_EXISTS(X) if(!istext(json[X])) { log_world("[##X] missing from json!"); return; }
/datum/map_config/proc/LoadConfig(filename, error_if_missing)
if(!fexists(filename))
if(error_if_missing)
log_world("map_config not found: [filename]")
return
var/json = file(filename)
if(!json)
log_world("Could not open map_config: [filename]")
return
json = file2text(json)
if(!json)
log_world("map_config is not text: [filename]")
return
json = json_decode(json)
if(!json)
log_world("map_config is not json: [filename]")
return
config_filename = filename
if(!json["version"])
log_world("map_config missing version!")
return
if(json["version"] != MAP_CURRENT_VERSION)
log_world("map_config has invalid version [json["version"]]!")
return
CHECK_EXISTS("map_name")
map_name = json["map_name"]
CHECK_EXISTS("map_path")
map_path = json["map_path"]
map_file = json["map_file"]
// "map_file": "MetaStation.dmm"
if (istext(map_file))
if (!fexists("_maps/[map_path]/[map_file]"))
log_world("Map file ([map_path]/[map_file]) does not exist!")
return
// "map_file": ["Lower.dmm", "Upper.dmm"]
else if (islist(map_file))
for (var/file in map_file)
if (!fexists("_maps/[map_path]/[file]"))
log_world("Map file ([map_path]/[file]) does not exist!")
return
else
log_world("map_file missing from json!")
return
if (islist(json["shuttles"]))
var/list/L = json["shuttles"]
for(var/key in L)
var/value = L[key]
shuttles[key] = value
else if ("shuttles" in json)
log_world("map_config shuttles is not a list!")
return
traits = json["traits"]
// "traits": [{"Linkage": "Cross"}, {"Space Ruins": true}]
if (islist(traits))
// "Station" is set by default, but it's assumed if you're setting
// traits you want to customize which level is cross-linked
for (var/level in traits)
if (!(ZTRAIT_STATION in level))
level[ZTRAIT_STATION] = TRUE
// "traits": null or absent -> default
else if (!isnull(traits))
log_world("map_config traits is not a list!")
return
var/temp = json["space_ruin_levels"]
if (isnum(temp))
space_ruin_levels = temp
else if (!isnull(temp))
log_world("map_config space_ruin_levels is not a number!")
return
temp = json["space_empty_levels"]
if (isnum(temp))
space_empty_levels = temp
else if (!isnull(temp))
log_world("map_config space_empty_levels is not a number!")
return
temp = json["wilderness_levels"]
if (isnum(temp))
wilderness_levels = temp
else if (!isnull(temp))
log_world("map_config wilderness_levels is not a number!")
return
if ("minetype" in json)
minetype = json["minetype"]
if ("planetary" in json)
planetary = json["planetary"]
if ("blacklist_file" in json)
blacklist_file = json["blacklist_file"]
if ("load_all_away_missions" in json)
load_all_away_missions = json["load_all_away_missions"]
if ("give_players_hooks" in json)
give_players_hooks = json["give_players_hooks"]
if ("bonus_weakpoints" in json)
bonus_weakpoints = json["bonus_weakpoints"]
allow_custom_shuttles = json["allow_custom_shuttles"] != FALSE
if ("job_changes" in json)
if(!islist(json["job_changes"]))
log_world("map_config \"job_changes\" field is missing or invalid!")
return
job_changes = json["job_changes"]
if("library_areas" in json)
if(!islist(json["library_areas"]))
log_world("map_config \"library_areas\" field is missing or invalid!")
return
for(var/path_as_text in json["library_areas"])
var/path = text2path(path_as_text)
if(!ispath(path, /area))
stack_trace("Invalid path in mapping config for additional library areas: \[[path_as_text]\]")
continue
library_areas += path
if ("height_autosetup" in json)
height_autosetup = json["height_autosetup"]
var/list/wilderness = json["wilderness"]
// If we got wilderness levels, fetch them from the config
if (islist(wilderness))
wilderness_directory = wilderness["directory"]
wilderness.Remove("directory")
// Just pick and take based on weight
for(var/i in 1 to wilderness_levels)
maps_to_spawn += pick_weight_take(wilderness)
shuffle(maps_to_spawn)
#ifdef UNIT_TESTS
// Check for unit tests to skip, no reason to check these if we're not running tests
for(var/path_as_text in json["ignored_unit_tests"])
var/path_real = text2path(path_as_text)
if(!ispath(path_real, /datum/unit_test))
stack_trace("Invalid path in mapping config for ignored unit tests: \[[path_as_text]\]")
continue
LAZYADD(skipped_tests, path_real)
#endif
defaulted = FALSE
return json
#undef CHECK_EXISTS
/datum/map_config/proc/GetFullMapPaths()
if (istext(map_file))
return list("_maps/[map_path]/[map_file]")
. = list()
for (var/file in map_file)
. += "_maps/[map_path]/[file]"
/datum/map_config/proc/MakeNextMap()
return config_filename == PATH_TO_NEXT_MAP_JSON || fcopy(config_filename, PATH_TO_NEXT_MAP_JSON)