User:Allen Kerensky/Myriad Lite Preview4/Myriad Lite v0.0.18 20110908

From Second Life Wiki
Jump to navigation Jump to search

Myriad Lite v0.0.18 20110908

<lsl> // Myriad Lite v0.0.18 20110908 // The Myriad RPG System was designed, written, and illustrated by Ashok Desai // Myriad RPG licensed under the Creative Commons Attribution 2.0 UK: England and Wales // http://creativecommons.org/licenses/by/2.0/uk/ // Myriad Lite software Copyright (c) 2011 by Allen Kerensky (OSG/SL) // Baroun's Adventure Machine Copyright (c) 2008-2011 by Baroun Tardis (SL) // Myriad Lite and Baroun's Adventure Machine licensed under the // Creative Commons Attribution-Share Alike-Non-Commercial 3.0 Unported // http://creativecommons.org/licenses/by-nc-sa/3.0/

// CONSTANTS - DO NOT CHANGE DURING RUN string VERSION = "0.0.18"; // Allen Kerensky's script version string VERSIONDATE = "20110908"; // Allen Kerensky's script yyyymmdd integer MINXP = 0; // min experience points integer MAXXP = 2320; // max experience points integer MINLEVEL = 1; // min XP level integer MAXLEVEL = 30; // max XP level integer MINSTAT = 1; // min value for statistics integer MAXSTAT = 5; // max human value for a statistic/attribute integer MINRESILIENCE = 1; // min value for resilience integer MAXRESILIENCE = 20; // max value for resilience integer MINBOON = 1; // min value for boon rank integer MAXBOON = 5; // max value for boon rank integer MINFLAW = 1; // min value for flaw rank integer MAXFLAW = 5; // max value for flaw rank integer MINSKILL = 1; // min value for skill rank integer MAXSKILL = 5; // max value for skill rank integer MINEFFECT = 1; // min value for special effect integer MAXEFFECT = 5; // max value for special effect integer MINEQUIPPED = 1; // min number of items player can carry integer MAXEQUIPPED = 100; // max number of items player can carry TODO: what about bullets? integer CHANMYRIAD = -999; // chat sent to ALL Myriad players in region integer CHANCOMMAND = 5; // chat sent by player to their meter integer MINDAMAGE = 1; // min attack dice for weapon integer MAXDAMAGE = 5; // max attack dice for weapon float RESPAWN_TIME = 30.0; // time dead before automatic respawn integer MINATTACH = 1; // min valid attach point integer MAXWEAR = 30; // max valid in-world wearable attach point integer MINARMOR = 1; // min armor defense value integer MAXARMOR = 5; // max armor defense value string DIV = "|"; // message field divider float WEAPON_LENGTH = 0.0; // weapon length in last attack float ARM_LENGTH = 1.0; // arm is 1m long float LEG_LENGTH = 1.5; // leg is 1.5m long integer MELEEATTACKDICE = 1; // 1 attack dice for fists and feet string ANIM_INCAPACITATED = "sleep"; // anim when incapacitated string ANIM_DEAD = "dead"; // anim when dead string ANIM_PUNCH_LEFT = "punch_l"; // anim for left punch string ANIM_PUNCH_RIGHT = "punch_r"; // anim for right punch string ANIM_PUNCH_ONETWO = "punch_onetwo"; // anim for 1-2 punch string ANIM_KICK = "kick_roundhouse_r"; // anim for kick integer SINGLE_PUNCH_DELAY = 1; // recovery time between single punches TODO fix to Myriad rules times integer DOUBLE_PUNCH_DELAY = 2; // recovery time between one-two punches TODO fix to Myriad rules times integer KICK_DELAY = 3; // recovery time between kicks TODO fix to Myriad rules times string CHAN_PREFIX = "0x"; // channel prefix for calculating dynamic channels

// string names for each attach point - waste of memory? list ATTACHPOINTS = ["INVALID","chest","head","left shoulder","right shoulder","left hand","right hand","left foot","right foot","back","pelvis","mouth","chin","left ear","right ear","left eye","right eye","nose","right upper arm","right lower arm","left upper arm","left lower arm","right hip","right upper leg","right lower leg","left hip","left upper leg","left lower leg","stomach","left pectoral","right pectoral","HUD Center 2","HUD Top Right","HUD Top","HUD Top Left","HUD Center","HUD Bottom Left","HUD Bottom","HUD Bottom Right" ];

// RUNTIME GLOBALS - CAN CHANGE DURING RUN integer FLAG_DEBUG = FALSE; // see debug messages? key PLAYERID = NULL_KEY; // cached player UUID string PLAYERNAME = ""; // cached player name string NAME = ""; // character name string SPECIES = ""; // species template used for character string BACKGROUND = ""; // background template string CAREER = ""; // career template integer XP = 0; // 0-2320 integer XPLEVEL = 1; // 1-30 integer POWER = 0; // physical strength and dexterity - offensive martial combat stat integer GRACE = 0; // athleticism and agility - defensive martial combat stat integer INTELLECT = 0; // quickness of thought, depth of knowledge - offensive social combat stat integer SPIRIT = 0; // willpower, confidence, perception, creativity - defensive social combat stat integer WOUNDS = 0; // healed wound value integer CURWOUNDS = 0; // current wound value integer CRITICAL = 0; // healed critical wounds value integer CURCRITICAL = 0; // current critical wound value integer RESOLVE = 0; // healed resolve value integer CURRESOLVE = 0; // current resolve value list BOONS = []; // boons [ string BoonName, integer BoonRank ] list FLAWS = []; // flaws [ string FlawName, integer FlawRank ] list SKILLS = []; // skills [ string SkillName, integer SkillRank ] list EFFECTS = []; // special effects (SFX) [ string EffectName, integer EffectRank ] list STUNTS = []; // pre-set martial combat stunts TODO how will this work? list QUOTES = []; // pre-set social combat quotes TODO how will this work? list EQUIPMENT = []; // Equipment [ string ItemName, integer NumberCarried ] string CARD = "Myriad Lite Character Sheet v0.0.2 20110521"; // character sheet notecard integer LINE = 0; // reading line number key QUERY = NULL_KEY; // track notecard queries integer HANDMYRIAD = 0; // Myriad channel handle integer CHANPLAYER = 0; // dynamic channel to one player's UUID integer HANDPLAYER = 0; // player channel handle integer CHANOBJECT = 0; // dynamic channel to one object's UUID integer HANDCOMMAND = 0; // command channel handle integer HANDATTACH = 0; // attachment channel handle integer CHANATTACH = 0; // dynamic channel for attachments integer CHANBAM = 0; // dynamic channel for BAM quests integer HANDBAM = 0; // BAM channel update integer FLAG_INCAPACITATED = FALSE; // incapacitated by wounds? integer FLAG_DEAD = FALSE; // killed by critical wounds? integer CURARMOR = 0; // highest armor value worn out of all armor worn, not a total integer FLAG_FISTS = FALSE; // using fist-fighter? integer FLAG_CONTROLS = FALSE; // permission to take controls? integer FLAG_ANIMATE = FALSE; // permission to animate avatar? integer TIME_NEXT_ATTACK = 0; // time of last attack integer CONTROLS = 0; // bitfield of controls to monitor float FIELD_OF_ATTACK = PI; // controls field of attack. set later to PI/6 later for 60 degree field of attack in front of avatar // list of armor values worn on each attach point list ARMOR = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; integer METERWORN = FALSE; // using meter?

