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 "
[report.Join("
")]
" + +/datum/team/blood_worm/proc/print_objective_list() + . = list() + for (var/i in 1 to length(objectives)) + var/datum/objective/objective = objectives[i] + . += "Objective #[i]: [objective.explanation_text] [objective.get_roundend_success_suffix()]" + +/datum/team/blood_worm/proc/did_we_win() + for (var/datum/objective/objective as anything in objectives) + if (!objective.check_completion()) + return FALSE + return TRUE diff --git a/code/modules/antagonists/blood_worm/blood_worm_tester.dm b/code/modules/antagonists/blood_worm/blood_worm_tester.dm new file mode 100644 index 00000000000..33d2fa61afc --- /dev/null +++ b/code/modules/antagonists/blood_worm/blood_worm_tester.dm @@ -0,0 +1,87 @@ +/obj/item/blood_worm_tester + name = "hemoparasite testing tool" + desc = "A proprietary device patented by the DeForest Medical Corporation that is tailor-made for detecting hemoparasites, such as the infamous space-faring blood worm. The testing process is allegedly very painful." + + icon = 'icons/obj/antags/blood_worm.dmi' + icon_state = "tester" + + inhand_icon_state = "blood_worm_tester" + lefthand_file = 'icons/mob/inhands/equipment/security_lefthand.dmi' + righthand_file = 'icons/mob/inhands/equipment/security_righthand.dmi' + + w_class = WEIGHT_CLASS_SMALL + + var/spent = FALSE + +/obj/item/blood_worm_tester/Initialize(mapload) + . = ..() + AddElement(/datum/element/update_icon_updates_onmob) + update_appearance(UPDATE_ICON_STATE | UPDATE_DESC) + +/obj/item/blood_worm_tester/update_icon_state() + icon_state = spent ? "tester_spent" : "tester" + inhand_icon_state = spent ? "blood_worm_tester_spent" : "blood_worm_tester" + return ..() + +/obj/item/blood_worm_tester/update_desc(updates) + desc = "[initial(desc)] [spent ? "This one is spent." : "It's loaded for a single use."]" + return ..() + +/obj/item/blood_worm_tester/attack(mob/living/target_mob, mob/living/user, list/modifiers, list/attack_modifiers) + if (spent) + target_mob.balloon_alert(user, "already spent!") + return + if (!ISADVANCEDTOOLUSER(user)) + target_mob.balloon_alert(user, "needs dexterity!") + return + if (!ishuman(target_mob)) + target_mob.balloon_alert(user, "target a human!") + return + if (!target_mob.try_inject(user, injection_flags = INJECT_TRY_SHOW_ERROR_MESSAGE)) + return + + if (target_mob != user) + user.visible_message( + message = span_danger("\The [user] jab[user.p_s()] \the [target_mob] with \the [src]!"), + self_message = span_danger("You jab \the [target_mob] with \the [src]!"), + ignored_mobs = target_mob, + ) + + target_mob.show_message( + msg = span_userdanger("\The [user] jab[user.p_s()] you with \the [src]!"), + type = MSG_VISUAL, + ) + else + user.visible_message( + message = span_notice("\The [user] jab[user.p_s()] [user.p_themselves()] with \the [src]."), + self_message = span_notice("You jab yourself with \the [src]."), + ) + + log_combat(user, target_mob, "tested", src) + + target_mob.painful_scream() + target_mob.apply_damage(rand(10, 15), BRUTE, def_zone = check_zone(user.zone_selected), wound_bonus = CANT_WOUND, attack_direction = get_dir(user, target_mob), attacking_item = src) + target_mob.add_mood_event("tester", /datum/mood_event/jabbed_with_tester) + + playsound(src, 'sound/items/hypospray.ogg', vol = 50, vary = TRUE) + + say("Scanning...") + + // Handling it as a timer instead of a do_after prevents abusing the do_after to do antag checks. + // Otherwise, if the target runs away from the testing do_after, then that means they're likely a host. + // So, contrary to intuition, having this on a timer is way better for the blood worms than a do_after. + addtimer(CALLBACK(src, PROC_REF(report_results), HAS_TRAIT(target_mob, TRAIT_BLOOD_WORM_HOST)), 3 SECONDS) + + spent = TRUE + update_appearance(UPDATE_ICON_STATE | UPDATE_DESC) + +/obj/item/blood_worm_tester/proc/can_inject(mob/living/target_mob, mob/living/user, target_zone) + return target_mob.try_inject(user, target_zone, injection_flags = INJECT_TRY_SHOW_ERROR_MESSAGE) + +/obj/item/blood_worm_tester/proc/report_results(is_worm) + if (is_worm) + say("Active hemoparasite presence detected!") + playsound(src, 'sound/machines/beep/twobeep.ogg', vol = 50, vary = TRUE) + else + say("No anomalous readings found.") + playsound(src, 'sound/machines/buzz/buzz-two.ogg', vol = 40, vary = TRUE) diff --git a/code/modules/antagonists/blood_worm/blood_worm_text_feedback.dm b/code/modules/antagonists/blood_worm/blood_worm_text_feedback.dm new file mode 100644 index 00000000000..c002e271717 --- /dev/null +++ b/code/modules/antagonists/blood_worm/blood_worm_text_feedback.dm @@ -0,0 +1,97 @@ +// Any proc that communicates text to/from a blood worm goes here. +// These procs are usually pretty big and there are quite a few of them. + +/mob/living/basic/blood_worm/examining(atom/target, list/result) + add_special_examining_messages(target, result) + +/mob/living/basic/blood_worm/proc/on_host_examining(datum/source, atom/target, list/examine_strings) + SIGNAL_HANDLER + add_special_examining_messages(target, examine_strings) + +/mob/living/basic/blood_worm/proc/add_special_examining_messages(atom/target, list/result) + if (!isliving(target) || target == host) + return + + var/mob/living/bloodbag = target + + var/cached_blood_volume = bloodbag.get_blood_volume() + + if (cached_blood_volume <= 0) + return + + var/list/blood_data = bloodbag.get_blood_data() + + var/synth_content = blood_data?[BLOOD_DATA_SYNTH_CONTENT] + if (!isnum(synth_content)) + synth_content = 0 // Otherwise the switch statement breaks. + + var/normal_content = 1 - synth_content + + var/normal_blood_after = consumed_normal_blood + cached_blood_volume * normal_content + var/synth_blood_after = min(consumed_synth_blood + cached_blood_volume * synth_content * synth_blood_efficiency, maximum_synth_blood) + + var/total_blood_now = get_consumed_blood() + var/total_blood_after = normal_blood_after + synth_blood_after + + var/potential_gain = total_blood_after - total_blood_now + + var/rounded_volume = CEILING(cached_blood_volume, 1) + + var/growth_string = "" + if (HAS_TRAIT(bloodbag, TRAIT_BLOOD_WORM_HOST)) + growth_string = ", but consuming it is impossible, as they are a host" + else if (total_blood_now < cocoon_action?.total_blood_required) + var/rounded_growth = CEILING(potential_gain / cocoon_action.total_blood_required * 100, 1) + if (rounded_growth > 0) + growth_string = ", consuming it would contribute [rounded_growth]% to your growth" + else + growth_string = ", but consuming it wouldn't contribute to your growth" + else + if (!istype(src, /mob/living/basic/blood_worm/adult)) + growth_string = ". You are already ready to mature" + else + growth_string = ". You are already fully grown" + + var/synth_string = "[CEILING(synth_content * 100, 1)]%" + switch(synth_content) + if (-INFINITY to 0) + synth_string = "not" + if (1 to INFINITY) + synth_string = "fully" + if (0 to 1) + synth_string = "[CEILING(synth_content * 100, 1)]%" + + result += span_notice("[target.p_They()] [target.p_have()] [rounded_volume] unit[rounded_volume == 1 ? "" : "s"] of blood[growth_string]. [target.p_Their()] blood is [synth_string] synthetic.") + +/mob/living/basic/blood_worm/get_status_tab_items() + return ..() + get_special_status_tab_items() + +/mob/living/basic/blood_worm/proc/on_host_get_status_tab_items(datum/source, list/items) + SIGNAL_HANDLER + items += "Worm Health: [round((health / maxHealth) * 100)]%" + items += get_special_status_tab_items() + +/mob/living/basic/blood_worm/proc/get_special_status_tab_items() + . = list() + + var/normal = consumed_normal_blood + var/synth = consumed_synth_blood + var/total = normal + synth + + var/total_required = cocoon_action?.total_blood_required + + if (total_required > 0) + . += "Growth: [FLOOR(total / total_required * 100, 1)]%" + . += "Blood Consumed" + . += "- Normal: [CEILING(normal, 1)]u" + . += "- Synthetic: [CEILING(synth, 1)]u (MAX: [maximum_synth_blood]u)" + . += "- Total: [CEILING(total, 1)]u (REQ: [total_required]u)" + +/// Sends text to the blood worm, whether they are possessing a host or not. +/mob/living/basic/blood_worm/proc/to_chat_self(text) + to_chat(is_possessing_host ? host : src, text) + +/// Sends a balloon alert to the blood worm, whether they are possessing a host or not. +/mob/living/basic/blood_worm/proc/balloon_alert_self(text) + var/mob/living/self = is_possessing_host ? host : src + self.balloon_alert(self, text) diff --git a/code/modules/antagonists/brother/brother.dm b/code/modules/antagonists/brother/brother.dm index a20da754d4e..6cf17e5efab 100644 --- a/code/modules/antagonists/brother/brother.dm +++ b/code/modules/antagonists/brother/brother.dm @@ -93,7 +93,7 @@ flashed.balloon_alert(source, "[flashed.p_theyre()] loyal to someone else!") return - if (HAS_TRAIT(flashed, TRAIT_UNCONVERTABLE)) + if (HAS_MIND_TRAIT(flashed, TRAIT_UNCONVERTABLE)) flashed.balloon_alert(source, "[flashed.p_they()] resist!") return diff --git a/code/modules/antagonists/changeling/powers/expel_worm.dm b/code/modules/antagonists/changeling/powers/expel_worm.dm new file mode 100644 index 00000000000..7cda9bbe87a --- /dev/null +++ b/code/modules/antagonists/changeling/powers/expel_worm.dm @@ -0,0 +1,26 @@ +/datum/action/changeling_expel_worm + name = "Expel Worm" + desc = "Forcefully expel the blood worm in your body." + + background_icon_state = "bg_changeling" + overlay_icon_state = "bg_changeling_border" + button_icon = 'icons/mob/actions/actions_changeling.dmi' + button_icon_state = "expel_worm" + +/datum/action/changeling_expel_worm/IsAvailable(feedback) + if (!IS_CHANGELING(owner)) + return FALSE + if (!istype(owner, /mob/living/blood_worm_host)) + return FALSE + if (!HAS_TRAIT(owner.loc, TRAIT_BLOOD_WORM_HOST)) + return FALSE + if (!locate(/mob/living/basic/blood_worm) in owner.loc) + return FALSE + return TRUE + +/datum/action/changeling_expel_worm/Trigger(mob/clicker, trigger_flags) + var/mob/living/basic/blood_worm/invader = locate() in owner.loc + to_chat(owner, span_danger("You expel \the [invader] from your body!")) + to_chat(invader, span_userdanger("You are forcefully expelled by the body of \the [owner.loc]!")) + invader.leave_host() // hasta la vista, worm + return TRUE diff --git a/code/modules/cargo/packs/security.dm b/code/modules/cargo/packs/security.dm index 9754885e423..fc4104420ce 100644 --- a/code/modules/cargo/packs/security.dm +++ b/code/modules/cargo/packs/security.dm @@ -400,3 +400,13 @@ /obj/item/mecha_parts/mecha_equipment/weapon/energy/thermal/inferno, ) crate_name = "thermal cannons crate" + +/datum/supply_pack/security/blood_worm_testers + name = "Hemoparasite Testing Crate" + desc = "Contains four single-use hemoparasite testing tools. \ + These devices accurately report whether a patient is inhabited by a hemoparasite, such as the infamous space-faring blood worm. \ + The DeForest Medical Corporation claims no liability for any mental and/or physical trauma caused to patients from the improper use of these devices." + cost = CARGO_CRATE_VALUE * 50 // These aren't meant to be bought en-masse. + access_view = ACCESS_SECURITY + contains = list(/obj/item/blood_worm_tester = 4) + crate_name = "hemoparasite testing crate" diff --git a/code/modules/language/_language_holder.dm b/code/modules/language/_language_holder.dm index dc65a708841..c4207f40f07 100644 --- a/code/modules/language/_language_holder.dm +++ b/code/modules/language/_language_holder.dm @@ -464,6 +464,15 @@ GLOBAL_LIST_INIT(prototype_language_holders, init_language_holder_prototypes()) blocked_speaking = list(/datum/language/common = list(LANGUAGE_ATOM)) blocked_understanding = list(/datum/language/common = list(LANGUAGE_ATOM)) +/datum/language_holder/blood_worm + understood_languages = list( + /datum/language/wormspeak = list(LANGUAGE_ATOM), + /datum/language/common = list(LANGUAGE_ATOM) + ) + + spoken_languages = list(/datum/language/wormspeak = list(LANGUAGE_ATOM)) + blocked_speaking = list(/datum/language/common = list(LANGUAGE_ATOM)) + /datum/language_holder/synthetic understood_languages = list( /datum/language/common = list(LANGUAGE_ATOM), diff --git a/code/modules/language/wormspeak.dm b/code/modules/language/wormspeak.dm new file mode 100644 index 00000000000..b068921853f --- /dev/null +++ b/code/modules/language/wormspeak.dm @@ -0,0 +1,8 @@ +/datum/language/wormspeak + name = "Wormspeak" + desc = "A language used by all kinds of large worms. Formed from the bellowing of their giant maws." + key = "w" + syllables = list("oaaa", "eeo", "aaeo", "ooe", "eaeaea", "aeaeoo", "o", "ae") + icon_state = "wormspeak" + default_priority = 90 + always_use_default_namelist = TRUE diff --git a/code/modules/logging/categories/log_category_game.dm b/code/modules/logging/categories/log_category_game.dm index f337d5f821c..fced988c1fc 100644 --- a/code/modules/logging/categories/log_category_game.dm +++ b/code/modules/logging/categories/log_category_game.dm @@ -61,3 +61,8 @@ category = LOG_CATEGORY_GAME_ACCESS config_flag = /datum/config_entry/flag/log_access master_category = /datum/log_category/game + +/datum/log_category/game_blood_worm + category = LOG_CATEGORY_GAME_BLOOD_WORM + config_flag = /datum/config_entry/flag/log_blood_worm + master_category = /datum/log_category/game diff --git a/code/modules/mob/living/blood.dm b/code/modules/mob/living/blood.dm index 7e97ef7a821..7bc5c93c8df 100644 --- a/code/modules/mob/living/blood.dm +++ b/code/modules/mob/living/blood.dm @@ -30,6 +30,10 @@ var/amount = blood_volume + // For simple multipliers, like a blood worm in a mob. + for (var/source in blood_volume_modifiers) + amount *= blood_volume_modifiers[source] + // Handled here instead of in the saline reagent datum, because this way the modification order is consistent. // E.g. if you have an effect that modifies blood volume over the dilution cap, then saline should do nothing. var/datum/reagent/medicine/salglu_solution/saline = reagents?.has_reagent(/datum/reagent/medicine/salglu_solution) @@ -48,15 +52,15 @@ if (!CAN_HAVE_BLOOD(src) && amount != 0) return cached_blood_volume - if (amount == cached_blood_volume) - return cached_blood_volume + // Don't return early even if "amount == cached_blood_volume", because we don't know if minimum or maximum would change it anyway. + // Putting this here because I made that mistake and it led to a bug. blood_volume = clamp(amount, minimum, maximum) var/updated_blood_volume = get_blood_volume() if (cached_blood_volume != updated_blood_volume) - living_flags |= QUEUE_BLOOD_UPDATE + QUEUE_BLOOD_UPDATE(src) return updated_blood_volume @@ -85,29 +89,53 @@ var/updated_blood_volume = set_blood_volume(cached_blood_volume + amount, minimum = minimum, maximum = maximum, cached_blood_volume = cached_blood_volume) return updated_blood_volume - cached_blood_volume -/// Updates effects that rely on blood volume, like blood HUDs. +/// Updates effects that rely on blood volume or status, like blood HUDs. /mob/living/proc/update_blood_effects() - living_flags &= ~QUEUE_BLOOD_UPDATE + living_flags &= ~BLOOD_UPDATE_QUEUED + blood_hud_set_status() /// Updates effects that rely on whether the mob can have blood. /mob/living/proc/update_blood_status() var/had_blood = CAN_HAVE_BLOOD(src) + var/has_blood = can_have_blood() - living_flags = can_have_blood() ? (living_flags | LIVING_CAN_HAVE_BLOOD) : (living_flags & ~LIVING_CAN_HAVE_BLOOD) - - var/has_blood = CAN_HAVE_BLOOD(src) - + // Must not return early on first init for mobs that can have blood. (otherwise they will miss being added to the blood hud) if (had_blood == has_blood) return var/old_blood_volume = get_blood_volume() + // Must be sent before living flags are updated. + SEND_SIGNAL(src, COMSIG_LIVING_PRE_UPDATE_BLOOD_STATUS, had_blood, has_blood, old_blood_volume) + + living_flags = has_blood ? (living_flags | LIVING_CAN_HAVE_BLOOD) : (living_flags & ~LIVING_CAN_HAVE_BLOOD) + set_blood_volume(has_blood ? default_blood_volume : 0) var/new_blood_volume = get_blood_volume() + // Must be sent after living flags are updated. SEND_SIGNAL(src, COMSIG_LIVING_UPDATE_BLOOD_STATUS, had_blood, has_blood, old_blood_volume, new_blood_volume) + var/datum/atom_hud/data/human/blood/blood_hud = GLOB.huds[DATA_HUD_BLOOD] + + if (has_blood) + blood_hud.add_atom_to_hud(src) + else + blood_hud.remove_atom_from_hud(src) + + update_blood_effects() + +/// Sets the blood volume multiplier for the given source to the given multiplier value. +/mob/living/proc/set_blood_volume_modifier(source, multiplier) + LAZYSET(blood_volume_modifiers, source, multiplier) + QUEUE_BLOOD_UPDATE(src) + +/// Removes the blood volume multiplier for the given source. +/mob/living/proc/remove_blood_volume_modifier(source) + LAZYREMOVE(blood_volume_modifiers, source) + QUEUE_BLOOD_UPDATE(src) + /// A one-time coagulating effect of the mob's bloodiest cut/stab /// Returns TRUE if a wound was affected, FALSE if no wound was found /mob/living/proc/coagulant_effect(amount_to_heal = 1) @@ -382,8 +410,20 @@ adjust_blood_volume(-amount) return amount + var/cached_target_blood_volume = target.get_blood_volume() + // And, obviously, cap it to how much blood the target can take if they're living. amount = target.adjust_blood_volume(amount, maximum = BLOOD_VOLUME_MAX_LETHAL) + + // Synthetic blood handling, used for capping blood worm growth from monkeys, blood bags and basic mobs. This forces blood worms to kill people to reach adulthood. + if (!IS_BLOOD_ALWAYS_SYNTHETIC(target)) + var/added_synth_volume = amount * get_blood_synth_content() + var/existing_synth_volume = cached_target_blood_volume * target.get_blood_synth_content() + + if (added_synth_volume != 0 || existing_synth_volume != 0) + // A simple weighted average that simplifies down to "total synth volume / total blood volume" i.e. "how much of their blood is synthetic" + target.AddComponent(/datum/component/synth_blood, (added_synth_volume + existing_synth_volume) / (amount + cached_target_blood_volume)) + adjust_blood_volume(-amount) return amount @@ -418,6 +458,10 @@ if(LAZYLEN(disease_resistances)) blood_data["resistances"] = disease_resistances.Copy() + var/synth_content = get_blood_synth_content() + if (synth_content > 0) + blood_data[BLOOD_DATA_SYNTH_CONTENT] = synth_content + // DNA, mind, facitons, etc don't get stored in stuff like oil if (!(blood_type.blood_flags & BLOOD_ADD_DNA)) return blood_data @@ -775,5 +819,14 @@ return blood_alcohol_content +/// Returns on a 0-1 scale how synthetic this mobs blood is. Used for blood worms to cap growth from easily accessible sources of blood. +/mob/living/proc/get_blood_synth_content() + if (IS_BLOOD_ALWAYS_SYNTHETIC(src)) + return 1 // Basic mobs, simple mobs, born monkeys and spawned mobs have fully synthetic blood. + + // Handles accounting for synthetic blood injected into mobs that don't normally have it. + var/datum/component/synth_blood/synth_comp = GetComponent(/datum/component/synth_blood) + return synth_comp?.synth_content + #undef BLOOD_DRIP_RATE_MOD #undef DRUNK_POWER_TO_BLOOD_ALCOHOL diff --git a/code/modules/mob/living/carbon/human/human_defines.dm b/code/modules/mob/living/carbon/human/human_defines.dm index fd063a075f2..c377632f2f4 100644 --- a/code/modules/mob/living/carbon/human/human_defines.dm +++ b/code/modules/mob/living/carbon/human/human_defines.dm @@ -5,7 +5,7 @@ icon = 'icons/mob/human/human.dmi' icon_state = "human_basic" appearance_flags = KEEP_TOGETHER|TILE_BOUND|PIXEL_SCALE|LONG_GLIDE - hud_possible = list(HEALTH_HUD,STATUS_HUD,ID_HUD,WANTED_HUD,IMPLOYAL_HUD,IMPSEC_FIRST_HUD,IMPSEC_SECOND_HUD,ANTAG_HUD,GLAND_HUD,FAN_HUD) + hud_possible = list(HEALTH_HUD,STATUS_HUD,BLOOD_HUD,ID_HUD,WANTED_HUD,IMPLOYAL_HUD,IMPSEC_FIRST_HUD,IMPSEC_SECOND_HUD,ANTAG_HUD,GLAND_HUD,FAN_HUD) hud_type = /datum/hud/human pressure_resistance = 25 buckle_lying = 0 diff --git a/code/modules/mob/living/carbon/init_signals.dm b/code/modules/mob/living/carbon/init_signals.dm index 415d1f908c0..a46acd8c520 100644 --- a/code/modules/mob/living/carbon/init_signals.dm +++ b/code/modules/mob/living/carbon/init_signals.dm @@ -14,6 +14,7 @@ RegisterSignal(src, SIGNAL_ADDTRAIT(TRAIT_VIRUSIMMUNE), PROC_REF(on_virusimmune_trait_gain)) RegisterSignal(src, SIGNAL_ADDTRAIT(TRAIT_TOXIMMUNE), PROC_REF(on_toximmune_trait_gain)) RegisterSignal(src, SIGNAL_ADDTRAIT(TRAIT_GENELESS), PROC_REF(on_geneless_trait_gain)) + RegisterSignal(src, SIGNAL_ADDTRAIT(TRAIT_NO_SPLIT_PERSONALITY), PROC_REF(on_no_split_personality_trait_gain)) /** * On gain of TRAIT_AGENDER @@ -130,3 +131,13 @@ SIGNAL_HANDLER dna?.remove_all_mutations() + +/** + * On gain of TRAIT_NO_SPLIT_PERSONALITY + * + * This will make the mob lose the split personality trauma if they have it. + */ +/mob/living/carbon/proc/on_no_split_personality_trait_gain(datum/source) + SIGNAL_HANDLER + + cure_trauma_type(/datum/brain_trauma/severe/split_personality, TRAUMA_LIMIT_ABSOLUTE) diff --git a/code/modules/mob/living/carbon/life.dm b/code/modules/mob/living/carbon/life.dm index 45b4142ed4b..1f5e2b908f9 100644 --- a/code/modules/mob/living/carbon/life.dm +++ b/code/modules/mob/living/carbon/life.dm @@ -486,9 +486,19 @@ if(blood_type.reagent_type != chem.type) return + var/cached_blood_volume = get_blood_volume() + var/blood_added = adjust_blood_volume(round(reac_volume, CHEMICAL_VOLUME_ROUNDING)) reagents.remove_reagent(chem.type, blood_added) + if(chem.data?[BLOOD_DATA_SYNTH_CONTENT] && !IS_BLOOD_ALWAYS_SYNTHETIC(src)) + var/added_synth_volume = blood_added * chem.data[BLOOD_DATA_SYNTH_CONTENT] + var/existing_synth_volume = cached_blood_volume * get_blood_synth_content() + + if (added_synth_volume != 0 || existing_synth_volume != 0) + // A simple weighted average that simplifies down to "total synth volume / total blood volume" i.e. "how much of our blood is synthetic" + AddComponent(/datum/component/synth_blood, (added_synth_volume + existing_synth_volume) / (blood_added + cached_blood_volume)) + if(chem.data?["blood_type"]) var/datum/blood_type/donor_type = chem.data["blood_type"] if(!(donor_type.type_key() in blood_type.compatible_types)) diff --git a/code/modules/mob/living/init_signals.dm b/code/modules/mob/living/init_signals.dm index eeaeb9d3b72..34579f82174 100644 --- a/code/modules/mob/living/init_signals.dm +++ b/code/modules/mob/living/init_signals.dm @@ -73,6 +73,11 @@ RegisterSignal(src, SIGNAL_ADDTRAIT(TRAIT_BASIC_HEALTH_HUD_VISIBLE), PROC_REF(add_to_basic_health_hud)) RegisterSignal(src, SIGNAL_REMOVETRAIT(TRAIT_BASIC_HEALTH_HUD_VISIBLE), PROC_REF(remove_from_basic_health_hud)) + RegisterSignal(src, SIGNAL_ADDTRAIT(TRAIT_APATHETIC), PROC_REF(on_apathetic_trait_gain)) + RegisterSignal(src, SIGNAL_REMOVETRAIT(TRAIT_APATHETIC), PROC_REF(on_apathetic_trait_loss)) + + RegisterSignal(src, SIGNAL_ADDTRAIT(TRAIT_MIND_TEMPORARILY_GONE), PROC_REF(on_mind_temporarily_gone_trait_gain)) + RegisterSignal(src, SIGNAL_REMOVETRAIT(TRAIT_MIND_TEMPORARILY_GONE), PROC_REF(on_mind_temporarily_gone_trait_loss)) /// Called when [TRAIT_KNOCKEDOUT] is added to the mob. /mob/living/proc/on_knockedout_trait_gain(datum/source) @@ -313,3 +318,24 @@ SIGNAL_HANDLER var/datum/atom_hud/data/human/medical/basic/hud = GLOB.huds[DATA_HUD_MEDICAL_BASIC] hud.remove_atom_from_hud(src) + +/// Called when [TRAIT_APATHETIC] is added to the mob. +/mob/living/proc/on_apathetic_trait_gain(datum/source) + SIGNAL_HANDLER + mob_mood?.update_mood() + mob_mood?.set_sanity(SANITY_NEUTRAL) + +/// Called when [TRAIT_APATHETIC] is removed from the mob. +/mob/living/proc/on_apathetic_trait_loss(datum/source) + SIGNAL_HANDLER + mob_mood?.update_mood() + +/// Called when [TRAIT_MIND_TEMPORARILY_GONE] is added to the mob. +/mob/living/proc/on_mind_temporarily_gone_trait_gain(datum/source) + SIGNAL_HANDLER + med_hud_set_status() + +/// Called when [TRAIT_MIND_TEMPORARILY_GONE] is removed from the mob. +/mob/living/proc/on_mind_temporarily_gone_trait_loss(datum/source) + SIGNAL_HANDLER + med_hud_set_status() diff --git a/code/modules/mob/living/life.dm b/code/modules/mob/living/life.dm index c5197578b77..dc50f5ec56c 100644 --- a/code/modules/mob/living/life.dm +++ b/code/modules/mob/living/life.dm @@ -64,6 +64,9 @@ update_nutrition() living_flags &= ~QUEUE_NUTRITION_UPDATE + if (living_flags & BLOOD_UPDATE_QUEUED) + update_blood_effects() + if(stat != DEAD) return TRUE diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm index f8ac8fa8d01..cfc7924aa3c 100644 --- a/code/modules/mob/living/living_defines.dm +++ b/code/modules/mob/living/living_defines.dm @@ -1,7 +1,7 @@ /mob/living see_invisible = SEE_INVISIBLE_LIVING abstract_type = /mob/living - hud_possible = list(HEALTH_HUD,STATUS_HUD,ANTAG_HUD) + hud_possible = list(HEALTH_HUD,STATUS_HUD,BLOOD_HUD,ANTAG_HUD) pressure_resistance = 10 hud_type = /datum/hud/living interaction_flags_click = ALLOW_RESTING @@ -169,6 +169,9 @@ var/blood_volume = 0 /// The default blood volume of the mob. Used primarily for healing bloodloss. var/default_blood_volume = 0 + /// Lazylist of blood volume modifiers. These multiply blood volume when get_blood_volume(apply_modifiers = TRUE) is used. + /// Use set_blood_volume_modifier(multiplier, source) and remove_blood_volume_modifier(source) to modify this. + var/list/blood_volume_modifiers = null ///a list of all status effects the mob has var/list/status_effects diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 82aa79711d8..00d80357717 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -583,6 +583,7 @@ if(!result_combined) var/list/result = examinify.examine(src) var/atom_title = examinify.examine_title(src, thats = TRUE) + examining(examinify, result) SEND_SIGNAL(src, COMSIG_MOB_EXAMINING, examinify, result) if(removes_double_click) result += span_notice("You can examine [examinify] closer...") @@ -591,6 +592,11 @@ to_chat(src, span_infoplain(result_combined)) SEND_SIGNAL(src, COMSIG_MOB_EXAMINATE, examinify) +/// Handles adding examine messages for the target that are specific to this mob, e.g. a blood worm examining how much blood a living target has. +/mob/proc/examining(atom/target, list/result) + SHOULD_NOT_SLEEP(TRUE) + return + /mob/Topic(href, list/href_list) . = ..() if(.) diff --git a/code/modules/reagents/chemistry/holder/holder.dm b/code/modules/reagents/chemistry/holder/holder.dm index 64e4d3ed09b..3d50d5c1e28 100644 --- a/code/modules/reagents/chemistry/holder/holder.dm +++ b/code/modules/reagents/chemistry/holder/holder.dm @@ -142,10 +142,10 @@ iter_reagent.purity = ((iter_reagent.creation_purity * iter_reagent.volume) + (added_purity * amount)) /(iter_reagent.volume + amount) //This should add the purity to the product iter_reagent.creation_purity = iter_reagent.purity iter_reagent.ph = ((iter_reagent.ph * (iter_reagent.volume)) + (added_ph * amount)) / (iter_reagent.volume + amount) + iter_reagent.on_merge(data, amount) // Update this before updating volume. FIXME: Move all of the surrounding crap into this proc so implementations decide whether they go first or base code goes first. iter_reagent.volume += amount update_total() - iter_reagent.on_merge(data, amount) if(reagtemp != cached_temp) var/new_heat_capacity = heat_capacity() if(new_heat_capacity) diff --git a/code/modules/reagents/chemistry/recipes/others.dm b/code/modules/reagents/chemistry/recipes/others.dm index f6ad2894b94..a7dc118f2c0 100644 --- a/code/modules/reagents/chemistry/recipes/others.dm +++ b/code/modules/reagents/chemistry/recipes/others.dm @@ -237,11 +237,23 @@ /datum/chemical_reaction/mix_virus/on_reaction(datum/reagents/holder, datum/equilibrium/reaction, created_volume) var/datum/reagent/blood/B = locate(/datum/reagent/blood) in holder.reagent_list - if(B?.data) - var/datum/disease/advance/D = locate(/datum/disease/advance) in B.data["viruses"] - if(D) - D.Evolve(level_min, level_max) + if(!B) + return + if(!B.data) + B.data = list() + var/datum/disease/advance/D = locate(/datum/disease/advance) in B.data["viruses"] + if(D) + D.Evolve(level_min, level_max) + + // All blood duplicated via this reaction is synthetic. + var/added_synth_volume = created_volume + + // Blood reagents handle their merger code BEFORE this code runs. So the existing synth volume is already averaged into the total volume. + var/existing_synth_volume = B.volume * B.data[BLOOD_DATA_SYNTH_CONTENT] + + // A simple weighted average that simplifies down to "total synth volume / total blood volume" i.e. "how much of the blood is synthetic" + B.data[BLOOD_DATA_SYNTH_CONTENT] = (added_synth_volume + existing_synth_volume) / B.volume /datum/chemical_reaction/mix_virus/mix_virus_2 required_reagents = list(/datum/reagent/toxin/mutagen = 1) diff --git a/code/modules/reagents/reagent_containers/blood_pack.dm b/code/modules/reagents/reagent_containers/blood_pack.dm index 8d98d91b55a..3503649e724 100644 --- a/code/modules/reagents/reagent_containers/blood_pack.dm +++ b/code/modules/reagents/reagent_containers/blood_pack.dm @@ -14,7 +14,8 @@ if (!blood_type) return var/datum/blood_type/bloodtype = get_blood_type(blood_type) - reagents.add_reagent(bloodtype.reagent_type, volume, list("blood_type" = bloodtype, "blood_DNA" = bloodtype.dna_string), creation_callback = CALLBACK(src, PROC_REF(on_blood_created))) + // Blood pack blood is halfway synthetic, meaning instead of a maximum of like 6 blood worms being able to become adults via 2 freezers, only 3 or so can. + reagents.add_reagent(bloodtype.reagent_type, volume, list("blood_type" = bloodtype, "blood_DNA" = bloodtype.dna_string, BLOOD_DATA_SYNTH_CONTENT = 0.5), creation_callback = CALLBACK(src, PROC_REF(on_blood_created))) /obj/item/reagent_containers/blood/proc/on_blood_created(datum/reagent/new_blood) new_blood.AddElement(/datum/element/blood_reagent, null, get_blood_type(blood_type)) diff --git a/code/modules/reagents/withdrawal/_addiction.dm b/code/modules/reagents/withdrawal/_addiction.dm index 0e763baaf47..2c3e788dec1 100644 --- a/code/modules/reagents/withdrawal/_addiction.dm +++ b/code/modules/reagents/withdrawal/_addiction.dm @@ -60,6 +60,11 @@ LAZYREMOVE(victim_mind.active_addictions, type) /datum/addiction/proc/process_addiction(mob/living/carbon/affected_carbon, seconds_per_tick) + // Acts as if you're on the drug at all times, while also forcibly preventing the effects of withdrawal by returning early. + if(HAS_TRAIT(affected_carbon, TRAIT_NO_WITHDRAWALS)) + end_withdrawal(affected_carbon) + return FALSE + var/current_addiction_cycle = LAZYACCESS(affected_carbon.mind.active_addictions, type) //If this is null, we're not addicted var/on_drug_of_this_addiction = FALSE for(var/datum/reagent/possible_drug as anything in affected_carbon.reagents.reagent_list) //Go through the drugs in our system diff --git a/code/modules/spells/spell_types/shapeshift/_shapeshift.dm b/code/modules/spells/spell_types/shapeshift/_shapeshift.dm index 06b04198e6b..7ecd0e6b852 100644 --- a/code/modules/spells/spell_types/shapeshift/_shapeshift.dm +++ b/code/modules/spells/spell_types/shapeshift/_shapeshift.dm @@ -160,7 +160,9 @@ // Make sure it's castable even in their new form. pre_shift_requirements = spell_requirements spell_requirements &= ~(SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_WIZARD_GARB) - ADD_TRAIT(new_shape, TRAIT_DONT_WRITE_MEMORY, SHAPESHIFT_TRAIT) // If you shapeshift into a pet subtype we don't want to update Poly's deathcount or something when you die + + // If you shapeshift into a pet subtype we don't want to update Poly's deathcount or something when you die + new_shape.add_traits(list(TRAIT_DONT_WRITE_MEMORY, TRAIT_SHAPESHIFTED), SHAPESHIFT_TRAIT) // Make sure that if you shapechanged into a bot, the AI can't just turn you off. var/mob/living/simple_animal/bot/polymorph_bot = new_shape diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_bloodworm.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_bloodworm.png new file mode 100644 index 00000000000..5e96a563b83 Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_bloodworm.png differ diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_bloodworminfestation.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_bloodworminfestation.png new file mode 100644 index 00000000000..5e96a563b83 Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_bloodworminfestation.png differ diff --git a/config/dynamic.toml b/config/dynamic.toml index e5b7f92b2f9..970281b85ff 100644 --- a/config/dynamic.toml +++ b/config/dynamic.toml @@ -288,6 +288,16 @@ repeatable_weight_decrease = 3 repeatable = 1 minimum_required_age = 0 +["Midround Blood Worm"] +weight = 2 +min_pop = 20 +blacklisted_roles = [] +min_antag_cap = 1 +max_antag_cap = 2 +repeatable_weight_decrease = 2 +repeatable = 1 +minimum_required_age = 0 + ["Nightmare"] weight = 5 min_pop = 15 @@ -516,6 +526,16 @@ repeatable_weight_decrease = 2 repeatable = 1 minimum_required_age = 0 +["Roundstart Blood Worm"] +weight = 1 +min_pop = 10 +blacklisted_roles = [] +min_antag_cap = 1 +max_antag_cap.denominator = 29 +repeatable_weight_decrease = 2 +repeatable = 0 +minimum_required_age = 0 + ["Roundstart Heretics"] weight = 3 min_pop = 30 diff --git a/config/logging.txt b/config/logging.txt index 4b51dfc4d8a..09728bd8fb2 100644 --- a/config/logging.txt +++ b/config/logging.txt @@ -23,6 +23,9 @@ LOG_ADMINCHAT ## log attack messages LOG_ATTACK +## log blood worm actions like entering/leaving hosts +LOG_BLOOD_WORM + ## log economy actions LOG_ECON diff --git a/icons/hud/screen_gen.dmi b/icons/hud/screen_gen.dmi index d410fa2e44f..e4f4bb4eda7 100644 Binary files a/icons/hud/screen_gen.dmi and b/icons/hud/screen_gen.dmi differ diff --git a/icons/mob/actions/actions_blood_worm.dmi b/icons/mob/actions/actions_blood_worm.dmi new file mode 100644 index 00000000000..95bce38ce6c Binary files /dev/null and b/icons/mob/actions/actions_blood_worm.dmi differ diff --git a/icons/mob/actions/actions_changeling.dmi b/icons/mob/actions/actions_changeling.dmi index 506f91fc5cc..a721505a8c0 100644 Binary files a/icons/mob/actions/actions_changeling.dmi and b/icons/mob/actions/actions_changeling.dmi differ diff --git a/icons/mob/huds/antag_hud.dmi b/icons/mob/huds/antag_hud.dmi index e2ba2ca89fc..bc8d5a74002 100644 Binary files a/icons/mob/huds/antag_hud.dmi and b/icons/mob/huds/antag_hud.dmi differ diff --git a/icons/mob/huds/hud.dmi b/icons/mob/huds/hud.dmi index 0fb0e796913..04b1299f71a 100644 Binary files a/icons/mob/huds/hud.dmi and b/icons/mob/huds/hud.dmi differ diff --git a/icons/mob/inhands/equipment/security_lefthand.dmi b/icons/mob/inhands/equipment/security_lefthand.dmi index e87eebd154e..9616efef550 100644 Binary files a/icons/mob/inhands/equipment/security_lefthand.dmi and b/icons/mob/inhands/equipment/security_lefthand.dmi differ diff --git a/icons/mob/inhands/equipment/security_righthand.dmi b/icons/mob/inhands/equipment/security_righthand.dmi index e153e6395ff..ce66a1bc627 100644 Binary files a/icons/mob/inhands/equipment/security_righthand.dmi and b/icons/mob/inhands/equipment/security_righthand.dmi differ diff --git a/icons/mob/nonhuman-player/blood_worm_32x32.dmi b/icons/mob/nonhuman-player/blood_worm_32x32.dmi new file mode 100644 index 00000000000..636d0f6ca57 Binary files /dev/null and b/icons/mob/nonhuman-player/blood_worm_32x32.dmi differ diff --git a/icons/mob/nonhuman-player/blood_worm_32x48.dmi b/icons/mob/nonhuman-player/blood_worm_32x48.dmi new file mode 100644 index 00000000000..9249cae543e Binary files /dev/null and b/icons/mob/nonhuman-player/blood_worm_32x48.dmi differ diff --git a/icons/obj/antags/blood_worm.dmi b/icons/obj/antags/blood_worm.dmi new file mode 100644 index 00000000000..2b9fa5a22ce Binary files /dev/null and b/icons/obj/antags/blood_worm.dmi differ diff --git a/icons/obj/weapons/guns/projectiles.dmi b/icons/obj/weapons/guns/projectiles.dmi index b682c3866cb..a0feb9ba372 100644 Binary files a/icons/obj/weapons/guns/projectiles.dmi and b/icons/obj/weapons/guns/projectiles.dmi differ diff --git a/icons/ui/chat/language.dmi b/icons/ui/chat/language.dmi index c45b61b1041..7cb84ee3af9 100644 Binary files a/icons/ui/chat/language.dmi and b/icons/ui/chat/language.dmi differ diff --git a/sound/effects/alien/alien_spitacid.ogg b/sound/effects/alien/alien_spitacid.ogg new file mode 100644 index 00000000000..b4aa578b171 Binary files /dev/null and b/sound/effects/alien/alien_spitacid.ogg differ diff --git a/sound/effects/alien/alien_spitacid2.ogg b/sound/effects/alien/alien_spitacid2.ogg new file mode 100644 index 00000000000..243d1aa1744 Binary files /dev/null and b/sound/effects/alien/alien_spitacid2.ogg differ diff --git a/tgstation.dme b/tgstation.dme index df2fb45e487..11b6f032ec8 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -61,6 +61,7 @@ #include "code\__DEFINES\blend_modes.dm" #include "code\__DEFINES\blob_defines.dm" #include "code\__DEFINES\blood.dm" +#include "code\__DEFINES\blood_worm_defines.dm" #include "code\__DEFINES\bodyparts.dm" #include "code\__DEFINES\botany.dm" #include "code\__DEFINES\callbacks.dm" @@ -317,6 +318,7 @@ #include "code\__DEFINES\dcs\signals\signals_beam.dm" #include "code\__DEFINES\dcs\signals\signals_bitrunning.dm" #include "code\__DEFINES\dcs\signals\signals_blob.dm" +#include "code\__DEFINES\dcs\signals\signals_blood_worm.dm" #include "code\__DEFINES\dcs\signals\signals_bot.dm" #include "code\__DEFINES\dcs\signals\signals_camera.dm" #include "code\__DEFINES\dcs\signals\signals_changeling.dm" @@ -420,6 +422,7 @@ #include "code\__DEFINES\traits\sources.dm" #include "code\__HELPERS\_auxtools_api.dm" #include "code\__HELPERS\_dreamluau.dm" +#include "code\__HELPERS\_level_traits.dm" #include "code\__HELPERS\_lists.dm" #include "code\__HELPERS\_planes.dm" #include "code\__HELPERS\_string_lists.dm" @@ -462,7 +465,6 @@ #include "code\__HELPERS\icons.dm" #include "code\__HELPERS\jatum.dm" #include "code\__HELPERS\lazy_templates.dm" -#include "code\__HELPERS\level_traits.dm" #include "code\__HELPERS\levels.dm" #include "code\__HELPERS\lighting.dm" #include "code\__HELPERS\maths.dm" @@ -542,6 +544,7 @@ #include "code\_globalvars\admin.dm" #include "code\_globalvars\arcade.dm" #include "code\_globalvars\bitfields.dm" +#include "code\_globalvars\blood_worms.dm" #include "code\_globalvars\colorvars.dm" #include "code\_globalvars\configuration.dm" #include "code\_globalvars\game_modes.dm" @@ -1294,6 +1297,7 @@ #include "code\datums\components\sisyphus_awarder.dm" #include "code\datums\components\sitcomlaughter.dm" #include "code\datums\components\sizzle.dm" +#include "code\datums\components\slide_under_doors.dm" #include "code\datums\components\slime_friends.dm" #include "code\datums\components\slippery.dm" #include "code\datums\components\smooth_tunes.dm" @@ -1324,6 +1328,7 @@ #include "code\datums\components\supermatter_crystal.dm" #include "code\datums\components\swabbing.dm" #include "code\datums\components\swarming.dm" +#include "code\datums\components\synth_blood.dm" #include "code\datums\components\tackle.dm" #include "code\datums\components\tactical.dm" #include "code\datums\components\takes_reagent_appearance.dm" @@ -1494,6 +1499,7 @@ #include "code\datums\elements\ai_swap_combat_mode.dm" #include "code\datums\elements\ai_target_damagesource.dm" #include "code\datums\elements\animal_variety.dm" +#include "code\datums\elements\anti_self_harm.dm" #include "code\datums\elements\art.dm" #include "code\datums\elements\atmos_requirements.dm" #include "code\datums\elements\atmos_sensitive.dm" @@ -1579,6 +1585,7 @@ #include "code\datums\elements\give_turf_traits.dm" #include "code\datums\elements\glass_pacifist.dm" #include "code\datums\elements\gravedigger.dm" +#include "code\datums\elements\hand_organ_insertion.dm" #include "code\datums\elements\hat_wearer.dm" #include "code\datums\elements\haunted.dm" #include "code\datums\elements\high_fiver.dm" @@ -3313,6 +3320,23 @@ #include "code\modules\antagonists\blob\structures\node.dm" #include "code\modules\antagonists\blob\structures\resource.dm" #include "code\modules\antagonists\blob\structures\shield.dm" +#include "code\modules\antagonists\blood_worm\blood_worm_antag.dm" +#include "code\modules\antagonists\blood_worm\blood_worm_blood_consumption.dm" +#include "code\modules\antagonists\blood_worm\blood_worm_host_handling.dm" +#include "code\modules\antagonists\blood_worm\blood_worm_host_mob.dm" +#include "code\modules\antagonists\blood_worm\blood_worm_mob.dm" +#include "code\modules\antagonists\blood_worm\blood_worm_objectives.dm" +#include "code\modules\antagonists\blood_worm\blood_worm_team.dm" +#include "code\modules\antagonists\blood_worm\blood_worm_tester.dm" +#include "code\modules\antagonists\blood_worm\blood_worm_text_feedback.dm" +#include "code\modules\antagonists\blood_worm\actions\_base_action.dm" +#include "code\modules\antagonists\blood_worm\actions\cocoon.dm" +#include "code\modules\antagonists\blood_worm\actions\inject_blood.dm" +#include "code\modules\antagonists\blood_worm\actions\invade_corpse.dm" +#include "code\modules\antagonists\blood_worm\actions\leave_host.dm" +#include "code\modules\antagonists\blood_worm\actions\leech_blood.dm" +#include "code\modules\antagonists\blood_worm\actions\revive_host.dm" +#include "code\modules\antagonists\blood_worm\actions\spit_blood.dm" #include "code\modules\antagonists\brainwashing\brainwashing.dm" #include "code\modules\antagonists\brother\brother.dm" #include "code\modules\antagonists\changeling\cellular_emporium.dm" @@ -3329,6 +3353,7 @@ #include "code\modules\antagonists\changeling\powers\darkness_adaptation.dm" #include "code\modules\antagonists\changeling\powers\defib_grasp.dm" #include "code\modules\antagonists\changeling\powers\digitalcamo.dm" +#include "code\modules\antagonists\changeling\powers\expel_worm.dm" #include "code\modules\antagonists\changeling\powers\fakedeath.dm" #include "code\modules\antagonists\changeling\powers\fleshmend.dm" #include "code\modules\antagonists\changeling\powers\headcrab.dm" @@ -4747,6 +4772,7 @@ #include "code\modules\language\terrum.dm" #include "code\modules\language\uncommon.dm" #include "code\modules\language\voltaic.dm" +#include "code\modules\language\wormspeak.dm" #include "code\modules\language\xenocommon.dm" #include "code\modules\library\admin_only.dm" #include "code\modules\library\barcode_scanner.dm" diff --git a/tgui/packages/tgui/interfaces/AntagInfoBloodWorm.tsx b/tgui/packages/tgui/interfaces/AntagInfoBloodWorm.tsx new file mode 100644 index 00000000000..349f209d3a2 --- /dev/null +++ b/tgui/packages/tgui/interfaces/AntagInfoBloodWorm.tsx @@ -0,0 +1,166 @@ +import { Section, Stack, LabeledList } from 'tgui-core/components'; + +import { useBackend } from '../backend'; +import { Window } from '../layouts'; +import { type Objective, ObjectivePrintout } from './common/Objectives'; + +type Info = { + objectives: Objective[]; + team: Team; +}; + +type Team = { + blood_consumed_total: number; + times_reproduced_total: number; +}; + +export const AntagInfoBloodWorm = (props) => { + const { data } = useBackend(); + const { objectives, team } = data; + return ( + + +
+ + + You are a Blood Worm! + + + + + + + Your team status: + + - Total blood consumed: {team.blood_consumed_total} units
+ - Total times reproduced: {team.times_reproduced_total} times +
+
+
+ +
+ + + You are immune to low temperature, low pressure and don't need to breathe. + Your hosts don't have to breathe either, but their bodies remain vulnerable to space. + + + You can grow by using Leech Blood or Invade Corpse to consume blood, + but synthetic blood, such as from monkeys, has a limit. + You can see blood levels at a glance using your HUD, + and examining targets yields advanced information on their blood. + + + You can track your growth in your status tab. Once you're fully grown, + you can incubate in a cocoon to reach the next growth stage. + Reaching juvenile lets you spit blood, and adults are extremely strong. + + + Hatchlings can ventcrawl. Once you grow up, you lose the ability to ventcrawl, + becoming reliant on doorcrawling, breaking doors and ID access to move around. + Secure a good spot or a host before maturing to a juvenile. + + + Hatchlings and juveniles can slide under doors, but adults can't, + becoming entirely reliant on breaking doors and ID access to move around. + Secure a really good spot or a host before maturing to an adult. + + + Once you enter your final stage of growth as an adult, you can reproduce to create 3 new hatchlings, in exchange for reverting into one yourself. + + + You have the supernatural ability to turn into blood upon command. + This allows you to enter a corpse, taking it as your host. + + + You slowly heal over time, taking roughly 5 minutes to fully heal from near-death. While in a host, this translates to recovering blood volume over time. + You can inject blood into the damaged tissues of your host to rapidly heal them in exchange for your own health. + + + While inside of a host, your host doesn't need vital organs to survive, except for a brain. Lungs let you speak, and a liver lets you process chems. + You can insert organs into your host by picking them up and right-clicking on your host with them. This works for cybernetics too. + + + Your mind is not that of a human. You don't experience cravings, fear, sadness or joy from normal sources, even while in a host. + + +
+
+ +
+ + + Your species is averse to heat and will rapidly burn up in hot environments. Your body is also flammable, so stay away from fires. + This weakness applies even while in a host, but can be covered by equipping your host with fire-resistant gear. + + + While in a host, you are transformed entirely into blood. This renders you extremely vulnerable to bleeding wounds. + When your host bleeds, it directly damages you, and your hosts continue bleeding even while dead. + Your hosts also bleed 50% faster than normal people. + + + Your hosts lack your advanced senses, leaving them vulnerable to ordinary impediments like darkness and flashbangs. + You can use human tools like night-vision goggles, eye protection and ear protection to circumvent this. + + + The DeForest corporation has engineered single-use devices for detecting blood worms. They can be ordered by security + and take no time to use, but using them is painful for the target and they're really expensive. + + +
+
+ +
+ + + Using medbay to heal your host instead of spending blood will avoid leaving your host with deathly pale skin. + As a bonus, you get to have more blood for spitting and healing later. + + + You can leave your host to surprise attack people, which is especially effective as an adult. Be sure to block + off any paths of escape first, as it takes 3 seconds to leave your host. + + + As a hatchling or a juvenile, you can slide under a door to stalk nearby victims. Be wary of anyone coming to open the door. + + + Hosts are practically meatshields for you, just keep track of any fires, bleeding wounds, cremators or gibbing mechanisms. + Remember to also heal your host periodically using Inject Blood whenever they get too low. + + + Because you can't control corpses while you have less than a tenth of your health in blood volume, you will be ejected from your host when you get really low. + You can use this as insurance to get a last ditch escape attempt. + + + Medbay's blood freezers are all-you-can-eat buffets for you! They are only half synthetic and their high volumes grant you a lot of growth. + Be careful of nearby crew, as the sounds of breaking freezers can travel through walls. + + + You can enter corpses to consume all of their blood extremely quickly. Blood gained while already in a host doesn't affect your growth. + Duplicated blood does, but it's synthetic, limiting its effectiveness. + + + You have night vision and no territory, meaning you can thrive anywhere. Hatchlings can ventcrawl, while + juveniles and adults can break doors and other obstacles with their bare teeth. Nowhere is safe from you! + + + Your kin often share your goals and are willing to work together. You have your own language, Wormspeak, + which is only understandable to your kin. You can tell if someone is a blood worm host via a HUD icon. + + + Sec got you? You can break any restraints on your host, including lockers and such, by right-clicking with Spit Blood active. + You can also leave your host and ventcrawl out or break through the door. + + + As an adult, you can gather corpses for your offspring to use as hosts. Gathering blood can jumpstart the growth of your + hatchlings to juveniles, or even send them all the way to adulthood. + + +
+
+
+
+
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/bloodworm.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/bloodworm.ts new file mode 100644 index 00000000000..951e0f79614 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/bloodworm.ts @@ -0,0 +1,15 @@ +import { type Antagonist, Category } from '../base'; + +const BloodWorm: Antagonist = { + key: 'bloodworm', + name: 'Blood Worm', + description: [ + ` + Become a giant, parasitic blood worm. Start as a hatchling in a host, + consume blood and conquer the entire station alongside your siblings! + `, + ], + category: Category.Roundstart, +}; + +export default BloodWorm; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/bloodworminfestation.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/bloodworminfestation.ts new file mode 100644 index 00000000000..5c524bc68ed --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/bloodworminfestation.ts @@ -0,0 +1,15 @@ +import { type Antagonist, Category } from '../base'; + +const BloodWormInfestation: Antagonist = { + key: 'bloodworminfestation', + name: 'Blood Worm Infestation', + description: [ + ` + Become a giant, parasitic blood worm. Start as a hatchling, take hosts, + consume blood and conquer the entire station alongside your siblings! + `, + ], + category: Category.Midround, +}; + +export default BloodWormInfestation;