Files
last-hearth/mods/hearth/runtime.lua
T
2026-11-01 19:35:46 -05:00

222 lines
9.6 KiB
Lua
Executable File

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