User:Allen Kerensky/Myriad Lite/Myriad Lite NPC Critter Goon-v0.0.1-20121227.lsl
From OpenSimulator
< User:Allen Kerensky | Myriad Lite
Revision as of 11:57, 31 December 2012 by Allen Kerensky (Talk | contribs)
Myriad_Lite_NPC_Critter_Goon-v0.0.1-20121227.lsl
// Myriad_Lite_NPC_Critter_Goon-v0.0.1-20121227.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/ //============================================================================ // MESSAGE FORMAT REFERENCE //============================================================================ // LINK_THIS,MODULE_GOON,"ARMOREFFECTHIT",NULL_KEY // LINK_THIS,MODULE_GOON,"ARMOREFFECTBLOCKED",NULL_KEY // LINK_THIS,MODULE_GOON,"WOUNDED",NULL_KEY // LINK_THIS,MODULE_GOON,"DEAD",NULL_KEY // CHANPLAYER IN - DEPRECATED - HITCHECK|int attackstat|int attackskill|int attackdice|key owner|str name // CHANPLAYER IN - RANGEDHIT|int attackstat|int attackskill|int attackdice|key weaponowner|str name // CHANPLAYER IN - CLOSEHIT|int attackstat|int attackskill|int attackdice|key weaponowner|str name string VERSION = "0.0.1"; // version number string VERDATE = "20121227"; // version date //============================================================================ // CRITTER/GOON CUSTOMIZATION //============================================================================ // By far the simplest kind of NPC is the Goon, sometimes called the Critter when its an animal such as a bear or a jackal. // The goon is bred for one thing only: conflict // The only ever roll a single die during any test and have only two skills, attack and defense. // The former is added to attack rolls and the latter to defense, surprisingly enough. // MORTAL COMBAT STATS integer POWER = 1; // attack stat - 1 dice rolled on attack integer SKILL_ATTACK = 1; // attack skill //list RANGED_ATTACKS = []; // list of ranged attacks //list MSG_RANGED_HIT = []; // list of attack hits messages //list MSG_RANGED_MISS = []; // list of attack miss messages //list CLOSE_MELEE_ATTACKS = []; // list of attack types - claw, bite, sting, etc //list MSG_MELEE_HIT = []; // list of attack hits messages //list MSG_MELEE_MISS = []; // list of attack miss messages float MELEE_WEAPON_LENGTH = 2.0; // a six-foot longsword is the default //list CLOSE_UNARMED_ATTACKS = []; // list of attack types - claw, bite, sting, etc //list MSG_UNARMED_HIT = []; // list of attack hits messages //list MSG_UNARMED_MISS = []; // list of attack miss messages float UNARMED_WEAPON_LENGTH = 1.0; // thrown punch, use 1.5 for kicks integer RANGED_DAMAGE_DICE = 1; // thrown rock integer MELEE_DAMAGE_DICE = 4; // long sword integer UNARMED_DAMAGE_DICE = 1; // fists integer GRACE = 1; // defense stat FIXME implement defense integer SKILL_DEFENSE = 1; // defense skill FIXME implement defense integer ARMOR; // armor worn FIXME implement armor check // Since they are specialized for combat, they also have a number of resilience boxes based on how tough they are, although like specialists they don't have critical boxes and are out of the fight when all their wounds are gone. integer RESILIENCE; // 1-20 how many hits/damage this goon can take list TREASURE; // list of treasure items dropped when killed or searched while incapacitated //integer FLAG_AGGRESSIVE; // if TRUE = monster does not flee? //integer FLAG_FOLLOWER; // if TRUE = follows after nearest av? //integer FLAG_GUARD; // if TRUE = guards some position //integer FLAG_WANDER; // if TRUE = wanders in an area integer FLAG_FACEENEMY; // if true, use llLookAt to face enemy before attacking integer FLAG_TARGETPOLICY; // ranged combat integer TARGET_NEAREST = 0; integer TARGET_RANDOM = 1; integer FLAG_TARGETINCAP; // FIXME target and shoot visibly incapacitated targets? // Although goons are primarily used in mortal combat there is no reason why they could be used in other forms of conflict too, but most goons are only useful in one specific kind of conflict. //============================================================================ // GLOBAL VARIABLES //============================================================================ integer MODULE_GOON=-12; // for link messages integer MINSTAT = 1; // minimum value of a statistic integer MAXSTAT = 10; // maximum value of a statistic integer MINSKILL = 1; // minimum value of a skill integer MAXSKILL = 5; // maximum value of a skill integer MINDAMAGE = 1; // minimum damage dice a weapon can inflict integer MAXDAMAGE = 5; // maximum damage dice a weapon can inflict float HEARTRATE = 0.5; float SENSOR_RANGE = 96.0; float SENSOR_ARC = PI; float SENSOR_RATE = 5.0; float BULLET_VELOCITY = 30.0; // change this to change the speed of the bullet. string GUNSOUND = "pistol_shot.wav"; // string; name of sound in inventory string AMMO = "Myriad Lite Bullet Turret v0.0.0 20120511"; //name of desired object to be shot out. Must be in the inventory of the "gun". vector REZ_OFFSET = <0,0, 2.1>; // rez offset for bullet string DIV = "|"; integer CHANMYRIAD = -999; // Myriad Region Channel string ANIM_DEAD = ""; // name of animation file in NPC inventory if used on osNPC - does nothing on prim NPCs float RESPAWN_TIME = 15.0; // 10 seconds to respawn //============================================================================ // RUNTIME //============================================================================ integer FLAG_DEBUG; integer HANDLE_PUB; integer CHANOBJECT; // channel the target listens on for attacks integer HANDOBJECT; // chat channel handle to remove channel later if needed vector POS; rotation ROT; vector OFFSET; integer ANTIDELAY; // use antidelay nodes for rapid fire? integer ISACTIVE; integer FLAG_DIE; // does NPC delete on death? list DETECTED_KEY = []; // list of keys found in last sensor sweep list DETECTED_POS = []; // list of positions found for all keys in last sensor sweep list DETECTED_ROT = []; // list of rotations found for all keys in last sensor sweep integer FLAG_PLAYANIM; // play an osNPC animation integer FLAG_DEAD; // dead waiting for respawn? integer FLAG_RESPAWN; integer FLAG_ATTACK_ON_SIT; // attack someone trying to sit on critter? // MOVE WANDER settings float RANGE = 60.0; float MOVE_SPEED = 1.0; float ROT_SPEED = 3.0; float STRENGTH = .2; float DELAY = 2.5; float ZOFFSET = 0.0; integer MODE_OFF = 0; integer MODE_WANDER = 1; integer MODE_REMOTE = 2; integer MODE_FOLLOW = 3; integer MODE_PATROL = 4; integer MODE_GUARD = 5; integer RESTRICT_Z; // restrict MODE_WANDER target point within a range of Z altitudes integer MINZ = 0; integer MAXZ = 30; integer MODE; vector REZ_POINT; vector NEXT_POINT; integer FOUND; string REZ_PARCEL; integer KEEP_IN_PARCEL; // if TRUE, new picked MODE WANDER points must be on same parcel as rezpoint integer COUNT; float FINDANGLE; float X; // x component of a position float Y; // y component of a position float DIST; // distance vector PREVPOS; // previous position vector MIN; //============================================================================ // 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 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 ABILITY_TEST_OPPOSED(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 } //============================================================================ // MORTAL COMBAT - MAKE RANGED ATTACK //============================================================================ ATTACK_RANGED() { if ( llGetListLength(DETECTED_KEY) == 0 ) return; // no targets, so return key target; vector target_pos; rotation target_rot; if ( FLAG_TARGETPOLICY == TARGET_NEAREST ) { // target nearest target = llList2Key(DETECTED_KEY,0); target_pos = llList2Vector(DETECTED_POS,0); target_rot = llList2Rot(DETECTED_ROT,0); } if ( FLAG_TARGETPOLICY == TARGET_RANDOM ) { integer dieroll = 1+(integer)llFrand((float)llGetListLength(DETECTED_KEY)); // reasonably uniform distribution die roll target = llList2Key(DETECTED_KEY,dieroll - 1); target_pos = llList2Vector(DETECTED_POS,dieroll - 1); target_rot = llList2Rot(DETECTED_ROT,dieroll - 1); } if ( FLAG_FACEENEMY == TRUE ) llLookAt(target_pos,1,1); float target_dist = llVecDist(llGetPos(),target_pos); if(llVecDist(llGetPos(),target_pos+llRot2Fwd(target_rot)*target_dist) < 1.5) { // Fire 1 bullet,, the heart of the firearm script. POS = llGetPos(); // get our current position ROT = llGetRot(); // get our current rotation OFFSET = REZ_OFFSET; // start with the base offset for the gun held in the right hand OFFSET *= ROT; // now, rotate the offset to match the avatar rotation POS += OFFSET; // now combine the rotated offset with avatar position vector fwd = llRot2Up(ROT); // calculate the direction that is "avatar's facing" fwd *= BULLET_VELOCITY; // now multiply that by bullet speed to tell bullet to push in that direction, that fast //rot *= llEuler2Rot(<0, PI_BY_TWO, 0>); // now, straighten rotation for object we're about to rez llPlaySound(GUNSOUND,1.0); // here "GUNSOUND"is a variable defined above. // DAMAGEDICE is passed to rez-param of bullet. Myriad Bullets read this as damage dice to do if they hit if ( ANTIDELAY == FALSE ) { llRezObject(AMMO, POS, fwd, ROT, RANGED_DAMAGE_DICE); // does the actual work rezzes the ammo in the specified variables. } else { llMessageLinked(LINK_THIS,-123,AMMO+"~~~"+(string)POS+"~~~"+(string)fwd+"~~~"+(string)ROT+"~~~"+(string)RANGED_DAMAGE_DICE,"rezobject"); } //llSleep(RATE); // force a pause between shots } } //============================================================================ // MORTAL COMBAT - MAKE CLOSE MELEE ATTACK //============================================================================ ATTACK_CLOSE_MELEE() { if ( llGetListLength(DETECTED_KEY) == 0 ) return; // no targets, so return // target nearest key target = llList2Key(DETECTED_KEY,0); vector target_pos = llList2Vector(DETECTED_POS,0); //rotation target_rot = llList2Rot(DETECTED_ROT,0); if ( FLAG_FACEENEMY == TRUE ) llLookAt(target_pos,1,1); vector A = ( llGetPos() + < MELEE_WEAPON_LENGTH,0,0> * llGetRot()); // Is defender center within 1m of my calculated point in front of me? // If so, my sword had a chance to hit when I swung it. if ( llVecDist(A,target_pos) < 1.0 ) { integer dynchan = (integer)("0x"+llGetSubString((string)llGetOwner(),0,6)); // calculate attackers HUD dynamic channel // send the close combat skill check message to attacker to start close combat skill check // attackers hud adds attacker stat and skill and sends the information to the victim to finish the opposed close combat skill check // we region say this so others can keep score or detect cheaters, etc llRegionSay(dynchan,"CLOSECOMBAT"+DIV+(string)MELEE_DAMAGE_DICE+DIV+(string)target+DIV+(string)llGetOwner()+DIV+llGetObjectName()); key owner = llList2Key(llGetObjectDetails(target,[OBJECT_OWNER]),0); // is this an agent/avatar for sure? if ( target == owner ) { // yep, we hit an avatar // tell the region an attempted attack is underway RPEVENT(llKey2Name(llGetOwner())+" strikes at "+llList2String(llGetObjectDetails(target,[OBJECT_NAME]),0)+" in Close Melee Combat!"); } } } //============================================================================ // MORTAL COMBAT - MAKE CLOSE UNARMED ATTACK //============================================================================ ATTACK_CLOSE_UNARMED() { if ( llGetListLength(DETECTED_KEY) == 0 ) return; // no targets, so return // target nearest key target = llList2Key(DETECTED_KEY,0); vector target_pos = llList2Vector(DETECTED_POS,0); //rotation target_rot = llList2Rot(DETECTED_ROT,0); if ( FLAG_FACEENEMY == TRUE ) llLookAt(target_pos,1,1); vector A = ( llGetPos() + < UNARMED_WEAPON_LENGTH,0,0> * llGetRot()); // Is defender center within 1m of my calculated point in front of me? // If so, my sword had a chance to hit when I swung it. if ( llVecDist(A,target_pos) < 1.0 ) { integer dynchan = (integer)("0x"+llGetSubString((string)llGetOwner(),0,6)); // calculate attackers HUD dynamic channel // send the close combat skill check message to attacker to start close combat skill check // attackers hud adds attacker stat and skill and sends the information to the victim to finish the opposed close combat skill check // we region say this so others can keep score or detect cheaters, etc llRegionSay(dynchan,"CLOSECOMBAT"+DIV+(string)UNARMED_DAMAGE_DICE+DIV+(string)target+DIV+(string)llGetOwner()+DIV+llGetObjectName()); key owner = llList2Key(llGetObjectDetails(target,[OBJECT_OWNER]),0); // is this an agent/avatar for sure? if ( target == owner ) { // yep, we hit an avatar // tell the region an attempted attack is underway RPEVENT(llKey2Name(llGetOwner())+" strikes at "+llList2String(llGetObjectDetails(target,[OBJECT_NAME]),0)+" in Close Unarmed Combat!"); } } } //============================================================================ // COMMAND - process CHAT or LINK MESSAGE commands //============================================================================ COMMAND(string cmd) { list tokens = llParseString2List(cmd,["|"],[]); string command = llToLower(llStringTrim(llList2String(tokens,0),STRING_TRIM)); if ( command == "attack_ranged" ) { ATTACK_RANGED(); return; } if ( command == "attack_close_melee" ) { ATTACK_CLOSE_MELEE(); return; } if ( command == "attack_close_unarmed" ) { ATTACK_CLOSE_UNARMED(); return; } if ( command == "defend_ranged" ) { DEFEND_RANGED(); return; } if ( command == "defend_close_melee" ) { DEFEND_CLOSE_MELEE(); return; } if ( command == "defend_close_unarmed" ) { DEFEND_CLOSE_UNARMED(); return; } if ( command == "move_off" ) { MODE = MODE_OFF; return; } if ( command == "move_wander" ) { MODE = MODE_WANDER; return; } if ( command == "move_remote" ) { MODE = MODE_REMOTE; return; } if ( command == "move_follow" ) { MODE = MODE_FOLLOW; return; } if ( command == "move_patrol" ) { MODE = MODE_PATROL; return; } if ( command == "move_guard" ) { MODE = MODE_GUARD; return; } } //============================================================================ // DAMAGE_CHECK //============================================================================ DAMAGE_CHECK(integer attackstat,integer attackskill, integer attackdice,key owner,string item) { // see if we're hit integer amihit = ABILITY_TEST_OPPOSED(attackstat,attackskill,GRACE,SKILL_DEFENSE); // attacker power+skill vs. defender grace+skill if ( amihit == TRUE ) { // we're hit! RPEVENT(llGetObjectName()+" attacked by "+llKey2Name(owner)+"'s "+item+" in mortal combat!"); DAMAGE_HIT(attackdice); // apply the hit } return; } //============================================================================ // DAMAGE_HIT - player is hit - check to see if attack dice breach armor // Making A Damage Roll (Myriad p25, Myriad Special Edition p31) //============================================================================ DAMAGE_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 > ARMOR ) { // attack roll stronger than armor worn? damagetaken++; // add a wound point } } // finished roll how did we do? if ( damagetaken > 0 ) { // we took damage if ( ARMOR > 0 ) { // wearing armor? tell them it was breached RPEVENT(llGetObjectName()+"'s armor penetrated and wounded by that attack."); llMessageLinked(LINK_THIS,MODULE_GOON,"ARMOREFFECTHIT",NULL_KEY); } else { // fighting in no armor? RPEVENT(llGetObjectName()+" wounded!"); } DAMAGE_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 RPEVENT(llGetObjectName()+"'s armor blocked damage from that attack!"); llMessageLinked(LINK_THIS,MODULE_GOON,"ARMOREFFECTBLOCKED",NULL_KEY); } } //============================================================================ // DAMAGE_WOUNDED - Player takes Resilience damage //============================================================================ DAMAGE_WOUNDED(integer amount) { llMessageLinked(LINK_THIS,MODULE_GOON,"WOUNDED",NULL_KEY); while (amount--) { // for each wound taken if ( RESILIENCE > 1 ) { // wound boxes left? RESILIENCE--; // scratch off one } else if ( RESILIENCE <= 1 ) { // Goon about to be out of wounds? RESILIENCE = 0; // force zero DAMAGE_ZZZ(); // show death } } // end while } //============================================================================ // DAMAGE_ZZZ - player is dead, kill them and wait to respawn //============================================================================ DAMAGE_ZZZ() { FLAG_DEAD = TRUE; // remember that we're now dead if ( FLAG_PLAYANIM == TRUE ) llStartAnimation(ANIM_DEAD); // start dead animation RPEVENT(llGetObjectName()+" has been killed!"); llMessageLinked(LINK_THIS,MODULE_GOON,"DEAD",NULL_KEY); // monster dead, if it carried treasure drop one if ( llGetListLength(TREASURE) > 0 ) { integer dieroll = 1+(integer)llFrand((float)llGetListLength(TREASURE)); llRezObject(llGetInventoryName(INVENTORY_OBJECT,dieroll),llGetPos(),ZERO_VECTOR,llGetRot(),0); } // Does critter/goon respawn on death? if ( FLAG_RESPAWN == TRUE) { llSleep(RESPAWN_TIME); // respawn in a bit RESET(); } // Does critter/goon die and disappear on death? if ( FLAG_DIE == FALSE ) return; llSleep(RESPAWN_TIME); // let dead critter/goon lay there dead for a bit... then // go into infinite loop trying to die while ( TRUE == TRUE ) { llDie(); } } //============================================================================ // DEBUG on debug channel //============================================================================ DEBUG(string debugmsg) { if ( FLAG_DEBUG == TRUE ) llSay(DEBUG_CHANNEL,"DEBUG: "+debugmsg); } //============================================================================ // MORTAL COMBAT - DEFEND AGAINST RANGED ATTACK //============================================================================ DEFEND_RANGED() { } //============================================================================ // MORTAL COMBAT - DEFEND AGAINST CLOSE MELEE ATTACK //============================================================================ DEFEND_CLOSE_MELEE() { } //============================================================================ // MORTAL COMBAT - DEFEND AGAINST CLOSE UNARMED ATTACK //============================================================================ DEFEND_CLOSE_UNARMED() { } //============================================================================ // ERROR() - report errors on debug channel //============================================================================ ERROR(string errmsg) { llSay(DEBUG_CHANNEL,"ERROR: "+errmsg); } //============================================================================ // HEARTBEAT - the main "brain" or thinking function - called from timer //============================================================================ HEARTBEAT() { POS = llGetPos(); ROT = llGetRot(); if ( MODE == MODE_OFF ) { } else if ( MODE == MODE_WANDER ) { MOVE_WANDER(); } else if ( MODE == MODE_REMOTE ) { MOVE_REMOTE(); } else if ( MODE == MODE_FOLLOW ) { MOVE_FOLLOW(); } else if ( MODE == MODE_PATROL ) { MOVE_PATROL(); } else if ( MODE == MODE_GUARD ) { MOVE_GUARD(); } } //============================================================================ // MOVE - move to a given point //============================================================================ MOVE(vector dest) { DIST = llVecDist(dest,llGetPos()); while(DIST > 1.0) { PREVPOS = llGetPos(); dest.z = llGround(ZERO_VECTOR) + ZOFFSET; //if ( llScriptDanger(dest) ) return; if ( dest.x > 255 || dest.x < 0 || dest.y > 255 || dest.y < 0 ) return; llSetPos(dest); if(llGetPos() == PREVPOS) { // Yipes! The object didnt move! Occurs with float error. DEBUG("MOVE: Recovered from position floating point error."); return; // return from this function immediately. } DIST = llVecDist(dest,llGetPos()); llSleep(MOVE_SPEED / 10.0); } } //============================================================================ // MOVEMENT - CHARGE ATTACKER //============================================================================ //MOVE_CHARGE() { // if ( FLAG_AGGRESSIVE == TRUE ) { // } //} //============================================================================ // MOVEMENT - FLEE AWAY FROM ATTACKER //============================================================================ //MOVE_FLEE() { // if ( FLAG_AGGRESSIVE == TRUE ) return; // do not flee when enraged/aggressive //} //============================================================================ // MOVE_FOLLOW - does critter/goon follow target //============================================================================ MOVE_FOLLOW() { // FIXME follower code } //============================================================================ // MOVEMENT - STAY BETWEEN ATTACKER AND POSITION TO GUARD //============================================================================ MOVE_GUARD() { // FIXME - intercept attacker - find point on circle at range on line between attack and defense point } //============================================================================ // MOVEMENT - PATROL A GIVEN LIST OF WAYPOINTS //============================================================================ MOVE_PATROL() { // FIXME - patrol a given list of waypoints } //============================================================================ //============================================================================ MOVE_REMOTE() { // FIXME - allow region owner to remote control critter/goon movement } //============================================================================ //============================================================================ //MOVE_STEPDOWN() { //} //============================================================================ //============================================================================ //MOVE_STEPUP() { //} //============================================================================ //============================================================================ //MOVE_TURN() { // turn left or right //} //============================================================================ // MOVE_WANDER - wander from point to point around a center point //============================================================================ MOVE_WANDER() { FOUND = FALSE; // reset the "found new point?" flag to false in prep for the next run. COUNT = 0; // reset the counter of number of times we've tried to find a new waypoint // pick a new waypoint constrained by a variety of rules while (!FOUND) { // The first constraint is a bounding box MIN = REZ_POINT - < RANGE, RANGE, RANGE >; // find the minimum bounds of the RANGED bounding box NEXT_POINT = MIN + < llFrand(RANGE * 2), llFrand(RANGE * 2), 0>; // pick a new random X,Y within bounding box NEXT_POINT.z = llGround( NEXT_POINT - llGetPos() ); // force next point Z to ground level at next point // The next constraint is to stay on parcel or not if ( KEEP_IN_PARCEL == TRUE ) { if ( REZ_PARCEL == llList2String(llGetParcelDetails(NEXT_POINT,[PARCEL_DETAILS_AREA]),0) ) { FOUND = TRUE; } } // The next constraint is to stay within an altitude band at the destination, i.e. pick only points in the valley not on cliff walls. if ( RESTRICT_Z == TRUE ) { if ( ( NEXT_POINT.z >= MINZ ) && ( NEXT_POINT.z <= MAXZ ) ) { FOUND = TRUE; } } // The next constraint is to check if there is a script problem with the target //if ( llScriptDanger(NEXT_POINT) ) FOUND = FALSE; // the next constraint is to stay within the sim if ( NEXT_POINT.x > 255 || NEXT_POINT.x < 0 || NEXT_POINT.y > 255 || NEXT_POINT.y < 0 ) FOUND = FALSE; // Now, check how many times we've tried to find a new waypoint. If too many, go back to rez point. if ( ++COUNT >= 100 ) { NEXT_POINT = REZ_POINT; FOUND = TRUE; } } llRotLookAt(MOVE_WANDER_HEADING(llGetPos(),NEXT_POINT),STRENGTH,ROT_SPEED); // turn to new heading MOVE(NEXT_POINT); // move worm to next wander point // Give lookat time to complete llSleep(llFrand(MOVE_SPEED+DELAY)); // give RotLookat time to finish llStopLookAt(); } //============================================================================ // MOVE_WANDER_HEADING - get a heading from one point to another for MOVE_WANDER //============================================================================ rotation MOVE_WANDER_HEADING(vector pos,vector newpos) { X = newpos.x - pos.x; Y = newpos.y - pos.y; if ( llFabs(X) < 0.000001 && llFabs(Y) < 0.000001 ) return ZERO_ROTATION; FINDANGLE = llAtan2(Y,X); return llEuler2Rot(<0,0,FINDANGLE>); } //============================================================================ // RESET - allow shutdown actions before resetting scripts //============================================================================ RESET() { // do any shutdown events // then, reset while ( TRUE == TRUE ) llResetScript(); // use while true to force reset } //============================================================================ // RPEVENT - send RPevents to region //============================================================================ RPEVENT(string rpmsg) { llRegionSay(CHANMYRIAD,"RPEVENT|"+rpmsg); } //============================================================================ // SETUP - CONFIGURE YOUR CRITTER/GOON //============================================================================ SETUP() { POWER = 1; // attack stat - 1 dice rolled on attack SKILL_ATTACK = 1; // attack skill MELEE_WEAPON_LENGTH = 2.0; // a six-foot longsword is the default UNARMED_WEAPON_LENGTH = 1.0; // thrown punch, use 1.5 for kicks RANGED_DAMAGE_DICE = 1; // thrown rock MELEE_DAMAGE_DICE = 4; // long sword UNARMED_DAMAGE_DICE = 1; // fists GRACE = 1; // defense stat FIXME implement defense SKILL_DEFENSE = 1; // defense skill FIXME implement defense ARMOR = 0; // armor worn FIXME implement armor check RESILIENCE = 1; // 1-20 how many hits/damage this goon can take TREASURE = []; // list of treasure items dropped when killed or searched while incapacitated FLAG_ATTACK_ON_SIT = TRUE; // should critter bite anyone who sits on it FLAG_FACEENEMY = TRUE; // if true, use llLookAt to face enemy before attacking FLAG_TARGETPOLICY = TARGET_RANDOM; // ranged combat FLAG_TARGETINCAP = FALSE; // FIXME target and shoot visibly incapacitated targets? FLAG_DEBUG = FALSE; // show debug messages ANTIDELAY = TRUE; // use antidelay nodes for rapid fire? ISACTIVE = FALSE; // turret active? FLAG_PLAYANIM = FALSE; // set TRUE for osNPC FLAG_DEAD = FALSE; FLAG_RESPAWN = TRUE; FLAG_DIE = FALSE; ARMOR = 0; // does critter/goon have armor? Range: 0 for none, 1-5 for valid armor SENSOR_ARC = PI/6; // constrain the field of fire. // Movement Setup RESTRICT_Z = TRUE; KEEP_IN_PARCEL = TRUE; vector size = llGetScale(); ZOFFSET = size.z / 2.0; llSetStatus(STATUS_PHANTOM|STATUS_PHYSICS|STATUS_ROTATE_X|STATUS_ROTATE_Y|STATUS_DIE_AT_EDGE,FALSE); llSetStatus(STATUS_ROTATE_Z|STATUS_BLOCK_GRAB|STATUS_RETURN_AT_EDGE,TRUE); REZ_POINT = llGetPos(); REZ_POINT.z = llGround(ZERO_VECTOR) + ZOFFSET; REZ_PARCEL = llList2String(llGetParcelDetails(REZ_POINT,[PARCEL_DETAILS_AREA]),0); MODE = MODE_WANDER; // default to wander mode for a critter MOVE(REZ_POINT); // inventory configuration // inventory sounds // inventory prizes // llSetPrimitiveParams([PRIM_PHANTOM, FALSE]); // ensure all prims are not phantom to register collisions CHANOBJECT = (integer)("0x"+llGetSubString((string)llGetKey(),0,6)); // calculate dynamic channel to listen on if ( HANDOBJECT != 0 ) llListenRemove(HANDOBJECT); // remove an existing listener channel HANDOBJECT = llListen(CHANOBJECT,"",NULL_KEY,""); // start listener for attack events if ( HANDLE_PUB != 0 ) llListenRemove(HANDLE_PUB); // remove an existing listener channel HANDLE_PUB = llListen(PUBLIC_CHANNEL,"",NULL_KEY,""); // start a listener on main chat } //============================================================================ //============================================================================ //STANCE_KNEEL() { //} //============================================================================ // PRONE STANCE //============================================================================ //STANCE_PRONE() { //} //============================================================================ //============================================================================ //STANCE_STAND() { // Stand up code //} //============================================================================ // DEFAULT STATE //============================================================================ default { //------------------------------------------------------------------------ // CHANGED - reset script when region starts or restarts //------------------------------------------------------------------------ changed(integer change) { if ( change & CHANGED_REGION_START ) { RESET(); } if ( change & CHANGED_LINK ) { key id = llAvatarOnSitTarget(); if (id) { if ( id != llGetOwner() && FLAG_ATTACK_ON_SIT ) { llShout(PUBLIC_CHANNEL,"The "+llGetObjectName()+" attacks "+llKey2Name(id)+" for trying to sit on it!"); integer dynchan = (integer)("0x"+llGetSubString((string)id,0,6)); // calculate attackers HUD dynamic channel llRegionSay(dynchan,"CLOSECOMBAT"+DIV+(string)UNARMED_DAMAGE_DICE+DIV+(string)id+DIV+(string)llGetKey()+DIV+llGetObjectName()); llUnSit(id); } } } } //------------------------------------------------------------------------ // LINK_MESSAGE - incoming messages from other Modules in NPC //------------------------------------------------------------------------ link_message(integer sender_num,integer num,string msg,key id) { if ( num == MODULE_GOON ) return; // ignore our own link messages DEBUG("link_message: sender_num=["+(string)sender_num+"] num=["+(string)num+"] msg=["+msg+"] id=["+(string)id+"]"); COMMAND(msg); } //------------------------------------------------------------------------ // LISTEN - receive owner commands and INCOMING attack messages //------------------------------------------------------------------------ listen(integer channel,string name,key id, string message) { DEBUG("listen: channel=["+(string)channel+"] name=["+name+"] id=["+(string)id+"] message=["+message+"]"); if ( channel == PUBLIC_CHANNEL ) { if ( id == llGetOwner() ) COMMAND(message); return; } if ( channel == CHANOBJECT ) { // is this message on the dynamic channel? list fields = llParseString2List(message,[DIV],[]); // break line of text into = delimited fields string command = llToLower(llStringTrim(llList2String(fields,0),STRING_TRIM)); // field zero is the "command" // INCOMING BULLET HIT MESSAGE FROM OUTGOING ATTACK // If NPC Critter/Goon Bullet has hit, fire a hitcheck regionwide at targetplayer's 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 llRegionSay(victimchan,"RANGEDHIT"+DIV+(string)POWER+DIV+(string)SKILL_ATTACK+DIV+(string)attdice+DIV+(string)bywho+DIV+bywhat); // attack! DEBUG((string)victimchan+" RANGEDHIT"+DIV+(string)POWER+DIV+(string)SKILL_ATTACK+DIV+(string)attdice+DIV+(string)bywho+DIV+bywhat); return; } // end if RANGEDCOMBAT/TOHIT // INCOMING ATTACK COMMAND FROM SOMEONE ELSE - FIXME - Armor, Damage, Incapacitated, Dead if ( command == "hitcheck" || command == "rangedhit" || command == "closehit" ) { // is this an attack command? integer attackstat = llList2Integer(fields,1); // get the value of the attacker's stat integer attackskill = llList2Integer(fields,2); // get the attackers skill level integer attackdice = llList2Integer(fields,3); // get the attackers weapon attack dice key owner = llList2Key(fields,4); // get the owner of the attacking object string item = llList2String(fields,5); // get the name of the attacking object if ( attackstat < MINSTAT || attackstat > MAXSTAT ) { // is attack stat valid? ERROR("Attack stat value out of range: "+(string)MINSTAT+"-"+(string)MAXSTAT); // report the invalid value return; // exit early since we've hit a fatal error with message } if ( attackskill < MINSKILL || attackstat > MAXSKILL ) { // is attacker skill value valid? ERROR("Attack skill value out of range: "+(string)MINSKILL+"-"+(string)MAXSKILL); // report invalid value return; // exit early since we've hit a fatal error with message } if ( attackdice < MINDAMAGE || attackdice > MAXDAMAGE ) { // is attack dice of object valid? ERROR("Attack dice value out of range: "+(string)MINDAMAGE+"-"+(string)MAXDAMAGE); // report invalid value return; // exit early since we've hit a fatal error with message } // its all good - report the hit RPEVENT(llGetObjectName()+" attacked with "+llKey2Name(owner)+"'s "+item+" for "+(string)attackdice+" attack dice!"); DAMAGE_CHECK(attackstat,attackskill,attackdice,owner,item); return; // exit early in case we add more commands later } // end if INCOMING attack command } // end if channel object } //------------------------------------------------------------------------ // OBJECT_REZ - when NPC fires, tell bullet the UUID of shooter //------------------------------------------------------------------------ object_rez(key child) { // tell child turret bullet our key for combat resolution integer childchan = (integer)("0x"+llGetSubString((string)child,0,6)); // get turret bullet's dynamic channel llRegionSay(childchan,(string)llGetKey()); // tell turret bullet who shot them } //------------------------------------------------------------------------ // ON_REZ EVENT //------------------------------------------------------------------------ on_rez(integer start_param) { start_param = 0; // LSLint RESET(); // nothing drastic, just reset script and start through state_entry } //------------------------------------------------------------------------ // NO_SENSOR - when it fires and nothing is found, clear the sensor save lists //------------------------------------------------------------------------ no_sensor() { DETECTED_KEY = []; // empty list of found keys DETECTED_POS = []; // empty list of found key positions DETECTED_ROT = []; // empty list of found key rotations } //------------------------------------------------------------------------ // SENSOR - what does NPC see? save the list of keys, positions, and rotations //------------------------------------------------------------------------ sensor(integer num_detected) { integer loop = 0; DETECTED_KEY = []; // empty list of found keys DETECTED_POS = []; // empty list of found key positions DETECTED_ROT = []; // empty list of found key rotations for ( loop = 0; loop < num_detected; loop++ ) { DETECTED_KEY = DETECTED_KEY + llDetectedKey(loop); DETECTED_POS = DETECTED_POS + llDetectedPos(loop); DETECTED_ROT = DETECTED_ROT + llDetectedRot(loop); } } //------------------------------------------------------------------------ // STATE_ENTRY - run setup and wait to be activated //------------------------------------------------------------------------ state_entry() { SETUP(); } //------------------------------------------------------------------------ // TIMER - pumps the heart over and over when touch activated //------------------------------------------------------------------------ timer() { HEARTBEAT(); } //------------------------------------------------------------------------ // TOUCH_START - activate or de-activate the NPC sensor and heartbeat function //------------------------------------------------------------------------ touch_start(integer num_detected) { num_detected = 0; // LSLINT if ( llDetectedKey(0) != llGetOwner() ) return; // ignore touches from anyone except owner if ( ISACTIVE == FALSE ) { ISACTIVE = TRUE; CHANOBJECT = (integer)("0x"+llGetSubString(llGetKey(),0,6)); if ( HANDOBJECT != 0 ) llListenRemove(HANDOBJECT); // remove an existing listener channel HANDOBJECT = llListen(CHANOBJECT,"",NULL_KEY,""); llSensorRepeat("",NULL_KEY,AGENT,SENSOR_RANGE,SENSOR_ARC,SENSOR_RATE); llSetTimerEvent(HEARTRATE); llOwnerSay("NPC Critter/Goon active"); } else { ISACTIVE = FALSE; if ( HANDOBJECT != 0 ) llListenRemove(HANDOBJECT); // remove an existing listener channel if ( HANDLE_PUB != 0 ) llListenRemove(HANDLE_PUB); // remove an existing listener channel llSensorRemove(); llSetTimerEvent(0.0); llOwnerSay("NPC Critter/Goon no longer active"); } } }