Basic Mobs: the cooler simple mobs that run on datum AI. (With reworked cockroach AI as proof of concept) (#60694)

Simple_animals / mobs are the biggest lie in this code-base. They're far from simple and have an extreme god-object problem. Especially when you get to /hostile, where there is so many procs, vars, and what not, that you can't make any interesting additions without snowflaking the hell out of the code.

This PR hopes to help kill this problem by introducing a new /living subtype, /living/basic. The idea of this refactor is to slowly start moving all old simple_animals to this new system, moving over behaviors like charging and more extravagant mobs like megafauna over bit by bit similar to how newfood was implemented.

One of the other big goals of this refactor is to move many of the fringe simple animal behaviors into either AI datums, or components/elements. (Some of which still needs to be done in this PR).

As a proof of concept, I created the base mob/living/basic, and moved cockroaches over to the system. Since cockroaches have both a passive, melee and ranged mob.

This PR does slightly affect balance as the behavior isn't 1-on-1 due to it no longer running on the janky /hostile behavior, but I tried to keep the effects to a minimum, and the glockroach and hauberoach are not spawnable through many means as far as I know.
This commit is contained in:
AMonkeyThatCodes
2021-08-30 16:22:24 +01:00
committed by GitHub
parent efe959b16d
commit 46cb925af0
60 changed files with 1389 additions and 201 deletions
@@ -0,0 +1,26 @@
/datum/ai_controller/basic_controller
movement_delay = 0.4 SECONDS
/datum/ai_controller/basic_controller/TryPossessPawn(atom/new_pawn)
if(!isbasicmob(new_pawn))
return AI_CONTROLLER_INCOMPATIBLE
var/mob/living/basic/basic_mob = new_pawn
update_speed(basic_mob)
RegisterSignal(basic_mob, POST_BASIC_MOB_UPDATE_VARSPEED, .proc/update_speed)
return ..() //Run parent at end
/datum/ai_controller/basic_controller/able_to_run()
. = ..()
if(isliving(pawn))
var/mob/living/living_pawn = pawn
if(IS_DEAD_OR_INCAP(living_pawn))
return FALSE
return TRUE
/datum/ai_controller/basic_controller/proc/update_speed(mob/living/basic/basic_mob)
SIGNAL_HANDLER
movement_delay = basic_mob.cached_multiplicative_slowdown
@@ -0,0 +1,67 @@
/datum/ai_behavior/basic_melee_attack
action_cooldown = 0.6 SECONDS
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
/datum/ai_behavior/basic_melee_attack/setup(datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
. = ..()
controller.current_movement_target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key] //Hiding location is priority
/datum/ai_behavior/basic_melee_attack/perform(delta_time, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
. = ..()
var/mob/living/basic/basic_mob = controller.pawn
var/atom/target = controller.blackboard[target_key]
var/datum/targetting_datum/targetting_datum = controller.blackboard[targetting_datum_key]
if(!targetting_datum.can_attack(basic_mob, target))
finish_action(controller, FALSE, target_key)
return
var/hiding_target = targetting_datum.find_hidden_mobs(basic_mob, target) //If this is valid, theyre hidden in something!
controller.blackboard[hiding_location_key] = hiding_target
if(hiding_target) //Slap it!
basic_mob.melee_attack(hiding_target)
else
basic_mob.melee_attack(target)
/datum/ai_behavior/basic_melee_attack/finish_action(datum/ai_controller/controller, succeeded, target_key, targetting_datum_key, hiding_location_key)
. = ..()
if(!succeeded)
controller.blackboard -= target_key
/datum/ai_behavior/basic_ranged_attack
action_cooldown = 0.6 SECONDS
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM
required_distance = 3
/datum/ai_behavior/basic_ranged_attack/setup(datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
. = ..()
controller.current_movement_target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key] //Hiding location is priority
/datum/ai_behavior/basic_ranged_attack/perform(delta_time, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
. = ..()
var/mob/living/basic/basic_mob = controller.pawn
var/atom/target = controller.blackboard[target_key]
var/datum/targetting_datum/targetting_datum = controller.blackboard[targetting_datum_key]
if(!targetting_datum.can_attack(basic_mob, target))
finish_action(controller, FALSE, target_key)
return
var/hiding_target = targetting_datum.find_hidden_mobs(basic_mob, target) //If this is valid, theyre hidden in something!
controller.blackboard[hiding_location_key] = hiding_target
if(hiding_target) //Shoot it!
basic_mob.RangedAttack(hiding_target)
else
basic_mob.RangedAttack(target)
/datum/ai_behavior/basic_ranged_attack/finish_action(datum/ai_controller/controller, succeeded, target_key, targetting_datum_key, hiding_location_key)
. = ..()
if(!succeeded)
controller.blackboard -= target_key
@@ -0,0 +1,45 @@
/datum/ai_behavior/find_potential_targets
action_cooldown = 2 SECONDS
var/vision_range = 9
///List of potentially dangerous objs
var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/vehicle/sealed/mecha))
/datum/ai_behavior/find_potential_targets/perform(delta_time, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
. = ..()
var/list/potential_targets
var/mob/living/living_mob = controller.pawn
var/datum/targetting_datum/targetting_datum = controller.blackboard[targetting_datum_key]
if(!targetting_datum)
CRASH("No target datum was supplied in the blackboard for [controller.pawn]")
potential_targets = hearers(vision_range, controller.pawn) - living_mob //Remove self, so we don't suicide
for(var/HM in typecache_filter_list(range(vision_range, living_mob), hostile_machines)) //Can we see any hostile machines?
if(can_see(living_mob, HM, vision_range))
potential_targets += HM
if(!potential_targets.len)
finish_action(controller, FALSE)
return
var/list/filtered_targets = list()
for(var/atom/pot_target in potential_targets)
if(targetting_datum.can_attack(living_mob, pot_target))//Can we attack it?
filtered_targets += pot_target
continue
if(!filtered_targets.len)
finish_action(controller, FALSE)
return
var/atom/target = pick(filtered_targets)
controller.blackboard[target_key] = target
var/atom/potential_hiding_location = targetting_datum.find_hidden_mobs(living_mob, target)
if(potential_hiding_location) //If they're hiding inside of something, we need to know so we can go for that instead initially.
controller.blackboard[hiding_location_key] = potential_hiding_location
finish_action(controller, TRUE)
@@ -0,0 +1,25 @@
/datum/ai_planning_subtree/basic_melee_attack_subtree
var/datum/ai_behavior/basic_melee_attack/melee_attack_behavior = /datum/ai_behavior/basic_melee_attack
/datum/ai_planning_subtree/basic_melee_attack_subtree/SelectBehaviors(datum/ai_controller/controller, delta_time)
. = ..()
var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
if(!target || QDELETED(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 you give this to something without the element you are a dumbass.
/datum/ai_planning_subtree/basic_ranged_attack_subtree
var/datum/ai_behavior/basic_ranged_attack/ranged_attack_behavior = /datum/ai_behavior/basic_ranged_attack
/datum/ai_planning_subtree/basic_ranged_attack_subtree/SelectBehaviors(datum/ai_controller/controller, delta_time)
. = ..()
var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
if(!target || QDELETED(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.
@@ -0,0 +1,8 @@
/datum/ai_planning_subtree/simple_find_target
/datum/ai_planning_subtree/simple_find_target/SelectBehaviors(datum/ai_controller/controller, delta_time)
. = ..()
var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
if(target && !QDELETED(target))
return
controller.queue_behavior(/datum/ai_behavior/find_potential_targets, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
@@ -0,0 +1,39 @@
/datum/ai_planning_subtree/random_speech
//The chance of an emote occuring each second
var/speech_chance = 0
///Hearable emotes
var/list/emote_hear = list()
///Unlike speak_emote, the list of things in this variable only show by themselves with no spoken text. IE: Ian barks, Ian yaps
var/list/emote_see = list()
///Possible lines of speech the AI can have
var/list/speak = list()
/datum/ai_planning_subtree/random_speech/New()
. = ..()
if(speak)
speak = string_list(speak)
if(emote_hear)
emote_hear = string_list(emote_hear)
if(emote_see)
emote_see = string_list(emote_see)
/datum/ai_planning_subtree/random_speech/SelectBehaviors(datum/ai_controller/controller, delta_time)
if(DT_PROB(speech_chance, delta_time))
var/audible_emotes_length = emote_hear?.len
var/non_audible_emotes_length = emote_see?.len
var/speak_lines_length = speak?.len
var/total_choices_length = audible_emotes_length + non_audible_emotes_length + speak_lines_length
var/random_number_in_range = rand(1, total_choices_length)
if(random_number_in_range <= audible_emotes_length)
controller.queue_behavior(/datum/ai_behavior/perform_emote, pick(emote_hear))
else if(random_number_in_range <= (audible_emotes_length + non_audible_emotes_length))
controller.queue_behavior(/datum/ai_behavior/perform_emote, pick(emote_see))
else
controller.queue_behavior(/datum/ai_behavior/perform_speech, pick(speak))
/datum/ai_planning_subtree/random_speech/cockroach
speech_chance = 5
emote_hear = list("chitters")
@@ -0,0 +1,55 @@
///Datum for basic mobs to define what they can attack.
/datum/targetting_datum
///Returns true or false depending on if the target can be attacked by the mob
/datum/targetting_datum/proc/can_attack(mob/living/living_mob, atom/target)
return
///Returns something the target might be hiding inside of
/datum/targetting_datum/proc/find_hidden_mobs(mob/living/living_mob, atom/target)
var/atom/target_hiding_location
if(istype(target.loc, /obj/structure/closet) || istype(target.loc, /obj/machinery/disposal) || istype(target.loc, /obj/machinery/sleeper))
target_hiding_location = target.loc
return target_hiding_location
/datum/targetting_datum/basic
/datum/targetting_datum/basic/can_attack(mob/living/living_mob, atom/the_target)
if(isturf(the_target) || !the_target) // bail out on invalids
return FALSE
if(ismob(the_target)) //Target is in godmode, ignore it.
var/mob/M = the_target
if(M.status_flags & GODMODE)
return FALSE
if(living_mob.see_invisible < the_target.invisibility)//Target's invisible to us, forget it
return FALSE
if(living_mob.z != the_target.z)
return FALSE
if(isliving(the_target)) //Targetting vs living mobs
var/mob/living/L = the_target
var/faction_check = living_mob.faction_check_mob(L)
if(faction_check || L.stat)
return FALSE
return TRUE
if(ismecha(the_target)) //Targetting vs mechas
var/obj/vehicle/sealed/mecha/M = the_target
for(var/occupant in M.occupants)
if(can_attack(living_mob, occupant)) //Can we attack any of the occupants?
return TRUE
if(istype(the_target, /obj/machinery/porta_turret)) //Cringe turret! kill it!
var/obj/machinery/porta_turret/P = the_target
if(P.in_faction(living_mob)) //Don't attack if the turret is in the same faction
return FALSE
if(P.has_cover && !P.raised) //Don't attack invincible turrets
return FALSE
if(P.machine_stat & BROKEN) //Or turrets that are already broken
return FALSE
return TRUE
return FALSE