A life twice-lived ...

FRANKIEMOD

Lightly customized Preview 6 NPC critter/goon to test some auto-respawn/auto-start mods.

Changes from Preview 6/v0.0.1 are tagged FRANKIEMOD for ease of searching.

//============================================================================
// Myriad_Lite_Critter_Goon_NPC-v0.0.2-20141209.lsl
// Copyright (c) 2012-2014 By Allen Kerensky (OSG/SL)
// The Myriad RPG System was designed, written, and illustrated by Ashok Desai
// Myriad RPG licensed under the Creative Commons Attribution 2.0 UK: England and Wales
// http://creativecommons.org/licenses/by/2.0/uk/
// Myriad Lite software Copyright (c) 2011-2014 by Allen Kerensky (OSG/SL)
// Baroun's Adventure Machine Copyright (c) 2008-2011 by Baroun Tardis (SL)
// Myriad Lite and Baroun's Adventure Machine licensed under the
// Creative Commons Attribution-Share Alike-Non-Commercial 3.0 Unported
// http://creativecommons.org/licenses/by-nc-sa/3.0/
// You must agree to the terms of this license before making any use of this software.
// If you do not agree to this license, simply delete these materials.
// There is no warranty, express or implied, for your use of these materials.
//============================================================================

//============================================================================
// MESSAGE FORMAT REFERENCE
//============================================================================
// LINK_THIS,MODULE_GOON,"ARMOREFFECTHIT",NULL_KEY
// LINK_THIS,MODULE_GOON,"ARMOREFFECTBLOCKED",NULL_KEY
// LINK_THIS,MODULE_GOON,"WOUNDED",NULL_KEY
// LINK_THIS,MODULE_GOON,"DEAD",NULL_KEY

// CHANPLAYER IN - DEPRECATED - HITCHECK|int attackstat|int attackskill|int attackdice|key owner|str name
// CHANPLAYER IN - RANGEDHIT|int attackstat|int attackskill|int attackdice|key weaponowner|str name
// CHANPLAYER IN - CLOSEHIT|int attackstat|int attackskill|int attackdice|key weaponowner|str name

//============================================================================
// CRITTER/GOON CUSTOMIZATION
//============================================================================
// By far the simplest kind of NPC is the Goon, sometimes called the Critter when its an animal such as a bear or a jackal.
// The goon is bred for one thing only: conflict
// The only ever roll a single die during any test and have only two skills, attack and defense.
// The former is added to attack rolls and the latter to defense, surprisingly enough.

// MORTAL COMBAT STATS
integer POWER = 1; // attack stat - 1 dice rolled on attack
integer SKILL_ATTACK = 1; // attack skill

//list RANGED_ATTACKS = []; // list of ranged attacks
//list MSG_RANGED_HIT = []; // list of attack hits messages
//list MSG_RANGED_MISS = []; // list of attack miss messages

//list CLOSE_MELEE_ATTACKS = []; // list of attack types - claw, bite, sting, etc
//list MSG_MELEE_HIT = []; // list of attack hits messages
//list MSG_MELEE_MISS = []; // list of attack miss messages
float MELEE_WEAPON_LENGTH = 2.0; // a six-foot longsword is the default

//list CLOSE_UNARMED_ATTACKS = []; // list of attack types - claw, bite, sting, etc
//list MSG_UNARMED_HIT = []; // list of attack hits messages
//list MSG_UNARMED_MISS = []; // list of attack miss messages
float UNARMED_WEAPON_LENGTH = 1.0; // thrown punch, use 1.5 for kicks

integer RANGED_DAMAGE_DICE = 1; // thrown rock
integer MELEE_DAMAGE_DICE = 4; // long sword
integer UNARMED_DAMAGE_DICE = 1; // fists

integer GRACE = 1; // defense stat FIXME implement defense
integer SKILL_DEFENSE = 1; // defense skill FIXME implement defense

integer ARMOR; // armor worn FIXME implement armor check

// Since they are specialized for combat, they also have a number of resilience boxes based on how tough they are, although like specialists they don't have critical boxes and are out of the fight when all their wounds are gone.
integer RESILIENCE; // 1-20 how many hits/damage this goon can take

list TREASURE; // list of treasure items dropped when killed or searched while incapacitated

//integer FLAG_AGGRESSIVE; // if TRUE = monster does not flee?
//integer FLAG_FOLLOWER; // if TRUE = follows after nearest av?
//integer FLAG_GUARD; // if TRUE = guards some position
//integer FLAG_WANDER; // if TRUE = wanders in an area
integer FLAG_FACEENEMY; // if true, use llLookAt to face enemy before attacking
integer FLAG_TARGETPOLICY; // ranged combat
integer TARGET_NEAREST = 0;
integer TARGET_RANDOM = 1;
integer FLAG_TARGETINCAP; // FIXME target and shoot visibly incapacitated targets?

// Although goons are primarily used in mortal combat there is no reason why they could be used in other forms of conflict too, but most goons are only useful in one specific kind of conflict.

//============================================================================
// GLOBAL VARIABLES
//============================================================================
integer MODULE_GOON=-10; // for link messages

integer MINSTAT = 1; // minimum value of a statistic
integer MAXSTAT = 10; // maximum value of a statistic
integer MINSKILL = 1; // minimum value of a skill
integer MAXSKILL = 5; // maximum value of a skill
integer MINDAMAGE = 1; // minimum damage dice a weapon can inflict
integer MAXDAMAGE = 5; // maximum damage dice a weapon can inflict

