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
This commit is contained in:
2026-03-03 22:24:17 +01:00
commit e031c9a1d2
155 changed files with 22334 additions and 0 deletions

213
web/js/food.js Normal file
View File

@@ -0,0 +1,213 @@
/**
* 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;
}