import { GameConfig } from "./config.js"; import { LootTables } from "./loot-tables.js"; import { getMutationEntries, getIncomeMultiplier } from "./mutation-rules.js"; import { getCellBiome, getBiomesCompatibleWithCell } from "./biome-rules.js"; import { cellKey } from "./grid-utils.js"; import { fillAnimalBlock, canPlaceMultiCell } from "./placement.js"; import { createSeededRng, pickId } from "./weighted-random.js"; const BIOME_TO_EGG_TYPE = { Meadow: "Color_1", Ocean: "Color_6", Mountain: "Color_11", Forest: "Color_1", Freshwater: "Color_6" }; /** * Loot entries for animals that match the cell biome. If the egg type has none, use the egg type for that biome. * @param {string} cellBiome * @param {Array<{ id: string, weight: number }>} loot * @returns {Array<{ id: string, weight: number }>} */ function lootForBiome(cellBiome, loot) { const allowedBiomes = getBiomesCompatibleWithCell(cellBiome); const allowed = loot.filter((entry) => { const def = LootTables.Animals[entry.id]; return def && allowedBiomes.includes(def.biome); }); if (allowed.length > 0) return allowed; const eggType = BIOME_TO_EGG_TYPE[cellBiome]; const fallbackDef = eggType ? LootTables.EggTypes[eggType] : null; return fallbackDef ? fallbackDef.loot : loot; } /** * @param {string} animalId * @param {string} mutationId * @param {number} nowUnix * @param {{ cellsWide?: number, cellsHigh?: number }} dimensions * @returns {import("./types.js").AnimalCell} */ function buildAnimalCell(animalId, mutationId, nowUnix, dimensions = {}) { return { kind: "animal", id: animalId, mutation: mutationId, level: 1, placedAt: nowUnix, lastVisitedAt: nowUnix, lastFedAt: nowUnix, ...dimensions, }; } /** * @param {import("./types.js").GameState} state * @param {string} key * @param {number} nowUnix * @returns {import("./types.js").EggCell | null} */ function getEggCellIfReady(state, key, nowUnix) { const cell = state.grid.cells[key]; if (cell === null || cell === undefined || cell.kind !== "egg") return null; if (nowUnix < cell.hatchAt) return null; return cell; } /** * @param {{ state: import("./types.js").GameState, cell: import("./types.js").EggCell, x: number, y: number, nowUnix: number, eventModifiers: { mutationBonus: number } }} opts * @returns {{ animalData: import("./types.js").AnimalCell, w: number, h: number } | null} */ function getHatchAnimalData(opts) { const { state, cell, x, y, nowUnix, eventModifiers } = opts; const eggDef = LootTables.EggTypes[cell.eggType]; if (eggDef === null || eggDef === undefined) return null; const cellBiome = getCellBiome(state.grid.width, state.grid.height, x, y); const loot = lootForBiome(cellBiome, eggDef.loot); if (loot.length === 0) return null; const rng = createSeededRng(cell.seed); const pickedAnimalId = pickId(rng, loot); const animalDef = LootTables.Animals[pickedAnimalId]; if (animalDef === null || animalDef === undefined) return null; const mutationChance = GameConfig.Mutation.BaseChance + eventModifiers.mutationBonus; let mutationId = "none"; if (rng() < mutationChance) mutationId = pickId(rng, getMutationEntries()); if (getIncomeMultiplier(mutationId) === undefined) mutationId = "none"; const w = animalDef.cellsWide ?? 1; const h = animalDef.cellsHigh ?? 1; const animalData = buildAnimalCell(pickedAnimalId, mutationId, nowUnix, { cellsWide: w, cellsHigh: h }); return { animalData, w, h }; } /** * @param {import("./types.js").GameState} state * @param {{ x: number, y: number, nowUnix: number, eventModifiers: { incomeMultiplier: number, mutationBonus: number } }} opts * @returns {boolean} */ export function tryHatchCell(state, opts) { const { x, y, nowUnix, eventModifiers } = opts; const key = cellKey(x, y); const cell = getEggCellIfReady(state, key, nowUnix); if (cell === null) return false; const eggDef = LootTables.EggTypes[cell.eggType]; if (eggDef === null || eggDef === undefined) throw new Error("HatchingService: unknown egg type"); const hatchData = getHatchAnimalData({ state, cell, x, y, nowUnix, eventModifiers }); if (hatchData === null) return false; const { animalData, w, h } = hatchData; const [canPlace] = canPlaceMultiCell(state, { originX: x, originY: y, w, h, excludeOriginKey: key }); if (!canPlace) return false; fillAnimalBlock(state, x, y, animalData); return true; } /** * @param {import("./types.js").GameState} state * @param {number} nowUnix * @param {{ incomeMultiplier: number, mutationBonus: number }} eventModifiers * @returns {{ changed: boolean, hatched: Array<{ x: number, y: number }> }} */ export function run(state, nowUnix, eventModifiers) { const hatched = []; const keysToProcess = []; for (const [key, cell] of Object.entries(state.grid.cells)) { if (cell.kind === "egg" && nowUnix >= cell.hatchAt) keysToProcess.push(key); } for (const key of keysToProcess) { const m = key.match(/^(\d+)_(\d+)$/); if (m) { const x = Number(m[1]); const y = Number(m[2]); const didHatch = tryHatchCell(state, { x, y, nowUnix, eventModifiers }); if (didHatch && state.grid.cells[cellKey(x, y)]?.kind === "animal") hatched.push({ x, y }); } } return { changed: hatched.length > 0, hatched }; }