Files
builazoo/web/js/zoo.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

323 lines
13 KiB
JavaScript

import { GameConfig } from "./config.js";
import { LootTables, getRarityHatchMultiplierForEggType } from "./loot-tables.js";
import { plotSizeFromLevel } from "./grid-utils.js";
import { getPlotUpgradeCost, getWorldMapUpgradeResearchCost } from "./economy.js";
import { findOffer } from "./conveyor.js";
import { placeEgg, fillAnimalBlock } from "./placement.js";
import { getMatureBabyPlacementData, getReceptionAnimalPlacementData } from "./zoo-placement.js";
import { getNurseryCellKeysOrdered, getFreeNurseryCellKey } from "./zoo-nursery.js";
export { getNurseryCellKeysOrdered, getFreeNurseryCellKey };
/**
* First reception cell key that has no reception animal. Returns null if none.
* @param {import("./types.js").GameState} state
* @returns {string | null}
*/
export function getFreeReceptionCellKey(state) {
const usedKeys = new Set((state.receptionAnimals ?? []).map((r) => r.receptionCellKey));
for (const [key, cell] of Object.entries(state.grid.cells)) {
if (cell && cell.kind === "reception" && !usedKeys.has(key)) return key;
}
return null;
}
/**
* Growth duration in seconds for a baby in a nursery of the given level.
* @param {number} nurseryLevel
* @returns {number}
*/
export function getBabyGrowthSeconds(nurseryLevel) {
const base = GameConfig.Nursery?.GrowthSecondsBase ?? 40;
const level = Math.max(1, nurseryLevel ?? 1);
return Math.max(5, Math.floor(base / level));
}
/**
* Acclimatation duration in seconds for reception of the given level.
* @param {number} receptionLevel
* @returns {number}
*/
export function getAcclimatationSeconds(receptionLevel) {
const base = GameConfig.Reception?.AcclimatationSecondsBase ?? 45;
const level = Math.max(1, receptionLevel ?? 1);
return Math.max(10, Math.floor(base / level));
}
/**
* Add a baby to the first free nursery slot. Mutates state.pendingBabies and state.nextTokenId.
* @param {import("./types.js").GameState} state
* @param {string} animalId
* @param {boolean} [fromOtherZoo] true if baby was bought from another zoo (conveyor/world), false if bred
* @returns {[boolean, string?]} [ok, nurseryCellKey or reason]
*/
export function addPendingBaby(state, animalId, fromOtherZoo) {
const key = getFreeNurseryCellKey(state);
if (key === null || key === undefined) return [false, "NoFreeNursery"];
if (LootTables.Animals[animalId] === null || LootTables.Animals[animalId] === undefined) return [false, "UnknownAnimal"];
const cell = state.grid.cells[key];
const level = cell && cell.kind === "nursery" ? (cell.level ?? 1) : 1;
const now = Math.floor(Date.now() / 1000);
const readyAt = now + getBabyGrowthSeconds(level);
const id = `baby_${state.nextTokenId}`;
state.nextTokenId += 1;
state.pendingBabies = state.pendingBabies ?? [];
state.pendingBabies.push({ id, animalId, nurseryCellKey: key, readyAt, fromOtherZoo: fromOtherZoo === true });
state.lastEvolutionAt = now;
return [true, key];
}
/**
* Add an animal to the first free reception slot.
* @param {import("./types.js").GameState} state
* @param {string} animalId
* @returns {[boolean, string?]} [ok, receptionCellKey or reason]
*/
export function addReceptionAnimal(state, animalId) {
const key = getFreeReceptionCellKey(state);
if (key === null || key === undefined) return [false, "NoFreeReception"];
if (LootTables.Animals[animalId] === null || LootTables.Animals[animalId] === undefined) return [false, "UnknownAnimal"];
const cell = state.grid.cells[key];
const level = cell && cell.kind === "reception" ? (cell.level ?? 1) : 1;
const now = Math.floor(Date.now() / 1000);
const readyAt = now + getAcclimatationSeconds(level);
const id = `reception_${state.nextTokenId}`;
state.nextTokenId += 1;
state.receptionAnimals = state.receptionAnimals ?? [];
state.receptionAnimals.push({ id, animalId, receptionCellKey: key, readyAt });
state.lastEvolutionAt = now;
return [true, key];
}
/**
* Buy a baby offer: pay price and add to nursery if slot free.
* @param {import("./types.js").GameState} state
* @param {string} animalId
* @param {number} price
* @returns {[boolean, string | { nurseryCellKey: string }]}
*/
export function tryBuyBaby(state, animalId, price) {
if (state.coins < price) return [false, "NotEnoughCoins"];
const [ok, result] = addPendingBaby(state, animalId, true);
if (!ok) return [false, result];
state.coins -= price;
return [true, { nurseryCellKey: result }];
}
/**
* Buy an animal offer: pay price and add to reception if slot free.
* @param {import("./types.js").GameState} state
* @param {string} animalId
* @param {number} price
* @returns {[boolean, string | { receptionCellKey: string }]}
*/
export function tryBuyAnimal(state, animalId, price) {
if (state.coins < price) return [false, "NotEnoughCoins"];
const [ok, result] = addReceptionAnimal(state, animalId);
if (!ok) return [false, result];
state.coins -= price;
return [true, { receptionCellKey: result }];
}
/**
* Place a mature baby on an empty cell. Baby must be at nurseryCellKey and readyAt <= now.
* @param {import("./types.js").GameState} state
* @param {{ nurseryCellKey: string, toX: number, toY: number, nowUnix: number }} opts
* @returns {[boolean, string?]}
*/
export function placeMatureBabyOnCell(state, opts) {
const { nurseryCellKey, toX, toY, nowUnix } = opts;
const [ok, result] = getMatureBabyPlacementData(state, opts);
if (!ok) return [false, result];
fillAnimalBlock(state, toX, toY, result);
state.pendingBabies = (state.pendingBabies ?? []).filter((p) => p.nurseryCellKey !== nurseryCellKey);
state.lastEvolutionAt = nowUnix;
return [true, undefined];
}
/**
* Place a ready reception animal on an empty cell.
* @param {import("./types.js").GameState} state
* @param {{ receptionCellKey: string, toX: number, toY: number, nowUnix: number }} opts
* @returns {[boolean, string?]}
*/
export function placeReceptionAnimalOnCell(state, opts) {
const { receptionCellKey, toX, toY, nowUnix } = opts;
const [ok, result] = getReceptionAnimalPlacementData(state, opts);
if (!ok) return [false, result];
fillAnimalBlock(state, toX, toY, result);
state.receptionAnimals = (state.receptionAnimals ?? []).filter((r) => r.receptionCellKey !== receptionCellKey);
state.lastEvolutionAt = nowUnix;
return [true, undefined];
}
/**
* Assign a token to the first nursery cell that has no tokenId. Mutates state.grid.cells.
* @param {import("./types.js").GameState} state
* @param {number} tokenId
* @returns {boolean} true if assigned
*/
export function assignTokenToNursery(state, tokenId) {
const keys = getNurseryCellKeysOrdered(state);
for (const key of keys) {
const cell = state.grid.cells[key];
if (cell && cell.kind === "nursery" && (cell.tokenId === null || cell.tokenId === undefined)) {
cell.tokenId = tokenId;
return true;
}
}
return false;
}
/**
* Clear the nursery cell that holds this tokenId.
* @param {import("./types.js").GameState} state
* @param {number} tokenId
*/
export function clearNurseryToken(state, tokenId) {
for (const cell of Object.values(state.grid.cells)) {
if (cell && cell.kind === "nursery" && cell.tokenId === tokenId) {
cell.tokenId = undefined;
return;
}
}
}
/**
* Nursery level of the cell that holds this token (for hatch duration). Returns 1 if not found.
* @param {import("./types.js").GameState} state
* @param {number} tokenId
* @returns {number}
*/
export function getNurseryLevelForToken(state, tokenId) {
for (const cell of Object.values(state.grid.cells)) {
if (cell && cell.kind === "nursery" && cell.tokenId === tokenId) {
return cell.level ?? 1;
}
}
return 1;
}
/**
* Hatch duration in seconds when placing from a nursery (rarity slows down, nursery level speeds up).
* @param {string} eggType
* @param {number} nurseryLevel
* @returns {number}
*/
export function getHatchDurationSeconds(eggType, nurseryLevel) {
const eggDef = LootTables.EggTypes[eggType];
if (eggDef === null || eggDef === undefined) return 30;
const base = eggDef.hatchSeconds;
const rarityMult = getRarityHatchMultiplierForEggType(eggType);
const level = Math.max(1, nurseryLevel ?? 1);
return Math.max(5, Math.floor((base * rarityMult) / level));
}
function consumeToken(state, tokenId) {
const idx = state.pendingEggTokens.findIndex((t) => t.tokenId === tokenId);
if (idx < 0) return null;
const [token] = state.pendingEggTokens.splice(idx, 1);
return token;
}
/**
* @param {import("./types.js").GameState} state
* @param {string} eggType
* @returns {[boolean, { tokenId: number, eggType: string } | string]}
*/
export function tryBuyEgg(state, eggType) {
const offer = findOffer(state, eggType);
if (offer === null || offer === undefined) return [false, "OfferUnavailable"];
const auctionBonus = Math.floor(Math.random() * (offer.price * 0.21));
const finalPrice = offer.price + auctionBonus;
if (state.coins < finalPrice) return [false, "NotEnoughCoins"];
if (LootTables.EggTypes[eggType] === null || LootTables.EggTypes[eggType] === undefined) return [false, "UnknownEgg"];
state.coins -= finalPrice;
const token = { tokenId: state.nextTokenId, eggType, boughtAt: Math.floor(Date.now() / 1000) };
state.nextTokenId += 1;
state.pendingEggTokens.push(token);
assignTokenToNursery(state, token.tokenId);
return [true, { tokenId: token.tokenId, eggType: token.eggType }];
}
/**
* Buy egg from laboratory offer (fixed price, no auction).
* @param {import("./types.js").GameState} state
* @param {string} eggType
* @returns {[boolean, { tokenId: number, eggType: string } | string]}
*/
export function tryBuyLabEgg(state, eggType) {
const offer = state.laboratoryOffer;
if (offer === null || offer === undefined || offer.eggType !== eggType) return [false, "OfferUnavailable"];
if (state.coins < offer.price) return [false, "NotEnoughCoins"];
if (LootTables.EggTypes[eggType] === null || LootTables.EggTypes[eggType] === undefined) return [false, "UnknownEgg"];
state.coins -= offer.price;
state.laboratoryOffer = null;
const token = { tokenId: state.nextTokenId, eggType, boughtAt: Math.floor(Date.now() / 1000) };
state.nextTokenId += 1;
state.pendingEggTokens.push(token);
assignTokenToNursery(state, token.tokenId);
return [true, { tokenId: token.tokenId, eggType: token.eggType }];
}
/**
* @param {import("./types.js").GameState} state
* @param {{ tokenId: number, x: number, y: number, nowUnix: number }} opts
* @returns {[boolean, string?]}
*/
export function tryPlaceEgg(state, opts) {
const { tokenId, x, y, nowUnix } = opts;
const token = consumeToken(state, tokenId);
if (token === null || token === undefined) return [false, "InvalidToken"];
const eggDef = LootTables.EggTypes[token.eggType];
if (eggDef === null || eggDef === undefined) throw new Error("ZooService: token contains unknown egg type");
const nurseryLevel = getNurseryLevelForToken(state, tokenId);
const hatchSeconds = getHatchDurationSeconds(token.eggType, nurseryLevel);
const hatchAt = nowUnix + hatchSeconds;
const seed = Math.floor(Math.random() * 2000000000) + 1;
const [ok, reason] = placeEgg(state, { eggType: token.eggType, tokenId, x, y, hatchAt, seed });
if (!ok) {
state.pendingEggTokens.push(token);
return [false, reason];
}
clearNurseryToken(state, tokenId);
state.lastEvolutionAt = Math.floor(Date.now() / 1000);
if (state.stats) state.stats.eggsPlaced = (state.stats.eggsPlaced ?? 0) + 1;
return [true, undefined];
}
/**
* @param {import("./types.js").GameState} state
* @returns {[boolean, string?]}
*/
export function tryUpgradePlot(state) {
if (state.plotLevel >= GameConfig.Plot.MaxLevel) return [false, "PlotMaxLevel"];
const cost = getPlotUpgradeCost(state.plotLevel);
if (state.coins < cost) return [false, "NotEnoughCoins"];
state.coins -= cost;
state.plotLevel += 1;
state.lastEvolutionAt = Math.floor(Date.now() / 1000);
const [width, height] = plotSizeFromLevel(state.plotLevel);
state.grid.width = width;
state.grid.height = height;
if (state.stats) state.stats.plotUpgrades = (state.stats.plotUpgrades ?? 0) + 1;
return [true, undefined];
}
/**
* @param {import("./types.js").GameState} state
* @returns {[boolean, string?]}
*/
export function tryUpgradeWorldMap(state) {
const cfg = GameConfig.WorldMap && GameConfig.WorldMap.MapUpgrade;
const maxLevel = cfg ? cfg.MaxLevel : 5;
const level = state.worldMapLevel ?? 1;
if (level >= maxLevel) return [false, "WorldMapMaxLevel"];
const cost = getWorldMapUpgradeResearchCost(level);
const points = state.researchPoints ?? 0;
if (points < cost) return [false, "NotEnoughResearch"];
state.researchPoints = points - cost;
state.worldMapLevel = level + 1;
state.lastEvolutionAt = Math.floor(Date.now() / 1000);
return [true, undefined];
}