port minebots to basic mobs and add some behavior (#29319)

* port minebots to basic mobs and add some behavior

* remove unused define

* update is_blocked_turf usage

* fix radio, goldgrub, and armor upgrade

* standardize blackboard names

* Apply suggestions from code review

Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com>
Signed-off-by: warriorstar-orion <orion@snowfrost.garden>

* lewc review

* whoops

* Apply suggestions from code review

Co-authored-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com>
Signed-off-by: warriorstar-orion <orion@snowfrost.garden>

---------

Signed-off-by: warriorstar-orion <orion@snowfrost.garden>
Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com>
Co-authored-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com>
This commit is contained in:
warriorstar-orion
2025-06-13 19:48:40 +00:00
committed by GitHub
co-authored by Luc PollardTheDragon
parent 586e2e6c4d
commit e63415a483
50 changed files with 1892 additions and 529 deletions
@@ -124,7 +124,7 @@
var/turf/picked_turf = get_closest_atom(/turf, possible_turfs, target)
step(living_pawn, get_dir(living_pawn, picked_turf))
/datum/ai_behavior/basic_ranged_attack/proc/calculate_trajectory(mob/living/source , atom/target)
/datum/ai_behavior/basic_ranged_attack/proc/calculate_trajectory(mob/living/source, atom/target)
var/list/turf_list = get_line(source, target)
var/list_length = length(turf_list) - 1
for(var/i in 1 to list_length)
@@ -0,0 +1,19 @@
///behavior to befriend any targets
/datum/ai_behavior/befriend_target
/datum/ai_behavior/befriend_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key, befriend_message)
var/mob/living/living_pawn = controller.pawn
var/mob/living/living_target = controller.blackboard[target_key]
if(QDELETED(living_target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
living_pawn.befriend(living_target)
var/befriend_text = controller.blackboard[befriend_message]
if(befriend_text)
to_chat(living_target, "<span class='notice'>[living_pawn] [befriend_text]</span>")
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/befriend_target/finish_action(datum/ai_controller/controller, succeeded, target_key)
. = ..()
controller.clear_blackboard_key(target_key)
@@ -0,0 +1,53 @@
/**
* # Travel Towards
* Moves towards the atom in the passed blackboard key.
* Planning continues during this action so it can be interrupted by higher priority actions.
*/
/datum/ai_behavior/travel_towards
required_distance = 0
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/// If true we will get rid of our target on completion
var/clear_target = FALSE
/// If desired, an alternate [/datum/ai_movement] to use for movement/pathfinding.
var/new_movement_type
/datum/ai_behavior/travel_towards/setup(datum/ai_controller/controller, target_key)
. = ..()
var/atom/target = controller.blackboard[target_key]
if(QDELETED(target))
return FALSE
set_movement_target(controller, target, new_movement_type)
/datum/ai_behavior/travel_towards/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/travel_towards/finish_action(datum/ai_controller/controller, succeeded, target_key)
. = ..()
if(clear_target)
controller.clear_blackboard_key(target_key)
if(new_movement_type)
controller.change_ai_movement_type(initial(controller.ai_movement))
/datum/ai_behavior/travel_towards/stop_on_arrival
clear_target = TRUE
/datum/ai_behavior/travel_towards/adjacent
required_distance = 1
/**
* # Travel Towards Atom
* Travel towards an atom you pass directly from the controller rather than a blackboard key.
* You might need to do this to avoid repeating some checks in both a controller and an action.
*/
/datum/ai_behavior/travel_towards_atom
required_distance = 0
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
/datum/ai_behavior/travel_towards_atom/setup(datum/ai_controller/controller, atom/target_atom)
. = ..()
if(isnull(target_atom))
return FALSE
set_movement_target(controller, target_atom)
/datum/ai_behavior/travel_towards_atom/perform(seconds_per_tick, datum/ai_controller/controller, atom/target_atom)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
@@ -0,0 +1,31 @@
/datum/ai_behavior/find_mineral_wall
/datum/ai_behavior/find_mineral_wall/perform(seconds_per_tick, datum/ai_controller/controller, found_wall_key)
var/mob/living_pawn = controller.pawn
for(var/turf/simulated/mineral/potential_wall in oview(9, living_pawn))
if(!check_if_mineable(controller, potential_wall)) // check if its surrounded by walls
continue
controller.set_blackboard_key(found_wall_key, potential_wall) // closest wall first!
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
/// Check to see if we can get to and mine the turf the mineral is in
/datum/ai_behavior/find_mineral_wall/proc/check_if_mineable(datum/ai_controller/controller, turf/target_wall)
var/mob/living/source = controller.pawn
var/direction_to_turf = get_dir(target_wall, source)
if(!IS_DIR_DIAGONAL(direction_to_turf))
return TRUE
var/list/directions_to_check = list()
for(var/direction_check in GLOB.cardinal)
if(direction_check & direction_to_turf)
directions_to_check += direction_check
for(var/direction in directions_to_check)
var/turf/test_turf = get_step(target_wall, direction)
if(isnull(test_turf))
continue
if(!test_turf.is_blocked_turf(source_atom = source))
return TRUE
return FALSE
@@ -11,3 +11,14 @@
controller.queue_behavior(melee_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
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)
var/datum/ai_behavior/basic_ranged_attack/ranged_attack_behavior = /datum/ai_behavior/basic_ranged_attack
/datum/ai_planning_subtree/basic_ranged_attack_subtree/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
if(!controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
return
controller.queue_behavior(ranged_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
return SUBTREE_RETURN_FINISH_PLANNING //we are going into battle...no distractions.
@@ -0,0 +1,19 @@
/datum/ai_planning_subtree/simple_find_target
/// Variable to store target in
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
/datum/ai_planning_subtree/simple_find_target/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
controller.queue_behavior(/datum/ai_behavior/find_potential_targets, target_key, BB_TARGETING_STRATEGY, 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
/datum/ai_planning_subtree/simple_find_target/not_while_observed/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
for(var/mob/living/carbon/human/watcher in hearers(11, controller.pawn))
if(watcher.stat != DEAD)
return
return ..()
/datum/ai_planning_subtree/simple_find_target/to_flee
target_key = BB_BASIC_MOB_FLEE_TARGET
@@ -0,0 +1,33 @@
/**
* Simple behaviours which simply try to use an ability whenever it is available.
* For something which wants a target try `targeted_mob_ability`.
*/
/datum/ai_planning_subtree/use_mob_ability
/// Blackboard key for the ability
var/ability_key = BB_GENERIC_ACTION
/// Behaviour to perform using ability
var/use_ability_behavior = /datum/ai_behavior/use_mob_ability
/// If true we terminate planning after trying to use the ability.
var/finish_planning = FALSE
/datum/ai_planning_subtree/use_mob_ability/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
if(!ability_key)
CRASH("You forgot to tell this mob where to find its ability")
var/datum/action/using_action = controller.blackboard[ability_key]
if(!using_action?.IsAvailable())
return
controller.queue_behavior(use_ability_behavior, ability_key)
if(finish_planning)
return SUBTREE_RETURN_FINISH_PLANNING
/datum/ai_behavior/use_mob_ability
/datum/ai_behavior/use_mob_ability/perform(seconds_per_tick, datum/ai_controller/controller, ability_key)
var/datum/action/using_action = controller.blackboard[ability_key]
if(QDELETED(using_action))
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
if(using_action.Trigger())
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
@@ -0,0 +1,52 @@
/**
* Tells the AI to find a certain target nearby to hunt.
* If a target has been found, we will start to move towards it, and eventually attack it.
*/
/datum/ai_planning_subtree/find_and_hunt_target
/// What key in the blacbkboard do we store our hunting target?
/// If you want to have multiple hunting behaviors on a controller be sure that this is unique
var/target_key = BB_CURRENT_HUNTING_TARGET
/// What behavior to execute if we have no target
var/finding_behavior = /datum/ai_behavior/find_hunt_target
/// What behavior to execute if we do have a target
var/hunting_behavior = /datum/ai_behavior/hunt_target
/// What targets we're hunting for
var/list/hunt_targets
/// In what radius will we hunt
var/hunt_range = 2
/// What are the chances we hunt something at any given moment
var/hunt_chance = 100
///do we finish planning subtree
var/finish_planning = TRUE
/datum/ai_planning_subtree/find_and_hunt_target/New()
. = ..()
hunt_targets = typecacheof(hunt_targets)
/datum/ai_planning_subtree/find_and_hunt_target/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
if(!SPT_PROB(hunt_chance, seconds_per_tick))
return
if(controller.blackboard[BB_HUNTING_COOLDOWN(type)] >= world.time)
return
if(!controller.blackboard_key_exists(target_key))
controller.queue_behavior(finding_behavior, target_key, hunt_targets, hunt_range)
return
// We ARE hunting something, execute the hunt.
// Note that if our AI controller has multiple hunting subtrees set,
// we may accidentally be executing another tree's hunt - not ideal,
// try to set a unique target key if you have multiple
controller.queue_behavior(hunting_behavior, target_key, BB_HUNTING_COOLDOWN(type))
if(finish_planning)
return SUBTREE_RETURN_FINISH_PLANNING //If we're hunting we're too busy for anything else
/datum/ai_planning_subtree/find_and_hunt_target/hunt_ores
target_key = BB_ORE_TARGET
hunting_behavior = /datum/ai_behavior/hunt_target/interact_with_target/hunt_ores
finding_behavior = /datum/ai_behavior/find_hunt_target/hunt_ores
hunt_targets = list(/obj/item/stack/ore)
hunt_chance = 90
hunt_range = 9
@@ -0,0 +1,39 @@
/// Finds a specific atom type to hunt.
/datum/ai_behavior/find_hunt_target
/// is this only meant to search for turf types?
var/search_turf_types = FALSE
/datum/ai_behavior/find_hunt_target/perform(seconds_per_tick, datum/ai_controller/controller, hunting_target_key, types_to_hunt, hunt_range)
var/mob/living/living_mob = controller.pawn
var/list/interesting_objects = search_turf_types ? RANGE_TURFS(hunt_range, living_mob) : oview(hunt_range, living_mob)
for(var/atom/possible_dinner as anything in typecache_filter_list(interesting_objects, types_to_hunt))
if(!valid_dinner(living_mob, possible_dinner, hunt_range, controller, seconds_per_tick))
continue
controller.set_blackboard_key(hunting_target_key, possible_dinner)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
/datum/ai_behavior/find_hunt_target/proc/valid_dinner(mob/living/source, atom/dinner, radius, datum/ai_controller/controller, seconds_per_tick)
if(isliving(dinner))
var/mob/living/living_target = dinner
if(living_target.stat == DEAD)
return FALSE
return can_see(source, dinner, radius)
/datum/ai_behavior/find_hunt_target/hunt_ores
/datum/ai_behavior/find_hunt_target/hunt_ores/valid_dinner(mob/living/basic/source, obj/item/stack/ore/target, radius, datum/ai_controller/controller)
var/list/forbidden_ore = controller.blackboard[BB_ORE_IGNORE_TYPES]
if(is_type_in_list(target, forbidden_ore))
return FALSE
if(!isturf(target.loc))
return FALSE
var/obj/item/pet_target = controller.blackboard[BB_CURRENT_PET_TARGET]
if(target == pet_target) // we are currently fetching this ore for master, dont eat it!
return FALSE
return can_see(source, target, radius)
@@ -0,0 +1,63 @@
/// Hunts down a specific atom type.
/datum/ai_behavior/hunt_target
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
/// How long do we have to wait after a successful hunt?
var/hunt_cooldown = 5 SECONDS
/// Do we reset the target after attacking something, so we can check for status changes.
var/always_reset_target = FALSE
/datum/ai_behavior/hunt_target/setup(datum/ai_controller/controller, hunting_target_key, hunting_cooldown_key)
. = ..()
var/atom/hunt_target = controller.blackboard[hunting_target_key]
if(isnull(hunt_target))
return FALSE
set_movement_target(controller, hunt_target)
/datum/ai_behavior/hunt_target/perform(seconds_per_tick, datum/ai_controller/controller, hunting_target_key, hunting_cooldown_key)
var/mob/living/hunter = controller.pawn
var/atom/hunted = controller.blackboard[hunting_target_key]
if(QDELETED(hunted))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
target_caught(hunter, hunted)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/hunt_target/proc/target_caught(mob/living/hunter, atom/hunted)
if(isliving(hunted)) // Are we hunting a living mob?
var/mob/living/living_target = hunted
hunter.manual_emote("chomps [living_target]!")
living_target.investigate_log("has been killed by [key_name(hunter)].", INVESTIGATE_DEATHS)
add_attack_logs(hunter, hunted, "AI controller killed", ATKLOG_ALL)
living_target.death()
else if(isfood(hunted))
hunted.attack_animal(hunter)
else // We're hunting an object, and should delete it instead of killing it. Mostly useful for decal bugs like ants or spider webs.
hunter.manual_emote("chomps [hunted]!")
qdel(hunted)
/datum/ai_behavior/hunt_target/finish_action(datum/ai_controller/controller, succeeded, hunting_target_key, hunting_cooldown_key)
. = ..()
if(succeeded && hunting_cooldown_key)
controller.set_blackboard_key(hunting_cooldown_key, world.time + hunt_cooldown)
else if(hunting_target_key)
controller.clear_blackboard_key(hunting_target_key)
if(always_reset_target && hunting_target_key)
controller.clear_blackboard_key(hunting_target_key)
/datum/ai_behavior/hunt_target/interact_with_target
/// Which intent should we use to interact with
var/behavior_intent = INTENT_HARM
/datum/ai_behavior/hunt_target/interact_with_target/target_caught(mob/living/hunter, atom/hunted)
var/datum/ai_controller/controller = hunter.ai_controller
controller.ai_interact(target = hunted, intent = behavior_intent)
/datum/ai_behavior/hunt_target/interact_with_target/hunt_ores
always_reset_target = TRUE
/datum/ai_behavior/hunt_target/interact_with_target/consume_ores/minebot
always_reset_target = TRUE
hunt_cooldown = 2 SECONDS
behavior_intent = INTENT_HELP
@@ -1,125 +0,0 @@
/**
* Tells the AI to find a certain target nearby to hunt.
* If a target has been found, we will start to move towards it, and eventually attack it.
*/
/datum/ai_planning_subtree/find_and_hunt_target
/// What key in the blacbkboard do we store our hunting target?
/// If you want to have multiple hunting behaviors on a controller be sure that this is unique
var/target_key = BB_CURRENT_HUNTING_TARGET
/// What behavior to execute if we have no target
var/finding_behavior = /datum/ai_behavior/find_hunt_target
/// What behavior to execute if we do have a target
var/hunting_behavior = /datum/ai_behavior/hunt_target
/// What targets we're hunting for
var/list/hunt_targets
/// In what radius will we hunt
var/hunt_range = 2
/// What are the chances we hunt something at any given moment
var/hunt_chance = 100
///do we finish planning subtree
var/finish_planning = TRUE
/datum/ai_planning_subtree/find_and_hunt_target/New()
. = ..()
hunt_targets = typecacheof(hunt_targets)
/datum/ai_planning_subtree/find_and_hunt_target/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
if(!SPT_PROB(hunt_chance, seconds_per_tick))
return
if(controller.blackboard[BB_HUNTING_COOLDOWN(type)] >= world.time)
return
if(!controller.blackboard_key_exists(target_key))
controller.queue_behavior(finding_behavior, target_key, hunt_targets, hunt_range)
return
// We ARE hunting something, execute the hunt.
// Note that if our AI controller has multiple hunting subtrees set,
// we may accidentally be executing another tree's hunt - not ideal,
// try to set a unique target key if you have multiple
controller.queue_behavior(hunting_behavior, target_key, BB_HUNTING_COOLDOWN(type))
if(finish_planning)
return SUBTREE_RETURN_FINISH_PLANNING //If we're hunting we're too busy for anything else
/// Finds a specific atom type to hunt.
/datum/ai_behavior/find_hunt_target
///is this only meant to search for turf types?
var/search_turf_types = FALSE
/datum/ai_behavior/find_hunt_target/perform(seconds_per_tick, datum/ai_controller/controller, hunting_target_key, types_to_hunt, hunt_range)
var/mob/living/living_mob = controller.pawn
var/list/interesting_objects = search_turf_types ? RANGE_TURFS(hunt_range, living_mob) : oview(hunt_range, living_mob)
for(var/atom/possible_dinner as anything in typecache_filter_list(interesting_objects, types_to_hunt))
if(!valid_dinner(living_mob, possible_dinner, hunt_range, controller, seconds_per_tick))
continue
controller.set_blackboard_key(hunting_target_key, possible_dinner)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
/datum/ai_behavior/find_hunt_target/proc/valid_dinner(mob/living/source, atom/dinner, radius, datum/ai_controller/controller, seconds_per_tick)
if(isliving(dinner))
var/mob/living/living_target = dinner
if(living_target.stat == DEAD)
return FALSE
return can_see(source, dinner, radius)
/datum/ai_behavior/find_hunt_target/search_turf_types
search_turf_types = TRUE
/// Hunts down a specific atom type.
/datum/ai_behavior/hunt_target
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
/// How long do we have to wait after a successful hunt?
var/hunt_cooldown = 5 SECONDS
/// Do we reset the target after attacking something, so we can check for status changes.
var/always_reset_target = FALSE
/datum/ai_behavior/hunt_target/setup(datum/ai_controller/controller, hunting_target_key, hunting_cooldown_key)
. = ..()
var/atom/hunt_target = controller.blackboard[hunting_target_key]
if(isnull(hunt_target))
return FALSE
set_movement_target(controller, hunt_target)
/datum/ai_behavior/hunt_target/perform(seconds_per_tick, datum/ai_controller/controller, hunting_target_key, hunting_cooldown_key)
var/mob/living/hunter = controller.pawn
var/atom/hunted = controller.blackboard[hunting_target_key]
if(QDELETED(hunted))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
target_caught(hunter, hunted)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/hunt_target/proc/target_caught(mob/living/hunter, atom/hunted)
if(isliving(hunted)) // Are we hunting a living mob?
var/mob/living/living_target = hunted
hunter.manual_emote("chomps [living_target]!")
living_target.investigate_log("has been killed by [key_name(hunter)].", INVESTIGATE_DEATHS)
living_target.death()
else if(isfood(hunted))
hunted.attack_animal(hunter)
else // We're hunting an object, and should delete it instead of killing it. Mostly useful for decal bugs like ants or spider webs.
hunter.manual_emote("chomps [hunted]!")
qdel(hunted)
/datum/ai_behavior/hunt_target/finish_action(datum/ai_controller/controller, succeeded, hunting_target_key, hunting_cooldown_key)
. = ..()
if(succeeded && hunting_cooldown_key)
controller.set_blackboard_key(hunting_cooldown_key, world.time + hunt_cooldown)
else if(hunting_target_key)
controller.clear_blackboard_key(hunting_target_key)
if(always_reset_target && hunting_target_key)
controller.clear_blackboard_key(hunting_target_key)
/datum/ai_behavior/hunt_target/interact_with_target
/// Which intent should we use to interact with
var/behavior_intent = INTENT_HARM
/datum/ai_behavior/hunt_target/interact_with_target/target_caught(mob/living/hunter, obj/structure/cable/hunted)
var/datum/ai_controller/controller = hunter.ai_controller
controller.ai_interact(target = hunted, intent = behavior_intent)