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)), }; }