Myriad Lite Module Resilience

// Myriad_Lite_Module_Resilience-v0.0.7-20131102.lsl
// Copyright (c) 2013 by Allen Kerensky (OSG/SL) All Rights Reserved.
// This work is dual-licensed under
// Creative Commons Attribution (CC BY) 3.0 Unported
// http://creativecommons.org/licenses/by/3.0/
// - or -
// Modified BSD License (3-clause)
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
// * Redistributions of source code must retain the above copyright notice, 
//   this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
//   this list of conditions and the following disclaimer in the documentation
//   and/or other materials provided with the distribution.
// * Neither the name of Myriad Lite nor the names of its contributors may be
//   used to endorse or promote products derived from this software without
//   specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY EXPRESS OR
// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
// NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
// NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// The Myriad RPG System was designed, written, and illustrated by Ashok Desai
// Myriad RPG System licensed under:
// Creative Commons Attribution (CC BY) 2.0 UK: England and Wales
// http://creativecommons.org/licenses/by/2.0/uk/
 
// FIXME ADD RESOLVEHIT
// FIXME ADD MORE RPEVENT TO MODULE
// FIXME ADD MORE HELP TO MODULE
// FIXME WHY IS DEAD ANIMATION INFINITE?
// FIXME REGION SETTING DEATHPOINT
// FIXME REGION SETTING SPAWNPOINT
// FIXME REGION SETTING RESPAWNTIME
 
// CONSTANTS - DO NOT CHANGE DURING RUN
string BASENAME = "Myriad Lite Module Resilience";
string VERSION = "0.0.7"; // Allen Kerensky's script version
string VERSIONDATE = "20131102"; // Allen Kerensky's script yyyymmdd
 
// Module to Module Messaging Constants
integer MODULE_RESILIENCE = -8;
integer LM_SENDTOATTACHMENT = 0x80000000;
integer RENDEZVOUS1 = -999; // chat sent to ALL Myriad players in region
 
// RUNTIME GLOBALS - CAN CHANGE DURING RUN
key LASTHITBY; // UUID of the last person to HIT us
 
/////////////////////// REMOVE ALL THIS
// ARMOR
integer CURARMOR = 0; // highest armor value worn out of all armor worn, not a total
// FIXME MOVE THIS TO ARMOR AND SEND LINK MESSAGES
integer CHANATTACH; // dynamic channel for attachments
 
string ANIM_INCAPACITATED = "sleep"; // FIXME WELL anim when incapacitated
string ANIM_DEAD = "dead"; // FIXME WELL anim when dead
//integer FLAG_ANIMATE; // FIXME WELL
//vector  MOVELOCK = <0,0,0>; // FIXME WELL movelock position when incapacitated or dead
//float   TAU = 0.05; // FIXME WELL movelock tau
 
vector DEATHPOINT; // FIXME MOVE TO REGION SETTING OPENSIM ONLY coordinates to move to on death
vector RESPAWNPOINT; // FIXME MOVE TO REGION SETTING OPENSIM ONLY coordinates to move to on respawn
float RESPAWN_TIME = 30.0; // FIXME MOVE TO REGION SETTING time dead before automatic respawn
//
/////////////////////// END REMOVE ALL THIS
 
//////////////////////////////////////////////////////////////////////////////
// PPMA FUNCTIONS
string KEY_NOT_FOUND = "[KEY_NOT_FOUND]";
string GET_VAL(string dbkey,string field) {
    //OWNERSAY("GET_VAL KEY=["+dbkey+"] FIELD=["+field+"]");
    string out = KEY_NOT_FOUND;
    integer i;
    string name;
    string desc;
    for ( i = 2; i <= llGetNumberOfPrims(); i++ ) {
        name = llList2String(llGetLinkPrimitiveParams(i,[PRIM_NAME]),0);
        desc = llList2String(llGetLinkPrimitiveParams(i,[PRIM_DESC]),0);
        if ( llToLower(name) == llToLower(dbkey) && llToLower(desc) == llToLower(field) ) {
            out = llList2String(llGetLinkPrimitiveParams(i,[PRIM_TEXT]),0);
            //DEBUG("GET_VAL RETURN=["+out+"]");
            return out; // bail on first match?
        }
    }
    //OWNERSAY("GET_VAL RETURN=["+out+"]");
    return out;
}
 
