#include "game.h"
namespace ai
{
    avoidset obstacles, wpavoid;
    int updatemillis = 0, iteration = 0, itermillis = 0;
    vec aitarget(0, 0, 0);

    VAR(0, aidebug, 0, 0, 7);
    VAR(0, aidebugfocus, 0, 1, 2);
    VAR(0, aipassive, 0, 0, 2); // 0 = off, 1 = passive to humans, 2 = completely passive

    VARF(0, showwaypoints, 0, 0, 1, if(showwaypoints) getwaypoints());

    VAR(IDF_PERSIST, showwaypointsdrop, 0, 1, 1);
    VAR(IDF_PERSIST, showwaypointsradius, 0, 256, VAR_MAX);
    VAR(IDF_PERSIST|IDF_HEX, showwaypointscolour, 0, 0xFF00FF, 0xFFFFFF);

    VAR(IDF_PERSIST, aideadfade, 0, 10000, VAR_MAX);
    VAR(IDF_PERSIST, showaiinfo, 0, 0, 2); // 0/1 = shows/hides bot join/parts, 2 = show more verbose info

    bool passive(gameent *d = NULL)
    {
        if(m_edit(game::gamemode)) return true;
        switch(aipassive)
        {
            case 2: return true;
            case 1: if(d && d->actortype == A_PLAYER) return true;
            case 0: default: break;
        }
        return false;
    }
    bool dbgfocus(gameent *d)  { return d->ai && (!aidebugfocus || d == game::focus || (aidebugfocus != 2 && !game::focus->ai)); }

    void startmap(const char *name, const char *reqname, bool empty)    // called just after a map load
    {
        savewaypoints();
        clearwaypoints(true);
        showwaypoints = dropwaypoints = 0;
    }

    float viewdist(int x) { return x <= 100 ? clamp((SIGHTMIN+(SIGHTMAX-SIGHTMIN))/100.f*float(x), float(SIGHTMIN), max(float(fog), SIGHTMIN)) : max(float(fog), SIGHTMIN); }
    float viewfieldx(int x) { return x <= 100 ? clamp((VIEWMIN+(VIEWMAX-VIEWMIN))/100.f*float(x), float(VIEWMIN), float(VIEWMAX)) : float(VIEWMAX); }
    float viewfieldy(int x) { return viewfieldx(x)*3.f/4.f; }

    float weapmindist(int weap, bool alt)
    {
        if(weaptype[weap].melee) return 0.f;
        if(WX(false, weap, explode, alt, game::gamemode, game::mutators, 1.f) > 0) return WX(false, weap, explode, alt, game::gamemode, game::mutators, 1.f);
        return CLOSEDIST;
    }

    float weapmaxdist(int weap, bool alt)
    {
        if(weaptype[weap].melee) return CLOSEDIST;
        if(W2(weap, aidist, alt) > 0) return W2(weap, aidist, alt);
        return getworldsize();
    }

    bool weaprange(gameent *d, int weap, bool alt, float dist)
    {
        if(!isweap(weap) || (W2(weap, extinguish, alt) && d->inliquid)) return false;
        float mindist = weapmindist(weap, alt), maxdist = weapmaxdist(weap, alt);
        return dist >= mindist*mindist && dist <= maxdist*maxdist;
    }

    bool targetable(gameent *d, gameent *e, bool solid)
    {
        if(d && e && d != e && !passive(e) && e->state == CS_ALIVE && (!solid || physics::issolid(e, d)))
        {
            if(d->team == T_ENEMY && e->team == T_ENEMY) return false;
            if(!m_team(game::gamemode, game::mutators) || d->team != e->team) return true;
        }
        return false;
    }

    bool cansee(gameent *d, vec &x, vec &y, bool force, vec &targ)
    {
        if(force) return raycubelos(x, y, targ);
        return getsight(x, d->yaw, d->pitch, y, targ, d->ai->views[2], d->ai->views[0], d->ai->views[1]);
    }

    bool badhealth(gameent *d)
    {
        if(d->skill <= 100) return d->health <= (111-d->skill)/4;
        return false;
    }

    bool canshoot(gameent *d, gameent *e, bool alt = true)
    {
        if(isweap(d->weapselect) && weaprange(d, d->weapselect, alt, e->o.squaredist(d->o)))
        {
            int prot = m_protect(game::gamemode, game::mutators);
            if((d->actortype >= A_ENEMY || !d->protect(lastmillis, prot)) && targetable(d, e, true))
                return d->canshoot(d->weapselect, alt ? HIT_ALT : 0, m_weapon(game::gamemode, game::mutators), lastmillis, (1<<W_S_RELOAD));
        }
        return false;
    }

    bool altfire(gameent *d, gameent *e)
    {
        if(e && !(W2(d->weapselect, cooked, true)&W_C_ZOOM) && canshoot(d, e, true))
        {
            if(d->weapstate[d->weapselect] == W_S_POWER || d->weapstate[d->weapselect] == W_S_ZOOM)
            {
                if(d->action[AC_SECONDARY] && (!d->action[AC_PRIMARY] || d->actiontime[AC_SECONDARY] > d->actiontime[AC_PRIMARY]))
                    return true;
            }
            switch(d->weapselect)
            {
                case W_PISTOL: return true; break;
                case W_MELEE: case W_ROCKET: default: return false; break;
                case W_SWORD: case W_SHOTGUN: case W_SMG: case W_FLAMER: case W_PLASMA: case W_ZAPPER: case W_GRENADE: case W_MINE:
                    if(rnd(d->skill*3) <= d->skill) return false;
                    break;
                case W_RIFLE: if(weaprange(d, d->weapselect, false, e->o.squaredist(d->o))) return false; break;
            }
            return true;
        }
        return false;
    }

    bool hastarget(gameent *d, aistate &b, gameent *e, bool alt, bool insight, float yaw, float pitch)
    { // add margins of error
        if((insight && weaprange(d, d->weapselect, alt, e->o.squaredist(d->o))) || (d->skill <= 100 && !rnd(d->skill)))
        {
            if(weaptype[d->weapselect].melee) return true;
            float skew = clamp(float(lastmillis-d->ai->enemymillis)/float((d->skill*W(d->weapselect, delayreload)/5000.f)+(d->skill*W2(d->weapselect, delayattack, alt)/500.f)), 0.f, weaptype[d->weapselect].thrown ? 0.25f : 1e16f),
                offy = yaw-d->yaw, offp = pitch-d->pitch;
            if(offy > 180) offy -= 360;
            else if(offy < -180) offy += 360;
            if(fabs(offy) <= d->ai->views[0]*skew && fabs(offp) <= d->ai->views[1]*skew) return true;
        }
        return false;
    }

    vec getaimpos(gameent *d, gameent *e, bool alt)
    {
        vec o = e->o;
        if(d->skill <= 100)
        {
            if(lastmillis >= d->ai->lastaimrnd)
            {
                int radius = ceilf(e->radius*W2(d->weapselect, aiskew, alt));
                float speed = clamp(e->vel.magnitude()/movespeed, 0.f, 1.f), scale = speed+((1-speed)*((101-d->skill)/100.f));
                loopk(3) d->ai->aimrnd[k] = (rnd((radius*2)+1)-radius)*scale;
                int dur = (d->skill+10)*10;
                d->ai->lastaimrnd = lastmillis+dur+rnd(dur);
            }
            o.add(d->ai->aimrnd);
        }
        return o;
    }

    int weappref(gameent *d)
    {
        if(d->loadweap.length()) return d->loadweap[0];
        return m_weapon(game::gamemode, game::mutators);
    }

    bool hasweap(gameent *d, int weap)
    {
        if(!isweap(weap)) return false;
        if(w_carry(weap, m_weapon(game::gamemode, game::mutators)))
            return d->hasweap(weap, m_weapon(game::gamemode, game::mutators));
        return d->ammo[weap] >= W(weap, ammomax);
    }

    bool wantsweap(gameent *d, int weap)
    {
        if(!isweap(weap) || hasweap(d, weap)) return false;
        if(d->carry(m_weapon(game::gamemode, game::mutators)) >= maxcarry && (hasweap(d, weappref(d)) || weap != weappref(d)))
            return false;
        return true;
    }

    void create(gameent *d)
    {
        if(!d->ai && !(d->ai = new aiinfo()))
        {
            fatal("could not create ai");
            return;
        }
        d->respawned = -1;
    }

    void destroy(gameent *d)
    {
        if(d->ai) DELETEP(d->ai);
    }

    void checkinfo(gameent *d)
    {
        gameent *o = game::getclient(d->ownernum);
        if(o)
        {
            copystring(d->hostname, o->hostname);
            copystring(d->hostip, o->hostip);
            d->version.grab(o->version);
        }
    }

