//=====================================================================
// NemesisWINGSv3.
//      ""(§)FrÅgÊd(§)""
//               **Monarch**
//
//              555 ---===CrAzYBoNeS===--- 555
//          Boppolis_The_Dog
//
//
// Thank you Al McElrath for SLV2.
//=====================================================================
/** Contains bot behavior for riding rockets. Spawned on the server
    when the pilot or gunner is set.
*/
class SLBotBrain extends SLInfo;


var StrangeShell sl;
var Bot bot;
var bool bIsPilot;
var bool bGuiding;                  // The bot is controlling the rocket thru gr.
var bool bEject;                    // An eject is recommended.

/** Current goal index. For CTF, this is the flag base index. For
    Assault, it's the fort standard index. Both in goals[] in SkyNet.
*/
var byte goal;

var SkyNode targetNode;             // The node we're heading for.
var SkyNode lastNode;               // The previous node.
var SkyNode launchNode;             // Our first node.

var bool bNoExpire;                 // Our target node doesn't expire.
var bool bReachEject;               // Eject when we reach our target node.
var float targetAcq;                // Time we acquired this node.
var() float targetExpireTime;       // Time to give up on this node and find a reachable one.

var() int ejectDist;                // If we get this close to our objective, eject.
var() int ejectDistFlag;            // Same, but just for flag carriers and flag bases.

var() float randomLinkAdjust;       // Random factor for nextNode().
var() float wallDeter;              // The % of wall deaths which will deter us.
var() float shotDeter;              // The % of shot deaths which will deter us.

var() float skill;                  // 0.0 to 1.0. Affects reflexes and turning speed.

/** This only happens when there are no enemy flags at base.
    NOT IMPLEMENTED. FIX.
*/
var bool bHunting;

/** E.g. somebody carrying our flag. :)
*/
var Actor nukeTarget;

var SkyNet skynet;

var float suggestedSpeed;

var Util u;


function postBeginPlay() {
    u = spawn(Class'Util', self);

    // Invalid goal.
    goal = -1;
}


/** The launcher usually handles this, but just in case.
*/
function spawnSkyNet() {
    foreach allActors(Class'SkyNet', skynet)
        break;

    if (skynet == none)
        skynet = spawn(Class'SkyNet');
}


/** Called from the Strangelove, right after spawn.
*/
function setBot(Bot b, StrangeShell s, optional SkyNet net, optional SkyNode ln) {
    bot = b;
    sl = s;
    launchNode = ln;
    targetNode = ln;

    // They need all the help they can get...
    //skill = b.skill / 3.0;
    skill = (b.skill / 3.0) * 0.2 + 0.8;

    bIsPilot = (b == sl.getPilot());

    if (bIsPilot) {
        bGuiding = true;
        skynet = net;
        spawnSkyNet();
    } else {
        gotoState('GunnerBoarding');
    }

    // Skill == reaction time. Minimum timer is 0.01.
    setTimer(fmax(1.0 * (1.0 - skill), 0.01), true);

    u.debug("setBot(): brain set for: " $ u.sname(bot) $ " skill: " $ u.chopf(skill) $ " launch node: " $ u.sname(ln));
}


function timer() {
    // Eject if fuel low! Max throttle first.
    if (sl.fuel < 3 && (sl.fc == none || !(sl.fc.getAmmo() > 0))) {
        u.debug("timer(): No fuel! Ejecting!");
        if (bIsPilot)
            sl.throttle = 1.0;
        bEject = true;

    } else {
        if (bIsPilot) {
            pilotBehavior();
        } else {
            gunnerBehavior();
        }
    }

    if (bEject)
        eject();
}


