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