further work

This commit is contained in:
Neerti
2018-04-03 22:12:50 -04:00
parent 6c26c84695
commit 61bb059ae2
28 changed files with 1289 additions and 357 deletions
+57
View File
@@ -0,0 +1,57 @@
/*
This module contains an AI implementation designed to be (mostly) mobtype-agnostic, by being held inside a datum instead of being on the mob directly.
Seperation:
The ai_holder datum is designed to be fairly distant from its mob holder, in terms of coupling.
This presents some advantages.
* Not being tied to the mob's Life() cycle allows for a different tick rate.
* Being seperate from the mob simplifies mob code greatly.
* It allows for better encapsulation and seperation of duties from the mob.
* It is more logical to think that the mob is the 'body', where as its ai_holder is the 'mind'.
* It can be made mobtype-independant with the use of Interfaces.
The datum is held by the mob that it should control, called the holder, and is not processed by the mob itself (as Life() did so in previous implementations).
Instead, each instance of /datum/ai_holder is processed by the 'AI' master controller subsystem. The datum itself has two seperate process tracks instead of one,
as most other objects ( process() ) or mobs ( Life() ) do.
Flow of Execution:
/ - Every 0.5s - > /datum/ai_holder/handle_tactics() - > /datum/ai_holder/handle_stance_tactical() - > switch(stance)...
AI Subsystem - *
\ - Every 2.0s - > /datum/ai_holder/handle_strategicals() - > /datum/ai_holder/handle_stance_strategical() - > switch(stance)...
The datum is not driven by its mob, as previous implementations did, meaning Life() is not involved. Instead, it is processed by a specific Master Controller Subsystem
titled 'AI', which by default ticks every half a second. Each instance of the ai_holder datum that is not 'asleep' is part of a list, containing ai_holders that are awake.
When the subsystem runs, each ai_holder instance inside the list is iterated on, and calls one or two procs on it.
Every tick, each instance has handle_tactics() called on it, and every four ticks, handle_strategicals(), meaning every half a second and very two seconds, respectively.
This means that the ai_holder datum has two seperate types of processing, for the purposes of efficency.
handle_tactics() is used for 'cheap' processing that needs to happen fast, such as walking along a path, initiating an attack, or firing a gun.
The rate that it is called allows for the ai_holder to interact with the world through its mob very often, giving a more convincing appearance of intelligence,
allowing for faster reaction times to certain events, and allowing for variable attack speeds that would not be possible when bound to a two second Life() cycle.
handle_strategicals() on the other hand, is for 'expensive' processing that might be too demanding on the CPU to do every half a second, such as re/calculating an A* path, or
running a complicated tension assessment to determine how brave the mob is feeling. This is the same delay used for certain tasks in the old implementation.
Having two seperate process procs allows for 'fast' and 'slow' actions to be more easily encapsulated, and ensures that all ai_holders are syncronized with each other,
as opposed to having individual tick counters inside all of the ai_holder instances. It should be noted that handle_tactics() is called first,
before handle_strategicals() every two seconds.
Both process procs work in a similar fashion, using a large amount of 'stances' to act in a specific way, effectively creating a state pattern.
There are 10 stances implemented, and more can easily be added by defining them and switching to them with the set_stance() proc.
This is similar to the old implementation, except with a vastly larger amount of options for the AI to make use of.
See code/__defines/mob.dm for the stance defines and descriptions about their purpose.
Each stance is evaluated once per tick, meaning that switching the stance takes effect on the next tick.
*/
+25 -3
View File
@@ -1,3 +1,25 @@
#define AI_DUMB 1
#define AI_NORMAL 2
#define AI_SMART 3
// Defines for the ai_intelligence var.
// Controls if the mob will do 'advanced tactics' like running from grenades.
#define AI_DUMB 1 // Be dumber than usual.
#define AI_NORMAL 2 // Default level.
#define AI_SMART 3 // Will do more processing to perceive threats such as active grenades. Use sparingly.
#define ai_log(M,V) if(debug_ai) ai_log_output(M,V)
// Logging level defines.
#define AI_LOG_OFF 0 // Don't show anything.
#define AI_LOG_ERROR 1 // Show logs of things likely causing the mob to not be functioning correctly.
#define AI_LOG_WARNING 2 // Show less serious but still helpful to know issues that might be causing things to work incorrectly.
#define AI_LOG_INFO 3 // Important regular events, like selecting a target or switching stances.
#define AI_LOG_DEBUG 4 // More detailed information about the flow of execution.
#define AI_LOG_TRACE 5 // Even more detailed than the last.
// Reasons for targets to not be valid. Based on why, the AI responds differently.
#define AI_TARGET_VALID 0 // We can fight them.
#define AI_TARGET_INVIS 1 // They were in field of view but became invisible. Switch to STANCE_BLINDFIGHT if no other viable targets exist.
#define AI_TARGET_NOSIGHT 2 // No longer in field of view. Go STANCE_REPOSITION to their last known location if no other targets are seen.
#define AI_TARGET_ALLY 3 // They are an ally. Find a new target.
#define AI_TARGET_DEAD 4 // They're dead. Find a new target.
#define AI_TARGET_INVINCIBLE 5 // Target is currently unable to receive damage for whatever reason. Find a new target or wait.
//TRACE DEBUG INFO WARN ERROR OFF
+143 -26
View File
@@ -18,7 +18,7 @@
/datum/ai_holder
var/mob/living/holder = null // The mob this datum is going to control.
var/stance = STANCE_IDLE // Determines if the mob should be doing a specific thing, e.g. attacking, following, standing around, etc.
var/intelligence_level = AI_NORMAL
var/intelligence_level = AI_NORMAL // Adjust to make the AI be intentionally dumber, or make it more robust (e.g. dodging grenades).
@@ -29,10 +29,6 @@
hostile = TRUE
retaliate = TRUE
/datum/ai_holder/test
hostile = TRUE
use_astar = TRUE
/datum/ai_holder/New(var/new_holder)
ASSERT(new_holder)
holder = new_holder
@@ -49,18 +45,31 @@
// Now for the actual AI stuff.
// Makes this ai holder not get processed.
// Called automatically when the host mob is killed.
// Potential future optimization would be to sleep AIs which mobs that are far away from in-round players.
/datum/ai_holder/proc/go_sleep()
if(stance == STANCE_SLEEP)
return
stance = STANCE_SLEEP
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
// Reverses the above proc.
// Revived mobs will wake their AI if they have one.
/datum/ai_holder/proc/go_wake()
if(stance != STANCE_SLEEP)
return
stance = STANCE_IDLE
set_stance(STANCE_IDLE)
SSai.processing += src
// Resets a lot of 'memory' vars.
/datum/ai_holder/proc/forget_everything()
// Some of these might be redundant, but hopefully this prevents future bugs if that changes.
lose_follow()
lose_target()
lose_target_position()
give_up_movement()
// 'Tactical' processes such as moving a step, meleeing an enemy, firing a projectile, and other fairly cheap actions that need to happen quickly.
/datum/ai_holder/proc/handle_tactics()
@@ -68,7 +77,7 @@
// 'Strategical' processes that are more expensive on the CPU and so don't get run as often as the above proc, such as A* pathfinding or robust targeting.
/datum/ai_holder/proc/handle_strategicals()
world << "[holder.name] Strategicals!"
// world << "[holder.name] Strategicals!"
handle_stance_strategical()
/*
@@ -100,42 +109,141 @@
// For setting the stance WITHOUT processing it
/datum/ai_holder/proc/set_stance(var/new_stance)
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()
/datum/ai_holder/proc/handle_stance_tactical(var/new_stance)
if(new_stance)
set_stance(new_stance)
// 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.
ai_log("handle_stance_tactical() : Called.", AI_LOG_TRACE)
if(attack_cooldown_left > 0)
attack_cooldown_left--
if(stance == STANCE_SLEEP)
ai_log("handle_stance_tactical() : Going to sleep.", AI_LOG_TRACE)
go_sleep()
return
if(target && can_see_target(target))
track_target_position()
if(!can_act())
ai_log("handle_stance_tactical() : Stunned.", AI_LOG_TRACE)
set_stance(STANCE_STUNNED)
return
if(stance in STANCES_COMBAT)
// Should resist? We check this before fleeing so that we can actually flee and not be trapped in a chair.
if(holder.incapacitated(INCAPACITATION_BUCKLED_PARTIALLY))
ai_log("handle_stance_tactical() : Going to handle_resist().", AI_LOG_TRACE)
handle_resist()
else if(istype(holder.loc, /obj/structure/closet))
var/obj/structure/closet/C = holder.loc
ai_log("handle_stance_tactical() : Inside a closet. Going to attempt escape.", AI_LOG_TRACE)
if(C.welded)
holder.resist()
else
C.open()
// Should we flee?
if(should_flee())
ai_log("handle_stance_tactical() : Going to flee.", AI_LOG_TRACE)
set_stance(STANCE_FLEE)
return
switch(stance)
if(STANCE_SLEEP)
go_sleep()
return
if(STANCE_IDLE)
holder.a_intent = I_HELP
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(hostile)
// find_target()
if(STANCE_ALERT)
ai_log("handle_stance_tactical() : STANCE_ALERT, going to threaten_target().", AI_LOG_TRACE)
threaten_target()
if(STANCE_ATTACK)
if(target)
walk_to_target()
if(STANCE_APPROACH)
ai_log("handle_stance_tactical() : STANCE_APPROACH, going to walk_to_target().", AI_LOG_TRACE)
walk_to_target()
if(STANCE_ATTACKING)
if(STANCE_FIGHT)
ai_log("handle_stance_tactical() : STANCE_FIGHT, going to engage_target().", AI_LOG_TRACE)
engage_target()
if(STANCE_MOVE)
ai_log("handle_stance_tactical() : STANCE_MOVE, going to walk_to_destination().", AI_LOG_TRACE)
walk_to_destination()
if(STANCE_REPOSITION) // This is the same as above but doesn't stop if an enemy is visible since its an 'in-combat' move order.
ai_log("handle_stance_tactical() : STANCE_REPOSITION, going to walk_to_destination().", AI_LOG_TRACE)
walk_to_destination()
if(STANCE_FOLLOW)
ai_log("handle_stance_tactical() : STANCE_FOLLOW, going to walk_to_leader().", AI_LOG_TRACE)
walk_to_leader()
if(STANCE_FLEE)
ai_log("handle_stance_tactical() : STANCE_FLEE, going to flee_from_target().", AI_LOG_TRACE)
flee_from_target()
if(STANCE_STUNNED)
ai_log("handle_stance_tactical() : STANCE_STUNNED.", AI_LOG_TRACE)
if(can_act())
ai_log("handle_stance_tactical() : No longer stunned.", AI_LOG_TRACE)
set_stance(STANCE_IDLE)
ai_log("handle_stance_tactical() : Exiting.", AI_LOG_TRACE)
ai_log("========= Fast Process Ending ==========", AI_LOG_TRACE)
// This is called every two seconds.
/datum/ai_holder/proc/handle_stance_strategical()
ai_log("++++++++++ Slow Process Beginning ++++++++++", AI_LOG_TRACE)
ai_log("handle_stance_strategical() : Called.", AI_LOG_TRACE)
switch(stance)
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(STANCE_ATTACK)
if(STANCE_APPROACH)
if(target)
ai_log("handle_stance_strategical() : STANCE_APPROACH, going to calculate_path([target]).", AI_LOG_TRACE)
calculate_path(target)
if(STANCE_MOVE)
if(hostile && find_target()) // This will switch its stance.
ai_log("handle_stance_strategical() : STANCE_MOVE, found target and was inturrupted.", AI_LOG_TRACE)
if(STANCE_FOLLOW)
if(hostile && find_target()) // This will switch its stance.
ai_log("handle_stance_strategical() : STANCE_FOLLOW, found target and was inturrupted.", AI_LOG_TRACE)
else if(leader)
ai_log("handle_stance_strategical() : STANCE_FOLLOW, going to calculate_path([leader]).", AI_LOG_TRACE)
calculate_path(leader)
ai_log("handle_stance_strategical() : Exiting.", AI_LOG_TRACE)
ai_log("++++++++++ Slow Process Ending ++++++++++", AI_LOG_TRACE)
/*
//Yes I'm breaking this into two if()'s for ease of reading
//If we ARE ALLOWED TO
if(returns_home && home_turf && !astarpathing && (world.time - stance_changed) > 10 SECONDS)
if(get_dist(src,home_turf) > wander_distance)
move_to_delay = initial(move_to_delay)*2 //Walk back.
GoHome()
else
stop_automated_movement = 0
*/
/*
// For proccessing the current stance, or setting and processing a new one
@@ -173,14 +281,23 @@
return
if(hostile)
FindTarget()
if(STANCE_ATTACK)
if(STANCE_APPROACH)
annoyed = 50
a_intent = I_HURT
RequestHelp()
MoveToTarget()
if(STANCE_ATTACKING)
if(STANCE_FIGHT)
annoyed = 50
AttackTarget()
*/
// If our holder is able to do anything.
/datum/ai_holder/proc/can_act()
if(holder.stat) // Dead or unconscious.
ai_log("can_act() : Stat was non-zero ([holder.stat]).", AI_LOG_TRACE)
return FALSE
if(holder.incapacitated(INCAPACITATION_DISABLED)) // Stunned in some form.
ai_log("can_act() : Incapacited.", AI_LOG_TRACE)
return FALSE
return TRUE
+166 -152
View File
@@ -1,158 +1,89 @@
// This file is for actual fighting. Targeting is in a seperate file.
/datum/ai_holder
var/hostile = FALSE // Do we try to hurt others? Setting to false disables most combat processing.
var/retaliate = FALSE // Requires hostile to be true to work. If both this and hostile are true, the mob won't attack unless attacked first.
var/cooperative = FALSE // If true, asks allies to help when fighting something.
var/firing_lanes = FALSE // 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/firing_lanes = FALSE // 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/atom/movable/target // The thing (mob or object) we're trying to kill.
var/attack_cooldown = 2 // If set, the mob will wait for the specified amount of ticks before attempting another attack.
var/attack_cooldown_left = 0 // Actual var for tracking if attacks are off cooldown or not. Note that melee and ranged attacks share this.
var/ranged = FALSE // If true, attempts to shoot at the enemy instead of charging at them wildly.
var/shoot_range = 5 // How close the mob needs to be to attempt to shoot at the enemy.
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/ranged = FALSE // If true, attempts to shoot at the enemy instead of charging at them wildly.
var/shoot_range = 5 // How close the mob needs to be to attempt to shoot at the enemy.
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/special_attack_prob = 0 // The chance to ATTEMPT a special_attack(). If it fails, it will do a regular attack instead.
var/special_attack_min_range = 2 // The minimum distance required for an attempt to be made.
var/special_attack_max_range = 7 // The maximum for an attempt.
var/vision_range = 7 // How far the targeting system will look for things to kill. Note that values higher than 7 are 'offscreen' and might be unsporting.
var/can_breakthrough = TRUE // If false, the AI will not try to break things like windows or other structures in the way.
/**************
* Strategical *
**************/
// Step 1, find out what we can see.
/datum/ai_holder/proc/list_targets()
. = hearers(vision_range, holder) - src // Remove ourselves to prevent suicidal decisions.
var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha))
for(var/HM in typecache_filter_list(range(vision_range, holder), hostile_machines))
if(can_see(holder, HM, vision_range))
. += HM
// Step 2, filter down possible targets to things we actually care about.
/datum/ai_holder/proc/find_target(var/list/possible_targets, var/has_targets_list = FALSE)
. = list()
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
var/new_target = pick_target(.)
give_target(new_target)
return new_target
// 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.
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
if(!targets.len) // We found nothing.
return
var/chosen_target = pick(targets)
return chosen_target
// Step 4, give us our selected target.
/datum/ai_holder/proc/give_target(new_target)
target = new_target
//LosePatience()
if(target != null)
//GainPatience()
//Aggro()
if(should_threaten())
set_stance(STANCE_ALERT)
else
set_stance(STANCE_ATTACK)
return TRUE
/datum/ai_holder/proc/can_attack(atom/movable/the_target)
if(!the_target) // Nothing to attack.
return FALSE
if(holder.see_invisible < the_target.invisibility) // Invisible, can't see it, oh well.
return FALSE
if(isliving(the_target))
var/mob/living/L = the_target
if(L.stat)
return FALSE
if(holder.IIsAlly(L))
return FALSE
return TRUE
if(istype(the_target, /obj/mecha))
var/obj/mecha/M = the_target
if(M.occupant)
return can_attack(M.occupant)
if(istype(the_target, /obj/machinery/porta_turret))
var/obj/machinery/porta_turret/P = the_target
if(P.stat & BROKEN)
return FALSE // Already dead.
if(P.faction == holder.faction)
return FALSE // Don't shoot allied turrets.
if(!P.raised && !P.raising)
return FALSE // Turrets won't get hurt if they're still in their cover.
return TRUE
return FALSE
/***********
* Tactical *
***********/
// 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(!target || !can_attack(target) || (!(target in list_targets())) )
lose_target()
return
ai_log("engage_target() : Lost sight of target.", AI_LOG_TRACE)
lose_target() // We lost them.
if(!find_target()) // If we can't get a new one, then wait for a bit and then time out.
set_stance(STANCE_IDLE)
lost_target()
ai_log("engage_target() : No more targets. Exiting.", AI_LOG_DEBUG)
return
// if(lose_target_time + lose_target_timeout < world.time)
// ai_log("engage_target() : Unseen enemy timed out.", AI_LOG_TRACE)
// set_stance(STANCE_IDLE) // It must've been the wind.
// lost_target()
// ai_log("engage_target() : Exiting.", AI_LOG_DEBUG)
// return
// // But maybe we do one last ditch effort.
// if(!target_last_seen_turf || intelligence_level < AI_SMART)
// ai_log("engage_target() : No last known position or is too dumb to fight unseen enemies.", AI_LOG_TRACE)
// set_stance(STANCE_IDLE)
// else
// ai_log("engage_target() : Fighting unseen enemy.", AI_LOG_TRACE)
// engage_unseen_enemy()
else
ai_log("engage_target() : Got new target ([target]).", AI_LOG_TRACE)
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
// Stab them.
if(distance <= 1 && !pointblank)
request_help() // Call our allies.
// Do a 'special' attack, if one is allowed.
if(prob(special_attack_prob) && (distance >= special_attack_min_range) && (distance <= special_attack_max_range))
ai_log("engage_target() : Attempting a special attack.", AI_LOG_TRACE)
on_engagement(target)
if(attack_cooldown_left <= 0)
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
pre_melee_attack(target)
if(melee_attack(target))
post_melee_attack(target)
if(attack_cooldown)
attack_cooldown_left = attack_cooldown
// Stab them.
else if(distance <= 1 && !pointblank)
ai_log("engage_target() : Attempting a melee attack.", AI_LOG_TRACE)
on_engagement(target)
melee_attack(target)
// Shoot them.
else if(ranged && (distance <= shoot_range) )
on_engagement(target)
if(attack_cooldown_left <= 0)
if(firing_lanes && !test_projectile_safety(target))
// Nudge them a bit, maybe they can shoot next time.
step_rand(holder)
holder.face_atom(target)
ai_log("engage_target() : Could not safely fire at target. Exiting.", AI_LOG_DEBUG)
return
if(firing_lanes && !test_projectile_safety(target))
// Nudge them a bit, maybe they can shoot next time.
step_rand(holder)
holder.face_atom(target)
return
pre_ranged_attack(target)
if(ranged_attack(target))
post_ranged_attack(target)
if(attack_cooldown)
attack_cooldown_left = attack_cooldown
ai_log("engage_target() : Attempting a ranged attack.", AI_LOG_TRACE)
ranged_attack(target)
// Run after them.
else
set_stance(STANCE_ATTACK)
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/movable/AM)
@@ -162,19 +93,15 @@
/datum/ai_holder/proc/ranged_attack(atom/movable/AM)
return holder.IRangedAttack(AM)
// Called when within striking distance, however cooldown is not considered.
// Most mobs probably won't have this defined but we don't care.
/datum/ai_holder/proc/special_attack(atom/movable/AM)
return holder.ISpecialAttack(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/movable/AM)
// These two are called before an attack attempt.
/datum/ai_holder/proc/pre_melee_attack(atom/movable/AM)
/datum/ai_holder/proc/pre_ranged_attack(atom/movable/AM)
// These two are called after a successful(ish) attack.
/datum/ai_holder/proc/post_melee_attack(atom/movable/AM)
/datum/ai_holder/proc/post_ranged_attack(atom/movable/AM)
// 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)
var/mob/living/L = check_trajectory(AM, holder) // This isn't always reliable but its better than the previous method.
@@ -195,17 +122,104 @@
return !conserve_ammo // If we have infinite ammo than shooting the wall isn't so bad, but otherwise lets not.
//We can't see the target
/datum/ai_holder/proc/lose_target()
target = null
set_stance(STANCE_IDLE)
give_up_movement()
//Target is no longer valid (?)
/datum/ai_holder/proc/lost_target()
set_stance(STANCE_IDLE)
give_up_movement()
// 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(ranged && distance <= shoot_range)
return TRUE // Can shoot.
return FALSE
// Can be used to conditionally do a ranged or melee attack.
/datum/ai_holder/proc/closest_distance()
return ranged ? shoot_range - 1 : 1 // Shoot range -1 just because we don't want to constantly get kited
// 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(!target || !can_attack(target))
ai_log("walk_to_target() : Lost target.", AI_LOG_INFO)
if(!find_target())
lost_target()
ai_log("walk_to_target() : Exiting.", AI_LOG_DEBUG)
return
else
ai_log("walk_to_target() : Found new target ([target]).", AI_LOG_INFO)
// Find out where we're going.
var/get_to = closest_distance()
var/distance = get_dist(holder, target)
ai_log("walk_to_target() : get_to is [get_to].", AI_LOG_TRACE)
// We're here!
if(distance <= get_to)
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.
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.
/datum/ai_holder/proc/breakthrough(atom/target_atom)
if(!can_breakthrough)
return FALSE
var/dir_to_target = get_dir(holder, target_atom)
holder.face_atom(target_atom)
// First, try to break things directly in front of us.
var/result = destroy_surroundings(dir_to_target)
// If that doesn't work, we might be trying to attack something diagonally.
// If so, we can try again with some adjustments to avoid invalid diagonal directions.
if(!result)
result = destroy_surroundings(turn(dir_to_target, 45))
// One last time, going the other way.
if(!result)
result = destroy_surroundings(turn(dir_to_target, -45))
// Welp.
return result
/datum/ai_holder/proc/destroy_surroundings(direction)
if(!direction)
direction = pick(cardinal) // FLAIL WILDLY
var/turf/problem_turf = get_step(holder, direction)
// First, kill windows in the way.
for(var/obj/structure/window/W in problem_turf)
if(W.dir == reverse_dir[holder.dir]) // So that windows get smashed in the right order
ai_log("destroy_surroundings() : Attacking diagonal window.", AI_LOG_INFO)
return holder.IAttack(W)
else if(W.is_fulltile())
ai_log("destroy_surroundings() : Attacking full tile window.", AI_LOG_INFO)
return holder.IAttack(W)
var/obj/structure/obstacle = locate(/obj/structure, problem_turf)
if(istype(obstacle, /obj/structure/window) || istype(obstacle, /obj/structure/closet) || istype(obstacle, /obj/structure/table) || istype(obstacle, /obj/structure/grille))
ai_log("destroy_surroundings() : Attacking generic structure.", AI_LOG_INFO)
return holder.IAttack(obstacle)
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 holder.IAttack(D)
return FALSE // Nothing to attack.
@@ -0,0 +1,43 @@
// Used for fighting invisible things.
// Used when a target is out of sight or invisible.
/datum/ai_holder/proc/engage_unseen_enemy()
// Lets do some last things before giving up.
if(!ranged)
if(get_dist(holder, target_last_seen_turf > 1)) // We last saw them over there.
// Go to where you last saw the enemy.
give_destination(target_last_seen_turf, 1, TRUE) // This will set it to STANCE_REPOSITION.
else // We last saw them next to us, so do a blind attack on that tile.
melee_on_tile(target_last_seen_turf)
else if(!conserve_ammo)
shoot_near_turf(target_last_seen_turf)
// This shoots semi-randomly near a specific turf.
/datum/ai_holder/proc/shoot_near_turf(turf/targeted_turf)
if(!ranged)
return // Can't shoot.
if(get_dist(holder, targeted_turf) > shoot_range)
return // Too far to shoot.
var/turf/T = pick(RANGE_TURFS(2, targeted_turf)) // The turf we're actually gonna shoot at.
on_engagement(T)
if(firing_lanes && !test_projectile_safety(T))
step_rand(holder)
holder.face_atom(T)
return
ranged_attack(T)
// Attempts to attack something on a specific tile.
// TODO: Put on mob/living?
/datum/ai_holder/proc/melee_on_tile(turf/T)
var/mob/living/L = locate() in T
if(!L)
T.visible_message("\The [holder] attacks nothing around \the [T].")
return
if(holder.IIsAlly(L)) // Don't hurt our ally.
return
melee_attack(L)
+53 -12
View File
@@ -1,6 +1,8 @@
// Contains code for speaking and emoting.
/datum/ai_holder
var/threaten = FALSE // If hostile and sees a valid target, gives a 'warning' to the target before beginning the attack.
var/threatened = FALSE // If the mob actually gave the warning, checked so it doesn't constantly yell every tick.
var/threatening = FALSE // If the mob actually gave the warning, checked so it doesn't constantly yell every tick.
var/threaten_delay = 3 SECONDS // How long a 'threat' lasts, until actual fighting starts. If null, the mob never starts the fight but still does the threat.
var/threaten_timeout = 1 MINUTE // If the mob threatens someone, they leave, and then come back before this timeout period, the mob escalates to fighting immediately.
var/last_conflict_time = null // Last occurance of threatening or fighting being used, in world.time.
@@ -8,13 +10,21 @@
var/threaten_sound = null // Sound file played when the mob calls threaten_target() for the first time.
var/stand_down_sound = null // Sound file played when the mob loses sight of the threatened target.
var/speak_chance = 0 // Probability that the mob talks (this is 'X in 200' chance since even 1/100 is pretty noisy)
var/reacts = 0 // Reacts to some things being said.
var/datum/say_list/say_list = null // Datum containing all of our lines.
var/say_list_type = /datum/say_list // Type to give us on initialization. Default has empty lists, so the mob will be silent.
/datum/ai_holder/New()
..()
say_list = new say_list_type()
/datum/ai_holder/proc/should_threaten()
if(!threaten)
return FALSE // We don't negotiate.
if(!will_threaten(target))
return FALSE // Pointless to threaten an animal, a mindless drone, or an object.
if(!(stance in list(STANCE_IDLE, STANCE_MOVE, STANCE_FOLLOW)))
if(stance in STANCES_COMBAT)
return FALSE // We're probably already fighting or recently fought if not in these stances.
if(last_conflict_time && threaten_delay && last_conflict_time + threaten_timeout > world.time)
return FALSE // We threatened someone recently, so lets show them we mean business.
@@ -23,27 +33,27 @@
/datum/ai_holder/proc/threaten_target()
holder.face_atom(target) // Constantly face the target.
if(!threatened) // First tick.
threatened = TRUE
if(!threatening) // First tick.
threatening = TRUE
last_conflict_time = world.time
//TODO: Actual speech.
holder.say("Oi, fuck off mate.")
holder.say(safepick(say_list.say_threaten))
playsound(holder.loc, threaten_sound, 75, 1) // We do this twice to make the sound -very- noticable to the target.
playsound(target.loc, threaten_sound, 75, 1) // Actual aim-mode also does that so at least it's consistant.
else // Otherwise we are waiting for them to go away or to wait long enough for escalate.
if(target in list_targets()) // Are they still visible?
if(threaten_delay && last_conflict_time + threaten_delay < world.time) // Waited too long.
threatened = FALSE
set_stance(STANCE_ATTACK)
holder.say("Fine, now you die!") //WIP
threatening = FALSE
set_stance(STANCE_APPROACH)
holder.say(safepick(say_list.say_escalate))
else
return // Wait a bit.
else // They left, or so we think.
threatened = FALSE
threatening = FALSE
set_stance(STANCE_IDLE)
holder.say("Good riddence.") //WIP
holder.say(safepick(say_list.say_stand_down))
playsound(holder.loc, stand_down_sound, 50, 1) // We do this twice to make the sound -very- noticable to the target.
playsound(target.loc, stand_down_sound, 50, 1) // Actual aim-mode also does that so at least it's consistant.
@@ -56,4 +66,35 @@
var/mob/living/simple_animal/them = target
if(them.intelligence_level < us.intelligence_level) // Todo: Bitflag these.
return FALSE // Humanoids don't care about drones/animals/etc. Drones don't care about animals, and so on.
return TRUE
return TRUE
// Temp defines to make the below code a bit more readable.
#define COMM_SAY "say"
#define COMM_AUDIBLE_EMOTE "audible emote"
#define COMM_VISUAL_EMOTE "visual emote"
/datum/ai_holder/proc/handle_idle_speaking()
if(rand(0,200) < speak_chance)
var/list/comm_types = list() // What kinds of things can we do?
if(say_list.speak.len)
comm_types += COMM_SAY
if(say_list.emote_hear.len)
comm_types += COMM_AUDIBLE_EMOTE
if(say_list.emote_see.len)
comm_types += COMM_VISUAL_EMOTE
if(!comm_types.len)
return // All the relevant lists are empty, so do nothing.
switch(pick(comm_types))
if(COMM_SAY)
holder.say(safepick(say_list.speak))
if(COMM_AUDIBLE_EMOTE)
holder.audible_emote(safepick(say_list.emote_hear))
if(COMM_VISUAL_EMOTE)
holder.visible_emote(safepick(say_list.emote_see))
#undef COMM_SAY
#undef COMM_AUDIBLE_EMOTE
#undef COMM_VISUAL_EMOTE
+110
View File
@@ -0,0 +1,110 @@
// Involves cooperating with other ai_holders.
/datum/ai_holder
var/cooperative = FALSE // If true, asks allies to help when fighting something.
var/call_distance = 14 // How far away calls for help will go for.
var/last_helpask_time = 0 // world.time when a mob asked for help.
var/list/faction_friends = list() // List of all mobs inside the faction with ai_holders that have cooperate on, to call for help without using range().
// Note that this is only used for sending calls out. Receiving calls doesn't care about this list, only if the mob is in the faction.
// This means the AI could respond to a player's call for help, if a way to do so was implemented.
// These vars don't do anything currently. They did before but an optimization made them nonfunctional.
// It was probably worth it.
var/call_players = FALSE // (Currently nonfunctional) If true, players get notified of an allied mob calling for help.
var/called_player_message = "needs help!" // (Currently nonfunctional) Part of a message used when above var is true. Full message is "\The [holder] [called_player_message]"
/datum/ai_holder/New(new_holder)
..()
if(cooperative)
build_faction_friends()
/datum/ai_holder/Destroy()
if(faction_friends.len) //This list is shared amongst the faction
faction_friends -= src
return ..()
// Handles everything about that list.
// Call on initialization or if something weird happened like the mob switched factions.
/datum/ai_holder/proc/build_faction_friends()
if(faction_friends.len) // Already have a list.
// Assume we're moving to a new faction.
faction_friends -= src // Get us out of the current list shared by everyone else.
faction_friends = list() // Then make our list empty and unshared in case we become a loner.
// Find another AI-controlled mob in the same faction if possible.
var/mob/living/first_friend
for(var/mob/living/L in living_mob_list)
if(L.faction == holder.faction && L.ai_holder)
first_friend = L
break
if(first_friend) // Joining an already established faction.
faction_friends = first_friend.ai_holder.faction_friends
faction_friends |= holder
else // We're the 'founder' (first and/or only member) of this faction.
faction_friends |= holder
// Requests help in combat from other mobs possessing ai_holders.
/datum/ai_holder/proc/request_help()
ai_log("request_help() : Entering.", AI_LOG_DEBUG)
if(!cooperative || ((world.time - last_helpask_time) < 10 SECONDS))
return
ai_log("request_help() : Asking for help.", AI_LOG_INFO)
last_helpask_time = world.time
// for(var/mob/living/L in range(call_distance, holder))
for(var/mob/living/L in faction_friends)
if(L == holder) // Lets not call ourselves.
continue
if(get_dist(L, holder) > call_distance) // Too far to 'hear' the call for help.
continue
if(holder.IIsAlly(L))
// This will currently never run sadly, until faction_friends is made to accept players too.
// That might be for the best since I can imagine it getting spammy in a big fight.
if(L.client && call_players) // Dealing with a player.
ai_log("request_help() : Asking [L] (Player) for help.", AI_LOG_INFO)
to_chat(L, "<span class='critical'>\The [holder] [called_player_message]</span>")
else if(L.ai_holder) // Dealing with an AI.
ai_log("request_help() : Asking [L] (AI) for help.", AI_LOG_INFO)
L.ai_holder.help_requested(holder)
ai_log("request_help() : Exiting.", AI_LOG_DEBUG)
// What allies receive when someone else is calling for help.
/datum/ai_holder/proc/help_requested(mob/living/friend)
ai_log("help_requested() : Entering.", AI_LOG_DEBUG)
if(stance == STANCE_SLEEP)
ai_log("help_requested() : Help requested by [friend] but we are asleep.", AI_LOG_INFO)
return
if(!cooperative)
ai_log("help_requested() : Help requested by [friend] but we're not cooperative.", AI_LOG_INFO)
return
if(stance in STANCES_COMBAT)
ai_log("help_requested() : Help requested by [friend] but we are busy fighting something else.", AI_LOG_INFO)
return
if(!can_act())
ai_log("help_requested() : Help requested by [friend] but cannot act (stunned or dead).", AI_LOG_INFO)
return
if(!holder.IIsAlly(friend)) // Extra sanity.
ai_log("help_requested() : Help requested by [friend] but we hate them.", AI_LOG_INFO)
return
if(get_dist(holder, friend) <= follow_distance)
ai_log("help_requested() : Help requested by [friend] but we're already here.", AI_LOG_INFO)
return
if(get_dist(holder, friend) <= vision_range) // Within our sight.
ai_log("help_requested() : Help requested by [friend], and within target sharing range.", AI_LOG_INFO)
if(friend.ai_holder) // AI calling for help.
if(friend.ai_holder.target && can_attack(friend.ai_holder.target)) // Friend wants us to attack their target.
last_conflict_time = world.time // So we attack immediately and not threaten.
give_target(friend.ai_holder.target) // This will set us to the appropiate stance.
ai_log("help_requested() : Given target [target] by [friend]. Exiting", AI_LOG_DEBUG)
return
// Otherwise they're outside our sight, lack a target, or aren't AI controlled, but within call range.
// So assuming we're AI controlled, we'll go to them and see whats wrong.
ai_log("help_requested() : Help requested by [friend], going to go to friend.", AI_LOG_INFO)
set_follow(friend, 10 SECONDS)
ai_log("help_requested() : Exiting.", AI_LOG_DEBUG)
+94 -6
View File
@@ -1,38 +1,126 @@
/datum/ai_holder
var/path_display = FALSE // Displays a visual path when A* is being used.
var/path_icon = 'icons/misc/debug_group.dmi' // What icon to use for the overlay
var/path_icon_state = "red" // What state to use for the overlay
var/image/path_overlay // A reference to the overlay
// Contains settings to make it easier to debug things.
/datum/ai_holder
var/path_display = FALSE // Displays a visual path when A* is being used.
var/path_icon = 'icons/misc/debug_group.dmi' // What icon to use for the overlay
var/path_icon_state = "red" // What state to use for the overlay
var/image/path_overlay // A reference to the overlay
var/last_turf_display = FALSE // Similar to above, but shows the target's last known turf visually.
var/last_turf_icon_state = "green" // A seperate icon_state from the previous.
var/image/last_turf_overlay // Another reference for an overlay.
var/stance_coloring = FALSE // Colors the mob depending on its stance.
var/debug_ai = AI_LOG_OFF // The level of debugging information to display to people who can see log_debug().
/*
#define AI_LOG_OFF 0 // Don't show anything.
#define AI_LOG_ERROR 1 // Show logs of things likely causing the mob to not be functioning correctly.
#define AI_LOG_WARNING 2 // Show less serious but still helpful to know issues that might be causing things to work incorrectly.
#define AI_LOG_INFO 3 // Important regular events, like selecting a target or switching stances.
#define AI_LOG_DEBUG 4 // More detailed information about the flow of execution.
#define AI_LOG_TRACE 5 // Even more detailed than the last.
*/
/datum/ai_holder/New()
..()
path_overlay = new(path_icon,path_icon_state)
last_turf_overlay = new(path_icon, last_turf_icon_state)
/datum/ai_holder/Destroy()
path_overlay = null
last_turf_overlay = null
return ..()
//For debug purposes!
/datum/ai_holder/proc/ai_log_output(var/msg = "missing message", var/ver = AI_LOG_INFO)
var/span_type
switch(ver)
if(AI_LOG_OFF)
return
if(AI_LOG_ERROR)
span_type = "debug_error"
if(AI_LOG_WARNING)
span_type = "debug_warning"
if(AI_LOG_INFO)
span_type = "debug_info"
if(AI_LOG_DEBUG)
span_type = "debug_debug" // RAS syndrome at work.
if(AI_LOG_TRACE)
span_type = "debug_trace"
if(ver <= debug_ai)
log_debug("<span class='[span_type]'>AI: ([holder]:\ref[holder] | [holder.x],[holder.y],[holder.z])(@[world.time]): [msg] </span>")
// Colors the mob based on stance, to visually tell what stance it is for debugging.
// Probably not something you want for regular use.
/datum/ai_holder/proc/stance_color()
var/new_color = null
switch(stance)
if(STANCE_SLEEP)
new_color = "#FFFFFF" // White
if(STANCE_IDLE)
new_color = "#00FF00" // Green
if(STANCE_ALERT)
new_color = "#FFFF00" // Yellow
if(STANCE_APPROACH)
new_color = "#FF9933" // Orange
if(STANCE_FIGHT)
new_color = "#FF0000" // Red
if(STANCE_MOVE)
new_color = "#0000FF" // Blue
if(STANCE_REPOSITION)
new_color = "#FF00FF" // Purple
if(STANCE_FOLLOW)
new_color = "#00FFFF" // Cyan
if(STANCE_FLEE)
new_color = "#666666" // Grey
if(STANCE_STUNNED)
new_color = "#000000" // Black
holder.color = new_color
// Turns on all the debugging stuff.
/datum/ai_holder/proc/debug()
stance_coloring = TRUE
path_display = TRUE
last_turf_display = TRUE
debug_ai = AI_LOG_INFO
// Remove this when finished.
/mob/living/simple_animal/corgi
ai_holder_type = /datum/ai_holder/test
/datum/ai_holder/test
hostile = TRUE
use_astar = TRUE
/datum/ai_holder/hostile/ranged
ranged = TRUE
cooperative = TRUE
firing_lanes = TRUE
conserve_ammo = TRUE
threaten = TRUE
wander = TRUE
threaten_sound = 'sound/weapons/TargetOn.ogg'
stand_down_sound = 'sound/weapons/TargetOff.ogg'
/datum/ai_holder/hostile/ranged/debug
wander = FALSE
conserve_ammo = FALSE
intelligence_level = AI_SMART
stance_coloring = TRUE
path_display = TRUE
last_turf_display = TRUE
debug_ai = AI_LOG_INFO
say_list_type = /datum/say_list/pirate
/mob/living/simple_animal/hostile/pirate
hostile = FALSE
ai_inactive = TRUE
ai_holder_type = /datum/ai_holder/hostile/ranged
ai_holder_type = /datum/ai_holder/hostile/ranged/debug
/datum/ai_holder/hostile/ranged/robust/on_engagement(atom/movable/AM)
step_rand(holder)
+38
View File
@@ -0,0 +1,38 @@
// This code handles what to do inside STANCE_FLEE.
/datum/ai_holder
var/can_flee = TRUE // If they're even allowed to flee.
var/flee_when_dying = TRUE // If they should flee when low on health.
var/dying_threshold = 0.3 // How low on health the holder needs to be before fleeing. Defaults to 30% or lower health.
var/flee_when_outmatched = FALSE // If they should flee upon reaching a specific tension threshold.
var/outmatched_threshold = 200 // The tension threshold needed for a mob to decide it should run away.
/datum/ai_holder/proc/should_flee(force = FALSE)
if(holder.has_modifier_of_type(/datum/modifier/berserk)) // Berserked mobs will never flee, even if 'forced' to.
return FALSE
if(force)
return TRUE
if(can_flee)
if(!hostile)
return TRUE // We're not hostile and someone attacked us first.
if(flee_when_dying && (holder.health / holder.getMaxHealth()) <= dying_threshold)
return TRUE // We're gonna die!
else if(flee_when_outmatched && holder.get_tension() >= outmatched_threshold)
return TRUE // We're fighting something way way stronger then us.
return FALSE
/datum/ai_holder/proc/flee_from_target()
ai_log("flee_from_target() : Entering.", AI_LOG_DEBUG)
if(!target || !can_attack(target)) // can_attack() is used since it checks the same things we would need to anyways.
ai_log("flee_from_target() : Lost target to flee from.", AI_LOG_INFO)
lose_target()
ai_log("flee_from_target() : Exiting.", AI_LOG_DEBUG)
return
ai_log("flee_from_target() : Stepping away.", AI_LOG_TRACE)
step_away(holder, target, vision_range)
ai_log("flee_from_target() : Exiting.", AI_LOG_DEBUG)
+60
View File
@@ -0,0 +1,60 @@
// This handles following a specific atom/movable, without violently murdering it.
/datum/ai_holder
// Following.
var/atom/movable/leader = null // The movable atom that the mob wants to follow.
var/follow_distance = 2 // How far leader must be to start moving towards them.
var/follow_until_time = 0 // world.time when the mob will stop following leader. 0 means it won't time out.
/datum/ai_holder/proc/walk_to_leader()
ai_log("walk_to_leader() : Entering.",AI_LOG_DEBUG)
if(!leader)
ai_log("walk_to_leader() : No leader.", AI_LOG_WARNING)
forget_path()
set_stance(STANCE_IDLE)
ai_log("walk_to_leader() : Exiting.", AI_LOG_DEBUG)
return
// Did we time out?
if(follow_until_time && world.time > follow_until_time)
ai_log("walk_to_leader() : Follow timed out, losing leader.", AI_LOG_INFO)
lose_follow()
set_stance(STANCE_IDLE)
ai_log("walk_to_leader() : Exiting.", AI_LOG_DEBUG)
return
var/get_to = follow_distance
var/distance = get_dist(holder, leader)
ai_log("walk_to_leader() : get_to is [get_to].", AI_LOG_TRACE)
// We're here!
if(distance <= get_to)
give_up_movement()
set_stance(STANCE_IDLE)
ai_log("walk_to_leader() : Within range, exiting.", AI_LOG_INFO)
return
ai_log("walk_to_leader() : Walking.", AI_LOG_TRACE)
walk_path(leader, get_to)
ai_log("walk_to_leader() : Exiting.",AI_LOG_DEBUG)
/datum/ai_holder/proc/set_follow(mob/living/L, follow_for = 0)
ai_log("set_follow() : Entered.", AI_LOG_DEBUG)
if(!L)
ai_log("set_follow() : Was told to follow a nonexistant mob.", AI_LOG_ERROR)
return FALSE
leader = L
follow_until_time = !follow_for ? 0 : world.time + follow_for
ai_log("set_follow() : Exited.", AI_LOG_DEBUG)
return TRUE
/datum/ai_holder/proc/lose_follow()
ai_log("lose_follow() : Entered.", AI_LOG_DEBUG)
ai_log("lose_follow() : Going to lose leader [leader].", AI_LOG_INFO)
leader = null
give_up_movement()
ai_log("lose_follow() : Exited.", AI_LOG_DEBUG)
/datum/ai_holder/proc/should_follow_leader()
return leader && get_dist(holder, leader) > follow_distance
+94 -113
View File
@@ -1,147 +1,108 @@
/datum/ai_holder
var/use_astar = FALSE // Do we use the more expensive A* implementation or stick with BYOND's default step_to()?
var/using_astar = FALSE // Are we currently using an A* path?
var/list/path = list() // A list of tiles that A* gave us as a solution to reach the target.
var/list/obstacles = list() // Things A* will try to avoid.
var/astar_adjacent_proc = /turf/proc/CardinalTurfsWithAccess // Proc to use when A* pathfinding. Default makes them bound to cardinals.
var/failed_steps = 0 // If move_once() fails to move the mob onto the correct tile, this increases. When it reaches 3, the path is recalc'd since they're probably stuck.
// General.
var/turf/destination = null // The targeted tile the mob wants to walk to.
var/min_distance_to_destination = 1 // Holds how close the mob should go to destination until they're done.
// Home.
var/turf/home_turf = null // The mob's 'home' turf. It will try to stay near it if told to do so.
var/return_home = FALSE // If true, makes the mob go to its 'home' if it strays too far.
var/returns_home = FALSE // If true, makes the mob go to its 'home' if it strays too far.
var/max_home_distance = 3 // How far the mob can go away from its home before being told to go_home().
/**************
* Strategical *
**************/
// Wandering.
var/wander = FALSE // If true, the mob will randomly move in the four cardinal directions when idle.
var/wander_delay = 0 // How many ticks until the mob can move a tile in handle_wander_movement().
var/base_wander_delay = 2 // What the above var gets set to when it wanders. Note that a tick happens every half a second.
var/wander_when_pulled = FALSE // If the mob will refrain from wandering if someone is pulling it.
//Giving up on moving
/datum/ai_holder/proc/give_up_movement()
// ai_log("GiveUpMoving()",1)
forget_path()
// stop_automated_movement = 0
//Forget the path entirely
/datum/ai_holder/proc/forget_path()
// ai_log("ForgetPath()",2)
if(path_display)
for(var/turf/T in path)
T.overlays -= path_overlay
using_astar = FALSE
// walk_list.Cut()
path.Cut()
/datum/ai_holder/proc/calculate_path(atom/A, get_to = 1)
if(!A)
/datum/ai_holder/proc/walk_to_destination()
ai_log("walk_to_destination() : Entering.",AI_LOG_DEBUG)
if(!destination)
ai_log("walk_to_destination() : No destination.", AI_LOG_WARNING)
forget_path()
set_stance(stance == STANCE_REPOSITION ? STANCE_APPROACH : STANCE_IDLE)
ai_log("walk_to_destination() : Exiting.", AI_LOG_DEBUG)
return
if(!use_astar) // If we don't use A* then this is pointless.
return
get_path(get_turf(A), get_to)
//A* now, try to a path to a target
/datum/ai_holder/proc/get_path(var/turf/target,var/get_to = 1, var/max_distance = world.view*6)
// ai_log("GetPath([target],[get_to],[max_distance])",2)
forget_path()
var/list/new_path = AStar(get_turf(holder.loc), target, astar_adjacent_proc, /turf/proc/Distance, min_target_dist = get_to, max_node_depth = max_distance, id = holder.IGetID(), exclude = obstacles)
if(new_path && new_path.len)
path = new_path
if(path_display)
for(var/turf/T in path)
T.overlays |= path_overlay
else
return 0
return path.len
/*
/datum/ai_holder/proc/walk_to_target()
//If we were chasing someone and we can't anymore, give up.
if(!target_mob)
// ai_log("MoveToTarget() Losing target at top",2)
lose_target()
return
//We recompute our path every time we're called if we can still see them
if(target in list_targets(vision_range))
if(using_astar)
forget_path()
// Find out where we're going.
var/get_to = 1 // TODO
var/distance = get_dist(holder, target)
//We're here!
if(distance <= get_to)
// ai_log("MoveToTarget() [src] attack range",2)
set_stance(STANCE_ATTACKING)
return
//We're just setting out, making a new path, or we can't path with A*
if(!path.len)
// ai_log("SA: MoveToTarget() pathing to [target_mob]",2)
//GetPath failed for whatever reason, just smash into things towards them
if(run_at_them || !GetPath(get_turf(target_mob),get_to))
//We try the built-in way to stay close
walk_to(src, target_mob, get_to, move_to_delay)
// ai_log("MoveToTarget() walk_to([src],[target_mob],[get_to],[move_to_delay])",3)
//Break shit in their direction! LEME SMAHSH
var/dir_to_mob = get_dir(src,target_mob)
face_atom(target_mob)
// DestroySurroundings(dir_to_mob)
// ai_log("MoveToTarget() DestroySurroundings([get_dir(src,target_mob)])",3)
*/
/***********
* Tactical *
***********/
// Goes to the target, to attack them.
// Called when in STANCE_ATTACK.
/datum/ai_holder/proc/walk_to_target()
// Make sure we can still chase/attack them.
if(!target || !can_attack(target))
lose_target()
return
// Find out where we're going.
var/get_to = closest_distance()
var/distance = get_dist(holder, target)
var/get_to = min_distance_to_destination
var/distance = get_dist(holder, destination)
ai_log("walk_to_destination() : get_to is [get_to].", AI_LOG_TRACE)
// We're here!
if(distance <= get_to)
// ai_log("MoveToTarget() [src] attack range",2)
forget_path()
set_stance(STANCE_ATTACKING)
give_up_movement()
set_stance(stance == STANCE_REPOSITION ? STANCE_APPROACH : STANCE_IDLE)
ai_log("walk_to_destination() : Destination reached. Exiting.", AI_LOG_INFO)
return
// Otherwise keep walking.
walk_path(target, get_to)
ai_log("walk_to_destination() : Walking.", AI_LOG_TRACE)
walk_path(destination, get_to)
ai_log("walk_to_destination() : Exiting.",AI_LOG_DEBUG)
/datum/ai_holder/proc/should_go_home()
return (returns_home && home_turf) && (get_dist(holder, home_turf) > max_home_distance)
/datum/ai_holder/proc/go_home()
if(home_turf)
ai_log("go_home() : Telling holder to go home.", AI_LOG_INFO)
lose_follow() // So they don't try to path back and forth.
give_destination(home_turf, max_home_distance)
else
ai_log("go_home() : Told to go home without home_turf.", AI_LOG_ERROR)
/datum/ai_holder/proc/give_destination(turf/new_destination, min_distance = 1, combat = FALSE)
ai_log("give_destination() : Entering.", AI_LOG_DEBUG)
destination = new_destination
min_distance_to_destination = min_distance
if(new_destination != null)
ai_log("give_destination() : Going to new destination.", AI_LOG_TRACE)
set_stance(combat ? STANCE_REPOSITION : STANCE_MOVE)
return TRUE
else
ai_log("give_destination() : Given null destination.", AI_LOG_ERROR)
ai_log("give_destination() : Exiting.", AI_LOG_DEBUG)
// Walk towards whatever.
/datum/ai_holder/proc/walk_path(atom/A, get_to = 1)
ai_log("walk_path() : Entered.", AI_LOG_DEBUG)
var/turf/pre_step_turf = get_turf(holder)
if(use_astar)
if(!path.len) // If we're missing a path, make a new one.
ai_log("walk_path() : No path. Attempting to calculate path.", AI_LOG_TRACE)
calculate_path(A, get_to)
if(!path.len) // If we still don't have one, then the target's probably somewhere inaccessible to us.
if(!path.len) // If we still don't have one, then the target's probably somewhere inaccessible to us. Get as close as we can.
ai_log("walk_path() : Failed to obtain path to target. Using step_to() instead.", AI_LOG_INFO)
step_to(holder, A)
if(get_turf(holder) == pre_step_turf)
breakthrough(A) // We failed to move, time to smash things.
return
if(!move_once()) // Start walking the path.
ai_log("walk_path() : Failed to step.", AI_LOG_TRACE)
++failed_steps
if(failed_steps > 3) // We're probably stuck.
ai_log("walk_path() : Too many failed_steps.", AI_LOG_INFO)
forget_path() // So lets try again with a new path.
failed_steps = 0
else
step_to(holder, A)
if(get_turf(holder) == pre_step_turf)
breakthrough(A) // We failed to move, time to smash things.
ai_log("walk_path() : Exited.", AI_LOG_DEBUG)
//Take one step along a path
/datum/ai_holder/proc/move_once()
ai_log("move_once() : Entered.", AI_LOG_DEBUG)
if(!path.len)
return
@@ -151,9 +112,29 @@
step_towards(holder, src.path[1])
if(holder.loc != src.path[1])
// ai_log("MoveOnce() step_towards returning 0",3)
ai_log("move_once() : Failed step. Exiting.", AI_LOG_TRACE)
return FALSE
else
path -= src.path[1]
// ai_log("MoveOnce() step_towards returning 1",3)
return TRUE
ai_log("move_once() : Successful step. Exiting.", AI_LOG_TRACE)
return TRUE
/datum/ai_holder/proc/should_wander()
return wander && !leader
// Wanders randomly in cardinal directions.
/datum/ai_holder/proc/handle_wander_movement()
ai_log("handle_wander_movement() : Entered.", AI_LOG_DEBUG)
if(isturf(holder.loc) && can_act())
wander_delay--
if(wander_delay <= 0)
if(!wander_when_pulled && holder.pulledby)
ai_log("handle_wander_movement() : Being pulled and cannot wander. Exiting.", AI_LOG_DEBUG)
return
var/moving_to = 0 // Apparently this is required or it always picks 4, according to the previous developer for simplemob AI.
moving_to = pick(cardinal)
holder.set_dir(moving_to)
holder.Move(get_step(holder,moving_to))
wander_delay = base_wander_delay
ai_log("handle_wander_movement() : Exited.", AI_LOG_DEBUG)
+58
View File
@@ -0,0 +1,58 @@
// This handles obtaining a (usually A*) path towards something, such as a target, destination, or leader.
// This interacts heavily with code inside ai_holder_movement.dm
/datum/ai_holder
// Pathfinding.
var/use_astar = FALSE // Do we use the more expensive A* implementation or stick with BYOND's default step_to()?
var/list/path = list() // A list of tiles that A* gave us as a solution to reach the target.
var/list/obstacles = list() // Things A* will try to avoid.
var/astar_adjacent_proc = /turf/proc/CardinalTurfsWithAccess // Proc to use when A* pathfinding. Default makes them bound to cardinals.
var/failed_steps = 0 // If move_once() fails to move the mob onto the correct tile, this increases. When it reaches 3, the path is recalc'd since they're probably stuck.
// This clears the stored A* path.
/datum/ai_holder/proc/forget_path()
ai_log("forget_path() : Entering.", AI_LOG_DEBUG)
if(path_display)
for(var/turf/T in path)
T.overlays -= path_overlay
path.Cut()
ai_log("forget_path() : Exiting.", AI_LOG_DEBUG)
/datum/ai_holder/proc/give_up_movement()
ai_log("give_up_movement() : Entering.", AI_LOG_DEBUG)
forget_path()
destination = null
ai_log("give_up_movement() : Exiting.", AI_LOG_DEBUG)
/datum/ai_holder/proc/calculate_path(atom/A, get_to = 1)
ai_log("calculate_path([A],[get_to]) : Entering.", AI_LOG_DEBUG)
if(!A)
ai_log("calculate_path() : Called without an atom. Exiting.",AI_LOG_WARNING)
return
if(!use_astar) // If we don't use A* then this is pointless.
ai_log("calculate_path() : Not using A*, Exiting.", AI_LOG_DEBUG)
return
get_path(get_turf(A), get_to)
ai_log("calculate_path() : Exiting.", AI_LOG_DEBUG)
//A* now, try to a path to a target
/datum/ai_holder/proc/get_path(var/turf/target,var/get_to = 1, var/max_distance = world.view*6)
ai_log("get_path() : Entering.",AI_LOG_DEBUG)
forget_path()
var/list/new_path = AStar(get_turf(holder.loc), target, astar_adjacent_proc, /turf/proc/Distance, min_target_dist = get_to, max_node_depth = max_distance, id = holder.IGetID(), exclude = obstacles)
if(new_path && new_path.len)
path = new_path
ai_log("get_path() : Made new path.",AI_LOG_DEBUG)
if(path_display)
for(var/turf/T in path)
T.overlays |= path_overlay
else
ai_log("get_path() : Failed to make new path. Exiting.",AI_LOG_DEBUG)
return 0
ai_log("get_path() : Exiting.", AI_LOG_DEBUG)
return path.len
+159
View File
@@ -0,0 +1,159 @@
// Used for assigning a target for attacking.
/datum/ai_holder
var/hostile = FALSE // Do we try to hurt others? Setting to false disables most combat processing.
var/retaliate = FALSE // Attacks whatever struck it first.
var/atom/movable/target = null // The thing (mob or object) we're trying to kill.
var/turf/target_last_seen_turf = null // Where the mob last observed the target being, used if the target disappears but the mob wants to keep fighting.
var/vision_range = 7 // How far the targeting system will look for things to kill. Note that values higher than 7 are 'offscreen' and might be unsporting.
var/lose_target_time = 0 // world.time when a target was lost.
var/lose_target_timeout = 5 SECONDS // How long until a mob 'times out' and stops trying to find the mob that disappeared.
// A lot of this is based off of /TG/'s AI code.
// Step 1, find out what we can see.
/datum/ai_holder/proc/list_targets()
. = hearers(vision_range, holder) - src // Remove ourselves to prevent suicidal decisions.
var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha))
for(var/HM in typecache_filter_list(range(vision_range, holder), hostile_machines))
if(can_see(holder, HM, vision_range))
. += HM
// Step 2, filter down possible targets to things we actually care about.
/datum/ai_holder/proc/find_target(var/list/possible_targets, var/has_targets_list = FALSE)
. = list()
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
var/new_target = pick_target(.)
give_target(new_target)
return new_target
// 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.
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
if(!targets.len) // We found nothing.
return
var/chosen_target = pick(targets)
return chosen_target
// Step 4, give us our selected target.
/datum/ai_holder/proc/give_target(new_target)
target = new_target
if(target != null)
if(should_threaten())
set_stance(STANCE_ALERT)
else
set_stance(STANCE_APPROACH)
return TRUE
/datum/ai_holder/proc/can_attack(atom/movable/the_target)
if(!can_see_target(the_target))
return FALSE
if(isliving(the_target))
var/mob/living/L = the_target
if(L.stat)
return FALSE
if(holder.IIsAlly(L))
return FALSE
return TRUE
if(istype(the_target, /obj/mecha))
var/obj/mecha/M = the_target
if(M.occupant)
return can_attack(M.occupant)
if(istype(the_target, /obj/machinery/porta_turret))
var/obj/machinery/porta_turret/P = the_target
if(P.stat & BROKEN)
return FALSE // Already dead.
if(P.faction == holder.faction)
return FALSE // Don't shoot allied turrets.
if(!P.raised && !P.raising)
return FALSE // Turrets won't get hurt if they're still in their cover.
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
//We can't see the target, go look or attack where they were last seen.
/datum/ai_holder/proc/lose_target()
if(target)
target = null
lose_target_time = world.time
give_up_movement()
//Target is no longer valid (?)
/datum/ai_holder/proc/lost_target()
set_stance(STANCE_IDLE)
lose_target_position()
lose_target()
// Check if target is visible to us.
/datum/ai_holder/proc/can_see_target(atom/movable/the_target, view_range = vision_range)
ai_log("can_see_target() : Entering.", AI_LOG_TRACE)
if(!the_target) // Nothing to target.
ai_log("can_see_target() : There is no target. Exiting.", AI_LOG_WARNING)
return FALSE
if(holder.see_invisible < the_target.invisibility) // Invisible, can't see it, oh well.
ai_log("can_see_target() : Target ([the_target]) was invisible to holder. Exiting.", AI_LOG_TRACE)
return FALSE
if(get_dist(holder, the_target) > view_range) // Too far away.
ai_log("can_see_target() : Target ([the_target]) was too far from holder. Exiting.", AI_LOG_TRACE)
return FALSE
if(!can_see(holder, the_target, view_range))
ai_log("can_see_target() : Target ([the_target]) failed can_see(). Exiting.", AI_LOG_TRACE)
return FALSE
ai_log("can_see_target() : Target ([the_target]) can be seen. Exiting.", AI_LOG_TRACE)
return TRUE
// Updates the last known position of the target.
/datum/ai_holder/proc/track_target_position()
if(!target)
lose_target_position()
if(last_turf_display && target_last_seen_turf)
target_last_seen_turf.overlays -= last_turf_overlay
target_last_seen_turf = get_turf(target)
if(last_turf_display)
target_last_seen_turf.overlays += last_turf_overlay
// Resets the last known position to null.
/datum/ai_holder/proc/lose_target_position()
if(last_turf_display && target_last_seen_turf)
target_last_seen_turf.overlays -= last_turf_overlay
ai_log("lose_target_position() : Last position is being reset.", AI_LOG_INFO)
target_last_seen_turf = null
+14 -8
View File
@@ -3,17 +3,23 @@
// since some actions work very differently between mob types (e.g. executing an attack as a simple animal compared to a human).
// The AI can just call holder.IAttack(target) and the mob is responsible for determining how to actually attack the target.
/mob/living/proc/IAttack(atom/movable/AM)
/mob/living/proc/IAttack(atom/A)
return FALSE
/mob/living/simple_animal/IAttack(atom/movable/AM)
target_mob = AM
return PunchTarget(AM) // TODO: Clean up on SA side.
/mob/living/simple_animal/IAttack(atom/A)
return attack_target(A)
/mob/living/proc/IRangedAttack(atom/movable/AM)
/mob/living/proc/IRangedAttack(atom/A)
return FALSE
/mob/living/simple_animal/IRangedAttack(atom/movable/AM)
target_mob = AM
return ShootTarget(AM, src.loc, src)
/mob/living/simple_animal/IRangedAttack(atom/A)
return shoot_target(A)
/mob/living/proc/ISpecialAttack(atom/A)
return FALSE
/mob/living/simple_animal/ISpecialAttack(atom/A)
return special_attack_target()
/mob/living/proc/ISay(message)
+34
View File
@@ -0,0 +1,34 @@
// A simple datum that just holds many lists of lines for AI mobs to pick from.
// This is its own datum in order to be able to have different types of mobs be able to use the same lines if desired,
// even when inheritence wouldn't be able to do so.
// Also note this also contains emotes, despite its name.
/datum/say_list
var/list/speak = list() // Things the mob might say if it talks while idle.
var/list/emote_hear = list() // Hearable emotes it might perform
var/list/emote_see = 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/say_understood = list() // When accepting an order.
var/list/say_cannot = list() // When they cannot comply.
var/list/say_maybe_target = list() // When they briefly see something.
var/list/say_got_target = list() // When a target is first assigned.
var/list/say_threaten = list() // When threatening someone.
var/list/say_stand_down = list() // When the threatened thing goes away.
var/list/say_escalate = list() // When the threatened thing doesn't go away.
// Subtypes.
// This one's pretty dumb, but pirates are dumb anyways and it makes for a good test.
/datum/say_list/pirate
speak = list("Yarr!")
say_understood = list("Alright, matey.")
say_cannot = list("No, matey.")
say_maybe_target = list("Eh?")
say_got_target = list("Yarrrr!")
say_threaten = list("You best leave, this booty is mine.", "No plank to walk on, just walk away.")
say_stand_down = list("Good.")
say_escalate = list("Yarr! The booty is mine!")
+1 -1
View File
@@ -17,7 +17,7 @@
//OTHER//
/////////
var/datum/preferences/prefs = null
var/move_delay = 1
//var/move_delay = 1
var/moving = null
var/adminobs = null
var/area = null
@@ -29,7 +29,7 @@ Berserk is a somewhat rare modifier to obtain freely (and for good reason), howe
- Red Slimes will berserk if they go rabid.
- Red slime core reactions will berserk slimes that can see the user in addition to making them go rabid.
- Red slime core reactions will berserk prometheans that can see the user.
- Bears will berserk when losing a fight.
- Saviks will berserk when losing a fight.
- Changelings can evolve a 2 point ability to use a changeling-specific variant of Berserk, that replaces the text with a 'we' variant.
Recursive Enhancement allows the changeling to instead used an improved variant that features less exhaustion time and less nutrition drain.
- Xenoarch artifacts may have forced berserking as one of their effects. This is especially fun if an artifact that makes hostile mobs is nearby.
+7
View File
@@ -46,6 +46,13 @@
if(O)
O.see_emote(src, message, m_type)
// Shortcuts for above proc
/mob/proc/visible_emote(var/act_desc)
custom_emote(1, act_desc)
/mob/proc/audible_emote(var/act_desc)
custom_emote(2, act_desc)
/mob/proc/emote_dead(var/message)
if(client.prefs.muted & MUTE_DEADCHAT)
@@ -21,6 +21,7 @@
var/list/atom/hallucinations = list() //A list of hallucinated people that try to attack the mob. See /obj/effect/fake_attacker in hallucinations.dm
var/last_special = 0 //Used by the resist verb, likely used to prevent players from bypassing next_move by logging in/out.
var/base_attack_cooldown = DEFAULT_ATTACK_COOLDOWN
var/t_phoron = null
var/t_oxygen = null
@@ -555,12 +555,6 @@
if(act)
..(act, type, desc)
/mob/living/simple_animal/proc/visible_emote(var/act_desc)
custom_emote(1, act_desc)
/mob/living/simple_animal/proc/audible_emote(var/act_desc)
custom_emote(2, act_desc)
/mob/living/simple_animal/bullet_act(var/obj/item/projectile/Proj)
ai_log("bullet_act() I was shot by: [Proj.firer]",2)
@@ -1239,8 +1233,11 @@
/mob/living/simple_animal/proc/PunchTarget()
if(!Adjacent(target_mob))
return
if(!client)
sleep(rand(melee_attack_minDelay, melee_attack_maxDelay))
if(!canClick())
return
setClickCooldown(get_attack_speed())
// if(!client)
// sleep(rand(melee_attack_minDelay, melee_attack_maxDelay))
if(isliving(target_mob))
var/mob/living/L = target_mob
@@ -1282,13 +1279,18 @@
//The actual top-level ranged attack proc
/mob/living/simple_animal/proc/ShootTarget()
if(!canClick())
return FALSE
setClickCooldown(get_attack_speed())
var/target = target_mob
var/tturf = get_turf(target)
if((firing_lines && !client) && !CheckFiringLine(tturf))
step_rand(src)
face_atom(tturf)
return 0
return FALSE
visible_message("<span class='danger'><b>[src]</b> fires at [target]!</span>")
if(rapid)
@@ -1309,7 +1311,7 @@
if(casingtype)
new casingtype
return 1
return TRUE
//Check firing lines for faction_friends (if we're not cooperative, we don't care)
/mob/living/simple_animal/proc/CheckFiringLine(var/turf/tturf)
@@ -0,0 +1,74 @@
// Reorganized and somewhat cleaned up.
// AI code has been made into a datum, inside the AI module folder.
// Does a melee attack.
/mob/living/simple_animal/proc/attack_target(atom/A)
set waitfor = FALSE // For attack animations, if they're ever added. Don't want the AI processor to get held up.
if(!A.Adjacent(src))
return FALSE
if(!canClick()) // Still on cooldown from a "click".
return FALSE
setClickCooldown(get_attack_speed())
return do_attack(A)
// This does the actual attack.
/mob/living/simple_animal/proc/do_attack(atom/A)
if(!A.Adjacent(src)) // They could've moved in the meantime.
return FALSE
var/damage_to_do = rand(melee_damage_lower, melee_damage_upper)
for(var/datum/modifier/M in modifiers)
if(!isnull(M.outgoing_melee_damage_percent))
damage_to_do *= M.outgoing_melee_damage_percent
if(isliving(A)) // Check defenses.
var/mob/living/L = A
if(prob(melee_miss_chance)) // This is stupid (The logging part).
src.attack_log += text("\[[time_stamp()]\] <font color='red'>attacked [L.name] ([L.ckey])</font>")
L.attack_log += text("\[[time_stamp()]\] <font color='orange'>was attacked by [src.name] ([src.ckey])</font>")
visible_message("<span class='danger'>[src] misses [L]!</span>")
do_attack_animation(src)
return FALSE // We missed.
if(ishuman(L))
var/mob/living/carbon/human/H = L
if(H.check_shields(damage = damage_to_do, damage_source = src, attacker = src, def_zone = null, attack_text = "the attack"))
return FALSE // We were blocked.
if(A.attack_generic(src, damage_to_do, pick(attacktext)) && attack_sound)
playsound(src, attack_sound, 75, 1)
return TRUE
//The actual top-level ranged attack proc
/mob/living/simple_animal/proc/shoot_target(atom/A)
if(!canClick())
return FALSE
setClickCooldown(get_attack_speed())
visible_message("<span class='danger'><b>\The [src]</b> fires at \the [A]!</span>")
shoot(A, src.loc, src)
if(casingtype)
new casingtype
return TRUE
//Shoot a bullet at someone (idk why user is an argument when src would fit???)
/mob/living/simple_animal/proc/shoot(atom/A, turf/start, mob/living/user, bullet = 0)
if(A == start)
return
var/obj/item/projectile/P = new projectiletype(user.loc)
playsound(user, projectilesound, 100, 1)
if(!P)
return
P.launch(A)
//Special attacks, like grenades or blinding spit or whatever
/mob/living/simple_animal/proc/special_attack_target(atom/A)
return FALSE
+2 -3
View File
@@ -730,13 +730,12 @@
/mob/proc/facedir(var/ndir)
if(!canface() || (client && (client.moving || (world.time < client.move_delay))))
if(!canface() || (client && (client.moving || (world.time < move_delay))))
return 0
set_dir(ndir)
if(buckled && buckled.buckle_movable)
buckled.set_dir(ndir)
if(client)
client.move_delay += movement_delay()
move_delay += movement_delay()
return 1
+2 -1
View File
@@ -6,6 +6,8 @@
var/datum/mind/mind
var/stat = 0 //Whether a mob is alive or dead. TODO: Move this to living - Nodrak
var/move_delay = null // For movement speed delays.
var/next_move = null // For click delay, despite the misleading name.
//Not in use yet
var/obj/effect/organstructure/organStructure = null
@@ -61,7 +63,6 @@
var/sdisabilities = 0 //Carbon
var/disabilities = 0 //Carbon
var/atom/movable/pulling = null
var/next_move = null
var/transforming = null //Carbon
var/other = 0.0
var/eye_blind = null //Carbon
+11 -12
View File
@@ -11,8 +11,7 @@
return
/mob/proc/setMoveCooldown(var/timeout)
if(client)
client.move_delay = max(world.time + timeout, client.move_delay)
move_delay = max(world.time + timeout, move_delay)
/client/North()
..()
@@ -199,7 +198,7 @@
if(moving) return 0
if(world.time < move_delay) return
if(world.time < mob.move_delay) return
if(locate(/obj/effect/stop/, mob.loc))
for(var/obj/effect/stop/S in mob.loc)
@@ -271,27 +270,27 @@
src << "<font color='blue'>You're pinned to a wall by [mob.pinned[1]]!</font>"
return 0
move_delay = world.time//set move delay
mob.move_delay = world.time//set move delay
switch(mob.m_intent)
if("run")
if(mob.drowsyness > 0)
move_delay += 6
move_delay += config.run_speed
mob.move_delay += 6
mob.move_delay += config.run_speed
if("walk")
move_delay += config.walk_speed
move_delay += mob.movement_delay()
mob.move_delay += config.walk_speed
mob.move_delay += mob.movement_delay()
var/tickcomp = 0 //moved this out here so we can use it for vehicles
if(config.Tickcomp)
// move_delay -= 1.3 //~added to the tickcomp calculation below
tickcomp = ((1/(world.tick_lag))*1.3) - 1.3
move_delay = move_delay + tickcomp
mob.move_delay = mob.move_delay + tickcomp
if(istype(mob.buckled, /obj/vehicle))
//manually set move_delay for vehicles so we don't inherit any mob movement penalties
//specific vehicle move delays are set in code\modules\vehicles\vehicle.dm
move_delay = world.time + tickcomp
mob.move_delay = world.time + tickcomp
//drunk driving
if(mob.confused && prob(20)) //vehicles tend to keep moving in the same direction
direct = turn(direct, pick(90, -90))
@@ -320,14 +319,14 @@
if(prob(50)) direct = turn(direct, pick(90, -90))
if("walk")
if(prob(25)) direct = turn(direct, pick(90, -90))
move_delay += 2
mob.move_delay += 2
return mob.buckled.relaymove(mob,direct)
//We are now going to move
moving = 1
//Something with pulling things
if(locate(/obj/item/weapon/grab, mob))
move_delay = max(move_delay, world.time + 7)
mob.move_delay = max(mob.move_delay, world.time + 7)
var/list/L = mob.ret_grab()
if(istype(L, /list))
if(L.len == 2)