SET_VAL(string dbkey, string field, string val) {
    //OWNERSAY("SET_VAL KEY=["+dbkey+"] FIELD=["+field+"] VAL=["+val+"]");
    integer i;
    string name;
    string desc;
    integer written = FALSE;
    for ( i = 2; i <= llGetNumberOfPrims(); i++ ) {
        name = llStringTrim(llList2String(llGetLinkPrimitiveParams(i,[PRIM_NAME]),0),STRING_TRIM);
        desc = llStringTrim(llList2String(llGetLinkPrimitiveParams(i,[PRIM_DESC]),0),STRING_TRIM);
        if ( ( llToLower(name) == llToLower(dbkey) && llToLower(desc) == llToLower(field) ) && written == FALSE ) {
            //OWNERSAY("SET_VAL UPDATE RECORD=["+(string)i+"] DBKEY=["+dbkey+"] DESC=["+field+"] VAL=["+val+"]");
            llSetLinkPrimitiveParamsFast(i,[PRIM_NAME,dbkey,PRIM_DESC,field,PRIM_TEXT,val,ZERO_VECTOR,0.0]);
            written = TRUE; // we did an update, remember it
        }    
    }
    if ( written == TRUE ) {
        //OWNERSAY("SET_VAL UPDATE COMPLETE.");
        return;
    }
    // data hasn't been written, scan for free prim
    for ( i = 2; i <= llGetNumberOfPrims(); i++ ) {
        name = llStringTrim(llList2String(llGetLinkPrimitiveParams(i,[PRIM_NAME]),0),STRING_TRIM);
        desc = llStringTrim(llList2String(llGetLinkPrimitiveParams(i,[PRIM_DESC]),0),STRING_TRIM);
        if ( ( name == "" && desc == "" ) && written == FALSE ) {
            //OWNERSAY("SET_VAL INSERT RECORD=["+(string)i+"] DBKEY=["+dbkey+"] DESC=["+field+"] VAL=["+val+"]");
            llSetLinkPrimitiveParamsFast(i,[PRIM_NAME,dbkey,PRIM_DESC,field,PRIM_TEXT,val,ZERO_VECTOR,0.0]);
            written = TRUE; // we did an update, remember it
        }
    }
    if ( written == TRUE ) {
        //OWNERSAY("SET_VAL INSERT COMPLETED.");
        return;
    }
    //OWNERSAY("SET_VAL NO FREE RECORD FOUND TO INSERT INTO! DATA LOST");
}
 
//////////////////////////////////////////////////////////////////////////////
// FLAG FUNCTIONS
// INCAPACITATED - lost wounds
// DEAD - lost critical wounds
// IDEA: INDECISION - lost resolve?
 
// LMIM: SET_FLAG|<name>=<TRUE|FALSE>
// PPMA: FLAG,<flagname>,<TRUE|FALSE>
// LMOUT: FLAG|<flagname>=<TRUE|FALSE>
// LMERR: FIXME
integer GET_FLAG(string flag) {
    string val = GET_VAL("FLAG",llToUpper(flag));
    if ( val == KEY_NOT_FOUND ) {
        ERROR("Flag ["+flag+"] does not exist.");
    }
    if ( val != "0" && val != "1") {
        ERROR("Flag: "+flag+" invalid value ["+val+"]");
    }
    integer bool = (integer)val;
    return bool;
}
 
// SET_FLAG
// LMIM: SET_FLAG|<flagname>=<TRUE|FALSE>
// PPMA: FLAG,<flagname>,<TRUE|FALSE>
// LMOUT: SET_FLAG|<flagname>=<TRUE|FALSE>
// LMERR: FIXME
SET_FLAG(string flag,integer value) {
    if ( flag == "" ) return; // FIXME add error
    if ( value != TRUE && value != FALSE ) return; // FIXME add err
    SET_VAL("FLAG",llToUpper(flag),(string)value);
}
 
//
// GET_MYRIAD
// Requires a MYRIAD CONSTANT NAME
// Returns the amount of that Myriad rules constant or defaultif the player does not currently have that constant
//
integer GET_MYRIAD(string setting) {
    integer retval;
    string value = GET_VAL("MYRIAD",setting);
    if ( value == KEY_NOT_FOUND ) {
        if ( setting == "MINSTAT" ) retval = 1;
        if ( setting == "MAXSTAT" ) retval = 10;
        if ( setting == "MINSKILL" ) retval = 0;
        if ( setting == "MAXSKILL" ) retval = 7;
        if ( setting == "MINEFFECT" ) retval = 0;
        if ( setting == "MAXEFFECT" ) retval = 5;
        if ( setting == "MINRESILIENCE" ) retval = 0;
        if ( setting == "MAXRESILIENCE" ) retval = 20;
        if ( setting == "MINBOON" ) retval = 0;
        if ( setting == "MAXBOON" ) retval = 5;
        if ( setting == "MINFLAW" ) retval = 0;
        if ( setting == "MAXFLAW" ) retval = 5;
        if ( setting == "MINRP" ) retval = 0;
        if ( setting == "MAXRP" ) retval = 10;
        if ( setting == "MINITEM" ) retval = 0;
        if ( setting == "MAXITEM" ) retval = 100;
        if ( setting == "MINARMOR" ) retval = 0;
        if ( setting == "MAXARMOR" ) retval = 5;
        if ( setting == "MINDAMAGE" ) retval = 1;
        if ( setting == "MAXDAMAGE" ) retval = 5;
        ERROR("Unable to locate Myriad setting "+setting+" returning "+(string)retval);
    } else {
        retval = (integer)value;
    }
    return retval;
}
 
// GET_WOUNDS
integer GET_WOUNDS() {
    string woundval = GET_VAL("RESILIENCE","Wounds");
    if ( woundval == KEY_NOT_FOUND ) {
        ERROR("GET_WOUNDS unable to locate RESILIENCE,Wounds key in PPMA.");
        return -1;
    }
    integer wounds = (integer)woundval;
    if ( wounds < GET_MYRIAD("MINRESILIENCE") || wounds > GET_MYRIAD("MAXRESILIENCE") ) {
        ERROR("GET_WOUNDS value "+woundval+" out of allowed range "+(string)GET_MYRIAD("MINRESILIENCE")+"-"+(string)GET_MYRIAD("MAXRESILIENCE"));
        return -2;
    }
    return wounds;
}
 
