From 032419f30f7fd615dec6a708f1bddf00447ad409 Mon Sep 17 00:00:00 2001 From: Ben10Omintrix <138636438+Ben10Omintrix@users.noreply.github.com> Date: Sun, 8 Oct 2023 05:04:35 +0300 Subject: [PATCH] ice demon basic mobs (#78539) ## About The Pull Request ice demons are now basic mobs. they still have their behavior where they can teleport around the player, run away from him and shoot him. they now also have a new ability they will only use when they are on their last legs, they will spawn weak and slow afterimage clones of theirselves to attack the player. damaging these clones will also damage the original ice demons. ice demons can also now be very easily countered as they are very afraid of fires. they will run away from you if they see u holding a lit torch/flare/welding tool and while running away they will freeze the floors around them to try to slip u to stop u from chasing them. ice demons now also get a new unique trophy! this trophy will summon 2 friendly spirits that will help you kill ur target, but these spirits will dissappear after a very short while. https://github.com/tgstation/tgstation/assets/138636438/6a48fb15-f447-441a-91c6-48ca120dc22c ## Why It's Good For The Game ## Changelog :cl: refactor: ice demons have been refactored into basic mbos. please report any bugs add: ice demons now have a unique trophy /:cl: --- code/__DEFINES/ai/monsters.dm | 16 +++ .../basic_subtrees/maintain_distance.dm | 36 +++++- .../basic_subtrees/ranged_skirmish.dm | 26 ++-- .../teleport_away_from_target.dm | 56 +++++++++ .../ai/hunting_behavior/hunting_behaviors.dm | 4 +- code/datums/components/joint_damage.dm | 35 ++++++ code/datums/mapgen/Cavegens/IcemoonCaves.dm | 4 +- .../structures/icemoon/cave_entrance.dm | 2 +- .../mining/equipment/kinetic_crusher.dm | 33 +++++ .../basic/icemoon/ice_demon/ice_demon.dm | 89 +++++++++++++ .../icemoon/ice_demon/ice_demon_abilities.dm | 117 +++++++++++++++++ .../basic/icemoon/ice_demon/ice_demon_ai.dm | 118 ++++++++++++++++++ .../basic/lavaland/watcher/watcher_ai.dm | 5 +- .../hostile/mining_mobs/ice_demon.dm | 85 ------------- .../unit_tests/simple_animal_freeze.dm | 1 - icons/obj/mining_zones/artefacts.dmi | Bin 47492 -> 48023 bytes tgstation.dme | 6 +- .../UpdatePaths/Scripts/78539_ice_demons.txt | 1 + 18 files changed, 523 insertions(+), 111 deletions(-) create mode 100644 code/datums/ai/basic_mobs/basic_subtrees/teleport_away_from_target.dm create mode 100644 code/datums/components/joint_damage.dm create mode 100644 code/modules/mob/living/basic/icemoon/ice_demon/ice_demon.dm create mode 100644 code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_abilities.dm create mode 100644 code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_ai.dm delete mode 100644 code/modules/mob/living/simple_animal/hostile/mining_mobs/ice_demon.dm create mode 100644 tools/UpdatePaths/Scripts/78539_ice_demons.txt diff --git a/code/__DEFINES/ai/monsters.dm b/code/__DEFINES/ai/monsters.dm index be9a4be34cd..89670d4f346 100644 --- a/code/__DEFINES/ai/monsters.dm +++ b/code/__DEFINES/ai/monsters.dm @@ -147,6 +147,22 @@ /// key holds the tray we will beam #define BB_BEAMABLE_HYDROPLANT_TARGET "beamable_hydroplant_target" +//ice demons +///the list of items we are afraid of +#define BB_LIST_SCARY_ITEMS "list_scary_items" +///our teleportation ability +#define BB_DEMON_TELEPORT_ABILITY "demon_teleport_ability" +///the destination of our teleport ability +#define BB_TELEPORT_DESTINATION "teleport_destination" +///the ability to clone ourself +#define BB_DEMON_CLONE_ABILITY "demon_clone_ability" +///our slippery ice ability +#define BB_DEMON_SLIP_ABILITY "demon_slip_ability" +///the turf we are escaping too +#define BB_ESCAPE_DESTINATION "escape_destination" +///how far away we will be from our target before teleporting +#define BB_MINIMUM_DISTANCE_RANGE "minimum_distance_range" + /// Corpse we have consumed #define BB_LEGION_CORPSE "legion_corpse" /// Things our target recently said diff --git a/code/datums/ai/basic_mobs/basic_subtrees/maintain_distance.dm b/code/datums/ai/basic_mobs/basic_subtrees/maintain_distance.dm index c09e7cdbf75..2a85e9e902b 100644 --- a/code/datums/ai/basic_mobs/basic_subtrees/maintain_distance.dm +++ b/code/datums/ai/basic_mobs/basic_subtrees/maintain_distance.dm @@ -8,6 +8,8 @@ var/maximum_distance = 6 /// How far do we look for our target? var/view_distance = 10 + /// the run away behavior we will use + var/run_away_behavior = /datum/ai_behavior/step_away /datum/ai_planning_subtree/maintain_distance/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) . = ..() @@ -16,12 +18,15 @@ return // Don't run away from cucumbers, they're not snakes var/range = get_dist(controller.pawn, target) if (range < minimum_distance) - controller.queue_behavior(/datum/ai_behavior/step_away, target_key) + controller.queue_behavior(run_away_behavior, target_key, minimum_distance) return if (range > maximum_distance) controller.queue_behavior(/datum/ai_behavior/pursue_to_range, target_key, maximum_distance) return +/datum/ai_planning_subtree/maintain_distance/cover_minimum_distance + run_away_behavior = /datum/ai_behavior/cover_minimum_distance + /// Take one step away /datum/ai_behavior/step_away behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION @@ -80,3 +85,32 @@ if (!QDELETED(current_target) && get_dist(controller.pawn, current_target) > range) return finish_action(controller, succeeded = TRUE) + +///instead of taking a single step, we cover the entire distance +/datum/ai_behavior/cover_minimum_distance + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + required_distance = 0 + action_cooldown = 0.2 SECONDS + +/datum/ai_behavior/cover_minimum_distance/setup(datum/ai_controller/controller, target_key, minimum_distance) + . = ..() + var/atom/target = controller.blackboard[target_key] + if(QDELETED(target)) + return FALSE + var/required_distance = minimum_distance - get_dist(controller.pawn, target) //the distance we need to move + var/distance = 0 + var/turf/chosen_turf + for(var/turf/open/potential_turf in oview(required_distance, controller.pawn)) + var/new_distance_from_target = get_dist(potential_turf, target) + if(potential_turf.is_blocked_turf()) + continue + if(new_distance_from_target > distance) + chosen_turf = potential_turf + distance = new_distance_from_target + if(isnull(chosen_turf)) + return FALSE + set_movement_target(controller, target = chosen_turf) + +/datum/ai_behavior/cover_minimum_distance/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + . = ..() + finish_action(controller, succeeded = TRUE) diff --git a/code/datums/ai/basic_mobs/basic_subtrees/ranged_skirmish.dm b/code/datums/ai/basic_mobs/basic_subtrees/ranged_skirmish.dm index 1ff752d925f..be395f3dfe4 100644 --- a/code/datums/ai/basic_mobs/basic_subtrees/ranged_skirmish.dm +++ b/code/datums/ai/basic_mobs/basic_subtrees/ranged_skirmish.dm @@ -4,28 +4,28 @@ /// Blackboard key holding target atom var/target_key = BB_BASIC_MOB_CURRENT_TARGET /// What AI behaviour do we actually run? - var/datum/ai_behavior/ranged_skirmish/attack_behavior = /datum/ai_behavior/ranged_skirmish - -/datum/ai_planning_subtree/ranged_skirmish/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) - . = ..() - if(!controller.blackboard_key_exists(target_key)) - return - controller.queue_behavior(attack_behavior, target_key, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION) - -/// How often will we try to perform our ranged attack? -/datum/ai_behavior/ranged_skirmish - action_cooldown = 1 SECONDS + var/attack_behavior = /datum/ai_behavior/ranged_skirmish /// If target is further away than this we don't fire var/max_range = 9 /// If target is closer than this we don't fire var/min_range = 2 -/datum/ai_behavior/ranged_skirmish/setup(datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key) +/datum/ai_planning_subtree/ranged_skirmish/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + . = ..() + if(!controller.blackboard_key_exists(target_key)) + return + controller.queue_behavior(attack_behavior, target_key, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION, max_range, min_range) + +/// How often will we try to perform our ranged attack? +/datum/ai_behavior/ranged_skirmish + action_cooldown = 1 SECONDS + +/datum/ai_behavior/ranged_skirmish/setup(datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key, max_range, min_range) . = ..() var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key] return !QDELETED(target) -/datum/ai_behavior/ranged_skirmish/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key) +/datum/ai_behavior/ranged_skirmish/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key, max_range, min_range) . = ..() var/atom/target = controller.blackboard[target_key] if (QDELETED(target)) diff --git a/code/datums/ai/basic_mobs/basic_subtrees/teleport_away_from_target.dm b/code/datums/ai/basic_mobs/basic_subtrees/teleport_away_from_target.dm new file mode 100644 index 00000000000..dadba992e9f --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_subtrees/teleport_away_from_target.dm @@ -0,0 +1,56 @@ +///behavior to activate ability to escape from target +/datum/ai_planning_subtree/teleport_away_from_target + ///minimum distance away from the target before we execute behavior + var/minimum_distance = 2 + ///the ability we will execute + var/ability_key + +/datum/ai_planning_subtree/teleport_away_from_target/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + if(!controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET)) + return + var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + var/distance_from_target = get_dist(target, controller.pawn) + if(distance_from_target >= minimum_distance) + controller.clear_blackboard_key(BB_ESCAPE_DESTINATION) + return + var/datum/action/cooldown/ability = controller.blackboard[ability_key] + if(!ability?.IsAvailable()) + return + var/turf/location_turf = controller.blackboard[BB_ESCAPE_DESTINATION] + + if(isnull(location_turf)) + controller.queue_behavior(/datum/ai_behavior/find_furthest_turf_from_target, BB_BASIC_MOB_CURRENT_TARGET, BB_ESCAPE_DESTINATION, minimum_distance) + return SUBTREE_RETURN_FINISH_PLANNING + + if(get_dist(location_turf, target) < minimum_distance || !can_see(controller.pawn, location_turf)) //target moved close too close or we moved too far since finding the target turf + controller.clear_blackboard_key(BB_ESCAPE_DESTINATION) + return + + controller.queue_behavior(/datum/ai_behavior/targeted_mob_ability/and_clear_target, ability_key, BB_ESCAPE_DESTINATION) + +///find furtherst turf target so we may teleport to it +/datum/ai_behavior/find_furthest_turf_from_target + +/datum/ai_behavior/find_furthest_turf_from_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key, set_key, range) + var/mob/living/living_target = controller.blackboard[target_key] + if(QDELETED(living_target)) + return + + var/distance = 0 + var/turf/chosen_turf + for(var/turf/open/potential_destination in oview(range, living_target)) + if(potential_destination.is_blocked_turf()) + continue + var/new_distance_to_target = get_dist(potential_destination, living_target) + if(new_distance_to_target > distance) + chosen_turf = potential_destination + distance = new_distance_to_target + if(distance == range) + break //we have already found the max distance + + if(isnull(chosen_turf)) + finish_action(controller, FALSE) + return + + controller.set_blackboard_key(set_key, chosen_turf) + finish_action(controller, TRUE) diff --git a/code/datums/ai/hunting_behavior/hunting_behaviors.dm b/code/datums/ai/hunting_behavior/hunting_behaviors.dm index ba7c7f2ffba..468cfed33fb 100644 --- a/code/datums/ai/hunting_behavior/hunting_behaviors.dm +++ b/code/datums/ai/hunting_behavior/hunting_behaviors.dm @@ -94,8 +94,6 @@ var/atom/hunted = controller.blackboard[hunting_target_key] if(QDELETED(hunted)) - //Target is gone for some reason. forget about this task! - controller[hunting_target_key] = null finish_action(controller, FALSE, hunting_target_key) else target_caught(hunter, hunted) @@ -136,7 +134,7 @@ /datum/ai_behavior/hunt_target/use_ability_on_target/perform(seconds_per_tick, datum/ai_controller/controller, hunting_target_key, hunting_cooldown_key) var/datum/action/cooldown/ability = controller.blackboard[ability_key] - if(QDELETED(ability) || !ability.IsAvailable()) + if(!ability?.IsAvailable()) finish_action(controller, FALSE, hunting_target_key) return ..() diff --git a/code/datums/components/joint_damage.dm b/code/datums/components/joint_damage.dm new file mode 100644 index 00000000000..5397bd307ca --- /dev/null +++ b/code/datums/components/joint_damage.dm @@ -0,0 +1,35 @@ +/* + * A component given to mobs to damage a linked mob + */ +/datum/component/joint_damage + ///the mob we will damage + var/datum/weakref/overlord_mob + ///our last health count + var/previous_health_count + +/datum/component/joint_damage/Initialize(mob/overlord_mob) + if(!isliving(parent)) + return COMPONENT_INCOMPATIBLE + var/mob/living/parent_mob = parent + previous_health_count = parent_mob.health + if(overlord_mob) + src.overlord_mob = WEAKREF(overlord_mob) + +/datum/component/joint_damage/RegisterWithParent() + RegisterSignal(parent, COMSIG_LIVING_HEALTH_UPDATE, PROC_REF(damage_overlord)) + RegisterSignal(parent, COMSIG_LIVING_DEATH, PROC_REF(damage_overlord)) + +/datum/component/joint_damage/UnregisterFromParent() + UnregisterSignal(parent, list(COMSIG_LIVING_HEALTH_UPDATE, COMSIG_LIVING_DEATH)) + +/datum/component/joint_damage/Destroy() + overlord_mob = null + return ..() + +/datum/component/joint_damage/proc/damage_overlord(mob/living/source) + SIGNAL_HANDLER + + var/mob/living/overlord_to_damage = overlord_mob?.resolve() + if(!isnull(overlord_to_damage)) + overlord_to_damage.adjustBruteLoss(previous_health_count - source.health) ///damage or heal overlord + previous_health_count = source.health diff --git a/code/datums/mapgen/Cavegens/IcemoonCaves.dm b/code/datums/mapgen/Cavegens/IcemoonCaves.dm index 7d7437ccda6..b0fcd471db2 100644 --- a/code/datums/mapgen/Cavegens/IcemoonCaves.dm +++ b/code/datums/mapgen/Cavegens/IcemoonCaves.dm @@ -62,12 +62,12 @@ weighted_closed_turf_types = list(/turf/closed/mineral/random/snow/underground = 1) weighted_mob_spawn_list = list( SPAWN_MEGAFAUNA = 1, + /mob/living/basic/mining/ice_demon = 100, /mob/living/basic/mining/ice_whelp = 60, /mob/living/basic/mining/legion/snow = 100, - /mob/living/simple_animal/hostile/asteroid/ice_demon = 100, /obj/structure/spawner/ice_moon/demonic_portal = 6, - /obj/structure/spawner/ice_moon/demonic_portal/snowlegion = 6, /obj/structure/spawner/ice_moon/demonic_portal/ice_whelp = 6, + /obj/structure/spawner/ice_moon/demonic_portal/snowlegion = 6, ) weighted_megafauna_spawn_list = list(/mob/living/simple_animal/hostile/megafauna/colossus = 1) weighted_flora_spawn_list = list( diff --git a/code/game/objects/structures/icemoon/cave_entrance.dm b/code/game/objects/structures/icemoon/cave_entrance.dm index 2a90270ef73..add1f278569 100644 --- a/code/game/objects/structures/icemoon/cave_entrance.dm +++ b/code/game/objects/structures/icemoon/cave_entrance.dm @@ -80,7 +80,7 @@ GLOBAL_LIST_INIT(ore_probability, list( name = "demonic portal" desc = "A portal that goes to another world, normal creatures couldn't survive there." icon_state = "nether" - mob_types = list(/mob/living/simple_animal/hostile/asteroid/ice_demon) + mob_types = list(/mob/living/basic/mining/ice_demon) light_range = 1 light_color = COLOR_SOFT_RED mob_gps_id = "WT|B" // watcher | bluespace diff --git a/code/modules/mining/equipment/kinetic_crusher.dm b/code/modules/mining/equipment/kinetic_crusher.dm index caee1bbac8d..4ceddb09854 100644 --- a/code/modules/mining/equipment/kinetic_crusher.dm +++ b/code/modules/mining/equipment/kinetic_crusher.dm @@ -437,3 +437,36 @@ chaser.monster_damage_boost = FALSE // Weaker cuz no cooldown chaser.damage = 20 log_combat(user, target, "fired a chaser at", src) + +/obj/item/crusher_trophy/ice_demon_cube + name = "demonic cube" + desc = "A stone cold cube dropped from an ice demon." + icon_state = "ice_demon_cube" + denied_type = /obj/item/crusher_trophy/ice_demon_cube + ///how many will we summon? + var/summon_amount = 2 + ///cooldown to summon demons upon the target + COOLDOWN_DECLARE(summon_cooldown) + +/obj/item/crusher_trophy/ice_demon_cube/effect_desc() + return "mark detonation to unleash demonic ice clones upon the target" + +/obj/item/crusher_trophy/ice_demon_cube/on_mark_detonation(mob/living/target, mob/living/user) + if(isnull(target) || !COOLDOWN_FINISHED(src, summon_cooldown)) + return + for(var/i in 1 to summon_amount) + var/turf/drop_off = find_dropoff_turf(target, user) + var/mob/living/basic/mining/demon_afterimage/crusher/friend = new(drop_off) + friend.faction = list(FACTION_NEUTRAL) + friend.befriend(user) + friend.ai_controller?.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, target) + COOLDOWN_START(src, summon_cooldown, 30 SECONDS) + +///try to make them spawn all around the target to surround him +/obj/item/crusher_trophy/ice_demon_cube/proc/find_dropoff_turf(mob/living/target, mob/living/user) + var/list/turfs_list = get_adjacent_open_turfs(target) + for(var/turf/possible_turf in turfs_list) + if(possible_turf.is_blocked_turf()) + continue + return possible_turf + return get_turf(user) diff --git a/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon.dm b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon.dm new file mode 100644 index 00000000000..960f875365b --- /dev/null +++ b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon.dm @@ -0,0 +1,89 @@ +/mob/living/basic/mining/ice_demon + name = "demonic watcher" + desc = "A creature formed entirely out of ice, bluespace energy emanates from inside of it." + icon = 'icons/mob/simple/icemoon/icemoon_monsters.dmi' + icon_state = "ice_demon" + icon_living = "ice_demon" + icon_gib = "syndicate_gib" + mob_biotypes = MOB_ORGANIC|MOB_BEAST + mouse_opacity = MOUSE_OPACITY_ICON + basic_mob_flags = DEL_ON_DEATH + speed = 2 + maxHealth = 150 + health = 150 + obj_damage = 40 + melee_damage_lower = 15 + melee_damage_upper = 15 + attack_verb_continuous = "slices" + attack_verb_simple = "slice" + attack_sound = 'sound/weapons/bladeslice.ogg' + attack_vis_effect = ATTACK_EFFECT_SLASH + move_force = MOVE_FORCE_VERY_STRONG + move_resist = MOVE_FORCE_VERY_STRONG + pull_force = MOVE_FORCE_VERY_STRONG + crusher_loot = /obj/item/crusher_trophy/ice_demon_cube + ai_controller = /datum/ai_controller/basic_controller/ice_demon + death_message = "fades as the energies that tied it to this world dissipate." + death_sound = 'sound/magic/demon_dies.ogg' + +/mob/living/basic/mining/ice_demon/Initialize(mapload) + . = ..() + var/datum/action/cooldown/mob_cooldown/slippery_ice_floors/ice_floor = new(src) + ice_floor.Grant(src) + ai_controller.set_blackboard_key(BB_DEMON_SLIP_ABILITY, ice_floor) + var/datum/action/cooldown/mob_cooldown/ice_demon_teleport/demon_teleport = new(src) + demon_teleport.Grant(src) + ai_controller.set_blackboard_key(BB_DEMON_TELEPORT_ABILITY, demon_teleport) + var/datum/action/cooldown/spell/conjure/create_afterimages/afterimage = new(src) + afterimage.Grant(src) + ai_controller.set_blackboard_key(BB_DEMON_CLONE_ABILITY, afterimage) + AddComponent(\ + /datum/component/ranged_attacks,\ + projectile_type = /obj/projectile/temp/ice_demon,\ + projectile_sound = 'sound/weapons/pierce.ogg',\ + ) + var/static/list/death_loot = list(/obj/item/stack/ore/bluespace_crystal = 3) + AddElement(/datum/element/death_drops, death_loot) + AddElement(/datum/element/simple_flying) + +/mob/living/basic/mining/ice_demon/death(gibbed) + if(prob(5)) + new /obj/item/raw_anomaly_core/bluespace(loc) + return ..() + +/mob/living/basic/mining/demon_afterimage + name = "afterimage demonic watcher" + desc = "Is this some sort of illusion?" + icon = 'icons/mob/simple/icemoon/icemoon_monsters.dmi' + icon_state = "ice_demon" + icon_living = "ice_demon" + icon_gib = "syndicate_gib" + mob_biotypes = MOB_ORGANIC|MOB_BEAST + mouse_opacity = MOUSE_OPACITY_ICON + basic_mob_flags = DEL_ON_DEATH + speed = 5 + maxHealth = 20 + health = 20 + melee_damage_lower = 5 + melee_damage_upper = 5 + attack_verb_continuous = "slices" + attack_verb_simple = "slice" + attack_sound = 'sound/weapons/bladeslice.ogg' + alpha = 80 + ai_controller = /datum/ai_controller/basic_controller/ice_demon/afterimage + ///how long do we exist for + var/existence_period = 15 SECONDS + +/mob/living/basic/mining/demon_afterimage/Initialize(mapload) + . = ..() + AddElement(/datum/element/simple_flying) + AddElement(/datum/element/temporary_atom, life_time = existence_period) + +///afterimage subtypes summoned by the crusher +/mob/living/basic/mining/demon_afterimage/crusher + speed = 2 + health = 60 + maxHealth = 60 + melee_damage_lower = 10 + melee_damage_upper = 10 + existence_period = 7 SECONDS diff --git a/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_abilities.dm b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_abilities.dm new file mode 100644 index 00000000000..79c9ee9bd58 --- /dev/null +++ b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_abilities.dm @@ -0,0 +1,117 @@ +/obj/projectile/temp/ice_demon + name = "ice blast" + icon_state = "ice_2" + damage = 5 + damage_type = BURN + armor_flag = ENERGY + speed = 1 + pixel_speed_multiplier = 0.25 + temperature = -75 + +/datum/action/cooldown/mob_cooldown/ice_demon_teleport + name = "Bluespace Teleport" + desc = "Teleport towards a destination target!" + button_icon = 'icons/obj/ore.dmi' + button_icon_state = "bluespace_crystal" + cooldown_time = 3 SECONDS + melee_cooldown_time = 0 SECONDS + ///time delay before teleport + var/time_delay = 0.5 SECONDS + +/datum/action/cooldown/mob_cooldown/ice_demon_teleport/Activate(atom/target_atom) + if(isclosedturf(get_turf(target_atom))) + owner.balloon_alert(owner, "blocked!") + return FALSE + animate(owner, transform = matrix().Scale(0.8), time = time_delay, easing = SINE_EASING) + addtimer(CALLBACK(src, PROC_REF(teleport_to_turf), target_atom), time_delay) + StartCooldown() + return TRUE + +/datum/action/cooldown/mob_cooldown/ice_demon_teleport/proc/teleport_to_turf(atom/target) + animate(owner, transform = matrix(), time = 0.5 SECONDS, easing = SINE_EASING) + do_teleport(teleatom = owner, destination = target, channel = TELEPORT_CHANNEL_BLUESPACE, forced = TRUE) + +/datum/action/cooldown/mob_cooldown/slippery_ice_floors + name = "Iced Floors" + desc = "Summon slippery ice floors all around!" + button_icon = 'icons/turf/floors/ice_turf.dmi' + button_icon_state = "ice_turf-6" + cooldown_time = 2 SECONDS + click_to_activate = FALSE + melee_cooldown_time = 0 SECONDS + ///perimeter we will spawn the iced floors on + var/radius = 1 + ///intervals we will spawn the ice floors in + var/spread_duration = 0.2 SECONDS + +/datum/action/cooldown/mob_cooldown/slippery_ice_floors/Activate(atom/target_atom) + for(var/i in 0 to radius) + var/list/list_of_turfs = border_diamond_range_turfs(owner, i) + addtimer(CALLBACK(src, PROC_REF(spawn_icy_floors), list_of_turfs), i * spread_duration) + StartCooldown() + return TRUE + +/datum/action/cooldown/mob_cooldown/slippery_ice_floors/proc/spawn_icy_floors(list/list_of_turfs) + if(!length(list_of_turfs)) + return + for(var/turf/location in list_of_turfs) + if(isnull(location)) + continue + if(isclosedturf(location) || isspaceturf(location)) + continue + new /obj/effect/temp_visual/slippery_ice(location) + +/obj/effect/temp_visual/slippery_ice + name = "slippery acid" + icon = 'icons/turf/floors/ice_turf.dmi' + icon_state = "ice_turf-6" + layer = BELOW_MOB_LAYER + plane = GAME_PLANE + anchored = TRUE + duration = 3 SECONDS + alpha = 100 + /// how long does it take for the effect to phase in + var/phase_in_period = 2 SECONDS + +/obj/effect/temp_visual/slippery_ice/Initialize(mapload) + . = ..() + animate(src, alpha = 160, time = phase_in_period) + animate(alpha = 0, time = duration - phase_in_period) /// slowly fade out of existence + addtimer(CALLBACK(src, PROC_REF(add_slippery_component), phase_in_period)) //only become slippery after we phased in + +/obj/effect/temp_visual/slippery_ice/proc/add_slippery_component() + AddComponent(/datum/component/slippery, 2 SECONDS) + +/datum/action/cooldown/spell/conjure/create_afterimages + name = "Create After Images" + button_icon = 'icons/mob/simple/icemoon/icemoon_monsters.dmi' + button_icon_state = "ice_demon" + spell_requirements = NONE + cooldown_time = 1 MINUTES + summon_type = list(/mob/living/basic/mining/demon_afterimage) + summon_radius = 1 + summon_amount = 2 + ///max number of after images + var/max_afterimages = 2 + ///How many clones do we have summoned + var/number_of_afterimages = 0 + +/datum/action/cooldown/spell/conjure/create_afterimages/can_cast_spell(feedback = TRUE) + . = ..() + if(!.) + return FALSE + if(number_of_afterimages >= max_afterimages) + return FALSE + return TRUE + +/datum/action/cooldown/spell/conjure/create_afterimages/post_summon(atom/summoned_object, atom/cast_on) + var/mob/living/basic/created_copy = summoned_object + created_copy.AddComponent(/datum/component/joint_damage, overlord_mob = owner) + RegisterSignals(created_copy, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH), PROC_REF(delete_copy)) + number_of_afterimages++ + +/datum/action/cooldown/spell/conjure/create_afterimages/proc/delete_copy(mob/source) + SIGNAL_HANDLER + + UnregisterSignal(source, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH)) + number_of_afterimages-- diff --git a/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_ai.dm b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_ai.dm new file mode 100644 index 00000000000..b17d5c1a30d --- /dev/null +++ b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_ai.dm @@ -0,0 +1,118 @@ +/datum/ai_controller/basic_controller/ice_demon + blackboard = list( + BB_TARGETTING_DATUM = new /datum/targetting_datum/basic, + BB_LIST_SCARY_ITEMS = list( + /obj/item/weldingtool, + /obj/item/flashlight/flare, + ), + BB_BASIC_MOB_FLEEING = TRUE, + BB_MINIMUM_DISTANCE_RANGE = 3, + ) + + ai_movement = /datum/ai_movement/basic_avoidance + idle_behavior = /datum/idle_behavior/idle_random_walk + planning_subtrees = list( + /datum/ai_planning_subtree/simple_find_target, + /datum/ai_planning_subtree/flee_target/ice_demon, + /datum/ai_planning_subtree/ranged_skirmish/ice_demon, + /datum/ai_planning_subtree/maintain_distance/cover_minimum_distance/ice_demon, + /datum/ai_planning_subtree/teleport_away_from_target, + /datum/ai_planning_subtree/find_and_hunt_target/teleport_destination, + /datum/ai_planning_subtree/targeted_mob_ability/summon_afterimages, + ) + + +/datum/ai_planning_subtree/maintain_distance/cover_minimum_distance/ice_demon + maximum_distance = 7 + +/datum/ai_planning_subtree/teleport_away_from_target + ability_key = BB_DEMON_TELEPORT_ABILITY + +/datum/ai_planning_subtree/find_and_hunt_target/teleport_destination + target_key = BB_TELEPORT_DESTINATION + hunting_behavior = /datum/ai_behavior/hunt_target/use_ability_on_target/demon_teleport + finding_behavior = /datum/ai_behavior/find_valid_teleport_location + hunt_targets = list(/turf/open) + hunt_range = 3 + finish_planning = FALSE + +/datum/ai_planning_subtree/find_and_hunt_target/teleport_destination/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + if(!controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET)) + return + if(controller.blackboard_key_exists(BB_ESCAPE_DESTINATION)) + controller.clear_blackboard_key(BB_TELEPORT_DESTINATION) + return + var/datum/action/cooldown/ability = controller.blackboard[BB_DEMON_TELEPORT_ABILITY] + if(!ability?.IsAvailable()) + return + return ..() + +/datum/ai_behavior/find_valid_teleport_location + +/datum/ai_behavior/find_valid_teleport_location/perform(seconds_per_tick, datum/ai_controller/controller, hunting_target_key, types_to_hunt, hunt_range) + . = ..() + var/mob/living/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + var/list/possible_turfs = list() + + if(QDELETED(target)) + finish_action(controller, FALSE) + return + + for(var/turf/open/potential_turf in oview(hunt_range, target)) //we check for turfs around the target + if(potential_turf.is_blocked_turf()) + continue + if(!can_see(target, potential_turf, hunt_range)) + continue + possible_turfs += potential_turf + + if(!length(possible_turfs)) + finish_action(controller, FALSE) + return + + controller.set_blackboard_key(hunting_target_key, pick(possible_turfs)) + finish_action(controller, TRUE) + +/datum/ai_behavior/hunt_target/use_ability_on_target/demon_teleport + hunt_cooldown = 2 SECONDS + ability_key = BB_DEMON_TELEPORT_ABILITY + behavior_flags = NONE + +/datum/ai_planning_subtree/targeted_mob_ability/summon_afterimages + ability_key = BB_DEMON_CLONE_ABILITY + +/datum/ai_planning_subtree/targeted_mob_ability/summon_afterimages/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + var/mob/living/living_pawn = controller.pawn + if(living_pawn.health / living_pawn.maxHealth > 0.5) //only use this ability when under half health + return + return ..() + +/datum/ai_planning_subtree/flee_target/ice_demon + +/datum/ai_planning_subtree/flee_target/ice_demon/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(QDELETED(target)) + return + if(!iscarbon(target)) + return + var/mob/living/carbon/human_target = target + + for(var/obj/held_item in human_target.held_items) + if(!is_type_in_list(held_item, controller.blackboard[BB_LIST_SCARY_ITEMS])) + continue + if(!held_item.light_on) + continue + var/datum/action/cooldown/slip_ability = controller.blackboard[BB_DEMON_SLIP_ABILITY] + if(slip_ability?.IsAvailable()) + controller.queue_behavior(/datum/ai_behavior/use_mob_ability, BB_DEMON_SLIP_ABILITY) + return ..() + +/datum/ai_planning_subtree/ranged_skirmish/ice_demon + min_range = 0 + +/datum/ai_controller/basic_controller/ice_demon/afterimage + planning_subtrees = list( + /datum/ai_planning_subtree/simple_find_target, + /datum/ai_planning_subtree/flee_target/ice_demon, //even the afterimages are afraid of flames! + /datum/ai_planning_subtree/basic_melee_attack_subtree, + ) + diff --git a/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm b/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm index a25234817f3..348bbcfcaa7 100644 --- a/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm +++ b/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm @@ -27,13 +27,10 @@ return ..() /datum/ai_planning_subtree/ranged_skirmish/watcher - attack_behavior = /datum/ai_behavior/ranged_skirmish/watcher + min_range = 0 /datum/ai_planning_subtree/ranged_skirmish/watcher/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) var/mob/living/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] if (QDELETED(target) || HAS_TRAIT(target, TRAIT_OVERWATCHED)) return // Don't bully people who are playing red light green light return ..() - -/datum/ai_behavior/ranged_skirmish/watcher - min_range = 0 diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/ice_demon.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/ice_demon.dm deleted file mode 100644 index 9d84fe2e1fe..00000000000 --- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/ice_demon.dm +++ /dev/null @@ -1,85 +0,0 @@ -/mob/living/simple_animal/hostile/asteroid/ice_demon - name = "demonic watcher" - desc = "A creature formed entirely out of ice, bluespace energy emanates from inside of it." - icon = 'icons/mob/simple/icemoon/icemoon_monsters.dmi' - icon_state = "ice_demon" - icon_living = "ice_demon" - icon_dead = "ice_demon_dead" - icon_gib = "syndicate_gib" - mob_biotypes = MOB_ORGANIC|MOB_BEAST - mouse_opacity = MOUSE_OPACITY_ICON - speak_emote = list("telepathically cries") - speed = 2 - move_to_delay = 2 - projectiletype = /obj/projectile/temp/ice_demon - projectilesound = 'sound/weapons/pierce.ogg' - ranged = TRUE - ranged_message = "manifests ice" - ranged_cooldown_time = 1.5 SECONDS - minimum_distance = 3 - retreat_distance = 3 - maxHealth = 150 - health = 150 - obj_damage = 40 - melee_damage_lower = 15 - melee_damage_upper = 15 - attack_verb_continuous = "slices" - attack_verb_simple = "slice" - attack_sound = 'sound/weapons/bladeslice.ogg' - attack_vis_effect = ATTACK_EFFECT_SLASH - vision_range = 9 - aggro_vision_range = 9 - move_force = MOVE_FORCE_VERY_STRONG - move_resist = MOVE_FORCE_VERY_STRONG - pull_force = MOVE_FORCE_VERY_STRONG - del_on_death = TRUE - loot = list() - crusher_loot = /obj/item/crusher_trophy/watcher_wing/ice_wing - death_message = "fades as the energies that tied it to this world dissipate." - death_sound = 'sound/magic/demon_dies.ogg' - stat_attack = HARD_CRIT - robust_searching = TRUE - footstep_type = FOOTSTEP_MOB_CLAW - /// Distance the demon will teleport from the target - var/teleport_distance = 3 - -/mob/living/simple_animal/hostile/asteroid/ice_demon/Initialize(mapload) - . = ..() - AddElement(/datum/element/simple_flying) - -/obj/projectile/temp/ice_demon - name = "ice blast" - icon_state = "ice_2" - damage = 5 - damage_type = BURN - armor_flag = ENERGY - speed = 1 - pixel_speed_multiplier = 0.25 - range = 200 - temperature = -75 - -/mob/living/simple_animal/hostile/asteroid/ice_demon/OpenFire() - ranged_cooldown = world.time + ranged_cooldown_time - // Sentient ice demons teleporting has been linked to server crashes - if(client) - return ..() - if(teleport_distance <= 0) - return ..() - var/list/possible_ends = view(teleport_distance, target.loc) - view(teleport_distance - 1, target.loc) - for(var/turf/closed/turf_to_remove in possible_ends) - possible_ends -= turf_to_remove - if(!possible_ends.len) - return ..() - var/turf/end = pick(possible_ends) - do_teleport(src, end, 0, channel=TELEPORT_CHANNEL_BLUESPACE, forced = TRUE) - SLEEP_CHECK_DEATH(8, src) - return ..() - -/mob/living/simple_animal/hostile/asteroid/ice_demon/death(gibbed) - move_force = MOVE_FORCE_DEFAULT - move_resist = MOVE_RESIST_DEFAULT - pull_force = PULL_FORCE_DEFAULT - new /obj/item/stack/ore/bluespace_crystal(loc, 3) - if(prob(5)) - new /obj/item/raw_anomaly_core/bluespace(loc) - return ..() diff --git a/code/modules/unit_tests/simple_animal_freeze.dm b/code/modules/unit_tests/simple_animal_freeze.dm index a0a4c91be2d..939df847cef 100644 --- a/code/modules/unit_tests/simple_animal_freeze.dm +++ b/code/modules/unit_tests/simple_animal_freeze.dm @@ -64,7 +64,6 @@ /mob/living/simple_animal/hostile/asteroid/gutlunch/grublunch, /mob/living/simple_animal/hostile/asteroid/gutlunch/gubbuck, /mob/living/simple_animal/hostile/asteroid/gutlunch/guthen, - /mob/living/simple_animal/hostile/asteroid/ice_demon, /mob/living/simple_animal/hostile/asteroid/polarbear, /mob/living/simple_animal/hostile/asteroid/polarbear/lesser, /mob/living/simple_animal/hostile/asteroid/wolf, diff --git a/icons/obj/mining_zones/artefacts.dmi b/icons/obj/mining_zones/artefacts.dmi index f3f7d00e4eef8f3c82cbbd739c191aaba17ae3b7..d4c603834d21bec61a21b57203eb6086bab45dfa 100644 GIT binary patch delta 4613 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 Vnz2wZ(*O~gzOIQ*&3z~M{{YTx>ly$6 delta 4078 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