import { refreshOffers, getSkillLevel } from "./conveyor.js"; import { getPlotUpgradeCost, getTruckUpgradeCost, getWorldMapUpgradeResearchCost } from "./economy.js"; import { getVisitorCount } from "./income.js"; import { getTimePhase } from "./time-weather.js"; import { canPrestige, doPrestige } from "./prestige.js"; import { playSound, isMusicEnabled } from "./audio.js"; import { questDescription, timePhaseLabel, weatherLabel, prestigeHint } from "./texts-fr.js"; import { GameConfig } from "./config.js"; import { renderWorldMap } from "./ui-world-map.js"; import { renderGrid } from "./ui-grid.js"; import { buildGameBar } from "./ui-render-gamebar.js"; import { buildWorldMapSection, buildGridSection } from "./ui-render-dom-panels.js"; /** * @param {{ errEl: HTMLElement }} _setup * @returns {{ tabsWrap: HTMLElement, tabContent: HTMLElement, panelZoo: HTMLElement, panelWorld: HTMLElement }} */ function createTabsStructure(_setup) { const tabsWrap = document.createElement("div"); tabsWrap.className = "tabs-wrap"; tabsWrap.setAttribute("aria-label", "Carte du zoo et carte du monde"); const tabContent = document.createElement("div"); tabContent.className = "tabs-content"; const panelZoo = document.createElement("div"); panelZoo.className = "tab-panel active"; panelZoo.id = "tab-panel-zoo"; panelZoo.setAttribute("role", "tabpanel"); panelZoo.setAttribute("aria-labelledby", "view-toggle"); const panelWorld = document.createElement("div"); panelWorld.className = "tab-panel"; panelWorld.id = "tab-panel-world"; panelWorld.setAttribute("role", "tabpanel"); panelWorld.setAttribute("aria-labelledby", "view-toggle"); return { tabsWrap, tabContent, panelZoo, panelWorld }; } /** * @param {Array<{ descriptionKey: string, target: number, current: number, done: boolean }>} quests * @returns {string} */ function formatQuestListHtml(quests) { return (quests ?? []).map((q) => { const desc = questDescription[q.descriptionKey]; const text = desc ? String(desc).replace("%d", String(q.target)) : q.descriptionKey; const done = q.done ? " ✓" : ""; return `
${text} : ${q.current}/${q.target}${done}
`; }).join(""); } /** * @param {object} gameBarResult * @param {object} setup * @returns {void} */ function updateStatusBody(gameBarResult, setup) { const { state } = setup; const { selected } = setup; const { statusBarCoins, statusBarPlot, statusBarCell, statusBarSkill, statusBarVisitors, statusBarOffers, statusBarTimeWeather, musicBtn, autoModeBtn, prestigeBtn, questListEl, } = gameBarResult; statusBarCoins.valueEl.textContent = String(Math.floor(state.coins)); statusBarPlot.valueEl.textContent = String(Math.floor(state.plotLevel)); statusBarCell.valueEl.textContent = `${Math.floor(selected.x)} ${Math.floor(selected.y)}`; statusBarSkill.valueEl.textContent = String(Math.floor(getSkillLevel(state))); const visitors = getVisitorCount(state); statusBarVisitors.valueEl.textContent = String(Math.floor(visitors)); const offersCount = (state.conveyorOffers ?? []).length; statusBarOffers.valueEl.textContent = String(Math.floor(offersCount)); const timePhase = getTimePhase(state.timeOfDay ?? 6); const weatherVal = weatherLabel[state.weather] ?? state.weather; statusBarTimeWeather.valueEl.textContent = `${timePhaseLabel[timePhase.phase]} · ${weatherVal}`; statusBarTimeWeather.item.className = "status-bar-item status-bar-time-weather weather-" + (state.weather ?? "sun"); musicBtn.classList.toggle("muted", !isMusicEnabled()); autoModeBtn.setAttribute("aria-pressed", state.autoMode ? "true" : "false"); autoModeBtn.title = state.autoMode ? "Mode automatique (désactiver)" : "Mode automatique (activer)"; autoModeBtn.setAttribute("aria-label", state.autoMode ? "Mode automatique actif" : "Activer le mode automatique"); autoModeBtn.textContent = state.autoMode ? "🤖" : "✋"; prestigeBtn.title = String(prestigeHint).replace("%d", String(GameConfig.Prestige.MinCoinsToReset)); prestigeBtn.disabled = !canPrestige(state); questListEl.innerHTML = formatQuestListHtml(state.quests); } /** * @param {object} gameBarResult * @param {object} setup * @returns {() => void} */ function createUpdateStatus(gameBarResult, setup) { return function updateStatus() { updateStatusBody(gameBarResult, setup); }; } /** * @param {object} opts * @returns {() => void} */ function createFullRender(opts) { const { setup, sellZone, plotUpgradeZone, worldMapUpgradeZone, worldMapCounters, worldMapCtx, gridCtx, updateStatus, } = opts; const { state } = setup; return function fullRender() { setup.clampSelection(); updateStatus(); const canUpTruck = (state.truckLevel ?? 1) < ((GameConfig.Truck && GameConfig.Truck.MaxLevel) || 5) && state.coins >= getTruckUpgradeCost(state.truckLevel ?? 1); sellZone.classList.toggle("can-upgrade", canUpTruck); const truckArrow = sellZone.querySelector(".sell-zone-upgrade-arrow"); if (truckArrow) truckArrow.style.display = canUpTruck ? "" : "none"; const plotMaxLevel = GameConfig.Plot.MaxLevel || 8; const canUpgradePlot = (state.plotLevel ?? 1) < plotMaxLevel && state.coins >= getPlotUpgradeCost(state.plotLevel ?? 1); plotUpgradeZone.classList.toggle("can-upgrade", canUpgradePlot); const plotArrow = plotUpgradeZone.querySelector(".plot-upgrade-zone-arrow"); if (plotArrow) plotArrow.style.display = canUpgradePlot ? "" : "none"; updateWorldMapUpgradeAndCounters(worldMapUpgradeZone, worldMapCounters, state); const eggPurchase = state.eggPurchaseTruck; if (eggPurchase && eggPurchase.startAt) { const truckLevel = state.truckLevel ?? 1; const baseMs = (GameConfig.WorldMap && GameConfig.WorldMap.TruckAnimationMs) || 2500; const durationMs = Math.max(1000, (baseMs * 2) / truckLevel); if (Date.now() - eggPurchase.startAt >= durationMs) delete state.eggPurchaseTruck; } renderWorldMap(worldMapCtx); renderGrid(gridCtx); }; } /** * @param {HTMLElement} worldMapUpgradeZone * @param {HTMLElement} worldMapCounters * @param {import("./types.js").GameState} state * @returns {void} */ function updateWorldMapUpgradeAndCounters(worldMapUpgradeZone, worldMapCounters, state) { const mapCfg = GameConfig.WorldMap && GameConfig.WorldMap.MapUpgrade; const mapMaxLevel = mapCfg ? mapCfg.MaxLevel : 5; const currentMapLevel = state.worldMapLevel ?? 1; const mapResearchCost = getWorldMapUpgradeResearchCost(currentMapLevel); const canUpgradeMap = currentMapLevel < mapMaxLevel && (state.researchPoints ?? 0) >= mapResearchCost; worldMapUpgradeZone.classList.toggle("can-upgrade", canUpgradeMap); worldMapUpgradeZone.title = currentMapLevel < mapMaxLevel ? `Agrandir la carte (${mapResearchCost} unités de recherche)` : "Agrandir la carte"; const mapCostEl = worldMapUpgradeZone.querySelector(".world-map-upgrade-zone-cost"); if (mapCostEl) mapCostEl.textContent = currentMapLevel < mapMaxLevel ? ` ${mapResearchCost} 🔬` : ""; const mapArrow = worldMapUpgradeZone.querySelector(".world-map-upgrade-zone-arrow"); if (mapArrow) mapArrow.style.display = canUpgradeMap ? "" : "none"; const babiesForSale = (state.saleListings ?? []).filter((s) => s.isBaby).length; const animalsForSale = (state.saleListings ?? []).filter((s) => !s.isBaby).length; const labsCount = GameConfig.WorldMap && GameConfig.WorldMap.Laboratory ? 1 : 0; const zoosCount = (state.worldZoos ?? []).length; const citiesCount = (GameConfig.WorldMap && GameConfig.WorldMap.Cities) ? GameConfig.WorldMap.Cities.length : 0; worldMapCounters.textContent = ""; const counterEntries = [ ["Bébés à vendre", babiesForSale], ["Animaux à vendre", animalsForSale], ["Laboratoires", labsCount], ["Zoos", zoosCount], ["Villes", citiesCount], ]; for (const [label, value] of counterEntries) { const span = document.createElement("span"); span.className = "world-map-counter"; span.title = label; span.setAttribute("aria-label", `${label}: ${value}`); span.textContent = `${label}: ${value}`; worldMapCounters.appendChild(span); } } /** * @param {{ setup: object, gameBarResult: object, worldMapResult: object, gridResult: object }} opts * @returns {{ worldMapCtx: object, gridCtx: object }} */ function buildFinishContexts(opts) { const { setup, worldMapResult, gridResult } = opts; const worldMapCtx = { worldMapEl: worldMapResult.worldMapEl, worldMapTruckEl: worldMapResult.worldMapTruckEl, worldMapNpcTrucksEl: worldMapResult.worldMapNpcTrucksEl, state: setup.state, setState: setup.setState, setError: setup.setError, playSound, animalEmoji: setup.animalEmoji, pendingTokenByEggType: setup.pendingTokenByEggType, }; const gridCtx = { state: setup.state, setState: setup.setState, setError: setup.setError, playSound, gridEl: gridResult.gridEl, getHatched: setup.getHatched, selected: setup.selected, emptyCellChoice: setup.emptyCellChoiceRef, selectedTokenId: setup.selectedTokenIdRef, lastActionWasDrop: setup.lastActionWasDropRef, clampSelection: setup.clampSelection, animalEmoji: setup.animalEmoji, }; return { worldMapCtx, gridCtx }; } /** * @param {{ root: HTMLElement, setup: object, gameBarResult: object, worldMapResult: object, gridResult: object, updateStatus: () => void }} opts * @returns {() => void} */ function finishBuildUIDOM(opts) { const { setup, gameBarResult, worldMapResult, gridResult, updateStatus } = opts; const { worldMapCtx, gridCtx } = buildFinishContexts(opts); renderWorldMap(worldMapCtx); renderGrid(gridCtx); gameBarResult.prestigeBtn.addEventListener("click", () => { if (!canPrestige(setup.state)) return; doPrestige(setup.state); refreshOffers(setup.state, Math.floor(Date.now() / 1000)); setup.setError(""); playSound("upgrade"); setup.setState(); }); const fullRender = createFullRender({ setup, sellZone: gridResult.sellZone, plotUpgradeZone: gridResult.plotUpgradeZone, worldMapUpgradeZone: worldMapResult.worldMapUpgradeZone, worldMapCounters: worldMapResult.worldMapCounters, worldMapCtx, gridCtx, updateStatus, }); fullRender(); return fullRender; } /** * @param {HTMLElement} root * @param {object} setup * @returns {() => void} */ export function buildUIDOM(root, setup) { const tabs = createTabsStructure(setup); const { panelZoo, panelWorld, tabsWrap, tabContent } = tabs; const gameBarResult = buildGameBar(setup, panelZoo, panelWorld); const worldMapResult = buildWorldMapSection(panelWorld, setup); const gridResult = buildGridSection(panelZoo, setup); tabsWrap.appendChild(setup.errEl); tabContent.appendChild(panelZoo); tabContent.appendChild(panelWorld); tabsWrap.appendChild(tabContent); gameBarResult.gameBarActions.insertBefore(gameBarResult.viewSwitcherWrap, gameBarResult.gameBarActions.firstChild); root.appendChild(gameBarResult.gameBar); root.appendChild(tabsWrap); const updateStatus = createUpdateStatus(gameBarResult, setup); return finishBuildUIDOM({ root, setup, gameBarResult, worldMapResult, gridResult, updateStatus }); }