← Natura·Docs/Modules/Source

rainforest-life.js

198 lines · in natura-src-1.0.0.zip as natura/rainforest-life.js · a module · its entry in the module reference · its test: rainforest-life-test.js

// rainforest-life.js -- the RAINFOREST biome's SPECIES, a render-free registry for the organisms life system.
//
// One concern: WHAT lives in a rainforest and HOW it behaves -- each species declaring its chronotype and a small
// behave() that uses the shared VERBS (activity is set for it by the system; it adds forage/flee/move). NO render
// here: state carries only data (a flower's hue is a number, a mould's spread a scalar); the renderer maps species +
// state to bodies. Deterministic: behaviours draw randomness from the world's seeded rng (api.ctx.rng), never
// Math.random, so a run reproduces. Reusable by any rainforest piece. UMD (node / window.RainforestLife).
(function (root, factory) {
  if (typeof module !== 'undefined' && module.exports) module.exports = factory();
  else root.RainforestLife = factory();
})(typeof self !== 'undefined' ? self : this, function () {
  'use strict';
  const clamp = (v, a, b) => v < a ? a : v > b ? b : v;
  const rndOf = api => (api.ctx && api.ctx.rng) ? api.ctx.rng : Math.random;   // seeded when run in a World

  // MORPHO butterfly: diurnal. Flees a hunting bird (fear beats hunger), else forages the flowers' nectar, else
  // drifts toward a light shaft. Folds to a perch when its layer's day ends.
  function morphoBehave(e, api) {
    const dt = api.dt, V = api.Verbs, P = e.pos, Vel = e.vel, rnd = rndOf(api), s = e.state;
    s.fed = Math.max(0, (s.fed || 0) - dt * 0.045);
    if (s.rest < 0.5) {
      e.ph += dt * (7 + s.fly * 3); s.feeding = 0;
      const threats = api.of('hunter').map(h => ({ x: h.pos[0], y: h.pos[1] }));
      const fl = V.flee({ x: P[0], y: P[1] }, threats, { near: 0.10, perceive: 0.20 });
      let vmax = 0.055;
      if (fl.fleeing) { Vel[0] += fl.fx * 0.16 * dt; Vel[1] += fl.fy * 0.12 * dt; vmax = 0.12; }
      else {
        let tx = null, ty = null;
        const nectar = api.of('flower').filter(f => f.state.active > 0.4 && f.state.nectar > 0.08).map(f => ({ x: f.pos[0], y: f.pos[1], amount: f.state.nectar, ref: f }));
        if (s.fed < 0.7 && nectar.length) { const fr = V.forage({ x: P[0], y: P[1] }, nectar, { reach: 0.03, min: 0.08 });
          if (fr.target) { tx = fr.target.x; ty = fr.target.y; if (fr.atSource) { s.feeding = 1; s.fed = Math.min(1, s.fed + dt * 0.6); fr.target.ref.state.nectar = Math.max(0, fr.target.ref.state.nectar - dt * 0.35); } } }
        if (tx == null) { const gaps = (api.struct && api.struct.gaps) || []; let gx = P[0], best = 9; for (const g of gaps) { const d = Math.abs(g - P[0]); if (d < best) { best = d; gx = g; } } tx = gaps.length ? gx : P[0]; ty = clamp(P[1] + Math.sin(e.ph * 0.5) * 0.12, 0.26, 0.64); }
        if (s.feeding) { Vel[0] *= (1 - 2.2 * dt); Vel[1] *= (1 - 2.2 * dt); }
        else { Vel[0] += (Math.sign(tx - P[0]) * 0.03 + (rnd() - 0.5) * 0.05) * dt; Vel[1] += (Math.sign(ty - P[1]) * 0.022 + (rnd() - 0.5) * 0.03) * dt; }
      }
      Vel[0] = clamp(Vel[0] * (1 - 0.5 * dt), -vmax, vmax); Vel[1] = clamp(Vel[1] * (1 - 0.5 * dt), -vmax * 0.75, vmax * 0.75);
      P[0] += Vel[0] * dt; if (P[0] < 0.03) { P[0] = 0.03; Vel[0] = Math.abs(Vel[0]) * 0.6; } if (P[0] > 0.97) { P[0] = 0.97; Vel[0] = -Math.abs(Vel[0]) * 0.6; }
      P[1] = clamp(P[1] + Vel[1] * dt, 0.24, 0.66);
    } else { P[1] += (s.land - P[1]) * (1 - Math.pow(0.06, dt)); e.ph += dt * 0.6; s.feeding = 0; }
  }

  // BAT: nocturnal. Hangs from its roost by day; flits the understory on a jinking path at night.
  function batBehave(e, api) {
    const dt = api.dt, P = e.pos, Vel = e.vel, rnd = rndOf(api), s = e.state;
    e.ph += dt * (s.rest < 0.5 ? 16 : 2.2);
    if (s.rest > 0.55) { P[0] += (s.hx - P[0]) * (1 - Math.pow(0.05, dt)); P[1] += (s.hy - P[1]) * (1 - Math.pow(0.05, dt)); }
    else {
      if (rnd() < dt * 1.4 || (Math.abs(P[0] - s.tx) < 0.04 && Math.abs(P[1] - s.ty) < 0.04)) { s.tx = 0.08 + rnd() * 0.84; s.ty = 0.40 + rnd() * 0.28; }
      Vel[0] += (Math.sign(s.tx - P[0]) * 0.55 + (rnd() - 0.5) * 1.3) * dt; Vel[1] += (Math.sign(s.ty - P[1]) * 0.4 + (rnd() - 0.5) * 0.9) * dt;
      Vel[0] = clamp(Vel[0] * (1 - 1.6 * dt), -0.5, 0.5); Vel[1] = clamp(Vel[1] * (1 - 1.6 * dt), -0.4, 0.4);
      P[0] = clamp(P[0] + Vel[0] * dt, 0.05, 0.95); P[1] = clamp(P[1] + Vel[1] * dt, 0.34, 0.70);
    }
  }

  // SNAIL: sessile-night mollusc. Tucked in its shell by day (out=0), crawls out in the dark.
  function snailBehave(e, api) {
    const dt = api.dt, P = e.pos, s = e.state; s.out = s.active;
    if (s.out > 0.4) { P[0] += s.dir * s.spd * s.out * dt; if (P[0] < 0.20) { P[0] = 0.20; s.dir = 1; } if (P[0] > 0.80) { P[0] = 0.80; s.dir = -1; } }
  }

  // HUNTER (an insectivorous bird): diurnal. Forages the morphos as prey (moving toward the best one); a catch sends
  // that morpho darting elsewhere and leaves the bird sated (digesting) a while.
  function hunterBehave(e, api) {
    const dt = api.dt, V = api.Verbs, P = e.pos, Vel = e.vel, rnd = rndOf(api), s = e.state;
    s.sated = Math.max(0, (s.sated || 0) - dt); e.ph += dt * (s.rest < 0.5 ? 18 : 3);
    if (s.rest < 0.5 && s.sated <= 0) {
      const prey = api.of('morpho').filter(m => m.state.rest < 0.5).map(m => ({ x: m.pos[0], y: m.pos[1], amount: 1, ref: m }));
      const fr = V.forage({ x: P[0], y: P[1] }, prey, { reach: 0.02, min: 0.5 });
      if (fr.target) { Vel[0] += Math.sign(fr.target.x - P[0]) * 0.10 * dt; Vel[1] += Math.sign(fr.target.y - P[1]) * 0.08 * dt;
        if (fr.atSource) { const m = fr.target.ref; m.pos[0] = 0.08 + rnd() * 0.84; m.pos[1] = 0.28 + rnd() * 0.34; m.vel[0] = 0; m.vel[1] = 0; s.sated = 2.5 + rnd() * 2.2; } }
    } else if (s.rest > 0.55) { Vel[0] *= (1 - 2 * dt); Vel[1] *= (1 - 2 * dt); }
    else { Vel[0] += (rnd() - 0.5) * 0.06 * dt; Vel[1] += (rnd() - 0.5) * 0.04 * dt; }
    Vel[0] = clamp(Vel[0] * (1 - 0.8 * dt), -0.10, 0.10); Vel[1] = clamp(Vel[1] * (1 - 0.8 * dt), -0.08, 0.08);
    P[0] = clamp(P[0] + Vel[0] * dt, 0.04, 0.96); P[1] = clamp(P[1] + Vel[1] * dt, 0.26, 0.62);
  }

  // MONKEY: diurnal, arboreal. A canopy troop -- flees the cat (bolting away AND up), else forages ripe fruit, else
  // ambles between perches; curls on its branch at night.
  function monkeyBehave(e, api) {
    const dt = api.dt, V = api.Verbs, P = e.pos, Vel = e.vel, rnd = rndOf(api), s = e.state;
    s.fed = Math.max(0, (s.fed || 0) - dt * 0.03);
    if (s.rest < 0.5) {
      e.ph += dt * 4;
      const cats = api.of('cat').map(c => ({ x: c.pos[0], y: c.pos[1] }));
      const fl = V.flee({ x: P[0], y: P[1] }, cats, { near: 0.14, perceive: 0.30 });
      if (fl.fleeing) { Vel[0] += fl.fx * 0.05 * dt; Vel[1] += (fl.fy - 0.4) * 0.045 * dt; }   // bolt away, and UP into the canopy
      else {
        let tx = null, ty = null;
        const fruit = api.of('fruit').filter(f => f.state.ripe > 0.15).map(f => ({ x: f.pos[0], y: f.pos[1], amount: f.state.ripe, ref: f }));
        if (s.fed < 0.7 && fruit.length) { const fr = V.forage({ x: P[0], y: P[1] }, fruit, { reach: 0.03, min: 0.12 });
          if (fr.target) { tx = fr.target.x; ty = fr.target.y; if (fr.atSource) { s.fed = Math.min(1, s.fed + dt * 0.5); fr.target.ref.state.ripe = Math.max(0, fr.target.ref.state.ripe - dt * 0.3); } } }
        if (tx == null) { if (rnd() < dt * 0.3 || Math.abs(P[0] - s.tx) < 0.03) s.tx = 0.10 + rnd() * 0.80; tx = s.tx; ty = clamp(s.hy + Math.sin(e.ph * 0.3) * 0.03, 0.16, 0.34); }
        Vel[0] += Math.sign(tx - P[0]) * 0.022 * dt; Vel[1] += Math.sign(ty - P[1]) * 0.02 * dt;
      }
      Vel[0] = clamp(Vel[0] * (1 - 0.6 * dt), -0.05, 0.05); Vel[1] = clamp(Vel[1] * (1 - 0.6 * dt), -0.04, 0.04);
      P[0] = clamp(P[0] + Vel[0] * dt, 0.04, 0.96); P[1] = clamp(P[1] + Vel[1] * dt, 0.14, 0.40);
    } else { P[1] += (s.hy - P[1]) * (1 - Math.pow(0.05, dt)); P[0] += (s.hx - P[0]) * (1 - Math.pow(0.04, dt)); e.ph += dt * 0.5; }
  }

  // JUNGLE CAT (ocelot): crepuscular, prowls the understory hunting the troop -- forages the monkeys as prey; a catch
  // sends that monkey bolting to a far perch and leaves the cat sated (digesting). The monkeys' height keeps it lean.
  function catBehave(e, api) {
    const dt = api.dt, V = api.Verbs, P = e.pos, Vel = e.vel, rnd = rndOf(api), s = e.state;
    s.sated = Math.max(0, (s.sated || 0) - dt); e.ph += dt * 2;
    if (s.rest < 0.5 && s.sated <= 0) {
      const prey = api.of('monkey').filter(m => m.state.rest < 0.6).map(m => ({ x: m.pos[0], y: m.pos[1], amount: 1, ref: m }));
      const fr = V.forage({ x: P[0], y: P[1] }, prey, { reach: 0.025, min: 0.5 });
      if (fr.target) { Vel[0] += Math.sign(fr.target.x - P[0]) * 0.05 * dt; Vel[1] += Math.sign(fr.target.y - P[1]) * 0.045 * dt;
        if (fr.atSource) { const m = fr.target.ref; m.pos[0] = 0.08 + rnd() * 0.84; m.pos[1] = 0.16 + rnd() * 0.10; m.vel[0] = 0; m.vel[1] = 0; s.sated = 4 + rnd() * 3; } }
      else { if (rnd() < dt * 0.4) s.tx = 0.10 + rnd() * 0.80; Vel[0] += Math.sign((s.tx || 0.5) - P[0]) * 0.02 * dt; }
    } else { Vel[0] *= (1 - 1.5 * dt); Vel[1] *= (1 - 1.5 * dt); }
    Vel[0] = clamp(Vel[0] * (1 - 0.7 * dt), -0.05, 0.05); Vel[1] = clamp(Vel[1] * (1 - 0.7 * dt), -0.04, 0.04);
    P[0] = clamp(P[0] + Vel[0] * dt, 0.04, 0.96); P[1] = clamp(P[1] + Vel[1] * dt, 0.36, 0.60);
  }

  // TREE FROG: nocturnal, on the understory leaves. Clings still through the day; at night it hops between leaves and
  // CALLS -- its throat sac swelling (state.call), the visible half of the frog-chorus the soundscape already sings.
  function frogBehave(e, api) {
    const dt = api.dt, P = e.pos, rnd = rndOf(api), s = e.state;
    if (s.rest < 0.5) { e.ph += dt * 3; s.call = 0.5 + 0.5 * Math.sin(e.ph * 1.5 + e.id);
      if (rnd() < dt * 0.4) { s.tx = clamp(s.hx + (rnd() - 0.5) * 0.12, 0.05, 0.95); s.ty = clamp(s.hy + (rnd() - 0.5) * 0.06, 0.42, 0.62); }
      P[0] += (s.tx - P[0]) * (1 - Math.pow(0.1, dt)); P[1] += (s.ty - P[1]) * (1 - Math.pow(0.1, dt)); }
    else { s.call = 0; P[0] += (s.hx - P[0]) * (1 - Math.pow(0.05, dt)); P[1] += (s.hy - P[1]) * (1 - Math.pow(0.05, dt)); }   // day: cling to the home leaf, still
  }

  // SLOTH: cathemeral -- never fully asleep, never fully busy. Hangs from the canopy and creeps along the branch at a
  // glacial pace, the one thing in the scene that keeps no hour at all, only its own slow tempo.
  function slothBehave(e, api) {
    const dt = api.dt, P = e.pos, rnd = rndOf(api), s = e.state;
    e.ph += dt * 0.35;
    if (rnd() < dt * 0.05) s.tx = 0.12 + rnd() * 0.76;                 // pick a new destination, very rarely
    P[0] = clamp(P[0] + Math.sign((s.tx != null ? s.tx : P[0]) - P[0]) * 0.005 * dt, 0.08, 0.92);   // glacial amble
  }

  // ARBOREAL SNAKE (a tree boa): nocturnal AMBUSH predator, the still counterpart to the ocelot's prowl. Draped
  // coiled on an understory branch, near-motionless; by night it stirs, and when a morpho or frog drifts within
  // reach it STRIKES (a brief lunge that sends the prey bolting), then coils back. It does not chase -- its
  // stillness IS its whole hunt. Wears the day/night verb (rest) like everything else; a new body, the same rule.
  function snakeBehave(e, api) {
    const dt = api.dt, P = e.pos, s = e.state;
    e.ph += dt * (s.rest < 0.5 ? 1.1 : 0.28);                              // a slow sway, a little livelier at night
    s.strike = Math.max(0, (s.strike || 0) - dt * 2.4);                    // a strike is brief
    P[0] += (s.hx - P[0]) * (1 - Math.pow(0.25, dt)); P[1] += (s.hy - P[1]) * (1 - Math.pow(0.25, dt));   // hold the coil (an ambusher stays put)
    if (s.rest < 0.5 && s.strike <= 0) {                                   // by night, watch for prey within reach and STRIKE the nearest
      const near = api.of('morpho').concat(api.of('frog'));
      let best = null, bd = 0.055;
      for (const m of near) { const d = Math.hypot(m.pos[0] - P[0], m.pos[1] - P[1]); if (d < bd) { bd = d; best = m; } }
      if (best) { s.strike = 1; s.sdir = Math.sign(best.pos[0] - P[0]) || 1;
        best.vel = best.vel || [0, 0]; best.vel[0] += s.sdir * 0.14; best.vel[1] -= 0.06; if (best.state) best.state.rest = 0; }   // the prey bolts away
    }
  }

  // the SPECIES REGISTRY (data + render-free behave). Pass to makeOrganisms({ species: RainforestLife.SPECIES,
  // structure: 'canopy' }). 'hue' is a colour INDEX (a number) the renderer maps; 'grow' a scalar the renderer draws.
  const SPECIES = {
    flower: { chronotype: 'phototroph', count: 12, band: [0.44, 0.68],
      spawn: (e, r) => { e.pos[0] = 0.07 + r() * 0.86; e.state.hue = (r() * 5) | 0; e.state.nectar = 0.5 + r() * 0.5; e.state.vigor = 0.7; },
      behave: (e, api) => { const m = (api.ctx.fields && api.ctx.fields.weather) ? api.ctx.fields.weather.moisture : 0.6;
        e.state.vigor += (m - e.state.vigor) * (1 - Math.pow(0.3, api.dt));                                       // blooms fatten in the wet, wilt in drought
        if (e.state.active > 0.35) e.state.nectar = api.Verbs.grow(e.state.nectar, api.dt, { rate: 0.03, light: e.state.active, resource: 0.4 + 0.9 * m }); } },   // nectar builds (light + moisture gated) -- the phenology GROW verb
    mould: { chronotype: 'sessile-night', count: 1, band: [0.893, 0.893],
      spawn: (e) => { e.pos[0] = 0.5; e.pos[1] = 0.893; e.state.grow = 0.12; },
      behave: (e, api) => { e.state.grow = api.Verbs.grow(e.state.grow, api.dt, { rate: 0.03, light: e.state.active }); } },   // the colony spreads while lit -- the phenology GROW verb
    snail: { chronotype: 'sessile-night', count: 4, band: [0.882, 0.90],
      spawn: (e, r) => { e.pos[0] = 0.24 + r() * 0.52; e.state.dir = r() < 0.5 ? -1 : 1; e.state.spd = 0.010 + r() * 0.008; e.state.out = 0; },
      behave: snailBehave },
    morpho: { chronotype: 'diurnal', count: 8, band: [0.30, 0.64],
      spawn: (e, r) => { e.pos[0] = 0.08 + r() * 0.84; e.state.fly = 0.7 + r() * 0.6; e.state.fed = 0.5 + r() * 0.4; e.state.feeding = 0; e.state.land = 0.42 + r() * 0.26; },
      behave: morphoBehave },
    bat: { chronotype: 'nocturnal', count: 5, band: [0.34, 0.46],
      spawn: (e) => { e.state.hx = e.pos[0]; e.state.hy = e.pos[1]; e.state.tx = e.pos[0]; e.state.ty = e.pos[1]; },
      behave: batBehave },
    hunter: { chronotype: 'diurnal', count: 2, band: [0.30, 0.50],
      spawn: (e, r) => { e.pos[0] = 0.3 + r() * 0.4; e.state.sated = 0; },
      behave: hunterBehave },
    fruit: { chronotype: 'phototroph', count: 10, band: [0.15, 0.32],
      spawn: (e, r) => { e.pos[0] = 0.07 + r() * 0.86; e.state.ripe = 0.3 + r() * 0.5; e.state.hue = (r() * 3) | 0; },
      behave: (e, api) => { const m = (api.ctx.fields && api.ctx.fields.weather) ? api.ctx.fields.weather.moisture : 0.6;
        if (e.state.active > 0.3) e.state.ripe = api.Verbs.grow(e.state.ripe, api.dt, { rate: 0.02, light: e.state.active, resource: 0.4 + 0.9 * m }); } },   // fruit ripens (light + moisture gated) -- the phenology GROW verb
    monkey: { chronotype: 'diurnal', count: 4, band: [0.16, 0.34],
      spawn: (e, r) => { e.pos[0] = 0.15 + r() * 0.7; e.state.fed = 0.6; e.state.hx = e.pos[0]; e.state.hy = e.pos[1]; e.state.tx = e.pos[0]; },
      behave: monkeyBehave },
    cat: { chronotype: 'crepuscular', count: 1, band: [0.40, 0.56],
      spawn: (e, r) => { e.pos[0] = 0.3 + r() * 0.4; e.state.sated = 0; e.state.tx = e.pos[0]; },
      behave: catBehave },
    frog: { chronotype: 'nocturnal', count: 4, band: [0.44, 0.60],
      spawn: (e, r) => { e.pos[0] = 0.1 + r() * 0.8; e.state.hx = e.pos[0]; e.state.hy = e.pos[1]; e.state.tx = e.pos[0]; e.state.ty = e.pos[1]; e.state.call = 0; },
      behave: frogBehave },
    sloth: { chronotype: 'cathemeral', count: 1, band: [0.16, 0.30],
      spawn: (e, r) => { e.pos[0] = 0.2 + r() * 0.6; e.state.tx = e.pos[0]; },
      behave: slothBehave },
    snake: { chronotype: 'nocturnal', count: 2, band: [0.42, 0.54],
      spawn: (e, r) => { e.pos[0] = 0.18 + r() * 0.64; e.state.hx = e.pos[0]; e.state.hy = e.pos[1]; e.state.strike = 0; e.state.sdir = 1; },
      behave: snakeBehave }
  };

  return { SPECIES, morphoBehave, batBehave, snailBehave, hunterBehave, monkeyBehave, catBehave, frogBehave, slothBehave, snakeBehave };
});