+AIController::~AIController() {
+ // ignore this for now
+ // state->Exit(*this, entity);
+}
+
+void AIController::SetState(const AIState &s, Entity &entity) {
+ state->Exit(*this, entity);
+ state = &s;
+ state->Enter(*this, entity);
+}
+
+void AIController::Update(Entity &e, float dt) {
+ think_timer.Update(dt);
+ decision_timer.Update(dt);
+ state->Update(*this, e, dt);
+
+ if (e.Moving()) {
+ // orient head towards heading
+ glm::vec3 heading(e.Heading());
+ // only half pitch, so we don't crane our neck
+ float tgt_pitch = std::atan(heading.y / length(glm::vec2(heading.x, heading.z))) * 0.5f;
+ // always look straight ahead
+ // maybe look at the pursuit target if there is one
+ float tgt_yaw = 0.0f;
+ e.SetHead(tgt_pitch, tgt_yaw);
+ e.OrientBody(dt);
+ }
+}
+
+Player *AIController::ClosestVisiblePlayer(const Entity &e) noexcept {
+ Player *target = nullptr;
+ float distance = sight_dist;
+ const glm::ivec3 &reference(e.ChunkCoords());
+ Ray aim(e.Aim(reference));
+ for (Player &p : world.Players()) {
+ const Entity &pe = p.GetEntity();
+
+ // distance test
+ const glm::vec3 diff(pe.AbsoluteDifference(e));
+ float dist = length(diff);
+ if (dist > distance) continue;
+
+ // FOV test, 45° in each direction
+ if (dot(diff / dist, aim.dir) < sight_angle) {
+ continue;
+ }
+
+ // LOS test, assumes all entities are see-through
+ WorldCollision col;
+ if (world.Intersection(aim, reference, col) && col.depth < dist) {
+ continue;
+ }
+
+ // we got a match
+ target = &p;
+ distance = dist;