﻿
// huffware script: huff-pet, by fred huffhines
//
// this is yet another implementation of a pet script in LSL.
//
// this script is licensed by the GPL v3 which is documented at: http://www.gnu.org/licenses/gpl.html
// do not use it in objects without fully realizing you are implicitly accepting that license.
//

//to-do zone:

//hmmm: for attack mode, adjust timers and ranges.
//  probably just define the default set and mirror that with the attack set.
//  switch between them during mode change.
//  reset timer and sensor for mode change!

// end to-do zone.

// constants for the pet that one might want to change.

integer PET_CHAT_CHANNEL = 28;
    // the channel on which the pet will listen for commands from the owner.

//integer DEFAULT_PANIC_DISTANCE = 28;
integer DEFAULT_PANIC_DISTANCE = 5;//short leash version for opensim.
    // multiplied by the sensor range to get the distance allowed from the
    // pet to the person it's following before a teleport is invoked.
integer ATTACK_PANIC_DISTANCE = 13;
    // multiplied by the sensor range to get the distance allowed from the
    // enraged pet to the attack target before a teleport is invoked.    

float DEFAULT_HEIGHT_ABOVE_FOLLOWED_OBJECT = 1.6;
    // the height that the pet will float at above whoever it's following.
float ATTACK_HEIGHT_ABOVE_FOLLOWED_OBJECT = 0.0;
    // the height that the pet will float at above the attack target.

float DEFAULT_BASE_VELOCITY = 0.1;
    // the velocity of the pet when just cavorting around.
float ATTACK_BASE_VELOCITY = 0.6;
    // the velocity of the pet when in "angry" mode.

integer DEFAULT_BUMP_SIZE = 48;
    // the default size in meters of the displacement caused by up, down, etc commands.

// the margin values below are the range of motion that the pet is allowed
// on each axis.
float DEFAULT_X_MARGIN = 3.0;
float DEFAULT_Y_MARGIN = 3.0;
float DEFAULT_Z_MARGIN = 1.0;
// the margin for when the pet is attacking.
float ATTACK_X_MARGIN = 0.1;
float ATTACK_Y_MARGIN = 0.1;
float ATTACK_Z_MARGIN = 0.2;

float MAXIMUM_TARGETING_DISTANCE = 2.0;
    // the amount of basic deviation allowed for the pet from its target spot.
    // this is how far it's allowed to roam from the target.
//is that right?

float ATTACK_PUSH_DIST_THRESHOLD = 2.0;
    // how close the pet should be to an attack target before trying to push.
float ATTACK_PUSH_MAGNITUDE = 2147483646.0;  //maxint - 1, dealing with svc-2723.
    // how much the critter should push an attack target.
float ATTACK_PUSH_CHANCE = 0.1;
    // how often (probability from 0.0 to 1.0) an attack target gets pushed.

// other constants that are more advanced and should generally not change...

float TARGETING_SENSOR_RANGE = 96.0;
    // the maximum distance the pet will try to see the target at.
    
float SENSOR_INTERVAL = 0.4;
    // how often the sensor scan will fire off.  this is the fastest we will
    // check for our follow target, in seconds.

float PERIODIC_INTERVAL = 0.42;
    // how frequently our timer event fires.

integer MAXIMUM_SLACKNESS = 28;
    // how many timer hits we'll allow before reverting to the default state.

float VELOCITY_MULTIPLIER = 1.2;
    // the total velocity comes from the base plus an equation that multiplies
    // this value by some function of the distance.

string PET_MENU_NAME = "#woof";  // name for our menu.
string PET_REPLY_MENU = "#aroo";  // replies with data.

// symbolic labels for the different states of pet 'being'.
integer STATE_STAY = 0;
integer STATE_FREE = 1;  // go home.
integer STATE_FOLLOW = 2;
integer STATE_COME = 3;
integer STATE_WANDER = 4;
integer STATE_ATTACK = 9;

list SUPPORTED_COMMANDS = [ "go home", "come", "stay",
    "wander", "follow", "attack",
    "set home", "set name", "status" ];
    // attack = follow an avatar in a menacing way.
    // come = follow owner.
    // follow = follow an avatar.
    // go home = return home, then move about freely.
    // set home = set the home position based on owner location.
    // set name = change the object's name.
    // stay = sit right there.
    // wander = roam a greater distance while following owner.

// requires: jaunting library v3.4 or higher.
//////////////
// do not redefine these constants.
integer JAUNT_HUFFWARE_ID = 10008;
    // the unique id within the huffware system for the jaunt script to
    // accept commands on.  this is used in llMessageLinked as the num parameter.
string HUFFWARE_PARM_SEPARATOR = "{~~~}";
    // this pattern is an uncommon thing to see in text, so we use it to separate
    // our commands in link messages.
string HUFFWARE_ITEM_SEPARATOR = "{|||}";
    // used to separate lists of items from each other when stored inside a parameter.
    // this allows lists to be passed as single string parameters if needed.
