Files
Bubberstation/code/datums/mutations/_mutations.dm
necromanceranne d556e92d52 Special version of hulk (wizard, warlord superhuman powers) no longer suffer recoil damage from breaking walls. Recoil actually uses wound rolls and not forced wounds (#92992)
## About The Pull Request

### Special versions are no longer vulnerable to recoil

There are two versions of hulk that are not available under normal
circumstances. The version given by wizard's Transformation, and the
version that Warlord pirates spawn with. These versions no longer break
their arms when destroying walls.

Normal versions, like the genetic hulks and the orc variant, still
suffer this effect.

### oof ow my bones

Hulk recoil damage now utilizes RNG wound determination. This allows for
the wound to escalate normally if the wound meets the threshold. The
damage passed means that there is a roughly 50/50 chance to cause a
dislocation, a very slim chance to roll a fracture, and this probability
will grow worse once the arm is wounded and threshold penalties start to
take effect.

This also means that the mutation respects wound resistance and wound
vulnerability, where as the previous behavior did not.

### Cleans up some mutation code a smidge

There is clearly some leftovers from prior refactors still littered
through mutation code, so I did some maintenance while I was here.

## Why It's Good For The Game

https://github.com/tgstation/tgstation/pull/51389 introduced this
weakness to dissuade hulks from flattening walls all the way to the
armory (for the fuckbillionth time). However, it applies to all versions
of hulk, including instances where it is an antagonist's ability or
power. Rather than have antagonists suffer from balance considerations
largely aimed at crew/tiders, we make them exempt so that they can SMASH
to their heart's content.

Hulk wound determination was kind of weird. For one, it relied heavily
on the arm health consistently being 50. When it wasn't, you started
getting into less reliable behaviour. In addition, it does not at all
respect any kind of vulnerability or resistance to wounding.

It is possible that utilizing this system was so that the wounding
effects would be staggered out rather than sporadic, and so slightly
fairer on the user by being more reliable. However, I think letting it
operate similarly to how our natural wound determination effects work
provides some more interesting outcomes for those who might want to use
the mutation in an earnest fashion, and still otherwise limits those
people who are just looking to low effort grief.

I think in retrospect this might have been a pretty heavy-handed nerf,
but I'm not wholly reversing it, I'm just making it...different.
Outcomes should be largely the same.
2025-09-22 19:54:07 -05:00

249 lines
9.2 KiB
Plaintext

/// Negatives that are virtually harmless and mostly just funny (language)
// Set to 0 because munchkinning via miscommunication = bad
#define NEGATIVE_STABILITY_MINI 0
/// Negatives that are slightly annoying (unused)
#define NEGATIVE_STABILITY_MINOR -20
/// Negatives that present an uncommon or weak, consistent hindrance to gameplay (cough, paranoia)
#define NEGATIVE_STABILITY_MODERATE -30
/// Negatives that present a major consistent hindrance to gameplay (deaf, mute, acid flesh)
#define NEGATIVE_STABILITY_MAJOR -40
/// Positives that provide basically no benefit (glowy)
#define POSITIVE_INSTABILITY_MINI 5
/// Positives that are niche in application or useful in rare circumstances (parlor tricks, geladikinesis, autotomy)
#define POSITIVE_INSTABILITY_MINOR 10
/// Positives that provide a new ability that's roughly par with station equipment (insulated, cryokinesis)
#define POSITIVE_INSTABILITY_MODERATE 25
/// Positives that are unique, very powerful, and noticeably change combat/gameplay (hulk, tk)
#define POSITIVE_INSTABILITY_MAJOR 35
/datum/mutation
var/name = "mutation"
/// Description of the mutation
var/desc = "A mutation."
/// Is this mutation currently locked?
var/locked
/// Quality of the mutation
var/quality
/// Message given to the user upon gaining this mutation
var/text_gain_indication = ""
/// Message given to the user upon losing this mutation
var/text_lose_indication = ""
/// Visual indicators upon the character of the owner of this mutation
var/static/list/visual_indicators = list()
/// The path of action we grant to our user on mutation gain
var/datum/action/cooldown/power_path
/// Which mutation layer to use
var/layer_used = MUTATIONS_LAYER
/// To restrict mutation to only certain species
var/list/species_allowed
/// Minimum health required to acquire the mutation
var/health_req
/// Required limbs to acquire this mutation
var/limb_req
/// The owner of this mutation's DNA
var/datum/dna/dna
/// Owner of this mutation
var/mob/living/carbon/human/owner
/// Instability the holder gets when the mutation is not native
var/instability = 0
/// Amount of those big blocks with gene sequences
var/blocks = 4
/// Amount of missing sequences. Sometimes it removes an entire pair for 2 points
var/difficulty = 8
/// 'Mutation #49', decided every round to get some form of distinction between undiscovered mutations
var/alias
/// Whether we can read it if it's active. To avoid cheesing with mutagen
var/scrambled = FALSE
/// The sources of the mutation (found in defines/dna.dm)
var/list/sources = list()
/**
* any mutations that might conflict.
* put mutation typepath defines in here.
* make sure to enter it both ways (so that A conflicts with B, and B with A)
*/
var/list/conflicts
var/remove_on_aheal = TRUE
/**
* can we take chromosomes?
* 0: CHROMOSOME_NEVER never
* 1: CHROMOSOME_NONE yeah
* 2: CHROMOSOME_USED no, already have one
*/
var/can_chromosome = CHROMOSOME_NONE
/// Name of the chromosome
var/chromosome_name
//Chromosome stuff - set to -1 to prevent people from changing it. Example: It'd be a waste to decrease cooldown on mutism
/// genetic stability coeff
var/stabilizer_coeff = 1
/// Makes the mutation hurt the user less
var/synchronizer_coeff = MUTATION_COEFFICIENT_UNMODIFIABLE
/// Boosts mutation strength
var/power_coeff = MUTATION_COEFFICIENT_UNMODIFIABLE
/// Lowers mutation cooldown
var/energy_coeff = MUTATION_COEFFICIENT_UNMODIFIABLE
/// List of strings of valid chromosomes this mutation can accept.
var/list/valid_chrom_list = list()
/// List of traits that are added or removed by the mutation with GENETIC_TRAIT source.
var/list/mutation_traits
/datum/mutation/New()
. = ..()
/datum/mutation/Destroy()
power_path = null
dna = null
owner = null
return ..()
/datum/mutation/proc/make_copy()
var/datum/mutation/copy = new type
copy.chromosome_name = chromosome_name
copy.stabilizer_coeff = stabilizer_coeff
copy.synchronizer_coeff = synchronizer_coeff
copy.power_coeff = power_coeff
copy.energy_coeff = energy_coeff
copy.can_chromosome = can_chromosome
copy.valid_chrom_list = valid_chrom_list
update_valid_chromosome_list()
return copy
/datum/mutation/proc/on_acquiring(mob/living/carbon/human/acquirer)
if(!acquirer || !istype(acquirer) || acquirer.stat == DEAD || (src in acquirer.dna.mutations))
return FALSE
if(species_allowed && !species_allowed.Find(acquirer.dna.species.id))
return FALSE
if(health_req && acquirer.health < health_req)
return FALSE
if(limb_req && !acquirer.get_bodypart(limb_req))
return FALSE
for(var/datum/mutation/mewtayshun as anything in acquirer.dna.mutations) //check for conflicting powers
if(!(mewtayshun.type in conflicts) && !(type in mewtayshun.conflicts))
continue
to_chat(acquirer, span_warning("You feel your genes resisting something."))
return FALSE
owner = acquirer
dna = acquirer.dna
dna.mutations += src
SEND_SIGNAL(src, COMSIG_MUTATION_GAINED, acquirer)
if(text_gain_indication)
to_chat(owner, text_gain_indication)
if(visual_indicators.len)
var/list/mut_overlay = list(get_visual_indicator())
if(owner.overlays_standing[layer_used])
mut_overlay = owner.overlays_standing[layer_used]
mut_overlay |= get_visual_indicator()
owner.remove_overlay(layer_used)
owner.overlays_standing[layer_used] = mut_overlay
owner.apply_overlay(layer_used)
grant_power() //we do checks here so nothing about hulk getting magic
if(mutation_traits)
owner.add_traits(mutation_traits, GENETIC_MUTATION)
return TRUE
/datum/mutation/proc/get_visual_indicator()
return
/datum/mutation/proc/on_life(seconds_per_tick, times_fired)
return
/datum/mutation/proc/on_losing(mob/living/carbon/human/owner)
if(!istype(owner) || !(owner.dna.mutations.Remove(src)))
return TRUE
. = FALSE
SEND_SIGNAL(src, COMSIG_MUTATION_LOST, owner)
if(text_lose_indication && owner.stat != DEAD)
to_chat(owner, text_lose_indication)
if(visual_indicators.len)
var/list/mut_overlay = list()
if(owner.overlays_standing[layer_used])
mut_overlay = owner.overlays_standing[layer_used]
owner.remove_overlay(layer_used)
mut_overlay.Remove(get_visual_indicator())
owner.overlays_standing[layer_used] = mut_overlay
owner.apply_overlay(layer_used)
if(mutation_traits)
owner.remove_traits(mutation_traits, GENETIC_MUTATION)
/mob/living/carbon/proc/update_mutations_overlay()
return
/mob/living/carbon/human/update_mutations_overlay()
for(var/datum/mutation/mutation as anything in dna.mutations)
if(mutation.species_allowed && !mutation.species_allowed.Find(dna.species.id))
dna.remove_mutation(mutation, mutation.sources) //shouldn't have that mutation at all
continue
if(mutation.visual_indicators.len == 0)
continue
var/list/mut_overlay = list()
if(overlays_standing[mutation.layer_used])
mut_overlay = overlays_standing[mutation.layer_used]
var/mutable_appearance/indicator_to_add = mutation.get_visual_indicator()
if(!mut_overlay.Find(indicator_to_add)) //either we lack the visual indicator or we have the wrong one
remove_overlay(mutation.layer_used)
for(var/mutable_appearance/indicator_to_remove in mutation.visual_indicators[mutation.type])
mut_overlay.Remove(indicator_to_remove)
mut_overlay |= indicator_to_add
overlays_standing[mutation.layer_used] = mut_overlay
apply_overlay(mutation.layer_used)
/**
* Called after on_aquiring, or when a chromosome is applied.
* returns the instance of 'power_path' for children calls to use without calling locate() again.
*/
/datum/mutation/proc/setup()
if(!power_path || QDELETED(owner))
return
var/datum/action/cooldown/modified_power = locate(power_path) in owner.actions
if(!modified_power)
CRASH("Genetic mutation [type] called setup(), but could not find a action to modify!")
modified_power.cooldown_time = initial(modified_power.cooldown_time) * GET_MUTATION_ENERGY(src)
return modified_power
/datum/mutation/proc/remove_chromosome()
stabilizer_coeff = initial(stabilizer_coeff)
synchronizer_coeff = initial(synchronizer_coeff)
power_coeff = initial(power_coeff)
energy_coeff = initial(energy_coeff)
can_chromosome = initial(can_chromosome)
chromosome_name = null
/datum/mutation/proc/grant_power()
if(!ispath(power_path) || !owner)
return FALSE
var/datum/action/cooldown/new_power = new power_path(src)
new_power.background_icon_state = "bg_tech_blue"
new_power.base_background_icon_state = new_power.background_icon_state
new_power.active_background_icon_state = "[new_power.base_background_icon_state]_active"
new_power.overlay_icon_state = "bg_tech_blue_border"
new_power.active_overlay_icon_state = "bg_spell_border_active_blue"
new_power.panel = "Genetic"
new_power.Grant(owner)
return new_power
// Runs through all the coefficients and uses this to determine which chromosomes the
// mutation can take. Stores these as text strings in a list.
/datum/mutation/proc/update_valid_chromosome_list()
valid_chrom_list.Cut()
if(can_chromosome == CHROMOSOME_NEVER)
valid_chrom_list += "none"
return
if(stabilizer_coeff != MUTATION_COEFFICIENT_UNMODIFIABLE)
valid_chrom_list += "Stabilizer"
if(synchronizer_coeff != MUTATION_COEFFICIENT_UNMODIFIABLE)
valid_chrom_list += "Synchronizer"
if(power_coeff != MUTATION_COEFFICIENT_UNMODIFIABLE)
valid_chrom_list += "Power"
if(energy_coeff != MUTATION_COEFFICIENT_UNMODIFIABLE)
valid_chrom_list += "Energetic"