glm::vec3 ControlForce(const Entity &, const EntityState &) const override;
 
-       static glm::vec3 Heading(const EntityState &) noexcept;
-
        /// get the closest player that given entity can see
        /// returns nullptr if none are in sight
        Player *ClosestVisiblePlayer(const Entity &) noexcept;
        void ExitHalt() noexcept;
        bool IsHalted() const noexcept;
        void SetHaltSpeed(float) noexcept;
-       glm::vec3 GetHaltForce(const EntityState &) const noexcept;
+       glm::vec3 GetHaltForce(const Entity &, const EntityState &) const noexcept;
 
        void StartFleeing() noexcept;
        void StopFleeing() noexcept;
        void SetFleeSpeed(float) noexcept;
        Entity &GetFleeTarget() noexcept;
        const Entity &GetFleeTarget() const noexcept;
-       glm::vec3 GetFleeForce(const EntityState &) const noexcept;
+       glm::vec3 GetFleeForce(const Entity &, const EntityState &) const noexcept;
 
        void StartSeeking() noexcept;
        void StopSeeking() noexcept;
        void SetSeekSpeed(float) noexcept;
        Entity &GetSeekTarget() noexcept;
        const Entity &GetSeekTarget() const noexcept;
-       glm::vec3 GetSeekForce(const EntityState &) const noexcept;
+       glm::vec3 GetSeekForce(const Entity &, const EntityState &) const noexcept;
 
        void StartEvading() noexcept;
        void StopEvading() noexcept;
        void SetEvadeSpeed(float) noexcept;
        Entity &GetEvadeTarget() noexcept;
        const Entity &GetEvadeTarget() const noexcept;
-       glm::vec3 GetEvadeForce(const EntityState &) const noexcept;
+       glm::vec3 GetEvadeForce(const Entity &, const EntityState &) const noexcept;
 
        void StartPursuing() noexcept;
        void StopPursuing() noexcept;
        void SetPursuitSpeed(float) noexcept;
        Entity &GetPursuitTarget() noexcept;
        const Entity &GetPursuitTarget() const noexcept;
-       glm::vec3 GetPursuitForce(const EntityState &) const noexcept;
+       glm::vec3 GetPursuitForce(const Entity &, const EntityState &) const noexcept;
 
        /// start wandering randomly
        void StartWandering() noexcept;
                float radius = 1.0f,
                float displacement = 1.0f
        ) noexcept;