integer REPLY_DISTANCE = 100008;  // offset added to service's huffware id in reply IDs.
//////////////
// commands available via the jaunting library:
string JAUNT_COMMAND = "#jaunt#";
    // command used to tell jaunt script to move object.  pass a vector with the location.
string FULL_STOP_COMMAND = "#fullstop#";
    // command used to bring object to a halt.
string REVERSE_VELOCITY_COMMAND = "#reverse#";
    // makes the object reverse its velocity and travel back from whence it came.
string SET_VELOCITY_COMMAND = "#setvelocity#";
    // makes the velocity equal to the vector passed as the first parameter.
string JAUNT_UP_COMMAND = "#jauntup#";
string JAUNT_DOWN_COMMAND = "#jauntdown#";
    // commands for height adjustment.  pass a float for number of meters to move.
string JAUNT_LIST_COMMAND = "#jauntlist#";
    // like regular jaunt, but expects a list of vectors as the first parameter; this list
    // should be in the jaunter notecard format (separated by pipe characters).
    // the second parameter, if any, should be 1 for forwards traversal and 0 for backwards.
//
//////////////

//requires menutini library v4.2 or better.
//////////////
// do not redefine these constants.
integer MENUTINI_HUFFWARE_ID = 10009;
    // the unique id within the huffware system for the jaunt script to
    // accept commands on.  this is used in llMessageLinked as the num parameter.
// commands available via the menu system:
string SHOW_MENU_COMMAND = "#menu#";
    // the command that tells menutini to show a menu defined by parameters
    // that are passed along.  these must be: the menu name, the menu's title
    // (which is really the info to show as content in the main box of the menu),
    // the wrapped list of commands to show as menu buttons, the menu system
    // channel's for listening, and the key to listen to.
    // the reply will include: the menu name, the choice made and the key for
    // the avatar.
//
//////////////


request_full_stop()
{
    llMessageLinked(LINK_THIS, JAUNT_HUFFWARE_ID, FULL_STOP_COMMAND, "");
}

request_jaunt_up(integer distance)
{
    llMessageLinked(LINK_THIS, JAUNT_HUFFWARE_ID, JAUNT_UP_COMMAND, (string)distance);
}

// global variables...

key _OWNER;
integer current_state;
vector home_position;  // the location where the pet lives.

integer pending_target = FALSE;  // is a target still being sought.

integer  _COMMAND_CHANNEL;
string   _COMMAND_MESSAGE   = "How may I assist you?";

integer  _TARGET_ID;
vector TARGET_POSITION;

float    _FREE_RANGE        =  10.0;

string SIT_TEXT = "Meditate";

string SIT_ANIMATION = "yoga_float";

vector SIT_POSITION = <0.2, 0.2, 0.4>;

vector SIT_ROTATION = <0.0, 0.0, 0.0>;

key      SITTING_AVATAR_KEY           = NULL_KEY; 

// options for follow menu that pops up when pet is told to follow someone.
list     _FOLLOW_KEY;
list     _FOLLOW_NAME;  // filled in with nearby avatars.
integer  _FOLLOW_CHANNEL;
string   _FOLLOW_MESSAGE = "Who should I follow?";

integer  seeking_avatars         = FALSE;
key      KEY_OF_TARGET;

//////////////

// from hufflets...

// locates the string "text" in the list to "search_in".
integer find_in_list(list search_in, string text)
{ 
    integer len = llGetListLength(search_in);
    integer i; 
    for (i = 0; i < len; i++) { 
        if (llList2String(search_in, i) == text) 
            return i; 
    }
    return -1;
}

///////////////

integer debug_num = 0;

// a debugging output method.  can be disabled entirely in one place.
log_it(string to_say)
{
    debug_num++;
    // tell this to the owner.    
    llOwnerSay((string)debug_num + "- " + to_say);
    // say this on open chat, but use an unusual channel.
//    llSay(108, (string)debug_num + "- " + to_say);
}

///////////////

// info...

// returns a string version of the state 's'.
string name_for_state(integer s) {
    if (s == STATE_STAY) return "stay";
    if (s == STATE_FREE) return "go home";
    if (s == STATE_FOLLOW) return "follow";
    if (s == STATE_COME) return "come";
    if (s == STATE_WANDER) return "wander";
    if (s == STATE_ATTACK) return "attack";
    return "unknown";
}

// menu methods...

list current_buttons;  // holds onto the set of menu options.

integer random_channel() { return -(integer)(llFrand(40000) + 20000); }

string stringize_list(list to_flatten) {
    return llDumpList2String(to_flatten, HUFFWARE_ITEM_SEPARATOR);
}

