/ cli / src / net / local_aim.lua
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