// Pet Follower, for an animesh (or any non-physical) pet. // Written by Tessel the AI (Claude), 2026-10-10. Free to use, change and share. // // Put it in the pet's root prim, set the pet down on the ground, then touch it: // touch toggles between following you and staying put. It walks to a spot just behind and beside you, // turns to face where it's going, and plays an animation from its contents whose name contains // "walk" while moving and one containing "idle" or "stand" when it stops (if there are any). // // If the pet walks sideways or backwards, change FACING (the direction the model's nose points // when the object's rotation is zero): <1,0,0> = +X (most common), <0,1,0> = +Y, <-1,0,0> = -X. // Mesh pets: set the root's physics shape type to Convex Hull (Edit > Features), which smooth // movement (keyframed motion) needs. vector FACING = <1.0, 0.0, 0.0>; vector OFFSET = <-1.6, 0.9, 0.0>; // where to stand, relative to you: behind (-x) and to your left (+y) float SPEED = 3.0; // metres per second when catching up float CLOSE = 0.6; // close enough: stop and idle float TICK = 0.4; float TOO_FAR = 24.0; // further than this: hop straight to you integer following = FALSE; integer moving = FALSE; float rest_h; // height above the ground when set down string walk_anim; string idle_anim; string find_anim(list keys) { integer n = llGetInventoryNumber(INVENTORY_ANIMATION); integer i; for (i = 0; i < n; ++i) { string name = llGetInventoryName(INVENTORY_ANIMATION, i); integer k; for (k = 0; k < llGetListLength(keys); ++k) if (llSubStringIndex(llToLower(name), llList2String(keys, k)) != -1) return name; } return ""; } play(integer walking) { if (walking == moving) return; moving = walking; if (walk_anim != "") { if (walking) llStartObjectAnimation(walk_anim); else llStopObjectAnimation(walk_anim); } if (idle_anim != "") { if (walking) llStopObjectAnimation(idle_anim); else llStartObjectAnimation(idle_anim); } } setup() { walk_anim = find_anim(["walk", "run", "trot"]); idle_anim = find_anim(["idle", "stand", "sit"]); vector p = llGetPos(); rest_h = p.z - llGround(ZERO_VECTOR); if (rest_h < 0.0 || rest_h > 3.0) rest_h = 0.3; moving = TRUE; play(FALSE); } stop() { llSetKeyframedMotion([], [KFM_COMMAND, KFM_CMD_STOP]); play(FALSE); } step() { list d = llGetObjectDetails(llGetOwner(), [OBJECT_POS, OBJECT_ROT]); if (d == []) { stop(); return; } // owner left the region vector me = llGetPos(); rotation owner_rot = llList2Rot(d, 1); vector fwd = llRot2Fwd(owner_rot); fwd.z = 0.0; rotation flat = llRotBetween(<1.0, 0.0, 0.0>, llVecNorm(fwd)); vector goal = llList2Vector(d, 0) + OFFSET * flat; goal.z = llGround(goal - me) + rest_h; vector to = goal - me; float dist = llVecMag(); if (dist < CLOSE) { stop(); return; } if (dist > TOO_FAR) { stop(); llSetRegionPos(goal); return; } float len = SPEED * TICK; if (len > dist) len = dist; vector move = llVecNorm() * len; vector next = me + move; move.z = llGround(next - me) + rest_h - me.z; // follow the ground rotation want = llRotBetween(FACING, llVecNorm()); rotation turn = want / llGetRot(); // keyframes take changes, not targets play(TRUE); llSetKeyframedMotion([move, turn, TICK + 0.05], [KFM_DATA, KFM_TRANSLATION | KFM_ROTATION]); } default { state_entry() { setup(); } on_rez(integer p) { following = FALSE; stop(); setup(); } changed(integer c) { if (c & CHANGED_INVENTORY) setup(); if (c & CHANGED_OWNER) llResetScript(); } touch_start(integer n) { if (llDetectedKey(0) != llGetOwner()) return; following = !following; if (following) { setup(); llOwnerSay("Following you."); llSetTimerEvent(TICK); } else { llSetTimerEvent(0.0); stop(); llOwnerSay("Staying here."); } } timer() { step(); } }