// 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();
}
}