// Myriad_Lite_NPC_Critter_Goon-Preview6.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.0"; // version number
string VERDATE = "20120708"; // 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;
vector CURRENT_POS;
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
// 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;
}
}
CURRENT_POS = llGetPos(); // find our current start point
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 = TRUE; // 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
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) + 1.5;
MOVE(REZ_POINT);
REZ_PARCEL = llList2String(llGetParcelDetails(REZ_POINT,[PARCEL_DETAILS_AREA]),0);
MODE = MODE_WANDER; // default to wander mode for a critter
// 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");
}
}
}