float HEARTRATE = 0.5;
float SENSOR_RANGE = 96.0;
float SENSOR_ARC = PI;
float SENSOR_RATE = 5.0;

float BULLET_VELOCITY = 30.0;                       // change this to change the speed of the bullet.
string GUNSOUND = "pistol_shot.wav";                            // string; name of sound in inventory
string AMMO =  "Myriad Lite Bullet Turret v0.0.0 20120511"; //name of desired object to be shot out. Must be in the inventory of the "gun".
vector REZ_OFFSET = <0,0, 2.1>;                // rez offset for bullet
string DIV = "|";
integer CHANMYRIAD = -999; // Myriad Region Channel
string ANIM_DEAD = ""; // name of animation file in NPC inventory if used on osNPC - does nothing on prim NPCs
float RESPAWN_TIME = 15.0; // 10 seconds to respawn

//============================================================================
// RUNTIME
//============================================================================
integer FLAG_DEBUG;
integer HANDLE_PUB;
integer CHANOBJECT; // channel the target listens on for attacks
integer HANDOBJECT; // chat channel handle to remove channel later if needed

vector POS;
rotation ROT;
vector OFFSET;
integer ANTIDELAY; // use antidelay nodes for rapid fire?
integer ISACTIVE;
integer FLAG_DIE; // does NPC delete on death?

list DETECTED_KEY = []; // list of keys found in last sensor sweep
list DETECTED_POS = []; // list of positions found for all keys in last sensor sweep
list DETECTED_ROT = []; // list of rotations found for all keys in last sensor sweep

integer FLAG_PLAYANIM; // play an osNPC animation
integer FLAG_DEAD; // dead waiting for respawn?
integer FLAG_RESPAWN;
integer FLAG_ATTACK_ON_SIT; // attack someone trying to sit on critter?

// MOVE WANDER settings
float RANGE = 60.0;
float MOVE_SPEED = 1.0;
float ROT_SPEED = 3.0;
float STRENGTH = .2;
float DELAY = 2.5;
float ZOFFSET = 0.0;
integer MODE_OFF = 0;
integer MODE_WANDER = 1;
integer MODE_REMOTE = 2;
integer MODE_FOLLOW = 3;
integer MODE_PATROL = 4;
integer MODE_GUARD = 5;
integer RESTRICT_Z; // restrict MODE_WANDER target point within a range of Z altitudes
integer MINZ = 0;
integer MAXZ = 30;

integer MODE;
vector REZ_POINT;
vector NEXT_POINT;
integer FOUND;
string REZ_PARCEL;
integer KEEP_IN_PARCEL; // if TRUE, new picked MODE WANDER points must be on same parcel as rezpoint
integer COUNT;
float FINDANGLE;
float X; // x component of a position
float Y; // y component of a position
float DIST; // distance
vector PREVPOS; // previous position
vector MIN;

//============================================================================
// ABILITY TEST
// Requires ATTRIBUTE NAME, SKILL NAME
// Returns the ability test score for use by success fail, opposed rolls, etc
// See Myriad PDF page 18, Myriad Special Edition page 24
//============================================================================
integer ABILITY_TEST(integer attribute,integer skill) {
    integer highroll = 0; // clear out the highest roll
    while( attribute-- ) { // roll a dice for each point of the attribute
        integer roll = 1+(integer)llFrand(5.0); // roll this d6
        if ( roll > highroll) highroll = roll; // if this is highest roll so far, remember it
    } // finished rolling a dice for each point of the base attribute
    return highroll + skill; // now, return the total of highest dice roll + skill value
}

//============================================================================
// An Opposed Ability Test - Myriad PDF p. 19 Myriad Special Edition p. 25
// Requires Attacker Attribute Name, Attacker Skill Name, Defender Attribute Name, Defender Skill Name
// Returns TRUE for Success, FALSE for failure
//============================================================================
integer ABILITY_TEST_OPPOSED(integer aattrib,integer askill,integer dattrib,integer dskill) {
    integer acheck = ABILITY_TEST(aattrib,askill); // calculate attacker's ability test
    integer dcheck = ABILITY_TEST(dattrib,dskill); // calculate defender's ability test
    if ( acheck > dcheck ) return TRUE; // attacker more than defender = attacker wins
    return FALSE; // defender wins
}

