← Natura·Docs/Modules/Source

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 };
});