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