//============================================================================
// MORTAL COMBAT - MAKE RANGED ATTACK
//============================================================================
ATTACK_RANGED() {
    if ( llGetListLength(DETECTED_KEY) == 0 ) return; // no targets, so return
    
    key target;
    vector target_pos;
    rotation target_rot; 
    
    if ( FLAG_TARGETPOLICY == TARGET_NEAREST ) { // target nearest
        target = llList2Key(DETECTED_KEY,0);
        target_pos = llList2Vector(DETECTED_POS,0);
        target_rot = llList2Rot(DETECTED_ROT,0);
    }
    if ( FLAG_TARGETPOLICY == TARGET_RANDOM ) {
        integer dieroll = 1+(integer)llFrand((float)llGetListLength(DETECTED_KEY)); // reasonably uniform distribution die roll
        target = llList2Key(DETECTED_KEY,dieroll - 1);
        target_pos = llList2Vector(DETECTED_POS,dieroll - 1);
        target_rot = llList2Rot(DETECTED_ROT,dieroll - 1);        
    }
    
    if ( FLAG_FACEENEMY == TRUE ) llLookAt(target_pos,1,1);

    float target_dist = llVecDist(llGetPos(),target_pos);
    if(llVecDist(llGetPos(),target_pos+llRot2Fwd(target_rot)*target_dist) < 1.5) {
        // Fire 1 bullet,,  the heart of the firearm script.
        POS = llGetPos(); // get our current position
        ROT = llGetRot(); // get our current rotation
        OFFSET = REZ_OFFSET; // start with the base offset for the gun held in the right hand
        OFFSET *= ROT; // now, rotate the offset to match the avatar rotation
        POS += OFFSET; // now combine the rotated offset with avatar position 
        vector fwd = llRot2Up(ROT); // calculate the direction that is "avatar's facing" 
        fwd *= BULLET_VELOCITY; // now multiply that by bullet speed to tell bullet to push in that direction, that fast
        //rot *= llEuler2Rot(<0, PI_BY_TWO, 0>); // now, straighten rotation for object we're about to rez
        llPlaySound(GUNSOUND,1.0); // here "GUNSOUND"is a variable defined above.
        // DAMAGEDICE is passed to rez-param of bullet. Myriad Bullets read this as damage dice to do if they hit
        if ( ANTIDELAY == FALSE ) {
            llRezObject(AMMO, POS, fwd, ROT, RANGED_DAMAGE_DICE); // does the actual work rezzes the ammo in the specified variables.
        } else {
            llMessageLinked(LINK_THIS,-123,AMMO+"~~~"+(string)POS+"~~~"+(string)fwd+"~~~"+(string)ROT+"~~~"+(string)RANGED_DAMAGE_DICE,"rezobject");
        }
        //llSleep(RATE); // force a pause between shots            
    }
}

//============================================================================
// MORTAL COMBAT - MAKE CLOSE MELEE ATTACK
//============================================================================
ATTACK_CLOSE_MELEE() {
    if ( llGetListLength(DETECTED_KEY) == 0 ) return; // no targets, so return
    // target nearest
    key target = llList2Key(DETECTED_KEY,0);
    vector target_pos = llList2Vector(DETECTED_POS,0);
    //rotation target_rot = llList2Rot(DETECTED_ROT,0);

    if ( FLAG_FACEENEMY == TRUE ) llLookAt(target_pos,1,1);
        
    vector A = ( llGetPos() + < MELEE_WEAPON_LENGTH,0,0> * llGetRot());
    // Is defender center within 1m of my calculated point in front of me?
    // If so, my sword had a chance to hit when I swung it.
    if ( llVecDist(A,target_pos) < 1.0 ) {         
        integer dynchan = (integer)("0x"+llGetSubString((string)llGetOwner(),0,6)); // calculate attackers HUD dynamic channel
        // send the close combat skill check message to attacker to start close combat skill check
        // attackers hud adds attacker stat and skill and sends the information to the victim to finish the opposed close combat skill check
        // we region say this so others can keep score or detect cheaters, etc
        llRegionSay(dynchan,"CLOSECOMBAT"+DIV+(string)MELEE_DAMAGE_DICE+DIV+(string)target+DIV+(string)llGetOwner()+DIV+llGetObjectName());
        key owner = llList2Key(llGetObjectDetails(target,[OBJECT_OWNER]),0); // is this an agent/avatar for sure?
        if ( target == owner ) { // yep, we hit an avatar
            // tell the region an attempted attack is underway
            RPEVENT(llKey2Name(llGetOwner())+" strikes at "+llList2String(llGetObjectDetails(target,[OBJECT_NAME]),0)+" in Close Melee Combat!");
        }
    }
}

//============================================================================
// MORTAL COMBAT - MAKE CLOSE UNARMED ATTACK
//============================================================================
ATTACK_CLOSE_UNARMED() {
    if ( llGetListLength(DETECTED_KEY) == 0 ) return; // no targets, so return
    // target nearest
    key target = llList2Key(DETECTED_KEY,0);
    vector target_pos = llList2Vector(DETECTED_POS,0);
    //rotation target_rot = llList2Rot(DETECTED_ROT,0);

    if ( FLAG_FACEENEMY == TRUE ) llLookAt(target_pos,1,1);

    vector A = ( llGetPos() + < UNARMED_WEAPON_LENGTH,0,0> * llGetRot());
    // Is defender center within 1m of my calculated point in front of me?
    // If so, my sword had a chance to hit when I swung it.
    if ( llVecDist(A,target_pos) < 1.0 ) {         
        integer dynchan = (integer)("0x"+llGetSubString((string)llGetOwner(),0,6)); // calculate attackers HUD dynamic channel
        // send the close combat skill check message to attacker to start close combat skill check
        // attackers hud adds attacker stat and skill and sends the information to the victim to finish the opposed close combat skill check
        // we region say this so others can keep score or detect cheaters, etc
        llRegionSay(dynchan,"CLOSECOMBAT"+DIV+(string)UNARMED_DAMAGE_DICE+DIV+(string)target+DIV+(string)llGetOwner()+DIV+llGetObjectName());            
        key owner = llList2Key(llGetObjectDetails(target,[OBJECT_OWNER]),0); // is this an agent/avatar for sure?
        if ( target == owner ) { // yep, we hit an avatar
            // tell the region an attempted attack is underway
            RPEVENT(llKey2Name(llGetOwner())+" strikes at "+llList2String(llGetObjectDetails(target,[OBJECT_NAME]),0)+" in Close Unarmed Combat!");
        }
    }
}

