mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-09 16:12:17 +00:00
327 lines
14 KiB
Plaintext
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
|