Files
2026-11-03 12:41:09 -05:00

167 lines
7.6 KiB
Lua
Executable File

local C, M = core, hearth.model
local map_limit = tonumber(C.settings and C.settings:get("mapgen_limit")) or 512
local recovery_limit = map_limit + 32
-- Clock, environment, and lifecycle modules are installed before scheduling.
-- Main installs hearth.movement before this module; feedback is optional.
function hearth.movement_step(player, dt)
local name = player:get_player_name()
local s, state = hearth.sessions[name], hearth.players[name]
if not s or not state or s.waiting or player:get_hp() <= 0 then return end
local pos, velocity = player:get_pos(), player:get_velocity()
local function definition(y)
local node = C.get_node({x = pos.x, y = pos.y + y, z = pos.z})
return C.registered_nodes[node.name] or {}
end
local below, feet, body = definition(-0.1), definition(0.1), definition(0.9)
local function water(def) return def.groups and (def.groups.water or 0) > 0 end
local result = hearth.movement.step(s, state, player:get_player_control(), dt, {
weapon = player:get_wielded_item():get_name(), velocity = velocity,
grounded = below.walkable ~= false and next(below) ~= nil,
swimming = water(body) or (water(feet) and below.walkable == false),
})
-- Only override speed: leave other mods' jump/gravity and camera untouched.
if not s.speed or math.abs(result.speed - s.speed) >= 0.005 then
player:set_physics_override({speed = result.speed}); s.speed = result.speed
end
return result
end
local REST_SECONDS, REST_COOLDOWN = 5, 30
function hearth.start_rest(player, pos)
if not player or not player:is_player() then return false end
local name = player:get_player_name()
local session, state = hearth.sessions[name], hearth.players[name]
if not session or not state or session.waiting or player:get_hp() <= 0 then return false end
if not hearth.construction.allowed(pos, player, "use") then return false end
local node = C.get_node_or_nil(pos)
if not node or node.name ~= "hearth:bedroll" then return false end
local now = C.get_us_time() / 1000000
if now < (session.rest_ready_at or 0) then
hearth.feedback(player, "Rest again in " .. math.ceil(session.rest_ready_at - now) .. "s", "warning")
return false
end
if session.resting then hearth.feedback(player, "Already resting", "info"); return false end
if now < (session.attack_ready_at or 0) or now < (session.spar_ready_at or 0) then
hearth.feedback(player, "Recover from the encounter before resting", "warning")
return false
end
local p, velocity = player:get_pos(), player:get_velocity()
local cover = hearth.environment(p)
if math.abs(velocity.x) + math.abs(velocity.z) > 0.25 or cover.shelter < 0.6 or cover.smoke then
hearth.feedback(player, "Stay still on the bedroll in sheltered, ventilated space to rest", "warning")
return false
end
session.resting = {pos = {x = pos.x, y = pos.y, z = pos.z},
player_pos = {x = p.x, y = p.y, z = p.z}, hp = player:get_hp(), elapsed = 0}
hearth.feedback(player, "Resting / remain still under cover for five seconds", "info")
return true
end
function hearth.rest_step(player, dt, controls)
local name = player:get_player_name()
local session, state = hearth.sessions[name], hearth.players[name]
local rest = session and session.resting
if not rest then return false end
local p, velocity = player:get_pos(), player:get_velocity()
local moved = math.abs(p.x - rest.player_pos.x) + math.abs(p.y - rest.player_pos.y)
+ math.abs(p.z - rest.player_pos.z)
local now = C.get_us_time() / 1000000
local interrupted = not state or not controls or controls.up or controls.down
or controls.left or controls.right or controls.jump or controls.sneak
or moved > 0.2 or math.abs(velocity.x) + math.abs(velocity.z) > 0.25
or player:get_hp() < rest.hp
-- Shelter/claim checks are spatial queries; cache them briefly instead of
-- rescanning roof/fire geometry at the full render-step frequency.
if now >= (rest.validate_at or 0) then
local bed = C.get_node_or_nil(rest.pos)
local cover = hearth.environment(p)
rest.valid = bed and bed.name == "hearth:bedroll"
and hearth.construction.allowed(rest.pos, player, "use")
and cover.shelter >= 0.6 and not cover.smoke
rest.validate_at = now + 0.25
end
interrupted = interrupted or not rest.valid
if interrupted then
session.resting = nil
hearth.feedback(player, "Rest interrupted / stay still in shelter with ventilation", "warning")
return false
end
rest.elapsed = rest.elapsed + math.min(math.max(dt, 0), 0.1)
if rest.elapsed + 0.000001 >= REST_SECONDS then
session.rest_warmth_pending = 8
session.resting, session.rest_ready_at = nil, C.get_us_time() / 1000000 + REST_COOLDOWN
hearth.feedback(player, "Rest complete / +8 warmth", "success")
return true
end
return true
end
local elapsed, clock_elapsed = 0, 0
C.register_globalstep(function(dt)
elapsed, clock_elapsed = elapsed + dt, clock_elapsed + dt
if clock_elapsed >= 10 then hearth.clock_update(); clock_elapsed = 0 end
-- Small bounded steps for smooth sprint state; needs/HUD at 1 Hz.
for _, player in ipairs(C.get_connected_players()) do
local result = hearth.movement_step(player, dt)
if result then
if hearth.feedback_step then hearth.feedback_step(player, dt) end
hearth.rest_step(player, dt, player:get_player_control())
end
end
if elapsed < 1 then return end
local step = math.min(elapsed, 2); elapsed = 0
for _, player in ipairs(C.get_connected_players()) do
local name, pos = player:get_player_name(), player:get_pos()
local s, state = hearth.sessions[name], hearth.players[name]
if s and state and not s.waiting and player:get_hp() > 0 then
if pos.y < -recovery_limit then
hearth.safe_arrival(player)
else
local env = hearth.environment(pos)
env.sprinting = s.sprinting
env.clothing = player:get_meta():get_int("hearth:cloak") == 1
local rack = env.shelter >= 0.6 and C.find_node_near(pos, 4, {"hearth:drying_rack"})
env.drying = env.heat > 0 or (env.shelter >= 0.6 and rack ~= nil)
local updated, danger = M.needs(state, env, step)
updated.stamina = state.stamina -- movement owns continuous stamina, not needs
local rest_completed = s.rest_warmth_pending ~= nil
if rest_completed then
updated.warmth = math.min(100, updated.warmth + s.rest_warmth_pending)
s.rest_warmth_pending = nil
end
hearth.players[name] = updated
if rest_completed then hearth.player_save(player) end
s.damage = s.damage + danger + (env.smoke and step or 0)
if s.damage >= 4 then
player:set_hp(player:get_hp() - 1, {type = "set_hp", from = "mod"}); s.damage = 0
end
hearth.update_hud(player, env)
s.save = s.save + step
if s.save >= 10 then hearth.player_save(player); s.save = 0 end
end
end
end
end)
-- Tiny client-local rain effect, not thousands of entities or a global ABM.
C.register_globalstep((function()
local timer = 0
return function(dt)
timer = timer + dt; if timer < 3 then return end; timer = 0
for _, p in ipairs(C.get_connected_players()) do
local density = hearth.weather == "rain" and 0.65 or 0.25
p:set_clouds({density = density})
if hearth.weather == "rain" then
local pos = p:get_pos()
if not hearth.environment(pos).roof then
C.add_particlespawner({amount = 80, time = 3,
minpos = vector.offset(pos, -7, 7, -7), maxpos = vector.offset(pos, 7, 10, 7),
minvel = {x = -1, y = -12, z = 0}, maxvel = {x = -1, y = -10, z = 0},
minexptime = 0.5, maxexptime = 0.8, minsize = 0.4, maxsize = 0.7,
collisiondetection = true, collision_removal = true,
texture = hearth.texture("#a4c4d4"), playername = p:get_player_name()})
end
end
end
end
end)())