0xffd6563c…95fesent to0x32a5ece9…9d43·#25,427,629·view on Etherscan
return {tip:pos, dir}; }
function drawBud(bcx,bcy,R,rs){ for(let gx=-R;gx<=R;gx+=GRID) for(let gy=-R*1.3;gy<=R*1.3;gy+=GRID){ if((gx*gx)/(R*R)+(gy*gy)/(R*1.3*R*1.3)>1) continue;
const edge=(gx*gx)/(R*R)+(gy*gy)/(R*1.3*R*1.3)>0.62; C.push(bcx+gx,bcy+gy,72, dirSym(0.25,1), shade(FC(mix(P.bud, P.leafDark, edge?0.4:0.1)),rs),'leaves'); }
for(let k=0;k<7;k++){ const a=ra()*TAU; C.push(bcx+Math.cos(a)*R*1.15*0.7, bcy+Math.sin(a)*R*1.4, 71, '/', shade(leafD,rs),'leaves'); } }
function drawBloom(bcx,bcy,R,open,faceA,rs){ const Rp=R*(0.5+0.5*open), off=ra()*TAU;
for(let k=0;k<4;k++){ const bk=off+k*TAU/4, ax=Math.cos(bk), ay=Math.sin(bk), px=-ay, py=ax, du=GRID/Math.max(Rp,1);
for(let u=0.12;u<=1;u+=du){ const flare=Math.pow(Math.sin(Math.PI*Math.min(u,0.999)),0.72), rag=1 - D.petalRag*Math.abs(Math.sin(u*7+bk*3)), halfW=0.62*Rp*flare*rag, cxp=bcx+ax*u*Rp, cyp=bcy+ay*u*Rp;
for(let w=-halfW;w<=halfW;w+=GRID){ const edge=Math.abs(w)>halfW-GRID*1.4;
const col = u<0.30 ? FC(P.blotch) : (edge? FC(P.petalEdge) : (w*ax+0>0?petalC:petalWarm));
C.push(cxp+px*w, cyp+py*w, 68, dirSym(ax,ay), shade(mix(col,FC(P.stemDry),browny*0.6),rs), 'flowers'); } } }
const Rc=R*0.26; for(let gx=-Rc;gx<=Rc;gx+=GRID) for(let gy=-Rc;gy<=Rc;gy+=GRID) if(Math.hypot(gx,gy)<Rc) C.push(bcx+gx,bcy+gy,90, ra()<0.4?'.':'+', shade(FC(P.center),rs),'flowers');
for(let k=0;k<13;k++){ const a=k/13*TAU; C.push(bcx+Math.cos(a)*R*0.36, bcy+Math.sin(a)*R*0.36, 91, '.', shade(FC(P.stamen),rs),'flowers'); } }
function drawCapsule(bcx,bcy,R,rs){ const Hc=R*1.45, dry=Math.max(sen,browny*0.6), cc=mix(capC,FC(P.capsuleDry),dry);
for(let gy=-Hc*0.5;gy<=Hc*0.55;gy+=GRID){ const u=(gy+Hc*0.5)/Hc, halfW=R*Math.sin(Math.PI*clamp(u*0.9+0.08,0,1));
for(let gx=-halfW;gx<=halfW;gx+=GRID){ const edge=Math.abs(gx)>halfW-GRID*1.2; C.push(bcx+gx,bcy+gy,86,'|', shade(edge?mix(cc,'#000000',0.18):cc,rs),'flowers'); } }
for(let k=0;k<11;k++){ const a=Math.PI + (k/10-0.5)*Math.PI; C.push(bcx+Math.cos(a)*R*0.95, bcy-Hc*0.5+Math.sin(a)*R*0.95*0.32, 88, '-', shade(mix(cc,'#000000',0.22),rs),'flowers'); }
for(let k=0;k<7;k++) C.push(bcx+(rs()*2-1)*R*0.7, bcy-Hc*0.36, 89, '.', shade(FC(P.pore),rs),'flowers'); }
for(let i=0;i<nStem;i++){ const rs=rng('stem',seed,i), emerge=i===nStem-1?clamp(nStemF-(nStem-1),0.02,1):1, grow=smooth(0,1,emerge);
let phase=clamp(0.12 + m*0.66 - i*0.12 + (rs()*2-1)*0.05, 0, 1.25); phase=clamp(phase + sen*0.6, 0, 1.3);
const sx=rootX + (i-(nStem-1)/2)*D.spread*0.62*S + (rs()*2-1)*8, hLen=((axisH+50*S) + (160+300*m)*D.stature*(0.80+0.30*rs())*(0.85+0.15*v)*S)*grow;
const seasonVig=0.6+0.4*seasonBloom, canBloom=rewards*(1-sen)*seasonVig;
const isBud=phase<0.44, isBloom=!isBud && phase<0.74 && canBloom>0.3, nod=isBud ? clamp(1 - smooth(0.16,0.44,phase),0,1) : 0;
const {tip}=growStem(sx, hLen, nod, rs), tx=tip[0], ty=tip[1];
if(isBud){ drawBud(tx,ty,(8+8*m)*emerge*(0.7+0.3*v)*S,rs); }
else if(isBloom){ const open=smooth(0.44,0.62,phase)*(0.6+0.4*canBloom); drawBloom(tx,ty,(30+34*m)*emerge*(0.72+0.28*v)*(0.6+0.4*open)*S,open,UP,rs); }
else { drawCapsule(tx,ty,(13+15*m)*emerge*(0.78+0.22*v)*S,rs); }
}
}
// ===================== SETTING: Solo / Pair / Drift / Meadow / rare Field carpet =====================
const roll=rng('setting',st.seed)();
const nC = roll<0.16?0 : roll<0.44?1 : roll<0.72?2 : roll<0.91?3+Math.floor(rng('fn',st.seed)()*2) : 8+Math.floor(rng('fn',st.seed)()*6);
const field=nC>=6, comps=[];
for(let i=0;i<nC;i++){ const cseed='0x'+(hashStr(st.seed+'|c'+i)>>>0).toString(16), rc=rng('comp',st.seed,i);
let scale,off,raise,fade,depth;
if(field){ const slot=(i+0.5)/nC, xr=clamp(slot+(rc()*2-1)*(0.6/nC),0.03,0.97); depth=0.15+0.85*rc();
scale=lerp(0.6,0.3,depth)*(0.9+0.2*rc()); raise=(depth*135+rc()*24); fade=lerp(0.42,0.70,depth); off=lerp(70,W-70,xr)-baseX; }
else { const side=(i%2===0?1:-1), f=i/Math.max(1,nC); depth=f; scale=lerp(0.72,0.44,f)*(0.9+0.2*rc()); off=side*(86+i*50+rc()*46); raise=(36+i*24+rc()*32); fade=lerp(0.38,0.58,f); }
comps.push({seed:cseed, artSeed:cseed, cx:baseX+off, by:baseY-raise, S:scale, fade,
m, density:clamp(density+(rc()*2-1)*(field?0.18:0.12),0.04,1), v, sen, browny, rewards, depth});
}
comps.sort((a,b)=>b.depth-a.depth).forEach(paintPoppy);
paintPoppy({seed:st.seed, artSeed:st.artSeed, cx:baseX, by:baseY, S:1.0, fade:0, m, density, v, sen, browny, rewards});
},
traits(st,env){
const roll=rng('setting',st.seed)();
const setting = roll<0.16?'Solo' : roll<0.44?'Pair' : roll<0.72?'Drift' : roll<0.91?'Meadow':'Field';
const dd=rng('dna',st.seed); dd();dd();dd();dd();dd();dd();dd();dd(); // advance to windStr's draw (9th)
const windStr=Math.pow(dd(),2.3);
return [
['Setting', setting],
['Stems', Math.max(1,Math.ceil(clamp(0.7+env.density*5.2,0.7,6.5)))],
['Wind', windStr<0.12?'Still':windStr<0.40?'Breezy':windStr<0.70?'Wind-bent':'Storm-bent'],
['Bloom', st.diedAt>0?'Capsules':(env.m>=0.58 && env.rewards>0.3?'Scarlet':'—')],
];
}
}));
})();
;
/* species/saguaro.js */
/* saguaro — species index 13. GENERATED from prototypes/saguaro-cactus.html by extract.js.
Do not edit here — edit the prototype and re-run `npm run extract`. The prototype's
mount() call is redirected to SeedlingRenderer.registerSpecies via the local shim. */
(function () {
const mount = (spec) => SeedlingRenderer.registerSpecies(13, spec);
/* Saguaro Cactus — Carnegiea gigantea.
A single tall RIBBED COLUMN with a rounded dome top — the ribs are vertical pleats that
accordion in/out with water; spines sit at areoles along the ribs. ARMS appear only after
long growth (high maturity): each curves OUT then bends UP at the elbow. The reward is an
apex CROWN of white funnel flowers with golden stamen centres → ruby fruit that splits to
black seeds. Near-unkillable (CAM desert); drought just contracts the pleats. Death = a
standing pale RIB SKELETON. Solitary, but framed by desert companions (ocotillo, prickly
pear, agave). */
const SP = window.SEEDLING_SPECIES.find(x=>x.key==='saguaro');
mount(Object.assign(window.SEEDLING_toSpec(SP), {
title:'Saguaro Cactus', sub:'carnegiea v2',
senFrom:0.86, noSoil:true,
note:'Legendary · slow ribbed column · arms only after long growth · apex crown of white flowers → red fruit · pleats accordion with drought · death = standing rib skeleton.',
PAL:{ soil:'#C2A573', sand:'#D8C29A', pebble:'#B59A6E',
bodyLt:'#88A56F', body:'#6E8B6E', bodyDk:'#47643F', rib:'#37502F',
spine:'#E0D4AE',
petal:'#FCFAF4', petalEdge:'#C2C4B2', petalShade:'#B7B4A8', stamen:'#EBC53E', stamenDk:'#C99A24', floBase:'#8A9A58',
bud:'#7E9560', budTip:'#5E6B3E',
fruit:'#A8332A', fruitDk:'#7E2018', fruitGreen:'#85974D', seed:'#241B14', pulp:'#C86A52',
skelLt:'#C3B088', skelDk:'#8C7A5A',
comp:'#9BA873', compDk:'#6E7A4E' },
stageName(st,env){ if(st.diedAt>0)return'Rib skeleton'; const m=env.m;
if(m<0.12)return'Seedling'; if(m<0.5)return'Column'; if(m<0.72)return'Juvenile';
if(m<0.86)return'Armed'; return'Crowned'; },
draw(st, C, env){
const {m,v,sen,season,browny,density,rewards,reproOK,dormancy,dead,baseX,baseY}=env, P=this.PAL;
const SPEC=this; // nested paintSaguaro() loses `this`
const bgC = BACKDROPS[backdropFor(st.seed)];
const seasonF = (season==='Spring'||season==='Summer')?1:0.35;
// ===================== one saguaro (repeated to form a random stand) =====================
function paintSaguaro(I){
const {cx, by, S, fade, m, v, sen, rewards, reproOK, browny, dead, pseed} = I;
const FC = c => fade>0 ? mix(c, bgC, fade) : c;
const drySkel = clamp((dead?1:0) + smooth(0.86,1.0,Math.max(sen, I.ageF||0))*0.9, 0, 1); // → standing rib skeleton
const accordion = 0.78 + 0.22*v; // pleats contract when thirsty (recovers fully)
const tone=(c,rs)=>shade(FC(mix(mix(c,'#b9a06a',browny*0.5), c===P.rib?P.skelDk:P.skelLt, drySkel)),rs);
const COLS={lt:P.bodyLt, dk:P.bodyDk, rib:P.rib};
const isAutumn=season==='Autumn';
// Flower → fruit is sequenced purely by MATURITY (monotonic m), flicker-free: a freshly-reproductive
// saguaro (m≈0.75) FLOWERS white; once well-established (m≥0.80) the same crown sets RED FRUIT.
// Because m only ever RISES, the flower phase ALWAYS precedes the fruit — but fruit is NOT gated on
// the in-app calendar season (a plant matured via entries, which barely advance the day, would
// otherwise show flowers forever and never fruit). Autumn still ripens the fruit DEEPER red below.
const crownMode = (m>=0.80) ? 'fruit' : 'flower';
const lush = clamp(rewards*(1-0.6*sen), 0, 1); // smooth amount control (no mode-flipping thresholds)
const crownOn = reproOK && lush>0.10 && m>=0.75; // vanishes only under severe neglect / off-season
// -- a ribbed segment: a cross-row of vertical pleats, 3-D shaded (centre lit, edges in shadow) --
function ribRow(px,py,dirX,dirY,hw,nRib,z,rs,areole){
const perpX=-dirY, perpY=dirX, gl=dirSym(dirX,dirY);
for(let j=0;j<nRib;j++){ const th=-Math.PI/2 + Math.PI*(nRib>1?j/(nRib-1):0.5);
const off=Math.sin(th)*hw, lit=Math.cos(th), edge=Math.abs(th)>Math.PI*0.42;
const col = edge?COLS.rib : mix(COLS.dk, COLS.lt, lit*lit);
C.push(px+perpX*off, py+perpY*off, z, gl, tone(col,rs),'stem');
if(areole && !edge && j%2===0 && drySkel<0.5)
C.push(px+perpX*off, py+perpY*off, z+1,'+', shade(FC(mix(P.spine,bgC,0.1)),rs),'stem');
}
}
// -- per-seed DNA: each saguaro's silhouette is distinct (girth, height, lean + a full arm plan),
// so seeds aren't all the predictable symmetric two-arm Y. Shared with traits() via armPlan(). --
const dna = SPEC.armPlan(st.seed, pseed);
const lean = dna.lean, crook = dna.crook||0;
// HEIGHT is driven by maturity only (monotonic — never shrinks); the accordion (thirst) contracts
// only the GIRTH/ribs laterally, the real saguaro pleating. Mixing accordion into H made the whole
// column pulse taller/shorter with vitality → "growth flicker".
const H=(70+560*m)*S*dna.tall, colW=(14+21*m)*accordion*S*dna.stout;
const steps=Math.max(2,Math.ceil(H/GRID)); let pos=[cx,by], dir=UP;
const nRibT=clamp(Math.round(colW/2.5),7,17); let apex=[cx,by-H];
for(let s=1;s<=steps;s++){ const u=s/steps;
dir += angTo(dir, UP + lean + crook*smooth(0.25,1,u))*0.05; // upper column curves → varied, non-ramrod silhouettes
pos=[pos[0]+Math.cos(dir)*GRID, pos[1]+Math.sin(dir)*GRID];
const dome = u<0.86?1 : Math.sqrt(Math.max(0,1-Math.pow((u-0.86)/0.14,2)));
const flare = u<0.06? 1+(0.06-u)*3 : 1;
ribRow(pos[0],pos[1],Math.cos(dir),Math.sin(dir),colW*(1-0.10*u)*dome*flare,nRibT,50,rng('col',st.seed,pseed,s), s%4===0);
if(u>0.9) apex=[pos[0],pos[1]];
}
// -- ARMS: emerge progressively (each has its own emergeAt) from the seed's arm plan; out then UP at
// the elbow. Asymmetric heights/sides/lengths + rare secondary sub-arms → varied candelabra. --
const armTips=[];
const nRibFor=aw=>clamp(Math.round(aw/2.0),6,12);
function growArm(p0, d0, A, grow, rsKey, depth){
const armLen=(135+205*m)*S*A.lenF*clamp(grow,0.22,1)*(depth?0.6:1);
const aw=colW*(0.50+0.14*A.curve)*(depth?0.7:1);
let p=p0.slice(), d=d0, sub=null;
const asteps=Math.max(3,Math.ceil(armLen/GRID)), nRibA=nRibFor(aw);
for(let k=1;k<=asteps;k++){ const u=k/asteps;
d += angTo(d, UP + sen*A.side*0.45)*(u<A.elbow?0.05:0.18*A.curve);
p=[p[0]+Math.cos(d)*GRID, p[1]+Math.sin(d)*GRID];
const tipR=u>0.84?Math.sqrt(Math.max(0,1-Math.pow((u-0.84)/0.16,2))):1;
ribRow(p[0],p[1], 0,1, aw*(1-0.10*u)*tipR, nRibA, 49, rng(rsKey,st.seed,pseed,depth,k), k%4===0);
if(depth===0 && A.sub && !sub && u>=A.subU) sub=[p.slice(), d]; // mark a secondary-branch point
}
armTips.push(p);
if(sub && grow>0.72){ const subA={side:A.side, lenF:A.lenF, elbow:0.32, curve:A.curve, spread:A.spread, sub:false, subU:1};
growArm(sub[0], sub[1]+A.side*0.85, subA, 0.85, rsKey+'s', 1); }
}
for(const A of dna.arms){ if(m < A.emergeAt) continue;
const grow=clamp((m-A.emergeAt)/0.18, 0.22, 1); // arms keep extending after emerging
const start=[cx + A.side*colW*0.55, by - H*A.atU], d0=UP + A.side*A.spread;
growArm(start, d0, A, grow, 'ar'+(A.side>0?'R':'L'), 0);
}
// -- apex CROWN: white funnel flowers + green club buds → ruby fruit, at the trunk top & arm tips --
// a green, club-shaped, scaly bud (the pre-bloom stage in the reference)
function drawBud(fx,fy,R,rs){
for(let gy=-R*1.45;gy<=R*0.45;gy+=GRID){ const u=(gy/(R*1.45)+1)/1.45;
const w=R*0.58*Math.sin(Math.PI*clamp(u*0.72+0.16,0,1));
for(let gx=-w;gx<=w;gx+=GRID){ const edge=Math.abs(gx)>w-GRID;
const scale=edge && (Math.round(gy/(GRID*2))%2===0); // suggest overlapping scales
C.push(fx+gx,fy+gy,86,'.',shade(FC(scale||gy<-R*1.0?P.budTip:P.bud),rs),'flowers'); } }
}
// a white funnel flower, read cleanly at plant scale ("fried-egg"): a darker shadow halo rims the
// bloom so the white separates from pale backdrops → white petal disc (thin sage rim) → gold centre.
function drawFlower(fx,fy,R,openF,rs){
const RR=R*(0.98+0.18*openF), halo=RR*1.16;
for(let gx=-halo;gx<=halo;gx+=GRID)for(let gy=-halo;gy<=halo*1.04;gy+=GRID){ if(gx*gx+gy*gy>halo*halo)continue;
C.push(fx+gx,fy+gy,83,'.',shade(FC(P.petalShade),rs),'flowers'); } // soft drop-shadow ring
const pr=RR; // white petal disc — white dominates
for(let gx=-pr;gx<=pr;gx+=GRID)for(let gy=-pr;gy<=pr;gy+=GRID){ const d=Math.hypot(gx,gy); if(d>pr)continue;
C.push(fx+gx,fy+gy,86,'.',shade(FC(d>pr-GRID*0.7?P.petalEdge:P.petal),rs),'flowers'); } // very thin rim
const cr=RR*0.34; // smaller golden stamen centre
for(let gx=-cr;gx<=cr;gx+=GRID)for(let gy=-cr;gy<=cr;gy+=GRID){ const rr=Math.hypot(gx,gy)/cr; if(rr>1)continue;
C.push(fx+gx,fy+gy,90,'.',shade(FC(rr<0.34?P.stamenDk:rr>0.78?P.stamenDk:P.stamen),rs),'flowers'); }
}
// a ruby fruit: a clean oval (green→red→ruby) with a shadow halo, areole dots + brown floral
// remnant; if open, a central red-flesh cup packed with black seeds (the iconic split saguaro fig).
function drawFruit(fx,fy,R,ripe,open,rs,fid){
const base = ripe<0.5 ? mix(P.fruitGreen,P.fruit, ripe*2) : mix(P.fruit,P.fruitDk,(ripe-0.5)*2);
const halo=R*1.06; // soft separation from neighbours
for(let gx=-halo;gx<=halo;gx+=GRID)for(let gy=-halo*1.35;gy<=halo;gy+=GRID){ if((gx*gx)/(halo*halo)+(gy*gy)/(halo*halo*1.6)>1)continue;
C.push(fx+gx,fy+gy,85,'.',shade(FC(P.petalShade),rs),'flowers'); }
for(let gx=-R;gx<=R;gx+=GRID)for(let gy=-R*1.35;gy<=R*1.35;gy+=GRID){ const ny=gy/(R*1.35), rr=(gx*gx)/(R*R)+ny*ny;
if(rr>1)continue; const edge=rr>0.76; C.push(fx+gx,fy+gy,86,'.',shade(FC(edge?mix(base,'#000',0.16):base),rs),'flowers'); }
for(let i=0;i<8;i++){ const aa=rng('arl',st.seed,pseed,fid,i); // areole dots on skin (STABLE seed)
const ax=(aa()*2-1)*R*0.7, ay=(aa()*2-1)*R*1.05; if((ax*ax)/(R*R)+(ay*ay)/(R*R*1.7)>0.7)continue;
C.push(fx+ax,fy+ay,87,'.',shade(FC(P.spine),rs),'flowers'); }
for(let gx=-R*0.3;gx<=R*0.3;gx+=GRID) // brown dried-flower remnant tuft
C.push(fx+gx,fy-R*1.24,88,'.',shade(FC(P.skelDk),rs),'flowers');
if(open){ const ow=R*0.56, oh=R*0.92; // split: central red-flesh cup
for(let gx=-ow;gx<=ow;gx+=GRID)for(let gy=-oh;gy<=oh*0.9;gy+=GRID){ if((gx*gx)/(ow*ow)+(gy*gy)/(oh*oh)>1)continue;
C.push(fx+gx,fy+gy,89,'.',shade(FC(P.pulp),rs),'flowers'); }
for(let i=0;i<16;i++){ const sr=rng('sd',st.seed,pseed,fid,i); // dense black seeds (STABLE seed)
const sx=(sr()*2-1)*ow*0.78, sy=(sr()*2-1)*oh*0.82; if((sx*sx)/(ow*ow)+(sy*sy)/(oh*oh)>0.82)continue;
C.push(fx+sx,fy+sy,90,'.',shade(FC(P.seed),sr),'flowers'); } }
}
// a coherent crown: one mode (buds OR flowers OR fruit, by season) so it doesn't muddle, spaced
// in a small fan so the blooms read individually.
// cid = a STABLE crown id (e.g. 'apex' / 'arm2'); all per-item randomness is seeded on it +
// st.seed, NOT on the drifting pixel position, so buds/flowers/fruit don't flicker as the plant grows.
function crown(tx,ty,scale,rs,cid){
if(drySkel>0.5 || !crownOn) return;
const Rf=(15+7*scale)*S;
if(crownMode==='fruit'){ // a row of distinct ruby figs, one or two split open
const nf=Math.max(2,Math.round(1.4+1.4*scale));
for(let k=0;k<nf;k++){ const jr=rng('cj',st.seed,pseed,cid,k), off=k-(nf-1)/2;
const fx=tx+off*Rf*1.34, fy=ty - Rf*0.35 - Math.abs(off)*Rf*0.30 + (jr()-0.5)*Rf*0.2;
const ripe=clamp((isAutumn?0.92:0.66)+(jr()-0.5)*0.28,0,1), open=jr()<0.4; // stable per-fruit (no vitality)
drawFruit(fx,fy,Rf*0.56, ripe, open, rs, cid+'_'+k); }
return;
}
const n=Math.max(2,Math.round(1.6+1.8*scale)); // spring blooms in a small fan
for(let k=0;k<n;k++){ const a=UP+(k-(n-1)/2)*0.82, rr=Rf*1.42;
const fx=tx+Math.cos(a)*rr, fy=ty+Math.sin(a)*rr*0.5 - Rf*0.5;
const jr=rng('cj',st.seed,pseed,cid,k);
if(jr()<0.18) drawBud(fx,fy,Rf*0.6,rs); else drawFlower(fx,fy,Rf, 0.85, rs); // fixed openness → no size shimmer
}
}
if(crownOn){
crown(apex[0],apex[1],1, rng('cr',st.seed,pseed), 'apex');
armTips.forEach((tp,i)=>{ if(rng('at',st.seed,pseed,i)()<0.7) crown(tp[0],tp[1],0.78,rng('cr',st.seed,pseed,i+1), 'arm'+i); });
}
}
// ===================== STAND: 1 → several saguaros from the seed =====================
const cr=rng('colony',st.seed), roll=cr();
const nC = roll<0.50?0 : roll<0.80?1 : roll<0.93?2 : 3+Math.floor(cr()*3); // companions: 0..5
const comps=[];
for(let k=0;k<nC;k++){ const r=rng('comp',st.seed,k);
const side=k%2?1:-1, rank=Math.floor(k/2)+1, depth=clamp(0.20+0.70*r(),0,1);
const S=lerp(0.62,0.30,depth), fade=lerp(0.24,0.62,depth);
const cxP=clamp(baseX+side*(150+(rank-1)*135)+(r()*2-1)*36, FRAME+70, W-FRAME-70);
const byP=baseY - depth*42; // far ones raised toward the horizon
const mm=clamp(m + (r()*2-1)*0.30, 0.16, 1), vv=clamp(v*(0.6+0.5*r()), 0.2, 1), ssn=clamp(sen + (r()*2-1)*0.12, 0, 1);
comps.push({cx:cxP, by:byP, S, fade, m:mm, v:vv, sen:ssn, ageF:env.ageF,
rewards:clamp(rewards*(0.5+0.7*r()), 0, 1), reproOK:mm>=0.75,
browny:clamp(Math.max(1-vv,ssn),0,1), dead:false, pseed:'c'+k, depth});
}
comps.sort((a,b)=>b.depth-a.depth).forEach(paintSaguaro); // far (faint) → near
paintSaguaro({cx:baseX, by:baseY, S:nC>0?0.92:1.0, fade:0, m, v, sen, ageF:env.ageF, rewards, reproOK, browny, dead, pseed:'hero'});
},
// per-seed silhouette plan — shared by draw() and traits() so the arm count stays in sync.
// Returns {stout, tall, lean, arms:[{side, atU, emergeAt, lenF, elbow, spread, curve, sub, subU}]}.
armPlan(seed, pseed){
const r=rng('dna',seed,pseed);
// wider girth/height/lean ranges + a gentle per-seed column CROOK so even armless columns differ
// (most slight, some clearly curved/leaning — the pow(r,2) skews bends LOW).
const stout=0.76+0.62*r(), tall=0.74+0.52*r(), lean=(r()*2-1)*0.13;
const crook=Math.pow(r(),2)*0.34*(r()<0.5?-1:1);
const t=r(), maxA = t<0.08?0 : t<0.22?1 : t<0.46?2 : t<0.70?3 : t<0.88?4 : 5; // ~8% lone columns, rest 1–5 arms
const arms=[]; let last=r()<0.5?-1:1;
for(let i=0;i<maxA;i++){ const rr=rng('armp',seed,pseed,i);
const side=rr()<0.66?-last:last; last=side;
// emerge at REACHABLE maturity, STAGGERED by arm index → a progressive candelabra instead of a
// pole that only sprouts arms at near-max m (the "always a single straight cactus" cause).
arms.push({ side, atU:0.30+0.46*rr(), emergeAt:0.54+i*0.05+0.15*rr(), lenF:0.60+0.85*rr(),
elbow:0.16+0.20*rr(), spread:0.85+0.70*rr(), curve:0.65+0.80*rr(), sub:rr()<0.22, subU:0.5+0.3*rr() }); }
return { stout, tall, lean, crook, arms };
},
traits(st,env){
const m=env.m, dna=this.armPlan(st.seed,'hero');
const nArm=dna.arms.filter(a=>m>=a.emergeAt).length;
const sub=dna.arms.some(a=>a.sub && m>=a.emergeAt+0.05);
const cr=rng('colony',st.seed), roll=cr();
const total = 1 + (roll<0.50?0 : roll<0.80?1 : roll<0.93?2 : 3+Math.floor(cr()*3));
return [
['Arms', nArm + (sub?' (branched)':'')],
['Crown', st.diedAt>0?'—' : (env.m>=0.75 && env.rewards>0.3 ? (env.season==='Summer'?'Fruit':'Flowers') : '—')],
['Stand', total===1?'Lone':total===2?'Pair':total<=3?'Cluster':'Forest'],
['Form', st.diedAt>0?'Rib skeleton':'Ribbed column'],
];
}
}));
})();
;
/* species/wattle.js */
/* wattle — species index 5. GENERATED from prototypes/golden-wattle.html by extract.js.
Do not edit here — edit the prototype and re-run `npm run extract`. The prototype's
mount() call is redirected to SeedlingRenderer.registerSpecies via the local shim. */
(function () {
const mount = (spec) => SeedlingRenderer.registerSpecies(5, spec);
/* Golden Wattle — Acacia. A low-forking broad DOMED little tree (recursive staggered-lateral frame)
clothed in feathery BIPINNATE leaves (a rachis of paired pinnae of tiny leaflets). Reward: green
buds → fuzzy GOLDEN POM-POM flower heads (late winter/spring, S-hemisphere) → flattened legume SEED
PODS (green→brown) at senescence. Coherent wind. Grows in stands/groves. Ported to the shared core. */
const SP = window.SEEDLING_SPECIES.find(x=>x.key==='wattle');
mount(Object.assign(window.SEEDLING_toSpec(SP), {
title:'Golden Wattle', sub:'acacia v2',
senFrom:0.82,
LW:{season:1.0, stem:1.2, leaves:1.0, flowers:1.4},
LS:{season:1.0, stem:1.0, leaves:0.85, flowers:0.8},
note:'Broad domed acacia · feathery bipinnate foliage · green buds → fuzzy golden pom-pom flowers (late winter/spring) → flat seed pods · wind · stands/groves.',
PAL:{ soil:'#6f5128', dry:'#7a5a30', grey:'#9a8f74',
bark:'#7A6A48', barkDk:'#56