// Baroun's Adventure Machine BAM_Adventure_Giver_NPC-v0.0.7-20120704.lsl
// Copyright (c) 2012 by Baroun Tardis and 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/
string VERSION = "0.0.7"; // version number
string VERDATE = "20120704"; // version date
//============================================================================
// Adventure-Specific Configuration
//============================================================================
// NPC-specific Info
string MSG_NPCNAME = "BAM Adventure Giver NPC"; // hovertext name for NPC - I hate hovertext
vector MSG_SETTEXT_COLOR = <1.0,1.0,1.0>; // color to display settext in
float MSG_SETTEXT_ALPHA = 1.0; // alpha for hovertext. 0.0 = clear, 1.0 = solid.
// Adventure-specific Info
string ADVNAME = "Red Salt"; // Adventure Name
string ADVTEXT = "Find some red salt for luck."; // brief description
string ADV_ATTRACT = "can you help me find some red salt for luck?"; // hook to get player to play
string MSG_OWNER_STARTADV = " has started the Red Salt Adventure";
string ADV_ALL_TASKS = "101,102"; // CSV list of task numbers that all have to be done for adventure to be complete example: "101, 102, 103"
string MSG_ADV_INCOMPLETE = ", It looks like you still have work to do."; // prefixed by AVNAME
string MSG_OWNER_DONEADV = " has completed Red Salt Adventure"; // message to speak when task complete
string ADVDONETEXT = "Thanks! I appreciate your help!";
string ADVDONEUUID = "f78027c9-e8bb-38f2-9b11-1d4e89ac10a4"; //object or sounds to give as a mission complete
string PRIZENAME = "NONE"; // prize to give when adventure is complete
// Task-Specific Info
// Task numbers are (AdvNum*100)+task, so they don't match up between adventures
integer ADVTASKTDNUM = 101; // task number for the task THIS node hands out
string ADVTASKTODO = "Find the Red Salt Mines"; // task description of what to do
string ADVTASKTODOHINT = "Look in the other corners of the platform"; // string
//integer TRIGGERWAIT = 60; // seconds to remember players to prevent re-triggering task?
float TOUCH_RANGE = 1.5; // in meters, how close to be for touch to work arms & legs = 1.5m
integer PRIZEWAIT = 3600; // seconds to remember players who got prize?
float EVENTTIMER = 15.0; // seconds between running memory and list cleanup timed events
integer FLAG_USESENSOR; // set to TRUE to use sensor alerts - set in state_entry
float SENSOR_RANGE = 3.0; // how close do you have to be to trigger the NPC
float SENSOR_ARC = PI; // what is the sensor's sweep?
float SENSOR_REPEAT = 10; // how often to repeat the sensor
//============================================================================
// MESSAGE FORMAT REFERENCE
//============================================================================
// -1 OUT - [YES,NO,CLOSE]
// -1 IN - YES
// BAMCHAN OUT - InAdv?
// BAMCHAN IN - InAdv|str NONE
// BAMCHAN IN - InAdv|str AdventureName
// BAMCHAN OUT - OfferAdv|str AdventureName|str AdventureText
// BAMCHAN OUT - TaskCP?
// BAMCHAN IN - AcceptAdv|str AdvName
// BAMCHAN OUT - AddTask|int TaskNumber|str TaskToDo
// BAMCHAN OUT - AddHint|int TaskNumber|str TaskHint
// BAMCHAN IN - TaskCP|
// BAMCHAN IN - TaskCP|str 999
// BAMCHAN OUT - DoneAdv|str AdventureName|str AdventureText|key AdventureDoneUUID
//============================================================================
// Global Constants
//============================================================================
integer FLAG_DEBUG;
integer REPLY_FLAG = FALSE; // flag to store if we got a reply in time or not for InAdv?
string CHAN_PREFIX = "0x"; // random chat channel prefix
string CHAT_DIVIDER = ", "; // to comma-separate elements in emitted chat elements
string MSG_STARTUP = "Baroun's Adventure Machine is activating.";
string DEBUG_LISTEN_CHANNEL= "BAM Listening on channel: "; // debug message to see picked channel
string API_INADV_QUERY = "InAdv?"; // API trigger to check if in an adventure.
string API_TASKCP_QUERY = "TaskCP?"; // task complete?
string API_INADV_RESPONSE = "InAdv"; // confirms player is already in an adventure
string DIV1 = "|"; // divides fields of API messages
string API_NONE = "NONE"; // magic text if player not in an adventure
string API_OFFER_ADV = "OfferAdv"; // offer player an adventure
string API_ACCEPT_ADV = "AcceptAdv"; // player accepts adventure
string API_ADD_TASK = "AddTask"; // add a task to player
string API_ADD_HINT = "AddHint"; // add a hint for the current task
string API_TASKCP_RESPONSE = "TaskCP"; // task completed
string API_DONEADV = "DoneAdv"; // adventure done
string MSG_NEED_HUD = ", you'll need Baroun's Adventure Machine HUD to join this adventure. Here's one for you to wear.";
string INVENTORY_HUD = "Baroun's Adventure Machine HUD"; // name of HUD item to give from inventory.
string INVENTORY_NOTE = "BAM Baroun's Adventure Machine Instructions"; // name of notecard to give
string MSG_HUDMENU = "Would you like a copy of Barouns Adventure Machine to join into adventures?";
list MENU = ["YES","NO","CLOSE"];
string API_RUMOR_FIND = "RUMOR_SERVER_FIND";
string API_RUMOR_FOUND = "RUMOR_SERVER_FOUND";
string API_RUMOR_PUT = "RUMOR_PUT";
integer CHAN_REGION = -999; // Myriad Region Server
integer HAND_OBJECT; // object channel handle for rumors
integer CHAN_OBJECT; // object channel for rumors
//============================================================================
// Runtime Globals
//============================================================================
list Recent; // list of [UUID,unixtime] who recently collided with this goal
list GotPrizes; // list of who got prizes [UUID,unixtime]
key AVKEY; // UUID of the avatar we are interacting with
string AVNAME; // String name of the avatar we are interacting with
integer CHANBAM; // Channel we listen on
integer CHANTARGET; // channel of thing we're talking to
integer MENUHAND; // llListenRemove handle for Menu chat channel
integer CHAN_RUMOR; // what channel is the rumor server listening on?
//============================================================================
// Runtime Globals
//============================================================================
DEBUG(string debugmsg) {
if ( FLAG_DEBUG == TRUE ) {
llInstantMessage(llGetOwnerKey(llGetKey()),"DEBUG: "+debugmsg);
}
}
//============================================================================
// DEFAULT STATE
//============================================================================
default {
//------------------------------------------------------------------------
// LISTEN EVENT
//------------------------------------------------------------------------
listen(integer chan, string name, key id, string msg) {
name = ""; // LSLINT
if ( chan == -1 ) {
if ( msg == "YES" ) { // player responded yes to menu asking if they want a HUD
llGiveInventory(id,INVENTORY_HUD); // send them the HUD
llGiveInventory(id,INVENTORY_NOTE); // send them instructions
return; // exit early in case we decide to add more commands, and we're done with this one
}
llListenRemove(MENUHAND); // done with stuff on menu channel, remove the listener
llSetTimerEvent(0.0); // stop a running timer
REPLY_FLAG = FALSE; // reset the reply flag
return; // exit early since we're done with menu channel
}
if ( msg == API_RUMOR_FOUND ) {
CHAN_RUMOR = (integer)("0x" + llGetSubString((string)id,0,6));
llOwnerSay("Rumor server found on channel "+(string)CHAN_RUMOR);
if ( CHAN_OBJECT != 0 ) llListenRemove(CHAN_OBJECT); // remove the unneeded object channel listener
return;
}
list tokens = llParseString2List(msg, [DIV1], []); // split incoming message apart using |
string command = llList2String(tokens, 0); // the first part of the message is a command
string data = llList2String(tokens, 1); // the second part is command-specific data
// if they answer they are in an adventure, react accordingly
if ( command == API_INADV_RESPONSE ) { // In An Adventure Response
REPLY_FLAG = TRUE; // we got a response from a HUD, remember it
if ( data == API_NONE ) { // responded with no current adventure
llSay(PUBLIC_CHANNEL,AVNAME+CHAT_DIVIDER+ADV_ATTRACT);
llSay(CHANTARGET,API_OFFER_ADV+DIV1+ADVNAME+DIV1+ADVTEXT); // offer one
return;
}
if ( data == ADVNAME ) { // already in the current adventure
llSay(CHANTARGET,API_TASKCP_QUERY); // so let's check if they have completed adventure?
return;
}
}
// If they are accepting the adventure, hand them first task
if ( command == API_ACCEPT_ADV ) { // accepting an adventure
if ( data == ADVNAME ) { // accepting this nodes adventure
llSay(PUBLIC_CHANNEL,ADVTEXT);
llSay(CHANTARGET,API_ADD_TASK+DIV1+(string)ADVTASKTDNUM+DIV1+ADVTASKTODO); // give them first task
llSay(CHANTARGET,API_ADD_HINT+DIV1+(string)ADVTASKTDNUM+DIV1+ADVTASKTODOHINT); // give them first task hint
DEBUG(llKey2Name(llGetOwnerKey(id))+MSG_OWNER_STARTADV); // tell owner adventure begins
if ( CHAN_RUMOR != 0 ) {
string who = llKey2Name(llGetOwnerKey(id));
llRegionSay(CHAN_RUMOR,API_RUMOR_PUT+DIV1+who+DIV1+who+MSG_OWNER_STARTADV);
}
return; // done with this chat command, exit early
}
}
if ( command == API_TASKCP_RESPONSE ) { // player sends TaskCP|task# to NPC
string playertasks = llList2CSV(llListSort(llCSV2List(data),1,TRUE));
string alltasks = llList2CSV(llListSort(llCSV2List(ADV_ALL_TASKS),1,TRUE));
if ( playertasks == alltasks ) { // if all tasks are in done list, then adventure is done
// tell player HUD that the adventure is done
llSay(CHANTARGET,API_DONEADV+DIV1+ADVNAME+DIV1+ADVDONETEXT+DIV1+ADVDONEUUID);
// tell player and public that adventure is done
llSay(PUBLIC_CHANNEL,llKey2Name(llGetOwnerKey(id))+CHAT_DIVIDER+ADVDONETEXT);
// give player the prize if one is defined
if (PRIZENAME!="NONE" && llListFindList(GotPrizes,[llGetOwnerKey(id)]) == -1 ) {
llGiveInventory(llGetOwnerKey(id),PRIZENAME);
GotPrizes = [ llGetOwnerKey(id), (llGetUnixTime() + PRIZEWAIT) ] + GotPrizes; // remember who and when
}
// send message to quest OWNER that a player finished and got the prize
DEBUG(llKey2Name(llGetOwnerKey(id))+MSG_OWNER_DONEADV);
if ( CHAN_RUMOR != 0 ) {
string who = llKey2Name(llGetOwnerKey(id));
llRegionSay(CHAN_RUMOR,API_RUMOR_PUT+DIV1+who+DIV1+who+MSG_OWNER_DONEADV);
}
return; // exit early
}
// player has not completed last quest task, so tell them there is more to do
llSay(PUBLIC_CHANNEL,llKey2Name(llList2String(llGetObjectDetails(id,[OBJECT_OWNER]),0))+MSG_ADV_INCOMPLETE);
return; // exit early in case we add more chat commands
}
}
//------------------------------------------------------------------------
// SENSOR
//------------------------------------------------------------------------
sensor(integer num_sensed) {
while (num_sensed--) { // count down through all touches in this event
AVKEY=llDetectedKey(num_sensed); // get the UUID of the toucher
if ( llListFindList(Recent,[AVKEY]) == -1 ) { // player not in list of people recently completing the quest
AVNAME=llDetectedName(num_sensed); // get the name of the toucher
CHANTARGET = (integer)(CHAN_PREFIX + llGetSubString((string)AVKEY,-7,-1));
llSay(CHANTARGET, API_INADV_QUERY); // ask player if they are in adventure, must be within 10m for effect
}
}
}
//------------------------------------------------------------------------
// STATE_ENTRY EVENT
//------------------------------------------------------------------------
state_entry() {
FLAG_DEBUG = FALSE; // do we want debug messages
FLAG_USESENSOR = FALSE; // use the sensor or not?
llParticleSystem([]); // shut off any running particles
llSetText(MSG_NPCNAME,MSG_SETTEXT_COLOR,MSG_SETTEXT_ALPHA);
DEBUG(MSG_STARTUP); // tell the owner we are initializing
CHANBAM = (integer)(CHAN_PREFIX + llGetSubString((string)llGetKey(),-7,-1)); // calculate channel to listen on
DEBUG(DEBUG_LISTEN_CHANNEL+(string)CHANBAM);
llListen(CHANBAM,"",NULL_KEY,""); // listen on channel for all messages from any name, name UUID, and any message
if ( CHAN_OBJECT != 0 ) llListenRemove(CHAN_OBJECT); // remove the unneeded object channel listener
CHAN_OBJECT = (integer)(CHAN_PREFIX + llGetSubString((string)llGetKey(),0,6)); // calculate channel to listen on
HAND_OBJECT = llListen(CHAN_OBJECT,"",NULL_KEY,""); // listen for rumor server
llSetTimerEvent(EVENTTIMER); // fire off a timer event once an hour
llRegionSay(CHAN_REGION,API_RUMOR_FIND); // send a region message to see if there is a rumor server
if ( FLAG_USESENSOR == TRUE ) {
llSensorRepeat("",NULL_KEY,AGENT,SENSOR_RANGE,SENSOR_ARC,SENSOR_REPEAT); // start NPC looking for someone to help
}
}
//------------------------------------------------------------------------
// TIMER EVENT
//------------------------------------------------------------------------
timer() {
if ( REPLY_FLAG == FALSE && FLAG_USESENSOR == TRUE) {
// no response to test if player already wearing HUD, so ask them if they want one
MENUHAND = llListen(-1,"",NULL_KEY,""); // set up a listen channel for the menu to reply on
// Send them the menu
llDialog(AVKEY,MSG_HUDMENU,MENU,-1);
llSay(PUBLIC_CHANNEL,AVNAME+MSG_NEED_HUD); // tell player they need a HUD to play
REPLY_FLAG = TRUE; // set a reply flag so we don't keep sending more HUDs
}
llSetTimerEvent(0.0); // remove the timer
// on timer, check memory left and clear recent list if needed
integer freemem = llGetFreeMemory(); // how much memory free?
if ( freemem < 1024 ) { // is it too little?
DEBUG("Memory low for "+llGetObjectName()+" in "+llGetRegionName()+". Resetting RECENT list.");
Recent=[]; // clear the recent list
GotPrizes=[]; // clear the gotPrizes list
return; // exit timer event, no sense in processing lists further since we just emptied them
}
// check to see if entries in Recent list have expired
integer i; // temporary index number into list
list temprecent = []; // temporary list to hold entries we want to keep
key who; // temporary place to keep the keys we process in the lists
integer time; // temporary place to keep the time we process in the lists
for (i = 0; i < llGetListLength(Recent); i += 2) { // step through strided list from begin to end
who = llList2Key(Recent,i); // get the UUID for this list stride
time = llList2Integer(Recent,i+1); // get the integer time for this list stride
if ( llGetUnixTime() < time ) temprecent = [who,time] + temprecent; // non expired, keep this entry
}
Recent = temprecent; // now, replace the Recent list with the pruned version
// check to see if entries in GotPrizes list have expired
temprecent = []; // clear the temp list again
for (i = 0; i < llGetListLength(GotPrizes); i += 2) { // step through next strided list
who = llList2Key(GotPrizes,i); // get the uuid for this list stride
time = llList2Integer(GotPrizes,i+1); // get the integer time for this list stride
if ( llGetUnixTime() < time ) temprecent = [who,time] + temprecent; // non expired, keep this entry
}
GotPrizes = temprecent; // replace the gotprizes list with the pruned one
}
//------------------------------------------------------------------------
// TOUCH_START EVENT
//------------------------------------------------------------------------
touch_start(integer times_touched) {
while (times_touched--) { // count down through all touches in this event
AVKEY=llDetectedKey(times_touched); // get the UUID of the toucher
AVNAME=llDetectedName(times_touched); // get the name of the toucher
float dist = llVecDist(llGetPos(),llList2Vector(llGetObjectDetails(AVKEY,[OBJECT_POS]),0)); // find distance between item and toucher
if ( dist <= TOUCH_RANGE ) { // is toucher within arms reach?
//if ( llListFindList(Recent,[AVKEY]) == -1 ) { // is player in recent list?
// Recent = [AVKEY,llGetUnixTime()+TRIGGERWAIT] + Recent; // no, add player to list
// // calculate player-specific BAM dynamic channel
CHANTARGET = (integer)(CHAN_PREFIX + llGetSubString((string)AVKEY,-7,-1));
llSay(CHANTARGET, API_INADV_QUERY); // ask player if they are in adventure, must be within 10m for effect
//}
}
}
REPLY_FLAG = FALSE; // set a flag to track if we get a reply or not...
}
}
//============================================================================
// END
//============================================================================