-- Pure server combat rules: no engine, clock, random, persistence or regeneration. local M = {} local weapons = { ["hearth:axe"] = {reach = 2.5, cadence = 1.1, stamina = 12, damage = 4, wear = 655, sound = "hearth_swing_axe"}, ["hearth:knife"] = {reach = 1.8, cadence = 0.7, stamina = 8, damage = 3, wear = 437, sound = "hearth_swing_knife"}, ["hearth:spear"] = {reach = 3.4, cadence = 1.2, stamina = 14, damage = 5, wear = 546, sound = "hearth_swing_spear"}, } local function finite(n) return type(n) == "number" and n == n and math.abs(n) < math.huge end local function position(p) return type(p) == "table" and finite(p.x) and finite(p.y) and finite(p.z) end function M.distance(a, b) return math.sqrt((a.x - b.x)^2 + (a.y - b.y)^2 + (a.z - b.z)^2) end function M.facing(origin, target, look, threshold) if not position(origin) or not position(target) or not position(look) then return false end local distance, length = M.distance(origin, target), math.sqrt(look.x^2 + look.y^2 + look.z^2) if distance < 0.001 or length < 0.001 then return false end return ((target.x - origin.x) * look.x + (target.y - origin.y) * look.y + (target.z - origin.z) * look.z) / (distance * length) + 1e-9 >= threshold end -- Recent displacement check, NOT a full teleport/flight anticheat. function M.sample(previous, pos, now) if not position(pos) or not finite(now) then return previous end local blocked = previous and previous.blocked_until or 0 if previous then local dt = now - previous.time if dt < 0 then return previous end if dt <= 0.5 and M.distance(previous.pos, pos) > 1.25 + 8 * dt then blocked = now + 0.5 end end return {pos = {x = pos.x, y = pos.y, z = pos.z}, time = now, blocked_until = blocked} end function M.valid_attack(c) local s, state, attack = c.session, c.state, weapons[c.weapon] if c.live ~= true or type(s) ~= "table" or type(state) ~= "table" or s.waiting or not finite(c.hp) or c.hp <= 0 then return nil, "inactive" end if not finite(c.now) or c.now < (s.combat_now or 0) then return nil, "clock" end if not attack then return nil, "weapon" end if c.now < (s.attack_ready_at or 0) or c.now < (s.spar_ready_at or 0) then return nil, "cadence" end if not finite(state.stamina) or state.stamina < attack.stamina then return nil, "stamina" end if c.allowed ~= true then return nil, "claim" end if not position(c.origin) or not position(c.eye) or not position(c.target) then return nil, "position" end if M.distance(c.eye, c.target) > attack.reach then return nil, "reach" end if not M.facing(c.eye, c.target, c.look, 0.35) then return nil, "aim" end if c.clear ~= true then return nil, "obstructed" end local sample = s.combat_sample if sample then local dt = c.now - sample.time if dt < 0 or c.now < (sample.blocked_until or 0) then return nil, "movement" end if dt <= 0.5 and M.distance(sample.pos, c.origin) > 1.25 + 8 * dt then return nil, "movement" end end local result = {}; for k, v in pairs(attack) do result[k] = v end return result end function M.valid_guard(c) return c.live == true and c.session ~= nil and not c.session.waiting and c.state ~= nil and finite(c.hp) and c.hp > 0 and finite(c.state.stamina) and c.state.stamina >= 8 and c.weapon == "hearth:spear" and c.controls ~= nil and c.controls.RMB == true and M.facing(c.eye, c.target, c.look, 0.4) end return M