167 lines
7.6 KiB
Lua
Executable File
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)())
|