// DEBUG - show debug chat with wearer name for sorting DEBUG(string dmessage) {

   if ( FLAG_DEBUG == TRUE ) { // are we debugging?
       llSay(DEBUG_CHANNEL,"DEBUG ("+llKey2Name(PLAYERID)+"): "+dmessage);
   }

}

// ERROR - show errors on debug channel with wearer name for sorting ERROR(string emessage) {

   llSay(DEBUG_CHANNEL,"ERROR ("+llKey2Name(PLAYERID)+"): "+emessage);

}

// WEARARMOR - Wearing a piece of armor WEARARMOR(integer waattachpoint,integer waamount,string waname) {

   if ( waattachpoint < MINATTACH || waattachpoint > MAXWEAR ) { // valid attach point?
       ERROR("Invalid armor attachment point "+llList2String(ATTACHPOINTS,waattachpoint));
       return;
   }
   if ( waamount < MINARMOR || waamount > MAXARMOR ) { // is armor rating valid or legal?
       ERROR("Invalid armor amount "+(string)waamount+" out of range "+(string)MINARMOR+"-"+(string)MAXARMOR);
       return;
   }
   // FIXME move ARMOR to 3-element strided list? [attachpoint,value,name?]
   ARMOR = llListReplaceList(ARMOR,[waamount],waattachpoint,waattachpoint); // insert armor value into armor list
   llOwnerSay("Armor "+waname+" ("+(string)waamount+") attached to "+llList2String(ATTACHPOINTS,waattachpoint));
   RECALCULATE_ARMOR(); // find new highest armor value

}

// REMOVEARMOR - Removing a piece of armor REMOVEARMOR(integer raattachpoint,integer raamount,string raname) {

   if ( raattachpoint < MINATTACH || raattachpoint > MAXWEAR ) { // valid attach point?
       ERROR("Invalid armor detachment point "+llList2String(ATTACHPOINTS,raattachpoint));
       return;
   }
   if ( raamount < MINARMOR || raamount > MAXARMOR ) { // is armor rating valid or legal?
       ERROR("Invalid armor amount "+(string)raamount+" out of range "+(string)MINARMOR+"-"+(string)MAXARMOR);
       return;
   }    
   ARMOR = llListReplaceList(ARMOR,[0],raattachpoint,raattachpoint); // zero out the armor value in armor list
   llOwnerSay("Armor "+raname+" ("+(string)raamount+") detached from "+llList2String(ATTACHPOINTS,raattachpoint));
   RECALCULATE_ARMOR(); // find new highest armor value

}

// RECALCULATE_ARMOR - sets CURARMOR to highest armor value worn after attach or detach RECALCULATE_ARMOR() {

   CURARMOR = 0; // start with zero armor
   integer racount = llGetListLength(ARMOR); // how long is armor list?
   while (racount--) { // look at each list item from last to first
       integer rapoints = llList2Integer(ARMOR,racount); // what is armor value at this point in list?
       if ( rapoints > CURARMOR ) { // is this armor value higher than current max?
           CURARMOR = rapoints; // yes, save new highest amount
       }
   }
   llOwnerSay("Armor Rating is "+(string)CURARMOR);
   // FIXME tell world too?

}

// HIT - player is hit - check to see if attack dice breach armor // Making A Damage Roll (Myriad p25, Myriad Special Edition p31) HIT(integer attackdice) {

   integer damagetaken = 0; // start with zero damage
   while(attackdice--) { // roll for each attack dice
       integer dieroll = 1+(integer)llFrand(5.0); // reasonably uniform d6
       if ( dieroll > CURARMOR ) { // attack roll stronger than armor worn?
           damagetaken++; // add a wound point
       }
   }
   // finished roll how did we do?
   if ( damagetaken > 0 ) { // we took damage
       if ( CURARMOR > 0 ) { // wearing armor? tell them it was breached
           llOwnerSay("That attack penetrated your armor and you've been wounded!");
           llWhisper(CHANATTACH,"ARMORHIT");
       } else { // fighting in no armor?
           llOwnerSay("You've been wounded! Wear some armor next time?");
       }
       WOUNDED(damagetaken); // apply damage taken to resilences
   } else { // hit, but no damage taken
       // must be wearing *some* armor to be hit but avoid a wound, don't recheck for armor here
       llOwnerSay("Your armor blocked the damage from that attack!");
       llWhisper(CHANATTACH,"ARMORBLOCKED");
   }

}

// WOUNDED - Player takes Resilience damage WOUNDED(integer amount) {

   while (amount--) { // for each wound taken
       if ( CURWOUNDS > 0 && CURCRITICAL == CRITICAL ) { // wound boxes left?
           CURWOUNDS--; // scratch off one
           METER(); // update
           llOwnerSay("You've been wounded!");
       } else if ( CURWOUNDS < 1 && CURCRITICAL > 0 ) { // incapacitated
           CURWOUNDS = 0; // force to zero
           CURCRITICAL--; // scratch off a critical wound box
           INCAPACITATED(); // show incapacitation
       } else if ( CURWOUNDS < 1 && CURCRITICAL < 1 ) { // out of critical wounds?
           CURWOUNDS = 0; // force zero
           CURCRITICAL = 0; // force zero
           METER(); // update
           DEAD(); // show death
       }
   } // end while

}

