**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
223 lines
8.3 KiB
JavaScript
223 lines
8.3 KiB
JavaScript
import { GameConfig } from "./config.js";
|
|
import { LootTables, getAnimalToEggTypeMap } from "./loot-tables.js";
|
|
import { defaultAnimalWeights } from "./state.js";
|
|
import { getZooSkillLevel } from "./bot-zoo.js";
|
|
import { getConveyorUpgradeCost, getSchoolUpgradeCost, getTruckUpgradeCost } from "./economy.js";
|
|
import { pickId } from "./weighted-random.js";
|
|
import { cellKey } from "./grid-utils.js";
|
|
|
|
const ANIMAL_TO_EGG_TYPE = getAnimalToEggTypeMap();
|
|
const DEFAULT_ZOO_WEIGHTS = defaultAnimalWeights();
|
|
|
|
/**
|
|
* Skill level = max level among school cells, or state.conveyorLevel for backward compat.
|
|
* @param {import("./types.js").GameState} state
|
|
* @returns {number}
|
|
*/
|
|
export function getSkillLevel(state) {
|
|
let maxSchool = 0;
|
|
for (const cell of Object.values(state.grid.cells)) {
|
|
if (cell.kind === "school") maxSchool = Math.max(maxSchool, cell.level);
|
|
}
|
|
return maxSchool || state.conveyorLevel || 1;
|
|
}
|
|
|
|
/**
|
|
* @param {number} conveyorLevel
|
|
* @returns {Array<{ id: string, weight: number }>}
|
|
*/
|
|
function getEligibleEggTypes(conveyorLevel) {
|
|
const entries = [];
|
|
for (const [eggType, def] of Object.entries(LootTables.EggTypes)) {
|
|
if (conveyorLevel >= def.minConveyorLevel)
|
|
entries.push({ id: eggType, weight: 100 - def.minConveyorLevel * 8 });
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
/**
|
|
* Player zoo weights from grid: more animals of a type => more of that egg type at own zoo.
|
|
* @param {import("./types.js").GameState} state
|
|
* @returns {Record<string, number>}
|
|
*/
|
|
export function getPlayerZooWeights(state) {
|
|
const colorKeys = Object.keys(LootTables.EggTypes);
|
|
const w = Object.fromEntries(colorKeys.map((k) => [k, 0]));
|
|
for (const cell of Object.values(state.grid.cells)) {
|
|
if (cell.kind === "animal") {
|
|
const eggType = ANIMAL_TO_EGG_TYPE[cell.id];
|
|
if (eggType) w[eggType] = (w[eggType] ?? 0) + 1;
|
|
}
|
|
}
|
|
return w;
|
|
}
|
|
|
|
/**
|
|
* @param {import("./types.js").GameState} state
|
|
* @param {object} zoo
|
|
* @param {string} eggType
|
|
* @returns {{ skillLevel: number, weight: number } | null}
|
|
*/
|
|
function getZooSkillAndWeightForEgg(state, zoo, eggType) {
|
|
const skillLevel = zoo.id === "player" ? getSkillLevel(state) : getZooSkillLevel(zoo);
|
|
const eggDef = LootTables.EggTypes[eggType];
|
|
const minLevel = eggDef ? eggDef.minConveyorLevel : 1;
|
|
if (skillLevel < minLevel) return null;
|
|
const playerWeights = zoo.id === "player" ? getPlayerZooWeights(state) : (zoo.animalWeights ?? {});
|
|
const weight = playerWeights[eggType] ?? 0;
|
|
return weight > 0 ? { skillLevel, weight } : null;
|
|
}
|
|
|
|
/**
|
|
* Zoos that can offer this egg type (skill level allows it and zoo has weight for it).
|
|
* @param {import("./types.js").GameState} state
|
|
* @param {string} eggType
|
|
* @returns {Array<{ id: string, weight: number }>}
|
|
*/
|
|
function getZoosForEggType(state, eggType) {
|
|
const zoos = state.worldZoos ?? [{ id: "player", name: "Mon zoo", x: 25, y: 50, animalWeights: DEFAULT_ZOO_WEIGHTS }];
|
|
const entries = [];
|
|
for (const zoo of zoos) {
|
|
const info = getZooSkillAndWeightForEgg(state, zoo, eggType);
|
|
if (info) entries.push({ id: zoo.id, weight: info.weight });
|
|
}
|
|
if (entries.length === 0) entries.push({ id: "player", weight: 1 });
|
|
return entries;
|
|
}
|
|
|
|
/**
|
|
* @param {import("./types.js").GameState} state
|
|
* @param {number} nowUnix
|
|
*/
|
|
export function refreshOffers(state, nowUnix) {
|
|
const rng = () => Math.random();
|
|
const skillLevel = getSkillLevel(state);
|
|
const pool = getEligibleEggTypes(skillLevel);
|
|
const offers = [];
|
|
for (let i = 0; i < GameConfig.Conveyor.OfferCount; i++) {
|
|
const eggType = pickId(rng, pool);
|
|
const eggDef = LootTables.EggTypes[eggType];
|
|
const zooPool = getZoosForEggType(state, eggType);
|
|
const zooId = zooPool.length ? pickId(rng, zooPool) : "player";
|
|
offers.push({ eggType, price: eggDef.price, zooId });
|
|
}
|
|
const animalIds = Object.keys(LootTables.Animals ?? {});
|
|
if (animalIds.length > 0) {
|
|
const babyAnimalId = animalIds[Math.floor(rng() * animalIds.length)];
|
|
const babyDef = LootTables.Animals[babyAnimalId];
|
|
const babyPrice = babyDef ? Math.floor(50 + (babyDef.rarityLevel ?? 1) * 30) : 80;
|
|
offers.push({ type: "baby", animalId: babyAnimalId, price: babyPrice, zooId: "player" });
|
|
const adultAnimalId = animalIds[Math.floor(rng() * animalIds.length)];
|
|
const adultDef = LootTables.Animals[adultAnimalId];
|
|
const adultPrice = adultDef ? Math.floor(80 + (adultDef.rarityLevel ?? 1) * 40) : 120;
|
|
offers.push({ type: "animal", animalId: adultAnimalId, price: adultPrice, zooId: "player" });
|
|
}
|
|
state.conveyorOffers = offers;
|
|
state.lastOfferRefreshAt = nowUnix;
|
|
}
|
|
|
|
/**
|
|
* @param {import("./types.js").GameState} state
|
|
* @param {number} nowUnix
|
|
* @returns {boolean}
|
|
*/
|
|
export function shouldRefresh(state, nowUnix) {
|
|
return nowUnix - state.lastOfferRefreshAt >= GameConfig.Conveyor.RefreshSeconds;
|
|
}
|
|
|
|
/**
|
|
* Upgrade school at cell (x,y). Returns [ok, reason].
|
|
* @param {import("./types.js").GameState} state
|
|
* @param {number} x
|
|
* @param {number} y
|
|
* @returns {[boolean, string?]}
|
|
*/
|
|
export function tryUpgradeSchool(state, x, y) {
|
|
const key = cellKey(x, y);
|
|
const cell = state.grid.cells[key];
|
|
if (cell === null || cell === undefined || cell.kind !== "school") return [false, "NoSchool"];
|
|
const maxLevel = (GameConfig.School && GameConfig.School.MaxLevel) || GameConfig.Conveyor.MaxLevel;
|
|
if (cell.level >= maxLevel) return [false, "ConveyorMaxLevel"];
|
|
const cost = getSchoolUpgradeCost(cell.level);
|
|
if (state.coins < cost) return [false, "NotEnoughCoins"];
|
|
state.coins -= cost;
|
|
cell.level += 1;
|
|
state.conveyorLevel = getSkillLevel(state);
|
|
state.lastEvolutionAt = Math.floor(Date.now() / 1000);
|
|
if (state.stats) state.stats.conveyorUpgrades = (state.stats.conveyorUpgrades ?? 0) + 1;
|
|
return [true, undefined];
|
|
}
|
|
|
|
/**
|
|
* Upgrade truck. Returns [ok, reason].
|
|
* @param {import("./types.js").GameState} state
|
|
* @returns {[boolean, string?]}
|
|
*/
|
|
export function tryUpgradeTruck(state) {
|
|
const level = state.truckLevel ?? 1;
|
|
const maxLevel = (GameConfig.Truck && GameConfig.Truck.MaxLevel) || 5;
|
|
if (level >= maxLevel) return [false, "TruckMaxLevel"];
|
|
const cost = getTruckUpgradeCost(level);
|
|
if (state.coins < cost) return [false, "NotEnoughCoins"];
|
|
state.coins -= cost;
|
|
state.truckLevel = level + 1;
|
|
state.lastEvolutionAt = Math.floor(Date.now() / 1000);
|
|
if (state.stats) state.stats.truckUpgrades = (state.stats.truckUpgrades ?? 0) + 1;
|
|
return [true, undefined];
|
|
}
|
|
|
|
/**
|
|
* @param {import("./types.js").GameState} state
|
|
* @returns {[boolean, string?]}
|
|
*/
|
|
export function tryUpgrade(state) {
|
|
if (state.conveyorLevel >= GameConfig.Conveyor.MaxLevel) return [false, "ConveyorMaxLevel"];
|
|
const cost = getConveyorUpgradeCost(state.conveyorLevel);
|
|
if (state.coins < cost) return [false, "NotEnoughCoins"];
|
|
state.coins -= cost;
|
|
state.conveyorLevel += 1;
|
|
state.lastEvolutionAt = Math.floor(Date.now() / 1000);
|
|
if (state.stats) state.stats.conveyorUpgrades = (state.stats.conveyorUpgrades ?? 0) + 1;
|
|
return [true, undefined];
|
|
}
|
|
|
|
/**
|
|
* @param {import("./types.js").GameState} state
|
|
* @param {string} eggType
|
|
* @returns {{ eggType: string, price: number, zooId?: string } | null}
|
|
*/
|
|
export function findOffer(state, eggType) {
|
|
return state.conveyorOffers.find((o) => o.eggType === eggType) ?? null;
|
|
}
|
|
|
|
/**
|
|
* @param {import("./types.js").GameState} state
|
|
* @param {string} [animalId]
|
|
* @returns {{ type: "baby", animalId: string, price: number, zooId?: string } | null}
|
|
*/
|
|
export function findBabyOffer(state, animalId) {
|
|
const o = state.conveyorOffers.find((x) => x.type === "baby" && (animalId === null || animalId === undefined || x.animalId === animalId));
|
|
return o && o.type === "baby" ? o : null;
|
|
}
|
|
|
|
/**
|
|
* @param {import("./types.js").GameState} state
|
|
* @param {string} [animalId]
|
|
* @returns {{ type: "animal", animalId: string, price: number, zooId?: string } | null}
|
|
*/
|
|
export function findAnimalOffer(state, animalId) {
|
|
const o = state.conveyorOffers.find((x) => x.type === "animal" && (animalId === null || animalId === undefined || x.animalId === animalId));
|
|
return o && o.type === "animal" ? o : null;
|
|
}
|
|
|
|
/**
|
|
* Pick another zoo (not player) for truck sale animation.
|
|
* @param {import("./types.js").GameState} state
|
|
* @returns {string}
|
|
*/
|
|
export function pickSaleTargetZoo(state) {
|
|
const zoos = (state.worldZoos ?? []).filter((z) => z.id !== "player");
|
|
if (zoos.length === 0) return "player";
|
|
return zoos[Math.floor(Math.random() * zoos.length)].id;
|
|
}
|