Myriad Lite Module Progress

// Myriad_Lite_Module_Progress-v0.0.2-20131019.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 - LEVEL TITLES - should this be part of the CAREER TEMPLATE?
 
// FIXME CONVERT INCAP and DEAD to FLAG
// FIXME more RPEVENT
// FIXME more HELP
 
// CONSTANTS - DO NOT CHANGE DURING RUN
string BASENAME = "Myriad Lite Module Progress";
string VERSION = "0.0.2"; // Allen Kerensky's script version
string VERSIONDATE = "20131019"; // Allen Kerensky's script yyyymmdd
 
// Module to Module Messaging Constants
integer MODULE_PROGRESS = -9;
integer LM_SENDTOATTACHMENT = 0x80000000;
 
// RUNTIME GLOBALS - MAY CHANGE
//integer XP;
//integer XPLEVEL;
list XP_BY_LEVEL; // list of XP per level from 0 to 30
integer ADD_XP_SESSION; // total of XP player has earned this session
string PROGRESSION; // Progression Method: LEVEL, GRADUAL, or RANDOM
integer FLAG_SESSION; // TRUE if player in session, FALSE if player out of session
integer XPSPENT;
integer DEL_XP_SESSION; // total of XP player lost this session
 
// Prim Persistent Memory Array Support
DELETE_RECORD(string dbkey, string field) {
    //OWNERSAY("DELETE_RECORD=["+dbkey+"] FIELD=["+field+"] START");
    // scan index or prim names for empty, get number for update
    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 ( name == dbkey && desc == field ) { // record found, delete
            llSetLinkPrimitiveParamsFast(i,[PRIM_NAME,"",PRIM_DESC,"",PRIM_TEXT,"",ZERO_VECTOR,0.0]); // save to database
        }
    }
    //OWNERSAY("DELETE_RECORD=["+dbkey+"] FIELD=["+field+"] ENDS");
}
 
integer COUNT_TYPE(string type) {
    //OWNERSAY("COUNT_TYPE TYPE=["+type+"] STARTS");
    type = llToUpper(type);
    // scan index or prim names for empty, get number for update
    integer count = 0;
    integer i;
    string name = "";
    //string desc = "";
    //string text = "";
    for ( i = 2; i <= llGetNumberOfPrims(); i++) {
        name = llList2String(llGetLinkPrimitiveParams(i,[PRIM_NAME]),0);
        if ( name == type ) count++;
    }
    //OWNERSAY("COUNT_TYPE TYPE=["+type+"] COUNT=["+(string)count+"]");
    return count;
}
 
DUMP_TYPE(integer chan,string type) {
    //OWNERSAY("DUMP_TYPES CHAN=["+(string)chan+"] TYPE=["+type+"] STARTS");
    type = llToUpper(type);
    // scan index or prim names for empty, get number for update
    integer i;
    string name = "";
    string desc = "";
    string text = "";
    for ( i = 2; i <= llGetNumberOfPrims(); i++) {
        name = llList2String(llGetLinkPrimitiveParams(i,[PRIM_NAME]),0);
        if ( name == type ) {
            desc = llList2String(llGetLinkPrimitiveParams(i,[PRIM_DESC]),0);
            text = llList2String(llGetLinkPrimitiveParams(i,[PRIM_TEXT]),0);
            if ( type == "PROGRESS" ) llSay(chan,"PROGRESS|"+desc+"="+text);
            if ( type == "STAT_INCREASE" ) llSay(chan,"STAT_INCREASE|"+desc+"="+text);
            if ( type == "SKILL_INCREASE" ) llSay(chan,"SKILL_INCREASE|"+desc+"="+text);
        }
    }
    //OWNERSAY("DUMP_TYPE ENDS");
}
 
// FIXME NEEDS TO BECOME LINK MESSAGE TO MOD_CHARSHEET!
ERASE_TYPE(string type) {
    //OWNERSAY("ERASE TYPE ["+type+"] STARTS");
    // scan index or prim names for empty, get number for update
    integer i;
    string name = "";
    string desc = "";
    for ( i = 2; i <= llGetNumberOfPrims(); i++) {
        name = llList2String(llGetLinkPrimitiveParams(i,[PRIM_NAME]),0);
        if ( name == type ) {
            desc = llList2String(llGetLinkPrimitiveParams(i,[PRIM_DESC]),0);
            DELETE_RECORD(type,desc);
        }
    }
    //OWNERSAY("ERASE_TYPE ENDS");    
}
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 = 0;
        if ( setting == "MAXDAMAGE" ) retval = 5;
        // CAMPAIGN constants FIXME Get from region server
        if ( setting == "MINGP" ) retval = 0;
        if ( setting == "MAXGP" ) retval = 112; // GP for godlike campaign
        if ( setting == "MINSTATPOOL" ) retval = 0;
        if ( setting == "MAXSTATPOOL" ) retval = 48; // 12 example stats in book times 4 pts per stat in godlike campaign
        if ( setting == "MINHEALTHPOOL") retval = 0;
        if ( setting == "MAXHEALTHPOOL") retval = 48; // 6 example resilience times 8 pts per resilience in godlike campaign
        if ( setting == "MINSKILLPOOL") retval = 0;
        if ( setting == "MAXSKILLPOOL") retval = 36; // 4 per 4 skills in godlike campaign, 36 example skills in book
        if ( setting == "MINEFFECTPOOL") retval = 0;
        if ( setting == "MAXEFFECTPOOL") retval = 7; // for godlike campaigns
        // Progress XP
        if ( setting == "MINXPLEVEL" ) retval = 1;
        if ( setting == "MAXXPLEVEL" ) retval = 30;
        if ( setting == "MINXP" ) retval = 0;
        if ( setting == "MAXXP") retval = 2320;
        if ( setting == "MINADDXP" ) retval = 1;
        if ( setting == "MAXADDXP" ) retval =  10;
        if ( setting == "MINDELXP" ) retval = 1;
        if ( setting == "MAXDELXP" ) retval = 10;
        if ( setting == "XPSCALE" ) retval = 1; // XPSCALE can be 1 or 10, FIXME region setting
        //    XP_BY_LEVEL = [ 0,0,10,25,45,70,100,135,175,220,270,325,385,450,520,595,675,760,850,945,1045,1150,1260,1375,1495,1620,1750,1885,2025,2170,2320 ];
        // Progress Gradual
        if ( setting == "STATCOSTFACTOR" ) retval = 5;
        if ( setting == "SKILLCOSTFACTOR" ) retval = 3;
        if ( setting == "EFFECTCOSTBASE" ) retval = 5;
        if ( setting == "EFFECTCOSTFACTOR" ) retval = 2;    
        // Progress Random
        if ( setting == "MINSTATINC" ) retval = 0;
        if ( setting == "MAXSTATINC" ) retval = 5; // after 5 stat increases, it should reset
        if ( setting == "MINSKILLINC" ) retval = 0;
        if ( setting == "MAXSKILLINC" ) retval = 5;
        if ( setting == "MINNEWSKILLS" ) retval = 0;
        if ( setting == "MAXNEWSKILLS" ) retval = 7; // FIXME arbitrarily chosen amount
        SET_VAL("MYRIAD",setting,(string)retval);
        ERROR("Unable to locate Myriad setting "+setting+" returning "+(string)retval);
    } else {
        retval = (integer)value;
    }
    return retval;
}
 
