diff --git a/_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_elite_tumor.dmm b/_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_elite_tumor.dmm new file mode 100644 index 00000000000..62721ec4f3d --- /dev/null +++ b/_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_elite_tumor.dmm @@ -0,0 +1,132 @@ +//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE +"a" = ( +/turf/template_noop, +/area/template_noop) +"k" = ( +/obj/structure/elite_tumor, +/obj/effect/mapping_helpers/no_lava, +/turf/simulated/floor/plating/asteroid/basalt/lava_land_surface, +/area/lavaland/surface/outdoors) +"q" = ( +/obj/effect/decal/remains/human, +/obj/effect/mapping_helpers/no_lava, +/turf/simulated/floor/plating/asteroid/basalt/lava_land_surface, +/area/lavaland/surface/outdoors) +"u" = ( +/obj/effect/mapping_helpers/no_lava, +/turf/simulated/floor/plating/asteroid/basalt/lava_land_surface, +/area/lavaland/surface/outdoors) +"E" = ( +/obj/item/stack/ore/iron, +/obj/effect/mapping_helpers/no_lava, +/turf/simulated/floor/plating/asteroid/basalt/lava_land_surface, +/area/lavaland/surface/outdoors) +"M" = ( +/obj/item/stack/ore/glass/basalt, +/obj/effect/mapping_helpers/no_lava, +/turf/simulated/floor/plating/asteroid/basalt/lava_land_surface, +/area/lavaland/surface/outdoors) +"V" = ( +/obj/effect/mapping_helpers/no_lava, +/turf/template_noop, +/area/template_noop) + +(1,1,1) = {" +a +a +a +a +a +a +a +a +a +"} +(2,1,1) = {" +a +V +V +V +V +V +V +V +a +"} +(3,1,1) = {" +a +V +u +E +u +u +u +V +a +"} +(4,1,1) = {" +a +V +u +u +u +u +q +V +a +"} +(5,1,1) = {" +a +V +u +u +k +u +u +V +a +"} +(6,1,1) = {" +a +V +u +M +u +u +u +V +a +"} +(7,1,1) = {" +a +V +u +u +u +u +u +V +a +"} +(8,1,1) = {" +a +V +V +V +V +V +V +V +a +"} +(9,1,1) = {" +a +a +a +a +a +a +a +a +a +"} diff --git a/code/__DEFINES/role_preferences.dm b/code/__DEFINES/role_preferences.dm index b7bc7864e62..89847fe1246 100644 --- a/code/__DEFINES/role_preferences.dm +++ b/code/__DEFINES/role_preferences.dm @@ -41,6 +41,7 @@ #define ROLE_DEATHSQUAD "deathsquad" #define ROLE_EVENTMISC "eventmisc" #define ROLE_GHOST "ghost role" +#define ROLE_ELITE "lavaland elite" // Misc jobban categories #define ROLEBAN_AHUD "AntagHUD" @@ -64,6 +65,7 @@ GLOBAL_LIST_INIT(special_roles, list( ROLE_REV = /datum/game_mode/revolution, // Revolutionary ROLE_SENTIENT, // Sentient animal ROLE_DEMON, // Slaguther demon + ROLE_ELITE, // Lavaland Elite ROLE_NINJA, // Space ninja ROLE_TRADER, // Trader ROLE_TRAITOR = /datum/game_mode/traitor, // Traitor diff --git a/code/__DEFINES/rolebans.dm b/code/__DEFINES/rolebans.dm index 4ac9aac00f3..ec5dd31daa8 100644 --- a/code/__DEFINES/rolebans.dm +++ b/code/__DEFINES/rolebans.dm @@ -25,5 +25,6 @@ GLOBAL_LIST_INIT(other_roles, list( ROLE_ERT, ROLE_GHOST, ROLEBAN_AHUD, - ROLEBAN_RECORDS + ROLEBAN_RECORDS, + ROLE_ELITE )) diff --git a/code/__DEFINES/status_effects.dm b/code/__DEFINES/status_effects.dm index 602dd47b76a..e2ccb39f534 100644 --- a/code/__DEFINES/status_effects.dm +++ b/code/__DEFINES/status_effects.dm @@ -44,6 +44,7 @@ #define STATUS_EFFECT_CHAINSAW_SLAYING /datum/status_effect/chainsaw_slaying // Stun immunity, very slight damage resistance +#define STATUS_EFFECT_HOPE /datum/status_effect/hope // Healing that scales when injured (but not in deep crit), and nice messages to keep the hope ///////////// // DEBUFFS // diff --git a/code/__HELPERS/traits.dm b/code/__HELPERS/traits.dm index dcd979ee1fd..922200948bb 100644 --- a/code/__HELPERS/traits.dm +++ b/code/__HELPERS/traits.dm @@ -193,6 +193,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_NOPAIN "no_pain" #define TRAIT_FORCE_DOORS "force_doors" #define TRAIT_AI_UNTRACKABLE "AI_untrackable" +#define TRAIT_ELITE_CHALLENGER "elite_challenger" //***** ITEM TRAITS *****// /// Show what machine/door wires do when held. diff --git a/code/_globalvars/lists/objects.dm b/code/_globalvars/lists/objects.dm index ee2fb9b16a1..e0a881643a7 100644 --- a/code/_globalvars/lists/objects.dm +++ b/code/_globalvars/lists/objects.dm @@ -43,6 +43,7 @@ GLOBAL_LIST_INIT(meteor_list, list()) //list of all meteors GLOBAL_LIST_INIT(poi_list, list()) //list of points of interest for observe/follow GLOBAL_LIST_INIT(active_jammers, list()) // List of active radio jammers GLOBAL_LIST_EMPTY(ladders) +GLOBAL_LIST_EMPTY(mirrors) //list of all mirrors and mirror shields. GLOBAL_LIST_INIT(active_diseases, list()) //List of Active disease in all mobs; purely for quick referencing. diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm index d32ced24403..fa009ae3102 100644 --- a/code/_globalvars/traits.dm +++ b/code/_globalvars/traits.dm @@ -66,7 +66,8 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_NOPAIN" = TRAIT_NOPAIN, "TRAIT_FORCE_DOORS" = TRAIT_FORCE_DOORS, "TRAIT_EMOTE_MUTE" = TRAIT_EMOTE_MUTE, - "TRAIT_AI_UNTRACKABLE" = TRAIT_AI_UNTRACKABLE + "TRAIT_AI_UNTRACKABLE" = TRAIT_AI_UNTRACKABLE, + "TRAIT_ELITE_CHALLENGER" = TRAIT_ELITE_CHALLENGER ), /obj/item = list( "TRAIT_SHOW_WIRE_INFO" = TRAIT_SHOW_WIRE_INFO, diff --git a/code/datums/action.dm b/code/datums/action.dm index c6267235683..e2328f7ac99 100644 --- a/code/datums/action.dm +++ b/code/datums/action.dm @@ -518,6 +518,10 @@ name = "View Storage" +/datum/action/item_action/accessory/herald + name = "Mirror Walk" + desc = "Use near a mirror to enter it" + //Preset for spells /datum/action/spell_action check_flags = 0 diff --git a/code/datums/ruins/lavaland.dm b/code/datums/ruins/lavaland.dm index 66181ae282c..13f4a628d58 100644 --- a/code/datums/ruins/lavaland.dm +++ b/code/datums/ruins/lavaland.dm @@ -214,6 +214,14 @@ suffix = "lavaland_surface_puzzle.dmm" cost = 5 +/datum/map_template/ruin/lavaland/tumor + name = "Elite Tumor" + id = "tumor" + description = "The victor freed, the loser the next fighter. The ghosts, the endless spectators. And thus the cycle loops..." + suffix = "lavaland_surface_elite_tumor.dmm" + cost = 5 + always_place = TRUE + /datum/map_template/ruin/lavaland/monster_nest name = "Monster Nest" id = "monsternest" diff --git a/code/datums/status_effects/buffs.dm b/code/datums/status_effects/buffs.dm index 80e64bba326..77530cd2778 100644 --- a/code/datums/status_effects/buffs.dm +++ b/code/datums/status_effects/buffs.dm @@ -412,3 +412,51 @@ H.physiology.brute_mod /= 0.8 H.physiology.burn_mod /=0.8 H.physiology.stamina_mod /= 0.8 + +/datum/status_effect/hope + id = "hope" + duration = -1 + tick_interval = 2 SECONDS + status_type = STATUS_EFFECT_UNIQUE + alert_type = /obj/screen/alert/status_effect/hope + +/obj/screen/alert/status_effect/hope + name = "Hope." + desc = "A ray of hope beyond dispair." + icon_state = "hope" + +/datum/status_effect/hope/tick() + if(owner.stat == DEAD || owner.health <= HEALTH_THRESHOLD_DEAD) // No dead healing, or healing in dead crit + return + if(owner.health > 50) + if(prob(0.5)) + hope_message() + return + var/heal_multiplier = min(3, ((50 - owner.health) / 50 + 1)) // 1 hp at 50 health, 2 at 0, 3 at -50 + owner.adjustBruteLoss(-heal_multiplier * 0.5) + owner.adjustFireLoss(-heal_multiplier * 0.5) + owner.adjustOxyLoss(-heal_multiplier) + if(prob(heal_multiplier * 2)) + hope_message() + +/datum/status_effect/hope/proc/hope_message() + var/list/hope_messages = list("You are filled with [pick("hope", "determination", "strength", "peace", "confidence", "robustness")].", + "Don't give up!", + "You see your [pick("friends", "family", "coworkers", "self")] [pick("rooting for you", "cheering you on", "worrying about you")].", + "You can't give up now, keep going!", + "But you refused to die!", + "You have been through worse, you can do this!", + "People need you, do not [pick("give up", "stop", "rest", "pass away", "falter", "lose hope")] yet!", + "This person is not nearly as robust as you!", + "You ARE robust, don't let anyone tell you otherwise!", + "[owner], don't lose hope, the future of the station depends on you!", + "Do not follow the light yet!") + var/list/un_hopeful_messages = list("DON'T FUCKING DIE NOW COWARD!", + "Git Gud, [owner]", + "I bet a [pick("vox", "vulp", "nian", "tajaran", "baldie")] could do better than you!", + "You hear people making fun of you for getting robusted.") + if(prob(99)) + to_chat(owner, "[pick(hope_messages)]") + else + to_chat(owner, "[pick(un_hopeful_messages)]") + diff --git a/code/game/gamemodes/cult/cult_items.dm b/code/game/gamemodes/cult/cult_items.dm index 87e168731ed..b270227a8f9 100644 --- a/code/game/gamemodes/cult/cult_items.dm +++ b/code/game/gamemodes/cult/cult_items.dm @@ -444,6 +444,14 @@ /// Shatter threshold for Energy weapons var/energy_threshold = 20 +/obj/item/shield/mirror/Initialize(mapload) + . = ..() + GLOB.mirrors += src + +/obj/item/shield/mirror/Destroy() + GLOB.mirrors -= src + return ..() + /** * Reflect/Block/Shatter proc. * diff --git a/code/game/jobs/job_exp.dm b/code/game/jobs/job_exp.dm index 7c355ce61c0..9b88f119835 100644 --- a/code/game/jobs/job_exp.dm +++ b/code/game/jobs/job_exp.dm @@ -20,6 +20,7 @@ GLOBAL_LIST_INIT(role_playtime_requirements, list( ROLE_NINJA = 20, ROLE_MORPH = 5, ROLE_DEMON = 5, + ROLE_ELITE = 5, // DUO ANTAGS ROLE_GUARDIAN = 20, diff --git a/code/game/objects/items/weapons/melee/energy.dm b/code/game/objects/items/weapons/melee/energy.dm index d5476bd11b8..b7bf841d5a2 100644 --- a/code/game/objects/items/weapons/melee/energy.dm +++ b/code/game/objects/items/weapons/melee/energy.dm @@ -285,6 +285,8 @@ var/swiping = FALSE /obj/item/melee/energy/cleaving_saw/nemesis_effects(mob/living/user, mob/living/target) + if(istype(target, /mob/living/simple_animal/hostile/asteroid/elite)) // you get the bonus damage, but the bleed buildup is too much. + return var/datum/status_effect/saw_bleed/B = target.has_status_effect(STATUS_EFFECT_SAWBLEED) if(!B) if(!active) //This isn't in the above if-check so that the else doesn't care about active diff --git a/code/game/objects/structures/mirror.dm b/code/game/objects/structures/mirror.dm index 725b557a78b..493e41ab4c6 100644 --- a/code/game/objects/structures/mirror.dm +++ b/code/game/objects/structures/mirror.dm @@ -22,9 +22,11 @@ pixel_x = -32 if(WEST) pixel_x = 32 + GLOB.mirrors += src /obj/structure/mirror/Destroy() QDEL_LIST_ASSOC_VAL(ui_users) + GLOB.mirrors -= src return ..() /obj/structure/mirror/attack_hand(mob/user) @@ -48,6 +50,7 @@ if(desc == initial(desc)) desc = "Oh no, seven years of bad luck!" broken = TRUE + GLOB.mirrors -= src /obj/structure/mirror/screwdriver_act(mob/user, obj/item/I) . = TRUE diff --git a/code/modules/client/preference/preferences.dm b/code/modules/client/preference/preferences.dm index df07c138759..94e84c318f9 100644 --- a/code/modules/client/preference/preferences.dm +++ b/code/modules/client/preference/preferences.dm @@ -14,6 +14,7 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts ROLE_ALIEN = 21, ROLE_DEMON = 21, ROLE_SENTIENT = 21, + ROLE_ELITE = 21, // ROLE_GANG = 21, ROLE_NINJA = 21, ROLE_GSPIDER = 21, diff --git a/code/modules/clothing/clothing.dm b/code/modules/clothing/clothing.dm index 50ff83049ce..da9e05fddd6 100644 --- a/code/modules/clothing/clothing.dm +++ b/code/modules/clothing/clothing.dm @@ -711,6 +711,23 @@ BLIND // can't see anything QDEL_LIST(accessories) return ..() + +/obj/item/clothing/under/dropped(mob/user, silent) + ..() + if(!ishuman(user)) + return + var/mob/living/carbon/human/H = user + if(H.get_item_by_slot(slot_w_uniform) == src) + for(var/obj/item/clothing/accessory/A in accessories) + A.attached_unequip() + +/obj/item/clothing/under/equipped(mob/user, slot, initial) + if(!ishuman(user)) + return + if(slot == slot_w_uniform) + for(var/obj/item/clothing/accessory/A in accessories) + A.attached_equip() + /* * # can_attach_accessory * diff --git a/code/modules/clothing/under/accessories/accessory.dm b/code/modules/clothing/under/accessories/accessory.dm index db8bbdfa676..55c13758203 100644 --- a/code/modules/clothing/under/accessories/accessory.dm +++ b/code/modules/clothing/under/accessories/accessory.dm @@ -99,6 +99,12 @@ return //we aren't an object on the ground so don't call parent ..() +/obj/item/clothing/accessory/proc/attached_unequip(mob/user) // If we need to do something special when clothing is removed from the user + return + +/obj/item/clothing/accessory/proc/attached_equip(mob/user) // If we need to do something special when clothing is removed from the user + return + /obj/item/clothing/accessory/blue name = "blue tie" icon_state = "bluetie" diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm index dba9400ac8a..1022756f7a9 100644 --- a/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm +++ b/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm @@ -673,7 +673,7 @@ Difficulty: Hard return for(var/mob/living/L in T.contents - hit_things) //find and damage mobs... hit_things += L - if((friendly_fire_check && caster && caster.faction_check_mob(L)) || L.stat == DEAD) + if((friendly_fire_check && caster && L.faction_check_mob(caster)) || L.stat == DEAD) continue L.flash_screen_color("#660099", 1) playsound(L,'sound/weapons/sear.ogg', 50, TRUE, -4) diff --git a/code/modules/mob/living/simple_animal/hostile/mining/elites/elite.dm b/code/modules/mob/living/simple_animal/hostile/mining/elites/elite.dm new file mode 100644 index 00000000000..e43e6b4cd3b --- /dev/null +++ b/code/modules/mob/living/simple_animal/hostile/mining/elites/elite.dm @@ -0,0 +1,460 @@ +#define TUMOR_INACTIVE 0 +#define TUMOR_ACTIVE 1 +#define TUMOR_PASSIVE 2 + +//Elite mining mobs +/mob/living/simple_animal/hostile/asteroid/elite + name = "elite" + desc = "An elite monster, found in one of the strange tumors on lavaland." + icon = 'icons/mob/lavaland/lavaland_elites.dmi' + faction = list("boss") + robust_searching = TRUE + ranged_ignores_vision = TRUE + ranged = TRUE + obj_damage = 30 + vision_range = 6 + aggro_vision_range = 18 + environment_smash = ENVIRONMENT_SMASH_NONE //This is to prevent elites smashing up the mining station (entirely), we'll make sure they can smash minerals fine below. + harm_intent_damage = 0 //Punching elites gets you nowhere + stat_attack = UNCONSCIOUS + layer = LARGE_MOB_LAYER + has_laser_resist = FALSE + universal_speak = TRUE + sentience_type = SENTIENCE_BOSS + var/chosen_attack = 1 + var/list/attack_action_types = list() + var/obj/loot_drop = null + var/revive_cooldown = FALSE + +//Gives player-controlled variants the ability to swap attacks +/mob/living/simple_animal/hostile/asteroid/elite/Initialize(mapload) + . = ..() + for(var/action_type in attack_action_types) + var/datum/action/innate/elite_attack/attack_action = new action_type() + attack_action.Grant(src) + +//Prevents elites from attacking members of their faction (can't hurt themselves either) and lets them mine rock with an attack despite not being able to smash walls. + +/mob/living/simple_animal/hostile/asteroid/elite/examine(mob/user) + . = ..() + if(del_on_death) + . += "However, this one appears appears less wild in nature, and calmer around people." + +/mob/living/simple_animal/hostile/asteroid/elite/AttackingTarget() + if(istype(target, /mob/living/simple_animal/hostile)) + var/mob/living/simple_animal/hostile/M = target + if(faction_check_mob(M)) + return FALSE + if(istype(target, /obj/structure/elite_tumor)) + var/obj/structure/elite_tumor/T = target + if(T.mychild == src && T.activity == TUMOR_PASSIVE) + var/response = alert(src, "Re-enter the tumor?","Despawn yourself?", "Yes", "No") + if(response == "No" || QDELETED(src) || !Adjacent(T)) + return + T.clear_activator(src) + T.mychild = null + T.activity = TUMOR_INACTIVE + T.icon_state = "advanced_tumor" + qdel(src) + return FALSE + . = ..() + if(ismineralturf(target)) + var/turf/simulated/mineral/M = target + M.gets_drilled() + if(istype(target, /obj/mecha)) + var/obj/mecha/M = target + M.take_damage(50, BRUTE, MELEE, 1) + if(. && isliving(target)) //Taken from megafauna. This exists purely to stop someone from cheesing a weaker melee fauna by letting it get punched. + var/mob/living/L = target + if(L.stat != DEAD) + if(!client && ranged && ranged_cooldown <= world.time) + OpenFire() + + +/mob/living/simple_animal/hostile/asteroid/elite/proc/revive_multiplier() //If on lavaland, return 1, or 1x cooldown. 10 if revived by a non antag, 2 if by an antag. 1 otherwise + if(is_mining_level(z)) + return 1 + if(revive_cooldown) + return 10 + if(del_on_death) + return 2 + return 1 + +/mob/living/simple_animal/hostile/asteroid/elite/adjustHealth(damage, updating_health) + . = ..() + if(del_on_death) + maxHealth -= damage / 3 + +/mob/living/simple_animal/hostile/asteroid/elite/ex_act(severity, origin) //No surrounding the tumor with gibtonite and one shotting them. + switch(severity) + if(EXPLODE_DEVASTATE) + adjustBruteLoss(75) + + if(EXPLODE_HEAVY) + adjustBruteLoss(50) + + if(EXPLODE_LIGHT) + adjustBruteLoss(25) + + +/*Basic setup for elite attacks, based on Whoneedspace's megafauna attack setup. +While using this makes the system rely on OnFire, it still gives options for timers not tied to OnFire, and it makes using attacks consistent accross the board for player-controlled elites.*/ + +/datum/action/innate/elite_attack + name = "Elite Attack" + icon_icon = 'icons/mob/actions/actions_elites.dmi' + button_icon_state = "" + background_icon_state = "bg_default" + ///The displayed message into chat when this attack is selected + var/chosen_message + ///The internal attack ID for the elite's OpenFire() proc to use + var/chosen_attack_num = 0 + +/datum/action/innate/elite_attack/New(Target) + . = ..() + button.maptext = "" + button.maptext_x = 8 + button.maptext_y = 0 + button.maptext_width = 24 + button.maptext_height = 12 + return button + +/datum/action/innate/elite_attack/process() + if(owner == null) + STOP_PROCESSING(SSfastprocess, src) + qdel(src) + return + UpdateButton() + +/datum/action/innate/elite_attack/proc/UpdateButton(status_only = FALSE) + if(status_only) + return + var/mob/living/simple_animal/hostile/asteroid/elite/elite_owner = owner + var/timeleft = max(elite_owner.ranged_cooldown - world.time, 0) + if(timeleft == 0) + button.maptext = "" + else + button.maptext = "[round(timeleft/10, 0.1)]" + +/datum/action/innate/elite_attack/Grant(mob/living/L) + if(istype(L, /mob/living/simple_animal/hostile/asteroid/elite)) + START_PROCESSING(SSfastprocess, src) + return ..() + return FALSE + +/datum/action/innate/elite_attack/Activate() + var/mob/living/simple_animal/hostile/asteroid/elite/elite_owner = owner + elite_owner.chosen_attack = chosen_attack_num + to_chat(elite_owner, chosen_message) + +//The Pulsing Tumor, the actual "spawn-point" of elites, handles the spawning, arena, and procs for dealing with basic scenarios. + +/obj/structure/elite_tumor + name = "pulsing tumor" + desc = "An odd, pulsing tumor sticking out of the ground. You feel compelled to reach out and touch it..." + resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF + icon = 'icons/obj/lavaland/tumor.dmi' + icon_state = "tumor" + pixel_x = -16 + light_color = LIGHT_COLOR_BLOOD_MAGIC + light_range = 3 + anchored = TRUE + density = FALSE + var/activity = TUMOR_INACTIVE + var/boosted = FALSE + var/times_won = 0 + var/mob/living/carbon/human/activator = null + var/mob/living/simple_animal/hostile/asteroid/elite/mychild = null + var/gps + ///List of all potentially spawned elites + var/potentialspawns = list( + /mob/living/simple_animal/hostile/asteroid/elite/broodmother, + /mob/living/simple_animal/hostile/asteroid/elite/pandora, + /mob/living/simple_animal/hostile/asteroid/elite/legionnaire, + /mob/living/simple_animal/hostile/asteroid/elite/herald, + ) + +/obj/structure/elite_tumor/attack_hand(mob/user, list/modifiers) + . = ..() + if(!ishuman(user)) + return + switch(activity) + if(TUMOR_PASSIVE) + // Prevents the user from being forcemoved back and forth between two elite arenas. + if(HAS_TRAIT(src, TRAIT_ELITE_CHALLENGER)) + user.visible_message("[user] reaches for [src] with [user.p_their()] arm, but nothing happens.", + "You reach for [src] with your arm... but nothing happens.") + return + activity = TUMOR_ACTIVE + user.visible_message("[src] convulses as [user]'s arm enters its radius. Uh-oh...", + "[src] convulses as your arm enters its radius. Your instincts tell you to step back.") + make_activator(user) + if(boosted) + mychild.playsound_local(get_turf(mychild), 'sound/magic/cult_spell.ogg', 40, 0) + to_chat(mychild, "Someone has activated your tumor. You will be returned to fight shortly, get ready!") + addtimer(CALLBACK(src, .proc/return_elite), 3 SECONDS) + INVOKE_ASYNC(src, .proc/arena_checks) + if(TUMOR_INACTIVE) + if(HAS_TRAIT(src, TRAIT_ELITE_CHALLENGER)) + user.visible_message("[user] reaches for [src] with [user.p_their()] arm, but nothing happens.", + "You reach for [src] with your arm... but nothing happens.") + return + activity = TUMOR_ACTIVE + var/mob/dead/observer/elitemind = null + visible_message("[src] begins to convulse. Your instincts tell you to step back.") + make_activator(user) + if(!boosted) + addtimer(CALLBACK(src, .proc/spawn_elite), 3 SECONDS) + return + visible_message("Something within [src] stirs...") + var/list/candidates = SSghost_spawns.poll_candidates("Do you want to play as a lavaland elite?", ROLE_ELITE, TRUE, 10 SECONDS, source = src) + if(length(candidates)) + audible_message("The stirring sounds increase in volume!") + elitemind = pick(candidates) + SEND_SOUND(elitemind, 'sound/magic/cult_spell.ogg') + to_chat(elitemind, "You have been chosen to play as a Lavaland Elite.\nIn a few seconds, you will be summoned on Lavaland as a monster to fight your activator, in a fight to the death.\n\ + Your attacks can be switched using the buttons on the top left of the HUD, and used by clicking on targets or tiles similar to a gun.\n\ + While the opponent might have an upper hand with powerful mining equipment and tools, you have great power normally limited by AI mobs.\n\ + If you want to win, you'll have to use your powers in creative ways to ensure the kill. It's suggested you try using them all as soon as possible.\n\ + Should you win, you'll receive extra information regarding what to do after. Good luck!") + addtimer(CALLBACK(src, .proc/spawn_elite, elitemind), 10 SECONDS) + else + visible_message("The stirring stops, and nothing emerges. Perhaps try again later.") + activity = TUMOR_INACTIVE + clear_activator(user) + +/obj/structure/elite_tumor/proc/spawn_elite(mob/dead/observer/elitemind) + var/selectedspawn = pick(potentialspawns) + mychild = new selectedspawn(loc) + visible_message("[mychild] emerges from [src]!") + playsound(loc,'sound/effects/phasein.ogg', 200, 0, 50, TRUE, TRUE) + if(boosted) + mychild.key = elitemind.key + mychild.sentience_act() + notify_ghosts("\A [mychild] has been awakened in \the [get_area(src)]!", enter_link="(Click to help)", source = mychild, action = NOTIFY_FOLLOW) + icon_state = "tumor_popped" + RegisterSignal(mychild, COMSIG_PARENT_QDELETING, .proc/onEliteLoss) + INVOKE_ASYNC(src, .proc/arena_checks) + +/obj/structure/elite_tumor/proc/return_elite() + mychild.forceMove(loc) + visible_message("[mychild] emerges from [src]!") + playsound(loc,'sound/effects/phasein.ogg', 200, 0, 50, TRUE, TRUE) + mychild.revive() + if(boosted) + mychild.maxHealth = mychild.maxHealth * 2.5 + mychild.health = mychild.maxHealth + mychild.grab_ghost() + notify_ghosts("\A [mychild] has been challenged in \the [get_area(src)]!", enter_link="(Click to help)", source = mychild, action = NOTIFY_FOLLOW) + +/obj/structure/elite_tumor/Initialize(mapload) + . = ..() + gps = new /obj/item/gps/internal/tumor(src) + START_PROCESSING(SSobj, src) + +/obj/structure/elite_tumor/Destroy() + STOP_PROCESSING(SSobj, src) + QDEL_NULL(gps) + if(activator) + clear_activator(activator) + if(mychild) + clear_activator(mychild) + return ..() + +/obj/structure/elite_tumor/proc/make_activator(mob/user) + if(activator) + return + activator = user + ADD_TRAIT(user, TRAIT_ELITE_CHALLENGER, "activation") + RegisterSignal(user, COMSIG_PARENT_QDELETING, .proc/clear_activator) + +/obj/structure/elite_tumor/proc/clear_activator(mob/source) + SIGNAL_HANDLER + if(source == activator) + activator = null + else + mychild = null + REMOVE_TRAIT(source, TRAIT_ELITE_CHALLENGER, "clear activation") + UnregisterSignal(source, COMSIG_PARENT_QDELETING) + +/obj/structure/elite_tumor/process() + if(!isturf(loc)) + return + + for(var/mob/living/simple_animal/hostile/asteroid/elite/elitehere in loc) + if(elitehere == mychild && activity == TUMOR_PASSIVE) + mychild.adjustHealth(-mychild.maxHealth * 0.025) + var/obj/effect/temp_visual/heal/H = new /obj/effect/temp_visual/heal(get_turf(mychild)) + H.color = "#FF0000" + +/obj/structure/elite_tumor/attackby(obj/item/attacking_item, mob/user, params) + . = ..() + if(istype(attacking_item, /obj/item/organ/internal/regenerative_core) && activity == TUMOR_INACTIVE && !boosted) + var/obj/item/organ/internal/regenerative_core/core = attacking_item + visible_message("As [user] drops the core into [src], [src] appears to swell.") + icon_state = "advanced_tumor" + boosted = TRUE + set_light(6) + qdel(core) + return TRUE + +/obj/structure/elite_tumor/examine(mob/user) + . = ..() + if(boosted) + . += "this one glows with a strong intensity" + +/obj/structure/elite_tumor/proc/arena_checks() + if(activity != TUMOR_ACTIVE || QDELETED(src)) + return + INVOKE_ASYNC(src, .proc/fighters_check) //Checks to see if our fighters died. + INVOKE_ASYNC(src, .proc/arena_trap) //Gets another arena trap queued up for when this one runs out. + INVOKE_ASYNC(src, .proc/border_check) //Checks to see if our fighters got out of the arena somehow. + addtimer(CALLBACK(src, .proc/arena_checks), 5 SECONDS) + +/obj/structure/elite_tumor/proc/fighters_check() + if(QDELETED(mychild) || mychild.stat == DEAD) + onEliteLoss() + return + if(QDELETED(activator) || activator.stat == DEAD || (activator.health <= HEALTH_THRESHOLD_DEAD)) + onEliteWon() + +/obj/structure/elite_tumor/proc/arena_trap() + var/turf/tumor_turf = get_turf(src) + if(loc == null) + return + for(var/tumor_range_turfs in RANGE_TURFS(12, tumor_turf)) + if(get_dist(tumor_range_turfs, tumor_turf) == 12) + var/obj/effect/temp_visual/elite_tumor_wall/newwall + newwall = new /obj/effect/temp_visual/elite_tumor_wall(tumor_range_turfs, src) + newwall.activator = activator + newwall.ourelite = mychild + +/obj/structure/elite_tumor/proc/border_check() + if(activator != null && get_dist(src, activator) >= 12) + activator.forceMove(loc) + visible_message("[activator] suddenly reappears above [src]!") + playsound(loc,'sound/effects/phasein.ogg', 200, 0, 50, TRUE, TRUE) + if(mychild != null && get_dist(src, mychild) >= 12) + mychild.forceMove(loc) + visible_message("[mychild] suddenly reappears above [src]!") + playsound(loc,'sound/effects/phasein.ogg', 200, 0, 50, TRUE, TRUE) + +/obj/structure/elite_tumor/proc/onEliteLoss() + playsound(loc,'sound/effects/tendril_destroyed.ogg', 200, 0, 50, TRUE, TRUE) + visible_message("[src] begins to convulse violently before beginning to dissipate.") + visible_message("As [src] closes, something is forced up from down below.") + var/obj/structure/closet/crate/necropolis/tendril/lootbox = new /obj/structure/closet/crate/necropolis/tendril(loc) + if(boosted) + if(mychild.loot_drop != null && prob(50)) + new mychild.loot_drop(lootbox) + else + new /obj/item/tumor_shard(lootbox) + SSblackbox.record_feedback("tally", "Player controlled Elite loss", 1, mychild.name) + else + SSblackbox.record_feedback("tally", "AI controlled Elite loss", 1, mychild.name) + qdel(src) + +/obj/structure/elite_tumor/proc/onEliteWon() + activity = TUMOR_PASSIVE + if(activator) + clear_activator(activator) + mychild.revive() + if(boosted) + SSblackbox.record_feedback("tally", "Player controlled Elite win", 1, mychild.name) + times_won++ + mychild.maxHealth = mychild.maxHealth * 0.4 + mychild.health = mychild.maxHealth + var/sound/elite_sound = sound('sound/magic/wandodeath.ogg') + var/turf/T = get_turf(src) + for(var/mob/M in GLOB.player_list) + if(M.z == z && M.client) + to_chat(M, "Thunder rumbles. Light glows in the distance. Something big happened... somewhere.") + M.playsound_local(T, null, 100, FALSE, 0, FALSE, pressure_affected = FALSE, S = elite_sound) + M.flash_screen_color("#FF0000", 2.5 SECONDS) + else + SSblackbox.record_feedback("tally", "AI controlled Elite win", 1, mychild.name) + if(times_won == 1) + mychild.playsound_local(get_turf(mychild), 'sound/magic/cult_spell.ogg', 40, 0) + to_chat(mychild, "") + to_chat(mychild, "Your max health has been halved, but can now heal by standing on your tumor. Note, it's your only way to heal.\n\ + Bear in mind, if anyone interacts with your tumor, you'll be resummoned here to carry out another fight. In such a case, you will regain your full max health.\n\ + Also, be weary of your fellow inhabitants, they likely won't be happy to see you!") + to_chat(mychild, "Note that you are a lavaland monster, and thus not allied to the station. You should not cooperate or act friendly with any station crew unless under extreme circumstances!") + +/obj/item/tumor_shard + name = "tumor shard" + desc = "A strange, sharp, crystal shard from an odd tumor on Lavaland. Stabbing the corpse of a lavaland elite with this will revive them, assuming their soul still lingers. Revived lavaland elites only have half their max health, but are completely loyal to their reviver." + icon = 'icons/obj/lavaland/artefacts.dmi' + icon_state = "crevice_shard" + throwforce = 5 + w_class = WEIGHT_CLASS_SMALL + throw_speed = 3 + throw_range = 5 + +/obj/item/tumor_shard/afterattack(atom/target, mob/user, proximity_flag) + . = ..() + if(istype(target, /mob/living/simple_animal/hostile/asteroid/elite) && proximity_flag) + var/mob/living/simple_animal/hostile/asteroid/elite/E = target + if(E.stat != DEAD || E.sentience_type != SENTIENCE_BOSS || !E.key) + user.visible_message("It appears [E] is unable to be revived right now. Perhaps try again later.") + return + E.faction = list("\ref[user]") + E.friends += user + E.revive() + user.visible_message("[user] stabs [E] with [src], reviving it.") + SEND_SOUND(E, 'sound/magic/cult_spell.ogg') + to_chat(E, "You have been revived by [user]. While you can't speak to them, you owe [user] a great debt. Assist [user.p_them()] in achieving [user.p_their()] goals, regardless of risk.") + to_chat(E, "Note that you now share the loyalties of [user]. You are expected not to intentionally sabotage their faction unless commanded to!") + if(user.mind.special_role) + E.maxHealth = 300 + E.health = 300 + else + E.maxHealth = 200 + E.health = 200 + E.revive_cooldown = TRUE + E.sentience_type = SENTIENCE_ORGANIC + E.del_on_death = TRUE + qdel(src) + else + to_chat(user, "[src] only works on the corpse of a sentient lavaland elite.") + +/obj/effect/temp_visual/elite_tumor_wall + name = "magic wall" + icon = 'icons/turf/walls/hierophant_wall_temp.dmi' + icon_state = "hierophant_wall_temp-0" + base_icon_state = "hierophant_wall_temp" + smoothing_flags = SMOOTH_BITMASK + smoothing_groups = list(SMOOTH_GROUP_HIERO_WALL) + canSmoothWith = list(SMOOTH_GROUP_HIERO_WALL) + duration = 50 + layer = BELOW_MOB_LAYER + plane = GAME_PLANE + color = rgb(255,0,0) + light_range = MINIMUM_USEFUL_LIGHT_RANGE + light_color = LIGHT_COLOR_PURE_RED + var/activator + var/ourelite + +/obj/effect/temp_visual/elite_tumor_wall/Initialize(mapload, new_caster) + . = ..() + if(smoothing_flags & (SMOOTH_CORNERS|SMOOTH_BITMASK)) + QUEUE_SMOOTH_NEIGHBORS(src) + QUEUE_SMOOTH(src) + +/obj/effect/temp_visual/elite_tumor_wall/Destroy() + if(smoothing_flags & (SMOOTH_CORNERS|SMOOTH_BITMASK)) + QUEUE_SMOOTH_NEIGHBORS(src) + return ..() + +/obj/effect/temp_visual/elite_tumor_wall/CanPass(atom/movable/mover, border_dir) + . = ..() + if(mover == ourelite || mover == activator) + return FALSE + +/obj/item/gps/internal/tumor + icon_state = null + gpstag = "Cancerous Signal" + desc = "Ghosts in a fauna? That's cancerous!" + invisibility = 100 diff --git a/code/modules/mob/living/simple_animal/hostile/mining/elites/goliath_broodmother.dm b/code/modules/mob/living/simple_animal/hostile/mining/elites/goliath_broodmother.dm new file mode 100644 index 00000000000..f81d74f8506 --- /dev/null +++ b/code/modules/mob/living/simple_animal/hostile/mining/elites/goliath_broodmother.dm @@ -0,0 +1,284 @@ +#define TENTACLE_PATCH 1 +#define SPAWN_CHILDREN 2 +#define RAGE 3 +#define CALL_CHILDREN 4 + +/** + * # Goliath Broodmother + * + * A stronger, faster variation of the goliath. Has the ability to spawn baby goliaths, which it can later detonate at will. + * When it's health is below half, tendrils will spawn randomly around it. When it is below a quarter of health, this effect is doubled. + * It's attacks are as follows: + * - Spawns a 3x3/plus shape of tentacles on the target location + * - Spawns 2 baby goliaths on its tile, up to a max of 8. Children blow up when they die. + * - The broodmother lets out a noise, and is able to move faster for 6.5 seconds. + * - Summons your children around you. + * The broodmother is a fight revolving around stage control, as the activator has to manage the baby goliaths and the broodmother herself, along with all the tendrils. + */ + +/mob/living/simple_animal/hostile/asteroid/elite/broodmother + name = "goliath broodmother" + desc = "An example of sexual dimorphism, this female goliath looks much different than the males of her species. She is, however, just as dangerous, if not more." + gender = FEMALE + icon_state = "broodmother" + icon_living = "broodmother" + icon_aggro = "broodmother" + icon_dead = "egg_sac" + icon_gib = "syndicate_gib" + maxHealth = 1000 + health = 1000 + melee_damage_lower = 30 + melee_damage_upper = 30 + armour_penetration = 30 + attacktext = "beats down on" + attack_sound = 'sound/weapons/punch1.ogg' + throw_message = "does nothing to the rocky hide of the" + speed = 2 + move_to_delay = 5 + mob_biotypes = MOB_ORGANIC|MOB_BEAST + mouse_opacity = MOUSE_OPACITY_ICON + deathmessage = "explodes into gore!" + loot_drop = /obj/item/crusher_trophy/broodmother_tongue + + attack_action_types = list(/datum/action/innate/elite_attack/tentacle_patch, + /datum/action/innate/elite_attack/spawn_children, + /datum/action/innate/elite_attack/rage, + /datum/action/innate/elite_attack/call_children) + + var/rand_tent = 0 + var/list/mob/living/simple_animal/hostile/asteroid/elite/broodmother_child/children_list = list() + +/datum/action/innate/elite_attack/tentacle_patch + name = "Tentacle Patch" + button_icon_state = "tentacle_patch" + chosen_message = "You are now attacking with a patch of tentacles." + chosen_attack_num = TENTACLE_PATCH + +/datum/action/innate/elite_attack/spawn_children + name = "Spawn Children" + button_icon_state = "spawn_children" + chosen_message = "You will spawn two children at your location to assist you in combat. You can have up to 8." + chosen_attack_num = SPAWN_CHILDREN + +/datum/action/innate/elite_attack/rage + name = "Rage" + button_icon_state = "rage" + chosen_message = "You will temporarily increase your movement speed." + chosen_attack_num = RAGE + +/datum/action/innate/elite_attack/call_children + name = "Call Children" + button_icon_state = "call_children" + chosen_message = "You will summon your children to your location." + chosen_attack_num = CALL_CHILDREN + +/mob/living/simple_animal/hostile/asteroid/elite/broodmother/Destroy() + for(var/mob/living/simple_animal/hostile/asteroid/elite/broodmother_child/C in children_list) + C.mother = null + children_list.Cut() + children_list = null + return ..() + +/mob/living/simple_animal/hostile/asteroid/elite/broodmother/OpenFire() + if(client) + switch(chosen_attack) + if(TENTACLE_PATCH) + tentacle_patch(target) + if(SPAWN_CHILDREN) + spawn_children() + if(RAGE) + rage() + if(CALL_CHILDREN) + call_children() + return + var/aiattack = rand(1,4) + switch(aiattack) + if(TENTACLE_PATCH) + tentacle_patch(target) + if(SPAWN_CHILDREN) + spawn_children() + if(RAGE) + rage() + if(CALL_CHILDREN) + call_children() + +/mob/living/simple_animal/hostile/asteroid/elite/broodmother/Life(seconds, times_fired) + . = ..() + if(!.) //Checks if they are dead as a rock. + return + if(health < maxHealth * 0.5 && rand_tent < world.time) + rand_tent = world.time + 30 + var/tentacle_amount = 5 + if(health < maxHealth * 0.25) + tentacle_amount = 10 + var/tentacle_loc = spiral_range_turfs(5, get_turf(src)) + for(var/i in 1 to tentacle_amount) + var/turf/t = pick_n_take(tentacle_loc) + new /obj/effect/temp_visual/goliath_tentacle/broodmother(t, src) + +/mob/living/simple_animal/hostile/asteroid/elite/broodmother/proc/tentacle_patch(target) + ranged_cooldown = world.time + 2.5 SECONDS * revive_multiplier() + var/tturf = get_turf(target) + if(!isturf(tturf)) + return + visible_message("[src] digs its tentacles under [target]!") + new /obj/effect/temp_visual/goliath_tentacle/broodmother/patch(tturf, src) + +/mob/living/simple_animal/hostile/asteroid/elite/broodmother/proc/spawn_children(target) + ranged_cooldown = world.time + 3 SECONDS * revive_multiplier() + visible_message("The ground churns behind [src]!") + for(var/i in 1 to 2) + if(length(children_list) >= 8) + return + var/mob/living/simple_animal/hostile/asteroid/elite/broodmother_child/newchild = new /mob/living/simple_animal/hostile/asteroid/elite/broodmother_child(loc) + newchild.GiveTarget(target) + newchild.faction = faction.Copy() + visible_message("[newchild] appears below [src]!") + newchild.mother = src + children_list += newchild + +/mob/living/simple_animal/hostile/asteroid/elite/broodmother/proc/rage() + ranged_cooldown = world.time + 5 SECONDS * revive_multiplier() + visible_message("[src] starts picking up speed!") + color = "#FF0000" + speed = 0 + move_to_delay = 3 + addtimer(CALLBACK(src, .proc/reset_rage), 65) + +/mob/living/simple_animal/hostile/asteroid/elite/broodmother/proc/reset_rage() + color = "#FFFFFF" + speed = 2 + move_to_delay = 5 + +/mob/living/simple_animal/hostile/asteroid/elite/broodmother/proc/call_children() + ranged_cooldown = world.time + 4 SECONDS * revive_multiplier() + visible_message("The ground shakes near [src]!") + var/list/directions = GLOB.cardinal.Copy() + GLOB.diagonals.Copy() + for(var/mob/child in children_list) + var/spawndir = pick_n_take(directions) + var/turf/T = get_step(src, spawndir) + if(T) + child.forceMove(T) + playsound(src, 'sound/effects/bamf.ogg', 100, 1) + +//The goliath's children. Pretty weak, simple mobs which are able to put a single tentacle under their target when at range. +/mob/living/simple_animal/hostile/asteroid/elite/broodmother_child + name = "baby goliath" + desc = "A young goliath recently born from it's mother. While they hatch from eggs, said eggs are incubated in the mother until they are ready to be born." + icon = 'icons/mob/lavaland/lavaland_monsters.dmi' + icon_state = "goliath_baby" + icon_living = "goliath_baby" + icon_aggro = "goliath_baby" + icon_dead = "goliath_baby_dead" + icon_gib = "syndicate_gib" + maxHealth = 30 + health = 30 + melee_damage_lower = 5 + melee_damage_upper = 5 + attacktext = "bashes against" + attack_sound = 'sound/weapons/punch1.ogg' + throw_message = "does nothing to the rocky hide of the" + speed = 2 + move_to_delay = 5 + mob_biotypes = MOB_ORGANIC|MOB_BEAST + mouse_opacity = MOUSE_OPACITY_ICON + deathmessage = "falls to the ground." + status_flags = CANPUSH + var/mob/living/simple_animal/hostile/asteroid/elite/broodmother/mother = null + + +/mob/living/simple_animal/hostile/asteroid/elite/broodmother_child/Destroy() + if(!QDELETED(mother)) + mother.children_list -= src + mother = null + return ..() + +/mob/living/simple_animal/hostile/asteroid/elite/broodmother_child/OpenFire(target) + ranged_cooldown = world.time + 40 + var/tturf = get_turf(target) + if(!isturf(tturf)) + return + if(get_dist(src, target) <= 7)//Screen range check, so it can't attack people off-screen + visible_message("[src] digs its tentacles under [target]!") + new /obj/effect/temp_visual/goliath_tentacle/broodmother(tturf, src) + +/mob/living/simple_animal/hostile/asteroid/elite/broodmother_child/death() + . = ..() + if(!QDELETED(mother)) + mother.children_list -= src + visible_message("[src] explodes!") + explosion(src, flame_range = 3, adminlog = FALSE) + new /obj/item/stack/sheet/animalhide/goliath_hide(loc) + new /obj/effect/gibspawner/human(get_turf(src)) + qdel(src) + +//Tentacles stun WAY less compared to regular variant, to balance being able to use them much more often. Also, 10 more damage. +/obj/effect/temp_visual/goliath_tentacle/broodmother/trip() + var/latched = FALSE + for(var/mob/living/L in loc) + if((!QDELETED(spawner) && spawner.faction_check_mob(L)) || L.stat == DEAD) + continue + visible_message("[src] grabs hold of [L]!") + L.Stun(1 SECONDS) + L.adjustBruteLoss(rand(20,25)) + latched = TRUE + if(!latched) + retract() + else + deltimer(timerid) + timerid = addtimer(CALLBACK(src, .proc/retract), 1 SECONDS, TIMER_STOPPABLE) + +/obj/effect/temp_visual/goliath_tentacle/broodmother/patch/Initialize(mapload, new_spawner) + . = ..() + INVOKE_ASYNC(src, .proc/createpatch) + +/obj/effect/temp_visual/goliath_tentacle/broodmother/patch/proc/createpatch() + var/tentacle_locs = spiral_range_turfs(1, get_turf(src)) + for(var/T in tentacle_locs) + new /obj/effect/temp_visual/goliath_tentacle/broodmother(T, spawner) + var/list/directions = GLOB.cardinal.Copy() + for(var/i in directions) + var/turf/T = get_step(get_turf(src), i) + T = get_step(T, i) + new /obj/effect/temp_visual/goliath_tentacle/broodmother(T, spawner) + +// Broodmother's loot: Broodmother Tongue +/obj/item/crusher_trophy/broodmother_tongue + name = "broodmother tongue" + desc = "The tongue of a broodmother. If attached a certain way, makes for a suitable crusher trophy. It also feels very spongey, I wonder what would happen if you squeezed it?..." + icon = 'icons/obj/lavaland/elite_trophies.dmi' + icon_state = "broodmother_tongue" + denied_type = /obj/item/crusher_trophy/broodmother_tongue + bonus_value = 25 + /// Time at which the item becomes usable again + var/use_time + +/obj/item/crusher_trophy/broodmother_tongue/effect_desc() + return "mark detonation to have a [bonus_value]% chance to summon a patch of goliath tentacles at the target's location" + +/obj/item/crusher_trophy/broodmother_tongue/on_mark_detonation(mob/living/target, mob/living/user) + if(rand(1, 100) <= bonus_value && target.stat != DEAD) + new /obj/effect/temp_visual/goliath_tentacle/broodmother/patch(get_turf(target), user) + +/obj/item/crusher_trophy/broodmother_tongue/attack_self(mob/user) + if(!isliving(user)) + return + var/mob/living/living_user = user + if(use_time > world.time) + to_chat(living_user, "The tongue looks dried out. You'll need to wait longer to use it again.") + return + else if("lava" in living_user.weather_immunities) + to_chat(living_user, "You stare at the tongue. You don't think this is any use to you.") + return + living_user.weather_immunities += "lava" + to_chat(living_user, "You squeeze the tongue, and some transluscent liquid shoots out all over you.") + addtimer(CALLBACK(src, .proc/remove_lava, living_user), 10 SECONDS) + use_time = world.time + 60 SECONDS + +/obj/item/crusher_trophy/broodmother_tongue/proc/remove_lava(mob/living/user) + user.weather_immunities -= "lava" + +#undef TENTACLE_PATCH +#undef SPAWN_CHILDREN +#undef RAGE +#undef CALL_CHILDREN diff --git a/code/modules/mob/living/simple_animal/hostile/mining/elites/herald.dm b/code/modules/mob/living/simple_animal/hostile/mining/elites/herald.dm new file mode 100644 index 00000000000..84337f2d341 --- /dev/null +++ b/code/modules/mob/living/simple_animal/hostile/mining/elites/herald.dm @@ -0,0 +1,336 @@ +#define HERALD_TRISHOT 1 +#define HERALD_DIRECTIONALSHOT 2 +#define HERALD_TELESHOT 3 +#define HERALD_MIRROR 4 + +/** + * # Herald + * + * A slow-moving projectile user with a few tricks up it's sleeve. Less unga-bunga than Colossus, with more cleverness in it's fighting style. + * As it's health gets lower, the amount of projectiles fired per-attack increases. + * It's attacks are as follows: + * - Fires three projectiles in a given direction. + * - Fires a spread in every cardinal and diagonal direction at once, then does it again after a bit. + * - Shoots a single, golden bolt. Wherever it lands, the herald will be teleported to the location. + * - Spawns a mirror which reflects projectiles directly at the target. + * Herald is a more concentrated variation of the Colossus fight, having less projectiles overall, but more focused attacks. + */ + +/mob/living/simple_animal/hostile/asteroid/elite/herald + name = "herald" + desc = "A monstrous beast which fires deadly projectiles at threats and prey." + icon_state = "herald" + icon_living = "herald" + icon_aggro = "herald" + icon_dead = "herald_dying" + icon_gib = "syndicate_gib" + maxHealth = 1000 + health = 1000 + melee_damage_lower = 20 + melee_damage_upper = 20 + light_power = 5 + light_range = 2 + light_color = "#FF0000" + attacktext = "preaches to" + attack_sound = 'sound/magic/ratvar_attack.ogg' + throw_message = "doesn't affect the purity of" + speed = 2 + move_to_delay = 10 + mouse_opacity = MOUSE_OPACITY_ICON + death_sound = 'sound/misc/demon_dies.ogg' + deathmessage = "begins to shudder as it becomes transparent..." + loot_drop = /obj/item/clothing/accessory/necklace/herald_cloak + + + attack_action_types = list(/datum/action/innate/elite_attack/herald_trishot, + /datum/action/innate/elite_attack/herald_directionalshot, + /datum/action/innate/elite_attack/herald_teleshot, + /datum/action/innate/elite_attack/herald_mirror) + + var/mob/living/simple_animal/hostile/asteroid/elite/herald/mirror/my_mirror = null + var/is_mirror = FALSE + +/mob/living/simple_animal/hostile/asteroid/elite/herald/death() + . = ..() + if(!is_mirror) + addtimer(CALLBACK(src, .proc/become_ghost), 0.8 SECONDS) + if(my_mirror) + QDEL_NULL(my_mirror) + + +/mob/living/simple_animal/hostile/asteroid/elite/herald/Destroy() + if(my_mirror) + QDEL_NULL(my_mirror) + return ..() + +/mob/living/simple_animal/hostile/asteroid/elite/herald/proc/become_ghost() + icon_state = "herald_ghost" + +/mob/living/simple_animal/hostile/asteroid/elite/herald/say(message, bubble_type, list/spans = list(), sanitize = TRUE, datum/language/language = null, ignore_spam = FALSE, forced = null, filterproof = null) + . = ..() + playsound(get_turf(src), 'sound/magic/clockwork/invoke_general.ogg', 20, TRUE) + +/datum/action/innate/elite_attack/herald_trishot + name = "Triple Shot" + button_icon_state = "herald_trishot" + chosen_message = "You are now firing three shots in your chosen direction." + chosen_attack_num = HERALD_TRISHOT + +/datum/action/innate/elite_attack/herald_directionalshot + name = "Circular Shot" + button_icon_state = "herald_directionalshot" + chosen_message = "You are firing projectiles in all directions." + chosen_attack_num = HERALD_DIRECTIONALSHOT + +/datum/action/innate/elite_attack/herald_teleshot + name = "Teleport Shot" + button_icon_state = "herald_teleshot" + chosen_message = "You will now fire a shot which teleports you where it lands." + chosen_attack_num = HERALD_TELESHOT + +/datum/action/innate/elite_attack/herald_mirror + name = "Summon Mirror" + button_icon_state = "herald_mirror" + chosen_message = "You will spawn a mirror which duplicates your attacks." + chosen_attack_num = HERALD_MIRROR + +/mob/living/simple_animal/hostile/asteroid/elite/herald/OpenFire() + if(client) + switch(chosen_attack) + if(HERALD_TRISHOT) + herald_trishot(target) + my_mirror?.herald_trishot(target) + if(HERALD_DIRECTIONALSHOT) + herald_directionalshot() + my_mirror?.herald_directionalshot() + if(HERALD_TELESHOT) + herald_teleshot(target) + my_mirror?.herald_teleshot(target) + if(HERALD_MIRROR) + herald_mirror() + return + var/aiattack = rand(1,4) + switch(aiattack) + if(HERALD_TRISHOT) + herald_trishot(target) + my_mirror?.herald_trishot(target) + if(HERALD_DIRECTIONALSHOT) + herald_directionalshot() + my_mirror?.herald_directionalshot() + if(HERALD_TELESHOT) + herald_teleshot(target) + my_mirror?.herald_teleshot(target) + if(HERALD_MIRROR) + herald_mirror() + +/mob/living/simple_animal/hostile/asteroid/elite/herald/proc/shoot_projectile(turf/marker, set_angle, is_teleshot, is_trishot) + var/turf/startloc = get_turf(src) + if(!is_teleshot) + var/obj/item/projectile/H = new /obj/item/projectile/herald(startloc) + H.preparePixelProjectile(marker, marker, src) + H.firer = src + H.firer_source_atom = src + if(target) + H.original = target + H.fire(set_angle) + if(is_trishot) + shoot_projectile(marker, set_angle + 15, FALSE, FALSE) + shoot_projectile(marker, set_angle - 15, FALSE, FALSE) + else + var/obj/item/projectile/H = new /obj/item/projectile/herald/teleshot(startloc) + H.preparePixelProjectile(marker, marker, startloc) + H.firer = src + H.firer_source_atom = src + if(target) + H.original = target + H.fire(set_angle) + +/mob/living/simple_animal/hostile/asteroid/elite/herald/proc/herald_trishot(target) + ranged_cooldown = world.time + 3 SECONDS * revive_multiplier() + playsound(get_turf(src), 'sound/magic/clockwork/invoke_general.ogg', 20, TRUE) + var/target_turf = get_turf(target) + var/angle_to_target = get_angle(src, target_turf) + shoot_projectile(target_turf, angle_to_target, FALSE, TRUE) + addtimer(CALLBACK(src, .proc/shoot_projectile, target_turf, angle_to_target, FALSE, TRUE), 0.2 SECONDS) + addtimer(CALLBACK(src, .proc/shoot_projectile, target_turf, angle_to_target, FALSE, TRUE), 0.4 SECONDS) + if(health < maxHealth * 0.5 && !is_mirror) + playsound(get_turf(src), 'sound/magic/clockwork/invoke_general.ogg', 20, TRUE) + addtimer(CALLBACK(src, .proc/shoot_projectile, target_turf, angle_to_target, FALSE, TRUE), 1 SECONDS) + addtimer(CALLBACK(src, .proc/shoot_projectile, target_turf, angle_to_target, FALSE, TRUE), 1.2 SECONDS) + addtimer(CALLBACK(src, .proc/shoot_projectile, target_turf, angle_to_target, FALSE, TRUE), 1.4 SECONDS) + +/mob/living/simple_animal/hostile/asteroid/elite/herald/proc/herald_circleshot(offset) + var/static/list/directional_shot_angles = list(1, 45, 90, 135, 180, 225, 270, 315) //Trust me, use 1. It really doesn't like zero. + for(var/i in directional_shot_angles) + shoot_projectile(get_turf(src), i + offset, FALSE, FALSE) + +/mob/living/simple_animal/hostile/asteroid/elite/herald/proc/unenrage() + if(stat == DEAD || is_mirror) + return + icon_state = "herald" + +/mob/living/simple_animal/hostile/asteroid/elite/herald/proc/herald_directionalshot() + ranged_cooldown = world.time + 3 SECONDS * revive_multiplier() + if(!is_mirror) + icon_state = "herald_enraged" + playsound(get_turf(src), 'sound/magic/clockwork/invoke_general.ogg', 20, TRUE) + addtimer(CALLBACK(src, .proc/herald_circleshot, 0), 0.5 SECONDS) + if(health < maxHealth * 0.5 && !is_mirror) + addtimer(CALLBACK(src, .proc/herald_circleshot, 22.5), 1.5 SECONDS) + addtimer(CALLBACK(src, .proc/unenrage), 20) + +/mob/living/simple_animal/hostile/asteroid/elite/herald/proc/herald_teleshot(target) + ranged_cooldown = world.time + 3 SECONDS * revive_multiplier() + playsound(get_turf(src), 'sound/magic/clockwork/invoke_general.ogg', 20, TRUE) + var/target_turf = get_turf(target) + var/angle_to_target = get_angle(src, target_turf) + shoot_projectile(target_turf, angle_to_target, TRUE, FALSE) + +/mob/living/simple_animal/hostile/asteroid/elite/herald/proc/herald_mirror() + ranged_cooldown = world.time + 4 SECONDS * revive_multiplier() + playsound(get_turf(src), 'sound/magic/clockwork/invoke_general.ogg', 20, TRUE) + if(my_mirror != null) + QDEL_NULL(my_mirror) + var/mob/living/simple_animal/hostile/asteroid/elite/herald/mirror/new_mirror = new /mob/living/simple_animal/hostile/asteroid/elite/herald/mirror(loc) + my_mirror = new_mirror + my_mirror.my_master = src + my_mirror.faction = faction.Copy() + +/mob/living/simple_animal/hostile/asteroid/elite/herald/mirror + name = "herald's mirror" + desc = "This fiendish work of magic copies the herald's attacks. Seems logical to smash it." + health = 60 + maxHealth = 60 + icon_state = "herald_mirror" + icon_aggro = "herald_mirror" + deathmessage = "shatters violently!" + death_sound = 'sound/effects/glassbr1.ogg' + del_on_death = TRUE + is_mirror = TRUE + move_resist = MOVE_FORCE_OVERPOWERING // no dragging your mirror around + flying = TRUE + var/mob/living/simple_animal/hostile/asteroid/elite/herald/my_master = null + +/mob/living/simple_animal/hostile/asteroid/elite/herald/mirror/Initialize(mapload) + . = ..() + toggle_ai(AI_OFF) + +/mob/living/simple_animal/hostile/asteroid/elite/herald/mirror/Destroy() + my_master?.my_mirror = null + my_master = null + . = ..() + +/obj/item/projectile/herald + name = "death bolt" + icon_state = "chronobolt" + damage = 15 + armour_penetration = 30 + speed = 2 + +/obj/item/projectile/herald/teleshot + name = "golden bolt" + damage = 0 + color = rgb(255,255,102) + +/obj/item/projectile/herald/prehit(atom/target) + if(ismob(target) && ismob(firer)) + var/mob/living/mob_target = target + if(mob_target.faction_check_mob(firer)) + nodamage = TRUE + damage = 0 + return + if(mob_target.buckled && mob_target.stat == DEAD) + mob_target.dust() //no body cheese + +/obj/item/projectile/herald/on_hit(atom/target, blocked = FALSE) + . = ..() + if(ismineralturf(target)) + var/turf/simulated/mineral/M = target + M.gets_drilled() + +/obj/item/projectile/herald/teleshot/on_hit(atom/target, blocked = FALSE) + . = ..() + if(!istype(target, /mob/living/simple_animal/hostile/asteroid/elite/herald)) + firer.forceMove(get_turf(src)) + + +//Herald's loot: Cloak of the Prophet + +/obj/item/clothing/accessory/necklace/herald_cloak + name = "cloak of the prophet" + desc = "A cloak which lts you travel through a perfect reflection of the world." + icon = 'icons/obj/lavaland/elite_trophies.dmi' + icon_state = "herald_cloak" + item_state = "herald_cloak" + item_color = "herald_cloak" + slot_flags = SLOT_TIE + allow_duplicates = FALSE + actions_types = list(/datum/action/item_action/accessory/herald) + +/obj/item/clothing/accessory/necklace/herald_cloak/attack_self() + if(has_suit) + mirror_walk() + +/obj/item/clothing/accessory/necklace/herald_cloak/proc/mirror_walk() + var/found_mirror = FALSE + var/list/mirrors_to_use = list() + var/list/areaindex = list() + var/obj/starting_mirror = null + + for(var/obj/i in GLOB.mirrors) + var/turf/T = get_turf(i) + if(!is_teleport_allowed(i.z)) + continue + if(T.z != usr.z) //No crossing zlvls + continue + if(istype(i, /obj/item/shield/mirror) && !iscultist(usr)) //No teleporting to cult bases + continue + if(istype(i, /obj/structure/mirror)) + var/obj/structure/mirror/B = i + if(B.broken) + return + var/tmpname = T.loc.name + if(areaindex[tmpname]) + tmpname = "[tmpname] ([++areaindex[tmpname]])" + else + areaindex[tmpname] = 1 + mirrors_to_use[tmpname] = i + if(get_dist(src, i) > 1) + continue + found_mirror = TRUE + starting_mirror = i + + if(!found_mirror) + to_chat(usr, "You are not close enough to a working mirror to teleport!") + return + var/input_mirror = input(usr, "Choose a mirror to teleport to.", "Mirror to Teleport to") as null|anything in mirrors_to_use + var/obj/chosen = mirrors_to_use[input_mirror] + if(chosen == null) + return + usr.visible_message("[usr] starts to crawl into [starting_mirror]...", \ + "You start to crawl into the [starting_mirror]...") + if(do_after(usr, 2 SECONDS, target = usr)) + var/turf/destination = get_turf(chosen) + if(QDELETED(chosen) || !usr|| HAS_TRAIT(usr, TRAIT_HANDS_BLOCKED) || !chosen || (get_dist(src, starting_mirror) > 1 || destination.z != usr.z)) + return + usr.visible_message("[usr] crawls into the [starting_mirror], and [usr.p_they()] disappear into it!", \ + "You crawl into the [starting_mirror]...") + usr.forceMove(destination) + usr.visible_message("[usr] crawls out of [chosen], causing it to shatter!", \ + "You crawl out of your own reflection, shattering the mirror!") + if(istype(chosen, /obj/structure/mirror)) + var/obj/structure/mirror/M = chosen + M.obj_break("brute") + else if(istype(chosen, /obj/item/shield/mirror)) + var/turf/T = get_turf(usr) + new /obj/effect/temp_visual/cult/sparks(T) + playsound(T, 'sound/effects/glassbr3.ogg', 100) + if(isliving(chosen.loc)) + var/mob/living/shatterer = loc + shatterer.Weaken(6 SECONDS) + qdel(chosen) + +#undef HERALD_TRISHOT +#undef HERALD_DIRECTIONALSHOT +#undef HERALD_TELESHOT +#undef HERALD_MIRROR diff --git a/code/modules/mob/living/simple_animal/hostile/mining/elites/legionnaire.dm b/code/modules/mob/living/simple_animal/hostile/mining/elites/legionnaire.dm new file mode 100644 index 00000000000..a9c4a697604 --- /dev/null +++ b/code/modules/mob/living/simple_animal/hostile/mining/elites/legionnaire.dm @@ -0,0 +1,346 @@ +#define LEGIONNAIRE_CHARGE 1 +#define HEAD_DETACH 2 +#define BONFIRE_TELEPORT 3 +#define SPEW_SMOKE 4 + +/** + * # Legionnaire + * + * A towering skeleton, embodying the power of Legion. + * As it's health gets lower, the head does more damage. + * It's attacks are as follows: + * - Charges at the target after a telegraph, throwing them across the arena should it connect. + * - Legionnaire's head detaches, attacking as it's own entity. Has abilities of it's own later into the fight. Once dead, regenerates after a brief period. If the skill is used while the head is off, it will be killed. + * - Leaves a pile of bones at your location. Upon using this skill again, you'll swap locations with the bone pile. + * - Spews a cloud of smoke from it's maw, wherever said maw is. + * A unique fight incorporating the head mechanic of legion into a whole new beast. Combatants will need to make sure the tag-team of head and body don't lure them into a deadly trap. + */ + +/mob/living/simple_animal/hostile/asteroid/elite/legionnaire + name = "legionnaire" + desc = "A towering skeleton, embodying the terrifying power of Legion." + icon_state = "legionnaire" + icon_living = "legionnaire" + icon_aggro = "legionnaire" + icon_dead = "legionnaire_dead" + icon_gib = "syndicate_gib" + maxHealth = 1000 + health = 1000 + melee_damage_lower = 35 + melee_damage_upper = 35 + attacktext = "slashes its arms at" + attack_sound = 'sound/weapons/bladeslice.ogg' + throw_message = "doesn't affect the sturdiness of" + speed = 0.5 //Since it is mainly melee, this *should* be right + move_to_delay = 3 + mouse_opacity = MOUSE_OPACITY_ICON + death_sound = 'sound/hallucinations/wail.ogg' + deathmessage = "'s arms reach out before it falls apart onto the floor, lifeless." + sight = SEE_MOBS // So it can see through smoke / charge through walls like the kool aid man. + var/datum/effect_system/smoke_spread/bad/smoke + loot_drop = /obj/item/crusher_trophy/legionnaire_spine + + attack_action_types = list(/datum/action/innate/elite_attack/legionnaire_charge, + /datum/action/innate/elite_attack/head_detach, + /datum/action/innate/elite_attack/bonfire_teleport, + /datum/action/innate/elite_attack/spew_smoke) + + var/mob/living/simple_animal/hostile/asteroid/elite/legionnairehead/myhead = null + var/obj/structure/legionnaire_bonfire/mypile = null + var/has_head = TRUE + /// Whether or not the legionnaire is currently charging, used to deny movement input if he is + var/charging = FALSE + +/datum/action/innate/elite_attack/legionnaire_charge + name = "Legionnaire Charge" + button_icon_state = "legionnaire_charge" + chosen_message = "You will attempt to grab your opponent and throw them." + chosen_attack_num = LEGIONNAIRE_CHARGE + +/datum/action/innate/elite_attack/head_detach + name = "Release Head" + button_icon_state = "head_detach" + chosen_message = "You will now detach your head or kill it if it is already released." + chosen_attack_num = HEAD_DETACH + +/datum/action/innate/elite_attack/bonfire_teleport + name = "Bonfire Teleport" + button_icon_state = "bonfire_teleport" + chosen_message = "You will leave a bonfire. Second use will let you swap positions with it indefintiely. Using this move on the same tile as your active bonfire removes it." + chosen_attack_num = BONFIRE_TELEPORT + +/datum/action/innate/elite_attack/spew_smoke + name = "Spew Smoke" + button_icon_state = "spew_smoke" + chosen_message = "Your head will spew smoke in an area, wherever it may be." + chosen_attack_num = SPEW_SMOKE + +/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/Destroy() + myhead = null + mypile = null + return ..() + +/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/OpenFire() + if(client) + switch(chosen_attack) + if(LEGIONNAIRE_CHARGE) + legionnaire_charge(target) + if(HEAD_DETACH) + head_detach(target) + if(BONFIRE_TELEPORT) + bonfire_teleport() + if(SPEW_SMOKE) + spew_smoke() + return + var/aiattack = rand(1,4) + switch(aiattack) + if(LEGIONNAIRE_CHARGE) + legionnaire_charge(target) + if(HEAD_DETACH) + head_detach(target) + if(BONFIRE_TELEPORT) + bonfire_teleport() + if(SPEW_SMOKE) + spew_smoke() + +/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/Move() + if(charging) + return FALSE + return ..() + +/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/MiddleClickOn(atom/A) + . = ..() + if(!myhead) + return + var/turf/T = get_turf(A) + if(T) + myhead.LoseTarget() + myhead.Goto(T, myhead.move_to_delay) + +/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/proc/legionnaire_charge(target) + ranged_cooldown = world.time + 4 SECONDS * revive_multiplier() + charging = TRUE + var/dir_to_target = get_dir(get_turf(src), get_turf(target)) + var/turf/T = get_step(get_turf(src), dir_to_target) + for(var/i in 1 to 6) + new /obj/effect/temp_visual/dragon_swoop/legionnaire(T) + T = get_step(T, dir_to_target) + playsound(src, 'sound/misc/demon_attack1.ogg', 200, 1) + visible_message("[src] prepares to charge!") + addtimer(CALLBACK(src, .proc/legionnaire_charge_to, dir_to_target, 0), 4) + +/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/proc/legionnaire_charge_to(move_dir, times_ran) + if(times_ran >= 6) + charging = FALSE + return + var/turf/T = get_step(get_turf(src), move_dir) + if(ismineralturf(T)) + var/turf/simulated/mineral/M = T + M.gets_drilled() + if(T.density) + charging = FALSE + return + for(var/obj/structure/window/W in T.contents) + charging = FALSE + return + for(var/obj/machinery/door/D in T.contents) + if(D.density) + charging = FALSE + return + forceMove(T) + playsound(src,'sound/effects/bang.ogg', 200, 1) + var/list/hit_things = list() + var/throwtarget = get_edge_target_turf(src, move_dir) + for(var/mob/living/L in T.contents - hit_things - src) + if(faction_check_mob(L)) + return + hit_things += L + visible_message("[src] tramples and kicks [L]!") + to_chat(L, "[src] tramples you and kicks you away!") + L.throw_at(throwtarget, 10, 1, src) + L.KnockDown(1 SECONDS) + L.adjustBruteLoss(melee_damage_upper) + addtimer(CALLBACK(src, .proc/legionnaire_charge_to, move_dir, (times_ran + 1)), 0.7) + +/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/proc/head_detach(target) + ranged_cooldown = world.time + 1 SECONDS * revive_multiplier() + if(myhead != null) + myhead.adjustBruteLoss(600) + return + if(has_head) + has_head = FALSE + icon_state = "legionnaire_headless" + icon_living = "legionnaire_headless" + icon_aggro = "legionnaire_headless" + visible_message("[src]'s head flies off!") + var/mob/living/simple_animal/hostile/asteroid/elite/legionnairehead/newhead = new /mob/living/simple_animal/hostile/asteroid/elite/legionnairehead(loc) + newhead.GiveTarget(target) + newhead.faction = faction.Copy() + myhead = newhead + myhead.body = src + if(health < maxHealth * 0.25) + myhead.melee_damage_lower = 40 + myhead.melee_damage_upper = 40 + else if(health < maxHealth * 0.5) + myhead.melee_damage_lower = 30 + myhead.melee_damage_upper = 30 + +/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/proc/onHeadDeath() + myhead = null + addtimer(CALLBACK(src, .proc/regain_head), 5 SECONDS) + +/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/proc/regain_head() + has_head = TRUE + if(stat == DEAD) + return + icon_state = "legionnaire" + icon_living = "legionnaire" + icon_aggro = "legionnaire" + visible_message("The top of [src]'s spine leaks a black liquid, forming into a skull!") + +/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/proc/bonfire_teleport() + ranged_cooldown = world.time + 0.5 SECONDS * revive_multiplier() + if(isnull(mypile)) + var/obj/structure/legionnaire_bonfire/newpile = new /obj/structure/legionnaire_bonfire(loc) + mypile = newpile + mypile.myowner = src + playsound(get_turf(src),'sound/items/fultext_deploy.ogg', 200, 1) + visible_message("[src] summons a bonfire on [get_turf(src)]!") + return + else + var/turf/legionturf = get_turf(src) + var/turf/pileturf = get_turf(mypile) + if(legionturf == pileturf) + QDEL_NULL(mypile) + return + playsound(pileturf,'sound/items/fultext_deploy.ogg', 200, 1) + playsound(legionturf,'sound/items/fultext_deploy.ogg', 200, 1) + visible_message("[src] melts down into a burning pile of bones!") + forceMove(pileturf) + visible_message("[src] forms from the bonfire!") + mypile.forceMove(legionturf) + +/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/proc/spew_smoke() + ranged_cooldown = world.time + 3 SECONDS * revive_multiplier() + var/smoke_location = null + if(isnull(myhead)) + smoke_location = src + else + smoke_location = myhead + if(myhead != null) + myhead.visible_message("[myhead] spews smoke from its maw!") + else if(!has_head) + visible_message("[src] spews smoke from the tip of their spine!") + else + visible_message("[src] spews smoke from its maw!") + var/datum/effect_system/smoke_spread/smoke = new + smoke.set_up(6, 0, smoke_location) + smoke.attach(smoke_location) + smoke.start() + +//The legionnaire's head. Basically the same as any legion head, but we have to tell our creator when we die so they can generate another head. +/mob/living/simple_animal/hostile/asteroid/elite/legionnairehead + name = "legionnaire head" + desc = "The legionnaire's head floating by itself. One shouldn't get too close, though once it sees you, you really don't have a choice." + icon_state = "legionnaire_head" + icon_living = "legionnaire_head" + icon_aggro = "legionnaire_head" + icon_dead = "legionnaire_dead" + icon_gib = "syndicate_gib" + maxHealth = 200 + health = 200 + melee_damage_lower = 20 + melee_damage_upper = 20 + attacktext = "bites at" + attack_sound = 'sound/hallucinations/growl1.ogg' + throw_message = "simply misses" + speed = 0 + move_to_delay = 2 + del_on_death = 1 + deathmessage = "crumbles away!" + faction = list() + ranged = FALSE + var/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/body = null + +/mob/living/simple_animal/hostile/asteroid/elite/legionnairehead/death() + . = ..() + if(body) + body.onHeadDeath() + +//The legionnaire's bonfire, which can be swapped positions with. Also sets flammable living beings on fire when they walk over it. +/obj/structure/legionnaire_bonfire + name = "bone pile" + desc = "A pile of bones which seems to occasionally move a little. It's probably a good idea to smash them." + icon = 'icons/obj/lavaland/legionnaire_bonfire.dmi' + icon_state = "bonfire" + max_integrity = 100 + move_resist = MOVE_FORCE_EXTREMELY_STRONG + anchored = TRUE + density = FALSE + light_range = 4 + light_color = LIGHT_COLOR_RED + var/mob/living/simple_animal/hostile/asteroid/elite/legionnaire/myowner = null + +/obj/structure/legionnaire_bonfire/Crossed(datum/source, atom/movable/mover) + if(isobj(source)) + var/obj/object = source + object.fire_act(1000, 500) + if(isliving(source)) + var/mob/living/fire_walker = source + fire_walker.adjust_fire_stacks(5) + fire_walker.IgniteMob() + +/obj/structure/legionnaire_bonfire/Destroy() + if(myowner != null) + myowner.mypile = null + . = ..() + +//The visual effect which appears in front of legionnaire when he goes to charge. +/obj/effect/temp_visual/dragon_swoop/legionnaire + duration = 10 + +/obj/effect/temp_visual/dragon_swoop/legionnaire/Initialize(mapload) + . = ..() + transform *= 0.33 + +// Legionnaire's loot: Legionnaire Spine + +/obj/item/crusher_trophy/legionnaire_spine + name = "legionnaire spine" + desc = "The spine of a legionnaire. With some creativity, you could use it as a crusher trophy. Alternatively, shaking it might do something as well." + icon = 'icons/obj/lavaland/elite_trophies.dmi' + icon_state = "legionnaire_spine" + denied_type = /obj/item/crusher_trophy/legionnaire_spine + bonus_value = 25 + /// Time at which the item becomes usable again + var/next_use_time + +/obj/item/crusher_trophy/legionnaire_spine/effect_desc() + return "mark detonation to have a [bonus_value]% chance to summon a loyal legion skull" + +/obj/item/crusher_trophy/legionnaire_spine/on_mark_detonation(mob/living/target, mob/living/user) + if(!prob(bonus_value) || target.stat == DEAD) + return + var/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/A = new /mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion(user.loc) + A.GiveTarget(target) + A.friends += user + A.faction = user.faction.Copy() + +/obj/item/crusher_trophy/legionnaire_spine/attack_self(mob/user) + if(!isliving(user)) + return + var/mob/living/LivingUser = user + if(next_use_time > world.time) + LivingUser.visible_message("[LivingUser] shakes the [src], but nothing happens...") + to_chat(LivingUser, "You need to wait longer to use this again.") + return + LivingUser.visible_message("[LivingUser] shakes the [src] and summons a legion skull!") + var/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/LegionSkull = new /mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion(LivingUser.loc) + LegionSkull.friends += LivingUser + LegionSkull.faction = LivingUser.faction.Copy() + next_use_time = world.time + 4 SECONDS + +#undef LEGIONNAIRE_CHARGE +#undef HEAD_DETACH +#undef BONFIRE_TELEPORT +#undef SPEW_SMOKE diff --git a/code/modules/mob/living/simple_animal/hostile/mining/elites/pandora.dm b/code/modules/mob/living/simple_animal/hostile/mining/elites/pandora.dm new file mode 100644 index 00000000000..f8ce3ae9bc8 --- /dev/null +++ b/code/modules/mob/living/simple_animal/hostile/mining/elites/pandora.dm @@ -0,0 +1,238 @@ +#define CHASER_BURST 1 +#define MAGIC_BOX 2 +#define PANDORA_TELEPORT 3 +#define AOE_SQUARES 4 +#define MAX_CHASERS 3 + +/** + * # Pandora + * + * A box with a similar design to the Hierophant which trades large, single attacks for more frequent smaller ones. + * As it's health gets lower, the time between it's attacks decrease. + * It's attacks are as follows: + * - Fires hierophant blasts in a straight line. Can only fire in a straight line in 8 directions, being the diagonals and cardinals. + * - Creates a box of hierophant blasts around the target. If they try to run away to avoid it, they'll very likely get hit. + * - Teleports the pandora from one location to another, almost identical to Hierophant. + * - Spawns a 7x7 AOE at the location of choice, spreading out from the center. + * Pandora's fight mirrors Hierophant's closely, but has stark differences in attack effects. Instead of long-winded dodge times and long cooldowns, Pandora constantly attacks the opponent, but leaves itself open for attack. + */ + +/mob/living/simple_animal/hostile/asteroid/elite/pandora + name = "pandora" + desc = "A large magic box with similar power and design to the Hierophant. Once it opens, it's not easy to close it." + icon_state = "pandora" + icon_living = "pandora" + icon_aggro = "pandora" + icon_dead = "pandora_dead" + icon_gib = "syndicate_gib" + + maxHealth = 1000 + health = 1000 + melee_damage_lower = 15 + melee_damage_upper = 15 + attacktext = "smashes into the side of" + attack_sound = 'sound/weapons/sonic_jackhammer.ogg' + throw_message = "merely dinks off of the" + ranged_cooldown_time = 20 + speed = 2 + move_to_delay = 10 + mouse_opacity = MOUSE_OPACITY_ICON + death_sound = 'sound/magic/repulse.ogg' + deathmessage = "'s lights flicker, before its top part falls down." + loot_drop = /obj/item/clothing/accessory/necklace/pandora_hope + + attack_action_types = list(/datum/action/innate/elite_attack/chaser_burst, + /datum/action/innate/elite_attack/magic_box, + /datum/action/innate/elite_attack/pandora_teleport, + /datum/action/innate/elite_attack/aoe_squares) + + var/sing_shot_length = 8 + var/cooldown_time = 2 SECONDS + var/chaser_speed = 3 + var/recalculation_speed = 4 //How many times chasers moves before recalculating + +/datum/action/innate/elite_attack/chaser_burst + name = "Chaser Burst" + button_icon_state = "singular_shot" + chosen_message = "You fire a chaser after all mobs in view." + chosen_attack_num = CHASER_BURST + +/datum/action/innate/elite_attack/magic_box + name = "Magic Box" + button_icon_state = "magic_box" + chosen_message = "You are now attacking with a box of magic squares." + chosen_attack_num = MAGIC_BOX + +/datum/action/innate/elite_attack/pandora_teleport + name = "Line Teleport" + button_icon_state = "pandora_teleport" + chosen_message = "You will now teleport to your target." + chosen_attack_num = PANDORA_TELEPORT + +/datum/action/innate/elite_attack/aoe_squares + name = "AOE Blast" + button_icon_state = "aoe_squares" + chosen_message = "Your attacks will spawn an AOE blast at your target location." + chosen_attack_num = AOE_SQUARES + +/mob/living/simple_animal/hostile/asteroid/elite/pandora/OpenFire() + if(client) + switch(chosen_attack) + if(CHASER_BURST) + chaser_burst(target) + if(MAGIC_BOX) + magic_box(target) + if(PANDORA_TELEPORT) + pandora_teleport(target) + if(AOE_SQUARES) + aoe_squares(target) + return + var/aiattack = rand(1,4) + switch(aiattack) + if(CHASER_BURST) + chaser_burst(target) + if(MAGIC_BOX) + magic_box(target) + if(PANDORA_TELEPORT) + pandora_teleport(target) + if(AOE_SQUARES) + aoe_squares(target) + +/mob/living/simple_animal/hostile/asteroid/elite/pandora/Life(seconds, times_fired) + . = ..() + if(health >= maxHealth * 0.5) + cooldown_time = 2 SECONDS * revive_multiplier() + chaser_speed = 3 + recalculation_speed = 4 + return + if(health < maxHealth * 0.5 && health > maxHealth * 0.25) + cooldown_time = 1.5 SECONDS * revive_multiplier() + chaser_speed = 2 + recalculation_speed = 3 + return + else + chaser_speed = 1 + cooldown_time = 1 SECONDS * revive_multiplier() + recalculation_speed = 2 + +/mob/living/simple_animal/hostile/asteroid/elite/pandora/proc/chaser_burst(target) + ranged_cooldown = world.time + cooldown_time * 2 + var/active_chasers = 0 + for(var/mob/living/M in orange(7, src)) + if(M.faction_check_mob(src)) + continue + if(active_chasers >= MAX_CHASERS) + return + var/obj/effect/temp_visual/hierophant/chaser/C = new(loc, src, M, chaser_speed, FALSE) + C.moving = 2 + C.standard_moving_before_recalc = recalculation_speed + C.moving_dir = text2dir(pick("NORTH", "SOUTH", "EAST", "WEST")) + active_chasers += 1 + +/mob/living/simple_animal/hostile/asteroid/elite/pandora/proc/singular_shot_line(procsleft, angleused, turf/T) + if(procsleft <= 0) + return + new /obj/effect/temp_visual/hierophant/blast/damaging/pandora(T, src, TRUE) + T = get_step(T, angleused) + procsleft = procsleft - 1 + addtimer(CALLBACK(src, .proc/singular_shot_line, procsleft, angleused, T), cooldown_time * 0.1) + +/mob/living/simple_animal/hostile/asteroid/elite/pandora/proc/magic_box(target) + ranged_cooldown = world.time + cooldown_time + var/turf/T = get_turf(target) + for(var/t in spiral_range_turfs(3, T)) + if(get_dist(t, T) > 1) + new /obj/effect/temp_visual/hierophant/blast/damaging/pandora(t, src, TRUE) + +/mob/living/simple_animal/hostile/asteroid/elite/pandora/proc/pandora_teleport(target) + var/turf/turf_target = get_turf(target) + if(!(turf_target in view(12, src))) + return + ranged_cooldown = world.time + cooldown_time * 2 + var/turf/source = get_turf(src) + new /obj/effect/temp_visual/hierophant/telegraph(turf_target, src) + new /obj/effect/temp_visual/hierophant/telegraph(source, src) + playsound(source,'sound/machines/airlock_open.ogg', 200, 1) + addtimer(CALLBACK(src, .proc/pandora_teleport_2, turf_target, source), 2) + +/mob/living/simple_animal/hostile/asteroid/elite/pandora/proc/pandora_teleport_2(turf/T, turf/source) + new /obj/effect/temp_visual/hierophant/telegraph/teleport(T, src) + new /obj/effect/temp_visual/hierophant/telegraph/teleport(source, src) + for(var/t in RANGE_TURFS(1, T)) + new /obj/effect/temp_visual/hierophant/blast/damaging/pandora(t, src, TRUE) + for(var/t in RANGE_TURFS(1, source)) + new /obj/effect/temp_visual/hierophant/blast/damaging/pandora(t, src, TRUE) + animate(src, alpha = 0, time = 2, easing = EASE_OUT) //fade out + visible_message("[src] fades out!") + set_density(FALSE) + addtimer(CALLBACK(src, .proc/pandora_teleport_3, T), 2) + +/mob/living/simple_animal/hostile/asteroid/elite/pandora/proc/pandora_teleport_3(turf/T) + forceMove(T) + animate(src, alpha = 255, time = 2, easing = EASE_IN) //fade IN + set_density(TRUE) + visible_message("[src] fades in!") + +/mob/living/simple_animal/hostile/asteroid/elite/pandora/proc/aoe_squares(target) + ranged_cooldown = world.time + cooldown_time * 2 + var/turf/T = get_turf(target) + new /obj/effect/temp_visual/hierophant/blast/damaging/pandora(T, src, TRUE) + var/max_size = 3 + addtimer(CALLBACK(src, .proc/aoe_squares_2, T, 0, max_size), 2) + +/mob/living/simple_animal/hostile/asteroid/elite/pandora/proc/aoe_squares_2(turf/T, ring, max_size) + if(ring > max_size) + return + for(var/t in spiral_range_turfs(ring, T)) + if(get_dist(t, T) == ring) + new /obj/effect/temp_visual/hierophant/blast/damaging/pandora(t, src, TRUE) + addtimer(CALLBACK(src, .proc/aoe_squares_2, T, (ring + 1), max_size), cooldown_time * 0.1) + +//The specific version of hiero's squares pandora uses +/obj/effect/temp_visual/hierophant/blast/damaging/pandora + damage = 30 + monster_damage_boost = FALSE + friendly_fire_check = TRUE + + +//Pandora's loot: Hope //Hope I know what to make it do +/obj/item/clothing/accessory/necklace/pandora_hope + name = "Hope" + desc = "Found at the bottom of Pandora. After all the evil was released, this was the only thing left inside." + icon = 'icons/obj/lavaland/elite_trophies.dmi' + icon_state = "hope" + item_state = "hope" + item_color = "hope" + slot_flags = SLOT_TIE + allow_duplicates = FALSE + resistance_flags = FIRE_PROOF + +/obj/item/clothing/accessory/necklace/pandora_hope/on_attached(obj/item/clothing/under/S, mob/user) + . = ..() + if(isliving(user)) + var/mob/living/M = user + M.apply_status_effect(STATUS_EFFECT_HOPE) + +/obj/item/clothing/accessory/necklace/pandora_hope/on_removed(mob/user) + . = ..() + if(isliving(user)) + var/mob/living/M = user + M.remove_status_effect(STATUS_EFFECT_HOPE) + +/obj/item/clothing/accessory/necklace/pandora_hope/attached_unequip() + if(isliving(usr)) + var/mob/living/M = usr + M.remove_status_effect(STATUS_EFFECT_HOPE) + return ..() + +/obj/item/clothing/accessory/necklace/pandora_hope/attached_equip() + if(isliving(usr)) + var/mob/living/M = usr + M.apply_status_effect(STATUS_EFFECT_HOPE) + return ..() + +#undef CHASER_BURST +#undef MAGIC_BOX +#undef PANDORA_TELEPORT +#undef AOE_SQUARES +#undef MAX_CHASERS diff --git a/code/modules/mob/living/simple_animal/hostile/mining/mining.dm b/code/modules/mob/living/simple_animal/hostile/mining/mining.dm index d5f60384e1f..2e6ad1055ef 100644 --- a/code/modules/mob/living/simple_animal/hostile/mining/mining.dm +++ b/code/modules/mob/living/simple_animal/hostile/mining/mining.dm @@ -22,6 +22,7 @@ mob_size = MOB_SIZE_LARGE var/icon_aggro = null var/crusher_drop_mod = 25 + var/has_laser_resist = TRUE //If we want the mob to have 66% resist from burn damage projectiles /mob/living/simple_animal/hostile/asteroid/Aggro() ..() @@ -37,7 +38,7 @@ /mob/living/simple_animal/hostile/asteroid/bullet_act(obj/item/projectile/P)//Reduces damage from most projectiles to curb off-screen kills if(!stat) Aggro() - if(P.damage < 30 && P.damage_type != BRUTE) + if(P.damage < 30 && P.damage_type != BRUTE && has_laser_resist) P.damage = (P.damage / 3) visible_message("[P] has a reduced effect on [src]!") ..() diff --git a/config/example/config.toml b/config/example/config.toml index d549b745ad9..82cd6208323 100644 --- a/config/example/config.toml +++ b/config/example/config.toml @@ -699,6 +699,7 @@ active_lava_ruins = [ "_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_cube.dmm", "_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_cultaltar.dmm", "_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_dead_ratvar.dmm", + "_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_elite_tumor.dmm", "_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_fountain_hall.dmm", "_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_monster_nest.dmm", "_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_pizzaparty.dmm", diff --git a/icons/mob/actions/actions_elites.dmi b/icons/mob/actions/actions_elites.dmi new file mode 100644 index 00000000000..814b6376119 Binary files /dev/null and b/icons/mob/actions/actions_elites.dmi differ diff --git a/icons/mob/lavaland/lavaland_elites.dmi b/icons/mob/lavaland/lavaland_elites.dmi new file mode 100644 index 00000000000..b53c0383e63 Binary files /dev/null and b/icons/mob/lavaland/lavaland_elites.dmi differ diff --git a/icons/mob/screen_alert.dmi b/icons/mob/screen_alert.dmi index a45d4b060ee..5b65a8175f1 100644 Binary files a/icons/mob/screen_alert.dmi and b/icons/mob/screen_alert.dmi differ diff --git a/icons/mob/ties.dmi b/icons/mob/ties.dmi index e399e5e3a8b..f6834ccacc2 100644 Binary files a/icons/mob/ties.dmi and b/icons/mob/ties.dmi differ diff --git a/icons/obj/clothing/ties_overlay.dmi b/icons/obj/clothing/ties_overlay.dmi index fd3b6023185..d7d8d55bc17 100644 Binary files a/icons/obj/clothing/ties_overlay.dmi and b/icons/obj/clothing/ties_overlay.dmi differ diff --git a/icons/obj/lavaland/artefacts.dmi b/icons/obj/lavaland/artefacts.dmi index 0408962f9cc..d4a75fd23c7 100644 Binary files a/icons/obj/lavaland/artefacts.dmi and b/icons/obj/lavaland/artefacts.dmi differ diff --git a/icons/obj/lavaland/elite_trophies.dmi b/icons/obj/lavaland/elite_trophies.dmi new file mode 100644 index 00000000000..d194c93853d Binary files /dev/null and b/icons/obj/lavaland/elite_trophies.dmi differ diff --git a/icons/obj/lavaland/legionnaire_bonfire.dmi b/icons/obj/lavaland/legionnaire_bonfire.dmi new file mode 100644 index 00000000000..aed00ed001e Binary files /dev/null and b/icons/obj/lavaland/legionnaire_bonfire.dmi differ diff --git a/icons/obj/lavaland/tumor.dmi b/icons/obj/lavaland/tumor.dmi new file mode 100644 index 00000000000..a41224c8232 Binary files /dev/null and b/icons/obj/lavaland/tumor.dmi differ diff --git a/paradise.dme b/paradise.dme index dda9eb652d7..9c8a6a9ca50 100644 --- a/paradise.dme +++ b/paradise.dme @@ -2122,6 +2122,11 @@ #include "code\modules\mob\living\simple_animal\hostile\mining\gutlunch.dm" #include "code\modules\mob\living\simple_animal\hostile\mining\hivelord.dm" #include "code\modules\mob\living\simple_animal\hostile\mining\mining.dm" +#include "code\modules\mob\living\simple_animal\hostile\mining\elites\elite.dm" +#include "code\modules\mob\living\simple_animal\hostile\mining\elites\goliath_broodmother.dm" +#include "code\modules\mob\living\simple_animal\hostile\mining\elites\herald.dm" +#include "code\modules\mob\living\simple_animal\hostile\mining\elites\legionnaire.dm" +#include "code\modules\mob\living\simple_animal\hostile\mining\elites\pandora.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\clown.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\drone.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\fish.dm"