/** * Terrain, Soil Decals & Carcass Decomposition System * Manages meadow grass, dynamic blood decals with plant nutrient absorption, * rain cleansing, and decomposing animal carcasses that sprout flower tufts. */ export class Carcass { constructor(x, y, species, size = 18) { this.x = x; this.y = y; this.species = species; // 'sheep' or 'wolf' this.size = size; this.decayProgress = 0; // 0.0 (fresh) to 1.0 (fully decomposed into soil) this.meatLeft = 1.0; this.isDecomposed = false; } update(dt, decayMultiplier = 1.0) { const baseDecayRate = 1 / 10.0; this.decayProgress += dt * baseDecayRate * decayMultiplier; if (this.decayProgress >= 1.0) { this.decayProgress = 1.0; this.isDecomposed = true; } } draw(ctx) { ctx.save(); const alpha = Math.max(0.2, 1 - this.decayProgress * 0.7); ctx.globalAlpha = alpha; const s = Math.max(6, Math.round(this.size * (1 - this.decayProgress * 0.35))); const px = Math.round(this.x - s / 2); const py = Math.round(this.y - s / 3); if (this.decayProgress < 0.6) { // Flesh stage if (this.species === 'sheep') { ctx.fillStyle = '#C8CDD0'; // soiled wool ctx.fillRect(px, py, s, Math.round(s * 0.65)); ctx.fillStyle = '#8A1010'; // blood patches on wool ctx.fillRect(px + 2, py + 2, Math.max(2, Math.round(s * 0.3)), 3); ctx.fillStyle = '#222'; ctx.fillRect(px - 3, py + 1, 4, 4); } else { // Wolf carcass ctx.fillStyle = '#3E4246'; ctx.fillRect(px, py, s, Math.round(s * 0.55)); ctx.fillStyle = '#8A1010'; ctx.fillRect(px + 4, py + 1, Math.max(2, Math.round(s * 0.25)), 2); } } else { // Skeletal ribs stage ctx.fillStyle = '#E5E1D3'; const boneLen = Math.round(s * 0.75); ctx.fillRect(px, py + 2, boneLen, 2); for (let r = 2; r < boneLen - 2; r += 3) { ctx.fillRect(px + r, py, 1, 6); } ctx.fillRect(px - 2, py + 1, 3, 3); } ctx.restore(); } } export class TerrainManager { constructor(width, height) { this.width = width; this.height = height; this.carcasses = []; // Active dynamic decals (blood pools, soil mulch) that can be absorbed or washed away this.decals = []; // Offscreen canvas for static meadow base grass this.baseCanvas = document.createElement('canvas'); this.baseCanvas.width = width; this.baseCanvas.height = height; this.baseCtx = this.baseCanvas.getContext('2d'); this.generateBaseTerrain(); } generateBaseTerrain() { const ctx = this.baseCtx; const grad = ctx.createLinearGradient(0, 0, 0, this.height); grad.addColorStop(0, '#559437'); grad.addColorStop(1, '#4A8330'); ctx.fillStyle = grad; ctx.fillRect(0, 0, this.width, this.height); // Subtle grass flecks const fleckColors = ['#5E9E3C', '#45792B', '#68A843', '#407028']; const totalFlecks = Math.floor((this.width * this.height) / 350); for (let i = 0; i < totalFlecks; i++) { const x = Math.floor(Math.random() * this.width); const y = Math.floor(Math.random() * this.height); const c = fleckColors[Math.floor(Math.random() * fleckColors.length)]; ctx.fillStyle = c; const w = 2 + Math.floor(Math.random() * 2); const h = 2 + Math.floor(Math.random() * 3); ctx.fillRect(x, y, w, h); } // Tiny wild micro-flowers const flowerColors = ['#FFF382', '#FFFFFF', '#FFB7C5', '#8FD3FE']; for (let i = 0; i < 90; i++) { const x = Math.floor(Math.random() * this.width); const y = Math.floor(Math.random() * this.height); const c = flowerColors[Math.floor(Math.random() * flowerColors.length)]; ctx.fillStyle = c; ctx.fillRect(x, y, 2, 2); } } reset() { this.carcasses = []; this.decals = []; } resize(newWidth, newHeight) { if (newWidth <= 0 || newHeight <= 0) return; this.width = newWidth; this.height = newHeight; this.baseCanvas.width = newWidth; this.baseCanvas.height = newHeight; this.generateBaseTerrain(); // Clamp decals and carcasses to new meadow boundaries for (let i = 0; i < this.carcasses.length; i++) { const c = this.carcasses[i]; c.x = Math.max(15, Math.min(newWidth - 15, c.x)); c.y = Math.max(15, Math.min(newHeight - 15, c.y)); } for (let i = 0; i < this.decals.length; i++) { const d = this.decals[i]; d.x = Math.max(10, Math.min(newWidth - 10, d.x)); d.y = Math.max(10, Math.min(newHeight - 10, d.y)); } } addCarcass(x, y, species, size) { const carcass = new Carcass(x, y, species, size); this.carcasses.push(carcass); this.addBloodSplotch(x, y, 7 + Math.random() * 4); return carcass; } addBloodSplotch(x, y, radius = 5) { const r = Math.max(3, Math.round(radius)); this.decals.push({ x, y, radius: r, type: 'blood', color: 'rgba(145, 14, 14,', maxLife: 28, // seconds before natural fade life: 28, opacity: 0.75, transformed: false, }); // Keep decal count bounded for performance if (this.decals.length > 70) { this.decals.shift(); } } addFertilizedSoilPatch(x, y, radius = 16) { this.decals.push({ x, y, radius, type: 'soil', maxLife: 35, life: 35, opacity: 0.75, transformed: true, }); } /** * Absorbs/cleanses blood and soil decals around a growing plant node. * As plants grow, they consume the nutrients and remove the blood splats! */ absorbDecalsNear(x, y, absorbRadius = 30) { for (let i = this.decals.length - 1; i >= 0; i--) { const d = this.decals[i]; const dist = Math.hypot(d.x - x, d.y - y); if (dist <= absorbRadius + d.radius) { // Fast fade or instant absorption by the plant d.life = Math.min(d.life, 0.4); } } } update(dt, decayMultiplier, rainIntensity, onCarcassDecomposed, onBloodEnriched) { // 1. Update carcasses for (let i = this.carcasses.length - 1; i >= 0; i--) { const c = this.carcasses[i]; c.update(dt, decayMultiplier); if (c.isDecomposed) { this.addFertilizedSoilPatch(c.x, c.y, 16); if (onCarcassDecomposed) { onCarcassDecomposed(c.x, c.y); } this.carcasses.splice(i, 1); } } // 2. Update decals: age, rain cleansing wash, and soil enrichment // Rain actively washes blood away! const rainWashFactor = 1.0 + (rainIntensity || 0) * 3.5; for (let i = this.decals.length - 1; i >= 0; i--) { const d = this.decals[i]; d.life -= dt * decayMultiplier * rainWashFactor; // When blood spot reaches maturity, turn into enriched soil and trigger a bloom if (d.type === 'blood' && !d.transformed && d.life <= d.maxLife * 0.4) { d.transformed = true; d.type = 'soil'; d.radius += 5; if (onBloodEnriched && Math.random() < 0.65) { onBloodEnriched(d.x, d.y); } } // Decal has completely faded or washed away if (d.life <= 0) { this.decals.splice(i, 1); } } } draw(ctx) { // 1. Base clean meadow ctx.drawImage(this.baseCanvas, 0, 0); // 2. Draw dynamic decals (blood and enriched soil) with smooth fading for (let i = 0; i < this.decals.length; i++) { const d = this.decals[i]; const alphaNorm = Math.max(0, Math.min(1, d.life / d.maxLife)); const currentAlpha = d.opacity * alphaNorm; if (currentAlpha <= 0.02) continue; ctx.save(); if (d.type === 'blood') { // Blood pool with subtle splatter dots ctx.fillStyle = `rgba(145, 14, 14, ${currentAlpha})`; ctx.beginPath(); ctx.arc(d.x, d.y, d.radius, 0, Math.PI * 2); ctx.fill(); // Satellites ctx.fillStyle = `rgba(110, 8, 8, ${currentAlpha * 0.8})`; ctx.fillRect(Math.round(d.x + d.radius * 0.7), Math.round(d.y - d.radius * 0.5), 2, 2); ctx.fillRect(Math.round(d.x - d.radius * 0.8), Math.round(d.y + d.radius * 0.6), 2, 2); } else { // Enriched mulch soil ring const grad = ctx.createRadialGradient(d.x, d.y, 2, d.x, d.y, d.radius); grad.addColorStop(0, `rgba(68, 45, 26, ${currentAlpha})`); grad.addColorStop(0.7, `rgba(84, 128, 48, ${currentAlpha * 0.7})`); grad.addColorStop(1, `rgba(84, 128, 48, 0)`); ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(d.x, d.y, d.radius, 0, Math.PI * 2); ctx.fill(); } ctx.restore(); } // 3. Draw carcasses lying on the field for (let i = 0; i < this.carcasses.length; i++) { this.carcasses[i].draw(ctx); } } }