User:Allen Kerensky/Myriad Lite Preview 5/Myriad Lite

From OpenSimulator

< User:Allen Kerensky | Myriad Lite Preview 5(Difference between revisions)
Jump to: navigation, search
(created)

Revision as of 08:02, 6 February 2012

Contents

Myriad Lite

Quick Start

  • (required) Wear the HUD.
  • (optional) Wear the hovertext meter
  • (optional) Wear armor, which you should see reported in your chat window.
  • (optional) Wear a holster
  • (optional) Wear a melee or ranged combat weapon

Character Creator

Region owners may install and configure a custom Character Creator to make character sheets specific for their region.

To use the Character Creator:

  1. Stand on the pose platform.
  2. Use the NEXT and PREV CATEGORY buttons to choose a part of your character sheet to edit.
  3. Use the NEXT and PREV ITEM buttons to choose a specific item in the current category.
  4. Use the BUY ITEM button to buy that item and add it to your character sheet, with points from the current point pool.
  5. Use the SELL ITEM button to return a purchased item, remove it from your character sheet, and return the points to the current point pool.
  6. When your points are spent, press SAVE to get a print out of your character sheet in the chat window.
  7. Use the Stand button to get up from the Character Creator.
  8. Use CTRL+H to open the chat history window if needed, and use CUT and PASTE to copy your character sheet text to a notecard.
  9. Copy the notecard into your Myriad Lite HUD.
  10. Use the /5 reset command in local chat to reset your Myriad Lite HUD and reload your character sheet.

Commands

Myriad Lite accepts a variety of chat commands on channel 5.

Many commands are detailed below in context, and more are described in the additional documentation below as well.

/5 <command> will activate that command.

Scripters can also send the same commands as a link message.

Custom HUDs can be constructed with buttons named each command, and the button will activate the command of the same name

You can send commands to Myriad Lite on chat channel 5.

  • /5 armoroff - turn off powered armor to save battery
  • /5 armoron - turn on powered armor
  • /5 checkammo - check the ammo left in your firearm
  • /5 checkarmor - check what amount of armor you currently have on
  • /5 checkbattery - check the battery level in powered armor
  • /5 combatoff - disable the built in fist fighter
  • /5 combaton - enable the built in fist fighter
  • /5 credits - credit where credit is due
  • /5 debugoff - turn off debugging messages if showing
  • /5 debugon - turn on debug messages
  • /5 drawboth - draw weapons in both hands if attached
  • /5 drawleft - draw a weapon in your left hand, if attached
  • /5 drawright - draw a weapon in your right hand, if attached
  • /5 holsterboth - holster the firearms in both hands, if attached
  • /5 holsterleft - holster the firearm in your left hand, if attached
  • /5 holsterright - holster the firearm in your right hand, if attached
  • /5 quest - see your current status in your current quest
  • /5 recharge - recharge the batteries in powered armor
  • /5 reload - reload your firearm
  • /5 reset - reset your meter and reload your character sheet
  • /5 safetyoff - unsafe your firearm
  • /5 safetyon - safe your firearm
  • /5 sheatheboth - sheathe the melee weapons in both hands, if attached
  • /5 sheatheleft - sheathe the melee weapon in your left hand, if attached
  • /5 sheatheright - sheathe the melee weapon in your right hand, if attached
  • /5 version - show the credits which includes the HUD version number and date

Martial Combat: Armor

Armor is always useful to deflect or absorb kinetic impacts.

Some armor is static, just a simple object like a medival shield or bulletproof vest.

In science-fiction, armor may be computerized or formed from an electronic field.

Myriad Lite calls armor that requires electricity to run "power armor"

For non-powered armor, simply wear the item and you should see the armor value reported.

  • /5 checkarmor to see what your current armor value is

The following commands let you control power armor:

  • /5 armoron to activate power armor.
  • /5 armoroff to deactivate power armor and save the battery
  • /5 recharge to recharge a power armor battery

Close Combat: Hand-To-Hand Fighting

For Hand-to-Hand combat, do not attach another weapon.

Say: /5 combaton in main chat to activate the built in "fist fighter"

In mouselook OR third person view, hold the left mouse button down, to start your attack, then press the following keys to actually attack

  • Up Arrow: one-two punch
  • Left Arrow: left-hand punch
  • Right-Arrow: right-hand punch
  • Down-Arrow: roundhouse kick

Say: /5 combatoff to disable the fist fighter mode

Close Combat: Melee Weapons

