mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-13 03:33:21 +00:00
Refactor AI into two subsystems and clean up targeting some
This commit is contained in:
@@ -74,6 +74,7 @@ var/global/list/runlevel_flags = list(RUNLEVEL_LOBBY, RUNLEVEL_SETUP, RUNLEVEL_G
|
||||
#define INIT_ORDER_XENOARCH -20
|
||||
#define INIT_ORDER_CIRCUIT -21
|
||||
#define INIT_ORDER_AI -22
|
||||
#define INIT_ORDER_AI_FAST -23
|
||||
#define INIT_ORDER_GAME_MASTER -24
|
||||
#define INIT_ORDER_TICKER -50
|
||||
#define INIT_ORDER_CHAT -100 //Should be last to ensure chat remains smooth during init.
|
||||
|
||||
@@ -2,7 +2,7 @@ SUBSYSTEM_DEF(ai)
|
||||
name = "AI"
|
||||
init_order = INIT_ORDER_AI
|
||||
priority = FIRE_PRIORITY_AI
|
||||
wait = 5 // This gets run twice a second, however this is technically two loops in one, with the second loop being run every four iterations.
|
||||
wait = 2 SECONDS
|
||||
flags = SS_NO_INIT|SS_TICKER
|
||||
runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
|
||||
|
||||
@@ -22,15 +22,11 @@ SUBSYSTEM_DEF(ai)
|
||||
var/list/currentrun = src.currentrun
|
||||
|
||||
while(currentrun.len)
|
||||
// var/mob/living/L = currentrun[currentrun.len]
|
||||
var/datum/ai_holder/A = currentrun[currentrun.len]
|
||||
--currentrun.len
|
||||
if(!A || QDELETED(A) || A.busy) // Doesn't exist or won't exist soon or not doing it this tick
|
||||
continue
|
||||
if(times_fired % 4 == 0 && A.holder.stat != DEAD)
|
||||
A.handle_strategicals()
|
||||
if(A.holder.stat != DEAD) // The /TG/ version checks stat twice, presumably in-case processing somehow got the mob killed in that instant.
|
||||
A.handle_tactics()
|
||||
|
||||
if(MC_TICK_CHECK)
|
||||
return
|
||||
|
||||
32
code/controllers/subsystems/aifast.dm
Normal file
32
code/controllers/subsystems/aifast.dm
Normal file
@@ -0,0 +1,32 @@
|
||||
SUBSYSTEM_DEF(aifast)
|
||||
name = "AI (Fast)"
|
||||
init_order = INIT_ORDER_AI_FAST
|
||||
priority = FIRE_PRIORITY_AI
|
||||
wait = 5 // This gets run twice a second, but shouldn't do much unless mobs are in combat
|
||||
flags = SS_NO_INIT|SS_TICKER
|
||||
runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
|
||||
|
||||
var/list/processing = list()
|
||||
var/list/currentrun = list()
|
||||
|
||||
/datum/controller/subsystem/aifast/stat_entry(msg_prefix)
|
||||
var/list/msg = list(msg_prefix)
|
||||
msg += "P:[processing.len]"
|
||||
..(msg.Join())
|
||||
|
||||
/datum/controller/subsystem/aifast/fire(resumed = 0)
|
||||
if (!resumed)
|
||||
src.currentrun = processing.Copy()
|
||||
|
||||
//cache for sanic speed (lists are references anyways)
|
||||
var/list/currentrun = src.currentrun
|
||||
|
||||
while(currentrun.len)
|
||||
var/datum/ai_holder/A = currentrun[currentrun.len]
|
||||
--currentrun.len
|
||||
if(!A || QDELETED(A) || A.busy) // Doesn't exist or won't exist soon or not doing it this tick
|
||||
continue
|
||||
A.handle_tactics()
|
||||
|
||||
if(MC_TICK_CHECK)
|
||||
return
|
||||
@@ -1,6 +1,14 @@
|
||||
// This is a datum-based artificial intelligence for simple mobs (and possibly others) to use.
|
||||
// The neat thing with having this here instead of on the mob is that it is independant of Life(), and that different mobs
|
||||
// can use a more or less complex AI by giving it a different datum.
|
||||
#define AI_NO_PROCESS 0
|
||||
#define AI_PROCESSING (1<<0)
|
||||
#define AI_FASTPROCESSING (1<<1)
|
||||
|
||||
#define START_AIPROCESSING(Datum) if (!(Datum.process_flags & AI_PROCESSING)) {Datum.process_flags |= AI_PROCESSING;SSai.processing += Datum}
|
||||
#define STOP_AIPROCESSING(Datum) Datum.process_flags &= ~AI_PROCESSING;SSai.processing -= Datum
|
||||
#define START_AIFASTPROCESSING(Datum) if (!(Datum.process_flags & AI_FASTPROCESSING)) {Datum.process_flags |= AI_FASTPROCESSING;SSaifast.processing += Datum}
|
||||
#define STOP_AIFASTPROCESSING(Datum) Datum.process_flags &= ~AI_FASTPROCESSING;SSaifast.processing -= Datum
|
||||
|
||||
/mob/living
|
||||
var/datum/ai_holder/ai_holder = null
|
||||
@@ -27,7 +35,20 @@
|
||||
var/busy = FALSE // If true, the ticker will skip processing this mob until this is false. Good for if you need the
|
||||
// mob to stay still (e.g. delayed attacking). If you need the mob to be inactive for an extended period of time,
|
||||
// consider sleeping the AI instead.
|
||||
|
||||
var/process_flags = 0 // Where we're processing, see flag defines.
|
||||
var/list/static/fastprocess_stances = list(
|
||||
STANCE_ALERT,
|
||||
STANCE_APPROACH,
|
||||
STANCE_FIGHT,
|
||||
STANCE_BLINDFIGHT,
|
||||
STANCE_REPOSITION,
|
||||
STANCE_MOVE,
|
||||
STANCE_FOLLOW,
|
||||
STANCE_FLEE
|
||||
)
|
||||
var/list/static/noprocess_stances = list(
|
||||
STANCE_SLEEP
|
||||
)
|
||||
|
||||
|
||||
/datum/ai_holder/hostile
|
||||
@@ -40,16 +61,34 @@
|
||||
/datum/ai_holder/New(var/new_holder)
|
||||
ASSERT(new_holder)
|
||||
holder = new_holder
|
||||
SSai.processing += src
|
||||
home_turf = get_turf(holder)
|
||||
manage_processing(AI_PROCESSING)
|
||||
GLOB.stat_set_event.register(holder, src, .proc/holder_stat_change)
|
||||
..()
|
||||
|
||||
/datum/ai_holder/Destroy()
|
||||
holder = null
|
||||
SSai.processing -= src // We might've already been asleep and removed, but byond won't care if we do this again and it saves a conditional.
|
||||
manage_processing(AI_NO_PROCESS)
|
||||
home_turf = null
|
||||
return ..()
|
||||
|
||||
/datum/ai_holder/proc/manage_processing(var/desired)
|
||||
if(desired & AI_PROCESSING)
|
||||
START_AIPROCESSING(src)
|
||||
else
|
||||
STOP_AIPROCESSING(src)
|
||||
|
||||
if(desired & AI_FASTPROCESSING)
|
||||
START_AIFASTPROCESSING(src)
|
||||
else
|
||||
STOP_AIFASTPROCESSING(src)
|
||||
|
||||
/datum/ai_holder/proc/holder_stat_change(var/mob, old_stat, new_stat)
|
||||
if(old_stat >= DEAD && new_stat <= DEAD) //Revived
|
||||
manage_processing(AI_PROCESSING)
|
||||
else if(old_stat <= DEAD && new_stat >= DEAD) //Killed
|
||||
manage_processing(AI_NO_PROCESS)
|
||||
|
||||
/datum/ai_holder/proc/update_stance_hud()
|
||||
var/image/stanceimage = holder.grab_hud(LIFE_HUD)
|
||||
stanceimage.icon_state = "ais_[stance]"
|
||||
@@ -78,7 +117,6 @@
|
||||
return
|
||||
forget_everything() // If we ever wake up, its really unlikely that our current memory will be of use.
|
||||
set_stance(STANCE_SLEEP)
|
||||
SSai.processing -= src
|
||||
update_paused_hud()
|
||||
|
||||
// Reverses the above proc.
|
||||
@@ -89,7 +127,6 @@
|
||||
if(!should_wake())
|
||||
return
|
||||
set_stance(STANCE_IDLE)
|
||||
SSai.processing += src
|
||||
update_paused_hud()
|
||||
|
||||
/datum/ai_holder/proc/should_wake()
|
||||
@@ -122,12 +159,23 @@
|
||||
|
||||
// For setting the stance WITHOUT processing it
|
||||
/datum/ai_holder/proc/set_stance(var/new_stance)
|
||||
if(stance == new_stance)
|
||||
ai_log("set_stance() : Ignoring change stance to same stance request.", AI_LOG_INFO)
|
||||
return
|
||||
|
||||
ai_log("set_stance() : Setting stance from [stance] to [new_stance].", AI_LOG_INFO)
|
||||
stance = new_stance
|
||||
if(stance_coloring) // For debugging or really weird mobs.
|
||||
stance_color()
|
||||
update_stance_hud()
|
||||
|
||||
if(new_stance in fastprocess_stances) //Becoming fast
|
||||
manage_processing(AI_PROCESSING|AI_FASTPROCESSING)
|
||||
else if(new_stance in noprocess_stances)
|
||||
manage_processing(AI_NO_PROCESS) //Becoming off
|
||||
else
|
||||
manage_processing(AI_PROCESSING) //Becoming slow
|
||||
|
||||
// This is called every half a second.
|
||||
/datum/ai_holder/proc/handle_stance_tactical()
|
||||
ai_log("========= Fast Process Beginning ==========", AI_LOG_TRACE) // This is to make it easier visually to disinguish between 'blocks' of what a tick did.
|
||||
@@ -167,19 +215,6 @@
|
||||
return
|
||||
|
||||
switch(stance)
|
||||
if(STANCE_IDLE)
|
||||
if(should_go_home())
|
||||
ai_log("handle_stance_tactical() : STANCE_IDLE, going to go home.", AI_LOG_TRACE)
|
||||
go_home()
|
||||
|
||||
else if(should_follow_leader())
|
||||
ai_log("handle_stance_tactical() : STANCE_IDLE, going to follow leader.", AI_LOG_TRACE)
|
||||
set_stance(STANCE_FOLLOW)
|
||||
|
||||
else if(should_wander())
|
||||
ai_log("handle_stance_tactical() : STANCE_IDLE, going to wander randomly.", AI_LOG_TRACE)
|
||||
handle_wander_movement()
|
||||
|
||||
if(STANCE_ALERT)
|
||||
ai_log("handle_stance_tactical() : STANCE_ALERT, going to threaten_target().", AI_LOG_TRACE)
|
||||
threaten_target()
|
||||
@@ -241,9 +276,23 @@
|
||||
if(STANCE_IDLE)
|
||||
if(speak_chance) // In the long loop since otherwise it wont shut up.
|
||||
handle_idle_speaking()
|
||||
|
||||
if(hostile)
|
||||
ai_log("handle_stance_strategical() : STANCE_IDLE, going to find_target().", AI_LOG_TRACE)
|
||||
find_target()
|
||||
|
||||
if(should_go_home())
|
||||
ai_log("handle_stance_tactical() : STANCE_IDLE, going to go home.", AI_LOG_TRACE)
|
||||
go_home()
|
||||
|
||||
else if(should_follow_leader())
|
||||
ai_log("handle_stance_tactical() : STANCE_IDLE, going to follow leader.", AI_LOG_TRACE)
|
||||
set_stance(STANCE_FOLLOW)
|
||||
|
||||
else if(should_wander())
|
||||
ai_log("handle_stance_tactical() : STANCE_IDLE, going to wander randomly.", AI_LOG_TRACE)
|
||||
handle_wander_movement()
|
||||
|
||||
if(STANCE_APPROACH)
|
||||
if(target)
|
||||
ai_log("handle_stance_strategical() : STANCE_APPROACH, going to calculate_path([target]).", AI_LOG_TRACE)
|
||||
@@ -292,3 +341,6 @@
|
||||
/mob/living/proc/taunt(atom/movable/taunter, force_target_switch = FALSE)
|
||||
if(ai_holder)
|
||||
ai_holder.receive_taunt(taunter, force_target_switch)
|
||||
|
||||
#undef AI_PROCESSING
|
||||
#undef AI_FASTPROCESSING
|
||||
@@ -6,7 +6,7 @@
|
||||
// If our holder is able to do anything.
|
||||
/datum/ai_holder/proc/can_act()
|
||||
if(!holder) // Holder missing.
|
||||
SSai.processing -= src
|
||||
manage_processing(AI_NO_PROCESS)
|
||||
return FALSE
|
||||
if(holder.stat) // Dead or unconscious.
|
||||
ai_log("can_act() : Stat was non-zero ([holder.stat]).", AI_LOG_TRACE)
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
ai_log("lose_follow() : Exited.", AI_LOG_DEBUG)
|
||||
|
||||
/datum/ai_holder/proc/should_follow_leader()
|
||||
if(!leader)
|
||||
if(!leader || target)
|
||||
return FALSE
|
||||
if(follow_until_time && world.time > follow_until_time)
|
||||
lose_follow()
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
ai_log("walk_to_destination() : Exiting.",AI_LOG_TRACE)
|
||||
|
||||
/datum/ai_holder/proc/should_go_home()
|
||||
if(stance != STANCE_IDLE)
|
||||
return FALSE
|
||||
if(!returns_home || !home_turf)
|
||||
return FALSE
|
||||
if(get_dist(holder, home_turf) > max_home_distance)
|
||||
@@ -139,7 +141,7 @@
|
||||
return MOVEMENT_ON_COOLDOWN
|
||||
|
||||
/datum/ai_holder/proc/should_wander()
|
||||
return wander && !leader
|
||||
return (stance == STANCE_IDLE) && wander && !leader
|
||||
|
||||
// Wanders randomly in cardinal directions.
|
||||
/datum/ai_holder/proc/handle_wander_movement()
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
|
||||
// Step 1, find out what we can see.
|
||||
/datum/ai_holder/proc/list_targets()
|
||||
. = hearers(vision_range, holder) - holder // Remove ourselves to prevent suicidal decisions. ~ SRC is the ai_holder.
|
||||
. -= dview_mob // Not the dview mob either, nerd.
|
||||
. = ohearers(vision_range, holder)
|
||||
. -= dview_mob // Not the dview mob!
|
||||
|
||||
var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha, /obj/structure/blob))
|
||||
|
||||
@@ -43,13 +43,8 @@
|
||||
if(!has_targets_list)
|
||||
possible_targets = list_targets()
|
||||
for(var/possible_target in possible_targets)
|
||||
var/atom/A = possible_target
|
||||
if(found(A)) // In case people want to override this.
|
||||
. = list(A)
|
||||
break
|
||||
if(can_attack(A)) // Can we attack it?
|
||||
. += A
|
||||
continue
|
||||
if(can_attack(possible_target)) // Can we attack it?
|
||||
. += possible_target
|
||||
|
||||
var/new_target = pick_target(.)
|
||||
give_target(new_target)
|
||||
@@ -57,7 +52,7 @@
|
||||
|
||||
// Step 3, pick among the possible, attackable targets.
|
||||
/datum/ai_holder/proc/pick_target(list/targets)
|
||||
if(target != null) // If we already have a target, but are told to pick again, calculate the lowest distance between all possible, and pick from the lowest distance targets.
|
||||
if(target) // If we already have a target, but are told to pick again, calculate the lowest distance between all possible, and pick from the lowest distance targets.
|
||||
targets = target_filter_distance(targets)
|
||||
else
|
||||
targets = target_filter_closest(targets)
|
||||
@@ -88,32 +83,31 @@
|
||||
|
||||
// Filters return one or more 'preferred' targets.
|
||||
|
||||
// This one is for closest targets.
|
||||
// This one is for targets closer than our current one.
|
||||
/datum/ai_holder/proc/target_filter_distance(list/targets)
|
||||
var/target_dist = get_dist(holder, target)
|
||||
var/list/better_targets = list()
|
||||
for(var/possible_target in targets)
|
||||
var/atom/A = possible_target
|
||||
var/target_dist = get_dist(holder, target)
|
||||
var/possible_target_distance = get_dist(holder, A)
|
||||
if(target_dist < possible_target_distance)
|
||||
targets -= A
|
||||
return targets
|
||||
if(possible_target_distance < target_dist)
|
||||
better_targets += A
|
||||
return better_targets
|
||||
|
||||
// Returns the closest target and anything tied with it for distance
|
||||
/datum/ai_holder/proc/target_filter_closest(list/targets)
|
||||
var/lowest_distance = -1
|
||||
var/list/sorted_targets = list()
|
||||
var/lowest_distance = 1e6 //fakely far
|
||||
var/list/closest_targets = list()
|
||||
for(var/possible_target in targets)
|
||||
var/atom/A = possible_target
|
||||
var/current_distance = get_dist(holder, A)
|
||||
if(lowest_distance == -1)
|
||||
if(current_distance < lowest_distance)
|
||||
closest_targets.Cut()
|
||||
lowest_distance = current_distance
|
||||
sorted_targets += A
|
||||
else if(current_distance < lowest_distance)
|
||||
targets.Cut()
|
||||
lowest_distance = current_distance
|
||||
sorted_targets += A
|
||||
closest_targets += A
|
||||
else if(current_distance == lowest_distance)
|
||||
sorted_targets += A
|
||||
return sorted_targets
|
||||
closest_targets += A
|
||||
return closest_targets
|
||||
|
||||
/datum/ai_holder/proc/can_attack(atom/movable/the_target, var/vision_required = TRUE)
|
||||
if(!can_see_target(the_target) && vision_required)
|
||||
@@ -163,11 +157,6 @@
|
||||
return TRUE
|
||||
// return FALSE
|
||||
|
||||
// Override this for special targeting criteria.
|
||||
// If it returns true, the mob will always select it as the target.
|
||||
/datum/ai_holder/proc/found(atom/movable/the_target)
|
||||
return FALSE
|
||||
|
||||
// 'Soft' loss of target. They may still exist, we still have some info about them maybe.
|
||||
/datum/ai_holder/proc/lose_target()
|
||||
ai_log("lose_target() : Entering.", AI_LOG_TRACE)
|
||||
|
||||
@@ -256,14 +256,10 @@
|
||||
|
||||
for(var/possible_target in possible_targets)
|
||||
var/atom/A = possible_target
|
||||
if(found(A))
|
||||
. = list(A)
|
||||
break
|
||||
if(istype(A, /mob/living) && !can_pick_mobs)
|
||||
continue
|
||||
if(can_attack(A)) // Can we attack it?
|
||||
. += A
|
||||
continue
|
||||
|
||||
for(var/obj/item/I in .)
|
||||
last_search = world.time
|
||||
|
||||
@@ -225,6 +225,7 @@
|
||||
#include "code\controllers\verbs.dm"
|
||||
#include "code\controllers\observer_listener\atom\observer.dm"
|
||||
#include "code\controllers\subsystems\ai.dm"
|
||||
#include "code\controllers\subsystems\aifast.dm"
|
||||
#include "code\controllers\subsystems\air.dm"
|
||||
#include "code\controllers\subsystems\airflow.dm"
|
||||
#include "code\controllers\subsystems\alarm.dm"
|
||||
|
||||
Reference in New Issue
Block a user