//============================================================================
// COMMAND - process CHAT or LINK MESSAGE commands
//============================================================================
COMMAND(string cmd) {
    list tokens = llParseString2List(cmd,["|"],[]);
    string command = llToLower(llStringTrim(llList2String(tokens,0),STRING_TRIM));
    if ( command == "attack_ranged" ) { ATTACK_RANGED(); return; }
    if ( command == "attack_close_melee" ) { ATTACK_CLOSE_MELEE(); return; }
    if ( command == "attack_close_unarmed" ) { ATTACK_CLOSE_UNARMED(); return; }
    if ( command == "defend_ranged" ) { DEFEND_RANGED(); return; }
    if ( command == "defend_close_melee" ) { DEFEND_CLOSE_MELEE(); return; }
    if ( command == "defend_close_unarmed" ) { DEFEND_CLOSE_UNARMED(); return; }
    
    if ( command == "move_off" ) { MODE = MODE_OFF; return; }
    if ( command == "move_wander" ) { MODE = MODE_WANDER; return; }
    if ( command == "move_remote" ) { MODE = MODE_REMOTE; return; }
    if ( command == "move_follow" ) { MODE = MODE_FOLLOW; return; }
    if ( command == "move_patrol" ) { MODE = MODE_PATROL; return; }
    if ( command == "move_guard"  ) { MODE = MODE_GUARD; return; }
}

//============================================================================
// DAMAGE_CHECK
//============================================================================
DAMAGE_CHECK(integer attackstat,integer attackskill, integer attackdice,key owner,string item) {
    // see if we're hit
    integer amihit = ABILITY_TEST_OPPOSED(attackstat,attackskill,GRACE,SKILL_DEFENSE); // attacker power+skill vs. defender grace+skill
    if ( amihit == TRUE ) { // we're hit!
        RPEVENT(llGetObjectName()+" attacked by "+llKey2Name(owner)+"'s "+item+" in mortal combat!");
        DAMAGE_HIT(attackdice); // apply the hit
    }
    return;
}

//============================================================================
// DAMAGE_HIT - player is hit - check to see if attack dice breach armor
// Making A Damage Roll (Myriad p25, Myriad Special Edition p31)
//============================================================================
DAMAGE_HIT(integer attackdice) {
    integer damagetaken = 0; // start with zero damage
    while(attackdice--) { // roll for each attack dice
        integer dieroll = 1+(integer)llFrand(5.0); // reasonably uniform d6
        if ( dieroll > ARMOR ) { // attack roll stronger than armor worn?
            damagetaken++; // add a wound point
        }
    }
    // finished roll how did we do?
    if ( damagetaken > 0 ) { // we took damage
        if ( ARMOR > 0 ) { // wearing armor? tell them it was breached
            RPEVENT(llGetObjectName()+"'s armor penetrated and wounded by that attack.");
            llMessageLinked(LINK_THIS,MODULE_GOON,"ARMOREFFECTHIT",NULL_KEY);
        } else { // fighting in no armor?
            RPEVENT(llGetObjectName()+" wounded!");
        }
        DAMAGE_WOUNDED(damagetaken); // apply damage taken to resilences
    } else { // hit, but no damage taken
        // must be wearing *some* armor to be hit but avoid a wound, don't recheck for armor here
        RPEVENT(llGetObjectName()+"'s armor blocked damage from that attack!");
        llMessageLinked(LINK_THIS,MODULE_GOON,"ARMOREFFECTBLOCKED",NULL_KEY);
    }
}

//============================================================================
// DAMAGE_WOUNDED - Player takes Resilience damage
//============================================================================
DAMAGE_WOUNDED(integer amount) {
    llMessageLinked(LINK_THIS,MODULE_GOON,"WOUNDED",NULL_KEY);
    while (amount--) { // for each wound taken
        if ( RESILIENCE > 1 ) { // wound boxes left?
            RESILIENCE--; // scratch off one
        } else if ( RESILIENCE <= 1 ) { // Goon about to be out of wounds?
            RESILIENCE = 0; // force zero
            DAMAGE_ZZZ(); // show death
        }
    } // end while
}

//============================================================================
// DAMAGE_ZZZ - player is dead, kill them and wait to respawn
//============================================================================
DAMAGE_ZZZ() {
    FLAG_DEAD = TRUE; // remember that we're now dead
    if ( FLAG_PLAYANIM == TRUE ) llStartAnimation(ANIM_DEAD); // start dead animation
    RPEVENT(llGetObjectName()+" has been killed!");
    llMessageLinked(LINK_THIS,MODULE_GOON,"DEAD",NULL_KEY);
    
    // monster dead, if it carried treasure drop one
    if ( llGetListLength(TREASURE) > 0 ) {
        integer dieroll = 1+(integer)llFrand((float)llGetListLength(TREASURE));
        llRezObject(llGetInventoryName(INVENTORY_OBJECT,dieroll),llGetPos(),ZERO_VECTOR,llGetRot(),0);
    }
    
    // Does critter/goon respawn on death?
    if ( FLAG_RESPAWN == TRUE) {
        llSleep(RESPAWN_TIME); // respawn in a bit
        RESET();
    }

    // Does critter/goon die and disappear on death?
    if ( FLAG_DIE == FALSE ) return;
    llSleep(RESPAWN_TIME); // let dead critter/goon lay there dead for a bit... then
    // FRANKIEMOD THIS GOON DIES AND YELLS MESSAGE TO CREATE NEW REPLACEMENT GOON
    // FRANKIEMOD llRegionSay(channel,"commandtospawner");
    // go into infinite loop trying to die
    while ( TRUE == TRUE ) {
        llDie();
    }
}