// SET_WOUNDS
//SET_WOUNDS(integer wounds) {
//    if ( wounds < GET_MYRIAD("MINRESILIENCE") || wounds > GET_MYRIAD("MAXRESILIENCE") ) {
//        ERROR("SET_WOUNDS value "+(string)wounds+" out of allowed range "+(string)GET_MYRIAD("MINRESILIENCE")+"-"+(string)GET_MYRIAD("MAXRESILIENCE"));
//        return;
//    }
//    llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"SET_RESILIENCE|Wounds="+(string)wounds,llGetOwner());    
//}
 
// GET_CURRENTWOUNDS
integer GET_CURRENTWOUNDS() {
    string woundval = GET_VAL("RESILIENCE","CurrentWounds");
    if ( woundval == KEY_NOT_FOUND ) {
        ERROR("GET_CURRENTWOUNDS unable to locate RESILIENCE,CurrentWounds key in PPMA.");
        return -1;
    }
    integer wounds = (integer)woundval;
    if ( wounds < GET_MYRIAD("MINRESILIENCE") || wounds > GET_MYRIAD("MAXRESILIENCE") ) {
        ERROR("GET_CURRENTWOUNDS value "+woundval+" out of allowed range "+(string)GET_MYRIAD("MINRESILIENCE")+"-"+(string)GET_MYRIAD("MAXRESILIENCE"));
        return -2;
    }
    return wounds;
}
 
// SET_CURRENTWOUNDS
SET_CURRENTWOUNDS(integer curwounds) {
    if ( curwounds < GET_MYRIAD("MINRESILIENCE") || curwounds > GET_MYRIAD("MAXRESILIENCE") ) {
        ERROR("SET_CURRENTWOUNDS value "+(string)curwounds+" out of allowed range "+(string)GET_MYRIAD("MINRESILIENCE")+"-"+(string)GET_MYRIAD("MAXRESILIENCE"));
        return;
    }
    // check if current <= max or only set to max
    integer max = GET_WOUNDS();
    if ( curwounds > max ) {
        ERROR("SET_CURRENTWOUNDS "+(string)curwounds+" greater than player max "+(string)max+", resetting to player max");
        curwounds = max;
    }
    llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"SET_RESILIENCE|CurrentWounds="+(string)curwounds,llGetOwner());    
}
 
// GET_CRITICALWOUNDS
integer GET_CRITICALWOUNDS() {
    string woundval = GET_VAL("RESILIENCE","CriticalWounds");
    if ( woundval == KEY_NOT_FOUND ) {
        ERROR("GET_CRITICALWOUNDS unable to locate RESILIENCE,CriticalWounds key in PPMA.");
        return -1;
    }
    integer wounds = (integer)woundval;
    if ( wounds < GET_MYRIAD("MINRESILIENCE") || wounds > GET_MYRIAD("MAXRESILIENCE") ) {
        ERROR("GET_CRITICALWOUNDS value "+woundval+" out of allowed range "+(string)GET_MYRIAD("MINRESILIENCE")+"-"+(string)GET_MYRIAD("MAXRESILIENCE"));
        return -2;
    }
    return wounds;
}
 
// SET_CRITICALWOUNDS
//SET_CRITICALWOUNDS(integer critwounds) {
//    if ( critwounds < GET_MYRIAD("MINRESILIENCE") || critwounds > GET_MYRIAD("MAXRESILIENCE") ) {
//        ERROR("SET_CRITICALWOUNDS value "+(string)critwounds+" out of allowed range "+(string)GET_MYRIAD("MINRESILIENCE")+"-"+(string)GET_MYRIAD("MAXRESILIENCE"));
//        return;
//    }
//    llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"SET_RESILIENCE|CriticalWounds="+(string)critwounds,llGetOwner());    
//}
 
// GET_CURRENTCRITICALWOUNDS
integer GET_CURRENTCRITICALWOUNDS() {
    string woundval = GET_VAL("RESILIENCE","CurrentCriticalWounds");
    if ( woundval == KEY_NOT_FOUND ) {
        ERROR("GET_CURRENTCRITICAL WOUNDS unable to locate RESILIENCE,CurrentCriticalWounds key in PPMA.");
        return -1;
    }
    integer wounds = (integer)woundval;
    if ( wounds < GET_MYRIAD("MINRESILIENCE") || wounds > GET_MYRIAD("MAXRESILIENCE") ) {
        ERROR("GET_CURRENTCRITICALWOUNDS value "+woundval+" out of allowed range "+(string)GET_MYRIAD("MINRESILIENCE")+"-"+(string)GET_MYRIAD("MAXRESILIENCE"));
        return -2;
    }
    return wounds;
}
 
