diff --git a/_maps/RandomRuins/LavaRuins/lavaland_surface_shuttle_wreckage.dmm b/_maps/RandomRuins/LavaRuins/lavaland_surface_shuttle_wreckage.dmm index 7a8c7bfacdc..b81d20e262d 100644 --- a/_maps/RandomRuins/LavaRuins/lavaland_surface_shuttle_wreckage.dmm +++ b/_maps/RandomRuins/LavaRuins/lavaland_surface_shuttle_wreckage.dmm @@ -284,7 +284,7 @@ /turf/open/misc/asteroid/basalt/lava_land_surface/biome_replace, /area/ruin/unpowered) "KN" = ( -/obj/structure/spawner/lavaland/legion, +/mob/living/basic/mining/tendril, /turf/open/misc/asteroid/basalt/lava_land_surface/biome_replace, /area/ruin/unpowered) "Lz" = ( diff --git a/code/__DEFINES/ai/monsters.dm b/code/__DEFINES/ai/monsters.dm index 1f54a54784d..8a4e9d56aa5 100644 --- a/code/__DEFINES/ai/monsters.dm +++ b/code/__DEFINES/ai/monsters.dm @@ -52,6 +52,11 @@ /// Key where goliath stores a hole it wants to get into #define BB_GOLIATH_HOLE_TARGET "BB_goliath_hole" +// Tendril AI keys +#define BB_TENDRIL_LASH "tendril_lash" +#define BB_TENDRIL_CHASER "tendril_chaser" +#define BB_TENDRIL_SPIKES "tendril_spikes" + // bee keys ///the bee hive we live inside #define BB_CURRENT_HOME "BB_current_home" diff --git a/code/__DEFINES/dcs/signals/signals_mining.dm b/code/__DEFINES/dcs/signals/signals_mining.dm index 13ec648c1ba..494068f3707 100644 --- a/code/__DEFINES/dcs/signals/signals_mining.dm +++ b/code/__DEFINES/dcs/signals/signals_mining.dm @@ -4,6 +4,9 @@ /// Fired by a mob which has been grabbed by a goliath #define COMSIG_GOLIATH_TENTACLED_GRABBED "comsig_goliath_tentacle_grabbed" #define COMPONENT_GOLIATH_CANCEL_TENTACLE_GRAB (1<<0) +/// Fired by a mob which has been grabbed by a tendril +#define COMSIG_TENDRIL_TENTACLED_GRABBED "comsig_tendril_tentacle_grabbed" + #define COMPONENT_TENDRIL_CANCEL_TENTACLE_GRAB (1<<0) /// Fired by a goliath tentacle which is returning to the earth #define COMSIG_GOLIATH_TENTACLE_RETRACTING "comsig_goliath_tentacle_retracting" /// Fired by a mob which has triggered a brimdust explosion from itself (not the mobs that get hit) diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm index 8fc2af35f4b..317b17c2d7e 100644 --- a/code/__DEFINES/traits/declarations.dm +++ b/code/__DEFINES/traits/declarations.dm @@ -1663,6 +1663,9 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai /// Sunlight on this turf is blocked and thus you can't get solar power or whatever #define TRAIT_TURF_SUN_BLOCKED "turf_sun_blocked" +/// Cannot be backstabbed with a crusher +#define TRAIT_BACKSTAB_IMMUNE "backstab_immune" + /// Makes the owner immune from the pacification from synthpax #define TRAIT_SYNTHPAX_IMMUNE "synthpax_immune" diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm index 9dcc0090962..24127005b6e 100644 --- a/code/_globalvars/traits/_traits.dm +++ b/code/_globalvars/traits/_traits.dm @@ -188,6 +188,7 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_ANXIOUS" = TRAIT_ANXIOUS, "TRAIT_APATHETIC" = TRAIT_APATHETIC, "TRAIT_ATTEMPTING_CHRISTENING" = TRAIT_ATTEMPTING_CHRISTENING, + "TRAIT_BACKSTAB_IMMUNE" = TRAIT_BACKSTAB_IMMUNE, "TRAIT_BADDNA" = TRAIT_BADDNA, "TRAIT_BADTOUCH" = TRAIT_BADTOUCH, "TRAIT_BALD" = TRAIT_BALD, diff --git a/code/datums/actions/mobs/projectileattack.dm b/code/datums/actions/mobs/projectileattack.dm index f27c0671c1f..183df71e03f 100644 --- a/code/datums/actions/mobs/projectileattack.dm +++ b/code/datums/actions/mobs/projectileattack.dm @@ -5,7 +5,7 @@ desc = "Fires a set of projectiles at a selected target." cooldown_time = 1.5 SECONDS /// The type of the projectile to be fired - var/projectile_type + var/obj/projectile/projectile_type /// The sound played when a projectile is fired var/projectile_sound /// If the projectile should home in on its target @@ -58,7 +58,7 @@ our_projectile.set_homing_target(target) if(isnum(set_angle)) our_projectile.fire(set_angle) - return + return our_projectile our_projectile.fire() return our_projectile diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm index 9d9f995e17f..5b41ae5254d 100644 --- a/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm @@ -24,17 +24,8 @@ var/atom/target = controller.blackboard[target_key] if (isnull(target)) return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED - if (!target.IsReachableBy(controller.pawn)) - controller.clear_blackboard_key(BB_BASIC_MOB_MELEE_COOLDOWN_TIMER) - return AI_BEHAVIOR_INSTANT - var/can_attack_time = controller.blackboard[BB_BASIC_MOB_MELEE_COOLDOWN_TIMER] - if (isnull(can_attack_time)) - var/blackboard_delay = controller.blackboard[BB_BASIC_MOB_MELEE_DELAY] - var/attack_delay = isnull(blackboard_delay) ? DEFAULT_ATTACK_DELAY : blackboard_delay - controller.set_blackboard_key(BB_BASIC_MOB_MELEE_COOLDOWN_TIMER, world.time + attack_delay) - return AI_BEHAVIOR_INSTANT - if (can_attack_time > world.time) + if (!can_attack(controller, target)) return AI_BEHAVIOR_INSTANT if (isliving(controller.pawn)) @@ -56,6 +47,23 @@ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED return AI_BEHAVIOR_DELAY +/datum/ai_behavior/basic_melee_attack/proc/can_attack(datum/ai_controller/controller, atom/target) + if (!target.IsReachableBy(controller.pawn)) + controller.clear_blackboard_key(BB_BASIC_MOB_MELEE_COOLDOWN_TIMER) + return FALSE + + var/can_attack_time = controller.blackboard[BB_BASIC_MOB_MELEE_COOLDOWN_TIMER] + if (isnull(can_attack_time)) + var/blackboard_delay = controller.blackboard[BB_BASIC_MOB_MELEE_DELAY] + var/attack_delay = isnull(blackboard_delay) ? DEFAULT_ATTACK_DELAY : blackboard_delay + controller.set_blackboard_key(BB_BASIC_MOB_MELEE_COOLDOWN_TIMER, world.time + attack_delay) + return FALSE + + if (can_attack_time > world.time) + return FALSE + + return TRUE + /datum/ai_behavior/basic_melee_attack/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key) . = ..() controller.clear_blackboard_key(BB_BASIC_MOB_MELEE_COOLDOWN_TIMER) @@ -78,7 +86,7 @@ /// range we will try chasing the target before giving up var/chase_range = 9 ///do we care about avoiding friendly fire? - var/avoid_friendly_fire = FALSE + var/avoid_friendly_fire = FALSE /datum/ai_behavior/basic_ranged_attack/setup(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key) . = ..() diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm index bf7d128fef9..b2aaaa23578 100644 --- a/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm @@ -35,7 +35,6 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED var/aggro_range = controller.blackboard[aggro_range_key] || vision_range - controller.clear_blackboard_key(target_key) // If we're using a field rn, just don't do anything yeah? diff --git a/code/datums/ai/basic_mobs/basic_subtrees/simple_find_target.dm b/code/datums/ai/basic_mobs/basic_subtrees/simple_find_target.dm index 52524852961..9824d41f823 100644 --- a/code/datums/ai/basic_mobs/basic_subtrees/simple_find_target.dm +++ b/code/datums/ai/basic_mobs/basic_subtrees/simple_find_target.dm @@ -3,10 +3,12 @@ var/target_key = BB_BASIC_MOB_CURRENT_TARGET /// Targeting strategy key to use var/strategy_key = BB_TARGETING_STRATEGY + /// Behavior to use to find targets + var/target_behavior = /datum/ai_behavior/find_potential_targets /datum/ai_planning_subtree/simple_find_target/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) . = ..() - controller.queue_behavior(/datum/ai_behavior/find_potential_targets, target_key, strategy_key, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION) + controller.queue_behavior(target_behavior, target_key, strategy_key, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION) // Prevents finding a target if a human is nearby /datum/ai_planning_subtree/simple_find_target/not_while_observed @@ -21,9 +23,8 @@ target_key = BB_BASIC_MOB_FLEE_TARGET /datum/ai_planning_subtree/simple_find_target/increased_range - -/datum/ai_planning_subtree/simple_find_target/increased_range/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) - controller.queue_behavior(/datum/ai_behavior/find_potential_targets/bigger_range, target_key, strategy_key, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION) + target_behavior = /datum/ai_behavior/find_potential_targets/bigger_range /datum/ai_planning_subtree/simple_find_target/hunt strategy_key = BB_HUNT_TARGETING_STRATEGY + diff --git a/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm b/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm index f1c04e1d8a7..c8699405461 100644 --- a/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm +++ b/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm @@ -9,9 +9,11 @@ var/hiding_place_key = BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION /// do we check for faction? var/check_faction = FALSE + /// Behavior to use to select our target + var/target_behavior = /datum/ai_behavior/target_from_retaliate_list /datum/ai_planning_subtree/target_retaliate/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) - controller.queue_behavior(/datum/ai_behavior/target_from_retaliate_list, BB_BASIC_MOB_RETALIATE_LIST, target_key, targeting_strategy_key, hiding_place_key, check_faction) + controller.queue_behavior(target_behavior, BB_BASIC_MOB_RETALIATE_LIST, target_key, targeting_strategy_key, hiding_place_key, check_faction) /datum/ai_planning_subtree/target_retaliate/check_faction check_faction = TRUE diff --git a/code/datums/ai/telegraph_effects.dm b/code/datums/ai/telegraph_effects.dm index 62bbe9a0397..b658c651988 100644 --- a/code/datums/ai/telegraph_effects.dm +++ b/code/datums/ai/telegraph_effects.dm @@ -6,6 +6,14 @@ light_range = 1 duration = 2 SECONDS +/obj/effect/temp_visual/telegraphing/Initialize(mapload) + . = ..() + update_appearance(UPDATE_OVERLAYS) + +/obj/effect/temp_visual/telegraphing/update_overlays() + . = ..() + . += emissive_appearance(icon, icon_state, src, alpha = 90) + /obj/effect/temp_visual/telegraphing/vending_machine_tilt duration = 1 SECONDS @@ -15,7 +23,18 @@ src.duration = duration return ..() -/obj/effect/temp_visual/telegraphing/thunderbolt +/obj/effect/temp_visual/telegraphing/circle icon = 'icons/mob/telegraphing/telegraph.dmi' icon_state = "target_circle" duration = 2 SECONDS + +/obj/effect/temp_visual/telegraphing/circle/short + duration = 1 SECONDS + +/obj/effect/temp_visual/telegraphing/line + icon = 'icons/mob/telegraphing/telegraph.dmi' + icon_state = "line" + duration = 0.8 SECONDS + +/obj/effect/temp_visual/telegraphing/line/short + duration = 0.5 SECONDS diff --git a/code/datums/mapgen/CaveGenerator.dm b/code/datums/mapgen/CaveGenerator.dm index 097e8740aff..63b47165db4 100644 --- a/code/datums/mapgen/CaveGenerator.dm +++ b/code/datums/mapgen/CaveGenerator.dm @@ -325,7 +325,7 @@ is_megafauna = TRUE var/can_spawn = TRUE - if(ispath(picked_mob, /obj/structure/spawner/lavaland)) + if(ispath(picked_mob, /mob/living/basic/mining/tendril)) // Prevents tendrils spawning in each other's collapse range for(var/turf/spawn_turf as anything in spawn_data[CAVE_SPAWN_TENDRIL]) if (get_dist(spawn_turf, target_turf) <= 2) @@ -352,7 +352,7 @@ if (!can_spawn) continue - if (ispath(picked_mob, /obj/structure/spawner/lavaland)) + if (ispath(picked_mob, /mob/living/basic/mining/tendril)) spawn_data[CAVE_SPAWN_TENDRIL] += target_turf else if (is_megafauna) diff --git a/code/datums/mapgen/biomes/_biome.dm b/code/datums/mapgen/biomes/_biome.dm index 8dc588680a8..dc2a17563b3 100644 --- a/code/datums/mapgen/biomes/_biome.dm +++ b/code/datums/mapgen/biomes/_biome.dm @@ -33,6 +33,8 @@ var/mob_exclusion_radius = 12 /// Radius around megafauna within which we avoid spawning tendrils var/megafauna_exclusion_radius = 7 + /// Minimum distance between tendril spawns + var/tendril_exclusion_radius = 12 /datum/biome/New() . = ..() @@ -168,18 +170,21 @@ is_megafauna = TRUE var/can_spawn = TRUE - if(ispath(picked_mob, /obj/structure/spawner/lavaland)) - // Prevents tendrils spawning in each other's collapse range + if(ispath(picked_mob, /mob/living/basic/mining/tendril)) for(var/turf/spawn_turf as anything in spawn_data[CAVE_SPAWN_TENDRIL]) - if (get_dist(spawn_turf, target_turf) <= 2) + if (get_dist(spawn_turf, target_turf) <= tendril_exclusion_radius) can_spawn = FALSE break + if (!can_spawn) + continue + // Also avoid spawning them next to megafauna for(var/turf/spawn_turf as anything in spawn_data[CAVE_SPAWN_MEGAFAUNA]) if (get_dist(spawn_turf, target_turf) <= megafauna_exclusion_radius) can_spawn = FALSE break + else if (is_megafauna) // Megafauna can spawn wherever it wants as long as its not next to another mega for(var/turf/spawn_turf as anything in spawn_data[CAVE_SPAWN_MEGAFAUNA]) @@ -195,12 +200,11 @@ if (!can_spawn) continue - if (ispath(picked_mob, /obj/structure/spawner/lavaland)) + if (ispath(picked_mob, /mob/living/basic/mining/tendril)) spawn_data[CAVE_SPAWN_TENDRIL] += target_turf - else - if (is_megafauna) - spawn_data[CAVE_SPAWN_MEGAFAUNA] += target_turf - spawn_data[CAVE_SPAWN_MOB] += target_turf + else if (is_megafauna) + spawn_data[CAVE_SPAWN_MEGAFAUNA] += target_turf + spawn_data[CAVE_SPAWN_MOB] += target_turf new picked_mob(target_turf) diff --git a/code/datums/mapgen/biomes/lavaland.dm b/code/datums/mapgen/biomes/lavaland.dm index 7461ef33d8a..88c907dc23f 100644 --- a/code/datums/mapgen/biomes/lavaland.dm +++ b/code/datums/mapgen/biomes/lavaland.dm @@ -1,6 +1,7 @@ /datum/biome/lavaland open_turf_type = /turf/open/misc/asteroid/basalt/lava_land_surface closed_turf_type = /turf/closed/mineral/volcanic + mob_exclusion_radius = 7 /datum/biome/lavaland/basalt closed_turf_type = /turf/closed/mineral/random/volcanic @@ -19,9 +20,7 @@ /mob/living/basic/mining/lobstrosity/lava = 20, /obj/effect/spawner/random/lavaland_mob/raptor = 15, /mob/living/basic/mining/goldgrub = 15, - /obj/structure/spawner/lavaland = 1, - /obj/structure/spawner/lavaland/goliath = 3, - /obj/structure/spawner/lavaland/legion = 3, + /mob/living/basic/mining/tendril = 7, ) flora_types = list( @@ -58,9 +57,7 @@ /mob/living/basic/mining/lobstrosity/lava = 40, /obj/effect/spawner/random/lavaland_mob/raptor = 15, /mob/living/basic/mining/goldgrub = 35, - /obj/structure/spawner/lavaland = 2, - /obj/structure/spawner/lavaland/goliath = 3, - /obj/structure/spawner/lavaland/legion = 3, + /mob/living/basic/mining/tendril = 8, ) flora_types = list( @@ -96,9 +93,7 @@ /mob/living/basic/mining/lobstrosity/lava = 25, /obj/effect/spawner/random/lavaland_mob/raptor = 20, /mob/living/basic/mining/goldgrub = 15, - /obj/structure/spawner/lavaland = 2, - /obj/structure/spawner/lavaland/goliath = 1, - /obj/structure/spawner/lavaland/legion = 3, + /mob/living/basic/mining/tendril = 6, ) flora_types = list( diff --git a/code/game/objects/structures/lavaland/necropolis_tendril.dm b/code/game/objects/structures/lavaland/necropolis_tendril.dm deleted file mode 100644 index e6ca59016ed..00000000000 --- a/code/game/objects/structures/lavaland/necropolis_tendril.dm +++ /dev/null @@ -1,141 +0,0 @@ -//Necropolis Tendrils, which spawn lavaland monsters and break into a chasm when killed -/obj/structure/spawner/lavaland - name = "necropolis tendril" - desc = "A vile tendril of corruption, originating deep underground. Terrible monsters are pouring out of it." - - icon = 'icons/mob/simple/lavaland/nest.dmi' - icon_state = "tendril" - - faction = list(FACTION_MINING, FACTION_ASHWALKER) - max_mobs = 3 - max_integrity = 250 - mob_types = list(/mob/living/basic/mining/watcher) - - move_resist=INFINITY // just killing it tears a massive hole in the ground, let's not move it - anchored = TRUE - resistance_flags = FIRE_PROOF | LAVA_PROOF - var/obj/effect/light_emitter/tendril/emitted_light - scanner_taggable = TRUE - mob_gps_id = "WT" - spawner_gps_id = "Necropolis Tendril" - -/obj/structure/spawner/lavaland/goliath - mob_types = list(/mob/living/basic/mining/goliath) - mob_gps_id = "GL" - -/obj/structure/spawner/lavaland/legion - mob_types = list(/mob/living/basic/mining/legion/spawner_made) - mob_gps_id = "LG" - -/obj/structure/spawner/lavaland/icewatcher - mob_types = list(/mob/living/basic/mining/watcher/icewing) - mob_gps_id = "WT|I" // icewing - -GLOBAL_LIST_INIT(tendrils, list()) -/obj/structure/spawner/lavaland/Initialize(mapload) - . = ..() - emitted_light = new(loc) - for(var/F in RANGE_TURFS(1, src)) - if(ismineralturf(F)) - var/turf/closed/mineral/M = F - M.ScrapeAway(null, CHANGETURF_IGNORE_AIR) - AddComponent(/datum/component/gps, "Eerie Signal") - GLOB.tendrils += src - -/obj/structure/spawner/lavaland/atom_deconstruct(disassembled) - new /obj/effect/collapse(loc) - -/obj/structure/spawner/lavaland/examine(mob/user) - var/list/examine_messages = ..() - examine_messages += span_notice("Once this thing gets hurts enough, it triggers a violent final retaliation.") - examine_messages += span_notice("You'll only have a few moments to run up, grab some loot with an open hand, and get out with it.") - return examine_messages - -/obj/structure/spawner/lavaland/Destroy() - var/last_tendril = TRUE - if(GLOB.tendrils.len>1) - last_tendril = FALSE - - if(last_tendril && !(flags_1 & ADMIN_SPAWNED_1)) - if(SSachievements.achievements_enabled) - for(var/mob/living/L in view(7,src)) - if(L.stat || !L.client) - continue - L.client.give_award(/datum/award/achievement/boss/tendril_exterminator, L) - L.client.give_award(/datum/award/score/tendril_score, L) //Progresses score by one - GLOB.tendrils -= src - QDEL_NULL(emitted_light) - return ..() - -/obj/effect/light_emitter/tendril - set_luminosity = 4 - set_cap = 2.5 - light_color = LIGHT_COLOR_LAVA - -/obj/effect/collapse - name = "collapsing necropolis tendril" - desc = "Get your loot and get clear!" - layer = TABLE_LAYER - icon = 'icons/mob/simple/lavaland/nest.dmi' - icon_state = "tendril" - anchored = TRUE - density = TRUE - /// weakref list of which mobs have gotten their loot from this effect. - var/list/collected = list() - /// a bit of light as to make less unfair deaths from the chasm - var/obj/effect/light_emitter/tendril/emitted_light - -/obj/effect/collapse/Initialize(mapload) - . = ..() - emitted_light = new(loc) - visible_message(span_bolddanger("The tendril writhes in fury as the earth around it begins to crack and break apart! Get back!")) - balloon_alert_to_viewers("interact to grab loot before collapse!", vision_distance = 7) - playsound(loc,'sound/effects/tendril_destroyed.ogg', 200, FALSE, 50, TRUE, TRUE) - addtimer(CALLBACK(src, PROC_REF(collapse)), 5 SECONDS) - -/obj/effect/collapse/examine(mob/user) - var/list/examine_messages = ..() - if(isliving(user)) - if(has_collected(user)) - examine_messages += span_boldnotice("You've grabbed what you can, now get out!") - else - examine_messages += span_boldnotice("You might have some time to grab some goodies with an open hand before it collapses!") - return examine_messages - -/obj/effect/collapse/attack_hand(mob/living/collector, list/modifiers) - . = ..() - if(has_collected(collector)) - to_chat(collector, span_danger("You've already gotten some loot, just get out of there with it!")) - return - visible_message(span_warning("Something falls free of the tendril!")) - var/obj/structure/closet/crate/necropolis/tendril/loot = new /obj/structure/closet/crate/necropolis/tendril(loc) - collector.start_pulling(loot) - collected += WEAKREF(collector) - -/obj/effect/collapse/Destroy() - collected.Cut() - QDEL_NULL(emitted_light) - return ..() - -///Helper proc that resolves weakrefs to determine if collector is in collected list, returning a boolean. -/obj/effect/collapse/proc/has_collected(mob/collector) - for(var/datum/weakref/weakref as anything in collected) - var/mob/living/resolved = weakref.resolve() - //it could have been collector, it could not have been, we don't care - if(!resolved) - continue - if(resolved == collector) - return TRUE - return FALSE - -/obj/effect/collapse/proc/collapse() - for(var/mob/M in range(7,src)) - shake_camera(M, 15, 1) - playsound(get_turf(src),'sound/effects/explosion/explosionfar.ogg', 200, TRUE) - visible_message(span_bolddanger("The tendril falls inward, the ground around it widening into a yawning chasm!")) - for(var/turf/T in RANGE_TURFS(2,src)) - if(HAS_TRAIT(T, TRAIT_NO_TERRAFORM)) - continue - if(!T.density) - T.TerraformTurf(/turf/open/chasm/lavaland, /turf/open/chasm/lavaland, flags = CHANGETURF_INHERIT_AIR) - qdel(src) diff --git a/code/modules/antagonists/changeling/powers/mutations.dm b/code/modules/antagonists/changeling/powers/mutations.dm index 20c4b7e220d..3dc6e81a6ef 100644 --- a/code/modules/antagonists/changeling/powers/mutations.dm +++ b/code/modules/antagonists/changeling/powers/mutations.dm @@ -478,7 +478,7 @@ return BULLET_ACT_HIT /obj/projectile/tentacle/Destroy() - qdel(chain) + QDEL_NULL(chain) source = null return ..() diff --git a/code/modules/clothing/suits/reactive_armour.dm b/code/modules/clothing/suits/reactive_armour.dm index 2f524915820..a38ffbadce8 100644 --- a/code/modules/clothing/suits/reactive_armour.dm +++ b/code/modules/clothing/suits/reactive_armour.dm @@ -534,7 +534,7 @@ shock_turf_windup(owner.loc) /obj/item/clothing/suit/armor/reactive/weather/proc/shock_turf_windup(turf/target) - new /obj/effect/temp_visual/telegraphing/thunderbolt(target) + new /obj/effect/temp_visual/telegraphing/circle(target) addtimer(CALLBACK(src, PROC_REF(shock_turf), target), 1 SECONDS) /obj/item/clothing/suit/armor/reactive/weather/proc/shock_turf(turf/target) diff --git a/code/modules/mapfluff/ruins/objects_and_mobs/ash_walker_den.dm b/code/modules/mapfluff/ruins/objects_and_mobs/ash_walker_den.dm index ce57035a766..b18e740f8cb 100644 --- a/code/modules/mapfluff/ruins/objects_and_mobs/ash_walker_den.dm +++ b/code/modules/mapfluff/ruins/objects_and_mobs/ash_walker_den.dm @@ -123,4 +123,77 @@ resistance_flags = FIRE_PROOF | LAVA_PROOF max_integrity = 200 +/obj/effect/light_emitter/tendril + set_luminosity = 4 + set_cap = 2.5 + light_color = LIGHT_COLOR_LAVA + +/obj/effect/collapse + name = "collapsing necropolis tendril" + desc = "Get your loot and get clear!" + layer = TABLE_LAYER + icon = 'icons/mob/simple/lavaland/nest.dmi' + icon_state = "tendril" + anchored = TRUE + density = TRUE + /// weakref list of which mobs have gotten their loot from this effect. + var/list/collected = list() + /// a bit of light as to make less unfair deaths from the chasm + var/obj/effect/light_emitter/tendril/emitted_light + +/obj/effect/collapse/Initialize(mapload) + . = ..() + emitted_light = new(loc) + visible_message(span_bolddanger("The tendril writhes in fury as the earth around it begins to crack and break apart! Get back!")) + balloon_alert_to_viewers("interact to grab loot before collapse!", vision_distance = 7) + playsound(loc,'sound/effects/tendril_destroyed.ogg', 200, FALSE, 50, TRUE, TRUE) + addtimer(CALLBACK(src, PROC_REF(collapse)), 5 SECONDS) + +/obj/effect/collapse/examine(mob/user) + var/list/examine_messages = ..() + if(isliving(user)) + if(has_collected(user)) + examine_messages += span_boldnotice("You've grabbed what you can, now get out!") + else + examine_messages += span_boldnotice("You might have some time to grab some goodies with an open hand before it collapses!") + return examine_messages + +/obj/effect/collapse/attack_hand(mob/living/collector, list/modifiers) + . = ..() + if(has_collected(collector)) + to_chat(collector, span_danger("You've already gotten some loot, just get out of there with it!")) + return + visible_message(span_warning("Something falls free of the tendril!")) + var/obj/structure/closet/crate/necropolis/tendril/loot = new /obj/structure/closet/crate/necropolis/tendril(loc) + collector.start_pulling(loot) + collected += WEAKREF(collector) + +/obj/effect/collapse/Destroy() + collected.Cut() + QDEL_NULL(emitted_light) + return ..() + +///Helper proc that resolves weakrefs to determine if collector is in collected list, returning a boolean. +/obj/effect/collapse/proc/has_collected(mob/collector) + for(var/datum/weakref/weakref as anything in collected) + var/mob/living/resolved = weakref.resolve() + //it could have been collector, it could not have been, we don't care + if(!resolved) + continue + if(resolved == collector) + return TRUE + return FALSE + +/obj/effect/collapse/proc/collapse() + for(var/mob/viewer in range(7, src)) + shake_camera(viewer, 15, 1) + playsound(get_turf(src),'sound/effects/explosion/explosionfar.ogg', 200, TRUE) + visible_message(span_bolddanger("The tendril falls inward, the ground around it widening into a yawning chasm!")) + for(var/turf/ground in RANGE_TURFS(2, src)) + if(HAS_TRAIT(ground, TRAIT_NO_TERRAFORM)) + continue + if(!ground.density) + ground.TerraformTurf(/turf/open/chasm/lavaland, /turf/open/chasm/lavaland, flags = CHANGETURF_INHERIT_AIR) + qdel(src) + #undef ASH_WALKER_SPAWN_THRESHOLD diff --git a/code/modules/mining/equipment/kinetic_crusher/kinetic_crusher.dm b/code/modules/mining/equipment/kinetic_crusher/kinetic_crusher.dm index bb07998534c..9f72047fcde 100644 --- a/code/modules/mining/equipment/kinetic_crusher/kinetic_crusher.dm +++ b/code/modules/mining/equipment/kinetic_crusher/kinetic_crusher.dm @@ -218,7 +218,7 @@ var/backstabbed = FALSE var/def_check = target.getarmor(type = BOMB) // Backstab bonus - if(check_behind(user, target) || boosted_mark) + if(check_behind(user, target) && !HAS_TRAIT(target, TRAIT_BACKSTAB_IMMUNE) || boosted_mark) backstabbed = TRUE combined_damage += backstab_bonus playsound(user, backstab_sound, 100, TRUE) //Seriously who spelled it wrong diff --git a/code/modules/mining/equipment/monster_organs/rush_gland.dm b/code/modules/mining/equipment/monster_organs/rush_gland.dm index 69c79974170..f22bf029a8c 100644 --- a/code/modules/mining/equipment/monster_organs/rush_gland.dm +++ b/code/modules/mining/equipment/monster_organs/rush_gland.dm @@ -24,6 +24,7 @@ /obj/item/organ/monster_core/rush_gland/on_mob_insert(mob/living/carbon/organ_owner) . = ..() RegisterSignal(organ_owner, COMSIG_GOLIATH_TENTACLED_GRABBED, PROC_REF(trigger_organ_action_on_sig)) + RegisterSignal(organ_owner, COMSIG_TENDRIL_TENTACLED_GRABBED, PROC_REF(trigger_organ_action_on_sig)) /obj/item/organ/monster_core/rush_gland/on_mob_remove(mob/living/carbon/organ_owner, special, movement_flags) . = ..() diff --git a/code/modules/mining/lavaland/mining_loot/megafauna/legion.dm b/code/modules/mining/lavaland/mining_loot/megafauna/legion.dm index cb8fa553271..4ec0dbfe49e 100644 --- a/code/modules/mining/lavaland/mining_loot/megafauna/legion.dm +++ b/code/modules/mining/lavaland/mining_loot/megafauna/legion.dm @@ -95,7 +95,7 @@ playsound(src, 'sound/effects/magic/lightningshock.ogg', 10, TRUE, extrarange = SILENCED_SOUND_EXTRARANGE, falloff_distance = 0) targeted_turfs += target_turf balloon_alert(user, "you aim at [target_turf]...") - new /obj/effect/temp_visual/telegraphing/thunderbolt(target_turf) + new /obj/effect/temp_visual/telegraphing/circle(target_turf) addtimer(CALLBACK(src, PROC_REF(throw_thunderbolt), target_turf, power_boosted), 1.5 SECONDS) thunder_charges-- addtimer(CALLBACK(src, PROC_REF(recharge)), thunder_charge_time) diff --git a/code/modules/mob/living/basic/lavaland/tendril/tendril.dm b/code/modules/mob/living/basic/lavaland/tendril/tendril.dm new file mode 100644 index 00000000000..b31e8a982a7 --- /dev/null +++ b/code/modules/mob/living/basic/lavaland/tendril/tendril.dm @@ -0,0 +1,155 @@ +#define HEARTBEAT_NORMAL (1.8 SECONDS) +#define HEARTBEAT_FAST (1 SECONDS) +#define HEARTBEAT_FRANTIC (0.4 SECONDS) + +GLOBAL_LIST_INIT(tendrils, list()) + +/mob/living/basic/mining/tendril + name = "necropolis tendril" + desc = "A vile tendril of corruption, originating deep underground." + icon = 'icons/mob/simple/lavaland/tendril.dmi' + icon_state = "tendril" + icon_living = "tendril" + pixel_w = -8 + base_pixel_w = -8 + status_flags = NONE + mob_biotypes = MOB_ORGANIC | MOB_SKELETAL | MOB_MINING + basic_mob_flags = DEL_ON_DEATH | IMMUNE_TO_FISTS + mob_size = MOB_SIZE_HUGE + maxHealth = 1200 + health = 1200 + + friendly_verb_continuous = "flails at" + friendly_verb_simple = "flail at" + speak_emote = list("resonates") + obj_damage = 100 + melee_damage_lower = 20 + melee_damage_upper = 20 + sharpness = SHARP_POINTY + wound_bonus = CANT_WOUND + attack_sound = 'sound/items/weapons/pierce.ogg' + attack_verb_continuous = "pierces through" + attack_verb_simple = "pierce through" + throw_blocked_message = "does nothing to the thick shell of" + move_resist = INFINITY + + light_range = 3 + light_power = 2 + light_color = LIGHT_COLOR_LAVA + + ai_controller = /datum/ai_controller/basic_controller/tendril + + /// Looping heartbeat sound + var/datum/looping_sound/heartbeat/soundloop + /// Melee attack ability to used in retaliation to melee strikes and whenever it manages to grab someone + var/datum/action/cooldown/mob_cooldown/projectile_attack/tendril_melee/tendril_melee + +/mob/living/basic/mining/tendril/Initialize(mapload) + . = ..() + GLOB.tendrils += src + AddElement(/datum/element/ai_retaliate) + AddElement(/datum/element/death_drops, /obj/structure/closet/crate/necropolis/tendril) + AddComponent(/datum/component/ai_target_timer) + AddComponent(/datum/component/gps, "Eerie Signal") + AddComponent(/datum/component/basic_mob_attack_telegraph, display_telegraph_overlay = FALSE, telegraph_duration = 0.4 SECONDS) + AddComponent(/datum/component/regenerator, regeneration_delay = 30 SECONDS, brute_per_second = 20) + add_traits(list(TRAIT_BACKSTAB_IMMUNE, TRAIT_IMMOBILIZED), INNATE_TRAIT) + + var/static/list/abilities = list( + /datum/action/cooldown/mob_cooldown/projectile_attack/tendril_lash = BB_TENDRIL_LASH, + /datum/action/cooldown/mob_cooldown/tendril_chaser = BB_TENDRIL_CHASER, + /datum/action/cooldown/mob_cooldown/tendril_cross_spikes = BB_TENDRIL_SPIKES, + ) + grant_actions_by_list(abilities) + + tendril_melee = new(src) + tendril_melee.Grant(src) + AddComponent(/datum/component/revenge_ability, tendril_melee, targeting = GET_TARGETING_STRATEGY(ai_controller.blackboard[BB_TARGETING_STRATEGY]), max_range = 2, target_self = TRUE) + + soundloop = new(src, start_immediately = FALSE) + soundloop.mid_length = HEARTBEAT_NORMAL + soundloop.pressure_affected = FALSE + soundloop.start() + update_appearance(UPDATE_OVERLAYS) + + var/turf/our_turf = get_turf(src) + for (var/turf/rock in range(4, src)) + var/dist = sqrt((rock.x - our_turf.x) ** 2 + (rock.y - our_turf.y) ** 2) + if (dist > 4.5) + continue + + if (ismineralturf(rock)) + rock.ScrapeAway(null, CHANGETURF_IGNORE_AIR) + + if (istype(rock, /turf/open/misc/asteroid) && prob(100 / sqrt(max(1, dist)))) + rock.ChangeTurf(/turf/open/indestructible/necropolis, null, CHANGETURF_IGNORE_AIR) + +/mob/living/basic/mining/tendril/Destroy() + GLOB.tendrils -= src + QDEL_NULL(soundloop) + + if(!SSachievements.achievements_enabled || (flags_1 & ADMIN_SPAWNED_1)) + return ..() + + for(var/mob/living/killer in view(7, src)) + if(killer.stat || !killer.client) + continue + killer.client.give_award(/datum/award/score/tendril_score, killer) + if (!length(GLOB.tendrils)) + killer.client.give_award(/datum/award/achievement/boss/tendril_exterminator, killer) + + return ..() + +/mob/living/basic/mining/tendril/update_overlays() + . = ..() + . += emissive_appearance(icon, "[icon_state]_e", src, effect_type = EMISSIVE_NO_BLOOM) + . += emissive_appearance(icon, "[icon_state]_e_bloom", src, effect_type = EMISSIVE_BLOOM) + +/mob/living/basic/mining/tendril/Life(seconds_per_tick) + . = ..() + update_heartbeat() // Just a single math op unless we need updating so its fine to put it here + +/mob/living/basic/mining/tendril/updatehealth() + . = ..() + update_heartbeat() + +/mob/living/basic/mining/tendril/proc/update_heartbeat() + if (!soundloop) + return + + var/beat_rate = HEARTBEAT_NORMAL + if (ai_controller?.blackboard[BB_BASIC_MOB_CURRENT_TARGET]) + beat_rate = round(HEARTBEAT_FRANTIC + health / maxHealth * (HEARTBEAT_FAST - HEARTBEAT_FRANTIC), 0.05 SECONDS) + + if (beat_rate != soundloop.mid_length) + soundloop.set_mid_length(beat_rate) + +/mob/living/basic/mining/tendril/do_attack_animation(atom/attacked_atom, visual_effect_icon, obj/item/used_item, no_effect, fov_effect = TRUE, item_animation_override = null) + if(!no_effect && (visual_effect_icon || used_item)) + do_item_attack_animation(attacked_atom, visual_effect_icon, used_item, animation_type = item_animation_override) + + // Stabby visuals + var/obj/effect/temp_visual/spike_stab/stab = new(get_turf(src)) + var/target_dir = get_dir(src, attacked_atom) + stab.transform = matrix().Turn(dir2angle(target_dir) + rand(-7, 7)) + if (target_dir & NORTH) + stab.pixel_z = 24 + else if (target_dir & SOUTH) + stab.pixel_z = -24 + if (target_dir & EAST) + stab.pixel_w = 24 + else if (target_dir & WEST) + stab.pixel_w = -24 + +/obj/effect/temp_visual/spike_stab + icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi' + icon_state = "spike_small" + duration = 0.4 SECONDS + +/mob/living/basic/mining/tendril/proc/snatch_react() + if (tendril_melee.IsAvailable()) + tendril_melee.Activate(warning = FALSE) + +#undef HEARTBEAT_NORMAL +#undef HEARTBEAT_FAST +#undef HEARTBEAT_FRANTIC diff --git a/code/modules/mob/living/basic/lavaland/tendril/tendril_actions.dm b/code/modules/mob/living/basic/lavaland/tendril/tendril_actions.dm new file mode 100644 index 00000000000..ba4bb3b219d --- /dev/null +++ b/code/modules/mob/living/basic/lavaland/tendril/tendril_actions.dm @@ -0,0 +1,358 @@ +/// Fires out two cross patterns of damaging tentacles which reel in anything they hit, then causes a followup attack +/datum/action/cooldown/mob_cooldown/projectile_attack/tendril_lash + name = "Tentacle Lash" + desc = "Lash out with your tentacles in 8 directions, reeling in whatever you hit and unleashing a deadly followup attack afterwards." + button_icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi' + button_icon_state = "goliath_tentacle_wiggle" + background_icon_state = "bg_demon" + overlay_icon_state = "bg_demon_border" + click_to_activate = FALSE + cooldown_time = 8 SECONDS + melee_cooldown_time = 0 + shared_cooldown = NONE + projectile_type = /obj/projectile/tentacle_lash + +/datum/action/cooldown/mob_cooldown/projectile_attack/tendril_lash/Activate(atom/target) + disable_cooldown_actions() + + for (var/swipe_dir in GLOB.cardinals) + var/turf/open/line_turf = get_step(owner, swipe_dir) + for (var/i in 1 to projectile_type::range) + if (!istype(line_turf) || line_turf.is_blocked_turf(exclude_mobs = TRUE)) + break + var/obj/effect/temp_visual/telegraphing/line/telegraph = new(line_turf) + telegraph.dir = swipe_dir + line_turf = get_step(line_turf, swipe_dir) + + SLEEP_CHECK_DEATH(0.8 SECONDS, owner) + + for (var/swipe_dir in GLOB.cardinals) + shoot_projectile(get_turf(owner), get_step(owner, swipe_dir), dir2angle(swipe_dir), owner) + + SLEEP_CHECK_DEATH(1.6 SECONDS, owner) + + for (var/swipe_dir in GLOB.diagonals) + var/turf/open/line_turf = get_step(owner, swipe_dir) + for (var/i in 1 to projectile_type::range) + if (!istype(line_turf) || line_turf.is_blocked_turf(exclude_mobs = TRUE)) + break + var/obj/effect/temp_visual/telegraphing/line/telegraph = new(line_turf) + telegraph.dir = swipe_dir + line_turf = get_step(line_turf, swipe_dir) + + SLEEP_CHECK_DEATH(0.8 SECONDS, owner) + + for (var/swipe_dir in GLOB.diagonals) + shoot_projectile(get_turf(owner), get_step(owner, swipe_dir), dir2angle(swipe_dir), owner) + + SLEEP_CHECK_DEATH(1.2 SECONDS, owner) + StartCooldown() + SLEEP_CHECK_DEATH(0.6 SECONDS, owner) + enable_cooldown_actions() + return TRUE + +/obj/projectile/tentacle_lash + name = "tentacle spike" + icon_state = "tentacle_spike" + pass_flags = PASSTABLE + damage = 5 // +10 from the grab + armor_flag = MELEE + range = 7 + hit_prone_targets = TRUE + hitsound = 'sound/effects/wounds/pierce1.ogg' + sharpness = SHARP_POINTY + /// Beam connecting us and the firer + var/datum/beam/tentacle_beam = null + /// Does this projectile persist and reel in targets? + var/reel_in = TRUE + /// How long does the projectile persist? + var/duration = 1.2 SECONDS + /// Damage dealt to targets who get snatched from entering the beam or being hit directly + var/snatch_damage = 10 + +/obj/projectile/tentacle_lash/stab + damage = 15 + range = 2 + duration = 0.2 SECONDS // Just for visual flair + reel_in = FALSE + +/obj/projectile/tentacle_lash/fire(fire_angle, atom/direct_target) + . = ..() + if (!firer) + return + tentacle_beam = Beam(firer, "goliath_tentacle", beam_type = (reel_in ? /obj/effect/ebeam/reacting : /obj/effect/ebeam), emissive = FALSE) + if (reel_in) + RegisterSignal(tentacle_beam, COMSIG_BEAM_ENTERED, PROC_REF(on_beam_entered)) + +/obj/projectile/tentacle_lash/Destroy() + QDEL_NULL(tentacle_beam) + return ..() + +// Don't range out, stop and persist until we're done +/obj/projectile/tentacle_lash/on_range() + STOP_PROCESSING(SSprojectiles, src) + // Reset to tile center + pixel_x = 0 + pixel_y = 0 + QDEL_IN(src, duration) + +/obj/projectile/tentacle_lash/prehit_pierce(atom/target) + // Ye 'ole colossus cheese + if (astype(target, /mob/living)?.stat == DEAD) + return PROJECTILE_PIERCE_PHASE + return ..() + +/obj/projectile/tentacle_lash/on_hit(atom/target, blocked, pierce_hit) + . = ..() + if (!reel_in || !isliving(target) || blocked >= 100 || pierce_hit || . != BULLET_ACT_HIT) + return + snatch_target(target) + +/obj/projectile/tentacle_lash/proc/on_beam_entered(datum/beam/source, obj/effect/ebeam/hit, atom/movable/entered) + SIGNAL_HANDLER + + if (!reel_in || entered == firer || !isliving(entered)) + return + + var/mob/living/victim = entered + if ((!firer || !firer.faction_check_atom(victim)) && victim.stat != DEAD) + INVOKE_ASYNC(src, PROC_REF(snatch_target), entered) + +/obj/projectile/tentacle_lash/proc/snatch_target(mob/living/victim) + if (HAS_TRAIT(victim, TRAIT_TENTACLE_IMMUNE) || SEND_SIGNAL(victim, COMSIG_TENDRIL_TENTACLED_GRABBED) & COMPONENT_TENDRIL_CANCEL_TENTACLE_GRAB) + return + + to_chat(victim, span_userdanger("You're snatched by [firer]'s tentacles!")) + victim.apply_damage(snatch_damage, BRUTE, BODY_ZONE_CHEST, wound_bonus = CANT_WOUND) + if (QDELETED(victim)) + qdel(src) + return + var/snatch_callback = null + if (istype(firer, /mob/living/basic/mining/tendril)) + var/mob/living/basic/mining/tendril/tendril = firer + snatch_callback = CALLBACK(tendril, TYPE_PROC_REF(/mob/living/basic/mining/tendril, snatch_react)) + victim.throw_at(firer, initial(range), 1, firer, FALSE, gentle = TRUE, callback = snatch_callback) + playsound(victim, hitsound, 50, -3, pressure_affected = FALSE) + qdel(src) + +/// An ability which makes spikes come out of the ground towards your target +/datum/action/cooldown/mob_cooldown/tendril_chaser + name = "Impaling Spikes" + desc = "Send a spiked subterranean tendril chasing after your target." + button_icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi' + button_icon_state = "spikes_stabbing" + background_icon_state = "bg_demon" + overlay_icon_state = "bg_demon_border" + cooldown_time = 8 SECONDS + click_to_activate = TRUE + shared_cooldown = NONE + /// Lazy list of references to spike trails + var/list/active_chasers + /// Health percentage threshold at which we send out wide charsers after the main target + var/wide_chaser_threshold = 0.7 + +/datum/action/cooldown/mob_cooldown/tendril_chaser/Grant(mob/granted_to) + . = ..() + RegisterSignal(granted_to, COMSIG_MOB_ABILITY_STARTED, PROC_REF(on_ability_started)) + RegisterSignal(granted_to, COMSIG_MOB_ABILITY_FINISHED, PROC_REF(on_ability_finished)) + +// Clean up after ourselves +/datum/action/cooldown/mob_cooldown/tendril_chaser/Remove(mob/removed_from) + UnregisterSignal(removed_from, list(COMSIG_MOB_ABILITY_STARTED, COMSIG_MOB_ABILITY_FINISHED)) + QDEL_LIST(active_chasers) + return ..() + +/datum/action/cooldown/mob_cooldown/tendril_chaser/proc/on_ability_started(mob/living/owner, datum/action/cooldown/activated) + SIGNAL_HANDLER + + // Delete all of our chasers when our owner triggers cross spikes as to not cause guaranteed damage + if (istype(activated, /datum/action/cooldown/mob_cooldown/tendril_cross_spikes)) + QDEL_LIST(active_chasers) + +/datum/action/cooldown/mob_cooldown/tendril_chaser/proc/on_ability_finished(mob/living/owner, datum/action/cooldown/activated) + SIGNAL_HANDLER + + if (istype(activated, /datum/action/cooldown/mob_cooldown/tendril_cross_spikes)) + ResetCooldown() + +/datum/action/cooldown/mob_cooldown/tendril_chaser/Activate(atom/target) + . = ..() + var/primary_type = /obj/effect/temp_visual/effect_trail/tendril_chaser + if (isliving(owner)) + var/mob/living/as_living = owner + if (as_living.health / as_living.maxHealth <= wide_chaser_threshold) + primary_type = /obj/effect/temp_visual/effect_trail/tendril_chaser/wide_area + + var/obj/effect/temp_visual/effect_trail/tendril_chaser/chaser = new primary_type(get_turf(owner), target) + LAZYADD(active_chasers, WEAKREF(chaser)) + RegisterSignal(chaser, COMSIG_QDELETING, PROC_REF(on_chaser_destroyed)) + playsound(owner, 'sound/effects/magic/demon_attack1.ogg', vol = 100, vary = TRUE, pressure_affected = FALSE) + +/// Remove a spike trail from our list of active trails +/datum/action/cooldown/mob_cooldown/tendril_chaser/proc/on_chaser_destroyed(atom/chaser) + SIGNAL_HANDLER + LAZYREMOVE(active_chasers, WEAKREF(chaser)) + +/obj/effect/temp_visual/effect_trail/tendril_chaser + duration = 10 SECONDS + move_speed = 4 + spawned_effect = /obj/effect/temp_visual/emerging_ground_spike/tendril + /// Do we spawn spikes around ourselves as well or only on our own turf? + var/area_spawn = FALSE + +/obj/effect/temp_visual/effect_trail/tendril_chaser/wide_area + area_spawn = TRUE + +/obj/effect/temp_visual/effect_trail/tendril_chaser/add_spawner() + return + +/obj/effect/temp_visual/effect_trail/tendril_chaser/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change) + . = ..() + if (!area_spawn) + var/turf/spawn_turf = get_turf(src) + if (!(locate(/obj/effect/temp_visual/emerging_ground_spike/tendril) in spawn_turf) && isopenturf(spawn_turf)) + new spawned_effect(spawn_turf) + return + + for (var/spawn_dir in GLOB.cardinals) + if (spawn_dir & REVERSE_DIR(movement_dir)) + continue + var/turf/spawn_loc = get_step(src, spawn_dir) + if (!(locate(/obj/effect/temp_visual/emerging_ground_spike/tendril) in spawn_loc) && isopenturf(spawn_loc)) + new spawned_effect(spawn_loc) + +/obj/effect/temp_visual/emerging_ground_spike/tendril + icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi' + icon_state = "spikes_stabbing" + duration = 0.7 SECONDS + position_variance = 3 + impale_damage = 10 + damage_blacklist_typecache = list( + /mob/living/basic/mining/tendril, + ) + impale_wound_bonus = CANT_WOUND + // Have we hit someone yet? + var/hit_loser = FALSE + +/obj/effect/temp_visual/emerging_ground_spike/tendril/single + icon_state = "spike" + duration = 1 SECONDS + harm_delay = 0.25 SECONDS + position_variance = 5 + +/obj/effect/temp_visual/emerging_ground_spike/tendril/impale() + . = ..() + hit_loser |= . + RegisterSignal(loc, COMSIG_ATOM_ENTERED, PROC_REF(on_entered)) + +/obj/effect/temp_visual/emerging_ground_spike/tendril/proc/on_entered(atom/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs) + SIGNAL_HANDLER + + if (!isliving(arrived)) + return + + if (harm_mob(arrived) && !hit_loser) + playsound(src, 'sound/items/weapons/slice.ogg', vol = 50, vary = TRUE, pressure_affected = FALSE) + hit_loser = TRUE + +/datum/action/cooldown/mob_cooldown/tendril_cross_spikes + name = "Cross Spikes" + desc = "Create a wave of spikes around yourself, impaling anyone caught in it." + button_icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi' + button_icon_state = "spike" + background_icon_state = "bg_demon" + overlay_icon_state = "bg_demon_border" + click_to_activate = FALSE + cooldown_time = 10 SECONDS + melee_cooldown_time = 0 + shared_cooldown = NONE + /// Health threshold at which we reduce the amount of empty spots on the ground + var/health_threshold = 0.3 + +/datum/action/cooldown/mob_cooldown/tendril_cross_spikes/Activate(atom/target) + disable_cooldown_actions() + spawn_spikes() + SLEEP_CHECK_DEATH(0.4 SECONDS, owner) + spawn_spikes(inverse = TRUE) + StartCooldown() + enable_cooldown_actions() + return TRUE + +/datum/action/cooldown/mob_cooldown/tendril_cross_spikes/proc/spawn_spikes(inverse = FALSE) + var/list/turf/spike_turfs = list() + var/turf/owner_turf = get_turf(owner) + + var/reduced_spawns = FALSE + if (isliving(owner)) + var/mob/living/as_living = owner + if (as_living.health / as_living.maxHealth <= health_threshold) + reduced_spawns = TRUE + + for (var/turf/open/target_turf in oview(7, owner_turf)) + if (sqrt((target_turf.x - owner_turf.x) ** 2 + (target_turf.y - owner_turf.y) ** 2) > 9.5) // big circle is a lie + continue + + if (reduced_spawns) + if (abs(target_turf.x - owner_turf.x) % 2 == abs(target_turf.y - owner_turf.y + inverse) % 2) + new /obj/effect/temp_visual/telegraphing/circle/short(target_turf) + spike_turfs += target_turf + continue + + var/row_diff = inverse ? abs(target_turf.x - owner_turf.x) : abs(target_turf.y - owner_turf.y) + var/column_diff = inverse ? abs(target_turf.y - owner_turf.y) : abs(target_turf.x - owner_turf.x) + var/row = floor((row_diff + 1) / 3) + if (row % 2 == 0) + if (row_diff - row * 3 == 0) + if (column_diff % 4 == 2) + continue + else + if (column_diff % 4 != 0) + continue + else + if (row_diff - row * 3 == 0) + if (column_diff % 4 == 0) + continue + else + if (column_diff % 4 != 2) + continue + + new /obj/effect/temp_visual/telegraphing/circle/short(target_turf) + spike_turfs += target_turf + + SLEEP_CHECK_DEATH(1 SECONDS, owner) + + for (var/turf/open/to_spawn in spike_turfs) + new /obj/effect/temp_visual/emerging_ground_spike/tendril/single(to_spawn) + + SLEEP_CHECK_DEATH(0.8 SECONDS, owner) + +/datum/action/cooldown/mob_cooldown/projectile_attack/tendril_melee + name = "Tentacle Stab" + desc = "Stab nearby hostiles with long tentacles." + button_icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi' + button_icon_state = "goliath_tentacle_wiggle" + background_icon_state = "bg_demon" + overlay_icon_state = "bg_demon_border" + click_to_activate = FALSE + cooldown_time = 4 SECONDS + melee_cooldown_time = 0 + shared_cooldown = NONE + projectile_type = /obj/projectile/tentacle_lash/stab + +/datum/action/cooldown/mob_cooldown/projectile_attack/tendril_melee/Activate(atom/target_atom, warning = TRUE) + if (warning) + for (var/stab_dir in GLOB.alldirs) + var/turf/open/stab_turf = get_step(owner, stab_dir) + if (!istype(stab_turf)) + continue + var/obj/effect/temp_visual/telegraphing/line/short/telegraph = new(stab_turf) + telegraph.dir = stab_dir + + SLEEP_CHECK_DEATH(0.5 SECONDS, owner) + + for (var/stab_dir in GLOB.alldirs) + shoot_projectile(get_turf(owner), get_step(owner, stab_dir), firer = owner) + + SLEEP_CHECK_DEATH(0.5 SECONDS, owner) + StartCooldown() + return TRUE diff --git a/code/modules/mob/living/basic/lavaland/tendril/tendril_ai.dm b/code/modules/mob/living/basic/lavaland/tendril/tendril_ai.dm new file mode 100644 index 00000000000..77da7086dfa --- /dev/null +++ b/code/modules/mob/living/basic/lavaland/tendril/tendril_ai.dm @@ -0,0 +1,35 @@ +/datum/ai_controller/basic_controller/tendril + blackboard = list( + BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic, + BB_TARGET_MINIMUM_STAT = HARD_CRIT, + ) + + planning_subtrees = list( + /datum/ai_planning_subtree/call_reinforcements/mining, + /datum/ai_planning_subtree/target_retaliate/check_faction, + /datum/ai_planning_subtree/simple_find_target, + /datum/ai_planning_subtree/targeted_mob_ability/tendril_chaser, + /datum/ai_planning_subtree/use_mob_ability/tendril_spikes, + /datum/ai_planning_subtree/use_mob_ability/tendril_lash, + ) + +/datum/ai_planning_subtree/targeted_mob_ability/tendril_chaser + ability_key = BB_TENDRIL_CHASER + operational_datums = list(/datum/component/ai_target_timer) + finish_planning = FALSE + +/datum/ai_planning_subtree/use_mob_ability/tendril_lash + ability_key = BB_TENDRIL_LASH + +/datum/ai_planning_subtree/use_mob_ability/tendril_lash/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + if (!controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]) + return FALSE + return ..() + +/datum/ai_planning_subtree/use_mob_ability/tendril_spikes + ability_key = BB_TENDRIL_SPIKES + +/datum/ai_planning_subtree/use_mob_ability/tendril_spikes/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + if (!controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]) + return FALSE + return ..() diff --git a/code/modules/mob/living/basic/space_fauna/meteor_heart/chasing_spikes.dm b/code/modules/mob/living/basic/space_fauna/meteor_heart/chasing_spikes.dm index b50b089b0ff..60288e9c5e7 100644 --- a/code/modules/mob/living/basic/space_fauna/meteor_heart/chasing_spikes.dm +++ b/code/modules/mob/living/basic/space_fauna/meteor_heart/chasing_spikes.dm @@ -45,6 +45,8 @@ var/position_variance = 8 /// Damage to deal on impale var/impale_damage = 15 + /// Wound bonus on impale + var/impale_wound_bonus = 0 /// Typecache of types of mobs not to damage var/list/damage_blacklist_typecache = list( /mob/living/basic/meteor_heart, @@ -67,14 +69,22 @@ /obj/effect/temp_visual/emerging_ground_spike/proc/impale() if (!isturf(loc)) return + var/hit_someone = FALSE for(var/mob/living/victim in loc) - if (is_type_in_typecache(victim, damage_blacklist_typecache)) - continue - hit_someone = TRUE - var/target_zone = victim.resting ? BODY_ZONE_CHEST : pick_weight(standing_damage_zones) - victim.apply_damage(impale_damage, damagetype = BRUTE, def_zone = target_zone, sharpness = SHARP_POINTY) + if (harm_mob(victim)) + hit_someone = TRUE + if (hit_someone) playsound(src, 'sound/items/weapons/slice.ogg', vol = 50, vary = TRUE, pressure_affected = FALSE) else playsound(src, 'sound/misc/splort.ogg', vol = 25, vary = TRUE, pressure_affected = FALSE) + return hit_someone + +/obj/effect/temp_visual/emerging_ground_spike/proc/harm_mob(mob/living/victim) + if (is_type_in_typecache(victim, damage_blacklist_typecache)) + return FALSE + + var/target_zone = victim.resting ? BODY_ZONE_CHEST : pick_weight(standing_damage_zones) + victim.apply_damage(impale_damage, damagetype = BRUTE, def_zone = target_zone, wound_bonus = impale_wound_bonus, sharpness = SHARP_POINTY) + return TRUE diff --git a/code/modules/procedural_mapping/mapGenerators/lavaland.dm b/code/modules/procedural_mapping/mapGenerators/lavaland.dm index 5251f5e8435..b638e7908dd 100644 --- a/code/modules/procedural_mapping/mapGenerators/lavaland.dm +++ b/code/modules/procedural_mapping/mapGenerators/lavaland.dm @@ -18,9 +18,7 @@ /datum/map_generator_module/splatter_layer/lavaland_tendrils spawnableTurfs = list() - spawnableAtoms = list(/obj/structure/spawner/lavaland = 5, - /obj/structure/spawner/lavaland/legion = 5, - /obj/structure/spawner/lavaland/goliath = 5) + spawnableAtoms = list(/mob/living/basic/mining/tendril = 15) /datum/map_generator/lavaland/ground_only modules = list(/datum/map_generator_module/bottom_layer/lavaland_default) diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index 3c695a10b73..d7aa59766b3 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -1251,8 +1251,8 @@ free_hitscan_forceMove = TRUE forceMove(source_loc) starting = source_loc - pixel_x = source.pixel_x - pixel_y = source.pixel_y + pixel_x = source.pixel_x - source.base_pixel_x + pixel_y = source.pixel_y - source.base_pixel_y original = target // Trim off excess pixel_x/y by converting them into turf offset diff --git a/code/modules/research/anomaly/anomaly_core.dm b/code/modules/research/anomaly/anomaly_core.dm index d9d15931c0f..54339a7aacf 100644 --- a/code/modules/research/anomaly/anomaly_core.dm +++ b/code/modules/research/anomaly/anomaly_core.dm @@ -206,7 +206,7 @@ if(!length(possible_targets)) return var/turf/target = pick(possible_targets) - new /obj/effect/temp_visual/telegraphing/thunderbolt(target) + new /obj/effect/temp_visual/telegraphing/circle(target) addtimer(CALLBACK(src, PROC_REF(strike), target), 1 SECONDS) /obj/item/assembly/signaler/anomaly/weather/proc/strike(turf/target) diff --git a/icons/mob/simple/lavaland/lavaland_monsters.dmi b/icons/mob/simple/lavaland/lavaland_monsters.dmi index 3f5eb9cbd3b..f338fab310a 100644 Binary files a/icons/mob/simple/lavaland/lavaland_monsters.dmi and b/icons/mob/simple/lavaland/lavaland_monsters.dmi differ diff --git a/icons/mob/simple/lavaland/tendril.dmi b/icons/mob/simple/lavaland/tendril.dmi new file mode 100644 index 00000000000..0bbd0ccbe21 Binary files /dev/null and b/icons/mob/simple/lavaland/tendril.dmi differ diff --git a/icons/mob/telegraphing/telegraph.dmi b/icons/mob/telegraphing/telegraph.dmi index 6afddd04b62..1483234c763 100644 Binary files a/icons/mob/telegraphing/telegraph.dmi and b/icons/mob/telegraphing/telegraph.dmi differ diff --git a/icons/obj/weapons/guns/projectiles.dmi b/icons/obj/weapons/guns/projectiles.dmi index 24aa777660d..19bdf00bac9 100644 Binary files a/icons/obj/weapons/guns/projectiles.dmi and b/icons/obj/weapons/guns/projectiles.dmi differ diff --git a/tgstation.dme b/tgstation.dme index e2ec6e08463..9d443c4f779 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -3083,7 +3083,6 @@ #include "code\game\objects\structures\icemoon\cave_entrance.dm" #include "code\game\objects\structures\lavaland\geyser.dm" #include "code\game\objects\structures\lavaland\gulag_vent.dm" -#include "code\game\objects\structures\lavaland\necropolis_tendril.dm" #include "code\game\objects\structures\lavaland\ore_vent.dm" #include "code\game\objects\structures\plaques\_plaques.dm" #include "code\game\objects\structures\plaques\static_plaques.dm" @@ -5378,6 +5377,9 @@ #include "code\modules\mob\living\basic\lavaland\raptor\raptor_egg.dm" #include "code\modules\mob\living\basic\lavaland\raptor\raptor_food_trough.dm" #include "code\modules\mob\living\basic\lavaland\raptor\raptor_inheritance.dm" +#include "code\modules\mob\living\basic\lavaland\tendril\tendril.dm" +#include "code\modules\mob\living\basic\lavaland\tendril\tendril_actions.dm" +#include "code\modules\mob\living\basic\lavaland\tendril\tendril_ai.dm" #include "code\modules\mob\living\basic\lavaland\watcher\watcher.dm" #include "code\modules\mob\living\basic\lavaland\watcher\watcher_ai.dm" #include "code\modules\mob\living\basic\lavaland\watcher\watcher_gaze.dm" diff --git a/tools/UpdatePaths/Scripts/96186_tendrils.txt b/tools/UpdatePaths/Scripts/96186_tendrils.txt new file mode 100644 index 00000000000..eb2f45499c4 --- /dev/null +++ b/tools/UpdatePaths/Scripts/96186_tendrils.txt @@ -0,0 +1 @@ +/obj/structure/spawner/lavaland : /mob/living/basic/mining/tendril