**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
356 lines
13 KiB
JavaScript
356 lines
13 KiB
JavaScript
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<string>}
|
|
*/
|
|
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");
|
|
}
|