182 lines
6.3 KiB
JavaScript
182 lines
6.3 KiB
JavaScript
|
|
/**
|
||
|
|
* 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();
|
||
|
|
}
|
||
|
|
}
|