// SET_CURRENTCRITICALWOUNDS
SET_CURRENTCRITICALWOUNDS(integer curcritwounds) {
    if ( curcritwounds < GET_MYRIAD("MINRESILIENCE") || curcritwounds > GET_MYRIAD("MAXRESILIENCE") ) {
        ERROR("SET_WOUNDS value "+(string)curcritwounds+" out of allowed range "+(string)GET_MYRIAD("MINRESILIENCE")+"-"+(string)GET_MYRIAD("MAXRESILIENCE"));
        return;
    }
    // check if current <= max or only set to max
    integer max = GET_CRITICALWOUNDS();
    if ( curcritwounds > max ) {
        ERROR("SET_CURRENCRITICALWOUNDS "+(string)curcritwounds+" greater than player max "+(string)max+", resetting to player max");
        curcritwounds = max;
    }
    llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"SET_RESILIENCE|CurrentCriticalWounds="+(string)curcritwounds,llGetOwner());    
}
 
// GET_RESOLVE
integer GET_RESOLVE() {
    string resolveval = GET_VAL("RESILIENCE","Resolve");
    if ( resolveval == KEY_NOT_FOUND ) {
        ERROR("GET_RESOLVE unable to locate RESILIENCE,Resolve key in PPMA.");
        return -1;
    }
    integer resolve = (integer)resolveval;
    if ( resolve < GET_MYRIAD("MINRESILIENCE") || resolve > GET_MYRIAD("MAXRESILIENCE") ) {
        ERROR("GET_RESOLVE value "+resolveval+" out of allowed range "+(string)GET_MYRIAD("MINRESILIENCE")+"-"+(string)GET_MYRIAD("MAXRESILIENCE"));
        return -2;
    }
    return resolve;
}
 
// SET_RESOLVE
//SET_RESOLVE(integer resolve) {
//    if ( resolve < GET_MYRIAD("MINRESILIENCE") || resolve > GET_MYRIAD("MAXRESILIENCE") ) {
//        ERROR("SET_RESOLVE value "+(string)resolve+" out of allowed range "+(string)GET_MYRIAD("MINRESILIENCE")+"-"+(string)GET_MYRIAD("MAXRESILIENCE"));
//        return;
//    }
//    llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"SET_RESILIENCE|Resolve="+(string)resolve,llGetOwner());    
//}
 
// GET_CURRENTRESOLVE
integer GET_CURRENTRESOLVE() {
    string resolveval = GET_VAL("RESILIENCE","CurrentResolve");
    if ( resolveval == KEY_NOT_FOUND ) {
        ERROR("GET_CURRENTRESOLVE unable to locate RESILIENCE,CurrentResolve key in PPMA.");
        return -1;
    }
    integer resolve = (integer)resolveval;
    if ( resolve < GET_MYRIAD("MINRESILIENCE") || resolve > GET_MYRIAD("MAXRESILIENCE") ) {
        ERROR("GET_CURRENTRESOLVE value "+resolveval+" out of allowed range "+(string)GET_MYRIAD("MINRESILIENCE")+"-"+(string)GET_MYRIAD("MAXRESILIENCE"));
        return -2;
    }
    return resolve;
}
 
// SET_CURRENTRESOLVE
SET_CURRENTRESOLVE(integer resolve) {
    if ( resolve < GET_MYRIAD("MINRESILIENCE") || resolve > GET_MYRIAD("MAXRESILIENCE") ) {
        ERROR("SET_CURRENTRESOLVE value "+(string)resolve+" out of allowed range "+(string)GET_MYRIAD("MINRESILIENCE")+"-"+(string)GET_MYRIAD("MAXRESILIENCE"));
        return;
    }
    // check if current <= max or only set to max
    integer max = GET_RESOLVE();
    if ( resolve > max ) {
        ERROR("SET_CURRENTRESOLVE "+(string)resolve+" greater than player max "+(string)max+", resetting to player max");
        resolve = max;
    }
    llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"SET_RESILIENCE|CurrentResolve="+(string)resolve,llGetOwner());    
}
 
DEAD() {
    SET_FLAG("DEAD",TRUE); // remember that we're now dead
    llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"STARTANIMATION|ANIMATION="+ANIM_DEAD,llGetOwner());
    //llStartAnimation(ANIM_DEAD); // start dead animation
    string hitname = llList2String(llParseString2List(llKey2Name(LASTHITBY),["@"],[]),0); // strip @where from HG names
    RPEVENT("has been killed by "+hitname+"!");
    OWNERSAY("You've been killed by "+hitname+"!");
    llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"DEAD",LASTHITBY);
    // FIXME if ( DEATHPOINT != ZERO_VECTOR ) osTeleportAgent(llGetKey(),DEATHPOINT);
    METER(); // update hover text
    llSetTimerEvent(RESPAWN_TIME); // respawn in a bit
}
 
// DEBUG - show debug chat with wearer name for sorting
DEBUG(string dmessage) {
    if ( GET_FLAG("DEBUG") == TRUE ) llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"DEBUG|"+dmessage,llGetOwner());
}
 
// ERROR - show errors on debug channel with wearer name for sorting
ERROR(string emessage) {
    llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"ERROR|"+emessage,llGetOwner());
}
 
// MEMORY
GET_MEMORY() {
    OWNERSAY(BASENAME+" free memory: "+(string)llGetFreeMemory());
}
 
// GETVERSION
GET_VERSION() {
    OWNERSAY(BASENAME+" v"+VERSION+"-"+VERSIONDATE);
}
 
