import { tryBuyLabEgg } from "./zoo.js"; import { GameConfig } from "./config.js"; import { eggTypeLabel, errorMessage, t } from "./texts-fr.js"; const EGG_EMOJI = "πŸ₯š"; /** * @param {{ state: import("./types.js").GameState, setError: (s: string) => void, playSound: (s: string) => void, setState: () => void, pendingTokenByEggType: Record }} ctx * @param {{ eggType: string }} labOffer * @param {boolean} ok * @param {{ tokenId?: number } | string} result */ function handleLabOfferClick(ctx, labOffer, ok, result) { if (!ok) { const msg = errorMessage[result] ?? result; ctx.setError(String(t.buyFailed).replace("%s", msg)); ctx.playSound("error"); ctx.setState(); return; } ctx.setError(""); ctx.playSound("buy"); ctx.pendingTokenByEggType[labOffer.eggType] = result.tokenId; ctx.setState(); } /** * @param {{ eggType: string, price: number }} labOffer * @param {{ state: import("./types.js").GameState, setState: () => void, setError: (s: string) => void, playSound: (s: string) => void, pendingTokenByEggType: Record }} ctx * @returns {HTMLElement} */ function createLabOfferButton(labOffer, ctx) { const { state } = ctx; const el = document.createElement("div"); el.className = "offer-btn world-map-offer world-map-offer-single world-map-lab-offer"; el.setAttribute("role", "button"); el.setAttribute("tabindex", "0"); el.setAttribute("draggable", "true"); const name = eggTypeLabel[labOffer.eggType] ?? labOffer.eggType; el.innerHTML = `${EGG_EMOJI}${name}${labOffer.price} piΓ¨ces`; let dragStarted = false; el.addEventListener("dragstart", (e) => { dragStarted = true; e.dataTransfer.setData("application/x-builazoo-eggtype", labOffer.eggType); e.dataTransfer.effectAllowed = "copy"; el.classList.add("dragging"); }); el.addEventListener("dragend", () => { dragStarted = false; el.classList.remove("dragging"); }); el.addEventListener("click", () => { if (dragStarted) return; const [ok, result] = tryBuyLabEgg(state, labOffer.eggType); handleLabOfferClick(ctx, labOffer, ok, result); }); el.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); el.click(); } }); return el; } /** * @param {{ worldMapEl: HTMLElement, state: import("./types.js").GameState, setState: () => void, setError: (s: string) => void, playSound: (s: string) => void, pendingTokenByEggType: Record }} ctx */ export function renderLab(ctx) { const lab = GameConfig.WorldMap?.Laboratory; if (!lab) return; const { state, worldMapEl } = ctx; const labNode = document.createElement("div"); labNode.className = "world-map-lab"; labNode.style.left = `${lab.x}%`; labNode.style.top = `${lab.y}%`; labNode.dataset.poi = "laboratory"; const labNameEl = document.createElement("div"); labNameEl.className = "world-map-zoo-name"; labNameEl.textContent = lab.name ?? "Laboratoire"; labNode.appendChild(labNameEl); const labSlotEl = document.createElement("div"); labSlotEl.className = "world-map-zoo-slot"; const labOffer = state.laboratoryOffer; if (labOffer) { labSlotEl.appendChild(createLabOfferButton(labOffer, ctx)); } else { const iconEl = document.createElement("span"); iconEl.className = "world-map-zoo-icon"; iconEl.setAttribute("aria-hidden", "true"); iconEl.textContent = "πŸ”¬"; labSlotEl.appendChild(iconEl); } labNode.appendChild(labSlotEl); worldMapEl.appendChild(labNode); } /** * @param {{ worldMapTruckEl: HTMLElement, state: import("./types.js").GameState, setState: () => void }} ctx * @param {Array} zoos * @param {number} truckMs * @returns {boolean} true if truck was updated (need setTimeout) */ function updateTruckSale(ctx, zoos, truckMs) { const { worldMapTruckEl, state, setState } = ctx; const truckSale = state.truckSale; if (!truckSale || !truckSale.toZooId) return false; const elapsed = Date.now() - (truckSale.startAt || 0); if (elapsed >= truckMs) { delete state.truckSale; return false; } const fromZoo = zoos.find((z) => z.id === "player"); const toZoo = zoos.find((z) => z.id === truckSale.toZooId); if (!fromZoo || !toZoo) return false; const progress = elapsed / truckMs; const x = fromZoo.x + (toZoo.x - fromZoo.x) * progress; const y = fromZoo.y + (toZoo.y - fromZoo.y) * progress; worldMapTruckEl.style.display = "block"; worldMapTruckEl.style.left = `${x}%`; worldMapTruckEl.style.top = `${y}%`; worldMapTruckEl.textContent = "🚚"; setTimeout(setState, 50); return true; } /** * @param {{ worldMapTruckEl: HTMLElement, state: import("./types.js").GameState, setState: () => void }} ctx * @param {Array} zoos * @param {number} truckMs * @returns {boolean} true if truck was updated (need setTimeout) */ function updateEggPurchaseTruck(ctx, zoos, truckMs) { const { worldMapTruckEl, state, setState } = ctx; const eggPurchase = state.eggPurchaseTruck; const truckLevel = state.truckLevel ?? 1; if (!eggPurchase || !eggPurchase.startAt) return false; const durationMs = Math.max(1000, (truckMs * 2) / truckLevel); const elapsed = Date.now() - eggPurchase.startAt; if (elapsed >= durationMs) { delete state.eggPurchaseTruck; worldMapTruckEl.style.display = "none"; return false; } const fromZoo = zoos.find((z) => z.id === eggPurchase.fromZooId); const toZoo = zoos.find((z) => z.id === eggPurchase.toZooId); if (!fromZoo || !toZoo) return false; const progress = elapsed / durationMs; const leg = progress < 0.5 ? progress * 2 : (progress - 0.5) * 2; const x = progress < 0.5 ? fromZoo.x + (toZoo.x - fromZoo.x) * leg : toZoo.x + (fromZoo.x - toZoo.x) * leg; const y = progress < 0.5 ? fromZoo.y + (toZoo.y - fromZoo.y) * leg : toZoo.y + (fromZoo.y - toZoo.y) * leg; worldMapTruckEl.style.display = "block"; worldMapTruckEl.style.left = `${x}%`; worldMapTruckEl.style.top = `${y}%`; worldMapTruckEl.textContent = "🚚"; setTimeout(setState, 50); return true; } /** * @param {{ worldMapTruckEl: HTMLElement, worldMapNpcTrucksEl: HTMLElement, state: import("./types.js").GameState, setState: () => void }} ctx * @param {Array} zoos */ function updatePlayerTruck(ctx, zoos) { const { worldMapTruckEl } = ctx; const truckMs = (GameConfig.WorldMap && GameConfig.WorldMap.TruckAnimationMs) || 2500; if (updateTruckSale(ctx, zoos, truckMs)) return; if (updateEggPurchaseTruck(ctx, zoos, truckMs)) return; worldMapTruckEl.style.display = "none"; } /** * @param {{ worldMapNpcTrucksEl: HTMLElement, state: import("./types.js").GameState, setState: () => void }} ctx * @param {Array} zoos * @param {number} truckMs */ function renderNpcTrucks(ctx, zoos, truckMs) { const { worldMapNpcTrucksEl, state } = ctx; worldMapNpcTrucksEl.innerHTML = ""; const npcTrucks = state.worldTruckSales ?? []; for (const truck of npcTrucks) { const fromZoo = zoos.find((z) => z.id === truck.fromZooId); const toZoo = zoos.find((z) => z.id === truck.toZooId); if (fromZoo && toZoo) { const elapsed = Date.now() - (truck.startAt || 0); if (elapsed < truckMs) { const progress = elapsed / truckMs; const x = fromZoo.x + (toZoo.x - fromZoo.x) * progress; const y = fromZoo.y + (toZoo.y - fromZoo.y) * progress; const truckDiv = document.createElement("div"); truckDiv.className = "world-map-truck world-map-truck-npc"; truckDiv.style.left = `${x}%`; truckDiv.style.top = `${y}%`; truckDiv.textContent = "🚚"; worldMapNpcTrucksEl.appendChild(truckDiv); } } } } /** * @param {{ worldMapEl: HTMLElement, worldMapTruckEl: HTMLElement, worldMapNpcTrucksEl: HTMLElement, state: import("./types.js").GameState, setState: () => void }} ctx * @param {Array} zoos */ export function renderTruckAndNpcTrucks(ctx, zoos) { const truckMs = (GameConfig.WorldMap && GameConfig.WorldMap.TruckAnimationMs) || 2500; const truckSale = ctx.state.truckSale; const eggPurchase = ctx.state.eggPurchaseTruck; updatePlayerTruck(ctx, zoos); renderNpcTrucks(ctx, zoos, truckMs); const npcTrucks = ctx.state.worldTruckSales ?? []; if (npcTrucks.length > 0 || (truckSale && truckSale.toZooId) || (eggPurchase && eggPurchase.startAt)) { setTimeout(ctx.setState, 50); } }