(function(scope){ 'use strict'; function calculate(recipe,requested){ const quantity=Number(requested); if(!String(requested).trim()||!Number.isSafeInteger(quantity)||quantity<1||quantity>999999)throw new RangeError('กรอกจำนวนเต็มตั้งแต่ 1 ถึง 999999'); if(recipe.variableOutput)throw new RangeError('สูตรนี้มีผลลัพธ์ขึ้นกับไอเทมต้นฉบับ จึงไม่ใช้ตัวคำนวณจำนวน'); const output=Number(recipe.outputQty||1);if(!Number.isSafeInteger(output)||output<1)throw new RangeError('จำนวนผลลัพธ์ของสูตรไม่ถูกต้อง'); const crafts=Math.ceil(quantity/output),produced=crafts*output; return{requested:quantity,crafts,produced,extra:produced-quantity,ingredients:recipe.ingredients.map(i=>({...i,count:i.count*crafts}))}; } function text(recipe,plan,itemLabel,itemStack=()=>64){ return[`${recipe.nameTh||recipe.nameEn||recipe.id}: ต้องการ ${plan.requested} ชิ้น`, `คราฟ ${plan.crafts} รอบ ได้ ${plan.produced} ชิ้น (เกิน ${plan.extra} ชิ้น)`,...plan.ingredients.map(i=>{ const alternatives=i.alternatives||[i.id],sizes=[...new Set(alternatives.map(itemStack))]; if(sizes.length!==1||!Number.isInteger(sizes[0])||sizes[0]<1)throw new RangeError('ขนาดสแต็กของวัตถุดิบไม่ชัดเจน'); const size=sizes[0],slots=Math.ceil(i.count/size),full=Math.floor(i.count/size),remainder=i.count%size; const detail=size===1?`${slots} สแต็ก (ชิ้นละ 1)`: remainder===0?`${slots} สแต็กเต็ม (สแต็กละ ${size})`: full?`${slots} สแต็ก (${full} เต็ม + ${remainder}/${size} ชิ้น)`: `1 สแต็ก (${remainder}/${size} ชิ้น)`; return `${alternatives.map(itemLabel).join(' หรือ ')} × ${i.count} — ${detail}`; }),'จำนวนเป็นวัตถุดิบสำหรับสูตรนี้ ยังไม่แตกเป็นวัตถุดิบต้นทาง และไม่หักภาชนะที่ได้คืนหลังคราฟ'].join('\n'); } const api={calculate,text};if(typeof module==='object'&&module.exports)module.exports=api;else scope.ZabaMaterialPlanner=api; })(typeof window==='undefined'?globalThis:window); const DATA_URL = window.ZabaCrafting?.dataUrl || "./generated/crafting-recipes-release.json"; const ICON_BASE = window.ZabaCrafting?.iconBase || "./assets/icons/"; const ICON_DATA = window.ZabaCrafting?.iconData || {}; const QUICK_FILTERS = [ { id: "all", label: "ทั้งหมด", test: () => true }, { id: "popular", label: "ยอดนิยม", test: (recipe) => recipe.searchText.match(/crafting_table|stick|torch|chest|furnace|bed|boat|sword|pickaxe|axe|shovel|shield|bow|arrow|bucket|door|ladder|rail|piston|hopper|observer/) }, { id: "wood", label: "ไม้", test: (recipe) => recipe.searchText.match(/wood|log|plank|slab|stairs|fence|door|trapdoor|sign|boat|shelf|ไม้|แผ่นไม้|ซุง/) }, { id: "stone", label: "หิน", test: (recipe) => recipe.searchText.match(/stone|cobble|deepslate|granite|diorite|andesite|tuff|blackstone|หิน/) }, { id: "copper", label: "ทองแดง", test: (recipe) => recipe.searchText.match(/copper|ทองแดง/) }, { id: "color", label: "สี/ย้อม", test: (recipe) => recipe.searchText.match(/dye|banner|wool|carpet|bed|stained|terracotta|candle|shulker|bundle|harness|สี|ย้อม/) }, { id: "redstone", label: "เรดสโตน", test: (recipe) => recipe.category === "redstone" || recipe.searchText.match(/redstone|piston|hopper|observer|rail|comparator|repeater|เรดสโตน/) }, { id: "gear", label: "อุปกรณ์", test: (recipe) => ["tools", "weapons", "armor"].includes(recipe.category) }, { id: "nether", label: "เนเธอร์/เอนด์", test: (recipe) => recipe.category === "nether_end" || recipe.searchText.match(/nether|end_|ender|quartz|blaze|obsidian|เนเธอร์|เอนด์/) } ]; const TYPE_LABELS = { all: "ทุกประเภทสูตร", "minecraft:crafting_shaped": "ต้องวางตำแหน่ง", "minecraft:crafting_shapeless": "ไม่บังคับตำแหน่ง", "minecraft:crafting_transmute": "สูตรแปลงไอเทม" }; let categories = []; let items = {}; let recipes = []; let activeCategory = "all"; let activeQuickFilter = "all"; let activeType = "all"; let activeSort = "name_th"; let selectedRecipe = null; let visibleLimit = 180; const el = { recipeCount: document.getElementById("recipe-count"), selectedIcon: document.getElementById("selected-icon"), selectedTitle: document.getElementById("selected-title"), selectedSubtitle: document.getElementById("selected-subtitle"), selectedDescription: document.getElementById("selected-description"), selectedEditions: document.getElementById("selected-editions"), ingredientList: document.getElementById("ingredient-list"), craftingGrid: document.getElementById("crafting-grid"), resultSlot: document.getElementById("result-slot"), tabs: document.getElementById("category-tabs"), grid: document.getElementById("recipe-card-grid"), search: document.getElementById("recipe-search"), empty: document.getElementById("empty-state") }; function ensureFinderControls() { const finderPanel = document.querySelector(".finder-panel"); const searchBox = document.querySelector(".search-box"); if (searchBox && !document.getElementById("recipe-search")) { const searchInput = document.createElement("input"); searchInput.type = "search"; searchInput.id = "recipe-search"; searchInput.placeholder = "ค้นหา เช่น ดาบ โต๊ะคราฟ redstone"; searchInput.autocomplete = "off"; searchBox.appendChild(searchInput); } el.search = document.getElementById("recipe-search"); if (!finderPanel || document.getElementById("quick-filter-tabs")) return; const controls = document.createElement("div"); controls.className = "craft-filter-panel"; controls.innerHTML = `
ตัวกรองเร็ว
คำนวณวัตถุดิบและจำนวนสแต็กจากขนาดซ้อนจริงของ Java 26.3 โดยปัดขึ้นเป็นรอบคราฟเต็ม ยังไม่แตกต่อเป็นวัตถุดิบต้นทาง เช่น ไม้และแท่งเหล็ก ไม่หักภาชนะที่ได้คืนหลังคราฟ
'; panel.prepend(wrap);el.quantity=document.getElementById('craft-quantity');el.materialOutput=document.getElementById('craft-material-output');el.materialSummary=document.getElementById('craft-quantity-summary');el.materialCopy=document.getElementById('craft-material-copy');el.materialStatus=document.getElementById('craft-material-copy-status'); el.quantity.addEventListener('input',renderMaterialPlan); el.materialCopy.addEventListener('click',async()=>{try{if(!navigator.clipboard?.writeText)throw new Error();await navigator.clipboard.writeText(el.materialOutput.value);el.materialStatus.textContent='คัดลอกรายการแล้ว';}catch{el.materialOutput.focus();el.materialOutput.select();el.materialStatus.textContent='เลือกข้อความในช่องแล้วคัดลอกด้วยตนเอง';}}); } function renderMaterialPlan(){ if(!selectedRecipe||!el.quantity)return; el.materialStatus.textContent=''; try{const plan=window.ZabaMaterialPlanner.calculate(selectedRecipe,el.quantity.value);el.quantity.removeAttribute('aria-invalid');el.materialSummary.textContent=`คราฟ ${plan.crafts.toLocaleString('th-TH')} รอบ ได้ ${plan.produced.toLocaleString('th-TH')} ชิ้น เกินจำนวนที่ต้องการ ${plan.extra.toLocaleString('th-TH')} ชิ้น`;el.materialOutput.value=window.ZabaMaterialPlanner.text(selectedRecipe,plan,id=>itemMeta(id).nameTh,id=>items[id]?.maxStack||64);el.materialCopy.disabled=false;} catch(error){el.materialSummary.textContent=error.message;el.materialOutput.value='';el.materialCopy.disabled=true;el.quantity.setAttribute('aria-invalid','true');} } function setSelected(recipeToShow, updateHash = true) { selectedRecipe = recipeToShow; if (updateHash) history.replaceState(null, "", `#${recipeToShow.id}`); el.selectedIcon.replaceChildren(icon(recipeToShow.result || recipeToShow.id, true)); el.selectedTitle.textContent = `${recipeToShow.nameTh} (${recipeToShow.nameEn})`; el.selectedSubtitle.textContent = `ผลลัพธ์: ${recipeToShow.nameTh} x${recipeToShow.outputQty}`; el.selectedDescription.textContent = recipeDescription(recipeToShow); el.selectedEditions.textContent = editionNote(recipeToShow); renderCraftingGrid(recipeToShow); renderIngredients(recipeToShow); if (el.quantity) { el.quantity.value = String(recipeToShow.outputQty || 1); renderMaterialPlan(); } renderCards(); } function renderCraftingGrid(recipeToShow) { el.craftingGrid.replaceChildren(); recipeToShow.grid.forEach((itemId) => { const slot = document.createElement("div"); slot.className = "crafting-slot"; if (itemId) { slot.appendChild(icon(itemId)); } else { slot.textContent = "-"; slot.classList.add("empty-slot"); } el.craftingGrid.appendChild(slot); }); el.resultSlot.replaceChildren(icon(recipeToShow.result || recipeToShow.id)); } function renderIngredients(recipeToShow) { el.ingredientList.replaceChildren(); recipeToShow.ingredients.forEach((ingredient) => { const li = document.createElement("li"); const primary = ingredient.id || ingredient.alternatives[0]; li.appendChild(icon(primary)); const text = document.createElement("span"); const meta = itemMeta(primary); const alternatives = ingredient.alternatives.filter((id) => id && id !== primary); const altText = alternatives.length ? ` หรือ ${alternatives.map((id) => itemMeta(id).nameTh).join(", ")}` : ""; text.innerHTML = `${meta.nameTh} x${ingredient.count}${meta.nameEn}${altText}`; li.appendChild(text); el.ingredientList.appendChild(li); }); } function renderTabs() { el.tabs.replaceChildren(); categories.forEach((category) => { const button = document.createElement("button"); button.type = "button"; button.className = category.id === activeCategory ? "active" : ""; const count = category.id === "all" ? recipes.length : recipes.filter((recipeToCheck) => recipeToCheck.category === category.id).length; button.textContent = `${category.label} (${count})`; button.addEventListener("click", () => { activeCategory = category.id; visibleLimit = 180; renderTabs(); renderCards(); }); el.tabs.appendChild(button); }); } function renderQuickFilters() { el.quickTabs.replaceChildren(); QUICK_FILTERS.forEach((filter) => { const count = recipes.filter(filter.test).length; const button = document.createElement("button"); button.type = "button"; button.className = filter.id === activeQuickFilter ? "active" : ""; button.textContent = `${filter.label} (${count})`; button.addEventListener("click", () => { activeQuickFilter = filter.id; visibleLimit = 180; renderQuickFilters(); renderCards(); }); el.quickTabs.appendChild(button); }); } function renderTypeFilter() { const types = ["all", ...new Set(recipes.map((recipe) => recipe.type).filter(Boolean))]; el.typeFilter.innerHTML = types .map((type) => ``) .join(""); } function escapeHtml(value) { return String(value ?? "").replace(/[&<>"']/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[ch])); } function matches(recipeToCheck) { const query = (el.search?.value || "").trim().toLowerCase(); const quickFilter = QUICK_FILTERS.find((filter) => filter.id === activeQuickFilter) || QUICK_FILTERS[0]; if (activeCategory !== "all" && recipeToCheck.category !== activeCategory) return false; if (activeType !== "all" && recipeToCheck.type !== activeType) return false; if (!quickFilter.test(recipeToCheck)) return false; if (!query) return true; return recipeToCheck.searchText.includes(query); } function compareRecipeName(a, b) { if (activeSort === "name_en") return a.nameEn.localeCompare(b.nameEn, "en"); if (activeSort === "category") return `${a.category} ${a.nameTh}`.localeCompare(`${b.category} ${b.nameTh}`, "th"); return a.nameTh.localeCompare(b.nameTh, "th"); } function sortRecipes(list) { const categoryOrder = new Map(categories.map((category, index) => [category.id, index])); return [...list].sort((a, b) => { if (activeCategory === "all") { const orderA = categoryOrder.get(a.category) ?? 999; const orderB = categoryOrder.get(b.category) ?? 999; if (orderA !== orderB) return orderA - orderB; } return compareRecipeName(a, b); }); } function getCategoryLabel(categoryId) { const category = categories.find((categoryItem) => categoryItem.id === categoryId); return category?.label || categoryId || "อื่นๆ"; } function createCategoryDivider(categoryId, count) { const divider = document.createElement("div"); divider.className = "recipe-category-divider"; const label = document.createElement("span"); label.textContent = getCategoryLabel(categoryId); const amount = document.createElement("small"); amount.textContent = `${count.toLocaleString("th-TH")} สูตร`; divider.append(label, amount); return divider; } function renderCards() { const visibleRecipes = sortRecipes(recipes.filter(matches)); const recipesToRender = visibleRecipes; const fragment = document.createDocumentFragment(); const showCategoryDividers = activeCategory === "all" && visibleRecipes.length > 0; const categoryCounts = visibleRecipes.reduce((counts, recipeToCount) => { counts.set(recipeToCount.category, (counts.get(recipeToCount.category) || 0) + 1); return counts; }, new Map()); let currentCategory = null; el.grid.replaceChildren(); recipesToRender.forEach((recipeToShow) => { if (showCategoryDividers && recipeToShow.category !== currentCategory) { currentCategory = recipeToShow.category; fragment.appendChild(createCategoryDivider(currentCategory, categoryCounts.get(currentCategory) || 0)); } const button = document.createElement("button"); button.type = "button"; button.className = recipeToShow.id === selectedRecipe?.id ? "recipe-card active" : "recipe-card"; const label = `${recipeToShow.nameTh} (${recipeToShow.nameEn})`; button.setAttribute("aria-label", label); button.title = label; button.dataset.tooltip = label; button.appendChild(icon(recipeToShow.result || recipeToShow.id)); button.addEventListener("click", () => setSelected(recipeToShow)); fragment.appendChild(button); }); el.grid.appendChild(fragment); el.empty.hidden = visibleRecipes.length > 0; el.visibleSummary.textContent = visibleRecipes.length ? `แสดงทั้งหมด ${visibleRecipes.length.toLocaleString("th-TH")} สูตรที่ตรงเงื่อนไข` : "ไม่พบสูตรที่ตรงกับตัวกรอง"; el.loadMore.hidden = true; } function clearFilters() { activeCategory = "all"; activeQuickFilter = "all"; activeType = "all"; activeSort = "name_th"; visibleLimit = 180; el.search.value = ""; el.typeFilter.value = activeType; el.sortFilter.value = activeSort; renderTabs(); renderQuickFilters(); renderCards(); } async function loadDatabase() { ensureFinderControls(); ensureMaterialControls(); if (window.ZabaCrafting?.iconMapUrl) { try { const iconResponse = await fetch(window.ZabaCrafting.iconMapUrl, { cache: "no-store" }); if (iconResponse.ok) Object.assign(ICON_DATA, await iconResponse.json()); } catch (error) { console.warn("Cannot load crafting icon map", error); } } const response = await fetch(DATA_URL, { cache: "no-store" }); if (!response.ok) throw new Error(`Cannot load crafting data: ${response.status}`); const data = await response.json(); categories = data.categories || []; items = data.items || {}; recipes = (data.recipes || []).map(normalizeRecipe); el.recipeCount.textContent = `${recipes.length.toLocaleString("th-TH")} สูตร`; const hashId = decodeURIComponent(location.hash.replace(/^#/, "")); const initialRecipe = recipes.find((recipe) => recipe.id === hashId || recipe.result === hashId) || recipes[0]; renderTabs(); renderQuickFilters(); renderTypeFilter(); setSelected(initialRecipe, Boolean(hashId)); el.search.addEventListener("input", () => { visibleLimit = 180; renderCards(); }); el.typeFilter.addEventListener("change", () => { activeType = el.typeFilter.value; visibleLimit = 180; renderCards(); }); el.sortFilter.addEventListener("change", () => { activeSort = el.sortFilter.value; renderCards(); }); el.clearFilter.addEventListener("click", clearFilters); el.loadMore.addEventListener("click", () => { visibleLimit += 180; renderCards(); }); } loadDatabase().catch((error) => { console.error(error); el.selectedTitle.textContent = "โหลดข้อมูลสูตรคราฟไม่สำเร็จ"; el.selectedSubtitle.textContent = "ตรวจไฟล์ JSON หรือ path ของฐานข้อมูล"; });