// HEAL - restore lost WOUND and CRITICAL resilience
// Thanks to Artemis Tesla for contributing summary report logic
HEAL(integer healamount) {
    integer critsHealed = 0; // track how many crit boxes restored for summary report
    integer woundsHealed = 0; // track how many non-crit boxes restored for summary report
    integer reborn = FALSE; // track if reborn/respawn or not for summary report
    integer revived = FALSE;  // track of revived or not for summary report
    integer curwounds = GET_CURRENTWOUNDS();
    integer maxwounds = GET_WOUNDS();
    integer curcritical = GET_CURRENTCRITICALWOUNDS();
    integer maxcritical = GET_CRITICALWOUNDS();
 
    // TODO report once for multiple healing amounts
    while ( healamount-- ) {
        // step through each point of healing
        if ( curcritical < maxcritical ) { // is current critical less than max critical
            DEBUG("Heal one critical wound");
            curcritical++; // heal one current critical
            SET_CURRENTCRITICALWOUNDS(curcritical);
            critsHealed++; // add a point back
            if ( GET_FLAG("DEAD") == TRUE ) {   // healed a critical, critical now > 0 so not dead anymore
                SET_FLAG("DEAD",FALSE); // no longer dead
                llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"STOPANIMATION|ANIMATION="+ANIM_DEAD,llGetOwner());
                //llStopAnimation(ANIM_DEAD);
                llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"STARTANIMATION|ANIMATION="+ANIM_INCAPACITATED,llGetOwner());
                //llStartAnimation(ANIM_INCAPACITATED);
                llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"ALIVE",llGetOwner());
                METER();
                reborn = TRUE; // show rebirth in summary report
                DEBUG("Heal: reborn");
            }
        } else {
                if ( curwounds < maxwounds ) {   // player not critical, heal non-critical?
                    DEBUG("Heal one wound");
                    curwounds++; // add the healing point to current wounds
                    SET_CURRENTWOUNDS(curwounds);
                    woundsHealed++; // add a point of non-critical
                    if ( GET_FLAG("INCAPACITATED") == TRUE ) { // were they incapacitated?
                        SET_FLAG("INCAPACITATED",FALSE); // no longer gravely wounded
                        //llStopAnimation(ANIM_DEAD);
                        llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"STOPANIMATION|ANIMATION="+ANIM_DEAD,llGetOwner());
                        //llStopAnimation(ANIM_INCAPACITATED);
                        llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"STOPANIMATION|ANIMATION="+ANIM_INCAPACITATED,llGetOwner());
                        llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"REVIVED",llGetOwner());
                        METER();
                        revived = TRUE; // show revival in summary report
                        DEBUG("Heal: Revived!");
                        llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"STOPMOVELOCK",llGetOwner());
                        //llStopMoveToTarget();
                        //MOVELOCK = <0,0,0>;
                    }
                } // end if curwounds < wounds
        }
    } // end while
 
    // Summary report of healing effects
    if ( critsHealed > 0 ) {   // was at least one critical healed?
        DEBUG("Critical Heal: "+(string)curcritical+" of "+(string)maxcritical+" critical wound boxes.");
        if (critsHealed > 1) { // was more then one critical wound healed?
            OWNERSAY("Critical " + (string)critsHealed + " wounds healed.");
        } else {
            OWNERSAY("Critical " + (string)critsHealed + " wound healed.");
        }
    }
    if (reborn == TRUE ) { // if player reborn from this heal
        RPEVENT("has been resurrected!");
        llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"STOPANIMATION|ANIMATION="+ANIM_DEAD,llGetOwner());
        //if ( FLAG_ANIMATE == TRUE ) { // if we're allowed to change animations
        //    llStopAnimation(ANIM_DEAD); // stop "we're dead" animation
        //}
        OWNERSAY("You've been resurrected! Welcome back to the land of the living.");
    }
 
    if ( woundsHealed > 0 ) { // was at least 1 non-critical healed?
        DEBUG("Heal Non-Critical Wounds: "+(string)curwounds+" of "+(string)maxwounds+" non-critical wound boxes.");
        if (woundsHealed > 1) { // was more than one non-critical healed?
            OWNERSAY((string)woundsHealed + " non-critical wounds healed.");
        } else {
            OWNERSAY((string)woundsHealed + " non-critical wound healed.");
        }
    }
 
    if ( revived == TRUE ) { // if player revived from this heal
        RPEVENT("has revived and is no longer incapacitated!");
        llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"STOPANIMATION|ANIMATION="+ANIM_INCAPACITATED,llGetOwner());
        //if ( FLAG_ANIMATE == TRUE ) { // if we're allowed to change anims
        //    llStopAnimation(ANIM_INCAPACITATED); // stop the "we're down" animation
        //}
        OWNERSAY("You are no longer incapacitated! Welcome back to the fight!");
    }
    METER(); // update hovertext
}
 
// HIT - player is hit - check to see if attack dice breach armor
// Making A Damage Roll (Myriad p25, Myriad Special Edition p31)
HIT(integer attackdice) {
    integer damagetaken = 0; // start with zero damage
    while(attackdice--) { // roll for each attack dice
        integer dieroll = 1+(integer)llFrand(6.0); // reasonably uniform d6
        if ( dieroll > CURARMOR ) { // attack roll stronger than armor worn?
            damagetaken++; // add a wound point
        }
    }
    // finished roll how did we do?
    if ( damagetaken > 0 ) { // we took damage
        if ( CURARMOR > 0 ) { // wearing armor? tell them it was breached
            OWNERSAY("That attack penetrated your armor and you've been wounded!");
            llWhisper(CHANATTACH,"ARMOREFFECTHIT");
        } else { // fighting in no armor?
            OWNERSAY("You've been wounded! Wear some armor next time?");
        }
        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
        OWNERSAY("Your armor blocked the damage from that attack!");
        llWhisper(CHANATTACH,"ARMOREFFECTBLOCKED");
    }
}
 
