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
This commit is contained in:
332
web/js/placement.js
Normal file
332
web/js/placement.js
Normal file
@@ -0,0 +1,332 @@
|
||||
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");
|
||||
}
|
||||
Reference in New Issue
Block a user