Myriad Lite Module Skill Close Combat

// Myriad_Lite_Module_Skill_Close_Combat-v0.0.3-20131026.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 RPEVENTS TO MODULE
// FIXME ADD HELP EVENTS TO MODULE
 
// CONSTANTS - DO NOT CHANGE DURING RUN
string BASENAME = "Myriad Lite Module Skill Close"; // base name for the script
string VERSION = "0.0.3"; // Allen Kerensky's script version
string VERSIONDATE = "20131026"; // Allen Kerensky's script yyyymmdd
 
// Module to Module Messaging Constants
integer MODULE_CLOSE = -6;
integer LM_SENDTOATTACHMENT = 0x80000000;
 
// CONFIG items
float ARM_LENGTH; // arm is 1m long PPMA CONFIG,ARMLENGTH,<value> default 1.0 (meters)
float LEG_LENGTH; // leg is 1.5m long PPMA CONFIG,LEGLENGTH,<value> default 1.5 (meters)
integer MELEEATTACKDICE; // 1 attack dice for fists and feet PPMA CONFIG,MELEEATTACKDICE,<value> default 1
string ANIM_PUNCH_LEFT; // anim for left punch PPMA CONFIG,ANIMPUNCHLEFT,<value> default punch_l
string ANIM_PUNCH_RIGHT; // anim for right punch PPMA CONFIG,ANIMPUNCHRIGHT,<value> default punch_r
string ANIM_PUNCH_ONETWO; // anim for 1-2 punch PPMA CONFIG,ANIMPUNCHONETWO,<value> default punch_onetwo
string ANIM_KICK; // anim for kick PPMA CONFIG,ANIMKICK,<value> default kick_roundhouse_r
integer SINGLE_PUNCH_DELAY; // time between a punch TODO fix to Myriad rules times PPMA CONFIG,SINGLEPUNCHDELAY,<value> default 1
integer DOUBLE_PUNCH_DELAY; // time between one-two punch TODO fix to Myriad rules times PPMA CONFIG,DOUBLEPUNCHDELAY,<value> default 2
integer KICK_DELAY; // recovery time between kicks TODO fix to Myriad rules times PPMA CONFIG,KICKDELAY,<value> default 3
float FIELD_OF_ATTACK; // controls field of attack. PI/6 = 60 degree field of attack PPPMA CONFIG,FIELDOFATTACK,<value> default PI
 
// RUNTIME GLOBALS - CAN CHANGE DURING RUN
integer TIME_NEXT_ATTACK; // time of last attack
integer CONTROLS; // bitfield of controls to monitor
float WEAPON_LENGTH; // weapon length in last attack
 
//
// PPMA GET_VAL
//
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
// DEBUG - show debug messages or not
// 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);
}
 
//
// PPMA GET_CONFIG
//
string GET_CONFIG(string config) {
    config = llToUpper(config);
    string out = GET_VAL("CONFIG",config);
    if ( out == KEY_NOT_FOUND ) {
        if ( config == "ARMLENGTH" ) out = "1.0";
        if ( config == "LEGLENGTH" ) out = "1.5";
        if ( config == "MELEEATTACKDICE" ) out = "1";
        if ( config == "ANIMPUNCHLEFT" ) out = "punch_l";
        if ( config == "ANIMPUNCHRIGHT" ) out = "punch_r";
        if ( config == "ANIMPUNCHONETWO" ) out = "punch_onetwo";
        if ( config == "ANIMKICK" ) out = "kick_roundhouse_r";
        if ( config == "SINGLEPUNCHDELAY" ) out = "1";
        if ( config == "DOUBLEPUNCHDELAY" ) out = "2";
        if ( config == "KICKDELAY" ) out = "3";
        if ( config == "FIELDOFATTACK") out = (string)PI;
        SET_VAL("CONFIG",config,out);
    }
    return out;
}
 
//
// 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_SKILL
// Requires a SKILL NAME
// Returns the rank value for that skill, or zero if player doesn't have skill
//
integer GET_SKILL(string askill) {
    string val = GET_VAL("SKILL",askill);
    if ( val == KEY_NOT_FOUND ) {
        ERROR("Unable to locate skill "+askill+" returning 0");
        return 0;
    }
    integer retval = (integer)val;
    if ( retval < GET_MYRIAD("MINSKILL") || retval > GET_MYRIAD("MAXSKILL") ) {
        ERROR("Skill "+askill+" value "+val+" out of range "+(string)GET_MYRIAD("MINSKILL")+"-"+(string)GET_MYRIAD("MAXSKILL"));
        return GET_MYRIAD("MINSKILL");
    }
    return retval;
}
 
