60 lines
3.3 KiB
Lua
Executable File
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
|