Files
CHOMPStation2/code/modules/ai/ai_holder_combat.dm
CHOMPStation2StaffMirrorBot 26bb8c46a9 [MIRROR] barricade breakthrough (#11657)
Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com>
2025-09-14 23:38:41 -04:00

327 lines
14 KiB
Plaintext

// This file is for actual fighting. Targeting is in a seperate file.
/datum/ai_holder
var/firing_lanes = TRUE // If ture, tries to refrain from shooting allies or the wall.
var/conserve_ammo = FALSE // If true, the mob will avoid shooting anything that does not have a chance to hit a mob. Requires firing_lanes to be true.
var/pointblank = FALSE // If ranged is true, and this is true, people adjacent to the mob will suffer the ranged instead of using a melee attack.
var/can_breakthrough = TRUE // If false, the AI will not try to open a path to its goal, like opening doors.
var/violent_breakthrough = TRUE // If false, the AI is not allowed to destroy things like windows or other structures in the way. Requires above var to be true.
var/stand_ground = FALSE // If true, the AI won't try to get closer to an enemy if out of range.
// This does the actual attacking.
/datum/ai_holder/proc/engage_target()
ai_log("engage_target() : Entering.", AI_LOG_DEBUG)
// Can we still see them?
if(QDELETED(target) || !can_attack(target)) //CHOMPEdit
ai_log("engage_target() : Lost sight of target.", AI_LOG_TRACE)
if(lose_target()) // We lost them (returns TRUE if we found something else to do)
ai_log("engage_target() : Pursuing other options (last seen, or a new target).", AI_LOG_TRACE)
return
var/distance = get_dist(holder, target)
ai_log("engage_target() : Distance to target ([target]) is [distance].", AI_LOG_TRACE)
holder.face_atom(target)
last_conflict_time = world.time
// Do a 'special' attack, if one is allowed.
// if(prob(special_attack_prob) && (distance >= special_attack_min_range) && (distance <= special_attack_max_range))
if(holder.ICheckSpecialAttack(target))
ai_log("engage_target() : Attempting a special attack.", AI_LOG_TRACE)
on_engagement(target)
if(special_attack(target)) // If this fails, then we try a regular melee/ranged attack.
ai_log("engage_target() : Successful special attack. Exiting.", AI_LOG_DEBUG)
return
// Stab them.
else if(distance <= 1 && !pointblank)
ai_log("engage_target() : Attempting a melee attack.", AI_LOG_TRACE)
on_engagement(target)
melee_attack(target)
else if(distance <= 1 && !holder.ICheckRangedAttack(target)) // Doesn't have projectile, but is pointblank
ai_log("engage_target() : Attempting a melee attack.", AI_LOG_TRACE)
on_engagement(target)
melee_attack(target)
// Shoot them.
else if(holder.ICheckRangedAttack(target) && (distance <= max_range(target)) )
on_engagement(target)
if(firing_lanes && !test_projectile_safety(target))
// Nudge them a bit, maybe they can shoot next time.
var/turf/T = get_step(holder, pick(GLOB.cardinal))
if(T)
holder.IMove(T) // IMove() will respect movement cooldown.
holder.face_atom(target)
ai_log("engage_target() : Could not safely fire at target. Exiting.", AI_LOG_DEBUG)
return
ai_log("engage_target() : Attempting a ranged attack.", AI_LOG_TRACE)
ranged_attack(target)
// Run after them.
else if(!stand_ground)
ai_log("engage_target() : Target ([target]) too far away. Exiting.", AI_LOG_DEBUG)
set_stance(STANCE_APPROACH)
// We're not entirely sure how holder will do melee attacks since any /mob/living could be holder, but we don't have to care because Interfaces.
/datum/ai_holder/proc/melee_attack(atom/A)
pre_melee_attack(A)
. = holder.IAttack(A)
if(. == ATTACK_SUCCESSFUL)
post_melee_attack(A)
// Ditto.
/datum/ai_holder/proc/ranged_attack(atom/A)
pre_ranged_attack(A)
. = holder.IRangedAttack(A)
if(. == ATTACK_SUCCESSFUL)
post_ranged_attack(A)
// Most mobs probably won't have this defined but we don't care.
/datum/ai_holder/proc/special_attack(atom/movable/AM)
pre_special_attack(AM)
. = holder.ISpecialAttack(AM)
if(. == ATTACK_SUCCESSFUL)
post_special_attack(AM)
// Called when within striking/shooting distance, however cooldown is not considered.
// Override to do things like move in a random step for evasiveness.
// Note that this is called BEFORE the attack.
/datum/ai_holder/proc/on_engagement(atom/A)
// Called before a ranged attack is attempted.
/datum/ai_holder/proc/pre_ranged_attack(atom/A)
// Called before a melee attack is attempted.
/datum/ai_holder/proc/pre_melee_attack(atom/A)
// Called before a 'special' attack is attempted.
/datum/ai_holder/proc/pre_special_attack(atom/A)
// Called after a successful (IE not on cooldown) ranged attack.
// Note that this is not whether the projectile actually hit, just that one was launched.
/datum/ai_holder/proc/post_ranged_attack(atom/A)
// Ditto but for melee.
/datum/ai_holder/proc/post_melee_attack(atom/A)
// And one more for special snowflake attacks.
/datum/ai_holder/proc/post_special_attack(atom/A)
// Used to make sure projectiles will probably hit the target and not the wall or a friend.
/datum/ai_holder/proc/test_projectile_safety(atom/movable/AM)
ai_log("test_projectile_safety([AM]) : Entering.", AI_LOG_TRACE)
// If they're right next to us then lets just say yes. check_trajectory() tends to spaz out otherwise.
if(holder.Adjacent(AM))
ai_log("test_projectile_safety() : Adjacent to target. Exiting with TRUE.", AI_LOG_TRACE)
return TRUE
// This will hold a list of all mobs in a line, even those behind the target, and possibly the wall.
// By default the test projectile goes through things like glass and grilles, which is desirable as otherwise the AI won't try to shoot through windows.
var/list/hit_things = check_trajectory(AM, holder) // This isn't always reliable but its better than the previous method.
// Test to see if the primary target actually has a chance to get hit.
// We'll fire anyways if not, if we have conserve_ammo turned off.
var/would_hit_primary_target = FALSE
if(AM in hit_things)
would_hit_primary_target = TRUE
ai_log("test_projectile_safety() : Test projectile did[!would_hit_primary_target ? " NOT " : " "]hit \the [AM]", AI_LOG_DEBUG)
// Make sure we don't have a chance to shoot our friends.
for(var/atom/A as anything in hit_things)
ai_log("test_projectile_safety() : Evaluating \the [A] ([A.type]).", AI_LOG_TRACE)
if(isliving(A)) // Don't shoot at our friends, even if they're behind the target, as RNG can make them get hit.
var/mob/living/L = A
if(holder.IIsAlly(L))
ai_log("test_projectile_safety() : Would threaten ally, exiting with FALSE.", AI_LOG_DEBUG)
return FALSE
// Don't fire if we cannot hit the primary target, and we wish to be conservative with our projectiles.
// We make an exception for turf targets because manual commanded AIs targeting the floor are generally intending to fire blindly.
if(!would_hit_primary_target && !isturf(AM) && conserve_ammo)
ai_log("test_projectile_safety() : conserve_ammo is set, and test projectile failed to hit primary target. Exiting with FALSE.", AI_LOG_DEBUG)
return FALSE
ai_log("test_projectile_safety() : Passed other tests, exiting with TRUE.", AI_LOG_TRACE)
return TRUE
// Test if we are within range to attempt an attack, melee or ranged.
/datum/ai_holder/proc/within_range(atom/movable/AM)
var/distance = get_dist(holder, AM)
if(distance <= 1)
return TRUE // Can melee.
else if(holder.ICheckRangedAttack(AM) && distance <= max_range(AM))
return TRUE // Can shoot.
return FALSE
// Determines how close the AI will move to its target.
/datum/ai_holder/proc/closest_distance(atom/movable/AM)
return max(max_range(AM) - 1, 1) // Max range -1 just because we don't want to constantly get kited
// Can be used to conditionally do a ranged or melee attack.
/datum/ai_holder/proc/max_range(atom/movable/AM)
return holder.ICheckRangedAttack(AM) ? 7 : 1
// Goes to the target, to attack them.
// Called when in STANCE_APPROACH.
/datum/ai_holder/proc/walk_to_target()
ai_log("walk_to_target() : Entering.", AI_LOG_DEBUG)
// Make sure we can still chase/attack them.
if(QDELETED(target) || !can_attack(target))
ai_log("walk_to_target() : Lost target.", AI_LOG_INFO)
lose_target()
return
// Find out where we're going.
var/get_to = closest_distance(target)
var/distance = get_dist(holder, target)
ai_log("walk_to_target() : get_to is [get_to].", AI_LOG_TRACE)
// We're here!
// Special case: Our holder has a special attack that is ranged, but normally the holder uses melee.
// If that happens, we'll switch to STANCE_FIGHT so they can use it. If the special attack is limited, they'll likely switch back next tick.
if(distance <= get_to || holder.ICheckSpecialAttack(target))
ai_log("walk_to_target() : Within range.", AI_LOG_INFO)
forget_path()
set_stance(STANCE_FIGHT)
ai_log("walk_to_target() : Exiting.", AI_LOG_DEBUG)
return
// Otherwise keep walking.
if(!stand_ground)
walk_path(target, get_to)
ai_log("walk_to_target() : Exiting.", AI_LOG_DEBUG)
// Resists out of things.
// Sometimes there are times you want your mob to be buckled to something, so override this for when that is needed.
/datum/ai_holder/proc/handle_resist()
holder.resist()
// Used to break through windows and barriers to a target on the other side.
// This does two passes, so that if its just a public access door, the windows nearby don't need to be smashed.
/datum/ai_holder/proc/breakthrough(atom/target_atom)
ai_log("breakthrough() : Entering", AI_LOG_TRACE)
if(!can_breakthrough)
ai_log("breakthrough() : Not allowed to breakthrough. Exiting.", AI_LOG_TRACE)
return FALSE
if(!isturf(holder.loc))
ai_log("breakthrough() : Trapped inside \the [holder.loc]. Exiting.", AI_LOG_TRACE)
return FALSE
var/dir_to_target = get_dir(holder, target_atom)
holder.face_atom(target_atom)
// Sometimes the mob will try to hit something diagonally, and generally this fails.
// So instead we will try two more times with some adjustments if the attack fails.
var/list/directions_to_try = list(
dir_to_target,
turn(dir_to_target, 45),
turn(dir_to_target, -45)
)
ai_log("breakthrough() : Starting peaceful pass.", AI_LOG_DEBUG)
var/result = FALSE
// First, we will try to peacefully make a path, I.E opening a door we have access to.
for(var/direction in directions_to_try)
result = destroy_surroundings(direction, violent = FALSE)
if(result)
break
// Alright, lets smash some shit instead, if it didn't work and we're allowed to be violent.
if(!result && can_violently_breakthrough())
ai_log("breakthrough() : Starting violent pass.", AI_LOG_DEBUG)
for(var/direction in directions_to_try)
result = destroy_surroundings(direction, violent = TRUE)
if(result)
break
ai_log("breakthrough() : Exiting with [result].", AI_LOG_TRACE)
return result
// Despite the name, this can also be used to help clear a path without any destruction.
/datum/ai_holder/proc/destroy_surroundings(direction, violent = TRUE)
ai_log("destroy_surroundings() : Entering.", AI_LOG_TRACE)
if(!direction)
direction = pick(GLOB.cardinal) // FLAIL WILDLY
ai_log("destroy_surroundings() : No direction given, picked [direction] randomly.", AI_LOG_DEBUG)
var/turf/problem_turf = get_step(holder, direction)
// First, give peace a chance.
if(!violent)
ai_log("destroy_surroundings() : Going to try to peacefully clear [problem_turf].", AI_LOG_DEBUG)
for(var/obj/machinery/door/D in problem_turf)
if(D.density && holder.Adjacent(D) && D.allowed(holder) && D.operable())
// First, try to open the door if possible without smashing it. We might have access.
ai_log("destroy_surroundings() : Opening closed door.", AI_LOG_INFO)
return D.open()
// Peace has failed us, can we just smash the things in the way?
else
ai_log("destroy_surroundings() : Going to try to violently clear [problem_turf].", AI_LOG_DEBUG)
// First, kill windows in the way.
for(var/obj/structure/window/W in problem_turf)
if(W.dir == GLOB.reverse_dir[holder.dir]) // So that windows get smashed in the right order
ai_log("destroy_surroundings() : Attacking side window.", AI_LOG_INFO)
return melee_attack(W)
else if(W.is_fulltile())
ai_log("destroy_surroundings() : Attacking full tile window.", AI_LOG_INFO)
return melee_attack(W)
// Kill hull shields in the way.
for(var/obj/effect/energy_field/shield in problem_turf)
if(shield.density) // Don't attack shields that are already down.
ai_log("destroy_surroundings() : Attacking hull shield.", AI_LOG_INFO)
return melee_attack(shield)
// Kill energy shields in the way.
for(var/obj/effect/shield/S in problem_turf)
if(S.density) // Don't attack shields that are already down.
ai_log("destroy_surroundings() : Attacking energy shield.", AI_LOG_INFO)
return melee_attack(S)
// Kill common obstacle in the way like tables.
var/obj/structure/obstacle = locate(/obj/structure, problem_turf)
var/list/common_obstacles = list(/obj/structure/window,
/obj/structure/closet,
/obj/structure/table,
/obj/structure/grille,
/obj/structure/barricade,
/obj/structure/girder,
)
if(is_type_in_list(obstacle, common_obstacles))
ai_log("destroy_surroundings() : Attacking generic structure.", AI_LOG_INFO)
return melee_attack(obstacle)
var/obj/effect/weaversilk/web = locate(/obj/effect/weaversilk, problem_turf)
if(istype(web, /obj/effect/weaversilk/wall)) //VOREStation Edit: spdr
ai_log("destroy_surroundings() : Attacking weaversilk effect.", AI_LOG_INFO)
return melee_attack(web)
for(var/obj/machinery/door/D in problem_turf) // Required since firelocks take up the same turf.
if(D.density)
ai_log("destroy_surroundings() : Attacking closed door.", AI_LOG_INFO)
return melee_attack(D)
// Should always be last thing attempted
if(!problem_turf.opacity)
ai_log("destroy_surroundings() : Attacking a transparent (window?) turf.", AI_LOG_INFO)
return melee_attack(problem_turf)
ai_log("destroy_surroundings() : Exiting due to nothing to attack.", AI_LOG_INFO)
return ATTACK_FAILED // Nothing to attack.
// Override for special behaviour.
/datum/ai_holder/proc/can_violently_breakthrough()
return violent_breakthrough