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