// INCAPACITATED - player lost all WOUNDS - unable to act
INCAPACITATED() {
    SET_FLAG("INCAPACITATED",TRUE); // yes, we're now incapacitated
    llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"STARTMOVELOCK",llGetOwner());
    //if ( MOVELOCK == <0,0,0> ) MOVELOCK = llGetPos();
    //llMoveToTarget(MOVELOCK,TAU);
    llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"STARTANIMATION|ANIMATION="+ANIM_INCAPACITATED,llGetOwner());
    //llStartAnimation(ANIM_INCAPACITATED); // "we're hurt and down" animation
    RPEVENT("has been incapacitated!");
    OWNERSAY("You've been incapacitated!");
    llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"INCAPACITATED",llGetOwner());
    METER(); // update meter
    llSetTimerEvent(RESPAWN_TIME); // heal in a bit
}
 
// METER - update a hovertext health meter or HUD bar graph
METER() {
    llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"METER",llGetOwner());
}
 
//
// OWNERSAY
//
OWNERSAY(string msg) {
    llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"OWNERSAY|"+msg,llGetOwner());
}
 
//
// RECOVER - restore lost RESOLVE resilience
//
RECOVER(integer healamount) {
    integer resolvehealed = 0; // track how many non-crit boxes restored for summary report
    integer recovered = FALSE;  // track of revived or not for summary report
    integer curresolve = GET_CURRENTRESOLVE();
    integer maxresolve = GET_RESOLVE();
 
    // TODO report once for multiple healing amounts
    while ( healamount-- ) {
        // step through each point of healing
        if ( curresolve < maxresolve ) {   // do we need to heal non-critical resolve?
            DEBUG("Heal one resolve");
            curresolve++; // add the healing point to current resolve
            SET_CURRENTRESOLVE(curresolve);
            resolvehealed++; // add a point of resolve healed for report
            llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"RECOVERED",llGetOwner());
            recovered = TRUE; // show revival in summary report
        } // end if curwounds < wounds
    } // end while
 
    if (recovered == TRUE ) { // if player reborn from this heal
        RPEVENT("has recovered their Resolve.");
        OWNERSAY("You've recovered your Resolve.");
    }
 
    if ( resolvehealed > 0 ) { // was at least 1 non-critical healed?
        DEBUG("Heal Non-Critical Resolve: "+(string)curresolve+" of "+(string)maxresolve+" non-critical boxes.");
        if (resolvehealed > 1) { // was more than one non-critical healed?
            OWNERSAY((string)resolvehealed + " non-critical Resolve boxes healed.");
        } else {
            OWNERSAY((string)resolvehealed + " non-critical Resolve box healed.");
        }
    }
} 
 
// RESET - shut down running animations then reset the script to reload character sheet
RESET() {
    // do any cleanup tasks here before reset    
    llResetScript(); // now reset
}
 
