import { getCellBiome } from "./biome-rules.js"; import { mapServerListingToClient } from "./api-client.js"; import { defaultAnimalWeights } from "./state.js"; import { eggTypeLabel, animalLabel, salesPanelAriaLabel } from "./texts-fr.js"; import { addSalesPanelSellerSection, addSalesPanelBuyerSection, addSalesPanelActiveSection } from "./ui-world-map-sales.js"; import { renderCities } from "./ui-world-map-cities.js"; import { renderLab, renderTruckAndNpcTrucks } from "./ui-world-map-trucks.js"; const EGG_EMOJI = "🥚"; const WORLD_MAP_GRID_COLS = 12; const WORLD_MAP_GRID_ROWS = 8; /** * @param {{ worldMapEl: HTMLElement, state: import("./types.js").GameState, setState: () => void, setError: (s: string) => void, animalEmoji: Record }} ctx * @param {import("./api-client.js").SalesFromApi | null} api * @param {string} playerZooId */ function renderSalesPanel(ctx, api, playerZooId) { const salesPanel = document.createElement("div"); salesPanel.className = "world-map-sales-panel"; salesPanel.setAttribute("aria-label", salesPanelAriaLabel); if (api) { addSalesPanelSellerSection(salesPanel, api, ctx); addSalesPanelBuyerSection(salesPanel, api, ctx); addSalesPanelActiveSection(salesPanel, api, playerZooId, ctx); } if (salesPanel.childNodes.length > 0) ctx.worldMapEl.appendChild(salesPanel); } /** * @param {{ worldMapEl: HTMLElement }} ctx */ function renderCellsLayer(ctx) { const cellsLayer = document.createElement("div"); cellsLayer.className = "world-map-cells"; cellsLayer.setAttribute("aria-hidden", "true"); cellsLayer.style.gridTemplateColumns = `repeat(${WORLD_MAP_GRID_COLS}, 1fr)`; cellsLayer.style.gridTemplateRows = `repeat(${WORLD_MAP_GRID_ROWS}, 1fr)`; for (let row = 0; row < WORLD_MAP_GRID_ROWS; row++) { for (let col = 0; col < WORLD_MAP_GRID_COLS; col++) { const cellDiv = document.createElement("div"); cellDiv.className = "world-map-cell"; const biome = getCellBiome(WORLD_MAP_GRID_COLS, WORLD_MAP_GRID_ROWS, col + 1, row + 1); cellDiv.classList.add(`world-map-cell-${biome.toLowerCase()}`); cellsLayer.appendChild(cellDiv); } } ctx.worldMapEl.appendChild(cellsLayer); } /** * @param {HTMLElement} slotEl * @param {Array<{ animalId: string, isBaby: boolean, price: number }>} listings * @param {{ animalEmoji: Record }} ctx */ function addZooSlotListings(slotEl, listings, ctx) { for (const listing of listings.slice(0, 3)) { const el = document.createElement("div"); el.className = "world-map-sale-listing"; const emoji = ctx.animalEmoji[listing.animalId] ?? "🐾"; const label = listing.isBaby ? `Bébé ${animalLabel[listing.animalId] ?? listing.animalId}` : (animalLabel[listing.animalId] ?? listing.animalId); el.innerHTML = `${emoji}${label}${listing.price} 💰`; el.title = "En vente sur la carte (phase 10)"; slotEl.appendChild(el); } } /** * @param {HTMLElement} slotEl * @param {{ eggType: string, price: number }} offer * @param {string} zooId * @param {{ state: import("./types.js").GameState, setState: () => void, setError: (s: string) => void }} ctx */ function addZooSlotNpcOffer(slotEl, offer, zooId, ctx) { const { setState, setError } = ctx; const el = document.createElement("div"); el.className = "offer-btn world-map-offer world-map-offer-single"; el.setAttribute("role", "button"); el.setAttribute("tabindex", "0"); el.setAttribute("draggable", "true"); const name = eggTypeLabel[offer.eggType] ?? offer.eggType; el.innerHTML = `${EGG_EMOJI}${name}${offer.price} pièces`; let dragStarted = false; el.addEventListener("dragstart", (e) => { dragStarted = true; e.dataTransfer.setData("application/x-builazoo-eggtype", offer.eggType); e.dataTransfer.setData("application/x-builazoo-offer-zooid", zooId); e.dataTransfer.effectAllowed = "copy"; el.classList.add("dragging"); }); el.addEventListener("dragend", () => { dragStarted = false; el.classList.remove("dragging"); }); el.addEventListener("click", () => { if (dragStarted) return; setError("Glissez l'œuf sur le camion pour l'acheter."); setState(); }); el.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); el.click(); } }); slotEl.appendChild(el); } /** * @param {HTMLElement} slotEl * @param {{ animalId: string, price: number } | null} babyOffer * @param {{ animalId: string, price: number } | null} animalOffer * @param {{ animalEmoji: Record }} ctx */ function addZooSlotPlayerOffers(slotEl, babyOffer, animalOffer, ctx) { if (babyOffer) { const el = document.createElement("div"); el.className = "offer-btn world-map-offer"; el.setAttribute("draggable", "true"); const emoji = ctx.animalEmoji[babyOffer.animalId] ?? "🐾"; const name = animalLabel[babyOffer.animalId] ?? babyOffer.animalId; el.innerHTML = `${emoji}Bébé ${name}${babyOffer.price}`; el.addEventListener("dragstart", (e) => { e.dataTransfer.setData("application/x-builazoo-baby-offer", `${babyOffer.animalId}:${babyOffer.price}`); e.dataTransfer.effectAllowed = "copy"; }); slotEl.appendChild(el); } if (animalOffer) { const el = document.createElement("div"); el.className = "offer-btn world-map-offer"; el.setAttribute("draggable", "true"); const emoji = ctx.animalEmoji[animalOffer.animalId] ?? "🐾"; const name = animalLabel[animalOffer.animalId] ?? animalOffer.animalId; el.innerHTML = `${emoji}${name}${animalOffer.price}`; el.addEventListener("dragstart", (e) => { e.dataTransfer.setData("application/x-builazoo-animal-offer", `${animalOffer.animalId}:${animalOffer.price}`); e.dataTransfer.effectAllowed = "copy"; }); slotEl.appendChild(el); } } /** * @param {{ slotEl: HTMLElement, zoo: import("./types.js").WorldZoo, isPlayer: boolean, zooListings: Array<{ animalId: string, isBaby: boolean, price: number }>, oneOffer: import("./conveyor.js").ConveyorOffer | null, playerBabyOffer: { animalId: string, price: number } | null, playerAnimalOffer: { animalId: string, price: number } | null, ctx: { animalEmoji: Record, setState: () => void, setError: (s: string) => void } }} opts */ function fillZooSlotContent(opts) { const { slotEl, zoo, isPlayer, zooListings, oneOffer, playerBabyOffer, playerAnimalOffer, ctx } = opts; if (isPlayer && zooListings.length > 0) { addZooSlotListings(slotEl, zooListings, ctx); } else if (oneOffer) { addZooSlotNpcOffer(slotEl, oneOffer, zoo.id, ctx); } else if (isPlayer && (playerBabyOffer || playerAnimalOffer)) { addZooSlotPlayerOffers(slotEl, playerBabyOffer, playerAnimalOffer, ctx); } else { const iconEl = document.createElement("span"); iconEl.className = "world-map-zoo-icon"; iconEl.setAttribute("aria-hidden", "true"); iconEl.textContent = "🏠"; slotEl.appendChild(iconEl); } } /** * @param {{ worldMapEl: HTMLElement, state: import("./types.js").GameState, setState: () => void, setError: (s: string) => void, animalEmoji: Record }} ctx * @param {import("./types.js").WorldZoo} zoo * @param {Array<{ animalId: string, isBaby: boolean, price: number }>} zooListingsForPlayer * @param {Array} offers */ function buildZooNode(ctx, zoo, zooListingsForPlayer, offers) { const { state, worldMapEl } = ctx; const isPlayer = zoo.id === "player"; const zooOffers = offers.filter((o) => (o.zooId ?? "player") === zoo.id); const oneOffer = !isPlayer && zooOffers.length > 0 ? zooOffers[0] : null; const playerBabyOffer = isPlayer ? zooOffers.find((o) => o.type === "baby") : null; const playerAnimalOffer = isPlayer ? zooOffers.find((o) => o.type === "animal") : null; const node = document.createElement("div"); node.className = "world-map-zoo" + (isPlayer ? " world-map-zoo-player" : ""); node.style.left = `${zoo.x}%`; node.style.top = `${zoo.y}%`; node.dataset.zooId = zoo.id; const nameEl = document.createElement("div"); nameEl.className = "world-map-zoo-name"; nameEl.textContent = zoo.name; node.appendChild(nameEl); if (isPlayer) { const scoreEl = document.createElement("div"); scoreEl.className = "world-map-zoo-reproduction-score"; scoreEl.textContent = `Score repro: ${(state.reproductionScore ?? 0).toFixed(1)}`; node.appendChild(scoreEl); const attrEl = document.createElement("div"); attrEl.className = "world-map-zoo-attractivity-score"; attrEl.textContent = `Score attractivité: ${(state.attractivityScore ?? 0).toFixed(1)}`; node.appendChild(attrEl); } if (!isPlayer && zoo.botState) { const indEl = document.createElement("div"); indEl.className = "world-map-zoo-indicators"; indEl.textContent = `${Math.floor(zoo.botState.coins)} · Parcelle ${zoo.botState.plotLevel}`; node.appendChild(indEl); } const slotEl = document.createElement("div"); slotEl.className = "world-map-zoo-slot"; const zooListings = isPlayer ? zooListingsForPlayer : []; fillZooSlotContent({ slotEl, zoo, isPlayer, zooListings, oneOffer, playerBabyOffer: playerBabyOffer ?? null, playerAnimalOffer: playerAnimalOffer ?? null, ctx }); node.appendChild(slotEl); worldMapEl.appendChild(node); } /** * @param {{ worldMapEl: HTMLElement, state: import("./types.js").GameState, setState: () => void, setError: (s: string) => void, animalEmoji: Record }} ctx * @param {Array} zoos * @param {Array} offers * @param {Array<{ animalId: string, isBaby: boolean, price: number }>} zooListingsForPlayer */ function renderZoos(ctx, zoos, offers, zooListingsForPlayer) { for (const zoo of zoos) { buildZooNode(ctx, zoo, zooListingsForPlayer, offers); } } /** * @param {{ worldMapEl: HTMLElement, worldMapTruckEl: HTMLElement, worldMapNpcTrucksEl: HTMLElement, state: import("./types.js").GameState, setState: () => void, setError: (s: string) => void, playSound: (s: string) => void, animalEmoji: Record, pendingTokenByEggType: Record }} ctx */ export function renderWorldMap(ctx) { ctx.worldMapEl.innerHTML = ""; const playerZooId = ctx.state.myZooId ?? "player"; const api = ctx.state.salesFromApi; const myListingsFromApi = api?.asSeller ? api.asSeller.map(mapServerListingToClient) : null; const zooListingsForPlayer = myListingsFromApi ?? (ctx.state.saleListings ?? []).filter((s) => s.zooId === playerZooId); renderSalesPanel(ctx, api, playerZooId); renderCellsLayer(ctx); const zoos = ctx.state.worldZoos ?? [{ id: "player", name: "Mon zoo", x: 25, y: 50, animalWeights: defaultAnimalWeights() }]; const offers = ctx.state.conveyorOffers || []; renderZoos(ctx, zoos, offers, zooListingsForPlayer); renderCities(ctx); renderLab(ctx); renderTruckAndNpcTrucks(ctx, zoos); } export { renderSalesPanel, renderCellsLayer, renderZoos, WORLD_MAP_GRID_COLS, WORLD_MAP_GRID_ROWS, EGG_EMOJI }; export { renderCities } from "./ui-world-map-cities.js";