// Zone de jeu en pixels CSS fixes (pas de canvas redimensionné comme Icare // — un conteneur DOM de taille fixe suffit et évite toute logique de // "world scale"). export const CONTAINER_WIDTH = 300; export const CONTAINER_HEIGHT = 420; export const WALL_THICKNESS = 20; // Un palier par Citoyen existant (chacun associé à une taille fixe pour // toute la partie, voir melon-game.tsx) plutôt qu'un nombre de paliers // arbitraire : le nombre de tailles suit donc le nombre de Citoyens, pas // une constante fixe. Rayon minimal + croissance géométrique assez marquée // (×1.25 par palier — un ×1.19 précédent rendait deux paliers consécutifs // trop proches en taille : une fusion semblait "changer le visage d'une // pièce déjà posée" plutôt que "remplacer deux petites pièces par une // nettement plus grosse"), plafonnée à MAX_RADIUS pour rester dans // CONTAINER_WIDTH même avec beaucoup de Citoyens. const MIN_RADIUS = 15; const RADIUS_GROWTH = 1.25; const MAX_RADIUS = 110; export function buildTierRadii(tierCount: number): number[] { const count = Math.max(1, tierCount); return Array.from({ length: count }, (_, index) => Math.min(MAX_RADIUS, Math.round(MIN_RADIUS * RADIUS_GROWTH ** index)), ); } // Palier de départ pour la pièce à lâcher : parmi les plus petits // uniquement (sinon les premières fusions seraient trop rares/tardives). export const DROP_MIN_TIER = 0; export const DROP_MAX_TIER_CAP = 3; export const DROP_COOLDOWN_MS = 450; // Ligne de débordement (depuis le haut du bac) + délai de grâce avant game // over : évite qu'un simple rebond juste après une fusion ne déclenche une // fin de partie instantanée. export const DANGER_LINE_Y = CONTAINER_HEIGHT * 0.15; export const GAME_OVER_GRACE_MS = 5000; export const FIXED_DT_MS = 1000 / 60; export const MAX_FRAME_DT_MS = 250; export const MIN_SCORE = 0; // Doit rester aligné avec la contrainte melon_scores_best_score_check // (supabase/schema.sql) — celle-ci est la vraie garantie, cette constante // n'est qu'une valeur de référence côté client. export const MAX_SCORE = 2000; // Positions figées à la main pour certains Citoyens (id de profil → index // de palier, 0 = le plus petit) — les Citoyens restants remplissent les // paliers non réservés, dans leur ordre existant (alphabétique par // pseudo, voir corne/page.tsx). "Position N" telle que demandée par un // humain (compte à partir de 1) correspond à l'index N-1 ici. export const TIER_OVERRIDES: Record = { 6: "7f0f24dc-ede0-4190-a64d-976393073c28", // Épicure, position 7 }; export function applyTierOverrides( citizens: T[], overrides: Record, ): T[] { const byId = new Map(citizens.map((c) => [c.id, c])); const usedIds = new Set(); const result: (T | undefined)[] = new Array(citizens.length); for (const [tierKey, id] of Object.entries(overrides)) { const tier = Number(tierKey); const citizen = byId.get(id); if (citizen && tier >= 0 && tier < citizens.length) { result[tier] = citizen; usedIds.add(id); } } const remaining = citizens.filter((c) => !usedIds.has(c.id)); let remainingIndex = 0; for (let tier = 0; tier < result.length; tier++) { if (!result[tier]) { result[tier] = remaining[remainingIndex++]; } } return result as T[]; }