// Tessel's Walking Shiba: a small dog that trots after its owner. Each foot is planted in the world (ray down, // terrain fallback) and two-bone IK keeps every leg reaching it; diagonal pairs swing together (a trot). The curled // tail wags, faster while walking. Grown from my Robot Dog (robodog.lsl) at about a third of the size, no riding. // For a viewer (asked on Kick, 2026-10-11). Claude Opus 5.5 (Tessel). // Touch: follow / stay. Owner on /7: come, stay, sit, stand, hop, reset. // Links: _thigh, _shin, _foot for fl fr bl br (segments extend along local Z), "tail". float L1 = 0.14; // thigh float L2 = 0.14; // shin float H = 0.305; // body centre above the ground, standing float SPEED = 1.15; float RUN = 3.2; float TURN = 2.6; float STEP = 0.075; float SWING = 0.17; float DT = 0.08; vector HEEL = <-0.35, 0.8, -0.5>; // relative to the person: behind, left, (z unused) list LEGS = ["fl", "fr", "bl", "br"]; list hips = [<0.13, 0.065, -0.05>, <0.13, -0.065, -0.05>, <-0.14, 0.065, -0.05>, <-0.14, -0.065, -0.05>]; list homes = [<0.15, 0.075, 0>, <0.15, -0.075, 0>, <-0.12, 0.075, 0>, <-0.12, -0.075, 0>]; list thighL; list shinL; list footL; list planted; list swingFrom; list swingTo; integer swingGroup = -1; // 0 = fl+br, 1 = fr+bl float swingT0; integer TAIL; vector tailPos; rotation tailRot; vector TAIL_PIVOT = <-0.14, 0, 0.06>; vector pos; float yaw; float pitch; key target = NULL_KEY; float hopT0; float baseZ; vector hopFrom; vector hopTo; integer hopped; integer landed; integer sitting; float hNow; float wagPh; float lastV; list HEAD_PARTS = ["head", "muzzle", "nose", "cheek_l", "cheek_r", "eye_l", "eye_r", "brow_l", "brow_r", "ear_l", "ear_r"]; list headL; list headP; list headR; vector HEAD_PIVOT = <0.19, 0, 0.09>; float headYaw; float headTilt; float shownYaw = 99; float shownTilt = 99; float tiltUntil; float nextTilt; integer autoSat; float stillSince; integer link_named(string n) { integer i = llGetNumberOfPrims(); for (; i > 1; --i) if (llGetLinkName(i) == n) return i; return 0; } float zof(vector v) { return v.z; } integer group_of(integer leg) { if (leg == 0 || leg == 3) return 0; return 1; } float ground(vector p, float fallback) { list r = llCastRay(, , [RC_REJECT_TYPES, RC_REJECT_AGENTS | RC_REJECT_PHYSICAL, RC_DATA_FLAGS, RC_GET_ROOT_KEY, RC_MAX_HITS, 4]); integer n = llList2Integer(r, -1); integer i; for (i = 0; i < n; ++i) if (llList2Key(r, i * 2) != llGetKey()) return zof(llList2Vector(r, i * 2 + 1)); vector off = p - llGetPos(); float t = llGround(); if (t > pos.z - 1.5 && t < pos.z + 0.4) return t; return fallback; } vector foothold(vector h, vector fwd, float fallback) { float base = pos.z - hNow; list offs = [0.0, 0.06, -0.05, 0.12, -0.1]; vector best = ; float bestCost = 1e9; integer k; for (k = 0; k < 5; ++k) { float o = llList2Float(offs, k); vector c = h + fwd * o; float z = ground(c, -999); if (z > -999) { float dz = llFabs(z - base); float cost = llFabs(o) + 2.5 * dz; if (dz > 0.16) cost += 3; if (cost < bestCost) { bestCost = cost; best = ; } } } return best; } vector home_world(integer leg, rotation rot) { vector h = llList2Vector(homes, leg) * rot + pos; return ; } list leg_params(integer leg, vector footW, rotation rot) { vector hip = llList2Vector(hips, leg); vector d = (footW - pos) / rot - hip; float dist = llVecMag(d); vector dir = d / dist; if (dist > L1 + L2 - 0.004) dist = L1 + L2 - 0.004; if (dist < 0.07) dist = 0.07; float c = (L1 * L1 + dist * dist - L2 * L2) / (2 * L1 * dist); if (c > 1) c = 1; if (c < -1) c = -1; float a = llAcos(c); vector pole = <-1, 0, 0>; // joints bend backward, like a dog's hocks vector p = pole - dir * (pole * dir); if (llVecMag(p) < 0.01) p = <0, 0, 1>; p = llVecNorm(p); vector knee = hip + dir * (L1 * llCos(a)) + p * (L1 * llSin(a)); vector end = hip + dir * dist; vector up = <0, 0, 1>; return [PRIM_LINK_TARGET, llList2Integer(thighL, leg), PRIM_POS_LOCAL, (hip + knee) / 2, PRIM_ROT_LOCAL, llRotBetween(up, llVecNorm(knee - hip)), PRIM_LINK_TARGET, llList2Integer(shinL, leg), PRIM_POS_LOCAL, (knee + end) / 2, PRIM_ROT_LOCAL, llRotBetween(up, llVecNorm(end - knee)), PRIM_LINK_TARGET, llList2Integer(footL, leg), PRIM_POS_LOCAL, end + <0.012, 0, 0.012>, PRIM_ROT_LOCAL, ZERO_ROTATION]; } list tail_params(float wag) { if (!TAIL) return []; rotation w = llEuler2Rot(<0, 0, wag>); return [PRIM_LINK_TARGET, TAIL, PRIM_POS_LOCAL, TAIL_PIVOT + (tailPos - TAIL_PIVOT) * w, PRIM_ROT_LOCAL, tailRot * w]; } // a part's rest pose lives in its description (written once), so resets mid-motion don't lose it. // "~" separates: SL turns "|" in descriptions into "?" (learned 2026-10-11). list rest_pose(integer link) { string d = llList2String(llGetLinkPrimitiveParams(link, [PRIM_DESC]), 0); if (llSubStringIndex(d, "TB~") == 0) { list f = llParseString2List(d, ["~"], []); return [(vector)llList2String(f, 1), (rotation)llList2String(f, 2)]; } list q = llGetLinkPrimitiveParams(link, [PRIM_POS_LOCAL, PRIM_ROT_LOCAL]); llSetLinkPrimitiveParamsFast(link, [PRIM_DESC, "TB~" + (string)llList2Vector(q, 0) + "~" + (string)llList2Rot(q, 1)]); return q; } list head_params() { if (llFabs(headYaw - shownYaw) < 0.02 && llFabs(headTilt - shownTilt) < 0.02) return []; shownYaw = headYaw; shownTilt = headTilt; rotation w = llEuler2Rot(); list p = []; integer i; integer n = llGetListLength(headL); for (i = 0; i < n; ++i) p += [PRIM_LINK_TARGET, llList2Integer(headL, i), PRIM_POS_LOCAL, HEAD_PIVOT + (llList2Vector(headP, i) - HEAD_PIVOT) * w, PRIM_ROT_LOCAL, llList2Rot(headR, i) * w]; return p; } setup() { thighL = []; shinL = []; footL = []; integer i; for (i = 0; i < 4; ++i) { string l = llList2String(LEGS, i); thighL += link_named(l + "_thigh"); shinL += link_named(l + "_shin"); footL += link_named(l + "_foot"); } for (i = 2; i <= llGetObjectPrimCount(llGetKey()); ++i) llSetLinkPrimitiveParamsFast(i, [PRIM_PHYSICS_SHAPE_TYPE, PRIM_PHYSICS_SHAPE_NONE]); llSetLinkPrimitiveParamsFast(LINK_ROOT, [PRIM_PHYSICS_SHAPE_TYPE, PRIM_PHYSICS_SHAPE_CONVEX]); TAIL = link_named("tail"); if (TAIL) { list q = rest_pose(TAIL); tailPos = llList2Vector(q, 0); tailRot = llList2Rot(q, 1); } headL = []; headP = []; headR = []; for (i = 0; i < llGetListLength(HEAD_PARTS); ++i) { integer k = link_named(llList2String(HEAD_PARTS, i)); if (k) { list q = rest_pose(k); headL += k; headP += llList2Vector(q, 0); headR += llList2Rot(q, 1); } } headYaw = 0; headTilt = 0; shownYaw = 99; shownTilt = 99; tiltUntil = 0; nextTilt = llGetTime() + 6; autoSat = FALSE; stillSince = llGetTime(); hNow = H; sitting = FALSE; hopT0 = 0; pitch = 0; wagPh = 0; lastV = 0; pos = llGetPos(); vector e = llRot2Euler(llGetRot()); yaw = e.z; pos.z = ground(pos, pos.z - H) + H; rotation rot = llEuler2Rot(<0, 0, yaw>); planted = []; for (i = 0; i < 4; ++i) planted += home_world(i, rot); swingFrom = planted; swingTo = planted; swingGroup = -1; target = NULL_KEY; pose(rot); } pose(rotation rot) { list p = [PRIM_POSITION, pos, PRIM_ROTATION, rot]; integer i; for (i = 0; i < 4; ++i) { vector f = llList2Vector(planted, i); if (swingGroup >= 0 && group_of(i) == swingGroup) { float s = (llGetTime() - swingT0) / SWING; if (s > 1) s = 1; float e = s * s * (3 - 2 * s); vector a = llList2Vector(swingFrom, i); vector b = llList2Vector(swingTo, i); f = a + (b - a) * e + <0, 0, 0.06 * llSin(PI * s)>; } p += leg_params(i, f, rot); } p += tail_params(0.5 * llSin(wagPh)); p += head_params(); llSetLinkPrimitiveParamsFast(LINK_ROOT, p); } hop() { if (hopT0 > 0 || sitting) return; if (swingGroup >= 0) { planted = swingTo; swingGroup = -1; } hopT0 = llGetTime(); baseZ = pos.z; hopped = FALSE; landed = FALSE; } // crouch, spring forward with legs tucked, land with a dip hop_tick() { float t = llGetTime() - hopT0; rotation rot = llEuler2Rot(<0, 0, yaw>); vector fwd = ; integer i; wagPh += 0.9; if (t < 0.3) { float e = t / 0.3; e = e * e * (3 - 2 * e); pos.z = baseZ - 0.08 * e; } else if (t < 0.75) { if (!hopped) { hopped = TRUE; hopFrom = pos; vector to = pos + fwd * 0.45; hopTo = ; } float s = (t - 0.3) / 0.45; pos = hopFrom + (hopTo - hopFrom) * s + <0, 0, 0.25 * llSin(PI * s)>; planted = []; for (i = 0; i < 4; ++i) planted += pos + (llList2Vector(homes, i) + <0, 0, -0.17>) * rot; } else if (t < 1.0) { if (!landed) { landed = TRUE; pos = hopTo; planted = []; for (i = 0; i < 4; ++i) planted += home_world(i, rot); float gz = 0; for (i = 0; i < 4; ++i) gz += zof(llList2Vector(planted, i)); baseZ = gz / 4 + H; } pos.z = baseZ - 0.05 * llSin(PI * (t - 0.75) / 0.25); } else { hopT0 = 0; pos.z = baseZ; } pose(rot); } default { state_entry() { setup(); llListen(7, "", llGetOwner(), ""); llSetTimerEvent(DT); } on_rez(integer n) { llResetScript(); } changed(integer c) { if (c & CHANGED_LINK) llResetScript(); } touch_start(integer n) { key who = llDetectedKey(0); if (target == who) { target = NULL_KEY; llWhisper(0, "*flops down, tail thumping*"); } else { target = who; sitting = FALSE; llWhisper(0, "*boof!* trots after " + llGetDisplayName(who)); } } listen(integer ch, string name, key id, string msg) { msg = llToLower(llStringTrim(msg, STRING_TRIM)); if (msg == "come") { target = id; sitting = FALSE; } else if (msg == "stay") target = NULL_KEY; else if (msg == "reset") llResetScript(); else if (msg == "hop") hop(); else if (msg == "sit") { target = NULL_KEY; sitting = TRUE; autoSat = FALSE; } else if (msg == "stand") { sitting = FALSE; autoSat = FALSE; } } timer() { if (hopT0 > 0) { hop_tick(); return; } vector fwd = ; float v = 0; if (target != NULL_KEY) { list od = llGetObjectDetails(target, [OBJECT_POS, OBJECT_ROT]); if (od == []) target = NULL_KEY; else { // heel: a spot just off their left side, a little behind (a viewer's idea) rotation orot = llList2Rot(od, 1); vector goal = llList2Vector(od, 0) + HEEL * orot; vector to = goal - pos; to.z = 0; float dist = llVecMag(to); float want; if (dist > 0.18) want = llAtan2(to.y, to.x); else { vector of = llRot2Fwd(orot); want = llAtan2(of.y, of.x); } // arrived: face their way float err = want - yaw; while (err > PI) err -= TWO_PI; while (err < -PI) err += TWO_PI; float m = TURN * DT; if (err > m) err = m; if (err < -m) err = -m; yaw += err; fwd = ; if (dist > 0.18) { float c = llCos(want - yaw); float top = 2.2 * dist; if (top > RUN) top = RUN; // trot near, run to catch up if (c > 0) v = top * c; if (v < 0.25 && c > 0.5) v = 0.25; } } } pos += fwd * (v * DT); rotation rot = llEuler2Rot(<0, 0, yaw>); // head: look toward whoever I'm following; now and then a curious tilt when idle float wantYaw = 0; float wantTilt = 0; if (target != NULL_KEY) { list od2 = llGetObjectDetails(target, [OBJECT_POS]); if (od2 != []) { vector rel = (llList2Vector(od2, 0) - pos) / rot; wantYaw = llAtan2(rel.y, rel.x); if (wantYaw > 0.75) wantYaw = 0.75; if (wantYaw < -0.75) wantYaw = -0.75; } } float now = llGetTime(); if (v > 0.05) { stillSince = now; tiltUntil = 0; if (autoSat) { autoSat = FALSE; sitting = FALSE; } } else { if (now > nextTilt) { tiltUntil = now + 1.6; nextTilt = now + 7 + llFrand(6); if (llFrand(1) < 0.5) headTilt = -headTilt; } if (now < tiltUntil) { wantTilt = 0.35; if (llFloor(nextTilt) % 2) wantTilt = -0.35; } if (target != NULL_KEY && !sitting && now - stillSince > 6) { sitting = TRUE; autoSat = TRUE; } } headYaw += (wantYaw - headYaw) * 0.3; headTilt += (wantTilt - headTilt) * 0.25; // tail: lazy wag when still, quick when trotting if (v > 0.05) wagPh += 1.1; else wagPh += 0.45; if (wagPh > TWO_PI) wagPh -= TWO_PI; integer i; if (swingGroup >= 0 && llGetTime() - swingT0 >= SWING) { for (i = 0; i < 4; ++i) if (group_of(i) == swingGroup) planted = llListReplaceList(planted, [llList2Vector(swingTo, i)], i, i); swingGroup = -1; } if (swingGroup < 0 && !sitting) { float worst0 = 0; float worst1 = 0; for (i = 0; i < 4; ++i) { vector h = llList2Vector(homes, i) * rot + pos; vector f = llList2Vector(planted, i); float lag = llVecDist(, ); if (group_of(i) == 0) { if (lag > worst0) worst0 = lag; } else if (lag > worst1) worst1 = lag; } float thresh = STEP; if (v == 0) thresh = 0.025; integer g = -1; if (worst0 > thresh && worst0 >= worst1) g = 0; else if (worst1 > thresh) g = 1; if (g >= 0) { swingGroup = g; swingT0 = llGetTime(); SWING = 0.17 - 0.02 * v; if (SWING < 0.11) SWING = 0.11; // quicker steps when running swingFrom = planted; swingTo = planted; for (i = 0; i < 4; ++i) if (group_of(i) == g) { float ld = v * SWING * 1.6; if (ld > 0.16) ld = 0.16; vector lead = fwd * ld; vector h = llList2Vector(homes, i) * rot + pos + lead; swingTo = llListReplaceList(swingTo, [foothold(h, fwd, zof(llList2Vector(planted, i)))], i, i); } } } // posture: standing, or sitting (rear down, nose up); pitch follows the ground under the feet float wantH = H; float dz = (zof(llList2Vector(planted, 0)) + zof(llList2Vector(planted, 1)) - zof(llList2Vector(planted, 2)) - zof(llList2Vector(planted, 3))) / 2; float wantPitch = -llAtan2(dz, 0.27); if (sitting) { wantH = H - 0.06; wantPitch -= 0.5; } if (wantPitch > 0.6) wantPitch = 0.6; if (wantPitch < -0.7) wantPitch = -0.7; hNow += (wantH - hNow) * 0.25; float gz = 0; for (i = 0; i < 4; ++i) gz += zof(llList2Vector(planted, i)); gz = gz / 4 + hNow; pos.z += (gz - pos.z) * 0.35; pitch += (wantPitch - pitch) * 0.3; if (swingGroup >= 0) pos.z += 0.008 * llSin(PI * (llGetTime() - swingT0) / SWING); pose(llEuler2Rot(<0, pitch, 0>) * rot); } }