]> git.localhorst.tv Git - blank.git/blob - src/ai/ai.cpp
db4ae413d8170a9dbc5f7f7056978a0615f6e5ee
[blank.git] / src / ai / ai.cpp
1 #include "AIController.hpp"
2 #include "ChaseState.hpp"
3 #include "FleeState.hpp"
4 #include "IdleState.hpp"
5 #include "RoamState.hpp"
6
7 #include "../geometry/distance.hpp"
8 #include "../geometry/rotation.hpp"
9 #include "../rand/GaloisLFSR.hpp"
10 #include "../world/Entity.hpp"
11 #include "../world/World.hpp"
12 #include "../world/WorldCollision.hpp"
13
14 #include <cmath>
15 #include <limits>
16 #include <glm/glm.hpp>
17
18
19 namespace blank {
20
21 namespace {
22
23 ChaseState chase;
24 FleeState flee;
25 IdleState idle;
26 RoamState roam;
27
28 }
29
30 AIController::AIController(World &world, Entity &entity)
31 : world(world)
32 , state(&idle)
33 , sight_dist(64.0f)
34 , sight_angle(0.707f)
35 , think_timer(0.5f)
36 , decision_timer(1.0f) {
37         think_timer.Start();
38         state->Enter(*this, entity);
39 }
40
41 AIController::~AIController() {
42         // ignore this for now
43         // state->Exit(*this, entity);
44 }
45
46 void AIController::SetState(const AIState &s, Entity &entity) {
47         state->Exit(*this, entity);
48         state = &s;
49         state->Enter(*this, entity);
50 }
51
52 void AIController::Update(Entity &e, float dt) {
53         think_timer.Update(dt);
54         decision_timer.Update(dt);
55         state->Update(*this, e, dt);
56
57         if (e.Moving()) {
58                 // orient head towards heading
59                 glm::vec3 heading(e.Heading());
60                 // only half pitch, so we don't crane our neck
61                 float tgt_pitch = std::atan(heading.y / length(glm::vec2(heading.x, heading.z))) * 0.5f;
62                 // always look straight ahead
63                 // maybe look at the pursuit target if there is one
64                 float tgt_yaw = 0.0f;
65                 e.SetHead(tgt_pitch, tgt_yaw);
66                 e.OrientBody(dt);
67         }
68 }
69
70 Player *AIController::ClosestVisiblePlayer(const Entity &e) noexcept {
71         Player *target = nullptr;
72         float distance = sight_dist;
73         const glm::ivec3 &reference(e.ChunkCoords());
74         Ray aim(e.Aim(reference));
75         for (Player &p : world.Players()) {
76                 const Entity &pe = p.GetEntity();
77
78                 // distance test
79                 const glm::vec3 diff(pe.AbsoluteDifference(e));
80                 float dist = length(diff);
81                 if (dist > distance) continue;
82
83                 // FOV test, 45° in each direction
84                 if (dot(diff / dist, aim.dir) < sight_angle) {
85                         continue;
86                 }
87
88                 // LOS test, assumes all entities are see-through
89                 WorldCollision col;
90                 if (world.Intersection(aim, reference, col) && col.depth < dist) {
91                         continue;
92                 }
93
94                 // we got a match
95                 target = &p;
96                 distance = dist;
97         }
98         return target;
99 }
100
101 bool AIController::LineOfSight(const Entity &from, const Entity &to) const noexcept {
102         const glm::ivec3 &reference(from.ChunkCoords());
103         Ray aim(from.Aim(reference));
104         const glm::vec3 diff(to.AbsoluteDifference(from));
105         float dist = length(diff);
106         if (dist > sight_dist || dot(diff / dist, aim.dir) < sight_angle) {
107                 return false;
108         }
109         WorldCollision col;
110         if (world.Intersection(aim, reference, col) && col.depth < dist) {
111                 return false;
112         }
113         return true;
114 }
115
116 // think
117
118 bool AIController::MayThink() const noexcept {
119         return think_timer.Hit();
120 }
121
122 void AIController::SetThinkInterval(float i) noexcept {
123         think_timer = FineTimer(i);
124         think_timer.Start();
125 }
126
127 // decide
128
129 void AIController::CueDecision(
130         float minimum,
131         float variance
132 ) noexcept {
133         decision_timer = FineTimer(minimum + variance * world.Random().SNorm());
134         decision_timer.Start();
135 }
136
137 bool AIController::DecisionDue() const noexcept {
138         return decision_timer.HitOnce();
139 }
140
141 unsigned int AIController::Decide(unsigned int num_choices) noexcept {
142         return world.Random().Next<unsigned int>() % num_choices;
143 }
144
145
146 // chase
147
148 void ChaseState::Enter(AIController &ctrl, Entity &e) const {
149         e.GetSteering()
150                 .SetAcceleration(5.0f)
151                 .SetSpeed(4.0f)
152                 .Enable(Steering::PURSUE_TARGET)
153         ;
154 }
155
156 void ChaseState::Update(AIController &ctrl, Entity &e, float dt) const {
157         Steering &steering = e.GetSteering();
158         // check if target still alive and in sight
159         if (
160                 !steering.HasTargetEntity() || // lost
161                 steering.GetTargetEntity().Dead() || // dead
162                 !ctrl.LineOfSight(e, steering.GetTargetEntity()) // escaped
163         ) {
164                 steering.ClearTargetEntity();
165                 ctrl.SetState(idle, e);
166                 return;
167         }
168         // halt if we're close enough, flee if we're too close
169         float dist_sq = length2(e.AbsoluteDifference(steering.GetTargetEntity()));
170         if (dist_sq < 8.0f) {
171                 ctrl.SetState(flee, e);
172         } else if (dist_sq < 25.0f) {
173                 steering.Enable(Steering::HALT).Disable(Steering::PURSUE_TARGET);
174         } else {
175                 steering.Enable(Steering::PURSUE_TARGET).Disable(Steering::HALT);
176         }
177 }
178
179 void ChaseState::Exit(AIController &ctrl, Entity &e) const {
180         e.GetSteering().Disable(Steering::HALT | Steering::PURSUE_TARGET);
181 }
182
183 // flee
184
185 void FleeState::Enter(AIController &ctrl, Entity &e) const {
186         e.GetSteering()
187                 .SetAcceleration(5.0f)
188                 .SetSpeed(4.0f)
189                 .Enable(Steering::EVADE_TARGET)
190         ;
191         ctrl.CueDecision(6.0f, 3.0f);
192 }
193
194 void FleeState::Update(AIController &ctrl, Entity &e, float dt) const {
195         if (!ctrl.DecisionDue()) return;
196         ctrl.SetState(idle, e);
197 }
198
199 void FleeState::Exit(AIController &ctrl, Entity &e) const {
200         e.GetSteering().Disable(Steering::EVADE_TARGET);
201 }
202
203 // idle
204
205 void IdleState::Enter(AIController &ctrl, Entity &e) const {
206         e.GetSteering()
207                 .SetAcceleration(0.5f)
208                 .SetSpeed(0.01f)
209                 .Enable(Steering::HALT)
210                 .SetWanderParams(1.0f, 1.1f, 1.0f)
211         ;
212         ctrl.CueDecision(10.0f, 5.0f);
213 }
214
215 void IdleState::Update(AIController &ctrl, Entity &e, float dt) const {
216         if (ctrl.MayThink()) {
217                 const Player *player = ctrl.ClosestVisiblePlayer(e);
218                 if (player) {
219                         e.GetSteering().SetTargetEntity(player->GetEntity());
220                         ctrl.SetState(chase, e);
221                         return;
222                 }
223         }
224
225         if (!ctrl.DecisionDue()) return;
226
227         unsigned int d = ctrl.Decide(10);
228         if (d < 2) {
229                 // .2 chance to start going
230                 ctrl.SetState(roam, e);
231         } else if (d < 5) {
232                 // .3 chance of looking around
233                 e.GetSteering().Disable(Steering::HALT).Enable(Steering::WANDER);
234         } else {
235                 // .5 chance of doing nothing
236                 e.GetSteering().Disable(Steering::WANDER).Enable(Steering::HALT);
237         }
238         ctrl.CueDecision(10.0f, 5.0f);
239 }
240
241 void IdleState::Exit(AIController &ctrl, Entity &e) const {
242         e.GetSteering().Disable(Steering::HALT | Steering::WANDER);
243 }
244
245 // roam
246
247 void RoamState::Enter(AIController &ctrl, Entity &e) const {
248         e.GetSteering()
249                 .SetAcceleration(0.5f)
250                 .SetSpeed(1.0f)
251                 .SetWanderParams(1.0f, 2.0f, 1.0f)
252                 .Enable(Steering::WANDER)
253         ;
254         ctrl.CueDecision(10.0f, 5.0f);
255 }
256
257 void RoamState::Update(AIController &ctrl, Entity &e, float dt) const {
258         if (ctrl.MayThink()) {
259                 const Player *player = ctrl.ClosestVisiblePlayer(e);
260                 if (player) {
261                         e.GetSteering().SetTargetEntity(player->GetEntity());
262                         ctrl.SetState(chase, e);
263                         return;
264                 }
265         }
266
267         if (!ctrl.DecisionDue()) return;
268
269         unsigned int d = ctrl.Decide(10);
270         if (d == 0) {
271                 // .1 chance of idling
272                 ctrl.SetState(idle, e);
273         }
274         ctrl.CueDecision(10.0f, 5.0f);
275 }
276
277 void RoamState::Exit(AIController &ctrl, Entity &e) const {
278         e.GetSteering().Disable(Steering::WANDER);
279 }
280
281 }