/** First, set the goal. If it's valid, decide where and what to do.
*/
function pilotBehavior() {
    local SkyNode temp;

    if (setGoal()) {
        // First check for nuke targets.
        if (bot.moveTarget == myFlag()) {
            nukeTarget = bot.moveTarget;
        } else if (bot.enemy == myFlag().holder) {
            // Ramming speed!!!
            sl.arm();
            sl.throttle = 0.1;
            nukeTarget = bot.enemy;
        } else {
            nukeTarget = none;
        }

        if (nukeTarget != none) {
            targetActor(nukeTarget);

        } else {
            if (targetNode == none) {
                // This might happen at a dead end node.
                u.debug("pilotBehavior(): no target node");
                temp = closestNode(skynet.linkRadius);
                if (temp != lastNode) {
                    setTargetNode(temp);
                } else {
                    bEject = true;
                }
            }

            if (bReachEject && targetNode != none) {
                if (inrange(targetNode, 0.5)) {
                    u.debug("pilotBehavior(): within range of eject node");

                    // So we don't kill outselves. FIX!
                    sl.disarm();
                    bEject = true;
                }
            } else {
                /*
                if (inrange(skynet.goalNodes[goal], 1.5)) {
                    u.debug("pilotBehavior(): within range of goal node");
                    bEject = true;
                }
                */
            }

            // Throttle! Shoot mode!
            if (targetNode != none && !inrange(targetNode, 2.0) && linedup(targetNode, 0.01)) {
                if (suggestedSpeed == -1)
                    sl.throttle = 0.5 + 0.5 * skill;

                if (bot.enemy != none) {
                    // Switch to shoot mode!
                    if (!sl.bShootMode) {
                        u.debug("pilotBehavior(): going to shoot mode");
                        sl.goShootMode();
                    }
                }
            } else {
                if (suggestedSpeed == -1)
                    sl.throttle = 0.1;

                // Man the wheel!
                if (sl.bShootMode) {
                    u.debug("pilotBehavior(): returning from shoot mode");
                    sl.goShootMode();
                }
            }

            // Handle speed.
            if (suggestedSpeed >= 0) {
                if (suggestedSpeed < 1.0) {
                    sl.throttle = suggestedSpeed;
                } else {
                    if (abs(vsize(sl.velocity) - suggestedSpeed) < 50.0) {
                        // Hold.
                    } else {
                        if (vsize(sl.velocity) < suggestedSpeed) {
                            sl.throttle += 0.1;
                        } else {
                            sl.throttle -= 0.1;
                        }

                        sl.throttle = fclamp(sl.throttle, 0.0, 1.0);
                    }
                }
            }

            // Can we see the goal node? Go for it! Did we already
            // spot it earlier? Keep going.
            if (targetNode != none && !bReachEject) {
                temp = skynet.goalNodes[goal].randomLink();
                if (temp != none && los(temp)) {
                    u.debug("pilotBehavior(): new beeline: " $ u.sname(temp));
                    setTargetNode(temp);
                    bReachEject = true;
                    bNoExpire = true;
                }
            }

            // Expire the node if it's been too long. Usually means
            // we're doing loop de loops.
            if (!bNoExpire && targetNodeExpired()) {
                u.debug("timer(): target expired: " $ u.sname(targetNode));
                setTargetNode(nextNode(targetNode, true));
            }

            // Target it.
            if (targetNode != none)
                targetActor(targetNode);
        }
    }
}


/** My flag?
*/
function CTFFlag myFlag() {
    if (skynet.bCTF) {


        return CTFReplicationInfo(level.game.gameReplicationInfo).flagList[bot.playerReplicationInfo.team];
    }

    return none;
}


/** Returns true if this flag is an enemy flag.
*/
function bool enemyFlag(CTFFlag flag) {
    return (flag != myFlag());
}


/** Returns the distance to the specified actor.
*/
function float distto(Actor a) {
    return vsize(a.location - bot.location);
}


/** Returns true if the specified actor is within the specified
    range. If the range is < 10.0, it's a factor of the link radius.
*/
function bool inrange(Actor a, float range) {
    if (range < 10.0) {
        return (distTo(a) < range * skynet.linkRadius);
    } else {
        return (distTo(a) < range);
    }
}


/** Returns true if the bot has LOS.
*/
function bool los(Actor a) {
    u.debug("los(" $ u.sname(a) $ ")", DL_Verbose);
    return fastTrace(a.location, bot.location);
}


/** Lined up.
*/
function bool linedup(Actor a, optional float error) {
    return ((1.0 - (normal(sl.velocity) dot normal(a.location - bot.location))) <= error);
}


/** Turns us towards the specified actor.
*/
function targetActor(Actor a) {
    desiredRotation = adjustRot(rotator(a.location - bot.location));
}


/** Same as above, but for a raw location vector.
*/
function targetLoc(vector l) {
    desiredRotation = adjustRot(rotator(l - bot.location));
}


/** This sets the goal index. We may set bEject, so this needs to
    happen just before we test for that in pilotBehavior(). Returns
    true if we have a valid goal.
*/
function bool setGoal() {
    local byte oldg;

    // We need to compare with the new one for debugging.
    oldg = goal;

    if (skynet.getGoal(bot, goal)) {
        if (goal != oldg)
            u.debug("setGoal(): new goal is: " $ u.sname(skynet.goals[goal]));
        return true;
    }

    bEject = true;
    return false;
}


