Lots of content

This commit is contained in:
2026-11-03 12:41:09 -05:00
parent 6ebe95b208
commit d34388bf2c
126 changed files with 28076 additions and 1027 deletions
+124 -179
View File
@@ -1,177 +1,110 @@
local C, M = core, hearth.model
local lat_setting, lon_setting = C.settings:get("hearth_latitude"), C.settings:get("hearth_longitude")
local configured = M.location(lat_setting, lon_setting)
local location_data = hearth.storage:get_string("location")
local stored = location_data ~= "" and C.parse_json(location_data) or nil
local selected = configured or (C.is_singleplayer() and stored and M.location(stored.latitude, stored.longitude))
if not C.is_singleplayer() and not configured then
error("Last Hearth server requires explicit hearth_latitude and hearth_longitude settings.")
end
hearth.has_location = selected ~= nil
local location = selected or {latitude = 0, longitude = 0}
function hearth.set_location(lat, lon)
assert(C.is_singleplayer(), "Only local worlds can select location in game")
local value = assert(M.location(lat, lon), "Invalid reference coordinates")
location, hearth.has_location = value, true
hearth.storage:set_string("location", C.write_json(value))
end
local function timestamp()
-- Date fixtures are only accepted for local play, never a public server.
if C.is_singleplayer() then
local fixture = tonumber(C.settings:get("hearth_preview_date"))
if fixture and fixture >= 946684800 and fixture <= 4102444800 then return fixture end
end
return os.time()
end
local function clock_update()
local now = timestamp()
hearth.clock = M.solar(now, location.latitude, location.longitude)
hearth.weather = M.weather(now, location.latitude, location.longitude)
if C.is_singleplayer() then
local weather = C.settings:get("hearth_preview_weather")
if weather == "rain" or weather == "clear" then hearth.weather = weather end
end
C.set_timeofday(hearth.clock.timeofday)
end
C.after(0, clock_update)
local function solid(pos)
local n = C.get_node_or_nil(pos)
if not n then return false end
local def = C.registered_nodes[n.name]
return def and def.walkable ~= false and not def.buildable_to
and C.get_item_group(n.name, "leaves") == 0
end
function hearth.environment(pos)
local head = vector.round({x = pos.x, y = pos.y + 1, z = pos.z})
local roof = false
for y = 1, 5 do if solid(vector.offset(head, 0, y, 0)) then roof = true; break end end
local walls = 0
for _, d in ipairs({{1, 0}, {-1, 0}, {0, 1}, {0, -1}}) do
for r = 1, 3 do
if solid(vector.offset(head, d[1] * r, 0, d[2] * r)) then walls = walls + 1; break end
end
end
local shelter = roof and (0.25 + walls * 0.1875) or 0
local fire = C.find_node_near(pos, 4, {"hearth:fire_lit"})
local heat = 0
if fire then
local deadline = tonumber(C.get_meta(fire):get_string("fuel_until")) or 0
if M.fuel_remaining(deadline, os.time()) > 0 then
-- A wall between the hearth and player blocks its warmth.
local from = vector.offset(fire, 0, 0.6, 0)
if C.line_of_sight(from, head, 0.5) then heat = 1 - math.min(0.8, vector.distance(pos, fire) / 5) end
end
end
return {roof = roof, shelter = shelter, heat = heat, smoke = heat > 0 and roof and walls == 4,
rain = hearth.weather == "rain", temperature = hearth.clock.temperature - math.max(0, pos.y) * 0.0065}
end
local defaults = {food = 85, water = 85, warmth = 80, wet = 0, stamina = 100}
local function save(player)
local s = hearth.players[player:get_player_name()]
if s then player:get_meta():set_string("hearth:needs", C.write_json(s)) end
end
local spawn_data = hearth.storage:get_string("spawn")
local spawn = spawn_data ~= "" and C.parse_json(spawn_data) or nil
local spawn_pending = false
local waiting = {}
local function arrive(player)
if spawn then
player:set_pos(spawn)
player:set_velocity({x = 0, y = 0, z = 0})
end
end
local function locate_spawn(player)
if spawn then arrive(player); return end
waiting[player:get_player_name()] = true
if spawn_pending then return end
spawn_pending = true
C.emerge_area({x = -48, y = -16, z = -48}, {x = 48, y = 96, z = 48}, function(_, _, left)
if left ~= 0 then return end
local best, best_score
for x = -44, 44, 4 do for z = -44, 44, 4 do
for y = 78, 3, -1 do
local n = C.get_node({x = x, y = y, z = z})
if n.name == "hearth:grass" then
local p = {x = x, y = y + 1, z = z}
if C.get_node(p).name == "air" and C.get_node(vector.offset(p, 0, 1, 0)).name == "air" then
local score = math.abs(x) + math.abs(z) + y * 0.2
if not best_score or score < best_score then best, best_score = p, score end
end
break
end
end
end end
if not best then
C.log("error", "[hearth] No safe arrival in initial area; select a different world seed.")
for name in pairs(waiting) do C.kick_player(name, "No safe spawn found. Please choose another world seed.") end
waiting, spawn_pending = {}, false
return
end
spawn = best
hearth.storage:set_string("spawn", C.write_json(spawn))
for name in pairs(waiting) do local p = C.get_player_by_name(name); if p then arrive(p) end end
waiting = {}
end)
end
C.register_on_joinplayer(function(player)
local name, meta = player:get_player_name(), player:get_meta()
local saved_data = meta:get_string("hearth:needs")
local saved = saved_data ~= "" and C.parse_json(saved_data) or {}
local state = {}
for key, value in pairs(defaults) do state[key] = M.clamp(tonumber(saved[key]) or value, 0, 100) end
hearth.players[name], hearth.sessions[name] = state, {page = 1, damage = 0, save = 0, ready = false}
local inv = player:get_inventory()
inv:set_size("main", 40); inv:set_size("craft", 9); inv:set_width("craft", 3)
inv:set_size("craftpreview", 1); inv:set_size("craftresult", 1)
player:set_properties({visual = "mesh", mesh = "hearth_person.obj", textures = {hearth.texture("#947853")},
visual_size = {x = 1, y = 1}, collisionbox = {-0.3, 0, -0.3, 0.3, 1.75, 0.3},
stepheight = 0.6, eye_height = 1.55, hp_max = 20})
player:set_armor_groups({fleshy = 100})
hearth.inventory(player); hearth.make_hud(player)
player:set_clouds({density = 0.35, color = "#bec4c8e0", height = 140, speed = {x = 2, z = -1}})
if meta:get_int("hearth:arrived") == 0 then
meta:set_int("hearth:arrived", 1)
locate_spawn(player)
end
C.after(1, function()
local p = C.get_player_by_name(name)
if not p then return end
if not hearth.has_location and C.is_singleplayer() then hearth.location_form(p)
else hearth.guide(p) end
end)
end)
C.register_on_leaveplayer(function(player)
save(player)
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()
hearth.players[name], hearth.sessions[name], waiting[name] = nil, nil, nil
end)
C.register_on_shutdown(function() for _, p in ipairs(C.get_connected_players()) do save(p) end end)
C.register_on_respawnplayer(function(player)
local s = hearth.players[player:get_player_name()]
if s then for k, v in pairs(defaults) do s[k] = v end end
locate_spawn(player)
return true
end)
-- PvP is off regardless of a deployment accidentally changing enable_pvp.
C.register_on_punchplayer(function() return true end)
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 clock_update(); clock_elapsed = 0 end
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 name = player:get_player_name()
local s, state = hearth.sessions[name], hearth.players[name]
if s then
local controls = player:get_player_control()
s.sprinting = controls.aux1 and controls.up and not controls.sneak and state.stamina > 8
s.blocking = player:get_wielded_item():get_name() == "hearth:spear" and controls.RMB
local speed = s.blocking and 0.6 or (s.sprinting and 1.45 or 1)
if speed ~= s.speed then player:set_physics_override({speed = speed, jump = 1, gravity = 1}); s.speed = speed end
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
@@ -179,20 +112,32 @@ C.register_globalstep(function(dt)
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 not waiting[name] and player:get_hp() > 0 then
local env = hearth.environment(pos)
env.sprinting = s.sprinting
local updated, danger = M.needs(state, env, step)
if env.heat > 0 then updated.wet = math.max(0, updated.wet - env.heat * step) end
hearth.players[name] = updated
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
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
if pos.y < -100 then arrive(player) end
hearth.update_hud(player, env)
s.save = s.save + step
if s.save >= 10 then save(player); s.save = 0 end
end
end
end)