734 lines
44 KiB
Lua
Executable File
734 lines
44 KiB
Lua
Executable File
-- Isolated journal / recipe / HUD tests; no engine or real player required.
|
|
local target = assert(arg[1], "interface.lua path required")
|
|
local sibling = target:match("^(.*[/\\])") or ""
|
|
local items = {
|
|
["hearth:knife"] = {description="Flint knife"}, ["hearth:loose_stone"] = {description="Loose flint"},
|
|
["hearth:firestarter"] = {description="Firestarter"}, ["hearth:torch"] = {description="Reed torch"},
|
|
["hearth:bedroll"] = {description="Bedroll"}, ["hearth:reeds"] = {description="Reeds"},
|
|
["hearth:planks"] = {description="Planks"}, ["hearth:fire"] = {description="Stone hearth"},
|
|
["hearth:quartz"] = {description="Quartz crystal"}, ["hearth:window"] = {description="Reed-lattice window"},
|
|
["hearth:copper_ore"] = {description="Copper ore"}, ["hearth:charcoal"] = {description="Prepared charcoal"},
|
|
["hearth:copper_ingot"] = {description="Copper ingot"}, ["hearth:copper_charge"] = {description="Copper charge"},
|
|
["hearth:copper_pick"] = {description="Copper pick"}, ["hearth:sticks"] = {description="Dry sticks"},
|
|
["hearth:log"] = {description="Timber log"}, ["hearth:stone"] = {description="Weathered stone"},
|
|
["hearth:alluvium"] = {description="Alluvium"},
|
|
}
|
|
local species = {"oak", "birch", "pine", "willow", "alder", "beech", "aspen", "rowan", "hazel", "spruce", "crab_apple", "wild_cherry", "field_maple"}
|
|
local rocks = {"granite", "basalt", "limestone", "slate", "sandstone"}
|
|
for _, s in ipairs(species) do
|
|
items["hearth:"..s.."_log"] = {description=s.." log"}
|
|
items["hearth:"..s.."_planks"] = {description=s.." planks"}
|
|
end
|
|
for _, rock in ipairs(rocks) do
|
|
items["hearth:"..rock] = {description=rock.." bedrock"}
|
|
items["hearth:"..rock.."_bricks"] = {description=rock.." masonry"}
|
|
end
|
|
local registered, screens, commands = {}, {}, {}
|
|
core = {registered_items=items, is_singleplayer=function() return true end}
|
|
function core.get_modpath() return sibling end
|
|
function core.formspec_escape(s)
|
|
return (tostring(s):gsub("\\","\\\\"):gsub("%[","\\["):gsub("%]","\\]"):gsub(";","\\;"):gsub(",","\\,"))
|
|
end
|
|
function core.colorize(color, s) return "\27(c@" .. color .. ")" .. s .. "\27(c@#ffffff)" end
|
|
function core.register_on_player_receive_fields(fn) registered.fields=fn end
|
|
function core.register_chatcommand(name, def) commands[name]=def end
|
|
function core.get_all_craft_recipes(output)
|
|
local recipes = {
|
|
["hearth:knife"] = {{method="normal",width=3,items={"hearth:loose_stone","hearth:loose_stone",nil,nil,"hearth:loose_stone"},output="hearth:knife"}},
|
|
["hearth:firestarter"] = {{method="normal",width=0,items={"hearth:loose_stone","hearth:sticks"},output="hearth:firestarter"}},
|
|
["hearth:torch"] = {{method="normal",width=0,items={"hearth:sticks","hearth:reeds"},output="hearth:torch 2"}},
|
|
["hearth:bedroll"] = {{method="normal",width=3,items={"hearth:planks","hearth:planks","hearth:planks","hearth:reeds","hearth:reeds","hearth:reeds"},output="hearth:bedroll"}},
|
|
["hearth:fire"] = {{method="normal",width=3,items={"hearth:loose_stone"},output="hearth:fire"}},
|
|
["hearth:copper_charge"] = {{method="normal",width=0,items={"hearth:copper_ore","hearth:charcoal"},output="hearth:copper_charge"}},
|
|
["hearth:copper_pick"] = {{method="normal",width=3,items={"hearth:copper_ingot","hearth:copper_ingot","hearth:copper_ingot","","hearth:sticks","","","hearth:sticks",""},output="hearth:copper_pick"}},
|
|
["hearth:window"] = {
|
|
{method="normal",width=0,items={"hearth:quartz","hearth:quartz","hearth:quartz"},output="hearth:window"},
|
|
{method="normal",width=3,items={"hearth:planks","hearth:reeds","hearth:planks","hearth:planks","hearth:reeds","hearth:planks"},output="hearth:window"}},
|
|
["hearth:log"] = {}, ["hearth:planks"] = {}, ["hearth:stone"] = {},
|
|
}
|
|
for _, s in ipairs(species) do
|
|
recipes["hearth:"..s.."_planks"] = {{method="normal",width=0,items={"hearth:"..s.."_log"},output="hearth:"..s.."_planks 4"}}
|
|
table.insert(recipes["hearth:log"], {method="normal",width=0,items={"hearth:"..s.."_log","hearth:"..s.."_log"},output="hearth:log 2"})
|
|
table.insert(recipes["hearth:planks"], {method="normal",width=0,items={"hearth:"..s.."_planks"},output="hearth:planks"})
|
|
end
|
|
for _, rock in ipairs(rocks) do
|
|
recipes["hearth:"..rock.."_bricks"] = {{method="normal",width=0,items={"hearth:"..rock},output="hearth:"..rock.."_bricks 4"}}
|
|
table.insert(recipes["hearth:stone"], {method="normal",width=0,items={"hearth:"..rock,"hearth:"..rock},output="hearth:stone 2"})
|
|
end
|
|
return recipes[output]
|
|
end
|
|
function core.show_formspec(name, form, spec) screens[#screens+1]={name=name,form=form,spec=spec} end
|
|
hearth = {sessions={alice={}}, model={location=function() end}}
|
|
dofile(target)
|
|
local player={get_player_name=function() return "alice" end}
|
|
local player_metadata={}
|
|
function player:get_meta()
|
|
return {get_string=function(_,key) return player_metadata[key] or "" end,
|
|
set_string=function(_,key,value) player_metadata[key]=value end}
|
|
end
|
|
function player:get_pos() return {x=10,y=4,z=-2} end
|
|
function player:set_inventory_formspec(spec) self.inventory_spec=spec end
|
|
local hud = {}
|
|
function player:hud_add(def) hud[#hud+1]=def; return #hud end
|
|
function player:hud_change(id, field, value) assert(hud[id],"unknown HUD id"); hud[id][field]=value end
|
|
function player:hud_set_flags(flags) self.flags=flags end
|
|
function player:hud_set_hotbar_itemcount(count) self.hotbar_count=count end
|
|
function player:hud_set_hotbar_image(texture) self.hotbar_texture=texture end
|
|
function player:hud_set_hotbar_selected_image(texture) self.hotbar_selected=texture end
|
|
function core.get_player_by_name(name) return name=="alice" and player or nil end
|
|
local total=0
|
|
local function test(name, fn)
|
|
local ok, err=pcall(fn); assert(ok, name..": "..tostring(err)); total=total+1
|
|
print("PASS " .. name)
|
|
end
|
|
local function latest() return screens[#screens] end
|
|
test("search lists craft recipes and elapsed real-time process instructions", function()
|
|
hearth.recipes(player,"",1)
|
|
local spec=latest().spec
|
|
assert(latest().form=="hearth:recipes" and spec:find("Page 1 /",1,true))
|
|
hearth.recipes(player,"knife",1)
|
|
assert(latest().spec:find("Flint knife",1,true))
|
|
hearth.recipes(player,"Cooked forage",1)
|
|
assert(latest().spec:find("6 real seconds",1,true), latest().spec)
|
|
hearth.recipes(player,"Boiled water",1)
|
|
assert(latest().spec:find("4 real seconds",1,true), latest().spec)
|
|
hearth.recipes(player,"Pottery firing",1)
|
|
assert(latest().spec:find("120 real seconds",1,true), latest().spec)
|
|
hearth.recipes(player,"River fish trap",1)
|
|
assert(latest().spec:find("600 real seconds",1,true), latest().spec)
|
|
hearth.recipes(player,"Spring fish run",1)
|
|
assert(latest().spec:find("scheduled deadline",1,true), latest().spec)
|
|
hearth.recipes(player,"River fish trap",1)
|
|
assert(latest().spec:find("600 real seconds",1,true), latest().spec)
|
|
hearth.recipes(player,"Spring fish run",1)
|
|
assert(latest().spec:find("scheduled deadline",1,true), latest().spec)
|
|
hearth.recipes(player,"firestarter",1)
|
|
assert(latest().spec:find("Firestarter",1,true))
|
|
hearth.recipes(player,"bedroll",1)
|
|
assert(latest().spec:find("Bedroll",1,true))
|
|
end)
|
|
test("plain text searches match ingredients and result names", function()
|
|
hearth.recipes(player,"Seasonal forage",1)
|
|
assert(latest().spec:find("Spring greens",1,true) and latest().spec:find("Autumn mushrooms",1,true))
|
|
hearth.recipes(player,"loose_stone",1)
|
|
assert(latest().spec:find("Flint knife",1,true))
|
|
hearth.recipes(player,"torch",1)
|
|
assert(latest().spec:find("Reed torch",1,true))
|
|
end)
|
|
test("search is bounded and formspec escaped, unknown term reports no matches", function()
|
|
local query=string.rep("x",80).."];button[0,0;"
|
|
hearth.recipes(player,query,1)
|
|
local spec=latest().spec
|
|
assert(spec:find("x",1,true) and not spec:find(query,1,true))
|
|
hearth.recipes(player,"no-such-item",1)
|
|
assert(latest().spec:find("No recipe or process matches",1,true))
|
|
end)
|
|
test("guide navigation and chat command enter the real recipe index", function()
|
|
assert(commands.recipes and commands.recipes.func("alice","reeds"))
|
|
assert(latest().form=="hearth:recipes" and latest().spec:find("Reed torch",1,true))
|
|
registered.fields(player,"hearth:guide",{recipe_index=true})
|
|
assert(latest().form=="hearth:recipes")
|
|
registered.fields(player,"hearth:recipes",{close=true})
|
|
assert(latest().form=="hearth:guide")
|
|
end)
|
|
test("all journal surfaces share Mocha controls, lists and botanical dividers", function()
|
|
local function themed(spec)
|
|
for _,color in ipairs({"#1e1e2e","#181825","#292940","#3c3d56","#cdd6f4","#cba6f7","#94e2d5"}) do
|
|
assert(spec:find(color,1,true), "missing palette anchor "..color)
|
|
end
|
|
assert(spec:find("style_type[button:hovered,button_exit:hovered",1,true))
|
|
assert(spec:find("bgimg="..core.formspec_escape("[fill:1x1:#ffffff"),1,true),"borderless buttons need visible backing")
|
|
assert(spec:find("style_type[field,textarea;",1,true))
|
|
assert(spec:find("listcolors[#292940;#3c3d56;#3c3d56;#181825;#cdd6f4]",1,true))
|
|
assert(spec:find("0.16,0.07;#94e2d5]",1,true),"pressed-sprig divider missing")
|
|
end
|
|
hearth.guide(player,1); themed(latest().spec)
|
|
hearth.recipes(player,"",1); themed(latest().spec)
|
|
hearth.inventory(player); themed(player.inventory_spec)
|
|
hearth.location_form(player); themed(latest().spec)
|
|
end)
|
|
test("guide keeps eight pages and every sidebar button inside 14 by 10", function()
|
|
for page=1,8 do
|
|
hearth.guide(player,page)
|
|
local spec,count=latest().spec,0
|
|
assert(spec:find("size[14,10]",1,true))
|
|
assert(spec:find(string.format("%02d / 08",page),1,true))
|
|
for x,y,w,h in spec:gmatch("button%[([%d.]+),([%d.]+);([%d.]+),([%d.]+);page_%d+;") do
|
|
assert(tonumber(x)+tonumber(w)<=3.3 and tonumber(y)+tonumber(h)<8.5)
|
|
count=count+1
|
|
end
|
|
assert(count==8 and not spec:find("page_9",1,true))
|
|
end
|
|
hearth.guide(player,6)
|
|
assert(latest().spec:find("Living ecosystems",1,true))
|
|
assert(latest().spec:find("Dynamic Life",1,true))
|
|
assert(not latest().spec:find("Riverward field season",1,true))
|
|
assert(latest().spec:find("no offline catch-up",1,true))
|
|
hearth.guide(player,0/0); assert(hearth.sessions.alice.page==1)
|
|
hearth.guide(player,math.huge); assert(hearth.sessions.alice.page==8)
|
|
end)
|
|
test("resource searches show quartz windows copper charges picks and process contract", function()
|
|
for _, query in ipairs({"quartz", "hearth:quartz"}) do
|
|
hearth.recipes(player,query,1)
|
|
assert(latest().spec:find("Reed-lattice window <- Quartz crystal + Quartz crystal + Quartz crystal",1,true))
|
|
end
|
|
hearth.recipes(player,"window",1)
|
|
assert(latest().spec:find("Planks + Planks + Planks + Planks + Reeds + Reeds",1,true))
|
|
hearth.recipes(player,"copper_ore",1)
|
|
assert(latest().spec:find("Copper charge <- Copper ore + Prepared charcoal",1,true))
|
|
hearth.recipes(player,"copper_pick",1)
|
|
assert(latest().spec:find("Copper pick <- Copper ingot + Copper ingot + Copper ingot + Dry sticks + Dry sticks",1,true))
|
|
hearth.recipes(player,"Copper smelting",1)
|
|
for _, text in ipairs({"45 real seconds", "30 fuel seconds", "75 fuel seconds", "one hearth:copper_ingot", "full inventory"}) do
|
|
assert(latest().spec:find(text,1,true),text)
|
|
end
|
|
hearth.recipes(player,"Fire fuel",1)
|
|
for _, text in ipairs({"180", "90", "240", "600", "nonrenewable", "not consumed"}) do
|
|
assert(latest().spec:find(text,1,true),text)
|
|
end
|
|
end)
|
|
for _, s in ipairs(species) do
|
|
test(s.." search discovers matching planks and canonical timber conversions", function()
|
|
hearth.recipes(player,"hearth:"..s.."_log",1)
|
|
assert(latest().spec:find("4 x "..s.." planks <- "..s.." log",1,true))
|
|
assert(latest().spec:find("2 x Timber log <- "..s.." log + "..s.." log",1,true))
|
|
assert(not latest().spec:find("\nTimber log <- "..s.." log",1,true))
|
|
hearth.recipes(player,"hearth:"..s.."_planks",1)
|
|
assert(latest().spec:find("Planks <- "..s.." planks",1,true))
|
|
end)
|
|
end
|
|
for _, rock in ipairs(rocks) do
|
|
test(rock.." search discovers four bricks and canonical stone", function()
|
|
hearth.recipes(player,"hearth:"..rock,1)
|
|
assert(latest().spec:find("4 x "..rock.." masonry <- "..rock.." bedrock",1,true))
|
|
assert(latest().spec:find("2 x Weathered stone <- "..rock.." bedrock + "..rock.." bedrock",1,true))
|
|
assert(not latest().spec:find("\nWeathered stone <- "..rock.." bedrock",1,true))
|
|
end)
|
|
end
|
|
test("regional guide covers ten biomes conversions and operator-controlled old-world seams", function()
|
|
hearth.guide(player,8)
|
|
for _, text in ipairs({"Temperate meadow", "oak woodland", "birch forest", "pine taiga", "Wet forest", "floodplain",
|
|
"Dry scrub", "alpine meadow", "rocky peaks", "riverbank", "canonical log", "45 real seconds", "240 seconds",
|
|
"fresh worlds only", "unexplored old-world boundaries", "Operators must explicitly choose", "climate HUD remains shared"}) do
|
|
assert(latest().spec:find(text,1,true),text)
|
|
end
|
|
for _, text in ipairs({"two matching species logs to two canonical logs", "two matching rocks convert to two canonical stones",
|
|
"separate crafting slots", "no single-log or single-rock canonical conversion"}) do
|
|
assert(latest().spec:find(text,1,true),text)
|
|
end
|
|
registered.fields(player,"hearth:guide",{page_8=true})
|
|
assert(hearth.sessions.alice.page==8)
|
|
hearth.guide(player,6)
|
|
for _,text in ipairs({"Birch pioneers", "Evergreen pine and spruce", "alluvium/peat", "two nutrient units", "Nitrogen-fixer policy",
|
|
"not a separate fertilizer process", "not fully simulated hydrology", "no offline catch-up"}) do
|
|
assert(latest().spec:find(text,1,true),text)
|
|
end
|
|
hearth.recipes(player,"Species timber",1)
|
|
assert(latest().spec:find("convert species logs in matching pairs first",1,true))
|
|
assert(latest().spec:find("two canonical hearth:log",1,true))
|
|
hearth.recipes(player,"Regional geology",1)
|
|
assert(latest().spec:find("two matching rocks to two canonical stones",1,true))
|
|
hearth.recipes(player,"Finite nitrogen",1)
|
|
assert(latest().spec:find("at most two nutrient units",1,true))
|
|
end)
|
|
|
|
test("inventory retains crafting, preview, ten by four storage and transfer rings", function()
|
|
hearth.inventory(player)
|
|
assert(player.inventory_spec:find("style_type[list;size=1,1;spacing=0.12,0.12]",1,true))
|
|
assert(0.6+10+9*0.12<12.4 and 5.7+4+3*0.12<11.3,"inventory grid must fit the paper")
|
|
for _,element in ipairs({"list[current_player;craft;0.6,1.8;3,3;]",
|
|
"list[current_player;craftpreview;4.8,2.9;1,1;]", "list[current_player;main;0.6,5.7;10,4;]",
|
|
"listring[current_player;main]listring[current_player;craft]", "field_guide;Open field notes]"}) do
|
|
assert(player.inventory_spec:find(element,1,true),element)
|
|
end
|
|
registered.fields(player,"",{field_guide=true}); assert(latest().form=="hearth:guide")
|
|
end)
|
|
test("recipe query escaping, literal matching and pagination remain bounded", function()
|
|
hearth.recipes(player,"];button[0,0;1,1;attack;Injected]",0/0)
|
|
assert(latest().spec:find(core.formspec_escape("];button[0,0;1,1;attack;Injected]"),1,true))
|
|
assert(not latest().spec:find(";attack;Injected]",1,true))
|
|
hearth.recipes(player,"%[.*",1)
|
|
assert(latest().spec:find("No recipe or process matches",1,true))
|
|
hearth.recipes(player,"\n\tfood",1); assert(hearth.sessions.alice.recipe_query==" food")
|
|
hearth.recipes(player,"",1)
|
|
registered.fields(player,"hearth:recipes",{next=true}); assert(hearth.sessions.alice.recipe_page==2)
|
|
registered.fields(player,"hearth:recipes",{previous=true}); assert(hearth.sessions.alice.recipe_page==1)
|
|
registered.fields(player,"hearth:recipes",{key_enter=true,query="torch"})
|
|
assert(latest().spec:find("Reed torch",1,true))
|
|
end)
|
|
test("ecosystem command and guide button only read one habitat and status snapshot", function()
|
|
local reads, snapshots=0,0
|
|
local function forbidden() error("observations must never mutate or load the map") end
|
|
for _,name in ipairs({"set_node","swap_node","emerge_area","forceload_block","add_entity","get_objects_inside_radius","get_meta"}) do
|
|
core[name]=forbidden
|
|
end
|
|
hearth.succession={habitat=function(pos)
|
|
assert(pos.x==10 and pos.y==4 and pos.z==-2); reads=reads+1
|
|
return {grass=3,flowers=2,cover=4,water=1,canopy=1.5,moisture=0.65,light=10,loaded=true,
|
|
ground="alluvium",pollination=7,nutrients=2}
|
|
end,step=forbidden,forage=forbidden,deposit=forbidden}
|
|
hearth.wildlife={status=function()
|
|
snapshots=snapshots+1
|
|
return {hare=3,deer=2,fox=1,total=6,global_cap=32,local_cap=6,player_cap=6,
|
|
recruitment={attempts=8,successes=2,last_reason="admitted"}}
|
|
end}
|
|
assert(commands.ecosystem.func("alice"))
|
|
assert(reads==1 and snapshots==1 and latest().form=="hearth:ecosystem")
|
|
for _,text in ipairs({"Natural grass cover: 3", "Flower-support cover: 2", "Shrub / fern / aquatic / tree cover: 4",
|
|
"Moisture: 65%", "Hares: 3", "Deer: 2", "Foxes: 1", "Active total: 6 / 32 globally",
|
|
"not a local census", "Loaded sample", "No terrain is loaded", "Ground: Alluvium",
|
|
"Pollinator-support plants: 7", "Nutrient reserve: 2 / 8", "pine and spruce stay evergreen", "deciduous canopy thins",
|
|
"Admissions: 2 / 8 attempts", "Latest outcome: admitted"}) do
|
|
assert(latest().spec:find(text,1,true),text)
|
|
end
|
|
registered.fields(player,"hearth:guide",{ecosystem_survey=true})
|
|
assert(reads==2 and snapshots==2 and latest().form=="hearth:ecosystem")
|
|
registered.fields(player,"hearth:ecosystem",{refresh=true})
|
|
assert(reads==3 and snapshots==3)
|
|
registered.fields(player,"hearth:ecosystem",{field_notes=true})
|
|
assert(hearth.sessions.alice.page==6 and reads==3 and snapshots==3)
|
|
assert(not commands.ecosystem.func("offline"))
|
|
end)
|
|
test("survey discloses unloaded and unavailable data without changing sessions or world", function()
|
|
hearth.succession.habitat=function() return {loaded=false,grass=1} end
|
|
hearth.ecosystem(player)
|
|
assert(latest().spec:find(core.formspec_escape("Incomplete sample / unloaded cells are unknown, not empty"),1,true))
|
|
assert(latest().spec:find("Natural grass cover: 1",1,true))
|
|
assert(latest().spec:find("Ground: unknown",1,true))
|
|
assert(latest().spec:find("Pollinator-support plants: unavailable",1,true))
|
|
assert(latest().spec:find("Nutrient reserve: unavailable",1,true))
|
|
hearth.succession.habitat=function()
|
|
return {loaded=true,ground="];button[0,0;1,1;attack;Injected]",pollination=0/0,nutrients=math.huge}
|
|
end
|
|
hearth.ecosystem(player)
|
|
assert(not latest().spec:find(";attack;Injected]",1,true))
|
|
assert(latest().spec:find(core.formspec_escape("hearth:];button[0,0;1,1;attack;Injected]"),1,true))
|
|
assert(latest().spec:find("Pollinator-support plants: 0",1,true))
|
|
assert(latest().spec:find("Nutrient reserve: 0 / 8",1,true))
|
|
hearth.succession,hearth.wildlife=nil,nil
|
|
hearth.ecosystem(player)
|
|
assert(latest().spec:find("Succession observations are not available",1,true))
|
|
assert(latest().spec:find("Wildlife observations are not available",1,true))
|
|
local session,count=hearth.sessions.alice,#screens
|
|
hearth.sessions.alice=nil
|
|
assert(not commands.ecosystem.func("alice"))
|
|
registered.fields(player,"hearth:guide",{ecosystem_survey=true})
|
|
registered.fields(player,"hearth:ecosystem",{refresh=true})
|
|
assert(#screens==count)
|
|
hearth.sessions.alice=session
|
|
end)
|
|
test("botanical guide tolerates absent empty and partial optional catalogue APIs", function()
|
|
for _, mock in ipairs({false, {}, {list=function() return {} end}}) do
|
|
hearth.botany=mock or nil
|
|
hearth.botanical_guide(player,"",math.huge)
|
|
assert(latest().form=="hearth:botany" and latest().spec:find("catalogue unavailable",1,true))
|
|
assert(hearth.sessions.alice.botany_page==1)
|
|
hearth.guide(player,6)
|
|
assert(latest().spec:find("catalogue unavailable",1,true))
|
|
end
|
|
end)
|
|
-- Actual catalogue factory/methods, not an invented list of test species.
|
|
local botany_path = target:match("^(.*[/\\])") or ""
|
|
local actual_botany = dofile(botany_path .. "botany.lua")(hearth,{})
|
|
test("actual botanical catalogue displays one hundred species in ten ten-row pages and eight uneven groups", function()
|
|
hearth.botany=actual_botany
|
|
local records, groups = actual_botany.list(), {}
|
|
assert(#records==100)
|
|
for page=1,10 do
|
|
hearth.botanical_guide(player,"",page)
|
|
local spec,rows=latest().spec,0
|
|
assert(spec:find("Page "..page.." / 10 / 100 matches",1,true))
|
|
for _ in spec:gmatch("Habitat:") do rows=rows+1 end
|
|
assert(rows==10,"each page must contain ten species rows")
|
|
for i=(page-1)*10+1,page*10 do
|
|
local r=records[i]; groups[r.group]=(groups[r.group] or 0)+1
|
|
assert(spec:find(core.formspec_escape(r.common.." ("..r.latin..")"),1,true),r.id)
|
|
assert(spec:find("Active seasons:",1,true) and spec:find("Roles / conditional policy or descriptors:",1,true))
|
|
end
|
|
end
|
|
local sizes={trees=13,shrubs=14,grasses=10,herbs=22,ferns_mosses=11,fungi=10,aquatic=10,lichens=10}
|
|
local count=0; for g,n in pairs(groups) do assert(n==sizes[g]); count=count+1 end; assert(count==8)
|
|
hearth.guide(player,6)
|
|
for _,r in ipairs(records) do assert(latest().spec:find(core.formspec_escape(r.common.." ("..r.latin..")"),1,true),r.id) end
|
|
end)
|
|
test("botanical searches use real scientific names niches seasons and honest role labels", function()
|
|
for _,query in ipairs({"Fagus sylvatica", "beech"}) do
|
|
hearth.botanical_guide(player,query,1)
|
|
assert(latest().spec:find("European beech (Fagus sylvatica)",1,true))
|
|
end
|
|
hearth.botanical_guide(player,"fly_agaric",1)
|
|
for _,text in ipairs({"Amanita muscaria", "toxic flag: YES", "Summer", "Autumn", "hosts:", "no catalogue food harvest"}) do
|
|
assert(latest().spec:find(text,1,true),text)
|
|
end
|
|
hearth.botanical_guide(player,"hornwort",1)
|
|
for _,text in ipairs({"Ceratophyllum demersum", "submerged", "water depth 1-4", "NOT food permission", "habitat engineering (descriptive tag)"}) do
|
|
assert(latest().spec:find(core.formspec_escape(text),1,true),text)
|
|
end
|
|
hearth.botanical_guide(player,"bracken",1)
|
|
assert(latest().spec:find("descriptor only",1,true) and latest().spec:find("no killing",1,true))
|
|
hearth.botanical_guide(player,"honey_fungus",1)
|
|
assert(latest().spec:find("no plant damage",1,true))
|
|
hearth.botanical_guide(player,"bilberry",1)
|
|
assert(latest().spec:find("seasonal game berry harvest",1,true))
|
|
assert(latest().spec:find("NOT a real-world forage guide",1,true))
|
|
end)
|
|
test("botanical query escaping literal search and finite pagination bounds", function()
|
|
local query="];button[0,0;1,1;attack;Injected]"
|
|
hearth.botanical_guide(player,query,0/0)
|
|
assert(latest().spec:find(core.formspec_escape(query),1,true))
|
|
assert(not latest().spec:find(";attack;Injected]",1,true))
|
|
hearth.botanical_guide(player,"%[.*",1)
|
|
assert(latest().spec:find("No species matches that literal search",1,true))
|
|
hearth.botanical_guide(player,string.rep("x",70),999)
|
|
assert(#hearth.sessions.alice.botany_query==48 and hearth.sessions.alice.botany_page==1)
|
|
hearth.botanical_guide(player,"\n\tlichen",-math.huge)
|
|
assert(hearth.sessions.alice.botany_query==" lichen" and hearth.sessions.alice.botany_page==1)
|
|
for _,p in ipairs({0/0,-8,"bad",0}) do hearth.botanical_guide(player,"",p); assert(hearth.sessions.alice.botany_page==1) end
|
|
hearth.botanical_guide(player,"",math.huge); assert(hearth.sessions.alice.botany_page==10)
|
|
local record=actual_botany.list()[1]; record.common=query
|
|
hearth.botany={list=function() return {record} end}
|
|
hearth.botanical_guide(player,"",1)
|
|
assert(latest().spec:find(core.formspec_escape(query),1,true) and not latest().spec:find(";attack;Injected]",1,true))
|
|
hearth.botany=actual_botany
|
|
end)
|
|
test("botanical commands buttons pagination and exits route without world access", function()
|
|
assert(commands.botany.func("alice","Quercus robur"))
|
|
assert(latest().form=="hearth:botany" and latest().spec:find("Pedunculate oak",1,true))
|
|
assert(not commands.botany.func("offline",""))
|
|
registered.fields(player,"hearth:guide",{botany_index=true})
|
|
assert(latest().form=="hearth:botany")
|
|
registered.fields(player,"hearth:botany",{next=true,query="ignored"}); assert(hearth.sessions.alice.botany_page==2)
|
|
registered.fields(player,"hearth:botany",{previous=true}); assert(hearth.sessions.alice.botany_page==1)
|
|
registered.fields(player,"hearth:botany",{key_enter=true,query="Picea abies"})
|
|
assert(latest().spec:find("Norway spruce",1,true))
|
|
registered.fields(player,"hearth:botany",{search=true,query="Betula pendula"})
|
|
assert(latest().spec:find("Silver birch",1,true) and hearth.sessions.alice.botany_page==1)
|
|
registered.fields(player,"hearth:botany",{field_notes=true}); assert(latest().form=="hearth:guide" and hearth.sessions.alice.page==6)
|
|
registered.fields(player,"hearth:ecosystem",{botany_index=true}); assert(latest().form=="hearth:botany")
|
|
registered.fields(player,"hearth:botany",{ecosystem_survey=true}); assert(latest().form=="hearth:ecosystem")
|
|
local session,count=hearth.sessions.alice,#screens; hearth.sessions.alice=nil
|
|
assert(not commands.botany.func("alice",""))
|
|
hearth.botanical_guide(player,"",1)
|
|
registered.fields(player,"hearth:botany",{next=true})
|
|
registered.fields(player,"hearth:ecosystem",{botany_index=true})
|
|
registered.fields(player,"hearth:guide",{botany_index=true})
|
|
assert(#screens==count); hearth.sessions.alice=session
|
|
end)
|
|
test("botanical navigation fits existing panels and does not add a ninth journal page", function()
|
|
hearth.guide(player,6)
|
|
assert(latest().spec:find("botany_index;Botanical guide",1,true) and not latest().spec:find("page_9",1,true))
|
|
local guide=latest().spec
|
|
hearth.ecosystem(player); local survey=latest().spec
|
|
hearth.botanical_guide(player,"",4); local browser=latest().spec
|
|
for _,spec in ipairs({guide,survey,browser}) do
|
|
local right=0
|
|
for x,y,w,h in spec:gmatch("button[%w_]*%[([%d.]+),([%d.]+);([%d.]+),([%d.]+);") do
|
|
x,y,w,h=tonumber(x),tonumber(y),tonumber(w),tonumber(h)
|
|
assert(x+w<=14 and y+h<=10)
|
|
if y==8.85 then assert(x>=right,"footer buttons overlap"); right=x+w end
|
|
end
|
|
end
|
|
end)
|
|
test("survey exposes actual optional botanical habitat fields without inventing counts", function()
|
|
hearth.succession={habitat=function() return {loaded=true,ground="peat",grass=2,flowers=1,cover=4,
|
|
near_water=8,water=2,aquatic="submerged",ph=4.5,nursery=3} end}
|
|
hearth.ecosystem(player)
|
|
for _,text in ipairs({"Nearby water cells: 8", "Water depth: 2", "Game substrate pH: 4.5", "Nursery-support plants: 3", "Aquatic niche: submerged"}) do
|
|
assert(latest().spec:find(text,1,true),text)
|
|
end
|
|
hearth.succession.habitat=function() return {loaded=false} end
|
|
hearth.ecosystem(player)
|
|
for _,text in ipairs({"Game substrate pH: unavailable", "Nursery-support plants: unavailable", "Water depth: unavailable", "Aquatic niche: unavailable"}) do
|
|
assert(latest().spec:find(text,1,true),text)
|
|
end
|
|
end)
|
|
test("fallen log guidance matches existing axe-only sideways cube behavior", function()
|
|
hearth.recipes(player,"Fallen logs",1)
|
|
for _,text in ipairs({"sideways", "full-cube", "axe", "one canonical hearth:log", "hands do not yield sticks"}) do
|
|
assert(latest().spec:find(text,1,true),text)
|
|
end
|
|
end)
|
|
test("location stays local-only, bounded and escaped", function()
|
|
local writes, checks=0,0
|
|
hearth.set_location=function(lat,lon) assert(lat==45 and lon==0); writes=writes+1 end
|
|
hearth.model.location=function(lat,lon) checks=checks+1; return lat=="45" and lon=="0" end
|
|
core.is_singleplayer=function() return false end
|
|
registered.fields(player,"hearth:location",{save_location=true,latitude="45",longitude="0"})
|
|
assert(writes==0 and checks==0 and not commands.location.func("alice"))
|
|
core.is_singleplayer=function() return true end
|
|
for _,value in ipairs({string.rep("1",17), "];button[0,0;", 42}) do
|
|
registered.fields(player,"hearth:location",{save_location=true,latitude=value,longitude="0"})
|
|
assert(writes==0 and latest().form=="hearth:location")
|
|
end
|
|
hearth.location_form(player,"];button[0,0;1,1;attack;Injected]")
|
|
assert(latest().spec:find(core.formspec_escape("];button[0,0;1,1;attack;Injected]"),1,true))
|
|
assert(not latest().spec:find(";attack;Injected]",1,true))
|
|
registered.fields(player,"hearth:location",{save_location=true,latitude="45",longitude="0"})
|
|
assert(writes==1 and latest().form=="hearth:guide")
|
|
end)
|
|
test("HUD preserves runtime keys, tiles ten hotbar slots and uses readable graphical reserves", function()
|
|
hearth.make_hud(player)
|
|
local ids=hearth.sessions.alice.hud
|
|
for _,key in ipairs({"title","clock","needs","context","help"}) do assert(hud[ids[key]],key) end
|
|
assert(player.hotbar_count==10 and player.flags.minimap==false)
|
|
local expected="[combine:320x32"
|
|
for i=0,9 do expected=expected..":"..(i*32)..",0=hearth_hotbar.png" end
|
|
assert(player.hotbar_texture==expected and player.hotbar_selected=="hearth_selected.png")
|
|
hearth.clock={hour=13.5,season="Autumn",date="2026-11-02"}
|
|
hearth.players={alice={warmth=10,water=35,food=75,stamina=100,wet=30}}
|
|
hearth.update_hud(player,{temperature=8,shelter=1,rain=true,heat=1,smoke=true})
|
|
assert(hud[ids.clock].text:find("Solar 13:30",1,true))
|
|
assert(hud[ids.warmth_label].text=="WARMTH 10%" and hud[ids.warmth_label].number==0xf38ba8)
|
|
assert(hud[ids.water_label].number==0xf9e2af and hud[ids.food_label].number==0x94e2d5)
|
|
assert(hud[ids.food_fill].text=="[fill:165x6:#94e2d5")
|
|
assert(hud[ids.context].text:find("SMOKE / open a wall or roof vent",1,true))
|
|
assert(hud[ids.context].number==0xf38ba8)
|
|
hearth.players.alice.food=0; hearth.players.alice.stamina=200
|
|
hearth.update_hud(player,{temperature=8,shelter=0,roof=true,heat=0})
|
|
assert(hud[ids.food_fill].text=="[fill:1x6:#18182500")
|
|
assert(hud[ids.stamina_label].text=="STAMINA 100%")
|
|
end)
|
|
test("HUD accepts legacy mocks without optional hotbar image setters", function()
|
|
local image,selected=player.hud_set_hotbar_image,player.hud_set_hotbar_selected_image
|
|
player.hud_set_hotbar_image,player.hud_set_hotbar_selected_image=nil,nil
|
|
hearth.make_hud(player)
|
|
assert(hearth.sessions.alice.hud.needs)
|
|
player.hud_set_hotbar_image,player.hud_set_hotbar_selected_image=image,selected
|
|
end)
|
|
assert(total==41,"The original checks plus three new tree recipe searches must remain intact")
|
|
test("inspect command uses exact species and rejects absent session unknown and URL input",function()
|
|
hearth.botany=actual_botany
|
|
hearth.wiki_articles=dofile(sibling.."wiki_articles.lua")
|
|
hearth.species_effects=dofile(sibling.."species_effects.lua")(hearth,{})
|
|
assert(commands.inspect and not commands.inspect.privs)
|
|
assert(commands.inspect.func("alice","Quercus robur"))
|
|
assert(latest().form=="hearth:species" and latest().spec:find("Wikipedia excerpt (offline)",1,true))
|
|
assert(not commands.inspect.func("alice","https://en.wikipedia.org/wiki/Oak"))
|
|
assert(not commands.inspect.func("alice","Quercus"))
|
|
assert(not commands.inspect.func("offline","oak"))
|
|
end)
|
|
test("botanical article selection uses trusted current-page numeric rows only",function()
|
|
hearth.botanical_guide(player,"",2)
|
|
assert(latest().spec:find("tooltip[species_choice;Species article;#181825;#cdd6f4]",1,true))
|
|
registered.fields(player,"hearth:botany",{species_choice="2",species_article=true,id="oak"})
|
|
assert(latest().form=="hearth:species" and hearth.sessions.alice.species_reader.id==actual_botany.list()[12].id)
|
|
for _,v in ipairs({"oak","0","11","2.5","1e0","999",{},string.rep("1",1000)}) do
|
|
hearth.botanical_guide(player,"",2); local n=#screens
|
|
registered.fields(player,"hearth:botany",{species_choice=v,species_article=true})
|
|
assert(#screens==n,"forged choice opened article")
|
|
end
|
|
hearth.botanical_guide(player,"no-such-species",1); local n=#screens
|
|
registered.fields(player,"hearth:botany",{species_article=true,id="oak"}); assert(#screens==n)
|
|
end)
|
|
test("article navigation retains server species and returns to selected botanical page",function()
|
|
hearth.botanical_guide(player,"",3)
|
|
registered.fields(player,"hearth:botany",{species_choice="2",species_article=true})
|
|
local id=hearth.sessions.alice.species_reader.id
|
|
registered.fields(player,"hearth:species",{reader_tab="2",id="pine",url="bad",text="edited"})
|
|
assert(hearth.sessions.alice.species_reader.id==id and latest().spec:find("SIMULATED",1,true))
|
|
registered.fields(player,"hearth:species",{reader_botany=true})
|
|
assert(latest().form=="hearth:botany" and hearth.sessions.alice.botany_page==3)
|
|
registered.fields(player,"hearth:botany",{species_choice="1",species_article=true})
|
|
registered.fields(player,"hearth:species",{quit=true}); local n=#screens
|
|
registered.fields(player,"hearth:species",{reader_tab="1"}); assert(#screens==n)
|
|
end)
|
|
test("fieldnotes inspect target uses loaded eye ray without changing other actions",function()
|
|
core.get_node_or_nil=function() return {name="hearth:oak_leaves"} end
|
|
function player:get_properties() return {eye_height=1.6} end
|
|
function player:get_look_dir() return {x=0,y=0,z=1} end
|
|
core.raycast=function(start,finish,objects,liquids)
|
|
assert(start.y==5.6 and finish.z==4 and not objects and not liquids)
|
|
local used=false
|
|
return function()
|
|
if used then return nil end; used=true
|
|
return {type="node",under={x=10,y=6,z=0},intersection_point={x=10,y=5.6,z=0}}
|
|
end
|
|
end
|
|
hearth.guide(player,6); assert(latest().spec:find("inspect_target;Inspect target",1,true))
|
|
registered.fields(player,"hearth:guide",{inspect_target=true})
|
|
assert(latest().form=="hearth:species" and hearth.sessions.alice.species_reader.id=="oak")
|
|
assert(commands.inspect.func("alice",""))
|
|
core.get_node_or_nil=function() return nil end
|
|
assert(not commands.inspect.func("alice",""))
|
|
end)
|
|
test("dropdown labels escape cached IDs and absent sessions reject article fields",function()
|
|
local r=actual_botany.list()[1]; r.id="];button[0,0;1,1;attack;Injected]"
|
|
hearth.botany={list=function() return {r} end}
|
|
hearth.botanical_guide(player,"",1)
|
|
assert(latest().spec:find(core.formspec_escape(r.id),1,true) and not latest().spec:find(";attack;Injected]",1,true))
|
|
hearth.botany=actual_botany
|
|
local s,n=hearth.sessions.alice,#screens; hearth.sessions.alice=nil
|
|
registered.fields(player,"hearth:botany",{species_choice="1",species_article=true})
|
|
registered.fields(player,"hearth:species",{reader_tab="2"})
|
|
assert(not commands.inspect.func("alice","oak") and #screens==n)
|
|
hearth.sessions.alice=s
|
|
end)
|
|
test("inventory appearance entry opens only the requesting player's creator", function()
|
|
local called = 0
|
|
hearth.character = {show = function(p) assert(p == player); called = called + 1 end}
|
|
hearth.inventory(player)
|
|
assert(player.inventory_spec:find("character_creator;Character appearance]",1,true))
|
|
assert(player.inventory_spec:find("Cosmetic only",1,true))
|
|
registered.fields(player,"",{character_creator=true}); assert(called == 1)
|
|
registered.fields(player,"hearth:recipes",{character_creator=true}); assert(called == 1)
|
|
hearth.character = nil
|
|
registered.fields(player,"",{character_creator=true}); assert(called == 1)
|
|
end)
|
|
assert(total==47,"Unexpected recipe/journal/character integration count")
|
|
test("inventory and journal tooltips contain escaped text, never an invalid third argument",function()
|
|
local specs={}
|
|
hearth.inventory(player); specs[#specs+1]=player.inventory_spec
|
|
local text="Preview woodland clothing and features. Cosmetic only; Save / Apply to keep changes."
|
|
assert(player.inventory_spec:find("tooltip[character_creator;"..core.formspec_escape(text)..";#181825;#cdd6f4]",1,true))
|
|
hearth.guide(player,6); specs[#specs+1]=latest().spec
|
|
hearth.recipes(player,"",1); specs[#specs+1]=latest().spec
|
|
hearth.location_form(player); specs[#specs+1]=latest().spec
|
|
hearth.botanical_guide(player,"",1); specs[#specs+1]=latest().spec
|
|
hearth.ecosystem(player); specs[#specs+1]=latest().spec
|
|
local n=0
|
|
for _,spec in ipairs(specs) do for body in spec:gmatch("tooltip%[(.-)%]") do
|
|
local clean=body:gsub("\\.",""); local _,separators=clean:gsub(";","")
|
|
assert(separators==1 or separators==3,"invalid tooltip arity: "..body); n=n+1
|
|
end end
|
|
assert(n>=10)
|
|
end)
|
|
test("survey loops actual counts for all 23 animals without changing the world",function()
|
|
hearth.lifecycle=dofile(sibling.."lifecycle.lua")
|
|
hearth.fauna=dofile(sibling.."fauna.lua")(hearth,{})
|
|
local counts={}; for i,r in ipairs(hearth.fauna.list()) do counts[r.id]=i end
|
|
local calls=0
|
|
hearth.wildlife={status=function() calls=calls+1; return {counts=counts,total=64,global_cap=64,local_cap=12,player_cap=16} end}
|
|
hearth.ecosystem(player); assert(calls==1)
|
|
for id,n in pairs(counts) do
|
|
local title=({hare="Hares",deer="Deer",fox="Foxes"})[id] or id:gsub("_"," ")
|
|
assert(latest().spec:find(title..": "..n,1,true),id)
|
|
end
|
|
assert(latest().spec:find("Calendar age includes unloaded time",1,true))
|
|
end)
|
|
test("actual fauna guide shows all identities sources and conservative lifecycle status read-only",function()
|
|
assert(commands.fauna.func("alice",""))
|
|
local records=hearth.fauna.list()
|
|
assert(#records==23 and latest().form=="hearth:fauna")
|
|
for page=1,5 do
|
|
hearth.fauna_guide(player,"",page)
|
|
assert(latest().spec:find("Page "..page.." / 5 / 23 matches",1,true))
|
|
for i=(page-1)*5+1,math.min(#records,page*5) do
|
|
local r=records[i]
|
|
assert(latest().spec:find(core.formspec_escape(r.source_url),1,true),r.id)
|
|
assert(latest().spec:find(core.formspec_escape(r.latin or "UNKNOWN species identity (legacy deer)"),1,true))
|
|
end
|
|
assert(latest().spec:find("automatic breeding disabled",1,true))
|
|
assert(latest().spec:find("no lifespan-endpoint death",1,true))
|
|
end
|
|
hearth.fauna_guide(player,"rabbit",1)
|
|
assert(latest().spec:find("Maturity: UNKNOWN",1,true))
|
|
hearth.fauna_guide(player,"bank_vole",1)
|
|
assert(latest().spec:find(core.formspec_escape("42-56 real days [source_reported]"),1,true))
|
|
assert(not commands.fauna.func("offline",""))
|
|
end)
|
|
test("fauna search pagination routing and escaping stay bounded and session-owned",function()
|
|
local query="];button[0,0;1,1;attack;Injected]"
|
|
hearth.fauna_guide(player,query,0/0)
|
|
assert(latest().spec:find(core.formspec_escape(query),1,true) and not latest().spec:find(";attack;Injected]",1,true))
|
|
assert(latest().spec:find("No animal matches",1,true))
|
|
hearth.fauna_guide(player,string.rep("x",80),1); assert(#hearth.sessions.alice.fauna_query==48)
|
|
hearth.fauna_guide(player,"",math.huge); assert(hearth.sessions.alice.fauna_page==5)
|
|
registered.fields(player,"hearth:guide",{fauna_index=true}); assert(hearth.sessions.alice.fauna_page==1)
|
|
registered.fields(player,"hearth:fauna",{next=true}); assert(hearth.sessions.alice.fauna_page==2)
|
|
registered.fields(player,"hearth:fauna",{previous=true}); assert(hearth.sessions.alice.fauna_page==1)
|
|
registered.fields(player,"hearth:fauna",{search=true,query="Lutra lutra"}); assert(latest().spec:find("otter",1,true))
|
|
registered.fields(player,"hearth:fauna",{botany_index=true}); assert(latest().form=="hearth:botany")
|
|
registered.fields(player,"hearth:botany",{fauna_index=true}); assert(latest().form=="hearth:fauna")
|
|
registered.fields(player,"hearth:fauna",{field_notes=true}); assert(latest().form=="hearth:guide")
|
|
registered.fields(player,"hearth:ecosystem",{fauna_index=true}); assert(latest().form=="hearth:fauna")
|
|
local original=hearth.fauna
|
|
local r=original.list()[1]; r.common=query; r.source_url=query
|
|
hearth.fauna={list=function() return {r} end}; hearth.fauna_guide(player,"",1)
|
|
assert(latest().spec:find(core.formspec_escape(query),1,true) and not latest().spec:find(";attack;Injected]",1,true))
|
|
hearth.fauna=nil; hearth.fauna_guide(player,"",1); assert(latest().spec:find("catalogue unavailable",1,true))
|
|
hearth.fauna=original
|
|
local session,n=hearth.sessions.alice,#screens; hearth.sessions.alice=nil
|
|
assert(not commands.fauna.func("alice","")); hearth.fauna_guide(player,"",1)
|
|
registered.fields(player,"hearth:fauna",{next=true}); assert(#screens==n)
|
|
hearth.sessions.alice=session
|
|
end)
|
|
test("fauna species selection and live inspection show bounded read-only species details",function()
|
|
local records=hearth.fauna.list(); local before=hearth.wildlife.status()
|
|
hearth.fauna_guide(player,"",1)
|
|
assert(latest().spec:find("species_choice;hare,deer,fox,rabbit,red_squirrel",1,true))
|
|
registered.fields(player,"hearth:fauna",{species_choice="3",species_details=true})
|
|
assert(latest().form=="hearth:fauna_detail" and latest().spec:find("Vulpes vulpes",1,true))
|
|
assert(latest().spec:find("Modeled prey:",1,true) and latest().spec:find("Winter rest policy:",1,true))
|
|
registered.fields(player,"hearth:fauna_detail",{fauna_index=true})
|
|
assert(latest().form=="hearth:fauna" and hearth.sessions.alice.fauna_page==1)
|
|
local now=os.time()
|
|
assert(hearth.inspect_fauna(player,"hare",{born_at=now-500*86400,origin="born",foodreserve=.42,
|
|
resident=true,observation="rest"}))
|
|
local spec=latest().spec
|
|
for _,text in ipairs({"Lepus europaeus","Lifecycle: mature","model years","real calendar days",
|
|
"Coarse modeled food reserve (policy, not biology): 42%","Current animation observation: resting",
|
|
"persistent registered resident","Modeled diet: browse","Activity / sleep policy: night / solar_night",
|
|
"Winter rest policy:","Modeled behavior:","Modeled prey:","Reproduction: enabled only within the coarse model",
|
|
"Active loaded animals of this species:"}) do
|
|
assert(spec:find(core.formspec_escape(text),1,true),text)
|
|
end
|
|
local after=hearth.wildlife.status()
|
|
assert(after.total==before.total and after.counts.hare==before.counts.hare,"panel reads status without game changes")
|
|
local original_get=hearth.fauna.get; local injection="];button[0,0;1,1;attack;Injected]"
|
|
hearth.fauna.get=function(id)
|
|
local r=original_get(id)
|
|
if id=="hare" then r.common=injection; r.latin=injection; r.source_url=injection end
|
|
return r
|
|
end
|
|
hearth.inspect_fauna(player,"hare",nil)
|
|
assert(latest().spec:find(core.formspec_escape(injection),1,true) and not latest().spec:find(";attack;Injected]",1,true))
|
|
hearth.fauna.get=original_get
|
|
local session,count=hearth.sessions.alice,#screens; hearth.sessions.alice=nil
|
|
assert(not hearth.inspect_fauna(player,"hare",nil) and #screens==count)
|
|
hearth.sessions.alice=session
|
|
local impostor={get_player_name=function() return "alice" end}
|
|
assert(not hearth.inspect_fauna(impostor,"hare",nil) and #screens==count)
|
|
assert(not hearth.inspect_fauna(player,"hare",setmetatable({},{})) and #screens==count)
|
|
assert(not hearth.inspect_fauna(player,"unknown",nil) and #screens==count)
|
|
assert(#records==23)
|
|
end)
|
|
test("wildlife encounters add canonical per-player field-journal entries; catalogue browsing does not",function()
|
|
local key="hearth:fauna_observations_v1"
|
|
player_metadata[key]=""
|
|
hearth.fauna_guide(player,"",1)
|
|
assert(latest().spec:find("0 / 23 species recorded",1,true))
|
|
hearth.inspect_fauna(player,"hare",{born_at=os.time()-86400,origin="established",foodreserve=.5,
|
|
resident=true,observation="awake"})
|
|
assert(player_metadata[key]=="hare")
|
|
assert(latest().spec:find("Field journal: first recorded encounter",1,true))
|
|
hearth.inspect_fauna(player,"fox",{born_at=os.time()-86400,origin="established",foodreserve=.5,
|
|
resident=true,observation="awake"})
|
|
assert(player_metadata[key]=="fox,hare","journal storage is canonical and sorted")
|
|
hearth.inspect_fauna(player,"hare",{born_at=os.time()-86400,origin="established",foodreserve=.5,
|
|
resident=true,observation="awake"})
|
|
assert(latest().spec:find("already recorded",1,true))
|
|
hearth.inspect_fauna(player,"deer",nil)
|
|
assert(player_metadata[key]=="fox,hare","catalogue profile selection is not a sighting")
|
|
hearth.fauna_guide(player,"",1)
|
|
assert(latest().spec:find("2 / 23 species recorded",1,true))
|
|
assert(latest().spec:find("recorded encounter",1,true))
|
|
player_metadata[key]="hare,unknown"
|
|
assert(hearth.inspect_fauna(player,"deer",{born_at=os.time()-86400,origin="established",foodreserve=.5,
|
|
resident=true,observation="awake"}))
|
|
assert(player_metadata[key]=="hare,unknown","malformed journal data fails closed without overwriting")
|
|
player_metadata[key]=nil
|
|
end)
|
|
test("calendar guidance never promises accelerated saplings daily berries or complete biology",function()
|
|
hearth.recipes(player,"Renewal",1)
|
|
for _,s in ipairs({"365.2425 days","no 24-hour refill","at most one berry","one real year","UNKNOWN"}) do
|
|
assert(latest().spec:find(s,1,true),s)
|
|
end
|
|
hearth.guide(player,6)
|
|
for _,s in ipairs({"100 named species","23 animal identities","eight uneven","real years to decades","fungi do not accelerate growth","disable breeding"}) do
|
|
assert(latest().spec:find(s,1,true),s)
|
|
end
|
|
assert(not latest().spec:find("eight categories of ten",1,true))
|
|
end)
|
|
assert(total==54,"Unexpected focused integration count")
|
|
print("PASS "..total.." recipe discovery tests")
|