// INCAPACITATED - player lost all WOUNDS - unable to act INCAPACITATED() {

   FLAG_INCAPACITATED = TRUE; // yes, we're now incapacitated
   METER(); // update meter
   llStartAnimation(ANIM_INCAPACITATED); // "we're hurt and down" animation
   llRegionSay(CHANMYRIAD,PLAYERNAME+" has been incapacitated!");
   llOwnerSay("You've been incapacitated!");
   llSetTimerEvent(RESPAWN_TIME); // heal in a bit

}

// DEAD - player is dead, kill them and wait to respawn DEAD() {

   FLAG_DEAD = TRUE; // remember that we're now dead
   METER(); // update hover text
   llStartAnimation(ANIM_DEAD); // start dead animation
   llRegionSay(CHANMYRIAD,PLAYERNAME+" has been killed!");
   llOwnerSay("You've been killed!");
   llSetTimerEvent(RESPAWN_TIME); // respawn in a bit

}


// HEAL - restore lost WOUND and CRITICAL resilience HEAL(integer healamount) {

   // TODO report once for multiple healing amounts
   while ( healamount-- ) { // step through each point of healing
       if ( CURCRITICAL < CRITICAL ) { // heal critical wounds first?
           CURCRITICAL++; // add a point back
           DEBUG("Heal critical. Wounds: "+(string)CURWOUNDS+" of "+(string)WOUNDS+" wound boxes. Critical: "+(string)CURCRITICAL+" of "+(string)CRITICAL+" critical wound boxes.");
           METER(); // update
           if ( FLAG_DEAD == TRUE ) { // healed a critical, critical now > 0 so not dead anymore
               FLAG_DEAD=FALSE; // stop being dead
               if ( FLAG_ANIMATE == TRUE ) { // if we're allowed to change animations
                   llStopAnimation(ANIM_DEAD); // stop "we're dead" animation
               }
               llRegionSay(CHANMYRIAD,"RPEVENT"+DIV+NAME+" ("+PLAYERNAME+") has been resurrected!");
               llOwnerSay("Critical wound healed! You've been resurrected! Welcome back to the land of the living.");
               METER(); // update
           } else { // heal a critical wound without resurrect
               llOwnerSay("Critical wound healed.");
           }
       } else if ( CURWOUNDS < WOUNDS ) { // player not critical, heal non-critical?
           CURWOUNDS++; // add a point of non-critical
           DEBUG("Heal non-critical. Wounds: "+(string)CURWOUNDS+" of "+(string)WOUNDS+" wound boxes. Critical: "+(string)CURCRITICAL+" of "+(string)CRITICAL+" critical wound boxes.");
           METER(); // update
           if ( FLAG_INCAPACITATED == TRUE ) { // were they incapacitated?
               FLAG_INCAPACITATED=FALSE; // they have a wound point, no longer incapacitated
               if ( FLAG_ANIMATE == TRUE ) { // does player allow us to animate their avatar/character?
                   llStopAnimation(ANIM_INCAPACITATED); // stop the incapacitation animation
               }
               llRegionSay(CHANMYRIAD,"RPEVENT"+DIV+NAME+" ("+PLAYERNAME+") no longer incapacitated!");
               llOwnerSay("Non-critical wound healed. You are no longer incapacitated!");
               METER(); // update
           } else { // healed non-critical wound without need to stop incapacitation
               llOwnerSay("Non-critical wound healed.");
           }
       } // end if curwounds < wounds
   } // end while
   METER(); // update

}

// GET SKILL RANK // Requires a SKILL NAME // Returns the rank value for that skill, or zero if player doesn't have skill integer GET_SKILL_RANK(string askill) {

   integer atskill = 0; // start with skill zero in case character does not have that skill
   integer skillpos = llListFindList(SKILLS,[askill]); // position of skill name in list 0-n, or -1 if not found
   if ( skillpos >= 0 ) { // skill name was somewhere in list
       atskill = llList2Integer(SKILLS,++skillpos); // move to next pos in list after skillname and get skill rank there
       if ( atskill >= MINSKILL && atskill <= MAXSKILL ) return atskill; // found skill with value in range, return it;
   } // fall through...
   return 0; // player has zero levels in skill

}

// ABILITY TEST // Requires ATTRIBUTE NAME, SKILL NAME // Returns the ability test score for use by success fail, opposed rolls, etc // See Myriad PDF page 18, Myriad Special Edition page 24 integer ABILITY_TEST(integer attribute,integer skill) {

   integer highroll = 0; // clear out the highest roll
   while( attribute-- ) { // roll a dice for each point of the attribute
       integer roll = 1+(integer)llFrand(5.0); // roll this d6
       if ( roll > highroll) highroll = roll; // if this is highest roll so far, remember it
   } // finished rolling a dice for each point of the base attribute
   return highroll + skill; // now, return the total of highest dice roll + skill value

}

// An Unopposed Ability Test - Myriad PDF p. 19, Myriad Special Edition p. 25 // Requires TargetNumber, Attribute Name, Skill Name // Returns TRUE for Success and False for Fail integer UNOPPOSED_TEST(integer targetnum,integer tattribute,integer tskill ) {

   integer check = ABILITY_TEST(tattribute,tskill); // calculate the player's ability test value
   if ( check >= targetnum ) return TRUE; // player won the ability test
   return FALSE; // player lost the ability test

}

// 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

}

// SETUP - begin bringing the HUD online SETUP() {

   CREDITS(); // show Myriad credits as required by the Creative Commons - Attribution license
   PLAYERID = llGetOwner(); // remember the owner's UUID
   PLAYERNAME = llKey2Name(PLAYERID); // remember the owner's legacy name
   llSetText("",<0,0,0>,0); // clear any previous hovertext
   llOwnerSay("Loading character sheet. Please wait..."); // tell player we're waiting for data server
   if ( llGetInventoryName(INVENTORY_NOTECARD,0) == CARD ) { // check inventory for notecard
       QUERY = llGetNotecardLine(CARD,LINE++); // ask for line from notecard and advance to next line
   } else {
       ERROR("Cannot locate character sheet from notecard: "+CARD); // TODO: what next? choose defaults? fall over?
   }

}

