From 5bafb5b9214b6a424e49d32eefc352a094681e3f Mon Sep 17 00:00:00 2001 From: tattax <71668564+tattax@users.noreply.github.com> Date: Mon, 31 Jan 2022 19:33:13 -0300 Subject: [PATCH 01/60] bs and mh above --- code/__DEFINES/antagonists.dm | 6 + code/__DEFINES/atom_hud.dm | 1 + code/__DEFINES/bloodsuckers.dm | 70 ++ code/__DEFINES/melee.dm | 3 + code/__DEFINES/misc.dm | 9 + code/__DEFINES/role_preferences.dm | 9 +- code/__DEFINES/status_effects.dm | 4 + code/__DEFINES/traits.dm | 6 + code/game/atoms.dm | 15 + .../gamemodes/bloodsuckers/bloodsucker.dm | 259 ++++++ .../gamemodes/bloodsuckers/monsterhunter.dm | 84 ++ .../gamemodes/bloodsuckers/traitorsuckers.dm | 63 ++ .../dynamic/dynamic_rulesets_latejoin.dm | 121 +++ .../dynamic/dynamic_rulesets_midround.dm | 44 + .../dynamic/dynamic_rulesets_roundstart.dm | 39 + code/game/gamemodes/objective.dm | 16 + code/game/objects/items/devices/scanners.dm | 4 +- .../items/implants/implant_mindshield.dm | 10 + code/modules/admin/sql_ban_system.dm | 4 +- .../bloodsuckers/bloodsucker_daylight.dm | 172 ++++ .../bloodsuckers/bloodsucker_flaws.dm | 98 +++ .../bloodsuckers/bloodsucker_frenzy.dm | 113 +++ .../bloodsuckers/bloodsucker_integration.dm | 160 ++++ .../bloodsuckers/bloodsucker_objectives.dm | 488 +++++++++++ .../antagonists/bloodsuckers/bloodsuckers.dm | 828 ++++++++++++++++++ .../bloodsuckers/bloodsuckers_objects.dm | 437 +++++++++ .../bloodsuckers/powers/_powers.dm | 211 +++++ .../antagonists/bloodsuckers/powers/brujah.dm | 33 + .../antagonists/bloodsuckers/powers/cloak.dm | 68 ++ .../bloodsuckers/powers/distress.dm | 22 + .../antagonists/bloodsuckers/powers/feed.dm | 367 ++++++++ .../bloodsuckers/powers/fortitude.dm | 82 ++ .../antagonists/bloodsuckers/powers/gohome.dm | 117 +++ .../bloodsuckers/powers/masquerade.dm | 110 +++ .../bloodsuckers/powers/recuperate.dm | 58 ++ .../powers/targeted/_powers_targeted.dm | 98 +++ .../bloodsuckers/powers/targeted/brawn.dm | 191 ++++ .../bloodsuckers/powers/targeted/haste.dm | 99 +++ .../bloodsuckers/powers/targeted/lunge.dm | 143 +++ .../bloodsuckers/powers/targeted/mesmerize.dm | 143 +++ .../bloodsuckers/powers/targeted/trespass.dm | 107 +++ .../powers/tremere/_powers_tremere.dm | 28 + .../bloodsuckers/powers/tremere/auspex.dm | 123 +++ .../bloodsuckers/powers/tremere/dominate.dm | 190 ++++ .../powers/tremere/thaumaturgy.dm | 187 ++++ .../antagonists/bloodsuckers/powers/veil.dm | 131 +++ .../structures/bloodsucker_coffin.dm | 271 ++++++ .../structures/bloodsucker_crypt.dm | 722 +++++++++++++++ .../structures/bloodsucker_life.dm | 443 ++++++++++ .../structures/bloodsucker_recipes.dm | 117 +++ .../antagonists/bloodsuckers/vassal.dm | 205 +++++ .../antagonists/monsterhunter/hunterfu.dm | 223 +++++ .../monsterhunter/monsterhunter.dm | 162 ++++ .../antagonists/monsterhunter/monstertrack.dm | 72 ++ code/modules/events/monsterhunter.dm | 46 + code/modules/language/vampiric.dm | 20 + .../mining/lavaland/necropolis_chests.dm | 3 + code/modules/mob/living/blood.dm | 2 + .../modules/mob/living/carbon/damage_procs.dm | 8 +- .../mob/living/carbon/human/examine.dm | 10 + .../carbon/human/species_types/vampire.dm | 2 +- icons/misc/language.dmi | Bin 6002 -> 6228 bytes icons/mob/actions/actions_bloodsucker.dmi | Bin 0 -> 21201 bytes .../actions/actions_tremere_bloodsucker.dmi | Bin 0 -> 5371 bytes icons/mob/hud.dmi | Bin 15817 -> 16750 bytes icons/mob/inhands/antag/bs_leftinhand.dmi | Bin 0 -> 6850 bytes icons/mob/inhands/antag/bs_rightinhand.dmi | Bin 0 -> 6962 bytes icons/mob/vampiric.dmi | Bin 0 -> 722 bytes icons/obj/crates.dmi | Bin 24651 -> 29192 bytes icons/obj/shields.dmi | Bin 5676 -> 7380 bytes icons/obj/stakes.dmi | Bin 0 -> 866 bytes icons/obj/vamp_obj.dmi | Bin 0 -> 3089 bytes icons/obj/vamp_obj_64.dmi | Bin 0 -> 1109 bytes sound/ambience/antag/bloodsuckeralert.ogg | Bin 0 -> 218645 bytes sound/effects/coffin_close.ogg | Bin 0 -> 59363 bytes sound/effects/coffin_open.ogg | Bin 0 -> 71218 bytes sound/effects/griffin_1.ogg | Bin 0 -> 4117 bytes sound/effects/griffin_10.ogg | Bin 0 -> 7282 bytes sound/effects/griffin_2.ogg | Bin 0 -> 4352 bytes sound/effects/griffin_3.ogg | Bin 0 -> 4912 bytes sound/effects/griffin_4.ogg | Bin 0 -> 5274 bytes sound/effects/griffin_5.ogg | Bin 0 -> 5512 bytes sound/effects/griffin_6.ogg | Bin 0 -> 5895 bytes sound/effects/griffin_7.ogg | Bin 0 -> 6307 bytes sound/effects/griffin_8.ogg | Bin 0 -> 6620 bytes sound/effects/griffin_9.ogg | Bin 0 -> 6913 bytes sound/effects/lunge_warn.ogg | Bin 0 -> 73076 bytes sound/effects/owl_1.ogg | Bin 0 -> 4208 bytes sound/effects/owl_10.oga | Bin 0 -> 7811 bytes sound/effects/owl_2.ogg | Bin 0 -> 4615 bytes sound/effects/owl_3.ogg | Bin 0 -> 5030 bytes sound/effects/owl_5.ogg | Bin 0 -> 5796 bytes sound/effects/owl_6.ogg | Bin 0 -> 6211 bytes sound/effects/owl_7.ogg | Bin 0 -> 6591 bytes sound/effects/owl_8.ogg | Bin 0 -> 7037 bytes sound/effects/owl_9.ogg | Bin 0 -> 7381 bytes tgui/yarn.lock | 12 +- yogstation.dme | 40 + 98 files changed, 7915 insertions(+), 14 deletions(-) create mode 100644 code/__DEFINES/bloodsuckers.dm create mode 100644 code/game/gamemodes/bloodsuckers/bloodsucker.dm create mode 100644 code/game/gamemodes/bloodsuckers/monsterhunter.dm create mode 100644 code/game/gamemodes/bloodsuckers/traitorsuckers.dm create mode 100644 code/modules/antagonists/bloodsuckers/bloodsucker_daylight.dm create mode 100644 code/modules/antagonists/bloodsuckers/bloodsucker_flaws.dm create mode 100644 code/modules/antagonists/bloodsuckers/bloodsucker_frenzy.dm create mode 100644 code/modules/antagonists/bloodsuckers/bloodsucker_integration.dm create mode 100644 code/modules/antagonists/bloodsuckers/bloodsucker_objectives.dm create mode 100644 code/modules/antagonists/bloodsuckers/bloodsuckers.dm create mode 100644 code/modules/antagonists/bloodsuckers/bloodsuckers_objects.dm create mode 100644 code/modules/antagonists/bloodsuckers/powers/_powers.dm create mode 100644 code/modules/antagonists/bloodsuckers/powers/brujah.dm create mode 100644 code/modules/antagonists/bloodsuckers/powers/cloak.dm create mode 100644 code/modules/antagonists/bloodsuckers/powers/distress.dm create mode 100644 code/modules/antagonists/bloodsuckers/powers/feed.dm create mode 100644 code/modules/antagonists/bloodsuckers/powers/fortitude.dm create mode 100644 code/modules/antagonists/bloodsuckers/powers/gohome.dm create mode 100644 code/modules/antagonists/bloodsuckers/powers/masquerade.dm create mode 100644 code/modules/antagonists/bloodsuckers/powers/recuperate.dm create mode 100644 code/modules/antagonists/bloodsuckers/powers/targeted/_powers_targeted.dm create mode 100644 code/modules/antagonists/bloodsuckers/powers/targeted/brawn.dm create mode 100644 code/modules/antagonists/bloodsuckers/powers/targeted/haste.dm create mode 100644 code/modules/antagonists/bloodsuckers/powers/targeted/lunge.dm create mode 100644 code/modules/antagonists/bloodsuckers/powers/targeted/mesmerize.dm create mode 100644 code/modules/antagonists/bloodsuckers/powers/targeted/trespass.dm create mode 100644 code/modules/antagonists/bloodsuckers/powers/tremere/_powers_tremere.dm create mode 100644 code/modules/antagonists/bloodsuckers/powers/tremere/auspex.dm create mode 100644 code/modules/antagonists/bloodsuckers/powers/tremere/dominate.dm create mode 100644 code/modules/antagonists/bloodsuckers/powers/tremere/thaumaturgy.dm create mode 100644 code/modules/antagonists/bloodsuckers/powers/veil.dm create mode 100644 code/modules/antagonists/bloodsuckers/structures/bloodsucker_coffin.dm create mode 100644 code/modules/antagonists/bloodsuckers/structures/bloodsucker_crypt.dm create mode 100644 code/modules/antagonists/bloodsuckers/structures/bloodsucker_life.dm create mode 100644 code/modules/antagonists/bloodsuckers/structures/bloodsucker_recipes.dm create mode 100644 code/modules/antagonists/bloodsuckers/vassal.dm create mode 100644 code/modules/antagonists/monsterhunter/hunterfu.dm create mode 100644 code/modules/antagonists/monsterhunter/monsterhunter.dm create mode 100644 code/modules/antagonists/monsterhunter/monstertrack.dm create mode 100644 code/modules/events/monsterhunter.dm create mode 100644 code/modules/language/vampiric.dm create mode 100644 icons/mob/actions/actions_bloodsucker.dmi create mode 100644 icons/mob/actions/actions_tremere_bloodsucker.dmi create mode 100644 icons/mob/inhands/antag/bs_leftinhand.dmi create mode 100644 icons/mob/inhands/antag/bs_rightinhand.dmi create mode 100644 icons/mob/vampiric.dmi create mode 100644 icons/obj/stakes.dmi create mode 100644 icons/obj/vamp_obj.dmi create mode 100644 icons/obj/vamp_obj_64.dmi create mode 100644 sound/ambience/antag/bloodsuckeralert.ogg create mode 100644 sound/effects/coffin_close.ogg create mode 100644 sound/effects/coffin_open.ogg create mode 100644 sound/effects/griffin_1.ogg create mode 100644 sound/effects/griffin_10.ogg create mode 100644 sound/effects/griffin_2.ogg create mode 100644 sound/effects/griffin_3.ogg create mode 100644 sound/effects/griffin_4.ogg create mode 100644 sound/effects/griffin_5.ogg create mode 100644 sound/effects/griffin_6.ogg create mode 100644 sound/effects/griffin_7.ogg create mode 100644 sound/effects/griffin_8.ogg create mode 100644 sound/effects/griffin_9.ogg create mode 100644 sound/effects/lunge_warn.ogg create mode 100644 sound/effects/owl_1.ogg create mode 100644 sound/effects/owl_10.oga create mode 100644 sound/effects/owl_2.ogg create mode 100644 sound/effects/owl_3.ogg create mode 100644 sound/effects/owl_5.ogg create mode 100644 sound/effects/owl_6.ogg create mode 100644 sound/effects/owl_7.ogg create mode 100644 sound/effects/owl_8.ogg create mode 100644 sound/effects/owl_9.ogg diff --git a/code/__DEFINES/antagonists.dm b/code/__DEFINES/antagonists.dm index 850d0312230a..b86c64771bb0 100644 --- a/code/__DEFINES/antagonists.dm +++ b/code/__DEFINES/antagonists.dm @@ -101,3 +101,9 @@ #define TIER_BLADE 5 #define TIER_3 6 #define TIER_ASCEND 7 + +//Bloodsuckers +#define IS_BLOODSUCKER(mob) (mob?.mind?.has_antag_datum(/datum/antagonist/bloodsucker)) +#define IS_VASSAL(mob) (mob?.mind?.has_antag_datum(/datum/antagonist/vassal)) +#define IS_MONSTERHUNTER(mob) (mob?.mind?.has_antag_datum(/datum/antagonist/monsterhunter)) + diff --git a/code/__DEFINES/atom_hud.dm b/code/__DEFINES/atom_hud.dm index 49137db46e99..2838bf95784d 100644 --- a/code/__DEFINES/atom_hud.dm +++ b/code/__DEFINES/atom_hud.dm @@ -84,6 +84,7 @@ #define ANTAG_HUD_HERETIC 29 #define ANTAG_HUD_MINDSLAVE 30 #define ANTAG_HUD_ZOMBIE 31 +#define ANTAG_HUD_BLOODSUCKER 32 // Notification action types #define NOTIFY_JUMP "jump" diff --git a/code/__DEFINES/bloodsuckers.dm b/code/__DEFINES/bloodsuckers.dm new file mode 100644 index 000000000000..238b921542f7 --- /dev/null +++ b/code/__DEFINES/bloodsuckers.dm @@ -0,0 +1,70 @@ +/** + * Bloodsucker defines + */ +/// Determines Bloodsucker regeneration rate +#define BS_BLOOD_VOLUME_MAX_REGEN 700 +/// Cost to torture someone, in blood +#define TORTURE_BLOOD_COST "15" +/// Cost to convert someone after successful torture, in blood +#define TORTURE_CONVERSION_COST "50" +/// Deals with constant processes off of LifeTick() +#define COMSIG_LIVING_BIOLOGICAL_LIFE "biological_life" +/// Once blood is this low, will enter Frenzy +#define FRENZY_THRESHOLD_ENTER 25 +/// Once blood is this high, will exit Frenzy +#define FRENZY_THRESHOLD_EXIT 250 +/// You have special interactions with Bloodsuckers +#define TRAIT_BLOODSUCKER_HUNTER "bloodsucker_hunter" + +/** + * 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 messages. +#define BLOODSUCKER_SPAM_SOL (30 SECONDS) + +/** + * Clan defines + */ +#define CLAN_BRUJAH "Brujah Clan" +#define CLAN_NOSFERATU "Nosferatu Clan" +#define CLAN_TREMERE "Tremere Clan" +#define CLAN_VENTRUE "Ventrue Clan" +#define CLAN_MALKAVIAN "Malkavian Clan" +// Flavortext-only clans +#define CLAN_TOREADOR "Toreador Clan" +#define CLAN_GANGREL "Gangrel Clan" + +/** + * 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 unless you're part of Brujah +#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 be purchased by Bloodsuckers +#define BLOODSUCKER_CAN_BUY (1<<0) +/// This Power can be purchased by Tremere Bloodsuckers +#define TREMERE_CAN_BUY (1<<1) +/// This Power can be purchased by Vassals +#define VASSAL_CAN_BUY (1<<2) +/// This Power can be purchased by Monster Hunters +#define HUNTER_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) diff --git a/code/__DEFINES/melee.dm b/code/__DEFINES/melee.dm index 59fcab1988b2..79a42444763f 100644 --- a/code/__DEFINES/melee.dm +++ b/code/__DEFINES/melee.dm @@ -10,6 +10,9 @@ #define MARTIALART_CQC_COOK "CQC cook" #define MARTIALART_PLASMAFIST "plasma fist" #define MARTIALART_FLYINGFANG "flying fang" +#define MARTIALART_HUNTERFU "hunterfu" +#define MARTIALART_FRENZYGRAB "frenzy grabbing" + //Weapon stat defines #define SWING_SPEED "swing_speed" diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm index 8ddf1db6f09e..f3ccef9ccd9c 100644 --- a/code/__DEFINES/misc.dm +++ b/code/__DEFINES/misc.dm @@ -488,3 +488,12 @@ GLOBAL_LIST_INIT(pda_styles, list(MONO, VT, ORBITRON, SHARE)) #define ALIGNMENT_GOOD "good" #define ALIGNMENT_NEUT "neutral" #define ALIGNMENT_EVIL "evil" + +/// Whether we have succesfully hidden out blood level +#define BLOODSUCKER_HIDE_BLOOD "hide_blood_volume" +/// 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" \ No newline at end of file diff --git a/code/__DEFINES/role_preferences.dm b/code/__DEFINES/role_preferences.dm index 00dc098a3d34..ad0ce241a368 100644 --- a/code/__DEFINES/role_preferences.dm +++ b/code/__DEFINES/role_preferences.dm @@ -46,7 +46,10 @@ #define ROLE_HOLOPARASITE "Holoparasite" // Yogs #define ROLE_INFILTRATOR "Infiltrator" // Yogs #define ROLE_ZOMBIE "Zombie" - +#define ROLE_BLOODSUCKER "Bloodsucker" +#define ROLE_VAMPIRICACCIDENT "Vampiric Accident" +#define ROLE_BLOODSUCKERBREAKOUT "Bloodsucker Breakout" +#define ROLE_MONSTERHUNTER "Monster Hunter" //Missing assignment means it's not a gamemode specific role, IT'S NOT A BUG OR ERROR. //The gamemode specific ones are just so the gamemodes can query whether a player is old enough @@ -83,7 +86,9 @@ GLOBAL_LIST_INIT(special_roles, list( ROLE_DARKSPAWN = /datum/game_mode/darkspawn, ROLE_SENTIENCE, ROLE_ZOMBIE = /datum/game_mode/zombie, - ROLE_FUGITIVE + ROLE_FUGITIVE, + ROLE_BLOODSUCKER = /datum/game_mode/bloodsucker, + ROLE_MONSTERHUNTER )) //Job defines for what happens when you fail to qualify for any job during job selection diff --git a/code/__DEFINES/status_effects.dm b/code/__DEFINES/status_effects.dm index c4c49d5e48ba..0ae9d51feb9d 100644 --- a/code/__DEFINES/status_effects.dm +++ b/code/__DEFINES/status_effects.dm @@ -46,6 +46,8 @@ #define STATUS_EFFECT_DETERMINED /datum/status_effect/determined //currently in a combat high from being seriously wounded +#define STATUS_EFFECT_FRENZY /datum/status_effect/frenzy //Makes you fast and stronger + ///////////// // DEBUFFS // ///////////// @@ -153,6 +155,8 @@ #define STATUS_EFFECT_PROGENITORCURSE /datum/status_effect/progenitor_curse +#define STATUS_EFFECT_MASQUERADE /datum/status_effect/masquerade + ///////////// // SLIME // ///////////// diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index c49127608823..e62e3bbdfd56 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -163,6 +163,7 @@ #define TRAIT_EASILY_WOUNDED "easy_limb_wound" #define TRAIT_HARDLY_WOUNDED "hard_limb_wound" #define TRAIT_TOXINLOVER "toxinlover" +#define TRAIT_TOXIMMUNE "toxin_immune" #define TRAIT_NOBREATH "no_breath" #define TRAIT_ANTIMAGIC "anti_magic" #define TRAIT_HOLY "holy" @@ -216,6 +217,9 @@ #define TRAIT_NO_PASSIVE_HEATING "no-passive-heating" #define TRAIT_BLOODY_MESS "bloody_mess" //from heparin, makes open bleeding wounds rapidly spill more blood #define TRAIT_COAGULATING "coagulating" //from coagulant reagents, this doesn't affect the bleeding itself but does affect the bleed warning messages +#define TRAIT_NOPULSE "nopulse" // Your heart doesn't beat +#define TRAIT_MASQUERADE "masquerade" // Falsifies Health analyzer blood levels +#define TRAIT_COLDBLOODED "coldblooded" // Your body is literal room temperature. Does not make you immune to the temp //non-mob traits /// Used for limb-based paralysis, where replacing the limb will fix it. @@ -319,6 +323,8 @@ #define GUARDIAN_TRAIT "guardian_trait" #define RANDOM_BLACKOUTS "random_blackouts" #define MADE_UNCLONEABLE "made-uncloneable" +#define BLOODSUCKER_TRAIT "bloodsucker_trait" +#define FRENZY_TRAIT "frenzy_trait" ///Traits given by station traits #define STATION_TRAIT_BANANIUM_SHIPMENTS "station_trait_bananium_shipments" diff --git a/code/game/atoms.dm b/code/game/atoms.dm index e0a02c9fe4f6..fe759c79a0b1 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -1290,3 +1290,18 @@ var/mouseparams = list2params(paramslist) usr_client.Click(src, loc, null, mouseparams) return TRUE + +/proc/is_source_facing_target(atom/source,atom/target) + if(!istype(source) || !istype(target)) + return FALSE + if(isliving(source)) + var/mob/living/source_mob = source + if(source_mob.mobility_flags & ~MOBILITY_STAND) + return FALSE + var/goal_dir = get_dir(source, target) + var/clockwise_source_dir = turn(source.dir, -45) + var/anticlockwise_source_dir = turn(source.dir, 45) + + if(source.dir == goal_dir || clockwise_source_dir == goal_dir || anticlockwise_source_dir == goal_dir) + return TRUE + return FALSE \ No newline at end of file diff --git a/code/game/gamemodes/bloodsuckers/bloodsucker.dm b/code/game/gamemodes/bloodsuckers/bloodsucker.dm new file mode 100644 index 000000000000..3cb131e73c62 --- /dev/null +++ b/code/game/gamemodes/bloodsuckers/bloodsucker.dm @@ -0,0 +1,259 @@ +/datum/game_mode + var/list/datum/mind/bloodsuckers = list() + var/list/datum/mind/vassals = list() // List of minds that have been turned into Vassals. + var/list/datum/mind/monsterhunter = list() // List of all Monster Hunters + var/obj/effect/sunlight/bloodsucker_sunlight // Sunlight Timer. Created on first Bloodsucker assign. Destroyed on last removed Bloodsucker. + // The antags you're allowed to be if turning Vassal: + var/list/vassal_allowed_antags = list(/datum/antagonist/brother, /datum/antagonist/traitor, /datum/antagonist/traitor/internal_affairs, /datum/antagonist/nukeop/lone, // Regular ops are bonded by team + /datum/antagonist/fugitive, /datum/antagonist/fugitive_hunter, /datum/antagonist/gang, /datum/antagonist/rev, + /datum/antagonist/pirate, /datum/antagonist/ert, /datum/antagonist/abductee, /datum/antagonist/valentine + ) + +/proc/AmBloodsucker(mob/living/M, falseIfInDisguise = FALSE) + if(!M.mind) + return FALSE + if(!M.mind.has_antag_datum(/datum/antagonist/bloodsucker)) + return FALSE + return TRUE + +/datum/game_mode/bloodsucker + name = "bloodsucker" + config_tag = "bloodsucker" + report_type = "Bloodsucker" + antag_flag = ROLE_BLOODSUCKER + false_report_weight = 10 + restricted_jobs = list("AI", "Cyborg") + protected_jobs = list("Captain", "Head of Personnel", "Head of Security", "Research Director", "Chief Engineer", "Chief Medical Officer", "Quartermaster", "Warden", "Security Officer", "Detective", "Brig Physician", "Deputy",) + required_players = 0 + required_enemies = 1 + recommended_enemies = 4 + reroll_friendly = 1 + round_ends_with_antag_death = FALSE + + announce_span = "greem" + announce_text = "Filthy, bloodsucking vampires are crawling around disguised as crewmembers!\n\ + Bloodsuckers: Claim a coffin and grow strength, turn the crew into your slaves.\n\ + Crew: Put an end to the undead menace and resist their brainwashing!" + +/datum/game_mode/bloodsucker/pre_setup() + + if(CONFIG_GET(flag/protect_roles_from_antagonist)) + restricted_jobs += protected_jobs + + if(CONFIG_GET(flag/protect_assistant_from_antagonist)) + restricted_jobs += "Assistant" + + recommended_enemies = clamp(round(num_players()/10), 1, 6); + + for(var/i = 0, i < recommended_enemies, i++) + if(!antag_candidates.len) + break + var/datum/mind/bloodsucker = pick(antag_candidates) + // Can we even BE a bloodsucker? + //if(can_make_bloodsucker(bloodsucker, display_warning=FALSE)) + bloodsuckers += bloodsucker + bloodsucker.restricted_roles = restricted_jobs + log_game("[bloodsucker.key] (ckey) has been selected as a Bloodsucker.") + antag_candidates.Remove(bloodsucker) // Apparently you can also write antag_candidates -= bloodsucker + + // Do we have enough vamps to continue? + return bloodsuckers.len >= required_enemies + +/datum/game_mode/bloodsucker/post_setup() + // Vamps + for(var/datum/mind/bloodsucker in bloodsuckers) + if(!make_bloodsucker(bloodsucker)) + bloodsuckers -= bloodsucker + ..() + +/// Init Sunlight, called from datum_bloodsucker.on_gain(), in case game mode isn't even Bloodsucker +/datum/game_mode/proc/check_start_sunlight() + // Already Sunlight (and not about to cancel) + if(istype(bloodsucker_sunlight)) + return + bloodsucker_sunlight = new() + +/// End Sun (If you're the last) +/datum/game_mode/proc/check_cancel_sunlight() + // No Sunlight + if(!istype(bloodsucker_sunlight)) + return + if(bloodsuckers.len <= 0) + qdel(bloodsucker_sunlight) + bloodsucker_sunlight = null + +/datum/game_mode/proc/is_daylight() + return istype(bloodsucker_sunlight) && bloodsucker_sunlight.amDay + +/datum/game_mode/bloodsucker/generate_report() + return "There's been a report of the undead roaming around the sector, especially those that display Vampiric abilities.\ + They've displayed the ability to disguise themselves as anyone and brainwash the minds of people they capture alive.\ + Please take care of the crew and their health, as it is impossible to tell if one is lurking in the darkness behind." + +/datum/game_mode/bloodsucker/make_antag_chance(mob/living/carbon/human/character) //Assigns changeling to latejoiners + var/bloodsuckercap = min(round(GLOB.joined_player_list.len / (3 * 4)) + 2, round(GLOB.joined_player_list.len / 2)) + if(bloodsuckers.len >= bloodsuckercap) //Caps number of latejoin antagonists + return + if(bloodsuckers.len <= (bloodsuckercap - 2) || prob(100 - (3 * 2))) + if(ROLE_BLOODSUCKER in character.client.prefs.be_special) + if(!is_banned_from(character.ckey, list(ROLE_BLOODSUCKER, ROLE_SYNDICATE)) && !QDELETED(character)) + if(age_check(character.client)) + if(!(character.job in restricted_jobs)) + character.mind.make_bloodsucker() + bloodsuckers += character.mind + +////////////////////////////////////////////////////////////////////////////// + +////////////////////////////////////////////// +// // +// ROUNDSTART BLOODSUCKER // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/roundstart/bloodsucker + name = "Bloodsuckers" + antag_flag = ROLE_BLOODSUCKER + antag_datum = /datum/antagonist/bloodsucker + protected_roles = list("Captain", "Head of Personnel", "Head of Security", "Research Director", "Chief Engineer", "Chief Medical Officer", "Quartermaster", "Warden", "Security Officer", "Detective", "Brig Physician", "Deputy",) + restricted_roles = list("AI", "Cyborg") + required_candidates = 1 + weight = 3 + cost = 10 + scaling_cost = 15 + requirements = list(70,70,60,50,40,20,20,10,10,10) + antag_cap = list(1,1,1,1,1,2,2,2,2,3) + +/datum/dynamic_ruleset/roundstart/bloodsucker/pre_execute() + . = ..() + var/num_bloodsuckers = antag_cap[indice_pop] * (scaled_times + 1) + for (var/i = 1 to num_bloodsuckers) + var/mob/M = pick_n_take(candidates) + assigned += M.mind + M.mind.restricted_roles = restricted_roles + M.mind.special_role = ROLE_BLOODSUCKER + return TRUE + +/datum/dynamic_ruleset/roundstart/bloodsucker/execute() + for(var/datum/mind/bloodsucker in assigned) + var/datum/antagonist/bloodsucker/new_antag = new antag_datum() + bloodsucker.add_antag_datum(new_antag) + return TRUE + +/* + * DON'T ADD DYNAMIC MIDROUNDS TO BLOODSUCKERS + * By the time they'll spawn, they'd have missed several Sol's, and Security would likely be geared up. + * -Willard + */ + +////////////////////////////////////////////////////////////////////////////// + +/// Creator is just here so we can display fail messages to whoever is turning us. +/datum/game_mode/proc/can_make_bloodsucker(datum/mind/bloodsucker, datum/mind/creator) + // Species Must have a HEART (Sorry Plasmamen) + var/mob/living/carbon/human/H = bloodsucker.current + if(NOBLOOD in H.dna.species.species_traits) + to_chat(creator, "[bloodsucker]'s DNA isn't compatible!") + return FALSE + // Already a Non-Human Antag + if(bloodsucker.has_antag_datum(/datum/antagonist/abductor) || bloodsucker.has_antag_datum(/datum/antagonist/changeling)) + return FALSE + // Already a vamp + if(bloodsucker.has_antag_datum(/datum/antagonist/bloodsucker)) + to_chat(creator, "[bloodsucker] is already a Bloodsucker!") + return FALSE + return TRUE + +/datum/game_mode/proc/can_make_vassal(mob/living/target, datum/mind/creator, display_warning = TRUE)//, check_antag_or_loyal=FALSE) + // Not Correct Type: Abort + if(!iscarbon(target) || !creator) + return FALSE + if(target.stat > UNCONSCIOUS) + return FALSE + // No Mind! + if(!target.mind || !target.mind.key) + if(display_warning) + to_chat(creator, "[target] isn't self-aware enough to be made into a Vassal.") + return FALSE + // Already MY Vassal + var/datum/antagonist/vassal/V = target.mind.has_antag_datum(/datum/antagonist/bloodsucker) + if(istype(V) && V.master) + if(V.master.owner == creator) + if(display_warning) + to_chat(creator, "[target] is already your loyal Vassal!") + else + if(display_warning) + to_chat(creator, "[target] is the loyal Vassal of another Bloodsucker!") + return FALSE + // Already Antag or Loyal (Vamp Hunters count as antags) + if(target.mind.enslaved_to || AmInvalidAntag(target.mind)) //!VassalCheckAntagValid(target.mind, check_antag_or_loyal)) // HAS_TRAIT(target, TRAIT_MINDSHIELD, "implant") || + if(display_warning) + to_chat(creator, "[target] resists the power of your blood to dominate their mind!") + return FALSE + return TRUE + +/// NOTE: This is a game_mode/proc, NOT a game_mode/bloodsucker/proc! We need to access this function despite the game mode. +/datum/game_mode/proc/make_bloodsucker(datum/mind/bloodsucker, datum/mind/creator = null) + if(!can_make_bloodsucker(bloodsucker)) + return FALSE + // Create Datum: Fledgling + var/datum/antagonist/bloodsucker/A + // [FLEDGLING] + if(creator) + A = new (bloodsucker) + bloodsucker.add_antag_datum(A) + // Log + message_admins("[bloodsucker] has become a Bloodsucker, and was created by [creator].") + log_admin("[bloodsucker] has become a Bloodsucker, and was created by [creator].") + // [MASTER] + else + A = bloodsucker.add_antag_datum(/datum/antagonist/bloodsucker) + return TRUE + +/// Mind version +/datum/mind/proc/make_bloodsucker() + var/datum/antagonist/bloodsucker/C = has_antag_datum(/datum/antagonist/bloodsucker) + if(!C) + C = add_antag_datum(/datum/antagonist/bloodsucker) + special_role = ROLE_BLOODSUCKER + return C + +/datum/mind/proc/remove_bloodsucker() + var/datum/antagonist/bloodsucker/C = has_antag_datum(/datum/antagonist/bloodsucker) + if(C) + remove_antag_datum(/datum/antagonist/bloodsucker) + special_role = null + +/datum/game_mode/proc/AmValidAntag(datum/mind/M) + // No List? + if(!islist(M.antag_datums) || M.antag_datums.len == 0) + return FALSE + // Am I NOT an invalid Antag? NOTE: We already excluded non-antags above. Don't worry about the "No List?" check in AmInvalidIntag() + return !AmInvalidAntag(M) + +/datum/game_mode/proc/AmInvalidAntag(datum/mind/M) + // No List? + if(!islist(M.antag_datums) || M.antag_datums.len == 0) + return FALSE + // Does even ONE antag appear in this mind that isn't in the list? Then FAIL! + for(var/datum/antagonist/antag_datum in M.antag_datums) + if(!(antag_datum.type in vassal_allowed_antags)) // vassal_allowed_antags is a list stored in the game mode, above. + //message_admins("DEBUG VASSAL: Found Invalid: [antag_datum] // [antag_datum.type]") + return TRUE + //message_admins("DEBUG VASSAL: Valid Antags! (total of [M.antag_datums.len])") + // WHEN YOU DELETE THE ABOVE: Remove the 3 second timer on converting the vassal too. + return FALSE + +/datum/game_mode/proc/make_vassal(var/mob/living/target, var/datum/mind/creator) + if(!can_make_vassal(target, creator)) + return FALSE + // Make Vassal + var/datum/antagonist/vassal/V = new(target.mind) + var/datum/antagonist/bloodsucker/B = creator.has_antag_datum(/datum/antagonist/bloodsucker) + V.master = B + target.mind.add_antag_datum(V, V.master.get_team()) + // Update Bloodsucker Title + B.SelectTitle(am_fledgling = FALSE) // Only works if you have no title yet. + // Log it + message_admins("[target] has become a Vassal, and is enslaved to [creator].") + log_admin("[target] has become a Vassal, and is enslaved to [creator].") + return TRUE \ No newline at end of file diff --git a/code/game/gamemodes/bloodsuckers/monsterhunter.dm b/code/game/gamemodes/bloodsuckers/monsterhunter.dm new file mode 100644 index 000000000000..dd63ad84ad31 --- /dev/null +++ b/code/game/gamemodes/bloodsuckers/monsterhunter.dm @@ -0,0 +1,84 @@ +/* + * MONSTER HUNTERS: + * Their job is to hunt Monsters. + * They spawn by default 35 minutes into a Bloodsucker round, + * They also randomly spawn in other rounds, as some unique flavor. + * They can also be used as Admin-only antags during rounds such as; + * - Changeling murderboning rounds + * - Lategame Cult round + * - Ect. + */ + +/// The default, for Bloodsucker rounds. +/datum/round_event_control/bloodsucker_hunters + name = "Spawn Monster Hunter - Bloodsucker" + typepath = /datum/round_event/bloodsucker_hunters + max_occurrences = 1 // We have to see how Bloodsuckers are in game to decide if having more than 1 is beneficial. + weight = 2000 + min_players = 10 + earliest_start = 35 MINUTES + alert_observers = FALSE + gamemode_whitelist = list("bloodsucker", "traitorsucker") + +/datum/round_event/bloodsucker_hunters + fakeable = FALSE + +/datum/round_event/bloodsucker_hunters/start() + for(var/mob/living/carbon/human/H in shuffle(GLOB.player_list)) + if(!H.client || !(ROLE_MONSTERHUNTER in H.client.prefs.be_special)) + continue + if(H.stat == DEAD) + continue + if(!SSjob.GetJob(H.mind.assigned_role) || (H.mind.assigned_role in GLOB.nonhuman_positions)) // Only crewmembers on-station. + continue + if(!SSjob.GetJob(H.mind.assigned_role) || (H.mind.assigned_role in GLOB.command_positions)) + continue + if(!SSjob.GetJob(H.mind.assigned_role) || (H.mind.assigned_role in GLOB.security_positions)) + continue + if(H.mind.has_antag_datum(/datum/antagonist/vassal)) + continue + if(H.mind.has_antag_datum(/datum/antagonist/bloodsucker)) + continue + if(!H.getorgan(/obj/item/organ/brain)) + continue + H.mind.add_antag_datum(/datum/antagonist/monsterhunter) + message_admins("BLOODSUCKER NOTICE: [H] has awoken as a Monster Hunter.") + break + +/// Randomly spawned Monster hunters during TraitorChangeling, Changeling, Heretic and Cult rounds. +/datum/round_event_control/monster_hunters + name = "Spawn Monster Hunter - Misc" + typepath = /datum/round_event/monster_hunters + max_occurrences = 1 + weight = 7 + min_players = 10 + earliest_start = 25 MINUTES + alert_observers = FALSE + gamemode_whitelist = list("traitorchan","changeling","heresy","cult") + +/datum/round_event/monster_hunters + fakeable = FALSE + +/datum/round_event/monster_hunters/start() + for(var/mob/living/carbon/human/H in shuffle(GLOB.player_list)) + if(!H.client || !(ROLE_MONSTERHUNTER in H.client.prefs.be_special)) + continue + if(H.stat == DEAD) + continue + if(!SSjob.GetJob(H.mind.assigned_role) || (H.mind.assigned_role in GLOB.nonhuman_positions)) + continue + if(!SSjob.GetJob(H.mind.assigned_role) || (H.mind.assigned_role in GLOB.command_positions)) + continue + if(!SSjob.GetJob(H.mind.assigned_role) || (H.mind.assigned_role in GLOB.security_positions)) + continue + if(H.mind.has_antag_datum(/datum/antagonist/changeling)) + continue + if(H.mind.has_antag_datum(/datum/antagonist/heretic)) + continue + if(H.mind.has_antag_datum(/datum/antagonist/cult)) + continue + if(!H.getorgan(/obj/item/organ/brain)) + continue + H.mind.add_antag_datum(/datum/antagonist/monsterhunter) + message_admins("MONSTERHUNTER NOTICE: [H] has awoken as a Monster Hunter.") + break \ No newline at end of file diff --git a/code/game/gamemodes/bloodsuckers/traitorsuckers.dm b/code/game/gamemodes/bloodsuckers/traitorsuckers.dm new file mode 100644 index 000000000000..1c2aa9857745 --- /dev/null +++ b/code/game/gamemodes/bloodsuckers/traitorsuckers.dm @@ -0,0 +1,63 @@ +/datum/game_mode/traitor/bloodsucker + name = "traitor+bloodsucker" + config_tag = "traitorsucker" + report_type = "traitorsucker" + false_report_weight = 10 + traitors_possible = 3 // Hard limit on Traitors if scaling is turned off + restricted_jobs = list("AI", "Cyborg") + protected_jobs = list("Captain", "Head of Personnel", "Head of Security", "Research Director", "Chief Engineer", "Chief Medical Officer", "Quartermaster", "Warden", "Security Officer", "Detective", "Brig Physician", "Deputy",) + required_players = 20 + required_enemies = 1 // How many of each type are required + recommended_enemies = 2 + reroll_friendly = 1 + announce_span = "Traitors and Bloodsuckers" + announce_text = "There are vampiric monsters on the station along with some syndicate operatives out for their own gain! Do not let the bloodsuckers or the traitors succeed!" + + var/list/possible_bloodsuckers = list() + var/const/bloodsucker_amount = 2 + +/datum/game_mode/traitor/bloodsucker/can_start() + . = ..() + if(!.) + return + possible_bloodsuckers = get_players_for_role(ROLE_BLOODSUCKER) + if(possible_bloodsuckers.len < required_enemies) + return FALSE + return TRUE + +/datum/game_mode/traitor/bloodsucker/pre_setup() + if(CONFIG_GET(flag/protect_roles_from_antagonist)) + restricted_jobs += protected_jobs + + if(CONFIG_GET(flag/protect_assistant_from_antagonist)) + restricted_jobs += "Assistant" + + var/list/datum/mind/possible_bloodsuckers = get_players_for_role(ROLE_BLOODSUCKER) + + var/num_bloodsuckers = 1 + num_bloodsuckers = max(1, min(num_players(), bloodsucker_amount/2)) + + if(possible_bloodsuckers.len>0) + for(var/j = 0, j < num_bloodsuckers, j++) + if(!possible_bloodsuckers.len) + break + var/datum/mind/bloodsucker = antag_pick(possible_bloodsuckers) + antag_candidates -= bloodsucker + possible_bloodsuckers -= bloodsucker + bloodsucker.special_role = ROLE_BLOODSUCKER + bloodsuckers += bloodsucker + bloodsucker.restricted_roles = restricted_jobs + . = ..() + return + else + return FALSE + +/datum/game_mode/traitor/bloodsucker/post_setup() + for(var/datum/mind/bloodsucker in bloodsuckers) + bloodsucker.add_antag_datum(/datum/antagonist/bloodsucker) + return ..() + +/datum/game_mode/traitor/bloodsucker/generate_report() + return "There's been a report of monsters roaming around with Vampiric abilities.\ + Nanotrasen believes it is entirely possible that said monsters have been sent by the Syndicate.\ + Please take care of the crew and their health, as it is impossible to tell if one is nearby." \ No newline at end of file diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm index 876f04442694..ce779ea94b5f 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm @@ -221,3 +221,124 @@ requirements = list(45,40,30,30,20,20,15,10,10,10) minimum_players = 36 repeatable = TRUE + +////////////////////////////////////////////// +// // +// BLOODSUCKER // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/latejoin/bloodsucker + name = "Bloodsucker Breakout" + antag_datum = /datum/antagonist/bloodsucker + antag_flag = ROLE_BLOODSUCKERBREAKOUT + antag_flag_override = ROLE_BLOODSUCKER + protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Research Director", "Chief Engineer", "Chief Medical Officer", "Brig Physician", "Curator") + restricted_roles = list("AI","Cyborg") + required_candidates = 1 + weight = 5 + cost = 10 + requirements = list(10,10,10,10,10,10,10,10,10,10) + repeatable = FALSE + +/datum/dynamic_ruleset/latejoin/bloodsucker/execute() + var/mob/latejoiner = pick(candidates) // This should contain a single player, but in case. + assigned += latejoiner.mind + + for(var/selected_player in assigned) + var/datum/mind/bloodsuckermind = selected_player + var/datum/antagonist/bloodsucker/sucker = new + if(!bloodsuckermind.make_bloodsucker(selected_player)) + assigned -= selected_player + message_admins("[ADMIN_LOOKUPFLW(selected_player)] was selected by the [name] ruleset, but couldn't be made into a Bloodsucker.") + return FALSE + sucker.bloodsucker_level_unspent = rand(2,3) + message_admins("[ADMIN_LOOKUPFLW(selected_player)] was selected by the [name] ruleset and has been made into a midround Bloodsucker.") + log_game("DYNAMIC: [key_name(selected_player)] was selected by the [name] ruleset and has been made into a midround Bloodsucker.") + return TRUE + + +////////////////////////////////////////////////////////////////////////////// + +/* + * # Assigning Bloodsucker status + * + * Here we assign the Bloodsuckers themselves, ensuring they arent Plasmamen + * Also deals with Vassalization status. + */ + +/datum/mind/proc/can_make_bloodsucker(datum/mind/convertee, datum/mind/converter) + // Species Must have a HEART (Sorry Plasmamen) + var/mob/living/carbon/human/user = convertee.current + if(!(user.dna?.species) || !(user.mob_biotypes & MOB_ORGANIC)) + if(converter) + to_chat(converter, span_danger("[convertee]'s DNA isn't compatible!")) + return FALSE + // Check for Fledgeling + if(converter) + message_admins("[convertee] has become a Bloodsucker, and was created by [converter].") + log_admin("[convertee] has become a Bloodsucker, and was created by [converter].") + return TRUE + +/datum/antagonist/bloodsucker/proc/can_make_vassal(mob/living/converted, datum/mind/converter, can_vassal_sleeping = FALSE)//, check_antag_or_loyal=FALSE) + // Not Correct Type: Abort + if(!iscarbon(converted) || !converter) + return FALSE + if(converted.stat > UNCONSCIOUS && !can_vassal_sleeping) + return FALSE + // No Mind! + if(!converted.mind) + to_chat(converter, span_danger("[converted] isn't self-aware enough to be made into a Vassal.")) + return FALSE + // Already MY Vassal + var/datum/antagonist/vassal/vassaldatum = converted.mind.has_antag_datum(/datum/antagonist/bloodsucker) + if(istype(vassaldatum) && vassaldatum.master) + if(vassaldatum.master.owner == converter) + to_chat(converter, span_danger("[converted] is already your loyal Vassal!")) + else + to_chat(converter, span_danger("[converted] is the loyal Vassal of another Bloodsucker!")) + return FALSE + // Already Antag or Loyal (Vamp Hunters count as antags) + if(!isnull(converted.mind.enslaved_to) || AmInvalidAntag(converted)) + to_chat(converter, span_danger("[converted] resists the power of your blood to dominate their mind!")) + return FALSE + return TRUE + +/datum/antagonist/bloodsucker/proc/AmValidAntag(mob/target) + /// Check if they are an antag, if so, check if they're Invalid. + if(target.mind?.special_role || !isnull(target.mind?.antag_datums)) + return !AmInvalidAntag(target) + /// Otherwise, just cancel out. + return FALSE + +/datum/antagonist/bloodsucker/proc/AmInvalidAntag(mob/target) + /// Not an antag? + if(!is_special_character(target)) + return FALSE + /// Checks if the person is an antag banned from being vassalized, stored in bloodsucker's datum. + for(var/datum/antagonist/antag_datum in target.mind.antag_datums) + if(antag_datum.type in vassal_banned_antags) + //message_admins("DEBUG VASSAL: Found Invalid: [antag_datum] // [antag_datum.type]") + return TRUE +// message_admins("DEBUG VASSAL: Valid Antags! (total of [target.antag_datums.len])") + // WHEN YOU DELETE THE ABOVE: Remove the 3 second timer on converting the vassal too. + return FALSE + +/datum/antagonist/bloodsucker/proc/attempt_turn_vassal(mob/living/carbon/convertee, can_vassal_sleeping = FALSE) + convertee.silent = 0 + return make_vassal(convertee, owner, can_vassal_sleeping) + +/datum/antagonist/bloodsucker/proc/make_vassal(mob/living/convertee, datum/mind/converter, sleeping = FALSE) + if(!can_make_vassal(convertee, converter, can_vassal_sleeping = sleeping)) + return FALSE + // Make Vassal + var/datum/antagonist/vassal/vassaldatum = new(convertee.mind) + var/datum/antagonist/bloodsucker/bloodsuckerdatum = converter.has_antag_datum(/datum/antagonist/bloodsucker) + vassaldatum.master = bloodsuckerdatum + convertee.mind.add_antag_datum(vassaldatum, vassaldatum.master.get_team()) + // Update Bloodsucker Title + bloodsuckerdatum.SelectTitle(am_fledgling = FALSE) // Only works if you have no title yet. + // Log it + message_admins("[convertee] has become a Vassal, and is enslaved to [converter].") + log_admin("[convertee] has become a Vassal, and is enslaved to [converter].") + return TRUE \ No newline at end of file diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm index 4af19660c245..e44967087396 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm @@ -602,3 +602,47 @@ new_character.mind.add_antag_datum(new_role, new_team) #undef ABDUCTOR_MAX_TEAMS + +////////////////////////////////////////////// +// // +// BLOODSUCKER // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/midround/bloodsucker + name = "Vampiric Accident" + antag_datum = /datum/antagonist/bloodsucker + antag_flag = ROLE_VAMPIRICACCIDENT + antag_flag_override = ROLE_BLOODSUCKER + protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Research Director", "Chief Engineer", "Chief Medical Officer", "Brig Physician", "Curator") + restricted_roles = list("AI","Cyborg", "Positronic Brain") + required_candidates = 1 + weight = 5 + cost = 10 + requirements = list(40,30,20,10,10,10,10,10,10,10) + repeatable = FALSE + +/datum/dynamic_ruleset/midround/bloodsucker/trim_candidates() + . = ..() + for(var/mob/living/player in living_players) + if(issilicon(player)) // Your assigned role doesn't change when you are turned into a silicon. + living_players -= player + else if(is_centcom_level(player.z)) + living_players -= player // We don't allow people in CentCom + else if(player.mind && (player.mind.special_role || player.mind.antag_datums?.len > 0)) + living_players -= player // We don't allow people with roles already + +/datum/dynamic_ruleset/midround/bloodsucker/execute() + var/mob/selected_mobs = pick(living_players) + assigned += selected_mobs + living_players -= selected_mobs + var/datum/mind/bloodsuckermind = selected_mobs + var/datum/antagonist/bloodsucker/sucker = new + if(!bloodsuckermind.make_bloodsucker(selected_mobs)) + assigned -= selected_mobs + message_admins("[ADMIN_LOOKUPFLW(selected_mobs)] was selected by the [name] ruleset, but couldn't be made into a Bloodsucker.") + return FALSE + sucker.bloodsucker_level_unspent = rand(2,3) + message_admins("[ADMIN_LOOKUPFLW(selected_mobs)] was selected by the [name] ruleset and has been made into a midround Bloodsucker.") + log_game("DYNAMIC: [key_name(selected_mobs)] was selected by the [name] ruleset and has been made into a midround Bloodsucker.") + return TRUE \ No newline at end of file diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm index 34afe164ae85..92323d4d7fc6 100644 --- a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm @@ -948,3 +948,42 @@ M.mind.restricted_roles = restricted_roles log_game("[key_name(M)] has been selected as a Darkspawn") return TRUE + +////////////////////////////////////////////// +// // +// BLOODSUCKER // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/roundstart/bloodsucker + name = "Bloodsuckers" + antag_flag = ROLE_BLOODSUCKER + antag_datum = /datum/antagonist/bloodsucker + protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Research Director", "Chief Engineer", "Chief Medical Officer", "Brig Physician", "Curator") + restricted_roles = list("AI", "Cyborg") + required_candidates = 1 + weight = 5 + cost = 10 + scaling_cost = 9 + requirements = list(10,10,10,10,10,10,10,10,10,10) + antag_cap = list("denominator" = 24) + +/datum/dynamic_ruleset/roundstart/bloodsucker/pre_execute(population) + . = ..() + var/num_bloodsuckers = get_antag_cap(population) * (scaled_times + 1) + + for(var/i = 1 to num_bloodsuckers) + if(candidates.len <= 0) + break + var/mob/selected_mobs = pick_n_take(candidates) + assigned += selected_mobs.mind + selected_mobs.mind.restricted_roles = restricted_roles + selected_mobs.mind.special_role = ROLE_BLOODSUCKER + return TRUE + +/datum/dynamic_ruleset/roundstart/bloodsucker/execute() + for(var/assigned_bloodsuckers in assigned) + var/datum/mind/bloodsuckermind = assigned_bloodsuckers + if(!bloodsuckermind.make_bloodsucker(assigned_bloodsuckers)) + assigned -= assigned_bloodsuckers + return TRUE \ No newline at end of file diff --git a/code/game/gamemodes/objective.dm b/code/game/gamemodes/objective.dm index cf9645e2dd96..2f424c141efe 100644 --- a/code/game/gamemodes/objective.dm +++ b/code/game/gamemodes/objective.dm @@ -1379,6 +1379,22 @@ GLOBAL_LIST_EMPTY(possible_items_special) /datum/objective/debrain, /datum/objective/protect, /datum/objective/assist, + // Fulp edit START - Bloodsuckers + // DEFAULT OBJECTIVES // + /datum/objective/bloodsucker/lair, + /datum/objective/survive/bloodsucker, + /datum/objective/bloodsucker/protege, + /datum/objective/bloodsucker/heartthief, + /datum/objective/bloodsucker/gourmand, + // CLAN OBJECTIVES // + /datum/objective/bloodsucker/gourmand/brujah, //Brujah + /datum/objective/bloodsucker/kindred, //Nosferatu + /datum/objective/bloodsucker/embrace, //Ventrue + // MISC OBJECTIVES // + /datum/objective/bloodsucker/monsterhunter, + /datum/objective/bloodsucker/vassalhim, + /datum/objective/bloodsucker/frenzy, + // Fulp edit END /datum/objective/destroy, /datum/objective/hijack, /datum/objective/escape, diff --git a/code/game/objects/items/devices/scanners.dm b/code/game/objects/items/devices/scanners.dm index a2372e14efdc..2ba1c663438c 100644 --- a/code/game/objects/items/devices/scanners.dm +++ b/code/game/objects/items/devices/scanners.dm @@ -415,7 +415,9 @@ GENE SCANNER blood_type = R.name else blood_type = blood_id - if(C.blood_volume <= BLOOD_VOLUME_SAFE(C) && C.blood_volume > BLOOD_VOLUME_OKAY(C)) + if(HAS_TRAIT(M, TRAIT_MASQUERADE)) //bloodsuckers + to_chat(user, span_info("Blood level 100%, 560 cl, type: [blood_type]")) + else if(C.blood_volume <= BLOOD_VOLUME_SAFE(C) && C.blood_volume > BLOOD_VOLUME_OKAY(C)) to_chat(user, "[span_danger("LOW blood level [blood_percent] %, [C.blood_volume] cl,")] [span_info("type: [blood_type]")]") else if(C.blood_volume <= BLOOD_VOLUME_OKAY(C)) to_chat(user, "[span_danger("CRITICAL blood level [blood_percent] %, [C.blood_volume] cl,")] [span_info("type: [blood_type]")]") diff --git a/code/game/objects/items/implants/implant_mindshield.dm b/code/game/objects/items/implants/implant_mindshield.dm index 1a8cac811931..db6df583bb9f 100644 --- a/code/game/objects/items/implants/implant_mindshield.dm +++ b/code/game/objects/items/implants/implant_mindshield.dm @@ -25,6 +25,9 @@ if(target.mind.has_antag_datum(/datum/antagonist/brainwashed)) target.mind.remove_antag_datum(/datum/antagonist/brainwashed) + + if(target.mind.has_antag_datum(/datum/antagonist/vassal)) // Fulpstation Bloodsuckers + target.mind.remove_antag_datum(/datum/antagonist/vassal) var/datum/antagonist/hivemind/host = target.mind.has_antag_datum(/datum/antagonist/hivemind) //Releases the target from mind control beforehand if(host) @@ -45,6 +48,13 @@ removed(target, TRUE) return FALSE + var/datum/antagonist/vassal/vassaldatum = IS_VASSAL(target) + if(target.mind.has_antag_datum(/datum/antagonist/vassal)) + if(!silent && vassaldatum.favorite_vassal) + target.visible_message(span_warning("[target] seems to resist the implant!"), span_warning("You feel something interfering with your mental conditioning, but you resist it!")) + removed(target, TRUE) + return FALSE + var/datum/antagonist/hivevessel/woke = target.is_wokevessel() if(is_hivemember(target)) for(var/datum/antagonist/hivemind/hive in GLOB.antagonists) diff --git a/code/modules/admin/sql_ban_system.dm b/code/modules/admin/sql_ban_system.dm index e460a43f3fa4..bb86db7d6606 100644 --- a/code/modules/admin/sql_ban_system.dm +++ b/code/modules/admin/sql_ban_system.dm @@ -268,9 +268,9 @@ var/list/long_job_lists = list("Civilian" = GLOB.original_civilian_positions, "Ghost and Other Roles" = list(ROLE_BRAINWASHED, ROLE_DEATHSQUAD, ROLE_DRONE, ROLE_FUGITIVE, ROLE_HOLOPARASITE, ROLE_LAVALAND, ROLE_MIND_TRANSFER, ROLE_POSIBRAIN, ROLE_SENTIENCE), "Antagonist Positions" = list(ROLE_ABDUCTOR, ROLE_ALIEN, ROLE_BLOB, - ROLE_BROTHER, ROLE_CHANGELING, ROLE_CULTIST, + ROLE_BLOODSUCKER, ROLE_BROTHER, ROLE_CHANGELING, ROLE_CULTIST, ROLE_DEVIL, ROLE_FUGITIVE, ROLE_HOLOPARASITE, ROLE_INTERNAL_AFFAIRS, ROLE_MALF, - ROLE_MONKEY, ROLE_NINJA, ROLE_OPERATIVE, + ROLE_MONKEY, ROLE_MONSTERHUNTER, ROLE_NINJA, ROLE_OPERATIVE, ROLE_REV, ROLE_REVENANT, ROLE_REV_HEAD, ROLE_SERVANT_OF_RATVAR, ROLE_SYNDICATE, ROLE_TRAITOR, ROLE_WIZARD, ROLE_GANG, ROLE_VAMPIRE, diff --git a/code/modules/antagonists/bloodsuckers/bloodsucker_daylight.dm b/code/modules/antagonists/bloodsuckers/bloodsucker_daylight.dm new file mode 100644 index 000000000000..513b4f1d3e7a --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/bloodsucker_daylight.dm @@ -0,0 +1,172 @@ +/// 1 minute +#define TIME_BLOODSUCKER_DAY 60 +/// 10 minutes +#define TIME_BLOODSUCKER_NIGHT 600 +/// 1.5 minutes +#define TIME_BLOODSUCKER_DAY_WARN 90 +/// 30 seconds +#define TIME_BLOODSUCKER_DAY_FINAL_WARN 30 +/// 5 seconds +#define TIME_BLOODSUCKER_BURN_INTERVAL 5 + +/// Over Time, tick down toward a "Solar Flare" of UV buffeting the station. This period is harmful to vamps. +/obj/effect/sunlight + ///If the Sun is currently out our not + var/amDay = FALSE + ///The time between the next cycle + var/time_til_cycle = TIME_BLOODSUCKER_NIGHT + ///If Bloodsuckers have been given their level yet + var/issued_XP = FALSE + +/obj/effect/sunlight/Initialize() + . = ..() + START_PROCESSING(SSprocessing, src) + +/obj/effect/sunlight/Destroy() + STOP_PROCESSING(SSprocessing, src) + return ..() + +/obj/effect/sunlight/process() + /// Update all Bloodsucker sunlight huds + for(var/datum/mind/bloodsucker_minds as anything in get_antag_minds(/datum/antagonist/bloodsucker)) + if(!istype(bloodsucker_minds) || !istype(bloodsucker_minds.current)) + continue + var/datum/antagonist/bloodsucker/bloodsuckerdatum = bloodsucker_minds.has_antag_datum(/datum/antagonist/bloodsucker) + if(istype(bloodsuckerdatum)) + bloodsuckerdatum.update_sunlight(max(0, time_til_cycle), amDay) // This pings all HUDs + time_til_cycle-- + if(amDay) + if(time_til_cycle > 0) + punish_vamps() + if(!issued_XP && time_til_cycle <= 15) + issued_XP = TRUE + /// Cycle through all vamp antags and check if they're inside a closet. + for(var/datum/mind/bloodsucker_minds as anything in get_antag_minds(/datum/antagonist/bloodsucker)) + if(!istype(bloodsucker_minds) || !istype(bloodsucker_minds.current)) + continue + var/datum/antagonist/bloodsucker/bloodsuckerdatum = bloodsucker_minds.has_antag_datum(/datum/antagonist/bloodsucker) + if(bloodsuckerdatum) + // Rank up! Must still be in a coffin to level! + bloodsuckerdatum.RankUp() + if(time_til_cycle <= 1) + warn_daylight(5, span_announce("The solar flare has ended, and the daylight danger has passed...for now."), \ + span_announce("The solar flare has ended, and the daylight danger has passed...for now."), \ + "") + amDay = FALSE + issued_XP = FALSE + time_til_cycle = TIME_BLOODSUCKER_NIGHT + message_admins("BLOODSUCKER NOTICE: Daylight Ended. Resetting to Night (Lasts for [TIME_BLOODSUCKER_NIGHT / 60] minutes.") + for(var/datum/mind/bloodsucker_minds as anything in get_antag_minds(/datum/antagonist/bloodsucker)) + if(!istype(bloodsucker_minds) || !istype(bloodsucker_minds.current)) + continue + var/datum/antagonist/bloodsucker/bloodsuckerdatum = bloodsucker_minds.has_antag_datum(/datum/antagonist/bloodsucker) + if(!istype(bloodsuckerdatum)) + continue + take_home_power() + else + switch(time_til_cycle) + if(TIME_BLOODSUCKER_DAY_WARN) + warn_daylight(1, span_danger("Solar Flares will bombard the station with dangerous UV in [TIME_BLOODSUCKER_DAY_WARN / 60] minutes. Prepare to seek cover in a coffin or closet."), \ + "", \ + "") + give_home_power() + if(TIME_BLOODSUCKER_DAY_FINAL_WARN) + message_admins("BLOODSUCKER NOTICE: Daylight beginning in [TIME_BLOODSUCKER_DAY_FINAL_WARN] seconds.)") + warn_daylight(2, span_userdanger("Solar Flares are about to bombard the station! You have [TIME_BLOODSUCKER_DAY_FINAL_WARN] seconds to find cover!"), \ + span_danger("In [TIME_BLOODSUCKER_DAY_FINAL_WARN / 10], your master will be at risk of a Solar Flare. Make sure they find cover!"), \ + "") + if(TIME_BLOODSUCKER_BURN_INTERVAL) + warn_daylight(3, span_userdanger("Seek cover, for Sol rises!"), \ + "", \ + "") + if(0) + amDay = TRUE + time_til_cycle = TIME_BLOODSUCKER_DAY + warn_daylight(4, span_userdanger("Solar flares bombard the station with deadly UV light!
Stay in cover for the next [TIME_BLOODSUCKER_DAY / 60] minutes or risk Final Death!"), \ + span_userdanger("Solar flares bombard the station with UV light!"), \ + span_userdanger("The sunlight is visible throughout the station, the Bloodsuckers must be asleep by now!")) + message_admins("BLOODSUCKER NOTICE: Daylight Beginning (Lasts for [TIME_BLOODSUCKER_DAY / 60] minutes.)") + +/obj/effect/sunlight/proc/warn_daylight(danger_level = 0, vampwarn = "", vassalwarn = "", hunteralert = "") + for(var/datum/mind/bloodsucker_minds as anything in get_antag_minds(/datum/antagonist/bloodsucker)) + if(!istype(bloodsucker_minds)) + continue + to_chat(bloodsucker_minds, vampwarn) + if(bloodsucker_minds.current) + switch(danger_level) + if(1) + bloodsucker_minds.current.playsound_local(null, 'sound/effects/griffin_3.ogg', 50 + danger_level, 1) + if(2) + bloodsucker_minds.current.playsound_local(null, 'sound/effects/griffin_5.ogg', 50 + danger_level, 1) + if(3) + bloodsucker_minds.current.playsound_local(null, 'sound/effects/alert.ogg', 75, 1) + if(4) + bloodsucker_minds.current.playsound_local(null, 'sound/ambience/ambimystery.ogg', 100, 1) + if(5) + bloodsucker_minds.current.playsound_local(null, 'sound/spookoween/ghosty_wind.ogg', 90, 1) + if(vassalwarn != "") + for(var/datum/mind/vassal_minds as anything in get_antag_minds(/datum/antagonist/vassal)) + if(!istype(vassal_minds)) + continue + if(vassal_minds.has_antag_datum(/datum/antagonist/bloodsucker)) + continue + to_chat(vassal_minds, vassalwarn) + if(hunteralert != "") + for(var/datum/mind/monsterhunter_minds as anything in get_antag_minds(/datum/antagonist/monsterhunter)) + if(!istype(monsterhunter_minds)) + continue + to_chat(monsterhunter_minds, hunteralert) + +/// Cycle through all vamp antags and check if they're inside a closet. +/obj/effect/sunlight/proc/punish_vamps() + for(var/datum/mind/bloodsucker_minds as anything in get_antag_minds(/datum/antagonist/bloodsucker)) + if(!istype(bloodsucker_minds) || !istype(bloodsucker_minds.current)) + continue + var/datum/antagonist/bloodsucker/bloodsuckerdatum = bloodsucker_minds.has_antag_datum(/datum/antagonist/bloodsucker) + if(!istype(bloodsuckerdatum)) + continue + if(istype(bloodsucker_minds.current.loc, /obj/structure)) + if(istype(bloodsucker_minds.current.loc, /obj/structure/closet/crate/coffin)) // Coffins offer the BEST protection + SEND_SIGNAL(bloodsucker_minds.current, COMSIG_ADD_MOOD_EVENT, "vampsleep", /datum/mood_event/coffinsleep) + continue + if(COOLDOWN_FINISHED(bloodsuckerdatum, bloodsucker_spam_sol_burn)) // Closets offer SOME protection + to_chat(bloodsucker_minds, span_warning("Your skin sizzles. [bloodsucker_minds.current.loc] doesn't protect well against UV bombardment.")) + COOLDOWN_START(bloodsuckerdatum, bloodsucker_spam_sol_burn, BLOODSUCKER_SPAM_SOL) //This should happen twice per Sol + bloodsucker_minds.current.adjustFireLoss(0.5 + bloodsuckerdatum.bloodsucker_level / 2) + bloodsucker_minds.current.updatehealth() + SEND_SIGNAL(bloodsucker_minds.current, COMSIG_ADD_MOOD_EVENT, "vampsleep", /datum/mood_event/daylight_1) + else // Out in the Open? + if(COOLDOWN_FINISHED(bloodsuckerdatum, bloodsucker_spam_sol_burn)) + if(bloodsuckerdatum.bloodsucker_level > 0) + to_chat(bloodsucker_minds, span_userdanger("The solar flare sets your skin ablaze!")) + else + to_chat(bloodsucker_minds, span_userdanger("The solar flare scalds your neophyte skin!")) + COOLDOWN_START(bloodsuckerdatum, bloodsucker_spam_sol_burn, BLOODSUCKER_SPAM_SOL) //This should happen twice per Sol + if(bloodsucker_minds.current.fire_stacks <= 0) + bloodsucker_minds.current.fire_stacks = 0 + if(bloodsuckerdatum.bloodsucker_level > 0) + bloodsucker_minds.current.adjust_fire_stacks(0.2 + bloodsuckerdatum.bloodsucker_level / 10) + bloodsucker_minds.current.IgniteMob() + bloodsucker_minds.current.adjustFireLoss(2 + bloodsuckerdatum.bloodsucker_level) + bloodsucker_minds.current.updatehealth() + SEND_SIGNAL(bloodsucker_minds.current, COMSIG_ADD_MOOD_EVENT, "vampsleep", /datum/mood_event/daylight_2) + +/// It's late, give the "Vanishing Act" (gohome) power to Bloodsuckers. +/obj/effect/sunlight/proc/give_home_power() + for(var/datum/mind/bloodsucker_minds as anything in get_antag_minds(/datum/antagonist/bloodsucker)) + if(!istype(bloodsucker_minds) || !istype(bloodsucker_minds.current)) + continue + var/datum/antagonist/bloodsucker/bloodsuckerdatum = bloodsucker_minds.has_antag_datum(/datum/antagonist/bloodsucker) + if(istype(bloodsuckerdatum) && bloodsuckerdatum.lair && !(locate(/datum/action/bloodsucker/gohome) in bloodsuckerdatum.powers)) + bloodsuckerdatum.BuyPower(new /datum/action/bloodsucker/gohome) + +/// It's over now, remove the "Vanishing Act" (gohome) power from Bloodsuckers. +/obj/effect/sunlight/proc/take_home_power() + for(var/datum/mind/bloodsucker_minds as anything in get_antag_minds(/datum/antagonist/bloodsucker)) + if(!istype(bloodsucker_minds) || !istype(bloodsucker_minds.current)) + continue + var/datum/antagonist/bloodsucker/bloodsuckerdatum = bloodsucker_minds.has_antag_datum(/datum/antagonist/bloodsucker) + for(var/datum/action/bloodsucker/power in bloodsuckerdatum.powers) + if(istype(power, /datum/action/bloodsucker/gohome)) + bloodsuckerdatum.powers -= power + power.Remove(bloodsucker_minds.current) diff --git a/code/modules/antagonists/bloodsuckers/bloodsucker_flaws.dm b/code/modules/antagonists/bloodsuckers/bloodsucker_flaws.dm new file mode 100644 index 000000000000..d81e176560ff --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/bloodsucker_flaws.dm @@ -0,0 +1,98 @@ +///////////////////////////////////////////////////////////////////////////////////////// +// Any changes to clans have to be reflected in '/obj/item/book/kindred' /search proc. // +///////////////////////////////////////////////////////////////////////////////////////// +/datum/antagonist/bloodsucker/proc/AssignClanAndBane() + var/static/list/clans = list( + CLAN_BRUJAH, + CLAN_NOSFERATU, + CLAN_TREMERE, + CLAN_VENTRUE, + CLAN_MALKAVIAN, + ) + var/list/options = list() + options = clans + var/mob/living/carbon/human/bloodsucker = owner.current + // Brief descriptions in case they don't read the Wiki. + to_chat(owner, span_announce("List of all Clans:\n\ + Brujah - Prone to Frenzy, Brawn buffed.\n\ + Nosferatu - Disfigured, no Masquerade, Ventcrawl.\n\ + Tremere - Burn in the Chapel, Blood Magic.\n\ + Ventrue - Cant drink from mindless mobs, can't level up, raise a vassal instead.\n\ + Malkavian - Complete insanity.")) + to_chat(owner, span_announce("* Read more about Clans here: https://wiki.fulp.gg/en/Bloodsucker.")) + + var/answer = alert(owner.current, "You have Ranked up far enough to remember your clan. Which clan are you part of?", "Our mind feels luxurious...", options) + if(!answer) + to_chat(owner, span_warning("You have wilingfully decided to stay ignorant.")) + return + switch(answer) + if(CLAN_BRUJAH) + my_clan = CLAN_BRUJAH + to_chat(owner, span_announce("You have Ranked up enough to learn: You are part of the Brujah Clan!\n\ + * As part of the Bujah Clan, you are more prone to falling into Frenzy, though you are used to it, and can enter it whenever you want!\n\ + * Additionally, Brawn and punches deal more damage than other Bloodsuckers. Use this to your advantage!\n\ + * Finally, your Favorite Vassal will gain the Brawn ability to help you in combat.")) + /// Makes their max punch, and by extension Brawn, stronger - Stolen from SpendRank() + var/datum/species/user_species = bloodsucker.dna.species + user_species.punchdamagehigh += 1.5 + AddHumanityLost(17.5) + BuyPower(new /datum/action/bloodsucker/brujah) + var/datum/objective/bloodsucker/gourmand/brujah/brujah_objective = new + brujah_objective.owner = owner + objectives += brujah_objective + if(CLAN_NOSFERATU) + my_clan = CLAN_NOSFERATU + to_chat(owner, span_announce("You have Ranked up enough to learn: You are part of the Nosferatu Clan!\n\ + * As part of the Nosferatu Clan, you unable to disguise yourself within the crew, as such you do not know how to use the Masquerade or Veil ability.\n\ + * Additionally, in exchange for having a bad back, always looking like Pale death, and not being identifiable, you can fit into vents using Alt+Click.\n\ + * Finally, your Favorite Vassal will become disfigured and will be able to ventcrawl wile naked.")) + for(var/datum/action/bloodsucker/power in powers) + if(istype(power, /datum/action/bloodsucker/masquerade) || istype(power, /datum/action/bloodsucker/veil)) + powers -= power + if(power.active) + power.DeactivatePower() + power.Remove(owner.current) + if(!bloodsucker.has_quirk(/datum/quirk/badback)) + bloodsucker.add_quirk(/datum/quirk/badback) + if(!HAS_TRAIT(bloodsucker, VENTCRAWLER_ALWAYS)) + ADD_TRAIT(bloodsucker, VENTCRAWLER_ALWAYS, BLOODSUCKER_TRAIT) + if(!HAS_TRAIT(bloodsucker, TRAIT_DISFIGURED)) + ADD_TRAIT(bloodsucker, TRAIT_DISFIGURED, BLOODSUCKER_TRAIT) + var/datum/objective/bloodsucker/kindred/kindred_objective = new + kindred_objective.owner = owner + objectives += kindred_objective + if(CLAN_TREMERE) + my_clan = CLAN_TREMERE + to_chat(owner, span_announce("You have Ranked up enough to learn: You are part of the Tremere Clan!\n\ + * As part of the Tremere Clan, you are weak to True Faith, as such are unable to enter the Chapel.\n\ + * Additionally, you cannot learn new Powers, instead you will upgrade your Blood Magic to grow stronger.\n\ + * You have been given a spare Rank to spend immediately, and you can get more manually by Vassalizing people.")) + remove_nondefault_powers() + bloodsucker_level_unspent++ + var/datum/objective/bloodsucker/tremere_power/bloodmagic_objective = new + bloodmagic_objective.owner = owner + objectives += bloodmagic_objective + BuyPower(new /datum/action/bloodsucker/targeted/tremere/dominate) + BuyPower(new /datum/action/bloodsucker/targeted/tremere/auspex) + BuyPower(new /datum/action/bloodsucker/targeted/tremere/thaumaturgy) + if(CLAN_VENTRUE) + my_clan = CLAN_VENTRUE + to_chat(owner, span_announce("You have Ranked up enough to learn: You are part of the Ventrue Clan!\n\ + * As part of the Ventrue Clan, you are extremely snobby with your meals, and refuse to drink blood from people without a Mind.\n\ + * Additionally, you will no longer Rank up. You are now instead able to get a Favorite vassal, by putting a Vassal on the persuasion rack and attempting to Tortute them.\n\ + * Finally, you may Rank your Favorite Vassal (and your own powers) up by buckling them onto a Candelabrum and using it, this will cost a Rank or Blood to do.")) + to_chat(owner, span_announce("* Bloodsucker Tip: Examine the Persuasion Rack/Candelabrum to see how they operate!")) + var/datum/objective/bloodsucker/embrace/embrace_objective = new + embrace_objective.owner = owner + objectives += embrace_objective + if(CLAN_MALKAVIAN) + my_clan = CLAN_MALKAVIAN + owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/creepalert.ogg', 80, FALSE, pressure_affected = FALSE, use_reverb = FALSE) + to_chat(owner, span_hypnophrase("Welcome to the Malkavian...")) + to_chat(owner, span_userdanger("* Bloodsucker Malkavian: The voices will not go away. It is endless. You are trapped.\n\ + * If you get a Favorite Vassal, they will suffer a near fate as you, pick wisely.")) + bloodsucker.gain_trauma(/datum/brain_trauma/mild/hallucinations, TRAUMA_RESILIENCE_ABSOLUTE) + bloodsucker.gain_trauma(/datum/brain_trauma/special/bluespace_prophet, TRAUMA_RESILIENCE_ABSOLUTE) + ADD_TRAIT(bloodsucker, TRAIT_XRAY_VISION, BLOODSUCKER_TRAIT) + + owner.announce_objectives() diff --git a/code/modules/antagonists/bloodsuckers/bloodsucker_frenzy.dm b/code/modules/antagonists/bloodsuckers/bloodsucker_frenzy.dm new file mode 100644 index 000000000000..efc0debcb4f7 --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/bloodsucker_frenzy.dm @@ -0,0 +1,113 @@ + +/** + * # 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 + ..() + +/** + * # 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. + */ + +/datum/status_effect/frenzy + id = "Frenzy" + status_type = STATUS_EFFECT_UNIQUE + duration = -1 + tick_interval = 10 + examine_text = "They seem... inhumane, and feral!" + alert_type = /atom/movable/screen/alert/status_effect/frenzy + /// Store whether they were an advancedtooluser, to give the trait back upon exiting. + var/was_tooluser = FALSE + /// The stored Bloodsucker antag datum + var/datum/antagonist/bloodsucker/bloodsuckerdatum + +/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 = 'icons/mob/actions/actions_bloodsucker.dmi' + icon_state = "power_recover" + +/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) + + // Disable ALL Powers and notify their entry + if(bloodsuckerdatum.my_clan != CLAN_BRUJAH) + bloodsuckerdatum.DisableAllPowers() + 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).")) + + to_chat(owner, "you enter a frenzy!") + + // Stamina resistances + if(bloodsuckerdatum.my_clan == CLAN_MALKAVIAN) + ADD_TRAIT(owner, TRAIT_STUNIMMUNE, FRENZY_TRAIT) + else if(bloodsuckerdatum.my_clan != CLAN_BRUJAH) + user.physiology.stamina_mod *= 0.4 + + // Give the other Frenzy effects + ADD_TRAIT(owner, TRAIT_MUTE, FRENZY_TRAIT) + ADD_TRAIT(owner, TRAIT_DEAF, FRENZY_TRAIT) + if(user.IsAdvancedToolUser()) + was_tooluser = TRUE + ADD_TRAIT(owner, TRAIT_MONKEYLIKE, SPECIES_TRAIT) + owner.add_movespeed_modifier(MOVESPEED_ID_DNA_VAULT, update=TRUE, priority=100, multiplicative_slowdown=-0.74, blacklisted_movetypes=(FLYING|FLOATING)) + bloodsuckerdatum.frenzygrab.teach(user, TRUE) + owner.add_client_colour(/datum/client_colour/cursed_heart_blood) + var/obj/cuffs = user.get_item_by_slot(SLOT_HANDCUFFED) + var/obj/legcuffs = user.get_item_by_slot(SLOT_LEGCUFFED) + if(user.handcuffed || user.legcuffed) + user.clear_cuffs(cuffs, TRUE) + user.clear_cuffs(legcuffs, TRUE) + // Keep track of how many times we've entered a Frenzy. + bloodsuckerdatum.frenzies += 1 + bloodsuckerdatum.frenzied = TRUE + return ..() + +/datum/status_effect/frenzy/on_remove() + var/mob/living/carbon/human/user = owner + to_chat(owner, "you come back to your senses.") + REMOVE_TRAIT(owner, TRAIT_MUTE, FRENZY_TRAIT) + REMOVE_TRAIT(owner, TRAIT_DEAF, FRENZY_TRAIT) + if(was_tooluser) + REMOVE_TRAIT(owner, TRAIT_MONKEYLIKE, SPECIES_TRAIT) + was_tooluser = FALSE + owner.remove_movespeed_modifier(MOVESPEED_ID_DNA_VAULT, update=FALSE, priority=100, multiplicative_slowdown=-0.74, blacklisted_movetypes=(FLYING|FLOATING)) + bloodsuckerdatum.frenzygrab.remove(user) + owner.remove_client_colour(/datum/client_colour/cursed_heart_blood) + + if(bloodsuckerdatum.my_clan == CLAN_MALKAVIAN) + REMOVE_TRAIT(owner, TRAIT_STUNIMMUNE, FRENZY_TRAIT) + else if(bloodsuckerdatum.my_clan != CLAN_BRUJAH) + owner.Dizzy(3 SECONDS) + owner.Paralyze(2 SECONDS) + user.physiology.stamina_mod /= 0.4 + + bloodsuckerdatum.frenzied = FALSE + return ..() + +/datum/status_effect/frenzy/tick() + var/mob/living/carbon/human/user = owner + if(!bloodsuckerdatum.frenzied) + return + if(bloodsuckerdatum.my_clan == CLAN_BRUJAH) + user.adjustBruteLoss(1 + (bloodsuckerdatum.humanity_lost / 10)) + else + user.adjustFireLoss(1.5 + (bloodsuckerdatum.humanity_lost / 10)) diff --git a/code/modules/antagonists/bloodsuckers/bloodsucker_integration.dm b/code/modules/antagonists/bloodsuckers/bloodsucker_integration.dm new file mode 100644 index 000000000000..524b877fa43a --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/bloodsucker_integration.dm @@ -0,0 +1,160 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// TG OVERWRITES + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Gives Curators their abilities +/datum/outfit/job/curator/post_equip(mob/living/carbon/human/user, visualsOnly = FALSE) + . = ..() + + ADD_TRAIT(user, TRAIT_BLOODSUCKER_HUNTER, JOB_TRAIT) + +/datum/species/jelly/slime/spec_life(mob/living/carbon/human/user) + // Prevents Slimeperson 'gaming + if(IS_BLOODSUCKER(user)) + return + . = ..() + +/// Prevents Bloodsuckers from naturally regenerating Blood - Even while on masquerade +/mob/living/carbon/human/handle_blood(delta_time, times_fired) + if(mind && IS_BLOODSUCKER(src)) + return + /// For Vassals -- Bloodsuckers get this removed while on Masquerade, so we don't want to remove the check above. + if(HAS_TRAIT(src, TRAIT_NOPULSE)) + return + . = ..() + +/mob/living/carbon/human/natural_bodytemperature_stabilization(datum/gas_mixture/environment, delta_time, times_fired) + // Return 0 as your natural temperature. Species proc handle_environment() will adjust your temperature based on this. + if(HAS_TRAIT(src, TRAIT_COLDBLOODED)) + return 0 + . = ..() + +// Overwrites mob/living/life.dm instead of doing handle_changeling +/mob/living/carbon/human/Life(delta_time = (SSmobs.wait/10), times_fired) + . = ..() + SEND_SIGNAL(src, COMSIG_LIVING_BIOLOGICAL_LIFE, delta_time, times_fired) + +// Used when analyzing a Bloodsucker, Masquerade will hide brain traumas (Unless you're a Beefman) +/mob/living/carbon/get_traumas() + if(!mind) + return ..() + var/datum/antagonist/bloodsucker/bloodsuckerdatum = IS_BLOODSUCKER(src) + if(bloodsuckerdatum && HAS_TRAIT(src, TRAIT_MASQUERADE)) + return + . = ..() + +// Used to keep track of how much Blood we've drank so far +/mob/living/carbon/human/get_status_tab_items() + . = ..() + if(mind) + var/datum/antagonist/bloodsucker/bloodsuckerdatum = mind.has_antag_datum(/datum/antagonist/bloodsucker) + if(bloodsuckerdatum) + . += "" + . += "Blood Drank: [bloodsuckerdatum.total_blood_drank]" + + +// INTEGRATION: Adding Procs and Datums to existing "classes" // + +/mob/living/proc/HaveBloodsuckerBodyparts(displaymessage = "") // displaymessage can be something such as "rising from death" for Torpid Sleep. givewarningto is the person receiving messages. + if(!getorganslot(ORGAN_SLOT_HEART)) + if(displaymessage != "") + to_chat(src, span_warning("Without a heart, you are incapable of [displaymessage].")) + return FALSE + if(!get_bodypart(BODY_ZONE_HEAD)) + if(displaymessage != "") + to_chat(src, span_warning("Without a head, you are incapable of [displaymessage].")) + return FALSE + if(!getorgan(/obj/item/organ/brain)) // NOTE: This is mostly just here so we can do one scan for all needed parts when creating a vamp. You probably won't be trying to use powers w/out a brain. + if(displaymessage != "") + to_chat(src, span_warning("Without a brain, you are incapable of [displaymessage].")) + return FALSE + return TRUE + +// EXAMINING +/mob/living/carbon/human/proc/ReturnVampExamine(mob/living/viewer) + if(!mind || !viewer.mind) + return "" + // Target must be a Vamp + var/datum/antagonist/bloodsucker/bloodsuckerdatum = mind.has_antag_datum(/datum/antagonist/bloodsucker) + if(!bloodsuckerdatum) + return "" + // Viewer is Target's Vassal? + if(viewer.mind.has_antag_datum(/datum/antagonist/vassal) in bloodsuckerdatum.vassals) + var/returnString = "\[This is your Master!\]" + var/returnIcon = "[icon2html('icons/mob/vampiric.dmi', world, "bloodsucker")]" + returnString += "\n" + return returnIcon + returnString + // Viewer not a Vamp AND not the target's vassal? + if(!viewer.mind.has_antag_datum((/datum/antagonist/bloodsucker)) && !(viewer in bloodsuckerdatum.vassals)) + if(!(HAS_TRAIT(viewer, TRAIT_BLOODSUCKER_HUNTER) && bloodsuckerdatum.broke_masquerade)) + return "" + // Default String + var/returnString = "\[[bloodsuckerdatum.ReturnFullName(1)]\]" + var/returnIcon = "[icon2html('icons/mob/vampiric.dmi', world, "bloodsucker")]" + + // In Disguise (Veil)? + //if (name_override != null) + // returnString += " ([real_name] in disguise!) " + + //returnString += "\n" Don't need spacers. Using . += "" in examine.dm does this on its own. + return returnIcon + returnString + +/mob/living/carbon/human/proc/ReturnVassalExamine(mob/living/viewer) + if(!mind || !viewer.mind) + return "" + // Target must be a Vassal + var/datum/antagonist/vassal/vassaldatum = mind.has_antag_datum(/datum/antagonist/vassal) + if(!vassaldatum) + return "" + // Default String + var/returnString = "\[" + var/returnIcon = "" + // Vassals and Bloodsuckers recognize eachother, while Monster Hunters can see Vassals. + if(IS_BLOODSUCKER(viewer) || IS_VASSAL(viewer) || IS_MONSTERHUNTER(viewer)) + // Am I Viewer's Vassal? + if(vassaldatum?.master.owner == viewer.mind) + returnString += "This [dna.species.name] bears YOUR mark!" + returnIcon = "[icon2html('icons/mob/vampiric.dmi', world, "vassal")]" + // Am I someone ELSE'S Vassal? + else if(IS_BLOODSUCKER(viewer) || IS_MONSTERHUNTER(viewer)) + returnString += "This [dna.species.name] bears the mark of [vassaldatum.master.ReturnFullName(vassaldatum.master.owner.current,TRUE)][vassaldatum.master.broke_masquerade ? " who has broken the Masquerade" : ""]" + returnIcon = "[icon2html('icons/mob/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 vassaldatum?.master.vassals) + returnString += "[p_they(TRUE)] bears the mark of your Master" + returnIcon = "[icon2html('icons/mob/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('icons/mob/vampiric.dmi', world, "vassal_grey")]" + else + return "" + + returnString += "\]" // \n" Don't need spacers. Using . += "" in examine.dm does this on its own. + return returnIcon + returnString + +/// Am I "pale" when examined? - Bloodsuckers on Masquerade will hide this. +/mob/living/carbon/human/proc/ShowAsPaleExamine(mob/living/user, blood_volume) + if(!mind) + return BLOODSUCKER_HIDE_BLOOD + var/datum/antagonist/bloodsucker/bloodsuckerdatum = mind.has_antag_datum(/datum/antagonist/bloodsucker) + // Not a Bloodsucker? + if(!bloodsuckerdatum) + return BLOODSUCKER_HIDE_BLOOD + // Blood level too low to be hidden? + if(blood_volume <= BLOOD_VOLUME_BAD(user) || bloodsuckerdatum.frenzied) + return BLOODSUCKER_HIDE_BLOOD + // Special check: Nosferatu will always be Pale Death + if(bloodsuckerdatum.my_clan == CLAN_NOSFERATU) + return "[p_they(TRUE)] look[p_s()] like pale death" + if(HAS_TRAIT(src, TRAIT_MASQUERADE)) + return BLOODSUCKER_HIDE_BLOOD + switch(blood_volume) + if(BLOOD_VOLUME_OKAY(user) to BLOOD_VOLUME_SAFE(user)) + return "[p_they(TRUE)] [p_have()] pale skin.\n" + if(BLOOD_VOLUME_BAD(user) to BLOOD_VOLUME_OKAY(user)) + return "[p_they(TRUE)] look[p_s()] like pale death.\n" + // If a Bloodsucker is malnourished, AND if his temperature matches his surroundings (aka he hasn't fed recently and looks COLD) +// return blood_volume < BLOOD_VOLUME_OKAY // && !(bodytemperature <= get_temperature() + 2) diff --git a/code/modules/antagonists/bloodsuckers/bloodsucker_objectives.dm b/code/modules/antagonists/bloodsuckers/bloodsucker_objectives.dm new file mode 100644 index 000000000000..2cb27e67fedc --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/bloodsucker_objectives.dm @@ -0,0 +1,488 @@ +/* + * # Hide a random object somewhere on the station: + * + * var/turf/targetturf = get_random_station_turf() + * var/turf/targetturf = get_safe_random_station_turf() + */ + +/datum/objective/bloodsucker + martyr_compatible = TRUE + +// GENERATE +/datum/objective/bloodsucker/New() + update_explanation_text() + ..() + +////////////////////////////////////////////////////////////////////////////// +// // PROCS // // + +/// Look at all crew members, and for/loop through. +/datum/objective/bloodsucker/proc/return_possible_targets() + var/list/possible_targets = list() + for(var/datum/mind/possible_target in get_crewmember_minds()) + // Check One: Default Valid User + if(possible_target != owner && ishuman(possible_target.current) && possible_target.current.stat != DEAD) + // Check Two: Am Bloodsucker? + if(IS_BLOODSUCKER(possible_target.current)) + continue + possible_targets += possible_target + + return possible_targets + +////////////////////////////////////////////////////////////////////////////////////// +// // OBJECTIVES // // +////////////////////////////////////////////////////////////////////////////////////// + +////////////////////////////// +// DEFAULT OBJECTIVES // +////////////////////////////// + +/datum/objective/bloodsucker/lair + name = "claimlair" + +// EXPLANATION +/datum/objective/bloodsucker/lair/update_explanation_text() + explanation_text = "Create a lair by claiming a coffin, and protect it until the end of the shift."// Make sure to keep it safe!" + +// WIN CONDITIONS? +/datum/objective/bloodsucker/lair/check_completion() + var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.has_antag_datum(/datum/antagonist/bloodsucker) + if(bloodsuckerdatum && bloodsuckerdatum.coffin && bloodsuckerdatum.lair) + return TRUE + return FALSE + +/// Space_Station_13_areas.dm <--- all the areas + +////////////////////////////////////////////////////////////////////////////////////// + +/datum/objective/survive/bloodsucker + name = "bloodsuckersurvive" + explanation_text = "Survive the entire shift without succumbing to Final Death." + +// WIN CONDITIONS? +// Handled by parent + +////////////////////////////////////////////////////////////////////////////////////// + +#define VASSALIZE_COMMAND "command_vassalization" + +/// Vassalize someone in charge (Head of Staff + QM) +/datum/objective/bloodsucker/protege + name = "vassalization" + + var/list/heads = list( + "Captain", + "Head of Personnel", + "Head of Security", + "Research Director", + "Chief Engineer", + "Chief Medical Officer", + "Quartermaster", + ) + + var/list/departments = list( + "Security", + "Supply", + "Science", + "Engineering", + "Medical", + ) + + var/target_department // Equals "HEAD" when it's not a department role. + var/department_string + +// GENERATE! +/datum/objective/bloodsucker/protege/New() + switch(rand(0, 2)) + // Vasssalize Command/QM + if(0) + target_amount = 1 + target_department = VASSALIZE_COMMAND + // Vassalize a certain department + else + target_amount = rand(2,3) + target_department = pick(departments) + ..() + +// EXPLANATION +/datum/objective/bloodsucker/protege/update_explanation_text() + if(target_department == VASSALIZE_COMMAND) + explanation_text = "Guarantee a Vassal ends up as a Department Head or in a Leadership role." + else + explanation_text = "Have [target_amount] Vassal[target_amount == 1 ? "" : "s"] in the [target_department] department." + +// WIN CONDITIONS? +/datum/objective/bloodsucker/protege/check_completion() + + var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.has_antag_datum(/datum/antagonist/bloodsucker) + if(!bloodsuckerdatum || !bloodsuckerdatum.vassals.len) + return FALSE + + // Get list of all jobs that are qualified (for HEAD, this is already done) + var/list/valid_jobs + if(target_department == VASSALIZE_COMMAND) + valid_jobs = heads + else + valid_jobs = list() + var/list/alljobs = subtypesof(/datum/job) // This is just a list of TYPES, not the actual variables! + for(var/listed_jobs in alljobs) + var/datum/job/all_jobs = SSjob.GetJobType(listed_jobs) + if(!istype(all_jobs)) + continue + // Found a job whose Dept Head matches either list of heads, or this job IS the head. We exclude the QM from this, HoP handles Cargo. + if((target_department in all_jobs.department_head) || target_department == all_jobs.title) + valid_jobs += all_jobs.title + + // Check Vassals, and see if they match + var/objcount = 0 + var/list/counted_roles = list() // So you can't have more than one Captain count. + for(var/datum/antagonist/vassal/bloodsucker_vassals in bloodsuckerdatum.vassals) + if(!bloodsucker_vassals || !bloodsucker_vassals.owner) // Must exist somewhere, and as a vassal. + continue + + var/this_role = "none" + + // Mind Assigned + if((bloodsucker_vassals.owner.assigned_role in valid_jobs) && !(bloodsucker_vassals.owner.assigned_role in counted_roles)) + //to_chat(owner, span_userdanger("PROTEGE OBJECTIVE: (MIND ROLE)")) + this_role = bloodsucker_vassals.owner.assigned_role + // Mob Assigned + else if((bloodsucker_vassals.owner.current.job in valid_jobs) && !(bloodsucker_vassals.owner.current.job in counted_roles)) + //to_chat(owner, span_userdanger("PROTEGE OBJECTIVE: (MOB JOB)")) + this_role = bloodsucker_vassals.owner.current.job + // PDA Assigned + else if(bloodsucker_vassals.owner.current && ishuman(bloodsucker_vassals.owner.current)) + var/mob/living/carbon/human/vassal_users = bloodsucker_vassals.owner.current + var/obj/item/card/id/id_cards = vassal_users.wear_id ? vassal_users.wear_id.GetID() : null + if(id_cards && (id_cards.assignment in valid_jobs) && !(id_cards.assignment in counted_roles)) + //to_chat(owner, span_userdanger("PROTEGE OBJECTIVE: (GET ID)")) + this_role = id_cards.assignment + + // NO MATCH + if(this_role == "none") + continue + + // SUCCESS! + objcount++ + if(target_department == VASSALIZE_COMMAND) + counted_roles += this_role // Add to list so we don't count it again (but only if it's a Head) + + return objcount >= target_amount + /** + * # IMPORTANT NOTE!! + * + * Look for Job Values on mobs! This is assigned at the start, but COULD be changed via the HoP + * ALSO - Search through all jobs (look for prefs earlier that look for all jobs, and search through all jobs to see if their head matches the head listed, or it IS the head) + * ALSO - registered_account in _vending.dm for banks, and assigning new ones. + */ + +////////////////////////////////////////////////////////////////////////////////////// + +// NOTE: Look up /steal in objective.dm for inspiration. +/// Steal hearts. You just really wanna have some hearts. +/datum/objective/bloodsucker/heartthief + name = "heartthief" + +// GENERATE! +/datum/objective/bloodsucker/heartthief/New() + target_amount = rand(2,3) + ..() + +// EXPLANATION +/datum/objective/bloodsucker/heartthief/update_explanation_text() + . = ..() + explanation_text = "Steal and keep [target_amount] organic heart\s." + +// WIN CONDITIONS? +/datum/objective/bloodsucker/heartthief/check_completion() + if(!owner.current) + return FALSE + + var/list/all_items = owner.current.GetAllContents() + var/heart_count = 0 + for(var/obj/item/organ/heart/current_hearts in all_items) + if(current_hearts.organ_flags & ORGAN_SYNTHETIC) // No robo-hearts allowed + continue + heart_count++ + + if(heart_count >= target_amount) + return TRUE + return FALSE + +////////////////////////////////////////////////////////////////////////////////////// + +///Eat blood from a lot of people +/datum/objective/bloodsucker/gourmand + name = "gourmand" + +// GENERATE! +/datum/objective/bloodsucker/gourmand/New() + target_amount = rand(450,650) + ..() + +// EXPLANATION +/datum/objective/bloodsucker/gourmand/update_explanation_text() + . = ..() + explanation_text = "Using your Feed ability, drink [target_amount] units of Blood." + +// WIN CONDITIONS? +/datum/objective/bloodsucker/gourmand/check_completion() + var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.current.mind.has_antag_datum(/datum/antagonist/bloodsucker) + if(!bloodsuckerdatum) + return FALSE + var/stolen_blood = bloodsuckerdatum.total_blood_drank + if(stolen_blood >= target_amount) + return TRUE + return FALSE + +// HOW: Track each feed (if human). Count victory. + + + +////////////////////////////// +// CLAN OBJECTIVES // +////////////////////////////// + +/// Drink certain amount of Blood while in a Frenzy - Brujah Clan Objective +/datum/objective/bloodsucker/gourmand/brujah + name = "brujah gourmand" +// NOTE: This is a copy paste from default Gourmand objective. + +// EXPLANATION +/datum/objective/bloodsucker/gourmand/brujah/update_explanation_text() + . = ..() + explanation_text = "While in a Frenzy, using your Feed ability, drink [target_amount] units of Blood." + +// WIN CONDITIONS? +/datum/objective/bloodsucker/gourmand/brujah/check_completion() + var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.current.mind.has_antag_datum(/datum/antagonist/bloodsucker) + if(!bloodsuckerdatum) + return FALSE + var/stolen_blood = bloodsuckerdatum.frenzy_blood_drank + if(stolen_blood >= target_amount) + return TRUE + return FALSE + +////////////////////////////////////////////////////////////////////////////////////// + +/// Steal the Archive of the Kindred - Nosferatu Clan objective +/datum/objective/bloodsucker/kindred + name = "steal kindred" + +// EXPLANATION +/datum/objective/bloodsucker/kindred/update_explanation_text() + . = ..() + explanation_text = "Ensure Nosferatu steals and keeps control over the Archive of the Kindred." + +// WIN CONDITIONS? +/datum/objective/bloodsucker/kindred/check_completion() + if(!owner.current) + return FALSE + var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.current.mind.has_antag_datum(/datum/antagonist/bloodsucker) + if(!bloodsuckerdatum) + return FALSE + + for(var/datum/mind/bloodsucker_minds in bloodsuckerdatum.clan?.members) + var/datum/antagonist/bloodsucker/allsuckers = bloodsucker_minds.has_antag_datum(/datum/antagonist/bloodsucker) + if(allsuckers.my_clan != CLAN_NOSFERATU) + continue + if(!isliving(bloodsucker_minds.current)) + continue + var/list/all_items = allsuckers.owner.current.GetAllContents() + for(var/obj/items in all_items) + if(istype(items, /obj/item/book/kindred)) + return TRUE + return FALSE + +////////////////////////////////////////////////////////////////////////////////////// + +/// Max out a Tremere Power - Tremere Clan objective +/datum/objective/bloodsucker/tremere_power + name = "tremerepower" + +// EXPLANATION +/datum/objective/bloodsucker/tremere_power/update_explanation_text() + explanation_text = "Upgrade a Blood Magic power to the maximum level, remember that Vassalizing gives more Ranks!" + +// WIN CONDITIONS? +/datum/objective/bloodsucker/tremere_power/check_completion() + var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.has_antag_datum(/datum/antagonist/bloodsucker) + for(var/datum/action/bloodsucker/targeted/tremere/tremere_powers in bloodsuckerdatum.powers) + if(tremere_powers.level_current >= 5) + return TRUE + return FALSE + +////////////////////////////////////////////////////////////////////////////////////// + +/// Convert a crewmate - Ventrue Clan objective +/datum/objective/bloodsucker/embrace + name = "embrace" + +// EXPLANATION +/datum/objective/bloodsucker/embrace/update_explanation_text() + . = ..() + explanation_text = "Use the Candelabrum to Rank your Favorite Vassal up enough to become a Bloodsucker." + +// WIN CONDITIONS? +/datum/objective/bloodsucker/embrace/check_completion() + var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.current.mind.has_antag_datum(/datum/antagonist/bloodsucker) + if(!bloodsuckerdatum || bloodsuckerdatum.my_clan != CLAN_VENTRUE) + return FALSE + for(var/datum/antagonist/vassal/vassaldatum in bloodsuckerdatum.vassals) + if(vassaldatum.owner && vassaldatum.favorite_vassal) + if(vassaldatum.owner.has_antag_datum(/datum/antagonist/bloodsucker)) + return TRUE + return FALSE + + + +////////////////////////////// +// MONSTERHUNTER OBJECTIVES // +////////////////////////////// + +/datum/objective/bloodsucker/monsterhunter + name = "destroymonsters" + +// EXPLANATION +/datum/objective/bloodsucker/monsterhunter/update_explanation_text() + . = ..() + explanation_text = "Destroy all monsters on [station_name()]." + +// WIN CONDITIONS? +/datum/objective/bloodsucker/monsterhunter/check_completion() + var/list/datum/mind/monsters = list() + for(var/mob/living/players in GLOB.alive_mob_list) + if(IS_HERETIC(players) || iscultist(players) || IS_BLOODSUCKER(players) || is_wizard(players) || is_servant_of_ratvar(players)) + monsters += players + if(players?.mind?.has_antag_datum(/datum/antagonist/changeling)) + monsters += players + if(players?.mind?.has_antag_datum(/datum/antagonist/wizard/apprentice)) + monsters += players + for(var/datum/mind/monster_minds in monsters) + if(monster_minds && monster_minds != owner && monster_minds.current.stat != DEAD) + return FALSE + return TRUE + + + +////////////////////////////// +// VASSAL OBJECTIVES // +////////////////////////////// + +/datum/objective/bloodsucker/vassal + +// EXPLANATION +/datum/objective/bloodsucker/vassal/update_explanation_text() + . = ..() + explanation_text = "Guarantee the success of your Master's mission!" + +// WIN CONDITIONS? +/datum/objective/bloodsucker/vassal/check_completion() + var/datum/antagonist/vassal/antag_datum = owner.has_antag_datum(/datum/antagonist/vassal) + return antag_datum.master?.owner?.current?.stat != DEAD + + + +////////////////////////////// +// REMOVED OBJECTIVES // +////////////////////////////// + +/// Defile a facility with blood +/datum/objective/bloodsucker/desecrate + + // Space_Station_13_areas.dm <--- all the areas + +////////////////////////////////////////////////////////////////////////////////////// + +/// Destroy the Solar Arrays +/datum/objective/bloodsucker/solars +/* // TG Updates broke this, it needs maintaining. +// Space_Station_13_areas.dm <--- all the areas +/datum/objective/bloodsucker/solars/update_explanation_text() + . = ..() + explanation_text = "Prevent all solar arrays on the station from functioning." + +/datum/objective/bloodsucker/solars/check_completion() + // Sort through all /obj/machinery/power/solar_control in the station ONLY, and check that they are functioning. + // Make sure that lastgen is 0 or connected_panels.len is 0. Doesnt matter if it's tracking. + for (var/obj/machinery/power/solar_control/solar_control_consoles in SSsun.solars) + // Check On Station: + var/turf/solar_turfs = get_turf(solar_control_consoles) + if(!solar_turfs || !is_station_level(solar_turfs.z)) // <------ Taken from NukeOp + //message_admins("DEBUG A: [solar_control_consoles] not on station!") + continue // Not on station! We don't care about this. + if(solar_control_consoles && solar_control_consoles.lastgen > 0 && solar_control_consoles.connected_panels.len > 0 && solar_control_consoles.connected_tracker) + return FALSE + return TRUE +*/ + +// NOTE: Look up /assassinate in objective.dm for inspiration. +/// Vassalize a target. +/datum/objective/bloodsucker/vassalhim + name = "vassalhim" + var/target_department_type = FALSE + +/datum/objective/bloodsucker/vassalhim/New() + var/list/possible_targets = return_possible_targets() + find_target(possible_targets) + ..() + +// EXPLANATION +/datum/objective/bloodsucker/vassalhim/update_explanation_text() + . = ..() + if(target?.current) + explanation_text = "Ensure [target.name], the [!target_department_type ? target.assigned_role : target.special_role], is Vassalized via the Persuasion Rack." + else + explanation_text = "Free Objective" + +/datum/objective/bloodsucker/vassalhim/admin_edit(mob/admin) + admin_simple_target_pick(admin) + +// WIN CONDITIONS? +/datum/objective/bloodsucker/vassalhim/check_completion() + if(!target || target.has_antag_datum(/datum/antagonist/vassal)) + return TRUE + return FALSE + +/// Enter Frenzy repeatedly +/datum/objective/bloodsucker/frenzy + name = "frenzy" + +/datum/objective/bloodsucker/frenzy/New() + target_amount = rand(3,4) + ..() + +/datum/objective/bloodsucker/frenzy/update_explanation_text() + . = ..() + explanation_text = "Enter Frenzy [target_amount] of times without succumbing to Final Death." + +/datum/objective/bloodsucker/frenzy/check_completion() + var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.current.mind.has_antag_datum(/datum/antagonist/bloodsucker) + if(!bloodsuckerdatum) + return FALSE + if(bloodsuckerdatum.frenzies >= target_amount) + return TRUE + return FALSE + +////////////////////////////////////////////////////////////////////////////////////// + +/// Mutilate a certain amount of Vassals +/* +/datum/objective/bloodsucker/vassal_mutilation + name = "steal kindred" +/datum/objective/bloodsucker/vassal_mutilation/New() + target_amount = rand(2,3) + ..() + +// EXPLANATION +/datum/objective/bloodsucker/vassal_mutilation/update_explanation_text() + . = ..() + explanation_text = "Mutate [target_amount] of Vassals into vile sevant creatures." + +// WIN CONDITIONS? +/datum/objective/bloodsucker/vassal_mutilation/check_completion() + var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.current.mind.has_antag_datum(/datum/antagonist/bloodsucker) + if(bloodsuckerdatum.vassals_mutated >= target_amount) + return TRUE + return FALSE +*/ diff --git a/code/modules/antagonists/bloodsuckers/bloodsuckers.dm b/code/modules/antagonists/bloodsuckers/bloodsuckers.dm new file mode 100644 index 000000000000..d3b0793605e5 --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/bloodsuckers.dm @@ -0,0 +1,828 @@ +/datum/antagonist/bloodsucker + name = "\improper Bloodsucker" + show_in_antagpanel = TRUE + roundend_category = "bloodsuckers" + antagpanel_category = "Bloodsucker" + job_rank = ROLE_BLOODSUCKER + show_name_in_check_antagonists = TRUE + can_coexist_with_others = FALSE + + // TIMERS // + ///Timer between alerts for Burn messages + COOLDOWN_DECLARE(static/bloodsucker_spam_sol_burn) + ///Timer between alerts for Healing messages + COOLDOWN_DECLARE(static/bloodsucker_spam_healing) + + ///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 + ///Blood required to enter Frenzy + var/frenzy_threshold = FRENZY_THRESHOLD_ENTER + ///If we are currently in a Frenzy + var/frenzied = FALSE + + ///ALL Powers currently owned + var/list/datum/action/powers = list() + ///Bloodsucker Clan - Used for dealing with Sol + var/datum/team/vampireclan/clan + ///Frenzy Grab Martial art given to Bloodsuckers in a Frenzy + var/datum/martial_art/frenzygrab/frenzygrab = new + ///You get assigned a Clan once you Rank up enough + var/my_clan = NONE + + ///Vassals under my control. Periodically remove the dead ones. + var/list/datum/antagonist/vassal/vassals = list() + ///Have we selected our Favorite Vassal yet? + var/has_favorite_vassal = FALSE + + var/bloodsucker_level + var/bloodsucker_level_unspent = 1 + var/passive_blood_drain = -0.2 + var/additional_regen + var/bloodsucker_regen_rate = 0.3 + var/max_blood_volume = 600 + + // Used for Bloodsucker Objectives + var/area/lair + var/obj/structure/closet/crate/coffin + var/total_blood_drank = 0 + var/frenzy_blood_drank = 0 + var/frenzies = 0 + + /// Static typecache of all bloodsucker powers. + var/static/list/all_bloodsucker_powers = typecacheof(/datum/action/bloodsucker, TRUE) + /// Antagonists that cannot be Vassalized no matter what + var/list/vassal_banned_antags = list( + /datum/antagonist/bloodsucker, + /datum/antagonist/monsterhunter, + /datum/antagonist/changeling, + /datum/antagonist/cult, + /datum/antagonist/heretic, + /datum/antagonist/xeno, + /datum/antagonist/obsessed + ) + ///Default Bloodsucker traits + var/static/list/bloodsucker_traits = list( + TRAIT_NOBREATH, + TRAIT_SLEEPIMMUNE, + TRAIT_NOCRITDAMAGE, + TRAIT_RESISTCOLD, + TRAIT_RADIMMUNE, + TRAIT_GENELESS, + TRAIT_STABLEHEART, + TRAIT_NOSOFTCRIT, + TRAIT_NOHARDCRIT, + TRAIT_AGEUSIA, + TRAIT_NOPULSE, + TRAIT_COLDBLOODED, + TRAIT_VIRUSIMMUNE, + TRAIT_TOXIMMUNE, + TRAIT_HARDLY_WOUNDED, + ) + +/mob/living/proc/explain_powers() + set name = "Bloodsucker Help" + set category = "Mentor" + + var/datum/antagonist/bloodsucker/bloodsuckerdatum = mind.has_antag_datum(/datum/antagonist/bloodsucker) + var/choice = alert(usr, "What Power are you looking into?", "Mentorhelp v2", bloodsuckerdatum.powers) + if(!choice) + return + var/datum/action/bloodsucker/power = choice + to_chat(usr, span_warning("[power.power_explanation]")) + +/// These handles the application of antag huds/special abilities +/datum/antagonist/bloodsucker/apply_innate_effects(mob/living/mob_override) + . = ..() + var/mob/living/current_mob = mob_override || owner.current + var/mob/living/carbon/H = owner + var/datum/atom_hud/antag/hud = GLOB.huds[ANTAG_HUD_BLOODSUCKER] + RegisterSignal(owner.current, COMSIG_LIVING_BIOLOGICAL_LIFE, .proc/LifeTick) + to_chat(owner, "As a vampiric clown, you are no longer a danger to yourself. Your clownish nature has been subdued by your thirst for blood.") + H.dna.remove_mutation(CLOWNMUT) + hud.join_hud(current_mob) + set_antag_hud(current_mob, "Bloodsucker") + +/datum/antagonist/bloodsucker/remove_innate_effects(mob/living/mob_override) + . = ..() + UnregisterSignal(owner.current, COMSIG_LIVING_BIOLOGICAL_LIFE) + +/datum/antagonist/bloodsucker/get_admin_commands() + . = ..() + .["Give Level"] = CALLBACK(src, .proc/RankUp) + if(bloodsucker_level_unspent >= 1) + .["Remove Level"] = CALLBACK(src, .proc/RankDown) + + if(broke_masquerade) + .["Fix Masquerade"] = CALLBACK(src, .proc/fix_masquerade) + else + .["Break Masquerade"] = CALLBACK(src, .proc/break_masquerade) + +/// Called by the add_antag_datum() mind proc after the instanced datum is added to the mind's antag_datums list. +/datum/antagonist/bloodsucker/on_gain() + if(IS_VASSAL(owner.current)) // Vassals shouldnt be getting the same benefits as Bloodsuckers. + bloodsucker_level_unspent = 0 + else + // Start Sunlight if first Bloodsucker + clan.check_start_sunlight() + // Name and Titles + SelectFirstName() + SelectTitle(am_fledgling = TRUE) + SelectReputation(am_fledgling = TRUE) + // Objectives + forge_bloodsucker_objectives() + + . = ..() + // Assign Powers + AssignStarterPowersAndStats() + +/// 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() + /// End Sunlight? (if last Vamp) + clan.check_cancel_sunlight() + ClearAllPowersAndStats() + return ..() + +/datum/antagonist/bloodsucker/greet() + . = ..() + var/fullname = ReturnFullName(TRUE) + 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, 'sound/ambience/antag/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. + owner.current.blood_volume = max(owner.current.blood_volume, BLOOD_VOLUME_NORMAL(owner.current)) + +/datum/antagonist/bloodsucker/proc/add_objective(datum/objective/added_objective) + objectives += added_objective + +/datum/antagonist/bloodsucker/proc/remove_objectives(datum/objective/removed_objective) + objectives -= removed_objective + +// 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(!isnull(levels)) + 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) + +/** + * # Vampire Clan + * + * This is used for dealing with the Vampire Clan. + * This handles Sol for Bloodsuckers, making sure to not have several. + * None of this should appear in game, we are using it JUST for Sol. All Bloodsuckers should have their individual report. + */ + +/datum/team/vampireclan + name = "Clan" + + /// Sunlight Timer. Created on first Bloodsucker assign. Destroyed on last removed Bloodsucker. + var/obj/effect/sunlight/bloodsucker_sunlight + +/datum/antagonist/bloodsucker/create_team(datum/team/vampireclan/team) + if(!team) + for(var/datum/antagonist/bloodsucker/bloodsuckerdatums in GLOB.antagonists) + if(!bloodsuckerdatums.owner) + continue + if(bloodsuckerdatums.clan) + clan = bloodsuckerdatums.clan + return + clan = new /datum/team/vampireclan + return + if(!istype(team)) + stack_trace("Wrong team type passed to [type] initialization.") + clan = team + +/datum/antagonist/bloodsucker/get_team() + return clan + +/datum/team/vampireclan/roundend_report() + if(members.len <= 0) + return + var/list/report = list() + report += "Lurking in the darkness, the Bloodsuckers were:
" + for(var/datum/mind/mind_members in members) + for(var/datum/antagonist/bloodsucker/individual_bloodsuckers in mind_members.antag_datums) + if(mind_members.has_antag_datum(/datum/antagonist/vassal)) // Skip over Ventrue's Favorite Vassal + continue + report += individual_bloodsuckers.roundend_report() + + return "
[report.Join("
")]
" + +/// Individual roundend report +/datum/antagonist/bloodsucker/roundend_report() + // Get the default Objectives + var/list/report = list() + // Vamp name + report += "
\[[ReturnFullName(TRUE)]\]" + report += printplayer(owner) + // Clan (Actual Clan, not Team) name + if(my_clan != NONE) + report += "They were part of the [my_clan]!" + + // Default Report + var/objectives_complete = TRUE + if(objectives.len) + report += printobjectives(objectives) + for(var/datum/objective/objective in objectives) + if(!objective.check_completion()) + objectives_complete = FALSE + break + + // Now list their vassals + if(vassals.len > 0) + report += "Their Vassals were..." + for(var/datum/antagonist/vassal/all_vassals in vassals) + if(all_vassals.owner) + var/jobname = all_vassals.owner.assigned_role ? "the [all_vassals.owner.assigned_role]" : "" + report += "[all_vassals.owner.name] [jobname][all_vassals.favorite_vassal == TRUE ? " and was the Favorite Vassal" : ""]" + + if(objectives.len == 0 || objectives_complete) + report += "The [name] was successful!" + else + report += "The [name] has failed!" + + return report + +/** + * # Assigning Sol + * + * Sol is the sunlight, during this period, all Bloodsuckers must be in their coffin, else they burn. + * This was originally dealt with by the gamemode, but as gamemodes no longer exist, it is dealt with by the team. + */ + +/// Start Sol, called when someone is assigned Bloodsucker +/datum/team/vampireclan/proc/check_start_sunlight() + if(members.len <= 1) + message_admins("New Sol has been created due to Bloodsucker assignment.") + bloodsucker_sunlight = new() + +/// End Sol, if you're the last Bloodsucker +/datum/team/vampireclan/proc/check_cancel_sunlight() + // No minds in the clan? Delete Sol. + if(members.len <= 1) + message_admins("Sol has been deleted due to the lack of Bloodsuckers") + QDEL_NULL(bloodsucker_sunlight) + +/// Buying powers +/datum/antagonist/bloodsucker/proc/BuyPower(datum/action/bloodsucker/power) + powers += power + power.Grant(owner.current) + +/datum/antagonist/bloodsucker/proc/RemovePower(datum/action/bloodsucker/power) + for(var/datum/action/bloodsucker/all_powers as anything in powers) + if(initial(power.name) == all_powers.name) + power = all_powers + break + if(power.active) + power.DeactivatePower() + powers -= power + power.Remove(owner.current) + +/datum/antagonist/bloodsucker/proc/AssignStarterPowersAndStats() + // Purchase Roundstart Powers + BuyPower(new /datum/action/bloodsucker/feed) + BuyPower(new /datum/action/bloodsucker/masquerade) + if(!IS_VASSAL(owner.current)) // Favorite Vassal gets their own. + BuyPower(new /datum/action/bloodsucker/veil) + add_verb(owner.current, /mob/living/proc/explain_powers) + // Traits: Species + var/mob/living/carbon/human/user = owner.current + if(ishuman(owner.current)) + var/datum/species/user_species = user.dna.species + user_species.species_traits += DRINKSBLOOD + user.dna?.remove_all_mutations() + user_species.punchdamagelow += 1 //lowest possible punch damage - 0 + user_species.punchdamagehigh += 1 //highest possible punch damage - 9 + /// Give Bloodsucker Traits + for(var/all_traits in bloodsucker_traits) + ADD_TRAIT(owner.current, all_traits, BLOODSUCKER_TRAIT) + /// 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 + HealVampireOrgans() + +/datum/antagonist/bloodsucker/proc/ClearAllPowersAndStats() + /// Remove huds + remove_hud() + // Powers + remove_verb(owner.current, /mob/living/proc/explain_powers) + while(powers.len) + var/datum/action/bloodsucker/power = pick(powers) + powers -= power + power.Remove(owner.current) + // owner.RemoveSpell(power) + /// Stats + if(ishuman(owner.current)) + var/mob/living/carbon/human/user = owner.current + var/datum/species/user_species = user.dna.species + user_species.species_traits -= DRINKSBLOOD + // Clown + if(istype(user) && owner.assigned_role == "Clown") + user.dna.add_mutation(CLOWNMUT) + /// Remove ALL Traits, as long as its from BLOODSUCKER_TRAIT's source. - This is because of unique cases like Nosferatu getting Ventcrawling. + for(var/all_status_traits in owner.current.status_traits) + REMOVE_TRAIT(owner.current, all_status_traits, BLOODSUCKER_TRAIT) + /// Update Health + owner.current.setMaxHealth(100) + // Language + owner.current.remove_language(/datum/language/vampiric) + /// Heart + RemoveVampOrgans() + /// Eyes + var/mob/living/carbon/user = owner.current + var/obj/item/organ/eyes/user_eyes = user.getorganslot(ORGAN_SLOT_EYES) + if(user_eyes) + user_eyes.flash_protect += 1 + user_eyes.sight_flags = 0 + user_eyes.see_in_dark = 2 + user_eyes.lighting_alpha = LIGHTING_PLANE_ALPHA_VISIBLE + user.update_sight() + +/datum/antagonist/bloodsucker/proc/RankUp() + set waitfor = FALSE + var/datum/antagonist/vassal/vassaldatum = IS_VASSAL(owner.current) + if(!owner || !owner.current || vassaldatum) + return + bloodsucker_level_unspent++ + // Spend Rank Immediately? + if(istype(owner.current.loc, /obj/structure/closet/crate/coffin)) + if(my_clan == CLAN_VENTRUE) + to_chat(owner, span_announce("You have recieved a new Rank to level up your Favorite Vassal with!")) + return + SpendRank() + else + to_chat(owner, span_notice("You have grown more ancient! Sleep in a coffin that you have claimed to thicken your blood and become more powerful.")) + if(bloodsucker_level_unspent >= 2) + to_chat(owner, span_announce("Bloodsucker Tip: If you cannot find or steal a coffin to use, you can build one from wood or metal.")) + +/datum/antagonist/bloodsucker/proc/RankDown() + bloodsucker_level_unspent-- + +/datum/antagonist/bloodsucker/proc/remove_nondefault_powers() + for(var/datum/action/bloodsucker/power as anything in powers) + if(istype(power, /datum/action/bloodsucker/feed) || istype(power, /datum/action/bloodsucker/masquerade) || istype(power, /datum/action/bloodsucker/veil)) + continue + RemovePower(power) + +/datum/antagonist/bloodsucker/proc/LevelUpPowers() + for(var/datum/action/bloodsucker/power as anything in powers) + if(istype(power, /datum/action/bloodsucker/targeted/tremere)) + continue + power.level_current++ + +///Disables all powers, accounting for torpor +/datum/antagonist/bloodsucker/proc/DisableAllPowers() + for(var/datum/action/bloodsucker/power as anything in powers) + if((power.check_flags & BP_CANT_USE_IN_TORPOR) && HAS_TRAIT(owner.current, TRAIT_NODEATH)) + if(power.active) + power.DeactivatePower() + +#define PURCHASE_VASSAL "Vassal" +#define PURCHASE_TREMERE "Tremere" +#define PURCHASE_DEFAULT "Default" + +/datum/antagonist/bloodsucker/proc/SpendRank(mob/living/carbon/human/target, spend_rank = TRUE) + set waitfor = FALSE + + var/datum/antagonist/vassal/vassaldatum = target?.mind.has_antag_datum(/datum/antagonist/vassal) + if(!owner || !owner.current || !owner.current.client || (spend_rank && bloodsucker_level_unspent <= 0)) + return + //Who am I purchasing Powers for? + var/power_mode = PURCHASE_DEFAULT + var/upgrade_message = "You have the opportunity to grow more ancient. Select a power to advance your Rank." + if(target) + power_mode = PURCHASE_VASSAL + upgrade_message = "You have the opportunity to level up your Favorite Vassal. Select a power you wish them to recieve." + if(my_clan == CLAN_TREMERE) + power_mode = PURCHASE_TREMERE + upgrade_message = "You have the opportunity to grow more ancient. Select a power you wish to upgrade." + // Purchase Power Prompt + var/list/options = list() + switch(power_mode) + if(PURCHASE_DEFAULT) + for(var/datum/action/bloodsucker/power as anything in all_bloodsucker_powers) + if(initial(power.purchase_flags) & BLOODSUCKER_CAN_BUY && !(locate(power) in powers)) + options[initial(power.name)] = power + if(PURCHASE_VASSAL) + for(var/datum/action/bloodsucker/power as anything in all_bloodsucker_powers) + if(initial(power.purchase_flags) & VASSAL_CAN_BUY && !(locate(power) in vassaldatum.powers)) + options[initial(power.name)] = power + if(PURCHASE_TREMERE) + for(var/datum/action/bloodsucker/targeted/tremere/power as anything in powers) + if(!(power.purchase_flags & TREMERE_CAN_BUY)) + continue + if(isnull(power.upgraded_power)) + continue + options[initial(power.name)] = power + + if(options.len < 1) + to_chat(owner.current, span_notice("You grow more ancient by the night!")) + else + // Give them the UI to purchase a power. + var/choice = alert(owner.current, upgrade_message, "Your Blood Thickens...", options) + // Prevent Bloodsuckers from closing/reopning their coffin to spam Levels. + if(spend_rank && bloodsucker_level_unspent <= 0) + return + // Did you choose a power? + if(!choice || !options[choice]) + to_chat(owner.current, span_notice("You prevent your blood from thickening just yet, but you may try again later.")) + return + if(power_mode != PURCHASE_TREMERE) + // Prevent Bloodsuckers from closing/reopning their coffin to spam Levels. + if((locate(options[choice]) in (power_mode == PURCHASE_VASSAL ? vassaldatum.powers : powers))) + to_chat(owner.current, span_notice("You prevent your blood from thickening just yet, but you may try again later.")) + return + if(power_mode != PURCHASE_VASSAL) + // Prevent Bloodsuckers from purchasing a power while outside of their Coffin. + if(!istype(owner.current.loc, /obj/structure/closet/crate/coffin)) + to_chat(owner.current, span_warning("You must be in your Coffin to purchase Powers.")) + return + + // Good to go - Buy Power! + var/datum/action/bloodsucker/purchased_power = options[choice] + switch(power_mode) + if(PURCHASE_DEFAULT) + BuyPower(new purchased_power) + to_chat(owner.current, span_notice("You have learned how to use [choice]!")) + if(PURCHASE_VASSAL) + vassaldatum.BuyPower(new purchased_power) + to_chat(owner.current, span_notice("You taught [target] how to use [choice]!")) + to_chat(target, span_notice("Your master taught you how to use [choice]!")) + if(PURCHASE_TREMERE) + var/datum/action/bloodsucker/targeted/tremere/tremere_power = purchased_power + if(isnull(tremere_power.upgraded_power)) + to_chat(owner.current, span_notice("[choice] is already at max level!")) + return + BuyPower(new tremere_power.upgraded_power) + RemovePower(tremere_power) + to_chat(owner.current, span_notice("You have upgraded [choice]!")) + + // Advance Powers - Includes the one you just purchased. + LevelUpPowers() + vassaldatum?.LevelUpPowers() + // Bloodsucker-only Stat upgrades + bloodsucker_regen_rate += 0.05 + max_blood_volume += 100 + // Misc. Stats Upgrades + if(ishuman(owner.current)) + var/mob/living/carbon/human/user = owner.current + var/datum/species/user_species = user.dna.species + user_species.punchdamagelow += 0.5 + // This affects the hitting power of Brawn. + user_species.punchdamagehigh += 0.5 + + // We're almost done - Spend your Rank now. + vassaldatum?.vassal_level++ + bloodsucker_level++ + if(spend_rank) + bloodsucker_level_unspent-- + + // Ranked up enough? Let them join a Clan. + if(bloodsucker_level == 3) + AssignClanAndBane() + // Ranked up enough to get your true Reputation? + if(bloodsucker_level == 4) + SelectReputation(am_fledgling = FALSE, forced = TRUE) + if(power_mode == PURCHASE_VASSAL) + if(vassaldatum.vassal_level == 2) + ADD_TRAIT(target, TRAIT_COLDBLOODED, BLOODSUCKER_TRAIT) + ADD_TRAIT(target, TRAIT_NOBREATH, BLOODSUCKER_TRAIT) + ADD_TRAIT(target, TRAIT_AGEUSIA, BLOODSUCKER_TRAIT) + to_chat(target, span_notice("Your blood begins you feel cold, as ash sits on your tongue, you stop breathing...")) + if(vassaldatum.vassal_level == 3) + ADD_TRAIT(target, TRAIT_NOCRITDAMAGE, BLOODSUCKER_TRAIT) + ADD_TRAIT(target, TRAIT_NOSOFTCRIT, BLOODSUCKER_TRAIT) + to_chat(target, span_notice("You feel your Master's blood reinforce you, strengthening you up.")) + if(vassaldatum.vassal_level == 4) + ADD_TRAIT(target, TRAIT_SLEEPIMMUNE, BLOODSUCKER_TRAIT) + ADD_TRAIT(target, TRAIT_VIRUSIMMUNE, BLOODSUCKER_TRAIT) + to_chat(target, span_notice("You feel your Master's blood begin to protect you from bacteria.")) + target.skin_tone = "albino" + if(vassaldatum.vassal_level == 5) + ADD_TRAIT(target, TRAIT_NOHARDCRIT, BLOODSUCKER_TRAIT) + ADD_TRAIT(target, TRAIT_HARDLY_WOUNDED, BLOODSUCKER_TRAIT) + to_chat(target, span_notice("You feel yourself able to take cuts and stabbings like it's nothing.")) + if(vassaldatum.vassal_level == 6) + to_chat(target, span_notice("You feel your heart stop pumping for the last time as you begin to thirst for blood, you feel... dead.")) + target.mind.add_antag_datum(/datum/antagonist/bloodsucker) + SEND_SIGNAL(owner.current, COMSIG_ADD_MOOD_EVENT, "madevamp", /datum/mood_event/madevamp) + if(vassaldatum.vassal_level >= 6) // We're a Bloodsucker now, lets update our Rank hud from now on. + set_vassal_level(target) + + // Done! Let them know & Update their HUD. + to_chat(owner.current, span_notice("You are now a rank [bloodsucker_level] Bloodsucker. Your strength, health, feed rate, regen rate, and maximum blood capacity have all increased!\n\ + * Your existing powers have all ranked up as well!")) + update_hud(owner.current) + owner.current.playsound_local(null, 'sound/effects/pope_entry.ogg', 25, TRUE, pressure_affected = FALSE) + +#undef PURCHASE_VASSAL +#undef PURCHASE_TREMERE +#undef PURCHASE_DEFAULT + +///Set the Vassal's rank to their Bloodsucker level +/datum/antagonist/bloodsucker/proc/set_vassal_level(mob/living/carbon/human/target) + var/datum/antagonist/bloodsucker/bloodsuckerdatum = IS_BLOODSUCKER(target) + var/datum/antagonist/vassal/vassaldatum = IS_VASSAL(target) + bloodsuckerdatum.bloodsucker_level = vassaldatum.vassal_level + +//////////////////////////////////////////////////////////////////////////////////////////////// + +/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) // Protege Objective + var/datum/objective/bloodsucker/protege/protege_objective = new + protege_objective.owner = owner + objectives += protege_objective + if(2) // Heart Thief Objective + var/datum/objective/bloodsucker/heartthief/heartthief_objective = new + heartthief_objective.owner = owner + objectives += heartthief_objective + if(3) // Drink Blood Objective + var/datum/objective/bloodsucker/gourmand/gourmand_objective = new + gourmand_objective.owner = owner + objectives += gourmand_objective + + +/// Name shown on antag list +/datum/antagonist/bloodsucker/antag_listing_name() + return ..() + "([ReturnFullName(TRUE)])" + +/// 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 ..() + +/* + * # Bloodsucker Names + * + * All Bloodsuckers get a name, and gets a better one when they hit Rank 4. + */ + +/// Names +/datum/antagonist/bloodsucker/proc/SelectFirstName() + if(owner.current.gender == MALE) + bloodsucker_name = pick( + "Desmond","Rudolph","Dracula","Vlad","Pyotr","Gregor", + "Cristian","Christoff","Marcu","Andrei","Constantin", + "Gheorghe","Grigore","Ilie","Iacob","Luca","Mihail","Pavel", + "Vasile","Octavian","Sorin","Sveyn","Aurel","Alexe","Iustin", + "Theodor","Dimitrie","Octav","Damien","Magnus","Caine","Abel", // Romanian/Ancient + "Lucius","Gaius","Otho","Balbinus","Arcadius","Romanos","Alexios","Vitellius", // Latin + "Melanthus","Teuthras","Orchamus","Amyntor","Axion", // Greek + "Thoth","Thutmose","Osorkon,","Nofret","Minmotu","Khafra", // Egyptian + "Dio", + ) + else + bloodsucker_name = pick( + "Islana","Tyrra","Greganna","Pytra","Hilda", + "Andra","Crina","Viorela","Viorica","Anemona", + "Camelia","Narcisa","Sorina","Alessia","Sophia", + "Gladda","Arcana","Morgan","Lasarra","Ioana","Elena", + "Alina","Rodica","Teodora","Denisa","Mihaela", + "Svetla","Stefania","Diyana","Kelssa","Lilith", // Romanian/Ancient + "Alexia","Athanasia","Callista","Karena","Nephele","Scylla","Ursa", // Latin + "Alcestis","Damaris","Elisavet","Khthonia","Teodora", // Greek + "Nefret","Ankhesenpep", // Egyptian + ) + +/datum/antagonist/bloodsucker/proc/SelectTitle(am_fledgling = 0, forced = FALSE) + // Already have Title + if(!forced && bloodsucker_title != null) + return + // Titles [Master] + if(!am_fledgling) + if(owner.current.gender == MALE) + bloodsucker_title = pick ("Count","Baron","Viscount","Prince","Duke","Tzar","Dreadlord","Lord","Master") + else + bloodsucker_title = pick ("Countess","Baroness","Viscountess","Princess","Duchess","Tzarina","Dreadlady","Lady","Mistress") + to_chat(owner, span_announce("You have earned a title! You are now known as [ReturnFullName(TRUE)]!")) + // Titles [Fledgling] + else + bloodsucker_title = null + +/datum/antagonist/bloodsucker/proc/SelectReputation(am_fledgling = FALSE, forced = FALSE) + // Already have Reputation + if(!forced && bloodsucker_reputation != null) + return + + if(am_fledgling) + bloodsucker_reputation = pick( + "Crude","Callow","Unlearned","Neophyte","Novice","Unseasoned", + "Fledgling","Young","Neonate","Scrapling","Untested","Unproven", + "Unknown","Newly Risen","Born","Scavenger","Unknowing","Unspoiled", + "Disgraced","Defrocked","Shamed","Meek","Timid","Broken","Fresh", + ) + else if(owner.current.gender == MALE && prob(10)) + bloodsucker_reputation = pick("King of the Damned", "Blood King", "Emperor of Blades", "Sinlord", "God-King") + else if(owner.current.gender == FEMALE && prob(10)) + bloodsucker_reputation = pick("Queen of the Damned", "Blood Queen", "Empress of Blades", "Sinlady", "God-Queen") + else + bloodsucker_reputation = pick( + "Butcher","Blood Fiend","Crimson","Red","Black","Terror", + "Nightman","Feared","Ravenous","Fiend","Malevolent","Wicked", + "Ancient","Plaguebringer","Sinister","Forgotten","Wretched","Baleful", + "Inqisitor","Harvester","Reviled","Robust","Betrayer","Destructor", + "Damned","Accursed","Terrible","Vicious","Profane","Vile", + "Depraved","Foul","Slayer","Manslayer","Sovereign","Slaughterer", + "Forsaken","Mad","Dragon","Savage","Villainous","Nefarious", + "Inquisitor","Marauder","Horrible","Immortal","Undying","Overlord", + "Corrupt","Hellspawn","Tyrant","Sanguineous", + ) + + to_chat(owner, span_announce("You have earned a reputation! You are now known as [ReturnFullName(TRUE)]!")) + + +/datum/antagonist/bloodsucker/proc/AmFledgling() + return !bloodsucker_title + +/datum/antagonist/bloodsucker/proc/ReturnFullName(include_rep = FALSE) + + var/fullname + // Name First + fullname = (bloodsucker_name ? bloodsucker_name : owner.current.name) + // Title + if(bloodsucker_title) + fullname = bloodsucker_title + " " + fullname + // Rep + if(include_rep && bloodsucker_reputation) + fullname = fullname + " the " + bloodsucker_reputation + + return fullname + +///When a Bloodsucker breaks the Masquerade, they get their HUD icon changed, and Malkavian Bloodsuckers get alerted. +/datum/antagonist/bloodsucker/proc/break_masquerade() + if(broke_masquerade) + return + owner.current.playsound_local(null, 'sound/effects/lunge_warn.ogg', 100, FALSE, pressure_affected = FALSE) + to_chat(owner.current, span_cultboldtalic("You have broken the Masquerade!")) + to_chat(owner.current, span_warning("Bloodsucker Tip: When you break the Masquerade, you become open for termination by fellow Bloodsuckers, and your Vassals are no longer completely loyal to you, as other Bloodsuckers can steal them for themselves!")) + broke_masquerade = TRUE + set_antag_hud(owner.current, "masquerade_broken") + for(var/datum/mind/clan_minds as anything in get_antag_minds(/datum/antagonist/bloodsucker)) + if(owner == clan_minds) + continue + if(!isliving(clan_minds.current)) + continue + to_chat(clan_minds, span_userdanger("[owner.current] has broken the Masquerade! Ensure they are eliminated at all costs!")) + var/datum/antagonist/bloodsucker/bloodsuckerdatum = clan_minds.has_antag_datum(/datum/antagonist/bloodsucker) + if(bloodsuckerdatum.my_clan != CLAN_MALKAVIAN) + continue + var/datum/objective/assassinate/masquerade_objective = new /datum/objective/assassinate + masquerade_objective.target = owner.current + masquerade_objective.explanation_text = "Ensure [owner.current], who has broken the Masquerade, is Final Death'ed." + bloodsuckerdatum.objectives += masquerade_objective + clan_minds.announce_objectives() + +///This is admin-only of reverting a broken masquerade, sadly it doesn't remove the Malkavian objectives yet. +/datum/antagonist/bloodsucker/proc/fix_masquerade() + if(!broke_masquerade) + return + to_chat(owner.current, span_cultboldtalic("You have re-entered the Masquerade.")) + broke_masquerade = FALSE + + +///////////////////////////////////// +// BLOOD COUNTER & RANK MARKER ! // +///////////////////////////////////// + +/datum/hud/human/New(mob/living/carbon/human/owner) + . = ..() + blood_display = new /atom/movable/screen/bloodsucker/blood_counter + infodisplay += blood_display + vamprank_display = new /atom/movable/screen/bloodsucker/rank_counter + infodisplay += vamprank_display + sunlight_display = new /atom/movable/screen/bloodsucker/sunlight_counter + infodisplay += sunlight_display + +/datum/hud + var/atom/movable/screen/bloodsucker/blood_counter/blood_display + var/atom/movable/screen/bloodsucker/rank_counter/vamprank_display + var/atom/movable/screen/bloodsucker/sunlight_counter/sunlight_display + +/datum/antagonist/bloodsucker/proc/add_hud() + return + +/datum/antagonist/bloodsucker/proc/remove_hud() + owner.current.hud_used.blood_display.invisibility = INVISIBILITY_ABSTRACT + owner.current.hud_used.vamprank_display.invisibility = INVISIBILITY_ABSTRACT + owner.current.hud_used.sunlight_display.invisibility = INVISIBILITY_ABSTRACT + +/atom/movable/screen/bloodsucker + icon = 'icons/mob/actions/actions_bloodsucker.dmi' + invisibility = INVISIBILITY_ABSTRACT + +/atom/movable/screen/bloodsucker/proc/clear() + invisibility = INVISIBILITY_ABSTRACT + +/atom/movable/screen/bloodsucker/proc/update_counter() + invisibility = 0 + +/atom/movable/screen/bloodsucker/blood_counter + name = "Blood Consumed" + icon_state = "blood_display" + screen_loc = ui_blood_display + +/atom/movable/screen/bloodsucker/rank_counter + name = "Bloodsucker Rank" + icon_state = "rank" + screen_loc = ui_vamprank_display + +/atom/movable/screen/bloodsucker/sunlight_counter + name = "Solar Flare Timer" + icon_state = "sunlight_night" + screen_loc = ui_sunlight_display + +/// Update Blood Counter + Rank Counter +/datum/antagonist/bloodsucker/proc/update_hud(updateRank = FALSE) + if(!owner.current.hud_used) + return + var/valuecolor + if(owner.current.hud_used && owner.current.hud_used.blood_display) + if(owner.current.blood_volume > BLOOD_VOLUME_SAFE(owner.current)) + valuecolor = "#FFDDDD" + else if(owner.current.blood_volume > BLOOD_VOLUME_BAD(owner.current)) + valuecolor = "#FFAAAA" + owner.current.hud_used.blood_display.update_counter(owner.current.blood_volume, valuecolor) + if(owner.current.hud_used && owner.current.hud_used.vamprank_display) + owner.current.hud_used.vamprank_display.update_counter(bloodsucker_level, valuecolor) + /// Only change icon on special request. + if(updateRank) + owner.current.hud_used.vamprank_display.icon_state = (bloodsucker_level_unspent > 0) ? "rank_up" : "rank" + +/// Update Sun Time +/datum/antagonist/bloodsucker/proc/update_sunlight(value, amDay = FALSE) + if(!owner.current.hud_used) + return + var/valuecolor + if(owner.current.hud_used && owner.current.hud_used.sunlight_display) + var/sunlight_display_icon = "sunlight_" + if(amDay) + sunlight_display_icon += "day" + valuecolor = "#FF5555" + else + switch(round(value, 1)) + if(0 to 30) + sunlight_display_icon += "30" + valuecolor = "#FFCCCC" + if(31 to 60) + sunlight_display_icon += "60" + valuecolor = "#FFE6CC" + if(61 to 90) + sunlight_display_icon += "90" + valuecolor = "#FFFFCC" + else + sunlight_display_icon += "night" + valuecolor = "#FFFFFF" + + var/value_string = (value >= 60) ? "[round(value / 60, 1)] m" : "[round(value, 1)] s" + owner.current.hud_used.sunlight_display.update_counter(value_string, valuecolor) + owner.current.hud_used.sunlight_display.icon_state = sunlight_display_icon + +/atom/movable/screen/bloodsucker/blood_counter/update_counter(value, valuecolor) + ..() + maptext = "
[round(value,1)]
" + +/atom/movable/screen/bloodsucker/rank_counter/update_counter(value, valuecolor) + ..() + maptext = "
[round(value,1)]
" + +/atom/movable/screen/bloodsucker/sunlight_counter/update_counter(value, valuecolor) + ..() + maptext = "
[value]
" diff --git a/code/modules/antagonists/bloodsuckers/bloodsuckers_objects.dm b/code/modules/antagonists/bloodsuckers/bloodsuckers_objects.dm new file mode 100644 index 000000000000..a47a0d79b506 --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/bloodsuckers_objects.dm @@ -0,0 +1,437 @@ +////////////////////// +// BLOODBAG // +////////////////////// + +/// Taken from drinks.dm +/obj/item/reagent_containers/blood/attack(mob/user, mob/user, def_zone) + if(reagents.total_volume > 0) + if(user != user) + user.visible_message( + span_notice("[user] forces [user] to drink from the [src]."), + span_notice("You put the [src] up to [user]'s mouth."), + ) + if(!do_mob(user, user, 5 SECONDS)) + return + else + if(!do_mob(user, user, 1 SECONDS)) + return + user.visible_message( + span_notice("[user] puts the [src] up to their mouth."), + span_notice("You take a sip from the [src]."), + ) + // Safety: In case you spam clicked the blood bag on yourself, and it is now empty (below will divide by zero) + if(reagents.total_volume <= 0) + return + var/gulp_size = 5 + reagents.trans_to(user, gulp_size, transfered_by = user, methods = INGEST) + playsound(user.loc, 'sound/items/drink.ogg', rand(10,50), 1) + . = ..() + +////////////////////// +// HEART // +////////////////////// + +/// Do I have any parts that need replacing? +/* // Removed - Replaced with HealVampireOrgans() +/datum/antagonist/bloodsucker/proc/CheckVampOrgans() + var/obj/item/organ/heart/vampiricheart = owner.current.getorganslot(ORGAN_SLOT_HEART) + if(!istype(vampiricheart, /obj/item/organ/heart/vampheart) || !istype(vampiricheart, /obj/item/organ/heart/demon) || !istype(vampiricheart, /obj/item/organ/heart/cursed)) + qdel(vampiricheart) + var/obj/item/organ/heart/vampheart/vampiricheart = new + vampiricheart.Insert(owner.current) + /// Now... stop beating! + vampiricheart.Stop() +*/ +/datum/antagonist/bloodsucker/proc/RemoveVampOrgans() + var/obj/item/organ/heart/newheart = owner.current.getorganslot(ORGAN_SLOT_HEART) + if(newheart) + qdel(newheart) + newheart = new() + newheart.Insert(owner.current) + +// HEART: OVERWRITE // +// HEART // +/obj/item/organ/heart/vampheart + beating = 0 + var/fakingit = 0 + +/obj/item/organ/heart/vampheart/Restart() + beating = 0 // DONT run ..() -- We don't want to start beating again. + return 0 + +/obj/item/organ/heart/vampheart/Stop() + fakingit = 0 + return ..() + +/obj/item/organ/heart/vampheart/proc/FakeStart() + fakingit = 1 // We're pretending to beat, to fool people. + +/// Bloodsuckers don't have a heartbeat at all when stopped (default is "an unstable") +/obj/item/organ/heart/vampheart/HeartStrengthMessage() + if(fakingit) + return "a healthy" + return span_danger("no") + +/// Proc for the default (Non-Bloodsucker) Heart! +/obj/item/organ/heart/proc/HeartStrengthMessage() + if(beating) + return "a healthy" + return span_danger("an unstable") + +////////////////////// +// EYES // +////////////////////// + +/* /// Removed due to Mothpeople & Flypeople spawning with Vampiric eyes, getting them instantly lynched. +/// Taken from augmented_eyesight.dm +/obj/item/organ/eyes/bloodsucker + lighting_alpha = 180 // LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE <--- This is too low a value at 128. We need to SEE what the darkness is so we can hide in it. + see_in_dark = 12 + sight_flags = SEE_MOBS // Bloodsuckers are predators, and detect life/heartbeats nearby. -2019 Breakdown of Bloodsuckers + flash_protect = -1 // These eyes are weaker to flashes, but let you see in the dark +*/ + +/* +////////////////////// +// LIVER // +////////////////////// + +/// Livers run on_life(), which calls reagents.metabolize() in holder.dm, which calls on_mob_life.dm in the cheam (medicine_reagents.dm) +/obj/item/organ/liver/vampliver +/obj/item/organ/liver/vampliver/on_life() + var/mob/living/carbon/user = owner + if(!istype(user)) + return +*/ + +////////////////////// +// STAKES // +////////////////////// + +/* + * NOTE: sheet_types.dm is where the WOOD stack lives. Maybe move this over there. + * Taken from /obj/item/stack/rods/attackby in [rods.dm] + */ + +/// Crafting +/obj/item/stack/sheet/mineral/wood/attackby(obj/item/item, mob/user, params) + if(item.get_sharpness()) + 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) + if(istype(item, merge_type)) + var/obj/item/stack/merged_stack = item + if(merge(merged_stack)) + to_chat(user, span_notice("Your [merged_stack.name] stack now contains [merged_stack.get_amount()] [merged_stack.singular_name]\s.")) + else + . = ..() + +/// Do I have a stake in my heart? +/mob/living/AmStaked() + 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 + +/mob/proc/AmStaked() + return FALSE + +/// You can't go to sleep in a coffin with a stake in you. +/mob/living/proc/StakeCanKillMe() + return IsSleeping() || stat >= UNCONSCIOUS || blood_volume <= 0 || HAS_TRAIT(src, TRAIT_NODEATH) + +/obj/item/stake + name = "wooden stake" + desc = "A simple wooden stake carved to a sharp point." + icon = 'icons/obj/stakes.dmi' + icon_state = "wood" + lefthand_file = 'icons/mob/inhands/antag/bs_leftinhand.dmi' + righthand_file = 'icons/mob/inhands/antag/bs_rightinhand.dmi' + slot_flags = ITEM_SLOT_BELT + w_class = WEIGHT_CLASS_SMALL + hitsound = 'sound/weapons/bladeslice.ogg' + attack_verb = list("staked", "stabbed", "tore into") + /// Embedding + sharpness = SHARP_EDGED + embedding = list("embedded_pain_multiplier" = 4, "embed_chance" = 20, "embedded_fall_chance" = 10) + 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/afterattack(mob/living/carbon/target, mob/living/user, proximity, discover_after = TRUE) + // Invalid Target, or not targetting the chest? + if(check_zone(user.zone_selected) != BODY_ZONE_CHEST) + return + // Needs to be Down/Slipped in some way to Stake. + if(!target.can_be_staked() || target == user) // 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_mob(user, target, staketime, extra_checks = CALLBACK(target, /mob/living/carbon.proc/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) + user.dropItemToGround(src, TRUE) //user.drop_item() // "drop item" doesn't seem to exist anymore. New proc is user.dropItemToGround() but it doesn't seem like it's needed now? + 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.FinalDeath() + else + to_chat(target, span_userdanger("You have been staked! Your powers are useless, your death forever, while it remains in place.")) + to_chat(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/carbon/proc/can_be_staked() + return !(mobility_flags & MOBILITY_MOVE) + +/// Created by welding and acid-treating a simple stake. +/obj/item/stake/hardened + name = "hardened stake" + desc = "A hardened wooden stake carved to a sharp point and scorched at the end." + 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" + 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 // +////////////////////// + +/* + * # 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. + * It is also Tremere's Clan objective to ensure a Tremere Bloodsucker has stolen this by the end of the round. + */ + +/obj/item/book/codex_gigas/Initialize(mapload) + . = ..() + var/turf/current_turf = get_turf(src) + new /obj/item/book/kindred(current_turf) + +/obj/item/book/kindred + name = "\improper Archive of the Kindred" + 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 = 'icons/obj/vamp_obj.dmi' + lefthand_file = 'icons/mob/inhands/antag/bs_rightinhand.dmi' + righthand_file = 'icons/mob/inhands/antag/bs_rightinhand.dmi' + icon_state = "kindred_book" + author = "dozens of generations of Curators" + unique = TRUE + throw_speed = 1 + throw_range = 10 + resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF + var/in_use = FALSE + +/obj/item/book/kindred/Initialize() + . = ..() + AddComponent(/datum/component/stationloving, FALSE, TRUE) + +// Overwriting attackby to prevent cutting the book out +/obj/item/book/kindred/attackby(obj/item/item, mob/user, params) + // Copied from '/obj/item/book/attackby(obj/item/item, mob/user, params)' + if((istype(item, /obj/item/kitchen/knife) || item.tool_behaviour == TOOL_WIRECUTTER) && !(flags_1 & HOLOGRAM_1)) + to_chat(user, span_notice("You feel the gentle whispers of a Librarian telling you not to cut [title].")) + return + . = ..() + +/* + * # Attacking someone with the Book + */ +// target is the person being hit here +/obj/item/book/kindred/afterattack(mob/living/target, mob/living/user, flag, params) + . = ..() + if(!user.can_read(src)) + return + // Curator/Tremere using it + if(HAS_TRAIT(user, TRAIT_BLOODSUCKER_HUNTER)) + if(in_use || (target == user) || !ismob(target)) + return + user.visible_message(span_notice("[user] begins to quickly look through [src], repeatedly looking back up at [target].")) + in_use = TRUE + if(!do_mob(user, target, 3 SECONDS, NONE, 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.ReturnFullName(TRUE)]', is indeed a [bloodsuckerdatum.my_clan] Bloodsucker, but you already knew this.")) + return + if(bloodsuckerdatum.my_clan != NONE) + to_chat(user, span_warning("You found the one! [target], also known as '[bloodsuckerdatum.ReturnFullName(TRUE)]', is part of the [bloodsuckerdatum.my_clan]! You quickly note this information down, memorizing it.")) + else + to_chat(user, span_warning("You found the one! [target], also known as '[bloodsuckerdatum.ReturnFullName(TRUE)]', is not knowingly 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.")) + // Bloodsucker using it + else if(IS_BLOODSUCKER(user)) + 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.")) + else + to_chat(user, span_warning("[src] burns your hands as you try to use it!")) + user.apply_damage(12, BURN, pick(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM)) + +/* + * # Reading the Book + */ +/obj/item/book/kindred/attack_self(mob/living/carbon/user) +// Don't call parent since it handles reading the book. +// . = ..() + if(!user.can_read(src)) + return + // Curator/Tremere using it + if(HAS_TRAIT(user, TRAIT_BLOODSUCKER_HUNTER)) + user.visible_message(span_notice("[user] opens [src] and begins reading intently.")) + ui_interact(user) + return + // Bloodsucker using it + else if(IS_BLOODSUCKER(user)) + 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 + to_chat(user, span_warning("You feel your eyes burn as you begin to read through [src]!")) + var/obj/item/organ/eyes/eyes = user.getorganslot(ORGAN_SLOT_EYES) + user.blur_eyes(10) + eyes.applyOrganDamage(5) + +/obj/item/book/kindred/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "KindredArchives", name) + ui.open() + +/obj/item/book/kindred/ui_act(action, params) + . = ..() + if(.) + return + if(!action) + return FALSE + SStgui.close_uis(src) + if(action == "search_brujah") + INVOKE_ASYNC(src, .proc/search, usr, CLAN_BRUJAH) + if(action == "search_toreador") + INVOKE_ASYNC(src, .proc/search, usr, CLAN_TOREADOR) + if(action == "search_nosferatu") + INVOKE_ASYNC(src, .proc/search, usr, CLAN_NOSFERATU) + if(action == "search_tremere") + INVOKE_ASYNC(src, .proc/search, usr, CLAN_TREMERE) + if(action == "search_gangrel") + INVOKE_ASYNC(src, .proc/search, usr, CLAN_GANGREL) + if(action == "search_ventrue") + INVOKE_ASYNC(src, .proc/search, usr, CLAN_VENTRUE) + if(action == "search_malkavian") + INVOKE_ASYNC(src, .proc/search, usr, CLAN_MALKAVIAN) + +/obj/item/book/kindred/proc/search(mob/reader, clan) + dat = "List of information gathered on the [clan]:
" + if(clan == CLAN_BRUJAH) + dat += "This Clan has proven to be the strongest in melee combat, boasting a powerful punch.
\ + They also appear to be more calm than the others, entering their 'frenzies' whenever they want, but dont seem affected.
\ + Be wary, as they are fearsome warriors, rebels and anarchists, with an inclination towards Frenzy.
\ + Favorite Vassal: Their favorite Vassal gains the Brawn ability. \ + Strength: Frenzy will not kill them, punches deal a lot of damage.
\ + Weakness: They have to spend Blood on powers while in Frenzy too." + if(clan == CLAN_TOREADOR) // Flavortext only + dat += "The most charming Clan of them all, being borderline party animals, allowing them to very easily disguise among the crew.
\ + They are more in touch with their morals, so they suffer and benefit more strongly from the humanity cost or gain of their actions.
\ + They can be best defined as 'The most humane kind of vampire', due to their kindred with an obsession with perfectionism and beauty
\ + Favorite Vassal: Their favorite Vassal gains the Mesmerize ability \ + Strength: Highly charismatic and influential.
\ + Weakness: Physically and Morally weak." + if(clan == CLAN_NOSFERATU) + dat += "This Clan has been the most obvious to find information about.
\ + They are disfigured, ghoul-like vampires upon embrace by their Sire, scouts that travel through desolate paths to avoid violating the Masquerade.
\ + They make no attempts at hiding themselves within the crew, and have a terrible taste for heavy items.
\ + They also seem to manage to fit themsleves into small spaces such as vents.
\ + Favorite Vassal: Their Favorite Vassal gains the ability to ventcrawl while naked and becomes disfigured. \ + Strength: Ventcrawl.
\ + Weakness: Can't disguise themselves, permanently pale, can easily be discovered by their DNA or Blood Level." + if(clan == CLAN_TREMERE) + dat += "This Clan seems to hate entering the Chapel.
\ + They are a secluded Clan, they are Vampires who've mastered the power of blood, and seek knowledge.
\ + They appear to be focused more on their Blood Magic than their other Powers, getting stronger faster the more Vassals they have.
\ + They have 3 different paths they can take, from reviving people as Vassals, to stealing blood with beams made of the same essence.
\ + Favorite Vassal: Their Favorite Vassal gains the ability to shift into a Bat at will. \ + Strength: 3 different Powers that get stupidly strong overtime.
\ + Weakness: Cannot get regular Powers, with no way to get stun resistance outside of Frenzy." + if(clan == CLAN_GANGREL) // Flavortext only + dat += "This Clan seems to be closer to Animals than to other Vampires.
\ + They also go by the name of Werewolves, as that is what appears when they enter a Frenzy.
\ + Despite this, they appear to be scared of 'True Faith', someone's ultimate and undying Faith, which itself doesn't require being something Religious.
\ + They hate seeing many people, and tend to avoid Stations that have more crewmembers than Nanotrasen's average. Due to this, they are harder to find than others.
\ + Favorite Vassal: Their Favorite Vassal turns into a Werewolf whenever their Master does.. \ + Strength: Feral, Werewolf during Frenzy.
\ + Weakness: Weak to True Faith." + if(clan == CLAN_VENTRUE) + dat += "This Clan seems to despise drinking from non sentient organics.
\ + They are Masters of manipulation, Greedy and entitled. Authority figures between the kindred society.
\ + They seem to take their Vassal's lives very seriously, going as far as to give Vassals some of their own Blood.
\ + Compared to other types, this one relies on their Vassals, rather than fighting for themselves.
\ + Favorite Vassal: Their Favorite Vassal will slowly be turned into a Bloodsucker overtime. \ + Strength: Slowly turns a Vassal into a Bloodsucker.
\ + Weakness: Does not gain more abilities overtime, it is best to target the Bloodsucker over the Vassal." + if(clan == CLAN_MALKAVIAN) + dat += "There is barely any information known about this Clan.
\ + Members of this Clan seems to mumble things to themselves, unaware of their surroundings.
\ + They also seem to enter and dissapear into areas randomly, as if not even they know where they are.
\ + Favorite Vassal: Unknown. \ + Strength: Unknown.
\ + Weakness: Unknown." + + reader << browse("Penned by [author].
" + "[dat]", "window=book[window_size != null ? ";size=[window_size]" : ""]") diff --git a/code/modules/antagonists/bloodsuckers/powers/_powers.dm b/code/modules/antagonists/bloodsuckers/powers/_powers.dm new file mode 100644 index 000000000000..c68966f9ee07 --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/powers/_powers.dm @@ -0,0 +1,211 @@ +/datum/action/bloodsucker + name = "Vampiric Gift" + desc = "A vampiric gift." + //This is the FILE for the background icon + button_icon = 'icons/mob/actions/actions_bloodsucker.dmi' + //This is the ICON_STATE for the background icon + background_icon_state = "vamp_power_off" + var/background_icon_state_on = "vamp_power_on" + var/background_icon_state_off = "vamp_power_off" + icon_icon = 'icons/mob/actions/actions_bloodsucker.dmi' + button_icon_state = "power_feed" + buttontooltipstyle = "cult" + + /// The text that appears when using the help verb, meant to explain how the Power changes when ranking up. + var/power_explanation = "" + ///The owner's stored Bloodsucker datum + var/datum/antagonist/bloodsucker/bloodsuckerdatum_power + + // FLAGS // + /// The effects on this Power (Toggled/Single Use/Static Cooldown) + var/power_flags = BP_AM_TOGGLE|BP_AM_SINGLEUSE|BP_AM_STATIC_COOLDOWN|BP_AM_COSTLESS_UNCONSCIOUS + /// Requirement flags for checks + check_flags = BP_CANT_USE_IN_TORPOR|BP_CANT_USE_IN_FRENZY|BP_CANT_USE_WHILE_STAKED|BP_CANT_USE_WHILE_INCAPACITATED|BP_CANT_USE_WHILE_UNCONSCIOUS + /// Who can purchase the Power + var/purchase_flags = NONE // BLOODSUCKER_CAN_BUY|TREMERE_CAN_BUY|VASSAL_CAN_BUY|HUNTER_CAN_BUY + + // COOLDOWNS // + ///Timer between Power uses. + COOLDOWN_DECLARE(bloodsucker_power_cooldown) + + // VARS // + /// If the Power is currently active. + var/active = FALSE + /// Cooldown you'll have to wait between each use, decreases depending on level. + var/cooldown = 2 SECONDS + ///Can increase to yield new abilities - Each Power ranks up each Rank + var/level_current = 0 + ///The cost to ACTIVATE this Power + var/bloodcost = 0 + ///The cost to MAINTAIN this Power - Only used for Constant Cost Powers + var/constant_bloodcost = 0 + +// Modify description to add cost. +/datum/action/bloodsucker/New(Target) + . = ..() + if(bloodcost > 0) + desc += "

COST: [bloodcost] Blood" + if(constant_bloodcost > 0) + desc += "

CONSTANT COST: [name] costs [constant_bloodcost] Blood maintain active." + if(power_flags & BP_AM_SINGLEUSE) + desc += "

SINGLE USE:
[name] can only be used once per night." + +/datum/action/bloodsucker/Destroy() + bloodsuckerdatum_power = null + return ..() + +/datum/action/bloodsucker/IsAvailable() + return TRUE + +/datum/action/bloodsucker/Grant(mob/user) + . = ..() + var/datum/antagonist/bloodsucker/bloodsuckerdatum = IS_BLOODSUCKER(owner) + if(bloodsuckerdatum) + bloodsuckerdatum_power = bloodsuckerdatum + +/** + * # NOTES + * + * click.dm <--- Where we can take over mouse clicks + * spells.dm /add_ranged_ability() <--- How we take over the mouse click to use a power on a target. + */ + +//This is when we CLICK on the ability Icon, not USING. +/datum/action/bloodsucker/Trigger(trigger_flags) + if(active && CheckCanDeactivate()) // Active? DEACTIVATE AND END! + DeactivatePower() + return FALSE + if(!CheckCanPayCost() || !CheckCanUse(owner)) + return FALSE + PayCost() + ActivatePower() + if(power_flags & BP_AM_SINGLEUSE) + RemoveAfterUse() + return TRUE + if(!(power_flags & BP_AM_TOGGLE) || !active) + StartCooldown() // Must come AFTER UpdateButtonIcon(), otherwise icon will revert! + return TRUE + +/datum/action/bloodsucker/proc/CheckCanPayCost() + if(!owner || !owner.mind) + return FALSE + // Cooldown? + if(!COOLDOWN_FINISHED(src, bloodsucker_power_cooldown)) + to_chat(owner, "[src] on cooldown!") + return FALSE + // Have enough blood? Bloodsuckers in a Frenzy don't need to pay them + var/mob/living/user = owner + if(bloodsuckerdatum_power?.frenzied) + return TRUE + if(user.blood_volume < bloodcost) + to_chat(owner, span_warning("You need at least [bloodcost] blood to activate [name]")) + return FALSE + return TRUE + +///Checks if the Power is available to use. +/datum/action/bloodsucker/proc/CheckCanUse(mob/living/carbon/user) + if(!owner) + return FALSE + if(!isliving(user)) + return FALSE + // Torpor? + if((check_flags & BP_CANT_USE_IN_TORPOR) && HAS_TRAIT(user, TRAIT_NODEATH)) + 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 && bloodsuckerdatum_power?.my_clan != CLAN_BRUJAH)) + 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.AmStaked()) + 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 = TRUE, ignore_grab = TRUE))) + to_chat(user, span_warning("Not while you're incapacitated!")) + return FALSE + // Constant Cost (out of blood) + if(constant_bloodcost > 0 && user.blood_volume <= 0) + to_chat(user, span_warning("You don't have the blood to upkeep [src].")) + return FALSE + return TRUE + +/// NOTE: With this formula, you'll hit half cooldown at level 8 for that power. +/datum/action/bloodsucker/proc/StartCooldown() + // Alpha Out + button.color = rgb(128,0,0,128) + button.alpha = 100 + // Calculate Cooldown (by power's level) + var/this_cooldown + if(power_flags & BP_AM_STATIC_COOLDOWN) + this_cooldown = cooldown + else + this_cooldown = max(cooldown / 2, cooldown - (cooldown / 16 * (level_current-1))) + + // Wait for cooldown + COOLDOWN_START(src, bloodsucker_power_cooldown, this_cooldown) + addtimer(CALLBACK(src, .proc/alpha_in), this_cooldown) + +/datum/action/bloodsucker/proc/alpha_in() + button.color = rgb(255,255,255,255) + button.alpha = 255 + +/datum/action/bloodsucker/proc/CheckCanDeactivate() + return TRUE + +/datum/action/bloodsucker/UpdateButtonIcon(force = FALSE) + background_icon_state = active ? background_icon_state_on : background_icon_state_off + . = ..() + +/datum/action/bloodsucker/proc/PayCost() + // Bloodsuckers in a Frenzy don't have enough Blood to pay it, so just don't. + if(bloodsuckerdatum_power?.frenzied) + return + var/mob/living/carbon/human/user = owner + user.blood_volume -= bloodcost + bloodsuckerdatum_power?.update_hud() + +/datum/action/bloodsucker/proc/ActivatePower() + if(power_flags & BP_AM_TOGGLE) + active = TRUE + RegisterSignal(owner, COMSIG_LIVING_BIOLOGICAL_LIFE, .proc/UsePower) + + owner.log_message("used [src].", LOG_ATTACK, color="red") + UpdateButtonIcon() + +/datum/action/bloodsucker/proc/DeactivatePower() + if(power_flags & BP_AM_TOGGLE) + UnregisterSignal(owner, COMSIG_LIVING_BIOLOGICAL_LIFE) + active = FALSE + UpdateButtonIcon() + StartCooldown() + +///Used by powers that are continuously active (That have BP_AM_TOGGLE flag) +/datum/action/bloodsucker/proc/UsePower(mob/living/user) + if(!active) // Power isn't active? Then stop here, so we dont keep looping UsePower for a non existent Power. + return FALSE + if(!ContinueActive(user)) // We can't afford the Power? Deactivate it. + DeactivatePower() + return FALSE + // We can keep this up (For now), so Pay Cost! + if(!(power_flags & BP_AM_COSTLESS_UNCONSCIOUS) && user.stat != CONSCIOUS) + bloodsuckerdatum_power?.AddBloodVolume(-constant_bloodcost) + return TRUE + +/// Checks to make sure this power can stay active +/datum/action/bloodsucker/proc/ContinueActive(mob/living/user, mob/living/target) + if(!active) + return FALSE + if(!user) + return FALSE + if(!constant_bloodcost > 0 || user.blood_volume > 0) + return TRUE + +/// Used to unlearn Single-Use Powers +/datum/action/bloodsucker/proc/RemoveAfterUse() + bloodsuckerdatum_power?.powers -= src + Remove(owner) diff --git a/code/modules/antagonists/bloodsuckers/powers/brujah.dm b/code/modules/antagonists/bloodsuckers/powers/brujah.dm new file mode 100644 index 000000000000..eb74489f25be --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/powers/brujah.dm @@ -0,0 +1,33 @@ +/datum/action/bloodsucker/brujah + name = "Frenzy" + desc = "Allow the Monster deep-inside of you, run free." + button_icon_state = "power_brujah" + power_explanation = "Frenzy:\n\ + A Brujah only Power. Activating it will make you enter a Frenzy.\n\ + When in a Frenzy, you get extra stun resistance, slowly gain brute damage, move faster, become mute/deaf,\n\ + and become unable to use complicated machinery as your screen goes blood-red." + power_flags = BP_AM_TOGGLE|BP_AM_STATIC_COOLDOWN + check_flags = BP_AM_COSTLESS_UNCONSCIOUS + purchase_flags = NONE + bloodcost = 2 + cooldown = 10 SECONDS + +/datum/action/bloodsucker/brujah/ActivatePower() + if(active && bloodsuckerdatum_power && bloodsuckerdatum_power.frenzied) + to_chat(owner, "already in a frenzy!") + return FALSE + var/mob/living/user = owner + user.apply_status_effect(STATUS_EFFECT_FRENZY) + . = ..() + +/datum/action/bloodsucker/brujah/DeactivatePower() + . = ..() + var/mob/living/user = owner + user.remove_status_effect(STATUS_EFFECT_FRENZY) + +/datum/action/bloodsucker/brujah/CheckCanDeactivate() + var/mob/living/user = owner + if(user.blood_volume < FRENZY_THRESHOLD_EXIT) + to_chat(owner, "not enough blood!") + return FALSE + . = ..() diff --git a/code/modules/antagonists/bloodsuckers/powers/cloak.dm b/code/modules/antagonists/bloodsuckers/powers/cloak.dm new file mode 100644 index 000000000000..ef12f7b3caf0 --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/powers/cloak.dm @@ -0,0 +1,68 @@ +/datum/action/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 + cooldown = 5 SECONDS + var/was_running + +/// Must have nobody around to see the cloak +/datum/action/bloodsucker/cloak/CheckCanUse(mob/living/carbon/user) + . = ..() + if(!.) + return FALSE + for(var/mob/living/watchers in viewers(9, owner) - owner) + to_chat(owner, "you can only vanish unseen.") + return FALSE + return TRUE + +/datum/action/bloodsucker/cloak/ActivatePower() + . = ..() + var/mob/living/user = owner + was_running = (user.m_intent == MOVE_INTENT_RUN) + if(was_running) + user.toggle_move_intent() + user.digitalinvis = 1 + user.digitalcamo = 1 + to_chat(user, "cloak turned on.") + +/datum/action/bloodsucker/cloak/UsePower(mob/living/user) + // Checks that we can keep using this. + . = ..() + if(!.) + return + 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) + to_chat(owner, "you attempt to run, crushing yourself.") + user.toggle_move_intent() + user.adjustBruteLoss(rand(5,15)) + +/datum/action/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/bloodsucker/cloak/DeactivatePower() + . = ..() + var/mob/living/user = owner + animate(user, alpha = 255, time = 1 SECONDS) + user.digitalinvis = 0 + user.digitalcamo = 0 + if(was_running && user.m_intent == MOVE_INTENT_WALK) + user.toggle_move_intent() + to_chat(user, "cloak turned off.") diff --git a/code/modules/antagonists/bloodsuckers/powers/distress.dm b/code/modules/antagonists/bloodsuckers/powers/distress.dm new file mode 100644 index 000000000000..fd346a33904f --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/powers/distress.dm @@ -0,0 +1,22 @@ +/datum/action/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 instnatly be alerted of your location." + power_flags = NONE + check_flags = NONE + purchase_flags = NONE + bloodcost = 10 + cooldown = 10 SECONDS + +/datum/action/bloodsucker/distress/ActivatePower() + . = ..() + var/turf/open/floor/target_area = get_area(owner) + var/datum/antagonist/vassal/vassaldatum = owner.mind.has_antag_datum(/datum/antagonist/vassal) + + to_chat(owner, "you call out for your master!") + to_chat(vassaldatum.master.owner, "[owner], your loyal Vassal, is desperately calling for aid at [target_area]!") + + var/mob/living/user = owner + user.adjustBruteLoss(10) diff --git a/code/modules/antagonists/bloodsuckers/powers/feed.dm b/code/modules/antagonists/bloodsuckers/powers/feed.dm new file mode 100644 index 000000000000..b496e54b9d86 --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/powers/feed.dm @@ -0,0 +1,367 @@ +/datum/action/bloodsucker/feed + name = "Feed" + desc = "Draw the heartsblood of living victims in your grasp. You will break the Masquerade if seen feeding." + 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\ + If passively grabbed, you will feed faster than default.\n\ + If aggressively grabbed, along with drinking even faster, your victim will additionally be put to sleep.\n\ + You cannot talk while Feeding, as your mouth is full of Blood.\n\ + If you feed off of a Rat, unless you are Malkavian or Nosferatu, you will lose Humanity and get a mood debuff.\n\ + Feeding off of someone until they die will cause you to lose Humanity.\n\ + If you are seen feeding off of someone (2 tiles) while your target is grabbed, you will break the Masquerade.\n\ + Higher levels will increase the feeding's speed." + 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 + bloodcost = 0 + cooldown = 3 SECONDS + + ///Amount of times we were seen Feeding. If seen 3 times, we broke the Masquerade. + var/feeds_noticed = 0 + ///Distance before silent feeding is noticed. + var/notice_range = 2 + ///Check if we were noticed Feeding. + var/was_noticed = FALSE + ///So we can validate more than just the guy we're grappling. + var/mob/living/feed_target + ///If you started grappled, then ending it will end your Feed. + var/target_grappled = FALSE + ///Am I Silent? + var/amSilent = FALSE + ///How much Blood did I drink? This is used for logs + var/amount_taken = 0 + ///The initial wait before you start drinking blood. + var/feed_time + ///Quantity to take per tick, based on Silent/frenzied or not. + var/blood_take_mult + /// CHECKS - To prevent spam. + var/warning_target_inhuman = FALSE + var/warning_target_dead = FALSE + var/warning_full = FALSE + var/warning_target_bloodvol = 99999 + var/was_alive = FALSE + +/datum/action/bloodsucker/feed/CheckCanUse(mob/living/carbon/user) + . = ..() + if(!.) + return FALSE + + // Wearing mask + if(user.is_mouth_covered()) + to_chat(owner, "your mouth is covered!") + return FALSE + // Find my Target! + if(!find_target()) + return FALSE + // DONE! + return TRUE + +/// Called twice: validating a subtle victim, or validating your grapple victim. +/datum/action/bloodsucker/feed/proc/ValidateTarget(mob/living/target) + // Must have Target. + if(!target)//|| !ismob(target) + to_chat(owner, span_warning("You must be next to or grabbing a victim to feed from them.")) + return FALSE + // Not even living! + if(!isliving(target) || issilicon(target)) + to_chat(owner, span_warning("You may only feed from living beings.")) + return FALSE + // Is a Mouse on an Invalid Clan. + if(istype(target, /mob/living/simple_animal/mouse)) + if(bloodsuckerdatum_power.my_clan == CLAN_VENTRUE) + to_chat(owner, span_warning("The thought of feeding off of a dirty rat leaves your stomach aching.")) + return FALSE + // Check for other animals (Supposed to be after Mouse so Mouse can skip over it) + else if(!iscarbon(target)) + to_chat(owner, span_warning("Such simple beings cannot be fed off of.")) + return FALSE + // Has no blood to take! + else if(target.blood_volume <= 0) + to_chat(owner, span_warning("Your victim has no blood to take.")) + return FALSE + // Bloodsuckers can be fed off of if they are grabbed more than Passively. + if(IS_BLOODSUCKER(target) && target == owner.pulling && owner.grab_state <= GRAB_PASSIVE) + to_chat(owner, span_warning("Other Bloodsuckers will not fall for your subtle approach.")) + return FALSE + if(ishuman(target)) + var/mob/living/carbon/human/target_user = target + if(!target_user.can_inject(owner, BODY_ZONE_HEAD, 1) && target == owner.pulling && owner.grab_state < GRAB_AGGRESSIVE) + to_chat(owner, span_warning("Their suit is too thick to feed through.")) + return FALSE + if(NOBLOOD in target_user.dna.species.species_traits)// || owner.get_blood_id() != target.get_blood_id()) + to_chat(owner, span_warning("Your victim's blood is not suitable for you to take.")) + return FALSE + // Special Check: If you're part of the Ventrue clan, they can't be mindless! + if(bloodsuckerdatum_power.my_clan == CLAN_VENTRUE && !bloodsuckerdatum_power.frenzied) + if(!target.mind) + to_chat(owner, span_warning("The thought of drinking blood from the mindsless leaves a distasteful feeling in your mouth.")) + return FALSE + return TRUE + +/// If I'm not grabbing someone, find me someone nearby. +/datum/action/bloodsucker/feed/proc/find_target() + // Default + feed_target = null + target_grappled = FALSE + // If you are pulling a mob, that's your target. If you don't like it, then release them. + if(owner.pulling && ismob(owner.pulling)) + // Check grapple target Valid + if(!ValidateTarget(owner.pulling)) // Grabbed targets display error. + return FALSE + target_grappled = TRUE + feed_target = owner.pulling + return TRUE + // Find Targets + var/list/mob/living/seen_mobs = list() + for(var/mob/living/watchers in view(1, owner) - owner) + if(!isliving(watchers)) + continue + seen_mobs |= watchers + // None Seen! + if(!seen_mobs.len) + to_chat(owner, span_warning("You must be next to or grabbing a victim to feed from them.")) + return FALSE + // Check Valids... + var/list/targets_valid = list() + var/list/targets_dead = list() + for(var/mob/living/watchers in seen_mobs) + // Check adjecent Valid target + if(watchers != owner && ValidateTarget(watchers)) // Do NOT display errors. We'll be doing this again in CheckCanUse(), which will rule out grabbed targets. + // Prioritize living, but remember dead as backup + if(watchers.stat < DEAD) + targets_valid |= watchers + else + targets_dead |= watchers + // No Living? Try dead. + if(!targets_valid.len && targets_dead.len) + targets_valid = targets_dead + // No Targets + if(!targets_valid.len) + // Did I see targets? Then display at least one error + if(seen_mobs.len > 1) + to_chat(owner, span_warning("None of these are valid targets to feed from subtly.")) + else + ValidateTarget(seen_mobs[1]) + return FALSE + else + feed_target = pick(targets_valid) + return TRUE + +/datum/action/bloodsucker/feed/ActivatePower() + . = ..() + var/mob/living/user = owner + // Checks: Step 1 - Am I SECRET or LOUD? + if(!bloodsuckerdatum_power.frenzied && (!target_grappled || owner.grab_state <= GRAB_PASSIVE)) // && iscarbon(target) // Non-carbons (animals) not passive. They go straight into aggressive. + amSilent = TRUE + + // Checks: Step 2 - Is it a Mouse? + if(istype(feed_target, /mob/living/simple_animal/mouse)) + var/mob/living/simple_animal/mouse_target = feed_target + bloodsuckerdatum_power.AddBloodVolume(25) + to_chat(user, span_notice("You recoil at the taste of a lesser lifeform.")) + if(bloodsuckerdatum_power.my_clan != CLAN_NOSFERATU && bloodsuckerdatum_power.my_clan != CLAN_MALKAVIAN) + SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, "drankblood", /datum/mood_event/drankblood_bad) + bloodsuckerdatum_power.AddHumanityLost(1) + DeactivatePower() + mouse_target.adjustBruteLoss(20) + return + // Checks: Step 3 - How fast should I be and how much should I drink? + var/feed_time_multiplier + if(bloodsuckerdatum_power.frenzied) + blood_take_mult = 2 + feed_time_multiplier = 8 + else if(!amSilent) + blood_take_mult = 1 + feed_time_multiplier = 25 - (2.5 * level_current) + else + blood_take_mult = 0.3 + feed_time_multiplier = 45 - (2.5 * level_current) + feed_time = max(8, feed_time_multiplier) + // Let's check if our target is alive + was_alive = feed_target.stat < DEAD && ishuman(feed_target) + + // Send pre-pull message + if(amSilent) + to_chat(owner, "you quietly lean towards [feed_target]") + else + to_chat(owner, "you pull [feed_target] close to you!") + + // Start the countdown + if(!do_mob(user, feed_target, feed_time, NONE, TRUE)) + to_chat(owner, "your feeding was interrupted!") + DeactivatePower() + return + + // Give them the effects (Depending on if we are silent or not) + if(!amSilent) + // Sleep & paralysis. + ApplyVictimEffects(feed_target) + // Pull target to you if they don't take up space. + if(!feed_target.density) + feed_target.Move(user.loc) + user.visible_message( + span_warning("[user] closes [user.p_their()] mouth around [feed_target]'s neck!"), + span_warning("You sink your fangs into [feed_target]'s neck."), + ) + if(amSilent) + var/deadmessage = feed_target.stat == DEAD ? "" : " [feed_target.p_they(TRUE)] looks dazed, and will not remember this." + user.visible_message( + span_notice("[user] puts [feed_target]'s wrist up to [user.p_their()] mouth."), \ + span_notice("You slip your fangs into [feed_target]'s wrist.[deadmessage]"), \ + vision_distance = notice_range, ignored_mobs = feed_target) // Only people who AREN'T the target will notice this action. + + // Check if we have anyone watching - If there is one, we broke the Masquerade. + for(var/mob/living/watchers in viewers(notice_range, owner) - owner - feed_target) + // Are they someone who will actually report our behavior? + if(watchers.client \ + && !watchers.has_unlimited_silicon_privilege \ + && watchers.stat != DEAD \ + && watchers.eye_blind == 0 \ + && watchers.eye_blurry == 0 \ + && !IS_BLOODSUCKER(watchers) \ + && !IS_VASSAL(watchers) \ + && !HAS_TRAIT(watchers, TRAIT_BLOODSUCKER_HUNTER)) + was_noticed = TRUE + break + if(was_noticed && !target_grappled) + feeds_noticed++ + to_chat(owner, "someone may have noticed...") + if(!bloodsuckerdatum_power.broke_masquerade) + to_chat(user, span_cultbold("You broke the Masquerade [feeds_noticed] time(s), if you break it 3 times, you become a criminal to the Bloodsucker's Cause!")) + else + to_chat(owner, "you think no one saw you...") + + // FEEEEEEEEED!! // + ADD_TRAIT(user, TRAIT_MUTE, BLOODSUCKER_TRAIT) // My mouth is full! + user.Immobilize(10 SECONDS) // Prevents spilling blood accidentally. + +/datum/action/bloodsucker/feed/UsePower(mob/living/user) + if(!ContinueActive(user, feed_target)) + if(amSilent) + to_chat(user, span_warning("Your feeding has been interrupted... but [feed_target.p_they()] didn't seem to notice you.")) + else + to_chat(user, span_warning("Your feeding has been interrupted!")) + 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, 1) + 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, /mob.proc/emote, "scream") + return + + /////////////////////////////////////////////////////////// + // Handle Feeding! User & Victim Effects (per tick) + bloodsuckerdatum_power.HandleFeeding(feed_target, blood_take_mult, level_current) + amount_taken += amSilent ? 0.3 : 1 + if(!amSilent) + ApplyVictimEffects(feed_target) // Sleep, paralysis, immobile, unconscious, and mute + + /////////////////////////////////////////////////////////// + // MOOD EFFECTS // + // Drank good blood? - GOOD + if(amount_taken > 5 && feed_target.stat < DEAD && ishuman(feed_target)) + SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, "drankblood", /datum/mood_event/drankblood) + // Drank mindless as Ventrue? - BAD + if(!feed_target.mind && bloodsuckerdatum_power.my_clan == CLAN_VENTRUE) + SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, "drankblood", /datum/mood_event/drankblood_bad) + if(!warning_target_inhuman) + to_chat(user, span_notice("You feel disgusted at the taste of a non-sentient creature.")) + warning_target_inhuman = TRUE + // Dead Blood? - BAD + if(feed_target.stat >= DEAD) + if(ishuman(feed_target)) + SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, "drankblood", /datum/mood_event/drankblood_dead) + if(!warning_target_dead) + to_chat(user, span_notice("Your victim is dead. [feed_target.p_their(TRUE)] blood barely nourishes you.")) + warning_target_dead = TRUE + + // Blood Remaining? (Carbons/Humans only) + else if(!IS_BLOODSUCKER(feed_target)) + if(feed_target.blood_volume <= BLOOD_VOLUME_BAD(feed_target) && warning_target_bloodvol > BLOOD_VOLUME_BAD(feed_target)) + to_chat(owner, "your victim's blood is fatally low!") + else if(feed_target.blood_volume <= BLOOD_VOLUME_OKAY(feed_target) && warning_target_bloodvol > BLOOD_VOLUME_OKAY(feed_target)) + to_chat(owner, "your victim's blood is dangerously low.") + else if(feed_target.blood_volume <= BLOOD_VOLUME_SAFE(feed_target) && warning_target_bloodvol > BLOOD_VOLUME_SAFE(feed_target)) + to_chat(owner, "your victim's blood is at an unsafe level.") + warning_target_bloodvol = feed_target.blood_volume // If we had a warning to give, it's been given by now. + // Full? + if(user.blood_volume >= bloodsuckerdatum_power.max_blood_volume && !warning_full) + to_chat(owner, "you are full, further blood will be wasted.") + warning_full = TRUE + // Done? + if(feed_target.blood_volume <= 0) + to_chat(owner, "you have bled your victim dry...") + DeactivatePower() + return + + // Blood Gulp Sound + owner.playsound_local(null, 'sound/effects/singlebeat.ogg', 40, TRUE) + if(!amSilent) + feed_target.playsound_local(null, 'sound/effects/singlebeat.ogg', 40, TRUE) + +/// Check if we killed our target +/datum/action/bloodsucker/feed/proc/CheckKilledTarget(mob/living/target) + if(target && target.stat >= DEAD && ishuman(target)) + SEND_SIGNAL(owner, COMSIG_ADD_MOOD_EVENT, "drankkilled", /datum/mood_event/drankkilled) + bloodsuckerdatum_power.AddHumanityLost(10) + +/// NOTE: We only care about pulling if target started off that way. Mostly only important for Aggressive feed. +/datum/action/bloodsucker/feed/ContinueActive(mob/living/user, mob/living/target) + if(!target) + return FALSE + if(!user.Adjacent(target)) + return FALSE + if(target_grappled && !user.pulling) + return FALSE + return TRUE + +/// Bloodsuckers not affected by "the Kiss" of another vampire +/datum/action/bloodsucker/feed/proc/ApplyVictimEffects(mob/living/target) + if(!IS_BLOODSUCKER(target)) + target.Unconscious(50,0) + target.Paralyze(40 + 5 * level_current,1) // NOTE: This is based on level of power! + if(ishuman(target)) + target.adjustStaminaLoss(5, forced = TRUE) // Base Stamina Damage + +/datum/action/bloodsucker/feed/DeactivatePower() + . = ..() // activate = FALSE + + if(feed_target) // Check: Otherwise it runtimes if you fail to feed on someone. + if(amSilent) + to_chat(owner, span_notice("You slowly release [feed_target]'s wrist." + (feed_target.stat == 0 ? " [feed_target.p_their(TRUE)] face lacks expression, like you've already been forgotten." : ""))) + else + owner.visible_message( + span_warning("[owner] unclenches their teeth from [feed_target]'s neck."), + span_warning("You retract your fangs and release [feed_target] from your bite.")) + log_combat(owner, feed_target, "fed on blood", addition="(and took [amount_taken] blood)") + // Did we kill our target? + if(was_alive) + CheckKilledTarget(feed_target) + // Only break it once we've broken it 3 times, not more. + if(feeds_noticed == 3) + bloodsuckerdatum_power.break_masquerade() + // Reset ALL checks for next time the Power is used. + amSilent = FALSE + was_noticed = FALSE + warning_target_inhuman = FALSE + warning_target_dead = FALSE + warning_full = FALSE + feed_target = null + warning_target_bloodvol = 99999 + // My mouth is no longer full + var/mob/living/O = owner + O.SetImmobilized(0) + REMOVE_TRAIT(owner, TRAIT_MUTE, BLOODSUCKER_TRAIT) diff --git a/code/modules/antagonists/bloodsuckers/powers/fortitude.dm b/code/modules/antagonists/bloodsuckers/powers/fortitude.dm new file mode 100644 index 000000000000..869675dc756d --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/powers/fortitude.dm @@ -0,0 +1,82 @@ +/datum/action/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 + check_flags = BP_CANT_USE_IN_TORPOR|BP_CANT_USE_IN_FRENZY|BP_AM_COSTLESS_UNCONSCIOUS + purchase_flags = BLOODSUCKER_CAN_BUY|VASSAL_CAN_BUY + bloodcost = 30 + cooldown = 8 SECONDS + constant_bloodcost = 0.2 + var/was_running + var/fortitude_resist // So we can raise and lower your brute resist based on what your level_current WAS. + +/datum/action/bloodsucker/fortitude/ActivatePower() + . = ..() + to_chat(owner, span_notice("Your flesh, skin, and muscles become as steel.")) + // Traits & Effects + ADD_TRAIT(owner, TRAIT_PIERCEIMMUNE, BLOODSUCKER_TRAIT) + ADD_TRAIT(owner, TRAIT_NODISMEMBER, BLOODSUCKER_TRAIT) + ADD_TRAIT(owner, TRAIT_PUSHIMMUNE, BLOODSUCKER_TRAIT) + if(level_current >= 4) + ADD_TRAIT(owner, TRAIT_STUNIMMUNE, BLOODSUCKER_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 + if(IS_MONSTERHUNTER(owner)) + bloodsucker_user.physiology.brute_mod *= 0.4 + bloodsucker_user.physiology.burn_mod *= 0.4 + ADD_TRAIT(owner, TRAIT_STUNIMMUNE, BLOODSUCKER_TRAIT) + + was_running = (owner.m_intent == MOVE_INTENT_RUN) + if(was_running) + bloodsucker_user.toggle_move_intent() + +/datum/action/bloodsucker/fortitude/UsePower(mob/living/carbon/user) + // Checks that we can keep using this. + . = ..() + if(!.) + return + /// Prevents running while on Fortitude + if(user.m_intent != MOVE_INTENT_WALK) + user.toggle_move_intent() + to_chat(user, "you attempt to run, crushing yourself.") + user.adjustBruteLoss(rand(5,15)) + /// We don't want people using fortitude being able to use vehicles + if(user.buckled && istype(user.buckled, /obj/vehicle)) + user.buckled.unbuckle_mob(src, force=TRUE) + +/datum/action/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, BLOODSUCKER_TRAIT)) + bloodsucker_user.physiology.stamina_mod /= fortitude_resist + if(IS_MONSTERHUNTER(owner)) + bloodsucker_user.physiology.brute_mod /= 0.4 + bloodsucker_user.physiology.burn_mod /= 0.4 + // Remove Traits & Effects + REMOVE_TRAIT(bloodsucker_user, TRAIT_PIERCEIMMUNE, BLOODSUCKER_TRAIT) + REMOVE_TRAIT(bloodsucker_user, TRAIT_NODISMEMBER, BLOODSUCKER_TRAIT) + REMOVE_TRAIT(bloodsucker_user, TRAIT_PUSHIMMUNE, BLOODSUCKER_TRAIT) + REMOVE_TRAIT(bloodsucker_user, TRAIT_STUNIMMUNE, BLOODSUCKER_TRAIT) + + if(was_running && bloodsucker_user.m_intent == MOVE_INTENT_WALK) + bloodsucker_user.toggle_move_intent() + return ..() + +/// Monster Hunter version +/datum/action/bloodsucker/fortitude/hunter + name = "Flow" + desc = "Use the arts to Flow, giving shove and stun immunity, as well as brute, burn, dismember and pierce resistance. You cannot run while this is active." + purchase_flags = HUNTER_CAN_BUY diff --git a/code/modules/antagonists/bloodsuckers/powers/gohome.dm b/code/modules/antagonists/bloodsuckers/powers/gohome.dm new file mode 100644 index 000000000000..5eb9fa48fefd --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/powers/gohome.dm @@ -0,0 +1,117 @@ +/datum/action/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" + background_icon_state_on = "vamp_power_off_oneshot" + background_icon_state_off = "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_SINGLEUSE|BP_AM_STATIC_COOLDOWN + check_flags = BP_CANT_USE_IN_FRENZY|BP_CANT_USE_WHILE_STAKED|BP_CANT_USE_WHILE_INCAPACITATED + // You only get this once you've claimed a lair and Sol is near. + purchase_flags = NONE + bloodcost = 100 + cooldown = 100 SECONDS + +/datum/action/bloodsucker/gohome/CheckCanUse(mob/living/carbon/user) + . = ..() + 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) || !bloodsuckerdatum_power.coffin) + to_chat(owner, "Your coffin has been destroyed!") + return FALSE + return TRUE + +/datum/action/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) + +/// IMPORTANT: Check for lair at every step! It might get destroyed. +/datum/action/bloodsucker/gohome/ActivatePower() + . = ..() + to_chat(owner, span_notice("You focus on separating your consciousness from your physical form...")) + /// STEP ONE: Flicker Lights + flicker_lights(3, 20) + sleep(50) + flicker_lights(4, 40) + sleep(50) + flicker_lights(4, 60) + for(var/obj/machinery/light/nearby_lights in view(6, get_turf(owner))) + nearby_lights.flicker(5) + playsound(get_turf(owner), 'sound/effects/singlebeat.ogg', 60, 1) + /// STEP TWO: Lights OFF? + /// CHECK: Still have Coffin? + if(!bloodsuckerdatum_power.coffin) + to_chat(owner, span_warning("Your coffin has been destroyed! You no longer have a destination.")) + return FALSE + if(!owner) + return + /// SEEN?: (effects ONLY if there are witnesses! Otherwise you just POOF) + + /// Do Effects (seen by anyone) + var/am_seen = FALSE + /// Drop Stuff (seen by non-vamp) + var/drop_item = FALSE + // Only check if I'm not in a Locker or something. + if(!isturf(owner.loc)) + return + // A) Check for Darkness (we can just leave) + var/turf/current_turf = get_turf(owner) + if(current_turf && current_turf.lighting_object && current_turf.get_lumcount()>= 0.1) + // B) Check for Viewers + for(var/mob/living/watchers in viewers(world.view, get_turf(owner)) - owner) + if(watchers.client && !watchers.has_unlimited_silicon_privilege && !watchers.eye_blind) + am_seen = TRUE + if(!IS_BLOODSUCKER(watchers) && !IS_VASSAL(watchers)) + drop_item = TRUE + break + /// LOSE CUFFS + var/mob/living/carbon/user = owner + if(user.handcuffed) + var/obj/handcuffs = user.handcuffed + user.dropItemToGround(handcuffs) + if(user.legcuffed) + var/obj/legcuffs = user.legcuffed + user.dropItemToGround(legcuffs) + /// SEEN! + if(drop_item) + // DROP: Clothes, held items, and cuffs etc + // NOTE: Taken from unequip_everything() in inventory.dm. We need to + // *force* all items to drop, so we had to just gut the code out of it. + var/list/items = list() + items |= user.get_equipped_items() + for(var/belongings in items) + user.dropItemToGround(belongings, TRUE) + for(var/obj/item/held_posessions in owner.held_items) //drop_all_held_items() + user.dropItemToGround(held_posessions, TRUE) + /// POOF EFFECTS + if(am_seen) + playsound(get_turf(owner), 'sound/magic/summon_karp.ogg', 60, 1) + var/datum/effect_system/steam_spread/puff = new /datum/effect_system/steam_spread() + puff.effect_type = /obj/effect/particle_effect/smoke/vampsmoke + puff.set_up(3, 0, get_turf(owner)) + puff.start() + + /// STEP FIVE: Create animal at prev location + var/mob/living/simple_animal/SA = pick(/mob/living/simple_animal/mouse,/mob/living/simple_animal/mouse,/mob/living/simple_animal/mouse, /mob/living/simple_animal/hostile/retaliate/bat) //prob(300) /mob/living/simple_animal/mouse, + new SA (owner.loc) + /// 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) + user.Stun(3 SECONDS, TRUE) + /// CLOSE LID: If fail, force me in. + if(!bloodsuckerdatum_power.coffin.close(owner)) + /// Puts me inside. + bloodsuckerdatum_power.coffin.insert(owner) + playsound(bloodsuckerdatum_power.coffin.loc, bloodsuckerdatum_power.coffin.close_sound, 15, 1, -3) + bloodsuckerdatum_power.coffin.opened = FALSE + bloodsuckerdatum_power.coffin.density = TRUE + bloodsuckerdatum_power.coffin.update_icon() + // Lock Coffin + bloodsuckerdatum_power.coffin.LockMe(owner) + bloodsuckerdatum_power.Check_Begin_Torpor(FALSE) // Are we meant to enter Torpor here? diff --git a/code/modules/antagonists/bloodsuckers/powers/masquerade.dm b/code/modules/antagonists/bloodsuckers/powers/masquerade.dm new file mode 100644 index 000000000000..207d05cad116 --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/powers/masquerade.dm @@ -0,0 +1,110 @@ +/** + * # 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/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 + check_flags = BP_CANT_USE_IN_FRENZY|BP_AM_COSTLESS_UNCONSCIOUS + purchase_flags = BLOODSUCKER_CAN_BUY + bloodcost = 10 + cooldown = 5 SECONDS + constant_bloodcost = 0.1 + +/datum/action/bloodsucker/masquerade/ActivatePower() + . = ..() + var/mob/living/carbon/user = owner + 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.")) + + // Remove Bloodsucker traits + REMOVE_TRAIT(user, TRAIT_NOHARDCRIT, BLOODSUCKER_TRAIT) + REMOVE_TRAIT(user, TRAIT_NOSOFTCRIT, BLOODSUCKER_TRAIT) + REMOVE_TRAIT(user, TRAIT_VIRUSIMMUNE, BLOODSUCKER_TRAIT) + REMOVE_TRAIT(user, TRAIT_RADIMMUNE, BLOODSUCKER_TRAIT) + REMOVE_TRAIT(user, TRAIT_TOXIMMUNE, BLOODSUCKER_TRAIT) + REMOVE_TRAIT(user, TRAIT_COLDBLOODED, BLOODSUCKER_TRAIT) + REMOVE_TRAIT(user, TRAIT_RESISTCOLD, BLOODSUCKER_TRAIT) + REMOVE_TRAIT(user, TRAIT_SLEEPIMMUNE, BLOODSUCKER_TRAIT) + REMOVE_TRAIT(user, TRAIT_NOPULSE, BLOODSUCKER_TRAIT) + REMOVE_TRAIT(user, TRAIT_NOBREATH, BLOODSUCKER_TRAIT) + // Falsifies Health & Genetic Analyzers + ADD_TRAIT(user, TRAIT_MASQUERADE, BLOODSUCKER_TRAIT) + REMOVE_TRAIT(user, TRAIT_GENELESS, BLOODSUCKER_TRAIT) + // Organs + var/obj/item/organ/eyes/eyes = user.getorganslot(ORGAN_SLOT_EYES) + eyes.flash_protect = initial(eyes.flash_protect) + var/obj/item/organ/heart/vampheart/vampheart = user.getorganslot(ORGAN_SLOT_HEART) + if(istype(vampheart)) + vampheart.FakeStart() + user.apply_status_effect(STATUS_EFFECT_MASQUERADE) + +/datum/action/bloodsucker/masquerade/DeactivatePower() + . = ..() // activate = FALSE + var/mob/living/carbon/user = owner + user.remove_status_effect(STATUS_EFFECT_MASQUERADE) + ADD_TRAIT(user, TRAIT_NOHARDCRIT, BLOODSUCKER_TRAIT) + ADD_TRAIT(user, TRAIT_NOSOFTCRIT, BLOODSUCKER_TRAIT) + ADD_TRAIT(user, TRAIT_VIRUSIMMUNE, BLOODSUCKER_TRAIT) + ADD_TRAIT(user, TRAIT_RADIMMUNE, BLOODSUCKER_TRAIT) + ADD_TRAIT(user, TRAIT_TOXIMMUNE, BLOODSUCKER_TRAIT) + ADD_TRAIT(user, TRAIT_COLDBLOODED, BLOODSUCKER_TRAIT) + ADD_TRAIT(user, TRAIT_RESISTCOLD, BLOODSUCKER_TRAIT) + ADD_TRAIT(user, TRAIT_SLEEPIMMUNE, BLOODSUCKER_TRAIT) + ADD_TRAIT(user, TRAIT_NOPULSE, BLOODSUCKER_TRAIT) + ADD_TRAIT(user, TRAIT_NOBREATH, BLOODSUCKER_TRAIT) + REMOVE_TRAIT(user, TRAIT_MASQUERADE, BLOODSUCKER_TRAIT) + // Remove genes, then make unable to get new ones. + user.dna.remove_all_mutations() + ADD_TRAIT(user, TRAIT_GENELESS, BLOODSUCKER_TRAIT) + // Organs + var/obj/item/organ/heart/vampheart/vampheart = user.getorganslot(ORGAN_SLOT_HEART) + if(istype(vampheart)) + vampheart.Stop() + var/obj/item/organ/eyes/eyes = user.getorganslot(ORGAN_SLOT_EYES) + if(eyes) + eyes.flash_protect = max(initial(eyes.flash_protect) - 1, - 1) + // Remove all diseases + for(var/thing in user.diseases) + var/datum/disease/disease = thing + disease.cure() + 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 = -1 + tick_interval = -1 + 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 = 'icons/mob/actions/actions_bloodsucker.dmi' + icon_state = "power_human" + +/atom/movable/screen/alert/status_effect/masquerade/MouseEntered(location,control,params) + desc = initial(desc) + return ..() diff --git a/code/modules/antagonists/bloodsuckers/powers/recuperate.dm b/code/modules/antagonists/bloodsuckers/powers/recuperate.dm new file mode 100644 index 000000000000..51a7a0bc0d4b --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/powers/recuperate.dm @@ -0,0 +1,58 @@ +/// Used by Vassals +/datum/action/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_INCAPACITATED|BP_CANT_USE_WHILE_UNCONSCIOUS + purchase_flags = NONE + bloodcost = 1.5 + cooldown = 10 SECONDS + +/datum/action/bloodsucker/recuperate/CheckCanUse(mob/living/carbon/user) + . = ..() + if(!.) + return + if(user.stat >= DEAD || user.incapacitated()) + to_chat(user, "You are incapacitated...") + return FALSE + return TRUE + +/datum/action/bloodsucker/recuperate/ActivatePower() + . = ..() + to_chat(owner, span_notice("Your muscles clench as your master's immortal blood mixes with your own, knitting your wounds.")) + +/datum/action/bloodsucker/recuperate/UsePower(mob/living/carbon/user) + . = ..() + if(!.) + return + + var/datum/antagonist/vassal/vassaldatum = IS_VASSAL(user) + vassaldatum.master.AddBloodVolume(-1) + user.Jitter(5) + user.adjustStaminaLoss(bloodcost * 1.1) + user.adjustBruteLoss(-2.5) + user.adjustToxLoss(-2, forced = TRUE) + // Plasmamen won't lose blood, they don't have any, so they don't heal from Burn. + if(!(NOBLOOD in user.dna.species.species_traits)) + user.blood_volume -= bloodcost + user.adjustFireLoss(-1.5) + // Stop Bleeding + if(istype(user) && user.is_bleeding()) + for(var/obj/item/bodypart/part in user.bodyparts) + part.generic_bleedstacks-- + +/datum/action/bloodsucker/recuperate/ContinueActive(mob/living/user, mob/living/target) + if(user.stat >= DEAD) + return FALSE + if(user.incapacitated()) + return FALSE + return TRUE + +/datum/action/bloodsucker/recuperate/DeactivatePower() + . = ..() diff --git a/code/modules/antagonists/bloodsuckers/powers/targeted/_powers_targeted.dm b/code/modules/antagonists/bloodsuckers/powers/targeted/_powers_targeted.dm new file mode 100644 index 000000000000..6057734f8e33 --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/powers/targeted/_powers_targeted.dm @@ -0,0 +1,98 @@ +// NOTE: All Targeted spells are Toggles! We just don't bother checking here. +/datum/action/bloodsucker/targeted + power_flags = BP_AM_TOGGLE + + var/obj/effect/proc_holder/bloodsucker/bs_proc_holder + var/target_range = 99 + 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/bloodsucker/targeted/New(Target) + desc += "
\[Targeted Power\]" + . = ..() + // Create Proc Holder for intercepting clicks + bs_proc_holder = new() + bs_proc_holder.linked_power = src + +/datum/action/bloodsucker/targeted/Trigger(trigger_flags) + if(active && CheckCanDeactivate()) + DeactivatePower() + return FALSE + if(!CheckCanPayCost(owner) || !CheckCanUse(owner)) + return FALSE + + ActivatePower() + UpdateButtonIcon() + // Create & Link Targeting Proc + var/mob/living/user = owner + if(user.ranged_ability) + user.ranged_ability.remove_ranged_ability() + bs_proc_holder.add_ranged_ability(user) + if(prefire_message != "") + to_chat(owner, span_announce("[prefire_message]")) + return TRUE + +/datum/action/bloodsucker/targeted/DeactivatePower() + if(power_flags & BP_AM_TOGGLE) + UnregisterSignal(owner, COMSIG_LIVING_BIOLOGICAL_LIFE) + active = FALSE + DeactivateRangedAbility() + UpdateButtonIcon() +// ..() // we don't want to pay cost here + +/// Only Turned off when CLICK is disabled...aka, when you successfully clicked +/datum/action/bloodsucker/targeted/proc/DeactivateRangedAbility() + bs_proc_holder.remove_ranged_ability() + +/// Check if target is VALID (wall, turf, or character?) +/datum/action/bloodsucker/targeted/proc/CheckValidTarget(atom/target_atom) + if(target_atom == owner) + return FALSE + return TRUE + +/// Check if valid target meets conditions +/datum/action/bloodsucker/targeted/proc/CheckCanTarget(atom/target_atom) + // 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. + to_chat(owner, "Target out of range.") + return FALSE + return istype(target_atom) + +/// Click Target +/datum/action/bloodsucker/targeted/proc/ClickWithPower(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(!CheckCanPayCost() || !CheckCanUse(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(), which has no input + // Skip this part so we can return TRUE right away. + if(power_activates_immediately) + PowerActivatedSuccessfully() // 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/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/bloodsucker/targeted/proc/PowerActivatedSuccessfully() + PayCost() + DeactivatePower() + StartCooldown() // Do AFTER UpdateIcon() inside of DeactivatePower. Otherwise icon just gets wiped. + +/// Target Proc Holder +/obj/effect/proc_holder/bloodsucker + ///The linked Bloodsucker power + var/datum/action/bloodsucker/targeted/linked_power + +/obj/effect/proc_holder/bloodsucker/InterceptClickOn(mob/living/caller, params, atom/targeted_atom) + return linked_power.ClickWithPower(targeted_atom) diff --git a/code/modules/antagonists/bloodsuckers/powers/targeted/brawn.dm b/code/modules/antagonists/bloodsuckers/powers/targeted/brawn.dm new file mode 100644 index 000000000000..d174fb91d165 --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/powers/targeted/brawn.dm @@ -0,0 +1,191 @@ +/datum/action/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 + cooldown = 9 SECONDS + target_range = 1 + power_activates_immediately = TRUE + prefire_message = "Select a target." + +/datum/action/bloodsucker/targeted/brawn/CheckCanUse(mob/living/carbon/user) + . = ..() + if(!.) // Default checks + return FALSE + + // Did we break out of our handcuffs? + if(CheckBreakRestraints()) + PowerActivatedSuccessfully() + 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 && CheckEscapePuller()) + PowerActivatedSuccessfully() + return FALSE + // Did neither, now we can PUNCH. + return TRUE + +// Look at 'biodegrade.dm' for reference +/datum/action/bloodsucker/targeted/brawn/proc/CheckBreakRestraints() + 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/break_closet, user, closet), 1) + used = TRUE + + // Remove both Handcuffs & Legcuffs + var/obj/cuffs = user.get_item_by_slot(SLOT_HANDCUFFED) + var/obj/legcuffs = user.get_item_by_slot(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/bloodsucker/targeted/brawn/proc/break_closet(mob/living/carbon/human/user, obj/structure/closet/closet) + if(closet) + closet.welded = FALSE + closet.locked = FALSE + closet.broken = TRUE + closet.open() + +/datum/action/bloodsucker/targeted/brawn/proc/CheckEscapePuller() + 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/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 + var/hitStrength = carbonuser.dna.species.punchdamagehigh * 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! + to_chat(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 + to_chat(user, "You prepare to bash [target_closet] open...") + if(!do_mob(user, target_closet, 2.5 SECONDS)) + return FALSE + target_closet.visible_message(span_danger("[target_closet] breaks open as [user] bashes it!")) + addtimer(CALLBACK(src, .proc/break_closet, user, target_closet), 1) + playsound(get_turf(user), 'sound/effects/grillehit.ogg', 80, 1, -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, 1, -1) + to_chat(owner, "You prepare to tear open [target_airlock]...") + if(!do_mob(user, target_airlock, 2.5 SECONDS)) + 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/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/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/code/modules/antagonists/bloodsuckers/powers/targeted/haste.dm b/code/modules/antagonists/bloodsuckers/powers/targeted/haste.dm new file mode 100644 index 000000000000..9f0d5c00f55c --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/powers/targeted/haste.dm @@ -0,0 +1,99 @@ +/* Level 1: Speed to location + * Level 2: Dodge Bullets + * Level 3: Stun People Passed + */ + +/datum/action/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 and Payalyzed, moreso if they are using Flow.\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 + cooldown = 12 SECONDS + target_range = 15 + power_activates_immediately = TRUE + var/list/hit //current hit, set while power is in use as we can't pass the list as an extra calling argument in registersignal. + /// If set, uses this speed in deciseconds instead of world.tick_lag + var/speed_override + +/datum/action/bloodsucker/targeted/haste/CheckCanUse(mob/living/carbon/user) + . = ..() + if(!.) + return FALSE + // Being Grabbed + if(user.pulledby && user.pulledby.grab_state >= GRAB_AGGRESSIVE) + to_chat(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 + to_chat(user, "You cannot dash while floating!") + return FALSE + if(user.mobility_flags & ~MOBILITY_STAND) + to_chat(user, "You must be standing to tackle!") + return FALSE + return TRUE + +/// Anything will do, if it's not me or my square +/datum/action/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/bloodsucker/targeted/haste/FireTargetedPower(atom/target_atom) + . = ..() + hit = list() + RegisterSignal(owner, COMSIG_MOVABLE_MOVED, .proc/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. + to_chat(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 + var/speed = isnull(speed_override)? world.tick_lag : speed_override + 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) + if(++consequetive_failures >= 3) //if 3 steps don't work + break //just stop + else + consequetive_failures = 0 + if(user.resting) + user.setDir(turn(user.dir, 90)) //down? spin2win? + if(user.incapacitated(ignore_restraints = TRUE, ignore_grab = TRUE)) //actually down? stop. + break + if(success) //don't sleep if we failed to move. + sleep(speed) + UnregisterSignal(owner, COMSIG_MOVABLE_MOVED) + hit = null + +/datum/action/bloodsucker/targeted/haste/proc/on_move() + for(var/mob/living/all_targets in dview(1, get_turf(owner))) + if(!hit[all_targets] && (all_targets != owner)) + hit[all_targets] = TRUE + playsound(all_targets, "sound/weapons/punch[rand(1,4)].ogg", 15, 1, -1) + all_targets.Knockdown(10 + level_current * 5) + all_targets.Paralyze(0.1) + all_targets.spin(10, 1) + if(IS_MONSTERHUNTER(all_targets) && HAS_TRAIT(all_targets, TRAIT_STUNIMMUNE)) + to_chat(all_targets, "Knocked down!") + for(var/datum/action/bloodsucker/power in all_targets.actions) + if(power.active) + power.DeactivatePower() + all_targets.Jitter(20) + all_targets.confused = max(8, all_targets.confused) + all_targets.stuttering = max(8, all_targets.stuttering) + all_targets.Knockdown(10 + level_current * 5) // Re-knock them down, the first one didn't work due to stunimmunity diff --git a/code/modules/antagonists/bloodsuckers/powers/targeted/lunge.dm b/code/modules/antagonists/bloodsuckers/powers/targeted/lunge.dm new file mode 100644 index 000000000000..39371747ea16 --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/powers/targeted/lunge.dm @@ -0,0 +1,143 @@ +/datum/action/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 instantly dash at them, aggressively grabbing them.\n\ + You cannot use the Power if you are aggressively grabbed.\n\ + If the target is wearing riot gear or is a Monster Hunter, you will merely passively grab them.\n\ + If grabbed from behind or from the darkness (Cloak of Darkness counts), you will additionally knock the target 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 = 10 + cooldown = 10 SECONDS + target_range = 3 + power_activates_immediately = TRUE + +/* + * Level 1: Grapple level 2 + * Level 2: Grapple 3 from Behind + * Level 3: Grapple 3 from Shadows + */ + +/datum/action/bloodsucker/targeted/lunge/CheckCanUse(mob/living/carbon/user) + . = ..() + if(!.) + return FALSE + /// Are we being grabbed? + if(user.pulledby && user.pulledby.grab_state >= GRAB_AGGRESSIVE) + to_chat(user, span_warning("You're being grabbed!")) + return FALSE + return TRUE + +/// Check: Are we lunging at a person? +/datum/action/bloodsucker/targeted/lunge/CheckValidTarget(atom/target_atom) + . = ..() + if(!.) + return FALSE + return isliving(target_atom) + +/datum/action/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.mobility_flags & ~ MOBILITY_STAND || user.IsImmobilized()) + return FALSE + return TRUE + +/datum/action/bloodsucker/targeted/lunge/FireTargetedPower(atom/target_atom) + . = ..() + var/mob/living/user = owner + var/mob/living/carbon/target = target_atom + var/turf/targeted_turf = get_turf(target) + + /// Stop pulling anyone (If we are) + owner.pulling = null + + owner.face_atom(target_atom) + /// Don't move as we perform this, please. + user.Immobilize(10 SECONDS) + /// Directly copied from haste.dm + var/safety = get_dist(user, targeted_turf) * 3 + 1 + var/consequetive_failures = 0 + while(--safety && !target.Adjacent(user)) + /// This does not try to go around obstacles. + var/success = step_towards(user, targeted_turf) + if(!success) + /// This does + success = step_to(user, targeted_turf) + if(!success) + consequetive_failures++ + /// If 3 steps don't work, just stop. + if(consequetive_failures >= 3) + break + /// we've succeeded at least once? Reset it. + else + consequetive_failures = 0 + /// It ended? Let's get our target now. + lunge_end(target) + +/datum/action/bloodsucker/targeted/lunge/proc/lunge_end(atom/hit_atom) + var/mob/living/user = owner + var/mob/living/carbon/target = hit_atom + var/turf/target_turf = get_turf(target) + // Check: Will our lunge knock them down? This is done if the target is looking away, the user is in Cloak of Darkness, or in a closet. + var/do_knockdown = !is_source_facing_target(target, owner) || owner.alpha <= 40 || istype(owner.loc, /obj/structure/closet) + + /// We got a target? + /// Am I next to my target to start giving the effects? + if(user.Adjacent(target)) + // Did I slip? + if(!target.mobility_flags & MOBILITY_STAND) + return + // Is my target a Monster hunter? + if(IS_MONSTERHUNTER(target)) + to_chat(owner, "You get pushed away!") + target.grabbedby(owner) + return + + to_chat(owner, "You lunge at [target]!") + /// Good to go! + target.Stun(10 + level_current * 5) + // Instantly aggro grab them if they don't have riot gear. + var/mob/living/carbon/human/H = target + H.grabbedby(owner) + if(!H.is_shove_knockdown_blocked()) + target.grippedby(owner, instant = TRUE) + // Did we knock them down? + if(do_knockdown && level_current >= 3) + target.Knockdown(10 + level_current * 5) + target.Paralyze(0.1) + /// Are they dead? + if(target.stat == DEAD) + var/obj/item/bodypart/chest = target.get_bodypart(BODY_ZONE_CHEST) + var/datum/wound/slash/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/heart/myheart_now = locate() in target.internal_organs + if(myheart_now) + myheart_now.Remove(target) + user.put_in_hands(myheart_now) + // Lastly, did we get knocked down by the time we did this? + if(user && user.incapacitated()) + if(!(user.mobility_flags & ~MOBILITY_STAND)) + 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) + +/datum/action/bloodsucker/targeted/lunge/DeactivatePower() + var/mob/living/O = owner + O.SetImmobilized(0) + . = ..() diff --git a/code/modules/antagonists/bloodsuckers/powers/targeted/mesmerize.dm b/code/modules/antagonists/bloodsuckers/powers/targeted/mesmerize.dm new file mode 100644 index 000000000000..cbef2805c032 --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/powers/targeted/mesmerize.dm @@ -0,0 +1,143 @@ +/** + * 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 + * Level 6: Causes the target to fall asleep + */ + +/datum/action/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\ + At level 6, you will cause your target to fall asleep.\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 + cooldown = 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/mob/living/mesmerized_target + +/datum/action/bloodsucker/targeted/mesmerize/CheckCanUse(mob/living/carbon/user) + . = ..() + if(!.) // Default checks + return FALSE + if(!user.getorganslot(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)) + to_chat(user, "Your eyes are concealed from sight.") + return FALSE + return TRUE + +/datum/action/bloodsucker/targeted/mesmerize/CheckValidTarget(atom/target_atom) + . = ..() + if(!.) + return FALSE + return isliving(target_atom) + +/datum/action/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) + to_chat(owner, "[current_target] is mindless.") + return FALSE + // Bloodsucker + if(IS_BLOODSUCKER(current_target)) + to_chat(owner, "Bloodsuckers are immune to [src].") + return FALSE + // Dead/Unconscious + if(current_target.stat > CONSCIOUS) + to_chat(owner, "[current_target] is not [(current_target.stat == DEAD || HAS_TRAIT(current_target, TRAIT_FAKEDEATH)) ? "alive" : "conscious"].") + return FALSE + // Target has eyes? + if(!current_target.getorganslot(ORGAN_SLOT_EYES)) + to_chat(owner, "[current_target] has no eyes.") + return FALSE + // Target blind? + if(current_target.eye_blind > 0) + to_chat(owner, "[current_target] is blind.") + return FALSE + // Facing target? + if(!is_source_facing_target(owner, current_target)) // in unsorted.dm + to_chat(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)) + to_chat(owner, "[current_target] must be facing you.") + return FALSE + + // Gone through our checks, let's mark our guy. + mesmerized_target = current_target + return TRUE + +/datum/action/bloodsucker/targeted/mesmerize/FireTargetedPower(atom/target_atom) + . = ..() + + var/mob/living/user = owner + + if(istype(mesmerized_target)) + to_chat(owner, "Attempting to hypnotically gaze [mesmerized_target]...") + + if(!do_mob(user, mesmerized_target, 4 SECONDS, NONE, TRUE, extra_checks = CALLBACK(src, .proc/ContinueActive, user, mesmerized_target))) + return + + PowerActivatedSuccessfully() // PAY COST! BEGIN COOLDOWN! + var/power_time = 90 + level_current * 15 + 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, BLOODSUCKER_TRAIT)) + to_chat(owner, "[mesmerized_target] is already in a hypnotic gaze.") + return + if(iscarbon(mesmerized_target)) + var/mob/living/carbon/mesmerized = mesmerized_target + to_chat(owner, "Successfully mesmerized [mesmerized].") + if(level_current >= 6) + mesmerized.SetUnconscious(world.time + power_time ) + else if(level_current >= 2) + ADD_TRAIT(mesmerized, TRAIT_MUTE, BLOODSUCKER_TRAIT) + mesmerized.Immobilize(power_time) + //mesmerized.silent += power_time / 10 // Silent isn't based on ticks. + mesmerized.next_move = world.time + power_time // <--- Use direct change instead. We want an unmodified delay to their next move // mesmerized.changeNext_move(power_time) // check click.dm + mesmerized.notransform = TRUE // <--- Fuck it. We tried using next_move, but they could STILL resist. We're just doing a hard freeze. + addtimer(CALLBACK(src, .proc/end_mesmerize, user, mesmerized_target), power_time) + if(issilicon(mesmerized_target)) + var/mob/living/silicon/mesmerized = mesmerized_target + mesmerized.emp_act(EMP_HEAVY) + // Finished, clear target. + mesmerized_target = null + +/datum/action/bloodsucker/targeted/mesmerize/DeactivatePower() + mesmerized_target = null + . = ..() + +/datum/action/bloodsucker/targeted/mesmerize/proc/end_mesmerize(mob/living/user, mob/living/target) + target.notransform = FALSE + REMOVE_TRAIT(target, TRAIT_MUTE, BLOODSUCKER_TRAIT) + // They Woke Up! (Notice if within view) + if(istype(user) && target.stat == CONSCIOUS && (target in view(6, get_turf(user)))) + to_chat(owner, "[target] snapped out of their trance.") + +/datum/action/bloodsucker/targeted/mesmerize/ContinueActive(mob/living/user, mob/living/target) + return ..() && CheckCanUse(user) && CheckCanTarget(mesmerized_target) diff --git a/code/modules/antagonists/bloodsuckers/powers/targeted/trespass.dm b/code/modules/antagonists/bloodsuckers/powers/targeted/trespass.dm new file mode 100644 index 000000000000..d673450ee8f2 --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/powers/targeted/trespass.dm @@ -0,0 +1,107 @@ +/datum/action/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 + cooldown = 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/bloodsucker/targeted/trespass/CheckCanUse(mob/living/carbon/user) + . = ..() + if(!.) + return FALSE + if(user.notransform || !get_turf(user)) + return FALSE + return TRUE + + +/datum/action/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/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" + to_chat(owner, "There is a wall [wallwarning].") + return FALSE + // Done + target_turf = from_turf + + return TRUE + +/datum/action/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', sound_strength, 1) + var/datum/effect_system/steam_spread/puff = new /datum/effect_system/steam_spread/() + puff.effect_type = /obj/effect/particle_effect/smoke/vampsmoke + 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', 60, 1) + puff = new /datum/effect_system/steam_spread/() + puff.effect_type = /obj/effect/particle_effect/smoke/vampsmoke + puff.set_up(3, 0, target_turf) + puff.start() diff --git a/code/modules/antagonists/bloodsuckers/powers/tremere/_powers_tremere.dm b/code/modules/antagonists/bloodsuckers/powers/tremere/_powers_tremere.dm new file mode 100644 index 000000000000..882a13451606 --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/powers/tremere/_powers_tremere.dm @@ -0,0 +1,28 @@ +/** + * # 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/bloodsucker/targeted/tremere + name = "Tremere Gift" + desc = "A Tremere exclusive gift." + button_icon_state = "power_feed" + background_icon_state = "tremere_power_off" + background_icon_state_on = "tremere_power_on" + background_icon_state_off = "tremere_power_off" + button_icon = 'icons/mob/actions/actions_tremere_bloodsucker.dmi' + icon_icon = 'icons/mob/actions/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 + target_range = 99 + power_activates_immediately = FALSE + + ///The upgraded version of this Power. 'null' means it's the max level. + var/upgraded_power = null diff --git a/code/modules/antagonists/bloodsuckers/powers/tremere/auspex.dm b/code/modules/antagonists/bloodsuckers/powers/tremere/auspex.dm new file mode 100644 index 000000000000..87968593cb2a --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/powers/tremere/auspex.dm @@ -0,0 +1,123 @@ +/** + * # 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 /obj/effect/proc_holder/spell/pointed/void_blink for help. + +/datum/action/bloodsucker/targeted/tremere/auspex + name = "Level 1: Auspex" + upgraded_power = /datum/action/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 + cooldown = 12 SECONDS + target_range = 2 + prefire_message = "Where do you wish to teleport to?" + +/datum/action/bloodsucker/targeted/tremere/auspex/two + name = "Level 2: Auspex" + upgraded_power = /datum/action/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 = 10 SECONDS + target_range = 3 + +/datum/action/bloodsucker/targeted/tremere/auspex/three + name = "Level 3: Auspex" + upgraded_power = /datum/action/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 = 8 SECONDS + target_range = 6 + +/datum/action/bloodsucker/targeted/tremere/auspex/advanced + name = "Level 4: Auspex" + upgraded_power = /datum/action/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" + background_icon_state_on = "tremere_power_gold_on" + background_icon_state_off = "tremere_power_gold_off" + bloodcost = 20 + cooldown = 6 SECONDS + target_range = 6 + +/datum/action/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 asleep for 10 seconds." + bloodcost = 25 + cooldown = 8 SECONDS + + +/datum/action/bloodsucker/targeted/tremere/auspex/CheckValidTarget(atom/target_atom) + . = ..() + if(!.) + return FALSE + return isturf(target_atom) + +/datum/action/bloodsucker/targeted/tremere/auspex/ActivatePower() + . = ..() + owner.digitalinvis = 1 + owner.digitalcamo = 1 + animate(owner, alpha = 15, time = 1 SECONDS) + +/datum/action/bloodsucker/targeted/tremere/auspex/DeactivatePower() + animate(owner, alpha = 255, time = 1 SECONDS) + owner.digitalinvis = 0 + owner.digitalcamo = 0 + return ..() + +/datum/action/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/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/smoke/vampsmoke(user.drop_location()) + new /obj/effect/particle_effect/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) + var/datum/wound/slash/critical/crit_wound = new + crit_wound.apply_wound(bodypart) + living_mob.adjustFireLoss(20) + if(level_current == 5) + living_mob.SetUnconscious(10 SECONDS) + + do_teleport(owner, targeted_turf, no_effects = TRUE, channel = TELEPORT_CHANNEL_QUANTUM) + PowerActivatedSuccessfully() diff --git a/code/modules/antagonists/bloodsuckers/powers/tremere/dominate.dm b/code/modules/antagonists/bloodsuckers/powers/tremere/dominate.dm new file mode 100644 index 000000000000..b8781a0b9782 --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/powers/tremere/dominate.dm @@ -0,0 +1,190 @@ +/** + * # 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/bloodsucker/targeted/tremere/dominate + name = "Level 1: Dominate" + upgraded_power = /datum/action/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 + bloodcost = 15 + constant_bloodcost = 2 + cooldown = 50 SECONDS + target_range = 6 + prefire_message = "Select a target." + +/datum/action/bloodsucker/targeted/tremere/dominate/two + name = "Level 2: Dominate" + upgraded_power = /datum/action/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 = 40 SECONDS + +/datum/action/bloodsucker/targeted/tremere/dominate/three + name = "Level 3: Dominate" + upgraded_power = /datum/action/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 = 35 SECONDS + +/datum/action/bloodsucker/targeted/tremere/dominate/CheckValidTarget(atom/target_atom) + . = ..() + if(!.) + return FALSE + return isliving(target_atom) + +/datum/action/bloodsucker/targeted/tremere/dominate/CheckCanTarget(atom/target_atom) + . = ..() + if(!.) + return FALSE + var/mob/living/selected_target = target_atom + if(!selected_target.mind) + to_chat(owner, "[selected_target] is mindless.") + return FALSE + return TRUE + +/datum/action/bloodsucker/targeted/tremere/dominate/advanced + name = "Level 4: Possession" + upgraded_power = /datum/action/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" + background_icon_state_on = "tremere_power_gold_on" + background_icon_state_off = "tremere_power_gold_off" + bloodcost = 80 + cooldown = 180 SECONDS // 3 minutes + +/datum/action/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 = 120 SECONDS // 2 minutes + +// The advanced version +/datum/action/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)) + to_chat(owner, "Out of range.") + return FALSE + return TRUE + +/datum/action/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)) + to_chat(owner, "Vassal cant be revived") + return + attempt_mesmerize(target, user) + +/datum/action/bloodsucker/targeted/tremere/dominate/proc/attempt_mesmerize(mob/living/target, mob/living/user) + to_chat(owner, "Attempting to mesmerize.") + if(!do_mob(user, target, 3 SECONDS, NONE, TRUE)) + return + + PowerActivatedSuccessfully() + 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)) + to_chat(owner, "[target] is already in some form of hypnotic gaze.") + return + if(iscarbon(target)) + var/mob/living/carbon/mesmerized = target + to_chat(owner, "Successfully mesmerized [mesmerized].") + if(level_current >= 2) + ADD_TRAIT(target, TRAIT_MUTE, BLOODSUCKER_TRAIT) + if(level_current >= 3) + ADD_TRAIT(target, TRAIT_BLIND, BLOODSUCKER_TRAIT) + mesmerized.Immobilize(power_time) + mesmerized.next_move = world.time + power_time + mesmerized.notransform = TRUE + addtimer(CALLBACK(src, .proc/end_mesmerize, user, target), power_time) + if(issilicon(target)) + var/mob/living/silicon/mesmerized = target + mesmerized.emp_act(EMP_HEAVY) + +/datum/action/bloodsucker/targeted/tremere/proc/end_mesmerize(mob/living/user, mob/living/target) + target.notransform = FALSE + REMOVE_TRAIT(target, TRAIT_BLIND, BLOODSUCKER_TRAIT) + REMOVE_TRAIT(target, TRAIT_MUTE, BLOODSUCKER_TRAIT) + if(istype(user) && target.stat == CONSCIOUS && (target in view(6, get_turf(user)))) + to_chat(owner, "[target] snapped out of their trance.") + +/datum/action/bloodsucker/targeted/tremere/dominate/proc/attempt_vassalize(mob/living/target, mob/living/user) + to_chat(owner, "Attempting to vassalize.") + if(!do_mob(user, target, 6 SECONDS, NONE, TRUE)) + return + + if(IS_VASSAL(target)) + PowerActivatedSuccessfully() + to_chat(user, span_warning("We revive [target]!")) + target.mind.grab_ghost() + target.revive(full_heal = TRUE, admin_revive = TRUE) + return + if(IS_MONSTERHUNTER(target)) + to_chat(target, span_notice("Their body refuses to react...")) + return + if(!bloodsuckerdatum_power.attempt_turn_vassal(target, TRUE)) + return + PowerActivatedSuccessfully() + to_chat(user, span_warning("We revive [target]!")) + target.mind.grab_ghost() + target.revive(full_heal = TRUE, admin_revive = TRUE) + var/living_time + if(level_current == 4) + living_time = 5 MINUTES + ADD_TRAIT(target, TRAIT_MUTE, BLOODSUCKER_TRAIT) + ADD_TRAIT(target, TRAIT_DEAF, BLOODSUCKER_TRAIT) + else if(level_current == 5) + living_time = 8 MINUTES + addtimer(CALLBACK(src, .proc/end_possession, target), living_time) + +/datum/action/bloodsucker/targeted/tremere/proc/end_possession(mob/living/user) + REMOVE_TRAIT(user, TRAIT_MUTE, BLOODSUCKER_TRAIT) + REMOVE_TRAIT(user, TRAIT_DEAF, BLOODSUCKER_TRAIT) + user.mind.remove_vassal() + to_chat(user, span_warning("You feel the Blood of your Master quickly flee!")) + user.death() diff --git a/code/modules/antagonists/bloodsuckers/powers/tremere/thaumaturgy.dm b/code/modules/antagonists/bloodsuckers/powers/tremere/thaumaturgy.dm new file mode 100644 index 000000000000..0effc500bcea --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/powers/tremere/thaumaturgy.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/bloodsucker/targeted/tremere/thaumaturgy + name = "Level 1: Thaumaturgy" + upgraded_power = /datum/action/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 + cooldown = 6 SECONDS + prefire_message = "Click where you wish to fire." + ///The shield this Power gives + var/datum/weakref/blood_shield + +/datum/action/bloodsucker/targeted/tremere/thaumaturgy/two + name = "Level 2: Thaumaturgy" + upgraded_power = /datum/action/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 = 4 SECONDS + +/datum/action/bloodsucker/targeted/tremere/thaumaturgy/three + name = "Level 3: Thaumaturgy" + upgraded_power = /datum/action/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 = 6 SECONDS + +/datum/action/bloodsucker/targeted/tremere/thaumaturgy/advanced + name = "Level 4: Blood Strike" + upgraded_power = /datum/action/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" + background_icon_state_on = "tremere_power_gold_on" + background_icon_state_off = "tremere_power_gold_off" + prefire_message = "Click where you wish to fire (using your power removes blood shield)." + bloodcost = 60 + cooldown = 6 SECONDS + +/datum/action/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.\n\ + If it hits a locker or door, it will break it open." + bloodcost = 80 + cooldown = 8 SECONDS + +/datum/action/bloodsucker/targeted/tremere/thaumaturgy/ActivatePower() + . = ..() + to_chat(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)) + 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/bloodsucker/targeted/tremere/thaumaturgy/DeactivatePower() + if(blood_shield) + QDEL_NULL(blood_shield) + return ..() + +/datum/action/bloodsucker/targeted/tremere/thaumaturgy/FireTargetedPower(atom/target_atom) + . = ..() + + var/mob/living/user = owner + 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/item/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, /obj/item/projectile.proc/fire) + playsound(user, 'sound/magic/wand_teleport.ogg', 60, TRUE) + PowerActivatedSuccessfully() + +/** + * # Blood Bolt + * + * This is the projectile this Power will fire. + */ +/obj/item/projectile/magic/arcane_barrage/bloodsucker + name = "blood bolt" + icon_state = "mini_leaper" + damage_type = BURN + damage = 20 + var/datum/action/bloodsucker/targeted/tremere/thaumaturgy/bloodsucker_power + +/obj/item/projectile/magic/arcane_barrage/bloodsucker/on_hit(target) + 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_icon() + 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_type = BURN + damage = 40 + if(bloodsucker_power.level_current >= 5) + var/mob/living/user = bloodsucker_power.owner + var/mob/living/person_hit = target + person_hit.blood_volume -= 100 + user.blood_volume += 100 + 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 = 'icons/obj/vamp_obj.dmi' + icon_state = "blood_shield" + lefthand_file = 'icons/mob/inhands/antag/bs_leftinhand.dmi' + righthand_file = 'icons/mob/inhands/antag/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/code/modules/antagonists/bloodsuckers/powers/veil.dm b/code/modules/antagonists/bloodsuckers/powers/veil.dm new file mode 100644 index 000000000000..3a7789810ad0 --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/powers/veil.dm @@ -0,0 +1,131 @@ +/datum/action/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 + purchase_flags = VASSAL_CAN_BUY + bloodcost = 15 + constant_bloodcost = 0.1 + cooldown = 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/bloodsucker/veil/ActivatePower() + . = ..() + cast_effect() // POOF +// if(blahblahblah) +// Disguise_Outfit() + veil_user() + +/* // Meant to disguise your character's clothing into fake ones. +/datum/action/bloodsucker/veil/proc/Disguise_Outfit() + return + // Step One: Back up original items +*/ + +/datum/action/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.hair_style + prev_facial_hair_style = user.facial_hair_style + 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 + randomize_human(user) + //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/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.hair_style = prev_hair_style + user.facial_hair_style = 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 + + +// CAST EFFECT // General effect (poof, splat, etc) when you cast. Doesn't happen automatically! +/datum/action/bloodsucker/veil/proc/cast_effect() + // Effect + playsound(get_turf(owner), 'sound/magic/smoke.ogg', 20, 1) + var/datum/effect_system/steam_spread/puff = new /datum/effect_system/steam_spread/() + puff.effect_type = /obj/effect/particle_effect/smoke/vampsmoke + 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/smoke/vampsmoke + opaque = FALSE + amount = 0 + lifetime = 0 + +/obj/effect/particle_effect/smoke/vampsmoke/fade_out(frames = 6) + ..(frames) diff --git a/code/modules/antagonists/bloodsuckers/structures/bloodsucker_coffin.dm b/code/modules/antagonists/bloodsuckers/structures/bloodsucker_coffin.dm new file mode 100644 index 000000000000..801295bbe5f0 --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/structures/bloodsucker_coffin.dm @@ -0,0 +1,271 @@ +/datum/antagonist/bloodsucker/proc/ClaimCoffin(obj/structure/closet/crate/claimed) + // 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(!(/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) + to_chat(owner, span_danger("You learned new recipes - You can view them in the Tribal section of the crafting menu!")) + // This is my Lair + coffin = claimed + lair = get_area(claimed) + to_chat(owner, span_userdanger("You have claimed the [claimed] as your place of immortal rest! Your lair is now [lair].")) + to_chat(owner, span_announce("Bloodsucker Tip: Find new lair recipes in the Misc tab of the Crafting Menu, including the Persuasion Rack for converting crew into Vassals.")) + return TRUE + +/// From crate.dm +/obj/structure/closet/crate + var/mob/living/resident /// This lets bloodsuckers claim any "crate" as a Coffin. + var/pryLidTimer = 25 SECONDS + breakout_time = 20 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 = 'icons/obj/vamp_obj.dmi' + open_sound = 'sound/effects/coffin_open.ogg' + close_sound = 'sound/effects/coffin_close.ogg' + breakout_time = 30 SECONDS + pryLidTimer = 20 SECONDS + resistance_flags = NONE + material_drop = /obj/item/stack/sheet/metal + material_drop_amount = 2 + armor = list(MELEE = 50, BULLET = 20, LASER = 30, ENERGY = 0, BOMB = 50, BIO = 0, 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 = 'icons/obj/vamp_obj.dmi' + open_sound = 'sound/effects/coffin_open.ogg' + close_sound = 'sound/effects/coffin_close.ogg' + breakout_time = 35 SECONDS + pryLidTimer = 35 SECONDS + resistance_flags = FIRE_PROOF | LAVA_PROOF | ACID_PROOF + material_drop = /obj/item/stack/sheet/metal + material_drop_amount = 2 + armor = list(MELEE = 35, BULLET = 20, LASER = 20, ENERGY = 0, BOMB = 100, BIO = 0, 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 = 'icons/obj/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 + pryLidTimer = 20 SECONDS + material_drop = /obj/item/reagent_containers/food/snacks/meat/slab + material_drop_amount = 3 + armor = list(MELEE = 70, BULLET = 10, LASER = 10, ENERGY = 0, BOMB = 70, BIO = 0, 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 = 'icons/obj/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 + pryLidTimer = 30 SECONDS + material_drop = /obj/item/stack/sheet/metal + armor = list(MELEE = 40, BULLET = 15, LASER = 50, ENERGY = 0, BOMB = 10, BIO = 0, FIRE = 70, ACID = 60) + +////////////////////////////////////////////// + +/// NOTE: This can be any coffin that you are resting AND inside of. +/obj/structure/closet/crate/coffin/proc/ClaimCoffin(mob/living/claimant) + // Bloodsucker Claim + var/datum/antagonist/bloodsucker/bloodsuckerdatum = claimant.mind.has_antag_datum(/datum/antagonist/bloodsucker) + if(bloodsuckerdatum) + // Successfully claimed? + if(bloodsuckerdatum.ClaimCoffin(src)) + resident = claimant + anchored = TRUE + START_PROCESSING(SSprocessing, src) + +/obj/structure/closet/crate/coffin/Destroy() + UnclaimCoffin() + STOP_PROCESSING(SSprocessing, src) + return ..() + +/obj/structure/closet/crate/coffin/process(mob/living/user) + . = ..() + if(!.) + return FALSE + if(user in src) + var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(/datum/antagonist/bloodsucker) + if(!bloodsuckerdatum) + return FALSE + if(bloodsuckerdatum.lair != get_area(bloodsuckerdatum.coffin)) + if(bloodsuckerdatum.coffin) + bloodsuckerdatum.coffin.UnclaimCoffin() + var/list/turf/area_turfs = get_area_turfs(bloodsuckerdatum.lair) + // Create Dirt etc. + var/turf/T_Dirty = pick(area_turfs) + if(T_Dirty && !T_Dirty.density) + // Default: Dirt + // CHECK: Cobweb already there? + //if (!locate(var/obj/effect/decal/cleanable/cobweb) in T_Dirty) // REMOVED! Cleanables don't stack. + // 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) + // STEP TWO: DIRT + new /obj/effect/decal/cleanable/dirt (T_Dirty) + // Find Animals in Area +/* if(rand(0,2) == 0) + var/mobCount = 0 + var/mobMax = clamp(area_turfs.len / 25, 1, 4) + for(var/turf/lair_turfs in area_turfs) + if(!lair_turfs) + continue + var/mob/living/simple_animal/SA = locate() in lair_turfs + if(SA) + mobCount++ + if(mobCount >= mobMax) // Already at max + break + Spawn One + if(mobCount < mobMax) +// Seek Out Location + while(area_turfs.len > 0) + var/turf/lair_turfs = pick(area_turfs) // We use while&pick instead of a for/loop so it's random, rather than from the top of the list. + if(lair_turfs && !lair_turfs.density) + var/mob/living/simple_animal/selected_simplemob = /mob/living/simple_animal/mouse // pick(/mob/living/simple_animal/mouse,/mob/living/simple_animal/mouse,/mob/living/simple_animal/mouse, /mob/living/simple_animal/hostile/retaliate/bat) //prob(300) /mob/living/simple_animal/mouse, + new selected_simplemob(lair_turfs) + break + area_turfs -= lair_turfs*/ + +/obj/structure/closet/crate/proc/UnclaimCoffin(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.lair = 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, span_cultitalic("You have unclaimed your coffin! This also unclaims all your other Bloodsucker structures!")) + else + to_chat(resident, span_cultitalic("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] is 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 + if(!bloodsuckerdatum.coffin && !resident) + switch(alert(user,"Do you wish to claim this as your coffin? [get_area(src)] will be your lair.","Claim Lair", list("Yes", "No"))) + if("Yes") + ClaimCoffin(user) + LockMe(user) + /// Level up? Auto-Fails if not appropriate - Ventrue cannot level up in a Coffin. + if(bloodsuckerdatum.my_clan != CLAN_VENTRUE) + 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) + 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 && istype(item, /obj/item/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 && istype(item, /obj/item/crowbar)) + var/pry_time = pryLidTimer * 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_mob(user, src, pry_time)) + 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 + . = ..() + +/// 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)) + switch(alert(user,"Do you wish to unclaim your coffin?","Unclaim Lair", list("Yes", "No"))) + if("Yes") + UnclaimCoffin(TRUE) + LockMe(user) + if("No") + return + return TRUE +/obj/structure/closet/crate/proc/LockMe(mob/user, inLocked = TRUE) + if(user == resident) + if(!broken) + locked = inLocked + to_chat(user, span_notice("You flip a secret latch and [locked?"":"un"]lock yourself inside [src].")) + return + // Broken? Let's fix it. + to_chat(resident, span_notice("The secret latch to lock [src] from the inside is broken. You set it back into place...")) + if(!do_mob(resident, src, 5 SECONDS)) + 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/code/modules/antagonists/bloodsuckers/structures/bloodsucker_crypt.dm b/code/modules/antagonists/bloodsuckers/structures/bloodsucker_crypt.dm new file mode 100644 index 000000000000..d64d7a0e363a --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/structures/bloodsucker_crypt.dm @@ -0,0 +1,722 @@ +/obj/structure/bloodsucker + ///Who owns this structure? + var/mob/living/owner + /* + * # Descriptions + * + * 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/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) + if(IS_MONSTERHUNTER(user) && Hunter_desc != "") + . += span_cult(Hunter_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 the Persuasion Rack to understand how it functions!")) + 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.lair) + to_chat(user, span_danger("You don't have a lair. Claim a coffin to make that location your lair.")) + return FALSE + if(bloodsuckerdatum.lair != get_area(src)) + to_chat(user, span_danger("You may only activate this structure in your lair: [bloodsuckerdatum.lair].")) + return FALSE + + /// Radial menu for securing your Persuasion rack in place. + switch(alert(user, "Do you wish to secure [src] here?", list("Yes","No"))) + 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)) + switch(alert(user,"Unbolt [src]?", list("Yes", "No"))) + 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 = 'icons/obj/vamp_obj.dmi' + icon_state = "vassalrack" + anchored = FALSE + /// Start dense. Once fixed in place, go non-dense. + 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\ + Once you have Vassals ready, you are able to select a Favorite Vassal;\n\ + Click the Rack as a Vassal is buckled onto it to turn them into your Favorite. This can only be done once, so choose carefully!\n\ + This process costs 150 Blood to do, and will make your Vassal unable to be deconverted, outside of you reaching Final Death." + 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." + /// So we can't spam buckle people onto the rack + var/use_lock = FALSE + var/mob/buckled + /// 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 + +/obj/structure/bloodsucker/vassalrack/deconstruct(disassembled = TRUE) + . = ..() + new /obj/item/stack/sheet/metal(src.loc, 4) + new /obj/item/stack/rods(loc, 4) + qdel(src) + +/obj/structure/bloodsucker/vassalrack/bolt() + . = ..() + density = FALSE + anchored = TRUE + +/obj/structure/bloodsucker/vassalrack/unbolt() + . = ..() + density = TRUE + 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) || use_lock || 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 Silicon cannot be vassalized, therefore it is useless to buckle them.")) + return + // Good to go - Buckle them! + use_lock = TRUE + if(do_mob(user, living_target, 5 SECONDS)) + attach_victim(living_target, user) + use_lock = FALSE + +/// Attempt Release (Owner vs Non Owner) +/obj/structure/bloodsucker/vassalrack/MouseDrop(mob/user, modifiers) + . = ..() + if(!user.canUseTopic(src, BE_CLOSE)) + return + if(!has_buckled_mobs() || !isliving(user) || use_lock) + 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) + +/// Attempt Buckle +/obj/structure/bloodsucker/vassalrack/proc/attach_victim(mob/living/target, mob/living/user) + // Standard Buckle Check + target.forceMove(get_turf(src)) + 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(src.loc, 'sound/effects/pop_expl.ogg', 25, 1) + density = TRUE + update_icon() + + // Set up Torture stuff now + convert_progress = 3 + disloyalty_confirm = FALSE + disloyalty_offered = FALSE + +/// Attempt Unbuckle +/obj/structure/bloodsucker/vassalrack/user_unbuckle_mob(mob/living/buckled_mob, mob/user) + if(!IS_BLOODSUCKER(user) || !IS_VASSAL(user)) + 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."), + ) + 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."), + ) + // Monster hunters are used to this sort of stuff, they know how they work, which includes breaking others out + var/breakout_timer = IS_MONSTERHUNTER(user) ? 20 SECONDS : 10 SECONDS + if(!do_mob(user, buckled_mob, breakout_timer)) + return + unbuckle_mob(buckled_mob) + . = ..() + +/obj/structure/bloodsucker/vassalrack/unbuckle_mob(mob/living/buckled_mob, force = FALSE, can_fall = TRUE) + . = ..() + if(!.) + return FALSE + src.visible_message(span_danger("[buckled_mob][buckled_mob.stat == DEAD ? "'s corpse" : ""] slides off of the rack.")) + density = FALSE + buckled_mob.Paralyze(3 SECONDS) + update_icon() + use_lock = FALSE // Failsafe + return TRUE + +/obj/structure/bloodsucker/vassalrack/attack_hand(mob/user, list/modifiers) + . = ..() + if(!.) + return FALSE + var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(/datum/antagonist/bloodsucker) + // Is there anyone on the rack & If so, are they being tortured? + if(use_lock || !has_buckled_mobs()) + return FALSE + 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 + var/datum/antagonist/vassal/vassaldatum = IS_VASSAL(buckled_carbons) + // Are they our Vassal, or Dead? + if(istype(vassaldatum) && vassaldatum.master == bloodsuckerdatum || buckled_carbons.stat >= DEAD) + // Can we assign a Favorite Vassal? + if(istype(vassaldatum) && !bloodsuckerdatum.has_favorite_vassal) + if(buckled_carbons.mind.can_make_bloodsucker(buckled_carbons.mind)) + offer_favorite_vassal(user, buckled_carbons) + use_lock = FALSE + return + + // Not our Vassal, but Alive & We're a Bloodsucker, good to torture! + torture_victim(user, buckled_carbons) + +/** + * Step One: Tick Down Conversion from 3 to 0 + * Step Two: Break mindshielding/antag (on approve) + * Step Three: Blood Ritual + */ + +/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) + /// Prep... + use_lock = TRUE + /// Conversion Process + if(convert_progress > 0) + to_chat(user, span_notice("You spill some blood and prepare to initiate [target] into your service.")) + bloodsuckerdatum.AddBloodVolume(-TORTURE_BLOOD_COST) + if(!do_torture(user,target)) + to_chat(user, span_danger("The ritual has been interrupted!")) + else + /// 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 <= 0) + if(IS_VASSAL(target)) + var/datum/antagonist/vassal/vassaldatum = target.mind.has_antag_datum(/datum/antagonist/vassal) + if(!vassaldatum.master.broke_masquerade) + to_chat(user, span_boldwarning("[target] is under the spell of another Bloodsucker!")) + return + if(RequireDisloyalty(user, target)) + to_chat(user, span_boldwarning("[target] has external loyalties! [target.p_they(TRUE)] will require more persuasion to break [target.p_them()] to your will!")) + else + to_chat(user, span_notice("[target] looks ready for the Dark Communion.")) + /// Otherwise, we're not done, we need to persuade them some more. + else + to_chat(user, span_notice("[target] could use [convert_progress == 1 ? "a little" : "some"] more persuasion.")) + use_lock = FALSE + return + /// Check: Mindshield & Antag + if(!disloyalty_confirm && RequireDisloyalty(user, target)) + if(!do_disloyalty(user,target)) + to_chat(user, span_danger("The ritual has been interrupted!")) + else if(!disloyalty_confirm) + to_chat(user, span_danger("[target] refuses to give into your persuasion. Perhaps a little more?")) + else + to_chat(user, span_notice("[target] looks ready for the Dark Communion.")) + use_lock = FALSE + return + user.visible_message( + span_notice("[user] marks a bloody smear on [target]'s forehead and puts a wrist up to [target.p_their()] mouth!"), + span_notice("You paint a bloody marking across [target]'s forehead, place your wrist to [target.p_their()] mouth, and subject [target.p_them()] to the Dark Communion."), + ) + if(!do_mob(user, src, 5 SECONDS)) + to_chat(user, span_danger("The ritual has been interrupted!")) + use_lock = FALSE + return + /// Convert to Vassal! + bloodsuckerdatum.AddBloodVolume(-TORTURE_CONVERSION_COST) + if(bloodsuckerdatum && bloodsuckerdatum.attempt_turn_vassal(target)) + if(HAS_TRAIT(target, TRAIT_MINDSHIELD)) + remove_loyalties(target) + if(bloodsuckerdatum.my_clan == CLAN_TREMERE) + to_chat(user, span_danger("You have now gained an additional Rank to spend!")) + bloodsuckerdatum.bloodsucker_level_unspent++ + user.playsound_local(null, 'sound/effects/explosion_distant.ogg', 40, TRUE) + target.playsound_local(null, 'sound/effects/explosion_distant.ogg', 40, TRUE) + target.playsound_local(null, 'sound/effects/singlebeat.ogg', 40, TRUE) + target.Jitter(15) + INVOKE_ASYNC(target, /mob.proc/emote, "laugh") + //remove_victim(target) // Remove on CLICK ONLY! + use_lock = FALSE + +/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 + /// Get Bodypart + var/target_string = "" + var/obj/item/bodypart/selected_bodypart = null + selected_bodypart = pick(target.bodyparts) + if(selected_bodypart) + target_string += selected_bodypart.name + /// Get Weapon + var/obj/item/held_item = user.get_active_held_item() + if(!istype(held_item)) + held_item = user.get_inactive_held_item() + /// Weapon Bonus + if(held_item) + torture_time -= held_item.force / 4 + torture_dmg_brute += held_item.force / 4 + //torture_dmg_burn += I. + if(held_item.sharpness == SHARP_EDGED) + torture_time -= 2 + else if(held_item.sharpness == SHARP_POINTY) + torture_time -= 3 + /// This will hurt your eyes. + else if(held_item.tool_behaviour == TOOL_WELDER) + if(held_item.use_tool(src, user, 0, volume = 5)) + torture_time -= 6 + torture_dmg_burn += 5 + held_item.play_tool_sound(target) + /// Minimum 5 seconds. + torture_time = max(50, torture_time * 10) + /// Now run process. + if(!do_mob(user, target, torture_time * mult)) + return FALSE + /// Success? + if(held_item) + playsound(loc, held_item.hitsound, 30, 1, -1) + held_item.play_tool_sound(target) + target.visible_message( + span_danger("[user] performs a ritual, spilling some of [target]'s blood from their [target_string] and shaking them up!"), + span_userdanger("[user] performs a ritual, spilling some blood from your [target_string], shaking you up!"), + ) + INVOKE_ASYNC(target, /mob.proc/emote, "scream") + target.Jitter(5) + target.apply_damages(brute = torture_dmg_brute, burn = torture_dmg_burn, def_zone = (selected_bodypart ? selected_bodypart.body_zone : null)) // take_overall_damage(6,0) + return TRUE + +/// Offer them the oppertunity to join now. +/obj/structure/bloodsucker/vassalrack/proc/do_disloyalty(mob/living/user, mob/living/target) + spawn(10) + /// Are we still torturing? Did we cancel? Are they still here? + if(use_lock && target && target.client) + to_chat(user, span_notice("[target] has been given the opportunity for servitude. You await their decision...")) + var/alert_text = "You are being tortured! Do you want to give in and pledge your undying loyalty to [user]?" + /* if(HAS_TRAIT(target, TRAIT_MINDSHIELD)) + alert_text += "\n\nYou will no longer be loyal to the station!" */ + alert_text += "\n\nYou will not lose your current objectives, but they come second to the will of your new master!" + to_chat(target, span_cultlarge("THE HORRIBLE PAIN! WHEN WILL IT END?!")) + var/list/torture_icons = list( + "Accept" = image(icon = 'icons/mob/actions/actions_bloodsucker.dmi', icon_state = "power_recup"), + "Refuse" = image(icon = 'icons/obj/items_and_weapons.dmi', icon_state = "stunbaton_active") + ) + var/torture_response = show_radial_menu(target, src, torture_icons, radius = 36, require_near = TRUE) + switch(torture_response) + if("Accept") + disloyalty_accept(target) + else + disloyalty_refuse(target) + if(!do_torture(user,target, 2)) + return FALSE + + // NOTE: We only remove loyalties when we're CONVERTED! + return TRUE + +/obj/structure/bloodsucker/vassalrack/proc/RequireDisloyalty(mob/living/user, mob/living/target) + var/datum/antagonist/bloodsucker/bloodsuckerdatum = IS_BLOODSUCKER(user) + return bloodsuckerdatum.AmValidAntag(target) || HAS_TRAIT(target, TRAIT_MINDSHIELD) + +/obj/structure/bloodsucker/vassalrack/proc/disloyalty_accept(mob/living/target) + // FAILSAFE: Still on the rack? + if(!(locate(target) in buckled_mobs)) + return + // NOTE: You can say YES after torture. It'll apply to next time. + disloyalty_confirm = TRUE + if(HAS_TRAIT(target, TRAIT_MINDSHIELD)) + to_chat(target, span_boldnotice("You give in to the will of your torturer. If they are successful, you will no longer be loyal to the station!")) + +/obj/structure/bloodsucker/vassalrack/proc/disloyalty_refuse(mob/living/target) + // FAILSAFE: Still on the rack? + if(!(locate(target) in buckled_mobs)) + return + // Failsafe: You already said YES. + if(disloyalty_confirm) + return + to_chat(target, span_notice("You refuse to give in! You will not break!")) + +/obj/structure/bloodsucker/vassalrack/proc/remove_loyalties(mob/living/target) + // Find Mind Implant & Destroy + if(HAS_TRAIT(target, TRAIT_MINDSHIELD)) + for(var/obj/item/implant/all_implants in target.implants) + if(all_implants.type == /obj/item/implant/mindshield) + all_implants.removed(target, silent = TRUE) + +/obj/structure/bloodsucker/vassalrack/proc/offer_favorite_vassal(mob/living/carbon/human/user, mob/living/target) + var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(/datum/antagonist/bloodsucker) + var/datum/antagonist/vassal/vassaldatum = target.mind.has_antag_datum(/datum/antagonist/vassal) + + switch(alert(user, "Would you like to turn this Vassal into your completely loyal Servant? This costs 150 Blood to do. You cannot undo this.", list("Yes", "No"))) + if("Yes") + user.blood_volume -= 150 + bloodsuckerdatum.has_favorite_vassal = TRUE + vassaldatum.make_favorite(user) + else + to_chat(user, span_danger("You decide not to turn [target] into your Favorite Vassal.")) + use_lock = FALSE + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/obj/structure/bloodsucker/candelabrum + name = "candelabrum" + desc = "It burns slowly, but doesn't radiate any heat." + icon = 'icons/obj/vamp_obj.dmi' + icon_state = "candelabrum" + light_color = "#66FFFF"//LIGHT_COLOR_BLUEGREEN // lighting.dm + light_power = 3 + light_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/proc/update_icon_state() + icon_state = "candelabrum[lit ? "_lit" : ""]" + return ..() + +/obj/structure/bloodsucker/candelabrum/examine(mob/user) + . = ..() + var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(/datum/antagonist/bloodsucker) + if(bloodsuckerdatum.my_clan == CLAN_VENTRUE) + . += span_cult("As part of the Ventrue Clan, you can Rank Up your Favorite Vassal.\n\ + Drag your Vassal's sprite onto the Candelabrum to secure them in place. From there, Clicking will Rank them up, while Right-click will unbuckle, as long as you are in reach.\n\ + Ranking up a Vassal will rank up what powers you currently have, and will allow you to choose what Power your Favorite Vassal will recieve.") + +/obj/structure/bloodsucker/candelabrum/bolt() + . = ..() + anchored = TRUE + density = TRUE + +/obj/structure/bloodsucker/candelabrum/unbolt() + . = ..() + anchored = FALSE + density = FALSE + +/obj/structure/bloodsucker/candelabrum/MouseDrop(mob/user, modifiers) + . = ..() + if(!has_buckled_mobs() || !isliving(user)) + return + var/mob/living/carbon/target = pick(buckled_mobs) + if(target) + unbuckle_mob(target, user) + +/obj/structure/bloodsucker/candelabrum/proc/toggle(mob/user) + lit = !lit + if(lit) + set_light(2, 3, "#66FFFF") + START_PROCESSING(SSobj, src) + else + 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)) + continue + nearly_people.hallucination += 5 + SEND_SIGNAL(nearly_people, COMSIG_ADD_MOOD_EVENT, "vampcandle", /datum/mood_event/vampcandle) + +/* + * # Candelabrum Ventrue Stuff + * + * Ventrue Bloodsuckers can buckle Vassals onto the Candelabrum to "Upgrade" them. + * This is limited to a Single vassal, called 'My Favorite Vassal'. + * + * Most of this is just copied over from Persuasion Rack. + */ + +/obj/structure/bloodsucker/candelabrum/attack_hand(mob/living/user, list/modifiers) + . = ..() + if(!.) + return + if(!anchored) + return + var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(/datum/antagonist/bloodsucker) + // Checks: We're Ventrue, they're Buckled & Alive. + if(bloodsuckerdatum && bloodsuckerdatum.my_clan == CLAN_VENTRUE) + if(!has_buckled_mobs()) + toggle() + return + var/mob/living/carbon/target = pick(buckled_mobs) + if(target.stat >= DEAD) + unbuckle_mob(target) + return + // Are we spending a Rank? + if(!bloodsuckerdatum.bloodsucker_level_unspent <= 0) + bloodsuckerdatum.SpendRank(target) + else if(user.blood_volume >= 550) + // We don't have any ranks to spare? Let them upgrade... with enough Blood. + switch(alert(user, "Do you wish to spend 550 Blood to Rank [target] up?", list("Yes","No"))) + if("Yes") + user.blood_volume -= 550 + bloodsuckerdatum.SpendRank(target, spend_rank = FALSE) + return + else + // Neither? Shame. Goodbye! + to_chat(user, span_danger("You don't have any levels or enough Blood to Rank [target] up with.")) + return + + if(IS_BLOODSUCKER(user) || IS_VASSAL(user)) + toggle() + +/// Buckling someone in +/obj/structure/bloodsucker/candelabrum/MouseDrop_T(mob/living/target, mob/user) + if(!anchored && IS_BLOODSUCKER(user)) + to_chat(user, span_danger("Until the candelabrum is secured in place, it cannot serve its purpose.")) + return + /// Default checks + if(!target.Adjacent(src) || target == user || !isliving(user) || has_buckled_mobs() || user.incapacitated() || target.buckled) + return + var/datum/antagonist/bloodsucker/bloodsuckerdatum = IS_BLOODSUCKER(user) + var/datum/antagonist/vassal/vassaldatum = IS_VASSAL(target) + /// Are you even a Bloodsucker? + if(!bloodsuckerdatum || !vassaldatum) + return + /// Are you part of Ventrue? No? Then go away. + if(!bloodsuckerdatum.my_clan == CLAN_VENTRUE) + return + /// Are they a Favorite Vassal? + if(!vassaldatum.favorite_vassal) + return + /// They are a Favorite vassal, but are they OUR Vassal? + if(!vassaldatum.master == bloodsuckerdatum) + return + + /// Good to go - Buckle them! + if(do_mob(user, target, 5 SECONDS)) + attach_mob(target, user) + +/obj/structure/bloodsucker/candelabrum/proc/attach_mob(mob/living/target, mob/living/user) + user.visible_message( + span_notice("[user] lifts and buckles [target] onto the candelabrum."), + span_boldnotice("You buckle [target] onto the candelabrum."), + ) + + playsound(src.loc, 'sound/effects/pop_expl.ogg', 25, 1) + target.forceMove(get_turf(src)) + + if(!buckle_mob(target)) + return + update_icon() + +/// Attempt Unbuckle +/obj/structure/bloodsucker/candelabrum/unbuckle_mob(mob/living/buckled_mob, force = FALSE, can_fall = TRUE) + . = ..() + src.visible_message(span_danger("[buckled_mob][buckled_mob.stat==DEAD?"'s corpse":""] slides off of the candelabrum.")) + update_icon() + +/// 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 = 'icons/obj/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, ROTATION_ALTCLICK | ROTATION_CLOCKWISE) + armrest = GetArmrest() + armrest.layer = ABOVE_MOB_LAYER + return ..() + +/obj/structure/bloodsucker/bloodthrone/Destroy() + QDEL_NULL(armrest) + return ..() + +/obj/structure/bloodsucker/bloodthrone/bolt() + . = ..() + anchored = TRUE + +/obj/structure/bloodsucker/bloodthrone/unbolt() + . = ..() + anchored = FALSE + +// Armrests +/obj/structure/bloodsucker/bloodthrone/proc/GetArmrest() + return mutable_appearance('icons/obj/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/handle_speech) + else + user.Paralyze(6 SECONDS) + to_chat(user, span_cult("The power of the blood throne overwhelms you!")) + user.apply_damage(10, BRUTE) + unbuckle_mob(user) + +/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, mob/speech_args) + + 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) + for(var/mob/living/carbon/human/vassals in GLOB.player_list) + var/datum/antagonist/vassal/vassaldatum = vassals.mind.has_antag_datum(/datum/antagonist/vassal) + if(vassals == user) // Just so they can hear themselves speak. + to_chat(vassals, rendered) + if(!istype(vassaldatum)) + continue + if(vassaldatum.master.owner == user.mind) + to_chat(vassals, rendered) + + for(var/mob/dead_mob in GLOB.dead_mob_list) + var/link = FOLLOW_LINK(dead_mob, user) + to_chat(dead_mob, "[link] [rendered]") + + speech_args[SPEECH_MESSAGE] = "" diff --git a/code/modules/antagonists/bloodsuckers/structures/bloodsucker_life.dm b/code/modules/antagonists/bloodsuckers/structures/bloodsucker_life.dm new file mode 100644 index 000000000000..349023313440 --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/structures/bloodsucker_life.dm @@ -0,0 +1,443 @@ +/// Runs from COMSIG_LIVING_BIOLOGICAL_LIFE, handles Bloodsucker constant proccesses. +/datum/antagonist/bloodsucker/proc/LifeTick() + + if(!owner) + INVOKE_ASYNC(src, .proc/HandleDeath) + return + // Deduct Blood + var/mob/living/O = owner + if(owner.current.stat == CONSCIOUS && !O.IsImmobilized() && !HAS_TRAIT(owner.current, TRAIT_NODEATH)) + INVOKE_ASYNC(src, .proc/AddBloodVolume, passive_blood_drain) // -.1 currently + if(HandleHealing(1)) + if((COOLDOWN_FINISHED(src, bloodsucker_spam_healing)) && owner.current.blood_volume > 0) + to_chat(owner.current, span_notice("The power of your blood begins knitting your wounds...")) + COOLDOWN_START(src, bloodsucker_spam_healing, BLOODSUCKER_SPAM_HEALING) + // Standard Updates + INVOKE_ASYNC(src, .proc/HandleDeath) + INVOKE_ASYNC(src, .proc/HandleStarving) + INVOKE_ASYNC(src, .proc/HandleTorpor) + + // Clan-unique Checks + if(my_clan == CLAN_TREMERE) + var/area/current_area = get_area(owner.current) + if(istype(current_area, /area/chapel)) + to_chat(owner.current, span_warning("You don't belong in holy areas!")) + owner.current.adjustFireLoss(10) + owner.current.adjust_fire_stacks(2) + owner.current.IgniteMob() + if(my_clan == CLAN_MALKAVIAN) + if(prob(85) || owner.current.stat != CONSCIOUS || HAS_TRAIT(owner.current, TRAIT_MASQUERADE)) + return + var/message = pick(strings("malkavian_revelations.json", "revelations", "fulp_modules/strings/bloodsuckers")) + INVOKE_ASYNC(owner.current, /atom/movable/proc/say, message, , , , , , CLAN_MALKAVIAN) + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// BLOOD +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/datum/antagonist/bloodsucker/proc/AddBloodVolume(value) + owner.current.blood_volume = clamp(owner.current.blood_volume + value, 0, max_blood_volume) + update_hud() + +/datum/antagonist/bloodsucker/proc/AddHumanityLost(value) + if(humanity_lost >= 500) + to_chat(owner.current, span_warning("You hit the maximum amount of lost Humanty, you are far from Human.")) + return + humanity_lost += value + to_chat(owner.current, span_warning("You feel as if you lost some of your humanity, you will now enter Frenzy at [FRENZY_THRESHOLD_ENTER + (humanity_lost * 10)] Blood.")) + +/// mult: SILENT feed is 1/3 the amount +/datum/antagonist/bloodsucker/proc/HandleFeeding(mob/living/carbon/target, mult=1, power_level) + // Starts at 15 (now 8 since we doubled the Feed time) + var/feed_amount = 15 + (power_level * 2) + var/blood_taken = min(feed_amount, target.blood_volume) * mult + target.blood_volume -= blood_taken + // Simple Animals lose a LOT of blood, and take damage. This is to keep cats, cows, and so forth from giving you insane amounts of blood. + if(!ishuman(target)) + target.blood_volume -= (blood_taken / max(target.mob_size, 0.1)) * 3.5 // max() to prevent divide-by-zero + target.apply_damage_type(blood_taken / 3.5) // Don't do too much damage, or else they die and provide no blood nourishment. + if(target.blood_volume <= 0) + target.blood_volume = 0 + target.death(0) + /////////// + // Shift Body Temp (toward Target's temp, by volume taken) + owner.current.bodytemperature = ((owner.current.blood_volume * owner.current.bodytemperature) + (blood_taken * target.bodytemperature)) / (owner.current.blood_volume + blood_taken) + // our volume * temp, + their volume * temp, / total volume + /////////// + // Reduce Value Quantity + if(target.stat == DEAD) // Penalty for Dead Blood + blood_taken /= 3 + if(!ishuman(target)) // Penalty for Non-Human Blood + blood_taken /= 2 + //if (!iscarbon(target)) // Penalty for Animals (they're junk food) + // Apply to Volume + AddBloodVolume(blood_taken) + // Reagents (NOT Blood!) + if(target.reagents && target.reagents.total_volume) + target.reagents.trans_to(owner.current, INGEST, 1) // Run transfer of 1 unit of reagent from them to me. + owner.current.playsound_local(null, 'sound/effects/singlebeat.ogg', 40, 1) // Play THIS sound for user only. The "null" is where turf would go if a location was needed. Null puts it right in their head. + total_blood_drank += blood_taken + if(frenzied) + frenzy_blood_drank += blood_taken + return blood_taken + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// HEALING + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Constantly runs on Bloodsucker's LifeTick, and is increased by being in Torpor/Coffins +/datum/antagonist/bloodsucker/proc/HandleHealing(mult = 1) + var/actual_regen = bloodsucker_regen_rate + additional_regen + // Don't heal if I'm staked or on Masquerade (+ not in a Coffin). Masqueraded Bloodsuckers in a Coffin however, will heal. + if(owner.current.AmStaked() || (HAS_TRAIT(owner.current, TRAIT_MASQUERADE) && !HAS_TRAIT(owner.current, TRAIT_NODEATH))) + return FALSE + owner.current.adjustCloneLoss(-1 * (actual_regen * 4) * mult, 0) + owner.current.adjustOrganLoss(ORGAN_SLOT_BRAIN, -1 * (actual_regen * 4) * mult) //adjustBrainLoss(-1 * (actual_regen * 4) * mult, 0) + if(!iscarbon(owner.current)) // Damage Heal: Do I have damage to ANY bodypart? + return + var/mob/living/carbon/user = owner.current + var/costMult = 1 // Coffin makes it cheaper + var/bruteheal = min(user.getBruteLoss(), actual_regen) // BRUTE: Always Heal + var/fireheal = 0 // BURN: Heal in Coffin while Fakedeath, or when damage above maxhealth (you can never fully heal fire) + /// Checks if you're in a coffin here, additionally checks for Torpor right below it. + var/amInCoffin = istype(user.loc, /obj/structure/closet/crate/coffin) + if(amInCoffin && HAS_TRAIT(user, TRAIT_NODEATH)) + if(HAS_TRAIT(owner.current, TRAIT_MASQUERADE)) + to_chat(user, span_warning("You will not heal while your Masquerade ability is active.")) + return + fireheal = min(user.getFireLoss(), actual_regen) + mult *= 5 // Increase multiplier if we're sleeping in a coffin. + costMult /= 2 // Decrease cost if we're sleeping in a coffin. + user.ExtinguishMob() + user.remove_all_embedded_objects() // Remove Embedded! + if(check_limbs(costMult)) + return TRUE + // In Torpor, but not in a Coffin? Heal faster anyways. + else if(HAS_TRAIT(user, TRAIT_NODEATH)) + mult *= 3 + // Heal if Damaged + if((bruteheal + fireheal > 0) && mult != 0) // Just a check? Don't heal/spend, and return. + // We have damage. Let's heal (one time) + user.adjustBruteLoss(-bruteheal * mult, forced=TRUE) // Heal BRUTE / BURN in random portions throughout the body. + user.adjustFireLoss(-fireheal * mult, forced=TRUE) + AddBloodVolume(((bruteheal * -0.5) + (fireheal * -1)) * costMult * mult) // Costs blood to heal + return TRUE + +/datum/antagonist/bloodsucker/proc/check_limbs(costMult = 1) + var/limb_regen_cost = 50 * -costMult + var/mob/living/carbon/user = owner.current + var/list/missing = user.get_missing_limbs() + if(missing.len && user.blood_volume < limb_regen_cost + 5) + return FALSE + for(var/targetLimbZone in missing) // 1) Find ONE Limb and regenerate it. + user.regenerate_limb(targetLimbZone, FALSE) // regenerate_limbs() <--- If you want to EXCLUDE certain parts, do it like this ----> regenerate_limbs(0, list("head")) + AddBloodVolume(limb_regen_cost) + var/obj/item/bodypart/missing_bodypart = user.get_bodypart(targetLimbZone) // 2) Limb returns Damaged + missing_bodypart.brute_dam = 60 + to_chat(user, span_notice("Your flesh knits as it regrows your [missing_bodypart]!")) + playsound(user, 'sound/magic/demon_consume.ogg', 50, TRUE) + return TRUE + +/* + * # Heal Vampire Organs + * + * This is used by Bloodsuckers, these are the steps of this proc: + * Step 1 - Cure husking and Regenerate organs. regenerate_organs() removes their Vampire Heart & Eye augments, which leads us to... + * Step 2 - Repair any (shouldn't be possible) Organ damage, then return their Vampiric Heart & Eye benefits. + * Step 3 - Revive them, clear all wounds, remove any Tumors (If any). + * + * This is called on Bloodsucker's Assign, and when they end Torpor. + */ + +/datum/antagonist/bloodsucker/proc/HealVampireOrgans() + var/mob/living/carbon/bloodsuckeruser = owner.current + + // Step 1 - Fix basic things, husk and organs. + bloodsuckeruser.cure_husk() + bloodsuckeruser.regenerate_organs() + + // Step 2 NOTE: Giving passive organ regeneration will cause Torpor to spam /datum/client_colour/monochrome at the Bloodsucker, permanently making them colorblind! + for(var/all_organs in bloodsuckeruser.internal_organs) + var/obj/item/organ/organ = all_organs + organ.setOrganDamage(0) + var/obj/item/organ/heart/current_heart = bloodsuckeruser.getorganslot(ORGAN_SLOT_HEART) + if(!istype(current_heart, /obj/item/organ/heart/vampheart) && !istype(current_heart, /obj/item/organ/heart/demon) && !istype(current_heart, /obj/item/organ/heart/cursed)) + qdel(current_heart) + var/obj/item/organ/heart/vampheart/vampiric_heart = new + vampiric_heart.Insert(owner.current) + vampiric_heart.Stop() + var/obj/item/organ/eyes/current_eyes = bloodsuckeruser.getorganslot(ORGAN_SLOT_EYES) + if(current_eyes) + current_eyes.flash_protect = max(initial(current_eyes.flash_protect) - 1, - 1) + current_eyes.sight_flags = SEE_MOBS + current_eyes.see_in_dark = 8 + current_eyes.lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_VISIBLE + bloodsuckeruser.update_sight() + + // Step 3 + if(bloodsuckeruser.stat == DEAD) + bloodsuckeruser.revive(full_heal = FALSE, admin_revive = FALSE) + for(var/i in bloodsuckeruser.all_wounds) + var/datum/wound/iter_wound = i + iter_wound.remove_wound() + // From [powers/panacea.dm] + var/list/bad_organs = list( + bloodsuckeruser.getorgan(/obj/item/organ/body_egg), + bloodsuckeruser.getorgan(/obj/item/organ/zombie_infection)) + for(var/tumors in bad_organs) + var/obj/item/organ/yucky_organs = tumors + if(!istype(yucky_organs)) + continue + yucky_organs.Remove(bloodsuckeruser) + yucky_organs.forceMove(get_turf(bloodsuckeruser)) + + // Part of Malkavian? Give them their traumas back. + if(my_clan == CLAN_MALKAVIAN) + bloodsuckeruser.gain_trauma(/datum/brain_trauma/mild/hallucinations, TRAUMA_RESILIENCE_ABSOLUTE) + bloodsuckeruser.gain_trauma(/datum/brain_trauma/special/bluespace_prophet, TRAUMA_RESILIENCE_ABSOLUTE) + // Good to go! + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// DEATH + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/// FINAL DEATH +/datum/antagonist/bloodsucker/proc/HandleDeath() + // Not "Alive"? + if(!owner.current || !iscarbon(owner.current) || isbrain(owner.current) || !get_turf(owner.current)) + FinalDeath() + return + // Fire Damage? (above double health) + if(owner.current.getFireLoss() >= owner.current.maxHealth * 2.5) + FinalDeath() + return + // Staked while "Temp Death" or Asleep + if(owner.current.StakeCanKillMe() && owner.current.AmStaked()) + FinalDeath() + return + // Not organic/living? (Zombie/Skeleton/Plasmaman) + if(!(owner.current.mob_biotypes & MOB_ORGANIC)) + FinalDeath() + return + // Temporary Death? Convert to Torpor. + if(owner.current.stat == DEAD) + var/mob/living/carbon/human/dead_bloodsucker = owner.current + if(!HAS_TRAIT(dead_bloodsucker, TRAIT_NODEATH)) + to_chat(dead_bloodsucker, span_danger("Your immortal body will not yet relinquish your soul to the abyss. You enter Torpor.")) + Check_Begin_Torpor(TRUE) + +/datum/antagonist/bloodsucker/proc/HandleStarving() // I am thirsty for blood! + // Nutrition - The amount of blood is how full we are. + owner.current.set_nutrition(min(owner.current.blood_volume, NUTRITION_LEVEL_FED)) + + // BLOOD_VOLUME_GOOD: [336] - Pale +// handled in bloodsucker_integration.dm + + // BLOOD_VOLUME_EXIT: [250] - Exit Frenzy (If in one) This is high because we want enough to kill the poor soul they feed off of. + if(owner.current.blood_volume >= FRENZY_THRESHOLD_EXIT && frenzied && my_clan != CLAN_BRUJAH) + owner.current.remove_status_effect(STATUS_EFFECT_FRENZY) + // BLOOD_VOLUME_BAD: [224] - Jitter + if(owner.current.blood_volume < BLOOD_VOLUME_BAD(owner.current) && prob(0.5) && !HAS_TRAIT(owner.current, TRAIT_NODEATH) && !HAS_TRAIT(owner.current, TRAIT_MASQUERADE)) + owner.current.Jitter(3) + // BLOOD_VOLUME_SURVIVE: [122] - Blur Vision + if(owner.current.blood_volume < BLOOD_VOLUME_SURVIVE(owner.current)) + owner.current.blur_eyes(8 - 8 * (owner.current / BLOOD_VOLUME_BAD(owner.current))) + + // The more blood, the better the Regeneration, get too low blood, and you enter Frenzy. + if(owner.current.blood_volume < (FRENZY_THRESHOLD_ENTER + (humanity_lost * 10)) && !frenzied) + enter_frenzy() + else if(owner.current.blood_volume < BLOOD_VOLUME_BAD(owner.current)) + additional_regen = 0.1 + else if(owner.current.blood_volume < BLOOD_VOLUME_OKAY(owner.current)) + additional_regen = 0.2 + else if(owner.current.blood_volume < BLOOD_VOLUME_NORMAL(owner.current)) + additional_regen = 0.3 + else if(owner.current.blood_volume < BS_BLOOD_VOLUME_MAX_REGEN) + additional_regen = 0.4 + else + additional_regen = 0.5 + +/datum/antagonist/bloodsucker/proc/enter_frenzy() + if(my_clan == CLAN_BRUJAH) + for(var/datum/action/bloodsucker/power in powers) + if(!(istype(power, /datum/action/bloodsucker/brujah))) + continue + if(power.active) + break + power.ActivatePower() + else + owner.current.apply_status_effect(STATUS_EFFECT_FRENZY) + +/** + * # Torpor + * + * Torpor is what deals with the Bloodsucker falling asleep, their healing, the effects, ect. + * This is basically what Sol is meant to do to them, but they can also trigger it manually if they wish to heal, as Burn is only healed through Torpor. + * You cannot manually exit Torpor, it is instead entered/exited by: + * + * Torpor is triggered by: + * - Being in a Coffin while Sol is on, dealt with by /HandleTorpor() + * - Entering a Coffin with more than 10 combined Brute/Burn damage, dealt with by /closet/crate/coffin/close() [bloodsucker_coffin.dm] + * - Death, dealt with by /HandleDeath() + * Torpor is ended by: + * - Having less than 10 Brute damage while OUTSIDE of your Coffin while it isnt Sol, dealt with by /HandleTorpor() + * - Having less than 10 Brute & Burn Combined while INSIDE of your Coffin while it isnt Sol, dealt with by /HandleTorpor() + * - Sol being over, dealt with by /sunlight/process() [bloodsucker_daylight.dm] +*/ + +/datum/antagonist/bloodsucker/proc/HandleTorpor() + if(!owner.current) + return + if(istype(owner.current.loc, /obj/structure/closet/crate/coffin)) + if(!HAS_TRAIT(owner.current, TRAIT_NODEATH)) + /// Staked? Dont heal + if(owner.current.AmStaked()) + to_chat(owner.current, span_userdanger("You are staked! Remove the offending weapon from your heart before sleeping.")) + return + /// Otherwise, check if it's Sol, to enter Torpor. + if(clan.bloodsucker_sunlight.amDay) + Check_Begin_Torpor(TRUE) + if(HAS_TRAIT(owner.current, TRAIT_NODEATH)) // Check so I don't go insane. + Check_End_Torpor() + +/datum/antagonist/bloodsucker/proc/Check_Begin_Torpor(SkipChecks = FALSE) + /// Are we entering Torpor via Sol/Death? Then entering it isnt optional! + if(SkipChecks) + Torpor_Begin() + return + var/mob/living/carbon/user = owner.current + var/total_brute = user.getBruteLoss() + var/total_burn = user.getFireLoss() + var/total_damage = total_brute + total_burn + /// Checks - Not daylight & Has more than 10 Brute/Burn & not already in Torpor + if(!clan.bloodsucker_sunlight.amDay && total_damage >= 10 && !HAS_TRAIT(owner.current, TRAIT_NODEATH)) + Torpor_Begin() + +/datum/antagonist/bloodsucker/proc/Check_End_Torpor() + var/mob/living/carbon/user = owner.current + var/total_brute = user.getBruteLoss() + var/total_burn = user.getFireLoss() + var/total_damage = total_brute + total_burn + // You are in a Coffin, so instead we'll check TOTAL damage, here. + if(istype(user.loc, /obj/structure/closet/crate/coffin)) + if(!clan.bloodsucker_sunlight.amDay && total_damage <= 10) + Torpor_End() + // You're not in a Coffin? We won't check for low Burn damage + else if(!clan.bloodsucker_sunlight.amDay && total_brute <= 10) + // You're under 10 brute, but over 200 Burn damage? Don't exit Torpor, to prevent spam revival/death. Only way out is healing that Burn. + if(total_burn >= 199) + return + Torpor_End() + +/datum/antagonist/bloodsucker/proc/Torpor_Begin() + to_chat(owner.current, span_notice("You enter the horrible slumber of deathless Torpor. You will heal until you are renewed.")) + /// Force them to go to sleep + REMOVE_TRAIT(owner.current, TRAIT_SLEEPIMMUNE, BLOODSUCKER_TRAIT) + /// Without this, you'll just keep dying while you recover. + ADD_TRAIT(owner.current, TRAIT_NODEATH, BLOODSUCKER_TRAIT) + ADD_TRAIT(owner.current, TRAIT_FAKEDEATH, BLOODSUCKER_TRAIT) + ADD_TRAIT(owner.current, TRAIT_DEATHCOMA, BLOODSUCKER_TRAIT) + ADD_TRAIT(owner.current, TRAIT_RESISTLOWPRESSURE, BLOODSUCKER_TRAIT) + owner.current.Jitter(0) + /// Disable ALL Powers + DisableAllPowers() + +/datum/antagonist/bloodsucker/proc/Torpor_End() + owner.current.grab_ghost() + to_chat(owner.current, span_warning("You have recovered from Torpor.")) + REMOVE_TRAIT(owner.current, TRAIT_RESISTLOWPRESSURE, BLOODSUCKER_TRAIT) + REMOVE_TRAIT(owner.current, TRAIT_DEATHCOMA, BLOODSUCKER_TRAIT) + REMOVE_TRAIT(owner.current, TRAIT_FAKEDEATH, BLOODSUCKER_TRAIT) + REMOVE_TRAIT(owner.current, TRAIT_NODEATH, BLOODSUCKER_TRAIT) + ADD_TRAIT(owner.current, TRAIT_SLEEPIMMUNE, BLOODSUCKER_TRAIT) + HealVampireOrgans() + +/// Gibs the Bloodsucker, roundremoving them. +/datum/antagonist/bloodsucker/proc/FinalDeath() + FreeAllVassals() + var/dust_timer + // If we have no body, end here. + if(!owner.current || dust_timer) + return + + DisableAllPowers() + if(!iscarbon(owner.current)) + owner.current.gib(TRUE, FALSE, FALSE) + return + // Drop anything in us and play a tune + var/mob/living/carbon/user = owner.current + owner.current.drop_all_held_items() + owner.current.unequip_everything() + user.remove_all_embedded_objects() + playsound(owner.current, 'sound/effects/tendril_destroyed.ogg', 40, TRUE) + // Elders get dusted, Fledglings get gibbed + if(bloodsucker_level >= 4) + owner.current.visible_message( + span_warning("[owner.current]'s skin crackles and dries, their skin and bones withering to dust. A hollow cry whips from what is now a sandy pile of remains."), + span_userdanger("Your soul escapes your withering body as the abyss welcomes you to your Final Death."), + span_hear("You hear a dry, crackling sound.")) + dust_timer = addtimer(CALLBACK(owner.current, /mob/living.proc/dust), 5 SECONDS, TIMER_UNIQUE|TIMER_STOPPABLE) + return + owner.current.visible_message( + span_warning("[owner.current]'s skin bursts forth in a spray of gore and detritus. A horrible cry echoes from what is now a wet pile of decaying meat."), + span_userdanger("Your soul escapes your withering body as the abyss welcomes you to your Final Death."), + span_hear("You hear a wet, bursting sound.")) + owner.current.gib(TRUE, FALSE, FALSE) + + +// Bloodsuckers moodlets // +/datum/mood_event/drankblood + description = "I have fed greedly from that which nourishes me.\n" + mood_change = 10 + timeout = 8 MINUTES + +/datum/mood_event/drankblood_bad + description = "I drank the blood of a lesser creature. Disgusting.\n" + mood_change = -4 + timeout = 3 MINUTES + +/datum/mood_event/drankblood_dead + description = "I drank dead blood. I am better than this.\n" + mood_change = -7 + timeout = 8 MINUTES + +/datum/mood_event/drankblood_synth + description = "I drank synthetic blood. What is wrong with me?\n" + mood_change = -7 + timeout = 8 MINUTES + +/datum/mood_event/drankkilled + description = "I drank from my victim until they died. I feel... less human.\n" + mood_change = -15 + timeout = 10 MINUTES + +/datum/mood_event/madevamp + description = "A soul has been cursed to undeath by my own hand.\n" + mood_change = 15 + timeout = 20 MINUTES + +/datum/mood_event/coffinsleep + description = "I slept in a coffin during the day. I feel whole again.\n" + mood_change = 10 + timeout = 6 MINUTES + +/datum/mood_event/daylight_1 + description = "I slept poorly in a makeshift coffin during the day.\n" + mood_change = -3 + timeout = 6 MINUTES + +/datum/mood_event/daylight_2 + description = "I have been scorched by the unforgiving rays of the sun.\n" + mood_change = -6 + timeout = 6 MINUTES + +///Candelabrum's mood event to non Bloodsucker/Vassals +/datum/mood_event/vampcandle + description = "Something is making your mind feel... loose.\n" + mood_change = -15 + timeout = 5 MINUTES + diff --git a/code/modules/antagonists/bloodsuckers/structures/bloodsucker_recipes.dm b/code/modules/antagonists/bloodsuckers/structures/bloodsucker_recipes.dm new file mode 100644 index 000000000000..3abcd08c1ee9 --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/structures/bloodsucker_recipes.dm @@ -0,0 +1,117 @@ +/// From recipes.dm + +/datum/crafting_recipe/blackcoffin + name = "Black Coffin" + result = /obj/structure/closet/crate/coffin/blackcoffin + tools = list(TOOL_WELDER, TOOL_SCREWDRIVER) + reqs = list( + /obj/item/stack/sheet/cloth = 1, + /obj/item/stack/sheet/mineral/wood = 5, + /obj/item/stack/sheet/metal = 1, + ) + time = 15 SECONDS + category = CAT_PRIMAL + +/datum/crafting_recipe/securecoffin + name = "Secure Coffin" + result = /obj/structure/closet/crate/coffin/securecoffin + tools = list(TOOL_WELDER, TOOL_SCREWDRIVER) + reqs = list( + /obj/item/stack/rods = 1, + /obj/item/stack/sheet/plasteel = 5, + /obj/item/stack/sheet/metal = 5, + ) + time = 15 SECONDS + category = CAT_PRIMAL + +/datum/crafting_recipe/meatcoffin + name = "Meat Coffin" + result = /obj/structure/closet/crate/coffin/meatcoffin + tools = list(TOOL_WELDER, TOOL_WRENCH) + reqs = list( + /obj/item/reagent_containers/food/snacks/meat/slab = 5, + /obj/item/restraints/handcuffs/cable = 1, + ) + time = 15 SECONDS + category = CAT_PRIMAL + always_availible = FALSE //The sacred coffin! + +/datum/crafting_recipe/metalcoffin + name = "Metal Coffin" + result = /obj/structure/closet/crate/coffin/metalcoffin + reqs = list( + /obj/item/stack/sheet/metal = 6, + /obj/item/stack/rods = 2, + ) + time = 10 SECONDS + category = CAT_PRIMAL + +/datum/crafting_recipe/vassalrack + name = "Persuasion Rack" + result = /obj/structure/bloodsucker/vassalrack + tools = list(TOOL_WELDER, TOOL_WRENCH) + reqs = list( + /obj/item/stack/sheet/mineral/wood = 3, + /obj/item/stack/sheet/metal = 2, + /obj/item/restraints/handcuffs/cable = 2, + ) + time = 15 SECONDS + category = CAT_PRIMAL + always_availible = FALSE + +/datum/crafting_recipe/candelabrum + name = "Candelabrum" + result = /obj/structure/bloodsucker/candelabrum + tools = list(TOOL_WELDER, TOOL_WRENCH) + reqs = list( + /obj/item/stack/sheet/metal = 3, + /obj/item/stack/rods = 1, + /obj/item/candle = 1, + ) + time = 10 SECONDS + category = CAT_PRIMAL + always_availible = FALSE + +/datum/crafting_recipe/bloodthrone + name = "Blood Throne" + result = /obj/structure/bloodsucker/bloodthrone + tools = list(TOOL_WRENCH) + reqs = list( + /obj/item/stack/sheet/cloth = 3, + /obj/item/stack/sheet/metal = 5, + /obj/item/stack/sheet/mineral/wood = 1, + ) + time = 5 SECONDS + category = CAT_PRIMAL + always_availible = 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_WEAPONRY + subcategory = CAT_WEAPON + +/datum/crafting_recipe/hardened_stake + name = "Hardened Stake" + result = /obj/item/stake/hardened + tools = list(TOOL_WELDER) + reqs = list(/obj/item/stack/rods = 1) + time = 6 SECONDS + category = CAT_WEAPONRY + subcategory = CAT_WEAPON + always_availible = FALSE + +/datum/crafting_recipe/silver_stake + name = "Silver Stake" + result = /obj/item/stake/hardened/silver + tools = list(TOOL_WELDER) + reqs = list( + /obj/item/stack/sheet/mineral/silver = 1, + /obj/item/stake/hardened = 1, + ) + time = 8 SECONDS + category = CAT_WEAPONRY + subcategory = CAT_WEAPON + always_availible = FALSE diff --git a/code/modules/antagonists/bloodsuckers/vassal.dm b/code/modules/antagonists/bloodsuckers/vassal.dm new file mode 100644 index 000000000000..fe0f95940f49 --- /dev/null +++ b/code/modules/antagonists/bloodsuckers/vassal.dm @@ -0,0 +1,205 @@ +#define VASSAL_SCAN_MIN_DISTANCE 5 +#define VASSAL_SCAN_MAX_DISTANCE 500 +/// 2s update time. +#define VASSAL_SCAN_PING_TIME 20 + +/datum/antagonist/vassal + var/hud_type = "bloodsucker" + name = "\improper Vassal" + roundend_category = "vassals" + antagpanel_category = "Bloodsucker" + job_rank = ROLE_BLOODSUCKER + hud_type = "vassal" + show_in_roundend = FALSE + + /// The Master Bloodsucker's antag datum. + var/datum/antagonist/bloodsucker/master + /// List of all Purchased Powers, like Bloodsuckers. + var/list/datum/action/powers = list() + /// The favorite vassal gets unique features, and Ventrue can upgrade theirs + var/favorite_vassal = FALSE + /// Bloodsucker levels, but for Vassals. + var/vassal_level + +/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 + set_antag_hud(current_mob) + set_antag_hud(current_mob, /datum/antagonist/bloodsucker) + +/datum/antagonist/vassal/on_gain() + /// Enslave them to their Master + if(master) + var/datum/antagonist/bloodsucker/bloodsuckerdatum = master.owner.has_antag_datum(/datum/antagonist/bloodsucker) + if(bloodsuckerdatum) + bloodsuckerdatum.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 Vassal Pinpointer + owner.current.apply_status_effect(/datum/status_effect/agent_pinpointer/vassal_edition) + /// Give Recuperate Power + BuyPower(new /datum/action/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) + . = ..() + +/datum/antagonist/vassal/on_removal() + /// Free them from their Master + if(master && master.owner) + master.vassals -= src + owner.enslaved_to = null + /// Remove Pinpointer + owner.current.remove_status_effect(/datum/status_effect/agent_pinpointer/vassal_edition) + /// Remove ALL Traits, as long as its from BLOODSUCKER_TRAIT's source. + for(var/all_status_traits in owner.current.status_traits) + REMOVE_TRAIT(owner.current, all_status_traits, BLOODSUCKER_TRAIT) + /// Remove Recuperate Power + while(powers.len) + var/datum/action/bloodsucker/power = pick(powers) + powers -= power + power.Remove(owner.current) + /// Remove Language & Hud + owner.current.remove_language(/datum/language/vampiric) + return ..() + +/datum/antagonist/vassal/proc/add_objective(datum/objective/added_objective) + objectives += added_objective + +/datum/antagonist/vassal/proc/remove_objectives(datum/objective/removed_objective) + objectives -= removed_objective + +/datum/antagonist/vassal/greet() + . = ..() + 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.")) + 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() + 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."), + ) + to_chat(owner, 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!")) + +/// Called when we are made into the Favorite Vassal +/datum/antagonist/vassal/proc/make_favorite(mob/living/master) + // Default stuff for all + favorite_vassal = TRUE + hud_type = "vassal6" + set_antag_hud(owner.current, /datum/antagonist/bloodsucker) + to_chat(master, span_danger("You have turned [owner.current] into your Favorite Vassal! They will no longer be deconverted upon Mindshielding!")) + to_chat(owner, span_notice("As Blood drips over your body, you feel closer to your Master... You are now the Favorite Vassal!")) + + // Now let's give them their assigned bonuses. + var/datum/antagonist/bloodsucker/bloodsuckerdatum = master.mind.has_antag_datum(/datum/antagonist/bloodsucker) + if(bloodsuckerdatum.my_clan == CLAN_BRUJAH) + BuyPower(new /datum/action/bloodsucker/targeted/brawn) + if(bloodsuckerdatum.my_clan == CLAN_NOSFERATU) + ADD_TRAIT(owner.current, VENTCRAWLER_NUDE, BLOODSUCKER_TRAIT) + ADD_TRAIT(owner.current, TRAIT_DISFIGURED, BLOODSUCKER_TRAIT) + to_chat(owner, span_notice("Additionally, you can now ventcrawl while naked, and are permanently disfigured.")) + if(bloodsuckerdatum.my_clan == CLAN_TREMERE) + var/obj/effect/proc_holder/spell/targeted/shapeshift/bat/batform = new + owner.current.AddSpell(batform) + if(bloodsuckerdatum.my_clan == CLAN_VENTRUE) + to_chat(master, span_announce("* Bloodsucker Tip: You can now upgrade your Favorite Vassal by buckling them onto a Candelabrum!")) + BuyPower(new /datum/action/bloodsucker/distress) + if(bloodsuckerdatum.my_clan == CLAN_MALKAVIAN) + var/mob/living/carbon/carbonowner = owner.current + carbonowner.gain_trauma(/datum/brain_trauma/mild/hallucinations, TRAUMA_RESILIENCE_ABSOLUTE) + carbonowner.gain_trauma(/datum/brain_trauma/special/bluespace_prophet, TRAUMA_RESILIENCE_ABSOLUTE) + to_chat(owner, span_notice("Additionally, you now suffer the same fate as your Master.")) + +/// If we weren't created by a bloodsucker, then we cannot be a vassal (assigned from antag panel) +/datum/antagonist/vassal/can_be_owned(datum/mind/new_owner) + if(!master) + return FALSE + return ..() + +/// Used for Admin removing Vassals. +/datum/mind/proc/remove_vassal() + var/datum/antagonist/vassal/selected_vassal = has_antag_datum(/datum/antagonist/vassal) + if(selected_vassal) + remove_antag_datum(/datum/antagonist/vassal) + +/// When a Bloodsucker gets FinalDeath, all Vassals are freed - This is a Bloodsucker proc, not a Vassal one. +/datum/antagonist/bloodsucker/proc/FreeAllVassals() + for(var/datum/antagonist/vassal/all_vassals in vassals) + // Skip over any Bloodsucker Vassals, they're too far gone to have all their stuff taken away from them + if(all_vassals.owner.has_antag_datum(/datum/antagonist/bloodsucker)) + continue + remove_vassal(all_vassals.owner) + +/// Called by FreeAllVassals() +/datum/antagonist/bloodsucker/proc/remove_vassal(datum/mind/vassal) + vassal.remove_antag_datum(/datum/antagonist/vassal) + +/// Used when your Master teaches you a new Power. +/datum/antagonist/vassal/proc/BuyPower(datum/action/bloodsucker/power) + powers += power + power.Grant(owner.current) + +/datum/antagonist/vassal/proc/LevelUpPowers() + for(var/datum/action/bloodsucker/power in powers) + power.level_current++ + +/** + * # 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 = -1 + 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 ..() + +/** + * # BATFORM + * + * TG removed this, so we're re-adding it + */ +/obj/effect/proc_holder/spell/targeted/shapeshift/bat + name = "Bat Form" + desc = "Take on the shape of a space bat." + invocation = "Squeak!" + convert_damage = FALSE + shapeshift_type = /mob/living/simple_animal/hostile/retaliate/bat diff --git a/code/modules/antagonists/monsterhunter/hunterfu.dm b/code/modules/antagonists/monsterhunter/hunterfu.dm new file mode 100644 index 000000000000..5d48e248e6e8 --- /dev/null +++ b/code/modules/antagonists/monsterhunter/hunterfu.dm @@ -0,0 +1,223 @@ +#define BODYSLAM_COMBO "GH" +#define STAKESTAB_COMBO "HH" +#define NECKSNAP_COMBO "GD" +#define HOLYKICK_COMBO "DG" + +// From CQC.dm +/datum/martial_art/hunterfu + name = "Hunter-Fu" + id = MARTIALART_HUNTERFU + help_verb = /mob/living/carbon/human/proc/hunterfu_help + block_chance = 60 + allow_temp_override = TRUE + var/old_grab_state = null + +/datum/martial_art/hunterfu/proc/check_streak(mob/living/user, mob/living/target) + if(findtext(streak, BODYSLAM_COMBO)) + streak = "" + body_slam(user, target) + return TRUE + if(findtext(streak, STAKESTAB_COMBO)) + streak = "" + stake_stab(user, target) + return TRUE + if(findtext(streak, NECKSNAP_COMBO)) + streak = "" + neck_snap(user, target) + return TRUE + if(findtext(streak, HOLYKICK_COMBO)) + streak = "" + holy_kick(user, target) + return TRUE + return FALSE + +/datum/martial_art/hunterfu/proc/body_slam(mob/living/user, mob/living/target) + if(target.mobility_flags & MOBILITY_STAND) + target.visible_message( + span_danger("[user] slams both them and [target] into the ground!"), + span_userdanger("You're slammed into the ground by [user]!"), + span_hear("You hear a sickening sound of flesh hitting flesh!"), + ) + to_chat(user, span_danger("You slam [target] into the ground!")) + playsound(get_turf(user), 'sound/weapons/slam.ogg', 50, TRUE, -1) + log_combat(user, target, "bodyslammed (Hunter-Fu)") + if(!target.mind) + target.Paralyze(40) + user.Paralyze(25) + return TRUE + if(target.mind.has_antag_datum(/datum/antagonist/changeling)) + to_chat(target, span_cultlarge("Our DNA shakes as we are body slammed!")) + target.apply_damage(15, BRUTE) + target.Paralyze(60) + user.Paralyze(25) + return TRUE + else + target.Paralyze(40) + user.Paralyze(25) + else + harm_act(user, target) + return TRUE + +/datum/martial_art/hunterfu/proc/stake_stab(mob/living/user, mob/living/target) + target.visible_message( + span_danger("[user] stabs [target] in the heart!"), + span_userdanger("You're staked in the heart by [user]!"), + span_hear("You hear a sickening sound of flesh hitting flesh!"), + ) + to_chat(user, span_danger("You stab [target] viciously!")) + playsound(get_turf(user), 'sound/weapons/bladeslice.ogg', 50, TRUE, -1) + log_combat(user, target, "stakestabbed (Hunter-Fu)") + if(!target.mind) + target.apply_damage(15, BRUTE, BODY_ZONE_CHEST) + return TRUE + if(target.mind.has_antag_datum(/datum/antagonist/changeling)) + to_chat(target, span_danger("Their arm tears through our monstrous form!")) + target.apply_damage(25, BRUTE, BODY_ZONE_CHEST) + return TRUE + if(target.mind.has_antag_datum(/datum/antagonist/bloodsucker)) + to_chat(target, span_cultlarge("Their arm stakes straight into our undead flesh!")) + target.apply_damage(20, BURN) + target.apply_damage(10, BRUTE, BODY_ZONE_CHEST) + return TRUE + else + target.apply_damage(15, BRUTE, BODY_ZONE_CHEST) + return TRUE + +/datum/martial_art/hunterfu/proc/neck_snap(mob/living/user, mob/living/target) + if(!target.stat) + target.visible_message( + span_danger("[user] snapped [target]'s neck!"), + span_userdanger("Your neck is snapped by [user]!"), + span_hear("You hear a snap!"), + ) + to_chat(user, span_danger("You snap [target]'s neck!")) + playsound(get_turf(user), 'sound/effects/snap.ogg', 50, TRUE, -1) + log_combat(user, target, "neck snapped (Hunter-Fu)") + if(!target.mind) + target.SetSleeping(45) + playsound(get_turf(user), 'sound/effects/snap.ogg', 50, TRUE, -1) + log_combat(user, target, "neck snapped (Hunter-Fu)") + return TRUE + if(target.mind.has_antag_datum(/datum/antagonist/changeling)) + to_chat(target, span_warning("Our monstrous form protects us from being put to sleep!")) + return TRUE + if(target.mind.has_antag_datum(/datum/antagonist/heretic)) + to_chat(target, span_cultlarge("The power of the Codex Cicatrix flares as we are swiftly put to sleep!")) + target.apply_damage(15, BRUTE, BODY_ZONE_HEAD) + target.SetSleeping(50) + return TRUE + if(target.mind.has_antag_datum(/datum/antagonist/bloodsucker)) + to_chat(target, span_warning("Our undead form protects us from being put to sleep!")) + return TRUE + else + target.SetSleeping(45) + return TRUE + +/datum/martial_art/hunterfu/proc/holy_kick(mob/living/user, mob/living/target) + target.visible_message( + span_warning("[user] kicks [target], splashing holy water in every direction!"), + span_userdanger("You're kicked by [user], with holy water dripping down on you!"), + span_hear("You hear a sickening sound of flesh hitting flesh!"), + ) + to_chat(user, span_danger("You holy kick [target]!")) + playsound(get_turf(user), 'sound/weapons/slash.ogg', 50, TRUE, -1) + log_combat(user, target, "holy kicked (Hunter-Fu)") + if(!target.mind) + target.adjustStaminaLoss(60) + target.Paralyze(20) + return TRUE + if(target.mind.has_antag_datum(/datum/antagonist/heretic)) + to_chat(target, span_cultlarge("The holy water burns our flesh!")) + target.apply_damage(25, BURN) + target.adjustStaminaLoss(60) + target.Paralyze(20) + return TRUE + if(target.mind.has_antag_datum(/datum/antagonist/bloodsucker)) + var/datum/antagonist/bloodsucker/bloodsuckerdatum = IS_BLOODSUCKER(target) + if(bloodsuckerdatum.my_clan == CLAN_TREMERE) + to_chat(target, span_cultlarge("The holy water burns our flesh!")) + target.apply_damage(25, BURN) + target.adjustStaminaLoss(60) + target.Paralyze(20) + return TRUE + else + to_chat(target, span_warning("This just seems like regular water...")) + return TRUE + if(target.mind.has_antag_datum(/datum/antagonist/cult)) + for(var/datum/action/innate/cult/blood_magic/BD in target.actions) + to_chat(target, span_cultlarge("Our blood rites falter as the holy water drips onto our body!")) + for(var/datum/action/innate/cult/blood_spell/BS in BD.spells) + qdel(BS) + target.adjustStaminaLoss(60) + target.Paralyze(20) + return TRUE + if(target.mind.has_antag_datum(/datum/antagonist/wizard) || (/datum/antagonist/wizard/apprentice)) + to_chat(target, span_danger("The holy water seems to be muting us somehow!")) + var/mob/living/carbon/human/human_target = target // I guess monkey wizards aren't getting affected. + if(human_target.silent <= 10) + human_target.silent = clamp(human_target.silent + 10, 0, 10) + target.adjustStaminaLoss(60) + target.Paralyze(20) + return TRUE + else + target.adjustStaminaLoss(60) + target.Paralyze(20) + return TRUE + +/// Intents +/datum/martial_art/hunterfu/disarm_act(mob/living/user, mob/living/target) + add_to_streak("D", target) + if(check_streak(user, target)) + return TRUE + log_combat(user, target, "disarmed (Hunter-Fu)") + return ..() + +/datum/martial_art/hunterfu/harm_act(mob/living/user, mob/living/target) + add_to_streak("H", target) + if(check_streak(user, target)) + return TRUE + var/obj/item/bodypart/affecting = target.get_bodypart(ran_zone(user.zone_selected)) + user.do_attack_animation(target, ATTACK_EFFECT_PUNCH) + var/atk_verb = pick("kick", "chop", "hit", "slam") + target.visible_message( + span_danger("[user] [atk_verb]s [target]!"), + span_userdanger("[user] [atk_verb]s you!"), + ) + to_chat(user, span_danger("You [atk_verb] [target]!")) + target.apply_damage(rand(10,15), BRUTE, affecting, wound_bonus = CANT_WOUND) + playsound(get_turf(target), 'sound/weapons/punch1.ogg', 25, TRUE, -1) + log_combat(user, target, "harmed (Hunter-Fu)") + return TRUE + +/datum/martial_art/hunterfu/grab_act(mob/living/user, mob/living/target) + if(user!=target && can_use(user)) + add_to_streak("G", target) + if(check_streak(user, target)) // If a combo is made no grab upgrade is done + return TRUE + old_grab_state = user.grab_state + target.grabbedby(user, 1) + if(old_grab_state == GRAB_PASSIVE) + target.drop_all_held_items() + user.grab_state = GRAB_AGGRESSIVE // Instant agressive grab + log_combat(user, target, "grabbed (Hunter-Fu)") + target.visible_message( + span_warning("[user] violently grabs [target]!"), + span_userdanger("You're grabbed violently by [user]!"), + span_hear("You hear sounds of aggressive fondling!"), + ) + to_chat(user, span_danger("You violently grab [target]!")) + return TRUE + ..() + +/mob/living/carbon/human/proc/hunterfu_help() + set name = "Remember The Basics" + set desc = "You try to remember some of the basics of Hunter-Fu." + set category = "Hunter-Fu" + to_chat(usr, span_notice("You try to remember some of the basics of Hunter-Fu.")) + + to_chat(usr, span_notice("Body Slam: Grab Harm. Slam opponent into the ground, knocking you both down.")) + to_chat(usr, span_notice("Stake Stab: Harm Harm. Stabs opponent with your bare fist, as strong as a Stake.")) + to_chat(usr, span_notice("Neck Snap: Grab Disarm. Snaps an opponents neck, knocking them out.")) + to_chat(usr, span_notice("Holy Kick: Disarm Grab. Splashes the user with Holy Water, removing Cult Spells, while dealing stamina damage.")) + + to_chat(usr, span_notice("In addition, by having your throw mode on, you take a defensive position, allowing you to block and sometimes even counter attacks done to you.")) diff --git a/code/modules/antagonists/monsterhunter/monsterhunter.dm b/code/modules/antagonists/monsterhunter/monsterhunter.dm new file mode 100644 index 000000000000..c123f0587792 --- /dev/null +++ b/code/modules/antagonists/monsterhunter/monsterhunter.dm @@ -0,0 +1,162 @@ +#define HUNTER_SCAN_MIN_DISTANCE 8 +#define HUNTER_SCAN_MAX_DISTANCE 15 +/// 5s update time +#define HUNTER_SCAN_PING_TIME 20 +/// Used for the pinpointer +#define STATUS_EFFECT_HUNTERPINPOINTER /datum/status_effect/agent_pinpointer/hunter_edition + +/datum/antagonist/monsterhunter + var/hud_type = "bloodsucker" + name = "\improper Monster Hunter" + roundend_category = "Monster Hunters" + antagpanel_category = "Monster Hunter" + job_rank = ROLE_MONSTERHUNTER + hud_type = "obssesed" + var/list/datum/action/powers = list() + var/datum/martial_art/hunterfu/my_kungfu = new + var/give_objectives = TRUE + var/datum/action/bloodsucker/trackvamp = new/datum/action/bloodsucker/trackvamp() + var/datum/action/bloodsucker/fortitude = new/datum/action/bloodsucker/fortitude/hunter() + +/datum/antagonist/monsterhunter/on_gain() + /// Buffs Monster Hunters + owner.unconvertable = TRUE + ADD_TRAIT(owner.current, TRAIT_NOSOFTCRIT, BLOODSUCKER_TRAIT) + ADD_TRAIT(owner.current, TRAIT_NOCRITDAMAGE, BLOODSUCKER_TRAIT) + /// Give Monster Hunter powers + trackvamp.Grant(owner.current) + fortitude.Grant(owner.current) + if(give_objectives) + /// Give Hunter Objective + var/datum/objective/bloodsucker/monsterhunter/monsterhunter_objective = new + monsterhunter_objective.owner = owner + objectives += monsterhunter_objective + /// Give Theft Objective + var/datum/objective/steal/steal_objective = new + steal_objective.owner = owner + steal_objective.find_target() + objectives += steal_objective + + /// Give Martial Arts + my_kungfu.teach(owner.current, 0) + /// Teach Stake crafting + owner.teach_crafting_recipe(/datum/crafting_recipe/hardened_stake) + owner.teach_crafting_recipe(/datum/crafting_recipe/silver_stake) + . = ..() + +/datum/antagonist/monsterhunter/on_removal() + /// Remove buffs + owner.unconvertable = FALSE + /// Remove ALL Traits, as long as its from BLOODSUCKER_TRAIT's source. + for(var/all_status_traits in owner.current.status_traits) + REMOVE_TRAIT(owner.current, all_status_traits, BLOODSUCKER_TRAIT) + /// Remove Monster Hunter powers + trackvamp.Remove(owner.current) + fortitude.Remove(owner.current) + /// Remove Martial Arts + if(my_kungfu) + my_kungfu.remove(owner.current) + to_chat(owner.current, span_userdanger("Your hunt has ended: You enter retirement once again, and are no longer a Monster Hunter.")) + return ..() + +/datum/outfit/monsterhunter + name = "Monster Hunter (Preview Only)" + + l_hand = /obj/item/stake + r_hand = /obj/item/stake/hardened/silver + uniform = /obj/item/clothing/under/rank/medical/purple + head = /obj/item/clothing/head/soft/emt + suit = /obj/item/clothing/suit/toggle/labcoat/emt + gloves = /obj/item/clothing/gloves/color/latex/nitrile + +/// Mind version +/datum/mind/proc/make_monsterhunter() + var/datum/antagonist/monsterhunter/monsterhunterdatum = has_antag_datum(/datum/antagonist/monsterhunter) + if(!monsterhunterdatum) + monsterhunterdatum = add_antag_datum(/datum/antagonist/monsterhunter) + special_role = ROLE_MONSTERHUNTER + return monsterhunterdatum + +/datum/mind/proc/remove_monsterhunter() + var/datum/antagonist/monsterhunter/monsterhunterdatum = has_antag_datum(/datum/antagonist/monsterhunter) + if(monsterhunterdatum) + remove_antag_datum(/datum/antagonist/monsterhunter) + special_role = null + +/// Called when using admin tools to give antag status +/datum/antagonist/monsterhunter/admin_add(datum/mind/new_owner, mob/admin) + message_admins("[key_name_admin(admin)] made [key_name_admin(new_owner)] into [name].") + log_admin("[key_name(admin)] made [key_name(new_owner)] into [name].") + new_owner.add_antag_datum(src) + +/// Called when removing antagonist using admin tools +/datum/antagonist/monsterhunter/admin_remove(mob/user) + if(!user) + return + message_admins("[key_name_admin(user)] has removed [name] antagonist status from [key_name_admin(owner)].") + log_admin("[key_name(user)] has removed [name] antagonist status from [key_name(owner)].") + on_removal() + +/datum/antagonist/monsterhunter/proc/add_objective(datum/objective/added_objective) + objectives += added_objective + +/datum/antagonist/monsterhunter/proc/remove_objectives(datum/objective/removed_objective) + objectives -= removed_objective + +/datum/antagonist/monsterhunter/greet() + . = ..() + to_chat(owner.current, span_userdanger("After witnessing recent events on the station, we return to your old profession, we are a Monster Hunter!")) + to_chat(owner.current, span_announce("While we can kill anyone in our way to destroy the monsters lurking around, causing property damage is unacceptable.")) + to_chat(owner.current, span_announce("However, security WILL detain us if they discover our mission.")) + to_chat(owner.current, span_announce("In exchange for our services, it shouldn't matter if a few items are gone missing for our... personal collection.")) + owner.current.playsound_local(null, 'sound/effects/his_grace_ascend.ogg', 100, FALSE, pressure_affected = FALSE) + owner.announce_objectives() + +////////////////////////////////////////////////////////////////////////// +// Monster Hunter Pinpointer +////////////////////////////////////////////////////////////////////////// + +/// TAKEN FROM: /datum/action/changeling/pheromone_receptors // pheromone_receptors.dm for a version of tracking that Changelings have! +/datum/status_effect/agent_pinpointer/hunter_edition + alert_type = /atom/movable/screen/alert/status_effect/agent_pinpointer/hunter_edition + minimum_range = HUNTER_SCAN_MIN_DISTANCE + tick_interval = HUNTER_SCAN_PING_TIME + duration = 10 SECONDS + range_fuzz_factor = 5 //PINPOINTER_EXTRA_RANDOM_RANGE + +/atom/movable/screen/alert/status_effect/agent_pinpointer/hunter_edition + name = "Monster Tracking" + desc = "You always know where the hellspawn are." + +/datum/status_effect/agent_pinpointer/hunter_edition/scan_for_target() + var/turf/my_loc = get_turf(owner) + + var/list/mob/living/carbon/monsters = list() + for(var/mob/living/carbon/all_carbons in GLOB.alive_mob_list) + if(all_carbons != owner && all_carbons.mind) + var/datum/mind/carbon_minds = all_carbons.mind + if(IS_HERETIC(all_carbons) || IS_BLOODSUCKER(all_carbons) || iscultist(all_carbons) || is_servant_of_ratvar(all_carbons) || is_wizard(all_carbons)) + monsters += carbon_minds + if(carbon_minds.has_antag_datum(/datum/antagonist/changeling)) + monsters += carbon_minds + if(carbon_minds.has_antag_datum(/datum/antagonist/ashwalker)) + monsters += carbon_minds + if(carbon_minds.has_antag_datum(/datum/antagonist/wizard/apprentice)) + monsters += carbon_minds + if(istype(monsters)) + var/their_loc = get_turf(all_carbons) + var/distance = get_dist_euclidian(my_loc, their_loc) + if(distance < HUNTER_SCAN_MAX_DISTANCE) + monsters[all_carbons] = (HUNTER_SCAN_MAX_DISTANCE ** 2) - (distance ** 2) + + if(monsters.len) + /// Point at a 'random' monster, biasing heavily towards closer ones. + scan_target = pickweight(monsters) + to_chat(owner, span_warning("You detect signs of monsters to the [dir2text(get_dir(my_loc,get_turf(scan_target)))]!")) + else + scan_target = null + +/datum/status_effect/agent_pinpointer/hunter_edition/Destroy() + if(scan_target) + to_chat(owner, span_notice("You've lost the trail.")) + . = ..() \ No newline at end of file diff --git a/code/modules/antagonists/monsterhunter/monstertrack.dm b/code/modules/antagonists/monsterhunter/monstertrack.dm new file mode 100644 index 000000000000..68906a260aa6 --- /dev/null +++ b/code/modules/antagonists/monsterhunter/monstertrack.dm @@ -0,0 +1,72 @@ +/// From 'Cellular Emporium'... somehow? +/datum/action/bloodsucker/trackvamp + name = "Track Monster" + desc = "Take a moment to look for clues of any nearby monsters.
These creatures are slippery, and often look like the crew." + button_icon = 'icons/mob/actions/actions_bloodsucker.dmi' + icon_icon = 'icons/mob/actions/actions_bloodsucker.dmi' + background_icon_state = "vamp_power_off" + button_icon_state = "power_hunter" + power_flags = NONE + check_flags = BP_CANT_USE_WHILE_INCAPACITATED|BP_CANT_USE_WHILE_UNCONSCIOUS + purchase_flags = NONE + cooldown = 30 SECONDS + bloodcost = 0 + /// Removed, set to TRUE to re-add, either here to be a default function, or in-game through VV for neat Admin stuff -Willard + var/give_pinpointer = FALSE + +/datum/action/bloodsucker/trackvamp/ActivatePower() + . = ..() + /// Return text indicating direction + to_chat(owner, span_notice("You look around, scanning your environment and discerning signs of any filthy, wretched affronts to the natural order.")) + if(!do_after(owner, 6 SECONDS, src)) + return + if(give_pinpointer) + var/mob/living/user = owner + user.apply_status_effect(STATUS_EFFECT_HUNTERPINPOINTER) + display_proximity() + +/datum/action/bloodsucker/trackvamp/proc/display_proximity() + /// Pick target + var/turf/my_loc = get_turf(owner) + var/best_dist = 9999 + var/mob/living/best_vamp + + /// Track ALL living Monsters. + var/list/datum/mind/monsters = list() + for(var/mob/living/carbon/all_carbons in GLOB.alive_mob_list) + if(!all_carbons.mind) + continue + var/datum/mind/carbon_minds = all_carbons.mind + if(IS_HERETIC(all_carbons) || IS_BLOODSUCKER(all_carbons) || iscultist(all_carbons) || is_servant_of_ratvar(all_carbons) || is_wizard(all_carbons)) + monsters += carbon_minds + if(carbon_minds.has_antag_datum(/datum/antagonist/changeling)) + monsters += carbon_minds + if(carbon_minds.has_antag_datum(/datum/antagonist/ashwalker)) + monsters += carbon_minds + if(carbon_minds.has_antag_datum(/datum/antagonist/wizard/apprentice)) + monsters += carbon_minds + + for(var/datum/mind/monster_minds in monsters) + if(!monster_minds.current || monster_minds.current == owner) // || !get_turf(M.current) || !get_turf(owner)) + continue + for(var/antag_datums in monster_minds.antag_datums) + var/datum/antagonist/antag_datum = antag_datums + if(!istype(antag_datum)) + continue + var/their_loc = get_turf(monster_minds.current) + var/distance = get_dist_euclidian(my_loc, their_loc) + /// Found One: Closer than previous/max distance + if(distance < best_dist && distance <= HUNTER_SCAN_MAX_DISTANCE) + best_dist = distance + best_vamp = monster_minds.current + /// Stop searching through my antag datums and go to the next guy + break + + /// Found one! + if(best_vamp) + var/distString = best_dist <= HUNTER_SCAN_MAX_DISTANCE / 2 ? "somewhere closeby!" : "somewhere in the distance." + to_chat(owner, span_warning("You detect signs of monsters [distString]")) + + /// Will yield a "?" + else + to_chat(owner, span_notice("There are no monsters nearby.")) diff --git a/code/modules/events/monsterhunter.dm b/code/modules/events/monsterhunter.dm new file mode 100644 index 000000000000..ab0f8cd58fb9 --- /dev/null +++ b/code/modules/events/monsterhunter.dm @@ -0,0 +1,46 @@ +/* + * MONSTER HUNTERS: + * Their job is to hunt Monsters (obviously). + * I didnt know what better way to implement this, so they just cancel out if there's no monsters. + */ + +/// Spawns monster hunters. +/datum/round_event_control/monster_hunters + name = "Spawn Monster Hunter" + typepath = /datum/round_event/monster_hunters + max_occurrences = 1 + weight = 5 + min_players = 10 + earliest_start = 30 MINUTES + alert_observers = FALSE + +/datum/round_event/monster_hunters + fakeable = FALSE + var/cancel_me = FALSE + +/datum/round_event/monster_hunters/start() + for(var/mob/living/carbon/human/H in GLOB.player_list) + if(!IS_CULTIST(H) && !IS_HERETIC(H) && !IS_BLOODSUCKER(H) && !IS_WIZARD(H) && !H.mind.has_antag_datum(/datum/antagonist/changeling)) + message_admins("MONSTERHUNTER NOTICE: Monster Hunters couldnt verify any Monsters.") + cancel_me = TRUE + break + message_admins("MONSTERHUNTER NOTICE: A Monster Hunter is attempting to awaken.") + // because kill() doesn't work. + if(cancel_me) + return + for(var/mob/living/carbon/human/H in shuffle(GLOB.player_list)) + /// From obsessed + if(!H.client || !H.mind || !(ROLE_MONSTERHUNTER in H.client.prefs.be_special)) + continue + if(H.stat == DEAD) + continue + if(H.mind.assigned_role.departments_bitflags & (DEPARTMENT_BITFLAG_SECURITY|DEPARTMENT_BITFLAG_COMMAND)) + continue + /// Bobux no IS_CHANGELING + if(IS_HERETIC(H) || IS_CULTIST(H) || IS_BLOODSUCKER(H) || IS_VASSAL(H) || IS_WIZARD(H) || H.mind.has_antag_datum(/datum/antagonist/changeling)) + continue + if(!H.getorgan(/obj/item/organ/brain)) + continue + H.mind.add_antag_datum(/datum/antagonist/monsterhunter) + message_admins("MONSTERHUNTER NOTICE: [H] has awoken as a Monster Hunter.") + break diff --git a/code/modules/language/vampiric.dm b/code/modules/language/vampiric.dm new file mode 100644 index 000000000000..d7b250be6bc6 --- /dev/null +++ b/code/modules/language/vampiric.dm @@ -0,0 +1,20 @@ +/datum/language/vampiric + name = "Blah-Sucker" + desc = "The native language of the Bloodsucker elders, learned intuitively by Fledglings as they pass from death into immortality." + key = "l" + space_chance = 40 + default_priority = 90 + + flags = TONGUELESS_SPEECH | LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD + syllables = list( + "luk","cha","no","kra","pru","chi","busi","tam","pol","spu","och", + "umf","ora","stu","si","ri","li","ka","red","ani","lup","ala","pro", + "to","siz","nu","pra","ga","ump","ort","a","ya","yach","tu","lit", + "wa","mabo","mati","anta","tat","tana","prol", + "tsa","si","tra","te","ele","fa","inz", + "nza","est","sti","ra","pral","tsu","ago","esch","chi","kys","praz", + "froz","etz","tzil", + "t'","k'","t'","k'","th'","tz'" + ) + + icon_state = "bloodsucker" \ No newline at end of file diff --git a/code/modules/mining/lavaland/necropolis_chests.dm b/code/modules/mining/lavaland/necropolis_chests.dm index 32edd4861c63..28604e8024c8 100644 --- a/code/modules/mining/lavaland/necropolis_chests.dm +++ b/code/modules/mining/lavaland/necropolis_chests.dm @@ -219,6 +219,9 @@ if(hasholos.len) to_chat(user, span_warning("The pendant refuses to work with a guardian spirit...")) return + if(IS_BLOODSUCKER(user)) + to_chat(user, span_warning("The Memento notices your undead soul, and refuses to react..")) + return to_chat(user, span_warning("You feel your life being drained by the pendant...")) if(do_after(user, 4 SECONDS, target = user)) to_chat(user, span_notice("Your lifeforce is now linked to the pendant! You feel like removing it would kill you, and yet you instinctively know that until then, you won't die.")) diff --git a/code/modules/mob/living/blood.dm b/code/modules/mob/living/blood.dm index c5f61b9a7be1..2d666284fd3d 100644 --- a/code/modules/mob/living/blood.dm +++ b/code/modules/mob/living/blood.dm @@ -27,6 +27,8 @@ if(NOBLOOD in dna.species.species_traits || bleedsuppress || (HAS_TRAIT(src, TRAIT_FAKEDEATH))) return + if(HAS_TRAIT(src, TRAIT_NOPULSE)) // Fulpstation Bloodsuckers edit - Dont regenerate blood, damnmit! + return if(bodytemperature >= TCRYO && !(HAS_TRAIT(src, TRAIT_HUSK))) //cryosleep or husked people do not pump the blood. diff --git a/code/modules/mob/living/carbon/damage_procs.dm b/code/modules/mob/living/carbon/damage_procs.dm index 78bd990b538b..fa8776cf662b 100644 --- a/code/modules/mob/living/carbon/damage_procs.dm +++ b/code/modules/mob/living/carbon/damage_procs.dm @@ -81,10 +81,14 @@ /mob/living/carbon/adjustToxLoss(amount, updating_health = TRUE, forced = FALSE) if(!forced && HAS_TRAIT(src, TRAIT_TOXINLOVER)) //damage becomes healing and healing becomes damage amount = -amount + if(HAS_TRAIT(src, TRAIT_TOXIMMUNE)) //Prevents toxin damage, but not healing + amount = min(amount, 0) if(amount > 0) - blood_volume -= 5*amount + blood_volume = max(blood_volume - (5*amount), 0) else - blood_volume -= amount + blood_volume = max(blood_volume - amount, 0) + else if(HAS_TRAIT(src, TRAIT_TOXIMMUNE)) //Prevents toxin damage, but not healing + amount = min(amount, 0) return ..() /mob/living/carbon/getStaminaLoss() diff --git a/code/modules/mob/living/carbon/human/examine.dm b/code/modules/mob/living/carbon/human/examine.dm index ba32915496ec..0ed176a46e7f 100644 --- a/code/modules/mob/living/carbon/human/examine.dm +++ b/code/modules/mob/living/carbon/human/examine.dm @@ -15,6 +15,13 @@ . = list("*---------*\nThis is [!obscure_name ? name : "Unknown"]!") + var/vampDesc = ReturnVampExamine(user) // Fulpstation Bloodsuckers edit STARTS + var/vassDesc = ReturnVassalExamine(user) + if(vampDesc != "") + . += vampDesc + if(vassDesc != "") + . += vassDesc // Fulpstation Bloodsucker edit ENDS + var/list/obscured = check_obscured_slots() var/skipface = (wear_mask && (wear_mask.flags_inv & HIDEFACE)) || (head && (head.flags_inv & HIDEFACE)) @@ -225,6 +232,9 @@ if(DISGUST_LEVEL_DISGUSTED to INFINITY) msg += "[t_He] look[p_s()] extremely disgusted.\n" + if(ShowAsPaleExamine()) // Fulpstation Bloodsuckers edit: More leeway for giving away blood loss. + msg += "[t_He] [t_has] pale skin.\n" // Used to be BLOOD_VOLUME_SAFE (475), BLOOD_VOLUME_OKAY is (336) + switch(get_blood_state()) if(BLOOD_OKAY) msg += "[t_He] [t_has] pale skin.\n" diff --git a/code/modules/mob/living/carbon/human/species_types/vampire.dm b/code/modules/mob/living/carbon/human/species_types/vampire.dm index e1fe2bbb3360..4717632842f7 100644 --- a/code/modules/mob/living/carbon/human/species_types/vampire.dm +++ b/code/modules/mob/living/carbon/human/species_types/vampire.dm @@ -135,7 +135,7 @@ /obj/effect/proc_holder/spell/targeted/shapeshift/bat name = "Bat Form" - desc = "Take on the shape a space bat." + desc = "Take on the shape of a space bat." invocation = "Squeak!" charge_max = 50 cooldown_min = 50 diff --git a/icons/misc/language.dmi b/icons/misc/language.dmi index b7283574e06ef5f1875731585435a4c981953d60..229d540d841722ccf5fcb28012bad66d032e112b 100644 GIT binary patch literal 6228 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 literal 6002 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≧(CH|enBFP+kaKD49-!%uC6d>6TCDgA3lERGi4wh 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 QPjrCd2UXd6X|u5Z0n9z(y#N3J diff --git a/icons/mob/actions/actions_bloodsucker.dmi b/icons/mob/actions/actions_bloodsucker.dmi new file mode 100644 index 0000000000000000000000000000000000000000..3d11ee38c28eb2e85e86c018df941e5394a50bec GIT binary patch literal 21201 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 FV1lvT-;V$Q literal 0 HcmV?d00001 diff --git a/icons/mob/actions/actions_tremere_bloodsucker.dmi b/icons/mob/actions/actions_tremere_bloodsucker.dmi new file mode 100644 index 0000000000000000000000000000000000000000..057b17bbe103d2053b2fdf2a701ec73818365061 GIT binary patch literal 5371 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 literal 0 HcmV?d00001 diff --git a/icons/mob/hud.dmi b/icons/mob/hud.dmi index b8792a8eab265bac24083a338acffa2e887a9a37..a7857c93856fb7c8facda2c4891be92e32dd99ed 100644 GIT binary patch literal 16750 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