// Myriad_Lite_Module_Resilience-v0.0.2-20120826.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/
// CONSTANTS - DO NOT CHANGE DURING RUN
string VERSION = "0.0.2"; // Allen Kerensky's script version
string VERDATE = "20120826"; // Allen Kerensky's script yyyymmdd
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 MINSKILL = 1; // min value for skill rank
integer MAXSKILL = 5; // max value for skill rank
integer CHANMYRIAD = -999; // chat sent to ALL Myriad players in region
float RESPAWN_TIME = 30.0; // time dead before automatic respawn
string DIV = "|"; // message field divider
string ANIM_INCAPACITATED = "sleep"; // anim when incapacitated
string ANIM_DEAD = "dead"; // anim when dead
integer MAXARMOR = 5; // max legal armor rating
// Module to Module Messaging Constants
// integer MODULE_HUD = -1;
// integer MODULE_CHARSHEET = -2;
// integer MODULE_ARMOR = -3;
// integer MODULE_BAM = -4;
// integer MODULE_RUMORS = -5;
// integer MODULE_CLOSE = -6;
// integer MODULE_RANGED = -7;
integer MODULE_RESILIENCE = -8;
// integer MODULE_PROGRESS = -9;
// integer MODULE_WELL = -10;
// integer MODULE_METER = -11;
integer LM_SENDTOATTACHMENT = 0x80000000;
// RUNTIME GLOBALS - CAN CHANGE DURING RUN
integer FLAG_DEBUG; // see debug messages?
key PLAYERID = NULL_KEY; // cached player UUID
string PLAYERNAME = ""; // cached player name
string NAME = ""; // character name
string SPECIES = ""; // character species
string BACKGROUND = ""; // character childhood history
string CAREER = ""; // character career or faction
list STATISTICS = [];
list RESILIENCES = [];
list CURRENT_RESILIENCES = [];
list SKILLS = []; // skills [ string SkillName, integer SkillRank ]
integer FLAG_ANIMATE; //
integer FLAG_INCAPACITATED; // incapacitated by wounds?
integer FLAG_DEAD; // killed by critical wounds?
vector MOVELOCK = <0,0,0>; // movelock position when incapacitated or dead
float TAU = 0.05; // movelock tau
integer CURARMOR = 0; // highest armor value worn out of all armor worn, not a total
// FIXME MOVE THIS TO ARMOR AND SEND LINK MESSAGES
integer CHANATTACH = 0; // dynamic channel for attachments
vector DEATHPOINT; // OPENSIM ONLY coordinates to move to on death
vector RESPAWNPOINT; // OPENSIM ONLY coordinates to move to on respawn
// 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!");
llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"DEAD",PLAYERID);
// FIXME if ( DEATHPOINT != ZERO_VECTOR ) osTeleportAgent(llGetKey(),DEATHPOINT);
llSetTimerEvent(RESPAWN_TIME); // respawn in a bit
}
// DEBUG - show debug chat with wearer name for sorting
DEBUG(string dmessage) {
if ( FLAG_DEBUG == TRUE ) { // are we debugging?
llSay(DEBUG_CHANNEL,"("+llKey2Name(PLAYERID)+") Module Resilience: "+dmessage);
}
}
// DEBUGOFF - turn off the DEBUG flag
DEBUGOFF() {
DEBUG("Debug Mode Deactivated");
FLAG_DEBUG = FALSE; // set debug flag to FALSE
}
// DEBUGON - turn on the DEBUG flag
DEBUGON() {
FLAG_DEBUG = TRUE; // set debug flag TRUE
DEBUG("Debug Mode Activated");
}
// ERROR - show errors on debug channel with wearer name for sorting
ERROR(string emessage) {
llSay(DEBUG_CHANNEL,"ERROR ("+llKey2Name(PLAYERID)+"): "+emessage);
}
// GET_MAX_RESILIENCE
integer GET_MAX_RESILIENCE(string name) {
integer pos = llListFindList(RESILIENCES,[name]);
if ( pos >= 0 ) {
return llList2Integer(RESILIENCES,pos + 1);
}
return 0;
}
// GET_RESILIENCE
integer GET_RESILIENCE(string name) {
integer pos = llListFindList(CURRENT_RESILIENCES,[name]);
if ( pos >= 0 ) {
return llList2Integer(CURRENT_RESILIENCES,pos + 1);
}
return 0;
}
// GETVERSION
GETVERSION() {
SENDTOHUD("VERSION="+VERSION+DIV+"VERSIONDATE="+VERDATE+DIV+llGetObjectName());
}
// 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
llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"ALIVE",PLAYERID);
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
llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"REVIVED",PLAYERID);
revived = TRUE; // show revival in summary report
DEBUG("Heal: Revived!");
llStopMoveToTarget();
MOVELOCK = <0,0,0>;
}
} // 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
}
// 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,"ARMOREFFECTHIT");
} 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,"ARMOREFFECTBLOCKED");
}
}
// INCAPACITATED - player lost all WOUNDS - unable to act
INCAPACITATED() {
FLAG_INCAPACITATED = TRUE; // yes, we're now incapacitated
if ( MOVELOCK == <0,0,0> ) 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!");
llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"INCAPACITATED",PLAYERID);
llSetTimerEvent(RESPAWN_TIME); // heal in a bit
}
// METER - update a hovertext health meter or HUD bar graph
METER() {
llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"METER",PLAYERID);
}
// RESET - shut down running animations then reset the script to reload character sheet
RESET() {
llResetScript(); // now reset
}
// RPEVENT
// FIXME - change all RPEVENT to link message for main HUD to send?
RPEVENT(string rpevent) {
llRegionSay(CHANMYRIAD,"RPEVENT|"+NAME+" ("+PLAYERNAME+") "+rpevent);
}
// SET_RESILIENCE
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);
}
}
// SENDTOHUD - send reponses to HUD as Link Messages
SENDTOHUD(string str) {
DEBUG("SENDTOHUD("+str+")");
llMessageLinked(LINK_THIS,LM_SENDTOATTACHMENT,str,PLAYERID);
}
// SETUP - begin bringing the HUD online
SETUP() {
FLAG_DEBUG = FALSE;
PLAYERID = llGetOwner(); // remember the owner's UUID
PLAYERNAME = llKey2Name(PLAYERID); // remember the owner's legacy name
CHANATTACH = (integer)("0x"+llGetSubString((string)PLAYERID,1,7)); // attachment-specific channel
llRegionSay(CHANMYRIAD,"GET_REGION_SETTING|DEATHPOINT");
llRegionSay(CHANMYRIAD,"GET_REGION_SETTING|RESPAWNPOINT");
llRequestPermissions(PLAYERID,PERMISSION_TRIGGER_ANIMATION);
}
// 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
}
// DEFAULT STATE - load character sheet
default {
// CHANGED - triggered for many changes to the avatar
// TODO reload sim-specific settings on region change
changed(integer changes) {
if ( changes & CHANGED_REGION || changes & CHANGED_TELEPORT ) {
llRequestPermissions(PLAYERID,PERMISSION_TRIGGER_ANIMATION);
}
}
link_message(integer sender_num,integer sender,string str,key id) {
if ( sender == MODULE_RESILIENCE || sender == LM_SENDTOATTACHMENT ) return; // ignore our own link messages
DEBUG("EVENT: link_message("+(string)sender_num+","+(string)sender+","+str+","+(string)id+")");
list fields = llParseString2List(str,[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
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
// Module Resilience specific commands
if ( command == "set_name" ) {
NAME = llList2String(subfields,1); // set the name
return;
}
if ( command == "set_species" ) {
SPECIES = llList2String(subfields,1); // set the species;
return;
}
if ( command == "set_background" ) {
BACKGROUND = llList2String(subfields,1); // set the species;
return;
}
if ( command == "set_career" ) {
CAREER = llList2String(subfields,1); // set the species;
return;
}
if ( command == "set_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);
}
return;
}
if ( command == "set_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);
}
return;
}
if ( command == "set_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);
}
return;
}
if ( command == "armorcurrent" ) { // ARMORCURRENT|integer newcurrentarmor
integer rating = llList2Integer(fields,1);
if ( rating >= 0 && rating <= MAXARMOR ) {
CURARMOR = rating;
}
return;
}
if ( command == "debugoff" ) { DEBUGOFF(); return; }
if ( command == "debugon" ) { DEBUGON(); return; }
if ( command == "hit") {
integer attdice = llList2Integer(fields,1);
if ( attdice >= 1 && attdice <= 5 ) {
HIT(attdice);
}
return;
}
if ( command == "healpartial" ) {
integer healpart = llList2Integer(fields,1);
HEAL(healpart);
return;
}
if ( command == "healfull" ) { HEAL(100); return; } // FIXME HEAL ALL means 100 points of healing
if ( command == "reset" ) { RESET(); return;}
if ( command == "version" ) { GETVERSION(); return; } // show the version
if ( command == "region_setting" ) { // look for deathpoint and respawnpoint
string attrib = llToLower(llStringTrim(llList2String(subfields,0),STRING_TRIM)); // find the boon name
vector pos = llList2Vector(subfields,1); // find the boon rank value
if ( attrib == "deathpoint" ) { DEATHPOINT = pos; }
if ( attrib == "respawnpoint" ) { RESPAWNPOINT = pos; }
return;
}
//COMMAND(str); // send to shared command processor for chat and link messages
return;
} // end of link_message event
// STATE ENTRY - called on Reset
state_entry() {
SETUP(); // show credits and start character sheet load
}
// TIMER - scheduled events
timer() {
// Respawn timer ended
if ( FLAG_DEAD == TRUE ) { // if dead
RPEVENT("respawns!");
llMessageLinked(LINK_THIS,MODULE_RESILIENCE,"ALIVE",PLAYERID);
//FIXME if ( RESPAWNPOINT != ZERO_VECTOR ) osTeleportAgent(llGetKey(),RESPAWNPOINT);
if ( RESPAWNPOINT != ZERO_VECTOR ) llTeleportAgentHome(PLAYERID);
HEAL(100); // heal 100 points of damage to respawn
}
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
}
}
} // end state running
// END