Files
FenodyreeAv cf585ef505 Ship combat rework (#22566)
Makes ship combat significantly more destructive (and a bit deadlier),
especially with non-explosive projectiles.

**Additions:**
- A shell that penetrates a solid (dense) structure will cause spalling.
This spall does 2-30 damage, depending on range and has a high embed
chance. These embedded chunks are large enough to be ripped out by hand.
(But you probably shouldn't). The pilot's suit has 30 ballistic armor,
so a prepared bridge crew can survive a lot of spall.
- Adds a gun that shoots any ship weapon that admins can spawn. Useful
for testing, or for adminbus where they don't want to have to spawn a
whole overmap ship.
- Made several structures destructible, vending machines, computer
frames, grilles (these were just bugged) and watertanks (and their
children). These structures could just block infinite shots and were
common enough to serve as effective armor for the horizon. These now
spawn shrapnel (of their material if it's set), in addition to sheets of
steel when destroyed.
- Added support for negative maim chance. This allows chosen projectiles
to be prevented from decapitating. No more accidentally gibbing the
bridge crew's heads. (Unless they get hit with something really really
big).

**Balance:**
- All non-explosive anti ship rounds got significant buffs. Typically
2-3x Anti-Material numbers.
- Armour piercing anti-ship rounds got massively increased penetration
stats. Typically 4-8 from 1-2.
- Doors now get destroyed in less hits, if they are hit by anti-material
projectiles. Hits to destroy -= Antimaterial / 2
- Francisca frag shells now shoot less fragments but deal more damage.
394 projectiles for every shot was a bit laggy, now it's only 280.

**Shields:**
Shields now take more power. 13-14 megawatts for a 10 strength shield.
Just inside the ship's generation capacity with both the INDRA and
Supermatter running.

Instead of deleting any projectile they are hit by, shields now have
different failure modes.
- Firstly, a piercing projectile has it's penetration reduced by the
shield strength. Eg A shot that would go through 10 walls, only
penetrates 4 after going through a 6 strength shield.
- Secondly, a projectile has it's damage multiplied by 1 - shield
strength / 10, then has shield strength subtracted from it's damage. Eg.
A damage 100 projectile, hitting a 5 strength shield, has it's damaged
reduced to 50, (100 * 0.5), then to 45. (50-5)
- Thirdly, an explosive projectile (that does not penetrate per the
first step) does not explode, but instead deals it's full damage to the
shield.

Hits to the shield now also damage the field itself, not just the shield
tile. As such, a very big hit on the shield can bring the whole thing
down, requiring an engineer to reset it.

Shields can now be upgraded with research components. Additionally,
shield generators can now support more than one capacitor. The
roundstart capacitor on the Horizon is only good enough to support a 6
strength shield.
The upgrades increase efficiency, strength loss over time and charge
speed.

**Overmap Targetting**
Previously, shells would spawn within 20 tiles of their target and aim
directly forwards. This was bugged for shots with burst, which would
instead all aim inwards and mostly miss. This is the reason the Grauwolf
(in addition to the explosion bug below) and Fransisca felt so useless,
only one of every shell in a burst would hit.

Shots now spawn at the map edge, based on the orientation of the
projectile and ship. Diagonals are now accounted for.
Shots are then aimed directly at the target selected on the console,
with 3 degrees of dispersion.

**Bugfixes:**
- Fixes lots of explosions that didn't pass a turf to the explosions
subsystem. This caused the explosion not to happen if the thing hit got
qdeled before the subsystem could get the turf to spawn the explosion
on.
- Embedded shrapnel no longer gets the initial projectile name, this
prevents it getting named shrapnelshrapnel.
- Fixes a bunch of projectile piercing bugs, mostly with grilles and
windows. Shots now go past window frames without hitting them.
- Fixes shields not having a click delay when attacked.

---------

Signed-off-by: FenodyreeAv <fenodyree.av@gmail.com>
Co-authored-by: Matt Atlas <mattiathebest2000@hotmail.it>
2026-06-03 16:52:13 +00:00

366 lines
10 KiB
Plaintext

// This file controls round-start runtime maploading.
SUBSYSTEM_DEF(atlas)
name = "Atlas"
flags = SS_NO_FIRE
init_order = INIT_ORDER_MAPPING
init_stage = INITSTAGE_EARLY
/// Whatever map is currently loaded. Null until SSatlas Initialize() starts.
var/datum/map/current_map
var/list/known_maps = list()
var/dmm_suite/maploader
var/list/mapload_callbacks = list()
/// If set, SSatlas will forcibly load this map. If the map does not exist, mapload will fail and SSatlas will panic.
var/map_override
var/list/spawn_locations = list()
var/datum/space_sector/current_sector
var/list/possible_sectors = list()
/**
* Note that the dirs here are REVERSE because they're used for entry points, so it'd be the dir facing starboard for example.
* These are strings because otherwise the list indexes would be out of bounds. Thanks BYOND.
*
* SOUTH = 1, NORTH = 2, WEST = 4, EAST = 8,
* NORTH-EAST = 5, SOUTH-EAST = 6, NORTH-WEST = 9, SOUTH-WEST = 10,
*/
var/list/naval_to_dir = list(
"1" = list( //Ship Fore is facing North. (These are reversed, a shot travelling south would enter from the north.)
"starboard" = WEST,
"port" = EAST,
"fore" = SOUTH,
"aft" = NORTH,
"aft-starboard" = NORTH|WEST,
"fore-starboard" = SOUTH|WEST,
"aft-port" = NORTH|EAST,
"fore-port" = SOUTH|EAST
),
"2" = list( //Ship Fore is facing South.
"starboard" = EAST,
"port" = WEST,
"fore" = NORTH,
"aft" = SOUTH,
"aft-starboard" = SOUTH|EAST,
"fore-starboard" = NORTH|EAST,
"aft-port" = SOUTH|WEST,
"fore-port" = NORTH|WEST
),
"4" = list( //Ship Fore is facing West.
"starboard" = NORTH,
"port" = SOUTH,
"fore" = WEST,
"aft" = EAST,
"aft-starboard" = EAST|NORTH,
"fore-starboard" = WEST|NORTH,
"aft-port" = EAST|SOUTH,
"fore-port" = WEST|SOUTH
),
"8" = list( //Ship Fore is facing East.
"starboard" = SOUTH,
"port" = NORTH,
"fore" = EAST,
"aft" = WEST,
"aft-starboard" = WEST|SOUTH,
"fore-starboard" = EAST|SOUTH,
"aft-port" = WEST|NORTH,
"fore-port" = EAST|NORTH
)
)
var/list/headings_to_naval = list(
"1" = list(
"1" = "aft",
"2" = "fore",
"4" = "port",
"5" = "aft-port",
"6" = "fore-port",
"8" = "starboard",
"9" = "aft-starboard",
"10" = "fore-starboard"
),
"2" = list(
"1" = "fore",
"2" = "aft",
"4" = "starboard",
"5" = "fore-starboard",
"6" = "aft-starboard",
"8" = "port",
"9" = "fore-port",
"10" = "aft-port"
),
"4" = list(
"1" = "starboard",
"2" = "port",
"4" = "aft",
"5" = "starboard-aft",
"6" = "port-aft",
"8" = "fore",
"9" = "starboard-fore",
"10" = "port-fore"
),
"5" = list( //northeast
"1" = "aft-starboard",
"2" = "fore-port",
"4" = "aft-port",
"5" = "aft",
"6" = "port",
"8" = "fore-starboard",
"9" = "starboard",
"10" = "fore"
),
"6" = list( //southeast
"1" = "fore-starboard",
"2" = "aft-port",
"4" = "aft-starboard",
"5" = "starboard",
"6" = "aft",
"8" = "fore-port",
"9" = "fore",
"10" = "port"
),
"8" = list(
"1" = "port",
"2" = "starboard",
"4" = "fore",
"5" = "port-fore",
"6" = "starboard-fore",
"8" = "aft",
"9" = "port-aft",
"10" = "starboard-aft"
),
"9" = list( //northwest
"1" = "aft-port",
"2" = "fore-starboard",
"4" = "fore-port",
"5" = "port",
"6" = "aft-starboard",
"8" = "fore",
"9" = "aft",
"10" = "starboard"
),
"10" = list( //southwest
"1" = "fore-port",
"2" = "aft-starboard",
"4" = "fore-starboard",
"5" = "fore",
"6" = "starboard",
"8" = "aft-port",
"9" = "aft",
"10" = "port"
)
)
/datum/controller/subsystem/atlas/stat_entry(msg)
msg = "W:{X:[world.maxx] Y:[world.maxy] Z:[world.maxz]} ZL:[length(SSmapping.z_list)]"
return ..()
/datum/controller/subsystem/atlas/Initialize(timeofday)
// Quick sanity check.
if (world.maxx != WORLD_MIN_SIZE || world.maxy != WORLD_MIN_SIZE || world.maxz != 1)
stack_trace(SPAN_WARNING("WARNING: Suspected pre-compiled map: things may break horribly!"))
log_subsystem_atlas("-- WARNING: Suspected pre-compiled map! --")
maploader = new
var/datum/map/M
for (var/type in subtypesof(/datum/map))
M = new type
if (!M.path)
log_subsystem_atlas("Map [M.name] ([M.type]) has no path set, discarding.")
qdel(M)
continue
known_maps[M.path] = M
#ifdef DEFAULT_MAP
map_override = DEFAULT_MAP
log_subsystem_atlas("Using compile-selected map.")
#endif
if (!map_override)
map_override = get_selected_map()
admin_notice(SPAN_DANGER("Loading map [map_override]."), R_DEBUG)
log_subsystem_atlas("Using map '[map_override]'.")
current_map = known_maps[map_override]
if (!current_map)
world.map_panic("Selected map does not exist!")
load_map_meta()
world.update_status()
// Begin loading the maps.
var/maps_loaded = load_map_directory("maps/[current_map.path]/", TRUE)
log_subsystem_atlas("Loaded [maps_loaded] maps.")
admin_notice(SPAN_DANGER("Loaded [maps_loaded] levels."))
if (!maps_loaded)
world.map_panic("No maps loaded!")
QDEL_NULL(maploader)
InitializeSectors()
var/chosen_sector
var/using_sector_config = FALSE
if(GLOB.config.current_space_sector)
chosen_sector = GLOB.config.current_space_sector
using_sector_config = TRUE
else
chosen_sector = current_map.default_sector
var/datum/space_sector/selected_sector = SSatlas.possible_sectors[chosen_sector]
if(!selected_sector)
if(using_sector_config)
log_config("[chosen_sector] used in the config file is not a valid space sector")
log_subsystem_atlas("[chosen_sector] used in the config file is not a valid space sector")
current_sector = new /datum/space_sector/tau_ceti //if all fails, we go with tau ceti
log_subsystem_atlas("Unable to select [chosen_sector] as a valid space sector. Tau Ceti will be used instead.")
else
current_sector = selected_sector
current_sector.setup_current_sector()
setup_spawnpoints()
return SS_INIT_SUCCESS
/datum/controller/subsystem/atlas/proc/load_map_directory(directory, overwrite_default_z = FALSE)
. = 0
if (!directory)
CRASH("No directory supplied.")
var/static/regex/mapregex = new(".+\\.dmm$")
var/list/files = flist(directory)
sortTim(files, GLOBAL_PROC_REF(cmp_text_asc))
var/list/filemaps_to_load
for(var/file in files)
if(mapregex.Find(file))
filemaps_to_load += list(file)
var/mfile
var/time = world.time
if(length(filemaps_to_load) >= 2)
stack_trace("Only one file is now supported per map!")
return
var/datum/space_level/first_level
for(var/traits in current_map.traits)
var/level = SSmapping.add_new_zlevel(name, traits, contain_turfs = FALSE)
if(!first_level)
first_level = level
mfile = "[directory][filemaps_to_load[1]]"
if(!maploader.load_map(file(mfile), 0, 0, first_level.z_value, no_changeturf = TRUE))
log_subsystem_atlas("Failed to load '[mfile]'!")
return FALSE
else
log_subsystem_atlas("Loaded level in [(world.time - time)/10] seconds.")
return TRUE
/datum/controller/subsystem/atlas/proc/get_selected_map()
if (GLOB.config.override_map)
if (known_maps[GLOB.config.override_map])
. = GLOB.config.override_map
log_subsystem_atlas("Using configured map.")
else
log_config("-- WARNING: CONFIGURED MAP DOES NOT EXIST, IGNORING! --")
log_subsystem_atlas("-- WARNING: CONFIGURED MAP DOES NOT EXIST, IGNORING! --")
. = "sccv_horizon"
else
. = "sccv_horizon"
/datum/controller/subsystem/atlas/proc/load_map_meta()
SHOULD_NOT_SLEEP(TRUE)
// This needs to be done after current_map is set, but before mapload.
GLOB.admin_departments = list(
"[current_map.boss_name]",
"External Routing",
"Supply"
)
priority_announcement = new(do_log = 0)
command_announcement = new(do_log = 0, do_newscast = 1)
log_subsystem_atlas("running [LAZYLEN(mapload_callbacks)] mapload callbacks.")
for (var/thing in mapload_callbacks)
var/datum/callback/cb = thing
cb.InvokeAsync()
mapload_callbacks.Cut()
/datum/controller/subsystem/atlas/proc/OnMapload(datum/callback/callback)
if (!istype(callback))
CRASH("Invalid callback.")
mapload_callbacks += callback
/datum/controller/subsystem/atlas/proc/setup_spawnpoints()
SHOULD_NOT_SLEEP(TRUE)
for (var/type in current_map.spawn_types)
var/datum/spawnpoint/S = new type
spawn_locations[S.display_name] = S
/datum/controller/subsystem/atlas/proc/InitializeSectors()
SHOULD_NOT_SLEEP(TRUE)
for (var/type in subtypesof(/datum/space_sector))
var/datum/space_sector/space_sector = new type()
possible_sectors[space_sector.name] = space_sector
if (!possible_sectors.len)
crash_with("No space sectors located in SSatlas.")
/// Checks if today is a Port of Call day at current_sector.
/// Returns FALSE if no port_of_call defined in current_map.ports_of_call, or if today is not listed as a Port of Call day in current_sector.scheduled_port_visits
/datum/controller/subsystem/atlas/proc/is_port_call_day()
if(!current_map || !current_sector)
return FALSE
if(current_map.ports_of_call && length(current_sector.scheduled_port_visits))
/// Get today
var/today = GLOB.all_days[GLOB.all_days.Find(time2text(world.realtime, "Day"))]
if(today in current_sector.scheduled_port_visits) //checks if today is a port of call day
return TRUE
return FALSE
// Called when there's a fatal, unrecoverable error in mapload. This reboots the server.
/world/proc/map_panic(reason)
to_chat(world, SPAN_DANGER("Fatal error during map setup, unable to continue! Server will reboot in 60 seconds."))
log_subsystem_atlas("-- FATAL ERROR DURING MAP SETUP: [uppertext(reason)] --")
sleep(1 MINUTE)
world.Reboot()
/// Called to retrieve the name of the station. When short is TRUE, the short name of the station will be provided instead.
/proc/station_name(var/short = FALSE)
ASSERT(SSatlas.current_map)
if(short)
. = SSatlas.current_map.station_short
else
. = SSatlas.current_map.station_name
var/sname
if (GLOB.config && GLOB.config.server_name)
sname = "[GLOB.config.server_name]: [.]"
else
sname = .
if (world.name != sname)
world.name = sname
world.log << "Set world.name to [sname]."
/proc/commstation_name()
ASSERT(SSatlas.current_map)
return SSatlas.current_map.dock_name