For melee weapon (knife, sword, club, etc)

Wear the melee weapon, which disables fist fighter mode if active

NOTE: The melee weapon is invisible when first attached, as if it is "sheathed".

Say /5 drawright or /5 drawleft in main chat to draw the weapon.

In mouselook or third person view, press left mouse button to trigger the weapon's attack.

Ranged Combat: Firearms

Wear a firearm with a Myriad bullet in it.

NOTE: The melee weapon is invisible when first attached, as if it is "sheathed".

Say /5 drawright or /5 drawleft in main chat to draw the weapon.

Go into mouselook for first person view

Aim.

Press left mouse button to fire.

Ranged combat does not work in third-person view.

/5 checkammo will show ammo count remaining.

/5 reload will reload ammo

Holstering a weapon

  • /5 holsterright
  • /5 holsterleft
  • /5 holsterboth

Quests

Thanks to a mighty contribution from Baroun Tardis, Myriad Lite also includes Baroun Tardis' Adventure Machine (BAM) v1.

Sim owners can create objects using BAM allowing you to find and participate in scavenger hunt-style "quests", "adventures", or "crusades" in their regions.

To go on a quest, you find an Non-Player Character (NPC) in the sim who will give you the quest, and the first task to complete, along with a hint. Complete each task in the quest, possibly earning prizes or necessary support equipment along the way. Once you have completed the quest tasks, return to the NPC to complete the quest itself, and possibly earn a prize.

You should get the first 'task' for the adventure, which will show what your goal and a hint on how to obtain it.

If you should lose track of what you're doing, touch the HUD on your screen, and it will give you a quick update on the status of your adventure.

If you remove your HUD, it will reset and clear the current adventure.

This will cause you to lose all goals you have achieved, and reset you to "no adventure in progress"

Adventures are like a scavenger hunt: You go after certain goals, and when you get all of them , you win the main prize at the end.

Say /5 quest or click the HUD attachment will show you your current quest status.

There are four basic types of goals in the BAM system:

1) Adventure Giver : if you're reading this, you've already found one.... These start adventures, and can also be the end point of one. Usually, some small prize is given out when you complete the adventure. These have a text floating over their head, indicating their name, such as "Baker" or "Clerk"

2) Collision Goal: These occupy an area, and when you run into it or step on it while looking for that area, it lets you know you've reached it, and gives you a clue for the next step in the adventure. These have no floating text, since that would be too much of a give away that this is the spot (grin)

3) Location Goal: These scan an area, and when you enter it while looking for that area, let you know you've reached it, and give you a clue for the next step in the adventure. These have no floating text, since that would be too much of a give away that this is the spot (grin)

4) Touch Goal : These sit still, waiting, until you touch them. If they're not the current goal for you in the adventure, then nothing happens. If they _are_ your current goal, then the victory music plays and you get clue/hint for the next step. These have a text floating over them, indicating their name.

If you have problems with an adventure, please contact the region owner of the region where you found the quest.

Heads Up Display (HUD)

The HUD is the core of the game system and is the only "mandatory" piece you need to play the game.

  1. Create a simple cube
    1. Size the cube to 0.250 meters for X,Y, and Z
    2. Apply the Myriad logo to it
    3. Set the cube to 50% transparent.
  2. Drag and Drop the following pieces from inventory into the cube:
    1. The default character sheet notecard
    2. The Myriad Lite Module Armor script
    3. The Myriad Lite Module BAM script
    4. The Myriad Lite script (below) itself (Must be compiled as Mono due to size)
  3. Edit the character sheet notecard to set a default character name for yourself at the top.
  4. Take the cube into inventory
  5. Right-click the cube in inventory and choose the "Attach to HUD -> Bottom Left" attachment point.

You should see Myriad Lite begin loading your character from the character sheet and tell you when its ready to play.

  1. Edit the HUD attachment to position it.
  2. Detach the HUD back to inventory to "save" the position.

Myriad_Lite-v0.1.0-20120125.lsl

// Myriad Lite v0.1.0 20120101
// Copyright (c) 2012 By Allen Kerensky (OSG/SL)
// 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-2012 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/
// You must agree to the terms of this license before making any use of this software.
// If you do not agree to this license, simply delete these materials.
// There is no warranty, express or implied, for your use of these materials.

// CONSTANTS - DO NOT CHANGE DURING RUN
string VERSION = "0.1.0"; // Allen Kerensky's script version
string VERSIONDATE = "20120101"; // 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
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

