Table of Contents

Change Log

22 April 2013

  • NPCs should now restart on a region restart without needing a script restart by the user.

19 April 2013

  • Merged with key-value data syntax, object description format changed. Please synchronize the changes and reset your spawner description.

31 March 2013

  • Added sitting and standing up to the scripting language.
  • Rewrote the code to use the object description storage.
  • The @wait command is now @pause.
  • Scripts now reset after a restart.

30 March 2013

  • Added rotations to the scripting language.
  • Added link-messages (currently, speaking text).
  • Added endless wandering by omitting the times parameter.

Shortnote

The NPC puppeteer can be used to script (in the sense of playwriting) an NPC to move around and perform actions. The LSL script translates an easy-to-grasp semantics from a notecard and then makes the NPC act on them. This can be used, for example, to set-up “real” NPCs for games or simulations where you need a true NPC to move around and act out a “script”.

Video

Soundtrack: Oldtimer - Amiga Game Soundtrack.

Setup

The NPC is controlled by a script in the inventory of a primitive. The puppeteer NPC script requires the following items to be present in the same inventory as the script:

This is how a puppeteer primitive should look like:

as you can observe, all the required components are placed inside a box. One may, of course, make the primitive transparent or turn it into an object that will blend-in with the rest of the environment.

Appearance

The appearance generator script can be used to generate an appearance notecard for the puppeteer. That script copies the appearance of the avatar touching the primitive and writes it to a notecard called appearance which is then used by this script. The example shown in the video uses an agent called Alter Ego, however if that agent would be wearing an animal costume, the puppeteer could be used to create very functional animals, as opposed to prim-only alternatives using the OpenSim wanderer script.

Scriptwriting

The script is supplied as a notecard named Script and dumped in the inventory of the controlling primitive. It uses the following syntax:

Command First Parameter Second Parameter Description
@spawn name (string) location (vector) Rezzes an NPC with name at a location.
@walk destination (vector) NaN Makes the NPC walk to destination.
@say phrase (string) NaN Makes the NPC speak a phrase.
@pause animation cycles (integer) NaN Makes the NPC wait for a multiple of standing animation cycles.
@wander radius (integer) cycles (integer) Makes the NPC wander in radius, for cycles cycles.
@stop NaN NaN Halts the NPC script indefinitely.
@delete NaN NaN Removes the NPC.
@animate animation (string) time (integer) Makes the NPC trigger the animation animation for time seconds.
@goto label (string) NaN Jump to the label label in the script.
@rotate degrees NaN Rotate the NPC degrees around the Z axis.
@sit primitive name NaN Sit on a primitive with a given name.
@stand NaN NaN If sitting on a primitive, stand up.

where every command has to be prefixed with the @ symbol. Any lines that do not start with @ will be ignored by the script and can be used either as comments or as jump labels for the @goto command.

Command Examples

The following are examples for every command:

Command Example Effect
@spawn @spawn=Alter Ego|<137.262848, 132.939011, 22.705553> Will rez an NPC called Alter Ego at the coordinates <137.262848, 132.939011, 22.705553>.
@walk @walk=<138.980637, 137.669067, 22.77791> Will make the NPC walk to the coordinates <138.980637, 137.669067, 22.77791>
@say @say=omg, walking is so tiresome... Will make he NPC say on local chat omg, walking is so tiresome...
@pause @pause=2 Will pause and stand for 2 animation cycles.
@stop @stop Will halt all script execution leaving the NPC standing.
@delete @delete Will delete the current rezzed NPC.
@animate @animate=Dance,10 Will trigger the animation in the primitive's inventory named Dance for 10 seconds.
@goto @goto=LABEL Will jump to the label LABEL and consume the jump.
@rotate @rotate=90 Will rotate the NPC 90 degrees around the positive sense of the z axis.
@sit @sit=Chair Will scan for an object named Chair and will make the NPC sit on it.
@stand @stand Will make the NPC stand up if it is currently sitting on a primitive.

Case Example

Consider the following example Script notecard:

@spawn=Alter Ego|<137.262848, 132.939011, 22.705553>
@walk=<138.980637, 137.669067, 22.77791>
@walk=<134.451523, 134.671173, 22.77791>
@say=omg, walking is so tiresome...
@walk=<134.451523, 130.5784, 22.77791>
@pause=2
@walk=<142.783859, 130.871552, 22.77791>
@wander=3|1
@walk=<137.5, 130.871552, 22.77791>
@say=Uff, I'm done...
@animate=Dance|10
@stop