/** When the pilot is cleared (and a damage type is specified), it
    notifies us so we can update the node counts.
*/
function deathBy(name type) {
    local SkyNode n;
    local byte team;

    // Find the closer node. We'll always have a targetNode. Right?
    if (lastNode != none && distto(lastNode) > distto(targetNode))
        n = targetNode;
    else
        n = lastNode;

    team = bot.playerReplicationInfo.team;

    switch (name) {
    case 'Shot':
        n.shotDeaths[team]++;
        break;
    case 'HitWall':
        n.wallDeaths++;
        break;
    }
}


function bool targetNodeExpired() {
    return ((level.timeseconds - targetAcq) > targetExpireTime);
}


function setTargetNode(SkyNode sn) {
    lastNode = targetNode;

    u.debug("setTargetNode(" $ u.sname(sn) $ ")");

    targetNode = sn;

    if (targetNode != none) {
        targetAcq = level.timeseconds;

        suggestedSpeed = sn.suggestedSpeed;

        if (skynet.bShowLinks) {
            if (lastNode != none)
                lastNode.vis(false);
            targetNode.vis(true, goal);
        }

        targetNode.passes++;
        targetNode.teamPasses[bot.playerReplicationInfo.team]++;
    }
}


function SkyNode closestNode(float r) {
    return skynet.closestNode(bot.location, r);
}


/** The Strangelove reports a touch on the sky node.
*/
function nodeReached(SkyNode sn) {
    if (sn == targetNode) {
        u.debug("nodeReached(): " $ u.sname(sn));

        if (sn != launchNode)
            // Success! Or so we think...
            launchNode.addLaunch(true);

        setTargetNode(nextNode(sn, true));
    }
}


/** Decide which node to go to next from the current one. If random is
    true, we add a slight random factor in, which essentially makes close
    calls go either way, rather than always to the higher weight.
*/
function SkyNode nextNode(SkyNode cur, optional bool random, optional bool bNextNext) {
    local float RLA;
    local int i;
    local SkyNode next, nn;
    local float w, curw;

    // Use ours if non-zero.
    if (randomLinkAdjust > 0)
        RLA = randomLinkAdjust;
    else
        RLA = skynet.randomLinkAdjust;

    for (i = 0; i < arrayCount(cur.links); i++) {
        if (bNextNext && !los(cur.links[i]))
            continue;

        // Skip it if we just came from there?
        if (cur.links[i] == lastNode)
            continue;

        curw = cur.links[i].weight[goal];
        if (random)
            curw += frand() * RLA;

        if (curw > w || next == none) {
            next = cur.links[i];
            w = curw;
        }
    }

    // If the next node is weighted lower than the current, eject.
    if (next == none || w < cur.weight[goal] || avoidNode(next, bot.playerReplicationInfo.team)) {
        u.debug("nextNode(" $ u.sname(cur) $ "): ejecting: next: " $ u.sname(next) $ " current/next weight: " $ cur.weight[goal] $ "/" $ curw);
        if (!bNextNext)
            bEject = true;
    } else {
        /** FIX. Infinite recursion.
        // Skip to the next next one if we can see it.
        if (next != none) {
            nn = nextNode(next, random, true);
            if (nn != none) {
                u.debug("nextNode(): skipping " $ u.sname(next) $ " to " $ u.sname(nn));

                // This is so it doesn't expire too soon.
                targetAcq += targetExpireTime;
                next = nn;
            }
        }
        */
    }

    return next;
}


/** Return true if we don't want to go to this node, because everytime
    someone's tried, they've hit a wall or gotten shot down.
*/
function bool avoidNode(SkyNode n, byte team) {
    local bool avoid;

    avoid = ((n.wallDeaths / float(n.passes)) > wallDeter || (n.shotDeaths[team] / float(n.teamPasses[team])) > shotDeter);
    if (avoid) {
        u.debug("avoidNode(" $ u.sname(n) $ ") death ratios: wall: " $ (n.wallDeaths / float(n.passes)) $ " shot: " $ (n.shotDeaths[team] / float(n.teamPasses[team])));

        // Turn off the warhead, if we can.
        sl.disarm();
    }

    return avoid;
}


function FlagBase getFlag() {
    return CTFReplicationInfo(level.game.gameReplicationInfo).flagList[bot.playerReplicationInfo.team].homeBase;
}


/** This takes the desired rotation and slowly moves to meet
    it. Basically our own home brewed bRotateToDesired (which doesn't
    seem to work in this instance). Note we could use the full
    rotation rate, but this uses the yaw rate for both yaw and pitch.
*/
function tick(float delta) {
    if (sl != none) {
        setRotation(rotation + sturn(desiredRotation, rotation, int(delta * skill * rotationRate.yaw)));
    }
}


