Files
builazoo/web/js/visitor-attraction.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

99 lines
3.6 KiB
JavaScript

import { LootTables } from "./loot-tables.js";
import { getSellValue } from "./economy.js";
import { getIncomeMultiplier } from "./mutation-rules.js";
const CELL_PITCH = 52;
const GRID_PADDING = 6;
const CELL_CENTER_OFFSET = 24;
/**
* Center of a cell in layer pixel coordinates (1-based x, y).
* @param {number} x 1-based column
* @param {number} y 1-based row
* @returns {{ cx: number, cy: number }}
*/
function cellCenterPx(x, y) {
const cx = GRID_PADDING + (x - 1) * CELL_PITCH + CELL_CENTER_OFFSET;
const cy = GRID_PADDING + (y - 1) * CELL_PITCH + CELL_CENTER_OFFSET;
return { cx, cy };
}
/**
* Weighted center of the zoo by animal value (sell value). Visitors are attracted to expensive animals.
* @param {import("./types.js").GameState} state
* @param {number} gridWidth
* @param {number} gridHeight
* @returns {{ centerX: number, centerY: number }}
*/
export function getAttractionCenter(state, gridWidth, gridHeight) {
let sumW = 0;
let sumWx = 0;
let sumWy = 0;
for (const [key, cell] of Object.entries(state.grid.cells)) {
if (cell.kind === "animal") {
const def = LootTables.Animals[cell.id];
if (def !== null && def !== undefined) {
const mut = getIncomeMultiplier(cell.mutation ?? "none");
const w = getSellValue(def.baseIncomePerSecond, cell.level ?? 1, mut, def.sellFactor);
const [x, y] = key.split("_").map(Number);
const { cx, cy } = cellCenterPx(x, y);
sumW += w;
sumWx += w * cx;
sumWy += w * cy;
}
}
}
if (sumW <= 0) {
const cx = GRID_PADDING + (gridWidth * CELL_PITCH - 4) / 2;
const cy = GRID_PADDING + (gridHeight * CELL_PITCH - 4) / 2;
return { centerX: cx, centerY: cy };
}
return {
centerX: sumWx / sumW,
centerY: sumWy / sumW,
};
}
/**
* Cell key (1-based x, y) at the given pixel position in the grid layer.
* @param {number} px
* @param {number} py
* @param {number} gridWidth
* @param {number} gridHeight
* @returns {string} key "x_y" or empty if out of bounds
*/
export function getCellKeyFromPixelPosition(px, py, gridWidth, gridHeight) {
const x = 1 + Math.round((px - GRID_PADDING - CELL_CENTER_OFFSET) / CELL_PITCH);
const y = 1 + Math.round((py - GRID_PADDING - CELL_CENTER_OFFSET) / CELL_PITCH);
if (x < 1 || x > gridWidth || y < 1 || y > gridHeight) return "";
return `${x}_${y}`;
}
/**
* Unique position for visitor i: orbits around the attraction center with per-visitor phase, radius and speed.
* A second harmonic gives figure-8 / Lissajous-style paths so each visitor has a distinct walk.
* @param {{ i: number, n: number, t: number, centerX: number, centerY: number, gridWidth: number, gridHeight: number }} opts
* @returns {{ px: number, py: number }}
*/
export function getVisitorPosition(opts) {
const { i, n, t, centerX, centerY, gridWidth, gridHeight } = opts;
const phase1 = (i / Math.max(1, n)) * Math.PI * 2 + ((i * 17) % 100) * 0.01;
const phase2 = ((i * 13) % 100) * 0.063;
const radius1 = 28 + (i * 31) % 55;
const radius2 = 12 + (i * 11) % 18;
const speed1 = 0.15 + ((i * 7) % 50) * 0.008;
const speed2 = 0.08 + ((i * 19) % 40) * 0.006;
const angle1 = phase1 + t * speed1;
const angle2 = phase2 + t * speed2;
const px = centerX + radius1 * Math.cos(angle1) + radius2 * Math.cos(angle2 * 1.3);
const py = centerY + radius1 * Math.sin(angle1) + radius2 * Math.sin(angle2 * 0.9);
const minX = GRID_PADDING;
const minY = GRID_PADDING;
const maxX = GRID_PADDING + gridWidth * CELL_PITCH - 4 - 20;
const maxY = GRID_PADDING + gridHeight * CELL_PITCH - 4 - 20;
return {
px: Math.max(minX, Math.min(maxX, px)),
py: Math.max(minY, Math.min(maxY, py)),
};
}