Files
Bubberstation/code/datums/components/embedded.dm
LemonInTheDark 364d4bd987 Human Life Optimization: Citrus Flavored (#66037)
* Life() Optimization Part 1: Organ order

It turns out organ code is really quite dumb. Wastes a lot of time.
I'm going to be doing what I can to optimize it in this branch.

To start with:

If we can ensure organs are sorted according to organ_process_order inside
internal_organs_slot
We can do away with a needless get_organ_slot call in handle_organs, and
avoid needing to iterate over 30 entries, one per possible organ slot,
instead iterating just the ones we have

This saves roughly 5% of life

* Organ Optimization Part 2: base on_life()

Organ on_life, despite not looking it, is a remarkably hot proc. Called
something close to 9 times for each human on the map, it's worth taking
care to clean it up.

As things currently stand, we do two dumb things.
The first is manually checking for organ failure each iteration.
We do this because we allow organ damage to be modified outside of the
applyOrganDamage proc, which also calls check_failing_thresholds.

There's no reason to do this, so I've gone through and removed all
instances of it

The second is calling applyOrganDamage no matter what, to "heal" the
organ. Even if it isn't damaged.

The fix for this is simple, just an if check.

This saves roughly 10% of pre changes Life() cost

* Blood cleanup beta version

Blood code is slightly more sane, but it calls get_part_bleed_rate a
lot, and does other checks that are the same so long as the bleed rate
never changes. This is reasonably expensive

So, I'm going to make it event driven, and cache the bleed rate. But to
do that, I need to be able to react to limb ownership changes, and well,
there's a few that don't use the existing setter. This commit fixes that

* Blood cleanup beta 2.0

I'm starting on the work to make bleed rate cached

Adds NO_BLEED to the signal registration of set_owner
This lets us generically react to it

Makes changing your bleedstacks into a proc we can hook into
Fully implements it

* Blood optimization beta 3.0

Hooks into embedding/embed details updating, this ensures the embed
aspect of bleed rate is properly accounted for

* Blood optimization beta 4.0

Hooks wounds into refresh_bleed_rate, adds a setter for blood flow to
support this

I feel like there are places where this would be useful, but I'm not
totally sure
Kinda wanted to ask ryll

* Blood optimization, the final countdown

Fully caches bleed_rate. Because I can do this, we only need to call update_part_wound_overlay when bleed rate updates
This saves 15%! of human life tick. Get owned

* Optimizes body_temperature_alerts

It turns out that clearing alerts and sending signals every process is
wasteful. There's no reason to do it unless last process was a problem
one

The change for that is quite easy, literally just a new var on human.

Saves 2% of human life tick.
Very nice for the amount of time I put into it

BTW, I have a feeling that most of the overhead of bodytemp is caused by
human body temperature being higher then room temperature. Not sure
what to do about this though

* Does something similar to disgust

Basically, don't continusly send signals if you have no disgust
It's rare enough that this is all the caching we really need to do

Saves roughly 1% of life tick. Right on the edge of not worth it

* Some cleanup to how mob fires handle their lighting, adds a check to liver coder

* Cleans up update_gravity slightly

It's very close to outside a measurable deviation faster, but I honestly
just did it to make it easier to read

* Fuck you

* Very minor organ optimization

Instead of using internal_organs_slot for our organ iteration,
we use it to produce a sorted internal_organs list.

This is barely worth it, ends up being 0.7% of life saved.
I only did it because it makes a semi noticable impact on our current
numbers.

* Revert "Very minor organ optimization"

It turns out mutating internal_organs is important sometimes
The usefulness of this change is small enough that I don't want to spend
more time on it

So back it goes into the dirt
This reverts commit 622bd34adb8d8a3d5f2763ac659446ef3362e3cd.

* Properly returns false in IgniteMob

* Adds a parent call to setBleedStacks

I like it, it's a good idea
2022-04-25 20:33:47 -07:00

286 lines
13 KiB
Plaintext

/*
This component is responsible for handling individual instances of embedded objects. The embeddable element is what allows an item to be embeddable and stores its embedding stats,
and when it impacts and meets the requirements to stick into something, it instantiates an embedded component. Once the item falls out, the component is destroyed, while the
element survives to embed another day.
- Carbon embedding has all the classical embedding behavior, and tracks more events and signals. The main behaviors and hooks to look for are:
-- Every process tick, there is a chance to randomly proc pain, controlled by pain_chance. There may also be a chance for the object to fall out randomly, per fall_chance
-- Every time the mob moves, there is a chance to proc jostling pain, controlled by jostle_chance (and only 50% as likely if the mob is walking or crawling)
-- Various signals hooking into carbon topic() and the embed removal surgery in order to handle removals.
In addition, there are 2 cases of embedding: embedding, and sticking
- Embedding involves harmful and dangerous embeds, whether they cause brute damage, stamina damage, or a mix. This is the default behavior for embeddings, for when something is "pointy"
- Sticking occurs when an item should not cause any harm while embedding (imagine throwing a sticky ball of tape at someone, rather than a shuriken). An item is considered "sticky"
when it has 0 for both pain multiplier and jostle pain multiplier. It's a bit arbitrary, but fairly straightforward.
Stickables differ from embeds in the following ways:
-- Text descriptors use phrasing like "X is stuck to Y" rather than "X is embedded in Y"
-- There is no slicing sound on impact
-- All damage checks and bloodloss are skipped
*/
/datum/component/embedded
dupe_mode = COMPONENT_DUPE_ALLOWED
var/obj/item/bodypart/limb
var/obj/item/weapon
// all of this stuff is explained in _DEFINES/combat.dm
var/embed_chance // not like we really need it once we're already stuck in but hey
var/fall_chance
var/pain_chance
var/pain_mult
var/impact_pain_mult
var/remove_pain_mult
var/rip_time
var/ignore_throwspeed_threshold
var/jostle_chance
var/jostle_pain_mult
var/pain_stam_pct
///if both our pain multiplier and jostle pain multiplier are 0, we're harmless and can omit most of the damage related stuff
var/harmful
/datum/component/embedded/Initialize(obj/item/I,
datum/thrownthing/throwingdatum,
obj/item/bodypart/part,
embed_chance = EMBED_CHANCE,
fall_chance = EMBEDDED_ITEM_FALLOUT,
pain_chance = EMBEDDED_PAIN_CHANCE,
pain_mult = EMBEDDED_PAIN_MULTIPLIER,
remove_pain_mult = EMBEDDED_UNSAFE_REMOVAL_PAIN_MULTIPLIER,
impact_pain_mult = EMBEDDED_IMPACT_PAIN_MULTIPLIER,
rip_time = EMBEDDED_UNSAFE_REMOVAL_TIME,
ignore_throwspeed_threshold = FALSE,
jostle_chance = EMBEDDED_JOSTLE_CHANCE,
jostle_pain_mult = EMBEDDED_JOSTLE_PAIN_MULTIPLIER,
pain_stam_pct = EMBEDDED_PAIN_STAM_PCT)
if(!iscarbon(parent) || !isitem(I))
return COMPONENT_INCOMPATIBLE
if(part)
limb = part
src.embed_chance = embed_chance
src.fall_chance = fall_chance
src.pain_chance = pain_chance
src.pain_mult = pain_mult
src.remove_pain_mult = remove_pain_mult
src.rip_time = rip_time
src.impact_pain_mult = impact_pain_mult
src.ignore_throwspeed_threshold = ignore_throwspeed_threshold
src.jostle_chance = jostle_chance
src.jostle_pain_mult = jostle_pain_mult
src.pain_stam_pct = pain_stam_pct
src.weapon = I
if(!weapon.isEmbedHarmless())
harmful = TRUE
weapon.embedded(parent, part)
START_PROCESSING(SSdcs, src)
var/mob/living/carbon/victim = parent
limb._embed_object(weapon) // on the inside... on the inside...
weapon.forceMove(victim)
RegisterSignal(weapon, list(COMSIG_MOVABLE_MOVED, COMSIG_PARENT_QDELETING), .proc/weaponDeleted)
victim.visible_message(span_danger("[weapon] [harmful ? "embeds" : "sticks"] itself [harmful ? "in" : "to"] [victim]'s [limb.name]!"), span_userdanger("[weapon] [harmful ? "embeds" : "sticks"] itself [harmful ? "in" : "to"] your [limb.name]!"))
var/damage = weapon.throwforce
if(harmful)
victim.throw_alert(ALERT_EMBEDDED_OBJECT, /atom/movable/screen/alert/embeddedobject)
playsound(victim,'sound/weapons/bladeslice.ogg', 40)
weapon.add_mob_blood(victim)//it embedded itself in you, of course it's bloody!
damage += weapon.w_class * impact_pain_mult
SEND_SIGNAL(victim, COMSIG_ADD_MOOD_EVENT, "embedded", /datum/mood_event/embedded)
if(damage > 0)
var/armor = victim.run_armor_check(limb.body_zone, MELEE, "Your armor has protected your [limb.name].", "Your armor has softened a hit to your [limb.name].",I.armour_penetration, weak_against_armour = I.weak_against_armour)
limb.receive_damage(brute=(1-pain_stam_pct) * damage, stamina=pain_stam_pct * damage, blocked=armor, wound_bonus = I.wound_bonus, bare_wound_bonus = I.bare_wound_bonus, sharpness = I.get_sharpness())
/datum/component/embedded/Destroy()
var/mob/living/carbon/victim = parent
if(victim && !victim.has_embedded_objects())
victim.clear_alert(ALERT_EMBEDDED_OBJECT)
SEND_SIGNAL(victim, COMSIG_CLEAR_MOOD_EVENT, "embedded")
if(weapon)
UnregisterSignal(weapon, list(COMSIG_MOVABLE_MOVED, COMSIG_PARENT_QDELETING))
weapon = null
limb = null
return ..()
/datum/component/embedded/RegisterWithParent()
RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/jostleCheck)
RegisterSignal(parent, COMSIG_CARBON_EMBED_RIP, .proc/ripOut)
RegisterSignal(parent, COMSIG_CARBON_EMBED_REMOVAL, .proc/safeRemove)
RegisterSignal(parent, COMSIG_PARENT_ATTACKBY, .proc/checkTweeze)
/datum/component/embedded/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_MOVABLE_MOVED, COMSIG_CARBON_EMBED_RIP, COMSIG_CARBON_EMBED_REMOVAL, COMSIG_PARENT_ATTACKBY))
/datum/component/embedded/process(delta_time)
var/mob/living/carbon/victim = parent
if(!victim || !limb) // in case the victim and/or their limbs exploded (say, due to a sticky bomb)
weapon.forceMove(get_turf(weapon))
qdel(src)
return
if(victim.stat == DEAD)
return
var/damage = weapon.w_class * pain_mult
var/pain_chance_current = DT_PROB_RATE(pain_chance / 100, delta_time) * 100
if(pain_stam_pct && HAS_TRAIT_FROM(victim, TRAIT_INCAPACITATED, STAMINA)) //if it's a less-lethal embed, give them a break if they're already stamcritted
pain_chance_current *= 0.2
damage *= 0.5
else if(victim.body_position == LYING_DOWN)
pain_chance_current *= 0.2
if(harmful && prob(pain_chance_current))
limb.receive_damage(brute=(1-pain_stam_pct) * damage, stamina=pain_stam_pct * damage, wound_bonus = CANT_WOUND)
to_chat(victim, span_userdanger("[weapon] embedded in your [limb.name] hurts!"))
var/fall_chance_current = DT_PROB_RATE(fall_chance / 100, delta_time) * 100
if(victim.body_position == LYING_DOWN)
fall_chance_current *= 0.2
if(prob(fall_chance_current))
fallOut()
////////////////////////////////////////
////////////BEHAVIOR PROCS//////////////
////////////////////////////////////////
/// Called every time a carbon with a harmful embed moves, rolling a chance for the item to cause pain. The chance is halved if the carbon is crawling or walking.
/datum/component/embedded/proc/jostleCheck()
SIGNAL_HANDLER
var/mob/living/carbon/victim = parent
var/chance = jostle_chance
if(victim.m_intent == MOVE_INTENT_WALK || victim.body_position == LYING_DOWN)
chance *= 0.5
if(harmful && prob(chance))
var/damage = weapon.w_class * jostle_pain_mult
limb.receive_damage(brute=(1-pain_stam_pct) * damage, stamina=pain_stam_pct * damage, wound_bonus = CANT_WOUND)
to_chat(victim, span_userdanger("[weapon] embedded in your [limb.name] jostles and stings!"))
/// Called when then item randomly falls out of a carbon. This handles the damage and descriptors, then calls safe_remove()
/datum/component/embedded/proc/fallOut()
var/mob/living/carbon/victim = parent
if(harmful)
var/damage = weapon.w_class * remove_pain_mult
limb.receive_damage(brute=(1-pain_stam_pct) * damage, stamina=pain_stam_pct * damage, wound_bonus = CANT_WOUND)
victim.visible_message(span_danger("[weapon] falls [harmful ? "out" : "off"] of [victim.name]'s [limb.name]!"), span_userdanger("[weapon] falls [harmful ? "out" : "off"] of your [limb.name]!"))
safeRemove()
/// Called when a carbon with an object embedded/stuck to them inspects themselves and clicks the appropriate link to begin ripping the item out. This handles the ripping attempt, descriptors, and dealing damage, then calls safe_remove()
/datum/component/embedded/proc/ripOut(datum/source, obj/item/I, obj/item/bodypart/limb)
SIGNAL_HANDLER
if(I != weapon || src.limb != limb)
return
var/mob/living/carbon/victim = parent
var/time_taken = rip_time * weapon.w_class
INVOKE_ASYNC(src, .proc/complete_rip_out, victim, I, limb, time_taken)
/// everything async that ripOut used to do
/datum/component/embedded/proc/complete_rip_out(mob/living/carbon/victim, obj/item/I, obj/item/bodypart/limb, time_taken)
victim.visible_message(span_warning("[victim] attempts to remove [weapon] from [victim.p_their()] [limb.name]."),span_notice("You attempt to remove [weapon] from your [limb.name]... (It will take [DisplayTimeText(time_taken)].)"))
if(!do_after(victim, time_taken, target = victim))
return
if(!weapon || !limb || weapon.loc != victim || !(weapon in limb.embedded_objects))
qdel(src)
return
if(harmful)
var/damage = weapon.w_class * remove_pain_mult
limb.receive_damage(brute=(1-pain_stam_pct) * damage, stamina=pain_stam_pct * damage, sharpness=SHARP_EDGED) //It hurts to rip it out, get surgery you dingus. unlike the others, this CAN wound + increase slash bloodflow
victim.emote("scream")
victim.visible_message(span_notice("[victim] successfully rips [weapon] [harmful ? "out" : "off"] of [victim.p_their()] [limb.name]!"), span_notice("You successfully remove [weapon] from your [limb.name]."))
safeRemove(victim)
/// This proc handles the final step and actual removal of an embedded/stuck item from a carbon, whether or not it was actually removed safely.
/// If you want the thing to go into someone's hands rather than the floor, pass them in to_hands
/datum/component/embedded/proc/safeRemove(mob/to_hands)
SIGNAL_HANDLER
var/mob/living/carbon/victim = parent
limb._unembed_object(weapon)
UnregisterSignal(weapon, list(COMSIG_MOVABLE_MOVED, COMSIG_PARENT_QDELETING)) // have to do it here otherwise we trigger weaponDeleted()
if(!weapon.unembedded()) // if it hasn't deleted itself due to drop del
UnregisterSignal(weapon, list(COMSIG_MOVABLE_MOVED, COMSIG_PARENT_QDELETING))
if(to_hands)
INVOKE_ASYNC(to_hands, /mob.proc/put_in_hands, weapon)
else
weapon.forceMove(get_turf(victim))
qdel(src)
/// Something deleted or moved our weapon while it was embedded, how rude!
/datum/component/embedded/proc/weaponDeleted()
SIGNAL_HANDLER
var/mob/living/carbon/victim = parent
limb._unembed_object(weapon)
if(victim)
to_chat(victim, span_userdanger("\The [weapon] that was embedded in your [limb.name] disappears!"))
qdel(src)
/// The signal for listening to see if someone is using a hemostat on us to pluck out this object
/datum/component/embedded/proc/checkTweeze(mob/living/carbon/victim, obj/item/possible_tweezers, mob/user)
SIGNAL_HANDLER
if(!istype(victim) || possible_tweezers.tool_behaviour != TOOL_HEMOSTAT || user.zone_selected != limb.body_zone)
return
if(weapon != limb.embedded_objects[1]) // just pluck the first one, since we can't easily coordinate with other embedded components affecting this limb who is highest priority
return
if(ishuman(victim)) // check to see if the limb is actually exposed
var/mob/living/carbon/human/victim_human = victim
if(!victim_human.try_inject(user, limb.body_zone, INJECT_CHECK_IGNORE_SPECIES | INJECT_TRY_SHOW_ERROR_MESSAGE))
return TRUE
INVOKE_ASYNC(src, .proc/tweezePluck, possible_tweezers, user)
return COMPONENT_NO_AFTERATTACK
/// The actual action for pulling out an embedded object with a hemostat
/datum/component/embedded/proc/tweezePluck(obj/item/possible_tweezers, mob/user)
var/mob/living/carbon/victim = parent
var/self_pluck = (user == victim)
if(self_pluck)
user.visible_message(span_danger("[user] begins plucking [weapon] from [user.p_their()] [limb.name]"), span_notice("You start plucking [weapon] from your [limb.name]..."),\
vision_distance=COMBAT_MESSAGE_RANGE, ignored_mobs=victim)
else
user.visible_message(span_danger("[user] begins plucking [weapon] from [victim]'s [limb.name]"),span_notice("You start plucking [weapon] from [victim]'s [limb.name]..."), \
vision_distance=COMBAT_MESSAGE_RANGE, ignored_mobs=victim)
to_chat(victim, span_userdanger("[user] begins plucking [weapon] from your [limb.name]..."))
var/pluck_time = 2.5 SECONDS * weapon.w_class * (self_pluck ? 2 : 1)
if(!do_after(user, pluck_time, victim))
if(self_pluck)
to_chat(user, span_danger("You fail to pluck [weapon] from your [limb.name]."))
else
to_chat(user, span_danger("You fail to pluck [weapon] from [victim]'s [limb.name]."))
to_chat(victim, span_danger("[user] fails to pluck [weapon] from your [limb.name]."))
return
to_chat(user, span_notice("You successfully pluck [weapon] from [victim]'s [limb.name]."))
to_chat(victim, span_notice("[user] plucks [weapon] from your [limb.name]."))
safeRemove(user)