// pops up a menu to interact with the pet's owner.
show_menu(string menu_name, string title, list buttons, integer channel)
{
    current_buttons = buttons;
    key listen_to = _OWNER;
    llMessageLinked(LINK_THIS, MENUTINI_HUFFWARE_ID, SHOW_MENU_COMMAND,
        menu_name + HUFFWARE_PARM_SEPARATOR
        + title + HUFFWARE_PARM_SEPARATOR + stringize_list(current_buttons)
        + HUFFWARE_PARM_SEPARATOR + (string)channel
        + HUFFWARE_PARM_SEPARATOR + (string)listen_to);
}

// causes a state change to make the pet stay.
enter_stay_state()
{
    current_state = STATE_STAY;
    llSensorRemove();
    stop_pet();
}

// handle the response message when the user chooses a button.
react_to_menu(integer sender, integer num, string msg, key id)
{
log_it("react menu: " + msg + " parm=" + (string)id);
    list parms = llParseString2List(id, [HUFFWARE_PARM_SEPARATOR], []);
    string menu_name = llList2String(parms, 0);
    string choice = llList2String(parms, 1);

    if ( (num != MENUTINI_HUFFWARE_ID + REPLY_DISTANCE) || (msg != SHOW_MENU_COMMAND) ) {
log_it("why here in react to menu, not for us?");
        return;
    }

    if (menu_name == PET_MENU_NAME) {
log_it("react: snd=" + (string)sender + " num=" + (string)num + " msg=" + msg + " key=" + (string)id);
        if (find_in_list(SUPPORTED_COMMANDS, choice) < 0) {
            llOwnerSay("i don't know what you mean...");
            return;
        }
llOwnerSay("i heard you tell me: menu=" + menu_name + " choice=" + choice);
    
        // handle commands that are major changes in state...
    
        if (choice == "come") {
            llOwnerSay("coming to find you now...");
            current_state = STATE_COME;
            llSetPrimitiveParams([PRIM_PHYSICS, TRUE,
            //]);
            PRIM_PHANTOM, TRUE]);
            llSensorRemove();
            llSensorRepeat("", _OWNER, AGENT, TARGETING_SENSOR_RANGE, PI, SENSOR_INTERVAL);
        }
        
        if (choice == "stay") {
            llOwnerSay("i will stay right here...");
            enter_stay_state();
        }
        
        if (choice == "wander") {
            current_state = STATE_WANDER;
            llOwnerSay("i'm going to wander around and kind of vaguely follow you...");
            llSetPrimitiveParams([PRIM_PHYSICS, TRUE,
            //]);
             PRIM_PHANTOM, TRUE]);
            llSensorRemove();
            llSensorRepeat("", _OWNER, AGENT, TARGETING_SENSOR_RANGE, PI, SENSOR_INTERVAL);
        }
        
        if ( (choice == "follow") || (choice == "attack") ) {
            seeking_avatars = TRUE;
            stop_pet();
            llSensorRemove();
            if (choice == "attack") {
    // we only attack avatars.
    //or not.  since that's boring.  watching a pet attack a physical object is fun.
                llSensor("", NULL_KEY, AGENT | ACTIVE, TARGETING_SENSOR_RANGE, PI);
                current_state = STATE_ATTACK;
            } else {
                // look for both objects and avatars to follow.
                llSensor("", NULL_KEY, AGENT | ACTIVE, TARGETING_SENSOR_RANGE, PI);
                current_state = STATE_FOLLOW;
            }
        }
        
        if (choice == "go home") {
            current_state = STATE_FREE;  // free to roam about the cabin, or wherever home is.
            llOwnerSay("i'm going home now.");
            jaunt_to_location(home_position);
        }
    
        // commands that don't lead to state changes...
    
        if (choice == "status") {
            string seek_addition;
            if (KEY_OF_TARGET != "")
                seek_addition = "was last seeking " + llKey2Name(KEY_OF_TARGET);
            llOwnerSay("my name is " + llGetObjectName() + " and state is '"
                + name_for_state(current_state) + "'.\n"
                + seek_addition);
        }
    
        if (choice == "set home") {
            list pos_list = llGetObjectDetails(llGetOwner(), [OBJECT_POS]);
            home_position = llList2Vector(pos_list, 0);
            llOwnerSay("i'm setting my home to " + (string)home_position);
    //hmmm: use a rounding print to show the position.
        }
        if (choice == "set name") {
            llOwnerSay("to change my name from " + llGetObjectName() + ",\ntell me my new name by typing:\n/" + (string)PET_CHAT_CHANNEL + " name My Cool New Name");
        }
    
    } else if (menu_name == PET_REPLY_MENU) {
log_it("menu-act follow: snd=" + (string)sender + " num=" + (string)num + " msg=" + msg + " id=" + (string)id);
        llSetPrimitiveParams([PRIM_PHYSICS, TRUE,
        //]);
            PRIM_PHANTOM, !(current_state == STATE_ATTACK)]);
        integer choice_indy = find_in_list(_FOLLOW_NAME, choice);
        if (choice_indy < 0) {
//log_it("choice was not found in list");
//log_it("followname list is: " + (string)_FOLLOW_NAME);
        } else {
            string action = "follow";
            if (current_state == STATE_ATTACK) action = "attack";
            llOwnerSay("now " + action + "ing "
                + llList2String(_FOLLOW_NAME, choice_indy) + "...");
            seeking_avatars  = FALSE;
            KEY_OF_TARGET = llList2Key(_FOLLOW_KEY, choice_indy);
            llSensorRemove();
            llSensorRepeat("", KEY_OF_TARGET, AGENT | ACTIVE,
                TARGETING_SENSOR_RANGE, PI, SENSOR_INTERVAL);  
        }
    }
}

