Files
Cirrial 31d4eb04f1 Fix Stoat Steal Behaviour (and some other AI stuff hopefully) (#94153)
## About The Pull Request

Two main things. 

Multiple instances of use of a proc in AI controllers seemingly assuming
the default behaviour will work for them, but what ends up happening is
`search_tactic` gets redefined and redefined with no defaut search range
parameter, so nothing ends up passed to the `search_tactic` child procs,
so they all call `oview` with `null` and this... somehow doesn't
runtime? Has behaviour that works some of the time??? I hate this
fucking language. Anyway.

Stoat steal items behaviour was completely broken and apparently was not
tested once since it was merged in. I've made the corrections I can, but
I haven't figured out why stoat AI never enters idle, so we have a
behaviour that leads to the stoat running up to an item, grabbing it,
and then just staying there, unmoving. I've sunk too many hours into
this, I'm just going to call it fixed and let someone else figure out
what exciting additions there need to be to a behaviour that was never
functional in the first place.

## Why It's Good For The Game

i don't know man i just want the pain to stop 

okay, generally speaking, when people write AI behaviours, they want
those AI behaviours to do something and not just silently fail for six
months or longer

## Changelog
🆑
fix: Stoats have a chance to try and grab items like they always should
have.
/🆑
2025-11-28 13:35:53 +02:00

364 lines
16 KiB
Plaintext

/datum/ai_controller/basic_controller/minebot
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_PET_TARGETING_STRATEGY = /datum/targeting_strategy/basic/not_friends,
BB_BASIC_MOB_FLEE_DISTANCE = 3,
BB_MINIMUM_SHOOTING_DISTANCE = 3,
BB_MINEBOT_PLANT_MINES = TRUE,
BB_MINEBOT_REPAIR_DRONE = TRUE,
BB_MINEBOT_AUTO_DEFEND = TRUE,
BB_BLACKLIST_MINERAL_TURFS = list(/turf/closed/mineral/gibtonite),
BB_AUTOMATED_MINING = FALSE,
BB_OWNER_SELF_HARM_RESPONSES = list(
"Please stop hurting yourself.",
"There is no need to do that.",
"Your actions are illogical.",
"Please make better choices.",
"Remember, you have beaten your worst days before."
)
)
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/launch_missiles,
/datum/ai_planning_subtree/pet_planning,
/datum/ai_planning_subtree/befriend_miners,
/datum/ai_planning_subtree/defend_node,
/datum/ai_planning_subtree/minebot_maintain_distance,
/datum/ai_planning_subtree/basic_ranged_attack_subtree/minebot,
/datum/ai_planning_subtree/find_and_hunt_target/hunt_ores/minebot,
/datum/ai_planning_subtree/minebot_mining,
/datum/ai_planning_subtree/locate_dead_humans,
)
/datum/ai_planning_subtree/launch_missiles
/datum/ai_planning_subtree/launch_missiles/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/datum/action/cooldown/missile_ability = controller.blackboard[BB_MINEBOT_MISSILE_ABILITY]
if(!missile_ability?.IsAvailable())
return
if(!controller.blackboard_key_exists(BB_MINEBOT_MISSILE_TARGET))
controller.queue_behavior(/datum/ai_behavior/find_and_set/clear_bombing_zone, BB_MINEBOT_MISSILE_TARGET, /obj/effect/temp_visual/minebot_target, 7)
return
controller.queue_behavior(/datum/ai_behavior/targeted_mob_ability/and_clear_target, BB_MINEBOT_MISSILE_ABILITY, BB_MINEBOT_MISSILE_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
/datum/ai_behavior/find_and_set/clear_bombing_zone
/datum/ai_behavior/find_and_set/clear_bombing_zone/search_tactic(datum/ai_controller/controller, locate_path, search_range = SEARCH_TACTIC_DEFAULT_RANGE)
for(var/obj/effect/temp_visual/minebot_target/target in oview(search_range, controller.pawn))
if(isclosedturf(get_turf(target)))
continue
return target
return null
/datum/ai_planning_subtree/befriend_miners/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
if(!controller.blackboard_key_exists(BB_MINER_FRIEND))
controller.queue_behavior(/datum/ai_behavior/find_and_set/miner_to_befriend, BB_MINER_FRIEND)
return
controller.queue_behavior(/datum/ai_behavior/befriend_target, BB_MINER_FRIEND)
/datum/ai_behavior/find_and_set/miner_to_befriend
/datum/ai_behavior/find_and_set/miner_to_befriend/search_tactic(datum/ai_controller/controller, locate_path, search_range = SEARCH_TACTIC_DEFAULT_RANGE)
for(var/mob/living/carbon/human/target in oview(search_range, controller.pawn))
if(HAS_TRAIT(target, TRAIT_ROCK_STONER))
return target
return null
/datum/ai_planning_subtree/defend_node/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/mob/living/target = controller.blackboard[BB_DRONE_DEFEND]
if(QDELETED(target))
controller.queue_behavior(/datum/ai_behavior/find_and_set, BB_DRONE_DEFEND, /mob/living/basic/node_drone)
return
var/mob/living/living_pawn = controller.pawn
if(!living_pawn.faction.Find(REF(target)))
controller.queue_behavior(/datum/ai_behavior/befriend_target, BB_DRONE_DEFEND)
return
if(target.health < (target.maxHealth * 0.75) && controller.blackboard[BB_MINEBOT_REPAIR_DRONE])
controller.queue_behavior(/datum/ai_behavior/repair_drone, BB_DRONE_DEFEND)
return SUBTREE_RETURN_FINISH_PLANNING
/datum/ai_behavior/repair_drone
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION | AI_BEHAVIOR_REQUIRE_REACH
/datum/ai_behavior/repair_drone/setup(datum/ai_controller/controller, target_key)
. = ..()
var/turf/target = controller.blackboard[target_key]
if(isnull(target))
return FALSE
set_movement_target(controller, target)
/datum/ai_behavior/repair_drone/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
var/atom/target = controller.blackboard[target_key]
if(QDELETED(target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/mob/living/living_pawn = controller.pawn
living_pawn.say("REPAIRING [target]!")
controller.ai_interact(target = target)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/repair_drone/finish_action(datum/ai_controller/controller, success, target_key)
. = ..()
if(!success)
controller.clear_blackboard_key(target_key)
///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)
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 = SEARCH_TACTIC_DEFAULT_RANGE)
for(var/mob/living/carbon/human/target in oview(search_range, controller.pawn))
if(target.stat >= UNCONSCIOUS && target.mind)
return target
return null
/datum/ai_behavior/send_sos_message
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
action_cooldown = 2 MINUTES
/datum/ai_behavior/send_sos_message/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
var/mob/living/carbon/target = controller.blackboard[target_key]
var/mob/living/living_pawn = controller.pawn
if(QDELETED(target) || is_station_level(target.z))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/turf/target_turf = get_turf(target)
var/obj/item/implant/radio/radio_implant = locate(/obj/item/implant/radio) in living_pawn.contents
if(!radio_implant)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/message = "ALERT, [target] in need of help at coordinates: [target_turf.x], [target_turf.y], [target_turf.z]!"
radio_implant.radio.talk_into(living_pawn, message, RADIO_CHANNEL_SUPPLY)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/send_sos_message/finish_action(datum/ai_controller/controller, success, target_key)
. = ..()
controller.clear_blackboard_key(target_key)
///operational datums is null because we dont use a ranged component, we use a gun in our contents
/datum/ai_planning_subtree/basic_ranged_attack_subtree/minebot
operational_datums = null
ranged_attack_behavior = /datum/ai_behavior/basic_ranged_attack/minebot
/datum/ai_planning_subtree/basic_ranged_attack_subtree/minebot/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
if(QDELETED(target))
return
var/mob/living/living_pawn = controller.pawn
if(!living_pawn.combat_mode) //we are not on attack mode
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
/datum/ai_planning_subtree/minebot_maintain_distance/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
if(QDELETED(target))
return
var/mob/living/living_pawn = controller.pawn
if(get_dist(living_pawn, target) <= controller.blackboard[BB_MINIMUM_SHOOTING_DISTANCE])
controller.queue_behavior(/datum/ai_behavior/run_away_from_target/run_and_shoot/minebot, BB_BASIC_MOB_CURRENT_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
/datum/ai_behavior/run_away_from_target/run_and_shoot/minebot
/datum/ai_behavior/run_away_from_target/run_and_shoot/minebot/perform(seconds_per_tick, datum/ai_controller/controller, target_key, hiding_location_key)
if(!controller.blackboard[BB_MINEBOT_PLANT_MINES])
return ..()
var/datum/action/cooldown/mine_ability = controller.blackboard[BB_MINEBOT_LANDMINE_ABILITY]
mine_ability?.Trigger()
return ..()
/datum/ai_behavior/basic_ranged_attack/minebot
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
avoid_friendly_fire = TRUE
///if our target is closer than this distance, finish action
var/minimum_distance = 3
/datum/ai_behavior/basic_ranged_attack/minebot/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
. = ..()
minimum_distance = controller.blackboard[BB_MINIMUM_SHOOTING_DISTANCE] ? controller.blackboard[BB_MINIMUM_SHOOTING_DISTANCE] : initial(minimum_distance)
var/atom/target = controller.blackboard[target_key]
if(QDELETED(target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/mob/living/living_pawn = controller.pawn
if(get_dist(living_pawn, target) <= minimum_distance)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
///mine walls if we are on automated mining mode
/datum/ai_planning_subtree/minebot_mining/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
if(!controller.blackboard[BB_AUTOMATED_MINING])
return
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
/datum/ai_behavior/find_mineral_wall/minebot/check_if_mineable(datum/ai_controller/controller, turf/target_wall)
var/list/forbidden_turfs = controller.blackboard[BB_BLACKLIST_MINERAL_TURFS]
var/turf/previous_unreachable_wall = controller.blackboard[BB_PREVIOUS_UNREACHABLE_WALL]
if(is_type_in_list(target_wall, forbidden_turfs) || target_wall == previous_unreachable_wall)
return FALSE
controller.clear_blackboard_key(BB_PREVIOUS_UNREACHABLE_WALL)
return ..()
/datum/ai_behavior/minebot_mine_turf
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
required_distance = 2
action_cooldown = 3 SECONDS
/datum/ai_behavior/minebot_mine_turf/setup(datum/ai_controller/controller, target_key)
. = ..()
var/turf/target = controller.blackboard[target_key]
if(isnull(target))
return FALSE
set_movement_target(controller, target)
/datum/ai_behavior/minebot_mine_turf/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
var/mob/living/basic/living_pawn = controller.pawn
var/turf/target = controller.blackboard[target_key]
if(QDELETED(target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
if(check_obstacles_in_path(controller, target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
if(!living_pawn.combat_mode)
living_pawn.set_combat_mode(TRUE)
living_pawn.RangedAttack(target)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/minebot_mine_turf/proc/check_obstacles_in_path(datum/ai_controller/controller, turf/target)
var/mob/living/source = controller.pawn
var/list/turfs_in_path = get_line(source, target) - target
for(var/turf/turf in turfs_in_path)
if(turf.is_blocked_turf(ignore_atoms = list(source)))
controller.set_blackboard_key(BB_PREVIOUS_UNREACHABLE_WALL, target)
return TRUE
return FALSE
/datum/ai_behavior/minebot_mine_turf/finish_action(datum/ai_controller/controller, success, target_key)
. = ..()
controller.clear_blackboard_key(target_key)
///store ores in our body
/datum/ai_planning_subtree/find_and_hunt_target/hunt_ores/minebot
hunting_behavior = /datum/ai_behavior/hunt_target/interact_with_target/consume_ores/minebot
hunt_chance = 100
/datum/ai_planning_subtree/find_and_hunt_target/hunt_ores/minebot/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/automated_mining = controller.blackboard[BB_AUTOMATED_MINING]
var/mob/living/living_pawn = controller.pawn
if(!automated_mining && living_pawn.combat_mode) //are we not on automated mining or collect mode?
return
return ..()
/datum/ai_behavior/hunt_target/interact_with_target/consume_ores/minebot
hunt_cooldown = 2 SECONDS
/datum/ai_behavior/hunt_target/interact_with_target/consume_ores/minebot/target_caught(mob/living/hunter, obj/item/stack/ore/hunted)
if(hunter.combat_mode)
hunter.set_combat_mode(FALSE)
return ..()
///pet commands
/datum/pet_command/free/minebot
/datum/pet_command/free/minebot/execute_action(datum/ai_controller/controller)
controller.set_blackboard_key(BB_AUTOMATED_MINING, FALSE)
return ..()
/datum/pet_command/automate_mining
command_name = "Automate mining"
command_desc = "Make your minebot automatically mine!"
radial_icon_state = "mine"
speech_commands = list("mine")
callout_type = /datum/callout_option/mine
/datum/pet_command/automate_mining/valid_callout_target(mob/living/speaker, datum/callout_option/callout, atom/target)
return ismineralturf(target)
/datum/pet_command/automate_mining/retrieve_command_text(atom/living_pet, atom/target)
return "signals [living_pet] to start mining!"
/datum/pet_command/automate_mining/execute_action(datum/ai_controller/controller)
controller.set_blackboard_key(BB_AUTOMATED_MINING, TRUE)
controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
/datum/pet_command/minebot_ability
command_name = "Minebot ability"
command_desc = "Make your minebot use one of its abilities."
radial_icon = 'icons/mob/actions/actions_mecha.dmi'
///the ability we will use
var/ability_key
/datum/pet_command/minebot_ability/execute_action(datum/ai_controller/controller)
var/datum/action/cooldown/ability = controller.blackboard[ability_key]
if(!ability?.IsAvailable())
return
controller.queue_behavior(/datum/ai_behavior/use_mob_ability, ability_key)
controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
return SUBTREE_RETURN_FINISH_PLANNING
/datum/pet_command/minebot_ability/light
command_name = "Toggle lights"
command_desc = "Make your minebot toggle its lights."
speech_commands = list("light")
radial_icon_state = "mech_lights_off"
ability_key = BB_MINEBOT_LIGHT_ABILITY
/datum/pet_command/minebot_ability/light/retrieve_command_text(atom/living_pet, atom/target)
return "signals [living_pet] to toggle its lights!"
/datum/pet_command/minebot_ability/dump
command_name = "Dump ore"
command_desc = "Make your minebot dump all its ore!"
speech_commands = list("dump", "ore")
radial_icon_state = "mech_eject"
ability_key = BB_MINEBOT_DUMP_ABILITY
/datum/pet_command/minebot_ability/dump/retrieve_command_text(atom/living_pet, atom/target)
return "signals [living_pet] to dump its ore!"
/datum/pet_command/attack/minebot
attack_behaviour = /datum/ai_behavior/basic_ranged_attack/minebot
/datum/pet_command/attack/minebot/execute_action(datum/ai_controller/controller)
controller.set_blackboard_key(BB_AUTOMATED_MINING, FALSE)
var/mob/living/living_pawn = controller.pawn
if(!living_pawn.combat_mode)
living_pawn.set_combat_mode(TRUE)
return ..()
/datum/pet_command/idle/minebot
/datum/pet_command/idle/minebot/execute_action(datum/ai_controller/controller)
controller.set_blackboard_key(BB_AUTOMATED_MINING, FALSE)
return ..()
/datum/pet_command/protect_owner/minebot
/datum/pet_command/protect_owner/minebot/set_command_target(mob/living/parent, atom/target)
if(!parent.ai_controller.blackboard[BB_MINEBOT_AUTO_DEFEND])
return FALSE
if(!parent.ai_controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET) && !QDELETED(target)) //we are already dealing with something,
parent.ai_controller.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, target)
return TRUE
/datum/pet_command/protect_owner/minebot/execute_action(datum/ai_controller/controller)
if(controller.blackboard[BB_MINEBOT_AUTO_DEFEND])
var/mob/living/living_pawn = controller.pawn
living_pawn.set_combat_mode(TRUE)
controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)