//
// GET_STAT
// Requires a STAT NAME
// Returns the rank value for that statistics, or zero if player doesn't have stat
//
integer GET_STATISTIC(string astat) {
    string val = GET_VAL("STATISTIC",astat);
    if ( val == KEY_NOT_FOUND ) {
        ERROR("Unabled to locate statistic "+astat+" returning 0");
        return 0;
    }
    integer retval = (integer)val;
    if ( retval < GET_MYRIAD("MINSTAT") || retval > GET_MYRIAD("MAXSTAT") ) {
        ERROR("Statistic "+astat+" value "+val+" out of range "+(string)GET_MYRIAD("MINSTAT")+"-"+(string)GET_MYRIAD("MAXSTAT"));
        return GET_MYRIAD("MINSTAT");
    }
    return retval;
}
 
// 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(6.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
}
 
// COMBATOFF - turn off fist fighter
COMBATOFF() {
    SET_FLAG("FISTS",FALSE); // disable flag to exit/ignore any more control events
    if ( GET_FLAG("CONTROLS") == TRUE ) { // do we have control permission?
        llReleaseControls(); // release the controls
        SET_FLAG("CONTROLS",FALSE); // remember that we released controls
    }
    OWNERSAY("Close Combat Deactivated");
}
 
// COMBATON - turn on fist fighter
COMBATON() {
    SET_FLAG("FISTS",TRUE); // yep, using fist fighter, for control events
    if ( GET_FLAG("CONTROLS") == FALSE ) { // do we have permission to read controls? No? we need it.
        llReleaseControls(); // release any previous controls on avatar
        llRequestPermissions(llGetOwner(),PERMISSION_TAKE_CONTROLS|PERMISSION_TRIGGER_ANIMATION); // request permissions needed for fist fighter
    }
    OWNERSAY("Close Combat Activated");
}
 
// DEBUG - show debug chat with wearer name for sorting
DEBUG(string dmessage) {
    if ( GET_FLAG("DEBUG") == TRUE ) llMessageLinked(LINK_THIS,MODULE_CLOSE,"DEBUG|"+dmessage,llGetOwner());
}
 
// ERROR - show errors on debug channel with wearer name for sorting
ERROR(string emessage) {
    llMessageLinked(LINK_THIS,MODULE_CLOSE,"ERROR|"+emessage,llGetOwner());
}
 
// FISTSOFF
FISTSOFF() {
    SET_FLAG("FISTS",FALSE); // turn off fist fighter
    if ( GET_FLAG("CONTROLS") == TRUE ) { // if we own controls...
        llReleaseControls(); // release them
        SET_FLAG("CONTROLS",FALSE); // and remember
    }
}
 
// FISTSON
FISTSON() {
    SET_FLAG("FISTS",TRUE); // turn fist fighter back on
    if ( GET_FLAG("CONTROLS") == FALSE ) { // if we don't have controls
        llReleaseControls(); // release them just in case
        llRequestPermissions(llGetOwner(),PERMISSION_TAKE_CONTROLS|PERMISSION_TRIGGER_ANIMATION); // then ask to take controls
    }
}
 
// MEMORY
GET_MEMORY() {
    OWNERSAY(BASENAME+" free memory: "+(string)llGetFreeMemory());
}
 
// GETVERSION
GET_VERSION() {
    OWNERSAY(BASENAME+" v"+VERSION+"-"+VERSIONDATE);
}
 
// HAND_TO_HAND attack for fist fighter
// TODO fix timing to Myriad rules
HAND_TO_HAND(integer delay,string anim,float reach) {
    // TODO need "someone moves to attack" RP event messages here?
    TIME_NEXT_ATTACK = llGetUnixTime() + delay; // attack again after delay for attack and followup recovery
    llStartAnimation(anim); // run the punch left animation
    WEAPON_LENGTH = reach; // save the weapon reach from the last attack
    llSensor("",NULL_KEY,(AGENT|ACTIVE|PASSIVE),reach,FIELD_OF_ATTACK); // sensor sweep to see if we hit someone
}
 