//============================================================================
// DEBUG on debug channel
//============================================================================
DEBUG(string debugmsg) {
    if ( FLAG_DEBUG == TRUE ) llSay(DEBUG_CHANNEL,"DEBUG: "+debugmsg);
}

//============================================================================
// MORTAL COMBAT - DEFEND AGAINST RANGED ATTACK
//============================================================================
DEFEND_RANGED() {
}

//============================================================================
// MORTAL COMBAT - DEFEND AGAINST CLOSE MELEE ATTACK
//============================================================================
DEFEND_CLOSE_MELEE() {
}

//============================================================================
// MORTAL COMBAT - DEFEND AGAINST CLOSE UNARMED ATTACK
//============================================================================
DEFEND_CLOSE_UNARMED() {
}


//============================================================================
// ERROR() - report errors on debug channel
//============================================================================
ERROR(string errmsg) {
    llSay(DEBUG_CHANNEL,"ERROR: "+errmsg);
}

//============================================================================
// HEARTBEAT - the main "brain" or thinking function - called from timer
//============================================================================
HEARTBEAT() { 
        POS = llGetPos();
        ROT = llGetRot();
        if ( MODE == MODE_OFF ) {
        } else if ( MODE == MODE_WANDER ) {
            MOVE_WANDER();
        } else if ( MODE == MODE_REMOTE ) {
            MOVE_REMOTE();
        } else if ( MODE == MODE_FOLLOW ) {
            MOVE_FOLLOW();
        } else if ( MODE == MODE_PATROL ) {
            MOVE_PATROL();
        } else if ( MODE == MODE_GUARD ) {
            MOVE_GUARD();
        }
}

//============================================================================
// MOVE - move to a given point
//============================================================================
MOVE(vector dest) {
    llOwnerSay("MOVE FROM "+(string)llGetPos()+" TO "+(string)dest);
    DIST = llVecDist(dest,llGetPos());
    
    while(DIST > 1.0) {
        PREVPOS = llGetPos();
        dest.z = llGround(ZERO_VECTOR) + ZOFFSET;
        //if ( llScriptDanger(dest) ) return;
        if ( dest.x > 255 || dest.x < 0 || dest.y > 255 || dest.y < 0 ) return;
        
        llSetPos(dest);
        if(llGetPos() == PREVPOS) { // Yipes! The object didnt move! Occurs with float error.
            DEBUG("MOVE: Recovered from position floating point error.");
            return; // return from this function immediately.
        }
        DIST = llVecDist(dest,llGetPos());
        llSleep(MOVE_SPEED / 10.0);
    }
}

//============================================================================
// MOVEMENT - CHARGE ATTACKER
//============================================================================
//MOVE_CHARGE() {
//    if ( FLAG_AGGRESSIVE == TRUE ) {
//    }
//}

//============================================================================
// MOVEMENT - FLEE AWAY FROM ATTACKER
//============================================================================
//MOVE_FLEE() {
//    if ( FLAG_AGGRESSIVE == TRUE ) return; // do not flee when enraged/aggressive
//}

//============================================================================
// MOVE_FOLLOW - does critter/goon follow target
//============================================================================
MOVE_FOLLOW() {
    // FIXME follower code
}

//============================================================================
// MOVEMENT - STAY BETWEEN ATTACKER AND POSITION TO GUARD
//============================================================================
MOVE_GUARD() {
    // FIXME - intercept attacker - find point on circle at range on line between attack and defense point
}

//============================================================================
// MOVEMENT - PATROL A GIVEN LIST OF WAYPOINTS
//============================================================================
MOVE_PATROL() {
    // FIXME - patrol a given list of waypoints
}

//============================================================================
//============================================================================
MOVE_REMOTE() {
    // FIXME - allow region owner to remote control critter/goon movement
}

//============================================================================
//============================================================================
//MOVE_STEPDOWN() {
//}

//============================================================================
//============================================================================
//MOVE_STEPUP() {
//}

//============================================================================
//============================================================================
//MOVE_TURN() {
    // turn left or right
//}

