// Myriad_Lite_Module_Skill_Ranged_Combat-v0.0.4-20131025.lsl
// Copyright (c) 2012 by Allen Kerensky (OSG/SL) All Rights Reserved.
// This work is dual-licensed under
// Creative Commons Attribution (CC BY) 3.0 Unported
// http://creativecommons.org/licenses/by/3.0/
// - or -
// Modified BSD License (3-clause)
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
// * Neither the name of Myriad Lite nor the names of its contributors may be
// used to endorse or promote products derived from this software without
// specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY EXPRESS OR
// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
// NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
// NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// The Myriad RPG System was designed, written, and illustrated by Ashok Desai
// Myriad RPG System licensed under:
// Creative Commons Attribution (CC BY) 2.0 UK: England and Wales
// http://creativecommons.org/licenses/by/2.0/uk/
// FIXME add more RPEVENTs to module
// FIXME add HELP messages to module
// CONSTANTS - DO NOT CHANGE DURING RUN
string BASENAME = "Myriad Lite Module Skill Ranged Combat";
string VERSION = "0.0.4"; // script version
string VERSIONDATE = "20131025"; // script yyyymmdd
// Module to Module Messaging Constants
integer MODULE_RANGED = -7;
integer LM_SENDTOATTACHMENT = 0x80000000;
//
// PPMA GET_VAL
//
string KEY_NOT_FOUND = "[KEY_NOT_FOUND]";
string GET_VAL(string dbkey,string field) {
//OWNERSAY("GET_VAL KEY=["+dbkey+"] FIELD=["+field+"]");
string out = KEY_NOT_FOUND;
integer i;
string name;
string desc;
for ( i = 2; i <= llGetNumberOfPrims(); i++ ) {
name = llList2String(llGetLinkPrimitiveParams(i,[PRIM_NAME]),0);
desc = llList2String(llGetLinkPrimitiveParams(i,[PRIM_DESC]),0);
if ( llToLower(name) == llToLower(dbkey) && llToLower(desc) == llToLower(field) ) {
out = llList2String(llGetLinkPrimitiveParams(i,[PRIM_TEXT]),0);
//DEBUG("GET_VAL RETURN=["+out+"]");
return out; // bail on first match?
}
}
//OWNERSAY("GET_VAL RETURN=["+out+"]");
return out;
}
//SET_VAL(string dbkey, string field, string val) {
// //OWNERSAY("SET_VAL KEY=["+dbkey+"] FIELD=["+field+"] VAL=["+val+"]");
// integer i;
// string name;
// string desc;
// integer written = FALSE;
// for ( i = 2; i <= llGetNumberOfPrims(); i++ ) {
// name = llStringTrim(llList2String(llGetLinkPrimitiveParams(i,[PRIM_NAME]),0),STRING_TRIM);
// desc = llStringTrim(llList2String(llGetLinkPrimitiveParams(i,[PRIM_DESC]),0),STRING_TRIM);
// if ( ( llToLower(name) == llToLower(dbkey) && llToLower(desc) == llToLower(field) ) && written == FALSE ) {
// //OWNERSAY("SET_VAL UPDATE RECORD=["+(string)i+"] DBKEY=["+dbkey+"] DESC=["+field+"] VAL=["+val+"]");
// llSetLinkPrimitiveParamsFast(i,[PRIM_NAME,dbkey,PRIM_DESC,field,PRIM_TEXT,val,ZERO_VECTOR,0.0]);
// written = TRUE; // we did an update, remember it
// }
// }
// if ( written == TRUE ) {
// //OWNERSAY("SET_VAL UPDATE COMPLETE.");
// return;
// }
// // data hasn't been written, scan for free prim
// for ( i = 2; i <= llGetNumberOfPrims(); i++ ) {
// name = llStringTrim(llList2String(llGetLinkPrimitiveParams(i,[PRIM_NAME]),0),STRING_TRIM);
// desc = llStringTrim(llList2String(llGetLinkPrimitiveParams(i,[PRIM_DESC]),0),STRING_TRIM);
// if ( ( name == "" && desc == "" ) && written == FALSE ) {
// //OWNERSAY("SET_VAL INSERT RECORD=["+(string)i+"] DBKEY=["+dbkey+"] DESC=["+field+"] VAL=["+val+"]");
// llSetLinkPrimitiveParamsFast(i,[PRIM_NAME,dbkey,PRIM_DESC,field,PRIM_TEXT,val,ZERO_VECTOR,0.0]);
// written = TRUE; // we did an update, remember it
// }
// }
// if ( written == TRUE ) {
// //OWNERSAY("SET_VAL INSERT COMPLETED.");
// return;
// }
// //OWNERSAY("SET_VAL NO FREE RECORD FOUND TO INSERT INTO! DATA LOST");
//}
//////////////////////////////////////////////////////////////////////////////
// FLAG FUNCTIONS
// INCAPACITATED - lost wounds
// DEAD - lost critical wounds
// DEBUG - show debug messages or not
// IDEA: INDECISION - lost resolve?
// LMIM: SET_FLAG|<name>=<TRUE|FALSE>
// PPMA: FLAG,<flagname>,<TRUE|FALSE>
// LMOUT: FLAG|<flagname>=<TRUE|FALSE>
// LMERR: FIXME
integer GET_FLAG(string flag) {
string val = GET_VAL("FLAG",llToUpper(flag));
if ( val == KEY_NOT_FOUND ) {
ERROR("Flag ["+flag+"] does not exist.");
}
if ( val != "0" && val != "1") {
ERROR("Flag: "+flag+" invalid value ["+val+"]");
}
integer bool = (integer)val;
return bool;
}
// SET_FLAG
// LMIM: SET_FLAG|<flagname>=<TRUE|FALSE>
// PPMA: FLAG,<flagname>,<TRUE|FALSE>
// LMOUT: SET_FLAG|<flagname>=<TRUE|FALSE>
// LMERR: FIXME
//SET_FLAG(string flag,integer value) {
// if ( flag == "" ) return; // FIXME add error
// if ( value != TRUE && value != FALSE ) return; // FIXME add err
// SET_VAL("FLAG",llToUpper(flag),(string)value);
//}
//
// GET_MYRIAD
// Requires a MYRIAD CONSTANT NAME
// Returns the amount of that Myriad rules constant or defaultif the player does not currently have that constant
//
integer GET_MYRIAD(string setting) {
integer retval;
string value = GET_VAL("MYRIAD",setting);
if ( value == KEY_NOT_FOUND ) {
if ( setting == "MINSTAT" ) retval = 1;
if ( setting == "MAXSTAT" ) retval = 10;
if ( setting == "MINSKILL" ) retval = 0;
if ( setting == "MAXSKILL" ) retval = 7;
if ( setting == "MINEFFECT" ) retval = 0;
if ( setting == "MAXEFFECT" ) retval = 5;
if ( setting == "MINRESILIENCE" ) retval = 0;
if ( setting == "MAXRESILIENCE" ) retval = 20;
if ( setting == "MINBOON" ) retval = 0;
if ( setting == "MAXBOON" ) retval = 5;
if ( setting == "MINFLAW" ) retval = 0;
if ( setting == "MAXFLAW" ) retval = 5;
if ( setting == "MINRP" ) retval = 0;
if ( setting == "MAXRP" ) retval = 10;
if ( setting == "MINITEM" ) retval = 0;
if ( setting == "MAXITEM" ) retval = 100;
if ( setting == "MINARMOR" ) retval = 0;
if ( setting == "MAXARMOR" ) retval = 5;
if ( setting == "MINDAMAGE" ) retval = 1;
if ( setting == "MAXDAMAGE" ) retval = 5;
ERROR("Unable to locate Myriad setting "+setting+" returning "+(string)retval);
} else {
retval = (integer)value;
}
return retval;
}
//
// GET_SKILL
// Requires a SKILL NAME
// Returns the rank value for that skill, or zero if player doesn't have skill
//
integer GET_SKILL(string askill) {
string val = GET_VAL("SKILL",askill);
if ( val == KEY_NOT_FOUND ) {
ERROR("Unable to locate skill "+askill+" returning 0");
return 0;
}
integer retval = (integer)val;
if ( retval < GET_MYRIAD("MINSKILL") || retval > GET_MYRIAD("MAXSKILL") ) {
ERROR("Skill "+askill+" value "+val+" out of range "+(string)GET_MYRIAD("MINSKILL")+"-"+(string)GET_MYRIAD("MAXSKILL"));
return GET_MYRIAD("MINSKILL");
}
return retval;
}
//
// GET_STAT
// Requires a STAT NAME
// Returns the rank value for that statistics, or zero if player doesn't have stat
//
integer GET_STATISTIC(string astat) {
string val = GET_VAL("STATISTIC",astat);
if ( val == KEY_NOT_FOUND ) {
ERROR("Unabled to locate statistic "+astat+" returning 0");
return 0;
}
integer retval = (integer)val;
if ( retval < GET_MYRIAD("MINSTAT") || retval > GET_MYRIAD("MAXSTAT") ) {
ERROR("Statistic "+astat+" value "+val+" out of range "+(string)GET_MYRIAD("MINSTAT")+"-"+(string)GET_MYRIAD("MAXSTAT"));
return GET_MYRIAD("MINSTAT");
}
return retval;
}
// ABILITY TEST
// Requires ATTRIBUTE NAME, SKILL NAME
// Returns the ability test score for use by success fail, opposed rolls, etc
// See Myriad PDF page 18, Myriad Special Edition page 24
integer ABILITY_TEST(integer attribute,integer skill) {
integer highroll = 0; // clear out the highest roll
while( attribute-- ) { // roll a dice for each point of the attribute
integer roll = 1+(integer)llFrand(6.0); // roll this d6
if ( roll > highroll) highroll = roll; // if this is highest roll so far, remember it
} // finished rolling a dice for each point of the base attribute
return highroll + skill; // now, return the total of highest dice roll + skill value
}
// COMBATOFF - turn off ranged combat
COMBATOFF() {
//llMessageLinked(LINK_THIS,MODULE_RANGED,"COMBATOFF",llGetOwner());
}
// COMBATON - turn on ranged combat
COMBATON() {
//llMessageLinked(LINK_THIS,MODULE_RANGED,"COMBATON",llGetOwner());
}
// DEBUG - show debug chat with wearer name for sorting
DEBUG(string dmessage) {
if ( GET_FLAG("DEBUG") == TRUE ) llMessageLinked(LINK_THIS,MODULE_RANGED,"DEBUG|"+dmessage,llGetOwner());
}
// ERROR - show errors on debug channel with wearer name for sorting
ERROR(string emessage) {
llMessageLinked(LINK_THIS,MODULE_RANGED,"ERROR|"+emessage,llGetOwner());
}
// MEMORY
GET_MEMORY() {
OWNERSAY(BASENAME+" free memory: "+(string)llGetFreeMemory());
}
// GETVERSION
GET_VERSION() {
OWNERSAY(BASENAME+" v"+VERSION+"-"+VERSIONDATE);
}
// 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
}
//
// OWNERSAY
//
OWNERSAY(string msg) {
llMessageLinked(LINK_THIS,MODULE_RANGED,"OWNERSAY|"+msg,llGetOwner());
}
// RESET - shut down running animations then reset the script to reload character sheet
RESET() {
llResetScript(); // now reset
}
// RPEVENT
RPEVENT(string rpevent) {
llMessageLinked(LINK_THIS,MODULE_RANGED,"RPEVENT|"+rpevent,llGetOwner());
}
// SETUP - begin bringing the HUD online
SETUP() {
OWNERSAY("Ranged Combat module active.");
}
// DEFAULT STATE
default {
link_message(integer sender_num,integer sender,string message,key id) {
if ( sender == MODULE_RANGED || sender == LM_SENDTOATTACHMENT ) return; // ignore our own messages
list fields = llParseString2List(message,["|"],[]); // break line of text into = delimited fields
string command = llToLower(llStringTrim(llList2String(fields,0),STRING_TRIM)); // field zero is the "command"
if ( command == "debug" || command == "error" || command == "help" || command == "ownersay" || command == "rpevent" ) return; // ignore WELL messages
// process the rest of the message for this module
//string data = llStringTrim(llList2String(fields,1),STRING_TRIM); // field one is the data
//list subfields = llParseString2List(data,["="],[]); // break data field into comma-delimited subfields if needed
// only show debug after ignoring well commands and stuff above
DEBUG("EVENT link_message sender_num=["+(string)sender_num+"] sender=["+(string)sender+"] message=["+message+"] id=["+(string)id+"]");
if ( command == "combatoff" ) { COMBATOFF(); return; }
if ( command == "combaton" ) { COMBATON(); return; }
if ( command == "reset" ) { RESET(); return; }
if ( command == "memory" ) { GET_MEMORY(); return; }
if ( command == "version" ) { GET_VERSION(); return; }
//
// We're hit by a ranged combat item
//
if ( command == "rangedhit" ) { // mortal combat attack message?
integer attackstat = llList2Integer(fields,1); // get attackers stat
integer attackskill = llList2Integer(fields,2); // get attackers skill
integer attackdice = llList2Integer(fields,3); // get attacker object's attack dice
key owner = llList2Key(fields,4); // get attacker object's key
string item = llList2String(fields,5); // get attacker object name
if ( attackstat < GET_MYRIAD("MINSTAT") || attackstat > GET_MYRIAD("MAXSTAT") ) { // is the attack stat value out of allowed range?
ERROR("Attack Statistic value "+(string)attackstat+" out of range: "+(string)GET_MYRIAD("MINSTAT")+"-"+(string)GET_MYRIAD("MAXSTAT"));
// FIXME make a tattletale RP event?
return;
}
if ( attackskill < GET_MYRIAD("MINSKILL") || attackstat > GET_MYRIAD("MAXSKILL") ) { // is the attack skill value out of allowed range?
ERROR("Attack Skill value "+(string)attackskill+" out of range: "+(string)GET_MYRIAD("MINSKILL")+"-"+(string)GET_MYRIAD("MAXSKILL"));
// FIXME make a tattletale RP event?
return;
}
if ( attackdice < GET_MYRIAD("MINDAMAGE") || attackdice > GET_MYRIAD("MAXDAMAGE") ) { // is the attacking weapon's attack dice value out of allowed range?
ERROR("Attack Damage Dice value out of range: "+(string)GET_MYRIAD("MINDAMAGE")+"-"+(string)GET_MYRIAD("MAXDAMAGE"));
// FIXME make a tattletale RP event?
return;
}
integer skillamount = 0; // create a place to hold the defenders mortal combat skill rank
skillamount = GET_SKILL("Ranged Combat"); // get ranged combat skill rank
// see if we're hit
integer amihit = OPPOSED_TEST(attackstat,attackskill,GET_STATISTIC("Grace"),skillamount); // attacker power+skill vs. defender grace+skill
if ( amihit == TRUE ) { // we're hit!
string ownername = llList2String(llParseString2List(llKey2Name(owner),["@"],[]),0); // strip @where from HG names
OWNERSAY("You've been hit in ranged combat by "+ownername+"'s "+item+"!");
RPEVENT("struck by "+ownername+"'s "+item+" in ranged combat!");
llMessageLinked(LINK_THIS,MODULE_RANGED,"HIT|"+(string)attackdice,owner); // apply the hit
}
return;
}
//
// Actions NOT Allowed When Dead/Incapacitated go below here
//
if ( GET_FLAG("DEAD") == TRUE || GET_FLAG("INCAPACITATED") == TRUE ) return;
//
if ( command == "attachranged" ) { // holding a ranged weapon rather than using fists
// FIXME state with close combat, social combat, etc
return;
}
//
if ( command == "detachranged" ) { // are we going back to fists?
// FIXME state with close combat, social combat, etc
return;
}
//
// We've hit someone with a ranged attack object
// If Your Bullet has hit, fire a hitcheck regionwide at target player's dynamic channel
//
if ( command == "rangedcombat" ) {
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 attstat = GET_STATISTIC("Power"); // get ranged combat attack stat level
integer attskill = GET_SKILL("Ranged Combat"); // get ranged combat skill level
// Message format: RANGEDHIT | <value of attack stat> | <value of attack skill> | <number of attack damage dice> | <uuid of who made attack> | <string name of what they attacked with>
llRegionSay(victimchan,"RANGEDHIT"+"|"+(string)attstat+"|"+(string)attskill+"|"+(string)attdice+"|"+bywho+"|"+bywhat); // attack!
return;
} // end if RANGEDCOMBAT/TOHIT
} // end of link_message event
// STATE ENTRY - called on Reset
state_entry() {
// Check required character sheet data is in PPMA, if not, disable module until HUD reset
// have to do this here, since state command cannot be called from global functions
// See http://lslwiki.net/lslwiki/wakka.php?wakka=FunctionStateChangeHack
// See also "Caveats" in http://wiki.secondlife.com/wiki/State
list requirements = [ "STATISTIC", "Power", "STATISTIC", "Grace", "SKILL", "Ranged Combat", "RESILIENCE", "Wounds", "RESILIENCE", "CurrentWounds", "RESILIENCE", "CriticalWounds", "RESILIENCE", "CurrentCriticalWounds", "MYRIAD", "MINSTAT", "MYRIAD", "MAXSTAT", "MYRIAD", "MINSKILL", "MYRIAD", "MAXSKILL", "MYRIAD", "MINRESILIENCE", "MYRIAD", "MAXRESILIENCE", "MYRIAD", "MINDAMAGE", "MYRIAD", "MAXDAMAGE", "FLAG", "INCAPACITATED", "FLAG", "DEAD" ];
integer i;
for ( i = 0; i < llGetListLength(requirements); i += 2 ) {
string category = llList2String(requirements, i);
string aspect = llList2String(requirements, i + 1);
if ( GET_VAL(category,aspect) == KEY_NOT_FOUND ) {
OWNERSAY("Required "+llToLower(category)+" \""+aspect+"\" not found. Ranged Combat module will not work correctly. Disabling...");
state disabled;
}
}
SETUP();
}
} // end state
state disabled {
link_message(integer sender_num,integer sender,string message,key id) {
if ( sender == MODULE_RANGED || sender == LM_SENDTOATTACHMENT ) return; // ignore our own messages
list fields = llParseString2List(message,["|"],[]); // break line of text into = delimited fields
string command = llToLower(llStringTrim(llList2String(fields,0),STRING_TRIM)); // field zero is the "command"
if ( command == "debug" || command == "error" || command == "help" || command == "ownersay" || command == "rpevent" ) return; // ignore WELL commands
if ( command == "reset" ) { RESET(); return; } }
}
// END