diff --git a/code/__DEFINES/bloodsuckers.dm b/code/__DEFINES/bloodsuckers.dm
new file mode 100644
index 0000000000000..a52a921ef30ac
--- /dev/null
+++ b/code/__DEFINES/bloodsuckers.dm
@@ -0,0 +1,169 @@
+///Uncomment this to enable testing of Bloodsucker features (such as vassalizing people with a mind instead of a client).
+//#define BLOODSUCKER_TESTING
+
+/**
+ * Blood-level defines
+ */
+/// Determines Bloodsucker regeneration rate
+#define BS_BLOOD_VOLUME_MAX_REGEN 700
+/// Cost to torture someone halfway, in blood. Called twice for full cost
+#define TORTURE_BLOOD_HALF_COST 8
+/// Cost to convert someone after successful torture, in blood
+#define TORTURE_CONVERSION_COST 50
+/// Once blood is this low, will enter Frenzy
+#define FRENZY_THRESHOLD_ENTER 25
+/// Once blood is this high, will exit Frenzyshak
+#define FRENZY_THRESHOLD_EXIT 250
+
+/// Minimum blood required for bloodsucker oozelings to auto-revive
+#define OOZELING_MIN_REVIVE_BLOOD_THRESHOLD (FRENZY_THRESHOLD_ENTER * 10)
+
+/**
+ * Vassal defines
+ */
+///If someone passes all checks and can be vassalized
+#define VASSALIZATION_ALLOWED 0
+///If someone has to accept vassalization
+#define VASSALIZATION_DISLOYAL 1
+///If someone is not allowed under any circimstances to become a Vassal
+#define VASSALIZATION_BANNED 2
+
+/**
+ * Cooldown defines
+ * Used in Cooldowns Bloodsuckers use to prevent spamming
+ */
+///Spam prevention for healing messages.
+#define BLOODSUCKER_SPAM_HEALING (15 SECONDS)
+///Span prevention for Sol Masquerade messages.
+#define BLOODSUCKER_SPAM_MASQUERADE (60 SECONDS)
+
+///Span prevention for Sol messages.
+#define BLOODSUCKER_SPAM_SOL (30 SECONDS)
+
+/**
+ * Clan defines
+ */
+#define CLAN_NONE "Caitiff"
+#define CLAN_BRUJAH "Brujah Clan"
+#define CLAN_TOREADOR "Toreador Clan"
+#define CLAN_NOSFERATU "Nosferatu Clan"
+#define CLAN_TREMERE "Tremere Clan"
+#define CLAN_GANGREL "Gangrel Clan"
+#define CLAN_VENTRUE "Ventrue Clan"
+#define CLAN_MALKAVIAN "Malkavian Clan"
+#define CLAN_TZIMISCE "Tzimisce Clan"
+
+#define TREMERE_VASSAL "tremere_vassal"
+#define FAVORITE_VASSAL "favorite_vassal"
+#define REVENGE_VASSAL "revenge_vassal"
+
+/**
+ * Power defines
+ */
+/// This Power can't be used in Torpor
+#define BP_CANT_USE_IN_TORPOR (1<<0)
+/// This Power can't be used in Frenzy.
+#define BP_CANT_USE_IN_FRENZY (1<<1)
+/// This Power can't be used with a stake in you
+#define BP_CANT_USE_WHILE_STAKED (1<<2)
+/// This Power can't be used while incapacitated
+#define BP_CANT_USE_WHILE_INCAPACITATED (1<<3)
+/// This Power can't be used while unconscious
+#define BP_CANT_USE_WHILE_UNCONSCIOUS (1<<4)
+/// This Power can't be used during Sol
+#define BP_CANT_USE_DURING_SOL (1<<5)
+
+/// This Power can be purchased by Bloodsuckers
+#define BLOODSUCKER_CAN_BUY (1<<0)
+/// This is a Default Power that all Bloodsuckers get.
+#define BLOODSUCKER_DEFAULT_POWER (1<<1)
+/// This Power can be purchased by Tremere Bloodsuckers
+#define TREMERE_CAN_BUY (1<<2)
+/// This Power can be purchased by Vassals
+#define VASSAL_CAN_BUY (1<<3)
+
+/// This Power is a Toggled Power
+#define BP_AM_TOGGLE (1<<0)
+/// This Power is a Single-Use Power
+#define BP_AM_SINGLEUSE (1<<1)
+/// This Power has a Static cooldown
+#define BP_AM_STATIC_COOLDOWN (1<<2)
+/// This Power doesn't cost bloot to run while unconscious
+#define BP_AM_COSTLESS_UNCONSCIOUS (1<<3)
+
+/**
+ * Bloodsucker Signals
+ */
+///Called when a Bloodsucker ranks up: (datum/bloodsucker_datum, mob/owner, mob/target)
+#define BLOODSUCKER_RANK_UP "bloodsucker_rank_up"
+///Called when a Bloodsucker interacts with a Vassal on their persuasion rack.
+#define BLOODSUCKER_INTERACT_WITH_VASSAL "bloodsucker_interact_with_vassal"
+///Called when a Bloodsucker makes a Vassal into their Favorite Vassal: (datum/vassal_datum, mob/master)
+#define BLOODSUCKER_MAKE_FAVORITE "bloodsucker_make_favorite"
+///Called when a new Vassal is successfully made: (datum/bloodsucker_datum)
+#define BLOODSUCKER_MADE_VASSAL "bloodsucker_made_vassal"
+///Called when a Bloodsucker exits Torpor.
+#define BLOODSUCKER_EXIT_TORPOR "bloodsucker_exit_torpor"
+///Called when a Bloodsucker reaches Final Death.
+#define BLOODSUCKER_FINAL_DEATH "bloodsucker_final_death"
+ ///Whether the Bloodsucker should not be dusted when arriving Final Death
+ #define DONT_DUST (1<<0)
+///Called when a Bloodsucker breaks the Masquerade
+#define COMSIG_BLOODSUCKER_BROKE_MASQUERADE "comsig_bloodsucker_broke_masquerade"
+///Called when a Bloodsucker enters Frenzy
+#define BLOODSUCKER_ENTERS_FRENZY "bloodsucker_enters_frenzy"
+///Called when a Bloodsucker exits Frenzy
+#define BLOODSUCKER_EXITS_FRENZY "bloodsucker_exits_frenzy"
+
+/**
+ * Sol signals & Defines
+ */
+#define COMSIG_SOL_RANKUP_BLOODSUCKERS "comsig_sol_rankup_bloodsuckers"
+#define COMSIG_SOL_RISE_TICK "comsig_sol_rise_tick"
+#define COMSIG_SOL_NEAR_START "comsig_sol_near_start"
+#define COMSIG_SOL_END "comsig_sol_end"
+///Sent when a warning for Sol is meant to go out: (danger_level, vampire_warning_message, vassal_warning_message)
+#define COMSIG_SOL_WARNING_GIVEN "comsig_sol_warning_given"
+///Called on a Bloodsucker's Lifetick.
+#define COMSIG_BLOODSUCKER_ON_LIFETICK "comsig_bloodsucker_on_lifetick"
+
+#define DANGER_LEVEL_FIRST_WARNING 1
+#define DANGER_LEVEL_SECOND_WARNING 2
+#define DANGER_LEVEL_THIRD_WARNING 3
+#define DANGER_LEVEL_SOL_ROSE 4
+#define DANGER_LEVEL_SOL_ENDED 5
+
+/**
+ * Clan defines
+ *
+ * This is stuff that is used solely by Clans for clan-related activity.
+ */
+///Drinks blood the normal Bloodsucker way.
+#define BLOODSUCKER_DRINK_NORMAL "bloodsucker_drink_normal"
+///Drinks blood but is snobby, refusing to drink from mindless
+#define BLOODSUCKER_DRINK_SNOBBY "bloodsucker_drink_snobby"
+///Drinks blood from disgusting creatures without Humanity consequences.
+#define BLOODSUCKER_DRINK_INHUMANELY "bloodsucker_drink_imhumanely"
+
+/**
+ * Role defines
+ */
+#define ROLE_BLOODSUCKER "Bloodsucker"
+#define ROLE_VAMPIRICACCIDENT "Vampiric Accident"
+#define ROLE_BLOODSUCKERBREAKOUT "Bloodsucker Breakout"
+#define ROLE_INFILTRATOR "Infiltrator"
+
+/**
+ * Macros
+ */
+///Whether a mob is a Bloodsucker
+#define IS_BLOODSUCKER(mob) (mob?.mind?.has_antag_datum(/datum/antagonist/bloodsucker))
+///Whether a mob is a Vassal
+#define IS_VASSAL(mob) (mob?.mind?.has_antag_datum(/datum/antagonist/vassal))
+///Whether a mob is a Favorite Vassal
+#define IS_FAVORITE_VASSAL(mob) (mob?.mind?.has_antag_datum(/datum/antagonist/vassal/favorite))
+///Whether a mob is a Revenge Vassal
+#define IS_REVENGE_VASSAL(mob) (mob?.mind?.has_antag_datum(/datum/antagonist/vassal/revenge))
+
+//Used in bloodsucker_life.dm
+#define MARTIALART_FRENZYGRAB "frenzy grabbing"
diff --git a/code/__DEFINES/role_preferences.dm b/code/__DEFINES/role_preferences.dm
index c6fb148b64708..020eee58f2b50 100644
--- a/code/__DEFINES/role_preferences.dm
+++ b/code/__DEFINES/role_preferences.dm
@@ -137,6 +137,7 @@ GLOBAL_LIST_INIT(special_roles, list(
ROLE_TRAITOR = 0,
ROLE_WIZARD = 14,
ROLE_SPY = 0,
+ ROLE_BLOODSUCKER = 0,
// Midround
ROLE_ABDUCTOR = 0,
@@ -158,12 +159,14 @@ GLOBAL_LIST_INIT(special_roles, list(
ROLE_SPIDER = 0,
ROLE_WIZARD_MIDROUND = 14,
ROLE_VOIDWALKER = 0,
+ ROLE_VAMPIRICACCIDENT = 0,
// Latejoin
ROLE_HERETIC_SMUGGLER = 0,
ROLE_PROVOCATEUR = 14,
ROLE_SYNDICATE_INFILTRATOR = 0,
ROLE_STOWAWAY_CHANGELING = 0,
+ ROLE_BLOODSUCKERBREAKOUT = 0,
// I'm not too sure why these are here, but they're not moving.
ROLE_GLITCH = 0,
diff --git a/code/__DEFINES/traits/sources.dm b/code/__DEFINES/traits/sources.dm
index c4e952ed77b7a..fb6c02a4852f1 100644
--- a/code/__DEFINES/traits/sources.dm
+++ b/code/__DEFINES/traits/sources.dm
@@ -315,3 +315,4 @@
/// Trait aquired from being painted a certain color
#define ATOM_COLOR_TRAIT "atom_color"
+
diff --git a/modular_bandastation/blood_suckers/_blood_suckers.dm b/modular_bandastation/blood_suckers/_blood_suckers.dm
new file mode 100644
index 0000000000000..a9f76ef4fd633
--- /dev/null
+++ b/modular_bandastation/blood_suckers/_blood_suckers.dm
@@ -0,0 +1,4 @@
+/datum/modpack/jobs
+ name = "Blood Sucker"
+ desc = "Портирование антаганиста бладсакер с монки."
+ author = "dwasint, Lime-7"
diff --git a/modular_bandastation/blood_suckers/_blood_suckers.dme b/modular_bandastation/blood_suckers/_blood_suckers.dme
new file mode 100644
index 0000000000000..eca89dcd88506
--- /dev/null
+++ b/modular_bandastation/blood_suckers/_blood_suckers.dme
@@ -0,0 +1,67 @@
+#include "_blood_suckers.dm"
+
+#include "code/bloodsucker/bloodsucker_conversion.dm"
+#include "code/bloodsucker/bloodsucker_datum.dm"
+#include "code/bloodsucker/bloodsucker_flaws.dm"
+#include "code/bloodsucker/bloodsucker_frenzy.dm"
+#include "code/bloodsucker/bloodsucker_guardian.dm"
+#include "code/bloodsucker/bloodsucker_hud.dm"
+#include "code/bloodsucker/bloodsucker_life.dm"
+#include "code/bloodsucker/bloodsucker_misc_procs.dm"
+#include "code/bloodsucker/bloodsucker_moodlets.dm"
+#include "code/bloodsucker/bloodsucker_names.dm"
+#include "code/bloodsucker/bloodsucker_objectives.dm"
+#include "code/bloodsucker/bloodsucker_overwrites.dm"
+#include "code/bloodsucker/bloodsucker_ruleset.dm"
+#include "code/bloodsucker/bloodsucker_shaded.dm"
+#include "code/bloodsucker/bloodsucker_sol.dm"
+#include "code/bloodsucker/bloodsucker_traumas.dm"
+
+#include "code/clans/_clan_base.dm"
+#include "code/clans/_clan_flavortext.dm"
+#include "code/clans/malkavian.dm"
+#include "code/clans/nosferatu.dm"
+#include "code/clans/tremere.dm"
+#include "code/clans/venture.dm"
+
+#include "code/controllers/sunlight.dm"
+
+#include "code/powers/targeted/_base_targeted.dm"
+#include "code/powers/targeted/brawn.dm"
+#include "code/powers/targeted/haste.dm"
+#include "code/powers/targeted/lunge.dm"
+#include "code/powers/targeted/mesmerize.dm"
+#include "code/powers/targeted/trespass.dm"
+
+#include "code/powers/tremere/_base_tremere.dm"
+#include "code/powers/tremere/auspex.dm"
+#include "code/powers/tremere/dominate.dm"
+#include "code/powers/tremere/thaumaturgey.dm"
+
+#include "code/powers/vassal/distress.dm"
+#include "code/powers/vassal/recuperate.dm"
+#include "code/powers/vassal/vassal_fold.dm"
+
+#include "code/powers/_base_power.dm"
+#include "code/powers/cloak.dm"
+#include "code/powers/feed.dm"
+#include "code/powers/fortitude.dm"
+#include "code/powers/go_home.dm"
+#include "code/powers/masquerade.dm"
+#include "code/powers/veil.dm"
+
+#include "code/structures/bloodsucker_coffin.dm"
+#include "code/structures/bloodsucker_crypt.dm"
+#include "code/structures/bloodsucker_objects.dm"
+#include "code/structures/bloodsucker_recipes.dm"
+
+#include "code/vassals/types/favorite.dm"
+#include "code/vassals/types/revenge.dm"
+
+#include "code/vassals/batform.dm"
+#include "code/vassals/ex_vassal.dm"
+#include "code/vassals/vassal_datum.dm"
+#include "code/vassals/vassal_misc_procs.dm"
+#include "code/vassals/vassal_pinpointer.dm"
+
+#include "code/bloodsucker_assets.dm"
diff --git a/modular_bandastation/blood_suckers/code/bloodsucker/bloodsucker_conversion.dm b/modular_bandastation/blood_suckers/code/bloodsucker/bloodsucker_conversion.dm
new file mode 100644
index 0000000000000..f18bb252a422f
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/bloodsucker/bloodsucker_conversion.dm
@@ -0,0 +1,122 @@
+/**
+ * Checks if the target has antag datums and, if so,
+ * are they allowed to be Vassalized, or not, or banned.
+ * Args:
+ * target - The person we check for antag datums.
+ */
+/datum/antagonist/bloodsucker/proc/AmValidAntag(mob/target)
+ . = VASSALIZATION_ALLOWED
+ if(!target.mind || HAS_MIND_TRAIT(target, TRAIT_UNCONVERTABLE))
+ return VASSALIZATION_BANNED
+
+ for(var/datum/antagonist/antag_datum as anything in target.mind.antag_datums)
+ if(antag_datum.type in vassal_banned_antags)
+ return VASSALIZATION_BANNED
+ return VASSALIZATION_DISLOYAL
+ if(HAS_TRAIT(target, TRAIT_MINDSHIELD))
+ return VASSALIZATION_DISLOYAL
+
+
+/**
+ * # can_make_vassal
+ * Checks if the person is allowed to turn into the Bloodsucker's
+ * Vassal, ensuring they are a player and valid.
+ * If they are a Vassal themselves, will check if their master
+ * has broken the Masquerade, to steal them.
+ * Args:
+ * conversion_target - Person being vassalized
+ */
+/datum/antagonist/bloodsucker/proc/can_make_vassal(mob/living/conversion_target)
+ if(!iscarbon(conversion_target))
+ return FALSE
+ if(length(vassals) == return_current_max_vassals())
+ to_chat(owner.current, span_danger("You find that your powers run thin and are unable to dominate their mind with your blood!"))
+ return FALSE
+ // No Mind!
+ if(!conversion_target.mind)
+ to_chat(owner.current, span_danger("[conversion_target] isn't self-aware enough to be made into a Vassal."))
+ return FALSE
+ if(AmValidAntag(conversion_target) == VASSALIZATION_BANNED)
+ to_chat(owner.current, span_danger("[conversion_target] resists the power of your blood to dominate their mind!"))
+ return FALSE
+ var/mob/living/master = conversion_target.mind.enslaved_to?.resolve()
+ if(!master || (master == owner.current))
+ return TRUE
+ var/datum/antagonist/bloodsucker/bloodsuckerdatum = master.mind.has_antag_datum(/datum/antagonist/bloodsucker)
+ if(bloodsuckerdatum?.broke_masquerade)
+ //vassal stealing
+ return TRUE
+ to_chat(owner.current, span_danger("[conversion_target]'s mind is overwhelmed with too much external force to put your own!"))
+ return FALSE
+
+/**
+ * This proc is responsible for calculating how many vassals you can have at any given
+ * time, ranges from 1 at 20 pop to 4 at 40 pop
+ */
+/datum/antagonist/bloodsucker/proc/return_current_max_vassals()
+ var/total_players = length(GLOB.joined_player_list)
+ switch(total_players)
+ if(1 to 20)
+ return 1
+ if(21 to 30)
+ return 3
+ else
+ return 4
+
+/**
+ * First will check if the target can be turned into a Vassal, if so then it will
+ * turn them into one, log it, sync their minds, then updates the Rank
+ * Args:
+ * conversion_target - The person converted.
+ */
+/datum/antagonist/bloodsucker/proc/make_vassal(mob/living/conversion_target)
+ if(!can_make_vassal(conversion_target))
+ return FALSE
+
+ //Check if they used to be a Vassal and was stolen.
+ if(IS_VASSAL(conversion_target))
+ conversion_target.mind.remove_antag_datum(/datum/antagonist/vassal)
+
+ var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.has_antag_datum(/datum/antagonist/bloodsucker)
+ bloodsuckerdatum.SelectTitle(am_fledgling = FALSE)
+
+ //set the master, then give the datum.
+ var/datum/antagonist/vassal/vassaldatum = new(conversion_target.mind)
+ vassaldatum.master = bloodsuckerdatum
+ conversion_target.mind.add_antag_datum(vassaldatum)
+
+ message_admins("[conversion_target] has become a Vassal, and is enslaved to [owner.current].")
+ log_admin("[conversion_target] has become a Vassal, and is enslaved to [owner.current].")
+ return TRUE
+
+/*
+ * # can_make_special
+ *
+ * MIND Helper proc that ensures the person can be a Special Vassal,
+ * without actually giving the antag datum to them.
+ * This is because Special Vassals get special abilities, without the unique Bloodsucker blood tracking,
+ * and we don't want this to be infinite.
+ * Args:
+ * creator - Person attempting to convert them.
+ */
+/datum/mind/proc/can_make_special(datum/mind/creator)
+ var/mob/living/user = current
+ if(!(user.mob_biotypes & MOB_ORGANIC))
+ if(creator)
+ to_chat(creator, span_danger("[user]'s DNA isn't compatible!"))
+ return FALSE
+ return TRUE
+
+/*
+ * # make_bloodsucker
+ *
+ * MIND Helper proc that turns the person into a Bloodsucker
+ * Args:
+ * creator - Person attempting to convert them.
+ */
+/datum/mind/proc/make_bloodsucker(datum/mind/creator)
+ var/datum/antagonist/bloodsuckerdatum = add_antag_datum(/datum/antagonist/bloodsucker)
+ if(bloodsuckerdatum && creator)
+ message_admins("[src] has become a Bloodsucker, and was created by [creator].")
+ log_admin("[src] has become a Bloodsucker, and was created by [creator].")
+ return bloodsuckerdatum
diff --git a/modular_bandastation/blood_suckers/code/bloodsucker/bloodsucker_datum.dm b/modular_bandastation/blood_suckers/code/bloodsucker/bloodsucker_datum.dm
new file mode 100644
index 0000000000000..caa6c9c0a5d61
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/bloodsucker/bloodsucker_datum.dm
@@ -0,0 +1,510 @@
+/datum/antagonist/bloodsucker
+ name = "\improper Bloodsucker"
+ show_in_antagpanel = TRUE
+ roundend_category = "bloodsuckers"
+ antagpanel_category = "Bloodsucker"
+ job_rank = ROLE_BLOODSUCKER
+ antag_hud_name = "bloodsucker"
+ show_name_in_check_antagonists = TRUE
+ can_coexist_with_others = FALSE
+ hijack_speed = 0.5
+ hud_icon = 'modular_bandastation/blood_suckers/icons/bloodsucker_icons.dmi'
+ ui_name = "AntagInfoBloodsucker"
+ preview_outfit = /datum/outfit/bloodsucker_outfit
+ /// How much blood we have, starting off at default blood levels.
+ var/bloodsucker_blood_volume = BLOOD_VOLUME_NORMAL
+ /// How much blood we can have at once, increases per level.
+ var/max_blood_volume = 600
+
+ var/datum/bloodsucker_clan/my_clan
+
+ // TIMERS //
+ ///Timer between alerts for Burn messages
+ COOLDOWN_DECLARE(bloodsucker_spam_sol_burn)
+ ///Timer between alerts for Healing messages
+ COOLDOWN_DECLARE(bloodsucker_spam_healing)
+ /// Cooldown for bloodsuckers going into Frenzy.
+ COOLDOWN_DECLARE(bloodsucker_frenzy_cooldown)
+
+ ///Used for assigning your name
+ var/bloodsucker_name
+ ///Used for assigning your title
+ var/bloodsucker_title
+ ///Used for assigning your reputation
+ var/bloodsucker_reputation
+
+ ///Amount of Humanity lost
+ var/humanity_lost = 0
+ ///Have we been broken the Masquerade?
+ var/broke_masquerade = FALSE
+ ///How many Masquerade Infractions do we have?
+ var/masquerade_infractions = 0
+ ///Blood required to enter Frenzy
+ var/frenzy_threshold = FRENZY_THRESHOLD_ENTER
+ ///If we are currently in a Frenzy
+ var/frenzied = FALSE
+ /// Whether the death handling code is active or not.
+ var/handling_death = FALSE
+
+ ///ALL Powers currently owned
+ var/list/datum/action/cooldown/bloodsucker/powers = list()
+ ///Frenzy Grab Martial art given to Bloodsuckers in a Frenzy
+ var/datum/martial_art/frenzygrab/frenzygrab = new
+
+ ///Vassals under my control. Periodically remove the dead ones.
+ var/list/datum/antagonist/vassal/vassals = list()
+ ///Special vassals I own, to not have double of the same type.
+ var/list/datum/antagonist/vassal/special_vassals = list()
+
+ var/bloodsucker_level = 0
+ var/bloodsucker_level_unspent = 1
+ var/additional_regen
+ var/bloodsucker_regen_rate = 0.3
+
+ // Used for Bloodsucker Objectives
+ var/area/bloodsucker_lair_area
+ var/obj/structure/closet/crate/coffin
+ var/total_blood_drank = 0
+
+ ///Blood display HUD
+ var/atom/movable/screen/bloodsucker/blood_counter/blood_display
+ ///Vampire level display HUD
+ var/atom/movable/screen/bloodsucker/rank_counter/vamprank_display
+ ///Sunlight timer HUD
+ var/atom/movable/screen/bloodsucker/sunlight_counter/sunlight_display
+
+ /// Static typecache of all bloodsucker powers.
+ var/static/list/all_bloodsucker_powers = typecacheof(/datum/action/cooldown/bloodsucker, ignore_root_path = TRUE)
+ /// Antagonists that cannot be Vassalized no matter what
+ var/static/list/vassal_banned_antags = list(
+ /datum/antagonist/bloodsucker,
+ /datum/antagonist/monsterhunter,
+ /datum/antagonist/changeling,
+ /datum/antagonist/cult,
+ )
+ ///Default Bloodsucker traits
+ var/static/list/bloodsucker_traits = list(
+ TRAIT_NOBREATH,
+ TRAIT_SLEEPIMMUNE,
+ TRAIT_NOCRITDAMAGE,
+ TRAIT_RESISTCOLD,
+ TRAIT_RADIMMUNE,
+ TRAIT_GENELESS,
+ TRAIT_STABLEHEART,
+ TRAIT_STABLELIVER,
+ TRAIT_NOSOFTCRIT,
+ TRAIT_NOHARDCRIT,
+ TRAIT_AGEUSIA,
+ TRAIT_COLD_BLOODED,
+ TRAIT_VIRUSIMMUNE,
+ TRAIT_TOXIMMUNE,
+ TRAIT_HARDLY_WOUNDED,
+ TRAIT_NO_MIRROR_REFLECTION,
+ TRAIT_ETHEREAL_NO_OVERCHARGE,
+ TRAIT_OOZELING_NO_CANNIBALIZE,
+ )
+ /// Traits applied during Torpor.
+ var/static/list/torpor_traits = list(
+ TRAIT_DEATHCOMA,
+ TRAIT_FAKEDEATH,
+ TRAIT_NODEATH,
+ TRAIT_RESISTHIGHPRESSURE,
+ TRAIT_RESISTLOWPRESSURE,
+ )
+ /// A typecache of organs we'll expel during Torpor.
+ var/static/list/yucky_organ_typecache = typecacheof(list(
+ /obj/item/organ/internal/body_egg,
+ /obj/item/organ/internal/zombie_infection,
+ ))
+
+/**
+ * Apply innate effects is everything given to the mob
+ * When a body is tranferred, this is called on the new mob
+ * while on_gain is called ONCE per ANTAG, this is called ONCE per BODY.
+ */
+/datum/antagonist/bloodsucker/apply_innate_effects(mob/living/mob_override)
+ . = ..()
+ var/mob/living/current_mob = mob_override || owner.current
+ RegisterSignal(current_mob, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
+ RegisterSignal(current_mob, COMSIG_LIVING_LIFE, PROC_REF(LifeTick))
+ RegisterSignal(current_mob, COMSIG_LIVING_DEATH, PROC_REF(on_death))
+ handle_clown_mutation(current_mob, mob_override ? null : "As a vampiric clown, you are no longer a danger to yourself. Your clownish nature has been subdued by your thirst for blood.")
+ add_team_hud(current_mob)
+ current_mob.clear_mood_event("vampcandle")
+
+ if(current_mob.hud_used)
+ on_hud_created()
+ else
+ RegisterSignal(current_mob, COMSIG_MOB_HUD_CREATED, PROC_REF(on_hud_created))
+#ifdef BLOODSUCKER_TESTING
+ var/turf/user_loc = get_turf(current_mob)
+ new /obj/structure/closet/crate/coffin(user_loc)
+ new /obj/structure/bloodsucker/vassalrack(user_loc)
+#endif
+
+/**
+ * Remove innate effects is everything given to the mob
+ * When a body is tranferred, this is called on the old mob.
+ * while on_removal is called ONCE per ANTAG, this is called ONCE per BODY.
+ */
+/datum/antagonist/bloodsucker/remove_innate_effects(mob/living/mob_override)
+ . = ..()
+ var/mob/living/current_mob = mob_override || owner.current
+ UnregisterSignal(current_mob, list(COMSIG_LIVING_LIFE, COMSIG_ATOM_EXAMINE, COMSIG_LIVING_DEATH))
+ handle_clown_mutation(current_mob, removing = FALSE)
+
+ if(current_mob.hud_used)
+ var/datum/hud/hud_used = current_mob.hud_used
+ hud_used.infodisplay -= blood_display
+ hud_used.infodisplay -= vamprank_display
+ hud_used.infodisplay -= sunlight_display
+ QDEL_NULL(blood_display)
+ QDEL_NULL(vamprank_display)
+ QDEL_NULL(sunlight_display)
+
+/datum/antagonist/bloodsucker/proc/on_hud_created(datum/source)
+ SIGNAL_HANDLER
+ var/datum/hud/bloodsucker_hud = owner.current.hud_used
+
+ blood_display = new /atom/movable/screen/bloodsucker/blood_counter()
+ blood_display.hud = bloodsucker_hud
+ bloodsucker_hud.infodisplay += blood_display
+
+ vamprank_display = new /atom/movable/screen/bloodsucker/rank_counter()
+ vamprank_display.hud = bloodsucker_hud
+ bloodsucker_hud.infodisplay += vamprank_display
+
+ sunlight_display = new /atom/movable/screen/bloodsucker/sunlight_counter()
+ sunlight_display.hud = bloodsucker_hud
+ bloodsucker_hud.infodisplay += sunlight_display
+
+ bloodsucker_hud.show_hud(bloodsucker_hud.hud_version)
+ UnregisterSignal(owner.current, COMSIG_MOB_HUD_CREATED)
+
+/datum/antagonist/bloodsucker/get_admin_commands()
+ . = ..()
+ .["Give Level"] = CALLBACK(src, PROC_REF(RankUp))
+ if(bloodsucker_level_unspent >= 1)
+ .["Remove Level"] = CALLBACK(src, PROC_REF(RankDown))
+
+ if(broke_masquerade)
+ .["Fix Masquerade"] = CALLBACK(src, PROC_REF(fix_masquerade))
+ else
+ .["Break Masquerade"] = CALLBACK(src, PROC_REF(break_masquerade))
+
+ if(my_clan)
+ .["Remove Clan"] = CALLBACK(src, PROC_REF(remove_clan))
+ else
+ .["Add Clan"] = CALLBACK(src, PROC_REF(admin_set_clan))
+
+///Called when you get the antag datum, called only ONCE per antagonist.
+/datum/antagonist/bloodsucker/on_gain()
+ RegisterSignal(SSsunlight, COMSIG_SOL_RANKUP_BLOODSUCKERS, PROC_REF(sol_rank_up))
+ RegisterSignal(SSsunlight, COMSIG_SOL_NEAR_START, PROC_REF(sol_near_start))
+ RegisterSignal(SSsunlight, COMSIG_SOL_END, PROC_REF(on_sol_end))
+ RegisterSignal(SSsunlight, COMSIG_SOL_RISE_TICK, PROC_REF(handle_sol))
+ RegisterSignal(SSsunlight, COMSIG_SOL_WARNING_GIVEN, PROC_REF(give_warning))
+
+ if(IS_FAVORITE_VASSAL(owner.current)) // Vassals shouldnt be getting the same benefits as Bloodsuckers.
+ bloodsucker_level_unspent = 0
+ show_in_roundend = FALSE
+ else
+ // Start Sunlight if first Bloodsucker
+ check_start_sunlight()
+ // Name and Titles
+ SelectFirstName()
+ SelectTitle(am_fledgling = TRUE)
+ SelectReputation(am_fledgling = TRUE)
+ // Objectives
+ forge_bloodsucker_objectives()
+
+ . = ..()
+ // Assign Powers
+ give_starting_powers()
+ assign_starting_stats()
+
+/// Called by the remove_antag_datum() and remove_all_antag_datums() mind procs for the antag datum to handle its own removal and deletion.
+/datum/antagonist/bloodsucker/on_removal()
+ UnregisterSignal(SSsunlight, list(COMSIG_SOL_RANKUP_BLOODSUCKERS, COMSIG_SOL_NEAR_START, COMSIG_SOL_END, COMSIG_SOL_RISE_TICK, COMSIG_SOL_WARNING_GIVEN))
+ clear_powers_and_stats()
+ check_cancel_sunlight() //check if sunlight should end
+ owner.special_role = null
+ return ..()
+
+/datum/antagonist/bloodsucker/on_body_transfer(mob/living/old_body, mob/living/new_body)
+ . = ..()
+ for(var/datum/action/cooldown/bloodsucker/all_powers as anything in powers)
+ if(old_body)
+ all_powers.Remove(old_body)
+ all_powers.Grant(new_body)
+ var/obj/item/bodypart/old_left_arm = old_body.get_bodypart(BODY_ZONE_L_ARM)
+ var/obj/item/bodypart/old_right_arm = old_body.get_bodypart(BODY_ZONE_R_ARM)
+ var/old_left_arm_unarmed_damage_low
+ var/old_left_arm_unarmed_damage_high
+ var/old_right_arm_unarmed_damage_low
+ var/old_right_arm_unarmed_damage_high
+ if(old_body && ishuman(old_body))
+ var/mob/living/carbon/human/old_user = old_body
+ var/datum/species/old_species = old_user.dna.species
+ old_species.inherent_traits -= TRAIT_DRINKS_BLOOD
+ //Keep track of what they were
+ old_left_arm_unarmed_damage_low = old_left_arm.unarmed_damage_low
+ old_left_arm_unarmed_damage_high = old_left_arm.unarmed_damage_high
+ old_right_arm_unarmed_damage_low = old_right_arm.unarmed_damage_low
+ old_right_arm_unarmed_damage_high = old_right_arm.unarmed_damage_high
+ //Then reset them
+ old_left_arm.unarmed_damage_low = initial(old_left_arm.unarmed_damage_low)
+ old_left_arm.unarmed_damage_high = initial(old_left_arm.unarmed_damage_high)
+ old_right_arm.unarmed_damage_low = initial(old_right_arm.unarmed_damage_low)
+ old_right_arm.unarmed_damage_high = initial(old_right_arm.unarmed_damage_high)
+ if(ishuman(new_body))
+ var/mob/living/carbon/human/new_user = new_body
+ var/datum/species/new_species = new_user.dna.species
+ new_species.inherent_traits += TRAIT_DRINKS_BLOOD
+ var/obj/item/bodypart/new_left_arm
+ var/obj/item/bodypart/new_right_arm
+ //Give old punch damage values
+ new_left_arm = new_body.get_bodypart(BODY_ZONE_L_ARM)
+ new_right_arm = new_body.get_bodypart(BODY_ZONE_R_ARM)
+ new_left_arm.unarmed_damage_low = old_left_arm_unarmed_damage_low
+ new_left_arm.unarmed_damage_high = old_left_arm_unarmed_damage_high
+ new_right_arm.unarmed_damage_low = old_right_arm_unarmed_damage_low
+ new_right_arm.unarmed_damage_high = old_right_arm_unarmed_damage_high
+
+ //Give Bloodsucker Traits
+ if(old_body)
+ old_body.remove_traits(bloodsucker_traits, BLOODSUCKER_TRAIT)
+ new_body.add_traits(bloodsucker_traits, BLOODSUCKER_TRAIT)
+
+/datum/antagonist/bloodsucker/greet()
+ . = ..()
+ var/fullname = return_full_name()
+ to_chat(owner, span_userdanger("You are [fullname], a strain of vampire known as a Bloodsucker!"))
+ owner.announce_objectives()
+ if(bloodsucker_level_unspent >= 2)
+ to_chat(owner, span_announce("As a latejoiner, you have [bloodsucker_level_unspent] bonus Ranks, entering your claimed coffin allows you to spend a Rank."))
+ owner.current.playsound_local(null, 'modular_bandastation/blood_suckers/sound/BloodsuckerAlert.ogg', 100, FALSE, pressure_affected = FALSE)
+ antag_memory += "Although you were born a mortal, in undeath you earned the name [fullname]. "
+
+/datum/antagonist/bloodsucker/farewell()
+ to_chat(owner.current, span_userdanger("With a snap, your curse has ended. You are no longer a Bloodsucker. You live once more!"))
+ // Refill with Blood so they don't instantly die.
+ if(!HAS_TRAIT(owner.current, TRAIT_NOBLOOD))
+ owner.current.blood_volume = max(owner.current.blood_volume, BLOOD_VOLUME_NORMAL)
+
+// Called when using admin tools to give antag status
+/datum/antagonist/bloodsucker/admin_add(datum/mind/new_owner, mob/admin)
+ var/levels = input("How many unspent Ranks would you like [new_owner] to have?","Bloodsucker Rank", bloodsucker_level_unspent) as null | num
+ var/msg = " made [key_name_admin(new_owner)] into \a [name]"
+ if(levels > 1)
+ bloodsucker_level_unspent = levels
+ msg += " with [levels] extra unspent Ranks."
+ message_admins("[key_name_admin(usr)][msg]")
+ log_admin("[key_name(usr)][msg]")
+ new_owner.add_antag_datum(src)
+
+/datum/antagonist/bloodsucker/get_preview_icon()
+
+ var/icon/final_icon = render_preview_outfit(/datum/outfit/bloodsucker_outfit)
+ final_icon.Blend(icon('icons/effects/blood.dmi', "uniformblood"), ICON_OVERLAY)
+
+ return finish_preview_icon(final_icon)
+
+/datum/antagonist/bloodsucker/ui_static_data(mob/user)
+ var/list/data = list()
+ //we don't need to update this that much.
+ data["in_clan"] = !!my_clan
+ var/list/clan_data = list()
+ if(my_clan)
+ clan_data["clan_name"] = my_clan.name
+ clan_data["clan_description"] = my_clan.description
+ clan_data["clan_icon"] = my_clan.join_icon_state
+
+ data["clan"] += list(clan_data)
+
+ for(var/datum/action/cooldown/bloodsucker/power as anything in powers)
+ var/list/power_data = list()
+
+ power_data["power_name"] = power.name
+ power_data["power_explanation"] = power.power_explanation
+ power_data["power_icon"] = power.button_icon_state
+
+ data["power"] += list(power_data)
+
+ return data + ..()
+
+/datum/antagonist/bloodsucker/ui_assets(mob/user)
+ return list(
+ get_asset_datum(/datum/asset/simple/bloodsucker_icons),
+ )
+
+/datum/antagonist/bloodsucker/ui_act(action, params, datum/tgui/ui)
+ . = ..()
+ if(.)
+ return
+
+ switch(action)
+ if("join_clan")
+ if(my_clan)
+ return
+ assign_clan_and_bane()
+ ui.send_full_update(force = TRUE)
+ return
+
+/datum/antagonist/bloodsucker/roundend_report()
+ var/list/report = list()
+
+ // Vamp name
+ report += " \[[return_full_name()]\]"
+ report += printplayer(owner)
+ if(my_clan)
+ report += "They were part of the [my_clan.name]!"
+
+ // Default Report
+ var/objectives_complete = TRUE
+ if(length(objectives))
+ report += printobjectives(objectives)
+ for(var/datum/objective/objective in objectives)
+ if(objective.objective_name == "Optional Objective")
+ continue
+ if(!objective.check_completion())
+ objectives_complete = FALSE
+ break
+
+ // Now list their vassals
+ if(length(vassals))
+ report += span_header("Their Vassals were...")
+ for(var/datum/antagonist/vassal/all_vassals as anything in vassals)
+ if(QDELETED(all_vassals?.owner))
+ continue
+ var/list/vassal_report = list()
+ vassal_report += "[all_vassals.owner.name]"
+
+ if(all_vassals.owner.assigned_role)
+ vassal_report += " the [all_vassals.owner.assigned_role.title]"
+ if(IS_FAVORITE_VASSAL(all_vassals.owner.current))
+ vassal_report += " and was the Favorite Vassal"
+ else if(IS_REVENGE_VASSAL(all_vassals.owner.current))
+ vassal_report += " and was the Revenge Vassal"
+ report += vassal_report.Join()
+
+ if(!length(objectives) || objectives_complete)
+ report += "The [name] was successful!"
+ else
+ report += "The [name] has failed!"
+
+ return report.Join(" ")
+
+/datum/antagonist/bloodsucker/proc/give_starting_powers()
+ for(var/datum/action/cooldown/bloodsucker/all_powers as anything in all_bloodsucker_powers)
+ if(!(initial(all_powers.purchase_flags) & BLOODSUCKER_DEFAULT_POWER))
+ continue
+ BuyPower(new all_powers)
+
+/datum/antagonist/bloodsucker/proc/assign_starting_stats()
+ //Traits: Species
+ var/mob/living/carbon/human/user = owner.current
+ if(ishuman(owner.current))
+ var/datum/species/user_species = user.dna.species
+ var/obj/item/bodypart/user_left_arm = user.get_bodypart(BODY_ZONE_L_ARM)
+ var/obj/item/bodypart/user_right_arm = user.get_bodypart(BODY_ZONE_R_ARM)
+ user_species.inherent_traits += TRAIT_DRINKS_BLOOD
+ user.dna?.remove_all_mutations()
+ user_left_arm.unarmed_damage_low += 1 //lowest possible punch damage - 0
+ user_left_arm.unarmed_damage_high += 1 //highest possible punch damage - 9
+ user_right_arm.unarmed_damage_low += 1 //lowest possible punch damage - 0
+ user_right_arm.unarmed_damage_high += 1 //highest possible punch damage - 9
+ //Give Bloodsucker Traits
+ owner.current.add_traits(bloodsucker_traits, BLOODSUCKER_TRAIT)
+ //Clear Addictions
+ for(var/addiction_type in subtypesof(/datum/addiction))
+ owner.current.mind.remove_addiction_points(addiction_type, MAX_ADDICTION_POINTS)
+ //No Skittish "People" allowed
+ if(HAS_TRAIT(owner.current, TRAIT_SKITTISH))
+ REMOVE_TRAIT(owner.current, TRAIT_SKITTISH, ROUNDSTART_TRAIT)
+ // Tongue & Language
+ owner.current.grant_all_languages(FALSE, FALSE, TRUE)
+ owner.current.grant_language(/datum/language/vampiric)
+ /// Clear Disabilities & Organs
+ heal_vampire_organs()
+
+/**
+ * ##clear_power_and_stats()
+ *
+ * Removes all Bloodsucker related Powers/Stats changes, setting them back to pre-Bloodsucker
+ * Order of steps and reason why:
+ * Remove clan - Clans like Nosferatu give Powers on removal, we have to make sure this is given before removing Powers.
+ * Powers - Remove all Powers, so things like Masquerade are off.
+ * Species traits, Traits, MaxHealth, Language - Misc stuff, has no priority.
+ * Organs - At the bottom to ensure everything that changes them has reverted themselves already.
+ * Update Sight - Done after Eyes are regenerated.
+ */
+/datum/antagonist/bloodsucker/proc/clear_powers_and_stats()
+ // Remove clan first
+ if(my_clan)
+ QDEL_NULL(my_clan)
+ // Powers
+ for(var/datum/action/cooldown/bloodsucker/all_powers as anything in powers)
+ RemovePower(all_powers)
+ /// Stats
+ if(ishuman(owner.current))
+ var/mob/living/carbon/human/user = owner.current
+ var/datum/species/user_species = user.dna.species
+ user_species.inherent_traits -= TRAIT_DRINKS_BLOOD
+ // Remove all bloodsucker traits
+ owner.current.remove_traits(bloodsucker_traits, BLOODSUCKER_TRAIT)
+ // Update Health
+ owner.current.setMaxHealth(initial(owner.current.maxHealth))
+ // Language
+ owner.current.remove_language(/datum/language/vampiric)
+ // Heart & Eyes
+ var/mob/living/carbon/user = owner.current
+ var/obj/item/organ/internal/heart/newheart = owner.current.get_organ_slot(ORGAN_SLOT_HEART)
+ if(newheart)
+ newheart.beating = initial(newheart.beating)
+ var/obj/item/organ/internal/eyes/user_eyes = user.get_organ_slot(ORGAN_SLOT_EYES)
+ if(user_eyes)
+ user_eyes.flash_protect = initial(user_eyes.flash_protect)
+ user_eyes.color_cutoffs = initial(user_eyes.color_cutoffs)
+ user_eyes.sight_flags = initial(user_eyes.sight_flags)
+ user.update_sight()
+
+/// Name shown on antag list
+/datum/antagonist/bloodsucker/antag_listing_name()
+ return ..() + "([return_full_name()])"
+
+/// Whatever interesting things happened to the antag admins should know about
+/// Include additional information about antag in this part
+/datum/antagonist/bloodsucker/antag_listing_status()
+ if(owner && !considered_alive(owner))
+ return "Final Death"
+ return ..()
+
+/datum/antagonist/bloodsucker/proc/forge_bloodsucker_objectives()
+ // Claim a Lair Objective
+ var/datum/objective/bloodsucker/lair/lair_objective = new
+ lair_objective.owner = owner
+ objectives += lair_objective
+ // Survive Objective
+ var/datum/objective/survive/bloodsucker/survive_objective = new
+ survive_objective.owner = owner
+ objectives += survive_objective
+
+ // Objective 1: Vassalize a Head/Command, or a specific target
+ switch(rand(1, 3))
+ if(1) // Conversion Objective
+ var/datum/objective/bloodsucker/conversion/chosen_subtype = pick(subtypesof(/datum/objective/bloodsucker/conversion))
+ var/datum/objective/bloodsucker/conversion/conversion_objective = new chosen_subtype
+ conversion_objective.owner = owner
+ conversion_objective.objective_name = "Optional Objective"
+ objectives += conversion_objective
+ if(2) // Heart Thief Objective
+ var/datum/objective/bloodsucker/heartthief/heartthief_objective = new
+ heartthief_objective.owner = owner
+ heartthief_objective.objective_name = "Optional Objective"
+ objectives += heartthief_objective
+ if(3) // Drink Blood Objective
+ var/datum/objective/bloodsucker/gourmand/gourmand_objective = new
+ gourmand_objective.owner = owner
+ gourmand_objective.objective_name = "Optional Objective"
+ objectives += gourmand_objective
diff --git a/modular_bandastation/blood_suckers/code/bloodsucker/bloodsucker_flaws.dm b/modular_bandastation/blood_suckers/code/bloodsucker/bloodsucker_flaws.dm
new file mode 100644
index 0000000000000..0c218d51879c1
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/bloodsucker/bloodsucker_flaws.dm
@@ -0,0 +1,44 @@
+/**
+ * Gives Bloodsuckers the ability to choose a Clan.
+ * If they are already in a Clan, or is in a Frenzy, they will not be able to do so.
+ * The arg is optional and should really only be an Admin setting a Clan for a player.
+ * If set however, it will give them the control of their Clan instead of the Bloodsucker.
+ * This is selected through a radial menu over the player's body, even when an Admin is setting it.
+ * Args:
+ * person_selecting - Mob override for stuff like Admins selecting someone's clan.
+ */
+/datum/antagonist/bloodsucker/proc/assign_clan_and_bane(mob/person_selecting)
+ if(my_clan || owner.current.has_status_effect(/datum/status_effect/frenzy))
+ return
+ person_selecting ||= owner.current
+
+ var/list/options = list()
+ var/list/radial_display = list()
+ for(var/datum/bloodsucker_clan/all_clans as anything in typesof(/datum/bloodsucker_clan))
+ if(!all_clans::joinable_clan) //flavortext only
+ continue
+ options[all_clans::name] = all_clans
+
+ var/datum/radial_menu_choice/option = new
+ option.image = image(icon = all_clans::join_icon, icon_state = all_clans::join_icon_state)
+ option.info = "[all_clans::name] - [span_boldnotice(all_clans::join_description)]"
+ radial_display[all_clans::name] = option
+
+ var/chosen_clan = show_radial_menu(person_selecting, owner.current, radial_display)
+ chosen_clan = options[chosen_clan]
+ if(QDELETED(src) || QDELETED(owner.current))
+ return FALSE
+ if(!chosen_clan)
+ to_chat(person_selecting, span_announce("You choose to remain ignorant, for now."))
+ return
+ my_clan = new chosen_clan(src)
+
+/datum/antagonist/bloodsucker/proc/remove_clan(mob/admin)
+ if(owner.current.has_status_effect(/datum/status_effect/frenzy))
+ to_chat(admin, span_announce("Removing a Bloodsucker from a Clan while they are in a Frenzy will break stuff, this action has been blocked."))
+ return
+ QDEL_NULL(my_clan)
+ to_chat(owner.current, span_announce("You have been forced out of your clan! You can re-enter one by regular means."))
+
+/datum/antagonist/bloodsucker/proc/admin_set_clan(mob/admin)
+ assign_clan_and_bane(admin)
diff --git a/modular_bandastation/blood_suckers/code/bloodsucker/bloodsucker_frenzy.dm b/modular_bandastation/blood_suckers/code/bloodsucker/bloodsucker_frenzy.dm
new file mode 100644
index 0000000000000..a3015f086b4f9
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/bloodsucker/bloodsucker_frenzy.dm
@@ -0,0 +1,102 @@
+/**
+ * # FrenzyGrab
+ *
+ * The martial art given to Bloodsuckers so they can instantly aggressively grab people.
+ */
+/datum/martial_art/frenzygrab
+ name = "Frenzy Grab"
+ id = MARTIALART_FRENZYGRAB
+
+/datum/martial_art/frenzygrab/grab_act(mob/living/user, mob/living/target)
+ if(user != target)
+ target.grabbedby(user)
+ target.grippedby(user, instant = TRUE)
+ return TRUE
+ return ..()
+
+/**
+ * # Status effect
+ *
+ * This is the status effect given to Bloodsuckers in a Frenzy
+ * This deals with everything entering/exiting Frenzy is meant to deal with.
+ */
+
+/atom/movable/screen/alert/status_effect/frenzy
+ name = "Frenzy"
+ desc = "You are in a Frenzy! You are entirely Feral and, depending on your Clan, fighting for your life!"
+ icon = 'modular_bandastation/blood_suckers/icons/actions_bloodsucker.dmi'
+ icon_state = "power_recover"
+ alerttooltipstyle = "cult"
+
+/datum/status_effect/frenzy
+ id = "Frenzy"
+ status_type = STATUS_EFFECT_UNIQUE
+ duration = STATUS_EFFECT_PERMANENT
+ tick_interval = 1 SECONDS
+ alert_type = /atom/movable/screen/alert/status_effect/frenzy
+ /// The stored Bloodsucker antag datum
+ var/datum/antagonist/bloodsucker/bloodsuckerdatum
+ /// Traits applied during Frenzy.
+ var/static/list/frenzy_traits = list(
+ TRAIT_BATON_RESISTANCE,
+ TRAIT_DEAF,
+ TRAIT_DISCOORDINATED_TOOL_USER,
+ TRAIT_IGNOREDAMAGESLOWDOWN
+ TRAIT_MUTE,
+ TRAIT_PUSHIMMUNE,
+ TRAIT_SLEEPIMMUNE,
+ TRAIT_STUNIMMUNE,
+ )
+
+/datum/status_effect/frenzy/get_examine_text()
+ return span_warning("[owner.p_They()] seem[owner.p_s()] inhumane and feral!")
+
+/atom/movable/screen/alert/status_effect/masquerade/MouseEntered(location,control,params)
+ desc = initial(desc)
+ return ..()
+
+/datum/status_effect/frenzy/on_apply()
+ var/mob/living/carbon/human/user = owner
+ bloodsuckerdatum = IS_BLOODSUCKER(user)
+
+ if(QDELETED(bloodsuckerdatum) || !COOLDOWN_FINISHED(bloodsuckerdatum, bloodsucker_frenzy_cooldown))
+ return FALSE
+
+ // Disable ALL Powers and notify their entry
+ bloodsuckerdatum.DisableAllPowers(forced = TRUE)
+ to_chat(owner, span_userdanger("Blood! You need Blood, now! You enter a total Frenzy!"))
+ to_chat(owner, span_announce("* Bloodsucker Tip: While in Frenzy, you instantly Aggresively grab, have stun resistance, cannot speak, hear, or use any powers outside of Feed and Trespass (If you have it)."))
+ owner.balloon_alert(owner, "you enter a frenzy!")
+ SEND_SIGNAL(bloodsuckerdatum, BLOODSUCKER_ENTERS_FRENZY)
+
+ // Give the other Frenzy effects
+ owner.add_traits(frenzy_traits, FRENZY_TRAIT)
+ owner.add_movespeed_modifier(/datum/movespeed_modifier/bloodsucker_frenzy)
+ bloodsuckerdatum.frenzygrab.teach(user, TRUE)
+ owner.add_client_colour(/datum/client_colour/cursed_heart_blood)
+ user.uncuff()
+ bloodsuckerdatum.frenzied = TRUE
+ return ..()
+
+/datum/status_effect/frenzy/on_remove()
+ if(bloodsuckerdatum?.frenzied)
+ var/mob/living/carbon/human/user = owner
+ owner.balloon_alert(owner, "you come back to your senses.")
+ owner.remove_traits(frenzy_traits, FRENZY_TRAIT)
+ owner.remove_movespeed_modifier(/datum/movespeed_modifier/bloodsucker_frenzy)
+ bloodsuckerdatum.frenzygrab.remove(user)
+ owner.remove_client_colour(/datum/client_colour/cursed_heart_blood)
+
+ SEND_SIGNAL(bloodsuckerdatum, BLOODSUCKER_EXITS_FRENZY)
+ bloodsuckerdatum.frenzied = FALSE
+ COOLDOWN_START(bloodsuckerdatum, bloodsucker_frenzy_cooldown, 30 SECONDS)
+ return ..()
+
+/datum/status_effect/frenzy/tick()
+ var/mob/living/carbon/human/user = owner
+ if(!bloodsuckerdatum?.frenzied)
+ return
+ user.adjustFireLoss(1.5 + (bloodsuckerdatum.humanity_lost / 10))
+
+/datum/movespeed_modifier/bloodsucker_frenzy
+ multiplicative_slowdown = -0.4
diff --git a/modular_bandastation/blood_suckers/code/bloodsucker/bloodsucker_guardian.dm b/modular_bandastation/blood_suckers/code/bloodsucker/bloodsucker_guardian.dm
new file mode 100644
index 0000000000000..006fc22913e65
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/bloodsucker/bloodsucker_guardian.dm
@@ -0,0 +1,105 @@
+///Bloodsuckers spawning a Guardian will get the Bloodsucker one instead.
+/obj/item/guardian_creator/attack_self(mob/living/user)
+ // If this code looks odd, it's because I'm intentionally inserting a hack,
+ // as I'm trying to avoid touching `guardian_creator.dm` in a major way. The
+ // intent with this hack is to force Bloodsuckers to always get a Timestop
+ // Guardian, no matter the item that a Bloodsucker uses to get a guardian.
+ //
+ // There is plans to refactor/modularization guardians, which will hopefully
+ // allow this all to happen without as much of a hack.
+
+ // START COPIED CODE FROM guardian_creator.dm
+ if(isguardian(user) && !allow_guardian)
+ balloon_alert(user, "can't do that!")
+ return
+ var/list/guardians = user.get_all_linked_holoparasites()
+ if(length(guardians) && !allow_multiple)
+ balloon_alert(user, "already have one!")
+ return
+ if(user.mind?.has_antag_datum(/datum/antagonist/changeling) && !allow_changeling)
+ to_chat(user, ling_failure)
+ return
+ if(used)
+ to_chat(user, used_message)
+ return
+ // END COPIED CODE FROM guardian_creator.dm
+
+ if (IS_BLOODSUCKER(user))
+ //var/mob/living/basic/guardian/standard/timestop/guardian_path = new(user, GUARDIAN_THEME_MAGIC)
+ var/mob/living/basic/guardian/guardian_path = /mob/living/basic/guardian/standard/timestop
+
+ // START COPIED CODE FROM guardian_creator.dm
+ used = TRUE
+ to_chat(user, use_message)
+ var/guardian_type_name = capitalize(initial(guardian_path.creator_name))
+ var/list/mob/dead/observer/candidates = SSpolling.poll_ghost_candidates(
+ "Do you want to play as [user.real_name]'s [guardian_type_name] [mob_name]?",
+ check_jobban = ROLE_PAI,
+ poll_time = 10 SECONDS,
+ ignore_category = POLL_IGNORE_HOLOPARASITE,
+ alert_pic = guardian_path,
+ role_name_text = "guardian spirit",
+ )
+ if(LAZYLEN(candidates))
+ var/mob/dead/observer/candidate = pick(candidates)
+ spawn_guardian(user, candidate, guardian_path)
+ else
+ to_chat(user, failure_message)
+ used = FALSE
+ // END COPIED CODE FROM guardian_creator.dm
+
+ return
+
+ // Call parent to deal with everyone else
+ return ..()
+
+/**
+ * The Guardian itself
+ */
+/mob/living/basic/guardian/standard/timestop
+ // Like Bloodsuckers do, you will take more damage to Burn and less to Brute
+ damage_coeff = list(BRUTE = 0.5, BURN = 2.5, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0)
+
+ creator_name = "Timestop"
+ creator_desc = "Devastating close combat attacks and high damage resistance. Can smash through weak walls and stop time."
+ creator_icon = "timestop"
+
+/mob/living/basic/guardian/standard/timestop/Initialize(mapload, theme)
+ //Wizard Holoparasite theme, just to be more visibly stronger than regular ones
+ theme = GLOB.guardian_themes[GUARDIAN_THEME_TECH]
+ . = ..()
+ var/datum/action/cooldown/spell/timestop/guardian/timestop_ability = new()
+ timestop_ability.Grant(src)
+
+/mob/living/basic/guardian/standard/timestop/set_summoner(mob/living/to_who, different_person = FALSE)
+ ..()
+ for(var/action in actions)
+ var/datum/action/cooldown/spell/timestop/guardian/timestop_ability = action
+ if(istype(timestop_ability))
+ timestop_ability.grant_summoner_immunity()
+
+/mob/living/basic/guardian/standard/timestop/cut_summoner(different_person = FALSE)
+ for(var/action in actions)
+ var/datum/action/cooldown/spell/timestop/guardian/timestop_ability = action
+ if(istype(timestop_ability))
+ timestop_ability.remove_summoner_immunity()
+ ..()
+
+///Guardian Timestop ability
+/datum/action/cooldown/spell/timestop/guardian
+ name = "Guardian Timestop"
+ desc = "This spell stops time for everyone except for you and your master, \
+ allowing you to move freely while your enemies and even projectiles are frozen."
+ cooldown_time = 60 SECONDS
+ spell_requirements = NONE
+ invocation_type = INVOCATION_NONE
+
+/datum/action/cooldown/spell/timestop/guardian/proc/grant_summoner_immunity()
+ var/mob/living/basic/guardian/standard/timestop/bloodsucker_guardian = owner
+ if(bloodsucker_guardian && istype(bloodsucker_guardian) && bloodsucker_guardian.summoner)
+ ADD_TRAIT(bloodsucker_guardian.summoner, TRAIT_TIME_STOP_IMMUNE, REF(src))
+
+/datum/action/cooldown/spell/timestop/guardian/proc/remove_summoner_immunity()
+ var/mob/living/basic/guardian/standard/timestop/bloodsucker_guardian = owner
+ if(bloodsucker_guardian && istype(bloodsucker_guardian) && bloodsucker_guardian.summoner)
+ REMOVE_TRAIT(bloodsucker_guardian.summoner, TRAIT_TIME_STOP_IMMUNE, REF(src))
diff --git a/modular_bandastation/blood_suckers/code/bloodsucker/bloodsucker_hud.dm b/modular_bandastation/blood_suckers/code/bloodsucker/bloodsucker_hud.dm
new file mode 100644
index 0000000000000..30938072c90c2
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/bloodsucker/bloodsucker_hud.dm
@@ -0,0 +1,88 @@
+/// 1 tile down
+#define UI_BLOOD_DISPLAY "WEST:6,CENTER-1:0"
+/// 2 tiles down
+#define UI_VAMPRANK_DISPLAY "WEST:6,CENTER-2:-5"
+/// 6 pixels to the right, zero tiles & 5 pixels DOWN.
+#define UI_SUNLIGHT_DISPLAY "WEST:6,CENTER-0:0"
+
+///Maptext define for Bloodsucker HUDs
+#define FORMAT_BLOODSUCKER_HUD_TEXT(valuecolor, value) MAPTEXT("
SINGLE USE: [name] can only be used once per night."
+ if(rebuild)
+ build_all_button_icons(UPDATE_BUTTON_NAME)
+
+/datum/action/cooldown/bloodsucker/Destroy()
+ bloodsuckerdatum_power = null
+ return ..()
+
+/datum/action/cooldown/bloodsucker/IsAvailable(feedback = FALSE)
+ return COOLDOWN_FINISHED(src, next_use_time)
+
+/datum/action/cooldown/bloodsucker/Grant(mob/user)
+ . = ..()
+ find_bloodsucker_datum()
+
+//This is when we CLICK on the ability Icon, not USING.
+/datum/action/cooldown/bloodsucker/Trigger(trigger_flags, atom/target)
+ find_bloodsucker_datum()
+ if(active && can_deactivate()) // Active? DEACTIVATE AND END!
+ DeactivatePower()
+ return FALSE
+ if(!can_pay_cost() || !can_use(owner, trigger_flags))
+ return FALSE
+ if(SEND_SIGNAL(src, COMSIG_ACTION_TRIGGER, src) & COMPONENT_ACTION_BLOCK_TRIGGER)
+ return FALSE
+ pay_cost()
+ ActivatePower(trigger_flags)
+ if(!(power_flags & BP_AM_TOGGLE) || !active)
+ StartCooldown()
+ return TRUE
+
+/datum/action/cooldown/bloodsucker/proc/find_bloodsucker_datum()
+ bloodsuckerdatum_power ||= IS_BLOODSUCKER(owner)
+
+/datum/action/cooldown/bloodsucker/proc/can_pay_cost()
+ if(QDELETED(owner) || QDELETED(owner.mind))
+ return FALSE
+ // Cooldown?
+ if(!COOLDOWN_FINISHED(src, next_use_time))
+ owner.balloon_alert(owner, "power unavailable!")
+ return FALSE
+ if(!bloodsuckerdatum_power)
+ var/mob/living/living_owner = owner
+ if(!HAS_TRAIT(living_owner, TRAIT_NOBLOOD) && living_owner.blood_volume < bloodcost)
+ to_chat(owner, span_warning("You need at least [bloodcost] blood to activate [name]"))
+ return FALSE
+ return TRUE
+
+ // Have enough blood? Bloodsuckers in a Frenzy don't need to pay them
+ if(bloodsuckerdatum_power.frenzied)
+ return TRUE
+ if(bloodsuckerdatum_power.bloodsucker_blood_volume < bloodcost)
+ to_chat(owner, span_warning("You need at least [bloodcost] blood to activate [name]"))
+ return FALSE
+ return TRUE
+
+///Called when the Power is upgraded.
+/datum/action/cooldown/bloodsucker/proc/upgrade_power()
+ level_current++
+
+///Checks if the Power is available to use.
+/datum/action/cooldown/bloodsucker/proc/can_use(mob/living/carbon/user, trigger_flags)
+ if(QDELETED(owner))
+ return FALSE
+ if(!isliving(user))
+ return FALSE
+ // Torpor?
+ if((check_flags & BP_CANT_USE_IN_TORPOR) && bloodsuckerdatum_power?.is_in_torpor())
+ to_chat(user, span_warning("Not while you're in Torpor."))
+ return FALSE
+ // Frenzy?
+ if((check_flags & BP_CANT_USE_IN_FRENZY) && (bloodsuckerdatum_power?.frenzied))
+ to_chat(user, span_warning("You cannot use powers while in a Frenzy!"))
+ return FALSE
+ // Stake?
+ if((check_flags & BP_CANT_USE_WHILE_STAKED) && user.am_staked())
+ to_chat(user, span_warning("You have a stake in your chest! Your powers are useless."))
+ return FALSE
+ // Conscious? -- We use our own (AB_CHECK_CONSCIOUS) here so we can control it more, like the error message.
+ if((check_flags & BP_CANT_USE_WHILE_UNCONSCIOUS) && user.stat != CONSCIOUS)
+ to_chat(user, span_warning("You can't do this while you are unconcious!"))
+ return FALSE
+ // Incapacitated?
+ if((check_flags & BP_CANT_USE_WHILE_INCAPACITATED) && (user.incapacitated(IGNORE_RESTRAINTS | IGNORE_GRAB)))
+ to_chat(user, span_warning("Not while you're incapacitated!"))
+ return FALSE
+ // Constant Cost (out of blood)
+ if(constant_bloodcost > 0)
+ var/can_upkeep = bloodsuckerdatum_power ? (bloodsuckerdatum_power.bloodsucker_blood_volume > 0) : (HAS_TRAIT(user, TRAIT_NOBLOOD) || (user.blood_volume > (bloodcost + BLOOD_VOLUME_OKAY)))
+ if(!can_upkeep)
+ to_chat(user, span_warning("You don't have the blood to upkeep [src]!"))
+ return FALSE
+ if((check_flags & BP_CANT_USE_DURING_SOL) && user.has_status_effect(/datum/status_effect/bloodsucker_sol))
+ to_chat(user, span_warning("You can't use [src] during Sol!"))
+ return FALSE
+ return TRUE
+
+/// NOTE: With this formula, you'll hit half cooldown at level 8 for that power.
+/datum/action/cooldown/bloodsucker/StartCooldown()
+ // Calculate Cooldown (by power's level)
+ if(power_flags & BP_AM_STATIC_COOLDOWN)
+ cooldown_time = initial(cooldown_time)
+ else
+ cooldown_time = max(initial(cooldown_time) / 2, initial(cooldown_time) - (initial(cooldown_time) / 16 * (level_current - 1)))
+
+ return ..()
+
+/datum/action/cooldown/bloodsucker/proc/can_deactivate()
+ return TRUE
+
+/datum/action/cooldown/bloodsucker/is_action_active()
+ return active
+
+/datum/action/cooldown/bloodsucker/proc/pay_cost()
+ // Non-bloodsuckers will pay in other ways.
+ if(!bloodsuckerdatum_power)
+ var/mob/living/living_owner = owner
+ if(!HAS_TRAIT(living_owner, TRAIT_NOBLOOD))
+ living_owner.blood_volume -= bloodcost
+ return
+ // Bloodsuckers in a Frenzy don't have enough Blood to pay it, so just don't.
+ if(bloodsuckerdatum_power.frenzied)
+ return
+ bloodsuckerdatum_power.bloodsucker_blood_volume -= bloodcost
+ bloodsuckerdatum_power.update_hud()
+
+/datum/action/cooldown/bloodsucker/proc/ActivatePower(trigger_flags)
+ active = TRUE
+ if(power_flags & BP_AM_TOGGLE)
+ START_PROCESSING(SSprocessing, src)
+
+ owner.log_message("used [src][bloodcost != 0 ? " at the cost of [bloodcost]" : ""].", LOG_ATTACK, color="red")
+ build_all_button_icons()
+
+/datum/action/cooldown/bloodsucker/proc/DeactivatePower()
+ if(!active) //Already inactive? Return
+ return
+ if(power_flags & BP_AM_TOGGLE)
+ STOP_PROCESSING(SSprocessing, src)
+ if(power_flags & BP_AM_SINGLEUSE)
+ remove_after_use()
+ return
+ active = FALSE
+ StartCooldown()
+ build_all_button_icons()
+
+///Used by powers that are continuously active (That have BP_AM_TOGGLE flag)
+/datum/action/cooldown/bloodsucker/process(seconds_per_tick)
+ SHOULD_CALL_PARENT(TRUE) //Need this to call parent so the cooldown system works
+ . = ..()
+ if(!active) // if we're not active anyways, then we shouldn't be processing!!!
+ return
+ if(!ContinueActive(owner)) // We can't afford the Power? Deactivate it.
+ DeactivatePower()
+ return
+ // We can keep this up (For now), so Pay Cost!
+ if(!(power_flags & BP_AM_COSTLESS_UNCONSCIOUS) && owner.stat != CONSCIOUS)
+ if(bloodsuckerdatum_power)
+ bloodsuckerdatum_power.AddBloodVolume(-constant_bloodcost)
+ else
+ var/mob/living/living_owner = owner
+ if(!HAS_TRAIT(living_owner, TRAIT_NOBLOOD))
+ living_owner.blood_volume -= constant_bloodcost
+ return TRUE
+
+/// Checks to make sure this power can stay active
+/datum/action/cooldown/bloodsucker/proc/ContinueActive(mob/living/user, mob/living/target)
+ if(QDELETED(user))
+ return FALSE
+ if(!constant_bloodcost > 0 || bloodsuckerdatum_power.bloodsucker_blood_volume > 0)
+ return TRUE
+
+/// Used to unlearn Single-Use Powers
+/datum/action/cooldown/bloodsucker/proc/remove_after_use()
+ bloodsuckerdatum_power?.powers -= src
+ Remove(owner)
diff --git a/modular_bandastation/blood_suckers/code/powers/cloak.dm b/modular_bandastation/blood_suckers/code/powers/cloak.dm
new file mode 100644
index 0000000000000..00899ef1f162e
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/powers/cloak.dm
@@ -0,0 +1,78 @@
+/datum/action/cooldown/bloodsucker/cloak
+ name = "Cloak of Darkness"
+ desc = "Blend into the shadows and become invisible to the untrained and Artificial eye."
+ button_icon_state = "power_cloak"
+ power_explanation = "Cloak of Darkness:\n\
+ Activate this Power in the shadows and you will slowly turn nearly invisible.\n\
+ While using Cloak of Darkness, attempting to run will crush you.\n\
+ Additionally, while Cloak is active, you are completely invisible to the AI.\n\
+ Higher levels will increase how invisible you are."
+ power_flags = BP_AM_TOGGLE
+ check_flags = BP_CANT_USE_IN_TORPOR | BP_CANT_USE_IN_FRENZY | BP_CANT_USE_WHILE_UNCONSCIOUS
+ purchase_flags = BLOODSUCKER_CAN_BUY | VASSAL_CAN_BUY
+ bloodcost = 5
+ constant_bloodcost = 0.2
+ sol_multiplier = 2.5
+ cooldown_time = 5 SECONDS
+ var/was_running
+
+/// Must have nobody around to see the cloak
+/datum/action/cooldown/bloodsucker/cloak/can_use(mob/living/carbon/user, trigger_flags)
+ . = ..()
+ if(!.)
+ return FALSE
+ for(var/mob/living/watcher in viewers(9, owner) - owner)
+ if(watcher.stat == DEAD || QDELETED(watcher.client))
+ continue
+ if(IS_BLOODSUCKER(watcher) || IS_VASSAL(watcher))
+ continue
+ if(watcher.is_blind())
+ continue
+ owner.balloon_alert(owner, "you can only vanish unseen.")
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/bloodsucker/cloak/ActivatePower(trigger_flags)
+ . = ..()
+ var/mob/living/user = owner
+ was_running = ((user.m_intent == MOVE_INTENT_RUN) || user.m_intent == MOVE_INTENT_SPRINT)
+ if(was_running)
+ user.set_move_intent(MOVE_INTENT_WALK)
+ ADD_TRAIT(user, TRAIT_NO_SPRINT, BLOODSUCKER_TRAIT)
+ user.AddElement(/datum/element/digitalcamo)
+ user.balloon_alert(user, "cloak turned on.")
+
+/datum/action/cooldown/bloodsucker/cloak/process(seconds_per_tick)
+ // Checks that we can keep using this.
+ . = ..()
+ if(!.)
+ return
+ if(!active)
+ return
+ var/mob/living/user = owner
+ animate(user, alpha = max(25, owner.alpha - min(75, 10 + 5 * level_current)), time = 1.5 SECONDS)
+ // Prevents running while on Cloak of Darkness
+ if(user.m_intent != MOVE_INTENT_WALK)
+ owner.balloon_alert(owner, "you attempt to run, crushing yourself.")
+ user.set_move_intent(MOVE_INTENT_WALK)
+ user.adjustBruteLoss(rand(5,15))
+
+/datum/action/cooldown/bloodsucker/cloak/ContinueActive(mob/living/user, mob/living/target)
+ . = ..()
+ if(!.)
+ return FALSE
+ /// Must be CONSCIOUS
+ if(user.stat != CONSCIOUS)
+ to_chat(owner, span_warning("Your cloak failed due to you falling unconcious!"))
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/bloodsucker/cloak/DeactivatePower()
+ var/mob/living/user = owner
+ animate(user, alpha = 255, time = 1 SECONDS)
+ user.RemoveElement(/datum/element/digitalcamo)
+ if(was_running && user.m_intent == MOVE_INTENT_WALK)
+ user.set_move_intent(MOVE_INTENT_RUN)
+ user.balloon_alert(user, "cloak turned off.")
+ REMOVE_TRAIT(user, TRAIT_NO_SPRINT, BLOODSUCKER_TRAIT)
+ return ..()
diff --git a/modular_bandastation/blood_suckers/code/powers/feed.dm b/modular_bandastation/blood_suckers/code/powers/feed.dm
new file mode 100644
index 0000000000000..bd3e5d76a1df4
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/powers/feed.dm
@@ -0,0 +1,256 @@
+#define FEED_NOTICE_RANGE 2
+#define FEED_DEFAULT_TIMER (10 SECONDS)
+
+/datum/action/cooldown/bloodsucker/feed
+ name = "Feed"
+ desc = "Feed blood off of a living creature."
+ button_icon_state = "power_feed"
+ power_explanation = "Feed:\n\
+ Activate Feed while next to someone and you will begin to feed blood off of them.\n\
+ The time needed before you start feeding speeds up the higher level you are.\n\
+ Feeding off of someone while you have them aggressively grabbed will put them to sleep.\n\
+ While feeding, you can't speak, as your mouth is covered.\n\
+ Feeding while nearby (2 tiles away from) a mortal who is unaware of Bloodsuckers' existence, will cause a Masquerade Infraction\n\
+ If you get too many Masquerade Infractions, you will break the Masquerade.\n\
+ If you are in desperate need of blood, mice can be fed off of, at a cost."
+ power_flags = BP_AM_TOGGLE | BP_AM_STATIC_COOLDOWN
+ check_flags = BP_CANT_USE_IN_TORPOR | BP_CANT_USE_WHILE_STAKED | BP_CANT_USE_WHILE_INCAPACITATED | BP_CANT_USE_WHILE_UNCONSCIOUS
+ purchase_flags = BLOODSUCKER_CAN_BUY | BLOODSUCKER_DEFAULT_POWER
+ bloodcost = 0
+ cooldown_time = 15 SECONDS
+ ///Amount of blood taken, reset after each Feed. Used for logging.
+ var/blood_taken = 0
+ ///The amount of Blood a target has since our last feed, this loops and lets us not spam alerts of low blood.
+ var/warning_target_bloodvol = BLOOD_VOLUME_MAX_LETHAL
+ ///Reference to the target we've fed off of
+ var/datum/weakref/target_ref
+ /// Whether the target was alive or not when we started feeding.
+ var/started_alive = TRUE
+ ///Are we feeding with passive grab or not?
+ var/silent_feed = TRUE
+
+/datum/action/cooldown/bloodsucker/feed/can_use(mob/living/carbon/user, trigger_flags)
+ . = ..()
+ if(!.)
+ return FALSE
+ if(target_ref) //already sucking blood.
+ return FALSE
+ if(user.is_mouth_covered() && !isplasmaman(user))
+ owner.balloon_alert(owner, "mouth covered!")
+ return FALSE
+ //Find target, it will alert what the problem is, if any.
+ if(!find_target())
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/bloodsucker/feed/ContinueActive(mob/living/user, mob/living/target)
+ if(QDELETED(user) || QDELETED(target))
+ return FALSE
+ if(!user.Adjacent(target))
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/bloodsucker/feed/DeactivatePower()
+ var/mob/living/user = owner
+ var/mob/living/feed_target = target_ref?.resolve()
+ if(!QDELETED(feed_target))
+ log_combat(user, feed_target, "fed on blood", addition="(and took [blood_taken] blood)")
+ to_chat(user, span_notice("You slowly release [feed_target]."))
+ if(feed_target.stat == DEAD && !started_alive)
+ user.add_mood_event("drankkilled", /datum/mood_event/drankkilled)
+ bloodsuckerdatum_power.AddHumanityLost(10)
+
+ target_ref = null
+ started_alive = TRUE
+ warning_target_bloodvol = BLOOD_VOLUME_MAX_LETHAL
+ blood_taken = 0
+ REMOVE_TRAIT(user, TRAIT_IMMOBILIZED, FEED_TRAIT)
+ REMOVE_TRAIT(user, TRAIT_MUTE, FEED_TRAIT)
+ return ..()
+
+/datum/action/cooldown/bloodsucker/feed/ActivatePower(trigger_flags)
+ var/mob/living/feed_target = target_ref.resolve()
+ if(istype(feed_target, /mob/living/basic/mouse))
+ to_chat(owner, span_notice("You recoil at the taste of a lesser lifeform."))
+ if(bloodsuckerdatum_power.my_clan && bloodsuckerdatum_power.my_clan.blood_drink_type != BLOODSUCKER_DRINK_INHUMANELY)
+ var/mob/living/user = owner
+ user.add_mood_event("drankblood", /datum/mood_event/drankblood_bad)
+ bloodsuckerdatum_power.AddHumanityLost(1)
+ bloodsuckerdatum_power.AddBloodVolume(25)
+ DeactivatePower()
+ feed_target.death()
+ return
+ var/feed_timer = clamp(round(FEED_DEFAULT_TIMER / (1.25 * (level_current || 1))), 1, FEED_DEFAULT_TIMER)
+ if(bloodsuckerdatum_power.frenzied)
+ feed_timer = 2 SECONDS
+
+ owner.balloon_alert(owner, "feeding off [feed_target]...")
+ started_alive = (feed_target.stat < HARD_CRIT)
+ if(!do_after(owner, feed_timer, feed_target, NONE, TRUE))
+ owner.balloon_alert(owner, "feed stopped")
+ DeactivatePower()
+ return
+ if(owner.pulling == feed_target && owner.grab_state >= GRAB_AGGRESSIVE)
+ if(!IS_BLOODSUCKER(feed_target) && !IS_VASSAL(feed_target) && !IS_MONSTERHUNTER(feed_target))
+ feed_target.Unconscious((5 + level_current) SECONDS)
+ if(!feed_target.density)
+ feed_target.Move(owner.loc)
+ owner.visible_message(
+ span_warning("[owner] closes [owner.p_their()] mouth around [feed_target]'s neck!"),
+ span_warning("You sink your fangs into [feed_target]'s neck."))
+ silent_feed = FALSE //no more mr nice guy
+ else
+ // Only people who AREN'T the target will notice this action.
+ var/dead_message = feed_target.stat != DEAD ? " [feed_target.p_they(TRUE)] looks dazed, and will not remember this." : ""
+ owner.visible_message(
+ span_notice("[owner] puts [feed_target]'s wrist up to [owner.p_their()] mouth."), \
+ span_notice("You slip your fangs into [feed_target]'s wrist.[dead_message]"), \
+ vision_distance = FEED_NOTICE_RANGE, ignored_mobs = feed_target)
+
+ //check if we were seen
+ for(var/mob/living/watchers in oviewers(FEED_NOTICE_RANGE) - feed_target)
+ if(QDELETED(watchers.client))
+ continue
+ if(watchers.has_unlimited_silicon_privilege)
+ continue
+ if(watchers.stat >= DEAD)
+ continue
+ if(watchers.is_blind() || watchers.is_nearsighted_currently())
+ continue
+ if(IS_BLOODSUCKER(watchers) || IS_VASSAL(watchers) || HAS_MIND_TRAIT(watchers, TRAIT_OCCULTIST))
+ continue
+ owner.balloon_alert(owner, "feed noticed!")
+ bloodsuckerdatum_power.give_masquerade_infraction()
+ break
+
+ ADD_TRAIT(owner, TRAIT_MUTE, FEED_TRAIT)
+ ADD_TRAIT(owner, TRAIT_IMMOBILIZED, FEED_TRAIT)
+ return ..()
+
+/datum/action/cooldown/bloodsucker/feed/process(seconds_per_tick)
+ if(!active) //If we aren't active (running on SSfastprocess)
+ return ..() //Manage our cooldown timers
+ var/mob/living/user = owner
+ var/mob/living/feed_target = target_ref?.resolve()
+ if(QDELETED(feed_target))
+ DeactivatePower()
+ return PROCESS_KILL
+ if(!ContinueActive(user, feed_target))
+ if(!silent_feed)
+ user.visible_message(
+ span_warning("[user] is ripped from [feed_target]'s throat. [feed_target.p_their(TRUE)] blood sprays everywhere!"),
+ span_warning("Your teeth are ripped from [feed_target]'s throat. [feed_target.p_their(TRUE)] blood sprays everywhere!"))
+ // Deal Damage to Target (should have been more careful!)
+ if(iscarbon(feed_target))
+ var/mob/living/carbon/carbon_target = feed_target
+ carbon_target.bleed(15)
+ playsound(get_turf(feed_target), 'sound/effects/splat.ogg', 40, TRUE)
+ if(ishuman(feed_target))
+ var/mob/living/carbon/human/target_user = feed_target
+ var/obj/item/bodypart/head_part = target_user.get_bodypart(BODY_ZONE_HEAD)
+ if(head_part)
+ head_part.generic_bleedstacks += 5
+ feed_target.add_splatter_floor(get_turf(feed_target))
+ user.add_mob_blood(feed_target) // Put target's blood on us. The donor goes in the ( )
+ feed_target.add_mob_blood(feed_target)
+ feed_target.apply_damage(10, BRUTE, BODY_ZONE_HEAD, wound_bonus = CANT_WOUND)
+ INVOKE_ASYNC(feed_target, TYPE_PROC_REF(/mob, emote), "scream")
+ DeactivatePower()
+ return PROCESS_KILL
+
+ var/feed_strength_mult = 0
+ if(bloodsuckerdatum_power.frenzied)
+ feed_strength_mult = 2
+ else if(owner.pulling == feed_target && owner.grab_state >= GRAB_AGGRESSIVE)
+ feed_strength_mult = 1
+ else
+ feed_strength_mult = 0.3
+ blood_taken += bloodsuckerdatum_power.handle_feeding(feed_target, feed_strength_mult, level_current)
+
+ if(feed_strength_mult > 5 && feed_target.stat < DEAD)
+ user.add_mood_event("drankblood", /datum/mood_event/drankblood)
+ // Drank mindless as Ventrue? - BAD
+ if(bloodsuckerdatum_power.my_clan?.blood_drink_type == BLOODSUCKER_DRINK_SNOBBY && QDELETED(feed_target.mind))
+ user.add_mood_event("drankblood", /datum/mood_event/drankblood_bad)
+ if(feed_target.stat >= DEAD && !started_alive)
+ user.add_mood_event("drankblood", /datum/mood_event/drankblood_dead)
+
+ if(!IS_BLOODSUCKER(feed_target))
+ if(feed_target.blood_volume <= BLOOD_VOLUME_BAD && warning_target_bloodvol > BLOOD_VOLUME_BAD)
+ owner.balloon_alert(owner, "your victim's blood is fatally low!")
+ else if(feed_target.blood_volume <= BLOOD_VOLUME_OKAY && warning_target_bloodvol > BLOOD_VOLUME_OKAY)
+ owner.balloon_alert(owner, "your victim's blood is dangerously low.")
+ else if(feed_target.blood_volume <= BLOOD_VOLUME_SAFE && warning_target_bloodvol > BLOOD_VOLUME_SAFE)
+ owner.balloon_alert(owner, "your victim's blood is at an unsafe level.")
+ warning_target_bloodvol = feed_target.blood_volume
+
+ if(bloodsuckerdatum_power.bloodsucker_blood_volume >= bloodsuckerdatum_power.max_blood_volume)
+ user.balloon_alert(owner, "full on blood!")
+ DeactivatePower()
+ return PROCESS_KILL
+ if(feed_target.blood_volume <= 0)
+ user.balloon_alert(owner, "no blood left!")
+ DeactivatePower()
+ return PROCESS_KILL
+ owner.playsound_local(null, 'sound/effects/singlebeat.ogg', 40, TRUE)
+ //play sound to target to show they're dying.
+ if(owner.pulling == feed_target && owner.grab_state >= GRAB_AGGRESSIVE)
+ feed_target.playsound_local(null, 'sound/effects/singlebeat.ogg', 40, TRUE)
+
+/datum/action/cooldown/bloodsucker/feed/proc/find_target()
+ if(isliving(owner.pulling) && !QDELING(owner.pulling))
+ if(!can_feed_from(owner.pulling, give_warnings = TRUE))
+ return FALSE
+ target_ref = WEAKREF(owner.pulling)
+ return TRUE
+
+ var/list/close_living_mobs = list()
+ var/list/close_dead_mobs = list()
+ for(var/mob/living/near_targets in oview(1, owner))
+ if(!owner.Adjacent(near_targets))
+ continue
+ if(near_targets.stat)
+ close_living_mobs |= near_targets
+ else
+ close_dead_mobs |= near_targets
+ //Check living first
+ for(var/mob/living/suckers in close_living_mobs)
+ if(can_feed_from(suckers))
+ target_ref = WEAKREF(suckers)
+ return TRUE
+ //If not, check dead
+ for(var/mob/living/suckers in close_dead_mobs)
+ if(can_feed_from(suckers))
+ target_ref = WEAKREF(suckers)
+ return TRUE
+ //No one to suck blood from.
+ return FALSE
+
+/datum/action/cooldown/bloodsucker/feed/proc/can_feed_from(mob/living/target, give_warnings = FALSE)
+ if(istype(target, /mob/living/basic/mouse))
+ if(bloodsuckerdatum_power.my_clan?.blood_drink_type == BLOODSUCKER_DRINK_SNOBBY)
+ if(give_warnings)
+ owner.balloon_alert(owner, "too disgusting!")
+ return FALSE
+ return TRUE
+ //Mice check done, only humans are otherwise allowed
+ if(!ishuman(target))
+ return FALSE
+
+ var/mob/living/carbon/human/target_user = target
+ if(!(target_user.dna?.species) || !(target_user.mob_biotypes & MOB_ORGANIC))
+ if(give_warnings)
+ owner.balloon_alert(owner, "no blood!")
+ return FALSE
+ if(!target_user.can_inject(owner, BODY_ZONE_HEAD, INJECT_CHECK_PENETRATE_THICK))
+ if(give_warnings)
+ owner.balloon_alert(owner, "suit too thick!")
+ return FALSE
+ if(bloodsuckerdatum_power.my_clan?.blood_drink_type == BLOODSUCKER_DRINK_SNOBBY && QDELETED(target_user.mind) && !bloodsuckerdatum_power.frenzied)
+ if(give_warnings)
+ owner.balloon_alert(owner, "cant drink from mindless!")
+ return FALSE
+ return TRUE
+
+#undef FEED_NOTICE_RANGE
+#undef FEED_DEFAULT_TIMER
diff --git a/modular_bandastation/blood_suckers/code/powers/fortitude.dm b/modular_bandastation/blood_suckers/code/powers/fortitude.dm
new file mode 100644
index 0000000000000..a6b1e15bef42a
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/powers/fortitude.dm
@@ -0,0 +1,84 @@
+/datum/action/cooldown/bloodsucker/fortitude
+ name = "Fortitude"
+ desc = "Withstand egregious physical wounds and walk away from attacks that would stun, pierce, and dismember lesser beings."
+ button_icon_state = "power_fortitude"
+ power_explanation = "Fortitude:\n\
+ Activating Fortitude will provide pierce, stun and dismember immunity.\n\
+ You will additionally gain resistance to Brute and Stamina damge, scaling with level.\n\
+ While using Fortitude, attempting to run will crush you.\n\
+ At level 4, you gain complete stun immunity.\n\
+ Higher levels will increase Brute and Stamina resistance."
+ power_flags = BP_AM_TOGGLE | BP_AM_COSTLESS_UNCONSCIOUS
+ check_flags = BP_CANT_USE_IN_TORPOR | BP_CANT_USE_IN_FRENZY
+ purchase_flags = BLOODSUCKER_CAN_BUY | VASSAL_CAN_BUY
+ bloodcost = 30
+ cooldown_time = 8 SECONDS
+ constant_bloodcost = 0.2
+ sol_multiplier = 3
+ var/was_running
+ var/fortitude_resist // So we can raise and lower your brute resist based on what your level_current WAS.
+ /// Base traits granted by fortitude.
+ var/static/list/base_traits = list(
+ TRAIT_PIERCEIMMUNE,
+ TRAIT_NODISMEMBER,
+ TRAIT_PUSHIMMUNE,
+ TRAIT_NO_SPRINT,
+ TRAIT_ABATES_SHOCK,
+ TRAIT_ANALGESIA,
+ TRAIT_NO_PAIN_EFFECTS,
+ TRAIT_NO_SHOCK_BUILDUP,
+ )
+ /// Upgraded traits granted by fortitude.
+ var/static/list/upgraded_traits = list(TRAIT_STUNIMMUNE, TRAIT_CANT_STAMCRIT)
+
+/datum/action/cooldown/bloodsucker/fortitude/ActivatePower(trigger_flags)
+ . = ..()
+ owner.balloon_alert(owner, "fortitude turned on.")
+ to_chat(owner, span_notice("Your flesh, skin, and muscles become as steel."))
+ // Traits & Effects
+ owner.add_traits(base_traits, FORTITUDE_TRAIT)
+ if(level_current >= 4)
+ owner.add_traits(upgraded_traits, FORTITUDE_TRAIT) // They'll get stun resistance + this, who cares.
+ var/mob/living/carbon/human/bloodsucker_user = owner
+ if(IS_BLOODSUCKER(owner) || IS_VASSAL(owner))
+ fortitude_resist = max(0.3, 0.7 - level_current * 0.1)
+ bloodsucker_user.physiology.brute_mod *= fortitude_resist
+ bloodsucker_user.physiology.stamina_mod *= fortitude_resist
+
+ was_running = ((owner.m_intent == MOVE_INTENT_RUN) || (owner.m_intent == MOVE_INTENT_SPRINT))
+ if(was_running)
+ bloodsucker_user.set_move_intent(MOVE_INTENT_WALK)
+
+/datum/action/cooldown/bloodsucker/fortitude/process(seconds_per_tick)
+ // Checks that we can keep using this.
+ . = ..()
+ if(!.)
+ return
+ if(!active)
+ return
+ var/mob/living/carbon/user = owner
+ /// Prevents running while on Fortitude
+ if(user.m_intent != MOVE_INTENT_WALK)
+ user.set_move_intent(MOVE_INTENT_WALK)
+ user.balloon_alert(user, "you attempt to run, crushing yourself.")
+ user.take_overall_damage(brute = rand(5, 15))
+ /// We don't want people using fortitude being able to use vehicles
+ if(istype(user.buckled, /obj/vehicle))
+ user.buckled.unbuckle_mob(src, force=TRUE)
+
+/datum/action/cooldown/bloodsucker/fortitude/DeactivatePower()
+ if(!ishuman(owner))
+ return
+ var/mob/living/carbon/human/bloodsucker_user = owner
+ if(IS_BLOODSUCKER(owner) || IS_VASSAL(owner))
+ bloodsucker_user.physiology.brute_mod /= fortitude_resist
+ if(!HAS_TRAIT_FROM(bloodsucker_user, TRAIT_STUNIMMUNE, FORTITUDE_TRAIT))
+ bloodsucker_user.physiology.stamina_mod /= fortitude_resist
+ // Remove Traits & Effects
+ owner.remove_traits(base_traits + upgraded_traits, FORTITUDE_TRAIT)
+
+ if(was_running && bloodsucker_user.m_intent == MOVE_INTENT_WALK)
+ bloodsucker_user.set_move_intent(MOVE_INTENT_RUN)
+ owner.balloon_alert(owner, "fortitude turned off.")
+
+ return ..()
diff --git a/modular_bandastation/blood_suckers/code/powers/go_home.dm b/modular_bandastation/blood_suckers/code/powers/go_home.dm
new file mode 100644
index 0000000000000..3f02a3488471f
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/powers/go_home.dm
@@ -0,0 +1,127 @@
+#define GOHOME_START 0
+#define GOHOME_FLICKER_ONE 2
+#define GOHOME_FLICKER_TWO 4
+#define GOHOME_TELEPORT 6
+
+/**
+ * Given to Bloodsuckers near Sol if they have a Coffin claimed.
+ * Teleports them to their Coffin after a delay.
+ * Makes them drop everything if someone witnesses the act.
+ */
+/datum/action/cooldown/bloodsucker/gohome
+ name = "Vanishing Act"
+ desc = "As dawn aproaches, disperse into mist and return directly to your Lair. WARNING: You will drop ALL of your possessions if observed by mortals."
+ button_icon_state = "power_gohome"
+ active_background_icon_state = "vamp_power_off_oneshot"
+ base_background_icon_state = "vamp_power_off_oneshot"
+ power_explanation = "Vanishing Act: \n\
+ Activating Vanishing Act will, after a short delay, teleport the user to their Claimed Coffin. \n\
+ The power will cancel out if the Claimed Coffin is somehow destroyed. \n\
+ Immediately after activating, lights around the user will begin to flicker. \n\
+ Once the user teleports to their coffin, in their place will be a Rat or Bat."
+ power_flags = BP_AM_TOGGLE | BP_AM_SINGLEUSE | BP_AM_STATIC_COOLDOWN
+ check_flags = BP_CANT_USE_IN_FRENZY | BP_CANT_USE_WHILE_STAKED
+ purchase_flags = NONE
+ bloodcost = 100
+ constant_bloodcost = 2
+ cooldown_time = 100 SECONDS
+ ///What stage of the teleportation are we in
+ var/teleporting_stage = GOHOME_START
+ ///The types of mobs that will drop post-teleportation.
+ var/static/list/spawning_mobs = list(
+ /mob/living/basic/mouse = 3,
+ /mob/living/basic/bat = 1,
+ )
+
+/datum/action/cooldown/bloodsucker/gohome/can_use(mob/living/carbon/user, trigger_flags)
+ . = ..()
+ if(!.)
+ return FALSE
+ /// Have No Lair (NOTE: You only got this power if you had a lair, so this means it's destroyed)
+ if(!istype(bloodsuckerdatum_power) || QDELETED(bloodsuckerdatum_power.coffin))
+ owner.balloon_alert(owner, "coffin was destroyed!")
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/bloodsucker/gohome/ActivatePower(trigger_flags)
+ . = ..()
+ owner.balloon_alert(owner, "preparing to teleport...")
+
+/datum/action/cooldown/bloodsucker/gohome/process(seconds_per_tick)
+ . = ..()
+ if(!.)
+ return FALSE
+
+ switch(teleporting_stage)
+ if(GOHOME_START)
+ INVOKE_ASYNC(src, PROC_REF(flicker_lights), 3, 20)
+ if(GOHOME_FLICKER_ONE)
+ INVOKE_ASYNC(src, PROC_REF(flicker_lights), 4, 40)
+ if(GOHOME_FLICKER_TWO)
+ INVOKE_ASYNC(src, PROC_REF(flicker_lights), 4, 60)
+ if(GOHOME_TELEPORT)
+ INVOKE_ASYNC(src, PROC_REF(teleport_to_coffin), owner)
+ teleporting_stage++
+
+/datum/action/cooldown/bloodsucker/gohome/ContinueActive(mob/living/user, mob/living/target)
+ . = ..()
+ if(!.)
+ return FALSE
+ if(!isturf(owner.loc))
+ return FALSE
+ if(QDELETED(bloodsuckerdatum_power.coffin))
+ user.balloon_alert(user, "coffin destroyed!")
+ to_chat(owner, span_warning("Your coffin has been destroyed! You no longer have a destination."))
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/bloodsucker/gohome/proc/flicker_lights(flicker_range, beat_volume)
+ for(var/obj/machinery/light/nearby_lights in view(flicker_range, get_turf(owner)))
+ nearby_lights.flicker(5)
+ playsound(get_turf(owner), 'sound/effects/singlebeat.ogg', beat_volume, 1)
+
+/datum/action/cooldown/bloodsucker/gohome/proc/teleport_to_coffin(mob/living/carbon/user)
+ var/drop_item = FALSE
+ var/turf/current_turf = get_turf(owner)
+ // If we aren't in the dark, anyone watching us will cause us to drop out stuff
+ if(!QDELETED(current_turf?.lighting_object) && current_turf.get_lumcount() >= 0.2)
+ for(var/mob/living/watchers in viewers(world.view, get_turf(owner)) - owner)
+ if(QDELETED(watchers.client) || watchers.stat != CONSCIOUS)
+ continue
+ if(watchers.has_unlimited_silicon_privilege)
+ continue
+ if(watchers.is_blind())
+ continue
+ if(!IS_BLOODSUCKER(watchers) && !IS_VASSAL(watchers))
+ drop_item = TRUE
+ break
+ // Drop all necessary items (handcuffs, legcuffs, items if seen)
+ user.uncuff()
+ if(drop_item)
+ user.unequip_everything()
+
+ playsound(current_turf, 'sound/magic/summon_karp.ogg', vol = 60, vary = TRUE)
+
+ var/datum/effect_system/steam_spread/bloodsucker/puff = new /datum/effect_system/steam_spread/bloodsucker()
+ puff.set_up(3, 0, current_turf)
+ puff.start()
+
+ /// STEP FIVE: Create animal at prev location
+ var/mob/living/simple_animal/new_mob = pick_weight(spawning_mobs)
+ new new_mob(current_turf)
+ /// TELEPORT: Move to Coffin & Close it!
+ user.set_resting(TRUE, TRUE, FALSE)
+ do_teleport(owner, bloodsuckerdatum_power.coffin, no_effects = TRUE, forced = TRUE, channel = TELEPORT_CHANNEL_QUANTUM)
+ bloodsuckerdatum_power.coffin.close(owner)
+ bloodsuckerdatum_power.coffin.take_contents()
+ playsound(bloodsuckerdatum_power.coffin.loc, bloodsuckerdatum_power.coffin.close_sound, vol = 15, vary = TRUE, extrarange = -3)
+
+ DeactivatePower()
+
+/datum/effect_system/steam_spread/bloodsucker
+ effect_type = /obj/effect/particle_effect/fluid/smoke/vampsmoke
+
+#undef GOHOME_START
+#undef GOHOME_FLICKER_ONE
+#undef GOHOME_FLICKER_TWO
+#undef GOHOME_TELEPORT
diff --git a/modular_bandastation/blood_suckers/code/powers/masquerade.dm b/modular_bandastation/blood_suckers/code/powers/masquerade.dm
new file mode 100644
index 0000000000000..86a509e88b2b9
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/powers/masquerade.dm
@@ -0,0 +1,95 @@
+/**
+ * # WITHOUT THIS POWER:
+ *
+ * - Mid-Blood: SHOW AS PALE
+ * - Low-Blood: SHOW AS DEAD
+ * - No Heartbeat
+ * - Examine shows actual blood
+ * - Thermal homeostasis (ColdBlooded)
+ * WITH THIS POWER:
+ * - Normal body temp -- remove Cold Blooded (return on deactivate)
+ */
+
+/datum/action/cooldown/bloodsucker/masquerade
+ name = "Masquerade"
+ desc = "Feign the vital signs of a mortal, and escape both casual and medical notice as the monster you truly are."
+ button_icon_state = "power_human"
+ power_explanation = "Masquerade:\n\
+ Activating Masquerade will forge your identity to be practically identical to that of a human;\n\
+ - You lose nearly all Bloodsucker benefits, including healing, sleep, radiation, crit, virus and cold immunity.\n\
+ - Your eyes turn to that of a regular human as your heart begins to beat.\n\
+ - You gain a Genetic sequence, and appear to have 100% blood when scanned by a Health Analyzer.\n\
+ - You will not appear as Pale when examined. Anything further than Pale, however, will not be hidden.\n\
+ At the end of a Masquerade, you will re-gain your Vampiric abilities, as well as lose any Disease & Gene you might have."
+ power_flags = BP_AM_TOGGLE | BP_AM_STATIC_COOLDOWN | BP_AM_COSTLESS_UNCONSCIOUS
+ check_flags = BP_CANT_USE_IN_FRENZY | BP_CANT_USE_DURING_SOL
+ purchase_flags = BLOODSUCKER_CAN_BUY | BLOODSUCKER_DEFAULT_POWER
+ bloodcost = 10
+ cooldown_time = 5 SECONDS
+ constant_bloodcost = 0.1
+
+/datum/action/cooldown/bloodsucker/masquerade/ActivatePower(trigger_flags)
+ . = ..()
+ var/mob/living/carbon/user = owner
+ owner.balloon_alert(owner, "masquerade turned on.")
+ to_chat(user, span_notice("Your heart beats falsely within your lifeless chest. You may yet pass for a mortal."))
+ to_chat(user, span_warning("Your vampiric healing is halted while imitating life."))
+
+ // Give status effect
+ user.apply_status_effect(/datum/status_effect/masquerade)
+
+ // Handle Traits
+ user.remove_traits(bloodsuckerdatum_power.bloodsucker_traits, BLOODSUCKER_TRAIT)
+ ADD_TRAIT(user, TRAIT_MASQUERADE, BLOODSUCKER_TRAIT)
+ // Handle organs
+ var/obj/item/organ/internal/heart/vampheart = user.get_organ_slot(ORGAN_SLOT_HEART)
+ vampheart.beating = TRUE
+ var/obj/item/organ/internal/eyes/eyes = user.get_organ_slot(ORGAN_SLOT_EYES)
+ if(eyes)
+ eyes.flash_protect = initial(eyes.flash_protect)
+
+/datum/action/cooldown/bloodsucker/masquerade/DeactivatePower()
+ . = ..() // activate = FALSE
+ var/mob/living/carbon/user = owner
+ owner.balloon_alert(owner, "masquerade turned off.")
+
+ // Remove status effect, mutations & diseases that you got while on masq.
+ user.remove_status_effect(/datum/status_effect/masquerade)
+ user.dna.remove_all_mutations()
+ for(var/datum/disease/diseases as anything in user.diseases)
+ diseases.cure()
+
+ // Handle Traits
+ user.add_traits(bloodsuckerdatum_power.bloodsucker_traits, BLOODSUCKER_TRAIT)
+ REMOVE_TRAIT(user, TRAIT_MASQUERADE, BLOODSUCKER_TRAIT)
+
+ // Handle organs
+ var/obj/item/organ/internal/heart/vampheart = user.get_organ_slot(ORGAN_SLOT_HEART)
+ vampheart.beating = FALSE
+ var/obj/item/organ/internal/eyes/eyes = user.get_organ_slot(ORGAN_SLOT_EYES)
+ if(eyes)
+ eyes.flash_protect = max(initial(eyes.flash_protect) - 1, FLASH_PROTECTION_SENSITIVE)
+ to_chat(user, span_notice("Your heart beats one final time, while your skin dries out and your icy pallor returns."))
+
+/**
+ * # Status effect
+ *
+ * This is what the Masquerade power gives, handles their bonuses and gives them a neat icon to tell them they're on Masquerade.
+ */
+
+/datum/status_effect/masquerade
+ id = "masquerade"
+ duration = STATUS_EFFECT_PERMANENT
+ tick_interval = STATUS_EFFECT_NO_TICK
+ alert_type = /atom/movable/screen/alert/status_effect/masquerade
+
+/atom/movable/screen/alert/status_effect/masquerade
+ name = "Masquerade"
+ desc = "You are currently hiding your identity using the Masquerade power. This halts Vampiric healing."
+ icon = 'modular_bandastation/blood_suckers/icons/actions_bloodsucker.dmi'
+ icon_state = "power_human"
+ alerttooltipstyle = "cult"
+
+/atom/movable/screen/alert/status_effect/masquerade/MouseEntered(location,control,params)
+ desc = initial(desc)
+ return ..()
diff --git a/modular_bandastation/blood_suckers/code/powers/targeted/_base_targeted.dm b/modular_bandastation/blood_suckers/code/powers/targeted/_base_targeted.dm
new file mode 100644
index 0000000000000..f9d139d1204dd
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/powers/targeted/_base_targeted.dm
@@ -0,0 +1,92 @@
+// NOTE: All Targeted spells are Toggles! We just don't bother checking here.
+/datum/action/cooldown/bloodsucker/targeted
+ power_flags = BP_AM_TOGGLE
+
+ ///If set, how far the target has to be for the power to work.
+ var/target_range
+ ///Message sent to chat when clicking on the power, before you use it.
+ var/prefire_message
+ ///Most powers happen the moment you click. Some, like Mesmerize, require time and shouldn't cost you if they fail.
+ var/power_activates_immediately = TRUE
+ ///Is this power LOCKED due to being used?
+ var/power_in_use = FALSE
+
+/// Modify description to add notice that this is aimed.
+/datum/action/cooldown/bloodsucker/targeted/New(Target)
+ desc += " \[Targeted Power\]"
+ return ..()
+
+/datum/action/cooldown/bloodsucker/targeted/Remove(mob/living/remove_from)
+ . = ..()
+ if(remove_from.click_intercept == src)
+ unset_click_ability(remove_from)
+
+/datum/action/cooldown/bloodsucker/targeted/Trigger(trigger_flags, atom/target)
+ if(active && can_deactivate())
+ DeactivatePower()
+ return FALSE
+ if(!can_pay_cost(owner) || !can_use(owner, trigger_flags))
+ return FALSE
+
+ if(prefire_message)
+ to_chat(owner, span_announce("[prefire_message]"))
+
+ ActivatePower(trigger_flags)
+ if(!QDELETED(target))
+ return InterceptClickOn(owner, null, target)
+
+ return set_click_ability(owner)
+
+/datum/action/cooldown/bloodsucker/targeted/DeactivatePower()
+ if(power_flags & BP_AM_TOGGLE)
+ STOP_PROCESSING(SSprocessing, src)
+ active = FALSE
+ build_all_button_icons()
+ unset_click_ability(owner)
+// ..() // we don't want to pay cost here
+
+/// Check if target is VALID (wall, turf, or character?)
+/datum/action/cooldown/bloodsucker/targeted/proc/CheckValidTarget(atom/target_atom)
+ if(target_atom == owner)
+ return FALSE
+ return TRUE
+
+/// Check if valid target meets conditions
+/datum/action/cooldown/bloodsucker/targeted/proc/CheckCanTarget(atom/target_atom)
+ if(target_range)
+ // Out of Range
+ if(!(target_atom in view(target_range, owner)))
+ if(target_range > 1) // Only warn for range if it's greater than 1. Brawn doesn't need to announce itself.
+ owner.balloon_alert(owner, "out of range.")
+ return FALSE
+ return istype(target_atom)
+
+/// Click Target
+/datum/action/cooldown/bloodsucker/targeted/proc/click_with_power(atom/target_atom)
+ // CANCEL RANGED TARGET check
+ if(power_in_use || !CheckValidTarget(target_atom))
+ return FALSE
+ // Valid? (return true means DON'T cancel power!)
+ if(!can_pay_cost() || !can_use(owner) || !CheckCanTarget(target_atom))
+ return TRUE
+ power_in_use = TRUE // Lock us into this ability until it successfully fires off. Otherwise, we pay the blood even if we fail.
+ FireTargetedPower(target_atom) // We use this instead of ActivatePower(trigger_flags), which has no input
+ // Skip this part so we can return TRUE right away.
+ if(power_activates_immediately)
+ power_activated_sucessfully() // Mesmerize pays only after success.
+ power_in_use = FALSE
+ return TRUE
+
+/// Like ActivatePower, but specific to Targeted (and takes an atom input). We don't use ActivatePower for targeted.
+/datum/action/cooldown/bloodsucker/targeted/proc/FireTargetedPower(atom/target_atom)
+ log_combat(owner, target_atom, "used [name] on")
+
+/// The power went off! We now pay the cost of the power.
+/datum/action/cooldown/bloodsucker/targeted/proc/power_activated_sucessfully()
+ unset_click_ability(owner)
+ pay_cost()
+ StartCooldown()
+ DeactivatePower()
+
+/datum/action/cooldown/bloodsucker/targeted/InterceptClickOn(mob/living/user, params, atom/target)
+ click_with_power(target)
diff --git a/modular_bandastation/blood_suckers/code/powers/targeted/brawn.dm b/modular_bandastation/blood_suckers/code/powers/targeted/brawn.dm
new file mode 100644
index 0000000000000..d9f37041b4519
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/powers/targeted/brawn.dm
@@ -0,0 +1,188 @@
+/datum/action/cooldown/bloodsucker/targeted/brawn
+ name = "Brawn"
+ desc = "Snap restraints, break lockers and doors, or deal terrible damage with your bare hands."
+ button_icon_state = "power_strength"
+ power_explanation = "Brawn:\n\
+ Click any person to bash into them, break restraints you have or knocking a grabber down. Only one of these can be done per use.\n\
+ Punching a Cyborg will heavily EMP them in addition to deal damage.\n\
+ At level 3, you get the ability to break closets open, additionally can both break restraints AND knock a grabber down in the same use.\n\
+ At level 4, you get the ability to bash airlocks open, as long as they aren't bolted.\n\
+ Higher levels will increase the damage and knockdown when punching someone."
+ power_flags = BP_AM_TOGGLE
+ check_flags = BP_CANT_USE_IN_TORPOR|BP_CANT_USE_IN_FRENZY|BP_CANT_USE_WHILE_INCAPACITATED|BP_CANT_USE_WHILE_UNCONSCIOUS
+ purchase_flags = BLOODSUCKER_CAN_BUY|VASSAL_CAN_BUY
+ bloodcost = 8
+ sol_multiplier = 5
+ cooldown_time = 9 SECONDS
+ target_range = 1
+ power_activates_immediately = TRUE
+ prefire_message = "Select a target."
+
+/datum/action/cooldown/bloodsucker/targeted/brawn/ActivatePower(trigger_flags)
+ // Did we break out of our handcuffs?
+ if(break_restraints())
+ power_activated_sucessfully()
+ return FALSE
+ // Did we knock a grabber down? We can only do this while not also breaking restraints if strong enough.
+ if(level_current >= 3 && escape_puller())
+ power_activated_sucessfully()
+ return FALSE
+ // Did neither, now we can PUNCH.
+ return ..()
+
+// Look at 'biodegrade.dm' for reference
+/datum/action/cooldown/bloodsucker/targeted/brawn/proc/break_restraints()
+ var/mob/living/carbon/human/user = owner
+ ///Only one form of shackles removed per use
+ var/used = FALSE
+
+ // Breaks out of lockers
+ if(istype(user.loc, /obj/structure/closet))
+ var/obj/structure/closet/closet = user.loc
+ if(!istype(closet))
+ return FALSE
+ closet.visible_message(
+ span_warning("[closet] tears apart as [user] bashes it open from within!"),
+ span_warning("[closet] tears apart as you bash it open from within!"),
+ )
+ to_chat(user, span_warning("We bash [closet] wide open!"))
+ addtimer(CALLBACK(src, PROC_REF(break_closet), user, closet), 1)
+ used = TRUE
+
+ // Remove both Handcuffs & Legcuffs
+ var/obj/cuffs = user.get_item_by_slot(ITEM_SLOT_HANDCUFFED)
+ var/obj/legcuffs = user.get_item_by_slot(ITEM_SLOT_LEGCUFFED)
+ if(!used && (istype(cuffs) || istype(legcuffs)))
+ user.visible_message(
+ span_warning("[user] discards their restraints like it's nothing!"),
+ span_warning("We break through our restraints!"),
+ )
+ user.clear_cuffs(cuffs, TRUE)
+ user.clear_cuffs(legcuffs, TRUE)
+ used = TRUE
+
+ // Remove Straightjackets
+ if(user.wear_suit?.breakouttime && !used)
+ var/obj/item/clothing/suit/straightjacket = user.get_item_by_slot(ITEM_SLOT_OCLOTHING)
+ user.visible_message(
+ span_warning("[user] rips straight through the [user.p_their()] [straightjacket]!"),
+ span_warning("We tear through our [straightjacket]!"),
+ )
+ if(straightjacket && user.wear_suit == straightjacket)
+ qdel(straightjacket)
+ used = TRUE
+
+ // Did we end up using our ability? If so, play the sound effect and return TRUE
+ if(used)
+ playsound(get_turf(user), 'sound/effects/grillehit.ogg', 80, 1, -1)
+ return used
+
+// This is its own proc because its done twice, to repeat code copypaste.
+/datum/action/cooldown/bloodsucker/targeted/brawn/proc/break_closet(mob/living/carbon/human/user, obj/structure/closet/closet)
+ closet?.bust_open()
+
+/datum/action/cooldown/bloodsucker/targeted/brawn/proc/escape_puller()
+ if(!owner.pulledby) // || owner.pulledby.grab_state <= GRAB_PASSIVE)
+ return FALSE
+ var/mob/pulled_mob = owner.pulledby
+ var/pull_power = pulled_mob.grab_state
+ playsound(get_turf(pulled_mob), 'sound/effects/woodhit.ogg', 75, 1, -1)
+ // Knock Down (if Living)
+ if(isliving(pulled_mob))
+ var/mob/living/hit_target = pulled_mob
+ hit_target.Knockdown(pull_power * 10 + 20)
+ // Knock Back (before Knockdown, which probably cancels pull)
+ var/send_dir = get_dir(owner, pulled_mob)
+ var/turf/turf_thrown_at = get_ranged_target_turf(pulled_mob, send_dir, pull_power)
+ owner.newtonian_move(send_dir) // Bounce back in 0 G
+ pulled_mob.throw_at(turf_thrown_at, pull_power, TRUE, owner, FALSE) // Throw distance based on grab state! Harder grabs punished more aggressively.
+ // /proc/log_combat(atom/user, atom/target, what_done, atom/object=null, addition=null)
+ log_combat(owner, pulled_mob, "used Brawn power")
+ owner.visible_message(
+ span_warning("[owner] tears free of [pulled_mob]'s grasp!"),
+ span_warning("You shrug off [pulled_mob]'s grasp!"),
+ )
+ owner.pulledby = null // It's already done, but JUST IN CASE.
+ return TRUE
+
+/datum/action/cooldown/bloodsucker/targeted/brawn/FireTargetedPower(atom/target_atom)
+ . = ..()
+ var/mob/living/user = owner
+ // Target Type: Mob
+ if(isliving(target_atom))
+ var/mob/living/target = target_atom
+ var/mob/living/carbon/carbonuser = user
+ //You know what I'm just going to take the average of the user's limbs max damage instead of dealing with 2 hands
+ var/obj/item/bodypart/user_active_arm = carbonuser.get_active_hand()
+ var/hitStrength = user_active_arm.unarmed_damage_high * 1.25 + 2
+ // Knockdown!
+ var/powerlevel = min(5, 1 + level_current)
+ if(rand(5 + powerlevel) >= 5)
+ target.visible_message(
+ span_danger("[user] lands a vicious punch, sending [target] away!"), \
+ span_userdanger("[user] has landed a horrifying punch on you, sending you flying!"),
+ )
+ target.Knockdown(min(5, rand(10, 10 * powerlevel)))
+ // Attack!
+ owner.balloon_alert(owner, "you punch [target]!")
+ playsound(get_turf(target), 'sound/weapons/punch4.ogg', 60, 1, -1)
+ user.do_attack_animation(target, ATTACK_EFFECT_SMASH)
+ var/obj/item/bodypart/affecting = target.get_bodypart(ran_zone(target.zone_selected))
+ target.apply_damage(hitStrength, BRUTE, affecting)
+ // Knockback
+ var/send_dir = get_dir(owner, target)
+ var/turf/turf_thrown_at = get_ranged_target_turf(target, send_dir, powerlevel)
+ owner.newtonian_move(send_dir) // Bounce back in 0 G
+ target.throw_at(turf_thrown_at, powerlevel, TRUE, owner) //new /datum/forced_movement(target, get_ranged_target_turf(target, send_dir, (hitStrength / 4)), 1, FALSE)
+ // Target Type: Cyborg (Also gets the effects above)
+ if(issilicon(target))
+ target.emp_act(EMP_HEAVY)
+ // Target Type: Locker
+ else if(istype(target_atom, /obj/structure/closet) && level_current >= 3)
+ var/obj/structure/closet/target_closet = target_atom
+ user.balloon_alert(user, "you prepare to bash [target_closet] open...")
+ if(!do_after(user, 2.5 SECONDS, target_closet))
+ user.balloon_alert(user, "interrupted!")
+ return FALSE
+ target_closet.visible_message(span_danger("[target_closet] breaks open as [user] bashes it!"))
+ addtimer(CALLBACK(src, PROC_REF(break_closet), user, target_closet), 1)
+ playsound(get_turf(user), 'sound/effects/grillehit.ogg', 80, TRUE, -1)
+ // Target Type: Door
+ else if(istype(target_atom, /obj/machinery/door) && level_current >= 4)
+ var/obj/machinery/door/target_airlock = target_atom
+ playsound(get_turf(user), 'sound/machines/airlock_alien_prying.ogg', 40, TRUE, -1)
+ owner.balloon_alert(owner, "you prepare to tear open [target_airlock]...")
+ if(!do_after(user, 2.5 SECONDS, target_airlock))
+ user.balloon_alert(user, "interrupted!")
+ return FALSE
+ if(target_airlock.Adjacent(user))
+ target_airlock.visible_message(span_danger("[target_airlock] breaks open as [user] bashes it!"))
+ user.Stun(10)
+ user.do_attack_animation(target_airlock, ATTACK_EFFECT_SMASH)
+ playsound(get_turf(target_airlock), 'sound/effects/bang.ogg', 30, 1, -1)
+ target_airlock.open(2) // open(2) is like a crowbar or jaws of life.
+
+/datum/action/cooldown/bloodsucker/targeted/brawn/CheckValidTarget(atom/target_atom)
+ . = ..()
+ if(!.)
+ return FALSE
+ return isliving(target_atom) || istype(target_atom, /obj/machinery/door) || istype(target_atom, /obj/structure/closet)
+
+/datum/action/cooldown/bloodsucker/targeted/brawn/CheckCanTarget(atom/target_atom)
+ // DEFAULT CHECKS (Distance)
+ . = ..()
+ if(!.) // Disable range notice for Brawn.
+ return FALSE
+ // Must outside Closet to target anyone!
+ if(!isturf(owner.loc))
+ return FALSE
+ // Target Type: Living
+ if(isliving(target_atom))
+ return TRUE
+ // Target Type: Door
+ else if(istype(target_atom, /obj/machinery/door))
+ return TRUE
+ // Target Type: Locker
+ else if(istype(target_atom, /obj/structure/closet))
+ return TRUE
+ return FALSE
diff --git a/modular_bandastation/blood_suckers/code/powers/targeted/haste.dm b/modular_bandastation/blood_suckers/code/powers/targeted/haste.dm
new file mode 100644
index 0000000000000..080eedc4db923
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/powers/targeted/haste.dm
@@ -0,0 +1,90 @@
+/* Level 1: Speed to location
+ * Level 2: Dodge Bullets
+ * Level 3: Stun People Passed
+ */
+
+/datum/action/cooldown/bloodsucker/targeted/haste
+ name = "Immortal Haste"
+ desc = "Dash somewhere with supernatural speed. Those nearby may be knocked away, stunned, or left empty-handed."
+ button_icon_state = "power_speed"
+ power_explanation = "Immortal Haste:\n\
+ Click anywhere to immediately dash towards that location.\n\
+ The Power will not work if you are lying down, in no gravity, or are aggressively grabbed.\n\
+ Anyone in your way during your Haste will be knocked down.\n\
+ Higher levels will increase the knockdown dealt to enemies."
+ power_flags = BP_AM_TOGGLE
+ check_flags = BP_CANT_USE_IN_TORPOR | BP_CANT_USE_IN_FRENZY | BP_CANT_USE_WHILE_INCAPACITATED | BP_CANT_USE_WHILE_UNCONSCIOUS
+ purchase_flags = BLOODSUCKER_CAN_BUY | VASSAL_CAN_BUY
+ bloodcost = 6
+ sol_multiplier = 10
+ cooldown_time = 12 SECONDS
+ target_range = 15
+ power_activates_immediately = TRUE
+ ///List of all people hit by our power, so we don't hit them again.
+ var/list/hit = list()
+
+/datum/action/cooldown/bloodsucker/targeted/haste/can_use(mob/living/carbon/user, trigger_flags)
+ . = ..()
+ if(!.)
+ return FALSE
+ // Being Grabbed
+ if(user.pulledby && user.pulledby.grab_state >= GRAB_AGGRESSIVE)
+ user.balloon_alert(user, "you're being grabbed!")
+ return FALSE
+ if(!user.has_gravity(user.loc)) //We dont want people to be able to use this to fly around in space
+ user.balloon_alert(user, "you cannot dash while floating!")
+ return FALSE
+ if(user.body_position == LYING_DOWN)
+ user.balloon_alert(user, "you must be standing to tackle!")
+ return FALSE
+ return TRUE
+
+/// Anything will do, if it's not me or my square
+/datum/action/cooldown/bloodsucker/targeted/haste/CheckValidTarget(atom/target_atom)
+ . = ..()
+ if(!.)
+ return FALSE
+ return target_atom.loc != owner.loc
+
+/// This is a non-async proc to make sure the power is "locked" until this finishes.
+/datum/action/cooldown/bloodsucker/targeted/haste/FireTargetedPower(atom/target_atom)
+ . = ..()
+ RegisterSignal(owner, COMSIG_MOVABLE_MOVED, PROC_REF(on_move))
+ var/mob/living/user = owner
+ var/turf/targeted_turf = isturf(target_atom) ? target_atom : get_turf(target_atom)
+ // Pulled? Not anymore.
+ user.pulledby?.stop_pulling()
+ // Go to target turf
+ // DO NOT USE WALK TO.
+ owner.balloon_alert(owner, "you dash into the air!")
+ playsound(get_turf(owner), 'sound/weapons/punchmiss.ogg', 25, 1, -1)
+ var/safety = get_dist(user, targeted_turf) * 3 + 1
+ var/consequetive_failures = 0
+ while(--safety && (get_turf(user) != targeted_turf))
+ var/success = step_towards(user, targeted_turf) //This does not try to go around obstacles.
+ if(!success)
+ success = step_to(user, targeted_turf) //this does
+ if(!success)
+ consequetive_failures++
+ if(consequetive_failures >= 3) //if 3 steps don't work
+ break //just stop
+ else
+ consequetive_failures = 0 //reset so we can keep moving
+ if(user.resting || user.incapacitated(IGNORE_RESTRAINTS, IGNORE_GRAB)) //actually down? stop.
+ break
+ if(success) //don't sleep if we failed to move.
+ sleep(world.tick_lag)
+
+/datum/action/cooldown/bloodsucker/targeted/haste/power_activated_sucessfully()
+ . = ..()
+ UnregisterSignal(owner, COMSIG_MOVABLE_MOVED)
+ hit.Cut()
+
+/datum/action/cooldown/bloodsucker/targeted/haste/proc/on_move()
+ for(var/mob/living/hit_living in dview(1, get_turf(owner)) - owner)
+ if(hit.Find(hit_living))
+ continue
+ hit += hit_living
+ playsound(hit_living, "sound/weapons/punch[rand(1,4)].ogg", 15, 1, -1)
+ hit_living.Knockdown(10 + level_current * 4)
+ hit_living.spin(10, 1)
diff --git a/modular_bandastation/blood_suckers/code/powers/targeted/lunge.dm b/modular_bandastation/blood_suckers/code/powers/targeted/lunge.dm
new file mode 100644
index 0000000000000..18b1a05852d4f
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/powers/targeted/lunge.dm
@@ -0,0 +1,175 @@
+/datum/action/cooldown/bloodsucker/targeted/lunge
+ name = "Predatory Lunge"
+ desc = "Spring at your target to grapple them without warning, or tear the dead's heart out. Attacks from concealment or the rear may even knock them down if strong enough."
+ button_icon_state = "power_lunge"
+ power_explanation = "Predatory Lunge:\n\
+ Click any player to start spinning wildly and, after a short delay, dash at them.\n\
+ When lunging at someone, you will grab them, immediately starting off at aggressive.\n\
+ Riot gear and Monster Hunters are protected and will only be passively grabbed.\n\
+ You cannot use the Power if you are already grabbing someone, or are being grabbed.\n\
+ If you grab from behind, or from darkness (Cloak of Darkness works), you will knock the target down.\n\
+ If used on a dead body, will tear their heart out.\n\
+ Higher levels increase the knockdown dealt to enemies.\n\
+ At level 4, you will no longer spin, but you will be limited to tackling from only 6 tiles away."
+ power_flags = NONE
+ check_flags = BP_CANT_USE_IN_TORPOR | BP_CANT_USE_IN_FRENZY | BP_CANT_USE_WHILE_INCAPACITATED | BP_CANT_USE_WHILE_UNCONSCIOUS
+ purchase_flags = BLOODSUCKER_CAN_BUY | VASSAL_CAN_BUY
+ bloodcost = 10
+ sol_multiplier = 15
+ cooldown_time = 10 SECONDS
+ power_activates_immediately = FALSE
+
+/datum/action/cooldown/bloodsucker/targeted/lunge/upgrade_power()
+ . = ..()
+ //range is lowered when you get stronger.
+ if(level_current > 3)
+ target_range = 6
+
+/datum/action/cooldown/bloodsucker/targeted/lunge/can_use(mob/living/carbon/user, trigger_flags)
+ . = ..()
+ if(!.)
+ return FALSE
+ // Are we being grabbed?
+ if(!QDELETED(user.pulledby) && user.pulledby.grab_state >= GRAB_AGGRESSIVE)
+ owner.balloon_alert(user, "grabbed!")
+ return FALSE
+ if(!QDELETED(user.pulling))
+ owner.balloon_alert(user, "grabbing someone!")
+ return FALSE
+ return TRUE
+
+/// Check: Are we lunging at a person?
+/datum/action/cooldown/bloodsucker/targeted/lunge/CheckValidTarget(atom/target_atom)
+ . = ..()
+ if(!.)
+ return FALSE
+ return isliving(target_atom)
+
+/datum/action/cooldown/bloodsucker/targeted/lunge/CheckCanTarget(atom/target_atom)
+ // Default Checks
+ . = ..()
+ if(!.)
+ return FALSE
+ // Check: Turf
+ var/mob/living/turf_target = target_atom
+ if(!isturf(turf_target.loc))
+ return FALSE
+ // Check: can the Bloodsucker even move?
+ var/mob/living/user = owner
+ if(user.body_position == LYING_DOWN || HAS_TRAIT(owner, TRAIT_IMMOBILIZED))
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/bloodsucker/targeted/lunge/can_deactivate()
+ return !(datum_flags & DF_ISPROCESSING) //only if you aren't lunging
+
+/datum/action/cooldown/bloodsucker/targeted/lunge/FireTargetedPower(atom/target_atom)
+ . = ..()
+ owner.face_atom(target_atom)
+ if(level_current > 3)
+ do_lunge(target_atom)
+ return
+
+ prepare_target_lunge(target_atom)
+ return TRUE
+
+///Starts processing the power and prepares the lunge by spinning, calls lunge at the end of it.
+/datum/action/cooldown/bloodsucker/targeted/lunge/proc/prepare_target_lunge(atom/target_atom)
+ START_PROCESSING(SSprocessing, src)
+ owner.balloon_alert(owner, "lunge started!")
+ //animate them shake
+ var/base_x = owner.base_pixel_x
+ var/base_y = owner.base_pixel_y
+ animate(owner, pixel_x = base_x, pixel_y = base_y, time = 1, loop = -1)
+ for(var/i in 1 to 25)
+ var/x_offset = base_x + rand(-3, 3)
+ var/y_offset = base_y + rand(-3, 3)
+ animate(pixel_x = x_offset, pixel_y = y_offset, time = 1)
+
+ if(!do_after(owner, 4 SECONDS, timed_action_flags = (IGNORE_USER_LOC_CHANGE|IGNORE_TARGET_LOC_CHANGE|IGNORE_SLOWDOWNS), extra_checks = CALLBACK(src, PROC_REF(CheckCanTarget), target_atom)))
+ end_target_lunge(base_x, base_y)
+
+ return FALSE
+
+ end_target_lunge()
+ do_lunge(target_atom)
+ return TRUE
+
+///When preparing to lunge ends, this clears it up.
+/datum/action/cooldown/bloodsucker/targeted/lunge/proc/end_target_lunge(base_x, base_y)
+ animate(owner, pixel_x = base_x, pixel_y = base_y, time = 1)
+ STOP_PROCESSING(SSprocessing, src)
+
+/datum/action/cooldown/bloodsucker/targeted/lunge/process()
+ if(!active) //If running SSfasprocess (on cooldown)
+ return ..() //Manage our cooldown timers
+ if(prob(75))
+ owner.spin(8, 1)
+ owner.balloon_alert_to_viewers("spins wildly!", "you spin!")
+ return
+ do_smoke(0, owner.loc, smoke_type = /obj/effect/particle_effect/fluid/smoke/transparent)
+
+///Actually lunges the target, then calls lunge end.
+/datum/action/cooldown/bloodsucker/targeted/lunge/proc/do_lunge(atom/hit_atom)
+ var/turf/targeted_turf = get_turf(hit_atom)
+
+ var/dist = get_dist(owner, targeted_turf)
+ if(dist <= target_range)
+ var/safety = dist * 3 + 1
+ var/consequetive_failures = 0
+ while(--safety && !hit_atom.Adjacent(owner))
+ if(!step_to(owner, targeted_turf))
+ consequetive_failures++
+ if(consequetive_failures >= 3) // If 3 steps don't work, just stop.
+ break
+ else
+ owner.balloon_alert(owner, "too far away!")
+
+ lunge_end(hit_atom, targeted_turf)
+
+/datum/action/cooldown/bloodsucker/targeted/lunge/proc/lunge_end(atom/hit_atom, turf/target_turf)
+ power_activated_sucessfully()
+ // Am I next to my target to start giving the effects?
+ if(!owner.Adjacent(hit_atom))
+ return
+
+ var/mob/living/user = owner
+ var/mob/living/carbon/target = hit_atom
+
+ // Did I slip or get knocked unconscious?
+ if(user.body_position != STANDING_UP || user.incapacitated())
+ var/send_dir = get_dir(user, target_turf)
+ new /datum/forced_movement(user, get_ranged_target_turf(user, send_dir, 1), 1, FALSE)
+ user.spin(10)
+ return
+ // Is my target a Monster hunter?
+ if(IS_MONSTERHUNTER(target) || target.is_shove_knockdown_blocked())
+ owner.balloon_alert(owner, "pushed away!")
+ target.grabbedby(owner)
+ return
+
+ owner.balloon_alert(owner, "you lunge at [target]!")
+ if(target.stat == DEAD)
+ var/obj/item/bodypart/chest = target.get_bodypart(BODY_ZONE_CHEST)
+ var/datum/wound/slash/flesh/moderate/crit_wound = new
+ crit_wound.apply_wound(chest)
+ owner.visible_message(
+ span_warning("[owner] tears into [target]'s chest!"),
+ span_warning("You tear into [target]'s chest!"))
+
+ var/obj/item/organ/internal/heart/myheart_now = locate() in target.organs
+ if(myheart_now)
+ myheart_now.Remove(target)
+ user.put_in_hands(myheart_now)
+
+ else
+ target.grabbedby(owner)
+ target.grippedby(owner, instant = TRUE)
+ // Did we knock them down?
+ if(!is_source_facing_target(target, owner) || owner.alpha <= 40)
+ target.Knockdown(10 + level_current * 5)
+ target.Paralyze(0.1)
+
+/datum/action/cooldown/bloodsucker/targeted/lunge/DeactivatePower()
+ REMOVE_TRAIT(owner, TRAIT_IMMOBILIZED, BLOODSUCKER_TRAIT)
+ return ..()
diff --git a/modular_bandastation/blood_suckers/code/powers/targeted/mesmerize.dm b/modular_bandastation/blood_suckers/code/powers/targeted/mesmerize.dm
new file mode 100644
index 0000000000000..db460dd51748c
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/powers/targeted/mesmerize.dm
@@ -0,0 +1,142 @@
+/**
+ * MEZMERIZE
+ * Locks a target in place for a certain amount of time.
+ *
+ * Level 2: Additionally mutes
+ * Level 3: Can be used through face protection
+ * Level 5: Doesn't need to be facing you anymore
+ */
+
+/datum/action/cooldown/bloodsucker/targeted/mesmerize
+ name = "Mesmerize"
+ desc = "Dominate the mind of a mortal who can see your eyes."
+ button_icon_state = "power_mez"
+ power_explanation = "Mesmerize:\n\
+ Click any player to attempt to mesmerize them.\n\
+ You cannot wear anything covering your face, and both parties must be facing eachother. Obviously, both parties need to not be blind. \n\
+ If your target is already mesmerized or a Monster Hunter, the Power will fail.\n\
+ Once mesmerized, the target will be unable to move for a certain amount of time, scaling with level.\n\
+ At level 2, your target will additionally be muted.\n\
+ At level 3, you will be able to use the power through items covering your face.\n\
+ At level 5, you will be able to mesmerize regardless of your target's direction.\n\
+ Higher levels will increase the time of the mesmerize's freeze."
+ power_flags = NONE
+ check_flags = BP_CANT_USE_IN_TORPOR | BP_CANT_USE_IN_FRENZY | BP_CANT_USE_WHILE_INCAPACITATED | BP_CANT_USE_WHILE_UNCONSCIOUS
+ purchase_flags = BLOODSUCKER_CAN_BUY | VASSAL_CAN_BUY
+ bloodcost = 30
+ sol_multiplier = 5
+ cooldown_time = 20 SECONDS
+ target_range = 8
+ power_activates_immediately = FALSE
+ prefire_message = "Whom will you subvert to your will?"
+ ///Our mesmerized target - Prevents several mesmerizes.
+ var/datum/weakref/target_ref
+
+/datum/action/cooldown/bloodsucker/targeted/mesmerize/can_use(mob/living/carbon/user, trigger_flags)
+ . = ..()
+ if(!.) // Default checks
+ return FALSE
+ if(!user.get_organ_slot(ORGAN_SLOT_EYES))
+ // Cant use balloon alert, they've got no eyes!
+ to_chat(user, span_warning("You have no eyes with which to mesmerize."))
+ return FALSE
+ // Check: Eyes covered?
+ if(istype(user) && (user.is_eyes_covered() && level_current <= 2) || !isturf(user.loc))
+ user.balloon_alert(user, "your eyes are concealed from sight.")
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/bloodsucker/targeted/mesmerize/CheckValidTarget(atom/target_atom)
+ . = ..()
+ if(!.)
+ return FALSE
+ return isliving(target_atom)
+
+/datum/action/cooldown/bloodsucker/targeted/mesmerize/CheckCanTarget(atom/target_atom)
+ . = ..()
+ if(!.)
+ return FALSE
+ var/mob/living/current_target = target_atom // We already know it's carbon due to CheckValidTarget()
+ // No mind
+ if(!current_target.mind)
+ owner.balloon_alert(owner, "[current_target] is mindless.")
+ return FALSE
+ // Bloodsucker
+ if(IS_BLOODSUCKER(current_target))
+ owner.balloon_alert(owner, "bloodsuckers are immune to [src].")
+ return FALSE
+ // Dead/Unconscious
+ if(current_target.stat > CONSCIOUS)
+ owner.balloon_alert(owner, "[current_target] is not [(current_target.stat == DEAD || HAS_TRAIT_NOT_FROM(current_target, TRAIT_FAKEDEATH, SPECIES_TRAIT)) ? "alive" : "conscious"].")
+ return FALSE
+ // Target has eyes?
+ if(!current_target.get_organ_slot(ORGAN_SLOT_EYES) && !issilicon(current_target))
+ owner.balloon_alert(owner, "[current_target] has no eyes.")
+ return FALSE
+ // Target blind?
+ if(current_target.is_blind() && !issilicon(current_target))
+ owner.balloon_alert(owner, "[current_target] is blind.")
+ return FALSE
+ // Facing target?
+ if(!is_source_facing_target(owner, current_target)) // in unsorted.dm
+ owner.balloon_alert(owner, "you must be facing [current_target].")
+ return FALSE
+ // Target facing me? (On the floor, they're facing everyone)
+ if(((current_target.mobility_flags & MOBILITY_STAND) && !is_source_facing_target(current_target, owner) && level_current <= 4))
+ owner.balloon_alert(owner, "[current_target] must be facing you.")
+ return FALSE
+
+ // Gone through our checks, let's mark our guy.
+ target_ref = WEAKREF(current_target)
+ return TRUE
+
+/datum/action/cooldown/bloodsucker/targeted/mesmerize/FireTargetedPower(atom/target_atom)
+ . = ..()
+
+ var/mob/living/user = owner
+ var/mob/living/carbon/mesmerized_target = target_ref.resolve()
+
+ if(issilicon(mesmerized_target))
+ var/mob/living/silicon/mesmerized = mesmerized_target
+ mesmerized.emp_act(EMP_HEAVY)
+ owner.balloon_alert(owner, "temporarily shut [mesmerized] down.")
+ power_activated_sucessfully() // PAY COST! BEGIN COOLDOWN!
+ return
+
+ if(istype(mesmerized_target))
+ owner.balloon_alert(owner, "attempting to hypnotically gaze [mesmerized_target]...")
+
+ if(!do_after(user, 4 SECONDS, mesmerized_target, NONE, TRUE, extra_checks = CALLBACK(src, PROC_REF(ContinueActive), user, mesmerized_target)))
+ return
+
+ var/power_time = (9 SECONDS) + level_current * (1.5 SECONDS)
+ if(IS_MONSTERHUNTER(mesmerized_target))
+ to_chat(mesmerized_target, span_notice("You feel your eyes burn for a while, but it passes."))
+ return
+ if(HAS_TRAIT_FROM(mesmerized_target, TRAIT_MUTE, MESMERIZED_TRAIT))
+ owner.balloon_alert(owner, "[mesmerized_target] is already in a hypnotic gaze.")
+ return
+ if(iscarbon(mesmerized_target))
+ owner.balloon_alert(owner, "successfully mesmerized [mesmerized_target].")
+ if(level_current >= 2)
+ ADD_TRAIT(mesmerized_target, TRAIT_MUTE, MESMERIZED_TRAIT)
+ mesmerized_target.Immobilize(power_time)
+ mesmerized_target.adjust_silence(power_time)
+ //mesmerized_target.silent += power_time / 10 // Silent isn't based on ticks.
+ COOLDOWN_START(mesmerized_target, next_move, power_time) // <--- Use direct change instead. We want an unmodified delay to their next move // mesmerized_target.changeNext_move(power_time) // check click.dm
+ ADD_TRAIT(mesmerized_target, TRAIT_NO_TRANSFORM, MESMERIZED_TRAIT)// <--- Fuck it. We tried using next_move, but they could STILL resist. We're just doing a hard freeze.
+ addtimer(CALLBACK(src, PROC_REF(end_mesmerize), user, mesmerized_target), power_time)
+ power_activated_sucessfully() // PAY COST! BEGIN COOLDOWN!
+
+/datum/action/cooldown/bloodsucker/targeted/mesmerize/DeactivatePower()
+ target_ref = null
+ . = ..()
+
+/datum/action/cooldown/bloodsucker/targeted/mesmerize/proc/end_mesmerize(mob/living/user, mob/living/target)
+ REMOVE_TRAITS_IN(target, MESMERIZED_TRAIT)
+ // They Woke Up! (Notice if within view)
+ if(istype(user) && target.stat == CONSCIOUS && (target in view(6, get_turf(user))))
+ owner.balloon_alert(owner, "[target] snapped out of their trance.")
+
+/datum/action/cooldown/bloodsucker/targeted/mesmerize/ContinueActive(mob/living/user, mob/living/target)
+ return ..() && can_use(user) && CheckCanTarget(target)
diff --git a/modular_bandastation/blood_suckers/code/powers/targeted/trespass.dm b/modular_bandastation/blood_suckers/code/powers/targeted/trespass.dm
new file mode 100644
index 0000000000000..2ca615d0ce903
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/powers/targeted/trespass.dm
@@ -0,0 +1,107 @@
+/datum/action/cooldown/bloodsucker/targeted/trespass
+ name = "Trespass"
+ desc = "Become mist and advance two tiles in one direction. Useful for skipping past doors and barricades."
+ button_icon_state = "power_tres"
+ power_explanation = "Trespass:\n\
+ Click anywhere from 1-2 tiles away from you to teleport.\n\
+ This power goes through all obstacles except Walls.\n\
+ Higher levels decrease the sound played from using the Power, and increase the speed of the transition."
+ power_flags = BP_AM_TOGGLE
+ check_flags = BP_CANT_USE_IN_TORPOR | BP_CANT_USE_WHILE_INCAPACITATED | BP_CANT_USE_WHILE_UNCONSCIOUS
+ purchase_flags = BLOODSUCKER_CAN_BUY | VASSAL_CAN_BUY
+ bloodcost = 10
+ sol_multiplier = 5
+ cooldown_time = 8 SECONDS
+ prefire_message = "Select a destination."
+ //target_range = 2
+ var/turf/target_turf // We need to decide where we're going based on where we clicked. It's not actually the tile we clicked.
+
+/datum/action/cooldown/bloodsucker/targeted/trespass/can_use(mob/living/carbon/user, trigger_flags)
+ . = ..()
+ if(!.)
+ return FALSE
+ if(HAS_TRAIT(user, TRAIT_NO_TRANSFORM) || !get_turf(user))
+ return FALSE
+ return TRUE
+
+
+/datum/action/cooldown/bloodsucker/targeted/trespass/CheckValidTarget(atom/target_atom)
+ . = ..()
+ if(!.)
+ return FALSE
+ // Can't target my tile
+ if(target_atom == get_turf(owner) || get_turf(target_atom) == get_turf(owner))
+ return FALSE
+ return TRUE // All we care about is destination. Anything you click is fine.
+
+
+/datum/action/cooldown/bloodsucker/targeted/trespass/CheckCanTarget(atom/target_atom)
+ // NOTE: Do NOT use ..()! We don't want to check distance or anything.
+
+ // Get clicked tile
+ var/final_turf = isturf(target_atom) ? target_atom : get_turf(target_atom)
+
+ // Are either tiles WALLS?
+ var/turf/from_turf = get_turf(owner)
+ var/this_dir // = get_dir(from_turf, target_turf)
+ for(var/i = 1 to 2)
+ // Keep Prev Direction if we've reached final turf
+ if(from_turf != final_turf)
+ this_dir = get_dir(from_turf, final_turf) // Recalculate dir so we don't overshoot on a diagonal.
+ from_turf = get_step(from_turf, this_dir)
+ // ERROR! Wall!
+ if(iswallturf(from_turf))
+ var/wallwarning = (i == 1) ? "in the way" : "at your destination"
+ owner.balloon_alert(owner, "There is a wall [wallwarning].")
+ return FALSE
+ // Done
+ target_turf = from_turf
+
+ return TRUE
+
+/datum/action/cooldown/bloodsucker/targeted/trespass/FireTargetedPower(atom/target_atom)
+ . = ..()
+
+ // Find target turf, at or below Atom
+ var/mob/living/carbon/user = owner
+ var/turf/my_turf = get_turf(owner)
+
+ user.visible_message(
+ span_warning("[user]'s form dissipates into a cloud of mist!"),
+ span_notice("You disspiate into formless mist."),
+ )
+ // Effect Origin
+ var/sound_strength = max(60, 70 - level_current * 10)
+ playsound(get_turf(owner), 'sound/magic/summon_karp.ogg', vol = sound_strength, vary = TRUE)
+ var/datum/effect_system/steam_spread/bloodsucker/puff = new /datum/effect_system/steam_spread()
+ puff.set_up(3, 0, my_turf)
+ puff.start()
+
+ var/mist_delay = max(5, 20 - level_current * 2.5) // Level up and do this faster.
+
+ // Freeze Me
+ user.Stun(mist_delay, ignore_canstun = TRUE)
+ user.density = FALSE
+ var/invis_was = user.invisibility
+ user.invisibility = INVISIBILITY_MAXIMUM
+
+ // Wait...
+ sleep(mist_delay / 2)
+ // Move & Freeze
+ if(isturf(target_turf))
+ do_teleport(owner, target_turf, no_effects=TRUE, channel = TELEPORT_CHANNEL_QUANTUM) // in teleport.dm?
+ user.Stun(mist_delay / 2, ignore_canstun = TRUE)
+
+ // Wait...
+ sleep(mist_delay / 2)
+ // Un-Hide & Freeze
+ user.dir = get_dir(my_turf, target_turf)
+ user.Stun(mist_delay / 2, ignore_canstun = TRUE)
+ user.density = 1
+ user.invisibility = invis_was
+ // Effect Destination
+ playsound(get_turf(owner), 'sound/magic/summon_karp.ogg', vol = 60, vary = TRUE)
+ puff = new /datum/effect_system/steam_spread/()
+ puff.effect_type = /obj/effect/particle_effect/fluid/smoke/vampsmoke
+ puff.set_up(3, 0, target_turf)
+ puff.start()
diff --git a/modular_bandastation/blood_suckers/code/powers/tremere/_base_tremere.dm b/modular_bandastation/blood_suckers/code/powers/tremere/_base_tremere.dm
new file mode 100644
index 0000000000000..003fa62410669
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/powers/tremere/_base_tremere.dm
@@ -0,0 +1,27 @@
+/**
+ * # Tremere Powers
+ *
+ * This file is for Tremere power procs and Bloodsucker procs that deals exclusively with Tremere.
+ * Tremere has quite a bit of unique things to it, so I thought it's own subtype would be nice
+ */
+
+/datum/action/cooldown/bloodsucker/targeted/tremere
+ name = "Tremere Gift"
+ desc = "A Tremere exclusive gift."
+ button_icon_state = "power_auspex"
+ background_icon_state = "tremere_power_off"
+ active_background_icon_state = "tremere_power_on"
+ base_background_icon_state = "tremere_power_off"
+ button_icon = 'modular_bandastation/blood_suckers/icons/actions_tremere_bloodsucker.dmi'
+ background_icon = 'modular_bandastation/blood_suckers/icons/actions_tremere_bloodsucker.dmi'
+
+ // Tremere powers don't level up, we have them hardcoded.
+ level_current = 0
+ // Re-defining these as we want total control over them
+ power_flags = BP_AM_TOGGLE|BP_AM_STATIC_COOLDOWN
+ purchase_flags = TREMERE_CAN_BUY
+ // Targeted stuff
+ power_activates_immediately = FALSE
+
+ ///The upgraded version of this Power. 'null' means it's the max level.
+ var/upgraded_power = null
diff --git a/modular_bandastation/blood_suckers/code/powers/tremere/auspex.dm b/modular_bandastation/blood_suckers/code/powers/tremere/auspex.dm
new file mode 100644
index 0000000000000..5e0a082b59692
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/powers/tremere/auspex.dm
@@ -0,0 +1,120 @@
+/**
+ * # Auspex
+ *
+ * Level 1 - Cloak of Darkness until clicking an area, teleports the user to the selected area (max 2 tile)
+ * Level 2 - Cloak of Darkness until clicking an area, teleports the user to the selected area (max 3 tiles)
+ * Level 3 - Cloak of Darkness until clicking an area, teleports the user to the selected area
+ * Level 4 - Cloak of Darkness until clicking an area, teleports the user to the selected area, causes nearby people to bleed.
+ * Level 5 - Cloak of Darkness until clicking an area, teleports the user to the selected area, causes nearby people to fall asleep.
+ */
+
+// Look to /datum/action/cooldown/spell/pointed/void_phase for help.
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/auspex
+ name = "Level 1: Auspex"
+ upgraded_power = /datum/action/cooldown/bloodsucker/targeted/tremere/auspex/two
+ level_current = 1
+ desc = "Hide yourself within a Cloak of Darkness, click on an area to teleport up to 2 tiles away."
+ button_icon_state = "power_auspex"
+ power_explanation = "Level 1: Auspex:\n\
+ When Activated, you will be hidden in a Cloak of Darkness.\n\
+ Click any area up to 2 tile away to teleport there, ending the Power."
+ check_flags = BP_CANT_USE_IN_TORPOR | BP_CANT_USE_WHILE_INCAPACITATED | BP_CANT_USE_WHILE_UNCONSCIOUS
+ bloodcost = 5
+ constant_bloodcost = 2
+ sol_multiplier = 4
+ cooldown_time = 12 SECONDS
+ target_range = 2
+ prefire_message = "Where do you wish to teleport to?"
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/auspex/two
+ name = "Level 2: Auspex"
+ upgraded_power = /datum/action/cooldown/bloodsucker/targeted/tremere/auspex/three
+ level_current = 2
+ desc = "Hide yourself within a Cloak of Darkness, click on an area to teleport up to 3 tiles away."
+ power_explanation = "Level 2: Auspex:\n\
+ When Activated, you will be hidden in a Cloak of Darkness.\n\
+ Click any area up to 3 tile away to teleport there, ending the Power."
+ bloodcost = 10
+ cooldown_time = 10 SECONDS
+ target_range = 3
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/auspex/three
+ name = "Level 3: Auspex"
+ upgraded_power = /datum/action/cooldown/bloodsucker/targeted/tremere/auspex/advanced
+ level_current = 3
+ desc = "Hide yourself within a Cloak of Darkness, click on an area to teleport."
+ power_explanation = "Level 3: Auspex:\n\
+ When Activated, you will be hidden in a Cloak of Darkness.\n\
+ Click any area up to teleport there, ending the Power."
+ bloodcost = 15
+ cooldown_time = 8 SECONDS
+ target_range = null
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/auspex/advanced
+ name = "Level 4: Auspex"
+ upgraded_power = /datum/action/cooldown/bloodsucker/targeted/tremere/auspex/advanced/two
+ level_current = 4
+ desc = "Hide yourself within a Cloak of Darkness, click on an area to teleport, leaving nearby people bleeding."
+ power_explanation = "Level 4: Auspex:\n\
+ When Activated, you will be hidden in a Cloak of Darkness.\n\
+ Click any area up to teleport there, ending the Power and causing people at your end location to start bleeding."
+ background_icon_state = "tremere_power_gold_off"
+ active_background_icon_state = "tremere_power_gold_on"
+ base_background_icon_state = "tremere_power_gold_off"
+ bloodcost = 20
+ cooldown_time = 6 SECONDS
+ target_range = null
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/auspex/advanced/two
+ name = "Level 5: Auspex"
+ upgraded_power = null
+ level_current = 5
+ desc = "Hide yourself within a Cloak of Darkness, click on an area to teleport, leaving nearby people bleeding and asleep."
+ power_explanation = "Level 5: Auspex:\n\
+ When Activated, you will be hidden in a Cloak of Darkness.\n\
+ Click any area up to teleport there, ending the Power and causing people at your end location to fall over in pain."
+ bloodcost = 25
+ cooldown_time = 8 SECONDS
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/auspex/CheckValidTarget(atom/target_atom)
+ . = ..()
+ if(!.)
+ return FALSE
+ return isturf(target_atom)
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/auspex/ActivatePower(trigger_flags)
+ . = ..()
+ owner.AddElement(/datum/element/digitalcamo)
+ animate(owner, alpha = 15, time = 1 SECONDS)
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/auspex/DeactivatePower()
+ animate(owner, alpha = 255, time = 1 SECONDS)
+ owner.RemoveElement(/datum/element/digitalcamo)
+ return ..()
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/auspex/FireTargetedPower(atom/target_atom)
+ . = ..()
+ var/mob/living/user = owner
+ var/turf/targeted_turf = get_turf(target_atom)
+ auspex_blink(user, targeted_turf)
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/auspex/proc/auspex_blink(mob/living/user, turf/targeted_turf)
+ playsound(user, 'sound/magic/summon_karp.ogg', 60)
+ playsound(targeted_turf, 'sound/magic/summon_karp.ogg', 60)
+
+ new /obj/effect/particle_effect/fluid/smoke/vampsmoke(user.drop_location())
+ new /obj/effect/particle_effect/fluid/smoke/vampsmoke(targeted_turf)
+
+ for(var/mob/living/carbon/living_mob in range(1, targeted_turf)-user)
+ if(IS_BLOODSUCKER(living_mob) || IS_VASSAL(living_mob))
+ continue
+ if(level_current >= 4)
+ var/obj/item/bodypart/bodypart = pick(living_mob.bodyparts)
+ living_mob.cause_wound_of_type_and_severity(WOUND_SLASH, bodypart, WOUND_SEVERITY_MODERATE, WOUND_SEVERITY_CRITICAL)
+ living_mob.adjustBruteLoss(15)
+ if(level_current >= 5)
+ living_mob.Knockdown(10 SECONDS, ignore_canstun = TRUE)
+
+ do_teleport(owner, targeted_turf, no_effects = TRUE, channel = TELEPORT_CHANNEL_QUANTUM)
+ power_activated_sucessfully()
diff --git a/modular_bandastation/blood_suckers/code/powers/tremere/dominate.dm b/modular_bandastation/blood_suckers/code/powers/tremere/dominate.dm
new file mode 100644
index 0000000000000..ac7edf31fd0b2
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/powers/tremere/dominate.dm
@@ -0,0 +1,191 @@
+/**
+ * # Dominate;
+ *
+ * Level 1 - Mesmerizes target
+ * Level 2 - Mesmerizes and mutes target
+ * Level 3 - Mesmerizes, blinds and mutes target
+ * Level 4 - Target (if at least in crit & has a mind) will revive as a Mute/Deaf Vassal for 5 minutes before dying.
+ * Level 5 - Target (if at least in crit & has a mind) will revive as a Vassal for 8 minutes before dying.
+ */
+
+// Copied from mesmerize.dm
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/dominate
+ name = "Level 1: Dominate"
+ upgraded_power = /datum/action/cooldown/bloodsucker/targeted/tremere/dominate/two
+ level_current = 1
+ desc = "Mesmerize any foe who stands still long enough."
+ button_icon_state = "power_dominate"
+ power_explanation = "Level 1: Dominate:\n\
+ Click any person to, after a 4 second timer, Mesmerize them.\n\
+ This will completely immobilize them for the next 10.5 seconds."
+ check_flags = BP_CANT_USE_IN_TORPOR | BP_CANT_USE_IN_FRENZY | BP_CANT_USE_WHILE_UNCONSCIOUS | BP_CANT_USE_DURING_SOL
+ bloodcost = 15
+ constant_bloodcost = 2
+ cooldown_time = 50 SECONDS
+ target_range = 6
+ prefire_message = "Select a target."
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/dominate/two
+ name = "Level 2: Dominate"
+ upgraded_power = /datum/action/cooldown/bloodsucker/targeted/tremere/dominate/three
+ level_current = 2
+ desc = "Mesmerize and mute any foe who stands still long enough."
+ power_explanation = "Level 2: Dominate:\n\
+ Click any person to, after a 4 second timer, Mesmerize them.\n\
+ This will completely immobilize and mute them for the next 12 seconds."
+ bloodcost = 20
+ cooldown_time = 40 SECONDS
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/dominate/three
+ name = "Level 3: Dominate"
+ upgraded_power = /datum/action/cooldown/bloodsucker/targeted/tremere/dominate/advanced
+ level_current = 3
+ desc = "Mesmerize, mute and blind any foe who stands still long enough."
+ power_explanation = "Level 3: Dominate:\n\
+ Click any person to, after a 4 second timer, Mesmerize them.\n\
+ This will completely immobilize, mute, and blind them for the next 13.5 seconds."
+ bloodcost = 30
+ cooldown_time = 35 SECONDS
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/dominate/CheckValidTarget(atom/target_atom)
+ . = ..()
+ if(!.)
+ return FALSE
+ return isliving(target_atom)
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/dominate/CheckCanTarget(atom/target_atom)
+ . = ..()
+ if(!.)
+ return FALSE
+ var/mob/living/selected_target = target_atom
+ if(!selected_target.mind)
+ owner.balloon_alert(owner, "[selected_target] is mindless.")
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/dominate/advanced
+ name = "Level 4: Possession"
+ upgraded_power = /datum/action/cooldown/bloodsucker/targeted/tremere/dominate/advanced/two
+ level_current = 4
+ desc = "Mesmerize, mute and blind any foe who stands still long enough, or convert the damaged to temporary Vassals."
+ power_explanation = "Level 4: Possession:\n\
+ Click any person to, after a 4 second timer, Mesmerize them.\n\
+ This will completely immobilize, mute, and blind them for the next 13.5 seconds.\n\
+ However, while adjacent to the target, if your target is in critical condition or dead, they will instead be turned into a temporary Vassal.\n\
+ If you use this on a currently dead normal Vassal, you will instead revive them normally.\n\
+ Despite being Mute and Deaf, they will still have complete loyalty to you, until their death in 5 minutes upon use."
+ background_icon_state = "tremere_power_gold_off"
+ active_background_icon_state = "tremere_power_gold_on"
+ base_background_icon_state = "tremere_power_gold_off"
+ bloodcost = 80
+ cooldown_time = 3 MINUTES
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/dominate/advanced/two
+ name = "Level 5: Possession"
+ desc = "Mesmerize, mute and blind any foe who stands still long enough, or convert the damaged to temporary Vassals."
+ level_current = 5
+ upgraded_power = null
+ power_explanation = "Level 5: Possession:\n\
+ Click any person to, after a 4 second timer, Mesmerize them.\n\
+ This will completely immobilize, mute, and blind them for the next 13.5 seconds.\n\
+ However, while adjacent to the target, if your target is in critical condition or dead, they will instead be turned into a temporary Vassal.\n\
+ If you use this on a currently dead normal Vassal, you will instead revive them normally.\n\
+ They will have complete loyalty to you, until their death in 8 minutes upon use."
+ bloodcost = 100
+ cooldown_time = 2 MINUTES
+
+// The advanced version
+/datum/action/cooldown/bloodsucker/targeted/tremere/dominate/advanced/CheckCanTarget(atom/target_atom)
+ . = ..()
+ if(!.)
+ return FALSE
+ var/mob/living/selected_target = target_atom
+ if((IS_VASSAL(selected_target) || selected_target.stat >= SOFT_CRIT) && !owner.Adjacent(selected_target))
+ owner.balloon_alert(owner, "out of range.")
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/dominate/FireTargetedPower(atom/target_atom)
+ . = ..()
+ var/mob/living/target = target_atom
+ var/mob/living/user = owner
+ if(target.stat >= SOFT_CRIT && user.Adjacent(target) && level_current >= 4)
+ attempt_vassalize(target, user)
+ return
+ else if(IS_VASSAL(target))
+ owner.balloon_alert(owner, "vassal cant be revived")
+ return
+ attempt_mesmerize(target, user)
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/dominate/proc/attempt_mesmerize(mob/living/target, mob/living/user)
+ owner.balloon_alert(owner, "attempting to mesmerize.")
+ if(!do_after(user, 3 SECONDS, target, NONE, TRUE))
+ return
+
+ power_activated_sucessfully()
+ var/power_time = 90 + level_current * 15
+ if(IS_MONSTERHUNTER(target))
+ to_chat(target, span_notice("You feel you something crawling under your skin, but it passes."))
+ return
+ if(HAS_TRAIT_FROM(target, TRAIT_MUTE, BLOODSUCKER_TRAIT))
+ owner.balloon_alert(owner, "[target] is already in some form of hypnotic gaze.")
+ return
+ if(iscarbon(target))
+ var/mob/living/carbon/mesmerized = target
+ owner.balloon_alert(owner, "successfully mesmerized [mesmerized].")
+ if(level_current >= 2)
+ ADD_TRAIT(target, TRAIT_MUTE, BLOODSUCKER_TRAIT)
+ if(level_current >= 3)
+ target.become_blind(BLOODSUCKER_TRAIT)
+ mesmerized.Immobilize(power_time)
+ mesmerized.next_move = world.time + power_time
+ ADD_TRAIT(mesmerized, TRAIT_NO_TRANSFORM, BLOODSUCKER_TRAIT)
+ addtimer(CALLBACK(src, PROC_REF(end_mesmerize), user, target), power_time)
+ if(issilicon(target))
+ var/mob/living/silicon/mesmerized = target
+ mesmerized.emp_act(EMP_HEAVY)
+ owner.balloon_alert(owner, "temporarily shut [mesmerized] down.")
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/proc/end_mesmerize(mob/living/user, mob/living/target)
+ REMOVE_TRAIT(target, TRAIT_NO_TRANSFORM, BLOODSUCKER_TRAIT)
+ target.cure_blind(BLOODSUCKER_TRAIT)
+ REMOVE_TRAIT(target, TRAIT_MUTE, BLOODSUCKER_TRAIT)
+ if(istype(user) && target.stat == CONSCIOUS && (target in view(6, get_turf(user))))
+ owner.balloon_alert(owner, "[target] snapped out of their trance.")
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/dominate/proc/attempt_vassalize(mob/living/target, mob/living/user)
+ owner.balloon_alert(owner, "attempting to vassalize.")
+ if(!do_after(user, 6 SECONDS, target, NONE, TRUE))
+ return
+
+ if(IS_VASSAL(target))
+ power_activated_sucessfully()
+ to_chat(user, span_warning("We revive [target]!"))
+ target.mind.grab_ghost()
+ target.revive(ADMIN_HEAL_ALL)
+ return
+ if(IS_MONSTERHUNTER(target))
+ to_chat(target, span_notice("Their body refuses to react..."))
+ return
+ if(!bloodsuckerdatum_power.make_vassal(target))
+ return
+ power_activated_sucessfully()
+ to_chat(user, span_warning("We revive [target]!"))
+ target.mind.grab_ghost()
+ target.revive(ADMIN_HEAL_ALL)
+ var/datum/antagonist/vassal/vassaldatum = target.mind.has_antag_datum(/datum/antagonist/vassal)
+ vassaldatum.special_type = TREMERE_VASSAL //don't turn them into a favorite please
+ var/living_time
+ if(level_current == 4)
+ living_time = 5 MINUTES
+ target.add_traits(list(TRAIT_MUTE, TRAIT_DEAF), BLOODSUCKER_TRAIT)
+ else if(level_current == 5)
+ living_time = 8 MINUTES
+ addtimer(CALLBACK(src, PROC_REF(end_possession), target), living_time)
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/proc/end_possession(mob/living/user)
+ user.remove_traits(list(TRAIT_MUTE, TRAIT_DEAF), BLOODSUCKER_TRAIT)
+ user.mind.remove_antag_datum(/datum/antagonist/vassal)
+ to_chat(user, span_warning("You feel the Blood of your Master run out!"))
+ user.death()
diff --git a/modular_bandastation/blood_suckers/code/powers/tremere/thaumaturgey.dm b/modular_bandastation/blood_suckers/code/powers/tremere/thaumaturgey.dm
new file mode 100644
index 0000000000000..9fab92bfddc90
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/powers/tremere/thaumaturgey.dm
@@ -0,0 +1,187 @@
+/**
+ * # Thaumaturgy
+ *
+ * Level 1 - One shot bloodbeam spell
+ * Level 2 - Bloodbeam spell - Gives them a Blood shield until they use Bloodbeam
+ * Level 3 - Bloodbeam spell that breaks open lockers/doors - Gives them a Blood shield until they use Bloodbeam
+ * Level 4 - Bloodbeam spell that breaks open lockers/doors + double damage to victims - Gives them a Blood shield until they use Bloodbeam
+ * Level 5 - Bloodbeam spell that breaks open lockers/doors + double damage & steals blood - Gives them a Blood shield until they use Bloodbeam
+ */
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/thaumaturgy
+ name = "Level 1: Thaumaturgy"
+ upgraded_power = /datum/action/cooldown/bloodsucker/targeted/tremere/thaumaturgy/two
+ desc = "Fire a blood bolt at your enemy, dealing Burn damage."
+ level_current = 1
+ button_icon_state = "power_thaumaturgy"
+ power_explanation = "Thaumaturgy:\n\
+ Gives you a one shot blood bolt spell, firing it at a person deals 20 Burn damage"
+ check_flags = BP_CANT_USE_IN_TORPOR | BP_CANT_USE_IN_FRENZY | BP_CANT_USE_WHILE_UNCONSCIOUS
+ bloodcost = 20
+ constant_bloodcost = 0
+ sol_multiplier = 4
+ cooldown_time = 6 SECONDS
+ prefire_message = "Click where you wish to fire."
+ ///Blood shield given while this Power is active.
+ var/datum/weakref/blood_shield
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/thaumaturgy/two
+ name = "Level 2: Thaumaturgy"
+ upgraded_power = /datum/action/cooldown/bloodsucker/targeted/tremere/thaumaturgy/three
+ desc = "Create a Blood shield and fire a blood bolt at your enemy, dealing Burn damage."
+ level_current = 2
+ power_explanation = "Thaumaturgy:\n\
+ Activating Thaumaturgy will temporarily give you a Blood Shield,\n\
+ The blood shield has a 75% block chance, but costs 15 Blood per hit to maintain.\n\
+ You will also have the ability to fire a Blood beam, ending the Power.\n\
+ If the Blood beam hits a person, it will deal 20 Burn damage."
+ prefire_message = "Click where you wish to fire (using your power removes blood shield)."
+ bloodcost = 40
+ cooldown_time = 4 SECONDS
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/thaumaturgy/three
+ name = "Level 3: Thaumaturgy"
+ upgraded_power = /datum/action/cooldown/bloodsucker/targeted/tremere/thaumaturgy/advanced
+ desc = "Create a Blood shield and fire a blood bolt, dealing Burn damage and opening doors/lockers."
+ level_current = 3
+ power_explanation = "Thaumaturgy:\n\
+ Activating Thaumaturgy will temporarily give you a Blood Shield,\n\
+ The blood shield has a 75% block chance, but costs 15 Blood per hit to maintain.\n\
+ You will also have the ability to fire a Blood beam, ending the Power.\n\
+ If the Blood beam hits a person, it will deal 20 Burn damage. If it hits a locker or door, it will break it open."
+ bloodcost = 50
+ cooldown_time = 6 SECONDS
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/thaumaturgy/advanced
+ name = "Level 4: Blood Strike"
+ upgraded_power = /datum/action/cooldown/bloodsucker/targeted/tremere/thaumaturgy/advanced/two
+ desc = "Create a Blood shield and fire a blood bolt, dealing Burn damage and opening doors/lockers."
+ level_current = 4
+ power_explanation = "Thaumaturgy:\n\
+ Activating Thaumaturgy will temporarily give you a Blood Shield,\n\
+ The blood shield has a 75% block chance, but costs 15 Blood per hit to maintain.\n\
+ You will also have the ability to fire a Blood beam, ending the Power.\n\
+ If the Blood beam hits a person, it will deal 40 Burn damage.\n\
+ If it hits a locker or door, it will break it open."
+ background_icon_state = "tremere_power_gold_off"
+ active_background_icon_state = "tremere_power_gold_on"
+ base_background_icon_state = "tremere_power_gold_off"
+ prefire_message = "Click where you wish to fire (using your power removes blood shield)."
+ bloodcost = 60
+ cooldown_time = 6 SECONDS
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/thaumaturgy/advanced/two
+ name = "Level 5: Blood Strike"
+ upgraded_power = null
+ desc = "Create a Blood shield and fire a blood bolt, dealing Burn damage, stealing Blood and opening doors/lockers."
+ level_current = 5
+ power_explanation = "Thaumaturgy:\n\
+ Activating Thaumaturgy will temporarily give you a Blood Shield,\n\
+ The blood shield has a 75% block chance, but costs 15 Blood per hit to maintain.\n\
+ You will also have the ability to fire a Blood beam, ending the Power.\n\
+ If the Blood beam hits a person, it will deal 40 Burn damage and steal blood to feed yourself, though at a net-negative.\n\
+ If it hits a locker or door, it will break it open."
+ bloodcost = 80
+ cooldown_time = 8 SECONDS
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/thaumaturgy/ActivatePower(trigger_flags)
+ . = ..()
+ owner.balloon_alert(owner, "you start thaumaturgy")
+ if(level_current >= 2) // Only if we're at least level 2.
+ var/obj/item/shield/bloodsucker/new_shield = new
+ blood_shield = WEAKREF(new_shield)
+ if(!owner.put_in_inactive_hand(new_shield))
+ owner.balloon_alert(owner, "off hand is full!")
+ to_chat(owner, span_notice("Blood shield couldn't be activated as your off hand is full."))
+ return FALSE
+ owner.visible_message(
+ span_warning("[owner]\'s hands begins to bleed and forms into a blood shield!"),
+ span_warning("We activate our Blood shield!"),
+ span_hear("You hear liquids forming together."),
+ )
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/thaumaturgy/DeactivatePower()
+ if(blood_shield)
+ QDEL_NULL(blood_shield)
+ return ..()
+
+/datum/action/cooldown/bloodsucker/targeted/tremere/thaumaturgy/FireTargetedPower(atom/target_atom)
+ . = ..()
+
+ var/mob/living/user = owner
+ owner.balloon_alert(owner, "you fire a blood bolt!")
+ to_chat(user, span_warning("You fire a blood bolt!"))
+ user.changeNext_move(CLICK_CD_RANGE)
+ user.newtonian_move(get_dir(target_atom, user))
+ var/obj/projectile/magic/arcane_barrage/bloodsucker/magic_9ball = new(user.loc)
+ magic_9ball.bloodsucker_power = src
+ magic_9ball.firer = user
+ magic_9ball.def_zone = ran_zone(user.zone_selected)
+ magic_9ball.preparePixelProjectile(target_atom, user)
+ INVOKE_ASYNC(magic_9ball, TYPE_PROC_REF(/obj/projectile, fire))
+ playsound(user, 'sound/magic/wand_teleport.ogg', 60, TRUE)
+ power_activated_sucessfully()
+
+/**
+ * # Blood Bolt
+ *
+ * This is the projectile this Power will fire.
+ */
+/obj/projectile/magic/arcane_barrage/bloodsucker
+ name = "blood bolt"
+ icon_state = "mini_leaper"
+ damage = 20
+ var/datum/action/cooldown/bloodsucker/targeted/tremere/thaumaturgy/bloodsucker_power
+
+/obj/projectile/magic/arcane_barrage/bloodsucker/on_hit(target, blocked, pierce_hit)
+ if(istype(target, /obj/structure/closet) && bloodsucker_power.level_current >= 3)
+ var/obj/structure/closet/hit_closet = target
+ if(hit_closet)
+ hit_closet.welded = FALSE
+ hit_closet.locked = FALSE
+ hit_closet.broken = TRUE
+ hit_closet.update_appearance()
+ qdel(src)
+ return BULLET_ACT_HIT
+ if(istype(target, /obj/machinery/door) && bloodsucker_power.level_current >= 3)
+ var/obj/machinery/door/hit_airlock = target
+ hit_airlock.open(2)
+ qdel(src)
+ return BULLET_ACT_HIT
+ if(ismob(target))
+ if(bloodsucker_power.level_current >= 4)
+ damage = 40
+ if(bloodsucker_power.level_current >= 5)
+ var/mob/living/person_hit = target
+ person_hit.blood_volume -= 60
+ bloodsucker_power.bloodsuckerdatum_power.AddBloodVolume(60)
+ qdel(src)
+ return BULLET_ACT_HIT
+ . = ..()
+
+/**
+ * # Blood Shield
+ *
+ * The shield spawned when using Thaumaturgy when strong enough.
+ * Copied mostly from '/obj/item/shield/changeling'
+ */
+
+/obj/item/shield/bloodsucker
+ name = "blood shield"
+ desc = "A shield made out of blood, requiring blood to sustain hits."
+ item_flags = ABSTRACT | DROPDEL
+ icon = 'modular_bandastation/blood_suckers/icons/vamp_obj.dmi'
+ icon_state = "blood_shield"
+ lefthand_file = 'modular_bandastation/blood_suckers/icons/bs_leftinhand.dmi'
+ righthand_file = 'modular_bandastation/blood_suckers/icons/bs_rightinhand.dmi'
+ block_chance = 75
+
+/obj/item/shield/bloodsucker/Initialize()
+ . = ..()
+ ADD_TRAIT(src, TRAIT_NODROP, BLOODSUCKER_TRAIT)
+
+/obj/item/shield/bloodsucker/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK)
+ var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.mind.has_antag_datum(/datum/antagonist/bloodsucker)
+ if(bloodsuckerdatum)
+ bloodsuckerdatum.AddBloodVolume(-15)
+ return ..()
diff --git a/modular_bandastation/blood_suckers/code/powers/vassal/distress.dm b/modular_bandastation/blood_suckers/code/powers/vassal/distress.dm
new file mode 100644
index 0000000000000..71a125dc8433c
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/powers/vassal/distress.dm
@@ -0,0 +1,22 @@
+/datum/action/cooldown/bloodsucker/distress
+ name = "Distress"
+ desc = "Injure yourself, allowing you to make a desperate call for help to your Master."
+ button_icon_state = "power_distress"
+ power_explanation = "Distress:\n\
+ Use this Power from anywhere and your Master Bloodsucker will instantly be alerted of your location."
+ power_flags = NONE
+ check_flags = NONE
+ purchase_flags = NONE
+ bloodcost = 10
+ cooldown_time = 10 SECONDS
+
+/datum/action/cooldown/bloodsucker/distress/ActivatePower(trigger_flags)
+ . = ..()
+ var/turf/open/floor/target_area = get_area(owner)
+ var/datum/antagonist/vassal/vassaldatum = owner.mind.has_antag_datum(/datum/antagonist/vassal)
+
+ owner.balloon_alert(owner, "you call out for your master!")
+ to_chat(vassaldatum.master.owner, span_userdanger("[owner], your loyal Vassal, is desperately calling for aid at [target_area]!"))
+
+ var/mob/living/user = owner
+ user.take_overall_damage(brute = 10)
diff --git a/modular_bandastation/blood_suckers/code/powers/vassal/recuperate.dm b/modular_bandastation/blood_suckers/code/powers/vassal/recuperate.dm
new file mode 100644
index 0000000000000..b93f5c0b4fa6b
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/powers/vassal/recuperate.dm
@@ -0,0 +1,70 @@
+/// Used by Vassals
+/datum/action/cooldown/bloodsucker/recuperate
+ name = "Sanguine Recuperation"
+ desc = "Slowly heals you overtime using your master's blood, in exchange for some of your own blood and effort."
+ button_icon_state = "power_recup"
+ power_explanation = "Recuperate:\n\
+ Activating this Power will begin to heal your wounds.\n\
+ You will heal Brute and Toxin damage, at the cost of Stamina damage, and blood from both you and your Master.\n\
+ If you aren't a bloodless race, you will additionally heal Burn damage.\n\
+ The power will cancel out if you are incapacitated or dead."
+ power_flags = BP_AM_TOGGLE
+ check_flags = BP_CANT_USE_WHILE_UNCONSCIOUS
+ purchase_flags = NONE
+ bloodcost = 1.5
+ cooldown_time = 10 SECONDS
+
+/datum/action/cooldown/bloodsucker/recuperate/can_use(mob/living/carbon/user, trigger_flags)
+ . = ..()
+ if(!.)
+ return
+ if(user.blood_volume <= BLOOD_VOLUME_OKAY)
+ user.balloon_alert(user, "not enough blood!")
+ return FALSE
+ if(user.stat >= DEAD)
+ user.balloon_alert(user, "you are incapacitated...")
+ return FALSE
+ return TRUE
+
+
+/datum/action/cooldown/bloodsucker/recuperate/ActivatePower(trigger_flags)
+ . = ..()
+ to_chat(owner, span_notice("Your muscles clench as your master's immortal blood mixes with your own, knitting your wounds."))
+ owner.balloon_alert(owner, "recuperate turned on.")
+
+/datum/action/cooldown/bloodsucker/recuperate/process(seconds_per_tick)
+ . = ..()
+ if(!.)
+ return
+ if(!active)
+ return
+ var/mob/living/carbon/user = owner
+ var/datum/antagonist/vassal/vassaldatum = IS_VASSAL(user)
+ vassaldatum.master.AddBloodVolume(-1)
+ user.set_timed_status_effect(5 SECONDS, /datum/status_effect/jitter, only_if_higher = TRUE)
+ user.stamina.adjust(-bloodcost * 1.1)
+ user.heal_overall_damage(brute = 2.5, updating_health = FALSE)
+ user.adjustToxLoss(-2, updating_health = FALSE, forced = TRUE)
+ // Plasmamen won't lose blood, they don't have any, so they don't heal from Burn.
+ if(!HAS_TRAIT(user, TRAIT_NOBLOOD))
+ user.blood_volume -= bloodcost
+ user.adjustFireLoss(-1.5, updating_health = FALSE)
+ user.updatehealth() // only update health once after we've healed everything we might've
+ // Stop Bleeding
+ if(istype(user) && user.is_bleeding())
+ for(var/obj/item/bodypart/part in user.bodyparts)
+ part.generic_bleedstacks--
+
+/datum/action/cooldown/bloodsucker/recuperate/ContinueActive(mob/living/user, mob/living/target)
+ if(QDELETED(user))
+ return FALSE
+ if(user.stat >= DEAD)
+ return FALSE
+ if(user.incapacitated())
+ owner.balloon_alert(owner, "too exhausted...")
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/bloodsucker/recuperate/DeactivatePower()
+ owner?.balloon_alert(owner, "recuperate turned off.")
+ return ..()
diff --git a/modular_bandastation/blood_suckers/code/powers/vassal/vassal_fold.dm b/modular_bandastation/blood_suckers/code/powers/vassal/vassal_fold.dm
new file mode 100644
index 0000000000000..16c9c058a329d
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/powers/vassal/vassal_fold.dm
@@ -0,0 +1,90 @@
+/datum/action/cooldown/bloodsucker/vassal_blood
+ name = "Help Vassal"
+ desc = "Bring an ex-Vassal back into the fold, or create blood using a bag. RMB: Check Vassal status."
+ button_icon_state = "power_torpor"
+ power_explanation = "Help Vassal:\n\
+ Use this power while you have an ex-Vassal grabbed to bring them back into the fold. \
+ Use this power with a bloodbag in your hand to instead fill it with Vampiric Blood which \
+ can be used to reset ex-vassal deconversion timers. \
+ Right-Click will show the status of all Vassals."
+ power_flags = NONE
+ check_flags = NONE
+ purchase_flags = NONE
+ bloodcost = 10
+ cooldown_time = 10 SECONDS
+
+ ///Bloodbag we have in our hands.
+ var/obj/item/reagent_containers/blood/bloodbag
+ ///Weakref to a target we're bringing into the fold.
+ var/datum/weakref/target_ref
+
+/datum/action/cooldown/bloodsucker/vassal_blood/can_use(mob/living/carbon/user, trigger_flags)
+ . = ..()
+ if(!.)
+ return FALSE
+ var/datum/antagonist/vassal/revenge/revenge_vassal = owner.mind.has_antag_datum(/datum/antagonist/ex_vassal)
+ if(revenge_vassal)
+ return FALSE
+
+ if(trigger_flags & TRIGGER_SECONDARY_ACTION)
+ if(!length(revenge_vassal.ex_vassals))
+ owner.balloon_alert(owner, "no vassals!")
+ return FALSE
+ return TRUE
+
+ if(owner.pulling && isliving(owner.pulling))
+ var/mob/living/pulled_target = owner.pulling
+ var/datum/antagonist/ex_vassal/former_vassal = pulled_target.mind.has_antag_datum(/datum/antagonist/ex_vassal)
+ if(!former_vassal)
+ owner.balloon_alert(owner, "not a former vassal!")
+ return FALSE
+ target_ref = WEAKREF(owner.pulling)
+ return TRUE
+
+ var/obj/item/reagent_containers/blood/blood_bag = user.is_holding_item_of_type(/obj/item/reagent_containers/blood)
+ if(QDELETED(blood_bag))
+ owner.balloon_alert(owner, "blood bag needed!")
+ return FALSE
+ if(istype(blood_bag, /obj/item/reagent_containers/blood/o_minus/bloodsucker))
+ owner.balloon_alert(owner, "already bloodsucker blood!")
+
+ bloodbag = blood_bag
+ return TRUE
+
+/datum/action/cooldown/bloodsucker/vassal_blood/ActivatePower(trigger_flags)
+ . = ..()
+ var/datum/antagonist/vassal/revenge/revenge_vassal = owner.mind.has_antag_datum(/datum/antagonist/vassal/revenge)
+ if(trigger_flags & TRIGGER_SECONDARY_ACTION)
+ for(var/datum/antagonist/ex_vassal/former_vassals as anything in revenge_vassal.ex_vassals)
+ var/information = "[former_vassals.owner.current]"
+ information += " - has [round(COOLDOWN_TIMELEFT(former_vassals, blood_timer) / 600)] minutes left of Blood"
+ var/turf/open/floor/target_area = get_area(owner)
+ if(target_area)
+ information += " - currently at [target_area]."
+ if(former_vassals.owner.current.stat >= DEAD)
+ information += " - DEAD."
+
+ to_chat(owner, "[information]")
+
+ DeactivatePower()
+ return
+
+ if(target_ref)
+ var/mob/living/target = target_ref.resolve()
+ var/datum/antagonist/ex_vassal/former_vassal = target.mind.has_antag_datum(/datum/antagonist/ex_vassal)
+ if(QDELETED(former_vassal) || former_vassal.revenge_vassal)
+ target_ref = null
+ return
+ if(do_after(owner, 5 SECONDS, target))
+ former_vassal.return_to_fold(revenge_vassal)
+ target_ref = null
+ DeactivatePower()
+ return
+
+ if(!QDELETED(bloodbag))
+ var/mob/living/living_owner = owner
+ living_owner.blood_volume -= 150
+ QDEL_NULL(bloodbag)
+ var/obj/item/reagent_containers/blood/o_minus/bloodsucker/new_bag = new(owner.drop_location())
+ owner.put_in_hands(new_bag)
+ DeactivatePower()
diff --git a/modular_bandastation/blood_suckers/code/powers/veil.dm b/modular_bandastation/blood_suckers/code/powers/veil.dm
new file mode 100644
index 0000000000000..a9b7e28d9594f
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/powers/veil.dm
@@ -0,0 +1,139 @@
+/datum/action/cooldown/bloodsucker/veil
+ name = "Veil of Many Faces"
+ desc = "Disguise yourself in the illusion of another identity."
+ button_icon_state = "power_veil"
+ power_explanation = "Veil of Many Faces: \n\
+ Activating Veil of Many Faces will shroud you in smoke and forge you a new identity.\n\
+ Your name and appearance will be completely randomized, and turning the ability off again will undo it all.\n\
+ Clothes, gear, and Security/Medical HUD status is kept the same while this power is active."
+ power_flags = BP_AM_TOGGLE
+ check_flags = BP_CANT_USE_IN_FRENZY | BP_CANT_USE_DURING_SOL
+ purchase_flags = BLOODSUCKER_DEFAULT_POWER | VASSAL_CAN_BUY
+ bloodcost = 15
+ constant_bloodcost = 0.1
+ cooldown_time = 10 SECONDS
+ // Outfit Vars
+// var/list/original_items = list()
+ // Identity Vars
+ var/prev_gender
+ var/prev_skin_tone
+ var/prev_hair_style
+ var/prev_facial_hair_style
+ var/prev_hair_color
+ var/prev_facial_hair_color
+ var/prev_underwear
+ var/prev_undershirt
+ var/prev_socks
+ var/prev_disfigured
+ var/list/prev_features // For lizards and such
+
+/datum/action/cooldown/bloodsucker/veil/ActivatePower(trigger_flags)
+ . = ..()
+ cast_effect() // POOF
+// if(blahblahblah)
+// Disguise_Outfit()
+ veil_user()
+ owner.balloon_alert(owner, "veil turned on.")
+
+/* // Meant to disguise your character's clothing into fake ones.
+/datum/action/cooldown/bloodsucker/veil/proc/Disguise_Outfit()
+ return
+ // Step One: Back up original items
+*/
+
+/datum/action/cooldown/bloodsucker/veil/proc/veil_user()
+ // Change Name/Voice
+ var/mob/living/carbon/human/user = owner
+ user.name_override = user.dna.species.random_name(user.gender)
+ user.name = user.name_override
+ user.SetSpecialVoice(user.name_override)
+ to_chat(owner, span_warning("You mystify the air around your person. Your identity is now altered."))
+
+ // Store Prev Appearance
+ prev_gender = user.gender
+ prev_skin_tone = user.skin_tone
+ prev_hair_style = user.hairstyle
+ prev_facial_hair_style = user.facial_hairstyle
+ prev_hair_color = user.hair_color
+ prev_facial_hair_color = user.facial_hair_color
+ prev_underwear = user.underwear
+ prev_undershirt = user.undershirt
+ prev_socks = user.socks
+// prev_eye_color
+ prev_disfigured = HAS_TRAIT(user, TRAIT_DISFIGURED) // I was disfigured! //prev_disabilities = user.disabilities
+ prev_features = user.dna.features
+
+ // Change Appearance
+ user.gender = pick(MALE, FEMALE, PLURAL)
+ user.skin_tone = random_skin_tone()
+ user.hairstyle = random_hairstyle(user.gender)
+ user.facial_hairstyle = pick(random_facial_hairstyle(user.gender), "Shaved")
+ user.hair_color = random_short_color()
+ user.facial_hair_color = user.hair_color
+ user.underwear = random_underwear(user.gender)
+ user.undershirt = random_undershirt(user.gender)
+ user.socks = random_socks(user.gender)
+ //user.eye_color = random_eye_color()
+ if(prev_disfigured)
+ REMOVE_TRAIT(user, TRAIT_DISFIGURED, null)
+ user.dna.features = random_features()
+
+ // Apply Appearance
+ user.update_body() // Outfit and underware, also body.
+ user.update_mutant_bodyparts() // Lizard tails etc
+ user.update_hair()
+ user.update_body_parts()
+
+/datum/action/cooldown/bloodsucker/veil/DeactivatePower()
+ . = ..()
+ if(!ishuman(owner))
+ return
+ var/mob/living/carbon/human/user = owner
+
+ // Revert Identity
+ user.UnsetSpecialVoice()
+ user.name_override = null
+ user.name = user.real_name
+
+ // Revert Appearance
+ user.gender = prev_gender
+ user.skin_tone = prev_skin_tone
+ user.hairstyle = prev_hair_style
+ user.facial_hairstyle = prev_facial_hair_style
+ user.hair_color = prev_hair_color
+ user.facial_hair_color = prev_facial_hair_color
+ user.underwear = prev_underwear
+ user.undershirt = prev_undershirt
+ user.socks = prev_socks
+
+ //user.disabilities = prev_disabilities // Restore HUSK, CLUMSY, etc.
+ if(prev_disfigured)
+ //We are ASSUMING husk. // user.status_flags |= DISFIGURED // Restore "Unknown" disfigurement
+ ADD_TRAIT(user, TRAIT_DISFIGURED, TRAIT_HUSK)
+ user.dna.features = prev_features
+
+ // Apply Appearance
+ user.update_body() // Outfit and underware, also body.
+ user.update_hair()
+ user.update_body_parts() // Body itself, maybe skin color?
+
+ cast_effect() // POOF
+ owner.balloon_alert(owner, "veil turned off.")
+
+
+// CAST EFFECT // General effect (poof, splat, etc) when you cast. Doesn't happen automatically!
+/datum/action/cooldown/bloodsucker/veil/proc/cast_effect()
+ // Effect
+ playsound(get_turf(owner), 'sound/magic/smoke.ogg', 20, 1)
+ var/datum/effect_system/steam_spread/bloodsucker/puff = new /datum/effect_system/steam_spread/()
+ puff.set_up(3, 0, get_turf(owner))
+ puff.attach(owner) //OPTIONAL
+ puff.start()
+ owner.spin(8, 1) //Spin around like a loon.
+
+/obj/effect/particle_effect/fluid/smoke/vampsmoke
+ opacity = FALSE
+ lifetime = 0
+
+/obj/effect/particle_effect/fluid/smoke/vampsmoke/fade_out(frames = 0.8 SECONDS)
+ ..(frames)
diff --git a/modular_bandastation/blood_suckers/code/structures/bloodsucker_coffin.dm b/modular_bandastation/blood_suckers/code/structures/bloodsucker_coffin.dm
new file mode 100644
index 0000000000000..06aaabd41d176
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/structures/bloodsucker_coffin.dm
@@ -0,0 +1,287 @@
+/datum/antagonist/bloodsucker/proc/claim_coffin(obj/structure/closet/crate/claimed, area/current_area)
+ // ALREADY CLAIMED
+ if(claimed.resident)
+ if(claimed.resident == owner.current)
+ to_chat(owner, "This is your [src].")
+ else
+ to_chat(owner, "This [src] has already been claimed by another.")
+ return FALSE
+ if(!(GLOB.the_station_areas.Find(current_area.type)))
+ claimed.balloon_alert(owner.current, "not part of station!")
+ return
+ // This is my Lair
+ coffin = claimed
+ bloodsucker_lair_area = current_area
+ if(!(/datum/crafting_recipe/vassalrack in owner?.learned_recipes))
+ owner.teach_crafting_recipe(/datum/crafting_recipe/vassalrack)
+ owner.teach_crafting_recipe(/datum/crafting_recipe/candelabrum)
+ owner.teach_crafting_recipe(/datum/crafting_recipe/bloodthrone)
+ owner.teach_crafting_recipe(/datum/crafting_recipe/meatcoffin)
+ owner.current.balloon_alert(owner.current, "new recipes learned!")
+ to_chat(owner, span_userdanger("You have claimed the [claimed] as your place of immortal rest! Your lair is now [bloodsucker_lair_area]."))
+ to_chat(owner, span_announce("Bloodsucker Tip: Find new lair recipes in the Structures tab of the Crafting Menu, including the Persuasion Rack for converting crew into Vassals."))
+ return TRUE
+
+/// From crate.dm
+/obj/structure/closet/crate
+ breakout_time = 20 SECONDS
+ ///The resident (owner) of this crate/coffin.
+ var/mob/living/resident
+ ///The time it takes to pry this open with a crowbar.
+ var/pry_lid_timer = 25 SECONDS
+
+/obj/structure/closet/crate/coffin/examine(mob/user)
+ . = ..()
+ if(user == resident)
+ . += span_cult("This is your Claimed Coffin.")
+ . += span_cult("Rest in it while injured to enter Torpor. Entering it with unspent Ranks will allow you to spend one.")
+ . += span_cult("Alt-Click while inside the Coffin to Lock/Unlock.")
+ . += span_cult("Alt-Click while outside of your Coffin to Unclaim it, unwrenching it and all your other structures as a result.")
+
+/obj/structure/closet/crate/coffin/blackcoffin
+ name = "black coffin"
+ desc = "For those departed who are not so dear."
+ icon_state = "coffin"
+ icon = 'modular_bandastation/blood_suckers/icons/vamp_obj.dmi'
+ open_sound = 'modular_bandastation/blood_suckers/sound/coffin_open.ogg'
+ close_sound = 'modular_bandastation/blood_suckers/sound/coffin_close.ogg'
+ breakout_time = 30 SECONDS
+ pry_lid_timer = 20 SECONDS
+ resistance_flags = NONE
+ material_drop = /obj/item/stack/sheet/iron
+ material_drop_amount = 2
+ armor_type = /datum/armor/blackcoffin
+
+/datum/armor/blackcoffin
+ melee = 50
+ bullet = 20
+ laser = 30
+ bomb = 50
+ fire = 70
+ acid = 60
+
+/obj/structure/closet/crate/coffin/securecoffin
+ name = "secure coffin"
+ desc = "For those too scared of having their place of rest disturbed."
+ icon_state = "securecoffin"
+ icon = 'modular_bandastation/blood_suckers/icons/vamp_obj.dmi'
+ open_sound = 'modular_bandastation/blood_suckers/sound/coffin_open.ogg'
+ close_sound = 'modular_bandastation/blood_suckers/sound/coffin_close.ogg'
+ breakout_time = 35 SECONDS
+ pry_lid_timer = 35 SECONDS
+ resistance_flags = FIRE_PROOF | LAVA_PROOF | ACID_PROOF
+ material_drop = /obj/item/stack/sheet/iron
+ material_drop_amount = 2
+ armor_type = /datum/armor/securecoffin
+
+/datum/armor/securecoffin
+ melee = 35
+ bullet = 20
+ laser = 20
+ bomb = 100
+ fire = 100
+ acid = 100
+
+/obj/structure/closet/crate/coffin/meatcoffin
+ name = "meat coffin"
+ desc = "When you're ready to meat your maker, the steaks can never be too high."
+ icon_state = "meatcoffin"
+ icon = 'modular_bandastation/blood_suckers/icons/vamp_obj.dmi'
+ resistance_flags = FIRE_PROOF
+ open_sound = 'sound/effects/footstep/slime1.ogg'
+ close_sound = 'sound/effects/footstep/slime1.ogg'
+ breakout_time = 25 SECONDS
+ pry_lid_timer = 20 SECONDS
+ material_drop = /obj/item/food/meat/slab/human
+ material_drop_amount = 3
+ armor_type = /datum/armor/meatcoffin
+
+/datum/armor/meatcoffin
+ melee = 70
+ bullet = 10
+ laser = 10
+ bomb = 70
+ fire = 70
+ acid = 60
+
+/obj/structure/closet/crate/coffin/metalcoffin
+ name = "metal coffin"
+ desc = "A big metal sardine can inside of another big metal sardine can, in space."
+ icon_state = "metalcoffin"
+ icon = 'modular_bandastation/blood_suckers/icons/vamp_obj.dmi'
+ resistance_flags = FIRE_PROOF | LAVA_PROOF
+ open_sound = 'sound/effects/pressureplate.ogg'
+ close_sound = 'sound/effects/pressureplate.ogg'
+ breakout_time = 25 SECONDS
+ pry_lid_timer = 30 SECONDS
+ material_drop = /obj/item/stack/sheet/iron
+ material_drop_amount = 5
+ armor_type = /datum/armor/metalcoffin
+
+/datum/armor/metalcoffin
+ melee = 40
+ bullet = 15
+ laser = 50
+ bomb = 10
+ fire = 70
+ acid = 60
+
+//////////////////////////////////////////////
+
+/// NOTE: This can be any coffin that you are resting AND inside of.
+/obj/structure/closet/crate/coffin/proc/claim_coffin(mob/living/claimant, area/current_area)
+ var/datum/antagonist/bloodsucker/bloodsuckerdatum = claimant.mind.has_antag_datum(/datum/antagonist/bloodsucker)
+ // Successfully claimed?
+ if(bloodsuckerdatum.claim_coffin(src, current_area))
+ resident = claimant
+ anchored = TRUE
+ START_PROCESSING(SSprocessing, src)
+
+/obj/structure/closet/crate/coffin/Destroy()
+ unclaim_coffin()
+ STOP_PROCESSING(SSprocessing, src)
+ return ..()
+
+/obj/structure/closet/crate/coffin/process(mob/living/user)
+ . = ..()
+ if(!.)
+ return FALSE
+ if(user in src)
+ var/list/turf/area_turfs = get_area_turfs(get_area(src))
+ // Create Dirt etc.
+ var/turf/T_Dirty = pick(area_turfs)
+ if(T_Dirty && !T_Dirty.density)
+ // Default: Dirt
+ // STEP ONE: COBWEBS
+ // CHECK: Wall to North?
+ var/turf/check_N = get_step(T_Dirty, NORTH)
+ if(istype(check_N, /turf/closed/wall))
+ // CHECK: Wall to West?
+ var/turf/check_W = get_step(T_Dirty, WEST)
+ if(istype(check_W, /turf/closed/wall))
+ new /obj/effect/decal/cleanable/cobweb(T_Dirty)
+ // CHECK: Wall to East?
+ var/turf/check_E = get_step(T_Dirty, EAST)
+ if(istype(check_E, /turf/closed/wall))
+ new /obj/effect/decal/cleanable/cobweb/cobweb2(T_Dirty)
+ new /obj/effect/decal/cleanable/dirt(T_Dirty)
+
+/obj/structure/closet/crate/proc/unclaim_coffin(manual = FALSE)
+ // Unanchor it (If it hasn't been broken, anyway)
+ anchored = FALSE
+ if(!resident || !resident.mind)
+ return
+ // Unclaiming
+ var/datum/antagonist/bloodsucker/bloodsuckerdatum = resident.mind.has_antag_datum(/datum/antagonist/bloodsucker)
+ if(bloodsuckerdatum && bloodsuckerdatum.coffin == src)
+ bloodsuckerdatum.coffin = null
+ bloodsuckerdatum.bloodsucker_lair_area = null
+ for(var/obj/structure/bloodsucker/bloodsucker_structure in get_area(src))
+ if(bloodsucker_structure.owner == resident)
+ bloodsucker_structure.unbolt()
+ if(manual)
+ to_chat(resident, ("You have unclaimed your coffin! This also unclaims all your other Bloodsucker structures!"))
+ else
+ to_chat(resident, ("You sense that the link with your coffin and your sacred lair has been broken! You will need to seek another."))
+ // Remove resident. Because this object isnt removed from the game immediately (GC?) we need to give them a way to see they don't have a home anymore.
+ resident = null
+
+/// You cannot lock in/out a coffin's owner. SORRY.
+/obj/structure/closet/crate/coffin/can_open(mob/living/user)
+ if(!locked)
+ return ..()
+ if(user == resident)
+ if(welded)
+ welded = FALSE
+ update_icon()
+ locked = FALSE
+ return TRUE
+ playsound(get_turf(src), 'sound/machines/door_locked.ogg', 20, 1)
+ to_chat(user, span_notice("[src] appears to be locked tight from the inside."))
+
+/obj/structure/closet/crate/coffin/close(mob/living/user)
+ . = ..()
+ if(!.)
+ return FALSE
+ // Only the User can put themself into Torpor. If already in it, you'll start to heal.
+ if(user in src)
+ var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(/datum/antagonist/bloodsucker)
+ if(!bloodsuckerdatum)
+ return FALSE
+ var/area/current_area = get_area(src)
+ if(!bloodsuckerdatum.coffin && !resident)
+ switch(tgui_alert(user, "Do you wish to claim this as your coffin? [current_area] will be your lair.", "Claim Lair", list("Yes", "No")))
+ if("Yes")
+ claim_coffin(user, current_area)
+ if("No")
+ return
+ LockMe(user)
+ //Level up if possible.
+ if(!bloodsuckerdatum.my_clan)
+ to_chat(user, span_notice("You must enter a Clan to rank up."))
+ else
+ bloodsuckerdatum.SpendRank()
+ // You're in a Coffin, everything else is done, you're likely here to heal. Let's offer them the oppertunity to do so.
+ bloodsuckerdatum.check_begin_torpor()
+ return TRUE
+
+/// You cannot weld or deconstruct an owned coffin. Only the owner can destroy their own coffin.
+/obj/structure/closet/crate/coffin/attackby(obj/item/item, mob/user, params)
+ if(!resident)
+ return ..()
+ if(user != resident)
+ if(istype(item, cutting_tool))
+ to_chat(user, span_notice("This is a much more complex mechanical structure than you thought. You don't know where to begin cutting [src]."))
+ return
+ if(anchored && (item.tool_behaviour == TOOL_WRENCH))
+ to_chat(user, span_danger("The coffin won't come unanchored from the floor.[user == resident ? " You can Alt-Click to unclaim and unwrench your Coffin." : ""]"))
+ return
+
+ if(locked && (item.tool_behaviour == TOOL_CROWBAR))
+ var/pry_time = pry_lid_timer * item.toolspeed // Pry speed must be affected by the speed of the tool.
+ user.visible_message(
+ span_notice("[user] tries to pry the lid off of [src] with [item]."),
+ span_notice("You begin prying the lid off of [src] with [item]. This should take about [DisplayTimeText(pry_time)]."))
+ if(!do_after(user, pry_time, src))
+ return
+ bust_open()
+ user.visible_message(
+ span_notice("[user] snaps the door of [src] wide open."),
+ span_notice("The door of [src] snaps open."))
+ return
+ return ..()
+
+/// Distance Check (Inside Of)
+/obj/structure/closet/crate/coffin/AltClick(mob/user)
+ . = ..()
+ if(user in src)
+ LockMe(user, !locked)
+ return
+
+ if(user == resident && user.Adjacent(src))
+ balloon_alert(user, "unclaim coffin?")
+ var/static/list/unclaim_options = list(
+ "Yes" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_yes"),
+ "No" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_no"))
+ var/unclaim_response = show_radial_menu(user, src, unclaim_options, radius = 36, require_near = TRUE)
+ switch(unclaim_response)
+ if("Yes")
+ unclaim_coffin(TRUE)
+
+/obj/structure/closet/crate/proc/LockMe(mob/user, inLocked = TRUE)
+ if(user == resident)
+ if(!broken)
+ locked = inLocked
+ if(locked)
+ to_chat(user, span_notice("You flip a secret latch and lock yourself inside [src]."))
+ else
+ to_chat(user, span_notice("You flip a secret latch and unlock [src]."))
+ return
+ // Broken? Let's fix it.
+ to_chat(resident, span_notice("The secret latch that would lock [src] from the inside is broken. You set it back into place..."))
+ if(!do_after(resident, 5 SECONDS, src))
+ to_chat(resident, span_notice("You fail to fix [src]'s mechanism."))
+ return
+ to_chat(resident, span_notice("You fix the mechanism and lock it."))
+ broken = FALSE
+ locked = TRUE
diff --git a/modular_bandastation/blood_suckers/code/structures/bloodsucker_crypt.dm b/modular_bandastation/blood_suckers/code/structures/bloodsucker_crypt.dm
new file mode 100644
index 0000000000000..3e08519f7cedd
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/structures/bloodsucker_crypt.dm
@@ -0,0 +1,625 @@
+/obj/structure/bloodsucker
+ ///Who owns this structure?
+ var/mob/living/owner
+ /*
+ * We use vars to add descriptions to items.
+ * This way we don't have to make a new /examine for each structure
+ * And it's easier to edit.
+ */
+ var/ghost_desc
+ var/vamp_desc
+ var/vassal_desc
+ var/hunter_desc
+
+/obj/structure/bloodsucker/Destroy()
+ owner = null
+ return ..()
+
+/obj/structure/bloodsucker/examine(mob/user)
+ . = ..()
+ if(!user.mind && ghost_desc != "")
+ . += span_cult(ghost_desc)
+ if(IS_BLOODSUCKER(user) && vamp_desc)
+ if(!owner)
+ . += span_cult("It is unsecured. Click on [src] while in your lair to secure it in place to get its full potential.")
+ return
+ . += span_cult(vamp_desc)
+ if(IS_VASSAL(user) && vassal_desc != "")
+ . += span_cult(vassal_desc)
+
+/// This handles bolting down the structure.
+/obj/structure/bloodsucker/proc/bolt(mob/user)
+ to_chat(user, span_danger("You have secured [src] in place."))
+ to_chat(user, span_announce("* Bloodsucker Tip: Examine [src] to understand how it functions!"))
+ owner = user
+
+/// This handles unbolting of the structure.
+/obj/structure/bloodsucker/proc/unbolt(mob/user)
+ to_chat(user, span_danger("You have unsecured [src]."))
+ owner = null
+
+/obj/structure/bloodsucker/attackby(obj/item/item, mob/living/user, params)
+ /// If a Bloodsucker tries to wrench it in place, yell at them.
+ if(item.tool_behaviour == TOOL_WRENCH && !anchored && IS_BLOODSUCKER(user))
+ user.playsound_local(null, 'sound/machines/buzz-sigh.ogg', 40, FALSE, pressure_affected = FALSE)
+ to_chat(user, span_announce("* Bloodsucker Tip: Examine Bloodsucker structures to understand how they function!"))
+ return
+ return ..()
+
+/obj/structure/bloodsucker/attack_hand(mob/user, list/modifiers)
+// . = ..() // Don't call parent, else they will handle unbuckling.
+ var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(/datum/antagonist/bloodsucker)
+ /// Claiming the Rack instead of using it?
+ if(istype(bloodsuckerdatum) && !owner)
+ if(!bloodsuckerdatum.bloodsucker_lair_area)
+ to_chat(user, span_danger("You don't have a lair. Claim a coffin to make that location your lair."))
+ return FALSE
+ if(bloodsuckerdatum.bloodsucker_lair_area != get_area(src))
+ to_chat(user, span_danger("You may only activate this structure in your lair: [bloodsuckerdatum.bloodsucker_lair_area]."))
+ return FALSE
+
+ /// Radial menu for securing your Persuasion rack in place.
+ to_chat(user, span_notice("Do you wish to secure [src] here?"))
+ var/static/list/secure_options = list(
+ "Yes" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_yes"),
+ "No" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_no"))
+ var/secure_response = show_radial_menu(user, src, secure_options, radius = 36, require_near = TRUE)
+ if(!secure_response)
+ return FALSE
+ switch(secure_response)
+ if("Yes")
+ user.playsound_local(null, 'sound/items/ratchet.ogg', 70, FALSE, pressure_affected = FALSE)
+ bolt(user)
+ return FALSE
+ return FALSE
+ return TRUE
+
+/obj/structure/bloodsucker/AltClick(mob/user)
+ . = ..()
+ if(user == owner && user.Adjacent(src))
+ balloon_alert(user, "unbolt [src]?")
+ var/static/list/unclaim_options = list(
+ "Yes" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_yes"),
+ "No" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_no"),
+ )
+ var/unclaim_response = show_radial_menu(user, src, unclaim_options, radius = 36, require_near = TRUE)
+ switch(unclaim_response)
+ if("Yes")
+ unbolt(user)
+/*
+/obj/structure/bloodsucker/bloodaltar
+ name = "bloody altar"
+ desc = "It is made of marble, lined with basalt, and radiates an unnerving chill that puts your skin on edge."
+/obj/structure/bloodsucker/bloodstatue
+ name = "bloody countenance"
+ desc = "It looks upsettingly familiar..."
+/obj/structure/bloodsucker/bloodportrait
+ name = "oil portrait"
+ desc = "A disturbingly familiar face stares back at you. Those reds don't seem to be painted in oil..."
+/obj/structure/bloodsucker/bloodbrazier
+ name = "lit brazier"
+ desc = "It burns slowly, but doesn't radiate any heat."
+/obj/structure/bloodsucker/bloodmirror
+ name = "faded mirror"
+ desc = "You get the sense that the foggy reflection looking back at you has an alien intelligence to it."
+/obj/item/restraints/legcuffs/beartrap/bloodsucker
+*/
+
+/obj/structure/bloodsucker/vassalrack
+ name = "persuasion rack"
+ desc = "If this wasn't meant for torture, then someone has some fairly horrifying hobbies."
+ icon = 'modular_bandastation/blood_suckers/icons/vamp_obj.dmi'
+ icon_state = "vassalrack"
+ anchored = FALSE
+ density = TRUE
+ can_buckle = TRUE
+ buckle_lying = 180
+ ghost_desc = "This is a Vassal rack, which allows Bloodsuckers to thrall crewmembers into loyal minions."
+ vamp_desc = "This is the Vassal rack, which allows you to thrall crewmembers into loyal minions in your service.\n\
+ Simply click and hold on a victim, and then drag their sprite on the vassal rack. Right-click on the vassal rack to unbuckle them.\n\
+ To convert into a Vassal, repeatedly click on the persuasion rack. The time required scales with the tool in your off hand. This costs Blood to do.\n\
+ Vassals can be turned into special ones by continuing to torture them once converted."
+ vassal_desc = "This is the vassal rack, which allows your master to thrall crewmembers into their minions.\n\
+ Aid your master in bringing their victims here and keeping them secure.\n\
+ You can secure victims to the vassal rack by click dragging the victim onto the rack while it is secured."
+ hunter_desc = "This is the vassal rack, which monsters use to brainwash crewmembers into their loyal slaves.\n\
+ They usually ensure that victims are handcuffed, to prevent them from running away.\n\
+ Their rituals take time, allowing us to disrupt it."
+
+ /// Resets on each new character to be added to the chair. Some effects should lower it...
+ var/convert_progress = 3
+ /// Mindshielded and Antagonists willingly have to accept you as their Master.
+ var/disloyalty_confirm = FALSE
+ /// Prevents popup spam.
+ var/disloyalty_offered = FALSE
+ // Prevent spamming torture via spam click. Otherwise they're able to lose a lot of blood quickly
+ var/blood_draining = FALSE
+
+/obj/structure/bloodsucker/vassalrack/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/elevation, pixel_shift = 14)
+
+/obj/structure/bloodsucker/vassalrack/deconstruct(disassembled = TRUE)
+ . = ..()
+ new /obj/item/stack/sheet/iron(drop_location(), 4)
+ new /obj/item/stack/rods(drop_location(), 4)
+ qdel(src)
+
+/obj/structure/bloodsucker/vassalrack/bolt()
+ . = ..()
+ set_density(FALSE)
+ set_anchored(TRUE)
+
+/obj/structure/bloodsucker/vassalrack/unbolt()
+ . = ..()
+ unbuckle_all_mobs()
+ set_density(TRUE)
+ set_anchored(FALSE)
+
+/obj/structure/bloodsucker/vassalrack/MouseDrop_T(atom/movable/movable_atom, mob/user)
+ var/mob/living/living_target = movable_atom
+ if(!anchored && IS_BLOODSUCKER(user))
+ to_chat(user, span_danger("Until this rack is secured in place, it cannot serve its purpose."))
+ to_chat(user, span_announce("* Bloodsucker Tip: Examine the Persuasion Rack to understand how it functions!"))
+ return
+ // Default checks
+ if(!isliving(movable_atom) || !living_target.Adjacent(src) || living_target == user || !isliving(user) || has_buckled_mobs() || user.incapacitated() || living_target.buckled)
+ return
+ // Don't buckle Silicon to it please.
+ if(issilicon(living_target))
+ to_chat(user, span_danger("You realize that this machine cannot be vassalized, therefore it is useless to buckle them."))
+ return
+ if(do_after(user, 5 SECONDS, living_target))
+ attach_victim(living_target, user)
+
+/// Attempt Release (Owner vs Non Owner)
+/obj/structure/bloodsucker/vassalrack/attack_hand_secondary(mob/user, modifiers)
+ . = ..()
+ if(. == SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN)
+ return
+ if(!user.can_perform_action(src))
+ return
+ if(!has_buckled_mobs() || !isliving(user))
+ return
+ var/mob/living/carbon/buckled_carbons = pick(buckled_mobs)
+ if(buckled_carbons)
+ if(user == owner)
+ unbuckle_mob(buckled_carbons)
+ else
+ user_unbuckle_mob(buckled_carbons, user)
+
+/**
+ * Attempts to buckle target into the vassalrack
+ */
+/obj/structure/bloodsucker/vassalrack/proc/attach_victim(mob/living/target, mob/living/user)
+ if(!buckle_mob(target))
+ return
+ user.visible_message(
+ span_notice("[user] straps [target] into the rack, immobilizing them."),
+ span_boldnotice("You secure [target] tightly in place. They won't escape you now."),
+ )
+
+ playsound(loc, 'sound/effects/pop_expl.ogg', vol = 25, vary = TRUE)
+ update_appearance(UPDATE_ICON)
+ set_density(TRUE)
+
+ // Set up Torture stuff now
+ reset_progress()
+
+/// Attempt Unbuckle
+/obj/structure/bloodsucker/vassalrack/user_unbuckle_mob(mob/living/buckled_mob, mob/user)
+ if(IS_BLOODSUCKER(user) || IS_VASSAL(user))
+ return ..()
+
+ if(buckled_mob == user)
+ buckled_mob.visible_message(
+ span_danger("[user] tries to release themself from the rack!"),
+ span_danger("You attempt to release yourself from the rack!"),
+ span_hear("You hear a squishy wet noise."))
+ if(!do_after(user, 20 SECONDS, buckled_mob))
+ return
+ else
+ buckled_mob.visible_message(
+ span_danger("[user] tries to pull [buckled_mob] rack!"),
+ span_danger("[user] tries to pull [buckled_mob] rack!"),
+ span_hear("You hear a squishy wet noise."))
+ if(!do_after(user, 10 SECONDS, buckled_mob))
+ return
+
+ return ..()
+
+/obj/structure/bloodsucker/vassalrack/unbuckle_mob(mob/living/buckled_mob, force = FALSE, can_fall = TRUE)
+ . = ..()
+ if(!.)
+ return FALSE
+ visible_message(span_danger("[buckled_mob][buckled_mob.stat == DEAD ? "'s corpse" : ""] slides off of the rack."))
+ set_density(FALSE)
+ buckled_mob.Paralyze(2 SECONDS)
+ update_appearance(UPDATE_ICON)
+ reset_progress()
+ return TRUE
+
+/obj/structure/bloodsucker/vassalrack/attack_hand(mob/user, list/modifiers)
+ . = ..()
+ if(!.)
+ return FALSE
+ // Is there anyone on the rack & If so, are they being tortured?
+ if(!has_buckled_mobs())
+ balloon_alert(user, "nobody buckled!")
+ return FALSE
+
+ var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(/datum/antagonist/bloodsucker)
+ var/mob/living/carbon/buckled_carbons = pick(buckled_mobs)
+ // If I'm not a Bloodsucker, try to unbuckle them.
+ if(!istype(bloodsuckerdatum))
+ user_unbuckle_mob(buckled_carbons, user)
+ return
+ if(!bloodsuckerdatum.my_clan)
+ to_chat(user, span_warning("You can't vassalize people until you enter a Clan (Through your Antagonist UI button)"))
+ user.balloon_alert(user, "join a clan first!")
+ return
+
+ var/datum/antagonist/vassal/vassaldatum = IS_VASSAL(buckled_carbons)
+ // Are they our Vassal?
+ if(vassaldatum?.master == bloodsuckerdatum)
+ SEND_SIGNAL(bloodsuckerdatum, BLOODSUCKER_INTERACT_WITH_VASSAL, vassaldatum)
+ return
+
+ // Not our Vassal, but Alive & We're a Bloodsucker, good to torture!
+ torture_victim(user, buckled_carbons)
+
+/**
+ * Torture steps:
+ *
+ * * Tick Down Conversion from 3 to 0
+ * * Break mindshielding/antag (on approve)
+ * * Vassalize target
+ */
+/obj/structure/bloodsucker/vassalrack/proc/torture_victim(mob/living/user, mob/living/target)
+ var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(/datum/antagonist/bloodsucker)
+ if(target.stat > UNCONSCIOUS)
+ balloon_alert(user, "too badly injured!")
+ return FALSE
+
+ if(IS_VASSAL(target))
+ var/datum/antagonist/vassal/vassaldatum = target.mind.has_antag_datum(/datum/antagonist/vassal)
+ if(!vassaldatum.master.broke_masquerade)
+ balloon_alert(user, "someone else's vassal!")
+ return FALSE
+
+ if(!ishuman(target))
+ balloon_alert(user, "you can't torture an animal or basic mob!")
+ return FALSE
+ var/disloyalty_requires = RequireDisloyalty(user, target)
+
+ if(disloyalty_requires == VASSALIZATION_BANNED)
+ balloon_alert(user, "can't be vassalized!")
+ return FALSE
+
+ // Conversion Process
+ if(convert_progress)
+ //Are we currently torturing this person? If so, do not spill blood more.
+ if(blood_draining)
+ balloon_alert(user, "already spilling blood!")
+ return
+ //We're torturing. Do not start another torture on this rack.
+ blood_draining = TRUE
+ balloon_alert(user, "spilling blood...")
+ bloodsuckerdatum.AddBloodVolume(-TORTURE_BLOOD_HALF_COST)
+ if(!do_torture(user, target))
+ return FALSE
+ bloodsuckerdatum.AddBloodVolume(-TORTURE_BLOOD_HALF_COST)
+ // Prevent them from unbuckling themselves as long as we're torturing.
+ target.Paralyze(1 SECONDS)
+ convert_progress--
+
+ // We're done? Let's see if they can be Vassal.
+ if(convert_progress)
+ balloon_alert(user, "needs more persuasion...")
+ return
+
+ if(disloyalty_requires)
+ balloon_alert(user, "has external loyalties! more persuasion required!")
+ else
+ balloon_alert(user, "ready for communion!")
+ return
+
+ if(!disloyalty_confirm && disloyalty_requires)
+ if(!do_disloyalty(user, target))
+ return
+ if(!disloyalty_confirm)
+ balloon_alert(user, "refused persuasion!")
+ convert_progress++
+ else
+ balloon_alert(user, "ready for communion!")
+ return
+ //If they don't need any more torture, start converting them into a vassal!
+ else
+ user.balloon_alert_to_viewers("smears blood...", "painting bloody marks...")
+ if(!do_after(user, 5 SECONDS, target))
+ balloon_alert(user, "interrupted!")
+ return
+ // Convert to Vassal!
+ bloodsuckerdatum.AddBloodVolume(-TORTURE_CONVERSION_COST)
+ if(bloodsuckerdatum.make_vassal(target))
+ remove_loyalties(target)
+ SEND_SIGNAL(bloodsuckerdatum, BLOODSUCKER_MADE_VASSAL, user, target)
+
+/obj/structure/bloodsucker/vassalrack/proc/do_torture(mob/living/user, mob/living/carbon/target, mult = 1)
+ // Fifteen seconds if you aren't using anything. Shorter with weapons and such.
+ var/torture_time = 15
+ var/torture_dmg_brute = 2
+ var/torture_dmg_burn = 0
+ var/obj/item/bodypart/selected_bodypart = pick(target.bodyparts)
+ // Get Weapon
+ var/obj/item/held_item = user.get_inactive_held_item()
+ /// Weapon Bonus
+ if(held_item)
+ torture_time -= held_item.force / 4
+ if(!held_item.use_tool(src, user, 0, volume = 5))
+ return
+ switch(held_item.damtype)
+ if(BRUTE)
+ torture_dmg_brute = held_item.force / 4
+ torture_dmg_burn = 0
+ if(BURN)
+ torture_dmg_brute = 0
+ torture_dmg_burn = held_item.force / 4
+ switch(held_item.sharpness)
+ if(SHARP_EDGED)
+ torture_time -= 2
+ if(SHARP_POINTY)
+ torture_time -= 3
+
+ // Minimum 5 seconds.
+ torture_time = max(5 SECONDS, torture_time * 10)
+ // Now run process.
+ if(!do_after(user, (torture_time * mult), target))
+ //Torture failed. You can start again.
+ blood_draining = FALSE
+ return FALSE
+
+ if(held_item)
+ held_item.play_tool_sound(target)
+ target.visible_message(
+ span_danger("[user] performs a ritual, spilling some of [target]'s blood from their [selected_bodypart.name] and shaking them up!"),
+ span_userdanger("[user] performs a ritual, spilling some blood from your [selected_bodypart.name], shaking you up!"))
+
+ INVOKE_ASYNC(target, TYPE_PROC_REF(/mob, emote), "scream")
+ target.set_timed_status_effect(5 SECONDS, /datum/status_effect/jitter, only_if_higher = TRUE)
+ target.apply_damages(brute = torture_dmg_brute, burn = torture_dmg_burn, def_zone = selected_bodypart.body_zone)
+ //Torture succeeded. You may torture again.
+ blood_draining = FALSE
+ return TRUE
+
+/// Offer them the oppertunity to join now.
+/obj/structure/bloodsucker/vassalrack/proc/do_disloyalty(mob/living/user, mob/living/target)
+ if(disloyalty_offered)
+ return FALSE
+
+ disloyalty_offered = TRUE
+ to_chat(user, span_notice("[target] has been given the opportunity for servitude. You await their decision..."))
+ var/alert_response = tgui_alert(
+ user = target, \
+ message = "You are being tortured! Do you want to give in and pledge your undying loyalty to [user]? \n\
+ You will not lose your current objectives, but they come second to the will of your new master!", \
+ title = "THE HORRIBLE PAIN! WHEN WILL IT END?!",
+ buttons = list("Accept", "Refuse"),
+ timeout = 10 SECONDS, \
+ autofocus = TRUE, \
+ )
+ switch(alert_response)
+ if("Accept")
+ disloyalty_confirm = TRUE
+ else
+ target.balloon_alert_to_viewers("stares defiantly", "refused vassalization!")
+ disloyalty_offered = FALSE
+ return TRUE
+
+/obj/structure/bloodsucker/vassalrack/proc/RequireDisloyalty(mob/living/user, mob/living/target)
+#ifdef BLOODSUCKER_TESTING
+ if(!target || !target.mind)
+#else
+ if(!target || !target.client)
+#endif
+ balloon_alert(user, "target has no mind!")
+ return VASSALIZATION_BANNED
+
+ var/datum/antagonist/bloodsucker/bloodsuckerdatum = IS_BLOODSUCKER(user)
+ return bloodsuckerdatum.AmValidAntag(target)
+
+/obj/structure/bloodsucker/vassalrack/proc/remove_loyalties(mob/living/target)
+ // Find Mind Implant & Destroy
+ for(var/obj/item/implant/implant as anything in target.implants)
+ if(istype(implant, /obj/item/implant/mindshield) && implant.removed(target, silent = TRUE))
+ qdel(implant)
+
+/obj/structure/bloodsucker/vassalrack/proc/reset_progress()
+ convert_progress = initial(convert_progress)
+ disloyalty_offered = initial(disloyalty_offered)
+ disloyalty_confirm = initial(disloyalty_confirm)
+ blood_draining = initial(blood_draining)
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/obj/structure/bloodsucker/candelabrum
+ name = "candelabrum"
+ desc = "It burns slowly, but doesn't radiate any heat."
+ icon = 'modular_bandastation/blood_suckers/icons/vamp_obj.dmi'
+ icon_state = "candelabrum"
+ light_color = "#66FFFF"//LIGHT_COLOR_BLUEGREEN // lighting.dm
+ light_power = 3
+ light_outer_range = 0 // to 2
+ density = FALSE
+ can_buckle = TRUE
+ anchored = FALSE
+ ghost_desc = "This is a magical candle which drains at the sanity of non Bloodsuckers and Vassals.\n\
+ Vassals can turn the candle on manually, while Bloodsuckers can do it from a distance."
+ vamp_desc = "This is a magical candle which drains at the sanity of mortals who are not under your command while it is active.\n\
+ You can right-click on it from any range to turn it on remotely, or simply be next to it and click on it to turn it on and off normally."
+ vassal_desc = "This is a magical candle which drains at the sanity of the fools who havent yet accepted your master, as long as it is active.\n\
+ You can turn it on and off by clicking on it while you are next to it.\n\
+ If your Master is part of the Ventrue Clan, they utilize this to upgrade their Favorite Vassal."
+ hunter_desc = "This is a blue Candelabrum, which causes insanity to those near it while active."
+ var/lit = FALSE
+
+/obj/structure/bloodsucker/candelabrum/Destroy()
+ STOP_PROCESSING(SSobj, src)
+ return ..()
+
+/obj/structure/bloodsucker/candelabrum/update_icon_state()
+ icon_state = "candelabrum[lit ? "_lit" : ""]"
+ return ..()
+
+/obj/structure/bloodsucker/candelabrum/bolt()
+ . = ..()
+ set_anchored(TRUE)
+ set_density(TRUE)
+
+/obj/structure/bloodsucker/candelabrum/unbolt()
+ . = ..()
+ set_anchored(FALSE)
+ set_density(FALSE)
+
+/obj/structure/bloodsucker/candelabrum/attack_hand(mob/living/user, list/modifiers)
+ . = ..()
+ if(!.)
+ return
+ if(anchored && (IS_VASSAL(user) || IS_BLOODSUCKER(user)))
+ toggle()
+ return ..()
+
+/obj/structure/bloodsucker/candelabrum/proc/toggle(mob/user)
+ lit = !lit
+ if(lit)
+ desc = initial(desc)
+ set_light(l_outer_range = 2, l_power = 3, l_color = "#66FFFF")
+ START_PROCESSING(SSobj, src)
+ else
+ desc = "Despite not being lit, it makes your skin crawl."
+ set_light(0)
+ STOP_PROCESSING(SSobj, src)
+ update_icon()
+
+/obj/structure/bloodsucker/candelabrum/process()
+ if(!lit)
+ return
+ for(var/mob/living/carbon/nearly_people in viewers(7, src))
+ /// We dont want Bloodsuckers or Vassals affected by this
+ if(IS_VASSAL(nearly_people) || IS_BLOODSUCKER(nearly_people) || IS_MONSTERHUNTER(nearly_people))
+ continue
+ nearly_people.set_hallucinations_if_lower(5 SECONDS)
+ nearly_people.add_mood_event("vampcandle", /datum/mood_event/vampcandle)
+
+/// Blood Throne - Allows Bloodsuckers to remotely speak with their Vassals. - Code (Mostly) stolen from comfy chairs (armrests) and chairs (layers)
+/obj/structure/bloodsucker/bloodthrone
+ name = "wicked throne"
+ desc = "Twisted metal shards jut from the arm rests. Very uncomfortable looking. It would take a masochistic sort to sit on this jagged piece of furniture."
+ icon = 'modular_bandastation/blood_suckers/icons/vamp_obj_64.dmi'
+ icon_state = "throne"
+ buckle_lying = 0
+ anchored = FALSE
+ density = TRUE
+ can_buckle = TRUE
+ ghost_desc = "This is a Bloodsucker throne, any Bloodsucker sitting on it can remotely speak to their Vassals by attempting to speak aloud."
+ vamp_desc = "This is a blood throne, sitting on it will allow you to telepathically speak to your vassals by simply speaking."
+ vassal_desc = "This is a blood throne, it allows your Master to telepathically speak to you and others like you."
+ hunter_desc = "This is a chair that hurts those that try to buckle themselves onto it, though the Undead have no problem latching on.\n\
+ While buckled, Monsters can use this to telepathically communicate with eachother."
+ var/mutable_appearance/armrest
+
+// Add rotating and armrest
+/obj/structure/bloodsucker/bloodthrone/Initialize()
+ AddComponent(/datum/component/simple_rotation)
+ armrest = GetArmrest()
+ armrest.layer = ABOVE_MOB_LAYER
+ return ..()
+
+/obj/structure/bloodsucker/bloodthrone/Destroy()
+ QDEL_NULL(armrest)
+ return ..()
+
+/obj/structure/bloodsucker/bloodthrone/bolt()
+ . = ..()
+ set_anchored(TRUE)
+
+/obj/structure/bloodsucker/bloodthrone/unbolt()
+ . = ..()
+ set_anchored(FALSE)
+
+// Armrests
+/obj/structure/bloodsucker/bloodthrone/proc/GetArmrest()
+ return mutable_appearance('modular_bandastation/blood_suckers/icons/vamp_obj_64.dmi', "thronearm")
+
+/obj/structure/bloodsucker/bloodthrone/proc/update_armrest()
+ if(has_buckled_mobs())
+ add_overlay(armrest)
+ else
+ cut_overlay(armrest)
+
+// Rotating
+/obj/structure/bloodsucker/bloodthrone/setDir(newdir)
+ . = ..()
+ if(has_buckled_mobs())
+ for(var/m in buckled_mobs)
+ var/mob/living/buckled_mob = m
+ buckled_mob.setDir(newdir)
+
+ if(has_buckled_mobs() && dir == NORTH)
+ layer = ABOVE_MOB_LAYER
+ else
+ layer = OBJ_LAYER
+
+// Buckling
+/obj/structure/bloodsucker/bloodthrone/buckle_mob(mob/living/user, force = FALSE, check_loc = TRUE)
+ if(!anchored)
+ to_chat(user, span_announce("[src] is not bolted to the ground!"))
+ return
+ . = ..()
+ user.visible_message(
+ span_notice("[user] sits down on [src]."),
+ span_boldnotice("You sit down onto [src]."),
+ )
+ if(IS_BLOODSUCKER(user))
+ RegisterSignal(user, COMSIG_MOB_SAY, PROC_REF(handle_speech))
+ else
+ unbuckle_mob(user)
+ user.Paralyze(10 SECONDS)
+ to_chat(user, span_cult("The power of the blood throne overwhelms you!"))
+
+/obj/structure/bloodsucker/bloodthrone/post_buckle_mob(mob/living/target)
+ . = ..()
+ update_armrest()
+ target.pixel_y += 2
+
+// Unbuckling
+/obj/structure/bloodsucker/bloodthrone/unbuckle_mob(mob/living/user, force = FALSE, can_fall = TRUE)
+ src.visible_message(span_danger("[user] unbuckles themselves from [src]."))
+ if(IS_BLOODSUCKER(user))
+ UnregisterSignal(user, COMSIG_MOB_SAY)
+ . = ..()
+
+/obj/structure/bloodsucker/bloodthrone/post_unbuckle_mob(mob/living/target)
+ target.pixel_y -= 2
+
+// The speech itself
+/obj/structure/bloodsucker/bloodthrone/proc/handle_speech(datum/source, list/speech_args)
+ SIGNAL_HANDLER
+
+ var/message = speech_args[SPEECH_MESSAGE]
+ var/mob/living/carbon/human/user = source
+ var/rendered = span_cultlarge("[user.real_name]: [message]")
+ user.log_talk(message, LOG_SAY, tag=ROLE_BLOODSUCKER)
+ var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(/datum/antagonist/bloodsucker)
+ for(var/datum/antagonist/vassal/receiver as anything in bloodsuckerdatum.vassals)
+ var/mob/receiver_mob = receiver?.owner?.current
+ if(QDELETED(receiver_mob))
+ continue
+ to_chat(receiver_mob, rendered, type = MESSAGE_TYPE_RADIO)
+ to_chat(user, rendered, type = MESSAGE_TYPE_RADIO, avoid_highlighting = TRUE) // tell yourself, too.
+
+ for(var/mob/dead_mob in GLOB.dead_mob_list)
+ var/link = FOLLOW_LINK(dead_mob, user)
+ to_chat(dead_mob, "[link] [rendered]", type = MESSAGE_TYPE_RADIO)
+
+ speech_args[SPEECH_MESSAGE] = ""
diff --git a/modular_bandastation/blood_suckers/code/structures/bloodsucker_objects.dm b/modular_bandastation/blood_suckers/code/structures/bloodsucker_objects.dm
new file mode 100644
index 0000000000000..9394b7da7587f
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/structures/bloodsucker_objects.dm
@@ -0,0 +1,298 @@
+//////////////////////
+// BLOODBAG //
+//////////////////////
+
+#define BLOODBAG_GULP_SIZE 5
+
+/// Taken from drinks.dm
+/obj/item/reagent_containers/blood/attack(mob/living/victim, mob/living/attacker, params)
+ if(!can_drink(victim, attacker))
+ return
+
+ if(victim != attacker)
+ if(!do_after(victim, 5 SECONDS, attacker))
+ return
+ attacker.visible_message(
+ span_notice("[attacker] forces [victim] to drink from the [src]."),
+ span_notice("You put the [src] up to [victim]'s mouth."))
+ reagents.trans_to(victim, BLOODBAG_GULP_SIZE, transfered_by = attacker, methods = INGEST)
+ playsound(victim.loc, 'sound/items/drink.ogg', 30, 1)
+ return TRUE
+
+ while(do_after(victim, 1 SECONDS, timed_action_flags = IGNORE_USER_LOC_CHANGE, extra_checks = CALLBACK(src, PROC_REF(can_drink), victim, attacker)))
+ victim.visible_message(
+ span_notice("[victim] puts the [src] up to their mouth."),
+ span_notice("You take a sip from the [src]."),
+ )
+ reagents.trans_to(victim, BLOODBAG_GULP_SIZE, transfered_by = attacker, methods = INGEST)
+ playsound(victim.loc, 'sound/items/drink.ogg', 30, 1)
+ return TRUE
+
+#undef BLOODBAG_GULP_SIZE
+
+/obj/item/reagent_containers/blood/proc/can_drink(mob/living/victim, mob/living/attacker)
+ if(!canconsume(victim, attacker))
+ return FALSE
+ if(!reagents || !reagents.total_volume)
+ to_chat(victim, span_warning("[src] is empty!"))
+ return FALSE
+ return TRUE
+
+///Bloodbag of Bloodsucker blood (used by Vassals only)
+/obj/item/reagent_containers/blood/o_minus/bloodsucker
+ name = "blood pack"
+ blood_type = /datum/blood_type/crew/bloodsucker
+
+/obj/item/reagent_containers/blood/o_minus/bloodsucker/examine(mob/user)
+ . = ..()
+ if(user.mind.has_antag_datum(/datum/antagonist/ex_vassal) || user.mind.has_antag_datum(/datum/antagonist/vassal/revenge))
+ . += span_notice("Seems to be just about the same color as your Master's...")
+
+//////////////////////
+// STAKES //
+//////////////////////
+/obj/item/stack/sheet/mineral/wood/attackby(obj/item/item, mob/user, params)
+ if(!item.get_sharpness())
+ return ..()
+ user.visible_message(
+ span_notice("[user] begins whittling [src] into a pointy object."),
+ span_notice("You begin whittling [src] into a sharp point at one end."),
+ span_hear("You hear wood carving."),
+ )
+ // 5 Second Timer
+ if(!do_after(user, 5 SECONDS, src, NONE, TRUE))
+ return
+ // Make Stake
+ var/obj/item/stake/new_item = new(user.loc)
+ user.visible_message(
+ span_notice("[user] finishes carving a stake out of [src]."),
+ span_notice("You finish carving a stake out of [src]."),
+ )
+ // Prepare to Put in Hands (if holding wood)
+ var/obj/item/stack/sheet/mineral/wood/wood_stack = src
+ var/replace = (user.get_inactive_held_item() == wood_stack)
+ // Use Wood
+ wood_stack.use(1)
+ // If stack depleted, put item in that hand (if it had one)
+ if(!wood_stack && replace)
+ user.put_in_hands(new_item)
+
+/// Do I have a stake in my heart?
+/mob/living/proc/am_staked()
+ var/obj/item/bodypart/chosen_bodypart = get_bodypart(BODY_ZONE_CHEST)
+ if(!chosen_bodypart)
+ return FALSE
+ for(var/obj/item/embedded_stake in chosen_bodypart.embedded_objects)
+ if(istype(embedded_stake, /obj/item/stake))
+ return TRUE
+ return FALSE
+
+/// You can't go to sleep in a coffin with a stake in you.
+/mob/living/proc/StakeCanKillMe()
+ if(IsSleeping())
+ return TRUE
+ if(stat >= UNCONSCIOUS)
+ return TRUE
+ if(HAS_TRAIT_FROM(src, TRAIT_NODEATH, TORPOR_TRAIT))
+ return TRUE
+ return FALSE
+
+/obj/item/stake
+ name = "wooden stake"
+ desc = "A simple wooden stake carved to a sharp point."
+ icon = 'modular_bandastation/blood_suckers/icons/stakes.dmi'
+ icon_state = "wood"
+ inhand_icon_state = "wood"
+ lefthand_file = 'modular_bandastation/blood_suckers/icons/bs_leftinhand.dmi'
+ righthand_file = 'modular_bandastation/blood_suckers/icons/bs_rightinhand.dmi'
+ slot_flags = ITEM_SLOT_POCKETS
+ w_class = WEIGHT_CLASS_SMALL
+ hitsound = 'sound/weapons/bladeslice.ogg'
+ attack_verb_continuous = list("staked", "stabbed", "tore into")
+ attack_verb_simple = list("staked", "stabbed", "tore into")
+ sharpness = SHARP_EDGED
+ embedding = list("embed_chance" = 20)
+ force = 6
+ throwforce = 10
+ max_integrity = 30
+
+ ///Time it takes to embed the stake into someone's chest.
+ var/staketime = 12 SECONDS
+
+/obj/item/stake/attack(mob/living/target, mob/living/user, params)
+ . = ..()
+ if(.)
+ return
+ // Invalid Target, or not targetting the chest?
+ if(check_zone(user.zone_selected) != BODY_ZONE_CHEST)
+ return
+ if(target == user)
+ return
+ if(!target.can_be_staked()) // Oops! Can't.
+ to_chat(user, span_danger("You can't stake [target] when they are moving about! They have to be laying down or grabbed by the neck!"))
+ return
+ if(HAS_TRAIT(target, TRAIT_PIERCEIMMUNE))
+ to_chat(user, span_danger("[target]'s chest resists the stake. It won't go in."))
+ return
+
+ to_chat(user, span_notice("You put all your weight into embedding the stake into [target]'s chest..."))
+ playsound(user, 'sound/magic/Demon_consume.ogg', 50, 1)
+ if(!do_after(user, staketime, target, extra_checks = CALLBACK(target, TYPE_PROC_REF(/mob/living/carbon, can_be_staked)))) // user / target / time / uninterruptable / show progress bar / extra checks
+ return
+ // Drop & Embed Stake
+ user.visible_message(
+ span_danger("[user.name] drives the [src] into [target]'s chest!"),
+ span_danger("You drive the [src] into [target]'s chest!"),
+ )
+ playsound(get_turf(target), 'sound/effects/splat.ogg', 40, 1)
+ if(tryEmbed(target.get_bodypart(BODY_ZONE_CHEST), TRUE, TRUE)) //and if it embeds successfully in their chest, cause a lot of pain
+ target.apply_damage(max(10, force * 1.2), BRUTE, BODY_ZONE_CHEST, wound_bonus = 0, sharpness = TRUE)
+ if(QDELETED(src)) // in case trying to embed it caused its deletion (say, if it's DROPDEL)
+ return
+ if(!target.mind)
+ return
+ var/datum/antagonist/bloodsucker/bloodsuckerdatum = target.mind.has_antag_datum(/datum/antagonist/bloodsucker)
+ if(bloodsuckerdatum)
+ // If DEAD or TORPID... Kill Bloodsucker!
+ if(target.StakeCanKillMe())
+ bloodsuckerdatum.final_death()
+ else
+ to_chat(target, span_userdanger("You have been staked! Your powers are useless, your death forever, while it remains in place."))
+ target.balloon_alert(target, "you have been staked!")
+
+///Can this target be staked? If someone stands up before this is complete, it fails. Best used on someone stationary.
+/mob/living/proc/can_be_staked()
+ return FALSE
+
+/mob/living/carbon/can_be_staked()
+ if(!(mobility_flags & MOBILITY_MOVE))
+ return TRUE
+ return FALSE
+
+/// Created by welding and acid-treating a simple stake.
+/obj/item/stake/hardened
+ name = "hardened stake"
+ desc = "A wooden stake carved to a sharp point and hardened by fire."
+ icon_state = "hardened"
+ force = 8
+ throwforce = 12
+ armour_penetration = 10
+ embedding = list("embed_chance" = 35)
+ staketime = 80
+
+/obj/item/stake/hardened/silver
+ name = "silver stake"
+ desc = "Polished and sharp at the end. For when some mofo is always trying to iceskate uphill."
+ icon_state = "silver"
+ inhand_icon_state = "silver"
+ siemens_coefficient = 1 //flags = CONDUCT // var/siemens_coefficient = 1 // for electrical admittance/conductance (electrocution checks and shit)
+ force = 9
+ armour_penetration = 25
+ embedding = list("embed_chance" = 65)
+ staketime = 60
+
+//////////////////////
+// ARCHIVES //
+//////////////////////
+
+/obj/item/book/codex_gigas/Initialize(mapload)
+ . = ..()
+ var/turf/current_turf = get_turf(src)
+ new /obj/item/book/kindred(current_turf)
+
+/**
+ * # Archives of the Kindred:
+ *
+ * A book that can only be used by Curators.
+ * When used on a player, after a short timer, will reveal if the player is a Bloodsucker, including their real name and Clan.
+ * This book should not work on Bloodsuckers using the Masquerade ability.
+ * If it reveals a Bloodsucker, the Curator will then be able to tell they are a Bloodsucker on examine (Like a Vassal).
+ * Reading it normally will allow Curators to read what each Clan does, with some extra flavor text ones.
+ *
+ * Regular Bloodsuckers won't have any negative effects from the book, while everyone else will get burns/eye damage.
+ */
+/obj/item/book/kindred
+ name = "\improper Archive of the Kindred"
+ starting_title = "the Archive of the Kindred"
+ desc = "Cryptic documents explaining hidden truths behind Undead beings. It is said only Curators can decipher what they really mean."
+ icon = 'monkestation/icons/bloodsuckers/vamp_obj.dmi'
+ lefthand_file = 'monkestation/icons/bloodsuckers/bs_leftinhand.dmi'
+ righthand_file = 'monkestation/icons/bloodsuckers/bs_rightinhand.dmi'
+ icon_state = "kindred_book"
+ starting_author = "dozens of generations of Curators"
+ unique = TRUE
+ throw_speed = 1
+ throw_range = 10
+ resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF
+ ///Boolean on whether the book is currently being used, so you can only use it on one person at a time.
+ var/in_use = FALSE
+
+/obj/item/book/kindred/Initialize()
+ . = ..()
+ AddComponent(/datum/component/stationloving, FALSE, TRUE)
+
+/* NOT YET IMPLEMENTED
+/obj/item/book/kindred/try_carve(obj/item/carving_item, mob/living/user, params)
+ to_chat(user, span_notice("You feel the gentle whispers of a Librarian telling you not to cut [starting_title]."))
+ return FALSE
+*/
+
+///Attacking someone with the book.
+/obj/item/book/kindred/afterattack(mob/living/target, mob/living/user, flag, params)
+ . = ..()
+ if(!user.can_read(src) || in_use || (target == user) || !ismob(target))
+ return
+ if(!HAS_MIND_TRAIT(user, TRAIT_OCCULTIST))
+ if(IS_BLOODSUCKER(user))
+ to_chat(user, span_warning("[src] burns your hands as you try to use it!"))
+ user.apply_damage(3, BURN, pick(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM))
+ else
+ to_chat(user, span_notice("[src] seems to be too complicated for you. It would be best to leave this for someone else to take."))
+ return
+
+ in_use = TRUE
+ user.balloon_alert_to_viewers("reading book...", "looks at [target] and [src]")
+ if(!do_after(user, 3 SECONDS, target, timed_action_flags = NONE, progress = TRUE))
+ to_chat(user, span_notice("You quickly close [src]."))
+ in_use = FALSE
+ return
+ in_use = FALSE
+ var/datum/antagonist/bloodsucker/bloodsuckerdatum = IS_BLOODSUCKER(target)
+ // Are we a Bloodsucker | Are we on Masquerade. If one is true, they will fail.
+ if(IS_BLOODSUCKER(target) && !HAS_TRAIT(target, TRAIT_MASQUERADE))
+ if(bloodsuckerdatum.broke_masquerade)
+ to_chat(user, span_warning("[target], also known as '[bloodsuckerdatum.return_full_name()]', is indeed a Bloodsucker, but you already knew this."))
+ return
+ to_chat(user, span_warning("[target], also known as '[bloodsuckerdatum.return_full_name()]', [bloodsuckerdatum.my_clan ? "is part of the [bloodsuckerdatum.my_clan]!" : "is not part of a clan."] You quickly note this information down, memorizing it."))
+ bloodsuckerdatum.break_masquerade()
+ else
+ to_chat(user, span_notice("You fail to draw any conclusions to [target] being a Bloodsucker."))
+
+/obj/item/book/kindred/attack_self(mob/living/user)
+ if(!HAS_MIND_TRAIT(user, TRAIT_OCCULTIST))
+ if(IS_BLOODSUCKER(user))
+ to_chat(user, span_warning("[src] burns your hands as you try to use it!"))
+ user.apply_damage(3, BURN, pick(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM))
+ else
+ to_chat(user, span_warning("You feel your eyes unable to read the boring texts..."))
+ user.set_eye_blur_if_lower(10 SECONDS)
+ return
+ ui_interact(user)
+
+/obj/item/book/kindred/ui_interact(mob/living/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "KindredBook", name)
+ ui.set_autoupdate(FALSE)
+ ui.open()
+
+/obj/item/book/kindred/ui_static_data(mob/user)
+ var/data = list()
+
+ for(var/datum/bloodsucker_clan/clans as anything in subtypesof(/datum/bloodsucker_clan))
+ var/clan_data = list()
+ clan_data["clan_name"] = initial(clans.name)
+ clan_data["clan_desc"] = initial(clans.description)
+ data["clans"] += list(clan_data)
+
+ return data
diff --git a/modular_bandastation/blood_suckers/code/structures/bloodsucker_recipes.dm b/modular_bandastation/blood_suckers/code/structures/bloodsucker_recipes.dm
new file mode 100644
index 0000000000000..e065a7d2ebd33
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/structures/bloodsucker_recipes.dm
@@ -0,0 +1,123 @@
+/// From recipes.dm
+
+/datum/crafting_recipe/blackcoffin
+ name = "Black Coffin"
+ result = /obj/structure/closet/crate/coffin/blackcoffin
+ tool_behaviors = list(TOOL_WELDER, TOOL_SCREWDRIVER)
+ reqs = list(
+ /obj/item/stack/sheet/cloth = 1,
+ /obj/item/stack/sheet/mineral/wood = 5,
+ /obj/item/stack/sheet/iron = 1,
+ )
+ time = 15 SECONDS
+ category = CAT_STRUCTURE
+
+/datum/crafting_recipe/securecoffin
+ name = "Secure Coffin"
+ result = /obj/structure/closet/crate/coffin/securecoffin
+ tool_behaviors = list(TOOL_WELDER, TOOL_SCREWDRIVER)
+ reqs = list(
+ /obj/item/stack/rods = 1,
+ /obj/item/stack/sheet/plasteel = 5,
+ /obj/item/stack/sheet/iron = 5,
+ )
+ time = 15 SECONDS
+ category = CAT_STRUCTURE
+
+/datum/crafting_recipe/meatcoffin
+ name = "Meat Coffin"
+ result = /obj/structure/closet/crate/coffin/meatcoffin
+ tool_behaviors = list(TOOL_KNIFE, TOOL_ROLLINGPIN)
+ reqs = list(
+ /obj/item/food/meat/slab = 5,
+ /obj/item/restraints/handcuffs/cable = 1,
+ )
+ time = 15 SECONDS
+ category = CAT_STRUCTURE
+ always_available = FALSE //The sacred coffin!
+
+/datum/crafting_recipe/metalcoffin
+ name = "Metal Coffin"
+ result = /obj/structure/closet/crate/coffin/metalcoffin
+ reqs = list(
+ /obj/item/stack/sheet/iron = 6,
+ /obj/item/stack/rods = 2,
+ )
+ time = 10 SECONDS
+ category = CAT_STRUCTURE
+
+/datum/crafting_recipe/vassalrack
+ name = "Persuasion Rack"
+ result = /obj/structure/bloodsucker/vassalrack
+ tool_behaviors = list(TOOL_WELDER, TOOL_WRENCH)
+ reqs = list(
+ /obj/item/stack/sheet/mineral/wood = 3,
+ /obj/item/stack/sheet/iron = 2,
+ /obj/item/restraints/handcuffs/cable = 2,
+ )
+ time = 15 SECONDS
+ category = CAT_STRUCTURE
+ always_available = FALSE
+
+/datum/crafting_recipe/candelabrum
+ name = "Candelabrum"
+ result = /obj/structure/bloodsucker/candelabrum
+ tool_behaviors = list(TOOL_WELDER, TOOL_WRENCH)
+ reqs = list(
+ /obj/item/stack/sheet/iron = 3,
+ /obj/item/stack/rods = 1,
+ /obj/item/flashlight/flare/candle = 1,
+ )
+ time = 10 SECONDS
+ category = CAT_STRUCTURE
+ always_available = FALSE
+
+/datum/crafting_recipe/bloodthrone
+ name = "Blood Throne"
+ result = /obj/structure/bloodsucker/bloodthrone
+ tool_behaviors = list(TOOL_WRENCH)
+ reqs = list(
+ /obj/item/stack/sheet/cloth = 3,
+ /obj/item/stack/sheet/iron = 5,
+ /obj/item/stack/sheet/mineral/wood = 1,
+ )
+ time = 5 SECONDS
+ category = CAT_STRUCTURE
+ always_available = FALSE
+
+/datum/crafting_recipe/stake
+ name = "Stake"
+ result = /obj/item/stake
+ reqs = list(/obj/item/stack/sheet/mineral/wood = 3)
+ time = 8 SECONDS
+ category = CAT_WEAPON_MELEE
+
+/datum/crafting_recipe/hardened_stake
+ name = "Hardened Stake"
+ result = /obj/item/stake/hardened
+ tool_behaviors = list(TOOL_WELDER)
+ reqs = list(/obj/item/stack/rods = 1)
+ time = 6 SECONDS
+ category = CAT_WEAPON_MELEE
+ always_available = FALSE
+
+/datum/crafting_recipe/silver_stake
+ name = "Silver Stake"
+ result = /obj/item/stake/hardened/silver
+ tool_behaviors = list(TOOL_WELDER)
+ reqs = list(
+ /obj/item/stack/sheet/mineral/silver = 1,
+ /obj/item/stake/hardened = 1,
+ )
+ time = 8 SECONDS
+ category = CAT_WEAPON_MELEE
+ always_available = FALSE
+
+/datum/crafting_recipe/coffin
+ name = "Coffin"
+ result = /obj/structure/closet/crate/coffin
+ reqs = list(
+ /obj/item/stack/sheet/mineral/wood = 5,
+ )
+ time = 15 SECONDS
+ category = CAT_STRUCTURE
diff --git a/modular_bandastation/blood_suckers/code/traits/blood_declarations.dm b/modular_bandastation/blood_suckers/code/traits/blood_declarations.dm
new file mode 100644
index 0000000000000..d6c5856c69050
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/traits/blood_declarations.dm
@@ -0,0 +1,2 @@
+/// Falsifies Health analyzer blood levels
+#define TRAIT_MASQUERADE "masquerade"
diff --git a/modular_bandastation/blood_suckers/code/traits/blood_sources.dm b/modular_bandastation/blood_suckers/code/traits/blood_sources.dm
new file mode 100644
index 0000000000000..a9ac33888e8d6
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/traits/blood_sources.dm
@@ -0,0 +1,5 @@
+/// Bloodsucker traits
+#define BLOODSUCKER_TRAIT "bloodsucker_trait"
+#define TORPOR_TRAIT "torpor_trait"
+#define FORTITUDE_TRAIT "fortitude_trait"
+#define FRENZY_TRAIT "frenzy_trait"
diff --git a/modular_bandastation/blood_suckers/code/traits/blood_traits_globalvars.dm b/modular_bandastation/blood_suckers/code/traits/blood_traits_globalvars.dm
new file mode 100644
index 0000000000000..262d5b60e64a9
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/traits/blood_traits_globalvars.dm
@@ -0,0 +1,5 @@
+GLOBAL_LIST_INIT(traits_by_type, list(
+ /mob = list(
+ "TRAIT_MASQUERADE" = TRAIT_MASQUERADE,
+ )
+))
diff --git a/modular_bandastation/blood_suckers/code/vassals/batform.dm b/modular_bandastation/blood_suckers/code/vassals/batform.dm
new file mode 100644
index 0000000000000..5ed97a112c819
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/vassals/batform.dm
@@ -0,0 +1,13 @@
+/**
+ * # BATFORM
+ *
+ * TG removed this, so we're re-adding it
+ */
+/datum/action/cooldown/spell/shapeshift/bat
+ name = "Bat Form"
+ desc = "Take on the shape of a space bat."
+ invocation = "SQUEAAAAK!"
+ invocation_type = INVOCATION_SHOUT
+ spell_requirements = NONE
+ convert_damage = FALSE
+ possible_shapes = list(/mob/living/basic/bat)
diff --git a/modular_bandastation/blood_suckers/code/vassals/ex_vassal.dm b/modular_bandastation/blood_suckers/code/vassals/ex_vassal.dm
new file mode 100644
index 0000000000000..6596afbe694c5
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/vassals/ex_vassal.dm
@@ -0,0 +1,104 @@
+#define BLOOD_TIMER_REQUIREMENT (10 MINUTES)
+#define BLOOD_TIMER_HALWAY (BLOOD_TIMER_REQUIREMENT / 2)
+
+/datum/antagonist/ex_vassal
+ name = "\improper Ex-Vassal"
+ roundend_category = "vassals"
+ antagpanel_category = "Bloodsucker"
+ job_rank = ROLE_BLOODSUCKER
+ antag_hud_name = "vassal_grey"
+ show_in_roundend = FALSE
+ show_in_antagpanel = FALSE
+ silent = TRUE
+ ui_name = FALSE
+ hud_icon = 'modular_bandastation/blood_suckers/icons/bloodsucker_icons.dmi'
+ antag_flags = FLAG_ANTAG_CAP_IGNORE
+
+ ///The revenge vassal that brought us into the fold.
+ var/datum/antagonist/vassal/revenge/revenge_vassal
+ ///Timer we have to live
+ COOLDOWN_DECLARE(blood_timer)
+
+/datum/antagonist/ex_vassal/on_gain()
+ . = ..()
+ RegisterSignal(owner.current, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
+
+/datum/antagonist/ex_vassal/on_removal()
+ if(revenge_vassal)
+ revenge_vassal.ex_vassals -= src
+ revenge_vassal = null
+ blood_timer = null
+ return ..()
+
+/datum/antagonist/ex_vassal/proc/on_examine(datum/source, mob/examiner, examine_text)
+ SIGNAL_HANDLER
+
+ var/datum/antagonist/vassal/revenge/vassaldatum = examiner.mind?.has_antag_datum(/datum/antagonist/vassal/revenge)
+ if(vassaldatum && !revenge_vassal)
+ examine_text += span_notice("[owner.current] is an ex-vassal!")
+
+/datum/antagonist/ex_vassal/add_team_hud(mob/target)
+ QDEL_NULL(team_hud_ref)
+
+ team_hud_ref = WEAKREF(target.add_alt_appearance(
+ /datum/atom_hud/alternate_appearance/basic/has_antagonist,
+ "antag_team_hud_[REF(src)]",
+ hud_image_on(target),
+ ))
+
+ var/datum/atom_hud/alternate_appearance/basic/has_antagonist/hud = team_hud_ref.resolve()
+
+ var/list/mob/living/mob_list = list()
+ mob_list += revenge_vassal.owner.current
+ for(var/datum/antagonist/ex_vassal/former_vassals as anything in revenge_vassal.ex_vassals)
+ mob_list += former_vassals.owner.current
+
+ for (var/datum/atom_hud/alternate_appearance/basic/has_antagonist/antag_hud as anything in GLOB.has_antagonist_huds)
+ if(!(antag_hud.target in mob_list))
+ continue
+ antag_hud.show_to(target)
+ hud.show_to(antag_hud.target)
+
+/**
+ * Fold return
+ *
+ * Called when a Revenge bloodsucker gets a vassal back into the fold.
+ */
+/datum/antagonist/ex_vassal/proc/return_to_fold(datum/antagonist/vassal/revenge/mike_ehrmantraut)
+ revenge_vassal = mike_ehrmantraut
+ mike_ehrmantraut.ex_vassals += src
+ COOLDOWN_START(src, blood_timer, BLOOD_TIMER_REQUIREMENT)
+ add_team_hud(owner.current)
+
+ RegisterSignal(src, COMSIG_LIVING_LIFE, PROC_REF(on_life))
+
+/datum/antagonist/ex_vassal/proc/on_life(datum/source, seconds_per_tick, times_fired)
+ SIGNAL_HANDLER
+
+ if(COOLDOWN_TIMELEFT(src, blood_timer) <= BLOOD_TIMER_HALWAY + 2 && COOLDOWN_TIMELEFT(src, blood_timer) >= BLOOD_TIMER_HALWAY - 2) //just about halfway
+ to_chat(owner.current, span_cultbold("You need new blood from your Master!"))
+ if(!COOLDOWN_FINISHED(src, blood_timer))
+ return
+ to_chat(owner.current, span_cultbold("You are out of blood!"))
+ to_chat(revenge_vassal.owner.current, span_cultbold("[owner.current] has ran out of blood and is no longer in the fold!"))
+ owner.remove_antag_datum(/datum/antagonist/ex_vassal)
+
+
+/**
+ * Bloodsucker Blood
+ *
+ * Artificially made, this must be fed to ex-vassals to keep them on their high.
+ */
+/datum/reagent/blood/bloodsucker
+ name = "Blood two"
+
+/datum/reagent/blood/bloodsucker/expose_mob(mob/living/exposed_mob, methods, reac_volume, show_message, touch_protection)
+ var/datum/antagonist/ex_vassal/former_vassal = exposed_mob.mind.has_antag_datum(/datum/antagonist/ex_vassal)
+ if(former_vassal)
+ to_chat(exposed_mob, span_cult("You feel the blood restore you... You feel safe."))
+ COOLDOWN_RESET(former_vassal, blood_timer)
+ COOLDOWN_START(former_vassal, blood_timer, BLOOD_TIMER_REQUIREMENT)
+ return ..()
+
+#undef BLOOD_TIMER_REQUIREMENT
+#undef BLOOD_TIMER_HALWAY
diff --git a/modular_bandastation/blood_suckers/code/vassals/types/favorite.dm b/modular_bandastation/blood_suckers/code/vassals/types/favorite.dm
new file mode 100644
index 0000000000000..9401f92613ace
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/vassals/types/favorite.dm
@@ -0,0 +1,26 @@
+/**
+ * Favorite Vassal
+ *
+ * Gets some cool abilities depending on the Clan.
+ */
+/datum/antagonist/vassal/favorite
+ name = "\improper Favorite Vassal"
+ show_in_antagpanel = FALSE
+ antag_hud_name = "vassal6"
+ special_type = FAVORITE_VASSAL
+ vassal_description = "The Favorite Vassal gets unique abilities over other Vassals depending on your Clan \
+ and becomes completely immune to Mindshields. If part of Ventrue, this is the Vassal you will rank up."
+
+ ///Bloodsucker levels, but for Vassals, used by Ventrue.
+ var/vassal_level
+
+/datum/antagonist/vassal/favorite/on_gain()
+ . = ..()
+ SEND_SIGNAL(master, BLOODSUCKER_MAKE_FAVORITE, src)
+
+/datum/antagonist/vassal/favorite/pre_mindshield(mob/implanter, mob/living/mob_override)
+ return COMPONENT_MINDSHIELD_RESISTED
+
+///Set the Vassal's rank to their Bloodsucker level
+/datum/antagonist/vassal/favorite/proc/set_vassal_level(mob/living/carbon/human/target)
+ master.bloodsucker_level = vassal_level
diff --git a/modular_bandastation/blood_suckers/code/vassals/types/revenge.dm b/modular_bandastation/blood_suckers/code/vassals/types/revenge.dm
new file mode 100644
index 0000000000000..32ca44cec90c5
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/vassals/types/revenge.dm
@@ -0,0 +1,82 @@
+/**
+ * Revenge Vassal
+ *
+ * Has the goal to 'get revenge' when their Master dies.
+ */
+/datum/antagonist/vassal/revenge
+ name = "\improper Revenge Vassal"
+ roundend_category = "abandoned Vassals"
+ show_in_roundend = FALSE
+ show_in_antagpanel = FALSE
+ antag_hud_name = "vassal4"
+ special_type = REVENGE_VASSAL
+ vassal_description = "The Revenge Vassal will not deconvert on your Final Death, \
+ instead they will gain all your Powers, and the objective to take revenge for your demise. \
+ They additionally maintain your Vassals after your departure, rather than become aimless."
+
+ ///all ex-vassals brought back into the fold.
+ var/list/datum/antagonist/ex_vassal/ex_vassals = list()
+
+/datum/antagonist/vassal/revenge/roundend_report()
+ var/list/report = list()
+ report += printplayer(owner)
+ if(length(objectives))
+ report += printobjectives(objectives)
+
+ // Now list their vassals
+ if(length(ex_vassals))
+ report += span_header("The Vassals brought back into the fold were...")
+ for(var/datum/antagonist/ex_vassal/all_vassals as anything in ex_vassals)
+ if(!all_vassals.owner)
+ continue
+ report += "[all_vassals.owner.name] the [all_vassals.owner.assigned_role.title]"
+
+ return report.Join(" ")
+
+/datum/antagonist/vassal/revenge/on_gain()
+ . = ..()
+ RegisterSignal(master, BLOODSUCKER_FINAL_DEATH, PROC_REF(on_master_death))
+
+/datum/antagonist/vassal/revenge/on_removal()
+ UnregisterSignal(master, BLOODSUCKER_FINAL_DEATH)
+ return ..()
+
+/datum/antagonist/vassal/revenge/ui_static_data(mob/user)
+ var/list/data = list()
+ for(var/datum/action/cooldown/bloodsucker/power as anything in powers)
+ var/list/power_data = list()
+
+ power_data["power_name"] = power.name
+ power_data["power_explanation"] = power.power_explanation
+ power_data["power_icon"] = power.button_icon_state
+
+ data["power"] += list(power_data)
+
+ return data + ..()
+
+/datum/antagonist/vassal/revenge/proc/on_master_death(datum/antagonist/bloodsucker/bloodsuckerdatum, mob/living/carbon/master)
+ SIGNAL_HANDLER
+
+ show_in_roundend = TRUE
+ for(var/datum/objective/all_objectives as anything in objectives)
+ objectives -= all_objectives
+ BuyPower(new /datum/action/cooldown/bloodsucker/vassal_blood)
+ for(var/datum/action/cooldown/bloodsucker/master_powers as anything in bloodsuckerdatum.powers)
+ if(master_powers.purchase_flags & BLOODSUCKER_DEFAULT_POWER)
+ continue
+ master_powers.Grant(owner.current)
+ owner.current.remove_status_effect(/datum/status_effect/agent_pinpointer/vassal_edition)
+
+ var/datum/objective/survive/new_objective = new
+ new_objective.name = "Avenge Bloodsucker"
+ new_objective.explanation_text = "Avenge your Bloodsucker's death by recruiting their ex-vassals and continuing their operations."
+ new_objective.owner = owner
+ objectives += new_objective
+
+ if(info_button_ref)
+ QDEL_NULL(info_button_ref)
+
+ ui_name = "AntagInfoRevengeVassal" //give their new ui
+ var/datum/action/antag_info/info_button = new(src)
+ info_button.Grant(owner.current)
+ info_button_ref = WEAKREF(info_button)
diff --git a/modular_bandastation/blood_suckers/code/vassals/vassal_datum.dm b/modular_bandastation/blood_suckers/code/vassals/vassal_datum.dm
new file mode 100644
index 0000000000000..0f6cc3001f127
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/vassals/vassal_datum.dm
@@ -0,0 +1,176 @@
+#define VASSAL_SCAN_MIN_DISTANCE 5
+#define VASSAL_SCAN_MAX_DISTANCE 500
+/// 2s update time.
+#define VASSAL_SCAN_PING_TIME 20
+
+/datum/antagonist/vassal
+ name = "\improper Vassal"
+ roundend_category = "vassals"
+ antagpanel_category = "Bloodsucker"
+ job_rank = ROLE_BLOODSUCKER
+ antag_flags = parent_type::antag_flags | FLAG_ANTAG_CAP_IGNORE
+ antag_hud_name = "vassal"
+ show_in_roundend = FALSE
+ hud_icon = 'modular_bandastation/blood_suckers/icons/bloodsucker_icons.dmi'
+
+ /// The Master Bloodsucker's antag datum.
+ var/datum/antagonist/bloodsucker/master
+ /// List of all Purchased Powers, like Bloodsuckers.
+ var/list/datum/action/powers = list()
+ ///Whether this vassal is already a special type of Vassal.
+ var/special_type = FALSE
+ ///Description of what this Vassal does.
+ var/vassal_description
+
+/datum/antagonist/vassal/antag_panel_data()
+ return "Master : [master.owner.name]"
+
+/datum/antagonist/vassal/apply_innate_effects(mob/living/mob_override)
+ . = ..()
+ var/mob/living/current_mob = mob_override || owner.current
+ current_mob.apply_status_effect(/datum/status_effect/agent_pinpointer/vassal_edition)
+ current_mob.clear_mood_event("vampcandle")
+ add_team_hud(current_mob)
+
+/datum/antagonist/vassal/add_team_hud(mob/target)
+ QDEL_NULL(team_hud_ref)
+
+ team_hud_ref = WEAKREF(target.add_alt_appearance(
+ /datum/atom_hud/alternate_appearance/basic/has_antagonist,
+ "antag_team_hud_[REF(src)]",
+ hud_image_on(target),
+ ))
+
+ var/datum/atom_hud/alternate_appearance/basic/has_antagonist/hud = team_hud_ref.resolve()
+
+ var/list/mob/living/mob_list = list()
+ mob_list += master.owner.current
+ for(var/datum/antagonist/vassal/vassal as anything in master.vassals)
+ mob_list += vassal.owner.current
+
+ for (var/datum/atom_hud/alternate_appearance/basic/has_antagonist/antag_hud as anything in GLOB.has_antagonist_huds)
+ if(!(antag_hud.target in mob_list))
+ continue
+ antag_hud.show_to(target)
+ hud.show_to(antag_hud.target)
+
+/datum/antagonist/vassal/remove_innate_effects(mob/living/mob_override)
+ . = ..()
+ var/mob/living/current_mob = mob_override || owner.current
+ current_mob.remove_status_effect(/datum/status_effect/agent_pinpointer/vassal_edition)
+
+/datum/antagonist/vassal/pre_mindshield(mob/implanter, mob/living/mob_override)
+ return COMPONENT_MINDSHIELD_PASSED
+
+/// This is called when the antagonist is successfully mindshielded.
+/datum/antagonist/vassal/on_mindshield(mob/implanter, mob/living/mob_override)
+ owner.remove_antag_datum(/datum/antagonist/vassal)
+ owner.current.log_message("has been deconverted from Vassalization by [implanter]!", LOG_ATTACK, color="#960000")
+ return COMPONENT_MINDSHIELD_DECONVERTED
+
+/datum/antagonist/vassal/proc/on_examine(datum/source, mob/examiner, examine_text)
+ SIGNAL_HANDLER
+ var/vassal_examine = return_vassal_examine(examiner)
+ if(vassal_examine)
+ examine_text += vassal_examine
+
+/datum/antagonist/vassal/on_gain()
+ RegisterSignal(owner.current, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
+ RegisterSignal(SSsunlight, COMSIG_SOL_WARNING_GIVEN, PROC_REF(give_warning))
+ /// Enslave them to their Master
+ if(!master || !istype(master, master))
+ return
+ if(special_type)
+ if(!master.special_vassals[special_type])
+ master.special_vassals[special_type] = list()
+ master.special_vassals[special_type] |= src
+ master.vassals |= src
+ owner.enslave_mind_to_creator(master.owner.current)
+ owner.current.log_message("has been vassalized by [master.owner.current]!", LOG_ATTACK, color="#960000")
+ /// Give Recuperate Power
+ BuyPower(new /datum/action/cooldown/bloodsucker/recuperate)
+ /// Give Objectives
+ var/datum/objective/bloodsucker/vassal/vassal_objective = new
+ vassal_objective.owner = owner
+ objectives += vassal_objective
+ /// Give Vampire Language & Hud
+ owner.current.grant_all_languages(FALSE, FALSE, TRUE)
+ owner.current.grant_language(/datum/language/vampiric)
+ return ..()
+
+/datum/antagonist/vassal/on_removal()
+ UnregisterSignal(owner.current, COMSIG_ATOM_EXAMINE)
+ UnregisterSignal(SSsunlight, COMSIG_SOL_WARNING_GIVEN)
+ //Free them from their Master
+ if(!QDELETED(master?.owner))
+ if(special_type && master.special_vassals[special_type])
+ master.special_vassals[special_type] -= src
+ master.vassals -= src
+ owner.enslaved_to = null
+ //Remove ALL Traits, as long as its from BLOODSUCKER_TRAIT's source.
+ for(var/allstatus_traits in owner.current._status_traits)
+ REMOVE_TRAIT(owner.current, allstatus_traits, BLOODSUCKER_TRAIT)
+ //Remove Recuperate Power
+ while(length(powers))
+ var/datum/action/cooldown/bloodsucker/power = pick(powers)
+ powers -= power
+ power.Remove(owner.current)
+ //Remove Language & Hud
+ owner.current.remove_language(/datum/language/vampiric)
+ return ..()
+
+/datum/antagonist/vassal/on_body_transfer(mob/living/old_body, mob/living/new_body)
+ . = ..()
+ for(var/datum/action/cooldown/bloodsucker/all_powers as anything in powers)
+ all_powers.Remove(old_body)
+ all_powers.Grant(new_body)
+
+/datum/antagonist/vassal/greet()
+ . = ..()
+ if(silent)
+ return
+
+ to_chat(owner, span_userdanger("You are now the mortal servant of [master.owner.current], a Bloodsucker!"))
+ to_chat(owner, span_boldannounce("The power of [master.owner.current.p_their()] immortal blood compels you to obey [master.owner.current.p_them()] in all things, even offering your own life to prolong theirs.\n\
+ You are not required to obey any other Bloodsucker, for only [master.owner.current] is your master. The laws of Nanotrasen do not apply to you now; only your vampiric master's word must be obeyed.")) // if only there was a /p_theirs() proc...
+ owner.current.playsound_local(null, 'sound/magic/mutate.ogg', 100, FALSE, pressure_affected = FALSE)
+ antag_memory += "You, becoming the mortal servant of [master.owner.current], a bloodsucking vampire! "
+ /// Message told to your Master.
+ to_chat(master.owner, span_userdanger("[owner.current] has become addicted to your immortal blood. [owner.current.p_they(TRUE)] [owner.current.p_are()] now your undying servant!"))
+ master.owner.current.playsound_local(null, 'sound/magic/mutate.ogg', 100, FALSE, pressure_affected = FALSE)
+
+/datum/antagonist/vassal/farewell()
+ if(silent)
+ return
+
+ owner.current.visible_message(
+ span_deconversion_message("[owner.current]'s eyes dart feverishly from side to side, and then stop. [owner.current.p_they(TRUE)] seem[owner.current.p_s()] calm, \
+ like [owner.current.p_they()] [owner.current.p_have()] regained some lost part of [owner.current.p_them()]self."), \
+ span_deconversion_message("With a snap, you are no longer enslaved to [master.owner]! You breathe in heavily, having regained your free will."))
+ owner.current.playsound_local(null, 'sound/magic/mutate.ogg', 100, FALSE, pressure_affected = FALSE)
+ /// Message told to your (former) Master.
+ if(master && master.owner)
+ to_chat(master.owner, span_cultbold("You feel the bond with your vassal [owner.current] has somehow been broken!"))
+
+/datum/antagonist/vassal/admin_add(datum/mind/new_owner, mob/admin)
+ var/list/datum/mind/possible_vampires = list()
+ for(var/datum/antagonist/bloodsucker/bloodsuckerdatums in GLOB.antagonists)
+ var/datum/mind/vamp = bloodsuckerdatums.owner
+ if(!vamp)
+ continue
+ if(!vamp.current)
+ continue
+ if(vamp.current.stat == DEAD)
+ continue
+ possible_vampires += vamp
+ if(!length(possible_vampires))
+ message_admins("[key_name_admin(usr)] tried vassalizing [key_name_admin(new_owner)], but there were no bloodsuckers!")
+ return
+ var/datum/mind/choice = input("Which bloodsucker should this vassal belong to?", "Bloodsucker") in possible_vampires
+ if(!choice)
+ return
+ log_admin("[key_name_admin(usr)] turned [key_name_admin(new_owner)] into a vassal of [key_name_admin(choice)]!")
+ var/datum/antagonist/bloodsucker/vampire = choice.has_antag_datum(/datum/antagonist/bloodsucker)
+ master = vampire
+ new_owner.add_antag_datum(src)
+ to_chat(choice, span_notice("Through divine intervention, you've gained a new vassal!"))
diff --git a/modular_bandastation/blood_suckers/code/vassals/vassal_misc_procs.dm b/modular_bandastation/blood_suckers/code/vassals/vassal_misc_procs.dm
new file mode 100644
index 0000000000000..3b56b2f886c45
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/vassals/vassal_misc_procs.dm
@@ -0,0 +1,72 @@
+/datum/antagonist/vassal/proc/give_warning(atom/source, danger_level, vampire_warning_message, vassal_warning_message)
+ SIGNAL_HANDLER
+ if(vassal_warning_message)
+ to_chat(owner, vassal_warning_message)
+
+/**
+ * Returns a Vassals's examine strings.
+ * Args:
+ * viewer - The person examining.
+ */
+/datum/antagonist/vassal/proc/return_vassal_examine(mob/living/viewer)
+ if(!viewer.mind || !iscarbon(owner.current))
+ return FALSE
+ var/mob/living/carbon/carbon_current = owner.current
+ // Target must be a Vassal
+ // Default String
+ var/returnString = "\["
+ var/returnIcon = ""
+ // Vassals and Bloodsuckers recognize eachother, while Monster Hunters can see Vassals.
+ if(!IS_BLOODSUCKER(viewer) && !IS_VASSAL(viewer))
+ return FALSE
+ // Am I Viewer's Vassal?
+ if(master.owner == viewer.mind)
+ returnString += "This [carbon_current.dna.species.name] bears YOUR mark!"
+ returnIcon = "[icon2html('modular_bandastation/blood_suckers/icons/vampiric.dmi', world, "vassal")]"
+ // Am I someone ELSE'S Vassal?
+ else if(IS_BLOODSUCKER(viewer))
+ returnString += "This [carbon_current.dna.species.name] bears the mark of [master.return_full_name()][master.broke_masquerade ? " who has broken the Masquerade" : ""]"
+ returnIcon = "[icon2html('modular_bandastation/blood_suckers/icons/vampiric.dmi', world, "vassal_grey")]"
+ // Are you serving the same master as I am?
+ else if(viewer.mind.has_antag_datum(/datum/antagonist/vassal) in master.vassals)
+ returnString += "[p_they(TRUE)] bears the mark of your Master"
+ returnIcon = "[icon2html('modular_bandastation/blood_suckers/icons/vampiric.dmi', world, "vassal")]"
+ // You serve a different Master than I do.
+ else
+ returnString += "[p_they(TRUE)] bears the mark of another Bloodsucker"
+ returnIcon = "[icon2html('modular_bandastation/blood_suckers/icons/vampiric.dmi', world, "vassal_grey")]"
+
+ returnString += "\]" // \n" Don't need spacers. Using . += "" in examine.dm does this on its own.
+ return returnIcon + returnString
+
+/// Used when your Master teaches you a new Power.
+/datum/antagonist/vassal/proc/BuyPower(datum/action/cooldown/bloodsucker/power)
+ powers += power
+ power.Grant(owner.current)
+ log_uplink("[key_name(owner.current)] purchased [power].")
+
+/datum/antagonist/vassal/proc/LevelUpPowers()
+ for(var/datum/action/cooldown/bloodsucker/power in powers)
+ power.level_current++
+
+/// Called when we are made into the Favorite Vassal
+/datum/antagonist/vassal/proc/make_special(datum/antagonist/vassal/vassal_type)
+ //store what we need
+ var/datum/mind/vassal_owner = owner
+ var/datum/antagonist/bloodsucker/bloodsuckerdatum = master
+
+ //remove our antag datum
+ silent = TRUE
+ vassal_owner.remove_antag_datum(/datum/antagonist/vassal)
+
+ //give our new one
+ var/datum/antagonist/vassal/vassaldatum = new vassal_type(vassal_owner)
+ vassaldatum.master = bloodsuckerdatum
+ vassaldatum.silent = TRUE
+ vassal_owner.add_antag_datum(vassaldatum)
+ vassaldatum.silent = FALSE
+
+ //send alerts of completion
+ to_chat(master, span_danger("You have turned [vassal_owner.current] into your [vassaldatum.name]! They will no longer be deconverted upon Mindshielding!"))
+ to_chat(vassal_owner, span_notice("As Blood drips over your body, you feel closer to your Master... You are now the Favorite Vassal!"))
+ vassal_owner.current.playsound_local(null, 'sound/magic/mutate.ogg', vol = 75, vary = FALSE, pressure_affected = FALSE)
diff --git a/modular_bandastation/blood_suckers/code/vassals/vassal_pinpointer.dm b/modular_bandastation/blood_suckers/code/vassals/vassal_pinpointer.dm
new file mode 100644
index 0000000000000..4d0c6b00f213d
--- /dev/null
+++ b/modular_bandastation/blood_suckers/code/vassals/vassal_pinpointer.dm
@@ -0,0 +1,31 @@
+/**
+ * # Vassal Pinpointer
+ *
+ * Pinpointer that points to their Master's location at all times.
+ * Unlike the Monster hunter one, this one is permanently active, and has no power needed to activate it.
+ */
+
+/atom/movable/screen/alert/status_effect/agent_pinpointer/vassal_edition
+ name = "Blood Bond"
+ desc = "You always know where your master is."
+
+/datum/status_effect/agent_pinpointer/vassal_edition
+ id = "agent_pinpointer"
+ alert_type = /atom/movable/screen/alert/status_effect/agent_pinpointer/vassal_edition
+ minimum_range = VASSAL_SCAN_MIN_DISTANCE
+ tick_interval = VASSAL_SCAN_PING_TIME
+ duration = STATUS_EFFECT_PERMANENT
+ range_fuzz_factor = 0
+
+/datum/status_effect/agent_pinpointer/vassal_edition/on_creation(mob/living/new_owner, ...)
+ ..()
+ var/datum/antagonist/vassal/antag_datum = new_owner.mind.has_antag_datum(/datum/antagonist/vassal)
+ scan_target = antag_datum?.master?.owner?.current
+
+/datum/status_effect/agent_pinpointer/vassal_edition/scan_for_target()
+ return
+
+/datum/status_effect/agent_pinpointer/vassal_edition/Destroy()
+ if(scan_target)
+ to_chat(owner, span_notice("You've lost your master's trail."))
+ return ..()
diff --git a/modular_bandastation/blood_suckers/icons/actions_bloodsucker.dmi b/modular_bandastation/blood_suckers/icons/actions_bloodsucker.dmi
new file mode 100644
index 0000000000000..6d9b2d5dd6ef4
Binary files /dev/null and b/modular_bandastation/blood_suckers/icons/actions_bloodsucker.dmi differ
diff --git a/modular_bandastation/blood_suckers/icons/actions_tremere_bloodsucker.dmi b/modular_bandastation/blood_suckers/icons/actions_tremere_bloodsucker.dmi
new file mode 100644
index 0000000000000..057b17bbe103d
Binary files /dev/null and b/modular_bandastation/blood_suckers/icons/actions_tremere_bloodsucker.dmi differ
diff --git a/modular_bandastation/blood_suckers/icons/bloodsucker_icons.dmi b/modular_bandastation/blood_suckers/icons/bloodsucker_icons.dmi
new file mode 100644
index 0000000000000..04b34e67e2187
Binary files /dev/null and b/modular_bandastation/blood_suckers/icons/bloodsucker_icons.dmi differ
diff --git a/modular_bandastation/blood_suckers/icons/bs_leftinhand.dmi b/modular_bandastation/blood_suckers/icons/bs_leftinhand.dmi
new file mode 100644
index 0000000000000..bb90d2c9a7200
Binary files /dev/null and b/modular_bandastation/blood_suckers/icons/bs_leftinhand.dmi differ
diff --git a/modular_bandastation/blood_suckers/icons/bs_rightinhand.dmi b/modular_bandastation/blood_suckers/icons/bs_rightinhand.dmi
new file mode 100644
index 0000000000000..9621475609e9f
Binary files /dev/null and b/modular_bandastation/blood_suckers/icons/bs_rightinhand.dmi differ
diff --git a/modular_bandastation/blood_suckers/icons/clan_icons.dmi b/modular_bandastation/blood_suckers/icons/clan_icons.dmi
new file mode 100644
index 0000000000000..f6242a2143db5
Binary files /dev/null and b/modular_bandastation/blood_suckers/icons/clan_icons.dmi differ
diff --git a/modular_bandastation/blood_suckers/icons/infils.dmi b/modular_bandastation/blood_suckers/icons/infils.dmi
new file mode 100644
index 0000000000000..67bb50af5a6b2
Binary files /dev/null and b/modular_bandastation/blood_suckers/icons/infils.dmi differ
diff --git a/modular_bandastation/blood_suckers/icons/stakes.dmi b/modular_bandastation/blood_suckers/icons/stakes.dmi
new file mode 100644
index 0000000000000..dfc1dc08bf433
Binary files /dev/null and b/modular_bandastation/blood_suckers/icons/stakes.dmi differ
diff --git a/modular_bandastation/blood_suckers/icons/timestop_guardian.dmi b/modular_bandastation/blood_suckers/icons/timestop_guardian.dmi
new file mode 100644
index 0000000000000..d4e6a60883d85
Binary files /dev/null and b/modular_bandastation/blood_suckers/icons/timestop_guardian.dmi differ
diff --git a/modular_bandastation/blood_suckers/icons/vamp_obj.dmi b/modular_bandastation/blood_suckers/icons/vamp_obj.dmi
new file mode 100644
index 0000000000000..b3937df385c30
Binary files /dev/null and b/modular_bandastation/blood_suckers/icons/vamp_obj.dmi differ
diff --git a/modular_bandastation/blood_suckers/icons/vamp_obj_64.dmi b/modular_bandastation/blood_suckers/icons/vamp_obj_64.dmi
new file mode 100644
index 0000000000000..4367da28b32b8
Binary files /dev/null and b/modular_bandastation/blood_suckers/icons/vamp_obj_64.dmi differ
diff --git a/modular_bandastation/blood_suckers/icons/vampiric.dmi b/modular_bandastation/blood_suckers/icons/vampiric.dmi
new file mode 100644
index 0000000000000..1cf92d5c97a7c
Binary files /dev/null and b/modular_bandastation/blood_suckers/icons/vampiric.dmi differ
diff --git a/modular_bandastation/blood_suckers/sound/BloodsuckerAlert.ogg b/modular_bandastation/blood_suckers/sound/BloodsuckerAlert.ogg
new file mode 100644
index 0000000000000..100e81018c388
Binary files /dev/null and b/modular_bandastation/blood_suckers/sound/BloodsuckerAlert.ogg differ
diff --git a/modular_bandastation/blood_suckers/sound/coffin_close.ogg b/modular_bandastation/blood_suckers/sound/coffin_close.ogg
new file mode 100644
index 0000000000000..9f5852d65b814
Binary files /dev/null and b/modular_bandastation/blood_suckers/sound/coffin_close.ogg differ
diff --git a/modular_bandastation/blood_suckers/sound/coffin_open.ogg b/modular_bandastation/blood_suckers/sound/coffin_open.ogg
new file mode 100644
index 0000000000000..d936db143e77e
Binary files /dev/null and b/modular_bandastation/blood_suckers/sound/coffin_open.ogg differ
diff --git a/modular_bandastation/blood_suckers/sound/griffin_1.ogg b/modular_bandastation/blood_suckers/sound/griffin_1.ogg
new file mode 100644
index 0000000000000..d722f609e70c7
Binary files /dev/null and b/modular_bandastation/blood_suckers/sound/griffin_1.ogg differ
diff --git a/modular_bandastation/blood_suckers/sound/griffin_10.ogg b/modular_bandastation/blood_suckers/sound/griffin_10.ogg
new file mode 100644
index 0000000000000..b1c1138d70e6e
Binary files /dev/null and b/modular_bandastation/blood_suckers/sound/griffin_10.ogg differ
diff --git a/modular_bandastation/blood_suckers/sound/griffin_2.ogg b/modular_bandastation/blood_suckers/sound/griffin_2.ogg
new file mode 100644
index 0000000000000..4b122afcde4ab
Binary files /dev/null and b/modular_bandastation/blood_suckers/sound/griffin_2.ogg differ
diff --git a/modular_bandastation/blood_suckers/sound/griffin_3.ogg b/modular_bandastation/blood_suckers/sound/griffin_3.ogg
new file mode 100644
index 0000000000000..7d73ad576531d
Binary files /dev/null and b/modular_bandastation/blood_suckers/sound/griffin_3.ogg differ
diff --git a/modular_bandastation/blood_suckers/sound/griffin_4.ogg b/modular_bandastation/blood_suckers/sound/griffin_4.ogg
new file mode 100644
index 0000000000000..38835540a7218
Binary files /dev/null and b/modular_bandastation/blood_suckers/sound/griffin_4.ogg differ
diff --git a/modular_bandastation/blood_suckers/sound/griffin_5.ogg b/modular_bandastation/blood_suckers/sound/griffin_5.ogg
new file mode 100644
index 0000000000000..878fd6f40ecb6
Binary files /dev/null and b/modular_bandastation/blood_suckers/sound/griffin_5.ogg differ
diff --git a/modular_bandastation/blood_suckers/sound/griffin_6.ogg b/modular_bandastation/blood_suckers/sound/griffin_6.ogg
new file mode 100644
index 0000000000000..4f7e0eb2c6374
Binary files /dev/null and b/modular_bandastation/blood_suckers/sound/griffin_6.ogg differ
diff --git a/modular_bandastation/blood_suckers/sound/griffin_7.ogg b/modular_bandastation/blood_suckers/sound/griffin_7.ogg
new file mode 100644
index 0000000000000..f3b76da177169
Binary files /dev/null and b/modular_bandastation/blood_suckers/sound/griffin_7.ogg differ
diff --git a/modular_bandastation/blood_suckers/sound/griffin_8.ogg b/modular_bandastation/blood_suckers/sound/griffin_8.ogg
new file mode 100644
index 0000000000000..8c328fd723249
Binary files /dev/null and b/modular_bandastation/blood_suckers/sound/griffin_8.ogg differ
diff --git a/modular_bandastation/blood_suckers/sound/griffin_9.ogg b/modular_bandastation/blood_suckers/sound/griffin_9.ogg
new file mode 100644
index 0000000000000..f7d6fcbdd254e
Binary files /dev/null and b/modular_bandastation/blood_suckers/sound/griffin_9.ogg differ
diff --git a/modular_bandastation/blood_suckers/sound/lunge_warn.ogg b/modular_bandastation/blood_suckers/sound/lunge_warn.ogg
new file mode 100644
index 0000000000000..db49b1e56ce2f
Binary files /dev/null and b/modular_bandastation/blood_suckers/sound/lunge_warn.ogg differ
diff --git a/modular_bandastation/blood_suckers/sound/owl_1.ogg b/modular_bandastation/blood_suckers/sound/owl_1.ogg
new file mode 100644
index 0000000000000..ed25c6ac4148c
Binary files /dev/null and b/modular_bandastation/blood_suckers/sound/owl_1.ogg differ
diff --git a/modular_bandastation/blood_suckers/sound/owl_10.oga b/modular_bandastation/blood_suckers/sound/owl_10.oga
new file mode 100644
index 0000000000000..97f45d4337886
Binary files /dev/null and b/modular_bandastation/blood_suckers/sound/owl_10.oga differ
diff --git a/modular_bandastation/blood_suckers/sound/owl_2.ogg b/modular_bandastation/blood_suckers/sound/owl_2.ogg
new file mode 100644
index 0000000000000..12f26fb6223ea
Binary files /dev/null and b/modular_bandastation/blood_suckers/sound/owl_2.ogg differ
diff --git a/modular_bandastation/blood_suckers/sound/owl_3.ogg b/modular_bandastation/blood_suckers/sound/owl_3.ogg
new file mode 100644
index 0000000000000..f64b193e4ee3e
Binary files /dev/null and b/modular_bandastation/blood_suckers/sound/owl_3.ogg differ
diff --git a/modular_bandastation/blood_suckers/sound/owl_5.ogg b/modular_bandastation/blood_suckers/sound/owl_5.ogg
new file mode 100644
index 0000000000000..e4fd7cd2bb083
Binary files /dev/null and b/modular_bandastation/blood_suckers/sound/owl_5.ogg differ
diff --git a/modular_bandastation/blood_suckers/sound/owl_6.ogg b/modular_bandastation/blood_suckers/sound/owl_6.ogg
new file mode 100644
index 0000000000000..8dacf98503728
Binary files /dev/null and b/modular_bandastation/blood_suckers/sound/owl_6.ogg differ
diff --git a/modular_bandastation/blood_suckers/sound/owl_7.ogg b/modular_bandastation/blood_suckers/sound/owl_7.ogg
new file mode 100644
index 0000000000000..249f171052cf7
Binary files /dev/null and b/modular_bandastation/blood_suckers/sound/owl_7.ogg differ
diff --git a/modular_bandastation/blood_suckers/sound/owl_8.ogg b/modular_bandastation/blood_suckers/sound/owl_8.ogg
new file mode 100644
index 0000000000000..0439517a30f17
Binary files /dev/null and b/modular_bandastation/blood_suckers/sound/owl_8.ogg differ
diff --git a/modular_bandastation/blood_suckers/sound/owl_9.ogg b/modular_bandastation/blood_suckers/sound/owl_9.ogg
new file mode 100644
index 0000000000000..54bc5f971ff11
Binary files /dev/null and b/modular_bandastation/blood_suckers/sound/owl_9.ogg differ
diff --git a/modular_bandastation/modular_bandastation.dme b/modular_bandastation/modular_bandastation.dme
index e0d19e63e5c8d..da123085a8050 100644
--- a/modular_bandastation/modular_bandastation.dme
+++ b/modular_bandastation/modular_bandastation.dme
@@ -44,6 +44,7 @@
#include "automatic_crew_transfer/_automatic_crew_transfer.dme"
#include "outfits/_outfits.dme"
#include "overrides/_overrides.dme"
+#include "blood_suckers/_blood_suckers.dme"
// --- PRIME --- //
diff --git a/tgstation.dme b/tgstation.dme
index 0fb0b25756864..52cf1f29584dd 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -54,6 +54,7 @@
#include "code\__DEFINES\blend_modes.dm"
#include "code\__DEFINES\blob_defines.dm"
#include "code\__DEFINES\blood.dm"
+#include "code\__DEFINES\bloodsuckers.dm"
#include "code\__DEFINES\bodyparts.dm"
#include "code\__DEFINES\botany.dm"
#include "code\__DEFINES\callbacks.dm"
diff --git a/tgui/packages/tgui/interfaces/AntagInfoBloodsucker.tsx b/tgui/packages/tgui/interfaces/AntagInfoBloodsucker.tsx
new file mode 100644
index 0000000000000..48b28d1cbba11
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/AntagInfoBloodsucker.tsx
@@ -0,0 +1,275 @@
+import { resolveAsset } from '../assets';
+import { BooleanLike } from 'common/react';
+import { useBackend, useLocalState } from '../backend';
+import {
+ Box,
+ Button,
+ Divider,
+ Dropdown,
+ Section,
+ Stack,
+ Tabs,
+} from '../components';
+import { Window } from '../layouts';
+
+type Objective = {
+ count: number;
+ name: string;
+ explanation: string;
+ complete: BooleanLike;
+ was_uncompleted: BooleanLike;
+ reward: number;
+};
+
+type BloodsuckerInformation = {
+ clan: ClanInfo[];
+ in_clan: BooleanLike;
+ power: PowerInfo[];
+};
+
+type ClanInfo = {
+ clan_name: string;
+ clan_description: string;
+ clan_icon: string;
+};
+
+type PowerInfo = {
+ power_name: string;
+ power_explanation: string;
+ power_icon: string;
+};
+
+type Info = {
+ objectives: Objective[];
+};
+
+const ObjectivePrintout = (props: any) => {
+ const { data } = useBackend();
+ const { objectives } = data;
+ return (
+
+ Your current objectives:
+
+ {(!objectives && 'None!') ||
+ objectives.map((objective) => (
+
+ #{objective.count}: {objective.explanation}
+
+ ))}
+
+
+ );
+};
+
+export const AntagInfoBloodsucker = (props: any) => {
+ const [tab, setTab] = useLocalState('tab', 1);
+ return (
+
+
+
+ setTab(1)}
+ >
+ Introduction
+
+ setTab(2)}
+ >
+ Clan & Powers
+
+
+ {tab === 1 && }
+ {tab === 2 && }
+
+
+ );
+};
+
+const BloodsuckerIntro = () => {
+ return (
+
+
+
+
+
+ You are a Bloodsucker, an undead blood-seeking monster living
+ aboard Space Station 13
+
+
+
+
+
+
+
+
+
+
+
+
+ You regenerate your health slowly, you're weak to fire, and
+ you depend on blood to survive. Don't allow your blood to
+ run too low, or you'll enter a
+
+ Frenzy!
+
+ Beware of your Humanity level! The more Humanity you lose, the
+ easier it is to fall into a{' '}
+ Frenzy!
+
+
+
+ Avoid using your Feed ability while near others, or else you
+ will risk breaking the Masquerade!
+
+
+
+
+
+
+
+
+
+ Rest in a Coffin to claim it, and that area, as your lair.
+
+ Examine your new structures to see how they function!
+
+ Medical and Genetic Analyzers can sell you out, your Masquerade
+ ability will hide your identity to prevent this.
+
+
+
+
+ Other Bloodsuckers are not necessarily your friends, but your
+ survival may depend on cooperation. Betray them at your own
+ discretion and peril.
+
+
+
+
+
+
+ );
+};
+
+const BloodsuckerClan = (props: any) => {
+ const { act, data } = useBackend();
+ const { clan, in_clan } = data;
+
+ if (!in_clan) {
+ return (
+
+
+ You are not in a Clan.
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {clan.map((ClanInfo) => (
+ <>
+
+
+ You are part of the {ClanInfo.clan_name}
+
+
+ {ClanInfo.clan_description}
+
+ >
+ ))}
+
+
+
+
+
+
+ );
+};
+
+const PowerSection = (props: any) => {
+ const { act, data } = useBackend();
+ const { power } = data;
+ if (!power) {
+ return ;
+ }
+
+ const [selectedPower, setSelectedPower] = useLocalState('power', power[0]);
+
+ return (
+
+ }
+ >
+
+
+ powers.power_name)}
+ onSelected={(powerName: string) =>
+ setSelectedPower(
+ power.find((p) => p.power_name === powerName) || power[0],
+ )
+ }
+ />
+ {selectedPower && (
+
+ )}
+
+
+
+
+ {selectedPower && selectedPower.power_explanation}
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/AntagInfoRevengeVassal.tsx b/tgui/packages/tgui/interfaces/AntagInfoRevengeVassal.tsx
new file mode 100644
index 0000000000000..ed30a27172f43
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/AntagInfoRevengeVassal.tsx
@@ -0,0 +1,150 @@
+import { resolveAsset } from '../assets';
+import { BooleanLike } from 'common/react';
+import { useBackend, useLocalState } from '../backend';
+import { Box, Button, Divider, Dropdown, Section, Stack } from '../components';
+import { Window } from '../layouts';
+
+type Objective = {
+ count: number;
+ name: string;
+ explanation: string;
+ complete: BooleanLike;
+ was_uncompleted: BooleanLike;
+ reward: number;
+};
+
+type BloodsuckerInformation = {
+ power: PowerInfo[];
+};
+
+type PowerInfo = {
+ power_name: string;
+ power_explanation: string;
+ power_icon: string;
+};
+
+type Info = {
+ objectives: Objective[];
+};
+
+const ObjectivePrintout = (props: any, context: any) => {
+ const { data } = useBackend(context);
+ const { objectives } = data;
+ return (
+
+ Your current objectives:
+
+ {(!objectives && 'None!') ||
+ objectives.map((objective) => (
+
+ #{objective.count}: {objective.explanation}
+
+ ))}
+
+
+ );
+};
+
+export const AntagInfoRevengeVassal = (props: any, context: any) => {
+ return (
+
+
+
+
+
+ );
+};
+
+const VassalInfo = () => {
+ return (
+
+
+
+
+
+ You are a Vassal tasked with taking revenge for the death of your
+ Master!
+
+
+
+
+
+
+
+
+
+
+
+
+ You have gained your Master's old Powers, and a brand new
+ power. You will have to survive and maintain your old
+ Master's integrity. Bring their old Vassals back into the
+ fold using your new Ability.
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const PowerSection = (props: any, context: any) => {
+ const { act, data } = useBackend(context);
+ const { power } = data;
+ if (!power) {
+ return ;
+ }
+
+ const [selectedPower, setSelectedPower] = useLocalState(
+ context,
+ 'power',
+ power[0]
+ );
+
+ return (
+
+ }>
+
+
+ powers.power_name)}
+ onSelected={(powerName: string) =>
+ setSelectedPower(
+ power.find((p) => p.power_name === powerName) || power[0]
+ )
+ }
+ />
+ {selectedPower && (
+
+ )}
+
+
+
+
+ {selectedPower && selectedPower.power_explanation}
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/KindredBook.tsx b/tgui/packages/tgui/interfaces/KindredBook.tsx
new file mode 100644
index 0000000000000..be035ba121493
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/KindredBook.tsx
@@ -0,0 +1,42 @@
+import { useBackend } from '../backend';
+import { Collapsible, Table, Section } from '../components';
+import { Window } from '../layouts';
+
+type Data = {
+ clans: ClanInfo[];
+};
+
+type ClanInfo = {
+ clan_name: string;
+ clan_desc: string;
+};
+
+export const KindredBook = (props, context) => {
+ const { data } = useBackend(context);
+ const { clans } = data;
+ return (
+
+
+
+
+
+ Written by generations of Curators, this holds all information we
+ the Curators know about the undead threat that looms the
+ station...
+
+ So, what Clan are you interested in?
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/bloodsucker.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/bloodsucker.ts
new file mode 100644
index 0000000000000..7807e6955ffd3
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/bloodsucker.ts
@@ -0,0 +1,16 @@
+import { Antagonist, Category } from '../base';
+
+const Bloodsucker: Antagonist = {
+ key: 'bloodsucker',
+ name: 'Bloodsucker',
+ description: [
+ `
+ After your death, you awaken to see yourself as an undead monster.
+ Use your Vampiric abilities as best you can.
+ Scrape by Space Station 13, or take over it, vassalizing your way.
+ `,
+ ],
+ category: Category.Roundstart,
+};
+
+export default Bloodsucker;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/bloodsuckerbreakout.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/bloodsuckerbreakout.ts
new file mode 100644
index 0000000000000..5a160821872ea
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/bloodsuckerbreakout.ts
@@ -0,0 +1,16 @@
+import { Antagonist, Category } from '../base';
+
+const BloodsuckerBreakout: Antagonist = {
+ key: 'bloodsuckerbreakout',
+ name: 'Bloodsucker (Latejoin)',
+ description: [
+ `
+ After your death, you awaken to see yourself as an undead monster.
+ Use your Vampiric abilities as best you can.
+ Scrape by Space Station 13, or take over it, vassalizing your way.
+ `,
+ ],
+ category: Category.Latejoin,
+};
+
+export default BloodsuckerBreakout;
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/vampiricaccident.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/vampiricaccident.ts
new file mode 100644
index 0000000000000..92efb9dc6b6d4
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/vampiricaccident.ts
@@ -0,0 +1,16 @@
+import { Antagonist, Category } from '../base';
+
+const VampiricAccident: Antagonist = {
+ key: 'vampiricaccident',
+ name: 'Bloodsucker (Midround)',
+ description: [
+ `
+ After your death, you awaken to see yourself as an undead monster.
+ Use your Vampiric abilities as best you can.
+ Scrape by Space Station 13, or take over it, vassalizing your way.
+ `,
+ ],
+ category: Category.Midround,
+};
+
+export default VampiricAccident;