//============================================================================
// MOVE_WANDER - wander from point to point around a center point
//============================================================================
MOVE_WANDER() {
    FOUND = FALSE; // reset the "found new point?" flag to false in prep for the next run.
    COUNT = 0; // reset the counter of number of times we've tried to find a new waypoint
    
    // pick a new waypoint constrained by a variety of rules
    while (!FOUND) {
        
        // The first constraint is a bounding box
        MIN = REZ_POINT - < RANGE, RANGE, RANGE >; // find the minimum bounds of the RANGED bounding box
        
        NEXT_POINT = MIN + < llFrand(RANGE * 2), llFrand(RANGE * 2), 0>; // pick a new random X,Y within bounding box
        
        // The next constraint is to stay on parcel or not
        if ( KEEP_IN_PARCEL == TRUE ) {
            if ( REZ_PARCEL == llList2String(llGetParcelDetails(NEXT_POINT,[PARCEL_DETAILS_AREA]),0) ) {
                FOUND = TRUE;
            }
        }
        
        // The next constraint is to stay within an altitude band at the destination, i.e. pick only points in the valley not on cliff walls.
        if ( RESTRICT_Z == TRUE ) {
            if ( ( NEXT_POINT.z >= MINZ ) && ( NEXT_POINT.z <= MAXZ ) ) {
                FOUND = TRUE;
            }
        }   
        
        // The next constraint is to check if there is a script problem with the target
        //if ( llScriptDanger(NEXT_POINT) ) FOUND = FALSE;
        
        // the next constraint is to stay within the sim
        if ( NEXT_POINT.x > 255 || NEXT_POINT.x < 0 || NEXT_POINT.y > 255 || NEXT_POINT.y < 0 ) FOUND = FALSE;
        
        // Now, check how many times we've tried to find a new waypoint. If too many, go back to rez point.
        if ( ++COUNT >= 100 ) {
            NEXT_POINT = REZ_POINT;
            FOUND = TRUE;
        }    
    }
    
    llRotLookAt(MOVE_WANDER_HEADING(llGetPos(),NEXT_POINT),STRENGTH,ROT_SPEED); // turn to new heading
    llOwnerSay("NEXT POINT = "+(string)NEXT_POINT);
    MOVE(NEXT_POINT); // move worm to next wander point
    
    // Give lookat time to complete
    llSleep(llFrand(MOVE_SPEED+DELAY)); // give RotLookat time to finish
    llStopLookAt();
}

//============================================================================
// MOVE_WANDER_HEADING - get a heading from one point to another for MOVE_WANDER
//============================================================================
rotation MOVE_WANDER_HEADING(vector pos,vector newpos) {
    X = newpos.x - pos.x;
    Y = newpos.y - pos.y;
    if ( llFabs(X) < 0.000001 && llFabs(Y) < 0.000001 ) return ZERO_ROTATION;
    FINDANGLE = llAtan2(Y,X);
    return llEuler2Rot(<0,0,FINDANGLE>);
}

//============================================================================
// RESET - allow shutdown actions before resetting scripts
//============================================================================
RESET() {
    // do any shutdown events
    // then, reset
    while ( TRUE == TRUE ) llResetScript(); // use while true to force reset
}

//============================================================================
// RPEVENT - send RPevents to region
//============================================================================
RPEVENT(string rpmsg) {
    llRegionSay(CHANMYRIAD,"RPEVENT|"+rpmsg);
}

//============================================================================
// SETUP - CONFIGURE YOUR CRITTER/GOON
//============================================================================
SETUP() {
    
    POWER = 1; // attack stat - 1 dice rolled on attack
    SKILL_ATTACK = 1; // attack skill
    MELEE_WEAPON_LENGTH = 2.0; // a six-foot longsword is the default
    UNARMED_WEAPON_LENGTH = 1.0; // thrown punch, use 1.5 for kicks

    RANGED_DAMAGE_DICE = 1; // thrown rock
    MELEE_DAMAGE_DICE = 4; // long sword
    UNARMED_DAMAGE_DICE = 1; // fists

    GRACE = 1; // defense stat FIXME implement defense
    SKILL_DEFENSE = 1; // defense skill FIXME implement defense

    ARMOR = 0; // armor worn FIXME implement armor check

    RESILIENCE = 1; // 1-20 how many hits/damage this goon can take

    TREASURE = []; // list of treasure items dropped when killed or searched while incapacitated

    FLAG_ATTACK_ON_SIT = TRUE; // should critter bite anyone who sits on it
    FLAG_FACEENEMY = TRUE; // if true, use llLookAt to face enemy before attacking
    FLAG_TARGETPOLICY = TARGET_RANDOM; // ranged combat
    FLAG_TARGETINCAP = FALSE; // FIXME target and shoot visibly incapacitated targets?

    FLAG_DEBUG = FALSE; // show debug messages
    ANTIDELAY = FALSE; // use antidelay nodes for rapid fire?
    ISACTIVE = FALSE; // turret active?
    FLAG_PLAYANIM = FALSE; // set TRUE for osNPC
    FLAG_DEAD = FALSE;
    FLAG_RESPAWN = TRUE;
    FLAG_DIE = FALSE;
    
    ARMOR = 0; // does critter/goon have armor? Range: 0 for none, 1-5 for valid armor
    SENSOR_ARC = PI; // constrain the field of fire.
    
    // Movement Setup
    llSetStatus(STATUS_PHANTOM|STATUS_PHYSICS|STATUS_ROTATE_X|STATUS_ROTATE_Y|STATUS_DIE_AT_EDGE,FALSE);
    llSetStatus(STATUS_ROTATE_Z|STATUS_BLOCK_GRAB|STATUS_RETURN_AT_EDGE,TRUE);
    REZ_POINT = llGetPos();
    REZ_POINT.z = llGround(ZERO_VECTOR) + 1.5;
    RESTRICT_Z = FALSE;
    KEEP_IN_PARCEL = FALSE;
    MODE = MODE_WANDER; // default to wander mode for a critter
    REZ_PARCEL = llList2String(llGetParcelDetails(REZ_POINT,[PARCEL_DETAILS_AREA]),0);
    MOVE(REZ_POINT);
    
    // inventory configuration
    // inventory sounds
    // inventory prizes

    // llSetPrimitiveParams([PRIM_PHANTOM, FALSE]); // ensure all prims are not phantom to register collisions
    CHANOBJECT = (integer)("0x"+llGetSubString((string)llGetKey(),0,6)); // calculate dynamic channel to listen on
    if ( HANDOBJECT != 0 ) llListenRemove(HANDOBJECT); // remove an existing listener channel
    HANDOBJECT = llListen(CHANOBJECT,"",NULL_KEY,""); // start listener for attack events
    
    if ( HANDLE_PUB != 0 ) llListenRemove(HANDLE_PUB); // remove an existing listener channel
    HANDLE_PUB = llListen(PUBLIC_CHANNEL,"",NULL_KEY,""); // start a listener on main chat    

    ISACTIVE = TRUE; // FRANKIEMOD
    llSensorRepeat("",NULL_KEY,AGENT,SENSOR_RANGE,SENSOR_ARC,SENSOR_RATE); // FRANKIEMOD
    llSetTimerEvent(HEARTRATE); // FRANKIEMOD
}