//
// OWNERSAY
//
OWNERSAY(string msg) {
    llMessageLinked(LINK_THIS,MODULE_CLOSE,"OWNERSAY|"+msg,llGetOwner());    
}
 
// 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 OPPOSED_TEST(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
}
 
 
// RESET - shut down running animations then reset the script to reload character sheet
RESET() {
    // stop all running animations
    if ( GET_FLAG("ANIMATE") == TRUE ) { // do we have permission to animate?
        list anims = llGetAnimationList(llGetOwner()); // get list of current animations for owner
        integer animcount = llGetListLength(anims); // count the number of animations in the list
        while (animcount--) { // step from end of animation list to beginning
            llStopAnimation(llList2String(anims,animcount)); // stopping each animation
        }
    }
    llResetScript(); // now reset
}
 
// RPEVENT
RPEVENT(string rpevent) {
    llMessageLinked(LINK_THIS,MODULE_CLOSE,"RPEVENT|"+rpevent,llGetOwner());
}
 
// SETUP - begin bringing the HUD online
SETUP() {
 
    // setup bitfield of controls we're going to monitor in fist fighter mode
    CONTROLS = CONTROL_ML_LBUTTON | CONTROL_LBUTTON | CONTROL_FWD | CONTROL_BACK | CONTROL_ROT_LEFT | CONTROL_LEFT | CONTROL_RIGHT | CONTROL_ROT_RIGHT | CONTROL_UP | CONTROL_DOWN;
    FIELD_OF_ATTACK = PI/6; // set fist fighter field of attack to +/- 30 degree cone from direction avatar faces - PERSONAL CHOICE NOT IN MYRIAD RULES
    llRequestPermissions(llGetOwner(),PERMISSION_TRIGGER_ANIMATION|PERMISSION_TAKE_CONTROLS);
 
    ARM_LENGTH = (float)GET_CONFIG("ARMLENGTH");
    LEG_LENGTH = (float)GET_CONFIG("LEGLENGTH");
    MELEEATTACKDICE = (integer)GET_CONFIG("MELEEATTACKDICE");
    ANIM_PUNCH_LEFT = GET_CONFIG("ANIMPUNCHLEFT");
    ANIM_PUNCH_RIGHT = GET_CONFIG("ANIMPUNCHRIGHT");
    ANIM_PUNCH_ONETWO = GET_CONFIG("ANIMPUNCHONETWO");
    ANIM_KICK = GET_CONFIG("ANIMKICK");
    SINGLE_PUNCH_DELAY = (integer)GET_CONFIG("SINGLEPUNCHDELAY");
    DOUBLE_PUNCH_DELAY = (integer)GET_CONFIG("DOUBLEPUNCHDELAY");
    KICK_DELAY = (integer)GET_CONFIG("KICKDELAY");
    FIELD_OF_ATTACK = (float)GET_CONFIG("FIELDOFATTACK");
    OWNERSAY("Close Combat module active.");
}
 
