mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-26 06:29:23 +01:00
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:
@@ -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
|
||||
Reference in New Issue
Block a user