LifeSimulator/js/entities/plant.js

182 lines
6.3 KiB
JavaScript
Raw Permalink Normal View History

/**
* 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();
}
}