// STATISTICS
integer GET_STATISTIC(string stat) {
    // FIXME how to verify stat?
    integer points = (integer)GET_VAL("STATISTIC",stat);
    integer minstat = GET_MYRIAD("MINSTAT");
    integer maxstat = GET_MYRIAD("MAXSTAT");
    if ( points < minstat ) { points = minstat; SET_STATISTIC(stat,points); }
    if ( points > maxstat ) { points = maxstat; SET_STATISTIC(stat,points); }
    return points;
}
 
SET_STATISTIC(string statname,integer statrank) {
    // FIXME how to verify stat names are valid?
    integer minstat = GET_MYRIAD("MINSTAT");
    integer maxstat = GET_MYRIAD("MAXSTAT");
    if ( statrank < minstat ) statrank = minstat;
    if ( statrank > maxstat ) statrank = maxstat;
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"SET_STATISTIC|"+statname+"="+(string)statrank,llGetOwner()); // Notify CharSheet to update statistic
}
 
// SKILLS
integer GET_SKILL(string skill) {
    integer rank = (integer)GET_VAL("SKILL",skill);
    integer minskill = GET_MYRIAD("MINSKILL");
    integer maxskill = GET_MYRIAD("MAXSKILL");
    if ( rank < minskill ) { rank = minskill; SET_SKILL(skill,rank); }
    if ( rank > maxskill ) { rank = maxskill; SET_SKILL(skill,rank); }
    return rank;
}
 
SET_SKILL(string skill,integer rank) {
    // FIXME how to verify skill names are valid?
    integer minskill = GET_MYRIAD("MINSKILL");
    integer maxskill = GET_MYRIAD("MAXSKILL");
    if ( rank < minskill ) rank = minskill;
    if ( rank > maxskill ) rank = maxskill;
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"SET_SKILL|"+skill+"="+(string)rank,llGetOwner()); // notify character sheet to update skill
}
 
// SFX
integer GET_EFFECT(string name) {
    integer rank = (integer)GET_VAL("EFFECT",name);
    integer mineffect = GET_MYRIAD("MINEFFECT");
    integer maxeffect = GET_MYRIAD("MAXEFFECT");
    if ( rank < mineffect ) { rank = mineffect; SET_EFFECT(name,rank); }
    if ( rank > maxeffect ) { rank = maxeffect; SET_EFFECT(name,rank); }
    return rank;
}
 
SET_EFFECT(string name,integer rank) {
    // FIXME how to verify effect name?
    integer mineffect = GET_MYRIAD("MINEFFECT");
    integer maxeffect = GET_MYRIAD("MAXEFFECT");
    if ( rank < mineffect ) rank = mineffect;
    if ( rank > maxeffect ) rank = maxeffect;
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"SET_EFFECT|"+name+"="+(string)rank,llGetOwner()); // notify character sheet to update effect
}
 
// RESILIENCES
integer GET_RESILIENCE(string name) {
    integer cr = (integer)GET_VAL("RESILIENCE",name);
    integer minres = GET_MYRIAD("MINRESILIENCE");
    integer maxres = GET_MYRIAD("MAXRESILIENCE");
    if ( cr < minres ) { cr = minres; SET_RESILIENCE(name,cr); }
    if ( cr > maxres ) { cr = maxres; SET_RESILIENCE(name,cr); }
    return cr;
}
 
SET_RESILIENCE(string resname,integer resrank) {
    // FIXME how to verify resilience names are valid?
    integer minres = GET_MYRIAD("MINRESILIENCE");
    integer maxres = GET_MYRIAD("MAXRESILIENCE");
    if ( resrank < minres ) resrank = minres;
    if ( resrank > maxres ) resrank = maxres;
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"SET_RESILIENCE|"+resname+"="+(string)resrank,llGetOwner()); //notify character sheet to update resilience
}
 
//////////////////////
// PROGRESS UTILITY //
//////////////////////
 
DEL_PROGRESS(key id) {
    key objectowner = llList2Key(llGetObjectDetails(id,[OBJECT_OWNER]),0);
    key regionowner = llList2Key(llGetParcelDetails(<0,0,0>,[PARCEL_DETAILS_OWNER]),0);
    if ( objectowner != regionowner ) {
        ERROR("DEL_PROGRESS called by object or player who is not region owner. ["+(string)objectowner+"]["+(string)regionowner+"]");
        return;
    }
    // Delete all progress for LEVEL, GRADUAL, and RANDOM
    SET_STATPOOL(0);
    SET_HEALTHPOOL(0);
    SET_SKILLPOOL(0);
    SET_EFFECTPOOL(0);
    SET_XPLEFT(0);
    SET_SKILL_INC(0);
    SET_NEW_SKILLS(0);
    ERASE_TYPE("SKILL_INCREASE");
    ERASE_TYPE("STAT_INCREASE");
    SET_SIXES_BURNED(0);
}
 