// DEFAULT STATE - load character sheet
default {
 
    // CHANGED - triggered for many changes to the avatar
    changed(integer changes) {
        if ( changes & CHANGED_REGION || changes & CHANGED_TELEPORT ) {
            llRequestPermissions(llGetOwner(),PERMISSION_TRIGGER_ANIMATION|PERMISSION_TAKE_CONTROLS);
        }
    }
 
    // CONTROL - read arrow keys and mouse button in first or third person mode
    control(key id,integer level,integer edge) {
        id = NULL_KEY; // LSLINT
        if ( GET_FLAG("DEAD") == TRUE || GET_FLAG("INCAPACITATED") == TRUE ) return; // dead or incapacitated can't fight
        if ( GET_FLAG("FISTS") == FALSE ) return; // not using fist fighter
        if ( GET_FLAG("ANIMATE") == FALSE ) return; // can't show animations
        if ( llGetUnixTime() <= TIME_NEXT_ATTACK ) return; // too soon since last attack
 
        // Is the mouse button held down?
        if ( ( level & CONTROL_LBUTTON ) || ( level & CONTROL_ML_LBUTTON ) ) {
            // Mouse + Left Arrow = left-handed punch
            if ( ( edge & CONTROL_LEFT ) || ( edge & CONTROL_ROT_LEFT ) ) {
                // TODO fix timing to Myriad rules
                HAND_TO_HAND(SINGLE_PUNCH_DELAY,ANIM_PUNCH_LEFT,ARM_LENGTH); // left punch with 1m reach, 1 second recover
                return;
            }
            // Mouse + Rigth Arrow = right-handed punch
            if ( ( edge & CONTROL_RIGHT ) || ( edge & CONTROL_ROT_RIGHT ) ) {
                // TODO fix timing to Myriad rules
                HAND_TO_HAND(SINGLE_PUNCH_DELAY,ANIM_PUNCH_RIGHT,ARM_LENGTH); // right punch, 1m reach, 1 second recover
                return;
            }
            if ( ( edge & CONTROL_UP ) || ( edge & CONTROL_FWD ) ) {
                // TODO fix timing to Myriad rules
                HAND_TO_HAND(DOUBLE_PUNCH_DELAY,ANIM_PUNCH_ONETWO,ARM_LENGTH); // left-right combo, 1m reach, 2 second recover
                return;
            }
            if ( ( edge & CONTROL_DOWN ) || ( edge & CONTROL_BACK ) ) {
                // TODO fix timing to Myriad rules
                HAND_TO_HAND(KICK_DELAY,ANIM_KICK,LEG_LENGTH); // kick, 1.5m reach, 3 second recover
                return;
            }
        } // end if mouse button held down
    } // end of control event
 
    // LINK MESSAGE - commands to and from other prims in HUD
    link_message(integer sender,integer sending_module,string str, key id) {
        if ( sending_module == MODULE_CLOSE || sending_module == LM_SENDTOATTACHMENT ) return; // ignore our own messages
 
        list fields = llParseString2List(str,["|"],[]); // 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 message
 
        // break down the rest of the message
        //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
 
 
        // only debug after we've ignore stuff for other modules
        DEBUG("EVENT: link_message("+(string)sender+","+(string)sending_module+","+str+","+(string)id+")");
 
        if ( command == "memory" ) { GET_MEMORY(); return; }
        if ( command == "reset" ) { RESET(); return; }
        if ( command == "version" ) { GET_VERSION(); return; }
 
        if ( command == "combatoff" ) { COMBATOFF(); return; }
        if ( command == "combaton" ) { COMBATON(); return; }
        if ( command == "attachmelee" || command == "attachranged" ) { FISTSOFF(); return;}
        if ( command == "detachmelee" || command == "detachranged" ) { FISTSON(); return; }
 
        // we've been hit and have to make an opposed ability test to avoid it
        if ( command == "closehit" ) { // mortal combat attack message?
            integer attackstat = llList2Integer(fields,1); // get attackers stat
            integer attackskill = llList2Integer(fields,2); // get attackers skill
            integer attackdice = llList2Integer(fields,3); // get attacker object's attack dice
            key owner = llList2Key(fields,4); // get attacker object's key
            string item = llList2String(fields,5); // get attacker object name
            if ( attackstat < GET_MYRIAD("MINSTAT") || attackstat > GET_MYRIAD("MAXSTAT") ) { // is the attack stat value out of allowed range?
                ERROR("Attack stat value "+(string)attackstat+" out of range: "+(string)GET_MYRIAD("MINSTAT")+"-"+(string)GET_MYRIAD("MAXSTAT"));
                // TODO make a tattletale RP event?
                return;
            }
            if ( attackskill < GET_MYRIAD("MINSKILL") || attackstat > GET_MYRIAD("MAXSKILL") ) { // is the attack skill value out of allowed range?
                ERROR("Attack skill value "+(string)attackskill+" out of range: "+(string)GET_MYRIAD("MINSKILL")+"-"+(string)GET_MYRIAD("MAXSKILL"));
                // TODO make a tattletale RP event?
                return;
            }
            if ( attackdice < GET_MYRIAD("MINDAMAGE") || attackdice > GET_MYRIAD("MAXDAMAGE") ) { // is the attacking weapon's attack dice value out of allowed range?
                ERROR("Attack dice value out of range: "+(string)GET_MYRIAD("MINDAMAGE")+"-"+(string)GET_MYRIAD("MAXDAMAGE"));
                // TODO make a tattletale RP event?
                return;
            }
            integer skillamount = 0; // create a place to hold the defenders mortal combat skill rank
            skillamount = GET_SKILL("Close Combat"); // get close combat skill rank
            // see if we're hit
            integer amihit = OPPOSED_TEST(attackstat,attackskill,GET_STATISTIC("Grace"),skillamount); // attacker power+skill vs. defender grace+skill
            if ( amihit == TRUE ) { // we're hit!
                string ownername = llList2String(llParseString2List(llKey2Name(owner),["@"],[]),0); // strip @where from HG names
                OWNERSAY("You been hit in close combat by "+ownername+"'s "+item+"!");
                llMessageLinked(LINK_THIS,MODULE_CLOSE,"HIT|"+(string)attackdice,owner); // Send HIT to the Message Bus
            }
            return;
        }
        // Actions NOT Allowed When Dead/Incapacitated go below here
        if ( GET_FLAG("DEAD") == TRUE || GET_FLAG("INCAPACITATED") == TRUE ) return;
        // If Your Bullet has hit, fire a hitcheck regionwide at targetplayer's channel
        if ( command == "closecombat" ) {
            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
            integer attskill = 0; // zero our attack skill
            attskill = GET_SKILL("Close Combat"); // get close combat skill level
            llRegionSay(victimchan,"CLOSEHIT"+"|"+(string)GET_STATISTIC("Power")+"|"+(string)attskill+"|"+(string)attdice+"|"+bywho+"|"+bywhat); // attack!
            return;
        } // end if CLOSECOMBAT/RANGEDCOMBAT/TOHID
    }
 
    // NO_SENSOR - this is called when the attack sensor detects nothing in range and field of attack
    no_sensor() {
        // here to fix rare bugs where sensor fails unles no_sensor is in state too
    }
 
    // RUN_TIME_PERMISSIONS
    run_time_permissions(integer perm) {
        if ( perm & PERMISSION_TAKE_CONTROLS ) { // was script granted permission to take avatar controls?
            llTakeControls(CONTROLS,TRUE,TRUE); // then take them, but still pass them to other scripts like vehicles
            SET_FLAG("CONTROLS",TRUE); // remember that we got permission for this
        }
        if ( perm & PERMISSION_TRIGGER_ANIMATION ) { // we script granted permission to trigger animations on the avatar?
            SET_FLAG("ANIMATE",TRUE); // remember that we got permission for this
        }
    }
 
    // SENSOR for who was in attack range and field of attack
    sensor(integer num_detected) {
        while(num_detected--) { // count down all results in range and field of attack
            key hitwho = llDetectedKey(num_detected); // key of who or what we hit
            string name = llDetectedName(num_detected); // name of who we hit
            integer attskill = GET_SKILL("Close Combat"); // get our close combat skill rank
            integer victimchan = (integer)("0x"+llGetSubString(hitwho,0,6)); // calculate dynamic channel of who we hit
            RPEVENT("strikes at "+name+" in Close Combat!");
            // tell victim HUD to perform a CLOSE COMBAT opposed ability test
            // attacker Power stat/Close Combat skill rank vs. Defender Grace stat/Close Combat skill rank
            // See Myriad PDF pp. 21-22 and Myriad Special Edition pp.27-28
            llRegionSay(victimchan,"CLOSEHIT"+"|"+(string)GET_STATISTIC("Power")+"|"+(string)attskill+"|"+(string)MELEEATTACKDICE+"|"+(string)llGetOwner()+"|"+"fists and feet");
            OWNERSAY("You struck at "+name+" in Close Combat");
        } // end while
    } // end sensor
 
    // STATE ENTRY
    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 = [ "STATISTIC", "Power", "STATISTIC", "Grace", "SKILL", "Close Combat", "RESILIENCE", "Wounds", "RESILIENCE", "CurrentWounds", "RESILIENCE", "CriticalWounds", "RESILIENCE", "CurrentCriticalWounds", "MYRIAD", "MINSTAT", "MYRIAD", "MAXSTAT", "MYRIAD", "MINSKILL", "MYRIAD", "MAXSKILL", "MYRIAD", "MINRESILIENCE", "MYRIAD", "MAXRESILIENCE", "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. Close Combat module will not work correctly. Disabling...");
                state disabled;
            }
        }
 
        SETUP();
    }
 
} // end state
 
state disabled {
    link_message(integer sender_num,integer sender,string message,key id) {
        if ( sender == MODULE_CLOSE || 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