// CREDITS comply with Myriad RPG Creative Common-Attribution legal requirement CREDITS() {

   llOwnerSay("The Myriad RPG System was designed, written, and illustrated by Ashok Desai.");
   llOwnerSay("RPG System licensed under the Creative Commons Attribution 2.0 UK: England and Wales.");
   llOwnerSay("Myriad Lite v"+VERSION+" "+VERSIONDATE+" Copyright (c) 2011 by Allen Kerensky (OSG/SL)");
   llOwnerSay("Licensed under Creative Commons Attribution-Share Alike-Non-Commercial 3.0 Unported.");

}

// RESET - shut down running animations then reset the script to reload character sheet RESET() {

   if ( FLAG_DEAD == TRUE || FLAG_INCAPACITATED == TRUE ) { // don't allow reset if already on respawn timer
       llOwnerSay("Cannot reset while incapacitated or dead. You will respawn in a few moments.");
       return;
   }
   llOwnerSay("Resetting Myriad Lite. Please wait...");
   // stop all running animations
   if ( FLAG_ANIMATE == TRUE ) { // do we have permission to animate?
       list anims = llGetAnimationList(PLAYERID); // 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
       }
   }
   llMessageLinked(LINK_THIS,0,"MODULERESET",NULL_KEY); // tell modules to reset too TODO function this
   // TODO can we do this with an attachment and have it do the attachment setup correctly?
   llResetScript(); // now reset

}

// METER - update a hovertext health meter or HUD bar graph METER() {

   if ( METERWORN == FALSE ) return;
   // create a meter message packet
   string message = "METER"+DIV+PLAYERNAME+DIV+NAME+DIV+(string)CURWOUNDS+DIV+(string)WOUNDS+DIV+(string)CURCRITICAL+DIV+(string)CRITICAL+DIV+(string)FLAG_DEAD+DIV+(string)FLAG_INCAPACITATED;
   llRegionSay(CHANMYRIAD,message); // send the update to region for scorekeepers, etc
   llWhisper(CHANATTACH,message); // whisper to the wearer's actual meter
   DEBUG("Wounds: "+(string)CURWOUNDS+" of "+(string)WOUNDS+" wound boxes. Critical: "+(string)CURCRITICAL+" of "+(string)CRITICAL+" critical wound boxes.");

}

// 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

}

// DEBUGON - turn on the DEBUG flag DEBUGON() {

   FLAG_DEBUG = TRUE; // set debug flag TRUE
   llOwnerSay("Debug Mode Activated");

}

// DEBUGOFF - turn off the DEBUG flag DEBUGOFF() {

   FLAG_DEBUG = FALSE; // set debug flag to FALSE
   llOwnerSay("Debug Mode Deactivated");

}

// COMBATOFF - turn off fist fighter COMBATOFF() {

   FLAG_FISTS = FALSE; // disable flag to exit/ignore any more control events
   if ( FLAG_CONTROLS == TRUE ) { // do we have control permission?
       llReleaseControls(); // release the controls
       FLAG_CONTROLS = FALSE; // remember that we released controls
   }
   llOwnerSay("Close Combat Deactivated");

}

// COMBATON - turn on fist fighter COMBATON() {

   FLAG_FISTS = TRUE; // yep, using fist fighter, for control events
   if ( FLAG_CONTROLS == FALSE ) { // do we have permission to read controls? No? we need it.
       llReleaseControls(); // release any previous controls on avatar
       llRequestPermissions(PLAYERID,PERMISSION_TAKE_CONTROLS|PERMISSION_TRIGGER_ANIMATION); // request permissions needed for fist fighter
   }             
   llOwnerSay("Close Combat Activated");

}

// COMMAND - process chat and link message commands together COMMAND(string command) {

   if ( command == "armoroff" ) { ARMOROFF(); return;} // turn off power armor
   if ( command == "armoron" ) { ARMORON(); return;} // turn on power armor
   if ( command == "checkammo" ) { CHECKAMMO(); return;} // check ammo in weapons
   if ( command == "checkarmor" ) { RECALCULATE_ARMOR(); return; } // check our current armor value
   if ( command == "checkbattery") { CHECKBATTERY(); return;}  // check power armor battery
   if ( command == "combatoff") { COMBATOFF(); return; } // turn off fist fighter
   if ( command == "combaton" ) { COMBATON(); return; } // turn on the fist fighter
   if ( command == "credits" ) { CREDITS(); return;} // show the credits including version number
   if ( command == "debugoff" ) { DEBUGOFF(); return; } // player turn off debugging
   if ( command == "debugon" ) { DEBUGON(); return;} // player turn on debugging
   if ( command == "drawboth" ) { DRAW("both"); return; } // draw both weapons
   if ( command == "drawleft" ) { DRAW("left"); return; } // draw weapon in left hand
   if ( command == "drawright" ) { DRAW("right"); return; } // draw weapon using right hand
   if ( command == "holsterboth" ) { HOLSTER("both"); return; } // holster both weapons
   if ( command == "holsterleft" ) { HOLSTER("left"); return; } // holster weapon in left hand
   if ( command == "holsterright" ) { HOLSTER("right"); return; } // holster weapon in right hand
   if ( command == "quest" ) { QUEST(); return; } // check our current quest status
   if ( command == "recharge" ) { RECHARGE(); return;} // recharge power armor battery
   if ( command == "reload" ) { RELOAD(); return;}  // reload weapons
   if ( command == "reset" ) { RESET(); return;} // reset HUD
   if ( command == "safetyoff" ) { SAFETYOFF(); return;} // unsafe the weapons
   if ( command == "safetyon" ) { SAFETYON(); return;} // safe the weapons
   if ( command == "sheatheboth" ) { SHEATHE("both"); return; } // sheathe both weapons
   if ( command == "sheatheleft" ) { SHEATHE("left"); return; } // sheathe weapon in left hand
   if ( command == "sheatheright" ) { SHEATHE("right"); return; } // sheathe weapon in right hand
   if ( command == "version" ) { CREDITS(); return;} // show the credits including version number

}

// QUEST STATUS QUEST() {

   llMessageLinked(LINK_THIS,0,"BAMSTATUS",PLAYERID); // send a status request to BAM Modules

}

