Files
builazoo/web/js/placement.js
Nicolas Cantu e031c9a1d2 Initial commit
**Motivations:**
- Initialisation du versionning git pour le projet

**Root causes:**
- N/A (Nouveau projet)

**Correctifs:**
- N/A

**Evolutions:**
- Structure initiale du projet
- Ajout du .gitignore

**Pages affectées:**
- Tous les fichiers
2026-03-03 22:24:17 +01:00

333 lines
12 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";
/**
* 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];
if (cell === null || cell === undefined || cell.kind !== "animal") return [key];
let ox = x;
let oy = 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 getBlockKeys(ox, oy, w, h);
}
/**
* 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 = new Set();
if (excludeOriginKey !== null && excludeOriginKey !== undefined) {
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));
}
}
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];
}
/**
* 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);
let ox = fromX;
let oy = fromY;
let w = source.cellsWide ?? 1;
let h = source.cellsHigh ?? 1;
if (source.originKey !== null && source.originKey !== undefined) {
const m = source.originKey.match(/^(\d+)_(\d+)$/);
if (m) {
ox = Number(m[1]);
oy = Number(m[2]);
const origin = state.grid.cells[source.originKey];
if (origin && origin.kind === "animal") {
w = origin.cellsWide ?? 1;
h = origin.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: toKey, cellsWide: w, cellsHigh: h };
for (const k of blockKeys) delete state.grid.cells[k];
fillAnimalBlock(state, toX, toY, animalData);
return [true, undefined];
}
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");
}