1746 lines
91 KiB
JavaScript
Executable File
1746 lines
91 KiB
JavaScript
Executable File
// Original pixel drawings: moonlit botanical field journal. No random/hash noise,
|
|
// external art, fonts, network access, or file-system writes in this module.
|
|
export const SIZE = 32;
|
|
export const PALETTE = Object.freeze(Object.fromEntries(Object.entries({
|
|
ink: '#1e1e2e', deep: '#181825', outline: '#313244', slate: '#45475a',
|
|
stone: '#747782', stoneLight: '#9b9b9c', moon: '#cdd6f4', mauve: '#cba6f7',
|
|
teal: '#94e2d5', gold: '#f9e2af', peach: '#fab387', rose: '#b67f96',
|
|
earthDark: '#443a38', earth: '#665044', earthLight: '#876957', clay: '#ad7e68',
|
|
barkDark: '#433b36', bark: '#685346', barkLight: '#92765a', wood: '#ad916b',
|
|
woodLight: '#c6ad83', strawDark: '#766848', straw: '#a39061', strawLight: '#c6b581',
|
|
leafDark: '#384c43', leaf: '#556e55', sage: '#7d9570', leafLight: '#a4b28b',
|
|
springDark: '#526b50', springLeaf: '#8fa67c', springLight: '#bac59a', bud: '#c19caa',
|
|
autumnDark: '#64454c', copper: '#a26c49', ochre: '#b59458', wine: '#845365',
|
|
waterDark: '#334952', water: '#425e66', waterLight: '#638187', glass: '#78959a',
|
|
berry: '#92627f', berryLight: '#c393aa', rust: '#9a7660', iron: '#909f9f',
|
|
fur: '#92816d', cream: '#d1c5a5', fox: '#b3815d', deer: '#93755a',
|
|
mineral: '#777980', mineralShade: '#696c73', mineralGlow: '#898b90', feldspar: '#a79792',
|
|
basalt: '#4d5358', basaltShade: '#42494e', basaltGlow: '#60686b',
|
|
limestone: '#a19c88', limeShade: '#8e8a79', limeGlow: '#b4af99',
|
|
slateBlue: '#697681', slateShade: '#58636e', slateGlow: '#82909a',
|
|
sand: '#b1a181', sandShade: '#a39476', sandGlow: '#c0b292',
|
|
loam: '#655347', loamShade: '#57473e', loamGlow: '#766052', humus: '#49413a',
|
|
turf: '#637653', turfShade: '#536649', turfGlow: '#778764',
|
|
birch: '#bbb8a6', birchShade: '#9e9d8e', birchGlow: '#d0ccba',
|
|
pine: '#405c51', pineShade: '#344b44', pineGlow: '#648074',
|
|
willow: '#81907a', willowShade: '#5e7567', willowGlow: '#a2ad8b',
|
|
snow: '#bfcbd1', snowShade: '#acbbc4', snowGlow: '#d5dfe0',
|
|
copperMetal: '#ba8560', copperShade: '#8c634d', copperGlow: '#d5a77d', verdigris: '#638d7b',
|
|
}).map(([name, hex]) => [name, Object.freeze([1,3,5].map(i => parseInt(hex.slice(i,i+2),16)))])));
|
|
|
|
const rgba = (color, alpha = 255) => [...(PALETTE[color] || color), alpha];
|
|
class Drawing {
|
|
constructor(background) {
|
|
this.width = SIZE; this.height = SIZE; this.data = new Uint8Array(SIZE*SIZE*4);
|
|
if (background) this.rect(0,0,32,32,background);
|
|
}
|
|
dot(x,y,color,alpha=255) {
|
|
x = Math.round(x); y = Math.round(y);
|
|
if (x>=0 && y>=0 && x<32 && y<32) this.data.set(rgba(color,alpha),(y*32+x)*4);
|
|
return this;
|
|
}
|
|
rect(x,y,w,h,color,alpha=255) {
|
|
for (let j=y;j<y+h;j++) for(let i=x;i<x+w;i++) this.dot(i,j,color,alpha);
|
|
return this;
|
|
}
|
|
line(x0,y0,x1,y1,color,width=1) {
|
|
const steps = Math.max(Math.abs(x1-x0),Math.abs(y1-y0),1);
|
|
for(let i=0;i<=steps;i++) this.rect(Math.round(x0+(x1-x0)*i/steps),Math.round(y0+(y1-y0)*i/steps),width,width,color);
|
|
return this;
|
|
}
|
|
poly(points,color) {
|
|
for(let y=0;y<32;y++) for(let x=0;x<32;x++) {
|
|
let hit=false;
|
|
for(let i=0,j=points.length-1;i<points.length;j=i++) {
|
|
const [a,b]=points[i], [c,d]=points[j];
|
|
if((b>y+.5)!==(d>y+.5) && x+.5<(c-a)*(y+.5-b)/(d-b)+a) hit=!hit;
|
|
}
|
|
if(hit) this.dot(x,y,color);
|
|
}
|
|
return this;
|
|
}
|
|
oval(cx,cy,rx,ry,color) {
|
|
for(let y=Math.floor(cy-ry);y<=cy+ry;y++) for(let x=Math.floor(cx-rx);x<=cx+rx;x++)
|
|
if(((x-cx)/rx)**2+((y-cy)/ry)**2<=1) this.dot(x,y,color);
|
|
return this;
|
|
}
|
|
}
|
|
|
|
// Authored clustered marks wrap as whole shapes, not individual random pixels.
|
|
function pebbles(d, dark='earthDark', light='earthLight') {
|
|
for(const [x,y,w] of [[3,5,4],[19,3,3],[11,14,5],[25,20,4],[2,27,3],[19,30,4]]) {
|
|
d.rect(x,y,w,2,dark).rect(x+1,y,w-1,1,light);
|
|
}
|
|
}
|
|
function earth(d) {
|
|
d.rect(0,0,32,32,'loam');
|
|
// Discontinuous lenses of compacted aggregates, never full-width cartoon bands.
|
|
for(const [x,y,w,h] of [[2,2,6,3],[15,0,5,2],[24,5,7,3],[8,9,8,2],[19,12,5,3],
|
|
[0,17,5,2],[10,20,7,3],[23,23,8,2],[3,28,8,3],[18,30,4,2]])
|
|
d.oval(x,y,w/2,h,'loamShade');
|
|
for(const [i,[x,y]] of GRAINS.entries()) {
|
|
d.rect(x,y,i%3===0?3:2,i%4===0?2:1,i%3===0?'loamGlow':'loamShade');
|
|
if(i%5===0) d.dot(x+1,y-1,'earthLight');
|
|
}
|
|
d.line(6,14,9,16,'humus').line(23,28,25,30,'humus');
|
|
}
|
|
function grass(d,side=false) {
|
|
if(side) {
|
|
earth(d);
|
|
for(let x=0;x<32;x++) {
|
|
const depth=Math.round(5+1.6*Math.sin(x*.43)+Math.sin(x*.91));
|
|
d.rect(x,0,1,depth,'turfShade');
|
|
if(x%3!==0) d.line(x,depth-1,x-1,depth+2+(x%4),'humus');
|
|
if(x%5===1) d.line(x,depth+2,x+2,depth+6,'loamGlow');
|
|
}
|
|
for(const [i,[x,y]] of GRAINS.entries()) if(y<8)
|
|
d.rect(x,y/2,2,1,i%2?'turf':'turfGlow');
|
|
return;
|
|
}
|
|
d.rect(0,0,32,32,'turf');
|
|
for(const [i,[x,y]] of GRAINS.entries()) {
|
|
d.poly([[x,y],[x+3,y-1],[x+4,y+1],[x+1,y+2]],i%3?'turfShade':'leaf');
|
|
d.rect(x+1,y-1,2,1,'turfGlow');
|
|
const u=(x+13)%32,v=(y+7)%32;
|
|
d.line(u,v,u+(i%2?1:-1),v+2,i%4?'turfShade':'sage');
|
|
}
|
|
}
|
|
const GRAINS = [[1,1],[9,3],[18,2],[27,1],[4,6],[14,7],[23,5],[30,8],[1,11],[8,13],
|
|
[17,11],[26,12],[4,17],[12,16],[22,18],[29,16],[0,22],[8,24],[17,22],[25,24],
|
|
[4,28],[13,30],[21,28],[29,30],[6,2],[20,8],[15,26],[30,21],[10,19],[2,31],
|
|
[24,14],[11,10],[18,17],[5,21],[27,27],[15,5]];
|
|
|
|
function mineral(d,name='stone') {
|
|
const colors={stone:['mineral','mineralShade','mineralGlow'],granite:['mineral','mineralShade','feldspar'],
|
|
basalt:['basalt','basaltShade','basaltGlow'],limestone:['limestone','limeShade','limeGlow'],
|
|
slate:['slateBlue','slateShade','slateGlow'],sandstone:['sand','sandShade','sandGlow']}[name];
|
|
const [base,dark,light]=colors; d.rect(0,0,32,32,base);
|
|
if(name==='slate' || name==='sandstone') {
|
|
for(const [i,[x,y]] of GRAINS.entries()) if(i%2===0) {
|
|
const length=name==='slate'?7:5, slope=name==='slate'?-2:1;
|
|
d.line(x,y,x+length,y+slope,dark).line(x+1,y-1,x+length-2,y+slope-1,light);
|
|
}
|
|
if(name==='slate') d.line(3,29,12,24,dark).line(12,24,16,25,dark);
|
|
if(name==='sandstone') GRAINS.forEach(([x,y],i)=> {
|
|
d.rect((x+11)%31,y,2,1,i%3?dark:light);
|
|
if(i%4===0) d.dot((x+14)%31,y+1,light);
|
|
});
|
|
} else {
|
|
// Unjointed microfacets sit inside continuous parent rock, not grout cells.
|
|
GRAINS.forEach(([x,y],i)=> {
|
|
const w=name==='granite'?2:3+(i%3), h=name==='basalt'?2:3;
|
|
d.poly([[x,y],[x+w-1,y-1],[x+w,y+1],[x+1,y+h]],i%3?dark:light);
|
|
if(i%4===0) d.line(x,y,x+1,y-1,light);
|
|
});
|
|
if(name==='limestone') {
|
|
d.line(2,20,7,18,dark).line(7,18,10,19,dark);
|
|
d.line(25,2,24,7,dark).line(24,7,27,10,dark);
|
|
d.oval(19,25,3,2,'limeShade').line(17,24,20,24,'limeGlow');
|
|
}
|
|
if(name==='granite') for(const [x,y] of [[6,4],[22,10],[12,27],[29,19]])
|
|
d.rect(x,y,2,2,'stoneLight').dot(x+2,y+2,'slate');
|
|
if(name==='basalt') for(const [x,y] of [[8,7],[23,19],[14,28]])
|
|
d.oval(x,y,2,1,'basaltShade').dot(x-1,y-1,'basaltGlow');
|
|
}
|
|
}
|
|
function masonry(d,foundation=false) {
|
|
d.rect(0,0,32,32,'outline');
|
|
for(let row=0;row<3;row++) {
|
|
const y=row*11-1, shift=row%2 ? -7 : 0;
|
|
for(let x=shift;x<32;x+=16) {
|
|
d.poly([[x+2,y+1],[x+13,y+1],[x+15,y+4],[x+14,y+10],[x+1,y+10],[x,y+6]],foundation?'earthLight':'stone');
|
|
d.line(x+3,y+2,x+11,y+2,foundation?'barkLight':'stoneLight');
|
|
d.line(x+2,y+9,x+12,y+9,'slate');
|
|
}
|
|
}
|
|
if(!foundation) d.line(8,4,10,7,'slate').line(10,7,9,10,'slate');
|
|
}
|
|
function planks(d,vertical=false,species='oak') {
|
|
const [base,dark,light]={oak:['wood','barkLight','woodLight'],birch:['birch','birchShade','birchGlow'],
|
|
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'],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);
|
|
put(x,y,y%8===0?dark:(grain>1.05?light:base));
|
|
}
|
|
for(const [x,y] of [[11,1],[25,17]]) for(let j=0;j<6;j++) put(x,y+j,dark);
|
|
for(const [x,y] of [[3,4],[22,20]]) { put(x,y,dark); put(x+1,y,light); }
|
|
}
|
|
function bark(d,species='oak') {
|
|
const [base,dark,light]={oak:['bark','barkDark','barkLight'],birch:['birch','birchShade','birchGlow'],
|
|
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'],
|
|
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()) {
|
|
if(species==='beech') {
|
|
if(i%3===0) d.line(x,y,x+1,y+5,dark).line(x+2,y+1,x+2,y+4,light);
|
|
} else if(species==='aspen') {
|
|
if(i%3===0) d.poly([[x,y-2],[x+3,y],[x,y+2],[x-2,y]],dark).dot(x,y-1,light);
|
|
} else if(species==='spruce') {
|
|
d.poly([[x,y-2],[x+3,y-1],[x+2,y+3],[x-1,y+2]],dark).line(x,y-1,x+2,y-1,light);
|
|
} else if(i%2===0) d.line(x,y,x+(species==='hazel'?4:2),y,dark).dot(x+1,y-1,light);
|
|
}
|
|
if(species==='beech') d.oval(20,18,3,5,dark).oval(20,17,2,3,base).line(18,14,21,12,light);
|
|
return;
|
|
}
|
|
if(species==='birch') {
|
|
for(const [i,[x,y]] of GRAINS.entries()) if(i%2===0)
|
|
d.line(x,y,x+2+i%4,y,i%3?dark:'barkDark').line(x+1,y-1,x+2,y-1,light);
|
|
d.poly([[24,0],[27,0],[26,9],[24,12],[25,4]],dark);
|
|
d.line(25,2,25,7,'bark'); return;
|
|
}
|
|
const fissures=species==='pine'?[[2,1],[12,4],[23,-2],[30,3]]:
|
|
species==='willow'?[[3,0],[9,2],[16,-3],[22,1],[29,5]]:[[1,0],[8,4],[18,-3],[27,1]];
|
|
for(const [i,[x,phase]] of fissures.entries()) for(let y=0;y<32;y++) {
|
|
const u=x+Math.round(Math.sin((y+phase)*.24+i)*1.5+Math.sin(y*.57)*.5);
|
|
if((y+phase+32)%13<11) d.dot(u,y,dark).dot(u+1,y,dark);
|
|
if((y+i)%9<6) d.dot(u+2,y,light);
|
|
if(y===11+i*3) d.line(u,y,u-2,y-4,dark);
|
|
}
|
|
if(species==='pine') for(const [x,y] of [[6,9],[17,21],[29,4]])
|
|
d.line(x,y,x+4,y-1,'bark').line(x+1,y-2,x+3,y-2,'wood');
|
|
else d.oval(14,19,2,4,dark).line(14,16,14,20,light).dot(15,21,base);
|
|
}
|
|
function endgrain(d,species='oak') {
|
|
const [base,ring,light]={oak:['wood','barkLight','woodLight'],birch:['birch','birchShade','birchGlow'],
|
|
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'],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);
|
|
const warped=r+.6*Math.sin(a*3+phase)+.35*Math.sin(a*5+r*.4);
|
|
const band=(warped+32)%3.7;
|
|
d.dot(x,y,r>14.3+.5*Math.sin(a*7)?'barkDark':r>13.1?'bark':band<.7?ring:band<1.3?light:base);
|
|
}
|
|
d.line(14,16,17,22,ring).line(17,22,21,28,ring);
|
|
d.line(4,8,7,10,'bark').line(7,10,9,13,ring);
|
|
}
|
|
function straw(d) {
|
|
d.rect(0,0,32,32,'strawDark');
|
|
for(let y=-8;y<32;y+=8) for(let x=-4;x<36;x+=6) {
|
|
d.poly([[x,y],[x+5,y],[x+8,y+8],[x+2,y+8]],'straw');
|
|
d.line(x+1,y+1,x+4,y+6,'strawLight').line(x+5,y+3,x+6,y+6,'strawDark');
|
|
}
|
|
d.line(0,15,31,15,'bark').line(0,16,31,16,'wood');
|
|
}
|
|
function leaf(d,x,y,flip=false,color='sage') {
|
|
const s=flip?-1:1;
|
|
d.poly([[x,y],[x+s*7,y-4],[x+s*9,y-3],[x+s*7,y],[x+s*2,y+2]],'leafDark');
|
|
d.line(x+s*2,y,x+s*7,y-2,color);
|
|
}
|
|
function twig(d,x=16,y=29,height=21) {
|
|
d.line(x,y,x-1,y-height,'bark',2).line(x,y-9,x-7,y-15,'bark');
|
|
d.line(x,y-14,x+7,y-19,'barkLight');
|
|
}
|
|
function seasonalPlant(d,name) {
|
|
if(name.startsWith('leaves_')) {
|
|
if(name==='leaves_winter') {
|
|
// Open, interlacing woody forks: no green fill or snow-painted foliage.
|
|
for(const [x,y,s] of [[5,27,1],[19,20,-1],[25,30,1]]) {
|
|
d.line(x,y,x+s*3,y-17,'bark').line(x+s*2,y-9,x-s*5,y-15,'barkLight');
|
|
d.line(x+s*3,y-17,x+s*7,y-22,'barkDark');
|
|
d.line(x-s*2,y-12,x-s*3,y-19,'bark');
|
|
}
|
|
return;
|
|
}
|
|
const spring=name==='leaves_spring';
|
|
for(const [x,y] of [[4,7],[18,4],[28,10],[10,19],[24,24],[0,28]]) {
|
|
d.oval(x,y,6,4,spring?'springDark':'autumnDark')
|
|
.oval(x-1,y-1,5,3,spring?'springLeaf':'copper');
|
|
d.line(x-2,y,x+3,y-2,spring?'springLight':'ochre');
|
|
if(spring) d.rect(x+3,y-4,2,2,'bud').dot(x+3,y-4,'cream');
|
|
else d.line(x-3,y+1,x,y+2,'wine');
|
|
}
|
|
return;
|
|
}
|
|
if(name==='spring_greens' || name==='spring_greens_dormant') {
|
|
const dormant=name.endsWith('_dormant');
|
|
d.line(16,30,15,20,dormant?'bark':'springDark');
|
|
for(const [x,y,tipX,tipY] of [[14,26,4,17],[16,23,11,8],[17,25,26,13],[15,28,25,24]]) {
|
|
// Broad, uneven basal blades with a central vein, not grass needles.
|
|
const ty=dormant?tipY+9:tipY;
|
|
d.poly([[x,y],[tipX-3,ty+2],[tipX,ty],[tipX+3,ty+3],[x+2,y]],dormant?'strawDark':'springDark');
|
|
d.line(x,y,tipX,ty+2,dormant?'straw':'springLeaf');
|
|
if(!dormant) d.line(x,y-1,tipX,ty+3,'springLight');
|
|
}
|
|
return;
|
|
}
|
|
if(name==='autumn_mushrooms' || name==='autumn_mushrooms_dormant') {
|
|
const dormant=name.endsWith('_dormant');
|
|
if(dormant) {
|
|
// Occupied litter/mycelial trace, conspicuously without harvestable caps.
|
|
d.line(5,28,24,25,'earthDark',2).line(9,27,16,24,'barkLight');
|
|
d.line(16,26,21,29,'strawDark').line(18,26,26,23,'bark');
|
|
d.line(10,28,7,25,'wood').line(22,25,24,28,'earthLight');
|
|
} else for(const [x,y,r] of [[10,18,6],[23,23,5],[17,12,4]]) {
|
|
d.rect(x-1,y,2,28-y,'cream').line(x+1,y+1,x+1,27,'barkLight');
|
|
d.oval(x,y,r,2,'autumnDark');
|
|
d.poly([[x-r,y-1],[x-r+2,y-4],[x,y-5],[x+r-1,y-3],[x+r,y-1]],'copper');
|
|
d.line(x-r+2,y-2,x,y-4,'ochre').line(x-2,y+1,x+2,y+1,'woodLight');
|
|
}
|
|
return;
|
|
}
|
|
if(name==='fern_dormant') {
|
|
for(const [x,y,s] of [[5,23,1],[26,22,-1],[10,17,1]]) {
|
|
d.line(16,30,x,y,'strawDark').line(x,y,x+s*2,y-4,'barkLight');
|
|
for(let j=0;j<3;j++) d.line(x+s*j,y+j,x+s*(j+3),y+j-2,'straw');
|
|
}
|
|
return;
|
|
}
|
|
for(const [x,top,bend] of (name==='wildflowers_dormant'?[[8,15,-3],[17,12,2],[24,20,3]]:
|
|
[[5,20,-2],[11,12,-4],[17,17,2],[23,14,4],[27,23,2]])) {
|
|
d.line(x,30,x+bend,top,'strawDark').line(x,27,x-3,22,'barkLight');
|
|
if(name==='wildflowers_dormant') {
|
|
d.line(x+bend,top,x+bend-2,top+2,'bark');
|
|
d.dot(x+bend,top,'earthLight'); // Spent seed head, never a bloom.
|
|
} else d.line(x,29,x+bend+2,top+3,'straw');
|
|
}
|
|
}
|
|
function plant(d,name) {
|
|
if(SEASONAL_PLANT_NAMES.includes(name)) { seasonalPlant(d,name); return; }
|
|
if(name==='leaves') {
|
|
for(const [x,y] of [[4,7],[18,4],[28,10],[10,19],[24,24],[0,28]]) {
|
|
d.oval(x,y,6,4,'leafDark').oval(x-1,y-1,5,3,'leaf'); leaf(d,x-3,y,false);
|
|
}
|
|
return;
|
|
}
|
|
if(name==='berry_bush' || name==='berry_bush_bare') {
|
|
twig(d); d.line(16,26,24,17,'bark',2);
|
|
for(const [x,y] of [[8,18],[10,10],[21,9],[25,18],[16,21]]) {
|
|
d.oval(x,y,5,4,'leafDark').oval(x-1,y-1,4,3,'leaf'); leaf(d,x,y,true);
|
|
}
|
|
if(name==='berry_bush') for(const [x,y] of [[7,16],[19,8],[24,18],[14,22]])
|
|
d.oval(x,y,2,2,'berry').dot(x-1,y-1,'berryLight');
|
|
return;
|
|
}
|
|
if(['sapling','tree_seedling','young_tree','mature_tree'].includes(name)) {
|
|
const mature=name==='mature_tree', young=name==='young_tree', seedling=name==='tree_seedling';
|
|
if(seedling) {
|
|
d.line(16,30,15,20,'barkLight'); leaf(d,15,23,true); leaf(d,15,21,false,'leafLight');
|
|
return;
|
|
}
|
|
twig(d,16,30,mature?24:young?22:14);
|
|
if(mature || young) for(const [x,y] of [[10,10],[22,11],[16,6],[7,17],[24,18]])
|
|
d.oval(x,y,mature?7:5,5,'leafDark').oval(x-1,y-1,mature?6:4,3,'leaf');
|
|
for(const [x,y,f] of [[14,20,true],[16,14,false],[14,11,true]]) leaf(d,x,y,f);
|
|
return;
|
|
}
|
|
if(name==='fern') {
|
|
for(const direction of [-1,1]) for(let i=0;i<4;i++) {
|
|
const x=16+direction*(i*3), y=26-i*5;
|
|
d.line(16,29,x,y-4,'sage');
|
|
for(let j=0;j<3;j++) {
|
|
d.line(x,y-j*2,x+direction*4,y-j*2-3,'leaf');
|
|
d.line(x,y-j*2,x-direction*2,y-j*2-2,'sage');
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if(name==='fungi') {
|
|
for(const [x,y,r] of [[10,19,7],[23,24,5]]) {
|
|
d.rect(x-1,y,3,9,'cream').line(x+2,y+1,x+2,y+7,'barkLight');
|
|
d.oval(x,y,r,3,'barkDark').poly([[x-r,y],[x-r+2,y-4],[x,y-6],[x+r-2,y-3],[x+r,y]],'clay');
|
|
d.line(x-r+3,y-3,x,y-4,'woodLight');
|
|
}
|
|
return;
|
|
}
|
|
if(name==='litter' || name==='dung' || name==='deadwood') {
|
|
if(name==='deadwood') {
|
|
d.poly([[4,24],[6,19],[25,13],[29,17],[27,23],[9,29]],'barkDark');
|
|
d.line(7,22,25,16,'barkLight',3).line(11,26,27,20,'bark');
|
|
d.oval(7,24,3,4,'wood').oval(7,24,1,2,'bark'); d.line(18,18,16,10,'bark',2);
|
|
} else if(name==='dung') {
|
|
d.oval(16,26,10,3,'earthDark').oval(16,23,7,4,'earth').oval(15,20,4,3,'earthLight');
|
|
d.line(12,24,17,24,'barkDark');
|
|
} else for(const [x,y,f] of [[5,23,false],[19,27,false],[24,17,true],[12,16,true]]) {
|
|
leaf(d,x,y,f,'straw'); d.line(x,y,x+(f?-5:5),y-2,'barkLight');
|
|
}
|
|
return;
|
|
}
|
|
const crop=name==='grain_patch'?4:Number(name.match(/^crop_([1-4])$/)?.[1] || 0);
|
|
for(const [x,top,bend] of [[7,9,-3],[15,4,1],[24,7,3],[19,15,-2]]) {
|
|
const y=crop ? 28-crop*5+(top%4) : top;
|
|
d.line(x,30,x+bend,y,crop>=3?'straw':'leaf',1);
|
|
if(name==='reeds') {
|
|
d.line(x+bend,y+4,x+bend,y-3,'bark',2).line(x+2,28,x+5,16,'sage');
|
|
} else {
|
|
d.line(x,28,x-4,20,'sage').line(x,26,x+4,18,'leaf');
|
|
if(crop>=3) for(let i=0;i<4;i++) {
|
|
d.line(x+bend,y+i*2,x+bend-2,y+i*2-2,crop===4?'strawLight':'sage');
|
|
d.line(x+bend,y+i*2+1,x+bend+2,y+i*2-1,crop===4?'straw':'leafLight');
|
|
}
|
|
if(name==='wildflowers') {
|
|
d.oval(x+bend,y,3,1,'rose').oval(x+bend,y,1,3,'mauve').dot(x+bend,y,'gold');
|
|
}
|
|
}
|
|
}
|
|
if(name==='grain_patch') {
|
|
d.line(3,30,2,23,'strawDark').line(28,30,29,25,'straw');
|
|
d.line(8,10,5,4,'strawLight').line(16,9,18,3,'strawLight');
|
|
}
|
|
}
|
|
|
|
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',
|
|
'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']);
|
|
export const DORMANT_UNDERSTORY_NAMES = Object.freeze(UNDERSTORY_NAMES.slice(0,6).map(n=>`${n}_dormant`));
|
|
// Explicit expansion of the biome/diversity contracts; no source-scanner guesses.
|
|
export const BIOME_WORLD_NAMES = Object.freeze([...ROCK_NAMES,...ROCK_NAMES.map(n=>`${n}_bricks`),
|
|
'forest_floor','podzol','alluvium','peat','alpine_turf','gravel','scree','snow',
|
|
'quartz_vein','copper_ore_vein','coal_ore_vein',
|
|
...SPECIES_NAMES.flatMap(s=>['log_top','log_side','leaves','planks'].map(n=>`${s}_${n}`))]);
|
|
export const SPECIES_SEASONAL_NAMES = Object.freeze(SPECIES_NAMES.filter(s=>!EVERGREEN_NAMES.includes(s))
|
|
.flatMap(s=>['spring','autumn','winter'].map(season=>`${s}_leaves_${season}`)));
|
|
export const PLANT_NAMES = Object.freeze(['leaves','reeds','berry_bush','berry_bush_bare','sapling',
|
|
'grain_patch','crop_1','crop_2','crop_3','crop_4','meadow_grass','wildflowers','fern','litter',
|
|
'fungi','deadwood','tree_seedling','young_tree','mature_tree','dung',...SEASONAL_PLANT_NAMES,
|
|
...SPECIES_NAMES.map(s=>`${s}_leaves`),...SPECIES_SEASONAL_NAMES,...UNDERSTORY_NAMES,...DORMANT_UNDERSTORY_NAMES]);
|
|
export const WORLD_NAMES = Object.freeze(['stone','soil','grass_top','grass_side','sand','log_top','log_side',
|
|
'leaves','planks','wall','roof','thatch','foundation','frame','stairs','door','window','vent','storage','pantry',
|
|
'reeds','berry_bush','berry_bush_bare','sapling','grain_patch','crop_1','crop_2','crop_3','crop_4',
|
|
'clay_bank','iron_ore_vein','water','fire','fire_lit','torch','sticks','loose_stone','bedroll',
|
|
'drying_rack','fish_trap','weir_stone',...PLANT_NAMES.slice(10,30),...BIOME_WORLD_NAMES,
|
|
...SPECIES_SEASONAL_NAMES,...UNDERSTORY_NAMES,...DORMANT_UNDERSTORY_NAMES]);
|
|
export const ITEM_NAMES = Object.freeze(['berries','grain','grain_seed','raw_food','cooked_food','fish_bait',
|
|
'raw_fish','cooked_fish','fish_drying_charge','dried_fish','iron_ore','iron_ingot','charcoal',
|
|
'charcoal_charge','smelt_charge','clay','clay_jar','clay_jar_charge','dried_food','dried_food_charge',
|
|
'cloak','vessel','water_raw','water_clean','firestarter','iron_axe','spring_greens','autumn_mushrooms',
|
|
'quartz','copper_ore','coal','copper_ingot','copper_charge','copper_pick']);
|
|
|
|
function speciesLeaves(d,species,season='summer') {
|
|
const index=SPECIES_NAMES.indexOf(species);
|
|
if(index>=5) {
|
|
const winter=season==='winter', spring=season==='spring', autumn=season==='autumn';
|
|
const dark=winter?'barkDark':spring?'springDark':autumn?'autumnDark':'leafDark';
|
|
const base=winter?'bark':spring?'springLeaf':autumn?'copper':species==='spruce'?'pine':'sage';
|
|
const light=winter?'barkLight':spring?'springLight':autumn?'ochre':'leafLight';
|
|
for(const [i,[x,y]] of [[5,7],[20,5],[28,19],[12,21],[2,30]].entries()) {
|
|
d.line(x-3,y+4,x+3,y-4,winter?'bark':'barkLight');
|
|
if(winter) {
|
|
d.line(x,y,x-3-index%4,y-4-index%3,dark).line(x+2,y-3,x+index-5,y-7-index%4,light);
|
|
continue;
|
|
}
|
|
if(species==='spruce') {
|
|
for(let j=0;j<6;j++) d.line(x-3+j,y+3-j,x-6+j,y-j,base)
|
|
.line(x-2+j,y+2-j,x+2+j,y+3-j,j%2?light:dark);
|
|
} else if(species==='rowan') {
|
|
for(let j=0;j<4;j++) d.line(x-2+j,y+3-j,x-5+j,y+2-j,dark,2)
|
|
.line(x-1+j,y+2-j,x+2+j,y+3-j,base,2).dot(x+2+j,y+3-j,light);
|
|
} else if(species==='aspen') {
|
|
d.oval(x,y,4,4,dark).oval(x-1,y-1,3,3,base).line(x-2,y+1,x+2,y-2,light);
|
|
} 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);
|
|
}
|
|
if(spring && i%2===0) d.dot(x+3,y-4,'bud');
|
|
if(autumn) d.line(x-2,y+3,x,y+3,'wine');
|
|
}
|
|
return;
|
|
}
|
|
if(season==='winter') {
|
|
// Species-dependent open tracery. No leaf polygons on a winter canopy.
|
|
for(let i=0;i<3;i++) {
|
|
const x=6+i*9, y=29-i*3, s=i%2?-1:1, bend=index-2;
|
|
d.line(x,y,x+bend,y-15,'bark').line(x+bend,y-15,x+s*5,y-23,'barkLight');
|
|
d.line(x,y-7,x-s*5,y-14-index,'barkDark');
|
|
d.line(x-s*3,y-11,x-s*4,y-18,'bark');
|
|
}
|
|
return;
|
|
}
|
|
const colors=season==='spring'?['springDark','springLeaf','springLight']:
|
|
season==='autumn'?['autumnDark',index%2?'ochre':'copper',index%2?'copper':'ochre']:
|
|
{oak:['leafDark','leaf','sage'],birch:['leaf','sage','leafLight'],pine:['pineShade','pine','pineGlow'],
|
|
willow:['willowShade','willow','willowGlow'],alder:['leafDark','turfShade','turfGlow']}[species];
|
|
const [dark,base,light]=colors;
|
|
for(const [i,[x0,y0]] of [[4,7],[16,5],[27,9],[7,20],[22,22],[1,30]].entries()) {
|
|
const x=x0+(index%2),y=y0+(i%2?index%3:0);
|
|
if(species==='pine') {
|
|
d.line(x-4,y+3,x+4,y-4,'bark');
|
|
for(let j=0;j<5;j++) d.line(x-3+j,y+1-j,x-6+j,y-1-j,base)
|
|
.line(x-2+j,y+1-j,x+1+j,y+3-j,j%2?light:dark);
|
|
} else if(species==='willow') {
|
|
d.line(x,y-5,x-1,y+6,'barkLight');
|
|
for(let j=0;j<3;j++) d.poly([[x,y-3+j*3],[x+5,y-4+j*3],[x+3,y+j*3]],base)
|
|
.line(x,y-3+j*3,x+3,y-3+j*3,light);
|
|
d.line(x-1,y,x-5,y+5,dark);
|
|
} else if(species==='birch') {
|
|
for(const [a,b] of [[-2,0],[3,-3],[3,4]])
|
|
d.poly([[x+a-3,y+b],[x+a,y+b-3],[x+a+3,y+b],[x+a,y+b+3]],dark)
|
|
.line(x+a-1,y+b,x+a+1,y+b-1,light).dot(x+a,y+b+1,base);
|
|
} else {
|
|
d.oval(x,y,species==='oak'?5:4,3,dark).oval(x-1,y-1,4,2,base);
|
|
d.line(x-3,y,x+2,y-2,light);
|
|
if(species==='oak') d.rect(x+3,y-2,2,2,base).rect(x-4,y+1,2,2,base);
|
|
else d.line(x-1,y-2,x+1,y-2,light);
|
|
}
|
|
if(season==='spring' && i%2===0) d.rect(x+2,y-4,2,2,'bud');
|
|
if(season==='autumn') d.line(x-2,y+2,x,y+2,'wine');
|
|
}
|
|
}
|
|
|
|
function understory(d,name) {
|
|
const dormant=name.endsWith('_dormant'), kind=name.replace(/_dormant$/,''),
|
|
stem=dormant?'strawDark':'leafDark', mid=dormant?'straw':'sage', light=dormant?'barkLight':'leafLight';
|
|
if(kind==='moss' || kind==='lichen') {
|
|
for(const [i,[x,y]] of [[6,26],[12,23],[20,25],[26,28],[16,29]].entries()) {
|
|
if(kind==='moss') {
|
|
d.oval(x,y,4,2,'leafDark');
|
|
for(let j=0;j<3;j++) d.line(x-2+j*2,y,x-1+j*2,y-3-i%2,j%2?'sage':'leaf');
|
|
} else {
|
|
d.line(x-2,y,x+2,y-3,'stoneLight',2).line(x,y-2,x-2,y-5,'willow');
|
|
d.line(x+2,y-3,x+4,y-2,'cream').dot(x-3,y-4,'sage');
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if(kind==='clover') {
|
|
for(const [i,[x,y]] of [[7,24],[14,17],[24,22],[18,26]].entries()) {
|
|
d.line(16,30,x,dormant?y+3:y,stem);
|
|
if(dormant) { d.line(x,y+3,x+3,y+4,mid); continue; }
|
|
for(const [a,b] of [[-2,0],[1,-2],[3,1]]) d.oval(x+a,y+b,2,2,'leaf').dot(x+a,y+b-1,'sage');
|
|
if(i===1) d.oval(x,y-5,2,2,'cream').dot(x,y-6,'rose');
|
|
}
|
|
} else if(kind==='nettle') {
|
|
for(const [x,top] of [[10,12],[19,5],[25,17]]) {
|
|
d.line(x,30,x-1,dormant?top+9:top,stem);
|
|
for(let y=dormant?23:top+4;y<29;y+=5) {
|
|
if(dormant) d.line(x,y,x+3,y-2,mid);
|
|
else for(const s of [-1,1]) d.poly([[x,y],[x+s*5,y-4],[x+s*4,y-1],[x+s*6,y],[x+s*2,y+2]],'leaf')
|
|
.line(x,y,x+s*4,y-2,light);
|
|
}
|
|
}
|
|
} else if(kind==='heather') {
|
|
for(const [x,y,bend] of [[6,17,-2],[12,10,-3],[18,8,2],[25,15,3]]) {
|
|
d.line(16,30,x,y+5,stem).line(x,y+5,x+bend,dormant?y+3:y-4,stem);
|
|
for(let j=0;j<(dormant?2:5);j++) {
|
|
d.line(x,y+j*2,x-2,y+j*2+1,mid);
|
|
if(!dormant) d.rect(x+bend,y-4+j*2,2,2,j%2?'rose':'wine').dot(x+bend,y-4+j*2,'bud');
|
|
}
|
|
}
|
|
} else if(kind==='bluebell') {
|
|
for(const [x,y,s] of [[9,11,1],[19,5,1],[24,17,-1]]) {
|
|
d.line(x,30,x,dormant?y+13:y+2,stem);
|
|
d.line(x,28,x-4,dormant?25:19,mid);
|
|
if(!dormant) {
|
|
d.line(x,y+2,x+s*4,y,mid).line(x+s*4,y,x+s*6,y+3,mid);
|
|
for(let j=0;j<3;j++) d.poly([[x+s*5,y+3+j*5],[x+s*8,y+3+j*5],[x+s*9,y+7+j*5],
|
|
[x+s*4,y+7+j*5]],'slateBlue').line(x+s*5,y+4+j*5,x+s*5,y+6+j*5,'mauve');
|
|
} else d.dot(x,dormant?y+13:y,'bark');
|
|
}
|
|
} else { // Sedge fans versus erect, cylindrical bulrush heads.
|
|
for(const [i,[x,top,bend]] of [[6,12,-3],[12,6,-2],[17,9,2],[23,4,1],[27,17,2]].entries()) {
|
|
const tip=dormant?top+10:top;
|
|
d.line(x,30,x+bend,tip,stem).line(x+1,29,x+bend+1,tip+2,mid);
|
|
if(kind==='bulrush' && i%2===1) {
|
|
d.line(x+bend,tip+2,x+bend,tip-3,'bark',2).dot(x+bend,tip-3,light);
|
|
if(dormant) d.line(x+bend,tip-1,x+bend+3,tip+1,'earthLight');
|
|
} else if(!dormant && i%2===0) d.line(x+bend,tip,x+bend+2,tip+3,'leafLight');
|
|
}
|
|
}
|
|
}
|
|
|
|
function habitatTile(d,name) {
|
|
if(name==='snow') {
|
|
d.rect(0,0,32,32,'snow');
|
|
for(const [i,[x,y]] of GRAINS.entries()) if(i%3===0)
|
|
d.line(x,y,x+5,y-1,'snowShade').line(x+1,y-1,x+4,y-2,'snowGlow');
|
|
return;
|
|
}
|
|
if(name==='gravel' || name==='scree') {
|
|
d.rect(0,0,32,32,name==='gravel'?'loamShade':'slateShade');
|
|
GRAINS.forEach(([x,y],i)=> {
|
|
if(name==='gravel') d.oval(x,y,2+i%2,1.5,'mineralShade').line(x-1,y-1,x+1,y-1,i%3?'mineralGlow':'sand');
|
|
else d.poly([[x-2,y+2],[x,y-3],[x+4,y-2],[x+2,y+2]],i%2?'slateBlue':'mineral')
|
|
.line(x,y-2,x+2,y-2,'slateGlow');
|
|
}); return;
|
|
}
|
|
earth(d);
|
|
if(name==='alpine_turf') {
|
|
grass(d);
|
|
for(const [i,[x,y]] of GRAINS.entries()) if(i%3===0)
|
|
d.oval(x,y,2,1,'slateShade').line(x-1,y-1,x+1,y-1,'mineralGlow');
|
|
return;
|
|
}
|
|
if(name==='alluvium') {
|
|
for(const [i,[x,y]] of GRAINS.entries()) if(i%2===0)
|
|
d.line(x,y,x+4,y-1,'earthLight').line(x+1,y+1,x+3,y,'loamGlow');
|
|
d.oval(23,16,3,2,'mineralShade').line(22,15,24,15,'sand'); return;
|
|
}
|
|
if(name==='peat') d.rect(0,0,32,32,'humus');
|
|
for(const [i,[x,y]] of GRAINS.entries()) {
|
|
if(name==='podzol') d.line(x,y,x+3,y+2,i%3?'bark':'strawDark').line(x+1,y,x+3,y+1,'loamGlow');
|
|
else if(name==='peat') d.line(x,y,x+2,y-2,i%3?'loamShade':'bark').dot(x+3,y-3,'loamGlow');
|
|
else { // Forest floor: fragmentary fallen leaves in humus, no giant symbols.
|
|
d.poly([[x-1,y],[x+2,y-2],[x+4,y],[x+1,y+2]],i%3?'humus':'barkLight');
|
|
if(i%3===0) d.line(x,y,x+2,y-1,'strawDark');
|
|
}
|
|
}
|
|
}
|
|
|
|
function worldTile(name) {
|
|
const d=new Drawing();
|
|
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);
|
|
else if(part==='log_side') bark(d,species);
|
|
else planks(d,false,species);
|
|
if(species==='oak' && part!=='leaves') {
|
|
if(part==='log_top') d.line(21,5,20,10,'barkLight');
|
|
else if(part==='log_side') d.line(23,14,24,18,'barkDark').dot(25,15,'barkLight');
|
|
else d.oval(20,12,2,1,'barkLight').dot(20,12,'wood');
|
|
}
|
|
return d;
|
|
}
|
|
if(UNDERSTORY_NAMES.includes(name.replace(/_dormant$/,''))) { understory(d,name); return d; }
|
|
if(ROCK_NAMES.includes(name)) { mineral(d,name); return d; }
|
|
if(name.endsWith('_bricks') && ROCK_NAMES.includes(name.slice(0,-7))) {
|
|
const rock=name.slice(0,-7); mineral(d,rock);
|
|
const joint={granite:'mineralShade',basalt:'basaltShade',limestone:'limeShade',slate:'slateShade',sandstone:'sandShade'}[rock];
|
|
const edge={granite:'mineralGlow',basalt:'basaltGlow',limestone:'limeGlow',slate:'slateGlow',sandstone:'sandGlow'}[rock];
|
|
for(const [row,y] of [0,11,22].entries()) {
|
|
d.line(0,y,31,y,joint).line(1,y+1,30,y+1,edge);
|
|
for(const x of row%2?[8,25]:[0,17]) d.line(x,y,x,y+10,joint);
|
|
}
|
|
return d;
|
|
}
|
|
if(['forest_floor','podzol','alluvium','peat','alpine_turf','gravel','scree','snow'].includes(name)) {
|
|
habitatTile(d,name); return d;
|
|
}
|
|
if(['quartz_vein','copper_ore_vein','coal_ore_vein'].includes(name)) {
|
|
mineral(d,name==='quartz_vein'?'granite':name==='coal_ore_vein'?'sandstone':'basalt');
|
|
if(name==='quartz_vein') {
|
|
d.line(1,28,12,16,'stoneLight',2).line(12,16,19,14,'stoneLight',2).line(19,14,30,1,'stoneLight',2);
|
|
for(const [x,y] of [[5,25],[13,16],[22,10],[28,4]])
|
|
d.poly([[x-1,y],[x,y-4],[x+3,y-3],[x+3,y+1]],'cream').line(x,y-3,x+1,y,'moon');
|
|
} else if(name==='copper_ore_vein') for(const [x,y] of [[4,7],[10,11],[15,17],[20,20],[27,27]])
|
|
d.poly([[x-2,y],[x,y-3],[x+3,y-2],[x+2,y+2]],'copperShade').line(x,y-2,x+2,y-2,'copperMetal')
|
|
.line(x-1,y+1,x+1,y+1,'verdigris');
|
|
else {
|
|
d.poly([[0,11],[7,10],[14,14],[23,13],[32,17],[32,21],[24,18],[14,19],[6,14],[0,15]],'ink');
|
|
d.line(2,12,9,13,'slate').line(16,16,22,15,'slate').line(26,18,30,19,'slate');
|
|
}
|
|
return d;
|
|
}
|
|
if(PLANT_NAMES.includes(name)) { plant(d,name); return d; }
|
|
switch(name) {
|
|
case 'stone': mineral(d); break;
|
|
case 'foundation': case 'weir_stone':
|
|
masonry(d,name==='foundation');
|
|
if(name==='weir_stone') d.line(0,25,31,25,'waterLight').line(2,27,12,27,'water');
|
|
break;
|
|
case 'soil': earth(d); break;
|
|
case 'grass_top': grass(d); break;
|
|
case 'grass_side': grass(d,true); break;
|
|
case 'sand':
|
|
d.rect(0,0,32,32,'sand');
|
|
for(let y=0;y<32;y++) for(let x=0;x<32;x++) {
|
|
const u=x===31?0:x, ripple=y+1.4*Math.sin(u*Math.PI*2/31)+.45*Math.sin(u*Math.PI*4/31);
|
|
const phase=(ripple+32)%8;
|
|
if(phase<.8) d.dot(x,y,'sandShade');
|
|
else if(phase>6.9) d.dot(x,y,'sandGlow');
|
|
}
|
|
for(const [x,y] of [[8,11],[21,25],[15,4],[27,17]]) d.rect(x,y,2,1,'sandShade');
|
|
break;
|
|
case 'log_top': endgrain(d); break;
|
|
case 'log_side': bark(d); break;
|
|
case 'planks': planks(d); break;
|
|
case 'wall':
|
|
d.rect(0,0,32,32,'clay');
|
|
for(const y of [4,14,25]) d.line(1,y,11,y,'earthLight').line(20,y+2,29,y+2,'wood');
|
|
d.rect(0,0,3,32,'bark').rect(29,0,3,32,'bark').rect(3,27,26,3,'wood');
|
|
d.line(8,8,11,11,'earth').line(11,11,9,16,'earth'); break;
|
|
case 'roof': case 'thatch': straw(d); if(name==='thatch') d.line(0,30,31,30,'strawLight'); break;
|
|
case 'frame':
|
|
d.rect(0,0,32,32,'earthDark').rect(4,4,24,24,'clay');
|
|
for(const x of [0,27]) d.rect(x,0,5,32,'bark').line(x+1,1,x+1,30,'wood');
|
|
for(const y of [0,27]) d.rect(0,y,32,5,'barkLight').line(2,y+1,29,y+1,'woodLight');
|
|
d.line(5,26,26,5,'bark',3).line(6,26,26,6,'wood'); break;
|
|
case 'stairs':
|
|
planks(d); for(const y of [0,8,16,24]) d.rect(0,y,32,2,'barkDark').line(0,y+2,31,y+2,'woodLight'); break;
|
|
case 'door':
|
|
planks(d,true); d.rect(1,2,30,3,'barkDark').rect(1,26,30,3,'barkDark');
|
|
d.line(3,24,27,7,'bark',3).line(4,24,28,7,'wood');
|
|
d.rect(24,16,3,4,'outline').dot(25,17,'gold'); break;
|
|
case 'window':
|
|
d.rect(3,3,26,26,'glass',95).line(7,13,12,8,'moon').line(19,24,24,19,'waterLight');
|
|
for(const x of [1,15,29]) d.rect(x,1,2,30,'barkLight');
|
|
for(const y of [1,15,29]) d.rect(1,y,30,2,'wood'); break;
|
|
case 'vent':
|
|
planks(d); d.rect(5,5,22,22,'barkDark');
|
|
for(const y of [6,12,18,24]) d.rect(6,y,20,2,'wood').line(7,y,25,y,'woodLight'); break;
|
|
case 'storage': case 'pantry':
|
|
planks(d,true); d.rect(1,2,30,4,'barkDark').rect(1,25,30,5,'barkDark');
|
|
d.line(2,3,29,3,'woodLight').rect(14,12,5,9,'outline').rect(16,14,1,3,'gold');
|
|
if(name==='pantry') {
|
|
d.rect(3,7,26,16,'earthDark');
|
|
for(const x of [7,15,23]) d.oval(x,16,3,5,'clay').rect(x-2,10,4,2,'wood');
|
|
d.line(3,22,28,22,'woodLight');
|
|
}
|
|
break;
|
|
case 'clay_bank':
|
|
d.rect(0,0,32,32,'clay');
|
|
for(const y of [3,13,24]) d.poly([[0,y],[10,y+2],[20,y],[32,y],[32,y+3],[20,y+3],[10,y+5],[0,y+3]],'earthLight');
|
|
d.line(4,8,10,9,'wood').line(21,19,28,18,'wood'); break;
|
|
case 'iron_ore_vein':
|
|
mineral(d); d.poly([[1,27],[8,18],[12,19],[18,10],[23,11],[29,2],[31,6],[25,15],[21,15],[15,24],[9,23],[4,30]],'earthDark');
|
|
for(const [x,y] of [[5,25],[11,20],[18,14],[24,9],[28,4]])
|
|
d.poly([[x-2,y],[x,y-3],[x+3,y-2],[x+2,y+2]],'rust').line(x,y-2,x+2,y-2,'iron'); break;
|
|
case 'water':
|
|
d.rect(0,0,32,32,'water');
|
|
// Opposite edge pixels agree; small, separated horizontal wavelets.
|
|
for(const [x,y,w] of [[4,5,8],[19,13,9],[7,23,11]])
|
|
d.line(x,y,x+w,y,'waterLight').line(x+2,y+1,x+w+2,y+1,'waterDark');
|
|
break;
|
|
case 'fire': case 'fire_lit':
|
|
d.oval(16,22,14,8,'outline').oval(16,21,11,5,'earthDark');
|
|
for(const [x,y] of [[5,19],[10,26],[22,26],[27,19],[16,15]]) d.oval(x,y,3,2,'stone').dot(x-1,y-1,'stoneLight');
|
|
d.line(9,24,24,18,'bark',3).line(8,18,23,24,'barkLight',2);
|
|
if(name==='fire_lit') flame(d,16,20,1); break;
|
|
case 'torch':
|
|
d.line(13,29,18,12,'barkDark',3).line(14,28,19,12,'wood');
|
|
d.line(16,15,21,16,'straw',2); flame(d,19,12,.65); break;
|
|
case 'sticks':
|
|
d.line(5,25,24,6,'barkDark',3).line(6,25,25,6,'wood').line(11,11,25,26,'bark',3).line(12,11,26,26,'barkLight');
|
|
d.line(14,17,9,12,'bark',2); break;
|
|
case 'loose_stone':
|
|
d.poly([[5,24],[7,16],[16,12],[25,17],[28,24],[22,28],[10,28]],'slate');
|
|
d.poly([[7,20],[10,16],[16,14],[23,17],[20,23],[12,24]],'stone').line(11,17,16,16,'stoneLight'); break;
|
|
case 'bedroll':
|
|
d.poly([[7,7],[25,7],[28,27],[4,27]],'outline').rect(7,8,18,17,'leaf');
|
|
d.rect(8,10,16,3,'sage').line(9,16,23,16,'leafDark').line(9,22,23,22,'leafDark');
|
|
d.oval(16,7,9,4,'barkLight').oval(16,6,8,3,'wood').line(6,27,26,27,'wood'); break;
|
|
case 'drying_rack':
|
|
d.line(5,30,8,4,'bark',3).line(24,4,27,30,'bark',3).line(6,5,25,5,'wood',2);
|
|
for(const x of [11,17,23]) { d.line(x,6,x,12,'straw'); fish(d,x,18,.45,'wood'); } break;
|
|
case 'fish_trap':
|
|
d.poly([[4,11],[18,5],[28,13],[28,24],[14,29],[4,22]],'barkDark');
|
|
for(let x=5;x<29;x+=4) d.line(x,12,x,24,'straw');
|
|
for(const y of [12,16,20,24]) d.line(5,y,27,y,'wood');
|
|
d.oval(15,10,10,5,'wood').oval(15,10,7,3,'earthDark').oval(15,10,3,2,'bark'); break;
|
|
default: throw new Error('Unknown world art name.');
|
|
}
|
|
return d;
|
|
}
|
|
function flame(d,x,y,s) {
|
|
const points=[[0,2],[-7,-1],[-6,-7],[-2,-10],[0,-19],[4,-12],[4,-6],[8,-9],[7,-2],[3,2]];
|
|
d.poly(points.map(([a,b])=>[x+a*s,y+b*s]),'clay');
|
|
d.poly([[-4,0],[-3,-7],[0,-12],[2,-5],[4,-2],[2,1]].map(([a,b])=>[x+a*s,y+b*s]),'peach');
|
|
d.poly([[-2,0],[0,-7],[2,-1],[1,1]].map(([a,b])=>[x+a*s,y+b*s]),'gold');
|
|
}
|
|
function fish(d,x=16,y=16,s=1,color='iron') {
|
|
d.poly([[x-9*s,y],[x-14*s,y-5*s],[x-14*s,y+5*s]],'waterLight');
|
|
d.oval(x,y,10*s,5*s,'outline').oval(x,y-s,9*s,4*s,color);
|
|
d.line(x-4*s,y-3*s,x+4*s,y-3*s,color==='iron'?'moon':'woodLight');
|
|
d.dot(x+6*s,y-s,'ink').line(x-s,y+2*s,x+2*s,y+2*s,'bark');
|
|
}
|
|
function jar(d,water=null) {
|
|
d.poly([[11,8],[21,8],[21,13],[25,18],[24,27],[8,27],[7,18],[11,13]],'earthDark');
|
|
d.poly([[12,9],[20,9],[19,14],[23,19],[22,25],[10,25],[9,19],[13,14]],'clay');
|
|
d.line(11,18,11,23,'woodLight').rect(10,7,12,3,'barkLight').rect(12,7,8,1,'earthDark');
|
|
if(water) d.rect(12,18,8,5,water).line(13,18,18,18,'waterLight');
|
|
}
|
|
function lump(d,color='clay') {
|
|
d.poly([[5,22],[8,13],[16,9],[25,14],[28,23],[20,28],[9,27]],'outline');
|
|
d.poly([[7,21],[10,14],[16,11],[24,15],[25,22],[19,25],[10,25]],color);
|
|
d.poly([[10,14],[16,11],[22,15],[16,18],[9,19]],color==='clay'?'wood':'slate');
|
|
}
|
|
function food(d,cooked=false,dried=false) {
|
|
d.poly([[5,21],[8,11],[17,7],[26,14],[25,24],[17,28],[8,26]],'earthDark');
|
|
d.poly([[7,21],[10,12],[17,10],[24,15],[23,23],[16,25],[9,24]],cooked?'barkLight':'rose');
|
|
d.line(12,14,19,20,dried?'straw':'cream',2).line(11,20,15,23,cooked?'earth':'berry');
|
|
if(dried) for(const x of [10,15,20]) d.line(x,16,x+2,21,'barkDark');
|
|
}
|
|
function itemTile(name) {
|
|
const d=new Drawing();
|
|
if(name.endsWith('_charge')) {
|
|
const base={fish_drying_charge:'raw_fish',charcoal_charge:'charcoal',smelt_charge:'iron_ore',
|
|
clay_jar_charge:'clay_jar',dried_food_charge:'raw_food',copper_charge:'copper_ore'}[name];
|
|
d.data.set(itemTile(base).data);
|
|
d.line(6,25,25,7,'straw',2).line(7,7,25,25,'woodLight');
|
|
d.rect(20,22,9,7,'outline').rect(21,23,7,5,'gold').line(22,24,26,24,'bark');
|
|
return d;
|
|
}
|
|
switch(name) {
|
|
case 'quartz':
|
|
d.poly([[7,25],[6,14],[10,9],[14,12],[16,5],[21,3],[25,8],[24,24],[17,28]],'slateBlue');
|
|
d.poly([[8,23],[8,14],[10,11],[13,14],[14,24]],'cream');
|
|
d.poly([[15,24],[17,6],[21,4],[22,9],[21,25]],'moon');
|
|
d.poly([[22,9],[24,9],[23,23],[21,25]],'stoneLight');
|
|
d.line(18,9,17,21,'snowGlow'); break;
|
|
case 'copper_ore':
|
|
lump(d,'basalt');
|
|
for(const [x,y] of [[11,18],[18,14],[21,23]]) d.poly([[x-2,y],[x,y-3],[x+3,y-1],[x+1,y+2]],'copperShade')
|
|
.line(x-1,y-1,x+1,y-2,'copperMetal').dot(x+2,y+1,'verdigris'); break;
|
|
case 'coal':
|
|
lump(d,'ink');
|
|
d.poly([[8,21],[14,13],[17,19],[13,25]],'basaltShade').line(14,13,16,16,'slateBlue');
|
|
d.line(19,13,22,17,'mineralShade').line(20,23,23,21,'basaltGlow'); break;
|
|
case 'copper_ingot':
|
|
d.poly([[5,16],[20,10],[28,15],[25,23],[10,28],[5,24]],'copperShade');
|
|
d.poly([[5,16],[20,10],[28,15],[11,22]],'copperMetal').line(7,16,20,11,'copperGlow');
|
|
d.line(12,24,24,19,'copperMetal'); break;
|
|
case 'copper_pick':
|
|
d.line(8,27,22,9,'barkDark',3).line(9,27,23,9,'wood');
|
|
d.poly([[4,12],[8,7],[15,5],[22,7],[28,12],[23,11],[16,9],[10,10]],'copperShade');
|
|
d.line(7,10,14,7,'copperGlow').line(14,7,21,8,'copperMetal').line(21,8,26,10,'copperMetal');
|
|
d.line(10,24,13,26,'straw'); break;
|
|
case 'spring_greens':
|
|
seasonalPlant(d,name);
|
|
d.line(12,26,21,25,'straw',2).line(14,29,20,28,'wood'); break;
|
|
case 'autumn_mushrooms':
|
|
seasonalPlant(d,name);
|
|
d.line(9,28,24,28,'bark').line(12,29,21,29,'wood'); break;
|
|
case 'berries':
|
|
d.line(8,14,20,7,'barkLight').line(20,7,23,13,'bark'); leaf(d,17,10,false);
|
|
for(const [x,y] of [[9,18],[18,16],[23,23],[13,25]])
|
|
d.oval(x,y,5,5,'outline').oval(x,y-1,4,4,'berry').rect(x-2,y-3,2,2,'berryLight'); break;
|
|
case 'grain':
|
|
for(const x of [10,16,22]) {
|
|
d.line(15,28,x,6,'strawDark');
|
|
for(let y=7;y<18;y+=3) d.line(x,y,x-3,y-2,'straw',2).line(x+1,y+1,x+3,y-1,'strawLight');
|
|
}
|
|
d.line(11,23,20,22,'bark',2); break;
|
|
case 'grain_seed':
|
|
for(const [x,y] of [[10,12],[20,10],[15,19],[8,25],[24,23]])
|
|
d.oval(x,y,3,2,'strawDark').line(x-1,y-1,x+1,y-1,'strawLight'); break;
|
|
case 'raw_food': food(d); break;
|
|
case 'cooked_food': food(d,true); break;
|
|
case 'dried_food': food(d,true,true); break;
|
|
case 'fish_bait':
|
|
d.line(7,21,11,25,'earthDark',4).line(11,25,17,20,'clay',3).line(17,20,17,13,'clay',3);
|
|
d.line(17,13,23,10,'rose',3).line(23,10,26,14,'rose',2).dot(25,12,'woodLight'); break;
|
|
case 'raw_fish': fish(d); break;
|
|
case 'cooked_fish': fish(d,16,16,1,'clay'); d.line(12,13,14,18,'earth').line(17,13,19,18,'earth'); break;
|
|
case 'dried_fish': fish(d,16,16,1,'straw'); for(const x of [11,16,21]) d.line(x,13,x,18,'bark'); break;
|
|
case 'iron_ore': lump(d,'stone'); d.line(10,23,22,13,'rust',3).line(12,23,23,14,'iron'); break;
|
|
case 'iron_ingot':
|
|
d.poly([[5,16],[20,10],[28,15],[25,23],[10,28],[5,24]],'slate');
|
|
d.poly([[5,16],[20,10],[28,15],[11,22]],'iron').line(7,16,20,11,'moon');
|
|
d.poly([[11,22],[28,15],[25,23],[10,28]],'waterLight'); break;
|
|
case 'charcoal': lump(d,'ink'); d.line(11,15,16,18,'slate').line(18,13,22,20,'barkDark'); break;
|
|
case 'clay': lump(d); break;
|
|
case 'clay_jar': jar(d); break;
|
|
case 'vessel':
|
|
d.oval(25,17,5,6,'barkLight').oval(25,17,2,3,'outline');
|
|
d.poly([[7,10],[23,10],[22,26],[9,26]],'bark').rect(9,11,12,13,'wood');
|
|
d.oval(15,10,8,3,'woodLight').oval(15,10,6,1,'earthDark'); break;
|
|
case 'water_raw': jar(d,'water'); d.dot(14,21,'sage').dot(18,20,'earth'); break;
|
|
case 'water_clean': jar(d,'waterLight'); d.line(16,15,16,21,'moon').line(13,18,19,18,'moon'); break;
|
|
case 'cloak':
|
|
d.poly([[12,4],[20,4],[23,11],[29,27],[20,26],[16,29],[12,26],[3,27],[9,11]],'outline');
|
|
d.poly([[13,5],[19,5],[21,12],[26,25],[19,24],[16,27],[12,24],[6,25],[11,12]],'leaf');
|
|
d.line(12,13,8,23,'sage').line(18,14,21,23,'leafDark').oval(16,8,3,3,'barkDark').dot(17,12,'gold'); break;
|
|
case 'firestarter':
|
|
d.poly([[5,24],[7,14],[13,10],[18,16],[15,25]],'slate').line(8,16,13,12,'stoneLight');
|
|
d.line(17,26,25,8,'bark',3).line(18,26,26,8,'wood');
|
|
d.line(18,7,18,11,'gold').line(13,5,15,7,'peach'); break;
|
|
case 'iron_axe':
|
|
d.line(9,26,22,8,'barkDark',4).line(10,26,23,8,'wood',2);
|
|
d.poly([[8,6],[16,3],[25,8],[24,15],[17,17],[13,12],[8,12]],'outline');
|
|
d.poly([[9,7],[16,5],[23,9],[22,13],[17,15],[14,10],[9,10]],'iron').line(9,7,16,5,'moon');
|
|
d.line(12,23,15,25,'straw',2); break;
|
|
default: throw new Error('Unknown item art name.');
|
|
}
|
|
return d;
|
|
}
|
|
|
|
function uiTile(name) {
|
|
const d=new Drawing();
|
|
if(name==='crosshair') {
|
|
for(const [x,y,w,h] of [[15,9,3,5],[15,19,3,5],[9,15,5,3],[19,15,5,3]])
|
|
d.rect(x,y,w,h,'deep');
|
|
d.line(16,10,16,12,'cream').line(16,20,16,22,'cream').line(10,16,12,16,'cream').line(20,16,22,16,'cream');
|
|
} else {
|
|
if(name==='hotbar') d.rect(1,1,30,30,'deep',220).rect(3,3,26,26,'ink',200);
|
|
for(const [x,y,w,h] of [[1,1,30,1],[1,30,30,1],[1,1,1,30],[30,1,1,30]])
|
|
d.rect(x,y,w,h,name==='selected'?'gold':'slate');
|
|
if(name==='selected') for(const [x,y] of [[3,3],[25,3],[3,25],[25,25]])
|
|
d.line(x,y,x+3,y,'mauve').line(x,y,x,y+3,'mauve');
|
|
else d.line(4,3,27,3,'stone').line(3,4,3,27,'slate');
|
|
}
|
|
return d;
|
|
}
|
|
|
|
// Atlas patches are 8x8, UV-inset by half a pixel. Every face has a semantic
|
|
// patch, never an unstable automatically-unwrapped coordinate or external MTL.
|
|
const FUR_PATCHES = ['coat','belly','dark','nose','eye','ear','horn','hoof'];
|
|
function animalTexture(species) {
|
|
const d=new Drawing('outline'), coat={hare:'fur',fox:'fox',deer:'deer'}[species];
|
|
FUR_PATCHES.forEach((patch,i)=> {
|
|
const x=(i%4)*8, y=Math.floor(i/4)*8;
|
|
const base={coat,belly:'cream',dark:'barkDark',nose:'ink',eye:'ink',ear:'rose',horn:'wood',hoof:'barkDark'}[patch];
|
|
d.rect(x,y,8,8,base);
|
|
if(patch==='coat') d.rect(x+1,y+1,5,2,species==='fox'?'clay':'barkLight').rect(x+4,y+5,3,2,'bark');
|
|
if(patch==='belly') d.line(x+1,y+6,x+6,y+6,'woodLight');
|
|
if(patch==='eye') d.rect(x+1,y+1,2,2,'cream');
|
|
if(patch==='ear') d.rect(x,y,2,8,coat).line(x+6,y,x+6,y+7,coat);
|
|
if(patch==='horn') d.line(x+2,y,x+2,y+7,'woodLight');
|
|
});
|
|
return d;
|
|
}
|
|
|
|
export const ANIMAL_NAMES = Object.freeze(['hare','fox','deer']);
|
|
// All model coordinates use Luanti's BS=10 convention. Origin is ground level,
|
|
// forward +Z, visual_size={x=1,y=1}; bounds below are in actual node units.
|
|
function animalParts(species) {
|
|
const parts=[];
|
|
const add=(name,center,size,patch='coat',tilt=0,taper=1)=>parts.push({name,center,size,patch,tilt,taper});
|
|
if(species==='hare') {
|
|
add('haunch',[0,2.4,-.7],[3.3,3.8,4.2]); add('chest',[0,2.5,1.3],[2.5,2.8,2.4]);
|
|
add('head',[0,4,2.4],[2.6,2.4,2.5]); add('muzzle',[0,3.5,3.65],[1.5,1,1.2],'belly',0,.65);
|
|
add('ear_left',[-.8,6.5,2],[.7,3.5,.75],'ear',-.12,.6); add('ear_right',[.8,6.6,2],[.7,3.7,.75],'ear',.12,.6);
|
|
add('tail',[0,2.7,-3],[1.4,1.4,1.4],'belly');
|
|
for(const s of [-1,1]) {
|
|
add(`hind_foot_${s}`,[s*1.25,.4,-.6],[1.2,.8,2.7],'belly');
|
|
add(`foreleg_${s}`,[s*.85,.8,2],[.65,1.6,.8]);
|
|
add(`eye_${s}`,[s*1.32,4.35,2.8],[.16,.5,.5],'eye');
|
|
}
|
|
add('nose',[0,3.8,4.22],[.5,.4,.15],'nose');
|
|
} else if(species==='fox') {
|
|
add('body',[0,3.2,0],[3.1,3.2,6]); add('bib',[0,3.2,2.2],[2.6,2.7,1.7],'belly');
|
|
add('neck',[0,4.4,2.4],[2.4,2.3,2]); add('head',[0,5.2,3.4],[2.7,2.1,2.4]);
|
|
add('muzzle',[0,4.8,4.9],[1.7,1.3,1.8],'belly',0,.4);
|
|
for(const s of [-1,1]) {
|
|
add(`ear_${s}`,[s*.85,6.9,3.2],[1,1.8,.85],'dark',s*.2,.2);
|
|
add(`eye_${s}`,[s*1.37,5.5,3.85],[.16,.45,.6],'eye');
|
|
for(const z of [-2,1.9]) add(`leg_${s}_${z}`,[s*1.05,1.1,z],[.65,2.2,.8],'dark');
|
|
}
|
|
add('tail',[0,3,-4.8],[2.4,2.3,4.2],'coat',-.28,.6);
|
|
add('tail_tip',[0,2.2,-7.2],[1.8,1.7,1.6],'belly',-.28,.4);
|
|
add('nose',[0,4.8,5.83],[.7,.55,.18],'nose');
|
|
} else if(species==='deer') {
|
|
add('body',[0,6,0],[4,4,7.5]); add('belly',[0,4.6,.3],[3.3,1.5,6],'belly');
|
|
add('neck',[0,8.5,3.2],[2,5.2,2.4],'coat',.28,.7);
|
|
add('head',[0,11,4.4],[2,2.1,3.4],'coat',0,.7);
|
|
add('muzzle',[0,10.5,6],[1.4,1.2,1.5],'dark',0,.8);
|
|
add('tail',[0,6.7,-4.1],[1.1,1.8,1.1],'belly',-.35);
|
|
for(const s of [-1,1]) {
|
|
for(const z of [-2.7,2.4]) {
|
|
add(`leg_${s}_${z}`,[s*1.35,2.4,z],[.65,4.8,.8]);
|
|
add(`hoof_${s}_${z}`,[s*1.35,.35,z+.1],[.8,.7,1],'hoof');
|
|
}
|
|
add(`ear_${s}`,[s*1.7,11.8,3.9],[2.2,.65,1.1],'ear',s*.2,.6);
|
|
add(`eye_${s}`,[s*1.02,11.3,5],[.14,.45,.55],'eye');
|
|
add(`antler_${s}`,[s*.85,13.3,3.9],[.35,3.2,.4],'horn',s*.15,.7);
|
|
add(`tine_${s}`,[s*1.4,13.8,4.1],[1.4,.3,.35],'horn');
|
|
add(`point_${s}`,[s*2,14.4,4.1],[.25,1.4,.3],'horn');
|
|
}
|
|
} else throw new Error('Unknown animal art name.');
|
|
return parts;
|
|
}
|
|
function partVertices({center:[cx,cy,cz],size:[w,h,l],tilt,taper}) {
|
|
return [[-1,-1,-1],[1,-1,-1],[1,1,-1],[-1,1,-1],[-1,-1,1],[1,-1,1],[1,1,1],[-1,1,1]].map(([x,y,z])=> {
|
|
const a=x*w/2*(y>0?taper:1), b=y*h/2, c=z*l/2;
|
|
return [cx+a,cy+b*Math.cos(tilt)-c*Math.sin(tilt),cz+b*Math.sin(tilt)+c*Math.cos(tilt)];
|
|
});
|
|
}
|
|
export function animalOBJ(species) {
|
|
const lines=[`# Original Last Hearth ${species}; BS=10; +Z forward; feet Y=0`];
|
|
let count=0;
|
|
for(const part of animalParts(species)) {
|
|
lines.push(`g ${part.name}`);
|
|
for(const v of partVertices(part)) lines.push(`v ${v.map(n=>Number(n.toFixed(6))).join(' ')}`);
|
|
const tile=FUR_PATCHES.indexOf(part.patch), x=(tile%4)*8, y=Math.floor(tile/4)*8;
|
|
for(const [u,v] of [[.5,7.5],[7.5,7.5],[7.5,.5],[.5,.5]]) lines.push(`vt ${(x+u)/32} ${1-(y+v)/32}`);
|
|
const faces=[[1,4,3,2],[5,6,7,8],[1,2,6,5],[4,8,7,3],[1,5,8,4],[2,3,7,6]];
|
|
const uv=count/8*4;
|
|
for(const f of faces) for(const tri of [[0,1,2],[0,2,3]])
|
|
lines.push(`f ${tri.map(i=>`${count+f[i]}/${uv+i+1}`).join(' ')}`);
|
|
count+=8;
|
|
}
|
|
return lines.join('\n')+'\n';
|
|
}
|
|
export function animalBounds(species) {
|
|
const vertices=animalParts(species).flatMap(partVertices);
|
|
return Object.freeze({min: Object.freeze([0,1,2].map(i=>Math.min(...vertices.map(v=>v[i]))/10)),
|
|
max: Object.freeze([0,1,2].map(i=>Math.max(...vertices.map(v=>v[i]))/10)),
|
|
visualSize: Object.freeze({x:1,y:1})});
|
|
}
|
|
|
|
// Transcribed from the actual botany.lua catalogue, not guessed from common
|
|
// names. Latin identity and diagnostic morphology travel with every sprite.
|
|
const BOTANY_GROUPS = {
|
|
trees: [
|
|
['oak','Quercus robur','rounded leaf lobes; stalked acorns'],
|
|
['birch','Betula pendula','serrate diamonds; pendant catkins'],
|
|
['pine','Pinus sylvestris','paired long needles; rounded cones'],
|
|
['willow','Salix alba','pendant lanceolate leaves; catkins'],
|
|
['alder','Alnus glutinosa','notched round leaves; woody conelets'],
|
|
['beech','Fagus sylvatica','oval parallel-veined leaves; pointed buds'],
|
|
['aspen','Populus tremula','scalloped round leaves; long petioles'],
|
|
['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'],
|
|
['blackberry','Rubus plicatus','arching prickly cane; palmate leaves; dark aggregate fruit'],
|
|
['raspberry','Rubus idaeus','upright canes; pale pinnate leaves; red aggregate fruit'],
|
|
['elder','Sambucus nigra','opposite pinnate leaves; broad dark fruit corymb'],
|
|
['blackthorn','Prunus spinosa','rigid spines; small leaves; blue sloes'],
|
|
['hawthorn','Crataegus monogyna','short thorns; deeply lobed leaves; red haws'],
|
|
['dog_rose','Rosa canina','curved prickles; five petals; elongated hips'],
|
|
['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'],
|
|
['bulrush','Typha latifolia','flat strap leaves; cylindrical brown reedmace'],
|
|
['meadow_fescue','Schedonorus pratensis','narrow blades; open nodding panicle'],
|
|
['cocksfoot','Dactylis glomerata','one-sided clustered panicle; folded blades'],
|
|
['timothy','Phleum pratense','long tight cylindrical green flower spike'],
|
|
['sweet_vernal_grass','Anthoxanthum odoratum','short golden ovoid spike; sparse leaves'],
|
|
['yorkshire_fog','Holcus lanatus','soft broad blades; dense pink-tinged panicle'],
|
|
['tufted_hairgrass','Deschampsia cespitosa','fine radiating airy panicle; dense basal tuft'],
|
|
['common_reed','Phragmites australis','tall jointed culm; broad leaves; feathery plume'],
|
|
['soft_rush','Juncus effusus','smooth round stems; lateral brown inflorescence'],
|
|
],
|
|
herbs: [
|
|
['clover','Trifolium repens','creeping stolon; trifoliate leaves; round white head'],
|
|
['nettle','Urtica dioica','opposite toothed leaves; drooping green flower strings'],
|
|
['heather','Calluna vulgaris','woody branching sprays; tiny scale leaves; pink bells'],
|
|
['bluebell','Hyacinthoides non-scripta','arching one-sided raceme; drooping blue bells'],
|
|
['yarrow','Achillea millefolium','finely divided leaves; flat cream corymb'],
|
|
['foxglove','Digitalis purpurea','basal rosette; tall one-sided tubular purple bells'],
|
|
['meadowsweet','Filipendula ulmaria','pinnate serrate leaves; frothy cream plume'],
|
|
['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'],
|
|
['lady_fern','Athyrium filix-femina','delicate tapering bipinnate frond'],
|
|
['male_fern','Dryopteris filix-mas','robust lanceolate pinnate frond; basal crown'],
|
|
['harts_tongue','Asplenium scolopendrium','undivided glossy strap fronds; parallel sori'],
|
|
['polypody','Polypodium vulgare','deeply lobed pinnate frond; round gold sori'],
|
|
['sphagnum','Sphagnum palustre','starry capitula; dangling branch fascicles'],
|
|
['haircap_moss','Polytrichum commune','upright leafy shoots; tall reddish capsule setae'],
|
|
['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'],
|
|
['birch_bolete','Leccinum scabrum','brown convex cap; pale scabrous stem'],
|
|
['bay_bolete','Imleria badia','chestnut cushion cap; stout buff stem'],
|
|
['chanterelle','Cantharellus cibarius','wavy golden funnel; decurrent folds'],
|
|
['wood_blewit','Collybia nuda','lilac broad cap; stout lilac stem'],
|
|
['shaggy_inkcap','Coprinus comatus','tall shaggy cylindrical cap; black deliquescent rim'],
|
|
['turkey_tail','Trametes versicolor','thin overlapping banded fan brackets'],
|
|
['oyster_fungus','Pleurotus ostreatus','lateral overlapping oyster caps; decurrent gills'],
|
|
['honey_fungus','Armillaria mellea','tufted honey caps; rings; shared woody base'],
|
|
['birch_polypore','Fomitopsis betulina','thick pale kidney bracket; dark attachment'],
|
|
],
|
|
aquatic: [
|
|
['duckweed','Lemna minor','tiny floating paired oval fronds; single dangling roots'],
|
|
['yellow_waterlily','Nuphar lutea','notched oval floating pads; small yellow cup'],
|
|
['white_waterlily','Nymphaea alba','round notched pads; many white pointed tepals'],
|
|
['water_crowfoot','Ranunculus aquatilis','lobed floating leaves; divided submerged leaves; five white petals'],
|
|
['water_mint','Mentha aquatica','opposite serrate leaves; terminal lilac globe'],
|
|
['water_forgetmenot','Myosotis scorpioides','scorpioid cyme; tiny blue five-lobed flowers'],
|
|
['brooklime','Veronica beccabunga','fleshy opposite oval leaves; lateral blue racemes'],
|
|
['water_starwort','Callitriche stagnalis','opposite narrow submerged leaves; oval terminal rosette'],
|
|
['hornwort','Ceratophyllum demersum','rootless submerged whorls of forked needle leaves'],
|
|
['pondweed','Potamogeton natans','elongated floating ovals; long petioles; emergent spike'],
|
|
],
|
|
lichens: [
|
|
['reindeer_lichen','Cladonia rangiferina','pale repeatedly forked shrubby fruticose tufts'],
|
|
['oakmoss','Evernia prunastri','flattened branching straps with pale undersides'],
|
|
['old_mans_beard','Usnea subfloridana','hanging hair-fine branching filaments'],
|
|
['lichen','Rhizocarpon geographicum','flat yellow-green crust islands divided by black map lines'],
|
|
['dog_lichen','Peltigera canina','broad grey lobes; raised brown margins; underside rhizines'],
|
|
['lungwort_lichen','Lobaria pulmonaria','large lobed green thallus; ridged reticulate surface'],
|
|
['sunburst_lichen','Xanthoria parietina','orange foliose rosette; orange disc apothecia'],
|
|
['hammered_shield','Parmelia sulcata','grey foliose shield; pale hammered ridge network'],
|
|
['cup_lichen','Cladonia chlorophaea','slender mealy stalks; flared shallow pixie cups'],
|
|
['pixie_cup','Cladonia pyxidata','stout pebbled stalks; broad cups; basal squamules'],
|
|
],
|
|
};
|
|
export const BOTANY_CATALOGUE = Object.freeze(Object.entries(BOTANY_GROUPS).flatMap(([group,rows])=>
|
|
rows.map(([id,latin,morphology])=>Object.freeze({id,latin,group,morphology}))));
|
|
export const BOTANY_ART_NAMES = Object.freeze(BOTANY_CATALOGUE.map(r=>r.id));
|
|
export const BOTANY_SPRITE_NAMES = Object.freeze(BOTANY_ART_NAMES.flatMap(id=>[`botany_${id}`,`botany_${id}_dormant`]));
|
|
|
|
// A specimen grammar, not generic coloured markers. Each taxon specifies its
|
|
// own leaf outline, branching, reproductive structure and habitat growth form.
|
|
function specimenLeaf(d,x,y,shape='oval',s=1,color='sage') {
|
|
const p=(a,b)=>[x+a*s,y+b];
|
|
if(shape==='needle') {
|
|
d.line(x,y,x+s*5,y-6,'leafDark').line(x,y,x+s*2,y-7,color); return;
|
|
}
|
|
const points={
|
|
oval:[[0,1],[-3,-1],[-3,-4],[0,-6],[3,-4],[3,-1]],
|
|
round:[[0,2],[-4,0],[-5,-3],[-3,-6],[1,-7],[4,-4],[4,-1]],
|
|
lance:[[0,1],[-2,-3],[0,-9],[2,-3]],
|
|
diamond:[[0,1],[-4,-3],[0,-8],[3,-4],[2,-2]],
|
|
heart:[[0,2],[-5,-2],[-4,-6],[-1,-6],[0,-4],[2,-6],[5,-4],[4,-1]],
|
|
lobe:[[0,2],[-3,0],[-2,-2],[-5,-3],[-4,-5],[-2,-5],[-3,-7],[0,-9],[2,-7],[1,-5],[4,-5],[4,-3],[2,-2],[3,0]],
|
|
thorn:[[0,2],[-3,0],[-2,-2],[-5,-3],[-2,-4],[-3,-7],[0,-6],[2,-8],[3,-4],[5,-3],[2,-1]],
|
|
}[shape];
|
|
d.poly(points.map(([a,b])=>p(a,b)),'leafDark').line(x,y,x,y-(shape==='lance'?6:4),color);
|
|
if(shape!=='lance') d.line(x,y-2,x+s*2,y-4,'leafLight');
|
|
}
|
|
function flower(d,x,y,color='cream',petals=5,r=3) {
|
|
for(let j=0;j<petals;j++) {
|
|
const a=j*Math.PI*2/petals-Math.PI/2;
|
|
d.oval(x+Math.cos(a)*r,y+Math.sin(a)*r,1.5,1.5,color);
|
|
}
|
|
d.dot(x,y,'ochre');
|
|
}
|
|
function compound(d,x,y,length=10,side=1) {
|
|
d.line(x,y,x+side*length,y-7,'leafDark');
|
|
for(let j=2;j<length;j+=3) {
|
|
d.line(x+side*j,y-j*.7,x+side*(j-2),y-j*.7-3,'sage',2);
|
|
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';
|
|
d.line(16,29,15,7,stem,2).line(16,24,7,15,stem).line(16,19,24,10,stem);
|
|
if(id==='hazel') d.line(13,29,8,7,'bark').line(18,29,25,16,'barkLight');
|
|
if(conifer) {
|
|
for(let i=0;i<4;i++) {
|
|
const y=9+i*5,w=id==='spruce'?3+i*2:6+i;
|
|
d.line(16,y,16-w,y+3,'bark').line(16,y,16+w,y+2,'bark');
|
|
for(let j=-w;j<=w;j+=id==='pine'?3:2) {
|
|
d.line(16+j,y+3,14+j,y-(id==='pine'?4:1),'pine');
|
|
d.line(16+j,y+3,18+j,y-(id==='pine'?3:0),'pineGlow');
|
|
}
|
|
}
|
|
if(!dormant) d.oval(id==='spruce'?23:9,22,id==='spruce'?2:3,id==='spruce'?5:3,'barkLight')
|
|
.line(id==='spruce'?23:9,20,id==='spruce'?23:9,24,'wood');
|
|
return;
|
|
}
|
|
const positions={
|
|
oak:[[6,17],[10,25],[23,16],[25,24],[15,8]],
|
|
birch:[[8,13],[11,22],[22,11],[24,22],[16,7]],
|
|
willow:[[5,20],[9,27],[24,19],[27,27],[17,11]],
|
|
alder:[[7,17],[10,24],[23,14],[26,22],[14,9]],
|
|
beech:[[6,16],[10,23],[22,13],[26,21],[15,7]],
|
|
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',
|
|
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');
|
|
if(id==='aspen') d.line(x,y+3,x,y+1,'wood');
|
|
if(id==='willow') d.line(x,y,x-1,y+5,'willow');
|
|
}
|
|
}
|
|
if(!dormant) {
|
|
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');
|
|
} else if(id==='alder') d.oval(24,27,2,2,'barkDark').dot(24,26,'barkLight');
|
|
else if(id==='birch' || id==='willow') d.line(5,20,5,26,id==='birch'?'straw':'cream',2);
|
|
else d.line(25,10,27,6,k===5?'copper':'birchShade');
|
|
}
|
|
}
|
|
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]];
|
|
d.line(13,29,18,19,'bark',2);
|
|
tips.forEach(([x,y],i)=> {
|
|
d.line(15,28,x,y,id==='bilberry'?'leafDark':'bark').line(x,y,x+(i%2?3:-2),y-2,'barkLight');
|
|
if(['blackthorn','hawthorn','blackberry','dog_rose'].includes(id)) d.line(x,y+5,x-3,y+3,'barkDark');
|
|
if(dormant && !['holly','juniper'].includes(id)) return;
|
|
if(['elder','raspberry','dog_rose'].includes(id)) compound(d,x,y+8,id==='elder'?9:6,i%2?-1:1);
|
|
else if(id==='juniper') for(let j=0;j<3;j++) {
|
|
d.line(x,y+9-j*3,x-4,y+4-j*3,'pineGlow').line(x,y+9-j*3,x+4,y+5-j*3,'pine');
|
|
d.line(x,y+9-j*3,x,y+3-j*3,'sage');
|
|
}
|
|
else specimenLeaf(d,x,y+8,{holly:'thorn',hawthorn:'lobe',guelder_rose:'lobe',blackberry:'diamond',
|
|
blackthorn:'oval',bilberry:'oval'}[id],i%2?-1:1);
|
|
if(dormant) return;
|
|
if(id==='dog_rose') { if(i===1) flower(d,x,y,'rose',5,3); else d.oval(x,y+1,1,2,'rust'); }
|
|
else if(id==='elder' || id==='guelder_rose') {
|
|
for(let j=-2;j<=2;j++) d.line(x,y+3,x+j*2,y,'barkLight').oval(x+j*2,y,1,1,id==='elder'?'slate':'rust');
|
|
} else if(id==='blackberry' || id==='raspberry') {
|
|
for(const [a,b] of [[0,0],[2,0],[1,2],[0,3]]) d.dot(x+a,y+b,id==='blackberry'?'slate':'wine');
|
|
d.dot(x+1,y,'berryLight');
|
|
} else d.oval(x,y,1.5,1.5,['holly','hawthorn'].includes(id)?'rust':id==='bilberry'?'slateBlue':'berry');
|
|
});
|
|
}
|
|
function botanicalGrass(d,id,dormant) {
|
|
const k=BOTANY_GROUPS.grasses.findIndex(r=>r[0]===id), stem=dormant?'strawDark':'leafDark';
|
|
for(let i=0;i<7;i++) {
|
|
const x=5+i*3,tip=17+(i*3+k)%9;
|
|
d.line(16,29,x,tip,stem).line(16,29,x+1,tip+2,dormant?'straw':'sage');
|
|
}
|
|
for(let i=0;i<3;i++) {
|
|
const x=9+i*7,y=5+(i*3+k)%7;
|
|
d.line(x,29,x-1,y,stem);
|
|
if(id==='sedge') {
|
|
// Three lit faces on an angular culm and a branching tussock panicle.
|
|
d.poly([[x-1,28],[x-2,y+5],[x,y+3],[x+1,28]],'sage').line(x,y+3,x,27,'leafLight');
|
|
for(let j=0;j<4;j++) d.line(x,y+7-j*2,x+(j%2?4:-3),y+3-j,'strawDark',2);
|
|
} else if(id==='bulrush') d.rect(x-2,y,3,8,dormant?'earthLight':'bark').line(x-1,y-2,x-1,y,'straw');
|
|
else if(id==='timothy' || id==='sweet_vernal_grass') {
|
|
const h=id==='timothy'?9:5;
|
|
d.oval(x-1,y+3,1.5,h/2,id==='timothy'?'sage':'straw').line(x-2,y+1,x-2,y+4,'strawLight');
|
|
} else if(id==='soft_rush') {
|
|
d.line(x-1,y,x-1,y+5,'sage');
|
|
for(let j=0;j<3;j++) d.line(x,y+8,x+2+j,y+6+j,'strawDark').dot(x+3+j,y+6+j,'barkLight');
|
|
} else {
|
|
const wide=id==='tufted_hairgrass'?7:id==='common_reed'?5:4;
|
|
for(let j=0;j<5;j++) {
|
|
const s=(j%2?-1:1), xx=x+s*(wide-j/2), yy=y+2+j*2;
|
|
d.line(x-1,yy+3,xx,yy,'strawDark');
|
|
if(id==='cocksfoot') d.oval(xx+1,yy,2,1.5,'sage');
|
|
else if(id==='yorkshire_fog') d.oval(xx,yy,2,2,'rose').dot(xx-1,yy-1,'cream');
|
|
else if(id==='common_reed') d.line(xx,yy,xx+s*2,yy-3,'barkLight').line(xx,yy,xx+s*2,yy+2,'straw');
|
|
else d.line(xx,yy,xx+1,yy-2,id==='meadow_fescue'?'sage':'strawLight');
|
|
}
|
|
}
|
|
if(id==='common_reed') d.poly([[x,24],[x+7,18],[x+5,23]],'sage').line(x,20,x-5,16,'leafLight');
|
|
}
|
|
}
|
|
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.
|
|
if(id==='clover') d.line(6,29,25,28,'leafDark');
|
|
if(id==='nettle' && !dormant) for(let j=0;j<4;j++) d.line(19,12+j,23+j,15+j,'sage');
|
|
if(id==='heather') d.line(3,28,9,24,'barkLight');
|
|
if(id==='bluebell') d.line(4,29,7,18,'sage');
|
|
return;
|
|
}
|
|
const k=BOTANY_GROUPS.herbs.findIndex(r=>r[0]===id), x=15+k%3;
|
|
d.line(x,29,x-1,7,'leafDark');
|
|
if(id==='yarrow' || id==='meadowsweet') {
|
|
for(let y=19;y<28;y+=4) compound(d,x,y,id==='yarrow'?6:9,y%2?-1:1);
|
|
if(!dormant) for(let j=-3;j<=3;j++) {
|
|
const y=id==='yarrow'?9:6+Math.abs(j)*2;
|
|
d.line(x,17,x+j*3,y,'leafDark'); flower(d,x+j*3,y,'cream',4,id==='yarrow'?1:2);
|
|
}
|
|
} else if(id==='foxglove') {
|
|
for(const [a,b] of [[-5,28],[5,26],[-7,25],[7,29]]) specimenLeaf(d,x+a,b,'oval');
|
|
if(!dormant) for(let j=0;j<5;j++) {
|
|
const y=5+j*4,w=2+j%2;
|
|
d.line(x-1,y,x+3,y+1,'sage').poly([[x+2,y],[x+2+w,y],[x+4+w,y+4],[x+2,y+4]],'rose')
|
|
.line(x+3,y+3,x+3+w,y+3,'mauve').dot(x+3,y+1,'wine');
|
|
}
|
|
} else if(id==='wild_garlic') {
|
|
specimenLeaf(d,10,29,'lance'); specimenLeaf(d,21,28,'lance');
|
|
d.poly([[11,28],[5,17],[8,14],[13,22]],'sage');
|
|
if(!dormant) for(const [a,b] of [[-5,1],[-2,-3],[3,-3],[6,1],[1,4]]) {
|
|
d.line(x,14,x+a,8+b,'sage'); flower(d,x+a,8+b,'cream',6,2);
|
|
}
|
|
} else if(id==='cowslip') {
|
|
for(const [a,b] of [[-6,29],[-3,27],[4,28],[8,30]]) specimenLeaf(d,x+a,b,'oval');
|
|
if(!dormant) for(let j=-2;j<=2;j++) d.line(x,7,x+j*3,8+Math.abs(j),'sage')
|
|
.oval(x+j*3,11+Math.abs(j),1.5,3,'ochre').dot(x+j*3,13+Math.abs(j),'gold');
|
|
} else { // Anemone: one flower above a whorl of three divided leaves.
|
|
for(const s of [-1,0,1]) specimenLeaf(d,x+s*7,23+(s===0?2:0),'lobe',s||1);
|
|
if(!dormant) flower(d,x-1,8,'cream',6,4);
|
|
}
|
|
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') {
|
|
for(const [x,y,s] of [[7,28,-1],[13,29,1],[20,28,1],[25,29,-1]]) {
|
|
d.poly([[x,y],[x-3,y-11],[x-2,y-20],[x+1,y-22],[x+3,y-18],[x+2,y-9]],'leaf')
|
|
.line(x,y-2,x,y-19,'leafLight');
|
|
for(let j=5;j<18;j+=3) d.line(x,y-j,x+s*2,y-j-1,'barkLight');
|
|
}
|
|
} else {
|
|
const tips=id==='bracken'?[[5,10],[16,3],[27,12]]:id==='lady_fern'?[[6,13],[15,4],[25,9]]:
|
|
id==='male_fern'?[[5,17],[12,7],[20,5],[27,18]]:[[7,19],[15,9],[25,14]];
|
|
for(const [i,[x,y]] of tips.entries()) {
|
|
d.line(16,29,x,y,'leafDark');
|
|
const n=id==='lady_fern'?7:5;
|
|
for(let j=1;j<n;j++) {
|
|
const xx=x+(16-x)*j/n,yy=y+(29-y)*j/n, w=id==='bracken'?j+1:Math.min(j+1,n-j+1);
|
|
for(const s of [-1,1]) {
|
|
d.line(xx,yy,xx+s*w,yy-2,id==='male_fern'?'leaf':'sage',id==='male_fern'?2:1);
|
|
if(id==='bracken' || id==='lady_fern') d.line(xx+s*w,yy-2,xx+s*w,yy-4,'leafLight');
|
|
if(id==='polypody' && !dormant) d.dot(xx+s*(w-1),yy-1,'ochre');
|
|
}
|
|
}
|
|
if(i===0 && id==='male_fern') d.oval(16,28,3,1,'barkLight');
|
|
}
|
|
}
|
|
} else {
|
|
const centers=[[5,26],[11,23],[18,25],[25,24],[16,29]];
|
|
for(const [i,[x,y]] of centers.entries()) {
|
|
d.oval(x,y,4,2,'leafDark');
|
|
if(id==='sphagnum') for(let j=0;j<6;j++) {
|
|
const a=j*Math.PI/3;
|
|
d.line(x,y,x+Math.cos(a)*4,y+Math.sin(a)*3,'sage').line(x,y,x+Math.cos(a)*3,y+5,'leaf');
|
|
} else if(id==='haircap_moss') {
|
|
d.line(x,y,x,y-9,'leaf');
|
|
for(let j=2;j<9;j+=2) d.line(x,y-j,x-2,y-j-2,'sage').line(x,y-j,x+2,y-j-2,'sage');
|
|
if(!dormant && i%2===0) d.line(x+1,y-6,x+2,y-17,'copper').rect(x+1,y-18,3,2,'barkLight');
|
|
} else if(id==='moss') {
|
|
d.line(x-3,y+1,x+4,y-4,'leaf');
|
|
for(let j=0;j<5;j++) d.line(x-2+j,y-j,x-4+j,y-j-2,'sage').line(x-2+j,y-j,x+1+j,y-j,'leafLight');
|
|
} else if(id==='fork_moss') {
|
|
for(let j=-2;j<3;j++) d.line(x+j,y,x+j+1,y-5,'leaf').line(x+j+1,y-5,x+j+4,y-8,'sage');
|
|
} else {
|
|
d.line(x,y,x,y-10,'leafDark');
|
|
for(let j=2;j<10;j+=3) d.oval(x-2,y-j,2,1,'sage').oval(x+2,y-j-1,2,1,'leaf');
|
|
if(i===2 && !dormant) d.line(x,y-9,x+2,y-17,'barkLight').line(x+2,y-17,x+5,y-15,'copper',2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function botanicalFungus(d,id,dormant) {
|
|
const k=BOTANY_GROUPS.fungi.findIndex(r=>r[0]===id);
|
|
if(['turkey_tail','oyster_fungus','birch_polypore'].includes(id)) {
|
|
d.line(7,28,8,9,'bark',3).line(8,24,25,26,'barkDark',2);
|
|
const tiers=id==='birch_polypore'?[[15,17,9,5]]:id==='turkey_tail'?[[15,23,8,4],[20,18,8,4],[13,12,7,3]]:
|
|
[[15,23,10,4],[20,17,8,4],[15,10,7,3]];
|
|
for(const [x,y,rx,ry] of tiers) {
|
|
d.oval(x,y,rx,dormant?1:ry,id==='oyster_fungus'?'stone':'barkLight');
|
|
if(!dormant) {
|
|
d.oval(x,y-1,rx-2,ry-1,id==='turkey_tail'?'cream':'birchGlow');
|
|
if(id==='turkey_tail') d.oval(x,y-2,rx-4,1,'earth').line(x-4,y,x+4,y,'rust');
|
|
else if(id==='oyster_fungus') for(let j=-4;j<5;j+=2) d.line(8,y+1,x+j,y+1,'slateShade');
|
|
else d.line(x-5,y+3,x+5,y+3,'cream');
|
|
}
|
|
}
|
|
} else {
|
|
const cluster=id==='honey_fungus'?[[8,21,5],[16,14,6],[24,20,5]]:[[16,13,k===5?5:k===2?10:9]];
|
|
for(const [x,y,w] of cluster) {
|
|
d.line(x,29,x,y+3,k===4?'rose':'cream',k===2?4:3);
|
|
if(dormant) {
|
|
d.oval(x,y+9+k%3,w/2,1+k%2,'earthLight').line(x-3,29,x+4,28,'barkLight');
|
|
if(id==='fly_agaric') d.oval(x,28,3,2,'birchGlow');
|
|
if(id==='birch_bolete') for(let j=0;j<4;j++) d.dot(x+j%2,20+j*2,'barkDark');
|
|
continue;
|
|
}
|
|
const color={fly_agaric:'rust',birch_bolete:'bark',bay_bolete:'earth',chanterelle:'ochre',wood_blewit:'rose',
|
|
shaggy_inkcap:'cream',honey_fungus:'straw'}[id];
|
|
if(id==='shaggy_inkcap') {
|
|
d.oval(x,12,5,10,'cream').rect(x-5,18,11,3,'slate');
|
|
for(let j=0;j<4;j++) d.line(x-3+j*2,6+j%2,x-4+j*2,15+j%3,'barkLight');
|
|
d.line(x+4,20,x+4,25,'ink');
|
|
} else if(id==='chanterelle') {
|
|
d.poly([[x-10,y-2],[x-6,y],[x-2,y-2],[x+3,y],[x+9,y-3],[x+7,y+3],[x+2,y+7],[x-2,y+7],[x-7,y+2]],color)
|
|
.line(x-6,y+1,x,y+8,'strawLight').line(x+5,y+1,x+1,y+8,'strawDark');
|
|
} else {
|
|
d.oval(x,y,w,k===4?3:5,color).line(x-w+1,y+(k===4?2:3),x+w-1,y+(k===4?2:3),k===4?'berryLight':'woodLight');
|
|
if(id==='fly_agaric') {
|
|
for(const [a,b] of [[-5,0],[-2,-3],[2,-2],[5,1]]) d.rect(x+a,y+b,2,1,'cream');
|
|
d.line(x-3,21,x+4,21,'cream').oval(x,28,3,2,'birchGlow');
|
|
}
|
|
if(id==='birch_bolete') for(let j=0;j<5;j++) d.dot(x+j%2,20+j*2,'barkDark');
|
|
if(id==='honey_fungus') d.line(x-2,y+8,x+3,y+8,'wood');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function botanicalAquatic(d,id,dormant) {
|
|
if(id==='yellow_waterlily' || id==='white_waterlily') {
|
|
// True floating round/oval notched pads, never horns or lotus seed pods.
|
|
for(const [x,y,rx,ry] of (id==='white_waterlily'?[[10,23,7,6],[23,16,6,5]]:[[10,23,8,5],[23,16,6,4]])) {
|
|
d.oval(x,y,rx,ry,'leafDark').oval(x-1,y-1,rx-1,ry-1,'sage');
|
|
for(let j=0;j<5;j++) for(let a=0;a<=j/2;a++) d.dot(x+a,y+j,[0,0,0],0);
|
|
d.line(x-4,y-1,x-1,y-2,'leafLight');
|
|
}
|
|
if(!dormant) {
|
|
if(id==='white_waterlily') flower(d,12,13,'cream',8,5);
|
|
else d.oval(13,13,3,3,'ochre').oval(13,12,2,1,'gold').line(13,16,13,20,'leaf');
|
|
}
|
|
} else if(id==='duckweed') {
|
|
for(const [i,[x,y]] of [[5,10],[15,6],[25,13],[10,21],[23,24]].entries()) {
|
|
d.oval(x,y,3,2,'leaf').oval(x+3,y+2,2,2,'sage').dot(x-1,y,'leafLight');
|
|
d.line(x,y+2,x+(i%2?1:-1),y+6,'willowShade');
|
|
}
|
|
} else if(id==='hornwort' || id==='water_starwort') {
|
|
d.line(12,28,19,5,'leafDark');
|
|
if(id==='hornwort') {
|
|
for(let j=0;j<5;j++) for(const s of [-1,1]) {
|
|
const x=13+j,y=25-j*4;
|
|
d.line(x,y,x+s*7,y-4,'sage').line(x+s*4,y-2,x+s*4,y-5,'leaf')
|
|
.line(x+s*7,y-4,x+s*8,y-7,'leafLight');
|
|
}
|
|
} else {
|
|
for(let j=0;j<5;j++) d.line(13+j,26-j*4,7+j,24-j*4,'sage')
|
|
.line(13+j,26-j*4,21+j,25-j*4,'leaf');
|
|
for(const [x,y] of [[15,7],[20,8],[18,12],[23,11]]) d.oval(x,y,2,3,'sage').dot(x,y-1,'leafLight');
|
|
}
|
|
} else if(id==='pondweed') {
|
|
d.line(15,29,17,8,'barkLight');
|
|
for(const [x,y] of [[7,23],[24,19],[10,12]]) d.line(16,y+5,x,y,'leafDark')
|
|
.oval(x,y,5,2,'leaf').line(x-3,y,x+3,y,'leafLight');
|
|
if(!dormant) d.line(18,14,19,5,'leafDark').oval(19,5,1.5,4,'strawDark');
|
|
} else if(id==='water_crowfoot') {
|
|
d.line(14,29,17,10,'leafDark');
|
|
for(const [x,y] of [[6,23],[23,25],[10,16]]) {
|
|
d.line(15,26,x,y,'sage');
|
|
for(const s of [-1,0,1]) d.line(x,y,x+s*4,y-4,'leafLight').line(x+s*4,y-4,x+s*5,y-6,'leaf');
|
|
}
|
|
if(!dormant) flower(d,18,9,'cream',5,3);
|
|
} else {
|
|
const x=id==='brooklime'?14:17;
|
|
d.line(x,29,x,9,'leafDark');
|
|
for(let j=0;j<3;j++) for(const s of [-1,1]) specimenLeaf(d,x+s*(4+j%2),28-j*5,
|
|
id==='water_mint'?'diamond':'oval',s);
|
|
if(!dormant) {
|
|
if(id==='water_mint') d.oval(x,8,4,3,'rose').line(x-2,7,x+2,7,'mauve').dot(x,9,'cream');
|
|
else {
|
|
d.line(x,18,x+7,8,'sage');
|
|
for(let j=0;j<4;j++) flower(d,x+6+(j%2?2:0),8+j*4,'slateGlow',5,1);
|
|
if(id==='water_forgetmenot') d.line(x+7,8,x+5,5,'sage').oval(x+4,5,1,1,'slateBlue');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function botanicalLichen(d,id,dormant) {
|
|
const k=BOTANY_GROUPS.lichens.findIndex(r=>r[0]===id);
|
|
if(k<3) {
|
|
if(id==='old_mans_beard') {
|
|
d.line(5,5,27,8,'barkLight');
|
|
for(let j=0;j<6;j++) {
|
|
const x=6+j*4;
|
|
d.line(x,6,x-2,16+j%3,'willow').line(x-2,16+j%3,x+1,26-j%2,'sage')
|
|
.line(x-1,13,x+3,20,'leafLight').line(x-1,19,x-4,25,'willow');
|
|
}
|
|
} else for(let j=0;j<5;j++) {
|
|
const x=5+j*5,y=28-j%2*3;
|
|
d.line(16,29,x,y-8,'willow',id==='oakmoss'?2:1);
|
|
for(const s of [-1,1]) {
|
|
d.line(x,y-8,x+s*4,y-15,'leafLight',id==='oakmoss'?2:1);
|
|
d.line(x+s*3,y-13,x+s*6,y-16-j%3,'sage').line(x+s*3,y-13,x+s*2,y-19,'cream');
|
|
if(id==='reindeer_lichen') d.line(x+s*2,y-17,x+s*5,y-21,'willow');
|
|
}
|
|
}
|
|
} else if(id==='cup_lichen' || id==='pixie_cup') {
|
|
for(const [i,[x,y]] of [[7,21],[15,13],[24,18]].entries()) {
|
|
const w=id==='pixie_cup'?4:3;
|
|
d.line(x,29,x,y+3,'willow',id==='pixie_cup'?3:2)
|
|
.poly([[x-w,y],[x+w,y],[x+1,y+5],[x-1,y+5]],'sage')
|
|
.line(x-w,y,x+w,y,'cream').line(x-w+1,y+1,x+w-1,y+1,'leafDark');
|
|
if(id==='pixie_cup') for(let j=0;j<3;j++) d.dot(x-1+j%2,y+6+j*2,'cream');
|
|
d.oval(x+(i%2?3:-2),28,3,1,'willow');
|
|
}
|
|
} else {
|
|
const crust=id==='lichen', color=id==='sunburst_lichen'?'ochre':id==='lungwort_lichen'?'leaf':'willow';
|
|
for(const [i,[x,y]] of [[7,15],[15,10],[24,16],[11,24],[22,25]].entries()) {
|
|
const r=crust?4:id==='dog_lichen'?6:id==='sunburst_lichen'?4:5;
|
|
d.poly([[x-r,y],[x-2,y-r],[x+2,y-r+1],[x+r,y-1],[x+r-1,y+3],[x+1,y+r-1],[x-3,y+3]],
|
|
crust?'barkDark':'leafDark');
|
|
d.oval(x,y,r-1,crust?2:3,color);
|
|
if(crust) d.line(x-2,y,x+2,y-1,'sage').dot(x+2,y+1,'straw');
|
|
else if(id==='sunburst_lichen') d.oval(x,y,2,2,'copper').dot(x,y,'ochre');
|
|
else if(id==='dog_lichen') d.line(x-3,y+2,x,y+3,'earth').line(x,y+3,x-1,y+5,'cream');
|
|
else d.line(x-3,y,x+2,y-2,'leafLight').line(x,y-1,x+2,y+2,'leafLight')
|
|
.line(x+2,y+2,x-1,y+3,id==='hammered_shield'?'cream':'sage');
|
|
if(id==='lungwort_lichen') d.poly([[x+2,y+1],[x+5,y+4],[x+3,y+7],[x,y+5]],'leaf').line(x+2,y+2,x+2,y+5,'sage');
|
|
if(id==='hammered_shield') d.rect(x-4,y-3,3,2,'willow').dot(x-4,y-3,'cream');
|
|
if(dormant && i===2) d.dot(x,y,'barkLight');
|
|
}
|
|
}
|
|
}
|
|
function botanySprite(record,dormant=false) {
|
|
const d=new Drawing();
|
|
({trees:botanicalTree,shrubs:botanicalShrub,grasses:botanicalGrass,herbs:botanicalHerb,
|
|
ferns_mosses:botanicalFernMoss,fungi:botanicalFungus,aquatic:botanicalAquatic,lichens:botanicalLichen})[record.group](d,record.id,dormant);
|
|
// Cutouts have one-pixel journal padding. Inactive specimens lose bloom/fruit
|
|
// above and settle to a reduced dry silhouette, not a tinted active sprite.
|
|
const out=new Drawing();
|
|
for(let y=1;y<31;y++) for(let x=1;x<31;x++) {
|
|
const p=d.data.subarray((y*32+x)*4,(y*32+x+1)*4);
|
|
if(!p[3]) continue;
|
|
if(dormant) {
|
|
const luminance=p[0]+p[1]+p[2];
|
|
const dry=luminance>470?'strawLight':luminance>350 || (p[1]>p[0] && luminance>310)?'straw':'barkLight';
|
|
out.dot(16+Math.round((x-16)*.86),29-Math.round((29-y)*.72),dry);
|
|
} else out.dot(x,y,[...p.subarray(0,3)]);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function worldArt() {
|
|
const images=new Map();
|
|
for(const name of WORLD_NAMES) images.set(`hearth_${name}.png`,worldTile(name));
|
|
for(const name of ITEM_NAMES) images.set(`hearth_item_${name}.png`,itemTile(name));
|
|
for(const name of ['hotbar','selected','crosshair']) images.set(`hearth_${name}.png`,uiTile(name));
|
|
for(const name of ANIMAL_NAMES) images.set(`hearth_${name}.png`,animalTexture(name));
|
|
for(const r of BOTANY_CATALOGUE) for(const dormant of [false,true])
|
|
images.set(`hearth_botany_${r.id}${dormant?'_dormant':''}.png`,botanySprite(r,dormant));
|
|
return images;
|
|
}
|
|
|
|
// Sorted filename order is also the contact sheet's legend: zero-based row-major,
|
|
// 16 columns, unscaled 32px cells. Alpha composited over journal checker paper.
|
|
export function contactSheet(images=worldArt()) {
|
|
if(!(images instanceof Map) || images.size<1 || images.size>512 || [...images.values()].some(image=>
|
|
image?.width!==32 || image?.height!==32 || !(image.data instanceof Uint8Array) || image.data.length!==4096))
|
|
throw new Error('Contact sheet requires 1..512 RGBA 32px tiles.');
|
|
const columns=16, width=columns*32, height=Math.ceil(images.size/columns)*32;
|
|
const data=new Uint8Array(width*height*4);
|
|
for(let y=0;y<height;y++) for(let x=0;x<width;x++) {
|
|
const color=(Math.floor(x/8)+Math.floor(y/8))%2?'outline':'ink';
|
|
data.set(rgba(color),(y*width+x)*4);
|
|
}
|
|
[...images.entries()].sort(([a],[b])=>String(a)<String(b)?-1:String(a)>String(b)?1:0).forEach(([,image],i)=> {
|
|
for(let y=0;y<32;y++) for(let x=0;x<32;x++) {
|
|
const from=(y*32+x)*4, to=((Math.floor(i/columns)*32+y)*width+(i%columns)*32+x)*4;
|
|
const alpha=image.data[from+3]/255;
|
|
for(let c=0;c<3;c++) data[to+c]=Math.round(image.data[from+c]*alpha+data[to+c]*(1-alpha));
|
|
}
|
|
});
|
|
return {width,height,data};
|
|
}
|
|
|
|
// Compatibility swatches are intentionally quiet clustered soil, not neon/hash
|
|
// noise. Named materials above are the integration target; no Lua parsing needed.
|
|
export function legacyPixel(hex,x,y) {
|
|
const rgb=[0,2,4].map(i=>parseInt(hex.slice(i,i+2),16));
|
|
const shade=(y>=19 && y<=22)?-8:((x>=5 && x<=10 && y>=6 && y<=8)?8:0);
|
|
return [...rgb.map((v,i)=>Math.round(v*.35+PALETTE.earth[i]*.65)+shade),255];
|
|
}
|