local_aim.lua
1 -- LocalAim 2 -- 3 -- Predicts local aim angle in radians: 4 -- - Seeds from snapshot (authoritative aim) when available. 5 -- - Applies aim_delta every frame (I/O keys) with same increment as server (0.10 rad per tick). 6 -- - Smooths toward authoritative aim to avoid drift. 7 -- 8 -- Public: 9 -- LocalAim.new({ step=0.10, corr_rate=14.0, snap_angle=1.6 }) 10 -- :on_snapshot(players, my_id) 11 -- :update(dt, input) -- dt used only for smoothing 12 -- :angle() -> radians | nil 13 14 local LocalAim = {} 15 LocalAim.__index = LocalAim 16 17 local function wrap_pi(a) 18 while a > math.pi do a = a - 2 * math.pi end 19 while a < -math.pi do a = a + 2 * math.pi end 20 return a 21 end 22 23 local function ang_diff(a, b) 24 -- shortest signed difference from a -> b 25 return wrap_pi(b - a) 26 end 27 28 local function exp_smooth(rate, dt) 29 return 1 - math.exp(-rate * dt) 30 end 31 32 local function find_player(players, my_id) 33 if not players or not my_id then return nil end 34 for _, p in ipairs(players) do 35 if p.id == my_id then return p end 36 end 37 return nil 38 end 39 40 function LocalAim.new(opts) 41 opts = opts or {} 42 return setmetatable({ 43 step = opts.step or 0.10, 44 corr_rate = opts.corr_rate or 14.0, 45 snap_angle = opts.snap_angle or 1.6, -- if drift > this, snap (radians) 46 47 have_auth = false, 48 auth_a = 0.0, 49 pred_a = 0.0, 50 }, LocalAim) 51 end 52 53 function LocalAim:reset() 54 self.have_auth = false 55 self.auth_a = 0.0 56 self.pred_a = 0.0 57 end 58 59 function LocalAim:on_snapshot(players, my_id) 60 local p = find_player(players, my_id) 61 if not p then return end 62 63 local a = tonumber(p.aim) or 0.0 64 self.auth_a = wrap_pi(a) 65 66 if not self.have_auth then 67 self.pred_a = self.auth_a 68 self.have_auth = true 69 return 70 end 71 72 local d = math.abs(ang_diff(self.pred_a, self.auth_a)) 73 if d > self.snap_angle then 74 self.pred_a = self.auth_a 75 end 76 end 77 78 function LocalAim:update(dt, input) 79 if not self.have_auth then return end 80 if not input then return end 81 82 local ad = tonumber(input.aim_delta) or 0 83 if ad ~= 0 then 84 self.pred_a = wrap_pi(self.pred_a + (ad * self.step)) 85 end 86 87 -- smooth toward auth 88 local a = exp_smooth(self.corr_rate, dt) 89 local d = ang_diff(self.pred_a, self.auth_a) 90 self.pred_a = wrap_pi(self.pred_a + d * a) 91 end 92 93 function LocalAim:angle() 94 if not self.have_auth then return nil end 95 return self.pred_a 96 end 97 98 return LocalAim