DUMP_PROGRESS(key id) { // id to dump progress back to
    key objectowner = llList2Key(llGetObjectDetails(id,[OBJECT_OWNER]),0);
    key regionowner = llList2Key(llGetParcelDetails(<0,0,0>,[PARCEL_DETAILS_OWNER]),0);
    if ( objectowner != regionowner ) {
        ERROR("DUMP_PROGRESS called by object or player who is not region owner. ["+(string)objectowner+"]["+(string)regionowner+"]");
        return;
    }
    integer chan = (integer)("0x"+llGetSubString((key)id,0,6));
    DUMP_TYPE(chan,"PROGRESS");
    DUMP_TYPE(chan,"STAT_INCREASE");
    DUMP_TYPE(chan,"SKILL_INCREASE");
    llSay(chan,"PROGRESS_LOADED");
}
 
END_SESSION() {
    if ( FLAG_SESSION == TRUE ) {
        DEBUG("Session ending. "+(string)ADD_XP_SESSION+"XP earned.");
        FLAG_SESSION = FALSE;
    }
}
 
REPORT_POOLS() {
    string report = "PROGRESS";
    if ( PROGRESSION == "LEVEL" ) {
        report += "|"+"XP="+(string)GET_XP();
        report += "|"+"XPLEVEL="+(string)GET_XPLEVEL();
        report += "|"+"STATPOOL="+(string)GET_STATPOOL();
        report += "|"+"SKILLPOOL="+(string)GET_SKILLPOOL();
        report += "|"+"HEALTHPOOL="+(string)GET_HEALTHPOOL();
        report += "|"+"SFXPOOL="+(string)GET_EFFECTPOOL();
    }
    if ( PROGRESSION == "GRADUAL" ) {
        report += "|"+"XP="+(string)GET_XP();
        report += "|"+"XPSPENT="+(string)XPSPENT;     
    }
    if ( PROGRESSION == "RANDOM" ) {
        DEBUG("RANDOM");
        // FIXME PPMA Storage of CSV list PROGRESS,STATISTICSUSED,<csv_statname_times>
        // FIXME PPMA Storage of CSV list PROGRESS,SKILLSUSED,<csv_skillname_times>
        // FIXME PPMA Storage of CSV list PROGRESS,EFFECTSUSED,<csv_effectname_times>
    }
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,report,llGetOwner());
}
 
SET_PROGRESSION(string method) {
    if ( method == "LEVEL" || method == "GRADUAL" || method == "RANDOM" ) {
        PROGRESSION = method;
        DEBUG("Character progression method set to: "+method);
    } else {
        ERROR("Unknown progression method: "+method+"! Roleplay progress will not be counted.");
    }
}
 
START_SESSION() {
    if ( PROGRESSION != "LEVEL" || PROGRESSION != "GRADUAL" || PROGRESSION != "RANDOM" ) {
        ERROR("Progression method not set. Cannot start roleplay session. Roleplay progress will not be counted.");
        return;
    }
    if ( FLAG_SESSION == FALSE ) {
        DEBUG("Session starting.");
        FLAG_SESSION = TRUE;
        ADD_XP_SESSION = 0; // zero out the XP earned this session counter
    }
}
 
////////////////
// GENERATORS //
////////////////
 
//
// GET_GP
//
integer GET_GP() {
    integer gp = (integer)GET_VAL("PROGRESS","GP");
    integer mingp = GET_MYRIAD("MINGP");
    integer maxgp = GET_MYRIAD("MAXGP");
    if ( gp < mingp ) { gp = mingp; SET_GP(gp); }
    if ( gp > maxgp ) { gp = maxgp; SET_GP(gp); }
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"PROGRESS|GP="+(string)gp,llGetOwner()); // FIXME remove unneeded link message when PPMA complete
    return gp;
}
 
SET_GP(integer gpamt) {
    integer mingp = GET_MYRIAD("MINGP");
    integer maxgp = GET_MYRIAD("MAXGP");
    if ( gpamt < mingp ) gpamt = mingp;
    if ( gpamt > maxgp ) gpamt = maxgp;
    SET_VAL("PROGRESS","GP",(string)gpamt);
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"PROGRESS|GP="+(string)gpamt,llGetOwner()); // FIXME remove unneeded link message when PPMA complete
}
 
// STATPOOL - GENERATOR POINTBUY, UPDATER LEVEL
integer GET_STATPOOL() {
    integer sp = (integer)GET_VAL("PROGRESS","STATPOOL");
    integer minstatpool = GET_MYRIAD("MINSTATPOOL");
    integer maxstatpool = GET_MYRIAD("MAXSTATPOOL");
    if ( sp < minstatpool ) { sp = minstatpool; SET_STATPOOL(sp); }
    if ( sp > maxstatpool ) { sp = maxstatpool; SET_STATPOOL(sp); }
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"PROGRESS|STATPOOL="+(string)sp,llGetOwner()); // FIXME remove unneeded link message when PPMA complete
    return sp;
}
 
SET_STATPOOL(integer statamt) {
    integer minstatpool = GET_MYRIAD("MINSTATPOOL");
    integer maxstatpool = GET_MYRIAD("MAXSTATPOOL");
    if ( statamt < minstatpool ) statamt = minstatpool;
    if ( statamt > maxstatpool ) statamt = maxstatpool;
    SET_VAL("PROGRESS","STATPOOL",(string)statamt);
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"PROGRESS|STATPOOL="+(string)statamt,llGetOwner()); //FIXME remove unneeded link message when PPMA complete
}
 
// HEALTHPOOL - GENERATOR POINTBUY, UPDATER LEVEL
integer GET_HEALTHPOOL() {
    integer hp = (integer)GET_VAL("PROGRESS","HEALTHPOOL");
    integer minhealthpool = GET_MYRIAD("MINHEALTHPOOL");
    integer maxhealthpool = GET_MYRIAD("MAXHEALTHPOOL");
    if ( hp < minhealthpool ) { hp = minhealthpool; SET_HEALTHPOOL(hp); }
    if ( hp > maxhealthpool ) { hp = maxhealthpool; SET_HEALTHPOOL(hp); }
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"PROGRESS|HEALTHPOOL="+(string)hp,llGetOwner()); // FIXME remove unneeded link message when PPMA complete
    return hp;
}
 
SET_HEALTHPOOL(integer healthamt) {
    integer minhealthpool = GET_MYRIAD("MINHEALTHPOOL");
    integer maxhealthpool = GET_MYRIAD("MAXHEALTHPOOL");
    if ( healthamt < minhealthpool ) healthamt = minhealthpool;
    if ( healthamt > maxhealthpool ) healthamt = maxhealthpool;
    SET_VAL("PROGRESS","HEALTHPOOL",(string)healthamt);
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"PROGRESS|HEALTHPOOL="+(string)healthamt,llGetOwner()); // FIXME remove unneeded link message when PPMA complete
}
 