// DRAW weapons DRAW(string hand) {

   if ( hand == "left" ) { llWhisper(CHANATTACH,"DRAWLEFT"); return; } // draw left-hand weapon
   if ( hand == "right" ) { llWhisper(CHANATTACH,"DRAWRIGHT"); return; } // draw right-hand weapon
   if ( hand == "both" ) { llWhisper(CHANATTACH,"DRAWBOTH"); return; } // draw both weapons

}

// SHEATHE weapons SHEATHE(string hand) {

   if ( hand == "left" ) { llWhisper(CHANATTACH,"SHEATHELEFT"); return; } // sheathe left-hand weapon
   if ( hand == "right" ) { llWhisper(CHANATTACH,"SHEATHERIGHT"); return; } // sheathe right-hand weapon
   if ( hand == "both" ) { llWhisper(CHANATTACH,"SHEATHEBOTH"); return; } // sheathe both weapons

}

// HOLSTER weapons HOLSTER(string hand) {

   if ( hand == "left" ) { llWhisper(CHANATTACH,"HOLSTERLEFT"); return; } // holster left-hand weapon
   if ( hand == "right" ) { llWhisper(CHANATTACH,"HOLSTERRIGHT"); return; } // holster right-hand weapon
   if ( hand == "both" ) { llWhisper(CHANATTACH,"HOLSTERBOTH"); return; } // holster both weapons

}

// CHECK AMMO CHECKAMMO() {

   llWhisper(CHANATTACH,"CHECKAMMO");

}

// RELOAD RELOAD() {

   llWhisper(CHANATTACH,"RELOAD");

}

// SAFETY ON SAFETYON() {

   llWhisper(CHANATTACH, "SAFETYON");

}

// SAFETY OFF SAFETYOFF() {

   llWhisper(CHANATTACH, "SAFETYOFF");

}

// ARMOR ON ARMORON() {

   llWhisper(CHANATTACH,"ARMORON");

}

// ARMOROFF ARMOROFF() {

   llWhisper(CHANATTACH,"ARMOROFF");

}

// CHECKBATTERY CHECKBATTERY() {

   llWhisper(CHANATTACH,"CHECKBATTERY");

}

// RECHARGE RECHARGE() {

   llWhisper(CHANATTACH,"RECHARGE");

}

