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