integer LM_ATTACHARMOR   = 0x80000000;
integer LM_DETACHARMOR   = 0x80000001;
integer LM_ARMORCURRENT  = 0x80000002;
integer LM_ARMORRESET    = 0x80000003;
integer LM_ARMORON       = 0x80000004;
integer LM_ARMOROFF      = 0x80000005;
integer LM_ARMORBATTERY  = 0x80000006;
integer LM_ARMORRECHARGE = 0x80000007;
integer LM_ARMORCHECK    = 0x80000008;
integer MAXARMOR = 5; // max legal armor rating

// 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
list STATISTICS = [];
list RESILIENCES = [];
list CURRENT_RESILIENCES = [];
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.3-20120101.txt"; // 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?
vector  MOVELOCK; // movelock position when incapacitated or dead
float   TAU = 0.5; // movelock tau
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
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);
}

RPEVENT(string rpevent) {
    llRegionSay(CHANMYRIAD,"RPEVENT|"+NAME+" ("+PLAYERNAME+") "+rpevent);
}

integer GETSTAT(string name) {
    integer pos = llListFindList(STATISTICS,[name]);
    if ( pos >= 0 ) {
        return llList2Integer(STATISTICS,pos + 1);
    }
    return 0;
}

integer GET_RESILIENCE(string name) {
    integer pos = llListFindList(CURRENT_RESILIENCES,[name]);
    if ( pos >= 0 ) {
        return llList2Integer(CURRENT_RESILIENCES,pos + 1);
    }
    return 0;
}

integer GET_MAX_RESILIENCE(string name) {
    integer pos = llListFindList(RESILIENCES,[name]);
    if ( pos >= 0 ) {
        return llList2Integer(RESILIENCES,pos + 1);
    }
    return 0;
}

SET_RESILIENCE(string name,integer value) {
    if ( value < 0 ) { return;} // out of range
    if ( value > 20 ) { return; } // out of range
    integer curpos = llListFindList(CURRENT_RESILIENCES,[name]);
    integer curval;
    integer maxval;
    if ( curpos >= 0 ) {
        curval = llList2Integer(CURRENT_RESILIENCES,curpos + 1);
    } else { // resilience not found
        return;
    }
    integer maxpos = llListFindList(RESILIENCES,[name]);
    if ( maxpos >=0 ) {
        maxval = llList2Integer(RESILIENCES,maxpos + 1);
    } else { // resilience not found
        return;
    }
    if ( value <= maxval) {
        CURRENT_RESILIENCES = llListReplaceList(CURRENT_RESILIENCES,[value],curpos + 1, curpos + 1);
    }
}

// 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
        integer curwounds = GET_RESILIENCE("Wounds");
        integer curcritical = GET_RESILIENCE("Critical");
        integer maxcritical = GET_MAX_RESILIENCE("Critical");
        if ( curwounds > 0 && curcritical != maxcritical ) {
            llSay(DEBUG_CHANNEL,"ERROR! WOUND STATE IS NONSENSE! CANNOT APPLY DAMAGE!");
            llOwnerSay("ERROR! WOUND STATE IS NONSENSE! CANNOT APPLY DAMAGE! TELL ALLEN KERENSKY!");
            return;
        }
        if ( curwounds > 0 && curcritical == maxcritical ) { // wound boxes left?
            curwounds--; // scratch off one
            SET_RESILIENCE("Wounds",curwounds);
            METER(); // update
            llOwnerSay("You've been wounded!");
        } else if ( curwounds < 1 && curcritical > 0 ) { // incapacitated
            curwounds = 0; // force to zero
            SET_RESILIENCE("Wounds",curwounds);
            curcritical--; // scratch off a critical wound box
            SET_RESILIENCE("Critical",curcritical);
            INCAPACITATED(); // show incapacitation
        } else if ( curwounds < 1 && curcritical < 1 ) { // out of critical wounds?
            curwounds = 0; // force zero
            SET_RESILIENCE("Wounds",curwounds);
            curcritical = 0; // force zero
            SET_RESILIENCE("Critical",curcritical);
            DEAD(); // show death
        }
    } // end while
}

