0xffd6563c…95fesent to0x72842d09…a7a6·#25,495,193·view on Etherscan
if(edge && rng('t',seed,i,s)()<tornF) continue; // torn margin (scar)
const vein = !edge && !midrib && (s%5===0);
let col = edge?P.leafDk : midrib?colMid : (w<0?P.leaf:P.leafLt);
if(vein) col=mix(col,P.leafDk,0.45);
PUSH(pos[0]+px*w, pos[1]+py*w, z, midrib?'|':(edge?dirSym(dx,dy):(vein?dirSym(px,py):dirSym(dx,dy))), tone(col), 'leaves'); } } }
}
// ---- the CRANE FLOWER: spathe beak + orange sepal crest + blue tongue ----
function blade(ox,oy,ang,len,wBase,cTip,cBody,z,rs){ // tapering pointed petal / flame
const px=-Math.sin(ang), py=Math.cos(ang); let p=[ox,oy]; const tn=Math.max(2,Math.ceil(len/GRID));
for(let t=1;t<=tn;t++){ p=[p[0]+Math.cos(ang)*GRID, p[1]+Math.sin(ang)*GRID]; const u=t/tn, hw=wBase*(1-u*0.9);
for(let w=-hw;w<=hw;w+=GRID) PUSH(p[0]+px*w, p[1]+py*w, z, u>0.82?'.':dirSym(Math.cos(ang),Math.sin(ang)), shade(FC(u>0.68?cTip:cBody),rs),'flowers'); }
}
function drawBud(jx,jy,fd,R,rs){ // unopened pointed sheath spear (maroon base → green)
const a=fd>0?-0.5:Math.PI+0.5, px=-Math.sin(a), py=Math.cos(a), len=R*1.9, tn=Math.ceil(len/GRID); let p=[jx,jy];
for(let t=1;t<=tn;t++){ p=[p[0]+Math.cos(a)*GRID,p[1]+Math.sin(a)*GRID]; const u=t/tn, hw=GRID*S*(2.1*Math.sin(Math.PI*clamp(u*0.78+0.12,0,1)));
for(let w=-hw;w<=hw;w+=GRID){ const edge=Math.abs(w)>hw-GRID;
const col=u<0.34?mix(P.spatheDk,P.spathe,u/0.34):(edge?P.leafDk:P.spathe);
PUSH(p[0]+px*w,p[1]+py*w,82, edge?dirSym(Math.cos(a),Math.sin(a)):'.', shade(FC(mix(col,'#7a5a30',dry*0.5)),rs),'flowers'); } }
}
function drawCrane(jx,jy,fd,R,open,rs){ // beak spathe + upward orange flames + blue arrow
const ba=fd>0?0.30:Math.PI-0.30, bdx=Math.cos(ba), bdy=Math.sin(ba), beak=R*(1.4+0.7*open), bpx=-Math.sin(ba), bpy=Math.cos(ba);
for(let i=1;i<=Math.ceil(beak/GRID);i++){ const t=i/Math.ceil(beak/GRID), bx=jx+bdx*t*beak, by=jy+bdy*t*beak;
const hh=GRID*S*(1.0+2.2*Math.sin(Math.PI*clamp(t*0.8+0.18,0,1)));
for(let w=-hh;w<=hh;w+=GRID){ const edge=Math.abs(w)>hh-GRID*1.1;
PUSH(bx+bpx*w, by+bpy*w, 80, edge?dirSym(bdx,bdy):'.', shade(FC(mix(edge?P.spatheDk:mix(P.spathe,P.spatheDk,0.25), '#7a5a30', dry*0.75)),rs),'flowers'); } } // boat spathe browns + persists as the husk
// crest: bright orange flames when alive → a couple of short brown withered nubs once dried
const withered=dry>0.6, ns=withered?2:3+Math.round(open*1.5);
const flameTip =mix(P.orange,'#7a5a30', withered?0.9:dry*0.6);
const flameBody=mix(mix(P.orange,P.orangeDk,0.35),'#7a5a30', withered?0.82:dry*0.35);
for(let k=0;k<ns;k++) blade(jx, jy, UP+(k-(ns-1)/2)*0.24+fd*0.06, R*(0.7+1.0*open)*(1+0.22*(k%2))*(withered?0.45:1), GRID*1.3*S, flameTip, flameBody, 90, rs);
if(open>0.5 && !withered) blade(jx+fd*GRID*S, jy-GRID*0.5, fd>0?-0.55:Math.PI+0.55, R*1.3*open, GRID*1.5*S, P.blueLt, P.blue, 91, rs); // blue tongue — only on a live open crane
}
// ---- BLOOM: cranes spread ACROSS the clump on tall ARCHING stalks of varied height ----
// The flower STRUCTURE (woody stalk + boat spathe) PERSISTS into death as a dried husk — it
// does NOT vanish (spec death state = "dried spathe husk"). So the stalk presence is gated on
// maturity+reward but NOT on (1-sen); only the live COLOUR (open orange crest + blue tongue)
// fades with senescence/season and finally withers to brown.
const seasonOK=(season==='Winter'||season==='Spring')?1:0.7;
const earned = m>=0.55 ? smooth(0.05,0.55,rewards) : 0; // did it earn cranes?
const standDrive = Math.max(earned*(1-winter*0.7), dead&&m>=0.55 ? 0.55 : 0); // a mature plant that died keeps standing dried stalks
const liveBloom = earned*(1-sen)*(1-winter)*seasonOK; // colourful open cranes (alive + in season)
if(standDrive>0.10){
const nb=clamp(1+Math.round(standDrive*2*(0.5+0.5*D.nFlowers/3)), 1, 3);
for(let b=0;b<nb;b++){ const rs=rng('cr',seed,b), hr=rng('h',seed,b)();
const spread = nb>1 ? (b-(nb-1)/2)/((nb-1)/2) : 0; // -1..1 across the clump
const leanDir = spread!==0 ? (spread>0?1:-1) : D.bloomSide; // outer cranes lean outward; solo leans bloomSide
const bx = cx + (spread*(42+34*m) + D.fanLean*30)*S; // wider → cranes clearly off-centre, not all from the middle
const open = clamp(liveBloom*1.6 - b*0.45, 0, 1); // older cranes open; newer still budding; 0 once dried
const sh = (215+320*m)*(0.82+0.18*v)*(1 - D.heightVar*0.45 + D.heightVar*hr)*S; // taller → cranes ride above the fan so the arch shows; strong height variation
const curl = D.stalkCurl*(0.5+0.7*Math.abs(spread))*(1+dry*0.5); // outer stalks arch more; dried stalks sag/arch further
let p=[bx, cy], dir=UP + leanDir*D.stalkLean*0.4 + D.fanLean*0.5;
const steps=Math.ceil(sh/GRID);
const stalkCol=mix(P.stalk,'#6b4a2a',dry*0.85); // green stalk → brown dried husk
for(let s=1;s<=steps;s++){ const u=s/steps;
dir += leanDir*curl*0.013*smooth(0.15,1,u); // progressive arch toward the top (not a straight pole)
p=[p[0]+Math.cos(dir)*GRID, p[1]+Math.sin(dir)*GRID];
for(let w=-GRID*0.8;w<=GRID*0.8;w+=GRID) PUSH(p[0]+w,p[1],50,'|',shade(FC(stalkCol),rs),'stem'); }
const R=(24+22*m)*(0.85+0.15*v)*S;
// dried → the empty boat spathe stands (browned crest); young & living → unopened spear bud; else open crane
if(dry>0.5) drawCrane(p[0],p[1],leanDir,R, clamp(open,0.28,0.5), rs);
else if(open<0.4) drawBud(p[0],p[1],leanDir,R,rs);
else drawCrane(p[0],p[1],leanDir,R,open,rs);
}
}
}
// ===================== background CLUMP (varied size, scattered) + sharp hero =====================
// Solo → 6-strong clump, companions SCATTERED across the canvas with per-seed jitter and INDEPENDENT
// depth (size/fade/height NOT tied to draw order) → no two seeds share a layout, no rigid L/R flanking.
const roll=rng('setting',st.seed)();
const nC = roll<0.20?0 : roll<0.44?1 : roll<0.66?2 : roll<0.84?3 : roll<0.95?4 : 5; // 0..5 companions
const comps=[];
for(let i=0;i<nC;i++){ const rc=rng('bcomp',st.seed,i);
const frac = nC>1 ? (i+0.5)/nC : 0.5; // spread across the width…
const slot = clamp(frac + (rc()*2-1)*(0.85/nC), 0.05, 0.95); // …+ strong jitter → unpredictable, not rigid L/R
const cxP = lerp(FRAME+85, W-FRAME-85, slot);
const depth = clamp(0.10 + 0.82*rng('bdep',st.seed,i)(), 0, 1); // INDEPENDENT of index → organic size/fade/height
const birthM = 0.06 + rc()*0.42; // randomized emergence (grows in from a sprout, no pop)
const cm = clamp((m-birthM)/0.46, 0, 1); if(cm<=0.03) continue;
const cseed='0x'+(hashStr(st.seed+'|b'+i)>>>0).toString(16);
comps.push({ seed:cseed, artSeed:cseed,
cx: cxP,
cy: baseY-4 - (6 + depth*42 + rc()*14), // deeper (far) plants sit higher → toward the horizon
S: lerp(0.60,0.34,depth)*(0.88+0.22*rc()),
dim: lerp(0.38,0.66,depth),
zb: -110 - Math.round(depth*60), // always behind the hero; far ones furthest back
m: cm, v, sen, browny, rewards: clamp(rewards*(0.82+0.32*rc()),0,1), density, dead,
depth }); }
comps.sort((a,b)=>b.depth-a.depth).forEach(paintBird); // far (faint) → near
paintBird({ seed:st.seed, artSeed:st.artSeed, cx:baseX, cy:baseY-4, S:1, dim:0, zb:0, m, v, sen, browny, rewards, density, dead });
},
traits(st,env){
const roll=rng('setting',st.seed)();
const total = 1 + (roll<0.20?0 : roll<0.44?1 : roll<0.66?2 : roll<0.84?3 : roll<0.95?4 : 5);
const setting = total===1?'Solo' : total===2?'Pair' : total===3?'Trio' : 'Clump';
const D=(function(seed){ const dd=rng('dna',seed); dd();dd();dd();dd();dd(); const nF=1+Math.floor(dd()*3); const side=dd()<0.5?1:-1; return {nFlowers:nF, bloomSide:side}; })(st.seed);
return [
['Leaves', clamp(Math.round(3+env.density*4),3,7)],
['Bloom', st.diedAt>0?'Dried husk':(env.m>=0.55&&env.rewards>0.3?'Crane':'—')],
['Faces', D.bloomSide>0?'East':'West'],
['Setting', setting],
];
}
}));
})();
;
/* species/guava.js */
/* guava — species index 8. GENERATED from prototypes/chilean-guava.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(8, spec);
/* Chilean Guava — Ugni molinae. A compact upright oval myrtle SHRUB: a recursive staggered-lateral
framework clothed in distinct rounded FOLIAGE CLUMPS with branches showing between them. Reward:
white powderpuff FLOWERS (late spring/summer) → ruby BERRIES ripening green→red→purple (autumn/
winter). Evergreen, then drops to bare twigs in old age. Grows in thickets/hedges. Ported to core. */
const SP = window.SEEDLING_SPECIES.find(x=>x.key==='guava');
mount(Object.assign(window.SEEDLING_toSpec(SP), {
title:'Chilean Guava', sub:'ugni v2',
senFrom:0.82,
LW:{season:1.0, stem:1.15, leaves:1.0, flowers:1.5},
LS:{season:1.0, stem:1.0, leaves:0.85, flowers:0.85},
note:'Compact upright myrtle shrub · rounded foliage clumps with twigs showing between · white powderpuff flowers → ruby berries (green→red→purple) · evergreen → bare in old age · thickets.',
PAL:{ soil:'#6f5128', grey:'#9a8f74',
bark:'#6E5B46', barkDk:'#46382A',
leaf:'#2F5E36', leafDk:'#1C3F22', leafLt:'#5C8C4C', leafYoung:'#9A6A4E', leafYellow:'#C2A848',
petal:'#F5ECEE', stamen:'#E0668A', stamenDk:'#C23A66', anther:'#E8A82C',
berryGreen:'#7FA84E', berryRed:'#C2384C', berryPurple:'#5A2746', berryRipe:'#3A1730', calyx:'#7E8A4A' },
stageName(st,env){ if(st.diedAt>0)return'Sealed'; const m=env.m, season=env.season;
if(env.ageF>0.84)return'Dormant';
if(m<0.12)return'Sprig'; if(m<0.34)return'Seedling'; if(m<0.55)return'Bush';
const fruity=(season==='Autumn'||season==='Winter'), flowery=(season==='Spring'||season==='Summer');
if(m<0.62)return'Mature'; return fruity?'Fruiting':(flowery?'Flowering':'Dense'); },
draw(st, C, env){
const {m,v,sen,season,browny,density,rewards,baseX,baseY}=env, P=this.PAL;
const bgC = BACKDROPS[backdropFor(st.seed)];
function guavaDNA(seed){ const dd=rng('dna',seed), g=()=>(dd()+dd()+dd()-1.5)/1.5;
return { dominance:0.46+0.4*dd(), divergence:0.5+0.4*dd(), droop:0.12+0.28*dd(), gnarl:0.3+0.5*dd(), taperLen:0.68+0.12*dd(),
lateralN:1.3+0.9*dd(), reach:0.5+0.32*dd(), lean:g()*0.09, stature:0.82+0.2*dd(), maxOrder:4+(dd()<0.5?1:0),
leafiness:0.95+0.45*dd(), flowerAb:0.5+0.6*dd(), fruitAb:0.5+0.6*dd(), windDir:dd()<0.5?1:-1, windStr:Math.pow(dd(),2.6) }; }
function paintGuava(I){
const {seed, artSeed, cx, by, S, fade, m, density, v, sen, browny, rewards} = I;
const FC = c => fade>0 ? mix(c, bgC, fade) : c;
const D=guavaDNA(seed), yellowF=clamp(Math.max(sen*0.9,(1-v)*0.5),0,1);
const seasonBloom = season==='Spring'?1 : season==='Summer'?0.85 : season==='Winter'?0.15 : 0.5;
const seasonRipe = season==='Winter'?1 : season==='Autumn'?0.8 : season==='Summer'?0.35 : 0.12;
const foliageF=clamp((1-sen*0.92)*(0.5+0.5*v),0,1);
const bloom=smooth(0.44,0.66,m)*(1-sen)*seasonBloom*rewards, fruitF=smooth(0.52,0.78,m)*rewards*clamp(seasonRipe+0.2,0,1)*(1-0.5*sen), ripe=clamp(smooth(0.55,0.85,m)*seasonRipe + sen*0.4, 0, 1);
const bark=FC(mix(P.bark,P.grey,Math.max((1-v)*0.3,sen*0.6))), barkDk=FC(mix(P.barkDk,P.grey,Math.max((1-v)*0.3,sen*0.6)));
const leafM=FC(mix(P.leaf,P.leafYellow,yellowF)), leafD=FC(mix(P.leafDk,P.leafYellow,yellowF*0.8)), leafL=FC(mix(P.leafLt,P.leafYellow,yellowF));
const greenOf=rs=>shade(mix(leafM, rs()<0.5?leafD:leafL, rs()), rs);
const windLean=D.windDir*D.windStr*0.24, rootX=cx - D.windDir*D.windStr*34*S;
function leafClump(ccx,ccy,rad,rs){ if(foliageF<=0.05) return; const r2=rad*rad;
for(let gx=-rad;gx<=rad;gx+=GRID) for(let gy=-rad;gy<=rad;gy+=GRID){ const rr=gx*gx+gy*gy; if(rr>r2) continue; const edge=rr>r2*0.5;
if(rs() > foliageF*(edge?0.4:0.94)) continue; const col= edge? shade(leafD,rs) : (rs()<0.34? shade(leafL,rs) : shade(leafM,rs));
C.push(ccx+gx, ccy+gy, 56, rs()<0.55?'O':'.', col, 'leaves'); } }
function berry(x,y,rp,rs){ const col = rp<0.45 ? mix(P.berryGreen,P.berryRed,rp/0.45) : mix(P.berryRed, mix(P.berryPurple,P.berryRipe,clamp(sen,0,1)), (rp-0.45)/0.55);
const cc=FC(mix(col, '#7a5a30', browny*0.4)); C.push(x,y,88,'.',shade(cc,rs),'flowers'); if(rp>0.6) C.push(x+GRID*0.4,y-GRID*0.4,89,'.',shade(mix(cc,'#ffffff',0.35),rs),'flowers'); }
function rewardClump(ccx,ccy,rad,kind,rp,rs){ leafClump(ccx,ccy,rad,rs); const n=2+Math.floor(rs()*3);
for(let k=0;k<n;k++){ const a=rs()*TAU, d=Math.sqrt(rs())*rad*0.7, bx=ccx+Math.cos(a)*d, byy=ccy+Math.sin(a)*d;
if(kind==='berry') berry(bx,byy,rp,rs);
else { for(let s=0;s<5;s++){ const sa=s/5*TAU; C.push(bx+Math.cos(sa)*GRID*1.1, byy+Math.sin(sa)*GRID*1.1, 90,'.', shade(FC(rs()<0.55?P.stamen:P.anther),rs),'flowers'); } C.push(bx,byy,91,'O',shade(FC(P.petal),rs),'flowers'); } } }
function sprout(rs,rx){ if(1-smooth(0.05,0.20,m) <= 0.02) return; const g=smooth(0,0.18,m), hLen=(46+44*g)*S; let dir=UP+D.lean*1.2, pos=[rx,by-3]; const steps=Math.max(3,Math.ceil(hLen/GRID));
for(let i=1;i<=steps;i++){ const u=i/steps; dir+=Math.sin(u*2.6+D.lean*4)*0.04; pos=[pos[0]+Math.cos(dir)*GRID, pos[1]+Math.sin(dir)*GRID]; C.push(pos[0],pos[1],50,dirSym(Math.cos(dir),Math.sin(dir)),shade(bark,rs),'stem'); }
for(const sd of [1,-1]){ let cd=UP+sd*0.95; let cp=[pos[0],pos[1]]; const cl=Math.max(2,Math.round((18+16*g)*S/GRID));
for(let i=1;i<=cl;i++){ cd+=angTo(cd,UP)*0.08; cp=[cp[0]+Math.cos(cd)*GRID, cp[1]+Math.sin(cd)*GRID]; const w=Math.sin(Math.PI*i/cl)*GRID*1.3, px=-Math.sin(cd),py=Math.cos(cd);
C.push(cp[0],cp[1],36,dirSym(Math.cos(cd),Math.sin(cd)),greenOf(rs),'leaves'); C.push(cp[0]+px*w,cp[1]+py*w,35,'.',greenOf(rs),'leaves'); C.push(cp[0]-px*w,cp[1]-py*w,35,'.',greenOf(rs),'leaves'); } }
if(g>0.4) leafClump(pos[0],pos[1], (5+5*g)*S, rs); }
const growWin=0.12, maxOrder=D.maxOrder; let budget=8000;
const chain=(1-Math.pow(D.taperLen,maxOrder+1))/(1-D.taperLen), baseRun=((by-400)/chain)*D.stature*(0.9+0.1*v)*S;
function grow(x,y,head,vigor,order,birthM,pid,sideSeq){ if(budget<=0) return; const grown=clamp((m-birthM)/growWin,0,1); if(grown<=0) return; budget--;
const rs=rng('gb', seed, pid); let run=baseRun*Math.pow(D.taperLen,order)*(0.82+0.36*rs())*grown; if(order===0) run*=0.92;
const steps=Math.max(1,Math.ceil(run/GRID)), woodM=smooth(0.10,0.85,m), baseThick=(maxOrder-order)/maxOrder, bz=54-order;
const f1=0.5+1.4*rs(), f2=1.1+2.2*rs(), ph1=rs()*6.28, ph2=rs()*6.28, gnarlA=D.gnarl*0.04*(1+order*0.12), bears=order>=1 ? D.leafiness : 0.3*D.leafiness;
const canChild = grown>=1 && order<maxOrder;
const nLat = order===0 ? 3+Math.floor(rs()*3) : clamp(Math.round(D.lateralN*(1-0.3*D.dominance)+gauss(rs)*0.9),0, order<2?3:2);
const lo=order===0?0.18:0.30, hi=0.94, pts=[], sides=[]; let lastSide=rs()<0.5?1:-1;
for(let c=0;c<nLat;c++){ pts.push(clamp(lerp(lo,hi,(c+0.5)/Math.max(nLat,1)) + (rs()*2-1)*(0.55/Math.max(nLat,1)), 0.1, 0.96)); if(order===0 || rs()<0.72) lastSide=-lastSide; sides.push(lastSide); }
pts.sort((a,b)=>a-b); let dir=head, pos=[x,y], ci=0;
for(let i=1;i<=steps;i++){ const u=i/steps; const photo=lerp(0.14,0.05,clamp(order/3,0,1))*Math.max(0,1-order/3.4), grav=0.06*D.droop*smooth(2,maxOrder,order)*(0.25+0.75*u);
dir += angTo(dir,UP)*photo + angTo(dir,DOWN)*grav + gnarlA*(Math.sin(i*0.5*f1+ph1)+0.5*Math.sin(i*0.3*f2+ph2));
const exposure=clamp((by-pos[1])/520,0,1); dir += D.windDir*D.windStr*0.05*exposure*(0.35+0.65*order/maxOrder);
const dx=Math.cos(dir), dy=Math.sin(dir); pos=[pos[0]+dx*GRID, pos[1]+dy*GRID]; const sym=dirSym(dx,dy);
C.push(pos[0],pos[1], bz, sym, shade(bark,rs), 'stem'); const px=-dy, py=dx;
const thick=clamp(Math.round(baseThick*baseThick*1.7*woodM*(0.7+0.3*v)*(1-0.5*u)*smooth(0.12,0.85,grown)),0,1);
for(let wq=1;wq<=thick;wq++){ C.push(pos[0]+px*wq*GRID, pos[1]+py*wq*GRID, bz, sym, shade(barkDk,rs), 'stem'); C.push(pos[0]-px*wq*GRID, pos[1]-py*wq*GRID, bz, sym, shade(barkDk,rs), 'stem'); }
if(bears>0 && u>0.14 && i%5===3 && rs()<bears*0.92){ const rad=(7+9*m)*(0.72+0.5*rs())*S;
if(order>=2 && fruitF>0.2 && rs()<fruitF*D.fruitAb*0.34) rewardClump(pos[0],pos[1],rad,'berry',ripe,rs);
else if(order>=2 && bloom>0.15 && rs()<bloom*D.flowerAb*0.24) rewardClump(pos[0],pos[1],rad,'flower',0,rs); else leafClump(pos[0],pos[1],rad,rs); }
while(canChild && ci<pts.length && u>=pts[ci] && budget>0){ const side=sides[ci], div=(order===0?(0.5+0.5*D.reach):D.divergence*(0.7+0.7*rs())*D.reach);
let la=dir + side*div + gauss(rs)*0.14; la += angTo(la,UP)*(order===0?0.10:0.16)*(1-D.reach*0.3);
const cv=vigor*(order===0?(0.7+0.26*rs()):(0.5+0.4*(1-D.dominance))*(0.8+0.3*rs())); grow(pos[0],pos[1], la, cv, order+1, birthM+growWin, pid*7+ci+2, sideSeq+ci+1); ci++; }
}
if(canChild && budget>0){ const leadV=vigor*lerp(0.55,0.92,D.dominance); grow(pos[0],pos[1], dir+angTo(dir,UP)*(order<=1?0.30:0.18)+gauss(rs)*0.05, leadV, order+1, birthM+growWin, pid*7+1, sideSeq+1); }
else if(grown>=1){ const rad=(8+11*m)*(0.8+0.3*rs())*S;
if(fruitF>0.2 && rs()<fruitF*D.fruitAb*0.38) rewardClump(pos[0],pos[1],rad,'berry',ripe,rs); else if(bloom>0.18 && rs()<bloom*D.flowerAb*0.3) rewardClump(pos[0],pos[1],rad,'flower',0,rs); else leafClump(pos[0],pos[1],rad,rs); }
else if(order>=1){ leafClump(pos[0],pos[1], (6+8*m)*S, rs); }
}
sprout(rng('sprout',seed), rootX);
grow(rootX, by-3, UP+D.lean*0.6+D.windDir*D.windStr*0.06, 1, 0, 0, 1, 0);
}
// ===================== SETTING: Solo / Pair / Thicket / Hedge =====================
const roll=rng('setting',st.seed)();
const nC = roll<0.26?0 : roll<0.56?1 : roll<0.84?2 : 3+Math.floor(rng('fn',st.seed)()*2);
const 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), side=(i%2===0?1:-1), f=i/Math.max(1,nC);
comps.push({seed:cseed, artSeed:cseed, cx:baseX+side*(140+i*70+rc()*40), by:baseY-(2+i*8+rc()*16), S:lerp(0.66,0.46,f)*(0.9+0.2*rc()), fade:lerp(0.40,0.60,f),
m, density:clamp(density+(rc()*2-1)*0.10,0.04,1), v, sen, browny, rewards, depth:f}); }
comps.sort((a,b)=>b.depth-a.depth).forEach(paintGuava);
paintGuava({seed:st.seed, artSeed:st.artSeed, cx:baseX, by:baseY, S:1.08, fade:0, m, density, v, sen, browny, rewards});
},
traits(st,env){
const roll=rng('setting',st.seed)();
const setting = roll<0.26?'Solo' : roll<0.56?'Pair' : roll<0.84?'Thicket' : 'Hedge';
const fruity=(env.season==='Autumn'||env.season==='Winter');
return [
['Setting', setting],
['Reward', st.diedAt>0?'—':(env.m>=0.55 && env.rewards>0.3 ? (fruity?'Berries':'Flowers') : '—')],
['Form', st.diedAt>0?'Bare twigs':'Oval shrub'],
];
}
}));
})();
;
/* species/hairgrass.js */
/* hairgrass — species index 15. GENERATED from prototypes/antarctic-hair-grass.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(15, spec);
/* Antarctic Hair Grass (Deschampsia antarctica) — the rarest Seedling. A wind-bleached
cushion-tuft of fine arching blades (green->yellow->brown) growing from a crevice in
angular gray ROCKS, dusted with SNOW, sending up delicate airy PANICLE seed-heads.
The lone survivor — one of only two native flowering plants of Antarctica. */
mount({
title:'Antarctic Hair Grass', sub:'hairgrass v2',
LIFESPAN:730, GRACE:150, DECAY:1/50, AGE_EQUIV:14, senFrom:0.86,
note:'LEGENDARY · a lone wind-bleached tuft rooted in the rocks, dusted with snow, sending up airy golden seed-heads. One of only two native Antarctic flowering plants.',
PAL:{ soil:'#8C8C88', accent:'#9DB36A',
blade:'#5E8A46', bladeDk:'#3F6330', bladeLt:'#8FB45E', straw:'#C7B173', strawDk:'#9F844A',
rock:'#7E868B', rockDk:'#474D51', rockLt:'#AEB4B8', crack:'#2B2F33',
stalk:'#9DAE6F', spikeGreen:'#86A857', spikeStraw:'#C7A85E', awn:'#B7A36E',
snow:'#F5F9FD', snowDk:'#D8E4EE' },
schemeExempt:['season','flowers'], // rocks + snow + spikelets read literal (solid), not scheme rings
LW:{season:2.4, stem:1.1, leaves:1.0, flowers:1.7}, // bold season -> rocks read as SOLID stone
stageName(st,env){ if(st.diedAt>0)return'Sealed'; const m=env.m;
if(m<0.16)return'Sprig'; if(m<0.5)return'Tuft'; if(m<0.84)return'Cushion'; return'Seeding'; },
draw(st, C, env){
const {m,v,sen,season,browny,density,rewards,baseX,baseY}=env, P=this.PAL;
const cx=baseX, cy=baseY-182; // tuft roots in the crevice on the central block
const flowering = smooth(0.40,0.66,m) * (1-sen*0.85);
const seasonGold = season==='Summer'?1 : season==='Autumn'?0.7 : season==='Spring'?0.5 : 0.3;
const snowF = season==='Winter'?1 : season==='Autumn'?0.55 : season==='Spring'?0.4 : 0.16;
// ---------- ROCKS: angular fractured granite blocks the tuft roots between ----------
function rockField(){
const rs=rng('rock',st.seed);
// 1. jagged ANGULAR skyline: piecewise-linear control points (not a smooth curve)
const nC=5+Math.floor(rs()*3), cpx=[], cpy=[];
for(let i=0;i<=nC;i++){ cpx.push(FRAME-4 + i*(W-2*FRAME+8)/nC); cpy.push((baseY-150) - rs()*72); }
cpy[Math.round(nC/2)] = (baseY-176) - rs()*14; // central block sits a touch higher -> tuft nestles on it
const topAt=x=>{ let i=0; while(i<nC-1 && cpx[i+1]<x) i++;
const t=clamp((x-cpx[i])/((cpx[i+1]-cpx[i])||1),0,1); return lerp(cpy[i],cpy[i+1],t); };
// 2. Voronoi seeds -> distinct angular faceted stones, each its own tone
const nS=12+Math.floor(rs()*6), seeds=[];
for(let i=0;i<nS;i++) seeds.push({x:FRAME+rs()*(W-2*FRAME), y:(baseY-115)+rs()*180, t:rs()});
for(let x=FRAME+6;x<=W-FRAME-6;x+=GRID){
const top=topAt(x);
for(let y=top;y<=baseY+38;y+=GRID){
let d0=1e9,d1=1e9,b0=seeds[0],b1=seeds[0];
for(const s of seeds){ const dx=x-s.x, dy=(y-s.y)*1.25, d=dx*dx+dy*dy;
if(d<d0){d1=d0;b1=b0;d0=d;b0=s;} else if(d<d1){d1=d;b1=s;} }
const seam = (Math.sqrt(d1)-Math.sqrt(d0)) < GRID*0.