Myriad Lite Unopposed Test2

// Myriad_Lite_Unopposed_Test_2-v0.0.1-20131104.lsl
// Copyright (c) 2013 by Allen Kerensky (OSG/SL) All Rights Reserved.
// This work is dual-licensed under
// Creative Commons Attribution (CC BY) 3.0 Unported
// http://creativecommons.org/licenses/by/3.0/
// - or -
// Modified BSD License (3-clause)
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
// * Redistributions of source code must retain the above copyright notice, 
//   this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
//   this list of conditions and the following disclaimer in the documentation
//   and/or other materials provided with the distribution.
// * Neither the name of Myriad Lite nor the names of its contributors may be
//   used to endorse or promote products derived from this software without
//   specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY EXPRESS OR
// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
// NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
// NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// The Myriad RPG System was designed, written, and illustrated by Ashok Desai
// Myriad RPG System licensed under:
// Creative Commons Attribution (CC BY) 2.0 UK: England and Wales
// http://creativecommons.org/licenses/by/2.0/uk/
 
// CONSTANTS - DO NOT CHANGE DURING RUN
string BASENAME = "Myriad Lite Unopposed Test2"; // base name of this script without version or date
string VERSION = "0.0.1"; // Allen Kerensky's script version
string VERSIONDATE = "20131104"; // Allen Kerensky's script yyyymmdd
integer CHANMYRIAD = -999;
integer MOD_UNOPPOSED = -1;
 
//////////////////////////////////////////////////////////////////////////////
// CONFIGURATION FOR THIS UNOPPOSED TEST
//
// TASK
// A descriptive name for the task
string TASK="Unlock the Locked Door";
 
// RPEVENT_START - a string to send as an RPEVENT when player starts - tell all
// leave empty for no RPEVENT
string RPEVENT_START = "tries to defeat the lock of an electronically-locked door";
 
// Statistic to use for the Unopposed Test
// Power, Grace, Intellect, Spirit
// Choose the one that makes sense based on the skill required for the test
string STATISTIC = "Intellect";
 
// Skill to test again
// See Myriad RPG System p.53 for many sample skill ideas
string SKILL = "Electronics";
 
// Standard Target Numbers
// Myriad RPG System p.19
// 4 = Foolproof
// 5 = Easy
// 6 = Average
// 7 = Tricky
// 8 = Difficult
// 9 = Very Hard
// 11 = Infeasible
// 13 = Superhuman
// 15 = Godlike
// 17 = Impossible
integer TARGETNUM = 6;
 
// Success Actions Recipes
// These are recipes based on Myriad Lite's Link Message formats. 
// See the Myriad Lite HUD Modules, specifically the WELL module for many examples.
// Replace the | in standard link messages with a ^ for these recipes
// Separate multiple actions with commas
// Keep it short to stay in LSL message length limits
string ONSUCCESS = "OWNERSAY|You have bypassed the electronic lock and the door can be opened.";
 
// RPEVENT_SUCCESS - a public chat message this object will say if a player succeeds.
// Leave empty for no RPEVENT event message
string RPEVENT_SUCCESS = "bypassed the door's electronic lock.";
 
// Success Actions Recipes
// These are recipes based on Myriad Lite's Link Message formats. 
// See the Myriad Lite HUD Modules, specifically the WELL module for many examples.
// Replace the | in standard link messages with a ^ for these recipes
// Separate multiple actions with commas
// Keep it short to stay in LSL message length limits
string ONFAIL = "OWNERSAY|You did not bypass the electronic door lock.";
 
// RPEVENT_FAIL - a public chat message this object will say if a player fails the task
// Leave empty for no RPEVENT message
string RPEVENT_FAIL = "failed to bypass the door's electronic lock";
 
// RETRIES
// TRUE or FALSE, retries are allowed
integer RETRIES = TRUE;
 
// TIME BETWEEN TRIES
// Number of seconds between retries
integer RETRYTIME = 10; // 10 minutes is usually good
string WAITMESSAGE = "You must wait 10 seconds between retries.";
 
// MAX RETRIES
// Max times before
integer MAX_RETRIES = 10;
string MORETRIES = "You can retry bypassing this lock.";
string NOMORETRIES = "The lock electronics short out and can no longer be bypassed.";
 
//////////////////////////////////////////////////////////////////////////////
// TEMPORARY STORAGE IN RUNTIME MEMORY
// SHOULD NOT NEED EDITING BEYOND THIS POINT.
list PLAYERS = []; // [ <playeruuid>,<timestried>,<lasttrytime> ] strided list of players accessing this task
integer HANDLE; // listenremove handle for this object's dynamic channel
integer CHANNEL; // calculated dynamic channel number for this object to listen on
 
// 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 modifiers) {
    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 + modifiers; // now, return the total of highest dice roll + skill value + modifiers
}
 
