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
/🆑
This commit is contained in:
Jacquerel
2023-09-23 02:12:43 +01:00
committed by GitHub
parent 48c0039ff7
commit b2ecd81be4
45 changed files with 548 additions and 393 deletions
@@ -0,0 +1,33 @@
/// Attack something which is already adjacent to us, without ending planning
/datum/ai_planning_subtree/basic_melee_attack_subtree/opportunistic
melee_attack_behavior = /datum/ai_behavior/basic_melee_attack/opportunistic
end_planning = FALSE
/datum/ai_planning_subtree/basic_melee_attack_subtree/opportunistic/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
if(QDELETED(target) || !controller.pawn.Adjacent(target))
return
if (isliving(controller.pawn))
var/mob/living/pawn = controller.pawn
if (LAZYLEN(pawn.do_afters))
return
controller.queue_behavior(melee_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
/// Attack something which is already adjacent to us without moving
/datum/ai_behavior/basic_melee_attack/opportunistic
action_cooldown = 0.2 SECONDS // We gotta check unfortunately often because we're in a race condition with nextmove
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/datum/ai_behavior/basic_melee_attack/opportunistic/setup(datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
if (!controller.blackboard_key_exists(targetting_datum_key))
CRASH("No target datum was supplied in the blackboard for [controller.pawn]")
return controller.blackboard_key_exists(target_key)
/datum/ai_behavior/basic_melee_attack/opportunistic/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
var/atom/movable/atom_pawn = controller.pawn
if(!atom_pawn.CanReach(controller.blackboard[target_key]))
finish_action(controller, TRUE, target_key) // Don't clear target
return FALSE
. = ..()
finish_action(controller, TRUE, target_key) // Try doing something else
@@ -7,11 +7,8 @@
if(!SPT_PROB(climb_chance, seconds_per_tick))
return
var/obj/structure/flora/tree/target = controller.blackboard[BB_CLIMBED_TREE]
if(controller.blackboard_key_exists(BB_CLIMBED_TREE))
controller.queue_behavior(/datum/ai_behavior/climb_tree, BB_CLIMBED_TREE)
return SUBTREE_RETURN_FINISH_PLANNING
if(QDELETED(target))
controller.queue_behavior(/datum/ai_behavior/find_and_set/valid_tree, BB_CLIMBED_TREE, /obj/structure/flora/tree)
return
controller.queue_behavior(/datum/ai_behavior/climb_tree, BB_CLIMBED_TREE)
return SUBTREE_RETURN_FINISH_PLANNING
controller.queue_behavior(/datum/ai_behavior/find_and_set/valid_tree, BB_CLIMBED_TREE, /obj/structure/flora/tree)
@@ -3,8 +3,7 @@
/datum/ai_planning_subtree/find_food/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
if(!QDELETED(target))
if(controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
// Busy with something
return
@@ -1,10 +1,9 @@
/datum/ai_planning_subtree/find_paper_and_write
/datum/ai_planning_subtree/find_paper_and_write/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/obj/item/inhand_paper = controller.blackboard[BB_SIMPLE_CARRY_ITEM]
var/mob/living/basic/wizard = controller.pawn
if(!QDELETED(inhand_paper))
if(controller.blackboard_key_exists(BB_SIMPLE_CARRY_ITEM))
controller.queue_behavior(/datum/ai_behavior/write_on_paper, BB_SIMPLE_CARRY_ITEM, BB_WRITING_LIST)
return SUBTREE_RETURN_FINISH_PLANNING
@@ -9,10 +9,7 @@
/datum/ai_planning_subtree/flee_target/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
if (!controller.blackboard[BB_BASIC_MOB_FLEEING])
return
var/atom/target = controller.blackboard[target_key]
if(QDELETED(target))
if (!controller.blackboard[BB_BASIC_MOB_FLEEING] || !controller.blackboard_key_exists(target_key))
return
controller.queue_behavior(flee_behaviour, target_key, hiding_place_key)
return SUBTREE_RETURN_FINISH_PLANNING //we gotta get out of here.
@@ -11,9 +11,6 @@
/datum/ai_planning_subtree/maintain_distance/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
var/mob/living/living_pawn = controller.pawn
if(LAZYLEN(living_pawn.do_afters))
return
var/atom/target = controller.blackboard[target_key]
if (!isliving(target) || !can_see(controller.pawn, target, view_distance))
return // Don't run away from cucumbers, they're not snakes
@@ -0,0 +1,71 @@
/// Try to line up with a cardinal direction of your target
/datum/ai_planning_subtree/move_to_cardinal
/// Behaviour to execute to line ourselves up
var/move_behaviour = /datum/ai_behavior/move_to_cardinal
/// Blackboard key in which to store selected target
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
/datum/ai_planning_subtree/move_to_cardinal/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
if(!controller.blackboard_key_exists(target_key))
return
controller.queue_behavior(move_behaviour, target_key)
/// Try to line up with a cardinal direction of your target
/datum/ai_behavior/move_to_cardinal
required_distance = 0
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/// How close to our target is too close?
var/minimum_distance = 1
/// How far away is too far?
var/maximum_distance = 9
/datum/ai_behavior/move_to_cardinal/setup(datum/ai_controller/controller, target_key)
var/atom/target = controller.blackboard[target_key]
if(QDELETED(target))
return FALSE
target_nearest_cardinal(controller, target)
return TRUE
/// Set our movement target to the closest cardinal space to our target
/datum/ai_behavior/move_to_cardinal/proc/target_nearest_cardinal(datum/ai_controller/controller, atom/target)
var/atom/move_target
var/closest = INFINITY
for (var/dir in GLOB.cardinals)
var/turf/cardinal_turf = get_ranged_target_turf(target, dir, minimum_distance)
if (cardinal_turf.is_blocked_turf())
continue
var/distance_to = get_dist(controller.pawn, cardinal_turf)
if (distance_to >= closest)
continue
closest = distance_to
move_target = cardinal_turf
if (isnull(move_target))
move_target = target
if (controller.current_movement_target == move_target)
return
set_movement_target(controller, move_target)
/datum/ai_behavior/move_to_cardinal/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
var/atom/target = controller.blackboard[target_key]
if (QDELETED(target))
finish_action(controller = controller, succeeded = FALSE, target_key = target_key)
return
if (!(get_dir(controller.pawn, target) in GLOB.cardinals))
target_nearest_cardinal(controller, target)
return
var/distance_to_target = get_dist(controller.pawn, target)
if (distance_to_target < minimum_distance)
target_nearest_cardinal(controller, target)
return
if (distance_to_target > maximum_distance)
return
finish_action(controller = controller, succeeded = TRUE, target_key = target_key)
return
/datum/ai_behavior/move_to_cardinal/finish_action(datum/ai_controller/controller, succeeded, target_key)
if (!succeeded)
controller.clear_blackboard_key(target_key)
return ..()
@@ -8,8 +8,7 @@
/datum/ai_planning_subtree/ranged_skirmish/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
var/atom/target = controller.blackboard[target_key]
if(QDELETED(target))
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)
@@ -1,13 +1,16 @@
/datum/ai_planning_subtree/basic_melee_attack_subtree
/// What do we do in order to attack someone?
var/datum/ai_behavior/basic_melee_attack/melee_attack_behavior = /datum/ai_behavior/basic_melee_attack
/// Is this the last thing we do? (if we set a movement target, this will usually be yes)
var/end_planning = TRUE
/datum/ai_planning_subtree/basic_melee_attack_subtree/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
if(QDELETED(target))
if(!controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
return
controller.queue_behavior(melee_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
return SUBTREE_RETURN_FINISH_PLANNING //we are going into battle...no distractions.
if (end_planning)
return SUBTREE_RETURN_FINISH_PLANNING //we are going into battle...no distractions.
/datum/ai_planning_subtree/basic_ranged_attack_subtree
operational_datums = list(/datum/component/ranged_attacks)
@@ -15,8 +18,7 @@
/datum/ai_planning_subtree/basic_ranged_attack_subtree/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
if(QDELETED(target))
if(!controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
return
controller.queue_behavior(ranged_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
return SUBTREE_RETURN_FINISH_PLANNING //we are going into battle...no distractions.
@@ -16,7 +16,7 @@
/datum/ai_behavior/sleep_after_targetless_time/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
var/atom/target = controller.blackboard[target_key]
finish_action(controller, succeeded = !target, seconds_per_tick = seconds_per_tick)
finish_action(controller, succeeded = QDELETED(target), seconds_per_tick = seconds_per_tick)
/datum/ai_behavior/sleep_after_targetless_time/finish_action(datum/ai_controller/controller, succeeded, seconds_per_tick)
. = ..()
@@ -13,9 +13,11 @@
if (!ability_key)
CRASH("You forgot to tell this mob where to find its ability")
var/mob/living/target = controller.blackboard[target_key]
if (!controller.blackboard_key_exists(target_key))
return
var/datum/action/cooldown/using_action = controller.blackboard[ability_key]
if (QDELETED(target) || QDELETED(using_action) || !using_action.IsAvailable())
if (!using_action?.IsAvailable())
return
controller.queue_behavior(use_ability_behaviour, ability_key, target_key)
@@ -15,7 +15,7 @@
CRASH("You forgot to tell this mob where to find its ability")
var/datum/action/cooldown/using_action = controller.blackboard[ability_key]
if (QDELETED(using_action) || !using_action.IsAvailable())
if (!using_action?.IsAvailable())
return
controller.queue_behavior(use_ability_behaviour, ability_key)