**Motivations:** - Initialisation du versionning git pour le projet **Root causes:** - N/A (Nouveau projet) **Correctifs:** - N/A **Evolutions:** - Structure initiale du projet - Ajout du .gitignore **Pages affectées:** - Tous les fichiers
1607 lines
80 KiB
JavaScript
1607 lines
80 KiB
JavaScript
import { tryBuyEgg, tryPlaceEgg, tryUpgradePlot, tryUpgradeWorldMap, tryBuyLabEgg, getNurseryCellKeysOrdered, tryBuyBaby, tryBuyAnimal, placeMatureBabyOnCell, placeReceptionAnimalOnCell, addPendingBaby, addReceptionAnimal } from "./zoo.js";
|
|
import { tryUpgrade as _tryUpgradeConveyor, refreshOffers, pickSaleTargetZoo, getSkillLevel, tryUpgradeSchool, tryUpgradeTruck } from "./conveyor.js";
|
|
import { sellAnimalToNpc, addMatureBabyToSale, addReceptionAnimalToSale } from "./trade.js";
|
|
import { getPlotUpgradeCost, getSchoolUpgradeCost, getTruckUpgradeCost, getWorldMapUpgradeResearchCost, getNurseryBuildCost, getSouvenirShopBuildCost, getNurseryUpgradeCost, getSouvenirShopUpgradeCost, getResearchBuildCost, getResearchUpgradeCost, getBilleterieBuildCost, getBilleterieUpgradeCost, getFoodBuildCost, getFoodUpgradeCost, getReceptionBuildCost, getReceptionUpgradeCost, getBiomeChangeColorBuildCost, getBiomeChangeColorUpgradeCost, getBiomeChangeTempBuildCost, getBiomeChangeTempUpgradeCost } from "./economy.js";
|
|
import { moveCell, tryBuildNursery, tryBuildSouvenirShop, tryUpgradeNursery, tryUpgradeSouvenirShop, tryBuildResearch, tryUpgradeResearch, tryBuildBilleterie, tryUpgradeBilleterie, tryBuildFood, tryUpgradeFood, tryBuildReception, tryUpgradeReception, tryBuildBiomeChangeColor, tryUpgradeBiomeChangeColor, tryBuildBiomeChangeTemp, tryUpgradeBiomeChangeTemp } from "./placement.js";
|
|
import { getCellBiome, getDisplayBiome, getDisplayTemperature, getTemperatureBand } from "./biome-rules.js";
|
|
import { getVisitorCount } from "./income.js";
|
|
import { getTimePhase } from "./time-weather.js";
|
|
import { canPrestige, doPrestige } from "./prestige.js";
|
|
import { playSound, setMusicEnabled, isMusicEnabled } from "./audio.js";
|
|
import {
|
|
t,
|
|
eggTypeLabel,
|
|
animalLabel,
|
|
errorMessage,
|
|
questDescription,
|
|
timePhaseLabel,
|
|
weatherLabel,
|
|
prestigeLabel,
|
|
prestigeButton as _prestigeButton,
|
|
prestigeHint,
|
|
visitorsLabel,
|
|
musicLabel,
|
|
sellZoneTitle,
|
|
sellZoneShortLabel,
|
|
restartButton,
|
|
helpRestart,
|
|
questsTitle,
|
|
salesPanelAriaLabel,
|
|
salesPanelMySales,
|
|
salesPanelToRecover,
|
|
salesPanelAuctions,
|
|
salesBtnAccept,
|
|
salesBtnReject,
|
|
salesBtnDeliver,
|
|
salesBtnBid,
|
|
salesPendingValidation,
|
|
salesValidationInMinutes,
|
|
salesBidInputAriaLabel,
|
|
noFreeNursery,
|
|
noFreeReception,
|
|
autoProfileFamilyLabel,
|
|
autoProfileSpecialisationLabel,
|
|
autoProfilePickerTitle,
|
|
autoProfilePickerFamilyStep,
|
|
autoProfilePickerSpecialisationStep,
|
|
autoProfileCancel,
|
|
} from "./texts-fr.js";
|
|
import { getProfilesByFamily, AUTO_MODE_FAMILY_IDS } from "./auto-mode-profiles.js";
|
|
import { GameConfig } from "./config.js";
|
|
import { defaultAnimalWeights } from "./state.js";
|
|
import { getApiBase, createSale, getSales, acceptSale, rejectSale, placeBid, deliverSale, mapServerListingToClient } from "./api-client.js";
|
|
|
|
const EGG_EMOJI = "🥚";
|
|
const EMOJI_BY_COLOR = ["🐰", "🦌", "🐸", "🦎", "🐢", "🐬", "🦭", "🐟", "🦈", "🐳", "🦅", "🐺", "🐻", "🦊", "🐗"];
|
|
const animalEmoji = {};
|
|
for (let c = 0; c < 15; c++) {
|
|
for (let r = 0; r < 5; r++) {
|
|
animalEmoji[`c${c}_r${r}`] = EMOJI_BY_COLOR[c] ?? "🐾";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} labelContent
|
|
* @param {string} tooltipText
|
|
* @returns {HTMLElement}
|
|
*/
|
|
function makeHelpWrap(labelContent, tooltipText) {
|
|
const wrap = document.createElement("div");
|
|
wrap.className = "help-wrap";
|
|
const label = document.createElement("span");
|
|
label.textContent = labelContent;
|
|
const icon = document.createElement("span");
|
|
icon.className = "help-icon";
|
|
icon.setAttribute("aria-label", "Aide");
|
|
icon.textContent = "?";
|
|
const bubble = document.createElement("div");
|
|
bubble.className = "tooltip-bubble";
|
|
bubble.textContent = tooltipText;
|
|
wrap.append(label, icon, bubble);
|
|
return wrap;
|
|
}
|
|
|
|
/**
|
|
* @param {HTMLElement} parent
|
|
* @param {string} titleText
|
|
* @param {string} helpText
|
|
*/
|
|
function _addSectionTitle(parent, titleText, helpText) {
|
|
const section = document.createElement("div");
|
|
section.className = "section-with-help";
|
|
const h2 = document.createElement("h2");
|
|
h2.textContent = titleText;
|
|
section.appendChild(h2);
|
|
section.appendChild(makeHelpWrap("", helpText));
|
|
parent.appendChild(section);
|
|
}
|
|
|
|
/**
|
|
* @param {HTMLElement} root
|
|
* @param {{ state: import("./types.js").GameState, setState: () => void, getLastHatched: () => Array<{ x: number, y: number }>, onRestart?: () => void, updateState?: (partial: Partial<import("./types.js").GameState>) => void }} opts
|
|
* @returns {void}
|
|
*/
|
|
export function render(root, opts) {
|
|
const { state, setState, getLastHatched, onRestart, updateState } = opts;
|
|
const getHatched = getLastHatched ?? (() => []);
|
|
const phase = getTimePhase(state.timeOfDay ?? 6);
|
|
const weather = state.weather || "sun";
|
|
document.body.classList.remove("bg-phase-dawn", "bg-phase-day", "bg-phase-dusk", "bg-phase-night", "bg-weather-sun", "bg-weather-cloudy", "bg-weather-rain");
|
|
document.body.classList.add(`bg-phase-${phase.phase}`, `bg-weather-${weather}`);
|
|
root.innerHTML = "";
|
|
const selected = { x: 1, y: 1 };
|
|
const pendingTokenByEggType = {};
|
|
let selectedTokenId = null;
|
|
let emptyCellChoice = null;
|
|
const errorMsg = { current: "" };
|
|
let lastActionWasDrop = false;
|
|
let sellZoneJustDropped = false;
|
|
|
|
const clampSelection = () => {
|
|
selected.x = Math.max(1, Math.min(state.grid.width, selected.x));
|
|
selected.y = Math.max(1, Math.min(state.grid.height, selected.y));
|
|
};
|
|
|
|
const setError = (msg) => {
|
|
errorMsg.current = msg;
|
|
if (errEl) {
|
|
errEl.textContent = msg;
|
|
errEl.hidden = !msg;
|
|
}
|
|
};
|
|
|
|
const gameBar = document.createElement("div");
|
|
gameBar.className = "game-bar";
|
|
gameBar.setAttribute("aria-label", "Barre du jeu");
|
|
const gameBarTitleWrap = document.createElement("div");
|
|
gameBarTitleWrap.className = "game-bar-title-wrap";
|
|
const gameBarTitle = document.createElement("h1");
|
|
gameBarTitle.className = "game-bar-title";
|
|
gameBarTitle.textContent = t.title;
|
|
const titleHelp = makeHelpWrap("", t.helpStatus);
|
|
titleHelp.querySelector(".tooltip-bubble").classList.add("below");
|
|
gameBarTitleWrap.append(gameBarTitle, titleHelp);
|
|
gameBar.appendChild(gameBarTitleWrap);
|
|
const statusBar = document.createElement("div");
|
|
statusBar.className = "status-bar";
|
|
statusBar.setAttribute("aria-label", "Indicateurs");
|
|
function addStatusItem(iconEmoji, tooltipText, initialValue) {
|
|
const item = document.createElement("span");
|
|
item.className = "status-bar-item";
|
|
const icon = document.createElement("span");
|
|
icon.className = "status-bar-icon";
|
|
icon.setAttribute("aria-hidden", "true");
|
|
icon.title = tooltipText;
|
|
icon.textContent = iconEmoji;
|
|
const value = document.createElement("span");
|
|
value.className = "status-bar-value";
|
|
value.textContent = initialValue;
|
|
item.append(icon, value);
|
|
return { item, valueEl: value };
|
|
}
|
|
const statusBarCoins = addStatusItem("🪙", "Pièces", "0");
|
|
const statusBarPlot = addStatusItem("📐", "Parcelle", "1");
|
|
const statusBarCell = addStatusItem("📍", "Case sélectionnée", "1 1");
|
|
const statusBarSkill = addStatusItem("🎓", "Compétences", "1");
|
|
const statusBarVisitors = addStatusItem("👤", visitorsLabel, "0");
|
|
const statusBarOffers = addStatusItem("🥚", "Œufs à vendre", "0");
|
|
const statusBarTimeWeather = addStatusItem("🌤️", "Météo et heure", "—");
|
|
statusBar.append(
|
|
statusBarCoins.item, statusBarPlot.item, statusBarCell.item, statusBarSkill.item,
|
|
statusBarVisitors.item, statusBarOffers.item, statusBarTimeWeather.item
|
|
);
|
|
gameBar.appendChild(statusBar);
|
|
const gameBarActions = document.createElement("div");
|
|
gameBarActions.className = "game-bar-actions";
|
|
const viewSwitcherWrap = document.createElement("div");
|
|
viewSwitcherWrap.className = "game-bar-view-switcher";
|
|
viewSwitcherWrap.setAttribute("aria-label", "Zoo ou carte du monde");
|
|
const viewToggleBtn = document.createElement("button");
|
|
viewToggleBtn.className = "game-bar-btn game-bar-view-btn";
|
|
viewToggleBtn.type = "button";
|
|
viewToggleBtn.id = "view-toggle";
|
|
viewToggleBtn.setAttribute("aria-label", "Afficher la carte du monde");
|
|
viewToggleBtn.title = "Carte du monde (cliquer pour afficher)";
|
|
viewToggleBtn.textContent = "🗺️";
|
|
viewSwitcherWrap.appendChild(viewToggleBtn);
|
|
function setViewToggleIcon(isZooActive) {
|
|
viewToggleBtn.textContent = isZooActive ? "🦒" : "🗺️";
|
|
viewToggleBtn.setAttribute("aria-label", isZooActive ? "Afficher la carte du monde" : "Afficher la carte du zoo");
|
|
viewToggleBtn.title = isZooActive ? "Carte du monde (cliquer pour afficher)" : "Carte du zoo (cliquer pour afficher)";
|
|
}
|
|
viewToggleBtn.addEventListener("click", () => {
|
|
const showZoo = !panelZoo.classList.contains("active");
|
|
if (showZoo) {
|
|
panelZoo.classList.add("active");
|
|
panelWorld.classList.remove("active");
|
|
setViewToggleIcon(true);
|
|
} else {
|
|
panelWorld.classList.add("active");
|
|
panelZoo.classList.remove("active");
|
|
setViewToggleIcon(false);
|
|
if (getApiBase()) {
|
|
getSales().then((data) => { state.salesFromApi = data; setState(); }).catch(() => {});
|
|
}
|
|
}
|
|
});
|
|
setViewToggleIcon(true);
|
|
const musicBtn = document.createElement("button");
|
|
musicBtn.className = "game-bar-btn game-bar-btn-music" + (isMusicEnabled() ? "" : " muted");
|
|
musicBtn.type = "button";
|
|
musicBtn.setAttribute("aria-label", musicLabel);
|
|
musicBtn.title = musicLabel;
|
|
musicBtn.textContent = "🎵";
|
|
musicBtn.addEventListener("click", () => {
|
|
const next = !isMusicEnabled();
|
|
setMusicEnabled(next);
|
|
try {
|
|
localStorage.setItem("builazoo_music", next ? "1" : "0");
|
|
} catch (_) {
|
|
// ignore localStorage
|
|
}
|
|
musicBtn.classList.toggle("muted", !next);
|
|
});
|
|
gameBarActions.appendChild(musicBtn);
|
|
const autoModeBtn = document.createElement("button");
|
|
autoModeBtn.className = "game-bar-btn game-bar-btn-auto-mode";
|
|
autoModeBtn.type = "button";
|
|
autoModeBtn.id = "auto-mode-btn";
|
|
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 ? "🤖" : "✋";
|
|
autoModeBtn.addEventListener("click", () => {
|
|
if (state.autoMode) {
|
|
updateState({ autoMode: false });
|
|
} else {
|
|
updateState({ autoProfilePickerOpen: true, autoProfilePickerFamily: undefined });
|
|
}
|
|
});
|
|
gameBarActions.appendChild(autoModeBtn);
|
|
if (state.autoProfilePickerOpen) {
|
|
const pickerWrap = document.createElement("div");
|
|
pickerWrap.className = "auto-profile-picker-wrap";
|
|
pickerWrap.setAttribute("role", "dialog");
|
|
pickerWrap.setAttribute("aria-label", autoProfilePickerTitle);
|
|
const pickerTitle = document.createElement("div");
|
|
pickerTitle.className = "auto-profile-picker-title";
|
|
pickerTitle.textContent = autoProfilePickerTitle;
|
|
pickerWrap.appendChild(pickerTitle);
|
|
const familyId = state.autoProfilePickerFamily;
|
|
if (familyId === null || familyId === undefined) {
|
|
const stepLabel = document.createElement("div");
|
|
stepLabel.className = "auto-profile-picker-step";
|
|
stepLabel.textContent = autoProfilePickerFamilyStep;
|
|
pickerWrap.appendChild(stepLabel);
|
|
const familyBtns = document.createElement("div");
|
|
familyBtns.className = "auto-profile-picker-families";
|
|
for (const fid of AUTO_MODE_FAMILY_IDS) {
|
|
const btn = document.createElement("button");
|
|
btn.type = "button";
|
|
btn.className = "auto-profile-picker-family-btn";
|
|
btn.textContent = autoProfileFamilyLabel[fid] ?? `Famille ${fid}`;
|
|
btn.addEventListener("click", () => updateState({ autoProfilePickerFamily: fid }));
|
|
familyBtns.appendChild(btn);
|
|
}
|
|
pickerWrap.appendChild(familyBtns);
|
|
} else {
|
|
const stepLabel = document.createElement("div");
|
|
stepLabel.className = "auto-profile-picker-step";
|
|
stepLabel.textContent = autoProfilePickerSpecialisationStep;
|
|
pickerWrap.appendChild(stepLabel);
|
|
const profiles = getProfilesByFamily(familyId);
|
|
const specWrap = document.createElement("div");
|
|
specWrap.className = "auto-profile-picker-specialisations";
|
|
for (const prof of profiles) {
|
|
const btn = document.createElement("button");
|
|
btn.type = "button";
|
|
btn.className = "auto-profile-picker-spec-btn";
|
|
btn.textContent = autoProfileSpecialisationLabel[String(prof.id)] ?? `Profil ${prof.id}`;
|
|
btn.addEventListener("click", () => {
|
|
updateState({
|
|
autoModeProfileId: prof.id,
|
|
autoMode: true,
|
|
autoProfilePickerOpen: false,
|
|
autoProfilePickerFamily: undefined,
|
|
});
|
|
});
|
|
specWrap.appendChild(btn);
|
|
}
|
|
pickerWrap.appendChild(specWrap);
|
|
}
|
|
const cancelBtn = document.createElement("button");
|
|
cancelBtn.type = "button";
|
|
cancelBtn.className = "auto-profile-picker-cancel";
|
|
cancelBtn.textContent = autoProfileCancel;
|
|
cancelBtn.addEventListener("click", () => updateState({ autoProfilePickerOpen: false, autoProfilePickerFamily: undefined }));
|
|
pickerWrap.appendChild(cancelBtn);
|
|
gameBarActions.appendChild(pickerWrap);
|
|
}
|
|
gameBarActions.insertBefore(viewSwitcherWrap, gameBarActions.firstChild);
|
|
const prestigeBtn = document.createElement("button");
|
|
prestigeBtn.className = "game-bar-btn game-bar-btn-prestige";
|
|
prestigeBtn.type = "button";
|
|
prestigeBtn.setAttribute("aria-label", prestigeLabel);
|
|
prestigeBtn.title = prestigeHint;
|
|
prestigeBtn.textContent = "⭐";
|
|
gameBarActions.appendChild(prestigeBtn);
|
|
const restartBtn = document.createElement("button");
|
|
restartBtn.className = "game-bar-btn game-bar-btn-restart";
|
|
restartBtn.type = "button";
|
|
restartBtn.setAttribute("aria-label", restartButton);
|
|
restartBtn.title = helpRestart;
|
|
restartBtn.textContent = "🔄";
|
|
if (onRestart) {
|
|
restartBtn.addEventListener("click", () => onRestart());
|
|
} else {
|
|
restartBtn.disabled = true;
|
|
}
|
|
gameBarActions.appendChild(restartBtn);
|
|
const questWrap = document.createElement("div");
|
|
questWrap.className = "game-bar-quest-wrap";
|
|
const questBtn = document.createElement("button");
|
|
questBtn.className = "game-bar-btn game-bar-btn-quest";
|
|
questBtn.type = "button";
|
|
questBtn.setAttribute("aria-label", questsTitle);
|
|
questBtn.setAttribute("aria-expanded", "false");
|
|
questBtn.title = questsTitle;
|
|
questBtn.textContent = "📋";
|
|
const questDropdown = document.createElement("div");
|
|
questDropdown.className = "quest-dropdown";
|
|
questDropdown.setAttribute("role", "dialog");
|
|
questDropdown.setAttribute("aria-label", questsTitle);
|
|
const questDropdownTitle = document.createElement("div");
|
|
questDropdownTitle.className = "quest-dropdown-title";
|
|
questDropdownTitle.textContent = questsTitle;
|
|
questDropdown.appendChild(questDropdownTitle);
|
|
const questListEl = document.createElement("div");
|
|
questListEl.className = "quest-list";
|
|
questDropdown.appendChild(questListEl);
|
|
questWrap.appendChild(questBtn);
|
|
questWrap.appendChild(questDropdown);
|
|
questBtn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
const open = questWrap.classList.toggle("open");
|
|
questBtn.setAttribute("aria-expanded", String(open));
|
|
});
|
|
document.addEventListener("click", () => {
|
|
questWrap.classList.remove("open");
|
|
questBtn.setAttribute("aria-expanded", "false");
|
|
});
|
|
questDropdown.addEventListener("click", (e) => e.stopPropagation());
|
|
gameBarActions.appendChild(questWrap);
|
|
gameBar.appendChild(gameBarActions);
|
|
|
|
const errEl = document.createElement("div");
|
|
errEl.className = "error-msg";
|
|
errEl.hidden = true;
|
|
|
|
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");
|
|
|
|
const worldMapWrap = document.createElement("div");
|
|
worldMapWrap.className = "world-map-wrap world-map-wrap-square";
|
|
const worldMapEl = document.createElement("div");
|
|
worldMapEl.className = "world-map world-map-biomes";
|
|
const mapLevel = state.worldMapLevel ?? 1;
|
|
const zoom = Math.min(0.65 + (mapLevel - 1) * 0.2, 1.45);
|
|
worldMapEl.style.transformOrigin = "50% 50%";
|
|
worldMapEl.style.transform = `scale(${zoom})`;
|
|
worldMapWrap.appendChild(worldMapEl);
|
|
const worldMapTruckEl = document.createElement("div");
|
|
worldMapTruckEl.className = "world-map-truck";
|
|
worldMapTruckEl.setAttribute("aria-hidden", "true");
|
|
worldMapWrap.appendChild(worldMapTruckEl);
|
|
const worldMapNpcTrucksEl = document.createElement("div");
|
|
worldMapNpcTrucksEl.className = "world-map-trucks";
|
|
worldMapNpcTrucksEl.setAttribute("aria-hidden", "true");
|
|
worldMapWrap.appendChild(worldMapNpcTrucksEl);
|
|
panelWorld.appendChild(worldMapWrap);
|
|
|
|
const worldMapActions = document.createElement("div");
|
|
worldMapActions.className = "world-map-actions";
|
|
const worldMapUpgradeZone = document.createElement("div");
|
|
worldMapUpgradeZone.className = "world-map-upgrade-zone";
|
|
worldMapUpgradeZone.setAttribute("aria-label", "Agrandir la carte");
|
|
worldMapUpgradeZone.title = "Agrandir la carte";
|
|
worldMapUpgradeZone.innerHTML = "<span class=\"world-map-upgrade-zone-icon\" aria-hidden=\"true\">🗺️</span><span class=\"world-map-upgrade-zone-label\">Agrandir carte</span><span class=\"world-map-upgrade-zone-cost\" aria-hidden=\"true\"></span><span class=\"world-map-upgrade-zone-arrow\" aria-hidden=\"true\">▲</span>";
|
|
worldMapUpgradeZone.setAttribute("role", "button");
|
|
worldMapUpgradeZone.setAttribute("tabindex", "0");
|
|
worldMapUpgradeZone.addEventListener("click", () => {
|
|
const [ok, reason] = tryUpgradeWorldMap(state);
|
|
if (!ok) {
|
|
setError(String(t.upgradeWorldMapFailed).replace("%s", errorMessage[reason] ?? reason));
|
|
playSound("error");
|
|
} else {
|
|
setError("");
|
|
playSound("worldMapUpgrade");
|
|
}
|
|
setState();
|
|
});
|
|
worldMapUpgradeZone.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter" || e.key === " ") {
|
|
e.preventDefault();
|
|
worldMapUpgradeZone.click();
|
|
}
|
|
});
|
|
worldMapActions.appendChild(worldMapUpgradeZone);
|
|
const worldMapCounters = document.createElement("div");
|
|
worldMapCounters.className = "world-map-counters";
|
|
worldMapCounters.setAttribute("aria-label", "Compteurs carte du monde");
|
|
worldMapActions.appendChild(worldMapCounters);
|
|
const worldMapTruckDropZone = document.createElement("div");
|
|
worldMapTruckDropZone.className = "world-map-truck-drop-zone";
|
|
worldMapTruckDropZone.setAttribute("aria-label", "Camion pour acheter un œuf");
|
|
worldMapTruckDropZone.title = "Glissez un œuf ici pour l'acheter";
|
|
worldMapTruckDropZone.innerHTML = "<span class=\"world-map-truck-drop-icon\" aria-hidden=\"true\">🚚</span><span class=\"world-map-truck-drop-label\">Acheter œuf</span>";
|
|
worldMapTruckDropZone.addEventListener("dragover", (e) => {
|
|
e.preventDefault();
|
|
const hasOffer = e.dataTransfer.types.includes("application/x-builazoo-eggtype")
|
|
|| e.dataTransfer.types.includes("application/x-builazoo-baby-offer")
|
|
|| e.dataTransfer.types.includes("application/x-builazoo-animal-offer");
|
|
e.dataTransfer.dropEffect = hasOffer ? "copy" : "none";
|
|
if (hasOffer) worldMapTruckDropZone.classList.add("dragover");
|
|
});
|
|
worldMapTruckDropZone.addEventListener("dragleave", () => worldMapTruckDropZone.classList.remove("dragover"));
|
|
worldMapTruckDropZone.addEventListener("drop", (e) => {
|
|
e.preventDefault();
|
|
worldMapTruckDropZone.classList.remove("dragover");
|
|
const babyOffer = e.dataTransfer.getData("application/x-builazoo-baby-offer");
|
|
if (babyOffer) {
|
|
const [animalId, priceStr] = babyOffer.split(":");
|
|
const price = Number(priceStr) || 80;
|
|
const [ok, result] = tryBuyBaby(state, animalId, price);
|
|
if (!ok) setError(String(t.buyFailed).replace("%s", errorMessage[result] ?? result));
|
|
else setError("");
|
|
playSound(ok ? "buy" : "error");
|
|
setState();
|
|
return;
|
|
}
|
|
const animalOffer = e.dataTransfer.getData("application/x-builazoo-animal-offer");
|
|
if (animalOffer) {
|
|
const [animalId, priceStr] = animalOffer.split(":");
|
|
const price = Number(priceStr) || 120;
|
|
const [ok, result] = tryBuyAnimal(state, animalId, price);
|
|
if (!ok) setError(String(t.buyFailed).replace("%s", errorMessage[result] ?? result));
|
|
else setError("");
|
|
playSound(ok ? "buy" : "error");
|
|
setState();
|
|
return;
|
|
}
|
|
const eggType = e.dataTransfer.getData("application/x-builazoo-eggtype");
|
|
const toZooId = e.dataTransfer.getData("application/x-builazoo-offer-zooid") || "player";
|
|
if (!eggType) return;
|
|
const [ok, result] = tryBuyEgg(state, eggType);
|
|
if (!ok) {
|
|
setError(String(t.buyFailed).replace("%s", errorMessage[result] ?? result));
|
|
playSound("error");
|
|
} else {
|
|
setError("");
|
|
playSound("buy");
|
|
state.eggPurchaseTruck = { eggType, fromZooId: "player", toZooId, startAt: Date.now() };
|
|
}
|
|
setState();
|
|
});
|
|
worldMapActions.appendChild(worldMapTruckDropZone);
|
|
panelWorld.appendChild(worldMapActions);
|
|
|
|
const gridWrap = document.createElement("div");
|
|
gridWrap.className = "grid-wrap";
|
|
const gridEl = document.createElement("div");
|
|
gridEl.className = "grid";
|
|
gridWrap.appendChild(gridEl);
|
|
const plotUpgradeZone = document.createElement("div");
|
|
plotUpgradeZone.className = "plot-upgrade-zone";
|
|
plotUpgradeZone.setAttribute("aria-label", t.upgradePlot);
|
|
plotUpgradeZone.title = t.upgradePlot;
|
|
plotUpgradeZone.innerHTML = "<span class=\"plot-upgrade-zone-icon\" aria-hidden=\"true\">📐</span><span class=\"plot-upgrade-zone-label\">Agrandir zoo</span><span class=\"plot-upgrade-zone-arrow\" aria-hidden=\"true\">▲</span>";
|
|
plotUpgradeZone.setAttribute("role", "button");
|
|
plotUpgradeZone.setAttribute("tabindex", "0");
|
|
plotUpgradeZone.addEventListener("click", () => {
|
|
const [ok, reason] = tryUpgradePlot(state);
|
|
if (!ok) {
|
|
setError(String(t.upgradePlotFailed).replace("%s", errorMessage[reason] ?? reason));
|
|
playSound("error");
|
|
} else {
|
|
setError("");
|
|
playSound("plotUpgrade");
|
|
}
|
|
setState();
|
|
});
|
|
plotUpgradeZone.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter" || e.key === " ") {
|
|
e.preventDefault();
|
|
plotUpgradeZone.click();
|
|
}
|
|
});
|
|
gridWrap.appendChild(plotUpgradeZone);
|
|
const sellZone = document.createElement("div");
|
|
sellZone.className = "sell-zone";
|
|
sellZone.setAttribute("aria-label", sellZoneTitle);
|
|
sellZone.title = sellZoneTitle;
|
|
sellZone.innerHTML = "<span class=\"sell-zone-icon\" aria-hidden=\"true\">🚚</span><span class=\"sell-zone-label\">" + sellZoneShortLabel + "</span><span class=\"sell-zone-upgrade-arrow\" aria-hidden=\"true\">▲</span>";
|
|
sellZone.addEventListener("dragover", (e) => {
|
|
e.preventDefault();
|
|
const hasCell = e.dataTransfer.types.includes("text/plain");
|
|
e.dataTransfer.dropEffect = hasCell ? "move" : "none";
|
|
if (hasCell) sellZone.classList.add("dragover");
|
|
});
|
|
sellZone.addEventListener("dragleave", () => sellZone.classList.remove("dragover"));
|
|
sellZone.addEventListener("drop", (e) => {
|
|
e.preventDefault();
|
|
sellZone.classList.remove("dragover");
|
|
sellZoneJustDropped = true;
|
|
const nurseryCellKey = e.dataTransfer.getData("application/x-builazoo-nursery-cell-key");
|
|
const receptionCellKey = e.dataTransfer.getData("application/x-builazoo-reception-cell-key");
|
|
if (nurseryCellKey) {
|
|
const [ok, result] = addMatureBabyToSale(state, nurseryCellKey);
|
|
if (!ok) {
|
|
setError(String(t.errorPrefix).replace("%s", errorMessage[result] ?? result));
|
|
playSound("error");
|
|
} else {
|
|
setError("");
|
|
playSound("sell");
|
|
const listing = state.saleListings[state.saleListings.length - 1];
|
|
if (getApiBase() && listing) {
|
|
createSale({ animalId: listing.animalId, isBaby: true, price: listing.price, endAt: new Date(listing.endAt * 1000).toISOString(), reproductionScoreAtSale: listing.reproductionScoreAtSale }).then(({ id }) => { listing.serverId = id; setState(); }).catch(() => {});
|
|
}
|
|
}
|
|
lastActionWasDrop = true;
|
|
setState();
|
|
return;
|
|
}
|
|
if (receptionCellKey) {
|
|
const [ok, result] = addReceptionAnimalToSale(state, receptionCellKey);
|
|
if (!ok) {
|
|
setError(String(t.errorPrefix).replace("%s", errorMessage[result] ?? result));
|
|
playSound("error");
|
|
} else {
|
|
setError("");
|
|
playSound("sell");
|
|
const listing = state.saleListings[state.saleListings.length - 1];
|
|
if (getApiBase() && listing) {
|
|
createSale({ animalId: listing.animalId, isBaby: false, price: listing.price, endAt: new Date(listing.endAt * 1000).toISOString(), reproductionScoreAtSale: listing.reproductionScoreAtSale }).then(({ id }) => { listing.serverId = id; setState(); }).catch(() => {});
|
|
}
|
|
}
|
|
lastActionWasDrop = true;
|
|
setState();
|
|
return;
|
|
}
|
|
const raw = e.dataTransfer.getData("text/plain");
|
|
if (!raw || !/^\d+_\d+$/.test(raw)) return;
|
|
const [sx, sy] = raw.split("_").map(Number);
|
|
const [ok, result] = sellAnimalToNpc(state, sx, sy);
|
|
if (!ok) {
|
|
setError(String(t.sellFailed).replace("%s", errorMessage[result] ?? result));
|
|
playSound("error");
|
|
} else {
|
|
setError("");
|
|
playSound("sell");
|
|
state.truckSale = { toZooId: pickSaleTargetZoo(state), startAt: Date.now() };
|
|
}
|
|
lastActionWasDrop = true;
|
|
setState();
|
|
});
|
|
sellZone.addEventListener("click", () => {
|
|
if (sellZoneJustDropped) {
|
|
sellZoneJustDropped = false;
|
|
return;
|
|
}
|
|
const truckLevel = state.truckLevel ?? 1;
|
|
const truckMax = (GameConfig.Truck && GameConfig.Truck.MaxLevel) || 5;
|
|
if (truckLevel >= truckMax) return;
|
|
if (state.coins < getTruckUpgradeCost(truckLevel)) return;
|
|
const [ok, reason] = tryUpgradeTruck(state);
|
|
if (!ok) {
|
|
setError(String(t.upgradeConveyorFailed).replace("%s", errorMessage[reason] ?? reason));
|
|
playSound("error");
|
|
} else {
|
|
setError("");
|
|
playSound("truckUpgrade");
|
|
}
|
|
setState();
|
|
});
|
|
gridWrap.appendChild(sellZone);
|
|
const visitorsLayer = document.createElement("div");
|
|
visitorsLayer.className = "visitors-layer";
|
|
visitorsLayer.setAttribute("aria-hidden", "true");
|
|
gridWrap.appendChild(visitorsLayer);
|
|
panelZoo.appendChild(gridWrap);
|
|
tabContent.appendChild(panelZoo);
|
|
tabContent.appendChild(panelWorld);
|
|
tabsWrap.appendChild(errEl);
|
|
tabsWrap.appendChild(tabContent);
|
|
|
|
gameBarActions.insertBefore(viewSwitcherWrap, gameBarActions.firstChild);
|
|
root.appendChild(gameBar);
|
|
root.appendChild(tabsWrap);
|
|
|
|
function updateStatus() {
|
|
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 phase = getTimePhase(state.timeOfDay ?? 6);
|
|
const weather = weatherLabel[state.weather] ?? state.weather;
|
|
statusBarTimeWeather.valueEl.textContent = `${timePhaseLabel[phase.phase]} · ${weather}`;
|
|
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);
|
|
const qList = state.quests ?? [];
|
|
questListEl.innerHTML = qList.map((q) => {
|
|
const desc = questDescription[q.descriptionKey];
|
|
const text = desc ? String(desc).replace("%d", String(q.target)) : q.descriptionKey;
|
|
const done = q.done ? " ✓" : "";
|
|
return `<div class="quest-item ${q.done ? "done" : ""}">${text} : ${q.current}/${q.target}${done}</div>`;
|
|
}).join("");
|
|
}
|
|
|
|
const WORLD_MAP_GRID_COLS = 12;
|
|
const WORLD_MAP_GRID_ROWS = 8;
|
|
|
|
function renderWorldMap() {
|
|
worldMapEl.innerHTML = "";
|
|
const playerZooId = state.myZooId ?? "player";
|
|
const api = state.salesFromApi;
|
|
const myListingsFromApi = api?.asSeller ? api.asSeller.map(mapServerListingToClient) : null;
|
|
const zooListingsForPlayer = myListingsFromApi ?? (state.saleListings ?? []).filter((s) => s.zooId === playerZooId);
|
|
|
|
const salesPanel = document.createElement("div");
|
|
salesPanel.className = "world-map-sales-panel";
|
|
salesPanel.setAttribute("aria-label", salesPanelAriaLabel);
|
|
if (api) {
|
|
if (api.asSeller && api.asSeller.length > 0) {
|
|
const sellerTitle = document.createElement("div");
|
|
sellerTitle.className = "sales-panel-title";
|
|
sellerTitle.textContent = salesPanelMySales;
|
|
salesPanel.appendChild(sellerTitle);
|
|
for (const s of api.asSeller) {
|
|
const row = document.createElement("div");
|
|
row.className = "sales-panel-row";
|
|
const emoji = animalEmoji[s.animal_id] ?? "🐾";
|
|
const label = s.is_baby ? `Bébé ${animalLabel[s.animal_id] ?? s.animal_id}` : (animalLabel[s.animal_id] ?? s.animal_id);
|
|
row.innerHTML = `<span class="offer-emoji">${emoji}</span><span class="offer-label">${label}</span><span class="offer-price">${s.initial_price} 💰</span>`;
|
|
if (s.best_bid_amount != null) {
|
|
const btnWrap = document.createElement("div");
|
|
btnWrap.className = "sales-panel-actions";
|
|
const acceptBtn = document.createElement("button");
|
|
acceptBtn.type = "button";
|
|
acceptBtn.textContent = salesBtnAccept;
|
|
acceptBtn.className = "sales-btn-accept";
|
|
acceptBtn.addEventListener("click", () => {
|
|
acceptSale(s.id).then(() => { state.salesFromApi = undefined; setState(); }).catch((e) => { setError(e.message || "Erreur"); setState(); });
|
|
});
|
|
const rejectBtn = document.createElement("button");
|
|
rejectBtn.type = "button";
|
|
rejectBtn.textContent = salesBtnReject;
|
|
rejectBtn.className = "sales-btn-reject";
|
|
rejectBtn.addEventListener("click", () => {
|
|
rejectSale(s.id).then(() => { state.salesFromApi = undefined; setState(); }).catch((e) => { setError(e.message || "Erreur"); setState(); });
|
|
});
|
|
btnWrap.appendChild(acceptBtn);
|
|
btnWrap.appendChild(rejectBtn);
|
|
row.appendChild(btnWrap);
|
|
}
|
|
salesPanel.appendChild(row);
|
|
}
|
|
}
|
|
if (api.asBuyerUndelivered && api.asBuyerUndelivered.length > 0) {
|
|
const buyerTitle = document.createElement("div");
|
|
buyerTitle.className = "sales-panel-title";
|
|
buyerTitle.textContent = salesPanelToRecover;
|
|
salesPanel.appendChild(buyerTitle);
|
|
for (const s of api.asBuyerUndelivered) {
|
|
const row = document.createElement("div");
|
|
row.className = "sales-panel-row";
|
|
const emoji = animalEmoji[s.animal_id] ?? "🐾";
|
|
const label = s.is_baby ? `Bébé ${animalLabel[s.animal_id] ?? s.animal_id}` : (animalLabel[s.animal_id] ?? s.animal_id);
|
|
row.innerHTML = `<span class="offer-emoji">${emoji}</span><span class="offer-label">${label}</span>`;
|
|
const validatedAtMs = s.validated_at ? new Date(s.validated_at).getTime() : 0;
|
|
const nowMs = Date.now();
|
|
const pendingValidation = s.status === "sold" && validatedAtMs > nowMs;
|
|
if (pendingValidation) {
|
|
const remainingMin = Math.ceil((validatedAtMs - nowMs) / 60000);
|
|
const pendingEl = document.createElement("span");
|
|
pendingEl.className = "sales-pending-validation";
|
|
pendingEl.setAttribute("aria-label", salesPendingValidation);
|
|
pendingEl.textContent = `⏳ ${salesValidationInMinutes.replace("%s", String(remainingMin))}`;
|
|
row.appendChild(pendingEl);
|
|
}
|
|
const deliverBtn = document.createElement("button");
|
|
deliverBtn.type = "button";
|
|
deliverBtn.textContent = salesBtnDeliver;
|
|
deliverBtn.className = "sales-btn-deliver";
|
|
deliverBtn.disabled = pendingValidation;
|
|
deliverBtn.addEventListener("click", () => {
|
|
const [ok, keyOrReason] = s.is_baby ? addPendingBaby(state, s.animal_id, true) : addReceptionAnimal(state, s.animal_id);
|
|
if (!ok) {
|
|
setError(keyOrReason === "NoFreeNursery" ? noFreeNursery : keyOrReason === "NoFreeReception" ? noFreeReception : String(keyOrReason));
|
|
setState();
|
|
return;
|
|
}
|
|
setState();
|
|
deliverSale(s.id).then(() => { state.salesFromApi = undefined; setState(); }).catch((e) => { setError(e.message || "Erreur"); setState(); });
|
|
});
|
|
row.appendChild(deliverBtn);
|
|
salesPanel.appendChild(row);
|
|
}
|
|
}
|
|
if (api.active && api.active.length > 0) {
|
|
const activeTitle = document.createElement("div");
|
|
activeTitle.className = "sales-panel-title";
|
|
activeTitle.textContent = salesPanelAuctions;
|
|
salesPanel.appendChild(activeTitle);
|
|
for (const s of api.active) {
|
|
if (s.seller_zoo_id === playerZooId) continue;
|
|
const row = document.createElement("div");
|
|
row.className = "sales-panel-row sales-panel-row-bid";
|
|
const emoji = animalEmoji[s.animal_id] ?? "🐾";
|
|
const label = s.is_baby ? `Bébé ${animalLabel[s.animal_id] ?? s.animal_id}` : (animalLabel[s.animal_id] ?? s.animal_id);
|
|
const minBid = (s.best_bid_amount ?? s.initial_price) + 1;
|
|
row.innerHTML = `<span class="offer-emoji">${emoji}</span><span class="offer-label">${label}</span><span class="offer-price">${s.initial_price} 💰</span>`;
|
|
const input = document.createElement("input");
|
|
input.type = "number";
|
|
input.min = String(minBid);
|
|
input.value = String(minBid);
|
|
input.className = "sales-bid-input";
|
|
input.setAttribute("aria-label", salesBidInputAriaLabel);
|
|
const bidBtn = document.createElement("button");
|
|
bidBtn.type = "button";
|
|
bidBtn.textContent = salesBtnBid;
|
|
bidBtn.className = "sales-btn-bid";
|
|
bidBtn.addEventListener("click", () => {
|
|
const amount = Number(input.value) || minBid;
|
|
placeBid(s.id, amount).then(() => { state.salesFromApi = undefined; setState(); }).catch((e) => { setError(e.message || "Erreur"); setState(); });
|
|
});
|
|
row.appendChild(input);
|
|
row.appendChild(bidBtn);
|
|
salesPanel.appendChild(row);
|
|
}
|
|
}
|
|
}
|
|
if (salesPanel.childNodes.length > 0) worldMapEl.appendChild(salesPanel);
|
|
|
|
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);
|
|
}
|
|
}
|
|
worldMapEl.appendChild(cellsLayer);
|
|
const zoos = state.worldZoos ?? [{ id: "player", name: "Mon zoo", x: 25, y: 50, animalWeights: defaultAnimalWeights() }];
|
|
const offers = state.conveyorOffers || [];
|
|
for (const zoo of zoos) {
|
|
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 : [];
|
|
if (isPlayer && zooListings.length > 0) {
|
|
for (const listing of zooListings.slice(0, 3)) {
|
|
const el = document.createElement("div");
|
|
el.className = "world-map-sale-listing";
|
|
const emoji = animalEmoji[listing.animalId] ?? "🐾";
|
|
const label = listing.isBaby ? `Bébé ${animalLabel[listing.animalId] ?? listing.animalId}` : (animalLabel[listing.animalId] ?? listing.animalId);
|
|
el.innerHTML = `<span class="offer-emoji">${emoji}</span><span class="offer-label">${label}</span><span class="offer-price">${listing.price} 💰</span>`;
|
|
el.title = "En vente sur la carte (phase 10)";
|
|
slotEl.appendChild(el);
|
|
}
|
|
} else if (oneOffer) {
|
|
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[oneOffer.eggType] ?? oneOffer.eggType;
|
|
el.innerHTML = `<span class="offer-emoji">${EGG_EMOJI}</span><span class="offer-label">${name}</span><span class="offer-price">${oneOffer.price} pièces</span>`;
|
|
let dragStarted = false;
|
|
el.addEventListener("dragstart", (e) => {
|
|
dragStarted = true;
|
|
e.dataTransfer.setData("application/x-builazoo-eggtype", oneOffer.eggType);
|
|
e.dataTransfer.setData("application/x-builazoo-offer-zooid", zoo.id);
|
|
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);
|
|
} else if (isPlayer && (playerBabyOffer || playerAnimalOffer)) {
|
|
if (playerBabyOffer) {
|
|
const el = document.createElement("div");
|
|
el.className = "offer-btn world-map-offer";
|
|
el.setAttribute("draggable", "true");
|
|
const emoji = animalEmoji[playerBabyOffer.animalId] ?? "🐾";
|
|
const name = animalLabel[playerBabyOffer.animalId] ?? playerBabyOffer.animalId;
|
|
el.innerHTML = `<span class="offer-emoji">${emoji}</span><span class="offer-label">Bébé ${name}</span><span class="offer-price">${playerBabyOffer.price}</span>`;
|
|
el.addEventListener("dragstart", (e) => {
|
|
e.dataTransfer.setData("application/x-builazoo-baby-offer", `${playerBabyOffer.animalId}:${playerBabyOffer.price}`);
|
|
e.dataTransfer.effectAllowed = "copy";
|
|
});
|
|
slotEl.appendChild(el);
|
|
}
|
|
if (playerAnimalOffer) {
|
|
const el = document.createElement("div");
|
|
el.className = "offer-btn world-map-offer";
|
|
el.setAttribute("draggable", "true");
|
|
const emoji = animalEmoji[playerAnimalOffer.animalId] ?? "🐾";
|
|
const name = animalLabel[playerAnimalOffer.animalId] ?? playerAnimalOffer.animalId;
|
|
el.innerHTML = `<span class="offer-emoji">${emoji}</span><span class="offer-label">${name}</span><span class="offer-price">${playerAnimalOffer.price}</span>`;
|
|
el.addEventListener("dragstart", (e) => {
|
|
e.dataTransfer.setData("application/x-builazoo-animal-offer", `${playerAnimalOffer.animalId}:${playerAnimalOffer.price}`);
|
|
e.dataTransfer.effectAllowed = "copy";
|
|
});
|
|
slotEl.appendChild(el);
|
|
}
|
|
} else {
|
|
const iconEl = document.createElement("span");
|
|
iconEl.className = "world-map-zoo-icon";
|
|
iconEl.setAttribute("aria-hidden", "true");
|
|
iconEl.textContent = "🏠";
|
|
slotEl.appendChild(iconEl);
|
|
}
|
|
node.appendChild(slotEl);
|
|
worldMapEl.appendChild(node);
|
|
}
|
|
const cities = GameConfig.WorldMap?.Cities ?? [];
|
|
for (const city of cities) {
|
|
const cityEl = document.createElement("div");
|
|
cityEl.className = "world-map-city";
|
|
cityEl.style.left = `${city.x}%`;
|
|
cityEl.style.top = `${city.y}%`;
|
|
const maxVisitors = city.maxVisitorsTowardZoos ?? 0;
|
|
cityEl.title = maxVisitors > 0 ? `${city.name} — max ${maxVisitors} visiteurs vers zoos` : city.name;
|
|
cityEl.setAttribute("aria-label", maxVisitors > 0 ? `${city.name}, ${maxVisitors} visiteurs max vers zoos` : city.name);
|
|
const icon = document.createElement("span");
|
|
icon.setAttribute("aria-hidden", "true");
|
|
icon.textContent = "🏙️";
|
|
cityEl.appendChild(icon);
|
|
const cityLabel = document.createElement("div");
|
|
cityLabel.className = "world-map-city-label";
|
|
cityLabel.textContent = city.name;
|
|
cityEl.appendChild(cityLabel);
|
|
if (maxVisitors > 0) {
|
|
const cityMax = document.createElement("div");
|
|
cityMax.className = "world-map-city-max-visitors";
|
|
cityMax.textContent = `max ${maxVisitors}`;
|
|
cityMax.setAttribute("aria-hidden", "true");
|
|
cityEl.appendChild(cityMax);
|
|
}
|
|
worldMapEl.appendChild(cityEl);
|
|
}
|
|
const lab = GameConfig.WorldMap?.Laboratory;
|
|
if (lab) {
|
|
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) {
|
|
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 = `<span class="offer-emoji">${EGG_EMOJI}</span><span class="offer-label">${name}</span><span class="offer-price">${labOffer.price} pièces</span>`;
|
|
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);
|
|
if (!ok) {
|
|
const msg = errorMessage[result] ?? result;
|
|
setError(String(t.buyFailed).replace("%s", msg));
|
|
playSound("error");
|
|
setState();
|
|
return;
|
|
}
|
|
setError("");
|
|
playSound("buy");
|
|
pendingTokenByEggType[labOffer.eggType] = result.tokenId;
|
|
setState();
|
|
});
|
|
el.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter" || e.key === " ") {
|
|
e.preventDefault();
|
|
el.click();
|
|
}
|
|
});
|
|
labSlotEl.appendChild(el);
|
|
} 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);
|
|
}
|
|
const truckMs = (GameConfig.WorldMap && GameConfig.WorldMap.TruckAnimationMs) || 2500;
|
|
const truckSale = state.truckSale;
|
|
const eggPurchase = state.eggPurchaseTruck;
|
|
const truckLevel = state.truckLevel ?? 1;
|
|
if (truckSale && truckSale.toZooId) {
|
|
const elapsed = Date.now() - (truckSale.startAt || 0);
|
|
if (elapsed >= truckMs) {
|
|
delete state.truckSale;
|
|
} else {
|
|
const fromZoo = zoos.find((z) => z.id === "player");
|
|
const toZoo = zoos.find((z) => z.id === truckSale.toZooId);
|
|
if (fromZoo && toZoo) {
|
|
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);
|
|
}
|
|
}
|
|
} else if (eggPurchase && eggPurchase.startAt) {
|
|
const durationMs = Math.max(1000, (truckMs * 2) / truckLevel);
|
|
const elapsed = Date.now() - eggPurchase.startAt;
|
|
if (elapsed >= durationMs) {
|
|
delete state.eggPurchaseTruck;
|
|
worldMapTruckEl.style.display = "none";
|
|
} else {
|
|
const fromZoo = zoos.find((z) => z.id === eggPurchase.fromZooId);
|
|
const toZoo = zoos.find((z) => z.id === eggPurchase.toZooId);
|
|
if (fromZoo && toZoo) {
|
|
const progress = elapsed / durationMs;
|
|
let x; let y;
|
|
if (progress < 0.5) {
|
|
const leg = progress * 2;
|
|
x = fromZoo.x + (toZoo.x - fromZoo.x) * leg;
|
|
y = fromZoo.y + (toZoo.y - fromZoo.y) * leg;
|
|
} else {
|
|
const leg = (progress - 0.5) * 2;
|
|
x = toZoo.x + (fromZoo.x - toZoo.x) * leg;
|
|
y = toZoo.y + (fromZoo.y - toZoo.y) * leg;
|
|
}
|
|
worldMapTruckEl.style.display = "block";
|
|
worldMapTruckEl.style.left = `${x}%`;
|
|
worldMapTruckEl.style.top = `${y}%`;
|
|
worldMapTruckEl.textContent = "🚚";
|
|
setTimeout(setState, 50);
|
|
}
|
|
}
|
|
} else {
|
|
worldMapTruckEl.style.display = "none";
|
|
}
|
|
worldMapNpcTrucksEl.innerHTML = "";
|
|
const npcTrucks = state.worldTruckSales ?? [];
|
|
for (const t of npcTrucks) {
|
|
const fromZoo = zoos.find((z) => z.id === t.fromZooId);
|
|
const toZoo = zoos.find((z) => z.id === t.toZooId);
|
|
if (fromZoo && toZoo) {
|
|
const elapsed = Date.now() - (t.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);
|
|
}
|
|
}
|
|
}
|
|
if (npcTrucks.length > 0 || (truckSale && truckSale.toZooId) || (eggPurchase && eggPurchase.startAt)) {
|
|
setTimeout(setState, 50);
|
|
}
|
|
}
|
|
|
|
function renderGrid() {
|
|
gridEl.style.gridTemplateColumns = `repeat(${state.grid.width}, 48px)`;
|
|
gridEl.style.gridTemplateRows = `repeat(${state.grid.height}, 48px)`;
|
|
gridEl.innerHTML = "";
|
|
for (let y = 1; y <= state.grid.height; y++) {
|
|
for (let x = 1; x <= state.grid.width; x++) {
|
|
const key = `${x}_${y}`;
|
|
const cell = state.grid.cells[key];
|
|
const div = document.createElement("div");
|
|
div.className = "cell";
|
|
const biome = getDisplayBiome(x, y, state.grid);
|
|
const temp = getDisplayTemperature(x, y, state.grid);
|
|
const tempBand = getTemperatureBand(temp);
|
|
div.classList.add(`biome-${biome.toLowerCase()}`, `temp-${tempBand}`);
|
|
const hatchedList = getHatched();
|
|
if (hatchedList.some((h) => h.x === x && h.y === y)) div.classList.add("just-hatched");
|
|
div.setAttribute("role", "button");
|
|
div.setAttribute("tabindex", "0");
|
|
div.dataset.x = String(x);
|
|
div.dataset.y = String(y);
|
|
const isSelected = selected.x === x && selected.y === y;
|
|
if (isSelected) div.classList.add("selected");
|
|
if (cell === null || cell === undefined) {
|
|
div.classList.add("empty");
|
|
if (emptyCellChoice && emptyCellChoice.x === x && emptyCellChoice.y === y) {
|
|
const nurseryCost = getNurseryBuildCost();
|
|
const shopCost = getSouvenirShopBuildCost();
|
|
const researchCost = getResearchBuildCost();
|
|
const billeterieCost = getBilleterieBuildCost();
|
|
const foodCost = getFoodBuildCost();
|
|
const receptionCost = getReceptionBuildCost();
|
|
const biomeColorCost = getBiomeChangeColorBuildCost();
|
|
const biomeTempCost = getBiomeChangeTempBuildCost();
|
|
const canNursery = state.coins >= nurseryCost;
|
|
const canShop = state.coins >= shopCost;
|
|
const canResearch = state.coins >= researchCost;
|
|
const canBilleterie = state.coins >= billeterieCost;
|
|
const canFood = state.coins >= foodCost;
|
|
const canReception = state.coins >= receptionCost;
|
|
const canBiomeColor = state.coins >= biomeColorCost;
|
|
const canBiomeTemp = state.coins >= biomeTempCost;
|
|
div.innerHTML = `<button type="button" class="cell-choice-btn" data-choice="nursery" ${canNursery ? "" : "disabled"}>🐣 Nurserie (${nurseryCost})</button><button type="button" class="cell-choice-btn" data-choice="shop" ${canShop ? "" : "disabled"}>🛒 Boutique (${shopCost})</button><button type="button" class="cell-choice-btn" data-choice="research" ${canResearch ? "" : "disabled"}>🔬 Recherche (${researchCost})</button><button type="button" class="cell-choice-btn" data-choice="billeterie" ${canBilleterie ? "" : "disabled"}>🎫 Billeterie (${billeterieCost})</button><button type="button" class="cell-choice-btn" data-choice="food" ${canFood ? "" : "disabled"}>🥗 Nourriture (${foodCost})</button><button type="button" class="cell-choice-btn" data-choice="reception" ${canReception ? "" : "disabled"}>📥 Accueil (${receptionCost})</button><button type="button" class="cell-choice-btn" data-choice="biomeColor" ${canBiomeColor ? "" : "disabled"}>🎨 Couleur (${biomeColorCost})</button><button type="button" class="cell-choice-btn" data-choice="biomeTemp" ${canBiomeTemp ? "" : "disabled"}>🌡️ Temp (${biomeTempCost})</button>`;
|
|
div.classList.add("empty-choice");
|
|
} else {
|
|
div.textContent = "";
|
|
}
|
|
} else if (cell.kind === "school") {
|
|
div.classList.add("school");
|
|
const schoolMaxLevel = (GameConfig.School && GameConfig.School.MaxLevel) || 8;
|
|
const canUpgradeSchool = cell.level < schoolMaxLevel && state.coins >= getSchoolUpgradeCost(cell.level);
|
|
if (canUpgradeSchool) div.classList.add("can-upgrade");
|
|
const arrow = canUpgradeSchool ? '<span class="cell-upgrade-arrow" aria-hidden="true">▲</span>' : "";
|
|
div.innerHTML = `<span class="cell-emoji">🏫</span><span class="cell-label">École ${cell.level}</span>${arrow}`;
|
|
} else if (cell.kind === "nursery") {
|
|
div.classList.add("nursery");
|
|
const nurseryLevel = cell.level ?? 1;
|
|
const nurseryMax = GameConfig.Nursery?.MaxLevel ?? 7;
|
|
const canUpgradeNursery = nurseryLevel < nurseryMax && state.coins >= getNurseryUpgradeCost(nurseryLevel);
|
|
if (canUpgradeNursery) div.classList.add("can-upgrade");
|
|
const pendingBaby = (state.pendingBabies ?? []).find((p) => p.nurseryCellKey === key);
|
|
const token = cell.tokenId !== null && cell.tokenId !== undefined ? state.pendingEggTokens.find((t) => t.tokenId === cell.tokenId) : null;
|
|
if (pendingBaby) {
|
|
const nowUnix = Math.floor(Date.now() / 1000);
|
|
const isMature = nowUnix >= pendingBaby.readyAt;
|
|
const emoji = animalEmoji[pendingBaby.animalId] ?? "🐾";
|
|
const label = isMature ? "Bébé prêt" : "Bébé…";
|
|
div.classList.add("cell-draggable");
|
|
div.draggable = isMature;
|
|
div.innerHTML = `<span class="cell-emoji">${emoji}</span><span class="cell-label">${label}</span>`;
|
|
if (isMature) div.dataset.nurseryCellKey = key;
|
|
} else if (token) {
|
|
div.classList.add("cell-draggable");
|
|
div.draggable = true;
|
|
const label = eggTypeLabel[token.eggType] ?? token.eggType;
|
|
div.innerHTML = `<span class="cell-emoji">${EGG_EMOJI}</span><span class="cell-label">${label}</span>`;
|
|
div.dataset.tokenId = String(cell.tokenId);
|
|
} else {
|
|
const arrow = canUpgradeNursery ? '<span class="cell-upgrade-arrow" aria-hidden="true">▲</span>' : "";
|
|
div.innerHTML = `<span class="cell-emoji">🐣</span><span class="cell-label">Nurserie ${nurseryLevel}</span>${arrow}`;
|
|
}
|
|
} else if (cell.kind === "souvenirShop") {
|
|
div.classList.add("souvenir-shop");
|
|
const shopLevel = cell.level ?? 1;
|
|
const shopMax = GameConfig.SouvenirShop?.MaxLevel ?? 7;
|
|
const canUpgradeShop = shopLevel < shopMax && state.coins >= getSouvenirShopUpgradeCost(shopLevel);
|
|
if (canUpgradeShop) div.classList.add("can-upgrade");
|
|
const arrow = canUpgradeShop ? '<span class="cell-upgrade-arrow" aria-hidden="true">▲</span>' : "";
|
|
div.innerHTML = `<span class="cell-emoji">🛒</span><span class="cell-label">Boutique ${shopLevel}</span>${arrow}`;
|
|
} else if (cell.kind === "research") {
|
|
div.classList.add("research");
|
|
const level = cell.level ?? 1;
|
|
const maxLevel = GameConfig.Research?.MaxLevel ?? 7;
|
|
const canUpgrade = level < maxLevel && state.coins >= getResearchUpgradeCost(level);
|
|
if (canUpgrade) div.classList.add("can-upgrade");
|
|
const arrow = canUpgrade ? '<span class="cell-upgrade-arrow" aria-hidden="true">▲</span>' : "";
|
|
div.innerHTML = `<span class="cell-emoji">🔬</span><span class="cell-label">Recherche ${level}</span>${arrow}`;
|
|
} else if (cell.kind === "billeterie") {
|
|
div.classList.add("billeterie");
|
|
const level = cell.level ?? 1;
|
|
const maxLevel = GameConfig.Billeterie?.MaxLevel ?? 7;
|
|
const canUpgrade = level < maxLevel && state.coins >= getBilleterieUpgradeCost(level);
|
|
if (canUpgrade) div.classList.add("can-upgrade");
|
|
const arrow = canUpgrade ? '<span class="cell-upgrade-arrow" aria-hidden="true">▲</span>' : "";
|
|
div.innerHTML = `<span class="cell-emoji">🎫</span><span class="cell-label">Billeterie ${level}</span>${arrow}`;
|
|
} else if (cell.kind === "food") {
|
|
div.classList.add("food");
|
|
const level = cell.level ?? 1;
|
|
const maxLevel = GameConfig.Food?.MaxLevel ?? 7;
|
|
const canUpgrade = level < maxLevel && state.coins >= getFoodUpgradeCost(level);
|
|
if (canUpgrade) div.classList.add("can-upgrade");
|
|
const arrow = canUpgrade ? '<span class="cell-upgrade-arrow" aria-hidden="true">▲</span>' : "";
|
|
div.innerHTML = `<span class="cell-emoji">🥗</span><span class="cell-label">Nourriture ${level}</span>${arrow}`;
|
|
} else if (cell.kind === "reception") {
|
|
div.classList.add("reception");
|
|
const level = cell.level ?? 1;
|
|
const maxLevel = GameConfig.Reception?.MaxLevel ?? 7;
|
|
const canUpgrade = level < maxLevel && state.coins >= getReceptionUpgradeCost(level);
|
|
if (canUpgrade) div.classList.add("can-upgrade");
|
|
const recAnimal = (state.receptionAnimals ?? []).find((r) => r.receptionCellKey === key);
|
|
const nowUnix = Math.floor(Date.now() / 1000);
|
|
if (recAnimal) {
|
|
const isReady = nowUnix >= recAnimal.readyAt;
|
|
const emoji = animalEmoji[recAnimal.animalId] ?? "🐾";
|
|
const label = isReady ? "Animal prêt" : "Acclimatation…";
|
|
div.classList.add("cell-draggable");
|
|
div.draggable = isReady;
|
|
const arrow = canUpgrade ? '<span class="cell-upgrade-arrow" aria-hidden="true">▲</span>' : "";
|
|
div.innerHTML = `<span class="cell-emoji">${emoji}</span><span class="cell-label">${label}</span>${arrow}`;
|
|
if (isReady) div.dataset.receptionCellKey = key;
|
|
} else {
|
|
const arrow = canUpgrade ? '<span class="cell-upgrade-arrow" aria-hidden="true">▲</span>' : "";
|
|
div.innerHTML = `<span class="cell-emoji">📥</span><span class="cell-label">Accueil ${level}</span>${arrow}`;
|
|
}
|
|
} else if (cell.kind === "biomeChangeColor") {
|
|
div.classList.add("biome-change-color");
|
|
const level = cell.level ?? 1;
|
|
const maxLevel = GameConfig.BiomeChangeColor?.MaxLevel ?? 7;
|
|
const canUpgrade = level < maxLevel && state.coins >= getBiomeChangeColorUpgradeCost(level);
|
|
if (canUpgrade) div.classList.add("can-upgrade");
|
|
const arrow = canUpgrade ? '<span class="cell-upgrade-arrow" aria-hidden="true">▲</span>' : "";
|
|
div.innerHTML = `<span class="cell-emoji">🎨</span><span class="cell-label">Couleur ${level}</span>${arrow}`;
|
|
} else if (cell.kind === "biomeChangeTemp") {
|
|
div.classList.add("biome-change-temp");
|
|
const level = cell.level ?? 1;
|
|
const maxLevel = GameConfig.BiomeChangeTemp?.MaxLevel ?? 7;
|
|
const canUpgrade = level < maxLevel && state.coins >= getBiomeChangeTempUpgradeCost(level);
|
|
if (canUpgrade) div.classList.add("can-upgrade");
|
|
const arrow = canUpgrade ? '<span class="cell-upgrade-arrow" aria-hidden="true">▲</span>' : "";
|
|
div.innerHTML = `<span class="cell-emoji">🌡️</span><span class="cell-label">Temp ${level}</span>${arrow}`;
|
|
} else if (cell.kind === "egg") {
|
|
div.classList.add("egg", "cell-draggable");
|
|
div.draggable = true;
|
|
const label = eggTypeLabel[cell.eggType] ?? cell.eggType;
|
|
div.innerHTML = `<span class="cell-emoji">${EGG_EMOJI}</span><span class="cell-label">${label}</span>`;
|
|
} else {
|
|
div.classList.add("animal", "cell-draggable");
|
|
div.draggable = true;
|
|
const w = cell.cellsWide ?? 1;
|
|
const h = cell.cellsHigh ?? 1;
|
|
const isMulti = w > 1 || h > 1;
|
|
const isOrigin = cell.originKey === null || cell.originKey === undefined || cell.originKey === key;
|
|
if (isMulti) div.classList.add("multi-cell");
|
|
if (isMulti && isOrigin) div.classList.add("multi-cell-origin");
|
|
const emoji = animalEmoji[cell.id] ?? "🐾";
|
|
const label = animalLabel[cell.id] ?? cell.id;
|
|
div.innerHTML = `<span class="cell-emoji">${emoji}</span><span class="cell-label">${label}</span>`;
|
|
}
|
|
if (cell !== null && cell !== undefined && (cell.kind === "egg" || cell.kind === "animal" || (cell.kind === "nursery" && (cell.tokenId !== null && cell.tokenId !== undefined || div.dataset.nurseryCellKey)) || (cell.kind === "reception" && div.dataset.receptionCellKey))) {
|
|
div.addEventListener("dragstart", (e) => {
|
|
let dragX = x;
|
|
let dragY = y;
|
|
if (div.dataset.nurseryCellKey) {
|
|
e.dataTransfer.setData("application/x-builazoo-nursery-cell-key", div.dataset.nurseryCellKey);
|
|
e.dataTransfer.effectAllowed = "move";
|
|
} else if (div.dataset.receptionCellKey) {
|
|
e.dataTransfer.setData("application/x-builazoo-reception-cell-key", div.dataset.receptionCellKey);
|
|
e.dataTransfer.effectAllowed = "move";
|
|
} else if (cell.kind === "animal" && cell.originKey !== null && cell.originKey !== undefined) {
|
|
const m = cell.originKey.match(/^(\d+)_(\d+)$/);
|
|
if (m) {
|
|
dragX = Number(m[1]);
|
|
dragY = Number(m[2]);
|
|
}
|
|
}
|
|
if (!div.dataset.nurseryCellKey && !div.dataset.receptionCellKey) {
|
|
e.dataTransfer.setData("text/plain", `${dragX}_${dragY}`);
|
|
}
|
|
if (cell.kind === "nursery" && cell.tokenId !== null && cell.tokenId !== undefined) e.dataTransfer.setData("application/x-builazoo-tokenid", String(cell.tokenId));
|
|
e.dataTransfer.effectAllowed = e.dataTransfer.effectAllowed || "move";
|
|
div.classList.add("dragging");
|
|
const ghost = div.cloneNode(true);
|
|
ghost.classList.add("drag-ghost");
|
|
ghost.style.opacity = "1";
|
|
document.body.appendChild(ghost);
|
|
e.dataTransfer.setDragImage(ghost, 24, 24);
|
|
const cleanup = () => { ghost.remove(); };
|
|
div.addEventListener("dragend", cleanup, { once: true });
|
|
});
|
|
}
|
|
if (cell !== null && cell !== undefined && (cell.kind === "egg" || cell.kind === "animal" || (cell.kind === "nursery" && (cell.tokenId !== null && cell.tokenId !== undefined || div.dataset.nurseryCellKey)) || (cell.kind === "reception" && div.dataset.receptionCellKey))) {
|
|
div.addEventListener("dragend", () => {
|
|
div.classList.remove("dragging");
|
|
gridEl.querySelectorAll(".cell").forEach((c) => c.classList.remove("dragover"));
|
|
});
|
|
}
|
|
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");
|
|
const toX = Number(div.dataset.x);
|
|
const toY = Number(div.dataset.y);
|
|
const nurseryCellKey = e.dataTransfer.getData("application/x-builazoo-nursery-cell-key");
|
|
if (nurseryCellKey && cell === null || cell === undefined) {
|
|
const nowUnix = Math.floor(Date.now() / 1000);
|
|
const [ok, reason] = placeMatureBabyOnCell(state, { nurseryCellKey, toX, toY, nowUnix });
|
|
if (ok) { setError(""); playSound("place"); } else { setError(String(t.errorPrefix).replace("%s", errorMessage[reason] ?? reason)); playSound("error"); }
|
|
lastActionWasDrop = true;
|
|
setState();
|
|
return;
|
|
}
|
|
const receptionCellKey = e.dataTransfer.getData("application/x-builazoo-reception-cell-key");
|
|
if (receptionCellKey && cell === null || cell === undefined) {
|
|
const nowUnix = Math.floor(Date.now() / 1000);
|
|
const [ok, reason] = placeReceptionAnimalOnCell(state, { receptionCellKey, toX, toY, nowUnix });
|
|
if (ok) { setError(""); playSound("place"); } else { setError(String(t.errorPrefix).replace("%s", errorMessage[reason] ?? reason)); playSound("error"); }
|
|
lastActionWasDrop = true;
|
|
setState();
|
|
return;
|
|
}
|
|
const tokenIdStr = e.dataTransfer.getData("application/x-builazoo-tokenid");
|
|
if (tokenIdStr && cell === null || cell === undefined) {
|
|
const tokenId = Number(tokenIdStr);
|
|
if (!Number.isNaN(tokenId)) {
|
|
const nowUnix = Math.floor(Date.now() / 1000);
|
|
const [placeOk, placeReason] = tryPlaceEgg(state, { tokenId, x: toX, y: toY, nowUnix });
|
|
if (placeOk) {
|
|
setError("");
|
|
playSound("place");
|
|
} else {
|
|
setError(String(t.errorPrefix).replace("%s", errorMessage[placeReason] ?? placeReason));
|
|
playSound("error");
|
|
}
|
|
lastActionWasDrop = true;
|
|
setState();
|
|
return;
|
|
}
|
|
}
|
|
const eggTypeFromConveyor = e.dataTransfer.getData("application/x-builazoo-eggtype");
|
|
if (eggTypeFromConveyor && cell === null || cell === undefined) {
|
|
const [buyOk, buyResult] = tryBuyEgg(state, eggTypeFromConveyor);
|
|
if (!buyOk) {
|
|
setError(String(t.buyFailed).replace("%s", errorMessage[buyResult] ?? buyResult));
|
|
playSound("error");
|
|
} else {
|
|
const tokenId = buyResult.tokenId;
|
|
const nowUnix = Math.floor(Date.now() / 1000);
|
|
const [placeOk, placeReason] = tryPlaceEgg(state, { tokenId, x: toX, y: toY, nowUnix });
|
|
if (placeOk) {
|
|
setError("");
|
|
playSound("place");
|
|
} else {
|
|
setError(String(t.errorPrefix).replace("%s", errorMessage[placeReason] ?? placeReason));
|
|
playSound("error");
|
|
}
|
|
}
|
|
lastActionWasDrop = true;
|
|
setState();
|
|
return;
|
|
}
|
|
const raw = e.dataTransfer.getData("text/plain");
|
|
if (!raw || !/^\d+_\d+$/.test(raw)) return;
|
|
const [sx, sy] = raw.split("_").map(Number);
|
|
const [ok, reason] = moveCell(state, { fromX: sx, fromY: sy, toX, toY });
|
|
if (!ok) setError(String(t.errorPrefix).replace("%s", errorMessage[reason] ?? reason));
|
|
else setError("");
|
|
lastActionWasDrop = true;
|
|
setState();
|
|
});
|
|
div.addEventListener("click", (e) => {
|
|
if (lastActionWasDrop) {
|
|
lastActionWasDrop = false;
|
|
return;
|
|
}
|
|
const choiceBtn = e.target.closest(".cell-choice-btn");
|
|
if (choiceBtn && cell === null || cell === undefined && emptyCellChoice && emptyCellChoice.x === x && emptyCellChoice.y === y) {
|
|
const choice = choiceBtn.dataset.choice;
|
|
if (choice === "nursery") {
|
|
const [ok, reason] = tryBuildNursery(state, x, y);
|
|
if (!ok) setError(String(t.errorPrefix).replace("%s", errorMessage[reason] ?? reason));
|
|
else setError("");
|
|
emptyCellChoice = null;
|
|
} else if (choice === "shop") {
|
|
const [ok, reason] = tryBuildSouvenirShop(state, x, y);
|
|
if (!ok) setError(String(t.errorPrefix).replace("%s", errorMessage[reason] ?? reason));
|
|
else setError("");
|
|
emptyCellChoice = null;
|
|
} else if (choice === "research") {
|
|
const [ok, reason] = tryBuildResearch(state, x, y);
|
|
if (!ok) setError(String(t.errorPrefix).replace("%s", errorMessage[reason] ?? reason));
|
|
else setError("");
|
|
emptyCellChoice = null;
|
|
} else if (choice === "billeterie") {
|
|
const [ok, reason] = tryBuildBilleterie(state, x, y);
|
|
if (!ok) setError(String(t.errorPrefix).replace("%s", errorMessage[reason] ?? reason));
|
|
else setError("");
|
|
emptyCellChoice = null;
|
|
} else if (choice === "food") {
|
|
const [ok, reason] = tryBuildFood(state, x, y);
|
|
if (!ok) setError(String(t.errorPrefix).replace("%s", errorMessage[reason] ?? reason));
|
|
else setError("");
|
|
emptyCellChoice = null;
|
|
} else if (choice === "reception") {
|
|
const [ok, reason] = tryBuildReception(state, x, y);
|
|
if (!ok) setError(String(t.errorPrefix).replace("%s", errorMessage[reason] ?? reason));
|
|
else setError("");
|
|
emptyCellChoice = null;
|
|
} else if (choice === "biomeColor") {
|
|
const [ok, reason] = tryBuildBiomeChangeColor(state, x, y);
|
|
if (!ok) setError(String(t.errorPrefix).replace("%s", errorMessage[reason] ?? reason));
|
|
else setError("");
|
|
emptyCellChoice = null;
|
|
} else if (choice === "biomeTemp") {
|
|
const [ok, reason] = tryBuildBiomeChangeTemp(state, x, y);
|
|
if (!ok) setError(String(t.errorPrefix).replace("%s", errorMessage[reason] ?? reason));
|
|
else setError("");
|
|
emptyCellChoice = null;
|
|
}
|
|
setState();
|
|
return;
|
|
}
|
|
if (cell !== null && cell !== undefined && cell.kind === "nursery" && cell.tokenId !== null && cell.tokenId !== undefined) {
|
|
selectedTokenId = 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 (cell !== null && cell !== undefined && 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;
|
|
}
|
|
if (cell !== null && cell !== undefined && 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;
|
|
}
|
|
if (cell !== null && cell !== undefined && 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;
|
|
}
|
|
if (cell !== null && cell !== undefined && 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;
|
|
}
|
|
if (cell !== null && cell !== undefined && 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;
|
|
}
|
|
if (cell !== null && cell !== undefined && 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;
|
|
}
|
|
if (cell !== null && cell !== undefined && 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;
|
|
}
|
|
if (cell !== null && cell !== undefined && 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;
|
|
}
|
|
selected.x = x;
|
|
selected.y = y;
|
|
clampSelection();
|
|
if (cell === null || cell === undefined && emptyCellChoice && emptyCellChoice.x === x && emptyCellChoice.y === y) {
|
|
emptyCellChoice = null;
|
|
setState();
|
|
return;
|
|
}
|
|
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 = selectedTokenId ?? firstTokenId;
|
|
if (cell === null || cell === undefined && tokenId !== null && tokenId !== undefined) {
|
|
const nowUnix = Math.floor(Date.now() / 1000);
|
|
const [ok, reason] = tryPlaceEgg(state, { tokenId, x, y, nowUnix });
|
|
if (ok) {
|
|
selectedTokenId = null;
|
|
setError("");
|
|
playSound("place");
|
|
} else {
|
|
setError(String(t.errorPrefix).replace("%s", errorMessage[reason] ?? reason));
|
|
playSound("error");
|
|
}
|
|
setState();
|
|
return;
|
|
}
|
|
if (cell === null || cell === undefined) {
|
|
emptyCellChoice = { x, y };
|
|
}
|
|
setState();
|
|
});
|
|
div.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter" || e.key === " ") {
|
|
e.preventDefault();
|
|
div.click();
|
|
}
|
|
});
|
|
gridEl.appendChild(div);
|
|
}
|
|
}
|
|
}
|
|
|
|
prestigeBtn.addEventListener("click", () => {
|
|
if (!canPrestige(state)) return;
|
|
doPrestige(state);
|
|
refreshOffers(state, Math.floor(Date.now() / 1000));
|
|
setError("");
|
|
playSound("upgrade");
|
|
setState();
|
|
});
|
|
|
|
const fullRender = () => {
|
|
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";
|
|
const mapCfg = GameConfig.WorldMap && GameConfig.WorldMap.MapUpgrade;
|
|
const mapMaxLevel = mapCfg ? mapCfg.MaxLevel : 5;
|
|
const mapLevel = state.worldMapLevel ?? 1;
|
|
const mapResearchCost = getWorldMapUpgradeResearchCost(mapLevel);
|
|
const canUpgradeMap = mapLevel < mapMaxLevel && (state.researchPoints ?? 0) >= mapResearchCost;
|
|
worldMapUpgradeZone.classList.toggle("can-upgrade", canUpgradeMap);
|
|
worldMapUpgradeZone.title = mapLevel < 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 = mapLevel < 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);
|
|
}
|
|
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();
|
|
renderGrid();
|
|
};
|
|
|
|
fullRender();
|
|
return fullRender;
|
|
}
|