0xffd6…95fe

All memos sent from and to 0xffd6…95fe.

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'], ]; } })); })();
an(). -- const dna = SPEC.armPlan(st.seed, pseed); const lean = dna.lean, crook = dna.crook||0; // HEIGHT is driven by maturity only (monotonic — never shrinks); the accordion (thirst) contracts // only the GIRTH/ribs laterally, the real saguaro pleating. Mixing accordion into H made the whole // column pulse taller/shorter with vitality → "growth flicker". const H=(70+560*m)*S*dna.tall, colW=(14+21*m)*accordion*S*dna.stout; const steps=Math.max(2,Math.ceil(H/GRID)); let pos=[cx,by], dir=UP; const nRibT=clamp(Math.round(colW/2.5),7,17); let apex=[cx,by-H]; for(let s=1;s<=steps;s++){ const u=s/steps; dir += angTo(dir, UP + lean + crook*smooth(0.25,1,u))*0.05; // upper column curves → varied, non-ramrod silhouettes pos=[pos[0]+Math.cos(dir)*GRID, pos[1]+Math.sin(dir)*GRID]; const dome = u<0.86?1 : Math.sqrt(Math.max(0,1-Math.pow((u-0.86)/0.14,2))); const flare = u<0.06? 1+(0.06-u)*3 : 1; ribRow(pos[0],pos[1],Math.cos(dir),Math.sin(dir),colW*(1-0.10*u)*dome*flare,nRibT,50,rng('col',st.seed,pseed,s), s%4===0); if(u>0.9) apex=[pos[0],pos[1]]; } // -- ARMS: emerge progressively (each has its own emergeAt) from the seed's arm plan; out then UP at // the elbow. Asymmetric heights/sides/lengths + rare secondary sub-arms → varied candelabra. -- const armTips=[]; const nRibFor=aw=>clamp(Math.round(aw/2.0),6,12); function growArm(p0, d0, A, grow, rsKey, depth){ const armLen=(135+205*m)*S*A.lenF*clamp(grow,0.22,1)*(depth?0.6:1); const aw=colW*(0.50+0.14*A.curve)*(depth?0.7:1); let p=p0.slice(), d=d0, sub=null; const asteps=Math.max(3,Math.ceil(armLen/GRID)), nRibA=nRibFor(aw); for(let k=1;k<=asteps;k++){ const u=k/asteps; d += angTo(d, UP + sen*A.side*0.45)*(u<A.elbow?0.05:0.18*A.curve); p=[p[0]+Math.cos(d)*GRID, p[1]+Math.sin(d)*GRID]; const tipR=u>0.84?Math.sqrt(Math.max(0,1-Math.pow((u-0.84)/0.16,2))):1; ribRow(p[0],p[1], 0,1, aw*(1-0.10*u)*tipR, nRibA, 49, rng(rsKey,st.seed,pseed,depth,k), k%4===0); if(depth===0 && A.sub && !sub && u>=A.subU) sub=[p.slice(), d]; // mark a secondary-branch point } armTips.push(p); if(sub && grow>0.72){ const subA={side:A.side, lenF:A.lenF, elbow:0.32, curve:A.curve, spread:A.spread, sub:false, subU:1}; growArm(sub[0], sub[1]+A.side*0.85, subA, 0.85, rsKey+'s', 1); } } for(const A of dna.arms){ if(m < A.emergeAt) continue; const grow=clamp((m-A.emergeAt)/0.18, 0.22, 1); // arms keep extending after emerging const start=[cx + A.side*colW*0.55, by - H*A.atU], d0=UP + A.side*A.spread; growArm(start, d0, A, grow, 'ar'+(A.side>0?'R':'L'), 0); } // -- apex CROWN: white funnel flowers + green club buds → ruby fruit, at the trunk top & arm tips -- // a green, club-shaped, scaly bud (the pre-bloom stage in the reference) function drawBud(fx,fy,R,rs){ for(let gy=-R*1.45;gy<=R*0.45;gy+=GRID){ const u=(gy/(R*1.45)+1)/1.45; const w=R*0.58*Math.sin(Math.PI*clamp(u*0.72+0.16,0,1)); for(let gx=-w;gx<=w;gx+=GRID){ const edge=Math.abs(gx)>w-GRID; const scale=edge && (Math.round(gy/(GRID*2))%2===0); // suggest overlapping scales C.push(fx+gx,fy+gy,86,'.',shade(FC(scale||gy<-R*1.0?P.budTip:P.bud),rs),'flowers'); } } } // a white funnel flower, read cleanly at plant scale ("fried-egg"): a darker shadow halo rims the // bloom so the white separates from pale backdrops → white petal disc (thin sage rim) → gold centre. function drawFlower(fx,fy,R,openF,rs){ const RR=R*(0.98+0.18*openF), halo=RR*1.16; for(let gx=-halo;gx<=halo;gx+=GRID)for(let gy=-halo;gy<=halo*1.04;gy+=GRID){ if(gx*gx+gy*gy>halo*halo)continue; C.push(fx+gx,fy+gy,83,'.',shade(FC(P.petalShade),rs),'flowers'); } // soft drop-shadow ring const pr=RR; // white petal disc — white dominates for(let gx=-pr;gx<=pr;gx+=GRID)for(let gy=-pr;gy<=pr;gy+=GRID){ const d=Math.hypot(gx,gy); if(d>pr)continue; C.push(fx+gx,fy+gy,86,'.',shade(FC(d>pr-GRID*0.7?P.petalEdge:P.petal),rs),'flowers'); } // very thin rim const cr=RR*0.34; // smaller golden stamen centre for(let gx=-cr;gx<=cr;gx+=GRID)for(let gy=-cr;gy<=cr;gy+=GRID){ const rr=Math.hypot(gx,gy)/cr; if(rr>1)continue; C.push(fx+gx,fy+gy,90,'.',shade(FC(rr<0.34?P.stamenDk:rr>0.78?P.stamenDk:P.stamen),rs),'flowers'); } } // a ruby fruit: a clean oval (green→red→ruby) with a shadow halo, areole dots + brown floral // remnant; if open, a central red-flesh cup packed with black seeds (the iconic split saguaro fig). function drawFruit(fx,fy,R,ripe,open,rs,fid){ const base = ripe<0.5 ? mix(P.fruitGreen,P.fruit, ripe*2) : mix(P.fruit,P.fruitDk,(ripe-0.5)*2); const halo=R*1.06; // soft separation from neighbours for(let gx=-halo;gx<=halo;gx+=GRID)for(let gy=-halo*1.35;gy<=halo;gy+=GRID){ if((gx*gx)/(halo*halo)+(gy*gy)/(halo*halo*1.6)>1)continue; C.push(fx+gx,fy+gy,85,'.',shade(FC(P.petalShade),rs),'flowers'); } for(let gx=-R;gx<=R;gx+=GRID)for(let gy=-R*1.35;gy<=R*1.35;gy+=GRID){ const ny=gy/(R*1.35), rr=(gx*gx)/(R*R)+ny*ny; if(rr>1)continue; const edge=rr>0.76; C.push(fx+gx,fy+gy,86,'.',shade(FC(edge?mix(base,'#000',0.16):base),rs),'flowers'); } for(let i=0;i<8;i++){ const aa=rng('arl',st.seed,pseed,fid,i); // areole dots on skin (STABLE seed) const ax=(aa()*2-1)*R*0.7, ay=(aa()*2-1)*R*1.05; if((ax*ax)/(R*R)+(ay*ay)/(R*R*1.7)>0.7)continue; C.push(fx+ax,fy+ay,87,'.',shade(FC(P.spine),rs),'flowers'); } for(let gx=-R*0.3;gx<=R*0.3;gx+=GRID) // brown dried-flower remnant tuft C.push(fx+gx,fy-R*1.24,88,'.',shade(FC(P.skelDk),rs),'flowers'); if(open){ const ow=R*0.56, oh=R*0.92; // split: central red-flesh cup for(let gx=-ow;gx<=ow;gx+=GRID)for(let gy=-oh;gy<=oh*0.9;gy+=GRID){ if((gx*gx)/(ow*ow)+(gy*gy)/(oh*oh)>1)continue; C.push(fx+gx,fy+gy,89,'.',shade(FC(P.pulp),rs),'flowers'); } for(let i=0;i<16;i++){ const sr=rng('sd',st.seed,pseed,fid,i); // dense black seeds (STABLE seed) const sx=(sr()*2-1)*ow*0.78, sy=(sr()*2-1)*oh*0.82; if((sx*sx)/(ow*ow)+(sy*sy)/(oh*oh)>0.82)continue; C.push(fx+sx,fy+sy,90,'.',shade(FC(P.seed),sr),'flowers'); } } } // a coherent crown: one mode (buds OR flowers OR fruit, by season) so it doesn't muddle, spaced // in a small fan so the blooms read individually. // cid = a STABLE crown id (e.g. 'apex' / 'arm2'); all per-item randomness is seeded on it + // st.seed, NOT on the drifting pixel position, so buds/flowers/fruit don't flicker as the plant grows. function crown(tx,ty,scale,rs,cid){ if(drySkel>0.5 || !crownOn) return; const Rf=(15+7*scale)*S; if(crownMode==='fruit'){ // a row of distinct ruby figs, one or two split open const nf=Math.max(2,Math.round(1.4+1.4*scale)); for(let k=0;k<nf;k++){ const jr=rng('cj',st.seed,pseed,cid,k), off=k-(nf-1)/2; const fx=tx+off*Rf*1.34, fy=ty - Rf*0.35 - Math.abs(off)*Rf*0.30 + (jr()-0.5)*Rf*0.2; const ripe=clamp((isAutumn?0.92:0.66)+(jr()-0.5)*0.28,0,1), open=jr()<0.4; // stable per-fruit (no vitality) drawFruit(fx,fy,Rf*0.56, ripe, open, rs, cid+'_'+k); } return; } const n=Math.max(2,Math.round(1.6+1.8*scale)); // spring blooms in a small fan for(let k=0;k<n;k++){ const a=UP+(k-(n-1)/2)*0.82, rr=Rf*1.42; const fx=tx+Math.cos(a)*rr, fy=ty+Math.sin(a)*rr*0.5 - Rf*0.5; const jr=rng('cj',st.seed,pseed,cid,k); if(jr()<0.18) drawBud(fx,fy,Rf*0.6,rs); else drawFlower(fx,fy,Rf, 0.85, rs); // fixed openness → no size shimmer } } if(crownOn){ crown(apex[0],apex[1],1, rng('cr',st.seed,pseed), 'apex'); armTips.forEach((tp,i)=>{ if(rng('at',st.seed,pseed,i)()<0.7) crown(tp[0],tp[1],0.78,rng('cr',st.seed,pseed,i+1), 'arm'+i); }); } } // ===================== STAND: 1 → several saguaros from the seed ===================== const cr=rng('colony',st.seed), roll=cr(); const nC = roll<0.50?0 : roll<0.80?1 : roll<0.93?2 : 3+Math.floor(cr()*3); // companions: 0..5 const comps=[]; for(let k=0;k<nC;k++){ const r=rng('comp',st.seed,k); const side=k%2?1:-1, rank=Math.floor(k/2)+1, depth=clamp(0.20+0.70*r(),0,1); const S=lerp(0.62,0.30,depth), fade=lerp(0.24,0.62,depth); const cxP=clamp(baseX+side*(150+(rank-1)*135)+(r()*2-1)*36, FRAME+70, W-FRAME-70); const byP=baseY - depth*42; // far ones raised toward the horizon const mm=clamp(m + (r()*2-1)*0.30, 0.16, 1), vv=clamp(v*(0.6+0.5*r()), 0.2, 1), ssn=clamp(sen + (r()*2-1)*0.12, 0, 1); comps.push({cx:cxP, by:byP, S, fade, m:mm, v:vv, sen:ssn, ageF:env.ageF, rewards:clamp(rewards*(0.5+0.7*r()), 0, 1), reproOK:mm>=0.75, browny:clamp(Math.max(1-vv,ssn),0,1), dead:false, pseed:'c'+k, depth}); } comps.sort((a,b)=>b.depth-a.depth).forEach(paintSaguaro); // far (faint) → near paintSaguaro({cx:baseX, by:baseY, S:nC>0?0.92:1.0, fade:0, m, v, sen, ageF:env.ageF, rewards, reproOK, browny, dead, pseed:'hero'}); }, // per-seed silhouette plan — shared by draw() and traits() so the arm count stays in sync. // Returns {stout, tall, lean, arms:[{side, atU, emergeAt, lenF, elbow, spread, curve, sub, subU}]}. armPlan(seed, pseed){ const r=rng('dna',seed,pseed); // wider girth/height/lean ranges + a gentle per-seed column CROOK so even armless columns differ // (most slight, some clearly curved/leaning — the pow(r,2) skews bends LOW). const stout=0.76+0.62*r(), tall=0.74+0.52*r(), lean=(r()*2-1)*0.13; const crook=Math.pow(r(),2)*0.34*(r()<0.5?-1:1); const t=r(), maxA = t<0.08?0 : t<0.22?1 : t<0.46?2 : t<0.70?3 : t<0.88?4 : 5; // ~8% lone columns, rest 1–5 arms const arms=[]; let last=r()<0.5?-1:1; for(let i=0;i<maxA;i++){ const rr=rng('armp',seed,pseed,i); const side=rr()<0.66?-last:last; last=side; // emerge at REACHABLE maturity, STAGGERED by arm index → a progressive candelabra instead of a // pole that only sprouts arms at near-max m (the "always a single straight cactus" cause). arms.push({ side, atU:0.30+0.46*rr(), emergeAt:0.54+i*0.05+0.15*rr(), lenF:0.60+0.85*rr(), elbow:0.16+0.20*rr(), spread:0.85+0.70*rr(), curve:0.65+0.80*rr(), sub:rr()<0.22, subU:0.5+0.3*rr() }); } return { stout, tall, lean, crook, arms }; }, traits(st,env){ const m=env.m, dna=this.armPlan(st.seed,'hero'); const nArm=dna.arms.filter(a=>m>=a.emergeAt).length; const sub=dna.arms.some(a=>a.sub && m>=a.emergeAt+0.05); const cr=rng('colony',st.seed), roll=cr(); const total = 1 + (roll<0.50?0 : roll<0.80?1 : roll<0.93?2 : 3+Math.floor(cr()*3)); return [ ['Arms', nArm + (sub?' (branched)':'')], ['Crown', st.diedAt>0?'—' : (env.m>=0.75 && env.rewards>0.3 ? (env.season==='Summer'?'Fruit':'Flowers') : '—')], ['Stand', total===1?'Lone':total===2?'Pair':total<=3?'Cluster':'Forest'], ['Form', st.diedAt>0?'Rib skeleton':'Ribbed column'], ]; } })); })(); ; /* species/wattle.js */ /* wattle — species index 5. GENERATED from prototypes/golden-wattle.html by extract.js. Do not edit here — edit the prototype and re-run `npm run extract`. The prototype's mount() call is redirected to SeedlingRenderer.registerSpecies via the local shim. */ (function () { const mount = (spec) => SeedlingRenderer.registerSpecies(5, spec); /* Golden Wattle — Acacia. A low-forking broad DOMED little tree (recursive staggered-lateral frame) clothed in feathery BIPINNATE leaves (a rachis of paired pinnae of tiny leaflets). Reward: green buds → fuzzy GOLDEN POM-POM flower heads (late winter/spring, S-hemisphere) → flattened legume SEED PODS (green→brown) at senescence. Coherent wind. Grows in stands/groves. Ported to the shared core. */ const SP = window.SEEDLING_SPECIES.find(x=>x.key==='wattle'); mount(Object.assign(window.SEEDLING_toSpec(SP), { title:'Golden Wattle', sub:'acacia v2', senFrom:0.82, LW:{season:1.0, stem:1.2, leaves:1.0, flowers:1.4}, LS:{season:1.0, stem:1.0, leaves:0.85, flowers:0.8}, note:'Broad domed acacia · feathery bipinnate foliage · green buds → fuzzy golden pom-pom flowers (late winter/spring) → flat seed pods · wind · stands/groves.', PAL:{ soil:'#6f5128', dry:'#7a5a30', grey:'#9a8f74', bark:'#7A6A48', barkDk:'#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);
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(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 armPl
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 = 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'); }
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; 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 //
if(edge && rng('t',seed,i,s)()<tornF) continue; // torn margin (scar) const vein = !edge && !midrib && (s%5===0); let col = edge?P.leafDk : midrib?colMid : (w<0?P.leaf:P.leafLt); if(vein) col=mix(col,P.leafDk,0.45); PUSH(pos[0]+px*w, pos[1]+py*w, z, midrib?'|':(edge?dirSym(dx,dy):(vein?dirSym(px,py):dirSym(dx,dy))), tone(col), 'leaves'); } } } } // ---- the CRANE FLOWER: spathe beak + orange sepal crest + blue tongue ---- function blade(ox,oy,ang,len,wBase,cTip,cBody,z,rs){ // tapering pointed petal / flame const px=-Math.sin(ang), py=Math.cos(ang); let p=[ox,oy]; const tn=Math.max(2,Math.ceil(len/GRID)); for(let t=1;t<=tn;t++){ p=[p[0]+Math.cos(ang)*GRID, p[1]+Math.sin(ang)*GRID]; const u=t/tn, hw=wBase*(1-u*0.9); for(let w=-hw;w<=hw;w+=GRID) PUSH(p[0]+px*w, p[1]+py*w, z, u>0.82?'.':dirSym(Math.cos(ang),Math.sin(ang)), shade(FC(u>0.68?cTip:cBody),rs),'flowers'); } } function drawBud(jx,jy,fd,R,rs){ // unopened pointed sheath spear (maroon base → green) const a=fd>0?-0.5:Math.PI+0.5, px=-Math.sin(a), py=Math.cos(a), len=R*1.9, tn=Math.ceil(len/GRID); let p=[jx,jy]; for(let t=1;t<=tn;t++){ p=[p[0]+Math.cos(a)*GRID,p[1]+Math.sin(a)*GRID]; const u=t/tn, hw=GRID*S*(2.1*Math.sin(Math.PI*clamp(u*0.78+0.12,0,1))); for(let w=-hw;w<=hw;w+=GRID){ const edge=Math.abs(w)>hw-GRID; const col=u<0.34?mix(P.spatheDk,P.spathe,u/0.34):(edge?P.leafDk:P.spathe); PUSH(p[0]+px*w,p[1]+py*w,82, edge?dirSym(Math.cos(a),Math.sin(a)):'.', shade(FC(mix(col,'#7a5a30',dry*0.5)),rs),'flowers'); } } } function drawCrane(jx,jy,fd,R,open,rs){ // beak spathe + upward orange flames + blue arrow const ba=fd>0?0.30:Math.PI-0.30, bdx=Math.cos(ba), bdy=Math.sin(ba), beak=R*(1.4+0.7*open), bpx=-Math.sin(ba), bpy=Math.cos(ba); for(let i=1;i<=Math.ceil(beak/GRID);i++){ const t=i/Math.ceil(beak/GRID), bx=jx+bdx*t*beak, by=jy+bdy*t*beak; const hh=GRID*S*(1.0+2.2*Math.sin(Math.PI*clamp(t*0.8+0.18,0,1))); for(let w=-hh;w<=hh;w+=GRID){ const edge=Math.abs(w)>hh-GRID*1.1; PUSH(bx+bpx*w, by+bpy*w, 80, edge?dirSym(bdx,bdy):'.', shade(FC(mix(edge?P.spatheDk:mix(P.spathe,P.spatheDk,0.25), '#7a5a30', dry*0.75)),rs),'flowers'); } } // boat spathe browns + persists as the husk // crest: bright orange flames when alive → a couple of short brown withered nubs once dried const withered=dry>0.6, ns=withered?2:3+Math.round(open*1.5); const flameTip =mix(P.orange,'#7a5a30', withered?0.9:dry*0.6); const flameBody=mix(mix(P.orange,P.orangeDk,0.35),'#7a5a30', withered?0.82:dry*0.35); for(let k=0;k<ns;k++) blade(jx, jy, UP+(k-(ns-1)/2)*0.24+fd*0.06, R*(0.7+1.0*open)*(1+0.22*(k%2))*(withered?0.45:1), GRID*1.3*S, flameTip, flameBody, 90, rs); if(open>0.5 && !withered) blade(jx+fd*GRID*S, jy-GRID*0.5, fd>0?-0.55:Math.PI+0.55, R*1.3*open, GRID*1.5*S, P.blueLt, P.blue, 91, rs); // blue tongue — only on a live open crane } // ---- BLOOM: cranes spread ACROSS the clump on tall ARCHING stalks of varied height ---- // The flower STRUCTURE (woody stalk + boat spathe) PERSISTS into death as a dried husk — it // does NOT vanish (spec death state = "dried spathe husk"). So the stalk presence is gated on // maturity+reward but NOT on (1-sen); only the live COLOUR (open orange crest + blue tongue) // fades with senescence/season and finally withers to brown. const seasonOK=(season==='Winter'||season==='Spring')?1:0.7; const earned = m>=0.55 ? smooth(0.05,0.55,rewards) : 0; // did it earn cranes? const standDrive = Math.max(earned*(1-winter*0.7), dead&&m>=0.55 ? 0.55 : 0); // a mature plant that died keeps standing dried stalks const liveBloom = earned*(1-sen)*(1-winter)*seasonOK; // colourful open cranes (alive + in season) if(standDrive>0.10){ const nb=clamp(1+Math.round(standDrive*2*(0.5+0.5*D.nFlowers/3)), 1, 3); for(let b=0;b<nb;b++){ const rs=rng('cr',seed,b), hr=rng('h',seed,b)(); const spread = nb>1 ? (b-(nb-1)/2)/((nb-1)/2) : 0; // -1..1 across the clump const leanDir = spread!==0 ? (spread>0?1:-1) : D.bloomSide; // outer cranes lean outward; solo leans bloomSide const bx = cx + (spread*(42+34*m) + D.fanLean*30)*S; // wider → cranes clearly off-centre, not all from the middle const open = clamp(liveBloom*1.6 - b*0.45, 0, 1); // older cranes open; newer still budding; 0 once dried const sh = (215+320*m)*(0.82+0.18*v)*(1 - D.heightVar*0.45 + D.heightVar*hr)*S; // taller → cranes ride above the fan so the arch shows; strong height variation const curl = D.stalkCurl*(0.5+0.7*Math.abs(spread))*(1+dry*0.5); // outer stalks arch more; dried stalks sag/arch further let p=[bx, cy], dir=UP + leanDir*D.stalkLean*0.4 + D.fanLean*0.5; const steps=Math.ceil(sh/GRID); const stalkCol=mix(P.stalk,'#6b4a2a',dry*0.85); // green stalk → brown dried husk for(let s=1;s<=steps;s++){ const u=s/steps; dir += leanDir*curl*0.013*smooth(0.15,1,u); // progressive arch toward the top (not a straight pole) p=[p[0]+Math.cos(dir)*GRID, p[1]+Math.sin(dir)*GRID]; for(let w=-GRID*0.8;w<=GRID*0.8;w+=GRID) PUSH(p[0]+w,p[1],50,'|',shade(FC(stalkCol),rs),'stem'); } const R=(24+22*m)*(0.85+0.15*v)*S; // dried → the empty boat spathe stands (browned crest); young & living → unopened spear bud; else open crane if(dry>0.5) drawCrane(p[0],p[1],leanDir,R, clamp(open,0.28,0.5), rs); else if(open<0.4) drawBud(p[0],p[1],leanDir,R,rs); else drawCrane(p[0],p[1],leanDir,R,open,rs); } } } // ===================== background CLUMP (varied size, scattered) + sharp hero ===================== // Solo → 6-strong clump, companions SCATTERED across the canvas with per-seed jitter and INDEPENDENT // depth (size/fade/height NOT tied to draw order) → no two seeds share a layout, no rigid L/R flanking. const roll=rng('setting',st.seed)(); const nC = roll<0.20?0 : roll<0.44?1 : roll<0.66?2 : roll<0.84?3 : roll<0.95?4 : 5; // 0..5 companions const comps=[]; for(let i=0;i<nC;i++){ const rc=rng('bcomp',st.seed,i); const frac = nC>1 ? (i+0.5)/nC : 0.5; // spread across the width… const slot = clamp(frac + (rc()*2-1)*(0.85/nC), 0.05, 0.95); // …+ strong jitter → unpredictable, not rigid L/R const cxP = lerp(FRAME+85, W-FRAME-85, slot); const depth = clamp(0.10 + 0.82*rng('bdep',st.seed,i)(), 0, 1); // INDEPENDENT of index → organic size/fade/height const birthM = 0.06 + rc()*0.42; // randomized emergence (grows in from a sprout, no pop) const cm = clamp((m-birthM)/0.46, 0, 1); if(cm<=0.03) continue; const cseed='0x'+(hashStr(st.seed+'|b'+i)>>>0).toString(16); comps.push({ seed:cseed, artSeed:cseed, cx: cxP, cy: baseY-4 - (6 + depth*42 + rc()*14), // deeper (far) plants sit higher → toward the horizon S: lerp(0.60,0.34,depth)*(0.88+0.22*rc()), dim: lerp(0.38,0.66,depth), zb: -110 - Math.round(depth*60), // always behind the hero; far ones furthest back m: cm, v, sen, browny, rewards: clamp(rewards*(0.82+0.32*rc()),0,1), density, dead, depth }); } comps.sort((a,b)=>b.depth-a.depth).forEach(paintBird); // far (faint) → near paintBird({ seed:st.seed, artSeed:st.artSeed, cx:baseX, cy:baseY-4, S:1, dim:0, zb:0, m, v, sen, browny, rewards, density, dead }); }, traits(st,env){ const roll=rng('setting',st.seed)(); const total = 1 + (roll<0.20?0 : roll<0.44?1 : roll<0.66?2 : roll<0.84?3 : roll<0.95?4 : 5); const setting = total===1?'Solo' : total===2?'Pair' : total===3?'Trio' : 'Clump'; const D=(function(seed){ const dd=rng('dna',seed); dd();dd();dd();dd();dd(); const nF=1+Math.floor(dd()*3); const side=dd()<0.5?1:-1; return {nFlowers:nF, bloomSide:side}; })(st.seed); return [ ['Leaves', clamp(Math.round(3+env.density*4),3,7)], ['Bloom', st.diedAt>0?'Dried husk':(env.m>=0.55&&env.rewards>0.3?'Crane':'—')], ['Faces', D.bloomSide>0?'East':'West'], ['Setting', setting], ]; } })); })(); ; /* species/guava.js */ /* guava — species index 8. GENERATED from prototypes/chilean-guava.html by extract.js. Do not edit here — edit the prototype and re-run `npm run extract`. The prototype's mount() call is redirected to SeedlingRenderer.registerSpecies via the local shim. */ (function () { const mount = (spec) => SeedlingRenderer.registerSpecies(8, spec); /* Chilean Guava — Ugni molinae. A compact upright oval myrtle SHRUB: a recursive staggered-lateral framework clothed in distinct rounded FOLIAGE CLUMPS with branches showing between them. Reward: white powderpuff FLOWERS (late spring/summer) → ruby BERRIES ripening green→red→purple (autumn/ winter). Evergreen, then drops to bare twigs in old age. Grows in thickets/hedges. Ported to core. */ const SP = window.SEEDLING_SPECIES.find(x=>x.key==='guava'); mount(Object.assign(window.SEEDLING_toSpec(SP), { title:'Chilean Guava', sub:'ugni v2', senFrom:0.82, LW:{season:1.0, stem:1.15, leaves:1.0, flowers:1.5}, LS:{season:1.0, stem:1.0, leaves:0.85, flowers:0.85}, note:'Compact upright myrtle shrub · rounded foliage clumps with twigs showing between · white powderpuff flowers → ruby berries (green→red→purple) · evergreen → bare in old age · thickets.', PAL:{ soil:'#6f5128', grey:'#9a8f74', bark:'#6E5B46', barkDk:'#46382A', leaf:'#2F5E36', leafDk:'#1C3F22', leafLt:'#5C8C4C', leafYoung:'#9A6A4E', leafYellow:'#C2A848', petal:'#F5ECEE', stamen:'#E0668A', stamenDk:'#C23A66', anther:'#E8A82C', berryGreen:'#7FA84E', berryRed:'#C2384C', berryPurple:'#5A2746', berryRipe:'#3A1730', calyx:'#7E8A4A' }, stageName(st,env){ if(st.diedAt>0)return'Sealed'; const m=env.m, season=env.season; if(env.ageF>0.84)return'Dormant'; if(m<0.12)return'Sprig'; if(m<0.34)return'Seedling'; if(m<0.55)return'Bush'; const fruity=(season==='Autumn'||season==='Winter'), flowery=(season==='Spring'||season==='Summer'); if(m<0.62)return'Mature'; return fruity?'Fruiting':(flowery?'Flowering':'Dense'); }, draw(st, C, env){ const {m,v,sen,season,browny,density,rewards,baseX,baseY}=env, P=this.PAL; const bgC = BACKDROPS[backdropFor(st.seed)]; function guavaDNA(seed){ const dd=rng('dna',seed), g=()=>(dd()+dd()+dd()-1.5)/1.5; return { dominance:0.46+0.4*dd(), divergence:0.5+0.4*dd(), droop:0.12+0.28*dd(), gnarl:0.3+0.5*dd(), taperLen:0.68+0.12*dd(), lateralN:1.3+0.9*dd(), reach:0.5+0.32*dd(), lean:g()*0.09, stature:0.82+0.2*dd(), maxOrder:4+(dd()<0.5?1:0), leafiness:0.95+0.45*dd(), flowerAb:0.5+0.6*dd(), fruitAb:0.5+0.6*dd(), windDir:dd()<0.5?1:-1, windStr:Math.pow(dd(),2.6) }; } function paintGuava(I){ const {seed, artSeed, cx, by, S, fade, m, density, v, sen, browny, rewards} = I; const FC = c => fade>0 ? mix(c, bgC, fade) : c; const D=guavaDNA(seed), yellowF=clamp(Math.max(sen*0.9,(1-v)*0.5),0,1); const seasonBloom = season==='Spring'?1 : season==='Summer'?0.85 : season==='Winter'?0.15 : 0.5; const seasonRipe = season==='Winter'?1 : season==='Autumn'?0.8 : season==='Summer'?0.35 : 0.12; const foliageF=clamp((1-sen*0.92)*(0.5+0.5*v),0,1); const bloom=smooth(0.44,0.66,m)*(1-sen)*seasonBloom*rewards, fruitF=smooth(0.52,0.78,m)*rewards*clamp(seasonRipe+0.2,0,1)*(1-0.5*sen), ripe=clamp(smooth(0.55,0.85,m)*seasonRipe + sen*0.4, 0, 1); const bark=FC(mix(P.bark,P.grey,Math.max((1-v)*0.3,sen*0.6))), barkDk=FC(mix(P.barkDk,P.grey,Math.max((1-v)*0.3,sen*0.6))); const leafM=FC(mix(P.leaf,P.leafYellow,yellowF)), leafD=FC(mix(P.leafDk,P.leafYellow,yellowF*0.8)), leafL=FC(mix(P.leafLt,P.leafYellow,yellowF)); const greenOf=rs=>shade(mix(leafM, rs()<0.5?leafD:leafL, rs()), rs); const windLean=D.windDir*D.windStr*0.24, rootX=cx - D.windDir*D.windStr*34*S; function leafClump(ccx,ccy,rad,rs){ if(foliageF<=0.05) return; const r2=rad*rad; for(let gx=-rad;gx<=rad;gx+=GRID) for(let gy=-rad;gy<=rad;gy+=GRID){ const rr=gx*gx+gy*gy; if(rr>r2) continue; const edge=rr>r2*0.5; if(rs() > foliageF*(edge?0.4:0.94)) continue; const col= edge? shade(leafD,rs) : (rs()<0.34? shade(leafL,rs) : shade(leafM,rs)); C.push(ccx+gx, ccy+gy, 56, rs()<0.55?'O':'.', col, 'leaves'); } } function berry(x,y,rp,rs){ const col = rp<0.45 ? mix(P.berryGreen,P.berryRed,rp/0.45) : mix(P.berryRed, mix(P.berryPurple,P.berryRipe,clamp(sen,0,1)), (rp-0.45)/0.55); const cc=FC(mix(col, '#7a5a30', browny*0.4)); C.push(x,y,88,'.',shade(cc,rs),'flowers'); if(rp>0.6) C.push(x+GRID*0.4,y-GRID*0.4,89,'.',shade(mix(cc,'#ffffff',0.35),rs),'flowers'); } function rewardClump(ccx,ccy,rad,kind,rp,rs){ leafClump(ccx,ccy,rad,rs); const n=2+Math.floor(rs()*3); for(let k=0;k<n;k++){ const a=rs()*TAU, d=Math.sqrt(rs())*rad*0.7, bx=ccx+Math.cos(a)*d, byy=ccy+Math.sin(a)*d; if(kind==='berry') berry(bx,byy,rp,rs); else { for(let s=0;s<5;s++){ const sa=s/5*TAU; C.push(bx+Math.cos(sa)*GRID*1.1, byy+Math.sin(sa)*GRID*1.1, 90,'.', shade(FC(rs()<0.55?P.stamen:P.anther),rs),'flowers'); } C.push(bx,byy,91,'O',shade(FC(P.petal),rs),'flowers'); } } } function sprout(rs,rx){ if(1-smooth(0.05,0.20,m) <= 0.02) return; const g=smooth(0,0.18,m), hLen=(46+44*g)*S; let dir=UP+D.lean*1.2, pos=[rx,by-3]; const steps=Math.max(3,Math.ceil(hLen/GRID)); for(let i=1;i<=steps;i++){ const u=i/steps; dir+=Math.sin(u*2.6+D.lean*4)*0.04; pos=[pos[0]+Math.cos(dir)*GRID, pos[1]+Math.sin(dir)*GRID]; C.push(pos[0],pos[1],50,dirSym(Math.cos(dir),Math.sin(dir)),shade(bark,rs),'stem'); } for(const sd of [1,-1]){ let cd=UP+sd*0.95; let cp=[pos[0],pos[1]]; const cl=Math.max(2,Math.round((18+16*g)*S/GRID)); for(let i=1;i<=cl;i++){ cd+=angTo(cd,UP)*0.08; cp=[cp[0]+Math.cos(cd)*GRID, cp[1]+Math.sin(cd)*GRID]; const w=Math.sin(Math.PI*i/cl)*GRID*1.3, px=-Math.sin(cd),py=Math.cos(cd); C.push(cp[0],cp[1],36,dirSym(Math.cos(cd),Math.sin(cd)),greenOf(rs),'leaves'); C.push(cp[0]+px*w,cp[1]+py*w,35,'.',greenOf(rs),'leaves'); C.push(cp[0]-px*w,cp[1]-py*w,35,'.',greenOf(rs),'leaves'); } } if(g>0.4) leafClump(pos[0],pos[1], (5+5*g)*S, rs); } const growWin=0.12, maxOrder=D.maxOrder; let budget=8000; const chain=(1-Math.pow(D.taperLen,maxOrder+1))/(1-D.taperLen), baseRun=((by-400)/chain)*D.stature*(0.9+0.1*v)*S; function grow(x,y,head,vigor,order,birthM,pid,sideSeq){ if(budget<=0) return; const grown=clamp((m-birthM)/growWin,0,1); if(grown<=0) return; budget--; const rs=rng('gb', seed, pid); let run=baseRun*Math.pow(D.taperLen,order)*(0.82+0.36*rs())*grown; if(order===0) run*=0.92; const steps=Math.max(1,Math.ceil(run/GRID)), woodM=smooth(0.10,0.85,m), baseThick=(maxOrder-order)/maxOrder, bz=54-order; const f1=0.5+1.4*rs(), f2=1.1+2.2*rs(), ph1=rs()*6.28, ph2=rs()*6.28, gnarlA=D.gnarl*0.04*(1+order*0.12), bears=order>=1 ? D.leafiness : 0.3*D.leafiness; const canChild = grown>=1 && order<maxOrder; const nLat = order===0 ? 3+Math.floor(rs()*3) : clamp(Math.round(D.lateralN*(1-0.3*D.dominance)+gauss(rs)*0.9),0, order<2?3:2); const lo=order===0?0.18:0.30, hi=0.94, pts=[], sides=[]; let lastSide=rs()<0.5?1:-1; for(let c=0;c<nLat;c++){ pts.push(clamp(lerp(lo,hi,(c+0.5)/Math.max(nLat,1)) + (rs()*2-1)*(0.55/Math.max(nLat,1)), 0.1, 0.96)); if(order===0 || rs()<0.72) lastSide=-lastSide; sides.push(lastSide); } pts.sort((a,b)=>a-b); let dir=head, pos=[x,y], ci=0; for(let i=1;i<=steps;i++){ const u=i/steps; const photo=lerp(0.14,0.05,clamp(order/3,0,1))*Math.max(0,1-order/3.4), grav=0.06*D.droop*smooth(2,maxOrder,order)*(0.25+0.75*u); dir += angTo(dir,UP)*photo + angTo(dir,DOWN)*grav + gnarlA*(Math.sin(i*0.5*f1+ph1)+0.5*Math.sin(i*0.3*f2+ph2)); const exposure=clamp((by-pos[1])/520,0,1); dir += D.windDir*D.windStr*0.05*exposure*(0.35+0.65*order/maxOrder); const dx=Math.cos(dir), dy=Math.sin(dir); pos=[pos[0]+dx*GRID, pos[1]+dy*GRID]; const sym=dirSym(dx,dy); C.push(pos[0],pos[1], bz, sym, shade(bark,rs), 'stem'); const px=-dy, py=dx; const thick=clamp(Math.round(baseThick*baseThick*1.7*woodM*(0.7+0.3*v)*(1-0.5*u)*smooth(0.12,0.85,grown)),0,1); for(let wq=1;wq<=thick;wq++){ C.push(pos[0]+px*wq*GRID, pos[1]+py*wq*GRID, bz, sym, shade(barkDk,rs), 'stem'); C.push(pos[0]-px*wq*GRID, pos[1]-py*wq*GRID, bz, sym, shade(barkDk,rs), 'stem'); } if(bears>0 && u>0.14 && i%5===3 && rs()<bears*0.92){ const rad=(7+9*m)*(0.72+0.5*rs())*S; if(order>=2 && fruitF>0.2 && rs()<fruitF*D.fruitAb*0.34) rewardClump(pos[0],pos[1],rad,'berry',ripe,rs); else if(order>=2 && bloom>0.15 && rs()<bloom*D.flowerAb*0.24) rewardClump(pos[0],pos[1],rad,'flower',0,rs); else leafClump(pos[0],pos[1],rad,rs); } while(canChild && ci<pts.length && u>=pts[ci] && budget>0){ const side=sides[ci], div=(order===0?(0.5+0.5*D.reach):D.divergence*(0.7+0.7*rs())*D.reach); let la=dir + side*div + gauss(rs)*0.14; la += angTo(la,UP)*(order===0?0.10:0.16)*(1-D.reach*0.3); const cv=vigor*(order===0?(0.7+0.26*rs()):(0.5+0.4*(1-D.dominance))*(0.8+0.3*rs())); grow(pos[0],pos[1], la, cv, order+1, birthM+growWin, pid*7+ci+2, sideSeq+ci+1); ci++; } } if(canChild && budget>0){ const leadV=vigor*lerp(0.55,0.92,D.dominance); grow(pos[0],pos[1], dir+angTo(dir,UP)*(order<=1?0.30:0.18)+gauss(rs)*0.05, leadV, order+1, birthM+growWin, pid*7+1, sideSeq+1); } else if(grown>=1){ const rad=(8+11*m)*(0.8+0.3*rs())*S; if(fruitF>0.2 && rs()<fruitF*D.fruitAb*0.38) rewardClump(pos[0],pos[1],rad,'berry',ripe,rs); else if(bloom>0.18 && rs()<bloom*D.flowerAb*0.3) rewardClump(pos[0],pos[1],rad,'flower',0,rs); else leafClump(pos[0],pos[1],rad,rs); } else if(order>=1){ leafClump(pos[0],pos[1], (6+8*m)*S, rs); } } sprout(rng('sprout',seed), rootX); grow(rootX, by-3, UP+D.lean*0.6+D.windDir*D.windStr*0.06, 1, 0, 0, 1, 0); } // ===================== SETTING: Solo / Pair / Thicket / Hedge ===================== const roll=rng('setting',st.seed)(); const nC = roll<0.26?0 : roll<0.56?1 : roll<0.84?2 : 3+Math.floor(rng('fn',st.seed)()*2); const comps=[]; for(let i=0;i<nC;i++){ const cseed='0x'+(hashStr(st.seed+'|c'+i)>>>0).toString(16), rc=rng('comp',st.seed,i), side=(i%2===0?1:-1), f=i/Math.max(1,nC); comps.push({seed:cseed, artSeed:cseed, cx:baseX+side*(140+i*70+rc()*40), by:baseY-(2+i*8+rc()*16), S:lerp(0.66,0.46,f)*(0.9+0.2*rc()), fade:lerp(0.40,0.60,f), m, density:clamp(density+(rc()*2-1)*0.10,0.04,1), v, sen, browny, rewards, depth:f}); } comps.sort((a,b)=>b.depth-a.depth).forEach(paintGuava); paintGuava({seed:st.seed, artSeed:st.artSeed, cx:baseX, by:baseY, S:1.08, fade:0, m, density, v, sen, browny, rewards}); }, traits(st,env){ const roll=rng('setting',st.seed)(); const setting = roll<0.26?'Solo' : roll<0.56?'Pair' : roll<0.84?'Thicket' : 'Hedge'; const fruity=(env.season==='Autumn'||env.season==='Winter'); return [ ['Setting', setting], ['Reward', st.diedAt>0?'—':(env.m>=0.55 && env.rewards>0.3 ? (fruity?'Berries':'Flowers') : '—')], ['Form', st.diedAt>0?'Bare twigs':'Oval shrub'], ]; } })); })(); ; /* species/hairgrass.js */ /* hairgrass — species index 15. GENERATED from prototypes/antarctic-hair-grass.html by extract.js. Do not edit here — edit the prototype and re-run `npm run extract`. The prototype's mount() call is redirected to SeedlingRenderer.registerSpecies via the local shim. */ (function () { const mount = (spec) => SeedlingRenderer.registerSpecies(15, spec); /* Antarctic Hair Grass (Deschampsia antarctica) — the rarest Seedling. A wind-bleached cushion-tuft of fine arching blades (green->yellow->brown) growing from a crevice in angular gray ROCKS, dusted with SNOW, sending up delicate airy PANICLE seed-heads. The lone survivor — one of only two native flowering plants of Antarctica. */ mount({ title:'Antarctic Hair Grass', sub:'hairgrass v2', LIFESPAN:730, GRACE:150, DECAY:1/50, AGE_EQUIV:14, senFrom:0.86, note:'LEGENDARY · a lone wind-bleached tuft rooted in the rocks, dusted with snow, sending up airy golden seed-heads. One of only two native Antarctic flowering plants.', PAL:{ soil:'#8C8C88', accent:'#9DB36A', blade:'#5E8A46', bladeDk:'#3F6330', bladeLt:'#8FB45E', straw:'#C7B173', strawDk:'#9F844A', rock:'#7E868B', rockDk:'#474D51', rockLt:'#AEB4B8', crack:'#2B2F33', stalk:'#9DAE6F', spikeGreen:'#86A857', spikeStraw:'#C7A85E', awn:'#B7A36E', snow:'#F5F9FD', snowDk:'#D8E4EE' }, schemeExempt:['season','flowers'], // rocks + snow + spikelets read literal (solid), not scheme rings LW:{season:2.4, stem:1.1, leaves:1.0, flowers:1.7}, // bold season -> rocks read as SOLID stone stageName(st,env){ if(st.diedAt>0)return'Sealed'; const m=env.m; if(m<0.16)return'Sprig'; if(m<0.5)return'Tuft'; if(m<0.84)return'Cushion'; return'Seeding'; }, draw(st, C, env){ const {m,v,sen,season,browny,density,rewards,baseX,baseY}=env, P=this.PAL; const cx=baseX, cy=baseY-182; // tuft roots in the crevice on the central block const flowering = smooth(0.40,0.66,m) * (1-sen*0.85); const seasonGold = season==='Summer'?1 : season==='Autumn'?0.7 : season==='Spring'?0.5 : 0.3; const snowF = season==='Winter'?1 : season==='Autumn'?0.55 : season==='Spring'?0.4 : 0.16; // ---------- ROCKS: angular fractured granite blocks the tuft roots between ---------- function rockField(){ const rs=rng('rock',st.seed); // 1. jagged ANGULAR skyline: piecewise-linear control points (not a smooth curve) const nC=5+Math.floor(rs()*3), cpx=[], cpy=[]; for(let i=0;i<=nC;i++){ cpx.push(FRAME-4 + i*(W-2*FRAME+8)/nC); cpy.push((baseY-150) - rs()*72); } cpy[Math.round(nC/2)] = (baseY-176) - rs()*14; // central block sits a touch higher -> tuft nestles on it const topAt=x=>{ let i=0; while(i<nC-1 && cpx[i+1]<x) i++; const t=clamp((x-cpx[i])/((cpx[i+1]-cpx[i])||1),0,1); return lerp(cpy[i],cpy[i+1],t); }; // 2. Voronoi seeds -> distinct angular faceted stones, each its own tone const nS=12+Math.floor(rs()*6), seeds=[]; for(let i=0;i<nS;i++) seeds.push({x:FRAME+rs()*(W-2*FRAME), y:(baseY-115)+rs()*180, t:rs()}); for(let x=FRAME+6;x<=W-FRAME-6;x+=GRID){ const top=topAt(x); for(let y=top;y<=baseY+38;y+=GRID){ let d0=1e9,d1=1e9,b0=seeds[0],b1=seeds[0]; for(const s of seeds){ const dx=x-s.x, dy=(y-s.y)*1.25, d=dx*dx+dy*dy; if(d<d0){d1=d0;b1=b0;d0=d;b0=s;} else if(d<d1){d1=d;b1=s;} } const seam = (Math.sqrt(d1)-Math.sqrt(d0)) < GRID*0.
(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, 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;
ll-bleed"], ["Z", "zoom (drag to pan)"], ["Space", "play / pause"], ["← →", "scrub the timeline"], ["L", "loop"], ["R", "ping-pong"], ["A", "full life"], ["C", "seasons"], ["N", "night"], ["D", "day"], ["M", "mono"], ["I", "traits"], ["G", "grid"], ["?", "this help"]]; (function () { var h = '<div style="max-width:82%"><div style="color:#cfe6bd;margin-bottom:8px;letter-spacing:2px">CONTROLS</div>'; for (var i = 0; i < KEYS.length; i++) h += '<div><b style="display:inline-block;min-width:56px;color:#cfe6bd">' + KEYS[i][0] + "</b> " + KEYS[i][1] + "</div>"; helpEl.innerHTML = h + "</div>"; })(); // ── mode state ─────────────────────────────────────────────────────────── var M = { frame: "square", mood: "day", nightStyle: "moonlit", motion: "default", paused: false, fullbleed: false, zoom: 0, zx: 0.5, zy: 0.45, grid: false, info: false, help: false }; var DUR = 4200, LIFEDUR = 8000, YEARDUR = 7000; var curSt = mapped.st, displaySec = planted; function displaySecFor(clock) { if (M.motion === "loop") return planted + ((clock % DUR) / DUR) * growSpan; if (M.motion === "pingpong") { var p = (clock % (2 * DUR)) / DUR; return planted + (p < 1 ? p : 2 - p) * growSpan; } if (M.motion === "fulllife") return planted + smooth(clampN(clock / LIFEDUR, 0, 1)) * (deathSec - planted); if (M.motion === "seasons") return (nowSec || planted) + ((clock % YEARDUR) / YEARDUR) * 365 * DAY; return planted + smooth(clampN(clock / DUR, 0, 1)) * growSpan; // default: grow → hold } function build(sec) { var st = toEngineState(state, sec).st; if (M.motion === "fulllife" && sec >= deathSec) { st.diedAt = deathDays; st.deathCause = "FullTerm"; } return st; } function svgEl() { return target.querySelector("svg"); } function hardRender(sec) { setFrame(M.frame); curSt = build(sec); target.innerHTML = renderPlant(spec, curSt); applyView(); applyMood(); if (M.info) refreshInfo(); } function applyView() { var svg = svgEl(); if (!svg) return; var d = DIMS[M.frame], px = 46, py = 46, pw = d.W - 92, ph = d.H - 92, vb = [0, 0, d.W, d.H]; if (M.fullbleed) vb = [px, py, pw, ph]; if (M.zoom > 0) { var frac = [1, 0.52, 0.34][M.zoom] || 0.34, s = pw * frac, cx = px + M.zx * pw, cy = py + M.zy * ph; vb = [clampN(cx - s / 2, px, px + pw - s), clampN(cy - s / 2, py, py + ph - s), s, s]; } svg.setAttribute("viewBox", vb.join(" ")); svg.setAttribute("preserveAspectRatio", "xMidYMid meet"); } function applyMood() { var svg = svgEl(); if (!svg) return; var r = svg.getElementsByTagName("rect"), sheet = r[0], panel = r[1], bleed = M.fullbleed; if (M.mood === "night") { var gen = M.nightStyle === "generic", sky = gen ? { sheet: "#070b15", panel: "#0b1122" } : NIGHT_SKY; if (sheet) sheet.setAttribute("fill", bleed ? "transparent" : sky.sheet); if (panel) panel.setAttribute("fill", sky.panel); if (gen) { svg.style.filter = "saturate(.92) brightness(1.07)"; } else { svg.style.filter = ""; var e = svg.querySelectorAll("[id^=lyr-] *"); for (var i = 0; i < e.length; i++) { var f = e[i].getAttribute("fill"); if (f && f !== "none") e[i].setAttribute("fill", nightGrade(f)); var st2 = e[i].getAttribute("stroke"); if (st2 && st2 !== "none") e[i].setAttribute("stroke", nightGrade(st2)); } } } else if (M.mood === "mono") { if (sheet) sheet.setAttribute("fill", bleed ? "transparent" : "#ffffff"); svg.style.filter = "grayscale(1) contrast(1.06)"; } else { if (sheet) sheet.setAttribute("fill", bleed ? "transparent" : "#ffffff"); svg.style.filter = ""; } } function row(k, v) { return '<div><span style="color:#96a08c">' + k + "</span> &nbsp;<b>" + v + "</b></div>"; } function refreshInfo() { if (!revealed) { infoEl.innerHTML = row("Status", "Revealing Soon") + row("Seed", "Mystery"); return; } var q = Math.floor(curSt.now / 2) * 2, mat = Math.round(100 * (1 - Math.exp(-(q + 2 * (curSt.wateredEpochs || 0) * 7) / (0.7 * spec.LIFESPAN)))); infoEl.innerHTML = row("Species", esc(spec.title)) + (state.name ? row("Name", esc(state.name)) : "") + row("Age", (curSt.now | 0) + " d") + row("Entries", curSt.entryCount) + row("Watered", (curSt.wateredEpochs || 0) + " wk") + row("Season", seasonName(curSt.now)) + row("Maturity", mat + "%") + row("State", curSt.diedAt ? "sealed" : "living"); } // ── clock (default no-interaction path = grow→hold, then idle: no re-render) ── var t0 = null, lastPaint = 0, lastSec = null; function tick(ts) { if (t0 === null) t0 = ts; if (!M.paused) displaySec = displaySecFor(ts - t0); if (ts - lastPaint > 70 && displaySec !== lastSec) { lastPaint = ts; lastSec = displaySec; hardRender(displaySec); } requestAnimationFrame(tick); } // ── drag-to-pan when zoomed ── var _drag = null; target.addEventListener("pointerdown", function (e) { if (M.zoom > 0) { _drag = { x: e.clientX, y: e.clientY }; try { target.setPointerCapture(e.pointerId); } catch (_) {} } }); target.addEventListener("pointermove", function (e) { if (!_drag) return; var r = target.getBoundingClientRect(), frac = [1, 0.52, 0.34][M.zoom] || 0.34; M.zx = clampN(M.zx - (e.clientX - _drag.x) / r.width * frac, 0, 1); M.zy = clampN(M.zy - (e.clientY - _drag.y) / r.height * frac, 0, 1); _drag = { x: e.clientX, y: e.clientY }; applyView(); }); target.addEventListener("pointerup", function () { _drag = null; }); function scrub(dir) { M.paused = true; if (M.motion === "seasons") M.motion = "default"; var span = M.motion === "fulllife" ? deathSec - planted : growSpan; displaySec = clampN(displaySec + dir * span * 0.04, planted, planted + span * 1.05); lastSec = displaySec; hardRender(displaySec); } // ── keys (desktop; needs iframe focus on marketplaces; mobile = passive default) ── function onKey(e) { var k = (e.key || "").toLowerCase(); if (k === "arrowleft") { e.preventDefault(); scrub(-1); return; } if (k === "arrowright") { e.preventDefault(); scrub(1); return; } if (k === " ") { e.preventDefault(); M.paused = !M.paused; if (!M.paused) t0 = null; hintEl.style.opacity = "0"; return; } if (k === "?" || k === "/") { e.preventDefault(); M.help = !M.help; helpEl.style.display = M.help ? "flex" : "none"; return; } switch (k) { case "s": M.frame = "square"; M.motion = "default"; M.mood = "day"; M.fullbleed = false; M.zoom = 0; M.zx = 0.5; M.zy = 0.45; M.paused = false; t0 = null; hardRender(displaySec); break; case "p": M.frame = M.frame === "portrait" ? "square" : "portrait"; hardRender(displaySec); break; case "w": M.fullbleed = !M.fullbleed; applyView(); applyMood(); break; case "z": M.zoom = (M.zoom + 1) % 3; if (M.zoom === 0) { M.zx = 0.5; M.zy = 0.45; } applyView(); break; case "l": M.motion = M.motion === "loop" ? "default" : "loop"; M.paused = false; t0 = null; break; case "r": M.motion = M.motion === "pingpong" ? "default" : "pingpong"; M.paused = false; t0 = null; break; case "a": if (revealed) { M.motion = M.motion === "fulllife" ? "default" : "fulllife"; M.paused = false; t0 = null; } break; // teaser: full-life disabled case "c": M.motion = M.motion === "seasons" ? "default" : "seasons"; M.paused = false; t0 = null; break; case "d": M.mood = "day"; hardRender(displaySec); break; case "n": if (M.mood !== "night") { M.mood = "night"; M.nightStyle = "moonlit"; } else { M.nightStyle = M.nightStyle === "moonlit" ? "generic" : "moonlit"; } hardRender(displaySec); break; case "m": M.mood = M.mood === "mono" ? "day" : "mono"; hardRender(displaySec); break; case "g": M.grid = !M.grid; gridEl.style.display = M.grid ? "block" : "none"; break; case "i": M.info = !M.info; infoEl.style.display = M.info ? "block" : "none"; if (M.info) refreshInfo(); break; } hintEl.style.opacity = "0"; } window.addEventListener("keydown", onKey); setTimeout(function () { hintEl.style.opacity = "0"; }, 4200); requestAnimationFrame(tick); } // 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), 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
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 felt 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. // Pin to the viewport with position:fixed;inset:0 — using 100vw/100vh triggers a scrollbar // feedback loop (they ignore scrollbar width), which is why the page could be scrolled. el.style.cssText = "position:fixed;inset:0;overflow:hidden"; if (!document.getElementById("seedling-fit")) { var s = document.createElement("style"); s.id = "seedling-fit"; // Lock the page to the viewport so it NEVER scrolls (html/body height:100% + overflow:hidden); // the SVG fits ANY screen via preserveAspectRatio=meet; leftover space is TRANSPARENT (host bg). s.textContent = "html,body{margin:0;height:100%;overflow:hidden;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(); if (typeof setFrame === "function") setFrame("square"); // default frame = 1000×1000 square (OpenSea 1:1) var spec = specFor(Number(state.species)); var planted = Number(state.plantedAt) || 0; var nowSec = planted > 0 ? Math.floor(Date.now() / 1000) : 0; var mapped = toEngineState(state, nowSec || planted); if (!spec || typeof renderPlant !== "function") { placeholder(target, Number(state.species), mapped.st); return; } // Pre-reveal teaser (Bamboo): hide real traits + disable full-life. Absent flag ⇒ revealed. var revealed = !(state.revealed === false || state.revealed === "false" || state.revealed === 0 || state.revealed === "0"); var deathDays = mapped.deathDays, deathSec = planted + deathDays * DAY; var growSpan = Math.max(1, (nowSec || planted) - planted); // sprout → now (seconds) // ── helpers ───────────────────────────────────────────────────────────── function clampN(v, a, b) { return v < a ? a : v > b ? b : v; } function smooth(p) { return p * p * (3 - 2 * p); } function esc(s) { return ("" + s).replace(/[&<>"]/g, function (c) { return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]; }); } function seasonName(day) { var d = ((day % 365) + 365) % 365; return d < 60 ? "Winter" : d < 152 ? "Spring" : d < 244 ? "Summer" : d < 335 ? "Autumn" : "Winter"; } var DIMS = { square: { W: 1000, H: 1000, BX: 500, BY: 925 }, portrait: { W: 720, H: 980, BX: 360, BY: 905 } }; // one coherent moonlight (plant keeps its colour, cooled + a silver highlight on near-black) var NIGHT_SKY = { sheet: "#05060a", panel: "#0a0d13" }, MOON = [206, 216, 238], _gc = {}; function _h2r(h) { h = ("" + h).replace("#", ""); if (h.length === 3) h = h.charAt(0) + h.charAt(0) + h.charAt(1) + h.charAt(1) + h.charAt(2) + h.charAt(2); var n = parseInt(h, 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; } function _2h(r, g, b) { function c(v) { v = v < 0 ? 0 : v > 255 ? 255 : Math.round(v); var s = v.toString(16); return s.length < 2 ? "0" + s : s; } return "#" + c(r) + c(g) + c(b); } function _r2h(r, g, b) { r /= 255; g /= 255; b /= 255; var mx = Math.max(r, g, b), mn = Math.min(r, g, b), h = 0, s = 0, l = (mx + mn) / 2; if (mx !== mn) { var d = mx - mn; s = l > 0.5 ? d / (2 - mx - mn) : d / (mx + mn); h = mx === r ? (g - b) / d + (g < b ? 6 : 0) : mx === g ? (b - r) / d + 2 : (r - g) / d + 4; h *= 60; } return [h, s, l]; } function _h2rgb(h, s, l) { h = (((h % 360) + 360) % 360) / 360; if (s === 0) { var v = l * 255; return [v, v, v]; } var q = l < 0.5 ? l * (1 + s) : l + s - l * s, p = 2 * l - q; function f(t) { t = (t % 1 + 1) % 1; return t < 1 / 6 ? p + (q - p) * 6 * t : t < 1 / 2 ? q : t < 2 / 3 ? p + (q - p) * (2 / 3 - t) * 6 : p; } return [f(h + 1 / 3) * 255, f(h) * 255, f(h - 1 / 3) * 255]; } function _mix(a, b, t) { return a + (b - a) * t; } function nightGrade(hex) { if (_gc[hex] !== undefined) return _gc[hex]; var rgb; try { rgb = _h2r(hex); } catch (e) { return hex; } var a = _r2h(rgb[0], rgb[1], rgb[2]), h = a[0], s = a[1], l = a[2]; h = h + (((218 - h + 540) % 360) - 180) * 0.1; s = s * 0.88; l = clampN(l * 1.06 + 0.02, 0, 0.97); var c = _h2rgb(h, s, l), g = 0.3 * Math.max(0, (l - 0.58) / 0.42); var out = _2h(_mix(c[0], MOON[0], g), _mix(c[1], MOON[1], g), _mix(c[2], MOON[2], g)); _gc[hex] = out; return out; } // ── overlays (grid / traits / help / hint) ─────────────────────────────── function mk(css) { var d = document.createElement("div"); d.style.cssText = css; document.body.appendChild(d); return d; } var OV = "position:fixed;inset:0;pointer-events:none;z-index:2;"; var gridEl = mk(OV + "display:none;background-size:40px 40px;background-image:linear-gradient(rgba(150,165,120,.16) 1px,transparent 1px),linear-gradient(90deg,rgba(150,165,120,.16) 1px,transparent 1px)"); var infoEl = mk(OV + "display:none;padding:16px;font:13px/1.7 ui-monospace,Menlo,monospace;color:#e7ecdf;text-shadow:0 1px 3px rgba(0,0,0,.6)"); var helpEl = mk(OV + "display:none;align-items:center;justify-content:center;background:rgba(6,8,12,.82);color:#e8eaf0;font:13px/1.85 ui-monospace,Menlo,monospace"); var hintEl = mk(OV + "display:flex;align-items:flex-end;justify-content:center;padding-bottom:14px;color:rgba(120,132,108,.9);font:12px ui-monospace,Menlo,monospace;transition:opacity 1s"); hintEl.textContent = "click, then press ? for controls"; var KEYS = [["S", "reset to default"], ["P", "portrait ↔ square"], ["W", "fu
/* 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 ---------- // Runtime-switchable frame. Default is PORTRAIT (720×980); the on-chain boot calls setFrame('square') // for OpenSea's 1000×1000 1:1 fit, and the P key toggles back. ?sq=1 also forces square (poster studio). // Only the canvas constants change — plant DNA + growth math are identical (RNG is independent of W/H). let W=720, H=980, BASEX=360, BASEY=905; const GRID=5, GLY=GRID*0.95; function setFrame(mode){ if(mode==='square'){ W=1000; H=1000; BASEX=500; BASEY=925; } else { W=720; H=980; BASEX=360; BASEY=905; } } if (typeof location !== 'undefined' && new URLSearchParams(location.search).has('sq')) setFrame('square'); 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){
<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>
0xffd6563c…95fe·#25,495,102·0xa9c3cbe4…c4ef42
A living on-chain diary you grow as a plant. Write to water it; `neglect wilts it; every diary is` mortal. Fully on-chain, eternal`data:application/json;base64,A living on-chain diary you grow` as a plant. Write to water it; `@neglect wilts it; every diary is``r{"name":"Seedling #`h - Revealing Soon","description"`3","attributes":[{"trait_type":"Status","value":"Revealing Soon"}` a ,V[o","attributes":[`{"trait_type":"Species","value":`{"trait_type":"Rarity","value":"`{"trait_type":"Status","value":"`{"trait_type":"Entries","value":`{"trait_type":"Watered Weeks","v`A living on-chain diary you grow` as a plant. Write to water it; `@neglect wilts it; every diary is``,"diedAt":0,"entryCount":24,"wateredEpochs":20,"deathTime":a ,V[q,"revealed":false}`p<!doctype html><html><head><meta` <meta name="viewport" content="w`Qidth=device-width,initial-scale=`q<style>html,body{margin:0;height`:100%;background:transparent}</s`</script><script>SeedlingRendere)</script></body></html>RlGolden Wattle`RoBird of Paradise`RlChilean Guava`RmJapanese Maple`RsAntarctic Hair Grass``ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef`ghijklmnopqrstuvwxyz0123456789+/`?R` ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef`ghijklmnopqrstuvwxyz0123456789+/`?R` data:image/svg+xml;base64,<svg xmlns="http://www.w3.org/20` 00/svg" viewBox="0 0 600 600"><rect width="600" height="600" f`^<rect x="30" y="30" width="540" `height="540" fill="none" stroke=`"#3a4a3a" stroke-width="2"/><text x="60" y="110" font-family`="Georgia,serif" font-size="40" a<text x="60" y="160" font-family`="Georgia,serif" font-size="22" `'<text x="60" y="520" font-family`="monospace" font-size="20" fill`'<text x="60" y="550" font-family`="monospace" font-size="20" fill`'="#5a6a5a">watered weeks a ,V[p,"revealed":true}`x
<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
0xffd6563c…95fe·#25,465,641·0x8c0e67be…fae3a8
A living on-chain diary you grow as a plant. Write to water it; `neglect wilts it; every diary is` mortal. Fully on-chain, eternal`data:application/json;base64,A living on-chain diary you grow` as a plant. Write to water it; `@neglect wilts it; every diary is``r{"name":"Seedling #`h - Revealing Soon","description"`3","attributes":[{"trait_type":"Status","value":"Revealing Soon"}` a ,V[o","attributes":[`{"trait_type":"Species","value":`{"trait_type":"Rarity","value":"`{"trait_type":"Status","value":"`{"trait_type":"Entries","value":`{"trait_type":"Watered Weeks","v`A living on-chain diary you grow` as a plant. Write to water it; `@neglect wilts it; every diary is``,"diedAt":0,"entryCount":24,"wateredEpochs":20,"deathTime":<!doctype html><html><head><meta` <meta name="viewport" content="w`Qidth=device-width,initial-scale=`q<style>html,body{margin:0;height`:100%;background:transparent}</s`</script><script>SeedlingRendere)</script></body></html>RlGolden Wattle`RoBird of Paradise`RlChilean Guava`RmJapanese Maple`RsAntarctic Hair Grass``ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef`ghijklmnopqrstuvwxyz0123456789+/`?R` ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef`ghijklmnopqrstuvwxyz0123456789+/`?R` data:image/svg+xml;base64,<svg xmlns="http://www.w3.org/20` 00/svg" viewBox="0 0 600 600"><rect width="600" height="600" f`^<rect x="30" y="30" width="540" `height="540" fill="none" stroke=`"#3a4a3a" stroke-width="2"/><text x="60" y="110" font-family`="Georgia,serif" font-size="40" a<text x="60" y="160" font-family`="Georgia,serif" font-size="22" `'<text x="60" y="520" font-family`="monospace" font-size="20" fill`'<text x="60" y="550" font-family`="monospace" font-size="20" fill`'="#5a6a5a">watered weeks
<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>
4934', 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'], ]; } })); })();
return {tip:pos, dir}; } function drawBud(bcx,bcy,R,rs){ for(let gx=-R;gx<=R;gx+=GRID) for(let gy=-R*1.3;gy<=R*1.3;gy+=GRID){ if((gx*gx)/(R*R)+(gy*gy)/(R*1.3*R*1.3)>1) continue; const edge=(gx*gx)/(R*R)+(gy*gy)/(R*1.3*R*1.3)>0.62; C.push(bcx+gx,bcy+gy,72, dirSym(0.25,1), shade(FC(mix(P.bud, P.leafDark, edge?0.4:0.1)),rs),'leaves'); } for(let k=0;k<7;k++){ const a=ra()*TAU; C.push(bcx+Math.cos(a)*R*1.15*0.7, bcy+Math.sin(a)*R*1.4, 71, '/', shade(leafD,rs),'leaves'); } } function drawBloom(bcx,bcy,R,open,faceA,rs){ const Rp=R*(0.5+0.5*open), off=ra()*TAU; for(let k=0;k<4;k++){ const bk=off+k*TAU/4, ax=Math.cos(bk), ay=Math.sin(bk), px=-ay, py=ax, du=GRID/Math.max(Rp,1); for(let u=0.12;u<=1;u+=du){ const flare=Math.pow(Math.sin(Math.PI*Math.min(u,0.999)),0.72), rag=1 - D.petalRag*Math.abs(Math.sin(u*7+bk*3)), halfW=0.62*Rp*flare*rag, cxp=bcx+ax*u*Rp, cyp=bcy+ay*u*Rp; for(let w=-halfW;w<=halfW;w+=GRID){ const edge=Math.abs(w)>halfW-GRID*1.4; const col = u<0.30 ? FC(P.blotch) : (edge? FC(P.petalEdge) : (w*ax+0>0?petalC:petalWarm)); C.push(cxp+px*w, cyp+py*w, 68, dirSym(ax,ay), shade(mix(col,FC(P.stemDry),browny*0.6),rs), 'flowers'); } } } const Rc=R*0.26; for(let gx=-Rc;gx<=Rc;gx+=GRID) for(let gy=-Rc;gy<=Rc;gy+=GRID) if(Math.hypot(gx,gy)<Rc) C.push(bcx+gx,bcy+gy,90, ra()<0.4?'.':'+', shade(FC(P.center),rs),'flowers'); for(let k=0;k<13;k++){ const a=k/13*TAU; C.push(bcx+Math.cos(a)*R*0.36, bcy+Math.sin(a)*R*0.36, 91, '.', shade(FC(P.stamen),rs),'flowers'); } } function drawCapsule(bcx,bcy,R,rs){ const Hc=R*1.45, dry=Math.max(sen,browny*0.6), cc=mix(capC,FC(P.capsuleDry),dry); for(let gy=-Hc*0.5;gy<=Hc*0.55;gy+=GRID){ const u=(gy+Hc*0.5)/Hc, halfW=R*Math.sin(Math.PI*clamp(u*0.9+0.08,0,1)); for(let gx=-halfW;gx<=halfW;gx+=GRID){ const edge=Math.abs(gx)>halfW-GRID*1.2; C.push(bcx+gx,bcy+gy,86,'|', shade(edge?mix(cc,'#000000',0.18):cc,rs),'flowers'); } } for(let k=0;k<11;k++){ const a=Math.PI + (k/10-0.5)*Math.PI; C.push(bcx+Math.cos(a)*R*0.95, bcy-Hc*0.5+Math.sin(a)*R*0.95*0.32, 88, '-', shade(mix(cc,'#000000',0.22),rs),'flowers'); } for(let k=0;k<7;k++) C.push(bcx+(rs()*2-1)*R*0.7, bcy-Hc*0.36, 89, '.', shade(FC(P.pore),rs),'flowers'); } for(let i=0;i<nStem;i++){ const rs=rng('stem',seed,i), emerge=i===nStem-1?clamp(nStemF-(nStem-1),0.02,1):1, grow=smooth(0,1,emerge); let phase=clamp(0.12 + m*0.66 - i*0.12 + (rs()*2-1)*0.05, 0, 1.25); phase=clamp(phase + sen*0.6, 0, 1.3); const sx=rootX + (i-(nStem-1)/2)*D.spread*0.62*S + (rs()*2-1)*8, hLen=((axisH+50*S) + (160+300*m)*D.stature*(0.80+0.30*rs())*(0.85+0.15*v)*S)*grow; const seasonVig=0.6+0.4*seasonBloom, canBloom=rewards*(1-sen)*seasonVig; const isBud=phase<0.44, isBloom=!isBud && phase<0.74 && canBloom>0.3, nod=isBud ? clamp(1 - smooth(0.16,0.44,phase),0,1) : 0; const {tip}=growStem(sx, hLen, nod, rs), tx=tip[0], ty=tip[1]; if(isBud){ drawBud(tx,ty,(8+8*m)*emerge*(0.7+0.3*v)*S,rs); } else if(isBloom){ const open=smooth(0.44,0.62,phase)*(0.6+0.4*canBloom); drawBloom(tx,ty,(30+34*m)*emerge*(0.72+0.28*v)*(0.6+0.4*open)*S,open,UP,rs); } else { drawCapsule(tx,ty,(13+15*m)*emerge*(0.78+0.22*v)*S,rs); } } } // ===================== SETTING: Solo / Pair / Drift / Meadow / rare Field carpet ===================== const roll=rng('setting',st.seed)(); const nC = roll<0.16?0 : roll<0.44?1 : roll<0.72?2 : roll<0.91?3+Math.floor(rng('fn',st.seed)()*2) : 8+Math.floor(rng('fn',st.seed)()*6); const field=nC>=6, comps=[]; for(let i=0;i<nC;i++){ const cseed='0x'+(hashStr(st.seed+'|c'+i)>>>0).toString(16), rc=rng('comp',st.seed,i); let scale,off,raise,fade,depth; if(field){ const slot=(i+0.5)/nC, xr=clamp(slot+(rc()*2-1)*(0.6/nC),0.03,0.97); depth=0.15+0.85*rc(); scale=lerp(0.6,0.3,depth)*(0.9+0.2*rc()); raise=(depth*135+rc()*24); fade=lerp(0.42,0.70,depth); off=lerp(70,W-70,xr)-baseX; } else { const side=(i%2===0?1:-1), f=i/Math.max(1,nC); depth=f; scale=lerp(0.72,0.44,f)*(0.9+0.2*rc()); off=side*(86+i*50+rc()*46); raise=(36+i*24+rc()*32); fade=lerp(0.38,0.58,f); } comps.push({seed:cseed, artSeed:cseed, cx:baseX+off, by:baseY-raise, S:scale, fade, m, density:clamp(density+(rc()*2-1)*(field?0.18:0.12),0.04,1), v, sen, browny, rewards, depth}); } comps.sort((a,b)=>b.depth-a.depth).forEach(paintPoppy); paintPoppy({seed:st.seed, artSeed:st.artSeed, cx:baseX, by:baseY, S:1.0, fade:0, m, density, v, sen, browny, rewards}); }, traits(st,env){ const roll=rng('setting',st.seed)(); const setting = roll<0.16?'Solo' : roll<0.44?'Pair' : roll<0.72?'Drift' : roll<0.91?'Meadow':'Field'; const dd=rng('dna',st.seed); dd();dd();dd();dd();dd();dd();dd();dd(); // advance to windStr's draw (9th) const windStr=Math.pow(dd(),2.3); return [ ['Setting', setting], ['Stems', Math.max(1,Math.ceil(clamp(0.7+env.density*5.2,0.7,6.5)))], ['Wind', windStr<0.12?'Still':windStr<0.40?'Breezy':windStr<0.70?'Wind-bent':'Storm-bent'], ['Bloom', st.diedAt>0?'Capsules':(env.m>=0.58 && env.rewards>0.3?'Scarlet':'—')], ]; } })); })(); ; /* species/saguaro.js */ /* saguaro — species index 13. GENERATED from prototypes/saguaro-cactus.html by extract.js. Do not edit here — edit the prototype and re-run `npm run extract`. The prototype's mount() call is redirected to SeedlingRenderer.registerSpecies via the local shim. */ (function () { const mount = (spec) => SeedlingRenderer.registerSpecies(13, spec); /* Saguaro Cactus — Carnegiea gigantea. A single tall RIBBED COLUMN with a rounded dome top — the ribs are vertical pleats that accordion in/out with water; spines sit at areoles along the ribs. ARMS appear only after long growth (high maturity): each curves OUT then bends UP at the elbow. The reward is an apex CROWN of white funnel flowers with golden stamen centres → ruby fruit that splits to black seeds. Near-unkillable (CAM desert); drought just contracts the pleats. Death = a standing pale RIB SKELETON. Solitary, but framed by desert companions (ocotillo, prickly pear, agave). */ const SP = window.SEEDLING_SPECIES.find(x=>x.key==='saguaro'); mount(Object.assign(window.SEEDLING_toSpec(SP), { title:'Saguaro Cactus', sub:'carnegiea v2', senFrom:0.86, noSoil:true, note:'Legendary · slow ribbed column · arms only after long growth · apex crown of white flowers → red fruit · pleats accordion with drought · death = standing rib skeleton.', PAL:{ soil:'#C2A573', sand:'#D8C29A', pebble:'#B59A6E', bodyLt:'#88A56F', body:'#6E8B6E', bodyDk:'#47643F', rib:'#37502F', spine:'#E0D4AE', petal:'#FCFAF4', petalEdge:'#C2C4B2', petalShade:'#B7B4A8', stamen:'#EBC53E', stamenDk:'#C99A24', floBase:'#8A9A58', bud:'#7E9560', budTip:'#5E6B3E', fruit:'#A8332A', fruitDk:'#7E2018', fruitGreen:'#85974D', seed:'#241B14', pulp:'#C86A52', skelLt:'#C3B088', skelDk:'#8C7A5A', comp:'#9BA873', compDk:'#6E7A4E' }, stageName(st,env){ if(st.diedAt>0)return'Rib skeleton'; const m=env.m; if(m<0.12)return'Seedling'; if(m<0.5)return'Column'; if(m<0.72)return'Juvenile'; if(m<0.86)return'Armed'; return'Crowned'; }, draw(st, C, env){ const {m,v,sen,season,browny,density,rewards,reproOK,dormancy,dead,baseX,baseY}=env, P=this.PAL; const SPEC=this; // nested paintSaguaro() loses `this` const bgC = BACKDROPS[backdropFor(st.seed)]; const seasonF = (season==='Spring'||season==='Summer')?1:0.35; // ===================== one saguaro (repeated to form a random stand) ===================== function paintSaguaro(I){ const {cx, by, S, fade, m, v, sen, rewards, reproOK, browny, dead, pseed} = I; const FC = c => fade>0 ? mix(c, bgC, fade) : c; const drySkel = clamp((dead?1:0) + smooth(0.86,1.0,Math.max(sen, I.ageF||0))*0.9, 0, 1); // → standing rib skeleton const accordion = 0.78 + 0.22*v; // pleats contract when thirsty (recovers fully) const tone=(c,rs)=>shade(FC(mix(mix(c,'#b9a06a',browny*0.5), c===P.rib?P.skelDk:P.skelLt, drySkel)),rs); const COLS={lt:P.bodyLt, dk:P.bodyDk, rib:P.rib}; const isAutumn=season==='Autumn'; // Flower → fruit is sequenced purely by MATURITY (monotonic m), flicker-free: a freshly-reproductive // saguaro (m≈0.75) FLOWERS white; once well-established (m≥0.80) the same crown sets RED FRUIT. // Because m only ever RISES, the flower phase ALWAYS precedes the fruit — but fruit is NOT gated on // the in-app calendar season (a plant matured via entries, which barely advance the day, would // otherwise show flowers forever and never fruit). Autumn still ripens the fruit DEEPER red below. const crownMode = (m>=0.80) ? 'fruit' : 'flower'; const lush = clamp(rewards*(1-0.6*sen), 0, 1); // smooth amount control (no mode-flipping thresholds) const crownOn = reproOK && lush>0.10 && m>=0.75; // vanishes only under severe neglect / off-season // -- a ribbed segment: a cross-row of vertical pleats, 3-D shaded (centre lit, edges in shadow) -- function ribRow(px,py,dirX,dirY,hw,nRib,z,rs,areole){ const perpX=-dirY, perpY=dirX, gl=dirSym(dirX,dirY); for(let j=0;j<nRib;j++){ const th=-Math.PI/2 + Math.PI*(nRib>1?j/(nRib-1):0.5); const off=Math.sin(th)*hw, lit=Math.cos(th), edge=Math.abs(th)>Math.PI*0.42; const col = edge?COLS.rib : mix(COLS.dk, COLS.lt, lit*lit); C.push(px+perpX*off, py+perpY*off, z, gl, tone(col,rs),'stem'); if(areole && !edge && j%2===0 && drySkel<0.5) C.push(px+perpX*off, py+perpY*off, z+1,'+', shade(FC(mix(P.spine,bgC,0.1)),rs),'stem'); } } // -- per-seed DNA: each saguaro's silhouette is distinct (girth, height, lean + a full arm plan), // so seeds aren't all the predictable symmetric two-arm Y. Shared with traits() via armPlan(). -- const dna = SPEC.armPlan(st.seed, pseed); const lean = dna.lean, crook = dna.crook||0; // HEIGHT is driven by maturity only (monotonic — never shrinks); the accordion (thirst) contracts // only the GIRTH/ribs laterally, the real saguaro pleating. Mixing accordion into H made the whole // column pulse taller/shorter with vitality → "growth flicker". const H=(70+560*m)*S*dna.tall, colW=(14+21*m)*accordion*S*dna.stout; const steps=Math.max(2,Math.ceil(H/GRID)); let pos=[cx,by], dir=UP; const nRibT=clamp(Math.round(colW/2.5),7,17); let apex=[cx,by-H]; for(let s=1;s<=steps;s++){ const u=s/steps; dir += angTo(dir, UP + lean + crook*smooth(0.25,1,u))*0.05; // upper column curves → varied, non-ramrod silhouettes pos=[pos[0]+Math.cos(dir)*GRID, pos[1]+Math.sin(dir)*GRID]; const dome = u<0.86?1 : Math.sqrt(Math.max(0,1-Math.pow((u-0.86)/0.14,2))); const flare = u<0.06? 1+(0.06-u)*3 : 1; ribRow(pos[0],pos[1],Math.cos(dir),Math.sin(dir),colW*(1-0.10*u)*dome*flare,nRibT,50,rng('col',st.seed,pseed,s), s%4===0); if(u>0.9) apex=[pos[0],pos[1]]; } // -- ARMS: emerge progressively (each has its own emergeAt) from the seed's arm plan; out then UP at // the elbow. Asymmetric heights/sides/lengths + rare secondary sub-arms → varied candelabra. -- const armTips=[]; const nRibFor=aw=>clamp(Math.round(aw/2.0),6,12); function growArm(p0, d0, A, grow, rsKey, depth){ const armLen=(135+205*m)*S*A.lenF*clamp(grow,0.22,1)*(depth?0.6:1); const aw=colW*(0.50+0.14*A.curve)*(depth?0.7:1); let p=p0.slice(), d=d0, sub=null; const asteps=Math.max(3,Math.ceil(armLen/GRID)), nRibA=nRibFor(aw); for(let k=1;k<=asteps;k++){ const u=k/asteps; d += angTo(d, UP + sen*A.side*0.45)*(u<A.elbow?0.05:0.18*A.curve); p=[p[0]+Math.cos(d)*GRID, p[1]+Math.sin(d)*GRID]; const tipR=u>0.84?Math.sqrt(Math.max(0,1-Math.pow((u-0.84)/0.16,2))):1; ribRow(p[0],p[1], 0,1, aw*(1-0.10*u)*tipR, nRibA, 49, rng(rsKey,st.seed,pseed,depth,k), k%4===0); if(depth===0 && A.sub && !sub && u>=A.subU) sub=[p.slice(), d]; // mark a secondary-branch point } armTips.push(p); if(sub && grow>0.72){ const subA={side:A.side, lenF:A.lenF, elbow:0.32, curve:A.curve, spread:A.spread, sub:false, subU:1}; growArm(sub[0], sub[1]+A.side*0.85, subA, 0.85, rsKey+'s', 1); } } for(const A of dna.arms){ if(m < A.emergeAt) continue; const grow=clamp((m-A.emergeAt)/0.18, 0.22, 1); // arms keep extending after emerging const start=[cx + A.side*colW*0.55, by - H*A.atU], d0=UP + A.side*A.spread; growArm(start, d0, A, grow, 'ar'+(A.side>0?'R':'L'), 0); } // -- apex CROWN: white funnel flowers + green club buds → ruby fruit, at the trunk top & arm tips -- // a green, club-shaped, scaly bud (the pre-bloom stage in the reference) function drawBud(fx,fy,R,rs){ for(let gy=-R*1.45;gy<=R*0.45;gy+=GRID){ const u=(gy/(R*1.45)+1)/1.45; const w=R*0.58*Math.sin(Math.PI*clamp(u*0.72+0.16,0,1)); for(let gx=-w;gx<=w;gx+=GRID){ const edge=Math.abs(gx)>w-GRID; const scale=edge && (Math.round(gy/(GRID*2))%2===0); // suggest overlapping scales C.push(fx+gx,fy+gy,86,'.',shade(FC(scale||gy<-R*1.0?P.budTip:P.bud),rs),'flowers'); } } } // a white funnel flower, read cleanly at plant scale ("fried-egg"): a darker shadow halo rims the // bloom so the white separates from pale backdrops → white petal disc (thin sage rim) → gold centre. function drawFlower(fx,fy,R,openF,rs){ const RR=R*(0.98+0.18*openF), halo=RR*1.16; for(let gx=-halo;gx<=halo;gx+=GRID)for(let gy=-halo;gy<=halo*1.04;gy+=GRID){ if(gx*gx+gy*gy>halo*halo)continue; C.push(fx+gx,fy+gy,83,'.',shade(FC(P.petalShade),rs),'flowers'); } // soft drop-shadow ring const pr=RR; // white petal disc — white dominates for(let gx=-pr;gx<=pr;gx+=GRID)for(let gy=-pr;gy<=pr;gy+=GRID){ const d=Math.hypot(gx,gy); if(d>pr)continue; C.push(fx+gx,fy+gy,86,'.',shade(FC(d>pr-GRID*0.7?P.petalEdge:P.petal),rs),'flowers'); } // very thin rim const cr=RR*0.34; // smaller golden stamen centre for(let gx=-cr;gx<=cr;gx+=GRID)for(let gy=-cr;gy<=cr;gy+=GRID){ const rr=Math.hypot(gx,gy)/cr; if(rr>1)continue; C.push(fx+gx,fy+gy,90,'.',shade(FC(rr<0.34?P.stamenDk:rr>0.78?P.stamenDk:P.stamen),rs),'flowers'); } } // a ruby fruit: a clean oval (green→red→ruby) with a shadow halo, areole dots + brown floral // remnant; if open, a central red-flesh cup packed with black seeds (the iconic split saguaro fig). function drawFruit(fx,fy,R,ripe,open,rs,fid){ const base = ripe<0.5 ? mix(P.fruitGreen,P.fruit, ripe*2) : mix(P.fruit,P.fruitDk,(ripe-0.5)*2); const halo=R*1.06; // soft separation from neighbours for(let gx=-halo;gx<=halo;gx+=GRID)for(let gy=-halo*1.35;gy<=halo;gy+=GRID){ if((gx*gx)/(halo*halo)+(gy*gy)/(halo*halo*1.6)>1)continue; C.push(fx+gx,fy+gy,85,'.',shade(FC(P.petalShade),rs),'flowers'); } for(let gx=-R;gx<=R;gx+=GRID)for(let gy=-R*1.35;gy<=R*1.35;gy+=GRID){ const ny=gy/(R*1.35), rr=(gx*gx)/(R*R)+ny*ny; if(rr>1)continue; const edge=rr>0.76; C.push(fx+gx,fy+gy,86,'.',shade(FC(edge?mix(base,'#000',0.16):base),rs),'flowers'); } for(let i=0;i<8;i++){ const aa=rng('arl',st.seed,pseed,fid,i); // areole dots on skin (STABLE seed) const ax=(aa()*2-1)*R*0.7, ay=(aa()*2-1)*R*1.05; if((ax*ax)/(R*R)+(ay*ay)/(R*R*1.7)>0.7)continue; C.push(fx+ax,fy+ay,87,'.',shade(FC(P.spine),rs),'flowers'); } for(let gx=-R*0.3;gx<=R*0.3;gx+=GRID) // brown dried-flower remnant tuft C.push(fx+gx,fy-R*1.24,88,'.',shade(FC(P.skelDk),rs),'flowers'); if(open){ const ow=R*0.56, oh=R*0.92; // split: central red-flesh cup for(let gx=-ow;gx<=ow;gx+=GRID)for(let gy=-oh;gy<=oh*0.9;gy+=GRID){ if((gx*gx)/(ow*ow)+(gy*gy)/(oh*oh)>1)continue; C.push(fx+gx,fy+gy,89,'.',shade(FC(P.pulp),rs),'flowers'); } for(let i=0;i<16;i++){ const sr=rng('sd',st.seed,pseed,fid,i); // dense black seeds (STABLE seed) const sx=(sr()*2-1)*ow*0.78, sy=(sr()*2-1)*oh*0.82; if((sx*sx)/(ow*ow)+(sy*sy)/(oh*oh)>0.82)continue; C.push(fx+sx,fy+sy,90,'.',shade(FC(P.seed),sr),'flowers'); } } } // a coherent crown: one mode (buds OR flowers OR fruit, by season) so it doesn't muddle, spaced // in a small fan so the blooms read individually. // cid = a STABLE crown id (e.g. 'apex' / 'arm2'); all per-item randomness is seeded on it + // st.seed, NOT on the drifting pixel position, so buds/flowers/fruit don't flicker as the plant grows. function crown(tx,ty,scale,rs,cid){ if(drySkel>0.5 || !crownOn) return; const Rf=(15+7*scale)*S; if(crownMode==='fruit'){ // a row of distinct ruby figs, one or two split open const nf=Math.max(2,Math.round(1.4+1.4*scale)); for(let k=0;k<nf;k++){ const jr=rng('cj',st.seed,pseed,cid,k), off=k-(nf-1)/2; const fx=tx+off*Rf*1.34, fy=ty - Rf*0.35 - Math.abs(off)*Rf*0.30 + (jr()-0.5)*Rf*0.2; const ripe=clamp((isAutumn?0.92:0.66)+(jr()-0.5)*0.28,0,1), open=jr()<0.4; // stable per-fruit (no vitality) drawFruit(fx,fy,Rf*0.56, ripe, open, rs, cid+'_'+k); } return; } const n=Math.max(2,Math.round(1.6+1.8*scale)); // spring blooms in a small fan for(let k=0;k<n;k++){ const a=UP+(k-(n-1)/2)*0.82, rr=Rf*1.42; const fx=tx+Math.cos(a)*rr, fy=ty+Math.sin(a)*rr*0.5 - Rf*0.5; const jr=rng('cj',st.seed,pseed,cid,k); if(jr()<0.18) drawBud(fx,fy,Rf*0.6,rs); else drawFlower(fx,fy,Rf, 0.85, rs); // fixed openness → no size shimmer } } if(crownOn){ crown(apex[0],apex[1],1, rng('cr',st.seed,pseed), 'apex'); armTips.forEach((tp,i)=>{ if(rng('at',st.seed,pseed,i)()<0.7) crown(tp[0],tp[1],0.78,rng('cr',st.seed,pseed,i+1), 'arm'+i); }); } } // ===================== STAND: 1 → several saguaros from the seed ===================== const cr=rng('colony',st.seed), roll=cr(); const nC = roll<0.50?0 : roll<0.80?1 : roll<0.93?2 : 3+Math.floor(cr()*3); // companions: 0..5 const comps=[]; for(let k=0;k<nC;k++){ const r=rng('comp',st.seed,k); const side=k%2?1:-1, rank=Math.floor(k/2)+1, depth=clamp(0.20+0.70*r(),0,1); const S=lerp(0.62,0.30,depth), fade=lerp(0.24,0.62,depth); const cxP=clamp(baseX+side*(150+(rank-1)*135)+(r()*2-1)*36, FRAME+70, W-FRAME-70); const byP=baseY - depth*42; // far ones raised toward the horizon const mm=clamp(m + (r()*2-1)*0.30, 0.16, 1), vv=clamp(v*(0.6+0.5*r()), 0.2, 1), ssn=clamp(sen + (r()*2-1)*0.12, 0, 1); comps.push({cx:cxP, by:byP, S, fade, m:mm, v:vv, sen:ssn, ageF:env.ageF, rewards:clamp(rewards*(0.5+0.7*r()), 0, 1), reproOK:mm>=0.75, browny:clamp(Math.max(1-vv,ssn),0,1), dead:false, pseed:'c'+k, depth}); } comps.sort((a,b)=>b.depth-a.depth).forEach(paintSaguaro); // far (faint) → near paintSaguaro({cx:baseX, by:baseY, S:nC>0?0.92:1.0, fade:0, m, v, sen, ageF:env.ageF, rewards, reproOK, browny, dead, pseed:'hero'}); }, // per-seed silhouette plan — shared by draw() and traits() so the arm count stays in sync. // Returns {stout, tall, lean, arms:[{side, atU, emergeAt, lenF, elbow, spread, curve, sub, subU}]}. armPlan(seed, pseed){ const r=rng('dna',seed,pseed); // wider girth/height/lean ranges + a gentle per-seed column CROOK so even armless columns differ // (most slight, some clearly curved/leaning — the pow(r,2) skews bends LOW). const stout=0.76+0.62*r(), tall=0.74+0.52*r(), lean=(r()*2-1)*0.13; const crook=Math.pow(r(),2)*0.34*(r()<0.5?-1:1); const t=r(), maxA = t<0.08?0 : t<0.22?1 : t<0.46?2 : t<0.70?3 : t<0.88?4 : 5; // ~8% lone columns, rest 1–5 arms const arms=[]; let last=r()<0.5?-1:1; for(let i=0;i<maxA;i++){ const rr=rng('armp',seed,pseed,i); const side=rr()<0.66?-last:last; last=side; // emerge at REACHABLE maturity, STAGGERED by arm index → a progressive candelabra instead of a // pole that only sprouts arms at near-max m (the "always a single straight cactus" cause). arms.push({ side, atU:0.30+0.46*rr(), emergeAt:0.54+i*0.05+0.15*rr(), lenF:0.60+0.85*rr(), elbow:0.16+0.20*rr(), spread:0.85+0.70*rr(), curve:0.65+0.80*rr(), sub:rr()<0.22, subU:0.5+0.3*rr() }); } return { stout, tall, lean, crook, arms }; }, traits(st,env){ const m=env.m, dna=this.armPlan(st.seed,'hero'); const nArm=dna.arms.filter(a=>m>=a.emergeAt).length; const sub=dna.arms.some(a=>a.sub && m>=a.emergeAt+0.05); const cr=rng('colony',st.seed), roll=cr(); const total = 1 + (roll<0.50?0 : roll<0.80?1 : roll<0.93?2 : 3+Math.floor(cr()*3)); return [ ['Arms', nArm + (sub?' (branched)':'')], ['Crown', st.diedAt>0?'—' : (env.m>=0.75 && env.rewards>0.3 ? (env.season==='Summer'?'Fruit':'Flowers') : '—')], ['Stand', total===1?'Lone':total===2?'Pair':total<=3?'Cluster':'Forest'], ['Form', st.diedAt>0?'Rib skeleton':'Ribbed column'], ]; } })); })(); ; /* species/wattle.js */ /* wattle — species index 5. GENERATED from prototypes/golden-wattle.html by extract.js. Do not edit here — edit the prototype and re-run `npm run extract`. The prototype's mount() call is redirected to SeedlingRenderer.registerSpecies via the local shim. */ (function () { const mount = (spec) => SeedlingRenderer.registerSpecies(5, spec); /* Golden Wattle — Acacia. A low-forking broad DOMED little tree (recursive staggered-lateral frame) clothed in feathery BIPINNATE leaves (a rachis of paired pinnae of tiny leaflets). Reward: green buds → fuzzy GOLDEN POM-POM flower heads (late winter/spring, S-hemisphere) → flattened legume SEED PODS (green→brown) at senescence. Coherent wind. Grows in stands/groves. Ported to the shared core. */ const SP = window.SEEDLING_SPECIES.find(x=>x.key==='wattle'); mount(Object.assign(window.SEEDLING_toSpec(SP), { title:'Golden Wattle', sub:'acacia v2', senFrom:0.82, LW:{season:1.0, stem:1.2, leaves:1.0, flowers:1.4}, LS:{season:1.0, stem:1.0, leaves:0.85, flowers:0.8}, note:'Broad domed acacia · feathery bipinnate foliage · green buds → fuzzy golden pom-pom flowers (late winter/spring) → flat seed pods · wind · stands/groves.', PAL:{ soil:'#6f5128', dry:'#7a5a30', grey:'#9a8f74', bark:'#7A6A48', barkDk:'#56
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(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'); } }
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 = 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(
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; 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;
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,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'); }
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, 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);
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"; el.style.width = "100vw"; el.style.height = "100vh"; document.body.appendChild(el); } 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; } // Looping life-cycle replay: sprout → growth → (death) → rest → loop (art §7.5). var DURATION = 7000; // ms growth sweep, renderer-tunable var REST = 1800; var start = null; function frame(ts) { if (start === null) start = ts; var t = (ts - start) % (DURATION + REST); var p = Math.min(1, t / DURATION); // 0..1 across life var anim = toEngineState(state, planted + p * mapped.deathDays * DAY).st; target.innerHTML = renderPlant(spec, anim); requestAnimationFrame(frame); } 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), 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*
/* 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
0xffd6563c…95fe·#25,427,466·0x343a01a9…82b61e
# Seedlings — Encryption Spec (client-side; open-source + etched on-chain) The exact, frozen recipe for reading/writing a Seedling diary. Per contract spec §6 this is **open-sourced from day one AND etched on-chain (SSTORE2)** alongside the renderer, so any community app can read/write diaries and they stay decryptable even if every frontend (including ours) disappears. **The chain carries the instructions for reading itself.** > ✅ **LOCKED (2026-06-20) — single launch scheme, post-quantum from day one.** Algorithm > choices below are final, implemented in the **reference client** (`../reference-client/`, > audited `@noble/*` primitives), and pinned with **test vectors** > (`../reference-client/test/test-vectors.json`) the client reproduces byte-for-byte in CI, > and **etched on-chain via SSTORE2** (`EncryptionSpec.sol`, with keccak integrity anchors). > Nothing here runs on-chain — the chain only ever sees ciphertext + opaque wrapped-key blobs. > > **Post-quantum:** the only quantum-exposed surface is the asymmetric **key wrap** (§4), and > its blobs are public-forever on-chain (worst case for "harvest-now-decrypt-later"). The wrap > is therefore a **hybrid KEM = X25519 + ML-KEM-768 (FIPS 203)** whose wrap key binds BOTH > shared secrets → secure as long as **either** primitive holds. Entry encryption (AES-256-GCM, > §3) and `eraKey` derivation (HKDF, §1) are symmetric and were already post-quantum-safe → > **unchanged**. There is no separate "v1/v2": this is the one scheme, with a `SCHEME` version > byte reserved for any future migration. > > **Locked byte conventions (the parts a second implementer must not guess):** > - **`SIG`** = the **65-byte** EIP-191 `personal_sign` signature (`r‖s‖v`, `v ∈ {27,28}`); > `seedBits = keccak256(SIG)`. > - **HKDF** = HKDF-SHA256 (extract+expand). String **salts/info are their UTF-8 bytes**; > `cloneAddress` (as HKDF `info` for the era key, and as AAD) is the **20 raw address bytes** > — never a hex string, never checksummed. Output length is 32 except the ML-KEM keygen seed > (length **64**). > - **AES-256-GCM**: 12-byte IV (`nonce`/`wn`), 16-byte tag appended after the ciphertext. > - **X25519** (classical half): the 32-byte HKDF output is the scalar (clamped internally); > `xPub = X25519_base(xPriv)`. > - **ML-KEM-768** (PQ half, FIPS 203): keygen is **seeded** (64-byte HKDF output) so keys are > stateless/re-derivable; sizes pk **1184**, ct **1088**, shared secret **32**, encaps msg **32**. > - **`SCHEME` = `0x01`** is the leading version byte of both the published `pubKey` and the > wrap `blob` (`0x01` = hybrid X25519+ML-KEM-768). > - All `‖` are byte concatenations. --- ## 0. Model — one key PER OWNER ERA (not one key for the whole diary) The diary's life is a sequence of **ownership eras** (owner A, then B, then C…), delimited by the deed's `Transfer` events. **Each owner encrypts the entries they write under their own `eraKey`.** This is the privacy backbone: - An owner can **always re-read their own era's entries** (they can re-derive their `eraKey` from their wallet signature) — even long after they've sold. *Their* words. - An owner **cannot read another era's entries** unless that era's owner **hands them the key**. In particular a past owner can **never** read a *future* owner's entries — they never hold a later `eraKey`. Selling truly hands over the future; the past stays the seller's to share or keep. - The **current owner** reads/writes their own era, owns the token + art, and may **receive past eras' keys** from past owners to read the diary's history. (A naïve "one key for the whole diary" would let a past owner read the buyer's *future* entries — a leak. Per-era keys close that.) **Trust:** sharing the past is **opt-in and open-ended** (revelation-on-sale, §6) — any past owner may hand their `eraKey` to the current owner **at any time**, or never. The contract provides the rail; it never coerces and never sees plaintext or private keys. **Phishing (documented):** the derivation signature **is** the key. Anyone who obtains it reads that era. Mitigations: the signed message is a self-describing warning (below), it is **never reused** for login/verification, and open-source clients are the audit surface. ## 1. Key derivation (client-side, deterministic, per era) Each user derives, from one wallet signature: - a **hybrid encryption keypair `EK`** (X25519 + ML-KEM-768) — its public half (`pubKey`) is published on-chain so others can wrap keys to them; - their **`eraKey`** for a given diary — the AES key for entries they write in it. ``` SIG = personal_sign(account, MESSAGE) // EIP-191 seedBits = keccak256(SIG) // 32 bytes // classical half (X25519) xPriv = HKDF-SHA256(seedBits, salt="seedling/ek/v1", info="x25519", 32) // clamp per X25519 xPub = X25519_base(xPriv) // post-quantum half (ML-KEM-768, FIPS 203) — seeded keygen ⇒ stateless kemSeed = HKDF-SHA256(seedBits, salt="seedling/ek/v1", info="ml-kem-768", 64) (kemPub, kemPriv) = ML-KEM-768.KeyGen(kemSeed) // published, version-tagged hybrid public key (this is what registerEncryptionPubKey stores) pubKey = 0x01 ‖ xPub(32) ‖ kemPub(1184) // 1217 bytes // per-diary symmetric era key (UNCHANGED — already PQ-safe) eraKey = HKDF-SHA256(seedBits, salt="seedling/era/v1", info=cloneAddress, 32) // AES-256, per diary ``` `MESSAGE` (exact bytes, **never** reused elsewhere — frozen at v1): ``` Seedling diary key — v1 This signature IS the encryption key to your diary. Anyone who gets this signature can read everything you write here, forever, and you cannot undo it. Only sign this in the official or an open-source Seedling client you trust. Never sign it to "verify", "log in", or "connect". ``` All of these are **stateless** — re-deriving from the same account (incl. ML-KEM's seeded keygen) always yields the same keys; no key storage needed. `registerEncryptionPubKey(pubKey)` publishes the version-tagged hybrid `pubKey` (once per user, updatable). ## 2. Lifecycle - **Mint / first write:** owner A derives `eraKey_A` and encrypts their entries with it. (No key blob needs committing for *their own* reading — they re-derive it. Blobs are only for sharing with *others*.) - **Sale (A → B):** on `KeyHandoffRequested`, A's client may **hybrid-wrap `eraKey_A` to B's published `pubKey`** and `commitWrappedKey(B, blob)` — now and/or anytime later, A's choice. B writes their own entries under `eraKey_B`. - **Read (owner X):** the client lists entries from the logs, maps each to its era via the `Transfer` timeline (entry timestamp ↔ which owner held the deed then), and decrypts each with the matching `eraKey` **if X holds it** (their own, re-derived; others', via a `KeyCommitted` blob X can unwrap). Eras X lacks keys for render as "still sealed by a previous keeper." - **Open-ended history:** any past owner may, at any future time, `commitWrappedKey` their era key to whoever owns the diary then — retroactively unlocking that era for them. ## 3. Entry encryption (per entry, in the log payload) ``` nonce = 12 random bytes (per entry) ct, tag = AES-256-GCM-Encrypt(key=eraKey_of_writer, iv=nonce, aad=AAD, plaintext=UTF8(text)) payload = 0x01 ‖ nonce(12) ‖ ct ‖ tag(16) // 0x01 = encrypted format/version ``` - `AAD = abi.encodePacked(cloneAddress)` (binds ciphertext to the diary). - `contentHash` stored with the log = `keccak256(payload)` → feeds the on-chain rolling `artSeed` integrity chain (§3.2). Integrity is era-independent (covers all entries). - **Public entries** (`isPublic = true`): `payload = 0x00 ‖ UTF8(text)` (plaintext, never encrypted), same 495-byte cap. The `0x00`/`0x01` version byte tells a reader whether to decrypt; public entries are always readable by anyone, no key needed. - **495-byte budget** is the *payload* length (~≤466 plaintext after the 29-byte AEAD overhead). The contract caps `payload.length ≤ 495`. ## 4. Key wrapping (`eraKey` → a recipient) — hybrid KEM (X25519 + ML-KEM-768) The wrap encapsulates to **both** halves of the recipient's `pubKey` and binds **both** shared secrets into the wrap key, so it stays secure if **either** X25519 or ML-KEM-768 holds. The combiner is an explicit application-layer HKDF (not a library "hybrid" module), so any second implementer can reproduce it byte-for-byte. ``` xPub, kemPub = parse(recipient pubKey) // strip 0x01, split 32 ‖ 1184 eph_priv, eph_pub = X25519 keypair (random, per wrap) ss_x = X25519(eph_priv, xPub) // classical shared secret (32) ct_kem, ss_kem = ML-KEM-768.Encapsulate(kemPub) // PQ ciphertext (1088) + shared secret (32) wrapKey = HKDF-SHA256( ikm = ss_x ‖ ss_kem, // secure if EITHER holds salt = eph_pub ‖ ct_kem ‖ xPub ‖ kemPub, // bind both cts + both recipient keys info = "seedling/wrap/v1", 32 ) wn = 12 random bytes wct, wtag = AES-256-GCM-Encrypt(key=wrapKey, iv=wn, plaintext=eraKey) blob = 0x01 ‖ eph_pub(32) ‖ ct_kem(1088) ‖ wn(12) ‖ wct(32) ‖ wtag(16) // 1181 bytes ``` Unwrap reverses it with the recipient's hybrid private key: `ss_x = X25519(xPriv, eph_pub)`, `ss_kem = ML-KEM-768.Decapsulate(ct_kem, kemPriv)`, recompute `wrapKey`, AES-GCM-decrypt. The sender never learns the recipient's private key; the recipient never reveals it. The blob **self-validates** — a wrong recipient yields the wrong `wrapKey` (ML-KEM's implicit rejection gives a junk `ss_kem` rather than an error), so the AES-GCM tag check simply fails. An open-ended/permissionless commit is therefore safe; the on-chain gate only requires the committer to be a past/current owner (to limit spam). ## 5. On-chain rail (what the contracts provide — contract spec §6) | Piece | Where | Purpose | |---|---|---| | `registerEncryptionPubKey(bytes pubKey)` | mother | publish a user's 1217-byte hybrid `pubKey` (event `EncryptionPubKey`) | | `wasEverOwner(tokenId, who)` | mother | gate: only a past/current owner may hand off a key | | `KeyHandoffRequested(tokenId, from, to)` | clone (on transfer) | prompt the seller's client to re-wrap | | `commitWrappedKey(forOwner, blob)` | clone | carry a wrapped `eraKey`; **blob persisted to SSTORE2** (state, not log) → returns + emits the pointer in `KeyCommitted(tokenId, forOwner, pointer, ts)`; open-ended | | `keyBlobCount/keyBlobPointer/keyBlob(forOwner[,i])` | clone | read committed blobs back from state (eternal availability) | The contracts **never validate** the crypto and never see plaintext/private keys — they carry opaque blobs + signals. All correctness lives in the reference client + this spec. Writing/growth is gated to the **current owner only**, separately — a key-handoff permission never grants write access. **Durability (why the blob lives in state, not a log):** a handoff may target a *future* owner who only appears years later (open-ended history). For **encrypted** data the wrapped key **is** availability — there is no plaintext archive to fall back on, so if the only copy were an event it could be lost to EIP-4444 history expiry and that era would become permanently unreadable. `commitWrappedKey` therefore writes the blob to **SSTORE2** (every node keeps it forever, like the etched entries §3.4 and this spec itself); the `KeyCommitted` event carries only the **pointer** as a cheap index. A reader fetches a blob with `keyBlob(forOwner, i)` (or `eth_getCode(pointer)[1:]`) and unwraps it (§4) entirely client-side. The blob byte format (§4) is unchanged. ## 6. Test vectors (✅ pinned) Pinned in **`../reference-client/test/test-vectors.json`** and reproduced byte-for-byte by the reference client (`npm test` in `reference-client/`). Covered: - **`SIG → EK / eraKey` derivation** — two users (A, B) from fixed test private keys, each signing the exact `MESSAGE`; pins `signature`, `seedBits`, `xPriv`, `xPub`, `kemPub`, the version-tagged `pubKey`, and `eraKey`. - **Entry encrypt/decrypt** with a known nonce → the full `0x01 ‖ nonce ‖ ct ‖ tag` payload + its `contentHash` (= `keccak256(payload)`), round-tripped. - **Public-entry no-op** → `0x00 ‖ UTF8(text)`, decoded with no key. - **`eraKey` hybrid wrap/unwrap** with fixed ephemeral key + ML-KEM encaps msg + nonce → the 1181-byte blob, and the revelation-on-sale flow (B unwraps A's era key, then reads A's entry). - **Negatives** (the privacy guarantees): wrong era key fails; AAD binds to the diary (a different `cloneAddress` fails); a stranger (wrong hybrid keypair) can't unwrap a blob wrapped to B; the 495-byte payload cap is enforced. Regenerate with `npm run vectors` (deterministic). ✅ **Etched on-chain:** `EncryptionSpec.sol` SSTORE2-writes this doc **and** the vectors as contract code, with keccak integrity anchors, deployed by `Deploy.s.sol` — the chain carries the instructions for reading itself. { "_spec": "ENCRYPTION_SPEC.md — hybrid X25519 + ML-KEM-768 (single launch scheme)", "_warning": "TEST-ONLY keys/nonces. Never use for a real diary.", "message": "Seedling diary key — v1\n\nThis signature IS the encryption key to your diary. Anyone who gets this\nsignature can read everything you write here, forever, and you cannot undo\nit. Only sign this in the official or an open-source Seedling client you\ntrust. Never sign it to \"verify\", \"log in\", or \"connect\".", "cloneAddress": "0x00000000000000000000000000000000000000c1", "derivation": { "description": "SIG (65-byte EIP-191 personal_sign of `message`) → hybrid keys; pubKey = 0x01 ‖ xPub(32) ‖ kemPub(1184)", "userA": { "privateKey": "0x1111111111111111111111111111111111111111111111111111111111111111", "signature": "0x30ee322abba50eafce6e0ee26170af995e2c6eaa9b75f4fe3abddfb8483ff78a5723863fc0a50a6f70a1a06b47594c1df30a1e40a2713fe494b122e10cb5093c1b", "seedBits": "0xc40b58775af20626866a56543e825a1a7cc1260fbe6c41d42a4129e3e0520aa8", "xPriv": "0xae0a5cf044a52600e8586223625d441d9481357595af70f19712506c83441ac1", "xPub": "0x9257a06ecb98575fc619a6eea8301bba0dcfa86e3c1cd5128702f02409821773", "kemPub": "0x8ba58ce7b29dd1562fbcc575ca969a13386d84b33cce8b6c6b9523b941c5be73938c502dfefac1949708644014d34260325379919c2f3ec1ada0f2ccdfc700ffe29a16ca2b08849eb4f14931d49590eb06f4b32bc040abd9032e25168707528425f9421a4bb1bb510546c9030130915280af212657732a24863b0b727a1b50ab320f7846abbb2e231b9c2818afb6ac1b8243b37759ae6c9a80efc12ae919a90a772f63f7addfc45b3865b1c6335c2a9b23e7782118b46c46786de18ab77a448f8b7a5e2ce3b27176050110547e19450dd27086f94f5ff887eac201ba394978ca8216b59dd7d0517ab57bd4039956572043a887a9ea4a1d2ba0d76bbfee268b098256150927dc9234982c431e2767bd3a1962da2b1f545f650440c8ca02a5336570c46855c296e9fb2cf51a5a13503c882279d0b968d8b1773a38615687addb78950f37617a635e71c299d44b52515a93246cb249d76f9bc010bd47b59bba1091b946834a8cebd986582061ebf21d73b69b8667b9ad2a0cd2a9530b9475f16b17e4f0c3d7e19bb0f547743cba7b543243d44e4b361a9386c36d47b999a177020215f10472791cb5073194e8345ae4874cdf172482f1625125b3d720cc1dfa9cc84a8905c723f4c6c531a83a4a8061fb0a2faa9032640bc1beb17cca50ca7cda1851766f27699140d6478b64c4d732aca47bb97aa0b23fdaa90abc131eb86a05c1a76cc71216c286fc92c68cdb8b1a875b06dac19c8ab229d07a6506b77442b047117d89a48e7b460f622765b9c464051a283b6449ebbb2eebe2aefdf802cd9abc70a092154362f69353b7f12afd7411ea84a981894a52559b5f0cc2a8f7974932bd002018a231c8e5b471edd14cf7610aee1a91cbeaaee4742db17678ce0a358b34b1565446e63894e5b17638a9a4443c2dc10764b056a929e489b7684acc83cfc3b8ba906a3879983658078739d22c7db69373629d1c13cb5e4c72ea699b9029bdfa6a2ace5baac8e646a93590488cbbc568325fd01bb03b21a245cf4f1637580b9fa021851d97b02a636e0f589adbba0b7669c59347b973d14d4b13a7dbe8116c840e260192e35373fe3094dfb553e869bf8b0876182985c7002ce7b21e3900be31ab04ea2004a8b5b4b152a2fcb28893046100902bc2002f19195e3ffb2c0d1b0347b961f757a94b3b7687362512f8873a2c1482395f5e485aa81c64f767b66c7ac86baca48a07ceb8a4188a6932199c7c96440104812c4b1026f11871de64ce7062435d4302d566ad9bdc8885e27227a4742a586266fa1d1ab66059f07f3002cfe71363f25c4f7a59bcebe025717c01a51079053baa6aeb2ccbc0bbc9866079934720161d736a6f931a90ad3a885a5ca935810a8fc2902cf57bf3f5344227c3074a4530768dda9bc8d387317159810577b5cbb384ab426840507d7302b7e3a21252573999fa941f9183c1a6b313d640f30601b55024999c6f5b5c9312dab9a6a85ac6b57f9ad4511618564c22b122167db4785ab011124d90b6fd956b8b453cc7221163a14cf94a919d63c2bed8606f9c38c734253d217b8d1cb89bb3b1b2e3cc87a34f4d2b5da935bc0db3bd40d1920e09bbbc03938f5b519d8383a167a200758284caff2161859ebfdca871be22e8a792f668b13473373bc9377a698abc", "pubKey": "0x019257a06ecb98575fc619a6eea8301bba0dcfa86e3c1cd5128702f024098217738ba58ce7b29dd1562fbcc575ca969a13386d84b33cce8b6c6b9523b941c5be73938c502dfefac1949708644014d34260325379919c2f3ec1ada0f2ccdfc700ffe29a16ca2b08849eb4f14931d49590eb06f4b32bc040abd9032e25168707528425f9421a4bb1bb510546c9030130915280af212657732a24863b0b727a1b50ab320f7846abbb2e231b9c2818afb6ac1b8243b37759ae6c9a80efc12ae919a90a772f63f7addfc45b3865b1c6335c2a9b23e7782118b46c46786de18ab77a448f8b7a5e2ce3b27176050110547e19450dd27086f94f5ff887eac201ba394978ca8216b59dd7d0517ab57bd4039956572043a887a9ea4a1d2ba0d76bbfee268b098256150927dc9234982c431e2767bd3a1962da2b1f545f650440c8ca02a5336570c46855c296e9fb2cf51a5a13503c882279d0b968d8b1773a38615687addb78950f37617a635e71c299d44b52515a93246cb249d76f9bc010bd47b59bba1091b946834a8cebd986582061ebf21d73b69b8667b9ad2a0cd2a9530b9475f16b17e4f0c3d7e19bb0f547743cba7b543243d44e4b361a9386c36d47b999a177020215f10472791cb5073194e8345ae4874cdf172482f1625125b3d720cc1dfa9cc84a8905c723f4c6c531a83a4a8061fb0a2faa9032640bc1beb17cca50ca7cda1851766f27699140d6478b64c4d732aca47bb97aa0b23fdaa90abc131eb86a05c1a76cc71216c286fc92c68cdb8b1a875b06dac19c8ab229d07a6506b77442b047117d89a48e7b460f622765b9c464051a283b6449ebbb2eebe2aefdf802cd9abc70a092154362f69353b7f12afd7411ea84a981894a52559b5f0cc2a8f7974932bd002018a231c8e5b471edd14cf7610aee1a91cbeaaee4742db17678ce0a358b34b1565446e63894e5b17638a9a4443c2dc10764b056a929e489b7684acc83cfc3b8ba906a3879983658078739d22c7db69373629d1c13cb5e4c72ea699b9029bdfa6a2ace5baac8e646a93590488cbbc568325fd01bb03b21a245cf4f1637580b9fa021851d97b02a636e0f589adbba0b7669c59347b973d14d4b13a7dbe8116c840e260192e35373fe3094dfb553e869bf8b0876182985c7002ce7b21e3900be31ab04ea2004a8b5b4b152a2fcb28893046100902bc2002f19195e3ffb2c0d1b0347b961f757a94b3b7687362512f8873a2c1482395f5e485aa81c64f767b66c7ac86baca48a07ceb8a4188a6932199c7c96440104812c4b1026f11871de64ce7062435d4302d566ad9bdc8885e27227a4742a586266fa1d1ab66059f07f3002cfe71363f25c4f7a59bcebe025717c01a51079053baa6aeb2ccbc0bbc9866079934720161d736a6f931a90ad3a885a5ca935810a8fc2902cf57bf3f5344227c3074a4530768dda9bc8d387317159810577b5cbb384ab426840507d7302b7e3a21252573999fa941f9183c1a6b313d640f30601b55024999c6f5b5c9312dab9a6a85ac6b57f9ad4511618564c22b122167db4785ab011124d90b6fd956b8b453cc7221163a14cf94a919d63c2bed8606f9c38c734253d217b8d1cb89bb3b1b2e3cc87a34f4d2b5da935bc0db3bd40d1920e09bbbc03938f5b519d8383a167a200758284caff2161859ebfdca871be22e8a792f668b13473373bc9377a698abc", "eraKey": "0xe3c8fbf51f290cc62603ab8fef847a08cd172fd9cd57179435ba9671ab5a0c58" }, "userB": { "privateKey": "0x2222222222222222222222222222222222222222222222222222222222222222", "signature": "0x1e665df522a3c5d47200898f16f3bd0d79ade3abcc1bd53a74fbfba49c493bb554dc49145f404aa70d5bac41891bfd6a3c93160e6535e95e45ac73b9cf86c1f31c", "seedBits": "0x048212e4f4056c25a07544fcabd39792e520f56762d40849fc9c1d6ab4ac8736", "xPriv": "0x51a15d67d3adf63f7883ed0c64bf121179546a3c982f5921ea1e6a4dd8dc6f2d", "xPub": "0x92ccc54d8600c7f19cf6e3364673d0e8e0cb78895b17599d1cf9edd0c25c453b", "kemPub": "0xadd23581526d80911ae78b9f5df0521d0590046a73af152701cab464b30a0d91b4e0f778cf726cdffc73f6d1599a39732945b21c74648cfb28c5e62de25ac2fb83545ff3736a5068ca632a7d28820ce14c8ad1ae6b78011af55533713d74a9b9bc68af113076791c791c0076abb4217c218881e5b0cda69896765f00b131b5e22f5b33321c2b7057b85ce10266206b372758b987fc01dfc1770cdcbaf7b4944ea4458a2a615a3a842ab9983c830bdf198481437adc727a378abe56824338c3c72ea9a87fda3addfc583d6380b0b8354603647fd9215b2aa746b64659a82dee4a8dd91b0d56581424da22e2b18ff9382fef652356ca1203c7799a3206e53671f8b90df6823cf2a46dffa0aa8af6c4ffe67406abaaa8495ffcd1680b28248c92501ee370cb9bb69c9915ad7c6cf7733d1e14c95c11187fa69208497a61b5c0ea0c8d778a9362b1bcd7141fb18a3055ba8b26fc88c104240a467b8866a099d090accbb4c2a279b04b1f7201123f63ab10a0c0bc029f6308722b96761b0c079e6435fe0bc5c3226f23100ee8f6b620d21d2b8c0882c56f38001862b05a4686be627279bb997c3bd96ec2f042db60bf7fc427e7511454d49c60d23173dcc375a75bdf8397f1e24df4f28ea35ca840a2a7b0240b42c00114a600ffb00ebd2b204f148bbb31549459684fbc5ef0ca638d80b581027ff5ca9d4d66cb1467507dd53ecae303dae3ce244511c166b1131ba2129c8a50aa20b0284553fbc1acd47e9279052c36478397bb51fb01d52a27c0a3a88df728c709cbcd79b6fecc818d5bb9303a666986c4e7831f1be2a673b91b6e345c375ab7e1837abe25946543948c4233b926628856b1013093143431165b889cec63132946248b7fa54473682b9e7979632527879ec8b686bbc7381b48e7c3bccf2c74af067d86fb302a02cd3001063ba146bcfccb45272e7fc8715d8b5969f7bd5f4901f17ac611742e8159002299bed4db830e902a817a3cd3051ea38a6a08394593783eb580586e3b468bc51b47e712be1c36621908330c59cc3158045739efb787d3cabb56944ec7d82484ac9d992b44451c66ba7542eff11b0c8cbf72d95394755f33f8bdf6b79dfa27aa7e567fc7f93e4ba6cfc47a248c8aa2bf667f0b42b9f7d08acb0cadb914417c225290150c307c357707bf6b2880ebac9561604a33f782ebb4872802834b47abeed192b1290edf2b1c0de6618728473f6b9cb5eb4ffff168371935e37cbb2143998dea8347f05f264a20b7c576778635364cb03c7a86b8a95dc19083dd715111ba0ff8c7c39b3948811c09580bafa332af1354a857e85694359140fa1eaf3224ef65adc42109eef4a7f099c5521c1491b991c075429ae44ca569378cb33caa463afa117515562dedd2c2e782a4c08306e313460df251998bc1656883fe3852b9c2af7b1a3cc6d3032a8ab6956a42c2c6431172686b4337551cad2540af1df93560f74e211804d4a5ce64046f5d24857101c8a25c121724c6a3332034645d230a21303252831c3855a590586a2a22eb2519460b0e885f925b3dfca7cb023963084c3284b2a45a57341f4a1d8a6a69b6ba0d908a138d83942cfa1003fa745ac72d3f1bac5724bfa4fcee88f83d8cc089a44ebbd2d9813c68e11a129a3cc3683156ef152b7bf9ec", "pubKey": "0x0192ccc54d8600c7f19cf6e3364673d0e8e0cb78895b17599d1cf9edd0c25c453badd23581526d80911ae78b9f5df0521d0590046a73af152701cab464b30a0d91b4e0f778cf726cdffc73f6d1599a39732945b21c74648cfb28c5e62de25ac2fb83545ff3736a5068ca632a7d28820ce14c8ad1ae6b78011af55533713d74a9b9bc68af113076791c791c0076abb4217c218881e5b0cda69896765f00b131b5e22f5b33321c2b7057b85ce10266206b372758b987fc01dfc1770cdcbaf7b4944ea4458a2a615a3a842ab9983c830bdf198481437adc727a378abe56824338c3c72ea9a87fda3addfc583d6380b0b8354603647fd9215b2aa746b64659a82dee4a8dd91b0d56581424da22e2b18ff9382fef652356ca1203c7799a3206e53671f8b90df6823cf2a46dffa0aa8af6c4ffe67406abaaa8495ffcd1680b28248c92501ee370cb9bb69c9915ad7c6cf7733d1e14c95c11187fa69208497a61b5c0ea0c8d778a9362b1bcd7141fb18a3055ba8b26fc88c104240a467b8866a099d090accbb4c2a279b04b1f7201123f63ab10a0c0bc029f6308722b96761b0c079e6435fe0bc5c3226f23100ee8f6b620d21d2b8c0882c56f38001862b05a4686be627279bb997c3bd96ec2f042db60bf7fc427e7511454d49c60d23173dcc375a75bdf8397f1e24df4f28ea35ca840a2a7b0240b42c00114a600ffb00ebd2b204f148bbb31549459684fbc5ef0ca638d80b581027ff5ca9d4d66cb1467507dd53ecae303dae3ce244511c166b1131ba2129c8a50aa20b0284553fbc1acd47e9279052c36478397bb51fb01d52a27c0a3a88df728c709cbcd79b6fecc818d5bb9303a666986c4e7831f1be2a673b91b6e345c375ab7e1837abe25946543948c4233b926628856b1013093143431165b889cec63132946248b7fa54473682b9e7979632527879ec8b686bbc7381b48e7c3bccf2c74af067d86fb302a02cd3001063ba146bcfccb45272e7fc8715d8b5969f7bd5f4901f17ac611742e8159002299bed4db830e902a817a3cd3051ea38a6a08394593783eb580586e3b468bc51b47e712be1c36621908330c59cc3158045739efb787d3cabb56944ec7d82484ac9d992b44451c66ba7542eff11b0c8cbf72d95394755f33f8bdf6b79dfa27aa7e567fc7f93e4ba6cfc47a248c8aa2bf667f0b42b9f7d08acb0cadb914417c225290150c307c357707bf6b2880ebac9561604a33f782ebb4872802834b47abeed192b1290edf2b1c0de6618728473f6b9cb5eb4ffff168371935e37cbb2143998dea8347f05f264a20b7c576778635364cb03c7a86b8a95dc19083dd715111ba0ff8c7c39b3948811c09580bafa332af1354a857e85694359140fa1eaf3224ef65adc42109eef4a7f099c5521c1491b991c075429ae44ca569378cb33caa463afa117515562dedd2c2e782a4c08306e313460df251998bc1656883fe3852b9c2af7b1a3cc6d3032a8ab6956a42c2c6431172686b4337551cad2540af1df93560f74e211804d4a5ce64046f5d24857101c8a25c121724c6a3332034645d230a21303252831c3855a590586a2a22eb2519460b0e885f925b3dfca7cb023963084c3284b2a45a57341f4a1d8a6a69b6ba0d908a138d83942cfa1003fa745ac72d3f1bac5724bfa4fcee88f83d8cc089a44ebbd2d9813c68e11a129a3cc3683156ef152b7bf9ec" } }, "entryEncrypt": { "description": "AES-256-GCM private entry, fixed nonce → 0x01 ‖ nonce(12) ‖ ct ‖ tag(16)", "eraKey": "0xe3c8fbf51f290cc62603ab8fef847a08cd172fd9cd57179435ba9671ab5a0c58", "nonce": "0xa0a1a2a3a4a5a6a7a8a9aaab", "plaintext": "Day one. Planted in the dark — let's see what you become. 🌱", "payload": "0x01a0a1a2a3a4a5a6a7a8a9aaabced18fcacb8a9ba999e6d1a9667db881e96ae446be0c9f92cab1bedede33eecc07ec01356fb921be2e593939a96a90aeaa4beb8bd5dd8e4fb68c753a6176c0ce6a02cab8d23c639844952a2a32db837e", "contentHash": "0x55f71a3d186225ba6dafa99534d936f130a6ff972ed6d1b03801defe36f58c7a" }, "publicEntry": { "description": "Plaintext public entry → 0x00 ‖ UTF8(text) (no key, no encryption)", "plaintext": "A public note anyone can read.", "payload": "0x0041207075626c6963206e6f746520616e796f6e652063616e20726561642e", "contentHash": "0x00a8f6044f6757a0fa5d433f317503f1126da9cc0111eba4c5146157dd4ff9a5" }, "keyWrap": { "description": "Hybrid (X25519 + ML-KEM-768) wrap of A's eraKey to B → 0x01 ‖ eph_pub(32) ‖ ct_kem(1088) ‖ wn(12) ‖ wct(32) ‖ wtag(16)", "recipientPubKey": "0x0192ccc54d8600c7f19cf6e3364673d0e8e0cb78895b17599d1cf9edd0c25c453badd23581526d80911ae78b9f5df0521d0590046a73af152701cab464b30a0d91b4e0f778cf726cdffc73f6d1599a39732945b21c74648cfb28c5e62de25ac2fb83545ff3736a5068ca632a7d28820ce14c8ad1ae6b78011af55533713d74a9b9bc68af113076791c791c0076abb4217c218881e5b0cda69896765f00b131b5e22f5b33321c2b7057b85ce10266206b372758b987fc01dfc1770cdcbaf7b4944ea4458a2a615a3a842ab9983c830bdf198481437adc727a378abe56824338c3c72ea9a87fda3addfc583d6380b0b8354603647fd9215b2aa746b64659a82dee4a8dd91b0d56581424da22e2b18ff9382fef652356ca1203c7799a3206e53671f8b90df6823cf2a46dffa0aa8af6c4ffe67406abaaa8495ffcd1680b28248c92501ee370cb9bb69c9915ad7c6cf7733d1e14c95c11187fa69208497a61b5c0ea0c8d778a9362b1bcd7141fb18a3055ba8b26fc88c104240a467b8866a099d090accbb4c2a279b04b1f7201123f63ab10a0c0bc029f6308722b96761b0c079e6435fe0bc5c3226f23100ee8f6b620d21d2b8c0882c56f38001862b05a4686be627279bb997c3bd96ec2f042db60bf7fc427e7511454d49c60d23173dcc375a75bdf8397f1e24df4f28ea35ca840a2a7b0240b42c00114a600ffb00ebd2b204f148bbb31549459684fbc5ef0ca638d80b581027ff5ca9d4d66cb1467507dd53ecae303dae3ce244511c166b1131ba2129c8a50aa20b0284553fbc1acd47e9279052c36478397bb51fb01d52a27c0a3a88df728c709cbcd79b6fecc818d5bb9303a666986c4e7831f1be2a673b91b6e345c375ab7e1837abe25946543948c4233b926628856b1013093143431165b889cec63132946248b7fa54473682b9e7979632527879ec8b686bbc7381b48e7c3bccf2c74af067d86fb302a02cd3001063ba146bcfccb45272e7fc8715d8b5969f7bd5f4901f17ac611742e8159002299bed4db830e902a817a3cd3051ea38a6a08394593783eb580586e3b468bc51b47e712be1c36621908330c59cc3158045739efb787d3cabb56944ec7d82484ac9d992b44451c66ba7542eff11b0c8cbf72d95394755f33f8bdf6b79dfa27aa7e567fc7f93e4ba6cfc47a248c8aa2bf667f0b42b9f7d08acb0cadb914417c225290150c307c357707bf6b2880ebac9561604a33f782ebb4872802834b47abeed192b1290edf2b1c0de6618728473f6b9cb5eb4ffff168371935e37cbb2143998dea8347f05f264a20b7c576778635364cb03c7a86b8a95dc19083dd715111ba0ff8c7c39b3948811c09580bafa332af1354a857e85694359140fa1eaf3224ef65adc42109eef4a7f099c5521c1491b991c075429ae44ca569378cb33caa463afa117515562dedd2c2e782a4c08306e313460df251998bc1656883fe3852b9c2af7b1a3cc6d3032a8ab6956a42c2c6431172686b4337551cad2540af1df93560f74e211804d4a5ce64046f5d24857101c8a25c121724c6a3332034645d230a21303252831c3855a590586a2a22eb2519460b0e885f925b3dfca7cb023963084c3284b2a45a57341f4a1d8a6a69b6ba0d908a138d83942cfa1003fa745ac72d3f1bac5724bfa4fcee88f83d8cc089a44ebbd2d9813c68e11a129a3cc3683156ef152b7bf9ec", "ephPriv": "0x3333333333333333333333333333333333333333333333333333333333333333", "kemMsg": "0x4444444444444444444444444444444444444444444444444444444444444444", "wn": "0xb0b1b2b3b4b5b6b7b8b9babb", "wrappedEraKey": "0xe3c8fbf51f290cc62603ab8fef847a08cd172fd9cd57179435ba9671ab5a0c58", "blob": "0x017b0d47d93427f8311160781c7c733fd89f88970aef490d8aa0ee19a4cb8a1b14f046e419c8ce8e95f1cd51f6ff1acb9e1b20aa1dcf75831c3c91fa60318fb4ddfb1f9ab7f9aae04b930d0afeffe5a7921b06bdbe647322cd3867b56f09a8cbb08cf8f1a3295c4299920e35d5499c5344b6deea1fd8f2671607b8868a6822e72a5418703e033dc5da0776a3e96a967bb7b256e67d99737a7b8ea6cc0eb2d6fa3e216c3f3f7d538017221a2c1fe6b5d98fe974c5401a122d02dc9d36cdfa1a96efbaa0c9457aa66979684b604d49cd0d469ce1d42859447deb183ad38c317d634de17b3958384527ec83d3d025eab183906d963ece7e511fb0f3d48567918800f56868d1c9a19b687dda7f862e3a967f6feeede6d2dac134e5d7263b35fdd22923d9a0eab18443d6e5b5978094892c40942483bb55ce9af12dfcd4b6af222694bdcb5dc8a6532aec93fe4305794a6fd47e60c7216459cf30b4cf26a17c659fe0deb6a41eda7558ca42f0b05805b48f4551b34a667fc5ee4bb53cc66b15c24813a15cc1a9cdbcbd580eea3507140a23466f97dd1ebbf1ebc17e0947ec68d17d85a6b5a18e580a533238f517093d1415de2ebf17e2546b99ce3bf6c04f4d3736dc40e5325165ead40eda8b353cfff45081b85d5ba245d81e222c28abc9a31a19898ddd87fb6349a91d67c7f233a7c27f8a1d28e253db1408ddfb2f44fda3db9769ccac1915fd87efd2dd1daf021a401c6d87dd369d7605f35d184bfb794f6e165eeafd2822e2d3998ad93df92fdf40183b2a0dfc343d36f84167b387bb6380b24f8cdcd820d2deb9a56bbaa5344a80881b139c89560d921aa499bc505a77ce7aab31d51f1c55c402e765582bf9f8715584e67b66d96e1cba9d25d24efdf0120052f6c9d4df9c518923e9654f7fbe3a20837ec7961e1db6fa78bef9ec3e177cb385ee84a09663017eaf0b2c1b885047fdddae65f6e41f22430de0cf9723f54956428d0d85a86cec6bc077b80d1db247652d40c67f7bcf58734f141d1d35af96fcfbbd025960ed8895eb3297ff7dd385c8eb710418e1cefa442ab872a2be02b1db5fb52ef99061793863b8f8bd2cbd47cea2ff1d03720de4acb01045622fdd52788056ed4fcaace1e70e8074961584fee328404b5d2c0be2ffba93cf1a43be04cd6bb742447bd7054a66146c25ba8b4371034979eb1c3d021f3d57854a9a8bb37b8de28b943c3817f945fe52b3f5d6321c50e7aae7c0b1affd591109c358ba5ed299efe9832d763be93b37f076b8416249a3018bafebfa2e7c1a77b3a8ce1d7a7704507624419bd9e1f709aae501290e7a819c26ad1d19a61f4bcfdc552241e0afa84130780de070f76f6b8c583891ee0e2eafb784031f9fe7c4afb71eaddcf1616918bcdb26a82b4c4b99da62e5f7dff95da43a409d226268eb3aceb951d8d06f42c01fea5ec7e59b8ec26b80a9f44796b053254aaed7b8a0506932bc2d1b7102cb35b71f71bb0bb56727d0f72052c86f69b234750f9ede59be7222c5f8cbdc5d79dc9301b9ae61366c2e8add8b7af4b1113c73975a8f15c86b433d83892216b2d0acb0b1b2b3b4b5b6b7b8b9babbbeb037d5ca6e8b227461bf573bdee9efde925ced5447fea8b2c14255f0bb595bde639ee24507ffb831d196597b420dd9" } }
0xffd6563c…95fe·#25,427,464·0xa3b0f20b…037d5a
A living on-chain diary you grow as a plant. Write to water it; `neglect wilts it; every diary is` mortal. Fully on-chain, eternal`data:application/json;base64,A living on-chain diary you grow` as a plant. Write to water it; `@neglect wilts it; every diary is``r{"name":"Seedling #`h - Revealing Soon","description"`3","attributes":[{"trait_type":"Status","value":"Revealing Soon"}` a ,V[o","attributes":[`{"trait_type":"Species","value":`{"trait_type":"Rarity","value":"`{"trait_type":"Status","value":"`{"trait_type":"Entries","value":`{"trait_type":"Watered Weeks","v`A living on-chain diary you grow` as a plant. Write to water it; `@neglect wilts it; every diary is``<!doctype html><html><head><meta` <meta name="viewport" content="w`Qidth=device-width,initial-scale=`q<style>html,body{margin:0;height`:100%;background:#f3efe6}</style`</script><script>SeedlingRendere)</script></body></html>RlGolden Wattle`RoBird of Paradise`RlChilean Guava`RmJapanese Maple`RsAntarctic Hair Grass``ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef`ghijklmnopqrstuvwxyz0123456789+/`?R` ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef`ghijklmnopqrstuvwxyz0123456789+/`?R` data:image/svg+xml;base64,<svg xmlns="http://www.w3.org/20` 00/svg" viewBox="0 0 600 600"><rect width="600" height="600" f`^<rect x="30" y="30" width="540" `height="540" fill="none" stroke=`"#3a4a3a" stroke-width="2"/><text x="60" y="110" font-family`="Georgia,serif" font-size="40" a<text x="60" y="160" font-family`="Georgia,serif" font-size="22" `'<text x="60" y="520" font-family`="monospace" font-size="20" fill`'<text x="60" y="550" font-family`="monospace" font-size="20" fill`'="#5a6a5a">watered weeks