// SKILLPOOL - GENERATOR POINTBUY, UPDATER LEVEL
integer GET_SKILLPOOL() {
    integer skp = (integer)GET_VAL("PROGRESS","SKILLPOOL");
    integer minskillpool = GET_MYRIAD("MINSKILLPOOL");
    integer maxskillpool = GET_MYRIAD("MAXSKILLPOOL");
    if ( skp < minskillpool ) { skp = minskillpool; SET_SKILLPOOL(skp); }
    if ( skp > maxskillpool ) { skp = maxskillpool; SET_SKILLPOOL(skp); }
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"PROGRESS|SKILLPOOL="+(string)skp,llGetOwner()); // FIXME remove unneeded link message when PPMA complete
    return skp;
}
 
SET_SKILLPOOL(integer skillamt) {
    integer minskillpool = GET_MYRIAD("MINSKILLPOOL");
    integer maxskillpool = GET_MYRIAD("MAXSKILLPOOL");
    if ( skillamt < minskillpool ) skillamt = minskillpool;
    if ( skillamt > maxskillpool ) skillamt = maxskillpool;
    SET_VAL("PROGRESS","SKILLPOOL",(string)skillamt);
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"PROGRESS|SKILLPOOL="+(string)skillamt,llGetOwner()); //FIXME remove unneeded link message when PPMA complete
}
 
// SFXPOOL - GENERATOR POINTBUY, UPDATER LEVEL
integer GET_EFFECTPOOL() {
    integer sfp = (integer)GET_VAL("PROGRESS","EFFECTPOOL");
    integer mineffectpool = GET_MYRIAD("MINEFFECTPOOL");
    integer maxeffectpool = GET_MYRIAD("MAXEFFECTPOOL");
    if ( sfp < mineffectpool ) { sfp = mineffectpool; SET_EFFECTPOOL(sfp); }
    if ( sfp > maxeffectpool ) { sfp = maxeffectpool; SET_EFFECTPOOL(sfp); }
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"PROGRESS|SFXPOOL="+(string)sfp,llGetOwner()); // FIXME remove unneeded link message when PPMA complete
    return sfp;
}
 
SET_EFFECTPOOL(integer sfxamt) {
    integer mineffectpool = GET_MYRIAD("MINEFFECTPOOL");
    integer maxeffectpool = GET_MYRIAD("MAXEFFECTPOOL");
    if ( sfxamt < mineffectpool ) sfxamt = mineffectpool;
    if ( sfxamt > maxeffectpool ) sfxamt = maxeffectpool;
    SET_VAL("PROGRESS","EFFECTPOOL",(string)sfxamt);
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"PROGRESS|SFXPOOL="+(string)sfxamt,llGetOwner()); // FIXME remove unneeded link message when PPMA complete    
}
 
///////////////////////////
// PROGRESS: LEVEL-BASED //
///////////////////////////
//
// XP
integer GET_XP() {
    integer xp = (integer)GET_VAL("PROGRESS","XP");
    integer minxp = GET_MYRIAD("MINXP");
    integer maxxp = GET_MYRIAD("MAXXP");
    if ( xp < minxp ) { xp = minxp; SET_XP(xp); }
    if ( xp > maxxp ) { xp = maxxp; SET_XP(xp); }
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"PROGRESS|XP="+(string)xp,llGetOwner());
    return xp;
}
 
SET_XP(integer amount) {
    integer xp;
    integer minxp = GET_MYRIAD("MINXP");
    integer maxxp = GET_MYRIAD("MAXXP");
    integer xplevel = GET_XPLEVEL();
    if ( amount >= minxp && amount <= maxxp ) {
        xp = amount;
        integer templevel = GET_LEVEL_BY_XP(xp);
        if ( templevel != 0 ) {
            if ( templevel > xplevel ) { // player is levelling up!
                LEVELUP(templevel); // level up player
            }
            xplevel = templevel; // set new XP level to current 
        }
        DEBUG("XP set to: "+(string)xp+" XPLEVEL set to: "+(string)xplevel);
    } else {
        ERROR("XP "+(string)amount+" out of range: "+(string)minxp+"-"+(string)maxxp);
    }
    if ( xp < minxp ) xp = minxp;
    if ( xp > maxxp ) xp = maxxp;
    SET_VAL("PROGRESS","XP",(string)xp);
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"PROGRESS|XP="+(string)xp,llGetOwner());
}
 
ADD_XP(integer amount) {
    integer maxxp = GET_MYRIAD("MAXXP");
    integer minaddxp = GET_MYRIAD("MINADDXP");
    integer maxaddxp = GET_MYRIAD("MAXADDXP");
    if ( amount >= minaddxp && amount <= maxaddxp ) {
        if ( amount + ADD_XP_SESSION > maxaddxp ) {
            DEBUG("Max XP gain per session reached. Unable to add more XP.");
        } else {
            SET_XP(GET_XP() + amount); // add XP to player pool
            ADD_XP_SESSION += amount; // keep track of how many earned this session
        }
    } else {
        ERROR("ADD_XP amount "+(string)amount+" out of range: "+(string)minaddxp+"-"+(string)maxaddxp);
    }
    if ( GET_XP() > maxxp ) SET_XP(maxxp); // FIXME need to set XP in PPMA lol
}
 
