Myriad Lite Module Social Combat

// Myriad_Lite_Module_Social_Combat-v0.0.0-20130905.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 PPMA
// FIXME FLAG INCAP DEAD
// FIXME MORE RPVENT
// FIXME MORE HELP
 
// CONSTANTS - DO NOT CHANGE DURING RUN
string BASENAME = "Myriad Lite Module Social Combat";
string VERSION = "0.0.0"; // script version
string VERSIONDATE = "20130905"; // script yyyymmdd
 
// Module to Module Messaging Constants
integer MODULE_SOCIAL = -14;
integer LM_SENDTOATTACHMENT = 0x80000000;
integer RENDEZVOUS1; // FIXME Rendezvous1 chat sent to ALL Myriad players in region
integer RENDEZVOUS2; // new Myriad Preview 7 regionwide dynamic channel
 
// CONFIGURATION ITEMS
string SOCIALATTACKDICE; // how many "dice" of damage does the social attack cause? FIXME CONFIG,SOCIALATTACKDICE,<number 1-5>
integer ISACTIVE; // is module active or not FIXME FLAG,SOCIALCOMBATACTIVE,<true|false>
 
// RUNTIME GLOBALS - CAN CHANGE DURING RUN
string SOCIALATTACK; // which social attack skill is used, Deceit or Persuasion?
 
 
//
// 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);
//}
 
//
// 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;
}
 
// 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
        //
        // FIXME if 6 = burn six for SFX OR +1 Bonus Myriad p18 MyriadSE p24
        //
        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
}
 
// DEBUG - show debug chat with wearer name for sorting
DEBUG(string dmessage) {
    if ( GET_FLAG("DEBUG") == TRUE ) llMessageLinked(LINK_THIS,MODULE_SOCIAL,"DEBUG|"+dmessage,llGetOwner());
}
 
// DECEIT SOCIAL COMBAT SKILL
DECEIVE(string hitwho) {
    integer victimchan = (integer)("0x"+llGetSubString(hitwho,0,6)); // calculate victim's dynamic channel
    string attstat = (string)GET_STATISTIC("Intellect"); // FIXME PPMA get Attacker's Social Combat Attack Stat
    string attskill = (string)GET_SKILL("Deceit"); // FIXME PPMA get Attacker's Social Combat Attack Skill rank
    llRegionSay(victimchan,"DECEITATTACK"+"|"+attstat+"|"+attskill+"|"+SOCIALATTACKDICE+"|"+(string)llGetOwner()+"|"+"Deceit"); // make the attack!
    //SOCIALATTACK=""; // cleanup from the event    
}
 
// ERROR - show errors on debug channel with wearer name for sorting
ERROR(string emessage) {
    llMessageLinked(LINK_THIS,MODULE_SOCIAL,"ERROR|"+emessage,llGetOwner());    
}
 
//
// GET_RESILIENCE
// Requires a RESILIENCE NAME
// Returns the amount of that resilience, or minimum if the player does not currently have that reslience
//
integer GET_RESILIENCE(string aresilience) {
    string val = GET_VAL("RESILIENCE",aresilience);
    if ( val == KEY_NOT_FOUND ) {
        ERROR("Unable to locate resilience "+aresilience+" returning "+(string)GET_MYRIAD("MINRESILIENCE"));
        return GET_MYRIAD("MINRESILIENCE");
    }
    integer retval = (integer)val;
    if ( retval < GET_MYRIAD("MINRESILIENCE") || retval > GET_MYRIAD("MAXRESILIENCE") ) {
        ERROR("Resilience "+aresilience+" value "+val+" out of range "+(string)GET_MYRIAD("MINRESILIENCE")+"-"+(string)GET_MYRIAD("MAXRESILIENCE"));
        return GET_MYRIAD("MINRESILIENCE");
    }
    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;
}
 
// MEMORY
GET_MEMORY() {
    OWNERSAY(BASENAME+" free memory: "+(string)llGetFreeMemory()); // show this module's free memory info
}
 
// GETVERSION
GET_VERSION() {
    OWNERSAY(BASENAME+" v"+VERSION+"-"+VERSIONDATE); // show this module's version info
}
 
