222 lines
9.6 KiB
Lua
Executable File
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)())
|