// INCAPACITATED - player lost all WOUNDS - unable to act
INCAPACITATED() {
    FLAG_INCAPACITATED = TRUE; // yes, we're now incapacitated
    MOVELOCK = llGetPos();
    llMoveToTarget(MOVELOCK,TAU);
    METER(); // update meter
    llStartAnimation(ANIM_INCAPACITATED); // "we're hurt and down" animation
    RPEVENT("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
    RPEVENT("has been killed!");
    llOwnerSay("You've been killed!");
    llSetTimerEvent(RESPAWN_TIME); // respawn in a bit
}

// HEAL - restore lost WOUND and CRITICAL resilience
// Thanks to Artemis Tesla for contributing summary report logic
HEAL(integer healamount) {
    integer critsHealed = 0; // track how many crit boxes restored for summary report
    integer woundsHealed = 0; // track how many non-crit boxes restored for summary report 
    integer reborn = FALSE; // track if reborn/respawn or not for summary report
    integer revived = FALSE;  // track of revived or not for summary report
    integer curwounds = GET_RESILIENCE("Wounds");
    integer maxwounds = GET_MAX_RESILIENCE("Wounds");
    integer curcritical = GET_RESILIENCE("Critical");
    integer maxcritical = GET_MAX_RESILIENCE("Critical");
    
    // TODO report once for multiple healing amounts
    while ( healamount-- ) {
        // step through each point of healing
        if ( curcritical < maxcritical ) { // is current critical less than max critical
            DEBUG("Heal one critical wound");
            curcritical++; // heal one current critical
            SET_RESILIENCE("Critical",curcritical);
            critsHealed++; // add a point back
            if ( FLAG_DEAD == TRUE ) {   // healed a critical, critical now > 0 so not dead anymore
                FLAG_DEAD = FALSE; // no longer dead
                reborn = TRUE; // show rebirth in summary report
                DEBUG("Heal: reborn");
            }
        } else {
            if ( curwounds < maxwounds ) {   // player not critical, heal non-critical?
                DEBUG("Heal one wound");
                curwounds++; // add the healing point to current wounds
                SET_RESILIENCE("Wounds",curwounds);
                woundsHealed++; // add a point of non-critical
                if ( FLAG_INCAPACITATED == TRUE ) { // were they incapacitated?
                    FLAG_INCAPACITATED = FALSE; // no longer gravely wounded
                    revived = TRUE; // show revival in summary report
                    DEBUG("Heal: Revived!");
                    llStopMoveToTarget();
                }
            } // end if curwounds < wounds
        }
    } // end while

    // Summary report of healing effects
    if ( critsHealed > 0 ) {   // was at least one critical healed?
        DEBUG("Critical Heal: "+(string)curcritical+" of "+(string)maxcritical+" critical wound boxes.");
        if (critsHealed > 1) { // was more then one critical wound healed?
            llOwnerSay("Critical " + (string)critsHealed + " wounds healed.");
        } else {
            llOwnerSay("Critical " + (string)critsHealed + " wound healed.");
        }
    }
    if (reborn == TRUE ) { // if player reborn from this heal
        RPEVENT("has been resurrected!");
        if ( FLAG_ANIMATE == TRUE ) { // if we're allowed to change animations
            llStopAnimation(ANIM_DEAD); // stop "we're dead" animation
        }
        llOwnerSay("You've been resurrected! Welcome back to the land of the living.");
    }

    if ( woundsHealed > 0 ) { // was at least 1 non-critical healed?
        DEBUG("Heal Non-Critical Wounds: "+(string)curwounds+" of "+(string)maxwounds+" non-critical wound boxes.");
        if (woundsHealed > 1) { // was more than one non-critical healed?
            llOwnerSay((string)woundsHealed + " non-critical wounds healed.");
        } else {
            llOwnerSay((string)woundsHealed + " non-critical wound healed.");
        }
    }

    if ( revived == TRUE ) { // if player revived from this heal
        RPEVENT("has revived and is no longer incapacitated!");
        if ( FLAG_ANIMATE == TRUE ) { // if we're allowed to change anims
            llStopAnimation(ANIM_INCAPACITATED); // stop the "we're down" animation
        }
        llOwnerSay("You are no longer incapacitated! Welcome back to the fight!");
    }    
    METER(); // update hovertext
}

// 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,"BAMRESET",NULL_KEY); // reset the BAM module too
    llMessageLinked(LINK_THIS,LM_ARMORRESET,"ARMORRESET",PLAYERID); // send reset to armor module
    llResetScript(); // now reset
}