// ADD_XP - Add a point of XP
ADD_XP2(key granterid) {
    key objectowner = llList2Key(llGetObjectDetails(granterid,[OBJECT_OWNER]),0);
    key regionowner = llList2Key(llGetParcelDetails(<0,0,0>,[PARCEL_DETAILS_OWNER]),0);
    if ( objectowner != regionowner ) {
        ERROR("ADD_XP called by object or player who is not region owner. ["+(string)objectowner+"]["+(string)regionowner+"]");
        return;
    }
    DEBUG("Progression="+PROGRESSION+" XP="+(string)GET_XP()+" XPLEVEL="+(string)GET_XPLEVEL());
    if ( PROGRESSION == "RANDOM" ) {
        ERROR("Unable to add XP in region using Random Progression");
        return;
    }
    integer maxxp = GET_MYRIAD("MAXXP");
    if ( GET_XP() < maxxp ) {
        SET_XP( GET_XP() + 1); // add one to total XP
        if ( PROGRESSION == "LEVEL-BASED" ) {
            integer currentlevel = GET_XPLEVEL();
            integer templevel = GET_LEVEL_BY_XP(GET_XP());            
            if ( templevel > currentlevel ) {
                SET_XPLEVEL(templevel);
                string ownername = llList2String(llParseString2List(llKey2Name(llGetOwner()),["@"],[]),0); // strip @where from HG names
                OWNERSAY("LEVEL UP! Congratulations, you are now XP Level "+(string)GET_XPLEVEL());
                RPEVENT("LEVEL UP! Congratulations, "+ownername+" is now XP Level "+(string)GET_XPLEVEL());
                LEVELUP(GET_XPLEVEL());
            }
            return;
        }
        if ( PROGRESSION == "GRADUAL" ) {
            SET_XPLEFT ( GET_XPLEFT() + 1); // add one to XP you can spend in gradual progress mode
            return;
        }
        llMessageLinked(LINK_THIS,MODULE_PROGRESS,"XP_CHANGED|XP="+(string)GET_XP(),llGetOwner());
        DEBUG("Progression="+PROGRESSION+" XP="+(string)GET_XP()+" XPLEVEL="+(string)GET_XPLEVEL());
    } else {
        ERROR("ADD_XP(): XP already maxed.");
    }
}
 
DEL_XP(integer amount) {
    integer minxp = GET_MYRIAD("MINXP");
    integer mindelxp = GET_MYRIAD("MINDELXP");
    integer maxdelxp = GET_MYRIAD("MAXDELXP");
    if ( amount >= mindelxp && amount <= maxdelxp ) {
        if ( amount + DEL_XP_SESSION > maxdelxp ) {
            DEBUG("Max XP loss per session reached. Unable to add more XP.");
        } else {
            SET_XP(GET_XP() - amount); // add XP to player pool
            DEL_XP_SESSION += amount; // 
        }
    } else {
        ERROR("DEL_XP amount "+(string)amount+" out of range: "+(string)mindelxp+"-"+(string)maxdelxp);
    }
    if ( GET_XP() < 0 ) SET_XP(minxp); // FIXME save XP to PPMA lol
}
 
integer GET_LEVEL_BY_XP(integer amount) {
    integer count = 0;
    integer outlevel = 0;
    integer maxxplevel = GET_MYRIAD("MAXXPLEVEL");
    for (count = 0; count < maxxplevel; count++ ) {
        if ( amount > llList2Integer(XP_BY_LEVEL,count) ) outlevel = count;
    }
    DEBUG("GET_LEVEL_BY_XP("+(string)amount+") returning "+(string)outlevel);
    return outlevel;
}
 
ADD_XPLEVEL() {
    integer maxxplevel = GET_MYRIAD("MAXXPLEVEL");
    integer xplevel = GET_XPLEVEL();
    if ( xplevel < maxxplevel ) {
        SET_XPLEVEL(xplevel++); // add one
        DEBUG("Level added. New level: "+(string)xplevel);
    } else {
        ERROR("Unable to add a level. Current level already: "+(string)xplevel);
    }
    integer tempxp = GET_XP_BY_LEVEL(xplevel);
    if ( tempxp != 0 ) { 
        if ( tempxp > GET_XP() ) SET_XP(tempxp); // if new level XP greater than current saved XP
    }
}
 
DEL_XPLEVEL() {
    integer minxplevel = GET_MYRIAD("MINXPLEVEL"); 
    integer xplevel = GET_XPLEVEL();
    if ( xplevel > minxplevel ) {
        SET_XPLEVEL(xplevel--);
        DEBUG("Level deleted. New level: "+(string)xplevel);
    } else {
        ERROR("Unable to delete a level. Current level already: "+(string)xplevel);
    }
    integer tempxp = GET_XP_BY_LEVEL(xplevel);
    if ( tempxp != 0 ) SET_XP(tempxp); // FIXME PPMA XP
}
 
SET_XPLEVEL(integer amount) {
    integer xplevel;
    integer minxplevel = GET_MYRIAD("MINXPLEVEL");
    integer maxxplevel = GET_MYRIAD("MAXXPLEVEL");
    if ( amount < minxplevel || amount > maxxplevel ) {
        ERROR("Set level "+(string)amount+" out of range: "+(string)minxplevel+"-"+(string)maxxplevel);
        return;
    }
    xplevel = amount; 
    integer tempxp = GET_XP_BY_LEVEL(xplevel);
    if ( tempxp != 0 ) SET_XP(tempxp); // set 
 
    if ( xplevel < minxplevel ) xplevel = minxplevel;
    if ( xplevel > maxxplevel ) xplevel = maxxplevel;
    SET_VAL("PROGRESS","XPLEVEL",(string)xplevel);
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"PROGRESS|XPLEVEL="+(string)xplevel,llGetOwner());
}
 
integer GET_XP_BY_LEVEL(integer amount ) {
    integer minxplevel = GET_MYRIAD("MINXPLEVEL");
    integer maxxplevel = GET_MYRIAD("MAXXPLEVEL");
    if ( amount >= minxplevel && amount <= maxxplevel ) {
        return llList2Integer(XP_BY_LEVEL,amount);
    }
    ERROR("Requested level "+(string)amount+" out of range "+(string)minxplevel+"-"+(string)maxxplevel);
    return 0;
}
 
// XPLEVEL
integer GET_XPLEVEL() {
    integer xplevel = (integer)GET_VAL("PROGRESS","XPLEVEL");
    integer minxplevel = GET_MYRIAD("MINXPLEVEL");
    integer maxxplevel = GET_MYRIAD("MAXXPLEVEL");
    if ( xplevel < minxplevel ) { xplevel = minxplevel; SET_XPLEVEL(xplevel); }
    if ( xplevel > maxxplevel ) { xplevel = maxxplevel; SET_XPLEVEL(xplevel); }
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"PROGRESS|XPLEVEL="+(string)xplevel,llGetOwner());
    return xplevel;
}
 
// XP LEVEL
CALCULATE_LEVEL_BY_XP() {
    integer i;
    for ( i=1; i < llGetListLength(XP_BY_LEVEL); i++ ) {
        integer basexp = llList2Integer(XP_BY_LEVEL,i);
        if ( GET_XP() >= basexp ) {
            SET_XPLEVEL(i);
        }
    }
}
 
