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