// METER - update a hovertext health meter or HUD bar graph
METER() {
    if ( METERWORN == FALSE ) return;
    integer curwounds = GET_RESILIENCE("Wounds");
    integer maxwounds = GET_MAX_RESILIENCE("Wounds");
    integer curcritical = GET_RESILIENCE("Critical");
    integer maxcritical = GET_MAX_RESILIENCE("Critical");
    // create a meter message packet
    string message = "METER"+DIV+PLAYERNAME+DIV+NAME+DIV+(string)curwounds+DIV+(string)maxwounds+DIV+(string)curcritical+DIV+(string)maxcritical+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)maxwounds+" wound boxes. Critical: "+(string)curcritical+" of "+(string)maxcritical+" 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 msg) {
    // break down the commands and messages into units we can work with
    list fields = llParseString2List(msg,[DIV],[]); // break into list of fields based on DIVider
    string command = llToLower(llStringTrim(llList2String(fields,0),STRING_TRIM)); // assume the first field is a Myriad Lite command
    // ARMOR
    if ( command == "attacharmor" ) { llMessageLinked(LINK_THIS,LM_ATTACHARMOR,msg,PLAYERID); return; }
    if ( command == "detacharmor" ) { llMessageLinked(LINK_THIS,LM_DETACHARMOR,msg,PLAYERID); return; }
    if ( command == "armoroff" ) { llMessageLinked(LINK_THIS,LM_ARMOROFF,msg,PLAYERID); return; }
    if ( command == "armoron" ) { llMessageLinked(LINK_THIS,LM_ARMORON,msg,PLAYERID); return; }
    if ( command == "checkarmor" ) { llMessageLinked(LINK_THIS,LM_ARMORCHECK,msg,PLAYERID); return; }
    if ( command == "checkbattery" ) { llMessageLinked(LINK_THIS,LM_ARMORBATTERY,msg,PLAYERID); return; }
    if ( command == "recharge" ) { llMessageLinked(LINK_THIS,LM_ARMORRECHARGE,msg,PLAYERID); return; }

    if ( command == "checkammo" ) { CHECKAMMO(); return;} // check ammo in weapons
    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 == "reload" ) { RELOAD(); return;}  // reload weapons
    if ( command == "reset" ) { RESET(); return;} // reset HUD
    if ( command == "rumor" ) { RUMOR(msg); return;} // rumors
    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
}

// RUMOR CONTROL
RUMOR(string rumor) {
    llMessageLinked(LINK_THIS,0,"RUMOR_GET|"+rumor,PLAYERID); // get a rumor
}

// 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");
}