    void init(gameent *d, int at, int et, int on, int sk, int bn, char *name, int tm, int cl, int md, const char *vn, vector<int> &lweaps)
    {
        getwaypoints();

        gameent *o = game::newclient(on);
        if(!o) return;
        bool resetthisguy = false;
        string m;
        copystring(m, game::colourname(o));
        if(!d->name[0])
        {
            if(at == A_BOT && showaiinfo && client::showpresence >= (client::waiting(false) ? 2 : 1))
            {
                if(showaiinfo > 1) conoutft(CON_EVENT, "\fg%s assigned to %s at skill %d", game::colourname(d, name), m, sk);
                else conoutft(CON_EVENT, "\fg%s was added to the game", game::colourname(d, name));//, m, sk);
            }
            game::specreset(d);
            resetthisguy = true;
        }
        else
        {
            if(d->ownernum != on)
            {
                if(at == A_BOT && showaiinfo && client::showpresence >= (client::waiting(false) ? 2 : 1))
                    conoutft(CON_EVENT, "\fg%s reassigned to %s", game::colourname(d, name), m);
                resetthisguy = true;
            }
            if(at == A_BOT && d->skill != sk && showaiinfo > 1 && client::showpresence >= (client::waiting(false) ? 2 : 1))
                conoutft(CON_EVENT, "\fg%s changed skill to %d", game::colourname(d, name), sk);
        }

        if((d->actortype = at) >= A_ENEMY) d->type = ENT_AI;
        else
        {
            d->loadweap.shrink(0);
            loopv(lweaps) d->loadweap.add(lweaps[i]);
        }
        d->setname(name);
        d->spawnpoint = et;
        d->ownernum = on;
        d->skill = sk;
        d->team = tm;
        d->colour = cl;
        d->model = md;
        d->setvanity(vn);

        if(resetthisguy) projs::remove(d);
        if(d->ownernum >= 0 && game::player1->clientnum == d->ownernum)
        {
            create(d);
            if(d->ai)
            {
                d->ai->views[0] = viewfieldx(d->skill);
                d->ai->views[1] = viewfieldy(d->skill);
                d->ai->views[2] = viewdist(d->skill);
            }
        }
        else if(d->ai) destroy(d);
        checkinfo(d);
    }

    void update()
    {
        if(!gs_playing(game::gamestate))
        {
            loopv(game::players) if(game::players[i] && game::players[i]->ai) game::players[i]->stopmoving(true);
        }
        else // fixed rate logic done out-of-sequence at 1 frame per second for each ai
        {
            if(totalmillis-updatemillis > 100) avoid();
            if(!iteration && totalmillis-itermillis > 1000)
            {
                iteration = 1;
                itermillis = totalmillis;
                if(multiplayer(false)) aipassive = 0;
                updatemillis = totalmillis;
            }
            int c = 0;
            loopv(game::players) if(game::players[i] && game::players[i]->ai)
            {
                checkinfo(game::players[i]);
                think(game::players[i], ++c == iteration ? true : false);
            }
            if(c && ++iteration > c) iteration = 0;
        }
    }

    int checkothers(vector<int> &targets, gameent *d, int state, int targtype, int target, bool teams, int *members)
    { // checks the states of other ai for a match
        targets.setsize(0);
        gameent *e = NULL;
        int numdyns = game::numdynents();
        loopi(numdyns) if((e = (gameent *)game::iterdynents(i)))
        {
            if(targets.find(e->clientnum) >= 0) continue;
            if(teams && (d->team != T_ENEMY || e->team != T_ENEMY) && m_team(game::gamemode, game::mutators) && d->team != e->team) continue;
            if(members) (*members)++;
            if(e == d || !e->ai || e->state != CS_ALIVE || e->actortype != d->actortype) continue;
            aistate &b = e->ai->getstate();
            if(state >= 0 && b.type != state) continue;
            if(target >= 0 && b.target != target) continue;
            if(targtype >=0 && b.targtype != targtype) continue;
            targets.add(e->clientnum);
        }
        return targets.length();
    }

    bool makeroute(gameent *d, aistate &b, int node, bool changed, int retries)
    {
        if(changed && !d->ai->route.empty() && d->ai->route[0] == node) return true;
        if(route(d, d->lastnode, node, d->ai->route, obstacles, retries))
        {
            b.override = false;
            return true;
        }
        // retry fails: 0 = first attempt, 1 = try ignoring obstacles, 2 = try ignoring prevnodes too
        if(retries <= 1) return makeroute(d, b, node, false, retries+1);
        return false;
    }

    bool makeroute(gameent *d, aistate &b, const vec &pos, bool changed, int retries)
    {
        int node = closestwaypoint(pos, CLOSEDIST, true);
        return makeroute(d, b, node, changed, retries);
    }

    bool randomnode(gameent *d, aistate &b, const vec &pos, float guard, float wander)
    {
        static vector<int> candidates;
        candidates.setsize(0);
        findwaypointswithin(pos, guard, wander, candidates);

        while(!candidates.empty())
        {
            int w = rnd(candidates.length()), n = candidates.removeunordered(w);
            if(n != d->lastnode && !d->ai->hasprevnode(n) && !obstacles.find(n, d) && makeroute(d, b, n)) return true;
        }
        return false;
    }

    bool randomnode(gameent *d, aistate &b, float guard, float wander)
    {
        return randomnode(d, b, d->feetpos(), guard, wander);
    }