//============================================================================
//============================================================================
//STANCE_KNEEL() {
//}

//============================================================================
// PRONE STANCE
//============================================================================
//STANCE_PRONE() {
//}

//============================================================================
//============================================================================
//STANCE_STAND() {
    // Stand up code
//}


//============================================================================
// DEFAULT STATE
//============================================================================
default {
    //------------------------------------------------------------------------
    // CHANGED - reset script when region starts or restarts
    //------------------------------------------------------------------------
    changed(integer change) {
        if ( change & CHANGED_REGION_START ) {
            RESET();
        }
        if ( change & CHANGED_LINK ) {
            key id = llAvatarOnSitTarget();
            if (id) {
                if ( id != llGetOwner() && FLAG_ATTACK_ON_SIT ) {
                    llShout(PUBLIC_CHANNEL,"The "+llGetObjectName()+" attacks "+llKey2Name(id)+" for trying to sit on it!");
                    integer dynchan = (integer)("0x"+llGetSubString((string)id,0,6)); // calculate attackers HUD dynamic channel                    
                    llRegionSay(dynchan,"CLOSECOMBAT"+DIV+(string)UNARMED_DAMAGE_DICE+DIV+(string)id+DIV+(string)llGetKey()+DIV+llGetObjectName());                                    llUnSit(id);
                }
            }
        }
    }

    //------------------------------------------------------------------------
    // LINK_MESSAGE - incoming messages from other Modules in NPC
    //------------------------------------------------------------------------
    link_message(integer sender_num,integer num,string msg,key id) {
        if ( num == MODULE_GOON ) return; // ignore our own link messages
        DEBUG("link_message: sender_num=["+(string)sender_num+"] num=["+(string)num+"] msg=["+msg+"] id=["+(string)id+"]");
        COMMAND(msg);
    }
    
    //------------------------------------------------------------------------
    // LISTEN - receive owner commands and INCOMING attack messages
    //------------------------------------------------------------------------
    listen(integer channel,string name,key id, string message) {
        DEBUG("listen: channel=["+(string)channel+"] name=["+name+"] id=["+(string)id+"] message=["+message+"]");
        if ( channel == PUBLIC_CHANNEL ) {
            if ( id == llGetOwner() ) COMMAND(message);
            return;
        }
        if ( channel == CHANOBJECT ) { // is this message on the dynamic channel?        
            list fields = llParseString2List(message,[DIV],[]); // break line of text into = delimited fields
            string command = llToLower(llStringTrim(llList2String(fields,0),STRING_TRIM)); // field zero is the "command"

            // INCOMING BULLET HIT MESSAGE FROM OUTGOING ATTACK
            // If NPC Critter/Goon Bullet has hit, fire a hitcheck regionwide at targetplayer's channel
            if ( command == "rangedcombat" ) {
                integer attdice = llList2Integer(fields,1); // get attack dice of weapon used
                string hitwho = llList2String(fields,2); // get UUID of who we hit
                string bywho = llList2String(fields,3); // should be our own UUID
                string bywhat = llList2String(fields,4); // name of item we hit with (good for bullets/missiles)

                integer victimchan = (integer)("0x"+llGetSubString(hitwho,0,6)); // calculate victim's dynamic channel
                llRegionSay(victimchan,"RANGEDHIT"+DIV+(string)POWER+DIV+(string)SKILL_ATTACK+DIV+(string)attdice+DIV+(string)bywho+DIV+bywhat); // attack!
                DEBUG((string)victimchan+" RANGEDHIT"+DIV+(string)POWER+DIV+(string)SKILL_ATTACK+DIV+(string)attdice+DIV+(string)bywho+DIV+bywhat);
                return;
            } // end if RANGEDCOMBAT/TOHIT

            // INCOMING ATTACK COMMAND FROM SOMEONE ELSE - FIXME - Armor, Damage, Incapacitated, Dead
            if ( command == "hitcheck" || command == "rangedhit" || command == "closehit" ) { // is this an attack command?
                integer attackstat = llList2Integer(fields,1); // get the value of the attacker's stat
                integer attackskill = llList2Integer(fields,2); // get the attackers skill level
                integer attackdice = llList2Integer(fields,3);  // get the attackers weapon attack dice
                key owner = llList2Key(fields,4); // get the owner of the attacking object
                string item = llList2String(fields,5); // get the name of the attacking object
                if ( attackstat < MINSTAT || attackstat > MAXSTAT ) { // is attack stat valid?
                    ERROR("Attack stat value out of range: "+(string)MINSTAT+"-"+(string)MAXSTAT); // report the invalid value
                    return; // exit early since we've hit a fatal error with message
                }
                if ( attackskill < MINSKILL || attackstat > MAXSKILL ) { // is attacker skill value valid?
                    ERROR("Attack skill value out of range: "+(string)MINSKILL+"-"+(string)MAXSKILL); // report invalid value
                    return; // exit early since we've hit a fatal error with message
                }                
                if ( attackdice < MINDAMAGE || attackdice > MAXDAMAGE ) { // is attack dice of object valid?
                    ERROR("Attack dice value out of range: "+(string)MINDAMAGE+"-"+(string)MAXDAMAGE); // report invalid value
                    return; // exit early since we've hit a fatal error with message
                }
                // its all good - report the hit
                RPEVENT(llGetObjectName()+" attacked with "+llKey2Name(owner)+"'s "+item+" for "+(string)attackdice+" attack dice!");
                DAMAGE_CHECK(attackstat,attackskill,attackdice,owner,item);
                return; // exit early in case we add more commands later
            } // end if INCOMING attack command           
        } // end if channel object        
    }

    //------------------------------------------------------------------------
    // OBJECT_REZ - when NPC fires, tell bullet the UUID of shooter
    //------------------------------------------------------------------------
    object_rez(key child) { // tell child turret bullet our key for combat resolution
        integer childchan = (integer)("0x"+llGetSubString((string)child,0,6)); // get turret bullet's dynamic channel
        llRegionSay(childchan,(string)llGetKey()); // tell turret bullet who shot them
    }    
            
    //------------------------------------------------------------------------
    // ON_REZ EVENT
    //------------------------------------------------------------------------
    on_rez(integer start_param) {
        start_param = 0; // LSLint
        RESET(); // nothing drastic, just reset script and start through state_entry
    }
    
    //------------------------------------------------------------------------
    // NO_SENSOR - when it fires and nothing is found, clear the sensor save lists
    //------------------------------------------------------------------------
    no_sensor() {
        DETECTED_KEY = []; // empty list of found keys
        DETECTED_POS = []; // empty list of found key positions
        DETECTED_ROT = []; // empty list of found key rotations
    }
    
    //------------------------------------------------------------------------
    // SENSOR - what does NPC see? save the list of keys, positions, and rotations
    //------------------------------------------------------------------------
    sensor(integer num_detected) {
        integer loop = 0;
        DETECTED_KEY = []; // empty list of found keys
        DETECTED_POS = []; // empty list of found key positions
        DETECTED_ROT = []; // empty list of found key rotations
        for ( loop = 0; loop < num_detected; loop++ ) {
            DETECTED_KEY = DETECTED_KEY + llDetectedKey(loop);
            DETECTED_POS = DETECTED_POS + llDetectedPos(loop);
            DETECTED_ROT = DETECTED_ROT + llDetectedRot(loop);
        }
    }
    
    //------------------------------------------------------------------------
    // STATE_ENTRY - run setup and wait to be activated
    //------------------------------------------------------------------------
    state_entry() {
        SETUP();
    }
    
    //------------------------------------------------------------------------
    // TIMER - pumps the heart over and over when touch activated
    //------------------------------------------------------------------------
    timer() {
        HEARTBEAT();
    }

    //------------------------------------------------------------------------
    // TOUCH_START - activate or de-activate the NPC sensor and heartbeat function
    //------------------------------------------------------------------------
    touch_start(integer num_detected) {
        num_detected = 0; // LSLINT
        if ( llDetectedKey(0) != llGetOwner() ) return; // ignore touches from anyone except owner
        if ( ISACTIVE == FALSE ) {
            ISACTIVE = TRUE;
            CHANOBJECT = (integer)("0x"+llGetSubString(llGetKey(),0,6));
            if ( HANDOBJECT != 0 ) llListenRemove(HANDOBJECT); // remove an existing listener channel
            HANDOBJECT = llListen(CHANOBJECT,"",NULL_KEY,"");
            llSensorRepeat("",NULL_KEY,AGENT,SENSOR_RANGE,SENSOR_ARC,SENSOR_RATE);
            llSetTimerEvent(HEARTRATE);
            llOwnerSay("NPC Critter/Goon active");
        } else {
            ISACTIVE = FALSE;
            if ( HANDOBJECT != 0 ) llListenRemove(HANDOBJECT); // remove an existing listener channel
            if ( HANDLE_PUB != 0 ) llListenRemove(HANDLE_PUB); // remove an existing listener channel
            llSensorRemove();
            llSetTimerEvent(0.0);
            llOwnerSay("NPC Critter/Goon no longer active");
        }
    }
}