diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm index 03778ec77400..dae1c56072c0 100644 --- a/code/__DEFINES/is_helpers.dm +++ b/code/__DEFINES/is_helpers.dm @@ -111,6 +111,8 @@ GLOBAL_LIST_INIT(turfs_without_ground, typecacheof(list( #define isrevenant(A) (istype(A, /mob/living/simple_animal/revenant)) +#define ishorror(A) (istype(A, /mob/living/simple_animal/horror)) + #define isbot(A) (istype(A, /mob/living/simple_animal/bot)) #define isshade(A) (istype(A, /mob/living/simple_animal/shade)) diff --git a/code/__DEFINES/role_preferences.dm b/code/__DEFINES/role_preferences.dm index 3c4a3043527e..27e99baa6dde 100644 --- a/code/__DEFINES/role_preferences.dm +++ b/code/__DEFINES/role_preferences.dm @@ -45,6 +45,7 @@ #define ROLE_GANG "gangster" // Yogs #define ROLE_DARKSPAWN "darkspawn" //Yogs #define ROLE_HOLOPARASITE "Holoparasite" // Yogs +#define ROLE_HORROR "Eldritch Horror" // Yogs #define ROLE_ZOMBIE "Zombie" @@ -63,6 +64,7 @@ GLOBAL_LIST_INIT(special_roles, list( ROLE_MALF, ROLE_REV = /datum/game_mode/revolution, ROLE_ALIEN, + ROLE_HORROR, ROLE_PAI, ROLE_CULTIST = /datum/game_mode/cult, ROLE_BLOB, diff --git a/code/_globalvars/lists/names.dm b/code/_globalvars/lists/names.dm index e36e464685e0..32029882bf1d 100644 --- a/code/_globalvars/lists/names.dm +++ b/code/_globalvars/lists/names.dm @@ -20,6 +20,7 @@ GLOBAL_LIST_INIT(plasmaman_names, world.file2list("strings/names/plasmaman.txt") GLOBAL_LIST_INIT(ethereal_names, world.file2list("strings/names/ethereal.txt")) GLOBAL_LIST_INIT(posibrain_names, world.file2list("strings/names/posibrain.txt")) GLOBAL_LIST_INIT(nightmare_names, world.file2list("strings/names/nightmare.txt")) +GLOBAL_LIST_INIT(horror_names, world.file2list("strings/names/horror.txt")) GLOBAL_LIST_INIT(megacarp_first_names, world.file2list("strings/names/megacarp1.txt")) GLOBAL_LIST_INIT(megacarp_last_names, world.file2list("strings/names/megacarp2.txt")) diff --git a/code/_onclick/hud/horror.dm b/code/_onclick/hud/horror.dm new file mode 100644 index 000000000000..a625df716845 --- /dev/null +++ b/code/_onclick/hud/horror.dm @@ -0,0 +1,17 @@ +/obj/screen/horror_chemicals + name = "chemicals" + icon_state = "horror_counter" + screen_loc = ui_lingchemdisplay + +/datum/hud/chemical_counter + ui_style = 'icons/mob/screen_midnight.dmi' + var/obj/screen/horror_chemicals/chemical_counter + +/datum/hud/chemical_counter/New(mob/owner) + . = ..() + chemical_counter = new /obj/screen/horror_chemicals + infodisplay += chemical_counter + +/datum/hud/chemical_counter/Destroy() + . = ..() + chemical_counter = null \ No newline at end of file diff --git a/code/game/objects/effects/temporary_visuals/miscellaneous.dm b/code/game/objects/effects/temporary_visuals/miscellaneous.dm index 6bd9e304cd8b..a716841b6317 100644 --- a/code/game/objects/effects/temporary_visuals/miscellaneous.dm +++ b/code/game/objects/effects/temporary_visuals/miscellaneous.dm @@ -476,3 +476,8 @@ /obj/effect/temp_visual/dir_setting/space_wind/Initialize(mapload, set_dir, set_alpha = 255) . = ..() alpha = set_alpha + +/obj/effect/temp_visual/summon + randomdir = 0 + duration = 20 + icon_state = "summon" diff --git a/code/game/objects/items/storage/uplink_kits.dm b/code/game/objects/items/storage/uplink_kits.dm index 163550862f7a..9e5c8e9a6b54 100644 --- a/code/game/objects/items/storage/uplink_kits.dm +++ b/code/game/objects/items/storage/uplink_kits.dm @@ -598,3 +598,16 @@ /obj/item/storage/box/syndie_kit/bee_grenades/PopulateContents() for(var/i in 1 to 3) new /obj/item/grenade/spawnergrenade/buzzkill(src) + +/obj/item/storage/box/syndie_kit/horror + name = "horror-in-a-box" + illustration = "writing_syndie" + desc = "A sleek, sturdy box with tentacles slithering from the inside. Uh oh." + +obj/item/storage/box/syndie_kit/horror/PopulateContents() + var/datum/reagent/W = pick(/datum/reagent/water/holywater, /datum/reagent/consumable/garlic, /datum/reagent/consumable/ketchup, /datum/reagent/consumable/eggyolk, /datum/reagent/consumable/sodiumchloride, /datum/reagent/consumable/hot_ramen) + W = new W + var/obj/item/horrorspawner/C = new /obj/item/horrorspawner(src) + C.weakness = W + var/obj/item/paper/crumpled/horrorweakness/N = new /obj/item/paper/crumpled/horrorweakness(src) + N.info = "..it's weakness...is...[W.name]...do.....not....
....let....it...." diff --git a/code/modules/admin/sql_ban_system.dm b/code/modules/admin/sql_ban_system.dm index 3480853aee8b..2d9c7b033784 100644 --- a/code/modules/admin/sql_ban_system.dm +++ b/code/modules/admin/sql_ban_system.dm @@ -266,10 +266,10 @@ break_counter++ output += "" var/list/long_job_lists = list("Civilian" = GLOB.civilian_positions, - "Ghost and Other Roles" = list(ROLE_BRAINWASHED, ROLE_DEATHSQUAD, ROLE_DRONE, ROLE_FUGITIVE, ROLE_HOLOPARASITE, ROLE_LAVALAND, ROLE_MIND_TRANSFER, ROLE_POSIBRAIN, ROLE_SENTIENCE), + "Ghost and Other Roles" = list(ROLE_BRAINWASHED, ROLE_DEATHSQUAD, ROLE_DRONE, ROLE_FUGITIVE, ROLE_HOLOPARASITE, ROLE_HORROR, ROLE_LAVALAND, ROLE_MIND_TRANSFER, ROLE_POSIBRAIN, ROLE_SENTIENCE), "Antagonist Positions" = list(ROLE_ABDUCTOR, ROLE_ALIEN, ROLE_BLOB, ROLE_BROTHER, ROLE_CHANGELING, ROLE_CULTIST, - ROLE_DEVIL, ROLE_FUGITIVE, ROLE_HOLOPARASITE, ROLE_INTERNAL_AFFAIRS, ROLE_MALF, + ROLE_DEVIL, ROLE_FUGITIVE, ROLE_HOLOPARASITE, ROLE_HORROR, ROLE_INTERNAL_AFFAIRS, ROLE_MALF, ROLE_MONKEY, ROLE_NINJA, ROLE_OPERATIVE, ROLE_OVERTHROW, ROLE_REV, ROLE_REVENANT, ROLE_REV_HEAD, ROLE_SERVANT_OF_RATVAR, ROLE_SYNDICATE, diff --git a/code/modules/antagonists/changeling/powers/headcrab.dm b/code/modules/antagonists/changeling/powers/headcrab.dm index 09a062747d2e..0a220e9a8ec9 100644 --- a/code/modules/antagonists/changeling/powers/headcrab.dm +++ b/code/modules/antagonists/changeling/powers/headcrab.dm @@ -32,6 +32,9 @@ to_chat(S, "Your sensors are disabled by a shower of blood!") S.Paralyze(60) var/turf = get_turf(user) + var/mob/living/simple_animal/horror/H = user.has_horror_inside() + if(H) + H.leave_victim() user.gib() . = TRUE sleep(5) // So it's not killed in explosion diff --git a/code/modules/antagonists/changeling/powers/panacea.dm b/code/modules/antagonists/changeling/powers/panacea.dm index bf6547f83cc5..a41d804accfb 100644 --- a/code/modules/antagonists/changeling/powers/panacea.dm +++ b/code/modules/antagonists/changeling/powers/panacea.dm @@ -10,6 +10,13 @@ //Heals the things that the other regenerative abilities don't. /datum/action/changeling/panacea/sting_action(mob/user) to_chat(user, "We cleanse impurities from our form.") + var/mob/living/simple_animal/horror/H = user.has_horror_inside() + if(H) + H.leave_victim() + if(iscarbon(user)) + var/mob/living/carbon/C = user + C.vomit(0, toxic = TRUE) + to_chat(user, "A parasite exits our form.") ..() var/list/bad_organs = list( user.getorgan(/obj/item/organ/body_egg), diff --git a/code/modules/antagonists/horror/horror.dm b/code/modules/antagonists/horror/horror.dm new file mode 100644 index 000000000000..8a5dba3473db --- /dev/null +++ b/code/modules/antagonists/horror/horror.dm @@ -0,0 +1,864 @@ +/mob/living/simple_animal/horror + name = "eldritch horror" + real_name = "eldritch horror" + desc = "Your eyes can barely comprehend what they're looking at." + icon_state = "horror" + icon_living = "horror" + icon_dead = "horror_dead" + health = 50 + maxHealth = 50 + melee_damage_lower = 10 + melee_damage_upper = 10 + see_in_dark = 7 + stop_automated_movement = TRUE + attacktext = "bites" + speak_emote = list("gurgles") + attack_sound = 'sound/weapons/bite.ogg' + pass_flags = PASSTABLE | PASSMOB + mob_size = MOB_SIZE_SMALL + faction = list("neutral","silicon","hostile","creature","heretics") + ventcrawler = VENTCRAWLER_ALWAYS + initial_language_holder = /datum/language_holder/universal + hud_type = /datum/hud/chemical_counter + + atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0) + minbodytemp = 0 + maxbodytemp = 1500 + + var/playstyle_string = "You are an eldritch horror, an evermutating parasitic abomination. Seek human souls to consume. \ + Crawl into people's heads and steal their essence. Use it to mutate yourself, giving you access to more power and abilities. \ + You operate on chemicals that get built up while you spend time in someone's head. You are weak when outside, play carefully.\ + Check your notes to see which chemical reagent is your bane, and avoid from getting in contact with it. " + + var/datum/reagent/weakness = /datum/reagent/consumable/sugar //default is sugar, but a random one is attributed + var/mob/living/carbon/victim + var/datum/mind/target + var/mob/living/captive_brain/host_brain + var/truename = null + var/available_points = 4 + var/consumed_souls = 0 + var/list/horrorabilities = list() //An associative list ("id" = ability datum) containing the abilities the horror has + var/list/horrorupgrades = list() //same, but for permanent upgrades + var/docile = FALSE + var/bonding = FALSE + var/controlling = FALSE + var/chemicals = 10 + var/chem_regen_rate = 2 + var/used_freeze + var/used_target + var/horror_chems = list(/datum/horror_chem/epinephrine,/datum/horror_chem/mannitol,/datum/horror_chem/bicaridine,/datum/horror_chem/kelotane,/datum/horror_chem/charcoal) + + var/leaving = FALSE + var/hiding = FALSE + var/invisible = FALSE + var/waketimerid = null + var/datum/action/innate/horror/talk_to_horror/talk_to_horror_action = new + +/mob/living/simple_animal/horror/Initialize(mapload, gen=1) + ..() + real_name = "Eldritch horror" + truename = "[pick(GLOB.horror_names)]" + + //default abilities + add_ability("mutate") + add_ability("seek_soul") + add_ability("consume_soul") + add_ability("talk_to_host") + add_ability("freeze_victim") + add_ability("infest") + add_ability("toggle_hide") + add_ability("talk_to_brain") + add_ability("take_control") + add_ability("leave_body") + add_ability("make_chems") + add_ability("talk_to_brain") + add_ability("release_control") + RefreshAbilities() + + var/datum/atom_hud/hud = GLOB.huds[DATA_HUD_MEDICAL_ADVANCED] + hud.add_hud_to(src) + update_horror_hud() + + +/mob/living/simple_animal/horror/Destroy() + host_brain = null + victim = null + return ..() + + +/mob/living/simple_animal/horror/proc/has_chemicals(amt) + return chemicals >= amt + +/mob/living/simple_animal/horror/proc/use_chemicals(amt) + if(!has_chemicals(amt)) + return FALSE + chemicals -= amt + update_horror_hud() + return TRUE + +/mob/living/simple_animal/horror/proc/regenerate_chemicals(amt) + chemicals += amt + chemicals = min(250, chemicals) + update_horror_hud() + +/mob/living/simple_animal/horror/proc/update_horror_hud() + if(!src || !hud_used) + return + var/datum/hud/chemical_counter/H = hud_used + var/obj/screen/counter = H.chemical_counter + counter.maptext = "
[chemicals]
" + +/mob/living/simple_animal/horror/proc/can_use_ability() + if(stat != CONSCIOUS) + to_chat(src, "You cannot do that in your current state.") + return FALSE + if(docile) + to_chat(src, "You are feeling far too docile to do that.") + return FALSE + return TRUE + +/mob/living/simple_animal/horror/proc/SearchTarget() + if(target) + if(world.time - used_target < 3 MINUTES) + to_chat(src, "You cannot use that ability again so soon.") + return + if(alert("You already have a target ([target.name]). Would you like to change that target?","Swap targets?","Yes","No") != "Yes") + return + + var/datum/objective/A = new + A.owner = mind + var/list/targets = list() + for(var/i in 0 to 4) + var/datum/mind/targeted + if(mind.enslaved_to) + targeted = A.find_target(null, list(mind.enslaved_to.mind, target)) + else + targeted = A.find_target(null, list(target)) + if(!targeted || !targeted.hasSoul) + break + targets[targeted.current.real_name] = targeted.current + + target = targets[input(src,"Choose your next target","Target") in targets] + qdel(A) + + if(target) + used_target = world.time + to_chat(src,"Your new target has been selected, go and consume [target.name]'s soul!") + apply_status_effect(/datum/status_effect/agent_pinpointer/horror) + for(var/datum/status_effect/agent_pinpointer/horror/status in status_effects) + status.scan_target = target + else + to_chat(src,"A new target could not be found.") + +/mob/living/simple_animal/horror/proc/ConsumeSoul() + if(!can_use_ability()) + return + + if(!src.victim.mind.hasSoul) + to_chat(src, "This host doesn't have a soul!") + return + + if(victim == mind.enslaved_to) + to_chat(src, "No, not yet... We still need them...") + return + + if(victim != target) + to_chat(src, "This soul isn't your target, you can't consume it!") + return + + to_chat(src, "You begin consuming [src.victim.name]'s soul!") + addtimer(CALLBACK(src, .proc/consume), 200) + +/mob/living/simple_animal/horror/proc/consume() + if(!can_use_ability() || !victim || !victim.mind.hasSoul) + return + consumed_souls++ + available_points++ + to_chat(src, "You succeed in consuming [victim.name]'s soul!") + to_chat(src.victim, "You suddenly feel weak and hollow inside...") + victim.health -= 20 + victim.maxHealth -= 20 + victim.mind.hasSoul = FALSE + target = null + remove_status_effect(/datum/status_effect/agent_pinpointer/horror) + playsound(src, 'sound/effects/curseattack.ogg', 150) + playsound(src, 'sound/effects/ghost.ogg', 50) + +/mob/living/simple_animal/horror/proc/Communicate() + if(!can_use_ability()) + return + if(!src.victim) + to_chat(src, "You do not have a host to communicate with!") + return + + var/input = stripped_input(src, "Please enter a message to tell your host.", "Horror", null) + if(!input) + return + + if(src && !QDELETED(src) && !QDELETED(src.victim)) + var/say_string = (src.docile) ? "slurs" :"states" + if(src.victim) + to_chat(victim, "[truename] [say_string]: [input]") + log_say("Horror Communication: [key_name(src)] -> [key_name(victim)] : [input]") + for(var/M in GLOB.dead_mob_list) + if(isobserver(M)) + var/rendered = "Horror Communication from [truename] : [input]" + var/link = FOLLOW_LINK(M, src) + to_chat(M, "[link] [rendered]") + to_chat(src, "[truename] [say_string]: [input]") + add_verb(victim, /mob/living/proc/horror_comm) + talk_to_horror_action.Grant(victim) + +/mob/living/proc/horror_comm() + set name = "Converse with Horror" + set category = "Horror" + set desc = "Communicate mentally with the thing in your head." + + var/mob/living/simple_animal/horror/B = has_horror_inside() + if(B) + var/input = stripped_input(src, "Please enter a message to tell the horror.", "Message", "") + if(!input) + return + + to_chat(B, "[src.name] says: [input]") + src.log_talk("Horror Communication: [key_name(src)] -> [key_name(B)] : [input]", LOG_SAY, tag="changeling") + + for(var/M in GLOB.dead_mob_list) + if(isobserver(M)) + var/rendered = "Horror Communication from [B.truename] : [input]" + var/link = FOLLOW_LINK(M, src) + to_chat(M, "[link] [rendered]") + to_chat(src, "[src] says: [input]") + +/mob/living/proc/trapped_mind_comm() + var/mob/living/simple_animal/horror/B = has_horror_inside() + if(!B || !B.host_brain) + return + var/mob/living/captive_brain/CB = B.host_brain + var/input = stripped_input(src, "Please enter a message to tell the trapped mind.", "Message", null) + if(!input) + return + + to_chat(CB, "[B.truename] says: [input]") + log_say("Horror Communication: [key_name(B)] -> [key_name(CB)] : [input]") + + for(var/M in GLOB.dead_mob_list) + if(isobserver(M)) + var/rendered = "Horror Communication from [B.truename] : [input]" + var/link = FOLLOW_LINK(M, src) + to_chat(M, "[link] [rendered]") + to_chat(src, "[B.truename] says: [input]") + +/mob/living/simple_animal/horror/Life() + ..() + if(horrorupgrades["regen"]) + heal_overall_damage(5) + + if(invisible) //don't regenerate chemicals when invisible + if(has_chemicals(5)) + use_chemicals(5) + alpha = max(alpha - 100, 1) + else + to_chat(src, "You ran out of chemicals to support your invisibility.") + invisible = FALSE + Update_Invisibility_Button() + else + if(horrorupgrades["nohost_regen"]) + regenerate_chemicals(chem_regen_rate) + else if(victim) + if(victim.stat == DEAD) + regenerate_chemicals(1) + else + regenerate_chemicals(chem_regen_rate) + alpha = min(255, alpha + 50) + + if(victim) + if(stat != DEAD && victim.stat != DEAD) + heal_overall_damage(1) + if(victim.reagents.has_reagent(weakness.type)) + if(!docile || waketimerid) + if(controlling) + to_chat(victim, "You feel the soporific flow of [weakness.name] in your host's blood, lulling you into docility.") + else + to_chat(src, "You feel the soporific flow of [weakness.name] in your host's blood, lulling you into docility.") + if(waketimerid) + deltimer(waketimerid) + waketimerid = null + docile = TRUE + else + if(docile && !waketimerid) + if(controlling) + to_chat(victim, "You start shaking off your lethargy as the [weakness.name] leaves your host's blood. This will take about 10 seconds...") + else + to_chat(src, "You start shaking off your lethargy as the [weakness.name] leaves your host's blood. This will take about 10 seconds...") + + waketimerid = addtimer(CALLBACK(src, "wakeup"), 10, TIMER_STOPPABLE) + if(controlling) + if(docile) + to_chat(victim, "You are feeling far too docile to continue controlling your host...") + victim.release_control() + return + + +/mob/living/simple_animal/horror/proc/wakeup() + if(controlling) + to_chat(victim, "You finish shaking off your lethargy.") + else + to_chat(src, "You finish shaking off your lethargy.") + docile = FALSE + if(waketimerid) + waketimerid = null + +/mob/living/simple_animal/horror/say(message, bubble_type, var/list/spans = list(), sanitize = TRUE, datum/language/language = null, ignore_spam = FALSE, forced = null) + if(victim) + to_chat(src, "You cannot speak out loud while inside a host!") + return + return ..() + +/mob/living/simple_animal/horror/emote(act, m_type = null, message = null, intentional = FALSE) + if(victim) + to_chat(src, "You cannot emote while inside a host!") + return + return ..() + +/mob/living/simple_animal/horror/UnarmedAttack(atom/A) + if(istype(A, /obj/machinery/door/airlock)) + visible_message("[src] slips their tentacles into the airlock and starts prying it open!", "You start moving onto the airlock.") + playsound(A, 'sound/misc/splort.ogg', 50, 1) + if(!do_after(src, 5 SECONDS, target = A)) + return + visible_message("[src] forces themselves through the airlock!", "You force yourself through the airlock.") + forceMove(get_turf(A)) + playsound(A, 'sound/machines/airlock_alien_prying.ogg', 50, 1) + return + if(isliving(A)) + if(victim || A == src.mind.enslaved_to) + healthscan(usr, A) + chemscan(usr, A) + else + alpha = 255 + if(hiding) + var/datum/action/innate/horror/H = has_ability("toggle_hide") + H.Activate() + if(invisible) + var/datum/action/innate/horror/H = has_ability("chameleon") + H.Activate() + Update_Invisibility_Button() + ..() + +/mob/living/simple_animal/horror/ex_act() + if(victim) + return + + ..() + +/mob/living/simple_animal/horror/proc/infect_victim() + if(!can_use_ability()) + return + if(victim) + to_chat(src, "You are already within a host.") + + if(stat == DEAD) + return + + var/list/choices = list() + for(var/mob/living/carbon/H in view(1,src)) + if(H!=src && Adjacent(H)) + choices += H + + if(!choices.len) + return + var/mob/living/carbon/H = choices.len > 1 ? input(src,"Who do you wish to infest?") in null|choices : choices[1] + if(!H || !src) + return + + if(!Adjacent(H)) + return + + if(H.has_horror_inside()) + to_chat(src, "[H] is already infested!") + return + + to_chat(src, "You slither your tentacles up [H] and begin probing at their ear canal...") + if(!do_mob(src, H, 30)) + to_chat(src, "As [H] moves away, you are dislodged and fall to the ground.") + return + + if(!H || !src) + return + + Infect(H) + +/mob/living/simple_animal/horror/proc/Infect(mob/living/carbon/C) + if(!C) + return + var/obj/item/bodypart/head/head = C.get_bodypart(BODY_ZONE_HEAD) + if(!head) + to_chat(src, "[C] doesn't have a head!") + return + var/hasbrain = FALSE + for(var/obj/item/organ/brain/X in C.internal_organs) + hasbrain = TRUE + break + if(!hasbrain) + to_chat(src, "[C] doesn't have a brain! ") + return + + if(C.has_horror_inside()) + to_chat(src, "[C] is already infested!") + return + + if((!C.key || !C.mind) && C != target) + to_chat(src, "[C]'s mind seems unresponsive. Try someone else!") + return + + invisible = FALSE + Update_Invisibility_Button() + victim = C + forceMove(victim) + RefreshAbilities() + log_game("[src]/([src.ckey]) has infested [victim]/([victim.ckey]") + +/mob/living/simple_animal/horror/proc/secrete_chemicals() + if(!victim) + to_chat(src, "You are not inside a host body.") + return + + if(!can_use_ability()) + return + + var content = "" + content += "

Chemicals: [chemicals]

" + + content += "" + + for(var/datum in horror_chems) + var/datum/horror_chem/C = new datum() + if(C.chemname) + content += "" + + content += "
[C.chemname] ([C.chemuse])

[C.chem_desc]

" + + var/html = get_html_template(content) + + usr << browse(null, "window=ViewHorror\ref[src]Chems;size=600x800") + usr << browse(html, "window=ViewHorror\ref[src]Chems;size=600x800") + +/mob/living/simple_animal/horror/proc/hide() + if(victim) + to_chat(src, "You cannot do this while you're inside a host.") + return + + if(stat != CONSCIOUS) + return + + if(!hiding) + layer = LATTICE_LAYER + visible_message("[src] scurries to the ground!", \ + "You are now hiding.") + hiding = TRUE + else + layer = MOB_LAYER + visible_message("[src] slowly peaks up from the ground...", \ + "You stop hiding.") + hiding = FALSE + +/mob/living/simple_animal/horror/proc/go_invisible() + if(victim) + to_chat(src, "You cannot do this while you're inside a host.") + return + + if(!can_use_ability()) + return + + if(!has_chemicals(10)) + to_chat(src, "You don't have enough chemicals to do that.") + return + + if(!invisible) + to_chat(src, "You focus your chameleon skin to blend into the environment.") + invisible = TRUE + else + to_chat(src, "You stop your camouflage.") + invisible = FALSE + +/mob/living/simple_animal/horror/proc/freeze_victim() + if(world.time - used_freeze < 150) + to_chat(src, "You cannot use that ability again so soon.") + return + + if(victim) + to_chat(src, "You cannot do that from within a host body.") + return + + if(!can_use_ability()) + return + + var/list/choices = list() + for(var/mob/living/carbon/C in view(1,src)) + if(C.stat == CONSCIOUS) + choices += C + + if(!choices.len) + return + var/mob/living/carbon/M = choices.len > 1 ? input(src,"Who do you wish to stun?") in null|choices : choices[1] + + + if(!M || !src || stat != CONSCIOUS || victim || (world.time - used_freeze < 150)) + return + if(!Adjacent(M)) + return + + layer = MOB_LAYER + if(horrorupgrades["paralysis"]) + to_chat(src, "You whip your electrocharged tentacle at [M]'s leg and knock them down!") + playsound(loc, "sound/effects/sparks4.ogg", 30, 1, -1) + M.SetSleeping(70) + M.electrocute_act(15, src, 1, FALSE, FALSE, FALSE, 1, FALSE) + else + to_chat(src, "You whip your tentacle at [M]'s leg and knock them down!") + to_chat(M, "You feel something wrapping around your leg, pulling you down!") + playsound(loc, "sound/weapons/whipgrab.ogg", 30, 1, -1) + M.Stun(50) + M.Knockdown(70) + used_freeze = world.time + +/mob/living/simple_animal/horror/proc/release_victim() + if(!victim) + to_chat(src, "You are not inside a host body.") + return + + if(!can_use_ability()) + return + + if(leaving) + leaving = FALSE + to_chat(src, "You decide against leaving your host.") + return + + to_chat(src, "You begin disconnecting from [victim]'s synapses and prodding at their internal ear canal.") + + if(victim.stat != DEAD && !horrorupgrades["invisible_exit"]) + to_chat(victim, "An odd, uncomfortable pressure begins to build inside your skull, behind your ear...") + + leaving = TRUE + + addtimer(CALLBACK(src, .proc/release_host), 100) + +/mob/living/simple_animal/horror/proc/release_host() + if(!victim || !src || QDELETED(victim) || QDELETED(src)) + return + if(!leaving) + return + if(controlling) + return + + if(stat != CONSCIOUS) + to_chat(src, "You cannot release your host in your current state.") + return + + if(horrorupgrades["invisible_exit"]) + alpha = 60 + if(has_ability("chameleon")) + invisible = TRUE + Update_Invisibility_Button() + to_chat(src, "You silently wiggle out of [victim]'s ear and plop to the ground before vanishing via reflective solution that covers you.") + else + to_chat(src, "You wiggle out of [victim]'s ear and plop to the ground.") + if(victim.mind) + if(!horrorupgrades["invisible_exit"]) + to_chat(victim, "Something slimy wiggles out of your ear and plops to the ground!") + + leaving = FALSE + + leave_victim() + +/mob/living/simple_animal/horror/proc/leave_victim() + if(!victim) + return + + if(controlling) + detatch() + + forceMove(get_turf(victim)) + + reset_perspective(null) + machine = null + + victim.reset_perspective(null) + victim.machine = null + + var/mob/living/V = victim + remove_verb(V, /mob/living/proc/horror_comm) + talk_to_horror_action.Remove(victim) + + for(var/obj/item/horrortentacle/T in victim) + victim.visible_message("[victim]'s tentacle transforms back!", "Your tentacle disappears!") + playsound(victim, 'sound/effects/blobattack.ogg', 30, 1) + qdel(T) + victim = null + + RefreshAbilities() + return + +/mob/living/simple_animal/horror/proc/jumpstart() + if(!victim) + to_chat(src, "You need a host to be able to use this.") + return + + if(!can_use_ability()) + return + + if(victim.stat != DEAD) + to_chat(src, "Your host is already alive!") + return + + if(!has_chemicals(250)) + to_chat(src, "You need 250 chemicals to use this!") + return + + if(victim.stat == DEAD) + victim.tod = null + victim.setToxLoss(0) + victim.setOxyLoss(0) + victim.setCloneLoss(0) + victim.SetUnconscious(0) + victim.SetStun(0) + victim.SetKnockdown(0) + victim.radiation = 0 + victim.heal_overall_damage(victim.getBruteLoss(), victim.getFireLoss()) + victim.reagents.clear_reagents() + if(ishuman(victim)) + var/mob/living/carbon/human/H = victim + H.restore_blood() + H.remove_all_embedded_objects() + victim.revive() + log_game("[src]/([src.ckey]) has revived [victim]/([victim.ckey]") + chemicals -= 250 + to_chat(src, "You send a jolt of energy to your host, reviving them!") + victim.grab_ghost(force = TRUE) //brings the host back, no eggscape + to_chat(victim, "You bolt upright, gasping for breath!") + +/mob/living/simple_animal/horror/proc/view_memory() + if(!victim) + to_chat(src, "You need a host to be able to use this.") + return + + if(!can_use_ability()) + return + + if(victim.stat == DEAD) + to_chat(src, "Your host brain is unresponsive. They are dead!") + return + + if(prob(20)) + to_chat(victim, "You suddenly feel your memory being tangled with...")//chance to alert the victim + + if(victim.mind) + var/datum/mind/suckedbrain = victim.mind + to_chat(src, "You skim through [victim]'s memories...[suckedbrain.memory]") + for(var/A in suckedbrain.antag_datums) + var/datum/antagonist/antag_types = A + var/list/all_objectives = antag_types.objectives.Copy() + if(antag_types.antag_memory) + to_chat(src, "[antag_types.antag_memory]") + if(LAZYLEN(all_objectives)) + to_chat(src, "Objectives:") + var/obj_count = 1 + for(var/O in all_objectives) + var/datum/objective/objective = O + to_chat(src, "Objective #[obj_count++]: [objective.explanation_text]") + var/list/datum/mind/other_owners = objective.get_owners() - suckedbrain + if(other_owners.len) + for(var/mind in other_owners) + var/datum/mind/M = mind + to_chat(src, "Conspirator: [M.name]") + + var/list/recent_speech = list() + var/list/say_log = list() + var/log_source = victim.logging + for(var/log_type in log_source) + var/nlog_type = text2num(log_type) + if(nlog_type & LOG_SAY) + var/list/reversed = log_source[log_type] + if(islist(reversed)) + say_log = reverseRange(reversed.Copy()) + break + if(LAZYLEN(say_log)) + for(var/spoken_memory in say_log) + if(recent_speech.len >= 5)//up to 5 random lines of speech, favoring more recent speech + break + if(prob(50)) + recent_speech[spoken_memory] = say_log[spoken_memory] + if(recent_speech.len) + to_chat(src, "You catch some drifting memories of their past conversations...") + for(var/spoken_memory in recent_speech) + to_chat(src, "[recent_speech[spoken_memory]]") + var/mob/living/carbon/human/H = victim + var/datum/dna/the_dna = H.has_dna() + if(the_dna) + to_chat(src, "You uncover that [H.p_their()] true identity is [the_dna.real_name].") + +/mob/living/simple_animal/horror/proc/bond_brain() + if(!victim) + to_chat(src, "You are not inside a host body.") + return + + if(!can_use_ability()) + return + + if(victim.stat == DEAD) + to_chat(src, "This host lacks enough brain function to control.") + return + + if(bonding) + bonding = FALSE + to_chat(src, "You stop attempting to take control of your host.") + return + + to_chat(src, "You begin delicately adjusting your connection to the host brain...") + + if(QDELETED(src) || QDELETED(victim)) + return + + bonding = TRUE + + var/delay = 200 + if(horrorupgrades["fast_control"]) + delay -= 120 + addtimer(CALLBACK(src, .proc/assume_control), delay) + +/mob/living/simple_animal/horror/proc/assume_control() + if(!victim || !src || controlling || victim.stat == DEAD) + return + if(!bonding) + return + if(docile) + to_chat(src, "You are feeling far too docile to do that.") + bonding = FALSE + return + if(is_servant_of_ratvar(victim) || iscultist(victim)) + to_chat(src, "[victim]'s mind seems to be blocked by some unknown force!") + bonding = FALSE + return + if(HAS_TRAIT(victim, TRAIT_MINDSHIELD)) + to_chat(src, "[victim]'s mind seems to be shielded from your influence!") + bonding = FALSE + return + else + RegisterSignal(victim, COMSIG_MOB_APPLY_DAMAGE, .proc/hit_detatch) + log_game("[src]/([src.ckey]) assumed control of [victim]/([victim.ckey] with eldritch powers.") + to_chat(src, "You plunge your probosci deep into the cortex of the host brain, interfacing directly with their nervous system.") + to_chat(victim, "You feel a strange shifting sensation behind your eyes as an alien consciousness displaces yours.") + + qdel(host_brain) + host_brain = new(src) + host_brain.H = src + victim.mind.transfer_to(host_brain) + + to_chat(host_brain, "You are trapped in your own mind. You feel that there must be a way to resist!") + + mind.transfer_to(victim) + + bonding = FALSE + controlling = TRUE + + remove_verb(victim, /mob/living/proc/horror_comm) + talk_to_horror_action.Remove(victim) + GrantControlActions() + + victim.med_hud_set_status() + if(target) + victim.apply_status_effect(/datum/status_effect/agent_pinpointer/horror) + for(var/datum/status_effect/agent_pinpointer/horror/status in victim.status_effects) + status.scan_target = target + +/mob/living/carbon/proc/release_control() + var/mob/living/simple_animal/horror/B = has_horror_inside() + if(B && B.host_brain) + to_chat(src, "You withdraw your probosci, releasing control of [B.host_brain]") + B.detatch() + +//Check for brain worms in head. +/mob/proc/has_horror_inside() + for(var/I in contents) + if(ishorror(I)) + return I + return FALSE + +/mob/living/simple_animal/horror/proc/hit_detatch() + if(victim.health <= 75) + detatch() + to_chat(src, "Upon taking damage, [victim]s brain detected danger, and hastily took over.") + to_chat(victim, "Your body is under attack, your brain immediately took over!") + +/mob/living/simple_animal/horror/proc/detatch() + if(!victim || !controlling) + return + + controlling = FALSE + UnregisterSignal(victim, COMSIG_MOB_APPLY_DAMAGE) + add_verb(victim, /mob/living/proc/horror_comm) + RemoveControlActions() + RefreshAbilities() + talk_to_horror_action.Grant(victim) + + victim.med_hud_set_status() + victim.remove_status_effect(/datum/status_effect/agent_pinpointer/horror) + + victim.mind.transfer_to(src) + if(host_brain) + host_brain.mind.transfer_to(victim) + + log_game("[src]/([src.ckey]) released control of [victim]/([victim.ckey]") + qdel(host_brain) + +/mob/living/simple_animal/horror/proc/Update_Invisibility_Button() + var/datum/action/innate/horror/action = has_ability("chameleon") + if(action) + action.button_icon_state = "horror_sneak_[invisible ? "true" : "false"]" + action.UpdateButtonIcon() + +/mob/living/simple_animal/horror/proc/GrantHorrorActions() + for(var/A in horrorabilities) + var/datum/action/innate/horror/ability = horrorabilities[A] + if("horror" in ability.category) + ability.Grant(src) + +/mob/living/simple_animal/horror/proc/RemoveHorrorActions() + for(var/A in horrorabilities) + var/datum/action/innate/horror/ability = horrorabilities[A] + if("horror" in ability.category) + ability.Remove(src) + +/mob/living/simple_animal/horror/proc/GrantInfestActions() + for(var/A in horrorabilities) + var/datum/action/innate/horror/ability = horrorabilities[A] + if("infest" in ability.category) + ability.Grant(src) + +/mob/living/simple_animal/horror/proc/RemoveInfestActions() + for(var/A in horrorabilities) + var/datum/action/innate/horror/ability = horrorabilities[A] + if("infest" in ability.category) + ability.Remove(src) + +/mob/living/simple_animal/horror/proc/GrantControlActions() + for(var/A in horrorabilities) + var/datum/action/innate/horror/ability = horrorabilities[A] + if("control" in ability.category) + ability.Grant(victim) + +/mob/living/simple_animal/horror/proc/RemoveControlActions() + for(var/A in horrorabilities) + var/datum/action/innate/horror/ability = horrorabilities[A] + if("control" in ability.category) + ability.Remove(victim) + +/mob/living/simple_animal/horror/proc/RefreshAbilities() //control abilities technically don't belong to horror + if(victim) + RemoveHorrorActions() + GrantInfestActions() + else + RemoveInfestActions() + GrantHorrorActions() \ No newline at end of file diff --git a/code/modules/antagonists/horror/horror_abilities_and_upgrades.dm b/code/modules/antagonists/horror/horror_abilities_and_upgrades.dm new file mode 100644 index 000000000000..04bf0568e91f --- /dev/null +++ b/code/modules/antagonists/horror/horror_abilities_and_upgrades.dm @@ -0,0 +1,443 @@ +//ABILITIES + +/datum/action/innate/horror + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_horror.dmi' + var/id //The ability's ID, for giving, taking and such + var/blacklisted = FALSE //If the ability can't be mutated + var/soul_price = 0 //How much souls the ability costs to buy; if this is 0, it isn't listed on the catalog + var/chemical_cost = 0 //How much chemicals the ability costs to use + var/mob/living/simple_animal/horror/B //Horror holding the ability + var/category //category for when the ability is active, "horror" is for creature, "infest" is during infestation, "controlling" is when a horror is controlling a body + +/datum/action/innate/horror/New(Target, horror) + B = horror + ..() + +/datum/action/innate/horror/IsAvailable() + if(!B) + return + if(!B.has_chemicals(chemical_cost)) + return + . = ..() + +/datum/action/innate/horror/mutate + name = "Mutate" + id = "mutate" + desc = "Use consumed souls to mutate your abilities." + button_icon_state = "mutate" + blacklisted = TRUE + category = list("horror","infest") + +/datum/action/innate/horror/mutate/Activate() + to_chat(usr, "You focus on mutating your body...") + B.ui_interact(usr) + return TRUE + +/datum/action/innate/horror/seek_soul + name = "Seek target soul" + id = "seek_soul" + desc = "Search for a soul weak enough for you to consume." + button_icon_state = "seek_soul" + blacklisted = TRUE + category = list("horror","infest") + +/datum/action/innate/horror/seek_soul/Activate() + B.SearchTarget() + +/datum/action/innate/horror/consume_soul + name = "Consume soul" + id = "consume_soul" + desc = "Consume your target's soul." + button_icon_state = "consume_soul" + blacklisted = TRUE + category = list("infest") + +/datum/action/innate/horror/consume_soul/Activate() + B.ConsumeSoul() + +/datum/action/innate/horror/talk_to_host + name = "Converse with Host" + id = "talk_to_host" + desc = "Send a silent message to your host." + button_icon_state = "talk_to_host" + blacklisted = TRUE + category = list("infest") + +/datum/action/innate/horror/talk_to_host/Activate() + B.Communicate() + +/datum/action/innate/horror/infest_host + name = "Infest" + id = "infest" + desc = "Infest a suitable humanoid host." + button_icon_state = "infest" + blacklisted = TRUE + category = list("horror") + +/datum/action/innate/horror/infest_host/Activate() + B.infect_victim() + +/datum/action/innate/horror/toggle_hide + name = "Toggle Hide" + id = "toggle_hide" + desc = "Become invisible to the common eye. Toggled on or off." + button_icon_state = "horror_hiding_false" + blacklisted = TRUE + category = list("horror") + +/datum/action/innate/horror/toggle_hide/Activate() + B.hide() + button_icon_state = "horror_hiding_[B.hiding ? "true" : "false"]" + UpdateButtonIcon() + +/datum/action/innate/horror/talk_to_horror + name = "Converse with Horror" + id = "talk_to_horror" + desc = "Communicate mentally with your horror." + button_icon_state = "talk_to_horror" + blacklisted = TRUE + +/datum/action/innate/horror/talk_to_horror/Activate() + var/mob/living/O = owner + O.horror_comm() + +/datum/action/innate/horror/talk_to_brain + name = "Converse with Trapped Mind" + id = "talk_to_brain" + desc = "Communicate mentally with the trapped mind of your host." + button_icon_state = "talk_to_trapped_mind" + blacklisted = TRUE + category = list("control") + +/datum/action/innate/horror/talk_to_brain/Activate() + B.victim.trapped_mind_comm() + +/datum/action/innate/horror/take_control + name = "Assume Control" + id = "take_control" + desc = "Fully connect to the brain of your host." + button_icon_state = "horror_brain" + blacklisted = TRUE + category = list("infest") + +/datum/action/innate/horror/take_control/Activate() + B.bond_brain() + +/datum/action/innate/horror/give_back_control + name = "Release Control" + id = "release_control" + desc = "Release control of your host's body." + button_icon_state = "horror_leave" + blacklisted = TRUE + category = list("control") + +/datum/action/innate/horror/give_back_control/Activate() + B.victim.release_control() + +/datum/action/innate/horror/leave_body + name = "Release Host" + id = "leave_body" + desc = "Slither out of your host." + button_icon_state = "horror_leave" + blacklisted = TRUE + category = list("infest") + +/datum/action/innate/horror/leave_body/Activate() + B.release_victim() + +/datum/action/innate/horror/make_chems + name = "Secrete chemicals" + id = "make_chems" + desc = "Push some chemicals into your host's bloodstream." + icon_icon = 'icons/obj/chemical.dmi' + button_icon_state = "minidispenser" + blacklisted = TRUE + category = list("infest") + +/datum/action/innate/horror/make_chems/Activate() + B.secrete_chemicals() + +/datum/action/innate/horror/freeze_victim + name = "Knockdown victim" + id = "freeze_victim" + desc = "Use your tentacle to trip a victim, stunning for a short duration." + button_icon_state = "trip" + blacklisted = TRUE + category = list("horror") + +/datum/action/innate/horror/freeze_victim/Activate() + B.freeze_victim() + UpdateButtonIcon() + addtimer(CALLBACK(src, .proc/UpdateButtonIcon), 150) + +/datum/action/innate/horror/freeze_victim/IsAvailable() + if(world.time - B.used_freeze < 150) + return FALSE + else + return ..() + +//non-default abilities, can be mutated + +/datum/action/innate/horror/tentacle + name = "Grow Tentacle" + id = "tentacle" + desc = "Makes your host grow a tentacle in their arm. Costs 50 chemicals to activate." + button_icon_state = "tentacle" + chemical_cost = 50 + category = list("infest", "control") + soul_price = 2 + +/datum/action/innate/horror/tentacle/IsAvailable() + if(!active && !B.has_chemicals(chemical_cost)) + return FALSE + return ..() + +/datum/action/innate/horror/tentacle/New() + ..() + START_PROCESSING(SSfastprocess, src) + +/datum/action/innate/horror/tentacle/Destroy() + STOP_PROCESSING(SSfastprocess, src) + return ..() + +/datum/action/innate/horror/tentacle/process() + ..() + active = locate(/obj/item/horrortentacle) in B.victim + UpdateButtonIcon() + + +/datum/action/innate/horror/tentacle/Activate() + B.use_chemicals(50) + B.victim.visible_message("[B.victim]'s arm contorts into tentacles!", "Your arm transforms into a giant tentacle. Examine it to see possible uses.") + playsound(B.victim, 'sound/effects/blobattack.ogg', 30, 1) + to_chat(B, "You transform [B.victim]'s arm into a tentacle!") + var/obj/item/horrortentacle/T = new + B.victim.put_in_hands(T) + return TRUE + +/datum/action/innate/horror/tentacle/Deactivate() + B.victim.visible_message("[B.victim]'s tentacle transforms back!", "Your tentacle disappears!") + playsound(B.victim, 'sound/effects/blobattack.ogg', 30, 1) + to_chat(B, "You transform [B.victim]'s arm back.") + for(var/obj/item/horrortentacle/T in B.victim) + qdel(T) + return TRUE + +/datum/action/innate/horror/transfer_host + name = "Transfer to another Host" + id = "transfer_host" + desc = "Move into another host directly. Grabbing makes the process faster." + button_icon_state = "transfer_host" + category = list("infest", "control") + soul_price = 1 + +/datum/action/innate/horror/transfer_host/Activate() + var/list/choices = list() + for(var/mob/living/carbon/C in range(1,B.victim)) + if(C!=B.victim && C.Adjacent(B.victim)) + choices += C + + if(!choices.len) + return + var/mob/living/carbon/C = choices.len > 1 ? input(owner,"Who do you wish to infest?") in null|choices : choices[1] + if(!C || !B) + return + if(!C.Adjacent(B.victim)) + return + var/obj/item/bodypart/head/head = C.get_bodypart(BODY_ZONE_HEAD) + if(!head) + to_chat(owner, "[C] doesn't have a head!") + return + var/hasbrain = FALSE + for(var/obj/item/organ/brain/X in C.internal_organs) + hasbrain = TRUE + break + if(!hasbrain) + to_chat(owner, "[C] doesn't have a brain! ") + return + if((!C.key || !C.mind) && C != B.target) + to_chat(owner, "[C]'s mind seems unresponsive. Try someone else!") + return + if(C.has_horror_inside()) + to_chat(owner, "[C] is already infested!") + return + + to_chat(owner, "You move your tentacles away from [B.victim] and begin to transfer to [C]...") + var/delay = 20 SECONDS + var/silent + if(B.victim.pulling != C) + silent = TRUE + else + switch(B.victim.grab_state) + if(GRAB_PASSIVE) + delay = 10 SECONDS + if(GRAB_AGGRESSIVE) + delay = 5 SECONDS + if(GRAB_NECK) + delay = 3 SECONDS + else + delay = 1 SECONDS + + if(!do_mob(B, C, delay)) + to_chat(owner, "As [C] moves away, your transfer gets interrupted!") + return + + if(!C || !B) + return + B.leave_victim() + B.Infect(C) + if(!silent) + to_chat(C, "Something slimy wiggles into your ear!") + playsound(B, 'sound/effects/blobattack.ogg', 30, 1) + +/datum/action/innate/horror/jumpstart_host + name = "Revive Host" + id = "jumpstart_host" + desc = "Bring your host back to life." + button_icon_state = "revive" + category = list("infest") + soul_price = 2 + +/datum/action/innate/horror/jumpstart_host/Activate() + B.jumpstart() + +/datum/action/innate/horror/view_memory + name = "View Memory" + id = "view_memory" + desc = "Read recent memory of the host you're inside of." + button_icon_state = "view_memory" + category = list("infest") + soul_price = 1 + +/datum/action/innate/horror/view_memory/Activate() + B.view_memory() + +/datum/action/innate/horror/chameleon + name = "Chameleon Skin" + id = "chameleon" + desc = "Adjust your skin color to blend into environment. Costs 5 chemicals per tick, also stopping chemical regeneration while active. Attacking stops the invisibility completely." + button_icon_state = "horror_sneak_false" + category = list("horror") + soul_price = 1 + +/datum/action/innate/horror/chameleon/Activate() + B.go_invisible() + button_icon_state = "horror_sneak_[B.invisible ? "true" : "false"]" + UpdateButtonIcon() + +//UPGRADES +/datum/horror_upgrade + var/name = "horror upgrade" + var/desc = "This is an upgrade." + var/id + var/soul_price = 0 //How much souls an upgrade costs to buy + var/mob/living/simple_animal/horror/B //Horror holding the upgrades + +/datum/horror_upgrade/proc/unlock() + if(!B) + return + apply_effects() + qdel(src) + return TRUE + +/datum/horror_upgrade/New(owner) + ..() + B = owner + +/datum/horror_upgrade/proc/apply_effects() + return + +//Upgrades the stun ability +/datum/horror_upgrade/paralysis + name = "Electrocharged tentacle" + id = "paralysis" + desc = "Empowers your tentacle knockdown ability by giving it extra charge, knocking your victim down unconcious." + soul_price = 3 + +/datum/horror_upgrade/paralysis/apply_effects() + var/datum/action/innate/horror/A = B.has_ability("freeze_victim") + if(A) + A.name = "Paralyze Victim" + A.desc = "Shock a victim with an electrically charged tentacle." + A.button_icon_state = "paralyze" + B.update_action_buttons() + +//Increases chemical regeneration rate by 2 +/datum/horror_upgrade/chemical_regen + name = "Efficient chemical glands" + id = "chem_regen" + desc = "Your chemical glands work more efficiently. Unlocking this increases your chemical regeneration." + soul_price = 2 + +/datum/horror_upgrade/chemical_regen/apply_effects() + B.chem_regen_rate += 2 + +//Lets horror regenerate chemicals outside of a host +/datum/horror_upgrade/nohost_regen + name = "Independent chemical glands" + id = "nohost_regen" + desc = "Your chemical glands become less parasitic and let you regenerate chemicals on their own without need for a host." + soul_price = 2 + +//Lets horror regenerate health +/datum/horror_upgrade/regen + name = "Regenerative skin" + id = "regen" + desc = "Your skin adapts to sustained damage and slowly regenerates itself, healing your wounds over time." + soul_price = 1 + +//Triples horror's health pool +/datum/horror_upgrade/hp_up + name = "Rhino skin" //Horror can....roll? + id = "hp_up" + desc = "Your skin becomes hard as rock, greatly increasing your maximum health - and odds of survival outside of host." + soul_price = 2 + +/datum/horror_upgrade/hp_up/apply_effects() + B.health = round(min(B.maxHealth,B.health * 3)) + B.maxHealth = round(B.maxHealth * 3) + +//Makes horror almost invisible for a short time after leaving a host +/datum/horror_upgrade/invisibility + name = "Reflective fluids" + id = "invisible_exit" + desc = "You build up reflective solution inside host's brain. Upon exiting a host, you're briefly covered in it, rendering you near invisible for a few seconds. This mutation also makes the host unable to notice you exiting it directly." + soul_price = 2 + +//Increases melee damage to 20 +/datum/horror_upgrade/dmg_up + name = "Sharpened teeth" + id = "dmg_up" + desc = "Your teeth become sharp blades, this mutation increases your melee damage." + soul_price = 2 + +/datum/horror_upgrade/dmg_up/apply_effects() + B.attacktext = "crushes" + B.attack_sound = 'sound/weapons/pierce_slow.ogg' //chunky + B.melee_damage_lower += 10 + B.melee_damage_upper += 10 + +//Expands the reagent selection horror can make +/datum/horror_upgrade/upgraded_chems + name = "Advanced reagent synthesis" + id = "upgraded_chems" + desc = "Lets you synthetize adrenaline, salicyclic acid, oxandrolone, pentetic acid and rezadone into your host." + soul_price = 2 + +/datum/horror_upgrade/upgraded_chems/apply_effects() + B.horror_chems += list(/datum/horror_chem/adrenaline,/datum/horror_chem/sal_acid,/datum/horror_chem/oxandrolone,/datum/horror_chem/pen_acid,/datum/horror_chem/rezadone) + +//faster mind control +/datum/horror_upgrade/fast_control + name = "Precise probosci" + id = "fast_control" + desc = "Your probosci become more precise, allowing you to take control over your host's brain noticably faster." + soul_price = 2 + +//makes it longer for host to snap out of mind control +/datum/horror_upgrade/deep_control + name = "Insulated probosci" + id = "deep_control" + desc = "Your probosci become insulated, protecting them from neural shocks. This makes it harder for the host to regain control over their body." + soul_price = 2 \ No newline at end of file diff --git a/code/modules/antagonists/horror/horror_chemicals.dm b/code/modules/antagonists/horror/horror_chemicals.dm new file mode 100644 index 000000000000..71b42fca9676 --- /dev/null +++ b/code/modules/antagonists/horror/horror_chemicals.dm @@ -0,0 +1,96 @@ +/mob/living/simple_animal/horror/Topic(href, href_list, hsrc) + if(href_list["horror_use_chem"]) + locate(href_list["src"]) + if(!istype(src, /mob/living/simple_animal/horror)) + return + + var/topic_chem = href_list["horror_use_chem"] + var/datum/horror_chem/C + + for(var/datum in typesof(/datum/horror_chem)) + var/datum/horror_chem/test = new datum() + if(test.chemname == topic_chem) + C = test + break + + if(!istype(C, /datum/horror_chem)) + return + + if(!C || !victim || controlling || !src || stat) + return + + if(!istype(C, /datum/horror_chem)) + return + + if(chemicals < C.chemuse) + to_chat(src, "You need [C.chemuse] chemicals stored to use this chemical!") + return + + to_chat(src, "You squirt a measure of [C.chemname] from your reservoirs into [victim]'s bloodstream.") + victim.reagents.add_reagent(C.R, C.quantity) + chemicals -= C.chemuse + log_game("[src]/([src.ckey]) has injected [C.chemname] into their host [victim]/([victim.ckey])") + + src << output(chemicals, "ViewHorror\ref[src]Chems.browser:update_chemicals") + + ..() + +/datum/horror_chem + var/chemname + var/chem_desc = "This is a chemical" + var/datum/reagent/R + var/chemuse = 30 + var/quantity = 10 + +/datum/horror_chem/epinephrine + chemname = "epinephrine" + R = /datum/reagent/medicine/epinephrine + chem_desc = "Stabilizes critical condition and slowly restores oxygen damage." + +/datum/horror_chem/mannitol + chemname = "mannitol" + R = /datum/reagent/medicine/mannitol + chem_desc = "Heals brain damage." + +/datum/horror_chem/bicaridine + chemname = "bicaridine" + R = /datum/reagent/medicine/bicaridine + chem_desc = "Heals brute damage." + +/datum/horror_chem/kelotane + chemname = "kelotane" + R = /datum/reagent/medicine/kelotane + chem_desc = "Heals burn damage." + +/datum/horror_chem/charcoal + chemname = "charcoal" + R = /datum/reagent/medicine/charcoal + chem_desc = "Slowly heals toxin damage, while also slowly removing any other chemicals." + +/datum/horror_chem/adrenaline + chemname = "adrenaline" + R = /datum/reagent/medicine/changelingadrenaline + chemuse = 100 + chem_desc = "Stimulates the brain, shrugging off effect of stuns while regenerating stamina." + +/datum/horror_chem/rezadone + chemname = "rezadone" + R = /datum/reagent/medicine/rezadone + chemuse = 50 + chem_desc = "Heals cellular damage." + +/datum/horror_chem/pen_acid + chemname = "pentetic acid" + R = /datum/reagent/medicine/pen_acid + chemuse = 50 + chem_desc = "Reduces massive amounts of radiation and toxin damage while purging other chemicals from the body." + +/datum/horror_chem/sal_acid + chemname = "salicyclic acid" + R = /datum/reagent/medicine/sal_acid + chem_desc = "Stimulates the healing of severe bruises. Rapidly heals severe bruising and slowly heals minor ones." + +/datum/horror_chem/oxandrolone + chemname = "oxandrolone" + R = /datum/reagent/medicine/oxandrolone + chem_desc = "Stimulates the healing of severe burns. Rapidly heals severe burns and slowly heals minor ones." \ No newline at end of file diff --git a/code/modules/antagonists/horror/horror_datums.dm b/code/modules/antagonists/horror/horror_datums.dm new file mode 100644 index 000000000000..7d97c1ecd124 --- /dev/null +++ b/code/modules/antagonists/horror/horror_datums.dm @@ -0,0 +1,334 @@ +//ANTAG DATUMS +/datum/antagonist/horror + name = "Horror" + show_in_antagpanel = TRUE + prevent_roundtype_conversion = FALSE + show_name_in_check_antagonists = TRUE + show_to_ghosts = TRUE + var/datum/mind/summoner + +/datum/antagonist/horror/on_gain() + . = ..() + give_objectives() + if(ishorror(owner) && owner.current.mind) + var/mob/living/simple_animal/horror/H = owner.current + H.update_horror_hud() + H.mind.store_memory("You become docile after contact with [H.weakness.name]. Avoid it.") + +/datum/antagonist/horror/proc/give_objectives() + if(summoner) + var/datum/objective/newobjective = new + newobjective.explanation_text = "Serve your summoner, [summoner.name]." + newobjective.owner = owner + newobjective.completed = TRUE + objectives += newobjective + else + var/datum/objective/horrorascend/ascend = new + ascend.owner = owner + ascend.target_amount = rand(6, 10) + objectives += ascend + ascend.update_explanation_text() + var/datum/objective/survive/survive = new + survive.owner = owner + objectives += survive + +/datum/objective/horrorascend + name = "consume souls" + +/datum/objective/horrorascend/update_explanation_text() + . = ..() + explanation_text = "Consume [target_amount] souls." + +/datum/objective/horrorascend/check_completion() + var/mob/living/simple_animal/horror/H = owner.current + if(istype(H) && H.consumed_souls > target_amount) + return TRUE + return FALSE + +//SPAWNER +/obj/item/horrorspawner + name = "suspicious pet carrier" + desc = "It contains some sort of creature inside. You can see tentacles sticking out of it." + icon = 'icons/obj/pet_carrier.dmi' + lefthand_file = 'icons/mob/inhands/items_lefthand.dmi' + righthand_file = 'icons/mob/inhands/items_righthand.dmi' + item_state = "pet_carrier" + icon_state = "pet_carrier_occupied" + var/used = FALSE + var/weakness + color = rgb(130, 105, 160) + +/obj/item/horrorspawner/attack_self(mob/living/user) + if(used) + to_chat(user, "The pet carrier appears unresponsive.") + return + used = TRUE + to_chat(user, "You're attempting to wake up the creature inside the box...") + sleep(5 SECONDS) + var/list/mob/dead/observer/candidates = pollGhostCandidates("Do you want to play as the eldritch horror in service of [user.real_name]?", ROLE_HORROR, null, FALSE, 100) + if(LAZYLEN(candidates)) + var/mob/dead/observer/C = pick(candidates) + var/mob/living/simple_animal/horror/H = new /mob/living/simple_animal/horror(get_turf(src)) + H.weakness = weakness + H.key = C.key + H.mind.enslave_mind_to_creator(user) + H.mind.add_antag_datum(C) + H.mind.memory += "You are [H.truename], an eldritch horror. Consume souls to evolve.
" + var/datum/antagonist/horror/S = new + S.summoner = user.mind + S.antag_memory += "[user.mind] woke you from your eternal slumber. Aid them in their objectives as a token of gratitude.
" + H.mind.add_antag_datum(S) + log_game("[key_name(user)] has summoned [key_name(H)], an eldritch horror.") + to_chat(user, "[H.truename] has awoken into your service!") + used = TRUE + icon_state = "pet_carrier_open" + sleep(5) + var/obj/item/horrorsummonhorn/horn = new /obj/item/horrorsummonhorn(get_turf(src)) + horn.summoner = user.mind + horn.horror = H + to_chat(user, "A strange looking [horn] falls out of [src]!") + else + to_chat(user, "The creatures looks at you with one of it's eyes before going back to slumber.") + used = FALSE + return + +//Summoning horn +/obj/item/horrorsummonhorn + name = "old horn" + desc = "A very old horn. You feel an incredible urge to blow into it." + icon = 'icons/obj/items_and_weapons.dmi' + lefthand_file = 'icons/mob/inhands/items_lefthand.dmi' + righthand_file = 'icons/mob/inhands/items_righthand.dmi' + item_state = "horn" + icon_state = "horn" + var/datum/mind/summoner + var/mob/living/simple_animal/horror/horror + var/cooldown + +/obj/item/horrorsummonhorn/examine(mob/user) + . = ..() + if(user.mind == summoner) + to_chat(user, "Blowing into this horn will recall the horror back to you. Be wary, the horn is loud, and may attract unwanted attention.") + +/obj/item/horrorsummonhorn/attack_self(mob/living/user) + if(cooldown > world.time) + to_chat(user, "Take a breath before you blow [src] again.") + return + to_chat(user, "You take a deep breath and prepare to blow into [src]...") + if(do_mob(user, src, 10 SECONDS)) + if(cooldown > world.time) + return + cooldown = world.time + 10 SECONDS + to_chat(src, "You blow the horn...") + playsound(loc, "sound/items/airhorn.ogg", 100, 1, 30) + var/turf/summonplace = get_turf(src) + sleep(5 SECONDS) + if(prob(20)) //yeah you're summoning an eldritch horror allright + new /obj/effect/temp_visual/summon(summonplace) + sleep(10) + var/type = pick(typesof(/mob/living/simple_animal/hostile/abomination)) + var/mob/R = new type(summonplace) + playsound(summonplace, "sound/effects/phasein.ogg", 30) + summonplace.visible_message("[R] emerges!") + else + if(!horror || horror.stat == DEAD) + summonplace.visible_message("But nothing responds to the call!") + else + new /obj/effect/temp_visual/summon(summonplace) + sleep(10) + horror.leave_victim() + horror.forceMove(summonplace) + playsound(summonplace, "sound/effects/phasein.ogg", 30) + summonplace.visible_message("[horror] appears out of nowhere!") + if(user.mind != summoner) + sleep(2 SECONDS) + playsound(summonplace, "sound/effects/glassbr2.ogg", 30, 1) + to_chat(user, "[src] breaks!") + qdel(src) + +/obj/item/horrorsummonhorn/suicide_act(mob/living/user) //"I am the prettiest unicorn that ever was!" ~Spy 2013 + user.visible_message("[user] stabs [user.p_their()] forehead with [src]! It looks like [user.p_theyre()] trying to commit suicide!") + return BRUTELOSS + +/obj/item/paper/crumpled/horrorweakness + name = "scruffed note" + infolang = /datum/language/aphasia //his previous owner got brought to madness, luckily curator can translate + +//Tentacle arm +/obj/item/horrortentacle + name = "tentacle" + desc = "A long, slimy, arm-like appendage." + icon = 'icons/obj/items_and_weapons.dmi' + icon_state = "horrortentacle" + item_state = "tentacle" + lefthand_file = 'icons/mob/inhands/antag/horror_lefthand.dmi' + righthand_file = 'icons/mob/inhands/antag/horror_righthand.dmi' + resistance_flags = ACID_PROOF + force = 17 + item_flags = ABSTRACT | DROPDEL + reach = 2 + hitsound = 'sound/weapons/whip.ogg' + +/obj/item/horrortentacle/Initialize(mapload) + . = ..() + ADD_TRAIT(src, TRAIT_NODROP, ABSTRACT_ITEM_TRAIT) + +/obj/item/horrortentacle/examine(mob/user) + . = ..() + to_chat(user, "Functions:") + to_chat(user, "All attacks work up to 2 tiles away.") + to_chat(user, "Help intent: Usual help function of an arm.") + to_chat(user, "Disarm intent: Whips the tentacle, disarming your opponent.") + to_chat(user, "Grab intent: Instant aggressive grab on an opponent. Can also throw them!") + to_chat(user, "Harm intent: Whips the tentacle, damaging your opponent.") + to_chat(user, "Also functions to pry open unbolted airlocks.") + +/obj/item/horrortentacle/attack(atom/target, mob/living/user) + if(isliving(target)) + user.Beam(target,"purpletentacle",time=5) + var/mob/living/L = target + switch(user.a_intent) + if(INTENT_HELP) + L.attack_hand(user) + return + if(INTENT_GRAB) + if(L != user) + L.grabbedby(user) + L.grippedby(user, instant = TRUE) + L.Knockdown(30) + return + if(INTENT_DISARM) + if(iscarbon(L)) + var/mob/living/carbon/C = L + var/obj/item/I = C.get_active_held_item() + if(I) + if(C.dropItemToGround(I)) + playsound(loc, "sound/weapons/whipgrab.ogg", 30) + target.visible_message("[I] is whipped out of [C]'s hand by [user]!","A tentacle whips [I] out of your hand!") + return + else + to_chat(user, "You can't seem to pry [I] off [C]'s hands!") + return + else + C.attack_hand(user) + return + . = ..() + +/obj/item/horrortentacle/afterattack(atom/target, mob/user, proximity) + if(isliving(user.pulling) && user.pulling != target) + var/mob/living/H = user.pulling + user.visible_message("[user] throws [H] with [user.p_their()] [src]!", "You throw [H] with [src].") + H.throw_at(target, 8, 2) + H.Knockdown(30) + return + if(!proximity) + return + if(istype(target, /obj/machinery/door/airlock)) + var/obj/machinery/door/airlock/A = target + + if((!A.requiresID() || A.allowed(user)) && A.hasPower()) + return + if(A.locked) + to_chat(user, "The airlock's bolts prevent it from being forced!") + return + + if(A.hasPower()) + user.visible_message("[user] jams [src] into the airlock and starts prying it open!", "You start forcing the airlock open.", + "You hear a metal screeching sound.") + playsound(A, 'sound/machines/airlock_alien_prying.ogg', 150, 1) + if(!do_after(user, 10 SECONDS, target = A)) + return + user.visible_message("[user] forces the airlock to open with [user.p_their()] [src]!", "You force the airlock to open.", + "You hear a metal screeching sound.") + A.open(2) + return + . = ..() + +/obj/item/horrortentacle/suicide_act(mob/user) //funnily enough, this will never be called, since horror stops suicide + user.visible_message("[src] coils itself around [user] tightly gripping [user.p_their()] neck! It looks like [user.p_theyre()] trying to commit suicide!") + return (OXYLOSS) + +//Pinpointer +/obj/screen/alert/status_effect/agent_pinpointer/horror + name = "Soul locator" + desc = "Find your target soul." + +/datum/status_effect/agent_pinpointer/horror + id = "horror_pinpointer" + minimum_range = 0 + range_fuzz_factor = 0 + tick_interval = 20 + alert_type = /obj/screen/alert/status_effect/agent_pinpointer/horror + +/datum/status_effect/agent_pinpointer/horror/scan_for_target() + return + +//TRAPPED MIND - when horror takes control over your body, you become a mute trapped mind +/mob/living/captive_brain + name = "host brain" + real_name = "host brain" + var/datum/action/innate/resist_control/R + var/mob/living/simple_animal/horror/H + +/mob/living/captive_brain/Initialize(mapload, gen=1) + ..() + R = new + R.Grant(src) + +/mob/living/captive_brain/say(message, bubble_type, var/list/spans = list(), sanitize = TRUE, datum/language/language = null, ignore_spam = FALSE, forced = null) + if(client) + if(client.prefs.muted & MUTE_IC) + to_chat(src, "You cannot speak in IC (muted).") + return + if(client.handle_spam_prevention(message,MUTE_IC)) + return + + if(ishorror(loc)) + message = sanitize(message) + if(!message) + return + log_say("[key_name(src)] : [message]") + if(stat == 2) + return say_dead(message) + + to_chat(src, "You whisper silently, \"[message]\"") + to_chat(H.victim, "The captive mind of [src] whispers, \"[message]\"") + + for (var/mob/M in GLOB.player_list) + if(isnewplayer(M)) + continue + else if(M.stat == 2 && M.client.prefs.toggles & CHAT_GHOSTEARS) + to_chat(M, "Thought-speech, [src] -> [H.truename]: [message]") + +/mob/living/captive_brain/emote(act, m_type = null, message = null, intentional = FALSE) + return + +/datum/action/innate/resist_control + name = "Resist control" + desc = "Try to take back control over your brain. A strong nerve impulse should do it." + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_horror.dmi' + button_icon_state = "resist_control" + +/datum/action/innate/resist_control/Activate() + var/mob/living/captive_brain/B = owner + if(B) + B.try_resist() + +/mob/living/captive_brain/resist() + try_resist() + +/mob/living/captive_brain/proc/try_resist() + var/delay = rand(20 SECONDS,30 SECONDS) + if(H.horrorupgrades["deep_control"]) + delay += rand(20 SECONDS,30 SECONDS) + to_chat(src, "You begin doggedly resisting the parasite's control.") + to_chat(H.victim, "You feel the captive mind of [src] begin to resist your control.") + addtimer(CALLBACK(src, .proc/return_control), delay) + +/mob/living/captive_brain/proc/return_control() + if(!H || !H.controlling) + return + to_chat(src, "With an immense exertion of will, you regain control of your body!") + to_chat(H.victim, "You feel control of the host brain ripped from your grasp, and retract your probosci before the wild neural impulses can damage you.") + H.detatch() \ No newline at end of file diff --git a/code/modules/antagonists/horror/horror_html.dm b/code/modules/antagonists/horror/horror_html.dm new file mode 100644 index 000000000000..4f70c856805d --- /dev/null +++ b/code/modules/antagonists/horror/horror_html.dm @@ -0,0 +1,102 @@ +/mob/living/simple_animal/horror/proc/get_html_template(content) + var/html = {" + + + Horror Chemicals + + + + + + +
+

Horror Chemicals

+
+ [content] +
"} + return html \ No newline at end of file diff --git a/code/modules/antagonists/horror/horror_mutate.dm b/code/modules/antagonists/horror/horror_mutate.dm new file mode 100644 index 000000000000..8c48fd808d99 --- /dev/null +++ b/code/modules/antagonists/horror/horror_mutate.dm @@ -0,0 +1,99 @@ +// Horror mutation menu +// Totally not a copypaste of darkspawn menu, not a copypaste of cellular emporium, i swear. + +/mob/living/simple_animal/horror/proc/has_ability(id) + if(isnull(horrorabilities[id])) + return + return horrorabilities[id] + +/mob/living/simple_animal/horror/proc/add_ability(id) + if(has_ability(id)) + return + for(var/V in subtypesof(/datum/action/innate/horror)) + var/datum/action/innate/horror/H = V + if(initial(H.id) == id) + var/datum/action/innate/horror/action = new H(null, src) + horrorabilities[id] = action + action.B = src + RefreshAbilities() + to_chat(src, "You have mutated the [action.name].") + available_points = max(0, available_points - action.soul_price) + return TRUE + +/mob/living/simple_animal/horror/proc/has_upgrade(id) + return horrorupgrades[id] + +/mob/living/simple_animal/horror/proc/add_upgrade(id) + if(has_upgrade(id)) + return + for(var/V in subtypesof(/datum/horror_upgrade)) + var/datum/horror_upgrade/_U = V + if(initial(_U.id) == id) + var/datum/horror_upgrade/U = new _U(src) + horrorupgrades[id] = TRUE + to_chat(src, "You have adapted the \"[U.name]\" upgrade.") + available_points = max(0, available_points - U.soul_price) + U.unlock() + +//mutation menu, 100% ripoff of psiweb, pls don't sue + +/mob/living/simple_animal/horror/ui_state(mob/user) + return GLOB.always_state + +/mob/living/simple_animal/horror/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "HorrorMutate", "Horror Mutation") + ui.open() + +/mob/living/simple_animal/horror/ui_data(mob/user) + var/list/data = list() + + data["available_points"] = "[available_points] | [consumed_souls] consumed souls total" + + var/list/abilities = list() + var/list/upgrades = list() + + for(var/path in subtypesof(/datum/action/innate/horror)) + var/datum/action/innate/horror/ability = path + + if(initial(ability.blacklisted)) + continue + + var/list/AL = list() + AL["name"] = initial(ability.name) + AL["id"] = initial(ability.id) + AL["desc"] = initial(ability.desc) + AL["soul_cost"] = initial(ability.soul_price) + AL["owned"] = has_ability(initial(ability.id)) + AL["can_purchase"] = !AL["owned"] && available_points >= initial(ability.soul_price) + + abilities += list(AL) + + data["abilities"] = abilities + + for(var/path in subtypesof(/datum/horror_upgrade)) + var/datum/horror_upgrade/upgrade = path + + var/list/DE = list() + DE["name"] = initial(upgrade.name) + DE["id"] = initial(upgrade.id) + DE["desc"] = initial(upgrade.desc) + DE["soul_cost"] = initial(upgrade.soul_price) + DE["owned"] = has_upgrade(initial(upgrade.id)) + DE["can_purchase"] = !DE["owned"] && available_points >= initial(upgrade.soul_price) + + upgrades += list(DE) + + data["upgrades"] = upgrades + + return data + +/mob/living/simple_animal/horror/ui_act(action, params) + if(..()) + return + switch(action) + if("unlock") + add_ability(params["id"]) + if("upgrade") + add_upgrade(params["id"]) \ No newline at end of file diff --git a/code/modules/client/verbs/suicide.dm b/code/modules/client/verbs/suicide.dm index 4e773fc274bb..f8cbe7e361b4 100644 --- a/code/modules/client/verbs/suicide.dm +++ b/code/modules/client/verbs/suicide.dm @@ -274,4 +274,7 @@ if(!(mobility_flags & MOBILITY_USE)) //just while I finish up the new 'fun' suiciding verb. This is to prevent metagaming via suicide to_chat(src, "You can't commit suicide whilst immobile! ((You can type Ghost instead however.))") return + if(has_horror_inside()) + to_chat(src, "Something inside your head stops your action!") + return return TRUE diff --git a/code/modules/events/horror.dm b/code/modules/events/horror.dm new file mode 100644 index 000000000000..e9a8efbe5f2d --- /dev/null +++ b/code/modules/events/horror.dm @@ -0,0 +1,37 @@ +/datum/round_event_control/horror + name = "Spawn Eldritch Horror" + typepath = /datum/round_event/ghost_role/horror + max_occurrences = 2 + min_players = 15 + earliest_start = 20 MINUTES + +/datum/round_event/ghost_role/horror + minimum_required = 1 + role_name = "horror" + fakeable = FALSE + +/datum/round_event/ghost_role/horror/spawn_role() + var/list/candidates = get_candidates(ROLE_HORROR, null, ROLE_HORROR) + if(!candidates.len) + return NOT_ENOUGH_PLAYERS + + var/mob/dead/selected = pick_n_take(candidates) + + var/datum/mind/player_mind = new /datum/mind(selected.key) + player_mind.active = 1 + if(!GLOB.generic_event_spawns) + return MAP_ERROR + var/mob/living/simple_animal/horror/S = new /mob/living/simple_animal/horror(get_turf(pick(GLOB.generic_event_spawns))) + var/datum/reagent/W = pick(/datum/reagent/water/holywater, /datum/reagent/consumable/garlic, /datum/reagent/consumable/ketchup, /datum/reagent/consumable/eggyolk, /datum/reagent/consumable/sodiumchloride, /datum/reagent/consumable/hot_ramen) + W = new W + S.weakness = W + player_mind.transfer_to(S) + player_mind.assigned_role = "Eldritch Horror" + player_mind.special_role = "Eldritch Horror" + player_mind.add_antag_datum(/datum/antagonist/horror) + to_chat(S, S.playstyle_string) + SEND_SOUND(S, sound('sound/hallucinations/growl2.ogg')) + message_admins("[ADMIN_LOOKUPFLW(S)] has been made into an eldritch horror by an event.") + log_game("[key_name(S)] was spawned as an eldritch horror by an event.") + spawned_mobs += S + return SUCCESSFUL_SPAWN diff --git a/code/modules/mob/living/brain/brain_item.dm b/code/modules/mob/living/brain/brain_item.dm index f1d6672df55a..3cf269bd69c1 100644 --- a/code/modules/mob/living/brain/brain_item.dm +++ b/code/modules/mob/living/brain/brain_item.dm @@ -56,6 +56,10 @@ /obj/item/organ/brain/Remove(mob/living/carbon/C, special = 0, no_id_transfer = FALSE) ..() + if(!special) + if(C.has_horror_inside()) + var/mob/living/simple_animal/horror/B = C.has_horror_inside() + B.leave_victim() if(C.mind && C.mind.has_antag_datum(/datum/antagonist/changeling)) var/datum/antagonist/changeling/bruh = C.mind.has_antag_datum(/datum/antagonist/changeling) for(var/d in bruh.purchasedpowers) diff --git a/code/modules/mob/living/carbon/death.dm b/code/modules/mob/living/carbon/death.dm index 3a998a7d4a2b..61cb09bc23da 100644 --- a/code/modules/mob/living/carbon/death.dm +++ b/code/modules/mob/living/carbon/death.dm @@ -25,7 +25,10 @@ stomach_contents.Remove(M) //yogs end M.forceMove(Tsec) - visible_message("[M] bursts out of [src]!") + if(ishorror(M)) //eldritch horror(aka. borer) check, they die along with their host to prevent mind controlled suicides + M.gib() + else + visible_message("[M] bursts out of [src]!") ..() /mob/living/carbon/spill_organs(no_brain, no_organs, no_bodyparts) diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index eacb0caba5c8..465b35a87c72 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -73,7 +73,10 @@ . += "" . += "Hivemind Vessels: [hivemind.hive_size] (+[hivemind.size_mod])" . += "Psychic Link Duration: [(hivemind.track_bonus + TRACKER_DEFAULT_TIME)/10] seconds" - + var/mob/living/simple_animal/horror/H = has_horror_inside() + if(H && H.controlling) + . += "" + . += "Horror chemicals: [H.chemicals]" if(mind) var/datum/antagonist/changeling/changeling = mind.has_antag_datum(/datum/antagonist/changeling) if(changeling) diff --git a/code/modules/mob/transform_procs.dm b/code/modules/mob/transform_procs.dm index a26791e84b88..6c85ca92c621 100644 --- a/code/modules/mob/transform_procs.dm +++ b/code/modules/mob/transform_procs.dm @@ -3,6 +3,9 @@ /mob/living/carbon/proc/monkeyize(tr_flags = (TR_KEEPITEMS | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_DEFAULTMSG)) if (notransform || transformation_timer) return + if(has_horror_inside()) + to_chat(src, "You feel something strongly clinging to your humanity!") + return //Handle items on mob if(tr_flags & TR_KEEPITEMS) @@ -647,4 +650,4 @@ //Not in here? Must be good! return 1 -#undef TRANSFORMATION_DURATION \ No newline at end of file +#undef TRANSFORMATION_DURATION diff --git a/code/modules/surgery/organ_manipulation.dm b/code/modules/surgery/organ_manipulation.dm index 8c6b731bb3d7..7cce0956e78f 100644 --- a/code/modules/surgery/organ_manipulation.dm +++ b/code/modules/surgery/organ_manipulation.dm @@ -102,6 +102,11 @@ else if(implement_type in implements_extract) current_type = "extract" var/list/organs = target.getorganszone(target_zone) + var/mob/living/simple_animal/horror/H = target.has_horror_inside() + if(H) + user.visible_message("[user] begins to extract [H] from [target]'s [parse_zone(target_zone)].", + "You begin to extract [H] from [target]'s [parse_zone(target_zone)]...") + return TRUE if(!organs.len) to_chat(user, "There are no removable organs in [target]'s [parse_zone(target_zone)]!") return -1 @@ -143,6 +148,13 @@ "[user] inserts something into [target]'s [parse_zone(target_zone)]!") else if(current_type == "extract") + var/mob/living/simple_animal/horror/H = target.has_horror_inside() + if(H && H.victim == target) + user.visible_message("[user] successfully extracts [H] from [target]'s [parse_zone(target_zone)]!", + "You successfully extract [H] from [target]'s [parse_zone(target_zone)].") + log_combat(user, target, "surgically removed [H] from", addition="INTENT: [uppertext(user.a_intent)]") + H.leave_victim() + return FALSE if(I && I.owner == target) display_results(user, target, "You successfully extract [I] from [target]'s [parse_zone(target_zone)].", "[user] successfully extracts [I] from [target]'s [parse_zone(target_zone)]!", diff --git a/code/modules/uplink/uplink_items.dm b/code/modules/uplink/uplink_items.dm index 9526565bd306..7d5b7df245c9 100644 --- a/code/modules/uplink/uplink_items.dm +++ b/code/modules/uplink/uplink_items.dm @@ -1902,6 +1902,17 @@ GLOBAL_LIST_INIT(uplink_items, subtypesof(/datum/uplink_item)) cost = 20 restricted_roles = list("Chaplain") surplus = 5 //Very low chance to get it in a surplus crate even without being the chaplain + +/datum/uplink_item/role_restricted/horror + name = "Horror-in-a-box" + desc = "When dissecting the head of a dead Nanotrasen scientist, our surgeons noticed an incredibly peculiar creature inside and managed to extract it into safe containment. \ + Either a failed experiment or otherworldly monster, this creature has been trained to aid whoever wakes it up. If you aren't afraid of it entering your head, it can prove a useful ally. \ + We take no responsibility for your newfound madness and accept no refunds." + item = /obj/item/storage/box/syndie_kit/horror + cost = 16 + surplus = 0 + restricted_roles = list("Curator") + player_minimum = 20 /datum/uplink_item/role_restricted/explosive_hot_potato name = "Exploding Hot Potato" diff --git a/icons/effects/beam.dmi b/icons/effects/beam.dmi index 0f7daf276d07..fbe6182716b7 100644 Binary files a/icons/effects/beam.dmi and b/icons/effects/beam.dmi differ diff --git a/icons/effects/effects.dmi b/icons/effects/effects.dmi index 1a3d0b762527..59a5ad1a79a3 100644 Binary files a/icons/effects/effects.dmi and b/icons/effects/effects.dmi differ diff --git a/icons/mob/actions/actions_horror.dmi b/icons/mob/actions/actions_horror.dmi new file mode 100644 index 000000000000..c8a0029e8cf5 Binary files /dev/null and b/icons/mob/actions/actions_horror.dmi differ diff --git a/icons/mob/animal.dmi b/icons/mob/animal.dmi index 6da9b16d5a0b..2c27d854e073 100644 Binary files a/icons/mob/animal.dmi and b/icons/mob/animal.dmi differ diff --git a/icons/mob/inhands/antag/horror_lefthand.dmi b/icons/mob/inhands/antag/horror_lefthand.dmi new file mode 100644 index 000000000000..c28c81ab31ba Binary files /dev/null and b/icons/mob/inhands/antag/horror_lefthand.dmi differ diff --git a/icons/mob/inhands/antag/horror_righthand.dmi b/icons/mob/inhands/antag/horror_righthand.dmi new file mode 100644 index 000000000000..10d2c64115a5 Binary files /dev/null and b/icons/mob/inhands/antag/horror_righthand.dmi differ diff --git a/icons/mob/screen_gen.dmi b/icons/mob/screen_gen.dmi index 75911b35aa75..bb78ada5d519 100644 Binary files a/icons/mob/screen_gen.dmi and b/icons/mob/screen_gen.dmi differ diff --git a/icons/obj/items_and_weapons.dmi b/icons/obj/items_and_weapons.dmi index f5d6c1d49f71..4b57eed5dcb3 100644 Binary files a/icons/obj/items_and_weapons.dmi and b/icons/obj/items_and_weapons.dmi differ diff --git a/strings/names/horror.txt b/strings/names/horror.txt new file mode 100644 index 000000000000..8472fe71611e --- /dev/null +++ b/strings/names/horror.txt @@ -0,0 +1,7 @@ +F'tan +Hoe'lef +Uhluhtc +Ret'sehc +Fnurgeetav +Man'lei +Gvyrm-itei diff --git a/tgui/packages/tgui/interfaces/HorrorMutate.js b/tgui/packages/tgui/interfaces/HorrorMutate.js new file mode 100644 index 000000000000..abc08b4202bd --- /dev/null +++ b/tgui/packages/tgui/interfaces/HorrorMutate.js @@ -0,0 +1,58 @@ +import { useBackend } from '../backend'; +import { Button, LabeledList, Section, Box } from '../components'; +import { Window } from '../layouts'; + +export const HorrorMutate = (props, context) => { + const { act, data } = useBackend(context); + return ( + + +
+ + + {data.available_points} + + +
+
+ + {data.abilities.map(ability => ( + + + {ability.desc} + Cost to unlock: {ability.soul_cost} +
+
+ + {data.upgrades.map(upgrade => ( + + + {upgrade.desc} + Cost to unlock: {upgrade.soul_cost} +
+
+
+ ); +}; \ No newline at end of file diff --git a/yogstation.dme b/yogstation.dme index c76072307d76..6c9b0568c37c 100644 --- a/yogstation.dme +++ b/yogstation.dme @@ -223,6 +223,7 @@ #include "code\_onclick\hud\generic_dextrous.dm" #include "code\_onclick\hud\ghost.dm" #include "code\_onclick\hud\guardian.dm" +#include "code\_onclick\hud\horror.dm" #include "code\_onclick\hud\hud.dm" #include "code\_onclick\hud\human.dm" #include "code\_onclick\hud\lavaland_elite.dm" @@ -1465,6 +1466,12 @@ #include "code\modules\antagonists\highlander\highlander.dm" #include "code\modules\antagonists\hivemind\hivemind.dm" #include "code\modules\antagonists\hivemind\vessel.dm" +#include "code\modules\antagonists\horror\horror.dm" +#include "code\modules\antagonists\horror\horror_abilities_and_upgrades.dm" +#include "code\modules\antagonists\horror\horror_chemicals.dm" +#include "code\modules\antagonists\horror\horror_datums.dm" +#include "code\modules\antagonists\horror\horror_html.dm" +#include "code\modules\antagonists\horror\horror_mutate.dm" #include "code\modules\antagonists\kudzu\kudzu.dm" #include "code\modules\antagonists\magic_servant\servant.dm" #include "code\modules\antagonists\malf\malf.dm" @@ -1803,6 +1810,7 @@ #include "code\modules\events\grid_check.dm" #include "code\modules\events\heart_attack.dm" #include "code\modules\events\high_priority_bounty.dm" +#include "code\modules\events\horror.dm" #include "code\modules\events\immovable_rod.dm" #include "code\modules\events\ion_storm.dm" #include "code\modules\events\major_dust.dm"