import { tryPlaceEgg, getNurseryCellKeysOrdered } from "./zoo.js"; import { tryUpgradeSchool } from "./conveyor.js"; import { tryBuildNursery, tryBuildSouvenirShop, tryBuildResearch, tryBuildBilleterie, tryBuildFood, tryBuildReception, tryBuildBiomeChangeColor, tryBuildBiomeChangeTemp, tryUpgradeNursery, tryUpgradeSouvenirShop, tryUpgradeResearch, tryUpgradeBilleterie, tryUpgradeFood, tryUpgradeReception, tryUpgradeBiomeChangeColor, tryUpgradeBiomeChangeTemp, } from "./placement.js"; import { t, errorMessage } from "./texts-fr.js"; import { attachDragListeners, handleCellDropNurseryReceptionToken, handleCellDropEggTypeOrMove } from "./ui-grid-drag.js"; const BUILD_FNS = [ ["nursery", tryBuildNursery], ["shop", tryBuildSouvenirShop], ["research", tryBuildResearch], ["billeterie", tryBuildBilleterie], ["food", tryBuildFood], ["reception", tryBuildReception], ["biomeColor", tryBuildBiomeChangeColor], ["biomeTemp", tryBuildBiomeChangeTemp], ]; /** * @param {{ state: import("./types.js").GameState, setState: () => void, setError: (s: string) => void, playSound: (s: string) => void, emptyCellChoice: { x: number, y: number } | null, selectedTokenId: number | null, lastActionWasDrop: boolean, clampSelection: () => void, selected: { x: number, y: number } }} ctx * @param {string} choice * @param {number} x * @param {number} y */ export function handleCellClickChoice(ctx, choice, x, y) { const { state, setError } = ctx; for (const [name, fn] of BUILD_FNS) { if (choice === name) { const [ok, reason] = fn(state, x, y); if (!ok) setError(String(t.errorPrefix).replace("%s", errorMessage[reason] ?? reason)); else setError(""); ctx.emptyCellChoice.current = null; return; } } } /** * @param {{ state: import("./types.js").GameState, setState: () => void, setError: (s: string) => void, playSound: (s: string) => void }} ctx * @param {import("./types.js").GridCell} cell * @param {number} x * @param {number} y * @returns {boolean} */ export function handleCellClickUpgradeShops(ctx, cell, x, y) { const { state, setState, setError, playSound } = ctx; if (cell.kind === "souvenirShop") { const [ok, reason] = tryUpgradeSouvenirShop(state, x, y); if (ok) { setError(""); playSound("upgrade"); } else if (reason !== "SouvenirShopMaxLevel") { setError(String(t.upgradeConveyorFailed).replace("%s", errorMessage[reason] ?? reason)); playSound("error"); } setState(); return true; } if (cell.kind === "research") { const [ok, reason] = tryUpgradeResearch(state, x, y); if (ok) { setError(""); playSound("upgrade"); } else if (reason !== "ResearchMaxLevel") { setError(String(t.upgradeConveyorFailed).replace("%s", errorMessage[reason] ?? reason)); playSound("error"); } setState(); return true; } if (cell.kind === "billeterie") { const [ok, reason] = tryUpgradeBilleterie(state, x, y); if (ok) { setError(""); playSound("upgrade"); } else if (reason !== "BilleterieMaxLevel") { setError(String(t.upgradeConveyorFailed).replace("%s", errorMessage[reason] ?? reason)); playSound("error"); } setState(); return true; } if (cell.kind === "food") { const [ok, reason] = tryUpgradeFood(state, x, y); if (ok) { setError(""); playSound("upgrade"); } else if (reason !== "FoodMaxLevel") { setError(String(t.upgradeConveyorFailed).replace("%s", errorMessage[reason] ?? reason)); playSound("error"); } setState(); return true; } return false; } /** * @param {{ state: import("./types.js").GameState, setState: () => void, setError: (s: string) => void, playSound: (s: string) => void }} ctx * @param {import("./types.js").GridCell} cell * @param {{ x: number, y: number, key: string }} pos * @returns {boolean} */ export function handleCellClickUpgradeRest(ctx, cell, pos) { const { x, y, key } = pos; const { state, setState, setError, playSound } = ctx; if (cell.kind === "reception") { const hasAnimal = (state.receptionAnimals ?? []).some((r) => r.receptionCellKey === key); if (!hasAnimal) { const [ok, reason] = tryUpgradeReception(state, x, y); if (ok) { setError(""); playSound("upgrade"); } else if (reason !== "ReceptionMaxLevel") { setError(String(t.upgradeConveyorFailed).replace("%s", errorMessage[reason] ?? reason)); playSound("error"); } } setState(); return true; } if (cell.kind === "biomeChangeColor") { const [ok, reason] = tryUpgradeBiomeChangeColor(state, x, y); if (ok) { setError(""); playSound("upgrade"); } else if (reason !== "BiomeChangeColorMaxLevel") { setError(String(t.upgradeConveyorFailed).replace("%s", errorMessage[reason] ?? reason)); playSound("error"); } setState(); return true; } if (cell.kind === "biomeChangeTemp") { const [ok, reason] = tryUpgradeBiomeChangeTemp(state, x, y); if (ok) { setError(""); playSound("upgrade"); } else if (reason !== "BiomeChangeTempMaxLevel") { setError(String(t.upgradeConveyorFailed).replace("%s", errorMessage[reason] ?? reason)); playSound("error"); } setState(); return true; } if (cell.kind === "school") { const [ok, reason] = tryUpgradeSchool(state, x, y); if (!ok) { setError(String(t.upgradeConveyorFailed).replace("%s", errorMessage[reason] ?? reason)); playSound("error"); } else { setError(""); playSound("schoolUpgrade"); } setState(); return true; } return false; } /** * @param {{ state: import("./types.js").GameState, setState: () => void, setError: (s: string) => void, playSound: (s: string) => void, selectedTokenId: number | null, emptyCellChoice: { x: number, y: number } | null }} ctx * @param {boolean} empty * @param {number} x * @param {number} y */ export function handleCellClickPlaceOrSelect(ctx, empty, x, y) { const { state, setState, setError, playSound } = ctx; const nurseryKeys = getNurseryCellKeysOrdered(state); let firstTokenId = null; for (const k of nurseryKeys) { const c = state.grid.cells[k]; if (c && c.kind === "nursery" && c.tokenId !== null && c.tokenId !== undefined) { firstTokenId = c.tokenId; break; } } const tokenId = ctx.selectedTokenId.current ?? firstTokenId; if (empty && tokenId !== null && tokenId !== undefined) { const nowUnix = Math.floor(Date.now() / 1000); const [ok, reason] = tryPlaceEgg(state, { tokenId, x, y, nowUnix }); if (ok) { ctx.selectedTokenId.current = null; setError(""); playSound("place"); } else { setError(String(t.errorPrefix).replace("%s", errorMessage[reason] ?? reason)); playSound("error"); } setState(); return; } if (empty) ctx.emptyCellChoice.current = { x, y }; setState(); } /** * @param {{ state: import("./types.js").GameState, setState: () => void, setError: (s: string) => void, playSound: (s: string) => void, gridEl: HTMLElement }} ctx * @param {{ div: HTMLElement, cell: import("./types.js").GridCell | null | undefined, x: number, y: number, key: string }} opts */ export function attachCellListeners(ctx, opts) { const { div, cell, x, y, key } = opts; attachDragListeners(ctx, opts); div.addEventListener("dragover", (e) => { e.preventDefault(); const hasEggType = e.dataTransfer.types.includes("application/x-builazoo-eggtype"); const hasTokenId = e.dataTransfer.types.includes("application/x-builazoo-tokenid"); const hasNurseryKey = e.dataTransfer.types.includes("application/x-builazoo-nursery-cell-key"); const hasReceptionKey = e.dataTransfer.types.includes("application/x-builazoo-reception-cell-key"); e.dataTransfer.dropEffect = hasEggType || hasTokenId ? "copy" : "move"; if ((cell === null || cell === undefined) && (hasEggType || hasTokenId || hasNurseryKey || hasReceptionKey)) { div.classList.add("dragover"); } }); div.addEventListener("dragleave", () => div.classList.remove("dragover")); div.addEventListener("drop", (e) => { e.preventDefault(); div.classList.remove("dragover"); handleCellDrop(ctx, e, { toX: Number(div.dataset.x), toY: Number(div.dataset.y), cell }); }); div.addEventListener("click", (e) => handleCellClick(ctx, { e, cell, x, y, key })); div.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); div.click(); } }); } /** * @param {{ state: import("./types.js").GameState, setState: () => void, setError: (s: string) => void, playSound: (s: string) => void, gridEl: HTMLElement }} ctx * @param {DragEvent} e * @param {{ toX: number, toY: number, cell: import("./types.js").GridCell | null | undefined }} target */ function handleCellDrop(ctx, e, target) { const { toX, toY, cell } = target; const empty = cell === null || cell === undefined; if (handleCellDropNurseryReceptionToken(ctx, e, { x: toX, y: toY, empty })) return; handleCellDropEggTypeOrMove(ctx, e, { x: toX, y: toY, empty }); } /** * @param {{ state: import("./types.js").GameState, setState: () => void, setError: (s: string) => void, playSound: (s: string) => void, emptyCellChoice: { x: number, y: number } | null, selectedTokenId: number | null, lastActionWasDrop: boolean, clampSelection: () => void, selected: { x: number, y: number } }} ctx * @param {{ e: Event, cell: import("./types.js").GridCell | null | undefined, x: number, y: number, key: string }} opts */ function handleCellClick(ctx, opts) { const { e, cell, x, y, key } = opts; const { state, setState, setError, playSound } = ctx; if (ctx.lastActionWasDrop.current) { ctx.lastActionWasDrop.current = false; return; } const choiceBtn = e.target.closest(".cell-choice-btn"); const empty = cell === null || cell === undefined; if (choiceBtn && empty && ctx.emptyCellChoice.current && ctx.emptyCellChoice.current.x === x && ctx.emptyCellChoice.current.y === y) { handleCellClickChoice(ctx, choiceBtn.dataset.choice, x, y); setState(); return; } if (cell !== null && cell !== undefined && cell.kind === "nursery" && cell.tokenId !== null && cell.tokenId !== undefined) { ctx.selectedTokenId.current = cell.tokenId; setState(); return; } if (cell !== null && cell !== undefined && cell.kind === "nursery" && (cell.tokenId === null || cell.tokenId === undefined)) { const hasBaby = (state.pendingBabies ?? []).some((p) => p.nurseryCellKey === key); if (!hasBaby) { const [ok, reason] = tryUpgradeNursery(state, x, y); if (ok) { setError(""); playSound("upgrade"); } else if (reason !== "NurseryMaxLevel") { setError(String(t.upgradeConveyorFailed).replace("%s", errorMessage[reason] ?? reason)); playSound("error"); } } setState(); return; } if (handleCellClickUpgrade(ctx, { cell, x, y, key })) return; ctx.selected.x = x; ctx.selected.y = y; ctx.clampSelection(); if (empty && ctx.emptyCellChoice.current && ctx.emptyCellChoice.current.x === x && ctx.emptyCellChoice.current.y === y) { ctx.emptyCellChoice.current = null; setState(); return; } handleCellClickPlaceOrSelect(ctx, empty, x, y); } /** * @param {{ state: import("./types.js").GameState, setState: () => void, setError: (s: string) => void, playSound: (s: string) => void }} ctx * @param {{ cell: import("./types.js").GridCell | null | undefined, x: number, y: number, key: string }} opts * @returns {boolean} */ function handleCellClickUpgrade(ctx, opts) { const { cell, x, y, key } = opts; if (cell === null || cell === undefined) return false; if (handleCellClickUpgradeShops(ctx, cell, x, y)) return true; if (handleCellClickUpgradeRest(ctx, cell, { x, y, key })) return true; return false; } export { handleCellDrop, handleCellClick };