Files
builazoo/web/js/food.js
ncantu c7d389ecbb Lint: fix errors and remove unused variables
**Motivations:**
- Ensure lint config is not degraded and fix all lint errors for pousse workflow.

**Root causes:**
- Unused variables kept with _ prefix instead of removed (_row, _questReward, _i).
- getAnimalBlockOrigin had 5 parameters (max 4).
- use of continue statement (no-continue rule).

**Correctifs:**
- ESLint config verified; no eslint-disable in codebase.
- Removed unused variable _row (biome-rules); removed dead function _questReward (quests); removed unused map param _i (state.js).
- getAnimalBlockOrigin refactored to 4 params (pos object instead of x, y).
- Replaced continue with if (cell) block in normalizeLoadedCells (state.js).
- JSDoc param names aligned with _height, _y (biome-rules).

**Evolutions:**
- (none)

**Pages affectées:**
- web/js/biome-rules.js
- web/js/quests.js
- web/js/state.js
- web/js/placement.js
2026-03-04 15:32:27 +01:00

332 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Food capacity and feeding tick. Animals are fed up to capacity each tick;
* unfed animals accumulate time without food and are removed by checkDeathCauses.
*/
import { GameConfig } from "./config.js";
import { LootTables } from "./loot-tables.js";
import { isOriginCell } from "./grid-utils.js";
import { getBlockKeysFromCell } from "./placement.js";
import { getDisplayBiome, getDisplayTemperature, isAnimalAllowedOnBiome } from "./biome-rules.js";
import { getSkillLevel } from "./conveyor.js";
import { getCurrentSeason, getSeasonTemperatureModifier } from "./seasons.js";
/**
* Total food capacity = sum over food cells of (level × AnimalsPerUnit).
* @param {import("./types.js").GameState} state
* @returns {number}
*/
export function getFoodCapacity(state) {
const cfg = GameConfig.Food;
if (!cfg) return 0;
const unit = cfg.AnimalsPerUnit ?? 5;
let total = 0;
for (const cell of Object.values(state.grid.cells)) {
if (cell !== null && cell !== undefined && cell.kind === "food") {
total += (cell.level ?? 1) * unit;
}
}
return total;
}
/**
* Count origin animal cells (each animal block counts once).
* @param {import("./types.js").GameState} state
* @returns {number}
*/
export function getOriginAnimalCount(state) {
let n = 0;
for (const [key, cell] of Object.entries(state.grid.cells)) {
if (cell !== null && cell !== undefined && cell.kind === "animal" && isOriginCell(key, cell)) n += 1;
}
return n;
}
/** @param {import("./types.js").GameState} state
* @param {number} nowUnix
* @returns {Array<{ key: string, cell: import("./types.js").AnimalCell, lastFed: number }>}
*/
function collectOriginAnimalsByLastFed(state, nowUnix) {
const originAnimals = [];
for (const [key, cell] of Object.entries(state.grid.cells)) {
if (cell !== null && cell !== undefined && cell.kind === "animal" && isOriginCell(key, cell)) {
originAnimals.push({ key, cell, lastFed: cell.lastFedAt ?? cell.placedAt ?? nowUnix });
}
}
originAnimals.sort((a, b) => a.lastFed - b.lastFed);
return originAnimals;
}
/**
* Feed up to `capacity` animals this tick. Animals with oldest lastFedAt are fed first.
* Sets lastFedAt = nowUnix on each fed animal (all cells of the block).
* @param {import("./types.js").GameState} state
* @param {number} nowUnix
*/
export function tickFeeding(state, nowUnix) {
const capacity = getFoodCapacity(state);
if (capacity <= 0) return;
const originAnimals = collectOriginAnimalsByLastFed(state, nowUnix);
let fed = 0;
for (const { key, cell } of originAnimals) {
if (fed >= capacity) break;
const m = key.match(/^(\d+)_(\d+)$/);
if (m) {
setBlockLastFedAt(state, {
ox: Number(m[1]),
oy: Number(m[2]),
w: cell.cellsWide ?? 1,
h: cell.cellsHigh ?? 1,
nowUnix,
});
fed += 1;
}
}
}
function setBlockLastFedAt(state, opts) {
const { ox, oy, w, h, nowUnix } = opts;
for (let dy = 0; dy < h; dy++) {
for (let dx = 0; dx < w; dx++) {
const k = `${ox + dx}_${oy + dy}`;
const c = state.grid.cells[k];
if (c && c.kind === "animal") c.lastFedAt = nowUnix;
}
}
}
/**
* Compute feeding rate = ratio of animals that were fed this period (instantaneous:
* fed count / total origin count). Call after tickFeeding; store in state.feedingRate for display.
* @param {import("./types.js").GameState} state
* @param {number} _nowUnix
* @returns {number} 0..1
*/
export function getFeedingRate(state, _nowUnix) {
const total = getOriginAnimalCount(state);
if (total <= 0) return 1;
const capacity = getFoodCapacity(state);
const fed = Math.min(total, capacity);
return fed / total;
}
/**
* Remove animals and entities that meet death conditions. Increments state.deathCountRecent.
* Causes: not visited, not fed, temperature out of range, biome not allowed,
* research level too low for animal rarity, baby mature not placed in time, reception animal ready not placed in time.
* @param {import("./types.js").GameState} state
* @param {number} nowUnix
*/
/**
* @param {import("./types.js").GameState} state
* @param {number} nowUnix
* @returns {number}
*/
function applyAnimalBlockDeaths(state, nowUnix) {
const maxVisit = GameConfig.Visitor?.MaxSecondsWithoutVisit ?? 300;
const maxFood = GameConfig.Food?.MaxSecondsWithoutFood ?? 120;
const grid = state.grid;
const cells = grid.cells;
const blocksToRemove = collectAnimalDeathBlocks({ state, grid, cells, nowUnix, maxVisit, maxFood });
for (const { ox, oy } of blocksToRemove) {
const blockKeys = getBlockKeysFromCell(state, ox, oy);
for (const k of blockKeys) delete cells[k];
}
return blocksToRemove.length;
}
/**
* @param {import("./types.js").GameState} state
* @param {number} nowUnix
* @returns {number}
*/
function applyBabyAndReceptionDeaths(state, nowUnix) {
const maxMatureNotPlaced = GameConfig.Nursery?.MaxSecondsMatureNotPlaced ?? 90;
const maxReadyNotPlaced = GameConfig.Reception?.MaxSecondsReadyNotPlaced ?? 90;
const babiesRemoved = filterPendingBabies(state, nowUnix, maxMatureNotPlaced);
const receptionRemoved = filterReceptionAnimals(state, nowUnix, maxReadyNotPlaced);
return babiesRemoved + receptionRemoved;
}
export function checkDeathCauses(state, nowUnix) {
const n1 = applyAnimalBlockDeaths(state, nowUnix);
const n2 = applyBabyAndReceptionDeaths(state, nowUnix);
const total = n1 + n2;
if (total > 0) state.deathCountRecent = (state.deathCountRecent ?? 0) + total;
}
/**
* @param {{ state: import("./types.js").GameState, grid: { width: number, height: number }, cells: Record<string, import("./types.js").Cell>, nowUnix: number, maxVisit: number, maxFood: number }} opts
* @returns {Array<{ ox: number, oy: number }>}
*/
function collectAnimalDeathBlocks(opts) {
const { state, grid, cells, nowUnix, maxVisit, maxFood } = opts;
const blocksToRemove = [];
for (const [key, cell] of Object.entries(cells)) {
if (cell === null || cell === undefined || cell.kind !== "animal" || !isOriginCell(key, cell)) {
// skip
} else {
const def = LootTables.Animals[cell.id];
if (def !== null && def !== undefined) {
const entry = maybeDeathBlock({ state, key, cell, grid, nowUnix, maxVisit, maxFood, def });
if (entry) blocksToRemove.push(entry);
}
}
}
return blocksToRemove;
}
/** @param {{ state: import("./types.js").GameState, key: string, cell: import("./types.js").AnimalCell, grid: object, nowUnix: number, maxVisit: number, maxFood: number, def: object }} opts
* @returns {boolean}
*/
function isBlockTempAndBiomeOk(opts) {
const { key, grid, state, def } = opts;
const m = key.match(/^(\d+)_(\d+)$/);
if (!m) return true;
const ox = Number(m[1]);
const oy = Number(m[2]);
const cellBiome = getDisplayBiome(ox, oy, grid);
const baseTemp = getDisplayTemperature(ox, oy, grid);
const seasonMod = getSeasonTemperatureModifier(getCurrentSeason(state));
const cellTemp = baseTemp + seasonMod;
const idealTemp = def.idealTemperature ?? 18;
const tolerance = def.temperatureTolerance ?? 5;
const tempOk = Math.abs(cellTemp - idealTemp) <= tolerance;
const biomeOk = isAnimalAllowedOnBiome(def.biome, cellBiome);
return tempOk && biomeOk;
}
/** @param {{ state: import("./types.js").GameState, key: string, cell: import("./types.js").AnimalCell, grid: object, nowUnix: number, maxVisit: number, maxFood: number, def: object }} opts
* @returns {boolean}
*/
function isBlockVisitAndFedOk(opts) {
const { cell, nowUnix, maxVisit, maxFood } = opts;
const lastVisited = cell.lastVisitedAt ?? cell.placedAt ?? nowUnix;
const lastFed = cell.lastFedAt ?? cell.placedAt ?? nowUnix;
const visitedOk = nowUnix - lastVisited < maxVisit;
const fedOk = nowUnix - lastFed < maxFood;
return visitedOk && fedOk;
}
/** @param {{ state: import("./types.js").GameState, key: string, cell: import("./types.js").AnimalCell, grid: object, nowUnix: number, maxVisit: number, maxFood: number, def: object }} opts
* @returns {boolean}
*/
function isBlockEnvironmentOk(opts) {
const { key } = opts;
const m = key.match(/^(\d+)_(\d+)$/);
if (!m) return true;
return isBlockVisitAndFedOk(opts) && isBlockTempAndBiomeOk(opts);
}
function maybeDeathBlock(opts) {
const { state, key, cell, def } = opts;
const skillLevel = getSkillLevel(state);
const rarityLevel = def.rarityLevel ?? 1;
if (rarityLevel > skillLevel) return getBlockOrigin(opts);
if (!isBlockEnvironmentOk(opts)) return getBlockOrigin(opts);
const aloneOk = checkNotAlone(state, { originKey: key, originCell: cell, nowUnix: opts.nowUnix });
if (!aloneOk) return getBlockOrigin(opts);
return null;
}
/**
* @param {import("./types.js").GameState} state
* @param {string} originKey
* @param {import("./types.js").AnimalCell} originCell
* @param {number} radius
* @returns {number}
*/
function countSameSpeciesInRadius(state, originKey, originCell, radius) {
const m = originKey.match(/^(\d+)_(\d+)$/);
if (!m) return 0;
const ox = Number(m[1]);
const oy = Number(m[2]);
const cells = state.grid.cells;
let count = 0;
for (const [k, c] of Object.entries(cells)) {
if (countSameSpeciesCell({ k, c, originCell, ox, oy, radius })) count += 1;
}
return count;
}
/**
* False if animal should die from solitude (no same species in radius for long enough).
* @param {import("./types.js").GameState} state
* @param {{ originKey: string, originCell: import("./types.js").AnimalCell, nowUnix: number }} opts
* @returns {boolean}
*/
function checkNotAlone(state, opts) {
const { originKey, originCell, nowUnix } = opts;
const cfg = GameConfig.Animal;
if (!cfg || cfg.MinSameSpeciesInRadius === null || cfg.MinSameSpeciesInRadius === undefined || cfg.MinSameSpeciesInRadius <= 0) return true;
const maxAlone = cfg.MaxSecondsAlone ?? 300;
const radius = cfg.RadiusCells ?? 5;
const placedAt = originCell.placedAt ?? nowUnix;
if (nowUnix - placedAt < maxAlone) return true;
const minSame = cfg.MinSameSpeciesInRadius ?? 1;
return countSameSpeciesInRadius(state, originKey, originCell, radius) >= minSame;
}
/**
* @param {{ k: string, c: import("./types.js").Cell, originCell: import("./types.js").AnimalCell, ox: number, oy: number, radius: number }} opts
* @returns {boolean}
*/
function countSameSpeciesCell(opts) {
const { k, c, originCell, ox, oy, radius } = opts;
if (c === null || c === undefined || c.kind !== "animal" || c.id !== originCell.id) return false;
if (!isOriginCell(k, c)) return false;
const km = k.match(/^(\d+)_(\d+)$/);
if (!km) return false;
const kx = Number(km[1]);
const ky = Number(km[2]);
if (kx === ox && ky === oy) return false;
const manhattan = Math.abs(kx - ox) + Math.abs(ky - oy);
return manhattan <= radius;
}
function getBlockOrigin(opts) {
const m = opts.key.match(/^(\d+)_(\d+)$/);
if (!m) return null;
return { ox: Number(m[1]), oy: Number(m[2]) };
}
/**
* @param {import("./types.js").GameState} state
* @param {number} nowUnix
* @param {number} maxMatureNotPlaced
* @returns {number}
*/
function filterPendingBabies(state, nowUnix, maxMatureNotPlaced) {
const pendingBabies = state.pendingBabies ?? [];
let removed = 0;
state.pendingBabies = pendingBabies.filter((p) => {
if (nowUnix <= p.readyAt) return true;
if (nowUnix - p.readyAt >= maxMatureNotPlaced) {
removed += 1;
return false;
}
return true;
});
return removed;
}
/**
* @param {import("./types.js").GameState} state
* @param {number} nowUnix
* @param {number} maxReadyNotPlaced
* @returns {number}
*/
function filterReceptionAnimals(state, nowUnix, maxReadyNotPlaced) {
const receptionAnimals = state.receptionAnimals ?? [];
let removed = 0;
state.receptionAnimals = receptionAnimals.filter((r) => {
if (nowUnix <= r.readyAt) return true;
if (nowUnix - r.readyAt >= maxReadyNotPlaced) {
removed += 1;
return false;
}
return true;
});
return removed;
}