Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions tools/gear_catalog/ratings/cyberware.json
Original file line number Diff line number Diff line change
Expand Up @@ -449,21 +449,21 @@
"/obj/item/organ/cyberimp/cyberware/dermal_mesh": {
"confidence": "high",
"group": "Armor and frame",
"hash": "5b60dbb7a15c8084",
"hash": "",
"name": "Dermal Mesh weave",
"note": "While online, adds physiology armor to a human wearer: melee 15, bullet 15, laser 10, wound 5. The armor is removed during an EMP reboot or an over-capacity brownout.",
"note": "While online, adds physiology armor to a human wearer: melee 15, bullet 15, laser 10, wound 5. Bullets and buckshot pellets from the front glance off at 15% (each pellet rolls on its own), keep flying at reduced damage and cost the wearer 5 stamina each. Never lasers, energy, melee or shots into the back. Shares the dodge arbiter: dodge plus glance never stop more than 60% of shots. The armor and the glance are off during an EMP reboot or an over-capacity brownout.",
"rank": 5,
"reason": "15 melee, 15 bullet, 10 laser on the whole body, on top of whatever you wear. A clear step up for load 2 and 2,000 cr. EMP switches it off for 9 s.",
"reason": "15 melee, 15 bullet, 10 laser on the whole body, on top of whatever you wear, and one bullet in seven from the front glances off. A clear step up for load 2 and 2,000 cr. EMP switches it off for 9 s.",
"tier": "yellow"
},
"/obj/item/organ/cyberimp/cyberware/dermal_mesh/hardweave": {
"confidence": "medium",
"group": "Armor and frame",
"hash": "",
"name": "Hardweave plating",
"note": "While online, adds physiology armor to a human wearer: melee 20, bullet 20, laser 15, energy 10, wound 10. The armor is removed during an EMP reboot or an over-capacity brownout.",
"note": "While online, adds physiology armor to a human wearer: melee 20, bullet 20, laser 15, energy 10, wound 10. Bullets and buckshot pellets from the front glance off at 30% (each pellet rolls on its own), keep flying at reduced damage and cost the wearer 5 stamina each. Never lasers, energy, melee or shots into the back. Shares the dodge arbiter: dodge plus glance never stop more than 60% of shots. The armor and the glance are off during an EMP reboot or an over-capacity brownout.",
"rank": 3,
"reason": "20 melee, 20 bullet, 15 laser, 10 energy and 10 wound armor on the whole body, on top of whatever you wear. A size up from the Mesh and well short of the Slabskin Plate, which adds bomb, fire and acid. Load 3, 3,000 cr at reputation 4. EMP switches it off for 9 s.",
"reason": "20 melee, 20 bullet, 15 laser, 10 energy and 10 wound armor on the whole body, on top of whatever you wear, and nearly a third of bullets from the front glance off. A size up from the Mesh and well short of the Slabskin Plate, which adds bomb, fire, acid and a bigger glance. Load 3, 3,000 cr at reputation 4. Strong for yellow against gunfire from the front, still useless against lasers and flanking. EMP switches it off for 9 s.",
"tier": "yellow"
},
"/obj/item/organ/cyberimp/cyberware/doppler": {
Expand Down Expand Up @@ -779,11 +779,11 @@
"/obj/item/organ/cyberimp/cyberware/slabskin": {
"confidence": "high",
"group": "Armor and frame",
"hash": "7b785a3adf8b7c37",
"hash": "",
"name": "Slabskin plate",
"note": "While online it adds built-in armor to the bearer (melee 25, bullet 25, laser 25, energy 20, bomb 25, fire 20, acid 20, wound 15), and any brute or burn hit of 5 or more throws sparks and often a ricochet sound so others can see the plate. The armor switches off while the ware is EMP'd or browned out.",
"note": "While online it adds built-in armor to the bearer (melee 25, bullet 25, laser 25, energy 20, bomb 25, fire 20, acid 20, wound 15), and any brute or burn hit of 5 or more throws sparks and often a ricochet sound so others can see the plate. Bullets and buckshot pellets from the front glance off at 45% (each pellet rolls on its own), keep flying at reduced damage and cost the wearer 5 stamina each. Never lasers, energy, melee or shots into the back. Shares the dodge arbiter: dodge plus glance never stop more than 60% of shots. The armor and the glance switch off while the ware is EMP'd or browned out.",
"rank": 1,
"reason": "25 armor against melee, bullets and lasers on the whole body, multiplied on top of whatever you wear, plus bomb, fire, acid and wounds. About a second armor vest that covers your head and limbs. Load 6, 3 vouchers at reputation 22. EMP switches it off for 9 s.",
"reason": "25 armor against melee, bullets and lasers on the whole body, multiplied on top of whatever you wear, plus bomb, fire, acid and wounds, and almost half of bullets from the front glance off. About a second armor vest that covers your head and limbs, and the best answer to a gunline in the game. Load 6, 3 vouchers at reputation 22. EMP switches it off for 9 s.",
"tier": "red"
},
"/obj/item/organ/cyberimp/cyberware/slipwire": {
Expand Down
12 changes: 11 additions & 1 deletion voidcrew/modules/cyberware/ware_military_body.dm
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@
*/
/obj/item/organ/cyberimp/cyberware/slabskin
name = "\improper Slabskin plate"
desc = "A keel and rib plates of sintered ceramic, grown into the skin of the chest and back. Bullets leave scuffs and a noise like a dropped pan."
desc = "A keel and rib plates of sintered ceramic, grown into the skin of the chest and back. Bullets leave scuffs, when they don't bounce off with a noise like a dropped pan."
icon_state = "slabskin"
zone = BODY_ZONE_CHEST
slot = ORGAN_SLOT_CYBERWARE_DERMAL
Expand All @@ -276,13 +276,16 @@
/// TRUE while the plate armor is mixed into the bearer's physiology.
/// Guards the failing-gated passive hooks against double add/subtract.
var/plate_armor_applied = FALSE
/// Percent of bullets from the front that glance off, through the shared dodge arbiter. The top of the dermal ladder.
var/deflect_chance = 45

/obj/item/organ/cyberimp/cyberware/slabskin/on_mob_insert(mob/living/carbon/organ_owner, special = FALSE, movement_flags)
. = ..()
RegisterSignal(organ_owner, COMSIG_MOB_APPLY_DAMAGE, PROC_REF(on_damaged))

/obj/item/organ/cyberimp/cyberware/slabskin/chrome_passives_on(mob/living/carbon/bearer)
. = ..()
cyberware_register_deflect_source(bearer, REF(src), CALLBACK(src, PROC_REF(get_deflect_chance)))
if(plate_armor_applied || !ishuman(bearer))
return
plate_armor_applied = TRUE
Expand All @@ -291,6 +294,7 @@

/obj/item/organ/cyberimp/cyberware/slabskin/chrome_passives_off(mob/living/carbon/bearer)
. = ..()
cyberware_unregister_deflect_source(bearer, REF(src))
if(!plate_armor_applied)
return
plate_armor_applied = FALSE // reset before the validity skip, or the plate never re-arms after a bearer deletes
Expand All @@ -303,6 +307,12 @@
. = ..()
UnregisterSignal(organ_owner, COMSIG_MOB_APPLY_DAMAGE)

/// Deflect-source callback: nothing while failing.
/obj/item/organ/cyberimp/cyberware/slabskin/proc/get_deflect_chance()
if(organ_flags & ORGAN_FAILING)
return 0
return deflect_chance

/**
* Signal proc for [COMSIG_MOB_APPLY_DAMAGE]: the telegraph. Solid brute or
* burn hits spark and ping off the plate where everyone can see them.
Expand Down
3 changes: 2 additions & 1 deletion voidcrew/modules/cyberware/ware_models_b.dm
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,13 @@

/obj/item/organ/cyberimp/cyberware/dermal_mesh/hardweave
name = "\improper Hardweave plating"
desc = "Rib plates over the chest and back with a layered polymer weave sewn in below them, a size up from the usual mesh. The scarring is the part nobody mentions."
desc = "Rib plates over the chest and back with a layered polymer weave sewn in below them, a size up from the usual mesh. Rounds that come in at an angle skip off. The scarring is the part nobody mentions."
icon_state = "dermal_mesh_hardweave"
aug_overlay = "dermal_mesh_hardweave"
chrome_load = 3
tier = CYBERWARE_TIER_2
mesh_armor = /datum/armor/cyberware_hardweave
deflect_chance = 30

// =========================================================================
// STRUT FRAME (T2): the bones and nothing else
Expand Down
134 changes: 121 additions & 13 deletions voidcrew/modules/cyberware/ware_pro_combat.dm
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
* COMSIG_ATOM_PRE_BULLET_ACT handler. A source registered with melee = TRUE
* (the Cascade line's windows) also dodges blows, through tg's own
* check_block() via COMSIG_LIVING_CHECK_BLOCK, so a dodged blow is a real
* miss and never fake damage.
* miss and never fake damage. The same arbiter rolls the dermal plating's
* bullet deflection (see cyberware_register_deflect_source()), capped so a
* dodge and a deflect together never stop more than CYBERWARE_AVOID_CAP
* percent of shots.
* - cyberware_is_ally(): the crew filter for auto-targeting ware (Deadeye's
* tag, Widowline's cleave). Allies are people who share a ship team with
* you; NPC boarders standing on your deck are NOT allies, and rival players
Expand All @@ -27,6 +30,10 @@
#define CYBERWARE_DODGE_MOVE_WINDOW (1 SECONDS)
/// The shortest gap between two dodge lines from a bearer nobody is playing (an NPC under fire).
#define CYBERWARE_DODGE_LINE_GAP (1.5 SECONDS)
/// Dodge and deflect together never stop more than this percent of shots.
#define CYBERWARE_AVOID_CAP 60
/// Stamina each deflected bullet costs the bearer, so focused fire still wears them down.
#define CYBERWARE_DEFLECT_STAMINA 5

/**
* The one dodge roll for all cyberware. Lives on the mob; wares and status
Expand All @@ -53,6 +60,10 @@
var/list/dodge_sources = list()
/// key -> /datum/callback for the sources that also dodge blows. Each is in dodge_sources too.
var/list/melee_sources = list()
/// key -> /datum/callback returning that source's live bullet-deflect chance (the dermal plating).
var/list/deflect_sources = list()
/// Dodge and deflect together never stop more than this percent of shots.
var/avoid_cap = CYBERWARE_AVOID_CAP
/// world.time of the bearer's last move.
var/last_move_time = 0
/// world.time a dodge was last announced: an NPC under fire says it at most once every CYBERWARE_DODGE_LINE_GAP, so chat isn't flooded
Expand All @@ -73,32 +84,91 @@
/datum/component/cyberware_dodge/Destroy()
dodge_sources = null
melee_sources = null
deflect_sources = null
return ..()

/// Signal proc for [COMSIG_MOVABLE_MOVED]: timestamp the movement window.
/datum/component/cyberware_dodge/proc/on_moved(atom/movable/source, atom/old_loc, dir, forced)
SIGNAL_HANDLER
last_move_time = world.time

/// Signal proc for [COMSIG_ATOM_PRE_BULLET_ACT]: the actual dodge roll.
/**
* Signal proc for [COMSIG_ATOM_PRE_BULLET_ACT]: the dodge roll, then the
* deflect roll. One handler for both, so the cap on the two together holds
* no matter which chrome registered first.
*/
/datum/component/cyberware_dodge/proc/on_pre_bullet_act(mob/living/source, obj/projectile/hitting_projectile, def_zone, piercing_hit, blocked)
SIGNAL_HANDLER
if(world.time - last_move_time > CYBERWARE_DODGE_MOVE_WINDOW)
return NONE
if(source.stat != CONSCIOUS || source.body_position == LYING_DOWN || source.buckled)
return NONE
if(!istype(hitting_projectile) || !hitting_projectile.is_hostile_projectile())
return NONE
var/dodge_chance = live_dodge_chance(source, hitting_projectile)
if(dodge_chance > 0 && prob(dodge_chance))
INVOKE_ASYNC(src, PROC_REF(perform_sidestep), source, hitting_projectile)
return COMPONENT_BULLET_PIERCED
var/deflect_chance = min(live_deflect_chance(source, hitting_projectile), cyberware_deflect_cap(dodge_chance, avoid_cap))
if(deflect_chance <= 0 || !prob(deflect_chance))
return NONE
glance(source, hitting_projectile)
return COMPONENT_BULLET_PIERCED

/// The dodge chance against this shot right now: 0 unless the bearer is moving, upright and not point-blank.
/datum/component/cyberware_dodge/proc/live_dodge_chance(mob/living/source, obj/projectile/hitting_projectile)
if(!length(dodge_sources))
return 0
if(world.time - last_move_time > CYBERWARE_DODGE_MOVE_WINDOW)
return 0
if(source.stat != CONSCIOUS || source.body_position == LYING_DOWN || source.buckled)
return 0
// Point-blank shots always land: an adjacent shooter is inside the
// reflex loop. This is the anti-"unhittable in a brawl" rule.
var/atom/shooter = hitting_projectile.firer
if(shooter && shooter != source && get_dist(source, shooter) <= 1)
return NONE
var/best_chance = best_chance(dodge_sources)
if(best_chance <= 0 || !prob(best_chance))
return NONE
INVOKE_ASYNC(src, PROC_REF(perform_sidestep), source, hitting_projectile)
return COMPONENT_BULLET_PIERCED
return 0
return best_chance(dodge_sources)

/**
* The deflect chance against this shot right now. Only real bullets (and
* each buckshot pellet on its own, since every pellet is its own projectile)
* glance off; lasers, energy and everything else never do. The plate only
* covers what the bearer is facing: a shot into the back always lands.
*/
/datum/component/cyberware_dodge/proc/live_deflect_chance(mob/living/source, obj/projectile/hitting_projectile)
if(!length(deflect_sources))
return 0
if(!istype(hitting_projectile, /obj/projectile/bullet) || hitting_projectile.armor_flag != BULLET)
return 0
if(source.stat == DEAD)
return 0
if(cyberware_shot_from_behind(source, hitting_projectile))
return 0
return best_chance(deflect_sources)

/**
* The real glance: the bullet's heading turns back off the plate at a random
* angle and the round keeps flying (COMPONENT_BULLET_PIERCED, the same exit
* tg's own reflect takes), so it can still hit whoever stands in its new
* path. It loses energy like any tg ricochet. The bearer stays in the
* bullet's impacted list, so it can't come back for them.
*/
/datum/component/cyberware_dodge/proc/glance(mob/living/source, obj/projectile/bullet)
bullet.set_angle(SIMPLIFY_DEGREES(bullet.angle + pick(-1, 1) * rand(100, 170)))
bullet.damage *= bullet.ricochet_decay_damage
bullet.stamina *= bullet.ricochet_decay_damage
INVOKE_ASYNC(src, PROC_REF(perform_glance), source, bullet.name)

/// The visible and physical half of a glance: sparks, a ping, and the stamina the hit still costs.
/datum/component/cyberware_dodge/proc/perform_glance(mob/living/source, bullet_name)
if(QDELETED(source))
return
do_sparks(1, TRUE, source)
playsound(source, SFX_RICOCHET, 60, TRUE)
source.adjustStaminaLoss(CYBERWARE_DEFLECT_STAMINA)
if(source.client || world.time - last_message_at >= CYBERWARE_DODGE_LINE_GAP)
last_message_at = world.time
source.visible_message(
span_danger("\The [bullet_name] glances off [source]!"),
span_danger("\The [bullet_name] glances off you!"),
)

/// The highest live chance among these sources. Never additive.
/datum/component/cyberware_dodge/proc/best_chance(list/sources)
Expand Down Expand Up @@ -206,9 +276,45 @@
return
arbiter.dodge_sources -= key
arbiter.melee_sources -= key
if(!length(arbiter.dodge_sources))
if(!length(arbiter.dodge_sources) && !length(arbiter.deflect_sources))
qdel(arbiter)

/**
* Adds (or refreshes) a keyed bullet-deflect source on the bearer, through
* the same arbiter as the dodge. The callback returns the source's CURRENT
* chance. Never additive: the highest live source rolls.
*/
/proc/cyberware_register_deflect_source(mob/living/bearer, key, datum/callback/chance_callback)
if(!istype(bearer) || !key || !chance_callback)
return
var/datum/component/cyberware_dodge/arbiter = bearer.LoadComponent(/datum/component/cyberware_dodge)
arbiter.deflect_sources[key] = chance_callback

/// Removes a keyed deflect source; the arbiter cleans itself up with the last source of either kind.
/proc/cyberware_unregister_deflect_source(mob/living/bearer, key)
var/datum/component/cyberware_dodge/arbiter = bearer?.GetComponent(/datum/component/cyberware_dodge)
if(!arbiter)
return
arbiter.deflect_sources -= key
if(!length(arbiter.dodge_sources) && !length(arbiter.deflect_sources))
qdel(arbiter)

/**
* The most a deflect may roll after a dodge roll at dodge_chance missed, so
* that dodge and deflect together never stop more than avoid_cap
* percent of shots: dodge + (1 - dodge) * deflect <= cap.
*/
/proc/cyberware_deflect_cap(dodge_chance, avoid_cap = CYBERWARE_AVOID_CAP)
dodge_chance = clamp(dodge_chance, 0, 100)
if(dodge_chance >= avoid_cap)
return 0
return (avoid_cap - dodge_chance) * 100 / (100 - dodge_chance)

/// Whether this shot is travelling the way the bearer faces, so it strikes them in the back.
/proc/cyberware_shot_from_behind(mob/living/bearer, obj/projectile/shot)
var/offset = SIMPLIFY_DEGREES(shot.angle - dir2angle(bearer.dir) + 180) - 180
return abs(offset) < 90

// ---- Shared ally filter ------------------------------------------------

/**
Expand Down Expand Up @@ -811,6 +917,8 @@

#undef CYBERWARE_DODGE_MOVE_WINDOW
#undef CYBERWARE_DODGE_LINE_GAP
#undef CYBERWARE_AVOID_CAP
#undef CYBERWARE_DEFLECT_STAMINA
#undef CYBERWARE_DEADEYE_TAG_RANGE
#undef CYBERWARE_DEADEYE_TAG_SHOTS
#undef CYBERWARE_DEADEYE_TAG_DURATION
Expand Down
16 changes: 15 additions & 1 deletion voidcrew/modules/cyberware/ware_street.dm
Original file line number Diff line number Diff line change
Expand Up @@ -745,10 +745,14 @@ GLOBAL_LIST_INIT(cyberware_ink_palette, list(
* add/subtract pair per flip is the whole lifecycle; re-applying on species
* gain would stack the armor. The pair rides the failing-gated passive hooks
* (BAL-4): EMP-scrambled or browned-out mesh armors nothing.
*
* The dermal ladder (Mesh, Hardweave, Slabskin) also turns bullets: a
* passive deflect source in the shared dodge arbiter (ware_pro_combat.dm),
* 15/30/45% by rung, front arc only, rides the same passive hooks.
*/
/obj/item/organ/cyberimp/cyberware/dermal_mesh
name = "\improper Dermal Mesh weave"
desc = "A subdermal lattice of impact polymer. It won't stop anything serious, but it takes the edge off."
desc = "A subdermal lattice of impact polymer. It won't stop anything serious, but now and then a round skips off it."
icon_state = "dermal_mesh"
zone = BODY_ZONE_CHEST
slot = ORGAN_SLOT_CYBERWARE_DERMAL
Expand All @@ -761,9 +765,12 @@ GLOBAL_LIST_INIT(cyberware_ink_palette, list(
var/datum/armor/mesh_armor = /datum/armor/cyberware_dermal_mesh
/// TRUE while mesh_armor is mixed in; guards against double add/subtract.
var/mesh_armor_applied = FALSE
/// Percent of bullets from the front that glance off, through the shared dodge arbiter.
var/deflect_chance = 15

/obj/item/organ/cyberimp/cyberware/dermal_mesh/chrome_passives_on(mob/living/carbon/bearer)
. = ..()
cyberware_register_deflect_source(bearer, REF(src), CALLBACK(src, PROC_REF(get_deflect_chance)))
if(mesh_armor_applied || !ishuman(bearer))
return
mesh_armor_applied = TRUE
Expand All @@ -772,6 +779,7 @@ GLOBAL_LIST_INIT(cyberware_ink_palette, list(

/obj/item/organ/cyberimp/cyberware/dermal_mesh/chrome_passives_off(mob/living/carbon/bearer)
. = ..()
cyberware_unregister_deflect_source(bearer, REF(src))
if(!mesh_armor_applied)
return
mesh_armor_applied = FALSE // reset before the validity skip, see Shock Coils
Expand All @@ -780,6 +788,12 @@ GLOBAL_LIST_INIT(cyberware_ink_palette, list(
var/mob/living/carbon/human/human_bearer = bearer
human_bearer.physiology.armor = human_bearer.physiology.armor.subtract_other_armor(mesh_armor)

/// Deflect-source callback: nothing while failing.
/obj/item/organ/cyberimp/cyberware/dermal_mesh/proc/get_deflect_chance()
if(organ_flags & ORGAN_FAILING)
return 0
return deflect_chance

// ---- 11. Gecko Grips ---------------------------------------------------

/**
Expand Down
1 change: 1 addition & 0 deletions voidcrew/modules/unit_tests/_unit_tests.dm
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
#include "voidcrew_cyberware_anim.dm"
#include "voidcrew_cyberware_anim2.dm"
#include "voidcrew_cyberware_camo.dm"
#include "voidcrew_cyberware_deflect.dm"
#include "voidcrew_cyberware_final.dm"
#include "voidcrew_cyberware_kit.dm"
#include "voidcrew_cyberware_mantis.dm"
Expand Down
Loading