/** * Plant & Flower Tuft Entity * Supports standard grass tufts and nutrient-dense flower tufts grown from decayed remains. * Features moisture loss, revival via rain, growth stages, and seed dispersal when clustered. */ export class Plant { constructor(x, y, type = 'grass', growth = 0.25, worldWidth = 1280, worldHeight = 760) { // Natural soft boundary matching meadow edge const margin = 20; this.x = Math.max(margin, Math.min(worldWidth - margin, x)); this.y = Math.max(margin, Math.min(worldHeight - margin, y)); this.type = type; // 'grass' or 'flower' (dense flower tuft) this.growth = growth; // 0.2 to 1.0 this.growthRate = type === 'flower' ? 0.08 : 0.05; this.moisture = 100; // 0 to 100 this.dryRate = 1.6; // moisture loss per second this.age = 0; this.seedCooldown = 8 + Math.random() * 8; // initial jitter this.bitesRemaining = type === 'flower' ? 3 : 1; // dense tufts provide multiple meals this.isDead = false; this.swayOffset = Math.random() * Math.PI * 2; // Pick flower colors for flower tufts const flowerPalette = ['#FF2A6D', '#FFD166', '#05D9E8', '#9B5DE5', '#F15BB5']; this.flowerColor = flowerPalette[Math.floor(Math.random() * flowerPalette.length)]; } receiveRain(amount) { this.moisture = Math.min(100, this.moisture + amount); } update(dt, allPlants, onSeedDispersed, onWithered, worldWidth, worldHeight, config = null, terrain = null) { if (this.isDead) return; this.age += dt; // As plants grow, they absorb surrounding blood/decay decals into nutrients! if (terrain && this.age < 8) { terrain.absorbDecalsNear(this.x, this.y, 30); } // 1. Dryness / moisture loss using config dry rate const dryRate = config && config.plantDryRate ? config.plantDryRate : 2.2; this.moisture -= dryRate * dt; if (this.moisture <= 0) { this.moisture = 0; this.isDead = true; if (onWithered) onWithered(this); return; } // 2. Growth if (this.growth < 1.0) { this.growth = Math.min(1.0, this.growth + this.growthRate * dt); } // 3. Proximity-based Seed Dispersal (heavily regulated to protect CPU & meadow balance) this.seedCooldown -= dt; if (this.seedCooldown <= 0 && this.growth >= 0.85 && this.age >= 24 && this.moisture >= 50) { this.seedCooldown = 24 + Math.random() * 18; const maxCap = config && config.maxPlantCap ? config.maxPlantCap : 90; if (allPlants.length >= maxCap) return; const seedChance = config && typeof config.plantSeedRate === 'number' ? config.plantSeedRate : 8; if (Math.random() * 100 > seedChance) return; // Check neighbor count: must have a close neighbor, but not overcrowded (< 3 in 55px) let neighborCount = 0; for (let i = 0; i < allPlants.length; i++) { const other = allPlants[i]; if (other === this || other.isDead) continue; const dist = Math.hypot(other.x - this.x, other.y - this.y); if (dist <= 55) { neighborCount++; } } if (neighborCount >= 1 && neighborCount <= 3 && onSeedDispersed) { const angle = Math.random() * Math.PI * 2; const dist = 35 + Math.random() * 50; const safeMargin = 22; const targetX = Math.max(safeMargin, Math.min(worldWidth - safeMargin, this.x + Math.cos(angle) * dist)); const targetY = Math.max(safeMargin, Math.min(worldHeight - safeMargin, this.y + Math.sin(angle) * dist)); onSeedDispersed(this.x, this.y - 6, targetX, targetY); } } } takeBite() { // Sheep eating a plant completely consumes and removes the node this.bitesRemaining = 0; this.isDead = true; return true; } draw(ctx, simTime = 0) { if (this.isDead) return; ctx.save(); const px = Math.round(this.x); const py = Math.round(this.y); const sway = Math.sin(simTime * 2.5 + this.swayOffset) * 1.5; // Color gradient based on moisture // Healthy: vibrant greens; Parched: pale yellow/olive; Withering: straw brown const moistPct = this.moisture / 100; let bladeColor = '#3CB043'; let darkBladeColor = '#2B822F'; if (moistPct < 0.3) { bladeColor = '#A89F58'; // withering straw darkBladeColor = '#8A8244'; } else if (moistPct < 0.6) { bladeColor = '#77B847'; // slightly dry darkBladeColor = '#5C9132'; } if (this.type === 'flower') { // --- DENSE FLOWER TUFT (High nutrition from decayed carcasses) --- const scale = this.growth; const w = Math.round(18 * scale); const h = Math.round(16 * scale); // Dense leafy foliage mound ctx.fillStyle = darkBladeColor; ctx.fillRect(px - Math.round(w / 2), py - 2, w, 4); // Lush pixel blades swaying ctx.fillStyle = bladeColor; ctx.fillRect(px - 6 + sway * 0.5, py - Math.round(h * 0.7), 2, Math.round(h * 0.7)); ctx.fillRect(px - 2 + sway * 0.8, py - h, 3, h); ctx.fillRect(px + 3 + sway, py - Math.round(h * 0.85), 2, Math.round(h * 0.85)); ctx.fillRect(px + 6 + sway * 0.6, py - Math.round(h * 0.6), 2, Math.round(h * 0.6)); // Blossoms / Flowers (if mature) if (this.growth > 0.6) { // Main flower ctx.fillStyle = this.flowerColor; const fx = Math.round(px + sway * 0.8); const fy = Math.round(py - h - 1); ctx.fillRect(fx - 2, fy - 2, 5, 5); ctx.fillStyle = '#FFFFFF'; // flower center ctx.fillRect(fx, fy, 1, 1); // Secondary small blossom ctx.fillStyle = '#FFE66D'; const fx2 = Math.round(px - 5 + sway * 0.5); const fy2 = Math.round(py - Math.round(h * 0.75)); ctx.fillRect(fx2 - 1, fy2 - 1, 3, 3); } } else { // --- STANDARD GRASS TUFT --- const scale = this.growth; const h = Math.round(10 * scale); ctx.fillStyle = bladeColor; ctx.fillRect(px - 3 + sway * 0.6, py - Math.round(h * 0.8), 2, Math.round(h * 0.8)); ctx.fillRect(px + sway, py - h, 2, h); ctx.fillRect(px + 3 + sway * 0.7, py - Math.round(h * 0.7), 2, Math.round(h * 0.7)); // Root base dot ctx.fillStyle = darkBladeColor; ctx.fillRect(px - 2, py - 1, 5, 2); } // Optional subtle low-moisture alert dot if dying of thirst if (this.moisture < 25) { ctx.fillStyle = '#E63946'; ctx.fillRect(px - 1, py - 16, 2, 2); } ctx.restore(); } }