[MIRROR] Basic Mob Brimdemon [MDB IGNORE] (#23863)

* Basic Mob Brimdemon (#78424)

## About The Pull Request

Fixes #71330

The brimdemon was basically already perfect (well, it has a novel means
of attacking) so I didn't get too fancy with this one, it's _largely_
just a straightforward conversion.
Following this change it's a little slower to back off, but better at
lining up with people in order to blast them. Additionally, its beam is
now a mob ability so you can give it to other mobs if you so desire.

Because I can't help doing a _little_ tinkering, Brimdemons now explode
2.5 seconds after they die, after a brief warning animation.

## Why It's Good For The Game

Simple mobs must die

## Changelog

🆑
add: Brimdemon corpses release an explosion shortly after death, just to
keep you on your toes.
refactor: Brimdemons now use the basic mob framework which (should)
improve their pathfinding somewhat. Please bug report any unusual
behaviour.
admin: The brimdemon's beam ability can be given to any mob, for your
Binding of Isaac event
/🆑

* Basic Mob Brimdemon

* Modular

---------

Co-authored-by: Jacquerel <hnevard@gmail.com>
Co-authored-by: Giz <13398309+vinylspiders@users.noreply.github.com>
This commit is contained in:
SkyratBot
2023-09-23 03:52:28 -04:00
committed by GitHub
co-authored by Jacquerel Giz
parent 5c1490bf7e
commit 7509292edd
46 changed files with 549 additions and 394 deletions
@@ -43,12 +43,12 @@
unset_click_ability(source, refund_cooldown = TRUE)
/datum/action/cooldown/spell/pointed/projectile/furious_steel/InterceptClickOn(mob/living/caller, params, atom/click_target)
/datum/action/cooldown/spell/pointed/projectile/furious_steel/InterceptClickOn(mob/living/caller, params, atom/target)
// Let the caster prioritize using items like guns over blade casts
if(caller.get_active_held_item())
return FALSE
// Let the caster prioritize melee attacks like punches and shoves over blade casts
if(get_dist(caller, click_target) <= 1)
if(get_dist(caller, target) <= 1)
return FALSE
return ..()
@@ -69,14 +69,10 @@
/datum/ai_planning_subtree/sculpt_statues
/datum/ai_planning_subtree/sculpt_statues/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/obj/target = controller.blackboard[BB_TARGET_ROCK]
if(QDELETED(target))
controller.queue_behavior(/datum/ai_behavior/find_and_set, BB_TARGET_ROCK, /obj/structure/flora/rock/icy)
return
controller.queue_behavior(/datum/ai_behavior/sculpt_statue, BB_TARGET_ROCK)
return SUBTREE_RETURN_FINISH_PLANNING
if(controller.blackboard_key_exists(BB_TARGET_ROCK))
controller.queue_behavior(/datum/ai_behavior/sculpt_statue, BB_TARGET_ROCK)
return SUBTREE_RETURN_FINISH_PLANNING
controller.queue_behavior(/datum/ai_behavior/find_and_set, BB_TARGET_ROCK, /obj/structure/flora/rock/icy)
/datum/ai_behavior/sculpt_statue
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
@@ -135,16 +131,13 @@
/datum/ai_planning_subtree/burn_trees/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/datum/action/cooldown/using_action = controller.blackboard[BB_WHELP_STRAIGHTLINE_FIRE]
if (!using_action.IsAvailable())
if (!using_action?.IsAvailable())
return
var/obj/structure/target = controller.blackboard[BB_TARGET_TREE]
if(QDELETED(target))
controller.queue_behavior(/datum/ai_behavior/set_target_tree, BB_TARGET_TREE)
return
controller.queue_behavior(/datum/ai_behavior/targeted_mob_ability/and_clear_target/burn_trees, BB_WHELP_STRAIGHTLINE_FIRE, BB_TARGET_TREE)
return SUBTREE_RETURN_FINISH_PLANNING
if(controller.blackboard_key_exists(BB_TARGET_TREE))
controller.queue_behavior(/datum/ai_behavior/targeted_mob_ability/and_clear_target/burn_trees, BB_WHELP_STRAIGHTLINE_FIRE, BB_TARGET_TREE)
return SUBTREE_RETURN_FINISH_PLANNING
controller.queue_behavior(/datum/ai_behavior/set_target_tree, BB_TARGET_TREE)
/datum/ai_behavior/set_target_tree
@@ -15,9 +15,7 @@
/datum/ai_planning_subtree/bileworm_attack
/datum/ai_planning_subtree/bileworm_attack/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/mob/living/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
if(QDELETED(target))
if (!controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
return
var/datum/action/cooldown/mob_cooldown/resurface = controller.blackboard[BB_BILEWORM_RESURFACE]
@@ -0,0 +1,126 @@
/// Fires a bloody beam. Brimdemon Blast!
/datum/action/cooldown/mob_cooldown/brimbeam
name = "brimstone blast"
desc = "Unleash a barrage of infernal energies in the targeted direction."
button_icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
button_icon_state = "brimdemon_firing"
background_icon_state = "bg_demon"
overlay_icon_state = "bg_demon_border"
click_to_activate = TRUE
cooldown_time = 5 SECONDS
melee_cooldown_time = 0
check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED
/// How far does our beam go?
var/beam_range = 10
/// How long does our beam last?
var/beam_duration = 2 SECONDS
/// How long do we wind up before firing?
var/charge_duration = 1 SECONDS
/// Overlay we show when we're about to fire
var/static/image/direction_overlay = image('icons/mob/simple/lavaland/lavaland_monsters.dmi', "brimdemon_telegraph_dir")
/// A list of all the beam parts.
var/list/beam_parts = list()
/datum/action/cooldown/mob_cooldown/brimbeam/Destroy()
extinguish_laser()
return ..()
/datum/action/cooldown/mob_cooldown/brimbeam/Activate(atom/target)
StartCooldown(360 SECONDS)
owner.face_atom(target)
owner.move_resist = MOVE_FORCE_VERY_STRONG
owner.add_overlay(direction_overlay)
owner.balloon_alert_to_viewers("charging...")
var/fully_charged = do_after(owner, delay = charge_duration, target = owner)
owner.cut_overlay(direction_overlay)
if (!fully_charged)
StartCooldown()
return TRUE
if (!fire_laser())
var/static/list/fail_emotes = list("coughs.", "wheezes.", "belches out a puff of black smoke.")
owner.manual_emote(pick(fail_emotes))
StartCooldown()
return TRUE
do_after(owner, delay = beam_duration, target = owner)
extinguish_laser()
StartCooldown()
return TRUE
/// Create a laser in the direction we are facing
/datum/action/cooldown/mob_cooldown/brimbeam/proc/fire_laser()
owner.visible_message(span_danger("[owner] fires a brimbeam!"))
playsound(owner, 'sound/creatures/brimdemon.ogg', 150, FALSE, 0, 3)
var/turf/target_turf = get_ranged_target_turf(owner, owner.dir, beam_range)
var/turf/origin_turf = get_turf(owner)
var/list/affected_turfs = get_line(origin_turf, target_turf) - origin_turf
for(var/turf/affected_turf in affected_turfs)
if(affected_turf.opacity)
break
var/blocked = FALSE
for(var/obj/potential_block in affected_turf.contents)
if(potential_block.opacity)
blocked = TRUE
break
if(blocked)
break
var/atom/new_brimbeam = new /obj/effect/brimbeam(affected_turf)
new_brimbeam.dir = owner.dir
beam_parts += new_brimbeam
for(var/mob/living/hit_mob in affected_turf.contents)
hit_mob.apply_damage(damage = 25, damagetype = BURN)
to_chat(hit_mob, span_userdanger("You're blasted by [owner]'s brimbeam!"))
RegisterSignal(new_brimbeam, COMSIG_QDELETING, PROC_REF(extinguish_laser)) // In case idk a singularity eats it or something
if(!length(beam_parts))
return FALSE
var/atom/last_brimbeam = beam_parts[length(beam_parts)]
last_brimbeam.icon_state = "brimbeam_end"
var/atom/first_brimbeam = beam_parts[1]
first_brimbeam.icon_state = "brimbeam_start"
return TRUE
/// Get rid of our laser when we are done with it
/datum/action/cooldown/mob_cooldown/brimbeam/proc/extinguish_laser()
if(!length(beam_parts))
return FALSE
owner.move_resist = initial(owner.move_resist)
for(var/obj/effect/brimbeam/beam in beam_parts)
beam.disperse()
beam_parts = list()
/// Segments of the actual beam, these hurt if you stand in them
/obj/effect/brimbeam
name = "brimbeam"
icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
icon_state = "brimbeam_mid"
layer = ABOVE_MOB_LAYER
plane = ABOVE_GAME_PLANE
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
light_color = LIGHT_COLOR_BLOOD_MAGIC
light_power = 3
light_range = 2
/obj/effect/brimbeam/Initialize(mapload)
. = ..()
START_PROCESSING(SSfastprocess, src)
/obj/effect/brimbeam/Destroy()
STOP_PROCESSING(SSfastprocess, src)
return ..()
/obj/effect/brimbeam/process()
for(var/mob/living/hit_mob in get_turf(src))
damage(hit_mob)
/// Hurt the passed mob
/obj/effect/brimbeam/proc/damage(mob/living/hit_mob)
hit_mob.apply_damage(damage = 5, damagetype = BURN)
to_chat(hit_mob, span_danger("You're damaged by [src]!"))
/// Disappear
/obj/effect/brimbeam/proc/disperse()
animate(src, time = 0.5 SECONDS, alpha = 0)
QDEL_IN(src, 0.5 SECONDS)
@@ -0,0 +1,87 @@
/// Lavaland mob which tries to line up with its target and fire a laser
/mob/living/basic/mining/brimdemon
name = "brimdemon"
desc = "A volatile creature resembling an enormous horned skull. Its response to almost any stimulus is to unleash a beam of infernal energy."
icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
icon_state = "brimdemon"
icon_living = "brimdemon"
icon_dead = "brimdemon_dead"
speed = 3
maxHealth = 250
health = 250
friendly_verb_continuous = "scratches at"
friendly_verb_simple = "scratch at"
speak_emote = list("cackles")
melee_damage_lower = 7.5
melee_damage_upper = 7.5
attack_sound = 'sound/weapons/bite.ogg'
melee_attack_cooldown = 0.6 SECONDS
attack_vis_effect = ATTACK_EFFECT_BITE
attack_verb_continuous = "bites"
attack_verb_simple = "bite"
death_message = "wails as infernal energy escapes from its wounds, leaving it an empty husk."
death_sound = 'sound/magic/demon_dies.ogg'
light_color = LIGHT_COLOR_BLOOD_MAGIC
light_power = 5
light_range = 1.4
ai_controller = /datum/ai_controller/basic_controller/brimdemon
crusher_loot = /obj/item/crusher_trophy/brimdemon_fang
butcher_results = list(
/obj/item/food/meat/slab = 2,
/obj/effect/decal/cleanable/brimdust = 1,
/obj/item/organ/internal/monster_core/brimdust_sac = 1,
)
/// How we get blasting
var/datum/action/cooldown/mob_cooldown/brimbeam/beam
/mob/living/basic/mining/brimdemon/Initialize(mapload)
. = ..()
AddElement(/datum/element/footstep, FOOTSTEP_MOB_CLAW)
beam = new(src)
beam.Grant(src)
ai_controller.set_blackboard_key(BB_TARGETTED_ACTION, beam)
/mob/living/basic/mining/brimdemon/Destroy()
QDEL_NULL(beam)
return ..()
/mob/living/basic/mining/brimdemon/RangedAttack(atom/target, modifiers)
beam.Trigger(target = target)
/mob/living/basic/mining/brimdemon/death(gibbed)
. = ..()
if (gibbed)
return
var/obj/effect/temp_visual/brim_burst/bang = new(loc)
forceMove(bang)
/// Show a funny animation before doing an explosion
/obj/effect/temp_visual/brim_burst
name = "bursting brimdemon"
icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
icon_state = "brimdemon_dead"
duration = 1.9 SECONDS
/obj/effect/temp_visual/brim_burst/Initialize(mapload)
. = ..()
addtimer(CALLBACK(src, PROC_REF(bang)), duration - (1 DECISECONDS), TIMER_DELETE_ME)
animate(src, color = "#ff8888", transform = matrix().Scale(1.1), time = 0.7 SECONDS)
animate(color = "#ffffff", transform = matrix(), time = 0.2 SECONDS)
animate(color = "#ff4444", transform = matrix().Scale(1.3), time = 0.5 SECONDS)
animate(color = "#ffffff", transform = matrix(), time = 0.2 SECONDS)
animate(color = "#ff0000", transform = matrix().Scale(1.5), time = 0.3 SECONDS)
/// Make an explosion
/obj/effect/temp_visual/brim_burst/proc/bang()
var/turf/origin_turf = get_turf(src)
playsound(origin_turf, 'sound/effects/pop_expl.ogg', 50)
new /obj/effect/temp_visual/explosion/fast(origin_turf)
var/list/possible_targets = range(1, origin_turf)
for(var/mob/living/target in possible_targets)
var/armor = target.run_armor_check(attack_flag = BOMB)
target.apply_damage(20, damagetype = BURN, blocked = armor, spread_damage = TRUE)
for (var/atom/movable/thing as anything in contents)
thing.forceMove(loc)
@@ -0,0 +1,50 @@
/**
* Slap someone who is nearby, line up with target, blast with a beam
*/
/datum/ai_controller/basic_controller/brimdemon
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/brimdemon,
)
ai_traits = PAUSE_DURING_DO_AFTER
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk/no_target
planning_subtrees = list(
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/basic_melee_attack_subtree/opportunistic,
/datum/ai_planning_subtree/move_to_cardinal/brimdemon,
/datum/ai_planning_subtree/targeted_mob_ability/brimbeam,
)
/datum/targetting_datum/basic/brimdemon
stat_attack = HARD_CRIT
/datum/ai_planning_subtree/move_to_cardinal/brimdemon
move_behaviour = /datum/ai_behavior/move_to_cardinal/brimdemon
/datum/ai_behavior/move_to_cardinal/brimdemon
minimum_distance = 2
/datum/ai_behavior/move_to_cardinal/brimdemon/finish_action(datum/ai_controller/controller, succeeded, target_key)
. = ..()
if (!succeeded)
return
var/mob/living/target = controller.blackboard[target_key]
var/datum/action/cooldown/ability = controller.blackboard[BB_TARGETTED_ACTION]
if(!ability?.IsAvailable())
return
ability.InterceptClickOn(caller = controller.pawn, target = target)
/datum/ai_planning_subtree/targeted_mob_ability/brimbeam
use_ability_behaviour = /datum/ai_behavior/targeted_mob_ability/brimbeam
/datum/ai_behavior/targeted_mob_ability/brimbeam
/// Don't shoot if too far away
var/max_target_distance = 9
/datum/ai_behavior/targeted_mob_ability/brimbeam/perform(seconds_per_tick, datum/ai_controller/controller, ability_key, target_key)
var/mob/living/target = controller.blackboard[target_key]
if (QDELETED(target) || !(get_dir(controller.pawn, target) in GLOB.cardinals) || get_dist(controller.pawn, target) > max_target_distance)
finish_action(controller, succeeded = FALSE, ability_key = ability_key, target_key = target_key)
return
return ..()
@@ -0,0 +1,56 @@
/// Brimdemon crusher trophy, it... makes a funny sound?
/obj/item/crusher_trophy/brimdemon_fang
name = "brimdemon's fang"
icon_state = "brimdemon_fang"
desc = "A fang from a brimdemon's corpse."
denied_type = /obj/item/crusher_trophy/brimdemon_fang
/// Cartoon punching vfx
var/static/list/comic_phrases = list("BOOM", "BANG", "KABLOW", "KAPOW", "OUCH", "BAM", "KAPOW", "WHAM", "POW", "KABOOM")
/obj/item/crusher_trophy/brimdemon_fang/effect_desc()
return "mark detonation creates visual and audiosensory effects on the target"
/obj/item/crusher_trophy/brimdemon_fang/on_mark_detonation(mob/living/target, mob/living/user)
target.balloon_alert_to_viewers("[pick(comic_phrases)]!")
playsound(target, 'sound/lavaland/brimdemon_crush.ogg', 100)
/// Reagent pool left by dying brimdemon
/obj/effect/decal/cleanable/brimdust
name = "brimdust"
desc = "Dust from a brimdemon. It is considered valuable for its' botanical abilities."
icon_state = "brimdust"
icon = 'icons/obj/mining.dmi'
layer = FLOOR_CLEAN_LAYER
mergeable_decal = FALSE
/obj/effect/decal/cleanable/brimdust/Initialize(mapload)
. = ..()
reagents.add_reagent(/datum/reagent/brimdust, 15)
/// Ashwalker ore sensor crafted from brimdemon ash
/obj/item/ore_sensor
name = "ore sensor"
desc = "Using demonic frequencies, this ear-mounted tool detects ores in the nearby terrain."
icon_state = "oresensor"
icon = 'icons/obj/mining.dmi'
slot_flags = ITEM_SLOT_EARS
var/range = 5
var/cooldown = 4 SECONDS //between the standard and the advanced ore scanner in strength
COOLDOWN_DECLARE(ore_sensing_cooldown)
/obj/item/ore_sensor/equipped(mob/user, slot, initial)
. = ..()
if(slot & ITEM_SLOT_EARS)
START_PROCESSING(SSobj, src)
else
STOP_PROCESSING(SSobj, src)
/obj/item/ore_sensor/dropped(mob/user, silent)
. = ..()
STOP_PROCESSING(SSobj, src)
/obj/item/ore_sensor/process(seconds_per_tick)
if(!COOLDOWN_FINISHED(src, ore_sensing_cooldown))
return
COOLDOWN_START(src, ore_sensing_cooldown, cooldown)
mineral_scan_pulse(get_turf(src), range)
@@ -108,10 +108,15 @@
if(!dig_ability.IsAvailable())
return
var/mob/living/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
var/has_target = controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET)
//a storm is coming or someone is nearby, its time to escape
if(currently_underground || !currently_underground && storm_approaching || !QDELETED(target))
if(currently_underground)
if(has_target)
return
controller.queue_behavior(/datum/ai_behavior/use_mob_ability/burrow, BB_BURROW_ABILITY)
return SUBTREE_RETURN_FINISH_PLANNING
if(storm_approaching || has_target)
controller.queue_behavior(/datum/ai_behavior/use_mob_ability/burrow, BB_BURROW_ABILITY)
return SUBTREE_RETURN_FINISH_PLANNING
@@ -122,14 +127,10 @@
/datum/ai_planning_subtree/grub_mine
/datum/ai_planning_subtree/grub_mine/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/turf/target_wall = controller.blackboard[BB_TARGET_MINERAL_WALL]
if(QDELETED(target_wall))
controller.queue_behavior(/datum/ai_behavior/find_mineral_wall, BB_TARGET_MINERAL_WALL)
return
controller.queue_behavior(/datum/ai_behavior/mine_wall, BB_TARGET_MINERAL_WALL)
return SUBTREE_RETURN_FINISH_PLANNING
if(controller.blackboard_key_exists(BB_TARGET_MINERAL_WALL))
controller.queue_behavior(/datum/ai_behavior/mine_wall, BB_TARGET_MINERAL_WALL)
return SUBTREE_RETURN_FINISH_PLANNING
controller.queue_behavior(/datum/ai_behavior/find_mineral_wall, BB_TARGET_MINERAL_WALL)
/datum/ai_behavior/mine_wall
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
@@ -168,7 +169,7 @@
/datum/pet_command/grub_spit/execute_action(datum/ai_controller/controller)
var/datum/action/cooldown/spit_ability = controller.blackboard[BB_SPIT_ABILITY]
if(QDELETED(spit_ability) || !spit_ability.IsAvailable())
if(!spit_ability?.IsAvailable())
return
controller.queue_behavior(/datum/ai_behavior/use_mob_ability, BB_SPIT_ABILITY)
controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
@@ -96,8 +96,7 @@
var/target_key = BB_GOLIATH_HOLE_TARGET
/datum/ai_planning_subtree/goliath_dig/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/turf/target_turf = controller.blackboard[target_key]
if (QDELETED(target_turf))
if (!controller.blackboard_key_exists(target_key))
return
controller.queue_behavior(/datum/ai_behavior/goliath_dig, target_key)
return SUBTREE_RETURN_FINISH_PLANNING
@@ -3,6 +3,7 @@
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
)
ai_traits = PAUSE_DURING_DO_AFTER
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
@@ -19,14 +19,10 @@
///find dead humans and report their location on the radio
/datum/ai_planning_subtree/locate_dead_humans/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/mob/living/dead_target = controller.blackboard[BB_NEARBY_DEAD_MINER]
if(QDELETED(dead_target))
controller.queue_behavior(/datum/ai_behavior/find_and_set/unconscious_human, BB_NEARBY_DEAD_MINER, /mob/living/carbon/human)
return
controller.queue_behavior(/datum/ai_behavior/send_sos_message, BB_NEARBY_DEAD_MINER)
return SUBTREE_RETURN_FINISH_PLANNING
if(controller.blackboard_key_exists(BB_NEARBY_DEAD_MINER))
controller.queue_behavior(/datum/ai_behavior/send_sos_message, BB_NEARBY_DEAD_MINER)
return SUBTREE_RETURN_FINISH_PLANNING
controller.queue_behavior(/datum/ai_behavior/find_and_set/unconscious_human, BB_NEARBY_DEAD_MINER, /mob/living/carbon/human)
/datum/ai_behavior/find_and_set/unconscious_human/search_tactic(datum/ai_controller/controller, locate_path, search_range)
for(var/mob/living/carbon/human/target in oview(search_range, controller.pawn))
@@ -73,18 +69,12 @@
///mine walls if we are on automated mining mode
/datum/ai_planning_subtree/minebot_mining/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/turf/mineral_target = controller.blackboard[BB_TARGET_MINERAL_TURF]
var/automated_mining = controller.blackboard[BB_AUTOMATED_MINING]
if(!automated_mining)
if(!controller.blackboard[BB_AUTOMATED_MINING])
return
if(QDELETED(mineral_target))
controller.queue_behavior(/datum/ai_behavior/find_mineral_wall/minebot, BB_TARGET_MINERAL_TURF)
return
controller.queue_behavior(/datum/ai_behavior/minebot_mine_turf, BB_TARGET_MINERAL_TURF)
return SUBTREE_RETURN_FINISH_PLANNING
if(controller.blackboard_key_exists(BB_TARGET_MINERAL_TURF))
controller.queue_behavior(/datum/ai_behavior/minebot_mine_turf, BB_TARGET_MINERAL_TURF)
return SUBTREE_RETURN_FINISH_PLANNING
controller.queue_behavior(/datum/ai_behavior/find_mineral_wall/minebot, BB_TARGET_MINERAL_TURF)
/datum/ai_behavior/find_mineral_wall/minebot
@@ -190,7 +180,7 @@
/datum/pet_command/minebot_ability/execute_action(datum/ai_controller/controller)
var/datum/action/cooldown/ability = controller.blackboard[ability_key]
if(QDELETED(ability) || !ability.IsAvailable())
if(!ability?.IsAvailable())
return
controller.queue_behavior(/datum/ai_behavior/use_mob_ability, ability_key)
controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
@@ -34,9 +34,7 @@
/datum/ai_planning_subtree/find_nearest_magicarp_spell_target/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/datum/action/cooldown/using_action = controller.blackboard[BB_MAGICARP_SPELL]
if (QDELETED(using_action))
return
if (!using_action.IsAvailable())
if (!using_action?.IsAvailable())
return
var/spell_targetting = controller.blackboard[BB_MAGICARP_SPELL_SPECIAL_TARGETTING]
@@ -12,9 +12,10 @@
if (!rift_behaviour)
CRASH("Forgot to specify rift behaviour for [src]")
var/mob/living/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
if (!controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
return
var/datum/action/cooldown/using_action = controller.blackboard[BB_CARP_RIFT]
if (QDELETED(target) || QDELETED(using_action) || !using_action.IsAvailable())
if (!using_action?.IsAvailable())
return
controller.queue_behavior(rift_behaviour, BB_CARP_RIFT, BB_BASIC_MOB_CURRENT_TARGET)
@@ -17,14 +17,10 @@
/datum/ai_planning_subtree/heal_the_blind
/datum/ai_planning_subtree/heal_the_blind/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/mob/living/carbon/target = controller.blackboard[BB_BLIND_TARGET]
if(QDELETED(target))
controller.queue_behavior(/datum/ai_behavior/find_the_blind, BB_BLIND_TARGET, BB_EYE_DAMAGE_THRESHOLD)
return
controller.queue_behavior(/datum/ai_behavior/heal_eye_damage, BB_BLIND_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
if(controller.blackboard_key_exists(BB_BLIND_TARGET))
controller.queue_behavior(/datum/ai_behavior/heal_eye_damage, BB_BLIND_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
controller.queue_behavior(/datum/ai_behavior/find_the_blind, BB_BLIND_TARGET, BB_EYE_DAMAGE_THRESHOLD)
/datum/targetting_datum/basic/eyeball/can_attack(mob/living/owner, atom/target)
. = ..()
@@ -54,14 +54,10 @@
if(!SPT_PROB(relay_chance, seconds_per_tick))
return
var/mob/hive_target = controller.blackboard[BB_HIVE_PARTNER]
if(QDELETED(hive_target))
controller.queue_behavior(/datum/ai_behavior/find_and_set/hive_partner, BB_HIVE_PARTNER, /mob/living/basic/hivebot)
return
controller.queue_behavior(/datum/ai_behavior/relay_message, BB_HIVE_PARTNER)
return SUBTREE_RETURN_FINISH_PLANNING
if (controller.blackboard_key_exists(BB_HIVE_PARTNER))
controller.queue_behavior(/datum/ai_behavior/relay_message, BB_HIVE_PARTNER)
return SUBTREE_RETURN_FINISH_PLANNING
controller.queue_behavior(/datum/ai_behavior/find_and_set/hive_partner, BB_HIVE_PARTNER, /mob/living/basic/hivebot)
/datum/ai_planning_subtree/find_and_hunt_target/repair_machines
target_key = BB_MACHINE_TARGET
@@ -4,7 +4,7 @@
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic(),
)
ai_traits = STOP_MOVING_WHEN_PULLED
ai_traits = STOP_MOVING_WHEN_PULLED | PAUSE_DURING_DO_AFTER
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk/less_walking
@@ -62,12 +62,9 @@
var/target_key = BB_SPIDER_WEB_TARGET
/datum/ai_planning_subtree/spin_web/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/datum/action/cooldown/using_action = controller.blackboard[action_key]
var/turf/target_turf = controller.blackboard[target_key]
if (QDELETED(using_action) || QDELETED(target_turf))
return
controller.queue_behavior(/datum/ai_behavior/spin_web, action_key, target_key)
return SUBTREE_RETURN_FINISH_PLANNING
if (controller.blackboard_key_exists(action_key) && controller.blackboard_key_exists(target_key))
controller.queue_behavior(/datum/ai_behavior/spin_web, action_key, target_key)
return SUBTREE_RETURN_FINISH_PLANNING
/// Move to an unwebbed nearby turf and web it up
/datum/ai_behavior/spin_web
@@ -1,242 +0,0 @@
#define BRIMBEAM_RANGE 10
/mob/living/simple_animal/hostile/asteroid/brimdemon
name = "brimdemon"
desc = "A misshapen demon with big, red eyes and a hinged mouth. Not much is known about the creatures \
due to their response to any unexpected stimulus being \"brimbeam\", a deadly blood-laser barrage."
icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
icon_state = "brimdemon"
icon_living = "brimdemon"
icon_dead = "brimdemon_dead"
mob_biotypes = MOB_ORGANIC|MOB_BEAST
speak_emote = list("cackles")
emote_taunt = list("screeches")
emote_hear = list("cackles","screeches")
combat_mode = TRUE
stat_attack = HARD_CRIT
ranged_cooldown_time = 5 SECONDS
vision_range = 9
retreat_distance = 2
speed = 3
move_to_delay = 5
maxHealth = 250
health = 250
obj_damage = 15
melee_damage_lower = 7.5
melee_damage_upper = 7.5
rapid_melee = 2 // every second attack
attack_verb_continuous = "bites"
attack_verb_simple = "bite"
attack_sound = 'sound/weapons/bite.ogg'
attack_vis_effect = ATTACK_EFFECT_BITE
butcher_results = list(
/obj/item/food/meat/slab = 2,
/obj/effect/decal/cleanable/brimdust = 1,
/obj/item/organ/internal/monster_core/brimdust_sac = 1,
)
loot = list()
robust_searching = TRUE
footstep_type = FOOTSTEP_MOB_CLAW
death_message = "wails as infernal energy escapes from its wounds, leaving it an empty husk."
death_sound = 'sound/magic/demon_dies.ogg'
light_color = LIGHT_COLOR_BLOOD_MAGIC
light_power = 5
light_range = 1.4
crusher_loot = /obj/item/crusher_trophy/brimdemon_fang
/// Are we charging/firing? If yes stops our movement.
var/firing = FALSE
/// A list of all the beam parts.
var/list/beamparts = list()
/mob/living/simple_animal/hostile/asteroid/brimdemon/Destroy()
QDEL_LIST(beamparts)
return ..()
/mob/living/simple_animal/hostile/asteroid/brimdemon/Login()
ranged = TRUE
return ..()
/mob/living/simple_animal/hostile/asteroid/brimdemon/Logout()
ranged = FALSE
return ..()
/mob/living/simple_animal/hostile/asteroid/brimdemon/death()
firing = FALSE
cut_overlay("brimdemon_telegraph_dir")
move_resist = initial(move_resist)
return ..()
/mob/living/simple_animal/hostile/asteroid/brimdemon/Goto(target, delay, minimum_distance)
if(firing)
return FALSE
return ..()
/mob/living/simple_animal/hostile/asteroid/brimdemon/MoveToTarget(list/possible_targets)
if(firing)
return FALSE
return ..()
/mob/living/simple_animal/hostile/asteroid/brimdemon/AttackingTarget(atom/attacked_target)
if(firing)
return FALSE
return ..()
/mob/living/simple_animal/hostile/asteroid/brimdemon/Move(atom/newloc, dir, step_x , step_y)
if(firing)
return FALSE
return ..()
/mob/living/simple_animal/hostile/asteroid/brimdemon/OpenFire()
if(firing)
balloon_alert(src, "already firing!")
return
if(!COOLDOWN_FINISHED(src, ranged_cooldown))
balloon_alert(src, "on cooldown!")
return
firing = TRUE
set_dir_on_move = FALSE
icon_state = "brimdemon_firing"
move_resist = MOVE_FORCE_VERY_STRONG
add_overlay("brimdemon_telegraph_dir")
visible_message(span_danger("[src] starts charging!"))
balloon_alert(src, "charging...")
addtimer(CALLBACK(src, PROC_REF(fire_laser)), 1 SECONDS)
COOLDOWN_START(src, ranged_cooldown, ranged_cooldown_time)
/mob/living/simple_animal/hostile/asteroid/brimdemon/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
. = ..()
check_fire()
/mob/living/simple_animal/hostile/asteroid/brimdemon/proc/check_fire()
if(key || QDELETED(target) || get_dist(src, target) > BRIMBEAM_RANGE || !(get_dir(src, target) in GLOB.cardinals))
return
face_atom(target)
OpenFire()
/// Fires a brimbeam, getting a line of turfs between it and the direction to the target and creating a brimbeam effect on every one of them.
/mob/living/simple_animal/hostile/asteroid/brimdemon/proc/fire_laser()
if(stat == DEAD)
return
visible_message(span_danger("[src] fires a brimbeam!"))
balloon_alert(src, "brimbeam fired")
playsound(src, 'sound/creatures/brimdemon.ogg', 150, FALSE, 0, 3)
cut_overlay("brimdemon_telegraph_dir")
var/turf/target_turf = get_ranged_target_turf(src, dir, BRIMBEAM_RANGE)
var/turf/origin_turf = get_turf(src)
var/list/affected_turfs = get_line(origin_turf, target_turf) - origin_turf
for(var/turf/affected_turf in affected_turfs)
var/blocked = FALSE
if(affected_turf.opacity)
blocked = TRUE
for(var/obj/potential_block in affected_turf.contents)
if(potential_block.opacity)
blocked = TRUE
break
if(blocked)
break
var/atom/new_brimbeam = new /obj/effect/brimbeam(affected_turf)
new_brimbeam.dir = dir
beamparts += new_brimbeam
for(var/mob/living/hit_mob in affected_turf.contents)
hit_mob.adjustFireLoss(25)
to_chat(hit_mob, span_userdanger("You're hit by [src]'s brimbeam!"))
if(length(beamparts))
var/atom/last_brimbeam = beamparts[length(beamparts)]
last_brimbeam.icon_state = "brimbeam_end"
var/atom/first_brimbeam = beamparts[1]
first_brimbeam.icon_state = "brimbeam_start"
addtimer(CALLBACK(src, PROC_REF(end_laser)), 2 SECONDS)
/// Deletes all the brimbeam parts and sets variables back to their initial ones.
/mob/living/simple_animal/hostile/asteroid/brimdemon/proc/end_laser()
if(stat != DEAD)
icon_state = initial(icon_state)
move_resist = initial(move_resist)
set_dir_on_move = initial(set_dir_on_move)
firing = FALSE
for(var/obj/effect/brimbeam/beam in beamparts)
animate(beam, time = 0.5 SECONDS, alpha = 0)
QDEL_IN(beam, 0.5 SECONDS)
beamparts -= beam
/obj/effect/brimbeam
name = "brimbeam"
icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
icon_state = "brimbeam_mid"
layer = ABOVE_MOB_LAYER
plane = ABOVE_GAME_PLANE
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
light_color = LIGHT_COLOR_BLOOD_MAGIC
light_power = 3
light_range = 2
/obj/effect/brimbeam/Initialize(mapload)
. = ..()
START_PROCESSING(SSfastprocess, src)
/obj/effect/brimbeam/Destroy()
STOP_PROCESSING(SSfastprocess, src)
return ..()
/obj/effect/brimbeam/process()
for(var/mob/living/hit_mob in get_turf(src))
damage(hit_mob)
/obj/effect/brimbeam/proc/damage(mob/living/hit_mob)
hit_mob.adjustFireLoss(5)
to_chat(hit_mob, span_danger("You're damaged by [src]!"))
/obj/item/crusher_trophy/brimdemon_fang
name = "brimdemon's fang"
icon_state = "brimdemon_fang"
desc = "A fang from a brimdemon's corpse."
denied_type = /obj/item/crusher_trophy/brimdemon_fang
var/static/list/comic_phrases = list("BOOM", "BANG", "KABLOW", "KAPOW", "OUCH", "BAM", "KAPOW", "WHAM", "POW", "KABOOM")
/obj/item/crusher_trophy/brimdemon_fang/effect_desc()
return "mark detonation creates visual and audiosensory effects on the target"
/obj/item/crusher_trophy/brimdemon_fang/on_mark_detonation(mob/living/target, mob/living/user)
target.balloon_alert_to_viewers("[pick(comic_phrases)]!")
playsound(target, 'sound/lavaland/brimdemon_crush.ogg', 100)
/obj/effect/decal/cleanable/brimdust
name = "brimdust"
desc = "Dust from a brimdemon. It is considered valuable for its' botanical abilities."
icon_state = "brimdust"
icon = 'icons/obj/mining.dmi'
layer = FLOOR_CLEAN_LAYER
mergeable_decal = FALSE
/obj/effect/decal/cleanable/brimdust/Initialize(mapload)
. = ..()
reagents.add_reagent(/datum/reagent/brimdust, 15)
/obj/item/ore_sensor
name = "ore sensor"
desc = "Using demonic frequencies, this ear-mounted tool detects ores in the nearby terrain."
icon_state = "oresensor"
icon = 'icons/obj/mining.dmi'
slot_flags = ITEM_SLOT_EARS
var/range = 5
var/cooldown = 4 SECONDS //between the standard and the advanced ore scanner in strength
COOLDOWN_DECLARE(ore_sensing_cooldown)
/obj/item/ore_sensor/equipped(mob/user, slot, initial)
. = ..()
if(slot & ITEM_SLOT_EARS)
START_PROCESSING(SSobj, src)
else
STOP_PROCESSING(SSobj, src)
/obj/item/ore_sensor/dropped(mob/user, silent)
. = ..()
STOP_PROCESSING(SSobj, src)
/obj/item/ore_sensor/process(seconds_per_tick)
if(!COOLDOWN_FINISHED(src, ore_sensing_cooldown))
return
COOLDOWN_START(src, ore_sensing_cooldown, cooldown)
mineral_scan_pulse(get_turf(src), range)
#undef BRIMBEAM_RANGE
@@ -64,17 +64,17 @@
build_all_button_icons()
return TRUE
/datum/action/cooldown/spell/pointed/InterceptClickOn(mob/living/caller, params, atom/click_target)
/datum/action/cooldown/spell/pointed/InterceptClickOn(mob/living/caller, params, atom/target)
var/atom/aim_assist_target
if(aim_assist && isturf(click_target))
if(aim_assist && isturf(target))
// Find any human in the list. We aren't picky, it's aim assist after all
aim_assist_target = locate(/mob/living/carbon/human) in click_target
aim_assist_target = locate(/mob/living/carbon/human) in target
if(!aim_assist_target)
// If we didn't find a human, we settle for any living at all
aim_assist_target = locate(/mob/living) in click_target
aim_assist_target = locate(/mob/living) in target
return ..(caller, params, aim_assist_target || click_target)
return ..(caller, params, aim_assist_target || target)
/datum/action/cooldown/spell/pointed/is_valid_target(atom/cast_on)
if(cast_on == owner)
@@ -35,27 +35,27 @@
return FALSE
return TRUE
/datum/action/cooldown/spell/pointed/swap/InterceptClickOn(mob/living/caller, params, atom/click_target)
/datum/action/cooldown/spell/pointed/swap/InterceptClickOn(mob/living/caller, params, atom/target)
if(LAZYACCESS(params2list(params), RIGHT_CLICK))
if(!IsAvailable(feedback = TRUE))
return FALSE
if(!target)
return FALSE
if(!isliving(click_target) || isturf(click_target))
if(!isliving(target) || isturf(target))
// Find any living being in the list. We aren't picky, it's aim assist after all
click_target = locate(/mob/living) in click_target
if(!click_target)
target = locate(/mob/living) in target
if(!target)
to_chat(owner, span_warning("You can only select living beings as secondary target!"))
return FALSE
if(click_target == owner)
if(target == owner)
if(!isnull(second_target))
to_chat(owner, span_notice("You cancel your secondary swap target!"))
second_target = null
else
to_chat(owner, span_warning("You have no secondary swap target!"))
return FALSE
second_target = click_target
to_chat(owner, span_notice("You select [click_target.name] as a secondary swap target!"))
second_target = target
to_chat(owner, span_notice("You select [target.name] as a secondary swap target!"))
return FALSE
return ..()
@@ -51,7 +51,6 @@
/mob/living/simple_animal/hostile/alien/queen/large,
/mob/living/simple_animal/hostile/alien/sentinel,
/mob/living/simple_animal/hostile/asteroid,
/mob/living/simple_animal/hostile/asteroid/brimdemon,
/mob/living/simple_animal/hostile/asteroid/curseblob,
/mob/living/simple_animal/hostile/asteroid/elite,
/mob/living/simple_animal/hostile/asteroid/elite/broodmother,