// LEVEL UP - Calculate bonuses related to new level
LEVELUP(integer newlevel) {
    // In the Myriad system, each time a character gains a new level he is given two skill points, each of which can be used to purchase a new skill at level 1 or improve an existing skill by one level.
    SET_SKILLPOOL ( GET_SKILLPOOL() + 2 ); // add two skill points per level
    // The character also gains an SFX point on every even-numbered level if the module being played supports SFX.
    if ( ( newlevel % 2 ) == 0 ) { // every even level
        SET_EFFECTPOOL ( GET_EFFECTPOOL () + 1 ); // add new SFX point
    }
    // One new health point may also be used to upgrade any one of the character's resilience lines, or saved in order to buy a box that would normally cost two points.
    SET_HEALTHPOOL ( GET_HEALTHPOOL() + 1 ); // add a point of health
    // Finally, the character earns one quarter of a statistic point to improve any one statistic with.
    if ( ( newlevel % 4 ) == 0 ) { // every 4th level
        SET_STATPOOL( GET_STATPOOL() + 1 ); // add a new stat point
    }
}
 
// ADD_OPPONENT_XP - add a scale of XP based on opponent's level
// +2 pts if enemy 2 or more levels above, 1 point if enemy is 1 level above or below, and zero points if enemy 2 or more levels lowere
ADD_OPPONENT_XP(integer opponentlevel) {
    integer diff = opponentlevel - GET_XPLEVEL();
    integer add = 0;
    if ( diff >= 2 ) add = 2;
    if ( diff >= -1 && diff <= 1 ) add = 1;
    if ( add == 0 ) {
        OWNERSAY("No XP given for lower level opponent.");
        return;
    }
    OWNERSAY("You earned "+(string)add+" XP for that opponent.");
    ADD_XP(add);
}
 
// ADD_OPPONENT_XP_X10
// if XPSCALE = 10, multiply all the values in the XP level table by ten and use the following rule:
// a monster is worth 3XP, minus 1 for every level that it is below the character's own to a minimum of zero, or plus one for every level that it is above the character's own to a maximum of ten.
//
ADD_OPPONENT_XP_X10(integer opponentlevel) {
    integer base = 3;
    integer add;
    integer diff;
    integer xplevel = GET_XPLEVEL();
    if ( opponentlevel > xplevel ) {
        diff = opponentlevel - xplevel;
        if ( diff > 10 ) diff = 10;
        add = base + diff;
    }
    if ( opponentlevel < xplevel ) {
        diff = xplevel - opponentlevel;
        add = base - diff;
        if ( add < 0 ) add = 0;
    }
    OWNERSAY("You earned "+(string)add+" XP for that opponent.");
    ADD_XP(add);
}
 
///////////////////////
// PROGRESS: GRADUAL //
///////////////////////
//
// XPLEFT
//
integer GET_XPLEFT() {
    integer xp = (integer)GET_VAL("PROGRESS","XPLEFT");
    integer minxp = GET_MYRIAD("MINXP");
    integer maxxp = GET_MYRIAD("MAXXP");
    if ( xp < minxp ) { xp = minxp; SET_XPLEFT(xp); }
    if ( xp > maxxp ) { xp = maxxp; SET_XPLEFT(xp); }
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"PROGRESS|XPLEFT="+(string)xp,llGetOwner());
    return xp;
}
 
SET_XPLEFT(integer xp) {
    integer minxp = GET_MYRIAD("MINXP");
    integer maxxp = GET_MYRIAD("MAXXP");
    if ( xp < minxp ) xp = minxp;
    if ( xp > maxxp ) xp = maxxp;
    SET_VAL("PROGRESS","XPLEFT",(string)xp);
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"PROGRESS|XPLEFT="+(string)xp,llGetOwner());
}
 
 
//
// FIXME SPENDXP
//
SPENDXP() {
    if ( FLAG_SESSION == TRUE ) {
        ERROR("Cannot spend XP during play session.");
        return;
    }
}
 
//
// FIXME IMPROVE_STATISTIC
// FIXME - list of stats and whether or not they've been improved this session?
//
IMPROVE_STAT(string statname) {
}
 
//
// FIXME IMPROVE_SKILL
//
IMPROVE_SKILL(string skillname) {
}
 
//
// FIXME IMPROVE_EFFECT
//
IMPROVE_EFFECT(string effect) {
}
 
//
// FIXME CALC_STAT_COST
//
integer CALC_STAT_COST(string statname) {
    integer statcostfactor = GET_MYRIAD("STATCOSTFACTOR");
    integer cost = ( GET_STATISTIC(statname) + 1) * statcostfactor;
    DEBUG("Cost to increase stat "+statname+": "+(string)cost);
    return cost;
}
 
//
// FIXME CALC_SKILL_COST
//
integer CALC_SKILL_COST(string skillname) { // calculate to raise existing OR buy new
    integer skillcostfactor = GET_MYRIAD("SKILLCOSTFACTOR");
    integer cost = ( GET_SKILL(skillname) + 1) * skillcostfactor;
    DEBUG("Cost to increase skill "+skillname+": "+(string)cost);
    return cost;
}
 
//
// FIXME CALC_EFFECT_COST
//
integer CALC_SFX_COST(string sfxname) { // calculate cost to buy new
    integer count = COUNT_TYPE("EFFECT");
    integer effectcostbase = GET_MYRIAD("EFFECTCOSTBASE");
    integer effectcostfactor = GET_MYRIAD("EFFECTCOSTFACTOR");
    integer cost = effectcostbase + ( effectcostfactor * count );
    DEBUG("Cost to buy new special effect ability "+sfxname+": "+(string)cost);
    return cost;
}
 
//////////////////////
// PROGRESS: RANDOM //
//////////////////////
 
INCREASE_STAT(string sname) {
    integer rank = GET_STATISTIC(sname);
    SET_STATISTIC(sname,rank + 1);
}
 
