Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding z-aware distance procs for better AI behavior. #4633

Draft
wants to merge 1 commit into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions code/_helpers/game.dm
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,7 @@
/proc/get_dist_euclidian(atom/Loc1, atom/Loc2)
var/dx = Loc1.x - Loc2.x
var/dy = Loc1.y - Loc2.y

var/dist = sqrt(dx**2 + dy**2)

return dist

/proc/get_dist_bounds(var/target, var/source) // Alternative to get_dist for multi-turf objects
Expand Down
13 changes: 13 additions & 0 deletions code/_helpers/turfs.dm
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,19 @@

return new_turf

/proc/get_dist_cross_z(turf/origin, turf/target)
if(!istype(origin) || !istype(target) || !(origin.z in SSmapping.get_connected_levels(target.z, include_lateral = TRUE)))
return INFINITY
if(origin == target)
return 0
var/datum/level_data/origin_level = SSmapping.levels_by_z[origin.z]
var/datum/level_data/target_level = SSmapping.levels_by_z[target.z]
if(origin_level == target_level)
return get_dist(origin, target)
if(!istype(origin_level) || !istype(target_level))
return INFINITY
return sqrt((((target.x + target_level.z_volume_level_x)-(origin.x + origin_level.z_volume_level_x))**2) + (((target.y + target_level.z_volume_level_y)-(origin.y + origin_level.z_volume_level_y))**2))

/proc/get_dir_z_text(turf/origin, turf/target)

origin = get_turf(origin)
Expand Down
3 changes: 3 additions & 0 deletions code/datums/ai/_ai.dm
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
/// What directions can we wander in? Uses global.cardinal if unset.
var/list/wander_directions

/// Should we use more expensive distance, viewer and hearer checking that goes across z-level borders?
var/use_cross_z_detection = FALSE

/// Should we retaliate/startle when grabbed or buckled?
var/spooked_by_grab = TRUE
/// Can we automatically escape from buckling?
Expand Down
11 changes: 6 additions & 5 deletions code/datums/ai/aggressive.dm
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
return FALSE
if(body.next_move >= world.time)
return FALSE
if(get_dist(body, target) > 1)
if((use_cross_z_detection ? get_dist_cross_z(body, target) : get_dist(body, target)) > 1)
move_to_target()
return FALSE
//Attacking
Expand Down Expand Up @@ -186,7 +186,7 @@
return

var/list/allies
var/list/around = view(body, 7)
var/list/around = use_cross_z_detection ? body.view_cross_z(7) : view(body, 7)
for(var/atom/movable/A in around)
if(A == body || !isliving(A))
continue
Expand All @@ -212,7 +212,8 @@
if(!istype(target) || !attackable(target) || !(target in list_targets(10)))
lose_target()
return
if(body.has_ranged_attack() && get_dist(body, target) <= body.get_ranged_attack_distance() && !move_only)
var/target_dist = use_cross_z_detection ? get_dist_cross_z(body, target) : get_dist(body, target)
if(body.has_ranged_attack() && target_dist <= body.get_ranged_attack_distance() && !move_only)
body.stop_automove()
open_fire()
return
Expand All @@ -223,11 +224,11 @@
// Base hostile mobs will just destroy everything in view.
// Mobs with an enemy list will filter the view by their enemies.
if(!only_attack_enemies)
return hearers(body, dist)-body
return (use_cross_z_detection ? body.hearers_cross_z(dist) : hearers(body, dist)) - body
var/list/enemies = get_enemies()
if(!LAZYLEN(enemies))
return
var/list/possible_targets = hearers(body, dist)-body
var/list/possible_targets = (use_cross_z_detection ? body.hearers_cross_z(dist) : hearers(body, dist)) - body
if(!length(possible_targets))
return
for(var/weakref/enemy in enemies) // Remove all entries that aren't in enemies
Expand Down
4 changes: 2 additions & 2 deletions code/datums/ai/beast.dm
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
if(nut > max_nut * 0.75 || beast.incapacitated())
LAZYCLEARLIST(prey)
return
for(var/mob/living/simple_animal/S in range(beast,1))
for(var/mob/living/simple_animal/S in (use_cross_z_detection ? beast.range_cross_z(1) : range(beast,1)))
if(S == beast)
continue
if(S.stat != DEAD)
Expand Down Expand Up @@ -46,6 +46,6 @@
if(M)
. |= M
else if(body.get_nutrition() < body.get_max_nutrition() * 0.75) //time to look for some food
for(var/mob/living/L in view(body, dist))
for(var/mob/living/L in (use_cross_z_detection ? body.view_cross_z(dist) : view(body, dist)))
if(attack_same_faction || L.faction != body.faction)
LAZYDISTINCTADD(prey, weakref(L))
2 changes: 1 addition & 1 deletion code/datums/ai/commanded.dm
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@