// 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 or equal to defender = attacker wins, ties go to attacker!
    return FALSE; // defender wins
}
 
// OWNERSAY
OWNERSAY(string str) {
    llMessageLinked(LINK_THIS,MODULE_SOCIAL,"OWNERSAY|"+str,llGetOwner());
}
 
// PERSUASION SOCIAL COMBAT SKILL
PERSUADE(string hitwho) {
    integer victimchan = (integer)("0x"+llGetSubString(hitwho,0,6)); // calculate victim's dynamic channel
    string attstat = (string)GET_STATISTIC("Intellect"); // FIXME PPMA get Attacker's Social Combat Attack Stat
    string attskill = (string)GET_SKILL("Persuasion"); // FIXME PPMA get Attacker's Social Combat Attack Skill rank
    llRegionSay(victimchan,"PERSUASIONATTACK"+"|"+attstat+"|"+attskill+"|"+SOCIALATTACKDICE+"|"+(string)llGetOwner()+"|"+"Persuasion"); // make the attack!
    //SOCIALATTACK=""; // cleanup from the event
}
 
// RESET - shut down running animations then reset the script to reload character sheet
RESET() {
    llResetScript(); // now reset
}
 
// RPEVENT
RPEVENT(string rpevent) {
    llMessageLinked(LINK_THIS,MODULE_SOCIAL,"RPEVENT|"+rpevent,llGetOwner());
}
 
// SETUP - begin bringing the HUD online
SETUP() {
 
    SOCIALATTACKDICE = "1"; // just do one damage dice
 
    RENDEZVOUS1 = -999; // FIXME Rendezvous1 chat sent to ALL Myriad players in region    
    list details = llGetParcelDetails(<0,0,0>,[PARCEL_DETAILS_ID]);
    string parcelid = llList2String(details,0);
    RENDEZVOUS2 = (integer)("0x"+llGetSubString(parcelid,0,7));    
 
    // FIXME - MODULE RESILIENCE SOCIAL ARMOR CLASS 1-5?
    // FIXME - MODULE RESILIENCE SOCIAL DAMAGE CLASS 1-5?
    // FIXME - MODULE RESILIENCE SOCIAL HEAL PARTIAL/HEAL ALL?
 
    SOCIALON(); // enable social combat by default
}
 
// SOCIALOFF - turn off social combat
SOCIALOFF() {
    ISACTIVE = FALSE;
    OWNERSAY("Social Combat module inactive.");
}
 
// SOCIALON - turn on social combat
SOCIALON() {
    ISACTIVE = TRUE;
    OWNERSAY("Social Combat module active.");    
}
 
// DEFAULT STATE
// FIXME - PERFORM SETUP AND REQUIRED SYSTEM SUPPORT HERE - MOVE TO RUNNING STATE WHEN COMPLETE?
 