integer GET_STAT_INC(string stat) {
    // FIXME how to verify stat?
    integer points = (integer)GET_VAL("STAT_INCREASE",stat);
    integer minstatinc = GET_MYRIAD("MINSTATINC");
    integer maxstatinc = GET_MYRIAD("MAXSTATINC");
    if ( points < minstatinc ) { points = minstatinc; SET_STAT_INC(stat,points); }
    if ( points > maxstatinc ) { points = maxstatinc; SET_STAT_INC(stat,points); }
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"STAT_INCREASE|"+stat+"="+(string)points,llGetOwner()); // FIXME remove unneeded link message when PPMA complete
    return points;
}
 
SET_STAT_INC(string statname,integer amt) {
    // FIXME how to verify stat names are valid?
    integer minstatinc = GET_MYRIAD("MINSTATINC");
    integer maxstatinc = GET_MYRIAD("MAXSTATINC");
    if ( amt < minstatinc ) { amt = maxstatinc; }
    if ( amt > maxstatinc ) { 
        amt = minstatinc; // reset to zero and increase stat itself
        SET_STATISTIC(statname, GET_STATISTIC(statname) + 1 );
    }
    SET_VAL("STAT_INCREASE",statname,(string)amt); // save new amount
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"SET_STAT_INCREASE|"+statname+"="+(string)amt,llGetOwner()); // FIXME remove unneeded link message when PPMA complete    
}
 
INCREASE_STAT_INC(string sname) {
    integer rank = GET_STAT_INC(sname);
    SET_STAT_INC(sname,rank + 1);
}
 
INCREASE_SKILL(string skill) {
    integer rank = GET_SKILL(skill);
    integer maxskill = GET_MYRIAD("MAXSKILL");
    if ( rank < maxskill ) {
        SET_SKILL(skill,rank + 1);
        RPEVENT("increased their "+skill+" skill by one.");    
    } else {
        ERROR("INCREASE_SKILL("+skill+") CANNOT INCREASE PAST MAX SKILL LEVEL "+(string)maxskill);
    }
}
 
// SKILL INCREASES
integer GET_SKILL_INC() {
    integer inc = (integer)GET_VAL("PROGRESS","SKILL_INCREASES");
    integer minskillinc = GET_MYRIAD("MINSKILLINC");
    integer maxskillinc = GET_MYRIAD("MAXSKILLINC");
    if ( inc < minskillinc ) { inc = minskillinc; SET_SKILL_INC(inc); }
    if ( inc > maxskillinc ) { inc = maxskillinc; SET_SKILL_INC(inc); }
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"PROGRESS|SKILL_INCREASES="+(string)inc,llGetOwner()); // FIXME remove unneeded link message when PPMA complete
    return inc;
}
 
SET_SKILL_INC(integer si) {
    integer minskillinc = GET_MYRIAD("MINSKILLINC");
    integer maxskillinc = GET_MYRIAD("MAXSKILLINC");
    if ( si < minskillinc ) si = minskillinc;
    if ( si > maxskillinc ) si = maxskillinc;
    SET_VAL("PROGRESS","SKILL_INCREASES",(string)si);
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"PROGRESS|SKILL_INCREASES="+(string)si,llGetOwner()); // FIXME remove unneeded link message when PPMA complete
}
 
// NEW SKILLS - number of new skills earned through RANDOM skill increases since last reset
integer GET_NEW_SKILLS() {
    integer new = (integer)GET_VAL("PROGRESS","NEW_SKILLS"); // FIXME NEWSKILLS
    integer minnewskills = GET_MYRIAD("MINNEWSKILLS");
    integer maxnewskills = GET_MYRIAD("MAXNEWSKILLS");
    if ( new < minnewskills ) { new = minnewskills; SET_NEW_SKILLS(new); }
    if ( new > minnewskills ) { new = maxnewskills; SET_NEW_SKILLS(new); }
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"PROGRESS|NEW_SKILLS="+(string)new,llGetOwner()); // FIXME remove unneeded link message when PPMA complete
    return new;
}
 
SET_NEW_SKILLS(integer new) {
    integer minnewskills = GET_MYRIAD("MINNEWSKILLS");
    integer maxnewskills = GET_MYRIAD("MAXNEWSKILLS");
    if ( new < minnewskills ) new = minnewskills;
    if ( new > maxnewskills ) new = maxnewskills;
    SET_VAL("PROGRESS","NEW_SKILLS",(string)new); // FIXME NEWSKILLS
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"PROGRESS|NEW_SKILLS="+(string)new,llGetOwner()); // FIXME remove unneeded link message when PPMA complete
}
 
integer GET_SKILLS_INCREASED(string name) {
    integer num = (integer)GET_VAL("SKILL_INCREASE",name);
    // FIXME need range checks
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"SKILL_INCREASE|"+name+"="+(string)num,llGetOwner()); // FIXME remove unneeded link message when PPMA complete
    return num;
}
 
SET_SKILLS_INCREASED(string name,integer rank) {
    // FIXME need range checks
    SET_VAL("SKILL_INCREASE",name,(string)rank);
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"SKILL_INCREASE|"+name+"="+(string)rank,llGetOwner()); // FIXME remove unneeded link message when PPMA complete
}
 
SKILL_FAILED(string sname) {
    if ( PROGRESSION != "RANDOM" ) return; // tracking skill failure only applies to random progression - ignore message
    integer current = GET_SKILLS_INCREASED(sname);
    if ( current < 4 ) { // not yet, just note it
    } else { // has hit 5, increase skill and stat
        INCREASE_SKILL(sname); // increase skill
        SET_SKILLS_INCREASED(sname,current + 1);
    }
    integer si = GET_SKILL_INC() + 1;
    SET_SKILL_INC(si);
    // now - 5 skill increases = new skill earned
    if ( si == 5 ) {
        SET_SKILL_INC(0);
        SET_NEW_SKILLS ( GET_NEW_SKILLS() + 1);
        RPEVENT("earned a new skill!");
    }
}
 
integer GET_SIXES_BURNED() {
    integer sb = (integer)GET_VAL("PROGRESS","SIXES_BURNED");
    // FIXME range checking
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"PROGRESS|SIXES_BURNED="+(string)sb,llGetOwner()); // FIXME remove unneeded link message when PPMA complete
    return sb;
}
 
SET_SIXES_BURNED(integer sb) {
    // FIXME range checking
    SET_VAL("PROGRESS","SIXES_BURNED",(string)sb);
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"PROGRESS|SIXES_BURNED="+(string)sb,llGetOwner()); // FIXME remove unneeded link message when PPMA complete
}
 
