import { cellKey, withinBounds, getBlockKeys } from "./grid-utils.js"; import { getNurseryBuildCost, getNurseryUpgradeCost, getSouvenirShopBuildCost, getSouvenirShopUpgradeCost, getBuildingBuildCost, getBuildingUpgradeCost, getBuildingMaxLevel, BUILDING_KINDS, } from "./economy.js"; import { GameConfig } from "./config.js"; /** * Origin and dimensions for an animal cell (possibly from originKey). Returns null if not animal. * @param {import("./types.js").GameState} state * @param {import("./types.js").Cell} cell * @param {string} key * @param {{ x: number, y: number }} pos * @returns {{ ox: number, oy: number, w: number, h: number } | null} */ function getAnimalBlockOrigin(state, cell, key, pos) { if (cell === null || cell === undefined || cell.kind !== "animal") return null; let ox = pos.x; let oy = pos.y; let w = cell.cellsWide ?? 1; let h = cell.cellsHigh ?? 1; if (cell.originKey !== null && cell.originKey !== undefined) { const m = cell.originKey.match(/^(\d+)_(\d+)$/); if (m) { ox = Number(m[1]); oy = Number(m[2]); const origin = state.grid.cells[cell.originKey]; if (origin && origin.kind === "animal") { w = origin.cellsWide ?? 1; h = origin.cellsHigh ?? 1; } } } return { ox, oy, w, h }; } /** * All keys that belong to the same animal block as the cell at (x, y). If not an animal or single-cell, returns [cellKey(x,y)]. * @param {import("./types.js").GameState} state * @param {number} x * @param {number} y * @returns {string[]} */ export function getBlockKeysFromCell(state, x, y) { const key = cellKey(x, y); const cell = state.grid.cells[key]; const origin = getAnimalBlockOrigin(state, cell, key, { x, y }); if (origin === null) return [key]; return getBlockKeys(origin.ox, origin.oy, origin.w, origin.h); } /** * Build set of cell keys to exclude when placing (block at excludeOriginKey). * @param {import("./types.js").GameState} state * @param {string} [excludeOriginKey] * @returns {Set} */ function buildExcludeSet(state, excludeOriginKey) { const excludeSet = new Set(); if (excludeOriginKey === null || excludeOriginKey === undefined) return excludeSet; const orig = state.grid.cells[excludeOriginKey]; if (orig && orig.kind === "animal") { const [ox, oy] = excludeOriginKey.split("_").map(Number); const ow = orig.cellsWide ?? 1; const oh = orig.cellsHigh ?? 1; getBlockKeys(ox, oy, ow, oh).forEach((k) => excludeSet.add(k)); } return excludeSet; } /** * Check if a rectangular block can be placed (all cells empty and in bounds). Optionally exclude keys that belong to excludeOriginKey block. * @param {import("./types.js").GameState} state * @param {{ originX: number, originY: number, w: number, h: number, excludeOriginKey?: string }} opts * @returns {[boolean, string?]} */ export function canPlaceMultiCell(state, opts) { const { originX, originY, w, h, excludeOriginKey } = opts; const excludeSet = buildExcludeSet(state, excludeOriginKey); for (let dy = 0; dy < h; dy++) { for (let dx = 0; dx < w; dx++) { const nx = originX + dx; const ny = originY + dy; if (!withinBounds(state.grid.width, state.grid.height, nx, ny)) return [false, "OutOfBounds"]; const k = cellKey(nx, ny); if (!excludeSet.has(k)) { const c = state.grid.cells[k]; if (c !== null && c !== undefined) return [false, "Occupied"]; } } } return [true, undefined]; } /** * Fill a block with the same animal data (originKey set to origin, each cell gets full copy). * @param {import("./types.js").GameState} state * @param {number} originX * @param {number} originY * @param {import("./types.js").AnimalCell} animalData */ export function fillAnimalBlock(state, originX, originY, animalData) { const w = animalData.cellsWide ?? 1; const h = animalData.cellsHigh ?? 1; const originKey = cellKey(originX, originY); const data = { ...animalData, originKey }; for (const k of getBlockKeys(originX, originY, w, h)) { state.grid.cells[k] = { ...data }; } } /** * @param {import("./types.js").GameState} state * @param {number} x * @param {number} y * @returns {[boolean, string?]} */ export function canPlace(state, x, y) { if (!withinBounds(state.grid.width, state.grid.height, x, y)) return [false, "OutOfBounds"]; const key = cellKey(x, y); if (state.grid.cells[key] !== null && state.grid.cells[key] !== undefined) return [false, "Occupied"]; return [true, undefined]; } /** * @param {import("./types.js").GameState} state * @param {{ eggType: string, tokenId: number, x: number, y: number, hatchAt: number, seed: number }} opts * @returns {[boolean, string?]} */ export function placeEgg(state, opts) { const { eggType, tokenId, x, y, hatchAt, seed } = opts; const [ok, reason] = canPlace(state, x, y); if (!ok) return [false, reason]; const key = cellKey(x, y); state.grid.cells[key] = { kind: "egg", eggType, tokenId, hatchAt, seed, }; return [true, undefined]; } /** * Move an animal block from (ox,oy) to (toX, toY). Caller ensures source is animal and keys differ. * @param {import("./types.js").GameState} state * @param {{ blockKeys: string[], ox: number, oy: number, toX: number, toY: number, source: import("./types.js").AnimalCell }} opts * @returns {[boolean, string?]} */ function moveAnimalBlock(state, opts) { const { blockKeys, ox, oy, toX, toY, source } = opts; const w = source.cellsWide ?? 1; const h = source.cellsHigh ?? 1; const originKey = cellKey(ox, oy); const [ok, reason] = canPlaceMultiCell(state, { originX: toX, originY: toY, w, h, excludeOriginKey: originKey }); if (!ok) return [false, reason]; const animalData = { ...source, originKey: cellKey(toX, toY), cellsWide: w, cellsHigh: h }; for (const k of blockKeys) delete state.grid.cells[k]; fillAnimalBlock(state, toX, toY, animalData); return [true, undefined]; } /** * Déplace le contenu d'une case vers une case vide (œuf ou animal). For multi-cell animals, moves the whole block. * @param {import("./types.js").GameState} state * @param {{ fromX: number, fromY: number, toX: number, toY: number }} opts * @returns {[boolean, string?]} */ export function moveCell(state, opts) { const { fromX, fromY, toX, toY } = opts; const fromKey = cellKey(fromX, fromY); const toKey = cellKey(toX, toY); if (fromKey === toKey) return [false, "SameCell"]; const source = state.grid.cells[fromKey]; if (source === null || source === undefined) return [false, "NoSource"]; if (source.kind === "animal") { const blockKeys = getBlockKeysFromCell(state, fromX, fromY); const origin = getAnimalBlockOrigin(state, source, fromKey, { x: fromX, y: fromY }); if (origin === null) return [false, "NoSource"]; return moveAnimalBlock(state, { blockKeys, ox: origin.ox, oy: origin.oy, toX, toY, source }); } const [ok, reason] = canPlace(state, toX, toY); if (!ok) return [false, reason]; state.grid.cells[toKey] = source; delete state.grid.cells[fromKey]; return [true, undefined]; } /** * Set an empty cell to nursery. Cell must be empty. Costs coins. * @param {import("./types.js").GameState} state * @param {number} x * @param {number} y * @returns {[boolean, string?]} */ export function tryBuildNursery(state, x, y) { const [ok, reason] = canPlace(state, x, y); if (!ok) return [ok, reason]; const cost = getNurseryBuildCost(); if (state.coins < cost) return [false, "NotEnoughCoins"]; state.coins -= cost; state.grid.cells[cellKey(x, y)] = { kind: "nursery", level: 1 }; state.lastEvolutionAt = Math.floor(Date.now() / 1000); return [true, undefined]; } /** * Set an empty cell to souvenir shop. Cell must be empty. Costs coins. * @param {import("./types.js").GameState} state * @param {number} x * @param {number} y * @returns {[boolean, string?]} */ export function tryBuildSouvenirShop(state, x, y) { const [ok, reason] = canPlace(state, x, y); if (!ok) return [ok, reason]; const cost = getSouvenirShopBuildCost(); if (state.coins < cost) return [false, "NotEnoughCoins"]; state.coins -= cost; state.grid.cells[cellKey(x, y)] = { kind: "souvenirShop", level: 1 }; state.lastEvolutionAt = Math.floor(Date.now() / 1000); return [true, undefined]; } /** * Upgrade a nursery cell. Cell must be nursery and below max level. * @param {import("./types.js").GameState} state * @param {number} x * @param {number} y * @returns {[boolean, string?]} */ export function tryUpgradeNursery(state, x, y) { const key = cellKey(x, y); const cell = state.grid.cells[key]; if (cell === null || cell === undefined || cell.kind !== "nursery") return [false, "NotNursery"]; const maxLevel = GameConfig.Nursery?.MaxLevel ?? 5; const level = cell.level ?? 1; if (level >= maxLevel) return [false, "NurseryMaxLevel"]; const cost = getNurseryUpgradeCost(level); if (state.coins < cost) return [false, "NotEnoughCoins"]; state.coins -= cost; cell.level = level + 1; state.lastEvolutionAt = Math.floor(Date.now() / 1000); return [true, undefined]; } /** * Upgrade a souvenir shop cell. Cell must be souvenirShop and below max level. * @param {import("./types.js").GameState} state * @param {number} x * @param {number} y * @returns {[boolean, string?]} */ export function tryUpgradeSouvenirShop(state, x, y) { const key = cellKey(x, y); const cell = state.grid.cells[key]; if (cell === null || cell === undefined || cell.kind !== "souvenirShop") return [false, "NotSouvenirShop"]; const maxLevel = GameConfig.SouvenirShop?.MaxLevel ?? 5; const level = cell.level ?? 1; if (level >= maxLevel) return [false, "SouvenirShopMaxLevel"]; const cost = getSouvenirShopUpgradeCost(level); if (state.coins < cost) return [false, "NotEnoughCoins"]; state.coins -= cost; cell.level = level + 1; state.lastEvolutionAt = Math.floor(Date.now() / 1000); return [true, undefined]; } /** * Build a building of the given kind on an empty cell. Used by research, billeterie, food, reception, biomeChangeColor, biomeChangeTemp. * @param {import("./types.js").GameState} state * @param {number} x * @param {number} y * @param {typeof BUILDING_KINDS[number]} kind * @returns {[boolean, string?]} */ export function tryBuildBuilding(state, x, y, kind) { if (!BUILDING_KINDS.includes(kind)) return [false, "UnknownBuilding"]; const [ok, reason] = canPlace(state, x, y); if (!ok) return [ok, reason]; const cost = getBuildingBuildCost(kind); if (state.coins < cost) return [false, "NotEnoughCoins"]; state.coins -= cost; state.grid.cells[cellKey(x, y)] = { kind, level: 1 }; state.lastEvolutionAt = Math.floor(Date.now() / 1000); return [true, undefined]; } /** * Upgrade a building cell of the given kind. Cell must be that kind and below max level. * @param {import("./types.js").GameState} state * @param {number} x * @param {number} y * @param {typeof BUILDING_KINDS[number]} kind * @returns {[boolean, string?]} */ export function tryUpgradeBuilding(state, x, y, kind) { if (!BUILDING_KINDS.includes(kind)) return [false, "UnknownBuilding"]; const key = cellKey(x, y); const cell = state.grid.cells[key]; if (cell === null || cell === undefined || cell.kind !== kind) return [false, `Not${kind.charAt(0).toUpperCase() + kind.slice(1)}`]; const maxLevel = getBuildingMaxLevel(kind); const level = cell.level ?? 1; if (level >= maxLevel) return [false, `${kind.charAt(0).toUpperCase() + kind.slice(1)}MaxLevel`]; const cost = getBuildingUpgradeCost(kind, level); if (state.coins < cost) return [false, "NotEnoughCoins"]; state.coins -= cost; cell.level = level + 1; state.lastEvolutionAt = Math.floor(Date.now() / 1000); return [true, undefined]; } export function tryBuildResearch(state, x, y) { return tryBuildBuilding(state, x, y, "research"); } export function tryUpgradeResearch(state, x, y) { return tryUpgradeBuilding(state, x, y, "research"); } export function tryBuildBilleterie(state, x, y) { return tryBuildBuilding(state, x, y, "billeterie"); } export function tryUpgradeBilleterie(state, x, y) { return tryUpgradeBuilding(state, x, y, "billeterie"); } export function tryBuildFood(state, x, y) { return tryBuildBuilding(state, x, y, "food"); } export function tryUpgradeFood(state, x, y) { return tryUpgradeBuilding(state, x, y, "food"); } export function tryBuildReception(state, x, y) { return tryBuildBuilding(state, x, y, "reception"); } export function tryUpgradeReception(state, x, y) { return tryUpgradeBuilding(state, x, y, "reception"); } export function tryBuildBiomeChangeColor(state, x, y) { return tryBuildBuilding(state, x, y, "biomeChangeColor"); } export function tryUpgradeBiomeChangeColor(state, x, y) { return tryUpgradeBuilding(state, x, y, "biomeChangeColor"); } export function tryBuildBiomeChangeTemp(state, x, y) { return tryBuildBuilding(state, x, y, "biomeChangeTemp"); } export function tryUpgradeBiomeChangeTemp(state, x, y) { return tryUpgradeBuilding(state, x, y, "biomeChangeTemp"); }