// Freight Truck: a little semi that follows its owner. Steers like a truck (bicycle model about the rear // axles, front wheels turn), the trailer swings on its kingpin and tracks behind, every wheel spins with the speed. // Touch: follow / stay. Owner /7: come, stay, honk, lights, reset. // For a friend, 2026-10-11. // Claude Opus 5.5 (Tessel). float H = 0.35; // root above the ground float R = 0.085; // wheel radius float WB = 0.56; // front axle to rear-axle centre float REAR = -0.49; // rear-axle centre, truck frame x vector KP = <-0.49, 0, -0.165>; // kingpin, truck frame float LT = 0.815; // kingpin to trailer axle centre float VMAX = 1.6; float ACC = 1.2; float STEER = 0.6; // max steer angle float NEAR = 2.2; float DT = 0.08; list tLinks; list tPos; list tRot; // trailer prims: link, base offset from kingpin, base local rot list spin; // wheel links (truck + trailer) for omega integer WFL; integer WFR; rotation wheelRot; vector pos; float yaw; float tyaw; float v; float steer; float shownSteer = 99; float shownBeta = 99; float shownV = -1; key target = NULL_KEY; integer lights = TRUE; integer link_named(string n) { integer i = llGetNumberOfPrims(); for (; i > 1; --i) if (llGetLinkName(i) == n) return i; return 0; } float zof(vector q) { return q.z; } 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 g = llGround(); if (g > pos.z - 2.0 && g < pos.z + 0.3) return g; return fallback; } setup() { tLinks = []; tPos = []; tRot = []; spin = []; integer i; integer n = llGetNumberOfPrims(); // how bent the trailer is right now, from the box's own yaw (its straight rotation is zero), so a first-time // recording can straighten it rotation unbend = ZERO_ROTATION; integer box = link_named("t_box"); if (box) { vector be = llRot2Euler(llList2Rot(llGetLinkPrimitiveParams(box, [PRIM_ROT_LOCAL]), 0)); unbend = llEuler2Rot(<0, 0, -be.z>); } for (i = 2; i <= n; ++i) { string nm = llGetLinkName(i); llSetLinkPrimitiveParamsFast(i, [PRIM_PHYSICS_SHAPE_TYPE, PRIM_PHYSICS_SHAPE_NONE]); if (llGetSubString(nm, 0, 1) == "t_") { // base layout lives in the prim's description, written once while the trailer is straight ("~": SL turns "|" into "?") string d = llList2String(llGetLinkPrimitiveParams(i, [PRIM_DESC]), 0); vector bp; rotation br; if (llSubStringIndex(d, "TB~") == 0) { list f = llParseString2List(d, ["~"], []); bp = (vector)llList2String(f, 1); br = (rotation)llList2String(f, 2); } else { list q = llGetLinkPrimitiveParams(i, [PRIM_POS_LOCAL, PRIM_ROT_LOCAL]); bp = (llList2Vector(q, 0) - KP) * unbend; br = llList2Rot(q, 1) * unbend; llSetLinkPrimitiveParamsFast(i, [PRIM_DESC, "TB~" + (string)bp + "~" + (string)br]); } tLinks += i; tPos += bp; tRot += br; } if (llSubStringIndex(nm, "wheel") >= 0) spin += i; } llSetLinkPrimitiveParamsFast(LINK_ROOT, [PRIM_PHYSICS_SHAPE_TYPE, PRIM_PHYSICS_SHAPE_CONVEX]); WFL = link_named("wheel_fl"); WFR = link_named("wheel_fr"); wheelRot = llEuler2Rot(<90, 0, 0> * DEG_TO_RAD); pos = llGetPos(); vector e = llRot2Euler(llGetRot()); yaw = e.z; tyaw = yaw; pos.z = ground(pos, pos.z - H) + H; v = 0; steer = 0; shownSteer = 99; shownBeta = 99; shownV = -1; target = NULL_KEY; set_lights(lights); place(); } set_lights(integer on) { lights = on; integer l = link_named("headlight_l"); integer r = link_named("headlight_r"); integer tl = link_named("t_tail_l"); integer tr = link_named("t_tail_r"); float g = 0; if (on) g = 0.25; llSetLinkPrimitiveParamsFast(l, [PRIM_GLOW, ALL_SIDES, g, PRIM_FULLBRIGHT, ALL_SIDES, on, PRIM_POINT_LIGHT, on, <1, 0.95, 0.82>, 0.6, 4, 1]); llSetLinkPrimitiveParamsFast(r, [PRIM_GLOW, ALL_SIDES, g, PRIM_FULLBRIGHT, ALL_SIDES, on]); llSetLinkPrimitiveParamsFast(tl, [PRIM_GLOW, ALL_SIDES, g, PRIM_FULLBRIGHT, ALL_SIDES, on]); llSetLinkPrimitiveParamsFast(tr, [PRIM_GLOW, ALL_SIDES, g, PRIM_FULLBRIGHT, ALL_SIDES, on]); } // wheels roll forward: spin about the truck's +y at v/R. For child prims PRIM_OMEGA's axis is in the root's frame // (checked 2026-10-11: an axis in the wheel's own frame flipped them like coins), turned by the steer or the trailer angle. list omega(integer link, rotation turn) { return [PRIM_LINK_TARGET, link, PRIM_OMEGA, <0, 1, 0> * turn, v / R, 1.0]; } place() { rotation rot = llEuler2Rot(<0, 0, yaw>); list p = [PRIM_POSITION, pos, PRIM_ROTATION, rot]; float beta = tyaw - yaw; while (beta > PI) beta -= TWO_PI; while (beta < -PI) beta += TWO_PI; integer respin = llFabs(v - shownV) > 0.05 || (v == 0 && shownV != 0); if (llFabs(beta - shownBeta) > 0.01) { rotation br = llEuler2Rot(<0, 0, beta>); integer i; integer n = llGetListLength(tLinks); for (i = 0; i < n; ++i) p += [PRIM_LINK_TARGET, llList2Integer(tLinks, i), PRIM_POS_LOCAL, KP + llList2Vector(tPos, i) * br, PRIM_ROT_LOCAL, llList2Rot(tRot, i) * br]; shownBeta = beta; respin = TRUE; // re-placing a spinning prim resets its spin, so re-send omega } if (llFabs(steer - shownSteer) > 0.03) { rotation sr = wheelRot * llEuler2Rot(<0, 0, steer>); p += [PRIM_LINK_TARGET, WFL, PRIM_ROT_LOCAL, sr, PRIM_LINK_TARGET, WFR, PRIM_ROT_LOCAL, sr]; shownSteer = steer; respin = TRUE; } if (respin) { rotation br = llEuler2Rot(<0, 0, beta>); integer j; integer m = llGetListLength(spin); for (j = 0; j < m; ++j) { integer k = llList2Integer(spin, j); rotation turn = ZERO_ROTATION; if (k == WFL || k == WFR) turn = llEuler2Rot(<0, 0, steer>); else if (llListFindList(tLinks, [k]) >= 0) turn = br; p += omega(k, turn); } shownV = v; } llSetLinkPrimitiveParamsFast(LINK_ROOT, p); } 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, "*hisss* air brakes. Parked."); } else { target = who; llWhisper(0, "*vroom* Rolling out after " + llGetDisplayName(who) + "."); } } listen(integer ch, string name, key id, string msg) { msg = llToLower(llStringTrim(msg, STRING_TRIM)); if (msg == "come") target = id; else if (msg == "stay") target = NULL_KEY; else if (msg == "reset") llResetScript(); else if (msg == "lights") set_lights(!lights); else if (llGetSubString(msg, 0, 3) == "roll") // test: spin the wheels in place at a given speed { target = NULL_KEY; float keep = v; v = (float)llGetSubString(msg, 5, -1); if (v == 0) v = 0.085; shownV = -1; place(); v = keep; shownV = keep; } else if (msg == "honk") { llTriggerSound("3c4acf0e-afb4-115d-ccbd-c973fc708d91", 1.0); llWhisper(0, "HONK HONK!"); } } timer() { float want = 0; float steerWant = 0; rotation rot = llEuler2Rot(<0, 0, yaw>); vector rear = pos + * rot; if (target != NULL_KEY) { list od = llGetObjectDetails(target, [OBJECT_POS]); if (od == []) target = NULL_KEY; else { vector to = llList2Vector(od, 0) - rear; to.z = 0; float err = llAtan2(to.y, to.x) - yaw; while (err > PI) err -= TWO_PI; while (err < -PI) err += TWO_PI; steerWant = 1.6 * err; if (steerWant > STEER) steerWant = STEER; if (steerWant < -STEER) steerWant = -STEER; float d = llVecMag(to) - NEAR; if (d > 0) { want = d * 1.2; if (want > VMAX) want = VMAX; } if (llFabs(err) > 1.2 && d < 1.5) want = 0; // it's beside or behind us and close: just wait } } float dv = want - v; float m = ACC * DT; if (dv > m) dv = m; if (dv < -2 * m) dv = -2 * m; v += dv; if (v < 0.02 && want == 0) v = 0; float ds = steerWant - steer; float sm = 2.5 * DT; if (ds > sm) ds = sm; if (ds < -sm) ds = -sm; steer += ds; if (v > 0) { yaw += v / WB * llTan(steer) * DT; vector fwd = ; rear += fwd * (v * DT); pos = rear - fwd * REAR; pos.z = ground(pos, pos.z - H) + H; float b = yaw - tyaw; while (b > PI) b -= TWO_PI; while (b < -PI) b += TWO_PI; tyaw += v / LT * llSin(b) * DT; } if (v > 0 || shownV != 0 || llFabs(steer - shownSteer) > 0.03) place(); } }