/////////////
// UTILITY //
/////////////
 
//
// DEBUG - show debug chat with wearer name for sorting
//
DEBUG(string dmessage) {
    if ( GET_FLAG("DEBUG") == TRUE ) llMessageLinked(LINK_THIS,MODULE_PROGRESS,"DEBUG|"+dmessage,llGetOwner());
}
 
//
// ERROR - show errors on debug channel with wearer name for sorting
//
ERROR(string emessage) {
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"ERROR|"+emessage,llGetOwner());
}
 
//
// GET_MEMORY
//
GET_MEMORY() {
    OWNERSAY(BASENAME+" free memory: "+(string)llGetFreeMemory()); // show this module's free memory info
}
 
//
// GET_VERSION
//
GET_VERSION() {
    OWNERSAY(BASENAME+" v"+VERSION+"-"+VERSIONDATE); // show this module's version info
}
 
//
// OWNERSAY
//
OWNERSAY(string msg) {
    llMessageLinked(LINK_THIS,MODULE_PROGRESS,"OWNERSAY|"+msg,llGetOwner());
}
 
//
// PARSE
//
PARSE(string message,key id) {
    // No need for debug - only link messages to this module and debug is done there
    // First - handle type 1 messages that do not require breaking down
    list tokens = llParseString2List(message,["|"],[]);
    string cmd = llToLower(llStringTrim(llList2String(tokens,0),STRING_TRIM));    
    string data = llList2String(tokens,1);
    list subtokens = llParseString2List(data,["="],[]);
    string attrib = llList2String(subtokens,0);
    integer idata = llList2Integer(subtokens,1);
    string sdata = llList2String(subtokens,1);
 
    if ( cmd == "memory" ) { GET_MEMORY(); return; }        
    if ( cmd == "reset" ) { RESET(); return; }
    if ( cmd == "version" ) { GET_VERSION(); return;} // show version info
 
    // Progression
    if ( cmd == "set_progression" ) { SET_PROGRESSION(sdata); return; } // set progress method to LEVEL, GRADUAL, or RANDOM
    if ( cmd == "start_session" ) { START_SESSION(); return; }
    if ( cmd == "end_session" ) { END_SESSION(); return; }
    if ( cmd == "report_pools" ) { REPORT_POOLS(); return;} // used for character builder to ask player HUD for current pools
    // Progression API?
    if ( cmd == "add_xp" ) { ADD_XP(idata); return; }// ADD_XP|XP=###
    if ( cmd == "del_xp" ) { DEL_XP(idata); return; } // DEL_XP|XP=###
    if ( cmd == "add_xplevel" ) { ADD_XPLEVEL(); return; }// ADD_XPLEVEL|XPLEVEL=###
    if ( cmd == "del_xplevel" ) { DEL_XPLEVEL(); return; }// DEL_XPLEVEL|XPLEVEL=###
 
    // FIXME remove unneeded link message when PPMA complete all GET_* command blocks once PPMA is integrated in all HUD scripts    
    if ( cmd == "dump_progress" ) { DUMP_PROGRESS(id); return; } // id is UUID of character builder, sent over from HUD script
    if ( cmd == "del_progress" ) { DEL_PROGRESS(id); return; } // id is UUID of item that requested del progress
    if ( cmd == "add_xp2" ) { ADD_XP2(id); return; } // add a point of total and spendable XP
    if ( cmd == "region_setting" && llToLower(attrib) == "progression" ) { SET_PROGRESSION(sdata); return;}
    if ( cmd == "set_xp" ) { SET_XP(idata); return;}
    if ( cmd == "set_xplevel" ) { SET_XPLEVEL(idata); return;}
    if ( cmd == "set_gp" ) { SET_GP(idata); return;}
    if ( cmd == "set_statpool" ) { SET_STATPOOL(idata); return;}
    if ( cmd == "set_healthpool" ) { SET_HEALTHPOOL(idata); return;}
    if ( cmd == "set_skillpool" ) { SET_SKILLPOOL(idata); return;}
    if ( cmd == "set_sfxpool" ) { SET_EFFECTPOOL(idata); return;}
    if ( cmd == "skill_failed" ) { SKILL_FAILED(sdata); return; } // SKILL_FAILED|SKILL=name
    if ( cmd == "progress" ) { // remove me later
        if ( llToLower(attrib) == "xp" ) { SET_XP(idata); return;}
        if ( llToLower(attrib) == "xplevel" ) { SET_XPLEVEL(idata); return;}
        if ( llToLower(attrib) == "statpool" ) { SET_STATPOOL(idata); return; }
        if ( llToLower(attrib) == "healthpool" ) { SET_HEALTHPOOL(idata); return; }
        if ( llToLower(attrib) == "skillpool" ) { SET_SKILLPOOL(idata); return; }
        if ( llToLower(attrib) == "sfxpool" ) { SET_EFFECTPOOL(idata); return; }
        return;
    }
}
 
//
// 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_PROGRESS,"RPEVENT|"+rpevent,llGetOwner());
}
 
//
// SETUP - begin bringing the HUD online
//
SETUP() {
    XP_BY_LEVEL = [ 0,0,10,25,45,70,100,135,175,220,270,325,385,450,520,595,675,760,850,945,1045,1150,1260,1375,1495,1620,1750,1885,2025,2170,2320 ];
    OWNERSAY("Progress module active.");
}
 
//
// DEFAULT STATE
//
default {
 
    link_message(integer sender_num,integer sender,string message,key id) {
        if ( sender == MODULE_PROGRESS || sender == LM_SENDTOATTACHMENT ) return; // ignore link messages not sent to us specifically
 
        // First - handle type 1 messages that do not require breaking down
        list tokens = llParseString2List(message,["|"],[]);
        string cmd = llToLower(llStringTrim(llList2String(tokens,0),STRING_TRIM));
 
        if ( cmd == "debug" || cmd == "error" || cmd == "help" || cmd == "ownersay" || cmd == "rpevent" ) return; // ignore WELL commands
        // only debug when its module specific 
        DEBUG("EVENT: link_message("+(string)sender_num+","+(string)sender+","+message+","+(string)id+")");        
        PARSE(message,id); // parse incoming message
    } // end of link_message event
 
    // STATE ENTRY - called on Reset
    state_entry() {
        SETUP(); // show credits and start character sheet load
    }
} // end state
// END