diff --git a/code/__DEFINES/atom_hud.dm b/code/__DEFINES/atom_hud.dm index 4fd16697714f..4616ce8a5180 100644 --- a/code/__DEFINES/atom_hud.dm +++ b/code/__DEFINES/atom_hud.dm @@ -57,6 +57,7 @@ #define ANTAG_HUD_SOULLESS 21 #define ANTAG_HUD_CLOCKWORK 22 #define ANTAG_HUD_BROTHER 23 +#define ANTAG_HUD_HIVE 24 // Notification action types #define NOTIFY_JUMP "jump" diff --git a/code/__DEFINES/role_preferences.dm b/code/__DEFINES/role_preferences.dm index 5d5bf1165e7c..8b9ec53004b7 100644 --- a/code/__DEFINES/role_preferences.dm +++ b/code/__DEFINES/role_preferences.dm @@ -28,6 +28,7 @@ #define ROLE_VAMPIRE "vampire" // Yogs #define ROLE_BRAINWASHED "brainwashed victim" #define ROLE_OVERTHROW "syndicate mutineer" +#define ROLE_HIVE "hivemind host" #define ROLE_SENTIENCE "sentience potion spawn" #define ROLE_MIND_TRANSFER "mind transfer potion" #define ROLE_POSIBRAIN "posibrain" @@ -61,6 +62,7 @@ GLOBAL_LIST_INIT(special_roles, list( ROLE_VAMPIRE = /datum/game_mode/vampire, // Yogs ROLE_OVERTHROW = /datum/game_mode/overthrow, ROLE_SHADOWLING = /datum/game_mode/shadowling, //yogs + ROLE_HIVE = /datum/game_mode/hivemind, ROLE_INTERNAL_AFFAIRS = /datum/game_mode/traitor/internal_affairs, ROLE_SENTIENCE )) diff --git a/code/datums/hud.dm b/code/datums/hud.dm index 0d7d0b41d8fa..ede2116aca6a 100644 --- a/code/datums/hud.dm +++ b/code/datums/hud.dm @@ -28,6 +28,7 @@ GLOBAL_LIST_INIT(huds, list( ANTAG_HUD_CLOCKWORK = new/datum/atom_hud/antag(), ANTAG_HUD_BROTHER = new/datum/atom_hud/antag/hidden(), ANTAG_HUD_VAMPIRE = new/datum/atom_hud/antag/hidden(), // Yogs + ANTAG_HUD_HIVE = new/datum/atom_hud/antag/hidden(), )) /datum/atom_hud diff --git a/code/game/gamemodes/hivemind/hivemind.dm b/code/game/gamemodes/hivemind/hivemind.dm new file mode 100644 index 000000000000..356e5d5334b6 --- /dev/null +++ b/code/game/gamemodes/hivemind/hivemind.dm @@ -0,0 +1,75 @@ +/datum/game_mode/hivemind + name = "Assimilation" + config_tag = "hivemind" + antag_flag = ROLE_HIVE + false_report_weight = 5 + protected_jobs = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain") + restricted_jobs = list("Cyborg","AI") + required_players = 25 + required_enemies = 2 + recommended_enemies = 3 + reroll_friendly = 1 + enemy_minimum_age = 0 + + announce_span = "danger" + announce_text = "The hosts of several psionic hiveminds have infiltrated the station and are looking to assimilate the crew!\n\ + Hosts: Expand your hivemind and complete your objectives at all costs!\n\ + Crew: Prevent the hosts from getting into your mind!" + + var/list/hosts = list() + +/proc/is_hivehost(mob/living/M) + return M && M.mind && M.mind.has_antag_datum(/datum/antagonist/hivemind) + +/proc/is_hivemember(mob/living/M) + if(!M) + return FALSE + for(var/datum/antagonist/hivemind/H in GLOB.antagonists) + if(H.hivemembers.Find(M)) + return TRUE + return FALSE + +/proc/remove_hivemember(mob/living/M) //Removes somebody from all hives as opposed to the antag proc remove_from_hive() + if(!M) + return + for(var/datum/antagonist/hivemind/H in GLOB.antagonists) + if(H.hivemembers.Find(M)) + H.hivemembers -= M + H.calc_size() + +/datum/game_mode/hivemind/pre_setup() + + if(CONFIG_GET(flag/protect_roles_from_antagonist)) + restricted_jobs += protected_jobs + + if(CONFIG_GET(flag/protect_assistant_from_antagonist)) + restricted_jobs += "Assistant" + + var/num_hosts = max( 1 , rand(0,1) + min(5, round(num_players() / 15) ) ) //1 host for every 15 players up to 75, with a 50% chance of an extra + + for(var/j = 0, j < num_hosts, j++) + if (!antag_candidates.len) + break + var/datum/mind/host = antag_pick(antag_candidates) + hosts += host + host.special_role = ROLE_HIVE + host.restricted_roles = restricted_jobs + log_game("[key_name(host)] has been selected as a hivemind host") + antag_candidates.Remove(host) + + if(hosts.len < required_enemies) + setup_error = "Not enough host candidates" + return FALSE + else + return TRUE + + +/datum/game_mode/hivemind/post_setup() + for(var/datum/mind/i in hosts) + i.add_antag_datum(/datum/antagonist/hivemind) + return ..() + +/datum/game_mode/hivemind/generate_report() + return "Reports of psychic activity have been showing up in this sector, and we believe this may have to do with a containment breach on \[REDACTED\] last month \ + when a sapient hive intelligence displaying paranormal powers escaped into the unknown. They present a very large risk as they can assimilate people into \ + the hivemind with ease, although they appear unable to affect mindshielded personnel." \ No newline at end of file diff --git a/code/game/gamemodes/hivemind/objectives.dm b/code/game/gamemodes/hivemind/objectives.dm new file mode 100644 index 000000000000..6a306a9347b9 --- /dev/null +++ b/code/game/gamemodes/hivemind/objectives.dm @@ -0,0 +1,60 @@ +/datum/objective/hivemind + +/datum/objective/hivemind/hivesize + explanation_text = "This is a bug. Error:HIVE2" + target_amount = 10 + +/datum/objective/hivemind/hivesize/New() + target_amount = ( max(8, round(GLOB.joined_player_list.len/3)) + rand(0,3) ) + update_explanation_text() + +/datum/objective/hivemind/hivesize/update_explanation_text() + explanation_text = "End the round with at least [target_amount] beings assimilated into the hive." + +/datum/objective/hivemind/hivesize/check_completion() + var/datum/antagonist/hivemind/host = owner.has_antag_datum(/datum/antagonist/hivemind) + if(!host) + return FALSE + return host.hive_size >= target_amount + +/datum/objective/hivemind/hiveescape + explanation_text = "This is a bug. Error:HIVE2" + target_amount = 10 + +/datum/objective/hivemind/hiveescape/New() + target_amount = ( max(5, round(GLOB.joined_player_list.len/6)) + rand(0,2) ) + update_explanation_text() + +/datum/objective/hivemind/hiveescape/update_explanation_text() + explanation_text = "Have at least [target_amount] members of the hive escape on the shuttle alive and free." + +/datum/objective/hivemind/hiveescape/check_completion() + var/count = 0 + var/datum/antagonist/hivemind/host = owner.has_antag_datum(/datum/antagonist/hivemind) + if(!host) + return FALSE + for(var/mob/living/L in host.hivemembers) + var/datum/mind/M = L.mind + if(M) + if(considered_escaped(M)) + count++ + return count >= target_amount + +/datum/objective/hivemind/assimilate + explanation_text = "This is a bug. Error:HIVE3" + +/datum/objective/hivemind/assimilate/update_explanation_text() + if(target) + explanation_text = "Assimilate [target.name] into the hive and ensure they survive." + else + explanation_text = "Free Objective." + +/datum/objective/hivemind/assimilate/check_completion() + var/datum/antagonist/hivemind/host = owner.has_antag_datum(/datum/antagonist/hivemind) + if(!host) + return FALSE + for(var/mob/living/L in host.hivemembers) + var/datum/mind/M = L.mind + if(M == target) + return considered_alive(target) + return FALSE \ No newline at end of file diff --git a/code/game/objects/items/implants/implant_mindshield.dm b/code/game/objects/items/implants/implant_mindshield.dm index 2f76b0cdc342..af2e7ffe765b 100644 --- a/code/game/objects/items/implants/implant_mindshield.dm +++ b/code/game/objects/items/implants/implant_mindshield.dm @@ -26,6 +26,16 @@ if(target.mind.has_antag_datum(/datum/antagonist/brainwashed)) target.mind.remove_antag_datum(/datum/antagonist/brainwashed) + if(is_hivemember(target)) + var/warning = "" + for(var/datum/antagonist/hivemind/hive in GLOB.antagonists) + if(hive.hivemembers.Find(target)) + var/hive_name = hive.get_real_name() + if(hive_name) + warning += "[hive_name]. " + to_chat(target, "You hear supernatural wailing echo throughout your mind. If you listen closely you can hear... [warning]Are those... names?") + remove_hivemember(target) + if(target.mind.has_antag_datum(/datum/antagonist/rev/head) || target.mind.unconvertable) if(!silent) target.visible_message("[target] seems to resist the implant!", "You feel something interfering with your mental conditioning, but you resist it!") diff --git a/code/modules/antagonists/hivemind/hivemind.dm b/code/modules/antagonists/hivemind/hivemind.dm new file mode 100644 index 000000000000..c7dca50044e4 --- /dev/null +++ b/code/modules/antagonists/hivemind/hivemind.dm @@ -0,0 +1,187 @@ +/datum/antagonist/hivemind + name = "Hivemind Host" + roundend_category = "hiveminds" + antagpanel_category = "Hivemind Host" + job_rank = ROLE_HIVE + antag_moodlet = /datum/mood_event/focused + var/special_role = ROLE_HIVE + var/list/hivemembers = list() + var/hive_size = 0 + + var/list/upgrade_tiers = list( + //Tier 1 + /obj/effect/proc_holder/spell/target_hive/hive_add = 0, + /obj/effect/proc_holder/spell/target_hive/hive_remove = 0, + /obj/effect/proc_holder/spell/target_hive/hive_see = 0, + /obj/effect/proc_holder/spell/target_hive/hive_shock = 2, + /obj/effect/proc_holder/spell/self/hive_drain = 4, + //Tier 2 + /obj/effect/proc_holder/spell/target_hive/hive_warp = 6, + /obj/effect/proc_holder/spell/targeted/hive_hack = 8, + /obj/effect/proc_holder/spell/target_hive/hive_control = 10, + /obj/effect/proc_holder/spell/targeted/induce_panic = 12, + //Tier 3 + /obj/effect/proc_holder/spell/self/hive_loyal = 15, + /obj/effect/proc_holder/spell/targeted/hive_assim = 18, + /obj/effect/proc_holder/spell/targeted/forcewall/hive = 20, + /obj/effect/proc_holder/spell/target_hive/hive_attack = 25) + +/datum/antagonist/hivemind/proc/calc_size() + listclearnulls(hivemembers) + var/old_size = hive_size + hive_size = hivemembers.len + if(hive_size != old_size) + check_powers() + +/datum/antagonist/hivemind/proc/check_powers() + for(var/power in upgrade_tiers) + var/level = upgrade_tiers[power] + if(hive_size >= level && !(locate(power) in owner.spell_list)) + owner.AddSpell(new power(null)) + else if(hive_size < level && (locate(power) in owner.spell_list)) + owner.RemoveSpell(power) + + +/datum/antagonist/hivemind/proc/get_real_name() //Gets the real name of the host, even if they're temporarily in another one + var/obj/effect/proc_holder/spell/target_hive/hive_control/the_spell = locate(/obj/effect/proc_holder/spell/target_hive/hive_control) in owner.spell_list + var/datum/mind/M = owner + if(M) + var/mob/living/L = owner.current + if(L) + if(the_spell && the_spell.active) + if(the_spell.original_body) + return the_spell.original_body.real_name + return L.real_name + return "" + +/datum/antagonist/hivemind/proc/add_to_hive(var/mob/living/carbon/human/H) + var/warning = "We detect a surge of psionic energy from [H.real_name] before they disappear from the hive. An enemy host, or simply a stolen vessel?" + var/user_warning = "We have detected an enemy hivemind using our physical form as a vessel and have begun ejecting their mind! They will be alerted of our disappearance once we succeed!" + for(var/datum/antagonist/hivemind/enemy_hive in GLOB.antagonists) + if(H.mind == enemy_hive.owner) + var/eject_time = rand(1400,1600) //2.5 minutes +- 10 seconds + addtimer(CALLBACK(GLOBAL_PROC, /proc/to_chat, H, user_warning), rand(500,1300)) // If the host has assimilated an enemy hive host, alert the enemy before booting them from the hive after a short while + addtimer(CALLBACK(GLOBAL_PROC, /proc/to_chat, owner, warning), eject_time) //As well as the host who just added them as soon as they're ejected + addtimer(CALLBACK(GLOBAL_PROC, /proc/remove_hivemember, H), eject_time) + hivemembers |= H + calc_size() + +/datum/antagonist/hivemind/proc/remove_from_hive(var/mob/living/carbon/human/H) + hivemembers -= H + calc_size() + +/datum/antagonist/hivemind/proc/destroy_hive() + hivemembers = list() + calc_size() + +/datum/antagonist/hivemind/antag_panel_data() + return "Vessels Assimilated: [hive_size]" + +/datum/antagonist/hivemind/on_gain() + + owner.special_role = special_role + check_powers() + forge_objectives() + ..() + +/datum/antagonist/hivemind/apply_innate_effects() + if(owner.assigned_role == "Clown") + var/mob/living/carbon/human/traitor_mob = owner.current + if(traitor_mob && istype(traitor_mob)) + if(!silent) + to_chat(traitor_mob, "The great psionic powers of the Hive lets you overcome your clownish nature, allowing you to wield weapons with impunity.") + traitor_mob.dna.remove_mutation(CLOWNMUT) + var/datum/atom_hud/antag/hud = GLOB.huds[ANTAG_HUD_HIVE] + hud.join_hud(owner.current) + set_antag_hud(owner.current, "hivemind") + +/datum/antagonist/hivemind/remove_innate_effects() + if(owner.assigned_role == "Clown") + var/mob/living/carbon/human/traitor_mob = owner.current + if(traitor_mob && istype(traitor_mob)) + traitor_mob.dna.add_mutation(CLOWNMUT) + var/datum/atom_hud/antag/hud = GLOB.huds[ANTAG_HUD_HIVE] + hud.leave_hud(owner.current) + set_antag_hud(owner.current, null) + + + +/datum/antagonist/hivemind/on_removal() + + //Remove all hive powers here + hive_size = -1 + check_powers() + + if(!silent && owner.current) + to_chat(owner.current," Your psionic powers fade, you are no longer the hivemind's host! ") + owner.special_role = null + ..() + +/datum/antagonist/hivemind/proc/forge_objectives() + if(prob(65)) + var/datum/objective/hivemind/hivesize/size_objective = new + size_objective.owner = owner + objectives += size_objective + else + var/datum/objective/hivemind/hiveescape/hive_escape_objective = new + hive_escape_objective.owner = owner + objectives += hive_escape_objective + if(prob(50)) + var/datum/objective/hivemind/assimilate/assim_objective = new + assim_objective.owner = owner + if(prob(25)) //Decently high chance to have to assimilate an implanted crew member + assim_objective.find_target_by_role(pick("Captain","Head of Security","Security Officer","Detective","Warden")) + if(!assim_objective.target) //If the prob doesn't happen or there are no implanted crew, find any target + assim_objective.find_target() + assim_objective.update_explanation_text() + objectives += assim_objective + else if(prob(70)) + var/datum/objective/assassinate/kill_objective = new + kill_objective.owner = owner + kill_objective.find_target() + objectives += kill_objective + else + var/datum/objective/maroon/maroon_objective = new + maroon_objective.owner = owner + maroon_objective.find_target() + objectives += maroon_objective + + var/datum/objective/escape/escape_objective = new + escape_objective.owner = owner + objectives += escape_objective + + return + +/datum/antagonist/hivemind/greet() + to_chat(owner.current, "You are the host of a powerful Hivemind.") + to_chat(owner.current, "Your psionic powers will grow by assimilating the crew into your hive. Use the Assimilate Vessel spell on a stationary \ + target, and after ten seconds he will be one of the hive. This is completely silent and safe to use, and failing will reset the cooldown. As \ + you assimilate the crew, you will gain more powers to use. Most are silent and won't help you in a fight, but grant you great power over your \ + vessels. There are other hiveminds onboard the station, collaboration is possible, but a strong enough hivemind can reap many rewards from a \ + well planned betrayal.") + owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/tatoralert.ogg', 100, FALSE, pressure_affected = FALSE) + + owner.announce_objectives() + +/datum/antagonist/hivemind/roundend_report() + var/list/result = list() + + result += printplayer(owner) + result += "Hive Size: [hive_size]" + var/greentext = TRUE + if(objectives) + result += printobjectives(objectives) + for(var/datum/objective/objective in objectives) + if(!objective.check_completion()) + greentext = FALSE + break + + if(objectives.len == 0 || greentext) + result += "The [name] was successful!" + else + result += "The [name] has failed!" + + return result.Join("
") + +/datum/antagonist/hivemind/is_gamemode_hero() + return SSticker.mode.name == "hivemind" diff --git a/code/modules/mob/living/carbon/human/death.dm b/code/modules/mob/living/carbon/human/death.dm index 0b4d5f609809..6ea3c6664816 100644 --- a/code/modules/mob/living/carbon/human/death.dm +++ b/code/modules/mob/living/carbon/human/death.dm @@ -40,6 +40,11 @@ SSblackbox.ReportDeath(src) if(is_devil(src)) INVOKE_ASYNC(is_devil(src), /datum/antagonist/devil.proc/beginResurrectionCheck, src) + if(is_hivemember(src)) + remove_hivemember(src) + if(is_hivehost(src)) + var/datum/antagonist/hivemind/hive = mind.has_antag_datum(/datum/antagonist/hivemind) + hive.destroy_hive() /mob/living/carbon/human/proc/makeSkeleton() add_trait(TRAIT_DISFIGURED, TRAIT_GENERIC) diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index d7cd6dd90894..c8606bea4d96 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -73,7 +73,9 @@ if(changeling) stat("Chemical Storage", "[changeling.chem_charges]/[changeling.chem_storage]") stat("Absorbed DNA", changeling.absorbedcount) - + var/datum/antagonist/hivemind/hivemind = mind.has_antag_datum(/datum/antagonist/hivemind) + if(hivemind) + stat("Hivemind Vessels", hivemind.hive_size) //NINJACODE if(istype(wear_suit, /obj/item/clothing/suit/space/space_ninja)) //Only display if actually a ninja. diff --git a/code/modules/research/nanites/nanite_programs/buffing.dm b/code/modules/research/nanites/nanite_programs/buffing.dm index a26cb7bd6847..5f17845067e4 100644 --- a/code/modules/research/nanites/nanite_programs/buffing.dm +++ b/code/modules/research/nanites/nanite_programs/buffing.dm @@ -120,7 +120,7 @@ /datum/nanite_program/mindshield/enable_passive_effect() . = ..() - if(!host_mob.mind.has_antag_datum(/datum/antagonist/rev)) //won't work if on a rev, to avoid having implanted revs + if(!host_mob.mind.has_antag_datum(/datum/antagonist/rev) && !is_hivemember(host_mob)) //won't work if on a rev, to avoid having implanted revs. same applies for hivemind members. host_mob.add_trait(TRAIT_MINDSHIELD, "nanites") host_mob.sec_hud_set_implants() diff --git a/code/modules/spells/spell_types/hivemind.dm b/code/modules/spells/spell_types/hivemind.dm new file mode 100644 index 000000000000..258bbd9b9b4e --- /dev/null +++ b/code/modules/spells/spell_types/hivemind.dm @@ -0,0 +1,636 @@ +/obj/effect/proc_holder/spell/target_hive + panel = "Hivemind Abilities" + invocation_type = "none" + selection_type = "range" + action_icon = 'icons/mob/actions/actions_hive.dmi' + action_background_icon_state = "bg_hive" + action_icon_state = "spell_default" + clothes_req = 0 + human_req = 1 + range = 0 //SNOWFLAKE, 0 is unlimited for target_external=0 spells + var/target_external = 0 //Whether or not we select targets inside or outside of the hive + + +/obj/effect/proc_holder/spell/target_hive/choose_targets(mob/user = usr) + var/datum/antagonist/hivemind/hive = user.mind.has_antag_datum(/datum/antagonist/hivemind) + if(!hive) + to_chat(user, "This is a bug. Error:HIVE1") + return + var/list/possible_targets = list() + var/list/targets = list() + + if(target_external) + for(var/mob/living/carbon/human/H in view_or_range(range, user, selection_type)) + if(user == H) + continue + if(!can_target(H)) + continue + if(!hive.hivemembers.Find(H)) + possible_targets += H + else + possible_targets = hive.hivemembers.Copy() + if(range) + possible_targets &= view_or_range(range, user, selection_type) + + var/mob/living/carbon/human/H = input("Choose the target for the spell.", "Targeting") as null|mob in possible_targets + if(!H) + revert_cast() + return + targets += H + perform(targets,user=user) + +/obj/effect/proc_holder/spell/target_hive/hive_add + name = "Assimilate Vessel" + desc = "We silently add an unsuspecting target to the hive." + selection_type = "view" + action_icon_state = "add" + + charge_max = 150 + range = 4 + target_external = 1 + var/ignore_mindshield = FALSE + +/obj/effect/proc_holder/spell/target_hive/hive_add/cast(list/targets, mob/living/user = usr) + var/mob/living/carbon/human/target = targets[1] + var/datum/antagonist/hivemind/hive = user.mind.has_antag_datum(/datum/antagonist/hivemind) + var/success = FALSE + + if(target.mind && target.client && target.stat != DEAD) + if(!target.has_trait(TRAIT_MINDSHIELD) || ignore_mindshield) + if(target.has_trait(TRAIT_MINDSHIELD) && ignore_mindshield) + to_chat(user, "We bruteforce our way past the mental barriers of [target.name] and begin linking our minds!") + else + to_chat(user, "We begin linking our mind with [target.name]!") + if(do_mob(user,user,50)) + if((target in view(range))) + to_chat(user, "[target.name] was added to the Hive!") + success = TRUE + hive.add_to_hive(target) + if(ignore_mindshield) + SEND_SIGNAL(target, COMSIG_NANITE_SET_VOLUME, 0) + for(var/obj/item/implant/mindshield/M in target.implants) + qdel(M) + else + to_chat(user, "[target.name] is too far away to assimilate!") + else + to_chat(user, "We fail to connect to [target.name].") + else + to_chat(user, "Powerful technology protects [target.name]'s mind.") + else + to_chat(user, "We detect no neural activity in this body.") + if(!success) + revert_cast() + +/obj/effect/proc_holder/spell/target_hive/hive_remove + name = "Release Vessel" + desc = "We silently remove a nearby target from the hive. We must be close to their body to do so." + selection_type = "view" + action_icon_state = "remove" + + charge_max = 100 + range = 4 + +/obj/effect/proc_holder/spell/target_hive/hive_remove/cast(list/targets, mob/living/user = usr) + var/mob/living/carbon/human/target = targets[1] + + var/datum/antagonist/hivemind/hive = user.mind.has_antag_datum(/datum/antagonist/hivemind) + if(!hive) + to_chat(user, "This is a bug. Error:HIVE1") + return + hive.hivemembers -= target + hive.calc_size() + to_chat(user, "We remove [target.name] from the hive") + +/obj/effect/proc_holder/spell/target_hive/hive_see + name = "Hive Vision" + desc = "We use the eyes of one of our vessels. Use again to look through our own eyes once more." + action_icon_state = "see" + var/mob/vessel + var/mob/living/host //Didn't really have any other way to auto-reset the perspective if the other mob got qdeled + + charge_max = 50 + +/obj/effect/proc_holder/spell/target_hive/hive_see/on_lose(mob/living/user) + user.reset_perspective() + +/obj/effect/proc_holder/spell/target_hive/hive_see/cast(list/targets, mob/living/user = usr) + if(!active) + vessel = targets[1] + if(vessel) + user.reset_perspective(vessel) + active = TRUE + host = user + revert_cast() + else + user.reset_perspective() + active = FALSE + +/obj/effect/proc_holder/spell/target_hive/hive_see/process() + if(active && (!vessel || !is_hivemember(vessel) || QDELETED(vessel))) + to_chat(host, "Our vessel is one of us no more!") + host.reset_perspective() + active = FALSE + ..() + +/obj/effect/proc_holder/spell/target_hive/hive_see/choose_targets(mob/user = usr) + if(!active) + ..() + else + perform(,user) + +/obj/effect/proc_holder/spell/target_hive/hive_shock + name = "Neural Shock" + desc = "After a short charging time, we overload the mind of one of our vessels with psionic energy, rendering them unconscious for a short period of time. This power weakens over distance, but strengthens with hive size." + action_icon_state = "shock" + + charge_max = 600 + +/obj/effect/proc_holder/spell/target_hive/hive_shock/cast(list/targets, mob/living/user = usr) + var/mob/living/carbon/human/target = targets[1] + var/datum/antagonist/hivemind/hive = user.mind.has_antag_datum(/datum/antagonist/hivemind) + if(!hive) + to_chat(user, "This is a bug. Error:HIVE1") + return + to_chat(user, "We begin increasing the psionic bandwidth between ourself and the vessel!") + if(do_mob(user,user,60)) + var/power = 120-get_dist(user, target) + if(!is_hivehost(target)) + switch(hive.hive_size) + if(0 to 4) + if(5 to 9) + power *= 1.5 + if(10 to 14) + power *= 2 + if(15 to 19) + power *= 2.5 + else + power *= 3 + if(power > 50 && user.z == target.z) + to_chat(target, "You feel a sharp pain, and a foreign presence in your mind!!") + to_chat(user, "We have overloaded the vessel for a short time!") + target.Jitter(round(power/10)) + target.Unconscious(power) + else + to_chat(user, "The vessel was too far away to be affected!") + else + to_chat(user, "Our concentration has been broken!") + revert_cast() + +/obj/effect/proc_holder/spell/self/hive_drain + name = "Repair Protocol" + desc = "Our many vessels sacrifice a small portion of their mind's vitality to cure us of our physical and mental ailments." + + panel = "Hivemind Abilities" + charge_max = 600 + clothes_req = 0 + invocation_type = "none" + action_icon = 'icons/mob/actions/actions_hive.dmi' + action_background_icon_state = "bg_hive" + action_icon_state = "drain" + human_req = 1 + +/obj/effect/proc_holder/spell/self/hive_drain/cast(mob/living/carbon/human/user) + var/datum/antagonist/hivemind/hive = user.mind.has_antag_datum(/datum/antagonist/hivemind) + if(!hive) + return + var/iterations = 0 + var/power = 5 + + if(!user.getBruteLoss() && !user.getFireLoss() && !user.getCloneLoss() && !user.getBrainLoss()) + to_chat(user, "We cannot heal ourselves any more with this power!") + revert_cast() + to_chat(user, "We begin siphoning power from our many vessels!") + while(iterations < 7) + var/mob/living/carbon/human/target = pick(hive.hivemembers) + if(!target) + break + if(!do_mob(user,user,15)) + to_chat(user, "Our concentration has been broken!") + break + target.adjustBrainLoss(5) + power = max(5-(round(get_dist(user, target)/40)),2) + if(user.getBruteLoss() > user.getFireLoss()) + user.heal_ordered_damage(power, list(CLONE, BRUTE, BURN)) + else + user.heal_ordered_damage(power, list(CLONE, BURN, BRUTE)) + if(!user.getBruteLoss() && !user.getFireLoss() && !user.getCloneLoss()) //If we don't have any of these, stop looping + to_chat(user, "We finish our healing") + break + iterations++ + user.setBrainLoss(0) + + +/mob/living/passenger + name = "mind control victim" + real_name = "unknown conscience" + +/mob/living/passenger/say(message, bubble_type, var/list/spans = list(), sanitize = TRUE, datum/language/language = null, ignore_spam = FALSE, forced = null) + to_chat(src, "You find yourself unable to speak, you aren't in control of your body!") + return FALSE + +/mob/living/passenger/emote(act, m_type = null, message = null, intentional = FALSE) + to_chat(src, "You find yourself unable to emote, you aren't in control of your body!") + return + +/obj/effect/proc_holder/spell/target_hive/hive_control + name = "Mind Control" + desc = "We assume direct control of one of our vessels, leaving our current body for up to ten seconds, although a larger hive may be able to sustain it for up to two minutes. Powers can be used via our vessel, although if it dies, the entire hivemind will come down with it." + charge_max = 600 + action_icon_state = "force" + active = FALSE + var/mob/living/carbon/human/original_body //The original hivemind host + var/mob/living/carbon/human/vessel + var/mob/living/passenger/backseat //Storage for the mind controlled vessel + var/power = 100 + var/time_initialized = 0 + +/obj/effect/proc_holder/spell/target_hive/hive_control/proc/release_control() //If the spell is active, force everybody into their original bodies if they exist, ghost them otherwise, delete the backseat + if(!active) + return + active = FALSE + charge_counter = max((0.5-(world.time-time_initialized)/power)*charge_max, 0) //Partially refund the power based on how long it was used, up to a max of half the charge time + + if(!QDELETED(vessel)) + if(vessel.mind) + if(QDELETED(original_body)) + vessel.ghostize(0) + else + vessel.mind.transfer_to(original_body, 1) + + if(!QDELETED(backseat) && backseat.mind) + if(QDELETED(vessel)) + backseat.ghostize(0) + else + backseat.mind.transfer_to(vessel,1) + + message_admins("[ADMIN_LOOKUPFLW(vessel)] is no longer being controlled by [ADMIN_LOOKUPFLW(original_body)] (Hivemind Host).") + log_game("[key_name(vessel)] was released from Mind Control by [key_name(original_body)].") + + QDEL_NULL(backseat) + +/obj/effect/proc_holder/spell/target_hive/hive_control/on_lose(mob/user) + release_control() + +/obj/effect/proc_holder/spell/target_hive/hive_control/cast(list/targets, mob/living/user = usr) + if(!active) + vessel = targets[1] + var/datum/antagonist/hivemind/hive = user.mind.has_antag_datum(/datum/antagonist/hivemind) + if(!hive) + to_chat(user, "This is a bug. Error:HIVE1") + return + switch(hive.hive_size) + if(10 to 14) + power = 100 + charge_max = 600 + if(15 to 19) + power = 300 + charge_max = 900 + if(20 to 24) + power = 600 + charge_max = 1200 + else + power = 1200 + charge_max = 1200 + for(var/datum/antagonist/hivemind/H in GLOB.antagonists) + if(H.owner == user.mind) + continue + if(H.hivemembers.Find(vessel)) + to_chat(user, "We have detected a foreign presence within this mind, it would be unwise to merge so intimately with it.") + revert_cast() + return + original_body = user + vessel = targets[1] + to_chat(user, "We begin merging our mind with [vessel.name].") + if(!do_mob(user,user,50)) + to_chat(user, "We fail to assume control of the target.") + revert_cast() + return + if(user.z != vessel.z) + to_chat(user, "Our vessel is too far away to control.") + revert_cast() + return + backseat = new /mob/living/passenger() + if(vessel && vessel.mind && backseat) + var/obj/effect/proc_holder/spell/target_hive/hive_see/the_spell = locate(/obj/effect/proc_holder/spell/target_hive/hive_see) in user.mind.spell_list + if(the_spell && the_spell.active) //Uncast Hive Sight just to make things easier when casting during mind control + the_spell.perform(,user) + + message_admins("[ADMIN_LOOKUPFLW(vessel)] has been temporarily taken over by [ADMIN_LOOKUPFLW(user)] (Hivemind Host).") + log_game("[key_name(vessel)] was Mind Controlled by [key_name(user)].") + + original_body = user + backseat.loc = vessel + backseat.name = vessel.real_name + backseat.real_name = vessel.real_name + vessel.mind.transfer_to(backseat, 1) + user.mind.transfer_to(vessel, 1) + active = TRUE + time_initialized = world.time + revert_cast() + if(do_mob(user,user,power)) + to_chat(vessel, "We cannot sustain the mind control any longer and release control!") + else + to_chat(vessel, "Our body has been disturbed, interrupting the mind control!") + release_control() + else + to_chat(usr, "We detect no neural activity in our vessel!") + revert_cast() + else + release_control() + +/obj/effect/proc_holder/spell/target_hive/hive_control/process() + if(active) + if(QDELETED(vessel)) //If we've been gibbed or otherwise deleted, ghost both of them and kill the original + original_body.adjustBrainLoss(200) + release_control() + else if(!is_hivemember(vessel)) //If the vessel is no longer a hive member, return to original bodies + to_chat(vessel, "Our vessel is one of us no more!") + release_control() + else if(!QDELETED(original_body) && (!vessel.ckey || vessel.stat == DEAD)) //If the original body exists and the vessel is dead/ghosted, return both to body but not before killing the original + original_body.adjustBrainLoss(200) + to_chat(vessel.mind, "Our vessel is one of us no more!") + release_control() + else if(!QDELETED(original_body) && original_body.z != vessel.z) //Return to original bodies + release_control() + to_chat(original_body, "Our vessel is too far away to control!") + if(QDELETED(original_body) || original_body.stat == DEAD) //Return vessel to its body, either return or ghost the original + to_chat(vessel, "Our body has been destroyed, the hive cannot survive without its host!") + release_control() + ..() + +/obj/effect/proc_holder/spell/target_hive/hive_control/choose_targets(mob/user = usr) + if(!active) + ..() + else + perform(,user) + +/obj/effect/proc_holder/spell/targeted/induce_panic + name = "Induce Panic" + desc = "We unleash a burst of psionic energy, inducing a debilitating fear in those around us and reducing their combat readiness. Mindshielded foes have a chance to resist this power." + panel = "Hivemind Abilities" + charge_max = 900 + range = 7 + invocation_type = "none" + clothes_req = 0 + max_targets = 0 + action_icon = 'icons/mob/actions/actions_hive.dmi' + action_background_icon_state = "bg_hive" + action_icon_state = "panic" + +/obj/effect/proc_holder/spell/targeted/induce_panic/cast(list/targets, mob/living/user = usr) + var/datum/antagonist/hivemind/hive = user.mind.has_antag_datum(/datum/antagonist/hivemind) + if(!hive) + to_chat(user, "This is a bug. Error:HIVE1") + return + for(var/mob/living/carbon/human/target in targets) + if(target.has_trait(TRAIT_MINDSHIELD) && prob(50-hive.hive_size)) //Mindshielded targets resist panic pretty well + continue + if(target.stat == DEAD) + continue + target.Jitter(14) + target.adjustStaminaLoss(min(35,hive.hive_size)) + if(prob(50)) + var/text = pick(";HELP!","I'm losing control of the situation!!","Get me outta here!") + target.say(text, forced = "panic") + var/effect = rand(1,4) + switch(effect) + if(1) + to_chat(target, "You panic and drop everything to the ground!") + target.drop_all_held_items() + if(2) + to_chat(target, "You panic and flail around!") + target.click_random_mob() + addtimer(CALLBACK(target, "click_random_mob"), 5) + addtimer(CALLBACK(target, "click_random_mob"), 10) + addtimer(CALLBACK(target, "click_random_mob"), 15) + addtimer(CALLBACK(target, "click_random_mob"), 20) + target.Dizzy(2) + if(3) + to_chat(target, "You freeze up in fear!") + target.Stun(70) + if(4) + to_chat(target, "You feel nauseous as dread washes over you!") + target.Dizzy(15) + target.adjustStaminaLoss(45) + target.hallucination += 45 + +/obj/effect/proc_holder/spell/target_hive/hive_attack + name = "Medullary Failure" + desc = "We overload the target's medulla, inducing an immediate heart attack." + + charge_max = 3000 + action_icon_state = "attack" + +/obj/effect/proc_holder/spell/target_hive/hive_attack/cast(list/targets, mob/living/user = usr) + var/mob/living/carbon/human/target = targets[1] + if(!target.undergoing_cardiac_arrest() && target.can_heartattack()) + target.set_heartattack(TRUE) + to_chat(target, "You feel a sharp pain, and foreign presence in your mind!!") + to_chat(user, "We have overloaded the vessel's medulla! Without medical attention, they will shortly die.") + if(target.stat == CONSCIOUS) + target.visible_message("[target] clutches at [target.p_their()] chest as if [target.p_their()] heart stopped!") + else + to_chat(user, "We are unable to induce a heart attack!") + +/obj/effect/proc_holder/spell/target_hive/hive_warp + name = "Distortion Field" + desc = "We warp reality surrounding a vessel, causing hallucinations in everybody around them. This power gets weaker over distance but increases in strength the larger our hive is, and will eventually weaken those caught within the field." + + charge_max = 900 + action_icon_state = "warp" + +/obj/effect/proc_holder/spell/target_hive/hive_warp/cast(list/targets, mob/living/user = usr) + var/mob/living/carbon/human/target = targets[1] + var/datum/antagonist/hivemind/hive = user.mind.has_antag_datum(/datum/antagonist/hivemind) + if(!hive) + to_chat(user, "This is a bug. Error:HIVE1") + return + var/power = 50-round(get_dist(user, target)/2) + if(power < 10 || target.z != user.z) + to_chat(user, "We are too far away from [target.name] to affect them!") + return + to_chat(user, "We succesfuly distort reality surrounding [target.name]!") + switch(hive.hive_size) + if(5 to 9) + if(10 to 14) + power *= 1.5 + if(15 to 19) + power *= 2 + if(20 to 24) + power *= 2.5 + else + power *= 3 + for(var/mob/living/carbon/human/victim in view(7,target)) + if(user == victim) + continue + victim.hallucination += power + victim.adjustStaminaLoss(max(0,power-100)) + +/obj/effect/proc_holder/spell/targeted/hive_hack + name = "Network Invasion" + desc = "We probe the mind of an adjacent target and extract valuable information on any enemy hives they may belong to. Takes longer if the target is not in our hive." + panel = "Hivemind Abilities" + charge_max = 600 + range = 1 + invocation_type = "none" + clothes_req = 0 + max_targets = 1 + action_icon = 'icons/mob/actions/actions_hive.dmi' + action_background_icon_state = "bg_hive" + action_icon_state = "hack" + +/obj/effect/proc_holder/spell/targeted/hive_hack/cast(list/targets, mob/living/user = usr) + var/datum/antagonist/hivemind/hive = user.mind.has_antag_datum(/datum/antagonist/hivemind) + if(!hive) + to_chat(user, "This is a bug. Error:HIVE1") + return + var/mob/living/carbon/human/target = targets[1] + var/in_hive = hive.hivemembers.Find(target) + var/list/enemies = list() + var/enemy_names = "" + + to_chat(user, "We begin probing [target.name]'s mind!") + if(do_mob(user,target,100)) + if(!in_hive) + to_chat(user, "Their mind slowly opens up to us.") + if(!do_mob(user,target,200)) + to_chat(user, "Our concentration has been broken!") + revert_cast() + return + for(var/datum/antagonist/hivemind/enemy in GLOB.antagonists) + var/datum/mind/M = enemy.owner + if(!M || M.current == user) + continue + if(enemy.hivemembers.Find(target)) + var/hive_name = enemy.get_real_name() + if(hive_name) + enemies += hive_name + enemy.remove_from_hive(target) + to_chat(M.current, "We detect a surge of psionic energy from [target.real_name] before they disappear from the hive. An enemy host, or simply a stolen vessel?") + if(enemy.owner == target) + user.Stun(70) + user.Jitter(14) + to_chat(user, "A sudden surge of psionic energy rushes into your mind, only a Hive host could have such power!!") + return + if(enemies.len) + enemy_names = enemies.Join(". ") + to_chat(user, "In a moment of clarity, we see all. Another hive. Faces. Our nemesis. [enemy_names]. They are watching us. They know we are coming.") + else + to_chat(user, "We peer into the inner depths of their mind and see nothing, no enemies lurk inside this mind.") + else + to_chat(user, "Our concentration has been broken!") + revert_cast() + +/obj/effect/proc_holder/spell/targeted/hive_assim + name = "Mass Assimilation" + desc = "Should we capture an enemy Hive host, we can assimilate their entire hive into ours. It is unlikely their mind will surive the ordeal." + panel = "Hivemind Abilities" + charge_max = 3000 + range = 1 + invocation_type = "none" + clothes_req = 0 + max_targets = 1 + action_icon = 'icons/mob/actions/actions_hive.dmi' + action_background_icon_state = "bg_hive" + action_icon_state = "assim" + +/obj/effect/proc_holder/spell/targeted/hive_assim/cast(list/targets, mob/living/user = usr) + var/datum/antagonist/hivemind/hive = user.mind.has_antag_datum(/datum/antagonist/hivemind) + if(!hive) + to_chat(user, "This is a bug. Error:HIVE1") + return + var/mob/living/carbon/human/target = targets[1] + + to_chat(user, "We tear into [target.name]'s mind with all our power!") + to_chat(target, "You feel an excruciating pain in your head!") + if(do_mob(user,target,150)) + if(!target.mind) + to_chat(user, "This being has no mind!") + revert_cast() + return + var/datum/antagonist/hivemind/enemy_hive = target.mind.has_antag_datum(/datum/antagonist/hivemind) + if(enemy_hive) + to_chat(user, "We begin assimilating every psionic link we can find!.") + to_chat(target, "Our grip on our mind is slipping!") + target.Jitter(14) + target.setBrainLoss(125) + if(do_mob(user,target,300)) + enemy_hive = target.mind.has_antag_datum(/datum/antagonist/hivemind) //Check again incase they lost it somehow + if(enemy_hive) + to_chat(user, "Ours. It is ours. Our mind has never been stronger, never been larger, never been mightier. And theirs is no more.") + to_chat(target, "Our vessels, they're! That's impossible! We can't... we can't... I can't...") + hive.hivemembers |= enemy_hive.hivemembers + enemy_hive.hivemembers = list() + hive.calc_size() + enemy_hive.calc_size() + target.setBrainLoss(200) + + message_admins("[ADMIN_LOOKUPFLW(target)] was killed and had their hive stolen by [ADMIN_LOOKUPFLW(user)].") + log_game("[key_name(target)] was killed via Mass Assimilation by [key_name(user)].") + else + to_chat(user, "It seems we have been mistaken, this mind is not the host of a hive.") + else + to_chat(user, "Our concentration has been broken, leaving our mind wide open for a counterattack!") + to_chat(target, "Their concentration has been broken... leaving them wide open for a counterattack!") + user.Unconscious(120) + user.adjustStaminaLoss(70) + user.Jitter(60) + else + to_chat(user, "We appear to have made a mistake... this mind is too weak to be the one we're looking for.") + else + to_chat(user, "Our concentration has been broken!") + revert_cast() + +/obj/effect/proc_holder/spell/self/hive_loyal + name = "Bruteforce" + desc = "Our ability to assimilate is temporarily boosted, allowing us to crush the technology shielding the minds of Security and Command personnel and assimilate them." + panel = "Hivemind Abilities" + charge_max = 1200 + invocation_type = "none" + clothes_req = 0 + action_icon = 'icons/mob/actions/actions_hive.dmi' + action_background_icon_state = "bg_hive" + action_icon_state = "loyal" + +/obj/effect/proc_holder/spell/self/hive_loyal/cast(mob/living/user = usr) + var/datum/antagonist/hivemind/hive = user.mind.has_antag_datum(/datum/antagonist/hivemind) + if(!hive) + to_chat(user, "This is a bug. Error:HIVE1") + return + var/obj/effect/proc_holder/spell/target_hive/hive_add/the_spell = locate(/obj/effect/proc_holder/spell/target_hive/hive_add) in user.mind.spell_list + if(the_spell) + the_spell.ignore_mindshield = TRUE + to_chat(user, "We prepare to crush mindshielding technology!") + addtimer(VARSET_CALLBACK(the_spell, ignore_mindshield, FALSE), 300) + addtimer(CALLBACK(GLOBAL_PROC, /proc/to_chat, user, "Our heightened power wears off, we are once again unable to assimilate mindshielded crew."), 300) + else + to_chat(user, "This is a bug. Error:HIVE5") + +/obj/effect/proc_holder/spell/targeted/forcewall/hive + name = "Telekinetic Field" + desc = "Our psionic powers form a barrier around us in the phsyical world that only we can pass through." + panel = "Hivemind Abilities" + charge_max = 600 + clothes_req = 0 + invocation_type = "none" + action_icon = 'icons/mob/actions/actions_hive.dmi' + action_background_icon_state = "bg_hive" + action_icon_state = "forcewall" + range = -1 + include_user = 1 + wall_type = /obj/effect/forcefield/wizard/hive + +/obj/effect/proc_holder/spell/targeted/forcewall/hive/cast(list/targets,mob/user = usr) + new wall_type(get_turf(user),user) + for(var/dir in GLOB.alldirs) + new wall_type(get_step(user, dir),user) + +/obj/effect/forcefield/wizard/hive + name = "Telekinetic Field" + desc = "A psychic barrier, usable by only the strongest of minds." + timeleft = 150 + +/obj/effect/forcefield/wizard/hive/CanPass(atom/movable/mover, turf/target) + if(mover == wizard) + return TRUE + return FALSE \ No newline at end of file diff --git a/icons/mob/actions/actions_hive.dmi b/icons/mob/actions/actions_hive.dmi new file mode 100644 index 000000000000..28e5ec10e136 Binary files /dev/null and b/icons/mob/actions/actions_hive.dmi differ diff --git a/icons/mob/actions/backgrounds.dmi b/icons/mob/actions/backgrounds.dmi index 4303c6fff6ca..edfeea004285 100644 Binary files a/icons/mob/actions/backgrounds.dmi and b/icons/mob/actions/backgrounds.dmi differ diff --git a/icons/mob/hud.dmi b/icons/mob/hud.dmi index 047f08094662..57ee9597222e 100644 Binary files a/icons/mob/hud.dmi and b/icons/mob/hud.dmi differ diff --git a/tgstation.dme b/tgstation.dme index a10b48fae368..60e9dbffac03 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -531,6 +531,8 @@ #include "code\game\gamemodes\devil\objectives.dm" #include "code\game\gamemodes\devil\devil agent\devil_agent.dm" #include "code\game\gamemodes\extended\extended.dm" +#include "code\game\gamemodes\hivemind\hivemind.dm" +#include "code\game\gamemodes\hivemind\objectives.dm" #include "code\game\gamemodes\meteor\meteor.dm" #include "code\game\gamemodes\meteor\meteors.dm" #include "code\game\gamemodes\monkey\monkey.dm" @@ -1254,6 +1256,7 @@ #include "code\modules\antagonists\ert\ert.dm" #include "code\modules\antagonists\greentext\greentext.dm" #include "code\modules\antagonists\highlander\highlander.dm" +#include "code\modules\antagonists\hivemind\hivemind.dm" #include "code\modules\antagonists\magic_servant\servant.dm" #include "code\modules\antagonists\monkey\monkey.dm" #include "code\modules\antagonists\morph\morph.dm" @@ -2576,6 +2579,7 @@ #include "code\modules\spells\spell_types\forcewall.dm" #include "code\modules\spells\spell_types\genetic.dm" #include "code\modules\spells\spell_types\godhand.dm" +#include "code\modules\spells\spell_types\hivemind.dm" #include "code\modules\spells\spell_types\infinite_guns.dm" #include "code\modules\spells\spell_types\inflict_handler.dm" #include "code\modules\spells\spell_types\knock.dm" diff --git a/yogstation.dme b/yogstation.dme index 5613c86dde4e..086ea8b90e2c 100644 --- a/yogstation.dme +++ b/yogstation.dme @@ -540,6 +540,8 @@ #include "code\game\gamemodes\devil\objectives.dm" #include "code\game\gamemodes\devil\devil agent\devil_agent.dm" #include "code\game\gamemodes\extended\extended.dm" +#include "code\game\gamemodes\hivemind\hivemind.dm" +#include "code\game\gamemodes\hivemind\objectives.dm" #include "code\game\gamemodes\meteor\meteor.dm" #include "code\game\gamemodes\meteor\meteors.dm" #include "code\game\gamemodes\monkey\monkey.dm" @@ -1259,6 +1261,7 @@ #include "code\modules\antagonists\ert\ert.dm" #include "code\modules\antagonists\greentext\greentext.dm" #include "code\modules\antagonists\highlander\highlander.dm" +#include "code\modules\antagonists\hivemind\hivemind.dm" #include "code\modules\antagonists\magic_servant\servant.dm" #include "code\modules\antagonists\monkey\monkey.dm" #include "code\modules\antagonists\morph\morph.dm" @@ -2568,6 +2571,7 @@ #include "code\modules\spells\spell_types\forcewall.dm" #include "code\modules\spells\spell_types\genetic.dm" #include "code\modules\spells\spell_types\godhand.dm" +#include "code\modules\spells\spell_types\hivemind.dm" #include "code\modules\spells\spell_types\infinite_guns.dm" #include "code\modules\spells\spell_types\inflict_handler.dm" #include "code\modules\spells\spell_types\knock.dm"