Lots of content
This commit is contained in:
+124
-179
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user