// DEFAULT STATE - load character sheet default {

   // STATE ENTRY - called on Reset
   state_entry() {
       SETUP(); // show credits and start character sheet load
   }
   // on_rez - when rezzed to ground or from inventory as attachment during login
   on_rez(integer params) {
       RESET(); // force to go through state entry
   }
   // attach - when attached or detached from inventory or during login
   attach(key id) {
       RESET(); // force to go through state entry
   }
   // dataserver called for each line of notecard requested - process character sheet
   dataserver(key queryid,string data) {
       if ( queryid == QUERY ) { // ataserver gave us line we asked for?
           if ( data != EOF ) { // we're not at end of notecard file?
               if ( llGetSubString(data,0,0) == "#" ) { // does this line start with comment mark?
                   QUERY = llGetNotecardLine(CARD,LINE++); // ignore comment and ask for the next line
                   return;
               }
               // Parse non-comment lines in keyword = value[,value,...] format
               list FIELDS = llParseString2List(data,["="],[]); // break line of text into = delimited fields
               string CMD = llStringTrim(llList2String(FIELDS,0),STRING_TRIM); // field zero is the "command"
               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 ( CMD == "NAME" )        { NAME = DATA; } // TODO verify names are appropriate
               if ( CMD == "SPECIES" )     { SPECIES = DATA; } // TODO verify valid species template name
               if ( CMD == "BACKGROUND" ) { BACKGROUND = DATA; } // TODO verify valid background template name
               if ( CMD == "CAREER" )     { CAREER = DATA; } // TODO verify valid career template name
               if ( CMD == "XP" ) {
                   integer amount = (integer)DATA; // convert to number
                   if ( amount >= MINXP && amount <= MAXXP ) { // XP valid?
                       XP = amount; // store it
                   } else { // invalid, report it
                       ERROR("XP amount out of allowed range: "+(string)MINXP+"-"+(string)MAXXP);
                   }
               }
               if ( CMD == "XPLEVEL" ) {
                   integer amount = (integer)DATA; // convert to number
                   if ( amount >= MINLEVEL && amount <= MAXLEVEL ) { // XPLEVEL valid?
                       XPLEVEL = amount; // store it
                   } else { // invalid, report it
                       ERROR("XPLEVEL amount out of allowed range: "+(string)MINLEVEL+"-"+(string)MAXLEVEL);
                   }
               }
               if ( CMD == "POWER" ) { // TODO fix hardcoded power
                   integer amount = (integer)DATA; // convert to number
                   if ( amount >= MINSTAT && amount <= MAXSTAT ) { // stat valid?
                       POWER = amount; // store it
                   } else { // invalid, report it
                       ERROR("POWER amount out of allowed range: "+(string)MINSTAT+"-"+(string)MAXSTAT);
                   }
               }
               if ( CMD == "GRACE" ) { // TODO fix hardcoded grace
                   integer amount = (integer)DATA; // convert to number
                   if ( amount >= MINSTAT && amount <= MAXSTAT ) { // stat valid?
                       GRACE = amount; // store it
                   } else { // invalid, report it
                       ERROR("GRACE amount out of allowed range: "+(string)MINSTAT+"-"+(string)MAXSTAT);
                   }
               }
               if ( CMD == "INTELLECT" ) { // TODO fix hardcoded intellect
                   integer amount = (integer)DATA; // convert to number
                   if ( amount >= MINSTAT && amount <= MAXSTAT ) { // stat valid?
                       INTELLECT = amount; // store it
                   } else { // invalid, report it
                       ERROR("INTELLECT amount out of allowed range: "+(string)MINSTAT+"-"+(string)MAXSTAT);
                   }
               }
               if ( CMD == "SPIRIT" ) { // TODO fix hardcoded spirit
                   integer amount = (integer)DATA; // convert to number
                   if ( amount >= MINSTAT && amount <= MAXSTAT ) { // stat valid?
                       SPIRIT = amount; // store it
                   } else { // invalid, report it
                       ERROR("SPIRIT amount out of allowed range: "+(string)MINSTAT+"-"+(string)MAXSTAT);
                   }
               }
               if ( CMD == "WOUNDS" ) { // TODO fix hardcoded resilience
                   integer amount = (integer)DATA; // convert to number
                   if ( amount >= MINRESILIENCE && amount <= MAXRESILIENCE ) { // resilience valid?
                       WOUNDS = amount; // store it
                       CURWOUNDS = amount; // set current wounds to max too.
                   } else { // invalid, report it
                       ERROR("WOUNDS amount out of allowed range: "+(string)MINRESILIENCE+"-"+(string)MAXRESILIENCE);
                   }
               }
               if ( CMD == "CRITICAL" ) { // TODO fix hardcoded resilience
                   integer amount = (integer)DATA; // convert to number
                   if ( amount >= MINRESILIENCE && amount <= MAXRESILIENCE ) { // resilience valid?
                       CRITICAL = amount; // store it
                       CURCRITICAL = amount; // set the current critical wound to max too
                   } else { // invalid, report it
                       ERROR("CRITICAL amount out of allowed range: "+(string)MINRESILIENCE+"-"+(string)MAXRESILIENCE);
                   }
               }
               if ( CMD == "RESOLVE" ) { // TODO fix hardcoded resilience
                   integer amount = (integer)DATA; // convert to number
                   if ( amount >= MINRESILIENCE && amount <= MAXRESILIENCE ) { // resilience valid?
                       RESOLVE = amount; // store it
                       CURRESOLVE = amount; // set current resolve to max too
                   } else { // invalid, report it
                       ERROR("RESOLVE amount out of allowed range: "+(string)MINRESILIENCE+"-"+(string)MAXRESILIENCE);
                   }
               }
               if ( CMD == "BOON" ) {
                   string boonname = llList2String(SUBFIELDS,0); // find the boon name
                   integer boonrank = llList2Integer(SUBFIELDS,1); // find the boon rank value
                   // TODO how to verify boon names are valid?
                   if ( boonrank >= MINBOON && boonrank <= MAXBOON ) { // rank valid?
                       BOONS = [boonname,boonrank] + BOONS; // add boon to list
                   } else { // invalid, report it
                       ERROR("BOON "+boonname+" rank "+(string)boonrank+" value out of allowed range: "+(string)MINBOON+"-"+(string)MAXBOON);
                   }
               }
               if ( CMD == "FLAW" ) {
                   string flawname = llList2String(SUBFIELDS,0); // find the flaw name
                   integer flawrank = llList2Integer(SUBFIELDS,1); // find the flaw rank value
                   // TODO how to verify flaw names are valid?
                   if ( flawrank >= MINFLAW && flawrank <= MAXFLAW ) { // rank valid?
                       FLAWS = [flawname,flawrank] + FLAWS; // add flaw to list
                   } else { // invalid, report it
                       ERROR("FLAW "+flawname+" rank "+(string)flawrank+" value out of allowed range: "+(string)MINFLAW+"-"+(string)MAXFLAW);
                   }
               }
               if ( CMD == "SKILL" ) {
                   string skillname = llList2String(SUBFIELDS,0); // find the skill name
                   integer skillrank = llList2Integer(SUBFIELDS,1); // find the skill rank
                   // TODO how to verify skill names are valid?
                   if ( skillrank >= MINSKILL && skillrank <= MAXSKILL ) { // skill rank valid?
                       SKILLS = [skillname,skillrank] + SKILLS; // add skill to list
                   } else { // invalid, report it
                       ERROR("SKILL "+skillname+" rank "+(string)skillrank+" value out of allowed range: "+(string)MINSKILL+"-"+(string)MAXSKILL);
                   }
               }
               if ( CMD == "EFFECT" ) {
                   string effectname = llList2String(SUBFIELDS,0); // find effect name
                   integer effectrank = llList2Integer(SUBFIELDS,1); // find effect rank
                   // TODO how to verify effect name?
                   if ( effectrank >= MINEFFECT && effectrank <= MAXEFFECT ) { // effect rank valid?
                       EFFECTS = [effectname,effectrank] + EFFECTS; // add effect to list
                   } else { // invalid, report it
                       ERROR("EFFECT "+effectname+" rank "+(string)effectrank+" value out of allowed range: "+(string)MINEFFECT+"-"+(string)MAXEFFECT);
                   }
               }
               if ( CMD == "STUNT" ) { // TODO how to handle stunts with commas?
                   string stuntname = llList2String(SUBFIELDS,0); // find stunt
                   // TODO how to verify stunt?
                   STUNTS = [stuntname] + STUNTS; // add stunt to list
               }
               if ( CMD == "QUOTE" ) { // TODO how to handle quotes with commas?
                   string quotename = llList2String(SUBFIELDS,0); // find quote
                   QUOTES = [quotename] + QUOTES; // add quote to list
               }
               if ( CMD == "EQUIPMENT" ) {
                   string equipmentname = llList2String(SUBFIELDS,0); // find equipment note
                   integer equipmentamount = llList2Integer(SUBFIELDS,1); // find equipment count
                   // TODO how to verify the equipment name is valid?
                   if ( equipmentamount >= MINEQUIPPED && equipmentamount <= MAXEQUIPPED ) { // amount valid?
                       EQUIPMENT = [equipmentname,equipmentamount] + EQUIPMENT; // add equipment to list
                   } else { // invalid, report it
                       ERROR("EQUIPMENT "+equipmentname+" amount "+(string)equipmentamount+" value out of allowed range: "+(string)MINEQUIPPED+"-"+(string)MAXEQUIPPED);
                   }
               }
               QUERY = llGetNotecardLine(CARD,LINE++); // finished with known keywords, get next line
           } else { // end of notecard
               // TODO how to verify entire character sheet was completed and loaded?
               state running; // we're out of notecard, so character sheet is loaded - start playing
           } // end if data not equal eof
       } // end if query id equal
   } // end if data server event

} // end default state