//
// CHECK ID to see if they pass/fail, and do retry list management - calls unopposed test
//
CHECK(key id) {    
    integer timestried; // place to hold how many times they have tried
    integer lasttime; // place to hold last time they tried
    // is player in list, get data, otherwise start data
    integer pos = llListFindList(PLAYERS,[id]); // find their current record in player list in memory
    if ( pos >= 0 ) { // they already had a record
        timestried = llList2Integer(PLAYERS,pos + 1); // get stored number of times tried
        lasttime = llList2Integer(PLAYERS,pos + 2); // get time of last try
        timestried++; // add one for this attempt
    } else { // no record so let's create and add one
        timestried = 1; // this counts as the first try
        lasttime = 0; // never tried in history
        PLAYERS = PLAYERS + [ id,timestried,lasttime ]; // add the record to the memory list
    }
    if ( timestried > MAX_RETRIES ) llInstantMessage(id,NOMORETRIES); // are they already out of tries? say so
    if ( llGetUnixTime() < ( lasttime + RETRYTIME ) ) llInstantMessage(id,WAITMESSAGE); // are they trying too soon? say so
    if ( timestried <= MAX_RETRIES && llGetUnixTime() >= ( lasttime + RETRYTIME ) ) { // if they have tries, and have waited long enough
        integer chan = (integer)("0x"+llGetSubString(id,0,6)); // calculate their player dynamic channel
        llRegionSay(chan,"UNOPPOSED_TEST2|"+TASK+"|"+STATISTIC+"|"+SKILL); // send the test request to them / their HUD
        if ( RPEVENT_START != "" ) llRegionSay(CHANMYRIAD,"RPEVENT|"+llKey2Name(id)+" "+RPEVENT_START);
    }
    // now update the player list in memory
    pos = llListFindList(PLAYERS,[id]); // where is their record, even if we just added one
    PLAYERS = llListReplaceList(PLAYERS,[id,timestried,llGetUnixTime()],pos, pos + 2); // now update the data in the player list in memory
}
 
 
DEBUG(string msg) {
    llSay(PUBLIC_CHANNEL,"DEBUG: "+msg);
}
 
ERROR(string msg) {
    llSay(PUBLIC_CHANNEL,"ERROR: "+msg);
}
 
SETUP() {
    llSetText("Easy Intellect/Electronics\nUnopposed Test2\nMyriad Lite Preview 7",<1,1,1>,1);
 
    // Setup This Object's Listener
    if ( HANDLE != 0 ) llListenRemove(HANDLE); // remove an existing handle
    CHANNEL = (integer)("0x"+llGetSubString(llGetKey(),0,6)); // calculate this object's dynamic channel number
    llListen(CHANNEL,"",NULL_KEY,""); // start a new listener on object's dynamic channel
}
 
// UNOPPOSED_TEST|<target number>|<stat name>|<skill name>|<CSV success actions>|<CSV fail actions>
// CSV success and fail actions are CSV lists action1[,action2[,...]]
// Each action is an embedded link message which uses ^ in place of the usual | used to separate fields
// Example Actions: SAY^<channel#>^<message_with_no_commas> , SHOUT^<channel#>,<message_with_no_commas>
// See the WELL module for a preset library of actions you can stack from, but any valid link message is possible
// FIXME This is a huge cheat and security hole as is without testing for valid actions by players vs. region owners
// Returns TRUE for Success and False for Fail
// UNOPPOSED_TEST2
// integer stat = amount of player's stat (0-10)
integer UNOPPOSED_TEST2(key id,integer stat,integer skill,integer mods) {
    //DEBUG("UNOPPOSED_TEST2 stat=["+(string)stat+"] skill=["+(string)skill+"] mods=["+(string)mods+"]");
    // FIXME check stat range - replace constants with MIN_STAT/MAX_STAT
    if ( stat < 0 || stat > 10 ) { ERROR("Invalid stat amount "+(string)stat+" in unopposed test. Ending unopposed test."); return FALSE;}
    // FIXME check skill range - replace constants with MIN_SKILL/MAX_SKILL
    if ( skill < 0 || skill > 7 ) { ERROR("Invalid skill amount "+(string)skill+" in unopposed test. Ending unopposed test."); return FALSE;}
    // FIXME check tskill
    integer check = ABILITY_TEST(stat,skill,mods); // calculate the player's ability test value
    integer result = FALSE;
    string acts = "" ;
    string rpevent = "";
    if ( check >= TARGETNUM ) {
        result = TRUE;
        acts = ONSUCCESS;
        rpevent = RPEVENT_SUCCESS;
    } else {  // player lost the ability test
        result = FALSE;
        acts = ONFAIL; // fail actions to player
        rpevent = RPEVENT_FAIL; // fail action rpevent to everyone
    }
    integer chan = (integer)("0x"+llGetSubString(id,0,6)); // calculate dynamic channel of player being tested
    llRegionSay(chan,acts);
    if ( rpevent != "" ) llRegionSay(CHANMYRIAD,"RPEVENT|"+llKey2Name(llGetOwnerKey(id))+" "+rpevent);
    return result;
}
 
default {
    state_entry() {
        SETUP();
    }
 
    link_message(integer sender_num,integer num,string str,key id) {
        if ( str == "UNOPPOSED_TEST2|CHECK" ) {
            CHECK(id);
        }
    }
 
    listen(integer channel,string name,key id,string message) {
        // Listen for responses from player on this objects dynamic channel
        list fields = llParseString2List(message,["|"],[]); // break into list of fields based on DIVider
        string cmd = llToUpper(llStringTrim(llList2String(fields,0),STRING_TRIM)); // assume the first field is a Myriad Lite command        
        if ( cmd == "UNOPPOSED_TEST2" ) { // UNOPPOSED_TEST2|<statamount>|<skillamount>|<modfiiers>
            integer stat = llList2Integer(fields,1);
            integer skill = llList2Integer(fields,2);
            integer mods = llList2Integer(fields,3);
            integer check = UNOPPOSED_TEST2(id,stat,skill,mods);
            if ( check == TRUE ) {
                // do more specific things if the check succeeds - unlock the door, file, etc
                llMessageLinked(LINK_THIS,MOD_UNOPPOSED,"UNOPPOSED_TEST2|SUCCESS",id);
            } else {
                // do more specific things if the check fails - set off trap, explode, sound alarm, etc
                llMessageLinked(LINK_THIS,MOD_UNOPPOSED,"UNOPPOSED_TEST2|FAILURE",id);
            }
            return;
        }
    }
 
    touch_start(integer num_detected) {
        while (num_detected--) {
            key who = llDetectedKey(num_detected); // who is clicking
            CHECK(who);
        }    
    }
}