verbs/phenology.js
66 lines · in natura-src-1.0.0.zip as natura/verbs/phenology.js · a module · its entry in the module reference · its test: verbs/phenology-test.js
// verbs/phenology.js -- ONE life ontology verb, one concern: the LIFE-STAGE GROWTH a living thing moves through over
// its lifetime. Pure and RENDER-FREE. Universal -- a seed swells to a sprout to a bud to an open bloom, a spat grows
// to an adult snail, a hypha spreads to a fruiting mould, a green fruit ripens, a nucleus grows to an impinged grain:
// growth ADVANCES while the thing is fed and lit, SLOWS as it nears full size, and life passes named STAGES at
// thresholds along the way. Prototyped in the "Under Glass" greenhouse piece (a watered, lit plant grows toward bloom
// then seeds) and already hand-rolled three times over in the rainforest (a mould's spread, a fruit's ripening, a
// flower's nectar) -- lifted OUT so every life system shares one growth law instead of re-writing `x += rate*dt` by
// hand. UMD (node / window.NaturaVerbs.phenology).
(function (root, factory) {
if (typeof module !== 'undefined' && module.exports) module.exports = factory();
else (root.NaturaVerbs = root.NaturaVerbs || {}).phenology = factory();
})(typeof self !== 'undefined' ? self : this, function () {
'use strict';
const nn = v => v < 0 ? 0 : v; // a gate is a non-negative multiplier: 0 = starved, 1 = normal, >1 = boosted
// grow(g, dt, opts) -> new growth in [0, target]. Advances a life-progress scalar, never past its target.
// opts = { rate:0.05 (progress per second at full vigour, gates = 1),
// target:1 (full size / ripeness),
// resource:1 (feeding gate, a non-negative MULTIPLIER: water, food, nectar supply, undercooling),
// light:1 (light gate, same shape: daylight for a phototroph; leave 1 for things that grow in the dark),
// taper:1 (fraction of target past which growth HALVES, so ripening slows near full size; 1 = no taper) }
// The two gates MULTIPLY the rate: a plant grows fastest watered AND lit, barely at all parched or in the dark.
// Monotone up (a gate can slow growth to nil but never reverse it -- decline/wilt is a different law, not this one).
function grow(g, dt, opts) {
opts = opts || {};
const rate = opts.rate != null ? opts.rate : 0.05;
const target = opts.target != null ? opts.target : 1;
const resource = opts.resource != null ? opts.resource : 1;
const light = opts.light != null ? opts.light : 1;
const taper = opts.taper != null ? opts.taper : 1;
const curve = opts.curve || 'linear';
g = g || 0;
// CURVE -- the SHAPE of the advance, not just the rate. Real growth is rarely a straight ramp:
// linear : a steady climb to the cap (the taper eases the last stretch). The default; back-compatible.
// logistic : the true biological S-curve -- a slow seed, a fast adolescence, a slow ripening (dg peaks mid).
// accelerating : compounding early growth (a colony/crystal that grows faster the bigger it gets).
const gn = target > 0 ? g / target : 0;
let shape;
if (curve === 'logistic') shape = 4 * Math.max(gn, 0.02) * (1 - gn); // self-slows at both ends
else if (curve === 'accelerating') shape = 0.12 + gn; // faster as it grows
else shape = (g < taper * target ? 1 : 0.5); // linear + taper (unchanged)
const eff = rate * nn(resource) * nn(light) * shape;
const ng = g + eff * dt;
return ng < target ? ng : target;
}
// stageOf(g, stages) -> the NAME of the current life stage. Life has thresholds; this reads which one you have passed.
// stages = [[name, threshold], ...] ascending, OR an object { name: threshold }. Returns the stage with the
// highest threshold that is still <= g; below every threshold, returns the earliest (lowest-threshold) stage.
// e.g. stageOf(0.7, {seed:0, sprout:0.08, bud:0.62, bloom:0.82}) -> 'bud'.
function stageOf(g, stages) {
g = g || 0;
const list = Array.isArray(stages) ? stages : Object.keys(stages).map(k => [k, stages[k]]);
if (!list.length) return null;
let name = null, best = -Infinity; // best passed stage
let earlyName = list[0][0], earlyTh = Infinity; // the lowest-threshold stage, for the below-all case
for (let i = 0; i < list.length; i++) {
const nm = list[i][0], th = list[i][1];
if (th < earlyTh) { earlyTh = th; earlyName = nm; }
if (g >= th && th > best) { best = th; name = nm; }
}
return name != null ? name : earlyName;
}
return { grow, stageOf };
});