import { LootTables } from "./loot-tables.js"; import { getIncomeMultiplier } from "./mutation-rules.js"; import { getSellValue } from "./economy.js"; import { cellKey } from "./grid-utils.js"; import { getBlockKeysFromCell } from "./placement.js"; import { GameConfig } from "./config.js"; import { getReproductionScore } from "./reproduction.js"; /** * Default duration and price for a new sale listing from config. * @returns {{ duration: number, price: number }} */ function getSaleListingDefaults() { return { duration: GameConfig.Sale?.ListingDurationSeconds ?? 3600, price: GameConfig.Sale?.DefaultPrice ?? 50, }; } /** * Put a mature baby from nursery on sale (phase 10). Removes it from pendingBabies and clears the nursery cell. * @param {import("./types.js").GameState} state * @param {string} nurseryCellKey * @returns {[boolean, string]} [ok, listingId or reason] */ export function addMatureBabyToSale(state, nurseryCellKey) { const now = Math.floor(Date.now() / 1000); const pendingBabies = state.pendingBabies ?? []; const idx = pendingBabies.findIndex( (p) => p.nurseryCellKey === nurseryCellKey && now >= p.readyAt ); if (idx < 0) { const first = pendingBabies.find((p) => p.nurseryCellKey === nurseryCellKey); return [false, first ? "BabyNotMature" : "NoBabyInNursery"]; } const baby = pendingBabies[idx]; state.pendingBabies = pendingBabies.filter((_, i) => i !== idx); const cell = state.grid.cells[nurseryCellKey]; if (cell && cell.kind === "nursery") cell.tokenId = undefined; state.saleListings = state.saleListings ?? []; const { duration, price } = getSaleListingDefaults(); const listingId = `sale_${state.nextTokenId}`; state.nextTokenId += 1; state.saleListings.push({ id: listingId, zooId: state.myZooId ?? "player", animalId: baby.animalId, isBaby: true, price, endAt: now + duration, reproductionScoreAtSale: getReproductionScore(state), }); state.lastEvolutionAt = now; return [true, listingId]; } /** * Put a ready reception animal on sale (phase 10). Removes it from receptionAnimals and clears the reception cell. * @param {import("./types.js").GameState} state * @param {string} receptionCellKey * @returns {[boolean, string]} [ok, listingId or reason] */ export function addReceptionAnimalToSale(state, receptionCellKey) { const now = Math.floor(Date.now() / 1000); const receptionAnimals = state.receptionAnimals ?? []; const idx = receptionAnimals.findIndex( (r) => r.receptionCellKey === receptionCellKey && now >= r.readyAt ); if (idx < 0) { const first = receptionAnimals.find((r) => r.receptionCellKey === receptionCellKey); return [false, first ? "AnimalNotReady" : "NoAnimalInReception"]; } const rec = receptionAnimals[idx]; state.receptionAnimals = receptionAnimals.filter((_, i) => i !== idx); state.saleListings = state.saleListings ?? []; const { duration, price } = getSaleListingDefaults(); const listingId = `sale_${state.nextTokenId}`; state.nextTokenId += 1; state.saleListings.push({ id: listingId, zooId: state.myZooId ?? "player", animalId: rec.animalId, isBaby: false, price, endAt: now + duration, reproductionScoreAtSale: rec.reproductionScoreAtSale ?? getReproductionScore(state), }); state.lastEvolutionAt = now; return [true, listingId]; } /** * Remove expired sale listings. If listing was a baby (isBaby), increment deathCountRecent (bébé invendu meurt). * If listing was an adult (isBaby false), also increment deathCountRecent (vente échouée = mort adulte). * @param {import("./types.js").GameState} state * @param {number} nowUnix */ export function tickSaleListings(state, nowUnix) { const listings = state.saleListings ?? []; const kept = []; let babyDeaths = 0; let adultDeaths = 0; for (const listing of listings) { if (nowUnix < listing.endAt) { kept.push(listing); } else if (listing.isBaby) { babyDeaths += 1; } else { adultDeaths += 1; } } state.saleListings = kept; const totalDeaths = babyDeaths + adultDeaths; if (totalDeaths > 0) state.deathCountRecent = (state.deathCountRecent ?? 0) + totalDeaths; } /** * Compute sell result for animal at (x,y). Returns [false, reason] or [true, { blockKeys, sellValue }]. * @param {import("./types.js").GameState} state * @param {number} x * @param {number} y * @returns {[false, string] | [true, { blockKeys: string[], sellValue: number }]} */ function getSellAnimalResult(state, x, y) { const key = cellKey(x, y); const cell = state.grid.cells[key]; if (cell === null || cell === undefined || cell.kind !== "animal") return [false, "NoAnimal"]; const animalDef = LootTables.Animals[cell.id]; if (animalDef === null || animalDef === undefined) throw new Error("TradeService: unknown animal"); const blockKeys = getBlockKeysFromCell(state, x, y); const originKey = blockKeys[0]; const originCell = state.grid.cells[originKey]; if (originCell === null || originCell === undefined || originCell.kind !== "animal") return [false, "NoAnimal"]; const mutationMultiplier = getIncomeMultiplier(originCell.mutation); const sellValue = getSellValue( animalDef.baseIncomePerSecond, originCell.level, mutationMultiplier, animalDef.sellFactor ); return [true, { blockKeys, sellValue }]; } /** * @param {import("./types.js").GameState} state * @param {number} x * @param {number} y * @returns {[boolean, number | string]} */ export function sellAnimalToNpc(state, x, y) { const result = getSellAnimalResult(state, x, y); if (result[0] === false) return [false, result[1]]; const { blockKeys, sellValue } = result[1]; for (const k of blockKeys) delete state.grid.cells[k]; state.coins += sellValue; state.lastEvolutionAt = Math.floor(Date.now() / 1000); if (state.stats) state.stats.animalsSold = (state.stats.animalsSold ?? 0) + 1; return [true, sellValue]; }