TIMAS_Bro

pilot.lua

May 17th, 2026 (edited)
125
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 53.23 KB | None | 0 0
  1. local altitude = peripheral.find("altitude_sensor")
  2. local relay = peripheral.find("redstone_relay")
  3. local speedRotation = peripheral.find("Create_RotationSpeedController")
  4. local swivel = peripheral.find("swivel_bearing")
  5. local ship = _G.sublevel or peripheral.find("ship_reader") or peripheral.find("vessel_info")
  6. local PID = require("pid")
  7. if not altitude then
  8.     error("altitude_sensor not found")
  9. end
  10. if not speedRotation then
  11.     error("Create_RotationSpeedController not found")
  12. end
  13. if not swivel then
  14.     error("swivel_bearing not found")
  15. end
  16. local TARGET_HEIGHT = 150
  17. local TARGET_ANGLE = 0
  18. local state = {
  19.     running = true,
  20.     auto = false,
  21.     clutchLocked = true,
  22.     burnerOn = false,
  23.     poseOk = false,
  24.     x = 0,
  25.     z = 0,
  26.     yaw = 0,
  27.     height = 0,
  28.     verticalSpeed = 0,
  29.     horizontalSpeed = 0,
  30.     distance = nil,
  31.     approachDistance = nil,
  32.     eta = nil,
  33.     targetX = nil,
  34.     targetZ = nil,
  35.     targetName = "",
  36.     previewX = nil,
  37.     previewZ = nil,
  38.     previewName = "",
  39.     targetHeight = 150,
  40.     cruiseHeight = 150,
  41.     landing = false,
  42.     landingRadius = 10,
  43.     landingSpeed = 0,
  44.     landingStage = "cruise",
  45.     navStage = "idle",
  46.     motorOutput = 0,
  47.     burnerSignal = 0,
  48.     currentAngle = 0,
  49.     yawAlignCommand = 0,
  50.     yawAlignSide = 0,
  51.     lastNavPoint = nil,
  52.     log = "Ready",
  53.     skipTakeoff = false,
  54.     takeoff = false,
  55.     shutdownDeadline = nil
  56. }
  57. local ui = {
  58.     screen = "search",
  59.     focus = "search",
  60.     search = "",
  61.     hInput = "150",
  62.     selectedSuggestion = 1,
  63.     selectedAction = "start",
  64.     widgets = {},
  65.     width = 0,
  66.     height = 0,
  67.     fullRedraw = true,
  68.     dirty = true,
  69.     lastScreen = nil,
  70.     lastFrameTime = os.clock(),
  71.     dt = 0.05,
  72.     pulse = 0,
  73.     animationUntil = 0,
  74.     anim = {searchX = nil, searchY = nil, searchW = nil, routeProgress = 0, routePulse = 0},
  75.     layout = {route = nil, stats = nil}
  76. }
  77. local LANDING_PREDICTION_TIME = 2.2
  78. local LANDING_BRAKE_DISTANCE = 65
  79. local LANDING_FINAL_DISTANCE = 8
  80. local LANDING_MIN_SPEED = 0.08
  81. local LANDING_MAX_ANGLE = 18
  82. local FINAL_YAW = 0
  83. local APPROACH_OFFSET_Z = 100
  84. local APPROACH_RADIUS = 20
  85. local places = {
  86.     {name = "Spawn", x = 0, z = 0},
  87.     {name = "Base", x = -546, z = -124},
  88.     {name = "Viasma", x = 115, z = 323 }
  89. }
  90. local drawCache = {}
  91. local function clamp(value, minValue, maxValue)
  92.     return math.max(minValue, math.min(maxValue, value))
  93. end
  94. local function normalizeAngle(angle)
  95.     return ((angle + 180) % 360) - 180
  96. end
  97. local function expLerp(current, target, rate, dt)
  98.     if current == nil then
  99.         return target
  100.     end
  101.     local amount = 1 - math.exp(-rate * dt)
  102.     return current + (target - current) * amount
  103. end
  104. local function trim(text)
  105.     return tostring(text or ""):gsub("^%s+", ""):gsub("%s+$", "")
  106. end
  107. local function parseNumber(text)
  108.     local cleaned = trim(text)
  109.     if cleaned == "" then
  110.         return nil
  111.     end
  112.     local sign = 1
  113.     local first = cleaned:sub(1, 1)
  114.     if first == "-" then
  115.         sign = -1
  116.         cleaned = cleaned:sub(2)
  117.     elseif first == "+" then
  118.         cleaned = cleaned:sub(2)
  119.     end
  120.     if cleaned == "" then
  121.         return nil
  122.     end
  123.     local whole, frac = cleaned:match("^(%d+)%.(%d+)$")
  124.     if not whole then
  125.         whole = cleaned:match("^(%d+)$")
  126.     end
  127.     if not whole then
  128.         return nil
  129.     end
  130.     local value = 0
  131.     for i = 1, #whole do
  132.         value = value * 10 + (string.byte(whole, i) - 48)
  133.     end
  134.     if frac then
  135.         local place = 0.1
  136.         for i = 1, #frac do
  137.             value = value + (string.byte(frac, i) - 48) * place
  138.             place = place * 0.1
  139.         end
  140.     end
  141.     return value * sign
  142. end
  143. local function padRight(text, width)
  144.     text = tostring(text or "")
  145.     if width <= 0 then
  146.         return ""
  147.     end
  148.     if #text >= width then
  149.         return text:sub(1, width)
  150.     end
  151.     return text .. string.rep(" ", width - #text)
  152. end
  153. local function padCenter(text, width)
  154.     text = tostring(text or "")
  155.     if width <= 0 then
  156.         return ""
  157.     end
  158.     if #text >= width then
  159.         return text:sub(1, width)
  160.     end
  161.     local left = math.floor((width - #text) / 2)
  162.     local right = width - #text - left
  163.     return string.rep(" ", left) .. text .. string.rep(" ", right)
  164. end
  165. local function shortNumber(value, digits)
  166.     if value == nil then
  167.         return "--"
  168.     end
  169.     return string.format("%." .. tostring(digits or 1) .. "f", value)
  170. end
  171. local function etaText(seconds)
  172.     if not seconds then
  173.         return "--"
  174.     end
  175.     seconds = math.max(0, math.floor(seconds + 0.5))
  176.     local minutes = math.floor(seconds / 60)
  177.     local secs = seconds % 60
  178.     if minutes >= 60 then
  179.         return string.format("%dh %02dm", math.floor(minutes / 60), minutes % 60)
  180.     end
  181.     return string.format("%d:%02d", minutes, secs)
  182. end
  183. local function writeAt(x, y, text, fg, bg)
  184.     x = math.floor(x + 0.5)
  185.     y = math.floor(y + 0.5)
  186.     fg = fg or colors.white
  187.     bg = bg or colors.black
  188.     text = tostring(text or "")
  189.     local changed = false
  190.     for i = 1, #text do
  191.         local key = tostring(x + i - 1) .. ":" .. tostring(y)
  192.         local ch = text:sub(i, i)
  193.         local cell = drawCache[key]
  194.         if not cell or cell.ch ~= ch or cell.fg ~= fg or cell.bg ~= bg then
  195.             changed = true
  196.             break
  197.         end
  198.     end
  199.     if not changed then
  200.         return
  201.     end
  202.     term.setCursorPos(x, y)
  203.     term.setTextColor(fg)
  204.     term.setBackgroundColor(bg)
  205.     term.write(text)
  206.     for i = 1, #text do
  207.         local key = tostring(x + i - 1) .. ":" .. tostring(y)
  208.         drawCache[key] = {ch = text:sub(i, i), fg = fg, bg = bg}
  209.     end
  210. end
  211. local function fill(x, y, w, h, bg)
  212.     if w <= 0 or h <= 0 then
  213.         return
  214.     end
  215.     for row = 0, h - 1 do
  216.         writeAt(x, y + row, string.rep(" ", w), colors.white, bg)
  217.     end
  218. end
  219. local function clearRect(x, y, w, h, bg)
  220.     if w <= 0 or h <= 0 then
  221.         return
  222.     end
  223.     for row = y, y + h - 1 do
  224.         for col = x, x + w - 1 do
  225.             drawCache[tostring(col) .. ":" .. tostring(row)] = nil
  226.         end
  227.     end
  228.     fill(x, y, w, h, bg)
  229. end
  230. local function markDirty()
  231.     if not ui.dirty then
  232.         ui.dirty = true
  233.         os.queueEvent("ui_dirty")
  234.     else
  235.         ui.dirty = true
  236.     end
  237. end
  238. local function startAnimation(duration)
  239.     ui.animationUntil = math.max(ui.animationUntil or 0, os.clock() + (duration or 0.2))
  240.     markDirty()
  241. end
  242. local function isAnimating()
  243.     return os.clock() < (ui.animationUntil or 0)
  244. end
  245. local function registerWidget(widget)
  246.     ui.widgets[#ui.widgets + 1] = widget
  247. end
  248. local function pointIn(mx, my, widget)
  249.     return mx >= widget.x and mx < widget.x + widget.w and my >= widget.y and my < widget.y + widget.h
  250. end
  251. local function clearIfNeeded()
  252.     local w, h = term.getSize()
  253.     if ui.width ~= w or ui.height ~= h then
  254.         ui.width = w
  255.         ui.height = h
  256.         term.setBackgroundColor(colors.black)
  257.         term.setTextColor(colors.white)
  258.         term.clear()
  259.         drawCache = {}
  260.         ui.fullRedraw = true
  261.         ui.dirty = true
  262.     end
  263.     return w, h
  264. end
  265. local function setLog(text)
  266.     state.log = tostring(text or "")
  267.     markDirty()
  268. end
  269. local function setClutch(flag)
  270.     if state.clutchLocked == flag then
  271.         redstone.setOutput("bottom", flag)
  272.         return
  273.     end
  274.     state.clutchLocked = flag
  275.     redstone.setOutput("bottom", flag)
  276.     markDirty()
  277. end
  278. local function switchScreen(screen)
  279.     if ui.screen ~= screen then
  280.         ui.screen = screen
  281.         ui.fullRedraw = true
  282.         startAnimation(0.25)
  283.         ui.layout.route = nil
  284.         ui.layout.stats = nil
  285.     end
  286. end
  287. local function getPose()
  288.     if not ship or not ship.getLogicalPose then
  289.         return nil
  290.     end
  291.     local ok, pose = pcall(ship.getLogicalPose)
  292.     if ok then
  293.         return pose
  294.     end
  295.     return nil
  296. end
  297. local function getVelocityVector()
  298.     if not ship or not ship.getVelocity then
  299.         return nil
  300.     end
  301.     local ok, velocity = pcall(ship.getVelocity)
  302.     if ok and velocity then
  303.         return velocity
  304.     end
  305.     return nil
  306. end
  307. local function velocityComponents(velocity)
  308.     if not velocity then
  309.         return nil, nil, nil
  310.     end
  311.     return velocity[1], velocity[2], velocity[3]
  312. end
  313. local function yawFromPose(pose)
  314.     local w = pose.orientation.a
  315.     local x = pose.orientation.v.x
  316.     local y = pose.orientation.v.y
  317.     local z = pose.orientation.v.z
  318.     local sinyCosp = 2 * (w * y + x * z)
  319.     local cosyCosp = 1 - 2 * (y * y + z * z)
  320.     return math.deg(math.atan(sinyCosp, cosyCosp))
  321. end
  322. local FORCE_MODEL = {
  323.     burnerPressure = 0.432,
  324.     burnerForce = 53.93,
  325.     kilopond = 9.80665,
  326.     offsetPower = -32.6
  327. }
  328. local function getMass()
  329.     if not ship or not ship.getMass then
  330.         return nil
  331.     end
  332.     local ok, mass = pcall(ship.getMass)
  333.     if ok then
  334.         return mass
  335.     end
  336.     return nil
  337. end
  338. local function getHoverSignalAtHeight(sampleHeight)
  339.     if not aero or not aero.getAirPressure or not aero.getGravity or not vector then
  340.         return nil
  341.     end
  342.     local pose = getPose()
  343.     local mass = getMass()
  344.     if not pose or not mass then
  345.         return nil
  346.     end
  347.     local pressure = aero.getAirPressure(vector.new(pose.position.x, sampleHeight, pose.position.z))
  348.     if not pressure then
  349.         return nil
  350.     end
  351.     local k = FORCE_MODEL.burnerForce / FORCE_MODEL.burnerPressure
  352.     local targetForce = mass * -aero.getGravity().y
  353.     for signal = 1, 15 do
  354.         local force = ((k / 15 * signal) * pressure * FORCE_MODEL.kilopond) + FORCE_MODEL.offsetPower
  355.         if force >= targetForce then
  356.             return signal
  357.         end
  358.     end
  359.     return 15
  360. end
  361. local function setBurnerAnalogOutput(side, signal)
  362.     if relay and relay.setAnalogOutput then
  363.         local ok = pcall(relay.setAnalogOutput, relay, side, signal)
  364.         if ok then
  365.             return
  366.         end
  367.     end
  368.     redstone.setAnalogOutput(side, signal)
  369. end
  370. local function getApproachPoint(targetX, targetZ)
  371.     if not targetX or not targetZ then
  372.         return nil, nil
  373.     end
  374.     return targetX, targetZ - APPROACH_OFFSET_Z
  375. end
  376. local function getActiveNavTarget()
  377.     if not state.targetX or not state.targetZ then
  378.         return nil, nil, nil
  379.     end
  380.     if state.navStage == "target" or state.landing then
  381.         return state.targetX, state.targetZ, "target", 0
  382.     end
  383.     local approachX, approachZ = getApproachPoint(state.targetX, state.targetZ)
  384.     if not state.landing and state.x and state.z then
  385.         local dx = approachX - state.x
  386.         local dz = approachZ - state.z
  387.         local distance = math.sqrt(dx * dx + dz * dz)
  388.         local remaining = math.max(0, distance - APPROACH_RADIUS)
  389.         if remaining > 0 then
  390.             state.navStage = "approach"
  391.             return approachX, approachZ, "approach", remaining
  392.         end
  393.     end
  394.     state.navStage = "target"
  395.     return state.targetX, state.targetZ, "target", 0
  396. end
  397. local function setDestination(x, z, name)
  398.     state.targetX = x
  399.     state.targetZ = z
  400.     state.targetName = name or string.format("%.0f, %.0f", x, z)
  401.     state.previewX = x
  402.     state.previewZ = z
  403.     state.previewName = state.targetName
  404.     state.landing = false
  405.     state.landingStage = "cruise"
  406.     state.navStage = "approach"
  407.     ui.search = state.targetName
  408.     markDirty()
  409. end
  410. local function setPreviewDestination(x, z, name)
  411.     state.previewX = x
  412.     state.previewZ = z
  413.     state.previewName = name or string.format("%.0f, %.0f", x, z)
  414.     ui.search = state.previewName
  415.     switchScreen("preview")
  416. end
  417. local function parseDestination()
  418.     local query = trim(ui.search):lower()
  419.     for i = 1, #places do
  420.         if query == places[i].name:lower() then
  421.             return places[i].x, places[i].z, places[i].name
  422.         end
  423.     end
  424.     local xText, zText = query:match("^(-?%d+%.?%d*)%s*,%s*(-?%d+%.?%d*)$")
  425.     if xText and zText then
  426.         return parseNumber(xText), parseNumber(zText), nil
  427.     end
  428.     return nil, nil, nil
  429. end
  430. local function getSuggestions()
  431.     local query = trim(ui.search):lower()
  432.     local suggestions = {}
  433.     for i = 1, #places do
  434.         local place = places[i]
  435.         if query == "" or place.name:lower():find(query, 1, true) then
  436.             suggestions[#suggestions + 1] = {
  437.                 label = place.name,
  438.                 detail = string.format("%.0f, %.0f", place.x, place.z),
  439.                 x = place.x,
  440.                 z = place.z,
  441.                 name = place.name
  442.             }
  443.         end
  444.     end
  445.     local xText, zText = query:match("^(-?%d+%.?%d*)%s*,?%s*(-?%d*%.?%d*)$")
  446.     if xText and xText ~= "" then
  447.         local x = parseNumber(xText)
  448.         local z = parseNumber(zText)
  449.         if x and z then
  450.             table.insert(
  451.                 suggestions,
  452.                 1,
  453.                 {label = "Coordinates", detail = string.format("%.0f, %.0f", x, z), x = x, z = z}
  454.             )
  455.         elseif x then
  456.             table.insert(suggestions, 1, {label = "Keep typing", detail = string.format("%.0f, ?", x), partial = true})
  457.         end
  458.     end
  459.     if #suggestions == 0 and query ~= "" then
  460.         suggestions[1] = {label = "No match", detail = "Try a place name or X,Z", partial = true}
  461.     end
  462.     ui.selectedSuggestion = clamp(ui.selectedSuggestion, 1, math.max(1, #suggestions))
  463.     return suggestions
  464. end
  465. local function previewDistance()
  466.     if not state.previewX or not state.previewZ then
  467.         return nil
  468.     end
  469.     local dx = state.previewX - state.x
  470.     local dz = state.previewZ - state.z
  471.     return math.sqrt(dx * dx + dz * dz)
  472. end
  473. local function previewEta(distance)
  474.     if not distance then
  475.         return nil
  476.     end
  477.     local speed = state.horizontalSpeed
  478.     if speed < 0.15 then
  479.         speed = 8
  480.     end
  481.     return distance / speed
  482. end
  483. local function drawInputField(id, x, y, w, label, value, focused)
  484.     fill(x, y, w, 3, colors.gray)
  485.     writeAt(x + 1, y, padRight(label, w - 2), colors.lightGray, colors.gray)
  486.     writeAt(
  487.         x + 1,
  488.         y + 1,
  489.         padRight(value, w - 2),
  490.         focused and colors.black or colors.white,
  491.         focused and colors.white or colors.gray
  492.     )
  493.     registerWidget({id = "focus:" .. id, x = x, y = y, w = w, h = 3})
  494. end
  495. local function drawButton(id, x, y, w, label, active)
  496.     local bg = active and colors.white or colors.gray
  497.     local fg = active and colors.black or colors.white
  498.     writeAt(x, y, " " .. padCenter(label, w - 2) .. " ", fg, bg)
  499.     registerWidget({id = id, x = x, y = y, w = w, h = 1})
  500. end
  501. local function drawSearchBar(x, y, w, text, focused)
  502.     local fillColor = focused and colors.lightBlue or colors.gray
  503.     writeAt(x, y, " " .. padRight(text, w - 2) .. " ", colors.black, fillColor)
  504.     registerWidget({id = "focus:search", x = x, y = y, w = w, h = 1})
  505. end
  506. local function drawCard(x, y, w, h, bg)
  507.     fill(x, y, w, h, bg)
  508. end
  509. local function drawMetricCard(x, y, w, title, value, bg)
  510.     drawCard(x, y, w, 3, bg)
  511.     writeAt(x + 1, y, padRight(title, w - 2), colors.lightGray, bg)
  512.     writeAt(x + 1, y + 1, padRight(value, w - 2), colors.white, bg)
  513. end
  514. local function drawRoutePanel(x, y, w, h, targetX, targetZ, accent)
  515.     if ui.fullRedraw then
  516.         drawCard(x, y, w, h, colors.black)
  517.     end
  518.     if w < 12 or h < 7 then
  519.         return
  520.     end
  521.     if
  522.         ui.fullRedraw or not ui.layout.route or ui.layout.route.x ~= x or ui.layout.route.y ~= y or
  523.             ui.layout.route.w ~= w or
  524.             ui.layout.route.h ~= h
  525.      then
  526.         writeAt(x, y, "+" .. string.rep("-", w - 2) .. "+", colors.gray, colors.black)
  527.         for row = 1, h - 2 do
  528.             writeAt(x, y + row, "|" .. string.rep(" ", w - 2) .. "|", colors.gray, colors.black)
  529.         end
  530.         writeAt(x, y + h - 1, "+" .. string.rep("-", w - 2) .. "+", colors.gray, colors.black)
  531.         writeAt(x + 2, y + 1, padRight("route", w - 4), colors.lightGray, colors.black)
  532.         ui.layout.route = {x = x, y = y, w = w, h = h}
  533.     end
  534.     clearRect(x + 1, y + 1, w - 2, h - 2, colors.black)
  535.     writeAt(x + 2, y + 1, padRight("route", w - 4), colors.lightGray, colors.black)
  536.     local sx = x + 3
  537.     local sy = y + h - 3
  538.     local tx = x + w - 4
  539.     local ty = y + 2
  540.     if targetX and targetZ then
  541.         local dx = targetX - state.x
  542.         local dz = targetZ - state.z
  543.         local span = math.max(math.abs(dx), math.abs(dz), 1)
  544.         tx = clamp(math.floor(x + 2 + ((dx / span) + 1) * (w - 5) * 0.5 + 0.5), x + 2, x + w - 3)
  545.         ty = clamp(math.floor(y + 2 + ((dz / span) + 1) * (h - 5) * 0.5 + 0.5), y + 2, y + h - 3)
  546.     end
  547.     local steps = math.max(math.abs(tx - sx), math.abs(ty - sy))
  548.     if steps < 1 then
  549.         steps = 1
  550.     end
  551.     for i = 0, steps do
  552.         local px = sx + math.floor((tx - sx) * i / steps + 0.5)
  553.         local py = sy + math.floor((ty - sy) * i / steps + 0.5)
  554.         writeAt(px, py, " ", colors.white, accent)
  555.     end
  556.     local headingChar = "A"
  557.     if state.yaw >= -45 and state.yaw < 45 then
  558.         headingChar = "^"
  559.     elseif state.yaw >= 45 and state.yaw < 135 then
  560.         headingChar = ">"
  561.     elseif state.yaw >= -135 and state.yaw < -45 then
  562.         headingChar = "<"
  563.     else
  564.         headingChar = "v"
  565.     end
  566.     writeAt(sx, sy, headingChar, colors.black, colors.lime)
  567.     writeAt(tx, ty, "D", colors.black, colors.white)
  568. end
  569. local function drawFooter(w)
  570.     writeAt(2, ui.height, padRight(state.log, w - 2), colors.lightGray, colors.black)
  571. end
  572. local function drawSearchScene(w, h, accent)
  573.     if ui.fullRedraw then
  574.         fill(1, 1, w, h, colors.black)
  575.     end
  576.     local compact = trim(ui.search) ~= ""
  577.     local targetW = compact and math.max(18, w - 8) or math.min(w - 8, 28)
  578.     local targetX = compact and 4 or math.max(2, math.floor((w - targetW) / 2) + 1)
  579.     local targetY = compact and 3 or math.max(5, math.floor(h * 0.28))
  580.     ui.anim.searchX = expLerp(ui.anim.searchX, targetX, 14, ui.dt)
  581.     ui.anim.searchY = expLerp(ui.anim.searchY, targetY, 14, ui.dt)
  582.     ui.anim.searchW = expLerp(ui.anim.searchW, targetW, 14, ui.dt)
  583.     local searchX = math.floor(ui.anim.searchX + 0.5)
  584.     local searchY = math.floor(ui.anim.searchY + 0.5)
  585.     local searchW = math.floor(ui.anim.searchW + 0.5)
  586.     writeAt(2, 2, padRight("Autopilot", w - 2), colors.lightGray, colors.black)
  587.     writeAt(2, math.max(1, searchY - 2), padRight("Where do you want to go?", w - 2), colors.white, colors.black)
  588.     drawSearchBar(searchX, searchY, searchW, ui.search, ui.focus == "search")
  589.     local suggestions = getSuggestions()
  590.     local listY = searchY + 2
  591.     writeAt(2, listY, padRight(compact and "Suggestions" or "Recent places", w - 2), colors.lightGray, colors.black)
  592.     local maxItems = math.max(1, math.floor((h - listY - 4) / 3))
  593.     local count = math.min(#suggestions, maxItems)
  594.     for i = 1, count do
  595.         local item = suggestions[i]
  596.         local cardY = listY + 1 + (i - 1) * 3
  597.         local selected = i == ui.selectedSuggestion
  598.         local bg = selected and colors.white or colors.gray
  599.         local fg = selected and colors.black or colors.white
  600.         fill(3, cardY, w - 4, 2, bg)
  601.         writeAt(4, cardY, padRight(item.label, w - 6), fg, bg)
  602.         writeAt(4, cardY + 1, padRight(item.detail or "", w - 6), selected and colors.gray or colors.lightGray, bg)
  603.         registerWidget({id = "suggest:" .. tostring(i), x = 3, y = cardY, w = w - 4, h = 2})
  604.     end
  605.     writeAt(2, h - 2, padRight("Enter select Up/Down browse Q quit", w - 2), colors.lightGray, colors.black)
  606.     drawFooter(w)
  607. end
  608. local function drawPreviewScene(w, h, accent)
  609.     if ui.fullRedraw then
  610.         fill(1, 1, w, h, colors.black)
  611.     end
  612.     local title = state.previewName ~= "" and state.previewName or "Destination preview"
  613.     local distance = previewDistance()
  614.     local eta = previewEta(distance)
  615.     writeAt(2, 2, padRight("Preview", w - 2), colors.lightGray, colors.black)
  616.     writeAt(2, 3, padRight(title, w - 2), colors.white, colors.black)
  617.     writeAt(2, 4, padRight("Confirm route and cruise height", w - 2), accent, colors.black)
  618.     local leftW = math.max(24, math.floor(w * 0.34))
  619.     drawMetricCard(2, 6, leftW, "ETA", etaText(eta), colors.gray)
  620.     drawMetricCard(2, 10, leftW, "Distance", shortNumber(distance, 1) .. " m", colors.gray)
  621.     drawInputField("height", 2, 14, leftW, "Cruise height (m)", ui.hInput, ui.focus == "height")
  622.     local buttonY = math.max(18, h - 4)
  623.     local routeBottom = buttonY - 2
  624.     local mapX = leftW + 4
  625.     local mapW = math.max(16, w - mapX - 1)
  626.     drawRoutePanel(mapX, 6, mapW, math.max(7, routeBottom - 5), state.previewX, state.previewZ, accent)
  627.     drawButton("back", 2, buttonY, 12, "Back", ui.selectedAction == "back")
  628.     drawButton("start", 16, buttonY, 18, "Start journey", ui.selectedAction == "start")
  629.     writeAt(
  630.         2,
  631.         h - 2,
  632.         padRight("Tab height/search Left/Right switch Enter start T toggle climb", w - 2),
  633.         colors.lightGray,
  634.         colors.black
  635.     )
  636.     drawFooter(w)
  637. end
  638. local function drawJourneyScene(w, h, accent)
  639.     if ui.fullRedraw then
  640.         fill(1, 1, w, h, colors.black)
  641.     end
  642.     local destination = state.targetName ~= "" and state.targetName or "Destination"
  643.     local statusLine = state.landing and ("Landing " .. state.landingStage) or "Autopilot engaged"
  644.     if not state.landing and state.approachDistance and state.approachDistance > 0 then
  645.         statusLine = "To align zone edge " .. shortNumber(state.approachDistance, 1) .. " m"
  646.     elseif not state.landing and state.navStage == "target" then
  647.         statusLine = "Final approach"
  648.     end
  649.     writeAt(2, 2, padRight("Journey", w - 2), colors.lightGray, colors.black)
  650.     writeAt(2, 3, padRight(destination, w - 2), colors.white, colors.black)
  651.     writeAt(2, 4, padRight(statusLine, w - 2), accent, colors.black)
  652.     local buttonY = h - 2
  653.     local statsTop = h - 9
  654.     drawRoutePanel(2, 6, w - 2, statsTop - 7, state.targetX, state.targetZ, accent)
  655.     clearRect(2, statsTop, w - 2, 6, colors.gray)
  656.     writeAt(
  657.         3,
  658.         statsTop,
  659.         padRight("ETA " .. etaText(state.eta) .. " Distance " .. shortNumber(state.distance, 1) .. " m", w - 4),
  660.         colors.white,
  661.         colors.gray
  662.     )
  663.     writeAt(
  664.         3,
  665.         statsTop + 1,
  666.         padRight(
  667.             "Height " ..
  668.                 shortNumber(state.height, 1) ..
  669.                     " / " .. shortNumber(TARGET_HEIGHT, 1) .. " V/S " .. shortNumber(state.verticalSpeed, 1),
  670.             w - 4
  671.         ),
  672.         colors.black,
  673.         colors.white
  674.     )
  675.     writeAt(
  676.         3,
  677.         statsTop + 2,
  678.         padRight(
  679.             "Angle " ..
  680.                 shortNumber(TARGET_ANGLE, 1) ..
  681.                     " Motor " .. shortNumber(state.motorOutput, 1) .. " " .. (state.clutchLocked and "LOCKED" or "FREE"),
  682.             w - 4
  683.         ),
  684.         colors.white,
  685.         colors.gray
  686.     )
  687.     writeAt(
  688.         3,
  689.         statsTop + 3,
  690.         padRight("Burner " .. (state.burnerOn and "ON" or "OFF") .. " Pose " .. (state.poseOk and "OK" or "NO"), w - 4),
  691.         colors.white,
  692.         colors.gray
  693.     )
  694.     drawInputField("height", 2, statsTop + 4, 22, "Cruise height (m)", ui.hInput, ui.focus == "height")
  695.     drawButton("stop", w - 15, buttonY, 13, state.landing and "Abort" or "Stop", false)
  696.     drawFooter(w)
  697. end
  698. local function drawUI()
  699.     local w, h = clearIfNeeded()
  700.     ui.widgets = {}
  701.     local now = os.clock()
  702.     ui.dt = now - ui.lastFrameTime
  703.     if ui.dt <= 0 or ui.dt > 0.25 then
  704.         ui.dt = 0.05
  705.     end
  706.     ui.lastFrameTime = now
  707.     local accent = colors.cyan
  708.     ui.fullRedraw = ui.fullRedraw or ui.lastScreen ~= ui.screen or ui.dirty
  709.     ui.lastScreen = ui.screen
  710.     if ui.screen == "search" then
  711.         drawSearchScene(w, h, accent)
  712.     elseif ui.screen == "preview" then
  713.         drawPreviewScene(w, h, accent)
  714.     else
  715.         drawJourneyScene(w, h, accent)
  716.     end
  717.     ui.dirty = false
  718.     ui.fullRedraw = false
  719. end
  720. local function applySelectedSuggestion()
  721.     local suggestions = getSuggestions()
  722.     local suggestion = suggestions[ui.selectedSuggestion]
  723.     if suggestion and not suggestion.partial and suggestion.x and suggestion.z then
  724.         setPreviewDestination(suggestion.x, suggestion.z, suggestion.name)
  725.         setLog("Selected " .. (suggestion.name or suggestion.detail))
  726.         return true
  727.     end
  728.     local x, z, name = parseDestination()
  729.     if x and z then
  730.         setPreviewDestination(x, z, name)
  731.         setLog("Selected " .. (name or string.format("%.0f, %.0f", x, z)))
  732.         return true
  733.     end
  734.     setLog("Type a place name or coordinates")
  735.     return false
  736. end
  737. local function stopFlight()
  738.     state.auto = false
  739.     TARGET_ANGLE = 0
  740.     state.yawAlignCommand = 0
  741.     state.yawAlignSide = 0
  742.     state.landing = false
  743.     state.landingStage = "idle"
  744.     state.navStage = "idle"
  745.     state.approachDistance = nil
  746.     state.takeoff = false
  747.     state.shutdownDeadline = nil
  748.     setClutch(true)
  749.     switchScreen("preview")
  750.     setLog("Autopilot stopped")
  751. end
  752. local function stopFlightAndShutdown()
  753.     stopFlight()
  754.     state.shutdownDeadline = os.clock() + 10
  755.     setLog("Landing complete; shutting down in 10s")
  756. end
  757. local function beginLanding()
  758.     if state.landing then
  759.         return
  760.     end
  761.     state.landing = true
  762.     state.landingStage = "descent"
  763.     state.navStage = "target"
  764.     TARGET_ANGLE = 0
  765.     state.yawAlignCommand = 0
  766.     state.yawAlignSide = 0
  767.     setClutch(true)
  768.     TARGET_HEIGHT = state.height
  769.     state.targetHeight = TARGET_HEIGHT
  770.     setLog("Landing descent")
  771. end
  772. local function startFlight()
  773.     if not ship then
  774.         setLog("No ship reader found")
  775.         return
  776.     end
  777.     local x = state.previewX
  778.     local z = state.previewZ
  779.     local name = state.previewName
  780.     if not x or not z then
  781.         local ok = applySelectedSuggestion()
  782.         if not ok then
  783.             return
  784.         end
  785.         x = state.previewX
  786.         z = state.previewZ
  787.         name = state.previewName
  788.     end
  789.     local height = parseNumber(ui.hInput)
  790.     if not height then
  791.         setLog("Cruise height must be a number")
  792.         return
  793.     end
  794.     TARGET_HEIGHT = height
  795.     state.targetHeight = height
  796.     state.cruiseHeight = height
  797.     setDestination(x, z, name)
  798.     state.auto = true
  799.     state.takeoff = not state.skipTakeoff
  800.     TARGET_ANGLE = 0
  801.     setClutch(true)
  802.     if state.takeoff then
  803.         TARGET_HEIGHT = math.max(state.cruiseHeight, state.height + 35)
  804.         setLog("Vertical takeoff")
  805.     else
  806.         TARGET_HEIGHT = state.height
  807.         setLog("Direct translation")
  808.     end
  809.     state.targetHeight = TARGET_HEIGHT
  810.     switchScreen("journey")
  811. end
  812. local function focusNext()
  813.     if ui.screen == "search" then
  814.         ui.focus = "search"
  815.     elseif ui.focus == "search" then
  816.         ui.focus = "height"
  817.     else
  818.         ui.focus = "search"
  819.     end
  820.     markDirty()
  821. end
  822. local function setFocusedText(text)
  823.     if ui.focus == "search" then
  824.         ui.search = text
  825.         ui.selectedSuggestion = 1
  826.         if ui.screen ~= "journey" then
  827.             switchScreen("search")
  828.         end
  829.     elseif ui.focus == "height" then
  830.         ui.hInput = text
  831.         local parsed = parseNumber(text)
  832.         if parsed and parsed > 0 then
  833.             state.cruiseHeight = parsed
  834.             if state.auto and not state.landing then
  835.                 TARGET_HEIGHT = parsed
  836.                 state.targetHeight = parsed
  837.             end
  838.         end
  839.     end
  840.     markDirty()
  841. end
  842. local function getFocusedText()
  843.     if ui.focus == "height" then
  844.         return ui.hInput
  845.     end
  846.     return ui.search
  847. end
  848. local function handleWidget(id)
  849.     if type(id) ~= "string" then
  850.         return
  851.     end
  852.     if id == "start" then
  853.         startFlight()
  854.     elseif id == "back" then
  855.         switchScreen("search")
  856.         setLog("Back to search")
  857.     elseif id == "stop" then
  858.         stopFlight()
  859.     elseif id:sub(1, 8) == "suggest:" then
  860.         ui.selectedSuggestion = parseNumber(id:sub(9)) or 1
  861.         applySelectedSuggestion()
  862.     elseif id:sub(1, 6) == "focus:" then
  863.         ui.focus = id:sub(7)
  864.         markDirty()
  865.     end
  866. end
  867. setClutch(true)
  868. parallel.waitForAny(
  869.     function()
  870.         local burnerSide = "top"
  871.         local lastHeight = altitude.getHeight()
  872.         local lastTime = os.clock()
  873.         local hoverSignal = nil
  874.         local lastHoverRefresh = 0
  875.         local wasAuto = false
  876.         local deadzone = 2
  877.         local strongZone = 40
  878.         local predictTime = 6
  879.         local maxBurn = 4
  880.         local minBurn = 0.15
  881.         local signalKp = 0.11
  882.         local signalKd = 0.32
  883.         local function setBurnerSignal(signal)
  884.             local clamped = clamp(math.floor((signal or 0) + 0.5), 0, 15)
  885.             local isOn = clamped > 0
  886.             state.burnerOn = isOn
  887.             state.burnerSignal = clamped
  888.             setBurnerAnalogOutput(burnerSide, clamped)
  889.             if ui.screen == "journey" then
  890.                 markDirty()
  891.             end
  892.         end
  893.         local function burner(on)
  894.             setBurnerSignal(on and 15 or 0)
  895.         end
  896.         local function analogHold(height, velocity, localTargetHeight)
  897.             local predictedHeight = height + velocity * predictTime
  898.             local error = localTargetHeight - predictedHeight
  899.             local signal = hoverSignal + (error * signalKp) - (velocity * signalKd)
  900.             if localTargetHeight <= 2 then
  901.                 signal = signal - 1.5
  902.             elseif localTargetHeight <= 8 then
  903.                 signal = signal - 0.75
  904.             end
  905.             if height > localTargetHeight + 12 and velocity > 1.5 then
  906.                 signal = 0
  907.             end
  908.             setBurnerSignal(signal)
  909.         end
  910.         while state.running do
  911.             local now = os.clock()
  912.             local dt = now - lastTime
  913.             if dt <= 0 then
  914.                 dt = 0.05
  915.             end
  916.             lastTime = now
  917.  
  918.             if not state.auto then
  919.                 if wasAuto then
  920.                     burner(false)
  921.                     hoverSignal = nil
  922.                     lastHoverRefresh = 0
  923.                     lastHeight = altitude.getHeight()
  924.                     wasAuto = false
  925.                 end
  926.                 sleep(0.1)
  927.                 goto continue_height_loop
  928.             end
  929.             wasAuto = true
  930.  
  931.             local height = altitude.getHeight()
  932.             local velocity = (height - lastHeight) / dt
  933.             lastHeight = height
  934.             state.height = height
  935.             state.verticalSpeed = velocity
  936.             if now - lastHoverRefresh > 1.5 then
  937.                 local hoverSampleHeight = TARGET_HEIGHT
  938.                 if state.landing and height < 8 then
  939.                     hoverSampleHeight = height
  940.                 end
  941.                 hoverSignal = getHoverSignalAtHeight(hoverSampleHeight)
  942.                 lastHoverRefresh = now
  943.             end
  944.             local closeLanding = state.landing and ((state.distance or math.huge) <= 4 or state.landingStage == "final" or state.landingStage == "yaw_align")
  945.             if closeLanding then
  946.                 if height > 1.5 then
  947.                     burner(false)
  948.                 elseif velocity < -0.9 then
  949.                     burner(true)
  950.                     sleep(0.10)
  951.                     burner(false)
  952.                 else
  953.                     burner(false)
  954.                 end
  955.                 sleep(0.05)
  956.             elseif state.landing and state.landingStage ~= "yaw_align" then
  957.                 if hoverSignal and height > 3 then
  958.                     analogHold(height, velocity, TARGET_HEIGHT)
  959.                 elseif height > 6 then
  960.                     burner(false)
  961.                 elseif velocity < -1.2 then
  962.                     burner(true)
  963.                     sleep(0.12)
  964.                     burner(false)
  965.                 else
  966.                     burner(false)
  967.                 end
  968.                 sleep(0.05)
  969.             elseif hoverSignal then
  970.                 analogHold(height, velocity, TARGET_HEIGHT)
  971.                 sleep(0.05)
  972.             else
  973.                 local predictedHeight = height + velocity * predictTime
  974.                 local error = TARGET_HEIGHT - predictedHeight
  975.                 if velocity > 3 and height > TARGET_HEIGHT then
  976.                     burner(false)
  977.                 elseif math.abs(error) <= deadzone then
  978.                     burner(false)
  979.                 elseif error > 0 then
  980.                     local burnTime
  981.                     if error > strongZone then
  982.                         burnTime = maxBurn
  983.                     else
  984.                         burnTime = math.max(minBurn, math.min(maxBurn, error * 0.5))
  985.                     end
  986.                     burner(true)
  987.                     sleep(burnTime)
  988.                     burner(false)
  989.                 else
  990.                     burner(false)
  991.                 end
  992.                 sleep(0.05)
  993.             end
  994.             ::continue_height_loop::
  995.         end
  996.     end,
  997.     function()
  998.         local pid = PID:new(0.5, 0.001, 0.5, -5, 5)
  999.         local pulsing = false
  1000.         local pulseUntil = 0
  1001.         local cooldownUntil = 0
  1002.         local pulseSpeed = 0
  1003.         local lastShownMotor = nil
  1004.         local pulseDuration = 0.08
  1005.         local pulseCooldown = 0.18
  1006.         local pulseMagnitude = 1
  1007.         while state.running do
  1008.             local now = os.clock()
  1009.             local angle = swivel.getTargetAngle()
  1010.             state.currentAngle = angle
  1011.             local raw = pid:compute(TARGET_ANGLE, angle, 1)
  1012.             local limited = clamp(raw, -20, 20)
  1013.             local yawPulse = state.yawAlignCommand or 0
  1014.             local yawAlignSideReady = math.abs(math.abs(TARGET_ANGLE) - 90) <= 0.001 and math.abs(angle - TARGET_ANGLE) <= 8
  1015.             if state.clutchLocked then
  1016.                 pulsing = false
  1017.                 state.motorOutput = 0
  1018.                 speedRotation.setTargetSpeed(0)
  1019.             elseif state.landingStage == "yaw_align" and not yawAlignSideReady then
  1020.                 pulsing = false
  1021.                 state.motorOutput = limited
  1022.                 speedRotation.setTargetSpeed(limited)
  1023.             elseif state.landingStage == "yaw_align" and math.abs(yawPulse) > 0.1 then
  1024.                 if pulsing and now < pulseUntil then
  1025.                     state.motorOutput = pulseSpeed
  1026.                     speedRotation.setTargetSpeed(pulseSpeed)
  1027.                 elseif now < cooldownUntil then
  1028.                     pulsing = false
  1029.                     state.motorOutput = 0
  1030.                     speedRotation.setTargetSpeed(0)
  1031.                 else
  1032.                     pulseSpeed = yawPulse > 0 and pulseMagnitude or -pulseMagnitude
  1033.                     pulseUntil = now + pulseDuration
  1034.                     cooldownUntil = pulseUntil + pulseCooldown
  1035.                     pulsing = true
  1036.                     state.motorOutput = pulseSpeed
  1037.                     speedRotation.setTargetSpeed(pulseSpeed)
  1038.                 end
  1039.             elseif math.abs(limited) > 0.02 and math.abs(limited) < 1 then
  1040.                 if pulsing and now < pulseUntil then
  1041.                     state.motorOutput = pulseSpeed
  1042.                     speedRotation.setTargetSpeed(pulseSpeed)
  1043.                 elseif now < cooldownUntil then
  1044.                     pulsing = false
  1045.                     state.motorOutput = 0
  1046.                     speedRotation.setTargetSpeed(0)
  1047.                 else
  1048.                     pulseSpeed = limited >= 0 and pulseMagnitude or -pulseMagnitude
  1049.                     if state.landingStage == "yaw_align" and math.abs(limited) > 6 then
  1050.                         pulseSpeed = limited >= 0 and 2 or -2
  1051.                     end
  1052.                     pulseUntil = now + pulseDuration
  1053.                     cooldownUntil = pulseUntil + pulseCooldown
  1054.                     pulsing = true
  1055.                     state.motorOutput = pulseSpeed
  1056.                     speedRotation.setTargetSpeed(pulseSpeed)
  1057.                 end
  1058.             else
  1059.                 pulsing = false
  1060.                 state.motorOutput = limited
  1061.                 speedRotation.setTargetSpeed(limited)
  1062.             end
  1063.             if state.landingStage == "yaw_align" and (math.abs(yawPulse) <= 0.1 or not yawAlignSideReady) then
  1064.                 pulsing = false
  1065.                 if math.abs(yawPulse) <= 0.1 then
  1066.                     state.motorOutput = 0
  1067.                     speedRotation.setTargetSpeed(0)
  1068.                 end
  1069.             elseif math.abs(TARGET_ANGLE) < 0.35 and math.abs(limited) < 0.35 and now >= cooldownUntil then
  1070.                 pulsing = false
  1071.                 state.motorOutput = 0
  1072.                 speedRotation.setTargetSpeed(0)
  1073.             end
  1074.             local shownMotor = math.floor((state.motorOutput or 0) * 10 + 0.5)
  1075.             if shownMotor ~= lastShownMotor then
  1076.                 lastShownMotor = shownMotor
  1077.                 if ui.screen == "journey" then
  1078.                     markDirty()
  1079.                 end
  1080.             end
  1081.             sleep(0.05)
  1082.         end
  1083.     end,
  1084.     function()
  1085.         local turnPid = PID:new(0.5, 0.001, 0.5, -50, 50)
  1086.         local lastUiPush = 0
  1087.         while state.running do
  1088.             local pose = getPose()
  1089.             if pose then
  1090.                 state.poseOk = true
  1091.                 local curX = pose.position.x
  1092.                 local curZ = pose.position.z
  1093.                 local yaw = yawFromPose(pose)
  1094.                 local now = os.clock()
  1095.                 local velocity = getVelocityVector()
  1096.                 local velX, velY, velZ = velocityComponents(velocity)
  1097.                 if state.lastNavPoint then
  1098.                     local dt = now - state.lastNavPoint.t
  1099.                     if dt > 0.05 then
  1100.                         local dx = curX - state.lastNavPoint.x
  1101.                         local dz = curZ - state.lastNavPoint.z
  1102.                         local instant = math.sqrt(dx * dx + dz * dz) / dt
  1103.                         state.horizontalSpeed = state.horizontalSpeed * 0.7 + instant * 0.3
  1104.                     end
  1105.                 end
  1106.                 if velX and velZ then
  1107.                     state.horizontalSpeed = math.sqrt(velX * velX + velZ * velZ)
  1108.                 end
  1109.                 if velY then
  1110.                     state.verticalSpeed = velY
  1111.                     state.landingSpeed = -velY
  1112.                 else
  1113.                     state.landingSpeed = math.max(0, -state.verticalSpeed)
  1114.                 end
  1115.                 state.lastNavPoint = {x = curX, z = curZ, t = now}
  1116.                 state.x = curX
  1117.                 state.z = curZ
  1118.                 state.yaw = yaw
  1119.                 if state.auto and state.targetX and state.targetZ then
  1120.                     local activeTargetX, activeTargetZ, navStage, approachDistance = getActiveNavTarget()
  1121.                     local dx = activeTargetX - curX
  1122.                     local dz = activeTargetZ - curZ
  1123.                     local distance = math.sqrt(dx * dx + dz * dz)
  1124.                     local finalDx = state.targetX - curX
  1125.                     local finalDz = state.targetZ - curZ
  1126.                     local finalDistance = math.sqrt(finalDx * finalDx + finalDz * finalDz)
  1127.                     state.distance = finalDistance
  1128.                     state.approachDistance = navStage == "approach" and approachDistance or 0
  1129.                     if state.horizontalSpeed > 0.15 and distance > 0 then
  1130.                         state.eta = finalDistance / state.horizontalSpeed
  1131.                     else
  1132.                         state.eta = nil
  1133.                     end
  1134.                     if state.takeoff then
  1135.                         TARGET_ANGLE = 0
  1136.                         if state.height >= state.cruiseHeight - 5 then
  1137.                             state.takeoff = false
  1138.                             setClutch(false)
  1139.                             TARGET_HEIGHT = state.cruiseHeight
  1140.                             state.targetHeight = TARGET_HEIGHT
  1141.                             setLog("Cruise translation")
  1142.                         else
  1143.                             sleep(0.05)
  1144.                             goto continue_navigation
  1145.                         end
  1146.                     end
  1147.                     if not state.landing and navStage == "target" and finalDistance <= state.landingRadius then
  1148.                         beginLanding()
  1149.                     end
  1150.                     if state.landing then
  1151.                         local speed = math.max(state.horizontalSpeed, LANDING_MIN_SPEED)
  1152.                         local predictX = curX
  1153.                         local predictZ = curZ
  1154.                         if velX and velZ then
  1155.                             predictX = curX + velX * LANDING_PREDICTION_TIME
  1156.                             predictZ = curZ + velZ * LANDING_PREDICTION_TIME
  1157.                         end
  1158.                         local pDx = state.targetX - predictX
  1159.                         local pDz = state.targetZ - predictZ
  1160.                         local predictedDistance = math.sqrt(pDx * pDx + pDz * pDz)
  1161.                         local desiredAngle = 0
  1162.                         local normalizedX = pDx / math.max(predictedDistance, 0.001)
  1163.                         local normalizedZ = pDz / math.max(predictedDistance, 0.001)
  1164.                         local bearing = math.deg(math.atan2(normalizedX, normalizedZ)) * (180 / math.pi)
  1165.                         local moveCorrection = 0
  1166.                         if predictedDistance > 5 then
  1167.                             local strength = clamp(predictedDistance / LANDING_BRAKE_DISTANCE, 0.08, 1)
  1168.                             moveCorrection = turnPid:compute(0, normalizeAngle(bearing - yaw), 1) * strength
  1169.                         end
  1170.                         local normalizedYaw = normalizeAngle(yaw)
  1171.                         local yawError = normalizeAngle(FINAL_YAW - normalizedYaw)
  1172.                         if predictedDistance <= 5 and math.abs(yawError) > 3 and state.yawAlignSide == 0 then
  1173.                             state.yawAlignSide = yawError > 0 and 1 or -1
  1174.                         elseif predictedDistance > 8 then
  1175.                             state.yawAlignSide = 0
  1176.                         end
  1177.                         local yawStrength
  1178.                         if predictedDistance > 25 then
  1179.                             yawStrength = 0.08
  1180.                         elseif predictedDistance > 10 then
  1181.                             yawStrength = 0.22
  1182.                         elseif predictedDistance > 4 then
  1183.                             yawStrength = 0.45
  1184.                         else
  1185.                             yawStrength = 0.9
  1186.                         end
  1187.                         local yawCorrection = clamp(yawError * 0.9 * yawStrength, -14, 14)
  1188.                         state.yawAlignCommand = 0
  1189.                         desiredAngle =
  1190.                             clamp(
  1191.                             yawCorrection + moveCorrection * math.max(0.15, 1 - yawStrength * 0.6),
  1192.                             -LANDING_MAX_ANGLE,
  1193.                             LANDING_MAX_ANGLE
  1194.                         )
  1195.                         TARGET_ANGLE = desiredAngle
  1196.                         local descentRate
  1197.                         if predictedDistance > 50 then
  1198.                             state.landingStage = "translate"
  1199.                             descentRate = 0.15
  1200.                         elseif predictedDistance > 20 then
  1201.                             state.landingStage = "approach"
  1202.                             descentRate = 0.35
  1203.                         elseif predictedDistance > 8 then
  1204.                             state.landingStage = "precision"
  1205.                             descentRate = 0.22
  1206.                         else
  1207.                             state.landingStage = "final"
  1208.                             descentRate = 0.18
  1209.                         end
  1210.                         local landingTarget = math.max(0, state.height - descentRate)
  1211.                         if velY and velY < -0.8 then
  1212.                             landingTarget = landingTarget + math.abs(velY) * 0.25
  1213.                         end
  1214.                         TARGET_HEIGHT = landingTarget
  1215.                         state.targetHeight = landingTarget
  1216.                         if predictedDistance <= 5 and math.abs(yawError) > 6 then
  1217.                             setClutch(false)
  1218.                             if state.yawAlignSide == 0 then
  1219.                                 state.yawAlignSide = yawError > 0 and 1 or -1
  1220.                             end
  1221.                         end
  1222.                         if math.abs(velY or 0) <= 0.12 and state.horizontalSpeed <= 0.25 then
  1223.                             state.landingStage = "yaw_align"
  1224.                             local yawError = normalizeAngle(FINAL_YAW - normalizedYaw)
  1225.                             TARGET_HEIGHT = 0
  1226.                             state.targetHeight = 0
  1227.                             if math.abs(yawError) > 1.5 then
  1228.                                 setClutch(false)
  1229.                                 state.yawAlignCommand = yawError
  1230.                                 if state.yawAlignSide == 0 then
  1231.                                     state.yawAlignSide = yawError > 0 and 1 or -1
  1232.                                 end
  1233.                                 TARGET_ANGLE = state.yawAlignSide > 0 and 90 or -90
  1234.                             else
  1235.                                 state.yawAlignCommand = 0
  1236.                                 state.yawAlignSide = 0
  1237.                                 TARGET_ANGLE = 0
  1238.                                 setClutch(true)
  1239.                                 stopFlightAndShutdown()
  1240.                             end
  1241.                         end
  1242.                         if math.abs(velY or 0) <= 0.12 and state.horizontalSpeed <= 0.25 then
  1243.                             state.yawAlignCommand = 0
  1244.                             state.yawAlignSide = 0
  1245.                             TARGET_ANGLE = 0
  1246.                             setClutch(true)
  1247.                             stopFlightAndShutdown()
  1248.                         end
  1249.                     elseif distance > 0.001 then
  1250.                         state.yawAlignSide = 0
  1251.                         local stoppingDistance = state.horizontalSpeed * 3.2
  1252.                         local targetX = activeTargetX
  1253.                         local targetZ = activeTargetZ
  1254.                         if velX and velZ then
  1255.                             targetX = targetX - velX * 1.4
  1256.                             targetZ = targetZ - velZ * 1.4
  1257.                         end
  1258.                         local correctedDx = targetX - curX
  1259.                         local correctedDz = targetZ - curZ
  1260.                         local correctedDistance = math.sqrt(correctedDx * correctedDx + correctedDz * correctedDz)
  1261.                         if correctedDistance > 0 then
  1262.                             local normalizedX = correctedDx / correctedDistance
  1263.                             local normalizedZ = correctedDz / correctedDistance
  1264.                             local bearing = math.atan(normalizedX, normalizedZ) * (180 / math.pi)
  1265.                             local error = turnPid:compute(0, normalizeAngle(bearing - yaw), 1)
  1266.                             local brakeFactor = 1
  1267.                             if distance < stoppingDistance then
  1268.                                 brakeFactor = clamp(distance / stoppingDistance, 0.18, 1)
  1269.                             end
  1270.                             TARGET_ANGLE = error * brakeFactor
  1271.                             if navStage == "target" and math.abs(correctedDx) < 3 and correctedDz > 0 then
  1272.                                 TARGET_ANGLE = clamp(TARGET_ANGLE, -LANDING_MAX_ANGLE, LANDING_MAX_ANGLE)
  1273.                             end
  1274.                         else
  1275.                             TARGET_ANGLE = 0
  1276.                         end
  1277.                 else
  1278.                     state.approachDistance = nil
  1279.                     state.yawAlignCommand = 0
  1280.                     state.yawAlignSide = 0
  1281.                     TARGET_ANGLE = 0
  1282.                 end
  1283.                 else
  1284.                     state.yawAlignCommand = 0
  1285.                     state.yawAlignSide = 0
  1286.                     state.distance = previewDistance()
  1287.                     state.eta = previewEta(state.distance)
  1288.                 end
  1289.             else
  1290.                 state.yawAlignCommand = 0
  1291.                 state.yawAlignSide = 0
  1292.                 state.poseOk = false
  1293.             end
  1294.             if (ui.screen == "preview" or ui.screen == "journey") and os.clock() - lastUiPush > 0.25 then
  1295.                 lastUiPush = os.clock()
  1296.                 markDirty()
  1297.             end
  1298.             if state.shutdownDeadline and not state.auto and os.clock() >= state.shutdownDeadline then
  1299.                 os.reboot()
  1300.                 break
  1301.             end
  1302.             ::continue_navigation::
  1303.             sleep(0.05)
  1304.         end
  1305.     end,
  1306.     function()
  1307.         drawUI()
  1308.         while state.running do
  1309.             local timer = nil
  1310.             if isAnimating() then
  1311.                 timer = os.startTimer(0.05)
  1312.             end
  1313.             local event = {os.pullEvent()}
  1314.             if event[1] == "ui_dirty" then
  1315.                 drawUI()
  1316.             elseif event[1] == "timer" and timer and event[2] == timer then
  1317.                 drawUI()
  1318.                 if isAnimating() then
  1319.                     markDirty()
  1320.                 end
  1321.             elseif event[1] == "mouse_click" then
  1322.                 if state.shutdownDeadline then
  1323.                     state.shutdownDeadline = nil
  1324.                     setLog("Shutdown canceled")
  1325.                 end
  1326.                 local mx = event[3]
  1327.                 local my = event[4]
  1328.                 for i = #ui.widgets, 1, -1 do
  1329.                     local widget = ui.widgets[i]
  1330.                     if pointIn(mx, my, widget) then
  1331.                         handleWidget(widget.id)
  1332.                         break
  1333.                     end
  1334.                 end
  1335.             elseif event[1] == "char" then
  1336.                 if state.shutdownDeadline then
  1337.                     state.shutdownDeadline = nil
  1338.                     setLog("Shutdown canceled")
  1339.                 end
  1340.                 local ch = event[2]
  1341.                 if ch == "q" then
  1342.                     state.running = false
  1343.                     break
  1344.                 end
  1345.                 setFocusedText(getFocusedText() .. ch)
  1346.                 startAnimation(0.18)
  1347.             elseif event[1] == "paste" then
  1348.                 if state.shutdownDeadline then
  1349.                     state.shutdownDeadline = nil
  1350.                     setLog("Shutdown canceled")
  1351.                 end
  1352.                 setFocusedText(getFocusedText() .. event[2])
  1353.                 startAnimation(0.18)
  1354.             elseif event[1] == "key" then
  1355.                 if state.shutdownDeadline then
  1356.                     state.shutdownDeadline = nil
  1357.                     setLog("Shutdown canceled")
  1358.                 end
  1359.                 local keyCode = event[2]
  1360.                 if keyCode == keys.backspace then
  1361.                     local text = getFocusedText()
  1362.                     local editingField = (ui.focus == "height") or (ui.focus == "search" and ui.screen == "search")
  1363.                     if editingField and #text > 0 then
  1364.                         setFocusedText(text:sub(1, #text - 1))
  1365.                         startAnimation(0.18)
  1366.                     elseif ui.screen == "preview" then
  1367.                         switchScreen("search")
  1368.                         setLog("Back to search")
  1369.                     elseif ui.screen == "journey" then
  1370.                         stopFlight()
  1371.                     end
  1372.                 elseif keyCode == keys.tab then
  1373.                     focusNext()
  1374.                 elseif keyCode == keys.up and ui.screen == "search" then
  1375.                     ui.selectedSuggestion = math.max(1, ui.selectedSuggestion - 1)
  1376.                     markDirty()
  1377.                 elseif keyCode == keys.down and ui.screen == "search" then
  1378.                     ui.selectedSuggestion = ui.selectedSuggestion + 1
  1379.                     markDirty()
  1380.                 elseif (keyCode == keys.left or keyCode == keys.right) and ui.screen == "preview" then
  1381.                     ui.selectedAction = ui.selectedAction == "start" and "back" or "start"
  1382.                     markDirty()
  1383.                 elseif keyCode == keys.enter then
  1384.                     if ui.screen == "search" then
  1385.                         applySelectedSuggestion()
  1386.                     elseif ui.screen == "preview" then
  1387.                         if ui.selectedAction == "back" then
  1388.                             switchScreen("search")
  1389.                             setLog("Back to search")
  1390.                         else
  1391.                             startFlight()
  1392.                         end
  1393.                     elseif ui.screen == "journey" then
  1394.                         stopFlight()
  1395.                     end
  1396.                 elseif keyCode == keys.t and ui.screen == "preview" then
  1397.                     state.skipTakeoff = not state.skipTakeoff
  1398.                     markDirty()
  1399.                 elseif keyCode == keys.space then
  1400.                     if ui.screen == "preview" then
  1401.                         startFlight()
  1402.                     elseif ui.screen == "journey" then
  1403.                         stopFlight()
  1404.                     end
  1405.                 end
  1406.             elseif event[1] == "terminate" then
  1407.                 state.running = false
  1408.                 break
  1409.             end
  1410.         end
  1411.     end
  1412. )
  1413. speedRotation.setTargetSpeed(0)
  1414. redstone.setOutput("top", false)
  1415. setClutch(true)
  1416. term.setBackgroundColor(colors.black)
  1417. term.setTextColor(colors.white)
  1418. term.clear()
  1419. term.setCursorPos(1, 1)
  1420. print("Autopilot UI closed")
  1421.  
Advertisement
Add Comment
Please, Sign In to add comment