NPC Action script

.[npcAction500] series

NPC Action is a component part of the IruMoto NPC Engine for use in virtual worlds running Open Simulator 9 and later.

Purpose and functionality

The purpose of .[npcAction500] is to manage the transition of five variables for the NPC (a.k.a. “bot”), each time a new action sequence is called. These variables are animation, position, rotation, outfit, and facial expression. A sixth variable, “say” is plugged into the code (see sceneChange() function) but currently unused.

A typical action sequence looks like this:

MASettle() {
    newA = ".XlayBk2-flat-hands-belly"; 
    newP = <220.606, 221.960, 26.454>;
    newR = 180.000; 
    newO = ".outfit7"; 
    newE = "Grin";
    sceneChange(); }

The effect of this is every visible aspect of the NPC, i.e. its actions and appearance.

This current series has the name format “.[npcAction600.??]” where ?? is a number then letter, representing the version. The six digit number afterwards (where listed in the version notes) is a date format: YYMMDD.


Variables for each scene change are edited in-script. This is the only script in the IruMoto NPC Engine that requires direct editing. Like the other scripts, global variables such as NPC name, gender etc are contained and edited in the supporting notecards.

The NPC Action script listens to a communications channel for commands from an external script such as IruMoto Love Engine. The script does not listen to the menu channel created by .[npcExist500]. The link_message event is used to communicate with other internal scripts.

If you have an IruMoto Time Generator (TimeGen 370 or later) on your region, a variable exists in .npcConfig to allow .[npcAction500] to listen to that channel for any time-sensitive actions you might wish to use, e.g. automatically send the bot to work each day at 9 am and return home at 5 pm.

Other components

Several other scripts, notecards and contents are necessary for the NPC Engine to function. Visit the main IruMoto NPC Engine page for full information.

Code (.[npcAction500.1o])

You may use this script for whatever purpose but must retain the commented header section.

developed by Xay Tomsen in:
InWorldz 2013-14, Reef VR 2015, DigiWorldz 2016-2021.
Special mention to the InWorldz grid monkeys for their brilliant tech of the day and for encouraging this rookie scripter to play with NPCs.
Script purpose and version notes are online at:
key bot;
vector anglesR; rotation finalRot; rotation rot;

integer botLive = 0; // 0 to save prim moving with timeGen unless bot exists
integer home = 1; // legacy: keep for bots that use multiple sit targets
integer sensorChan;
integer tgChan;

float newR;
vector newP;
string newO;
string newA;
string newS;
string newE;

float prevR = 10.00;
vector prevP = <5.000, 5.000, 21.000>;
string prevO = "NULL"; 
string prevA = "NULL";
string prevS = "NULL";
string prevE = "NULL";