    bool enemy(gameent *d, aistate &b, const vec &pos, float guard, int pursue, bool force, bool retry = false)
    {
        if(passive() || (d->ai->enemy >= 0 && lastmillis-d->ai->enemymillis >= (111-d->skill)*50)) return false;
        gameent *t = NULL, *e = NULL;
        float mindist = guard*guard, bestdist = 1e16f;
        int numdyns = game::numdynents();
        loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && targetable(d, e))
        {
            float dist = d->o.squaredist(e->o);
            if(dist < bestdist && (force || dist <= mindist || cansee(d, d->o, e->o, d->actortype >= A_ENEMY)))
            {
                t = e;
                bestdist = dist;
            }
        }
        if(t && violence(d, b, t, pursue)) return true;
        if(retry && !force) return enemy(d, b, pos, guard, pursue, true, false);
        return false;
    }

    bool patrol(gameent *d, aistate &b, const vec &pos, float guard, float wander, int walk, bool retry)
    {
        if(actor[d->actortype].canmove)
        {
            vec feet = d->feetpos();
            float dist = feet.squaredist(pos);
            if(walk == 2 || b.override || (walk && dist <= guard*guard) || !makeroute(d, b, pos))
            { // run away and back to keep ourselves busy
                if(!b.override && wander > 0 && randomnode(d, b, pos, guard, wander))
                {
                    b.override = true;
                    return true;
                }
                if(d->ai->route.empty())
                {
                    b.override = false;
                    if(!retry) return patrol(d, b, pos, guard, wander, walk, true);
                    if(wander > 0) return false;
                }
                if(wander <= 0)
                {
                    b.acttype = AI_A_IDLE;
                    return true;
                }
            }
            if(!b.override && dist > guard*guard) b.acttype = AI_A_HASTE;
        }
        b.override = false;
        return true;
    }

    bool defense(gameent *d, aistate &b, const vec &pos, float guard, float wander, int walk)
    {
        if(!actor[d->actortype].canmove)
        {
            b.acttype = enemy(d, b, pos, wander, weaptype[d->weapselect].melee ? 1 : 0, false, true) ? AI_A_PROTECT : AI_A_IDLE;
            return true;
        }
        if(!walk)
        {
            if(d->feetpos().squaredist(pos) <= guard*guard)
            {
                b.acttype = enemy(d, b, pos, d->ai->views[2], weaptype[d->weapselect].melee ? 1 : 0, false, true) ? AI_A_PROTECT : AI_A_IDLE;
                return true;
            }
            walk++;
        }
        return patrol(d, b, pos, guard, wander, walk);
    }

    bool violence(gameent *d, aistate &b, gameent *e, int pursue)
    {
        if(passive() || !targetable(d, e)) return false;
        if(d->ai->enemy != e->clientnum)
        {
            gameent *f = game::getclient(d->ai->enemy);
            if(f && (d->o.squaredist(e->o) < d->o.squaredist(f->o) || (d->ai->enemyseen && lastmillis-d->ai->enemyseen <= (d->skill*10)+1000))) return false;
        }
        if(pursue)
        {
            if((b.targtype != AI_T_AFFINITY || !(pursue%2)) && makeroute(d, b, e->lastnode))
                d->ai->switchstate(b, AI_S_PURSUE, AI_T_ACTOR, e->clientnum, b.targtype != AI_T_AFFINITY ? AI_A_NORMAL : AI_A_HASTE);
            else if(pursue >= 3) return false; // can't pursue
        }
        if(d->ai->enemy != e->clientnum)
        {
            d->ai->enemyseen = d->ai->enemymillis = lastmillis;
            d->ai->enemy = e->clientnum;
        }
        return true;
    }


    struct targcache
    {
        gameent *d;
        bool dominated, visible;
        float dist;

        targcache() : d(NULL), dominated(false), visible(false), dist(0) {}
        ~targcache() {}

        static bool tcsort(targcache &a,  targcache &b)
        {
            if(a.dominated && !b.dominated) return true;
            if(!a.dominated && b.dominated) return false;
            if(a.visible && !b.visible) return true;
            if(!a.visible && b.visible) return false;
            if(a.dist < b.dist) return true;
            if(a.dist > b.dist) return false;
            return true;
        }
    };

    bool target(gameent *d, aistate &b, int pursue = 0, bool force = false)
    {
        if(passive()) return false;
        static vector<targcache> targets;
        targets.setsize(0);
        gameent *e = NULL;
        int numdyns = game::numdynents();
        loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && targetable(d, e))
        {
            targcache &c = targets.add();
            c.d = e;
            c.dist = d->o.squaredist(e->o);
            if(d->dominating.find(c.d) >= 0) c.dominated = true;
            c.visible = force || cansee(d, d->o, e->o, d->actortype >= A_ENEMY);
        }
        if(targets.empty()) return false;
        targets.sort(targcache::tcsort);
        d->ai->enemy = -1;
        d->ai->enemymillis = d->ai->enemyseen = 0;
        loopv(targets) if(violence(d, b, targets[i].d, pursue || targets[i].dominated ? 1 : 0)) return true;
        return false;
    }

    void assist(gameent *d, aistate &b, vector<interest> &interests, bool all = false, bool force = false)
    {
        gameent *e = NULL;
        int numdyns = game::numdynents();
        loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && e != d && (all || e->actortype == A_PLAYER) && d->team == e->team)
        {
            interest &n = interests.add();
            n.state = AI_S_DEFEND;
            n.node = e->lastnode;
            n.target = e->clientnum;
            n.targtype = AI_T_ACTOR;
            n.score = e->o.squaredist(d->o)/(force ? 1e8f : (hasweap(d, weappref(d)) ? 1.f : 0.5f));
            n.tolerance = 0.25f;
            n.team = true;
            n.acttype = AI_A_PROTECT;
        }
    }

    void items(gameent *d, aistate &b, vector<interest> &interests, bool force = false)
    {
        vec pos = d->feetpos();
        int sweap = m_weapon(game::gamemode, game::mutators);
        loopj(entities::lastuse(EU_ITEM))
        {
            gameentity &e = *(gameentity *)entities::ents[j];
            if(enttype[e.type].usetype != EU_ITEM || e.type != WEAPON) continue;
            int attr = w_attr(game::gamemode, game::mutators, e.type, e.attrs[0], sweap);
            if(e.spawned() && isweap(attr) && wantsweap(d, attr))
            { // go get a weapon upgrade
                interest &n = interests.add();
                n.state = AI_S_INTEREST;
                n.node = closestwaypoint(e.o, CLOSEDIST, true);
                n.target = j;
                n.targtype = AI_T_ENTITY;
                n.score =  pos.squaredist(e.o)/(attr == weappref(d) ? 1e8f : (force ? 1e4f : 1.f));
                n.tolerance = 0;
            }
        }

        loopvj(projs::projs) if(projs::projs[j]->projtype == PRJ_ENT && projs::projs[j]->ready())
        {
            projent &proj = *projs::projs[j];
            if(!entities::ents.inrange(proj.id)) continue;
            gameentity &e = *(gameentity *)entities::ents[proj.id];
            if(enttype[e.type].usetype != EU_ITEM || e.type != WEAPON) continue;
            int attr = w_attr(game::gamemode, game::mutators, e.type, e.attrs[0], sweap);
            if(isweap(attr) && wantsweap(d, attr) && proj.owner != d)
            { // go get a weapon upgrade
                interest &n = interests.add();
                n.state = AI_S_INTEREST;
                n.node = closestwaypoint(proj.o, CLOSEDIST, true);
                n.target = proj.id;
                n.targtype = AI_T_DROP;
                n.score = pos.squaredist(proj.o)/(attr == weappref(d) ? 1e8f : (force ? 1e4f : 1.f));
                n.tolerance = 0;
            }
        }
    }

    bool find(gameent *d, aistate &b)
    {
        static vector<interest> interests; interests.setsize(0);
        if(d->actortype == A_BOT)
        {
            if(!passive())
            {
                int sweap = m_weapon(game::gamemode, game::mutators);
                if(!hasweap(d, weappref(d)) || d->carry(sweap) == 0) items(d, b, interests, d->carry(sweap) == 0);
                if(m_team(game::gamemode, game::mutators) && !m_duke(game::gamemode, game::mutators))
                    assist(d, b, interests, false, false);
            }
            if(m_fight(game::gamemode))
            {
                if(m_capture(game::gamemode)) capture::aifind(d, b, interests);
                else if(m_defend(game::gamemode)) defend::aifind(d, b, interests);
                else if(m_bomber(game::gamemode)) bomber::aifind(d, b, interests);
            }
        }
        else if(entities::ents.inrange(d->spawnpoint)) loopv(entities::ents[d->spawnpoint]->links)
        {
            int t = entities::ents[d->spawnpoint]->links[i];
            if(!entities::ents.inrange(t)) continue;
            interest &n = interests.add();
            n.state = AI_S_DEFEND;
            n.target = t;
            n.node = closestwaypoint(entities::ents[t]->o, CLOSEDIST, true);
            n.targtype = AI_T_ENTITY;
            n.score = -1;
            n.tolerance = 1;
        }
        while(!interests.empty())
        {
            int q = interests.length()-1;
            loopi(interests.length()-1) if(interests[i].score < interests[q].score) q = i;
            interest n = interests.removeunordered(q);
            if(d->actortype == A_BOT && m_fight(game::gamemode) && m_team(game::gamemode, game::mutators))
            {
                int members = 0;
                static vector<int> targets; targets.setsize(0);
                int others = checkothers(targets, d, n.state, n.targtype, n.target, n.team, &members);
                if(d->actortype == A_BOT && n.state == AI_S_DEFEND && members == 1) continue;
                if(others >= int(ceilf(members*n.tolerance))) continue;
            }
            if(!actor[d->actortype].canmove || makeroute(d, b, n.node))
            {
                d->ai->switchstate(b, n.state, n.targtype, n.target, n.acttype);
                return true;
            }
        }
        return false;
    }

    void damaged(gameent *d, gameent *e, int weap, int flags, int damage)
    {
        if(d != e)
        {
            if(d->ai && (d->actortype >= A_ENEMY || (hithurts(flags) && damage > 0) || d->ai->enemy < 0 || d->dominating.find(e))) // see if this ai is interested in a grudge
            {
                aistate &b = d->ai->getstate();
                violence(d, b, e, d->actortype != A_BOT || weaptype[d->weapselect].melee ? 1 : 0);
            }
            static vector<int> targets; // check if one of our ai is defending them
            targets.setsize(0);
            if(checkothers(targets, d, AI_S_DEFEND, AI_T_ACTOR, d->clientnum, true))
            {
                gameent *t;
                loopv(targets) if((t = game::getclient(targets[i])) && t->ai && t->actortype == A_BOT && ((hithurts(flags) && damage > 0) || t->ai->enemy < 0 || t->dominating.find(e)))
                {
                    aistate &c = t->ai->getstate();
                    violence(t, c, e, weaptype[d->weapselect].melee ? 1 : 0);
                }
            }
        }
    }

    void setup(gameent *d, int ent = -1)
    {
        d->spawnpoint = ent;
        if(d->ai)
        {
            d->ai->clean();
            d->ai->reset(true);
            d->ai->lastrun = lastmillis;
            findorientation(d->o, d->yaw, d->pitch, d->ai->target);
        }
    }

    void respawned(gameent *d, bool local, int ent)
    {
        if(d->ai) setup(d, ent);
    }

    void killed(gameent *d, gameent *e)
    {
        if(d->ai) d->ai->reset(true);
    }

    void itemspawned(int ent, int spawned)
    {
        if(!passive() && m_fight(game::gamemode) && entities::ents.inrange(ent) && entities::ents[ent]->type == WEAPON && spawned > 0)
        {
            int sweap = m_weapon(game::gamemode, game::mutators), attr = w_attr(game::gamemode, game::mutators, entities::ents[ent]->type, entities::ents[ent]->attrs[0], sweap);
            loopv(game::players) if(game::players[i] && game::players[i]->ai && game::players[i]->actortype == A_BOT && game::players[i]->state == CS_ALIVE && iswaypoint(game::players[i]->lastnode))
            {
                gameent *d = game::players[i];
                aistate &b = d->ai->getstate();
                if(b.targtype == AI_T_AFFINITY) continue; // don't override any affinity states
                if(!hasweap(d, attr) && (!hasweap(d, weappref(d)) || d->carry(sweap) == 0) && wantsweap(d, attr))
                {
                    if(b.type == AI_S_INTEREST && (b.targtype == AI_T_ENTITY || b.targtype == AI_T_DROP))
                    {
                        if(entities::ents.inrange(b.target))
                        {
                            int weap = w_attr(game::gamemode, game::mutators, entities::ents[ent]->type, entities::ents[b.target]->attrs[0], sweap);
                            if((attr == weappref(d) && weap != weappref(d)) || d->o.squaredist(entities::ents[ent]->o) < d->o.squaredist(entities::ents[b.target]->o))
                                d->ai->switchstate(b, AI_S_INTEREST, AI_T_ENTITY, ent);
                        }
                        continue;
                    }
                    d->ai->switchstate(b, AI_S_INTEREST, AI_T_ENTITY, ent);
                }
            }
        }
    }

    bool check(gameent *d, aistate &b)
    {
        if(d->actortype == A_BOT)
        {
            if(m_capture(game::gamemode) && capture::aicheck(d, b)) return true;
            else if(m_defend(game::gamemode) && defend::aicheck(d, b)) return true;
            else if(m_bomber(game::gamemode) && bomber::aicheck(d, b)) return true;
        }
        return false;
    }

    bool dowait(gameent *d, aistate &b)
    {
        //d->ai->clear(true); // ensure they're clean
        if(check(d, b) || find(d, b)) return true;
        if(!passive())
        {
            if(target(d, b, 4, false)) return true;
            if(target(d, b, 4, true)) return true;
        }
        if(actor[d->actortype].canmove && randomnode(d, b, CLOSEDIST, 1e16f))
        {
            d->ai->switchstate(b, AI_S_INTEREST, AI_T_NODE, d->ai->route[0]);
            return true;
        }
        return false; // but don't pop the state
    }

    bool dodefense(gameent *d, aistate &b)
    {
        if(d->state != CS_ALIVE) return false;
        switch(b.targtype)
        {
            case AI_T_ENTITY:
            {
                if(check(d, b)) return true;
                if(entities::ents.inrange(b.target)) return defense(d, b, entities::ents[b.target]->o);
                break;
            }
            case AI_T_AFFINITY:
            {
                if(m_capture(game::gamemode)) return capture::aidefense(d, b);
                else if(m_defend(game::gamemode)) return defend::aidefense(d, b);
                else if(m_bomber(game::gamemode)) return bomber::aidefense(d, b);
                break;
            }
            case AI_T_ACTOR:
            {
                if(check(d, b)) return true;
                gameent *e = game::getclient(b.target);
                if(e && d->team == e->team)
                {
                    if(e->state == CS_ALIVE) return defense(d, b, e->feetpos());
                    if(b.owner >= 0) return patrol(d, b, d->feetpos());
                }
                break;
            }
            default:
            {
                if(check(d, b)) return true;
                if(iswaypoint(b.target)) return defense(d, b, waypoints[b.target].o, CLOSEDIST, 0, 0);
                break;
            }
        }
        return false;
    }

    bool dointerest(gameent *d, aistate &b)
    {
        if(d->state != CS_ALIVE || !actor[d->actortype].canmove) return false;
        switch(b.targtype)
        {
            case AI_T_ENTITY:
            {
                if(entities::ents.inrange(b.target))
                {
                    gameentity &e = *(gameentity *)entities::ents[b.target];
                    if(enttype[e.type].usetype != EU_ITEM || e.type != WEAPON) return false;
                    int sweap = m_weapon(game::gamemode, game::mutators), attr = w_attr(game::gamemode, game::mutators, e.type, e.attrs[0], sweap);
                    if(!isweap(attr) || !e.spawned() || !wantsweap(d, attr)) return false;
                    //float guard = enttype[e.type].radius;
                    //if(d->feetpos().squaredist(e.o) <= guard*guard)
                    //    b.acttype = enemy(d, b, e.o, guard*4, weaptype[d->weapselect].melee ? 1 : 0, false) ? AI_A_PROTECT : AI_A_IDLE;
                    return makeroute(d, b, e.o);
                }
                break;
            }
            case AI_T_DROP:
            {
                loopvj(projs::projs) if(projs::projs[j]->projtype == PRJ_ENT && projs::projs[j]->ready() && projs::projs[j]->id == b.target)
                {
                    projent &proj = *projs::projs[j];
                    if(!entities::ents.inrange(proj.id) || proj.owner == d) return false;
                    gameentity &e = *(gameentity *)entities::ents[proj.id];
                    if(enttype[entities::ents[proj.id]->type].usetype != EU_ITEM || e.type != WEAPON) return false;
                    int sweap = m_weapon(game::gamemode, game::mutators), attr = w_attr(game::gamemode, game::mutators, e.type, e.attrs[0], sweap);
                    if(!isweap(attr) || !wantsweap(d, attr)) return false;
                    //float guard = enttype[e.type].radius;
                    //if(d->feetpos().squaredist(e.o) <= guard*guard)
                    //    b.acttype = enemy(d, b, e.o, guard*4, weaptype[d->weapselect].melee ? 1 : 0, false) ? AI_A_PROTECT : AI_A_IDLE;
                    return makeroute(d, b, proj.o);
                }
                break;
            }
            default: // this is like a wait state without sitting still..
            {
                if(check(d, b) || (b.owner < 0 && find(d, b))) return true;
                if(target(d, b, b.owner < 0 ? 0 : 4, true)) return true;
                if(iswaypoint(b.target) && (b.owner >= 0 || d->lastnode != b.target))
                    return defense(d, b, waypoints[b.target].o, CLOSEDIST, b.owner >= 0 ? 0.f : FARDIST, b.owner >= 0 ? 0 : 2);
                break;
            }
        }
        return false;
    }

    bool dopursue(gameent *d, aistate &b)
    {
        if(d->state != CS_ALIVE) return false;
        switch(b.targtype)
        {
            case AI_T_AFFINITY:
            {
                if(m_capture(game::gamemode)) return capture::aipursue(d, b);
                else if(m_defend(game::gamemode)) return defend::aipursue(d, b);
                else if(m_bomber(game::gamemode)) return bomber::aipursue(d, b);
                break;
            }

            case AI_T_ACTOR:
            {
                //if(check(d, b)) return true;
                gameent *e = game::getclient(b.target);
                if(e && targetable(d, e))
                {
                    if(e->state == CS_ALIVE)
                    {
                        bool alt = altfire(d, e);
                        if(!actor[d->actortype].canmove)
                        {
                            if(cansee(d, d->o, e->o, d->actortype >= A_ENEMY) || (e->clientnum == d->ai->enemy && d->ai->enemyseen && lastmillis-d->ai->enemyseen <= (d->skill*30)+1000))
                                return true;
                            return false;
                        }
                        return patrol(d, b, e->feetpos(), weapmindist(d->weapselect, alt), weapmaxdist(d->weapselect, alt));
                    }
                    if(b.owner >= 0) return patrol(d, b, d->feetpos());
                }
                break;
            }
            default:
            {
                if(check(d, b)) return true;
                if(iswaypoint(b.target)) return defense(d, b, waypoints[b.target].o);
                break;
            }
        }
        return false;
    }

    int closenode(gameent *d)
    {
        vec pos = d->feetpos();
        int node1 = -1, node2 = -1, node3 = -1;
        float mindist1 = CLOSEDIST*CLOSEDIST, mindist2 = RETRYDIST*RETRYDIST, mindist3 = mindist2;
        loopv(d->ai->route) if(iswaypoint(d->ai->route[i]))
        {
            vec epos = waypoints[d->ai->route[i]].o;
            float dist = epos.squaredist(pos);
            if(dist > mindist3) continue;
            if(dist < mindist1) { node1 = i; mindist1 = dist; }
            else
            {
                int entid = obstacles.remap(d, d->ai->route[i], epos);
                if(entid >= 0)
                {
                    if(entid != i) dist = epos.squaredist(pos);
                    if(dist < mindist2) { node2 = i; mindist2 = dist; }
                }
                else if(dist < mindist3) { node3 = i; mindist3 = dist; }
            }
        }
        return node1 >= 0 ? node1 : (node2 >= 0 ? node2 : node3);
    }

    int wpspot(gameent *d, int n, bool check = false)
    {
        if(iswaypoint(n)) loopk(2)
        {
            vec epos = waypoints[n].o;
            int entid = obstacles.remap(d, n, epos, k!=0);
            if(iswaypoint(entid))
            {
                vec feet = d->feetpos();
                float zoff = epos.z-d->feetpos().z;
                if(!actor[d->actortype].canjump && zoff >= JUMPMIN) epos.z = feet.z;
                else if(actor[d->actortype].canjump && d->airtime(lastmillis) >= 25 && zoff <= -JUMPMIN) epos.z = feet.z;
                d->ai->spot = epos;
                d->ai->targnode = entid;
                return !check || feet.squaredist(epos) > MINWPDIST*MINWPDIST ? 1 : 2;
            }
        }
        return 0;
    }

    int randomlink(gameent *d, int n)
    {
        if(iswaypoint(n) && waypoints[n].haslinks())
        {
            waypoint &w = waypoints[n];
            static vector<int> linkmap; linkmap.setsize(0);
            loopi(MAXWAYPOINTLINKS)
            {
                if(!w.links[i]) break;
                if(iswaypoint(w.links[i]) && !d->ai->hasprevnode(w.links[i]) && d->ai->route.find(w.links[i]) < 0)
                    linkmap.add(w.links[i]);
            }
            if(!linkmap.empty()) return linkmap[rnd(linkmap.length())];
        }
        return -1;
    }

    bool anynode(gameent *d, aistate &b, int len = NUMPREVNODES)
    {
        if(iswaypoint(d->lastnode)) loopk(2)
        {
            //d->ai->clear(k ? true : false);
            int n = randomlink(d, d->lastnode);
            if(wpspot(d, n))
            {
                d->ai->route.add(n);
                d->ai->route.add(d->lastnode);
                loopi(len)
                {
                    n = randomlink(d, n);
                    if(iswaypoint(n)) d->ai->route.insert(0, n);
                    else break;
                }
                return true;
            }
        }
        return false;
    }

    bool checkroute(gameent *d, int n)
    {
        if(d->ai->route.empty() || !d->ai->route.inrange(n)) return false;
        int len = d->ai->route.length();
        if(len <= 2 || (d->ai->lastcheck && lastmillis-d->ai->lastcheck <= 100)) return false;
        int w = iswaypoint(d->lastnode) ? d->lastnode : d->ai->route[n], c = min(len, NUMPREVNODES);
        if(c >= 3) loopj(c) // check ahead to see if we need to go around something
        {
            int m = len-j-1;
            if(m <= 1) return false; // route length is too short from this point
            int v = d->ai->route[j];
            if(d->ai->hasprevnode(v) || obstacles.find(v, d)) // something is in the way, try to remap around it
            {
                d->ai->lastcheck = lastmillis;
                loopi(m)
                {
                    int q = j+i+1, t = d->ai->route[q];
                    if(!d->ai->hasprevnode(t) && !obstacles.find(t, d))
                    {
                        static vector<int> remap; remap.setsize(0);
                        if(route(d, w, t, remap, obstacles))
                        { // kill what we don't want and put the remap in
                            while(d->ai->route.length() > i) d->ai->route.pop();
                            loopvk(remap) d->ai->route.add(remap[k]);
                            return true;
                        }
                        return false; // we failed
                    }
                }
                return false;
            }
        }
        return false;
    }

    bool hunt(gameent *d, aistate &b)
    {
        if(!d->ai->route.empty())
        {
            int n = closenode(d);
            if(d->ai->route.inrange(n) && checkroute(d, n)) n = closenode(d);
            if(d->ai->route.inrange(n))
            {
                if(!n)
                {
                    switch(wpspot(d, d->ai->route[n], true))
                    {
                        case 2: d->ai->clear(false);
                        case 1: return true; // not close enough to pop it yet
                        case 0: default: break;
                    }
                }
                else
                {
                    while(d->ai->route.length() > n+1) d->ai->route.pop(); // waka-waka-waka-waka
                    int m = n-1; // next, please!
                    if(d->ai->route.inrange(m) && wpspot(d, d->ai->route[m])) return true;
                }
            }
        }
        b.override = false;
        return anynode(d, b);
    }

    void jumpto(gameent *d, aistate &b, const vec &pos)
    {
        vec off = vec(pos).sub(d->feetpos());
        int airtime = d->airtime(lastmillis);
        bool sequenced = d->ai->blockseq || d->ai->targseq, offground = airtime && !physics::liquidcheck(d) && !d->onladder,
             impulse = airtime > (b.acttype >= AI_A_LOCKON ? 100 : 250) && !d->turnside && (b.acttype >= AI_A_LOCKON || off.z >= JUMPMIN) && physics::canimpulse(d, IM_A_BOOST, false) && (m_freestyle(game::gamemode, game::mutators) || impulsemeter-d->impulse[IM_METER] >= impulsecost),
             jumper = !offground && (b.acttype == AI_A_LOCKON || sequenced || off.z >= JUMPMIN || (d->actortype == A_BOT && lastmillis >= d->ai->jumprand)),
             jump = (impulse || jumper) && lastmillis >= d->ai->jumpseed;
        if(jump)
        {
            vec old = d->o;
            d->o = vec(pos).add(vec(0, 0, d->height));
            if(collide(d, vec(0, 0, 1))) jump = false;
            d->o = old;
            if(jump)
            {
                loopi(entities::lastent(PUSHER)) if(entities::ents[i]->type == PUSHER)
                {
                    gameentity &e = *(gameentity *)entities::ents[i];
                    float radius = (e.attrs[3] ? e.attrs[3] : enttype[e.type].radius)*1.5f; radius *= radius;
                    if(e.o.squaredist(pos) <= radius) { jump = false; break; }
                }
            }
        }
        if(d->action[AC_JUMP] != jump)
        {
            d->action[AC_JUMP] = jump;
            d->actiontime[AC_JUMP] = lastmillis;
        }
        if(jumper && d->action[AC_JUMP])
        {
            int seed = (111-d->skill)*(b.acttype == AI_A_LOCKON || b.acttype == AI_A_HASTE ? 2 : (d->onladder || d->inliquid || b.acttype == AI_A_PROTECT ? 4 : 8));
            d->ai->jumpseed = lastmillis+seed+rnd(seed);
            seed *= b.acttype == AI_A_IDLE ? 500 : 100;
            d->ai->jumprand = lastmillis+seed+rnd(seed);
        }
        if(!sequenced && !d->onladder && airtime)
        {
            if(airtime > (b.acttype >= AI_A_LOCKON ? 250 : 500) && !d->turnside && (d->skill >= 100 || !rnd(101-d->skill)) && physics::canimpulse(d, IM_A_PARKOUR, true))
                d->action[AC_SPECIAL] = true;
            else if(!passive() && lastmillis-d->ai->lastmelee >= (201-d->skill)*5 && d->canmelee(m_weapon(game::gamemode, game::mutators), lastmillis))
            {
                d->action[AC_SPECIAL] = true;
                d->ai->lastmelee = lastmillis;
            }
        }
    }

    bool lockon(gameent *d, gameent *e, float maxdist, bool check)
    {
        if(!passive() && check && !d->blocked && (d->inmaterial&MATF_CLIP) != MAT_AICLIP)
        {
            vec dir = vec(e->o).sub(d->o);
            float xydist = dir.x*dir.x+dir.y*dir.y, zdist = dir.z*dir.z, mdist = maxdist*maxdist, ddist = d->radius*d->radius+e->radius*e->radius;
            if(zdist <= ddist && xydist >= ddist+4 && xydist <= mdist+ddist) return true;
        }
        return false;
    }

    void process(gameent *d, aistate &b, bool &occupied, bool &firing, bool &enemyok)
    {
        int skmod = max(101-d->skill, 1);
        float frame = d->skill <= 100 ? ((lastmillis-d->ai->lastrun)*(100.f/gamespeed))/float(skmod*10) : 1;
        if(!actor[d->actortype].canstrafe && d->skill <= 100) frame *= 2;
        if(d->dominating.length()) frame *= 1+d->dominating.length();

        d->action[AC_SPECIAL] = d->ai->dontmove = false;
        if(b.acttype == AI_A_IDLE || !actor[d->actortype].canmove)
        {
            frame *= 10;
            d->ai->dontmove = true;
            d->ai->spot = d->feetpos();
        }
        else if(hunt(d, b))
        {
            vec fp = d->feetpos();
            game::getyawpitch(fp, d->ai->spot, d->ai->targyaw, d->ai->targpitch);
            if(d->ai->route.length() <= 1 && d->ai->spot.squaredist(fp) <= MINWPDIST*MINWPDIST) d->ai->dontmove = true;
        }
        else
        {
            if((d->blocked || (d->inmaterial&MATF_CLIP) == MAT_AICLIP) && (!d->ai->lastturn || lastmillis-d->ai->lastturn >= 1000))
            {
                d->ai->targyaw += 90+rnd(180);
                d->ai->lastturn = lastmillis;
            }
            d->ai->targpitch = 0;
            vec dir(d->ai->targyaw, d->ai->targpitch);
            d->ai->spot = vec(d->feetpos()).add(dir.mul(CLOSEDIST));
            d->ai->targnode = -1;
        }

        gameent *e = game::getclient(d->ai->enemy);
        if(passive()) enemyok = false;
        else if(!(enemyok = (e && targetable(d, e, true))) || d->skill >= 50 || d->ai->dontmove)
        {
            gameent *f = game::intersectclosest(d->o, d->ai->target, d);
            if(f)
            {
                if(targetable(d, f, true))
                {
                    if(!enemyok) violence(d, b, f, weaptype[d->weapselect].melee ? 1 : 0);
                    enemyok = true;
                    e = f;
                }
                else enemyok = false; // would hit non-targetable person
            }
            else if((!enemyok || d->ai->dontmove) && target(d, b, weaptype[d->weapselect].melee ? 1 : 0, d->ai->dontmove))
                enemyok = (e = game::getclient(d->ai->enemy)) != NULL;
        }
        if(enemyok)
        {
            bool alt = altfire(d, e);
            vec ep = getaimpos(d, e, alt);
            float yaw, pitch;
            game::getyawpitch(d->o, ep, yaw, pitch);
            game::fixrange(yaw, pitch);
            bool insight = cansee(d, d->o, e->o), hasseen = d->ai->enemyseen && lastmillis-d->ai->enemyseen <= (d->skill*10)+1000,
                 quick = d->ai->enemyseen && lastmillis-d->ai->enemyseen <= (W2(d->weapselect, fullauto, alt) ? W2(d->weapselect, delayattack, alt)*3 : skmod*3)+skmod*3;
            if(insight) d->ai->enemyseen = lastmillis;
            if(d->ai->dontmove || insight || hasseen || quick)
            {
                frame *= insight || d->skill > 100 ? 1.5f : (hasseen || quick ? 1.25f : 1.f);
                if(lockon(d, e, actor[d->actortype].canstrafe ? 32 : 16, weaptype[d->weapselect].melee))
                {
                    frame *= 2.f;
                    b.acttype = AI_A_LOCKON;
                    d->ai->dontmove = false;
                    d->ai->targyaw = yaw;
                    d->ai->targpitch = pitch;
                    d->ai->spot = e->feetpos();
                }
                game::scaleyawpitch(d->yaw, d->pitch, yaw, pitch, frame, frame*0.75f);
                bool shoot = canshoot(d, e, alt);
                if(d->action[alt ? AC_SECONDARY : AC_PRIMARY] && W2(d->weapselect, cooktime, alt) && W2(d->weapselect, cooked, alt))
                { // TODO: make AI more aware of what they're shooting
                    int cooked = W2(d->weapselect, cooked, alt);
                    if(cooked&8) shoot = false; // inverted life
                }
                if(shoot && hastarget(d, b, e, alt, insight || (!d->ai->dontmove && quick), yaw, pitch))
                {
                    d->action[alt ? AC_SECONDARY : AC_PRIMARY] = true;
                    d->actiontime[alt ? AC_SECONDARY : AC_PRIMARY] = lastmillis;
                    firing = true;
                }
                occupied = true;
            }
            else enemyok = false;
        }
        if(!enemyok)
        {
            d->ai->enemy = -1;
            d->ai->enemyseen = d->ai->enemymillis = 0;
        }
        if(!firing) d->action[AC_PRIMARY] = d->action[AC_SECONDARY] = false;

        game::fixrange(d->ai->targyaw, d->ai->targpitch);
        if(!occupied)
        {
            if(!m_insta(game::gamemode, game::mutators))
            {
                if(b.acttype == AI_A_NORMAL && (d->health <= m_health(game::gamemode, game::mutators, d->model)/3 || (iswaypoint(d->ai->targnode) && obstacles.find(d->ai->targnode, d))))
                    b.acttype = AI_A_HASTE;
                if(b.acttype == AI_A_HASTE) frame *= 1+(max(m_health(game::gamemode, game::mutators, d->model)/3, 1)/float(max(d->health, 1)));
            }
            else frame *= 2;
            game::scaleyawpitch(d->yaw, d->pitch, d->ai->targyaw, d->ai->targpitch, frame, frame*0.5f);
        }

        if(actor[d->actortype].canjump) jumpto(d, b, d->ai->spot);
        if((d->actortype == A_BOT || d->actortype == A_GRUNT) && d->action[AC_CROUCH] != d->ai->dontmove)
            if((d->action[AC_CROUCH] = !d->action[AC_CROUCH]) == true) d->actiontime[AC_CROUCH] = lastmillis;

        if(d->ai->dontmove || (d->actortype >= A_ENEMY && lastmillis-d->lastpain <= PHYSMILLIS/3)) d->move = d->strafe = 0;
        else if(!actor[d->actortype].canmove || !actor[d->actortype].canstrafe)
        {
            d->move = actor[d->actortype].canmove ? 1 : 0;
            d->strafe = 0;
        }
        else
        { // our guys move one way.. but turn another?! :)
            const struct aimdir { int move, strafe, offset; } aimdirs[8] =
            {
                {  1,  0,   0 },
                {  1,  -1,  45 },
                {  0,  -1,  90 },
                { -1,  -1,  135 },
                { -1,  0,   180 },
                { -1,  1,   225 },
                {  0,  1,   270 },
                {  1,  1,   315 }
            };
            float yaw = d->ai->targyaw-d->yaw;
            while(yaw < 0.0f) yaw += 360.0f;
            while(yaw >= 360.0f) yaw -= 360.0f;
            const aimdir &ad = aimdirs[clamp(((int)floor((yaw+22.5f)/45.0f))&7, 0, 7)];
            d->move = ad.move;
            d->strafe = ad.strafe;
        }
        if(!actor[d->actortype].canstrafe && d->move && enemyok && lockon(d, e, 8, weaptype[d->weapselect].melee)) d->move = 0;
        findorientation(d->o, d->yaw, d->pitch, d->ai->target);
    }

    bool hasrange(gameent *d, gameent *e, int weap)
    {
        if(!targetable(d, e)) return false;
        loopk(2)
        {
            float dist = e->o.squaredist(d->o);
            if(weaprange(d, weap, k!=0, dist)) return true;
        }
        return false;
    }

    bool request(gameent *d, aistate &b)
    {
        int sweap = m_weapon(game::gamemode, game::mutators);
        bool occupied = false, firing = false, enemyok = false,
             haswaited = d->weapwaited(d->weapselect, lastmillis, (1<<W_S_RELOAD));
        process(d, b, occupied, firing, enemyok);
        if(d->actortype == A_BOT)
        {
            if(d->ai->dontmove && haswaited && !firing && d->carry(sweap, 1) > 1)
            {
                loopirev(W_ITEM) if(i != weappref(d) && d->candrop(i, sweap, lastmillis, m_loadout(game::gamemode, game::mutators), (1<<W_S_SWITCH)|(1<<W_S_RELOAD)))
                {
                    client::addmsg(N_DROP, "ri3", d->clientnum, lastmillis-game::maptime, i);
                    d->setweapstate(d->weapselect, W_S_WAIT, weaponswitchdelay, lastmillis);
                    d->ai->lastaction = lastmillis;
                    return true;
                }
            }
            if(haswaited && !firing && !d->action[AC_USE])
            {
                static vector<actitem> actitems;
                actitems.setsize(0);
                vec pos = d->center();
                float radius = max(d->height*0.5f, max(d->xradius, d->yradius));
                if(entities::collateitems(d, pos, radius, actitems))
                {
                    while(!actitems.empty())
                    {
                        actitem &t = actitems.last();
                        int ent = -1;
                        switch(t.type)
                        {
                            case actitem::ENT:
                            {
                                if(!entities::ents.inrange(t.target)) break;
                                extentity &e = *entities::ents[t.target];
                                if(enttype[e.type].usetype != EU_ITEM || e.type != WEAPON) break;
                                ent = t.target;
                                break;
                            }
                            case actitem::PROJ:
                            {
                                if(!projs::projs.inrange(t.target)) break;
                                projent &proj = *projs::projs[t.target];
                                if(!entities::ents.inrange(proj.id)) break;
                                extentity &e = *entities::ents[proj.id];
                                if(enttype[e.type].usetype != EU_ITEM || e.type != WEAPON || proj.owner == d) break;
                                ent = proj.id;
                                break;
                            }
                            default: break;
                        }
                        if(entities::ents.inrange(ent))
                        {
                            extentity &e = *entities::ents[ent];
                            int attr = w_attr(game::gamemode, game::mutators, e.type, e.attrs[0], sweap);
                            if(d->canuse(e.type, attr, e.attrs, sweap, lastmillis, (1<<W_S_SWITCH)|(1<<W_S_RELOAD)))
                            {
                                if(!wantsweap(d, attr)) break;
                                d->action[AC_USE] = true;
                                d->ai->lastaction = d->actiontime[AC_USE] = lastmillis;
                                return true;
                            }
                        }
                        actitems.pop();
                    }
                }
            }
        }

        bool timepassed = d->weapstate[d->weapselect] == W_S_IDLE && (d->ammo[d->weapselect] <= 0 || lastmillis-d->weaplast[d->weapselect] >= max(6000-(d->skill*50), weaponswitchdelay));

        if(!firing && (!occupied || d->ammo[d->weapselect] <= 0) && timepassed && d->hasweap(d->weapselect, sweap) && weapons::weapreload(d, d->weapselect))
        {
            d->ai->lastaction = lastmillis;
            return true;
        }

        if(!firing && timepassed)
        {
            int weap = weappref(d);
            gameent *e = game::getclient(d->ai->enemy);
            if(!isweap(weap) || !d->hasweap(weap, sweap) || (e && !hasrange(d, e, weap)))
            {
                loopirev(W_MAX) if(i >= W_MELEE && d->hasweap(i, sweap) && (!e || hasrange(d, e, i)))
                {
                    weap = i;
                    break;
                }
            }
            if(isweap(weap) && weap != d->weapselect && weapons::weapselect(d, weap, (1<<W_S_SWITCH)|(1<<W_S_RELOAD)))
            {
                d->ai->lastaction = lastmillis;
                return true;
            }
        }

        return occupied;
    }

    bool transport(gameent *d, int find = 0)
    {
        vec pos = d->feetpos();
        static vector<int> candidates; candidates.setsize(0);
        if(find) findwaypointswithin(pos, WAYPOINTRADIUS, RETRYDIST*find, candidates);
        if(find ? !candidates.empty() : !d->ai->route.empty()) loopk(2)
        {
            int best = -1;
            float dist = 1e16f;
            loopv(find ? candidates : d->ai->route)
            {
                int n = find ? candidates[i] : d->ai->route[i];
                if((k || (!d->ai->hasprevnode(n) && n != d->lastnode)) && !obstacles.find(n, d))
                {
                    float v = waypoints[n].o.squaredist(pos);
                    if(!iswaypoint(best) || v < dist)
                    {
                        best = n;
                        dist = v;
                    }
                }
            }
            if(iswaypoint(best))
            {
                d->o = waypoints[best].o;
                d->o.z += d->height;
                d->resetinterp();
                return true;
            }
        }
        if(find <= 1) return transport(d, find+1);
        return false;
    }

    void timeouts(gameent *d, aistate &b)
    {
        if(d->blocked || (d->inmaterial&MATF_CLIP) == MAT_AICLIP)
        {
            d->ai->blocktime += lastmillis-d->ai->lastrun;
            if(d->ai->blocktime > (d->ai->blockseq+1)*500)
            {
                d->ai->blockseq++;
                switch(d->ai->blockseq)
                {
                    case 1: break;
                    case 2:
                        d->ai->clear(d->ai->blockseq != 1);
                        if(d->ai->blockseq != 1 && iswaypoint(d->ai->targnode))
                        {
                            if(!d->ai->hasprevnode(d->ai->targnode)) d->ai->addprevnode(d->ai->targnode);
                            waypoints[d->ai->targnode].weight = -1;
                            wpavoid.avoidnear(NULL, waypoints[d->ai->targnode].o.z + WAYPOINTRADIUS, waypoints[d->ai->targnode].o, WAYPOINTRADIUS);
                        }
                        break;
                    case 3: if(!transport(d)) d->ai->reset(false); break;
                    case 4: default:
                        if(b.type != AI_S_WAIT) { game::suicide(d, HIT_LOST); return; } // this is our last resort..
                        else d->ai->blockseq = 0; // waiting, so just try again..
                        break;
                }
                if(aidebug >= 7 && dbgfocus(d))
                    conoutf("%s blocked %dms sequence %d", game::colourname(d), d->ai->blocktime, d->ai->blockseq);
            }
        }
        else d->ai->blocktime = d->ai->blockseq = 0;

        if(iswaypoint(d->ai->targnode) && (d->ai->targnode == d->ai->targlast || d->ai->hasprevnode(d->ai->targnode)))
        {
            d->ai->targtime += lastmillis-d->ai->lastrun;
            if(d->ai->targtime > (d->ai->targseq+1)*500)
            {
                d->ai->targseq++;
                switch(d->ai->targseq)
                {
                    case 1: break;
                    case 2:
                        d->ai->clear(d->ai->targseq != 1);
                        if(iswaypoint(d->ai->targnode) && !d->ai->hasprevnode(d->ai->targnode))
                            d->ai->addprevnode(d->ai->targnode);
                        break;
                    case 3: if(!transport(d)) d->ai->reset(false); break;
                    case 4: default:
                        if(b.type != AI_S_WAIT) { game::suicide(d, HIT_LOST); return; } // this is our last resort..
                        else d->ai->blockseq = 0; // waiting, so just try again..
                        break;
                }
                if(aidebug >= 7 && dbgfocus(d))
                    conoutf("%s targeted %d too long %dms sequence %d", game::colourname(d), d->ai->targnode, d->ai->targtime, d->ai->targseq);
            }
        }
        else
        {
            d->ai->targtime = d->ai->targseq = 0;
            d->ai->targlast = d->ai->targnode;
        }
    }

    void logic(gameent *d, aistate &b)
    {
        if(d->speedscale != 0)
        {
            if(d->state != CS_ALIVE || !game::allowmove(d)) d->stopmoving(true);
            else
            {
                if(!request(d, b)) target(d, b, weaptype[d->weapselect].melee ? 1 : 0);
                weapons::shoot(d, d->ai->target);
            }
        }
        if(d->state == CS_DEAD || d->state == CS_WAITING)
        {
            if(d->ragdoll) moveragdoll(d, true);
            else if(lastmillis-d->lastpain < 5000)
                physics::move(d, 1, false);
        }
        else
        {
            if(d->ragdoll) cleanragdoll(d);
            if(d->state == CS_ALIVE && gs_playing(game::gamestate))
            {
                if(d->speedscale != 0)
                {
                    physics::move(d, 1, true);
                    if(actor[d->actortype].canmove && !d->ai->dontmove) timeouts(d, b);
                }
                else
                {
                    d->move = d->strafe = 0;
                    physics::move(d, 1, true);
                }
            }
        }
        if(gs_playing(game::gamestate) && (d->state == CS_ALIVE || d->state == CS_DEAD || d->state == CS_WAITING))
            entities::checkitems(d);
    }

    void avoid()
    {
        float guessradius = max(actor[A_PLAYER].xradius, actor[A_PLAYER].yradius);
        obstacles.clear();
        int numdyns = game::numdynents();
        loopi(numdyns)
        {
            gameent *d = (gameent *)game::iterdynents(i);
            if(!d) continue; // || d->actortype >= A_ENEMY) continue;
            if(d->state != CS_ALIVE || !physics::issolid(d)) continue;
            obstacles.avoidnear(d, d->o.z + d->aboveeye + 1, d->feetpos(), guessradius + d->radius + 1);
        }
        obstacles.add(wpavoid);
        loopv(projs::projs)
        {
            projent *p = projs::projs[i];
            if(p && p->state == CS_ALIVE && p->projtype == PRJ_SHOT)
            {
                float expl = WX(WK(p->flags), p->weap, explode, WS(p->flags), game::gamemode, game::mutators, p->curscale);
                if(expl > 0) obstacles.avoidnear(p, p->o.z + expl, p->o, guessradius + expl + 1);
            }
        }
        loopi(entities::lastent(MAPMODEL)) if(entities::ents[i]->type == MAPMODEL && !entities::ents[i]->spawned())
        {
            gameentity &e = *(gameentity *)entities::ents[i];
            bool skip = false;
            loopvk(e.links) if(entities::ents.inrange(e.links[k]))
            {
                gameentity &f = *(gameentity *)entities::ents[e.links[k]];
                if(f.type != TRIGGER || !m_check(f.attrs[5], f.attrs[6], game::gamemode, game::mutators)) continue;
                if(f.attrs[2] != TA_AUTO || (f.attrs[1] != TR_TOGGLE && f.attrs[1] != TR_LINK)) continue;
                skip = true;
                break;
            }
            if(skip) continue;
            mapmodelinfo *mmi = getmminfo(e.attrs[0]);
            if(!mmi || !mmi->m) continue;
            vec center, radius;
            mmi->m->collisionbox(center, radius);
            if(e.attrs[5])
            {
                center.mul(e.attrs[5]/100.f);
                radius.mul(e.attrs[5]/100.f);
            }
            if(!mmi->m->ellipsecollide) rotatebb(center, radius, int(e.attrs[1]), int(e.attrs[2]));
            obstacles.avoidnear(NULL, e.o.z + radius.z, e.o, max(radius.x, max(radius.y, radius.z)) + WAYPOINTRADIUS);
        }
    }

    void think(gameent *d, bool run)
    {
        // the state stack works like a chain of commands, certain commands simply replace each other
        // others spawn new commands to the stack the ai reads the top command from the stack and executes
        // it or pops the stack and goes back along the history until it finds a suitable command to execute
        bool cleannext = false;
        if(d->ai->state.empty()) d->ai->addstate(AI_S_WAIT);
        loopvrev(d->ai->state)
        {
            aistate &c = d->ai->state[i];
            if(c.owner >= 0)
            {
                gameent *e = game::getclient(c.owner);
                if(!e || e->team != d->team) c.owner = -1;
            }
            if(cleannext)
            {
                c.millis = lastmillis;
                c.override = false;
                cleannext = false;
            }
            if(d->state == CS_DEAD)
            {
                if(d->respawned < 0 && (!d->lastdeath || lastmillis-d->lastdeath > (d->actortype == A_BOT ? 500 : enemyspawntime)))
                {
                    client::addmsg(N_TRYSPAWN, "ri", d->clientnum);
                    d->respawned = lastmillis;
                }
            }
            else if(d->state == CS_ALIVE && run)
            {
                bool result = false;
                c.acttype = m_insta(game::gamemode, game::mutators) ? AI_A_HASTE : AI_A_NORMAL;
                switch(c.type)
                {
                    case AI_S_WAIT: result = dowait(d, c); break;
                    case AI_S_DEFEND: result = dodefense(d, c); break;
                    case AI_S_PURSUE: result = dopursue(d, c); break;
                    case AI_S_INTEREST: result = dointerest(d, c); break;
                    default: result = 0; break;
                }
                if(!result && c.type != AI_S_WAIT)
                {
                    d->ai->removestate(i);
                    cleannext = true;
                    continue; // logic is run on working states
                }
            }
            logic(d, c);
            break;
        }
        if(d->ai->tryreset) d->ai->reset();
        d->ai->lastrun = lastmillis;
    }

    void drawroute(gameent *d, float amt)
    {
        int colour = game::getcolour(d, game::playerdisplaytone), last = -1;
        loopvrev(d->ai->route)
        {
            if(d->ai->route.inrange(last))
            {
                int index = d->ai->route[i], prev = d->ai->route[last];
                if(iswaypoint(index) && iswaypoint(prev))
                {
                    waypoint &w = waypoints[index], &v = waypoints[prev];
                    vec fr = v.o, dr = w.o;
                    fr.z += amt; dr.z += amt;
                    part_trace(fr, dr, 1, 1, 1, colour);
                }
            }
            last = i;
        }
        if(aidebug >= 5)
        {
            vec pos = d->feetpos();
            if(!d->ai->spot.iszero()) part_trace(pos, d->ai->spot, 1, 1, 1, 0x00FFFF);
            if(iswaypoint(d->ai->targnode)) part_trace(pos, waypoints[d->ai->targnode].o, 1, 1, 1, 0xFF00FF);
            if(iswaypoint(d->lastnode)) part_trace(pos, waypoints[d->lastnode].o, 1, 1, 1, 0xFFFF00);
            loopi(NUMPREVNODES) if(iswaypoint(d->ai->prevnodes[i]))
            {
                part_trace(pos, waypoints[d->ai->prevnodes[i]].o, 1, 1, 1, 0x884400);
                pos = waypoints[d->ai->prevnodes[i]].o;
            }
        }
    }

    const char *stnames[AI_S_MAX] = {
        "wait", "defend", "pursue", "interest"
    }, *sttypes[AI_T_MAX+1] = {
        "none", "node", "actor", "affinity", "entity", "drop"
    }, *attypes[AI_A_MAX] = {
        "normal", "idle", "lockon", "protect", "haste"
    };
    void render()
    {
        if(aidebug >= 2)
        {
            int total = 0, alive = 0;
            loopv(game::players) if(game::players[i] && dbgfocus(game::players[i])) total++;
            loopv(game::players) if(game::players[i] && game::players[i]->state == CS_ALIVE && dbgfocus(game::players[i]))
            {
                gameent *d = game::players[i];
                vec pos = d->abovehead();
                pos.z += 3;
                alive++;
                if(aidebug >= 4 && actor[d->actortype].canmove) drawroute(d, 4.f*(float(alive)/float(total)));
                if(aidebug >= 3)
                {
                    defformatstring(q)("node: %d route: %d (%d)",
                        d->lastnode,
                        !d->ai->route.empty() ? d->ai->route[0] : -1,
                        d->ai->route.length()
                    );
                    part_textcopy(pos, q);
                    pos.z += 2;
                }
                bool top = true;
                loopvrev(d->ai->state)
                {
                    aistate &b = d->ai->state[i];
                    gameent *e = b.owner >= 0 ? game::getclient(b.owner) : NULL;
                    defformatstring(s)("%s%s (%s) %s:%d (\fs%s%s\fS%s%s%s)",
                        top ? "<default>\fg" : "<sub>\fa",
                        stnames[b.type],
                        timestr(lastmillis-b.millis),
                        sttypes[b.targtype+1], b.target,
                        top ? "\fc" : "\fw", attypes[b.acttype],
                        e ? " [" : "", e ? game::colourname(e) : "", e ? "]" : ""
                    );
                    part_textcopy(pos, s);
                    pos.z += 2;
                    if(top)
                    {
                        if(aidebug >= 3) top = false;
                        else break;
                    }
                }
                if(aidebug >= 3)
                {
                    if(isweap(weappref(d)))
                    {
                        part_textcopy(pos, W(weappref(d), name));
                        pos.z += 2;
                    }
                    gameent *e = game::getclient(d->ai->enemy);
                    if(e)
                    {
                        part_textcopy(pos, game::colourname(e));
                        pos.z += 2;
                    }
                }
            }
            if(aidebug >= 4)
            {
                int cur = 0;
                loopv(obstacles.obstacles)
                {
                    const avoidset::obstacle &ob = obstacles.obstacles[i];
                    int next = cur + ob.numwaypoints;
                    for(; cur < next; cur++)
                    {
                        int ent = obstacles.waypoints[cur];
                        if(iswaypoint(ent))
                            part_create(PART_EDIT, 1, waypoints[ent].o, 0xFF6600, 2);
                    }
                    cur = next;
                }
            }
        }
        if(showwaypoints || (dropwaypoints && showwaypointsdrop) || aidebug >= 7)
        {
            vector<int> close;
            int len = waypoints.length(), col = vec::hexcolor(showwaypointscolour).mul(0.5f).tohexcolor();
            if(showwaypointsradius)
            {
                findwaypointswithin(camera1->o, 0, showwaypointsradius, close);
                len = close.length();
            }
            loopi(len)
            {
                int idx = showwaypointsradius ? close[i] : i;
                waypoint &w = waypoints[idx];
                if(!w.haslinks()) part_create(PART_EDIT, 1, w.o, showwaypointscolour, 1.f);
                else loopj(MAXWAYPOINTLINKS)
                {
                     int link = w.links[j];
                     if(!link) break;
                     waypoint &v = waypoints[link];
                     bool both = false;
                     loopk(MAXWAYPOINTLINKS) if(v.links[k] == idx) { both = true; break; }
                     part_trace(w.o, v.o, 1, 1, 1, both ? showwaypointscolour : col);
                }
            }
            if(game::player1->state == CS_ALIVE && iswaypoint(game::player1->lastnode))
                part_trace(game::player1->feetpos(), waypoints[game::player1->lastnode].o, 1, 1, 1, 0xFFFF00);
        }
    }

    void preload()
    {
        loopi(A_TOTAL) loopk(3) preloadmodel(actor[i+A_ENEMY].playermodel[1]);
    }

    void botsay(gameent *d, gameent *t, const char *fmt, ...)
    {
        if(!d || !t) return;
        defvformatbigstring(msg, fmt, fmt);
        client::addmsg(N_TEXT, "ri3s", d->clientnum, t->clientnum, SAY_WHISPER, msg);
    }

    void scanchat(gameent *d, gameent *t, int flags, const char *text)
    {
        if((!m_edit(game::gamemode) && !m_team(game::gamemode, game::mutators)) || flags&SAY_ACTION || d->actortype != A_PLAYER) return;
        bigstring msg;
        filterbigstring(msg, text, true, true, true, true);
        const int MAXWORDS = 8;
        int numargs = MAXWORDS;
        char *w[MAXWORDS];
        const char *p = (const char *)msg;
        loopi(MAXWORDS)
        {
            w[i] = (char *)"";
            if(i > numargs) continue;
            char *s = parsetext(p);
            if(s) w[i] = s;
            else numargs = i;
        }
        if(*w[0]) loopvj(game::players) if(game::players[j] && game::players[j]->actortype == A_BOT && game::players[j]->ai)
        {
            gameent *e = game::players[j];
            if(!m_edit(game::gamemode) && d->team != e->team) continue;
            int pos = 0;
            if(!(flags&SAY_WHISPER))
            {
                if(!strncmp(w[0], "bots", 4)) pos = 1;
                else
                {
                    size_t len = strlen(e->name);
                    if(!len || strncasecmp(w[0], e->name, len)) continue;
                    switch(w[0][len])
                    {
                        case 0: break;
                        case ':': case ',': case ';': len++; break;
                        default: continue;
                    }
                    if(w[0][len] != 0) continue;
                    pos = 1;
                }
            }
            const char *affirm[4] = { "roger", "okay", "will do", "i'm on it" };
            if(!strcasecmp(w[pos], "defend"))
            {
                pos++;
                if(!strcasecmp(w[pos], "me"))
                {
                    e->ai->clear();
                    e->ai->addstate(AI_S_DEFEND, AI_T_ACTOR, d->clientnum, AI_A_PROTECT, d->clientnum);
                    botsay(e, d, "%s, defending you", affirm[rnd(4)]);
                }
                else if(!strcasecmp(w[pos], "here"))
                {
                    e->ai->clear();
                    e->ai->addstate(AI_S_DEFEND, AI_T_NODE, e->lastnode, AI_A_PROTECT, d->clientnum);
                    botsay(e, d, "%s, defending your position", affirm[rnd(4)]);
                }
                else
                {
                    bool defend = false;
                    gameent *f = NULL;
                    int numdyns = game::numdynents();
                    loopi(numdyns) if((f = (gameent *)game::iterdynents(i)) && f != e && f->team == e->team && !strcmp(w[pos], f->name))
                    {
                        e->ai->clear();
                        e->ai->addstate(AI_S_DEFEND, AI_T_ACTOR, f->clientnum, AI_A_PROTECT, d->clientnum);
                        botsay(e, d, "%s, defending %s", affirm[rnd(4)], f->name);
                        defend = true;
                        break;
                    }
                    if(!defend)
                    {
                        if(!strcasecmp(w[pos], "the")) pos++;
                        switch(game::gamemode)
                        {
                            case G_CAPTURE:
                            {
                                if(!strcasecmp(w[pos], "flag"))
                                {
                                    loopv(capture::st.flags) if(capture::st.flags[i].team == e->team)
                                    {
                                        e->ai->clear();
                                        e->ai->addstate(AI_S_DEFEND, AI_T_AFFINITY, i, AI_A_PROTECT, d->clientnum);
                                        botsay(e, d, "%s, defending the flag", affirm[rnd(4)]);
                                        break;
                                    }
                                }
                                #if 0
                                else if(!strcasecmp(w[pos], "base"))
                                {
                                    loopv(capture::st.flags) if(capture::st.flags[i].team == e->team)
                                    {
                                        e->ai->clear();
                                        e->ai->addstate(AI_S_DEFEND, AI_T_ENTITY, capture::st.flags[i].ent, AI_A_PROTECT, d->clientnum);
                                        botsay(e, d, "%s, defending base for the flag", affirm[rnd(4)]);
                                        break;
                                    }
                                }
                                #endif
                                else botsay(e, d, "'me', 'here', or 'flag'");
                                break;
                            }
                            case G_BOMBER:
                            {
                                if(!strcasecmp(w[pos], "goal") || !strcasecmp(w[pos], "base"))
                                {
                                    loopv(bomber::st.flags) if(!isbomberaffinity(bomber::st.flags[i]) && bomber::st.flags[i].team == e->team)
                                    {
                                        e->ai->clear();
                                        e->ai->addstate(AI_S_DEFEND, AI_T_AFFINITY, i, AI_A_PROTECT, d->clientnum);
                                        botsay(e, d, "%s, defending the goal", affirm[rnd(4)]);
                                        break;
                                    }
                                }
                                else botsay(e, d, "'me', 'here', or 'goal'");
                                break;
                            }
                            default: botsay(e, d, "'me', or 'here'"); break;
                        }
                    }
                }
            }
            else if(!strcasecmp(w[pos], "attack"))
            {
                pos++;
                if(!strcasecmp(w[pos], "the")) pos++;
                switch(game::gamemode)
                {
                    case G_CAPTURE:
                    {
                        if(!strcasecmp(w[pos], "flag"))
                        {
                            loopv(capture::st.flags) if(capture::st.flags[i].team != e->team)
                            {
                                e->ai->clear();
                                e->ai->addstate(AI_S_PURSUE, AI_T_AFFINITY, i, AI_A_HASTE, d->clientnum);
                                botsay(e, d, "%s, attacking the flag", affirm[rnd(4)]);
                                break;
                            }
                        }
                        #if 0
                        else if(!strcasecmp(w[pos], "base"))
                        {
                            loopv(capture::st.flags) if(capture::st.flags[i].team != e->team)
                            {
                                e->ai->clear();
                                e->ai->addstate(AI_S_DEFEND, AI_T_ENTITY, capture::st.flags[i].ent, AI_A_HASTE, d->clientnum);
                                botsay(e, d, "%s, attacking the flag", affirm[rnd(4)]);
                                break;
                            }
                        }
                        #endif
                        else botsay(e, d, "'flag' is the only option");

                        break;
                    }
                    case G_BOMBER:
                    {
                        if(!strcasecmp(w[pos], "goal") || !strcasecmp(w[pos], "base"))
                        {
                            loopv(bomber::st.flags) if(!isbomberaffinity(bomber::st.flags[i]) && bomber::st.flags[i].team != e->team)
                            {
                                e->ai->clear();
                                e->ai->addstate(AI_S_PURSUE, AI_T_AFFINITY, i, AI_A_HASTE, d->clientnum);
                                botsay(e, d, "%s, attacking the goal", affirm[rnd(4)]);
                                break;
                            }
                        }
                        else if(!strcasecmp(w[pos], "ball"))
                        {
                            loopv(bomber::st.flags) if(isbomberaffinity(bomber::st.flags[i]))
                            {
                                e->ai->clear();
                                e->ai->addstate(AI_S_PURSUE, AI_T_AFFINITY, i, AI_A_HASTE, d->clientnum);
                                botsay(e, d, "%s, attacking the ball", affirm[rnd(4)]);
                                break;
                            }
                        }
                        else botsay(e, d, "'goal', or 'ball'");
                        break;
                    }
                    default: botsay(e, d, "sorry, no attack directions in this game"); break;
                }
            }
            else if(!strcasecmp(w[pos], "forget"))
            {
                loopvrev(e->ai->state) if(e->ai->state[i].owner == d->clientnum) e->ai->state.remove(i);
                const char *quip[4] = { "back to what i was doing then", "resuming previous operations", "i am no longer your slave", "jolly good show then" };
                botsay(e, d, "%s, %s", affirm[rnd(4)], quip[rnd(4)]);
            }
            else if(!strcasecmp(w[pos], "reset"))
            {
                e->ai->reset(true, false);
                const char *quip[4] = { "what was i doing again?", "duh... off i go..", "who were you again?", "ummmm... wtf do i do now?" };
                botsay(e, d, "%s, %s", affirm[rnd(4)], quip[rnd(4)]);
            }
            else botsay(e, d, "'defend', 'attack', 'forget', or 'reset'");
        }
        loopi(numargs) DELETEA(w[i]);
    }

}