This will make the NPC, in order:

  1. rez at the coordinates <137.262848, 132.939011, 22.705553> with the first name Alter and last name Ego.
  2. walk to the point <138.980637, 137.669067, 22.77791>.
  3. walk to the point <134.451523, 134.671173, 22.77791>.
  4. say on local chat “omg, walking is so tiresome...”
  5. walk to the point <134.451523, 130.5784, 22.77791>.
  6. wait at its current location for 2 seconds.
  7. walk to the point <142.783859, 130.871552, 22.77791>.
  8. wander around its current location in a 3 meter radius circle and perform a walk-wait cycle once.
  9. walk to the point <137.5, 130.871552, 22.77791>.
  10. say on local chat “Uff, I'm done...”
  11. play the animation Dance for 10 seconds.
  12. stop and wait forever.

A walk-wait cycle means a transition from walking to waiting and back. The default behavior of the puppeteer is to repeat all the commands. To avoid that use @stop or @delete at the end of the script.

Continuations

The @goto command can be used to jump inside the script to a named label. For example:

REPEAT
@walk=<138.980637, 137.669067, 22.77791>
@walk=<134.451523, 134.671173, 22.77791>
@say=omg, walking is so tiresome...
@goto=REPEAT

Exceptionally, the @goto and the @spawn command are used linearly and removed from the queue once a jump has been performed. In other words, if we wanted to repeat the three previous commands, three times, one would write:

REPEAT
@walk=<138.980637, 137.669067, 22.77791>
@walk=<134.451523, 134.671173, 22.77791>
@say=omg, walking is so tiresome...
@goto=REPEAT
@goto=REPEAT
@goto=REPEAT

Consequently, it is not possible to write a loop that will never terminate. @goto is only used to repeat certain parts of the script. However, if an endless repetition is desired, then the script can be written without the @stop command which will make the script repeat itself indefinitely.

The @goto command supports both forward and backward jumps. For example, in the snippet below:

REPEAT
@walk=<138.980637, 137.669067, 22.77791>
@walk=<134.451523, 134.671173, 22.77791>
@say=omg, walking is so tiresome...
@goto=REPEAT
@goto=NEXT
@say=i will never say this...
NEXT
@say=now i can sit around doing nothing
@stop

the command @say=i will never say this... will never get executed since after the @goto=REPEAT command, the @goto=NEXT command will jump over @say=i will never say this....

Design

The script is based on automata, allowing for extension to be written that perform other actions (sit, fly, etc...). A rough automata, omitting some states, is illustrated below:


\begin{tikzpicture}[->,>=stealth',shorten >=1pt,auto,node distance=2.8cm,
                    semithick]
  \tikzstyle{every state}=[fill=red,draw=none,text=white]
    
  \node[initial,state] (A)                    {$default$};
  \node[state] (B) [below right of=A] {$script$};
  \node[state] (C) [above of=B] {$spawn$};
  \node[state] (D) [above right of=B] {$say$};
  \node[state] (E) [right of=B] {$walk$};
  \node[state] (F) [below of=B] {$wander$};
  \node[state] (G) [below left of=B] {$rotate$};
  \node[state] (H) [left of=B] {$\cdots$};
  
  \path (A) edge	node {} (B)
        (B) edge        node {} (C)
        (C) edge        node {} (B)
        (B) edge        node {} (E)
        (E) edge        node {} (B)
        (B) edge        node {} (D)
        (D) edge        node {} (B)
        (B) edge        node {} (F)
        (F) edge        node {} (B)
        (B) edge        node {} (G)
        (G) edge        node {} (B)
        (B) edge[dotted]        node {} (H)
        (H) edge[dotted]        node {} (B);

\end{tikzpicture}

Every state defines an action for the NPC to perform. After the action is preformed, the script returns to the script state which is responsible for interpreting the commands in the notecard. The script state shuffles the commands by using a queue-based approach, where the first enqueued commands get dequeued off from the top of the queue and then enqueued at the end. This ensures that we can repeat the commands instead of deleting them.