//
// RESOLVEHIT - player is hit - check to see if attack dice breach armor
// Making A Damage Roll (Myriad p25, Myriad Special Edition p31)
RESOLVEHIT(integer attackdice) {
    integer damagetaken = 0; // start with zero damage
    while(attackdice--) { // roll for each attack dice
        //integer dieroll = 1+(integer)llFrand(6.0); // FIXME SOCIAL ARMOR? reasonably uniform d6
        //if ( dieroll > CURARMOR ) { // FIXME SOCIAL 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 ( CURARMOR > 0 ) { // wearing armor? tell them it was breached
        //    OWNERSAY("That attack penetrated your armor and you've been wounded!");
        //    llWhisper(CHANATTACH,"ARMOREFFECTHIT");
        //} else { // fighting in no armor?
        //}
 
        OWNERSAY("You've lost some of your resolve to continue. Can you rally?");            
        RESOLVELOST(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
    //    OWNERSAY("Your armor blocked the damage from that attack!");
    //    llWhisper(CHANATTACH,"ARMOREFFECTBLOCKED");
    //}
}
 
//
// RESOLVELOST - Player takes Resolve Resilience damage
//
RESOLVELOST(integer amount) {
    while (amount--) { // for each wound taken
        integer curresolve = GET_CURRENTRESOLVE();
        //integer curwounds = GET_CURRENTWOUNDS();
        //integer curcritical = GET_CURRENTCRITICALWOUNDS();
        //integer maxcritical = GET_CRITICALWOUNDS();
        //if ( curwounds > 0 && curcritical != maxcritical ) {
        //    llSay(DEBUG_CHANNEL,"ERROR! WOUND STATE IS NONSENSE! CANNOT APPLY DAMAGE!");
        //    OWNERSAY("ERROR! WOUND STATE IS NONSENSE! CANNOT APPLY DAMAGE! TELL ALLEN KERENSKY!");
        //    return;
        //}
        if ( curresolve > 1 ) { // resolve boxes left?
            curresolve--; // scratch off one
            SET_CURRENTRESOLVE(curresolve);
            RPEVENT("has lost some of the resolve to continue.");
        } else { // out of resolve, out of the social combat
            curresolve = 0; // force zero
            SET_CURRENTRESOLVE(curresolve);
            string hitbyname = llList2String(llParseString2List(llKey2Name(LASTHITBY),["@"],[]),0); // strip @where from HG names
            RPEVENT("has lost all resolve and been defeated by "+hitbyname+"!");
            OWNERSAY("You've lost all resolve and been defeated by "+hitbyname+"!");
            llSetTimerEvent(RESPAWN_TIME); // respawn in a bit
        }
    } // end while
}
 
// RPEVENT
// FIXME - change all RPEVENT to link message for main HUD to send?
RPEVENT(string rpevent) {
    llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"RPEVENT|"+rpevent,llGetOwner());
}
 
// SETUP - begin bringing the HUD online
SETUP() {
 
    SET_FLAG("INCAPACITATED",FALSE); // FIXME set them to default in setup, need a function to scan PPMA-stored resilience and calculate at setup based on values rather than a default
    SET_FLAG("DEAD",FALSE); // FIXME set them to default in setup, need a function to scan PPMA-stored resilience and calculate at setup based on values rather than a default
 
    CHANATTACH = (integer)("0x"+llGetSubString((string)llGetOwner(),1,7)); // attachment-specific channel
    llRegionSay(RENDEZVOUS1,"GET_REGION_SETTING|DEATHPOINT");
    llRegionSay(RENDEZVOUS1,"GET_REGION_SETTING|RESPAWNPOINT");
    //llRequestPermissions(llGetOwner(),PERMISSION_TRIGGER_ANIMATION);
    OWNERSAY("Resilience module active.");
}
 
// WOUNDED - Player takes Resilience damage
WOUNDED(integer amount) {
    while (amount--) { // for each wound taken
        integer curwounds = GET_CURRENTWOUNDS();
        integer curcritical = GET_CURRENTCRITICALWOUNDS();
        integer maxcritical = GET_CRITICALWOUNDS();
        if ( curwounds > 0 && curcritical != maxcritical ) {
            llSay(DEBUG_CHANNEL,"ERROR! WOUND STATE IS NONSENSE! CANNOT APPLY DAMAGE!");
            OWNERSAY("ERROR! WOUND STATE IS NONSENSE! CANNOT APPLY DAMAGE! TELL ALLEN KERENSKY!");
            return;
        }
        if ( curwounds > 1 && curcritical == maxcritical ) { // wound boxes left?
            curwounds--; // scratch off one
            SET_CURRENTWOUNDS(curwounds);
        } else if ( curwounds > 0 && curcritical == maxcritical ) { // last wound box lost, now incapacitated
            curwounds = 0; // force to zero
            SET_CURRENTWOUNDS(curwounds);
            INCAPACITATED(); // show incapacitation
        } else if ( curwounds == 0 && curcritical > 1 ) { // out of wounds, but still have critical
            curwounds = 0; // force zero
            SET_CURRENTWOUNDS(curwounds);
            curcritical--; // force zero
            SET_CURRENTCRITICALWOUNDS(curcritical);            
        } else { // out of critical wounds too, dead!
            curwounds = 0; // force zero
            SET_CURRENTWOUNDS(curwounds);
            curcritical = 0; // force zero
            SET_CURRENTCRITICALWOUNDS(curcritical);
            DEAD(); // show death
        }
    } // end while
}
 
// DEFAULT STATE - load character sheet
default {
 
    // CHANGED - triggered for many changes to the avatar
    // TODO reload sim-specific settings on region change
    //changed(integer changes) {
    //    if ( changes & CHANGED_REGION || changes & CHANGED_TELEPORT ) {
    //        llRequestPermissions(llGetOwner(),PERMISSION_TRIGGER_ANIMATION);
    //    }
    //}
 
    link_message(integer sender_num,integer sender,string str,key id) {
        if ( sender == MODULE_RESILIENCE || sender == LM_SENDTOATTACHMENT ) return; // ignore our own link messages
        list fields = llParseString2List(str,["|"],[]); // break into list of fields based on "|"ider
        string command = llToLower(llStringTrim(llList2String(fields,0),STRING_TRIM)); // assume the first field is a Myriad Lite command    
 
        if ( command == "debug" || command == "error" || command == "help" || command == "ownersay" || command == "rpevent" ) return; // ignore WELL commands
 
        // finish processing module-related commands only
        string data = llStringTrim(llList2String(fields,1),STRING_TRIM); // field one is the data
        list subfields = llParseString2List(data,["="],[]); // break data field into comma-delimited subfields if needed
 
        if ( command == "memory" ) { GET_MEMORY(); return;} // show the memory info
        if ( command == "reset" ) { RESET(); return;} // reset wrapper
        if ( command == "version" ) { GET_VERSION(); return; } // show the version
 
        // only debug module related events...
         DEBUG("EVENT: link_message("+(string)sender_num+","+(string)sender+","+str+","+(string)id+")");
 
        if ( command == "armorcurrent" ) { // ARMORCURRENT|integer newcurrentarmor
            integer rating = llList2Integer(fields,1);
            if ( rating >= GET_MYRIAD("MINARMOR") && rating <= GET_MYRIAD("MAXARMOR") ) {
                CURARMOR = rating;
            }
            return;
        }
        if ( command == "hit") {
            integer attdice = llList2Integer(fields,1);
            if ( attdice >= GET_MYRIAD("MINDAMAGE") && attdice <= GET_MYRIAD("MAXDAMAGE") ) {
                LASTHITBY = id;
                HIT(attdice);
            }
            return;
        }
        if ( command == "resolvehit" ) {
            integer attdice = llList2Integer(fields,1);
            if ( attdice >= GET_MYRIAD("MINDAMAGE") && attdice <= GET_MYRIAD("MAXDAMAGE") ) {
                LASTHITBY = id;
                RESOLVEHIT(attdice);
            }
            return;
        }
        if ( command == "healpartial" ) {
            integer healpart = llList2Integer(fields,1);
            HEAL(healpart);
            return;
        }
        if ( command == "healfull" || command == "healall" ) { HEAL(20); return; } // FIXME HEAL ALL means 100 points of healing
        if ( command == "recoverpartial" ) {
            integer recoverpart = llList2Integer(fields,1);
            RECOVER(recoverpart);
            return;
        }
        if ( command == "recoverfull" || command == "recoverall" ) { RECOVER(20); return; } // FIXME RECOVER ALL means 20 points of healing
        if ( command == "region_setting" ) { // look for deathpoint and respawnpoint
            string attrib = llToLower(llStringTrim(llList2String(subfields,0),STRING_TRIM)); // find the boon name
            vector pos = llList2Vector(subfields,1); // find the boon rank value
            if ( attrib == "deathpoint" ) { DEATHPOINT = pos; }
            if ( attrib == "respawnpoint" ) { RESPAWNPOINT = pos; }
            return;
        }
        //COMMAND(str); // send to shared command processor for chat and link messages
        return;
    } // end of link_message event
 
    // STATE ENTRY - called on Reset
    state_entry() {
        // Check required character sheet data is in PPMA, if not, disable module until HUD reset
        // have to do this here, since state command cannot be called from global functions
        // See http://lslwiki.net/lslwiki/wakka.php?wakka=FunctionStateChangeHack
        // See also "Caveats" in http://wiki.secondlife.com/wiki/State        
        list requirements = [ "RESILIENCE","Wounds","RESILIENCE","CurrentWounds","RESILIENCE","CriticalWounds","RESILIENCE","CurrentCriticalWounds","RESILIENCE", "Resolve", "RESILIENCE", "CurrentResolve", "MYRIAD", "MINRESILIENCE", "MYRIAD", "MAXRESILIENCE", "MYRIAD","MINARMOR","MYRIAD","MAXARMOR", "MYRIAD", "MINDAMAGE", "MYRIAD", "MAXDAMAGE", "FLAG", "INCAPACITATED", "FLAG", "DEAD" ];
        integer i;
        for ( i = 0; i < llGetListLength(requirements); i += 2 ) {
            string category = llList2String(requirements, i);
            string aspect = llList2String(requirements, i + 1);
            if ( GET_VAL(category,aspect) == KEY_NOT_FOUND ) {
                OWNERSAY("Required "+llToLower(category)+" \""+aspect+"\" not found. Resilience module will not work correctly. Disabling...");
                state disabled;
            }
        }
 
        SETUP(); // show credits and start character sheet load
    }
 
    // TIMER - scheduled events
    timer() {
        // Respawn timer ended
        if ( GET_FLAG("DEAD") == TRUE ) { // if dead
            RPEVENT("respawns!");
            HEAL(20); // heal 20 points of damage to respawn
            //FIXME if ( RESPAWNPOINT != ZERO_VECTOR ) osTeleportAgent(llGetKey(),RESPAWNPOINT);
            if ( RESPAWNPOINT != ZERO_VECTOR ) llTeleportAgentHome(llGetOwner());
        }
        if ( GET_FLAG("INCAPACITATED") == TRUE ) { // if hurt
            HEAL(1); // heal 1 wound
        }
        integer curwounds = GET_CURRENTWOUNDS();
        integer maxwounds = GET_WOUNDS();
        integer curresolve = GET_CURRENTRESOLVE();
        integer maxresolve = GET_RESOLVE();
        if ( curresolve < maxresolve ) {
            RECOVER(1); // heal 1 resolve
        }
        if ( curwounds == maxwounds && curresolve == maxresolve ) { // fully healed?
            llSetTimerEvent(0.0); // stop timer
        }
    }
} // end state running
 
state disabled {
    link_message(integer sender_num,integer sender,string message,key id) {
        if ( sender == MODULE_RESILIENCE || sender == LM_SENDTOATTACHMENT ) return; // ignore our own messages
 
        list fields = llParseString2List(message,["|"],[]); // break line of text into = delimited fields
        string command = llToLower(llStringTrim(llList2String(fields,0),STRING_TRIM)); // field zero is the "command"
 
        if ( command == "debug" || command == "error" || command == "help" || command == "ownersay" || command == "rpevent" ) return; // ignore WELL commands
 
        if ( command == "reset" ) { RESET(); return; }    }
}
// END