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>
This commit is contained in:
FenodyreeAv
2026-06-03 16:52:13 +00:00
committed by GitHub
co-authored by Matt Atlas
parent fb3189f542
commit cf585ef505
49 changed files with 859 additions and 251 deletions
@@ -4,7 +4,9 @@
build_path = /obj/structure/machinery/shield_gen/external
origin_tech = list(TECH_BLUESPACE = 4, TECH_PHORON = 3)
req_components = list(
"/obj/item/stock_parts/manipulator/pico" = 2,
"/obj/item/stock_parts/manipulator" = 2,
"/obj/item/stock_parts/micro_laser" = 2,
"/obj/item/stock_parts/capacitor" = 2,
"/obj/item/stock_parts/subspace/transmitter" = 1,
"/obj/item/stock_parts/subspace/crystal" = 1,
"/obj/item/stock_parts/subspace/amplifier" = 1,
@@ -17,7 +19,9 @@
build_path = /obj/structure/machinery/shield_gen
origin_tech = list(TECH_BLUESPACE = 4, TECH_PHORON = 3)
req_components = list(
"/obj/item/stock_parts/manipulator/pico" = 2,
"/obj/item/stock_parts/manipulator" = 2,
"/obj/item/stock_parts/micro_laser" = 2,
"/obj/item/stock_parts/capacitor" = 2,
"/obj/item/stock_parts/subspace/transmitter" = 1,
"/obj/item/stock_parts/subspace/crystal" = 1,
"/obj/item/stock_parts/subspace/amplifier" = 1,
@@ -30,7 +34,8 @@
build_path = /obj/structure/machinery/shield_capacitor
origin_tech = list(TECH_MAGNET = 3, TECH_POWER = 4)
req_components = list(
"/obj/item/stock_parts/manipulator/pico" = 2,
"/obj/item/stock_parts/capacitor" = 4,
"/obj/item/stock_parts/micro_laser" = 1,
"/obj/item/stock_parts/subspace/filter" = 1,
"/obj/item/stock_parts/subspace/treatment" = 1,
"/obj/item/stock_parts/subspace/analyzer" = 1,
@@ -1,8 +1,29 @@
/proc/fragem(var/source,var/fragx,var/fragy,var/light_dam,var/flash_dam,var/p_dam,var/p_range,var/can_cover=TRUE,var/shard_range = 50)
/**
* Fragem works by shooting a projectile containing fragments at every tile in a circle around it.
*
* Source is the tile the projectiles spawn on.
*
* fragx and fragy are the upper and lower bounds of the random number of fragments produced.
*
* light_dam and flash_dam are the parameters for the explosion produced by the grenade. light_dam is the radius of minor explosive damage, flash_dam is the radius of the flash effect.
*
* p_dam is the damage of each individual fragments.
*
* p_range is the distance each projectile travels before losing a fragment. When a projectile loses all of its fragments, it is deleted.
*
* can_cover determines if a mob can lie on the source turf to absorb most of the fragments.
*
* shard_range is the maximum range of the projectiles produced by the fragem. This is separate from p_range to allow for projectiles that lose fragments over distance but have a hard maximum range. Default 50
*
* spread_range is the radius of the circle used to launch projectiles. Lower values mean less projectiles are used but if set too low gaps may appear in the spread pattern. Default 7
*
* maim_rate is the bonus for projectiles to decapitate or gib limbs. Negative values reduce this chance, positive values increase it. There is a minimum maim multiplier of 0.1, see handle_limb_gibbing for details.
*/
/proc/fragem(var/source,var/fragx,var/fragy,var/light_dam,var/flash_dam,var/p_dam,var/p_range,var/can_cover=TRUE,var/shard_range = 50,var/spread_range = 7,var/maim_rate)
var/turf/O = get_turf(source)
var/fragger = rand(fragx,fragy)
explosion(O, -1, -1, light_dam, flash_dam)
var/list/target_turfs = getcircle(O, 7)
var/list/target_turfs = getcircle(O, spread_range)
var/fragments_per_projectile = round(fragger/target_turfs.len)
for(var/turf/T in target_turfs)
@@ -12,9 +33,10 @@
P.damage = p_dam
P.pellets = fragments_per_projectile
P.range_step = p_range
if (maim_rate)
P.maim_rate = maim_rate
P.range = shard_range
P.name = "shrapnel"
P.preparePixelProjectile(T, get_turf(source))
P.firer = source
P.fired_from = source
@@ -106,6 +106,11 @@
/obj/item/material/shard/shrapnel/Initialize(newloc, material_key)
. = ..(loc, MATERIAL_STEEL)
/obj/item/material/shard/shrapnel/large/Initialize(newloc, material_key) //Shrapnel large enough to be removed by hand. Used by ship weapons.
. = ..(loc, MATERIAL_STEEL)
icon_state = "large"
w_class = WEIGHT_CLASS_NORMAL
/obj/item/material/shard/shrapnel/flechette/Initialize(newloc, material_key)
. = ..(loc, MATERIAL_TITANIUM)
+7 -3
View File
@@ -95,9 +95,13 @@
else
dismantle_material = get_material()
if(should_use_health && health <= 0)
build_amt /= rand(2, 4) //if the structure is destroyed by damage, it will yield less materials
for(var/i = 1 to build_amt)
dismantle_material.place_sheet(loc)
build_amt /= rand(2, 4) //if the structure is destroyed by damage, it will yield less materials.
build_amt = max(1, min(build_amt, 5)) //Bound between 5 and 1, as shards don't stack into sheets.
for(var/i = 1 to build_amt)
dismantle_material.place_shard(loc)
else
for(var/i = 1 to build_amt)
dismantle_material.place_sheet(loc)
qdel(src)
/obj/structure/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit)
@@ -250,6 +250,8 @@ Class Procs:
for(var/i = 1 to 2)
if(prob(50))
metal_to_spawn++
else
new /obj/item/material/shard(current_turf, DEFAULT_WALL_MATERIAL)
if(metal_to_spawn)
new /obj/item/stack/material/steel(get_turf(src), metal_to_spawn)
if(!should_use_health)
@@ -6,6 +6,7 @@
maxhealth = OBJECT_HEALTH_MEDIUM
color = COLOR_GRAY20
build_amt = 4
pass_flags_self = PASSRAILING
layer = WINDOW_FRAME_LAYER
anchored = TRUE
density = TRUE
@@ -59,6 +60,11 @@
return TRUE
return FALSE
/obj/structure/window_frame/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit)
. = ..()
if(. != BULLET_ACT_HIT)
return .
/obj/structure/window_frame/attackby(obj/item/attacking_item, mob/user)
if((attacking_item.tool_behaviour == TOOL_SCREWDRIVER) && (istype(loc, /turf/simulated) || anchored))
if(has_glass_installed)
+4 -1
View File
@@ -159,6 +159,9 @@
var/damage = hitting_projectile.get_structure_damage()
var/passthrough = 0
if(piercing_hit)
return BULLET_ACT_FORCE_PIERCE //Shots that would have penetrated anyway don't get the damage reduction or chance to not penetrate.
if(!damage)
return BULLET_ACT_BLOCK
@@ -181,7 +184,7 @@
passthrough = 1
if(passthrough)
. = BULLET_ACT_HIT
. = BULLET_ACT_FORCE_PIERCE
damage = between(0, (damage - hitting_projectile.damage)*(hitting_projectile.damage_type == DAMAGE_BRUTE? 0.4 : 1), 10) //if the bullet passes through then the grille avoids most of the damage
add_damage(damage * 0.2)
@@ -8,6 +8,8 @@
icon_state = "0"
build_amt = 5
var/state = 0
maxhealth = OBJECT_HEALTH_MEDIUM
var/obj/item/circuitboard/circuit = null
/obj/structure/computerframe/attackby(obj/item/attacking_item, mob/user)
@@ -256,7 +256,7 @@
// Emitter Blasts - these will eventually completely destroy the door, given enough time.
if (damage > 90)
destroy_hits--
destroy_hits -= (1 * hitting_projectile.anti_materiel_potential)
if (destroy_hits <= 0)
visible_message(SPAN_DANGER("\The [src.name] disintegrates!"))
switch (hitting_projectile.damage_type)
@@ -63,6 +63,7 @@
density = 1
clicksound = SFX_BUTTON
manufacturer = "idris"
maxhealth = OBJECT_HEALTH_HIGH
// Every vending machine has one of these.
/// `icon_state` when off. Defined on init.
@@ -358,6 +359,24 @@
return
return
/obj/structure/machinery/vending/on_death(damage, damage_flags, damage_type, armor_penetration, obj/weapon)
var/turf/current_turf = get_turf(src)
if(current_turf)
var/list/stocked_products = list()
for(var/datum/data/vending_product/product in product_records)
if(product.amount > 0)
stocked_products += product
var/items_to_drop = rand(1, 5)
while(items_to_drop-- > 0 && length(stocked_products))
var/datum/data/vending_product/product = pick(stocked_products)
new product.product_path(current_turf)
product.amount--
if(product.amount < 1)
stocked_products -= product
. = ..()
/obj/structure/machinery/vending/emag_act(var/remaining_charges, var/mob/user)
if (!emagged)
src.emagged = 1
+1 -1
View File
@@ -114,7 +114,7 @@
if(. != BULLET_ACT_HIT)
return .
add_damage(proj_damage)
add_damage(proj_damage, hitting_projectile.damage_flags(), hitting_projectile.damage_type, hitting_projectile.armor_penetration, hitting_projectile)
/obj/structure/window/ex_act(severity)
switch(severity)