Also, since the script uses a queue to store the commands, that queue can be easily wound and unwound in order to create continuations, letting the playwright repeat certain commands without having to write them again.

Code

This script was tested and works on OpenSim version 0.7.4!

npc_puppeteer.lsl
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2013 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
 
//////////////////////////////////////////////////////////
//                  CONFIGURATION                       //
//////////////////////////////////////////////////////////
 
// How much time, in seconds, does a standing animation 
// cycle take? 
float ANIMATION_CYCLE_TIME = 10;
 
///////////////////////////////////////////////////////////////////////////
//                              INTERNALS                                //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
vector wasCirclePoint(float radius) {
    float x = llPow(-1, 1 + (integer) llFrand(2)) * llFrand(radius*2);
    float y = llPow(-1, 1 + (integer) llFrand(2)) * llFrand(radius*2);
    if(llPow(x,2) + llPow(y,2) <= llPow(radius,2))
        return <x, y, 0>;
    return wasCirclePoint(radius);
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueGet(string var, string kvp) {
    list dVars = llParseString2List(kvp, ["&"], []);
    do {
        list data = llParseString2List(llList2String(dVars, 0), ["="], []);
        string k = llList2String(data, 0);
        if(k != var) jump continue;
        return llList2String(data, 1);
@continue;
        dVars = llDeleteSubList(dVars, 0, 0);
    } while(llGetListLength(dVars));
    return "";
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueSet(string var, string val, string kvp) {
    list dVars = llParseString2List(kvp, ["&"], []);
    if(llGetListLength(dVars) == 0) return var + "=" + val;
    list result = [];
    do {
        list data = llParseString2List(llList2String(dVars, 0), ["="], []);
        string k = llList2String(data, 0);
        if(k == "") jump continue;
        if(k == var && val == "") jump continue;
        if(k == var) {
            result += k + "=" + val;
            val = "";
            jump continue;
        }
        string v = llList2String(data, 1);
        if(v == "") jump continue;
        result += k + "=" + v;
@continue;
        dVars = llDeleteSubList(dVars, 0, 0);
    } while(llGetListLength(dVars));
    if(val != "") result += var + "=" + val;
    return llDumpList2String(result, "&");
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueDelete(string var, string kvp) {
    list dVars = llParseString2List(kvp, ["&"], []);
    list result = [];
    list added = [];
    do {
        list data = llParseString2List(llList2String(dVars, 0), ["="], []);
        string k = llList2String(data, 0);
        if(k == var) jump continue;
        string v = llList2String(data, 1);
        if(v == "") jump continue;
        if(llListFindList(added, (list)k) != -1) jump continue;
        result += k + "=" + v;
        added += k;
@continue;
        dVars = llDeleteSubList(dVars, 0 ,0);
    } while(llGetListLength(dVars));
    return llDumpList2String(result, "&");
}
 
// Vector that will be filled by the script with
// the initial starting position in region coordinates.
vector iPos = ZERO_VECTOR;
// Storage for destination position.
vector dPos = ZERO_VECTOR;
// Storage for the NPC script.
list npcScript = [];
// Storage for the next action.
string npcAction = "";
string npcParams = "";
// Storage for talking on local chat.
string npcSay = "";
 
default {
    state_entry() {
        osNpcRemove(wasKeyValueGet("key", llGetObjectDesc()));
        llSetTimerEvent(5);
    }
    timer() {
        npcScript = llParseString2List(osGetNotecard("Script"), ["\n"], []);
        if(llGetListLength(npcScript) == 0) {
            llSay(DEBUG_CHANNEL, "No script notecard found.");
            llSetTimerEvent(0);
            return;
        }
        llSetTimerEvent(0);
        state script;
    }
    changed(integer change) {
        if(change & CHANGED_REGION_START) llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
state script
{
    state_entry() {
@ignore;
        string next = llList2String(npcScript, 0);
        npcScript = llDeleteSubList(npcScript, 0, 0);
        npcScript += next;
        if(llGetSubString(next, 0, 0) != "@") jump ignore;
        list data  = llParseString2List(next, ["="], []);
        npcAction = llToLower(llStringTrim(llList2String(data, 0), STRING_TRIM));
        npcParams = llStringTrim(llList2String(data, 1), STRING_TRIM);
        llSetTimerEvent(1);
    }
    timer() {
        llSetTimerEvent(0);
@commands;
        if(npcAction == "@spawn") {
            integer lastIdx = llGetListLength(npcScript)-1;
            npcScript = llDeleteSubList(npcScript, lastIdx, lastIdx);
            list spawnData = llParseString2List(npcParams, ["|"], []);
            llSetObjectDesc(wasKeyValueSet("name", llList2String(spawnData, 0), llGetObjectDesc()));
            list spawnDest = llParseString2List(llList2String(spawnData, 1), ["<", ",", ">"], []);
            iPos.x = llList2Float(spawnDest, 0);
            iPos.y = llList2Float(spawnDest, 1);
            iPos.z = llList2Float(spawnDest, 2);
            state spawn;
        }
        if(npcAction == "@goto") {
            integer lastIdx = llGetListLength(npcScript)-1;
            npcScript = llDeleteSubList(npcScript, lastIdx, lastIdx);
            // Wind commands till goto label.
@wind;
            string next = llList2String(npcScript, 0);
            npcScript = llDeleteSubList(npcScript, 0, 0);
            npcScript += next;
            if(next != npcParams) jump wind;
            // Wind the label too.
            next = llList2String(npcScript, 0);
            npcScript = llDeleteSubList(npcScript, 0, 0);
            npcScript += next;
            // Get next command.
            list data  = llParseString2List(next, ["="], []);
            npcAction = llToLower(llStringTrim(llList2String(data, 0), STRING_TRIM));
            npcParams = llStringTrim(llList2String(data, 1), STRING_TRIM);
            // Reschedule.
            jump commands;
        }
        if(npcAction == "@walk") {
            list dest = llParseString2List(npcParams, ["<", ",", ">"], []);
            dPos.x = llList2Float(dest, 0);
            dPos.y = llList2Float(dest, 1);
            dPos.z = llList2Float(dest, 2);
            state walk;
        }
        if(npcAction == "@say") {
            npcSay = npcParams;
            state say;
        }
        if(npcAction == "@pause") {
            llSetObjectDesc(wasKeyValueSet("pause", npcParams, llGetObjectDesc()));
            state pause;
        }
        if(npcAction == "@wander") {
            list wanderData = llParseString2List(npcParams, ["|"], []);
            llSetObjectDesc(wasKeyValueSet("wd", llList2String(wanderData, 0), llGetObjectDesc()));
            llSetObjectDesc(wasKeyValueSet("wc", llList2String(wanderData, 1), llGetObjectDesc()));
            iPos = llGetPos();
            state wander;
        }
        if(npcAction == "@rotate") {
            llSetObjectDesc(wasKeyValueSet("rot", npcParams, llGetObjectDesc()));
            state rotate;
        }
        if(npcAction == "@sit") {
            llSetObjectDesc(wasKeyValueSet("sit", npcParams, llGetObjectDesc()));
            state sit;
        }
        if(npcAction == "@stand") {
            state stand;
        }
        if(npcAction == "@stop") {
            state stop;
        }
        if(npcAction == "@delete") {
            state delete;
        }
        if(npcAction == "@animate") {
            list animateData = llParseString2List(npcParams, ["|"], []);
            llSetObjectDesc(wasKeyValueSet("an", llList2String(animateData, 0), llGetObjectDesc()));
            llSetObjectDesc(wasKeyValueSet("at", llList2String(animateData, 1), llGetObjectDesc()));
            state animate;
        }
        llSay(DEBUG_CHANNEL, "ERROR: Unrecognized script line: " + npcAction + "=" + npcParams);
    }
    changed(integer change) {
        if(change & CHANGED_REGION_START) llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
state rotate {
    state_entry() {
        osNpcSetRot(wasKeyValueGet("key", llGetObjectDesc()), llEuler2Rot(<0,0,(float)wasKeyValueGet("rot", llGetObjectDesc())> * DEG_TO_RAD));
        llSetTimerEvent(2);
    }
    link_message(integer sender, integer num, string str, key id) {
        if(id != "@npc_say") return;
        osNpcSay(wasKeyValueGet("key", llGetObjectDesc()), str);
    }
    timer() {
        llSetObjectDesc(wasKeyValueDelete("rot", llGetObjectDesc()));
        state script;
    }
    changed(integer change) {
        if(change & CHANGED_REGION_START) llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
state sit {
    state_entry() {
        llSensorRepeat(wasKeyValueGet("sit", llGetObjectDesc()), "", PASSIVE|ACTIVE, TWO_PI, 96, 1);
    }
    sensor(integer num) {
        llSensorRemove();
        osNpcSit(wasKeyValueGet("key", llGetObjectDesc()), llDetectedKey(0), OS_NPC_SIT_NOW);
        llSetTimerEvent(2);
    }
    timer() {
        llSetObjectDesc(wasKeyValueDelete("sit", llGetObjectDesc()));
        state script;
    }
    changed(integer change) {
        if(change & CHANGED_REGION_START) llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
state stand {
    state_entry() {
        osNpcStand(wasKeyValueGet("key", llGetObjectDesc()));
        state script;
    }
    changed(integer change) {
        if(change & CHANGED_REGION_START) llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
state animate {
    state_entry() {
        if(llGetInventoryType(wasKeyValueGet("an", llGetObjectDesc())) == INVENTORY_ANIMATION) osNpcPlayAnimation(wasKeyValueGet("key", llGetObjectDesc()), wasKeyValueGet("an", llGetObjectDesc()));
        llSetTimerEvent((integer)wasKeyValueGet("at", llGetObjectDesc()));
    }
    link_message(integer sender, integer num, string str, key id) {
        if(id != "@npc_say") return;
        osNpcSay(wasKeyValueGet("key", llGetObjectDesc()), str);
    }
    timer() {
        if(llGetInventoryType(wasKeyValueGet("an", llGetObjectDesc())) == INVENTORY_ANIMATION) osNpcStopAnimation(wasKeyValueGet("key", llGetObjectDesc()), wasKeyValueGet("an", llGetObjectDesc()));
        llSetObjectDesc(wasKeyValueDelete("an", llGetObjectDesc()));
        llSetObjectDesc(wasKeyValueDelete("at", llGetObjectDesc()));
        state script;
    }
    changed(integer change) {
        if(change & CHANGED_REGION_START) llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
state spawn
{
    state_entry() {
        list name = llParseString2List(wasKeyValueGet("name", llGetObjectDesc()), [" "], []);
        llSetObjectDesc(wasKeyValueSet("key", osNpcCreate(llList2String(name, 0), llList2String(name, 1), iPos, "appearance"), llGetObjectDesc()));
        osNpcLoadAppearance(wasKeyValueGet("key", llGetObjectDesc()), "appearance");
        if(llGetInventoryType("Stand") == INVENTORY_ANIMATION) osNpcPlayAnimation(wasKeyValueGet("key", llGetObjectDesc()), "Stand");
        llSetTimerEvent(10);
    }
    timer() {
        llSetTimerEvent(0);
        if(llGetInventoryType("Stand") == INVENTORY_ANIMATION) osNpcStopAnimation(wasKeyValueGet("key", llGetObjectDesc()), "Stand");
        llSetObjectDesc(wasKeyValueDelete("name", llGetObjectDesc()));
        state script;
    }
    changed(integer change) {
        if(change & CHANGED_REGION_START) llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
 
}
 
state delete {
    state_entry() {
        osNpcRemove(llGetObjectDesc());
        llResetScript();
    }
    changed(integer change) {
        if(change & CHANGED_REGION_START) llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
state stop {
    state_entry() {
        if(llGetInventoryType("Stand") == INVENTORY_ANIMATION) osNpcPlayAnimation(wasKeyValueGet("key", llGetObjectDesc()), "Stand");
    }
    link_message(integer sender, integer num, string str, key id) {
        if(id != "@npc_say") return;
        osNpcSay(wasKeyValueGet("key", llGetObjectDesc()), str);
    }
    changed(integer change) {
        if(change & CHANGED_REGION_START) llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
state walk {
    state_entry() {
        if(llGetInventoryType("Walk") == INVENTORY_ANIMATION) osNpcPlayAnimation(wasKeyValueGet("key", llGetObjectDesc()), "Walk");
        osNpcMoveToTarget(wasKeyValueGet("key", llGetObjectDesc()), dPos, OS_NPC_NO_FLY);
        llSetTimerEvent(1);
    }
    link_message(integer sender, integer num, string str, key id) {
        if(id != "@npc_say") return;
        osNpcSay(wasKeyValueGet("key", llGetObjectDesc()), str);
    }
    timer() {
        if (llVecDist(osNpcGetPos(wasKeyValueGet("key", llGetObjectDesc())), dPos) > 2) return;
        llSetTimerEvent(0);
        if(llGetInventoryType("Walk") == INVENTORY_ANIMATION) osNpcStopAnimation(wasKeyValueGet("key", llGetObjectDesc()), "Walk");
        state script;
    }
    changed(integer change) {
        if(change & CHANGED_REGION_START) llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
state say {
    state_entry() {
        osNpcSay(wasKeyValueGet("key", llGetObjectDesc()), npcSay);
        npcSay = "";
        state script;
    }
    changed(integer change) {
        if(change & CHANGED_REGION_START) llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
state wander
{
    state_entry() {
        dPos = iPos + wasCirclePoint((integer)wasKeyValueGet("wd", llGetObjectDesc()));
        if(llGetInventoryType("Walk") == INVENTORY_ANIMATION) osNpcPlayAnimation(wasKeyValueGet("key", llGetObjectDesc()), "Walk");
        osNpcMoveToTarget(wasKeyValueGet("key", llGetObjectDesc()), dPos, OS_NPC_NO_FLY);
        llSetTimerEvent(0.1);
    }
    link_message(integer sender, integer num, string str, key id) {
        if(id != "@npc_say") return;
        osNpcSay(wasKeyValueGet("key", llGetObjectDesc()), str);
    }
    timer() {
        if (llVecDist(osNpcGetPos(wasKeyValueGet("key", llGetObjectDesc())), dPos) > 2) return;
        llSetTimerEvent(0);
        if(llGetInventoryType("Walk") == INVENTORY_ANIMATION) osNpcStopAnimation(wasKeyValueGet("key", llGetObjectDesc()), "Walk");
        if(wasKeyValueGet("wc", llGetObjectDesc()) == "0") {
            llSetObjectDesc(wasKeyValueDelete("wc", llGetObjectDesc()));
            llSetObjectDesc(wasKeyValueDelete("wd", llGetObjectDesc()));
            state script;
        }
        llSetObjectDesc(wasKeyValueSet("wc", (string)((integer)wasKeyValueGet("wc", llGetObjectDesc())-1), llGetObjectDesc()));
        state wait;
    }
    changed(integer change) {
        if(change & CHANGED_REGION_START) llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
state wait {
    state_entry() {
        if(llGetInventoryType("Stand") == INVENTORY_ANIMATION) osNpcPlayAnimation(wasKeyValueGet("key", llGetObjectDesc()), "Stand");
        llSetTimerEvent(ANIMATION_CYCLE_TIME);
    }
    link_message(integer sender, integer num, string str, key id) {
        if(id != "@npc_say") return;
        osNpcSay(wasKeyValueGet("key", llGetObjectDesc()), str);
    }
    timer() {
        llSetTimerEvent(0);
        if(llGetInventoryType("Stand") == INVENTORY_ANIMATION) osNpcStopAnimation(wasKeyValueGet("key", llGetObjectDesc()), "Stand");
        state wander;
    }
    changed(integer change) {
        if(change & CHANGED_REGION_START) llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
state pause {
    state_entry() {
        if(llGetInventoryType("Stand") == INVENTORY_ANIMATION) osNpcPlayAnimation(wasKeyValueGet("key", llGetObjectDesc()), "Stand");
        llSetTimerEvent(ANIMATION_CYCLE_TIME * 1+llFrand((integer)wasKeyValueGet("pause", llGetObjectDesc())-1));
    }
    link_message(integer sender, integer num, string str, key id) {
        if(id != "@npc_say") return;
        osNpcSay(wasKeyValueGet("key", llGetObjectDesc()), str);
    }
    timer() {
        llSetTimerEvent(0);
        if(llGetInventoryType("Stand") == INVENTORY_ANIMATION) osNpcStopAnimation(wasKeyValueGet("key", llGetObjectDesc()), "Stand");
        llSetObjectDesc(wasKeyValueDelete("pause", llGetObjectDesc()));
        state script;
    }
    changed(integer change) {
        if(change & CHANGED_REGION_START) llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
}