This commit is contained in:
2026-11-05 09:57:11 -05:00
parent d34388bf2c
commit 89e392cc2e
81 changed files with 12410 additions and 1135 deletions
+228 -8
View File
@@ -160,7 +160,8 @@ function planks(d,vertical=false,species='oak') {
pine:['straw','strawDark','strawLight'],willow:['willow','willowShade','willowGlow'],
alder:['clay','barkLight','wood'],beech:['woodLight','clay','cream'],aspen:['birchGlow','wood','cream'],
rowan:['clay','earthLight','woodLight'],hazel:['wood','earthLight','strawLight'],
spruce:['strawLight','wood','cream']}[species];
spruce:['strawLight','wood','cream'],crab_apple:['wood','earthLight','cream'],
wild_cherry:['clay','bark','peach'],field_maple:['strawLight','barkLight','cream']}[species];
const put=(x,y,c)=>d.dot(vertical?y:x,vertical?x:y,c);
for(let y=0;y<32;y++) for(let x=0;x<32;x++) {
const board=Math.floor(y/8), grain=Math.sin(x*.31+board*2+y*1.6)+.45*Math.sin(x*.73-y);
@@ -174,8 +175,19 @@ function bark(d,species='oak') {
pine:['barkLight','bark','wood'],willow:['humus','barkDark','loamGlow'],
alder:['loamShade','humus','earthLight'],beech:['mineralShade','slateShade','mineralGlow'],
aspen:['birchShade','willowShade','birchGlow'],rowan:['earthLight','bark','birchShade'],
hazel:['barkLight','loamShade','willow'],spruce:['earth','humus','rust']}[species];
hazel:['barkLight','loamShade','willow'],spruce:['earth','humus','rust'],
crab_apple:['earth','barkDark','willow'],wild_cherry:['rust','bark','woodLight'],
field_maple:['barkLight','earthDark','straw']}[species];
d.rect(0,0,32,32,base);
if(['crab_apple','wild_cherry','field_maple'].includes(species)) {
for(const [i,[x,y]] of GRAINS.entries()) {
if(species==='wild_cherry') d.line(x,y,x+4+i%3,y,dark).line(x+1,y-1,x+4,y-1,light);
else if(species==='field_maple') d.poly([[x,y-3],[x+2,y],[x,y+5],[x-2,y+1]],dark)
.line(x,y-2,x,y+2,light);
else d.line(x,y,x+1,y+4,dark).line(x+2,y+1,x+2,y+3,light);
}
return;
}
if(['beech','aspen','rowan','hazel','spruce'].includes(species)) {
// Smooth beech, diamond-scarred aspen, lenticellate rowan/hazel and scaly spruce.
for(const [i,[x,y]] of GRAINS.entries()) {
@@ -213,7 +225,8 @@ function endgrain(d,species='oak') {
pine:['straw','barkLight','strawLight'],willow:['willow','willowShade','willowGlow'],
alder:['clay','barkLight','wood'],beech:['woodLight','clay','cream'],aspen:['birchGlow','wood','cream'],
rowan:['clay','earthLight','woodLight'],hazel:['wood','earthLight','strawLight'],
spruce:['strawLight','wood','cream']}[species];
spruce:['strawLight','wood','cream'],crab_apple:['wood','earthLight','cream'],
wild_cherry:['clay','bark','peach'],field_maple:['strawLight','barkLight','cream']}[species];
const phase=SPECIES_NAMES.indexOf(species)*.7;
for(let y=0;y<32;y++) for(let x=0;x<32;x++) {
const a=Math.atan2(y-16,x-14), r=Math.hypot((x-14)*1.01,(y-16)*.96);
@@ -392,7 +405,8 @@ function plant(d,name) {
export const SEASONAL_PLANT_NAMES = Object.freeze(['leaves_spring','leaves_autumn','leaves_winter',
'meadow_grass_dormant','fern_dormant','wildflowers_dormant','spring_greens','spring_greens_dormant',
'autumn_mushrooms','autumn_mushrooms_dormant']);
export const SPECIES_NAMES = Object.freeze(['oak','birch','pine','willow','alder','beech','aspen','rowan','hazel','spruce']);
export const SPECIES_NAMES = Object.freeze(['oak','birch','pine','willow','alder','beech','aspen','rowan','hazel','spruce',
'crab_apple','wild_cherry','field_maple']);
export const EVERGREEN_NAMES = Object.freeze(['pine','spruce']);
export const ROCK_NAMES = Object.freeze(['granite','basalt','limestone','slate','sandstone']);
export const UNDERSTORY_NAMES = Object.freeze(['clover','nettle','heather','bluebell','sedge','bulrush','moss','lichen']);
@@ -444,6 +458,16 @@ function speciesLeaves(d,species,season='summer') {
} else if(species==='hazel') {
d.poly([[x,y+4],[x-5,y],[x-4,y-4],[x-1,y-4],[x,y-2],[x+3,y-4],[x+5,y],[x,y+4]],dark)
.line(x,y+2,x-2,y-2,base,2).line(x,y,x+3,y-2,light);
} else if(species==='field_maple') {
d.poly([[x,y+4],[x-2,y+1],[x-5,y],[x-3,y-2],[x-4,y-5],[x-1,y-3],
[x,y-7],[x+2,y-3],[x+5,y-4],[x+3,y-1],[x+5,y+1],[x+1,y+2]],dark)
.line(x,y+2,x,y-4,base).line(x,y,x+3,y-2,light).line(x,y,x-3,y-2,base);
} else if(species==='crab_apple'||species==='wild_cherry') {
const cherry=species==='wild_cherry';
d.poly([[x,y+4],[x-4,y],[x-3,y-3],[x+(cherry?2:0),y-6],[x+4,y-2],[x+3,y+1]],dark)
.line(x,y+2,x+1,y-3,base,2).line(x,y,x-2,y-2,light);
if(spring&&i%2===0) flower(d,x+3,y-3,cherry?'cream':'bud',5,1.5);
if(autumn&&i===1) d.oval(x+3,y+3,1,1,cherry?'wine':'ochre');
} else {
d.poly([[x-4,y+2],[x-3,y-2],[x,y-5],[x+4,y-2],[x+3,y+2],[x,y+4]],dark)
.line(x-2,y+1,x+1,y-3,base,2).line(x-1,y,x+2,y-1,light);
@@ -595,8 +619,8 @@ function habitatTile(d,name) {
function worldTile(name) {
const d=new Drawing();
const speciesMatch=name.match(/^(oak|birch|pine|willow|alder|beech|aspen|rowan|hazel|spruce)_(log_top|log_side|planks|leaves)(?:_(spring|autumn|winter))?$/);
if(speciesMatch) {
const speciesMatch=name.match(/^([a-z]+(?:_[a-z]+)*)_(log_top|log_side|planks|leaves)(?:_(spring|autumn|winter))?$/);
if(speciesMatch && SPECIES_NAMES.includes(speciesMatch[1])) {
const [,species,part,season]=speciesMatch;
if(part==='leaves') speciesLeaves(d,species,season);
else if(part==='log_top') endgrain(d,species);
@@ -989,6 +1013,9 @@ const BOTANY_GROUPS = {
['rowan','Sorbus aucuparia','pinnate leaflets; clustered red pomes'],
['hazel','Corylus avellana','heart-shaped serrate leaves; husked nuts'],
['spruce','Picea abies','short radial needles; pendant long cones'],
['crab_apple','Malus sylvestris','oval serrate leaves; pink white blossom; small pomes'],
['wild_cherry','Prunus avium','pointed serrate leaves; paired cherries; horizontal lenticels'],
['field_maple','Acer campestre','five blunt leaf lobes; paired winged samaras'],
],
shrubs: [
['bilberry','Vaccinium myrtillus','angular green twigs; small ovals; solitary blue berries'],
@@ -1001,6 +1028,10 @@ const BOTANY_GROUPS = {
['holly','Ilex aquifolium','spiny glossy evergreen leaves; red berries'],
['juniper','Juniperus communis','needle whorls of three; blue berry-like cones'],
['guelder_rose','Viburnum opulus','three-lobed opposite leaves; translucent red fruit cluster'],
['honeysuckle','Lonicera periclymenum','twining stems; paired leaves; long curved honey tubes'],
['ivy','Hedera helix','climbing rootlets; lobed leaves; globular autumn umbels'],
['gorse','Ulex europaeus','dense branching green spines; golden pea flowers'],
['broom','Cytisus scoparius','ridged green switches; sparse trifoliate leaves; pea flowers'],
],
grasses: [
['sedge','Carex paniculata','triangular culms; dense branched panicle; tussock'],
@@ -1025,6 +1056,18 @@ const BOTANY_GROUPS = {
['wild_garlic','Allium ursinum','broad basal lance leaves; white six-tepal star umbel'],
['cowslip','Primula veris','wrinkled basal rosette; nodding yellow tubular umbel'],
['wood_anemone','Anemone nemorosa','whorl of divided leaves; single pale six-tepal flower'],
['dandelion','Taraxacum officinale','toothed basal rosette; hollow scape; golden ligulate head'],
['daisy','Bellis perennis','spatulate rosette; short scape; white rays and gold disc'],
['red_clover','Trifolium pratense','upright trifoliate leaves; pale chevrons; red globose head'],
['birdsfoot_trefoil','Lotus corniculatus','five leaflets; yellow pea-flower umbel; radiating pods'],
['ribwort_plantain','Plantago lanceolata','ribbed lance rosette; brown spike with pale anther ring'],
['selfheal','Prunella vulgaris','opposite oval leaves; square stem; violet stacked flower whorls'],
['knapweed','Centaurea nigra','dark involucral bracts; ragged purple floret head'],
['oxeye_daisy','Leucanthemum vulgare','tall branched stem; large white rays; golden disc'],
['field_scabious','Knautia arvensis','divided leaves; long peduncle; lilac pincushion head'],
['teasel','Dipsacus fullonum','prickly ovoid head; curved bracts; opposite clasping leaf cups'],
['cow_parsley','Anthriscus sylvestris','finely divided leaves; branched compound white umbels'],
['garlic_mustard','Alliaria petiolata','heart-shaped toothed leaves; four white petals; upright siliques'],
],
ferns_mosses: [
['bracken','Pteridium aquilinum','three-part triangular divided frond'],
@@ -1037,6 +1080,7 @@ const BOTANY_GROUPS = {
['moss','Hypnum cupressiforme','creeping feather-like plaits; hooked tips'],
['fork_moss','Dicranum scoparium','cushion of swept sickle-shaped leaves'],
['star_moss','Mnium hornum','upright shoots; broad oval leaves; nodding capsules'],
['holly_fern','Polystichum setiferum','arching bipinnate fronds; bristle-tipped pinnules; forked pinnae'],
],
fungi: [
['fly_agaric','Amanita muscaria','red white-warted cap; ring; bulbous base'],
@@ -1113,6 +1157,166 @@ function compound(d,x,y,length=10,side=1) {
d.line(x+side*j,y-j*.7,x+side*(j+3),y-j*.7+1,'leaf',2);
}
}
// New catalogue specimens have authored diagnostic reproductive structures.
// Dormancy draws spent stems/pods, never a recoloured copy of an open flower.
function botanicalAddition(d,id,dormant) {
const stem=dormant?'bark':'leafDark',leafColor=dormant?'straw':'sage';
const stalk=(x,y,bx=16)=>d.line(bx,29,x,y,stem);
const rosette=(shape='lance')=> {
for(const [x,y,s] of [[6,29,-1],[11,28,-1],[19,29,1],[25,28,1]])
specimenLeaf(d,x,y,shape,s,leafColor);
};
const pea=(x,y)=>d.oval(x,y-2,3,2,'gold').poly([[x-3,y],[x,y+3],[x+3,y]],'ochre').dot(x,y,'strawLight');
if(id==='honeysuckle'||id==='ivy') {
d.line(13,29,12,4,'bark',2);
d.line(17,29,20,23,stem).line(20,23,13,16,stem).line(13,16,20,9,stem).line(20,9,17,3,stem);
for(const [x,y,s] of [[9,25,-1],[24,22,1],[8,16,-1],[25,12,1]]) {
if(!dormant) specimenLeaf(d,x,y,id==='ivy'?'lobe':'oval',s);
else d.line(x,y,x-s*3,y-3,'barkLight');
}
if(id==='ivy') {
for(let y=8;y<28;y+=4) d.line(12,y,9,y+2,'barkLight');
if(!dormant) for(const [x,y] of [[23,6],[24,26]]) {
for(let j=0;j<5;j++) {
const a=j*Math.PI*2/5;
d.line(x,y,x+Math.cos(a)*3,y+Math.sin(a)*3,'sage').oval(x+Math.cos(a)*3,y+Math.sin(a)*3,1,1,'ochre');
}
}
} else for(const [x,y,s] of [[20,6,1],[11,13,-1],[22,21,1]]) {
if(dormant) { d.oval(x,y,1,2,'barkLight'); continue; }
// Narrow curved tubes and splayed lips, not round flower icons.
d.line(x,y+3,x+s*3,y,'rose',2).line(x+s*3,y,x+s*5,y-1,'cream')
.line(x+s*5,y-1,x+s*6,y-3,'gold').line(x+s*5,y-1,x+s*7,y,'cream');
}
return;
}
if(id==='gorse'||id==='broom') {
for(let i=0;i<5;i++) {
const x=5+i*5,y=6+(i%3)*3; stalk(x,y,15+i%2);
for(let j=0;j<4;j++) {
const yy=13+j*4,xx=x+(16-x)*(yy-y)/(29-y);
if(id==='gorse') d.line(xx,yy,xx-3,yy-4,leafColor).line(xx,yy,xx+3,yy-3,stem);
else d.line(xx,yy,xx+(i%2?3:-3),yy-5,leafColor);
}
if(!dormant) pea(x,y+3);
else d.line(x,y+1,x+2,y+5,'barkDark',2);
}
if(id==='broom'&&!dormant) for(const x of [7,15,23]) {
d.oval(x,23,2,1,'leaf').oval(x-2,21,1,2,'sage').oval(x+2,21,1,2,'leafLight');
}
return;
}
if(id==='holly_fern') {
for(const [i,[tx,ty]] of [[4,17],[9,9],[18,4],[27,13]].entries()) {
stalk(tx,ty,15);
for(let j=1;j<=6;j++) {
const x=tx+(15-tx)*j/7,y=ty+(29-ty)*j/7,w=Math.min(j,7-j)+1;
for(const s of [-1,1]) {
d.line(x,y,x+s*w,y-2,leafColor,2).line(x+s*w,y-2,x+s*(w+1),y-4,stem);
d.line(x+s*(w-1),y-1,x+s*(w+1),y,'leafLight');
if(!dormant&&i%2===0) d.dot(x+s*2,y-1,'ochre');
}
}
}
return;
}
if(['dandelion','daisy','oxeye_daisy'].includes(id)) {
const tall=id==='oxeye_daisy', small=id==='daisy',x=small?12:tall?19:16,y=small?16:tall?7:10;
stalk(x,y,small?11:16); rosette(id==='dandelion'?'lobe':small?'oval':'lance');
if(tall) { d.line(18,19,25,14,stem); specimenLeaf(d,11,23,'diamond'); }
if(dormant) {
d.oval(x,y,small?2:3,2,'barkLight');
if(id==='dandelion') for(let j=-2;j<=2;j++) d.line(x,y,x+j*2,y-3,'strawLight');
} else if(id==='dandelion') {
flower(d,x,y,'ochre',12,4); flower(d,x,y,'gold',8,2); d.dot(x,y,'strawLight');
} else {
flower(d,x,y,'cream',small?8:12,small?3:5); d.oval(x,y,small?1:2,small?1:2,'gold');
if(tall) flower(d,25,13,'cream',8,3);
}
return;
}
if(id==='red_clover'||id==='birdsfoot_trefoil') {
const trefoil=id==='birdsfoot_trefoil';
for(const [i,[x,y]] of [[8,17],[19,8],[25,19]].entries()) {
stalk(x,y,14);
for(const s of [-1,1]) {
const xx=x+s*3,yy=24-i*3;
d.line(x,yy+3,xx,yy,stem).oval(xx,yy-2,2,3,leafColor).oval(xx+s*3,yy,2,2,'leaf');
if(!dormant) d.line(xx-1,yy-2,xx+1,yy-3,'leafLight');
}
if(dormant) {
if(trefoil) for(let j=-1;j<3;j++) d.line(x,y,x+j*2,y+5,'barkDark');
else d.oval(x,y,2,3,'barkLight');
} else if(trefoil) { pea(x-2,y); pea(x+3,y+2); }
else {
d.oval(x,y,3,4,'wine');
for(let j=0;j<4;j++) d.line(x-2,y-2+j*2,x+2,y-3+j*2,j%2?'rose':'mauve');
}
}
return;
}
if(id==='ribwort_plantain') {
rosette();
for(const [x,y] of [[10,8],[18,5],[25,13]]) {
stalk(x,y,16); d.oval(x,y,1.5,4,'bark');
if(!dormant) d.line(x-2,y+1,x+2,y+1,'cream').dot(x-2,y,'gold');
}
return;
}
if(id==='selfheal') {
for(const [i,[x,y]] of [[8,17],[16,8],[25,15]].entries()) {
stalk(x,y,14+i);
for(let j=0;j<3;j++) {
const yy=25-j*5;
d.oval(x-3,yy,3,1.5,leafColor).oval(x+3,yy,3,1.5,'leaf');
d.rect(x-2,y+j*3,5,2,dormant?'bark':'wine');
if(!dormant) d.line(x-2,y+j*3,x,y+j*3-1,'mauve');
}
}
return;
}
if(id==='cow_parsley'||id==='garlic_mustard') {
stalk(17,6);
if(id==='cow_parsley') {
for(const [x,y] of [[7,12],[17,6],[26,13]]) {
d.line(16,21,x,y+3,stem);
for(let j=-2;j<=2;j++) {
d.line(x,y+3,x+j*2,y,stem);
if(!dormant) flower(d,x+j*2,y,'cream',4,1);
else d.line(x+j*2,y,x+j*2,y+2,'barkLight');
}
}
compound(d,15,25,10,-1); compound(d,17,23,10,1);
} else {
for(const [x,y,s] of [[8,27,-1],[24,24,1],[10,19,-1],[22,15,1]]) specimenLeaf(d,x,y,'heart',s,leafColor);
for(let j=0;j<4;j++) {
const y=6+j*4,x=17+(j%2?3:-2); d.line(17,y+3,x,y,stem);
if(!dormant&&j<2) flower(d,x,y,'cream',4,1.5);
else d.line(x,y,x+(j%2?1:-1),y-4,'straw');
}
}
return;
}
// Ragged knapweed florets, a pincushion scabious, and prickly teasel cones.
const teasel=id==='teasel', scabious=id==='field_scabious';
for(const [i,[x,y]] of (teasel?[[16,9],[26,18]]:scabious?[[8,11],[22,7]]:[[12,9],[25,16]]).entries()) {
stalk(x,y+3,16);
if(teasel) {
d.oval(x,y,3,6,dormant?'bark':'strawDark');
for(let j=0;j<5;j++) d.line(x-3,y-4+j*2,x+3,y-5+j*2,'wood');
for(const s of [-1,1]) d.line(x,y+5,x+s*5,y+1,stem).line(x+s*5,y+1,x+s*4,y-2,stem);
if(!dormant) d.line(x-3,y,x+3,y,'mauve',2);
} else {
d.oval(x,y+2,3,2,'barkDark');
if(!dormant) for(let j=-3;j<=3;j++) d.line(x+j,y+1,x+j+(j%2),y-(scabious?2:4),scabious?'mauve':'wine')
.dot(x+j+(j%2),y-(scabious?3:4),'rose');
else d.line(x-3,y,x+3,y-1,'barkLight');
}
if(teasel) d.poly([[10,24],[16,26],[22,24],[18,28],[14,28]],leafColor);
else if(scabious) compound(d,16,27,8,i%2?-1:1);
else specimenLeaf(d,8+i*14,27,'lance',i%2?-1:1,leafColor);
}
}
function botanicalTree(d,id,dormant) {
const k=SPECIES_NAMES.indexOf(id), conifer=EVERGREEN_NAMES.includes(id);
const stem=id==='birch'?'birch':id==='beech'?'mineralGlow':id==='aspen'?'birchShade':'bark';
@@ -1140,13 +1344,17 @@ function botanicalTree(d,id,dormant) {
aspen:[[7,14],[9,24],[23,12],[25,24],[17,8]],
rowan:[[7,18],[10,26],[21,13],[26,23],[14,8]],
hazel:[[5,17],[10,25],[22,16],[26,24],[12,9]],
crab_apple:[[6,19],[11,26],[22,17],[26,25],[14,9]],
wild_cherry:[[7,15],[10,24],[22,12],[26,23],[16,8]],
field_maple:[[6,16],[9,25],[23,14],[26,24],[15,9]],
}[id];
for(const [i,[x,y]] of positions.entries()) {
d.line(16,24-i*3,x,y,'barkLight');
if(dormant) { d.line(x,y,x+(i%2?2:-2),y-3,'bark').dot(x,y-3,'wood'); continue; }
if(id==='rowan') compound(d,x-4,y+1,9,i%2?-1:1);
else {
const shape={oak:'lobe',birch:'diamond',willow:'lance',alder:'round',beech:'oval',aspen:'round',hazel:'heart'}[id];
const shape={oak:'lobe',birch:'diamond',willow:'lance',alder:'round',beech:'oval',aspen:'round',hazel:'heart',
crab_apple:'oval',wild_cherry:'diamond',field_maple:'lobe'}[id];
specimenLeaf(d,x,y,shape,i%2?-1:1);
if(id==='beech') for(let j=1;j<4;j++) d.line(x,y-j,x+2,y-j-2,'woodLight');
if(id==='alder') d.rect(x-1,y-7,2,2,'leafLight');
@@ -1155,7 +1363,12 @@ function botanicalTree(d,id,dormant) {
}
}
if(!dormant) {
if(id==='rowan') for(const [x,y] of [[22,6],[25,7],[23,9],[26,10]]) d.oval(x,y,1,1,'rust');
if(id==='crab_apple') { flower(d,23,9,'bud',5,2); d.oval(24,27,2,2,'ochre').dot(24,24,'bark'); }
else if(id==='wild_cherry') d.line(23,21,21,26,'bark').line(23,21,26,26,'bark')
.oval(21,27,1.5,1.5,'wine').oval(26,27,1.5,1.5,'rust');
else if(id==='field_maple') d.poly([[21,25],[17,22],[16,24],[21,27],[26,24],[27,22]],'wood')
.line(21,25,21,28,'barkLight');
else if(id==='rowan') for(const [x,y] of [[22,6],[25,7],[23,9],[26,10]]) d.oval(x,y,1,1,'rust');
else if(id==='oak' || id==='hazel') {
d.oval(23,26,2,3,'wood').line(21,24,24,24,id==='oak'?'bark':'sage');
if(id==='hazel') d.oval(27,25,2,3,'straw').line(25,23,28,23,'leaf');
@@ -1165,6 +1378,9 @@ function botanicalTree(d,id,dormant) {
}
}
function botanicalShrub(d,id,dormant) {
if(['honeysuckle','ivy','gorse','broom'].includes(id)) {
botanicalAddition(d,id,dormant); return;
}
const k=BOTANY_GROUPS.shrubs.findIndex(r=>r[0]===id), arch=['blackberry','dog_rose'].includes(id);
const tips=arch?[[5,18],[10,id==='dog_rose'?12:9],[22,10],[28,id==='dog_rose'?22:18]]:
[[6,13+k%4],[14,6+k%3],[23,11+k%2],[27,20+k%5]];
@@ -1225,6 +1441,9 @@ function botanicalGrass(d,id,dormant) {
}
}
function botanicalHerb(d,id,dormant) {
if(BOTANY_GROUPS.herbs.findIndex(r=>r[0]===id)>=10) {
botanicalAddition(d,id,dormant); return;
}
if(['clover','nettle','heather','bluebell'].includes(id)) {
understory(d,id+(dormant?'_dormant':''));
// Catalogue specimen includes diagnostic stolon/flower strings/scale sprays.
@@ -1266,6 +1485,7 @@ function botanicalHerb(d,id,dormant) {
if(dormant) d.line(x-1,7,x+2,10,'straw');
}
function botanicalFernMoss(d,id,dormant) {
if(id==='holly_fern') { botanicalAddition(d,id,dormant); return; }
const k=BOTANY_GROUPS.ferns_mosses.findIndex(r=>r[0]===id);
if(k<5) {
if(id==='harts_tongue') {