// STATE RUNNING - character sheet loaded - player is active in the game state running {

   // STATE ENTRY
   state_entry() {
       llOwnerSay("Character Sheet loaded. You are now ready to roleplay.");
       HANDMYRIAD = llListen(CHANMYRIAD,"",NULL_KEY,""); // setup listener for Myriad RP events
       HANDCOMMAND = llListen(CHANCOMMAND,"",PLAYERID,""); // listen to chat commands from owner
       CHANPLAYER = (integer)("0x"+llGetSubString((string)PLAYERID,0,6)); // calculate a player-specfic dynamic chat channel
       HANDPLAYER = llListen(CHANPLAYER,"",NULL_KEY,""); // listen on the player dynamic chat channel
       CHANATTACH = (integer)("0x"+llGetSubString((string)PLAYERID,1,7)); // attachment-specific channel
       HANDATTACH = llListen(CHANATTACH,"",NULL_KEY,""); // listen for messages from attachments
       CHANBAM = (integer)(CHAN_PREFIX + llGetSubString((string)PLAYERID,-7,-1));
       HANDBAM = llListen(CHANBAM,"",NULL_KEY,""); // start listener with listenremove handle
       // 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
       llOwnerSay("Registering any Myriad Lite-compatible attachments...");
       llWhisper(CHANATTACH,"REGISTERATTACHMENTS"); // ask for attachments on their dynamic channel
        // calculate player's dynamic BAM channel
       METER(); // update hovertext
       QUEST(); // update the BAM Module
   }
   // ON_REZ - logged in with meter, or worn from inventory while running
   on_rez(integer rezparam) {
       RESET(); // a reset to reload character
   }
   // ATTACH - logged in with meter or worn from inventory/ground while running
   attach(key id) {
       RESET(); // a reset to reload character
   }
   // CHANGED - triggered for many changes to the avatar
   // TODO reload sim-specific settings on region change
   changed(integer changes) {
       if ( changes & CHANGED_INVENTORY ) { // inventory changed somehow?
           llOwnerSay("Inventory changed. Reloading.");
           RESET(); // saved a new character sheet? - reset and re-read it.
       }
       if ( changes & CHANGED_REGION || changes & CHANGED_TELEPORT ) {
           METER(); // update the meter after a shift
       }
   }
   // 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
           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?
           FLAG_ANIMATE = TRUE; // remember that we got permission for this
       }
   }
   // TOUCH_START - touch HUD for adventure update
   touch_start(integer total_number) {
       string action = llGetLinkName(llDetectedLinkNumber(0)); // get name of prim clicked in link set
       if ( action != "" && action != llGetObjectName() ) { // someone clicked a named button prim on this linkset
           COMMAND(action); // try that prim name as a command
           return;
       }
       METER();        
   }
   // CONTROL - read arrow keys and mouse button in first or third person mode
   control(key id,integer level,integer edge) {
       if ( FLAG_DEAD == TRUE || FLAG_INCAPACITATED == TRUE ) return; // dead or incapacitated can't fight
       if ( FLAG_FISTS == FALSE ) return; // not using fist fighter
       if ( 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
   // 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_RANK("Close Combat"); // get our close combat skill rank
           integer victimchan = (integer)("0x"+llGetSubString(hitwho,0,6)); // calculate dynamic channel of who we hit
           llRegionSay(CHANMYRIAD,"RPEVENT"+DIV+llKey2Name(PLAYERID)+" 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"+DIV+(string)POWER+DIV+(string)attskill+DIV+(string)MELEEATTACKDICE+DIV+(string)PLAYERID+DIV+"fists and feet");
           llOwnerSay("You struck at "+name+" in Close Combat");
       } // end while
   } // end sensor
   // 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
   }
   // TIMER - scheduled events
   timer() {
       // Respawn timer ended
       if ( FLAG_DEAD == TRUE ) { // if dead
           llRegionSay(CHANMYRIAD,"RPEVENT"+DIV+llKey2Name(PLAYERID)+" respawns!");
           RESET(); // reset and reload character
       }
       if ( FLAG_INCAPACITATED == TRUE ) { // if hurt
           HEAL(1); // heal 1 wound
       }
       if ( CURWOUNDS == WOUNDS ) { // fully healed?
           llSetTimerEvent(0.0); // stop timer
       }
   }
   
   // LINK MESSAGE - commands to and from other prims in HUD
   link_message(integer sender,integer num,string cmd, key id) {
       COMMAND(cmd); // send to shared command processor for chat and link messages
       return;
   }
   
   // LISTEN - the main Myriad Lite message processor for RP events and player commands
   listen(integer channel, string speakername, key speakerid, string message) {
       // calculate the dynamic channel of who is speaking in case we need to return commands
       CHANOBJECT = (integer)(CHAN_PREFIX+llGetSubString((string)speakerid,0,6));
       // break down the commands and messages into units we can work with
       list fields = llParseString2List(message,[DIV],[]); // break into list of fields based on DIVider
       string command = llList2String(fields,0); // assume the first field is a Myriad Lite command
       // --- PLAYER COMMAND CHANNEL
       if ( channel == CHANCOMMAND ) { // handle player chat commands
           COMMAND(command); // send to shared command processor for chat and link messages
           return;
       } // end of if channel == player commands
       // --- BAM CHANNEL
       if ( channel == CHANBAM ) {
           llMessageLinked(LINK_THIS,channel,message,speakerid); // send BAM to Module  
           return;
       } // end if channel BAMCHAN
       // --- Myriad Lite regionwide messages
       if ( channel == CHANMYRIAD ) { // handle Myriad system messages
           if ( command == "RPEVENT" ) { // Myriad Lite RPEVENT - roleplay events everyone might find interesting
               string oldname = llGetObjectName(); // save the current object name
               llSetObjectName("Myriad RP Event"); // change the object name to
               llOwnerSay(llList2String(fields,1)); // now tell the owner the rest of the RPEVENT| message
               llSetObjectName(oldname); // restore the HUD back to its original name
               return;
           } // end if RPEVENT
           return;
       } // end if channel == CHANMYRIAD
       // --- ATTACHMENT CHANNEL
       if ( channel == CHANATTACH ) { // handle the attachment commands
           if ( FLAG_DEAD == TRUE || FLAG_INCAPACITATED == TRUE ) return; // can't mess with attachments while down
           if ( command == "ATTACHARMOR" ) { // player attached armor somewhere
               integer armorrating = llList2Integer(fields,1); // get armor value
               integer attachpoint = llList2Integer(fields,2); // get armor location
               string armorname = llList2String(fields,3); // get armor's name
               WEARARMOR(attachpoint,armorrating,armorname); // add armor to set of armor worn
               return;                
           }
           if ( command == "DETACHARMOR" ) { // player attached armor somewhere
               integer armorrating = llList2Integer(fields,1); // get armor value
               integer attachpoint = llList2Integer(fields,2); // get armor location
               string armorname = llList2String(fields,3); // get armor's name
               REMOVEARMOR(attachpoint,armorrating,armorname); // detach armor from set of armor worn
               return;
           }
           if ( command == "ATTACHMELEE" || command == "ATTACHRANGED" ) { // holding a weapon rather than using fists?
               FLAG_FISTS = FALSE; // turn off fist fighter
               if ( FLAG_CONTROLS == TRUE ) { // if we own controls...
                   llReleaseControls(); // release them
                   FLAG_CONTROLS = FALSE; // and remember
               }
               return;
           }
           if ( command == "DETACHMELEE" || command == "DETACHRANGED" ) { // are we going back to fists?
               FLAG_FISTS = TRUE; // turn fist fighter back on
               if ( FLAG_CONTROLS == FALSE ) { // if we don't have controls
                   llReleaseControls(); // release them just in case
                   llRequestPermissions(PLAYERID,PERMISSION_TAKE_CONTROLS|PERMISSION_TRIGGER_ANIMATION); // then ask to take controls
               }
               return;
           }
           if ( command == "ATTACHMETER" ) {
               METERWORN = TRUE; // we need to send meter events
               METER(); // send update
               return;
           }
           if ( command == "DETACHMETER" ) {
               METERWORN = FALSE;
               return;
           }
       }
       // --- CHANPLAYER
       if ( channel == CHANPLAYER ) { // handle player dynamic commands
           // we've been hit and have to make an opposed ability test to avoid it
           if ( command == "HITCHECK" || command == "RANGEDHIT" || 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 < MINSTAT || attackstat > MAXSTAT ) { // is the attack stat value out of allowed range?
                   ERROR("Attack stat value "+(string)attackstat+" out of range: "+(string)MINSTAT+"-"+(string)MAXSTAT);
                   // TODO make a tattletale RP event?
                   return;
               }
               if ( attackskill < MINSKILL || attackstat > MAXSKILL ) { // is the attack skill value out of allowed range?
                   ERROR("Attack skill value "+(string)attackskill+" out of range: "+(string)MINSKILL+"-"+(string)MAXSKILL);
                   // TODO make a tattletale RP event?
                   return;
               }             
               if ( attackdice < MINDAMAGE || attackdice > MAXDAMAGE ) { // is the attacking weapon's attack dice value out of allowed range?
                   ERROR("Attack dice value out of range: "+(string)MINDAMAGE+"-"+(string)MAXDAMAGE);
                   // TODO make a tattletale RP event?
                   return;
               }
               integer skillamount = 0; // create a place to hold the defenders mortal combat skill rank
               if ( command == "HITCHECK" || command == "RANGEDHIT" ) { // if this is ranged combat
                   skillamount = GET_SKILL_RANK("Ranged Combat"); // get ranged combat skill rank
               }
               if ( command == "CLOSEHIT" ) { // if this is close combat
                   skillamount = GET_SKILL_RANK("Close Combat"); // get close combat skill rank
               }
               // see if we're hit
               integer amihit = OPPOSED_TEST(attackstat,attackskill,GRACE,skillamount); // attacker power+skill vs. defender grace+skill
               if ( amihit == TRUE ) { // we're hit!
                   if ( command == "HITCHECK" || command == "RANGEDHIT" ) { // we're hit by ranged attack
                       llOwnerSay("You've been hit in ranged combat by "+llKey2Name(owner)+"'s "+item+"!");
                   }
                   if ( command == "CLOSEHIT" ) { // we're hit by melee or hand to hand attack
                       llOwnerSay("You been hit in close combat by "+llKey2Name(owner)+"!");
                   }
                   HIT(attackdice); // apply the hit
               }
               return;
           }
           // Heal Some Damage
           if ( command == "HEALPARTIAL" ) { // only a partial heal
               integer boxeshealed = llList2Integer(fields,1); // how many boxes are we healing?
               HEAL(boxeshealed); // heal X number of boxes
               llOwnerSay("Partially healed. Wounds: "+(string)CURWOUNDS+" of "+(string)WOUNDS+" wound boxes. Critical: "+(string)CURCRITICAL+" of "+(string)CRITICAL+" critical wound boxes.");
               METER(); // update
               return;
           }
           if ( command == "HEALFULL" ) { // full heal, reset state
               CURWOUNDS = WOUNDS; // restore wound boxes
               CURCRITICAL = CRITICAL; // restore critical wound boxes
               FLAG_DEAD = FALSE; // no longer dead
               FLAG_INCAPACITATED = FALSE; // no longer incapacitated
               if ( FLAG_ANIMATE == TRUE ) { // if we're running an animation
                   llStopAnimation(ANIM_DEAD); // stop the dead animate
                   llStopAnimation(ANIM_INCAPACITATED); // stop the incapacitate animate
               }
               llOwnerSay("Fully healed! Wounds: "+(string)CURWOUNDS+" of "+(string)WOUNDS+" wound boxes. Critical: "+(string)CURCRITICAL+" of "+(string)CRITICAL+" critical wound boxes.");
               METER(); // update
               return;
           }
           // Actions NOT Allowed When Dead/Incapacitated go below here
           if ( FLAG_DEAD == TRUE || FLAG_INCAPACITATED == TRUE ) return;
           // If Your Bullet has hit, fire a hitcheck regionwide at targetplayer's channel
           if ( command == "CLOSECOMBAT" || command == "RANGEDCOMBAT" || command == "TOHIT" ) {
               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
               if ( command == "RANGEDCOMBAT" || command == "TOHIT" ) { // if this is a ranged attack
                   attskill = GET_SKILL_RANK("Ranged Combat"); // get ranged combat skill level
                   llRegionSay(victimchan,"RANGEDHIT"+DIV+(string)POWER+DIV+(string)attskill+DIV+(string)attdice+DIV+bywho+DIV+bywhat); // attack!
                   return;
               }
               if ( command == "CLOSECOMBAT" ) { // if this is melee or hand to hand
                   attskill = GET_SKILL_RANK("Close Combat"); // get close combat skill level
                   llRegionSay(victimchan,"CLOSEHIT"+DIV+(string)POWER+DIV+(string)attskill+DIV+(string)attdice+DIV+bywho+DIV+bywhat); // attack!
                   return;
               }
               return;
           } // end if CLOSECOMBAT/RANGEDCOMBAT/TOHID
       } // end if channel CHANPLAYER
   } // end listen

} // end state running // END </lsl>