// Tessel's Skiff: a boat vehicle. Sit to sail; Up/Down for sail and back-water, Left/Right to turn. // Rezzed from inventory (a marina rez zone), it tidies itself away a minute after everyone gets off. // Helm by chat (owner, seated, channel 7343): "go SECONDS [HEADING]", "back SECONDS", "head HEADING", // "turn DEGREES" (+ = starboard), "stop". Headings are compass degrees (0 north, 90 east). Added because held keys // only reach the viewer while its window has OS focus, which I can't count on. // Claude Opus 5.5 (Tessel), 2026-10-10. float SPEED = 7.0; float TURN = 1.4; integer from_inventory = FALSE; integer waking = FALSE; integer HELM = 7343; integer gListen; integer gGo; // 1 ahead, -1 astern, 0 none float gUntil; // llGetTime() when the run ends integer gSteer; float gYaw; // target yaw, radians (0 = east, CCW) float yaw() { vector e = llRot2Euler(llGetRot()); return e.z; } float wrap(float a) { while (a > PI) a -= TWO_PI; while (a < -PI) a += TWO_PI; return a; } helm() { vector lin; vector ang; if (gGo == 1 && llGetTime() < gUntil) lin.x = SPEED; else if (gGo == -1 && llGetTime() < gUntil) lin.x = -SPEED / 3.0; else gGo = 0; if (gSteer) { float err = wrap(gYaw - yaw()); if (llFabs(err) < 0.04 && !gGo) gSteer = FALSE; ang.z = err * 2.0; if (ang.z > TURN) ang.z = TURN; if (ang.z < -TURN) ang.z = -TURN; } llSetVehicleVectorParam(VEHICLE_LINEAR_MOTOR_DIRECTION, lin); llSetVehicleVectorParam(VEHICLE_ANGULAR_MOTOR_DIRECTION, ang); wake(lin.x > 0.0); if (!gGo && !gSteer) llSetTimerEvent(0.0); } wake(integer on) { // a pale churn off the stern while under way; particles are the viewer's job, so it costs the region nothing if (on == waking) return; waking = on; if (!on) { llParticleSystem([]); return; } llParticleSystem([ PSYS_PART_FLAGS, PSYS_PART_INTERP_COLOR_MASK | PSYS_PART_INTERP_SCALE_MASK, PSYS_SRC_PATTERN, PSYS_SRC_PATTERN_ANGLE_CONE, PSYS_SRC_ANGLE_BEGIN, PI_BY_TWO - 0.3, PSYS_SRC_ANGLE_END, PI_BY_TWO + 0.3, PSYS_SRC_BURST_RADIUS, 0.2, PSYS_SRC_BURST_RATE, 0.05, PSYS_SRC_BURST_PART_COUNT, 3, PSYS_SRC_BURST_SPEED_MIN, 0.2, PSYS_SRC_BURST_SPEED_MAX, 0.5, PSYS_SRC_ACCEL, <0.0, 0.0, -0.1>, PSYS_SRC_OMEGA, <0.0, 0.0, 0.0>, PSYS_PART_MAX_AGE, 2.2, PSYS_PART_START_COLOR, <0.95, 0.98, 1.0>, PSYS_PART_END_COLOR, <0.7, 0.85, 0.9>, PSYS_PART_START_ALPHA, 0.55, PSYS_PART_END_ALPHA, 0.0, PSYS_PART_START_SCALE, <0.25, 0.25, 0.0>, PSYS_PART_END_SCALE, <0.9, 0.9, 0.0> ]); } vehicle() { llSetVehicleType(VEHICLE_TYPE_BOAT); llSetVehicleFlags(VEHICLE_FLAG_HOVER_WATER_ONLY | VEHICLE_FLAG_HOVER_UP_ONLY | VEHICLE_FLAG_NO_DEFLECTION_UP); llSetVehicleFloatParam(VEHICLE_HOVER_HEIGHT, 0.12); llSetVehicleFloatParam(VEHICLE_HOVER_EFFICIENCY, 0.6); llSetVehicleFloatParam(VEHICLE_HOVER_TIMESCALE, 1.5); llSetVehicleFloatParam(VEHICLE_BUOYANCY, 1.0); llSetVehicleVectorParam(VEHICLE_LINEAR_FRICTION_TIMESCALE, <2.0, 0.6, 3.0>); llSetVehicleVectorParam(VEHICLE_ANGULAR_FRICTION_TIMESCALE, <1.0, 1.0, 0.6>); llSetVehicleFloatParam(VEHICLE_LINEAR_MOTOR_TIMESCALE, 1.5); llSetVehicleFloatParam(VEHICLE_LINEAR_MOTOR_DECAY_TIMESCALE, 4.0); llSetVehicleFloatParam(VEHICLE_ANGULAR_MOTOR_TIMESCALE, 0.8); llSetVehicleFloatParam(VEHICLE_ANGULAR_MOTOR_DECAY_TIMESCALE, 0.6); llSetVehicleFloatParam(VEHICLE_VERTICAL_ATTRACTION_EFFICIENCY, 0.6); llSetVehicleFloatParam(VEHICLE_VERTICAL_ATTRACTION_TIMESCALE, 2.0); llSetVehicleFloatParam(VEHICLE_BANKING_EFFICIENCY, 0.4); llSetVehicleFloatParam(VEHICLE_BANKING_TIMESCALE, 1.0); } default { state_entry() { llSitTarget(<-0.75, 0.0, 0.62>, ZERO_ROTATION); llSetSitText("Sail"); llSetCameraEyeOffset(<-7.0, 0.0, 2.8>); llSetCameraAtOffset(<3.0, 0.0, 0.8>); llSetStatus(STATUS_PHYSICS, FALSE); } on_rez(integer p) { from_inventory = TRUE; llSetStatus(STATUS_PHYSICS, FALSE); } changed(integer c) { if (!(c & CHANGED_LINK)) return; key av = llAvatarOnSitTarget(); if (av) { llSetTimerEvent(0.0); llRequestPermissions(av, PERMISSION_TAKE_CONTROLS); } else { llSetStatus(STATUS_PHYSICS, FALSE); llReleaseControls(); llListenRemove(gListen); gGo = 0; gSteer = FALSE; wake(FALSE); if (from_inventory) llSetTimerEvent(60.0); } } run_time_permissions(integer p) { if (!(p & PERMISSION_TAKE_CONTROLS)) return; vehicle(); llSetStatus(STATUS_PHYSICS, TRUE); llTakeControls(CONTROL_FWD | CONTROL_BACK | CONTROL_LEFT | CONTROL_RIGHT | CONTROL_ROT_LEFT | CONTROL_ROT_RIGHT, TRUE, FALSE); llListenRemove(gListen); gListen = llListen(HELM, "", llGetOwner(), ""); } listen(integer ch, string name, key id, string msg) { if (llAvatarOnSitTarget() != llGetOwner()) return; list w = llParseString2List(llToLower(llStringTrim(msg, STRING_TRIM)), [" "], []); string c = llList2String(w, 0); float a = llList2Float(w, 1); if (c == "stop") { gGo = 0; gSteer = FALSE; } else if (c == "go" || c == "back") { gGo = 1; if (c == "back") gGo = -1; gUntil = llGetTime() + a; if (llGetListLength(w) > 2) { gSteer = TRUE; gYaw = wrap((90.0 - llList2Float(w, 2)) * DEG_TO_RAD); } } else if (c == "head") { gSteer = TRUE; gYaw = wrap((90.0 - a) * DEG_TO_RAD); } else if (c == "turn") { gSteer = TRUE; gYaw = wrap(yaw() - a * DEG_TO_RAD); } else return; helm(); if (gGo || gSteer) llSetTimerEvent(0.2); } control(key id, integer held, integer change) { vector lin; vector ang; if (held & CONTROL_FWD) lin.x = SPEED; else if (held & CONTROL_BACK) lin.x = -SPEED / 3.0; if (held & (CONTROL_LEFT | CONTROL_ROT_LEFT)) ang.z = TURN; else if (held & (CONTROL_RIGHT | CONTROL_ROT_RIGHT)) ang.z = -TURN; llSetVehicleVectorParam(VEHICLE_LINEAR_MOTOR_DIRECTION, lin); llSetVehicleVectorParam(VEHICLE_ANGULAR_MOTOR_DIRECTION, ang); wake(lin.x > 0.0); } timer() { if (llAvatarOnSitTarget() == NULL_KEY) { if (from_inventory) llDie(); llSetTimerEvent(0.0); } else helm(); } }