SHOTS = { { slot = 1, hold = 0.9, yaw = -math.pi/2, pitch = -0.25, label = "bow_d_neg25" }, { slot = 1, hold = 0.9, yaw = -math.pi/2, pitch = -0.15, label = "bow_d_neg15" }, { slot = 1, hold = 0.9, yaw = -math.pi/2, pitch = -0.08, label = "bow_d_neg08" }, { slot = 1, hold = 0.9, yaw = -math.pi/2, pitch = -0.03, label = "bow_d_neg03" }, { slot = 1, hold = 0.9, yaw = -math.pi/2, pitch = 0.00, label = "bow_d_000" }, { slot = 1, hold = 0.9, yaw = -math.pi/2, pitch = 0.04, label = "bow_d_004" }, { slot = 1, hold = 0.9, yaw = -math.pi/2, pitch = 0.08, label = "bow_d_008" }, { slot = 1, hold = 0.9, yaw = -math.pi/2, pitch = 0.13, label = "bow_d_013" }, } -- flight_probe.lua — measure Tribes projectile flight physics from the game's own data. -- Run: relayctl exec -f flight_probe.lua (uses the default SHOTS below) -- any prefix may set SHOTS = { ... } before this file's body (concatenated script). -- -- It wraps, for the duration of the run, three things and restores all of them before -- returning: -- * Effects.Projectile — the client's launch point/velocity/gravity (client prediction) -- * Ballistics.Advance — the game's own integrator, so every step is recorded verbatim -- * WeaponAction:FireServer (via __namecall) — the exact charge/dir/origin the client sent -- and listens on BOTH fx channels (CombatFX / CombatFXU) for the server's ProjectileHit. -- -- Output: one "FLIGHT2" line per shot (tab-separated key=value), plus "ADV" summary lines. local Players = game:GetService("Players") local RS = game:GetService("ReplicatedStorage") local LP = Players.LocalPlayer local char = LP.Character local Cam = require(RS.Shared.Camera) local Util = require(LP.PlayerScripts.Combat.Util) local Effects = require(LP.PlayerScripts.Combat.Effects) local Ball = require(RS.Shared.Combat.Ballistics) local C = require(LP.PlayerScripts.Combat.Controller) local Weapons = require(RS.Shared.Weapons) -- weapon slot map for the active loadout (Archer): 1 bow, 2 sling, 3 knives, 4 bandage, 5 bomb local SHOTS = SHOTS or { { slot = 1, hold = 0.9, pitch = 0.0, label = "bow_default" }, } local function f(x) return string.format("%.4f", x) end -- ---------------------------------------------------------------- hooks local projCalls = {} local origProj = Effects.Projectile Effects.Projectile = function(key, kind, origin, velocity, gravity, shooter, prediction) table.insert(projCalls, { key = key, kind = kind, origin = origin, velocity = velocity, gravity = gravity, mine = (shooter == char), t = os.clock() }) return origProj(key, kind, origin, velocity, gravity, shooter, prediction) end local advances = {} local origAdvance = Ball.Advance Ball.Advance = function(shot, dt) local p0, v0, g = shot.pos, shot.vel, shot.gravity local a, b = origAdvance(shot, dt) table.insert(advances, { shot = shot, dt = dt, p0 = p0, v0 = v0, dp = b, p1 = shot.pos, v1 = shot.vel, g = g, t = os.clock() }) return a, b end local fired = {} -- FireServer Shoot payloads local hits = {} -- ProjectileHit events local fxConns = {} local origNamecall local nc = newcclosure(function(self, ...) if self == RS.Remotes.WeaponAction and getnamecallmethod() == "FireServer" then local action, payload = ... if action == "Shoot" then table.insert(fired, { charge = payload.charge, dir = payload.dir, origin = payload.origin, id = payload.id, t = os.clock() }) end end return origNamecall(self, ...) end) origNamecall = hookmetamethod(game, "__namecall", nc) for _, name in ipairs({ "CombatFX", "CombatFXU" }) do local r = RS.Remotes:FindFirstChild(name) if r then table.insert(fxConns, r.OnClientEvent:Connect(function(action, data) if action == "ProjectileHit" and type(data) == "table" then table.insert(hits, { key = data.key, position = data.position, expired = data.expired, part = data.part, character = data.character, t = os.clock() }) end end)) end end -- ---------------------------------------------------------------- helpers local savedYaw, savedPitch = Cam.GetYaw(), Cam.GetPitch() local function equipSlot(slot) LP:SetAttribute("EquipSlot", slot) local deadline = os.clock() + 3 while os.clock() < deadline do local t = char:FindFirstChildWhichIsA("Tool") if t and t:GetAttribute("SlotIndex") == slot then return t end task.wait(0.05) end return char:FindFirstChildWhichIsA("Tool") end local function lookAt(from, to) local d = (to - from).Unit Cam.SetLook(math.atan2(-d.X, -d.Z), math.asin(math.clamp(d.Y, -1, 1))) end -- overlay's closed-form launch-speed solver local function solveLaunchSpeed(origin, u, impact, gravity) local hu = math.sqrt(u.X * u.X + u.Z * u.Z) if hu < 1e-3 then return nil end local dx, dz = impact.X - origin.X, impact.Z - origin.Z local horizontal = math.sqrt(dx * dx + dz * dz) if horizontal < 1 then return nil end local R = horizontal / hu local den = origin.Y + u.Y * R - impact.Y if den <= 0.05 then return nil end local s2 = 0.5 * gravity * R * R / den if s2 <= 0 then return nil end return math.sqrt(s2) end -- solve (t, g) from origin+velocity+impact using the two gravity-free horizontal axes local function solveTG(origin, velocity, impact) local tx, tz if math.abs(velocity.X) > 0.01 then tx = (impact.X - origin.X) / velocity.X end if math.abs(velocity.Z) > 0.01 then tz = (impact.Z - origin.Z) / velocity.Z end local t if tx and tz then t = (tx + tz) * 0.5 elseif tx then t = tx else t = tz end if not t or t <= 0.02 or t > 5 then return nil end local g = 2 * (origin.Y + velocity.Y * t - impact.Y) / (t * t) local pred = origin + velocity * t + Vector3.new(0, -0.5 * g * t * t, 0) return t, g, (pred - impact).Magnitude, tx, tz end -- ---------------------------------------------------------------- run local report = {} local dummies = game:GetService("CollectionService"):GetTagged("TrainingDummy") for si, shot in ipairs(SHOTS) do local tool = equipSlot(shot.slot) task.wait(0.7) local weaponId = tool and tool:GetAttribute("WeaponId") local def = weaponId and Weapons.Get(weaponId) local tuning = def and def.tuning or {} -- aim local origin = Util.ShotOrigin(tool, "Muzzle") if shot.dummy then local d = dummies[shot.dummy] if d then local p = (d:FindFirstChild("HumanoidRootPart") or d.PrimaryPart).Position lookAt(origin, p + Vector3.new(0, shot.aimY or 0, 0)) end else -- fixed yaw toward the far dummy, given pitch local yaw = shot.yaw if yaw == nil then local d = dummies[4] or dummies[1] local p = (d:FindFirstChild("HumanoidRootPart") or d.PrimaryPart).Position local delta = p - origin yaw = math.atan2(-delta.X, -delta.Z) end Cam.SetLook(yaw, shot.pitch or 0) end task.wait(0.08) local nFired0, nHits0, nProj0, nAdv0 = #fired, #hits, #projCalls, #advances local t0 = os.clock() -- fire C.Dispatch("Primary", true) task.wait(shot.hold or 0.9) C.Dispatch("Primary", false) -- wait for this shot's impact (or timeout) local hit local deadline = os.clock() + 3.5 while os.clock() < deadline do if #hits > nHits0 then hit = hits[nHits0 + 1] break end task.wait(0.03) end task.wait(0.25) local fire = fired[#fired] local proj = projCalls[#projCalls] local rec = { label = shot.label or ("shot" .. si), weaponId = weaponId, kind = def and def.kind, charge = fire and fire.charge, gravityPassed = proj and proj.gravity or tuning.gravity, speedLocal = proj and proj.velocity and proj.velocity.Magnitude, origin = proj and proj.origin, velocity = proj and proj.velocity, dirSent = fire and fire.dir, hit = hit and hit.position, expired = hit and hit.expired, hitPart = hit and hit.part and hit.part.Name, hitTarget = hit and hit.character and hit.character.Name } if proj and proj.origin and hit and hit.position and not hit.expired then local t, g, resid, tx, tz = solveTG(proj.origin, proj.velocity, hit.position) rec.t = t; rec.gSolved = g; rec.yResid = resid; rec.tx = tx; rec.tz = tz rec.distH = (Vector3.new(hit.position.X - proj.origin.X, 0, hit.position.Z - proj.origin.Z)).Magnitude rec.dist3D = (hit.position - proj.origin).Magnitude rec.speedSolved = solveLaunchSpeed(proj.origin, fire and fire.dir, hit.position, rec.gravityPassed) end -- advances for this shot's window local win = {} for i = nAdv0 + 1, #advances do win[#win + 1] = advances[i] end rec.advN = #win if #win > 0 then local tsum, gsum, wsum, resid, first = 0, 0, 0, 0, win[1] local modelMax = 0 for _, a in ipairs(win) do tsum = tsum + a.dt local dvy = (a.v0.y - a.v1.y) gsum = gsum + dvy / a.dt * a.dt wsum = wsum + a.dt local expect = a.v0 * a.dt + Vector3.new(0, -0.5 * a.g * a.dt * a.dt, 0) local m = (a.dp - expect).Magnitude if m > modelMax then modelMax = m end end rec.advT = tsum rec.gMeasured = gsum / wsum rec.advModelResid = modelMax rec.advVy0 = first.v0.y rec.advVyN = win[#win].v1.y rec.advPos0 = first.p0 rec.advPosN = win[#win].p1 end report[#report + 1] = rec task.wait(0.4) end -- ---------------------------------------------------------------- restore Cam.SetLook(savedYaw, savedPitch) Effects.Projectile = origProj Ball.Advance = origAdvance hookmetamethod(game, "__namecall", origNamecall) for _, c in ipairs(fxConns) do c:Disconnect() end -- ---------------------------------------------------------------- print for _, r in ipairs(report) do local parts = { "FLIGHT2", "label=" .. r.label, "weapon=" .. tostring(r.weaponId), "kind=" .. tostring(r.kind), "charge=" .. tostring(r.charge and f(r.charge) or "nil"), "gPassed=" .. tostring(r.gravityPassed), "speedLocal=" .. tostring(r.speedLocal and f(r.speedLocal) or "nil"), "speedSolved=" .. tostring(r.speedSolved and f(r.speedSolved) or "nil"), "o=" .. tostring(r.origin), "v=" .. tostring(r.velocity), "dir=" .. tostring(r.dirSent), "hit=" .. tostring(r.hit), "expired=" .. tostring(r.expired), "part=" .. tostring(r.hitPart), "target=" .. tostring(r.hitTarget), "distH=" .. tostring(r.distH and f(r.distH) or "nil"), "dist3D=" .. tostring(r.dist3D and f(r.dist3D) or "nil"), "t=" .. tostring(r.t and f(r.t) or "nil"), "gSolved=" .. tostring(r.gSolved and f(r.gSolved) or "nil"), "yResid=" .. tostring(r.yResid and f(r.yResid) or "nil"), "tx=" .. tostring(r.tx and f(r.tx) or "nil"), "tz=" .. tostring(r.tz and f(r.tz) or "nil"), "advN=" .. tostring(r.advN), "advT=" .. tostring(r.advT and f(r.advT) or "nil"), "gMeasured=" .. tostring(r.gMeasured and f(r.gMeasured) or "nil"), "advVy0=" .. tostring(r.advVy0 and f(r.advVy0) or "nil"), "advVyN=" .. tostring(r.advVyN and f(r.advVyN) or "nil"), "advModelResid=" .. tostring(r.advModelResid and f(r.advModelResid) or "nil"), } print(table.concat(parts, " ")) end print("DONE shots=" .. #report)