//returns a list of everybody we wanna do stuff with.
/datum/mob_controller/aggressive/commanded/proc/get_targets_by_name(var/message, var/filter_friendlies = 0)
var/list/possible_targets = hearers(body, 10)
var/list/possible_targets = use_cross_z_detection ? body.hearers_cross_z(10) : hearers(body, 10)
for(var/mob/M in possible_targets)
if((filter_friendlies && is_friend(M)) || M.faction == body.faction || M == master)
continue
Expand Down
4 changes: 2 additions & 2 deletions code/datums/ai/hunter.dm
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// Fleeing takes precedence.
. = ..()
if(!. && !get_target() && world.time >= next_hunt) // TODO: generalized nutrition process. && body.get_nutrition() < body.get_max_nutrition() * 0.5)
for(var/mob/living/snack in view(body)) //search for a new target
for(var/mob/living/snack in (use_cross_z_detection ? body.view_cross_z() : view(body))) //search for a new target
if(can_hunt(snack))
set_target(snack)
break
Expand Down Expand Up @@ -57,7 +57,7 @@
return

var/mob/living/target = get_target()
if(!istype(target) || QDELETED(target) || !(target in view(body)))
if(!istype(target) || QDELETED(target) || !(target in (use_cross_z_detection ? body.view_cross_z() : view(body))))
set_target(null)
resume_wandering()
return
Expand Down
3 changes: 2 additions & 1 deletion code/datums/ai/monkey.dm
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@

if(!held && !body.restrained() && prob(2.5))
var/list/touchables = list()
for(var/obj/O in range(1,get_turf(body)))
var/turf/my_turf = get_turf(body)
for(var/obj/O in (use_cross_z_detection ? my_turf.range_cross_z(1) : range(1, my_turf)))
if(O.simulated && CanPhysicallyInteractWith(body, O) && !is_type_in_list(O, no_touchie))
touchables += O
if(touchables.len)
Expand Down
2 changes: 1 addition & 1 deletion code/datums/ai/passive.dm
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
/datum/mob_controller/passive/proc/update_targets()
//see if we should stop fleeing
var/atom/flee_target_atom = flee_target?.resolve()
if(istype(flee_target_atom) && (flee_target_atom.loc in view(body)))
if(istype(flee_target_atom) && (flee_target_atom.loc in (use_cross_z_detection ? body.view_cross_z() : view(body))))
startle()
stop_wandering()
if(body.MayMove())
Expand Down
84 changes: 84 additions & 0 deletions code/game/atoms.dm
Original file line number Diff line number Diff line change
Expand Up @@ -994,3 +994,87 @@

/atom/proc/is_watertight()
return ATOM_IS_OPEN_CONTAINER(src)

// Helper procs for detecting atoms across lateral z-level boundaries.Likely much,
// much, MUCH more expensive than regular range/view/viewers/hearers, so use sparingly.
// Technically will lose any turfs that don't have a mimic, regardless of distance into
// neighboring z that it would like to scan. Should be close enough for government work.
/atom/proc/viewers_cross_z(dist = world.view, atom/origin = src)
if(isnum(origin))
dist = origin
origin = src
if(!istype(origin))
return null
var/list/viewer_list = viewers(dist, origin)
if(!length(viewer_list))
return viewer_list
var/datum/level_data/origin_level = SSmapping.levels_by_z[origin.z]
if(!istype(origin_level))
return viewer_list
for(var/turf/turf in viewer_list)
var/turf/real_turf = turf.resolve_to_actual_turf()
if(turf != real_turf)
viewer_list |= real_turf
if(length(real_turf.contents))
viewer_list |= real_turf.contents
return viewer_list

/atom/proc/view_cross_z(dist = world.view, atom/origin = src)
if(isnum(origin))
dist = origin
origin = src
if(!istype(origin))
return null
var/list/view_list = view(dist, origin)
if(!length(view_list))
return view_list
var/datum/level_data/origin_level = SSmapping.levels_by_z[origin.z]
if(!istype(origin_level))
return view_list
for(var/turf/turf in view_list)
var/turf/real_turf = turf.resolve_to_actual_turf()
if(turf != real_turf)
view_list |= real_turf
if(length(real_turf.contents))
view_list |= real_turf.contents
return view_list

/atom/proc/hearers_cross_z(dist = world.view, atom/origin = src)
if(isnum(origin))
dist = origin
origin = src
if(!istype(origin))
return null
var/list/hearer_list = hearers(dist, origin)
if(!length(hearer_list))
return hearer_list
var/datum/level_data/origin_level = SSmapping.levels_by_z[origin.z]
if(!istype(origin_level))
return hearer_list
for(var/turf/turf in hearer_list)
var/turf/real_turf = turf.resolve_to_actual_turf()
if(turf != real_turf)
hearer_list |= real_turf
if(length(real_turf.contents))
hearer_list |= real_turf.contents
return hearer_list

/atom/proc/range_cross_z(dist = world.view, atom/origin = src)
if(isnum(origin))
dist = origin
origin = src
if(!istype(origin))
return null
var/list/range_list = range(dist, origin)
if(!length(range_list))
return range_list
var/datum/level_data/origin_level = SSmapping.levels_by_z[origin.z]
if(!istype(origin_level))
return range_list
for(var/turf/turf in range_list)
var/turf/real_turf = turf.resolve_to_actual_turf()
if(turf != real_turf)
range_list |= real_turf
if(length(real_turf.contents))
range_list |= real_turf.contents
return range_list
Loading