Files
builazoo/web/js/food.js
Nicolas Cantu e031c9a1d2 Initial commit
**Motivations:**
- Initialisation du versionning git pour le projet

**Root causes:**
- N/A (Nouveau projet)

**Correctifs:**
- N/A

**Evolutions:**
- Structure initiale du projet
- Ajout du .gitignore

**Pages affectées:**
- Tous les fichiers
2026-03-03 22:24:17 +01:00

214 lines
7.3 KiB
JavaScript
Raw 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";
/**
* 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;
}
/**
* 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 = [];
for (const [key, cell] of Object.entries(state.grid.cells)) {
if (cell !== null && cell !== undefined && cell.kind === "animal" && isOriginCell(key, cell)) {
const lastFed = cell.lastFedAt ?? cell.placedAt ?? nowUnix;
originAnimals.push({ key, cell, lastFed });
}
}
originAnimals.sort((a, b) => a.lastFed - b.lastFed);
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,
* baby mature not placed in time, reception animal ready not placed in time.
* @param {import("./types.js").GameState} state
* @param {number} nowUnix
*/
export function checkDeathCauses(state, nowUnix) {
const maxVisit = GameConfig.Visitor?.MaxSecondsWithoutVisit ?? 300;
const maxFood = GameConfig.Food?.MaxSecondsWithoutFood ?? 120;
const maxMatureNotPlaced = GameConfig.Nursery?.MaxSecondsMatureNotPlaced ?? 90;
const maxReadyNotPlaced = GameConfig.Reception?.MaxSecondsReadyNotPlaced ?? 90;
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];
state.deathCountRecent = (state.deathCountRecent ?? 0) + 1;
}
const babiesRemoved = filterPendingBabies(state, nowUnix, maxMatureNotPlaced);
if (babiesRemoved > 0) state.deathCountRecent = (state.deathCountRecent ?? 0) + babiesRemoved;
const receptionRemoved = filterReceptionAnimals(state, nowUnix, maxReadyNotPlaced);
if (receptionRemoved > 0) state.deathCountRecent = (state.deathCountRecent ?? 0) + receptionRemoved;
}
/**
* @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 { 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({ key, cell, grid, nowUnix, maxVisit, maxFood, def });
if (entry) blocksToRemove.push(entry);
}
}
}
return blocksToRemove;
}
function maybeDeathBlock(opts) {
const { key, cell, grid, nowUnix, maxVisit, maxFood, def } = opts;
const lastVisited = cell.lastVisitedAt ?? cell.placedAt ?? nowUnix;
const lastFed = cell.lastFedAt ?? cell.placedAt ?? nowUnix;
const m = key.match(/^(\d+)_(\d+)$/);
if (!m) return null;
const ox = Number(m[1]);
const oy = Number(m[2]);
const cellBiome = getDisplayBiome(ox, oy, grid);
const cellTemp = getDisplayTemperature(ox, oy, grid);
const idealTemp = def.idealTemperature ?? 18;
const tolerance = def.temperatureTolerance ?? 5;
const tempOk = Math.abs(cellTemp - idealTemp) <= tolerance;
const biomeOk = isAnimalAllowedOnBiome(def.biome, cellBiome);
const visitedOk = nowUnix - lastVisited < maxVisit;
const fedOk = nowUnix - lastFed < maxFood;
if (!visitedOk || !fedOk || !tempOk || !biomeOk) return { ox, oy };
return null;
}
/**
* @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;
}