/** This compensates for the sliding of the rocket.
*/
function rotator adjustRot(rotator r) {
    return (r - sturn(rotator(sl.velocity), r, 0, 16384));
}


/** Given a new and old rotation, it figures the smallest deltas to
    get to the new rotation. If max is given, it limits the turn by that
    much.

    The limit optional arg is used when trying to compensate for
    momentum. I can't explain it in less than 500 words, but if you don't
    drop the compensation when the delta is over 90 degrees, it goes in
    the direct opposite direction.
*/
function rotator sturn(rotator newr, rotator oldr, optional int max, optional int limit) {
    local int dyaw, dpitch;
    local rotator turn;

    dyaw = (newr.yaw & 65535) - (oldr.yaw & 65535);
    dpitch = (newr.pitch & 65535) - (oldr.pitch & 65535);

    // If it's more than the limit, zero that component.
    if (limit > 0) {
        if (abs(dyaw) > limit)
            dyaw = 0;
        if (abs(dpitch) > limit)
            dpitch = 0;
    }

    if (dyaw < -32768)
        dyaw += 65536;
    else if (dyaw > 32768)
        dyaw -= 65536;

    if (dpitch < -32768)
        dpitch += 65536;
    else if (dpitch > 32768)
        dpitch -= 65536;

    if (max > 0) {
        dyaw = fmin(dyaw, max);
        dpitch = fmin(dpitch, max);
    }

    turn.yaw = dyaw;
    turn.pitch = dpitch;
    turn.roll = 0;

    return turn;
}


function rotator getGuidedRotation() {
    return rotation;
}


/** Decide what the bot does, as far as ejecting, etc., as a gunner.
*/
function gunnerBehavior() {

    bot.bCanFly = true;
    bot.bCanWalk = false;
    bot.bAdvancedTactics = false;

    //bot.bCanDuck = true;
    //bot.moveTarget = -1;

    // If the rider ejects or gets killed, eject!
    if (sl.getPilot() == none && !isInState('GunnerEject'))
        gotoState('GunnerEject');

    // Don't jump off unless we're less than 30' (360 uu) off the ground.
    if (sl.fastTrace(sl.location + vect(0, 0, -1) * 360)) {
        u.debug("gunnerBehavior(): over 360 off ground", DL_Verbose);

    } else {

        // We have the flag! Even if we're set not to obey orders, eject if we get within flag base dist.
        if (skynet.bCTF && bot.playerReplicationInfo.hasFlag != none && inrange(getFlag(), ejectDistFlag)) {
            u.debug("gunnerBehavior(): eject! close to flag base");
            eject();

        // We're in eject distance of something we want.
        } else if (bot.moveTarget != none && bot.moveTarget != sl.getPilot() && inrange(bot.moveTarget, ejectDist)) {
            // Eject if we're obeying orders (and they're not 'follow'
            // orders) or we're out of ammo and our movetarget is a weapon or ammo.
            if ((sl.bBotsObeyOrders && !(bot.orders == 'Follow' && bot.orderObject == sl.getPilot())) || ((bot.moveTarget.isA('Weapon') || bot.moveTarget.isA('Ammo')) && bot.weapon != none && (bot.weapon.bMeleeWeapon || bot.weapon.ammoType.ammoAmount == 0))) {
                u.debug("gunnerBehavior(): eject! within range of movetarget: " $ u.sname(bot.moveTarget));
                eject();
            }
        }
    }
}


/** This state makes it so they don't jump off immediately.
*/
state GunnerBoarding {
    ignores gunnerBehavior;

    begin:
    sleep(3.0);
    gotoState('');
}

/** Give them some response time for ejecting. This only happens if
    the rider ejects or is killed.
*/
state GunnerEject {
    ignores gunnerBehavior;

    begin:
    sleep(1.5);
    if (sl.gunner != none)
        eject();
}


function eject() {
    sl.eject(bot);
    //bot.gotoState('Roaming', 'PreBegin');
    bot.whatToDoNext('', '');
}


function destroyed() {
    u.debug("destroyed()");

    if (skynet != none) {
        if (skynet.bShowLinks) {
            if (targetNode != none) {
                targetNode.vis(false);
            }
        }
    } else {
        u.debug("destroyed(): skynet == none");
    }

    super.destroyed();
}



// end

defaultproperties
{
     targetExpireTime=3.000000
     ejectDist=1000
     ejectDistFlag=2000
     wallDeter=0.500000
     shotDeter=0.500000
     Skill=0.500000
     RotationRate=(Pitch=10000,Yaw=10000)
}
