diff --git a/code/__DEFINES/~~oculis_defines/antagonists.dm b/code/__DEFINES/~~oculis_defines/antagonists.dm new file mode 100644 index 000000000000..9af4cf4d00e4 --- /dev/null +++ b/code/__DEFINES/~~oculis_defines/antagonists.dm @@ -0,0 +1,4 @@ +/// Checks if the given mob is a vampire +#define IS_VAMPIRE(mob) (mob?.mind?.has_antag_datum(/datum/antagonist/vampire)) +/// Checks if the given mob is a vassal +#define IS_VASSAL(mob) (mob?.mind?.has_antag_datum(/datum/antagonist/vassal)) diff --git a/code/__DEFINES/~~oculis_defines/crafting.dm b/code/__DEFINES/~~oculis_defines/crafting.dm new file mode 100644 index 000000000000..7468227b6eae --- /dev/null +++ b/code/__DEFINES/~~oculis_defines/crafting.dm @@ -0,0 +1 @@ +#define CAT_VAMPIRE "Vampire" diff --git a/code/__DEFINES/~~oculis_defines/dcs/signals/signals_mob/signals_mob_living.dm b/code/__DEFINES/~~oculis_defines/dcs/signals/signals_mob/signals_mob_living.dm new file mode 100644 index 000000000000..3dbe004aa9dd --- /dev/null +++ b/code/__DEFINES/~~oculis_defines/dcs/signals/signals_mob/signals_mob_living.dm @@ -0,0 +1,6 @@ +/// From base of /mob/living/simple_animal/attack_hand() and /mob/living/basic/attack_hand() when petting (non-combat): (mob/living/pet) +#define COMSIG_LIVING_PET_ANIMAL "living_pet_animal" +/// From base of carbon_defense.dm when hugging: (mob/living/carbon/hugged) +#define COMSIG_LIVING_HUG_CARBON "living_hug_carbon" +/// From base of /datum/element/art when appraising art: (atom/art_piece) +#define COMSIG_LIVING_APPRAISE_ART "living_appraise_art" diff --git a/code/__DEFINES/~~oculis_defines/do_afters.dm b/code/__DEFINES/~~oculis_defines/do_afters.dm new file mode 100644 index 000000000000..38ee7810c520 --- /dev/null +++ b/code/__DEFINES/~~oculis_defines/do_afters.dm @@ -0,0 +1,2 @@ +#define DOAFTER_SOURCE_ARCHIVE_OF_THE_KINDRED "doafter_archive_of_the_kindred" +#define DOAFTER_SOURCE_PERSUASION_RACK "doafter_persuasion_rack" diff --git a/code/__DEFINES/~~oculis_defines/factions.dm b/code/__DEFINES/~~oculis_defines/factions.dm new file mode 100644 index 000000000000..756591c43e79 --- /dev/null +++ b/code/__DEFINES/~~oculis_defines/factions.dm @@ -0,0 +1 @@ +#define FACTION_VAMPIRE "Vampire" diff --git a/code/__DEFINES/~~oculis_defines/hud.dm b/code/__DEFINES/~~oculis_defines/hud.dm new file mode 100644 index 000000000000..68d1b3e09a83 --- /dev/null +++ b/code/__DEFINES/~~oculis_defines/hud.dm @@ -0,0 +1,5 @@ +#define HUD_VAMPIRE_BLOOD "vampire_blood" +#define HUD_VAMPIRE_RANK "vampire_rank" +#define HUD_VAMPIRE_HUMANITY "vampire_humanity" +#define HUD_VAMPIRE_SUNLIGHT "vampire_sunlight" +#define HUD_VASSAL_TRACKER "vassal_tracker" diff --git a/code/__DEFINES/~~oculis_defines/language.dm b/code/__DEFINES/~~oculis_defines/language.dm new file mode 100644 index 000000000000..d08cf62d68d9 --- /dev/null +++ b/code/__DEFINES/~~oculis_defines/language.dm @@ -0,0 +1,2 @@ +#define LANGUAGE_VAMPIRE "vampire" +#define LANGUAGE_VASSAL "vassal" diff --git a/code/__DEFINES/~~oculis_defines/role_preferences.dm b/code/__DEFINES/~~oculis_defines/role_preferences.dm index 31175a045504..4d35e3b507b9 100644 --- a/code/__DEFINES/~~oculis_defines/role_preferences.dm +++ b/code/__DEFINES/~~oculis_defines/role_preferences.dm @@ -1,2 +1,8 @@ #define ROLE_HERETIC_SMUGGLER "Heretic Smuggler" #define ROLE_FORBIDDENCALLING "Forbidden Calling" + +#define ROLE_VAMPIRE "Vampire" +#define ROLE_VAMPIRIC_ACCIDENT "Vampiric Accident" +#define ROLE_VAMPIRE_BREAKOUT "Vampire Breakout" + +#define ROLE_VASSAL "Vassal" diff --git a/code/__DEFINES/~~oculis_defines/span.dm b/code/__DEFINES/~~oculis_defines/span.dm new file mode 100644 index 000000000000..53cc3b11844f --- /dev/null +++ b/code/__DEFINES/~~oculis_defines/span.dm @@ -0,0 +1,2 @@ +#define span_awe(str) ("" + str + "") +#define span_vampire_master(str) ("" + str + "") diff --git a/code/__DEFINES/~~oculis_defines/traits/declarations.dm b/code/__DEFINES/~~oculis_defines/traits/declarations.dm index 16cdde7bdaf1..03fd6cc86e2f 100644 --- a/code/__DEFINES/~~oculis_defines/traits/declarations.dm +++ b/code/__DEFINES/~~oculis_defines/traits/declarations.dm @@ -11,4 +11,14 @@ #define TRAIT_AMNESTICS "trait_amnestics" #define TRAIT_MNESTICS "trait_mnestics" +/// Hides TRAIT_GENELESS. +#define TRAIT_FAKEGENES "fakegenes" + +/// The user is "vampire aligned" - i.e a vampire or vassal. +/// Basically just check for `HAS_MIND_TRAIT(user, TRAIT_VAMPIRE_ALIGNED)` instead of `IS_VAMPIRE(user) || IS_VASSAL(user)` +#define TRAIT_VAMPIRE_ALIGNED "vampire_aligned" + +/// Slimepeople with this trait will not lose limbs from low blood/nutrition. +#define TRAIT_SLIME_NO_CANNIBALIZE "slime_no_cannibalize" + // END TRAIT DEFINES diff --git a/code/__DEFINES/~~oculis_defines/vampires.dm b/code/__DEFINES/~~oculis_defines/vampires.dm new file mode 100644 index 000000000000..5f7981b9e9b9 --- /dev/null +++ b/code/__DEFINES/~~oculis_defines/vampires.dm @@ -0,0 +1,208 @@ +/// Uncomment this to enable testing of Vampire features (such as vassalizing people with a mind instead of a client). +//#define VAMPIRE_TESTING +#if defined(VAMPIRE_TESTING) && defined(CIBUILDING) + #error VAMPIRE_TESTING is enabled, disable this! +#endif +#ifdef TESTING + #define VAMPIRE_TESTING +#endif + +// Blood-level defines +/// Determines Vampire regeneration rate +#define BS_BLOOD_VOLUME_MAX_REGEN 700 +/// Cost to vassalize someone halfway, in blood. Called twice for full cost. +#define VASSALIZATION_BLOOD_HALF_COST 8 +/// Cost to convert someone after successful vassalization, in blood. +#define VASSALIZATION_CONVERSION_COST 50 +/// Once blood is this low, will enter a Frenzy +#define FRENZY_THRESHOLD_ENTER 25 +/// Once blood is this high, will exit the Frenzy. Intentionally high, we want to kill the person we feed off of +#define FRENZY_THRESHOLD_EXIT 500 +/// How much blood drained from the vampire each lifetick +#define VAMPIRE_PASSIVE_BLOOD_DRAIN 0.1 +/// The number that incoming levels are divided by when comitting the Amaranth. Example: 2 would divide the victims level by 2, and give that to the diablerist +#define DIABLERIE_DIVISOR 1.5 +/// Amount of vitae drunk from another player required to level up. +#define VITAE_GOAL_STANDARD 250 + +/// Default amount of damage the vampire's punch/kick damage increases with each level. +#define VAMPIRE_UNARMED_DMG_INCREASE_ON_RANKUP 0.5 + +/// How many starting levels do we want each one to have? +#define VAMPIRE_STARTING_LEVELS 3 +/// How many free levels the vampire gets gradually. +#define VAMPIRE_FREE_LEVELS 3 +/// Vampire's default stamina resist. +#define VAMPIRE_INHERENT_STAMINA_RESIST 0.75 + +/// When do we warn them about their low blood? +#define VAMPIRE_LOW_BLOOD_WARNING 300 + +/// How much blood drained from the vampire each tick during sol +#define VAMPIRE_SOL_BURN 15 +/// We don't go below this threshold when in a shielded area during sol +#define VAMPIRE_SOL_SHIELD_THRESHOLD 500 + +/// Minimum blood required for vampire slimes to auto-revive. +#define SLIME_MIN_REVIVE_BLOOD_THRESHOLD (FRENZY_THRESHOLD_ENTER * 5) +/// How long it takes for a vampire slime to auto-revive, when left alone. +#define SLIME_VAMPIRE_REVIVE_TIME (1.5 MINUTES) +/// How many times faster a slime vampire will revive if their core is being held by a non-vampire/non-ally. +#define SLIME_VAMPIRE_REVIVE_HELD_MULTIPLIER 0.5 +/// How many times faster a slime vampire will revive if their core is being held by an ally. +#define SLIME_VAMPIRE_REVIVE_ALLY_MULTIPLIER 1.2 +/// How many times faster a slime vampire will revive if their core is in a coffin. +#define SLIME_VAMPIRE_REVIVE_COFFIN_MULTIPLIER 2.5 + +// vassal defines +/// If someone passes all checks and can be vassalized +#define VASSALIZATION_ALLOWED 0 +/// If someone has to accept vassalization +#define VASSALIZATION_DISLOYAL 1 +/// If someone is not allowed under any circimstances to become a vassal +#define VASSALIZATION_BANNED 2 + +/// How long Sol lasts +#define TIME_VAMPIRE_DAY 180 // 3 minutes +/// The grace period inbetween Sol +#define TIME_VAMPIRE_NIGHT 2700 // 45 minutes +/// First audio warning that Sol is coming +#define TIME_VAMPIRE_DAY_WARN_1 90 +/// Second audio warning that Sol is coming +#define TIME_VAMPIRE_DAY_WARN_2 30 +/// Final audio warning that Sol is coming +#define TIME_VAMPIRE_DAY_WARN_3 15 + +///How much time Sol can be 'off' by, keeping the time inconsistent. +#define TIME_VAMPIRE_SOL_DELAY 120 + +// Humanity gains (The actual tracking lists and such are in the datum duh) +// These are supposed to be somewhat nontrivial, to the point of sometimes not being viable. +/// Hugging of separate people +#define HUMANITY_HUGGING_TYPE "hug" + +/// Petting of separate animals +#define HUMANITY_PETTING_TYPE "pet" + +/// Watching of art +#define HUMANITY_ART_TYPE "art" + +#define HUMANITY_GAIN_TYPES list(HUMANITY_HUGGING_TYPE, HUMANITY_PETTING_TYPE, HUMANITY_ART_TYPE) + +/// Default Humanity +#define VAMPIRE_DEFAULT_HUMANITY 7 + +// List of areas that are shielded from sol. +#define VAMPIRE_SOL_SHIELDED list(/area/station/maintenance, /area/station/medical/morgue, /area/station/security/prison, /area/shuttle, /area/centcom) + +// Cooldown defines +// Used to prevent spamming vampires +/// Spam prevention for healing messages. +#define VAMPIRE_SPAM_HEALING 15 SECONDS +/// Spam prevention for Sol Masquerade messages. +#define VAMPIRE_SPAM_MASQUERADE 60 SECONDS + +/// Spam prevention for Sol messages. +#define VAMPIRE_SPAM_SOL 30 SECONDS + +// Clan defines +#define CLAN_BRUJAH "Brujah Clan" +#define CLAN_TOREADOR "Toreador Clan" +#define CLAN_NOSFERATU "Nosferatu Clan" +#define CLAN_TREMERE "Tremere Clan" +#define CLAN_GANGREL "Gangrel Clan" +#define CLAN_VENTRUE "Ventrue Clan" +#define CLAN_MALKAVIAN "Malkavian Clan" +#define CLAN_TZIMISCE "Tzimisce Clan" +#define CLAN_HECATA "Hecata Clan" +#define CLAN_LASOMBRA "Lasombra 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. +#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 used while silver cuffed +#define BP_ALLOW_WHILE_SILVER_CUFFED (1<<5) + +/// This is a Default Power that all Vampires get. +#define VAMPIRE_DEFAULT_POWER (1<<1) + +/// 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) +/// This Power has a cooldown that is more dynamic than a typical power +#define BP_AM_VERY_DYNAMIC_COOLDOWN (1<<4) + +///Called when a Vampire reaches Final Death. +#define COMSIG_VAMPIRE_FINAL_DEATH "vampire_final_death" + ///Whether the vampire should not be dusted when arriving Final Death + #define DONT_DUST (1<<0) + +// Vampire Signals +/// Called when a Vampire breaks the Masquerade +#define COMSIG_VAMPIRE_BROKE_MASQUERADE "comsig_vampire_broke_masquerade" + +// Signals & Defines +/// Sent every Sol tick. +#define COMSIG_SOL_TICK "comsig_sol_tick" +/// Sent every Sol tick while the sun is up. +#define COMSIG_SOL_RISE_TICK "comsig_sol_rise_tick" +/// Sent 90 seconds before Sol begins +#define COMSIG_SOL_NEAR_START "comsig_sol_near_start" +/// Sent at the end of Sol +#define COMSIG_SOL_END "comsig_sol_end" +/// Sent 15 seconds before Sol ends +#define COMSIG_SOL_NEAR_END "comsig_sol_near_end" +/// Sent when a warning for Sol is meant to go out: (danger_level, vampire_warning_message, vassal_warning_message) +#define COMSIG_SOL_WARNING_GIVEN "comsig_sol_warning_given" +/// Sent when tracking humanity gain progress: (type, subject) +#define COMSIG_VAMPIRE_TRACK_HUMANITY_GAIN "comsig_vampire_track_humanity_gain" + +#define DANGER_LEVEL_FIRST_WARNING 1 +#define DANGER_LEVEL_SECOND_WARNING 2 +#define DANGER_LEVEL_THIRD_WARNING 3 +#define DANGER_LEVEL_SOL_ROSE 4 +#define DANGER_LEVEL_SOL_ENDED 5 + +/// Called on the mind when a Vampire chooses a clan: (datum/antagonist/vampire, datum/vampire_clan) +#define COMSIG_VAMPIRE_CLAN_CHOSEN "vampire_clan_chosen" + +// Clan defines +/// Drinks blood the normal Vampire way. +#define VAMPIRE_DRINK_NORMAL "vampire_drink_normal" +/// Drinks blood but is snobby, refusing to drink from mindless +#define VAMPIRE_DRINK_SNOBBY "vampire_drink_snobby" +// Masquerade ability given at this point or above +#define VAMPIRE_HUMANITY_MASQUERADE_POWER 7 + +// Traits +/// Falsifies Health analyzer blood levels +#define TRAIT_FEIGN_LIFE "feign_life" +/// For people in the middle of being staked +#define TRAIT_BEINGSTAKED "beingstaked" +/// This vampire is currently in a frenzy, +#define TRAIT_FRENZY "frenzy" +/// This vampire is currently in torpor. +#define TRAIT_TORPOR "torpor" +/// This vampire can tell if another vampire has committed diablere on examine. +#define TRAIT_SEE_DIABLERIE "see_diablerie" + +// Trait sources +/// Source trait for all vampire traits +#define TRAIT_VAMPIRE "trait_vampire" + +// Macros +#define IS_CURATOR(mob) istype(mob?.mind?.assigned_role, /datum/job/curator) +// #define IS_CURATOR(mob) (IS_CURATOR(mob) || IS_MONSTERHUNTER(mob)) diff --git a/code/__HELPERS/~~oculis_helpers/view.dm b/code/__HELPERS/~~oculis_helpers/view.dm new file mode 100644 index 000000000000..2ffbcebf1b90 --- /dev/null +++ b/code/__HELPERS/~~oculis_helpers/view.dm @@ -0,0 +1,15 @@ +//Returns an in proportion scaled out view, with zoom_amt extra tiles on the y axis. +/proc/get_zoomed_view(view, zoom_amt) + var/view_x + var/view_y + if(IS_FINITE(view)) + return view + zoom_amt + else + var/list/viewrangelist = splittext(view, "x") + view_x = text2num(viewrangelist[1]) + view_y = text2num(viewrangelist[2]) + var/proportion = view_x / view_x + view_x += zoom_amt * proportion + view_y += zoom_amt + //God, I hate that we have to round this. + return "[round(view_x, 1)]x[round(view_y, 1)]" diff --git a/code/game/objects/items/devices/scanners/sequence_scanner.dm b/code/game/objects/items/devices/scanners/sequence_scanner.dm index 96db499a1e6d..4f86fccbe6ce 100644 --- a/code/game/objects/items/devices/scanners/sequence_scanner.dm +++ b/code/game/objects/items/devices/scanners/sequence_scanner.dm @@ -48,7 +48,7 @@ add_fingerprint(user) //no scanning if its a husk or DNA-less Species - if (!HAS_TRAIT(interacting_with, TRAIT_GENELESS) && !HAS_TRAIT(interacting_with, TRAIT_BADDNA)) + if ((!HAS_TRAIT(interacting_with, TRAIT_GENELESS) || HAS_TRAIT(interacting_with, TRAIT_FAKEGENES)) && !HAS_TRAIT(interacting_with, TRAIT_BADDNA)) // OCULIS EDIT CHANGE - VAMPIRES - ORIGINAL: if (!HAS_TRAIT(interacting_with, TRAIT_GENELESS) && !HAS_TRAIT(interacting_with, TRAIT_BADDNA)) user.visible_message(span_notice("[user] analyzes [interacting_with]'s genetic sequence.")) balloon_alert(user, "sequence analyzed") playsound(user, 'sound/items/healthanalyzer.ogg', 50) // close enough diff --git a/code/modules/admin/sql_ban_system.dm b/code/modules/admin/sql_ban_system.dm index d954bf7a1217..51a796fd0cc4 100644 --- a/code/modules/admin/sql_ban_system.dm +++ b/code/modules/admin/sql_ban_system.dm @@ -407,6 +407,11 @@ ROLE_BORER, ROLE_ASSAULT_OPERATIVE, // NOVA EDIT ADDITION END + // OCULIS EDIT ADDITION START - VAMPIRES + ROLE_VAMPIRE, + ROLE_VAMPIRIC_ACCIDENT, + ROLE_VAMPIRE_BREAKOUT, + // OCULIS EDIT ADDITION END ), // NOVA EDIT ADDITION START - EXTRA_BANS "Nova Ban Options" = list( diff --git a/code/modules/antagonists/_common/antag_datum.dm b/code/modules/antagonists/_common/antag_datum.dm index 3ef5ff1def22..2fc551ab51b2 100644 --- a/code/modules/antagonists/_common/antag_datum.dm +++ b/code/modules/antagonists/_common/antag_datum.dm @@ -615,6 +615,10 @@ GLOBAL_LIST_EMPTY(antagonists) /datum/objective/survive, ///datum/objective/martyr, // NOVA EDIT REMOVAL /datum/objective/exile, + // OCULIS EDIT ADDITION START - VAMPIRES + /datum/objective/vampire/scourge, + /datum/objective/vampire/prince, + // OCULIS EDIT ADDITION END ) for (var/datum/objective/check_objective in objectives) if (is_type_in_list(check_objective, escape_objectives)) diff --git a/code/modules/antagonists/brainwashing/brainwashing.dm b/code/modules/antagonists/brainwashing/brainwashing.dm index 716e2cb494fd..ab9262138f1e 100644 --- a/code/modules/antagonists/brainwashing/brainwashing.dm +++ b/code/modules/antagonists/brainwashing/brainwashing.dm @@ -1,4 +1,5 @@ -/proc/brainwash(mob/living/brainwash_victim, directives) +/proc/brainwash(mob/living/brainwash_victim, directives, source) // OCULIS EDIT - original: /proc/brainwash(mob/living/brainwash_victim, directives, source) + . = list() // OCULIS EDIT ADDITION - brainwashing refactor if(!brainwash_victim.mind) return if(!islist(directives)) @@ -8,16 +9,27 @@ if(brainwashed_datum) for(var/O in directives) var/datum/objective/brainwashing/objective = new(O) + // OCULIS EDIT ADDITION START + if(source) + objective.source = source + . += WEAKREF(objective) + // OCULIS EDIT ADDITION END brainwashed_datum.objectives += objective brainwashed_datum.greet() else brainwashed_datum = new() for(var/O in directives) var/datum/objective/brainwashing/objective = new(O) + // OCULIS EDIT ADDITION START + if(source) + objective.source = source + . += WEAKREF(objective) + // OCULIS EDIT ADDITION END brainwashed_datum.objectives += objective brainwash_mind.add_antag_datum(brainwashed_datum) - var/begin_message = " has been brainwashed with the following objectives: " + var/source_message = source ? " by [source]" : "" // OCULIS EDIT ADDITION + var/begin_message = " has been brainwashed with the following objective[length(directives) > 1 ? "s" : ""][source_message]: " // OCULIS EDIT CHANGE - ORIGINAL: var/begin_message = " has been brainwashed with the following objectives: " var/obj_message = english_list(directives) var/rendered = begin_message + obj_message if(!(rendered[length(rendered)] in list(",",":",";",".","?","!","\'","-"))) @@ -26,6 +38,43 @@ if(check_holidays(APRIL_FOOLS)) // Note: most of the time you're getting brainwashed you're unconscious brainwash_victim.say("You son of a bitch! I'm in.", forced = "That son of a bitch! They're in. (April Fools)") + brainwashed_datum.update_static_data_for_all_viewers() // OCULIS EDIT ADDITION - ensure that objectives show up properly + +// OCULIS EDIT ADDITION START - add unbrainwash proc. kept in here so that it's right next to /proc/brainwash +/// Removes objectives from someone's brainwash. +/proc/unbrainwash(mob/living/victim, list/directives) + var/datum/antagonist/brainwashed/brainwash = victim?.mind?.has_antag_datum(/datum/antagonist/brainwashed) + if(!brainwash) + return FALSE + if(directives) + if(!isnull(directives) && !islist(directives)) + directives = list(directives) + var/list/removed_objectives = list() + var/list/objective_texts = list() + for(var/datum/objective/directive as anything in directives) + if(istype(directive, /datum/weakref)) + var/datum/weakref/directive_weakref = directive + directive = directive_weakref.resolve() + if(!istype(directive)) + continue + brainwash.objectives -= directive + removed_objectives += directive + objective_texts += "\"[directive.explanation_text]\"" + log_admin("[key_name(victim)] had the following brainwashing objective[length(removed_objectives) > 1 ? "s" : ""] removed: [english_list(objective_texts)].") + if(LAZYLEN(brainwash.objectives)) + to_chat(victim, span_userdanger("[length(removed_objectives) > 1 ? "Some" : "One"] of your Directives fade away! You only have to obey the remaining Directives now.")) + victim.mind.announce_objectives() + else + victim.mind.remove_antag_datum(/datum/antagonist/brainwashed) + QDEL_LIST(removed_objectives) + else + var/list/objective_texts = list() + for(var/datum/objective/directive as anything in brainwash.objectives) + objective_texts += "\"[directive.explanation_text]\"" + log_admin("[key_name(victim)] had all of their brainwashing objectives removed: [english_list(objective_texts)].") + QDEL_LIST(brainwash.objectives) + victim.mind.remove_antag_datum(/datum/antagonist/brainwashed) +// OCULIS EDIT ADDITION END /datum/antagonist/brainwashed name = "\improper Brainwashed Victim" @@ -84,3 +133,4 @@ /datum/objective/brainwashing completed = TRUE + var/source // OCULIS EDIT ADDITION diff --git a/code/modules/antagonists/heretic/influences.dm b/code/modules/antagonists/heretic/influences.dm index 957bfae4ccde..54c40fbfa5a7 100644 --- a/code/modules/antagonists/heretic/influences.dm +++ b/code/modules/antagonists/heretic/influences.dm @@ -159,6 +159,12 @@ if(IS_HERETIC(user) || !ishuman(user)) return + // OCULIS EDIT ADDITION START - VAMPIRES + var/datum/antagonist/vampire/vampire_datum = IS_VAMPIRE(user) + if(istype(vampire_datum?.my_clan, /datum/vampire_clan/malkavian)) // yeah yeah the time knife all malks have seen it + return + // OCULIS EDIT ADDITION END + . += span_userdanger("Your mind burns as you stare at the tear!") user.adjust_organ_loss(ORGAN_SLOT_BRAIN, 10, 190) user.add_mood_event("gates_of_mansus", /datum/mood_event/gates_of_mansus) diff --git a/code/modules/mob/living/carbon/carbon_defense.dm b/code/modules/mob/living/carbon/carbon_defense.dm index 143881f20d0f..744513be3d61 100644 --- a/code/modules/mob/living/carbon/carbon_defense.dm +++ b/code/modules/mob/living/carbon/carbon_defense.dm @@ -371,6 +371,7 @@ if(HAS_TRAIT(src, TRAIT_SENSITIVESNOUT) && is_location_accessible(BODY_ZONE_PRECISE_MOUTH)) var/datum/quirk/sensitivesnout/poor_snout = src.get_quirk(/datum/quirk/sensitivesnout) poor_snout?.get_booped(helper) + SEND_SIGNAL(helper, COMSIG_LIVING_HUG_CARBON, src) // OCULIS EDIT ADDITION - VAMPIRES return //NOVA EDIT ADDITION END else if(check_zone(helper.zone_selected) == BODY_ZONE_HEAD && get_bodypart(BODY_ZONE_HEAD)) //Headpats! @@ -397,6 +398,8 @@ emote("wag") //NOVA EDIT ADDITION END + SEND_SIGNAL(helper, COMSIG_LIVING_HUG_CARBON, src) // OCULIS EDIT ADDITION - VAMPIRES + else if ((helper.zone_selected == BODY_ZONE_PRECISE_GROIN) && !isnull(src.get_organ_by_type(/obj/item/organ/tail))) helper.visible_message(span_notice("[helper] pulls on [src]'s tail!"), \ null, span_hear("You hear a soft patter."), DEFAULT_MESSAGE_RANGE, list(helper, src)) @@ -427,6 +430,7 @@ null, span_hear("You hear a soft patter."), DEFAULT_MESSAGE_RANGE, list(helper, src)) to_chat(helper, span_notice ("You shake [src]'s hand.")) to_chat(src, span_notice ("[helper] shakes your hand.")) + SEND_SIGNAL(helper, COMSIG_LIVING_HUG_CARBON, src) // OCULIS EDIT ADDITION - VAMPIRES //IRIS ADDITION END else @@ -450,6 +454,8 @@ // Warm them up with hugs share_bodytemperature(helper) + SEND_SIGNAL(helper, COMSIG_LIVING_HUG_CARBON, src) // OCULIS EDIT ADDITION - VAMPIRES + // No moodlets for people who hate touches if(!HAS_TRAIT(src, TRAIT_BADTOUCH)) if (helper.grab_state >= GRAB_AGGRESSIVE) diff --git a/code/modules/mob/living/carbon/human/species_types/jellypeople.dm b/code/modules/mob/living/carbon/human/species_types/jellypeople.dm index 88fd18347db5..6982d0ff9b05 100644 --- a/code/modules/mob/living/carbon/human/species_types/jellypeople.dm +++ b/code/modules/mob/living/carbon/human/species_types/jellypeople.dm @@ -92,6 +92,10 @@ return HANDLE_BLOOD_NO_NUTRITION_DRAIN|HANDLE_BLOOD_NO_OXYLOSS /datum/species/jelly/proc/Cannibalize_Body(mob/living/carbon/human/H) + // OCULIS EDIT ADDITION START - add TRAIT_SLIME_NO_CANNIBALIZE + if(HAS_TRAIT(H, TRAIT_SLIME_NO_CANNIBALIZE)) + return + // OCULIS EDIT ADDITION END var/list/limbs_to_consume = list(BODY_ZONE_R_ARM, BODY_ZONE_L_ARM, BODY_ZONE_R_LEG, BODY_ZONE_L_LEG) - H.get_missing_limbs() var/obj/item/bodypart/consumed_limb if(!length(limbs_to_consume)) diff --git a/code/modules/mob/living/simple_animal/animal_defense.dm b/code/modules/mob/living/simple_animal/animal_defense.dm index ac3637d7fd26..e302aed0106d 100644 --- a/code/modules/mob/living/simple_animal/animal_defense.dm +++ b/code/modules/mob/living/simple_animal/animal_defense.dm @@ -14,6 +14,8 @@ span_notice("[user] [response_help_continuous] you."), null, null, user) to_chat(user, span_notice("You [response_help_simple] [src].")) playsound(loc, 'sound/items/weapons/thudswoosh.ogg', 50, TRUE, -1) + + SEND_SIGNAL(user, COMSIG_LIVING_PET_ANIMAL, src) // OCULIS EDIT ADDITION - VAMPIRES else if(HAS_TRAIT(user, TRAIT_PACIFISM)) to_chat(user, span_warning("You don't want to hurt [src]!")) @@ -62,6 +64,7 @@ span_notice("[user.name] [response_help_continuous] you."), null, COMBAT_MESSAGE_RANGE, user) to_chat(user, span_notice("You [response_help_simple] [src].")) playsound(loc, 'sound/items/weapons/thudswoosh.ogg', 50, TRUE, -1) + SEND_SIGNAL(user, COMSIG_LIVING_PET_ANIMAL, src) // OCULIS EDIT ADDITION - VAMPIRES /mob/living/simple_animal/attack_alien(mob/living/carbon/alien/adult/user, list/modifiers) diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_vampire.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_vampire.png new file mode 100644 index 000000000000..b7f69042fc6b Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_vampire.png differ diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_vampirebreakout.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_vampirebreakout.png new file mode 100644 index 000000000000..b7f69042fc6b Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_vampirebreakout.png differ diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_vampiricaccident.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_vampiricaccident.png new file mode 100644 index 000000000000..b7f69042fc6b Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_vampiricaccident.png differ diff --git a/modular_iris/bubber_ports/code/bloodsuckers/bloodsucker_defines.dm b/modular_iris/bubber_ports/code/bloodsuckers/bloodsucker_defines.dm deleted file mode 100644 index 9dfc8d6aafe7..000000000000 --- a/modular_iris/bubber_ports/code/bloodsuckers/bloodsucker_defines.dm +++ /dev/null @@ -1,221 +0,0 @@ -///Uncomment this to enable testing of Bloodsucker features (such as ghoulizing people with a mind instead of a client). -// #define BLOODSUCKER_TESTING // if this isn't commented out, someone is a dumbfuck - -/// You have special interactions with Bloodsuckers -#define TRAIT_BLOODSUCKER_HUNTER "bloodsucker_hunter" - -// how much to multiply the coffin size by mob_size -#define COFFIN_ENLARGE_MULT 0.5 - -/// At what health to burn damage ratio you Final Death -#define FINAL_DEATH_HEALTH_TO_BURN 2.5 -/** - * Blood-level defines - */ -/// Determines Bloodsucker regeneration rate -#define BS_BLOOD_VOLUME_MAX_REGEN 700 -/// Cost to torture someone halfway, in blood. Called twice for full cost -#define TORTURE_BLOOD_HALF_COST 4 -/// Cost to convert someone after successful torture, in blood -#define TORTURE_CONVERSION_COST 10 -/// How much blood it costs you to make a ghoul into a special ghoul -#define SPECIAL_GHOUL_COST 150 -/// Minimum and maximum frenzy blood thresholds -/// 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 - -/// a bloodsucker can't loose more humanity than this, and looses the masquerade ability when reaching it -#define HUMANITY_LOST_MAXIMUM 50 - -/// Level up blood cost define, max_blood * this = blood cost -#define BLOODSUCKER_LEVELUP_PERCENTAGE 0.40 -#define BLOODSUCKER_LEVELUP_PERCENTAGE_VENTRUE BLOODSUCKER_LEVELUP_PERCENTAGE - 0.1 - -///The level when at a bloodsucker becomes snobby about who they drink from and gain their non-fledling reputation -#define BLOODSUCKER_HIGH_LEVEL 4 - -/** - * Sol defines - */ -///How long Sol will last until it's night again. -#define TIME_BLOODSUCKER_DAY 180 -///Base time nighttime should be in for, until Sol rises. -// Can't put defines in defines, so we have to use deciseconds. -#define TIME_BLOODSUCKER_NIGHT_MAX 1320 // 22 minutes -#define TIME_BLOODSUCKER_NIGHT_MIN 1020 // 17 minutes - -///Time left to send an alert to Bloodsuckers about an incoming Sol. -#define TIME_BLOODSUCKER_DAY_WARN 90 -///Time left to send an urgent alert to Bloodsuckers about an incoming Sol. -#define TIME_BLOODSUCKER_DAY_FINAL_WARN 30 -///Time left to alert that Sol is rising. -#define TIME_BLOODSUCKER_BURN_INTERVAL 5 - -///How much time Sol can be 'off' by, keeping the time inconsistent. -#define TIME_BLOODSUCKER_SOL_DELAY 90 - -/** - * Ghoul defines - */ -///If someone passes all checks and can be ghouled -#define GHOULING_ALLOWED 0 -///If someone has to accept ghouling -#define GHOULING_DISLOYAL 1 -///If someone is not allowed under any circimstances to become a Ghoul -#define GHOULING_BANNED 2 - -/** - * Cooldown defines - * Used in Cooldowns Bloodsuckers use to prevent spamming - */ -///Spam prevention for healing messages. -#define BLOODSUCKER_SPAM_HEALING (15 SECONDS) -///Span prevention for Sol Masquerade messages. -#define BLOODSUCKER_SPAM_MASQUERADE (60 SECONDS) - -///Span prevention for Sol messages. -#define BLOODSUCKER_SPAM_SOL (30 SECONDS) - - -/** - * Clan defines - */ -#define CLAN_NONE "Caitiff" -#define CLAN_BRUJAH "Brujah Clan" -#define CLAN_TOREADOR "Toreador Clan" -#define CLAN_NOSFERATU "Nosferatu Clan" -#define CLAN_TREMERE "Tremere Clan" -#define CLAN_GANGREL "Gangrel Clan" -#define CLAN_VENTRUE "Ventrue Clan" -#define CLAN_MALKAVIAN "Malkavian Clan" -#define CLAN_TZIMISCE "Tzimisce Clan" - -#define TREMERE_GHOUL "tremere_ghoul" -#define FAVORITE_GHOUL "favorite_ghoul" -#define REVENGE_GHOUL "revenge_ghoul" - -/** - * Power defines - */ -/// This Power can't be used in Torpor -#define BP_CANT_USE_IN_TORPOR (1<<0) -/// This Power can't be used in Frenzy. -#define BP_CANT_USE_IN_FRENZY (1<<1) -/// This Power can be used while transformed, for example by the shapeshift spell -#define BP_CAN_USE_TRANSFORMED (1<<2) -/// This Power can be used with a stake in you -#define BP_CAN_USE_WHILE_STAKED (1<<4) -/// This Power can be used while heartless -#define BP_CAN_USE_HEARTLESS (1<<5) - -/// This Power can be purchased by Bloodsuckers -#define BLOODSUCKER_CAN_BUY (1<<0) -/// This is a Default Power that all Bloodsuckers get. -#define BLOODSUCKER_DEFAULT_POWER (1<<1) -/// This Power can be purchased by Tremere Bloodsuckers -#define TREMERE_CAN_BUY (1<<2) - -/// This Power can be purchased by Ghouls -#define GHOUL_CAN_BUY (1<<3) - -/// If this Power can be bought if you already own it -#define CAN_BUY_OWNED (1<<4) - - -/// This Power is a Continuous Effect, processing every tick -#define BP_CONTINUOUS_EFFECT (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) - -#define DEACTIVATE_POWER_DO_NOT_REMOVE (1<<0) -#define DEACTIVATE_POWER_NO_COOLDOWN (1<<1) - -// ability levels that are used cross-file -#define DOMINATE_GHOULIZE_LEVEL 2 -#define TREMERE_OBJECTIVE_POWER_LEVEL 4 - -#define COFFIN_HEAL_COST_MULT 0.5 - - -/** - * Torpor check bitflags - */ -#define TORPOR_SKIP_CHECK_ALL (1<<0) -#define TORPOR_SKIP_CHECK_FRENZY (1<<1) -#define TORPOR_SKIP_CHECK_DAMAGE (1<<2) - -/** - * Bloodsucker Signals - */ -///Called when a Bloodsucker ranks up: (datum/bloodsucker_datum, mob/owner, mob/target) -#define COMSIG_BLOODSUCKER_RANK_UP "bloodsucker_rank_up" -///Called when a Bloodsucker interacts with a Ghoul on their persuasion rack. -#define COMSIG_BLOODSUCKER_INTERACT_WITH_GHOUL "bloodsucker_interact_with_ghoul" -///Called when a Bloodsucker makes a Ghoul into their Favorite Ghoul: (datum/ghoul_datum, mob/master) -#define COMSIG_BLOODSUCKER_MAKE_FAVORITE "bloodsucker_make_favorite" -// called when a bloodsucker looses their favorite ghoul, cleaning up whatever they gained -#define COMSIG_BLOODSUCKER_LOOSE_FAVORITE "bloodsucker_loose_favorite" -///Called when a new Ghoul is successfully made: (datum/bloodsucker_datum) -#define COMSIG_BLOODSUCKER_MADE_GHOUL "bloodsucker_made_ghoul" -///Called when a Bloodsucker exits Torpor. -#define COMSIG_BLOODSUCKER_EXIT_TORPOR "bloodsucker_exit_torpor" -///Called when a Bloodsucker reaches Final Death. -#define COMSIG_BLOODSUCKER_FINAL_DEATH "bloodsucker_final_death" - ///Whether the Bloodsucker should not be dusted when arriving Final Death - #define DONT_DUST (1<<0) -///Called when a Bloodsucker breaks the Masquerade -#define COMSIG_BLOODSUCKER_BROKE_MASQUERADE "comsig_bloodsucker_broke_masquerade" -///Called when a Bloodsucker enters Frenzy -#define COMSIG_BLOODSUCKER_ENTERS_FRENZY "bloodsucker_enters_frenzy" -///Called when a Bloodsucker exits Frenzy -#define COMSIG_BLOODSUCKER_EXITS_FRENZY "bloodsucker_exits_frenzy" -/// COMSIG_ATOM_EXAMINE that correctly updates when the bloodsucker datum is moved -#define COMSIG_BLOODSUCKER_EXAMINE "bloodsucker_examine" -// Called when anyone enters the coffin -#define COMSIG_ENTER_COFFIN "enter_coffin" -#define COMSIG_MOB_STAKED "staked" -#define COMSIG_BODYPART_STAKED "staked" -// called when a targeted ability is cast -#define COMSIG_FIRE_TARGETED_POWER "comsig_fire_targeted_power" - -/** - * Sol signals & Defines - */ -#define COMSIG_SOL_RANKUP_BLOODSUCKERS "comsig_sol_rankup_bloodsuckers" -#define COMSIG_SOL_RISE_TICK "comsig_sol_rise_tick" -#define COMSIG_SOL_NEAR_START "comsig_sol_near_start" -#define COMSIG_SOL_END "comsig_sol_end" -///Sent when a warning for Sol is meant to go out: (danger_level, vampire_warning_message, ghoul_warning_message) -#define COMSIG_SOL_WARNING_GIVEN "comsig_sol_warning_given" -///Called on a Bloodsucker's Lifetick. -#define COMSIG_BLOODSUCKER_ON_LIFETICK "comsig_bloodsucker_on_lifetick" -/// Called when a Bloodsucker's blood is updated -#define BLOODSUCKER_UPDATE_BLOOD "bloodsucker_update_blood" - #define BLOODSUCKER_UPDATE_BLOOD_DISABLED (1<<0) - -#define DANGER_LEVEL_FIRST_WARNING 1 -#define DANGER_LEVEL_SECOND_WARNING 2 -#define DANGER_LEVEL_THIRD_WARNING 3 -#define DANGER_LEVEL_SOL_ROSE 4 -#define DANGER_LEVEL_SOL_ENDED 5 - -/** - * Clan defines - * - * This is stuff that is used solely by Clans for clan-related activity. - */ -///Drinks blood the normal Bloodsucker way. -#define BLOODSUCKER_DRINK_NORMAL "bloodsucker_drink_normal" -///Drinks blood but is snobby, refusing to drink from mindless -#define BLOODSUCKER_DRINK_SNOBBY "bloodsucker_drink_snobby" -///Drinks blood from disgusting creatures without Humanity consequences. -#define BLOODSUCKER_DRINK_INHUMANELY "bloodsucker_drink_inhumanely" - -#define BLOODSUCKER_SIGHT_COLOR_CUTOFF list(25, 8, 5) -#define POLL_IGNORE_GHOUL "ghoul" diff --git a/modular_iris/bubber_ports/code/bloodsuckers/hud.dm b/modular_iris/bubber_ports/code/bloodsuckers/hud.dm deleted file mode 100644 index bf904a79ccb6..000000000000 --- a/modular_iris/bubber_ports/code/bloodsuckers/hud.dm +++ /dev/null @@ -1,80 +0,0 @@ -/// 1 tile down -#define UI_BLOOD_DISPLAY "WEST:6,CENTER-1:0" -/// 2 tiles down -#define UI_VAMPRANK_DISPLAY "WEST:6,CENTER-2:-5" -/// 6 pixels to the right, zero tiles & 5 pixels DOWN. -#define UI_SUNLIGHT_DISPLAY "WEST:6,CENTER-0:0" - -///Maptext define for Bloodsucker HUDs -#define FORMAT_BLOODSUCKER_HUD_TEXT(valuecolor, value) MAPTEXT("
[round(value,1)]
") -///Maptext define for Bloodsucker Sunlight HUDs -#define FORMAT_BLOODSUCKER_SUNLIGHT_TEXT(valuecolor, value) MAPTEXT("
[value]
") - -/atom/movable/screen/bloodsucker - icon = 'modular_iris/bubber_ports/icons/mob/actions/bloodsucker.dmi' - -/atom/movable/screen/bloodsucker/blood_counter - name = "Blood Consumed" - icon_state = "blood_display" - screen_loc = UI_BLOOD_DISPLAY - -/atom/movable/screen/bloodsucker/blood_counter/proc/update_blood_hud(blood_volume) - maptext = FORMAT_BLOODSUCKER_HUD_TEXT(hud_text_color(), blood_volume) - -/atom/movable/screen/bloodsucker/sunlight_counter - name = "Solar Flare Timer" - icon_state = "sunlight" - screen_loc = UI_SUNLIGHT_DISPLAY - -/atom/movable/screen/bloodsucker/sunlight_counter/Initialize(mapload, datum/hud/hud_owner) - . = ..() - update_sol_hud() - START_PROCESSING(SSsunlight, src) - -/atom/movable/screen/bloodsucker/sunlight_counter/Destroy() - STOP_PROCESSING(SSsunlight, src) - . = ..() - -/atom/movable/screen/bloodsucker/sunlight_counter/proc/update_sol_hud() - var/valuecolor = hud_text_color() - if(!SSsunlight) - return - if(SSsunlight.sunlight_active) - valuecolor = "#FF5555" - icon_state = "[initial(icon_state)]_day" - else - switch(round(SSsunlight.time_til_cycle, 1)) - if(0 to 30) - icon_state = "[initial(icon_state)]_30" - valuecolor = "#FFCCCC" - if(31 to 60) - icon_state = "[initial(icon_state)]_60" - valuecolor = "#FFE6CC" - if(61 to 90) - icon_state = "[initial(icon_state)]_90" - valuecolor = "#FFFFCC" - else - icon_state = "[initial(icon_state)]_night" - valuecolor = "#FFFFFF" - maptext = FORMAT_BLOODSUCKER_SUNLIGHT_TEXT( \ - valuecolor, \ - (SSsunlight.time_til_cycle >= 60) ? "[round(SSsunlight.time_til_cycle / 60, 1)] m" : "[round(SSsunlight.time_til_cycle, 1)] s" \ - ) - -/atom/movable/screen/bloodsucker/sunlight_counter/process(seconds_per_tick) - update_sol_hud() - -/atom/movable/screen/bloodsucker/proc/hud_text_color(blood_volume) - return blood_volume > BLOOD_VOLUME_SAFE ? "#FFDDDD" : "#FFAAAA" - -/// 1 tile down -#undef UI_BLOOD_DISPLAY -/// 2 tiles down -#undef UI_VAMPRANK_DISPLAY -/// 6 pixels to the right, zero tiles & 5 pixels DOWN. -#undef UI_SUNLIGHT_DISPLAY - -///Maptext define for Bloodsucker HUDs -#undef FORMAT_BLOODSUCKER_HUD_TEXT -///Maptext define for Bloodsucker Sunlight HUDs -#undef FORMAT_BLOODSUCKER_SUNLIGHT_TEXT diff --git a/modular_iris/bubber_ports/code/controllers/subsystem/processing/sol.dm b/modular_iris/bubber_ports/code/controllers/subsystem/processing/sol.dm new file mode 100644 index 000000000000..0f6079204fd3 --- /dev/null +++ b/modular_iris/bubber_ports/code/controllers/subsystem/processing/sol.dm @@ -0,0 +1,64 @@ +SUBSYSTEM_DEF(sol) + name = "Sol" + wait = 1 SECONDS + ss_flags = SS_NO_INIT | SS_BACKGROUND + + ///If the Sun is currently out our not. + var/sunlight_active = FALSE + ///The time between the next cycle, randomized every night. + var/time_til_cycle = TIME_VAMPIRE_NIGHT + ///If Vampire levels for the night has been given out yet. + var/issued_XP = FALSE + +/datum/controller/subsystem/sol/fire(resumed = FALSE) + time_til_cycle-- + SEND_SIGNAL(src, COMSIG_SOL_TICK) + if(sunlight_active) + if(time_til_cycle > 0) + SEND_SIGNAL(src, COMSIG_SOL_RISE_TICK) + if(!issued_XP && time_til_cycle <= 15) + issued_XP = TRUE + SEND_SIGNAL(src, COMSIG_SOL_NEAR_END) + if(time_til_cycle <= 1) + sunlight_active = FALSE + issued_XP = FALSE + //randomize the next sol timer + time_til_cycle = round(rand((TIME_VAMPIRE_NIGHT-TIME_VAMPIRE_SOL_DELAY), (TIME_VAMPIRE_NIGHT+TIME_VAMPIRE_SOL_DELAY)), 1) + SEND_SIGNAL(src, COMSIG_SOL_END) + warn_daylight( + danger_level = DANGER_LEVEL_SOL_ENDED, + vampire_warning_message = span_announce("The solar flare has ended, and the daylight danger has passed... for now."), + vassal_warning_message = span_announce("The solar flare has ended, and the daylight danger has passed... for now."), + ) + return + + switch(time_til_cycle) + if(TIME_VAMPIRE_DAY_WARN_1) + SEND_SIGNAL(src, COMSIG_SOL_NEAR_START) + warn_daylight( + danger_level = DANGER_LEVEL_FIRST_WARNING, + vampire_warning_message = span_danger("Solar Flares will bombard the station with dangerous UV radiation in [TIME_VAMPIRE_DAY_WARN_1 / 60] minutes. Prepare to seek cover in a coffin or closet.") + ) + if(TIME_VAMPIRE_DAY_WARN_2) + warn_daylight( + danger_level = DANGER_LEVEL_SECOND_WARNING, + vampire_warning_message = span_bolddanger("Solar Flares are about to bombard the station! You have [TIME_VAMPIRE_DAY_WARN_2] seconds to find cover!"), + vassal_warning_message = span_danger("In [TIME_VAMPIRE_DAY_WARN_2] seconds, your master will be at risk of a Solar Flare. Make sure they find cover!"), + ) + if(TIME_VAMPIRE_DAY_WARN_3) + warn_daylight( + danger_level = DANGER_LEVEL_THIRD_WARNING, + vampire_warning_message = span_narsiesmall("YET AGAIN, SOL RISES!"), + ) + if(NONE) + sunlight_active = TRUE + //set the timer to countdown daytime now. + time_til_cycle = TIME_VAMPIRE_DAY + warn_daylight( + danger_level = DANGER_LEVEL_SOL_ROSE, + vampire_warning_message = span_danger("Solar flares bombard the station with deadly UV light! Stay in cover for the next [TIME_VAMPIRE_DAY / 60] minute\s!"), + vassal_warning_message = span_danger("Solar flares bombard the station with UV light!"), + ) + +/datum/controller/subsystem/sol/proc/warn_daylight(danger_level, vampire_warning_message, vassal_warning_message) + SEND_SIGNAL(src, COMSIG_SOL_WARNING_GIVEN, danger_level, vampire_warning_message, vassal_warning_message) diff --git a/modular_iris/bubber_ports/code/controllers/subsystem/processing/sol_subsystem.dm b/modular_iris/bubber_ports/code/controllers/subsystem/processing/sol_subsystem.dm deleted file mode 100644 index a508cc174668..000000000000 --- a/modular_iris/bubber_ports/code/controllers/subsystem/processing/sol_subsystem.dm +++ /dev/null @@ -1,127 +0,0 @@ - - -PROCESSING_SUBSYSTEM_DEF(sunlight) - name = "Sol" - can_fire = FALSE - runlevels = RUNLEVEL_GAME - wait = 2 SECONDS - ss_flags = SS_NO_INIT | SS_KEEP_TIMING | SS_TICKER - - ///If the Sun is currently out our not. - var/sunlight_active = FALSE - ///The time between the next cycle, randomized every night. - var/time_til_cycle = TIME_BLOODSUCKER_NIGHT_MAX - ///If Bloodsucker levels for the night has been given out yet. - var/issued_XP = FALSE - /// Mobs that make use of the sunlight system, doesn't use weakrefs as that makes removing them a pain, and we already cleanup on qdel. - var/list/sun_sufferers = list() - -/datum/controller/subsystem/processing/sunlight/fire(resumed = FALSE) - time_til_cycle-- - if(sunlight_active) - if(time_til_cycle > 0) - SEND_SIGNAL(src, COMSIG_SOL_RISE_TICK) - if(!issued_XP && time_til_cycle <= 15) - issued_XP = TRUE - SEND_SIGNAL(src, COMSIG_SOL_RANKUP_BLOODSUCKERS) - if(time_til_cycle <= 1) - sunlight_active = FALSE - issued_XP = FALSE - //randomize the next sol timer - time_til_cycle = rand(TIME_BLOODSUCKER_NIGHT_MIN, TIME_BLOODSUCKER_NIGHT_MAX) - message_admins("BLOODSUCKER NOTICE: Daylight Ended. Resetting to Night (Lasts for [time_til_cycle / 60] minutes.") - SEND_SIGNAL(src, COMSIG_SOL_END) - warn_daylight( - danger_level = DANGER_LEVEL_SOL_ENDED, - vampire_warning_message = span_announce("The solar flare has ended, and the daylight danger has passed... for now."), - ghoul_warning_message = span_announce("The solar flare has ended, and the daylight danger has passed... for now."), - ) - return ..() - - switch(time_til_cycle) - if(TIME_BLOODSUCKER_DAY_WARN) - SEND_SIGNAL(src, COMSIG_SOL_NEAR_START) - warn_daylight( - danger_level = DANGER_LEVEL_FIRST_WARNING, - vampire_warning_message = span_danger("Solar Flares will bombard the station with dangerous UV radiation in [TIME_BLOODSUCKER_DAY_WARN / 60] minutes. Prepare to seek cover in a coffin or closet."), - ) - if(TIME_BLOODSUCKER_DAY_FINAL_WARN) - message_admins("BLOODSUCKER NOTICE: Daylight beginning in [TIME_BLOODSUCKER_DAY_FINAL_WARN] seconds.)") - warn_daylight( - danger_level = DANGER_LEVEL_SECOND_WARNING, - vampire_warning_message = span_userdanger("Solar Flares are about to bombard the station! You have [TIME_BLOODSUCKER_DAY_FINAL_WARN] seconds to find cover!"), - ghoul_warning_message = span_danger("In [TIME_BLOODSUCKER_DAY_FINAL_WARN] seconds, your master will be at risk of a Solar Flare. Make sure they find cover!"), - ) - if(TIME_BLOODSUCKER_BURN_INTERVAL) - warn_daylight( - danger_level = DANGER_LEVEL_THIRD_WARNING, - vampire_warning_message = span_userdanger("Seek cover, for Sol rises!"), - ) - if(NONE) - sunlight_active = TRUE - //set the timer to countdown daytime now. - time_til_cycle = TIME_BLOODSUCKER_DAY - message_admins("BLOODSUCKER NOTICE: Daylight Beginning (Lasts for [TIME_BLOODSUCKER_DAY / 60] minutes.)") - warn_daylight( - danger_level = DANGER_LEVEL_SOL_ROSE, - vampire_warning_message = 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!"), - ghoul_warning_message = span_userdanger("Solar flares bombard the station with UV light!"), - ) - ..() - -/datum/controller/subsystem/processing/sunlight/proc/warn_daylight(danger_level, vampire_warning_message, ghoul_warning_message) - SEND_SIGNAL(src, COMSIG_SOL_WARNING_GIVEN, danger_level, vampire_warning_message, ghoul_warning_message) - -/datum/controller/subsystem/processing/sunlight/proc/add_sun_sufferer(mob/victim) - if(is_sufferer(victim)) - return FALSE - victim.hud_used.add_screen_object(/atom/movable/screen/bloodsucker/sunlight_counter, HUD_SUNLIGHT) - RegisterSignal(victim, COMSIG_QDELETING, PROC_REF(remove_sun_sufferer), victim) - sun_sufferers[victim] = TRUE - if(length(sun_sufferers)) - can_fire = TRUE - return TRUE - -/datum/controller/subsystem/processing/sunlight/proc/signal_remove_sun_sufferer(subsystem, mob/victim) - SIGNAL_HANDLER - remove_sun_sufferer(victim) - -/datum/controller/subsystem/processing/sunlight/proc/remove_sun_sufferer(mob/victim) - if(!is_sufferer(victim)) - return FALSE - victim.hud_used.remove_screen_object(/atom/movable/screen/bloodsucker/sunlight_counter) - sun_sufferers -= victim - UnregisterSignal(victim, COMSIG_QDELETING) - if(!length(sun_sufferers)) - can_fire = FALSE - sunlight_active = initial(sunlight_active) - time_til_cycle = initial(time_til_cycle) - issued_XP = initial(issued_XP) - return TRUE - -/datum/controller/subsystem/processing/sunlight/proc/warn_notify(mob/target, danger_level, message) - if(!target) - return - to_chat(target, message) - - switch(danger_level) - if(DANGER_LEVEL_FIRST_WARNING) - target.playsound_local(null, 'modular_iris/bubber_ports/sound/bloodsucker/griffin_3.ogg', 50, TRUE) - if(DANGER_LEVEL_SECOND_WARNING) - target.playsound_local(null, 'modular_iris/bubber_ports/sound/bloodsucker/griffin_5.ogg', 50, TRUE) - if(DANGER_LEVEL_THIRD_WARNING) - target.playsound_local(null, 'sound/effects/alert.ogg', 75, TRUE) - if(DANGER_LEVEL_SOL_ROSE) - target.playsound_local(null, 'sound/ambience/misc/ambimystery.ogg', 75, TRUE) - if(DANGER_LEVEL_SOL_ENDED) - target.playsound_local(null, 'sound/music/antag/bloodcult/ghosty_wind.ogg', 90, TRUE) - -/datum/controller/subsystem/processing/sunlight/proc/is_sufferer(mob/victim) - if(!sun_sufferers) - CRASH("Sol subsystem sun_sufferers list is null, when it should never be.") - if(isnull(victim) || !length(sun_sufferers)) - return FALSE - - if(sun_sufferers[victim]) - return TRUE - return FALSE diff --git a/modular_iris/bubber_ports/code/datum/components/weatherannouncer.dm b/modular_iris/bubber_ports/code/datum/components/weatherannouncer.dm index 9b2c9a76c445..156ed950f92d 100644 --- a/modular_iris/bubber_ports/code/datum/components/weatherannouncer.dm +++ b/modular_iris/bubber_ports/code/datum/components/weatherannouncer.dm @@ -1,6 +1,6 @@ /datum/component/weather_announcer var/warning_range_low = 60 - var/warning_range_high = TIME_BLOODSUCKER_DAY_WARN + var/warning_range_high = TIME_VAMPIRE_DAY_WARN_1 /datum/component/weather_announcer/Initialize( state_normal, @@ -9,7 +9,11 @@ radar_z_trait, ) . = ..() - RegisterSignal(SSsunlight, COMSIG_SOL_NEAR_START, PROC_REF(on_daylight_warning)) + RegisterSignal(SSsol, COMSIG_SOL_NEAR_START, PROC_REF(on_daylight_warning)) + +/datum/component/weather_announcer/Destroy(force) + UnregisterSignal(SSsol, COMSIG_SOL_NEAR_START) + return ..() /datum/component/weather_announcer/proc/on_daylight_warning(datum/controller/subsystem/processing/sunlight) var/variability = rand(warning_range_low, warning_range_high) @@ -18,8 +22,8 @@ /datum/component/weather_announcer/proc/warn_user() var/list/messages = list( "Detecting solar activity. Seek cover if vulnerable.", - "Warning: Solar flares detected incoming within [TIME_BLOODSUCKER_DAY_WARN] seconds.", - "Solar activity imminent within [TIME_BLOODSUCKER_DAY_WARN] seconds. Please find shelter.", + "Warning: Solar flares detected incoming within [TIME_VAMPIRE_DAY_WARN_1] seconds.", + "Solar activity imminent within [TIME_VAMPIRE_DAY_WARN_1] seconds. Please find shelter.", ) var/atom/movable/speaker = parent speaker?.say(pick(messages)) diff --git a/modular_iris/bubber_ports/code/datums/quirks/negative_quirks/sol_weakness.dm b/modular_iris/bubber_ports/code/datums/quirks/negative_quirks/sol_weakness.dm index 6f73df595f08..591974f8c51a 100644 --- a/modular_iris/bubber_ports/code/datums/quirks/negative_quirks/sol_weakness.dm +++ b/modular_iris/bubber_ports/code/datums/quirks/negative_quirks/sol_weakness.dm @@ -17,20 +17,19 @@ quirk_flags = QUIRK_HIDE_FROM_SCAN | QUIRK_HUMAN_ONLY COOLDOWN_DECLARE(sun_burn) -/datum/quirk/sol_weakness/add_to_holder(mob/living/new_holder, quirk_transfer = FALSE, client/client_source, unique = TRUE, announce = TRUE) - return ..() - /datum/quirk/sol_weakness/add() + RegisterSignal(SSsol, COMSIG_SOL_RISE_TICK, PROC_REF(sun_risen)) + RegisterSignal(SSsol, COMSIG_SOL_WARNING_GIVEN, PROC_REF(sun_warning)) RegisterSignal(quirk_holder, COMSIG_MOB_HEMO_BLOOD_REGEN_TICK, PROC_REF(on_blood_healing)) if(!quirk_holder.hud_used) RegisterSignal(quirk_holder, COMSIG_MOB_HUD_CREATED, PROC_REF(add_sun_timer_hud)) - return - add_sun_timer_hud() + else + add_sun_timer_hud() /datum/quirk/sol_weakness/remove() + remove_sun_timer_hud() UnregisterSignal(quirk_holder, COMSIG_MOB_HEMO_BLOOD_REGEN_TICK) - SSsunlight.remove_sun_sufferer(quirk_holder) - UnregisterSignal(SSsunlight, list(COMSIG_SOL_RISE_TICK, COMSIG_SOL_WARNING_GIVEN)) + UnregisterSignal(SSsol, list(COMSIG_SOL_RISE_TICK, COMSIG_SOL_WARNING_GIVEN)) /datum/quirk/sol_weakness/proc/on_blood_healing(mob/living/owner, seconds_between_ticks, datum/status_effect/blood_regen_active/effect) if(effect && in_coffin()) @@ -39,15 +38,14 @@ else effect.cost_blood = initial(effect.cost_blood) // prevent healing if sol is active - return SSsunlight.sunlight_active ? COMSIG_CANCEL_MOB_HEMO_BLOOD_REGEN : NONE + return SSsol.sunlight_active ? COMSIG_CANCEL_MOB_HEMO_BLOOD_REGEN : NONE /datum/quirk/sol_weakness/proc/add_sun_timer_hud() - if(!quirk_holder.hud_used) - CRASH("Sol Weakness quirk holder has no HUD") - SSsunlight.add_sun_sufferer(quirk_holder) + quirk_holder.hud_used?.add_screen_object(/atom/movable/screen/vampire/sunlight_counter, HUD_VAMPIRE_SUNLIGHT, HUD_GROUP_INFO, update_screen = TRUE) + +/datum/quirk/sol_weakness/proc/remove_sun_timer_hud() UnregisterSignal(quirk_holder, COMSIG_MOB_HUD_CREATED) - RegisterSignal(SSsunlight, COMSIG_SOL_RISE_TICK, PROC_REF(sun_risen)) - RegisterSignal(SSsunlight, COMSIG_SOL_WARNING_GIVEN, PROC_REF(sun_warning)) + quirk_holder.hud_used?.remove_screen_object(HUD_VAMPIRE_SUNLIGHT) /datum/quirk/sol_weakness/proc/sun_risen() SIGNAL_HANDLER @@ -81,11 +79,22 @@ to_chat(quirk_holder, text) COOLDOWN_START(src, sun_burn, 30 SECONDS) -/datum/quirk/sol_weakness/proc/sun_warning(atom/source, danger_level, vampire_warning_message, ghoul_warning_message) +/datum/quirk/sol_weakness/proc/sun_warning(atom/source, danger_level, vampire_warning_message, vassal_warning_message) SIGNAL_HANDLER - if(danger_level == DANGER_LEVEL_SOL_ROSE) - vampire_warning_message = span_userdanger("Solar flares bombard the station with deadly UV light! Stay in cover for the next [TIME_BLOODSUCKER_DAY / 60] minutes or risk death!") - SSsunlight.warn_notify(quirk_holder, danger_level, vampire_warning_message) + if(!vampire_warning_message) + return + to_chat(quirk_holder, vampire_warning_message, type = MESSAGE_TYPE_WARNING) + switch(danger_level) + if(DANGER_LEVEL_FIRST_WARNING) + quirk_holder.playsound_local(null, 'modular_oculis/modules/vampires/sound/griffin_3.ogg', 50, TRUE) + if(DANGER_LEVEL_SECOND_WARNING) + quirk_holder.playsound_local(null, 'modular_oculis/modules/vampires/sound/griffin_5.ogg', 50, TRUE) + if(DANGER_LEVEL_THIRD_WARNING) + quirk_holder.playsound_local(null, 'sound/effects/alert.ogg', 75, TRUE) + if(DANGER_LEVEL_SOL_ROSE) + quirk_holder.playsound_local(null, 'sound/ambience/misc/ambimystery.ogg', 75, TRUE) + if(DANGER_LEVEL_SOL_ENDED) + quirk_holder.playsound_local(null, 'sound/music/antag/bloodcult/ghosty_wind.ogg', 90, TRUE) /datum/quirk/sol_weakness/proc/in_coffin() return istype(quirk_holder.loc, /obj/structure/closet/crate/coffin) diff --git a/modular_oculis/master_files/code/code/modules/client/client_colour.dm b/modular_oculis/master_files/code/code/modules/client/client_colour.dm new file mode 100644 index 000000000000..432a87671cf2 --- /dev/null +++ b/modular_oculis/master_files/code/code/modules/client/client_colour.dm @@ -0,0 +1,2 @@ +/datum/client_colour/glass_colour/pink + color = "#ffcfe9" diff --git a/modular_oculis/master_files/code/datums/elements/art.dm b/modular_oculis/master_files/code/datums/elements/art.dm new file mode 100644 index 000000000000..f7b8595dcc83 --- /dev/null +++ b/modular_oculis/master_files/code/datums/elements/art.dm @@ -0,0 +1,3 @@ +/datum/element/art/apply_moodlet(atom/source, mob/living/user, impress) + . = ..() + SEND_SIGNAL(user, COMSIG_LIVING_APPRAISE_ART, source) diff --git a/modular_oculis/master_files/code/game/objects/structures/crates_lockers/crates.dm b/modular_oculis/master_files/code/game/objects/structures/crates_lockers/crates.dm new file mode 100644 index 000000000000..fa65a82e698f --- /dev/null +++ b/modular_oculis/master_files/code/game/objects/structures/crates_lockers/crates.dm @@ -0,0 +1,170 @@ +/obj/structure/closet/crate/coffin + /// The vampire owner of this coffin. + var/datum/mind/resident + /// The time it takes to pry this open with a crowbar. + var/pry_lid_timer = 25 SECONDS + +/obj/structure/closet/crate/coffin/Destroy() + unclaim_coffin() + return ..() + +/obj/structure/closet/crate/coffin/examine(mob/user) + . = ..() + if(user.mind == 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/insertion_allowed(atom/movable/AM) + . = ..() + if(. || !isliving(AM)) + return + var/mob/living/person = AM + if(!IS_VAMPIRE(person)) // we only use the snowflake checks for vampires + return + if(person.anchored || person.buckled || person.incorporeal_move || person.has_buckled_mobs()) + return FALSE + if(horizontal && person.density) + return FALSE + // if there's nobody else in here, then we'll be allowed to sleep in here, regardless of our mob size + if(!(locate(/mob/living) in contents - person)) + return TRUE + +/obj/structure/closet/crate/coffin/can_open(mob/living/user, force) + if(!locked) + return ..() + if(user.mind == resident) + if(welded) + welded = FALSE + update_appearance(UPDATE_ICON) + locked = FALSE + return TRUE + playsound(src, 'modular_oculis/modules/vampires/sound/door_locked.ogg', vol = 20, vary = TRUE) + to_chat(user, span_notice("[src] appears to be locked tight from the inside.")) + return FALSE + +/obj/structure/closet/crate/coffin/after_close(mob/living/user, force) + if(!user || user.loc != src) + return + var/datum/antagonist/vampire/vampire = IS_VAMPIRE(user) + if(!vampire) + return + if(!vampire.coffin && !resident) + switch(tgui_alert(user, "Do you wish to claim this as your coffin? [get_area(src)] will be your haven.", "Claim Haven", list("Yes", "No"))) + if("Yes") + claim_coffin(user) + if("No") + return + lock_me(user) + + INVOKE_ASYNC(vampire, TYPE_PROC_REF(/datum/antagonist/vampire, rank_up_if_goal)) + + // You're in a Coffin, everything else is done, you're likely here to heal. Let's offer them the opportunity to do so. + vampire.check_begin_torpor() + +/obj/structure/closet/crate/coffin/click_alt(mob/living/user) + if(!isliving(user) || !IS_VAMPIRE(user)) + return NONE + if(user.loc == src) + return lock_me(user) ? CLICK_ACTION_SUCCESS : CLICK_ACTION_BLOCKING + if(user.mind == resident && user.Adjacent(src)) + balloon_alert(user, "unclaim coffin?") + var/list/unclaim_options = list( + "Yes" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_yes"), + "No" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_no") + ) + var/unclaim_response = show_radial_menu(user, src, unclaim_options, radius = 36, require_near = TRUE) + if(unclaim_response == "Yes") + unclaim_coffin(TRUE) + return CLICK_ACTION_SUCCESS + return NONE + +/obj/structure/closet/crate/coffin/relaymove(mob/living/user, direction) + if(user.stat == CONSCIOUS && user.mind == resident && !user.resting && !opened) + open(user) + return + return ..() + +/obj/structure/closet/crate/coffin/container_resist_act(mob/living/user, loc_required) + if(user.stat == CONSCIOUS && user.mind == resident && !opened) + open(user) + return + return ..() + +/obj/structure/closet/crate/coffin/crowbar_act(mob/living/user, obj/item/tool) + if(user.combat_mode || !locked) + return FALSE + user.visible_message( + span_notice("[user] tries to pry the lid off of [src] with [tool]."), + span_notice("You begin prying the lid off of [src] with [tool]."), + ) + if(!tool.use_tool(src, user, pry_lid_timer)) + return FALSE + bust_open() + user.visible_message( + span_notice("[user] snaps the door of [src] wide open."), + span_notice("The door of [src] snaps open."), + ) + return TRUE + +/obj/structure/closet/crate/coffin/wrench_act_secondary(mob/living/user, obj/item/tool) + if(!resident) + return ..() + to_chat(user, span_danger("The coffin won't detach from the floor.[user.mind == resident ? " You can Alt-Click to unclaim and unwrench your Coffin." : ""]")) + return TRUE + +/obj/structure/closet/crate/coffin/proc/claim_coffin(mob/living/claimer) + var/datum/antagonist/vampire/vampiredatum = IS_VAMPIRE(claimer) + if(vampiredatum.claim_coffin(src)) + resident = claimer.mind + set_anchored(TRUE) + +/obj/structure/closet/crate/coffin/proc/unclaim_coffin(manual = FALSE) + if(!resident) + return + + // Unanchor it (If it hasn't been broken, anyway) + if(!QDELETED(src)) + set_anchored(FALSE) + + // Unclaiming + var/datum/antagonist/vampire/vampiredatum = resident.has_antag_datum(/datum/antagonist/vampire) + if(vampiredatum?.coffin == src) + vampiredatum.coffin = null + vampiredatum.vampire_haven_area = null + for(var/datum/action/cooldown/vampire/gohome/gohome in vampiredatum.powers) + vampiredatum.remove_power(gohome) + + for(var/obj/structure/vampire/vampire_structure in get_area(src)) + if(vampire_structure.owner == resident) + vampire_structure.unbolt() + + if(manual) + to_chat(resident.current, span_cult_italic("You have unclaimed your coffin! This also unclaims all your other Vampire structures!")) + else + to_chat(resident.current, span_cult_italic("You sense that the link with your coffin and your haven has been broken! You will need to seek another.")) + + // Remove resident. Because this objec (GC?) we need to give them a way to see they don't have a home anymore. + resident = null + +/obj/structure/closet/crate/coffin/proc/lock_me(mob/user, in_locked = TRUE) + if(user.mind != resident) + return FALSE + if(!broken) + locked = in_locked + if(locked) + to_chat(user, span_notice("You flip a secret latch and lock yourself inside [src].")) + else + to_chat(user, span_notice("You flip a secret latch and unlock [src].")) + return TRUE + + // Broken? Let's fix it. + to_chat(user, span_notice("The secret latch that would lock [src] from the inside is broken. You set it back into place...")) + if(!do_after(user, 5 SECONDS, src)) + to_chat(user, span_notice("You fail to fix [src]'s mechanism.")) + return TRUE + to_chat(user, span_notice("You fix the mechanism and lock it.")) + broken = FALSE + locked = TRUE + return TRUE diff --git a/modular_oculis/master_files/code/modules/jobs/job_types/curator.dm b/modular_oculis/master_files/code/modules/jobs/job_types/curator.dm new file mode 100644 index 000000000000..a0a078d062ed --- /dev/null +++ b/modular_oculis/master_files/code/modules/jobs/job_types/curator.dm @@ -0,0 +1,5 @@ +/datum/outfit/job/curator/pre_equip(mob/living/carbon/human/H, visuals_only = FALSE) + if(visuals_only) + return ..() + backpack_contents[/obj/item/book/kindred] = 1 + return ..() diff --git a/modular_oculis/master_files/code/modules/reagents/reagent_containers/blood_pack.dm b/modular_oculis/master_files/code/modules/reagents/reagent_containers/blood_pack.dm new file mode 100644 index 000000000000..5175ec23749d --- /dev/null +++ b/modular_oculis/master_files/code/modules/reagents/reagent_containers/blood_pack.dm @@ -0,0 +1,39 @@ +/obj/item/reagent_containers/blood/attack(mob/living/victim, mob/living/attacker, params) + if(!can_drink(victim, attacker)) + return + + var/to_feed = reagents.total_volume + if(victim != attacker) + if(!do_after(attacker, 5 SECONDS, victim, hidden = TRUE)) + return + attacker.visible_message( + span_notice("[attacker] forces [victim] to drink from \the [src]."), + span_notice("You put \the [src] up to [victim]'s mouth.") + ) + reagents.trans_to(victim, to_feed, transferred_by = attacker, methods = INGEST) + playsound(victim, 'sound/items/drink.ogg', vol = 30, vary = TRUE) + return TRUE + + attacker.visible_message( + span_notice("[victim] puts \the [src] up to [victim.p_their()] mouth."), + span_notice("You put \the [src] up to your mouth.") + ) + + if(!do_after(victim, 5 SECONDS, victim, timed_action_flags = IGNORE_USER_LOC_CHANGE, extra_checks = CALLBACK(src, PROC_REF(can_drink), victim, attacker), hidden = TRUE)) + return + + victim.visible_message( + span_notice("[victim] sucks the contents out of \the [src]!"), + span_notice("You feed from \the [src].") + ) + reagents.trans_to(victim, to_feed, transferred_by = attacker, methods = INGEST) + playsound(victim, 'sound/items/drink.ogg', vol = 30, vary = TRUE) + return TRUE + +/obj/item/reagent_containers/blood/proc/can_drink(mob/living/victim, mob/living/attacker) + if(!canconsume(victim, attacker)) + return FALSE + if(!reagents?.total_volume) + to_chat(victim, span_warning("[src] is empty!")) + return FALSE + return TRUE diff --git a/modular_oculis/modules/vampires/code/clans/_clan.dm b/modular_oculis/modules/vampires/code/clans/_clan.dm new file mode 100644 index 000000000000..26804d9cf815 --- /dev/null +++ b/modular_oculis/modules/vampires/code/clans/_clan.dm @@ -0,0 +1,234 @@ +/** + * Vampire clans + * + * Handles everything related to clans. + * The entire idea of datumizing this came to me in a dream. + */ +/datum/vampire_clan + /// The name of the clan we're in. + var/name = "Caitiff" + /// Description of what the clan is, given when joining and through your antag UI. + var/description = "Vile thinblooded mongrel. Choose a clan or die like the freak you are." + /// Description shown when trying to join the clan. + var/join_description + + /// The vampire datum that owns this clan. Use this over 'source', because while it's the same thing, this is more consistent (and used for deletion). + var/datum/antagonist/vampire/vampiredatum + + /// The icon of this clan on the selection radial menu. + var/join_icon = 'modular_oculis/modules/vampires/icons/clan_icons.dmi' + var/join_icon_state = "base" + + /// Whether the clan can be joined by players. FALSE for flavortext-only clans. + var/joinable_clan = FALSE + + /// How we will drink blood using Feed. + var/blood_drink_type = VAMPIRE_DRINK_NORMAL + + // Societee + var/is_sabbat = FALSE // In case we want a bad guy clan that doesn't care about the masquerade. + var/princely_score_bonus = -10 // Will be added to playtime in get_princely_score() + + /// Unique antag HUD icon of this clan, if any. + var/antag_hud_icon + +/** + * Starting Humanity score, some clans are closer to the beast, some closer to humanity. + * We start out at null and set it in new because we want a fall back to the global default if none is set. + * 10 Saintly Toreador + * 9 Compassionate Ventrue + * 8 Caring Malkavian, Brujah + * 7 Normal Tremere + * 6 Distant + * 5 Removed + * 4 Unfeeling + * 3 Cold + * 2 Bestial + * 1 Horrific + * 0 Wight + */ + var/default_humanity + +/datum/vampire_clan/New(datum/antagonist/vampire/owner_datum) + . = ..() + RegisterSignal(SSdcs, COMSIG_VAMPIRE_BROKE_MASQUERADE, PROC_REF(on_vampire_broke_masquerade)) + + vampiredatum = owner_datum + // Apply clan-specific default humanity; fall back to the global default only if none was set. + if(isnull(default_humanity)) + default_humanity = VAMPIRE_DEFAULT_HUMANITY + vampiredatum.adjust_humanity(default_humanity - VAMPIRE_DEFAULT_HUMANITY, TRUE) + + // Masquerade breakers + for(var/datum/antagonist/vampire/unmasked in GLOB.masquerade_breakers) + if(unmasked.owner.current) + on_vampire_broke_masquerade(vampiredatum.owner.current, unmasked) + + vampiredatum.owner.current.playsound_local(get_turf(vampiredatum.owner.current), 'modular_oculis/modules/vampires/sound/VampireAlert.ogg', 80, FALSE, pressure_affected = FALSE, use_reverb = FALSE) + to_chat(vampiredatum.owner.current, span_narsiesmall("I remember now. I belong with the [name]...")) + + vampiredatum.update_static_data_for_all_viewers() + + log_uplink("[key_name(vampiredatum.owner.current)] has joined the [name].") + +/datum/vampire_clan/proc/on_apply() + for(var/datum/discipline/disciple as anything in vampiredatum.owned_disciplines) + disciple.apply_discipline_quirks(vampiredatum) + + for(var/datum/action/cooldown/vampire/clanselect/clanselect in vampiredatum.powers) + vampiredatum.remove_power(clanselect) + + if(!QDELETED(vampiredatum.owner?.current)) + apply_effects(vampiredatum.owner.current) + +/datum/vampire_clan/proc/apply_effects(mob/living/body) + return + +/datum/vampire_clan/proc/remove_effects(mob/living/body) + return + +/datum/vampire_clan/Destroy(force) + if(!QDELETED(vampiredatum?.owner?.current)) + remove_effects(vampiredatum.owner.current) + vampiredatum = null + UnregisterSignal(SSdcs, COMSIG_VAMPIRE_BROKE_MASQUERADE) + return ..() + +/** + * Called when a Vampire exits Torpor + */ +/datum/vampire_clan/proc/on_exit_torpor() + return + +/** + * Called during Vampire's life_tick + */ +/datum/vampire_clan/proc/handle_clan_life() + if(!is_type_in_list(/datum/action/cooldown/vampire/levelup, vampiredatum.powers) && vampiredatum.vampire_level_unspent > 0) + vampiredatum.grant_power(new /datum/action/cooldown/vampire/levelup) + +/** + * Called when a Vampire successfully vassalizes someone via the persuasion rack. + * Do not call this on [/datum/antagonist/vampire/proc/make_vassal()] !!! + */ +/datum/vampire_clan/proc/on_vassal_made(mob/living/living_vampire, mob/living/living_vassal) + living_vampire.playsound_local(null, 'sound/effects/singlebeat.ogg', 70, TRUE) + + living_vassal.playsound_local(null, 'sound/effects/singlebeat.ogg', 70, TRUE) + living_vassal.set_jitter_if_lower(30 SECONDS) + living_vassal.emote("laugh") + +/** + * Called when we level up inside a coffin. + */ + + /** + * For every discipline in clan_disciplines we do: + * if the next level returns anything but null, we add it to the options + * /// + * Then we display the radial with the options. + * Picking a choice will do the following: + * Remove all powers from the discipline's current level, by: + * for every power in get_abilities_with_level(current level) > remove + * increase discipline level + * for every power in get_abilities_with_level(current level) > add + */ +/datum/vampire_clan/proc/spend_rank(mob/living/carbon/carbon_vampire) + if(QDELETED(vampiredatum.owner?.current) || vampiredatum.vampire_level_unspent <= 0) + return + + // Generate radial menu + var/list/options = list() + var/list/radial_display = list() + + for(var/datum/discipline/discipline as anything in vampiredatum.owned_disciplines) // We do owned_disciplines, not clan_disciplines. clan_disciplines is used to populate owned_disciplines. + if(discipline.get_abilities_with_level("next")) + options[discipline.name] = discipline + var/datum/radial_menu_choice/option = new + option.image = image(icon = 'modular_oculis/modules/vampires/icons/disciplines.dmi', icon_state = discipline.icon_state) + option.info = "[span_boldnotice(discipline.name)]\n[span_cult(discipline.discipline_explanation)]" + radial_display[initial(discipline.name)] = option + + var/mob/living/living_vampire = vampiredatum.owner.current + + // Show radial menu + if(!length(options)) + to_chat(living_vampire, span_notice("You grow more familiar with your powers!")) + else + to_chat(living_vampire, span_notice("You have the opportunity to grow your expertise. Select a discipline to advance your Rank.")) + + // If we're in a closet, anchor the radial menu to it. If not, anchor it to the vampire body + var/datum/discipline/discipline_response + + if(istype(living_vampire.loc, /obj/structure/closet)) + var/obj/structure/closet/container = living_vampire.loc + discipline_response = show_radial_menu(living_vampire, container, radial_display) + else + discipline_response = show_radial_menu(living_vampire, living_vampire, radial_display) + + var/datum/discipline/chosen_discipline + + for(var/datum/discipline/discipline as anything in vampiredatum.owned_disciplines) + if(discipline.name == discipline_response) + chosen_discipline = discipline + break + + if(isnull(discipline_response) || QDELETED(src) || QDELETED(living_vampire)) + return FALSE + + // Remove all current powers + for(var/datum/action/cooldown/vampire/power_old as anything in vampiredatum.powers) + if(is_type_in_list(power_old, chosen_discipline.get_abilities_with_level("current"))) + vampiredatum.remove_power(power_old) + + // increment level + chosen_discipline.level_up() + + // add all current powers (of the new level) + for(var/datum/action/cooldown/vampire/power_new as anything in chosen_discipline.get_abilities_with_level("current")) + vampiredatum.grant_power(new power_new) + + living_vampire.balloon_alert(living_vampire, "learned [discipline_response] level [chosen_discipline.level - 1]!") + to_chat(living_vampire, span_notice("You have learned how to use [discipline_response]!")) + + finalize_spend_rank() + + // QoL + if(vampiredatum.vampire_level_unspent > 0) + spend_rank(carbon_vampire) + +/datum/vampire_clan/proc/finalize_spend_rank() + // Level up the vampire + vampiredatum.vampire_regen_rate += 0.05 + vampiredatum.max_vitae += 100 + + /* if(ishuman(vampiredatum.owner.current)) + var/mob/living/carbon/human/vampire_human = vampiredatum.owner.current + vampire_human.dna.species.punchdamage += 0.5 */ + + // We're almost done - Spend your Rank now. + vampiredatum.vampire_level++ + vampiredatum.vampire_level_unspent-- + + // Flavor + to_chat(vampiredatum.owner.current, span_notice("You are now a rank [vampiredatum.vampire_level] Vampire. \ + Your strength, health, feed rate, regen rate, and maximum blood capacity have all increased! \n\ + * Your existing powers have all ranked up as well!")) + vampiredatum.update_hud() + +/datum/vampire_clan/proc/on_vampire_broke_masquerade(datum/source, datum/antagonist/vampire/masquerade_breaker) + SIGNAL_HANDLER + + if(masquerade_breaker == vampiredatum) + return + + var/breaker_name = masquerade_breaker.owner.name || masquerade_breaker.owner.current.real_name || masquerade_breaker.owner.current.name + to_chat(vampiredatum.owner.current, span_userdanger("[breaker_name] has broken the Masquerade! We must destroy them at all costs, for the good of all kindred!\n\ + (Hint: You may feed on a vampire that has broken the masquerade to steal their powers.)")) + var/datum/objective/assassinate/masquerade_objective = new() + masquerade_objective.target = masquerade_breaker.owner + masquerade_objective.name = "Masquerade Objective" + masquerade_objective.explanation_text = "Ensure [breaker_name], who has broken the Masquerade, succumbs to Final Death." + vampiredatum.objectives += masquerade_objective + vampiredatum.owner.announce_objectives() + vampiredatum.update_static_data_for_all_viewers() diff --git a/modular_oculis/modules/vampires/code/clans/assignclan.dm b/modular_oculis/modules/vampires/code/clans/assignclan.dm new file mode 100644 index 000000000000..cfd3162ec107 --- /dev/null +++ b/modular_oculis/modules/vampires/code/clans/assignclan.dm @@ -0,0 +1,43 @@ +/** + * Gives Vampires the ability to choose a Clan. + * If they are already in a Clan, or is in a Frenzy, they will not be able to do so. + * The arg is optional and should really only be an Admin setting a Clan for a player. + * If set however, it will give them the control of their Clan instead of the Vampire. + * This is selected through a radial menu over the player's body, even when an Admin is setting it. + * Args: + * person_selecting - Mob override for stuff like Admins selecting someone's clan. + */ +/datum/antagonist/vampire/proc/assign_clan_and_bane() + if(my_clan || HAS_TRAIT(owner.current, TRAIT_FRENZY)) + return + + var/list/options = list() + var/list/radial_display = list() + for(var/datum/vampire_clan/all_clans as anything in typesof(/datum/vampire_clan)) + if(!initial(all_clans.joinable_clan)) //flavortext only + continue + + options[initial(all_clans.name)] = all_clans + + var/datum/radial_menu_choice/option = new + option.image = image(icon = initial(all_clans.join_icon), icon_state = initial(all_clans.join_icon_state)) + option.info = "[span_boldnotice(initial(all_clans.name))]\n[span_cult(get_clan_description(all_clans.name))]" + radial_display[initial(all_clans.name)] = option + + var/chosen_clan + if(istype(owner.current.loc, /obj/structure/closet)) + var/obj/structure/closet/container = owner.current.loc + chosen_clan = show_radial_menu(owner.current, container, radial_display, radius = 45) + else + chosen_clan = show_radial_menu(owner.current, owner.current, radial_display, radius = 45) + + chosen_clan = options[chosen_clan] + + if(QDELETED(src) || QDELETED(owner.current) || !chosen_clan) + return FALSE + + my_clan = new chosen_clan(src) + my_clan.on_apply() + if(my_clan.antag_hud_icon) // regenerate their HUD if this clan has different icon + add_team_hud(owner.current) + SEND_SIGNAL(owner, COMSIG_VAMPIRE_CLAN_CHOSEN, src, my_clan) diff --git a/modular_oculis/modules/vampires/code/clans/brujah.dm b/modular_oculis/modules/vampires/code/clans/brujah.dm new file mode 100644 index 000000000000..52823cce3a92 --- /dev/null +++ b/modular_oculis/modules/vampires/code/clans/brujah.dm @@ -0,0 +1,16 @@ +/datum/vampire_clan/brujah + name = CLAN_BRUJAH + description = "Mostly independent of the Camarilla's strictures, the Brujah prefer their own councils and street courts over princely salons.
\ + They are a fallen clan, a people who have slid from warrior-scholars into fierce, argumentative rebels. Yet the embers of discipline and wisdom still glow beneath the rage.

\ + At the same time, many Brujah are pragmatic. They respect competence, reward power, and will accept arrangements that let them keep their autonomy while serving a purpose. For the right price, leverage, or chance to settle scores, princes were known recruit Brujah as scourges or enforcers, so long as those Brujah retain visible independence." + join_icon_state = "brujah" + default_humanity = 8 + princely_score_bonus = 2 + joinable_clan = TRUE + antag_hud_icon = "brujah" + +/datum/vampire_clan/brujah/New(datum/antagonist/vampire/owner_datum) + . = ..() + vampiredatum.owned_disciplines += new /datum/discipline/celerity(vampiredatum) + vampiredatum.owned_disciplines += new /datum/discipline/potence/brujah(vampiredatum) + vampiredatum.owned_disciplines += new /datum/discipline/presence(vampiredatum) diff --git a/modular_oculis/modules/vampires/code/clans/debug.dm b/modular_oculis/modules/vampires/code/clans/debug.dm new file mode 100644 index 000000000000..942ae3cd2bf2 --- /dev/null +++ b/modular_oculis/modules/vampires/code/clans/debug.dm @@ -0,0 +1,20 @@ +#ifdef VAMPIRE_TESTING +// debug clan with every discipline +/datum/vampire_clan/debug + name = "Debug Clan" + description = "wtf you shouldn't be seeing this outside of testing" + default_humanity = 10 + princely_score_bonus = 99 + joinable_clan = TRUE + +/datum/vampire_clan/debug/New(datum/antagonist/vampire/owner_datum) + . = ..() + vampiredatum.vampire_level_unspent = 35 + vampiredatum.owned_disciplines += new /datum/discipline/auspex/malkavian(vampiredatum) + vampiredatum.owned_disciplines += new /datum/discipline/celerity(vampiredatum) + vampiredatum.owned_disciplines += new /datum/discipline/dominate/ventrue(vampiredatum) + vampiredatum.owned_disciplines += new /datum/discipline/fortitude(vampiredatum) + vampiredatum.owned_disciplines += new /datum/discipline/potence/brujah(vampiredatum) + vampiredatum.owned_disciplines += new /datum/discipline/presence(vampiredatum) + vampiredatum.owned_disciplines += new /datum/discipline/thaumaturgy(vampiredatum) +#endif diff --git a/modular_oculis/modules/vampires/code/clans/flavortext_clans.dm b/modular_oculis/modules/vampires/code/clans/flavortext_clans.dm new file mode 100644 index 000000000000..ad7b3016b8aa --- /dev/null +++ b/modular_oculis/modules/vampires/code/clans/flavortext_clans.dm @@ -0,0 +1,30 @@ +// These have no functionality. They're just flavortext for the Archive of the Kindred +/datum/vampire_clan/tzimisce + name = CLAN_TZIMISCE + description = "The Tzimisce Clan has no knowledge about it. \n\ + If you see one, you should probably run away.\n\ + *the rest of the page is full of undecipherable scribbles...*" + joinable_clan = FALSE + +/datum/vampire_clan/hecata + name = CLAN_HECATA + description = "This Clan is composed of curious practioners of dark magic who enjoy toying with the dead. \n\ + Often compared to the Lasombra, they sometimes act in similar ways and draw power from the void. \n\ + However, they are also very different, and place an emphasis on creating zombie like puppets from the dead. \n\ + They are able to raise the dead as temporary vassals, permanently revive dead vassals, communicate to their vassals from afar, and summon wraiths." + joinable_clan = FALSE + +/datum/vampire_clan/lasombra + name = CLAN_LASOMBRA + description = "This Clan seems to adore living in the Shadows, worshipping it's secrets. \n\ + They take their research and vanity seriously, they are always very proud of themselves after even minor achievements. \n\ + They appear to be in search of a station with a veil weakness to be able to channel their shadow's abyssal powers. \n\ + Thanks to this, they have also evolved a dark liquid in their veins, which makes them able to manipulate shadows." + joinable_clan = FALSE + +/datum/vampire_clan/nosferatu + name = CLAN_NOSFERATU + description = "The Nosferatu Clan is unable to blend in with the crew, with no abilities such as Feign Life and Veil. \n\ + Additionally, has a permanent bad back and looks like a Vampire upon a simple examine, and is entirely unidentifiable, \n\ + they can fit in the vents regardless of their form and equipment." + joinable_clan = FALSE diff --git a/modular_oculis/modules/vampires/code/clans/malkavian.dm b/modular_oculis/modules/vampires/code/clans/malkavian.dm new file mode 100644 index 000000000000..9ff5c19845c2 --- /dev/null +++ b/modular_oculis/modules/vampires/code/clans/malkavian.dm @@ -0,0 +1,62 @@ +#define REVELATION_MIN_COOLDOWN 20 SECONDS +#define REVELATION_MAX_COOLDOWN 1 MINUTES + +/datum/vampire_clan/malkavian + name = CLAN_MALKAVIAN + description = "Malkavians are the brood of Malkav and one of the great vampiric clans. They are deranged vampires, afflicted with the insanity of their Antediluvian progenitor.

\ + Members of the clan have assumed the roles of seers and oracles among Kindred and kine, eerie figures bound by strange compulsions and the ability to perceive what others cannot.

\ + They are also notorious pranksters whose 'jokes' range from silly to sadistic. Against all odds, however, the children of Malkav are among the oldest surviving vampiric lineages." + join_icon_state = "malkavian" + joinable_clan = TRUE + default_humanity = 8 + princely_score_bonus = 6 + + COOLDOWN_DECLARE(revelation_cooldown) + +/datum/vampire_clan/malkavian/New(datum/antagonist/vampire/owner_datum) + . = ..() + + vampiredatum.owned_disciplines += new /datum/discipline/auspex/malkavian(vampiredatum) + vampiredatum.owned_disciplines += new /datum/discipline/obfuscate(vampiredatum) + vampiredatum.owned_disciplines += new /datum/discipline/dominate(vampiredatum) + + vampiredatum.owner.current.playsound_local(get_turf(vampiredatum.owner.current), 'sound/music/antag/creepalert.ogg', 80, FALSE, pressure_affected = FALSE, use_reverb = FALSE) + to_chat(vampiredatum.owner.current, span_hypnophrase("Welcome, childe of Malkav...")) + +/datum/vampire_clan/malkavian/apply_effects(mob/living/body) + if(iscarbon(body)) + var/mob/living/carbon/carbon_body = body + carbon_body.gain_trauma(/datum/brain_trauma/mild/hallucinations, TRAUMA_RESILIENCE_ABSOLUTE) + carbon_body.gain_trauma(/datum/brain_trauma/special/bluespace_prophet/phobetor, TRAUMA_RESILIENCE_ABSOLUTE) + + ADD_TRAIT(body, TRAIT_XRAY_VISION, TRAIT_VAMPIRE) + body.update_sight() + +/datum/vampire_clan/malkavian/remove_effects(mob/living/body) + if(iscarbon(body)) + var/mob/living/carbon/carbon_body = body + carbon_body.cure_trauma_type(/datum/brain_trauma/mild/hallucinations, TRAUMA_RESILIENCE_ABSOLUTE) + carbon_body.cure_trauma_type(/datum/brain_trauma/special/bluespace_prophet/phobetor, TRAUMA_RESILIENCE_ABSOLUTE) + + REMOVE_TRAIT(body, TRAIT_XRAY_VISION, TRAIT_VAMPIRE) + body.update_sight() + +/datum/vampire_clan/malkavian/handle_clan_life() + . = ..() + var/mob/living/living_vampire = vampiredatum.owner.current + if(!COOLDOWN_FINISHED(src, revelation_cooldown) || HAS_TRAIT(living_vampire, TRAIT_FEIGN_LIFE) || living_vampire.stat != CONSCIOUS) + return + + if(prob(15)) + var/message = pick(strings("oculis/malkavian_revelations.json", "revelations", "strings")) + INVOKE_ASYNC(living_vampire, TYPE_PROC_REF(/atom/movable, say), message, , , , , , "Malkavian Revelation") + COOLDOWN_START(src, revelation_cooldown, rand(REVELATION_MIN_COOLDOWN, REVELATION_MAX_COOLDOWN)) + +/datum/vampire_clan/malkavian/on_exit_torpor() + var/mob/living/carbon/carbon_vampire = vampiredatum.owner.current + if(istype(carbon_vampire)) + carbon_vampire.gain_trauma(/datum/brain_trauma/mild/hallucinations, TRAUMA_RESILIENCE_ABSOLUTE) + carbon_vampire.gain_trauma(/datum/brain_trauma/special/bluespace_prophet/phobetor, TRAUMA_RESILIENCE_ABSOLUTE) + +#undef REVELATION_MAX_COOLDOWN +#undef REVELATION_MIN_COOLDOWN diff --git a/modular_oculis/modules/vampires/code/clans/toreador.dm b/modular_oculis/modules/vampires/code/clans/toreador.dm new file mode 100644 index 000000000000..5bb0f23f3aeb --- /dev/null +++ b/modular_oculis/modules/vampires/code/clans/toreador.dm @@ -0,0 +1,16 @@ +/datum/vampire_clan/toreador + name = CLAN_TOREADOR + description = "The Toreador are a clan of vampires known for being some of the most beautiful, sensual, seductive, emotional and glamorous of the Kindred.

\ + Responsible for the legends of vampires who seduce and entice their prey with beauty, love and sensuality. Famous and infamous as a clan of artists and innovators, they are one of the bastions of the Camarilla, as their very survival depends on the facades of civility and grace on which the sect prides itself.

\ + They are inherently divas by blood, and their humanity and sense of morality may plummit as fast as it rises." + join_icon_state = "toreador" + blood_drink_type = VAMPIRE_DRINK_SNOBBY + default_humanity = 10 + princely_score_bonus = 10 + joinable_clan = TRUE + +/datum/vampire_clan/toreador/New(datum/antagonist/vampire/owner_datum) + . = ..() + vampiredatum.owned_disciplines += new /datum/discipline/celerity(vampiredatum) + vampiredatum.owned_disciplines += new /datum/discipline/auspex(vampiredatum) + vampiredatum.owned_disciplines += new /datum/discipline/presence(vampiredatum) diff --git a/modular_oculis/modules/vampires/code/clans/tremere.dm b/modular_oculis/modules/vampires/code/clans/tremere.dm new file mode 100644 index 000000000000..fc663257dc58 --- /dev/null +++ b/modular_oculis/modules/vampires/code/clans/tremere.dm @@ -0,0 +1,15 @@ +/datum/vampire_clan/tremere + name = CLAN_TREMERE + description = "In the (comparatively) little time since their founding, the Tremere have made incredible inroads within vampiric society and are arguably the most powerful clan in the modern nights.

\ + This is due in no small part to their strict hierarchy, secretive nature, and mastery of Thaumaturgy, all of which elicit suspicion, fear, and respect from other Cainites.

\ + The Tremere stand as a pillar of the Camarilla and are one of its main defenders, despite the fact that they exist almost as a subsect." + join_icon_state = "tremere" + default_humanity = 7 + princely_score_bonus = 8 + joinable_clan = TRUE + +/datum/vampire_clan/tremere/New(datum/antagonist/vampire/owner_datum) + . = ..() + vampiredatum.owned_disciplines += new /datum/discipline/dominate(vampiredatum) + vampiredatum.owned_disciplines += new /datum/discipline/auspex(vampiredatum) + vampiredatum.owned_disciplines += new /datum/discipline/thaumaturgy(vampiredatum) diff --git a/modular_oculis/modules/vampires/code/clans/ventrue.dm b/modular_oculis/modules/vampires/code/clans/ventrue.dm new file mode 100644 index 000000000000..71c280d1648f --- /dev/null +++ b/modular_oculis/modules/vampires/code/clans/ventrue.dm @@ -0,0 +1,17 @@ +/datum/vampire_clan/ventrue + name = CLAN_VENTRUE + description = "The Ventrue have long been one of the proudest lines of vampires. Its members work hard to maintain a reputation for honor, genteel behavior, and leadership.

\ + A sense of noblesse oblige has long pervaded the clan, accompanied by the genuine belief that the Ventrue know what is best for everyone.

\ + They not only consider themselves the oldest clan, but see themselves as the enforcers of tradition and the rightful leaders of Kindred society. " + join_icon_state = "ventrue" + blood_drink_type = VAMPIRE_DRINK_SNOBBY + default_humanity = 9 + princely_score_bonus = 15 // IT'S OVER NIN- ten. It's over ten. + joinable_clan = TRUE + +/datum/vampire_clan/ventrue/New(datum/antagonist/vampire/owner_datum) + . = ..() + + vampiredatum.owned_disciplines += new /datum/discipline/presence(vampiredatum) + vampiredatum.owned_disciplines += new /datum/discipline/dominate/ventrue(vampiredatum) + vampiredatum.owned_disciplines += new /datum/discipline/fortitude(vampiredatum) diff --git a/modular_oculis/modules/vampires/code/controllers/leveling_vampire.dm b/modular_oculis/modules/vampires/code/controllers/leveling_vampire.dm new file mode 100644 index 000000000000..90cc5bcaceaf --- /dev/null +++ b/modular_oculis/modules/vampires/code/controllers/leveling_vampire.dm @@ -0,0 +1,25 @@ +/** + * Called every 35 minutes or so. Give our vampire a free level to spend. +**/ +/datum/antagonist/vampire/proc/give_natural_level() + if(QDELETED(owner.current) || free_levels_remaining < 1) + return + owner.current.balloon_alert(owner.current, "you have grown more ancient!") + free_levels_remaining-- + INVOKE_ASYNC(src, PROC_REF(rank_up), 1, TRUE) + +SUBSYSTEM_DEF(vampire_leveling) + name = "Vampire Leveling" + wait = 35 MINUTES + ss_flags = SS_NO_INIT | SS_KEEP_TIMING + can_fire = FALSE + +/datum/controller/subsystem/vampire_leveling/fire(resumed = FALSE) + for(var/datum/antagonist/vampire/vampire as anything in GLOB.all_vampires) + vampire.give_natural_level() + +/datum/controller/subsystem/vampire_leveling/proc/check_enable() + if(length(GLOB.all_vampires)) + can_fire = TRUE + else + can_fire = FALSE diff --git a/modular_oculis/modules/vampires/code/controllers/society.dm b/modular_oculis/modules/vampires/code/controllers/society.dm new file mode 100644 index 000000000000..dbd2565a965a --- /dev/null +++ b/modular_oculis/modules/vampires/code/controllers/society.dm @@ -0,0 +1,214 @@ +/// A global list of vampire antag datums that have broken the Masquerade +GLOBAL_LIST_EMPTY(masquerade_breakers) + +/// A global list of vampire antag datums in general +GLOBAL_LIST_EMPTY(all_vampires) + +SUBSYSTEM_DEF(vsociety) + name = "Vampire Society" + wait = 5 MINUTES + ss_flags = SS_NO_INIT | SS_BACKGROUND + can_fire = FALSE + + // Are we currently polling? + var/currently_polling = FALSE + + // Ref to the prince datum + var/datum/weakref/princedatum + + var/start_time = 0 + +/datum/controller/subsystem/vsociety/fire(resumed = FALSE) + var/time_elapsed = world.time - start_time + + // Give them some breathing room + if(time_elapsed < 9 MINUTES) + return + + if(!princedatum && !currently_polling) + for(var/datum/antagonist/vampire as anything in GLOB.all_vampires) + to_chat(vampire.owner.current, span_announce("* Vampire Tip: A vote for Prince will occur soon. If you are interested in leading your fellow kindred, read up on princes in your info panel now!")) + addtimer(CALLBACK(src, PROC_REF(poll_for_prince)), 2 MINUTES) + message_admins("Vampire society has fired, and a prince poll will occur in 2 minutes.") + log_game("Vampire society has fired, and a prince poll will occur soon.") + +/datum/controller/subsystem/vsociety/proc/poll_for_prince() + message_admins("Vampire society is now polling for a new prince.") + log_game("Vampire society is now polling for a new prince.") + + //Build a list of mobs in GLOB.all_vampires + var/list/vampire_living_candidates = list() + + for(var/datum/antagonist/vampire as anything in GLOB.all_vampires) + var/currentmob = vampire.owner?.current + + if(!isliving(currentmob)) //Are we mob/living? + continue + + var/mob/living/livingmob = currentmob + if(livingmob.health <= HEALTH_THRESHOLD_DEAD) // we check health instead of stat to avoid skipping out on vamps that are in torpor or something + continue + + vampire_living_candidates += currentmob + + currently_polling = TRUE + var/icon/prince_icon = icon('modular_oculis/modules/vampires/icons/vampiric.dmi', "prince") + prince_icon.Scale(24, 24) + var/list/pollers = SSpolling.poll_candidates( + "You are eligible for princedom.", + poll_time = 3 MINUTES, + flash_window = TRUE, + group = vampire_living_candidates, + alert_pic = image(prince_icon), + role_name_text = "Prince", + custom_response_messages = list( + POLL_RESPONSE_SIGNUP = "You have made your bid for princedom.
* Note: Princedom has certain expectations placed upon you. If you are not in a position to enforce the masquerade, consider letting someone else take this burden.", + POLL_RESPONSE_UNREGISTERED = "You have removed your bid to princedom.", + ), + amount_to_pick = length(GLOB.all_vampires), + announce_chosen = FALSE, + ) + currently_polling = FALSE + + var/datum/antagonist/vampire/chosen_datum + var/mob/living/chosen_candidate + + // We have to do this shit because the polling proc doesn't always return a list. Sometimes it just returns a mob. + var/list/candidates = list() + candidates += pollers + + for(var/mob/living/current_candidate in candidates) // Pick the ideal one from the list. + var/datum/antagonist/vampire/current_datum = IS_VAMPIRE(current_candidate) + + if(!chosen_candidate) // If we are the first in line, just be the prince by default + chosen_candidate = current_candidate + chosen_datum = IS_VAMPIRE(current_candidate) + continue + + if(current_datum.get_princely_score() >= chosen_datum.get_princely_score()) + chosen_candidate = current_candidate + chosen_datum = IS_VAMPIRE(current_candidate) + + if(chosen_datum) + chosen_datum.princify() + + +////////////////////////////////////////////////// +//////////// ON THE VAMP ANTAG DATUM ///////////// +////////////////////////////////////////////////// +/** + * Resumes society, called when someone is assigned Vampire +**/ +/datum/antagonist/vampire/proc/check_start_society() + + if(SSvsociety.can_fire) + return + + if(length(GLOB.all_vampires) >= 3) + SSvsociety.start_time = world.time + SSvsociety.can_fire = TRUE + message_admins("Vampire Society has started, as there are [length(GLOB.all_vampires)] vampires active.") + log_game("Vampire Society has started, as there are [length(GLOB.all_vampires)] vampires active.") + +/** + * Pauses society, called when someone is unassigned Vampire +**/ +/datum/antagonist/vampire/proc/check_cancel_society() + + if(!SSvsociety.can_fire) + return + + if(length(GLOB.all_vampires) < 3) + SSvsociety.can_fire = FALSE + message_admins("Vampire Society has paused, as there are only [length(GLOB.all_vampires)] vampires active.") + log_game("Vampire Society has paused, as there are only [length(GLOB.all_vampires)] vampires active.") + +/** + * Turns the player into a prince. +**/ +/datum/antagonist/vampire/proc/princify() + SSvsociety.princedatum = WEAKREF(src) + + rank_up(8, TRUE) // Rank up a lot. + to_chat(owner.current, span_cult_bold("As a true prince, you find some of your old power returning to you!")) + owner.current.playsound_local(null, 'modular_oculis/modules/vampires/sound/prince.ogg', 100, FALSE, pressure_affected = FALSE) + prince = TRUE + add_team_hud(owner.current) + + var/full_name = return_full_name() + for(var/datum/antagonist/vampire as anything in GLOB.all_vampires) + to_chat(vampire.owner.current, span_narsiesmall("[full_name], also known as [owner.name || owner.current.real_name || owner.current.name], has claimed the role of Prince!")) + + grant_power(new /datum/action/cooldown/vampire/targeted/scourgify) + + var/datum/objective/vampire/prince/prince_objective = new() + objectives += prince_objective + owner.announce_objectives() + + message_admins("[ADMIN_LOOKUP(owner.current)] has received the role of Vampire Prince. ([get_princely_score()] princely score, with [my_clan?.princely_score_bonus]/[min(50, owner.current?.client?.get_exp_living(TRUE) / 60) / 10] clan/hour bonus.)") + log_game("[key_name(owner.current)] has become the Vampire Prince. ([get_princely_score()] princely score, with [my_clan?.princely_score_bonus]/[min(50, owner.current?.client?.get_exp_living(TRUE) / 60) / 10] clan/hour bonus.)") + + notify_ghosts( + "[owner.name] has become the Vampire Prince!", + source = owner.current, + header = "bloodclan confirmed???", + notify_flags = NOTIFY_CATEGORY_NOFLASH, + ) + + update_static_data_for_all_viewers() + tgui_alert(owner.current, "Congratulations, you have been chosen for Princedom.\nPlease note that this entails a certain responsibility. Your job, now, is to keep order, and to enforce the masquerade.", "Welcome, my Prince.", list("I understand"), 30 SECONDS, TRUE) + +/** + * Turns the player into a scourge. +**/ +/datum/antagonist/vampire/proc/scourgify() + ASSERT(!prince, "Somehow a prince was going to be turned into a scourge") // Literally how would this happen. Still, just in case. + + rank_up(4, TRUE) // Rank up less. + to_chat(owner.current, span_cult_bold("As a Camarilla scourge, your newfound purpose empowers you!")) + owner.current.playsound_local(null, 'modular_oculis/modules/vampires/sound/scourge_recruit.ogg', 100, FALSE, pressure_affected = FALSE) + scourge = TRUE + add_team_hud(owner.current) + + var/datum/objective/vampire/scourge/scourge_objective = new() + objectives += scourge_objective + owner.announce_objectives() + + for(var/datum/antagonist/vampire as anything in GLOB.all_vampires) + to_chat(vampire.owner.current, span_cult_bold(span_big("Under authority of the Prince, [owner.name || owner.current.real_name || owner.current.name] has been raised to the duty of the Scourge!"))) + + message_admins("[ADMIN_LOOKUPFLW(owner.current)] has been made a Scourge of the Vampires!") + log_game("[key_name(owner.current)] has become a Scourge of the Vampires.") + + notify_ghosts( + "[owner.name] has been raised to the duty Scourge of the Vampires!", + source = owner.current, + header = "bloodclan confirmed???", + notify_flags = NOTIFY_CATEGORY_NOFLASH, + ) + + update_static_data_for_all_viewers() + +/** + * Returns the princyness of this vampire. + * get the players hours, convert it into a 10 point scale, 0-100 hours. + * get their clans default princely score. 0-10(mostly). + * Add those together. +**/ +/datum/antagonist/vampire/proc/get_princely_score() + var/calculated_hour_score = min(50, owner.current?.client?.get_exp_living(TRUE) / 60) / 10 + + var/clan_bonus = my_clan?.princely_score_bonus || -10 + + return clan_bonus + calculated_hour_score + +// We could put this in objectives but like, it's just two tiny hardcoded things. It's fine here. +/datum/objective/vampire/scourge + name = "Camarilla Scourge" + explanation_text = "Obey your prince! Ensure order! Safeguard the Masquerade!" + completed = TRUE + +/datum/objective/vampire/prince + name = "Camarilla Prince" + explanation_text = "Rule your fellow kindred with an iron fist! Ensure the sanctity of the Masquerade, at ALL costs!" + completed = TRUE diff --git a/modular_oculis/modules/vampires/code/conversion_vampire.dm b/modular_oculis/modules/vampires/code/conversion_vampire.dm new file mode 100644 index 000000000000..37219fe968f4 --- /dev/null +++ b/modular_oculis/modules/vampires/code/conversion_vampire.dm @@ -0,0 +1,63 @@ +/** + * This file contains all of the procs related to vassalizing someone + * +**/ + +/** + * Checks if the target's antag_datums contain any of the banned antags. +**/ +/datum/antagonist/vampire/proc/is_blacklisted_antag(mob/target) + for(var/datum/antagonist/antag_datum as anything in target.mind.antag_datums) + if(antag_datum.type in vassal_banned_antags) + return TRUE + return FALSE + +/** + * Checks if the person is allowed to turn into the Vampire's vassal +**/ +/datum/antagonist/vampire/proc/can_make_vassal(mob/living/conversion_target, ignore_concious_check = FALSE) + var/mob/living/living_vampire = owner.current + + if(!my_clan) + living_vampire.balloon_alert(living_vampire, "enter a clan first.") + return FALSE + + if(count_vassals() >= max_vampire_vassals()) + living_vampire.balloon_alert(living_vampire, "too many vassals.") + return FALSE + +#ifndef VAMPIRE_TESTING + if(!conversion_target.ckey) + living_vampire.balloon_alert(living_vampire, "can't be vassalized.") + return FALSE +#endif + + if(!iscarbon(conversion_target) || !conversion_target.mind || is_blacklisted_antag(conversion_target)) + living_vampire.balloon_alert(living_vampire, "can't be vassalized.") + return FALSE + + var/datum/antagonist/vassal/vassaldatum = IS_VASSAL(conversion_target) + var/mob/living/vassal_master = conversion_target.mind.enslaved_to?.resolve() + if((vassaldatum && !vassaldatum.master.broke_masquerade) || (vassal_master && vassal_master != owner.current)) + living_vampire.balloon_alert(living_vampire, "enslaved to someone else.") + return FALSE + + if(!ignore_concious_check && conversion_target.stat > UNCONSCIOUS) + living_vampire.balloon_alert(living_vampire, "must be awake.") + return FALSE + + return TRUE + +/datum/antagonist/vampire/proc/make_vassal(mob/living/conversion_target) + if(IS_VASSAL(conversion_target)) + conversion_target.mind.remove_antag_datum(/datum/antagonist/vassal) + + // Set the master, then give the datum. + var/datum/antagonist/vassal/vassaldatum = new(conversion_target.mind) + vassaldatum.master = src + conversion_target.mind.add_antag_datum(vassaldatum) + + message_admins("[ADMIN_LOOKUPFLW(conversion_target)] has become a vassal, and is enslaved to [ADMIN_LOOKUPFLW(owner.current)].") + log_admin("[key_name(conversion_target)] has become a vassal, and is enslaved to [key_name(owner.current)].") + + return TRUE diff --git a/modular_oculis/modules/vampires/code/crafting/crafting_stakes.dm b/modular_oculis/modules/vampires/code/crafting/crafting_stakes.dm new file mode 100644 index 000000000000..043e9946256d --- /dev/null +++ b/modular_oculis/modules/vampires/code/crafting/crafting_stakes.dm @@ -0,0 +1,28 @@ +/datum/crafting_recipe/stake + name = "Stake" + result = /obj/item/stake + reqs = list(/obj/item/stack/sheet/mineral/wood = 3) + time = 8 SECONDS + category = CAT_WEAPON_MELEE + crafting_flags = NONE + +/datum/crafting_recipe/hardened_stake + name = "Hardened Stake" + result = /obj/item/stake/hardened + tool_behaviors = list(TOOL_WELDER) + reqs = list(/obj/item/stack/rods = 1) + time = 6 SECONDS + category = CAT_WEAPON_MELEE + crafting_flags = CRAFT_MUST_BE_LEARNED + +/datum/crafting_recipe/silver_stake + name = "Silver Stake" + result = /obj/item/stake/hardened/silver + tool_behaviors = list(TOOL_WELDER) + reqs = list( + /obj/item/stack/sheet/mineral/silver = 1, + /obj/item/stake/hardened = 1, + ) + time = 8 SECONDS + category = CAT_WEAPON_MELEE + crafting_flags = CRAFT_MUST_BE_LEARNED diff --git a/modular_oculis/modules/vampires/code/crafting/crafting_vampire.dm b/modular_oculis/modules/vampires/code/crafting/crafting_vampire.dm new file mode 100644 index 000000000000..0b8bda10d429 --- /dev/null +++ b/modular_oculis/modules/vampires/code/crafting/crafting_vampire.dm @@ -0,0 +1,85 @@ +/datum/crafting_recipe/vassalrack + name = "Vassalization Rack" + result = /obj/structure/vampire/vassalrack + time = 5 SECONDS + + reqs = list( + /obj/item/stack/sheet/iron = 5, + /obj/item/stack/rods = 6, + ) + + category = CAT_VAMPIRE + crafting_flags = CRAFT_CHECK_DENSITY | CRAFT_ONE_PER_TURF | CRAFT_ON_SOLID_GROUND | CRAFT_MUST_BE_LEARNED + +/datum/crafting_recipe/candelabrum + name = "Candelabrum" + result = /obj/structure/vampire/candelabrum + time = 5 SECONDS + + reqs = list( + /obj/item/stack/sheet/iron = 1, + /obj/item/stack/rods = 3, + /obj/item/flashlight/flare/candle = 2, + ) + + category = CAT_VAMPIRE + crafting_flags = CRAFT_CHECK_DENSITY | CRAFT_ONE_PER_TURF | CRAFT_ON_SOLID_GROUND | CRAFT_MUST_BE_LEARNED + +/datum/crafting_recipe/bloodthrone + name = "Blood Throne" + result = /obj/structure/vampire/bloodthrone + time = 5 SECONDS + + reqs = list( + /obj/item/stack/sheet/iron = 10, + /obj/item/stack/rods = 2, + ) + + category = CAT_VAMPIRE + crafting_flags = CRAFT_CHECK_DENSITY | CRAFT_ONE_PER_TURF | CRAFT_ON_SOLID_GROUND | CRAFT_MUST_BE_LEARNED + +/datum/crafting_recipe/blackcoffin + name = "Black Coffin" + result = /obj/structure/closet/crate/coffin/blackcoffin + tool_behaviors = list(TOOL_WELDER, TOOL_SCREWDRIVER) + reqs = list( + /obj/item/stack/sheet/cloth = 1, + /obj/item/stack/sheet/mineral/wood = 5, + /obj/item/stack/sheet/iron = 1, + ) + time = 15 SECONDS + category = CAT_STRUCTURE + +/datum/crafting_recipe/securecoffin + name = "Secure Coffin" + result = /obj/structure/closet/crate/coffin/securecoffin + tool_behaviors = list(TOOL_WELDER, TOOL_SCREWDRIVER) + reqs = list( + /obj/item/stack/rods = 1, + /obj/item/stack/sheet/plasteel = 5, + /obj/item/stack/sheet/iron = 5, + ) + time = 15 SECONDS + category = CAT_STRUCTURE + +/datum/crafting_recipe/meatcoffin + name = "Meat Coffin" + result = /obj/structure/closet/crate/coffin/meatcoffin + tool_behaviors = list(TOOL_KNIFE, TOOL_ROLLINGPIN) + reqs = list( + /obj/item/food/meat/slab = 5, + /obj/item/restraints/handcuffs/cable = 1, + ) + time = 15 SECONDS + category = CAT_STRUCTURE + crafting_flags = CRAFT_CHECK_DENSITY | CRAFT_ONE_PER_TURF | CRAFT_ON_SOLID_GROUND | CRAFT_MUST_BE_LEARNED + +/datum/crafting_recipe/metalcoffin + name = "Metal Coffin" + result = /obj/structure/closet/crate/coffin/metalcoffin + reqs = list( + /obj/item/stack/sheet/iron = 6, + /obj/item/stack/rods = 2, + ) + time = 10 SECONDS + category = CAT_STRUCTURE diff --git a/modular_oculis/modules/vampires/code/datum_vampire.dm b/modular_oculis/modules/vampires/code/datum_vampire.dm new file mode 100644 index 000000000000..a3158a704f77 --- /dev/null +++ b/modular_oculis/modules/vampires/code/datum_vampire.dm @@ -0,0 +1,825 @@ +/datum/antagonist/vampire + name = "\improper Vampire" + roundend_category = "vampires" + antagpanel_category = "Vampire" + show_in_roundend = ROLE_VAMPIRE + ui_name = "AntagInfoVampire" + hijack_speed = 0 + stinger_sound = 'modular_oculis/modules/vampires/sound/lunge_warn.ogg' + hud_icon = 'modular_oculis/modules/vampires/icons/antag_hud.dmi' + antag_hud_name = "vampire" + preview_outfit = /datum/outfit/vampire_outfit + view_exploitables = TRUE + can_assign_self_objectives = TRUE + + show_to_ghosts = TRUE + + jobban_flag = ROLE_VAMPIRE + pref_flag = ROLE_VAMPIRE + + desensitized_modifier = DESENSITIZED_THRESHOLD + + /// How much blood we have, starting off at default blood levels. + /// We don't use our actual body's temperature because some species don't have blood and we don't want to exclude them + var/current_vitae = BLOOD_VOLUME_NORMAL + /// How much blood we can have at once, increases per level. + var/max_vitae = 600 + + /// The vampire team, used for vassals + var/datum/team/vampire/vampire_team + /// The vampire's clan + var/datum/vampire_clan/my_clan + /// Our disciplines + var/list/owned_disciplines = list() + + /// Timer between alerts for Healing messages + COOLDOWN_DECLARE(vampire_spam_healing) + + /// Should we automatically forge objectives? + var/should_forge_objectives = TRUE + + /// Flavor only + var/vampire_name + + /// Are we the prince? + var/prince = FALSE + /// Are we the scourge? Literally only used for the examine. Okay. + var/scourge = FALSE + /// Have we been broken the Masquerade? + var/broke_masquerade = FALSE + /// How many Masquerade Infractions do we have? + var/masquerade_infractions = 0 + /// Cooldown between masquerade infractions, so you can't have a bunch of them in the span of a single fight. + COOLDOWN_DECLARE(masquerade_infraction_cooldown) + + /// How many vampires we've diablerized, if any. + var/diablerie_count = 0 + + /// How many humanity points do we have? 0-10 + /// We actually always start with 7 and then add the clan's default humanity + var/humanity = VAMPIRE_DEFAULT_HUMANITY + + /// Blood required to enter Frenzy + var/frenzy_threshold = FRENZY_THRESHOLD_ENTER + /// If we've already alerted the player about low blood + var/low_blood_alerted = FALSE + /// Cooldown for re-entering frenzy after we exit it, to prevent potential spam/loops. + COOLDOWN_DECLARE(frenzy_cooldown) + + /// Goal of vitae required for the next level up + var/current_vitae_goal = VITAE_GOAL_STANDARD + /// progress to that goal + var/vitae_goal_progress = 0 + /// To keep track of objective + var/total_blood_drank = 0 + + /// Powers currently owned + var/list/datum/action/cooldown/vampire/powers = list() + + /// Vassals under my control. Periodically remove the dead ones. + var/list/datum/antagonist/vassal/vassals = list() + + /// The rank this vampire is at, used to level abilities and strength up + var/vampire_level = 0 + var/vampire_level_unspent = VAMPIRE_STARTING_LEVELS + /// How many more "free" levels this vampire will get. + var/free_levels_remaining = VAMPIRE_FREE_LEVELS + + + /// If the poor sap has suffered final death. + var/final_death = FALSE + + /// Additional regeneration when the vampire has a lot of blood + var/additional_regen + /// How much damage the vampire heals each life tick. Increases per rank up + var/vampire_regen_rate = 0.3 + + /// Minimum cooldown when reviving. + COOLDOWN_DECLARE(revive_cooldown) + + /// How much more punch/kick damage the vampire gets per rank. + var/extra_damage_per_rank = VAMPIRE_UNARMED_DMG_INCREASE_ON_RANKUP + + /// Haven + var/area/vampire_haven_area + var/obj/structure/closet/crate/coffin/coffin + + /// List of limbs we've applied modifications to. + var/list/affected_limbs = list( + BODY_ZONE_L_ARM = null, + BODY_ZONE_R_ARM = null, + BODY_ZONE_L_LEG = null, + BODY_ZONE_R_LEG = null, + ) + + /// Static typecache of all vampire powers. + var/static/list/all_vampire_powers = typecacheof(/datum/action/cooldown/vampire, ignore_root_path = TRUE) + /// Antagonists that cannot be vassalized no matter what + var/static/list/vassal_banned_antags = list( + /datum/antagonist/vampire, + /datum/antagonist/changeling, + /datum/antagonist/cult, + /datum/antagonist/clock_cultist, + ) + + /// List of traits that the Feign Life ability does not remove. + var/static/list/always_traits = list( + TRAIT_DRINKS_BLOOD, + TRAIT_GENELESS, // prevents vamps from having genes at all. masquerade will work around this being an antag test with TRAIT_FAKEGENES + TRAIT_NO_DNA_COPY, // no, you can't cheat your curse with a cloner. + TRAIT_NO_MINDSWAP, // mindswapping vampires is buggy af and I'm too lazy to properly fix it. ~Absolucy + TRAIT_SLIME_NO_CANNIBALIZE, // prevents weird softlocks + ) + + /// List of traits applied inherently + var/static/list/vampire_traits = list( + TRAIT_AGEUSIA, + TRAIT_HARDLY_WOUNDED, + TRAIT_NOBREATH, + TRAIT_NOCRITDAMAGE, + TRAIT_NOHARDCRIT, + TRAIT_NOSOFTCRIT, + TRAIT_NO_MIRROR_REFLECTION, + TRAIT_RADIMMUNE, + TRAIT_RESISTCOLD, + TRAIT_SLEEPIMMUNE, + TRAIT_STABLEHEART, + TRAIT_STABLELIVER, + TRAIT_TOXIMMUNE, + TRAIT_VIRUSIMMUNE, + // they eject zombie tumors and xeno larvae during eepy time anyways + TRAIT_NO_ZOMBIFY, // they're already undead lol + TRAIT_XENO_IMMUNE, // something something facehuggers only latch onto living things + ) + + /// Humanity gain tracking, when adding more, remember to add the type define + var/humanity_petting_goal = 5 + var/humanity_art_goal = 2 + var/humanity_hugging_goal = 3 + var/list/humanity_trackgain_hugged = list() + var/list/humanity_trackgain_petted = list() + var/list/humanity_trackgain_art = list() + +/datum/antagonist/vampire/proc/create_vampire_team() + vampire_team = new(owner) + vampire_team.name = "[ADMIN_LOOKUP(owner.current)]'s vampire team" // only displayed to admins + vampire_team.master_vampire = src + +/datum/team/vampire + name = "vampire team" + var/datum/antagonist/vampire/master_vampire + +/datum/team/vampire/roundend_report() + return + +/** + * Apply innate effects is everything given to the mob + * When a body is tranferred, this is called on the new mob + * while on_gain is called ONCE per ANTAG, this is called ONCE per BODY. + */ +/datum/antagonist/vampire/apply_innate_effects(mob/living/mob_override) + . = ..() + var/mob/living/current_mob = mob_override || owner.current + RegisterSignals(current_mob, list(COMSIG_MOB_LOGIN, COMSIG_MOVABLE_Z_CHANGED), PROC_REF(on_login)) + RegisterSignal(current_mob, COMSIG_LIVING_LIFE, PROC_REF(life_tick)) + RegisterSignal(current_mob, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine)) + RegisterSignal(current_mob, COMSIG_ATOM_EXPOSE_REAGENTS, PROC_REF(after_expose_reagents)) + RegisterSignal(current_mob, COMSIG_LIVING_DEATH, PROC_REF(on_death)) + RegisterSignal(current_mob, COMSIG_MOVABLE_MOVED, PROC_REF(update_all_trackers)) + RegisterSignal(current_mob, COMSIG_HUMAN_ON_HANDLE_BLOOD, PROC_REF(handle_blood)) + RegisterSignal(current_mob, COMSIG_MOB_UPDATE_SIGHT, PROC_REF(on_update_sight)) + + RegisterSignal(current_mob, COMSIG_LIVING_PET_ANIMAL, PROC_REF(on_pet_animal)) + RegisterSignal(current_mob, COMSIG_LIVING_HUG_CARBON, PROC_REF(on_hug_carbon)) + RegisterSignal(current_mob, COMSIG_LIVING_APPRAISE_ART, PROC_REF(on_appraise_art)) + + handle_clown_mutation(current_mob, "Your clownish nature has been subdued by your thirst for blood.") + + current_mob.update_sight() + current_mob.clear_mood_event("vampcandle") + + addtimer(CALLBACK(src, TYPE_PROC_REF(/datum/antagonist, add_team_hud), current_mob), 0.5 SECONDS, TIMER_OVERRIDE | TIMER_UNIQUE) //i don't trust this to not act weird + + current_mob.add_faction(FACTION_VAMPIRE) + + if(current_mob.hud_used) + on_hud_created() + else + RegisterSignal(current_mob, COMSIG_MOB_HUD_CREATED, PROC_REF(on_hud_created)) + + ensure_brain_nonvital(current_mob) + setup_limbs(current_mob) + + if(ishuman(current_mob)) + var/mob/living/carbon/human/current_human = current_mob + current_human.physiology?.stamina_mod *= VAMPIRE_INHERENT_STAMINA_RESIST + + var/datum/dna/current_mob_dna = current_mob.has_dna() + if(current_mob_dna) + if(current_mob_dna.check_mutation(/datum/mutation/dwarfism)) + ADD_TRAIT(current_mob, TRAIT_DWARF, TRAIT_VAMPIRE) + current_mob_dna.remove_all_mutations() + current_mob.add_traits(vampire_traits + always_traits, TRAIT_VAMPIRE) + + current_mob.grant_language(/datum/language/vampiric, source = LANGUAGE_VAMPIRE) + + my_clan?.apply_effects(current_mob) + +/** + * Remove innate effects is everything given to the mob + * When a body is tranferred, this is called on the old mob. + * while on_removal is called ONCE per ANTAG, this is called ONCE per BODY. +**/ +/datum/antagonist/vampire/remove_innate_effects(mob/living/mob_override) + . = ..() + var/mob/living/current_mob = mob_override || owner.current + UnregisterSignal(current_mob, list( + COMSIG_MOB_LOGIN, + COMSIG_MOVABLE_Z_CHANGED, + COMSIG_LIVING_LIFE, + COMSIG_ATOM_EXAMINE, + COMSIG_ATOM_EXPOSE_REAGENTS, + COMSIG_LIVING_DEATH, + COMSIG_MOVABLE_MOVED, + COMSIG_HUMAN_ON_HANDLE_BLOOD, + COMSIG_MOB_UPDATE_SIGHT, + COMSIG_LIVING_PET_ANIMAL, + COMSIG_LIVING_HUG_CARBON, + COMSIG_LIVING_APPRAISE_ART, + )) + current_mob.update_sight() + current_mob.remove_status_effect(/datum/status_effect/frenzy) + current_mob.remove_traits(vampire_traits + always_traits, TRAIT_VAMPIRE) + + handle_clown_mutation(current_mob, removing = FALSE) + + cleanup_limbs(current_mob) + + remove_hud_elements(current_mob) + + current_mob.remove_faction(FACTION_VAMPIRE) + + if(ishuman(current_mob)) + var/mob/living/carbon/human/current_human = current_mob + current_human.physiology?.stamina_mod /= VAMPIRE_INHERENT_STAMINA_RESIST + + if(!QDELETED(current_mob)) + my_clan?.remove_effects(current_mob) + +/datum/antagonist/vampire/proc/remove_hud_elements(mob/living/current_mob) + var/datum/hud/vampire_hud = current_mob?.hud_used + if(!vampire_hud) + return + vampire_hud.remove_screen_object(HUD_VAMPIRE_BLOOD, update = FALSE) + vampire_hud.remove_screen_object(HUD_VAMPIRE_RANK, update = FALSE) + vampire_hud.remove_screen_object(HUD_VAMPIRE_HUMANITY, update = FALSE) + +/datum/antagonist/vampire/proc/on_hud_created(datum/source) + SIGNAL_HANDLER + var/datum/hud/vampire_hud = owner.current.hud_used + + vampire_hud.add_screen_object(/atom/movable/screen/vampire/blood_counter, HUD_VAMPIRE_BLOOD, HUD_GROUP_INFO) + vampire_hud.add_screen_object(/atom/movable/screen/vampire/rank_counter, HUD_VAMPIRE_RANK, HUD_GROUP_INFO) + vampire_hud.add_screen_object(/atom/movable/screen/vampire/humanity_counter, HUD_VAMPIRE_HUMANITY, HUD_GROUP_INFO) + +/datum/antagonist/vampire/get_admin_commands() + . = ..() + .["Level Add"] = CALLBACK(src, PROC_REF(rank_up), 1) + + if(vampire_level_unspent > 0) + .["Level Deduct"] = CALLBACK(src, PROC_REF(rank_down)) + + if(!broke_masquerade) + .["Break Masq"] = CALLBACK(src, PROC_REF(break_masquerade)) + .["Add Infraction"] = CALLBACK(src, PROC_REF(give_masquerade_infraction), TRUE) + + if(humanity > 0) + .["Humanity Deduct"] = CALLBACK(src, PROC_REF(adjust_humanity), -1, FALSE) + else if(humanity < 10) + .["Humanity Add"] = CALLBACK(src, PROC_REF(adjust_humanity), 1, FALSE) + +/datum/antagonist/vampire/on_gain() + . = ..() + ADD_TRAIT(owner, TRAIT_VAMPIRE_ALIGNED, REF(src)) + + RegisterSignal(src, COMSIG_VAMPIRE_TRACK_HUMANITY_GAIN, PROC_REF(on_track_humanity_gain_signal)) + RegisterSignal(owner, COMSIG_SLIME_CORE_EJECTED, PROC_REF(on_slime_core_ejected)) + RegisterSignal(owner, COMSIG_SLIME_REVIVED, PROC_REF(on_slime_revive)) + + owner.teach_crafting_recipe(list( + /datum/crafting_recipe/vassalrack, + /datum/crafting_recipe/candelabrum, + /datum/crafting_recipe/bloodthrone, + /datum/crafting_recipe/meatcoffin, + )) + + // Set name and reputation + select_first_name() + + // Objectives + if(should_forge_objectives) + forge_objectives() + + create_vampire_team() + + // Assign starting stats skill point. + give_starting_powers() + GLOB.all_vampires += src + + SSvampire_leveling.check_enable() + + // Start society if we're the first vampire + check_start_society() + + if(!QDELETED(owner.current)) + for(var/quirk_type in typesof(/datum/quirk/item_quirk/addict) + /datum/quirk/skittish) + owner.current.remove_quirk(quirk_type) + +#ifdef VAMPIRE_TESTING + var/turf/user_loc = get_turf(owner.current) + new /obj/structure/closet/crate/coffin(user_loc) + new /obj/structure/vampire/vassalrack(user_loc) +#endif + +/datum/antagonist/vampire/on_removal() + REMOVE_TRAIT(owner, TRAIT_VAMPIRE_ALIGNED, REF(src)) + UnregisterSignal(owner, list(COMSIG_SLIME_CORE_EJECTED, COMSIG_SLIME_REVIVED)) + + owner.forget_crafting_recipe(list( + /datum/crafting_recipe/vassalrack, + /datum/crafting_recipe/candelabrum, + /datum/crafting_recipe/bloodthrone, + /datum/crafting_recipe/meatcoffin, + )) + + clear_powers_and_stats() + GLOB.all_vampires -= src + SSvampire_leveling.check_enable() + check_cancel_society() + + if(iscarbon(owner.current)) + var/mob/living/carbon/carbon_owner = owner.current + var/obj/item/organ/brain/not_vamp_brain = carbon_owner.get_organ_slot(ORGAN_SLOT_BRAIN) + if(not_vamp_brain && (not_vamp_brain.decoy_override != initial(not_vamp_brain.decoy_override))) + not_vamp_brain.organ_flags |= ORGAN_VITAL + not_vamp_brain.decoy_override = FALSE + + return ..() + +/datum/antagonist/vampire/on_body_transfer(mob/living/old_body, mob/living/new_body) + . = ..() + + // Transfer powers + for(var/datum/action/cooldown/vampire/all_powers in powers) + if(old_body) + all_powers.Remove(old_body) + all_powers.Grant(new_body) + + // Vampire Traits + old_body?.remove_traits(vampire_traits + always_traits, TRAIT_VAMPIRE) + new_body.add_traits(vampire_traits + always_traits, TRAIT_VAMPIRE) + +/datum/antagonist/vampire/greet() + if(silent) + return + var/fullname = return_full_name() + var/list/msg = list() + + msg += span_cult_large("You are a Vampire!\n") + msg += span_cult("Open the Vampire Information panel for information about your Powers, Clan, and more. \n\n\ + You can also click on all of your hud meters for more information about them!") + + to_chat(owner, boxed_message(msg.Join("\n"))) + play_stinger() + + if(should_forge_objectives) + owner.announce_objectives() + antag_memory += "Although you were born a mortal, in undeath you earned the name [fullname].
" + +/datum/antagonist/vampire/farewell() + to_chat(owner.current, span_userdanger("With a snap, your curse has ended. You are no longer a Vampire. You live once more!")) + // Refill with Blood so they don't instantly die. + if(!HAS_TRAIT(owner.current, TRAIT_NOBLOOD)) + owner.current.set_blood_volume(BLOOD_VOLUME_NORMAL) + +// Called when using admin tools to give antag status +/datum/antagonist/vampire/admin_add(datum/mind/new_owner, mob/admin) + var/levels = input("How many unspent Ranks would you like [new_owner] to have?","Vampire Rank", vampire_level_unspent) as null | num + var/msg = "made [key_name_admin(new_owner)] into \a [name]" + if(levels > 0) + vampire_level_unspent = levels + msg += " with [levels] extra unspent Ranks." + message_admins("[key_name_admin(usr)] [msg]") + log_admin("[key_name(usr)] [msg]") + new_owner.add_antag_datum(src) + +/datum/antagonist/vampire/ui_assets(mob/user) + return list( + get_asset_datum(/datum/asset/simple/vampire_header), + ) + +/datum/antagonist/vampire/ui_data(mob/user) + return list( + "vassal_count" = count_vassals(), + "max_vassals" = max_vampire_vassals(), + ) + +/datum/antagonist/vampire/ui_static_data(mob/user) + . = ..() + + //we don't need to update this that much. + .["in_clan"] = !!my_clan + var/list/clan_data = list() + if(my_clan) + clan_data["name"] = my_clan.name + clan_data["description"] = my_clan.description + clan_data["icon"] = my_clan.join_icon + clan_data["icon_state"] = my_clan.join_icon_state + + .["clan"] += list(clan_data) + + for(var/datum/action/cooldown/vampire/power as anything in powers) + var/list/power_data = list() + + power_data["name"] = power.name + power_data["explanation"] = power.power_explanation + power_data["icon"] = power.button_icon + power_data["icon_state"] = power.button_icon_state + + power_data["cost"] = power.vitaecost ? power.vitaecost : "0" + power_data["constant_cost"] = power.constant_vitaecost ? power.constant_vitaecost : "0" + power_data["cooldown"] = power.cooldown_time / 10 + + .["powers"] += list(power_data) + +/datum/antagonist/vampire/get_preview_icon() + var/datum/universal_icon/final_icon = render_preview_outfit(/datum/outfit/vampire_outfit) + var/datum/universal_icon/blood_icon = uni_icon('icons/effects/blood.dmi', "suitblood") + blood_icon.blend_color(BLOOD_COLOR_RED, ICON_MULTIPLY) + final_icon.blend_icon(blood_icon, ICON_OVERLAY) + + return finish_preview_icon(final_icon) + +/datum/antagonist/vampire/roundend_report() + var/list/report = list() + + // Vamp name + report += "
[span_header(return_full_name())]" + report += printplayer(owner) + if(my_clan) + report += "They were part of the [my_clan.name]!" + + // Default Report + var/objectives_complete = TRUE + if(length(objectives)) + report += printobjectives(objectives) + for(var/datum/objective/objective in objectives) + if(!objective.check_completion()) + objectives_complete = FALSE + break + + // Now list their vassals + if(length(vassals)) + report += span_header("
Their vassals were...") + var/list/vassal_minds = list() + for(var/datum/antagonist/vassal/vassal in vassals) + vassal_minds += vassal.owner + report += printplayerlist(vassal_minds) + + if(objectives_complete) + report += span_greentext(span_big("The [name] was successful!")) + else + report += span_redtext(span_big("The [name] has failed!")) + + return report.Join("
") + +/datum/antagonist/vampire/hijack_speed() + . = ..() + if(istype(my_clan, /datum/vampire_clan/malkavian)) // the voices told them to do it + return max(., 1) + +/// "Oh, well, that's step one. What about two through ten?" +/// Beheading vampires is kinda buggy and results in them being dead-dead without actually being final deathed, which is NOT something that's desired. +/// Just stake them. No shortcuts. +/datum/antagonist/vampire/proc/ensure_brain_nonvital(mob/living/mob_override) + var/mob/living/carbon/carbon_owner = mob_override || owner.current + if(!iscarbon(carbon_owner) || isjellyperson(carbon_owner)) + return + var/obj/item/organ/brain/brain = carbon_owner.get_organ_slot(ORGAN_SLOT_BRAIN) + if(QDELETED(brain)) + return + brain.organ_flags &= ~ORGAN_VITAL + brain.decoy_override = TRUE + + +/datum/antagonist/vampire/proc/give_starting_powers() + for(var/datum/action/cooldown/vampire/all_powers as anything in all_vampire_powers) + if(!(initial(all_powers.special_flags) & VAMPIRE_DEFAULT_POWER)) + continue + grant_power(new all_powers) + +/** + * ##clear_power_and_stats() + * + * Removes all Vampire related Powers/Stats changes, setting them back to pre-Vampire + * Order of steps and reason why: + * Remove clan - Clans like Nosferatu give Powers on removal, we have to make sure this is given before removing Powers. + * Powers - Remove all Powers, so things like Feign Life are off. + * Species traits, Traits, MaxHealth, Language - Misc stuff, has no priority. + * Organs - At the bottom to ensure everything that changes them has reverted themselves already. + * Update Sight - Done after Eyes are regenerated. + */ +/datum/antagonist/vampire/proc/clear_powers_and_stats() + var/mob/living/carbon/user = owner.current + + // Remove clan first + if(my_clan) + my_clan.remove_effects(user) + QDEL_NULL(my_clan) + + // Powers + for(var/datum/action/cooldown/vampire/all_powers as anything in powers) + remove_power(all_powers) + + /// Stats + if(ishuman(owner.current)) + var/mob/living/carbon/human/human_user = user + human_user.physiology.stamina_mod /= VAMPIRE_INHERENT_STAMINA_RESIST + + // Remove all vampire traits + user.remove_traits(vampire_traits + always_traits, TRAIT_VAMPIRE) + + // Update Health + user.setMaxHealth(initial(user.maxHealth)) + + // Language + user.remove_language(/datum/language/vampiric, source = LANGUAGE_VAMPIRE) + + // Heart + var/obj/item/organ/heart/newheart = user.get_organ_slot(ORGAN_SLOT_HEART) + newheart?.Restart() + +/datum/antagonist/vampire/proc/claim_coffin(obj/structure/closet/crate/coffin/claimed) + var/list/banned_areas = list( + /area/icemoon, + /area/lavaland, + /area/ocean, + /area/space, + ) + + // ALREADY CLAIMED + if(claimed.resident) + if(claimed.resident == owner) + to_chat(owner, span_notice("This is your [claimed].")) + else + to_chat(owner, span_warning("This [claimed] has already been claimed by another.")) + return FALSE + var/turf/coffin_turf = get_turf(claimed) + var/area/current_area = get_area(coffin_turf) + // this if check is split up bc it's annoying to read and mentally parse when it's combined into one big if statement + var/valid_haven_area = TRUE + if(!coffin_turf) + valid_haven_area = FALSE + else if(is_type_in_list(current_area, banned_areas) || (istype(current_area, /area/ruin) && current_area.outdoors)) + valid_haven_area = FALSE + if(!valid_haven_area) + claimed.balloon_alert(owner.current, "ineligible area!") + return + // This is my Haven + coffin = claimed + coffin.resident = owner + vampire_haven_area = current_area + + to_chat(owner, span_userdanger("You have claimed [claimed] as your place of immortal rest! Your haven is now [vampire_haven_area].")) + return TRUE + +/// Name shown on antag list +/datum/antagonist/vampire/antag_listing_name() + return ..() + return_full_name() + +/datum/action/antag_info/vampire + name = "Vampire Guide" + background_icon = 'modular_oculis/modules/vampires/icons/actions_vampire.dmi' + background_icon_state = "vamp_power_off" + +/datum/antagonist/vampire/add_team_hud(mob/target, antag_to_check, passed_hud_keys) + if(broke_masquerade) + antag_hud_name = "masquerade_broken" + else if(scourge) + antag_hud_name = "scourge" + else if(prince) + antag_hud_name = "prince" + else + antag_hud_name = my_clan?.antag_hud_icon || initial(antag_hud_name) + + QDEL_NULL(team_hud_ref) + + var/datum/atom_hud/alternate_appearance/basic/has_antagonist/hud = target.add_alt_appearance( + /datum/atom_hud/alternate_appearance/basic/has_antagonist, + "antag_team_hud_[REF(src)]", + hud_image_on(target), + ) + team_hud_ref = WEAKREF(hud) + + var/list/mob/living/mob_list = list() + for(var/datum/antagonist/antag as anything in GLOB.antagonists) + if(!istype(antag, /datum/antagonist/vampire) && !istype(antag, /datum/antagonist/vassal)) + continue + var/mob/living/current = antag.owner?.current + if(!QDELETED(current)) + mob_list |= current + + for (var/datum/atom_hud/alternate_appearance/basic/has_antagonist/antag_hud as anything in GLOB.has_antagonist_huds) + if(!(antag_hud.target in mob_list)) + continue + antag_hud.show_to(target) + hud.show_to(antag_hud.target) + + +/** + * Every vampire has 3 starting objective categories: + * Ego: Grow more powerful / strengthen your position / etc + * Hedonism: Indulge in bad things that feel all too right. + * Survival: Survive. Obviously. + */ +/datum/antagonist/vampire/forge_objectives() + var/datum/objective/vampire/extra_objective + + /* if(prob(80)) */ + extra_objective = new /datum/objective/vampire/ego/vassals + /* else + extra_objective = new /datum/objective/vampire/ego/department_vassal */ + + extra_objective.owner = owner + objectives += extra_objective + + //pick Hedonism objective + switch(rand(1, 2)) + if(1) + extra_objective = new /datum/objective/vampire/hedonism/gourmand + if(2) + extra_objective = new /datum/objective/vampire/hedonism/thirster + + extra_objective.owner = owner + objectives += extra_objective + + // Survive Objective + var/datum/objective/survive/vampire/survive_objective = new + survive_objective.owner = owner + objectives += survive_objective + +/// Use this instead of `length(vassals)`, as it won't count round removed vassals and such. +/datum/antagonist/vampire/proc/count_vassals(only_living = FALSE) + . = 0 + for(var/datum/antagonist/vassal/vassal as anything in vassals) + var/mob/living/vassal_body = vassal.owner.current + if(QDELETED(vassal_body)) + continue + if(only_living && !considered_alive(vassal.owner)) + continue + if(!HAS_TRAIT(vassal_body, TRAIT_MIND_TEMPORARILY_GONE)) + if(vassal_body.stat == DEAD) + if(HAS_TRAIT(vassal_body, TRAIT_DEFIB_BLACKLISTED)) + continue + if(!vassal_body.key) + var/mob/dead/observer/vassal_ghost = vassal_body.get_ghost(TRUE, TRUE) + if(isnull(vassal_ghost) || (istype(vassal_ghost) && !vassal_ghost.can_reenter_corpse)) // soulcatcher shitcode workaround + continue + else if(!vassal_body.key) + continue + .++ + +/datum/antagonist/vampire/proc/on_examine(datum/source, mob/examiner, list/examine_text) + SIGNAL_HANDLER + var/text + if(prince) + text = " " + else if(scourge) + text = " " + else + text = " " + + if(IS_VASSAL(examiner) in vassals) + text += span_vampire_master("This is, [return_full_name()] your Master!") + examine_text += text + return + + if(HAS_MIND_TRAIT(examiner, TRAIT_VAMPIRE_ALIGNED)) + if(my_clan) + text += span_cult("[return_full_name()], of the [my_clan].") + else + text += span_cult("[return_full_name()], a disgusting caitiff thinblood.") + + if(examiner != owner.current) // So many ifs. where is yanderedev. + if(scourge) + text += span_cult_large("
[owner.current.p_They()] [owner.current.p_are()] the Scourge!") + if(prince) + text += span_cult_large("
[owner.current.p_They()] [owner.current.p_are()] your Prince!") + if(broke_masquerade) + text += span_cult_large("
You recognize [owner.current.p_them()] as a masquerade breaker!") + + examine_text += text + + if(diablerie_count > 0 && HAS_TRAIT(examiner, TRAIT_SEE_DIABLERIE)) + examine_text += span_cult_large("
You can see the corrupted marks of a diablerist in [owner.current.p_their()] aura!") + +/datum/antagonist/vampire/proc/setup_limbs(mob/living/carbon/target) + if(!iscarbon(target)) + return + RegisterSignal(target, COMSIG_CARBON_POST_ATTACH_LIMB, PROC_REF(register_limb)) + RegisterSignal(target, COMSIG_CARBON_POST_REMOVE_LIMB, PROC_REF(unregister_limb)) + for(var/body_part in affected_limbs) + var/obj/item/bodypart/limb = target.get_bodypart(check_zone(body_part)) + if(limb) + register_limb(target, limb, initial = TRUE) + +/datum/antagonist/vampire/proc/cleanup_limbs(mob/living/carbon/target) + if(!iscarbon(target)) + return + UnregisterSignal(target, list(COMSIG_CARBON_POST_ATTACH_LIMB, COMSIG_CARBON_POST_REMOVE_LIMB)) + for(var/body_part in affected_limbs) + var/obj/item/bodypart/limb = target.get_bodypart(check_zone(body_part)) + if(limb) + unregister_limb(target, limb) + +/datum/antagonist/vampire/proc/register_limb(mob/living/carbon/owner, obj/item/bodypart/new_limb, special, initial = FALSE) + SIGNAL_HANDLER + + affected_limbs[new_limb.body_zone] = new_limb + RegisterSignal(new_limb, COMSIG_QDELETING, PROC_REF(limb_gone)) + + var/extra_damage = 1 + (vampire_level * extra_damage_per_rank) + new_limb.unarmed_damage_low += extra_damage + new_limb.unarmed_damage_high += extra_damage + +/datum/antagonist/vampire/proc/unregister_limb(mob/living/carbon/owner, obj/item/bodypart/lost_limb, special) + SIGNAL_HANDLER + + var/extra_damage = 1 + (vampire_level * extra_damage_per_rank) + lost_limb.unarmed_damage_low = max(lost_limb.unarmed_damage_low - extra_damage, initial(lost_limb.unarmed_damage_low)) + lost_limb.unarmed_damage_high = max(lost_limb.unarmed_damage_high - extra_damage, initial(lost_limb.unarmed_damage_high)) + affected_limbs[lost_limb.body_zone] = null + UnregisterSignal(lost_limb, COMSIG_QDELETING) + +/datum/antagonist/vampire/proc/limb_gone(obj/item/bodypart/deleted_limb) + SIGNAL_HANDLER + if(affected_limbs[deleted_limb.body_zone]) + affected_limbs[deleted_limb.body_zone] = null + UnregisterSignal(deleted_limb, COMSIG_QDELETING) + +/datum/antagonist/vampire/proc/after_expose_reagents(mob/source_mob, list/reagents, datum/reagents/source, methods = TOUCH, volume_modifier = 1, show_message = TRUE) + SIGNAL_HANDLER + var/datum/reagent/blood/blood_reagent = locate() in reagents + if(!blood_reagent) + return + var/blood_volume = round(reagents[blood_reagent], 0.1) + if(blood_volume > 0) + adjust_blood_volume(blood_volume) + +/datum/antagonist/vampire/proc/on_login() + SIGNAL_HANDLER + var/mob/living/current = owner.current + if(!QDELETED(current)) + addtimer(CALLBACK(src, TYPE_PROC_REF(/datum/antagonist, add_team_hud), current), 0.5 SECONDS, TIMER_OVERRIDE | TIMER_UNIQUE) //i don't trust this to not act weird + +/datum/antagonist/vampire/proc/on_update_sight(mob/user) + SIGNAL_HANDLER + user.add_sight(SEE_MOBS) + user.lighting_cutoff = max(user.lighting_cutoff, LIGHTING_CUTOFF_HIGH) + user.lighting_color_cutoffs = user.lighting_color_cutoffs ? blend_cutoff_colors(user.lighting_color_cutoffs, list(25, 8, 5)) : list(25, 8, 5) + +/datum/outfit/vampire_outfit + name = "Vampire outfit (Preview only)" + suit = /obj/item/clothing/suit/costume/dracula + +/datum/outfit/vampire_outfit/post_equip(mob/living/carbon/human/enrico, visualsOnly=FALSE) + enrico.hairstyle = "Undercut" + enrico.hair_color = "FFF" + enrico.skin_tone = "african2" + enrico.eye_color_left = "#663300" + enrico.eye_color_right = "#663300" + + enrico.update_body(is_creating = TRUE) + +/datum/asset/simple/vampire_header + assets = list("vampire.png" = 'modular_oculis/modules/vampires/html/images/vampire.png') + +/obj/item/antag_granter/vampire + name = "strange vial" + desc = "A large vial filled with a strange viscous, red substance. It has no markings apart from an orange warning stripe near the cap." + icon = 'icons/obj/mining_zones/artefacts.dmi' + icon_state = "vial" + antag_datum = /datum/antagonist/vampire + user_message = "As you chug the strange liquid within the bottle, you start to feel... thirsty..." + +/datum/opposing_force_equipment/uplink/vampire + item_type = /obj/item/antag_granter/vampire + name = "Vampiric Blood" + description = "A mysterious vial filled with a strange viscous, red substance, said to turn the user into a \"Vampire\"." + admin_note = "Vampire antag granter." + +/obj/item/clothing/neck/necklace/memento_mori/memento(mob/living/carbon/human/user) + if(IS_VAMPIRE(user)) + to_chat(user, span_warning("\The [src] rejects you.")) + return FALSE + return ..() diff --git a/modular_oculis/modules/vampires/code/dynamic_vampire.dm b/modular_oculis/modules/vampires/code/dynamic_vampire.dm new file mode 100644 index 000000000000..55790528dadd --- /dev/null +++ b/modular_oculis/modules/vampires/code/dynamic_vampire.dm @@ -0,0 +1,141 @@ +/datum/dynamic_ruleset/roundstart/vampire + name = "Vampires" + config_tag = "Roundstart Vampire" + preview_antag_datum = /datum/antagonist/vampire + pref_flag = ROLE_VAMPIRE + // higher for testing purposes, since it's surprisingly uncommon for us to reach the min pop anyways + weight = alist( + DYNAMIC_TIER_LOW = /* 8 */ 10, + DYNAMIC_TIER_LOWMEDIUM = /* 8 */ 10, + DYNAMIC_TIER_MEDIUMHIGH = /* 8 */ 10, + DYNAMIC_TIER_HIGH = /* 10 */ 12, + ) + min_pop = 15 + min_antag_cap = 2 + max_antag_cap = 2 + blacklisted_roles = list( + JOB_CURATOR, + ) + +/datum/dynamic_ruleset/roundstart/vampire/assign_role(datum/mind/candidate) + candidate.add_antag_datum(/datum/antagonist/vampire) + +/datum/dynamic_ruleset/midround/from_living/vampire + name = "Midround Vampire" + config_tag = "Midround Vampire" + preview_antag_datum = /datum/antagonist/vampire + midround_type = LIGHT_MIDROUND + pref_flag = ROLE_VAMPIRIC_ACCIDENT + jobban_flag = ROLE_VAMPIRE + ruleset_flags = RULESET_VARIATION + min_antag_cap = 1 + // higher for testing purposes, since it's surprisingly uncommon for us to reach the min pop anyways + weight = alist( + DYNAMIC_TIER_LOW = /* 8 */ 10, + DYNAMIC_TIER_LOWMEDIUM = /* 8 */ 10, + DYNAMIC_TIER_MEDIUMHIGH = /* 8 */ 10, + DYNAMIC_TIER_HIGH = /* 10 */ 12, + ) + min_pop = 15 + blacklisted_roles = list( + JOB_CURATOR, + ) + + /// Prevents can_be_selected from returning false after we've already started polling. + var/locked_in = FALSE + +/datum/dynamic_ruleset/midround/from_living/vampire/assign_role(datum/mind/candidate) + candidate.add_antag_datum(/datum/antagonist/vampire) + +/datum/dynamic_ruleset/midround/from_living/vampire/collect_candidates() + var/list/candidates = ..() + return poll_candidates_for_one(trim_candidates(candidates)) + +/datum/dynamic_ruleset/midround/from_living/vampire/mass + name = "Mass Vampires" + config_tag = "Mass Vampires" + min_antag_cap = 1 + max_antag_cap = list("denominator" = 25, offset = 1) // note: the amount of existing vampires is taken off of existing pop + midround_type = HEAVY_MIDROUND + +/** + * Polls a group of candidates to see if they want to be a vampire. + * + * @param candidates a list containing a candidate mobs + */ +/datum/dynamic_ruleset/midround/from_living/vampire/proc/poll_candidates_for_one(list/candidates) + locked_in = TRUE + var/max_candidates = get_antag_cap(length(GLOB.alive_player_list) - length(GLOB.all_vampires), max_antag_cap || min_antag_cap) + message_admins("[name]: Attempting to poll [length(candidates)] people individually, trying to select [max_candidates]") + log_dynamic("[name]: Attempting to poll [length(candidates)] people individually, trying to select [max_candidates]") + var/list/yes_candidates = list() + var/sanity = 5 + while((length(yes_candidates) < max_candidates) && length(candidates) && sanity > 0) + sanity-- + var/mob/living/candidate = pick_n_take(candidates) + if(QDELETED(candidate) || candidate.stat == DEAD || !candidate.client) + continue + log_dynamic("[name]: Polling candidate [key_name(candidate)]") + if(poll_for_vampire(candidate, yes_candidates)) + log_dynamic("[name]: Candidate [key_name(candidate)] has accepted being a Vampire") + else + log_dynamic("[name]: Candidate [key_name(candidate)] has declined to be a Vampire") + + log_dynamic("[name]: [length(yes_candidates)] candidates accepted") + return yes_candidates + +/datum/dynamic_ruleset/midround/from_living/vampire/proc/poll_for_vampire(mob/living/candidate, list/yes_candidates) + var/list/response = SSpolling.poll_candidates( + question = "Do you want to be a Vampire?", + group = list(candidate), + poll_time = 15 SECONDS, + flash_window = TRUE, + start_signed_up = FALSE, + announce_chosen = FALSE, + role_name_text = "Vampiric Accident", + alert_pic = image('modular_oculis/modules/vampires/icons/actions_vampire.dmi', "clanselect"), + custom_response_messages = list( + POLL_RESPONSE_SIGNUP = "You have signed up to be a vampire!", + POLL_RESPONSE_ALREADY_SIGNED = "You are already signed up to be a vampire.", + POLL_RESPONSE_NOT_SIGNED = "You aren't signed up to be a vampire.", + POLL_RESPONSE_TOO_LATE_TO_UNREGISTER = "It's too late to decide against being a vampire.", + POLL_RESPONSE_UNREGISTERED = "You decide against being a vampire.", + ), + chat_text_border_icon = image('modular_oculis/modules/vampires/icons/actions_vampire.dmi', "clanselect"), + ) + if(response) + yes_candidates += response + return TRUE + else + return FALSE + +/datum/dynamic_ruleset/midround/from_living/vampire/execute() + . = ..() + locked_in = FALSE + +/datum/dynamic_ruleset/midround/from_living/vampire/can_be_selected() + return locked_in || count_vampires() < 3 + +/datum/dynamic_ruleset/midround/from_living/vampire/mass/can_be_selected() + return locked_in || count_vampires() < 2 + +/datum/dynamic_ruleset/latejoin/vampire + name = "Latejoin Vampire" + config_tag = "Latejoin Vampire" + preview_antag_datum = /datum/antagonist/vampire + pref_flag = ROLE_VAMPIRE_BREAKOUT + jobban_flag = ROLE_VAMPIRE + // higher for testing purposes, since it's surprisingly uncommon for us to reach the min pop anyways + weight = alist( + DYNAMIC_TIER_LOW = /* 8 */ 10, + DYNAMIC_TIER_LOWMEDIUM = /* 8 */ 10, + DYNAMIC_TIER_MEDIUMHIGH = /* 8 */ 10, + DYNAMIC_TIER_HIGH = /* 10 */ 12, + ) + min_pop = 15 + blacklisted_roles = list( + JOB_CURATOR, + ) + +/datum/dynamic_ruleset/latejoin/vampire/assign_role(datum/mind/candidate) + candidate.add_antag_datum(/datum/antagonist/vampire) diff --git a/modular_oculis/modules/vampires/code/frenzy_vampire.dm b/modular_oculis/modules/vampires/code/frenzy_vampire.dm new file mode 100644 index 000000000000..aa66cd9273cc --- /dev/null +++ b/modular_oculis/modules/vampires/code/frenzy_vampire.dm @@ -0,0 +1,119 @@ +/** + * # Status effect + * + * This is the status effect given to Vampires in a Frenzy + * This deals with everything entering/exiting Frenzy is meant to deal with. + */ +/atom/movable/screen/alert/status_effect/frenzy + name = "Frenzy" + desc = "You are in a Frenzy! You are entirely Feral and, depending on your Clan, fighting for your life!" + icon = 'modular_oculis/modules/vampires/icons/actions_vampire.dmi' + icon_state = "frenzy_alert" + alerttooltipstyle = "cult" + +/datum/status_effect/frenzy + id = "frenzy" + status_type = STATUS_EFFECT_UNIQUE + duration = STATUS_EFFECT_PERMANENT + tick_interval = 1 SECONDS + alert_type = /atom/movable/screen/alert/status_effect/frenzy + + /// The stored vampire antag datum + var/datum/antagonist/vampire/vampiredatum + + /// Traits given by frenzy. + var/static/list/frenzy_traits = list( + TRAIT_DISCOORDINATED_TOOL_USER, + TRAIT_FRENZY, + TRAIT_PUSHIMMUNE, + TRAIT_STRONG_GRABBER, + TRAIT_STUNIMMUNE, + ) + +/datum/status_effect/frenzy/Destroy() + . = ..() + vampiredatum = null + +/datum/status_effect/frenzy/on_apply() + var/mob/living/carbon/carbon_owner = owner + if(!iscarbon(carbon_owner)) + return FALSE + vampiredatum = IS_VAMPIRE(carbon_owner) + + ASSERT(!isnull(vampiredatum), "Frenzy status effect applied to a non-vampire!") + + if(vampiredatum.current_vitae >= FRENZY_THRESHOLD_EXIT) + return FALSE + + // Basic stuff + carbon_owner.add_movespeed_modifier(/datum/movespeed_modifier/frenzy_speed) + carbon_owner.add_client_colour(/datum/client_colour/manual_heart_blood, TRAIT_STATUS_EFFECT(id)) + carbon_owner.uncuff() + carbon_owner.pulledby?.stop_pulling() + carbon_owner.set_stamina_loss(0) + carbon_owner.SetAllImmobility(0) + carbon_owner.set_resting(FALSE, silent = TRUE, instant = TRUE) + + // Alert them + vampiredatum.disable_all_powers(forced = TRUE) + vampiredatum.adjust_humanity(-2) + to_chat(carbon_owner, span_userdanger("BLOOD! YOU NEED BLOOD NOW!")) + to_chat(carbon_owner, span_announce("* Vampire Tip: While in Frenzy, you instantly aggressively grab, have stun immunity, and cannot use any powers outside of Feed and Trespass (If you have it).")) + carbon_owner.balloon_alert(carbon_owner, "you enter a frenzy!") + carbon_owner.playsound_local(null, 'modular_oculis/modules/vampires/sound/rage_increase.ogg', 100, FALSE, pressure_affected = FALSE) + + // Stamina modifier + if (ishuman(carbon_owner)) + var/mob/living/carbon/human/human_owner = carbon_owner + human_owner.physiology?.stamina_mod *= 0.4 + + // Traits + carbon_owner.add_traits(frenzy_traits, TRAIT_STATUS_EFFECT(id)) + + carbon_owner.log_message("has entered a vampiric Frenzy due to low blood!", LOG_ATTACK) + + return TRUE + +/datum/status_effect/frenzy/on_remove() + var/mob/living/carbon/carbon_owner = owner + if(!iscarbon(carbon_owner)) + return + + carbon_owner.log_message("has exited their vampiric Frenzy.", LOG_ATTACK) + + COOLDOWN_START(vampiredatum, frenzy_cooldown, 30 SECONDS) + + // Basic stuff + carbon_owner.remove_movespeed_modifier(/datum/movespeed_modifier/frenzy_speed) + carbon_owner.remove_client_colour(TRAIT_STATUS_EFFECT(id)) + + // Alert them + carbon_owner.balloon_alert(carbon_owner, "you come back to your senses.") + carbon_owner.playsound_local(null, 'modular_oculis/modules/vampires/sound/rage_decrease.ogg', 100, FALSE, pressure_affected = FALSE) + + // Stamina modifier + if (ishuman(carbon_owner)) + var/mob/living/carbon/human/human_owner = carbon_owner + human_owner.physiology?.stamina_mod /= 0.4 + + // Traits + carbon_owner.remove_traits(frenzy_traits, TRAIT_STATUS_EFFECT(id)) + +/datum/status_effect/frenzy/tick() + var/mob/living/carbon/carbon_owner = owner + if(vampiredatum.current_vitae >= FRENZY_THRESHOLD_EXIT) + qdel(src) + return + carbon_owner.adjust_fire_loss(0.75) + carbon_owner.set_jitter_if_lower(10 SECONDS) + +/datum/status_effect/frenzy/get_examine_text() + return span_danger("[owner.p_They()] seem[owner.p_s()]... inhumane, and feral!") + +/datum/movespeed_modifier/frenzy_speed + blacklisted_movetypes = FLYING | FLOATING + multiplicative_slowdown = -0.1 // Might seem very low but at this point we are already slow as balls from hunger + +/atom/movable/screen/alert/status_effect/feign_life/MouseEntered(location,control,params) + desc = initial(desc) + return ..() diff --git a/modular_oculis/modules/vampires/code/hud_vampire.dm b/modular_oculis/modules/vampires/code/hud_vampire.dm new file mode 100644 index 000000000000..9d343f88b373 --- /dev/null +++ b/modular_oculis/modules/vampires/code/hud_vampire.dm @@ -0,0 +1,247 @@ +/// 1 tile up +#define UI_HUMANITY_DISPLAY "WEST:6,CENTER+1:-8" +/// 1 tile down +#define UI_BLOOD_DISPLAY "WEST:6,CENTER:0" +/// 2 tiles down +#define UI_VAMPRANK_DISPLAY "WEST:6,CENTER-1:-2" +/// 6 pixels to the right, zero tiles & 5 pixels DOWN. +#define UI_SUNLIGHT_DISPLAY "WEST:6,CENTER-0:0" + +///Maptext define for Vampire HUDs +#define FORMAT_VAMPIRE_HUD_TEXT(valuecolor, value) MAPTEXT("
[round(value,1)]
") +///Maptext define for Vampire Sunlight HUDs +#define FORMAT_VAMPIRE_SUNLIGHT_TEXT(valuecolor, value) MAPTEXT("
[value]
") + +/atom/movable/screen/vampire + icon = 'modular_oculis/modules/vampires/icons/actions_vampire.dmi' + mouse_over_pointer = MOUSE_HAND_POINTER + +/atom/movable/screen/vampire/blood_counter + name = "Vitae" + icon_state = "blood_display" + screen_loc = UI_BLOOD_DISPLAY + +/atom/movable/screen/vampire/blood_counter/Click() + . = ..() + var/list/msg = list() + var/mob/living/owner_mob = hud.mymob + var/datum/antagonist/vampire/owner_vamp = IS_VAMPIRE(owner_mob) + + if(!owner_vamp) + return + + msg += span_cult_large("This is your Vitae-Counter.") + msg += span_cult("Here you see your current level of blood-energy. This is used for all of your abilities, and sustains your very being.") + msg += span_cult("\nYou need to drink a certain amount from living, sentient beings in order to level up.") + msg += span_cult("Your healing also depends on it. You reach your maximum healing potential at [BS_BLOOD_VOLUME_MAX_REGEN].") + + var/bloodlevel + switch(owner_vamp.current_vitae) + if(0 to 200) + bloodlevel = "starved" + if(201 to 500) + bloodlevel = "thirsty" + if(501 to 700) + bloodlevel = "peckish" + if(701 to INFINITY) + bloodlevel = "content" + + msg += span_cult("Your current maximum is: [owner_vamp.max_vitae].") + msg += span_cult("This shift, you have drank [owner_vamp.total_blood_drank] units of blood.") + + msg += span_cult_large("\nRight now, you are feeling [bloodlevel].") + + if(owner_vamp.vitae_goal_progress <= owner_vamp.current_vitae_goal) + msg += span_cult_large("\nYour progress to the next level is: [owner_vamp.vitae_goal_progress]/[owner_vamp.current_vitae_goal].") + else + msg += span_cult_large("\nYou have drank deeply and greedily. Sleep in a coffin to level up.") + + to_chat(usr, boxed_message(msg.Join("\n"))) + +/atom/movable/screen/vampire/rank_counter + name = "Vampire Rank" + icon_state = "rank" + screen_loc = UI_VAMPRANK_DISPLAY + +/atom/movable/screen/vampire/rank_counter/Click() + . = ..() + var/list/msg = list() + var/mob/living/owner_mob = hud.mymob + var/datum/antagonist/vampire/owner_vamp = IS_VAMPIRE(owner_mob) + + if(!owner_vamp) + return + + /* var/mob/living/carbon/human/vampire_human = owner_mob */ + msg += span_cult_large("This is your Rank-Counter.") + msg += span_cult("Here you see your current progress in the mastery of your disciplines.") + msg += span_cult("This is a measure of your main progress as a vampire, and, should you feed on another vampire(that has broken the masquerade), you will absorb half of their levels.") + msg += span_cult("With your current rank, you are considered as [owner_vamp.get_rank_string()] of your craft.") + msg += span_cult("\nCurrently, your rank affords you the following benefits:") + msg += span_cult("Max Regeneration rate: +[owner_vamp.vampire_regen_rate]") + msg += span_cult("Max Vitae pool: +[owner_vamp.max_vitae - 600] ") + msg += span_cult("Unarmed damage: +[1 + (owner_vamp.vampire_level * owner_vamp.extra_damage_per_rank)]") + + var/list/disciplinestext + for(var/datum/discipline/discipline in owner_vamp.owned_disciplines) + disciplinestext += "\n[discipline.name] - " + disciplinestext += "Level:" + disciplinestext += "[discipline.level - 1]" + + if(disciplinestext) + msg += span_cult("\nYour disciplines and their levels are:[disciplinestext]") + + to_chat(usr, boxed_message(msg.Join("\n"))) + +/atom/movable/screen/vampire/humanity_counter + name = "Humanity" + icon_state = "humanity" + screen_loc = UI_HUMANITY_DISPLAY + +/atom/movable/screen/vampire/humanity_counter/Click() + . = ..() + var/list/msg = list() + var/mob/living/owner_mob = hud.mymob + var/datum/antagonist/vampire/owner_vamp = IS_VAMPIRE(owner_mob) + + msg += span_cult_large("This is your Humanity score.") + msg += span_cult("Humanity is a measure of how closely a vampire clings to the morality and values of mortal life, and consequently how well they are able to resist the urges of the Beast.") + msg += span_cult("This has an active effect on the curse of all cainites. Vampires with little humanity may find it harder to stay awake during the day, or to awaken from long periods of torpor. If your humanity is particularly low, you may even burst into flames in the presence of god's light.") + + var/humanitylevel + switch(owner_vamp.humanity) + if(0) + humanitylevel = "Monstrous" + if(1) + humanitylevel = "Horrific" + if(2) + humanitylevel = "Bestial" + if(3) + humanitylevel = "Cold" + if(4) + humanitylevel = "Unfeeling" + if(5) + humanitylevel = "Removed" + if(6) + humanitylevel = "Distant" + if(7) + humanitylevel = "Normal" + if(8) + humanitylevel = "Caring" + if(9) + humanitylevel = "Compassionate" + if(10) + humanitylevel = "Saintly" + + msg += span_cult("\nRight now, others would describe you as '[humanitylevel]'.") + if(owner_vamp.humanity > 7) + msg += span_cult("Due to your connection to your own human soul, you have achieved the masquerade ability.") + + msg += span_cult("\nYou may gain humanity by engaging in human activities, such as:") + msg += span_cult("Hugging different mortals: [length(owner_vamp.humanity_trackgain_hugged)] of [owner_vamp.humanity_hugging_goal].") + msg += span_cult("Petting various animals: [length(owner_vamp.humanity_trackgain_petted)] of [owner_vamp.humanity_petting_goal].") + msg += span_cult("Looking at art: [length(owner_vamp.humanity_trackgain_art)] of [owner_vamp.humanity_art_goal].") + + to_chat(usr, boxed_message(msg.Join("\n"))) + +// actually only used for the "Sol Weakness" hemophage quirk lol. just in here so i can reuse the FORMAT_VAMPIRE_SUNLIGHT_TEXT macro +/atom/movable/screen/vampire/sunlight_counter + name = "Solar Flare Timer" + icon_state = "sunlight" + screen_loc = UI_SUNLIGHT_DISPLAY + +/atom/movable/screen/vampire/sunlight_counter/Initialize(mapload, datum/hud/hud_owner) + . = ..() + RegisterSignal(SSsol, COMSIG_SOL_TICK, PROC_REF(update_sunlight_display)) + +/atom/movable/screen/vampire/sunlight_counter/Destroy() + UnregisterSignal(SSsol, COMSIG_SOL_TICK) + return ..() + +/atom/movable/screen/vampire/sunlight_counter/proc/update_sunlight_display() + SIGNAL_HANDLER + if(QDELETED(hud) || QDELETED(hud.mymob) || !hud.mymob.client) + return + var/sunlightvaluecolor = "#ffffff" + if(SSsol.sunlight_active) + sunlightvaluecolor = "#FF5555" + icon_state = "[initial(icon_state)]_day" + else + switch(round(SSsol.time_til_cycle, 1)) + if(0 to 30) + icon_state = "[initial(icon_state)]_30" + sunlightvaluecolor = "#FFCCCC" + if(31 to 60) + icon_state = "[initial(icon_state)]_60" + sunlightvaluecolor = "#FFE6CC" + if(61 to 90) + icon_state = "[initial(icon_state)]_90" + sunlightvaluecolor = "#FFFFCC" + else + icon_state = "[initial(icon_state)]_night" + sunlightvaluecolor = "#FFFFFF" + + maptext = FORMAT_VAMPIRE_SUNLIGHT_TEXT( \ + sunlightvaluecolor, \ + (SSsol.time_til_cycle >= 60) ? "[round(SSsol.time_til_cycle / 60, 1)] m" : "[round(SSsol.time_til_cycle, 1)] s" \ + ) + +/// Update Blood Counter + Rank Counter +/datum/antagonist/vampire/proc/update_hud() + var/datum/hud/hud = owner.current?.hud_used + if(!hud) + return + var/valuecolor + switch(current_vitae) + if(0 to 200) + valuecolor = "#560808" + if(201 to 300) + valuecolor = "#a32a2a" + if(301 to 500) + valuecolor = "#d55c5c" + if(501 to 700) // This isn't janky, a tiny bit lenience is baked in. + valuecolor = "#ffc2c2" + if(701 to INFINITY) + valuecolor = "#ffffff" + + var/atom/movable/screen/vampire/blood_counter/blood_display = hud.screen_objects[HUD_VAMPIRE_BLOOD] + blood_display?.maptext = FORMAT_VAMPIRE_HUD_TEXT(valuecolor, current_vitae) + + var/atom/movable/screen/vampire/rank_counter/vamprank_display = hud.screen_objects[HUD_VAMPIRE_RANK] + if(vamprank_display) + if(vampire_level_unspent > 0) + vamprank_display.icon_state = "[initial(vamprank_display.icon_state)]_up" + else + vamprank_display.icon_state = initial(vamprank_display.icon_state) + vamprank_display.maptext = FORMAT_VAMPIRE_HUD_TEXT("#ffd8d8", vampire_level) + + var/atom/movable/screen/vampire/humanity_counter/humanity_display = hud.screen_objects[HUD_VAMPIRE_HUMANITY] + if(humanity_display) + var/humanityvaluecolor + switch(humanity) + if(0 to 2) + humanityvaluecolor = "#600000" + if(3 to 4) + humanityvaluecolor = "#a71c1c" + if(4 to 5) + humanityvaluecolor = "#db4646" + if(6 to 8) // same here + humanityvaluecolor = "#e8adad" + if(9 to 10) + humanityvaluecolor = "#ffffff" + + humanity_display.maptext = FORMAT_VAMPIRE_HUD_TEXT(humanityvaluecolor, humanity) + +/// 1 tile up +#undef UI_HUMANITY_DISPLAY +/// 1 tile down +#undef UI_BLOOD_DISPLAY +/// 2 tiles down +#undef UI_VAMPRANK_DISPLAY +/// 6 pixels to the right, zero tiles & 5 pixels DOWN. +#undef UI_SUNLIGHT_DISPLAY + +///Maptext define for Vampire HUDs +#undef FORMAT_VAMPIRE_HUD_TEXT +///Maptext define for Vampire Sunlight HUDs +#undef FORMAT_VAMPIRE_SUNLIGHT_TEXT diff --git a/modular_oculis/modules/vampires/code/language_vampire.dm b/modular_oculis/modules/vampires/code/language_vampire.dm new file mode 100644 index 000000000000..4813b6354b0f --- /dev/null +++ b/modular_oculis/modules/vampires/code/language_vampire.dm @@ -0,0 +1,21 @@ +/datum/language/vampiric + name = "Enochian" + desc = "The language of Enoch, the first city of man. It was preserved past the great flood by the antediluvians, and is now 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 = "vampire" + icon = 'modular_oculis/modules/vampires/icons/vampiric.dmi' diff --git a/modular_oculis/modules/vampires/code/life_vampire.dm b/modular_oculis/modules/vampires/code/life_vampire.dm new file mode 100644 index 000000000000..dde3534c4590 --- /dev/null +++ b/modular_oculis/modules/vampires/code/life_vampire.dm @@ -0,0 +1,330 @@ +/// Runs from COMSIG_LIVING_LIFE, handles Vampire constant processes. +/datum/antagonist/vampire/proc/life_tick(datum/source, seconds_per_tick, times_fired) + SIGNAL_HANDLER + + // Weirdness shield + if(isbrain(owner?.current)) + update_hud() + return + if(QDELETED(owner)) + INVOKE_ASYNC(src, PROC_REF(handle_death)) + return + + // Deduct Blood + if(owner.current.stat == CONSCIOUS && !HAS_TRAIT(owner.current, TRAIT_IMMOBILIZED) && !HAS_TRAIT(owner.current, TRAIT_NODEATH)) + adjust_blood_volume(-VAMPIRE_PASSIVE_BLOOD_DRAIN) + + // Healing + if(handle_healing(seconds_per_tick) && !isanimal_or_basicmob(owner)) + if((COOLDOWN_FINISHED(src, vampire_spam_healing)) && current_vitae > 0) + to_chat(owner.current, span_notice("The power of your blood knits your wounds...")) + COOLDOWN_START(src, vampire_spam_healing, VAMPIRE_SPAM_HEALING) + + var/area/current_area = get_area(owner.current) + if(istype(current_area, /area/station/service/chapel) && !is_chaplain_job(owner.assigned_role) && humanity <= 2) + to_chat(owner, span_warning("Your inhuman nature is rejected by a holy presence!")) + owner.current.adjust_fire_loss(10) + owner.current.adjust_fire_stacks(4) + owner.current.ignite_mob() + + // Standard Updates + + // Clan specific stuff + if(my_clan) + my_clan.handle_clan_life() + else if(!(locate(/datum/action/cooldown/vampire/clanselect) in powers)) + grant_power(new /datum/action/cooldown/vampire/clanselect) + + // Set our body's blood_volume to mimick our vampire one (if we aren't using the Feign Life power) + update_hud() + +/** + * Pretty simple, add a value to the vampire's blood volume +**/ +/datum/antagonist/vampire/proc/adjust_blood_volume(value) + current_vitae = clamp(current_vitae + value, 0, max_vitae) + +/** + * Runs on the vampire's lifetick. + * Heal clone, brain, brute and burn damage. + * + * By default, burn damage is healed 50% as effectively as brute + * When undergoing torpor it's 80%, if you're in a coffin 100% +**/ +/datum/antagonist/vampire/proc/handle_healing(seconds_per_tick) + var/mob/living/current = owner.current + + // Weirdness shield + if(QDELETED(current)) + return FALSE + + var/in_torpor = HAS_TRAIT(current, TRAIT_TORPOR) + if(!in_torpor && HAS_TRAIT(current, TRAIT_FEIGN_LIFE)) + return FALSE + + // oh god why + if(HAS_TRAIT_FROM(current, TRAIT_DEATHCOMA, CHANGELING_TRAIT)) + return FALSE + + var/actual_regen = vampire_regen_rate + additional_regen + + // Heal brain damage + current.adjust_organ_loss(ORGAN_SLOT_BRAIN, -1 * (actual_regen * 4) * seconds_per_tick) + + if(!iscarbon(current)) + return FALSE + var/mob/living/carbon/carbon_owner = current + + var/vitaecost_multiplier = 0.5 // Coffin makes it cheaper + var/healing_multiplier = 1 + + if(carbon_owner.on_fire) + healing_multiplier = 0 + else if(HAS_TRAIT(carbon_owner, TRAIT_SLIME_HYDROPHOBIA)) + healing_multiplier = 0.75 + + if(current.has_status_effect(/datum/status_effect/frenzy)) + healing_multiplier *= 0.25 + + var/brute_heal = min(carbon_owner.get_brute_loss(), actual_regen) + var/burn_heal = min(carbon_owner.get_fire_loss(), actual_regen) * 0.5 + + if(length(carbon_owner.all_wounds)) + var/datum/wound/bloodiest_wound + for(var/datum/wound/wound as anything in carbon_owner.all_wounds) + if(wound.blood_flow && (!bloodiest_wound || wound.blood_flow > bloodiest_wound?.blood_flow)) + bloodiest_wound = wound + + bloodiest_wound?.adjust_blood_flow(-0.25 * seconds_per_tick) + + for(var/obj/item/bodypart/bodypart as anything in carbon_owner.bodyparts) + if(bodypart.generic_bleedstacks) + bodypart.adjustBleedStacks(-1, 0) + + if(in_torpor) + // If in a coffin: heal 5x as fast, heal burn damage at full capacity, set vitaecost to 50%, and regenerate limbs + // If not: heal 3x as fast and heal burn damage at 80% + if(istype(carbon_owner.loc, /obj/structure/closet/crate/coffin)) + if(HAS_TRAIT(owner.current, TRAIT_FEIGN_LIFE) && COOLDOWN_FINISHED(src, vampire_spam_healing)) + to_chat(carbon_owner, span_alert("You do not heal while your Feign Life ability is active.")) + COOLDOWN_START(src, vampire_spam_healing, VAMPIRE_SPAM_MASQUERADE) + return FALSE + + burn_heal = min(carbon_owner.get_fire_loss(), actual_regen) + healing_multiplier = 5 + vitaecost_multiplier = 0.25 // Decrease cost if we're sleeping in a coffin. + + // Extinguish and remove embedded objects + carbon_owner.extinguish_mob() + INVOKE_ASYNC(carbon_owner, TYPE_PROC_REF(/mob/living/carbon, remove_all_embedded_objects)) + + if(try_regenerate_limbs(vitaecost_multiplier)) + return TRUE + else + burn_heal = min(carbon_owner.get_fire_loss(), actual_regen) * 0.8 + healing_multiplier = 3 + + // Heal if Damaged + brute_heal *= healing_multiplier + burn_heal *= healing_multiplier + + if(brute_heal > 0 || burn_heal > 0) // Just a check? Don't heal/spend, and return. + var/vitaecost = (brute_heal * 0.5 + burn_heal) * vitaecost_multiplier * healing_multiplier + carbon_owner.heal_overall_damage(brute_heal * seconds_per_tick, burn_heal * seconds_per_tick) + adjust_blood_volume(-vitaecost) + return TRUE + + // Revive them if dead and there is no damage left to heal, just in case we are not in torpor because of some wackyness. + // Note this doesn't revive when staked. + if(carbon_owner.stat == DEAD && !in_torpor) + heal_vampire_organs() + return TRUE + + return FALSE + +/datum/antagonist/vampire/proc/try_regenerate_limbs(cost_muliplier = 1) + var/mob/living/carbon/carbon_owner = owner.current + if(!iscarbon(carbon_owner) || QDELING(carbon_owner)) + return + var/limb_regen_cost = 50 * -cost_muliplier + + var/list/missing = carbon_owner.get_missing_limbs() + if(length(missing) && (current_vitae < limb_regen_cost + 5)) + return FALSE + for(var/missing_limb in missing) //Find ONE Limb and regenerate it. + carbon_owner.regenerate_limb(missing_limb, FALSE) + adjust_blood_volume(-limb_regen_cost) + var/obj/item/bodypart/missing_bodypart = carbon_owner.get_bodypart(missing_limb) + if(missing_limb == BODY_ZONE_HEAD) + ensure_brain_nonvital(carbon_owner) + else + missing_bodypart.receive_damage(brute = 60, wound_bonus = CANT_WOUND) + to_chat(carbon_owner, span_notice("Your flesh knits as it regrows your [missing_bodypart]!")) + playsound(carbon_owner, 'sound/effects/magic/demon_consume.ogg', 50, TRUE) + return TRUE + +/** + * This is used when exiting Torpor and when given vampire status, 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). +**/ +/datum/antagonist/vampire/proc/heal_vampire_organs() + var/mob/living/carbon/carbon_user = owner.current + if(!istype(carbon_user)) + return + + // Clear husk and regenerate organs + carbon_user.cure_husk() + carbon_user.regenerate_organs() + ensure_brain_nonvital(carbon_user) + + // Heal organs + for(var/obj/item/organ/organ as anything in carbon_user.organs) + organ.set_organ_damage(0) + + // Heart + if(!HAS_TRAIT(carbon_user, TRAIT_FEIGN_LIFE)) + var/obj/item/organ/heart/current_heart = carbon_user.get_organ_slot(ORGAN_SLOT_HEART) + current_heart?.Stop() + + // Heal wounds + for(var/datum/wound/iter_wound as anything in carbon_user.all_wounds) + iter_wound.remove_wound() + + // Get rid of icky organs. From `panacea.dm` + var/static/list/bad_organs = typecacheof(list( + /obj/item/organ/body_egg, + /obj/item/organ/zombie_infection, + )) + for(var/obj/item/organ/organ as anything in carbon_user.organs) + if(!is_type_in_typecache(organ, bad_organs)) + continue + + organ.Remove(carbon_user) + organ.forceMove(carbon_user.drop_location()) + + // Don't Revive if staked or being staked + if(carbon_user.stat == DEAD && COOLDOWN_FINISHED(src, revive_cooldown) && !check_if_staked() && !HAS_TRAIT(carbon_user, TRAIT_BEINGSTAKED)) + carbon_user.revive() + // Heal suffocation + carbon_user.set_oxy_loss(0) + +/** + * Called when we die +**/ +/datum/antagonist/vampire/proc/on_death(mob/living/source, gibbed) + SIGNAL_HANDLER + + if(source.stat != DEAD || isjellyperson(source)) + return + + COOLDOWN_START(src, revive_cooldown, 25 SECONDS) // ensure we take at minimum 25 seconds to revive. + INVOKE_ASYNC(src, PROC_REF(handle_death)) + +/datum/antagonist/vampire/proc/handle_death() + if(QDELETED(owner.current)) + return + if(check_if_staked()) + final_death() + else + owner.current.apply_status_effect(/datum/status_effect/vampire_torpor) + +/** + * Handle things related to blood + * + * Step 1 - Set nutrition to our blood level + * Step 2 - If we are in a frenzy, check if we have enough blood to exit it + * Step 3 - If we have too little blood, enter a frenzy + * Step 4 - If we're low on blood, start jittering + * Step 5 - Set regeneration rate based off how much blood we have +**/ +/datum/antagonist/vampire/proc/handle_blood() + SIGNAL_HANDLER + // Set nutrition + owner.current.set_nutrition(min(current_vitae, NUTRITION_LEVEL_WELL_FED)) + + if(HAS_TRAIT(owner.current, TRAIT_FEIGN_LIFE)) + owner.current.set_blood_volume(BLOOD_VOLUME_NORMAL) + else + owner.current.set_blood_volume(current_vitae, BLOOD_VOLUME_BAD, BLOOD_VOLUME_NORMAL) // we want to get pale but not completely fucked up + + // Try and exit frenzy + if(current_vitae >= FRENZY_THRESHOLD_EXIT) + owner.current.remove_status_effect(/datum/status_effect/frenzy) + + // Blood is low, lets show some effects + if(current_vitae < 100 && !HAS_TRAIT(owner.current, TRAIT_FEIGN_LIFE)) + owner.current.set_jitter_if_lower(6 SECONDS) + + // Enter frenzy if our blood is low enough + if(current_vitae < FRENZY_THRESHOLD_ENTER && COOLDOWN_FINISHED(src, frenzy_cooldown)) + owner.current.apply_status_effect(/datum/status_effect/frenzy) + + // Warn them at low blood + if(current_vitae < VAMPIRE_LOW_BLOOD_WARNING && !low_blood_alerted) + owner.current.playsound_local(null, 'modular_oculis/modules/vampires/sound/bloodneed.ogg', 100, FALSE, pressure_affected = FALSE) + to_chat(owner.current, span_narsiesmall("Care now. Your vitae runs low!"), type = MESSAGE_TYPE_WARNING) + low_blood_alerted = TRUE + else if(current_vitae > VAMPIRE_LOW_BLOOD_WARNING) + low_blood_alerted = FALSE + + // The more blood, the better the regeneration + if(current_vitae < BLOOD_VOLUME_BAD) + additional_regen = 0.1 + else if(current_vitae < BLOOD_VOLUME_OKAY) + additional_regen = 0.2 + else if(current_vitae < BLOOD_VOLUME_NORMAL) + additional_regen = 0.3 + else if(current_vitae < BS_BLOOD_VOLUME_MAX_REGEN) + additional_regen = 0.4 + else + additional_regen = 0.5 + + return HANDLE_BLOOD_HANDLED | HANDLE_BLOOD_NO_NUTRITION_DRAIN + +/// dust +/datum/antagonist/vampire/proc/final_death(skip_destruction = FALSE) + var/mob/living/body = owner.current + // If we have no body, end here. + if(QDELETED(body)) + return + + UnregisterSignal(body, list( + COMSIG_LIVING_LIFE, + COMSIG_ATOM_EXAMINE, + COMSIG_LIVING_DEATH, + COMSIG_HUMAN_ON_HANDLE_BLOOD, + COMSIG_MOVABLE_MOVED, + COMSIG_LIVING_APPRAISE_ART, + )) + + final_death = TRUE + free_all_vassals() + + if(!skip_destruction) + if(iscarbon(body)) + // Drop anything in us + var/mob/living/carbon/carbon_body = body + carbon_body.drop_all_held_items() + carbon_body.unequip_everything() + carbon_body.remove_all_embedded_objects() + playsound(owner.current, 'modular_oculis/modules/vampires/sound/burning_death.ogg', 100, TRUE) + else + body.dust(drop_items = TRUE) + + if(SEND_SIGNAL(src, COMSIG_VAMPIRE_FINAL_DEATH) & DONT_DUST) + return + + if(skip_destruction || QDELETED(body)) + return + + // Get dusted lmao + body.visible_message( + span_warning("[body]'s skin crackles and dries, [body.p_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.") + ) + + body.remove_status_effect(/datum/status_effect/vampire_torpor) + body.dust(just_ash = FALSE, drop_items = TRUE) diff --git a/modular_oculis/modules/vampires/code/misc_procs_vampire.dm b/modular_oculis/modules/vampires/code/misc_procs_vampire.dm new file mode 100644 index 000000000000..9a85548203bb --- /dev/null +++ b/modular_oculis/modules/vampires/code/misc_procs_vampire.dm @@ -0,0 +1,453 @@ +/** + * Helper proc for adding a power +**/ +/datum/antagonist/vampire/proc/grant_power(datum/action/cooldown/vampire/power) + for(var/datum/action/cooldown/vampire/current_powers as anything in powers) + if(current_powers.type == power.type) + return FALSE + powers += power + + power.Grant(owner.current) + log_uplink("[key_name(owner.current)] has purchased: [power].") + update_static_data_for_all_viewers() + return TRUE + +/** + * Helper proc for removing a power +**/ +/datum/antagonist/vampire/proc/remove_power(datum/action/cooldown/vampire/power) + if(power.currently_active) + power.deactivate_power() + powers -= power + power.Remove(owner.current) + update_static_data_for_all_viewers() + +/** + * When a Vampire breaks the Masquerade, they get their HUD icon changed, and Malkavian Vampires get alerted. +**/ +/datum/antagonist/vampire/proc/break_masquerade(mob/admin) + if(broke_masquerade) + return + broke_masquerade = TRUE + + owner.current.playsound_local(null, 'modular_oculis/modules/vampires/sound/masquerade_violation.ogg', 100, FALSE, pressure_affected = FALSE) + to_chat(owner.current, span_userdanger("You have broken the Masquerade!")) + to_chat(owner.current, span_warning("Vampire Tip: When you break the Masquerade, you become open for termination by fellow Vampires, and your vassals are no longer completely loyal to you, as other Vampires can steal them for themselves!")) + + add_team_hud(owner.current) + + SEND_GLOBAL_SIGNAL(COMSIG_VAMPIRE_BROKE_MASQUERADE, src) + GLOB.masquerade_breakers += src + +/** + * Increment the masquerade infraction counter and warn the vampire accordingly +**/ +/datum/antagonist/vampire/proc/give_masquerade_infraction(ignore_cooldown = FALSE) + if(broke_masquerade) + return + if(!ignore_cooldown) + if(!COOLDOWN_FINISHED(src, masquerade_infraction_cooldown)) + return + COOLDOWN_START(src, masquerade_infraction_cooldown, 90 SECONDS) + masquerade_infractions++ + + owner.current.playsound_local(null, 'modular_oculis/modules/vampires/sound/lunge_warn.ogg', 100, FALSE, pressure_affected = FALSE) + + if(masquerade_infractions >= 3) + break_masquerade() + else + to_chat(owner.current, span_cult_bold("You violated the Masquerade! Break the Masquerade [3 - masquerade_infractions] more times and you will become hunted by all other Vampires!")) + +/** + * Offers the vampire the option to thicken their blood if they've reached their vitae goal. + * Called when the vampire sleeps in a coffin. +**/ +/datum/antagonist/vampire/proc/rank_up_if_goal() + while(vitae_goal_progress >= current_vitae_goal) + if(!rank_up(1)) + return + +/** + * Increase our unspent vampire levels by one and try to rank up if inside a coffin + * Called when sleeping in a coffin, and admin abuse +**/ +/datum/antagonist/vampire/proc/rank_up(levels, ignore_reqs = FALSE, increase_goal = TRUE) + if(QDELETED(owner) || QDELETED(owner.current)) + return FALSE + + if(vitae_goal_progress < current_vitae_goal && !ignore_reqs) + to_chat(owner.current, span_notice("Your lack of experience has left you unable to level up. Fulfill your vitae goal next time in order to level up.")) + return FALSE + + vampire_level_unspent += levels + for(var/limb_slot, current_limb in affected_limbs) + var/obj/item/bodypart/limb = current_limb + if(QDELETED(limb) || !(limb_slot in GLOB.limb_zones)) + continue + // This affects the hitting power of regular unarmed attacks and Brawn. + limb.unarmed_damage_low += extra_damage_per_rank + limb.unarmed_damage_high += extra_damage_per_rank + + if(!my_clan) + to_chat(owner.current, span_notice("You have grown in power. Join a clan to spend it.")) + return FALSE + + to_chat(owner, span_notice("You have grown familiar with your powers!")) + + if(!ignore_reqs) + vitae_goal_progress = max(vitae_goal_progress - current_vitae_goal, 0) + /* else + vitae_goal_progress = 0 */ + if(increase_goal) + current_vitae_goal += VITAE_GOAL_STANDARD + + return TRUE + +/** + * Decrease the unspent vampire levels by one. Only for admins +**/ +/datum/antagonist/vampire/proc/rank_down() + vampire_level_unspent-- + +/datum/antagonist/vampire/proc/remove_nondefault_powers(return_levels = FALSE) + for(var/datum/action/cooldown/vampire/power as anything in powers) + if(power.special_flags & VAMPIRE_DEFAULT_POWER) + continue + remove_power(power) + if(return_levels) + vampire_level_unspent++ + +/** + * Disables all Torpor exclusive powers, if forced is TRUE, disable all powers +**/ +/datum/antagonist/vampire/proc/disable_all_powers(forced = FALSE) + for(var/datum/action/cooldown/vampire/power as anything in powers) + if(forced || ((power.vampire_check_flags & BP_CANT_USE_IN_TORPOR) && HAS_TRAIT(owner.current, TRAIT_TORPOR))) + if(power.currently_active) + power.deactivate_power() + +/** + * Check if we have a stake in our heart +**/ +/datum/antagonist/vampire/proc/check_if_staked() + var/obj/item/bodypart/chosen_bodypart = owner.current?.get_bodypart(BODY_ZONE_CHEST) + if(locate(/obj/item/stake) in chosen_bodypart?.embedded_objects) + return TRUE + return FALSE +/** + * ##adjust_humanity(count, silent) + * + * Adds the specified amount of humanity to the vampire + * Checks to make sure it doesn't exceed 10, + * Checks to make sure it doesn't go under 0, + * Adds the masquerade power at 9 or above + */ +/datum/antagonist/vampire/proc/adjust_humanity(count, silent = FALSE) + // Step one: Toreadors have doubled gains and losses + if(istype(my_clan, /datum/vampire_clan/toreador)) + count = count * 2 + + // No-op if nothing to change + if(count == 0) + return FALSE + + // If trying to add but already at max, there's nothing to do + if(count > 0 && humanity >= 10) + return FALSE + + // Same for removing + if(count < 0 && humanity <= 0) + return FALSE + + var/temp_humanity = humanity + count + + var/power_given = FALSE + var/power_removed = FALSE + + // Are we adding or removing? + if(count > 0) + // We are adding + if(temp_humanity >= VAMPIRE_HUMANITY_MASQUERADE_POWER && !is_type_in_list(/datum/action/cooldown/vampire/feign_life, powers)) + // Grant_power might fail, so we need to check if it actually got granted + var/was_granted = grant_power(new /datum/action/cooldown/vampire/feign_life) + if(was_granted) + power_given = TRUE + + // Only run this code if there is an actual increase in humanity. Also don't run it if we wanna be silent. + if(humanity < temp_humanity && !silent) + owner.current.playsound_local(null, 'modular_oculis/modules/vampires/sound/humanity_gain.ogg', 50, TRUE, pressure_affected = FALSE) + if(power_given) + to_chat(owner.current, span_userdanger("Your closeness to humanity has granted you the ability to feign life!")) + else + to_chat(owner.current, span_userdanger("You have gained humanity.")) + else + // We are removing + if(temp_humanity < VAMPIRE_HUMANITY_MASQUERADE_POWER) + for(var/datum/action/cooldown/vampire/feign_life/power in powers) + remove_power(power) + power_removed = TRUE + + // Only run this code if there is an actual decrease in humanity + if(humanity > temp_humanity && !silent) + owner.current.playsound_local(null, 'modular_oculis/modules/vampires/sound/humanity_loss.ogg', 50, TRUE, pressure_affected = FALSE) + if(power_removed) + to_chat(owner.current, span_userdanger("Your inhuman actions have caused you to lose the masquerade ability!")) + else + to_chat(owner.current, span_userdanger("You have lost humanity.")) + + // Clamp to valid range, we are so sane we might see the face of god + if(temp_humanity > 10) + temp_humanity = 10 + if(temp_humanity < 0) + temp_humanity = 0 + + humanity = temp_humanity + return TRUE + +/// Bacon wanted a signal +/datum/antagonist/vampire/proc/on_track_humanity_gain_signal(datum/source, type, subject) + SIGNAL_HANDLER + return track_humanity_gain_progress(type, subject) + +/// Signal handler for when the vampire pets an animal +/datum/antagonist/vampire/proc/on_pet_animal(datum/source, mob/living/pet) + SIGNAL_HANDLER + return track_humanity_gain_progress(HUMANITY_PETTING_TYPE, pet) + +/// Signal handler for when the vampire hugs a carbon +/datum/antagonist/vampire/proc/on_hug_carbon(datum/source, mob/living/carbon/hugged) + SIGNAL_HANDLER + if(!hugged.client) // Only count hugs with real players for humanity + return + return track_humanity_gain_progress(HUMANITY_HUGGING_TYPE, hugged) + +/// Signal handler for when the vampire appraises art +/datum/antagonist/vampire/proc/on_appraise_art(datum/source, atom/art_piece) + SIGNAL_HANDLER + return track_humanity_gain_progress(HUMANITY_ART_TYPE, art_piece) + +/** + * ##track_humanity_gain_progress(type, subject) + * + * Adds the specified subject to the tracking lists and handles all the other stuff related to it. + * When a defined threshold is met, hands out humanity as appropriate and stops tracking. + * Ideally this can be expanded on easily by just defining a new threshold and tracking list in the datum and defines respectively. + * We return TRUE if it successfully added to tracked, and FALSE if it was already tracked or failed for some other reason. + */ +/datum/antagonist/vampire/proc/track_humanity_gain_progress(type, subject) + // placeholders to populate // I dunno why this works btw, i thought i made a mistake but it worked anyways. + var/list/tracking_list = null + var/goal = null + + if(humanity >= 10) // Don't add anything if we're already at max. + return + + // map all the placeholders to the correct type, get the list for easier handling + switch(type) + if(HUMANITY_HUGGING_TYPE) + tracking_list = humanity_trackgain_hugged + goal = humanity_hugging_goal + if(HUMANITY_PETTING_TYPE) + tracking_list = humanity_trackgain_petted + goal = humanity_petting_goal + if(HUMANITY_ART_TYPE) + tracking_list = humanity_trackgain_art + goal = humanity_art_goal + else + return FALSE // Cheeky check for type built in? Tsunami you genius! + + if(isatom(subject)) + var/atom/atom_subject = subject + if(atom_subject.flags_1 & HOLOGRAM_1) // doesn't count!! + return FALSE + + // track the weakref, not the actual reference + subject = WEAKREF(subject) + + // already tracked? + if(subject in tracking_list) + return FALSE + + // Update the corresponding list + switch(type) + if(HUMANITY_HUGGING_TYPE) + humanity_trackgain_hugged += subject + if(HUMANITY_PETTING_TYPE) + humanity_trackgain_petted += subject + if(HUMANITY_ART_TYPE) + humanity_trackgain_art += subject + + if(length(tracking_list) >= goal) + // set the corresponding gained flag and award humanity + switch(type) + if(HUMANITY_HUGGING_TYPE) + humanity_hugging_goal *= 2 + if(HUMANITY_PETTING_TYPE) + humanity_petting_goal *= 2 + if(HUMANITY_ART_TYPE) + humanity_art_goal *= 2 + adjust_humanity(1) + + return TRUE + +/datum/antagonist/vampire/proc/get_rank_string() + switch(vampire_level) + if(0 to 1) + return "'Initiate'" + if(2 to 3) + return "'Novice'" + if(4 to 5) + return "'Apprentice'" + if(6 to 7) + return "'Adept'" + if(8 to 9) + return "'Expert'" + if(10 to 11) + return "'Master'" + if(12 to 24) + return "'Grand Master'" + if(25 to INFINITY) + return "[span_narsiesmall("'Methuselah'")]" + +/// This is where we store clan descriptions. +/// We will need to know the description of a clan before we "make" one, +/// because we can't just get the description from the "not-made" clan ref. +/datum/antagonist/vampire/proc/get_clan_description(clan_name) + /// This makes descriptions about a billion times cleaner: Spans for discipline names and their individual descriptions: + var/disciplines = "[span_tooltip("Disciplines are the aspects of the original curse bestowed upon caine, of which every kindred suffers/benefits. In terms of gameplay, they are groups of abilities that you level up.", "Disciplines")]" + var/animalism = "[span_tooltip("Animalism is a Discipline that brings the vampire closer to their animalistic nature. This typically allows them to communicate with and gain dominance over creatures of nature.", "Animalism")]" + var/auspex = "[span_tooltip("Auspex is a Discipline that grants vampires supernatural senses, letting them peer far further and deeper than any mortal. The malkavians especially have a strong bond with it.", "Auspex")]" + var/celerity = "[span_tooltip("Celerity is a Discipline that grants vampires supernatural quickness and reflexes.", "Celerity")]" + var/dominate = "[span_tooltip("Dominate is a Discipline that overwhelms another person's mind with the vampire's will.", "Dominate")]" + var/fortitude = "[span_tooltip("Fortitude is a Discipline that grants Kindred unearthly toughness.", "Fortitude")]" + var/obfuscate = "[span_tooltip("Obfuscate is a Discipline that allows vampires to conceal themselves, deceive the mind of others, or make them ignore what the user does not want to be seen.", "Obfuscate")]" + var/potence = "[span_tooltip("Potence is the Discipline that endows vampires with physical vigor and preternatural strength.", "Potence")]" + var/presence = "[span_tooltip("Presence is the Discipline of supernatural allure and emotional manipulation which allows Kindred to attract, sway, and control crowds.", "Presence")]" + var/protean = "[span_tooltip("Protean is a Discipline that gives vampires the ability to change form, from growing feral claws to turning into something entirely different.", "Protean")]" + var/thaumaturgy = "[span_tooltip("Thaumaturgy is the secret blood-art of the clan tremere. Allowing them all manners of blood-sorcery and pacts.", "Thaumaturgy")]" + + /// All the descriptions: + var/ventrue = "The Ventrue are the de-facto leaders of the Camarilla. They style themselves as kings and emperors, often inhabiting positions of power.\n\ + IMPORTANT: Members of the Ventrue Clan are the most eligible for princedom. Please remember that princes are expected to behave in a manner befitting their office.\n\ + [disciplines]: [dominate], [fortitude], [presence]" + var/tremere = "With a powerful ancestry of wizards and magicians, the Tremere wield the secret art of blood magic, which they guard with utmost care.\n\ + [disciplines]: [thaumaturgy], [auspex], [dominate]" + var/toreador = "Artists, Pleasure-workers, Celebrities. These are the people of the Toreador clan. They are by far the closest to humanity of all kindred, each a deeply sensitive individual.\n\ + [disciplines]: [presence], [auspex], [celerity]" + var/malkavian = "Completely insane. You gain constant hallucinations, become a prophet with unintelligable rambling, and gain insights better left unknown. You can also travel through Phobetor tears, rifts through spacetime only you can travel through.\n\ + [disciplines]: [dominate], [auspex], [obfuscate]" + var/gangrel = "Often mistaken as werewolves, Gangrel carry the smell of wet dog wherever they go. Their unique bond with the beast within allows them to transform parts of their body into powerful claws, even becoming entirely different beings.\n\ + [disciplines]: [animalism], [protean], [fortitude]" + var/brujah = "A clan now, of mostly rebels. Though some still show fragments of their lost lineage of warrior-poets. They are long split from the Camarilla, and often form their own groups.\n\ + [disciplines]: [potence], [celerity], [presence]" + + // Now the logic + switch(clan_name) + if(CLAN_TOREADOR) + return toreador + if(CLAN_VENTRUE) + return ventrue + if(CLAN_BRUJAH) + return brujah + if(CLAN_MALKAVIAN) + return malkavian + if(CLAN_TREMERE) + return tremere + if(CLAN_GANGREL) + return gangrel + + log_runtime("Unknown clan name passed to get_clan_description: [clan_name]") + return "No description available." + +/// Checks to see if an entity counts as a "watcher" for a masquerade breach +/datum/antagonist/vampire/proc/is_masq_watcher(mob/living/watcher, recursion = 1) + /// List of "weirdo" antags who won't count as masq breaks due to also being supernaturals or supernatural-adjacent. + var/static/list/weirdo_antags + if(isnull(weirdo_antags)) + weirdo_antags = list( + /datum/antagonist/changeling, + /datum/antagonist/heretic, + /datum/antagonist/heretic_monster, + /datum/antagonist/nightmare, + /datum/antagonist/wizard, + /datum/antagonist/wizard_minion, + ) + + if(!isliving(watcher) || QDELING(watcher)) + return FALSE + if(!watcher.mind || !watcher.client || watcher.client.is_afk()) + return FALSE + if(HAS_MIND_TRAIT(watcher, TRAIT_VAMPIRE_ALIGNED)) + return FALSE + if(watcher.has_faction(list(FACTION_VAMPIRE, REF(owner.current))) || watcher.has_ally(owner.current)) + return FALSE + if(watcher.mind.has_antag_datum_in_list(weirdo_antags)) + return FALSE + if(isanimal_or_basicmob(watcher)) + return FALSE + if(watcher.stat != CONSCIOUS || HAS_TRAIT(watcher, TRAIT_RESTRAINED)) + return FALSE + if(is_jaunting(watcher) || HAS_TRAIT(watcher, TRAIT_MOVE_VENTCRAWLING)) + return FALSE + if(watcher.is_blind() || watcher.is_nearsighted_currently()) + return FALSE + if(HAS_SILICON_ACCESS(watcher)) + return FALSE + if(recursion > 0) + var/mob/living/master = watcher.mind.enslaved_to?.resolve() + if(master) + return .(master, recursion - 1) + return TRUE + +/** + * Called when a Vampire reaches Final Death + * Releases all Vassals. + */ +/datum/antagonist/vampire/proc/free_all_vassals() + for(var/datum/antagonist/vassal/all_vassals in vassals) + all_vassals.owner.remove_antag_datum(/datum/antagonist/vassal) + +/// Checks to see if someone is a vassal of another vampire. +/datum/antagonist/vampire/proc/is_someone_elses_vassal(mob/living/target) + if(!isliving(target)) + return FALSE + var/datum/antagonist/vassal/vassal = IS_VASSAL(target) + if(vassal && !(vassal in vassals)) + return TRUE + return FALSE + +/* /datum/status_effect/silver_cuffed + id = "silver cuffed" + alert_type = null + processing_speed = STATUS_EFFECT_NORMAL_PROCESS + remove_on_fullheal = TRUE + +/datum/status_effect/silver_cuffed/on_apply() + if(!iscarbon(owner)) + return FALSE + return TRUE + +/datum/status_effect/silver_cuffed/tick(seconds_between_ticks) + var/mob/living/carbon/carbon_owner = owner + if(!istype(carbon_owner.handcuffed, /obj/item/restraints/handcuffs/silver)) + qdel(src) + */ + +/proc/max_vampire_vassals() + var/total_players = length(GLOB.player_list) + switch(total_players) + if(1 to 20) + return 1 + if(21 to 30) + return 2 + else + return 3 + +/proc/count_vampires() + . = 0 + for(var/datum/antagonist/vampire/vampire as anything in GLOB.all_vampires) + if(vampire.final_death) + continue + var/mob/living/body = vampire.owner?.current + if(!QDELETED(body)) + .++ diff --git a/modular_oculis/modules/vampires/code/moodlets_vampire.dm b/modular_oculis/modules/vampires/code/moodlets_vampire.dm new file mode 100644 index 000000000000..5ada0c5d2d96 --- /dev/null +++ b/modular_oculis/modules/vampires/code/moodlets_vampire.dm @@ -0,0 +1,55 @@ +/datum/mood_event/drankblood + description = span_nicegreen("I have fed greedily from that which nourishes me.") + mood_change = 10 + timeout = 8 MINUTES + hidden = TRUE + +/datum/mood_event/drankblood_bad + description = span_boldwarning("I drank the blood of a lesser creature. Disgusting.") + mood_change = -4 + timeout = 3 MINUTES + hidden = TRUE + +/datum/mood_event/drankblood_dead + description = span_boldwarning("I drank dead blood. I am better than this.") + mood_change = -7 + timeout = 8 MINUTES + hidden = TRUE + +/datum/mood_event/drankblood_synth + description = span_boldwarning("I drank synthetic blood. What is wrong with me?") + mood_change = -7 + timeout = 8 MINUTES + hidden = TRUE + +/datum/mood_event/drankkilled + description = span_boldwarning("I drained a mortal to death. I feel... inhuman.") + mood_change = -15 + timeout = 10 MINUTES + hidden = TRUE + +/datum/mood_event/coffinsleep + description = span_nicegreen("I slept in a coffin during the day. I feel whole again.") + mood_change = 10 + timeout = 6 MINUTES + hidden = TRUE + +///Candelabrum's mood event to non Vampire/Vassals +/datum/mood_event/vampcandle + description = span_boldwarning("You feel something crawling in your mind...") + mood_change = -15 + timeout = 5 MINUTES + +/datum/mood_event/vassal_happy + description = span_awe("I feel complete when I'm near my master...") + mood_change = 10 + hidden = TRUE + +/datum/mood_event/vassal_away + description = span_warning("I crave my master's blood...") + mood_change = -5 + hidden = TRUE + +/datum/mood_event/vassal_away_severe //not hidden since it's so severe + description = span_awe(span_big("I feel so empty without my master's blood...")) + mood_change = -30 diff --git a/modular_oculis/modules/vampires/code/names_vampire.dm b/modular_oculis/modules/vampires/code/names_vampire.dm new file mode 100644 index 000000000000..27009ef7bf38 --- /dev/null +++ b/modular_oculis/modules/vampires/code/names_vampire.dm @@ -0,0 +1,42 @@ +GLOBAL_VAR_INIT(vampire_names_male, shuffle(list( + "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", +))) + +GLOBAL_VAR_INIT(vampire_names, shuffle(list( + "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/vampire/proc/return_full_name() + var/fullname = vampire_name || owner.name || owner.current.real_name || owner.current.name + + fullname += " the [get_rank_string(vampire_level)]" + + return fullname + +///Returns a First name for the Vampire. +/datum/antagonist/vampire/proc/select_first_name() + var/list/name_list // blah blah blah lists are references + if(owner.current.gender == MALE) + name_list = GLOB.vampire_names_male + else + name_list = GLOB.vampire_names + // as the list is shuffled initially, we can just pick the first name, then move it to the back. + // in theory, as long as there's not a morbillion vampires, we should never have any duplicate names + vampire_name = popleft(name_list) + name_list += vampire_name diff --git a/modular_oculis/modules/vampires/code/objectives_vampire.dm b/modular_oculis/modules/vampires/code/objectives_vampire.dm new file mode 100644 index 000000000000..1dc19846df6e --- /dev/null +++ b/modular_oculis/modules/vampires/code/objectives_vampire.dm @@ -0,0 +1,182 @@ +/datum/objective/vampire + martyr_compatible = TRUE + +/datum/objective/vampire/New() + update_explanation_text() + return ..() + +////////////////////////////////////////////////////////////////////////////////////// +// // EGO OBJECTIVES // // +////////////////////////////////////////////////////////////////////////////////////// +/datum/objective/vampire/ego + name = "Dominion" + explanation_text = "You crave power, the authority to rule:" + +////////////////////////////////////////////////// Haven +//////////////////////////////////////////////////////////////////// + +/datum/objective/vampire/ego/vassals + name = "Vassalize Mortals" + +/datum/objective/vampire/ego/vassals/New() + target_amount = 1 /* rand(1, 3) */ + return ..() + +/datum/objective/vampire/ego/vassals/update_explanation_text() + . = ..() + explanation_text = "[initial(explanation_text)] Vassalize [target_amount] mortal\s using a Vassalization Rack, and ensure they survive the shift." + +// WIN CONDITIONS? +/datum/objective/vampire/ego/vassals/check_completion() + var/datum/antagonist/vampire/vampire_datum = owner.has_antag_datum(/datum/antagonist/vampire) + return vampire_datum?.count_vassals(only_living = TRUE) >= target_amount + + +////////////////////////////////////////////////// Department Vassal +//////////////////////////////////////////////////////////////////// + +/datum/objective/vampire/ego/department_vassal + name = "Bind a Department" + + ///The selected department we have to vassalize. + var/target_department + ///List of all departments that can be selected for the objective. + var/static/list/possible_departments = list( + "engineering" = DEPARTMENT_BITFLAG_ENGINEERING, + "medical" = DEPARTMENT_BITFLAG_MEDICAL, + "science" = DEPARTMENT_BITFLAG_SCIENCE, + "cargo" = DEPARTMENT_BITFLAG_CARGO, + "service" = DEPARTMENT_BITFLAG_SERVICE, + ) + +/datum/objective/vampire/ego/department_vassal/New() + target_department = pick(possible_departments) + target_amount = 1 + return ..() + +/datum/objective/vampire/ego/department_vassal/update_explanation_text() + explanation_text = "[initial(explanation_text)] Convert a crew member from the [target_department] department into your vassal." + return ..() + +/datum/objective/vampire/ego/department_vassal/proc/get_vassal_occupations() + var/datum/antagonist/vampire/vampire_datum = owner.has_antag_datum(/datum/antagonist/vampire) + if(!length(vampire_datum?.vassals)) + return FALSE + + var/list/all_vassal_jobs = list() + for(var/datum/antagonist/vassal/vassal_datum in vampire_datum.vassals) + if(!vassal_datum.owner) + continue + + var/datum/mind/vassal_mind = vassal_datum.owner + + // Mind Assigned + if(vassal_mind.assigned_role) + all_vassal_jobs += vassal_mind.assigned_role + continue + // Mob Assigned + if(vassal_mind.current?.job) + all_vassal_jobs += SSjob.get_job(vassal_mind.current.job) + continue + // PDA Assigned + if(ishuman(vassal_mind.current)) + var/mob/living/carbon/human/human_vassal = vassal_mind.current + all_vassal_jobs += SSjob.get_job(human_vassal.get_assignment()) + continue + + return all_vassal_jobs + +/datum/objective/vampire/ego/department_vassal/check_completion() + var/list/vassal_jobs = get_vassal_occupations() + var/converted_count = 0 + for(var/datum/job/checked_job in vassal_jobs) + if(checked_job.departments_bitflags & possible_departments[target_department]) + converted_count++ + if(converted_count >= target_amount) + return TRUE + return FALSE + + +////////////////////////////////////////////////// Big Places +//////////////////////////////////////////////////////////////////// + +/datum/objective/vampire/ego/bigplaces + name = "Ascend the Ranks" + +/datum/objective/vampire/ego/bigplaces/update_explanation_text() + . = ..() + explanation_text = "[initial(explanation_text)] Rise in power, reach prince or scourge, or prey on enough mortals to rank up as much as possible. You must reach at least rank 8 by the end of the shift!" + +// WIN CONDITIONS? +/datum/objective/vampire/ego/bigplaces/check_completion() + var/datum/antagonist/vampire/vampire_datum = owner.has_antag_datum(/datum/antagonist/vampire) + if(!vampire_datum) + return FALSE + + if(vampire_datum.vampire_level + vampire_datum.vampire_level_unspent >= 8) + return TRUE + + return FALSE + + +////////////////////////////////////////////////////////////////////////////////////// +// // HEDONISM OBJECTIVES // // +////////////////////////////////////////////////////////////////////////////////////// + +/datum/objective/vampire/hedonism + name = "Hunger" + explanation_text = "You crave depravity, to sate your thirst on the mortals:" + + +////////////////////////////////////////////////// Gourmand +//////////////////////////////////////////////////////////////////// + +/datum/objective/vampire/hedonism/gourmand + name = "Gorge" + +/datum/objective/vampire/hedonism/gourmand/New() + target_amount = rand(500, 1000) // This is blood, not vitae. + return ..() + +/datum/objective/vampire/hedonism/gourmand/update_explanation_text() + . = ..() + explanation_text = "[initial(explanation_text)] Consume at least [target_amount] units of blood to sate your ravenous thirst." + +/datum/objective/vampire/hedonism/gourmand/check_completion() + var/datum/antagonist/vampire/vampiredatum = owner.has_antag_datum(/datum/antagonist/vampire) + if(!vampiredatum) + return FALSE + var/stolen_blood = vampiredatum.total_blood_drank + if(stolen_blood >= target_amount) + return TRUE + return FALSE + +////////////////////////////////////////////////// Thirster +//////////////////////////////////////////////////////////////////// + +/datum/objective/vampire/hedonism/thirster + name = "Complete Drain" + // no check_completion, we just manually set `completed` in feed. + +/datum/objective/vampire/hedonism/thirster/update_explanation_text() + . = ..() + explanation_text = "[initial(explanation_text)] Drain a mortal completely, letting their lifeblood become your sustenance and their body fall cold and spent." + +////////////////////////////////////////////////////////////////////////////////////// +////////////////////////////// MISC ////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////////////// + +/datum/objective/survive/vampire + name = "Endure" + explanation_text = "Avoid final death at all costs." + +/** + * Vassal + */ +/datum/objective/vampire/vassal + name = "assist master" + explanation_text = "You crave the blood of your vampiric master! Obey and protect them at all costs!" + +/datum/objective/vampire/vassal/check_completion() + var/datum/antagonist/vassal/vassal_datum = owner.has_antag_datum(/datum/antagonist/vassal) + return vassal_datum.master?.owner?.current?.stat != DEAD diff --git a/modular_oculis/modules/vampires/code/objects/_vampire_object.dm b/modular_oculis/modules/vampires/code/objects/_vampire_object.dm new file mode 100644 index 000000000000..f20e4544d46c --- /dev/null +++ b/modular_oculis/modules/vampires/code/objects/_vampire_object.dm @@ -0,0 +1,92 @@ +/obj/structure/vampire + pass_flags_self = parent_type::pass_flags_self | LETPASSCLICKS + /// Who owns this structure? + var/datum/mind/owner + /* + * We use vars to add descriptions to items. + * This way we don't have to make a new /examine for each structure + * And it's easier to edit. + */ + var/ghost_desc + var/vampire_desc + var/vassal_desc + var/curator_desc + +/obj/structure/vampire/Destroy() + owner = null + return ..() + +/obj/structure/vampire/examine(mob/user) + . = ..() + if(isobserver(user) && ghost_desc) + . += span_cult(ghost_desc) + if(IS_VAMPIRE(user) && vampire_desc) + if(!owner) + . += span_cult("It is unsecured. Click on [src] while in your haven to secure it in place to get its full potential") + return + . += span_cult(vampire_desc) + if(IS_VASSAL(user) && vassal_desc) + . += span_cult(vassal_desc) + if(IS_CURATOR(user) && curator_desc) + . += span_cult(curator_desc) + +/// This handles bolting down the structure. +/obj/structure/vampire/proc/bolt(mob/user) + if(!user?.mind) + return + to_chat(user, span_danger("You have secured [src] in place.")) + to_chat(user, span_announce("* Vampire Tip: Examine [src] to understand how it functions!")) + user.playsound_local(null, 'sound/items/tools/ratchet.ogg', 70, FALSE, pressure_affected = FALSE) + set_anchored(TRUE) + owner = user.mind + +/// This handles unbolting of the structure. +/obj/structure/vampire/proc/unbolt(mob/user) + if(user) + to_chat(user, span_danger("You have unsecured [src].")) + user.playsound_local(null, 'sound/items/tools/ratchet.ogg', 70, FALSE, pressure_affected = FALSE) + set_anchored(FALSE) + owner = null + +/obj/structure/vampire/attackby(obj/item/item, mob/living/user, params) + /// If a Vampire tries to wrench it in place, yell at them. + if(item.tool_behaviour == TOOL_WRENCH && !anchored && IS_VAMPIRE(user)) + user.playsound_local(null, 'sound/machines/buzz/buzz-sigh.ogg', 40, FALSE, pressure_affected = FALSE) + to_chat(user, span_announce("* Vampire Tip: Examine vampire structures to understand how they function!")) + return + return ..() + +/obj/structure/vampire/attack_hand(mob/user, list/modifiers) + var/datum/antagonist/vampire/vampiredatum = IS_VAMPIRE(user) + /// Claiming the Rack instead of using it? + if(vampiredatum && !owner) + if(!vampiredatum.vampire_haven_area) + to_chat(user, span_danger("You don't have a haven. Claim a coffin to make that location your haven.")) + return FALSE + if(vampiredatum.vampire_haven_area != get_area(src)) + to_chat(user, span_danger("You may only activate this structure in your haven: [vampiredatum.vampire_haven_area].")) + return FALSE + + /// Radial menu for securing your Persuasion rack in place. + to_chat(user, span_notice("Do you wish to secure [src] here?")) + var/list/secure_options = list( + "Yes" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_yes"), + "No" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_no")) + var/secure_response = show_radial_menu(user, src, secure_options, radius = 36, require_near = TRUE) + if(secure_response == "Yes") + bolt(user) + return FALSE + return TRUE + +/obj/structure/vampire/click_alt(mob/user) + if(!owner || user != owner.current || !user.Adjacent(src)) + return NONE + balloon_alert(user, "unbolt [src]?") + var/list/unsecure_options = list( + "Yes" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_yes"), + "No" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_no"), + ) + var/unsecure_response = show_radial_menu(user, src, unsecure_options, radius = 36, require_near = TRUE) + if(unsecure_response == "Yes") + unbolt(user) + return CLICK_ACTION_SUCCESS diff --git a/modular_oculis/modules/vampires/code/objects/archive_of_the_kindred.dm b/modular_oculis/modules/vampires/code/objects/archive_of_the_kindred.dm new file mode 100644 index 000000000000..11b9d8f3f6f4 --- /dev/null +++ b/modular_oculis/modules/vampires/code/objects/archive_of_the_kindred.dm @@ -0,0 +1,91 @@ +/** + * # Archives of the Kindred: + * + * A book that can only be used by Curators and Monster Hnters. + * When used on a player, after a short timer, will reveal if the player is a Vampire, including their real name and Clan. + * This book should not work on Vampires using the Feign Life ability. + * If it reveals a Vampire, the Curator will then be able to tell they are a Vampire on examine (Like a vassal). + * Reading it normally will allow Curators to read what each Clan does, with some extra flavor text ones. + * + * Regular Vampires won't have any negative effects from the book, while everyone else will get burns/eye damage. + */ +/obj/item/book/kindred + name = "\improper Archive of the Kindred" + desc = "Cryptic documents explaining hidden truths behind Undead beings. It is said only Curators can decipher what they really mean." + icon = 'modular_oculis/modules/vampires/icons/vamp_obj.dmi' + lefthand_file = 'modular_oculis/modules/vampires/icons/bs_leftinhand.dmi' + righthand_file = 'modular_oculis/modules/vampires/icons/bs_rightinhand.dmi' + icon_state = "kindred_book" + starting_title = "the Archive of the Kindred" + starting_author = "dozens of generations of Curators" + unique = TRUE + cannot_carve = TRUE + throw_speed = 1 + throw_range = 10 + resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | ACID_PROOF + +/* /obj/item/book/kindred/Initialize(mapload) + . = ..() + AddComponent(/datum/component/stationloving) */ + +///Attacking someone with the book. +/obj/item/book/kindred/interact_with_atom(mob/target, mob/living/user, list/modifiers) + if(!ismob(target) || user == target || !user.can_read(src)) + return NONE + if(DOING_INTERACTION(user, DOAFTER_SOURCE_ARCHIVE_OF_THE_KINDRED)) + return ITEM_INTERACT_BLOCKING + if(!IS_CURATOR(user)) + if(!IS_VAMPIRE(user)) + to_chat(user, span_warning("[src] burns your hands as you try to use it!")) + user.apply_damage(3, BURN, pick(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM)) + else + to_chat(user, span_notice("[src] seems to be too complicated for you. It would be best to leave this for someone else to take.")) + return ITEM_INTERACT_BLOCKING + + to_chat(user, span_notice("You begin carefully examining [target] while consulting [src]...")) + user.visible_message(span_notice("[user] looks at [target] while reading [src]."), ignored_mobs = list(user)) + if(!do_after(user, 3 SECONDS, target, interaction_key = DOAFTER_SOURCE_ARCHIVE_OF_THE_KINDRED)) + to_chat(user, span_notice("You quickly close [src].")) + return ITEM_INTERACT_SUCCESS + + var/datum/antagonist/vampire/vampiredatum = IS_VAMPIRE(target) + // Are we a Vampire | Are we on Masquerade. If one is true, they will fail. + if(vampiredatum && !HAS_TRAIT(target, TRAIT_FEIGN_LIFE)) + if(vampiredatum.broke_masquerade) + to_chat(user, span_warning("[target], also known as '[vampiredatum.return_full_name()]', is indeed a Vampire, but you already knew this.")) + return ITEM_INTERACT_SUCCESS + to_chat(user, span_warning("[target], also known as '[vampiredatum.return_full_name()]', [vampiredatum.my_clan ? "is part of the [vampiredatum.my_clan]!" : "is not part of a clan."] You quickly note this information down, memorizing it.")) + user.log_message("used [src] to break [key_name(target)]'s masquerade.", LOG_ATTACK, color = "red") + target.log_message("had their masquerade broken by [key_name(user)] with [src].", LOG_VICTIM, color = "orange", log_globally = FALSE) + vampiredatum.break_masquerade() + else + to_chat(user, span_notice("You fail to draw any conclusions to [target] being a Vampire.")) + return ITEM_INTERACT_SUCCESS + +/obj/item/book/kindred/ranged_interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers) + return interact_with_atom(interacting_with, user, modifiers) + +/obj/item/book/kindred/attack_self(mob/living/user) + if(!IS_CURATOR(user)) + if(IS_VAMPIRE(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("You feel your eyes unable to read the boring texts...")) + return + ui_interact(user) + +/obj/item/book/kindred/ui_interact(mob/living/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "KindredBook", name) + ui.set_autoupdate(FALSE) + ui.open() + +/obj/item/book/kindred/ui_static_data(mob/user) + . = list("clans" = list()) + + for(var/datum/vampire_clan/clan as anything in subtypesof(/datum/vampire_clan)) + .["clans"] += list(list( + "name" = clan::name, + "desc" = clan::description, + )) diff --git a/modular_oculis/modules/vampires/code/objects/blood_throne.dm b/modular_oculis/modules/vampires/code/objects/blood_throne.dm new file mode 100644 index 000000000000..3717605b3f49 --- /dev/null +++ b/modular_oculis/modules/vampires/code/objects/blood_throne.dm @@ -0,0 +1,113 @@ +/// Blood Throne - Allows Vampires to remotely speak with their vassals. - Code (Mostly) stolen from comfy chairs (armrests) and chairs (layers) +/obj/structure/vampire/bloodthrone + name = "blood throne" + desc = "Twisted metal shards jut from the arm rests. Very uncomfortable looking. It would take a masochistic sort to sit on this jagged piece of furniture." + icon = 'modular_oculis/modules/vampires/icons/vamp_obj_64.dmi' + icon_state = "throne" + buckle_lying = 0 + anchored = FALSE + density = TRUE + can_buckle = TRUE + ghost_desc = "This is a blood throne. Any vampire sitting on it can remotely speak to all other vampires by attempting to speak aloud." + vampire_desc = "This is a blood throne. Sitting on it will allow you to communicate telepathically to all other vampires by simply speaking." + vassal_desc = "This is a blood throne. It allows your master to telepathically speak to all other vampires." + curator_desc = "This is a chair that hurts those who 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 each other." + custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 11) + +// Add rotating and armrest +/obj/structure/vampire/bloodthrone/Initialize(mapload) + AddElement(/datum/element/simple_rotation) + return ..() + +// Rotating +/obj/structure/vampire/bloodthrone/setDir(newdir) + . = ..() + for(var/mob/living/buckled in buckled_mobs) + buckled.setDir(newdir) + + if(has_buckled_mobs() && dir == NORTH) + layer = ABOVE_MOB_LAYER + else + layer = OBJ_LAYER + +/obj/structure/vampire/bloodthrone/update_overlays() + . = ..() + if(has_buckled_mobs()) + var/mutable_appearance/armrest = mutable_appearance('modular_oculis/modules/vampires/icons/vamp_obj_64.dmi', "thronearm") + armrest.layer = ABOVE_MOB_LAYER + . += armrest + +// Buckling +/obj/structure/vampire/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 + set_density(FALSE) + . = ..() + set_density(TRUE) + user.visible_message( + span_notice("[user] sits down on \the [src]."), + span_boldnotice("You sit down onto [src]."), + ) + if(IS_VAMPIRE(user)) + RegisterSignal(user, COMSIG_MOB_SAY, PROC_REF(handle_speech)) + else + unbuckle_mob(user) + user.Paralyze(10 SECONDS) + to_chat(user, span_cult("The power of the blood throne overwhelms you!")) + +/obj/structure/vampire/bloodthrone/post_buckle_mob(mob/living/target) + update_appearance(UPDATE_OVERLAYS) + target.pixel_z += 2 + +// Unbuckling +/obj/structure/vampire/bloodthrone/unbuckle_mob(mob/living/user, force = FALSE, can_fall = TRUE) + visible_message(span_danger("[user] unbuckles [user.p_them()]self from \the [src].")) + UnregisterSignal(user, COMSIG_MOB_SAY) + return ..() + +/obj/structure/vampire/bloodthrone/post_unbuckle_mob(mob/living/target) + target.pixel_z -= 2 + update_appearance(UPDATE_OVERLAYS) + +// The speech itself +/obj/structure/vampire/bloodthrone/proc/handle_speech(datum/source, list/speech_args) + SIGNAL_HANDLER + + if(speech_args[SPEECH_FORCED]) + return + + var/mob/living/carbon/human/user = source + var/datum/antagonist/vampire/vampire_datum = IS_VAMPIRE(user) + if(!vampire_datum) + CRASH("Non-vampire speaking on blood throne somehow!") + + var/rank_icon_state = "vampire" + if(vampire_datum.prince) + rank_icon_state = "prince" + else if(vampire_datum.scourge) + rank_icon_state = "scourge" + + var/icon_html = "" + var/message = speech_args[SPEECH_MESSAGE] + var/rendered = span_cult_large("[icon_html] [user.real_name]: [message]") + user.log_talk(message, LOG_SAY, tag = ROLE_VAMPIRE) + + for(var/datum/antagonist/vampire/receiver as anything in GLOB.all_vampires) + if(!receiver.owner.current) + continue + var/mob/receiver_mob = receiver.owner.current + to_chat(receiver_mob, rendered, type = MESSAGE_TYPE_RADIO, avoid_highlighting = receiver_mob == user) + + for(var/datum/antagonist/vassal/vassal as anything in vampire_datum.vassals) + if(!vassal.owner.current) + continue + var/mob/receiver_mob = vassal.owner.current + to_chat(receiver_mob, rendered, type = MESSAGE_TYPE_RADIO) + + for(var/mob/dead_mob in GLOB.dead_mob_list) + var/link = FOLLOW_LINK(dead_mob, user) + to_chat(dead_mob, "[link] [rendered]", type = MESSAGE_TYPE_RADIO) + + speech_args[SPEECH_MESSAGE] = "" diff --git a/modular_oculis/modules/vampires/code/objects/candelabrum.dm b/modular_oculis/modules/vampires/code/objects/candelabrum.dm new file mode 100644 index 000000000000..b70e08bd39ab --- /dev/null +++ b/modular_oculis/modules/vampires/code/objects/candelabrum.dm @@ -0,0 +1,160 @@ +/obj/structure/vampire/candelabrum + name = "candelabrum" + desc = "It burns slowly, but doesn't radiate any heat." + icon = 'modular_oculis/modules/vampires/icons/vamp_obj.dmi' + icon_state = "candelabrum" + base_icon_state = "candelabrum" + light_color = "#66FFFF" + light_power = 3 + light_range = 2 + light_on = FALSE + density = FALSE + anchored = FALSE + ghost_desc = "This is a magical candle which drains the sanity of non-vampires and non-vassals." + vampire_desc = "This is a magical candle which drains the sanity of mortals who are not under your command while it is active." + vassal_desc = "This is a magical candle which drains the sanity of the fools who haven't yet accepted your master." + curator_desc = "This is a blue Candelabrum, which causes insanity to those near it while active." + custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 2.5) + var/lit = FALSE + +/obj/structure/vampire/candelabrum/Initialize(mapload) + . = ..() + RegisterSignal(src, COMSIG_CLICK, PROC_REF(distance_toggle)) + update_appearance() + register_context() + +/obj/structure/vampire/candelabrum/Destroy() + STOP_PROCESSING(SSobj, src) + return ..() + +/obj/structure/vampire/candelabrum/add_context(atom/source, list/context, obj/item/held_item, mob/user) + . = NONE + if(held_item) + return NONE + if(!anchored) + if(Adjacent(user) && IS_VAMPIRE(user)) + context[SCREENTIP_CONTEXT_LMB] = "Bolt" + return CONTEXTUAL_SCREENTIP_SET + return NONE + + if(!HAS_MIND_TRAIT(user, TRAIT_VAMPIRE_ALIGNED)) + return NONE + + var/is_full_vampire = IS_VAMPIRE(user) + if(Adjacent(user) || is_full_vampire) + context[SCREENTIP_CONTEXT_LMB] = lit ? "Extinguish" : "Light" + . = CONTEXTUAL_SCREENTIP_SET + + if(is_full_vampire) + context[SCREENTIP_CONTEXT_RMB] = lit ? "Extinguish All Nearby" : "Light All Nearby" + . = CONTEXTUAL_SCREENTIP_SET + + +/obj/structure/vampire/candelabrum/update_icon_state() + icon_state = "[base_icon_state][lit ? "_lit" : ""]" + return ..() + +/obj/structure/vampire/candelabrum/update_overlays() + . = ..() + if(lit) + . += emissive_appearance(icon, "[base_icon_state]_emissive", src) + +/obj/structure/vampire/candelabrum/update_desc(updates) + if(lit) + desc = initial(desc) + else + desc = "Despite not being lit, it makes your skin crawl." + return ..() + +/obj/structure/vampire/candelabrum/bolt() + set_density(TRUE) + return ..() + +/obj/structure/vampire/candelabrum/unbolt() + set_density(FALSE) + return ..() + +/obj/structure/vampire/candelabrum/set_anchored(anchorvalue) + . = ..() + if(!anchored) + set_lit(FALSE) + +/obj/structure/vampire/candelabrum/attack_hand(mob/living/user, list/modifiers) + if(!..()) + return + if(anchored && HAS_MIND_TRAIT(user, TRAIT_VAMPIRE_ALIGNED)) + set_lit(!lit) + return ..() + +/obj/structure/vampire/candelabrum/attack_hand_secondary(mob/user, list/modifiers) + . = ..() + if(. == SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN) + return + + if(!anchored || !IS_VAMPIRE(user)) + return + + user.balloon_alert_to_viewers("gestures dramatically") + dramatic_toggle_all(user, !lit) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + +/obj/structure/vampire/candelabrum/proc/dramatic_toggle_all(mob/user, value) + var/turf/our_turf = get_turf(src) + var/list/nearby_candels = list() + for(var/obj/structure/vampire/candelabrum/candel in view(7, src) | view(user)) + if(!candel.anchored || candel.lit == value) + continue + nearby_candels[candel] = get_dist(our_turf, get_turf(candel)) + + if(!length(nearby_candels)) + return + + sortTim(nearby_candels, cmp = value ? GLOBAL_PROC_REF(cmp_numeric_asc) : GLOBAL_PROC_REF(cmp_numeric_dsc), associative = TRUE) + + // maximum aurafarming + user.visible_message( + span_notice("[user] waves [user.p_their()] hand around, [value ? "igniting" : "extinguishing"] nearby candelabrums with a single gesture."), + span_notice("You wave your hand around, [value ? "igniting" : "extinguishing"] nearby candelabrums with a single gesture."), + ) + for(var/i = 1 to length(nearby_candels)) + var/obj/structure/vampire/candelabrum/candel = nearby_candels[i] + addtimer(CALLBACK(candel, PROC_REF(set_lit), value), i * (0.2 SECONDS), TIMER_UNIQUE | TIMER_OVERRIDE) + +/obj/structure/vampire/candelabrum/proc/distance_toggle(datum/source, atom/location, control, params, mob/user) + SIGNAL_HANDLER + if(!anchored || user.incapacitated || user.get_active_held_item() || !IS_VAMPIRE(user) || user.Adjacent(src)) + return + var/list/modifiers = params2list(params) + user.balloon_alert_to_viewers("gestures dramatically") + if(LAZYACCESS(modifiers, RIGHT_CLICK)) + dramatic_toggle_all(user, !lit) + else + set_lit(!lit) + // gotta aurafarm + user.visible_message( + span_notice("[user] waves [user.p_their()] hand towards \the [src], causing its cold flame to [lit ? "ignite" : "extinguish"]."), + span_notice("You wave your hand towards \the [src], [lit ? "igniting" : "extinguishing"] it."), + ) + +/obj/structure/vampire/candelabrum/proc/set_lit(value) + if(lit == value) + return + lit = value + if(lit) + set_light_on(TRUE) + START_PROCESSING(SSobj, src) + else + set_light_on(FALSE) + STOP_PROCESSING(SSobj, src) + update_light() + update_appearance() + +/obj/structure/vampire/candelabrum/process() + if(!lit) + return PROCESS_KILL + for(var/mob/living/carbon/nearby_people in viewers(7, src)) + /// We don't want vampires or vassals affected by this + if(HAS_MIND_TRAIT(nearby_people, TRAIT_VAMPIRE_ALIGNED) || IS_CURATOR(nearby_people)) + continue + nearby_people.set_hallucinations_if_lower(10 SECONDS) + nearby_people.add_mood_event("vampcandle", /datum/mood_event/vampcandle) diff --git a/modular_oculis/modules/vampires/code/objects/coffin_variants.dm b/modular_oculis/modules/vampires/code/objects/coffin_variants.dm new file mode 100644 index 000000000000..ac4a850b225b --- /dev/null +++ b/modular_oculis/modules/vampires/code/objects/coffin_variants.dm @@ -0,0 +1,99 @@ +/obj/structure/closet/crate/coffin/blackcoffin + name = "black coffin" + desc = "For those departed who are not so dear." + icon_state = "coffin" + base_icon_state = "coffin" + icon = 'modular_oculis/modules/vampires/icons/vamp_obj.dmi' + open_sound = 'modular_oculis/modules/vampires/sound/coffin_open.ogg' + close_sound = 'modular_oculis/modules/vampires/sound/coffin_close.ogg' + breakout_time = 30 SECONDS + pry_lid_timer = 20 SECONDS + resistance_flags = NONE + material_drop = /obj/item/stack/sheet/iron + material_drop_amount = 2 + armor_type = /datum/armor/blackcoffin + custom_materials = list(/datum/material/wood = SHEET_MATERIAL_AMOUNT * 5, /datum/material/iron = SHEET_MATERIAL_AMOUNT) + +/datum/armor/blackcoffin + melee = 50 + bullet = 20 + laser = 30 + bomb = 50 + fire = 70 + acid = 60 + +/obj/structure/closet/crate/coffin/securecoffin + name = "secure coffin" + desc = "For those too scared of having their place of rest disturbed." + icon_state = "securecoffin" + base_icon_state = "securecoffin" + icon = 'modular_oculis/modules/vampires/icons/vamp_obj.dmi' + open_sound = 'modular_oculis/modules/vampires/sound/coffin_open.ogg' + close_sound = 'modular_oculis/modules/vampires/sound/coffin_close.ogg' + breakout_time = 35 SECONDS + pry_lid_timer = 35 SECONDS + resistance_flags = FIRE_PROOF | LAVA_PROOF | ACID_PROOF + material_drop = /obj/item/stack/sheet/iron + material_drop_amount = 2 + armor_type = /datum/armor/securecoffin + custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 5.5, /datum/material/alloy/plasteel = SHEET_MATERIAL_AMOUNT * 5) + +/datum/armor/securecoffin + melee = 35 + bullet = 20 + laser = 20 + bomb = 100 + fire = 100 + acid = 100 + +/obj/structure/closet/crate/coffin/meatcoffin + name = "meat coffin" + desc = "When you're ready to meat your maker, the steaks can never be too high." + icon_state = "meatcoffin" + base_icon_state = "meatcoffin" + icon = 'modular_oculis/modules/vampires/icons/vamp_obj.dmi' + resistance_flags = FIRE_PROOF + open_sound = 'sound/effects/footstep/slime1.ogg' + close_sound = 'sound/effects/footstep/slime1.ogg' + breakout_time = 25 SECONDS + pry_lid_timer = 20 SECONDS + material_drop = /obj/item/food/meat/slab/human + material_drop_amount = 3 + armor_type = /datum/armor/meatcoffin + custom_materials = list( + /datum/material/meat = SHEET_MATERIAL_AMOUNT * 20, + /datum/material/iron = SMALL_MATERIAL_AMOUNT * 1.5, + /datum/material/glass = SMALL_MATERIAL_AMOUNT * 1.5, + ) + +/datum/armor/meatcoffin + melee = 70 + bullet = 10 + laser = 10 + bomb = 70 + fire = 70 + acid = 60 + +/obj/structure/closet/crate/coffin/metalcoffin + name = "metal coffin" + desc = "A big metal sardine can inside of another big metal sardine can, in space." + icon_state = "metalcoffin" + base_icon_state = "metalcoffin" + icon = 'modular_oculis/modules/vampires/icons/vamp_obj.dmi' + resistance_flags = FIRE_PROOF | LAVA_PROOF + open_sound = 'sound/effects/pressureplate.ogg' + close_sound = 'sound/effects/pressureplate.ogg' + breakout_time = 25 SECONDS + pry_lid_timer = 30 SECONDS + material_drop = /obj/item/stack/sheet/iron + material_drop_amount = 5 + armor_type = /datum/armor/metalcoffin + custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 7) + +/datum/armor/metalcoffin + melee = 40 + bullet = 15 + laser = 50 + bomb = 10 + fire = 70 + acid = 60 diff --git a/modular_oculis/modules/vampires/code/objects/stakes.dm b/modular_oculis/modules/vampires/code/objects/stakes.dm new file mode 100644 index 000000000000..ed867b1b0ea7 --- /dev/null +++ b/modular_oculis/modules/vampires/code/objects/stakes.dm @@ -0,0 +1,150 @@ +/obj/item/stake + name = "wooden stake" + desc = "A simple wooden stake carved to a sharp point." + icon = 'modular_oculis/modules/vampires/icons/stakes.dmi' + icon_state = "wood" + inhand_icon_state = "wood" + lefthand_file = 'modular_oculis/modules/vampires/icons/bs_leftinhand.dmi' + righthand_file = 'modular_oculis/modules/vampires/icons/bs_rightinhand.dmi' + slot_flags = ITEM_SLOT_POCKETS + hitsound = 'sound/items/weapons/bladeslice.ogg' + attack_verb_continuous = list("staked", "stabbed", "tore into") + attack_verb_simple = list("staked", "stabbed", "tore into") + sharpness = SHARP_POINTY + force = 6 + throwforce = 10 + max_integrity = 30 + embed_type = /datum/embedding/stake + custom_materials = list(/datum/material/wood = SHEET_MATERIAL_AMOUNT * 3) + ///Time it takes to embed the stake into someone's chest. + var/staketime = 4 SECONDS + +/obj/item/stake/attack(mob/living/target, mob/living/user, params) + . = ..() + if(.) + return + + if(DOING_INTERACTION_WITH_TARGET(user, target)) + return + // Cannot target yourself, must be in combat mode and targeting the chest + if(target == user) + return + if(!user.combat_mode) + return + if(check_zone(user.zone_selected) != BODY_ZONE_CHEST) + return + + if(HAS_TRAIT(target, TRAIT_BEINGSTAKED)) + to_chat(user, span_notice("[target] is already having a stake driven into [target.p_their()] chest!")) + return + + // lol, cry about it + if(HAS_TRAIT(target, TRAIT_PIERCEIMMUNE)) + to_chat(user, span_notice("[target]'s chest is too thick! [src] won't go in!")) + return + + // Cannot have something in your chest + var/obj/item/bodypart/chest = target.get_bodypart(BODY_ZONE_CHEST) + if(!chest) + return + for(var/obj/item/embedded_object in chest.embedded_objects) + to_chat(user, span_boldannounce("[target]'s chest already has [embedded_object] inside of it!")) + return + + playsound(target, 'sound/effects/magic/demon_consume.ogg', vol = 50, vary = TRUE) + to_chat(target, span_userdanger("[user] is driving a stake into your chest!")) + to_chat(user, span_notice("You put all your weight into embedding [src] into [target]'s chest...")) + + ADD_TRAIT(target, TRAIT_BEINGSTAKED, TRAIT_VAMPIRE) + if(!do_after(user, staketime, target)) + REMOVE_TRAIT(target, TRAIT_BEINGSTAKED, TRAIT_VAMPIRE) + return + + REMOVE_TRAIT(target, TRAIT_BEINGSTAKED, TRAIT_VAMPIRE) + + // Actually embed the stake and apply damage + if(!force_embed(target, target.get_bodypart(BODY_ZONE_CHEST))) + return + + target.apply_damage(force * 2, BRUTE, BODY_ZONE_CHEST) + + playsound(target, 'sound/effects/splat.ogg', vol = 40, vary = TRUE) + user.visible_message( + span_danger("[user] drives the [src] into [target]'s chest!"), + span_danger("You drive the [src] into [target]'s chest!"), + ) + + var/datum/antagonist/vampire/vampire_datum = IS_VAMPIRE(target) + if(vampire_datum) + if(HAS_TRAIT(target, TRAIT_TORPOR) || target.stat >= UNCONSCIOUS) + vampire_datum.final_death() + else + to_chat(target, span_userdanger("You have been staked! Your powers are useless while it's in your chest!")) + target.balloon_alert(target, "you have been staked!") + +/obj/item/stake/examine(mob/user) + . = ..() + . += span_notice("To stake someone: Target the chest, activate combat mode, and hit them.") + . += span_notice("* Hunter Tip: Remember that they can just pull it out if they are awake. Cuff them or kill them. A stake will stop them from reviving, not from regenerating. It will also stop all of their abilities.") + +///Can this target be staked? If someone stands up before this is complete, it fails. Best used on someone stationary. +/obj/item/stake/proc/can_be_staked(mob/living/carbon/target) + if(!istype(target)) + return FALSE + if(!(target.mobility_flags & MOBILITY_MOVE)) + return TRUE + return FALSE + +/// Created by welding and acid-treating a simple stake. +/obj/item/stake/hardened + name = "hardened stake" + desc = "A wooden stake carved to a sharp point and hardened by fire." + icon_state = "hardened" + force = 8 + throwforce = 12 + armour_penetration = 10 + staketime = 3 SECONDS + custom_materials = list(/datum/material/iron = HALF_SHEET_MATERIAL_AMOUNT) + +/obj/item/stake/hardened/silver + name = "silver stake" + desc = "Polished and sharp at the end. For when some mofo is always trying to iceskate uphill." + icon_state = "silver" + inhand_icon_state = "silver" + siemens_coefficient = 1 + force = 9 + armour_penetration = 25 + staketime = 2 SECONDS + custom_materials = list(/datum/material/silver = SHEET_MATERIAL_AMOUNT, /datum/material/iron = HALF_SHEET_MATERIAL_AMOUNT) + +/obj/item/stack/sheet/mineral/wood/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(!tool.get_sharpness()) + return NONE + 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)) + return ITEM_INTERACT_BLOCKING + // Make Stake + var/obj/item/stake/new_item = new(user.drop_location()) + 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) + return ITEM_INTERACT_SUCCESS + +/datum/embedding/stake + embed_chance = 100 + fall_chance = 0 + rip_time = 2.5 SECONDS // this is actually 5 seconds because it gets multiplied by the w_class diff --git a/modular_oculis/modules/vampires/code/objects/vassal_rack.dm b/modular_oculis/modules/vampires/code/objects/vassal_rack.dm new file mode 100644 index 000000000000..1cc8256b8359 --- /dev/null +++ b/modular_oculis/modules/vampires/code/objects/vassal_rack.dm @@ -0,0 +1,253 @@ +/obj/structure/vampire/vassalrack + name = "vassalization rack" + desc = "If this wasn't meant for brainwashing, then someone has some fairly horrifying hobbies." + icon = 'modular_oculis/modules/vampires/icons/vamp_obj.dmi' + icon_state = "vassalrack" + anchored = FALSE + density = TRUE + can_buckle = TRUE + buckle_lying = 180 + ghost_desc = "This is a vassalization rack, which allows vampires to turn crew members into loyal vassals." + vampire_desc = "This is the vassalization rack, which allows you to turn crew members into loyal vassals in your service. This costs blood to do.\n\ + Simply click and hold on a victim, and then drag their sprite onto the vassalization rack. Right-click on the vassalization rack to unbuckle them.\n\ + To convert into a vassal, repeatedly click on the vassalization rack." + vassal_desc = "This is the vassalization rack, which allows your master to turn crew members into loyal vassals.\n\ + Aid your master in bringing their victims here and keeping them secure.\n\ + You can secure victims to the vassalization rack by click-dragging the victim onto the rack while it is secured." + curator_desc = "This is the vassalization rack, which monsters use to blood-slave crew members into vassals.\n\ + They usually ensure that victims are handcuffed to prevent them from running away.\n\ + Their rituals take time, allowing us to disrupt them." + + custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 8) + + /// How many times a buckled person has to be interacted with to be converted. + var/convert_progress = 3 + /// Mindshielded individuals and antagonists must willingly accept you as their master. + var/wants_vassalization = FALSE + /// Prevents popup spam. + var/vassalization_offered = FALSE + /// No spamming vassalization + var/in_progress = FALSE + +/obj/structure/vampire/vassalrack/examine(mob/user) + . = ..() + var/datum/antagonist/vampire/vampiredatum = IS_VAMPIRE(user) + if(vampiredatum) + var/remaining_vassals = max_vampire_vassals() - vampiredatum.count_vassals() + if(remaining_vassals > 0) + . += span_info("You are currently capable of creating [remaining_vassals] more vassal[remaining_vassals == 1 ? "" : "s"].") + else + . += span_warning("You cannot create any more vassals at the moment!") + +/obj/structure/vampire/vassalrack/atom_deconstruct(disassembled = TRUE) + new /obj/item/stack/sheet/iron(src.loc, 4) + new /obj/item/stack/rods(loc, 4) + +/obj/structure/vampire/vassalrack/mouse_drop_receive(atom/dropped, mob/user, params) + if(DOING_INTERACTION(user, DOAFTER_SOURCE_PERSUASION_RACK)) + return + var/mob/living/living_target = dropped + if(!anchored && IS_VAMPIRE(user)) + to_chat(user, span_danger("Until this rack is secured in place, it cannot serve its purpose.")) + to_chat(user, span_announce("* Vampire Tip: Examine the vassal rack to understand how it functions!")) + return + // Default checks + if(!isliving(living_target) || !living_target.Adjacent(src) || living_target == user || !isliving(user) || has_buckled_mobs() || user.incapacitated || living_target.buckled) + return + // Don't buckle Silicon to it please. + if(issilicon(living_target)) + to_chat(user, span_danger("You realize that this machine cannot be vassalized, therefore it is useless to buckle [living_target.p_them()].")) + return + if(do_after(user, 5 SECONDS, living_target, interaction_key = DOAFTER_SOURCE_PERSUASION_RACK)) + set_density(FALSE) + attach_victim(living_target, user) + set_density(TRUE) + +/** + * Attempts to buckle target into the Vassalization Rack + */ +/obj/structure/vampire/vassalrack/proc/attach_victim(mob/living/target, mob/living/user) + if(!buckle_mob(target)) + return + user.visible_message( + span_notice("[user] straps [target] into the rack, immobilizing [target.p_them()]."), + span_boldnotice("You secure [target] tightly in place. [target.p_They()] won't escape you now.")) + + playsound(loc, 'sound/effects/pop_expl.ogg', vol = 25, vary = TRUE) + update_appearance(UPDATE_ICON) + + // Set up vassalization stuff now + reset_progress() + +/// Attempt Unbuckle +/obj/structure/vampire/vassalrack/user_unbuckle_mob(mob/living/buckled_mob, mob/user) + if(HAS_MIND_TRAIT(user, TRAIT_VAMPIRE_ALIGNED)) + return ..() + + if(buckled_mob == user) + buckled_mob.visible_message( + span_danger("[user] tries to release [user.p_them()]self from the rack!"), + span_danger("You attempt to release yourself from the rack!"), + span_hear("You hear a squishy wet noise."), + ) + if(!do_after(user, 20 SECONDS, buckled_mob)) + return FALSE + else + buckled_mob.visible_message( + span_danger("[user] tries to pull [buckled_mob] from the rack!"), + span_danger("You attempt to release [buckled_mob] from the rack!"), + span_hear("You hear a squishy wet noise."), + ) + if(!do_after(user, 10 SECONDS, buckled_mob)) + return FALSE + + return ..() + +/obj/structure/vampire/vassalrack/post_unbuckle_mob(mob/living/unbuckled_mob) + visible_message(span_danger("[unbuckled_mob][unbuckled_mob.stat == DEAD ? "'s corpse" : ""] slides off of the rack.")) + unbuckled_mob.Paralyze(2 SECONDS) + update_appearance(UPDATE_ICON) + reset_progress() + +/obj/structure/vampire/vassalrack/attack_hand(mob/user, list/modifiers) + . = ..() + if(!. || !has_buckled_mobs() || DOING_INTERACTION(user, DOAFTER_SOURCE_PERSUASION_RACK)) + return FALSE + + var/datum/antagonist/vampire/vampiredatum = IS_VAMPIRE(user) + var/mob/living/carbon/buckled_person = pick(buckled_mobs) + + // oh no let me free this poor soul + if(!vampiredatum) + user_unbuckle_mob(buckled_person, user) + return TRUE + + try_to_progress(user, buckled_person) + +/obj/structure/vampire/vassalrack/attack_hand_secondary(mob/user, list/modifiers) + . = ..() + if(. == SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN) + return + if(!has_buckled_mobs() || !isliving(user) || DOING_INTERACTION(user, DOAFTER_SOURCE_PERSUASION_RACK)) + return + var/mob/living/carbon/buckled_carbons = pick(buckled_mobs) + if(buckled_carbons) + if(user == owner.current) + unbuckle_mob(buckled_carbons) + else + user_unbuckle_mob(buckled_carbons, user) + +/** + * Conversion steps: + * + * * When convert_progress reaches 0, the victim is ready to be converted + * * If the victim has a mindshield or is an antagonist, they must accept the conversion. If they don't accept, they aren't converted + * * vassalize target + */ +/obj/structure/vampire/vassalrack/proc/try_to_progress(mob/living/living_vampire, mob/living/living_target) + if(DOING_INTERACTION(living_vampire, DOAFTER_SOURCE_PERSUASION_RACK)) + return + + if(vassalization_offered) + balloon_alert(living_vampire, "wait a moment!") + return + + var/datum/antagonist/vampire/vampiredatum = IS_VAMPIRE(living_vampire) + + if(!vampiredatum.can_make_vassal(living_target) || in_progress) + return + + // These if statements can be simplified but aren't for better code-readability. + if(convert_progress > 0) + balloon_alert(living_vampire, "catering blood...") + + in_progress = TRUE + living_target.Paralyze(1 SECONDS) + vampiredatum.adjust_blood_volume(-VASSALIZATION_BLOOD_HALF_COST) + + if(!attempt_progress(living_vampire, living_target)) + in_progress = FALSE + return + in_progress = FALSE + + vampiredatum.adjust_blood_volume(-VASSALIZATION_BLOOD_HALF_COST) + convert_progress-- + + if(convert_progress > 0) + balloon_alert(living_vampire, "needs more persuasion...") + return + + // If the victim is mindshielded or an antagonist, they choose to accept or refuse vassilization. + if(!wants_vassalization && (HAS_TRAIT(living_target, TRAIT_UNCONVERTABLE) || living_target.is_antag())) + balloon_alert(living_vampire, "has external loyalties! more persuasion required!") + if(!ask_for_vassalization(living_vampire, living_target)) + balloon_alert(living_vampire, "refused persuasion!") + convert_progress++ + return + + balloon_alert(living_vampire, "ready for communion!") + return + + if(wants_vassalization || !(HAS_TRAIT(living_target, TRAIT_UNCONVERTABLE) || living_target.is_antag())) + living_vampire.balloon_alert_to_viewers("smears blood...", "paints bloody marks...") + if(!do_after(living_vampire, 5 SECONDS, living_target, interaction_key = DOAFTER_SOURCE_PERSUASION_RACK)) + balloon_alert(living_vampire, "interrupted!") + return + + // Make our target into a vassal + vampiredatum.adjust_blood_volume(-VASSALIZATION_CONVERSION_COST) + if(vampiredatum.make_vassal(living_target)) + // We've made a vassal the proper way, do clan stuff + vampiredatum.my_clan?.on_vassal_made(living_vampire, living_target) + vampiredatum.rank_up(1, ignore_reqs = TRUE, increase_goal = FALSE) + remove_loyalties(living_target) + +/obj/structure/vampire/vassalrack/proc/attempt_progress(mob/living/user, mob/living/carbon/target) + if(do_after(user, 5 SECONDS, target, interaction_key = DOAFTER_SOURCE_PERSUASION_RACK)) + target.visible_message( + span_danger("[user] performs a ritual, catering some of [user.p_their()] blood to [target]!"), + span_userdanger("[user] feeds some of [user.p_their()] blood to you! " + span_awe("You feel as if your mind is slipping away...?")) + ) + target.set_jitter_if_lower(10 SECONDS) + return TRUE + else + balloon_alert(user, "interrupted!") + return FALSE + +/// Offer them the oppertunity to join now. +/obj/structure/vampire/vassalrack/proc/ask_for_vassalization(mob/living/user, mob/living/target) + if(vassalization_offered) + balloon_alert(user, "wait a moment!") + return FALSE + vassalization_offered = TRUE + + to_chat(user, span_notice("[target] has been given the opportunity for servitude. You await [target.p_their()] decision...")) + var/alert_response = tgui_alert( + user = target, \ + message = "You are being brainwashed! Do you want to give into the addiction to the blood of [user]? \n\ + You will not lose your current objectives, but they come second to the will of your new master!", \ + title = "Give into the addiction?", + buttons = list("Accept", "Refuse"), + timeout = 15 SECONDS, \ + autofocus = TRUE + ) + if(alert_response == "Accept") + wants_vassalization = TRUE + else + target.balloon_alert_to_viewers("refused vassalization!") + + vassalization_offered = FALSE + + return wants_vassalization + +/obj/structure/vampire/vassalrack/proc/reset_progress() + convert_progress = initial(convert_progress) + wants_vassalization = initial(wants_vassalization) + vassalization_offered = initial(vassalization_offered) + in_progress = initial(in_progress) + +/obj/structure/vampire/vassalrack/proc/remove_loyalties(mob/living/target) + // Find Mind Implant & Destroy + for(var/obj/item/implant/implant as anything in target.implants) + if(istype(implant, /obj/item/implant/mindshield) && implant.removed(target, silent = TRUE)) + qdel(implant) diff --git a/modular_oculis/modules/vampires/code/phobetor.dm b/modular_oculis/modules/vampires/code/phobetor.dm new file mode 100644 index 000000000000..bdbe9dccd149 --- /dev/null +++ b/modular_oculis/modules/vampires/code/phobetor.dm @@ -0,0 +1,179 @@ +/** + * # Phobetor Brain Trauma + * + * Beefmen's Brain trauma, causing phobetor tears to traverse through. + */ + +/datum/brain_trauma/special/bluespace_prophet/phobetor + name = "Sleepless Dreamer" + desc = "The patient, after undergoing untold psychological hardship, believes they can travel between the dreamscapes of this dimension." + scan_desc = "awoken sleeper" + gain_text = span_notice("Your mind snaps, and you wake up. You really wake up.") + lose_text = span_warning("You succumb once more to the sleepless dream of the unwoken.") + random_gain = FALSE + known_trauma = FALSE + + ///Created tears, only checking the FIRST one, not the one it's created to link to. + var/list/created_firsts = list() + +///When the trauma is removed from a mob. +/datum/brain_trauma/special/bluespace_prophet/phobetor/on_lose(silent) + QDEL_LIST(created_firsts) + return ..() + +/datum/brain_trauma/special/bluespace_prophet/phobetor/on_life(seconds_per_tick, times_fired) + if(!COOLDOWN_FINISHED(src, portal_cooldown)) + return + COOLDOWN_START(src, portal_cooldown, 10 SECONDS) + var/list/turf/possible_tears = list() + for(var/turf/nearby_turfs as anything in RANGE_TURFS(8, owner)) + if(nearby_turfs.density) + continue + possible_tears += nearby_turfs + if(!LAZYLEN(possible_tears)) + return + + var/turf/first_tear + var/turf/second_tear + first_tear = return_valid_floor_in_range(owner, 6, 0, TRUE) + if(!first_tear) + return + second_tear = return_valid_floor_in_range(first_tear, 20, 6, TRUE) + if(!second_tear) + return + + var/obj/effect/client_image_holder/phobetor/first = new(first_tear, owner) + var/obj/effect/client_image_holder/phobetor/second = new(second_tear, owner) + + first.linked_to = second + first.seer = owner + first.desc += " This one leads to [get_area(second)]." + first.name += " ([get_area(second)])" + created_firsts += first + + second.linked_to = first + second.seer = owner + second.desc += " This one leads to [get_area(first)]." + second.name += " ([get_area(first)])" + + // Delete Next Portal if it's time (it will remove its partner) + var/obj/effect/client_image_holder/phobetor/first_on_the_stack = created_firsts[1] + if(length(created_firsts) && world.time >= first_on_the_stack.created_on + first_on_the_stack.exist_length) + var/targetGate = first_on_the_stack + created_firsts -= targetGate + qdel(targetGate) + +/datum/brain_trauma/special/bluespace_prophet/phobetor/proc/return_valid_floor_in_range(atom/targeted_atom, checkRange = 8, minRange = 0, check_floor = TRUE) + // FAIL: Atom doesn't exist. Aren't you real? + if(!istype(targeted_atom)) + return FALSE + var/delta_x = rand(minRange,checkRange)*pick(-1,1) + var/delta_y = rand(minRange,checkRange)*pick(-1,1) + var/turf/center = get_turf(targeted_atom) + + var/target = locate((center.x + delta_x),(center.y + delta_y), center.z) + if(check_turf_is_valid(target, check_floor)) + return target + return FALSE + +/** + * Used as a helper that checks if you can successfully teleport to a turf. + * Returns a boolean, and checks for if the turf has density, if the turf's area has the NOTELEPORT flag, + * and if the objects in the turf have density. + * If check_floor is TRUE in the argument, it will return FALSE if it's not a type of [/turf/open/floor]. + * Arguments: + * * turf/open_turf - The turf being checked for validity. + * * check_floor - Checks if it's a type of [/turf/open/floor]. If this is FALSE, lava/chasms will be able to be selected. + */ +/datum/brain_trauma/special/bluespace_prophet/phobetor/proc/check_turf_is_valid(turf/open_turf, check_floor = TRUE) + if(check_floor && !istype(open_turf, /turf/open/floor)) + return FALSE + if(open_turf.density) + return FALSE + var/area/turf_area = get_area(open_turf) + if(turf_area.area_flags & NOTELEPORT) + return FALSE + // Checking for Objects... + for(var/obj/object in open_turf) + if(object.density) + return FALSE + return TRUE + +/** + * # Phobetor Tears + * + * The phobetor tears created by the Brain trauma. + */ + +/obj/effect/client_image_holder/phobetor + name = "phobetor tear" + desc = "A subdimensional rip in reality, which gives extra-spacial passage to those who have woken from the sleepless dream." + image_icon = 'modular_oculis/modules/vampires/icons/phobetor_tear.dmi' + image_state = "phobetor_tear" + // Place this above shadows so it always glows. + image_layer = ABOVE_MOB_LAYER + + /// How long this will exist for + var/exist_length = 50 SECONDS + /// The time of this tear's creation + var/created_on + /// The phobetor tear this is linked to + var/obj/effect/client_image_holder/phobetor/linked_to + /// The person able to see this tear. + var/mob/living/carbon/seer + +/obj/effect/client_image_holder/phobetor/Initialize(mapload) + . = ..() + created_on = world.time + AddElement(/datum/element/block_turf_fingerprints) + AddComponent(/datum/component/redirect_attack_hand_from_turf, interact_check = CALLBACK(src, PROC_REF(verify_user_can_see))) + +/obj/effect/client_image_holder/phobetor/Destroy() + seer = null + if(linked_to) + linked_to.linked_to = null + QDEL_NULL(linked_to) + return ..() + +/obj/effect/client_image_holder/phobetor/proc/verify_user_can_see(mob/user) + return user == seer + +/obj/effect/client_image_holder/phobetor/proc/check_location_seen(atom/subject, turf/target_turf) + if(!isturf(target_turf)) + return FALSE + if(target_turf.get_lumcount() <= LIGHTING_TILE_IS_DARK) + return FALSE + for(var/mob/living/nearby_viewers in viewers(target_turf) - subject) + if(!nearby_viewers.mind || !nearby_viewers.client || nearby_viewers.client?.is_afk()) + continue + if(isanimal_or_basicmob(nearby_viewers)) + continue + if(HAS_MIND_TRAIT(nearby_viewers, TRAIT_VAMPIRE_ALIGNED)) + continue + if(nearby_viewers.is_blind() || nearby_viewers.is_nearsighted_currently()) + continue + if(HAS_SILICON_ACCESS(nearby_viewers)) + continue + return TRUE + return FALSE + +/obj/effect/client_image_holder/phobetor/attack_hand(mob/living/user, list/modifiers) + if(user != seer || !linked_to) + return + if(user.loc != loc) + to_chat(user, span_warning("Step into the Tear before using it.")) + return + var/obj/item/implant/tracking/tracking_implant = locate() in user.implants + if(tracking_implant) + to_chat(user, span_warning("[tracking_implant] gives you the sense that you're being watched.")) + return + // Is this, or linked, stream being watched? + if(check_location_seen(user, get_turf(user))) + to_chat(user, span_warning("Not while you're being watched.")) + return + if(check_location_seen(user, get_turf(linked_to))) + to_chat(user, span_warning("Your destination is being watched.")) + return + to_chat(user, span_notice("You slip unseen through [src].")) + user.playsound_local(null, 'sound/effects/magic/wand_teleport.ogg', 30, FALSE, pressure_affected = FALSE) + user.forceMove(get_turf(linked_to)) diff --git a/modular_oculis/modules/vampires/code/powers/_power.dm b/modular_oculis/modules/vampires/code/powers/_power.dm new file mode 100644 index 000000000000..22044d11353a --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/_power.dm @@ -0,0 +1,258 @@ +/datum/action/cooldown/vampire + name = "Vampiric Gift" + desc = "A vampiric gift." + background_icon = 'modular_oculis/modules/vampires/icons/actions_vampire.dmi' + background_icon_state = "vamp_power_off" + button_icon = 'modular_oculis/modules/vampires/icons/actions_vampire.dmi' + button_icon_state = "power_feed" + buttontooltipstyle = "cult" + transparent_when_unavailable = TRUE + + /// Cooldown you'll have to wait between each use, decreases depending on level. + cooldown_time = 2 SECONDS + + active_background_icon_state = "vamp_power_on" + base_background_icon_state = "vamp_power_off" + + /// A sort of tutorial text found in the Antagonist tab. + var/power_explanation = "Use this power to do... something" + /// The owner's vampire datum + var/datum/antagonist/vampire/vampiredatum_power + + /// The effects on this Power (Toggled/Single Use/Static Cooldown) + var/vampire_power_flags = BP_AM_TOGGLE | BP_AM_SINGLEUSE | BP_AM_STATIC_COOLDOWN | BP_AM_COSTLESS_UNCONSCIOUS + /// Vampire-specific requirement flags for checks + var/vampire_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 + + // Special flags you can give to powers. Mainly used for any powers we want them to have by default, so, feed. + var/special_flags = NONE + /// If the Power is currently active, differs from action cooldown because of how powers are handled. + var/currently_active = FALSE + ///Can increase to yield new abilities + var/level_current = 1 + ///The cost to ACTIVATE this Power + var/vitaecost = 0 + ///The cost to MAINTAIN this Power Only used for constant powers + var/constant_vitaecost = 0 + + ///The upgraded version of this Power. 'null' means it's the max level. + var/upgraded_power = null + +// Modify description to add cost. +/datum/action/cooldown/vampire/New(Target) + . = ..() + update_desc() + +/datum/action/cooldown/vampire/Destroy() + vampiredatum_power = null + return ..() + +/datum/action/cooldown/vampire/Grant(mob/user) + . = ..() + var/datum/antagonist/vampire/vampiredatum = IS_VAMPIRE(owner) + if(vampiredatum) + vampiredatum_power = vampiredatum + + if(vampire_check_flags & BP_CANT_USE_IN_TORPOR) + RegisterSignals(owner, list(SIGNAL_ADDTRAIT(TRAIT_TORPOR), SIGNAL_REMOVETRAIT(TRAIT_TORPOR)), PROC_REF(update_status_on_signal)) + if(vampire_check_flags & BP_CANT_USE_IN_FRENZY) + RegisterSignals(owner, list(SIGNAL_ADDTRAIT(TRAIT_FRENZY), SIGNAL_REMOVETRAIT(TRAIT_FRENZY)), PROC_REF(update_status_on_signal)) + if(vampire_check_flags & BP_CANT_USE_WHILE_INCAPACITATED) + RegisterSignals(owner, list(SIGNAL_ADDTRAIT(TRAIT_INCAPACITATED), SIGNAL_REMOVETRAIT(TRAIT_INCAPACITATED)), PROC_REF(update_status_on_signal)) + if(vampire_check_flags & BP_CANT_USE_WHILE_UNCONSCIOUS) + RegisterSignal(owner, COMSIG_MOB_STATCHANGE, PROC_REF(update_status_on_signal)) + +/datum/action/cooldown/vampire/Remove(mob/removed_from) + if(owner) + UnregisterSignal(owner, list( + COMSIG_MOB_STATCHANGE, + SIGNAL_ADDTRAIT(TRAIT_TORPOR), + SIGNAL_ADDTRAIT(TRAIT_FRENZY), + SIGNAL_ADDTRAIT(TRAIT_INCAPACITATED), + SIGNAL_REMOVETRAIT(TRAIT_TORPOR), + SIGNAL_REMOVETRAIT(TRAIT_FRENZY), + SIGNAL_REMOVETRAIT(TRAIT_INCAPACITATED), + )) + return ..() + +/datum/action/cooldown/vampire/is_action_active(atom/movable/screen/movable/action_button/current_button) + if(currently_active) + return TRUE + return ..() + +//This is when we CLICK on the ability Icon, not USING. +/datum/action/cooldown/vampire/Activate(atom/target) + if(currently_active) + deactivate_power() + return FALSE + if(!can_pay_cost() || !can_use()) + return FALSE + pay_cost() + activate_power() + if(!(vampire_power_flags & BP_AM_TOGGLE) || !currently_active) + StartCooldown() + + return TRUE + +/datum/action/cooldown/vampire/proc/update_desc() + desc = initial(desc) + if(vitaecost > 0) + desc += "

COST: [vitaecost] Blood" + if(constant_vitaecost > 0) + desc += "

CONSTANT COST: [constant_vitaecost] Blood." + if(vampire_power_flags & BP_AM_SINGLEUSE) + desc += "

SINGLE USE:
Can only be used once per night." + +/datum/action/cooldown/vampire/proc/can_pay_cost() + if(QDELETED(owner)) + return FALSE + + // Check if we have enough blood for non-vampires + if(!vampiredatum_power) + var/mob/living/living_owner = owner + if(!HAS_TRAIT(living_owner, TRAIT_NOBLOOD) && living_owner.blood_volume < vitaecost) + living_owner.balloon_alert(living_owner, "not enough blood.") + return FALSE + + return TRUE + + // Have enough blood? Vampires in a Frenzy don't need to pay them + if(HAS_TRAIT(owner, TRAIT_FRENZY)) + return TRUE + if(vampiredatum_power.current_vitae < vitaecost) + owner.balloon_alert(owner, "not enough blood.") + return FALSE + + return TRUE + +///Checks if the Power is available to use. +/datum/action/cooldown/vampire/proc/can_use() + if(!iscarbon(owner)) + return FALSE + var/mob/living/carbon/carbon_owner = owner + + // Torpor? + if((vampire_check_flags & BP_CANT_USE_IN_TORPOR) && HAS_TRAIT(carbon_owner, TRAIT_TORPOR)) + to_chat(carbon_owner, span_warning("Not while you're in Torpor.")) + return FALSE + // Frenzy? + if((vampire_check_flags & BP_CANT_USE_IN_FRENZY) && HAS_TRAIT(carbon_owner, TRAIT_FRENZY)) + to_chat(carbon_owner, span_warning("You cannot use powers while in a Frenzy!")) + return FALSE + // Stake? + if((vampire_check_flags & BP_CANT_USE_WHILE_STAKED) && vampiredatum_power?.check_if_staked()) + to_chat(carbon_owner, 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((vampire_check_flags & BP_CANT_USE_WHILE_UNCONSCIOUS) && carbon_owner.stat != CONSCIOUS) + to_chat(carbon_owner, span_warning("You can't do this while you are unconcious!")) + return FALSE + // Incapacitated? + if((vampire_check_flags & BP_CANT_USE_WHILE_INCAPACITATED) && INCAPACITATED_IGNORING(carbon_owner, INCAPABLE_RESTRAINTS | INCAPABLE_GRAB)) + to_chat(carbon_owner, span_warning("Not while you're incapacitated!")) + return FALSE + // Constant Cost (out of blood) + if(constant_vitaecost > 0 && vampiredatum_power?.current_vitae <= 0) + to_chat(carbon_owner, span_warning("You don't have the blood to upkeep [src].")) + return FALSE + // Silver cuffed? + /* if(!(vampire_check_flags & BP_ALLOW_WHILE_SILVER_CUFFED) && owner.has_status_effect(/datum/status_effect/silver_cuffed)) + owner.balloon_alert(owner, "the silver cuffs on your wrists prevent you from using your powers!") + return FALSE */ + return TRUE + +/datum/action/cooldown/vampire/proc/pay_cost() + // Vassals get powers too! + if(!vampiredatum_power) + var/mob/living/living_owner = owner + if(!HAS_TRAIT(living_owner, TRAIT_NOBLOOD)) + living_owner.adjust_blood_volume(-vitaecost) + return + + // Vampires in a Frenzy don't have enough Blood to pay it, so just don't. + if(!HAS_TRAIT(owner, TRAIT_FRENZY)) + vampiredatum_power.adjust_blood_volume(-vitaecost) + vampiredatum_power.update_hud() + +/datum/action/cooldown/vampire/proc/activate_power() + currently_active = TRUE + if(vampire_power_flags & BP_AM_TOGGLE) + RegisterSignal(owner, COMSIG_LIVING_LIFE, PROC_REF(use_power)) + + owner.log_message("used [src][vitaecost != 0 ? " at the cost of [vitaecost]" : ""].", LOG_ATTACK, color="red") + build_all_button_icons(UPDATE_BUTTON_NAME | UPDATE_BUTTON_BACKGROUND) + +/datum/action/cooldown/vampire/proc/deactivate_power() + if(!currently_active) //Already inactive? Return + return + + if(vampire_power_flags & BP_AM_TOGGLE) + UnregisterSignal(owner, COMSIG_LIVING_LIFE) + if(vampire_power_flags & BP_AM_SINGLEUSE) + remove_after_use() + return + + currently_active = FALSE + StartCooldown() + build_all_button_icons(UPDATE_BUTTON_NAME | UPDATE_BUTTON_BACKGROUND) + +/// Used by powers that are continuously active (That have BP_AM_TOGGLE flag) +/datum/action/cooldown/vampire/proc/use_power() + if(!continue_active()) // We can't afford the Power? Deactivate it. + deactivate_power() + return FALSE + + // IF USER IS UNCONSCIOUS + if((vampire_power_flags & BP_AM_COSTLESS_UNCONSCIOUS) && owner.stat != CONSCIOUS) + return TRUE + else + if(vampiredatum_power) + vampiredatum_power.adjust_blood_volume(-constant_vitaecost) + else + var/mob/living/living_owner = owner + if(!HAS_TRAIT(living_owner, TRAIT_NOBLOOD)) + living_owner.adjust_blood_volume(-constant_vitaecost) + return TRUE + +/// Checks to make sure this power can stay active +/datum/action/cooldown/vampire/proc/continue_active() + if(QDELETED(owner)) + return FALSE + /* if (!(check_flags & BP_ALLOW_WHILE_SILVER_CUFFED) && owner.has_status_effect(/datum/status_effect/silver_cuffed)) + return FALSE */ + if(vampiredatum_power && vampiredatum_power.current_vitae < constant_vitaecost) + return FALSE + + return TRUE + +/// Used to unlearn Single-Use Powers +/datum/action/cooldown/vampire/proc/remove_after_use() + vampiredatum_power?.powers -= src + if(!QDELETED(src) && !QDELETED(owner)) + Remove(owner) + +// If there's a mortal in line of sight, we get a masq infraction +/datum/action/cooldown/vampire/proc/check_witnesses(mob/living/target, fallback_find_target = TRUE) + var/turf/our_turf = get_turf(owner) + if(fallback_find_target && target && (!isliving(target) || !vampiredatum_power.is_masq_watcher(target))) + find_target_loop: + for(var/turf/nearby_turf as anything in spiral_range_turfs(6, target)) + for(var/mob/living/nearby_mob in nearby_turf) + if(vampiredatum_power.is_masq_watcher(nearby_mob)) + target = nearby_mob + break find_target_loop + var/turf/target_turf = get_turf(target) + var/min_darkness = target_turf ? min(our_turf.get_lumcount(), target_turf.get_lumcount()) : our_turf.get_lumcount() + var/is_dark = min_darkness <= LIGHTING_TILE_IS_DARK + for(var/mob/living/watcher in oviewers(6, owner) - target) + if(!vampiredatum_power.is_masq_watcher(watcher)) + continue + if(is_dark && !watcher.Adjacent(owner) && (!target || !watcher.Adjacent(target))) + continue + if(!watcher.incapacitated) + watcher.face_atom(owner) + + watcher.do_alert_animation(watcher) + playsound(watcher, 'sound/machines/chime.ogg', 50, FALSE, -5) + vampiredatum_power.give_masquerade_infraction() + break diff --git a/modular_oculis/modules/vampires/code/powers/_targeted.dm b/modular_oculis/modules/vampires/code/powers/_targeted.dm new file mode 100644 index 000000000000..bda0e222f63a --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/_targeted.dm @@ -0,0 +1,111 @@ +// NOTE: All Targeted spells are Toggles! We just don't bother checking here. +/datum/action/cooldown/vampire/targeted + vampire_power_flags = BP_AM_TOGGLE + + ///If set, how far the target has to be for the power to work. + var/target_range + ///Message sent to chat when clicking on the power, before you use it. + var/prefire_message + ///Most powers happen the moment you click. Some, like Mesmerize, require time and shouldn't cost you if they fail. + var/power_activates_immediately = TRUE + ///Is this power LOCKED due to being used? + var/power_in_use = FALSE + +/// Modify description to add notice that this is aimed. +/datum/action/cooldown/vampire/targeted/update_desc() + . = ..() + desc += "

Targeted Power" + +/datum/action/cooldown/vampire/targeted/Activate(atom/target) + if(currently_active) + deactivate_power() + return FALSE + if(owner.click_intercept) + owner.balloon_alert(owner, "already using a targeted power!") + return FALSE + if(!can_pay_cost(owner) || !can_use()) + return FALSE + + if(prefire_message) + to_chat(owner, span_announce(prefire_message)) + + activate_power() + + if(currently_active) + set_click_ability(owner) + +/datum/action/cooldown/vampire/targeted/Remove(mob/removed_from) + if(removed_from?.click_intercept == src) + unset_click_ability(removed_from, refund_cooldown = FALSE) + return ..() + +/datum/action/cooldown/vampire/targeted/activate_power() + currently_active = TRUE + + owner.log_message("used [src][vitaecost != 0 ? " at the cost of [vitaecost]" : ""].", LOG_ATTACK, color="red") + build_all_button_icons(UPDATE_BUTTON_NAME | UPDATE_BUTTON_BACKGROUND) + +/datum/action/cooldown/vampire/targeted/deactivate_power(successful = FALSE) + if(vampire_power_flags & BP_AM_TOGGLE) + UnregisterSignal(owner, COMSIG_LIVING_LIFE) + + if((vampire_power_flags & BP_AM_SINGLEUSE) && successful) + remove_after_use() + return + + currently_active = FALSE + build_all_button_icons(UPDATE_BUTTON_NAME | UPDATE_BUTTON_BACKGROUND) + unset_click_ability(owner) + + + +/// Check if target is VALID (wall, turf, or character?) +/datum/action/cooldown/vampire/targeted/proc/check_valid_target(atom/target_atom) + // No targeting yourself + if(target_atom == owner) + return FALSE + // Check if in range + if(target_range && !(target_atom in view(target_range, owner))) + if(target_range > 1) + owner.balloon_alert(owner, "out of range.") + return FALSE + + return TRUE + +/datum/action/cooldown/vampire/targeted/InterceptClickOn(mob/living/clicker, params, atom/target) + INVOKE_ASYNC(src, PROC_REF(click_with_power), target) + return TRUE + +/// Click Target +/datum/action/cooldown/vampire/targeted/proc/click_with_power(atom/target_atom) + // Already using? + if(power_in_use) + return + // Can use? + if(!can_use()) + return + // Valid target? + if(!check_valid_target(target_atom)) + return + // Enough blood? + if(!can_pay_cost()) + return + + power_in_use = TRUE + fire_targeted_power(target_atom) + if(vampire_power_flags & BP_AM_TOGGLE) + RegisterSignal(owner, COMSIG_LIVING_LIFE, PROC_REF(use_power)) + // Skip this part so we can return TRUE right away. + if(power_activates_immediately) + power_activated_sucessfully() // Mesmerize pays only after success. + power_in_use = FALSE + +/datum/action/cooldown/vampire/targeted/proc/fire_targeted_power(atom/target_atom) + unset_click_ability(owner) + log_combat(owner, target_atom, "used [name] on") + +/// The power went off! We now pay the cost of the power. +/datum/action/cooldown/vampire/targeted/proc/power_activated_sucessfully() + pay_cost() + StartCooldown() + deactivate_power(TRUE) diff --git a/modular_oculis/modules/vampires/code/powers/auspex/astral_project.dm b/modular_oculis/modules/vampires/code/powers/auspex/astral_project.dm new file mode 100644 index 000000000000..68f6d33cfb80 --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/auspex/astral_project.dm @@ -0,0 +1,25 @@ +/datum/action/cooldown/vampire/astral_projection + name = "Astral Projection" + desc = "The power of your blood empowers your auspex. Become able to project your consciousness outside your body." + power_explanation = "When Activated, you will become a ghost.\n\ + Visit anywhere you like, watch anyone you want.\n\ + Talk to the spriits, and know all things." + active_background_icon_state = "tremere_power_gold_on" + base_background_icon_state = "tremere_power_gold_off" + button_icon_state = "power_astral_projection" + vampire_power_flags = BP_AM_TOGGLE + vampire_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 + vitaecost = 400 + cooldown_time = 60 SECONDS + +/datum/action/cooldown/vampire/astral_projection/activate_power() + . = ..() + owner.playsound_local(null, 'modular_oculis/modules/vampires/sound/auspex.ogg', 50, TRUE, pressure_affected = FALSE) + var/mob/dead/observer/ghost = owner.ghostize(can_reenter_corpse = TRUE) + ADD_TRAIT(ghost, TRAIT_NO_OBSERVE, TRAIT_VAMPIRE) + ghost.add_atom_colour(COLOR_VOID_PURPLE, ADMIN_COLOUR_PRIORITY) + var/ghost_name = "Astral Shade of [ghost.name]" + ghost.name = ghost_name + ghost.deadchat_name = ghost_name + ghost.add_filter("astral_projection", 1, outline_filter(size = 1, color = BLOOD_COLOR_RED)) + deactivate_power() diff --git a/modular_oculis/modules/vampires/code/powers/auspex/auspex.dm b/modular_oculis/modules/vampires/code/powers/auspex/auspex.dm new file mode 100644 index 000000000000..eafd85c56f67 --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/auspex/auspex.dm @@ -0,0 +1,143 @@ +/datum/discipline/auspex + name = "Auspex" + discipline_explanation = "Auspex is a Discipline that grants vampires supernatural senses, letting them peer far further and see things best left unseen.\n\ + The malkavians especially have a bond with it, being seers at heart." + icon_state = "auspex" + + // Lists of abilities granted per level + level_1 = list(/datum/action/cooldown/vampire/auspex) + level_2 = list(/datum/action/cooldown/vampire/auspex/two) + level_3 = list(/datum/action/cooldown/vampire/auspex/three) + level_4 = list(/datum/action/cooldown/vampire/auspex/four) + +/datum/discipline/auspex/malkavian + level_5 = list(/datum/action/cooldown/vampire/auspex/four, /datum/action/cooldown/vampire/astral_projection) + +/** + * # Auspex + * + * Level 1 - Raise sightrange by 2, project sight 2 tiles ahead. + * Level 2 - Raise sightrange by 3, project sight 4 tiles ahead. Meson Vision + * Level 3 - Raise sightrange by 5, project sight 6 tiles ahead. + * Level 4 - Raise sightrange by 7, project sight 8 tiles ahead. Xray Vision + * Level 5 - For Malkavians: Gain ability to astral project like a wizard. + */ +/datum/action/cooldown/vampire/auspex + name = "Auspex" + desc = "Sense the vitae of any creature directly, and use your keen senses to widen your perception." + button_icon_state = "power_auspex" + power_explanation = "- Level 1: When Activated, you will see further. \n\ + - Level 2: When Activated, you will see further, be able to sense walls and the layout of rooms, and, upon examining a fellow Kindred, be able to tell if they have committed Diablerie. \n\ + - Level 3: When Activated, You still have meson vision, same as level 3, but even more range. \n\ + - Level 4: When Activated, you will see further, and be able to sense anything in sight, seeing through walls and barriers as if they were glass." + vampire_power_flags = BP_AM_TOGGLE | BP_AM_STATIC_COOLDOWN + vampire_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 + vitaecost = 20 + constant_vitaecost = 0.75 + cooldown_time = 10 SECONDS + var/add_meson = FALSE + var/add_xray = FALSE + var/zoom_out_amt = 2 + var/zoom_amt = 6 + var/see_diablerie = FALSE + + + var/looking = FALSE + var/mob/listening_to + +/datum/action/cooldown/vampire/auspex/two + name = "Auspex" + vitaecost = 20 + constant_vitaecost = 1 + zoom_out_amt = 4 + zoom_amt = 7 + add_meson = TRUE + see_diablerie = TRUE + +/datum/action/cooldown/vampire/auspex/three + name = "Auspex" + vitaecost = 20 + constant_vitaecost = 1.25 + zoom_out_amt = 6 + zoom_amt = 8 + add_meson = TRUE + see_diablerie = TRUE + +/datum/action/cooldown/vampire/auspex/four + name = "Auspex" + vitaecost = 20 + constant_vitaecost = 1.5 + zoom_out_amt = 10 + zoom_amt = 10 + add_xray = TRUE + see_diablerie = TRUE + +/datum/action/cooldown/vampire/auspex/activate_power() + . = ..() + if(!looking) + lookie() + +/datum/action/cooldown/vampire/auspex/deactivate_power() + . = ..() + if(looking) + unlooky() + +/datum/action/cooldown/vampire/auspex/proc/lookie() + SIGNAL_HANDLER + + if(!listening_to) + RegisterSignals(owner, list(COMSIG_MOVABLE_MOVED, COMSIG_MOB_LOGOUT), PROC_REF(deactivate_power)) + RegisterSignal(owner, COMSIG_ATOM_POST_DIR_CHANGE, PROC_REF(lookie)) + listening_to = owner + var/client/client = owner?.client + if(!client) + return + var/_x = 0 + var/_y = 0 + switch(owner.dir) + if(NORTH) + _y = zoom_amt + if(EAST) + _x = zoom_amt + if(SOUTH) + _y = -zoom_amt + if(WEST) + _x = -zoom_amt + + client?.change_view(get_zoomed_view(world.view, zoom_out_amt)) + client?.pixel_x = ICON_SIZE_X * _x + client?.pixel_y = ICON_SIZE_Y * _y + looking = TRUE + + if(see_diablerie) + ADD_TRAIT(owner, TRAIT_SEE_DIABLERIE, REF(src)) + + if(add_meson) + ADD_TRAIT(owner, TRAIT_MESON_VISION, REF(src)) + + if(add_xray) + ADD_TRAIT(owner, TRAIT_XRAY_VISION, REF(src)) + + owner.update_sight() + +/datum/action/cooldown/vampire/auspex/proc/unlooky() + SIGNAL_HANDLER + + if(listening_to) + UnregisterSignal(listening_to, list(COMSIG_MOVABLE_MOVED, COMSIG_MOB_LOGOUT, COMSIG_ATOM_POST_DIR_CHANGE)) + listening_to = null + + looking = FALSE + owner.remove_traits(list(TRAIT_SEE_DIABLERIE, TRAIT_MESON_VISION, TRAIT_XRAY_VISION), REF(src)) + owner.update_sight() + + // do this last in case weird client shit happens and runtimes + var/client/client = owner.client + if(client) + client?.change_view(client?.view_size?.default) + client?.pixel_x = 0 + client?.pixel_y = 0 + +/datum/action/cooldown/vampire/auspex/Destroy() + listening_to = null + return ..() diff --git a/modular_oculis/modules/vampires/code/powers/celerity/celerity.dm b/modular_oculis/modules/vampires/code/powers/celerity/celerity.dm new file mode 100644 index 000000000000..bdd33096c363 --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/celerity/celerity.dm @@ -0,0 +1,11 @@ +/datum/discipline/celerity + name = "Celerity" + discipline_explanation = "Celerity is a Discipline that grants vampires supernatural quickness and reflexes." + icon_state = "celerity" + + // Lists of abilities granted per level + level_1 = list(/datum/action/cooldown/vampire/targeted/haste) + level_2 = list(/datum/action/cooldown/vampire/targeted/haste/two) + level_3 = list(/datum/action/cooldown/vampire/targeted/haste/three) + level_4 = list(/datum/action/cooldown/vampire/targeted/haste/three, /datum/action/cooldown/vampire/exactitude) + level_5 = null diff --git a/modular_oculis/modules/vampires/code/powers/celerity/haste.dm b/modular_oculis/modules/vampires/code/powers/celerity/haste.dm new file mode 100644 index 000000000000..0b3cad157dc8 --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/celerity/haste.dm @@ -0,0 +1,102 @@ +/* Level 1: Speed to location + * Level 2: Dodge Bullets + * Level 3: Stun People Passed + */ +/datum/action/cooldown/vampire/targeted/haste + name = "Immortal Haste" + desc = "Dash somewhere with supernatural speed. Those nearby may be knocked away or stunned." + button_icon_state = "power_speed" + power_explanation = "Click anywhere to immediately dash towards that location.\n\ + The Power will not work if you are lying down, zero-gravity, or are being aggressively grabbed.\n\ + Anyone in your way during your Haste will be knocked down.\n\ + Higher levels will increase the knockdown dealt to enemies." + vampire_power_flags = BP_AM_TOGGLE + vampire_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 + vitaecost = 15 + cooldown_time = 12 SECONDS + target_range = 15 + power_activates_immediately = TRUE + ///List of all people hit by our power, so we don't hit them again. + var/list/hit = list() + +/datum/action/cooldown/vampire/targeted/haste/two + vitaecost = 30 + cooldown_time = 6 SECONDS + level_current = 2 + +/datum/action/cooldown/vampire/targeted/haste/three + vitaecost = 45 + cooldown_time = 2 SECONDS + level_current = 3 + +/datum/action/cooldown/vampire/targeted/haste/can_use() + . = ..() + if(!.) + return FALSE + + // Being Grabbed + if(owner.pulledby && owner.pulledby.grab_state >= GRAB_AGGRESSIVE) + owner.balloon_alert(owner, "you're being grabbed!") + return FALSE + if(!owner.has_gravity(owner.loc)) //We dont want people to be able to use this to fly around in space + owner.balloon_alert(owner, "you cannot dash while floating!") + return FALSE + var/mob/living/carbon/user = owner + if(user?.body_position == LYING_DOWN) + owner.balloon_alert(owner, "you must be standing to tackle!") + return FALSE + return TRUE + +/// Anything will do, if it's not me or my square +/datum/action/cooldown/vampire/targeted/haste/check_valid_target(atom/target_atom) + . = ..() + if(!.) + return FALSE + + // Can't dash to the same tile we're already on + if(target_atom.loc == owner.loc) + return FALSE + +/// This is a non-async proc to make sure the power is "locked" until this finishes. +/datum/action/cooldown/vampire/targeted/haste/fire_targeted_power(atom/target_atom) + . = ..() + RegisterSignal(owner, COMSIG_MOVABLE_MOVED, PROC_REF(on_move)) + var/mob/living/user = owner + var/turf/targeted_turf = get_turf(target_atom) + // Pulled? Not anymore. + user.pulledby?.stop_pulling() + // Go to target turf + // DO NOT USE WALK TO. + // check_witnesses() + owner.balloon_alert(owner, "you dash into the air!") + playsound(get_turf(owner), 'sound/items/weapons/punchmiss.ogg', 25, TRUE, -1) + var/safety = get_dist(user, targeted_turf) * 3 + 1 + var/consequetive_failures = 0 + while(--safety && (get_turf(user) != targeted_turf)) + var/success = step_towards(user, targeted_turf) //This does not try to go around obstacles. + if(!success) + success = step_to(user, targeted_turf) //this does + if(!success) + consequetive_failures++ + if(consequetive_failures >= 3) //if 3 steps don't work + break //just stop + else + consequetive_failures = 0 //reset so we can keep moving + if(user.resting || INCAPACITATED_IGNORING(user, INCAPABLE_RESTRAINTS | INCAPABLE_GRAB)) //actually down? stop. + break + if(success) //don't sleep if we failed to move. + sleep(world.tick_lag) + +/datum/action/cooldown/vampire/targeted/haste/power_activated_sucessfully() + . = ..() + UnregisterSignal(owner, COMSIG_MOVABLE_MOVED) + hit.Cut() + +/datum/action/cooldown/vampire/targeted/haste/proc/on_move() + for(var/mob/living/hit_living in dview(1, get_turf(owner)) - owner) + if(hit.Find(hit_living)) + continue + hit += hit_living + playsound(hit_living, SFX_PUNCH, 15, TRUE, -1) + hit_living.Knockdown(10 + level_current * 8) + hit_living.spin(1 SECONDS, 1) diff --git a/modular_oculis/modules/vampires/code/powers/celerity/quickness.dm b/modular_oculis/modules/vampires/code/powers/celerity/quickness.dm new file mode 100644 index 000000000000..fc5f80e88289 --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/celerity/quickness.dm @@ -0,0 +1,63 @@ +/datum/action/cooldown/vampire/exactitude + name = "Exactitude" + desc = "Focus your powers into your hands, enabling you to attack with preternatural precision." + button_icon_state = "power_exactitude" + power_explanation = "Imbues your hands with supernatural precision. Cannot be used with gloves on.\n\ + Use with combat mode. When punching, you will automatically hit the closest being. Best used without moving your mouse at all." + vampire_power_flags = BP_AM_TOGGLE + vampire_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 + cooldown_time = 30 SECONDS + vitaecost = 25 + constant_vitaecost = 3 + +/datum/action/cooldown/vampire/exactitude/can_use() + . = ..() + if(!.) + return FALSE + if(owner.get_item_by_slot(ITEM_SLOT_GLOVES)) + owner.balloon_alert(owner, "you're wearing gloves!") + return FALSE + +/datum/action/cooldown/vampire/exactitude/activate_power() + . = ..() + RegisterSignal(owner, COMSIG_LIVING_EARLY_UNARMED_ATTACK, PROC_REF(on_unarmed_attack)) + ADD_TRAIT(owner, TRAIT_PERFECT_ATTACKER, REF(src)) + +/datum/action/cooldown/vampire/exactitude/deactivate_power() + . = ..() + UnregisterSignal(owner, COMSIG_LIVING_EARLY_UNARMED_ATTACK) + REMOVE_TRAIT(owner, TRAIT_PERFECT_ATTACKER, REF(src)) + +/datum/action/cooldown/vampire/exactitude/continue_active() + . = ..() + if(owner.get_item_by_slot(ITEM_SLOT_GLOVES)) + return FALSE + +/datum/action/cooldown/vampire/exactitude/proc/on_unarmed_attack(mob/living/source, atom/target, proximity, modifiers) + if(!source.combat_mode || !currently_active) + return NONE + + if(isliving(target) && target != source) + var/mob/living/living_target = target + if(living_target.stat != DEAD) // don't focus on dead targets + return NONE + + var/list/potential_targets = list() + for(var/mob/living/potential_target in oview(1, source)) + if(potential_target.stat == DEAD || potential_target.invisibility > source.see_invisible) + continue + // if it's a fellow vampire (who doesn't have the same masquerade breaker status as us), a vassal, or has ourself in its factions, then it'll be prioritized last. + var/datum/antagonist/vampire/target_vampire = IS_VAMPIRE(potential_target) + if((target_vampire && target_vampire.broke_masquerade != vampiredatum_power.broke_masquerade) || IS_VASSAL(potential_target) || potential_target.has_faction(REF(source)) || potential_target.has_ally(source)) + potential_targets += potential_target + else + potential_targets.Insert(1, potential_target) + + if(length(potential_targets)) + var/mob/living/to_attack = potential_targets[1] + source.face_atom(to_attack) + to_attack.attack_hand(source, modifiers) + source.changeNext_move(CLICK_CD_MELEE) + return COMPONENT_CANCEL_ATTACK_CHAIN + + diff --git a/modular_oculis/modules/vampires/code/powers/disciplines.dm b/modular_oculis/modules/vampires/code/powers/disciplines.dm new file mode 100644 index 000000000000..7d41009ee328 --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/disciplines.dm @@ -0,0 +1,75 @@ +/datum/discipline + ///Name of this Discipline. + var/name = "ERROR" + ///Text description of this Discipline. + var/discipline_explanation = "ERROR" + + ///Icon for this Discipline + var/icon_state = "error" + + // Lists of abilities granted per level. Set to null if unused. + var/list/level_1 = null // Level 1 + var/list/level_2 = null // Level 2 + var/list/level_3 = null // Level 3 + var/list/level_4 = null // Level 4 + var/list/level_5 = null // Level 5 + + // Backend shit + ///What level the user has in this Discipline. In case we want to add persistant effects to having a discipline. + var/level = 1 + ///The mob that owns and is using this Discipline. + var/mob/living/carbon/human/owner + /// The owner's vampire datum + var/datum/antagonist/vampire/vampiredatum_discipline + +/datum/discipline/Destroy() + vampiredatum_discipline = null + owner = null + return ..() + +/** + * Needs to be called after we have been created and assigned to a vampire. + */ +/datum/discipline/proc/assigned_to_owner(mob/living/carbon/carbon_owner) + owner = carbon_owner + vampiredatum_discipline = IS_VAMPIRE(carbon_owner) + +// 0 is null, and false is also null, which is 0. So, we gotta use 1 as the starting point that doesn't have any abilities. +// Yes this means all levels everywhere else do not match up with this. +// You know, null kind of exists so we can tell if there is no data vs it being a 0. Just a thought, lummox. +// You can also give it a string "current" and it'll return the current set! +/datum/discipline/proc/get_abilities_with_level(what_level) + if(what_level == "current") + what_level = level + + if(what_level == "next") + what_level = level + 1 + + switch(what_level) + if(1) // 0, null, do not change + return null + if(2) + return level_1 + if(3) + return level_2 + if(4) + return level_3 + if(5) + return level_4 + if(6) + return level_5 + else + return null + +/// Can't go over 5 even if you define more +/datum/discipline/proc/level_up() + if(level >= 6) // it's six cuz 1 is null, yadda yadda + level = 6 + return FALSE + else + level++ + return TRUE + +// For example, extra damage for potence. +/datum/discipline/proc/apply_discipline_quirks(datum/antagonist/vampire/clan_owner) + return diff --git a/modular_oculis/modules/vampires/code/powers/dominate/command.dm b/modular_oculis/modules/vampires/code/powers/dominate/command.dm new file mode 100644 index 000000000000..6e8e90688590 --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/dominate/command.dm @@ -0,0 +1,257 @@ +/** + * Command + * Gives a one-word brainwash-command to a target for 60 seconds. + * Level 2: Now lasts 180 seconds. + */ +/datum/action/cooldown/vampire/targeted/command + name = "Command" + desc = "Dominate the mind of another with a simple command." + button_icon_state = "power_command" + power_explanation = "Click any player to attempt to compel them.\n\ + If your target is already commanded, a Curator, or a vampire, you will fail.\n\ + Once commanded, the target will do their best to fulfill it, with a duration scaling with level.\n\ + If your target is mindshielded, your command's duration will be halved.\n\ + At level 1, your command will stay for 60 seconds.\n\ + At level 2, it will remain for 3 minutes.\n\ + Be smart with your wording. They will become pacified, and won't obey violent commands.\n\ + In addition, attacking your target will immediately snap them out of their compulsion." + vampire_power_flags = NONE + vampire_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 + vitaecost = 120 + cooldown_time = 80 SECONDS + target_range = 3 + power_activates_immediately = FALSE + prefire_message = "Whom will you subvert to your will?" + ranged_mousepointer = 'modular_oculis/modules/vampires/icons/mouse_pointers/vampire_command.dmi' + + /// How long the command is in effect. + var/power_time = 60 SECONDS + + /// Reference to the target + var/datum/weakref/target_ref + +/datum/action/cooldown/vampire/targeted/command/two + name = "Command" + power_time = 180 SECONDS + vitaecost = 240 + cooldown_time = 200 SECONDS + target_range = 6 + +/datum/action/cooldown/vampire/targeted/command/can_use() + . = ..() + if(!.) + return FALSE + var/mob/living/carbon/carbon_owner = owner + + // Must have ears + if(!owner.get_organ_slot(ORGAN_SLOT_TONGUE)) + to_chat(owner, span_warning("You have no tongue with which to command!")) + return FALSE + + // Must have mouth unobstructed + if(carbon_owner.is_mouth_covered() || !isturf(carbon_owner.loc)) + owner.balloon_alert(owner, "your mouth is blocked.") + return FALSE + + if(HAS_TRAIT(carbon_owner, TRAIT_MUTE) || !isturf(carbon_owner.loc)) + owner.balloon_alert(owner, "you cannot speak!") + return FALSE + return TRUE + +/datum/action/cooldown/vampire/targeted/command/check_valid_target(atom/target_atom) + . = ..() + if(!.) + return FALSE + + // Must be a carbon or silicon + if(!iscarbon(target_atom)) + return FALSE + + var/mob/living/living_target = target_atom + // No mind + if(!living_target.mind) + owner.balloon_alert(owner, "[living_target] is mindless.") + return FALSE + + // Vampire/Curator check + if(IS_VAMPIRE(living_target) || IS_CURATOR(living_target)) + owner.balloon_alert(owner, "too powerful.") + return FALSE + + // Is our target alive or unconcious? + if(living_target.stat != CONSCIOUS) + owner.balloon_alert(owner, "[living_target] is not [(living_target.stat == DEAD || HAS_TRAIT(living_target, TRAIT_FAKEDEATH)) ? "alive" : "conscious"].") + return FALSE + + // Is our target deaf? + if(HAS_TRAIT(living_target, TRAIT_DEAF)) /* if(!living_target.can_hear()) */ + owner.balloon_alert(owner, "[living_target] cannot hear you!") + return FALSE + + // Is our target a silicon? + if(issilicon(living_target)) + owner.balloon_alert(owner, "[living_target] cannot be compelled!") + return FALSE + + // Already commanded? + if(living_target.has_status_effect(/datum/status_effect/commanded)) + owner.balloon_alert(owner, "[living_target] is already compelled!") + return FALSE + +/datum/action/cooldown/vampire/targeted/command/fire_targeted_power(atom/target_atom) + . = ..() + + var/mob/living/living_target = target_atom + target_ref = WEAKREF(living_target) + + owner.balloon_alert(owner, "commanding [living_target]...") + + var/command = get_single_word_command() + + if(!command) + deactivate_power() + return + + // They left while we were writing + if(!(living_target in hearers(target_range, owner))) + deactivate_power() + return + + //Actually command them now + owner.say(command, forced = "[type]") + + var/time_multiplier = 1 + if(HAS_TRAIT(living_target, TRAIT_UNCONVERTABLE)) + time_multiplier = 0.5 + + living_target.apply_status_effect(/datum/status_effect/commanded, owner, command, power_time * time_multiplier) + + power_activated_sucessfully() // PAY COST! BEGIN COOLDOWN! + +/datum/action/cooldown/vampire/targeted/command/proc/get_single_word_command() + . = TRUE + var/command = tgui_input_text(owner, "What would you like to command?", "Input a command", "STOP", encode = FALSE, timeout = 2 MINUTES) + if(QDELETED(src)) + return FALSE + /* if(CHAT_FILTER_CHECK(command)) + to_chat(owner, span_warning("The command '[span_bold("[command]")]' is forbidden!")) + return FALSE */ + if(findtext_char(command, " ")) + to_chat(owner, span_warning("Please only input a single word.")) + return FALSE + if(length_char(command) > 10) + to_chat(owner, span_warning("Command too long!")) + return FALSE + if(copytext(command, 1, 5) == "kill" || copytext(command, 1, 7) == "murder" || copytext(command, 1, 8) == "suicide" || copytext(command, 1, 4) == "die") + owner.balloon_alert(owner, "that won't work!") + to_chat(owner, span_warning(" * Remember, victims will be pacified for the duration of the command!")) + return FALSE + + return command + +/datum/action/cooldown/vampire/targeted/command/continue_active() + . = ..() + if(!.) + return FALSE + + if(!can_use()) + return FALSE + + var/mob/living/living_target = target_ref?.resolve() + if(!living_target || !check_valid_target(living_target)) + return FALSE + +/datum/action/cooldown/vampire/targeted/command/deactivate_power() + . = ..() + target_ref = null + +/datum/status_effect/commanded + id = "commanded" + duration = 1 MINUTES + tick_interval = STATUS_EFFECT_NO_TICK + on_remove_on_mob_delete = TRUE + alert_type = /atom/movable/screen/alert/status_effect/commanded + /// The vampire that casted this command. + var/mob/living/caster + /// The actual command used for the objective. + var/command + /// The brainwash objectives, so we can unbrainwash when it ends. + var/list/directives + +/datum/status_effect/commanded/on_creation(mob/living/new_owner, mob/living/caster, command, duration) + src.caster = caster + src.command = command + if(duration) + src.duration = duration + return ..() + +/datum/status_effect/commanded/on_apply() + if(!owner.mind) + return FALSE + + ADD_TRAIT(owner, TRAIT_PACIFISM, TRAIT_STATUS_EFFECT(id)) + directives = brainwash(owner, "[command]!", "[caster.real_name]'s Command") + + // make sure they have a moment to realize what's going on + owner.Immobilize(2 SECONDS, TRUE) + to_chat(owner, "
" + span_awe(span_extremelybig("[command]!")) + "
", type = MESSAGE_TYPE_WARNING) + + // also log it. + message_admins("[ADMIN_LOOKUPFLW(caster)] used the COMMAND ability on [ADMIN_LOOKUPFLW(owner)], commanding them to [command].") + log_game("[key_name(caster)] used the command ability on [key_name(owner)], commanding them to [command].") + + var/atom/movable/screen/alert/status_effect/commanded/command_alert = linked_alert + if(command_alert) + command_alert.command = command + + owner.AddElement(/datum/element/relay_attackers) + RegisterSignal(owner, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_attacked)) + RegisterSignal(owner, COMSIG_LIVING_SLAPPED, PROC_REF(on_slapped)) + return TRUE + +/datum/status_effect/commanded/on_remove() + UnregisterSignal(owner, list(COMSIG_ATOM_WAS_ATTACKED, COMSIG_LIVING_SLAPPED)) + REMOVE_TRAIT(owner, TRAIT_PACIFISM, TRAIT_STATUS_EFFECT(id)) + unbrainwash(owner, directives) + owner.balloon_alert(caster, "[owner] snapped out of [owner.p_their()] trance!") + directives = null + caster = null + +/datum/status_effect/commanded/proc/on_attacked(datum/source, atom/attacker, attack_flags) + SIGNAL_HANDLER + if(attacker != caster || !(attack_flags & ATTACKER_DAMAGING_ATTACK)) + return + if(owner.pulledby == caster) + caster.stop_pulling() + owner.SetAllImmobility(0) + if(caster.Adjacent(owner)) // give them a split second to run away + caster.Stun(0.5 SECONDS, TRUE) + to_chat(owner, span_awe(span_reallybig("You quickly come back to your senses as you're hit by [attacker]!"))) + qdel(src) + +/datum/status_effect/commanded/proc/on_slapped(datum/source, mob/living/carbon/human/slapper) + SIGNAL_HANDLER + // no slapping yourself out of it + if(slapper == owner) + return + // gotta slap 'em in the face + if(slapper.zone_selected != BODY_ZONE_HEAD && slapper.zone_selected != BODY_ZONE_PRECISE_MOUTH) + return + if(slapper == caster || prob(10)) + to_chat(owner, span_awe(span_reallybig("You quickly come back to your senses as you're slapped by [slapper]!"))) + qdel(src) + +/atom/movable/screen/alert/status_effect/commanded + name = "Commanded" + desc = "You've been brainwashed, you can't resist the Directives engraved upon your mind!" + icon = 'modular_oculis/modules/vampires/icons/screen_alert.dmi' + icon_state = "vampire_command" + var/command + +/atom/movable/screen/alert/status_effect/commanded/Click(location, control, params) + . = ..() + if(!.) + return + to_chat(owner, span_awe(span_reallybig("[command]"))) + var/datum/antagonist/brainwashed/brainwashed = owner.mind.has_antag_datum(/datum/antagonist/brainwashed) + brainwashed.ui_interact(owner) diff --git a/modular_oculis/modules/vampires/code/powers/dominate/dominate.dm b/modular_oculis/modules/vampires/code/powers/dominate/dominate.dm new file mode 100644 index 000000000000..2b6e504fae5a --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/dominate/dominate.dm @@ -0,0 +1,21 @@ +/datum/discipline/dominate + name = "Dominate" + discipline_explanation = "Dominate is a Discipline that overwhelms another person's mind with the vampire's will, forcing victims to think or act according to the vampire's decree." + icon_state = "dominate" + + // Base only has mez, ventrue get command earlier and can upgrade it + level_1 = list(/datum/action/cooldown/vampire/targeted/mesmerize) + level_2 = list(/datum/action/cooldown/vampire/targeted/mesmerize/two) + level_3 = list(/datum/action/cooldown/vampire/targeted/mesmerize/three) + level_4 = list(/datum/action/cooldown/vampire/targeted/mesmerize/four, /datum/action/cooldown/vampire/targeted/command) + level_5 = null + +// Dominate grants a controlled Voice of God ability as a passive discipline quirk, +// similar to how Potence grants extra punch damage. +/datum/discipline/dominate/apply_discipline_quirks(datum/antagonist/vampire/clan_owner) + . = ..() + clan_owner.grant_power(new /datum/action/cooldown/vampire/voice_of_domination) + +/datum/discipline/dominate/ventrue + level_3 = list(/datum/action/cooldown/vampire/targeted/mesmerize/three, /datum/action/cooldown/vampire/targeted/command) + level_4 = list(/datum/action/cooldown/vampire/targeted/mesmerize/four, /datum/action/cooldown/vampire/targeted/command/two) diff --git a/modular_oculis/modules/vampires/code/powers/dominate/mesmerize.dm b/modular_oculis/modules/vampires/code/powers/dominate/mesmerize.dm new file mode 100644 index 000000000000..65d4d22d4687 --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/dominate/mesmerize.dm @@ -0,0 +1,193 @@ +/** + * MEZMERIZE + * Locks a target in place for a certain amount of time. + * + * Level 2: Additionally mutes + * Level 3: Can be used through face protection + * Level 5: Doesn't need to be facing you anymore + */ +/datum/action/cooldown/vampire/targeted/mesmerize + name = "Mesmerize" + desc = "Transfix the mind of a mortal after a few seconds, freezing them in place." + button_icon_state = "power_mez" + power_explanation = "Click any player to attempt to mesmerize them, and freeze them in place.\n\ + You cannot wear anything covering your face.\n\ + This will take a few seconds, and they may attempt to flee - the spell will fail if they exit the range.\n\ + If your target is already mesmerized or a Curator, you will fail.\n\ + Once mesmerized, the target will be unable to move or speak for a certain amount of time, scaling with level.\n\ + At level 3, you will be able to use the power through masks and helmets.\n\ + At level 4, you will be able to mesmerize regardless of your target's direction." + vampire_power_flags = NONE + vampire_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 + vitaecost = 75 + cooldown_time = 20 SECONDS + target_range = 4 + power_activates_immediately = FALSE + prefire_message = "Whom will you submit to your will?" + level_current = 1 + ranged_mousepointer = 'modular_oculis/modules/vampires/icons/mouse_pointers/vampire_mesmerize.dmi' + + /// Reference to the target + var/datum/weakref/target_ref + /// How long it takes us to mesmerize our target. + var/mesmerize_delay = 5 SECONDS + +/datum/action/cooldown/vampire/targeted/mesmerize/Destroy() + target_ref = null + return ..() + +/datum/action/cooldown/vampire/targeted/mesmerize/two + vitaecost = 45 + level_current = 2 + +/datum/action/cooldown/vampire/targeted/mesmerize/three + vitaecost = 60 + level_current = 3 + +/datum/action/cooldown/vampire/targeted/mesmerize/four + vitaecost = 85 + level_current = 4 + target_range = 6 + +/datum/action/cooldown/vampire/targeted/mesmerize/can_use() + . = ..() + if(!.) + return FALSE + + // Must have eyes + if(!owner.get_organ_slot(ORGAN_SLOT_EYES)) + to_chat(owner, span_warning("You have no eyes with which to mesmerize."), type = MESSAGE_TYPE_COMBAT) + return FALSE + + // Must have eyes unobstructed + var/mob/living/carbon/carbon_owner = owner + if((carbon_owner.is_eyes_covered() && level_current <= 2) || !isturf(carbon_owner.loc)) + // stupid workaround for a weird edge case with prescription glasses + if(HAS_TRAIT(carbon_owner, TRAIT_NEARSIGHTED_CORRECTED) && !carbon_owner.is_eyes_covered(~ITEM_SLOT_EYES)) + return TRUE + owner.balloon_alert(owner, "your eyes are concealed from sight.") + return FALSE + return TRUE + +/datum/action/cooldown/vampire/targeted/mesmerize/check_valid_target(atom/target_atom) + . = ..() + if(!.) + return FALSE + + // Must be a carbon or silicon + if(!iscarbon(target_atom) && !issilicon(target_atom)) + return FALSE + var/mob/living/living_target = target_atom + + // No mind + if(!living_target.mind) + owner.balloon_alert(owner, "[living_target] is mindless.") + return FALSE + + // Vampire/Curator check + if(IS_VAMPIRE(living_target) || IS_CURATOR(living_target)) + owner.balloon_alert(owner, "too powerful.") + return FALSE + + // Is our target alive or unconcious? + if(living_target.stat != CONSCIOUS) + owner.balloon_alert(owner, "[living_target] is not [(living_target.stat == DEAD || HAS_TRAIT(living_target, TRAIT_FAKEDEATH)) ? "alive" : "conscious"].") + return FALSE + + // Is our target blind? + if((!living_target.get_organ_slot(ORGAN_SLOT_EYES) || living_target.is_blind()) && !issilicon(living_target)) + owner.balloon_alert(owner, "[living_target] is blind.") + return FALSE + + // Already mesmerized? + if(living_target.has_status_effect(/datum/status_effect/mesmerized)) + owner.balloon_alert(owner, "[living_target] is already in a hypnotic gaze.") + return FALSE + +/datum/action/cooldown/vampire/targeted/mesmerize/fire_targeted_power(atom/target_atom) + . = ..() + var/mob/living/living_target = target_atom + target_ref = WEAKREF(living_target) + + // Mesmerizing silicons is instant + if(issilicon(living_target)) + var/mob/living/silicon/silicon_target = living_target + silicon_target.emp_act(EMP_HEAVY) + owner.balloon_alert(owner, "temporarily shut [silicon_target] down.") + power_activated_sucessfully() // PAY COST! BEGIN COOLDOWN! + return + + var/modified_delay = mesmerize_delay + var/eye_protection = living_target.get_eye_protection() + if(eye_protection > 0) + modified_delay += (eye_protection * 0.25) * mesmerize_delay + to_chat(owner, span_warning("[living_target] is wearing eye-protection, it will take longer to mesmerize [living_target.p_them()]."), type = MESSAGE_TYPE_COMBAT) + owner.balloon_alert(owner, "attempting to hypnotize [living_target], but [living_target.p_they()] [living_target.p_are()] partially protected!") + else + owner.balloon_alert(owner, "attempting to hypnotize [living_target]...") + + if(!do_after(owner, modified_delay, living_target, extra_checks = CALLBACK(src, PROC_REF(continue_active)), hidden = TRUE)) + deactivate_power() + return + + owner.balloon_alert(owner, "successfully mesmerized [living_target].") + + //Actually mesmerize them now + var/power_time = 9 SECONDS + level_current * 1.5 SECONDS + living_target.apply_status_effect(/datum/status_effect/mesmerized, owner, power_time) + + power_activated_sucessfully() // PAY COST! BEGIN COOLDOWN! + +/datum/action/cooldown/vampire/targeted/mesmerize/continue_active() + . = ..() + if(!.) + return FALSE + + if(!can_use()) + return FALSE + + var/mob/living/living_target = target_ref?.resolve() + if(!living_target || !check_valid_target(living_target)) + return FALSE + +/datum/action/cooldown/vampire/targeted/mesmerize/deactivate_power() + . = ..() + target_ref = null + +/datum/status_effect/mesmerized + id = "mesmerized" + duration = 15 SECONDS + tick_interval = STATUS_EFFECT_NO_TICK + alert_type = null + /// The mob that mesmerized the victim. + var/mob/living/caster + /// Traits given to the mesmerized victim. + var/list/mesmerized_traits = list( + TRAIT_HANDS_BLOCKED, + TRAIT_IMMOBILIZED, + TRAIT_INCAPACITATED, + TRAIT_MUTE, + ) + +/datum/status_effect/mesmerized/Destroy() + . = ..() + caster = null + +/datum/status_effect/mesmerized/on_creation(mob/living/new_owner, mob/living/caster, duration) + src.caster = caster + src.duration = duration + return ..() + +/datum/status_effect/mesmerized/on_apply() + owner.add_client_colour(/datum/client_colour/glass_colour/pink, TRAIT_STATUS_EFFECT(id)) + owner.add_traits(mesmerized_traits, TRAIT_STATUS_EFFECT(id)) + to_chat(owner, span_awe("[caster]'s eyes glitter so beautifully... You're mesmerized!"), type = MESSAGE_TYPE_COMBAT) + owner.playsound_local(null, 'modular_oculis/modules/vampires/sound/mesmerize.ogg', 100, FALSE, pressure_affected = FALSE) + return TRUE + +/datum/status_effect/mesmerized/on_remove() + owner.remove_client_colour(TRAIT_STATUS_EFFECT(id)) + owner.remove_traits(mesmerized_traits, TRAIT_STATUS_EFFECT(id)) + to_chat(owner, span_awe(span_big("With the spell waning, so does your memory of being mesmerized.")), type = MESSAGE_TYPE_COMBAT) + if(CAN_THEY_SEE(owner, caster)) + owner.balloon_alert(caster, "snapped out of [owner.p_their()] trance!") diff --git a/modular_oculis/modules/vampires/code/powers/dominate/voice_of_domination.dm b/modular_oculis/modules/vampires/code/powers/dominate/voice_of_domination.dm new file mode 100644 index 000000000000..760fe718d520 --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/dominate/voice_of_domination.dm @@ -0,0 +1,42 @@ +/// Tiny ability datum just to get vampies a voice of god power +/datum/action/cooldown/vampire/voice_of_domination + name = "Voice of Domination" + desc = "Speak with an overwhelmingly dominant voice, forcing mortals to briefly obey your command." + button_icon = 'icons/mob/actions/actions_items.dmi' + button_icon_state = "voice_of_god" + power_explanation = "Activate this power to speak a command using the Voice of God.\n\ + Listeners will be compelled to obey simple commands such as 'stop', 'drop', 'sleep', 'come here', etc.\n\ + This is a weaker version of the divine Voice of God, granted passively by your mastery of Dominate." + vampire_power_flags = NONE + vampire_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 + vitaecost = 75 + cooldown_time = 60 SECONDS + +/datum/action/cooldown/vampire/voice_of_domination/can_use() + . = ..() + if(!.) + return FALSE + var/mob/living/carbon/carbon_owner = owner + if(!carbon_owner.get_organ_slot(ORGAN_SLOT_TONGUE)) + owner.balloon_alert(owner, "you have no tongue!") + return FALSE + if(carbon_owner.is_mouth_covered() || !isturf(carbon_owner.loc)) + owner.balloon_alert(owner, "your mouth is blocked.") + return FALSE + if(HAS_TRAIT(carbon_owner, TRAIT_MUTE)) + owner.balloon_alert(owner, "you cannot speak!") + return FALSE + return TRUE + +/datum/action/cooldown/vampire/voice_of_domination/activate_power() + . = ..() + var/command = tgui_input_text(owner, "Speak with the Voice of Domination", "Command", max_length = MAX_MESSAGE_LEN, encode = FALSE) + if(QDELETED(src) || QDELETED(owner) || !command || !currently_active) + vampiredatum_power.adjust_blood_volume(vitaecost) // refund the blood + deactivate_power() + StartCooldown(0) + return + playsound(get_turf(owner), 'sound/effects/magic/clockwork/invoke_general.ogg', 100, TRUE, 3) + var/command_cooldown = voice_of_god(command, owner, list("colossus", "commands"), base_multiplier = 2) + cooldown_time = max(command_cooldown, 60 SECONDS) + deactivate_power() diff --git a/modular_oculis/modules/vampires/code/powers/feed.dm b/modular_oculis/modules/vampires/code/powers/feed.dm new file mode 100644 index 000000000000..8e4a6e95f673 --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/feed.dm @@ -0,0 +1,637 @@ +#define FEED_SILENT_NOTICE_RANGE 1 +#define FEED_LOUD_NOTICE_RANGE 7 +#define FEED_DEFAULT_TIME 10 SECONDS +#define FEED_FRENZY_TIME 2 SECONDS +#define FEED_BLOOD_FROM_MICE 25 + +/datum/action/cooldown/vampire/targeted/feed + name = "Feed" + desc = "Feed blood off of a living creature." + button_icon_state = "power_feed" + power_explanation = "Activate Feed and select a target to start draining their blood.\n\ + You will begin to entrance them into accepting your advances.\n\ + The time needed before you start feeding decreases the higher level you are.\n\ + If you are feeding normally they will forget that they were ever fed off.\n\ + Mice can be fed off if you are in desperate need of blood.\n\ + Feeding off of someone while you have them aggressively grabbed while in combat mode, will put them to sleep and make you feed faster. \ + This is very obvious and the radius in which you can be detected is much larger!\n\ + IMPORTANT: You are given a Masquerade Infraction if a mortal witnesses you while feeding.\n\ + IMPORTANT: Should you drain another vampire, you will absorb their power!" + vampire_power_flags = BP_AM_TOGGLE | BP_AM_STATIC_COOLDOWN + vampire_check_flags = BP_CANT_USE_IN_TORPOR | BP_CANT_USE_WHILE_STAKED | BP_CANT_USE_WHILE_INCAPACITATED | BP_CANT_USE_WHILE_UNCONSCIOUS + special_flags = VAMPIRE_DEFAULT_POWER + cooldown_time = 1 SECONDS + target_range = 1 + prefire_message = "Select a target." + power_activates_immediately = FALSE + ranged_mousepointer = 'modular_oculis/modules/vampires/icons/mouse_pointers/vampire_feed.dmi' + /// Amount of blood taken, reset after each Feed. Used for logging. + var/blood_taken = 0 + /// The amount of Blood a target has since our last feed, this loops and lets us not spam alerts of low blood. + var/warning_target_bloodvol = BLOOD_VOLUME_MAXIMUM + /// Reference to the target we've fed off of + var/datum/weakref/target_ref + /// Are we feeding with passive grab or not? + var/silent_feed = TRUE + + /// Have we fed till fatal? + var/feed_fatal = FALSE + /// During feeding, have we breached the masquerade? + var/masquerade_breached = FALSE + /// Are we at a stage of the process where we can be noticed? + var/currently_feeding = FALSE + /// Did we complete the thirster objective this drain? + /// We won't give a bad moodlet for dead bodies if so. + var/completing_thirster = FALSE + +/datum/action/cooldown/vampire/targeted/feed/can_use() + . = ..() + if(!.) + return FALSE + + // Already feeding + if(target_ref) + return FALSE + // Mouth covered + var/mob/living/carbon/user = owner + if(user?.is_mouth_covered() && !isplasmaman(user)) + owner.balloon_alert(owner, "mouth covered!") + return FALSE + +/datum/action/cooldown/vampire/targeted/feed/continue_active() + . = ..() + if(!.) + return FALSE + + var/mob/living/target = target_ref.resolve() + if(!target) + return FALSE + if(!owner.Adjacent(target)) + return FALSE + + // Check if we are seen while feeding, from the vampire's POV + if(currently_feeding) + var/turf/our_turf = get_turf(owner) + var/turf/target_turf = get_turf(target) + var/is_dark = min(our_turf.get_lumcount(), target_turf.get_lumcount()) <= LIGHTING_TILE_IS_DARK + + var/notice_range = silent_feed ? FEED_SILENT_NOTICE_RANGE : FEED_LOUD_NOTICE_RANGE + var/list/potential_watchers = oviewers(notice_range, target) | oviewers(notice_range, owner) + for(var/mob/living/watcher in potential_watchers - target) + if(!vampiredatum_power.is_masq_watcher(watcher)) + continue + if(is_dark && !watcher.Adjacent(owner) && !watcher.Adjacent(target)) + continue + + if(!INCAPACITATED_IGNORING(watcher, INCAPABLE_RESTRAINTS)) + watcher.face_atom(owner) + watcher.do_alert_animation(watcher) + to_chat(watcher, span_warning("[owner] is biting [target]'s neck!"), type = MESSAGE_TYPE_WARNING) + playsound(watcher, 'sound/machines/chime.ogg', 50, FALSE, -5) + + owner.balloon_alert(owner, "feed noticed!") + if(!masquerade_breached) + masquerade_breached = TRUE + vampiredatum_power.give_masquerade_infraction() + + return TRUE + +/datum/action/cooldown/vampire/targeted/feed/check_valid_target(atom/target_atom) + . = ..() + if(!.) + return FALSE + + // Must be living + if(!isliving(target_atom)) + return FALSE + var/mob/living/target = target_atom + // Mice check + if(ismouse(target)) + if(vampiredatum_power.my_clan?.blood_drink_type == VAMPIRE_DRINK_SNOBBY) + owner.balloon_alert(owner, "too disgusting!") + return FALSE + else + return TRUE + // Has to be human or a monkey + if(!ishuman(target)) + owner.balloon_alert(owner, "cant feed off!") + return FALSE + // Mindless and snobby? + if(!target.mind && vampiredatum_power.my_clan?.blood_drink_type == VAMPIRE_DRINK_SNOBBY && !HAS_TRAIT(owner, TRAIT_FRENZY)) + owner.balloon_alert(owner, "ew, no!") + return FALSE + // Cannot be a curator + if(IS_CURATOR(target)) + owner.balloon_alert(owner, "[target] is too powerful!") + return FALSE + var/datum/antagonist/vampire/target_vampire = IS_VAMPIRE(target) + if(target_vampire && (vampiredatum_power.scourge || vampiredatum_power.prince) && !target_vampire.broke_masquerade) + owner.balloon_alert(owner, "cannot diablerize non-masquerade breakers as royalty!") + return FALSE + // Human checks + if(ishuman(target)) + // Cannot drink from those without blood + var/mob/living/carbon/human/human_target = target + if(!human_target.dna?.species || HAS_TRAIT(human_target, TRAIT_NOBLOOD)) + owner.balloon_alert(owner, "no blood!") + return FALSE + // Cannot be wearing super thick gear + if(!human_target.can_inject(owner, BODY_ZONE_HEAD, INJECT_CHECK_PENETRATE_THICK)) + owner.balloon_alert(owner, "suit too thick!") + return FALSE + + if(isliving(owner)) + var/mob/living/living_owner = owner + if(living_owner.body_position != STANDING_UP) + living_owner.balloon_alert(living_owner, "must be standing!") + return FALSE + + if(iscarbon(owner)) + var/mob/living/carbon/carbon_owner = owner + if(carbon_owner.handcuffed) + carbon_owner.balloon_alert(carbon_owner, "can't feed while restrained!") + return FALSE + + silent_feed = TRUE + +/datum/action/cooldown/vampire/targeted/feed/fire_targeted_power(atom/target_atom) + . = ..() + var/mob/living/feed_target = target_atom + var/mob/living/living_owner = owner + target_ref = WEAKREF(feed_target) + + // Mice + if(ismouse(feed_target)) + to_chat(owner, span_warning("You recoil at the taste of a lesser lifeform.")) + vampiredatum_power.adjust_blood_volume(FEED_BLOOD_FROM_MICE) + power_activated_sucessfully() + feed_target.death() + return + + ////////////////////////// + //We start here properly// + ////////////////////////// + + currently_feeding = FALSE + masquerade_breached = FALSE + + if(!living_owner.combat_mode) + + // Don't allow normal feed on vamps. It's too easy and feels unfair. + if(IS_VAMPIRE(feed_target)) + owner.balloon_alert(owner, "too powerful, knock them out and combat feed on them!") + deactivate_power() + return + + if(!IS_VASSAL(feed_target)) // Vassals don't need all this shit. + owner.balloon_alert(owner, "mesmerizing [feed_target]...") + + // Initial ""mesmerize"" + if(!do_after(owner, 2 SECONDS, feed_target, hidden = TRUE)) + owner.balloon_alert(owner, "interrupted!") + deactivate_power() + return + + // Succesful. Start feeding process by getting feed time. + var/feed_time = (HAS_TRAIT(owner, TRAIT_FRENZY) ? FEED_FRENZY_TIME : clamp(round(FEED_DEFAULT_TIME / (1.25 * (level_current || 1))), 1, FEED_DEFAULT_TIME)) / 2 + + if(!IS_VASSAL(feed_target)) + feed_time /= 4 + + feed_target.playsound_local(null, 'modular_oculis/modules/vampires/sound/mesmerize.ogg', 100, FALSE, pressure_affected = FALSE) + feed_target.Stun(feed_time, TRUE) + feed_target.become_blind(REF(src)) + ADD_TRAIT(feed_target, TRAIT_DEAF, REF(src)) + + to_chat(feed_target, span_hypnophrase("You suddenly fall into a deep trance..."), type = MESSAGE_TYPE_WARNING) + owner.balloon_alert(owner, "subdued! starting feed...") + + // Do the pre-feed. + if(!do_after(owner, feed_time, feed_target, NONE, TRUE, hidden = TRUE)) + owner.balloon_alert(owner, "interrupted!") + deactivate_power() + return + + // It begins... + currently_feeding = TRUE + living_owner.playsound_local(null, 'modular_oculis/modules/vampires/sound/drinkblood1.ogg', 100, FALSE, pressure_affected = FALSE) + + // Just to make sure + living_owner.stop_pulling() + feed_target.stop_pulling() + + // omega switch + switch(get_dir(owner.loc, feed_target.loc)) + if(NORTH) + owner.dir = WEST + feed_target.dir = EAST + animate(owner, 0.2 SECONDS, pixel_x = 8, pixel_y = 16) + animate(feed_target, 0.2 SECONDS, pixel_x = -8, pixel_y = -16) + if(NORTHEAST) + owner.dir = EAST + feed_target.dir = WEST + animate(owner, 0.2 SECONDS, pixel_x = 8, pixel_y = 16) + animate(feed_target, 0.2 SECONDS, pixel_x = -8, pixel_y = -16) + if(EAST) + owner.dir = EAST + feed_target.dir = WEST + animate(owner, 0.2 SECONDS, pixel_x = 8) + animate(feed_target, 0.2 SECONDS, pixel_x = -8) + if(SOUTH) + owner.dir = EAST + feed_target.dir = WEST + animate(owner, 0.2 SECONDS, pixel_x = -8, pixel_y = -16) + animate(feed_target, 0.2 SECONDS, pixel_x = 8, pixel_y = 16) + if(SOUTHEAST) + owner.dir = EAST + feed_target.dir = WEST + animate(owner, 0.2 SECONDS, pixel_x = 8, pixel_y = -16) + animate(feed_target, 0.2 SECONDS, pixel_x = -8, pixel_y = 16) + if(SOUTHWEST) + owner.dir = WEST + feed_target.dir = EAST + animate(owner, 0.2 SECONDS, pixel_x = -8, pixel_y = -16) + animate(feed_target, 0.2 SECONDS, pixel_x = 8, pixel_y = 16) + if(WEST) + owner.dir = WEST + feed_target.dir = EAST + animate(owner, 0.2 SECONDS, pixel_x = -8) + animate(feed_target, 0.2 SECONDS, pixel_x = 8) + if(NORTHWEST) + owner.dir = WEST + feed_target.dir = EAST + animate(owner, 0.2 SECONDS, pixel_x = -8, pixel_y = 16) + animate(feed_target, 0.2 SECONDS, pixel_x = 8, pixel_y = -16) + if(0) // We are on the same tile. Just move them a bit so they don't overlap + owner.dir = WEST + feed_target.dir = EAST + animate(owner, 0.2 SECONDS, pixel_x = 8,) + animate(feed_target, 0.2 SECONDS, pixel_x = -8) + + owner.visible_message( + span_notice("[owner] grabs [feed_target] tightly, biting into [feed_target.p_their()] neck!"), + span_notice("You slip your fangs into [feed_target]'s neck."), + vision_distance = FEED_SILENT_NOTICE_RANGE, ignored_mobs = feed_target + ) + + else if(owner.pulling == feed_target && owner.grab_state == GRAB_AGGRESSIVE) // COMBAT FEED BELOW HERE!!!!!!!!!! + + playsound(living_owner, 'modular_oculis/modules/vampires/sound/drinkblood1.ogg', 50) + + feed_target.Stun((5 + level_current) SECONDS) + feed_target.set_jitter_if_lower((5 + level_current) SECONDS) + + owner.visible_message( + span_warning("[owner] closes [owner.p_their()] mouth around [feed_target]'s neck!"), + span_warning("You sink your fangs into [feed_target]'s neck."), ignored_mobs = feed_target + ) + + to_chat(feed_target, span_bolddanger("[owner] seizes you with incredible strength, sinking [owner.p_their()] fangs into your neck!"), type = MESSAGE_TYPE_WARNING) + + to_chat(owner, span_announce("* Vampire Tip: Combat feeding does not erase their memories!")) + + currently_feeding = TRUE + silent_feed = FALSE + + // Garlic in 'em + var/mob/living/smacked = feed_target + if(smacked.reagents?.has_reagent(/datum/reagent/consumable/garlic, 2)) + + // We check which turf is one step away from our target, in the direction of the angle of the bullet. Christ. We do this twice, for range. + var/target_turf = get_step_away(smacked.loc, owner, 2) + + to_chat(owner, span_hypnophrase(span_big("eugh.. garlic..."))) + + living_owner.Stun(5 SECONDS) + living_owner.set_dizzy_if_lower(1 SECONDS) + living_owner.set_jitter_if_lower(1.5 SECONDS) + living_owner.set_eye_blur_if_lower(0.5 SECONDS) + + smacked.Unconscious(1 SECONDS) + smacked.throw_at(target_turf, 2, 1, spin = TRUE) + playsound(smacked, 'sound/items/weapons/cqchit2.ogg', 80) + deactivate_power() + return + /* else if(istype(smacked.get_item_by_slot(ITEM_SLOT_NECK), /obj/item/clothing/neck/crucifix)) + owner.visible_message( + span_warning("[owner] recoils, quickly releasing [smacked] from [owner.p_their()] grip!"), + span_userdanger("The Faith burns you, preventing you from feeding!"), + ) + living_owner.take_overall_damage(burn = rand(5, 15)) + living_owner.set_jitter_if_lower(5 SECONDS) + living_owner.set_eye_blur_if_lower(2 SECONDS) + playsound( + owner, + pick('sound/effects/wounds/sizzle1.ogg', 'sound/effects/wounds/sizzle2.ogg'), + vol = 50, + vary = TRUE, + extrarange = SHORT_RANGE_SOUND_EXTRARANGE, + ) + deactivate_power() + if(owner.pulling == smacked) + owner.stop_pulling() + return */ + + if(currently_feeding) // Check if we actually started successfully. + owner.add_traits(list(TRAIT_IMMOBILIZED, TRAIT_MUTE, TRAIT_HANDS_BLOCKED), REF(src)) + feed_target.add_traits(list(TRAIT_IMMOBILIZED, TRAIT_MUTE, TRAIT_HANDS_BLOCKED), REF(src)) + + // Normally removed traits are done. Now we give the victim a lil something to remember us by. + feed_target.apply_status_effect(/datum/status_effect/feed_marked) + else + owner.balloon_alert(owner, "combat feed requires aggressive grab!") + deactivate_power() + return FALSE + +/datum/action/cooldown/vampire/targeted/feed/use_power() + var/mob/living/user = owner + + var/mob/living/feed_target = target_ref?.resolve() + if(!feed_target) + power_activated_sucessfully() + return + + if(!continue_active()) + if(!silent_feed) + user.visible_message( + span_warning("[user] is ripped from [feed_target]'s throat. [feed_target.p_their(TRUE)] blood sprays everywhere!"), + span_warning("Your teeth are ripped from [feed_target]'s throat. [feed_target.p_their(TRUE)] blood sprays everywhere!")) + + // Time to start bleeding + if(iscarbon(feed_target)) + var/mob/living/carbon/carbon_target = feed_target + carbon_target.bleed(15) + playsound(get_turf(feed_target), 'sound/effects/splat.ogg', 40, TRUE) + + feed_target.add_splatter_floor(get_turf(feed_target)) + + // Cover both parties in blood + user.add_mob_blood(feed_target) // Put target's blood on us. The donor goes in the ( ) + feed_target.add_mob_blood(feed_target) + + 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.adjustBleedStacks(10) + + // Ow + feed_target.apply_damage(10, BRUTE, BODY_ZONE_HEAD) + INVOKE_ASYNC(feed_target, TYPE_PROC_REF(/mob, emote), "scream") + + power_activated_sucessfully() + return + + // Adjust blood + var/feed_strength_mult = 0.3 + if(HAS_TRAIT(user, TRAIT_FRENZY)) + feed_strength_mult = 2 + else if(!silent_feed) + feed_strength_mult = 1 + + handle_feeding(feed_target, feed_strength_mult) + + // Mood events + if(vampiredatum_power.my_clan?.blood_drink_type == VAMPIRE_DRINK_SNOBBY && !feed_target.mind) // Snobby + user.add_mood_event("drankblood", /datum/mood_event/drankblood_bad) + else if(feed_target.stat == DEAD) // Dead + user.add_mood_event("drankblood", /datum/mood_event/drankblood_dead) + else // Normal + user.add_mood_event("drankblood", /datum/mood_event/drankblood) + + // Alert the vampire to the target's blood level + if(feed_target.blood_volume <= BLOOD_VOLUME_BAD && warning_target_bloodvol > BLOOD_VOLUME_BAD) + owner.balloon_alert(owner, "your victim's blood is fatally low!") + feed_fatal = TRUE + else if(feed_target.blood_volume <= BLOOD_VOLUME_OKAY && warning_target_bloodvol > BLOOD_VOLUME_OKAY) + owner.balloon_alert(owner, "your victim's blood is dangerously low.") + else if(feed_target.blood_volume <= BLOOD_VOLUME_SAFE && warning_target_bloodvol > BLOOD_VOLUME_SAFE) + owner.balloon_alert(owner, "your victim's blood is at an unsafe level.") + warning_target_bloodvol = feed_target.blood_volume + + // Check if full on blood + if(vampiredatum_power.current_vitae >= vampiredatum_power.max_vitae) + if(IS_VAMPIRE(feed_target)) + owner.balloon_alert(owner, "we are full on blood, but we can continue feeding to absorb [feed_target.p_their()] power!") + else + owner.balloon_alert(owner, "we are full on blood!") + + // Check if target has an acceptable amount of blood left + if(feed_target.blood_volume <= 10) + owner.balloon_alert(owner, "no blood left!") + if(feed_target.client) + var/datum/objective/vampire/hedonism/thirster/yumy = locate() in vampiredatum_power.objectives + if(yumy && !yumy.completed) + yumy.completed = TRUE + completing_thirster = TRUE + power_activated_sucessfully() + return + + if(IS_VAMPIRE(feed_target)) + var/datum/antagonist/vampire/target_vampire = IS_VAMPIRE(feed_target) + if(target_vampire.current_vitae <= 50) + diablerie(feed_target) + power_activated_sucessfully() + return + + // Play heartbeat sound effect to vampire and target + owner.playsound_local(null, 'sound/effects/singlebeat.ogg', 40, TRUE) + feed_target.playsound_local(null, 'sound/effects/singlebeat.ogg', 40, TRUE) + +/// We assume the target is a vampire. +/datum/action/cooldown/vampire/targeted/feed/proc/diablerie(mob/living/poor_sap) + var/datum/antagonist/vampire/victim = IS_VAMPIRE(poor_sap) + + var/levels_absorbed = ceil((victim.vampire_level + victim.vampire_level_unspent) / DIABLERIE_DIVISOR) + + vampiredatum_power.rank_up(levels_absorbed, TRUE) + vampiredatum_power.adjust_humanity(-victim.humanity / 3) + vampiredatum_power.diablerie_count++ + + victim.final_death() + +/datum/action/cooldown/vampire/targeted/feed/deactivate_power() + . = ..() + REMOVE_TRAITS_IN(owner, REF(src)) + + // Did we already take humanity for killing them? + var/humanity_deducted = FALSE + var/mob/living/feed_target = target_ref?.resolve() + var/mob/living/living_owner = owner + + if(feed_target) + // Call cure_blind after a (truly tiny) delay to make sure they don't see NOTHING + addtimer(CALLBACK(feed_target, TYPE_PROC_REF(/mob/living, remove_status_effect), /datum/status_effect/grouped/blindness, REF(src)), 1 SECONDS) + REMOVE_TRAITS_IN(feed_target, REF(src)) + if(currently_feeding) + + animate(owner, 0.2 SECONDS, pixel_x = 0, pixel_y = 0) + animate(feed_target, 0.2 SECONDS, pixel_x = 0, pixel_y = 0) + + log_combat(owner, feed_target, "fed on blood [silent_feed ? "silently" : "aggressively"]", addition = "(and took [blood_taken] blood)") + + to_chat(owner, span_notice("You slowly release [feed_target].")) + + if(feed_target.stat != DEAD && silent_feed) + to_chat(owner, span_notice("[feed_target.p_They()] look[feed_target.p_s()] dazed, and will not remember this."), type = MESSAGE_TYPE_INFO) + if(!IS_VASSAL(feed_target)) + to_chat(feed_target, span_awe(span_reallybig("You wake from your trance. Everything is so... hazy... You don't remember the last few moments...")), type = MESSAGE_TYPE_INFO) + to_chat(feed_target, span_warning(" * You do not remember that you have been fed on, the identity of the person who just fed on you, or the fact that they are a vampire."), type = MESSAGE_TYPE_INFO) + to_chat(feed_target, span_notice(" * If you already knew this person was a vampire from before your current encounter with them, however, you retain memory of that."), type = MESSAGE_TYPE_INFO) + else + to_chat(feed_target, span_awe(span_reallybig("You wake from your trance. Everything is so... hazy...")), type = MESSAGE_TYPE_INFO) + if(feed_target.blood_volume >= BLOOD_VOLUME_OKAY) + to_chat(feed_target, span_announce("You feel dizzy, but it will probably pass by itself!"), type = MESSAGE_TYPE_INFO) + + if(!completing_thirster) + if(feed_target.stat == DEAD) + living_owner.add_mood_event("drankkilled", /datum/mood_event/drankkilled) + humanity_deducted = TRUE + + if(feed_fatal && !humanity_deducted) + living_owner.add_mood_event("drankkilled", /datum/mood_event/drankkilled) + to_chat(owner, span_userdanger("No way will [feed_target.p_they()] survive that...")) + vampiredatum_power.adjust_humanity(-1) + +/* + if(iscarbon(feed_target)) + var/mob/living/carbon/carbon_target = feed_target + // More/less humanity adds/deducts bleedy. + switch(vampiredatum_power.humanity) + if(0 to 2) + carbon_target.bleed(BLEED_CRITICAL) + if(3 to 4) + carbon_target.bleed(BLEED_DEEP_WOUND) + if(5 to 6) + carbon_target.bleed(BLEED_CUT) + if(7 to 8) + carbon_target.bleed(BLEED_SURFACE) + if(9 to 10) + carbon_target.bleed(BLEED_SCRATCH) +*/ + + feed_fatal = FALSE + humanity_deducted = FALSE + completing_thirster = FALSE + + target_ref = null + + warning_target_bloodvol = BLOOD_VOLUME_MAXIMUM + blood_taken = 0 + +/datum/action/cooldown/vampire/targeted/feed/proc/handle_feeding(mob/living/carbon/target, mult = 1) + var/mob/living/living_owner = owner + var/feed_amount = 50 + (level_current * 2) + + // If we are already at fatal, we speed up more. + if(feed_fatal) + feed_amount *= 1.5 + + // But, if we are in combat we want to get them some time to react. + if(!silent_feed) + feed_amount *= 0.3 + + var/blood_to_take = min(feed_amount * mult, target.blood_volume) + + // Remove target's blood + target.adjust_blood_volume(-blood_to_take) + + // Shift body temperature (toward target's temp, by volume taken) + // ((vamp_blood_volume * vamp_temp) + (target_blood_volume * target_temp)) / (vamp_blood_volume + blood_to_take) + // owner.bodytemperature = ((vampiredatum_power.current_vitae * owner.bodytemperature) + (blood_to_take * target.bodytemperature)) / (vampiredatum_power.current_vitae + blood_to_take) + + // Penalty for dead blood(at least it's still human, right?) + if(target.stat == DEAD) + blood_to_take /= 3 + // Penalty for non-human blood + if(!ishuman(target) || ismonkey(target)) + blood_to_take /= 10 + // Penalty for frenzy(messy eater) + if(HAS_TRAIT(living_owner, TRAIT_FRENZY)) + blood_to_take /= 2 + + // Give vampire the blood^ + var/vitae_absorbed = blood_to_take * 4 + + /// Tracking of the vitae goal + if(target.client && !target.mind?.has_antag_datum(/datum/antagonist/changeling)) + vampiredatum_power.vitae_goal_progress += vitae_absorbed + + vampiredatum_power.adjust_blood_volume(vitae_absorbed) + + // Diablerie takes vitae directly + var/datum/antagonist/vampire/vampire_target = IS_VAMPIRE(target) + if(vampire_target) + vampire_target.adjust_blood_volume(- (blood_to_take * 4)) + + // Transfer the target's reagents into the vampire's blood + if(target.reagents?.total_volume) + target.reagents.trans_to(owner, 1, methods = INGEST) // Run transfer of 1 unit of reagent from them to me. + + // Play heartbeat sound for flavor + owner.playsound_local(null, 'sound/effects/singlebeat.ogg', 40, TRUE) + + vampiredatum_power.total_blood_drank += blood_to_take + blood_taken += blood_to_take + + // If we are on combat feed, we only want it to take a bit and then stop. Except if they are not conscious or if they're restrained. + if(!silent_feed && blood_taken >= 60 && target.stat <= SOFT_CRIT && !HAS_TRAIT(target, TRAIT_RESTRAINED)) + + playsound(target, 'sound/items/weapons/cqchit2.ogg', 80) + + owner.visible_message( + span_warning("[target] struggles, pushing [owner] away!"), + span_warning("[target] manages to struggle free from your grip!"), ignored_mobs = target + ) + + var/shove_dir = get_dir(target.loc, owner.loc) + var/turf/target_shove_turf = get_step(owner.loc, shove_dir) + owner.Move(target_shove_turf, shove_dir) + + target.SetStun(0 SECONDS) + living_owner.Stun(1 SECONDS) + + owner.balloon_alert(owner, "struggles free!") + deactivate_power() + +#undef FEED_SILENT_NOTICE_RANGE +#undef FEED_LOUD_NOTICE_RANGE +#undef FEED_DEFAULT_TIME +#undef FEED_FRENZY_TIME +#undef FEED_BLOOD_FROM_MICE + +/atom/movable/screen/fullscreen/blind/feed + icon_state = "feed" + render_target = "blind_fullscreen_overlay" + layer = BLIND_LAYER + plane = FULLSCREEN_PLANE + +/datum/status_effect/feed_marked + id = "feed marked" + tick_interval = STATUS_EFFECT_NO_TICK + processing_speed = STATUS_EFFECT_NORMAL_PROCESS + status_type = STATUS_EFFECT_REFRESH + alert_type = null + remove_on_fullheal = TRUE + heal_flag_necessary = HEAL_WOUNDS + +/datum/status_effect/feed_marked/on_apply() + if(!iscarbon(owner)) + return FALSE + RegisterSignal(owner, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine)) + return TRUE + +/datum/status_effect/feed_marked/on_remove() + UnregisterSignal(owner, COMSIG_ATOM_EXAMINE) + +/datum/status_effect/feed_marked/on_creation(mob/living/new_owner, ...) + duration = rand(5 MINUTES, 10 MINUTES) + return ..() + +/datum/status_effect/feed_marked/refresh(effect, ...) + duration = max(duration, world.time + rand(5 MINUTES, 10 MINUTES)) + +/datum/status_effect/feed_marked/proc/on_examine(atom/source, mob/user, list/examine_list) + SIGNAL_HANDLER + if(isobserver(user) || (get_dist(user, owner) <= 3 && !user.is_nearsighted_currently())) + examine_list += span_warning("There are two strange punctures on [owner.p_their()] neck.") diff --git a/modular_oculis/modules/vampires/code/powers/feign_life.dm b/modular_oculis/modules/vampires/code/powers/feign_life.dm new file mode 100644 index 000000000000..a33e67b8466e --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/feign_life.dm @@ -0,0 +1,79 @@ +/datum/action/cooldown/vampire/feign_life + name = "Feign Life" + 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 = "Feign Life will forge your identity to be practically identical to that of a human.\n\ + You lose nearly all Vampire benefits, including your passive healing.\n\ + You gain a Genetic sequence, and appear to have 100% blood when scanned by a Health Analyzer.\n\ + You won't appear as pale when examined. Anything further than pale, however, will not be hidden.\n\ + After deactivating Feign Life, you will re-gain your Vampiric abilities, as well as lose any Diseases or mutations you might have gained." + vampire_power_flags = BP_AM_TOGGLE | BP_AM_STATIC_COOLDOWN | BP_AM_COSTLESS_UNCONSCIOUS + vampire_check_flags = BP_CANT_USE_IN_FRENZY | BP_CANT_USE_IN_TORPOR | BP_CANT_USE_WHILE_STAKED | BP_ALLOW_WHILE_SILVER_CUFFED + vitaecost = 15 + cooldown_time = 5 SECONDS + constant_vitaecost = 0.5 + +/datum/action/cooldown/vampire/feign_life/activate_power() + . = ..() + var/mob/living/carbon/carbon_owner = owner + carbon_owner.balloon_alert(carbon_owner, "masquerade turned on.") + carbon_owner.apply_status_effect(/datum/status_effect/masquerade) + +/datum/action/cooldown/vampire/feign_life/deactivate_power() + . = ..() + var/mob/living/carbon/carbon_owner = owner + carbon_owner.balloon_alert(carbon_owner, "masquerade turned off.") + carbon_owner.remove_status_effect(/datum/status_effect/masquerade) + +/datum/status_effect/masquerade + id = "masquerade" + duration = STATUS_EFFECT_PERMANENT + tick_interval = STATUS_EFFECT_NO_TICK + alert_type = /atom/movable/screen/alert/status_effect/feign_life + +/atom/movable/screen/alert/status_effect/feign_life + name = "Feign Life" + desc = "You are currently hiding your identity using the Feign Life power. This halts Vampiric healing." + icon = 'modular_oculis/modules/vampires/icons/actions_vampire.dmi' + icon_state = "masquerade_alert" + alerttooltipstyle = "cult" + +/datum/status_effect/masquerade/on_apply() + var/mob/living/carbon/carbon_owner = owner + var/datum/antagonist/vampire/vampiredatum = IS_VAMPIRE(carbon_owner) + if(!vampiredatum) + return FALSE + + // Handle Traits + carbon_owner.remove_traits(vampiredatum.vampire_traits - vampiredatum.always_traits, TRAIT_VAMPIRE) + carbon_owner.add_traits(list(TRAIT_FEIGN_LIFE, TRAIT_FAKEGENES), TRAIT_STATUS_EFFECT(id)) + + // Handle organs + var/obj/item/organ/heart/vampheart = carbon_owner.get_organ_slot(ORGAN_SLOT_HEART) + vampheart?.Restart() + + to_chat(carbon_owner, span_notice("Your heart beats falsely within your lifeless chest. You may yet pass for a mortal.")) + to_chat(carbon_owner, span_warning("Your vampiric healing is halted while imitating life.")) + + return TRUE + +/datum/status_effect/masquerade/on_remove() + var/mob/living/carbon/carbon_owner = owner + var/datum/antagonist/vampire/vampiredatum = IS_VAMPIRE(carbon_owner) + if(!vampiredatum) + return + + // Clear mutations and diseases + carbon_owner.dna.remove_all_mutations() + for(var/datum/disease/diseases in carbon_owner.diseases) + diseases.cure() + + // Handle Traits + carbon_owner.add_traits(vampiredatum.vampire_traits, TRAIT_VAMPIRE) + carbon_owner.remove_traits(list(TRAIT_FEIGN_LIFE, TRAIT_FAKEGENES), TRAIT_STATUS_EFFECT(id)) + + // Handle organs + var/obj/item/organ/heart/vampheart = carbon_owner.get_organ_slot(ORGAN_SLOT_HEART) + vampheart?.Stop() + + to_chat(carbon_owner, span_notice("Your heart beats one final time, while your skin dries out and your icy pallor returns.")) diff --git a/modular_oculis/modules/vampires/code/powers/fortitude/fortitude.dm b/modular_oculis/modules/vampires/code/powers/fortitude/fortitude.dm new file mode 100644 index 000000000000..830cd80ebdf6 --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/fortitude/fortitude.dm @@ -0,0 +1,118 @@ +/datum/discipline/fortitude + name = "Fortitude" + discipline_explanation = "Fortitude is a Discipline that grants Kindred unearthly toughness." + icon_state = "fortitude" + + // Lists of abilities granted per level + level_1 = list(/datum/action/cooldown/vampire/fortitude) + level_2 = list(/datum/action/cooldown/vampire/fortitude/two) + level_3 = list(/datum/action/cooldown/vampire/fortitude/three) + level_4 = list(/datum/action/cooldown/vampire/fortitude/four) + level_5 = null + +/** + * FORTITUDE + * All levels: Incrementally increasing brute and stamina resistance. + * Level 1: Pierce resistance + * Level 2: Push immunity + * Level 3: Dismember resistance + * Level 4: Complete stun immunity + */ + +/datum/action/cooldown/vampire/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 = "Grants increasing levels of brute and stamina resistance, as well as various immunities to physical harm.\n\ + At level 1: Gain pierce resistance.\n\ + At level 2: Gain push immunity.\n\ + At level 3: Gain dismember resistance.\n\ + At level 4: Gain complete stun immunity." + vampire_power_flags = BP_AM_TOGGLE | BP_AM_COSTLESS_UNCONSCIOUS + vampire_check_flags = BP_CANT_USE_IN_TORPOR | BP_CANT_USE_IN_FRENZY | BP_CANT_USE_WHILE_STAKED + vitaecost = 20 + cooldown_time = 5 SECONDS + constant_vitaecost = 0.75 + + var/resistance = 0.8 + + // Flags for what immunities to turn on at which level + var/pierce = TRUE + var/push = FALSE + var/dismember = FALSE + var/stun = FALSE + + var/calculated_burn_resist // do not touch + +/datum/action/cooldown/vampire/fortitude/two + vitaecost = 20 + constant_vitaecost = 1 + resistance = 0.6 + pierce = TRUE + push = TRUE + +/datum/action/cooldown/vampire/fortitude/three + vitaecost = 20 + constant_vitaecost = 1.25 + resistance = 0.4 + pierce = TRUE + push = TRUE + dismember = TRUE + +/datum/action/cooldown/vampire/fortitude/four + vitaecost = 20 + constant_vitaecost = 1.5 + resistance = 0.3 + pierce = TRUE + push = TRUE + dismember = TRUE + stun = TRUE + +/datum/action/cooldown/vampire/fortitude/activate_power() + . = ..() + owner.balloon_alert(owner, "fortitude turned on.") + to_chat(owner, span_notice("Your flesh has become as hard as steel!")) + owner.playsound_local(null, 'modular_oculis/modules/vampires/sound/fortitude_on.ogg', 100, FALSE, pressure_affected = FALSE) + + calculated_burn_resist = min(1, resistance * 3) + + // Traits & Effects + if(pierce) + ADD_TRAIT(owner, TRAIT_PIERCEIMMUNE, REF(src)) + if(dismember) + ADD_TRAIT(owner, TRAIT_NODISMEMBER, REF(src)) + if(push) + ADD_TRAIT(owner, TRAIT_PUSHIMMUNE, REF(src)) + if(stun) + ADD_TRAIT(owner, TRAIT_STUNIMMUNE, REF(src)) // They'll get stun resistance + this, who cares. + + var/mob/living/carbon/human/user = owner + user.physiology.brute_mod *= resistance + user.physiology.stamina_mod *= resistance * 2 // Stamina resistance is half as effective because they have it inherently. + user.physiology.burn_mod *= calculated_burn_resist // they get burn resistance, but way less + +/datum/action/cooldown/vampire/fortitude/use_power() + . = ..() + if(!.) + return + + var/mob/living/carbon/user = owner + if(istype(user.buckled, /obj/vehicle)) + user.buckled.unbuckle_mob(src, force = TRUE) + +/datum/action/cooldown/vampire/fortitude/deactivate_power() + if(!ishuman(owner)) + return + + var/mob/living/carbon/human/vampire_user = owner + vampire_user.physiology.brute_mod /= resistance + vampire_user.physiology.burn_mod /= calculated_burn_resist + vampire_user.physiology.stamina_mod /= resistance * 2 + + // Remove Traits & Effects + REMOVE_TRAITS_IN(owner, REF(src)) + + owner.balloon_alert(owner, "fortitude turned off.") + owner.playsound_local(null, 'modular_oculis/modules/vampires/sound/fortitude_off.ogg', 100, FALSE, pressure_affected = FALSE) + + return ..() diff --git a/modular_oculis/modules/vampires/code/powers/gohome.dm b/modular_oculis/modules/vampires/code/powers/gohome.dm new file mode 100644 index 000000000000..5dd787010ddf --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/gohome.dm @@ -0,0 +1,101 @@ +#define GOHOME_START 0 +#define GOHOME_FLICKER_ONE 2 +#define GOHOME_FLICKER_TWO 4 +#define GOHOME_TELEPORT 6 + +/** + * Given to Vampires if they have a Coffin claimed. + * Teleports them to their Coffin on use. + * Makes them drop everything if someone witnesses the act. + */ +/datum/action/cooldown/vampire/gohome + name = "Vanishing Act" + desc = "As dawn aproaches, disperse into mist and return directly to your haven.
WARNING: You will drop ALL of your possessions if observed by mortals." + button_icon_state = "power_gohome" + power_explanation = "Activating Vanishing Act will, after a short delay, teleport you to your Claimed Coffin.\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." + vampire_power_flags = BP_AM_STATIC_COOLDOWN + vampire_check_flags = BP_CANT_USE_IN_TORPOR | BP_CANT_USE_WHILE_STAKED | BP_CANT_USE_WHILE_INCAPACITATED | BP_CANT_USE_WHILE_UNCONSCIOUS | BP_CANT_USE_IN_FRENZY + vitaecost = 100 + cooldown_time = 5 MINUTES + ///What stage of the teleportation are we in + var/teleporting_stage = GOHOME_START + /// The types of mobs that will drop post-teleportation. + var/static/list/spawning_mobs = list( + /mob/living/basic/mouse = 3, + /mob/living/basic/bat = 1, + ) + +/datum/action/cooldown/vampire/gohome/can_use() + . = ..() + if(!.) + return FALSE + + /// Have No haven (NOTE: You only got this power if you had a haven, so this means it's destroyed) + if(!vampiredatum_power?.coffin) + owner.balloon_alert(owner, "coffin was destroyed!") + return FALSE + + if(owner.loc == vampiredatum_power.coffin) + owner.balloon_alert(owner, "you're already in your coffin!") + return FALSE + + if(!check_teleport_valid(owner, vampiredatum_power.coffin, TELEPORT_CHANNEL_MAGIC)) + owner.balloon_alert(owner, "something holds you back!") + return FALSE + + if((vampiredatum_power.current_vitae - vitaecost) <= vampiredatum_power.frenzy_threshold) + owner.balloon_alert(owner, "using this would send you into a frenzy!") + return FALSE + + if(!isturf(owner.loc)) + owner.balloon_alert(owner, "you cannot teleport right now!") + return FALSE + +/datum/action/cooldown/vampire/gohome/activate_power() + . = ..() + var/turf/old_turf = get_turf(owner) + teleport_to_coffin(owner) + flicker_lights(4, 60, old_turf) + +/datum/action/cooldown/vampire/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', vol = beat_volume, vary = TRUE) + +/datum/action/cooldown/vampire/gohome/proc/teleport_to_coffin(mob/living/carbon/user) + var/turf/current_turf = get_turf(owner) + // If we aren't in the dark, anyone watching us will cause us to drop out stuff + if(current_turf.get_lumcount() > LIGHTING_TILE_IS_DARK) + for(var/mob/living/watcher in oviewers(world.view, get_turf(owner)) - owner) + if(vampiredatum_power.is_masq_watcher(watcher)) + user.unequip_everything() + break + user.uncuff() + + playsound(current_turf, 'sound/effects/magic/summon_karp.ogg', vol = 60, vary = TRUE) + + /* var/datum/effect_system/steam_spread/vampire/puff = new /datum/effect_system/steam_spread/vampire() + puff.set_up(3, 0, current_turf) + puff.start() */ + + /// STEP FIVE: Create animal at prev location + var/mob/living/simple_animal/new_mob = pick_weight(spawning_mobs) + new new_mob(current_turf) + /// TELEPORT: Move to Coffin & Close it! + user.set_resting(TRUE, TRUE, FALSE) + do_teleport(owner, vampiredatum_power.coffin, channel = TELEPORT_CHANNEL_MAGIC, no_effects = TRUE) + vampiredatum_power.coffin.close(owner) + vampiredatum_power.coffin.take_contents() + playsound(vampiredatum_power.coffin.loc, vampiredatum_power.coffin.close_sound, 15, TRUE, -3) + + deactivate_power() + +/* /datum/effect_system/steam_spread/vampire + effect_type = /obj/effect/particle_effect/fluid/smoke/vampsmoke */ + +#undef GOHOME_START +#undef GOHOME_FLICKER_ONE +#undef GOHOME_FLICKER_TWO +#undef GOHOME_TELEPORT diff --git a/modular_oculis/modules/vampires/code/powers/levelspells.dm b/modular_oculis/modules/vampires/code/powers/levelspells.dm new file mode 100644 index 000000000000..a7149231dddf --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/levelspells.dm @@ -0,0 +1,182 @@ +/** + * Given to Vampires at the start and taken away as soon as they select a clan. + */ +/datum/action/cooldown/vampire/clanselect + name = "Select Clan" + desc = "Take the first step as a true kindred and remember your true lineage." + button_icon_state = "clanselect" + power_explanation = "Activate to select your unique vampire clan." + vampire_power_flags = BP_AM_SINGLEUSE + vampire_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 + vitaecost = 0 + cooldown_time = 5 SECONDS + +/datum/action/cooldown/vampire/clanselect/activate_power() + . = ..() + vampiredatum_power.assign_clan_and_bane() + deactivate_power() + +/** + * Given to Vampires every levelup. Opens the radial. + */ +/datum/action/cooldown/vampire/levelup + name = "Level Up" + desc = "Take another step as a full kindred, and remember your true lineage." + button_icon_state = "power_levelup" + power_explanation = "Activate to level one of your disciplines." + vampire_power_flags = BP_AM_SINGLEUSE + vampire_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 + vitaecost = 0 + cooldown_time = 5 SECONDS + +/datum/action/cooldown/vampire/levelup/activate_power() + . = ..() + vampiredatum_power.my_clan.spend_rank() + deactivate_power() + +/** + * Given to Princes once chosen. Picks a scourge. + */ +/datum/action/cooldown/vampire/targeted/scourgify + name = "Select Scourge" + desc = "Select another kindred or one of your vassals as your scourge." + button_icon_state = "power_scourge" + power_explanation = "Activate to select another kindred, or one of your vassals, as your personal scourge.\n\n\ + When used on another kindred, they will receive some levels and an objective to obey you.\n\ + When used on your vassal, you will become their sire, embracing them as a full-blooded vampire.\n\ + They will be part of your own clan, and of course receive some bonus levels as well.\n\n\ + The Scourge is your enforcer, your tool to wield in the name of the Camarilla. Use them to enforce the masquerade, and to keep control over your fellow kindred." + vampire_power_flags = BP_AM_SINGLEUSE + vampire_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 + vitaecost = 0 + cooldown_time = 35 SECONDS + power_activates_immediately = FALSE + prefire_message = "Whom will you choose?" + + /// Reference to the target antag datum + var/datum/weakref/target_ref + + /// Are we currently in the process of promote someone? + /// This is just so we don't have any issues with using the power in the time between using it to scourgify someone and when it's removed. + var/promoting = FALSE + +/datum/action/cooldown/vampire/targeted/scourgify/check_valid_target(atom/target_atom) + . = ..() + if(!isliving(target_atom)) + return FALSE + + var/mob/living/living_target = target_atom + var/datum/antagonist/vampire/target_vampire = IS_VAMPIRE(living_target) + + // No mind + if(!living_target.mind) + owner.balloon_alert(owner, "mindless") + return FALSE + + // No client + if(!living_target.client) + owner.balloon_alert(owner, "not a player") + return FALSE + + // Is our target alive or unconcious? + if(living_target.stat != CONSCIOUS) + owner.balloon_alert(owner, "not [(living_target.stat == DEAD || HAS_TRAIT(living_target, TRAIT_FAKEDEATH)) ? "alive" : "conscious"]") + return FALSE + + if(vampiredatum_power.is_someone_elses_vassal(living_target)) // Only our own vassal may be promoted. + owner.balloon_alert(owner, "not your vassal") + return FALSE + + if(!IS_VAMPIRE(living_target) && !IS_VASSAL(living_target)) + owner.balloon_alert(owner, "not vassal or vampire") + return FALSE + + if(target_vampire && (target_vampire.prince || target_vampire.scourge)) + owner.balloon_alert(owner, "cannot promote elders!") + return FALSE + + if(target_ref || promoting) // Already offering + owner.balloon_alert(owner, "already offering!") + return FALSE + + return TRUE + +/datum/action/cooldown/vampire/targeted/scourgify/fire_targeted_power(atom/target_atom) + . = ..() + var/mob/living/living_target = target_atom + + var/datum/antagonist/vassal/vassal = IS_VASSAL(living_target) + + promoting = TRUE + + if(vassal) // We don't need to ask a lowly vassal. + // Pull them into our clan + var/datum/vampire_clan/masterclan_type = vampiredatum_power.my_clan?.type + + if(!masterclan_type) // How did a caitiff get prince, bro. Fine. + owner.balloon_alert(owner, "select clan first!") + deactivate_power() + return + + vassal.silent = TRUE + living_target.mind.remove_antag_datum(/datum/antagonist/vassal) + + // Make, then give the datum + var/datum/antagonist/vampire/scourgedatum = new(living_target.mind) + scourgedatum.can_assign_self_objectives = FALSE + scourgedatum.should_forge_objectives = FALSE // their one objective is to enforce their prince's authority + scourgedatum.stinger_sound = null // to avoid several sounds stacking on top of each other + living_target.mind.add_antag_datum(scourgedatum) + + scourgedatum.my_clan = new masterclan_type(scourgedatum) + scourgedatum.my_clan.on_apply() + + // Scourgify and end power + scourgedatum.scourgify() + target_ref = null + power_activated_sucessfully() + return + else + target_ref = WEAKREF(IS_VAMPIRE(living_target)) + + owner.balloon_alert(owner, "you offer [living_target] the rank of Scourge...") + living_target.playsound_local(null, 'modular_oculis/modules/vampires/sound/scourge_offer.ogg', 100, FALSE, pressure_affected = FALSE) + + ASYNC + var/choice = tgui_alert(living_target, + message = "Your Prince has selected you as [owner.p_their()] enforcer. Should you accept, you will receive the rank of 'Scourge', be bound to [owner.p_their()] authority, and increase in power considerably.", + title = "Scourge Offer", + buttons = list("Accept", "Refuse"), + timeout = cooldown_time - 5 SECONDS, + autofocus = TRUE + ) + handle_choice(choice) + + addtimer(CALLBACK(src, PROC_REF(choice_timeout)), cooldown_time) + deactivate_power() + +/datum/action/cooldown/vampire/targeted/scourgify/proc/accepted() + var/datum/antagonist/vampire/target_datum = target_ref.resolve() + target_datum.scourgify() + target_ref = null + power_activated_sucessfully() + +/datum/action/cooldown/vampire/targeted/scourgify/proc/refused() + owner.balloon_alert(owner, "offer refused") + target_ref = null + promoting = FALSE + +/datum/action/cooldown/vampire/targeted/scourgify/proc/choice_timeout() + if(owner && target_ref) // This might happen AFTER we remove the power from our owner. + owner.balloon_alert(owner, "offer ignored") + target_ref = null + promoting = FALSE + +/datum/action/cooldown/vampire/targeted/scourgify/proc/handle_choice(choice) + switch(choice) + if("Accept") + accepted() + return + if("Refuse") + refused() + return diff --git a/modular_oculis/modules/vampires/code/powers/obfuscate/cloak.dm b/modular_oculis/modules/vampires/code/powers/obfuscate/cloak.dm new file mode 100644 index 000000000000..58d3370656f5 --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/obfuscate/cloak.dm @@ -0,0 +1,70 @@ +/datum/action/cooldown/vampire/cloak + name = "Cloak of Darkness" + desc = "Blend into the shadows and become invisible to the artificial eye." + button_icon_state = "power_cloak" + power_explanation = "Activate this Power while unseen and you will turn nearly invisible, scaling with your rank.\n\ + Additionally, while Cloak is active, you are completely invisible to silicons." + vampire_power_flags = BP_AM_TOGGLE + vampire_check_flags = BP_CANT_USE_IN_TORPOR | BP_CANT_USE_WHILE_STAKED | BP_CANT_USE_IN_FRENZY | BP_CANT_USE_WHILE_UNCONSCIOUS + vitaecost = 20 + constant_vitaecost = 0.75 + cooldown_time = 5 SECONDS + var/cloaklevel = 20 + +/datum/action/cooldown/vampire/cloak/two + vitaecost = 20 + constant_vitaecost = 1 + cloaklevel = 15 + +/datum/action/cooldown/vampire/cloak/three + vitaecost = 20 + constant_vitaecost = 1.25 + cloaklevel = 10 + +/datum/action/cooldown/vampire/cloak/four + vitaecost = 20 + constant_vitaecost = 1.5 + cloaklevel = 5 + +/// Must have nobody around to see the cloak +/datum/action/cooldown/vampire/cloak/can_use() + . = ..() + if(!.) + return FALSE + + return TRUE + +/datum/action/cooldown/vampire/cloak/activate_power() + . = ..() + check_witnesses() + var/mob/living/user = owner + user.add_traits(list(TRAIT_UNKNOWN_APPEARANCE, TRAIT_UNKNOWN_VOICE), REF(src)) + user.add_movespeed_modifier(/datum/movespeed_modifier/cloak) + user.AddElement(/datum/element/digitalcamo) + user.balloon_alert(user, "cloak turned on.") + animate(user, alpha = cloaklevel, time = 1 SECONDS) + apply_wibbly_filters(user) + +/datum/action/cooldown/vampire/cloak/continue_active() + . = ..() + if(!.) + return FALSE + + if(owner.stat != CONSCIOUS) + to_chat(owner, span_warning("Your cloak failed because you fell unconcious!")) + return FALSE + return TRUE + +/datum/action/cooldown/vampire/cloak/deactivate_power() + var/mob/living/user = owner + + remove_wibbly_filters(user, 1 SECONDS) + animate(user, alpha = 255, time = 1 SECONDS) + user.remove_traits(list(TRAIT_UNKNOWN_APPEARANCE, TRAIT_UNKNOWN_VOICE), REF(src)) + user.RemoveElement(/datum/element/digitalcamo) + user.remove_movespeed_modifier(/datum/movespeed_modifier/cloak) + user.balloon_alert(user, "cloak turned off.") + return ..() + +/datum/movespeed_modifier/cloak + multiplicative_slowdown = 1.5 diff --git a/modular_oculis/modules/vampires/code/powers/obfuscate/obfuscate.dm b/modular_oculis/modules/vampires/code/powers/obfuscate/obfuscate.dm new file mode 100644 index 000000000000..6e2d231d3049 --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/obfuscate/obfuscate.dm @@ -0,0 +1,11 @@ +/datum/discipline/obfuscate + name = "Obfuscate" + discipline_explanation = "Obfuscate is a Discipline that allows vampires to conceal themselves, deceive the mind of others, or make them ignore what the user does not want to be seen." + icon_state = "obfuscate" + + // Lists of abilities granted per level + level_1 = list(/datum/action/cooldown/vampire/cloak) + level_2 = list(/datum/action/cooldown/vampire/cloak/two, /datum/action/cooldown/vampire/targeted/trespass) + level_3 = list(/datum/action/cooldown/vampire/cloak/three, /datum/action/cooldown/vampire/targeted/trespass/two) + level_4 = list(/datum/action/cooldown/vampire/cloak/four, /datum/action/cooldown/vampire/targeted/trespass/three, /datum/action/cooldown/vampire/veil) + level_5 = null diff --git a/modular_oculis/modules/vampires/code/powers/obfuscate/trespass.dm b/modular_oculis/modules/vampires/code/powers/obfuscate/trespass.dm new file mode 100644 index 000000000000..b0636f18f0a8 --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/obfuscate/trespass.dm @@ -0,0 +1,107 @@ +/datum/action/cooldown/vampire/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 = "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." + vampire_power_flags = BP_AM_TOGGLE + vampire_check_flags = BP_CANT_USE_IN_TORPOR | BP_CANT_USE_WHILE_STAKED | BP_CANT_USE_WHILE_INCAPACITATED | BP_CANT_USE_WHILE_UNCONSCIOUS + vitaecost = 30 + cooldown_time = 8 SECONDS + prefire_message = "Select a destination." + ranged_mousepointer = 'modular_oculis/modules/vampires/icons/mouse_pointers/vampire_trespass.dmi' + //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. + level_current = 1 + +/datum/action/cooldown/vampire/targeted/trespass/two + level_current = 2 + vitaecost = 50 + +/datum/action/cooldown/vampire/targeted/trespass/three + level_current = 3 + vitaecost = 70 + +/datum/action/cooldown/vampire/targeted/trespass/can_use() + . = ..() + if(!.) + return FALSE + + if(HAS_TRAIT(owner, TRAIT_NO_TRANSFORM)) + return FALSE + if(!get_turf(owner)) + return FALSE + +/datum/action/cooldown/vampire/targeted/trespass/check_valid_target(atom/target_atom) + . = ..() + if(!.) + return FALSE + + // Can't trespass to the same tile we're already on + if(target_atom.loc == owner.loc) + return FALSE + // Check if path is obstructed + var/turf/starting_turf = get_turf(owner) + var/turf/ending_turf = isturf(target_atom) ? target_atom : get_turf(target_atom) + var/this_dir + for(var/i = 1 to 2) + // Keep Prev Direction if we've reached final turf + if(starting_turf != ending_turf) + this_dir = get_dir(starting_turf, ending_turf) + starting_turf = get_step(starting_turf, this_dir) + // Walls block trespass + if(iswallturf(starting_turf)) + var/wallwarning = (i == 1) ? "in the way" : "at your destination" + owner.balloon_alert(owner, "there is a wall [wallwarning].") + return FALSE + + target_turf = starting_turf + +/datum/action/cooldown/vampire/targeted/trespass/fire_targeted_power(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(40, 100 - level_current * 20) + playsound(get_turf(owner), 'sound/effects/magic/summon_karp.ogg', vol = sound_strength, vary = TRUE) + /* var/datum/effect_system/steam_spread/vampire/puff = new /datum/effect_system/steam_spread() + puff.set_up(3, FALSE, 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) + ADD_TRAIT(user, TRAIT_UNDENSE, REF(src)) + user.SetInvisibility(INVISIBILITY_MAXIMUM, "vampire_trespass") + + // 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.setDir(get_dir(my_turf, target_turf)) + user.Stun(mist_delay / 2, ignore_canstun = TRUE) + REMOVE_TRAIT(user, TRAIT_UNDENSE, REF(src)) + user.RemoveInvisibility("vampire_trespass") + + check_witnesses() + // Effect Destination + playsound(get_turf(owner), 'sound/effects/magic/summon_karp.ogg', vol = 60, vary = TRUE) + /* puff = new /datum/effect_system/steam_spread() + puff.set_up(3, FALSE, target_turf) + puff.start() */ diff --git a/modular_oculis/modules/vampires/code/powers/obfuscate/veil.dm b/modular_oculis/modules/vampires/code/powers/obfuscate/veil.dm new file mode 100644 index 000000000000..386071e78d4a --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/obfuscate/veil.dm @@ -0,0 +1,76 @@ +/datum/action/cooldown/vampire/veil + name = "Veil of Many Faces" + desc = "Disguise yourself in the illusion of another identity." + button_icon_state = "power_veil" + power_explanation = "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, deactivating the ability will restore you to your former self." + vampire_power_flags = BP_AM_TOGGLE + vampire_check_flags = BP_CANT_USE_IN_FRENZY | BP_CANT_USE_WHILE_STAKED | BP_ALLOW_WHILE_SILVER_CUFFED + vitaecost = 25 + constant_vitaecost = 1.5 + cooldown_time = 10 SECONDS + + var/datum/dna/original_dna + var/prev_disfigured + var/original_name + var/alist/original_clothing_prefs + +/datum/action/cooldown/vampire/veil/activate_power() + . = ..() + cast_effect() // POOF + veil_user() + owner.balloon_alert(owner, "veil turned on.") + +/datum/action/cooldown/vampire/veil/proc/veil_user() + if(!ishuman(owner)) + return + var/mob/living/carbon/human/user = owner + to_chat(owner, span_warning("You mystify the air around your person. Your identity is now altered.")) + original_dna = new user.dna.type + original_name = user.real_name + // original_clothing_prefs = user.backup_clothing_prefs() + user.dna.copy_dna(original_dna) + randomize_human(user) + prev_disfigured = HAS_TRAIT(user, TRAIT_DISFIGURED) // I was disfigured! //prev_disabilities = user.disabilities + if(prev_disfigured) + REMOVE_TRAIT(user, TRAIT_DISFIGURED, null) + + to_chat(owner, span_warning("You mystify the air around your person. Your identity is now altered.")) + +/datum/action/cooldown/vampire/veil/deactivate_power() + . = ..() + if(!ishuman(owner)) + return + var/mob/living/carbon/human/user = owner + to_chat(user, span_notice("You return to your old form.")) + original_dna.copy_dna(user.dna, COPY_DNA_SE|COPY_DNA_SPECIES|COPY_DNA_MUTATIONS) + user.real_name = original_name + // user.restore_clothing_prefs(original_clothing_prefs) + user.updateappearance(mutcolor_update = TRUE) + //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) + + original_dna = null + + cast_effect() // POOF + owner.balloon_alert(owner, "veil turned off.") + +// CAST EFFECT // General effect (poof, splat, etc) when you cast. Doesn't happen automatically! +/datum/action/cooldown/vampire/veil/proc/cast_effect() + // Effect + playsound(get_turf(owner), 'sound/effects/magic/smoke.ogg', 20, 1) + /* var/datum/effect_system/steam_spread/vampire/puff = new /datum/effect_system/steam_spread/() + puff.set_up(3, 0, get_turf(owner)) + puff.attach(owner) //OPTIONAL + puff.start() */ + owner.spin(0.8 SECONDS, 1) //Spin around like a loon. + check_witnesses() + +/obj/effect/particle_effect/fluid/smoke/vampsmoke + opacity = FALSE + lifetime = 0 + +/obj/effect/particle_effect/fluid/smoke/vampsmoke/fade_out(frames = 0.8 SECONDS) + ..(frames) diff --git a/modular_oculis/modules/vampires/code/powers/potence/brash.dm b/modular_oculis/modules/vampires/code/powers/potence/brash.dm new file mode 100644 index 000000000000..df7fd940ba39 --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/potence/brash.dm @@ -0,0 +1,172 @@ +/** + * A Brujah exclusive ability that acts as an enhanced version of "Brawn" + * 'vitaecost' and 'cooldown_time' vary depending on what the power is used for. + * Lots of code has been copied over from Brawn wherever inheritance might prove insufficient. + * Comments from copied code have been removed (they can still be found in their original location.) +**/ + +/datum/action/cooldown/vampire/targeted/brawn/brash + name = "Brash" + desc = "Break most structures apart with overwhelming force. Cooldown and cost vary depending on the object broken." + button_icon_state = "power_strength_brujah" + power_explanation = "This is an enhanced version of the regular 'Brawn' ability.\n\ + Use on a person to send them flying. Use while restrained, grabbed, or trapped in a locker to break free.\n\ + Punching a cyborg will temporarily disable it in addition to usual damage. \n\ + At level 2 this ability will allow you to break through unbolted airlocks. \n\ + At level 3 this ability will allow you to break through bolted airlocks. \n\ + At level 4 this ability will allow you to break through normal walls and windows, and can break silver handcuffs. \n\ + At level 5 this ability will allow you to break through reinforced walls and windows. \n\ + Higher levels will increase this ability's damage and knockdown." + vampire_power_flags = BP_AM_VERY_DYNAMIC_COOLDOWN + vitaecost = 0 // Set on use + cooldown_time = 1 SECONDS // Same as above + damage_coefficient = 1.625 + brujah = TRUE + level_current = 1 + +/datum/action/cooldown/vampire/targeted/brawn/brash/two + level_current = 2 + +/datum/action/cooldown/vampire/targeted/brawn/brash/three + level_current = 3 + +/datum/action/cooldown/vampire/targeted/brawn/brash/four + level_current = 4 + vampire_check_flags = parent_type::vampire_check_flags | BP_ALLOW_WHILE_SILVER_CUFFED + +/datum/action/cooldown/vampire/targeted/brawn/brash/five + level_current = 5 + vampire_check_flags = parent_type::vampire_check_flags | BP_ALLOW_WHILE_SILVER_CUFFED + +/// Hit an atom, set vitaecost, set cooldown time, play a sound, and deconstruct the atom +/// with this one convenient proc! +/datum/action/cooldown/vampire/targeted/brawn/brash/proc/hit_with_style(atom/target_atom, sound, vol as num, cost as num, cooldown) + if(!isobj(target_atom)) + return + + var/obj/target_obj = target_atom + owner.do_attack_animation(target_obj) + vitaecost = cost + cooldown_time = cooldown + playsound(target_atom, sound, 75, TRUE) + target_obj.deconstruct(FALSE) + +/datum/action/cooldown/vampire/targeted/brawn/brash/fire_targeted_power(atom/target_atom) + . = ..() + // People + if(isliving(target_atom)) + vitaecost = 50 + cooldown_time = 10 SECONDS + return + + // Closets + if(istype(target_atom, /obj/structure/closet)) + vitaecost = 50 + cooldown_time = 7 SECONDS + return + + // Girders + if(istype(target_atom, /obj/structure/girder)) + hit_with_style(target_atom, 'sound/effects/bang.ogg', 60, 300, 5 SECONDS) + return + + // Grilles + if(istype(target_atom, /obj/structure/grille)) + hit_with_style(target_atom, 'sound/effects/grillehit.ogg', 50, 10, 0.5 SECONDS) + return + + // Windows + if(istype(target_atom, /obj/structure/window)) + var/obj/structure/window/window = target_atom + if(istype(target_atom, /obj/structure/window/reinforced) && level_current < 5) + window.balloon_alert(owner, "level 5 required!") + return + else if(level_current < 4) + window.balloon_alert(owner, "level 4 required!") + return + + if(istype(window, /obj/structure/window/reinforced) || istype(window, /obj/structure/window/plasma)) + hit_with_style(window, 'sound/effects/bang.ogg', 30, 50, 15 SECONDS) + else + hit_with_style(window, 'sound/effects/bang.ogg', 20, 75, 10 SECONDS) + return + + // Windoors + if(istype(target_atom, /obj/machinery/door/window)) + hit_with_style(target_atom, 'sound/effects/bang.ogg', 50, 35, 5 SECONDS) + return + + // Tables + if(istype(target_atom, /obj/structure/table)) + hit_with_style(target_atom, 'sound/effects/bang.ogg', 35, 25, 5 SECONDS) + return + + // Walls + if(iswallturf(target_atom)) + if(isindestructiblewall(target_atom)) + target_atom.balloon_alert(owner, "this wall is indestructible!") + return + + if(istype(target_atom, /turf/closed/wall/r_wall) && level_current < 5) + target_atom.balloon_alert(owner, "level 5 required!") + return + else if(level_current < 4) + target_atom.balloon_alert(owner, "level 4 required!") + return + + rip_and_tear(owner, target_atom) + +/// Copied over from '/datum/element/wall_tearer/proc/rip_and_tear' with appropriate adjustment. +/datum/action/cooldown/vampire/targeted/brawn/brash/proc/rip_and_tear(mob/living/tearer, atom/target) + var/tear_time = 0.75 SECONDS + var/reinforced_multiplier = 5 + var/rip_time = (istype(target, /turf/closed/wall/r_wall) ? tear_time * reinforced_multiplier : tear_time) + + if(istype(target, /turf/closed/wall/r_wall)) + vitaecost = 100 + cooldown_time = 20 SECONDS + else + vitaecost = 85 + cooldown_time = 15 SECONDS + + while(iswallturf(target)) + var/turf/closed/wall/wall = target + + tearer.visible_message(span_warning("[tearer] viciously rips into [wall]!")) + playsound(tearer, 'sound/machines/airlock/airlock_alien_prying.ogg', vol = 50, vary = TRUE, frequency = 2) + wall.balloon_alert(tearer, "tearing...") + + if(do_after(tearer, delay = rip_time, target = wall, interaction_key = "vampire interaction")) + playsound(tearer, 'sound/effects/meteorimpact.ogg', 100, TRUE) + tearer.do_attack_animation(wall) + wall.dismantle_wall(1) + return + else + tearer.balloon_alert(tearer, "interrupted!") + +/// TODO: check if switch statements work with istype() +/datum/action/cooldown/vampire/targeted/brawn/brash/check_valid_target(atom/target_atom) + . = ..() + if(!.) + return FALSE + + if(target_atom.resistance_flags & INDESTRUCTIBLE) + return FALSE + if(isliving(target_atom)) + return TRUE + if(istype(target_atom, /obj/machinery/door/airlock)) + return TRUE + if(istype(target_atom, /obj/structure/table)) + return TRUE + if(istype(target_atom, /obj/structure/closet)) + return TRUE + if(istype(target_atom, /obj/structure/girder)) + return TRUE + if(istype(target_atom, /obj/structure/grille)) + return TRUE + if(istype(target_atom, /obj/structure/window)) + return TRUE + if(iswallturf(target_atom)) + return TRUE + + return FALSE diff --git a/modular_oculis/modules/vampires/code/powers/potence/brawn.dm b/modular_oculis/modules/vampires/code/powers/potence/brawn.dm new file mode 100644 index 000000000000..ebab810c0563 --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/potence/brawn.dm @@ -0,0 +1,236 @@ +/datum/action/cooldown/vampire/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 = "Use this power to deal a horrific blow. Punching a Cyborg will EMP it and deal high damage.\n\ + At level 3, you can break closets open and break restraints.\n\ + At level 4, you can bash airlocks open, and you get the ability to break even silver handcuffs. Use wisely - security is unlikely to try and capture you alive again after the first time!\n\ + Higher ranks will increase the damage when punching someone." + vampire_power_flags = BP_AM_TOGGLE + vampire_check_flags = BP_CANT_USE_IN_TORPOR | BP_CANT_USE_WHILE_STAKED | BP_CANT_USE_IN_FRENZY | BP_CANT_USE_WHILE_INCAPACITATED | BP_CANT_USE_WHILE_UNCONSCIOUS + vitaecost = 50 + cooldown_time = 9 SECONDS + target_range = 1 + power_activates_immediately = TRUE + prefire_message = "Select a target." + level_current = 1 + ranged_mousepointer = 'modular_oculis/modules/vampires/icons/mouse_pointers/vampire_strength.dmi' + + /// Only changed by the '/brawn/brash' subtype; acts as a general purpose damage multipler. + var/damage_coefficient = 1.25 + /// Boolean indicating whether or not this version of '/brawn' is in the '/brash' subtype and should + /// bypass typical ability level restrictions. (There is probably a better way to do this.) + var/brujah = FALSE + +/datum/action/cooldown/vampire/targeted/brawn/two + level_current = 2 + +/datum/action/cooldown/vampire/targeted/brawn/three + level_current = 3 + +/datum/action/cooldown/vampire/targeted/brawn/four + level_current = 4 + vampire_check_flags = parent_type::vampire_check_flags | BP_ALLOW_WHILE_SILVER_CUFFED + +/datum/action/cooldown/vampire/targeted/brawn/activate_power() + // Did we break out of our handcuffs? + if(break_restraints()) + power_activated_sucessfully() + return + // Did we knock a grabber down? We can only do this while not also breaking restraints if strong enough. + if(level_current >= 3 && escape_puller()) + power_activated_sucessfully() + return + // Did neither, now we can PUNCH. + . = ..() + +// Look at 'biodegrade.dm' for reference +/datum/action/cooldown/vampire/targeted/brawn/proc/break_restraints() + if(!ishuman(owner)) + return FALSE + + var/mob/living/carbon/human/human_owner = owner + + var/used = FALSE + + // Lockers + if(istype(human_owner.loc, /obj/structure/closet)) + var/obj/structure/closet/closet = human_owner.loc + addtimer(CALLBACK(closet, TYPE_PROC_REF(/obj/structure/closet, bust_open), FALSE), 0.1 SECONDS) + closet.visible_message( + span_warning("[closet] tears apart as [human_owner] bashes it open from within!"), + span_warning("[closet] tears apart as you bash it open from within!") + ) + to_chat(human_owner, span_warning("We bash [closet] wide open!")) + used = TRUE + + // Cuffs + if(human_owner.handcuffed || human_owner.legcuffed) + human_owner.uncuff() + human_owner.visible_message( + span_warning("[human_owner] discards [human_owner.p_their()] restraints like it's nothing!"), + span_warning("We break through our restraints!") + ) + used = TRUE + + // Straightjackets + if(human_owner.wear_suit?.breakouttime) + var/obj/item/clothing/suit/straightjacket = human_owner.get_item_by_slot(ITEM_SLOT_OCLOTHING) + if(straightjacket && human_owner.wear_suit == straightjacket) + qdel(straightjacket) + human_owner.visible_message( + span_warning("[human_owner] rips straight through the [human_owner.p_their()] [straightjacket]!"), + span_warning("We tear through our [straightjacket]!") + ) + used = TRUE + + if(used) + playsound(get_turf(human_owner), 'sound/effects/grillehit.ogg', 80, TRUE, -1) + + /* if(used) + check_witnesses() */ + return used + +/datum/action/cooldown/vampire/targeted/brawn/proc/escape_puller() + if(!owner.pulledby) + 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, TRUE, -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. + + log_combat(owner, pulled_mob, "used [src.name] 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?.stop_pulling() // It's already done, but JUST IN CASE. + + // check_witnesses() + return TRUE + +/datum/action/cooldown/vampire/targeted/brawn/fire_targeted_power(atom/target_atom) + . = ..() + var/mob/living/carbon/carbon_owner = owner + + // Living Targets + if(isliving(target_atom)) + var/mob/living/living_target = target_atom + + // Strength of the attack + var/obj/item/bodypart/user_active_arm = carbon_owner.get_active_hand() + var/hit_strength = user_active_arm.unarmed_damage_high * damage_coefficient + 2 + + var/powerlevel = min(5, 1 + level_current) + + if(rand(5 + powerlevel) >= 5) + living_target.visible_message( + span_danger("[carbon_owner] lands a vicious punch, sending [living_target] away!"), \ + span_userdanger("[carbon_owner] has landed a horrifying punch on you, sending you flying!"), + ) + living_target.Knockdown(min(5, rand(10, 10 * powerlevel))) + + // Attack! + owner.balloon_alert(owner, "you punch [living_target]!") + playsound(get_turf(living_target), 'sound/items/weapons/punch4.ogg', 60, TRUE, -1) + // check_witnesses(living_target) + carbon_owner.do_attack_animation(living_target, ATTACK_EFFECT_SMASH) + + var/obj/item/bodypart/affecting = living_target.get_bodypart(ran_zone(living_target.zone_selected)) + living_target.apply_damage(hit_strength, BRUTE, affecting) + + // Knockback + var/send_dir = get_dir(owner, living_target) + var/turf/turf_thrown_at = get_ranged_target_turf(living_target, send_dir, powerlevel) + owner.newtonian_move(send_dir) // Bounce back in 0 G + living_target.throw_at(turf_thrown_at, powerlevel, TRUE, owner) + + // Target Type: Cyborg (Also gets the effects above) + if(issilicon(living_target)) + living_target.emp_act(EMP_HEAVY) + // Lockers + else if(istype(target_atom, /obj/structure/closet)) + var/obj/structure/closet/target_closet = target_atom + + playsound(get_turf(carbon_owner), 'sound/machines/airlock/airlock_alien_prying.ogg', 40, TRUE, -1) + carbon_owner.balloon_alert(carbon_owner, "you prepare to bash [target_closet] open...") + if(!do_after(carbon_owner, 2.5 SECONDS, target_closet)) + carbon_owner.balloon_alert(carbon_owner, "interrupted!") + return FALSE + target_closet.visible_message(span_danger("[target_closet] breaks open as [carbon_owner] bashes it!")) + + INVOKE_ASYNC(target_closet, TYPE_PROC_REF(/obj/structure/closet, bust_open), FALSE) + playsound(get_turf(carbon_owner), 'sound/effects/grillehit.ogg', 80, TRUE, -1) + check_witnesses() + // Airlocks + else if(istype(target_atom, /obj/machinery/door/airlock)) + var/obj/machinery/door/airlock/target_airlock = target_atom + + playsound(get_turf(carbon_owner), 'sound/machines/airlock/airlock_alien_prying.ogg', 40, TRUE, -1) + check_witnesses() + owner.balloon_alert(owner, "you prepare to tear open [target_airlock]...") + if(!do_after(carbon_owner, 2.5 SECONDS, target_airlock)) + carbon_owner.balloon_alert(carbon_owner, "interrupted!") + return FALSE + + if(target_airlock.Adjacent(carbon_owner)) + target_airlock.visible_message(span_danger("[target_airlock] breaks open as [carbon_owner] bashes it!")) + + // Adjust cost and cooldown if Brujah + if(brujah) + if(target_airlock.locked) + vitaecost = 100 + cooldown_time = 10 SECONDS + else + vitaecost = 75 + cooldown_time = 6 SECONDS + else // If not Brujah then just make the vampire wait a second... + carbon_owner.Stun(1 SECONDS) + + carbon_owner.Stun(1 SECONDS) + carbon_owner.do_attack_animation(target_airlock, ATTACK_EFFECT_SMASH) + playsound(get_turf(target_airlock), 'sound/effects/bang.ogg', 30, TRUE, -1) + if(brujah && level_current >= 3 && target_airlock.locked) + target_airlock.unbolt() + target_airlock.open(BYPASS_DOOR_CHECKS) + +/datum/action/cooldown/vampire/targeted/brawn/check_valid_target(atom/target_atom) + . = ..() + if(!.) + return FALSE + + // Brujah has their own checks + if(brujah) + return TRUE + + if(isliving(target_atom)) + return TRUE + + if(istype(target_atom, /obj/machinery/door/airlock)) + if(level_current < 4) + owner.balloon_alert(owner, "level 4 required!") + return FALSE + + return TRUE + + if(istype(target_atom, /obj/structure/closet)) + if(level_current < 3) + owner.balloon_alert(owner, "level 3 required!") + return FALSE + + var/obj/structure/closet/target_closet = target_atom + if(target_closet.welded || target_closet.locked) + return TRUE + + return FALSE diff --git a/modular_oculis/modules/vampires/code/powers/potence/lunge.dm b/modular_oculis/modules/vampires/code/powers/potence/lunge.dm new file mode 100644 index 000000000000..e35eeb513a8a --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/potence/lunge.dm @@ -0,0 +1,178 @@ +#define LUNGE_TIME 3 SECONDS + +/datum/action/cooldown/vampire/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 = "Click any player to start spinning wildly and, after a short delay, lunge at them.\n\ + You cannot lunge if you are already grabbing someone, or are being grabbed.\n\ + If you lunge from behind or darkness, you will aggro-grab and knock the target down, scaling with your rank.\n\ + If used on a dead body, you will tear their organs out.\n\ + At level 4, you will instantly lunge, but are limited to tackling from only 6 tiles away." + vampire_power_flags = NONE + vampire_check_flags = BP_CANT_USE_IN_TORPOR | BP_CANT_USE_WHILE_STAKED | BP_CANT_USE_IN_FRENZY | BP_CANT_USE_WHILE_INCAPACITATED | BP_CANT_USE_WHILE_UNCONSCIOUS + vitaecost = 45 + cooldown_time = 10 SECONDS + power_activates_immediately = FALSE + ranged_mousepointer = 'modular_oculis/modules/vampires/icons/mouse_pointers/vampire_lunge.dmi' + + var/instant = FALSE + var/knockdown_bonus = 1 + +/datum/action/cooldown/vampire/targeted/lunge/two + vitaecost = 60 + cooldown_time = 10 SECONDS + knockdown_bonus = 2 + +/datum/action/cooldown/vampire/targeted/lunge/three + vitaecost = 75 + cooldown_time = 8 SECONDS + knockdown_bonus = 3 + +/datum/action/cooldown/vampire/targeted/lunge/four + vitaecost = 90 + cooldown_time = 6 SECONDS + knockdown_bonus = 4 + instant = TRUE + +/datum/action/cooldown/vampire/targeted/lunge/can_use() + . = ..() + if(!.) + return FALSE + + if(owner.pulledby && owner.pulledby.grab_state >= GRAB_AGGRESSIVE) + owner.balloon_alert(owner, "grabbed!") + return FALSE + if(owner.pulling) + owner.balloon_alert(owner, "grabbing someone!") + return FALSE + if(datum_flags & DF_ISPROCESSING) + owner.balloon_alert(owner, "already lunging!") + return FALSE + return TRUE + +/// Check: Are we lunging at a person? +/datum/action/cooldown/vampire/targeted/lunge/check_valid_target(atom/target_atom) + . = ..() + if(!.) + return FALSE + + // Has to be alive + if(!isliving(target_atom)) + return FALSE + // Has to be on a turf + if(!isturf(target_atom.loc)) + return FALSE + // Has to be mobile + var/mob/living/user = owner + if(user.body_position == LYING_DOWN || HAS_TRAIT(owner, TRAIT_IMMOBILIZED)) + return FALSE + + if(get_dist(owner, target_atom) > 7) + owner.balloon_alert(owner, "too far away!") + return FALSE + +/datum/action/cooldown/vampire/targeted/lunge/fire_targeted_power(atom/target_atom) + . = ..() + owner.face_atom(target_atom) + if(instant) + do_lunge(target_atom) + return TRUE + + prepare_target_lunge(target_atom) + return TRUE + +///Starts processing the power and prepares the lunge by spinning, calls lunge at the end of it. +/datum/action/cooldown/vampire/targeted/lunge/proc/prepare_target_lunge(atom/target_atom) + START_PROCESSING(SSprocessing, src) + owner.balloon_alert(owner, "lunge started!") + + // Spin + owner.spin(8, 1) + owner.balloon_alert_to_viewers("spins wildly!", "you spin!") + // Smoke + do_smoke(0, owner.loc, smoke_type = /obj/effect/particle_effect/fluid/smoke/transparent) + //animate them shake + var/base_x = owner.base_pixel_x + var/base_y = owner.base_pixel_y + animate(owner, pixel_x = base_x, pixel_y = base_y, time = 0.1 SECONDS, loop = -1) + for(var/i in 1 to 25) + var/x_offset = base_x + rand(-3, 3) + var/y_offset = base_y + rand(-3, 3) + animate(pixel_x = x_offset, pixel_y = y_offset, time = 0.1 SECONDS) + + // Actually lunge now + if(!do_after(owner, LUNGE_TIME, timed_action_flags = (IGNORE_USER_LOC_CHANGE|IGNORE_TARGET_LOC_CHANGE), extra_checks = CALLBACK(src, PROC_REF(check_valid_target), target_atom))) + end_target_lunge(base_x, base_y) + return FALSE + + end_target_lunge() + do_lunge(target_atom) + return TRUE + +///When preparing to lunge ends, this clears it up. +/datum/action/cooldown/vampire/targeted/lunge/proc/end_target_lunge(base_x, base_y) + animate(owner, pixel_x = base_x, pixel_y = base_y, time = 0.1 SECONDS) + STOP_PROCESSING(SSprocessing, src) + +/datum/action/cooldown/vampire/targeted/lunge/process() + if(!power_in_use) //If running SSfasprocess (on cooldown) + return ..() //Manage our cooldown timers + +///Actually lunges the target, then calls lunge end. +/datum/action/cooldown/vampire/targeted/lunge/proc/do_lunge(atom/hit_atom) + var/turf/targeted_turf = get_turf(hit_atom) + + var/safety = get_dist(owner, targeted_turf) * 3 + 1 + var/consequetive_failures = 0 + while(--safety && !hit_atom.Adjacent(owner)) + if(!step_to(owner, targeted_turf)) + consequetive_failures++ + if(consequetive_failures >= 3) // If 3 steps don't work, just stop. + break + + lunge_end(hit_atom, targeted_turf) + +/datum/action/cooldown/vampire/targeted/lunge/proc/lunge_end(atom/hit_atom, turf/target_turf) + power_activated_sucessfully() + // Am I next to my target to start giving the effects? + if(!owner.Adjacent(hit_atom)) + // check_witnesses() + return + + var/mob/living/user = owner + var/mob/living/carbon/target = hit_atom + + // check_witnesses(target) + // Did I slip or get knocked unconscious? + if(user.body_position != STANDING_UP || user.incapacitated) + /* var/send_dir = get_dir(user, target_turf) + new /datum/forced_movement(user, get_ranged_target_turf(user, send_dir, 1), 1, FALSE) */ + user.spin(1 SECONDS) + return + + if(IS_CURATOR(target) || HAS_TRAIT(target, TRAIT_BRAWLING_KNOCKDOWN_BLOCKED)) + owner.balloon_alert(owner, "pushed away!") + target.grabbedby(owner) + return + + owner.balloon_alert(owner, "you lunge at [target]!") + if(target.stat == DEAD) + playsound(get_turf(target), 'sound/effects/splat.ogg', 40, TRUE) + owner.visible_message( + span_warning("[owner] tears into [target]'s chest!"), + span_warning("You tear into [target]'s chest!"), + ) + var/obj/item/bodypart/chest/chest = target.get_bodypart(BODY_ZONE_CHEST) + chest.dismember() + else + // Did we knock them down? + if(!is_source_facing_target(target, owner) || owner.alpha <= 40) + target.Knockdown((1 SECONDS) + knockdown_bonus * 5) + target.Paralyze(0.1) + + target.drop_all_held_items() + target.grabbedby(owner) + target.grippedby(owner, instant = TRUE) + +#undef LUNGE_TIME diff --git a/modular_oculis/modules/vampires/code/powers/potence/potence.dm b/modular_oculis/modules/vampires/code/powers/potence/potence.dm new file mode 100644 index 000000000000..9e5b4b073b45 --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/potence/potence.dm @@ -0,0 +1,25 @@ +/datum/discipline/potence + name = "Potence" + discipline_explanation = "Potence is the Discipline that endows vampires with physical vigor and preternatural strength.\n\ + Vampires with the Potence Discipline possess physical prowess beyond mortal bounds." + icon_state = "potence" + + // Lists of abilities granted per level + level_1 = list(/datum/action/cooldown/vampire/targeted/brawn, /datum/action/cooldown/vampire/targeted/lunge) + level_2 = list(/datum/action/cooldown/vampire/targeted/brawn/two, /datum/action/cooldown/vampire/targeted/lunge/two) + level_3 = list(/datum/action/cooldown/vampire/targeted/brawn/three, /datum/action/cooldown/vampire/targeted/lunge/three) + level_4 = list(/datum/action/cooldown/vampire/targeted/brawn/four, /datum/action/cooldown/vampire/targeted/lunge/four) + level_5 = null + +/datum/discipline/potence/brujah + level_1 = list(/datum/action/cooldown/vampire/targeted/brawn/brash, /datum/action/cooldown/vampire/targeted/lunge) + level_2 = list(/datum/action/cooldown/vampire/targeted/brawn/brash/two, /datum/action/cooldown/vampire/targeted/lunge/two) + level_3 = list(/datum/action/cooldown/vampire/targeted/brawn/brash/three, /datum/action/cooldown/vampire/targeted/lunge/three) + level_4 = list(/datum/action/cooldown/vampire/targeted/brawn/brash/four, /datum/action/cooldown/vampire/targeted/lunge/four) + level_5 = list(/datum/action/cooldown/vampire/targeted/brawn/brash/five, /datum/action/cooldown/vampire/targeted/lunge/four) + +/datum/discipline/potence/apply_discipline_quirks(datum/antagonist/vampire/clan_owner) + . = ..() + clan_owner.cleanup_limbs(clan_owner.owner.current) + clan_owner.extra_damage_per_rank = VAMPIRE_UNARMED_DMG_INCREASE_ON_RANKUP * 2 + clan_owner.setup_limbs(clan_owner.owner.current) diff --git a/modular_oculis/modules/vampires/code/powers/presence/awe.dm b/modular_oculis/modules/vampires/code/powers/presence/awe.dm new file mode 100644 index 000000000000..f788a2eab2d6 --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/presence/awe.dm @@ -0,0 +1,118 @@ +/datum/action/cooldown/vampire/awe + name = "Awe" + desc = "Project an aura of supernatural presence that subtly influences those around you." + button_icon_state = "power_awe" + power_explanation = "Project an aura around yourself that subtly affects everyone nearby.\n\ + Effects on those in your aura:\n\ + - They can only whisper, unable to speak loudly.\n\ + - They are slightly slowed.\n\ + - They occasionally lose focus: facing you, stepping towards you, or dropping items.\n\ + Targets must be able to see you to be affected." + vampire_power_flags = BP_AM_TOGGLE | BP_AM_STATIC_COOLDOWN + vampire_check_flags = BP_CANT_USE_IN_TORPOR | BP_CANT_USE_WHILE_STAKED | BP_CANT_USE_IN_FRENZY + vitaecost = 20 + constant_vitaecost = 1 + cooldown_time = 10 SECONDS + /// The range of the aura in tiles + var/aura = 5 + +/datum/action/cooldown/vampire/awe/activate_power() + . = ..() + to_chat(owner, span_notice("You extend your supernatural presence."), type = MESSAGE_TYPE_INFO) + +/datum/action/cooldown/vampire/awe/deactivate_power() + . = ..() + to_chat(owner, span_notice("You withdraw your supernatural presence."), type = MESSAGE_TYPE_INFO) + +/datum/action/cooldown/vampire/awe/use_power() + . = ..() + for(var/mob/living/victim in oviewers(aura, owner)) + if(can_affect(victim)) + victim.apply_status_effect(/datum/status_effect/awed, owner) + +/// Checks if this victim can be affected by the awe aura +/datum/action/cooldown/vampire/awe/proc/can_affect(mob/living/victim) + if(!victim.client) + return FALSE + if(HAS_SILICON_ACCESS(victim)) + return FALSE + if(victim.stat != CONSCIOUS) + return FALSE + if(victim.is_blind() || victim.is_nearsighted_currently()) + return FALSE + if(HAS_MIND_TRAIT(victim, TRAIT_VAMPIRE_ALIGNED) || IS_CURATOR(victim)) + return FALSE + return TRUE + +/datum/status_effect/awed + id = "awed" + status_type = STATUS_EFFECT_REFRESH + duration = 4 SECONDS + tick_interval = 1 SECONDS + processing_speed = STATUS_EFFECT_PRIORITY + alert_type = null + var/mob/living/source_vampire + COOLDOWN_DECLARE(awe_effect_cooldown) + +/datum/status_effect/awed/on_creation(mob/living/new_owner, mob/living/vampire) + source_vampire = vampire + return ..() + +/datum/status_effect/awed/Destroy() + source_vampire = null + return ..() + +/datum/status_effect/awed/on_apply() + if(!iscarbon(owner)) + return FALSE + ADD_TRAIT(owner, TRAIT_SOFTSPOKEN, TRAIT_STATUS_EFFECT(id)) + owner.add_movespeed_modifier(/datum/movespeed_modifier/status_effect/awed) + return TRUE + +/datum/status_effect/awed/on_remove() + REMOVE_TRAIT(owner, TRAIT_SOFTSPOKEN, TRAIT_STATUS_EFFECT(id)) + owner.remove_movespeed_modifier(/datum/movespeed_modifier/status_effect/awed) + +/datum/status_effect/awed/tick(seconds_between_ticks) + if(QDELETED(source_vampire) || source_vampire.stat == DEAD) + qdel(src) + return + + if(INCAPACITATED_IGNORING(owner, INCAPABLE_RESTRAINTS) || !COOLDOWN_FINISHED(src, awe_effect_cooldown)) + return + + COOLDOWN_START(src, awe_effect_cooldown, 5 SECONDS) + // Pick a random disruptive effect each tick + switch(rand(1, 5)) + // Nothingburger + if(1) + to_chat(owner, span_awe("Your mind drifts...")) + // Only face them, nothing else + if(2) + owner.face_atom(source_vampire) + // Smile + if(3) + owner.face_atom(source_vampire) + owner.emote("smiles") + to_chat(owner, span_awe("You find yourself smiling...")) + // Step Towards + if(4) + owner.face_atom(source_vampire) + if(owner.body_position == STANDING_UP && get_step(owner.loc, get_dir(owner.loc, source_vampire.loc)) != source_vampire.loc) + owner.balloon_alert(owner, "you stumble...") + owner.visible_message(span_warning("[owner] stumbles."), span_awe("You suddenly stumble...")) + owner.Move(get_step(owner.loc, get_dir(owner.loc, source_vampire.loc))) + // Wobbly Knees + if(5) + owner.face_atom(source_vampire) + if(owner.body_position == STANDING_UP && owner.get_stamina_loss() < 80) + owner.balloon_alert(owner, "your knees feel wobbly...") + owner.visible_message(span_warning("[owner] seems quite wobbly on [owner.p_their()] feet."), span_awe("Your knees feel wobbly...")) + owner.adjust_stamina_loss(rand(20, 40)) + +/datum/status_effect/awed/get_examine_text() + return span_warning("[owner.p_They()] seem[owner.p_s()] distracted and unfocused.") + +/// Movespeed modifier for the awed status effect +/datum/movespeed_modifier/status_effect/awed + multiplicative_slowdown = 0.6 diff --git a/modular_oculis/modules/vampires/code/powers/presence/entrance.dm b/modular_oculis/modules/vampires/code/powers/presence/entrance.dm new file mode 100644 index 000000000000..64d1ed56fc36 --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/presence/entrance.dm @@ -0,0 +1,115 @@ +/** + * ENTRANCE + * A stunning spell that slows, mutes, and impairs the target for a short duration. + * Like a softer mesmerize - target isn't fully out of commission but is very impaired. + */ +/datum/action/cooldown/vampire/targeted/entrance + name = "Entrance" + desc = "Capture a mortal's attention momentarily, leaving them slowed, muted, and dazed." + button_icon_state = "power_entrance" + power_explanation = "Click any player to entrance them, leaving them momentarily impaired.\n\ + Your target will be slowed, muted, and unable to use items for a short duration.\n\ + The effect only lasts half as long on mindshielded targets.\n\ + If the target is attacked by anyone, the effect will instantly wear off.\n\ + This is a softer form of control - they can still move and resist, but are heavily hindered." + vampire_power_flags = NONE + vampire_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 + vitaecost = 80 + cooldown_time = 60 SECONDS + target_range = 7 + prefire_message = "Who will you entrance?" + ranged_mousepointer = 'modular_oculis/modules/vampires/icons/mouse_pointers/vampire_entrance.dmi' + +/datum/action/cooldown/vampire/targeted/entrance/check_valid_target(atom/target_atom) + . = ..() + if(!.) + return FALSE + + if(!iscarbon(target_atom)) + return FALSE + var/mob/living/carbon/carbon_target = target_atom + + if(!carbon_target.mind) + owner.balloon_alert(owner, "[carbon_target] is mindless.") + return FALSE + + if(HAS_MIND_TRAIT(carbon_target, TRAIT_VAMPIRE_ALIGNED) || IS_CURATOR(carbon_target)) + owner.balloon_alert(owner, "immune to your presence.") + return FALSE + + if(carbon_target.stat != CONSCIOUS) + owner.balloon_alert(owner, "[carbon_target] is not [(carbon_target.stat == DEAD || HAS_TRAIT(carbon_target, TRAIT_FAKEDEATH)) ? "alive" : "conscious"].") + return FALSE + + if(carbon_target.has_status_effect(/datum/status_effect/entranced)) + owner.balloon_alert(owner, "[carbon_target] is already entranced.") + return FALSE + + if(carbon_target.get_eye_protection() >= FLASH_PROTECTION_WELDER) + owner.balloon_alert(owner, "[carbon_target] has too much eye protection.") + return FALSE + + return TRUE + +/datum/action/cooldown/vampire/targeted/entrance/fire_targeted_power(atom/target_atom) + . = ..() + var/mob/living/carbon/carbon_target = target_atom + + carbon_target.apply_status_effect(/datum/status_effect/entranced, 20 SECONDS) + + owner.balloon_alert(owner, "entranced [carbon_target]") + to_chat(carbon_target, span_awe("Your mind goes blank..."), type = MESSAGE_TYPE_WARNING) + to_chat(owner, span_notice("You capture [carbon_target]'s attention, leaving [carbon_target.p_them()] dazed."), type = MESSAGE_TYPE_INFO) + + carbon_target.playsound_local(null, 'modular_oculis/modules/vampires/sound/mesmerize.ogg', 50, FALSE, pressure_affected = FALSE) + +/// Status effect for being entranced +/datum/status_effect/entranced + id = "entranced" + status_type = STATUS_EFFECT_UNIQUE + tick_interval = STATUS_EFFECT_NO_TICK + alert_type = /atom/movable/screen/alert/status_effect/entranced + +/datum/status_effect/entranced/on_creation(mob/living/new_owner, set_duration) + if(IS_FINITE(set_duration)) + duration = set_duration + if(HAS_TRAIT(new_owner, TRAIT_UNCONVERTABLE)) + duration /= 2 + return ..() + +/datum/status_effect/entranced/on_apply() + if(!iscarbon(owner)) + return FALSE + owner.add_traits(list(TRAIT_MUTE, TRAIT_HANDS_BLOCKED, TRAIT_GRABWEAKNESS), TRAIT_STATUS_EFFECT(id)) + owner.add_movespeed_modifier(/datum/movespeed_modifier/status_effect/entranced) + owner.set_jitter_if_lower(duration) + owner.add_client_colour(/datum/client_colour/glass_colour/pink, TRAIT_STATUS_EFFECT(id)) + owner.AddElement(/datum/element/relay_attackers) + RegisterSignal(owner, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_attacked)) + return TRUE + +/datum/status_effect/entranced/on_remove() + UnregisterSignal(owner, COMSIG_ATOM_WAS_ATTACKED) + owner.remove_traits(list(TRAIT_MUTE, TRAIT_HANDS_BLOCKED, TRAIT_GRABWEAKNESS), TRAIT_STATUS_EFFECT(id)) + owner.remove_movespeed_modifier(/datum/movespeed_modifier/status_effect/entranced) + owner.remove_client_colour(TRAIT_STATUS_EFFECT(id)) + to_chat(owner, span_awe("Your mind clears and you regain your focus.")) + +/datum/status_effect/entranced/get_examine_text() + return span_warning("[owner.p_They()] seem[owner.p_s()] dazed and unfocused.") + +/datum/status_effect/entranced/proc/on_attacked(datum/source, atom/attacker, attack_flags) + SIGNAL_HANDLER + if(attacker != owner && (attack_flags & ATTACKER_DAMAGING_ATTACK)) + to_chat(owner, span_awe("You quickly come back to your senses as you're hit by [attacker]!")) + qdel(src) + +/// Alert for entranced status +/atom/movable/screen/alert/status_effect/entranced + name = "Entranced" + desc = "Your mind has been captured by a supernatural presence. You cannot speak or use items." + icon_state = "hypnosis" + +/// Movespeed modifier for the entranced status effect +/datum/movespeed_modifier/status_effect/entranced + multiplicative_slowdown = 2 diff --git a/modular_oculis/modules/vampires/code/powers/presence/force_of_personality.dm b/modular_oculis/modules/vampires/code/powers/presence/force_of_personality.dm new file mode 100644 index 000000000000..928737d5d3fc --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/presence/force_of_personality.dm @@ -0,0 +1,101 @@ +/datum/action/cooldown/vampire/force_of_personality + name = "Force of Personality" + desc = "Project an overwhelming aura of authority that causes those around you to involuntarily step back." + button_icon_state = "power_fop" + power_explanation = "Project an aura around yourself that subtly pushes people away.\n\ + Effects on those in 3 tile range. No one will be able to voluntarily approach you.\n\ + Targets must be able to see you to be affected.\n\ + Lasts 1 minute." + vampire_power_flags = BP_AM_TOGGLE | BP_AM_STATIC_COOLDOWN + vampire_check_flags = BP_CANT_USE_IN_TORPOR | BP_CANT_USE_WHILE_STAKED | BP_CANT_USE_IN_FRENZY + vitaecost = 30 + constant_vitaecost = 2 + cooldown_time = 30 SECONDS + /// The range of the aura in tiles, this is further than the actual effect just so we can hit them with the status effect before they even get close enough. + var/aura_range = 7 + var/deactivate_timer + +/datum/action/cooldown/vampire/force_of_personality/activate_power() + . = ..() + to_chat(owner, span_notice("You project an overwhelming sense of authority."), type = MESSAGE_TYPE_INFO) + deactivate_timer = addtimer(CALLBACK(src, PROC_REF(deactivate_power)), 1 MINUTES, TIMER_STOPPABLE) + +/datum/action/cooldown/vampire/force_of_personality/deactivate_power() + . = ..() + to_chat(owner, span_notice("You withdraw your authoritative presence."), type = MESSAGE_TYPE_INFO) + if(deactivate_timer) + deltimer(deactivate_timer) + deactivate_timer = null + +/datum/action/cooldown/vampire/force_of_personality/use_power() + . = ..() + for(var/mob/living/victim in oviewers(aura_range, owner)) + if(can_affect(victim)) + victim.apply_status_effect(/datum/status_effect/intimidated, owner) + +/// Checks if this victim can be affected by the force of personality aura +/datum/action/cooldown/vampire/force_of_personality/proc/can_affect(mob/living/victim) + if(!victim.client) + return FALSE + if(HAS_SILICON_ACCESS(victim)) + return FALSE + if(victim.stat != CONSCIOUS) + return FALSE + if(victim.is_blind() || victim.is_nearsighted_currently()) + return FALSE + if(HAS_MIND_TRAIT(victim, TRAIT_VAMPIRE_ALIGNED) || IS_CURATOR(victim)) + return FALSE + return TRUE + +/// Status effect for being affected by Force of Personality +/datum/status_effect/intimidated + id = "intimidated" + status_type = STATUS_EFFECT_REFRESH + duration = 10 SECONDS + tick_interval = 0.2 SECONDS + processing_speed = STATUS_EFFECT_PRIORITY + alert_type = null + /// The vampire projecting the aura + var/mob/living/source_vampire + /// The range at which the effect triggers + var/trigger_range = 3 + COOLDOWN_DECLARE(message_cooldown) + +/datum/status_effect/intimidated/on_creation(mob/living/new_owner, mob/living/vampire) + source_vampire = vampire + return ..() + +/datum/status_effect/intimidated/Destroy() + source_vampire = null + return ..() + +/datum/status_effect/intimidated/on_apply() + if(!iscarbon(owner)) + return FALSE + return TRUE + +/datum/status_effect/intimidated/tick(seconds_between_ticks) + if(QDELETED(source_vampire) || source_vampire.stat == DEAD) + qdel(src) + return + + if(INCAPACITATED_IGNORING(owner, INCAPABLE_RESTRAINTS)) + return + + // Only check if we're within range of the vampire + if(get_dist(owner, source_vampire) > trigger_range) + return + + // Step away from the vampire + if(owner.body_position == STANDING_UP) + var/away_dir = get_dir(source_vampire.loc, owner.loc) + var/turf/retreat_turf = get_step(owner.loc, away_dir) + // Make sure we're not stepping into the vampire or into a wall + if(retreat_turf && !retreat_turf.is_blocked_turf()) + if(COOLDOWN_FINISHED(src, message_cooldown)) + COOLDOWN_START(src, message_cooldown, 3 SECONDS) + owner.visible_message(span_warning("[owner] takes a hurried step back."), span_awe("You don't dare approach [source_vampire.p_them()]...")) + owner.Move(retreat_turf, away_dir) + +/datum/status_effect/intimidated/get_examine_text() + return span_warning("[owner.p_They()] seem[owner.p_s()] intimidated.") diff --git a/modular_oculis/modules/vampires/code/powers/presence/presence.dm b/modular_oculis/modules/vampires/code/powers/presence/presence.dm new file mode 100644 index 000000000000..e6151021a73c --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/presence/presence.dm @@ -0,0 +1,9 @@ +/datum/discipline/presence + name = "Presence" + discipline_explanation = "Presence is the Discipline of supernatural presence and subtle manipulation which allows Kindred to dominate the attention of those around them." + icon_state = "presence" + level_1 = list(/datum/action/cooldown/vampire/targeted/entrance) + level_2 = list(/datum/action/cooldown/vampire/targeted/entrance, /datum/action/cooldown/vampire/targeted/summon) + level_3 = list(/datum/action/cooldown/vampire/awe, /datum/action/cooldown/vampire/targeted/entrance, /datum/action/cooldown/vampire/targeted/summon) + level_4 = list(/datum/action/cooldown/vampire/awe, /datum/action/cooldown/vampire/targeted/entrance, /datum/action/cooldown/vampire/targeted/summon, /datum/action/cooldown/vampire/force_of_personality) + level_5 = null diff --git a/modular_oculis/modules/vampires/code/powers/presence/summon.dm b/modular_oculis/modules/vampires/code/powers/presence/summon.dm new file mode 100644 index 000000000000..0424e18fa13c --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/presence/summon.dm @@ -0,0 +1,183 @@ +/** + * SUMMON + * Target can no longer act and is forced to approach the vampire. + * Uses an AI controller to force movement towards the caster. + */ +/datum/action/cooldown/vampire/targeted/summon + name = "Summon" + desc = "Compel a mortal to approach you against their will." + button_icon_state = "power_summon" + power_explanation = "Click any player to summon them towards you.\n\ + Your target will be unable to act and will be compelled to walk towards you.\n\ + The effect ends when they reach you, after a duration, or if line of sight is broken.\n\ + They must be able to see you to be affected." + vampire_power_flags = NONE + vampire_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 + vitaecost = 100 + cooldown_time = 60 SECONDS + target_range = 10 + prefire_message = "Who will you summon to your presence?" + ranged_mousepointer = 'modular_oculis/modules/vampires/icons/mouse_pointers/vampire_summon.dmi' + + /// Maximum duration of the summon effect + var/summon_duration = 30 SECONDS + +/datum/action/cooldown/vampire/targeted/summon/check_valid_target(atom/target_atom) + . = ..() + if(!.) + return FALSE + + if(!iscarbon(target_atom)) + return FALSE + var/mob/living/carbon/carbon_target = target_atom + + if(!carbon_target.mind) + owner.balloon_alert(owner, "[carbon_target] is mindless.") + return FALSE + + if(HAS_MIND_TRAIT(carbon_target, TRAIT_VAMPIRE_ALIGNED) || IS_CURATOR(carbon_target)) + owner.balloon_alert(owner, "immune to your presence.") + return FALSE + + if(HAS_SILICON_ACCESS(carbon_target)) + owner.balloon_alert(owner, "[carbon_target] is immune.") + return FALSE + + if(carbon_target.stat != CONSCIOUS) + owner.balloon_alert(owner, "[carbon_target] is not [(carbon_target.stat == DEAD || HAS_TRAIT(carbon_target, TRAIT_FAKEDEATH)) ? "alive" : "conscious"].") + return FALSE + + if(carbon_target.is_blind()) + owner.balloon_alert(owner, "[carbon_target] is blind.") + return FALSE + + if(carbon_target.has_status_effect(/datum/status_effect/summoned)) + owner.balloon_alert(owner, "[carbon_target] is already being summoned.") + return FALSE + + return TRUE + +/datum/action/cooldown/vampire/targeted/summon/fire_targeted_power(atom/target_atom) + . = ..() + var/mob/living/carbon/carbon_target = target_atom + + carbon_target.apply_status_effect(/datum/status_effect/summoned, summon_duration, owner) + + owner.balloon_alert(owner, "summoning [carbon_target]") + to_chat(carbon_target, span_awe("An irresistible compulsion draws you towards [owner]..."), type = MESSAGE_TYPE_WARNING) + to_chat(owner, span_notice("You beckon [carbon_target] towards you."), type = MESSAGE_TYPE_INFO) + + carbon_target.playsound_local(null, 'modular_oculis/modules/vampires/sound/mesmerize.ogg', 70, FALSE, pressure_affected = FALSE) + +/// Status effect for being summoned towards the vampire +/datum/status_effect/summoned + id = "summoned" + status_type = STATUS_EFFECT_UNIQUE + duration = 30 SECONDS + tick_interval = 0.5 SECONDS + processing_speed = STATUS_EFFECT_PRIORITY + alert_type = /atom/movable/screen/alert/status_effect/summoned + /// The vampire who is summoning us + var/mob/living/source_vampire + /// The move loop handling our movement + var/datum/move_loop/move_loop + /// How long between each step (slow, staggering movement) + var/step_delay = 1.5 SECONDS + +/datum/status_effect/summoned/on_creation(mob/living/new_owner, set_duration, mob/living/vampire) + if(IS_FINITE(set_duration)) + duration = set_duration + source_vampire = vampire + return ..() + +/datum/status_effect/summoned/Destroy() + source_vampire = null + QDEL_NULL(move_loop) + return ..() + +/datum/status_effect/summoned/on_apply() + if(!iscarbon(owner)) + return FALSE + ADD_TRAIT(owner, TRAIT_MUTE, TRAIT_STATUS_EFFECT(id)) + RegisterSignal(owner, COMSIG_MOB_CLIENT_PRE_MOVE, PROC_REF(block_player_move)) + owner.add_client_colour(/datum/client_colour/glass_colour/pink, TRAIT_STATUS_EFFECT(id)) + start_movement() + return TRUE + +/datum/status_effect/summoned/on_remove() + REMOVE_TRAIT(owner, TRAIT_MUTE, TRAIT_STATUS_EFFECT(id)) + UnregisterSignal(owner, COMSIG_MOB_CLIENT_PRE_MOVE) + + owner.remove_client_colour(TRAIT_STATUS_EFFECT(id)) + + if(move_loop) + UnregisterSignal(move_loop, COMSIG_QDELETING) + qdel(move_loop) + move_loop = null + + // Stop any residual movement + GLOB.move_manager.stop_looping(owner) + + to_chat(owner, span_awe("The compulsion fades and you regain control of yourself.")) + +/datum/status_effect/summoned/tick(seconds_between_ticks) + // Check if vampire is still valid + if(QDELETED(source_vampire) || source_vampire.stat == DEAD) + qdel(src) + return + + // Check if we've reached the vampire (adjacent) + if(owner.Adjacent(source_vampire)) + to_chat(owner, span_awe("You have arrived before [source_vampire]...")) + to_chat(source_vampire, span_notice("[owner] has arrived before you.")) + // Brief stun when arriving so we don't look weird with the movespeed + owner.Immobilize(2 SECONDS) + qdel(src) + return + + // Check line of sight - if broken, end the effect + if(!CAN_SEE_RANGED(source_vampire, owner, 10)) + to_chat(owner, span_awe("You lose sight of your summoner and the compulsion breaks.")) + qdel(src) + return + + // Make sure we're facing the vampire + owner.face_atom(source_vampire) + + // Restart movement if it stopped for some reason (blocked by obstacle, etc) + if(!move_loop) + start_movement() + +/datum/status_effect/summoned/get_examine_text() + return span_warning("[owner.p_They()] [owner.p_are()] walking with a blank expression, as if compelled.") + +/datum/status_effect/summoned/nextmove_modifier() + return 2 + +/// Blocks the player from moving themselves while summoned +/datum/status_effect/summoned/proc/block_player_move(mob/source, atom/new_loc) + SIGNAL_HANDLER + return COMSIG_MOB_CLIENT_BLOCK_PRE_MOVE + +/// Starts or restarts the movement loop towards the vampire +/datum/status_effect/summoned/proc/start_movement() + if(move_loop) + qdel(move_loop) + if(QDELETED(source_vampire) || QDELETED(owner)) + return + move_loop = GLOB.move_manager.home_onto(owner, source_vampire, step_delay, timeout = INFINITY) + if(move_loop) + RegisterSignal(move_loop, COMSIG_QDELETING, PROC_REF(on_move_loop_deleted)) + +/// Called when the move loop is deleted externally +/datum/status_effect/summoned/proc/on_move_loop_deleted(datum/source) + SIGNAL_HANDLER + move_loop = null + +/// Alert for summoned status +/atom/movable/screen/alert/status_effect/summoned + name = "Summoned" + desc = "You are being compelled to approach someone. You cannot resist." + icon = 'modular_oculis/modules/vampires/icons/screen_alert.dmi' + icon_state = "vampire_summon" + diff --git a/modular_oculis/modules/vampires/code/powers/thaumaturgy/bloodboil.dm b/modular_oculis/modules/vampires/code/powers/thaumaturgy/bloodboil.dm new file mode 100644 index 000000000000..5d2fa5d5124d --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/thaumaturgy/bloodboil.dm @@ -0,0 +1,131 @@ +/datum/action/cooldown/vampire/targeted/bloodboil + name = "Thaumaturgy: Boil Blood" + desc = "Boil the target's blood inside their body." + button_icon_state = "power_bloodboil" + active_background_icon_state = "tremere_power_bronze_on" + base_background_icon_state = "tremere_power_bronze_off" + power_explanation = "Afflict a debilitating status effect on a target within range, causing them to suffer bloodloss and burn damage.\n\ + The effect weakens if the target is further than 5 tiles away from you, or if you are also draining their blood.\n\ + This is the only thaumaturgy ability to scale with level. It will become more powerful, last longer, gain range, and have a shorter cooldown." + vampire_power_flags = NONE + vampire_check_flags = BP_CANT_USE_IN_TORPOR | BP_CANT_USE_IN_FRENZY | BP_CANT_USE_WHILE_INCAPACITATED | BP_CANT_USE_WHILE_STAKED | BP_CANT_USE_WHILE_UNCONSCIOUS + vitaecost = 30 + cooldown_time = 35 SECONDS + target_range = 7 + power_activates_immediately = FALSE + prefire_message = "Whom will you afflict?" + ranged_mousepointer = 'modular_oculis/modules/vampires/icons/mouse_pointers/vampire_bloodboil.dmi' + + var/powerlevel = 1 + /// How much burn damage is done to the victim per second. + var/burn_damage = 8 + /// How much blood is taken from the victim per second. + var/blood_loss = 8 + /// How long the blood boil effect lasts. + var/effect_duration = 8 SECONDS + +/datum/action/cooldown/vampire/targeted/bloodboil/two + cooldown_time = 30 SECONDS + vitaecost = 45 + target_range = 10 + powerlevel = 2 + burn_damage = 9 + blood_loss = 10 + effect_duration = 10 SECONDS + +/datum/action/cooldown/vampire/targeted/bloodboil/check_valid_target(mob/living/carbon/target) + . = ..() + if(!.) + return FALSE + + // Must be a carbon + if(!iscarbon(target)) + owner.balloon_alert(owner, "not a valid target.") + return FALSE + + if(HAS_TRAIT(target, TRAIT_NOBLOOD)) + owner.balloon_alert(owner, "[target.p_they()] [target.p_have()] no blood anyways!") + return FALSE + + // Check for magic immunity + if(target.can_block_magic(MAGIC_RESISTANCE_HOLY)) + owner.balloon_alert(owner, "your curse was blocked.") + return FALSE + + // Already boiled + if(target.has_status_effect(/datum/status_effect/bloodboil)) + owner.balloon_alert(owner, "[target.p_their()] blood is already boiling!") + return FALSE + +/datum/action/cooldown/vampire/targeted/bloodboil/fire_targeted_power(mob/living/carbon/target) + . = ..() + // Just to make absolutely sure + if(!iscarbon(target)) + return FALSE + + owner.whisper("Potestas Vitae...", forced = "[src]") + + if(target.apply_status_effect(/datum/status_effect/bloodboil, owner, effect_duration, burn_damage, blood_loss)) + to_chat(owner, span_warning("You cause [target]'s blood to boil inside [target.p_their()] body!")) + owner.log_message("used [name] (level [powerlevel]) on [key_name(target)]", LOG_ATTACK) + target.log_message("was hit by [key_name(owner)] with [name] (level [powerlevel])", LOG_VICTIM, log_globally = FALSE) + power_activated_sucessfully() // PAY COST! BEGIN COOLDOWN! + else + to_chat(owner, span_warning("Your thaumaturgy fails to take hold.")) + deactivate_power() + +/datum/status_effect/bloodboil + id = "bloodboil" + duration = 4 SECONDS + tick_interval = 1 SECONDS + status_type = STATUS_EFFECT_UNIQUE + processing_speed = STATUS_EFFECT_PRIORITY + alert_type = /atom/movable/screen/alert/status_effect/bloodboil + /// How much burn damage is dealt per second. + var/burn_damage = 8 + /// How much blood is lost per second. + var/blood_loss = 8 + /// The vampire that casted blood boil. + var/mob/living/caster + +/datum/status_effect/bloodboil/Destroy() + caster = null + return ..() + +/datum/status_effect/bloodboil/on_creation(mob/living/new_owner, mob/living/caster, duration, burn_damage, blood_loss) + src.caster = caster + src.duration = duration + src.burn_damage = burn_damage + src.blood_loss = blood_loss + return ..() + +/datum/status_effect/bloodboil/on_apply() + if(!iscarbon(owner) || HAS_TRAIT(owner, TRAIT_NOBLOOD)) + return FALSE + return TRUE + +/datum/status_effect/bloodboil/tick(seconds_between_ticks) + var/multiplier = 1 + // if their blood is also being drained, halve the damage. + if(owner.has_status_effect(/datum/status_effect/blood_drain)) + multiplier *= 0.5 + + if(get_dist(owner, caster) > 5) + multiplier *= 0.5 + + owner.take_overall_damage(burn = round(burn_damage * multiplier, 1)) + owner.adjust_blood_volume(-round(blood_loss * multiplier, 1)) + + if(SPT_PROB(50, seconds_between_ticks)) + to_chat(owner, span_warning("Oh god! IT BURNS!")) + INVOKE_ASYNC(owner, TYPE_PROC_REF(/mob, emote), "scream") + playsound(owner, pick('sound/effects/wounds/sizzle1.ogg', 'sound/effects/wounds/sizzle2.ogg'), 50, vary = TRUE) + +/datum/status_effect/bloodboil/get_examine_text() + return span_warning("[owner.p_They()] writhe[owner.p_s()] and squirm[owner.p_s()], [owner.p_they()] seem[owner.p_s()] weirdly red?") + +/atom/movable/screen/alert/status_effect/bloodboil + name = "Blood Boil" + desc = "You feel an intense heat coursing through your veins. Your blood is boiling!" + icon = 'modular_oculis/modules/vampires/icons/screen_alert.dmi' + icon_state = "bloodboil" diff --git a/modular_oculis/modules/vampires/code/powers/thaumaturgy/bloodbolt.dm b/modular_oculis/modules/vampires/code/powers/thaumaturgy/bloodbolt.dm new file mode 100644 index 000000000000..7cbec0441358 --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/thaumaturgy/bloodbolt.dm @@ -0,0 +1,73 @@ +/datum/action/cooldown/vampire/targeted/bloodbolt + name = "Thaumaturgy: Blood Bolt" + desc = "Fire a blood bolt at your enemy, dealing Burn damage." + button_icon_state = "power_bloodbolt" + active_background_icon_state = "tremere_power_plat_on" + base_background_icon_state = "tremere_power_plat_off" + power_explanation = "Shoots a blood bolt spell that deals burn damage" + vampire_power_flags = NONE + vampire_check_flags = BP_CANT_USE_IN_TORPOR | BP_CANT_USE_WHILE_STAKED | BP_CANT_USE_IN_FRENZY | BP_CANT_USE_WHILE_INCAPACITATED | BP_CANT_USE_WHILE_UNCONSCIOUS + vitaecost = 75 + cooldown_time = 60 SECONDS + target_range = 80 // Sniper :) + power_activates_immediately = FALSE + prefire_message = "Select your target." + ranged_mousepointer = 'modular_oculis/modules/vampires/icons/mouse_pointers/vampire_bloodbolt.dmi' + +/datum/action/cooldown/vampire/targeted/bloodbolt/fire_targeted_power(atom/target_atom) + . = ..() + var/mob/living/living_owner = owner + // check_witnesses(target_atom) + living_owner.balloon_alert(living_owner, "you fire a blood bolt!") + living_owner.face_atom(target_atom) + living_owner.changeNext_move(CLICK_CD_RANGE) + living_owner.newtonian_move(get_dir(target_atom, living_owner)) + + var/obj/projectile/magic/arcane_barrage/vampire/bolt = new(living_owner.loc) + // bolt.vampire_power = src + bolt.firer = living_owner + bolt.fired_from = src + bolt.original = target_atom + bolt.def_zone = ran_zone(living_owner.zone_selected) + bolt.aim_projectile(target_atom, living_owner) + INVOKE_ASYNC(bolt, TYPE_PROC_REF(/obj/projectile, fire)) + + playsound(living_owner, 'modular_oculis/modules/vampires/sound/bloodbolt_fire.ogg', 60, TRUE) + power_activated_sucessfully() + +/** + * # Blood Bolt + * + * This is the projectile this Power will fire. + */ +/obj/projectile/magic/arcane_barrage/vampire + name = "blood bolt" + icon_state = "mini_leaper" + damage = 40 + hitsound = 'modular_oculis/modules/vampires/sound/bloodbolt.ogg' + antimagic_flags = MAGIC_RESISTANCE_HOLY + // var/datum/action/cooldown/vampire/targeted/bloodbolt/vampire_power + +/obj/projectile/magic/arcane_barrage/vampire/on_hit(atom/target, blocked = 0, pierce_hit) + if(istype(target, /obj/structure/closet)) + var/obj/structure/closet/hit_closet = target + hit_closet.bust_open() + qdel(src) + return BULLET_ACT_HIT + + if(istype(target, /obj/machinery/door/airlock) || istype(target, /obj/machinery/door/window)) + var/obj/machinery/door/airlock = target + airlock.open(FORCING_DOOR_CHECKS) + qdel(src) + return BULLET_ACT_HIT + + if(isliving(target)) + var/mob/living/living_target = target + living_target.adjust_blood_volume(-50) + living_target.emote("scream") + living_target.set_jitter_if_lower(6 SECONDS) + living_target.Unconscious(3 SECONDS) + visible_message(span_danger("[living_target]'s wounds spray boiling hot blood!"), span_userdanger("Oh god it burns!")) + qdel(src) + return BULLET_ACT_HIT + . = ..() diff --git a/modular_oculis/modules/vampires/code/powers/thaumaturgy/blooddrain.dm b/modular_oculis/modules/vampires/code/powers/thaumaturgy/blooddrain.dm new file mode 100644 index 000000000000..639d51f0d903 --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/thaumaturgy/blooddrain.dm @@ -0,0 +1,153 @@ +/datum/action/cooldown/vampire/targeted/blooddrain + name = "Thaumaturgy: Blood Drain" + desc = "Cast a beam of draining magic that saps the vitality of your target to steal their blood and heal yourself." + button_icon_state = "power_blooddrain" + active_background_icon_state = "tremere_power_on" + base_background_icon_state = "tremere_power_off" + power_explanation = "Cast a beam of draining magic that saps the vitality of your target to steal their blood and heal yourself.\n\ + You must maintain line of sight to the victim for the effect to continue." + vampire_power_flags = BP_AM_TOGGLE + vampire_check_flags = BP_CANT_USE_IN_TORPOR | BP_CANT_USE_WHILE_STAKED | BP_CANT_USE_IN_FRENZY | BP_CANT_USE_WHILE_INCAPACITATED | BP_CANT_USE_WHILE_UNCONSCIOUS + vitaecost = 75 + cooldown_time = 10 SECONDS // Very unlikely to ever last past 10 seconds even if the actual duration is longer. Combat is a fuck. + target_range = 7 + power_activates_immediately = FALSE + prefire_message = "Select your target." + ranged_mousepointer = 'modular_oculis/modules/vampires/icons/mouse_pointers/vampire_blooddrain.dmi' + + var/datum/status_effect/blood_drain/active_effect + +/datum/action/cooldown/vampire/targeted/blooddrain/fire_targeted_power(atom/target_atom) + . = ..() + var/mob/living/living_owner = owner + // check_witnesses(target_atom) + living_owner.face_atom(target_atom) + living_owner.changeNext_move(CLICK_CD_RANGE) + living_owner.newtonian_move(get_dir(target_atom, living_owner)) + + var/obj/projectile/magic/blood_drain/drain = new(living_owner.loc) + drain.firer = living_owner + drain.fired_from = src + if(isliving(target_atom)) + drain.original = target_atom + drain.def_zone = ran_zone(living_owner.zone_selected) + drain.aim_projectile(target_atom, living_owner) + INVOKE_ASYNC(drain, TYPE_PROC_REF(/obj/projectile, fire)) + + playsound(living_owner, 'sound/effects/magic/wandodeath.ogg', 60, TRUE) + +/datum/action/cooldown/vampire/targeted/blooddrain/deactivate_power() + . = ..() + active_effect?.end_drain() + +/obj/projectile/magic/blood_drain + name = "vitality draining stream" + icon_state = "nothing" + range = 7 + antimagic_flags = MAGIC_RESISTANCE_HOLY + hitsound = 'modular_oculis/modules/vampires/sound/bloodbolt.ogg' + var/datum/beam/drain_beam + +/obj/projectile/magic/blood_drain/fire(angle, atom/direct_target) + if(!firer) + CRASH("Projectile [src] fired with no firer") //We don't even want any of the rest of this to play out if we don't have a firer + drain_beam = firer.Beam(src, icon = 'icons/effects/beam.dmi', icon_state = "lifedrain", time = 10 SECONDS, maxdistance = 7, beam_color = COLOR_RED) + return ..() + +/obj/projectile/magic/blood_drain/on_hit(mob/living/target, blocked, pierce_hit) + . = ..() + if(isliving(target)) + QDEL_NULL(drain_beam) + target.apply_status_effect(/datum/status_effect/blood_drain, firer, fired_from) + +/obj/projectile/magic/blood_drain/Destroy() + if(!QDELETED(drain_beam)) + qdel(drain_beam) + drain_beam = null + return ..() + +/// +/// Status Effect. Literally copied from life drain spell of wizards, but modified to work with vampires. +/// +/datum/status_effect/blood_drain + id = "blood_drain" + duration = 20 SECONDS + tick_interval = 0.25 SECONDS + status_type = STATUS_EFFECT_REPLACE + processing_speed = STATUS_EFFECT_PRIORITY + alert_type = null + var/datum/beam/drain_beam + var/mob/living/carbon/vampire + var/datum/action/cooldown/vampire/targeted/blooddrain/spell + var/blood_drain = 5 // Amount of blood drained per second + +/datum/status_effect/blood_drain/Destroy() + . = ..() + vampire = null + +/datum/status_effect/blood_drain/on_creation(mob/living/new_owner, mob/living/firer, fired_from, duration_override) + if(isnull(firer) || isnull(fired_from) || !iscarbon(firer) || !iscarbon(new_owner)) + qdel(src) + return + vampire = firer + spell = fired_from + spell.active_effect = src + . = ..() + +/datum/status_effect/blood_drain/on_apply() + owner.add_movespeed_modifier(/datum/movespeed_modifier/status_effect/life_drain) + drain_beam = vampire.Beam(owner, icon = 'icons/effects/beam.dmi', icon_state = "blood_drain", time = 22 SECONDS, maxdistance = 7, beam_color = COLOR_RED) + RegisterSignal(drain_beam, COMSIG_QDELETING, PROC_REF(end_drain)) + owner.visible_message( + span_boldwarning("[vampire] begins draining the life force from [owner]!"), + span_boldwarning("[vampire] is draining your life force! You need to get away from [vampire.p_them()] to stop it!"), + ) + return TRUE + +/datum/status_effect/blood_drain/on_remove() + owner.remove_movespeed_modifier(/datum/movespeed_modifier/status_effect/life_drain) + if(spell) + spell.active_effect = null + spell.deactivate_power() + spell.StartCooldown() + spell = null + if(!QDELETED(drain_beam)) + qdel(drain_beam) + drain_beam = null + +/datum/status_effect/blood_drain/tick(seconds_between_ticks) + if(!iscarbon(owner) || owner.stat > HARD_CRIT) //If they're dead or non-humanoid, this spell fails + end_drain() + return + if(!iscarbon(vampire)) //You never know what might happen with wizards around + end_drain() + return + if(!CAN_THEY_SEE(vampire, owner)) // if they leave line of sight, no more drain. + end_drain() + return + + if(HAS_TRAIT(owner, TRAIT_INCAPACITATED) || owner.stat) + //If the victim is incapacitated, drain their blood + owner.adjust_blood_volume(-blood_drain * seconds_between_ticks) + else + //If they aren't incapacitated yet, drain only their stamina + owner.adjust_stamina_loss(7 * seconds_between_ticks) + + if(SPT_PROB(20, seconds_between_ticks)) + INVOKE_ASYNC(owner, TYPE_PROC_REF(/mob, emote), "scream") + owner.visible_message(span_boldwarning("[vampire] absorbs blood from [owner]!"), span_boldwarning("It BURNS!")) + + //Vampire heals at a steady rate over the duration of the spell regardless of the victim's state + vampire.heal_overall_damage(brute = 0.5, burn = 0.5, stamina = 5) + + spell.vampiredatum_power.adjust_blood_volume(blood_drain * seconds_between_ticks * 2) // Vampires get double the blood drained because of balance + //Weird beam visuals if it isn't redrawn due to the beam sending players into crit + drain_beam.redrawing() + +/datum/status_effect/blood_drain/proc/end_drain() + SIGNAL_HANDLER + if(!QDELETED(src)) + qdel(src) + +/datum/movespeed_modifier/status_effect/life_drain + multiplicative_slowdown = 1.25 diff --git a/modular_oculis/modules/vampires/code/powers/thaumaturgy/bloodshield.dm b/modular_oculis/modules/vampires/code/powers/thaumaturgy/bloodshield.dm new file mode 100644 index 000000000000..ce1b8919c6fe --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/thaumaturgy/bloodshield.dm @@ -0,0 +1,69 @@ +/datum/action/cooldown/vampire/bloodshield + name = "Thaumaturgy: Blood Shield" + desc = "Create a Blood shield to protect yourself from damage." + button_icon_state = "power_bloodshield" + active_background_icon_state = "tremere_power_gold_on" + base_background_icon_state = "tremere_power_gold_off" + power_explanation = "Activating Thaumaturgy will temporarily give you a Blood Shield.\n\ + The blood shield has very good block power, but costs 15 Blood per hit to maintain.\n\ + However, it is slightly less effective at blocking lasers or lethal energy projectiles." + + vampire_power_flags = BP_AM_TOGGLE | BP_AM_STATIC_COOLDOWN + vampire_check_flags = BP_CANT_USE_IN_TORPOR | BP_CANT_USE_WHILE_STAKED | BP_CANT_USE_IN_FRENZY | BP_CANT_USE_WHILE_INCAPACITATED | BP_CANT_USE_WHILE_UNCONSCIOUS + + vitaecost = 20 + cooldown_time = 10 SECONDS + constant_vitaecost = 3 + + /// Blood shield given while this Power is active. + var/datum/weakref/blood_shield + +/datum/action/cooldown/vampire/bloodshield/activate_power() + . = ..() + var/obj/item/shield/vampire/new_shield = new + if(!owner.put_in_inactive_hand(new_shield)) + qdel(new_shield) + owner.balloon_alert(owner, "off hand is full!") + to_chat(owner, span_notice("Blood shield couldn't be activated as your off hand is full.")) + deactivate_power() + return FALSE + blood_shield = WEAKREF(new_shield) + owner.visible_message( + span_warning("[owner]'s hands begins to bleed and forms into a blood shield!"), + span_warning("We activate our Blood shield!"), + span_hear("You hear liquids forming together."), + ) + +/datum/action/cooldown/vampire/bloodshield/deactivate_power() + . = ..() + to_chat(owner, span_notice("Blood shield couldn't be activated as your off hand is full.")) + if(blood_shield) + QDEL_NULL(blood_shield) + +/** + * # Blood Shield + * Copied mostly from '/obj/item/shield/changeling' + */ +/obj/item/shield/vampire + name = "blood shield" + desc = "A shield made out of blood, requiring blood to sustain hits." + item_flags = ABSTRACT | DROPDEL + icon = 'modular_oculis/modules/vampires/icons/vamp_obj.dmi' + icon_state = "blood_shield" + lefthand_file = 'modular_oculis/modules/vampires/icons/bs_leftinhand.dmi' + righthand_file = 'modular_oculis/modules/vampires/icons/bs_rightinhand.dmi' + block_chance = 100 + +/obj/item/shield/vampire/Initialize(mapload) + . = ..() + ADD_TRAIT(src, TRAIT_NODROP, INNATE_TRAIT) + +/obj/item/shield/vampire/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK) + if(attack_type == PROJECTILE_ATTACK) + // energy and laser beams have less block chance + if(istype(hitby, /obj/projectile/energy) || istype(hitby, /obj/projectile/beam/laser)) + final_block_chance -= 25 + . = ..() + if(. && damage > 0) + var/datum/antagonist/vampire/vampire = IS_VAMPIRE(owner) + vampire?.adjust_blood_volume(-15) diff --git a/modular_oculis/modules/vampires/code/powers/thaumaturgy/thaumaturgy.dm b/modular_oculis/modules/vampires/code/powers/thaumaturgy/thaumaturgy.dm new file mode 100644 index 000000000000..a7a1f31cac57 --- /dev/null +++ b/modular_oculis/modules/vampires/code/powers/thaumaturgy/thaumaturgy.dm @@ -0,0 +1,11 @@ +/datum/discipline/thaumaturgy + name = "Thaumaturgy" + discipline_explanation = "Thaumaturgy is the closely guarded form of blood magic practiced by the vampiric clan Tremere." + icon_state = "thaumaturgy" + + // Lists of abilities granted per level + level_1 = list(/datum/action/cooldown/vampire/targeted/bloodbolt) + level_2 = list(/datum/action/cooldown/vampire/targeted/bloodbolt, /datum/action/cooldown/vampire/bloodshield) + level_3 = list(/datum/action/cooldown/vampire/targeted/bloodbolt, /datum/action/cooldown/vampire/bloodshield, /datum/action/cooldown/vampire/targeted/blooddrain) + level_4 = list(/datum/action/cooldown/vampire/targeted/bloodbolt, /datum/action/cooldown/vampire/bloodshield, /datum/action/cooldown/vampire/targeted/blooddrain, /datum/action/cooldown/vampire/targeted/bloodboil) + level_5 = list(/datum/action/cooldown/vampire/targeted/bloodbolt, /datum/action/cooldown/vampire/bloodshield, /datum/action/cooldown/vampire/targeted/blooddrain, /datum/action/cooldown/vampire/targeted/bloodboil/two) diff --git a/modular_oculis/modules/vampires/code/slime_vampire.dm b/modular_oculis/modules/vampires/code/slime_vampire.dm new file mode 100644 index 000000000000..42d676072a5f --- /dev/null +++ b/modular_oculis/modules/vampires/code/slime_vampire.dm @@ -0,0 +1,96 @@ +// Slime vampires have some unique behavior, so I'm just gonna stick it all in this one file. + +/// Handles setting up self-revival when an slime vampire dies. +/datum/antagonist/vampire/proc/on_slime_core_ejected(datum/source, obj/item/organ/brain/slime/core) + SIGNAL_HANDLER + if(QDELETED(core) || final_death) + return + core.organ_flags |= ORGAN_FROZEN + if(current_vitae < SLIME_MIN_REVIVE_BLOOD_THRESHOLD) + to_chat(core.brainmob, span_narsiesmall("You do not have enough vitae to recollect yourself on your own!"), type = MESSAGE_TYPE_WARNING) + return + adjust_blood_volume(-SLIME_MIN_REVIVE_BLOOD_THRESHOLD * 0.5) + to_chat(core.brainmob, span_narsiesmall("You begin recollecting yourself. You will rise again soon, if your core remains undisturbed."), type = MESSAGE_TYPE_INFO) + new /datum/vampire_slime_reviver(src, core) + +/// Heals an slime vampire's organs when they revive. +/datum/antagonist/vampire/proc/on_slime_revive(datum/source, mob/living/carbon/human/new_body, obj/item/organ/brain/slime/core) + SIGNAL_HANDLER + core.organ_flags &= ~ORGAN_FROZEN + if(ishuman(owner.current)) + var/mob/living/carbon/human/human_owner = owner.current + human_owner.regenerate_limbs() + heal_vampire_organs() + +/datum/antagonist/vampire/proc/slime_self_revive(obj/item/organ/brain/slime/core) + if(QDELETED(core) || final_death) + return +#ifdef is_slime_core + var/mob/living/carbon/human/new_body = core.rebuild_body(nugget = FALSE) + to_chat(new_body, span_cult_large("You recollect yourself, your vitae reforming your body from your core!"), type = MESSAGE_TYPE_INFO) +#else + core.regenerate() + to_chat(owner.current, span_cult_large("You recollect yourself, your vitae reforming your body from your core!"), type = MESSAGE_TYPE_INFO) +#endif + +/// Stupid datum used for, to avoid a bunch of slime-specific vars on the vampire datum +/datum/vampire_slime_reviver + /// The parent vampire antag datum. + var/datum/antagonist/vampire/vampire + /// The slime core we're reviving. + var/obj/item/organ/brain/slime/core + /// Progress ticker used for reviving. + var/slime_revival_progress = SLIME_VAMPIRE_REVIVE_TIME + /// The last world.time where we processed. + var/last_process + /// Cooldown for sending a chat message to the slime player how long until they revive. + COOLDOWN_DECLARE(reminder_cooldown) + +/datum/vampire_slime_reviver/New(datum/antagonist/vampire/vampire, obj/item/organ/brain/slime/core) + src.vampire = vampire + src.core = core + src.last_process = world.time + RegisterSignal(core, COMSIG_QDELETING, PROC_REF(delete_self)) + RegisterSignal(vampire.owner, COMSIG_SLIME_REVIVED, PROC_REF(delete_self)) + START_PROCESSING(SSfastprocess, src) + +/datum/vampire_slime_reviver/Destroy(force) + UnregisterSignal(core, COMSIG_QDELETING) + UnregisterSignal(vampire.owner, COMSIG_SLIME_REVIVED) + STOP_PROCESSING(SSfastprocess, src) + vampire = null + core = null + return ..() + +/datum/vampire_slime_reviver/proc/delete_self() + SIGNAL_HANDLER + if(!QDELETED(src)) + qdel(src) + +/datum/vampire_slime_reviver/proc/progress_multiplier() + . = 1 + if(HAS_TRAIT(core, TRAIT_BEINGSTAKED)) + return 0 + if(istype(core.loc, /obj/structure/closet/crate/coffin)) + return SLIME_VAMPIRE_REVIVE_COFFIN_MULTIPLIER + var/mob/living/holder = get(core, /mob/living) + if(!QDELETED(holder)) + if(HAS_MIND_TRAIT(holder, TRAIT_VAMPIRE_ALIGNED)) + return SLIME_VAMPIRE_REVIVE_ALLY_MULTIPLIER + else + return SLIME_VAMPIRE_REVIVE_HELD_MULTIPLIER + +/datum/vampire_slime_reviver/process(seconds_per_tick) + if(QDELETED(core) || !core.core_ejected || vampire.final_death) + delete_self() + return + slime_revival_progress -= (world.time - last_process) * progress_multiplier() + last_process = world.time + if(slime_revival_progress <= 0) + vampire.slime_self_revive(core) + delete_self() + else if(COOLDOWN_FINISHED(src, reminder_cooldown)) + var/progress = round((1 - (slime_revival_progress / SLIME_VAMPIRE_REVIVE_TIME)) * 100, 1) + to_chat(core.brainmob, span_cult_large("Your vitae coagulates... You are approximately [progress]% reformed."), type = MESSAGE_TYPE_INFO) + core.balloon_alert(core.brainmob, "[progress]% reformed...") + COOLDOWN_START(src, reminder_cooldown, 15 SECONDS) diff --git a/modular_oculis/modules/vampires/code/torpor_vampire.dm b/modular_oculis/modules/vampires/code/torpor_vampire.dm new file mode 100644 index 000000000000..b0f7d8605d0f --- /dev/null +++ b/modular_oculis/modules/vampires/code/torpor_vampire.dm @@ -0,0 +1,104 @@ +/** + * # Torpor + * + * Torpor is what deals with the Vampire 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: + * - Entering a Coffin with more than 10 combined Brute/Burn damage, dealt with by /closet/crate/coffin/close() [coffins.dm] + * - Death, dealt with by /HandleDeath() + * Torpor is ended by: + * - Having less than 10 Burn damage while OUTSIDE of your Coffin while it isnt Sol. + * - Having less than 10 Brute & Burn Combined while INSIDE of your Coffin while it isnt Sol. + * - Sol being over, dealt with by /sunlight/process() [vampire_daylight.dm] +**/ +/datum/antagonist/vampire/proc/check_begin_torpor() + var/mob/living/carbon/carbon_owner = owner.current + if(QDELETED(carbon_owner)) + return + var/total_damage = carbon_owner.get_brute_loss() + carbon_owner.get_fire_loss() + if(total_damage >= 10 || length(carbon_owner.all_wounds)) + carbon_owner.apply_status_effect(/datum/status_effect/vampire_torpor) + +/datum/status_effect/vampire_torpor + id = "torpor" + alert_type = null + remove_on_fullheal = TRUE + heal_flag_necessary = HEAL_ADMIN // so admins can aheal in case stuff goes fucky wucky + /// Antag datum of the vampire. + var/datum/antagonist/vampire/vampire_datum + /// Cooldown twhere, if it finishes, we'll just force heal the vampire, to avoid eternal torpor. + COOLDOWN_DECLARE(force_heal_time) + /// List of traits applied while in torpor. + var/static/list/torpor_traits = list( + TRAIT_DEATHCOMA, + TRAIT_FAKEDEATH, + TRAIT_NODEATH, + TRAIT_RESISTHIGHPRESSURE, + TRAIT_RESISTLOWPRESSURE, + TRAIT_TORPOR, + ) + +/datum/status_effect/vampire_torpor/on_apply() + if(!iscarbon(owner) || QDELING(owner)) + return FALSE + vampire_datum = IS_VAMPIRE(owner) + if(!vampire_datum) + . = FALSE + CRASH("Attempted to apply [type] to a non-vampire!") + if(vampire_datum.final_death) + return FALSE + REMOVE_TRAIT(owner, TRAIT_SLEEPIMMUNE, TRAIT_VAMPIRE) + owner.add_traits(torpor_traits, TRAIT_STATUS_EFFECT(id)) + owner.remove_status_effect(/datum/status_effect/jitter) + vampire_datum.disable_all_powers() + to_chat(owner, span_notice("You enter the horrible slumber of deathless Torpor. You will heal until you are renewed.")) + COOLDOWN_START(src, force_heal_time, 5 MINUTES) + return TRUE + +/datum/status_effect/vampire_torpor/on_remove() + if(!iscarbon(owner) || vampire_datum.final_death) + return + + owner.grab_ghost() + owner.remove_traits(torpor_traits, TRAIT_STATUS_EFFECT(id)) + if(!HAS_TRAIT(owner, TRAIT_FEIGN_LIFE)) + ADD_TRAIT(owner, TRAIT_SLEEPIMMUNE, TRAIT_VAMPIRE) + + vampire_datum.heal_vampire_organs() + vampire_datum.my_clan?.on_exit_torpor() + vampire_datum = null + + to_chat(owner, span_notice("You have recovered from Torpor.")) + +/datum/status_effect/vampire_torpor/tick(seconds_between_ticks) + if(should_end()) + qdel(src) + return + + if(COOLDOWN_FINISHED(src, force_heal_time) && !QDELETED(owner)) + log_game("[key_name(owner)] was in Torpor for 5 minutes, immediately reviving them to prevent a potential softlock.") + owner.revive(HEAL_ALL) + qdel(src) + +/datum/status_effect/vampire_torpor/proc/should_end() + if(HAS_TRAIT(owner, TRAIT_FRENZY)) + return TRUE + + var/total_brute = owner.get_brute_loss() + var/total_burn = owner.get_fire_loss() + + if(total_burn >= 199) + return FALSE + + // You are in a coffin, so instead we'll check TOTAL damage. + if(istype(owner.loc, /obj/structure/closet/crate/coffin)) + if((total_brute + total_burn) <= 10) + owner.heal_overall_damage(brute = 10, burn = 10) // heal minor leftover damage + return TRUE + else if(total_brute <= 10) + owner.heal_overall_damage(brute = 10) // heal minor leftover damage + return TRUE + + return FALSE diff --git a/modular_oculis/modules/vampires/code/tracking_vampire.dm b/modular_oculis/modules/vampires/code/tracking_vampire.dm new file mode 100644 index 000000000000..ac4d5a6190ec --- /dev/null +++ b/modular_oculis/modules/vampires/code/tracking_vampire.dm @@ -0,0 +1,45 @@ +/atom/movable/screen/tracking_arrow + icon = 'modular_oculis/modules/vampires/icons/arrow.dmi' + icon_state = "hud_arrow" + screen_loc = "CENTER,CENTER" + color = "#960000" + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + +/atom/movable/screen/tracking_arrow/proc/update(mob/user, atom/target) + var/turf/our_turf = get_turf(user) + var/turf/their_turf = get_turf(target) + if(!our_turf || !their_turf) + invisibility = INVISIBILITY_ABSTRACT + return + invisibility = INVISIBILITY_NONE + var/matrix/rotation_matrix = matrix() + rotation_matrix.Scale(1.5) + rotation_matrix.Translate(0, -20) + rotation_matrix.Turn(get_angle(their_turf, our_turf)) + var/new_alpha = 240 + var/new_color = "#808080" + if(their_turf.z == our_turf.z) + switch(get_dist(their_turf, our_turf)) + if(0) + new_alpha = 0 + if(1) + new_alpha = 60 + if(2) + new_alpha = 100 + if(3) + new_alpha = 150 + else + new_alpha = 240 + new_color = "#960000" + animate(src, alpha = new_alpha, color = new_color, transform = rotation_matrix, time = 0.2 SECONDS) + +/datum/antagonist/vampire/proc/update_all_trackers() + SIGNAL_HANDLER + if(QDELETED(owner.current) || !length(vassals)) + return + for(var/datum/antagonist/vassal/vassal as anything in vassals) + var/mob/living/vassal_mob = vassal.owner.current + if(QDELETED(vassal_mob)) + continue + var/atom/movable/screen/tracking_arrow/tracking_arrow = vassal_mob.hud_used?.screen_objects[HUD_VASSAL_TRACKER] + tracking_arrow?.update(vassal_mob, owner.current) diff --git a/modular_oculis/modules/vampires/code/vassals/datum_vassal.dm b/modular_oculis/modules/vampires/code/vassals/datum_vassal.dm new file mode 100644 index 000000000000..2482e182ec99 --- /dev/null +++ b/modular_oculis/modules/vampires/code/vassals/datum_vassal.dm @@ -0,0 +1,327 @@ +/datum/antagonist/vassal + name = "\improper Vassal" + roundend_category = "Vassal" + antagpanel_category = "Vampire" + show_in_roundend = FALSE + hud_icon = 'modular_oculis/modules/vampires/icons/antag_hud.dmi' + antag_hud_name = "vassal" + stinger_sound = 'sound/effects/magic/mutate.ogg' + hijack_speed = 0 + + pref_flag = ROLE_VASSAL + + desensitized_modifier = DESENSITIZED_THRESHOLD + + /// The Master Vampire's antag datum. + var/datum/antagonist/vampire/master + /// The Vampire's team + var/datum/team/vampire/vampire_team + /// List of Powers, like Vampires. + var/list/datum/action/powers = list() + + /// How much time has been spent away from their master, used for moodlets. + var/time_away_from_master = 0 + var/last_life_tick = 0 + +/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 + + last_life_tick = world.time + + RegisterSignal(current_mob, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine)) + RegisterSignals(current_mob, list(COMSIG_MOB_LOGIN, COMSIG_MOVABLE_Z_CHANGED), PROC_REF(on_login)) + RegisterSignal(current_mob, COMSIG_MOB_UPDATE_SIGHT, PROC_REF(on_update_sight)) + RegisterSignal(current_mob, COMSIG_MOVABLE_MOVED, PROC_REF(update_tracker)) + RegisterSignal(current_mob, COMSIG_LIVING_LIFE, PROC_REF(on_life)) + RegisterSignal(current_mob, COMSIG_MOVABLE_HEAR, PROC_REF(handle_hearing)) + + current_mob.update_sight() + + // HUD + add_team_hud(current_mob) + + // Tracking + // setup_monitor(current_mob) + current_mob.grant_language(/datum/language/vampiric, source = LANGUAGE_VASSAL) + + current_mob.add_faction(FACTION_VAMPIRE) + + current_mob.clear_mood_event("vampcandle") + + if(current_mob.hud_used) + on_hud_created() + else + RegisterSignal(current_mob, COMSIG_MOB_HUD_CREATED, PROC_REF(on_hud_created)) + +/datum/antagonist/vassal/remove_innate_effects(mob/living/mob_override) + . = ..() + var/mob/living/current_mob = mob_override || owner.current + + UnregisterSignal(current_mob, list( + COMSIG_ATOM_EXAMINE, + COMSIG_MOB_LOGIN, + COMSIG_MOVABLE_Z_CHANGED, + COMSIG_MOB_UPDATE_SIGHT, + COMSIG_MOB_HUD_CREATED, + COMSIG_MOVABLE_MOVED, + COMSIG_LIVING_LIFE, + COMSIG_MOVABLE_HEAR, + )) + current_mob.update_sight() + current_mob.clear_mood_event("vassal") + + // Tracking + remove_hud_elements(current_mob) + current_mob.remove_language(/datum/language/vampiric, source = LANGUAGE_VASSAL) + + // Remove traits + REMOVE_TRAITS_IN(current_mob, TRAIT_VAMPIRE) + current_mob.remove_faction(FACTION_VAMPIRE) + +/datum/antagonist/vassal/on_gain() + . = ..() + if(!master) + owner.remove_antag_datum(src) + CRASH("[owner.current] was vassalized without a master!") + + ADD_TRAIT(owner, TRAIT_VAMPIRE_ALIGNED, REF(src)) + + vampire_team = master.vampire_team + vampire_team.add_member(owner) + + // Enslave them to their Master + master.vassals |= src + owner.enslave_mind_to_creator(master.owner.current) + owner.current.log_message("has been vassalized by [master.owner.name]!", LOG_ATTACK, color="#960000") + + // Give powers + grant_power(new /datum/action/cooldown/vampire/recuperate) + grant_power(new /datum/action/cooldown/vampire/distress) + + // Give objectives + forge_objectives() + +/datum/antagonist/vassal/on_removal() + REMOVE_TRAIT(owner, TRAIT_VAMPIRE_ALIGNED, REF(src)) + + // Free them from their Master + if(master) + master.vassals -= src + owner.enslaved_to = null + + vampire_team.remove_member(owner) + vampire_team = null + + // Remove powers + for(var/datum/action/cooldown/vampire/power in powers) + powers -= power + power.Remove(owner.current) + + return ..() + +/datum/antagonist/vassal/on_body_transfer(mob/living/old_body, mob/living/new_body) + . = ..() + for(var/datum/action/cooldown/vampire/power in powers) + power.Remove(old_body) + power.Grant(new_body) + +/* +/datum/antagonist/vassal/after_body_transfer(mob/living/old_body, mob/living/new_body) + addtimer(CALLBACK(src, TYPE_PROC_REF(/datum/antagonist, add_team_hud), new_body), 0.5 SECONDS, TIMER_OVERRIDE | TIMER_UNIQUE) //i don't trust this to not act weird +*/ + +/datum/antagonist/vassal/greet() + var/mob/living/living_vassal = owner.current + var/mob/living/living_master = master.owner.current + + // Alert vassal + var/list/msg = list() + msg += span_cult_large("You are now the mortal servant of [master.owner.name], a Vampire!") + msg += span_cult("You are not required to obey any other Vampire, for only [master.owner.name] is your master. The laws of Nanotrasen do not apply to you now; only your Master's word must be obeyed.") + to_chat(living_vassal, boxed_message(msg.Join("\n"))) + + play_stinger() + + antag_memory += "You are the mortal servant of [master.owner.name], a vampire!
" + + // Alert master + to_chat(living_master, span_userdanger("[living_vassal] has become addicted to your immortal blood. [living_vassal.p_They()] [living_vassal.p_are()] now your undying servant")) + living_master.playsound_local(null, 'sound/effects/magic/mutate.ogg', 100, FALSE, pressure_affected = FALSE) + +/datum/antagonist/vassal/farewell() + if(silent) + return + + owner.current.visible_message( + span_deconversion_message("[owner.current]'s eyes dart feverishly from side to side, and then stop. [owner.current.p_They()] seem[owner.current.p_s()] calm, \ + like [owner.current.p_they()] [owner.current.p_have()] regained some lost part of [owner.current.p_them()]self."), + span_deconversion_message("With a snap, you are no longer enslaved to [master.owner]! You breathe in heavily, having regained your free will, albeit the memories of your time serving them feel like a vague fever dream...") + ) + owner.current.playsound_local(null, 'sound/effects/magic/mutate.ogg', 100, FALSE, pressure_affected = FALSE) + + // Alert master + if(master.owner) + to_chat(master.owner, span_cult_bold("You feel the bond with your vassal [owner.name] has somehow been broken!")) + +/datum/antagonist/vassal/on_mindshield(mob/implanter, mob/living/mob_override) + var/mob/living/target = mob_override || owner.current + target.log_message("has been deconverted from Vassalization by [key_name(implanter)]!", LOG_ATTACK, color="#960000") + owner.remove_antag_datum(/datum/antagonist/vassal) + return COMPONENT_MINDSHIELD_DECONVERTED + +/datum/antagonist/vassal/proc/on_login() + SIGNAL_HANDLER + var/mob/living/current = owner.current + if(!QDELETED(current)) + addtimer(CALLBACK(src, TYPE_PROC_REF(/datum/antagonist, add_team_hud), current), 0.5 SECONDS, TIMER_OVERRIDE | TIMER_UNIQUE) //i don't trust this to not act weird + +/datum/antagonist/vassal/admin_add(datum/mind/new_owner, mob/admin) + var/list/datum/mind/possible_vampires = list() + + // Get possible vampires + for(var/datum/antagonist/vampire/vampire in GLOB.antagonists) + var/datum/mind/vampire_mind = vampire.owner + if(QDELETED(vampire_mind?.current) || vampire_mind.current.stat == DEAD) + continue + + possible_vampires += vampire_mind + + if(!length(possible_vampires)) + return + + // CHOOSE A DAMN PERSON + var/datum/mind/choice = tgui_input_list(admin, "Which vampire should this vassal belong to?", "Vampire", possible_vampires) + if(!choice) + return + + log_admin("[key_name_admin(usr)] turned [key_name_admin(new_owner)] into a vassal of [key_name_admin(choice)]!") + var/datum/antagonist/vampire/vampire = IS_VAMPIRE(choice.current) + master = vampire + new_owner.add_antag_datum(src) + + to_chat(choice, span_notice("Through divine intervention, you've gained a new vassal!")) + +/datum/antagonist/vassal/forge_objectives() + var/datum/objective/vampire/vassal/vassal_objective = new + vassal_objective.owner = owner + objectives += vassal_objective + addtimer(CALLBACK(src, TYPE_PROC_REF(/datum, update_static_data_for_all_viewers)), 0.5 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE) + +/datum/antagonist/vassal/add_team_hud(mob/target) + QDEL_NULL(team_hud_ref) + + var/datum/atom_hud/alternate_appearance/basic/has_antagonist/hud = target.add_alt_appearance( + /datum/atom_hud/alternate_appearance/basic/has_antagonist, + "antag_team_hud_[REF(src)]", + hud_image_on(target), + ) + team_hud_ref = WEAKREF(hud) + + var/list/mob/living/mob_list = list() + for(var/datum/antagonist/antag as anything in GLOB.antagonists) + if(!istype(antag, /datum/antagonist/vampire) && !istype(antag, /datum/antagonist/vassal)) + continue + var/mob/living/current = antag.owner?.current + if(!QDELETED(current)) + mob_list |= current + + for (var/datum/atom_hud/alternate_appearance/basic/has_antagonist/antag_hud as anything in GLOB.has_antagonist_huds) + if(!(antag_hud.target in mob_list)) + continue + antag_hud.show_to(target) + hud.show_to(antag_hud.target) + +/datum/antagonist/vassal/proc/on_examine(datum/source, mob/examiner, list/examine_text) + SIGNAL_HANDLER + + var/text = " " + + var/datum/antagonist/vampire/vampiredatum = IS_VAMPIRE(examiner) + if(src in vampiredatum?.vassals) + text += span_cult("This is your vassal!") + examine_text += text + else if(vampiredatum || (master?.broke_masquerade && IS_CURATOR(examiner)) || IS_VASSAL(examiner)) + text += span_cult("This is [master.return_full_name()]'s vassal") + examine_text += text + +// Vassals get night vision, so they can at least somewhat be useful to their masters in dark areas without revealing themselves with a flashlight or something. +// The night vision is weaker than true night vision like a vampire has, but it's still better than mesons. +/datum/antagonist/vassal/proc/on_update_sight(mob/user) + SIGNAL_HANDLER + user.lighting_cutoff = max(user.lighting_cutoff, round((LIGHTING_CUTOFF_HIGH + LIGHTING_CUTOFF_MEDIUM) / 2, 1)) + user.lighting_color_cutoffs = user.lighting_color_cutoffs ? blend_cutoff_colors(user.lighting_color_cutoffs, list(25, 8, 5)) : list(25, 8, 5) + +/// Used when your Master teaches you a new Power. +/datum/antagonist/vassal/proc/grant_power(datum/action/cooldown/vampire/power) + powers += power + power.Grant(owner.current) + log_uplink("[key_name(owner.current)] has received \"[power]\" as a vassal") + +/datum/antagonist/vassal/proc/on_hud_created(datum/source) + SIGNAL_HANDLER + var/datum/hud/hud_used = owner.current.hud_used + var/atom/movable/screen/tracking_arrow/tracking_arrow = hud_used.add_screen_object(/atom/movable/screen/tracking_arrow, HUD_VASSAL_TRACKER, HUD_GROUP_STATIC, update_screen = TRUE) + + var/mob/living/master_body = master?.owner?.current + if(!QDELETED(master_body)) + tracking_arrow.update(owner.current, master_body) + +/datum/antagonist/vassal/proc/remove_hud_elements(mob/living/current_mob) + current_mob?.hud_used?.remove_screen_object(HUD_VASSAL_TRACKER) + +/datum/antagonist/vassal/proc/on_life(datum/source) + SIGNAL_HANDLER + var/mob/living/current = owner.current + if(QDELETED(current) || current.stat != CONSCIOUS) + return + var/mob/living/master_body = master.owner.current + if(QDELETED(master_body)) + return + time_away_from_master += (world.time - last_life_tick) + last_life_tick = world.time + if(CAN_THEY_SEE(master_body, current)) + time_away_from_master = 0 + current.add_mood_event("vassal", /datum/mood_event/vassal_happy) + else if(time_away_from_master >= 25 MINUTES) + current.add_mood_event("vassal", /datum/mood_event/vassal_away_severe) + current.set_jitter_if_lower(5 SECONDS) + else if(time_away_from_master >= 5 MINUTES) + current.add_mood_event("vassal", /datum/mood_event/vassal_away) + else + current.clear_mood_event("vassal") + +/datum/antagonist/vassal/proc/update_tracker() + SIGNAL_HANDLER + var/atom/movable/screen/tracking_arrow/tracking_arrow = owner.current?.hud_used?.screen_objects[HUD_VASSAL_TRACKER] + tracking_arrow?.update(owner.current, master?.owner?.current) + +/** + * Makes it so stuff our master's speech is more noticable by adding a chat effect to it. + */ +/datum/antagonist/vassal/proc/handle_hearing(datum/source, list/hearing_args) + SIGNAL_HANDLER + if(hearing_args[HEARING_SPEAKER] == master.owner?.current) + hearing_args[HEARING_SPANS] = list("vampire_master") + hearing_args[HEARING_SPANS] + +/datum/antagonist/vassal/proc/give_warning(atom/source, danger_level, vampire_warning_message, vassal_warning_message) + SIGNAL_HANDLER + + if(!owner?.current) + return + to_chat(owner, vassal_warning_message || vampire_warning_message, type = MESSAGE_TYPE_WARNING) + + switch(danger_level) + if(DANGER_LEVEL_FIRST_WARNING) + owner.current.playsound_local(null, 'modular_oculis/modules/vampires/sound/griffin_3.ogg', 50, TRUE) + if(DANGER_LEVEL_SECOND_WARNING) + owner.current.playsound_local(null, 'modular_oculis/modules/vampires/sound/griffin_5.ogg', 50, TRUE) + if(DANGER_LEVEL_THIRD_WARNING) + owner.current.playsound_local(null, 'sound/effects/alert.ogg', 75, TRUE) + if(DANGER_LEVEL_SOL_ROSE) + owner.current.playsound_local(null, 'sound/ambience/misc/ambimystery.ogg', 75, TRUE) + if(DANGER_LEVEL_SOL_ENDED) + owner.current.playsound_local(null, 'sound/music/antag/bloodcult/ghosty_wind.ogg', 90, TRUE) diff --git a/modular_oculis/modules/vampires/code/vassals/powers/distress.dm b/modular_oculis/modules/vampires/code/vassals/powers/distress.dm new file mode 100644 index 000000000000..2eba3cd5fa8c --- /dev/null +++ b/modular_oculis/modules/vampires/code/vassals/powers/distress.dm @@ -0,0 +1,22 @@ +/datum/action/cooldown/vampire/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 = "Use this Power anywhere and your Master will instantly be alerted to your location." + vampire_power_flags = NONE + vampire_check_flags = NONE + special_flags = NONE + vitaecost = 10 + cooldown_time = 10 SECONDS + +/datum/action/cooldown/vampire/distress/activate_power() + . = ..() + var/datum/antagonist/vassal/vassaldatum = IS_VASSAL(owner) + + owner.balloon_alert(owner, "you call out for your master!") + to_chat(vassaldatum.master.owner, span_userdanger("[owner.real_name], your loyal vassal, is desperately calling for aid at [get_area(owner)]!")) + + var/mob/living/living_owner = owner + if(living_owner.health > (living_owner.crit_threshold + 10)) + living_owner.adjust_brute_loss(10) + deactivate_power() diff --git a/modular_oculis/modules/vampires/code/vassals/powers/recuperate.dm b/modular_oculis/modules/vampires/code/vassals/powers/recuperate.dm new file mode 100644 index 000000000000..2abe273c6591 --- /dev/null +++ b/modular_oculis/modules/vampires/code/vassals/powers/recuperate.dm @@ -0,0 +1,79 @@ +/// Used by vassals +/datum/action/cooldown/vampire/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 = "Activating this Power will begin to heal your wounds.\n\ + You will heal Brute and Toxin damage at the cost of your Stamina and blood.\n\ + If you aren't a bloodless race, you will additionally heal Burn damage." + vampire_power_flags = BP_AM_TOGGLE + vampire_check_flags = BP_CANT_USE_WHILE_INCAPACITATED | BP_CANT_USE_WHILE_UNCONSCIOUS + special_flags = NONE + vitaecost = 1.5 + cooldown_time = 10 SECONDS + +/datum/action/cooldown/vampire/recuperate/can_use() + . = ..() + if(!.) + return FALSE + + if(owner.stat >= DEAD || INCAPACITATED_IGNORING(owner, INCAPABLE_RESTRAINTS)) + owner.balloon_alert(owner, "you are incapacitated...") + return FALSE + + var/mob/living/living_owner = owner + if(!HAS_TRAIT(owner, TRAIT_NOBLOOD) && living_owner.blood_volume <= BLOOD_VOLUME_OKAY) + owner.balloon_alert(owner, "not enough blood!") + return FALSE + +/datum/action/cooldown/vampire/recuperate/activate_power() + . = ..() + to_chat(owner, span_notice("Your muscles clench as your master's immortal blood mixes with your own, knitting your wounds.")) + owner.balloon_alert(owner, "recuperate turned on.") + +/datum/action/cooldown/vampire/recuperate/use_power() + . = ..() + if(!. || !currently_active) + return + + var/mob/living/carbon/carbon_owner = owner + if(!istype(carbon_owner)) + return + + var/needs_update = FALSE + carbon_owner.set_jitter_if_lower(10 SECONDS) + // carbon_owner.stamina?.adjust_to(-vitaecost * 1.1, 5) // can't stamcrit you. barely. + needs_update += carbon_owner.adjust_brute_loss(-2.5, updating_health = FALSE) + needs_update += carbon_owner.adjust_tox_loss(-2, updating_health = FALSE, forced = TRUE) + // Plasmamen won't lose blood, they don't have any, so they don't heal from Burn. + if(!HAS_TRAIT(carbon_owner, TRAIT_NOBLOOD)) + carbon_owner.adjust_blood_volume(-vitaecost) + needs_update += carbon_owner.adjust_fire_loss(-1.5, updating_health = FALSE) + + // Stop Bleeding + var/datum/wound/bloodiest_wound + for(var/datum/wound/iter_wound as anything in carbon_owner.all_wounds) + if(iter_wound.blood_flow && (!bloodiest_wound || (iter_wound.blood_flow > bloodiest_wound.blood_flow))) + bloodiest_wound = iter_wound + + bloodiest_wound?.adjust_blood_flow(-0.25) + + for(var/obj/item/bodypart/bodypart as anything in carbon_owner.bodyparts) + if(bodypart.generic_bleedstacks) + bodypart.adjustBleedStacks(-1, 0) + + if(needs_update) + carbon_owner.updatehealth() + +/datum/action/cooldown/vampire/recuperate/continue_active() + if(QDELETED(owner) || owner.stat == DEAD) + return FALSE + if(INCAPACITATED_IGNORING(owner, INCAPABLE_RESTRAINTS)) + owner.balloon_alert(owner, "too exhausted...") + return FALSE + return TRUE + +/datum/action/cooldown/vampire/recuperate/deactivate_power() + if(!QDELETED(owner)) + owner.balloon_alert(owner, "recuperate turned off.") + return ..() diff --git a/modular_oculis/modules/vampires/html/images/vampire.png b/modular_oculis/modules/vampires/html/images/vampire.png new file mode 100644 index 000000000000..e712971c75fa Binary files /dev/null and b/modular_oculis/modules/vampires/html/images/vampire.png differ diff --git a/modular_oculis/modules/vampires/icons/64x64.dmi b/modular_oculis/modules/vampires/icons/64x64.dmi new file mode 100644 index 000000000000..41a12a375c32 Binary files /dev/null and b/modular_oculis/modules/vampires/icons/64x64.dmi differ diff --git a/modular_oculis/modules/vampires/icons/actions_vampire.dmi b/modular_oculis/modules/vampires/icons/actions_vampire.dmi new file mode 100644 index 000000000000..847f16ba9f62 Binary files /dev/null and b/modular_oculis/modules/vampires/icons/actions_vampire.dmi differ diff --git a/modular_oculis/modules/vampires/icons/antag_hud.dmi b/modular_oculis/modules/vampires/icons/antag_hud.dmi new file mode 100644 index 000000000000..3ca59f46076e Binary files /dev/null and b/modular_oculis/modules/vampires/icons/antag_hud.dmi differ diff --git a/modular_oculis/modules/vampires/icons/arrow.dmi b/modular_oculis/modules/vampires/icons/arrow.dmi new file mode 100644 index 000000000000..400249e8d4d2 Binary files /dev/null and b/modular_oculis/modules/vampires/icons/arrow.dmi differ diff --git a/modular_oculis/modules/vampires/icons/bs_leftinhand.dmi b/modular_oculis/modules/vampires/icons/bs_leftinhand.dmi new file mode 100644 index 000000000000..a2f682fa8dfc Binary files /dev/null and b/modular_oculis/modules/vampires/icons/bs_leftinhand.dmi differ diff --git a/modular_oculis/modules/vampires/icons/bs_rightinhand.dmi b/modular_oculis/modules/vampires/icons/bs_rightinhand.dmi new file mode 100644 index 000000000000..075f73bc779b Binary files /dev/null and b/modular_oculis/modules/vampires/icons/bs_rightinhand.dmi differ diff --git a/modular_oculis/modules/vampires/icons/clan_icons.dmi b/modular_oculis/modules/vampires/icons/clan_icons.dmi new file mode 100644 index 000000000000..b9cc68f03402 Binary files /dev/null and b/modular_oculis/modules/vampires/icons/clan_icons.dmi differ diff --git a/modular_oculis/modules/vampires/icons/disciplines.dmi b/modular_oculis/modules/vampires/icons/disciplines.dmi new file mode 100644 index 000000000000..7becdd41ac2a Binary files /dev/null and b/modular_oculis/modules/vampires/icons/disciplines.dmi differ diff --git a/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_bloodboil.dmi b/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_bloodboil.dmi new file mode 100644 index 000000000000..10b8a51d9f09 Binary files /dev/null and b/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_bloodboil.dmi differ diff --git a/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_bloodbolt.dmi b/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_bloodbolt.dmi new file mode 100644 index 000000000000..d8920f7ab627 Binary files /dev/null and b/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_bloodbolt.dmi differ diff --git a/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_blooddrain.dmi b/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_blooddrain.dmi new file mode 100644 index 000000000000..3b0cb526f6e7 Binary files /dev/null and b/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_blooddrain.dmi differ diff --git a/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_command.dmi b/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_command.dmi new file mode 100644 index 000000000000..6f610a01492f Binary files /dev/null and b/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_command.dmi differ diff --git a/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_entrance.dmi b/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_entrance.dmi new file mode 100644 index 000000000000..5a5c6440f802 Binary files /dev/null and b/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_entrance.dmi differ diff --git a/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_feed.dmi b/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_feed.dmi new file mode 100644 index 000000000000..89714e00d81a Binary files /dev/null and b/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_feed.dmi differ diff --git a/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_lunge.dmi b/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_lunge.dmi new file mode 100644 index 000000000000..e1c1e2b20bff Binary files /dev/null and b/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_lunge.dmi differ diff --git a/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_mesmerize.dmi b/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_mesmerize.dmi new file mode 100644 index 000000000000..ba1631b41b84 Binary files /dev/null and b/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_mesmerize.dmi differ diff --git a/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_strength.dmi b/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_strength.dmi new file mode 100644 index 000000000000..1707e2c21ae5 Binary files /dev/null and b/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_strength.dmi differ diff --git a/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_summon.dmi b/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_summon.dmi new file mode 100644 index 000000000000..f01893652fd8 Binary files /dev/null and b/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_summon.dmi differ diff --git a/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_trespass.dmi b/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_trespass.dmi new file mode 100644 index 000000000000..562cd9e61236 Binary files /dev/null and b/modular_oculis/modules/vampires/icons/mouse_pointers/vampire_trespass.dmi differ diff --git a/modular_oculis/modules/vampires/icons/phobetor_tear.dmi b/modular_oculis/modules/vampires/icons/phobetor_tear.dmi new file mode 100644 index 000000000000..4086c31d6134 Binary files /dev/null and b/modular_oculis/modules/vampires/icons/phobetor_tear.dmi differ diff --git a/modular_oculis/modules/vampires/icons/screen_alert.dmi b/modular_oculis/modules/vampires/icons/screen_alert.dmi new file mode 100644 index 000000000000..d7f4b5f0d6cb Binary files /dev/null and b/modular_oculis/modules/vampires/icons/screen_alert.dmi differ diff --git a/modular_oculis/modules/vampires/icons/stakes.dmi b/modular_oculis/modules/vampires/icons/stakes.dmi new file mode 100644 index 000000000000..c8dfa6bf2da2 Binary files /dev/null and b/modular_oculis/modules/vampires/icons/stakes.dmi differ diff --git a/modular_oculis/modules/vampires/icons/vamp_obj.dmi b/modular_oculis/modules/vampires/icons/vamp_obj.dmi new file mode 100644 index 000000000000..89ee920fb77b Binary files /dev/null and b/modular_oculis/modules/vampires/icons/vamp_obj.dmi differ diff --git a/modular_oculis/modules/vampires/icons/vamp_obj_64.dmi b/modular_oculis/modules/vampires/icons/vamp_obj_64.dmi new file mode 100644 index 000000000000..d455ddcd92a2 Binary files /dev/null and b/modular_oculis/modules/vampires/icons/vamp_obj_64.dmi differ diff --git a/modular_oculis/modules/vampires/icons/vampiric.dmi b/modular_oculis/modules/vampires/icons/vampiric.dmi new file mode 100644 index 000000000000..6b2be9eed78e Binary files /dev/null and b/modular_oculis/modules/vampires/icons/vampiric.dmi differ diff --git a/modular_oculis/modules/vampires/icons/worn.dmi b/modular_oculis/modules/vampires/icons/worn.dmi new file mode 100644 index 000000000000..063aa5e70600 Binary files /dev/null and b/modular_oculis/modules/vampires/icons/worn.dmi differ diff --git a/modular_oculis/modules/vampires/readme.md b/modular_oculis/modules/vampires/readme.md new file mode 100644 index 000000000000..2fec670b8427 --- /dev/null +++ b/modular_oculis/modules/vampires/readme.md @@ -0,0 +1,86 @@ +https://github.com/Monkestation/OculisStation/pull/48 + +## Vampire Antagonists + +Module ID: VAMPIRES + +### Description: + +Adds a new antagonist, Vampires. + +\[insert infodump here later\] + +### TG Proc/File Changes: + +- `code/game/objects/items/devices/scanners/sequence_scanner.dm` + - `/obj/item/sequence_scanner/interact_with_atom` +- `code/modules/admin/sql_ban_system.dm` + - `/datum/admins/proc/ban_panel` +- `code/modules/antagonists/brainwashing/brainwashing.dm` + - `/proc/brainwash` + - new proc: `/proc/unbrainwash` +- `code/modules/antagonists/heretic/influences.dm` + - `/obj/effect/visible_heretic_influence/examine` +- `code/modules/mob/living/carbon/human/species_types/jellypeople.dm` + - `/datum/species/jelly/proc/Cannibalize_Body` +- `code/modules/mob/living/carbon/carbon_defense.dm` + - `/mob/living/carbon/proc/help_shake_act` +- `code/modules/mob/living/simple_animal/animal_defense.dm` + - `/mob/living/simple_animal/attack_hand` + - `/mob/living/simple_animal/attack_paw` + +### Modular Overrides: + +- `modular_oculis/master_files/code/datums/elements/art.dm` + - `/datum/element/art/proc/apply_moodlet` +- `modular_oculis/master_files/code/game/machinery/newscaster/newscaster_data.dm` + - `/datum/feed_network/New()` +- `modular_oculis/master_files/code/modules/reagents/reagent_containers/blood_pack.dm` + - `/obj/item/reagent_containers/blood/attack` +- `modular_oculis/master_files/code/modules/jobs/job_types/curator.dm` + - `/datum/outfit/job/curator/pre_equip()` + +### Defines: + +- `code/__DEFINES/~~oculis_defines/dcs/signals/signals_mob/signals_mob_living.dm` + - `COMSIG_LIVING_PET_ANIMAL` + - `COMSIG_LIVING_HUG_CARBON` + - `COMSIG_LIVING_APPRAISE_ART` +- `code/__DEFINES/~~oculis_defines/traits/declarations.dm` + - `TRAIT_FAKEGENES` + - `TRAIT_VAMPIRE_ALIGNED` + - `TRAIT_SLIME_NO_CANNIBALIZE` +- `code/__DEFINES/~~oculis_defines/antagonists.dm` + - `IS_VAMPIRE` + - `IS_VASSAL` +- `code/__DEFINES/~~oculis_defines/crafting.dm` + - `CAT_VAMPIRE` +- `code/__DEFINES/~~oculis_defines/factions.dm` + - `FACTION_VAMPIRE` +- `code/__DEFINES/~~oculis_defines/language.dm` + - `LANGUAGE_VAMPIRE` + - `LANGUAGE_VASSAL` +- `code/__DEFINES/~~oculis_defines/role_preferences.dm` + - `ROLE_VAMPIRE` + - `ROLE_VAMPIRIC_ACCIDENT` + - `ROLE_VAMPIRE_BREAKOUT` + - `ROLE_VASSAL` +- `code/__DEFINES/~~oculis_defines/span.dm` + - `span_awe` +- `code/__DEFINES/~~oculis_defines/vampires.dm` + - Many vampire-related defines, too many to list + +### Included files that are not contained in this module: + +- `code/__HELPERS/~~oculis_helpers/view.dm` +- `modular_oculis/master_files/code/code/modules/client/client_colour.dm` +- `modular_oculis/master_files/code/game/objects/structures/crates_lockers/crates.dm` + +### Credits: + +- Absolucy +- TsunamiAnt +- TheCarnalest +- mrmanlikesbt +- Laikodaemon +- prolly many others I forgot diff --git a/modular_oculis/modules/vampires/sound/VampireAlert.ogg b/modular_oculis/modules/vampires/sound/VampireAlert.ogg new file mode 100644 index 000000000000..5f3817e3eb97 Binary files /dev/null and b/modular_oculis/modules/vampires/sound/VampireAlert.ogg differ diff --git a/modular_oculis/modules/vampires/sound/auspex.ogg b/modular_oculis/modules/vampires/sound/auspex.ogg new file mode 100644 index 000000000000..beb0a28aee1b Binary files /dev/null and b/modular_oculis/modules/vampires/sound/auspex.ogg differ diff --git a/modular_oculis/modules/vampires/sound/awo1.ogg b/modular_oculis/modules/vampires/sound/awo1.ogg new file mode 100644 index 000000000000..6e0f79291e7b Binary files /dev/null and b/modular_oculis/modules/vampires/sound/awo1.ogg differ diff --git a/modular_oculis/modules/vampires/sound/bloodbolt.ogg b/modular_oculis/modules/vampires/sound/bloodbolt.ogg new file mode 100644 index 000000000000..de1bd9c3a416 Binary files /dev/null and b/modular_oculis/modules/vampires/sound/bloodbolt.ogg differ diff --git a/modular_oculis/modules/vampires/sound/bloodbolt_fire.ogg b/modular_oculis/modules/vampires/sound/bloodbolt_fire.ogg new file mode 100644 index 000000000000..57647d6872b0 Binary files /dev/null and b/modular_oculis/modules/vampires/sound/bloodbolt_fire.ogg differ diff --git a/modular_oculis/modules/vampires/sound/bloodhealing.ogg b/modular_oculis/modules/vampires/sound/bloodhealing.ogg new file mode 100644 index 000000000000..c4c6c253e471 Binary files /dev/null and b/modular_oculis/modules/vampires/sound/bloodhealing.ogg differ diff --git a/modular_oculis/modules/vampires/sound/bloodneed.ogg b/modular_oculis/modules/vampires/sound/bloodneed.ogg new file mode 100644 index 000000000000..4e613452a4d5 Binary files /dev/null and b/modular_oculis/modules/vampires/sound/bloodneed.ogg differ diff --git a/modular_oculis/modules/vampires/sound/burning_death.ogg b/modular_oculis/modules/vampires/sound/burning_death.ogg new file mode 100644 index 000000000000..59884b851dcf Binary files /dev/null and b/modular_oculis/modules/vampires/sound/burning_death.ogg differ diff --git a/modular_oculis/modules/vampires/sound/coffin_close.ogg b/modular_oculis/modules/vampires/sound/coffin_close.ogg new file mode 100644 index 000000000000..1b73a0fa1bcb Binary files /dev/null and b/modular_oculis/modules/vampires/sound/coffin_close.ogg differ diff --git a/modular_oculis/modules/vampires/sound/coffin_open.ogg b/modular_oculis/modules/vampires/sound/coffin_open.ogg new file mode 100644 index 000000000000..472c344d85a1 Binary files /dev/null and b/modular_oculis/modules/vampires/sound/coffin_open.ogg differ diff --git a/modular_oculis/modules/vampires/sound/door_locked.ogg b/modular_oculis/modules/vampires/sound/door_locked.ogg new file mode 100644 index 000000000000..60d3be189902 Binary files /dev/null and b/modular_oculis/modules/vampires/sound/door_locked.ogg differ diff --git a/modular_oculis/modules/vampires/sound/drinkblood1.ogg b/modular_oculis/modules/vampires/sound/drinkblood1.ogg new file mode 100644 index 000000000000..a987252afe7c Binary files /dev/null and b/modular_oculis/modules/vampires/sound/drinkblood1.ogg differ diff --git a/modular_oculis/modules/vampires/sound/fortitude_off.ogg b/modular_oculis/modules/vampires/sound/fortitude_off.ogg new file mode 100644 index 000000000000..ef191ff81304 Binary files /dev/null and b/modular_oculis/modules/vampires/sound/fortitude_off.ogg differ diff --git a/modular_oculis/modules/vampires/sound/fortitude_on.ogg b/modular_oculis/modules/vampires/sound/fortitude_on.ogg new file mode 100644 index 000000000000..92e3fc5056b3 Binary files /dev/null and b/modular_oculis/modules/vampires/sound/fortitude_on.ogg differ diff --git a/modular_oculis/modules/vampires/sound/griffin_1.ogg b/modular_oculis/modules/vampires/sound/griffin_1.ogg new file mode 100644 index 000000000000..f37e3fdd2232 Binary files /dev/null and b/modular_oculis/modules/vampires/sound/griffin_1.ogg differ diff --git a/modular_oculis/modules/vampires/sound/griffin_10.ogg b/modular_oculis/modules/vampires/sound/griffin_10.ogg new file mode 100644 index 000000000000..2239b36e1f9b Binary files /dev/null and b/modular_oculis/modules/vampires/sound/griffin_10.ogg differ diff --git a/modular_oculis/modules/vampires/sound/griffin_2.ogg b/modular_oculis/modules/vampires/sound/griffin_2.ogg new file mode 100644 index 000000000000..b35090f1bd3f Binary files /dev/null and b/modular_oculis/modules/vampires/sound/griffin_2.ogg differ diff --git a/modular_oculis/modules/vampires/sound/griffin_3.ogg b/modular_oculis/modules/vampires/sound/griffin_3.ogg new file mode 100644 index 000000000000..d354ba48d94d Binary files /dev/null and b/modular_oculis/modules/vampires/sound/griffin_3.ogg differ diff --git a/modular_oculis/modules/vampires/sound/griffin_4.ogg b/modular_oculis/modules/vampires/sound/griffin_4.ogg new file mode 100644 index 000000000000..9af1b2987967 Binary files /dev/null and b/modular_oculis/modules/vampires/sound/griffin_4.ogg differ diff --git a/modular_oculis/modules/vampires/sound/griffin_5.ogg b/modular_oculis/modules/vampires/sound/griffin_5.ogg new file mode 100644 index 000000000000..1df4bdffa058 Binary files /dev/null and b/modular_oculis/modules/vampires/sound/griffin_5.ogg differ diff --git a/modular_oculis/modules/vampires/sound/griffin_6.ogg b/modular_oculis/modules/vampires/sound/griffin_6.ogg new file mode 100644 index 000000000000..bd2aee5c66cc Binary files /dev/null and b/modular_oculis/modules/vampires/sound/griffin_6.ogg differ diff --git a/modular_oculis/modules/vampires/sound/griffin_7.ogg b/modular_oculis/modules/vampires/sound/griffin_7.ogg new file mode 100644 index 000000000000..7f4b38d08910 Binary files /dev/null and b/modular_oculis/modules/vampires/sound/griffin_7.ogg differ diff --git a/modular_oculis/modules/vampires/sound/griffin_8.ogg b/modular_oculis/modules/vampires/sound/griffin_8.ogg new file mode 100644 index 000000000000..68cc01f7a5ed Binary files /dev/null and b/modular_oculis/modules/vampires/sound/griffin_8.ogg differ diff --git a/modular_oculis/modules/vampires/sound/griffin_9.ogg b/modular_oculis/modules/vampires/sound/griffin_9.ogg new file mode 100644 index 000000000000..b3229adc2aa7 Binary files /dev/null and b/modular_oculis/modules/vampires/sound/griffin_9.ogg differ diff --git a/modular_oculis/modules/vampires/sound/growl.ogg b/modular_oculis/modules/vampires/sound/growl.ogg new file mode 100644 index 000000000000..9a72f8dc2d94 Binary files /dev/null and b/modular_oculis/modules/vampires/sound/growl.ogg differ diff --git a/modular_oculis/modules/vampires/sound/humanity_gain.ogg b/modular_oculis/modules/vampires/sound/humanity_gain.ogg new file mode 100644 index 000000000000..08a22d05b3f4 Binary files /dev/null and b/modular_oculis/modules/vampires/sound/humanity_gain.ogg differ diff --git a/modular_oculis/modules/vampires/sound/humanity_loss.ogg b/modular_oculis/modules/vampires/sound/humanity_loss.ogg new file mode 100644 index 000000000000..86d6e47157d8 Binary files /dev/null and b/modular_oculis/modules/vampires/sound/humanity_loss.ogg differ diff --git a/modular_oculis/modules/vampires/sound/lunge_warn.ogg b/modular_oculis/modules/vampires/sound/lunge_warn.ogg new file mode 100644 index 000000000000..46ca901b0be9 Binary files /dev/null and b/modular_oculis/modules/vampires/sound/lunge_warn.ogg differ diff --git a/modular_oculis/modules/vampires/sound/masquerade_violation.ogg b/modular_oculis/modules/vampires/sound/masquerade_violation.ogg new file mode 100644 index 000000000000..c8139a9560cc Binary files /dev/null and b/modular_oculis/modules/vampires/sound/masquerade_violation.ogg differ diff --git a/modular_oculis/modules/vampires/sound/mesmerize.ogg b/modular_oculis/modules/vampires/sound/mesmerize.ogg new file mode 100644 index 000000000000..bd46b8500f07 Binary files /dev/null and b/modular_oculis/modules/vampires/sound/mesmerize.ogg differ diff --git a/modular_oculis/modules/vampires/sound/prince.ogg b/modular_oculis/modules/vampires/sound/prince.ogg new file mode 100644 index 000000000000..f914c46297e8 Binary files /dev/null and b/modular_oculis/modules/vampires/sound/prince.ogg differ diff --git a/modular_oculis/modules/vampires/sound/rage_decrease.ogg b/modular_oculis/modules/vampires/sound/rage_decrease.ogg new file mode 100644 index 000000000000..fa7114bcfc90 Binary files /dev/null and b/modular_oculis/modules/vampires/sound/rage_decrease.ogg differ diff --git a/modular_oculis/modules/vampires/sound/rage_increase.ogg b/modular_oculis/modules/vampires/sound/rage_increase.ogg new file mode 100644 index 000000000000..b7d445783c1c Binary files /dev/null and b/modular_oculis/modules/vampires/sound/rage_increase.ogg differ diff --git a/modular_oculis/modules/vampires/sound/scourge_offer.ogg b/modular_oculis/modules/vampires/sound/scourge_offer.ogg new file mode 100644 index 000000000000..fc52f29eb7bd Binary files /dev/null and b/modular_oculis/modules/vampires/sound/scourge_offer.ogg differ diff --git a/modular_oculis/modules/vampires/sound/scourge_recruit.ogg b/modular_oculis/modules/vampires/sound/scourge_recruit.ogg new file mode 100644 index 000000000000..87e9e3dc9a5a Binary files /dev/null and b/modular_oculis/modules/vampires/sound/scourge_recruit.ogg differ diff --git a/modular_oculis/modules/vampires/sound/wolf_ask.ogg b/modular_oculis/modules/vampires/sound/wolf_ask.ogg new file mode 100644 index 000000000000..cc35612aaaad Binary files /dev/null and b/modular_oculis/modules/vampires/sound/wolf_ask.ogg differ diff --git a/modular_oculis/modules/vampires/sound/wolf_speak.ogg b/modular_oculis/modules/vampires/sound/wolf_speak.ogg new file mode 100644 index 000000000000..300124cd0d8b Binary files /dev/null and b/modular_oculis/modules/vampires/sound/wolf_speak.ogg differ diff --git a/strings/oculis/malkavian_revelations.json b/strings/oculis/malkavian_revelations.json new file mode 100644 index 000000000000..fc5e4c7a99e5 --- /dev/null +++ b/strings/oculis/malkavian_revelations.json @@ -0,0 +1,117 @@ +{ + "revelations": [ + "#There could have an entirely separate dimension only visible through pools of Blood, and we will never see it.", + "#Explosions happen often, I wonder if one will strike me one day. I wonder if I'll survive.", + "#The Captain will fall eventually, everything is only a matter of time.", + "#There's always something there to be enlightened from. Something to learn. Something to teach.", + "#Why have we been abandoned in this universe... When will we be taken away to the rest of the living?", + "#When will the dreams stop following me? Why have they picked me, of all people?", + "#The one listening to the voices in his head is called foolish from those unaware, but does that make him insane?", + "#Oh dear... perhaps I've taken my life a little too far today. I wonder what's next.", + "#Sometimes I feel like I am the last prophet to ever exist... Maybe I am.", + "#I wonder what made me this way. Is it my Malkavian blood? My hatred for those around me?", + "#Maybe I should start to think about what consequences my actions lead me to. Or maybe it's best not to think about it.", + "#If I focus on my goals, rather than what I want to do, am I truly happy?", + "#Maybe... in an alternate universe... I could be part of another family. One that cares more about me...", + ",LI wonder what other people think of me... Possibly terribly.", + "#...What would happen if I Vassalized a Clown?", + "#Why can't we just walk? Does anyone walk anymore? Why do we run? What rush are we in?", + "#Medbay is overworking, I wonder why they are always so shortstaffed.", + "#Why do we take a pod instead of the shuttle? Where's the fun in that?", + ",LHuh...", + "#I was so close to a new revelation, but I lost my train of thought for a moment there.", + "#What happens once all the organics die? Would just the unorganics remain?", + "#Hmmm... What would happen if I killed a Command member...", + "#Supermatters unnaturally look like candy... I wonder what would happen if I licked it...", + "#Everyone thinks of me as a freak, at least I'm not a creep, then they'd be sorry.", + ",LDon't forget to use the Mentor tab to ask for help!", + "#I wonder what they put on the bikes to cost a million credits...", + "#I wonder if my upstream would accept me for who I am.", + "#Is it possible... for the undead to get a heart attack? Nevermind, strange question.", + ",LWhat would happen if a Vampire got their hands on a Power Miner?", + "#There are Aliens, they exist. It isn't a conspiracy. The real question is when they will attack us.", + "#Is Brain damage real, or is it just our brains adapting to reality?", + "#How do we all understand eachother when we speak over eachother on the radio?", + "#Huds are broken again, it seems...", + "#Never make a deal with the devil... worst mistake of my life.", + "#Does plasma still affect the minds of people who can't get poisoned?", + "#Who thought sending a research station into a contested area was a good idea? Unless it's just sick and twisted humor... like a game!", + "#How well would a Cryogenic Blob deal against my power?", + "#It's possible to learn how to bloodcrawl...", + "#Changelings are the purest form of a Human... if Humans were the most unpure thing.", + "#I would like to take a stroll around the station, floating through the space around us... must feel nice.", + "#Who thought of the idea of Health Analyzers? Like, something that instantly knows everything wrong with you?", + "#How much of our soul does Nanotrasen REALLY own... they certainly don't own enough if they always have traitors among their crew...", + "#I wonder how the Devil is doing today... haven't seen them in a long time.", + "#We straight gassing cutting straight to the bricks ha ha.", + "#This shit ain't nothing to me man.", + "#I had to do it to them snipe.", + "#I'm not loyal to anybody I'm a demon.", + "#I have no loyalty for anyone never did never will.", + "#Shorty chose to be with a demon sounds like her problem to, me ha ha!", + "#Moving like Dracula we get it back in blood.", + "#You see it I really did this I'm really him.", + "#Flipped a whole brick into an empire stop playing with me.", + "#Smoking fentanyl-laced blood; I see God.", + "#Yeah we getting that Pirate Bay alien shish kabab cordycep money.", + "#I just popped a whole garbanzo bean, fuck you mean?", + "#I smoke real Emrānī rapscallion ghost nuggets.", + "I'm him! I been him!! I will continue to be him!!!", + "#They thought they could stop the demon, I'm back!", + "#The zaza got me speaking Esperanto.", + "#You can't trust me, I don't even trust myself. I don't even know who I am anymore, I'm getting too much money.", + "#Get the Captain on the holocall now! I fronted him a brick, I need my money!", + "#We smokin' Symbiotes.", + "#Smokin' that Whoopi Goldberg south Egyptian kindred deluxe Mega Millions scratcher skunk bubba kush.", + "#We smokin' Sequoia banshee boogers.", + "#They must have amnesia, they forgot that I'm him.", + "#Motherfucker look like a Resident Evil 5 campaign extra after we was done with him.", + "#Ops wanted some initiative, blew up their entire quadrant, I'm moving like Cuban Pete.", + "#I was flipping bricks for Mansa Musa before y'all even became a type 1 civilization.", + "#I have seen the Magna Carta. I've seen the Eye of Hora.", + "#You think I care about this shit? Ask me if I care about this shit, 'cause I don't give a shit! If I had a credit for every time they said I gave a shit, I'd be broke 'Cause I don't give a shit!", + "#This .357 got me moving like an invasive species.", + "#I got Midas touch shitter.", + "#I'm at the vault boutta withdraw all of it.", + "#That Fentanyl gave me Vitruvian Man flexibility. Got me in a state of rigor mortis.", + "#Caught a broke boy trying to come up on my Amazon package, so I skinned his ass alive.", + "#We smokin' Serge Ibaka spinal fluid infused quick-release percs.", + "#They needеd a stealth soldier, so I put my hands on the hibachi hot plate at Benihana, and burned my fucking finger prints off. They will not find me...", + "#Konichiwa you little jit.", + "#Snortin' some premium Matisyahu got me fightin' for my life.", + "#The Cuban link will turn the diamond tester into a pipe bomb.", + "#Stechkin shivered his timbers.", + "#I'm smoking Mesopotamian, Stanley Cup triple-award-winning, soul-bleeder, J.D. Power Associates, dingleberry zaza.", + "#We smoking that IBM Quantum Computer.", + "#My diamonds come from the most horrific situations possible.", + "#Fuck it, I ate the opp.", + "#Fuck it, I'm coming for every enzyme.", + "#I'll fucking kill you!.", + "#The first time I smoked runts, I coughed so fucking hard, I started passing kidney stones, then toolboxed myself in front of the gang!", + "#Hold on, lemme get some sip.", + "#The Codex should be treated like a Nuclear Authentication Disk, it is what guards this realm from the one below, afterall...", + "#No one knows how to read anymore, no matter how 'in your face' you put things, they'll never get it.", + "#150, 149, 148... 147, 146, 145, 144... What number was I at, again?", + "#No matter what we do, the feeling of pain will be inevitable.", + "#It seems Revolutionaries might take over the station today", + "#Huh, Nuclear Operatives lost in space. That's new.", + ",LWhere did I go wrong in my mortal life to end up here...", + "#The one that knows the Monster's tricks is sure to arrive. Only time will tell when.", + "#What are we even doing on such a Station? Don't we all know this will end in disaster?", + "#I can't think properly...", + "#I wonder what the Ancient Greek philosophers would say if they were alive today.", + "#I could go for some food just about now...", + "#Some coffee would be life-changing right about now...", + "#If only everyone saw the world in the same way I have", + "#What did Humanity do to deserve my creation?", + "#If we were all born for a reason, mine is completely idiotic.", + "#If there really is a God, why would they allow me to exist?", + "#...I think I lost track of something... I can't remember what...", + "#Who is humanity to decide who someone is? Why should they meddle in my affairs?", + "#The person everyone tries to silence, is the one people will miss the most", + "#It's hard to tell if people just don't understand my level of philosophy, or if they just play dumb to get reactions out of me.", + ";This is your fault.", + ",LWhy do we always infight, what's wrong with a little teamwork, it gets us further.", + "#What's a hacked autodrobe but a machine forced to show itself to you. Is it moral?" + ] +} diff --git a/tgstation.dme b/tgstation.dme index 7d3e1be69eaf..b7ad1500356c 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -549,19 +549,28 @@ #include "code\__DEFINES\~~iris_defines\traits\declarations.dm" #include "code\__DEFINES\~~oculis_defines\_floxy.dm" #include "code\__DEFINES\~~oculis_defines\admin.dm" +#include "code\__DEFINES\~~oculis_defines\antagonists.dm" #include "code\__DEFINES\~~oculis_defines\astar.dm" #include "code\__DEFINES\~~oculis_defines\colors.dm" #include "code\__DEFINES\~~oculis_defines\combat.dm" +#include "code\__DEFINES\~~oculis_defines\crafting.dm" +#include "code\__DEFINES\~~oculis_defines\do_afters.dm" +#include "code\__DEFINES\~~oculis_defines\factions.dm" +#include "code\__DEFINES\~~oculis_defines\hud.dm" #include "code\__DEFINES\~~oculis_defines\is_helpers.dm" +#include "code\__DEFINES\~~oculis_defines\language.dm" #include "code\__DEFINES\~~oculis_defines\loadout.dm" #include "code\__DEFINES\~~oculis_defines\mobs.dm" #include "code\__DEFINES\~~oculis_defines\plexora.dm" #include "code\__DEFINES\~~oculis_defines\quirks.dm" #include "code\__DEFINES\~~oculis_defines\redaction.dm" #include "code\__DEFINES\~~oculis_defines\role_preferences.dm" +#include "code\__DEFINES\~~oculis_defines\span.dm" +#include "code\__DEFINES\~~oculis_defines\vampires.dm" #include "code\__DEFINES\~~oculis_defines\_globalvars\lists\objects.dm" #include "code\__DEFINES\~~oculis_defines\dcs\signals\signals_datum.dm" #include "code\__DEFINES\~~oculis_defines\dcs\signals\signals_mind.dm" +#include "code\__DEFINES\~~oculis_defines\dcs\signals\signals_mob\signals_mob_living.dm" #include "code\__DEFINES\~~oculis_defines\traits\declarations.dm" #include "code\__HELPERS\_auxtools_api.dm" #include "code\__HELPERS\_dreamluau.dm" @@ -696,6 +705,7 @@ #include "code\__HELPERS\~~oculis_helpers\mapping.dm" #include "code\__HELPERS\~~oculis_helpers\text.dm" #include "code\__HELPERS\~~oculis_helpers\time.dm" +#include "code\__HELPERS\~~oculis_helpers\view.dm" #include "code\_globalvars\_regexes.dm" #include "code\_globalvars\admin.dm" #include "code\_globalvars\arcade.dm" @@ -7098,9 +7108,7 @@ #include "interface\fonts\vcr_osd_mono.dm" #include "modular_iris\bay_ports\objects\toys.dm" #include "modular_iris\bubber_ports\code\__DEFINES\quirks.dm" -#include "modular_iris\bubber_ports\code\bloodsuckers\bloodsucker_defines.dm" -#include "modular_iris\bubber_ports\code\bloodsuckers\hud.dm" -#include "modular_iris\bubber_ports\code\controllers\subsystem\processing\sol_subsystem.dm" +#include "modular_iris\bubber_ports\code\controllers\subsystem\processing\sol.dm" #include "modular_iris\bubber_ports\code\datum\components\weatherannouncer.dm" #include "modular_iris\bubber_ports\code\datums\quirks\negative_quirks\sol_weakness.dm" #include "modular_iris\bubber_ports\code\game\machinery\crew_monitor.dm" @@ -10031,11 +10039,14 @@ #include "modular_nova\modules\xenomorph\queen.dm" #include "modular_oculis\master_files\code\__HELPERS\files.dm" #include "modular_oculis\master_files\code\_globalvars\lists\quirks.dm" +#include "modular_oculis\master_files\code\code\modules\client\client_colour.dm" #include "modular_oculis\master_files\code\controllers\configuration\entries\game_options.dm" #include "modular_oculis\master_files\code\controllers\subsystem\persistence\_persistence.dm" #include "modular_oculis\master_files\code\datums\http.dm" +#include "modular_oculis\master_files\code\datums\elements\art.dm" #include "modular_oculis\master_files\code\game\atoms_movable.dm" #include "modular_oculis\master_files\code\game\area\areas\mining.dm" +#include "modular_oculis\master_files\code\game\objects\structures\crates_lockers\crates.dm" #include "modular_oculis\master_files\code\game\turfs\turf.dm" #include "modular_oculis\master_files\code\game\turfs\open\openspace.dm" #include "modular_oculis\master_files\code\modules\admin\verbs\getlogs.dm" @@ -10056,6 +10067,7 @@ #include "modular_oculis\master_files\code\modules\discord\toggle_notify.dm" #include "modular_oculis\master_files\code\modules\error_handler\error_viewer.dm" #include "modular_oculis\master_files\code\modules\events\portal_storm.dm" +#include "modular_oculis\master_files\code\modules\jobs\job_types\curator.dm" #include "modular_oculis\master_files\code\modules\logging\log_category.dm" #include "modular_oculis\master_files\code\modules\logging\log_holder.dm" #include "modular_oculis\master_files\code\modules\mentor\mentorsay.dm" @@ -10065,6 +10077,7 @@ #include "modular_oculis\master_files\code\modules\projectiles\gun.dm" #include "modular_oculis\master_files\code\modules\projectiles\projectile_speed.dm" #include "modular_oculis\master_files\code\modules\projectiles\boxes_magazines\ammo_boxes.dm" +#include "modular_oculis\master_files\code\modules\reagents\reagent_containers\blood_pack.dm" #include "modular_oculis\master_files\modular_nova\master_files\code\controllers\subsystem\dynamic_rulesets_midround.dm" #include "modular_oculis\modules\abel\code\id_cards.dm" #include "modular_oculis\modules\abel\code\modsuit.dm" @@ -10168,5 +10181,78 @@ #include "modular_oculis\modules\unique_lizards\code\client_colour.dm" #include "modular_oculis\modules\unique_lizards\code\lizardpeople.dm" #include "modular_oculis\modules\unique_lizards\code\lizardpeople_organs.dm" +#include "modular_oculis\modules\vampires\code\conversion_vampire.dm" +#include "modular_oculis\modules\vampires\code\datum_vampire.dm" +#include "modular_oculis\modules\vampires\code\dynamic_vampire.dm" +#include "modular_oculis\modules\vampires\code\frenzy_vampire.dm" +#include "modular_oculis\modules\vampires\code\hud_vampire.dm" +#include "modular_oculis\modules\vampires\code\language_vampire.dm" +#include "modular_oculis\modules\vampires\code\life_vampire.dm" +#include "modular_oculis\modules\vampires\code\misc_procs_vampire.dm" +#include "modular_oculis\modules\vampires\code\moodlets_vampire.dm" +#include "modular_oculis\modules\vampires\code\names_vampire.dm" +#include "modular_oculis\modules\vampires\code\objectives_vampire.dm" +#include "modular_oculis\modules\vampires\code\phobetor.dm" +#include "modular_oculis\modules\vampires\code\slime_vampire.dm" +#include "modular_oculis\modules\vampires\code\torpor_vampire.dm" +#include "modular_oculis\modules\vampires\code\tracking_vampire.dm" +#include "modular_oculis\modules\vampires\code\clans\_clan.dm" +#include "modular_oculis\modules\vampires\code\clans\assignclan.dm" +#include "modular_oculis\modules\vampires\code\clans\brujah.dm" +#include "modular_oculis\modules\vampires\code\clans\debug.dm" +#include "modular_oculis\modules\vampires\code\clans\flavortext_clans.dm" +#include "modular_oculis\modules\vampires\code\clans\malkavian.dm" +#include "modular_oculis\modules\vampires\code\clans\toreador.dm" +#include "modular_oculis\modules\vampires\code\clans\tremere.dm" +#include "modular_oculis\modules\vampires\code\clans\ventrue.dm" +#include "modular_oculis\modules\vampires\code\controllers\leveling_vampire.dm" +#include "modular_oculis\modules\vampires\code\controllers\society.dm" +#include "modular_oculis\modules\vampires\code\crafting\crafting_stakes.dm" +#include "modular_oculis\modules\vampires\code\crafting\crafting_vampire.dm" +#include "modular_oculis\modules\vampires\code\objects\_vampire_object.dm" +#include "modular_oculis\modules\vampires\code\objects\archive_of_the_kindred.dm" +#include "modular_oculis\modules\vampires\code\objects\blood_throne.dm" +#include "modular_oculis\modules\vampires\code\objects\candelabrum.dm" +#include "modular_oculis\modules\vampires\code\objects\coffin_variants.dm" +#include "modular_oculis\modules\vampires\code\objects\stakes.dm" +#include "modular_oculis\modules\vampires\code\objects\vassal_rack.dm" +#include "modular_oculis\modules\vampires\code\powers\_power.dm" +#include "modular_oculis\modules\vampires\code\powers\_targeted.dm" +#include "modular_oculis\modules\vampires\code\powers\disciplines.dm" +#include "modular_oculis\modules\vampires\code\powers\feed.dm" +#include "modular_oculis\modules\vampires\code\powers\feign_life.dm" +#include "modular_oculis\modules\vampires\code\powers\gohome.dm" +#include "modular_oculis\modules\vampires\code\powers\levelspells.dm" +#include "modular_oculis\modules\vampires\code\powers\auspex\astral_project.dm" +#include "modular_oculis\modules\vampires\code\powers\auspex\auspex.dm" +#include "modular_oculis\modules\vampires\code\powers\celerity\celerity.dm" +#include "modular_oculis\modules\vampires\code\powers\celerity\haste.dm" +#include "modular_oculis\modules\vampires\code\powers\celerity\quickness.dm" +#include "modular_oculis\modules\vampires\code\powers\dominate\command.dm" +#include "modular_oculis\modules\vampires\code\powers\dominate\dominate.dm" +#include "modular_oculis\modules\vampires\code\powers\dominate\mesmerize.dm" +#include "modular_oculis\modules\vampires\code\powers\dominate\voice_of_domination.dm" +#include "modular_oculis\modules\vampires\code\powers\fortitude\fortitude.dm" +#include "modular_oculis\modules\vampires\code\powers\obfuscate\cloak.dm" +#include "modular_oculis\modules\vampires\code\powers\obfuscate\obfuscate.dm" +#include "modular_oculis\modules\vampires\code\powers\obfuscate\trespass.dm" +#include "modular_oculis\modules\vampires\code\powers\obfuscate\veil.dm" +#include "modular_oculis\modules\vampires\code\powers\potence\brash.dm" +#include "modular_oculis\modules\vampires\code\powers\potence\brawn.dm" +#include "modular_oculis\modules\vampires\code\powers\potence\lunge.dm" +#include "modular_oculis\modules\vampires\code\powers\potence\potence.dm" +#include "modular_oculis\modules\vampires\code\powers\presence\awe.dm" +#include "modular_oculis\modules\vampires\code\powers\presence\entrance.dm" +#include "modular_oculis\modules\vampires\code\powers\presence\force_of_personality.dm" +#include "modular_oculis\modules\vampires\code\powers\presence\presence.dm" +#include "modular_oculis\modules\vampires\code\powers\presence\summon.dm" +#include "modular_oculis\modules\vampires\code\powers\thaumaturgy\bloodboil.dm" +#include "modular_oculis\modules\vampires\code\powers\thaumaturgy\bloodbolt.dm" +#include "modular_oculis\modules\vampires\code\powers\thaumaturgy\blooddrain.dm" +#include "modular_oculis\modules\vampires\code\powers\thaumaturgy\bloodshield.dm" +#include "modular_oculis\modules\vampires\code\powers\thaumaturgy\thaumaturgy.dm" +#include "modular_oculis\modules\vampires\code\vassals\datum_vassal.dm" +#include "modular_oculis\modules\vampires\code\vassals\powers\distress.dm" +#include "modular_oculis\modules\vampires\code\vassals\powers\recuperate.dm" #include "modular_oculis\modules\wall_pinning\code\wall_pin.dm" // END_INCLUDE diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss index 037637e4af2b..2ac5a315575c 100644 --- a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss +++ b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss @@ -1529,3 +1529,38 @@ $border-width-px: $border-width * 1px; font-style: italic; } // NOVA EDIT ADDITION END + +// OCULIS EDIT ADDITION START - VAMPIRES +.awe { + color: hsl(227, 87.7%, 68%); + animation: awe_glow 2s ease-in-out infinite; +} + +@keyframes awe_glow { + 0%, + 100% { + text-shadow: 0 0 8px rgb(255, 131, 197); + } + 50% { + text-shadow: 0 0 16px rgb(255, 0, 136); + } +} + +.vampire_master { + color: hsl(350, 75%, 62%); + font-weight: bold; + font-size: 120%; + animation: vampire_master_pulse 3s ease-in-out infinite; +} + +@keyframes vampire_master_pulse { + 0%, + 100% { + text-shadow: 0 0 5px hsl(350, 70%, 45%); + } + 50% { + color: hsl(355, 85%, 68%); + text-shadow: 0 0 10px hsl(352, 75%, 55%); + } +} +// OCULIS EDIT ADDITION END - VAMPIRES diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss index 7a03756b29a4..8caf97c32959 100644 --- a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss +++ b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss @@ -1441,3 +1441,38 @@ $border-width-px: $border-width * 1px; } } // NOVA EDIT ADDITION END + +// OCULIS EDIT ADDITION START - VAMPIRES +.awe { + color: hsl(240, 100%, 30%); + animation: awe_glow 2s ease-in-out infinite; +} + +@keyframes awe_glow { + 0%, + 100% { + text-shadow: 0 0 8px rgb(255, 131, 197); + } + 50% { + text-shadow: 0 0 16px rgb(255, 0, 136); + } +} + +.vampire_master { + color: hsl(350, 75%, 50%); + font-weight: bold; + font-style: italic; + animation: vampire_master_pulse 3s ease-in-out infinite; +} + +@keyframes vampire_master_pulse { + 0%, + 100% { + text-shadow: 0 0 5px hsl(350, 70%, 40%); + } + 50% { + color: hsl(355, 85%, 55%); + text-shadow: 0 0 10px hsl(352, 75%, 45%); + } +} +// OCULIS EDIT ADDITION END - VAMPIRES diff --git a/tgui/packages/tgui/interfaces/AntagInfoVampire.tsx b/tgui/packages/tgui/interfaces/AntagInfoVampire.tsx new file mode 100644 index 000000000000..c44b7726cb68 --- /dev/null +++ b/tgui/packages/tgui/interfaces/AntagInfoVampire.tsx @@ -0,0 +1,1070 @@ +// THIS IS AN OCULIS UI FILE + +import '../styles/interfaces/AntagInfoVampire.scss'; + +import { type SetStateAction, useState } from 'react'; +import { sanitizeText } from 'tgui/sanitize'; +import { + Box, + Button, + DmIcon, + Icon, + Section, + Stack, + Tabs, +} from 'tgui-core/components'; +import type { BooleanLike } from 'tgui-core/react'; +import { useBackend } from '../backend'; +import { Window } from '../layouts'; +import { + type Objective, + ObjectivePrintout, + ReplaceObjectivesButton, +} from './common/Objectives'; + +type VampireInformation = { + clan: ClanInfo[]; + in_clan: BooleanLike; + powers: PowerInfo[]; + vassal_count: number; + max_vassals: number; + objectives: Objective[]; + can_change_objective: BooleanLike; +}; + +type ClanInfo = { + name: string; + description: string; + icon: string; + icon_state: string; +}; + +type PowerInfo = { + name: string; + explanation: string; + icon: string; + icon_state: string; + cost: string; + constant_cost: string; + cooldown: string; +}; + +enum InfoTab { + General = 1, + Basics, + Powers, +} + +export const AntagInfoVampire = () => { + // Set default to 2 so Basics (now in the middle) opens by default + const [tab, setTab] = useState(InfoTab.Basics); + + return ( + + + + + setTab(InfoTab.General)} + > + General Guide + + + setTab(InfoTab.Basics)} + > + Basics + + + setTab(InfoTab.Powers)} + > + Powers + + + + + {tab === InfoTab.General && } + {tab === InfoTab.Basics && } + {tab === InfoTab.Powers && } + + + ); +}; + +const VampireIntroduction = (props: { + setTab: React.Dispatch>; +}) => { + const { data } = useBackend(); + const { objectives, vassal_count, max_vassals, can_change_objective } = data; + return ( + + +
+ + } + /> +
+
+ + = max_vassals ? 'vamp-blood' : undefined} + > + Vassals: {vassal_count} / {max_vassals} + + + + + + + + +
+ ); +}; + +enum GuideTab { + Basics = 1, + Masquerade, + Humanity, + Society, + Leveling, + Vitae, + Combat, + Haven, + Structures, + Vassals, +} + +const VampireGuide = () => { + const [tab, setTab] = useState(GuideTab.Basics); + + return ( +
+ + + + setTab(GuideTab.Basics)} + > + The Basics + + setTab(GuideTab.Masquerade)} + > + The Masquerade + + setTab(GuideTab.Humanity)} + > + Humanity + + setTab(GuideTab.Society)} + > + Princes & Society + + setTab(GuideTab.Leveling)} + > + Leveling + + setTab(GuideTab.Vitae)} + > + Vitae + + setTab(GuideTab.Combat)} + > + Combat + + setTab(GuideTab.Haven)} + > + Your Haven + + setTab(GuideTab.Structures)} + > + Structures + + setTab(GuideTab.Vassals)} + > + Vassals + + + + + + {tab === GuideTab.Basics && ( + + + So you're a big bad vampire. Congrats. + + + Now keep it to yourself. + + + - 'Smiling' Jack, Los Angeles, circa 2001-2008. + +
+ Vampires survive because mortals think they're myths. + That's the{' '} + + Masquerade + + . The wolf doesn't want the sheep to know they're there. + Except these sheep have guns. + + {' '} + You must stay hidden. + +
+
+ + Blending In + + You're dead: no breath, heartbeat, or need for food. That + makes you stand out. Avoid doctors, health scans, and especially + the{' '} + + Curator + + . They know vampires exist and can expose you. + + Tip: You have incredible powers, but using them draws + attention. Wise kindred blend in by acting like mortals. Use a + gun instead of claws. Walk instead of leaping across rooms. + Reserve your powers for when you truly need them. + +
+ + First Steps + + Take a moment to look at your screen. See those icons on the left? + That's your vampire HUD. Each icon gives you important + information, so click through them and learn what they show. +
+
+ Your next priority should be finding another kindred. They can + help you learn the ropes, and they might point you toward the + local{' '} + + Prince + + . +
+ + + #1 RULE OF SURVIVAL + + + Keep vitae above 300. + + + A starving vampire is a dead vampire. Panic leads to mistakes. + + + Feed often. Feed smart. Stay alive. + + +
+ )} + {tab === GuideTab.Masquerade && ( + + + The Masquerade + + + How to keep from getting us all killed. + +
+ The{' '} + + Masquerade + {' '} + is an organized disinformation campaign enforced by{' '} + + Kindred + {' '} + society (mainly the{' '} + + Camarilla + + ) to convince humans that vampires do not exist. +
+
+ If a mortal witnesses anything suspicious, you receive a{' '} + + Masquerade Infraction + + . After three, you are exiled and{' '} + + ALL + {' '} + vampires turn against you. +
+
+ The{' '} + + Curator + {' '} + possesses the{' '} + + Archive of the Kindred + + , which can instantly expose you. However, if your{' '} + + Feign Life Ability + {' '} + is active, even this ancient tome cannot see through your + disguise. +
+
+ At{' '} + + humanity + {' '} + above 7, you gain the{' '} + + Feign Life Ability + + , which fools health analyzers and the{' '} + + Curator + + . However, you will not heal normally while it is active. + + Tip: Too many bloodloss patients in medbay is just as + suspicious as a bloodless corpse in the halls. + +
+ + I broke the Masquerade. Now what? + + + • Everyone hunts you, vampires more than mortals +
• Your vassals are up for grabs +
• Other vampires can feed on you +
Draining another vampire grants you their powers +
• It is too late for mercy +
+
+ )} + {tab === GuideTab.Humanity && ( + + + Humanity + + + Are we human? Or are we dancer? + +
+ Most{' '} + + Kindred + {' '} + were human before their Embrace. Clinging to{' '} + + humanity + {' '} + is how they resist the{' '} + + Beast's + {' '} + feral nature. +
+
+ Your{' '} + + humanity + {' '} + directly affects the vampiric curse. Lower{' '} + + humanity + {' '} + means: +
+ + • Harder to interact with mortals +
• Difficult to stay active during daylight +
• Longer{' '} + + torpor + {' '} + recovery +
+
+ + Click the humanity counter on your HUD for detailed information. + +
+ Why call it{' '} + + Humanity + {' '} + when not all{' '} + + kindred + {' '} + were human? Simple: tradition. Centuries-old vampires are slow to + change their ways. +
+ )} + {tab === GuideTab.Society && ( + + + Princes & Scourges + +
A{' '} + + Prince + {' '} + is an elder vampire entrusted by the{' '} + + Camarilla + {' '} + to rule a territory. They keep track of every{' '} + + kindred + {' '} + present and enforce the{' '} + + Masquerade + {' '} + with an iron fist. +
+
+ Of course, they do not work alone. Many{' '} + + Princes + {' '} + employ a{' '} + + Scourge + + , a personal enforcer loyal only to them. Scourges are often + chosen from clans like the Tremere, though some rare{' '} + + Princes + {' '} + have been known to employ even Brujah. + + Important: Princes have higher expectations placed upon + them. They must protect the Masquerade at all costs and deliver + final death to misbehaving kindred without hesitation. + +
+ + The Camarilla + +
+ The{' '} + + Camarilla + {' '} + is the most organized vampiric sect: an elite club that favors + tradition and covert control of mortals from behind the scenes. + Most vampire clans are part of them, though the{' '} + + Brujah notably insist on remaining independent + + . +
+
+ Every city, station, colony, or outpost with a{' '} + + kindred + {' '} + presence has a{' '} + + Prince + {' '} + assigned by the{' '} + + Camarilla + {' '} + to oversee it. They are the chief enforcers of the{' '} + + Masquerade + + . +
+ )} + {tab === GuideTab.Leveling && ( + + + Leveling + + + Growing in Power + + As a vampire, you grow stronger over time by meeting your feeding + requirements. Click your blood meter on the HUD to see your + current progress toward the next rank. +
+ If you have consumed enough vitae to meet your goal, you will gain + a Rank whenever you next sleep in a coffin. Each rank provides + significant benefits: + + • Increased physical strength +
• Greater health pool +
• Faster feeding rate +
• Higher blood capacity +
• Additional discipline points to unlock new powers +
+
+ In addition, you also passively gain a few ranks over time, and + will gain one rank whenever you vassalize a mortal into your + servant. +
+ )} + {tab === GuideTab.Vitae && ( + + + Vitae + +
+ + Vitae + {' '} + is the lifeblood that sustains every vampire. The{' '} + + Beast + {' '} + within you demands constant feeding, and ignoring this need is not + an option. When your blood reserves reach zero, you will + experience blurred vision, impaired healing, and far worse + consequences. +
+
+ Your current rank determines how much{' '} + + vitae + {' '} + you can store and utilize at any given time. +
+
+ + Sources of{' '} + + vitae + + : + + + • Crewmembers +
• Monkeys +
• Mice +
• Bloodbags +
+ + Tip: Feed from crew regularly. Mice and monkeys will not + sustain you in the long run. + +
+ + Frenzy + + When your{' '} + + vitae + {' '} + is completely depleted, you lose control and enter a state known + as{' '} + + frenzy + + . In this feral state, the{' '} + + Beast + {' '} + takes over and compels you to attack the nearest mortal without + hesitation. +
+
+ While in{' '} + + frenzy + + , you gain the ability to grab victims instantly, making you + extremely dangerous but also highly conspicuous. The only way to + regain control of yourself is to feed until you have enough{' '} + + vitae + {' '} + to suppress the{' '} + + Beast + + . +
+
+ + Powers & Vitae + + All of your vampiric powers require{' '} + + vitae + {' '} + to use. Some abilities drain blood continuously while they remain + active, while others have an upfront cost when activated. Check + the Powers tab for specific costs and details on each ability. +
+ )} + {tab === GuideTab.Combat && ( + + + Combat + +
+ As a vampire, you have significant advantages in combat, but also + critical weaknesses that can be exploited. +
+
+ + Strengths + + + Enhanced Senses: Night vision and thermal vision let you + track prey in complete darkness. +
+
+ Undead Physiology: No need to breathe, sleep, or eat. You + are immune to disease. Fatal wounds put you into{' '} + + Torpor + {' '} + instead of killing you. You will rise again if you have{' '} + + vitae + {' '} + and are not staked. +
+
+ Resilience: Immune to cold, radiation, and toxins. + Critical injuries do not knock you down. +
+
+ Supernatural Strength: Your fists deal devastating + damage, scaling with your rank. +
+
+ + Weaknesses + + + Stakes: Paralyze you, disable powers, halt healing, and + prevent revival from{' '} + + Torpor + + . +
+
+ Fire and Lasers: Deal devastating damage. Fortitude + offers minimal protection. +
+
+ The Masquerade: Break it and every vampire turns against + you. You will be hunted by kindred and mortals alike. +
+
+ )} + {tab === GuideTab.Haven && ( + + + Your Haven + +
A{' '} + + haven + {' '} + is a location you have claimed as your own, where you can rest in + your coffin and perform certain vampiric rituals. Some vampires + find them useful. Many more have been caught because of them. +
+
+ + Do You Need a{' '} + + Haven + + ? + + + Honestly? Probably not. A{' '} + + haven + {' '} + is only necessary if you intend to create{' '} + + vassals + {' '} + or use certain structures. + +
+ + Claiming a{' '} + + Haven + + + + If you still want one: acquire a coffin from the Chapel or craft + one via the Furniture category. Find somewhere{' '} + truly hidden, place the coffin, and rest inside to claim + the area. Once claimed, you can anchor vampiric structures like + the{' '} + + Vassalization Rack + {' '} + or{' '} + + Blood Throne + + . + +
+ + Warning: Maintenance is the first place people look. If + someone finds your haven, they find everything: your coffin, + your structures, your vassals, and you. + +
+ )} + {tab === GuideTab.Structures && ( + + + Structures + + + These can be built via the Vampire crafting tab. + +
+ + Vassalization Rack + + + The vassalization rack is your tool for converting captured + crewmembers into loyal{' '} + + vassals + {' '} + who will serve your every command. +
+
+ Usage: Secure the rack in your{' '} + + haven + {' '} + → restrain your target → drag them onto the rack → click the + rack to begin the vassalization process. +
+
+ + Crewmembers with{' '} + + mindshields + {' '} + or strong loyalties require their mental defenses to be weakened + first.{' '} + + Eldritch servants + {' '} + are completely immune and can never be converted. + +
+ + Candelabrum + + + A vampiric candelabra that radiates an unsettling aura. Any + mortal who gazes upon its{' '} + + flame + {' '} + will find their sanity slowly draining away. + +
+ + Blood Throne + + + When you sit upon a Blood Throne, your words are broadcast + telepathically to all{' '} + + kindred + {' '} + on the station. Other vampires will need their own throne if + they wish to respond. + +
+ )} + {tab === GuideTab.Vassals && ( + + + Vassals + +
+ + Vassals + {' '} + are mortals who have been rendered addicted to your vitae, binding + them to your will. They serve as your eyes, ears, and hands among + the living, carrying out your commands while you remain hidden in + the shadows. +
+
+ Creating Vassals + + To create a vassal, you will need a{' '} + + Vassalization Rack + {' '} + secured within your{' '} + + haven + + . Capture your target and restrain them so they cannot escape, + then drag them onto the rack. Click the rack to begin the{' '} + + vassalization + {' '} + process that will give them an addiction to your blood, binding + them to your will. + +
+ Limitations + + Crewmembers protected by{' '} + + mindshields + {' '} + or those with strong existing loyalties cannot be converted + until their mental defenses have been weakened. Those who serve{' '} + + eldritch powers + {' '} + are completely immune and can never be turned. +
+
+ Once someone has become your vassal, the only way to free them + is through implantation of a{' '} + + mindshield + + . +
+
+ )} +
+
+
+ ); +}; + +const PowerSection = () => { + const { data } = useBackend(); + const { powers } = data; + if (!powers) { + return
; + } + + const [tab, setTab] = useState(0); + return ( +
+ + + + {powers.map((power, index) => ( + setTab(index)} + > + + + + } + width="32px" + style={{ + imageRendering: 'pixelated', + }} + /> + + {power.name} + + + ))} + + + + + {powers.map( + (power, index) => + tab === index && ( + + + {power.cost !== '0' && <>BLOOD COST: {power.cost}} + {power.cost !== '0' && power.constant_cost !== '0' && ( +
+ )} + {power.constant_cost !== '0' && ( + <>BLOOD DRAIN: {power.constant_cost} + )} + {(power.cost !== '0' || power.constant_cost !== '0') && + power.cooldown !== '0' && ( + <> +
+
+ + )} + {power.cooldown !== '0' && ( + <> + COOLDOWN: {power.cooldown} seconds +
+
+ + )} +
+ + + ), + )} +
+
+
+ ); +}; + +const ClanSection = () => { + const { data } = useBackend(); + const { clan, in_clan } = data; + + if (!in_clan) { + return ( +
+ + + + You are not in a clan! + + + + To determine your clan, utilize the clan selection ability. + + +
+ ); + } + + return ( +
+ {clan.map((ClanInfo, index) => ( + + + } + width="128px" + style={{ + imageRendering: 'pixelated', + }} + /> + + + + + You are part of the {ClanInfo.name}! + + + + + + ))} +
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/KindredBook.tsx b/tgui/packages/tgui/interfaces/KindredBook.tsx new file mode 100644 index 000000000000..0cdd8aebdf1f --- /dev/null +++ b/tgui/packages/tgui/interfaces/KindredBook.tsx @@ -0,0 +1,43 @@ +// THIS IS AN OCULIS UI FILE +import { Collapsible, Section, Table } from 'tgui-core/components'; +import { useBackend } from '../backend'; +import { Window } from '../layouts'; + +type Data = { + clans: ClanInfo[]; +}; + +type ClanInfo = { + name: string; + desc: string; +}; + +export const KindredBook = (props) => { + const { data } = useBackend(); + const { clans } = data; + return ( + + +
+ + + Written by generations of Curators, this holds all information we + the Curators know about the undead threat that looms the + station... + + So, what Clan are you interested in? +
+ + + {clans.map((clan) => ( + + {clan.desc} + + ))} + +
+
+
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/vampire.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/vampire.ts new file mode 100644 index 000000000000..091a2aa24bc3 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/vampire.ts @@ -0,0 +1,17 @@ +// THIS IS A OCULIS UI FILE +import { type Antagonist, Category } from '../base'; + +const Vampire: Antagonist = { + key: 'vampire', + name: 'Vampire', + description: [ + ` + After your death, you awaken to see yourself as an undead monster. + Scrape by Space Station 13, or take it over, ruling from the shadows! + `, + ], + category: Category.Roundstart, + priority: -1, +}; + +export default Vampire; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/vampire_breakout.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/vampire_breakout.ts new file mode 100644 index 000000000000..c0e6673f78fa --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/vampire_breakout.ts @@ -0,0 +1,17 @@ +// THIS IS A OCULIS UI FILE +import { type Antagonist, Category } from '../base'; + +const VampireBreakout: Antagonist = { + key: 'vampirebreakout', + name: 'Vampire Breakout', + description: [ + ` + After your death, you awaken to see yourself as an undead monster. + Use your Vampiric abilities as best you can. + Scrape by Space Station 13, or take over it, vassalizing your way. + `, + ], + category: Category.Latejoin, +}; + +export default VampireBreakout; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/vampiricaccident.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/vampiricaccident.ts new file mode 100644 index 000000000000..02e7ad1d3617 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/vampiricaccident.ts @@ -0,0 +1,17 @@ +// THIS IS A OCULIS UI FILE +import { type Antagonist, Category } from '../base'; + +const VampiricAccident: Antagonist = { + key: 'vampiricaccident', + name: 'Vampiric Accident', + description: [ + ` + After your death, you awaken to see yourself as an undead monster. + Use your Vampiric abilities as best you can. + Scrape by Space Station 13, or take over it, vassalizing your way. + `, + ], + category: Category.Midround, +}; + +export default VampiricAccident; diff --git a/tgui/packages/tgui/interfaces/common/AntagInfoHeader.tsx b/tgui/packages/tgui/interfaces/common/AntagInfoHeader.tsx new file mode 100644 index 000000000000..690cd7efc42b --- /dev/null +++ b/tgui/packages/tgui/interfaces/common/AntagInfoHeader.tsx @@ -0,0 +1,40 @@ +// THIS IS AN OCULIS UI FILE +import { Box, Image, Section, Stack } from 'tgui-core/components'; +import { resolveAsset } from '../../assets'; + +type Props = { + name: string; + asset?: string; + color?: string; + indefinite?: boolean; +}; + +export const AntagInfoHeader = (props: Props) => { + const { name, asset, color, indefinite } = props; + return ( +
+ + {!!asset && ( + + + + )} + +

+ You are {indefinite ? 'a' : 'the'}{' '} + + {name} + + ! +

+
+
+
+ ); +}; diff --git a/tgui/packages/tgui/styles/interfaces/AntagInfo.scss b/tgui/packages/tgui/styles/interfaces/AntagInfo.scss new file mode 100644 index 000000000000..7931211a21a6 --- /dev/null +++ b/tgui/packages/tgui/styles/interfaces/AntagInfo.scss @@ -0,0 +1,19 @@ +// THIS IS AN OCULIS SCSS FILE + +.AntagInfo__header_outer { + display: flex; + align-items: center; + justify-content: center; + height: 5rem; +} + +.AntagInfo__header_img { + position: absolute; + left: 0; + top: 50%; /* position the top edge of the image to the middle of the container */ + transform: translateY(-50%); /* shift the image up by half its height */ +} + +.AntagInfo__header_text { + text-align: center; /* Center the text */ +} diff --git a/tgui/packages/tgui/styles/interfaces/AntagInfoVampire.scss b/tgui/packages/tgui/styles/interfaces/AntagInfoVampire.scss new file mode 100644 index 000000000000..9606f6e29030 --- /dev/null +++ b/tgui/packages/tgui/styles/interfaces/AntagInfoVampire.scss @@ -0,0 +1,107 @@ +// THIS IS AN OCULIS UI FILE + +:root { + --vamp-blood: oklch(0.65 0.22 22); // vivid crimson — vitae, danger, blood + --vamp-blood-dark: oklch(0.52 0.18 22); // deep crimson — darkred headings + --vamp-kindred: oklch( + 0.72 0.15 300 + ); // violet-purple — kindred society, vassals + --vamp-masquerade: oklch( + 0.82 0.12 78 + ); // warm gold — the Masquerade, highlights + --vamp-arcane: oklch(0.72 0.12 242); // cool periwinkle — tips, humanity, info + --vamp-beast: oklch(0.75 0.17 55); // amber-orange — the Beast, frenzy + --vamp-lair: oklch(0.73 0.14 150); // sage green — lair, nature + --vamp-curator: oklch(0.79 0.11 340); // rose-pink — the Curator + --vamp-muted: oklch(0.58 0.02 240); // cool gray — secondary/quote text + --vamp-ember: oklch(0.84 0.13 96); // warm yellow — candelabrum +} + +// Semantic color helpers +.vamp-blood { + color: var(--vamp-blood); +} +.vamp-blood-dark { + color: var(--vamp-blood-dark); +} +.vamp-kindred { + color: var(--vamp-kindred); +} +.vamp-masquerade { + color: var(--vamp-masquerade); +} +.vamp-arcane { + color: var(--vamp-arcane); +} +.vamp-beast { + color: var(--vamp-beast); +} +.vamp-lair { + color: var(--vamp-lair); +} +.vamp-curator { + color: var(--vamp-curator); +} +.vamp-muted { + color: var(--vamp-muted); +} +.vamp-ember { + color: var(--vamp-ember); +} + +// Callout boxes +.vamp-tip { + font-size: 13px; + border-left: 3px solid var(--vamp-arcane); + border-radius: 0 4px 4px 0; + padding: 6px 10px; + color: var(--vamp-arcane); + background-color: oklch(0.72 0.12 242 / 0.07); +} + +.vamp-gold-note { + font-size: 13px; + border-left: 3px solid var(--vamp-masquerade); + border-radius: 0 4px 4px 0; + padding: 6px 10px; + color: var(--vamp-masquerade); + background-color: oklch(0.82 0.12 78 / 0.07); +} + +.vamp-danger-box { + border: 2px solid var(--vamp-blood); + border-radius: 8px; + padding: 10px; + background-color: oklch(0.65 0.22 22 / 0.1); +} + +// Top navigation tab bar +.vamp-top-tabs { + display: flex; + width: 100%; +} + +.vamp-top-tab { + flex: 1; + display: flex; + justify-content: center; + align-items: center; + text-align: center; + font-size: 25px; + font-weight: 900; + letter-spacing: 0.5px; + padding: 10px 12px; + font-family: + 'Cinzel Decorative', 'Uncial Antiqua', 'Old English Text MT', serif; + text-shadow: 0 1px 0 oklch(0 0 0 / 0.6); + + &--featured { + font-size: 30px; + } +} + +// Guide section sidebar tab spacing +.vamp-guide-tab { + padding-top: 10px; + padding-bottom: 10px; +} diff --git a/tgui/packages/tgui/styles/main.scss b/tgui/packages/tgui/styles/main.scss index f3ebc83a5611..eb0b1057b350 100644 --- a/tgui/packages/tgui/styles/main.scss +++ b/tgui/packages/tgui/styles/main.scss @@ -56,6 +56,9 @@ @include meta.load-css('./interfaces/LimbsPage.scss'); @include meta.load-css('./themes/clockwork.scss'); // NOVA EDIT ADDITION END +// OCULIS EDIT ADDITION START +@include meta.load-css('./interfaces/AntagInfo.scss'); +// OCULIS EDIT ADDITION END // Layouts @include meta.load-css('./layouts/Layout.scss');