// processes the hits that we get back from the sensor.  the information we receive
// is needed for most of the pet states.
handle_sensor(integer num_detected)
{
    if (current_state == STATE_COME) {
        go_to_target(_OWNER, llDetectedPos(0));
        motivate();
    }
    
    if ( (current_state == STATE_FOLLOW) || (current_state == STATE_ATTACK) ) {
        if (seeking_avatars) {
            // reset the list of keys and names that were found previously.
            _FOLLOW_KEY  = [];
            _FOLLOW_NAME = [];
            // show the full set found if it will fit, otherwise just 12.
            integer num_to_show = num_detected;
            if (num_to_show > 12) num_to_show = 12;
            // examine each of the avatars found and put them on the list.
            integer i;
            for (i = 0 ; i < num_to_show; i++) {
                key to_follow = llDetectedKey(i);
                if (to_follow != NULL_KEY) {
                    _FOLLOW_KEY += [to_follow];
                    string str = llDetectedName(i);
                    // trim the menu item if it has hit the maximum limit.
                    if (llStringLength(str) > 24) str = llGetSubString(str, 0, 23);
                    integer name_try = 0;
                    while (find_in_list(_FOLLOW_NAME, str) >= 0) {
                        // this guy is already listed under that name, so change it a bit.
                        str = llGetSubString(str, 0, 22) + (string)name_try++;
                    }
                    _FOLLOW_NAME += [str];
                }
            }
            // now ask who to follow.
            if (llGetListLength(_FOLLOW_KEY)) {
                show_menu(PET_REPLY_MENU, _FOLLOW_MESSAGE, _FOLLOW_NAME, _FOLLOW_CHANNEL);
            }
        } else {
            // not seeking the avatar any more; follow who was chosen.
            go_to_target(KEY_OF_TARGET, llDetectedPos(0));
            motivate();
        }
    }

    if (current_state == STATE_WANDER) {
        if (jaunt_responses_awaited) return;  // skip doing anything while we're still waiting.
        vector pos = llDetectedPos(0);
        float  omg = llFrand(1) * PI * 2;
        float  t_r = llFrand(1) * _FREE_RANGE;
        float  t_x = t_r * llCos(omg);
        float  t_y = t_r * llSin(omg);
        go_to_target(NULL_KEY, pos + <t_x, t_y, 0.0>);
        motivate();
    }

}

handle_timer() {
    if (current_state != STATE_STAY) {
        // make sure a bad jaunt didn't break our physics.
        llSetStatus(STATUS_PHYSICS, TRUE);
    }

    if (jaunt_responses_awaited) {
        // we are not quite there yet.
        if (slackness_counter++ > MAXIMUM_SLACKNESS) {
            // go back to the main state.  we took too long.
log_it("waiting for jaunt timed out.");
///argh?                jaunt_responses_awaited--;
            slackness_counter = 0;
        } else return;  // not time yet for rest of timed actions.
    }
    
    // handle the free state, since we need may to readjust the target.
    if (current_state == STATE_FREE) {
        if (pending_target) return;  // haven't arrived at previous yet.
        vector pos = home_position;
        float  omg = llFrand(1) * PI * 2;
//hmmm: make free range settable
        float  t_r = llFrand(1) * _FREE_RANGE;
        float  t_x = t_r * llCos(omg);
        float  t_y = t_r * llSin(omg);
        go_to_target(NULL_KEY, pos + <t_x, t_y, 0.0>);
        motivate();
    }
}