// 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) {
        params = 0; // LSLINT
        RESET(); // force to go through state entry
    }

    // attach - when attached or detached from inventory or during login
    attach(key id) {
        id = NULL_KEY; // LSLINT
        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 == "STATISTIC" ) {
                    string statname = llList2String(SUBFIELDS,0); // find the boon name
                    integer statrank = llList2Integer(SUBFIELDS,1); // find the boon rank value
                    // TODO how to verify stat names are valid?
                    if ( statrank >= MINSTAT && statrank <= MAXSTAT ) { // rank valid?
                        STATISTICS = [statname,statrank] + STATISTICS; // add statistic to list
                    } else { // invalid, report it
                        ERROR("STATISTIC "+statname+" rank "+(string)statrank+" value out of allowed range: "+(string)MINSTAT+"-"+(string)MAXSTAT);
                    }
                }
                if ( CMD == "RESILIENCE" ) {
                    string resname = llList2String(SUBFIELDS,0); // find the boon name
                    integer resrank = llList2Integer(SUBFIELDS,1); // find the boon rank value
                    // TODO how to verify resilience names are valid?
                    if ( resrank >= MINRESILIENCE && resrank <= MAXRESILIENCE ) { // rank valid?
                        RESILIENCES = [resname,resrank] + RESILIENCES; // add resilience to list
                        CURRENT_RESILIENCES = [resname,resrank] + CURRENT_RESILIENCES; // add to current list too
                    } else { // invalid, report it
                        ERROR("RESILIENCE "+resname+" rank "+(string)resrank+" value 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.");
        if ( HANDMYRIAD != 0 ) llListenRemove(HANDMYRIAD);
        HANDMYRIAD = llListen(CHANMYRIAD,"",NULL_KEY,""); // setup listener for Myriad RP events
        if ( HANDCOMMAND != 0 ) llListenRemove(HANDCOMMAND);
        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
        if ( HANDPLAYER != 0 ) llListenRemove(HANDPLAYER);
        HANDPLAYER = llListen(CHANPLAYER,"",NULL_KEY,""); // listen on the player dynamic chat channel
        CHANATTACH = (integer)("0x"+llGetSubString((string)PLAYERID,1,7)); // attachment-specific channel
        if ( HANDATTACH != 0 ) llListenRemove(HANDATTACH);
        HANDATTACH = llListen(CHANATTACH,"",NULL_KEY,""); // listen for messages from attachments
        CHANBAM = (integer)(CHAN_PREFIX + llGetSubString((string)PLAYERID,-7,-1));
        if ( HANDBAM != 0 ) llListenRemove(HANDBAM);
        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
        llRequestPermissions(PLAYERID,PERMISSION_TRIGGER_ANIMATION|PERMISSION_TAKE_CONTROLS);
         // calculate player's dynamic BAM channel
        METER(); // update hovertext
        QUEST(); // update the BAM Module
        llOwnerSay("HUD startup complete. "+(string)llGetFreeMemory()+" bytes free.");
    }

    // ON_REZ - logged in with meter, or worn from inventory while running
    on_rez(integer param) {
        param = 0; // LSLINT
        RESET(); // a reset to reload character
    }

    // ATTACH - logged in with meter or worn from inventory/ground while running
    attach(key id) {
        id = NULL_KEY; // LSLINT
        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 ) {
            llRequestPermissions(PLAYERID,PERMISSION_TRIGGER_ANIMATION|PERMISSION_TAKE_CONTROLS);
            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) {
        total_number = 0; // LSLINT
        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) {
        id = NULL_KEY; // LSLINT
        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
            RPEVENT("strikes at "+name+" in Close Combat!");    
            // tell victim HUD to perform a CLOSE COMBAT opposed ability test
            // attacker Power stat/Close Combat skill rank vs. Defender Grace stat/Close Combat skill rank
            // See Myriad PDF pp. 21-22 and Myriad Special Edition pp.27-28
            llRegionSay(victimchan,"CLOSEHIT"+DIV+(string)GETSTAT("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
            RPEVENT("respawns!");
            RESET(); // reset and reload character
        }
        if ( FLAG_INCAPACITATED == TRUE ) { // if hurt
            HEAL(1); // heal 1 wound
        }
        integer curwounds = GET_RESILIENCE("Wounds");
        integer maxwounds = GET_MAX_RESILIENCE("Wounds");
        if ( curwounds == maxwounds ) { // fully healed?
            llSetTimerEvent(0.0); // stop timer
        }
    }
    
    // LINK MESSAGE - commands to and from other prims in HUD
    link_message(integer sender,integer num,string msg, key id) {
        sender = 0; // LSLINT
        id = NULL_KEY; // LSLINT
        if ( num == LM_ARMORCURRENT ) {
            // ARMORCURRENT|integer newcurrentarmor 
            list fields = llParseString2List(msg,[DIV],[]); // break into list of fields based on DIVider
            string command = llToLower(llStringTrim(llList2String(fields,0),STRING_TRIM)); // assume the first field is a Myriad Lite command
            if ( command == "armorcurrent" ) {
                integer rating = llList2Integer(fields,1);
                if ( rating > 0 && rating <= MAXARMOR ) { 
                    CURARMOR = rating;
                }
                return;
            }
            return;
        }
        COMMAND(msg); // 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) {
        speakername = ""; // LSLINT
        // 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(message); // 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
                llMessageLinked(LINK_THIS,LM_ATTACHARMOR,message,PLAYERID);
                return;                
            }
            if ( command == "DETACHARMOR" ) { // player attached armor somewhere
                llMessageLinked(LINK_THIS,LM_DETACHARMOR,message,PLAYERID);
                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
            if ( command == "RPEVENT" ) { // Myriad Lite RPEVENT - roleplay events everyone might find interesting
                string oldname = llGetObjectName(); // save the current object name
                llSetObjectName("Myriad RP Event (Private)"); // 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
            if ( command == "UNOPPOSED_CHECK" ) { // object in sim wants a simple skill check
                integer targetnum = llList2Integer(fields,1); // what is unopposed check target num?
                integer tattrib = llList2Integer(fields,2); // target attribute
                integer tskill = llList2Integer(fields,3); // target skill
                UNOPPOSED_TEST(targetnum,tattrib,tskill);
                return;
            }
            // 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,GETSTAT("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
                METER(); // update
                return;
            }
            if ( command == "HEALFULL" ) { // full heal, reset state
                HEAL(100); // heal up to 100 damage
                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)GETSTAT("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)GETSTAT("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

Personal tools
General
About This Wiki