local C = core local esc = C.formspec_escape local bg, text, dim, gold = "#1e1e2e", "#cdd6f4", "#a6adc8", "#e5c890" local pages = { {"First light", "YOUR FIRST HEARTH", "Start with what the valley gives you.", "Collect loose flint and dry sticks from the ground. Gather berries for food and leaves for extra sticks.\n\nOpen your inventory with I. Make a flint axe, then fell timber. Split logs into planks; craft walls and a thatch roof.\n\nA roof keeps the rain off. Walls stop exposure. A fueled hearth dries and warms you. Leave a small opening: enclosed fires need ventilation.\n\nThere is no quest timer. Your first success is a home that works."}, {"Craft & build", "WORK WITH YOUR HANDS", "All recipes use the 3 x 3 inventory grid.", "AXE: two flints across the top; flint + stick below; a stick beneath the handle.\nKNIFE: two flints above one stick (right column).\nSPEAR: flint above two sticks.\nCUP: flint at top-left and top-right, flint at center.\nHEARTH: eight flints around an empty center.\n\nOne log -> four planks. One plank -> four sticks. Four planks in a square -> four walls.\nROOF: three reeds across the top, two sticks at the ends of the next row.\n\nWalls and roofs need support within two horizontal blocks. Columns may be up to 16 blocks high. Existing unsupported blocks do not collapse in this prototype."}, {"Fire & food", "A FIRE WORTH KEEPING", "Gather. Prepare. Eat. Recover.", "Place your stone hearth. Right-click it with sticks or logs to add fuel. Each stick burns for 45 real seconds; a log for three minutes.\n\nCraft two berries into forage. Right-click a fueled hearth with forage to cook; left-click with cooked food to eat. Berries can also be eaten raw.\n\nUse the stone cup on river water to fill it. Right-click a fueled hearth with untreated water to boil it. Left-click to drink boiled water.\n\nCooking is a fuel-consuming instant prototype, not a realistic timed process. No spoilage, farming or pottery yet."}, {"Read the world", "SHELTER IS A SYSTEM", "The HUD tells you why you feel the way you do.", "A covered roof plus nearby walls reduces exposure. Wetness rises in rain and falls under cover; a hearth helps you dry. Check the shelter indicator as you build.\n\nA very enclosed fire carries a smoke warning and causes harm: provide ventilation. This is a bounded enclosure approximation, not smoke simulation.\n\nAux1 (usually E) sprints. Right-click while holding a spear to brace against the practice opponent; bracing costs stamina and reduces walking speed.\n\nApproach the practice target near arrival. It retaliates only after you strike it. Step outside its short reach to recover. PvP is disabled."}, {"Your homestead", "MAKE A PLACE YOURS", "A community without compulsory chores.", "Use /claim at your homestead to protect a 25 x 25 area, extending 24 blocks up/down. One claim per person. /claim remove releases it. Stand at least 26 blocks from another claim.\n\nYour inventory and needs persist between sessions. Logged-out characters do not starve or freeze. Fuel continues to expire in real time.\n\nDeath keeps your inventory and returns you to arrival, with needs partially restored. This forgiving prototype policy will be revisited after playtests.\n\nThis is not yet a hardened public server: moderation, shared claim membership and all grief scenarios need further testing."}, {"Clock & location", "ONE WORLD. ONE CLOCK.", "A real calendar, not a daily login requirement.", "Local play asks you to choose approximate coordinates. No location service, IP lookup or external request is used. Use /location to change them. Coordinates stay in local world storage.\n\nA community server must set its shared latitude and longitude in configuration. Remote players cannot override them. The world follows the host's real UTC clock.\n\nSun height and day length follow latitude, longitude and date. The engine's fixed sky orbit is approximated; it is not an exact astronomical renderer. The HUD uses UTC and local solar time, not civil timezone labels.\n\nClimate and rain are synthetic. Calendar-season labels use meteorological months; southern latitudes reverse them."}, } function hearth.guide(player, page) page = math.max(1, math.min(#pages, page or 1)) hearth.sessions[player:get_player_name()].page = page local p = pages[page] local f = {"formspec_version[6]size[14,10]bgcolor[", bg, ";true]box[0,0;14,10;#1e1e2eff]", "style_type[button;border=false;bgcolor=#313244;textcolor=", text, "]", "box[0,0;3.3,10;#181825]label[0.45,0.65;", esc("eom.dev / SEASON VI"), "]", "label[0.45,1.3;", esc("THE LAST HEARTH"), "]"} for i, pg in ipairs(pages) do f[#f + 1] = "button[0.35," .. (2 + i * 0.72) .. ";2.6,0.58;page_" .. i .. ";" .. esc(pg[1]) .. "]" end f[#f + 1] = "label[3.85,0.7;" .. esc("FIELD NOTES / " .. string.format("%02d", page)) .. "]" f[#f + 1] = "style[title;textcolor=" .. gold .. "]label[3.85,1.35;" .. esc(p[2]) .. "]" f[#f + 1] = "label[3.85,2.0;" .. esc(p[3]) .. "]" f[#f + 1] = "textarea[3.8,2.7;9.65,5.6;;;" .. esc(p[4]) .. "]" f[#f + 1] = "button_exit[10.5,8.85;2.8,0.65;close;Back to the valley]" C.show_formspec(player:get_player_name(), "hearth:guide", table.concat(f)) end function hearth.inventory(player) player:set_inventory_formspec("formspec_version[6]size[12.4,11.3]bgcolor[" .. bg .. ";true]box[0,0;12.4,11.3;#1e1e2eff]" .. "label[0.5,0.45;eom.dev / THE LAST HEARTH]label[0.5,1.05;Prepare something worth carrying.]" .. "listcolors[#313244;#45475a;#cdd6f4;#181825;#cdd6f4]" .. "list[current_player;craft;0.6,1.8;3,3;]list[current_player;craftpreview;4.8,2.9;1,1;]" .. "label[4.9,2.2;RESULT]label[6.5,1.8;GATHER / BUILD / ENDURE]" .. "textarea[6.5,2.4;5.4,2.6;;;Collect flint and sticks first.\nA roof keeps rain off.\nA hearth makes shelter a home.]" .. "button[7.1,4.35;3.8,0.6;field_guide;Open field notes]" .. "list[current_player;main;0.6,5.7;10,4;]listring[current_player;main]listring[current_player;craft]") end function hearth.location_form(player, error_text) C.show_formspec(player:get_player_name(), "hearth:location", "formspec_version[6]size[10.8,7.3]bgcolor[" .. bg .. ";true]box[0,0;10.8,7.3;#1e1e2eff]" .. "label[0.65,0.6;THE LAST HEARTH / SET YOUR WORLD'S LOCATION]" .. "textarea[0.65,1.2;9.5,1.7;;;" .. esc("Choose approximate coordinates near you. No location is collected automatically.\nThey stay in this world; no internet lookup is performed.\nExample only: 45 latitude, 0 longitude.") .. "]" .. "field[0.65,3.2;4.4,0.8;latitude;Latitude (-90 to 90);]" .. "field[5.6,3.2;4.4,0.8;longitude;Longitude (-180 to 180);]" .. "label[0.65,4.5;" .. esc(error_text or "No location chosen: neutral equatorial preview until configured.") .. "]" .. "button[0.65,5.3;4.4,0.75;save_location;Use this location]" .. "button_exit[5.6,5.3;4.4,0.75;later;Choose later]") end C.register_on_player_receive_fields(function(player, form, fields) if form == "hearth:guide" then for i = 1, #pages do if fields["page_" .. i] then hearth.guide(player, i); return true end end elseif form == "hearth:location" and fields.save_location then if not C.is_singleplayer() then return true end local lat, lon = fields.latitude or "", fields.longitude or "" if #lat > 16 or #lon > 16 or not hearth.model.location(lat, lon) then hearth.location_form(player, "Enter valid finite coordinates within the ranges above.") else hearth.set_location(tonumber(lat), tonumber(lon)) hearth.guide(player) end return true elseif form == "" and fields.field_guide then hearth.guide(player); return true end end) C.register_chatcommand("guide", {description = "Open the Last Hearth field notes", func = function(name) local p = C.get_player_by_name(name); if p then hearth.guide(p) end; return true end}) C.register_chatcommand("location", {description = "Choose local-world reference coordinates", func = function(name) if not C.is_singleplayer() then return false, "Server coordinates are selected by its administrator." end local p = C.get_player_by_name(name); if p then hearth.location_form(p) end; return true end}) function hearth.make_hud(player) local s = hearth.sessions[player:get_player_name()] s.hud = {} s.hud.title = player:hud_add({type = "text", position = {x = 0.025, y = 0.04}, alignment = {x = 1, y = 0}, text = "THE LAST HEARTH / eom.dev", number = 0xe5c890, size = {x = 1, y = 1}}) s.hud.clock = player:hud_add({type = "text", position = {x = 0.025, y = 0.085}, alignment = {x = 1, y = 0}, text = "", number = 0xcdd6f4, size = {x = 1, y = 1}}) s.hud.needs = player:hud_add({type = "text", position = {x = 0.025, y = 0.14}, alignment = {x = 1, y = 0}, text = "", number = 0xcdd6f4, size = {x = 1, y = 1}}) s.hud.context = player:hud_add({type = "text", position = {x = 0.5, y = 0.77}, alignment = {x = 0, y = 0}, text = "", number = 0xe5c890, size = {x = 1, y = 1}}) s.hud.help = player:hud_add({type = "text", position = {x = 0.975, y = 0.04}, alignment = {x = -1, y = 0}, text = "I Craft & field notes | E Sprint | /claim Homestead", number = 0xa6adc8, size = {x = 1, y = 1}}) player:hud_set_flags({minimap = false}) player:hud_set_hotbar_itemcount(10) end function hearth.update_hud(player, env) local name = player:get_player_name() local s, state = hearth.sessions[name], hearth.players[name] if not s or not s.hud then return end local clock = hearth.clock local hour = math.floor(clock.hour) player:hud_change(s.hud.clock, "text", clock.season .. " / " .. clock.date .. string.format(" / Solar %02d:%02d / %d C%s", hour, math.floor(clock.hour % 1 * 60), math.floor(env.temperature), hearth.has_location and "" or " [LOCATION NOT SET]")) local function bar(n) return string.rep("|", math.floor(n / 10)) .. string.rep(".", 10 - math.floor(n / 10)) end player:hud_change(s.hud.needs, "text", "WARMTH " .. bar(state.warmth) .. "\nWATER " .. bar(state.water) .. "\nFOOD " .. bar(state.food) .. "\nSTAMINA " .. bar(state.stamina)) local context = env.shelter >= 0.6 and "SHELTERED" or (env.roof and "ROOFED / add walls" or "EXPOSED / find cover") context = context .. (env.rain and " | RAIN" or " | CLEAR") if state.wet > 25 then context = context .. " | WET CLOTHING" end if env.heat > 0 then context = context .. " | HEARTH WARMTH" end if env.smoke then context = "SMOKE / open a wall or roof vent" end if state.warmth < 20 then context = context .. " | COLD: shelter and fire needed" end player:hud_change(s.hud.context, "text", context) player:hud_change(s.hud.context, "number", (env.smoke or state.warmth < 20) and 0xf38ba8 or 0xe5c890) end