From c7e4e900044bb772511f8b671e74d55abf695667 Mon Sep 17 00:00:00 2001
From: RikuTheKiller <88713943+RikuTheKiller@users.noreply.github.com>
Date: Thu, 15 Jan 2026 07:30:35 +0200
Subject: [PATCH] Adds a new antag, the Blood Worm (#93787)
## LTS Document
Check this document before making any significant future changes to
blood worms, please.
https://hackmd.io/@RikuTheKiller/H1AHQSKNZx
## About The Pull Request
THIS PR SHOULD ABSOLUTELY BE TM'D FIRST
Blood worms are a new progression antag. When the event runs, 2
candidates are picked from ghosts and spawned in as blood worm
hatchlings, which then have to grow up, do a couple objectives and take
over the station.
Hatchlings are weak outside of a host, while juveniles can stand their
own reasonably well. Adults have high offensive power and can only be
dealt with using the right gear or a lot of luck and robustness. They're
meant to be a moment of glory for achieving maximum progression and they
can bootstrap the next hatchlings by gathering corpses before cocooning.
Each growth stage requires 30 seconds in a cocoon, which can only be
created after consuming a lot of blood. There's a falloff curve on a
per-blood-type basis, meaning you can't drain the same person over and
over again to reach adulthood. The medbay freezer is a priority target
for the blood worms and can get one of them to the juvenile stage if
fully ransacked.
It takes 500 blood to mature from hatchling to juvenile, and 1500 blood
to mature from juvenile to adult. You can only get up to 1000 blood from
synthetic sources like monkeys, and consuming synthetic blood is 30%
less efficient. Blood worms can also examine living targets to see how
much blood a target has, and how much growth the blood worm would gain
for consuming that blood.
Blood worms spawn in vents and have night vision for maneuvering in
maintenance. Hatchlings can ventcrawl, while juveniles can move around
by breaking things. Optionally, you can take over a host with a lot of
access like the Captain to go basically anywhere, especially if nobody
knows you killed the captain.
Behind the scenes, host-taking kicks the host's original mind to a
backseat mob. This needs the most testing in practice, but it's
confirmed that it returns the host's mind back to their body, at least
in testing.
All mob, ability and action sprites are made by INFRARED_BARON. Legal
rights were transferred to me after I paid for the commission.
Note, I've been working on this massive PR for quite a while, so
documenting every small change is really hard! Apologies for anything
I've missed. There's a lot.
Final note, admins can spawn these by either:
A. Trigger the midround event via the dynamic-panel verb, under the
Rulesets tab.
B. Giving someone the Blood Worm antag datum via the Traitor Panel in
the Player Panel for the target player. This will transform their mob
into a valid Blood Worm, with all of the associated objectives and such.
### Active Abilities
1. Leech Blood (No Host) - Lets the blood worm drain blood from living
targets and reagent containers. Uses an aggressive grab to restrain
living targets until leeching is over, which takes around a second to
initiate. Causes oxyloss during the leeching. NPC monkeys can't escape
from this and it floors targets as well.
2. Spit Blood (Both) - Multi-function ability, lets the blood worm fire
ranged corrosive blood spit at targets, melt restraints on their hosts
by right-clicking, and as an adult, shoot a burst of blood spit at a
target by right-clicking. Note of the right-click abilities, shooting
bursts can't be done while in a host. (to avoid unfair stealth kills)
Shooting a burst has a much longer cooldown than shooting normally. All
spit types cost blood to use.
3. Invade Corpse (No Host) - Lets the blood worm take a host for
themselves, consuming all of the host's blood and in essence, "becoming"
the host. Any bloodloss inflicted on the host is taken as damage to the
blood worm, and the blood worm retains its weakness to fire even in this
state. Burn damage itself no longer has any extra damage, though.
4. Leave Host (Host) - Title, literally just leaves the host after a
delay. Notably works even while the host is moving, dead, incapacitated
or otherwise fucked up in any way, shape or form.
5. Inject Blood (Host) - Lets the blood worm heal its host. The potency
of this increases as the worm grows up, but so does the cooldown and
blood consumption. This works on organ damage, injuries, etc.
6. Mature (No Host) - Makes the blood worm enter a cocoon for 30
seconds, emerging as the next growth stage. Requires an increasing
amount of consumed blood / growth as the blood worm uses it.
7. Reproduce (No Host, Adult Only) - Makes the blood worm enter a cocoon
for 30 seconds, with 4 hatchlings emerging out of it, including the
original blood worm, now reverted back into a hatchling as well.
8. Revive Host (Host) - If the host is in a viable state to be revived,
revives them after an animation sequence plays out.
### Passive Abilities
1. Space Immunity - Blood worms are immune to the cold, low pressures
and a lack of oxygen. Only the immunity to a lack of oxygen carries on
to hosts from this.
2. Organ Insertion - Blood worms can insert organs into their hosts by
right-clicking on them with the organ in-hand. This mainly exists to
deal with hosts that lack organs, and avoids the gotcha where an adult
blood worm ends up gutting their host by hitting them too hard, as they
can simply fix it on the spot.
3. Life Support - Blood worm hosts don't need a heart, lungs or a liver
to survive. Lungs are useful for speaking, and a liver is necessary to
process reagents.
4. Regeneration - Blood worms slowly heal over time. This is nowhere
near enough to overcome bleeding or heat damage, since it's 0.3 hp/s for
a hatchling, 0.4 hp/s for a juvenile and 0.5 hp/s for an adult.
5. Night Vision - Blood worms can see in the dark. This doesn't extend
to hosts.
6. Ventcrawling - Hatchling blood worms can ventcrawl.
7. Doorcrawling - Hatchling and juvenile blood worms can slide under
doors. Doing so takes 3 seconds for a hatchling and 5 seconds for a
juvenile.
8. HUD - Blood worms can tell how much blood targets have at a glance,
via a blood HUD bar exclusive to them. They can also tell apart other
blood worm hosts from normal people via an antag HUD. There's also an
examine message they can use on living targets for even more info.
### Weaknesses
1. Heat and Fire - Blood worms quickly die to heat, their bodies are
flammable and their blood will burn up if their host's core temperature
is too high. The main counter to this is getting a host with
flame-resistant gear.
2. Bleeding - While in a host, bleeding wounds will directly damage the
blood worm itself. How much a host needs to bleed before the worm dies
depends on their growth stage. Blood worm hosts keep bleeding even while
dead, so just keep hitting them and they'll die. Blood worms
automatically leave their hosts when they hit 10% health or lower, and
their hosts bleed 50% faster than normal people.
3. Stuns - Blood worms have no way of dealing with a stunned host other
than getting out. They can deal with any restraints by melting them,
though.
4. Testing - Security can order a blood worm testing crate from cargo,
either for a 20 minute cooldown via the security cargo interface
console, or for 10000 credits via the supply console. It contains 4
single-use testers that hurt a bit when applied, but are instant to use
and 100% accurate. The stopgap is that they're really fucking expensive
and only work once per item.
### Screenshot
## Why It's Good For The Game
Antagonist variety, mainly. This is basically Cortical Borers 2:
Electric Boogaloo.
Currently, we lack any antagonists with mind control abilities. That
really sucks!
I've also gotten a lot of positive feedback about the antagonist while
working on it.
This antagonist also has great potential for roleplay, as they can take
over hosts, surprise attack people by getting out of a dead corpse, talk
to each other using Wormspeak, etc.
I think we're also itching for variety on "pest" antagonists. Right now
we just have spiders and xenos. Everybody knows these two, so why not
mix it up a bit?
And as for balance? Blood worms are relatively easy to dispatch when you
know their weaknesses, which are extremely clear. Bleeding for hosts,
fire for either one, lasers for the worms themselves. As long as you get
the host in crit and keep hitting, you've pretty much won, and they
can't keep spamming Inject Blood forever since they'll quickly run out
of blood to use.
## Changelog
:cl:
add: Added a new heavy roundstart/midround antagonist, the Blood Worm.
Credit to INFRARED_BARON for the sprites!
fix: Removing traits based on a source no longer causes issues with
trait signals.
fix: High-priority effects no longer double-trigger due to subsystem
issues.
fix: Weighted averaging in reagent merging code has been band-aid fixed.
It's not the best, but it works.
/:cl:
---
code/__DEFINES/_flags.dm | 2 +
code/__DEFINES/antagonists.dm | 1 +
code/__DEFINES/atom_hud.dm | 5 +-
code/__DEFINES/blood.dm | 7 +
code/__DEFINES/blood_worm_defines.dm | 12 +
.../dcs/signals/signals_blood_worm.dm | 4 +
.../signals/signals_mob/signals_mob_living.dm | 3 +
.../signals/signals_mob/signals_mob_simple.dm | 2 +-
code/__DEFINES/is_helpers.dm | 2 +
code/__DEFINES/language.dm | 1 +
code/__DEFINES/living.dm | 7 +-
code/__DEFINES/logging.dm | 1 +
code/__DEFINES/mobfactions.dm | 2 +
code/__DEFINES/role_preferences.dm | 2 +
code/__DEFINES/sound.dm | 1 +
code/__DEFINES/traits/_traits.dm | 2 +-
code/__DEFINES/traits/declarations.dm | 15 +
code/__DEFINES/traits/sources.dm | 6 +
.../{level_traits.dm => _level_traits.dm} | 0
code/__HELPERS/events.dm | 19 +
code/__HELPERS/logging/antagonists.dm | 4 +
code/__HELPERS/mobs.dm | 1 +
code/_globalvars/blood_worms.dm | 1 +
code/_globalvars/lists/poll_ignore.dm | 1 +
code/_globalvars/traits/_traits.dm | 6 +
code/_globalvars/traits/admin_tooling.dm | 3 +
.../configuration/entries/general.dm | 4 +
.../dynamic/dynamic_ruleset_midround.dm | 61 ++-
.../dynamic/dynamic_ruleset_roundstart.dm | 34 ++
code/datums/actions/action.dm | 5 +
code/datums/brain_damage/split_personality.dm | 2 +-
code/datums/components/slide_under_doors.dm | 188 ++++++++
code/datums/components/synth_blood.dm | 39 ++
code/datums/elements/anti_self_harm.dm | 27 ++
code/datums/elements/blood_reagent.dm | 11 +
code/datums/elements/hand_organ_insertion.dm | 83 ++++
code/datums/hud.dm | 2 +
code/datums/mood.dm | 93 ++--
.../mood_events/generic_negative_events.dm | 5 +
.../mood_events/generic_positive_events.dm | 5 +
code/datums/status_effects/_status_effect.dm | 2 +-
code/game/data_huds.dm | 26 ++
code/game/objects/items/food/meatslab.dm | 22 +
code/game/sound/sound_keys/sound_keys.dm | 7 +
.../antagonists/_common/antag_datum.dm | 14 +-
.../blood_worm/actions/_base_action.dm | 21 +
.../antagonists/blood_worm/actions/cocoon.dm | 405 ++++++++++++++++++
.../blood_worm/actions/inject_blood.dm | 243 +++++++++++
.../blood_worm/actions/invade_corpse.dm | 66 +++
.../blood_worm/actions/leave_host.dm | 51 +++
.../blood_worm/actions/leech_blood.dm | 272 ++++++++++++
.../blood_worm/actions/revive_host.dm | 105 +++++
.../blood_worm/actions/spit_blood.dm | 282 ++++++++++++
.../blood_worm/blood_worm_antag.dm | 101 +++++
.../blood_worm_blood_consumption.dm | 38 ++
.../blood_worm/blood_worm_host_handling.dm | 260 +++++++++++
.../blood_worm/blood_worm_host_mob.dm | 17 +
.../antagonists/blood_worm/blood_worm_mob.dm | 403 +++++++++++++++++
.../blood_worm/blood_worm_objectives.dm | 81 ++++
.../antagonists/blood_worm/blood_worm_team.dm | 86 ++++
.../blood_worm/blood_worm_tester.dm | 87 ++++
.../blood_worm/blood_worm_text_feedback.dm | 97 +++++
code/modules/antagonists/brother/brother.dm | 2 +-
.../changeling/powers/expel_worm.dm | 26 ++
code/modules/cargo/packs/security.dm | 10 +
code/modules/language/_language_holder.dm | 9 +
code/modules/language/wormspeak.dm | 8 +
.../logging/categories/log_category_game.dm | 5 +
code/modules/mob/living/blood.dm | 71 ++-
.../mob/living/carbon/human/human_defines.dm | 2 +-
.../modules/mob/living/carbon/init_signals.dm | 11 +
code/modules/mob/living/carbon/life.dm | 10 +
code/modules/mob/living/init_signals.dm | 26 ++
code/modules/mob/living/life.dm | 3 +
code/modules/mob/living/living_defines.dm | 5 +-
code/modules/mob/mob.dm | 6 +
.../reagents/chemistry/holder/holder.dm | 2 +-
.../reagents/chemistry/recipes/others.dm | 20 +-
.../reagents/reagent_containers/blood_pack.dm | 3 +-
.../modules/reagents/withdrawal/_addiction.dm | 5 +
.../spell_types/shapeshift/_shapeshift.dm | 4 +-
.../screenshot_antag_icons_bloodworm.png | Bin 0 -> 508 bytes
...nshot_antag_icons_bloodworminfestation.png | Bin 0 -> 508 bytes
config/dynamic.toml | 20 +
config/logging.txt | 3 +
icons/hud/screen_gen.dmi | Bin 113998 -> 114129 bytes
icons/mob/actions/actions_blood_worm.dmi | Bin 0 -> 3719 bytes
icons/mob/actions/actions_changeling.dmi | Bin 10647 -> 24497 bytes
icons/mob/huds/antag_hud.dmi | Bin 5405 -> 5048 bytes
icons/mob/huds/hud.dmi | Bin 11413 -> 11442 bytes
.../inhands/equipment/security_lefthand.dmi | Bin 12179 -> 12683 bytes
.../inhands/equipment/security_righthand.dmi | Bin 13669 -> 12875 bytes
.../mob/nonhuman-player/blood_worm_32x32.dmi | Bin 0 -> 5727 bytes
.../mob/nonhuman-player/blood_worm_32x48.dmi | Bin 0 -> 3701 bytes
icons/obj/antags/blood_worm.dmi | Bin 0 -> 496 bytes
icons/obj/weapons/guns/projectiles.dmi | Bin 149951 -> 147060 bytes
icons/ui/chat/language.dmi | Bin 6213 -> 5437 bytes
sound/effects/alien/alien_spitacid.ogg | Bin 0 -> 15239 bytes
sound/effects/alien/alien_spitacid2.ogg | Bin 0 -> 17473 bytes
tgstation.dme | 28 +-
.../tgui/interfaces/AntagInfoBloodWorm.tsx | 166 +++++++
.../antagonists/antagonists/bloodworm.ts | 15 +
.../antagonists/bloodworminfestation.ts | 15 +
103 files changed, 3737 insertions(+), 93 deletions(-)
create mode 100644 code/__DEFINES/blood_worm_defines.dm
create mode 100644 code/__DEFINES/dcs/signals/signals_blood_worm.dm
rename code/__HELPERS/{level_traits.dm => _level_traits.dm} (100%)
create mode 100644 code/_globalvars/blood_worms.dm
create mode 100644 code/datums/components/slide_under_doors.dm
create mode 100644 code/datums/components/synth_blood.dm
create mode 100644 code/datums/elements/anti_self_harm.dm
create mode 100644 code/datums/elements/hand_organ_insertion.dm
create mode 100644 code/modules/antagonists/blood_worm/actions/_base_action.dm
create mode 100644 code/modules/antagonists/blood_worm/actions/cocoon.dm
create mode 100644 code/modules/antagonists/blood_worm/actions/inject_blood.dm
create mode 100644 code/modules/antagonists/blood_worm/actions/invade_corpse.dm
create mode 100644 code/modules/antagonists/blood_worm/actions/leave_host.dm
create mode 100644 code/modules/antagonists/blood_worm/actions/leech_blood.dm
create mode 100644 code/modules/antagonists/blood_worm/actions/revive_host.dm
create mode 100644 code/modules/antagonists/blood_worm/actions/spit_blood.dm
create mode 100644 code/modules/antagonists/blood_worm/blood_worm_antag.dm
create mode 100644 code/modules/antagonists/blood_worm/blood_worm_blood_consumption.dm
create mode 100644 code/modules/antagonists/blood_worm/blood_worm_host_handling.dm
create mode 100644 code/modules/antagonists/blood_worm/blood_worm_host_mob.dm
create mode 100644 code/modules/antagonists/blood_worm/blood_worm_mob.dm
create mode 100644 code/modules/antagonists/blood_worm/blood_worm_objectives.dm
create mode 100644 code/modules/antagonists/blood_worm/blood_worm_team.dm
create mode 100644 code/modules/antagonists/blood_worm/blood_worm_tester.dm
create mode 100644 code/modules/antagonists/blood_worm/blood_worm_text_feedback.dm
create mode 100644 code/modules/antagonists/changeling/powers/expel_worm.dm
create mode 100644 code/modules/language/wormspeak.dm
create mode 100644 code/modules/unit_tests/screenshots/screenshot_antag_icons_bloodworm.png
create mode 100644 code/modules/unit_tests/screenshots/screenshot_antag_icons_bloodworminfestation.png
create mode 100644 icons/mob/actions/actions_blood_worm.dmi
create mode 100644 icons/mob/nonhuman-player/blood_worm_32x32.dmi
create mode 100644 icons/mob/nonhuman-player/blood_worm_32x48.dmi
create mode 100644 icons/obj/antags/blood_worm.dmi
create mode 100644 sound/effects/alien/alien_spitacid.ogg
create mode 100644 sound/effects/alien/alien_spitacid2.ogg
create mode 100644 tgui/packages/tgui/interfaces/AntagInfoBloodWorm.tsx
create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/bloodworm.ts
create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/bloodworminfestation.ts
diff --git a/code/__DEFINES/_flags.dm b/code/__DEFINES/_flags.dm
index 53b1514ed6a..e54b939404c 100644
--- a/code/__DEFINES/_flags.dm
+++ b/code/__DEFINES/_flags.dm
@@ -318,6 +318,8 @@ GLOBAL_LIST_INIT(bitflags, list(1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 204
#define IGNORE_INCAPACITATED (1<<3)
/// Used to prevent important slowdowns from being abused by drugs like kronkaine
#define IGNORE_SLOWDOWNS (1<<4)
+/// Cancel the action if the user does another action (mainly via clicking)
+#define DO_AFTER_CHECK_NEXT_MOVE (1<<5)
// Spacevine-related flags
/// Is the spacevine / flower bud heat resistant
diff --git a/code/__DEFINES/antagonists.dm b/code/__DEFINES/antagonists.dm
index 60d1212540c..36225923fe0 100644
--- a/code/__DEFINES/antagonists.dm
+++ b/code/__DEFINES/antagonists.dm
@@ -422,6 +422,7 @@ GLOBAL_LIST_INIT(human_invader_antagonists, list(
#define ANTAG_GROUP_ARACHNIDS "Arachnid Infestation"
#define ANTAG_GROUP_ASHWALKERS "Ash Walkers"
#define ANTAG_GROUP_BIOHAZARDS "Biohazards"
+#define ANTAG_GROUP_BLOODY "Bloody Monstrosities"
#define ANTAG_GROUP_CLOWNOPS "Clown Operatives"
#define ANTAG_GROUP_ERT "Emergency Response Team"
#define ANTAG_GROUP_GLITCH "Digital Anomalies"
diff --git a/code/__DEFINES/atom_hud.dm b/code/__DEFINES/atom_hud.dm
index 7b65c539cd4..3c043eae400 100644
--- a/code/__DEFINES/atom_hud.dm
+++ b/code/__DEFINES/atom_hud.dm
@@ -46,8 +46,10 @@
#define DIAG_CAMERA_HUD "21"
/// Steady Hacked APC effect, visible only to Malf AIs
#define MALF_APC_HUD "22"
+/// Blood volume bar, visible to blood worms
+#define BLOOD_HUD "23"
/// Big Manipulator interaction point HUDs
-#define BIG_MANIP_HUD "23"
+#define BIG_MANIP_HUD "24"
//by default everything in the hud_list of an atom is an image
//a value in hud_list with one of these will change that behavior
@@ -65,6 +67,7 @@
#define DATA_HUD_AI_DETECT 8
#define DATA_HUD_FAN 9
#define DATA_HUD_MALF_APC 10
+#define DATA_HUD_BLOOD 11
/// cooldown for being shown the images for any particular data hud
#define ADD_HUD_TO_COOLDOWN 20
diff --git a/code/__DEFINES/blood.dm b/code/__DEFINES/blood.dm
index 92753667574..aacd75db56f 100644
--- a/code/__DEFINES/blood.dm
+++ b/code/__DEFINES/blood.dm
@@ -24,3 +24,10 @@
/// Mobs will drop everything
#define DROP_ALL_REMAINS (DROP_BRAIN | DROP_ORGANS | DROP_BODYPARTS | DROP_ITEMS)
+
+// Keys for indexing blood data lists. HIGHLY INCOMPLETE.
+/// Indexing a blood reagent data list with this returns how synthetic the blood is, used for blood worms to nerf common blood sources like monkeys.
+#define BLOOD_DATA_SYNTH_CONTENT "synth_content"
+
+/// Returns whether this mob always has synthetic blood. Used to cap growth for blood worms from easily accessible sources of blood.
+#define IS_BLOOD_ALWAYS_SYNTHETIC(mob) (!ishuman(mob) || HAS_TRAIT(mob, TRAIT_BORN_MONKEY) || HAS_TRAIT(mob, TRAIT_SPAWNED_MOB))
diff --git a/code/__DEFINES/blood_worm_defines.dm b/code/__DEFINES/blood_worm_defines.dm
new file mode 100644
index 00000000000..f0a5bed60d9
--- /dev/null
+++ b/code/__DEFINES/blood_worm_defines.dm
@@ -0,0 +1,12 @@
+#define BLOOD_WORM_BLOOD_TO_HEALTH (100 / BLOOD_VOLUME_NORMAL)
+#define BLOOD_WORM_HEALTH_TO_BLOOD (BLOOD_VOLUME_NORMAL / 100)
+
+/// The health percentage at which blood worms are forcibly kicked out from their hosts.
+#define BLOOD_WORM_EJECT_THRESHOLD 0.1
+
+/// How much faster blood worm hosts bleed. Required because blood worms, unlike people, don't die at ~60% blood, instead they leave hosts at 10% blood, and adults have a ton of blood.
+/// This means blood worms are ridiculously strong against one of their own weaknesses without this. Don't make this too high, because blood worms use blood to heal, attack and survive.
+#define BLOOD_WORM_BLEED_MOD 1.5
+
+/// Returns a blood worm from the mob if it's a blood worm or the host of one.
+#define FIND_BLOOD_WORM(mob) ((isbloodworm(mob) ? mob : null) || ((mob && HAS_TRAIT(mob, TRAIT_BLOOD_WORM_HOST)) ? (locate(/mob/living/basic/blood_worm) in mob) : null))
diff --git a/code/__DEFINES/dcs/signals/signals_blood_worm.dm b/code/__DEFINES/dcs/signals/signals_blood_worm.dm
new file mode 100644
index 00000000000..55ede86dbdf
--- /dev/null
+++ b/code/__DEFINES/dcs/signals/signals_blood_worm.dm
@@ -0,0 +1,4 @@
+/// From /mob/living/basic/blood_worm/proc/consume_blood() : (normal_blood_amount, synth_blood_amount, total_blood_amount)
+#define COMSIG_BLOOD_WORM_CONSUMED_BLOOD "blood_worm_consumed_blood"
+/// From /datum/action/cooldown/mob_cooldown/blood_worm/cocoon/adult/transfer() : ()
+#define COMSIG_BLOOD_WORM_REPRODUCED "blood_worm_reproduced"
diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm
index e6d71fefae7..4922c80f9d1 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm
@@ -370,6 +370,9 @@
/// From /mob/living/proc/mob_pickup() : (mob/living/user, obj/item/mob_holder/holder)
#define COMSIG_LIVING_SCOOPED_UP "living_scooped_up"
+/// From /mob/living/proc/update_blood_status(), sent when the return value of /mob/living/proc/can_have_blood() changes, but before the new blood status is applied : (had_blood, has_blood, old_blood_volume)
+#define COMSIG_LIVING_PRE_UPDATE_BLOOD_STATUS "living_pre_update_blood_status"
+
/// From /mob/living/proc/update_blood_status(), sent when the return value of /mob/living/proc/can_have_blood() changes : (had_blood, has_blood, old_blood_volume, new_blood_volume)
#define COMSIG_LIVING_UPDATE_BLOOD_STATUS "living_update_blood_status"
diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_simple.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_simple.dm
index cdbd0d3e432..e2f04dd8fe0 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_simple.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_simple.dm
@@ -5,7 +5,7 @@
#define COMSIG_SIMPLEMOB_TRANSFERPOTION "simplemob_transferpotion"
// /mob/living/simple_animal/hostile signals
-///before attackingtarget has happened, source is the attacker and target is the attacked
+///before attackingtarget has happened, source is the attacker and target is the attacked : (atom/target, is_adjacent, modifiers)
#define COMSIG_HOSTILE_PRE_ATTACKINGTARGET "hostile_pre_attackingtarget"
#define COMPONENT_HOSTILE_NO_ATTACK COMPONENT_CANCEL_ATTACK_CHAIN //cancel the attack, only works before attack happens
///after attackingtarget has happened, source is the attacker and target is the attacked, extra argument for if the attackingtarget was successful
diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm
index c5e9d39690f..c4cffab911e 100644
--- a/code/__DEFINES/is_helpers.dm
+++ b/code/__DEFINES/is_helpers.dm
@@ -177,6 +177,8 @@ GLOBAL_LIST_INIT(turfs_pass_meteor, typecacheof(list(
#define isstargazer(A) (istype(A, /mob/living/basic/heretic_summon/star_gazer))
+#define isbloodworm(A) (istype(A, /mob/living/basic/blood_worm))
+
/// returns whether or not the atom is either a basic mob OR simple animal
#define isanimal_or_basicmob(A) (istype(A, /mob/living/simple_animal) || istype(A, /mob/living/basic))
diff --git a/code/__DEFINES/language.dm b/code/__DEFINES/language.dm
index 9189ae20b2e..937fc987aa6 100644
--- a/code/__DEFINES/language.dm
+++ b/code/__DEFINES/language.dm
@@ -40,6 +40,7 @@
#define LANGUAGE_BABEL "babel"
#define LANGUAGE_EMP "emp"
#define LANGUAGE_TONGUE "tongue"
+#define LANGUAGE_BLOOD_WORM "blood_worm"
// Language flags. Used in granting and removing languages.
/// This language can be spoken.
diff --git a/code/__DEFINES/living.dm b/code/__DEFINES/living.dm
index ac2cb64efae..0aa8bee1064 100644
--- a/code/__DEFINES/living.dm
+++ b/code/__DEFINES/living.dm
@@ -12,8 +12,9 @@
#define STOP_OVERLAY_UPDATE_BODY_PARTS (1<<2)
/// Nutrition changed last life tick, so we should bulk update this tick
#define QUEUE_NUTRITION_UPDATE (1<<3)
-/// Blood volume has changed since the last [proc/update_blood_effects] call
-#define QUEUE_BLOOD_UPDATE (1<<4)
+/// Blood volume or status has changed since the last [proc/update_blood_effects] call.
+/// Nowhere near guaranteed to happen only once per life tick, or at all.
+#define BLOOD_UPDATE_QUEUED (1<<4)
/// This mob can have blood, cached value of [proc/can_have_blood]
#define LIVING_CAN_HAVE_BLOOD (1<<5)
@@ -21,6 +22,8 @@
#define GET_LYING_ANGLE(mob) (UNLINT(mob.lying_angle))
/// Checks if the mob can have blood
#define CAN_HAVE_BLOOD(mob) (mob.living_flags & LIVING_CAN_HAVE_BLOOD)
+/// Queues a blood update for the next life tick for the mob
+#define QUEUE_BLOOD_UPDATE(mob) mob.living_flags |= BLOOD_UPDATE_QUEUED
// Used in living mob offset list for determining pixel offsets
#define PIXEL_W_OFFSET "w"
diff --git a/code/__DEFINES/logging.dm b/code/__DEFINES/logging.dm
index 53a82a57555..8b730ef4659 100644
--- a/code/__DEFINES/logging.dm
+++ b/code/__DEFINES/logging.dm
@@ -141,6 +141,7 @@
// Game categories
#define LOG_CATEGORY_GAME "game"
#define LOG_CATEGORY_GAME_ACCESS "game-access"
+#define LOG_CATEGORY_GAME_BLOOD_WORM "game-blood-worm"
#define LOG_CATEGORY_GAME_EMOTE "game-emote"
#define LOG_CATEGORY_GAME_INTERNET_REQUEST "game-internet-request"
#define LOG_CATEGORY_GAME_OOC "game-ooc"
diff --git a/code/__DEFINES/mobfactions.dm b/code/__DEFINES/mobfactions.dm
index ae3b506484a..04126755f9d 100644
--- a/code/__DEFINES/mobfactions.dm
+++ b/code/__DEFINES/mobfactions.dm
@@ -13,6 +13,8 @@
#define FACTION_ASHWALKER "ashwalker"
/// Bears of all varieties
#define FACTION_BEAR "bear"
+/// Blood worms
+#define FACTION_BLOOD_WORM "blood_worm"
/// Megafauna bosses of mining
#define FACTION_BOSS "boss"
/// CARPS
diff --git a/code/__DEFINES/role_preferences.dm b/code/__DEFINES/role_preferences.dm
index 004f990bcdf..12e2718caad 100644
--- a/code/__DEFINES/role_preferences.dm
+++ b/code/__DEFINES/role_preferences.dm
@@ -8,6 +8,7 @@
//then you MUST update the database!
// Roundstart roles
+#define ROLE_BLOOD_WORM "Blood Worm"
#define ROLE_BROTHER "Blood Brother"
#define ROLE_CHANGELING "Changeling"
#define ROLE_CULTIST "Cultist"
@@ -23,6 +24,7 @@
#define ROLE_ALIEN "Xenomorph"
#define ROLE_BLOB "Blob"
#define ROLE_BLOB_INFECTION "Blob Infection"
+#define ROLE_BLOOD_WORM_INFESTATION "Blood Worm Infestation"
#define ROLE_CHANGELING_MIDROUND "Changeling (Midround)"
#define ROLE_FUGITIVE "Fugitive"
#define ROLE_LONE_OPERATIVE "Lone Operative"
diff --git a/code/__DEFINES/sound.dm b/code/__DEFINES/sound.dm
index 23fc91dca3b..1e6a51e23b2 100644
--- a/code/__DEFINES/sound.dm
+++ b/code/__DEFINES/sound.dm
@@ -320,6 +320,7 @@ GLOBAL_LIST_EMPTY(sfx_datum_by_key)
#define SFX_HARD_HAT_PICKUP "hard_hat_pickup"
#define SFX_HARD_HAT_DROP "hard_hat_drop"
#define SFX_HARD_HAT_EQUIP "hard_hat_equip"
+#define SFX_ALIEN_SPIT_ACID "alien_split_acid"
// Standard is 44.1khz
#define MIN_EMOTE_PITCH 40000
diff --git a/code/__DEFINES/traits/_traits.dm b/code/__DEFINES/traits/_traits.dm
index 0b1231c081f..c873152d2d5 100644
--- a/code/__DEFINES/traits/_traits.dm
+++ b/code/__DEFINES/traits/_traits.dm
@@ -104,7 +104,7 @@
}; \
if (!length(_L[_T])) { \
_L -= _T; \
- SEND_SIGNAL(target, SIGNAL_REMOVETRAIT(_T)); \
+ SEND_SIGNAL(target, SIGNAL_REMOVETRAIT(_T), _T); \
}; \
};\
if (!length(_L)) { \
diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm
index 3b8503f9706..38f63b681b2 100644
--- a/code/__DEFINES/traits/declarations.dm
+++ b/code/__DEFINES/traits/declarations.dm
@@ -262,6 +262,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_EASYBLEED "easybleed"
/// Mob recovers from addictions at an accelerated rate
#define TRAIT_ADDICTIONRESILIENT "addiction_resilient"
+/// Mob is immune to the effects of addictions
+#define TRAIT_NO_WITHDRAWALS "no_withdrawals"
#define TRAIT_TOXINLOVER "toxinlover"
/// Doesn't get overlays from being in critical.
#define TRAIT_NOCRITOVERLAY "no_crit_overlay"
@@ -444,6 +446,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_MEDICAL_HUD_SENSOR_ONLY "med_hud_lesser"
#define TRAIT_SECURITY_HUD "sec_hud"
#define TRAIT_SECURITY_HUD_ID_ONLY "sec_hud_lesser"
+#define TRAIT_BLOOD_HUD "blood_hud"
#define TRAIT_ABDUCTOR_HUD "abductor_hud"
/// Stop the user from seeing the sechud. Only works for trait handled sechuds.
#define TRAIT_BLOCK_SECHUD "block_sechud"
@@ -1616,6 +1619,18 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
/// Mob is artificially spawned rather than being created through more natural means - applied to monkey cubes and such
#define TRAIT_SPAWNED_MOB "spawned_mob"
+/// Mob is the host of a living blood worm.
+#define TRAIT_BLOOD_WORM_HOST "blood_worm_host"
+
+/// Mob just doesn't fucking care. No mood and no sanity.
+#define TRAIT_APATHETIC "apathetic"
+
+/// Mob can't be inflicted with the split personality trauma.
+#define TRAIT_NO_SPLIT_PERSONALITY "no_split_personality"
+
+/// Mob is shapeshifted, e.g. via a polymorph belt.
+#define TRAIT_SHAPESHIFTED "shapeshifted"
+
/// Turf is one that ai mobs will generally avoid pathing through
/// Doesn't need to be applied to any turfs that override can_cross_safely
#define TRAIT_AI_AVOID_TURF "warning_turf"
diff --git a/code/__DEFINES/traits/sources.dm b/code/__DEFINES/traits/sources.dm
index 8f40c3c8b8e..8cbe3049d0f 100644
--- a/code/__DEFINES/traits/sources.dm
+++ b/code/__DEFINES/traits/sources.dm
@@ -330,6 +330,12 @@
/// Permanent trait from an overdose effect
#define OVERDOSE_TRAIT "overdose"
+/// Trait from a blood worm being in a host
+#define BLOOD_WORM_HOST_TRAIT "blood_worm_host_trait"
+
+/// Trait from being under a door via [/datum/component/slide_under_doors]
+#define UNDER_DOOR_TRAIT "under_door_trait"
+
/// Trait gained from bioware surgeries
#define BIOWARE_TRAIT "bioware"
diff --git a/code/__HELPERS/level_traits.dm b/code/__HELPERS/_level_traits.dm
similarity index 100%
rename from code/__HELPERS/level_traits.dm
rename to code/__HELPERS/_level_traits.dm
diff --git a/code/__HELPERS/events.dm b/code/__HELPERS/events.dm
index 58d91c8caa8..ad937029026 100644
--- a/code/__HELPERS/events.dm
+++ b/code/__HELPERS/events.dm
@@ -47,6 +47,25 @@
return pick(possible_spawns)
+/// Finds us all suitable vent spawn locations on the station.
+/proc/find_vent_spawns()
+ var/list/vents = list()
+ var/list/vent_pumps = SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/atmospherics/components/unary/vent_pump)
+ for(var/obj/machinery/atmospherics/components/unary/vent_pump/temp_vent as anything in vent_pumps)
+ if(QDELETED(temp_vent))
+ continue
+ if(!is_station_level(temp_vent.loc.z) || temp_vent.welded)
+ continue
+ var/datum/pipeline/temp_vent_parent = temp_vent.parents[1]
+ if(!temp_vent_parent)
+ continue
+ // Stops antagonists getting stuck in small networks.
+ // See: Security, Virology
+ if(length(temp_vent_parent.other_atmos_machines) <= 20)
+ continue
+ vents += temp_vent
+ return vents
+
/proc/force_event(event_typepath, cause)
var/datum/round_event_control/our_event = locate(event_typepath) in SSevents.control
if(!our_event)
diff --git a/code/__HELPERS/logging/antagonists.dm b/code/__HELPERS/logging/antagonists.dm
index 5df39c69ade..514c4a3acfa 100644
--- a/code/__HELPERS/logging/antagonists.dm
+++ b/code/__HELPERS/logging/antagonists.dm
@@ -25,3 +25,7 @@
/// Logs bounties completed by spies and their rewards
/proc/log_spy(text, list/data)
logger.Log(LOG_CATEGORY_UPLINK_SPY, text, data)
+
+/// Logs blood worm events like entering/leaving a host.
+/proc/log_blood_worm(text, list/data)
+ logger.Log(LOG_CATEGORY_GAME_BLOOD_WORM, text, data)
diff --git a/code/__HELPERS/mobs.dm b/code/__HELPERS/mobs.dm
index a93f3d7402a..4a9d2e205e2 100644
--- a/code/__HELPERS/mobs.dm
+++ b/code/__HELPERS/mobs.dm
@@ -267,6 +267,7 @@ GLOBAL_LIST_INIT(skin_tone_names, list(
|| (!(timed_action_flags & IGNORE_USER_LOC_CHANGE) && !drifting && user.loc != user_loc) \
|| (!(timed_action_flags & IGNORE_HELD_ITEM) && user.get_active_held_item() != holding) \
|| (!(timed_action_flags & IGNORE_INCAPACITATED) && HAS_TRAIT(user, TRAIT_INCAPACITATED)) \
+ || ((timed_action_flags & DO_AFTER_CHECK_NEXT_MOVE) && world.time < user.next_move) \
|| (extra_checks && !extra_checks.Invoke()))
. = FALSE
break
diff --git a/code/_globalvars/blood_worms.dm b/code/_globalvars/blood_worms.dm
new file mode 100644
index 00000000000..e3ffde57443
--- /dev/null
+++ b/code/_globalvars/blood_worms.dm
@@ -0,0 +1 @@
+GLOBAL_DATUM(blood_worm_team, /datum/team/blood_worm)
diff --git a/code/_globalvars/lists/poll_ignore.dm b/code/_globalvars/lists/poll_ignore.dm
index 2a88c05de6a..5a16fa6bff1 100644
--- a/code/_globalvars/lists/poll_ignore.dm
+++ b/code/_globalvars/lists/poll_ignore.dm
@@ -5,6 +5,7 @@
#define POLL_IGNORE_ASH_SPIRIT "ash_spirit"
#define POLL_IGNORE_ASHWALKER "ashwalker"
#define POLL_IGNORE_BLOB "blob"
+#define POLL_IGNORE_BLOOD_WORM "blood_worm"
#define POLL_IGNORE_BOTS "bots"
#define POLL_IGNORE_CARGORILLA "cargorilla"
#define POLL_IGNORE_CONSTRUCT "construct"
diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm
index a975dc57457..3124abd35d7 100644
--- a/code/_globalvars/traits/_traits.dm
+++ b/code/_globalvars/traits/_traits.dm
@@ -179,6 +179,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_ANTIMAGIC" = TRAIT_ANTIMAGIC,
"TRAIT_ANTIMAGIC_NO_SELFBLOCK" = TRAIT_ANTIMAGIC_NO_SELFBLOCK,
"TRAIT_ANXIOUS" = TRAIT_ANXIOUS,
+ "TRAIT_APATHETIC" = TRAIT_APATHETIC,
"TRAIT_ATTEMPTING_CHRISTENING" = TRAIT_ATTEMPTING_CHRISTENING,
"TRAIT_BADDNA" = TRAIT_BADDNA,
"TRAIT_BADTOUCH" = TRAIT_BADTOUCH,
@@ -197,6 +198,8 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_BLOODY_MESS" = TRAIT_BLOODY_MESS,
"TRAIT_BLOOD_CLANS" = TRAIT_BLOOD_CLANS,
"TRAIT_BLOOD_FOUNTAIN" = TRAIT_BLOOD_FOUNTAIN,
+ "TRAIT_BLOOD_HUD" = TRAIT_BLOOD_HUD,
+ "TRAIT_BLOOD_WORM_HOST" = TRAIT_BLOOD_WORM_HOST,
"TRAIT_BOMBIMMUNE" = TRAIT_BOMBIMMUNE,
"TRAIT_BONSAI" = TRAIT_BONSAI,
"TRAIT_BOOZE_SLIDER" = TRAIT_BOOZE_SLIDER,
@@ -453,12 +456,14 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_NO_SLIP_SLIDE" = TRAIT_NO_SLIP_SLIDE,
"TRAIT_NO_SLIP_WATER" = TRAIT_NO_SLIP_WATER,
"TRAIT_NO_SOUL" = TRAIT_NO_SOUL,
+ "TRAIT_NO_SPLIT_PERSONALITY" = TRAIT_NO_SPLIT_PERSONALITY,
"TRAIT_NO_STAGGER" = TRAIT_NO_STAGGER,
"TRAIT_NO_STRIP" = TRAIT_NO_STRIP,
"TRAIT_NO_THROWING" = TRAIT_NO_THROWING,
"TRAIT_NO_TRANSFORM" = TRAIT_NO_TRANSFORM,
"TRAIT_NO_TWOHANDING" = TRAIT_NO_TWOHANDING,
"TRAIT_NO_UNDERWEAR" = TRAIT_NO_UNDERWEAR,
+ "TRAIT_NO_WITHDRAWALS" = TRAIT_NO_WITHDRAWALS,
"TRAIT_NO_ZOMBIFY" = TRAIT_NO_ZOMBIFY,
"TRAIT_HIPPOCRATIC_OATH" = TRAIT_HIPPOCRATIC_OATH,
"TRAIT_NUKEIMMUNE" = TRAIT_NUKEIMMUNE,
@@ -530,6 +535,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_SELF_AWARE" = TRAIT_SELF_AWARE,
"TRAIT_SELF_SURGERY" = TRAIT_SELF_SURGERY,
"TRAIT_SETTLER" = TRAIT_SETTLER,
+ "TRAIT_SHAPESHIFTED" = TRAIT_SHAPESHIFTED,
"TRAIT_SHAVED" = TRAIT_SHAVED,
"TRAIT_SHELL_RETREATED" = TRAIT_SHELL_RETREATED,
"TRAIT_SHIFTY_EYES" = TRAIT_SHIFTY_EYES,
diff --git a/code/_globalvars/traits/admin_tooling.dm b/code/_globalvars/traits/admin_tooling.dm
index 3332013b485..d44821c7857 100644
--- a/code/_globalvars/traits/admin_tooling.dm
+++ b/code/_globalvars/traits/admin_tooling.dm
@@ -49,6 +49,7 @@ GLOBAL_LIST_INIT(admin_visible_traits, list(
"TRAIT_ANTIMAGIC" = TRAIT_ANTIMAGIC,
"TRAIT_ANTIMAGIC_NO_SELFBLOCK" = TRAIT_ANTIMAGIC_NO_SELFBLOCK,
"TRAIT_ANXIOUS" = TRAIT_ANXIOUS,
+ "TRAIT_APATHETIC" = TRAIT_APATHETIC,
"TRAIT_BADDNA" = TRAIT_BADDNA,
"TRAIT_BADTOUCH" = TRAIT_BADTOUCH,
"TRAIT_BALD" = TRAIT_BALD,
@@ -204,10 +205,12 @@ GLOBAL_LIST_INIT(admin_visible_traits, list(
"TRAIT_NO_SLIP_SLIDE" = TRAIT_NO_SLIP_SLIDE,
"TRAIT_NO_SLIP_WATER" = TRAIT_NO_SLIP_WATER,
"TRAIT_NO_SOUL" = TRAIT_NO_SOUL,
+ "TRAIT_NO_SPLIT_PERSONALITY" = TRAIT_NO_SPLIT_PERSONALITY,
"TRAIT_NO_STAGGER" = TRAIT_NO_STAGGER,
"TRAIT_NO_TRANSFORM" = TRAIT_NO_TRANSFORM,
"TRAIT_NO_TWOHANDING" = TRAIT_NO_TWOHANDING,
"TRAIT_NO_UNDERWEAR" = TRAIT_NO_UNDERWEAR,
+ "TRAIT_NO_WITHDRAWALS" = TRAIT_NO_WITHDRAWALS,
"TRAIT_NO_ZOMBIFY" = TRAIT_NO_ZOMBIFY,
"TRAIT_NOBLOOD" = TRAIT_NOBLOOD,
"TRAIT_NOBREATH" = TRAIT_NOBREATH,
diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm
index d523dfc9866..51b0a579dbb 100644
--- a/code/controllers/configuration/entries/general.dm
+++ b/code/controllers/configuration/entries/general.dm
@@ -66,6 +66,10 @@
/// log login/logout
/datum/config_entry/flag/log_access
+/// log blood worm actions like entering/leaving hosts
+/datum/config_entry/flag/log_blood_worm
+ default = TRUE
+
/// Config entry which special logging of failed logins under suspicious circumstances.
/datum/config_entry/flag/log_suspicious_login
diff --git a/code/controllers/subsystem/dynamic/dynamic_ruleset_midround.dm b/code/controllers/subsystem/dynamic/dynamic_ruleset_midround.dm
index 95672517f9c..2feceaef761 100644
--- a/code/controllers/subsystem/dynamic/dynamic_ruleset_midround.dm
+++ b/code/controllers/subsystem/dynamic/dynamic_ruleset_midround.dm
@@ -435,7 +435,7 @@
max_antag_cap += prob(50) // 50% chance to get a second xeno, free!
/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/can_be_selected()
- return ..() && length(find_vents()) > 0
+ return ..() && length(find_vent_spawns()) > 0
/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/execute()
. = ..()
@@ -451,30 +451,53 @@
return new /mob/living/carbon/alien/larva
/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/create_execute_args()
- return list(find_vents())
+ return list(find_vent_spawns())
/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/assign_role(datum/mind/candidate, list/vent_list)
// xeno login gives antag datums
var/obj/vent = length(vent_list) >= 2 ? pick_n_take(vent_list) : vent_list[1]
candidate.current.move_into_vent(vent)
-/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/proc/find_vents()
- var/list/vents = list()
- var/list/vent_pumps = SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/atmospherics/components/unary/vent_pump)
- for(var/obj/machinery/atmospherics/components/unary/vent_pump/temp_vent as anything in vent_pumps)
- if(QDELETED(temp_vent))
- continue
- if(!is_station_level(temp_vent.loc.z) || temp_vent.welded)
- continue
- var/datum/pipeline/temp_vent_parent = temp_vent.parents[1]
- if(!temp_vent_parent)
- continue
- // Stops Aliens getting stuck in small networks.
- // See: Security, Virology
- if(length(temp_vent_parent.other_atmos_machines) <= 20)
- continue
- vents += temp_vent
- return vents
+/datum/dynamic_ruleset/midround/from_ghosts/blood_worms
+ name = "Blood Worm Infestation"
+ config_tag = "Midround Blood Worm"
+ preview_antag_datum = /datum/antagonist/blood_worm/infestation
+ // Please set this to HEAVY_MIDROUND once dynamic has fine-grained handling for spawn times and doesn't restrict all heavy midrounds to spawning after 40 minutes.
+ // Blood worms are intended to spawn 10-30 minutes into a round. This is a band-aid fix, and the better of two evils. (wrong threat tier vs wrong round timing)
+ midround_type = LIGHT_MIDROUND
+ false_alarm_able = TRUE
+ pref_flag = ROLE_BLOOD_WORM_INFESTATION
+ candidate_role = "Blood Worm"
+ ruleset_flags = RULESET_INVADER
+ weight = 2 // For reference, Nightmare has a weight of 5.
+ min_pop = 20 // Blood worms are limited by resources, so low pop means they have a harder time getting their tail in the door.
+ min_antag_cap = 1
+ max_antag_cap = 2
+ signup_atom_appearance = /mob/living/basic/blood_worm/juvenile
+
+/datum/dynamic_ruleset/midround/from_ghosts/blood_worms/can_be_selected()
+ return ..() && length(find_vent_spawns()) > 0
+
+/datum/dynamic_ruleset/midround/from_ghosts/blood_worms/execute()
+ . = ..()
+ addtimer(CALLBACK(src, PROC_REF(announce_worms)), rand(450, 750) SECONDS)
+
+/datum/dynamic_ruleset/midround/from_ghosts/blood_worms/create_ruleset_body()
+ return new /mob/living/basic/blood_worm/hatchling
+
+/datum/dynamic_ruleset/midround/from_ghosts/blood_worms/create_execute_args()
+ return list(find_vent_spawns())
+
+/datum/dynamic_ruleset/midround/from_ghosts/blood_worms/assign_role(datum/mind/candidate, list/vent_list)
+ candidate.add_antag_datum(/datum/antagonist/blood_worm)
+ var/obj/vent = length(vent_list) >= 2 ? pick_n_take(vent_list) : vent_list[1]
+ candidate.current.move_into_vent(vent)
+
+/datum/dynamic_ruleset/midround/from_ghosts/blood_worms/proc/announce_worms()
+ priority_announce("Unidentified lifesigns detected coming aboard [station_name()]. Secure any exterior access, including ducting and ventilation.", "Lifesign Alert", ANNOUNCER_ALIENS)
+
+/datum/dynamic_ruleset/midround/from_ghosts/blood_worms/false_alarm()
+ announce_worms()
/datum/dynamic_ruleset/midround/from_ghosts/nightmare
name = "Nightmare"
diff --git a/code/controllers/subsystem/dynamic/dynamic_ruleset_roundstart.dm b/code/controllers/subsystem/dynamic/dynamic_ruleset_roundstart.dm
index 417f7164c21..8554937f752 100644
--- a/code/controllers/subsystem/dynamic/dynamic_ruleset_roundstart.dm
+++ b/code/controllers/subsystem/dynamic/dynamic_ruleset_roundstart.dm
@@ -98,6 +98,40 @@
/datum/dynamic_ruleset/roundstart/changeling/assign_role(datum/mind/candidate)
candidate.add_antag_datum(/datum/antagonist/changeling)
+// Quick thing about this event, it turns out roundstart blood worms, despite being granted a host, are worse at antaggery in practice.
+// They have their static, so unlike blood worms who spawn in the middle of the round, they just kinda tend to do nothing. So having at least 2 is ideal.
+// This way, if one ends up being a fluke, the other can hopefully pick up the slack. It's a semi-rare event, so having a massive chance to just fizzle out isn't ideal.
+/datum/dynamic_ruleset/roundstart/blood_worm
+ name = "Blood Worms"
+ config_tag = "Roundstart Blood Worm"
+ preview_antag_datum = /datum/antagonist/blood_worm
+ pref_flag = ROLE_BLOOD_WORM
+ weight = 1
+ min_pop = 10 // Resource limited, spawning on lowpop is fine.
+ max_antag_cap = list("denominator" = 29) // +1 blood worm per 30 pop.
+ repeatable = FALSE // Yeah no.
+
+/datum/dynamic_ruleset/roundstart/blood_worm/is_valid_candidate(mob/living/candidate, client/candidate_client)
+ if (!..())
+ return FALSE
+
+ var/species_type = candidate_client.prefs.read_preference(/datum/preference/choiced/species)
+ var/datum/species/species = GLOB.species_prototypes[species_type]
+
+ return !(TRAIT_NOBLOOD in species.inherent_traits)
+
+/datum/dynamic_ruleset/roundstart/blood_worm/assign_role(datum/mind/candidate)
+ if (!CAN_HAVE_BLOOD(candidate.current))
+ CRASH("A roundstart blood worm tried to spawn into a candidate mob with no blood. This shouldn't happen, because we already checked for TRAIT_NOBLOOD in species traits.")
+
+ var/mob/living/carbon/human/host = candidate.current
+ var/mob/living/basic/blood_worm/hatchling/worm = new(get_turf(host))
+
+ candidate.transfer_to(worm)
+ candidate.add_antag_datum(/datum/antagonist/blood_worm/infestation)
+
+ worm.enter_host(host, silent = TRUE, gain_progress = FALSE)
+
/datum/dynamic_ruleset/roundstart/heretic
name = "Heretics"
config_tag = "Roundstart Heretics"
diff --git a/code/datums/actions/action.dm b/code/datums/actions/action.dm
index 5b72c3c21e0..64f2f2a6c2b 100644
--- a/code/datums/actions/action.dm
+++ b/code/datums/actions/action.dm
@@ -452,3 +452,8 @@
else
source.next_click = world.time + CLICK_CD_ACTIVATE_ABILITY
INVOKE_ASYNC(src, PROC_REF(Trigger))
+
+/// Used for setting the keybind via external sources.
+/datum/action/proc/set_key(new_full_key)
+ full_key = new_full_key
+ build_all_button_icons(UPDATE_BUTTON_STATUS)
diff --git a/code/datums/brain_damage/split_personality.dm b/code/datums/brain_damage/split_personality.dm
index 0cf605afc34..57c94e60b1d 100644
--- a/code/datums/brain_damage/split_personality.dm
+++ b/code/datums/brain_damage/split_personality.dm
@@ -21,7 +21,7 @@
/datum/brain_trauma/severe/split_personality/on_gain()
var/mob/living/brain_owner = owner
- if(brain_owner.stat == DEAD || !GET_CLIENT(brain_owner) || istype(get_area(brain_owner), /area/deathmatch)) //No use assigning people to a corpse or braindead
+ if(brain_owner.stat == DEAD || !GET_CLIENT(brain_owner) || istype(get_area(brain_owner), /area/deathmatch) || HAS_TRAIT(brain_owner, TRAIT_NO_SPLIT_PERSONALITY)) //No use assigning people to a corpse or braindead
return FALSE
. = ..()
make_backseats()
diff --git a/code/datums/components/slide_under_doors.dm b/code/datums/components/slide_under_doors.dm
new file mode 100644
index 00000000000..a43e9a66de9
--- /dev/null
+++ b/code/datums/components/slide_under_doors.dm
@@ -0,0 +1,188 @@
+// A component for basic mobs that makes them able to slide under doors by right-clicking on them.
+// Sliding under doors has a configurable initial delay, and allows the mob to remain under the door indefinitely.
+// If the door is opened while the mob is under it, then the mob will be ejected from their hiding spot.
+// Instantly sliding under doors should be handled by giving the mob the PASSDOORS pass flag.
+
+/datum/component/slide_under_doors
+ can_transfer = TRUE
+
+ /// The delay for sliding under a door in deciseconds.
+ var/slide_in_delay = 0
+ /// The delay for sliding out from under a door in deciseconds.
+ var/slide_out_delay = 0
+
+ /// The user that is currently under a door, if any.
+ var/mob/living/current_user = null
+ /// The door the user is currently under, if any.
+ var/obj/machinery/door/current_door = null
+
+/datum/component/slide_under_doors/Initialize(slide_in_delay = 5 SECONDS, slide_out_delay = 1 SECONDS)
+ if (!isbasicmob(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ src.slide_in_delay = slide_in_delay
+ src.slide_out_delay = slide_out_delay
+
+/datum/component/slide_under_doors/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_MOB_CLIENT_LOGIN, PROC_REF(on_user_login))
+ RegisterSignal(parent, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(on_user_unarmed_attack))
+
+ notify_user()
+
+/datum/component/slide_under_doors/UnregisterFromParent()
+ UnregisterSignal(parent, list(COMSIG_MOB_CLIENT_LOGIN, COMSIG_LIVING_UNARMED_ATTACK))
+
+ if (!QDELETED(current_user) && !QDELETED(current_door))
+ slide_out_from_under_door()
+ else if (current_user || current_door)
+ unregister_user_and_door()
+
+/datum/component/slide_under_doors/proc/on_user_login(mob/living/user, client/user_client)
+ SIGNAL_HANDLER
+ notify_user()
+
+/datum/component/slide_under_doors/proc/notify_user()
+ to_chat(parent, span_notice("You can slide under doors! Right-click on a door to slide under it."))
+
+/datum/component/slide_under_doors/proc/on_user_unarmed_attack(mob/living/user, atom/target, is_adjacent, modifiers)
+ SIGNAL_HANDLER
+ if (!modifiers[RIGHT_CLICK])
+ return
+ if (!istype(target, /obj/machinery/door))
+ return
+
+ INVOKE_ASYNC(src, PROC_REF(try_slide_under_door), user, target)
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+/datum/component/slide_under_doors/proc/try_slide_under_door(mob/living/user, obj/machinery/door/door)
+ if (!can_slide_under_door(user, door))
+ return
+
+ user.visible_message(
+ message = span_danger("\The [user] start[user.p_s()] sliding under \the [door]!"),
+ self_message = span_notice("You start sliding under \the [door]."),
+ blind_message = span_hear("You hear squeezing."),
+ )
+
+ playsound(user, 'sound/effects/footstep/gib_step.ogg', vol = 50, vary = TRUE, ignore_walls = FALSE)
+
+ if (!do_after(user, slide_in_delay, door, extra_checks = CALLBACK(src, PROC_REF(can_slide_under_door), user, door)))
+ return
+
+ slide_under_door(user, door)
+
+/datum/component/slide_under_doors/proc/can_slide_under_door(mob/living/user, obj/machinery/door/door)
+ if (!user.Adjacent(door))
+ return FALSE
+ if (user.loc == door)
+ return FALSE
+ if (!isturf(user.loc))
+ user.balloon_alert(user, "not on the ground!")
+ return FALSE
+ if (!door.IsReachableBy(user))
+ user.balloon_alert(user, "can't reach!")
+ return FALSE
+ if (HAS_TRAIT(user, TRAIT_INCAPACITATED))
+ user.balloon_alert(user, "incapacitated!")
+ return FALSE
+ if (!(door.pass_flags_self & PASSDOORS))
+ door.balloon_alert(user, "impassable!")
+ return FALSE
+ if (!door.density)
+ door.balloon_alert(user, "already open!")
+ return FALSE
+ return TRUE
+
+/datum/component/slide_under_doors/proc/slide_under_door(mob/living/user, obj/machinery/door/door)
+ user.visible_message(
+ message = span_danger("\The [user] slide[user.p_s()] under \the [door] with a pop!"),
+ self_message = span_notice("You slide under \the [door] with a pop!"),
+ blind_message = span_hear("You hear a pop."),
+ )
+
+ playsound(user, 'sound/effects/meatslap.ogg', vol = 50, vary = TRUE, ignore_walls = FALSE)
+
+ user.forceMove(door)
+
+ register_user_and_door(user, door)
+
+/datum/component/slide_under_doors/proc/try_slide_out_from_under_door(move_dir)
+ if (DOING_INTERACTION_WITH_TARGET(current_user, current_door))
+ return
+
+ var/turf/visible_turf = get_step(current_user, move_dir)
+
+ if (visible_turf)
+ visible_turf.visible_message(
+ message = span_danger("Something starts sliding out from under \the [current_door]!"),
+ blind_message = span_hear("You hear squeezing."),
+ ignored_mobs = current_user,
+ )
+
+ playsound(visible_turf, 'sound/effects/footstep/gib_step.ogg', vol = 50, vary = TRUE, ignore_walls = FALSE)
+
+ to_chat(current_user, span_notice("You start sliding out from under \the [current_door]."))
+
+ if (!do_after(current_user, slide_out_delay, current_door, timed_action_flags = IGNORE_INCAPACITATED))
+ return
+
+ slide_out_from_under_door(move_dir)
+
+/datum/component/slide_under_doors/proc/slide_out_from_under_door(move_dir)
+ // We need to store local references to the user and the door before unregistering them.
+ var/mob/living/user = current_user
+ var/obj/machinery/door/door = current_door
+
+ unregister_user_and_door()
+
+ user.forceMove(door.drop_location())
+
+ if (move_dir)
+ user.Move(get_step(user, move_dir), move_dir)
+
+ user.visible_message(
+ message = span_danger("\The [user] slide[user.p_s()] out from under \the [door] with a pop!"),
+ self_message = span_notice("You slide out from under \the [door] with a pop!"),
+ blind_message = span_hear("You hear a pop."),
+ )
+
+ playsound(user, 'sound/effects/meatslap.ogg', vol = 50, vary = TRUE, ignore_walls = FALSE)
+
+/datum/component/slide_under_doors/proc/register_user_and_door(mob/living/user, obj/machinery/door/door)
+ ADD_TRAIT(user, TRAIT_INCAPACITATED, UNDER_DOOR_TRAIT)
+ RegisterSignal(user, COMSIG_MOVABLE_MOVED, PROC_REF(on_user_moved))
+ current_user = user
+
+ RegisterSignal(door, COMSIG_QDELETING, PROC_REF(on_door_qdeleting))
+ RegisterSignal(door, COMSIG_ATOM_DENSITY_CHANGED, PROC_REF(on_door_density_changed))
+ RegisterSignal(door, COMSIG_ATOM_RELAYMOVE, PROC_REF(on_door_relaymove))
+ current_door = door
+
+/datum/component/slide_under_doors/proc/unregister_user_and_door()
+ if (current_user)
+ REMOVE_TRAITS_IN(current_user, UNDER_DOOR_TRAIT)
+ UnregisterSignal(current_user, COMSIG_MOVABLE_MOVED)
+ current_user = null
+
+ if (current_door)
+ UnregisterSignal(current_door, list(COMSIG_QDELETING, COMSIG_ATOM_DENSITY_CHANGED, COMSIG_ATOM_RELAYMOVE))
+ current_door = null
+
+/datum/component/slide_under_doors/proc/on_user_moved(mob/living/user, obj/machinery/door/door, dir, forced, list/old_locs)
+ SIGNAL_HANDLER
+ if (user.loc != door)
+ unregister_user_and_door()
+
+/datum/component/slide_under_doors/proc/on_door_qdeleting(obj/machinery/door/door, force)
+ SIGNAL_HANDLER
+ unregister_user_and_door()
+
+/datum/component/slide_under_doors/proc/on_door_density_changed(obj/machinery/door/door)
+ SIGNAL_HANDLER
+ if (!door.density)
+ slide_out_from_under_door()
+
+/datum/component/slide_under_doors/proc/on_door_relaymove(obj/machinery/door/door, mob/living/user, move_dir)
+ SIGNAL_HANDLER
+ INVOKE_ASYNC(src, PROC_REF(try_slide_out_from_under_door), move_dir)
+ return COMSIG_BLOCK_RELAYMOVE
diff --git a/code/datums/components/synth_blood.dm b/code/datums/components/synth_blood.dm
new file mode 100644
index 00000000000..7b835a752ed
--- /dev/null
+++ b/code/datums/components/synth_blood.dm
@@ -0,0 +1,39 @@
+/// Used for tracking mobs that have had synthetic blood injected into them.
+/// The higher the synth content, the more nerfed the blood is for blood worms.
+/// This decays over time while the mob is alive.
+/datum/component/synth_blood
+ // This has a performance improvement over COMPONENT_DUPE_HIGHLANDER as it skips creating a new component instance.
+ dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
+
+ /// 0-1 of how synthetic this mobs blood is right now.
+ var/synth_content = 0
+
+/datum/component/synth_blood/Initialize(new_synth_content = 0)
+ if (!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+ if (new_synth_content <= 0)
+ return COMPONENT_REDUNDANT
+
+ synth_content = min(new_synth_content, 1)
+
+ RegisterSignal(parent, COMSIG_LIVING_LIFE, PROC_REF(on_life))
+
+/datum/component/synth_blood/InheritComponent(datum/component/synth_blood/new_component, i_am_original, new_synth_content)
+ if (new_component)
+ new_synth_content = new_component.synth_content
+ if (new_synth_content <= 0)
+ qdel(src)
+
+ synth_content = min(new_synth_content, 1)
+
+/datum/component/synth_blood/Destroy(force)
+ UnregisterSignal(parent, COMSIG_LIVING_LIFE)
+ return ..()
+
+/datum/component/synth_blood/proc/on_life(mob/living/living_parent, seconds_per_tick, times_fired)
+ SIGNAL_HANDLER
+ if (synth_content <= 0)
+ qdel(src)
+ else if (living_parent.stat != DEAD)
+ // Ten minutes (or 600 seconds) to fully clear synth content from one to zero.
+ synth_content = max(synth_content - (1/600) * seconds_per_tick, 0)
diff --git a/code/datums/elements/anti_self_harm.dm b/code/datums/elements/anti_self_harm.dm
new file mode 100644
index 00000000000..97e32766ca9
--- /dev/null
+++ b/code/datums/elements/anti_self_harm.dm
@@ -0,0 +1,27 @@
+// Prevents a basic mob from hitting themselves by accident.
+// This is useful if you don't want self-harm as a balancing factor.
+// There are a lot of basic mobs that never benefit from hitting themselves.
+// Properly implementing this for carbons would require a more complex setup.
+
+/datum/element/anti_self_harm
+
+/datum/element/anti_self_harm/Attach(datum/target)
+ . = ..()
+ if (!isbasicmob(target))
+ return ELEMENT_INCOMPATIBLE
+ RegisterSignal(target, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(on_hostile_pre_attackingtarget))
+
+/datum/element/anti_self_harm/Detach(datum/target, ...)
+ UnregisterSignal(target, COMSIG_HOSTILE_PRE_ATTACKINGTARGET)
+ return ..()
+
+/datum/element/anti_self_harm/proc/on_hostile_pre_attackingtarget(mob/living/basic/parent_mob, atom/target, is_adjacent, modifiers)
+ SIGNAL_HANDLER
+ if (parent_mob != target)
+ return
+ if (modifiers[RIGHT_CLICK])
+ return
+
+ // A balloon alert would be too distracting mid-combat.
+ to_chat(parent_mob, span_warning("You decide against attacking yourself. You can still do so with right-click."))
+ return COMPONENT_HOSTILE_NO_ATTACK
diff --git a/code/datums/elements/blood_reagent.dm b/code/datums/elements/blood_reagent.dm
index 78ecae8c117..f71c1a7aca8 100644
--- a/code/datums/elements/blood_reagent.dm
+++ b/code/datums/elements/blood_reagent.dm
@@ -172,6 +172,17 @@
/datum/element/blood_reagent/proc/on_merge(datum/reagent/source, list/mix_data, amount)
SIGNAL_HANDLER
+ // Proportionally mix synth contents
+ if (source.data?[BLOOD_DATA_SYNTH_CONTENT] || mix_data?[BLOOD_DATA_SYNTH_CONTENT])
+ var/added_synth_volume = amount * mix_data?[BLOOD_DATA_SYNTH_CONTENT]
+ var/existing_synth_volume = source.volume * source.data?[BLOOD_DATA_SYNTH_CONTENT]
+
+ if (!source.data)
+ source.data = list()
+
+ // A simple weighted average that simplifies down to "total synth volume / total blood volume" i.e. "how much of the blood is synthetic"
+ source.data[BLOOD_DATA_SYNTH_CONTENT] = (added_synth_volume + existing_synth_volume) / (amount + source.volume)
+
// Presumably artificially generated blood
if (!source.data || !mix_data)
return
diff --git a/code/datums/elements/hand_organ_insertion.dm b/code/datums/elements/hand_organ_insertion.dm
new file mode 100644
index 00000000000..2320af18e4d
--- /dev/null
+++ b/code/datums/elements/hand_organ_insertion.dm
@@ -0,0 +1,83 @@
+/datum/element/hand_organ_insertion
+ element_flags = ELEMENT_BESPOKE
+ argument_hash_start_idx = 2
+
+ var/insertion_time = 0 SECONDS
+
+/datum/element/hand_organ_insertion/Attach(datum/target, insertion_time = 5 SECONDS)
+ . = ..()
+ if (!iscarbon(target))
+ return ELEMENT_INCOMPATIBLE
+
+ src.insertion_time = insertion_time
+
+ RegisterSignal(target, COMSIG_USER_ITEM_INTERACTION_SECONDARY, PROC_REF(on_item_interaction_secondary))
+
+/datum/element/hand_organ_insertion/Detach(datum/source, ...)
+ . = ..()
+
+ UnregisterSignal(source, COMSIG_USER_ITEM_INTERACTION_SECONDARY)
+
+/datum/element/hand_organ_insertion/proc/on_item_interaction_secondary(mob/living/carbon/parent, atom/target, obj/item/tool, list/modifiers)
+ SIGNAL_HANDLER
+ if (target != parent)
+ return FALSE
+ if (!isorgan(tool))
+ return FALSE
+
+ INVOKE_ASYNC(src, PROC_REF(attempt_to_insert_organ), parent, tool)
+ return TRUE
+
+/datum/element/hand_organ_insertion/proc/attempt_to_insert_organ(mob/living/carbon/user, obj/item/organ/organ)
+ if (!can_insert_organ(user, organ, feedback = TRUE))
+ return
+
+ var/zone_name = user.parse_zone_with_bodypart(organ.zone)
+
+ user.visible_message(
+ message = span_danger("\The [user] begin[user.p_s()] inserting \the [organ] into [user.p_their()] [zone_name]!"),
+ self_message = span_danger("You begin inserting \the [organ] into your [zone_name]!"),
+ blind_message = span_hear("You hear squelching!")
+ )
+
+ user.balloon_alert(user, "inserting...")
+
+ playsound(user, 'sound/items/handling/surgery/organ2.ogg', vol = 80, vary = TRUE, ignore_walls = FALSE)
+
+ if (!do_after(user, insertion_time, extra_checks = CALLBACK(src, PROC_REF(can_insert_organ), user, organ)))
+ user.balloon_alert(user, "interrupted!")
+ return
+
+ zone_name = user.parse_zone_with_bodypart(organ.zone)
+
+ user.visible_message(
+ message = span_danger("\The [user] insert[user.p_s()] \the [organ] into [user.p_their()] [zone_name]!"),
+ self_message = span_danger("You insert \the [organ] into your [zone_name]!"),
+ blind_message = span_hear("You hear a loud, final squelch!")
+ )
+
+ user.balloon_alert(user, "inserted!")
+
+ playsound(user, 'sound/items/handling/surgery/organ1.ogg', vol = 80, vary = TRUE, ignore_walls = FALSE)
+
+ user.temporarilyRemoveItemFromInventory(organ, force = TRUE)
+ organ.Insert(user)
+ organ.on_surgical_insertion(user, user, organ.zone, organ)
+
+/datum/element/hand_organ_insertion/proc/can_insert_organ(mob/living/carbon/user, obj/item/organ/organ, feedback = FALSE)
+ if (!user.get_bodypart(deprecise_zone(organ.zone)))
+ user.balloon_alert(user, "you don't have a [parse_zone(organ.zone)]!")
+ return FALSE
+
+ var/obj/item/organ/existing_organ = user.get_organ_slot(organ.slot)
+ if (existing_organ)
+ user.balloon_alert(user, "your [existing_organ] [existing_organ.p_are()] in the way!")
+ return FALSE
+
+ if (!organ.pre_surgical_insertion(user, user, organ.zone))
+ user.balloon_alert(user, "failed!")
+ return FALSE
+ if (!organ.useable)
+ user.balloon_alert(user, "unusable!")
+ return FALSE
+ return TRUE
diff --git a/code/datums/hud.dm b/code/datums/hud.dm
index 7741eeca095..4e69d701355 100644
--- a/code/datums/hud.dm
+++ b/code/datums/hud.dm
@@ -18,6 +18,7 @@ GLOBAL_ALIST_INIT(huds, alist(
DATA_HUD_AI_DETECT = new /datum/atom_hud/ai_detector(),
DATA_HUD_FAN = new /datum/atom_hud/data/human/fan_hud(),
DATA_HUD_MALF_APC = new /datum/atom_hud/data/malf_apc(),
+ DATA_HUD_BLOOD = new /datum/atom_hud/data/human/blood(),
))
/// Assoc list of traits to the huds they give.
@@ -31,6 +32,7 @@ GLOBAL_LIST_INIT(trait_to_hud, list(
TRAIT_MIME_FAN = DATA_HUD_FAN,
TRAIT_SECURITY_HUD = DATA_HUD_SECURITY_ADVANCED,
TRAIT_SECURITY_HUD_ID_ONLY = DATA_HUD_SECURITY_BASIC,
+ TRAIT_BLOOD_HUD = DATA_HUD_BLOOD,
))
/// Assoc list of traits that block other traits' huds to list of hud (traits) that they block
diff --git a/code/datums/mood.dm b/code/datums/mood.dm
index 1fc07130b13..6ee75391517 100644
--- a/code/datums/mood.dm
+++ b/code/datums/mood.dm
@@ -271,16 +271,17 @@
mood = 0
shown_mood = 0
- for(var/category in mood_events)
- var/datum/mood_event/the_event = mood_events[category]
- var/event_mood = the_event.mood_change
- event_mood *= max((event_mood > 0) ? positive_mood_modifier : negative_mood_modifier, 0)
- mood += event_mood
- if (!the_event.hidden)
- shown_mood += event_mood
+ if (!HAS_TRAIT(mob_parent, TRAIT_APATHETIC))
+ for(var/category in mood_events)
+ var/datum/mood_event/the_event = mood_events[category]
+ var/event_mood = the_event.mood_change
+ event_mood *= max((event_mood > 0) ? positive_mood_modifier : negative_mood_modifier, 0)
+ mood += event_mood
+ if (!the_event.hidden)
+ shown_mood += event_mood
- mood *= max(mood_modifier, 0)
- shown_mood *= max(mood_modifier, 0)
+ mood *= max(mood_modifier, 0)
+ shown_mood *= max(mood_modifier, 0)
switch(mood)
if (-INFINITY to MOOD_SAD4)
@@ -423,41 +424,44 @@
if(81 to INFINITY)
msg += "[span_boldwarning("I'm completely wasted.")]
"
- msg += span_notice("My current sanity: ") //Long term
- switch(sanity)
- if(SANITY_GREAT to INFINITY)
- msg += "[span_boldnicegreen("My mind feels like a temple!")]
"
- if(SANITY_NEUTRAL to SANITY_GREAT)
- msg += "[span_nicegreen("I have been feeling great lately!")]
"
- if(SANITY_DISTURBED to SANITY_NEUTRAL)
- msg += "[span_nicegreen("I have felt quite decent lately.")]
"
- if(SANITY_UNSTABLE to SANITY_DISTURBED)
- msg += "[span_warning("I'm feeling a little bit unhinged...")]
"
- if(SANITY_CRAZY to SANITY_UNSTABLE)
- msg += "[span_warning("I'm freaking out!!")]
"
- if(SANITY_INSANE to SANITY_CRAZY)
- msg += "[span_boldwarning("AHAHAHAHAHAHAHAHAHAH!!")]
"
+ if (HAS_TRAIT(mob_parent, TRAIT_APATHETIC))
+ msg += span_notice("My mood: [span_grey("I don't feel anything.")]
")
+ else
+ msg += span_notice("My current sanity: ") //Long term
+ switch(sanity)
+ if(SANITY_GREAT to INFINITY)
+ msg += "[span_boldnicegreen("My mind feels like a temple!")]
"
+ if(SANITY_NEUTRAL to SANITY_GREAT)
+ msg += "[span_nicegreen("I have been feeling great lately!")]
"
+ if(SANITY_DISTURBED to SANITY_NEUTRAL)
+ msg += "[span_nicegreen("I have felt quite decent lately.")]
"
+ if(SANITY_UNSTABLE to SANITY_DISTURBED)
+ msg += "[span_warning("I'm feeling a little bit unhinged...")]
"
+ if(SANITY_CRAZY to SANITY_UNSTABLE)
+ msg += "[span_warning("I'm freaking out!!")]
"
+ if(SANITY_INSANE to SANITY_CRAZY)
+ msg += "[span_boldwarning("AHAHAHAHAHAHAHAHAHAH!!")]
"
- msg += span_notice("My current mood: ") //Short term
- switch(mood_level)
- if(MOOD_LEVEL_SAD4)
- msg += "[span_boldwarning("I wish I was dead!")]
"
- if(MOOD_LEVEL_SAD3)
- msg += "[span_boldwarning("I feel terrible...")]
"
- if(MOOD_LEVEL_SAD2)
- msg += "[span_boldwarning("I feel very upset.")]
"
- if(MOOD_LEVEL_SAD1)
- msg += "[span_warning("I'm a bit sad.")]
"
- if(MOOD_LEVEL_NEUTRAL)
- msg += "[span_grey("I'm alright.")]
"
- if(MOOD_LEVEL_HAPPY1)
- msg += "[span_nicegreen("I feel pretty okay.")]
"
- if(MOOD_LEVEL_HAPPY2)
- msg += "[span_boldnicegreen("I feel pretty good.")]
"
- if(MOOD_LEVEL_HAPPY3)
- msg += "[span_boldnicegreen("I feel amazing!")]
"
- if(MOOD_LEVEL_HAPPY4)
- msg += "[span_boldnicegreen("I love life!")]
"
+ msg += span_notice("My current mood: ") //Short term
+ switch(mood_level)
+ if(MOOD_LEVEL_SAD4)
+ msg += "[span_boldwarning("I wish I was dead!")]
"
+ if(MOOD_LEVEL_SAD3)
+ msg += "[span_boldwarning("I feel terrible...")]
"
+ if(MOOD_LEVEL_SAD2)
+ msg += "[span_boldwarning("I feel very upset.")]
"
+ if(MOOD_LEVEL_SAD1)
+ msg += "[span_warning("I'm a bit sad.")]
"
+ if(MOOD_LEVEL_NEUTRAL)
+ msg += "[span_grey("I'm alright.")]
"
+ if(MOOD_LEVEL_HAPPY1)
+ msg += "[span_nicegreen("I feel pretty okay.")]
"
+ if(MOOD_LEVEL_HAPPY2)
+ msg += "[span_boldnicegreen("I feel pretty good.")]
"
+ if(MOOD_LEVEL_HAPPY3)
+ msg += "[span_boldnicegreen("I feel amazing!")]
"
+ if(MOOD_LEVEL_HAPPY4)
+ msg += "[span_boldnicegreen("I love life!")]
"
var/list/additional_lines = list()
SEND_SIGNAL(user, COMSIG_CARBON_MOOD_CHECK, additional_lines)
@@ -574,6 +578,9 @@
if (amount > maximum)
amount = min(amount, maximum)
+ if (HAS_TRAIT(mob_parent, TRAIT_APATHETIC))
+ amount = SANITY_NEUTRAL
+
if(amount == sanity) //Prevents stuff from flicking around.
return
diff --git a/code/datums/mood_events/generic_negative_events.dm b/code/datums/mood_events/generic_negative_events.dm
index 8bce84f73b4..fa358243f45 100644
--- a/code/datums/mood_events/generic_negative_events.dm
+++ b/code/datums/mood_events/generic_negative_events.dm
@@ -638,3 +638,8 @@
mood_change = 0
description = "I just got coated in blood. Fascinating!"
return
+
+/datum/mood_event/jabbed_with_tester
+ description = "Man, getting jabbed with that thing really sucked."
+ mood_change = -4
+ timeout = 5 MINUTES
diff --git a/code/datums/mood_events/generic_positive_events.dm b/code/datums/mood_events/generic_positive_events.dm
index cb4aa022224..b8a4e90ecc5 100644
--- a/code/datums/mood_events/generic_positive_events.dm
+++ b/code/datums/mood_events/generic_positive_events.dm
@@ -268,6 +268,11 @@
mood_change = 6
timeout = 5 MINUTES
+/datum/mood_event/blood_worm
+ description = "KILL, CONSUME, MULTIPLY, CONQUER."
+ mood_change = 999 // Makes it bold green and gives the special obj a higher priority. Blood worm hosts are apathetic, so this is otherwise meaningless.
+ hidden = TRUE
+
/datum/mood_event/family_heirloom
description = "My family heirloom is safe with me."
mood_change = 1
diff --git a/code/datums/status_effects/_status_effect.dm b/code/datums/status_effects/_status_effect.dm
index 4a569858655..8b4de47d9ea 100644
--- a/code/datums/status_effects/_status_effect.dm
+++ b/code/datums/status_effects/_status_effect.dm
@@ -100,7 +100,7 @@
/// Updates the status effect alert's maptext (if possible)
/datum/status_effect/proc/update_shown_duration()
- PRIVATE_PROC(TRUE)
+ PROTECTED_PROC(TRUE)
if(!linked_alert || !show_duration)
return
diff --git a/code/game/data_huds.dm b/code/game/data_huds.dm
index 8611d41305c..c5836e75016 100644
--- a/code/game/data_huds.dm
+++ b/code/game/data_huds.dm
@@ -68,6 +68,9 @@
/datum/atom_hud/data/malf_apc
hud_icons = list(MALF_APC_HUD)
+/datum/atom_hud/data/human/blood
+ hud_icons = list(BLOOD_HUD)
+
/* MED/SEC/DIAG HUD HOOKS */
/*
@@ -150,6 +153,21 @@ Medical HUD! Basic mode needs suit sensors on.
else
return "health-100"
+/// A helper for getting the appropriate icon state for the blood hud.
+/proc/round_blood_for_hud(mob/living/bloodbag)
+ var/blood_level = (bloodbag.get_blood_volume(apply_modifiers = TRUE) / BLOOD_VOLUME_NORMAL) * 100
+ switch(blood_level)
+ if(87.5 to INFINITY)
+ return "hudblood100"
+ if(62.5 to 87.5)
+ return "hudblood75"
+ if(37.5 to 62.5)
+ return "hudblood50"
+ if(12.5 to 37.5)
+ return "hudblood25"
+ if(-INFINITY to 12.5)
+ return "hudblood0"
+
//HOOKS
//called when a living mob changes health
@@ -496,6 +514,14 @@ Diagnostic HUDs!
SET_PLANE(holder,ABOVE_LIGHTING_PLANE,src)
set_hud_image_active(MALF_APC_HUD)
+/*~~~~~~~~~~~~
+ BLOOD FOR THE BLOOD GOD!!!
+~~~~~~~~~~~~~*/
+
+/mob/living/proc/blood_hud_set_status()
+ if (CAN_HAVE_BLOOD(src))
+ set_hud_image_state(BLOOD_HUD, round_blood_for_hud(src))
+
#define CACHED_WIDTH_INDEX "width"
#define CACHED_HEIGHT_INDEX "height"
diff --git a/code/game/objects/items/food/meatslab.dm b/code/game/objects/items/food/meatslab.dm
index 55fa78c3295..1a7d14df2af 100644
--- a/code/game/objects/items/food/meatslab.dm
+++ b/code/game/objects/items/food/meatslab.dm
@@ -506,6 +506,22 @@
) // Marble
starting_reagent_purity = 1
+/obj/item/food/meat/slab/blood_worm
+ name = "blood worm meat"
+ desc = "Who thought eating this would be a good idea? At least it's juicy..."
+ foodtypes = RAW | MEAT | BUGS | GORE
+ food_reagents = list(
+ /datum/reagent/blood = 30, // What did you expect?
+ /datum/reagent/consumable/nutriment/protein = 5, // Rich in protein.
+ /datum/reagent/consumable/nutriment/vitamin = 3, // Rich in vitamins.
+ /datum/reagent/consumable/nutriment/fat = 2, // Rather lean.
+ )
+ bite_consumption = 10
+ starting_reagent_purity = 1
+
+/obj/item/food/meat/slab/blood_worm/make_grillable()
+ AddComponent(/datum/component/grillable, /obj/item/food/meat/steak/blood_worm, rand(30 SECONDS, 90 SECONDS), TRUE, TRUE) //Add medium rare later maybe?
+
////////////////////////////////////// MEAT STEAKS ///////////////////////////////////////////////////////////
/obj/item/food/meat/steak
name = "steak"
@@ -619,6 +635,12 @@
tastes = list("pig" = 1)
foodtypes = MEAT
+/obj/item/food/meat/steak/blood_worm
+ name = "blood worm steak"
+ desc = "Now that's a bloody good steak!"
+ tastes = list("meat" = 1)
+ foodtypes = MEAT | BUGS
+
//////////////////////////////// MEAT CUTLETS ///////////////////////////////////////////////////////
//Raw cutlets
diff --git a/code/game/sound/sound_keys/sound_keys.dm b/code/game/sound/sound_keys/sound_keys.dm
index 52d97f4f8f6..d65615e707b 100644
--- a/code/game/sound/sound_keys/sound_keys.dm
+++ b/code/game/sound/sound_keys/sound_keys.dm
@@ -1136,3 +1136,10 @@
'sound/items/hard_hat/hard_hat_equip2.ogg',
'sound/items/hard_hat/hard_hat_equip3.ogg',
)
+
+/datum/sound_effect/alien_spit_acid
+ key = SFX_ALIEN_SPIT_ACID
+ file_paths = list(
+ 'sound/effects/alien/alien_spitacid.ogg',
+ 'sound/effects/alien/alien_spitacid2.ogg'
+ )
diff --git a/code/modules/antagonists/_common/antag_datum.dm b/code/modules/antagonists/_common/antag_datum.dm
index 1ba92e66292..4e6ade226cd 100644
--- a/code/modules/antagonists/_common/antag_datum.dm
+++ b/code/modules/antagonists/_common/antag_datum.dm
@@ -194,6 +194,10 @@ GLOBAL_LIST_EMPTY(antagonists)
if(old_body)
info_button.Remove(old_body)
info_button.Grant(new_body)
+ if (antag_moodlet)
+ if (old_body)
+ clear_antag_moodies(old_body)
+ give_antag_moodies(new_body)
apply_innate_effects(new_body)
if(new_body.stat != DEAD)
new_body.add_to_current_living_antags()
@@ -364,18 +368,20 @@ GLOBAL_LIST_EMPTY(antagonists)
/**
* Proc that assigns this antagonist's ascribed moodlet to the player.
*/
-/datum/antagonist/proc/give_antag_moodies()
+/datum/antagonist/proc/give_antag_moodies(mob/living/mob_override)
if(!antag_moodlet)
return
- owner.current.add_mood_event("antag_moodlet_[type]", antag_moodlet)
+ var/mob/living/target = mob_override || owner.current
+ target.add_mood_event("antag_moodlet_[type]", antag_moodlet)
/**
* Proc that removes this antagonist's ascribed moodlet from the player.
*/
-/datum/antagonist/proc/clear_antag_moodies()
+/datum/antagonist/proc/clear_antag_moodies(mob/living/mob_override)
if(!antag_moodlet)
return
- owner.current.clear_mood_event("antag_moodlet_[type]")
+ var/mob/living/target = mob_override || owner.current
+ target.clear_mood_event("antag_moodlet_[type]")
/**
* Proc that will return the team this antagonist belongs to, when called. Helpful with antagonists that may belong to multiple potential teams in a single round.
diff --git a/code/modules/antagonists/blood_worm/actions/_base_action.dm b/code/modules/antagonists/blood_worm/actions/_base_action.dm
new file mode 100644
index 00000000000..0dd381b6d79
--- /dev/null
+++ b/code/modules/antagonists/blood_worm/actions/_base_action.dm
@@ -0,0 +1,21 @@
+/datum/action/cooldown/mob_cooldown/blood_worm
+ abstract_type = /datum/action/cooldown/mob_cooldown/blood_worm
+
+ background_icon = 'icons/mob/actions/backgrounds.dmi'
+ background_icon_state = "bg_demon"
+
+ button_icon = 'icons/mob/actions/actions_blood_worm.dmi'
+
+ overlay_icon = 'icons/mob/actions/backgrounds.dmi'
+ overlay_icon_state = "bg_demon_border"
+ active_overlay_icon_state = "bg_spell_border_active_red"
+
+ ranged_mousepointer = 'icons/effects/mouse_pointers/weapon_pointer.dmi'
+
+/datum/action/cooldown/mob_cooldown/blood_worm/set_click_ability(mob/on_who)
+ . = ..()
+ build_all_button_icons(UPDATE_BUTTON_OVERLAY)
+
+/datum/action/cooldown/mob_cooldown/blood_worm/unset_click_ability(mob/on_who, refund_cooldown)
+ . = ..()
+ build_all_button_icons(UPDATE_BUTTON_OVERLAY)
diff --git a/code/modules/antagonists/blood_worm/actions/cocoon.dm b/code/modules/antagonists/blood_worm/actions/cocoon.dm
new file mode 100644
index 00000000000..2fa5facf264
--- /dev/null
+++ b/code/modules/antagonists/blood_worm/actions/cocoon.dm
@@ -0,0 +1,405 @@
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon
+ cooldown_time = 30 SECONDS
+ shared_cooldown = NONE
+
+ click_to_activate = FALSE
+
+ var/cocoon_type = null
+ var/obj/structure/blood_worm_cocoon/cocoon = null
+
+ var/new_worm_type = null
+
+ var/total_blood_required = 0
+
+ var/timer_id = null
+
+ var/cocoon_time = 30 SECONDS
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/Grant(mob/granted_to)
+ . = ..()
+ if (!owner)
+ return
+
+ RegisterSignal(owner, COMSIG_MOB_STATCHANGE, PROC_REF(on_worm_stat_changed), override = TRUE)
+ RegisterSignal(owner, COMSIG_BLOOD_WORM_CONSUMED_BLOOD, PROC_REF(update_status_on_signal))
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/Remove(mob/removed_from)
+ if (!QDELETED(cocoon))
+ cancel()
+ UnregisterSignal(owner, COMSIG_BLOOD_WORM_CONSUMED_BLOOD)
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/IsAvailable(feedback)
+ if (!istype(owner, /mob/living/basic/blood_worm))
+ return FALSE
+ if (!ispath(cocoon_type, /obj/structure/blood_worm_cocoon))
+ return FALSE
+ if (!ispath(new_worm_type, /mob/living/basic/blood_worm))
+ return FALSE
+ if (HAS_TRAIT(owner, TRAIT_SHAPESHIFTED))
+ if (feedback)
+ owner.balloon_alert(owner, "not while shapeshifted!")
+ return FALSE
+ if (!isturf(owner.loc))
+ if (feedback)
+ owner.balloon_alert(owner, "get on the ground!")
+ return FALSE
+ if (!check_consumed_blood(feedback))
+ return FALSE
+ if (!QDELETED(cocoon))
+ return FALSE
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/Activate(atom/target)
+ owner.visible_message(
+ message = span_danger("\The [owner] start[owner.p_s()] growing a cocoon!"),
+ self_message = span_notice("You start growing a cocoon."),
+ blind_message = span_hear("You start hearing fleshy knitting!")
+ )
+
+ if (!do_after(owner, 5 SECONDS, extra_checks = CALLBACK(src, PROC_REF(check_consumed_blood))))
+ return FALSE
+
+ owner.visible_message(
+ message = span_danger("\The [owner] enter[owner.p_s()] a cocoon!"),
+ self_message = span_green("You enter your freshly grown cocoon!"),
+ blind_message = span_hear("You stop hearing fleshy knitting!")
+ )
+
+ cocoon = new cocoon_type(get_turf(owner))
+
+ playsound(cocoon, 'sound/effects/blob/blobattack.ogg', vol = 60, vary = TRUE, ignore_walls = FALSE)
+
+ owner.forceMove(cocoon)
+
+ owner.add_traits(list(TRAIT_INCAPACITATED, TRAIT_IMMOBILIZED, TRAIT_MUTE, TRAIT_EMOTEMUTE), REF(src))
+
+ RegisterSignal(owner, COMSIG_MOVABLE_MOVED, PROC_REF(on_worm_moved))
+ RegisterSignal(cocoon, COMSIG_QDELETING, PROC_REF(on_cocoon_qdel))
+
+ INVOKE_ASYNC(src, PROC_REF(handle_timer))
+
+ return TRUE
+
+/// Override this if you want special timer behaviors like polling ghosts for hatchling candidates.
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/proc/handle_timer()
+ timer_id = addtimer(CALLBACK(src, PROC_REF(finalize)), cocoon_time, TIMER_UNIQUE | TIMER_STOPPABLE | TIMER_DELETE_ME)
+
+/// Called upon successfully finishing the incubation process.
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/proc/finalize()
+ var/mob/living/basic/blood_worm/new_worm = new new_worm_type(get_turf(cocoon))
+
+ transfer(owner, new_worm)
+
+ for (var/mob/living/unfortunate_observer in view(3, cocoon))
+ if (new_worm.faction_check_atom(unfortunate_observer))
+ continue // Don't harm our siblings.
+ if (unfortunate_observer.stat == DEAD)
+ continue // Harms potential hosts.
+
+ unfortunate_observer.visible_message(
+ message = span_danger("\The [unfortunate_observer] is splashed with a wave of corrosive blood!"),
+ self_message = span_userdanger("You're splashed with a wave of corrosive blood! YEOWCH!"),
+ blind_message = span_hear("You hear sizzling!")
+ )
+
+ unfortunate_observer.Knockdown(3 SECONDS)
+ unfortunate_observer.adjust_fire_loss(rand(30, 50))
+
+ var/range = 4 - get_dist(cocoon, unfortunate_observer)
+ unfortunate_observer.throw_at(get_ranged_target_turf_direct(cocoon, unfortunate_observer, range), range = range, speed = 2)
+
+ for (var/turf/turf in view(3, cocoon))
+ if (prob(100 - get_dist(cocoon, turf) * 20))
+ new /obj/effect/decal/cleanable/blood(turf)
+
+ playsound(cocoon, 'sound/effects/splat.ogg', vol = 100, vary = TRUE, ignore_walls = FALSE)
+
+ shared_unregister_cocoon()
+
+ qdel(owner)
+
+/// Transfers the owning blood worm from one worm mob to another.
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/proc/transfer(mob/living/basic/blood_worm/old_worm, mob/living/basic/blood_worm/new_worm)
+ old_worm.mind?.transfer_to(new_worm)
+
+ new_worm.id_number = old_worm.id_number
+ new_worm.update_name()
+
+ // Safety check: If someone gets turned into a blood worm, they need to keep their old mind name.
+ if (new_worm.mind.name == old_worm.real_name)
+ new_worm.mind.name = new_worm.real_name
+
+ new_worm.consumed_normal_blood = old_worm.consumed_normal_blood
+ new_worm.consumed_synth_blood = old_worm.consumed_synth_blood
+
+ new_worm.spit_action?.set_key(old_worm.spit_action?.full_key)
+ new_worm.leech_action?.set_key(old_worm.leech_action?.full_key)
+ new_worm.invade_action?.set_key(old_worm.invade_action?.full_key)
+ new_worm.cocoon_action?.set_key(old_worm.cocoon_action?.full_key)
+
+ new_worm.transfuse_action?.set_key(old_worm.transfuse_action?.full_key)
+ new_worm.eject_action?.set_key(old_worm.eject_action?.full_key)
+ new_worm.revive_action?.set_key(old_worm.revive_action?.full_key)
+
+ new_worm.cocoon_action?.StartCooldown()
+
+/// Cancels the incubation process, destroying the cocoon early.
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/proc/cancel()
+ cocoon.visible_message(
+ message = span_danger("\The [cocoon] fall[cocoon.p_s()] apart, expelling \the [owner] within."),
+ blind_message = span_danger("You hear a splat!"),
+ ignored_mobs = owner
+ )
+
+ if (!QDELETED(owner) && owner.stat != DEAD)
+ to_chat(owner, span_userdanger("Your cocoon falls apart!"))
+
+ playsound(cocoon, 'sound/effects/splat.ogg', vol = 60, vary = TRUE, ignore_walls = FALSE)
+
+ // A little less punishing since you need a do_after to set it up again after anyway, and because this can occur due to adults canceling Reproduce for meta reasons outside of their control.
+ StartCooldown(10 SECONDS)
+
+ shared_unregister_cocoon()
+
+/// Unregisters the cocoon. Used by both [proc/cancel] and [proc/finalize].
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/proc/shared_unregister_cocoon()
+ UnregisterSignal(owner, COMSIG_MOVABLE_MOVED)
+ UnregisterSignal(cocoon, COMSIG_QDELETING)
+
+ owner.remove_traits(list(TRAIT_INCAPACITATED, TRAIT_IMMOBILIZED, TRAIT_MUTE, TRAIT_EMOTEMUTE), REF(src))
+
+ if (!QDELETED(owner))
+ owner.forceMove(cocoon.drop_location())
+
+ if (!QDELETED(cocoon))
+ qdel(cocoon)
+
+ cocoon = null
+
+ if (timer_id)
+ deltimer(timer_id)
+ timer_id = null
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/proc/on_worm_stat_changed(datum/source, new_stat, old_stat)
+ SIGNAL_HANDLER
+ if (cocoon && old_stat != DEAD && new_stat == DEAD) // Alive -> Dead
+ cancel()
+ update_status_on_signal(source, new_stat, old_stat)
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/proc/on_worm_moved(datum/source, atom/old_loc, dir, forced, list/old_locs)
+ SIGNAL_HANDLER
+ cancel()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/proc/on_cocoon_qdel(datum/source)
+ SIGNAL_HANDLER
+ cancel()
+
+/// Checks if the blood worm has consumed enough blood to use this action.
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/proc/check_consumed_blood(feedback = FALSE)
+ var/mob/living/basic/blood_worm/worm = owner
+ var/total_consumed_blood = worm.get_consumed_blood()
+
+ if (total_consumed_blood < total_blood_required)
+ if (feedback)
+ worm.balloon_alert(worm, "only at [FLOOR(total_consumed_blood / total_blood_required * 100, 1)]% of required growth!")
+ return FALSE
+ return TRUE
+
+/obj/structure/blood_worm_cocoon
+ icon = 'icons/mob/nonhuman-player/blood_worm_32x32.dmi'
+
+ faction = list(FACTION_BLOOD_WORM)
+
+ density = TRUE
+ anchored = TRUE
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/hatchling
+ name = "Mature"
+ desc = "Enter incubation in a cocoon, emerging as a juvenile blood worm."
+
+ button_icon_state = "mature_hatchling"
+
+ cocoon_type = /obj/structure/blood_worm_cocoon/hatchling
+ new_worm_type = /mob/living/basic/blood_worm/juvenile
+
+ total_blood_required = 500
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/hatchling/Activate(atom/target)
+ if (tgui_alert(owner, "Are you sure? After [cocoon_time / 10] seconds, you will become a juvenile, gaining stat increases and the ability to spit corrosive blood, but losing the ability to ventcrawl.", "Mature", list("Yes", "No"), 30 SECONDS) != "Yes")
+ return
+ if (!IsAvailable(feedback = TRUE))
+ return
+
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/hatchling/transfer(mob/living/basic/blood_worm/old_worm, mob/living/basic/blood_worm/new_worm)
+ . = ..()
+
+ log_blood_worm("[key_name(new_worm)] finished maturing into a juvenile blood worm")
+
+/obj/structure/blood_worm_cocoon/hatchling
+ name = "small blood cocoon"
+ desc = "The incubation cocoon of a hatchling blood worm. Its surface is slowly shifting."
+
+ icon_state = "cocoon-small"
+
+ max_integrity = 100
+ damage_deflection = 10
+
+/obj/structure/blood_worm_cocoon/hatchling/examine(mob/user)
+ return ..() + span_warning("It can be broken to prevent the blood worm from maturing.")
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/juvenile
+ name = "Mature"
+ desc = "Enter incubation in a cocoon, emerging as an adult blood worm."
+
+ button_icon_state = "mature_juvenile"
+
+ cocoon_type = /obj/structure/blood_worm_cocoon/juvenile
+ new_worm_type = /mob/living/basic/blood_worm/adult
+
+ total_blood_required = 1500
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/juvenile/Activate(atom/target)
+ if (tgui_alert(owner, "Are you sure? After [cocoon_time / 10] seconds, you will become an adult, gaining stat increases and the ability to spit bursts of corrosive blood by right-clicking with Spit Blood while outside of a host.", "Mature", list("Yes", "No"), 30 SECONDS) != "Yes")
+ return
+ if (!IsAvailable(feedback = TRUE))
+ return
+
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/juvenile/transfer(mob/living/basic/blood_worm/old_worm, mob/living/basic/blood_worm/new_worm)
+ . = ..()
+
+ log_blood_worm("[key_name(new_worm)] finished maturing into an adult blood worm")
+
+/obj/structure/blood_worm_cocoon/juvenile
+ name = "medium blood cocoon"
+ desc = "The incubation cocoon of a juvenile blood worm. Its surface is slowly shifting."
+
+ icon_state = "cocoon-medium"
+
+ max_integrity = 150
+ damage_deflection = 15
+
+/obj/structure/blood_worm_cocoon/juvenile/examine(mob/user)
+ return ..() + span_warning("It can be broken to prevent the blood worm from maturing, but it looks rather tough.")
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/adult
+ name = "Reproduce"
+ desc = "Enter incubation in a cocoon, creating 4 new hatchlings including yourself."
+
+ button_icon_state = "reproduce"
+
+ cocoon_type = /obj/structure/blood_worm_cocoon/adult
+ new_worm_type = /mob/living/basic/blood_worm/hatchling
+
+ total_blood_required = 0
+
+ var/num_hatchlings = 3 // in addition to the original
+
+ var/list/candidates = null
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/adult/Grant(mob/granted_to)
+ . = ..()
+ if (!owner)
+ return
+
+ RegisterSignal(SSdcs, COMSIG_GLOB_MOB_LOGGED_IN, PROC_REF(update_status_on_signal))
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/adult/Remove(mob/removed_from)
+ UnregisterSignal(SSdcs, COMSIG_GLOB_MOB_LOGGED_IN, PROC_REF(update_status_on_signal))
+
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/adult/Activate(atom/target)
+ if (tgui_alert(owner, "Are you sure? After [cocoon_time / 10] seconds, you will create [num_hatchlings + 1] new hatchlings, including yourself.", "Reproduce", list("Yes", "No"), 30 SECONDS) != "Yes")
+ return
+ if (!IsAvailable(feedback = TRUE))
+ return
+
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/adult/handle_timer()
+ cocoon.balloon_alert(owner, "polling ghosts")
+
+ candidates = SSpolling.poll_ghost_candidates(
+ question = "Would you like to become a newly hatched blood worm? (x[num_hatchlings])",
+ role = ROLE_BLOOD_WORM_INFESTATION,
+ check_jobban = ROLE_BLOOD_WORM_INFESTATION,
+ poll_time = cocoon_time,
+ ignore_category = POLL_IGNORE_BLOOD_WORM,
+ alert_pic = cocoon_type, // The hatchling icon is too small, and a well-cropped juvenile icon is already used for the main spawn event.
+ jump_target = cocoon,
+ role_name_text = "blood worm",
+ amount_to_pick = num_hatchlings
+ )
+
+ var/num_candidates = length(candidates)
+
+ if (QDELETED(cocoon))
+ send_apology_to_candidates() // If this is reached, then [proc/cancel], which normally handles apologizing to the candidates, has already been called before the poll was finished.
+ return
+ if (num_candidates <= 0)
+ cancel()
+ owner.balloon_alert(owner, "no candidates!") // We can't host this balloon alert on a deleted cocoon.
+ return
+ if (num_candidates < num_hatchlings && tgui_alert(owner, "There are only [num_candidates]/[num_hatchlings] candidates for hatchlings, want to proceed anyway?", "Ghost Shortage", list("Yes", "No"), 10 SECONDS) != "Yes")
+ cancel()
+ return
+
+ finalize() // The poll is the timer.
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/adult/finalize()
+ for (var/mob/candidate as anything in candidates)
+ if (isnull(candidate) || isnull(candidate.key) || isnull(candidate.client))
+ continue
+
+ // The crew now has 3 new problems to deal with.
+ var/mob/living/basic/blood_worm/hatchling/new_hatchling = new(cocoon.drop_location())
+ var/datum/mind/fresh_mind = new(candidate.key)
+
+ fresh_mind.transfer_to(new_hatchling, force_key_move = TRUE)
+ fresh_mind.add_antag_datum(/datum/antagonist/blood_worm/infestation)
+
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/adult/transfer(mob/living/basic/blood_worm/old_worm, mob/living/basic/blood_worm/new_worm)
+ . = ..()
+
+ new_worm.reset_consumed_blood()
+
+ SEND_SIGNAL(new_worm, COMSIG_BLOOD_WORM_REPRODUCED)
+
+ log_blood_worm("[key_name(new_worm)] finished reproducing, resetting their growth back into a hatchling blood worm")
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/adult/cancel()
+ send_apology_to_candidates()
+
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/adult/shared_unregister_cocoon()
+ candidates = null
+
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/adult/proc/send_apology_to_candidates()
+ for (var/mob/candidate as anything in candidates)
+ if (isnull(candidate) || isnull(candidate.key) || isnull(candidate.client))
+ continue
+
+ // Sucks, but that's just how it is sometimes.
+ to_chat(candidate, span_warning("The blood worm cocoon you rolled a hatchling spot for was canceled. Sorry."))
+
+/obj/structure/blood_worm_cocoon/adult
+ name = "large blood cocoon"
+ desc = "The incubation cocoon of an adult blood worm. You can see many faint shadows within."
+
+ icon_state = "cocoon-large"
+
+ max_integrity = 200
+ damage_deflection = 20
+
+/obj/structure/blood_worm_cocoon/adult/examine(mob/user)
+ return ..() + span_warning("It can be broken to prevent the blood worm from reproducing, but it looks extremely tough.")
diff --git a/code/modules/antagonists/blood_worm/actions/inject_blood.dm b/code/modules/antagonists/blood_worm/actions/inject_blood.dm
new file mode 100644
index 00000000000..57f12a05ace
--- /dev/null
+++ b/code/modules/antagonists/blood_worm/actions/inject_blood.dm
@@ -0,0 +1,243 @@
+#define REQUIRED_ACCUMULATION(wound) (1 + (wound.severity - 1) * 0.3)
+
+/datum/action/cooldown/mob_cooldown/blood_worm/inject
+ name = "Inject Blood"
+ desc = "Inject your blood into the damaged tissues of your host, healing them in exchange for your own health."
+
+ button_icon_state = "inject_blood"
+
+ cooldown_time = 30 SECONDS
+ shared_cooldown = NONE
+
+ click_to_activate = FALSE
+
+ check_flags = NONE
+
+ var/health_cost = 0
+ var/minimum_health = 10
+
+ var/status_effect_type = null
+
+/datum/action/cooldown/mob_cooldown/blood_worm/inject/New(Target, original)
+ . = ..()
+ RegisterSignal(target, COMSIG_LIVING_HEALTH_UPDATE, PROC_REF(update_status_on_signal))
+
+/datum/action/cooldown/mob_cooldown/blood_worm/inject/Destroy()
+ UnregisterSignal(target, COMSIG_LIVING_HEALTH_UPDATE)
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/inject/IsAvailable(feedback)
+ if (!ishuman(owner) && !istype(owner, /mob/living/basic/blood_worm))
+ return FALSE
+
+ var/mob/living/basic/blood_worm/worm = target
+
+ if (worm.get_worm_health() - health_cost < minimum_health)
+ if (feedback)
+ owner.balloon_alert(owner, "out of blood!")
+ return FALSE
+
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/inject/Activate(atom/target)
+ var/mob/living/basic/blood_worm/worm = src.target
+ var/mob/living/carbon/human/host = worm.host
+
+ host.apply_status_effect(status_effect_type, worm)
+
+ host.visible_message(
+ message = span_danger("[host]'s wounds start healing unnaturally quickly!"),
+ ignored_mobs = owner
+ )
+
+ to_chat(owner, span_notice("You inject blood into the damaged tissues of your host."))
+
+ worm.adjust_worm_health(-health_cost)
+
+ return ..()
+
+/datum/status_effect/blood_worm_transfuse
+ id = "blood_worm_transfuse"
+ duration = 20 SECONDS
+ show_duration = TRUE
+ alert_type = /atom/movable/screen/alert/status_effect/blood_worm_transfuse
+ status_type = STATUS_EFFECT_REPLACE
+ processing_speed = STATUS_EFFECT_PRIORITY
+
+ var/damage_regen_rate = 0
+
+ var/wound_regen_rate = 0
+ var/wound_regen_accumulation = 0
+
+ var/organ_regen_rate = 0
+
+ var/trauma_regen_rate = 0
+ var/trauma_regen_accumulation = 0
+
+ var/atom/movable/screen/alert/status_effect/worm_alert = null
+ var/mob/living/basic/blood_worm/worm = null
+
+ var/last_tick_time = 0
+
+/datum/status_effect/blood_worm_transfuse/on_creation(mob/living/new_owner, mob/living/basic/blood_worm/new_worm)
+ . = ..()
+ if (!.)
+ return
+
+ attach_to_worm(new_worm)
+ last_tick_time = world.time
+
+/datum/status_effect/blood_worm_transfuse/Destroy()
+ detach_from_worm()
+ return ..()
+
+/datum/status_effect/blood_worm_transfuse/tick(seconds_between_ticks)
+ var/need_mob_update = FALSE
+
+ need_mob_update |= heal_damage(seconds_between_ticks)
+ need_mob_update |= heal_wounds(seconds_between_ticks)
+ need_mob_update |= heal_organs(seconds_between_ticks)
+ heal_traumas(seconds_between_ticks)
+
+ if (need_mob_update)
+ owner.updatehealth()
+
+ last_tick_time = world.time
+
+/datum/status_effect/blood_worm_transfuse/update_shown_duration()
+ . = ..()
+
+ if (worm_alert && linked_alert)
+ worm_alert.maptext = linked_alert.maptext
+
+/datum/status_effect/blood_worm_transfuse/proc/heal_damage(seconds_between_ticks)
+ var/healing_left = damage_regen_rate * seconds_between_ticks
+
+ if (owner.get_brute_loss() > 0 && healing_left > 0)
+ var/amount_healed = max(0, owner.adjust_brute_loss(-healing_left, forced = TRUE, updating_health = FALSE))
+ healing_left -= amount_healed
+ . |= amount_healed
+
+ if (owner.get_fire_loss() > 0 && healing_left > 0)
+ var/amount_healed = max(0, owner.adjust_fire_loss(-healing_left, forced = TRUE, updating_health = FALSE))
+ healing_left -= amount_healed
+ . |= amount_healed
+
+ if (owner.get_tox_loss() > 0 && healing_left > 0)
+ var/amount_healed = max(0, owner.adjust_tox_loss(-healing_left, forced = TRUE, updating_health = FALSE))
+ healing_left -= amount_healed
+ . |= amount_healed
+
+// I tried to set this up reasonably with SPT_PROB(), but it was too inconsistent, especially for wounds with high severity.
+// So I switched to an accumulation system instead. This way, the blood worm gets a consistent return on their health investment.
+/datum/status_effect/blood_worm_transfuse/proc/heal_wounds(seconds_between_ticks)
+ var/mob/living/carbon/human/host = owner
+
+ if (length(host.all_wounds))
+ wound_regen_accumulation += wound_regen_rate * seconds_between_ticks
+ else
+ wound_regen_accumulation = 0
+ return
+
+ while (wound_regen_accumulation >= 1 && length(host.all_wounds))
+ var/list/candidates = list()
+
+ for (var/datum/wound/wound in host.all_wounds)
+ if (wound_regen_accumulation >= REQUIRED_ACCUMULATION(wound))
+ candidates += wound
+
+ if (!length(candidates))
+ return
+
+ var/datum/wound/wound_to_heal = pick(candidates)
+ wound_regen_accumulation = max(0, wound_regen_accumulation - REQUIRED_ACCUMULATION(wound_to_heal))
+ wound_to_heal.remove_wound()
+ . = TRUE
+
+/datum/status_effect/blood_worm_transfuse/proc/heal_organs(seconds_between_ticks)
+ var/mob/living/carbon/human/host = owner
+
+ for (var/obj/item/organ/organ in host.organs)
+ // I thought about making this require ORGAN_ORGANIC, but changelings can heal robotic organs, so why not blood worms?
+ // In addition, none of the other blood worm code gives a shit about targets being organic, so this is more consistent.
+ . |= organ.apply_organ_damage(-organ_regen_rate * seconds_between_ticks)
+
+/datum/status_effect/blood_worm_transfuse/proc/heal_traumas(seconds_between_ticks)
+ var/mob/living/carbon/human/host = owner
+
+ if (host.has_trauma_type(resilience = TRAUMA_RESILIENCE_SURGERY))
+ trauma_regen_accumulation += trauma_regen_rate * seconds_between_ticks
+ else
+ trauma_regen_accumulation = 0
+ return
+
+ while (trauma_regen_accumulation >= 1 && host.has_trauma_type(resilience = TRAUMA_RESILIENCE_SURGERY))
+ host.cure_trauma_type(resilience = TRAUMA_RESILIENCE_SURGERY)
+ trauma_regen_accumulation = max(0, trauma_regen_accumulation - 1)
+
+/datum/status_effect/blood_worm_transfuse/proc/attach_to_worm(mob/living/basic/blood_worm/new_worm)
+ worm = new_worm
+
+ if (!worm || !alert_type)
+ return
+
+ RegisterSignal(worm, COMSIG_QDELETING, PROC_REF(detach_from_worm))
+
+ worm_alert = worm.throw_alert(id, alert_type)
+ worm_alert.attached_effect = src
+ update_shown_duration()
+
+/datum/status_effect/blood_worm_transfuse/proc/detach_from_worm(datum/source, force)
+ SIGNAL_HANDLER
+
+ UnregisterSignal(worm, COMSIG_QDELETING)
+
+ worm_alert = null
+ worm.clear_alert(id)
+
+ worm = null
+
+/atom/movable/screen/alert/status_effect/blood_worm_transfuse
+ name = "Blood Injection"
+ desc = "The injected blood is rapidly healing your host."
+ icon = 'icons/mob/actions/actions_blood_worm.dmi'
+ icon_state = "inject_blood"
+
+/atom/movable/screen/alert/status_effect/blood_worm_transfuse/Initialize(mapload, datum/hud/hud_owner)
+ . = ..()
+ underlays += mutable_appearance('icons/mob/actions/backgrounds.dmi', "bg_demon", layer = layer, offset_spokesman = src, plane = plane)
+
+/datum/action/cooldown/mob_cooldown/blood_worm/inject/hatchling
+ health_cost = 20
+ cooldown_time = 30 SECONDS // Effective host healing rate is 4 hp/s, Effective worm consumption rate is 0.667 hp/s, Worm max health is 2.5x less than host max health, Worm heals at 0.3 hp/s
+ status_effect_type = /datum/status_effect/blood_worm_transfuse/hatchling
+
+/datum/status_effect/blood_worm_transfuse/hatchling
+ damage_regen_rate = 6 // 20 s * 6 hp/s = 120 hp, note that major host healing is expected as the worm itself is very vulnerable to bleeding.
+ wound_regen_rate = 1 / 6 // One wound every 6 seconds, +30% per wound severity level.
+ organ_regen_rate = 2.5 // 20 s * 2.5 hp/s = 50 hp
+ trauma_regen_rate = 1 / 6 // One trauma every 6 seconds.
+
+/datum/action/cooldown/mob_cooldown/blood_worm/inject/juvenile
+ health_cost = 35
+ cooldown_time = 40 SECONDS // Effective host healing rate is 4 hp/s, Effective worm consumption rate is 0.875 hp/s, Worm max health is 1.66x less than host max health, Worm heals at 0.4 hp/s
+ status_effect_type = /datum/status_effect/blood_worm_transfuse/juvenile
+
+/datum/status_effect/blood_worm_transfuse/juvenile
+ damage_regen_rate = 8 // 20 s * 8 hp/s = 160 hp, note that major host healing is expected as the worm itself is very vulnerable to bleeding.
+ wound_regen_rate = 1 / 5 // One wound every 5 seconds, +30% per wound severity level.
+ organ_regen_rate = 4 // 20 s * 4 hp/s = 80 hp
+ trauma_regen_rate = 1 / 5 // One trauma every 5 seconds.
+
+/datum/action/cooldown/mob_cooldown/blood_worm/inject/adult
+ health_cost = 50
+ cooldown_time = 50 SECONDS // Effective host healing rate is 4 hp/s, Effective worm consumption rate is 1 hp/s, Worm max health is 1.11x less than host max health, Worm heals at 0.5 hp/s
+ status_effect_type = /datum/status_effect/blood_worm_transfuse/adult
+
+/datum/status_effect/blood_worm_transfuse/adult
+ damage_regen_rate = 10 // 20 s * 10 hp/s = 200 hp, note that major host healing is expected as the worm itself is very vulnerable to bleeding.
+ wound_regen_rate = 1 / 4 // One wound every 4 seconds, +30% per wound severity level.
+ organ_regen_rate = 5 // 20 s * 5 hp/s = 100 hp, which is also the standard organ health threshold.
+ trauma_regen_rate = 1 / 4 // One trauma every 4 seconds.
+
+#undef REQUIRED_ACCUMULATION
diff --git a/code/modules/antagonists/blood_worm/actions/invade_corpse.dm b/code/modules/antagonists/blood_worm/actions/invade_corpse.dm
new file mode 100644
index 00000000000..26ad54e931e
--- /dev/null
+++ b/code/modules/antagonists/blood_worm/actions/invade_corpse.dm
@@ -0,0 +1,66 @@
+/datum/action/cooldown/mob_cooldown/blood_worm/invade
+ name = "Invade Corpse"
+ desc = "Invade a humanoid corpse, taking it as your host."
+
+ button_icon_state = "invade_corpse"
+
+ cooldown_time = 0 SECONDS
+ shared_cooldown = NONE
+
+ unset_after_click = FALSE // Unsetting is handled explicitly.
+
+/datum/action/cooldown/mob_cooldown/blood_worm/invade/IsAvailable(feedback)
+ if (!istype(owner, /mob/living/basic/blood_worm))
+ return FALSE
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/invade/Activate(atom/target)
+ if (!ishuman(target))
+ return FALSE
+
+ var/mob/living/basic/blood_worm/worm = owner
+ var/mob/living/carbon/human/victim = target
+
+ if (!worm.Adjacent(victim))
+ victim.balloon_alert(worm, "too far away!")
+ return FALSE
+ if (!victim.IsReachableBy(worm))
+ victim.balloon_alert(worm, "can't reach!")
+ return FALSE
+
+ unset_click_ability(worm, refund_cooldown = FALSE) // If you fail after this point, it's because your attempt got interrupted or because the victim is invalid.
+
+ if (!invade_check(worm, victim, feedback = TRUE))
+ return TRUE // Don't bite the victim.
+
+ worm.visible_message(
+ message = span_danger("\The [worm] starts entering \the [victim]!"),
+ self_message = span_notice("You start entering \the [victim]."),
+ blind_message = span_hear("You hear squeezing.")
+ )
+
+ if (!do_after(worm, 5 SECONDS, victim, extra_checks = CALLBACK(src, PROC_REF(invade_check), worm, victim)))
+ return TRUE // Don't bite the victim.
+
+ worm.enter_host(victim)
+
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/invade/proc/invade_check(mob/living/basic/blood_worm/worm, mob/living/carbon/human/victim, feedback = FALSE)
+ if (HAS_TRAIT(victim, TRAIT_BLOOD_WORM_HOST))
+ if (feedback)
+ victim.balloon_alert(worm, "already a host!")
+ return FALSE
+ if (victim.stat != DEAD)
+ if (feedback)
+ victim.balloon_alert(worm, "still alive!")
+ return FALSE
+ if (!CAN_HAVE_BLOOD(victim))
+ if (feedback)
+ victim.balloon_alert(worm, "no blood!")
+ return FALSE
+ if (victim.get_blood_volume() + worm.health * BLOOD_WORM_HEALTH_TO_BLOOD <= worm.get_eject_volume_threshold())
+ if (feedback)
+ victim.balloon_alert(worm, "not enough blood for control!")
+ return FALSE
+ return TRUE
diff --git a/code/modules/antagonists/blood_worm/actions/leave_host.dm b/code/modules/antagonists/blood_worm/actions/leave_host.dm
new file mode 100644
index 00000000000..ac9c7edb2d4
--- /dev/null
+++ b/code/modules/antagonists/blood_worm/actions/leave_host.dm
@@ -0,0 +1,51 @@
+/datum/action/cooldown/mob_cooldown/blood_worm/eject
+ name = "Leave Host"
+ desc = "Leave your host, taking their blood with you."
+
+ button_icon_state = "leave_host"
+
+ cooldown_time = 0 SECONDS
+ shared_cooldown = NONE
+
+ click_to_activate = FALSE
+
+ check_flags = NONE
+
+/datum/action/cooldown/mob_cooldown/blood_worm/eject/IsAvailable(feedback)
+ if (!ishuman(owner) && !istype(owner, /mob/living/basic/blood_worm))
+ return FALSE
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/eject/Activate(atom/target)
+ var/mob/living/basic/blood_worm/worm = src.target
+ var/mob/living/carbon/human/host = worm.host
+
+ host.visible_message(
+ message = span_danger("\The [host] collapse[host.p_s()] and start[host.p_s()] shaking violently!"),
+ ignored_mobs = owner
+ )
+
+ to_chat(owner, span_danger("You begin leaving your host..."))
+
+ host.add_traits(list(TRAIT_FLOORED, TRAIT_INCAPACITATED, TRAIT_IMMOBILIZED), REF(src))
+
+ do_jitter_effect(worm)
+
+ for (var/i in 1 to 2)
+ addtimer(CALLBACK(src, PROC_REF(do_jitter_effect), worm), i SECONDS, TIMER_DELETE_ME)
+
+ if (!do_after(owner, 3 SECONDS, worm.host, timed_action_flags = IGNORE_TARGET_LOC_CHANGE | IGNORE_USER_LOC_CHANGE | IGNORE_INCAPACITATED, extra_checks = CALLBACK(src, PROC_REF(make_sure_shit_doesnt_go_wack), worm)))
+ REMOVE_TRAITS_IN(host, REF(src))
+ return
+
+ REMOVE_TRAITS_IN(host, REF(src))
+
+ worm.leave_host()
+
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/eject/proc/make_sure_shit_doesnt_go_wack(mob/living/basic/blood_worm/worm)
+ return worm.host // Basically, if the worm is somehow removed midway through, the do_after will continue because it ignores the loc change of the worm. This check prevents that.
+
+/datum/action/cooldown/mob_cooldown/blood_worm/eject/proc/do_jitter_effect(mob/living/basic/blood_worm/worm)
+ worm.host?.do_jitter_animation(100)
diff --git a/code/modules/antagonists/blood_worm/actions/leech_blood.dm b/code/modules/antagonists/blood_worm/actions/leech_blood.dm
new file mode 100644
index 00000000000..e7b0360cbcd
--- /dev/null
+++ b/code/modules/antagonists/blood_worm/actions/leech_blood.dm
@@ -0,0 +1,272 @@
+/datum/action/cooldown/mob_cooldown/blood_worm/leech
+ name = "Leech Blood"
+ desc = "Aggressively grab a target with your teeth and leech off of their blood. Also works on reagent containers like blood packs. Leeching will be canceled if you do any other actions."
+
+ button_icon_state = "leech_blood"
+
+ cooldown_time = 5 SECONDS
+ shared_cooldown = NONE
+
+ unset_after_click = FALSE // Unsetting is handled explicitly.
+ click_cd_override = 0 // Click cooldown is also handled explicitly.
+
+ var/leech_rate = 0
+ var/oxyloss_rate = 0
+
+ var/leech_grab_delay = 1 SECONDS
+
+ /// Associative list of all reagent types that are compatible for leeching from reagent containers. Format is "list[reagent_type] = TRUE"
+ var/static/list/compatible_container_reagent_types = list(
+ /datum/reagent/blood = TRUE, // If it's blood, then it works :D
+ /datum/reagent/consumable/liquidelectricity = TRUE, // Rare enough to allow.
+ )
+
+/datum/action/cooldown/mob_cooldown/blood_worm/leech/IsAvailable(feedback)
+ if (!istype(owner, /mob/living/basic/blood_worm))
+ return FALSE
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/leech/InterceptClickOn(mob/living/clicker, params, atom/target)
+ var/modifiers = params2list(params)
+
+ // Don't block examines, grabs, etc.
+ if (modifiers[SHIFT_CLICK] || modifiers[ALT_CLICK] || modifiers[CTRL_CLICK])
+ return FALSE
+
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/leech/Activate(atom/target)
+ owner.face_atom(target)
+
+ if (!ismovable(target))
+ return FALSE
+ if (!owner.Adjacent(target))
+ target.balloon_alert(owner, "too far away!")
+ return FALSE
+ if (!target.IsReachableBy(owner))
+ target.balloon_alert(owner, "can't reach!")
+ return FALSE
+
+ // If you fail after this point, it's because your attempt got interrupted or because the victim is invalid.
+ unset_click_ability(owner, refund_cooldown = FALSE)
+
+ if (isliving(target))
+ leech_living(owner, target)
+ else if (is_reagent_container(target))
+ leech_container(owner, target)
+ else
+ target.balloon_alert(owner, "can't leech from this!")
+
+ return TRUE // Prevents biting.
+
+/datum/action/cooldown/mob_cooldown/blood_worm/leech/proc/leech_living(mob/living/basic/blood_worm/leech, mob/living/target)
+ if (!leech_living_start_check(leech, target))
+ return
+
+ leech.visible_message(
+ message = span_danger("\The [leech] start[leech.p_s()] trying to bite into \the [target]!"),
+ self_message = span_danger("You start trying to bite into \the [target]!"),
+ ignored_mobs = list(target)
+ )
+
+ target.show_message(
+ msg = span_userdanger("\The [leech] start[leech.p_s()] trying to bite into you!"),
+ type = MSG_VISUAL
+ )
+
+ leech.changeNext_move(CLICK_CD_CLICK_ABILITY)
+
+ if (!do_after(leech, leech_grab_delay, target, extra_checks = CALLBACK(src, PROC_REF(leech_living_start_check), leech, target)))
+ return
+
+ if (leech.pulling != target && !leech.grab(target))
+ target.balloon_alert(leech, "unable to grab!")
+ return
+ if (leech.grab_state < GRAB_AGGRESSIVE)
+ leech.setGrabState(GRAB_AGGRESSIVE)
+
+ // Flooring the target makes them treat our grab state as one higher, halving their escape chance.
+ // Using a neck grab would reduce their escape chance to a third, which is too low.
+ ADD_TRAIT(target, TRAIT_FLOORED, REF(src))
+
+ // Prevents NPCs from resisting out of this.
+ if (!target.client)
+ incapacitate_leech_living_target(target)
+
+ // Remove incapacitation if a client logs into the target, and readd it if they log back out.
+ RegisterSignal(target, COMSIG_MOB_CLIENT_LOGIN, PROC_REF(deincapacitate_leech_living_target))
+ RegisterSignal(target, COMSIG_MOB_LOGOUT, PROC_REF(incapacitate_leech_living_target))
+
+ leech.visible_message(
+ message = span_danger("\The [leech] bite[leech.p_s()] into \the [target]!"),
+ self_message = span_danger("You bite into \the [target]!"),
+ blind_message = span_hear("You hear a bite, followed by a sickening crunch!"),
+ ignored_mobs = list(target)
+ )
+
+ target.show_message(
+ msg = span_userdanger("\The [leech] bite[leech.p_s()] into you!"),
+ type = MSG_VISUAL,
+ alt_msg = span_userdanger("You feel something bite into you!"),
+ alt_type = MSG_AUDIBLE
+ )
+
+ playsound(target, 'sound/items/weapons/bite.ogg', vol = 80, vary = TRUE, ignore_walls = FALSE)
+ playsound(target, 'sound/effects/wounds/pierce3.ogg', vol = 100, vary = TRUE, ignore_walls = FALSE)
+
+ var/synth_content = target.get_blood_synth_content()
+ if (synth_content >= 1)
+ target.balloon_alert(leech, "fully synthetic")
+ else if (synth_content > 0)
+ target.balloon_alert(leech, "[CEILING(synth_content * 100, 1)]% synthetic")
+
+ // Because of DO_AFTER_CHECK_NEXT_MOVE
+ leech.next_move = 0
+
+ while (do_after(leech, 1 SECONDS, target, timed_action_flags = IGNORE_USER_LOC_CHANGE | IGNORE_TARGET_LOC_CHANGE | DO_AFTER_CHECK_NEXT_MOVE, extra_checks = CALLBACK(src, PROC_REF(leech_living_active_check), leech, target)))
+ leech.consume_blood(-target.adjust_blood_volume(-leech_rate), target.get_blood_synth_content())
+
+ if (target.stat != DEAD)
+ target.adjust_oxy_loss(oxyloss_rate) // It's really weird if they just stand there until they literally drop dead from going below BLOOD_VOLUME_SURVIVE.
+
+ playsound(target, 'sound/effects/wounds/splatter.ogg', vol = 80, vary = TRUE, ignore_walls = FALSE)
+
+ if (leech.pulling == target && leech.grab_state >= GRAB_AGGRESSIVE)
+ leech.setGrabState(GRAB_PASSIVE)
+
+ UnregisterSignal(target, list(COMSIG_MOB_CLIENT_LOGIN, COMSIG_MOB_LOGOUT))
+
+ REMOVE_TRAITS_IN(target, REF(src))
+
+ StartCooldown()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/leech/proc/leech_living_start_check(mob/living/basic/blood_worm/leech, mob/living/target)
+ if (target.get_blood_volume() <= 0)
+ target.balloon_alert(leech, "no blood!")
+ return FALSE
+ if (HAS_TRAIT(target, TRAIT_BLOOD_WORM_HOST))
+ target.balloon_alert(leech, "occupied by our kin!")
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/mob_cooldown/blood_worm/leech/proc/leech_living_active_check(mob/living/basic/blood_worm/leech, mob/living/target)
+ if (target.get_blood_volume() <= 0)
+ target.balloon_alert(leech, "no more blood!")
+ return FALSE
+ if (HAS_TRAIT(target, TRAIT_BLOOD_WORM_HOST))
+ target.balloon_alert(leech, "occupied by our kin!")
+ return FALSE
+ if (!leech.Adjacent(target) || leech.pulling != target || leech.grab_state < GRAB_AGGRESSIVE)
+ target.balloon_alert(leech, "grab lost!")
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/mob_cooldown/blood_worm/leech/proc/incapacitate_leech_living_target(mob/living/target)
+ SIGNAL_HANDLER
+ ADD_TRAIT(target, TRAIT_INCAPACITATED, REF(src))
+
+/datum/action/cooldown/mob_cooldown/blood_worm/leech/proc/deincapacitate_leech_living_target(mob/living/target)
+ SIGNAL_HANDLER
+ REMOVE_TRAIT(target, TRAIT_INCAPACITATED, REF(src))
+
+/datum/action/cooldown/mob_cooldown/blood_worm/leech/proc/leech_container(mob/living/basic/blood_worm/leech, obj/item/reagent_containers/target)
+ if (!leech_container_start_check(leech, target, feedback = TRUE))
+ return
+
+ leech.visible_message(
+ message = span_danger("\The [leech] start[leech.p_s()] trying to bite into \the [target]!"),
+ self_message = span_danger("You start trying to bite into \the [target]!")
+ )
+
+ leech.changeNext_move(CLICK_CD_CLICK_ABILITY)
+
+ if (!do_after(leech, leech_grab_delay, target, extra_checks = CALLBACK(src, PROC_REF(leech_container_start_check), leech, target)))
+ return
+
+ leech.visible_message(
+ message = span_danger("\The [leech] bite[leech.p_s()] into \the [target]!"),
+ self_message = span_danger("You bite into \the [target]!"),
+ blind_message = span_hear("You hear a bite!"),
+ ignored_mobs = list(target)
+ )
+
+ playsound(target, 'sound/items/weapons/bite.ogg', vol = 80, vary = TRUE, ignore_walls = FALSE)
+
+ leech_container_alert_synth_info(leech, target)
+
+ // Because of DO_AFTER_CHECK_NEXT_MOVE
+ leech.next_move = 0
+
+ while (do_after(leech, 1 SECONDS, target, timed_action_flags = DO_AFTER_CHECK_NEXT_MOVE, extra_checks = CALLBACK(src, PROC_REF(leech_container_active_check), leech, target)))
+ var/list/blood = get_blood_in_container(target)
+
+ var/total_volume = 0
+ for (var/datum/reagent/reagent as anything in blood)
+ total_volume += reagent.volume
+
+ for (var/datum/reagent/reagent as anything in blood)
+ var/synth_content = reagent.data?[BLOOD_DATA_SYNTH_CONTENT]
+ var/amount_consumed = target.reagents.remove_reagent(reagent.type, leech_rate * (reagent.volume / total_volume))
+
+ leech.consume_blood(amount_consumed, synth_content)
+
+ playsound(target, 'sound/effects/wounds/splatter.ogg', vol = 80, vary = TRUE, ignore_walls = FALSE)
+
+ StartCooldown()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/leech/proc/leech_container_alert_synth_info(mob/living/basic/blood_worm/leech, obj/item/reagent_containers/target)
+ var/list/blood = get_blood_in_container(target)
+
+ var/total_volume = 0
+ var/synth_content = 0
+
+ for (var/datum/reagent/reagent as anything in blood)
+ total_volume += reagent.volume
+ synth_content += reagent.data?[BLOOD_DATA_SYNTH_CONTENT] * reagent.volume
+
+ synth_content /= total_volume
+
+ if (synth_content >= 1)
+ target.balloon_alert(leech, "fully synthetic")
+ else if (synth_content > 0)
+ target.balloon_alert(leech, "[CEILING(synth_content * 100, 1)]% synthetic")
+
+/datum/action/cooldown/mob_cooldown/blood_worm/leech/proc/leech_container_start_check(mob/living/basic/blood_worm/leech, obj/item/reagent_containers/target, feedback = FALSE)
+ if (!length(get_blood_in_container(target)))
+ if (feedback)
+ target.balloon_alert(leech, "no blood!")
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/mob_cooldown/blood_worm/leech/proc/leech_container_active_check(mob/living/basic/blood_worm/leech, obj/item/reagent_containers/target)
+ if (!length(get_blood_in_container(target)))
+ target.balloon_alert(leech, "no more blood!")
+ return FALSE
+ return TRUE
+
+/// Returns all of the blood in the given container. Format is "list[reagent_type] = volume"
+/datum/action/cooldown/mob_cooldown/blood_worm/leech/proc/get_blood_in_container(obj/item/reagent_containers/target)
+ . = list()
+
+ if (target.reagents.total_volume <= 0)
+ return
+
+ for (var/datum/reagent/reagent as anything in target.reagents.reagent_list)
+ if (reagent.volume <= 0)
+ continue
+ if (!compatible_container_reagent_types[reagent.type])
+ continue
+
+ . += reagent
+
+/datum/action/cooldown/mob_cooldown/blood_worm/leech/hatchling
+ leech_rate = BLOOD_VOLUME_NORMAL * 0.05 // 28 units of blood, 5 points of health, or 6.25% of a hatchling blood worm's health
+ oxyloss_rate = 11 // crosses from 44 to 55 at 5 seconds (50 is unconscious)
+
+/datum/action/cooldown/mob_cooldown/blood_worm/leech/juvenile
+ leech_rate = BLOOD_VOLUME_NORMAL * 0.075 // 42 units of blood, 7.5 points of health, or 6.25% of a juvenile blood worm's health
+ oxyloss_rate = 15 // crosses from 45 to 60 at 4 seconds (50 is unconscious)
+
+/datum/action/cooldown/mob_cooldown/blood_worm/leech/adult
+ leech_rate = BLOOD_VOLUME_NORMAL * 0.1 // 56 units of blood, 10 points of health, or 5.55% of an adult blood worm's health
+ oxyloss_rate = 20 // crosses from 40 to 60 at 3 seconds (50 is unconscious)
diff --git a/code/modules/antagonists/blood_worm/actions/revive_host.dm b/code/modules/antagonists/blood_worm/actions/revive_host.dm
new file mode 100644
index 00000000000..a9aa47555f6
--- /dev/null
+++ b/code/modules/antagonists/blood_worm/actions/revive_host.dm
@@ -0,0 +1,105 @@
+/datum/action/cooldown/mob_cooldown/blood_worm/revive
+ name = "Revive Host"
+ desc = "Restart the blood circulation of your host, bringing them back to life."
+
+ button_icon_state = "revive_host"
+
+ cooldown_time = 30 SECONDS
+ shared_cooldown = NONE
+
+ click_to_activate = FALSE
+
+ check_flags = NONE
+
+/datum/action/cooldown/mob_cooldown/blood_worm/revive/Grant(mob/granted_to)
+ . = ..()
+
+ var/mob/living/basic/blood_worm/worm = target
+ var/mob/living/carbon/human/host = worm.host
+
+ RegisterSignals(host, list(COMSIG_MOB_STATCHANGE, COMSIG_LIVING_HEALTH_UPDATE), PROC_REF(update_status_on_signal))
+
+/datum/action/cooldown/mob_cooldown/blood_worm/revive/Remove(mob/removed_from)
+ var/mob/living/basic/blood_worm/worm = target
+ var/mob/living/carbon/human/host = worm.host
+
+ UnregisterSignal(host, list(COMSIG_MOB_STATCHANGE, COMSIG_LIVING_HEALTH_UPDATE))
+
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/revive/IsAvailable(feedback)
+ if (!ishuman(owner) && !istype(owner, /mob/living/basic/blood_worm))
+ return FALSE
+
+ var/mob/living/basic/blood_worm/worm = target
+ var/mob/living/carbon/human/host = worm.host
+
+ if (!run_checks(worm, host, feedback))
+ return FALSE
+
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/revive/Activate(atom/target)
+ var/mob/living/basic/blood_worm/worm = target
+ var/mob/living/carbon/human/host = worm.host
+
+ to_chat(owner, span_notice("You begin restarting \the [host]'s blood circulation..."))
+
+ for (var/i in 1 to 3)
+ if (!do_after(owner, 2 SECONDS, host, timed_action_flags = IGNORE_INCAPACITATED | IGNORE_USER_LOC_CHANGE | IGNORE_TARGET_LOC_CHANGE, extra_checks = CALLBACK(src, PROC_REF(run_checks), worm, host)))
+ return FALSE
+
+ REMOVE_TRAIT(worm, TRAIT_DEAF, BLOOD_WORM_HOST_TRAIT)
+ playsound(host, 'sound/effects/singlebeat.ogg', vol = 50, vary = TRUE, ignore_walls = FALSE)
+ ADD_TRAIT(worm, TRAIT_DEAF, BLOOD_WORM_HOST_TRAIT)
+
+ var/original_transform = host.transform
+ animate(host, transform = host.transform.Translate(0, 3), time = 0.2 SECONDS, easing = CUBIC_EASING | EASE_OUT, flags = ANIMATION_PARALLEL)
+ animate(transform = original_transform, time = 0.2 SECONDS, easing = CUBIC_EASING | EASE_IN, flags = ANIMATION_PARALLEL)
+
+ host.visible_message(
+ message = span_danger("\The [host] shake[host.p_s()] violently!"),
+ ignored_mobs = owner
+ )
+
+ if (!host.revive())
+ host.balloon_alert(owner, "revival failed!")
+ return FALSE
+
+ host.visible_message(
+ message = span_danger("\The [host] rise[host.p_s()] from the dead!"),
+ ignored_mobs = owner
+ )
+
+ to_chat(owner, span_green("You successfully revive \the [host]!"))
+
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/revive/proc/run_checks(mob/living/basic/blood_worm/worm, mob/living/carbon/human/host, feedback = FALSE)
+ if (!worm.host)
+ return FALSE
+ if (host.stat != DEAD)
+ if (feedback)
+ host.balloon_alert(owner, "not dead!")
+ return FALSE
+ if (HAS_TRAIT(host, TRAIT_HUSK))
+ if (feedback)
+ host.balloon_alert(owner, "husked!")
+ return FALSE
+ if (!host.get_organ_slot(ORGAN_SLOT_BRAIN))
+ if (feedback)
+ host.balloon_alert(owner, "no brain!")
+ return FALSE
+ if (host.get_organ_loss(ORGAN_SLOT_BRAIN) >= BRAIN_DAMAGE_DEATH && !HAS_TRAIT(host, TRAIT_BRAIN_DAMAGE_NODEATH))
+ if (feedback)
+ host.balloon_alert(owner, "brain too damaged!")
+ return FALSE
+ if (host.health <= HEALTH_THRESHOLD_DEAD)
+ if (feedback)
+ host.balloon_alert(owner, "body too damaged!")
+ return FALSE
+ if (!host.can_be_revived()) // Fallback, ideally caught by earlier, more descriptive checks.
+ if (feedback)
+ host.balloon_alert(owner, "unable to revive!")
+ return FALSE
+ return TRUE
diff --git a/code/modules/antagonists/blood_worm/actions/spit_blood.dm b/code/modules/antagonists/blood_worm/actions/spit_blood.dm
new file mode 100644
index 00000000000..fc765c00906
--- /dev/null
+++ b/code/modules/antagonists/blood_worm/actions/spit_blood.dm
@@ -0,0 +1,282 @@
+/datum/action/cooldown/mob_cooldown/blood_worm/spit
+ name = "Spit Blood"
+ desc = "Spit corrosive blood at your target in exchange for your own health. Right-click to melt restraints while in a host."
+
+ button_icon_state = "spit_blood"
+
+ cooldown_time = 0 SECONDS
+ shared_cooldown = NONE
+
+ unset_after_click = FALSE // Unsetting is handled explicitly.
+
+ var/health_cost = 0
+ var/minimum_health = 10
+
+ var/projectile_type = null
+ var/burst_projectile_type = null
+ var/burst_count = 5
+
+/datum/action/cooldown/mob_cooldown/blood_worm/spit/New(Target, original)
+ . = ..()
+ RegisterSignal(target, COMSIG_LIVING_HEALTH_UPDATE, PROC_REF(update_status_on_signal))
+
+/datum/action/cooldown/mob_cooldown/blood_worm/spit/Destroy()
+ UnregisterSignal(target, COMSIG_LIVING_HEALTH_UPDATE)
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/spit/set_click_ability(mob/on_who)
+ . = ..()
+ var/right_click_message = ishuman(owner) ? ", right-click to melt restraints" : (burst_projectile_type ? ", right-click for a burst" : "")
+ to_chat(owner, span_notice("You fill your [ishuman(owner) ? "mouth" : "maw"] with blood. Left-click to spit corrosive blood[right_click_message]!"))
+
+/datum/action/cooldown/mob_cooldown/blood_worm/spit/unset_click_ability(mob/on_who, refund_cooldown)
+ . = ..()
+ to_chat(owner, span_notice("You empty your [ishuman(owner) ? "mouth" : "maw"] of blood."))
+
+/datum/action/cooldown/mob_cooldown/blood_worm/spit/IsAvailable(feedback)
+ if (!ishuman(owner) && !istype(owner, /mob/living/basic/blood_worm))
+ return FALSE
+
+ var/mob/living/basic/blood_worm/worm = target
+
+ if (worm.host?.is_mouth_covered())
+ if (feedback)
+ owner.balloon_alert(owner, "mouth is covered!")
+ return FALSE
+ if (worm.get_worm_health() - health_cost < minimum_health)
+ if (feedback)
+ owner.balloon_alert(owner, "out of blood!")
+ return FALSE
+
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/spit/InterceptClickOn(mob/living/clicker, params, atom/target)
+ . = ..()
+ if (!.)
+ unset_click_ability(owner, refund_cooldown = FALSE)
+ return FALSE
+
+ var/modifiers = params2list(params)
+
+ // Don't block examines, grabs, etc.
+ if (modifiers[SHIFT_CLICK] || modifiers[ALT_CLICK] || modifiers[CTRL_CLICK])
+ return FALSE
+
+ owner.face_atom(target)
+
+ var/mob/living/basic/blood_worm/worm = src.target
+
+ if (modifiers[RIGHT_CLICK] && worm.host)
+ melt_restraints()
+ else if (modifiers[RIGHT_CLICK] && burst_projectile_type)
+ fire_burst(clicker, modifiers, target)
+ else
+ fire_normal(clicker, modifiers, target)
+
+ if (!IsAvailable(feedback = FALSE))
+ unset_click_ability(owner, refund_cooldown = FALSE)
+
+ return TRUE // Intercepts the attack chain.
+
+/datum/action/cooldown/mob_cooldown/blood_worm/spit/Activate(atom/target)
+ return TRUE // Has to return true, as otherwise the parent proc of InterceptClickOn will return false, canceling the firing of the projectile.
+
+/datum/action/cooldown/mob_cooldown/blood_worm/spit/proc/fire_normal(mob/living/clicker, modifiers, atom/target)
+ if (target == owner)
+ return
+
+ owner.visible_message(
+ message = span_danger("\The [owner] spit[owner.p_s()] corrosive blood!"),
+ self_message = span_danger("You spit corrosive blood!"),
+ blind_message = span_hear("You hear spitting.")
+ )
+
+ spit(target, modifiers, projectile_type)
+
+ playsound(owner, SFX_ALIEN_SPIT_ACID, vol = 25, vary = TRUE)
+
+ owner.changeNext_move(CLICK_CD_RANGE)
+
+/datum/action/cooldown/mob_cooldown/blood_worm/spit/proc/fire_burst(mob/living/clicker, modifiers, atom/target)
+ if (target == owner)
+ return
+
+ var/mob/living/basic/blood_worm/worm = src.target
+ if (worm.get_worm_health() - health_cost * burst_count < minimum_health)
+ owner.balloon_alert(owner, "out of blood!")
+ return
+
+ owner.visible_message(
+ message = span_danger("\The [owner] spit[owner.p_s()] a burst of corrosive blood!"),
+ self_message = span_danger("You spit a burst of corrosive blood!"),
+ blind_message = span_hear("You hear spitting.")
+ )
+
+ spit(target, modifiers, burst_projectile_type, count = burst_count, spread = 10)
+
+ playsound(owner, SFX_ALIEN_SPIT_ACID, vol = 40, vary = TRUE)
+ playsound(owner, 'sound/mobs/non-humanoids/bileworm/bileworm_spit.ogg', vol = 40, vary = TRUE)
+
+ owner.changeNext_move(CLICK_CD_RANGE)
+ StartCooldown(10 SECONDS)
+
+/datum/action/cooldown/mob_cooldown/blood_worm/spit/proc/spit(target, modifiers, projectile_type, count = 1, spread = 0)
+ for (var/i in 1 to count)
+ var/obj/projectile/blood_worm_spit/spit = new projectile_type(owner.loc)
+
+ spit.firer = owner
+ spit.fired_from = owner
+ spit.aim_projectile(target, owner, modifiers, deviation = count == 1 ? 0 : lerp(-spread, spread, (i - 1) / (count - 1)))
+ spit.fire()
+
+ owner.newtonian_move(get_angle(target, owner), instant = TRUE, drift_force = count)
+
+ var/mob/living/basic/blood_worm/worm = src.target
+
+ worm.adjust_worm_health(-health_cost * count)
+
+/datum/action/cooldown/mob_cooldown/blood_worm/spit/proc/melt_restraints()
+ var/mob/living/carbon/human/host = owner
+
+ var/melted_something = FALSE
+ var/something_to_melt = FALSE
+
+ if (host.handcuffed)
+ something_to_melt = TRUE
+ melted_something |= melt_restraints_in_slot(host, ITEM_SLOT_HANDCUFFED)
+ if (host.legcuffed)
+ something_to_melt = TRUE
+ melted_something |= melt_restraints_in_slot(host, ITEM_SLOT_LEGCUFFED)
+ if (host.wear_suit?.breakouttime)
+ something_to_melt = TRUE
+ melted_something |= melt_restraints_in_slot(host, ITEM_SLOT_OCLOTHING)
+ if (host.shoes?.tied == SHOES_KNOTTED)
+ something_to_melt = TRUE
+ melted_something |= melt_restraints_in_slot(host, ITEM_SLOT_FEET)
+ if (istype(host.loc, /obj/structure/closet))
+ something_to_melt = TRUE
+ melted_something |= melt_closet(host, host.loc)
+ if (istype(host.loc, /obj/structure/spider/cocoon))
+ something_to_melt = TRUE
+ melted_something |= melt_cocoon(host, host.loc)
+
+ if (melted_something)
+ playsound(host, SFX_SIZZLE, vol = 80, vary = TRUE, ignore_walls = FALSE)
+ StartCooldown(20 SECONDS)
+ if (!something_to_melt)
+ host.balloon_alert(host, "not restrained!")
+
+/datum/action/cooldown/mob_cooldown/blood_worm/spit/proc/melt_restraints_in_slot(mob/living/carbon/human/host, slot)
+ var/obj/restraints = host.get_item_by_slot(slot)
+
+ if (!istype(restraints))
+ return FALSE
+ if (restraints.resistance_flags & (INDESTRUCTIBLE | UNACIDABLE | ACID_PROOF))
+ host.balloon_alert(host, "\the [restraints] [restraints.p_are()] too tough!")
+ return FALSE
+
+ host.visible_message(
+ message = span_danger("\The [host] spit[host.p_s()] corrosive blood all over \the [restraints]!"),
+ self_message = span_danger("You spit corrosive blood all over \the [restraints]!"),
+ blind_message = span_hear("You hear sizzling.")
+ )
+
+ log_combat(host, restraints, "melted", addition = "(Spit Blood)")
+
+ addtimer(CALLBACK(src, PROC_REF(finish_melting_restraints), restraints), 5 SECONDS)
+ return TRUE
+
+/datum/action/cooldown/mob_cooldown/blood_worm/spit/proc/finish_melting_restraints(obj/restraints)
+ restraints.visible_message(span_danger("\The [restraints] melt[restraints.p_s()] into a pile of goopy blood!"))
+ new /obj/effect/decal/cleanable/blood/old(get_turf(restraints))
+ qdel(restraints)
+
+/datum/action/cooldown/mob_cooldown/blood_worm/spit/proc/melt_closet(mob/living/carbon/human/host, obj/structure/closet/closet)
+ if (closet.resistance_flags & (INDESTRUCTIBLE | UNACIDABLE | ACID_PROOF))
+ host.balloon_alert(host, "\the [closet] [closet.p_are()] too tough!")
+ return FALSE
+
+ closet.visible_message(
+ message = span_danger("\The [closet]'s hinges overflow with corrosive blood and begin to melt!"),
+ blind_message = span_hear("You hear sizzling."),
+ ignored_mobs = host
+ )
+
+ to_chat(host, span_danger("You spit corrosive blood all over \the [closet]'s interior hinges!"))
+
+ log_combat(host, closet, "melted", addition = "(Spit Blood)")
+
+ addtimer(CALLBACK(src, PROC_REF(finish_melting_closet), closet), 5 SECONDS)
+ return TRUE
+
+/datum/action/cooldown/mob_cooldown/blood_worm/spit/proc/finish_melting_closet(obj/structure/closet/closet)
+ closet.visible_message(span_danger("\The [closet]'s hinges melt into a pile of goopy blood!"))
+ new /obj/effect/decal/cleanable/blood/old(get_turf(closet))
+
+ closet.welded = FALSE
+ closet.locked = FALSE
+ closet.broken = TRUE
+ closet.open()
+
+/datum/action/cooldown/mob_cooldown/blood_worm/spit/proc/melt_cocoon(mob/living/carbon/human/host, obj/structure/spider/cocoon/cocoon)
+ if (cocoon.resistance_flags & (INDESTRUCTIBLE | UNACIDABLE | ACID_PROOF))
+ host.balloon_alert(host, "\the [cocoon] [cocoon.p_are()] too tough!")
+ return FALSE
+
+ cocoon.visible_message(
+ message = span_danger("\The [cocoon]'s threads begin to fall apart!"),
+ blind_message = span_hear("You hear sizzling."),
+ ignored_mobs = host
+ )
+
+ to_chat(host, span_danger("You spit corrosive blood all over the inside of \the [cocoon]!"))
+
+ log_combat(host, cocoon, "melted", addition = "(Spit Blood)")
+
+ addtimer(CALLBACK(src, PROC_REF(finish_melting_cocoon), cocoon), 5 SECONDS)
+ return TRUE
+
+/datum/action/cooldown/mob_cooldown/blood_worm/spit/proc/finish_melting_cocoon(obj/structure/spider/cocoon/cocoon)
+ cocoon.visible_message(span_danger("\The [cocoon] melt[cocoon.p_s()] into a pile of goopy blood!"))
+ new /obj/effect/decal/cleanable/blood/old(get_turf(cocoon))
+ qdel(cocoon)
+
+/obj/projectile/blood_worm_spit
+ name = "corrosive blood spit"
+ icon_state = "blood_spit"
+
+ damage_type = BURN
+ armor_flag = BULLET // I'm sorry. Acid armor is too nonsensical for combat, as its granted based on how easily acid should destroy objects.
+
+ hitsound = 'sound/items/weapons/sear.ogg'
+ hitsound_wall = 'sound/items/weapons/sear.ogg'
+
+ impact_effect_type = /obj/effect/temp_visual/impact_effect/blood_worm_spit
+
+/obj/effect/temp_visual/impact_effect/blood_worm_spit
+ color = "#ff1313"
+
+/datum/action/cooldown/mob_cooldown/blood_worm/spit/juvenile
+ health_cost = 5.5 // This is enough for 20 shots in a row at full health. (keep in mind that health is VERY important)
+ projectile_type = /obj/projectile/blood_worm_spit/juvenile
+
+/obj/projectile/blood_worm_spit/juvenile
+ damage = 20 // 300 damage total, assuming no armor.
+ armour_penetration = 30 // So that sec cant just nullify half the kit of the blood worms with bulletproof armor.
+ wound_bonus = 0 // Juveniles can afford to fix wounds on their hosts. This doesn't cause critical wounds. (at least not in testing)
+
+/datum/action/cooldown/mob_cooldown/blood_worm/spit/adult
+ desc = "Spit corrosive blood at your target in exchange for your own health. Right-click to melt restraints while in a host, or fire a burst while out of a host."
+ health_cost = 6.5 // This is enough for 26 shots in a row at full health. (keep in mind that health is VERY important)
+ projectile_type = /obj/projectile/blood_worm_spit/adult
+ burst_projectile_type = /obj/projectile/blood_worm_spit/adult_burst
+
+/obj/projectile/blood_worm_spit/adult
+ damage = 25 // 500 damage total, assuming no armor.
+ armour_penetration = 50 // Yeah no your armor isn't saving you from this.
+ wound_bonus = 5 // Okay, now we're talking.
+
+/obj/projectile/blood_worm_spit/adult_burst
+ damage = 15 // 75 damage per burst if all hit.
+ armour_penetration = 50 // Same armor penetration.
+ wound_bonus = 2 // Slightly less wound power than normal.
diff --git a/code/modules/antagonists/blood_worm/blood_worm_antag.dm b/code/modules/antagonists/blood_worm/blood_worm_antag.dm
new file mode 100644
index 00000000000..fc8206ffdfd
--- /dev/null
+++ b/code/modules/antagonists/blood_worm/blood_worm_antag.dm
@@ -0,0 +1,101 @@
+/datum/antagonist/blood_worm
+ name = "\improper Blood Worm"
+ roundend_category = "blood worms"
+ antagpanel_category = ANTAG_GROUP_BLOODY
+ pref_flag = ROLE_BLOOD_WORM
+ antag_hud_name = "blood_worm"
+ antag_moodlet = /datum/mood_event/blood_worm
+ hijack_speed = 0.5
+ show_to_ghosts = TRUE // I mean let's be honest, even the roundstart versions will become painfully apparent soon enough.
+ stinger_sound = 'sound/effects/magic/exit_blood.ogg'
+ ui_name = "AntagInfoBloodWorm"
+
+ var/datum/team/blood_worm/team = null
+
+/datum/antagonist/blood_worm/Destroy()
+ team = null
+ return ..()
+
+/datum/antagonist/blood_worm/greet()
+ . = ..()
+
+ to_chat(owner, span_bold("A species of space-faring leech, massive in size and ferocious in hunting. \
+ Your origins are unknown to most, but to some, you are among their greatest creations. \
+ A failed Syndicate bioweapons project, snuffed out by benefactors after a \"lack of results\", and yet... \
+ here you find yourself. On a Nanotrasen space station. What a fitting habitat for you, isn't it?"))
+ to_chat(owner, span_bolddanger("KILL, CONSUME, MULTIPLY, CONQUER."))
+
+ owner.announce_objectives()
+
+/datum/antagonist/blood_worm/forge_objectives()
+ objectives |= team.objectives
+
+/datum/antagonist/blood_worm/create_team(datum/team/new_team)
+ GLOB.blood_worm_team ||= new()
+ team = GLOB.blood_worm_team
+
+/datum/antagonist/blood_worm/get_team()
+ return team
+
+/datum/antagonist/blood_worm/on_gain()
+ forge_objectives()
+ ADD_TRAIT(owner, TRAIT_UNCONVERTABLE, REF(src)) // No blood cultist worms or whatever the fuck
+ return ..()
+
+/datum/antagonist/blood_worm/on_removal()
+ REMOVE_TRAITS_IN(owner, REF(src))
+ return ..()
+
+/datum/antagonist/blood_worm/apply_innate_effects(mob/living/mob_override)
+ var/mob/living/target = mob_override || owner.current
+
+ add_team_hud(target)
+
+ if (!istype(target, /mob/living/basic/blood_worm))
+ target.faction |= FACTION_BLOOD_WORM
+
+ // Apathy and fearlessness are traits inherent to the very mind of a blood worm.
+ // Being immune to hunger, withdrawals, etc. are physical traits of blood worm hosts.
+ target.add_traits(list(TRAIT_APATHETIC, TRAIT_FEARLESS), REF(src))
+
+/datum/antagonist/blood_worm/remove_innate_effects(mob/living/mob_override)
+ var/mob/living/target = mob_override || owner.current
+
+ if (!istype(target, /mob/living/basic/blood_worm))
+ target.faction -= FACTION_BLOOD_WORM
+
+ REMOVE_TRAITS_IN(target, REF(src))
+
+/datum/antagonist/blood_worm/admin_add(datum/mind/new_owner, mob/admin)
+ if (!new_owner.current)
+ return
+
+ if (!istype(new_owner.current, /mob/living/basic/blood_worm))
+ var/old_mob = new_owner.current
+
+ var/mob/living/basic/blood_worm/hatchling/new_worm = new(get_turf(new_owner.current))
+ new_owner.transfer_to(new_worm, force_key_move = TRUE)
+
+ qdel(old_mob)
+
+ return ..()
+
+/datum/antagonist/blood_worm/get_preview_icon()
+ var/icon/icon = icon('icons/mob/nonhuman-player/blood_worm_32x32.dmi', "juvenile")
+
+ icon.Crop(1, 1, 32, 32)
+ icon.Shift(SOUTH, 4)
+ icon.Scale(ANTAGONIST_PREVIEW_ICON_SIZE, ANTAGONIST_PREVIEW_ICON_SIZE)
+
+ return icon
+
+/datum/antagonist/blood_worm/ui_static_data(mob/user)
+ . = ..()
+ var/list/team_data = list()
+ team_data["blood_consumed_total"] = floor(team.blood_consumed_total)
+ team_data["times_reproduced_total"] = floor(team.times_reproduced_total)
+ .["team"] = team_data
+
+/datum/antagonist/blood_worm/infestation
+ pref_flag = ROLE_BLOOD_WORM_INFESTATION
+ show_in_antagpanel = FALSE
diff --git a/code/modules/antagonists/blood_worm/blood_worm_blood_consumption.dm b/code/modules/antagonists/blood_worm/blood_worm_blood_consumption.dm
new file mode 100644
index 00000000000..0d36e2202e9
--- /dev/null
+++ b/code/modules/antagonists/blood_worm/blood_worm_blood_consumption.dm
@@ -0,0 +1,38 @@
+// Any procs that work with blood consumption go here.
+// There are quite a few of them, and some of them are lengthy.
+
+/mob/living/basic/blood_worm/proc/consume_blood(blood_amount, synth_content = 0, should_heal = TRUE)
+ if (blood_amount <= 0)
+ return
+
+ synth_content = clamp(synth_content, 0, 1)
+
+ var/was_synth_capped = consumed_synth_blood >= maximum_synth_blood
+ var/was_ready_to_mature = get_consumed_blood() >= cocoon_action?.total_blood_required
+
+ var/added_normal_blood = blood_amount * (1 - synth_content)
+ var/added_synth_blood = blood_amount * synth_content * synth_blood_efficiency
+ var/added_total_blood = added_normal_blood + added_synth_blood
+
+ consumed_synth_blood = min(consumed_synth_blood + added_synth_blood, maximum_synth_blood)
+ consumed_normal_blood += added_normal_blood
+
+ if (!was_synth_capped && consumed_synth_blood >= maximum_synth_blood)
+ balloon_alert_self("synthetic cap reached!")
+
+ if (!was_ready_to_mature && get_consumed_blood() >= cocoon_action?.total_blood_required)
+ balloon_alert_self("ready to mature!")
+
+ if (should_heal)
+ // Synthetic blood works just fine for healing.
+ adjust_worm_health(blood_amount * BLOOD_WORM_BLOOD_TO_HEALTH)
+
+ // This is intentionally passing the synth blood amount without any regard for the synth blood growth cap.
+ SEND_SIGNAL(src, COMSIG_BLOOD_WORM_CONSUMED_BLOOD, added_normal_blood, added_synth_blood, added_total_blood)
+
+/mob/living/basic/blood_worm/proc/reset_consumed_blood()
+ consumed_normal_blood = 0
+ consumed_synth_blood = 0
+
+/mob/living/basic/blood_worm/proc/get_consumed_blood()
+ return consumed_normal_blood + consumed_synth_blood
diff --git a/code/modules/antagonists/blood_worm/blood_worm_host_handling.dm b/code/modules/antagonists/blood_worm/blood_worm_host_handling.dm
new file mode 100644
index 00000000000..67543842cd0
--- /dev/null
+++ b/code/modules/antagonists/blood_worm/blood_worm_host_handling.dm
@@ -0,0 +1,260 @@
+// Any proc that handles host logic and doesn't fit in a more specific file goes here.
+// There are a ton of these and they are pretty tightly coupled to each other.
+
+/mob/living/basic/blood_worm/proc/enter_host(mob/living/carbon/human/new_host, silent = FALSE, gain_progress = TRUE)
+ if (!silent)
+ playsound(src, 'sound/effects/magic/enter_blood.ogg', vol = 60, vary = TRUE, ignore_walls = FALSE)
+
+ visible_message(
+ message = span_bolddanger("\The [src] enter[p_s()] \the [new_host]!"),
+ self_message = span_notice("You enter \the [new_host]."),
+ blind_message = span_hear("You hear a squelch.")
+ )
+
+ new /obj/effect/temp_visual/blood_worm_invade_host(get_turf(new_host), effect_name)
+
+ host = new_host
+
+ RegisterSignal(host, COMSIG_QDELETING, PROC_REF(on_host_qdel))
+ RegisterSignal(host, COMSIG_MOB_STATCHANGE, PROC_REF(on_host_stat_changed))
+ RegisterSignal(host, COMSIG_HUMAN_ON_HANDLE_BLOOD, PROC_REF(on_host_handle_blood))
+ RegisterSignal(host, COMSIG_LIVING_LIFE, PROC_REF(on_host_life))
+ RegisterSignal(host, COMSIG_LIVING_ADJUST_OXY_DAMAGE, PROC_REF(on_host_adjust_oxy_damage))
+ RegisterSignal(host, COMSIG_LIVING_PRE_UPDATE_BLOOD_STATUS, PROC_REF(on_host_pre_update_blood_status))
+ RegisterSignal(host, COMSIG_MOB_GET_STATUS_TAB_ITEMS, PROC_REF(on_host_get_status_tab_items))
+ RegisterSignal(host, COMSIG_MOB_EXAMINING, PROC_REF(on_host_examining))
+
+ START_PROCESSING(SSfastprocess, src)
+
+ add_traits(list(TRAIT_INCAPACITATED, TRAIT_IMMOBILIZED, TRAIT_MUTE, TRAIT_EMOTEMUTE), BLOOD_WORM_HOST_TRAIT)
+
+ host.add_traits(list(
+ TRAIT_NOBREATH, // Makes blood worms carry at least one of their atmos immunities to a host. Also allows them to take off masks to be able to spit, without risking suffocation.
+ TRAIT_STABLEHEART, // Allows blood worms to take fucked up hosts. Especially useful if you accidentally gut your future host yourself. (very possible as an adult)
+ TRAIT_STABLELIVER, // Ditto.
+ TRAIT_NOCRITDAMAGE, // Prevents blood worms from noob trapping themselves by reviving a host "too early", since that shouldn't be a thing.
+ TRAIT_NOHUNGER, // Every single long-deceased corpse is starved. Many people also just ignore hunger the whole shift. Starving with every new host gets old fast.
+ TRAIT_NO_WITHDRAWALS, // Prevents OOC quirk choices from impacting blood worms as much. Stops withdrawals instead of addictions since the latter can be metagamed.
+ TRAIT_NO_SPLIT_PERSONALITY, // How about no?
+ TRAIT_BLOOD_HUD, // Self-explanatory, allows blood worms to seek prey even while in a host.
+ TRAIT_BLOOD_WORM_HOST), // Used in code for recognizing blood worm hosts with a simple trait check.
+ BLOOD_WORM_HOST_TRAIT)
+
+ if (client)
+ ADD_TRAIT(host, TRAIT_MIND_TEMPORARILY_GONE, BLOOD_WORM_HOST_TRAIT)
+
+ host.physiology.bleed_mod *= BLOOD_WORM_BLEED_MOD
+
+ host.AddElement(/datum/element/hand_organ_insertion)
+
+ remove_actions(src, innate_actions)
+ grant_actions(src, host_actions)
+
+ var/cached_blood_volume = host.get_blood_volume()
+
+ if (gain_progress)
+ // Apply the host's blood volume to our growth.
+ consume_blood(cached_blood_volume, host.get_blood_synth_content(), should_heal = FALSE)
+
+ // Combine our health with the blood of our host.
+ host.set_blood_volume(cached_blood_volume + health * BLOOD_WORM_HEALTH_TO_BLOOD)
+
+ // Modify host blood such that it's BLOOD_VOLUME_NORMAL when we're at max health.
+ host.set_blood_volume_modifier(REF(src), BLOOD_VOLUME_NORMAL / (maxHealth * BLOOD_WORM_HEALTH_TO_BLOOD))
+
+ // Caps host blood volume to our custom maximum and syncs our bruteloss with host health.
+ sync_health()
+
+ if (host.hud_used)
+ create_host_hud(host)
+ else
+ RegisterSignal(host, COMSIG_MOB_HUD_CREATED, PROC_REF(create_host_hud))
+
+ forceMove(host)
+
+ log_blood_worm("[key_name(src)] entered their new host [key_name(host)]")
+
+ if (host.stat != DEAD)
+ possess_host()
+
+/mob/living/basic/blood_worm/proc/leave_host()
+ if (!host)
+ return
+
+ visible_message(
+ message = span_bolddanger("\The [src] emerge[p_s()] from \the [host]!"),
+ blind_message = span_hear("You hear a squelch."),
+ ignored_mobs = list(host, src)
+ )
+
+ to_chat_self(span_notice("You emerge from \the [host]."))
+
+ log_blood_worm("[key_name(src)] left their host [key_name(host)]")
+
+ forceMove(host.drop_location()) // This will call unregister_host() via Moved()
+
+ playsound(src, 'sound/effects/magic/exit_blood.ogg', vol = 60, vary = TRUE, ignore_walls = FALSE)
+
+ Immobilize(leave_host_duration, ignore_canstun = TRUE)
+ incapacitate(leave_host_duration, ignore_canstun = TRUE)
+
+ // Uses the icon file of the current mob. This means the animation is 32x48 for the adults.
+ flick("leave-[effect_name]", src)
+
+/mob/living/basic/blood_worm/proc/unregister_host()
+ if (!host)
+ return
+
+ possess_worm()
+
+ UnregisterSignal(host, list(
+ COMSIG_QDELETING,
+ COMSIG_MOB_STATCHANGE,
+ COMSIG_HUMAN_ON_HANDLE_BLOOD,
+ COMSIG_LIVING_LIFE,
+ COMSIG_LIVING_ADJUST_OXY_DAMAGE,
+ COMSIG_LIVING_PRE_UPDATE_BLOOD_STATUS,
+ COMSIG_MOB_GET_STATUS_TAB_ITEMS,
+ COMSIG_MOB_EXAMINING,
+ COMSIG_MOB_HUD_CREATED,
+ ))
+
+ STOP_PROCESSING(SSfastprocess, src)
+
+ REMOVE_TRAITS_IN(src, BLOOD_WORM_HOST_TRAIT)
+ REMOVE_TRAITS_IN(host, BLOOD_WORM_HOST_TRAIT)
+ host.physiology.bleed_mod /= BLOOD_WORM_BLEED_MOD
+ host.RemoveElement(/datum/element/hand_organ_insertion)
+
+ remove_actions(src, host_actions)
+ grant_actions(src, innate_actions)
+
+ host.remove_blood_volume_modifier(REF(src))
+ sync_health(already_ejecting = TRUE)
+
+ remove_host_hud()
+
+ host.set_blood_volume(0)
+
+ if (host.stat != DEAD)
+ host.death() // I don't care if you have TRAIT_NODEATH, can't die from bloodloss normally, or whatever else. I just need you to die.
+
+ log_blood_worm("[key_name(src)] unregistered their host [key_name(host)]")
+
+ host = null
+
+/mob/living/basic/blood_worm/proc/possess_host()
+ if (!host || is_possessing_host)
+ return
+
+ is_possessing_host = TRUE
+
+ if (host.mind)
+ backseat = new(host)
+ backseat.death(gibbed = TRUE) // Same thing that the corpse mob spawners do to stop deathgasps and such.
+
+ // If the host is a changeling, then we forcibly move their client to the backseat so they can use Expel Worm if they wish to.
+ host.mind.transfer_to(backseat, force_key_move = host.mind.has_antag_datum(/datum/antagonist/changeling))
+
+ mind?.transfer_to(host)
+
+ remove_actions(src, host_actions)
+ grant_actions(host, host_actions)
+
+ host.grant_language(/datum/language/wormspeak, UNDERSTOOD_LANGUAGE, LANGUAGE_BLOOD_WORM)
+
+ log_blood_worm("[key_name(src)] possessed their host [key_name(host)]")
+
+/mob/living/basic/blood_worm/proc/possess_worm()
+ if (!host || !is_possessing_host)
+ return
+
+ is_possessing_host = FALSE
+
+ host.mind?.transfer_to(src)
+
+ if (backseat)
+ backseat.mind?.transfer_to(host)
+ QDEL_NULL(backseat)
+
+ remove_actions(host, host_actions)
+ grant_actions(src, host_actions)
+
+ host.remove_language(/datum/language/wormspeak, UNDERSTOOD_LANGUAGE, LANGUAGE_BLOOD_WORM)
+
+ log_blood_worm("[key_name(src)] unpossessed their host [key_name(host)]")
+
+/mob/living/basic/blood_worm/proc/on_host_qdel(datum/source, force)
+ SIGNAL_HANDLER
+ qdel(src)
+
+/mob/living/basic/blood_worm/proc/on_host_stat_changed(datum/source, new_stat, old_stat)
+ SIGNAL_HANDLER
+ if (old_stat == DEAD && new_stat != DEAD)
+ possess_host()
+ else if (old_stat != DEAD && new_stat == DEAD)
+ possess_worm()
+
+/mob/living/basic/blood_worm/proc/on_host_handle_blood(datum/source, seconds_per_tick, times_fired)
+ SIGNAL_HANDLER
+ return HANDLE_BLOOD_NO_OXYLOSS | HANDLE_BLOOD_NO_NUTRITION_DRAIN
+
+/mob/living/basic/blood_worm/proc/on_host_life(datum/source, seconds_per_tick, times_fired)
+ // Moved to worm life when not in a host.
+ adjust_worm_health(regen_rate * seconds_per_tick)
+
+ // Required for now, because TRAIT_NOBREATH does not actually prevent oxygen damage.
+ // This is really weird because it also sets oxygen damage to 0 when added to a mob.
+ host.set_oxy_loss(0, forced = TRUE)
+
+ if (!HAS_TRAIT(host, TRAIT_STASIS))
+ handle_host_blood(seconds_per_tick, times_fired)
+ handle_host_temperature(seconds_per_tick, times_fired)
+
+/mob/living/basic/blood_worm/proc/handle_host_blood(seconds_per_tick, times_fired)
+ if (host.stat == DEAD)
+ host.handle_blood(seconds_per_tick, times_fired)
+
+ // Ignored while possessing a host, as [carbon/proc/bleed_warn] handles it instead.
+ if (!is_possessing_host && COOLDOWN_FINISHED(src, host_bleed_alert_cooldown) && host.get_bleed_rate() > 0)
+ to_chat_self(span_userdanger("Your host is bleeding!"))
+ COOLDOWN_START(src, host_bleed_alert_cooldown, 15 SECONDS)
+
+/mob/living/basic/blood_worm/proc/handle_host_temperature(seconds_per_tick, times_fired)
+ if (host.coretemperature <= maximum_survivable_temperature)
+ return
+
+ var/burn_coeff = damage_coeff[BURN]
+ adjust_worm_health(-unsuitable_heat_damage * (burn_coeff ? burn_coeff : 1) * seconds_per_tick)
+
+ if (COOLDOWN_FINISHED(src, host_heat_alert_cooldown))
+ to_chat_self(span_userdanger("Your blood is burning up!"))
+ COOLDOWN_START(src, host_heat_alert_cooldown, 15 SECONDS)
+
+/mob/living/basic/blood_worm/proc/on_host_adjust_oxy_damage(datum/source, type, amount, forced)
+ SIGNAL_HANDLER
+ return COMPONENT_IGNORE_CHANGE // Functionally, this unimplements oxy damage from hosts altogether. Which is exactly what we want.
+
+/mob/living/basic/blood_worm/proc/on_host_pre_update_blood_status(datum/source, had_blood, has_blood, old_blood_volume)
+ SIGNAL_HANDLER
+ if (!has_blood)
+ leave_host()
+
+/mob/living/basic/blood_worm/proc/create_host_hud(datum/source)
+ SIGNAL_HANDLER
+ UnregisterSignal(host, COMSIG_MOB_HUD_CREATED)
+
+ var/datum/hud/hud = host.hud_used
+ blood_display = new(null, hud)
+ hud.infodisplay += blood_display
+ hud.show_hud(hud.hud_version)
+
+/mob/living/basic/blood_worm/proc/remove_host_hud()
+ var/datum/hud/hud = host.hud_used
+
+ if (!hud)
+ QDEL_NULL(blood_display)
+ return
+
+ hud.infodisplay -= blood_display
+ QDEL_NULL(blood_display)
diff --git a/code/modules/antagonists/blood_worm/blood_worm_host_mob.dm b/code/modules/antagonists/blood_worm/blood_worm_host_mob.dm
new file mode 100644
index 00000000000..310f8a0bd94
--- /dev/null
+++ b/code/modules/antagonists/blood_worm/blood_worm_host_mob.dm
@@ -0,0 +1,17 @@
+/mob/living/blood_worm_host
+ name = "Host"
+ desc = "...how are you examining this? THIS THING ISN'T EVEN EMBODIED."
+
+ var/datum/action/changeling_expel_worm/expel_worm_action
+
+/mob/living/blood_worm_host/Login()
+ . = ..()
+ if (!.)
+ return
+
+ if (IS_CHANGELING(src))
+ to_chat(src, span_good("The blood worm in your body is vulnerable to your genetic prowess!"))
+
+ if (!expel_worm_action)
+ expel_worm_action = new(src)
+ expel_worm_action.Grant(src)
diff --git a/code/modules/antagonists/blood_worm/blood_worm_mob.dm b/code/modules/antagonists/blood_worm/blood_worm_mob.dm
new file mode 100644
index 00000000000..a3a960abcd9
--- /dev/null
+++ b/code/modules/antagonists/blood_worm/blood_worm_mob.dm
@@ -0,0 +1,403 @@
+/mob/living/basic/blood_worm
+ icon = 'icons/mob/nonhuman-player/blood_worm_32x32.dmi'
+
+ faction = list(FACTION_BLOOD_WORM)
+
+ initial_language_holder = /datum/language_holder/blood_worm
+
+ // FIXME: This should use MOB_BUG, but it makes hunter boxing instakill juveniles.
+ // I.e. blood worms should be susceptible to pest killer, but not hunter boxing.
+ mob_biotypes = MOB_ORGANIC
+ basic_mob_flags = FLAMMABLE_MOB
+ status_flags = CANPUSH // No CANSTUN, blood worms are immune to stuns by design.
+
+ damage_coeff = list(BRUTE = 1, BURN = 1.5, TOX = 0, STAMINA = 0, OXY = 0)
+
+ pressure_resistance = 200
+
+ combat_mode = TRUE
+
+ melee_attack_cooldown = CLICK_CD_MELEE
+
+ attack_sound = 'sound/items/weapons/bite.ogg'
+ attack_vis_effect = ATTACK_EFFECT_BITE
+ attack_verb_continuous = "bites"
+ attack_verb_simple = "bite"
+
+ minimum_survivable_temperature = 0
+ maximum_survivable_temperature = T0C + 100
+ unsuitable_cold_damage = 0
+
+ habitable_atmos = null
+
+ // A vivid red.
+ lighting_cutoff_red = 40
+ lighting_cutoff_green = 20
+ lighting_cutoff_blue = 20
+
+ /// Identification number, i.e. "hatchling blood worm (id)"
+ /// Used for carrying the same number through growth stages.
+ var/id_number = null
+
+ /// Effect name for stuff like "invade-[effect_name]".
+ /// Should correspond to the growth stage, like "adult".
+ var/effect_name = ""
+
+ /// How long the leave host animation lasts for this type, in deciseconds.
+ var/leave_host_duration = 0
+
+ /// How much regular (human, blood pack, etc.) blood the worm has consumed.
+ var/consumed_normal_blood = 0
+ /// How much synthetic (monkey, duplicated, etc.) blood the worm has consumed.
+ var/consumed_synth_blood = 0
+ /// The maximum amount of synthetic blood counted for growth.
+ var/maximum_synth_blood = 1000
+ /// How efficient ingesting synthetic blood is compared to normal blood. (ingested amount is multiplied by this)
+ var/synth_blood_efficiency = 0.7
+
+ /// The current host of the blood worm, if any.
+ /// You can use this to check if the blood worm has a host.
+ var/mob/living/carbon/human/host
+ /// The backseat mob for the mind of the current host, if any.
+ /// This mob is always dead as it's just a mind holder.
+ var/mob/living/blood_worm_host/backseat
+
+ /// The blood display on the left side of the screen, which is shown to the blood worm while in a host, if any.
+ var/atom/movable/screen/blood_level/blood_display
+
+ // Innate and shared actions
+
+ /// Typed, please initialize with a proper action subtype. (empty = no action)
+ var/datum/action/cooldown/mob_cooldown/blood_worm/spit/spit_action
+ /// Typed, please initialize with a proper action subtype. (empty = no action)
+ var/datum/action/cooldown/mob_cooldown/blood_worm/leech/leech_action
+ /// Not typed, please leave empty.
+ var/datum/action/cooldown/mob_cooldown/blood_worm/invade/invade_action
+ /// Typed, please initialize with a proper action subtype. (empty = no action)
+ var/datum/action/cooldown/mob_cooldown/blood_worm/cocoon/cocoon_action
+
+ // Host actions
+
+ /// Typed, please initialize with a proper action subtype. (empty = no action)
+ var/datum/action/cooldown/mob_cooldown/blood_worm/inject/transfuse_action
+ /// Not typed, please leave empty.
+ var/datum/action/cooldown/mob_cooldown/blood_worm/eject/eject_action
+ /// Not typed, please leave empty.
+ var/datum/action/cooldown/mob_cooldown/blood_worm/revive/revive_action
+
+ /// List of actions outside of a host.
+ var/list/innate_actions = list()
+ /// List of actions inside of a host.
+ var/list/host_actions = list()
+
+ /// Whether the blood worm has a host AND is currently in control of that host.
+ var/is_possessing_host = FALSE
+
+ /// How quickly the blood worm regenerates, in health per second.
+ var/regen_rate = 0
+
+ COOLDOWN_DECLARE(host_heat_alert_cooldown)
+ COOLDOWN_DECLARE(host_bleed_alert_cooldown)
+
+/mob/living/basic/blood_worm/Initialize(mapload)
+ . = ..()
+
+ // Innate and shared actions
+
+ if (ispath(spit_action, /datum/action/cooldown/mob_cooldown/blood_worm/spit))
+ spit_action = new spit_action(src)
+ innate_actions += spit_action
+ host_actions += spit_action
+
+ if (ispath(leech_action, /datum/action/cooldown/mob_cooldown/blood_worm/leech))
+ leech_action = new leech_action(src)
+ innate_actions += leech_action
+
+ invade_action = new(src)
+ innate_actions += invade_action
+
+ if (ispath(cocoon_action, /datum/action/cooldown/mob_cooldown/blood_worm/cocoon))
+ cocoon_action = new cocoon_action(src)
+ innate_actions += cocoon_action
+
+ // Host actions
+
+ if (ispath(transfuse_action, /datum/action/cooldown/mob_cooldown/blood_worm/inject))
+ transfuse_action = new transfuse_action(src)
+ host_actions += transfuse_action
+
+ eject_action = new(src)
+ host_actions += eject_action
+
+ revive_action = new(src)
+ host_actions += revive_action
+
+ grant_actions(src, innate_actions)
+
+ ADD_TRAIT(src, TRAIT_BLOOD_HUD, INNATE_TRAIT)
+
+ // Move speed delays at min health
+ // Hatchling goes from 1.5 up to 2 deciseconds
+ // Juvenile goes from 1.8 up to 2.3 deciseconds
+ // Adult goes from 2 up to 2.5 deciseconds
+ // For reference, a cyborg has a move speed delay of 1.5 deciseconds
+ AddComponent(/datum/component/health_scaling_effects, min_health_slowdown = 0.5)
+
+ AddElement(/datum/element/anti_self_harm)
+
+ id_number = rand(1, 999)
+ update_name()
+
+/mob/living/basic/blood_worm/Destroy()
+ unregister_host()
+
+ spit_action = null
+ leech_action = null
+ invade_action = null
+ cocoon_action = null
+
+ transfuse_action = null
+ eject_action = null
+ revive_action = null
+
+ innate_actions = null
+ host_actions = null
+
+ return ..()
+
+/mob/living/basic/blood_worm/Login()
+ . = ..()
+ if (!. || !client)
+ return FALSE
+ if (host)
+ ADD_TRAIT(host, TRAIT_MIND_TEMPORARILY_GONE, BLOOD_WORM_HOST_TRAIT)
+
+/mob/living/basic/blood_worm/Logout()
+ . = ..()
+ if (host)
+ REMOVE_TRAIT(host, TRAIT_MIND_TEMPORARILY_GONE, BLOOD_WORM_HOST_TRAIT)
+
+/mob/living/basic/blood_worm/process(seconds_per_tick, times_fired)
+ if (!host)
+ return
+
+ sync_health()
+
+/mob/living/basic/blood_worm/Life(seconds_per_tick, times_fired)
+ . = ..()
+
+ if (!host)
+ // Moved to host life while in a host.
+ adjust_worm_health(regen_rate * seconds_per_tick)
+ else
+ bodytemperature = T20C
+
+/mob/living/basic/blood_worm/handle_environment(datum/gas_mixture/environment, seconds_per_tick, times_fired)
+ if (host)
+ bodytemperature = T20C
+ else
+ return ..()
+
+/mob/living/basic/blood_worm/update_name(updates)
+ . = ..()
+ name = "[initial(name)] ([id_number])"
+ real_name = name
+
+/mob/living/basic/blood_worm/adjust_health(amount, updating_health, forced)
+ return host ? 0 : ..() // Prevents damage from adjust_x_loss while in a host, because that damage would be nullified by the next [proc/sync_health] call. Adjust host blood volume instead.
+
+/mob/living/basic/blood_worm/set_stat(new_stat)
+ . = ..()
+
+ if (host && stat != CONSCIOUS)
+ leave_host()
+
+/mob/living/basic/blood_worm/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
+ . = ..()
+
+ if (host && loc != host)
+ unregister_host()
+
+/// Gets the current health of the worm, regardless of if its in a host or not.
+/mob/living/basic/blood_worm/proc/get_worm_health()
+ return host ? min(host.get_blood_volume() * BLOOD_WORM_BLOOD_TO_HEALTH, maxHealth) : health
+
+/// Adjusts the current health of the worm, regardless of if its in a host or not.
+/mob/living/basic/blood_worm/proc/adjust_worm_health(amount)
+ return host ? host.adjust_blood_volume(amount * BLOOD_WORM_HEALTH_TO_BLOOD) * BLOOD_WORM_BLOOD_TO_HEALTH : adjust_brute_loss(-amount)
+
+/mob/living/basic/blood_worm/proc/grant_actions(mob/target, list/actions)
+ for (var/datum/action/action as anything in actions)
+ action.Grant(target)
+
+/mob/living/basic/blood_worm/proc/remove_actions(mob/target, list/actions)
+ for (var/datum/action/action as anything in actions)
+ action.Remove(target)
+
+/mob/living/basic/blood_worm/proc/sync_health(already_ejecting = FALSE)
+ if (!host)
+ return
+
+ var/host_max_blood = maxHealth * BLOOD_WORM_HEALTH_TO_BLOOD
+
+ // Cap host blood to maximum
+ if (host.get_blood_volume() > host_max_blood)
+ host.set_blood_volume(host_max_blood)
+
+ var/cached_blood_volume = host.get_blood_volume()
+
+ // Sync mob health to host blood
+ set_brute_loss(maxHealth * (1 - cached_blood_volume / host_max_blood))
+
+ // Checks if we still have a host since setBruteLoss() can kill us, causing us to leave our host.
+ if (!already_ejecting && cached_blood_volume <= get_eject_volume_threshold())
+ // Sent before leave_host() for the correct message order in chat
+ to_chat_self(span_userdanger("You run out of blood to control your host with!"))
+
+ leave_host()
+
+ // Has to be sent after the forceMove() in leave_host()
+ balloon_alert_self("out_of_blood!")
+
+/// Gets BLOOD_WORM_EJECT_THRESHOLD as an actionable blood volume threshold.
+/mob/living/basic/blood_worm/proc/get_eject_volume_threshold()
+ return maxHealth * BLOOD_WORM_HEALTH_TO_BLOOD * BLOOD_WORM_EJECT_THRESHOLD
+
+/obj/effect/temp_visual/blood_worm_invade_host
+ icon = 'icons/mob/nonhuman-player/blood_worm_32x32.dmi'
+ icon_state = "invade-hatchling" // Not actually used for anything, it's just a default because otherwise the unit tests scream about it.
+ duration = 2 SECONDS
+
+/obj/effect/temp_visual/blood_worm_invade_host/Initialize(mapload, effect_name)
+ . = ..()
+ icon_state = "invade-[effect_name]"
+
+/mob/living/basic/blood_worm/hatchling
+ name = "hatchling blood worm"
+ desc = "A freshly hatched blood worm. It looks hungry and weak, requiring blood to grow further."
+
+ icon_state = "hatchling"
+ icon_living = "hatchling"
+ icon_dead = "hatchling-dead"
+
+ mob_size = MOB_SIZE_TINY
+ pass_flags = PASSTABLE | PASSMOB // The benefits of being a tiny little bastard.
+
+ guaranteed_butcher_results = list(/obj/item/food/meat/slab/blood_worm = 1)
+
+ maxHealth = 80 // In practice, escaping into a vent from someone who could 3 hit you with a basic bitch welder was really hard. This used to be 50, and was buffed to 80, but speed was slowed a bit.
+ health = 80
+
+ unsuitable_heat_damage = 1
+
+ obj_damage = 15 // 10 -> 15, in testing 10 proved to be way too slow at breaking morgue trays and such. Make sure that this doesn't go above airlock damage deflection.
+ melee_damage_lower = 12
+ melee_damage_upper = 14
+ armour_penetration = 10
+
+ speed = 0
+
+ effect_name = "hatchling"
+ leave_host_duration = 0.6 SECONDS
+
+ leech_action = /datum/action/cooldown/mob_cooldown/blood_worm/leech/hatchling
+ cocoon_action = /datum/action/cooldown/mob_cooldown/blood_worm/cocoon/hatchling
+
+ transfuse_action = /datum/action/cooldown/mob_cooldown/blood_worm/inject/hatchling
+
+ regen_rate = 0.3 // 266 seconds to recover from 0 to 80, or almost 4 and a half minutes.
+
+/mob/living/basic/blood_worm/hatchling/Initialize(mapload)
+ . = ..()
+
+ ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT)
+
+ AddComponent(/datum/component/slide_under_doors, slide_in_delay = 3 SECONDS)
+
+/mob/living/basic/blood_worm/juvenile
+ name = "juvenile blood worm"
+ desc = "A mid-sized blood worm. It looks bloodthirsty and has numerous long and extremely sharp teeth."
+
+ icon_state = "juvenile"
+ icon_living = "juvenile"
+ icon_dead = "juvenile-dead"
+
+ mob_size = MOB_SIZE_SMALL
+
+ guaranteed_butcher_results = list(/obj/item/food/meat/slab/blood_worm = 2)
+
+ maxHealth = 120 // Note that the juveniles are bigger and slower than hatchlings, making them far easier to hit by comparison.
+ health = 120
+
+ unsuitable_heat_damage = 1.5
+
+ obj_damage = 35 // Able to break most obstacles, such as airlocks. This is mandatory since they can't ventcrawl anymore.
+ melee_damage_lower = 18 // Juveniles can't run away nearly as easily, so they are expected to do direct combat against normal crew. (but lose hard to well-equipped sec)
+ melee_damage_upper = 22
+ armour_penetration = 30
+
+ wound_bonus = 0 // Juveniles can afford to heal wounds on their hosts, unlike hatchlings. Note that this can't cause critical wounds. (at least it didn't in testing)
+ sharpness = SHARP_POINTY
+
+ speed = 0.3
+
+ effect_name = "juvenile"
+ leave_host_duration = 1 SECONDS
+
+ spit_action = /datum/action/cooldown/mob_cooldown/blood_worm/spit/juvenile
+ leech_action = /datum/action/cooldown/mob_cooldown/blood_worm/leech/juvenile
+ cocoon_action = /datum/action/cooldown/mob_cooldown/blood_worm/cocoon/juvenile
+
+ transfuse_action = /datum/action/cooldown/mob_cooldown/blood_worm/inject/juvenile
+
+ regen_rate = 0.4 // 300 seconds to recover from 0 to 120, or exactly 5 minutes.
+
+/mob/living/basic/blood_worm/juvenile/Initialize(mapload)
+ . = ..()
+
+ AddComponent(/datum/component/slide_under_doors, slide_in_delay = 5 SECONDS)
+
+/mob/living/basic/blood_worm/adult
+ name = "adult blood worm"
+ desc = "A monstrosity of a blood worm. It'd probably be better to put your head in an industrial shredder rather than its maw."
+
+ icon = 'icons/mob/nonhuman-player/blood_worm_32x48.dmi'
+
+ icon_state = "adult"
+ icon_living = "adult"
+ icon_dead = "adult-dead"
+ health_doll_icon = "adult_blood_worm"
+
+ // We undershoot a tiny bit so that Proto-Kinetic Crushers don't work on adult blood worms.
+ mob_size = MOB_SIZE_HUMAN
+
+ guaranteed_butcher_results = list(/obj/item/food/meat/slab/blood_worm = 3)
+
+ maxHealth = 180 // Used to be 150, turns out their lack of armor and weakness to burn made them too squishy. People kited them using lasguns, leaving them with no way to fight back at all.
+ health = 180
+
+ unsuitable_heat_damage = 2
+
+ obj_damage = 50 // You are not getting away.
+ melee_damage_lower = 25
+ melee_damage_upper = 30 // Turns out adults regularly end up encountering advanced weapons like cap's sabre, lasguns + armor, eswords, etc. They need a strong melee.
+ armour_penetration = 50 // Adults will 100% encounter sec, they are shit out of luck without proper armor pen.
+
+ wound_bonus = 0 // Able to cause critical wounds.
+ sharpness = SHARP_POINTY
+
+ attack_verb_simple = "gore"
+ attack_verb_continuous = "gores"
+
+ speed = 0.5
+
+ effect_name = "adult"
+ leave_host_duration = 1.4 SECONDS
+
+ spit_action = /datum/action/cooldown/mob_cooldown/blood_worm/spit/adult
+ leech_action = /datum/action/cooldown/mob_cooldown/blood_worm/leech/adult
+ cocoon_action = /datum/action/cooldown/mob_cooldown/blood_worm/cocoon/adult
+
+ transfuse_action = /datum/action/cooldown/mob_cooldown/blood_worm/inject/adult
+
+ regen_rate = 0.5 // 360 seconds to recover from 0 to 180, or exactly 6 minutes.
diff --git a/code/modules/antagonists/blood_worm/blood_worm_objectives.dm b/code/modules/antagonists/blood_worm/blood_worm_objectives.dm
new file mode 100644
index 00000000000..de74b29b754
--- /dev/null
+++ b/code/modules/antagonists/blood_worm/blood_worm_objectives.dm
@@ -0,0 +1,81 @@
+/datum/objective/blood_worm
+ abstract_type = /datum/objective/blood_worm
+
+/datum/objective/blood_worm/proc/get_blood_worm_team()
+ RETURN_TYPE(/datum/team/blood_worm)
+ return team
+
+/datum/objective/blood_worm/kill
+ name = "KILL"
+ explanation_text = "We must prevent all members of station command from escaping alive on the emergency shuttle."
+
+/datum/objective/blood_worm/kill/check_completion()
+ for (var/mob/player_mob as anything in GLOB.player_list)
+ if (!(player_mob.mind?.assigned_role.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND))
+ continue
+ if (!considered_alive(player_mob.mind))
+ continue
+ if (considered_exiled(player_mob.mind))
+ continue
+ // Counts whiteship as escaped, but not pods.
+ if (player_mob.onCentCom() || player_mob.onSyndieBase())
+ return FALSE
+
+ return TRUE
+
+/datum/objective/blood_worm/consume
+ name = "CONSUME"
+
+ var/blood_required = 0
+
+/datum/objective/blood_worm/consume/New(text)
+ blood_required = rand(20, 30) * 100
+ update_explanation_text()
+
+/datum/objective/blood_worm/consume/update_explanation_text()
+ explanation_text = "We must consume a total of at least [blood_required] units of real blood to sate our appetite."
+
+/datum/objective/blood_worm/consume/check_completion()
+ return get_blood_worm_team().blood_consumed_total >= blood_required
+
+/datum/objective/blood_worm/multiply
+ name = "MULTIPLY"
+
+ var/times_required = 0
+
+/datum/objective/blood_worm/multiply/New(text)
+ times_required = rand(2, 3)
+ update_explanation_text()
+
+/datum/objective/blood_worm/multiply/update_explanation_text()
+ explanation_text = "At least [times_required] of us must reproduce to pave the way for our overwhelming numbers."
+
+/datum/objective/blood_worm/multiply/check_completion()
+ return get_blood_worm_team().times_reproduced_total >= times_required
+
+/datum/objective/blood_worm/conquer
+ name = "CONQUER"
+
+ var/worms_required = 0
+
+/datum/objective/blood_worm/conquer/New(text)
+ worms_required = rand(3, 4)
+ update_explanation_text()
+
+/datum/objective/blood_worm/conquer/update_explanation_text()
+ explanation_text = "At least [worms_required] of us must escape alive on the emergency shuttle to conquer what is on the other side."
+
+/datum/objective/blood_worm/conquer/check_completion()
+ var/conquerors = 0
+
+ for (var/datum/mind/member as anything in team.members)
+ if (QDELETED(member.current))
+ continue
+ if (!isbloodworm(member.current) && !HAS_TRAIT(member.current, TRAIT_BLOOD_WORM_HOST))
+ continue
+ if (member.current.stat == DEAD) // This assumes that the mind of a blood worm that is inhabiting a dead host is in their blood worm mob.
+ continue
+ if (member.current.onCentCom() || member.current.onSyndieBase())
+ conquerors++
+
+ return conquerors >= worms_required
diff --git a/code/modules/antagonists/blood_worm/blood_worm_team.dm b/code/modules/antagonists/blood_worm/blood_worm_team.dm
new file mode 100644
index 00000000000..5d186eb1280
--- /dev/null
+++ b/code/modules/antagonists/blood_worm/blood_worm_team.dm
@@ -0,0 +1,86 @@
+/datum/team/blood_worm
+ name = "\improper Blood Worms"
+ member_name = "blood worm"
+
+ var/blood_consumed_total = 0
+ var/times_reproduced_total = 0
+
+ var/static/list/objective_types = list(
+ /datum/objective/blood_worm/kill,
+ /datum/objective/blood_worm/consume,
+ /datum/objective/blood_worm/multiply,
+ /datum/objective/blood_worm/conquer,
+ )
+
+/datum/team/blood_worm/New(starting_members)
+ . = ..()
+
+ for (var/objective_type in objective_types)
+ objectives += new objective_type()
+
+ for (var/datum/objective/objective as anything in objectives)
+ objective.team = src
+
+/datum/team/blood_worm/add_member(datum/mind/member)
+ . = ..()
+ register_member_mob(member.current)
+ RegisterSignal(member, COMSIG_MIND_TRANSFERRED, PROC_REF(on_mind_transferred))
+
+/datum/team/blood_worm/remove_member(datum/mind/member)
+ UnregisterSignal(member, COMSIG_MIND_TRANSFERRED)
+ unregister_member_mob(member.current)
+ return ..()
+
+/datum/team/blood_worm/proc/on_mind_transferred(datum/mind/member, mob/previous_body)
+ SIGNAL_HANDLER
+ unregister_member_mob(previous_body)
+ register_member_mob(member.current)
+
+/datum/team/blood_worm/proc/register_member_mob(mob/member_mob)
+ if (!member_mob)
+ return
+
+ RegisterSignal(member_mob, COMSIG_BLOOD_WORM_CONSUMED_BLOOD, PROC_REF(on_blood_worm_consumed_blood))
+ RegisterSignal(member_mob, COMSIG_BLOOD_WORM_REPRODUCED, PROC_REF(on_blood_worm_reproduced))
+
+/datum/team/blood_worm/proc/unregister_member_mob(mob/member_mob)
+ if (!member_mob)
+ return
+
+ UnregisterSignal(member_mob, list(COMSIG_BLOOD_WORM_CONSUMED_BLOOD, COMSIG_BLOOD_WORM_REPRODUCED))
+
+/datum/team/blood_worm/proc/on_blood_worm_consumed_blood(mob/living/basic/blood_worm/worm, normal_blood_amount, synth_blood_amount, total_blood_amount)
+ SIGNAL_HANDLER
+ blood_consumed_total += normal_blood_amount
+
+/datum/team/blood_worm/proc/on_blood_worm_reproduced(mob/living/basic/blood_worm/worm)
+ SIGNAL_HANDLER
+ times_reproduced_total++
+
+/datum/team/blood_worm/roundend_report()
+ var/list/report = list()
+
+ report += span_header("\The [name]:")
+ report += printplayerlist(members)
+
+ if (length(objectives))
+ report += span_header("Their collective goals:")
+ report += print_objective_list()
+
+ report += ""
+
+ report += did_we_win() ? span_greentext("The [name] were successful!") : span_redtext("The [name] have failed!")
+
+ return "
hiikg&*&Yq@^{?MdTv0$6Jn*5IJ)aSK zd<@p8C7VJXM-Q@0w^Yq*>&~Y@?bYqkS&!|sORG$Qb_=_R0HOZ{KF#b&7*U5&b7hXN z?di-dt9|@qt%mMcLA#mlteQ7o>0CNknYGX0my&9FJKp2nB5}L%=aMAusuW7_D%1+- z3t5q@UUABsGYic7!+06i9SQGd )Y8D8R>d$ggD)w->8f>x?-$-te!DLl_Lu13zS< zK^4Oa$pL7Y_3!&|-%J{F>potIPQp28O1FtoP3!N@j68q!HK-l%jX;!7o#gEwks@PM z5a*YjxAD|c_ifPw{H^PQEO|rvFN|g`zTVQmG14^Eou&qb(y&J+>4J;j$sVp!a@jtf z<(IdNIoj n0(ZzWp=wA zqrN{yNCNu@|GHt**-BDOZ1o594RzAt3A>< )Ihu=sFCYLw0 zo;0^w{>I}vz1PyU_RqVy`KF@SFG(AeUTuRS2M{i#1$=99M`}APHsSPnD5$-tE~=`k z@^^R)4@=MurKF@p=$uSDothA&9SdMinOf(3ig)*n@=8KB5mdF+*szhjw{u|q1l}WU zp)Lgno|}5)* M@u;ghPPYU{#<}u9{W?<4TIR>XeO=YP;C324JWDcv*Q{?^5;YP zqwnQ;g2p^TE(b(3s5oi^ln)!DqH9QhaqmpmGK9als{@ete9biZ{O}PFZ0j_2=w!|w zV?X_iqZ+K%S@4zmM
eB<}*s))XbmN{XAPuDW RY86InT{fuF$IF{h|I 6%808ICh>V{}h4XpgMT{7lL%&fFY#+nEyuH0Vu4N~= zxNFQ{O%i}y>3OX@PY%UbPNP0=4w|=qZhN|X`>v20M5!X9T5jhHebTsAb+gK62LVZr zWb gDEd44Z1e^oeKql}IdY zT=4ZJ6<61{TPdC&m&Z%y&NepQMBmz{ZZ{$Zbtg2J6ASWrCw2Zj >@u}Y>GKby1q=f>T-v^Lha2=t8rYDgU> zWWpbWh8k-E+L=jCzkY%#tb4ETF+`ecv8;JX4akKy1(`v<9q7;g)Z@i-MSK8rO0)cQ z3tY;L3LHLnv H1gwc@!@|PBh^RnHkOqAx&knibsg9SH1WcXd7^_5L#-eh82?u z- LTT-tDm(f zwEgHe0vjusUmqOE0R4OIsr1B!UFy0%S({zJM}0;K6)7-(4%QNCC!n}H51%wXy6qj5 zlr|m(6U0yhm7 aD$sP_jn%6%tcr!)a%2H0KZE3k#l+2=KMhF?8<$F1EuTO& L*Wn@ zRSJeoX5jhC`CZy^v~SLzg@tR@|A4n{s6WuD{jH-!&E@Rq&$VX( N~?r_MV z{k5`PtFieK(7=9bw7&ESHI2hG3ds5}-p^${-k(Iq{!zjHJBWr#z7=%S`k{>2_ vNi`(3)1mnJw&v_N}(!IYUozS)fx^v?aiDU~4p|N=Y7x^t5lDcYc zYa`CE+kF;e@pGtO8XQEBJEZ<=se8>))|=6V6pPpBo_C3fjo`1L7IBsTb8)oXUbT#^ zpVOCfe%afK{92r80rS}O5Qj*E1;#!fs{ZCy0qIA&|DC7gz`4X-I5~Aa`>MaGvQPx; z62T9d{zSJ7g#IlxMQC&+OTsGhC;}ex23Tp~PkSqGzR3T_)<$t)>qAYP&kgTzNtZ5_ zggw8o{Xah`!dHGh#VZ}QMO;;%Xbw?vUz;d=_cN`!Cj2y1iKD|OkpO~MYy3Nf|Dk$F zx$#|NL>w|+1k*^AS~>grZH-?#nnWn?0lvhrC+}_7g0FAQp=c@dM6X|6D5&^A#LVA$ z*Qz8lm((#n6pIxGfll9Z(cdzvDze7lP=oh4Dc2d@Kgp)BtP{dhe0T9Bty@Ebv8XyF zF#s#+7|HPVEg5Ear{U)*P+h-#dtVUwOpl|NqRH@R%=gFA1RG^ueIk-oX6*rlk r0FMVGH zt2N@Ae%bz4!=;eo-o(VD(rJkv`M@j&NuKk$?`c2r+1jhz+4>fYMursP=MS6n*^E7o zfG$i1*rcfwY4Z~4JhKq 2)eASzM)u(bOcN8UAWWRvF}|1|*gqx1NxHu) zfvMoo?kKsOevjztv>wkz!BnCkARrLFUYA?Nc*`_Jn#vd_?C`CLB^S6Rki019rQ{A6 zctqkyN>>|j^|SqijF`cC6ZudSE>s83{I1a?S!X_h8~t#6GlO)}+vHaov+8Oo2nE;2 zVsbRv!Hem)elU6oSb6G60q`O3H@^ENseJfUsI6x%T0(_Dbav0hE?2b{{ESzMhQz^n zq0XWrgI+;S4$TvQj(qXk+pKO~{md_+_G?9HoK$!v7HHP6LjA#!WsNZY8FuEvYdgg> zlDWM?Yy-}A)D#$&-ETiP5 `odA=J50MIhI+K#|V@LDil@YLNV2nmV8## zxgtQ??AGg`0*EWig*&eISP#XKNB3cvO`iZCOLpB!&~4SAhKsN3zSivwggaKv2&X >)5c9!{Q%vW$BZqyEw510g`0BJVV_Zwv7R)L_w$H82k81a>!xV*lB9W|O# zN_1d=PBpBGx%wrT=C{4#s`D2wZWQNn}Zw+I~!cXzWrm#|G=>0Qn-~7 zUYyv6H%5o|ScWgPr>9r3gk% h+*`WW zii+7YG)jZdlu4?Df~$6$S d^;WU3crvbtn&9ebeoy-4@V1=ruSKM zqS7!{t+XO(Xh5T53jNQBI0zZ2zLHeT9(Q@MyJzA?O7my)xMrma3SQY m>cBG1i2p|1saY z8xxv22?$hX@S@!C$vN74F4SUe#ScHia0<+Z?&M31!^&zHrQ@(;%A}`kBKY1-fr+-< zFYqBXr&;Cf&$xWmBMx@WBJK_N$IJOi_UXTSlQe(~e*F_lGbuMAkX7@sLn{jVY-3F9 z1RXMxBPKNCesE}Yyx0uEB$qkOG9$b;2t0olrSpu5&1E!>!YBrUg4mpM_|s4#9vDmC z-7Pjb!_S+Bqnr5H8g8ODUH9*fCHI;@Q N99%xW*C|RC )B7sasbw%Cj{S5?L_Ve#2QX^X2^`Q5#ucuZ}0V#OT9sKcFYXDlbWC|=SSFrjN1 zLSFKj+bu3O+dn+rNX|K1wa>aH!fv3JItW%yKU4Q{d^oGePpZ7Om%bm2F#$AiTbdxd zewG7Q+x@b7!sb(inw)t?=4_SWCX3V!4&?3m&gdQ#+7C}WbhP@Y81uy9mJ@9P`_uC( zVupOuQAkjM9G0JA$P9n;8{kQHV$$(a>$}^_>fqt`IJMXb@GMm_;o0>oRX(?IG30Va zbSjUwgmPxq05dQ8Zl0K%3#DE`+J7fDi+fAjotY?80ST)iQqt%?+~fkVK7?rJN_rgV zlSV73XYf9Ep=;qon(VVzHw2ph8puG^dxP}jL _Rua$N_d2I;ngXLJ51g^x#`I z#)BCz%`!=t$6n_xQkdrQe0Q9lh`^<1ev*)Nq+=G}&{zS7bnHBz=4!WAax;J2d{k`{ z&Q~GzZY#3ky9*7?F7FW24QHeM?#p3zQF+XySxISW0DW&%=*j|g6YulUfZ*wkeE4tw zJvl&!?#lWC*dvFqYE_j;N`sp}m5W%)#rU4uxW?wB*k%Dh_A+vP2*U!Gq sAm zy)sP#bsjZ6DK+8Pc==wMiXKUsyzbBUhNTxdI$5JYmeA&REc##P<5KY}onD);d2=qV zqO^09{%+2V@j8p2G2mwNQYTh-x8Ldg?MY#)mzK!mCaYm`d|J`AcV^416DH2xdS{h_ zT>F#?qT09XtG&8pZTHv-Yc8`}(&c6We%TipWaIb3rG?VWR1&4n`$_2VmRh^bNQ4xZ zTIvG<1U!oVi)?}R8Tyz=?Q4$GcOjr@;?+Jr(?sy!LOxAt{cUS3rnjbp9h$HGf_d$i zq;JHDLCfJiW^J%E6V6)fq$PhJg>O^O_E6L^3C)WyypETx5UyKX$Ha_^7g|frRX=9J zj1+~;TeV$G_c#<86y|OrD6C38Gk(239k&1k16DMMY8DVQ!`>_@t?Latc2SB-!Ex2Z ztCXd{EUSN9A-?l{k4Ndx=73hDA%SCzqos-HZAmih@b=IOv@eXq`C9t7A$Xr;|FEK* zI+hO &z7d23|~e-Rs+xoj)C(sQ~m` z257zTS$)@2nXB-;z6Y0|d+hsZ7+RvWH~0P@Y?iq v*Z<(Pz%m&&L2b0GGw8_DWcq?Gh!a|4-`xmqyLv9sJwN-8AURt!J^} zoRRme?C_sN_n{Qs<0#nz$Z$g)7%0A)_x6adI6{P+mLoqIa>fMcc^ Xo$gThaTtMlh{$4lJ-V{Knb^~whN@Y|jp>)mP zbl(L!-mA@;VlKVCdy ?uFVfz3lIdq2;qCaCnde(T4u^oYOv0>= zolTBa+rvBb{=b#9>&*oMkJoqe%*7TOf`nH0!mSF#IX^zhH!fQA7PEy-pQ+XI6&w#t z6v(mP17|?rXwR_bmyfbH0dqgT1a`p>sj@?^GTjkO3TK_w!s T? zSK{Aq#xG*Kdbx*RCaw<8|AV0c(6-IEjwiP)YD|c` tYoB27hj#p9BW%knp{#l+7Q0SU2X9yCyIk+CyP`P!&iy zTiG?5BW7riBot!CdKTM2SV AgpJE;wO!;NW+!|D`&k>5QT5ST?SA$eqqC}UblDX& z@9ijD?oAf)d>HxSDmR<~5Tw*pJ1(_Y%vKs0=iZCLT= J z?pEgf?+y(?pe;b2N^u}&`vNh_Z=tT4X|QnDavXzMV(}xS2ifU)j0$< 3(1E0R*d zSJp-N@=3u|UsZiOUYLvp6;f}h GbT@$(8hVR})#>ysM&=^!J{{PQVz}1)=yF z%I$;@=drT3p&`Xge1hZBc6h>zkUCHq9gcSp1PEHP4z3@<@m;ac-Prj_Iy{jgptkJ0 z&y*L *
fTXmBkme&u0z$BX-IJe;cDKr+UZPyn3FcnjG71Z_AO0RX+qhCQZw6 z3DvK{EQ&vxB>Z5}+tl2QBHjMCq||x2jX0TE<41er*z78iiTin>>x(Bx_gPk%I|J2( z*AGRgc`f{aj==&FJoQD;E?>_J_v^HOF TD>DhofbgMjy}aVV%k&|D`I z2`e|l@JxQ&$B+~tGIKzX#x@c~x%=;SE8!SY!%i;`t_z>S)E2l)CDtuJ3z^cCs3P~_ zDd4gVA~l;g_tI*0918N@U#dIqL4Be|39BH;# Cw?a~&Y_iH z`WiD!D@b g^j{0h2uoG 40EtmFUqdAzI(9#G*A=CPg192JHMlCaPl~VFXvFAGjKHrDyomc@qDl z6*pV9Ls@1OR+Qh5q8mOyyN&ZBfcTKQYlf68d?5b}BnEkjY%LX)pTM8-MnIR_Hz)W# zIEH_KL|iVjT2g6g;zEN}^o7$e=Kq*7j^0)<;2WYKh>7&t)ZXqINcrF{>s*%WSXOcE zeUGDE0XLW)Qv&Lp{}FsT*CkpR4D7b=Ul6>+1xs1J`V;8}1Q!j%GdW!}^b|NspON!z zOLguh{7V?-e>~gc7`o{OLs=nqOw`E0M-5FU(su|7A!!!>zfivCuwK!_6Q*sR4_W2n zbuzD!LN{|#ZoOac-weD~ecW{qKP-e-1vbCPL;+ff?!JA)B#?L-w)ewl++)?e8`=l+ zL^i)A6lU&SW2oIeIQF;sAWhtn0wn%#Hvqa`o8H8KL@D-}!=9lZWZOE;F0p*z#5!$C zl+ezcX{@ny+=ocpHgHOiS#{XRH}k*#!>w#PW|jj_yA^CJl(IxuKoWYM4R~%1s!5gR zT9dW$4@;NTn?A6mM6xfG7a8Ik10QQ#b-2Jhj^fkXag67}F=$@d?M!P)?VN#R;pbp! z7O-1oRfNdKSoDD-eOeqoL54V6bXvf4)ox;}`7ndoVQjV^u|jy|I_4 -ZK0+bKUhu7{S2rNH1RT&y&PW)0hZSCgd)fXR-jsYXfA-q1) zE`-#D`gF+q7Z1J4oQIy4ZIA^VP!QLA;Olmj9Q86Dx_8dB31X wYo|^86dL-d z&-B9e8?~~WLI|v~CBxA}<-4K>gbL18bnFNTGVSgbzDSTeE!bpP|2%3EvyXNof&}Cv zxj&REs>hR5)z`bER{j?B0eh_a-no1~2}DNVCcFN_LOP&8+RCbY9%}D11Yr$F?XJLR zyZ-~Nqnf-4`8?$Igdq-%sdC&U=!KCH$D)`2A(pq#&qhd4S?=A*ze}h4@2-CRf2Ql< zi2{%yXHz$Uep-p1<-OP5i1d13?+}RzX{-AyBy9bG%jpoo9HcAv9>!>|UHzY;q%!Z- zxN4NVv S4eh-53>j<%9dNmU7mVe%fMmhqe01mo{O)8E7*yeIG0S|c1m@P2@h zA@jrvDzb+*-sFL_WRGM37v1udN#np8Mt=24txEvycG;$OwM%Uy3RGY$-nLNQCOhx5 zp+{HK^xwkVJnR9%Zx9~ QbBT90R@S zYgO*a=|bYs0`U@YgGvyZ(wulV7Jd}SPR{M_HA)b1tz?8i?YnQjIy4p!9vKSr^8T7$ zwmDzJWr3}3Gr&V@*Dbs!XapsdAVCBr$$ynSinqpH9;Ckm8ZpMw`DQgTXlvbtul@xN zKbFO&iahq^>~zF|=*N_tC7*D~{FDLL-kCmtXg=2e>pL3o9ltl=LZ++huMz(fePc77 z>L=AJMmN0GQf}(o(eRn^4oL`qJS|@S3=&Ivl&+U%6ArTrEVOB(fw~MlRqY_?J@$31 zQk wlb*ZVh|1_A+A^rJ%juR7O8XV0>x zYSp^%b(V2mWro9ND- qNixSZp z!$gFb$MRnCS%ti~RR9}Jo>Hw7<^Yu3$KdT#*Lp7x@LuhoL<^q=w!P5DT0L ~0rUf!k~sEs zeow~5?`iyx{;}7A9(euWk5%+Wq4B1H7=ix~&mUY;*z=!C+b#4YJ@1z;Xrl8~g!o5j zDP@~l3g$3@LAxSR#|=QyNL}9AuAVSqT!c~^ HIxGB&33O!3?cn^@0350CxSZ z#CI?69=Z^5`Vr{ltOBx{pjdUMa4s~~lFTP!H%#KNEx-g+TSaze19*6ILPpTZ9L^_R zzw^4+62YwlDyOyvnGT%0TtE++Q*L;wxdQHN_tPhh`bJc`4RfYr!OoY^-9A24^b10> zN=pWPa7mbSoEK-E#=><`X4 e;oG4R4CDwq{CxTSY>Vxm#~x-0CgzM z)3B`!pf^Ayj?N49;iV}Cs|`rki;bI&|4Nk*(y&uMz`mo`>v ntYo>$Xzc@Ujz=}^AE%~C&|*GOXcuJFH` z%OG&w6*Vgrf *RAN0q%M6K{h#MD868K&c|h*&8Gb-M=)}I*4kIG zw<_B7vcDCh?mbf!TJ kIMa i`nbBUMQ*6YVmWe*gWF)YDG8L2#PXXaI{=Xey~)Eqi= zJf~L_K_shN>Pe8`jwY=tb+m3ARvgPlLi9@M9iYDVX;P@0(CFLyP^qKxE#?=`+OL#; z#DH-kcAB9ZjR|!&FwA~_>;o2_XXUPrV=uX6RepdwFdp6DkQ8t#>J>OW#W3{N2Nosz zYPb0f`@FmH7tzbQc`2|tM%r!~VnCCTo>08!G}GRi3M*@piCtzOS#Nd7K7T!`Z*c4H zzDiQ)DXW2ZC9tI#tg6R1vTGE7_19OFIo`6KbKyXs$hPe`cSj&Var*R|$4pbcBU^6W z+p6?TU^n}Jk7iQ`E #8+t*) zk)BR+*-PwUq*?U0m9x( X-= zNtO8Q9{Ea$AFKY`EI9| 06cKhGkeqq ^6RVgZ5M9M7ThSpqUaR`1mB*@W{@VNij zNrUnDmPlz$W8q#)v=-LJCLFk=qB;?{b=(P!gOoHMe`Q!fY9*C_V7AYreH!*YK?bd~ z*yB9rTwZlR@2fFz$Mf4Rv~+Q|K#pZpQT+>Iy4txzDiPXWRTFnnS`*D5<2`dM(#s+m zC4Ein*X;IK$)vS@s4$~a;m19Bhg4_ESU^QF%!z0E=BdSl67-DFpN?p}8ojzbZ2sy? zzx&}ytDYNmPQkfmZr+c61w+#5;tvy^Kjgi;( s~4W&N0s) znug15enLR*$Nef>oeRX==SEIh $Y;CvbQ2i2m;(WQ}~@>nbFt?I+yG)jB1L(DT}lKQjOTCVk@-qV^+;!f+t z^Xq7(AfCOmy$Z!~Gjk`^bRmJM_)^1N<|cDOC_JUy$%9W9!~M)FN3~>M^+fCFPTbs5 zXYK%L^`0lG67_SA9gZ9wt(Q!q^H&z_0pA1Cf0 6{U>DR<|+4PrZI zLiWP U#X$4PZuMzY;f&@~!#vg%}iA z4ByV9Ch;>}PEn>a%$*OlcZ#{XL8HZqr}N&re3TtvB#r SeLedN9QDtiX(1u~b!so6eM0<7?7qKi_=(IpFf0fPTDMa({Pd z+-n_t;d_H!ZjiQ@AKsUyU0}Tmc9Rmg$b#OxxR>^~IfHvU%x-@gFD{bgSyXUK#|~Md z&QR=xV6g~j$XUmU2r{$KSoEFlkIUu?rGp#z^Lu<}Oij|Rt`ZoEyFXME0N*;MZs+a; z_S@`De0(Ji*_}FVabJp-AiHggFKQ33g=-F3Gd_}0=_+n@#Kp~Me6~d-p){CYqVG9x zwKK|$HDT8x$)GbhT(J!9yS2%C?+bXcx7a>VBM9vh`SZ}{f8JDtFWvcO(C>DS>7$7r zM~ImXkUS?4J@AF8Cv5`p9W{phyeGR-_??}>yXacUO_C6GBuA-#gVggH2hllyiZ67v zwb~l{S7*N)VAuP6C0B-lw%t6YwutG|`yktS4)eG}QPGd1O}4_-V;9|3-+ZX@SiKcK zZ8)*0pt0}6zkS>LVqU~} wk5L~`H)$LD5r^~bzLL+Gw)st=Wxwt^(5bb#*kJXPa z_g8Bbchnjg+|0AAF-<)`GMq|R*F2|5Ep2Ud;qxB;Pj=O_!lzFq>7aY8y`+Rv2>#r! zd mI@BYi>F!cM zQ%>7UHQ5P-w%x|EaK03VeL3#@f2|DrBKW*g&=~Y9&x7E)dHspA1Hn=c*+2dcv?iG! z-x0oS`@7k8a^R=>2{X4bn525454WL)@PaB%%PbUT0%r#rs(U;6kKqG4ne$h`z-~>n zsqoV(cob;!tQoOf)wRkJe((Rl8*ti>mt3Nm-y0%3;^H?mZOhg>%JRP3LUMn}Z2M?k zDwspopFpl#0kodG?T(2nL20Khr%V|wERW^)9!d;$`~9_@XQbQL)0!0(spC(Nd`^8A zspru@@c<4wH1kQjU34)J?z$J56k|8Ak0YWJKF1m2NZaDr*`WZglOrgmXA$pXw?YHn zr#S32vtfbsLvJ7KR`(#|FDd^tJbrmKI8PGojk_w_wmQO~9Nhe@tkq_-ndj)mlCD@A zRfo(=AlyX|M=B6Ly#u`W#!}1D<$=W@BghV*>S)RDYyCfUd L1(Bt)MvmhE=&w z>pWX2Mk9kbkK%J>N`!Yv_mKeg(L0{rM1ksx +UKUaO2#`NB|X#qi2~W#9$~g8C>kt zZg*72!jGYo8qBthuATz=Kh V zjKz%_+PM-YUIGF6 8g8o=3{U48WOT;x6paPKc-DSz3?kucWb>mT?y1&JMW`2 zU&APXNX{#mg}>Qu&cIu=foMu*5#r)W7Q8fU{8_tEYP6o*PmAHwM_N?6*+t_l(%ZPc z)!E$A%aMrCQtXz!2_e&OI<(_ 1w%Vl+GYIw4@3>~g=WM3WA zR{4Q9-t|PnpRG^)n7-bZd8n0kD<+S#EPM2Ea?(3J+p{=2%XO;x 5OLi{1EQiR;nOH87 z&8&5~!3Jt%E{s-))+u}4_AqreEXdQCpGXeDhuH?_?ZdX9l>Ztb@0649Ky+R^@>55~ zd(g_WrS&op%^9p@M>;)u`DUUge)m3C1BH3F{=Em;I|RC~TH@$iEN%;;mu71|op+1> zU{5I-=l4U_)aQ0}wE4l1ggP!aKnvH$wedtg5d#b%NGRBgOnPEo4DB~u22XIJKIM1& zxYpN@5Bv319x4yGV-d08v_gGvow{3HZm#a_J%{yN41GF^)~i!>H!%>?;aM**7TAQg z%PM=zN}t4OlQV(`)L^bJ>kGc~leEmc2N4<_ZeP-vNJPUYKC$LhTR{VVR8FLl{KEoZ zN>42;ue`4KR2k_`315bOeYW1fgOy)5)8uwE?EDmYQsdK&_{h%j**mX$GV(*bOR(ii z-o&|zL?eRZ8G3jUF5&Fm-d<7N2Jc_2!`-tVEM=47RIL+JCsA|J(|UHdV(FTWPUS26 z4*Wd1hkKO!*wj4#*9i`G@S@@ajYsGW{y>nok!UMu6-k;1;w|X^k=p&gSTmH;jS*gU zcE)A14hpfUii#(`8d9I((MFi>{crY=1`FU|x_W<-9y+B&Uwe5XW81vFYuaYh@#G}i zETX~8kB~H->+|SfG&!dlogJ3HKsFWU@8f)fRq>NHf5kovA%0XyryNFD;p}z@s@t}r@JMMl#)!WjkSLJWkPO^0)7-W7%-^`j4{ayvX zD}O8{$(E$vS=rzB%TG4K%EPYNuruQG2ZqiSrtgSeTkZ8H!MPbmgcSdSt22e8z6r`| z{e=3&eO~X5itD|(UwyNDHlZ50+85y4(@?F^3qA7{&wza`E1gS z)egBN?)M){&HGzv#(pc;PRMAOaKL9v-mwd~*Aa7V19%Yw5c(P7{$9FNB#S#-p+&uE zBGO3YTXz1$Bu0?m>F#z3o^?94f%*KN+)`HNNf>9q@|DImhrjVb$XYGl9ryElhA^Tm zzkFc#AqUS|diqLmX4+=DCM|s>;jH7P8Tl{Z# e0t-bD3 ztuF02+2_D76?Fn_6yO|>M1Z>kDfMnQC%_^X_K9j)(bM+EJ3uDacp2A|$tQbFz}?YU zdkbg}b0ucib?7%<`uOC=f-$f#Mw>2AI_Kuirr^%SSzfV>{i5zaL85pb(TAPRP2kC$ z>Ut=XB&EIyYzM4s+s~s~5P}-2xALgkwm=P2qO2fRv5#* V{Ce-!x~%Kvt?7d7LccjX_rN+G{i_ac4Gc zQ&~tCO##=az`CKz8LiS#b!Dh %W zS9MhLoG$l1lb^H5+i?bb4Nx4umj{sc7sk(sb6(5lG7q_PY>#ql`z}m*BAKgBNT8R9 zXk&HN--{bUER(%2f48wJQj+5BQ5%XoP+_2$XE#9~C2{3d8VQ(4=I$3@f7hDpUK9e< zwEs_k$#>DN2w7+zRolDrBQW5WS(_B^qlkOAsxq6VFXDpD$KsvrlyG@kz0BTeB$N4o z!`aVtS7Ppo0V`MN8(bNV-X3n{=Wr3637<-(Y&Y JxLZ$M-Y<413u< zq?fm34-NZ($rx!7kE4&P)nf4+|5oWMr pmH;&xsb=6Fii$c_TQCt4P7)IC2(Bb&zhId`N@qitC6S+um+3{h;8@@7DNa~` zOmShdG)5V%vh39E2wOm%mer%A2x%nFXMr(O` zLKJs@7H6s@l$n`{BL~7jeYM^O6y&6u `v(Ppb($V0atx_CNsXX|1DlH$>Om6%Ss>a=jgEKG_-{5 zm7$DGgXc%|Cd^P@9_DUhkYB&toK@>W49nm7G0}|V*mGANK=nM#eI$q?FKVIK4ggo3 zig+dV`Zf$ELx7401?M5&x=oJT0-^bTeN5!UI;8r7V~O2$5%)5svnyjBt2~OZdJEhQ z tba%qneu2}pnvzOUFIr}Eu?`n%tA>a9?s2pO-@iweZi^ScN%x(1uQ(U*v2 zQdoIsz;54XI2FLn0TEcOXxco $lWIFcn}W1JA(`fL?~u5<9`b z02-M>{DYLIRtFFW5w;YLn)*p=)Ie}k%_XCHSkQaVtclkiAj!=DJmkoF(cm0NJPTk z8u= -P|`lkC>@EY62}P)9g{-}f8r_kp}&& W5g@+y^E=<-O-M#e5nKw; zv(`&Ug7QUp2W$`-__o=%73g>u6hF6mnP~%_Pp^Fooxp)_k9hy?06eUT8xkto<~Rl& z4gE(CT^VL-znyx@h(tT=%RLr!e|L?X*AT14wg~o!;+O!wSLmD_vwf9Xsn=IqB+s*l zD-jVOaQ%Zp+a{Pdfk?}ffLY{y%CNZ0E($VECXvFcrIAwmp{V}n>YLVdWPzsnXK=ji zWl^ }345W7AWY9{q0R+) zvNkz#U%AVNfG1oW5S< bslPbgfehxkc!M-&2bImA`L5+)F@3q(_(xknTaY;77AFV1_i`ZVqbp zYQCc1aqidpFgA*864TDyaWAllN<44*(Qe91?*nIJ@c0C$2>y1~@n1h?UfU&}XBr z1IVjC!&Cmn4A9=IzYjsjL)g9Sp<`p47x&4XWp(HVzyFU>KntSajwr 31*h@AgFNf2a#ZzkAZb5v_1`TA!5z zM|anDprfy4?<`EB~#v)>^MjWxY0KfRXe%@ZR&00UY3BL!H( zzr8>#zI6r8N`ApEb^_mJ-!lg{P|BqTGSa&4axVF>FW;v7+0RgFx!QctGXfMqiB@ie zG~y3}2X3 ksN*yb5eibX6k+9t9Aw}5Gl@y@C-j@Vf?Glb<^Cn~CVB<8 zg$nLGcEhnDmR5cQ&{AO|OEOZ>D-Ye*!i)qoH=2{oJiC^^3kJ^es eYzoTQB{5*v`8z&g zvipL$>jN+VF0kg0%@Iv|FKB?VukO$PTQo!>t1Aa5{}^pBOImW+Z z^pXUQ#WD9WXvy)j{kEsWC*mb4bJ15@IXmwO&rGk}iTy_l*5eYLrJ38n9;hEL`Ji^K zl#)OVARIpd#Np6S8QAV!K^BWzCe@YW++pmua(!;?Q~W6>MzYX9Qs3wo|MXf|XzCl~ zYSq!c2*d7B1WN2VQ09cB^f4ruC0 _^WP& z3O`%-xCq6m&Cdp?uHaO^XX>ic6Gda?gvf>1UgjnjY?i$#s=Qk!-s~tz#2TrS4}$S+ zDDuS8*miF%oY_$nCwWzJVVCT6fVoc*m6F7R5AZ3ZydW20e)oI@w50%|i2CK&V!r9y zChTTfU)LUzNnaocj^Tw$PteTy!vq~gR8S%INfQ2?CFpcScu$L<8*R01UBRB?tST`O zY_f(H`-{W`(+WLWPGB@o(esm$3yTqm0DUM>B|b(2%-O>T9X4$=3k24Ga5bKD=??zG z3Sr>6RxrCGh$PSDkpBw~JU6C<&cCa|hw`4I#{YeeT4JXjhS _%d z7k8S>FhRDYJgHN&nq8VCvuB&6eD3VG=#`W(Y*n~ g! z=N$U#P4UMAu=-nWlYSD+zVBi}+FKi4VJwA)u&1SNlhfKI=^G*hFmL2BS;82l2H_F> zYpF%sDrofVsev$lx$47 sX0eyA_W{% zvk-h+liW71gMGL4uS3+`@7fA$J<*5*VlZ9b8Z(74dtu%)nSm!ovybGIdxeSNgBP2P zLhWQ;lDaU>Y1_R{e9JFXl1NAbO~F=2Fa@#%Cg|0-vR?q3Y=9BkF~`R~@DfEKa&`n} z%e(BR{N_`d(%eZd0~JNTOK2w0o+$4jCo}hubrI$Z>$*LFpYo?0CJ8?Qk ZFO9>gr|Pkc#s zyFB%#l*jJkcXnTZSvXinm@ zD&_Y~#p;-&djM>) @*4{4BxtbOdD4Gly0(4iM xI{ zD$hlDNqqB>slu`{Q83-G zmHz|DCE!}mse>>Kzd?|#)r;QYVPK~XTFk~@A;-$~>G$}t-9c)5#&)K+h|7J2RejC> ztBV!B>(#mDmh8d7 Xn2 p<5n3rz6rWFPp4xt#{9i`yIO)=!=Qy!dl6Uv3d2NkZj z3Q0G3EErqC_4q*)&!#wqXr9Qo?;$Gi(Z1zh0s^`E>$Wnt{p46Q&La2-FjJGCra@f( zfq|v6r5n>(cgjw^5qnG=7>E
6{)+`|N)i zUO}K@ec`7=z=Tql#+4XMg7C4I5yy!{i=A5>mcL>#Z#e7jUlc$ugunPs?|w9JM|vLc zGSsyvl)P@YG?`uOGzo01hxnAqUl`N>kjycy57h4v=W*$*96#QCP1^h~Pg)`9)>n$a zn?~m@5MF(T`k9~eHe4K^8h99Z)ywemMJI|?d3br|jnEK^Bg`uzqMY^Z1k6KG2=tJv zC?w)3Qj9+-QiNp8XEY8dj#b;{H1V0ONNc|dJSCP$A&$JAr+it;FKY;BJoYu~`qAUv zs}kdJ#C6~j8sqk eKD zE_95tcL?v4%>p7Xj}DsZ6+czV(b}~3pl829-N5|hJx E#MFx1Z}Lf$4gr zY87OAU7<>G{;76VKK;4=DCGjWdrH@jr$4H8F*LE1E4*KjZ#StD=UBsj*S;CRmrFp| zGZ^_bE02}x>%vA$$sJ|HpiXLLjZQVcGS4*BH;1o!Ja?^lP}=2D<876Bs+_?BE{3>L ztPn!8&pXP*Z ~>S@pt282IO4bJ+T&<(cUJ1J3gbJOBUy delta 25574 zcmaI7Wk6Kj`vp35NJ^K0AdM&;!T?Hl34)XeNOwCNQb8o8rMo+&Q9wZHlo%B0jv)qy zxd-3h|K4x+!-2zL*yq`MKe3**7vopBBS_phcHn`(zLDoMYY!`TJ6BIT7iS2>H{)me zPiICkqM*^`;#7{=${o{yrN1jr@3h&G4D#Y 7Z z1;PxRD!D6nIo-5h9v*pkpte?DMP SugtumTbG@R4~`|0I2^^3cG#v#sRNkDw$ zhoz@~Un)L)hMhXOFYZ-qzPjyM|B~Nu81W~}Z6jYg@7r#Mzy*Z?-9~^?r_Td4V $%riE&k8nfk*l<>o%~2U+v^DXlpRrOd!MYWM!eCRs;Ci3362b&(`j=2PlclaO7{ zscP}((kM+E_FdHk;%4E;@JQPH*yhN#A$|##%F*uM&2k=iq;smfQgHKlgu7|mJ!)U0 zO8DSe(0errq|wUnmn2?~FJDIS*=kjvX(8tqNV~tXn}^>sK2v6A&U{LrWXoY<{l*#y zrPs*(`jYZ2A@*2Als>8;Z;tGY(3GC=KwIW}T+#gj{Pz}A)iPdKJQDFp1!kuHNP8C> zlr!39L{n@SeFP6(%KyXjV1f6z=I%rD`1g%>5eQ$SQho3?Ngwgy$zr=dsepOM`>x@8 zH?EZ{WV?u;%(Gt{%kR%a@TgRTlzRa)J5x1DY+fMLhr}cPv1qqkzw-N9)?JZb`l(NA z-0$Riq(*&=#~=~xm;6X%Sv_9V%NUY0d@^GBP*4$9{5$Q=Bf+o^#m|Hu_|Nx?J#U8) z1cxwsAOC+t9Q(Z3ctk_ThQ;{$qZch*uUWoExCFMza9Yy3>BgfJAWiUf&{uW3z+fV} z-oU2JOF@#JnY))B3!-}Oh+I>-zY4^$KAjtF5;E^+oip}2T5S56$cn{iGjw?8^>?zf z`n?}1cDW3So4AuYWw6S7?OT|_v<3%%8B|o>uO}p`E=Uz-n-Z$liARJkCUMDzs(f0^ z<(3T{Kb0UHv&eIg@&-tBoP}{|T~H~ICtUuutPd2JsC0EHS-~4zQf}@EbLG-xse3v} zkB%bB+CL~IE*cPX&!6)QiPI`EKenALW^c~EH`wrDzjxln=ZH4@jR9?=-%S*n1$;6hey|T@ zR2OJS`5mnmq+-XxktLUu$0p2F+4$!fd1mL}9nQ9NaS1hQW}tQYXUEud|gHpSXLu z`uG~Nd8Trje>~|}lv`fy;eGhiC*7WHA^(*6vg)CG&34pU-(x=_t9a(Ytb@C3mQ@AJ z^Ja$ MUibFzET3+7GQBLdd$~{7eqW0qJ~4iCy>slYLSb@{Le^9N z>J)RFBbmQBBZ0W|jpi>_tii{a->t5d%sdF43U{S^El80-x+$TPpqK!8nI}m$4f61A zZr27u($slaGex}L`Y7fITp+MPnolI!BY1@^b-^DOn(E<_Rr2eJfk71E=;(0vcB%DQ ztG|63P?cqD{m4+O&|UmEMNlbB>a0;4I!jufcE64L>^nc#pKoFlKk~6&`&}>Us?Hc? zE9oZwG=EeiKEpX#m@4A8!2m7#qbH25x67k-?A|qKTfDPcpG~1^=UA0K{m04U CH$E02^oH8UFmHZ(#m*~3 6=don+>cDTtRMU;r_m;Kv6?L5* z@O4y|VlGCR+&bf(0Pk|E-r5?w5M{Ge50>3|JaG<2{DlI7FEb%1?hWiaq8enZVFjZ( z!Sf=d*Ji}!J#`Deehs00#5@;- (cl;5CS|O`1=JE6u z!zRAj1x{%l76aOJ 3Ond-s zV%c sJVm8H(w5P{{3Pq86#E%j&@-A5kyUILX zlq#mYBLIJR(Nn#hS!(Ya2(5on`pz(xBVzqM8RmsyhY TT2KWxo|f eXwi23z8w;PMqgCs0E=#A_zt<1mW3L*tkgDJ75T=u#$-amCH^l% C%|iG*)S4})JQm#dgmAK<0U6?~@cZp6vL*i)n`A?fpIK%p7?v1H0I$(z z>~#$(8Ai}P(?kH-j|m#fT&VG<26)Vf6M0^!bMVNaW}Vrc*IUdM;Pd ?vod_| U&S$mtA(BN#DVXVcB~{yq~a={W9@@ zIXvKo@r)BK2H%iP54c%$LlwUSDta=_zK@zE2avo&4W9p%35}-S9dx9nklUA-b3;>b zQP+--hp4t0g-LZxp8CKumj@3z`Iu +o iF^}%vU-Kegok5AM#u#>Y+95;3G zjFBeN&hmxqP}s!v5ZeoSItydUc+P54jAQ@B*FnN+X7A szAuzz4_0MGJ z|7URSL*#$+m;&xb )l!z0`!8|d|F$`vKZExZUTd|XuL4_YiG7cp zN*S{y$No3iSK)h8EkpAcckF5R=W0Fd>B)0gU`xJsI@w5F J)NJoh_1Ooa* zbgZAHh|!rHo8n6CQk&&LbMy{{yt1^Q^-v;!fYexieW%Y5I(_Ozapqg11=B !d!fVcy*APIyKBi9 ^gme@bebe~F@q58zePviJ %qg-cLY%jLeT2J9 zZ8S9C=;+hWECysf*j KvJJ6zVkZrRc%WSS^y%uO}j|^mf~vMMOG6}b--nN?)=t7C&TL8f-3H% z+0+?)?O)S5>V|EAdrsrmmk ThZ$BE`jVX`9MG6f>vJg+r+30U|;6ETv-&-(lOmH3B4PH-Um zc6M}vmAEC9mHds&k)ZcT+f5V*i!u@ZhaJA160mk-p>SBh`t-qNG)*tiWqWySX69Y- zs5qAS?sjhkIor_f>y32zKBR}){#wuzl7rT;_@fOA;RLMyI>fG2!8%%(Uxj;@h82Mq zvz5$Ko1#wdGOpr_SateG6MrA(6&CurSy`>-^B_ya4>IPXrg{0D_PAZ0oSZhDH)o9R zrrZ`c$kX>E{4_zSQns3X0Tgd5%I|1pyD7FpzL}y9piTBv1p)F)Q{%x8c%QF;JI(fB zyn`z(=V3r(f5~gR#c~yKpFYCWa==GChzE5uU-j^4G<6Dk(B6CyK|m4H`sSq?Z`Rx6 zQuV?F1&z@!=$@ptsRtsJwY5j%4QB$};qsQXZLd=G?X$1iTh9lb#ul&E&1Rh*EtXuG z4IC^M=lIDD^Mo#F*{`CWu_oQemt>O@hK7-a^B`9$Ld1MVGLBChF0BIsT7l!tdctKe zK%zGdSBq{i+#lSRE2Dikq8vwmPRONzR<{DPhY!+Ys_Sz+XrO@AK#cUO-Y~0&>BqSg z1wPhfL-=7DN$!eP89TmIFY(I{rnzNyzm$D~G44IO`rBwe@;&h(>XD8>^(aqFQ-G8l zy-xDp*4EEwwM5_!x3w`?E-fuVPE%QmFF{B{A?oxi@hvCYiZFD o&O zrdasog*aTzWB&uy9Ef~U36MqBzuBWXlnXn+CDxG<{Z!J>e9+WvHFrng@J375R=(@< za*RN+Uxq0xrS%QQQ{a3dxdYzhiPm=f)j$9SpBhhPep_4HTgSV|usGdNT3SYw&cUSf zp$T#Fo(S%QsclAZte01$PXhWQad}JC8*Yl%_KsZP;26nsHHiess-#`-))H+l24*iu z$ul9o*uCZGQE#tYa7yJ@-bbJfYl(WAW{=jMa;|Jz#Q;>}K4R;rh+hIv5!|K8Avi>; zpD@W5$*P;AySy@SD?o> O#xgaZv;937MCe(dm?s=8Uo$PNe(WYeK7BR+ 9Qgb0^6J2=oeoo1J3vPyYMDreV;T1h{?=MhcC7HtU`Y+JO-?$ z%Z>v~$zuyjJlNwg!QKM!Ea+CDldxdHi6NtKTxi-OwhqYxL@5;NaDNAGSGB!%kHZ+$ zwd$lkL{_j9X9bw6>tlh4wfNG`&d!XeKpWR2nzDMCbN;!T>3DC0?sC^NG;`*FJibbQ zo;=1K!;0>NG@)A8b@i9O{ax;_0Xt1a9G>1@mVSB^W}Y3;pG~#&ZMyLHEHYMKk-?3< zX8K&+r;t|JEt$)oxWhM&hBRO9ZqtB?n74>(H=Or*1k>;aJ>OLWjoAt+o7Qrn$;m}n zSojJ43gZ^+=<93lH=O(5wYAeWOLiBU4^Cgz(zZ#Oj=eRIms-wx@b&UV2XMW;bGJ=f zE?3*A9jy7K`AV_IT~-5{Y)LXm4Q++LN wVC&(XO)N--rh)X62lXtoF8#|cCThY?4l6<_J>cy`k!S=b-&`5y zpH@u8TJTR$wN($V>pGU^Q0QSWfr5Zx^&I^|gKbZGfC(>0{}|PuID+Vjeroo1Ey<8s z-D*KHLXtX86D#iBr7l*k sRBZYcAxzpZcTj{wfoLG zK2rD}+PD#xE6t }R6x%}0Y$pGBPnjd8|**3g(iEQ$#8>b2&m z Qh+04rTBt__vToV9|?{fzO@k>-PX4<{1Efs!8u#g zJUyj6uG7es5=H-nz~_P4pUjzaSg |P&?0G35~H^M5FZpj2I9pZ5@Zd zB9G1Xz3&VL9}NgylHMVh7djYxDvZO?Z(PK5agM-UTWs^>E+U`>awb6>b}x|!nl-b; zzYN#nG>aY=r}}=qp6xTT+gC<9^p2Q;N4E>fuyRL#^<|-tU#h_9l+;F@<%~#@KUGRY z{~p0M))W}?o3|hntNXE1G-)Grx*zR^u9ku4$jG8_t>xm1;BU{LbXi~Jn-z 4(vR zGSO5ppY=mMy?$>AjeLJLc;(;gq?(sn{Rqnqm+nyl|DSZnYQMnYG8rTFU^Wos+fxow ziYJ}fNaARgb$#++c`YA;uS&o^AB0;n5(l;*fUG?h?#zxG2;p9KuF#%u-Ra9ay83I? z=3_{N{iNLPH4hS6qx#iS98X>}5^Nqvg5xHI0|dH|U)~J9GX15AMP~ikj oyO;fCR5B-3Y@I_5SsOpVDWH1Lpul+Y zCEV{0{?bfrtYOa-mKeIU`GB)?44#gzi7{W !}cFx^Bh9kJ_{p!nb2Q zwlbc8O9=E^YEgguHs@9uke;rnWWvpMMZ&&-X6;_#A@cXBzFT;^T!=semgAyq0(HEQ zSLs#J3}TqJ4$Cy)L~}1Dt^tAh=4E%@{6lsju9Ht;dkX<1Gj&1+v_eH|(iD=GuIcYP zJDGX|D6+Y(mZya2H8k>r-R4$b;5_^(=xLce+}25{GcleJ$G#T>wsfGa5af^1!{1-s zE`D2;r+-3Sof}@m$HvHvH-3sWGquA4Mtfce%E=DOK8p- TgWl0;S-q5E4rcsajTmSIm UtZnyGh|W z)lhoIj&NyRqqYbvtq#{`S4N(s%y2&NOl9s1#=CdAyuZ|jN5^Oi7R8lrEK+~cpm=3H zeG;kNIJwCz-#@d!#doC}$FbAB1&v)D-}bX5(v42TXF(a!Xsa0dYRhI^ZMHT^jiAr% zJGKPPZaU~Tjp#{6iIk&X??^bbvOX$^3Dhmcv*_X6O(gke)fVrn+g4%9^N~ld@fZ6C zq?aOY_}!5c* u2!rqkk3@RR4(0J}OPk3L(&4|LXLX(jIwzd7}4N&Jy9$S6RBL2HZ>_ z3nW$Ypt7QDvMOqxN~KfOlnR6Xl-~gdw5H6Z*3pz+7s2AW`usT9sCGLb!H)Sf!#RY- zX%MB#a3`5+=6bHiDT%&V_m5|cfd1aVz~H=XwyEYPH3RtZ&31 gLEn)wVeo*of+{a@;8uk9g@SKgOx0>N zRl9oBhkbGLz+qe`PM+;86AurMO|SINpBtsaPk`z3NC<}PNlWe44xYu|3ox=6H uu9tI z@{Ck&*6~a4mJIj(Z&6YQ?G%tlKKqN>lcm)ujSjPzv@fxHT>Pb13@?91aVbW|6?_ws zGJE|P6PxIxNxk36`tgNG;3;BHR&R3Ch(lRHHDk*~LxXMTBpZxx@!w&G@!Fq!6@rjR zZ^LK^F-A~U)^goA*j7Y!l;yktk_-9Q9B#J`D?)@MMo?iNcA}=^v1;4xN#+e#eyjg< zpc+Hab|gceSf|$L+pk6kX+*JBc1-USX>k$pcQMEVM3Q1$VER=Y4y? biQ=2-WGVe2`ya8jK6xN>Ct%NW2 zCh>(uqPuyk9ec)=n*SsjHtGK`oaE5Vz|VOdmUow ^xB09Dsh@nk>E&J)IPVOn=mxTAP^AvSPOXeU oB6jnhq@}ER=|6{TFIqQwbXv{WgA>F><2#f5O;$O0IWG z^@{V1_@1W{DdcKHf{sr?GxLr3UTsg6MxNAUUcyz)?UE2zY%(L1seh1gyDhxGBTyga zu6m wE8K02|X}hlMMxKa^6Iobi@S8TFzA3v`p}* z=D23zEww-a1RWmi#@>v6Hd_UyldqIS9J4Sdo}$As=#mC*tFhTQt6*xy83rcqeOZ@| zMj8rE&2J_yd&C*kc+*PjY|NMS$1PB+fhGV`*3Xz9fdMYP?X-;See0S~-7#do?BVv+ z>fakY!-x2Ksj~bj4H3bDZhpKGaX3glA>NPgdo$fzy!U?9aZf~=z)OFt2bi>ZJK5X0 znhh}9rWsHBh`N5_dy4usoh<|NxA^cP9r>cy?kziVNg074K<;Xsb_o_9eEijZ@dt?V zJ?z0dx;`1TnyoVRU5yqS8ZKG8PIhyNx)6Hx4WF%A91JZ*bGY-{!=8Z6odwevY^UXR zy07m^D^qF22v5%z4xYD{{Zx}wj3=3rX*sXf$8l!C{Oj{lM1ri$5PpvnWz$y=jVa}d zQ|XU!h&~D%=xwTWnfewz-sDta-UW32!7I|{(ti@{F~Un7RhpX9>ao#V{;XXO`cp ~$G`fnAstj0){bym|jcdR`h<_-BPYHmhe(}3^aov{9uh8uOe z_+7N9I{{NAI#`q5`xTEwER)B7&hqQOq>+pwSB^)>$U=s6?je?4vi4 #~{lcitn}4R-+&pVv zG>AdW`CD%}b$=&!8-i*iP-$q>S0Qj>!mz@$O>}G%JbRdmtp>np81vNSAFB17qz4W# z`LL+0!dt7%hdu*_M>rc-1?z0elP;ZAr{d=u=3JCR0y)PgKBHK;GU(gsvLi59sxEa( zJeygpC~W9V#eZsbXy1Va>6>SR)dyEID%8-N&blrHIsO`>(R>uz<-K__TFqqtqWiPU zS$Caw7PUBucBPI}du9W2>!NTrZS2hI;_x>Wy6bcTi0T~H% s&`P+A zdjos^cVIB7T3A5r#^b5i*x#G> e=<50v2G@&NVt1u6R57N Boq1)*|DJfCR7i>hE4*K($kCsnS&(_$i8>%lFvNeI1@VSQ1t%G>y9%NM9tTF2b z_cavr@5f{H4Ds4OYag+ fVUFpUP|>r zyr-L*<%qDy$}Mwxzcss>Vd&IavOeGfy MK>Je;KhGe>E0kiO`mP7QVyMk}WSe+5_fR-oS`SU*sNw za;h2{$BoLdo}4 vW+6}cW=f3*^+%HrL=oCttsX( z=^tgei0h9!uj8>x@U^Gj0gd?D$wt1@@($xaqjqbp23{?;xUIcQ7DCf=nVOh1JNK0t zls7y _{-^SQ;3RBA`~vL4Jb zr`=JXLSMjOg(qJ&l(*;ddGHQ%Xt8}*pY#&%whq7K!~`(W?mG~ZsrQwU@o1pQ=_Bo9 z%qAzRjS;co$$=Orex-m6fC>~!RM2D8S-jvO{Ch3+h8bC0$V9NczyA(NUgy-dNT#bm z*5tfo+#Jnpv2tNxrN;#1YIA+g7LJAc-Jo=#$9V?`VR0>_GgeknXFbODZqWW~ E3JIaQ GSN-Zu`4}CBU_7Q6;CKAGq!R*C; 6VcAh^u z{?0bn>S61Lz2A^0TXs~Lg*lSxp<+MIT--@-us94d9Jchg8=0vM1&||@-y0CY!MG)c z+l7Kqj(b5DX*nj&tq%~jZ!&bKH)QmMY$PMCz9; fzM}Q7& zV=F3;jdfwxZ{GykhgK*A=|^sqDE4 MfN;?1J!K86!NKDAx zhgjGUO^P^N9@Nj_pualihS-~toHxbp$hjH0VaWjqW(b?C-Q8ITMt3Nw^qc0!#vZ${ zDL9DzX~C1b(r1C~+R%_9hU~DvOm Zg3 zaN@&ZhG2&lq)Yjo35CJKCc7phdwUQ;nn=HCmj *hhkA8M>mYS1(_xgE+V5b(43=X;o11LKURwySF2#>3naYRVs1fT44X_4(m8O^Q~ z7L`1Df*VZ=3g~*28HD8jj4{VcfH`Xquej9e1$F;NyCdNj31I8izy9-cp%0s=&RIvP zo#$o8^xRwvA#FrZYOg7#R5fx?ZzZN?;G yV0q z_)_5(E)Z{j|JI2$c-G|@6hWjyOs2h!M`_yuGKRh}J63n;|M6_fk3bgi9>G!tEj|nK ze-xtmB82I@uT3ju3jJ#yVB?N;Gf`LzChkRcjWpkEUA;zZNcQU{tVD|~lTDi=h7HDv zTEc-9JY3N?o1g%SW+b5M pw6E^FWr@H>o(8F&d}?~EV>~i3`A_V^zM(H9 zq0_9lqsVnfJUa2}#4UTHfep|yLq0}4Deo?Pfh!rk_n$^aS8f8=F9Y%E&6nBe@@gxf z{fEX_ohaX2jjm93Ao6i+wPTPv5w;{KRY8md%T7-GC)*PVtJ;j6_5Lav7n;Z9CXz2Y zHhruL-3@F>@o;A28A1HnwrywDRlxpk7Z@X9-~K!othgPZ)=3$Z{e3I;*oV&xX-QE* zYSwE9YOhCt=H4IqGPelNcs5o+pQ0dQGRUx>E#^|WuIjVDDi+Zsn)i>Je^K|ADAfM* z5H`phN4~eW_#x@R0kN1#6FU0!!*);WWgt%Ab;pB?fi?50mzED5Go#72^|FykNc4hx zEb8X!09eiRS?+;$rgp80m1akLRAPq|P~b#)EKc+6hntFh^`*}N6(X(Om+0G;cUNRd zVpIa|ke+v|@c=#Y`{FffO;qz~>rrm%Y3?fT4bPOjP!~`Jn{FTZ234H`6C*DIy1a}F znVX+|A4)2T&n)46Kg-vx{xOUNx{-|!koOZ!M{K#h0?|p8F4cSyn9Dnj_#R&D9R!+N z>S|seX7fQH=bj=5w_=jcTe$(Ey+|U(#nJJtzMVTGi;iCm);G7HX9x)r?FYP33Rf|} zm5kZ^As=L=loQ?7-toDsYcb1fUA-dkH8ErbUe!v!8k?c*?8(=D!)tU3AOIZ5%dta; z43Rd{W(15vwv@FF881>YgP#koKC=dTEoCOZxS3`N@&)4ubd!az?eEuCi9jc$9Rls9 zw|9@p`)zK{X *LAYFS zq_C{8?1!YjJYtAph7pVvlGEsg8Kleh`W9rx-u&bo8on8ZE@MMni}J)@puW>r`0ctu zx}i72vZ_PXH#-Bjh-?&AaBH3^`ZM|<1(z;|p$q2o^B#Jjn5qW5dyf-cm_ika!3Nyy zpGQp(^EUq6LEA5UsIvl9q+&KSpn|iJ<`C&1@NbEppv1ahjn{t-PzFrV=o=_=S?c*Y z`ck{^?M6HN`fS<)<%iQQv;XgUg21?SNqc1A)Sn~Zt#R)4O NoMrL&~r(RM0z~s4s+P>10Na}07jRF`9Y4)V0wb~}Oi$T@|ZN7?> z9MT%dHC{Ed#aeX8V4i#yIVKLJsNVLqEB>k=4JD6pio-Ytwm}$qOX6I7BSlBrGP7(5 zR$waO4^n;s-U=HIr}_OVHHrZQx(v5CsvUi)9qt7%>XBfH{tzeKP qaq%)aIz@ R61G n zWeWxL8sNN>Az)kfSy0Ms%XNPZmHZ%#OptZ~VeGwd7g9p2wmJYe33-R58XX6Jf@G;; zKpFLkf?sI^Pc}n+t-sd45zlF8&oh=VZYw{wHU`dGhG-C_* y$W2`-CC=rDPv@udRk1?z6koc$7OKA?$;9P)(K%iP_1v#24ot9v$>ag$CT-#m zM@pLA<9eFiPB8d4C$+$^*kzOLH`iC6&@|2bm*f?rH|C7eYuF#5?(%*9w;2PmZ4UD) zwd|^|CWRcfj=~-q4pbX-DY6kL;(Iu&XFyGDhOiCJqHYD2g53vgfzUU LCk|slU39G@D8fq)X4))vY9aNloxb?pBDNpp#*c z7&P%|w(a2NUQCud=FF2b@hrdV*^6&$h874mheH9hFU50c9d35ZHR1I|fZA=6b~{a^ za6Mmw^FkvFK^LtyWTpkr(6k1!KXh(8Bup!M{rUz(*QG{`Oub@e$@X|uLdf{&(MoCW zeC)<8vF1whaa(M(L;4Koepnn&OQ2#*mXP*4YDkvR34F%Tw)c5{b++ercGBkdn^?3jVd`RX3o$0iZNL02%Mx4hsEO4U5xve*Z@xTaOPn#1uR<|U zV(41ebl}Q&fVuY06nf-*-*05^QiC&$?C!@a0zPf~unf>F%ijod3~f~r3I8Di-|azk z6S!>h5+c^% t}z&LgvHYm=Lfq`X;el^TMPyhhGxn +^8j84d3e>+zIdf^I!Wh06W^B z#3B{6?({x`D-T&MKG*TH2(7uPKB)wdGA~fA9_ta?T}S;4dC2m>*0OaZBpP}w9 ta5s$;Ik%n z?p;paB%s?Zm_?pr&+QU;Qmz49PS1vlg8yN_9u)iIS}kD0ocM_;4Aq$OWMEEva#-t? zt&A;a)wgHhxpOu$1WC2|F#}}kaWp)X(#sUG7EjpoWM9PGY|1cWj7w?(Sf|J3)vLD> z)k(tzcF+;BCqBDGs>Il6VRSp8UMmerZEW0&U0XmSqb^-iEM)DU{NUw^(7z VEuB!EvhK {g46^&m7MnLrl?Ow5V?Lt&*l zb+jE33>qKY83W1hF-;%Xd;Q1~A7Flr+(^SL&J+Fz1;Yw76G;x;W~1dN8X8N6@zRnW zz03J^@ucxpASL(b99BpTs2g^F-LD^}Shz?o58PtSKWdGNPL(429fwlyN>*_SMYQcQ zbz&gnasLUa$rrV*=Y5$oNN8+c=Cy>g)F~%^@FyQMZn74h*bhrGfGtshkaaxnCcbLA zec!mqxOKtb-V^-)m`uGkT;(qqEQQS8uKe;7{fPwl=F&&%)mEGAsB0fX1a z4EXWjhYcL(M1!)^C+aOswUKIA`cg>A?a5CT>Pg<}dQWNOh<|W#u&6v0QMZB@uzv5w z!nebs)X)9iK%OYGVfGpopn=ay*Ygg>{K!I8eOn}zY)Z2+-j6`#15VrZM60yI41 {_o-H)Zqd(cr3sEBi5u`lvw*-ymOFsWe-J>+`ac) zYa;oyp~hbqhKxe#msDm;sd^Jv)rIc=w$7OrZYG$3btv#F*#Gedv|d%@+iw~}SCAK* z-{Wf>jX)*ii+ww@;T!wehi^UqD#Q1#!8`Hjckm`kb0&$q%x+ud;IEfk9B4YXbE{`s z#|sad`Q`EB^UjFkN;x}ULnxx7Yc50%C*EANHZz|BiEH+ytYi<;S7}5(sHJWTa6gZT zY?r36`=qKQQUsuQZVISS$3pc*c{)Wp1LSA*h;6pN2q@?lI`+em@JsUFq#9quN2~3u z*io17ou+IDA8|ae|N5)Ewn`6Px!Mf<@>6+k)b!l;-s`{&@|*_sIK!vQm1~PMOGvY; z)n=_156U7zNq&N49WK`})cD2yZ9mnkcAJi#cuFSXB*0$+`eicc4;iHwew*Xnx-%4; z&0*BK{#QF+Uw+UlIOy&ld-BBCOfjDN`~FJ)JD|6*ksi)`?0 rU*GkRxi z%I|P8R?-~LjGTExSPvUUf>2KN*cF4XbZi>7d1FxT#)2HDEpSh7ajqI|zzUPcCs2EO z8EK~t_-iF{-b^?B{$Wb0mccw{jMkZ%znBV6%#C1hQmscvQc8AaH)?8Q;1ZZlUSa5q z_CNR$Ysb#!s)H11y;3E3%MqwRrvBUT77^6-#~rujFf?7M d|$02h4L0vj$m79TY823igl+J3l@JlkILx0Y0UCW1a(*@W$( %g!ND-|`z)6wm| zyrND|p0Q~=7+N;k5nY=3n8n`_If59iS(HKB(hQiKUt5baXGWfy7=PZ&BFK(v-dBUG zyuBWdYFhG}o$=i<6C}yR;XOJs_j7I^MK|Fk05{%n_pA5?DXYqWb-;SqQse>px_UR# z@0y=J2EDv?_BHb;23@f>Y-yUfdr?@*7fOs>>>chDbitopkgogk!b<<@TPw-L8|>1( zFXN@3`ek;U6LA!RdY^r@M@enT+z7{Vs~Z==8}IpaP5F;>y?DG9I6#ovI*XTvuE@xz z&pQ}0yS_+YtDQk%{{ci6GcwTiQAhPBDSc)%8=R8|X@?Sg=uwAPl?#`CkQUMSzztKA z+Uo?j+Sxvh$iHrDtsO1v{l3}Iv9!a@U?pdeI!@`E1pc0Bt{i;YGM}G}a!4L{ax?OG z0PSkaIr5?O_M1H_Mob(>6BU77cpcW)8g1-yn)>X#wQLRWVBSEg?4)~$VV8>#)q5^G zbhuHk5wtt^KEW=V;VemZO=m^`l-m*$%nwkLo;vU7H?A3EB2Qc{UOOWOA%=mu6yW7C zcGbR#)$R3|O$d?a64GO^vZzIjDry55GfN-uzsn8yGWhb0Pr8GBTqPl-^aL6C>TS?3 z4VYC|8(`7CA28BYRFWK%npgvK!XNMZPQBvWGpuIJEW5E${S>@-JF-P<1TuY`__eBS zVSP?w8hzoM#O-3yi8o%E+26b}A5c`?e>xQLa&|uL3wQjCn8qMv{$T(nk?`?iJd3Sy z*esgRXFDuKk?hOKtcqvd7_H_prAYJ*UZZk4pq01t`&$32Q((uB*b_6`AxQf6-vgN& zdtXzRM#m5N`)!88HOaAX$~OO?4Q#DX4Kk1)*NjDh{wxow93^Mo;-%76S8Ho*ou7QK zLtG3B6`mUg`gJ@uwL|G0_T<>j@|nl%NJ_c={75HWID*hvHN_bjzIfKd%R}iSr9y9{ z0?gv!Dh*uIGg+pTQ>mXh5NvP6QFb|n%@?z0Th7P?Fu}tg?d(YIS2%H&``i7|48Ie# zFQa3o_=ER2IT<6`pKpKd qfbpw7lsC&BRtHw^OVa&{x7F|j)K-`Y@UbBAU^`}KY z|Iq}J(DqB3Kd_*o7hRaEvSmJdsgfaL1>kD-;Q6-H*I(;Cxjvl)RXvCYw0|U~VP8o; zk?sf5ZznyZNh*N8Mr1@~ao7p}$rJt1d)kKY|7E+=CjZ{2fJ_$tE?D#=*;M94fr+`{ zv+Cry8=?owNHU_5Ykp;alF%QqQfxgPOiW=Y_0UsKhOd&jjxtTcDIARODA0-6fk1DB zbfG-f=zRCl^W9fCFJ|$O!~@0EJ;Jmf#4=A>To~1?y?ht5^&?3rV9Jg7 z;Yyfphq?_-tt^a(Pf8Qri*9)Ifeim?L+}*CAsLR9Gj(7i8nlxY2z<#1JYx@<=SKA1 zI&Mp_HcGkNGU+*XURK0JHV>alw(GWPJRt>&Aaqu5Rh(TTSmi^?;PKV GqLccYVLi}|EG>G0f(}U`hSKjWi2H_V|_`9vX(L=>sX3P%2tw)rIJc! zo}ofzCzC8gwv=ooie^NS6ca+oC}GAn#S9r{=KnmSdf)%`eV>bKJoB7;JNG%ibMEDw zl$mSaS*{=7iqwSz0IH6abG}>6ku^$fyGI{pwV;FX+;RP**w)%ArE`{NCTu{idIwO0 z3AkcmGa=%*&*JH5vJVaJ%cWUWaiGl8jRQtMvSaGE;k~UCCNott5Vp5-1#@19RmILk zEcg$c%Q2@9IrQX?Hl7jCm%k`~JZbfse(aGlyhcfZEA8qqe!GmJfKv-m^^0X6J#!{D z(`IC*8rvPBg$!Ls)_8BK ziH?_E+44T$T}- hS;AQ1J0*{rP(R1Ci60A9d3O#ig8PGE-AlC-->HDcEHj1vP}em)~PFk(gt4W06{q z_Exn#22H$4)`mWulj_*(u1-T$Czd0hQ9!e(+d`<#@cM`Y$x&gD{$K7tj8mK+E|KlV z7Z`{pDZfv&?x$ISMLqiQgoEUB%b()x2aD^m10qj?`l8*E${M$BE|Tl|D7EmjWfx_4 ze(gcLtdtI&i>&$@g1;X$t)QcX z3zJfQTrRA+Erjyq_*T4)(mIvL<+miVu6&-oWn!qFBN)LRiv*$Y@+1eYlpn@t_aKY_ zcPk@yo#rn|&r9#Y=ROmaY$!gvMnUjRK;}%Sfx-ZzKTIaHxZ%yKt=VYhJ6kn(JHP$5 ze=bz2C+?o x||tr9NN z`O&sv9eKY)USMjE(8GnwgMR)Fx=x0y&cmMym^Xx71hxlU-s@MYJ~y(Us&5y5FpXK| zkTxgD%zE=#!Z%+VJzjqV<%y~%4xErn#@=a|n3lJZdb7T;Q0{d2y``@g?K`Jl?~_e5 zqjz|Yxnic5$MvituQRvWqRSV>cmLeAjdaS(ZgXzu8%kLQG#Us&mWlx@D9hp}ZZ=kk2lui*Y9RX4DiZ8o${V;O#80k_G zr_Z%9>EWJ``01T e)y7+pm-Ui*gyi`X=B->0fpQC> zOpk@!f)tj6^H)3b%&$-4f(n>gM>qCW&@XICeR}ZpBZJKM*sIxN5^>Pt(D0$6LOj_f z%e$m_q+{nybXh`la2EaUO}bAX!^+*Kx9OWtv(0YskcaQ|X7noAyvWhO(8>P7@Z7@c z&ogyZhCMIrYxMJd(ZTlvxV}e&TJ(3Kj6vCUD_> 9O~1=}rE&oJG&iQggqK>&bIpwoS)) zC(ytPJ+q?~+ik;^JG+bigf1t^q&W|t?ZYpS_R2MQ^8Y$}uTi>`zKdIPqE1)S@%$9} z_!?H;?M?H9%qNKD>KmsV_oOspz7j(`c?U<|ykBkBK?OOdNC)lp!QQ`{a-C(#6ju6d zCcDEqdpi`7E19D8&ZojY1Buc~1y5gZj!9bX1i6a=G`1pq?j%}mC=MOlsP5k_(2;!J z4z9C_3|-BSz0#gOgn%xhtCp`k-i?B9& wiyrFKO@gh} a?LP>kzKAjg|DRiD6rt{%HH7i0Z^l36_%_Z)Dmg^9+ph)lm5N`V0(F%01 z9V)rP3)8Vk@^e1 X6L@xRqW=;Vqo+68qx!s4}1L6SL%dHcN FdUB;5VsmFJX zJg9X2?W}X^T3PpFspErZBxH5h!k?3U9`fvKiMwqMfmFi#b)SOov0U!6TC#Q;_p&<2 zJz1<&4##E)9CJ5%&*3=EAD!DS4^{Z77?xny%_%El;t2ERygis CDP=G?(}cmxZqJ1rJPfEpd*vG(;&kD5J-jFdlPnoWuAsMn~0M#vD9Z%V Zliq`CQ| z7sfpK#x-6AEdK`y}gDPrVueVONi>4lAS4^qhk&XS{9Ll;Q*G2Pn z?ZTUIJ9`O?1xFUgdp8^W5Km(m8Wwd?2374remG@ ugDf-9Fhqa%Kp&ntX76rh-m1({go?A z>Tp!7sKHd5eI{As-Z=ts!uH_JCB_j~ms<{l#q<|WY2Pf7p@nDO#1pBu2iv0lBNc?& zk4X0|=g;M%UO0XK1_hB8zI_wVxo(r}4V}2n>cm9uGy#B()^IGnKz#Rx&c($}ykaq8 zgAswjj$vQ(&j%&NBwP1F-HkQ;zj@iW5Cd(sMr{dEUBCwyAxy#5K&@EH4AoH|Q)AYt zz%D7G8(6={M}1;^5!_;X(MZ}%@%olC*MF_+zWboZ2tH7Sj 3l>tuEP7FgN#vj52!x zDn*BK*V8z=pb&xI3RekAE(xhY$u~|RigMRFjo`7x2w4!C$0n*{palqQQsjfahoL2D z8A?Y4zz^#b*^I+1+7m|CB^vcwD8u5|Us>&1Kwe^H(x{2X><^)gmTb=H-K-G@@U|2i zaRz6-`cGAEru=D$yU?YC616OaB(?jfcJaP-j)ZIlvRV$&*cco}??+|{2&>-Lb|wFn zty#{ JW;fnpmX~pH87>R-<59@`!$un0~pmP&WVL6IM zhbQeLp+BAj=$QhxTh16vEfp{_qAab6l_xws5|6=vZBY5rGwkNl*#Y99cChRh?bY@% z&(7KNK(7xMIDpTLp!JQn;s?*5b9CWTfNp&FhdM7AW`G+7zo!hL1{&Gv-CO}f==haK z0d4Fz<>yJI9P-up^Ze&k;_ONIXcr!Ws^Mz`QwZ!%qs4J62Mq9uz;D8XCszH3?nOlhzBk (wdE# jaS4?jX^fRI4|jamH5)4mcn3%KT56GIq!W9}nx z2(}E_<@MLvpT)AQ0=ntpLfKXp-NEH5Fu+H^0N+{9z3vbxa*xi5>|`^vCRsB`>o1Pn zhqO9S@gj=-=s%cbbBUoo`5P&o%TZtCOTb8Faf#usHu)DqY$#ws2bYq-$^FJf&KN1W zP)O}AZY(Rg9{PD&&A1&jU=i4mkyIB52P`C`42}ejhyG_WSCnNpqj~|6(*82Nr{gOA zcqw8th{7b?f=tRKhEf%AjEeq1LleNSla8#VjZ9AJTfj#Nb^I8LzW8+t`I=*v!s=u@ z7c7liPeN2Qm`WYPO*Y}qFyy*ekP*Gax^s?QmIc4%)q!Vl^7Z*t*$gYF4o5C*fz&%b z3ek##|6n7Y{TLwxk{DgS5Pn={nC*sy4Z;&y_RUhlG1;@Q$V1xOx~Wnu$$0q`i{>cJ z$fijY5jfAgkaj5cg`h0;jf%VrK gEZfwd~GlT#=l0MZEa!4ti)_W8XH7Di7U7O7Bc5@mOc q%Pmzv~oAak{De`KXyF|c6UA!_E;ec z2t=E|OaxcB*y%_pB!S-`{!ni}g{-JALp>SJ*uz8MB>599AOwYXvrn-LwErj2Re4=o zLF#?y5zbI@HD0Oz=6hNvmwRQSxRm{U-u*2|;!%k# e {QJ)we1iu@7;n-{FpcazpWOpS@?IZtM1pvW5apN3aB+#L=Hn zE8uQruItn$oqxDu(mv8X$y`6up;1GotrJ2ViqiZu7i(Jqn{gA^KEag{tT42}rEQG8 zZ%rw;KQ*$PW?eseYL$YID>&P1*x|ILeCzdy6pmT$zij9fODT*IJ-$Muwe1AvyW0w6 z-KU^vOC3e_nFril> vF(evWo>i8wb z|KZccawcS0yF0kmI~W%|z$YiE0pJq6ihx|Y#)7wnM+6*p`#?eFe;A7{F1228d~3=T zrE#`&vM>Dhn_4(#O3J2(A!Ufq`VX<8Y881wksqUdL}U`(>T3a3S8%EuSx;1%la=&D z< 5 oZ8PKT<#U012k~N{F^s_kKA8d3Qm%G%QPgANLrZ*i%ebVz|^Xsx6WG zs4mOp#w;g()+2XT5!CtZn66&RHaroU*8T-TF4hDXHn2YUu<3?olw_!G`TR&O|9@Ke z|DD5~x}V-3)L-Wk$&YIZmOnfM-W(wG`xda`Qx6mN7L; %P8lPk zj+t^bVz5`mVO{wFuXP{@GD>#HxQ`_=3!RS?ENwoOOTQ8N7dD{9nKy+X?9H#XS(sUs zDwldzc`fX*aHpvf` pL9QwtW$LrWEsHAAAPns)@LWBs zGdoP7<-&@W8vC!8`gy@>epwF7qBn;!n&Kh =PVaqpcnoxbJ6BbK`MMi;{KY0 z1naXn2)Aufm~<@u>=VQmQ2WwHq&gJ94g5t0xBm^L=N@qLY(A_g8=Ac@FX<>M?5}Mr z(T}#K14^#mLlxZO3GYZcebKP8-o?r4hbEj4FAmz7h!q-%L5>;InKeTOwd!Me2y*;RC!+hN;)VNv9O9;m{x5X4|0qFvI%XxZ% zgUHB0A-;Sru!j-TKjWb#3nH$( trMewEDF@C=Z>&j`O`bRRN?cT_zzByt zr=*LQE-yKY{JNc25kuz>j{)52mIE<~j{f$AMyw9Qz>-$W$pH|$@%WE@(o;mT0S<5m zV9`w3Y!}(4F&!g%_d(lI{0IFozvUt}un8f*%FEiEW-i|YK)*)~$g9(&_ej>tHnhex z3?+tGQb$scO>gbcqMBr_OZAG`Vb-};($Go)lfs@D=#bF`Ty+#3?Bq;&D>S`4#5=`z z%WhvN;NUWTA<_hZc3R5q$dW+JG1ED0ZXV*YT!Rph{<%RI5CUo^6ancUAmY8zs`)O& zKh_z9;KAh|xmUQMpIk4qGPG_cOctn!OZY?;s3xOp|9n?ydpORks0_ z=o0NSxSh>Edi>^_{~1njpCtbbs=a6csDa}nuTnUHrADi2r+$Qq2D9_oJ?Rv2|M%Dk zr$;Ymb1smFh^sEN^Nn%mHjG}Wi3nUxJU7=wop`~ATKf8EZQ_FzS@r58`erl2v8wKl zD1U{E44e5!<#%;~AUiop^>u zPEVTx<_J-tir$aMi(540ADmryTK+V?pgo#`M74mn&hoGRnkhh60V@@3Rj*TFyj`33$xbIqAIYmzrsNk*o0 z3w~b_3@1-$1-ohk8-YX7=^b<1Z41k+^45P|`Z`P zSz`6o$Uf2GQO;uer(szER`H|uM#CVij8HJWc>}*}$kD0-P!3(kWbiA0Og2#Lv@QbR zd-?^OT*N!coA*&ctgUMAt8kyhuZ4EhHQdVX8178&(v;>hxC?tDrf-p6p%yX~x5>t` zw$ThxrnAE2ULrTZ?d5mUj|=Y|o+_kB6#VWu2z5M8cM~p9!{VyA5c!tV>v63#lTsyM z6A07`(Ww$ZTqVx?;4SHRW1#jvY7Ijfm#cc;cr!*GpM%xpJf}=)aA?}OYO`}sC_&8% zi6#m0+V#nokE