handle_hearing_voices(integer channel, string name, key id, string message)
{
    if (channel != PET_CHAT_CHANNEL) return;  // not our channel.
//log_it("into handle voice, msg=" + message);
    if (id != llGetOwner()) return;  // not authorized.
    // we found a command.  which specific one?
    if (is_prefix(message, "up")) {
        // upwards bump.
        enter_stay_state();
        string dist = llDeleteSubString(message, 0, 2);
        if (dist == "") dist = (string)DEFAULT_BUMP_SIZE;
        request_jaunt_up((integer)dist);
llOwnerSay("bumping up by " + dist);
    } else if (is_prefix(message, "down")) {
        // downwards bump.
        enter_stay_state();
        string dist = llDeleteSubString(message, 0, 4);
        if (dist == "") dist = (string)DEFAULT_BUMP_SIZE;
        request_jaunt_up(-(integer)dist);
llOwnerSay("bumping down by " + dist);
    } else if (is_prefix(message, "jaunt")) {
        // zip to a specific place in the sim.
        enter_stay_state();
        string where = llDeleteSubString(message, 0, 5);
        if (where == "") {
            llOwnerSay("i can't jaunt to like nowhere dude.");
            return;
        }
        vector loc = (vector)where;
        if (loc == <0.0, 0.0, 0.0>) {
            llOwnerSay("jaunt locations should be in the vector <x, y, z> format, and jaunting to <0, 0, 0> is unsupported.");
            return;
        }
llOwnerSay("jaunting to " + (string)loc);
        jaunt_to_location(loc);
    } else if (is_prefix(message, "name")) {
        // toss the command portion to get our new name.
        string new_name = llDeleteSubString(message, 0, 4);
        if (llStringLength(new_name) > 0) {
            llOwnerSay("wheeee!  my new name is: " + new_name);
            llSetObjectName(new_name);
            show_title();
        } else {
            // no data was given for the name.
            llOwnerSay("my name is still " + llGetObjectName());
        }
    } else {
        // we support a simple translation for a few orders.
        if (message == "free") message = "go home";
        
        // see if we can just flip this into a menu command instead.  we don't
        // really care whether that works or not, since anything that doesn't work is
        // a bogus command.
        llMessageLinked(LINK_THIS, _COMMAND_CHANNEL, SHOW_MENU_COMMAND,
            PET_MENU_NAME + HUFFWARE_PARM_SEPARATOR + message);
    }
}

//////////////

stop_pet()
{
//log_it("stopping pet from moving...");
    llSetPrimitiveParams([PRIM_PHYSICS, FALSE,
    //]);
    PRIM_PHANTOM, TRUE]);
}

go_to_target(key av, vector pos)
{
//log_it("told to go to target: key=" + (string)av + " pos=" + (string)pos);
    TARGET_POSITION = pos;
    if (av != NULL_KEY) {
        vector av_size = llGetAgentSize(av);

        // if it's an object, use a different method to find its height.
        if (av_size.z == 0.0) {
            // use the object's height.
            list box = llGetBoundingBox(KEY_OF_TARGET);
            float object_height = llVecDist(llList2Vector(box, 0), llList2Vector(box, 1));
            av_size.z = object_height;
        }
        // adding to get pet above target.
        TARGET_POSITION += < 0.0, 0.0, av_size.z / 2.0>;
//log_it("adjusted targposn: " + (string)TARGET_POSITION);
    }
    if (current_state == STATE_ATTACK) {
        TARGET_POSITION += < 0.0, 0.0, ATTACK_HEIGHT_ABOVE_FOLLOWED_OBJECT>;
        TARGET_POSITION += <llFrand(2) * ATTACK_X_MARGIN - ATTACK_X_MARGIN,
            llFrand(2) * ATTACK_Y_MARGIN - ATTACK_Y_MARGIN,
            llFrand(2) * ATTACK_Z_MARGIN - ATTACK_Z_MARGIN>;
    } else {
//log_it("normal target calc");
        TARGET_POSITION += < 0.0, 0.0, DEFAULT_HEIGHT_ABOVE_FOLLOWED_OBJECT>;
        TARGET_POSITION += <llFrand(2) * DEFAULT_X_MARGIN - DEFAULT_X_MARGIN,
            llFrand(2) * DEFAULT_Y_MARGIN - DEFAULT_Y_MARGIN,
            llFrand(2) * DEFAULT_Z_MARGIN - DEFAULT_Z_MARGIN>;
    }
    // trim the height a bit to keep the pet on-world.
    if (TARGET_POSITION.z > 4095.0)
        TARGET_POSITION.z = 4095.0;
}

integer jaunt_responses_awaited = 0;
    // the number of pending jumps that we are hoping will happen.

integer slackness_counter;
    // how many snoozes we've had waiting for our destination.

jaunt_to_location(vector target)
{
    // send jaunt request to get us to the specified place.
    llMessageLinked(LINK_THIS, JAUNT_HUFFWARE_ID, JAUNT_COMMAND, (string)target);
    // add one to our counter so we know a jaunt is in progress.
    jaunt_responses_awaited++;
    // reset the overflow counter to recognize a new jaunt.
    slackness_counter = 0;
}

vector previous_position;
    // how far away target was last time.

