0xbcd8…650f

All memos sent from and to 0xbcd8…650f.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240"> <rect width="240" height="240" rx="54" fill="#0c0e09"/> <g transform="translate(48 44) scale(6)" fill="none"> <g transform="translate(0 -3)"> <path d="M12 21c0-5 0-8 3-11 2.2-2.2 5-2.5 5-2.5s-.2 3-2.4 5.2C16.6 14.8 13 15 12 15" stroke="#8fb673" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/> <path d="M12 21c0-4-.2-6-2.6-8.4C7.2 10.4 4 10.2 4 10.2s.2 3 2.6 5.4C8.6 17.6 11 18 12 18" stroke="#8fb673" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/> </g> <path d="M4.5 20.5H16" stroke="#8fb673" stroke-width="2.2" stroke-linecap="round"/> <path d="M16 19.1 20 20.5 16 21.9Z" fill="#8fb673"/> </g> </svg>
rm', 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:'#564934', leaf:'#5E7E3C', leafDk:'#3C5828', leafLt:'#86A65A', flower:'#F4C020', flowerLt:'#FBDD5A', flowerDk:'#D9A50E', bud:'#9DAE5A', pod:'#8A9A55', podDry:'#A2854A', podBrown:'#7a5a30' }, stageName(st,env){ if(st.diedAt>0)return'Sealed'; const m=env.m; if(env.ageF>0.84)return'Podding'; if(m<0.12)return'Sprig'; if(m<0.36)return'Young'; if(m<0.55)return'Branching'; const wintery=(env.season==='Winter'||env.season==='Spring'); if(m<0.66)return'Leafy'; return wintery?'Golden':'Leafy'; }, draw(st, C, env){ const {m,v,sen,season,browny,density,rewards,ageF,baseX,baseY}=env, P=this.PAL; const bgC = BACKDROPS[backdropFor(st.seed)]; function wattleDNA(seed){ const dd=rng('dna',seed), g=()=>(dd()+dd()+dd()-1.5)/1.5; return { dominance:0.18+0.34*dd(), divergence:0.45+0.45*dd(), droop:0.30+0.45*dd(), gnarl:0.25+0.5*dd(), taperLen:0.66+0.12*dd(), lateralN:1.6+1.1*dd(), reach:0.78+0.3*dd(), lean:g()*0.12, stature:0.86+0.18*dd(), maxOrder:5+(dd()<0.5?1:0), leafiness:0.7+0.5*dd(), flowerAb:0.55+0.6*dd(), pods:0.4+0.6*dd(), windDir:dd()<0.5?1:-1, windStr:Math.pow(dd(),2.5) }; } function paintWattle(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=wattleDNA(seed); const seasonGold = season==='Winter'?1 : season==='Spring'?0.9 : season==='Autumn'?0.5 : 0.45; const foliageF=(1-0.80*sen)*(0.4+0.6*v), bloom=smooth(0.42,0.74,m)*(1-sen)*(0.55+0.45*seasonGold)*rewards; const leafKeep=clamp(1-0.82*sen, 0.12, 1); // phyllodes SHED as it dries/dies → fewer leaf tufts → branch skeleton + dry pods show through const budF=smooth(0.36,0.5,m)*(1-smooth(0.5,0.64,m)), podF=D.pods*smooth(0.66,0.95,Math.max(sen*1.2, ageF*0.9, m-0.2))*(1-0.3*sen); const bark=FC(mix(P.bark,P.grey,Math.max((1-v)*0.3,sen))), barkDk=FC(mix(P.barkDk,P.grey,Math.max((1-v)*0.3,sen))); const leafD=FC(mix(P.leafDk,P.dry,browny*0.9)), leafM=FC(mix(P.leaf,P.dry,browny*0.9)), leafL=FC(mix(P.leafLt,P.dry,browny*0.85)); const greenOf=rs=>shade(mix(leafM, rs()<0.5?leafD:leafL, rs()), rs); const windLean=D.windDir*D.windStr*0.30, rootX=cx - D.windDir*D.windStr*46*S; function bipinnateLeaf(bx, byy, baseAng, size, rs){ if(foliageF<=0.04) return; const segs=Math.max(2,Math.round(size)), droop=(0.5+0.7*rs())*(0.5+0.7*D.droop); let dir=baseAng, pos=[bx,byy]; for(let i=1;i<=segs;i++){ dir += angTo(dir,DOWN)*0.045*droop*(0.3+0.7*(i/segs)); const cd=[Math.cos(dir),Math.sin(dir)]; pos=[pos[0]+cd[0]*GRID, pos[1]+cd[1]*GRID]; C.push(pos[0],pos[1], 34, dirSym(cd[0],cd[1]), greenOf(rs), 'leaves'); if(i===1) continue; const px=-cd[1], py=cd[0]; for(const sd of [1,-1]){ let pa=Math.atan2(py*sd,px*sd); pa += angTo(pa,dir)*0.42; let pp=[pos[0],pos[1]], pdir=pa; const plen=2+Math.floor(rs()*2); for(let k=1;k<=plen;k++){ pdir += angTo(pdir,DOWN)*0.06; pp=[pp[0]+Math.cos(pdir)*GRID, pp[1]+Math.sin(pdir)*GRID]; C.push(pp[0],pp[1], 33, '.', greenOf(rs), 'leaves'); } } } } function leafTuft(x,y,baseAng,rs){ const n=2+Math.floor(rs()*2); for(let k=0;k<n;k++) bipinnateLeaf(x,y, baseAng+(k-(n-1)/2)*0.5+(rs()*2-1)*0.15, (3.5+5*m)*(0.7+0.4*rs())*S, rs); } function pomPom(x,y,green,rs){ const fc = green ? P.bud : (rs()<0.5?P.flower:P.flowerLt), R=GRID*(green?0.85:1.0)*Math.sqrt(S); C.push(x,y,91, green?'O':'+', shade(FC(green?P.bud:mix(fc,P.flowerDk,0.3)),rs), 'flowers'); const n=green?6:7; for(let k=0;k<n;k++){ const a=k/n*TAU + rs()*0.3; C.push(x+Math.cos(a)*R, y+Math.sin(a)*R, 90, '.', shade(FC(fc),rs), 'flowers'); } } function pomCluster(x,y,green,rs){ const n=1+Math.floor(rs()*2); for(let k=0;k<n;k++) pomPom(x+(rs()*2-1)*GRID*2.4, y+(rs()*2-1)*GRID*2.4, green, rs); } function seedPod(x,y,rs){ const cc=FC(mix(P.pod, mix(P.podDry,P.podBrown,sen), Math.max(sen, browny*0.7))); let dir=DOWN+(rs()*2-1)*0.5, pos=[x,y]; const len=4+Math.floor(rs()*3); for(let i=1;i<=len;i++){ dir += angTo(dir,DOWN)*0.18; pos=[pos[0]+Math.cos(dir)*GRID, pos[1]+Math.sin(dir)*GRID]; const px=-Math.sin(dir), py=Math.cos(dir), bump=(i%2?1.1:0.55); C.push(pos[0],pos[1], 86, '|', shade(cc,rs), 'flowers'); C.push(pos[0]+px*bump*GRID, pos[1]+py*bump*GRID, 86, '.', shade(cc,rs), 'flowers'); C.push(pos[0]-px*bump*GRID, pos[1]-py*bump*GRID, 86, '.', shade(mix(cc,'#000000',0.12),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=(58+50*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.045; 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.8+0.3*D.reach); let cp=[pos[0],pos[1]]; const cl=Math.max(3,Math.round((26+20*g)*S/GRID)); for(let i=1;i<=cl;i++){ cd+=angTo(cd,UP)*0.07; cp=[cp[0]+Math.cos(cd)*GRID, cp[1]+Math.sin(cd)*GRID]; const w=Math.sin(Math.PI*i/cl)*GRID*1.4, 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) bipinnateLeaf(pos[0],pos[1], UP+(rs()*2-1)*0.3, (3+3*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-300)/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('wb', seed, pid); let run=baseRun*Math.pow(D.taperLen,order)*(0.82+0.36*rs())*grown; if(order===0) run*=0.86; 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.045*(1+order*0.12), bears=order>=2 ? D.leafiness : (order>=1? 0.25*D.leafiness:0); const canChild = grown>=1 && order<maxOrder; const nLat = order===0 ? 2+Math.floor(rs()*3) : clamp(Math.round(D.lateralN*(1-0.35*D.dominance)+gauss(rs)*0.9),0, order<2?3:2); const lo=order===0?0.24:0.32, hi=0.92, 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.6/Math.max(nLat,1)), 0.12, 0.95)); 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.13,0.04,clamp(order/3,0,1))*Math.max(0,1-order/3.2), grav=0.10*D.droop*smooth(1.5,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])/600,0,1); dir += D.windDir*D.windStr*0.06*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*2.3*woodM*(0.7+0.3*v)*(1-0.5*u)*smooth(0.12,0.85,grown)),0,2); 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.12 && i%2===0 && rs()<bears*leafKeep){ const side=((sideSeq+(i/2|0))%2===0)?1:-1; leafTuft(pos[0],pos[1], Math.atan2(py*side,px*side), rs); } if(order>=2 && i%7===4){ if(podF>0.25 && rs()<podF*0.4) seedPod(pos[0],pos[1],rs); else if(bloom>0.1 && rs()<bloom*D.flowerAb*0.3) pomCluster(pos[0]+(rs()*2-1)*GRID*2, pos[1]+(rs()*2-1)*GRID*2, budF>0.25 && rs()<0.4, rs); } while(canChild && ci<pts.length && u>=pts[ci] && budget>0){ const side=sides[ci], div=(order===0?(0.55+0.55*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.05:0.10)*(1-D.reach*0.3); const cv=vigor*(order===0?(0.72+0.26*rs()):(0.45+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.42,0.9,D.dominance); grow(pos[0],pos[1], dir+angTo(dir,UP)*(order<=1?0.28:0.14)+gauss(rs)*0.05, leadV, order+1, birthM+growWin, pid*7+1, sideSeq+1); } else if(grown>=1){ if(rs()<leafKeep) leafTuft(pos[0],pos[1], dir, rs); const dn=Math.atan2(-Math.sin(dir),-Math.cos(dir)); if(rs()<0.6*leafKeep) leafTuft(pos[0],pos[1], dn, rs); if(podF>0.3 && rs()<podF*0.4) seedPod(pos[0],pos[1],rs); else if(bloom>0.12 && rs()<bloom*D.flowerAb*0.5) pomCluster(pos[0],pos[1], budF>0.25 && rs()<0.4, rs); } else if(order>=2){ if(rs()<leafKeep) leafTuft(pos[0],pos[1], dir, 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 / Stand / Grove ===================== const roll=rng('setting',st.seed)(); const nC = roll<0.30?0 : roll<0.62?1 : roll<0.88?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*(150+i*70+rc()*46), by:baseY-(8+i*12+rc()*20), S:lerp(0.6,0.4,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(paintWattle); paintWattle({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.30?'Solo' : roll<0.62?'Pair' : roll<0.88?'Stand' : 'Grove'; const wintery=(env.season==='Winter'||env.season==='Spring'); return [ ['Setting', setting], ['Bloom', st.diedAt>0?'Pods':(env.m>=0.55 && env.rewards>0.3 ? (wintery?'Golden':'Buds') : '—')], ['Form', st.diedAt>0?'Pods + bare':'Domed'], ]; } })); })();
nFolF); for(let i=0;i<nFol;i++){ const rl=rng('leaf',seed,i), side=i%2?1:-1, emerge=i===nFol-1?clamp(nFolF-(nFol-1),0.2,1):1, u=(nFol>1? i/(nFol-1):0); const nodeX=rootX + windLean*38*u*S + (rl()*2-1)*5, nodeY=by-4 - u*axisH*0.96, spread=lerp(1.18,0.46,u); const ang=UP + side*spread + windLean*0.3 + (rl()*2-1)*0.12, len=(46+78*m)*lerp(1.15,0.5,u)*(0.78+0.34*rl())*emerge*S; lobedLeaf(nodeX,nodeY,ang,len,rl); } const boltF=smooth(0.24,0.62,m), nStemF=clamp(boltF*(0.8 + density*4.2*D.stems) + (rng('ns',seed)()*2-1)*0.4, 0, 5), nStem=Math.ceil(nStemF); function growStem(sx, hLen, nod, rs){ const steps=Math.max(3,Math.ceil(hLen/GRID)), wob=(rs()*2-1)*0.6, ph=rs()*6.28, aim=UP+windLean*0.7; let dir=UP+windLean*0.5, pos=[sx,by-4]; for(let s=1;s<=steps;s++){ const u=s/steps, exposure=clamp((by-pos[1])/520,0,1); dir += angTo(dir,aim)*0.06 + Math.sin(u*5+ph+wob*6)*0.009*D.waviness + sen*0.045*exposure; if(nod>0 && u>0.66) dir += angTo(dir,DOWN)*nod*0.17*smooth(0.66,1,u); const cd=[Math.cos(dir),Math.sin(dir)]; pos=[pos[0]+cd[0]*GRID, pos[1]+cd[1]*GRID]; C.push(pos[0],pos[1], 50, dirSym(cd[0],cd[1]), shade(stemC,rs), 'stem'); if(s%3===0 && rs()<0.5*D.bristly){ const px=-cd[1], py=cd[0], sd=rs()<0.5?1:-1; C.push(pos[0]+px*sd*GRID, pos[1]+py*sd*GRID, 48, '-', shade(leafD,rs), 'leaves'); } } 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'], ['Fo
full height const fx0 = cx + fside*122*S + posJit*16*S; // blooms fan across the plant's full span const targetH = ((by-padTop) + lerp(18+28*m, 205+225*m, Math.pow(hr,1.9))*S) * (0.85+0.18*v)*standC; const shortH = (by-padTop) + (14+12*m)*S; // just-emerged: a bud peeking above the pads const ph = lerp(shortH, Math.max(shortH,targetH), smooth(0,1,fgrow)); // the peduncle RISES as the flower grows const lean = leanJit*0.10 + fside*0.05 + windDir*windStr*(0.22+0.45*hr)*fgrow; const top = rise(fx0, by+2, ph, lean, 52+f, P.stalk, true, rng('flStem',st.seed,pseed,f)); // stem on its own stream const R = (40+42*m)*S*(0.88+0.14*Rjit); // blooms SMALLER than the pads (lotus proportions) const hRs = rng('flHead',st.seed,pseed,f); // head on its own stream → stable regardless of stem height const localPod = clamp(podDrive + f*0.06 + (dead?1:0), 0, 1); const opening = smooth(0.50,0.92,fgrow) * clamp(bloomDrive*1.7, 0, 1); // opens near full height, gated by season/rewards if(dead || localPod>0.5) drawPod(top[0], top[1], R*0.6, clamp(sen*1.1+(season==='Autumn'?0.3:0)+(dead?0.4:0),0,1), 0.04*sgn, hRs); else if(opening>0.40) drawBloom(top[0], top[1], R, opening, hRs); else { const ripe=clamp(Math.max(opening/0.40, smooth(0.50,0.95,fgrow)*0.8), 0, 1); // green when young → colours up as it nears bloom drawBud(top[0], top[1], R*0.56, 0.14*sgn, ripe, fgrow, hRs); } } } // ===================== COLONY: 1 → many plants from the seed ===================== // Lotuses grow in STANDS — default to a pond BUNCH (solo is now the minority). Companions SCATTER // across the FULL width (stratified slots L→R, so neither side is empty), near (big, low, opaque) → // far (small, high, faint) for depth. Big bunches put many plants + many blooms across the canvas. const cr=rng('colony',st.seed), roll=cr(); const nC = roll<0.14 ? 0 : roll<0.36 ? 1+Math.floor(cr()*2) // 1–2 : roll<0.68 ? 2+Math.floor(cr()*3) // 2–4 : 4+Math.floor(cr()*6); // 4–9 (a full pond bunch) const comps=[]; for(let k=0;k<nC;k++){ const r=rng('comp',st.seed,k); const frac=nC>1? k/(nC-1) : 0.5; // stratified L edge → R edge so BOTH sides fill const slot=clamp(frac + (r()*2-1)*(0.7/nC), 0, 1); // + jitter (still spans the full width) const cxP=clamp(FRAME+45 + slot*(W-2*FRAME-90), FRAME+40, W-FRAME-40); const depth=clamp(0.10+0.85*r(),0,1); const S=lerp(0.80,0.34,depth), fade=lerp(0.20,0.64,depth); const byP=waterY - depth*46; // far plants sit higher (toward the horizon) const mm=clamp(m + (r()*2-1)*0.32, 0.18, 1), vv=clamp(v*(0.6+0.55*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, density:clamp(mm*1.05+(r()*2-1)*0.15, 0.15, 1), rewards:clamp(rewards*(0.55+0.7*r()), 0, 1), reproOK:mm>=0.55, browny:clamp(Math.max(1-vv,ssn),0,1), dead:false, pseed:'c'+k, depth}); } comps.sort((a,b)=>b.depth-a.depth).forEach(paintLotus); // far (faint) → near paintLotus({cx:baseX, by:waterY, S:nC>4?0.95:nC>0?1.05:1.14, fade:0, m, v, sen, density, rewards, reproOK, browny, dead, pseed:'hero'}); }, traits(st,env){ const cr=rng('colony',st.seed), roll=cr(); const nC = roll<0.14 ? 0 : roll<0.36 ? 1+Math.floor(cr()*2) : roll<0.68 ? 2+Math.floor(cr()*3) : 4+Math.floor(cr()*6); const total = 1 + nC; const vx=rng('variety',st.seed,'hero')(), variety = vx<0.42?'Pink':vx<0.80?'White':'Rose'; const florif = 0.7 + 0.6*rng('florif',st.seed,'hero')(); const nFlow = env.reproOK ? clamp(Math.round((1 + env.rewards*3.4)*florif), 1, 5) : 0; const wS=Math.pow(rng('windS',st.seed)(),2.6), wArrow=rng('windD',st.seed)()<0.5?'←':'→'; const wind = wS<0.12?'Still' : (wS<0.4?'Breezy':wS<0.7?'Windswept':'Storm')+' '+wArrow; return [ ['Pads', clamp(Math.ceil(1+env.density*4),1,5)], ['Flowers', st.diedAt>0?'—':nFlow], ['Bloom', st.diedAt>0?'Dry showerhead' : (env.m>=0.55 && env.rewards>0.3 ? (env.season==='Summer'?'Open':'In bud') : '—')], ['Variety', variety], ['Colony', total===1?'Solo':total===2?'Pair':total<=4?'Cluster':'Bunch'], ['Wind', wind], ]; } })); })(); ; /* species/maple.js */ /* maple — species index 9. GENERATED from prototypes/japanese-maple.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(9, spec); /* Japanese Maple — Acer palmatum. A spreading, layered specimen tree whose TRADEMARK is the deeply-lobed PALMATE STAR leaf (5 or 7 pointed lobes from one point, middle lobe longest), drawn as a solid '*'-filled silhouette so it reads. DRAMATIC day-of-year colour arc: bare winter twigs+buds → red-bronze spring flush → green (or burgundy cultivar) summer → a MOTTLED blaze of gold/orange/scarlet autumn (each leaf turns at its own pace) → brown & fall. Winged SAMARAS (keys) + tiny red spring florets. Leaves/samaras/flowers stay LITERAL (scheme-exempt). Ported to the shared core. */ const SP = window.SEEDLING_SPECIES.find(x=>x.key==='maple'); function mapleDNA(seed){ const dd=rng('dna',seed), g=()=>(dd()+dd()+dd()-1.5)/1.5; return { dominance:0.18+0.32*dd(), divergence:0.6+0.5*dd(), droop:0.35+0.5*dd(), gnarl:0.3+0.5*dd(), taperLen:0.66+0.12*dd(), lateralN:1.0+0.8*dd(), reach:0.9+0.5*dd(), lean:g()*0.12, stature:0.84+0.2*dd(), maxOrder:4+(dd()<0.5?1:0), leafiness:0.8+0.4*dd(), lobes:dd()<0.5?5:7, cultivar:Math.pow(dd(),3.2)*0.9, autumnHue:dd(), greenHold:0.25+0.5*dd(), samaraAb:0.4+0.6*dd(), windDir:dd()<0.5?1:-1, windStr:Math.pow(dd(),2.5) }; } mount(Object.assign(window.SEEDLING_toSpec(SP), { title:'Japanese Maple', sub:'acer v2', senFrom:0.85, schemeExempt:['flowers','leaves'], // palmate stars + samaras carry literal seasonal colour LW:{season:1.0, stem:1.15, leaves:1.05, flowers:1.4}, LS:{season:1.0, stem:1.0, leaves:1.05, flowers:0.85}, note:'Spreading layered tree · palmate star leaves · deciduous colour arc (bare → spring red → green → autumn blaze) · winged samaras · mostly solitary specimen.', PAL:{ soil:'#6f5128', dry:'#7a5a30', grey:'#9a8f74', bark:'#7A5E48', barkDk:'#523E2C', green:'#4E8A38', greenLt:'#79B24E', burgundy:'#9A3A4E', springRed:'#D85A44', gold:'#F2C233', orange:'#F0801E', scarlet:'#E22820', crimson:'#B81830', brown:'#9A6A34', flower:'#B81E32', samara:'#C88A6A', samaraDry:'#9A7E4A', bud:'#8A4A3C' }, stageName(st,env){ if(st.diedAt>0)return'Sealed'; const m=env.m, doy=(((env.now+70)%365)+365)%365; if(env.ageF>0.86)return'Veteran'; if(m<0.12)return'Sapling'; if(m<0.4)return'Young'; if(m<0.7)return'Branching'; if(doy<60||doy>344)return'Dormant'; if(doy>248)return'Ablaze'; return'Crown'; }, draw(st, C, env){ const {m,v,sen,season,density,rewards,ageF,baseX,baseY,now}=env, P=this.PAL; const bgC = BACKDROPS[backdropFor(st.seed)]; // ---- the DRAMA: day-of-year drives a deciduous colour arc (shared across all plants) ---- // GERM offset: a maple germinates in SPRING, so age 0 maps to leaf-out (a new seedling is leafy, // not a bare winter twig). The bare-winter dormancy then falls on older, established trees. const GERM=70, doy=(((now+GERM)%365)+365)%365; const leafOut=clamp(smooth(56,84,doy)*(1-smooth(330,358,doy)),0,1); // bare winter → flush spring → fall late autumn const springFlush=smooth(58,82,doy)*(1-smooth(96,134,doy)); // new red-bronze growth const fire=smooth(248,300,doy); // autumn colour ramps up const fadeAut=smooth(322,352,doy); // late autumn: browning before fall const foliageF=clamp(leafOut*(1-sen*0.85)*(0.45+0.55*v),0,1); // canopy payoff ← care (vitality), not reproduction const bareWinter = leafOut<0.25; function paintMaple(I){ const {seed, artSeed, cx, by, S, dim, m, density, v, sen, rewards} = I; const FC = c => dim>0 ? mix(c, bgC, dim) : c; const D=mapleDNA(seed); const bark=FC(mix(P.bark,P.grey,Math.max((1-v)*0.3,sen*0.5))), barkDk=FC(mix(P.barkDk,P.grey,Math.max((1-v)*0.3,sen*0.5))); // per-leaf seasonal colour — mottled so leaves turn at their own pace function leafColor(rs){ let c=mix(P.green, P.burgundy, D.cultivar); c=mix(c, P.springRed, springFlush*(0.45+0.4*rs())); const turn=clamp(fire*1.75 - rs()*0.45 - D.greenHold*0.28, 0, 1); const au=mix(P.gold, mix(P.orange,P.scarlet,D.autumnHue), 0.2+0.6*rs()); c=mix(c, au, turn); c=mix(c, P.brown, fadeAut*0.55*(0.6+0.4*rs())); c=mix(c, P.dry, sen*0.4); return FC(shade(c,rs)); } // THE TRADEMARK: deeply-lobed PALMATE STAR leaf (solid '*' fill so the silhouette reads) function mapleLeaf(cx0,cy0,baseAng,size,rs){ if(foliageF<=0.04) return; const col=leafColor(rs), edgeCol=FC(shade(mix(col,'#000000',0.16),rs)); const nL=D.lobes, notch=0.5, spreadMax=2.6; for(let gx=-size;gx<=size;gx+=GRID) for(let gy=-size;gy<=size;gy+=GRID){ const r=Math.hypot(gx,gy); let phi=Math.atan2(gy,gx)-baseAng; phi=Math.atan2(Math.sin(phi),Math.cos(phi)); if(Math.abs(phi)>spreadMax && r>GRID*1.2) continue; const frac=clamp((phi+spreadMax)/(2*spreadMax),0,1); const lobeWave=Math.pow(Math.abs(Math.cos(frac*(nL-1)*Math.PI)),2.0); const sideTaper=0.62+0.38*Math.cos(phi); const boundary=Math.max(GRID*0.8, size*sideTaper*(notch+(1-notch)*lobeWave)); if(r<=boundary){ const edge=r>boundary-GRID*1.0; C.push(cx0+gx,cy0+gy, 56, '*', edge?edgeCol:col, 'leaves'); } } } function leafSpray(x,y,twigDir,size,rs){ const px=-Math.sin(twigDir), py=Math.cos(twigDir), sd=rs()<0.5?1:-1; let pa=Math.atan2(py*sd,px*sd); pa += angTo(pa,UP)*0.4; const ex=x+Math.cos(pa)*GRID*2.4*S, ey=y+Math.sin(pa)*GRID*2.4*S; C.push((x+ex)/2,(y+ey)/2, 55, dirSym(Math.cos(pa),Math.sin(pa)), FC(shade(P.springRed,rs)), 'leaves'); mapleLeaf(ex,ey, pa, size*(0.85+0.3*rs()), rs); } function samara(x,y,rs){ const col=FC(mix(P.samara, P.samaraDry, Math.max(fadeAut,fire*0.5,sen))); for(const sd of [1,-1]){ let dir=DOWN + sd*(0.55+0.2*rs()), pos=[x,y]; const len=3+Math.floor(rs()*2); for(let i=1;i<=len;i++){ dir+=angTo(dir,DOWN)*0.12; pos=[pos[0]+Math.cos(dir)*GRID, pos[1]+Math.sin(dir)*GRID]; C.push(pos[0],pos[1],84,dirSym(Math.cos(dir),Math.sin(dir)),shade(col,rs),'flowers'); } C.push(x+sd*GRID*0.4, y+GRID*0.6, 85, '.', FC(shade(mix(P.brown,col,0.4),rs)), 'flowers'); } } function flowerCluster(x,y,rs){ const n=3+Math.floor(rs()*3); for(let k=0;k<n;k++){ const a=rs()*TAU, r=GRID*(1+rs()); C.push(x+Math.cos(a)*r, y+Math.sin(a)*r+GRID*0.5, 88, '.', FC(shade(P.flower,rs)), 'flowers'); } C.push(x,y,89,'.',FC(shade(mix(P.flower,'#000000',0.2),rs)),'flowers'); } function bud(x,y,rs){ C.push(x,y,80,'O',FC(shade(P.bud,rs)),'flowers'); C.push(x,y-GRID*0.6,81,'.',FC(shade(mix(P.bud,P.springRed,springFlush),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=(52+46*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.045; 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'); } if(bareWinter){ bud(pos[0],pos[1],rs); return; } // juvenile seedling carries a small SPRAY of first palmate leaves (not a bare whip with one leaf) const nl=1+Math.round(clamp(g,0,1)*2.4); // 1–3 first leaves for(let k=0;k<nl;k++){ const a=UP + (k-(nl-1)/2)*0.52 + (rs()*2-1)*0.16; leafSpray(pos[0],pos[1], a, (6.5+6*g)*S, rs); } if(g>0.5){ const md=steps>3?Math.floor(steps*0.6):2, mp=[rx+Math.cos(UP)*GRID*md, by-3+Math.sin(UP)*GRID*md]; // a lower pair along the stem for(const sd of [1,-1]) if(rs()<0.7) leafSpray(mp[0],mp[1], UP+sd*0.7, (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-300)/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('mb', seed, pid); let run=baseRun*Math.pow(D.taperLen,order)*(0.82+0.36*rs())*grown; if(order===0) run*=0.82; 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.045*(1+order*0.12), bears=order>=2 ? D.leafiness : 0; const canChild = grown>=1 && order<maxOrder; const nLat = order===0 ? 2+Math.floor(rs()*3) : clamp(Math.round(D.lateralN*(1-0.35*D.dominance)+gauss(rs)*0.9),0, order<2?3:2); const lo=order===0?0.22:0.30, hi=0.93, 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.6/Math.max(nLat,1)), 0.1, 0.95)); 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.12,0.035,clamp(order/3,0,1))*Math.max(0,1-order/3.2), grav=0.12*D.droop*smooth(1.4,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])/600,0,1); dir += D.windDir*D.windStr*0.06*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.9*woodM*(0.7+0.3*v)*(1-0.5*u)*smooth(0.12,0.85,grown)),0,2); 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(order>=2 && u>0.1 && i%3===1){ if(bareWinter){ if(rs()<0.22) bud(pos[0],pos[1],rs); } else if(rs()<bears){ const side=((sideSeq+(i/2|0))%2===0)?1:-1; leafSpray(pos[0],pos[1], Math.atan2(py*side,px*side), (12+11*m)*(0.85+0.3*rs())*S, rs); } if(springFlush>0.3 && rewards>0.25 && rs()<0.05) flowerCluster(pos[0],pos[1],rs); if((fadeAut>0.2 || (springFlush<0.2 && leafOut>0.6 && doy>150)) && rewards>0.25 && rs()<0.05*D.samaraAb) samara(pos[0],pos[1],rs); } while(canChild && ci<pts.length && u>=pts[ci] && budget>0){ const side=sides[ci], div=(order===0?(0.55+0.55*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.05:0.08)*(1-D.reach*0.3); const cv=vigor*(order===0?(0.72+0.26*rs()):(0.45+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.38,0.9,D.dominance); grow(pos[0],pos[1], dir+angTo(dir,UP)*(order<=1?0.26:0.12)+gauss(rs)*0.06, leadV, order+1, birthM+growWin, pid*7+1, sideSeq+1); } else if(grown>=1){ if(bareWinter){ bud(pos[0],pos[1],rs); } else { leafSpray(pos[0],pos[1], dir, (15+13*m)*S, rs); if(rs()<0.5) leafSpray(pos[0],pos[1], dir+0.7, (13+11*m)*S, rs); } } else if(order>=2 && !bareWinter){ leafSpray(pos[0],pos[1], dir, (13+10*m)*S, rs); } } const rootX=cx - D.windDir*D.windStr*48*S; sprout(rng('sprout',seed), rootX); grow(rootX, by-3, UP+D.lean*0.6+D.windDir*D.windStr*0.07, 1, 0, 0, 1, 0); } // ===================== SETTING: maples are specimen trees (mostly Solo) ===================== const roll=rng('setting',st.seed)(); const nC = roll<0.5?0 : roll<0.82?1 : 2+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*(170+i*70+rc()*40), by:baseY-(4+i*10+rc()*16), S:lerp(0.58,0.42,f)*(0.9+0.2*rc()), dim:lerp(0.42,0.60,f), m:clamp(m+(rc()*2-1)*0.06,0.04,1), density, v, sen, rewards, depth:f}); } comps.sort((a,b)=>b.depth-a.depth).forEach(paintMaple); paintMaple({seed:st.seed, artSeed:st.artSeed, cx:baseX, by:baseY, S:1.0, dim:0, m, density, v, sen, rewards}); }, traits(st,env){ const D=mapleDNA(st.seed); const roll=rng('setting',st.seed)(); const setting = roll<0.5?'Solo' : roll<0.82?'Pair' : 'Grove'; const doy=(((env.now+70)%365)+365)%365; const colour = (doy<56||doy>352)?'Bare' : doy<96?'Spring red' : doy<240?'Green' : doy<300?'Turning' : 'Autumn fire'; const cult = D.cultivar>0.55?'Purple' : D.cultivar>0.25?'Bronze' : 'Green'; const auh = D.autumnHue>0.6?'Scarlet' : D.autumnHue>0.3?'Orange' : 'Gold'; return [ ['Setting', setting], ['Colour now', colour], ['Lobes', D.lobes], ['Cultivar', cult], ['Autumn', auh], ['Wind', D.windStr<0.12?'Still':D.windStr<0.40?'Breezy':D.windStr<0.70?'Windswept':'Storm-bent'], ]; } })); })(); ; /* species/poppy.js */ /* poppy — species index 1. GENERATED from prototypes/field-poppy.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(1, spec); /* Field Poppy — Papaver rhoeas. A leafy clump of deeply pinnately-lobed (fern-like) leaves, out of which slender hairy stems BOLT and each runs its own lifecycle: a nodding green bud → straightens → 4 papery scarlet petals with a dark blotch + stamen ring → a ribbed pepper-pot seed CAPSULE. An older plant carries all stages at once. Coherent WIND leans the whole plant. Monocarpic. Scatters in drifts → a rare Field carpet. Ported to the shared core. */ const SP = window.SEEDLING_SPECIES.find(x=>x.key==='poppy'); mount(Object.assign(window.SEEDLING_toSpec(SP), { title:'Field Poppy', sub:'papaver v2', senFrom:0.80, note:'Lobed fern-like rosette · slender hairy stems bolt and bloom · nodding bud → 4 papery scarlet petals → ribbed capsule (all stages at once) · wind-leaned · scatters into drifts/fields.', PAL:{ soil:'#6f5128', dry:'#7a5a30', stem:'#5E7B3A', stemDry:'#9A8A52', leafDark:'#46662F', leafMid:'#6E8B4E', leafLight:'#8BA86A', petal:'#E0382C', petal2:'#C42718', petalEdge:'#F0664E', blotch:'#2A0E07', center:'#1D1208', stamen:'#241606', bud:'#7E924E', capsule:'#8FA05C', capsuleDry:'#A6924E', pore:'#241a0c' }, stageName(st,env){ if(st.diedAt>0)return'Sealed'; const m=env.m; if(env.ageF>0.85)return'Senescent'; if(m<0.12)return'Sprout'; if(m<0.40)return'Rosette'; if(m<0.58)return'Budding'; if(m<0.80)return'Blooming'; return'Seeding'; }, draw(st, C, env){ const {m,v,sen,season,browny,density,rewards,ageF,baseX,baseY}=env, P=this.PAL; const bgC = BACKDROPS[backdropFor(st.seed)]; const seasonBloom = season==='Summer'?1 : season==='Spring'?0.9 : season==='Autumn'?0.7 : 0.55; function poppyDNA(seed){ const dd=rng('dna',seed), g=()=>(dd()+dd()+dd()-1.5)/1.5; return { stems:0.7+0.9*dd(), spread:26+26*dd(), stature:0.84+0.30*dd(), waviness:0.5+1.0*dd(), petalRag:0.10+0.30*dd(), petalHue:g()*0.5, leafCut:0.55+0.45*dd(), bristly:0.5+0.5*dd(), windDir:dd()<0.5?1:-1, windStr:Math.pow(dd(),2.3) }; } // ===================== one poppy plant (repeated to scatter into a drift/field) ===================== function paintPoppy(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 ra=rng('struct',artSeed), D=poppyDNA(seed); const stemC=FC(mix(P.stem,P.stemDry,Math.max((1-v)*0.5,sen))); const leafD=FC(mix(P.leafDark,P.stemDry,browny)), leafM=FC(mix(P.leafMid,P.stemDry,browny)), leafL=FC(mix(P.leafLight,P.stemDry,browny*0.9)); const petalC=FC(mix(P.petal,P.petal2,clamp(-D.petalHue,0,1)*0.7)), petalWarm=FC(mix(mix(P.petal,P.petal2,clamp(-D.petalHue,0,1)*0.7),'#F08A3C',clamp(D.petalHue,0,1)*0.6)); const capC=FC(mix(P.capsule,P.capsuleDry,Math.max(sen,browny*0.55))); const greenOf=rs=>shade(mix(leafM, rs()<0.5?leafD:leafL, rs()),rs); const windLean=D.windDir*D.windStr*0.42, rootX=cx - D.windDir*D.windStr*46*S; function lobedLeaf(bx,byy,ang,len,rl){ const steps=Math.max(3,Math.ceil(len/GRID)), droop=clamp(sen+(1-v)*0.4,0,1)*0.7; let dx=Math.cos(ang),dy=Math.sin(ang); dx=lerp(dx,0,droop); dy=lerp(dy,1,droop); const n=Math.hypot(dx,dy)||1; dx/=n; dy/=n; let dir=Math.atan2(dy,dx), pos=[bx,byy]; for(let s=1;s<=steps;s++){ const u=s/steps; dir += Math.sin(u*3.0+rl()*6)*0.04; const cd=[Math.cos(dir),Math.sin(dir)]; pos=[pos[0]+cd[0]*GRID, pos[1]+cd[1]*GRID]; C.push(pos[0],pos[1], 42, dirSym(cd[0],cd[1]), shade(leafM,rl), 'leaves'); const px=-cd[1], py=cd[0], ev=Math.sin(Math.PI*Math.min(u,0.999)), lobeLen=GRID*(0.8+(2.4*D.leafCut)*ev); if(s%2===0 && ev>0.18){ for(const sd of [1,-1]){ let la=Math.atan2(py*sd,px*sd); la += angTo(la,dir)*0.30; const ll=lobeLen*(0.7+0.5*rl()), lsteps=Math.max(1,Math.round(ll/GRID)); for(let t=1;t<=lsteps;t++){ const lp=[pos[0]+Math.cos(la)*GRID*t, pos[1]+Math.sin(la)*GRID*t], edge=t>=lsteps; C.push(lp[0],lp[1], 41, edge?'X':dirSym(Math.cos(la),Math.sin(la)), edge?shade(leafD,rl):greenOf(rl), 'leaves'); } } } } } const foliageF=(1-0.5*sen), axisH=(78+150*m)*(0.85+0.30*D.stature)*foliageF*S; { const rl=rng('axis',seed); let dir=UP+windLean*0.4, pos=[rootX,by-4]; const steps=Math.max(2,Math.ceil(axisH/GRID)); for(let s=1;s<=steps;s++){ const u=s/steps; dir += Math.sin(u*2+rl()*3)*0.02 + windLean*0.03*u; pos=[pos[0]+Math.cos(dir)*GRID, pos[1]+Math.sin(dir)*GRID]; C.push(pos[0],pos[1],46,dirSym(Math.cos(dir),Math.sin(dir)),shade(stemC,rl),'stem'); } } const nFolF=clamp(3 + density*6*foliageF + (rng('nl',seed)()*2-1)*0.6, 2.5, 10), nFol=Math.ceil(
for(let i=1;i<=steps;i++){ const u=i/steps; dir += angTo(dir,ang0)*0.05 + Math.sin(i*0.5+rs()*6)*0.009 + a0*0.004*u; const dx=Math.cos(dir),dy=Math.sin(dir); pos=[pos[0]+dx*GRID, pos[1]+dy*GRID]; tip=[pos[0],pos[1]]; tdir=dir; C.push(pos[0],pos[1], stemZ, dirSym(dx,dy), shade(stemC,rs), 'stem'); if(u<leafTop && i%2===0 && rs() < (u<0.28?0.45:0.85)){ // OPPOSITE linear leaves, airier near the base for(const sd of [1,-1]){ const la=dir + sd*(0.5+0.18*rs()) - 0.05; const ll=2+Math.floor((2+m*3)*S*(0.6+0.5*rs())); let lp=[pos[0],pos[1]]; for(let kk=1;kk<=ll;kk++){ const ax=Math.cos(la),ay=Math.sin(la); lp=[lp[0]+ax*GRID,lp[1]+ay*GRID]; const lt=kk>=ll-1; C.push(lp[0],lp[1], leafZ+(lt?2:0), dirSym(ax,ay), lt?shade(leafD,rs):greenOf(rs), 'leaves'); } } } } if(stemFlower){ // tip FLOWER-SPIKE — develops with the stem's own age const stemOpen=smooth(0.4,1.0,grown); const open=clamp(smooth(0.18,0.82,flowering)*D.openBias*seasonOpen*stemOpen,0,1); const sLen=(30+66*m)*smooth(0.05,0.7,flowering)*D.spikeF*stemOpen*(0.72+0.5*rs())*S; const sW=GRID*(1.5+0.8*rs())*Math.sqrt(S), sst=Math.max(3,Math.ceil(sLen/GRID)); let sp=[tip[0],tip[1]], d=tdir; for(let i=1;i<=sst;i++){ const u=i/sst; d += angTo(d,UP)*0.16; sp=[sp[0]+Math.cos(d)*GRID, sp[1]+Math.sin(d)*GRID]; const taper=Math.min(1,u*4)*(1-Math.pow(u,1.7)), hw=sW*taper, px=-Math.sin(d), py=Math.cos(d); for(let w=-hw; w<=hw; w+=GRID){ if(rs()<0.12) continue; const fx=sp[0]+px*w, fy=sp[1]+py*w, localOpen=clamp(open*(1.25-u),0,1), isOpen=rs()<localOpen*0.85; const col = isOpen ? shade(mix(cFlower, cFlowerLt, rs()*0.7), rs) : shade(mix(cBud, cFlowerDk, localOpen), rs); const sym = isOpen ? (rs()<0.5?'+':'X') : (rs()<0.6?'.':'O'); C.push(fx, fy, flZ, sym, col, 'flowers'); } } } } } // ===================== SETTING: a patch — Solo / Pair / Cluster / Field ===================== const sr=rng('setting',st.seed), roll=sr(); const nC = roll<0.18?0 : roll<0.50?1 : roll<0.82?2 : 3+Math.floor(rng('fn',st.seed)()*2); const comps=[]; for(let i=0;i<nC;i++){ const rc=rng('comp',st.seed,i); const side=(i%2===0?1:-1), f=i/Math.max(1,nC); const scale=lerp(0.72,0.46,f)*(0.9+0.2*rc()), off=side*(80+i*46+rc()*46), raise=(34+i*24+rc()*30); comps.push({cx:baseX+off, by:baseY-raise, S:scale, fade:lerp(0.5,0.74,f), m, v, sen, browny, density:clamp(density+(rc()*2-1)*0.12,0.05,1), rewards, zb:-10, pseed:'c'+i, depth:f}); } comps.sort((a,b)=>b.depth-a.depth).forEach(paintBush); // far (faint) → near paintBush({cx:baseX, by:baseY, S:1.0, fade:0, m, v, sen, browny, density, rewards, zb:0, pseed:'hero'}); }, traits(st,env){ const sr=rng('setting',st.seed), roll=sr(); const setting = roll<0.18?'Solo' : roll<0.50?'Pair' : roll<0.82?'Cluster' : 'Field'; return [ ['Setting', setting], ['Stems', clamp(Math.round(1+env.density*14),1,15)], ['Stature', Math.round((110+230*env.m)*(0.7+0.3*env.v))+' px'], ]; } })); })(); ; /* species/lotus.js */ /* lotus — species index 10. GENERATED from prototypes/sacred-lotus.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(10, spec); /* Sacred Lotus — Nelumbo nucifera. An aquatic plant: large peltate pads with a WAVY / undulate margin and radial veins, held on SHORT petioles low over the water (heavy leaves don't reach far up; the newest leaf is a small cupped emerging blade), and a solitary red-pink BLOOM raised above the foliage on its own taller peduncle — broad layered petals around a golden receptacle with a fringe of stamens. Lifecycle on the peduncle: nodding BUD → open BLOOM → green POD → dry "showerhead" seed pod. Lotuses often grow in a stand: a seed-driven COLONY renders 1 → many plants (solo to a pond bunch). Summer bloom, winter pad-dieback. */ const SP = window.SEEDLING_SPECIES.find(x=>x.key==='lotus'); mount(Object.assign(window.SEEDLING_toSpec(SP), { title:'Sacred Lotus', sub:'nelumbo v4', senFrom:0.82, note:'Aquatic · big wavy pads low on the water, side-view cup blooms (pointed petals + golden stamen crown) on stems of varied height → showerhead seed pod · flower varieties Pink / White / Rose · coherent WIND bends the stalks downwind · 1–5 flowers per root, spread both sides · solo to a full pond bunch scattered across the water.', PAL:{ soil:'#6a7a5a', stalk:'#6E9A6B', stalkDk:'#4C7754', speck:'#365E3C', pad:'#6E9A5E', padDk:'#3E6B43', padLt:'#9CC07E', padVein:'#33572F', padEdge:'#274A29', padYellow:'#C7A24E', petal:'#E0796E', petalLt:'#F7D6CE', petalDk:'#C53A30', petalVein:'#962017', center:'#E8B23A', centerDk:'#B07A1E', stamen:'#EFC85C', pod:'#7E9A55', podRim:'#5A7A3C', podDry:'#8A714A', podDryDk:'#5A4730', pit:'#3E2E1A' }, stageName(st,env){ if(st.diedAt>0)return'Dry pod'; const m=env.m; if(m<0.15)return'Sprout'; if(m<0.50)return'Floating pads'; if(m<0.72)return'Budding'; return'Bloom'; }, draw(st, C, env){ const {m,v,sen,season,browny,density,rewards,reproOK,dormancy,dead,baseX,baseY}=env, P=this.PAL; const winter=dormancy||0; const bgC = BACKDROPS[backdropFor(st.seed)]; const waterY = baseY - 8; // plants root at the bottom (like the other species), grow UP from here const seasonBloom = season==='Summer'?1 : season==='Spring'?0.7 : season==='Autumn'?0.45 : 0.30; // monsoon bias, floored // per-seed POND environment, shared by every plant in the colony const windDir = rng('windD',st.seed)()<0.5?-1:1; const windStr = Math.pow(rng('windS',st.seed)(),2.6); // 0..1, skewed LOW → mostly calm, rarely a storm function varietyPal(key){ const x=rng('variety',st.seed,key)(); // per-PLANT flower colour (so a pond can mix pink + white) if(x<0.42) return {name:'Pink', lt:'#F7D6CE', pt:'#E0796E', dk:'#C53A30', vn:'#962017'}; if(x<0.80) return {name:'White', lt:'#FFFFFF', pt:'#F6F1EA', dk:'#EBA48C', vn:'#C56A50'}; // white petals, soft rose-flushed tips + rim (reads white & pops on cream) return {name:'Rose', lt:'#F3C4D4', pt:'#D85A7A', dk:'#A82A52', vn:'#7C1B3C'}; } // ===================== one lotus plant (repeated to form a colony) ===================== function paintLotus(I){ const {cx, by, S, fade, m, v, sen, density, rewards, reproOK, browny, dead, pseed} = I; const FC = c => fade>0 ? mix(c, bgC, fade) : c; const PV = varietyPal(pseed); // this plant's petal palette (Pink / White / Rose) const collapse = clamp(1-0.45*sen-0.42*winter, 0.18, 1); // pads sink & yellow as they die back const standC = clamp(1-0.12*sen-0.42*winter, 0.55, 1); // woody flower/pod stalks stay STANDING const bloomDrive = reproOK ? clamp(rewards*(0.55+0.45*seasonBloom)*(1-sen),0,1) : 0; const podDrive = reproOK ? clamp(sen*1.2 + (season==='Autumn'?0.35:0) + smooth(0.82,1.0,m)*0.5, 0,1) : 0; // -- petiole / peduncle: erect at the base, GENTLY arcing toward `lean` near the top, with a // faint sway — so stems read natural (not ramrod-straight parallel poles). -- function rise(x0,y0,h,lean,z,col,wideF,rs){ const steps=Math.max(2,Math.ceil(h/GRID)); let pos=[x0,y0], dir=UP; const sway=(rs()*2-1), swF=0.7+rs()*0.8; for(let s=1;s<=steps;s++){ const u=s/steps; const target=UP + lean*smooth(0,1,u); // straight low → curving over near the flower dir += angTo(dir, target)*0.08 + Math.sin(u*Math.PI*swF)*0.006*sway; pos=[pos[0]+Math.cos(dir)*GRID, pos[1]+Math.sin(dir)*GRID]; const px=-Math.sin(dir), py=Math.cos(dir); C.push(pos[0],pos[1],z,'|',shade(FC(col),rs),'stem'); if(wideF) C.push(pos[0]+px*GRID,pos[1]+py*GRID,z,'|',shade(FC(mix(col,P.stalkDk,0.35)),rs),'stem'); if(s%4===0) C.push(pos[0]+px*(rs()<0.5?GRID:-GRID), pos[1]+py*(rs()<0.5?GRID:-GRID), z,'.', shade(FC(P.speck),rs),'stem'); } return pos; } // -- WAVY peltate pad: undulate margin (not a clean circle) + radial veins + sinus notch -- function drawPad(px,py,R,z,yellowF,ragged,cupped,rs){ const sq = cupped? 0.60 : 0.84; const seam = rng('pr',Math.round(px),Math.round(py),pseed)()*TAU; const Rf = a => R*(1 + 0.14*Math.sin(7*a+seam) + 0.06*Math.sin(3*a-seam*1.3)); // undulate edge const notch = seam + DOWN; // sinus toward the petiole const tone = c => shade(FC(mix(c,P.padYellow,yellowF)),rs); for(let gx=-R*1.25;gx<=R*1.25;gx+=GRID)for(let gy=-R*sq*1.25;gy<=R*sq*1.25;gy+=GRID){ const a=Math.atan2(gy/sq,gx), rr=Math.hypot(gx,gy/sq), re=Rf(a); if(rr>re)continue; const rad=rr/re, cr=rng('pf',Math.round(px),Math.round(gx),Math.round(gy)); if(Math.abs(angTo(a,notch))<0.22 && rad>0.5) continue; // sinus cut if(cr()>0.78) continue; // airy stipple (ground shows through) if(rad>0.6 && ragged>0 && cr()<ragged) continue; // collapsing margin const sheen=Math.sin(a*3+seam)*0.5*(0.6-rad); const col=mix(mix(P.pad, sheen<0?P.padDk:P.padLt, Math.abs(sheen)), P.padDk, smooth(0.7,1.0,rad)*0.3 + (cupped&&gy>0?0.22:0)); C.push(px+gx,py+gy, z, dirSym(gx,gy), tone(col),'leaves'); } const nVein=cupped?9:13; for(let k=0;k<nVein;k++){ const a=seam + k/nVein*TAU; if(Math.abs(angTo(a,notch))<0.22) continue; const ca=Math.cos(a), sa=Math.sin(a)*sq, re=Rf(a); for(let t=R*0.16;t<=re*0.92;t+=GRID){ const gx=ca*t, gy=sa*t; if(ragged>0 && t>re*0.6 && rng('vg',Math.round(px),Math.round(gx),k)()<ragged) continue; C.push(px+gx, py+gy, z+0.6, dirSym(ca,sa), tone(P.padVein),'leaves'); } } for(let a=0;a<TAU;a+=0.045){ if(Math.abs(angTo(a,notch))<0.20) continue; const re=Rf(a), ca=Math.cos(a), sa=Math.sin(a)*sq; if(ragged>0 && rng('eg',Math.round(px),Math.round(a*30))()<ragged*0.8) continue; for(const rf of [0.99,0.92]) C.push(px+ca*re*rf, py+sa*re*rf, z+0.7, dirSym(ca,sa), tone(P.padEdge),'leaves'); } if(cupped) for(let a=Math.PI*1.04;a<=Math.PI*1.96;a+=0.05){ const re=Rf(a), ca=Math.cos(a), sa=Math.sin(a)*sq; // curled upper rim C.push(px+ca*re*0.85, py+sa*re*0.85, z+0.8, dirSym(ca,sa), tone(mix(P.padEdge,'#000000',0.18)),'leaves'); } C.push(px,py,z+0.9,'O',tone(P.padEdge),'leaves'); // peltate hub C.push(px,py,z+1.0,'.',tone(P.padEdge),'leaves'); } // -- one DISTINCT pointed lotus petal: almond shape, dark outline (so petals separate), // central vein, pale base → red tip. `ang` is an absolute direction; the petal grows // from a base point. Outline sits a touch higher z so it cuts over the petal behind. -- function petal(bx,by,ang,len,halfW,z,ez,pal,rs){ const nx=Math.cos(ang), ny=Math.sin(ang), px=-ny, py=nx, tn=Math.max(3,Math.ceil(len/GRID)); for(let t=1;t<=tn;t++){ const u=t/tn; const taper=1-smooth(0.5,1.0,u); // taper over the top half const hw=halfW*Math.min(1, 2.2*Math.sin(Math.PI*u*0.5))*taper*taper; // broad lower body → pointed lanceolate tip (lotus) if(hw<GRID*0.32 && u>0.5) continue; const ox=bx+nx*GRID*t, oy=by+ny*GRID*t; for(let w=-hw;w<=hw;w+=GRID){ const aw=Math.abs(w); const edge = aw>hw-GRID*1.5 || (u>0.78 && hw<halfW*0.5); // outline: sides + the pointed tip const vein = aw<GRID*0.7 && u<0.85; // central vein const grad = u<0.28?pal.base : u<0.62?mix(pal.base,pal.body,smooth(0.28,0.62,u)) : mix(pal.body,pal.tip,smooth(0.62,1,u)); const col = edge?pal.edge : vein?mix(pal.body,pal.edge,0.4) : grad; // outlines go to a HIGH shared z (ez) so EVERY petal boundary renders over the fills → petals read as separate C.push(ox+px*w, oy+py*w, edge?ez:z, '+', shade(FC(col),rs),'flowers'); } } } // a SIDE-VIEW lotus: an egg/goblet of pointed petals opening upward from the stem tip, // top petals longest (tall egg), reflexed lower petals, paler inner cup, gold stamen centre. function drawBloom(fx,fy,R,open,rs){ const PB ={base:PV.lt, body:mix(PV.pt,PV.lt,0.45), tip:PV.dk, edge:PV.vn}; // outer: pale body, saturated tip, dark rim (per variety) const PBi={base:'#ffffff', body:mix(PV.lt,'#ffffff',0.35), tip:PV.pt, edge:PV.dk}; // inner cup, paler // SIDE VIEW: the flower is a CUP / egg opening UPWARD. Petals fan UP & OUT from a NARROW base just // above the stem (which hides behind them) — NONE point straight down (that would be a top view). // A shorter, PALER FRONT whorl sits in front of the centre so we read the cup's DEPTH; the golden // stamens nestle LOW in the middle of the cup. Broad pointed petals overlap into a full flower. const fcx=fx, fcy=fy - R*0.14, ringR=R*0.15; const sp=lerp(1.04,1.38,open); const bN=7; for(let k=0;k<bN;k++){ const t=(k/(bN-1))*2-1, ang=UP+t*sp; // BACK whorl: tall outer-face cup-wall petals (behind) petal(fcx+Math.cos(ang)*ringR, fcy+Math.sin(ang)*ringR, ang, R*(1.10-0.16*Math.abs(t)), R*0.245, 66+Math.round(Math.abs(t)*2), 82, PB, rs); } const fN=6; for(let k=0;k<fN;k++){ const t=((k+0.5)/fN)*2-1, ang=UP+t*lerp(0.60,1.04,open); // FRONT whorl: shorter, PALER inner-face petals, offset between, in FRONT → cup depth petal(fcx+Math.cos(ang)*ringR*0.66, fcy+Math.sin(ang)*ringR*0.66, ang, R*(0.76-0.12*Math.abs(t)), R*0.215, 74+Math.round((1-Math.abs(t))*2), 81, PBi, rs); } // ---- the golden POLLEN crown in the MIDDLE: a ring of stamens (filament + anther) radiating // from a small central receptacle, surrounded by the petals (the lotus signature). ---- const ccx=fcx, ccy=fcy - R*0.30, baseR=R*0.075; // pollen crown LOW in the middle of the cup const nStam=Math.round(46*(0.6+0.4*open)); // dense → a fuzzy golden cluster, not a sparse star for(let k=0;k<nStam;k++){ const sr=rng('st',Math.round(fx),Math.round(fy),k); const a=(k/nStam)*TAU + (sr()*2-1)*0.22; const up=(-Math.sin(a)+1)/2; // 1 at top → 0 at bottom const r0=baseR*(0.4+1.0*sr()); // start radius jitter → fuzzy core, no clean spokes const len=R*(0.085+0.085*sr())*(0.55+0.55*up)*(0.7+0.5*open); // upper stamens longer → a crown, not a flat disc const f0x=ccx+Math.cos(a)*r0, f0y=ccy+Math.sin(a)*r0, steps=Math.max(1,Math.ceil(len/GRID)); for(let t=1;t<=steps;t++) C.push(f0x+Math.cos(a)*GRID*t, f0y+Math.sin(a)*GRID*t, 84, '.', shade(FC(mix(P.stamen,P.center,sr()*0.5)),sr),'flowers'); C.push(f0x+Math.cos(a)*len, f0y+Math.sin(a)*len, 85,'.',shade(FC(P.centerDk),sr),'flowers'); // anther tip (pollen) } // central receptacle dome (the seed-pod head), on top of the stamens const cw=R*0.13*(0.7+0.3*open), ch=cw*0.85; for(let gx=-cw;gx<=cw;gx+=GRID)for(let gy=-ch;gy<=ch;gy+=GRID){ if((gx*gx)/(cw*cw)+(gy*gy)/(ch*ch)>1)continue; const top=clamp(-gy/ch*0.5+0.5,0,1); C.push(ccx+gx,ccy+gy,87,'+',shade(FC(mix(P.centerDk,P.center,top)),rs),'flowers'); } for(let k=0;k<5;k++){ const a=k/5*TAU; C.push(ccx+Math.cos(a)*cw*0.42, ccy+Math.sin(a)*ch*0.42, 88,'O',shade(FC(mix(P.centerDk,'#5e4e1c',0.45)),rs),'flowers'); } } // young = a tight GREEN egg (sepal cover); as it ripens the coloured petals BREAK out of the tip // (colour spreads down from the top) and tips poke out — then it opens into the bloom. // `ripe` 0→1 = green→coloured/about-to-open · `grow` 0→1 = young round egg → elongated bud. function drawBud(fx,fy,R,lean,ripe,grow,rs){ const a=UP+lean, nx=Math.cos(a), ny=Math.sin(a), px=-ny, py=nx; const len=R*(1.3+0.9*grow), maxW=R*0.58, tn=Math.max(3,Math.ceil(len/GRID)); let p=[fx,fy]; const grA=mix(P.padDk,'#46663c',0.25), grB=mix(P.pad,P.padLt,0.35), grE=P.padEdge; // green sepal cover for(let t=1;t<=tn;t++){ p=[p[0]+nx*GRID, p[1]+ny*GRID]; const u=t/tn; const hw=maxW*Math.pow(Math.sin(Math.PI*clamp(u*0.62+0.12,0,1)),0.60)*(1-0.85*smooth(0.55,1,u)); // egg: round fat base → pointed tip if(hw<GRID*0.3 && u>0.55) continue; for(let w=-hw;w<=hw;w+=GRID){ const aw=Math.abs(w), seam=aw<GRID*0.7, e=aw>hw-GRID*1.1; const pink=clamp(ripe*1.5 - (1-u)*0.95, 0, 1); // colour breaks at the TIP first, spreads down const gcol = e?grE : seam?mix(grA,grE,0.5) : (w<0?grA:grB); const pcol = e?PV.dk : seam?PV.vn : (w<0?PV.pt:PV.lt); C.push(p[0]+px*w, p[1]+py*w, 70, '.', shade(FC(mix(gcol,pcol,pink)),rs),'flowers'); } } if(ripe>0.45){ const tipN=2+Math.round(ripe*3), tbx=fx+nx*len*0.82, tby=fy+ny*len*0.82; // coloured petal tips poking from the top for(let k=0;k<tipN;k++){ const tt=(tipN>1?(k/(tipN-1)-0.5):0)*lerp(0.25,0.9,ripe), ta=a+tt, tlen=R*(0.18+0.5*ripe); let pp=[tbx,tby]; const ptn=Math.max(2,Math.ceil(tlen/GRID)); for(let s=1;s<=ptn;s++){ pp=[pp[0]+Math.cos(ta)*GRID, pp[1]+Math.sin(ta)*GRID]; const pu=s/ptn; C.push(pp[0],pp[1],71, pu>0.7?'.':dirSym(Math.cos(ta),Math.sin(ta)), shade(FC(mix(PV.pt,PV.dk,pu*0.4)),rs),'flowers'); } } } } function drawPod(fx,fy,R,dryF,lean,rs){ // the iconic flat-topped "showerhead" receptacle const topW=R*1.08, topH=R*0.40, bodyH=R*0.66, lx=Math.sin(lean)*bodyH*0.5; const cTop=mix(P.pod,P.podDry,dryF), cSide=mix(P.podRim,P.podDryDk,dryF), cFace=mix(mix(P.pod,P.podDry,dryF),'#000000',0.10); const rows=Math.ceil(bodyH/GRID); for(let r=0;r<=rows;r++){ const u=r/rows, wdt=lerp(topW*0.92, topW*0.34, Math.pow(u,0.8)), yy=fy+topH*0.55+u*bodyH, xx=fx+lx*u; for(let gx=-wdt;gx<=wdt;gx+=GRID){ const e=Math.abs(gx)>wdt-GRID*1.1; C.push(xx+gx,yy,80, e?dirSym(0,1):'\\', shade(FC(e?mix(cSide,'#000000',0.15):cSide),rs),'flowers'); } } for(let gx=-topW;gx<=topW;gx+=GRID)for(let gy=-topH;gy<=topH;gy+=GRID){ if((gx*gx)/(topW*topW)+(gy*gy)/(topH*topH)>1)continue; const e=(gx*gx)/(topW*topW)+(gy*gy)/(topH*topH)>0.8; C.push(fx+gx,fy+gy,83,'-',shade(FC(e?cSide:cFace),rs),'flowers'); } function pit(ox,oy){ C.push(ox,oy,85,'O',shade(FC(mix(P.podRim,'#000000',0.1+0.2*dryF)),rs),'flowers'); C.push(ox,oy,86,'.',shade(FC(P.pit),rs),'flowers'); } const nOut=Math.round(7+dryF*3); for(let k=0;k<nOut;k++){ const a=k/nOut*TAU + rs()*0.2; pit(fx+Math.cos(a)*topW*0.62, fy+Math.sin(a)*topH*0.62); } for(let k=0,nIn=Math.round(3+dryF);k<nIn;k++){ const a=k/nIn*TAU + 0.5; pit(fx+Math.cos(a)*topW*0.30, fy+Math.sin(a)*topH*0.30); } pit(fx,fy); } // small rhizome crown the petioles emerge from (NO long roots) for(let k=0;k<6;k++){ const rs=rng('crown',pseed,k), a=Math.PI + (k/5-0.5)*1.7; C.push(cx+Math.cos(a)*GRID*(1+k%3)*S, by+2+Math.abs(Math.sin(a))*GRID*0.5, 18, dirSym(Math.cos(a),Math.sin(a)), shade(FC(P.stalkDk),rs),'stem'); } // ---- LEAVES: SHORT petioles (heavy pads stay low), wavy pads, 1–3 per plant ---- const nF=clamp(1+density*1.6, 1, 2.4), n=Math.ceil(nF); // 1–2 big distinct leaves (reference shows two) const leafHmax=(55+110*m)*S; // SHORT relative to the heavy leaf (no towering petioles) let tallest=by, padTop=by; for(let i=0;i<n;i++){ const rs=rng('pad',st.seed,pseed,i); const emerge=i===n-1?clamp(nF-(n-1),0.2,1):1, newest=i===n-1 && n>1; const t=n>1? i/(n-1):0.5; const bx=cx+(t-0.5)*120*S+(rs()*2-1)*8*S; // leaves sit apart with a clear gap const hFrac=newest ? 0.64 : (0.88+0.12*rs()); // STAGGER heights → two pads separate, don't form a clover const leafH=leafHmax*hFrac*emerge*collapse + 10; const lean=(t-0.5)*0.55 + (rs()*2-1)*0.05 + windDir*windStr*0.14; // pads ride low on the water → only a slight wind lean const top=rise(bx, by+2, leafH, lean, 22, mix(P.stalk,P.stalkDk,0.25*(1-hFrac)), false, rs); tallest=Math.min(tallest, top[1]); const yellowF=clamp(browny*0.55+sen*0.9+winter*0.7, 0, 1)*(0.6+0.4*(1-i/n)); const ragged=clamp(sen*0.7+winter*0.5-0.15,0,0.6); const z=30+Math.round((1-hFrac)*9); const R=(58+72*m)*emerge*(0.84+0.24*rs())*S*(newest?0.7:1); // BIG pads — lotus leaves are bigger than its flowers drawPad(top[0], top[1], R, z, yellowF, ragged, newest, rs); padTop=Math.min(padTop, top[1]-R*0.80); // visual top of the foliage (for flower-height reference) } // ---- FLOWER stalks: each flower EMERGES as a short budded stem just above the pads, then its // peduncle RISES and the bud OPENS into a bloom (→ pod). Staggered birth maturities mean // flowers appear one at a time and GROW IN — no full-length stem popping out of nowhere. // A well-cared mature root carries up to 3, each at its own phase. ---- // Up to 5 flowers per plant — count = per-seed floriferousness × earned rewards (lots of per-plant // variation). Positions are SPREAD SYMMETRICALLY across the plant's width (never center+left only, // so no side is starved); central blooms emerge first, outer L/R pairs together at higher maturity. const florif = 0.7 + 0.6*rng('florif',st.seed,pseed)(); const nFlow = reproOK ? clamp(Math.round((1 + rewards*3.4)*florif), 1, 5) : 0; const fpos = nFlow>1 ? Array.from({length:nFlow},(_,i)=>(i/(nFlow-1)-0.5)*2) : [0]; // symmetric −1..1 for(let f=0; f<nFlow; f++){ const fside = fpos[f], sgn = fside<0?-1:1; const birthF = 0.50 + Math.abs(fside)*0.20; // centre 0.50 → outer 0.70; L/R pairs emerge together if(!reproOK || m < birthF) continue; const rs=rng('fl',st.seed,pseed,f); // structural draws in FIXED order (frame-stable) const posJit=(rs()*2-1), hr=rs()*(Math.abs(fside)<0.34?1:0.6), leanJit=(rs()*2-1), Rjit=rs(); const fgrow = clamp((m - birthF)/0.18, 0, 1); // 0 = just emerged (short bud) → 1 =
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.62; // THIN Voronoi boundary -> clean fracture line const edge = y < top + GRID*1.5; // top facet catches light const crev = Math.abs(x-cx) < GRID*1.5 && y < top+52; // central crevice the tuft emerges from let col, sym='.'; if(crev){ col=P.crack; } else if(seam){ col=P.crack; // crack runs perpendicular to the seed-pair axis sym = dirSym(-(b1.y-b0.y), b1.x-b0.x); } else { col = b0.t<0.30?P.rockDk : b0.t<0.68?P.rock : P.rockLt; if(edge) col=mix(col,P.rockLt,0.5); } C.push(x,y, edge?12:(seam?11:8), sym, shade(col,rs), 'season'); if(edge && !seam && snowF>0.2 && rs()<snowF*0.75) C.push(x,y-GRID, 14, '.', shade(rs()<0.6?P.snow:P.snowDk,rs), 'season'); // snow caps the lit edges } } // 3. a few long straight MASTER CRACKS across the faces (fractured-granite feel) for(let kc=0; kc<3+Math.floor(rs()*3); kc++){ let px=FRAME+24+rs()*(W-2*FRAME-48), py=topAt(px)+12+rs()*120; let dir=DOWN+(rs()*2-1)*1.15; const len=44+rs()*120, stp=Math.ceil(len/GRID); for(let s=0;s<stp;s++){ px+=Math.cos(dir)*GRID; py+=Math.sin(dir)*GRID; if(py>baseY+36 || px<FRAME+6 || px>W-FRAME-6) break; C.push(px,py, 11, dirSym(Math.cos(dir),Math.sin(dir)), shade(P.crack,rs), 'season'); } } } rockField(); // panicles bloom GREEN; they turn straw only as they SET SEED — late maturity, autumn, or senescence const seedSet=clamp(smooth(0.80,1.0,m)*0.8 + sen*0.9 + (season==='Autumn'?0.45:0), 0, 1); const bgc=BACKDROPS[backdropFor(st.seed)]; // backdrop colour, for fading distant tufts back const fmix=(c,f)=> f>0 ? mix(c,bgc,f) : c; // ---------- one TUFT (cushion of arching blades + airy panicles); sc=size, fade=distance, tag=identity ---------- function tuft(ox, oy, sc, fade, tag){ const zo = tag==='hero' ? 0 : -13; // companions sit BEHIND the hero const nF=clamp(16+density*74,16,90)*(0.55+0.45*sc), n=Math.ceil(nF); // blade COUNT (cushion lushness) ← entries (density); floor 16 = the survivor always tufts for(let k=0;k<n;k++){ const rs=rng('blade',st.seed,tag,k); const emerge=k===n-1?clamp(nF-(n-1),0.12,1):1; const a0=(rs()*2-1); // stable fan position const ang=UP + a0*1.18; const len=(84+205*m)*sc*(0.5+0.6*rs())*emerge*(1-0.22*Math.abs(a0)); const arch=0.5+0.85*rs(), outer=Math.abs(a0); // outer = older blade const bladeBrown=clamp(smooth(0.62,1.0,outer)*0.9 + browny*0.6, 0, 1); // ONLY outer/older blades straw off let dir=ang, pos=[ox+a0*11*sc, oy]; const steps=Math.max(2,Math.ceil(len/GRID)); for(let i=1;i<=steps;i++){ const u=i/steps; dir += angTo(dir,DOWN)*arch*0.05*(0.3+0.7*u); // arch outward then over the rocks pos=[pos[0]+Math.cos(dir)*GRID, pos[1]+Math.sin(dir)*GRID]; const tipBrown=clamp(bladeBrown*(0.6+0.4*u) + browny*u*0.35, 0, 1); const green=mix(P.blade, rs()<0.5?P.bladeDk:P.bladeLt, rs()); const col=fmix(mix(green, mix(P.straw,P.strawDk,rs()), tipBrown), fade); C.push(pos[0],pos[1], 40+zo, dirSym(Math.cos(dir),Math.sin(dir)), shade(col,rs), 'leaves'); } } function spikelet(x,y,dir,rs){ const col=fmix(mix(P.spikeGreen, P.spikeStraw, clamp(seedSet*(0.55+0.45*rs()),0,1)), fade); const awnCol=fmix(mix(P.spikeGreen, P.awn, clamp(0.35+seedSet,0,1)), fade); let sp=[x,y]; const nn=3+Math.floor(rs()*2); // a small oval lemma cluster for(let i=0;i<nn;i++){ sp=[sp[0]+Math.cos(dir)*GRID*0.9, sp[1]+Math.sin(dir)*GRID*0.9]; C.push(sp[0],sp[1], 92+zo, i%2?'O':'.', shade(col,rs), 'flowers'); } let ap=[sp[0],sp[1]]; // a fine awn extending past the tip for(let i=0;i<3+Math.floor(rs()*3);i++){ ap=[ap[0]+Math.cos(dir)*GRID, ap[1]+Math.sin(dir)*GRID]; C.push(ap[0],ap[1], 90+zo, dirSym(Math.cos(dir),Math.sin(dir)), shade(awnCol,rs), 'flowers'); } } if(flowering>0.08){ const nPan=Math.max(1, Math.round((1+m*4)*flowering*(0.45+0.55*rewards)*(tag==='hero'?1:0.55))); // seed-heads ← rewards (floor keeps the survivor seeding) for(let p=0;p<nPan;p++){ const rs=rng('pan',st.seed,tag,p); const grown=clamp((m-(0.4+p*0.09))/0.12,0,1); // panicles emerge progressively if(grown<=0) continue; const sx=ox+(rs()*2-1)*(20+m*44)*sc; const sh=(190+260*m)*sc*grown*(0.7+0.4*rs()); let pos=[sx,oy-4], dir=UP+(rs()*2-1)*0.2; const steps=Math.ceil(sh/GRID); for(let s=1;s<=steps;s++){ const u=s/steps; dir += Math.sin(u*3+rs()*6)*0.02 + (rs()*2-1)*0.004; // wiry wobble pos=[pos[0]+Math.cos(dir)*GRID, pos[1]+Math.sin(dir)*GRID]; C.push(pos[0],pos[1], 70+zo, dirSym(Math.cos(dir),Math.sin(dir)), shade(fmix(P.stalk,fade),rs), 'stem'); if(u>0.5 && s%3===0){ // open panicle branches in the upper half const nb=1+Math.floor(rs()*2); for(let b=0;b<nb;b++){ const side=rs()<0.5?1:-1; const bl=(16+30*m)*sc*(1-u)*(0.6+0.6*rs()); let bp=[pos[0],pos[1]], bd=dir+side*(0.6+0.5*rs()); const bst=Math.max(2,Math.ceil(bl/GRID)); for(let bs=1;bs<=bst;bs++){ bd += angTo(bd, UP+side*0.3)*0.1; // branch arcs up bp=[bp[0]+Math.cos(bd)*GRID, bp[1]+Math.sin(bd)*GRID]; C.push(bp[0],bp[1], 71+zo, dirSym(Math.cos(bd),Math.sin(bd)), shade(fmix(P.stalk,fade),rs), 'stem'); } spikelet(bp[0],bp[1],bd,rs); } } } spikelet(pos[0],pos[1],dir,rs); // a terminal spikelet } } } // ---------- a small COLONY: companions scatter in the crevices behind the hero (varies by seed; not always solo) ---------- const cr=rng('colony',st.seed); const nComp=[0,1,1,2,2,3][Math.floor(cr()*6)]; for(let i=0;i<nComp;i++){ const side=cr()<0.5?-1:1, depth=cr(); // depth 0 = near ... 1 = far const bx=clamp(cx + side*(78+cr()*210), FRAME+72, W-FRAME-72); const by=(baseY-150) + cr()*46; // rooted lower on the rock surface than the hero tuft(bx, by, lerp(0.62,0.38,depth), lerp(0.40,0.64,depth), 'bg'+i); } tuft(cx, cy, 1, 0, 'hero'); // the hero tuft, full size and in front // ---------- SNOW: scattered flecks (more in winter), settled + drifting ---------- const sr=rng('snow',st.seed); const nSnow=Math.round(70*snowF + 14); for(let i=0;i<nSnow;i++){ const x=FRAME+8 + sr()*(W-2*FRAME-16); const y=lerp(baseY+10, 140, sr()*sr()); // biased low (settled on the ground/rocks) C.push(x,y, 96, '.', shade(sr()<0.5?P.snow:P.snowDk, sr), 'flowers'); } }, traits(st,env){ const season=seasonOf(st.now); const snow = season==='Winter'?'Snowfall' : (season==='Summer'?'Clear':'Dusting'); return [['Habitat','Rock crevice'], ['Frost', st.diedAt>0?'Frozen':snow], ['Hardiness','Extreme']]; } }); })(); ; /* species/lavender.js */ /* lavender — species index 3. GENERATED from prototypes/lavender.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(3, spec); /* Lavender — Lavandula. A radiating fountain of fine woody stems (a rounded clump), narrow opposite grey-green leaves, and vertical violet FLOWER-SPIKES of whorled florets held above the foliage. Hardy Mediterranean shrub: slow to wilt, summer bloom. Stems emerge progressively — old stems splay outward, the newest keep rising from the centre. Death = grey dried wands. Grows in a patch (Setting: Solo / Pair / Cluster / Field). Ported to the shared core. */ const SP = window.SEEDLING_SPECIES.find(x=>x.key==='lavender'); mount(Object.assign(window.SEEDLING_toSpec(SP), { title:'Lavender', sub:'lavandula v2', senFrom:0.80, schemeExempt:[], // spikes follow the per-seed scheme (woven, like the rest) LW:{season:1.0, stem:1.05, leaves:1.1, flowers:1.9}, LS:{season:1.0, stem:1.0, leaves:0.9, flowers:0.8}, note:'Hardy Mediterranean clump · radiating woody stems · narrow opposite leaves · violet flower-spikes above the foliage · summer bloom · death = grey dried wands.', PAL:{ soil:'#6f5128', grey:'#9a9180', flower:'#7A4FC0', flowerLt:'#AE8FE2', flowerDk:'#553399', bud:'#7E8C56', leafDark:'#566E50', leaf:'#88A07E', leafLt:'#B4C3A4', stem:'#6E7A54' }, stageName(st,env){ if(st.diedAt>0)return'Sealed'; const m=env.m; if(env.ageF>0.86)return'Spent'; if(m<0.12)return'Sprout'; if(m<0.42)return'Young bush'; if(m<0.6)return'Bush'; return (env.season==='Summer'||env.season==='Spring')?'Flowering':'Bush'; }, draw(st, C, env){ const {m,v,sen,season,browny,density,rewards,baseX,baseY}=env, P=this.PAL; const bgC = BACKDROPS[backdropFor(st.seed)]; const seasonOpen = season==='Summer'?1 : season==='Spring'?0.8 : season==='Autumn'?0.55 : 0.4; // ===================== one lavender bush (repeated to form a patch) ===================== function paintBush(I){ const {cx, by, S, fade, m, v, sen, browny, density, rewards, zb, pseed} = I; const FC = c => fade>0 ? mix(c, bgC, fade) : c; const flowering = smooth(0.40,0.62,m) * rewards * (1-sen*0.8); const cFlower=FC(mix(P.flower,P.grey,browny)), cFlowerLt=FC(mix(P.flowerLt,P.grey,browny)), cFlowerDk=FC(mix(P.flowerDk,P.grey,browny)); const cBud=FC(mix(P.bud,P.grey,browny*0.9)); const leafD=FC(mix(P.leafDark,P.grey,browny)), leafM=FC(mix(P.leaf,P.grey,browny)), leafL=FC(mix(P.leafLt,P.grey,browny*0.9)); const stemC=FC(mix(P.stem,P.grey,Math.max((1-v)*0.5,sen))); const greenOf=rs=>shade(mix(leafM, rs()<0.5?leafD:leafL, rs()), rs); // per-seed personality so two bushes differ structurally (not just noise) const hero=pseed==='hero'; const dd= hero?rng('dna',st.seed):rng('dna',st.seed,pseed); const D={ spread:0.50+0.55*dd(), lean:(dd()*2-1)*0.16, fanBase:16+30*dd(), vigor:0.85+0.30*dd(), bushiness:dd(), openBias:0.85+0.30*dd(), spikeF:0.80+0.40*dd() }; const MAXS=30, growWin=0.12, splayOpen=smooth(0.0,0.34,m); const lenBase=(180+440*m)*(0.72+0.28*v)*D.vigor*S; for(let k=0;k<MAXS;k++){ const rs= hero?rng('stem',st.seed,k):rng('stem',st.seed,pseed,k); const birthM=(k/MAXS)*0.70; const grown=clamp((density-birthM)/growWin, k===0?0.2:0, 1); // stem COUNT ← density; first always present if(grown<=0) continue; const outerness=lerp(1.0,0.22,k/MAXS); // OLD outer · NEW central → fresh growth from the middle const a0=(rs()*2-1)*outerness*splayOpen; const ang0=UP + a0*D.spread + D.lean; const baseX0=cx + a0*D.fanBase*S; const maxLen=lenBase*(1-0.30*Math.abs(a0))*(0.8+0.4*rs()); const len=maxLen*grown; const zf=rs()<0.5, stemZ=(zf?42:30)+zb, leafZ=(zf?44:28)+zb, flZ=(zf?94:84)+zb; const stemFlower = flowering>0.05 && grown>0.4 && rs() < (0.3+0.66*flowering); const leafTop = stemFlower?0.58:0.86; let dir=ang0, pos=[baseX0, by-3]; const steps=Math.max(3,Math.ceil(len/GRID)); let tip=[pos[0],pos[1]], tdir=dir;
, y, ang, len, wide, rs){ const steps=Math.max(2,Math.ceil(len/GRID)); let dir=ang, pos=[x,y]; for(let s=1;s<=steps;s++){ const u=s/steps; dir += angTo(dir,DOWN)*(0.05+0.05*yellowF)*u; const cd=[Math.cos(dir),Math.sin(dir)]; pos=[pos[0]+cd[0]*GRID, pos[1]+cd[1]*GRID]; const px=-cd[1], py=cd[0], hw=Math.pow(Math.sin(Math.PI*Math.min(u,0.999)),0.6)*wide, sym=dirSym(cd[0],cd[1]); for(let w=-hw;w<=hw;w+=GRID){ const edge=Math.abs(w)>hw-GRID*0.9, col= edge?shade(leafD,rs) : Math.abs(w)<GRID*0.6?shade(leafL,rs):shade(leafC,rs); C.push(pos[0]+px*w, pos[1]+py*w, 60, sym, col, 'leaves'); } } } function leafSpray(x, y, side, size, rs){ const n=1+Math.floor(rs()*(1+D.leafiness*2)), bx0=side>0?-0.55:Math.PI+0.55; let bx=x, by2=y, bdir=bx0; const bsteps=2+Math.floor(rs()*2); for(let b=1;b<=bsteps;b++){ bdir+=angTo(bdir,UP)*0.12; bx+=Math.cos(bdir)*GRID; by2+=Math.sin(bdir)*GRID; C.push(bx,by2,58,dirSym(Math.cos(bdir),Math.sin(bdir)),shade(culmDk,rs),'leaves'); } for(let k=0;k<n;k++){ const ang=bx0 + (k-(n-1)/2)*0.4 + (rs()*2-1)*0.12 - 0.1; lanceLeaf(bx, by2, ang, size*(0.8+0.5*rs()), (0.85+0.6*D.leafiness)*(0.8+0.3*rs())*Math.sqrt(S), rs); } } function flowerPlume(x, y, side, rs){ let dir=(side>0?0.15:Math.PI-0.15), pos=[x,y]; const steps=7+Math.floor(rs()*7); for(let s=1;s<=steps;s++){ const u=s/steps; dir += angTo(dir,DOWN)*(0.16+0.12*u); pos=[pos[0]+Math.cos(dir)*GRID, pos[1]+Math.sin(dir)*GRID]; C.push(pos[0],pos[1], 90, dirSym(Math.cos(dir),Math.sin(dir)), shade(FC(P.stamenDk),rs), 'flowers'); if(s%2===0){ if(flowerPhase<0.5){ C.push(pos[0],pos[1]+GRID*0.9, 91,'|', shade(FC(P.stamen),rs), 'flowers'); C.push(pos[0],pos[1]+GRID*1.8, 92,'.', shade(FC(P.stamen),rs), 'flowers'); } else { const cc=FC(mix(P.seed,P.seedDk,flowerPhase*0.6)); C.push(pos[0],pos[1]+GRID*0.8, 91,'O', shade(cc,rs), 'flowers'); C.push(pos[0],pos[1]+GRID*1.5, 91,'.', shade(cc,rs), 'flowers'); } } } } function drawCulm(sx, lean, hScale, emerge, ci){ const rs=rng('culm',seed,ci), grow=smooth(0,1,emerge), Hc=(245+520*m)*D.height*hScale*grow*(0.9+0.1*v)*S; if(Hc<GRID*4) return; const steps=Math.ceil(Hc/GRID), cm=clamp(m*(0.4+0.6*grow),0,1), sheathTop=clamp(1.12 - cm*2.7, 0, 1.0), archDir=(Math.abs(lean)>0.01? Math.sign(lean) : (rs()<0.5?1:-1)); const swayPh=rs()*6.28, swayAmp=D.sway*(0.7+0.6*rs()), baseThick=(1.5+D.thickness*3.4)*grow*(0.7+0.3*v)*(0.8+0.2*hScale)*Math.sqrt(S), internode=D.nodeLen*(0.85+0.3*rs())*S; let dir=UP+lean+windLean*0.5, pos=[sx,cy], run=internode*0.55; const upperNodes=[]; for(let s=1;s<=steps;s++){ const u=s/steps, exposure=clamp((cy-pos[1])/680,0,1); dir += Math.sin(u*Math.PI*D.swayFreq + swayPh)*swayAmp*0.011 + windLean*0.05*exposure; if(u>0.5) dir += archDir*D.arch*0.018*(u-0.5)*(0.5+0.6*cm); const cd=[Math.cos(dir),Math.sin(dir)]; pos=[pos[0]+cd[0]*GRID, pos[1]+cd[1]*GRID]; const px=-cd[1], py=cd[0], sym=dirSym(cd[0],cd[1]); const sheathed = u<sheathTop ? clamp((sheathTop-u)/0.14,0,1) : 0, hwCulm=Math.max(0.6, baseThick*(1-0.4*u)), hwShoot=baseThick*2.0*clamp((sheathTop-u)/Math.max(sheathTop,0.01),0.1,1), hw=sheathed>0 ? lerp(hwCulm,hwShoot,sheathed) : hwCulm; run+=GRID; let isNode=false; if(run>=internode){ run=0; isNode=true; } for(let w=-hw;w<=hw;w+=GRID){ const a=Math.abs(w)/Math.max(hw,1), edge=Math.abs(w)>hw-GRID*1.0; if(sheathed>0.4){ const col= edge?shade(FC(P.sheathDk),rs):(a<0.4?shade(FC(mix(P.sheath,'#ffffff',0.08)),rs):shade(FC(P.sheath),rs)); C.push(pos[0]+px*w, pos[1]+py*w, 50, w<0?'/':'\\', mix(col, FC(P.dead), sen*0.5), 'stem'); } else { const col= edge?shade(culmDk,rs):(a<0.34?shade(mix(culmC,'#ffffff',0.07),rs):shade(culmC,rs)); C.push(pos[0]+px*w, pos[1]+py*w, 50, sym, col, 'stem'); } } if(isNode){ const nhw=hw+GRID*1.1; for(let w=-nhw;w<=nhw;w+=GRID) C.push(pos[0]+px*w, pos[1]+py*w, 54, sheathed>0.4?(w<0?'/':'\\'):'-', shade(sheathed>0.4?FC(P.sheathDk):nodeC,rs), 'stem'); if(sheathed<0.3){ const rx=pos[0]-cd[0]*GRID, ry=pos[1]-cd[1]*GRID; for(let w=-hw;w<=hw;w+=GRID) C.push(rx+px*w, ry+py*w, 53, '-', shade(mix(culmC,'#ffffff',0.2),rs), 'stem'); } if(u>0.42 && sheathed<0.3) upperNodes.push([pos[0],pos[1],u]); } } for(const [nx,ny,u] of upperNodes){ const nr=rng('node',seed,ci*131+Math.round(u*100)), leafSize=(32+54*m)*(0.72+0.4*hScale)*S; if(flowering && u>0.55){ flowerPlume(nx,ny, nr()<0.5?1:-1, nr); if(nr()<0.6) flowerPlume(nx,ny, nr()<0.5?-1:1, nr); if(nr()<0.35) leafSpray(nx,ny, nr()<0.5?1:-1, leafSize*0.8, nr); } else { leafSpray(nx,ny, 1, leafSize, nr); if(nr()<0.7) leafSpray(nx,ny,-1, leafSize*0.92, nr); } } } const nCulmF=clamp(1 + density*4.2*D.clumpiness + (rng('nc',seed)()*2-1)*0.5, 1, 5.4), nCulm=Math.ceil(nCulmF); for(let i=0;i<nCulm;i++){ const emerge=i===nCulm-1?clamp(nCulmF-(nCulm-1),0.05,1):1, pairIdx=Math.ceil(i/2), side=(i%2?1:-1); const lean=(i===0?0:side*pairIdx*0.085) + D.lean, hScale=(i===0?1:1-pairIdx*0.12)*(0.9+0.2*rng('hs',seed,i)()), sx=rootX + (i===0?0:side*pairIdx*22*S) + (rng('sx',seed,i)()*2-1)*7; drawCulm(sx, lean, hScale, emerge, i); } } // ===================== SETTING: Solo / Pair / Grove / Thicket / rare Forest ===================== const roll=rng('setting',st.seed)(); const nC = roll<0.18?0 : roll<0.46?1 : roll<0.74?2 : roll<0.91?3+Math.floor(rng('fn',st.seed)()*2) : 7+Math.floor(rng('fn',st.seed)()*5); const forest=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(forest){ 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.62,0.34,depth)*(0.9+0.2*rc()); raise=(depth*150+rc()*26); fade=lerp(0.44,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.46,f)*(0.9+0.2*rc()); off=side*(96+i*54+rc()*40); raise=(26+i*18+rc()*26); fade=lerp(0.40,0.60,f); } comps.push({seed:cseed, artSeed:cseed, cx:baseX+off, by:baseY-raise, S:scale, fade, m, density:clamp(density+(rc()*2-1)*(forest?0.16:0.12),0.04,1), v, sen, browny, rewards, depth}); } comps.sort((a,b)=>b.depth-a.depth).forEach(paintBamboo); paintBamboo({seed:st.seed, artSeed:st.artSeed, cx:baseX, by:baseY, S:1.16, fade:0, m, density, v, sen, browny, rewards}); }, traits(st,env){ const roll=rng('setting',st.seed)(); const setting = roll<0.18?'Solo' : roll<0.46?'Pair' : roll<0.74?'Grove' : roll<0.91?'Thicket':'Forest'; const dd=rng('dna',st.seed); for(let i=0;i<9;i++)dd(); const flowering=dd()<0.13; return [ ['Setting', setting], ['Culms', Math.max(1,Math.ceil(clamp(1+env.density*4.2,1,5)))], ['Flowering', flowering?'Gregarious':'—'], ]; } })); })(); ; /* species/bird.js */ /* bird — species index 7. GENERATED from prototypes/bird-of-paradise.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(7, spec); /* Bird of Paradise — Strelitzia reginae. A clumping distichous FAN of large oblong paddle leaves on long petioles, and the iconic CRANE FLOWER: a horizontal boat-shaped spathe (the "beak"), a crest of pointed ORANGE sepals spraying up, and a BLUE arrow petal (the "tongue") jutting forward. Cranes ride tall ARCHING stalks that emerge across the clump (not all from the middle) at varied heights, each facing outward. Per-seed personality (fan spread, lean, bloom side, stalk curl). Grows in clumps — faded companions stand behind. */ const SP = window.SEEDLING_SPECIES.find(x=>x.key==='bird'); mount(Object.assign(window.SEEDLING_toSpec(SP), { title:'Bird of Paradise', sub:'strelitzia v2', senFrom:0.82, note:'Distichous paddle-leaf fan · crane-flowers (orange crest + blue tongue from a beak spathe) on tall arching stalks across the clump · bird-pollinated · grows in clumps.', PAL:{ soil:'#5a4a2e', leaf:'#2E5A3C', leafDk:'#1d3d28', leafLt:'#4E7A52', spathe:'#4a6b3a', spatheDk:'#6b3b2a', orange:'#F28C28', orangeDk:'#C4641A', blue:'#2B4C9B', blueLt:'#3f63b8', stalk:'#3a5a3c' }, stageName(st,env){ if(st.diedAt>0)return'Husk'; const m=env.m; if(m<0.15)return'Sprout'; if(m<0.5)return'Fan'; if(m<0.66)return'Budding'; return'Crane'; }, draw(st, C, env){ const {m,v,sen,season,browny,density,rewards,reproOK,dormancy,dead,baseX,baseY}=env, P=this.PAL; const bgC = BACKDROPS[backdropFor(st.seed)]; const winter=dormancy||0; function birdDNA(seed){ const dd=rng('dna',seed), g=()=>(dd()+dd()+dd()-1.5)/1.5; return { startSide:dd()<0.5?1:-1, // the fan leads LEFT or RIGHT (was always left) fanSpread:0.06+0.34*dd(), // tight upright clump (filled centre) → wide splayed fan fanAsym:g()*0.55, // lopsided: one side opens wider than the other fanLean:g()*0.20, leafiness:0.85+0.45*dd(), nFlowers:1+Math.floor(dd()*3), bloomSide:dd()<0.5?1:-1, stalkCurl:0.5+0.7*dd(), stalkLean:0.10+0.22*dd(), heightVar:0.3+0.5*dd() }; } // ===================== one clump (hero or faded background companion) ===================== function paintBird(I){ const {seed, artSeed, cx, cy, S, dim, zb, m, v, sen, browny, rewards, density, dead} = I; const FC = c => dim>0 ? mix(c, bgC, dim) : c; const PUSH = (x,y,z,sym,col,layer)=>C.push(x,y,z+zb,sym,col,layer); const D = birdDNA(seed); const dry=Math.max(sen,winter*0.5); // ---- distichous FAN of oblong paddle leaves; each leaf GROWS IN (no pop) ---- const colMid=mix(P.leafLt,'#e7efcb',0.5); const nF=clamp(3.4 + density*4*D.leafiness, 3, 7); const tornF=clamp(browny*0.7 + sen*0.5, 0, 0.8); for(let i=0;i<7;i++){ const rs=rng('leaf',seed,i); const em=clamp(nF-i, 0, 1); if(em<=0.02) continue; // newest leaf eases in from 0 const side=(i%2?1:-1)*D.startSide, rank=Math.floor(i/2); // fan leads either side per seed const sideSpread=D.fanSpread*(1 + D.fanAsym*side); // asymmetric → not a clean symmetric V const ang=UP + side*(sideSpread+0.13*rank) + D.fanLean + (rs()*2-1)*0.13; // stronger per-leaf jitter const dx=Math.cos(ang), dy=Math.sin(ang), px=-dy, py=dx; const plen=(150+330*m)*em*(0.82+0.30*rs())*(1-0.16*winter)*S; // more leaf-length variation const z=44+i; const tone=c=>shade(FC(mix(mix(c,'#6b5a2e',winter*0.4),'#33301f',browny*0.7)),rs); const steps=Math.max(3,Math.ceil(plen/GRID)); let pos=[cx + side*(7+11*rank)*S + (rs()*2-1)*5*S, cy]; for(let s=1;s<=steps;s++){ const u=s/steps; pos=[pos[0]+dx*GRID, pos[1]+dy*GRID]; if(u<0.40){ for(let w=-GRID*0.8;w<=GRID*0.8;w+=GRID) PUSH(pos[0]+px*w,pos[1]+py*w,z,dirSym(dx,dy),tone(Math.abs(w)<GRID?colMid:P.leaf),'leaves'); } // pale petiole else { const bu=(u-0.40)/0.60, hw=GRID*(0.5+6.8*Math.pow(Math.sin(Math.PI*bu),0.62))*S; // broad oblong, rounded point for(let w=-hw;w<=hw;w+=GRID){ const aw=Math.abs(w), edge=aw>hw-GRID*1.4, midrib=aw<GRID*1.0; 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,
), D=aloeDNA(seed), wilt=clamp(Math.max(1-v,sen),0,1); const leafD=FC(mix(P.leafDk,P.dry,browny*0.9)), leafM=FC(mix(P.leaf,P.dry,browny*0.9)), leafL=FC(mix(P.leafLt,P.dry,browny*0.85)); const toothC=FC(mix(P.tooth,P.dry,browny*0.7)), stalkC=FC(mix(P.stalk,P.dry,Math.max((1-v)*0.5,sen))); const floC=FC(mix(P.flower, P.flowerDk, clamp(-D.flowerHue,0,1)*0.5)); const windLean=D.windDir*D.windStr*0.26, cx=cx0 - D.windDir*D.windStr*26*S, cy=by-14*S; const leanTilt=D.lean*0.20, axisUP=UP+leanTilt; // central growth axis — tilts L/R per seed (±~11°) function leafHalfW(u, fat){ const body=Math.pow(1-u,0.56), attach=u<0.12?lerp(0.78,1,u/0.12):1; return Math.max(0.5, fat*body*attach); } function drawLeaf(ang, len, fat, recurve, rl, z, teeth, flecky){ const steps=Math.max(3,Math.ceil(len/GRID)); let dir=ang, pos=[cx,cy]; for(let s=1;s<=steps;s++){ const u=s/steps; dir += angTo(dir,DOWN)*recurve*0.022*u*u + windLean*0.018*u + leanTilt*D.leanCurve*0.020*u; const cd=[Math.cos(dir),Math.sin(dir)]; pos=[pos[0]+cd[0]*GRID, pos[1]+cd[1]*GRID]; const px=-cd[1], py=cd[0], hw=leafHalfW(u,fat), sym=dirSym(cd[0],cd[1]); for(let w=-hw;w<=hw;w+=GRID){ const a=Math.abs(w)/Math.max(hw,1), edge=Math.abs(w)>hw-GRID*1.05; const col = edge ? shade(mix(leafD,'#243520',0.5),rl) : a<0.22 ? shade(leafL,rl) : shade(mix(leafM,leafD,a*0.85),rl); C.push(pos[0]+px*w, pos[1]+py*w, z, sym, col, 'leaves'); } if(teeth && s%4===0 && u<0.92){ C.push(pos[0]+px*(hw+GRID*0.6), pos[1]+py*(hw+GRID*0.6), z+4, '.', shade(toothC,rl), 'leaves'); C.push(pos[0]-px*(hw+GRID*0.6), pos[1]-py*(hw+GRID*0.6), z+4, '.', shade(toothC,rl), 'leaves'); } if(flecky && u>0.12 && rl()<0.10){ const fw=(rl()*2-1)*hw*0.7; C.push(pos[0]+px*fw, pos[1]+py*fw, z+3, '.', shade(FC(P.fleck),rl), 'leaves'); } } } const nLeafF=clamp(2 + density*11*D.leafN + (rng('nl',seed)()*2-1)*0.7, 2, 14), nLeaf=Math.ceil(nLeafF), nPair=Math.max(1,Math.ceil(nLeaf/2)); const openF=smooth(0.12,0.7,m), spreadMax=lerp(0.28,0.62,openF); for(let i=0;i<nLeaf;i++){ const rl=rng('leaf',seed,i), emerge=i===nLeaf-1?clamp(nLeafF-(nLeaf-1),0.06,1):1, grow=smooth(0,1,emerge); const pair=Math.floor(i/2), side=(i%2?1:-1), tp=nPair>1? pair/(nPair-1) : 0.3, mag=lerp(spreadMax, 0.12, tp); const wob=Math.sin(i*GA*D.chir + D.spiralPh)*0.13*openF*D.twist; // per-seed handedness + phase (was uniform) const full=side===D.fullSide, lop=full?1+0.22*D.fullAmt:1-0.16*D.fullAmt, lopLen=full?1+0.12*D.fullAmt:1-0.08*D.fullAmt; const ang=axisUP + side*mag*lop + wob + windLean*0.4, recurve=(1-tp)*0.7*D.recurve + wilt*1.0, vase=lerp(1.0, lerp(1.0,0.74,tp), smooth(0.15,0.6,m)); const len=(92+208*m)*D.stature*vase*(0.82+0.26*rl())*lopLen*grow*S, widthM=lerp(0.32,1.0,smooth(0.04,0.55,m)); const fat=(4.4 + 10.5*D.fat)*(0.62+0.38*v)*grow*lerp(1.12,0.76,tp)*widthM*Math.sqrt(S), z=40 + i; drawLeaf(ang, len, fat, recurve, rl, z, D.teeth>0.4, D.flecks>0.35); } const flowerVigor=smooth(0.52,0.84,m)*rewards, bloomOn=flowerVigor*seasonBloom*(1-0.5*sen); function drawCone(axis, racStart, steps, span, spikeColor, dry, wide, seedk){ const maxCone=GRID*(2.0+1.2*wide)*Math.sqrt(S), front=spikeColor*1.3; for(let k=racStart;k<steps;k++){ const p=(k-racStart)/span, coneW=maxCone*Math.pow(clamp(1-p,0,1),0.72)+GRID*0.35, ax=axis[k][0], ay=axis[k][1], nf=Math.max(1,Math.round(coneW/GRID)); for(let j=-nf;j<=nf;j++){ if(j===0 && p<0.9) continue; const rr=rng('flo',seed, seedk + k*29 + (j+nf)*7), off=j*GRID*0.92 + (rr()*2-1)*GRID*0.18; if(Math.abs(off)>coneW+GRID*0.4) continue; const aw=Math.abs(off)/Math.max(coneW,1), fx=ax+off, fy=ay + aw*GRID*1.1 + (rr()*2-1)*GRID*0.2; if(rr()<0.4) C.push(ax+off*0.45, ay+aw*GRID*0.5, 88, off>0?'\\':'/', shade(FC(P.budDk),rr), 'flowers'); const green = p>0.86 || p>=front; let col; if(green) col=shade(FC(mix(P.bud,P.budDk,0.25)),rr); else { const u=rr(); col=shade(FC(mix(u<0.22?P.flowerDk:u<0.82?P.flower:P.flowerTip, P.capsuleDry, dry*0.7)),rr); if(u>=0.22&&u<0.82) col=shade(mix(floC,FC(P.capsuleDry),dry*0.7),rr); } C.push(fx, fy, green?90:92, rr()<0.42?'.':(rr()<0.72?'+':'x'), col, 'flowers'); } } } function drawRaceme(sx, lean, hScale, spikeColor, grow, seedk, rs){ const sh=(185+330*m)*D.stature*hScale*smooth(0.5,0.96,m)*grow*S; if(sh<GRID*5) return; const steps=Math.ceil(sh/GRID), ph=rs()*6.28, aim=axisUP+windLean*0.7+lean; let dir=axisUP+windLean*0.4+lean*0.7, pos=[sx,cy-6*S], axis=[]; for(let s=1;s<=steps;s++){ const u=s/steps; dir += angTo(dir,aim)*0.06 + Math.sin(u*4+ph)*0.006 + sen*0.04*u; pos=[pos[0]+Math.cos(dir)*GRID, pos[1]+Math.sin(dir)*GRID]; C.push(pos[0],pos[1], 70, '|', shade(stalkC,rs), 'stem'); axis.push([pos[0],pos[1]]); } const infFrac=lerp(0.20, 0.40, smooth(0.5,0.96,m)), racStart=Math.floor(steps*(1-infFrac)), span=Math.max(1, steps-1-racStart); drawCone(axis, racStart, steps, span, spikeColor, Math.max(sen,browny*0.6), hScale, seedk); } if(bloomOn>0.12){ const nSpikeF=clamp(smooth(0.5,0.82,m)*(1.2+3.8*D.pups), 0, 5), nSpike=Math.ceil(nSpikeF); for(let i=0;i<nSpike;i++){ const emerge=i===nSpike-1?clamp(nSpikeF-(nSpike-1),0.04,1):1, grow=smooth(0,1,emerge), pairIdx=Math.ceil(i/2), side=(i%2?1:-1)*D.chir; const lean=(i===0?0:side*pairIdx*0.18), hScale=(i===0?1:1-pairIdx*0.18)*(0.92+0.16*rng('sh',seed,i)()), sx=cx + (i===0?0:side*pairIdx*15*S) + (ra()*2-1)*4; const spikeColor=smooth(0.58,0.86,m)*grow; drawRaceme(sx, lean, hScale, spikeColor, grow, 1000+i*263, rng('rac',seed,i)); } } const pupN=Math.floor(smooth(0.5,0.96,m)*D.pups*3.2); for(let p=0;p<pupN;p++){ const rp=rng('pup',seed,p), side=p%2?1:-1, sx=cx + side*(42+p*26+rp()*26)*D.stature*S, sy=cy+4*S, nl=3+Math.floor(rp()*3); for(let k=0;k<nl;k++){ const ps=(k%2?1:-1), pm=lerp(0.2,1.0,Math.floor(k/2)/Math.max(1,Math.ceil(nl/2)-1)), pang=UP + leanTilt*0.4 + ps*lerp(0.2,0.95,pm) + (rp()*2-1)*0.1; let dir=pang, pos=[sx,sy]; const plen=(18+24*rp())*S, pst=Math.ceil(plen/GRID); for(let s=1;s<=pst;s++){ const u=s/pst; dir+=angTo(dir,DOWN)*0.02*u; pos=[pos[0]+Math.cos(dir)*GRID, pos[1]+Math.sin(dir)*GRID]; const px=-Math.sin(dir),py=Math.cos(dir), hw=Math.max(0.5,(1.6+2.4*D.fat)*Math.pow(1-u,0.7)*Math.sqrt(S)); for(let w=-hw;w<=hw;w+=GRID){ const edge=Math.abs(w)>hw-GRID; C.push(pos[0]+px*w,pos[1]+py*w, 38, dirSym(Math.cos(dir),Math.sin(dir)), edge?shade(toothC,rp):shade(leafM,rp), 'leaves'); } } } } } // ===================== SETTING: Solo / Pair / Clump / Colony (hero scaled up) ===================== const roll=rng('setting',st.seed)(); const nC = roll<0.18?0 : roll<0.48?1 : roll<0.80?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); const scale=lerp(0.52,0.34,f)*(0.9+0.2*rc()), off=side*(120+i*54+rc()*40), raise=(20+i*16+rc()*22); comps.push({seed:cseed, artSeed:cseed, cx0:baseX+off, by:baseY-raise, S:scale, fade:lerp(0.38,0.58,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(paintAloe); paintAloe({seed:st.seed, artSeed:st.artSeed, cx0:baseX, by:baseY, S:1.2, fade:0, m, density, v, sen, browny, rewards}); }, traits(st,env){ const roll=rng('setting',st.seed)(); const setting = roll<0.18?'Solo' : roll<0.48?'Pair' : roll<0.80?'Clump' : 'Colony'; return [ ['Setting', setting], ['Leaves', Math.max(2,Math.ceil(clamp(2+env.density*15,2,19)))], ['Spikes', st.diedAt>0?'—':(env.m>=0.6 && env.rewards>0.3?'Coral':'—')], ]; } })); })(); ; /* species/amla.js */ /* amla — species index 12. GENERATED from prototypes/amla.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(12, spec); /* Amla (Indian Gooseberry) — Phyllanthus emblica. A DECURRENT/weeping tree: a recursive staggered- lateral framework with a short clear trunk that forks low into a broad LOW crown weeping to a near- ground skirt. Clothed in airy feathery FRONDS (thin arching rachis + sparse leaflets — fern-like, see-through). Reward: pale round translucent FRUIT hung along the outer wood (autumn/winter), which PERSISTS through senescence as the leaflets drop → a skeletal fruit-bearing tree. Coherent WIND flags the whole tree downwind. Deciduous. Grows in groves/orchards — faded trees stand behind. Ported to the shared core. */ const SP = window.SEEDLING_SPECIES.find(x=>x.key==='amla'); mount(Object.assign(window.SEEDLING_toSpec(SP), { title:'Amla', sub:'phyllanthus v2', senFrom:0.82, LW:{season:1.0, stem:1.2, leaves:1.0, flowers:1.7}, LS:{season:1.0, stem:1.0, leaves:0.9, flowers:0.8}, note:'Weeping decurrent tree · short trunk forks low into a broad low crown · airy feathery fronds · pale round fruit hung on the outer wood (persists as leaflets drop → skeletal fruiting tree) · wind-flagged · deciduous · groves.', PAL:{ soil:'#6f5128', grey:'#9a9180', bark:'#8A8577', barkDk:'#6B6658', leafDark:'#5E7A3E', leaf:'#86A256', leafLt:'#B2C77E', fruit:'#B9C24E', fruitLt:'#D4DA72', fruitDk:'#959D3A' }, stageName(st,env){ if(st.diedAt>0)return'Sealed'; const m=env.m; if(env.ageF>0.87)return'Senescent'; if(m<0.12)return'Sprout'; if(m<0.40)return'Sapling'; if(m<0.62)return'Young tree'; if(m<0.82)return'Branching'; return'Fruiting'; }, 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 amlaDNA(seed){ const dd=rng('dna',seed), g=()=>(dd()+dd()+dd()-1.5)/1.5; return { dominance:0.14+0.40*dd(), divergence:0.50+0.55*dd(), droop:0.50+0.70*dd(), gnarl:0.22+0.55*dd(), taperLen:0.62+0.12*dd(), lateralN:1.5+1.0*dd(), reach:0.80+0.45*dd(), lean:g()*0.16, stature:0.86+0.16*dd(), maxOrder:5+(dd()<0.5?1:0), windDir:dd()<0.5?1:-1, windStr:Math.pow(dd(),2.6) }; } const sLeaf = season==='Summer'||season==='Spring'?1 : season==='Autumn'?0.6 : 0.12; const sFruit= season==='Autumn'?1 : season==='Winter'?0.8 : season==='Summer'?0.35 : 0.08; // ===================== one amla tree (hero or faded background companion) ===================== function paintAmla(I){ const {seed, artSeed, cx, by, S, fade, zb, m, v, sen, browny, rewards, density} = I; const FC = c => fade>0 ? mix(c, bgC, fade) : c; const PUSH = (x,y,z,sym,col,layer)=>C.push(x,y,z+zb,sym,col,layer); const D=amlaDNA(seed); const foliageF=(1-sen)*(0.35+0.65*v)*sLeaf, fruitF=smooth(0.42,0.80,m)*rewards*sFruit; const bark=FC(mix(P.bark,P.grey,Math.max((1-v)*0.35,sen))), barkDk=FC(mix(P.barkDk,P.grey,Math.max((1-v)*0.35,sen))); const leafD=FC(mix(P.leafDark,P.grey,browny)), leafM=FC(mix(P.leaf,P.grey,browny)), leafL=FC(mix(P.leafLt,P.grey,browny*0.9)); const fruitC=FC(mix(P.fruit,P.grey,browny*0.8)), fruitLt=FC(mix(P.fruitLt,P.grey,browny*0.8)), fruitDk=FC(mix(P.fruitDk,P.grey,browny*0.8)); const greenOf=rs=>shade(mix(leafM, rs()<0.5?leafD:leafL, rs()), rs); function frond(bx,by0,baseAng,len,rs){ if(foliageF<=0.03) return; const steps=Math.max(2,Math.ceil(len/GRID)), curl=(0.45+0.9*rs())*(0.4+0.8*D.droop), wob=(rs()*2-1)*0.25; let dir=baseAng, pos=[bx,by0]; for(let i=1;i<=steps;i++){ const u=i/steps; dir += angTo(dir,DOWN)*curl*0.05*(0.25+0.75*u) + wob*0.01; const dx=Math.cos(dir), dy=Math.sin(dir); pos=[pos[0]+dx*GRID, pos[1]+dy*GRID]; PUSH(pos[0],pos[1], 34, dirSym(dx,dy), greenOf(rs), 'leaves'); const taper=Math.sin(Math.PI*Math.min(u,0.999)); if(taper<0.18 || rs()<0.38) continue; const px=-Math.sin(dir), py=Math.cos(dir), ll=GRID*(0.8+1.3*taper); for(const sd of [1,-1]){ if(rs()<0.34) continue; let la=Math.atan2(py*sd,px*sd); la += angTo(la,DOWN)*0.18; PUSH(pos[0]+Math.cos(la)*ll, pos[1]+Math.sin(la)*ll, 33, dirSym(Math.cos(la),Math.sin(la)), greenOf(rs), 'leaves'); } } } function fruitBall(fx,fy,fc,rs){ PUSH(fx,fy, 84, '.', shade(fc,rs), 'flowers'); for(const [ox,oy] of [[1,0],[-1,0],[0,1],[0,-1]]) PUSH(fx+ox*GRID, fy+oy*GRID, 83, '.', shade(fc,rs), 'flowers'); for(const [ox,oy] of [[1,1],[1,-1],[-1,1],[-1,-1]]) PUSH(fx+ox*GRID, fy+oy*GRID, 82, 'O', shade(fruitDk,rs), 'flowers'); } function fruitCluster(x,y,rs,wgt){ if(rs()>fruitF*0.8) return; const n=1+Math.floor(rs()*2*wgt); for(let k=0;k<n;k++){ const fx=x+(rs()*2-1)*GRID*2.0*S, fy=y+GRID*(1.2+rs()*3.0)*S; fruitBall(fx,fy, rs()<0.45?fruitLt:fruitC, rs); } } function sprout(rs,rootX){ if(1-smooth(0.05,0.20,m) <= 0.02) return; const g=smooth(0,0.18,m), hLen=(64+54*g)*S; let dir=UP+D.lean*1.3, pos=[rootX,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.045; pos=[pos[0]+Math.cos(dir)*GRID, pos[1]+Math.sin(dir)*GRID]; PUSH(pos[0],pos[1], 50, dirSym(Math.cos(dir),Math.sin(dir)), shade(bark,rs), 'stem'); } const tip=[pos[0],pos[1]]; for(const sd of [1,-1]){ let cd=UP + sd*(0.85+0.30*D.reach) + D.lean*0.5, cp=[tip[0],tip[1]]; const cl=Math.max(4,Math.round((34+26*g)*S/GRID)); for(let i=1;i<=cl;i++){ const u=i/cl; cd += angTo(cd,UP)*0.07; cp=[cp[0]+Math.cos(cd)*GRID, cp[1]+Math.sin(cd)*GRID]; const w=Math.sin(Math.PI*u)*GRID*1.6, px=-Math.sin(cd), py=Math.cos(cd); PUSH(cp[0],cp[1], 36, dirSym(Math.cos(cd),Math.sin(cd)), greenOf(rs), 'leaves'); for(let q=GRID*0.6;q<=w;q+=GRID){ PUSH(cp[0]+px*q, cp[1]+py*q, 35, q>w-GRID?'O':'.', greenOf(rs), 'leaves'); PUSH(cp[0]-px*q, cp[1]-py*q, 35, q>w-GRID?'O':'.', greenOf(rs), 'leaves'); } } } } const growWin=0.12, maxOrder=D.maxOrder; let budget=8000; const chain=(1-Math.pow(D.taperLen,maxOrder+1))/(1-D.taperLen), baseRun=((by-285)/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('amlabr', seed, pid); let run=baseRun*Math.pow(D.taperLen,order)*(0.82+0.36*rs())*grown; if(order===0) run*=0.72; 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.045*(1+order*0.12); const trunkFrond=clamp((0.40-m)/0.22,0,1), bears = order>=1 ? 1 : trunkFrond, canChild = grown>=1 && order<maxOrder; const nLat = order===0 ? 2+Math.floor(rs()*3) : clamp(Math.round(D.lateralN*(1-0.4*D.dominance)+gauss(rs)*0.9),0, order<2?3:2); const lo=order===0?0.22:0.30, hi=0.92, 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.6/Math.max(nLat,1)), 0.1, 0.95)); 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.12,0.035,clamp(order/3,0,1))*Math.max(0,1-order/3.0), grav=0.13*D.droop*smooth(1.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])/600,0,1); dir += D.windDir*D.windStr*0.068*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); PUSH(pos[0],pos[1], bz, sym, shade(bark,rs), 'stem'); const px=-dy, py=dx; const thick=clamp(Math.round(baseThick*baseThick*2.3*woodM*(0.7+0.3*v)*(1-0.5*u)*smooth(0.12,0.85,grown)),0,2); for(let wq=1;wq<=thick;wq++){ PUSH(pos[0]+px*wq*GRID, pos[1]+py*wq*GRID, bz, sym, shade(barkDk,rs), 'stem'); PUSH(pos[0]-px*wq*GRID, pos[1]-py*wq*GRID, bz, sym, shade(barkDk,rs), 'stem'); } if(bears>0 && u>0.18 && i%5===2 && rs()<bears*0.7){ const side=((sideSeq+(i/2|0))%2===0)?1:-1, fa=Math.atan2(py*side,px*side); frond(pos[0],pos[1], fa, (32+40*m)*S, rs); } if(order>=maxOrder-3 && i%7===0) fruitCluster(pos[0],pos[1],rs,grown); while(canChild && ci<pts.length && u>=pts[ci] && budget>0){ const side=sides[ci], div=(order===0?(0.55+0.55*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.06:0.12)*(1-D.reach*0.4); const cv=vigor*(order===0?(0.82+0.24*rs()):(0.45+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.4,0.9,D.dominance); grow(pos[0],pos[1], dir+angTo(dir,UP)*(order<=1?0.24:0.16)+gauss(rs)*0.06, leadV, order+1, birthM+growWin, pid*7+1, sideSeq+1); } else if(grown>=1){ const px=-Math.sin(dir),py=Math.cos(dir), baseDn=Math.atan2(-py,-px); frond(pos[0],pos[1], baseDn, (22+24*m)*S, rs); if(rs()<0.55) frond(pos[0],pos[1], dir, (20+22*m)*S, rs); } else if(order>=1){ frond(pos[0],pos[1], dir, (16+20*m)*S, rs); } } const rootX=cx - D.windDir*D.windStr*70*S; sprout(rng('sprout',seed), rootX); grow(rootX, by-3, UP+D.lean*0.6+D.windDir*D.windStr*0.07, 1, 0, 0, 1, 0); } // ===================== faded BACKGROUND amla trees (grove) + sharp hero ===================== const roll=rng('setting',st.seed)(); const nC = roll<0.22?0 : roll<0.56?1 : roll<0.82?2 : 3; // Solo / Pair / Stand / Grove const comps=[]; for(let i=0;i<nC;i++){ const cseed='0x'+(hashStr(st.seed+'|a'+i)>>>0).toString(16), rc=rng('acomp',st.seed,i), side=(i%2===0?-1:1), f=i/Math.max(1,nC); const birthM=0.10+i*0.16+rc()*0.06; // staggered emergence — the grove fills in over time const cm=clamp((m-birthM)/0.50, 0, 1); // each background tree GROWS IN from a sprout (no pop) if(cm<=0.03) continue; comps.push({ seed:cseed, artSeed:cseed, cx: baseX + side*(150+i*78+rc()*60), by: baseY-(8+i*14+rc()*22), // lifted + offset → reads as further back S: lerp(0.58,0.40,f)*(0.9+0.18*rc()), fade: lerp(0.42,0.62,f), // hazed toward the backdrop zb: -130 + i*6, // strictly behind the hero m: cm, v, sen, browny, rewards: clamp(rewards*(0.85+0.25*rc()),0,1), density, depth:f }); } comps.sort((a,b)=>b.depth-a.depth).forEach(paintAmla); // far → near paintAmla({ seed:st.seed, artSeed:st.artSeed, cx:baseX, by:baseY, S:1, fade:0, zb:0, m, v, sen, browny, rewards, density }); }, traits(st,env){ const dd=rng('dna',st.seed); for(let i=0;i<10;i++)dd(); const windDir=dd()<0.5?'→':'←', windStr=Math.pow(dd(),2.6); const roll=rng('setting',st.seed)(), setting = roll<0.22?'Solo':roll<0.56?'Pair':roll<0.82?'Stand':'Grove'; return [ ['Setting', setting], ['Wind', windStr<0.12?'Still':(windStr<0.40?'Breezy':windStr<0.70?'Windswept':'Storm-bent')], ['Branch depth', clamp(Math.round(2+env.m*3),2,5)], ['Fruiting', ((env.season==='Autumn'||env.season==='Winter')&&env.m>0.5&&st.diedAt===0)?'Yes':'No'], ]; } })); })(); ; /* species/bamboo.js */ /* bamboo — species index 4. GENERATED from prototypes/bamboo.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(4, spec); /* Bamboo — a clump of fluid swaying CULMS: each rises as a sheathed conical shoot, then becomes a pale sage-gold segmented pole with swollen NODE rings, gently S-swaying and arching at the top, with lanceolate leaf-sprays bursting from the UPPER nodes. Culms & leaves yellow with age. RARE gregarious FLOWERING (a badge) at end of life: pendulous stamen plumes → grain seeds → die. Clump fans (central tall, outer leaning); grove/forest via Setting. Ported to the shared core. */ const SP = window.SEEDLING_SPECIES.find(x=>x.key==='bamboo'); mount(Object.assign(window.SEEDLING_toSpec(SP), { title:'Bamboo', sub:'bambusa v2', senFrom:0.82, LW:{season:1.0, stem:1.25, leaves:1.05, flowers:1.5}, LS:{season:1.0, stem:1.0, leaves:0.85, flowers:0.8}, note:'Swaying segmented culms (sheathed shoot → noded pole) · lanceolate leaf-sprays from upper nodes · yellows with age · rare gregarious flowering → seed → death · grove/forest.', PAL:{ soil:'#6f5128', dry:'#7a5a30', culm:'#AEC078', culmDk:'#82984E', culmGold:'#C8B852', node:'#6B7438', sheath:'#A89A60', sheathDk:'#7E6E3C', leaf:'#4C8A33', leafDk:'#305A22', leafLt:'#79B451', leafYellow:'#BEB047', stamen:'#DBC766', stamenDk:'#B89A48', seed:'#9A8A52', seedDk:'#7a5a30', dead:'#9A8A52', deadDk:'#7a5a30' }, stageName(st,env){ if(st.diedAt>0)return'Sealed'; const m=env.m; if(env.ageF>0.85)return'Senescent'; if(m<0.10)return'Shoot'; if(m<0.4)return'Young culm'; if(m<0.7)return'Culm'; return'Clump'; }, draw(st, C, env){ const {m,v,sen,season,browny,density,rewards,ageF,baseX,baseY}=env, P=this.PAL; const bgC = BACKDROPS[backdropFor(st.seed)]; function bambooDNA(seed){ const dd=rng('dna',seed), g=()=>(dd()+dd()+dd()-1.5)/1.5; return { clumpiness:0.7+0.8*dd(), height:0.84+0.34*dd(), sway:0.5+1.0*dd(), swayFreq:1.2+1.6*dd(), arch:0.4+1.1*dd(), nodeLen:34+26*dd(), thickness:0.6+0.7*dd(), leafiness:0.6+0.8*dd(), lean:g()*0.10, flowering:dd()<0.13, windDir:dd()<0.5?1:-1, windStr:Math.pow(dd(),2.4) }; } function paintBamboo(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=bambooDNA(seed), yellowF=clamp(Math.max(sen*1.1,(1-v)*0.55),0,1); const flowering=D.flowering && (sen>0.12 || m>0.94), flowerPhase=flowering? smooth(0.12,0.62,Math.max(sen,(m-0.94)*4)) : 0; const culmC=FC(mix(mix(P.culm,P.culmGold,yellowF*0.7), P.dead, sen*0.5)), culmDk=FC(mix(mix(P.culmDk,P.culmGold,yellowF*0.6), P.deadDk, sen*0.5)), nodeC=FC(mix(P.node, P.deadDk, sen*0.5)); const leafC=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 windLean=D.windDir*D.windStr*0.34, rootX=cx - D.windDir*D.windStr*40*S, cy=by-4; function lanceLeaf(x
t templated, so nudge the base // x by a seed-stable, bell-weighted amount (mostly small, occasional larger) — the whole plant + // its companions shift together; the full-width ground line stays, so it just sits left/right of // centre. Bounded so wide canopies don't clip. Species can opt out / override via spec.basePlace. const baseDX = (spec.basePlace===false ? 0 : gauss(rng('placeX',st.seed)) * (spec.basePlace||45)); const env={ageF,m,density,structure:structureOf(spec,st),rho,rewards,reproOK,v,dead,sen,dormancy, season:seasonOf(st.now),browny:clamp(Math.max(1-v,sen),0,1),now:st.now,W,H,baseX:BASEX+baseDX,baseY:BASEY}; env.pollinators=pollinatorsOf(spec,st,env); return env; } function renderPlant(spec, st){ const env=computeEnv(spec,st); const C=makeCanvas(); spec.draw(st,C,env); let soil=''; if(!spec.noSoil){ // species can opt out of the drawn ground line const soilC=mix((spec.PAL&&spec.PAL.soil)||'#6f5128','#9a9180',0.25); for(let i=0;i<30;i++){ const x=40+i*((W-80)/29), yy=BASEY+16+Math.sin(i*1.7)*3; soil+=glyph('.',x,yy,GLY,soilC,1.1); } } const sc=schemeFor(st.seed); const plant=C.resolve(sc, rng('struct',st.artSeed), spec.LW||DEFLW, spec.schemeExempt||['flowers'], spec.LS||DEFLS); // ecosystem visitors: drawn ABOVE the plant in negative space; never the plant body (§6.3) let poll=''; if(env.pollinators.length && !spec.placesPollinators){ // species may place its own instead placePollinators(C, env.pollinators, st); // anchor to this species' actual blooms for(const p of env.pollinators) poll+=drawPollinator(p,env); } const inner=soil+plant+(poll?`<g id="lyr-pollinators">${poll}</g>`:''); return framed(BACKDROPS[backdropFor(st.seed)], inner); } // ---------- common stage / traits ---------- function defaultStage(spec, st, env){ if(st.diedAt>0) return 'Sealed'; const m=env.m, age=env.ageF; if(age>0.85) return 'Senescent'; if(m<0.12) return 'Sprout'; if(m<0.4) return 'Young'; if(m<0.62) return 'Developing'; if(m<0.82) return 'Mature'; return 'Full'; } function commonTraits(spec, st, env){ const stage = spec.stageName ? spec.stageName(st,env) : defaultStage(spec,st,env); const rows = [ ['Backdrop', BACKDROP_NAMES[backdropFor(st.seed)]], ['Scheme', SchemeName(schemeFor(st.seed))], ['Stage', stage], ['Vitality', Math.round(env.v*100)+'%'], ['Devotion', st.entryCount>=20?'High':st.entryCount>=8?'Medium':'Low'], ['Never wilted', st.everWilted?'No':'Yes'], ]; const ps=env.pollinators||[]; // earned via public love (>10 notes, in season) // exact tokenURI attribute names — match ART_SPEC §8 / CONTRACT_SPEC §8.5 so the contract emits these const cap=s=>s.charAt(0).toUpperCase()+s.slice(1); rows.push(['Pollinator', ps.length ? [...new Set(ps.map(p=>p.type))].map(cap).join('/') : 'None']); rows.push(['Pollinator Count', String(ps.length)]); const morph=ps.map(p=>p.morph).find(Boolean); // rare collectible variant, if any rows.push(['Pollinator Morph', morph?cap(morph):'None']); return rows; } // ============================================================ // mount(spec): inject chrome + wire the controller // ============================================================ function mount(spec){ document.title='Seedling · '+spec.title; const accent=(spec.PAL&&spec.PAL.accent)||'#7FA66B'; document.body.innerHTML=` <style> :root{ --bg:#11140f; --panel:#1b1f17; --line:#2c3322; --ink:#e9e6d8; --muted:#9aa088; --accent:${accent}; } *{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--ink); font:14px/1.5 ui-sans-serif,system-ui,sans-serif;display:flex;min-height:100vh} .stage{flex:1;display:flex;align-items:center;justify-content:center;padding:22px} .stage svg{height:min(90vh,880px);width:auto;box-shadow:0 20px 60px rgba(0,0,0,.45)} .panel{width:340px;background:var(--panel);border-left:1px solid var(--line);padding:20px;overflow-y:auto;display:flex;flex-direction:column;gap:16px} h1{font-size:15px;margin:0;letter-spacing:.05em;text-transform:uppercase} h1 small{display:block;color:var(--muted);font-size:11px;letter-spacing:.12em;margin-top:4px} .group{border:1px solid var(--line);border-radius:11px;padding:13px} .group h2{font-size:10.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--muted);margin:0 0 9px} textarea{width:100%;min-height:52px;resize:vertical;background:#0e120b;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:8px;font:inherit} .btns{display:flex;flex-wrap:wrap;gap:7px;margin-top:9px} button{background:#23291b;color:var(--ink);border:1px solid var(--line);padding:8px 11px;border-radius:8px;cursor:pointer;font:inherit;transition:.15s} button:hover{border-color:var(--accent);background:#2b3322} button.primary{background:var(--accent);color:#10140c;border-color:var(--accent);font-weight:600} button:disabled{opacity:.4;cursor:not-allowed} .stat{display:flex;justify-content:space-between;align-items:center;margin:6px 0;font-size:13px} .stat b{font-variant-numeric:tabular-nums} .bar{height:7px;background:#0e120b;border-radius:5px;overflow:hidden;margin-top:3px} .bar>i{display:block;height:100%;border-radius:5px;transition:width .35s} .meta{display:grid;grid-template-columns:1fr 1fr;gap:5px 12px;font-size:12.5px} .pill{display:inline-block;padding:2px 9px;border-radius:20px;font-size:11px;background:#0e120b;border:1px solid var(--line);color:var(--muted)} .pill.alive{color:#9fd68a;border-color:#3c5a30}.pill.dead{color:#d98a8a;border-color:#5a3030} .traits{display:flex;flex-wrap:wrap;gap:6px}.traits .t{font-size:11px;padding:3px 8px;border-radius:6px;background:#0e120b;border:1px solid var(--line)} .note{color:var(--muted);font-size:11px;line-height:1.5} a.dl{color:var(--accent);font-size:12px;text-decoration:none} </style> <div class="stage"><div id="art"></div></div> <div class="panel"> <h1>Seedling <small>${spec.title} · ${spec.sub||'v1'}</small></h1> <div class="group"><h2>Write an entry (waters the plant)</h2> <textarea id="entry" placeholder="Write today's thought… what you write shapes the plant."></textarea> <div class="btns"><button class="primary" id="write">✎ Write entry</button> <button id="w7">+1 week</button><button id="w30">+1 month</button></div> ${spec.note?`<div class="note" style="margin-top:8px">${spec.note}</div>`:''} </div> <div class="group"><h2>The three inputs</h2> <div class="stat"><span>Maturity</span><b id="mVal">0%</b></div><div class="bar"><i id="mBar" style="width:0%;background:var(--accent)"></i></div> <div class="stat"><span>Vitality</span><b id="vVal">100%</b></div><div class="bar"><i id="vBar" style="width:100%;background:#E8A200"></i></div> <div class="stat"><span>Age</span><b id="aVal">0%</b></div><div class="bar"><i id="aBar" style="width:0%;background:#c47a4a"></i></div> </div> <div class="group"><h2>State</h2> <div class="meta"><span>Stage</span><b id="stage">—</b><span>Entries</span><b id="entries">0</b> <span>Age</span><b id="age">0 d</b><span>Season</span><b id="season">—</b> <span>Status</span><span id="status" class="pill alive">alive</span></div> </div> <div class="group"><h2>Live traits</h2><div class="traits" id="traits"></div></div> <div class="btns"><button id="play" disabled>▶ Memorial</button><button id="reset">↺ New seed</button></div> <a class="dl" id="download" href="#">⤓ Download SVG</a> </div>`; const LIFESPAN=spec.LIFESPAN, DECAY=spec.DECAY||1/21; let st, history; const $=id=>document.getElementById(id); function fresh(seed){ st={seed,artSeed:seed,entryCount:0,now:0,lastWatered:0,vitality:1, wateredEpochs:0,lastWaterWeek:-1,diedAt:0,everWilted:false}; history=[snap()]; } function snap(){ return {now:st.now,entryCount:st.entryCount,vitality:st.vitality, wateredEpochs:st.wateredEpochs,artSeed:st.artSeed,diedAt:st.diedAt}; } function paint(s){ $('art').innerHTML=renderPlant(spec, s||st); } function refresh(){ paint(); const env=computeEnv(spec,st), age=clamp(st.now/LIFESPAN,0,1); $('mVal').textContent=Math.round(env.m*100)+'%'; $('mBar').style.width=(env.m*100)+'%'; $('vVal').textContent=Math.round(st.vitality*100)+'%'; $('vBar').style.width=(st.vitality*100)+'%'; $('aVal').textContent=Math.round(age*100)+'%'; $('aBar').style.width=(age*100)+'%'; $('stage').textContent=spec.stageName?spec.stageName(st,env):defaultStage(spec,st,env); $('entries').textContent=st.entryCount; $('age').textContent=Math.round(st.now)+' d'; $('season').textContent=seasonOf(st.now); const dead=st.diedAt>0, status=$('status'); status.textContent=dead?('sealed · '+st.deathCause):'alive'; status.className='pill '+(dead?'dead':'alive'); $('play').disabled=!dead; ['write','w7','w30'].forEach(id=>$(id).disabled=dead); const tw=$('traits'); tw.innerHTML=''; const tr=commonTraits(spec,st,env).concat(spec.traits?spec.traits(st,env):[]); tr.forEach(([k,val])=>{const e=document.createElement('span');e.className='t';e.innerHTML=`<b>${k}</b> ${val}`;tw.appendChild(e);}); const blob=new Blob([renderPlant(spec,st)],{type:'image/svg+xml'}); $('download').href=URL.createObjectURL(blob); $('download').download='seedling-'+spec.title.toLowerCase().replace(/\W+/g,'-')+'.svg'; } function checkDeath(){ if(st.diedAt>0)return; // §3.2: deathTime monotonic in watered weeks — never moves earlier, so no retroactive death. const dt=deathTimeOf(spec,st); if(st.now>=dt){ st.diedAt=st.now; st.deathCause = spec.MONOCARPIC ? 'MonocarpicBloom' : (consistency(st)<0.6 ? 'CareShortened' : 'FullTerm'); } } function writeEntry(){ if(st.diedAt>0)return; const txt=$('entry').value.trim()||('e'+(st.entryCount+1)+'-'+Math.random()); st.artSeed=hashStr(st.artSeed+'|'+txt+'|'+st.entryCount).toString(16).padStart(8,'0'); st.entryCount++; st.lastWatered=st.now; st.vitality=1; const wk=Math.floor(st.now/EPOCH); // distinct watered weeks (anti-gaming) if(wk!==st.lastWaterWeek){ st.wateredEpochs++; st.lastWaterWeek=wk; } $('entry').value=''; history.push(snap()); refresh(); } function advance(d){ if(st.diedAt>0)return; st.now+=d; st.vitality=vitalityOf(spec,st); // §3.1 exponential recency decay if(st.vitality<0.25)st.everWilted=true; checkDeath(); history.push(snap()); refresh(); } let raf=null; function memorial(){ if(history.length<2)return; cancelAnimationFrame(raf); const F=history,n=F.length,DUR=5200,t0=performance.now(); (function step(t){ const p=clamp((t-t0)/DUR,0,1), fp=p*(n-1), i=Math.floor(fp), f=fp-i; const A=F[i],B=F[Math.min(i+1,n-1)]; paint({seed:st.seed,artSeed:(f<0.5?A.artSeed:B.artSeed),entryCount:lerp(A.entryCount,B.entryCount,f), now:lerp(A.now,B.now,f),vitality:lerp(A.vitality,B.vitality,f), wateredEpochs:lerp(A.wateredEpochs,B.wateredEpochs,f),lastWatered:lerp(A.now,B.now,f), diedAt:(p>=1?st.diedAt:0)}); if(p<1)raf=requestAnimationFrame(step); else refresh(); })(t0); } function newSeed(){ fresh('0x'+Math.floor(Math.random()*0xffffffff).toString(16).padStart(8,'0')); refresh(); } $('write').onclick=writeEntry; $('w7').onclick=()=>advance(7); $('w30').onclick=()=>advance(30); $('reset').onclick=newSeed; $('play').onclick=memorial; $('entry').addEventListener('keydown',e=>{if(e.key==='Enter'&&(e.metaKey||e.ctrlKey))writeEntry();}); // expose for headless/preview harnesses window.__seedling={spec,renderPlant,maturity,computeEnv,get st(){return st;},set st(x){st=x;},refresh,fresh,schemeFor,settingName:null}; newSeed(); // URL params for gallery/preview: ?seed=..&e=entries&d=days&v=vitality&bare(hide panel) const q=new URLSearchParams(location.search); if(q.has('e')||q.has('seed')){ fresh(q.get('seed')||st.seed); st.entryCount=q.has('e')?+q.get('e'):20; st.now=q.has('d')?+q.get('d'):220; st.lastWatered=st.now; st.vitality=q.has('v')?+q.get('v'):0.92; const weeks=Math.max(1,Math.floor(st.now/EPOCH)); // assume ≈1 watering/week up to entryCount st.wateredEpochs=q.has('w')?+q.get('w'):clamp(st.entryCount,0,weeks); st.lastWaterWeek=Math.floor(st.now/EPOCH); st.notes=q.has('notes')?+q.get('notes'):0; // public notes → pollinators (§6.4) if(st.now>=deathTimeOf(spec,st)){st.diedAt=st.now;st.deathCause='FullTerm';} refresh(); } if(q.has('bare')){ document.querySelector('.panel').style.display='none'; const stg=document.querySelector('.stage'); stg.style.padding='0'; document.querySelector('.stage svg').style.height='100vh'; } // Studio/export support: when embedded in an iframe, PUSH the rendered SVG to the parent via // postMessage. Origin-independent — works on file:// and in Safari, where the parent reading this // iframe's contentDocument cross-origin throws SecurityError. token echoes ?_= so the parent can // match this exact frame; harmless when no parent is listening. try{ if(window.parent && window.parent!==window){ const tok=q.get('_')||''; const emit=()=>{ const sv=document.querySelector('#art svg'); if(sv) window.parent.postMessage({seedlingSVG:1, token:tok, svg:sv.outerHTML}, '*'); }; emit(); requestAnimationFrame(emit); } }catch(e){} } // ---------- exports ---------- const __api={ EPOCH,K_DEV,clamp,lerp,smooth,rng,hashStr,seasonOf, consistency,maturity,structureOf,densityOf,densityCap,deathTimeOf, vitalityOf,dormancyOf,pollinatorsOf,drawPollinator,computeEnv }; // headless (Node simulation harness): if(typeof module!=='undefined'&&module.exports) module.exports=__api; // browser namespace (so self-contained species can converge onto the corrected math // without colliding with their own local helpers — see e.g. sunflower.html): if(typeof window!=='undefined') window.SeedlingCore=__api; ; /* boot.js */ /* ============================================================ Seedling — on-chain engine BOOT entrypoint. Concatenated LAST into the engine bundle (after species-params.js, seedling-core.js, and the per-species registry files). Defines the single symbol the on-chain renderer calls: SeedlingRenderer.boot(state) `state` is the JSON object SeedlingRenderer.sol injects, mirroring the clone's DiaryState (all times in SECONDS, seed/artSeed as 0x-hex): { tokenId, species, seed, artSeed, plantedAt, lastWatered, diedAt, entryCount, wateredEpochs, deathTime } boot() maps that to the engine's `st` (which counts in DAYS, EPOCH=7), then renders + loops the life-cycle animation into the page. ============================================================ */ (function () { var DAY = 86400; var EPOCH_DAYS = 7; // species index -> spec-extension ({ title, PAL, LW, LS, draw, stageName, … }), // populated by the bundled bundle/species/*.js files (extracted from the prototypes). var REGISTRY = {}; function registerSpecies(idx, ext) { REGISTRY[idx] = ext; } // contract seconds -> engine st (DAYS). Mirrors the known-good prototype harness st. function toEngineState(state, nowSec) { var planted = Number(state.plantedAt) || 0; var ageDays = Math.max(0, (nowSec - planted) / DAY); var deathDays = planted > 0 ? Math.max(1, (Number(state.deathTime) - planted) / DAY) : 365; var lastWaterDays = Number(state.lastWatered) > planted ? (Number(state.lastWatered) - planted) / DAY : ageDays; var st = { seed: state.seed, artSeed: state.artSeed || state.seed, entryCount: Number(state.entryCount) || 0, wateredEpochs: Number(state.wateredEpochs) || 0, now: ageDays, lastWatered: lastWaterDays, lastWaterWeek: Math.floor(lastWaterDays / EPOCH_DAYS), vitality: 1, notes: 0, diedAt: 0, deathCause: null, }; if (Number(state.diedAt) > 0) { st.diedAt = Math.max(0, (Number(state.diedAt) - planted) / DAY); st.deathCause = "FullTerm"; } return { st: st, deathDays: deathDays }; } // Each species file registers its FULLY-RESOLVED spec (the prototype already merges // SEEDLING_toSpec(SP) before registering), so boot just hands it to the engine as-is. function specFor(species) { return REGISTRY[species] || null; } function mountTarget() { var el = document.getElementById("art"); if (!el) { el = document.createElement("div"); el.id = "art"; document.body.appendChild(el); } // Fill the frame; the injected SVG is styled (below) to letterbox into ANY aspect. // OpenSea renders animation_url in a SQUARE, but the art is portrait (720×980) — so the // whole plant must fit via preserveAspectRatio=meet instead of overflowing → no scroll/crop. el.style.cssText = "width:100vw;height:100vh;overflow:hidden"; if (!document.getElementById("seedling-fit")) { var s = document.createElement("style"); s.id = "seedling-fit"; // Contain the whole portrait card into any frame; leftover space is TRANSPARENT (takes the // host background — dark on OpenSea) so there are never cream bars. Portrait frames get no bars. s.textContent = "html,body{margin:0;background:transparent}#art{background:transparent}#art svg{display:block;width:100%;height:100%}"; document.head.appendChild(s); } return el; } // Honest placeholder until a species' draw() is registered (extraction is the remaining // art work). Keeps the bundle runnable + the on-chain pipeline demonstrable today. function placeholder(target, species, st) { target.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600" style="width:100%;height:100%">' + '<rect width="600" height="600" fill="#f3efe6"/>' + '<text x="300" y="290" text-anchor="middle" font-family="Georgia,serif" font-size="26" fill="#2c3a2c">Seedling engine</text>' + '<text x="300" y="330" text-anchor="middle" font-family="monospace" font-size="16" fill="#5a6a5a">species ' + species + " · entries " + st.entryCount + "</text></svg>"; } function boot(state) { var target = mountTarget(); var spec = specFor(Number(state.species)); var planted = Number(state.plantedAt) || 0; var nowSec = planted > 0 ? Math.floor(Date.now() / 1000) : 0; // Static frame (used when the species engine isn't bundled yet, or for a still). var mapped = toEngineState(state, nowSec || planted); if (!spec || typeof renderPlant !== "function") { placeholder(target, Number(state.species), mapped.st); return; } // One-way growth to NOW, then HOLD (art §7.5, revised). We sweep sprout → the plant's // CURRENT age only — never fast-forward past it — so the art shows real growth-so-far and // then freezes on the present frame. That held "now" frame is the still marketplaces poster // (the "last image"); a living plant never renders senescence/death. For the pre-reveal // teaser the contract feeds a lush mid-life state, so this grows a full green canopy + holds. var DURATION = 4200; // ms sprout→now sweep (short so the held frame settles fast) var targetDays = Math.max(0, mapped.st.now); // grow only up to the current age (NOW), never beyond var start = null; function frame(ts) { if (start === null) start = ts; var p = Math.min(1, (ts - start) / DURATION); var eased = p * p * (3 - 2 * p); // smoothstep — gentle sprout, easy settle var anim = toEngineState(state, planted + eased * targetDays * DAY).st; target.innerHTML = renderPlant(spec, anim); if (p < 1) requestAnimationFrame(frame); // stop at NOW → holds the last image (no loop, no death) } requestAnimationFrame(frame); } // public entrypoint + registry hook for the bundled species files if (typeof window !== "undefined") { window.SeedlingRenderer = { boot: boot, registerSpecies: registerSpecies, _registry: REGISTRY }; } })(); ; /* species/aloe.js */ /* aloe — species index 2. GENERATED from prototypes/aloe-vera.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(2, spec); /* Aloe Vera — a stemless rosette of thick succulent toothed leaves: it starts as a 2-leaf V, builds a narrow distichous fan, then opens into a spiral vase-rosette (newest leaves central & upright, oldest outer, long & recurved). A mature rosette throws up several thin green RACEMES, each topped by a cone of small coral florets that colour from the base up (green bud tip). Clonal PUPS crowd the base; clumps via Setting. Hardy; wind barely leans it. Ported to the shared core. */ const SP = window.SEEDLING_SPECIES.find(x=>x.key==='aloe'); mount(Object.assign(window.SEEDLING_toSpec(SP), { title:'Aloe Vera', sub:'aloe v9', senFrom:0.82, LW:{season:1.0, stem:1.2, leaves:1.25, flowers:1.45}, LS:{season:1.0, stem:1.0, leaves:0.9, flowers:0.7}, note:'Succulent rosette (2-leaf V → fan → spiral vase) · toothed flecked blades · fanned racemes of coral florets (colour rises from the base) · clonal pups · clumps.', PAL:{ soil:'#6f5128', dry:'#7a5a30', leaf:'#79A074', leafDk:'#496B47', leafLt:'#A7C58D', tooth:'#3A5638', fleck:'#E2EFD2', bud:'#8FA653', budDk:'#5E7338', stalk:'#7E9B54', flower:'#E8602E', flowerDk:'#C5331A', flowerTip:'#F4C24A', capsule:'#90A158', capsuleDry:'#9C7E40' }, stageName(st,env){ if(st.diedAt>0)return'Sealed'; const m=env.m; if(env.ageF>0.85)return'Senescent'; if(m<0.10)return'Sprout'; if(m<0.34)return'Fan'; if(m<0.58)return'Rosette'; if(m<0.78)return'Mature'; return'Flowering'; }, draw(st, C, env){ const {m,v,sen,season,browny,density,rewards,baseX,baseY}=env, P=this.PAL; const bgC = BACKDROPS[backdropFor(st.seed)], GA=Math.PI*(3-Math.sqrt(5)); const seasonBloom = (season==='Winter'||season==='Spring')?1 : season==='Autumn'?0.7 : 0.58; function aloeDNA(seed){ const dd=rng('dna',seed), g=()=>(dd()+dd()+dd()-1.5)/1.5; return { leafN:0.8+0.55*dd(), fat:0.65+0.6*dd(), recurve:0.45+0.85*dd(), twist:0.5+0.9*dd(), teeth:0.45+0.7*dd(), flecks:dd(), stature:0.85+0.30*dd(), flowerHue:g()*0.5, nRaceme:dd()<0.62?1:2, pups:0.25+0.75*dd(), windDir:dd()<0.5?1:-1, windStr:Math.pow(dd(),2.4), // — posture DNA — the rosette spiral used to be globally one-handed (sin(i*GA) is seed-independent), // so EVERY plant's crown drifted the same way → "always leans right". These break that symmetry: chir:dd()<0.5?1:-1, // spiral handedness — flips which way the phyllotaxis winds spiralPh:dd()*TAU, // per-seed phyllotaxis phase → a different wobble signature each seed lean:g(), // bell-weighted BIDIRECTIONAL posture (most upright; some tilt L or R) leanCurve:0.6+0.8*dd(), // how strongly the lean reads through blade curvature vs a rigid tilt fullSide:dd()<0.5?1:-1, // which flank carries fuller / longer blades (no rosette is a mirror) fullAmt:Math.pow(dd(),1.3) }; // strength of that lopsidedness (skewed subtle, occasionally bold) } function paintAloe(I){ const {seed, artSeed, cx0, by, S, fade, m, density, v, sen, browny, rewards} = I; const FC = c => fade>0 ? mix(c, bgC, fade) : c; const ra=rng('struct',artSeed
/* species-params.js */ /* ============================================================ Seedling — per-species engine parameters (SEEDLING_ART_SPEC §3.1/§3.3) Single source of truth for the growth/lifespan tuning constants. Consumed by the simulation harness now, and by the species prototypes as they are refined. Numbers are v1 starting points — the simulation pass exists to retune them before lockLifespanParams(). Fields: L expected lifespan, MONTHS (12–18, compressed band → ~1–1.5 yr base; care f/c then pushes actual death to ~70–105% of L) N structure saturation — entries for ~63% density (low = fast/bushy) Hhalf vitality HALF-LIFE in days (high = hardy/slow-wilt) repro maturity θ to first flower/fruit (juveniles don't reproduce) mono monocarpic — flowering ends life (cause = MonocarpicBloom) fatTail optional {p,lo,hi} — rare early death (bamboo gregarious bloom) dormant {peak, display, hemi} — §6.1 warped dormancy (peak = yr-fraction, display = fraction of year shown dormant; hemi flips calendar) poll pollinator type library (§6.4); grasses get charm + motes ============================================================ */ const SPECIES = [ { key:'sunflower', name:'Sunflower', L:12, N:10, Hhalf:14, repro:0.55, mono:true, dormant:null, poll:['bee'] }, { key:'poppy', name:'Field Poppy', L:13, N:10, Hhalf:8, repro:0.45, mono:true, dormant:null, poll:['bee'] }, { key:'aloe', name:'Aloe Vera', L:13, N:13, Hhalf:40, repro:0.60, dormant:{peak:0.55,display:0.10,hemi:'N'}, poll:['sunbird'] }, { key:'lavender', name:'Lavender', L:13, N:12, Hhalf:35, repro:0.40, dormant:{peak:0.04,display:0.15,hemi:'N'}, poll:['bee','butterfly'] }, { key:'bamboo', name:'Bamboo', L:15, N:11, Hhalf:35, repro:0.50, mono:true, dormant:{peak:0.04,display:0.08,hemi:'N'}, poll:['bird','mote'] }, { key:'wattle', name:'Golden Wattle', L:14, N:12, Hhalf:32, repro:0.50, dormant:{peak:0.55,display:0.12,hemi:'S'}, poll:['bee'] }, { key:'flytrap', name:'Venus Flytrap', L:14, N:10, Hhalf:9, repro:0.50, dormant:{peak:0.04,display:0.20,hemi:'N'}, poll:['bee'] }, { key:'bird', name:'Bird of Paradise', L:15, N:9, Hhalf:16, repro:0.65, dormant:{peak:0.04,display:0.10,hemi:'S'}, poll:['sunbird'] }, { key:'guava', name:'Chilean Guava', L:14, N:12, Hhalf:16, repro:0.50, dormant:{peak:0.04,display:0.12,hemi:'S'}, poll:['bee','butterfly'] }, { key:'maple', name:'Japanese Maple', L:17, N:12, Hhalf:16, repro:0.60, deciduous:true, dormant:{peak:0.04,display:0.22,hemi:'N'}, poll:['bee'] }, { key:'lotus', name:'Sacred Lotus', L:17, N:11, Hhalf:9, repro:0.55, deciduous:true, dormant:{peak:0.04,display:0.18,hemi:'N'}, poll:['bee','butterfly'] }, { key:'protea', name:'King Protea', L:16, N:12, Hhalf:30, repro:0.60, dormant:{peak:0.55,display:0.10,hemi:'S'}, poll:['sunbird','butterfly'] }, { key:'amla', name:'Amla', L:16, N:12, Hhalf:16, repro:0.60, deciduous:true, dormant:{peak:0.55,display:0.15,hemi:'N'}, poll:['bee'] }, { key:'saguaro', name:'Saguaro', L:17, N:20, Hhalf:45, repro:0.75, dormant:{peak:0.55,display:0.06,hemi:'N'}, poll:['bee','bird'] }, { key:'orchid', name:'Cattleya Orchid', L:17, N:9, Hhalf:9, repro:0.60, dormant:{peak:0.04,display:0.15,hemi:'S'}, poll:['butterfly'] }, { key:'hairgrass', name:'Antarctic Hair Grass',L:17, N:10, Hhalf:45, repro:0.50, dormant:{peak:0.04,display:0.18,hemi:'S'}, poll:['bird','mote'] }, ]; const MONTH_DAYS = 365 / 12; const W_FULL_FRAC = 0.6; // watered weeks (as a fraction of L in weeks) to reach the c lifespan cap // produce a core-shaped `spec` (LIFESPAN in days; H = exp time-constant from the half-life) function toSpec(p){ return { title: p.name, key: p.key, LIFESPAN: Math.round(p.L * MONTH_DAYS), // days N_DENS: p.N, H: p.Hhalf / Math.LN2, // half-life → exp(-Δt/H) time-constant W_FULL_FRAC, REPRO_THETA: p.repro, MONOCARPIC: !!p.mono, fatTail: p.fatTail || null, DORMANT: p.dormant || null, POLLINATOR: p.poll, senFrom: 0.80, }; } if (typeof module !== 'undefined' && module.exports) module.exports = { SPECIES, toSpec, W_FULL_FRAC, MONTH_DAYS }; if (typeof window !== 'undefined') { window.SEEDLING_SPECIES = SPECIES; window.SEEDLING_toSpec = toSpec; } ; /* seedling-core.js */ /* ============================================================ Seedling — shared engine core (v2) Reusable machinery for every species. A species file only defines its botany via mount({ ... draw(st, C, env) ... }). Invariant here: glyph vocab, Symbol Scheme, occupancy grid + z-order, fine grid, frame + 9 backdrops, growth = entries+age, vitality/season, the panel/controller harness. ============================================================ */ // ---------- PRNG ---------- function hashStr(s){ let h=2166136261>>>0; for(let i=0;i<s.length;i++){ h^=s.charCodeAt(i); h=Math.imul(h,16777619);} return h>>>0; } function mulberry32(a){ return function(){ a|=0; a=a+0x6D2B79F5|0; let t=Math.imul(a^a>>>15,1|a); t=t+Math.imul(t^t>>>7,61|t)^t; return ((t^t>>>14)>>>0)/4294967296; };} function rng(...p){ return mulberry32(hashStr(p.join('|'))); } // ---------- math / color ---------- const clamp=(x,a,b)=>Math.max(a,Math.min(b,x)); const lerp=(a,b,t)=>a+(b-a)*t; function smooth(e0,e1,x){const t=clamp((x-e0)/(e1-e0),0,1); return t*t*(3-2*t);} function hex2rgb(h){h=h.replace('#','');return[parseInt(h.slice(0,2),16),parseInt(h.slice(2,4),16),parseInt(h.slice(4,6),16)];} function rgb2hex(r){return'#'+r.map(v=>clamp(Math.round(v),0,255).toString(16).padStart(2,'0')).join('');} function mix(c1,c2,t){const a=hex2rgb(c1),b=hex2rgb(c2);return rgb2hex([lerp(a[0],b[0],t),lerp(a[1],b[1],t),lerp(a[2],b[2],t)]);} // HSL-space saturation boost — multiplies S by (1+f), keeps hue & lightness. Applied to every plant // glyph in resolve() so the whole botany reads a touch more alive (the marks felt muted). Pollinators // + soil bypass it on purpose (they stay desaturated/earthy). const SAT_BOOST=0.10; // +10% per user; dial here if needed function saturate(hex, f){ if(!f) return hex; const c=hex2rgb(hex), r=c[0]/255, g=c[1]/255, b=c[2]/255; const mx=Math.max(r,g,b), mn=Math.min(r,g,b), l=(mx+mn)/2; if(mx===mn) return hex; // achromatic grey — no hue to enrich const d=mx-mn; let s=l>0.5? d/(2-mx-mn) : d/(mx+mn); let h; if(mx===r) h=((g-b)/d+(g<b?6:0))/6; else if(mx===g) h=((b-r)/d+2)/6; else h=((r-g)/d+4)/6; s=clamp(s*(1+f),0,1); const q=l<0.5? l*(1+s) : l+s-l*s, p=2*l-q; const hue=(t)=>{ if(t<0)t+=1; if(t>1)t-=1; if(t<1/6)return p+(q-p)*6*t; if(t<0.5)return q; if(t<2/3)return p+(q-p)*(2/3-t)*6; return p; }; return rgb2hex([hue(h+1/3)*255, hue(h)*255, hue(h-1/3)*255]); } function shade(c,r){ const t=(r()*2-1)*0.13; return t>0?mix(c,'#ffffff',t):mix(c,'#000000',-t); } const angTo=(a,b)=>Math.atan2(Math.sin(b-a),Math.cos(b-a)); // shortest turn a->b const gauss=r=>(r()+r()+r()-1.5)/1.5; // bell-ish [-1,1] const TAU=Math.PI*2, UP=-Math.PI/2, DOWN=Math.PI/2; // ---------- grid / glyphs ---------- const W=720, H=980, BASEX=360, BASEY=905, GRID=5, GLY=GRID*0.95; function L_(x1,y1,x2,y2,c,w){return `<line x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${x2.toFixed(1)}" y2="${y2.toFixed(1)}" stroke="${c}" stroke-width="${w}" stroke-linecap="round"/>`;} function glyph(sym,x,y,s,c,w){ const h=s*0.5; switch(sym){ case '|': return L_(x,y-h,x,y+h,c,w); case '-': return L_(x-h,y,x+h,y,c,w); case '/': return L_(x-h,y+h,x+h,y-h,c,w); case '\\':return L_(x-h,y-h,x+h,y+h,c,w); case 'X': return L_(x-h,y-h,x+h,y+h,c,w)+L_(x-h,y+h,x+h,y-h,c,w); case '+': return L_(x,y-h,x,y+h,c,w)+L_(x-h,y,x+h,y,c,w); case 'O': return `<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="${(h*0.74).toFixed(1)}" fill="none" stroke="${c}" stroke-width="${w}"/>`; case '.': return `<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="${(w*1.15).toFixed(1)}" fill="${c}"/>`; case '*': return `<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="${(h*0.9).toFixed(1)}" fill="${c}"/>`; // solid area-fill (not in any scheme; only via exempt/literal layers — e.g. maple palmate leaves) case '[': return L_(x-h*0.3,y-h,x-h*0.3,y+h,c,w)+L_(x-h*0.3,y-h,x+h*0.3,y-h,c,w)+L_(x-h*0.3,y+h,x+h*0.3,y+h,c,w); case ']': return L_(x+h*0.3,y-h,x+h*0.3,y+h,c,w)+L_(x+h*0.3,y-h,x-h*0.3,y-h,c,w)+L_(x+h*0.3,y+h,x-h*0.3,y+h,c,w); } return ''; } function dirSym(dx,dy){ const ax=Math.abs(dx),ay=Math.abs(dy); if(ax<0.34*ay) return '|'; if(ay<0.34*ax) return '-'; return (dx*dy>=0)?'\\':'/'; } // ---------- Symbol Scheme (Autoglyphs-style; the per-seed source of visual uniqueness) ---------- // Full glyph vocabulary: . O + - | / \ X [ ] (every symbol below has a case in glyph()). // Mix of multi-, dual- and single-symbol schemes — single-symbol ones give the boldest, most // distinct look (the whole plant rendered in one mark, à la Autoglyphs). Brackets [ ] stand in // for the Autoglyphs "#"-class. Editing/reordering this list changes existing seeds' schemes. const SCHEMES=[ // multi-symbol — legible, layered ['\\','|','/','-','.','O'], ['\\','|','-','/'], ['O','|','-'], ['+','-','|','O'], ['+','-','|'], ['X','/','\\'], ['/','\\'], ['O','\\','/'], ['.','O','+'], ['X','+','-','|'], ['+','X','.'], ['|','-'], // single-symbol — bold, uniform ['O'], ['\\'], ['X'], ['+'], // bracket schemes — the "#"-class adapted to [ ] ['[',']'], ['[',']','O'], ['[',']','O','-'], ['[',']','|','-','+'], ['[',']','+','\\','/','O'] ]; function schemeFor(seed){ return SCHEMES[Math.floor(rng('scheme',seed)()*SCHEMES.length)]; } const SchemeName=sc=>sc.join(''); function pickSym(ideal, sc, r){ if(sc.indexOf(ideal)>=0) return ideal; // brackets are vertical-ish stand-ins; tried first so bracket-schemes actually render them const fb={ '|':['[',']','+','X','-','O','.'], '-':['[',']','+','X','|','O','.'], '/':['X','\\','+','O','[','.'], '\\':['X','/','+','O',']','.'], 'X':['+','/','\\','O'], '+':['X','O','-','|'], 'O':['.','+','X'], '.':['O','+'], '[':[']','|','O'], ']':['[','|','O'] }; const chain=fb[ideal]||['O']; for(const c of chain) if(sc.indexOf(c)>=0) return c; return sc[Math.floor(r()*sc.length)]; } // ---------- season ---------- function seasonOf(day){ const d=((day%365)+365)%365; if(d<60)return'Winter'; if(d<152)return'Spring'; if(d<244)return'Summer'; if(d<335)return'Autumn'; return'Winter'; } // ---------- 9 backdrops + white frame ---------- const BACKDROPS=['#D6E6F5','#F6EED5','#D7ECDD','#F7DEE4','#E8DEF2','#F4EBC4','#F7E0CB','#E2E6E9','#DBE1F3']; const BACKDROP_NAMES=['Sky','Cream','Mint','Blush','Lilac','Butter','Peach','Ash','Periwinkle']; function backdropFor(seed){ return Math.floor(rng('backdrop',seed)()*BACKDROPS.length); } const FRAME=46; function framed(bgc,inner){ return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg"> <rect width="${W}" height="${H}" fill="#ffffff"/> <rect x="${FRAME}" y="${FRAME}" width="${W-2*FRAME}" height="${H-2*FRAME}" fill="${bgc}"/> <clipPath id="pan"><rect x="${FRAME}" y="${FRAME}" width="${W-2*FRAME}" height="${H-2*FRAME}"/></clipPath> <g clip-path="url(#pan)">${inner}</g></svg>`; } // ---------- canvas: shared occupancy grid + z-order resolve ---------- const DEFLW={season:1.0, stem:1.15, leaves:1.0, flowers:1.7}; // per-layer glyph SIZE (× GLY): finer objects use smaller marks -> more detail. Systematic, central. const DEFLS={season:1.0, stem:1.0, leaves:0.9, flowers:0.8}; function makeCanvas(){ const cells=new Map(); const snap=x=>Math.round(x/GRID)*GRID; function push(x,y,z,ideal,col,layer){ const gx=snap(x),gy=snap(y); if(gx<FRAME+4||gx>W-FRAME-4||gy<FRAME+4||gy>H-FRAME-4) return; // stay inside the frame const k=gx+'_'+gy, ex=cells.get(k); if(!ex||z>ex.z) cells.set(k,{z,ideal,col,layer,gx,gy}); } function resolve(sc,ra,LW,exempt,LS){ const layer={season:'',stem:'',leaves:'',flowers:''}; for(const c of cells.values()){ const sym = exempt.indexOf(c.layer)>=0 ? c.ideal : pickSym(c.ideal,sc,ra); layer[c.layer]+=glyph(sym,c.gx,c.gy,GLY*LS[c.layer],saturate(c.col,SAT_BOOST),LW[c.layer]); } // NOTE: prefixed ids — must NOT collide with panel element ids (e.g. <b id="season">), // else getElementById('season') hits this <g> and refresh() wipes the layer's content. return `<g id="lyr-season">${layer.season}</g><g id="lyr-stem">${layer.stem}</g><g id="lyr-leaves">${layer.leaves}</g><g id="lyr-flowers">${layer.flowers}</g>`; } return {push,snap,resolve,cells}; } // ---------- growth env (§3.1 dual-clock, post-audit) ---------- // Four independent forces, per SEEDLING_ART_SPEC §3.1 / §3.2 (corrected): // STATURE (m) — time + watered weeks (dual clock; entries cannot buy weeks) // STRUCTURE — entryCount, CAPPED by stature (a sapling can't hold a full crown) // REWARDS — vitality × consistency, gated by repro-maturity + season // LIFESPAN — deathTime MONOTONIC in watered weeks (no retroactive death, §3.2) const EPOCH=7, K_DEV=2; // 1 week; watered week ≈ K extra weeks const fracDist=(a,b)=>{const d=Math.abs((a-b)%1);return d>0.5?1-d:d;}; // circular dist on [0,1) function consistency(st){ const weeks=Math.max(1,Math.floor(st.now/EPOCH)); return clamp((st.wateredEpochs||0)/weeks,0,1); } // ρ "showed up" (rewards only, not death) function maturity(spec, st){ // stature 0..1 (asymptotic, monotonic ↑) const qnow=Math.floor(st.now/2)*2; // ambient ~2-day render tick const D=qnow + K_DEV*(st.wateredEpochs||0)*EPOCH; // developmental time return 1-Math.exp(-D/(0.7*spec.LIFESPAN)); } function structureOf(spec, st){ return 1-Math.exp(-st.entryCount/(spec.N_DENS||12)); } // potential function densityCap(spec, m){ return spec.densityCap?spec.densityCap(m):clamp(m*1.15,0,1); } function densityOf(spec, st, m){ if(m==null)m=maturity(spec,st); // VISIBLE branches return Math.min(structureOf(spec,st), densityCap(spec,m)); } // capped by stature (§3.1) function deathTimeOf(spec, st){ // MONOTONIC in wateredEpochs (§3.2) const r=rng('life',st.seed); const f=lerp(0.70,0.85,r()); const c=lerp(0.95,1.05,r()); // every species uses the same band — no fat-tail early-death (bamboo stays monocarpic only // for the seal CAUSE via MONOCARPIC, matching the contract's cause 2). const Wfull=Math.max(1,(spec.W_FULL_FRAC||0.6)*spec.LIFESPAN/EPOCH); // watered weeks to reach the c cap const frac=clamp((st.wateredEpochs||0)/Wfull,0,1); // ↑ only as weeks accrue return spec.LIFESPAN*(f+(c-f)*frac); } // plantedAt = 0 in proto clock function vitalityOf(spec, st){ const H=spec.H||(1/(spec.DECAY||1/28)); return clamp(Math.exp(-(st.now-st.lastWatered)/H),0,1); } function dormancyOf(spec, st){ // §6.1 warped: narrow display window, calendar-anchored if(!spec.DORMANT) return 0; const {peak=0.04,display=0.15,hemi='N'}=spec.DORMANT; const a=(((st.now/365)%1)+1)%1, pk=hemi==='S'?(peak+0.5)%1:peak; const d=fracDist(a,pk), half=display/2; return d<=half ? smooth(half,0,d) : 0; } // 1 at dormant peak → 0 at window edge // ---------- pollinators (§6.3/6.4): public-attention ecosystem, never the plant ---------- // Drawn as a precise SVG layer (like soil — bypasses the glyph occupancy grid) so the tiny // marks keep their geometry instead of snapping to the 5px grid. Still the same visual // vocabulary — dots, circles, lines, short arcs — just placed exactly. Frozen mid-moment, // no animation on a living diary ("life is stillness; death earns the motion," §7). const POLL_MORPHS=['','','','','','pale','golden','night']; // mostly none; rare morphs (collectible) function pollinatorsOf(spec, st, env){ const notes=st.notes||0; if(notes<=10 || !env.reproOK || env.dormancy>0.5 || env.dead) return []; // earned + in-season only const types=spec.POLLINATOR||['bee']; const r=rng('poll',st.seed,Math.floor(notes/10)); // stable per ~10-note bucket const n=1+Math.floor(r()*3); // 1..3 (3 = rarest) const out=[]; for(let i=0;i<n;i++){ const side=r()<0.5?-1:1; out.push({ type:types[Math.floor(r()*types.length)], morph:POLL_MORPHS[Math.floor(r()*POLL_MORPHS.length)], rx:clamp(0.5+side*(0.13+r()*0.30),0.06,0.94), // off the trunk's centre column… ry:0.09+r()*0.26, // …in upper-canopy negative space (§6.4) jit:r() }); } return out; } // Library (2026-06-17, post user review): five graceful, mostly-airborne marks — bee, butterfly, // sunbird, bird, mote. The chunky moth/bat and the can't-float perched finch were dropped; the // bee was kept but redrawn quiet (two hairline variants A/B, picked per-instance). Species are // mapped onto these in species-params.js. desaturated vs the plant so focus stays on the diary; // morphs recolour the whole visitor. function pollPalette(type, morph){ if(morph==='pale') return {main:'#d7cfb6', accent:'#a89e80'}; // pale/albino — soft but still legible if(morph==='golden') return {main:'#E2A92E', accent:'#A9771A'}; if(morph==='night') return {main:'#4a4660', accent:'#726d8a'}; switch(type){ case 'butterfly': return {main:'#a596a0', accent:'#8a7c87'}; // muted, quiet (was popping) case 'sunbird': case 'bird': return {main:'#5f8076', accent:'#3c554d'}; case 'bee': return {main:'#9a895c', accent:'#6f6242'}; // soft amber, hairline case 'mote': return {main:'#d8d1ba', accent:'#cfc7b0'}; default: return {main:'#7d8a82', accent:'#566057'}; } } // Per-species placement (§6.4): after a species draws, anchor each visitor to the rewards it actually // drew — the `flowers` layer cells (blooms/fruit). The visitor hovers just beside/above an upper bloom // in clear negative space, never on the trunk. Fully automatic → covers all 16 species via their own // bloom layouts; if a species drew no flower cells (e.g. wind-pollinated grasses), the generic // upper-canopy rx/ry from pollinatorsOf is kept. function placePollinators(C, polls, st){ const fc=[]; for(const c of C.cells.values()) if(c.layer==='flowers') fc.push(c); if(fc.length<2) return; // nothing bloomed → keep generic fallback fc.sort((a,b)=>a.gy-b.gy); // top blooms first (clear sky above them) const top=fc.slice(0, Math.max(4, Math.ceil(fc.length*0.45))); const r=rng('place', st.seed, st.notes||0); polls.forEach((p)=>{ const cell=top[Math.floor(r()*top.length)]; const side=r()<0.5?-1:1, off=15+r()*13; p.x=clamp(cell.gx+side*off, FRAME+12, W-FRAME-12); // beside the bloom… p.y=clamp(cell.gy-off, FRAME+12, H-FRAME-12); // …and a touch above it (into open space) }); } function drawPollinator(p, env){ // returns an SVG string (small, STILL) const x=(p.x!=null)? p.x : FRAME+p.rx*(W-2*FRAME); // p.x/p.y set by placePollinators; else generic const y=(p.y!=null)? p.y : FRAME+p.ry*(H-2*FRAME); const big=(p.type==='sunbird'||p.type==='bird'); const s=(11 + 4*((env&&env.m)||0.6)) * (big?1.12:1); // ≈3–5% of plant height; subordinate const w=1.6, {main:c,accent:a}=pollPalette(p.type,p.morph), f=(p.jit||0)-0.5; let op=0.9; // overall whisper; bee/butterfly even softer const LN=(x1,y1,x2,y2,cc,ww)=>`<line x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${x2.toFixed(1)}" y2="${y2.toFixed(1)}" stroke="${cc}" stroke-width="${ww}" stroke-linecap="round"/>`; const DOT=(cx,cy,r,cc)=>`<circle cx="${cx.toFixed(1)}" cy="${cy.toFixed(1)}" r="${r.toFixed(1)}" fill="${cc}"/>`; const ARC=(d,cc,ww,fill)=>`<path d="${d}" fill="${fill||'none'}" stroke="${cc}" stroke-width="${ww}" stroke-linecap="round" stroke-linejoin="round"/>`; const ELL=(cx,cy,rx,ry,ang,cc,ww,fill)=>`<ellipse cx="${cx.toFixed(1)}" cy="${cy.toFixed(1)}" rx="${rx.toFixed(1)}" ry="${ry.toFixed(1)}" transform="rotate(${ang.toFixed(0)} ${cx.toFixed(1)} ${cy.toFixed(1)})" fill="${fill||'none'}" stroke="${cc}" stroke-width="${ww}"/>`; let g=''; switch(p.type){ case 'bee': { // quiet hairline bee — variant A or B per instance op=0.7; if((p.jit||0)<0.5){ // A · open oval + stripe + wing dashes g+=ELL(x,y,s*0.74,s*0.44,-10,a,w*0.8); g+=LN(x-s*0.04,y-s*0.36,x-s*0.04,y+s*0.36,a,w*0.65); // single stripe g+=LN(x-s*0.26,y-s*0.58,x-s*0.06,y-s*0.46,a,w*0.6)+LN(x+s*0.30,y-s*0.56,x+s*0.10,y-s*0.46,a,w*0.6); // wing dashes } else { // B · body dot + open V wings (most minimal) g+=DOT(x,y,w*1.45,c); g+=LN(x,y-s*0.10,x-s*0.70,y-s*0.72,a,w*0.7)+LN(x,y-s*0.10,x+s*0.70,y-s*0.72,a,w*0.7); } break; } case 'butterfly': { // two mirrored wing-arcs + slender body (kept subtle) const bw=w*0.78; // thinner strokes so it whispers, not pops g+=ARC(`M ${x} ${y} C ${x-s*1.5} ${y-s*1.2}, ${x-s*1.4} ${y+s*1.0}, ${x} ${y+s*0.18}`,c,bw); g+=ARC(`M ${x} ${y} C ${x+s*1.5} ${y-s*1.2}, ${x+s*1.4} ${y+s*1.0}, ${x} ${y+s*0.18}`,c,bw); g+=LN(x,y-s*0.22,x,y+s*0.16,a,bw); // slender body g+=LN(x,y-s*0.22,x-s*0.45,y-s*0.78,a,bw*0.85)+LN(x,y-s*0.22,x+s*0.45,y-s*0.78,a,bw*0.85); // antennae op=0.72; // softest of the four break; } case 'sunbird': { // gull-stroke wings + body + long bill g+=ARC(`M ${x-s*1.45} ${y} Q ${x-s*0.55} ${y-s*0.95} ${x} ${y-s*0.05} Q ${x+s*0.55} ${y-s*0.95} ${x+s*1.45} ${y}`,c,w); g+=DOT(x,y-s*0.05,w*1.0,a); g+=LN(x,y+s*0.05,x-s*1.15,y+s*0.95,a,w*0.8); // long sunbird bill, dipped to a flower break; } case 'bird': { // single gull-stroke + dot body g+=ARC(`M ${x-s*1.5} ${y} Q ${x-s*0.6} ${y-s*0.9} ${x} ${y} Q ${x+s*0.6} ${y-s*0.9} ${x+s*1.5} ${y}`,c,w); g+=DOT(x,y-s*0.02,w*0.9,a); break; } case 'mote': default: { // 2–3 pale drifting seed/pollen motes g+=DOT(x,y,w*1.1,c)+DOT(x+s*0.9,y-s*0.6+f*s*0.3,w*0.9,c)+DOT(x-s*0.7,y+s*0.55-f*s*0.3,w*1.0,c); break; } } return `<g opacity="${op}">${g}</g>`; } function computeEnv(spec, st){ const ageF=clamp(st.now/spec.LIFESPAN,0,1); const m=maturity(spec,st), density=densityOf(spec,st,m), rho=consistency(st); const v=clamp(st.vitality!=null?st.vitality:vitalityOf(spec,st),0,1), dead=st.diedAt>0; const sen=Math.max(smooth(spec.senFrom||0.80,1.0,ageF), dead?1:0); const reproOK=m>=(spec.REPRO_THETA||0.4); // juveniles don't flower/fruit (§3.1) const rewards=reproOK?clamp(v*(0.25+0.75*Math.sqrt(rho)),0,1):0; const dormancy=dormancyOf(spec,st); // per-seed off-centre placement: every plant rooted dead-centre fel