454 lines
24 KiB
Lua
Executable File
454 lines
24 KiB
Lua
Executable File
-- Standalone Lua 5.1+; mocked APIs only, never starts Luanti.
|
|
local target=assert(arg[1],"botany.lua path required")
|
|
local factory=dofile(target)
|
|
local passed=0
|
|
local B,H,A,world,fields,defs,time,season,allowed,room,leftover,added,auth,writes
|
|
local policies,seasonal_ids,clock_reads,climate_calls
|
|
local p={x=2,y=1,z=3}
|
|
local function key(q) return q.x..":"..q.y..":"..q.z end
|
|
local function reset()
|
|
world,fields,defs={},{},{}
|
|
policies,seasonal_ids,clock_reads,climate_calls={},{},0,0
|
|
time,season,allowed,room,leftover,added,auth,writes=100000,"Summer",true,true,0,0,0,0
|
|
A={registered_nodes=defs,now=function() clock_reads=clock_reads+1; return time end}
|
|
function A.get_node_or_nil(q) return world[key(q)] end
|
|
function A.get_meta(q)
|
|
fields[key(q)]=fields[key(q)] or {}; local f=fields[key(q)]
|
|
return {get_string=function(_,k) return f[k] or "" end,
|
|
set_string=function(_,k,v) writes=writes+1; f[k]=v end}
|
|
end
|
|
-- Missing construction at factory time is deliberate: content wires it later.
|
|
H={climate_at=function(epoch)
|
|
assert(type(epoch)=="number" and epoch==epoch and epoch==math.floor(time))
|
|
climate_calls=climate_calls+1; return {season=season}
|
|
end, seasons={
|
|
add_variants=function(id,definitions,active)
|
|
assert(not policies[id],id); policies[id]={definitions=definitions,active=active}
|
|
for s in pairs(definitions) do seasonal_ids["hearth:"..id.."_"..s:lower()]=id end
|
|
end,
|
|
kind=function(name) return seasonal_ids[name] or name:match("^hearth:(.+)$") end,
|
|
}}
|
|
B=factory(H,A)
|
|
H.construction={allowed=function(q,player,action)
|
|
auth=auth+1; assert(q==p and player:is_player() and action=="use"); return allowed
|
|
end}
|
|
end
|
|
local inv={room_for_item=function(_,list,item) assert(list=="main" and (item=="hearth:berries 2" or item=="hearth:berries 3")); return room end,
|
|
add_item=function(_,list,item)
|
|
assert(list=="main" and (item=="hearth:berries 2" or item=="hearth:berries 3")); added=added+tonumber(item:match("(%d+)$"))-leftover
|
|
return {get_count=function() return leftover end}
|
|
end}
|
|
local player={is_player=function() return true end,get_inventory=function() return inv end}
|
|
local function test(label,fn)
|
|
reset(); local ok,err=pcall(fn); assert(ok,label..": "..tostring(err))
|
|
passed=passed+1; print("PASS botany "..passed.." - "..label)
|
|
end
|
|
local function habitat(id)
|
|
local r=assert(B.get(id)); local n=r.niche
|
|
local ground=next(n.substrates)
|
|
if n.aquatic=="surface" or n.aquatic=="submerged" then ground="alluvium" end
|
|
return {loaded=true,ground=ground,substrate=id=="hornwort" and "water" or nil,
|
|
light=(n.light[1]+n.light[2])/2,moisture=(n.moisture[1]+n.moisture[2])/2,
|
|
ph=(n.ph[1]+n.ph[2])/2,water=n.water[1],aquatic=n.aquatic,
|
|
freshwater=true,nutrients=2,host=n.hosts and next(n.hosts) or nil}
|
|
end
|
|
local function register()
|
|
B.register_nodes(function(id,def) assert(not defs["hearth:"..id],id); defs["hearth:"..id]=def end)
|
|
end
|
|
local function bush(id) world[key(p)]={name="hearth:"..(id or "bilberry")} end
|
|
test("exactly one hundred disjoint real taxa in eight uneven groups",function()
|
|
assert(#B.groups==8 and #B.catalogue==100 and #B.list()==100)
|
|
assert(table.concat(B.groups,",")=="trees,shrubs,grasses,herbs,ferns_mosses,fungi,aquatic,lichens")
|
|
local ids,latin={},{},{}
|
|
for _,g in ipairs(B.groups) do
|
|
assert(#B.list(g)==({trees=13,shrubs=14,grasses=10,herbs=22,ferns_mosses=11,fungi=10,aquatic=10,lichens=10})[g],g)
|
|
for _,r in ipairs(B.list(g)) do
|
|
assert(not ids[r.id] and not latin[r.latin]); ids[r.id]=true; latin[r.latin]=true
|
|
assert(r.common~="" and r.latin:match("^[A-Z][a-z]+ [a-z%-]+$"),r.latin)
|
|
assert(r.group==g and B.group_of(r.node)==g and B.kind(r.node)==r.id)
|
|
end
|
|
end
|
|
end)
|
|
test("twenty expansion identities match the lifecycle contract with nitrogen and flower seasons",function()
|
|
local L=dofile(target:gsub("[^/\\]+$","lifecycle.lua"))
|
|
local ids={"dandelion","daisy","red_clover","birdsfoot_trefoil","ribwort_plantain","selfheal","knapweed",
|
|
"oxeye_daisy","field_scabious","teasel","cow_parsley","garlic_mustard","honeysuckle","ivy","gorse","broom",
|
|
"crab_apple","wild_cherry","field_maple","holly_fern"}
|
|
assert(#ids==20)
|
|
for _,id in ipairs(ids) do
|
|
local r=B.get(id); local p=L.plant(id)
|
|
assert(r and p and r.latin==p.latin and r.group==p.group and not r.food,id)
|
|
end
|
|
for _,id in ipairs({"red_clover","birdsfoot_trefoil","gorse","broom"}) do assert(B.nitrogen(id,0)==1) end
|
|
assert(B.pollinator("ivy","Autumn") and not B.pollinator("ivy","Spring"))
|
|
assert(B.pollinator("gorse","Winter") and not B.pollinator("ribwort_plantain","Summer"))
|
|
assert(B.seeding("garlic_mustard","Summer") and not B.seeding("garlic_mustard","Spring"))
|
|
end)
|
|
test("legacy IDs map exactly without counting generic bushes or patches",function()
|
|
local expected={oak="Quercus robur",birch="Betula pendula",pine="Pinus sylvestris",willow="Salix alba",
|
|
alder="Alnus glutinosa",clover="Trifolium repens",nettle="Urtica dioica",heather="Calluna vulgaris",
|
|
bluebell="Hyacinthoides non-scripta",sedge="Carex paniculata",bulrush="Typha latifolia",
|
|
moss="Hypnum cupressiforme",lichen="Rhizocarpon geographicum"}
|
|
for id,latin in pairs(expected) do local r=B.get(id); assert(r.existing and r.latin==latin) end
|
|
for _,id in ipairs({"berry_bush","fern","meadow_grass","wildflowers","tree_seedling"}) do assert(not B.get(id)) end
|
|
assert(not B.get("foreign:oak") and not B.kind(false))
|
|
end)
|
|
test("tree stages and resources resolve through seasons without registering them",function()
|
|
H.seasons={kind=function(value) return value:gsub("_winter$",""):gsub("^hearth:","") end}
|
|
for _,r in ipairs(B.list("trees")) do
|
|
for _,part in ipairs({"seedling","young","mature","leaves","log","planks"}) do
|
|
assert(B.kind("hearth:"..r.id.."_"..part.."_winter")==r.id)
|
|
end
|
|
end
|
|
register()
|
|
for _,r in ipairs(B.list("trees")) do assert(not defs[r.node]) end
|
|
end)
|
|
test("catalogue snapshots and getters cannot alter internal policy",function()
|
|
B.catalogue[1].latin="broken"; B.get("oak").niche.light[1]=100
|
|
local list=B.list(); list[1].tags.pollinator=true
|
|
assert(B.get("oak").latin=="Quercus robur" and not B.has_tag("oak","pollinator"))
|
|
assert(B.get("oak").niche.light[1]==4)
|
|
end)
|
|
test("all one hundred species have a passing season niche vector",function()
|
|
for _,r in ipairs(B.list()) do assert(B.suitable(r.id,habitat(r.id),next(r.seasons)),r.id) end
|
|
end)
|
|
test("each niche dimension rejects out-of-window values for all species",function()
|
|
for _,r in ipairs(B.list()) do
|
|
local s=next(r.seasons)
|
|
for _,field in ipairs({"light","moisture","ph"}) do
|
|
local h=habitat(r.id); h[field]=r.niche[field][1]-.01
|
|
assert(not B.suitable(r.id,h,s),r.id..field)
|
|
h[field]=r.niche[field][2]+.01; assert(not B.suitable(r.id,h,s))
|
|
end
|
|
local h=habitat(r.id); h.ground="lava"; h.substrate="lava"; assert(not B.suitable(r.id,h,s))
|
|
end
|
|
end)
|
|
test("unloaded, malformed and nonfinite habitats fail closed",function()
|
|
for _,field in ipairs({"light","moisture","ph","water"}) do
|
|
for _,bad in ipairs({0/0,math.huge,-math.huge,"5"}) do
|
|
local h=habitat("clover"); h[field]=bad; assert(not B.suitable("clover",h,"Summer"))
|
|
end
|
|
end
|
|
local h=habitat("clover"); h.loaded=false; assert(not B.suitable("clover",h,"Summer"))
|
|
h.loaded=true; h.ph=nil; assert(not B.suitable("clover",h,"Summer"))
|
|
assert(not B.suitable("clover",nil,"Summer") and not B.suitable("unknown",{},"Summer"))
|
|
end)
|
|
test("spring ephemerals and fungal fruit seasons are not year-round",function()
|
|
for _,id in ipairs({"bluebell","wild_garlic","cowslip","wood_anemone"}) do
|
|
assert(B.suitable(id,habitat(id),"Spring")); assert(not B.suitable(id,habitat(id),"Summer"))
|
|
end
|
|
assert(B.active("wood_blewit","Winter") and not B.active("fly_agaric","Winter"))
|
|
assert(B.active("moss","Winter") and not B.active("oak","Winter"))
|
|
end)
|
|
test("host-specific root fungi reject unrelated or absent tree roots",function()
|
|
local h=habitat("birch_bolete"); assert(B.suitable("birch_bolete",h,"Autumn"))
|
|
h.host="oak"; assert(not B.suitable("birch_bolete",h,"Autumn"))
|
|
h.host=nil; assert(not B.suitable("birch_bolete",h,"Autumn"))
|
|
assert(B.root_fungi("birch_bolete","birch_seedling",2)==1)
|
|
assert(B.root_fungi("birch_bolete","oak",2)==0)
|
|
end)
|
|
test("deadwood decomposers require deadwood rather than live bark",function()
|
|
local h=habitat("birch_polypore"); assert(B.suitable("birch_polypore",h,"Autumn"))
|
|
h.ground="bark"; assert(not B.suitable("birch_polypore",h,"Autumn"))
|
|
h.ground="deadwood"; h.host="pine"; assert(not B.suitable("birch_polypore",h,"Autumn"))
|
|
assert(B.has_tag("honey_fungus","parasite") and not B.interactions("honey_fungus").parasite.enabled)
|
|
end)
|
|
test("lichens distinguish acid stone, bark and humid old-woodland niches",function()
|
|
local h=habitat("lichen"); assert(B.suitable("lichen",h,"Winter"))
|
|
h.ground="limestone"; assert(not B.suitable("lichen",h,"Winter"))
|
|
h=habitat("oakmoss"); h.ground="soil"; assert(not B.suitable("oakmoss",h,"Winter"))
|
|
h=habitat("lungwort_lichen"); h.moisture=.2; assert(not B.suitable("lungwort_lichen",h,"Winter"))
|
|
end)
|
|
test("surface plants need freshwater, depth and appropriate bottom",function()
|
|
for _,id in ipairs({"duckweed","yellow_waterlily","white_waterlily","water_crowfoot","pondweed"}) do
|
|
local h=habitat(id); assert(B.suitable(id,h,"Summer"))
|
|
h.freshwater=false; assert(not B.suitable(id,h,"Summer")); h.freshwater=true
|
|
h.water=0; assert(not B.suitable(id,h,"Summer")); h.water=1
|
|
h.aquatic="submerged"; assert(not B.suitable(id,h,"Summer"))
|
|
end
|
|
end)
|
|
test("submerged rootless hornwort and rooted starwort have separate supports",function()
|
|
local h=habitat("hornwort"); assert(B.suitable("hornwort",h,"Summer"))
|
|
h.substrate=nil; assert(not B.suitable("hornwort",h,"Summer"))
|
|
h=habitat("water_starwort"); assert(B.suitable("water_starwort",h,"Summer"))
|
|
h.ground="stone"; assert(not B.suitable("water_starwort",h,"Summer"))
|
|
end)
|
|
test("wet banks are dry destination cells, not overwritten water sources",function()
|
|
for _,id in ipairs({"sedge","bulrush","water_mint","brooklime","water_forgetmenot"}) do
|
|
local h=habitat(id); assert(h.water==0 and B.suitable(id,h,"Summer"))
|
|
h.aquatic="land"; assert(not B.suitable(id,h,"Summer")); h.aquatic="bank"
|
|
h.water=1; assert(not B.suitable(id,h,"Summer"))
|
|
end
|
|
end)
|
|
test("deterministic choose returns only a suitable candidate and never mutates",function()
|
|
local h=habitat("clover"); local a,r=B.choose(p,h,"Summer")
|
|
assert(a and a==B.choose(p,h,"Summer") and B.suitable(a,h,"Summer"))
|
|
assert(r.node=="hearth:"..a and B.group_of(a)~="trees" and writes==0 and next(world)==nil)
|
|
h.loaded=false; assert(not B.choose(p,h,"Summer"))
|
|
assert(not B.choose({x=0/0,y=1,z=2},h,"Summer") and not B.choose(nil,h,"Summer"))
|
|
end)
|
|
test("choose respects surface and submerged niches without map operations",function()
|
|
for _,id in ipairs({"duckweed","hornwort"}) do
|
|
local h=habitat(id); local k=B.choose(p,h,"Summer")
|
|
assert(k and B.group_of(k)=="aquatic" and B.suitable(k,h,"Summer"))
|
|
end
|
|
assert(writes==0)
|
|
end)
|
|
test("pollinator support follows flowers, not wind-pollinated grasses or fungi",function()
|
|
assert(B.pollinator("bluebell","Spring") and not B.pollinator("bluebell","Summer"))
|
|
assert(B.pollinator("heather","Autumn") and not B.pollinator("heather","Spring"))
|
|
for _,id in ipairs({"sedge","oak","birch","nettle","fly_agaric","bracken","moss"}) do
|
|
assert(not B.pollinator(id,"Summer"))
|
|
end
|
|
end)
|
|
test("only three fictional game berry outputs are food, never toxic taxa or fungi",function()
|
|
local count=0
|
|
for _,r in ipairs(B.list()) do
|
|
if r.food then count=count+1; assert(({bilberry=true,blackberry=true,raspberry=true})[r.id] and not r.toxic) end
|
|
if r.toxic or r.group=="fungi" then assert(not r.food and not B.edible(r.id,"Summer")) end
|
|
end
|
|
assert(count==3 and B.edible("blackberry","Autumn") and not B.edible("raspberry","Winter"))
|
|
assert(not B.edible("clover","Summer") and B.browse("clover","Summer"))
|
|
end)
|
|
test("interaction descriptors cover every actual tag without mutation",function()
|
|
local seen={}
|
|
for _,r in ipairs(B.list()) do
|
|
local d=B.interactions(r.id)
|
|
for tag in pairs(r.tags) do assert(d[tag] and #d[tag].description>20); seen[tag]=true end
|
|
end
|
|
for _,tag in ipairs({"nitrogenfixer","browse","pollinator","ectomycorrhiza","saprotroph","parasite","engineer","nursery","allelopathy","resin"}) do assert(seen[tag],tag) end
|
|
local d=B.interactions("bracken"); d.allelopathy.enabled=true
|
|
assert(B.interactions("bracken").allelopathy.enabled==false and writes==0)
|
|
end)
|
|
test("nitrogen is capped and fungi transfer only an existing positive budget",function()
|
|
assert(B.nitrogen("alder",0)==1 and B.nitrogen("clover",1.5)==.5 and B.nitrogen("clover",2)==0)
|
|
assert(B.nitrogen("oak",0)==0 and B.nitrogen("alder",0/0)==0)
|
|
assert(B.root_fungi("fly_agaric","pine",.25)==.25 and B.root_fungi("fly_agaric","pine",0)==0)
|
|
assert(B.root_fungi("turkey_tail","oak",2)==0 and B.root_fungi("fly_agaric","pine",math.huge)==0)
|
|
assert(B.resin("spruce") and B.resin("pine_log") and not B.resin("oak"))
|
|
assert(writes==0)
|
|
end)
|
|
test("register exactly seventy-nine missing understory nodes, never legacy or trees",function()
|
|
register(); local count=0
|
|
for name,def in pairs(defs) do
|
|
count=count+1; local r=B.get(name)
|
|
assert(r and not r.existing and r.group~="trees")
|
|
assert(def.tiles[1]=="hearth_botany_"..r.id..".png" and not def.mesh)
|
|
assert(def.description==r.common.." ("..r.latin..")")
|
|
assert(not def.walkable and not def.buildable_to and def.groups.hearth_ecosystem==1)
|
|
end
|
|
assert(count==79); register(); local again=0; for _ in pairs(defs) do again=again+1 end; assert(again==count)
|
|
end)
|
|
test("existing registered definitions are preserved and construction stays late-bound",function()
|
|
local marker={description="external owner"}; defs["hearth:bilberry"]=marker
|
|
register(); assert(defs["hearth:bilberry"]==marker)
|
|
H.construction=nil; bush(); assert(not B.harvest(p,player) and added==0)
|
|
end)
|
|
test("normal plants dig as nodes; shrubs drop sticks preventing cooldown-reset replant",function()
|
|
register()
|
|
assert(defs["hearth:yarrow"].drop=="hearth:yarrow")
|
|
for _,r in ipairs(B.list("shrubs")) do assert(defs[r.node].drop=="hearth:sticks") end
|
|
defs["hearth:bilberry"].after_place_node(p)
|
|
assert(fields[key(p)].player_placed=="1")
|
|
end)
|
|
test("aquatic sprites and flat pads remain inside one block and are never liquids",function()
|
|
register()
|
|
for _,r in ipairs(B.list("aquatic")) do
|
|
local d=defs[r.node]; assert(not d.groups.water and not d.liquidtype)
|
|
if r.niche.aquatic=="surface" then
|
|
assert(d.drawtype=="nodebox")
|
|
for _,v in ipairs(d.node_box.fixed) do assert(v>=-.5 and v<=.5) end
|
|
else assert(d.drawtype=="plantlike") end
|
|
if r.niche.aquatic=="surface" or r.niche.aquatic=="submerged" then
|
|
local stack={}; assert(d.on_place(stack,player,{})==stack and writes==0)
|
|
end
|
|
end
|
|
end)
|
|
test("harvest checks permission before metadata or inventory and returns at most two",function()
|
|
bush(); allowed=false; local ok,reason=B.harvest(p,player)
|
|
assert(not ok and reason=="denied" and auth==1 and writes==0 and added==0)
|
|
allowed=true; ok,reason=B.harvest(p,player)
|
|
assert(ok and reason==2 and added==2 and tonumber(fields[key(p)].botany_ready_at)==time+365.2425*86400)
|
|
end)
|
|
test("cooldown has no catchup and survives a new module instance",function()
|
|
bush(); assert(B.harvest(p,player)); assert(not B.harvest(p,player) and added==2)
|
|
policies={} -- a fresh factory may reinstall the same seasonal policy
|
|
B=factory(H,A); assert(not B.harvest(p,player)); time=time+86400*30
|
|
assert(not B.harvest(p,player) and added==2)
|
|
time=time+86400*400
|
|
assert(B.harvest(p,player) and added==4); assert(not B.harvest(p,player) and added==4)
|
|
end)
|
|
test("full inventory does not consume stock or start cooldown",function()
|
|
bush(); room=false; local ok,reason=B.harvest(p,player)
|
|
assert(not ok and reason=="inventory" and fields[key(p)].botany_ready_at=="" and added==0)
|
|
room=true; leftover=2; assert(not B.harvest(p,player) and fields[key(p)].botany_ready_at=="")
|
|
leftover=1; ok,reason=B.harvest(p,player); assert(ok and reason==1 and added==1)
|
|
assert(not B.harvest(p,player))
|
|
end)
|
|
test("harvest rejects wrong node, season, unloaded and nonplayer callers",function()
|
|
for _,id in ipairs({"elder","holly","foxglove","fly_agaric","berry_bush","bilberry_fake"}) do
|
|
bush(id); assert(not B.harvest(p,player) and added==0)
|
|
end
|
|
bush(); season="Winter"; assert(not B.harvest(p,player))
|
|
season="Summer"; world[key(p)]=nil; assert(not B.harvest(p,player))
|
|
assert(not B.harvest(p,nil) and not B.harvest(p,{is_player=function() return false end}))
|
|
assert(writes==0)
|
|
end)
|
|
test("malformed clocks and cooldown metadata fail closed",function()
|
|
bush()
|
|
for _,t in ipairs({0/0,math.huge,-math.huge,-1,"clock",9007199254740991}) do
|
|
time=t; local ok,reason=B.harvest(p,player); assert(not ok and reason=="clock")
|
|
end
|
|
time=nil; local ok,reason=B.harvest(p,player); assert(not ok and reason=="clock")
|
|
assert(climate_calls==0 and writes==0 and next(fields)==nil)
|
|
time=100000; A.get_meta(p):set_string("botany_ready_at","broken"); assert(not B.harvest(p,player))
|
|
A.get_meta(p):set_string("botany_ready_at","-2"); assert(not B.harvest(p,player))
|
|
assert(added==0)
|
|
end)
|
|
test("rightclick returns item stack and reentrant inventory cannot duplicate",function()
|
|
register(); bush(); local original=inv.add_item
|
|
inv.add_item=function(self,list,item)
|
|
assert(not B.harvest(p,player)); return original(self,list,item)
|
|
end
|
|
local stack={}; assert(defs["hearth:bilberry"].on_rightclick(p,{},player,stack)==stack)
|
|
inv.add_item=original; assert(added==2)
|
|
end)
|
|
test("the actual position selector can return each of all hundred exact node IDs",function()
|
|
for index,r in ipairs(B.list()) do
|
|
local h=habitat(r.id); h.include_trees=true
|
|
local x=0
|
|
while (x*17+7)%100~=index-1 do x=x+1; assert(x<100) end
|
|
local k,selected=B.choose({x=x,y=1,z=0},h,next(r.seasons))
|
|
assert(k==r.node:sub(8) and selected.id==r.id,r.id)
|
|
assert(B.suitable(k,h,next(r.seasons)))
|
|
end
|
|
assert(writes==0)
|
|
end)
|
|
test("submerged candidates work as separate air-cell stands, never as liquid node definitions",function()
|
|
register()
|
|
for _,id in ipairs({"water_starwort","hornwort"}) do
|
|
local r=B.get(id); local h=habitat(id)
|
|
assert(B.suitable(id,h,"Summer") and h.aquatic=="submerged" and h.water>=1)
|
|
local def=defs[r.node]
|
|
assert(def.groups.hearth_submerged==1 and not def.groups.water and not def.liquidtype)
|
|
assert(not def.buildable_to and not def.walkable and def.drawtype=="plantlike")
|
|
end
|
|
end)
|
|
test("all seventy-nine new understory policies match actual activity and dormant art",function()
|
|
local count=0
|
|
for _,r in ipairs(B.list()) do
|
|
local policy=policies[r.id]
|
|
if r.existing or r.group=="trees" then assert(not policy,r.id)
|
|
else
|
|
count=count+1; assert(policy,r.id)
|
|
for _,s in ipairs({"Spring","Summer","Autumn","Winter"}) do
|
|
assert((policy.active[s]==true)==B.active(r.id,s))
|
|
local v=policy.definitions[s]
|
|
assert((v~=nil)==not B.active(r.id,s),r.id..s)
|
|
if v then
|
|
assert(v.texture=="hearth_botany_"..r.id.."_dormant.png")
|
|
assert(B.kind(r.node.."_"..s:lower())==r.id)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
assert(count==79)
|
|
policies.holly.active.Winter=false; assert(B.active("holly","Winter"))
|
|
assert(next(policies.juniper.definitions)==nil and next(policies.holly.definitions)==nil)
|
|
end)
|
|
test("optional seasons mocks without add_variants still support base harvesting",function()
|
|
for _,mock in ipairs({{}, {kind=function(name) return name:match("^hearth:(.+)$") end}}) do
|
|
H.seasons=mock; B=factory(H,A); bush(); fields={}
|
|
assert(B.harvest(p,player))
|
|
end
|
|
H.seasons=nil; B=factory(H,A); fields={}; assert(B.harvest(p,player))
|
|
end)
|
|
test("harvest samples one validated epoch before climate and uses the same cooldown clock",function()
|
|
time=1782043200.75; bush()
|
|
H.climate_at=function(epoch)
|
|
climate_calls=climate_calls+1
|
|
assert(clock_reads==1 and epoch==1782043200)
|
|
return {season="Summer"}
|
|
end
|
|
assert(B.harvest(p,player) and clock_reads==1 and climate_calls==1)
|
|
assert(tonumber(fields[key(p)].botany_ready_at)==1782043200+365.2425*86400)
|
|
end)
|
|
test("only exact owned seasonal aliases harvest in an active fruit season",function()
|
|
bush("bilberry_winter"); season="Winter"
|
|
local ok,reason=B.harvest(p,player); assert(not ok and reason=="season" and writes==0)
|
|
-- A loaded appearance can lag the climate; harvesting never resets its stock.
|
|
season="Summer"; assert(B.harvest(p,player))
|
|
bush(); assert(not B.harvest(p,player) and added==2)
|
|
H.seasons.kind=function() return "bilberry" end
|
|
for _,id in ipairs({"bilberry_fake","bilberry_summer","bilberry_winter_extra"}) do
|
|
bush(id); ok,reason=B.harvest(p,player); assert(not ok and reason=="species")
|
|
end
|
|
world[key(p)]={name="foreign:bilberry"}; ok,reason=B.harvest(p,player)
|
|
assert(not ok and reason=="species" and added==2)
|
|
end)
|
|
test("southern berry seasons use the solar model at the validated epoch",function()
|
|
local model=dofile(target:gsub("[^/\\]+$","model.lua"))
|
|
H.climate_at=function(epoch) assert(epoch==time); return model.solar(epoch,-45,0) end
|
|
bush(); time=1782043200
|
|
local ok,reason=B.harvest(p,player); assert(not ok and reason=="season" and writes==0)
|
|
time=1797854400; assert(B.harvest(p,player) and added==2)
|
|
assert(tonumber(fields[key(p)].botany_ready_at)==time+365.2425*86400)
|
|
end)
|
|
test("shared annual fruit readiness rejects an animal debit and has no daily refill",function()
|
|
bush(); local m=A.get_meta(p)
|
|
m:set_string("botany_ready_at",tostring(time+365.2425*86400))
|
|
assert(not B.harvest(p,player) and added==0)
|
|
time=time+86400; assert(not B.harvest(p,player))
|
|
time=time+(365.2425-1)*86400; assert(B.harvest(p,player) and added==2)
|
|
end)
|
|
test("one unexpired pollination credit adds only one berry and is spent once",function()
|
|
bush(); local m=A.get_meta(p)
|
|
m:set_string("succ_pollinated_until",tostring(time+86400))
|
|
local ok,n=B.harvest(p,player); assert(ok and n==3 and added==3)
|
|
assert(m:get_string("succ_pollinated_until")=="0")
|
|
assert(not B.harvest(p,player) and added==3)
|
|
time=time+365.2425*86400; ok,n=B.harvest(p,player)
|
|
assert(ok and n==2 and added==5)
|
|
end)
|
|
test("expired pollination is not a bonus and invalid credits fail closed",function()
|
|
bush(); local m=A.get_meta(p)
|
|
for _,v in ipairs({"broken","-1",tostring(time+86401),"inf"}) do
|
|
m:set_string("succ_pollinated_until",v)
|
|
local before=writes; assert(not B.harvest(p,player) and added==0 and writes==before)
|
|
end
|
|
m:set_string("succ_pollinated_until",tostring(time))
|
|
local ok,n=B.harvest(p,player); assert(ok and n==2 and added==2)
|
|
end)
|
|
test("empty fruit cannot be manufactured by fresh pollination",function()
|
|
bush(); local m=A.get_meta(p)
|
|
m:set_string("botany_ready_at",tostring(time+86400))
|
|
m:set_string("succ_pollinated_until",tostring(time+86400))
|
|
local before=writes; assert(not B.harvest(p,player) and writes==before and added==0)
|
|
assert(tonumber(m:get_string("succ_pollinated_until"))==time+86400)
|
|
end)
|
|
test("inventory rejection restores fruit and credit; partial delivery spends both",function()
|
|
bush(); local m=A.get_meta(p); local pollen=tostring(time+86400)
|
|
m:set_string("succ_pollinated_until",pollen)
|
|
room=false; assert(not B.harvest(p,player))
|
|
assert(m:get_string("botany_ready_at")=="" and m:get_string("succ_pollinated_until")==pollen)
|
|
room=true; leftover=3; assert(not B.harvest(p,player))
|
|
assert(m:get_string("botany_ready_at")=="" and m:get_string("succ_pollinated_until")==pollen)
|
|
leftover=2; local ok,n=B.harvest(p,player); assert(ok and n==1 and added==1)
|
|
assert(m:get_string("succ_pollinated_until")=="0" and not B.harvest(p,player))
|
|
end)
|
|
test("both inventory callbacks observe reserved fruit and pollination credit",function()
|
|
bush(); local m=A.get_meta(p); m:set_string("succ_pollinated_until",tostring(time+86400))
|
|
local old_room,old_add=inv.room_for_item,inv.add_item
|
|
local function check()
|
|
assert(not B.harvest(p,player))
|
|
assert(m:get_string("succ_pollinated_until")=="0")
|
|
end
|
|
inv.room_for_item=function(...) check(); return old_room(...) end
|
|
inv.add_item=function(...) check(); return old_add(...) end
|
|
local ok,n=B.harvest(p,player)
|
|
inv.room_for_item,inv.add_item=old_room,old_add
|
|
assert(ok and n==3 and added==3)
|
|
end)
|
|
assert(passed==44,"Unexpected focused botany integration count")
|
|
print("BOTANY_SPEC_PASS "..passed)
|