Files
last-hearth/mods/hearth/combat.lua
T
2026-11-03 12:41:09 -05:00

60 lines
3.3 KiB
Lua
Executable File

-- 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