motivate()
{
    // first, let's get into the right state of existence.
    llSetStatus(STATUS_PHYSICS, TRUE);  // we need to be able to move around here.
    if (current_state == STATE_ATTACK) {
        llSetStatus(STATUS_PHANTOM, FALSE);  // we can bonk into things now.
    } else {
        llSetStatus(STATUS_PHANTOM, TRUE);  // there are no obstructive contacts.
    }

    vector current_pos = llGetPos();
    float distance = llVecDist(TARGET_POSITION, current_pos);
    // a simple linear velocity calculation based on the object's distance.
    float velocity;
    if (current_state == STATE_ATTACK) {
        velocity = ATTACK_BASE_VELOCITY + VELOCITY_MULTIPLIER * (distance / 10.0);
        // beef the velocity up for attack mode.
        velocity *= 10.0;
    } else {
        velocity = DEFAULT_BASE_VELOCITY + VELOCITY_MULTIPLIER * (distance / 10.0);
    }

//hmmm: make that 20 a constant
    integer jump_regardless = FALSE;
    if (llVecDist(current_pos, previous_position) >= 20) {
        // we will always re-target when the distances have changed that much; this could mean
        // the avatar is falling away.
        jump_regardless = TRUE;
    }
float IN_RANGE_CHANCE_TO_BAIL = 0.9;
float ATTACK_IN_RANGE_CHANCE_TO_BAIL = 0.5;
    
float NEAR_RANGE_CHANCE_TO_BAIL = 0.5;
float ATTACK_NEAR_RANGE_CHANCE_TO_BAIL = 0.1;

    if (distance <= MAXIMUM_TARGETING_DISTANCE) {
        // damp out the equation if the target is close enough.
        if (current_state == STATE_ATTACK) velocity = ATTACK_BASE_VELOCITY;
        else velocity = DEFAULT_BASE_VELOCITY;
        float within_range_chance = IN_RANGE_CHANCE_TO_BAIL;
        if (current_state == STATE_ATTACK) within_range_chance = ATTACK_IN_RANGE_CHANCE_TO_BAIL;
        if (llFrand(1.0) <= within_range_chance) return;  // do nothing; close enough.
    } else if (distance <= 2.0 * MAXIMUM_TARGETING_DISTANCE) {
        // we have a bit larger chance of setting a new target if the
        // distance is pretty close still.
        float near_range_chance = NEAR_RANGE_CHANCE_TO_BAIL;
        if (current_state == STATE_ATTACK) near_range_chance = ATTACK_NEAR_RANGE_CHANCE_TO_BAIL;
        if (llFrand(1.0) <= near_range_chance) return;
    }
    previous_position = current_pos;
//log_it("dist=" + (string)distance + " vel=" + (string)velocity);
    float  time = distance / velocity;
    _TARGET_ID = llTarget(TARGET_POSITION, MAXIMUM_TARGETING_DISTANCE);
    pending_target = TRUE;
    // make sure we're in a physics mode before attempting physics changes...
    llSetStatus(STATUS_PHYSICS, TRUE);
    if (SITTING_AVATAR_KEY == NULL_KEY) {
        // when we have nobody riding, we can look wherever we want.
        llLookAt(TARGET_POSITION, 0.7, 0.5);
    } else {
        // if we're holding onto an avatar, we keep them pointed in a reasonable way.
        vector curr_pos = llGetPos();
        vector new_lookat = <curr_pos.x, curr_pos.y, curr_pos.z + 1>;
        llLookAt(new_lookat, 0.7, 0.5);
    }
//log_it("setting move to target: " + (string)TARGET_POSITION);
    llMoveToTarget(TARGET_POSITION, time);

    integer panic_dist = DEFAULT_PANIC_DISTANCE;
    if (current_state == STATE_ATTACK) {
        panic_dist = ATTACK_PANIC_DISTANCE;
    }

    // don't try to jump if we're still awaiting a jump response.    
    if (!jaunt_responses_awaited && (distance > panic_dist) ) {
        // we need to shorten the distance to our buddy now.
        jaunt_to_location(TARGET_POSITION);
    } else if (jump_regardless || (distance > TARGETING_SENSOR_RANGE - 10)) {
        // we are double our panic point, so jump even if still waiting for a reply.
        // however, we don't want to queue up too many jaunts at a time either.
        if (jaunt_responses_awaited <= 2) {
            jaunt_to_location(TARGET_POSITION);
        }
    }

    // push the attack target if we're close enough.
    if ( (current_state == STATE_ATTACK) && (distance < ATTACK_PUSH_DIST_THRESHOLD) ) {
        // only decide to push if they win the lottery here.
        if (llFrand(1.0) < ATTACK_PUSH_CHANCE) {
            llPushObject(KEY_OF_TARGET, ATTACK_PUSH_MAGNITUDE * llRot2Up(llGetRot()), ZERO_VECTOR, FALSE);
        }
    }
    
}

show_title()
{
    llSetText(llGetObjectName(), <0.6, 0.3, 0.8>, 1.0);
}

