Ruined
This commit is contained in:
+269
-25
@@ -1,5 +1,10 @@
|
||||
local C = core
|
||||
local esc = C.formspec_escape
|
||||
local function tooltip(name,text)
|
||||
-- Luanti 5.18 requires both tooltip colors when the optional color slots are
|
||||
-- parsed. Supplying the Mocha pair also keeps high-density creator menus legible.
|
||||
return "tooltip[" .. esc(name) .. ";" .. esc(text) .. ";#181825;#cdd6f4]"
|
||||
end
|
||||
-- eom.dev Mocha anchors: shared by every journal surface, not terrain tints.
|
||||
local ink = {base="#1e1e2e", mantle="#181825", raised="#292940", border="#3c3d56",
|
||||
text="#cdd6f4", muted="#a6adc8", mauve="#cba6f7", teal="#94e2d5", yellow="#f9e2af", danger="#f38ba8"}
|
||||
@@ -50,11 +55,11 @@ local pages = {
|
||||
{"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. Use /claim show for temporary boundary markers. Share with /claim share <player> resident (build and household use) or /claim share <player> storekeeper (storage only). /claim unshare <player> revokes access; /claim list shows members; /claim remove releases the claim and grants.\n\nPlace homestead storage inside a claim. Its inventory stays private to the owner, residents and storekeepers; non-empty storage cannot be removed. Workstations and construction require resident permissions.\n\nUse /sethome in your own safe claim; /home rechecks that position before moving you and safely falls back to the world arrival if the home is blocked or hazardous. Your inventory, food, water and wetness persist through death.\n\nLogged-out player needs pause. Hearth processes and fuel use real elapsed time; completed output remains stored until claimed. Food does not spoil in this slice. This remains a prototype, not a hardened public service."},
|
||||
{"Living ecosystems", "NOTHING GROWS ALONE", "Watch a meadow change; there is no quest to complete.",
|
||||
"An original succession model inspired by Dynamic Life connects light, moisture, plants and animals. The botanical catalogue has 80 named species: eight categories of ten each. Use the Botanical guide button or /botany [search] for common and scientific names, habitat, seasons, toxicity labels and roles. These are game-only notes, not a real-world forage guide.\n\nBirch pioneers open gaps; oak favors meadow/forest soils. Evergreen pine and spruce keep winter shade; deciduous oak, birch, willow, alder, beech, aspen, rowan and hazel thin in Winter. Willow favors wet alluvium/peat. Nitrogen-fixer policy can add at most two nutrient units per support across replacements/restarts while reserves are low. The legacy alder terrain profile alone is not a separate fertilizer process. Litter, fungi, deadwood and dung recycle finite nutrients. Fallen logs are ordinary sideways cubes: an axe yields one log, not hand-gathered sticks.\n\nSpring/Summer clover and flowers support pollinators; spring bluebells and autumn heather have seasonal windows. Wet sedge/bulrush, shade moss/ferns, rock lichen and nutrient-needing nettle have distinct niches. The survey counts supporting plants, not insects. Hares and deer graze natural cover; foxes pursue hares. Use /ecosystem for actual ground, light, moisture, canopy, nutrient reserve and active wildlife totals. Moisture is a bounded habitat estimate, not fully simulated hydrology, erosion or flooding.\n\nRoles are conditional policies or descriptors, not effects guaranteed for every species: parasitism and allelopathy cause no plant damage, and a resin tag is not an extraction recipe. Wild succession advances only around connected players in loaded areas. It pauses across unloading and restart, with no offline catch-up. Claims, placed plants, crops and construction are excluded from autonomous changes. Stages take active sessions, not a daily login. Crops and planted saplings keep separate growth rules."},
|
||||
"An original succession model inspired by Dynamic Life connects light, moisture, plants and animals. The botanical catalogue has 100 named species in eight uneven ecological groups, not eight groups of ten. Use /botany [search] for names, habitat, seasons, toxicity labels and roles; /fauna lists 23 animal identities with sources and lifecycle status. These are game-only notes, not a real-world forage guide.\n\nBirch pioneers open gaps; oak favors meadow/forest soils. Evergreen pine and spruce keep winter shade; deciduous trees including crab apple, wild cherry and field maple thin in Winter. Willow favors wet alluvium/peat. Nitrogen-fixer policy can add at most two nutrient units per support after 90 real calendar days, across replacements/restarts while reserves are low. The legacy alder terrain profile alone is not a separate fertilizer process. Litter, fungi, deadwood and dung recycle finite nutrients. Fallen logs are ordinary sideways cubes: an axe yields one log, not hand-gathered sticks.\n\nSpring/Summer clover and flowers support pollinators; spring bluebells and autumn heather have seasonal windows. Wet sedge/bulrush, shade moss/ferns, rock lichen and nutrient-needing nettle have distinct niches. The plant survey counts supporting plants, not insects; wildlife counts include every active species. Animals use finite browse, nectar, fruit, seeds, litter or modeled prey. Use /ecosystem for observations. Moisture is a bounded habitat estimate, not fully simulated hydrology, erosion or flooding.\n\nRoles are conditional policies or descriptors: parasitism and allelopathy cause no plant damage; a resin tag is not an extraction recipe. Calendar ages include unloaded time. Ecological work advances only in loaded areas with no offline catch-up or missed-event replay. Claims, placed plants, crops and construction are excluded from autonomous changes. Named tree stages use real years to decades, not active sessions; fungi do not accelerate growth. Source-reported fields, policy fallbacks and UNKNOWN are distinct. All incomplete animal profiles disable breeding; initial established wildlife are immigration, not adult births. Unknown age and lifespan endpoints never authorize automatic death."},
|
||||
{"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."},
|
||||
{"Geology & timber", "READ THE REGIONAL MATERIALS", "Ten biomes; shared sky, different ground.",
|
||||
"Temperate meadow and oak woodland favor limestone; birch forest and pine taiga favor granite. Wet forest has peat over slate; floodplain has alluvium over sandstone. Dry scrub has sandstone, alpine meadow has turf/scree over slate, rocky peaks have snow/scree over granite, and riverbank has sand/gravel over sandstone. These are ten terrain communities, not ten independent weather simulations. The climate HUD remains shared; altitude still affects exposure temperature.\n\nQuartz seams occur in granite/slate; copper in basalt/limestone/slate up to Y=32; coal in sandstone/slate/limestone up to Y=70. Deeper beds and basalt intrusions differ from surface rock. Mine with a pick; each seam drops two resource items. Three quartz craft one existing window, without a glass furnace.\n\nOne copper ore + one charcoal craft one copper charge. At a lit hearth it takes 45 real seconds and reserves 30 fuel seconds, yielding one ingot. Three copper ingots across the top row, with two sticks down the center, make one faster, longer-lasting copper pick. Iron remains a separate 60-second smelt and axe route.\n\nOak, birch, pine, willow, alder, beech, aspen, rowan, hazel and spruce logs each burn for 180 seconds, or craft four matching planks. Convert two matching species logs to two canonical logs for log + clay charcoal charges; convert species planks 1:1 to canonical planks for exact-name building recipes. One regional rock crafts four matching bricks; two matching rocks convert to two canonical stones. Put each input in separate crafting slots (shapeless); there is no single-log or single-rock canonical conversion. Fallen logs are sideways full cubes; an axe yields one canonical log, never two hand-gathered sticks. Coal burns 240 seconds; charcoal 90; sticks 45. All fuel is capped at 600 seconds. Coal is geological and nonrenewable, not a renewable fuel loop.\n\nThe broad-relief terrain preset is for fresh worlds only. Existing chunks stay untouched; new biome registrations can change unexplored old-world boundaries. Operators must explicitly choose a new world or accept those seams; never delete map metadata to force a migration."},
|
||||
"Temperate meadow and oak woodland favor limestone; birch forest and pine taiga favor granite. Wet forest has peat over slate; floodplain has alluvium over sandstone. Dry scrub has sandstone, alpine meadow has turf/scree over slate, rocky peaks have snow/scree over granite, and riverbank has sand/gravel over sandstone. These are ten terrain communities, not ten independent weather simulations. The climate HUD remains shared; altitude still affects exposure temperature.\n\nQuartz seams occur in granite/slate; copper in basalt/limestone/slate up to Y=32; coal in sandstone/slate/limestone up to Y=70. Deeper beds and basalt intrusions differ from surface rock. Mine with a pick; each seam drops two resource items. Three quartz craft one existing window, without a glass furnace.\n\nOne copper ore + one charcoal craft one copper charge. At a lit hearth it takes 45 real seconds and reserves 30 fuel seconds, yielding one ingot. Three copper ingots across the top row, with two sticks down the center, make one faster, longer-lasting copper pick. Iron remains a separate 60-second smelt and axe route.\n\nOak, birch, pine, willow, alder, beech, aspen, rowan, hazel, spruce, crab apple, wild cherry and field maple logs each burn for 180 seconds, or craft four matching planks. Convert two matching species logs to two canonical logs for log + clay charcoal charges; convert species planks 1:1 to canonical planks for exact-name building recipes. One regional rock crafts four matching bricks; two matching rocks convert to two canonical stones. Put each input in separate crafting slots (shapeless); there is no single-log or single-rock canonical conversion. Fallen logs are sideways full cubes; an axe yields one canonical log, never two hand-gathered sticks. Coal burns 240 seconds; charcoal 90; sticks 45. All fuel is capped at 600 seconds. Coal is geological and nonrenewable, not a renewable fuel loop.\n\nThe broad-relief terrain preset is for fresh worlds only. Existing chunks stay untouched; new biome registrations can change unexplored old-world boundaries. Operators must explicitly choose a new world or accept those seams; never delete map metadata to force a migration."},
|
||||
}
|
||||
local botanical_groups = {
|
||||
{"trees", "Trees"}, {"shrubs", "Shrubs"}, {"grasses", "Grasses, sedges & rushes"},
|
||||
@@ -68,7 +73,7 @@ end
|
||||
local function botanical_summary()
|
||||
local records, lines = catalogue(), {}
|
||||
if #records == 0 then return "Botanical catalogue unavailable in this runtime. Use /botany when installed." end
|
||||
lines[1] = "BOTANICAL CATALOGUE / " .. #records .. " named species; eight categories of ten in the full catalogue.\nUse /botany [search] for scientific names, habitat windows, seasons and ecological roles."
|
||||
lines[1] = "BOTANICAL CATALOGUE / " .. #records .. " named species; eight ecological groups of uneven sizes.\nUse /botany [search] for scientific names, habitat windows, seasons and ecological roles; /fauna for 23 animal identities and lifecycle evidence."
|
||||
for _, group in ipairs(botanical_groups) do
|
||||
local names = {}
|
||||
for _, r in ipairs(records) do
|
||||
@@ -99,11 +104,12 @@ function hearth.guide(player, page)
|
||||
local notes = p[4] .. (page == 6 and "\n\n" .. botanical_summary() or "")
|
||||
f[#f + 1] = "textarea[3.8,2.65;9.6,5.6;;;" .. esc(notes) .. "]"
|
||||
f[#f + 1] = "button[3.85,8.15;2.3,0.55;inspect_target;Inspect target]"
|
||||
f[#f + 1] = "button[6.3,8.15;2.1,0.55;fauna_index;Fauna guide]"
|
||||
f[#f + 1] = "button[3.85,8.85;2.3,0.65;ecosystem_survey;Ecosystem survey]"
|
||||
.. "tooltip[ecosystem_survey;Read a loaded habitat patch and global active wildlife. No world changes.;" .. ink.mantle .. ";" .. ink.text .. "]"
|
||||
.. tooltip("ecosystem_survey","Read a loaded habitat patch and global active wildlife. No world changes.")
|
||||
f[#f + 1] = "button[6.3,8.85;2.1,0.65;botany_index;Botanical guide]"
|
||||
f[#f + 1] = "button[8.55,8.85;2.1,0.65;recipe_index;Recipe index]"
|
||||
.. "tooltip[recipe_index;Search ingredients, results and real-time processes.;" .. ink.mantle .. ";" .. ink.text .. "]"
|
||||
.. tooltip("recipe_index","Search ingredients, results and real-time processes.")
|
||||
f[#f + 1] = "button_exit[10.8,8.85;2.5,0.65;close;Back to the valley]"
|
||||
C.show_formspec(player:get_player_name(), "hearth:guide", table.concat(f))
|
||||
end
|
||||
@@ -112,6 +118,7 @@ local recipe_outputs = {"hearth:knife", "hearth:axe", "hearth:pick", "hearth:spe
|
||||
"hearth:copper_charge", "hearth:copper_pick", "hearth:log", "hearth:stone",
|
||||
"hearth:oak_planks", "hearth:birch_planks", "hearth:pine_planks", "hearth:willow_planks", "hearth:alder_planks",
|
||||
"hearth:beech_planks", "hearth:aspen_planks", "hearth:rowan_planks", "hearth:hazel_planks", "hearth:spruce_planks",
|
||||
"hearth:crab_apple_planks", "hearth:wild_cherry_planks", "hearth:field_maple_planks",
|
||||
"hearth:granite_bricks", "hearth:basalt_bricks", "hearth:limestone_bricks", "hearth:slate_bricks", "hearth:sandstone_bricks",
|
||||
"hearth:firestarter", "hearth:vessel", "hearth:torch", "hearth:fire", "hearth:wall",
|
||||
"hearth:roof", "hearth:foundation", "hearth:frame", "hearth:stairs", "hearth:door",
|
||||
@@ -127,8 +134,8 @@ local process_notes = {
|
||||
{"Smelting", "One iron ore/charcoal charge + lit hearth: 60 real seconds; 30 fuel seconds reserved at start. Copper uses its separate 45-second charge."},
|
||||
{"Copper smelting", "One hearth:copper_charge (one hearth:copper_ore + one hearth:charcoal) + lit hearth: 45 real seconds; 30 fuel seconds reserved at start; one hearth:copper_ingot output. Requires at least 75 fuel seconds to queue. Collect empty-handed; a full inventory leaves the ingot waiting. Copper pick: three ingots across the top and two central sticks."},
|
||||
{"Regional geology", "Quartz in granite/slate: three hearth:quartz craft one window; the reed/plank route remains. Copper ore in basalt/limestone/slate; coal in sandstone/slate/limestone. Each seam drops two items. One granite, basalt, limestone, slate or sandstone crafts four matching bricks. Convert two matching rocks to two canonical stones in separate crafting slots (shapeless); no single-rock canonical conversion."},
|
||||
{"Species timber", "One oak, birch, pine, willow, alder, beech, aspen, rowan, hazel or spruce log crafts four matching planks. Convert two matching species logs in separate crafting slots (shapeless) to two canonical hearth:log; no single-log canonical conversion. Species planks convert 1:1 to canonical hearth:planks. Charcoal charges accept only canonical log + clay: convert species logs in matching pairs first, not four planks back into logs."},
|
||||
{"Finite nitrogen", "Nitrogen-fixer policy, including clover and alder in the botanical runtime, can add at most two nutrient units per support across replacements and restarts while the reserve is low. The legacy alder terrain profile alone is not a fertilizer process. Nutrient reserve is bounded at eight; litter, deadwood, fungi and dung recycle finite material."},
|
||||
{"Species timber", "One oak, birch, pine, willow, alder, beech, aspen, rowan, hazel, spruce, crab_apple, wild_cherry or field_maple log crafts four matching planks. Convert two matching species logs in separate crafting slots (shapeless) to two canonical hearth:log; no single-log canonical conversion. Species planks convert 1:1 to canonical hearth:planks. Charcoal charges accept only canonical log + clay: convert species logs in matching pairs first, not four planks back into logs."},
|
||||
{"Finite nitrogen", "After 90 real calendar days, nitrogen-fixer policy, including clover and alder in the botanical runtime, can add at most two nutrient units per support across replacements and restarts while the reserve is low and conditions are warm. This cadence is game policy, not a measured species rate. The legacy alder terrain profile alone is not a fertilizer process. Nutrient reserve is bounded at eight; litter, deadwood, fungi and dung recycle finite material."},
|
||||
{"Fallen logs", "Ordinary full-cube fallen logs lie sideways. Use an axe to harvest one canonical hearth:log; hands do not yield sticks. They also support decomposer habitat. Player-placed and claimed material is excluded from autonomous succession."},
|
||||
{"Pottery firing", "One shaped clay-jar charge + lit hearth: 120 real seconds; 30 fuel seconds reserved at start."},
|
||||
{"Food drying", "One cooked-food/reed charge + lit hearth: 300 real seconds; 30 fuel seconds reserved at start."},
|
||||
@@ -136,11 +143,11 @@ local process_notes = {
|
||||
{"Fish preparation", "Roast one raw fish at a lit hearth for 8 real seconds; reserve 20 fuel seconds. Combine cooked fish with reeds into a drying charge, then dry for 300 seconds with 30 fuel seconds reserved. Dried fish is shelf-stable."},
|
||||
{"Spring fish run", "A trap finishing during spring returns two fish; other seasons return one. The catch is determined by its scheduled deadline, even if you are away."},
|
||||
{"Light a hearth", "Fuel first, then use a flint firestarter. Each starter has 20 successful ignitions."},
|
||||
{"Fire fuel", "Each stick adds 45 real seconds; canonical, oak, birch, pine, willow, alder, beech, aspen, rowan, hazel and spruce logs each add 180; charcoal adds 90; coal adds 240 seconds. Stored reserve is capped at 600; a fuel item that would exceed it is not consumed. Planks and buildings are not direct fuel. Coal is geological and nonrenewable; it cannot be crafted or grown."},
|
||||
{"Fire fuel", "Each stick adds 45 real seconds; canonical, oak, birch, pine, willow, alder, beech, aspen, rowan, hazel, spruce, crab_apple, wild_cherry and field_maple logs each add 180; charcoal adds 90; coal adds 240 seconds. Stored reserve is capped at 600; a fuel item that would exceed it is not consumed. Planks and buildings are not direct fuel. Coal is geological and nonrenewable; it cannot be crafted or grown."},
|
||||
{"Charcoal", "Combine one log with river clay, then burn the charge at a lit hearth for 60 seconds with 30 fuel seconds reserved. The resulting charcoal stores 90 seconds; total fuel recovered never exceeds the log's 180 seconds."},
|
||||
{"Bedroll rest", "Craft from three planks and three reeds. Right-click beneath a roof; remain still and safe for five seconds."},
|
||||
{"Crop growth", "Plant grain seed on grass or soil. Growth samples historical local solar climate; cold weather pauses it. Harvest ripe crops for grain, then reserve one kernel to replant."},
|
||||
{"Renewal", "Berry bushes return after one real day. A planted sapling grows after three days when its bounded tree volume is clear. No resource can regrow above map limits or through claims."},
|
||||
{"Renewal", "Berry crops rest one mean real year (365.2425 days), then require their fruit season; no 24-hour refill or missed-crop replay. Human and animal botanical fruit share one readiness deadline. Pollination adds at most one berry to an existing crop once. A planted generic sapling requires one real year and clear bounded tree volume; named trees use species lifecycle endpoints, often years to decades. Unknown timing remains labelled policy or UNKNOWN, never invented biology. No resource grows through claims or map limits."},
|
||||
{"Preservation", "Combine cooked food with reeds into a drying charge, then process it at a lit hearth. Dried rations are shelf-stable; general food spoilage is currently off."},
|
||||
{"Spring fish", "Spring catches can produce two fish per baited trap. Search this index for fish preparation; it is seasonal but not time-critical."},
|
||||
{"Pottery", "Shape six river clay into an unfired jar, then fire it at a lit hearth. Use the fired jar with planks to make a larger pantry."},
|
||||
@@ -207,8 +214,8 @@ function hearth.recipes(player, query, page)
|
||||
.. "box[0.65,2.2;9.3,0.8;" .. ink.mantle .. "]"
|
||||
.. "field[0.7,2.25;9.2,0.7;query;Result or ingredient (plain text);" .. esc(query) .. "]field_close_on_enter[query;false]"
|
||||
.. "button[10.25,2.25;3.1,0.7;search;Search]"
|
||||
.. "tooltip[query;Up to 48 characters. Search matches literal text, not patterns.;" .. ink.mantle .. ";" .. ink.text .. "]"
|
||||
.. "tooltip[search;Search crafting recipes and real-time hearth processes.;" .. ink.mantle .. ";" .. ink.text .. "]"
|
||||
.. tooltip("query","Up to 48 characters. Search matches literal text, not patterns.")
|
||||
.. tooltip("search","Search crafting recipes and real-time hearth processes.")
|
||||
.. label(0.7,3.3,"Page " .. page .. " / " .. pages_count .. " / " .. #all .. " matches",ink.muted)
|
||||
.. "textarea[0.65,3.7;12.7,4.6;;;" .. esc(table.concat(lines, "\n\n")) .. "]"
|
||||
.. (page > 1 and "button[0.7,8.85;2.8,0.65;previous;Previous]" or "")
|
||||
@@ -284,7 +291,7 @@ function hearth.botanical_guide(player, query, page)
|
||||
session.botany_query, session.botany_page = query, page
|
||||
C.show_formspec(name,"hearth:botany",journal(14,10)
|
||||
.. label(0.65,0.55,"eom.dev / BOTANICAL FIELD GUIDE",ink.mauve)
|
||||
.. label(0.65,1.15,#records .. " named species / eight categories of ten in the full catalogue",ink.yellow)
|
||||
.. label(0.65,1.15,#records .. " named species / eight ecological groups of uneven sizes",ink.yellow)
|
||||
.. divider(0.65,1.65,12.7)
|
||||
.. "field[0.7,1.95;9.2,0.7;query;Species, scientific name, category, habitat or role;" .. esc(query) .. "]field_close_on_enter[query;false]"
|
||||
.. "button[10.25,1.95;3.1,0.7;search;Search]"
|
||||
@@ -292,13 +299,201 @@ function hearth.botanical_guide(player, query, page)
|
||||
.. "textarea[0.65,3.3;12.7,4.35;;;" .. esc(table.concat(lines,"\n\n")) .. "]"
|
||||
.. "textarea[0.65,7.7;12.7,0.55;;;" .. esc("Game-only notes, NOT a real-world forage guide. Unflagged does not mean safe to eat. Roles are conditional, not universal effects.") .. "]"
|
||||
.. (#choices > 0 and "dropdown[0.7,8.3;5,0.45;species_choice;" .. table.concat(choices,",") .. ";1;true]"
|
||||
.. "tooltip[species_choice;Species article]button[6,8.3;3.3,0.45;species_article;Species article]" or "")
|
||||
.. tooltip("species_choice","Species article") .. "button[6,8.3;3.3,0.45;species_article;Species article]" or "")
|
||||
.. "button[10.05,8.3;3.3,0.45;fauna_index;Fauna guide]"
|
||||
.. (page > 1 and "button[0.7,8.85;2.2,0.65;previous;Previous]" or "")
|
||||
.. (page < count and "button[3.05,8.85;2.2,0.65;next;Next]" or "")
|
||||
.. "button[6,8.85;3.3,0.65;ecosystem_survey;Ecosystem survey]"
|
||||
.. "button[10.05,8.85;3.3,0.65;field_notes;Back to field notes]")
|
||||
end
|
||||
|
||||
-- Observational catalogue, independent of the plant-only article reader.
|
||||
local function fauna_observations(player, discovered_id)
|
||||
local F=hearth.fauna
|
||||
if not F or type(F.list)~="function" or type(player.get_meta)~="function" then return nil end
|
||||
local allowed={}; for _,record in ipairs(F.list()) do allowed[record.id]=true end
|
||||
local ok,meta=pcall(function() return player:get_meta() end)
|
||||
if not ok or not meta or type(meta.get_string)~="function" or type(meta.set_string)~="function" then return nil end
|
||||
local key="hearth:fauna_observations_v1"
|
||||
local read_ok,raw=pcall(meta.get_string,meta,key)
|
||||
if not read_ok or type(raw)~="string" or #raw>512 then return nil end
|
||||
local seen,ordered={},{}
|
||||
if raw~="" then
|
||||
for id in raw:gmatch("[^,]+") do
|
||||
if not allowed[id] or seen[id] then return nil end
|
||||
seen[id]=true; ordered[#ordered+1]=id
|
||||
end
|
||||
table.sort(ordered)
|
||||
if table.concat(ordered,",")~=raw then return nil end
|
||||
end
|
||||
local first=discovered_id and not seen[discovered_id] or false
|
||||
if first then
|
||||
seen[discovered_id]=true; ordered[#ordered+1]=discovered_id; table.sort(ordered)
|
||||
local encoded=table.concat(ordered,",")
|
||||
local write_ok=pcall(meta.set_string,meta,key,encoded)
|
||||
local verify_ok,verified=pcall(meta.get_string,meta,key)
|
||||
if not write_ok or not verify_ok or verified~=encoded then return nil end
|
||||
end
|
||||
return seen,first
|
||||
end
|
||||
|
||||
function hearth.inspect_fauna(player,id,snapshot)
|
||||
local ok,name=pcall(function() return player:get_player_name() end)
|
||||
if not ok or type(name)~="string" or #name<1 or #name>64 or not hearth.sessions[name]
|
||||
or type(C.get_player_by_name)~="function" then return false end
|
||||
local online_ok,online=pcall(C.get_player_by_name,name)
|
||||
if not online_ok or online~=player then return false end
|
||||
local F=hearth.fauna
|
||||
if type(id)~="string" or #id>80 or not F or type(F.get)~="function" then return false end
|
||||
local loaded,record=pcall(F.get,id)
|
||||
if not loaded or type(record)~="table" or record.id~=id then return false end
|
||||
local view
|
||||
if snapshot~=nil then
|
||||
if type(snapshot)~="table" or getmetatable(snapshot)~=nil then return false end
|
||||
view={}
|
||||
for _,key in ipairs({"born_at","origin","foodreserve","resident","observation"}) do
|
||||
local value=snapshot[key]
|
||||
if key=="born_at" or key=="foodreserve" then
|
||||
if type(value)~="number" or value~=value or math.abs(value)==math.huge then return false end
|
||||
elseif value~=nil and type(value)~="string" and type(value)~="boolean" then return false end
|
||||
view[key]=value
|
||||
end
|
||||
if view.born_at<0 or view.born_at%1~=0 or view.foodreserve<0 or view.foodreserve>1
|
||||
or type(view.resident)~="boolean"
|
||||
or (view.origin~="established" and view.origin~="born")
|
||||
or (view.observation~="rest" and view.observation~="awake") then return false end
|
||||
end
|
||||
local sightings,first_sighting=fauna_observations(player,view and id or nil)
|
||||
local function safe(value,limit)
|
||||
if type(value)~="string" then return "unknown" end
|
||||
return value:gsub("[%c]"," "):sub(1,limit or 240)
|
||||
end
|
||||
local function safe_words(set)
|
||||
local out={}
|
||||
if type(set)=="table" then
|
||||
for key,enabled in pairs(set) do
|
||||
if enabled and type(key)=="string" and #out<32 then out[#out+1]=safe(key,40):gsub("_"," ") end
|
||||
end
|
||||
end
|
||||
table.sort(out)
|
||||
return #out>0 and table.concat(out,", ") or "not specified"
|
||||
end
|
||||
local profile=record.profile or {}
|
||||
local lines={}
|
||||
local now_ok,now=pcall(os.time)
|
||||
local phase
|
||||
if view and now_ok and type(F.phase)=="function" then
|
||||
local phase_ok,result=pcall(F.phase,id,view.born_at,now)
|
||||
if phase_ok and type(result)=="table" then phase=result end
|
||||
end
|
||||
if phase then
|
||||
lines[#lines+1]=string.format("Lifecycle: %s / approx %.2f model years; %.1f real calendar days",safe(phase.phase,40),phase.age_days/365.2425,phase.age_days)
|
||||
else
|
||||
lines[#lines+1]="Lifecycle age: unavailable for this profile or no individual selected."
|
||||
end
|
||||
if view then
|
||||
local reserve=math.max(0,math.min(1,view.foodreserve))
|
||||
lines[#lines+1]=string.format("Coarse modeled food reserve (policy, not biology): %.0f%%",reserve*100)
|
||||
lines[#lines+1]="Current animation observation: "..(view.observation=="rest" and "resting" or "awake / active")
|
||||
lines[#lines+1]="Residency: "..(view.resident and "persistent registered resident" or "persistent mapblock resident; registry ID unavailable")
|
||||
.." ("..safe(view.origin,24).." origin); individual position is not shown."
|
||||
lines[#lines+1]=sightings and (first_sighting and "Field journal: first recorded encounter with this species."
|
||||
or "Field journal: this species is already recorded.") or "Field journal: unavailable; encounter was not recorded."
|
||||
else
|
||||
lines[#lines+1]="Individual observation: none; this is a species profile, not a local census."
|
||||
lines[#lines+1]="Field journal: browsing this catalogue entry does not count as a wildlife encounter."
|
||||
end
|
||||
lines[#lines+1]="Modeled diet: "..safe_words(record.foods)
|
||||
lines[#lines+1]="Activity / sleep policy: "..safe(record.activity,32).." / "..safe(record.sleep and record.sleep.kind,40)
|
||||
lines[#lines+1]="Winter rest policy: "..(record.winter_dormant and "coarse dormancy; resident remains persistent" or "no special winter dormancy modeled")
|
||||
lines[#lines+1]="Modeled behavior: "..safe(profile.movement or "movement details unavailable",220)
|
||||
lines[#lines+1]="Modeled prey: "..safe_words(record.prey)
|
||||
if profile.reproduction_enabled==true then
|
||||
lines[#lines+1]="Reproduction: enabled only within the coarse model; mature compatible pairs, season, food and safe habitat are still required."
|
||||
else
|
||||
local evidence=profile.evidence and profile.evidence.reproduction_context
|
||||
lines[#lines+1]="Reproduction: disabled / incomplete species-specific lifecycle evidence. "
|
||||
..safe(evidence and evidence.note or "No breeding policy is enabled for this profile.",220)
|
||||
end
|
||||
local status=hearth.wildlife
|
||||
if type(status)=="table" and type(status.status)=="function" then
|
||||
local status_ok,data=pcall(status.status)
|
||||
if status_ok and type(data)=="table" and type(data.counts)=="table" then
|
||||
local n=tonumber(data.counts[id]); if n and n==n and math.abs(n)<math.huge then
|
||||
lines[#lines+1]="Active loaded animals of this species: "..math.max(0,math.floor(n))
|
||||
end
|
||||
end
|
||||
end
|
||||
local source=safe(type(record.source_url)=="string" and record.source_url or "source unavailable",300)
|
||||
local lifecycle_source=safe(type(profile.source_url)=="string" and profile.source_url or "UNKNOWN",300)
|
||||
C.show_formspec(name,"hearth:fauna_detail",journal(14,10)
|
||||
..label(0.65,0.55,"eom.dev / FAUNA FIELD NOTES",ink.mauve)
|
||||
..label(0.65,1.15,safe(record.common,80):upper().." / "..safe(record.latin or "identity unverified",100),ink.yellow)
|
||||
..divider(0.65,1.65,12.7)
|
||||
.."textarea[0.65,2.0;12.7,5.7;;;"..esc(table.concat(lines,"\n\n")).."]"
|
||||
.."textarea[0.65,7.8;12.7,0.8;;;"..esc("Identity source: "..source.."\nLifecycle source: "..lifecycle_source).."]"
|
||||
.."button[6.15,8.85;3.3,0.65;fauna_index;Back to fauna guide]"
|
||||
.."button_exit[10.05,8.85;3.3,0.65;close;Close]" )
|
||||
return true
|
||||
end
|
||||
|
||||
function hearth.fauna_guide(player,query,page)
|
||||
local name=player:get_player_name()
|
||||
local session=hearth.sessions[name]; if not session then return end
|
||||
query=tostring(query or ""):sub(1,48):gsub("[%c]"," ")
|
||||
local F=hearth.fauna
|
||||
local records=F and type(F.list)=="function" and F.list() or {}
|
||||
local observations=fauna_observations(player)
|
||||
local observed_count=0
|
||||
if observations then for _ in pairs(observations) do observed_count=observed_count+1 end end
|
||||
local all,all_ids,needle={},{},query:lower()
|
||||
local function timing(p,field)
|
||||
local r=p and p[field]
|
||||
if type(r)~="table" then return "UNKNOWN" end
|
||||
return tostring(r.min).."-"..tostring(r.max).." real days ["..tostring(r.confidence).."]"
|
||||
end
|
||||
for _,r in ipairs(records) do
|
||||
local p=r.profile
|
||||
local marker=observations and observations[r.id] and " / recorded encounter" or " / not yet encountered"
|
||||
local row=r.id.." / "..r.common.." / "..(r.latin or "UNKNOWN species identity (legacy deer)")..marker
|
||||
.."\nHabitat/movement medium: "..r.medium.."; activity: "..r.activity
|
||||
.."; food roles: "..words(r.foods)
|
||||
.."\nMaturity: "..timing(p,"maturity_days").."; longevity context: "..timing(p,"lifespan_days")
|
||||
.."\nLifecycle evidence: "..(p and p.confidence or "UNKNOWN")
|
||||
.."; automatic breeding "..(p and p.reproduction_enabled==true and "enabled only for sourced profiles" or "disabled (incomplete profile)")
|
||||
.."; no lifespan-endpoint death."
|
||||
.."\nIdentity source: "..r.source_url
|
||||
.."\nLifecycle source: "..(p and p.source_url or "UNKNOWN")
|
||||
if needle=="" or row:lower():find(needle,1,true) then all[#all+1]=row; all_ids[#all_ids+1]=r.id end
|
||||
end
|
||||
local pages_count=math.max(1,math.ceil(#all/5)); page=index(page,pages_count)
|
||||
local lines,choices={},{}
|
||||
session.fauna_ids,session.fauna_selected={},1
|
||||
for i=(page-1)*5+1,math.min(#all,page*5) do
|
||||
lines[#lines+1]=all[i]; session.fauna_ids[#session.fauna_ids+1]=all_ids[i]; choices[#choices+1]=esc(all_ids[i])
|
||||
end
|
||||
if #lines==0 then lines[1]=#records==0 and "Fauna catalogue unavailable in this runtime."
|
||||
or "No animal matches that literal search." end
|
||||
session.fauna_query,session.fauna_page=query,page
|
||||
C.show_formspec(name,"hearth:fauna",journal(14,10)
|
||||
..label(0.65,0.55,"eom.dev / FAUNA FIELD GUIDE",ink.mauve)
|
||||
..label(0.65,1.15,(observations and (observed_count.." / "..#records.." species recorded in your field journal")
|
||||
or "Field journal unavailable / source evidence is not complete biology"),ink.yellow)
|
||||
..divider(0.65,1.65,12.7)
|
||||
.."field[0.7,1.95;9.2,0.7;query;Species, scientific name, food or status;"..esc(query).."]field_close_on_enter[query;false]"
|
||||
.."button[10.25,1.95;3.1,0.7;search;Search]"
|
||||
..label(0.7,2.95,"Page "..page.." / "..pages_count.." / "..#all.." matches",ink.muted)
|
||||
.."textarea[0.65,3.3;12.7,4.0;;;"..esc(table.concat(lines,"\n\n")).."]"
|
||||
.."textarea[0.65,7.4;12.7,0.8;;;"..esc("Real calendar age includes unloaded time; no offline meals, births or event replay. Movement and feeding cadence are conservative game policies, not complete true-life behavior. No web requests or world changes.").."]"
|
||||
..(#choices>0 and "dropdown[0.7,8.25;5.0,0.45;species_choice;"..table.concat(choices,",")..";1;true]"
|
||||
..tooltip("species_choice","Choose one of the species shown on this page.")
|
||||
.."button[6,8.2;3.3,0.55;species_details;Species details]" or "")
|
||||
..(page>1 and "button[0.7,8.85;2.2,0.65;previous;Previous]" or "")
|
||||
..(page<pages_count and "button[3.05,8.85;2.2,0.65;next;Next]" or "")
|
||||
.."button[6,8.85;3.3,0.65;botany_index;Botanical guide]"
|
||||
.."button[10.05,8.85;3.3,0.65;field_notes;Back to field notes]")
|
||||
end
|
||||
|
||||
function hearth.inventory(player)
|
||||
player:set_inventory_formspec(journal(12.4,11.3)
|
||||
.. label(0.6,0.45,"eom.dev / THE LAST HEARTH",ink.mauve)
|
||||
@@ -307,9 +502,9 @@ function hearth.inventory(player)
|
||||
.. label(4.8,2.2,"RESULT",ink.teal) .. label(6.5,1.95,"GATHER / BUILD / BELONG",ink.yellow)
|
||||
.. "textarea[6.5,2.4;5.2,1.7;;;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]"
|
||||
.. "tooltip[field_guide;Read survival notes, recipes and the ecosystem survey.;" .. ink.mantle .. ";" .. ink.text .. "]"
|
||||
.. tooltip("field_guide","Read survival notes, recipes and the ecosystem survey.")
|
||||
.. "button[7.1,3.65;3.8,0.6;character_creator;Character appearance]"
|
||||
.. "tooltip[character_creator;Preview woodland clothing and features. Cosmetic only; Save / Apply to keep changes.]"
|
||||
.. tooltip("character_creator","Preview woodland clothing and features. Cosmetic only; Save / Apply to keep changes.")
|
||||
.. label(0.6,5.25,"SATCHEL / first row is your hotbar",ink.muted)
|
||||
.. "list[current_player;main;0.6,5.7;10,4;]listring[current_player;main]listring[current_player;craft]")
|
||||
end
|
||||
@@ -322,8 +517,8 @@ function hearth.location_form(player, error_text)
|
||||
.. "box[0.6,3.95;4.5,0.8;" .. ink.mantle .. "]box[5.55,3.95;4.5,0.8;" .. ink.mantle .. "]"
|
||||
.. "field[0.65,4;4.4,0.7;latitude;Latitude (-90 to 90);]field_close_on_enter[latitude;false]"
|
||||
.. "field[5.6,4;4.4,0.7;longitude;Longitude (-180 to 180);]field_close_on_enter[longitude;false]"
|
||||
.. "tooltip[latitude;Approximate decimal degrees, at most 16 characters.;" .. ink.mantle .. ";" .. ink.text .. "]"
|
||||
.. "tooltip[longitude;Local world only. Shared servers use administrator-selected coordinates.;" .. ink.mantle .. ";" .. ink.text .. "]"
|
||||
.. tooltip("latitude","Approximate decimal degrees, at most 16 characters.")
|
||||
.. tooltip("longitude","Local world only. Shared servers use administrator-selected coordinates.")
|
||||
.. "textarea[0.65,4.95;9.5,0.75;;;" .. esc(C.colorize(error_text and ink.danger or ink.muted,
|
||||
tostring(error_text or "No location chosen: neutral equatorial preview until configured."):sub(1,180))) .. "]"
|
||||
.. "button[0.65,6;4.4,0.7;save_location;Use this location]"
|
||||
@@ -359,9 +554,27 @@ function hearth.ecosystem(player)
|
||||
.. "\nAquatic niche: " .. (type(h.aquatic) == "string" and h.aquatic:sub(1,32) or "unavailable")
|
||||
.. " Water depth: " .. (h.near_water ~= nil and optional(h.water) or "unavailable")
|
||||
end
|
||||
local fauna = w and string.format("Hares: %d Deer: %d Foxes: %d\nActive total: %d / %d globally Local cap: %d Per-player cap: %d",
|
||||
number(w.hare),number(w.deer),number(w.fox),number(w.total),number(w.global_cap),number(w.local_cap),number(w.player_cap))
|
||||
or "Wildlife observations are not available in this runtime."
|
||||
local fauna = "Wildlife observations are not available in this runtime."
|
||||
if w then
|
||||
local counts,lines=w.counts,{}
|
||||
-- Legacy optional API fallback; production uses every returned species.
|
||||
counts=type(counts)=="table" and counts or {hare=w.hare,deer=w.deer,fox=w.fox}
|
||||
local ids={}; for id in pairs(counts) do if type(id)=="string" then ids[#ids+1]=id end end
|
||||
table.sort(ids)
|
||||
for i=1,math.min(#ids,64) do
|
||||
local id=ids[i]
|
||||
local name=({hare="Hares",deer="Deer",fox="Foxes"})[id] or id:sub(1,64):gsub("_"," ")
|
||||
lines[#lines+1]=name .. ": " .. number(counts[id])
|
||||
end
|
||||
fauna=table.concat(lines," / ") .. string.format("\nActive total: %d / %d globally Local cap: %d Per-player cap: %d",
|
||||
number(w.total),number(w.global_cap),number(w.local_cap),number(w.player_cap))
|
||||
local recruitment=w.recruitment
|
||||
if type(recruitment)=="table" then
|
||||
local reason=type(recruitment.last_reason)=="string" and recruitment.last_reason:sub(1,40):gsub("[%c]"," ") or "unavailable"
|
||||
fauna=fauna..string.format("\nAdmissions: %d / %d attempts Latest outcome: %s",
|
||||
number(recruitment.successes),number(recruitment.attempts),reason)
|
||||
end
|
||||
end
|
||||
C.show_formspec(player:get_player_name(), "hearth:ecosystem", journal(14,10)
|
||||
.. label(0.65,0.55,"eom.dev / LIVING ECOSYSTEMS",ink.mauve)
|
||||
.. label(0.65,1.15,"A MOMENT IN THE MEADOW",ink.yellow) .. divider(0.65,1.65,12.7)
|
||||
@@ -370,11 +583,12 @@ function hearth.ecosystem(player)
|
||||
.. label(0.7,4.85,h and (h.loaded and "Loaded sample / natural-origin plant counts; ground and shade include terrain" or "Incomplete sample / unloaded cells are unknown, not empty") or "Sample unavailable",
|
||||
h and h.loaded and ink.muted or ink.yellow)
|
||||
.. label(0.7,5.15,"WILDLIFE / all currently active animals, not a local census",ink.teal)
|
||||
.. "textarea[0.65,5.55;12.7,1.25;;;" .. esc(fauna) .. "]"
|
||||
.. "textarea[0.65,7.15;12.7,1.05;;;" .. esc("Observation does not advance succession or spawn animals. No terrain is loaded.\nNatural succession pauses while unloaded; crops and planted saplings follow separate rules.") .. "]"
|
||||
.. "textarea[0.65,5.55;12.7,1.55;;;" .. esc(fauna) .. "]"
|
||||
.. "textarea[0.65,7.15;12.7,1.05;;;" .. esc("Observation does not advance succession or spawn animals. No terrain is loaded.\nCalendar age includes unloaded time; ecological work is bounded and loaded-only. No missed-event replay; unknown breeding is disabled.") .. "]"
|
||||
.. "button[0.7,8.85;2.8,0.65;refresh;Observe again]"
|
||||
.. "tooltip[refresh;Read one fresh sample without changing the world.;" .. ink.mantle .. ";" .. ink.text .. "]"
|
||||
.. tooltip("refresh","Read one fresh sample without changing the world.")
|
||||
.. "button[3.7,8.85;3.3,0.65;botany_index;Botanical guide]"
|
||||
.. "button[7.15,8.85;2.6,0.65;fauna_index;Fauna guide]"
|
||||
.. "button[10.05,8.85;3.3,0.65;field_notes;Back to field notes]")
|
||||
end
|
||||
|
||||
@@ -382,6 +596,27 @@ C.register_on_player_receive_fields(function(player, form, fields)
|
||||
if form == "hearth:species" then
|
||||
reader.handle(player,fields)
|
||||
return true
|
||||
elseif form == "hearth:fauna_detail" then
|
||||
local session=hearth.sessions[player:get_player_name()]
|
||||
if not session then return true end
|
||||
if fields.fauna_index then hearth.fauna_guide(player,session.fauna_query or "",session.fauna_page or 1) end
|
||||
return true
|
||||
elseif form == "hearth:fauna" then
|
||||
local session=hearth.sessions[player:get_player_name()]; if not session then return true end
|
||||
if fields.field_notes then hearth.guide(player,6)
|
||||
elseif fields.botany_index then hearth.botanical_guide(player,"",1)
|
||||
elseif fields.species_details then
|
||||
local value=fields.species_choice
|
||||
local n=type(value)=="string" and #value<=2 and value:match("^%d+$") and tonumber(value)
|
||||
if n and n>=1 and n<=#(session.fauna_ids or {}) then
|
||||
session.fauna_selected=n
|
||||
local id=(session.fauna_ids or {})[n]
|
||||
if id then hearth.inspect_fauna(player,id,nil) end
|
||||
end
|
||||
elseif fields.search or fields.key_enter then hearth.fauna_guide(player,fields.query,1)
|
||||
elseif fields.next then hearth.fauna_guide(player,session.fauna_query,(session.fauna_page or 1)+1)
|
||||
elseif fields.previous then hearth.fauna_guide(player,session.fauna_query,(session.fauna_page or 1)-1) end
|
||||
return true
|
||||
elseif form == "hearth:botany" then
|
||||
local session = hearth.sessions[player:get_player_name()]
|
||||
if not session then return true end
|
||||
@@ -391,7 +626,8 @@ C.register_on_player_receive_fields(function(player, form, fields)
|
||||
if not n or n<1 or n>#(session.botany_ids or {}) then return true end
|
||||
session.botany_selected = n
|
||||
end
|
||||
if fields.field_notes then hearth.guide(player,6)
|
||||
if fields.fauna_index then hearth.fauna_guide(player,"",1)
|
||||
elseif fields.field_notes then hearth.guide(player,6)
|
||||
elseif fields.ecosystem_survey then hearth.ecosystem(player)
|
||||
elseif fields.species_article then
|
||||
local id = (session.botany_ids or {})[session.botany_selected or 1]
|
||||
@@ -402,7 +638,8 @@ C.register_on_player_receive_fields(function(player, form, fields)
|
||||
return true
|
||||
elseif form == "hearth:ecosystem" then
|
||||
if not hearth.sessions[player:get_player_name()] then return true end
|
||||
if fields.field_notes then hearth.guide(player,6)
|
||||
if fields.fauna_index then hearth.fauna_guide(player,"",1)
|
||||
elseif fields.field_notes then hearth.guide(player,6)
|
||||
elseif fields.botany_index then hearth.botanical_guide(player,"",1)
|
||||
elseif fields.refresh then hearth.ecosystem(player) end
|
||||
return true
|
||||
@@ -420,6 +657,7 @@ C.register_on_player_receive_fields(function(player, form, fields)
|
||||
if fields.recipe_index then hearth.recipes(player, "", 1); return true end
|
||||
if fields.ecosystem_survey then hearth.ecosystem(player); return true end
|
||||
if fields.botany_index then hearth.botanical_guide(player,"",1); return true end
|
||||
if fields.fauna_index then hearth.fauna_guide(player,"",1); return true end
|
||||
if fields.inspect_target then
|
||||
local ok, message = inspect(player, "")
|
||||
if not ok and hearth.notify then hearth.notify(player,message) end
|
||||
@@ -463,6 +701,12 @@ C.register_chatcommand("botany", {params = "[search]", description = "Browse nam
|
||||
hearth.botanical_guide(player,param or "",1)
|
||||
return true
|
||||
end})
|
||||
C.register_chatcommand("fauna", {params="[search]",description="Observe animal catalogue sources and conservative lifecycle status",
|
||||
func=function(name,param)
|
||||
local player=C.get_player_by_name(name)
|
||||
if not player or not hearth.sessions[name] then return false,"Join the world first." end
|
||||
hearth.fauna_guide(player,param or "",1); return true
|
||||
end})
|
||||
C.register_chatcommand("inspect", {params = "[exact species ID or Latin name]",
|
||||
description = "Read a targeted species' offline Wikipedia excerpt and current game effects",
|
||||
func = function(name, param)
|
||||
|
||||
Reference in New Issue
Block a user