LifeSimulator/js/terrain.js

286 lines
8.6 KiB
JavaScript

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