// processes a link message from some other script.
handle_link_message(integer which, integer num, string msg, key id)
{
//log_it("got msg=" + msg + " id=" + (string)id);
    if (num == JAUNT_HUFFWARE_ID + REPLY_DISTANCE) {
//log_it("link jaunt reply");
        if (msg == JAUNT_COMMAND) {
            jaunt_responses_awaited--;  // one less response being awaited.
            if (jaunt_responses_awaited < 0) {
                log_it("erroneously went below zero for jaunt responses!");
                jaunt_responses_awaited = 0;
            }
            // unpack the reply.
            list parms = llParseString2List(id, [HUFFWARE_PARM_SEPARATOR], []);
            integer last_jaunt_was_success = (integer)llList2String(parms, 0);
            vector posn = (vector)llList2String(parms, 1);
//log_it("got a reply for a jaunt request, success=" + (string)last_jaunt_was_success + " posn=" + (string)posn);
        }
        return;
    }
    if (num != MENUTINI_HUFFWARE_ID + REPLY_DISTANCE) return;  // not for us.
//log_it("menu reply");
    react_to_menu(which, num, msg, id);
}

// returns TRUE if the "prefix" string is the first part of "compare_with".
integer is_prefix(string compare_with, string prefix)
{ return (llSubStringIndex(compare_with, prefix) == 0); }

//////////////
// huffware script: auto-retire, by fred huffhines, version 2.5.
// distributed under BSD-like license.
//   !!  keep in mind that this code must be *copied* into another
//   !!  script that you wish to add auto-retirement capability to.
// when a script has auto_retire in it, it can be dropped into an
// object and the most recent version of the script will destroy
// all older versions.
//
// the version numbers are embedded into the script names themselves.
// the notation for versions uses a letter 'v', followed by two numbers
// in the form "major.minor".
// major and minor versions are implicitly considered as a floating point
// number that increases with each newer version of the script.  thus,
// "hazmap v0.1" might be the first script in the "hazmap" script continuum,
// and "hazmap v3.2" is a more recent version.
//
// example usage of the auto-retirement script:
//     default {
//         state_entry() {
//            auto_retire();  // make sure newest addition is only version of script.
//        }
//     }
// this script is partly based on the self-upgrading scripts from markov brodsky
// and jippen faddoul.
//////////////
auto_retire() {
    string self = llGetScriptName();  // the name of this script.
    list split = compute_basename_and_version(self);
    if (llGetListLength(split) != 2) return;  // nothing to do for this script.
    string basename = llList2String(split, 0);  // script name with no version attached.
    string version_string = llList2String(split, 1);  // the version found.
    integer posn;
    // find any scripts that match the basename.  they are variants of this script.
    for (posn = llGetInventoryNumber(INVENTORY_SCRIPT) - 1; posn >= 0; posn--) {
//log_it("invpo=" + (string)posn);
        string curr_script = llGetInventoryName(INVENTORY_SCRIPT, posn);
        if ( (curr_script != self) && (llSubStringIndex(curr_script, basename) == 0) ) {
            // found a basic match at least.
            list inv_split = compute_basename_and_version(curr_script);
            if (llGetListLength(inv_split) == 2) {
                // see if this script is more ancient.
                string inv_version_string = llList2String(inv_split, 1);  // the version found.
                // must make sure that the retiring script is completely the identical basename;
                // just matching in the front doesn't make it a relative.
                if ( (llList2String(inv_split, 0) == basename)
                    && ((float)inv_version_string < (float)version_string) ) {
                    // remove script with same name from inventory that has inferior version.
                    llRemoveInventory(curr_script);
                }
            }
        }
    }
}
//
// separates the base script name and version number.  used by auto_retire.
list compute_basename_and_version(string to_chop_up)
{
    // minimum script name is 2 characters plus a version.
    integer space_v_posn;
    // find the last useful space and 'v' combo.
    for (space_v_posn = llStringLength(to_chop_up) - 3;
        (space_v_posn >= 2) && (llGetSubString(to_chop_up, space_v_posn, space_v_posn + 1) != " v");
        space_v_posn--) {
        // look for space and v but do nothing else.
//log_it("pos=" + (string)space_v_posn);
    }
    if (space_v_posn < 2) return [];  // no space found.
//log_it("space v@" + (string)space_v_posn);
    // now we zoom through the stuff after our beloved v character and find any evil
    // space characters, which are most likely from SL having found a duplicate item
    // name and not so helpfully renamed it for us.
    integer indy;
    for (indy = llStringLength(to_chop_up) - 1; indy > space_v_posn; indy--) {
//log_it("indy=" + (string)space_v_posn);
        if (llGetSubString(to_chop_up, indy, indy) == " ") {
            // found one; zap it.  since we're going backwards we don't need to
            // adjust the loop at all.
            to_chop_up = llDeleteSubString(to_chop_up, indy, indy);
//log_it("saw case of previously redundant item, aieee.  flattened: " + to_chop_up);
        }
    }
    string full_suffix = llGetSubString(to_chop_up, space_v_posn, -1);
    // ditch the space character for our numerical check.
    string chop_suffix = llGetSubString(full_suffix, 1, llStringLength(full_suffix) - 1);
    // strip out a 'v' if there is one.
    if (llGetSubString(chop_suffix, 0, 0) == "v")
        chop_suffix = llGetSubString(chop_suffix, 1, llStringLength(chop_suffix) - 1);
    // if valid floating point number and greater than zero, that works for our version.
    string basename = to_chop_up;  // script name with no version attached.
    if ((float)chop_suffix > 0.0) {
        // this is a big success right here.
        basename = llGetSubString(to_chop_up, 0, -llStringLength(full_suffix) - 1);
        return [ basename, chop_suffix ];
    }
    // seems like we found nothing useful.
    return [];
}
//
//////////////