default {
 
    link_message(integer sender_num,integer sender,string message,key id) {
        if ( sender == MODULE_SOCIAL || 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
 
        // only debug after ignored commands
        DEBUG("EVENT: link_message("+(string)sender_num+","+(string)sender+","+message+","+(string)id+")");
 
        // 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; }
        if ( command == "reset" ) { RESET(); return; }
        if ( command == "version" ) { GET_VERSION(); return; }
 
        if ( command == "socialoff" ) { SOCIALOFF(); return; }
        if ( command == "socialon" ) { SOCIALON(); return; }
 
        if ( ISACTIVE == FALSE ) return; // if module is disabled, no need to go further
 
        //
        // Defend against an incoming Social Attack
        //
        if ( command == "deceitattack" || command == "persuasionattack" ) { // incoming social combat attack message?
            integer attackstat = llList2Integer(fields,1); // get attackers social combat attack stat (INTELLECT)
            integer attackskill = llList2Integer(fields,2); // get attackers social combat attack skill (PERSUASION)
            integer attackdice = llList2Integer(fields,3); // get attacker's social combat attack damage dice (1 or more with SFX bonuses)
            key owner = llList2Key(fields,4); // get attacker's/object's key 
            // string item = llList2String(fields,5); // get attacker's/object's name
            // Is the attacker's attack stat value out of range?
            if ( attackstat < GET_MYRIAD("MINSTAT") || attackstat > GET_MYRIAD("MAXSTAT") ) {
                ERROR("Social Attack stat value "+(string)attackstat+" out of range: "+(string)GET_MYRIAD("MINSTAT")+"-"+(string)GET_MYRIAD("MAXSTAT"));
                RPEVENT("ignored an incoming social attack with attack stat value out of range!");
                return;
            }
            // is the attack social combat attack skill value out of allowed range?    
            if ( attackskill < GET_MYRIAD("MINSKILL") || attackstat > GET_MYRIAD("MAXSKILL") ) {
                ERROR("Social Attack skill value "+(string)attackskill+" out of range: "+(string)GET_MYRIAD("MINSKILL")+"-"+(string)GET_MYRIAD("MAXSKILL"));
                RPEVENT("ignored an incoming social attack with attack skill value out of range!");
                return;
            }
            // is the attacking weapon's attack dice value out of allowed range?
            if ( attackdice < GET_MYRIAD("MINDAMAGE") || attackdice > GET_MYRIAD("MAXDAMAGE") ) {
                ERROR("Social Attack dice value out of range: "+(string)GET_MYRIAD("MINDAMAGE")+"-"+(string)GET_MYRIAD("MAXDAMAGE"));
                RPEVENT("ignored an incoming social attack with attack damage value out of range!");
                return;
            }
 
            //
            // DEFEND AGAINST INCOMING ATTACK
            //
            if ( GET_RESILIENCE("CurrentResolve") <= 0 ) { // no resolve, you can't attack or defend social combat
                return;
            }
            integer defendstat = 0; // hold the defenders social combat stat amount
            integer defendskill = 0; // create a place to hold the defenders mortal combat skill rank
            defendstat = GET_STATISTIC("Spirit"); // get defenders social combat defense stat amount FIXME PPMA
            if ( command == "deceitattack" ) // FIXME
                defendskill = GET_SKILL("Deceit"); // get defenders social combat defense skill rank FIXME PPMA
            if ( command == "persuasionattack" ) // FIXME
                defendskill = GET_SKILL("Persuasion"); // get defenders social combat defense skill rank FIXME PPMA
            //
            // see if we're hit - Opposed Ability Test between Attacker Intellect+Persuasion vs. Defender Spirit+Persuasion
            // FIXME - add a variety of results based on Degree of Success through Degree of Failure
            // need a sliding scale of result messages based on how successful the attack or defense is
            // use a sliding scale of success, and multiple different messages to the user
            // "X is very persuasive"
            // "What X says makes sense"
            // "While you have misgivings, X's argument doesn't have any obvious holes in it"
            // "X is quite convincing"
            // enable SFX like THE PERFECT LIE here too
 
            integer amihit = OPPOSED_TEST(attackstat,attackskill,defendstat,defendskill);
            if ( amihit == TRUE ) { // we're hit!
 
                //if ( command == "deceitattack" ) {
                //    OWNERSAY("Your RESOLVE may weaken under "+llKey2Name(owner)+"'s DECEIT!");
                //    RPEVENT("may lose RESOLVE under "+llKey2Name(owner)+"'s DECEIT!");
                //}
                //if ( command == "persuasionattack" ) {
                //    OWNERSAY("Your RESOLVE may weaken by "+llKey2Name(owner)+"'s PERSUASION!");
                //    RPEVENT("may lose RESOLVE from "+llKey2Name(owner)+"'s PERSUASION!");
                //}
 
                // Show a generic "HIT" message to disguise if it was DECEIT or PERSUASION
                string ownername = llList2String(llParseString2List(llKey2Name(owner),["@"],[]),0); // strip @where from HG names
                OWNERSAY("You may be swayed by "+ownername);
                RPEVENT("may be swayed by "+ownername);
                llMessageLinked(LINK_THIS,MODULE_SOCIAL,"RESOLVEHIT|"+(string)attackdice,owner); // apply the hit
            } else {
 
                //if ( command == "deceitattack" ) {
                //    OWNERSAY(llKey2Name(owner)+" attempts unsuccessfully to DECEIVE you!");
                //    RPEVENT("is not DECEIVED by "+llKey2Name(owner));
                //}
                //if ( command == "persuasionattack" ) {
                //    OWNERSAY(llKey2Name(owner)+" unsuccessfully attempts to PERSUADE you!");
                //    RPEVENT("is not PERSUADED by "+llKey2Name(owner));
                //}
 
                // Show a generic "HIT" message to disguise if it was DECEIT or PERSUASION
                string ownername = llList2String(llParseString2List(llKey2Name(llGetOwner()),["@"],[]),0); // strip @where from HG names
                OWNERSAY(ownername+" attempts unsuccessfully to sway you!");
                RPEVENT("is not swayed by "+ownername);
            }
            return;
        }
 
        //
        // Actions NOT Allowed When Dead/Incapacitated go below here
        //
        if ( GET_FLAG("DEAD") == TRUE || GET_FLAG("INCAPACITATED") == TRUE ) return;
 
        if ( GET_RESILIENCE("CurrentResolve") <= 0  ) { // cannot make social attack without some resolve
            OWNERSAY("You are unable to make a social attack without rallying some Resolve.");
            return;
        }        
        //
        // Make a social "attack" check regionwide at targetplayer's channel
        //
        if ( command == "deceive" ) {
            SOCIALATTACK="DECEIT";
            llSensor("",NULL_KEY,AGENT_BY_LEGACY_NAME,20,PI); // FIXME how to socially attack NPC?
            return;
        }
        if ( command == "persuade" ) {
            SOCIALATTACK="PERSUASION";
            llSensor("",NULL_KEY,AGENT_BY_LEGACY_NAME,20,PI); // FIXME how to socially attack NPC?
            return;
        }
    } // end of link_message event
 
    // NO_SENSOR - social combat
    no_sensor() {
        if ( SOCIALATTACK == "DECEIT" ) {
            OWNERSAY("No one around to deceive...");
        }
        if ( SOCIALATTACK == "PERSUASION" ) {
            OWNERSAY("No one around to persuade...");
        }
        SOCIALATTACK=""; // clean up
    }
 
    sensor(integer num_detected) {
        //list who;
        //integer count;
        //for ( count = 0; count < num_detected; count++) {
        //    who = who + llDetectedName(count);
        //    SENSED = SENSED + llDetectedKey(count);
        //}
        //llDialog(llGetOwner(),"Socially attack who?",[who],CHANPLAYER);
        if ( SOCIALATTACK == "" ) return; // not a valid attack
        while(num_detected--) {
            key who = llDetectedKey(num_detected);
            // FIXME - if in mouselook and cursor pointing at 1 avatar, make TARGETED SOCIAL ATTACK else make AREA SOCIAL ATTACK
            // FIXME if in mouselook, focus the attack on that one person, with a bonus Gotta look them in the eye when you're lying LOL
            if ( SOCIALATTACK == "DECEIT" ) DECEIVE((string)who);
            //llMessageLinked(LINK_THIS,MODULE_HUD,"DECEIVE|1|"+(string)who+"|"+(string)llGetOwner(),who);
            if ( SOCIALATTACK == "PERSUASION" ) PERSUADE((string)who);
            //llMessageLinked(LINK_THIS,MODULE_HUD,"PERSUADE|1|"+(string)who+"|"+(string)llGetOwner(),who);
        }
        SOCIALATTACK="";
    }
 
    // 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 = [ "STATISTIC","Intellect", "STATISTIC", "Spirit", "SKILL", "Deceit", "SKILL", "Persuasion", "RESILIENCE", "Resolve", "RESILIENCE", "CurrentResolve", "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. Social Combat module will not work correctly. Disabling...");
                state disabled;
            }
        }
 
        // FIXME - OPTIONAL EFFECT: AURA OF TERROR
        // FIXME - OPTIONAL EFFECT: CHANGE THE SUBJECT
        // FIXME - OPTIONAL EFFECT: HUSKY TONES
        // FIXME - OPTIONAL EFFECT: THE PERFECT LIE
        // FIXME - OPTIONAL EFFECT: WITTICISM
 
        SETUP();
    }
} // end default state
 
state disabled {
    link_message(integer sender_num,integer sender,string message,key id) {
        if ( sender == MODULE_SOCIAL || 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; }    }
}
// SCRIPT END