-       glm::vec3 GetWanderForce(const EntityState &) const noexcept;
+       glm::vec3 GetWanderForce(const Entity &, const EntityState &) const noexcept;
 
 private:
        World &world;
 
 
        if (e.Moving()) {
                // orient head towards heading
-               glm::vec3 heading(Heading(e.GetState()));
+               glm::vec3 heading(e.Heading());
                float tgt_pitch = std::atan(heading.y / length(glm::vec2(heading.x, heading.z)));
                float tgt_yaw = std::atan2(-heading.x, -heading.z);
                e.SetHead(tgt_pitch, tgt_yaw);
 
 glm::vec3 AIController::ControlForce(const Entity &entity, const EntityState &state) const {
        if (IsHalted()) {
-               return GetHaltForce(state);
+               return GetHaltForce(entity, state);
        }
        glm::vec3 force(0.0f);
        if (IsFleeing()) {
-               if (MaxOutForce(force, GetFleeForce(state), entity.MaxControlForce())) {
+               if (MaxOutForce(force, GetFleeForce(entity, state), entity.MaxControlForce())) {
                        return force;
                }
        }
        if (IsSeeking()) {
-               if (MaxOutForce(force, GetSeekForce(state), entity.MaxControlForce())) {
+               if (MaxOutForce(force, GetSeekForce(entity, state), entity.MaxControlForce())) {
                        return force;
                }
        }
        if (IsEvading()) {
-               if (MaxOutForce(force, GetEvadeForce(state), entity.MaxControlForce())) {
+               if (MaxOutForce(force, GetEvadeForce(entity, state), entity.MaxControlForce())) {
                        return force;
                }
        }
        if (IsPursuing()) {
-               if (MaxOutForce(force, GetPursuitForce(state), entity.MaxControlForce())) {
+               if (MaxOutForce(force, GetPursuitForce(entity, state), entity.MaxControlForce())) {
                        return force;
                }
        }
        if (IsWandering()) {
-               if (MaxOutForce(force, GetWanderForce(state), entity.MaxControlForce())) {
+               if (MaxOutForce(force, GetWanderForce(entity, state), entity.MaxControlForce())) {
                        return force;
                }
        }
        return force;
 }
 
-glm::vec3 AIController::Heading(const EntityState &state) noexcept {
-       if (dot(state.velocity, state.velocity) > std::numeric_limits<float>::epsilon()) {
-               return normalize(state.velocity);
-       } else {
-               float cp = std::cos(state.pitch);
-               return glm::vec3(std::cos(state.yaw) * cp, std::sin(state.yaw) * cp, std::sin(state.pitch));
-       }
-}
-
 Player *AIController::ClosestVisiblePlayer(const Entity &e) noexcept {
        Player *target = nullptr;
        float distance = sight_dist;
 
                // distance test
                const glm::vec3 diff(pe.AbsoluteDifference(e));
-               float dist = length_squared(diff);
+               float dist = length(diff);
                if (dist > distance) continue;
 
                // FOV test, 45° in each direction
-               if (dot(normalize(diff), aim.dir) < sight_angle) {
+               if (dot(diff / dist, aim.dir) < sight_angle) {
                        continue;
                }
 
                // LOS test, assumes all entities are see-through
                WorldCollision col;
-               if (world.Intersection(aim, glm::mat4(1.0f), reference, col) && col.depth * col.depth < dist) {
+               if (world.Intersection(aim, glm::mat4(1.0f), reference, col) && col.depth < dist) {
                        continue;
                }
 
        halt_speed = speed;
 }
 
-glm::vec3 AIController::GetHaltForce(const EntityState &state) const noexcept {
+glm::vec3 AIController::GetHaltForce(const Entity &, const EntityState &state) const noexcept {
        return Halt(state, halt_speed);
 }
 
        return *flee_target;
 }
 
-glm::vec3 AIController::GetFleeForce(const EntityState &state) const noexcept {
+glm::vec3 AIController::GetFleeForce(const Entity &, const EntityState &state) const noexcept {
        return Flee(state, GetFleeTarget().GetState(), flee_speed, 2.0f);
 }
 
        return *seek_target;
 }
 
-glm::vec3 AIController::GetSeekForce(const EntityState &state) const noexcept {
+glm::vec3 AIController::GetSeekForce(const Entity &, const EntityState &state) const noexcept {
        return Seek(state, GetSeekTarget().GetState(), seek_speed, 2.0f);
 }
 
        return *evade_target;
 }
 
-glm::vec3 AIController::GetEvadeForce(const EntityState &state) const noexcept{
+glm::vec3 AIController::GetEvadeForce(const Entity &, const EntityState &state) const noexcept{
        glm::vec3 cur_diff(state.Diff(GetEvadeTarget().GetState()));
        float time_estimate = length(cur_diff) / evade_speed;
        EntityState pred_state(GetEvadeTarget().GetState());
        return *pursuit_target;
 }
 
-glm::vec3 AIController::GetPursuitForce(const EntityState &state) const noexcept {
+glm::vec3 AIController::GetPursuitForce(const Entity &, const EntityState &state) const noexcept {
        glm::vec3 cur_diff(state.Diff(GetPursuitTarget().GetState()));
        float time_estimate = length(cur_diff) / pursuit_speed;
        EntityState pred_state(GetPursuitTarget().GetState());
        wander_disp = displacement;
 }
 
-glm::vec3 AIController::GetWanderForce(const EntityState &state) const noexcept {
-       glm::vec3 wander_target(normalize(Heading(state) * wander_dist + wander_pos) * wander_speed);
+glm::vec3 AIController::GetWanderForce(const Entity &e, const EntityState &state) const noexcept {
+       glm::vec3 wander_target(normalize(e.Heading() * wander_dist + wander_pos) * wander_speed);
        return TargetVelocity(wander_target, state, 0.5f);
 }
 
 
 
        vector<WorldCollision> col;
        while (entry != end) {
-               replay.Velocity(entry->state.velocity);
                SetMovement(entry->movement);
                GetWorld().Update(replay, entry->delta_t);
                entry->state.chunk_pos = replay.GetState().chunk_pos;
 
        glm::vec3 ControlForce(const EntityState &) const noexcept;
 
        const glm::vec3 &Velocity() const noexcept { return state.velocity; }
-       void Velocity(const glm::vec3 &v) noexcept { state.velocity = v; }
 
        bool Moving() const noexcept {
                return dot(Velocity(), Velocity()) > std::numeric_limits<float>::epsilon();
 
        /// orientation of local coordinate system
        const glm::quat &Orientation() const noexcept { return state.orient; }
-       void Orientation(const glm::quat &o) noexcept { state.orient = o; }
 
        /// orientation of head within local coordinate system, in radians
        float Pitch() const noexcept { return state.pitch; }
        /// get a ray in entity's face direction originating from center of vision
        Ray Aim(const Chunk::Pos &chunk_offset) const noexcept;
 
+       const glm::vec3 &Heading() const noexcept { return heading; }
+
        void SetState(const EntityState &s) noexcept { state = s; UpdateModel(); }
        const EntityState &GetState() const noexcept { return state; }
 
 
 private:
        void UpdateModel() noexcept;
+       void UpdateView() noexcept;
+       void UpdateHeading() noexcept;
 
 private:
        EntityController *ctrl;
        AABB bounds;
        EntityState state;
 
+       /// local transform of eyes
+       /// if this entity has no model, the eyes are assumed to
+       /// be at local origin and oriented towards -Z
+       glm::mat4 view_local;
+       /// normalized velocity or heading if standing still
+       glm::vec3 heading;
+
        // TODO: I'd prefer a drag solution
        float max_vel;
        float max_force;
 
 , name("anonymous")
 , bounds()
 , state()
+, heading(0.0f, 0.0f, -1.0f)
 , max_vel(5.0f)
 , max_force(25.0f)
 , ref_count(0)
 }
 
 glm::mat4 Entity::ViewTransform(const glm::ivec3 &reference) const noexcept {
-       glm::mat4 transform = Transform(reference);
-       if (model) {
-               transform *= model.EyesTransform();
-       }
+       glm::mat4 transform = view_local;
+       transform[3] += glm::vec4(state.RelativePosition(reference), 0.0f);
        return transform;
 }
 
 Ray Entity::Aim(const Chunk::Pos &chunk_offset) const noexcept {
        glm::mat4 transform = ViewTransform(chunk_offset);
-       glm::vec4 from = transform * glm::vec4(0.0f, 0.0f, 0.0f, 1.0f);
-       from /= from.w;
-       glm::vec4 to = transform * glm::vec4(0.0f, 0.0f, -1.0f, 1.0f);
-       to /= to.w;
-       return Ray{ glm::vec3(from), glm::normalize(glm::vec3(to - from)) };
+       return Ray{ glm::vec3(transform[3]), -glm::vec3(transform[2]) };
 }
 
 void Entity::UpdateModel() noexcept {
 }
 
 void Entity::Update(float dt) {
+       UpdateView();
+       UpdateHeading();
        if (HasController()) {
                GetController().Update(*this, dt);
        }
 }
 
+void Entity::UpdateView() noexcept {
+       // create local transform
+       view_local = Transform(ChunkCoords());
+       // clear the translation part
+       view_local[3] = glm::vec4(0.0f, 0.0f, 0.0f, 1.0f);
+       // add the model's eyes translation, if any
+       if (model) {
+               view_local *= model.EyesTransform();
+       }
+}
+
+void Entity::UpdateHeading() noexcept {
+       if (Moving()) {
+               heading = normalize(Velocity());
+       } else {
+               // use -Z (forward axis) of local view transform
+               heading = -glm::vec3(view_local[2]);
+       }
+}
+
 
 EntityController::~EntityController() {
 
 }
 
 void EntityState::AdjustHeading() noexcept {
-       while (pitch > PI / 2) {
-               pitch = PI / 2;
-       }
-       while (pitch < -PI / 2) {
-               pitch = -PI / 2;
-       }
+       glm::clamp(pitch, -PI_0p5, PI_0p5);
        while (yaw > PI) {
-               yaw -= PI * 2;
+               yaw -= PI_2p0;
        }
        while (yaw < -PI) {
-               yaw += PI * 2;
+               yaw += PI_2p0;
        }
 }
 
 glm::mat4 EntityState::Transform(const glm::ivec3 &reference) const noexcept {
        const glm::vec3 translation = RelativePosition(reference);
        glm::mat4 transform(toMat4(orient));
-       transform[3].x = translation.x;
-       transform[3].y = translation.y;
-       transform[3].z = translation.z;
+       transform[3] = glm::vec4(translation, 1.0f);
        return transform;
 }