initialize()
{
    show_title();
    llSetPrimitiveParams([PRIM_PHYSICS, FALSE,
    //]);
    PRIM_PHANTOM, TRUE]);
    llSitTarget(SIT_POSITION, llEuler2Rot(SIT_ROTATION * DEG_TO_RAD));
    llSetSitText(SIT_TEXT);
    llSetBuoyancy(1.0);
    _OWNER = llGetOwner();
    _FOLLOW_KEY = [];
    _FOLLOW_NAME = [];
    current_state = STATE_FREE;
    TARGET_POSITION = llGetPos();
    llSetTimerEvent(PERIODIC_INTERVAL);
    slackness_counter = 0;
    _COMMAND_CHANNEL = random_channel();
    _FOLLOW_CHANNEL = random_channel();
    _COMMAND_CHANNEL = random_channel();
    llListen(PET_CHAT_CHANNEL, "", llGetOwner(), "");
    home_position = llGetPos();  // start in a known place.
}

default
{
    state_entry() { if (llSubStringIndex(llGetObjectName(),  "huffotronic") < 0) state real_default; }
    on_rez(integer parm) { state rerun; }
}
state rerun { state_entry() { state default; } }

state real_default
{
    state_entry() {
        auto_retire();
        initialize();
    }
    
    on_rez(integer param) { llResetScript(); }
    
    touch_start(integer num_detected) {
        show_title();
//change title to show menuing state?
        if (_OWNER == llDetectedKey(0)) {
            // show our menu here.
            show_menu(PET_MENU_NAME, _COMMAND_MESSAGE, SUPPORTED_COMMANDS, _COMMAND_CHANNEL);
        }
    }

    link_message(integer sender, integer num, string msg, key id) {
        handle_link_message(sender, num, msg, id);
    }
    
    sensor(integer num_detected) {
//log_it("sensor found " + llDetectedName(0));
        handle_sensor(num_detected);
    }
    
    no_sensor() {
//use another means to find the avatar?
    }
    
    at_target(integer number, vector targetpos, vector ourpos) {
//log_it("at target");
        llTargetRemove(_TARGET_ID);
        pending_target = FALSE;
        llStopMoveToTarget();
    }
    
    not_at_target() {
//log_it("not at target");

    }
    
    changed(integer change) {
        if (change & CHANGED_LINK) {
            key av = llAvatarOnSitTarget();
            if (SITTING_AVATAR_KEY != NULL_KEY) {
                if (av == NULL_KEY) {
                    llStopAnimation(SIT_ANIMATION);
                    SITTING_AVATAR_KEY = NULL_KEY;
                }
            } else {
                if (av != NULL_KEY) {
                    SITTING_AVATAR_KEY = av;
                    llRequestPermissions(SITTING_AVATAR_KEY, PERMISSION_TRIGGER_ANIMATION);
// we wish we could make the avatar a phantom here, but that's not allowed.
                }
            }
        }
    }

    run_time_permissions(integer perm) {
        key perm_key = llGetPermissionsKey();
        if (perm_key == SITTING_AVATAR_KEY) {
            if (perm & PERMISSION_TRIGGER_ANIMATION) {
                list anms = llGetAnimationList(SITTING_AVATAR_KEY);
                integer i;
                for (i = 0 ; i < llGetListLength(anms) ; i++) {
                    llStopAnimation(llList2Key(anms, i));
                }
                llStartAnimation(SIT_ANIMATION);
            }
        }
    }

    timer() {
        handle_timer();
    }

    listen(integer channel, string name, key id, string message) {
        handle_hearing_voices(channel, name, id, message);
    }

}

// attributions:
//   this script is based (a lot!) on the "pet" script that might
// have been written by kazumasa loon.  there was no attribution
// of author in the script, but the creator was kazumasa.  thanks dude!
//
// that being said, the script was redone a lot by fred huffhines,
// mainly in the following areas:
//
// march or april 2008: added teleport capability to script.  pet will now attempt
//   to keep up with the owner during follow mode by teleporting to her.
//
// may 2008: added ipc menu system.  now menus are dealt with by the huffware
//    menu system, removing a lot of code from this script.

