/** * Visual state of an animal cell for feedback (no gauges). Used by UI to add CSS classes. * Refs: docs/specs/animal_generique.md, temperature.md. */ import { LootTables } from "./loot-tables.js"; import { getDisplayTemperature } from "./biome-rules.js"; import { getCurrentSeason, getSeasonTemperatureModifier } from "./seasons.js"; import { GameConfig } from "./config.js"; /** * @param {number} cellTemp * @param {number} idealTemp * @param {number} tolerance * @returns {{ cold: boolean, hot: boolean }} */ function getTemperatureState(cellTemp, idealTemp, tolerance) { return { cold: cellTemp < idealTemp - tolerance, hot: cellTemp > idealTemp + tolerance, }; } /** * @param {{ fedAgo: number, visitAgo: number, maxFood: number, maxVisit: number, cold: boolean, hot: boolean }} opts * @returns {{ hungry: boolean, sick: boolean, happy: boolean }} */ function getCareState(opts) { const { fedAgo, visitAgo, maxFood, maxVisit, cold, hot } = opts; const hungry = fedAgo > maxFood * 0.6; const sick = cold || hot || hungry || visitAgo > maxVisit * 0.8; const happy = !sick && fedAgo < maxFood * 0.3 && visitAgo < maxVisit * 0.3 && !cold && !hot; return { hungry, sick, happy }; } const EMPTY_VISUAL = { cold: false, hot: false, hungry: false, sick: false, happy: false }; /** * @param {import("./types.js").AnimalCell} cell * @param {import("./types.js").GameState} state * @param {{ width: number, height: number }} grid * @param {string} originKey * @returns {{ cellTemp: number, idealTemp: number, tolerance: number } | null} */ function getAnimalTempInputs(cell, state, grid, originKey) { const def = LootTables.Animals[cell.id]; if (!def) return null; const m = originKey.match(/^(\d+)_(\d+)$/); if (!m) return null; const ox = Number(m[1]); const oy = Number(m[2]); 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; return { cellTemp, idealTemp, tolerance }; } /** * @param {import("./types.js").AnimalCell} cell * @returns {{ fedAgo: number, visitAgo: number, maxFood: number, maxVisit: number }} */ function getAnimalTimeInputs(cell) { const nowUnix = Math.floor(Date.now() / 1000); const lastFed = cell.lastFedAt ?? cell.placedAt ?? nowUnix; const lastVisited = cell.lastVisitedAt ?? cell.placedAt ?? nowUnix; const maxFood = GameConfig.Food?.MaxSecondsWithoutFood ?? 120; const maxVisit = GameConfig.Visitor?.MaxSecondsWithoutVisit ?? 300; return { fedAgo: nowUnix - lastFed, visitAgo: nowUnix - lastVisited, maxFood, maxVisit }; } /** * @param {import("./types.js").AnimalCell} cell * @param {import("./types.js").GameState} state * @param {{ width: number, height: number }} grid * @param {string} originKey * @returns {{ cellTemp: number, idealTemp: number, tolerance: number, fedAgo: number, visitAgo: number, maxFood: number, maxVisit: number } | null} */ function getAnimalVisualInputs(cell, state, grid, originKey) { const temp = getAnimalTempInputs(cell, state, grid, originKey); if (!temp) return null; const time = getAnimalTimeInputs(cell); return { ...temp, ...time }; } /** * @param {import("./types.js").AnimalCell} cell Origin animal cell. * @param {import("./types.js").GameState} state * @param {{ width: number, height: number }} grid * @param {string} originKey "x_y" of origin cell. * @returns {{ cold: boolean, hot: boolean, hungry: boolean, sick: boolean, happy: boolean }} */ export function getAnimalVisualState(cell, state, grid, originKey) { const inputs = getAnimalVisualInputs(cell, state, grid, originKey); if (!inputs) return EMPTY_VISUAL; const { cold, hot } = getTemperatureState(inputs.cellTemp, inputs.idealTemp, inputs.tolerance); const { hungry, sick, happy } = getCareState({ fedAgo: inputs.fedAgo, visitAgo: inputs.visitAgo, maxFood: inputs.maxFood, maxVisit: inputs.maxVisit, cold, hot }); return { cold, hot, hungry, sick, happy }; }