string firstName; string lastName; string botType; string chanString; string sensorString; string triggerWord; string timeGenActive; string timeGenChan;
string outfitsMain; string outfit1; string outfit2; string outfit3; string outfit4; string outfit5; string outfit6; string outfit7; string outfit8; string outfit9; string outfit10; string outfit11;

    firstName = osGetNotecardLine(".npcConfig500.1s", 5);
    lastName = osGetNotecardLine(".npcConfig500.1s", 8);
    botType = osGetNotecardLine(".npcConfig500.1s", 21);
    chanString = osGetNotecardLine(".npcConfig500.1s", 27);
    sensorString = osGetNotecardLine(".npcConfig500.1s", 30);
    triggerWord = osGetNotecardLine(".npcConfig500.1s", 33);
    timeGenActive = osGetNotecardLine(".npcConfig500.1s", 42);
    timeGenChan = osGetNotecardLine(".npcConfig500.1s", 45);
    outfit1 = osGetNotecardLine(".botOutfits", 8);
    outfit2 = osGetNotecardLine(".botOutfits", 11);
    outfit3 = osGetNotecardLine(".botOutfits", 14);
    outfit4 = osGetNotecardLine(".botOutfits", 17);
    outfit5 = osGetNotecardLine(".botOutfits", 20);
    outfit6 = osGetNotecardLine(".botOutfits", 23);
    outfit7 = osGetNotecardLine(".botOutfits", 26);
    outfit8 = osGetNotecardLine(".botOutfits", 29);
    outfit9 = osGetNotecardLine(".botOutfits", 32);
    outfit10 = osGetNotecardLine(".botOutfits", 35);
    outfit11 = osGetNotecardLine(".botOutfits", 38);

    llMessageLinked(LINK_THIS, 0, "ClearEmos", NULL_KEY);
    integer x = 0;
    while(x &lt; llGetInventoryNumber(INVENTORY_ANIMATION)){
        osNpcStopAnimation(bot,llGetInventoryName(INVENTORY_ANIMATION, x++));

sceneChange() { // for menu driven animations
// NOTE: prevA and prevE are superfluous because we need to kill anim each time.
    osNpcPlayAnimation(bot, newA); 
    if (newP == prevP) {}
    if (newP != prevP) { 
        llSetRegionPos(newP); }
    if (newR == prevR) {}
    if (newR != prevR) { 
        llSetRot(ZERO_ROTATION); anglesR = &lt;0,0,newR> * DEG_TO_RAD;
        finalRot = llEuler2Rot(anglesR);
        rot = llGetRot(); llSetRot(rot * finalRot); }
    llMessageLinked(LINK_THIS, 0, newE, NULL_KEY);
    if (newO == prevO) {}
    if (newO != prevO) {
        osNpcLoadAppearance(bot, newO); }
    prevP = newP; prevR = newR; prevO = newO;

sceneChangeTG() { // for time sensitive animations
    if (newA != prevA) { 
        osNpcPlayAnimation(bot, newA); }
    if (newP != prevP) { 
        llSetRegionPos(newP); }
    if (newR != prevR) { 
        llSetRot(ZERO_ROTATION); anglesR = <0,0,newR> * DEG_TO_RAD;
        finalRot = llEuler2Rot(anglesR);
        rot = llGetRot(); llSetRot(rot * finalRot); }
    if (newO != prevO) {
        osNpcLoadAppearance(bot, newO); }
    if (newS != prevS) {
        osNpcSay(bot, newS);}
    prevA = newA; prevP = newP; prevR = newR; prevO = newO; prevS = newS;

// default animation - the mainDefault() function might appear superfluous but it is needed due to the different way that GUEST bots are animated. It is easier to have a universal function for all bots.

mainDefault() {  
    if (botType == "GUEST") { }
    else if (botType == "SENSOR") { startup0(); }
    else if (botType == "TRIGGER") { }
    else if (botType == "ALWAYS") { }

//---- ANIMATION SEQUENCES - IF THIS NPC IS CONTROLLED BY A TIME GENERATOR, replace sceneChange() below with sceneChangeTG().

startup0() {
    newA = "stand-restless"; 
    newP = <221.876, 222.769, 25.467>;
    newR = 318.000; 
    newO = ".outfit1"; 
    newE = "Oops";
    sceneChange(); }

Sunbathe() {
    newA = "sunbathe-by-pool"; 
    newP = <lt;221.876, 222.769, 25.467>;
    newR = 318.000; 
    newO = ".outfit2"; 
    newE = "Grin";
    sceneChange(); }
DrinkTea() {
    newA = "drink-tea"; 
    newP = <lt;221.876, 222.804, 25.903>;
    newR = 318.000; 
    newO = ".outfit2"; 
    newE = "NULL";
    sceneChange(); }

        bot = llGetObjectDesc();
        sensorChan = (integer)(sensorString);
        llListen(sensorChan, "", "", "");
        if (timeGenActive == "YES") {
            tgChan = (integer)(timeGenChan);
            llListen(tgChan, "", NULL_KEY, "");

    listen(integer chan, string name, string ID, string msg)
        chan = sensorChan;
        if (msg == "mainDefault") { 
            mainDefault(); }
        else if (msg == triggerWord + "Restore") { mainDefault(); }
        else if (msg == triggerWord) { mainDefault(); }

// Animation calls - must correspond with Action Sequences

        else if (msg == "Sunbathe") { Sunbathe(); }
        else if (msg == "DrinkTea") { DrinkTea(); }

// -- end Animation calls
    link_message(integer sender_num, integer num, string msg, key id)
        if (msg == "resetAll") { llResetScript(); }
        else if (msg == "botLive0") { botLive = 0; }
        else if (msg == "botLive1") { 
            botLive = 1;
            bot = llGetObjectDesc();

    on_rez(integer start_param){
    changed(integer change)
        if (change &amp; CHANGED_OWNER)
        if (change &amp; CHANGED_REGION_START)

Version Notes


  • v601 – 230824 optionally combines region-wide comms channels.
  • to upgrade from v600, replace state_entry, and remove llListenRemove as noted below. If changing to a single channel, in npcConfig600 card, make sensorChan same as tgChan and reboot.
  • in state_entry() added if/else statement to allow for using the same channel for TimeGen and sensor.
  • combines the two channels with messages filtered by triggerWord variable.
  • overcomes issues with single-channel shower heads, props etc.
  • simplifies and reduces the number of listens on the region.
  • if/else statement allows for a separate channel for legacy sensor function if ever needed for shop models etc, though unlikely.
  • also removed llListenRemove(llListen(tgChan, “”, NULL_KEY, “”)) from listen event which was prompted to kill listen upon hearing triggerWord. Now that they are combined, we don’t want that kill to happen, plus its effectiveness in the past was dubious anyway.
  • v600 – 230326 requires .npcConfig600 notecard.
  • to upgrade from 500.1w, replace header down to and incl sceneChange().
  • no longer directly changes outfits
  • removes the need to load outfits notecard to free up memory.
  • instructs npcExist to change outfits via link_message same as npcEmotions.
  • added generic offline sequence with no emotion, delete online condition.
  • removed botLive check for emotions, instead added to npcEmotions600.
  • v500.1w – 230318 in sceneChange, added ‘if (botLive ==1)’ for newE.
  • removes unnecessary load from emote animations when NPC is offline.


  • v500.1v – 220126 replaced section and added preamble to listen event.
  • allows temporary conversion of TG bots into TRIG,
  • eliminates the need for duplicating the same bot for different purposes.


  • v500.1u – 211229 fixed error reading notecard line for “triggerWord”.
  • v500.1t – 211224 changed loadSettings() to readCards(),
  • removed legacy sceneChangeTG() section,
  • removed legacy ‘integer home’ condition,
  • condensed overly complicated default sequence declaration,
  • created setValues() to establish default variables that used to be up in globals,
  • added detailed explanatory notes in-script re sceneChange() and patrol(),
  • shifted listen() to the very end of script to make it easier to edit events.
  • v500.1s – 211223 added .patrol() function loosely from .[Actor2.22],
  • kept alternative “plug-in” script .[npcPatrol] for possible future projects.

v500.1r – 211215 added extra lines around line 36 to read additional outfits implemented in .[npcExist500.2d]. If upgrading from .[npcAction500.1q], simply copy and paste the new outfit strings in globals, and loadSettings ().

v500.1q – 211127 added line to listens: if (msg == “tgDeath”) { mainDefault(); } to return NPC Engine to default location when bot dies.

v500.1p – 211123 re-jigging scripts to allow new TimeGen4 to act as an NPC controller. Requires new notecard .npcConfig500.1x

v500.1o – 210827 removed llSleep() call previously needed before llGetObjectDesc(), from message_linked event, superseded by changes made in npcExist500.1v – this appears to have eliminated the sporadic offshore failed coms on bot startup/reset.

v500.1n – 210826 removed llSleep() calls from all actions.

v500.1m – 210721 updated to support .npcConfig500.1s

v500.1k – 210621 changed startup variables for float prevR and vector prevP (line 33 and 34) which I think are somehow causing some avatars to shoot offscreen or into sim corner at rez.

v500.1i – 210114 commented out if (timeGenActive == “NO”) statement in state_entry. Unnecessary listen event.

V500.1h – 210109 adds “ALWAYS” option to main default.


v500.1g – 190107 changed sceneChange to eliminate unnecessary script calls.

v500.1f – included clearEmos in endAnim. Added new/prevE strings for emotions, remove old clearEmos from mainDefault().

v500.1e – added CLEAREMOS to mainDefault() function.

v500.1d – reverted and renamed GUEST and VENUE.

v500.1c – added ESCORT botType. redundant ALWAYS sections retained for legacy.


v500.1b – reverted to using function as too top heavy.

v500.1a – above didn’t quite work. went deeper & modified state_entry & listen events instead.

V500.1 – modified if clauses in mainDefault function to exclude ALWAYS bots.

v500 – successfully combined the old 200, 300, and 400 series bots. One code set now does all so I’ve rebadged them all 500 onwards.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.