Files
last-hearth/mods/hearth/species_effects.lua
T
2026-11-05 09:57:11 -05:00

122 lines
8.9 KiB
Lua
Executable File

-- Reviewed policy, not executable Wikipedia prose. No network, scheduler or map writes.
return function(hearth, api)
local E = {}
local B = assert(hearth.botany, "species effects require botany")
local articles=hearth.wiki_articles
if not articles and api.get_modpath then
articles=dofile(api.get_modpath("hearth").."/wiki_articles.lua")
end
local function copy(t)
if type(t)~="table" then return t end
local out={}; for k,v in pairs(t) do out[k]=copy(v) end; return out
end
local function finite(n) return type(n)=="number" and n==n and math.abs(n)<math.huge end
local function count(n,cap) return finite(n) and math.min(cap,math.max(0,math.floor(n))) or 0 end
local wiki="https://en.wikipedia.org/wiki/"
-- Original summaries of pages reviewed 2026-11-03. Numbers below are game
-- approximations, NOT measurements reported by these articles.
local reviewed={
alder={article="Alnus_glutinosa",note="Ecology: Frankia root nodules fix nitrogen and improve soil fertility."},
clover={article="Trifolium_repens",note="Cultivation and uses / Forage: symbiotic root-nodule nitrogen fixation."},
dog_lichen={article="Peltigera_canina",related="Peltigera",
note="Species Description identifies Nostoc; genus Description explains nitrogen fixation. Genus-level mechanism, not a measured species rate."},
sphagnum={article="Sphagnum_palustre",related="Sphagnum",
note="Species introduction describes water absorption; genus introduction explains cation exchange and acidification. Acidification is genus-level evidence."},
duckweed={article="Lemna_minor",related="Lemnoideae",
note="Species Cultivation describes floating carpets; subfamily Duckweed in natural environments describes shade. Shade is a group-level mechanism."},
birch_polypore={article="Fomitopsis_betulina",
note="Distribution, habitat and ecology: brown rot of birch wood; living-tree parasitism is not simulated."},
fly_agaric={article="Amanita_muscaria",
note="Distribution and ecology: ectomycorrhizal tree associations. Game host list is deliberately narrower; carbon and water exchange are not simulated."},
}
local function record(value)
return B.get(type(value)=="table" and value.id or value)
end
-- Identity references cover all 100 records. An identity link is not evidence
-- that every tagged trait has been reviewed or implemented.
function E.source(value)
local r=record(value); if not r then return nil end
local evidence=reviewed[r.id]
local snapshot=hearth.wiki_articles or articles
local article=type(snapshot)=="table" and snapshot[r.id]
return {source_species=r.latin,source_url=evidence and wiki..evidence.article
or (article and article.url) or wiki..r.latin:gsub(" ","_"),
reviewed=evidence~=nil,reviewed_on=evidence and "2026-11-03" or nil,
note=evidence and evidence.note or "Identity reference only; no species-effect evidence reviewed here.",
evidence_url=evidence and wiki..(evidence.related or evidence.article) or nil,
-- Snapshot revision is identity provenance; the intro may omit the
-- reviewed body section. Never present it as a pinned effect claim.
identity_revision=article and article.revision or nil,
identity_revision_url=article and article.revision_url or nil}
end
function E.sources()
local out={}; for _,r in ipairs(B.list()) do out[r.id]=E.source(r) end; return out
end
local tags={"nitrogenfixer","browse","pollinator","ectomycorrhiza","saprotroph","parasite","engineer","nursery","allelopathy","resin"}
local nitrogen="After 90 real calendar days of root age, in warm active conditions: at most 1 nutrient per eligible evaluation, 2 total per support via succ_fixed_nitrogen shared across species/restarts; stops at fertility 4 (soil cap 8). Timing is game policy, not a sourced fixation rate."
local descriptions={
browse="Active natural plants count as wildlife forage; grazing debits a shared partial-canopy ration with a seven-real-day rest, not the perennial root. Not human food permission.",
pollinator="Mature natural plants in their flowering season support pollination. Nectar visits reserve a shared one-hour nectar deadline and at most one pollination credit expiring after one real day. Human harvesting spends that credit for at most one extra berry from an existing fruit crop; it never resets annual fruit readiness or creates seeds.",
nursery="Each active natural non-component plant adds nursery cover; any cover reduces succession stress accumulation to 75%. No extra animal spawn is promised.",
parasite="Not simulated: no infection or attack on living trees or player plants.",
allelopathy="Not simulated: no chemical suppression or plant killing.",
resin="Not simulated by species effects: no automatic resin production or extraction.",
}
function E.describe(value)
local r=record(value); if not r then return {} end
local out={}
local function add(title,status,game)
local row=E.source(r); row.title,row.status,row.game=title,status,game
out[#out+1]=row
end
for _,tag in ipairs(tags) do if r.tags[tag] then
local status,game="descriptive",descriptions[tag]
if tag=="nitrogenfixer" then status,game="simulated",nitrogen
elseif tag=="browse" or tag=="pollinator" or tag=="nursery" then status="simulated"
elseif tag=="ectomycorrhiza" then
if r.group=="fungi" then
status,game="simulated","Active compatible nearby fungus permits a finite nutrient exchange: successful calendar-eligible seedling growth debits at most 1 existing soil nutrient once, regardless of fungus count. No accelerated growth, nutrient creation or carbon/water ledger; lifecycle age thresholds are unchanged."
else game="Not simulated as a tree aura: only an active compatible nearby fungus can enable the finite seedling nutrient debit." end
elseif tag=="saprotroph" then
status,game="simulated","On suitable natural deadwood/litter only: add at most 60 seconds per evaluation to its finite decay bonus, capped at one mean real year for deadwood or 90 real days for litter. Only substrate decay pays its remaining soil nutrients once; the fungus pays none. Soil-growing saprotrophs receive no special nutrient effect."
elseif tag=="engineer" then
if r.id=="sphagnum" then
status,game="simulated","Local land habitat estimate: +0.10 moisture and -0.20 pH per mat, at most 2 mats; moisture clamped 0..1, pH 3.5..8.5. No terrain, water or persistent pH writes."
elseif r.id=="duckweed" then
status,game="simulated","Active floating mats reduce nearby submerged freshwater habitat light by 1.5 each, at most 4 mats / 6 light; never brighten an already-dark site and never lower it below 2. Source-water nodes remain intact."
else game="Not simulated: no species-specific erosion, bank stabilization, rock weathering or hydrology effect for this engineer tag." end
end
add(tag,status,game or "Not simulated: natural-history descriptor only.")
end end
if r.group=="trees" then
add("canopy","simulated","Trunks/crowns reduce local habitat light and contribute moisture through existing canopy sampling; pine/spruce retain full Winter influence, deciduous foliage retains 15%. Not a hydrological solver.")
end
local profile=hearth.lifecycle and hearth.lifecycle.plant(r.id)
add("lifecycle","simulated", "Calendar ages use real elapsed days, including unloaded time; reconciliation is bounded and loaded-only, with no missed-event replay. "
.. (profile and "Profile: "..profile.confidence..". Each timing field is separately source_reported, policy_fallback or UNKNOWN; an identity URL is not timing evidence."
or "Lifecycle profile UNKNOWN in this runtime; no invented maturity or lifespan.")
.. " Lifespan endpoints never authorize automatic death; fungal/lichen unknown maturity is not propagation permission.")
if r.toxic then add("toxicity","descriptive","Not simulated as an attack: this warning never grants a poison aura or human food permission.") end
return out
end
-- Pure derivation from ONE base sample. Callers must supply fresh baseline h
-- on each observation, never feed a derived result back as a new baseline.
function E.habitat(h,observations)
local out=copy(h)
if type(h)~="table" or h.loaded~=true then return out end
local seen=type(observations)=="table" and observations or h.species_counts or {}
out.source_moisture,out.source_ph,out.source_light=h.moisture,h.ph,h.light
if (h.water or 0)==0 and (h.aquatic==nil or h.aquatic=="land" or h.aquatic=="bank") then
local mats=count(seen.sphagnum,2)
if mats>0 then
if finite(h.moisture) then out.moisture=math.max(0,math.min(1,h.moisture+mats*0.1)) end
if finite(h.ph) then out.ph=math.max(3.5,math.min(8.5,h.ph-mats*0.2)) end
end
elseif h.freshwater==true and h.aquatic=="submerged" and finite(h.light) then
out.light=h.light-math.min(count(seen.duckweed,4)*1.5,math.max(0,h.light-2))
end
return out
end
return E
end