"use strict"; const SCALABLE_TOOLS = new Set(scalableToolIds()); const PHYSICS_ICON_TOOL_IDS = new Set(window.TarinaiItemToolMetadata?.PHYSICS_TOOL_IDS || []); const TEXT_ICON_TOOL_IDS = new Set(["poke", "pinch", "water_hose", "copy", "undo", "redo", "area_delete", "clear_tool"]); function drawPhysicsToolIcon(canvas, toolId) { const ctx = canvas?.getContext?.("2d"); if (!ctx || !PHYSICS_ICON_TOOL_IDS.has(toolId)) return false; const w = canvas.width || 160; const h = canvas.height || 160; const sx = w / 160; const sy = h / 160; ctx.clearRect(0, 0, w, h); ctx.save(); ctx.scale(sx, sy); ctx.lineCap = "round"; ctx.lineJoin = "round"; const line = (x1, y1, x2, y2, width = 10, stroke = "#4f4a42") => { ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.lineWidth = width; ctx.strokeStyle = stroke; ctx.stroke(); }; const circle = (x, y, r, fill, stroke = "#4f4a42", width = 6) => { ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fillStyle = fill; ctx.fill(); ctx.lineWidth = width; ctx.strokeStyle = stroke; ctx.stroke(); }; const rounded = (x, y, width, height, radius, fill, stroke = "#4f4a42", lineWidth = 6) => { const r = Math.max(0, Math.min(radius, width / 2, height / 2)); ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + width - r, y); ctx.quadraticCurveTo(x + width, y, x + width, y + r); ctx.lineTo(x + width, y + height - r); ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height); ctx.lineTo(x + r, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - r); ctx.lineTo(x, y + r); ctx.quadraticCurveTo(x, y, x + r, y); ctx.closePath(); ctx.fillStyle = fill; ctx.fill(); ctx.lineWidth = lineWidth; ctx.strokeStyle = stroke; ctx.stroke(); }; if (toolId === "rope") { ctx.beginPath(); ctx.moveTo(22, 112); ctx.bezierCurveTo(48, 22, 92, 144, 138, 48); ctx.lineWidth = 13; ctx.strokeStyle = "#9a6b3f"; ctx.stroke(); circle(22, 112, 10, "#f3d3a6", "#6c4b2e", 6); circle(138, 48, 10, "#f3d3a6", "#6c4b2e", 6); } else if (toolId === "rod") { line(28, 126, 132, 34, 18, "#8a633d"); circle(28, 126, 12, "#d7e0e8"); circle(132, 34, 12, "#d7e0e8"); } else if (toolId === "spring") { const pts = [[18,80],[36,80],[48,48],[64,112],[80,48],[96,112],[112,48],[124,80],[142,80]]; ctx.beginPath(); ctx.moveTo(...pts[0]); for (const p of pts.slice(1)) ctx.lineTo(...p); ctx.lineWidth = 11; ctx.strokeStyle = "#647487"; ctx.stroke(); circle(18,80,8,"#e8eef4","#465564",5); circle(142,80,8,"#e8eef4","#465564",5); } else if (toolId === "wire" || toolId === "insulated_wire") { ctx.beginPath(); ctx.moveTo(18,112); ctx.bezierCurveTo(52,22,104,138,142,52); ctx.lineWidth = toolId === "insulated_wire" ? 16 : 10; ctx.strokeStyle = toolId === "insulated_wire" ? "#3474b8" : "#3e3b39"; ctx.stroke(); if (toolId === "insulated_wire") { ctx.lineWidth = 5; ctx.strokeStyle = "#b9ddff"; ctx.stroke(); } else { ctx.beginPath(); ctx.moveTo(83,47); ctx.lineTo(70,78); ctx.lineTo(87,76); ctx.lineTo(75,108); ctx.lineWidth = 8; ctx.strokeStyle = "#f0b62b"; ctx.stroke(); } circle(18,112,8,"#e9eef1"); circle(142,52,8,"#e9eef1"); } else if (toolId === "pressure_switch") { rounded(27,91,106,36,12,"#6f7881"); rounded(46,50,68,48,15,"#e7b842"); line(58,112,102,112,5,"#c9d2d8"); } else if (toolId === "glass_wall") { rounded(28,35,104,90,8,"rgba(111,205,225,0.34)","#4d8795",8); line(42,105,78,50,7,"#d9f7ff"); line(83,105,116,55,5,"#d9f7ff"); } else if (toolId === "bounce_fence") { line(28,42,28,124,13,"#5b5260"); line(132,42,132,124,13,"#5b5260"); const pts = [[36,82],[52,62],[68,102],[84,62],[100,102],[124,82]]; ctx.beginPath(); ctx.moveTo(...pts[0]); for (const p of pts.slice(1)) ctx.lineTo(...p); ctx.lineWidth = 13; ctx.strokeStyle = "#e05286"; ctx.stroke(); } else if (toolId === "gate_fence") { line(28,32,28,132,13,"#665c50"); line(132,32,132,132,13,"#665c50"); line(34,92,74,72,12,"#c18c55"); line(126,92,86,72,12,"#c18c55"); circle(80,75,8,"#f2d197","#6d5842",5); } else if (toolId === "rotator") { circle(80,80,20,"#e4aa48","#5f574d",8); for (const a of [0, Math.PI*2/3, Math.PI*4/3]) { const x=80+Math.cos(a)*52, y=80+Math.sin(a)*52; line(80,80,x,y,15,"#6d7c8c"); circle(x,y,9,"#cfd8df","#53606b",5); } } else if (toolId === "poison_block") { rounded(31,31,98,98,18,"#65b94f","#285f35",8); circle(62,68,10,"#eaffdf","#285f35",4); circle(98,68,10,"#eaffdf","#285f35",4); line(60,105,100,105,8,"#eaffdf"); line(56,118,104,90,6,"#d5f6ca"); line(56,90,104,118,6,"#d5f6ca"); } else if (toolId === "reciprocator") { line(24,98,136,98,13,"#536474"); rounded(56,55,48,58,10,"#e59a52","#536474",8); line(34,64,50,64,8,"#536474"); line(110,64,126,64,8,"#536474"); } else if (toolId === "one_way_fence") { // Slatted barrier with a single clear pass direction through its center. line(24,119,136,119,13,"#6f6254"); for (const x of [32,54,106,128]) line(x,45,x,126,7,"#a28d73"); rounded(56,52,48,48,10,"rgba(67,164,111,0.18)","#3e8f62",5); line(48,76,111,76,12,"#3e9d69"); ctx.beginPath(); ctx.moveTo(94,56); ctx.lineTo(116,76); ctx.lineTo(94,96); ctx.lineWidth=12; ctx.strokeStyle="#3e9d69"; ctx.stroke(); } else if (toolId === "fence_h") { line(24,55,24,130,12,"#75512f"); line(136,55,136,130,12,"#75512f"); line(24,72,136,72,14,"#b9854d"); line(24,108,136,108,14,"#b9854d"); } else { ctx.restore(); return false; } ctx.restore(); return true; } function attachPhysicsToolIcon(btn, toolId) { if (!btn || !PHYSICS_ICON_TOOL_IDS.has(toolId)) return false; const canvas = createToolPreviewCanvas("tool-sprite-preview tool-physics-preview", 160); if (!drawPhysicsToolIcon(canvas, toolId)) return false; btn.classList.add("tool-has-canvas-icon", "tool-has-physics-icon"); btn.prepend(canvas); return true; } function applyToolTextIcon(btn, def, toolId) { if (!btn || !def?.iconText || !TEXT_ICON_TOOL_IDS.has(toolId)) return false; btn.classList.add("tool-emoji-icon"); btn.style.setProperty("--tool-emoji", JSON.stringify(String(def.iconText))); return true; } function cssToolIconPath(path) { const raw = String(path || "").trim(); if (!raw) return ""; if (/^(?:[a-z]+:|data:|\/)/i.test(raw)) return raw; if (raw.startsWith("../")) return raw; if (raw.startsWith("./assets/")) return `../${raw.slice(2)}`; if (raw.startsWith("assets/")) return `../${raw}`; return raw; } function applyToolImageIcon(btn, def) { if (!btn || !def?.icon) return false; const iconPath = cssToolIconPath(def.icon); if (!iconPath) return false; btn.style.setProperty("--tool-icon", `url(${JSON.stringify(iconPath)})`); return true; } const SIZE_ORDER = window.TarinaiItemToolMetadata?.TOOL_SIZE_ORDER || ["small", "medium", "large"]; const SIZE_LABELS = window.TarinaiItemToolMetadata?.TOOL_SIZE_LABELS || { small: "\u5c0f", medium: "\u4e2d", large: "\u5927" }; const TOOL_SHORTCUTS = window.TarinaiItemToolMetadata?.TOOL_SHORTCUTS || {}; const PICK_MODE_LABELS = Object.freeze({ free: "\u81ea\u7531", grid20: "20px", grid40: "40px", grid60: "60px", }); const PICK_MODE_NAMES = Object.freeze({ free: "\u81ea\u7531\u914d\u7f6e", grid20: "20px\u683c\u5b50", grid40: "40px\u683c\u5b50", grid60: "60px\u683c\u5b50", }); function normalizedToolPickMode() { const value = String(world?.toolPickMode || "free").toLowerCase(); if (value === "grid") return "grid40"; return PICK_MODE_LABELS[value] ? value : "free"; } function toolPickModeLabel() { return PICK_MODE_LABELS[normalizedToolPickMode()] || "\u81ea\u7531"; } function createToolPickModeControl() { const btn = document.createElement("button"); btn.className = "tool tool-emoji-icon tool-pick-mode-control"; btn.type = "button"; btn.dataset.pickModeControl = "1"; btn.style.setProperty("--tool-emoji", JSON.stringify("\u25a6")); const label = document.createElement("span"); label.className = "tool-label"; label.textContent = "\u5438\u7740"; const value = document.createElement("b"); value.className = "tool-pick-mode-value"; btn.append(label, value); window.TarinaiTooltips?.bind?.(btn, () => { const mode = normalizedToolPickMode(); const name = PICK_MODE_NAMES[mode] || PICK_MODE_NAMES.free; return `\u5438\u7740\u30e2\u30fc\u30c9: ${name}\u3002\u62bc\u3059\u3068\u81ea\u7531\u914d\u7f6e\u300120px\u683c\u5b50\u300140px\u683c\u5b50\u300160px\u683c\u5b50\u306e\u9806\u306b\u5207\u308a\u66ff\u3048\u307e\u3059\u3002`; }); return btn; } const PANEL_SEARCH_STATE = { query: "", matches: [], index: -1 }; function panelSearchControl() { return document.getElementById("panelSearchControl") || document.querySelector(".panel-search-control"); } function normalizePanelSearchText(value = "") { return String(value || "").normalize("NFKC").replace(/\s+/g, " ").trim().toLocaleLowerCase("ja-JP"); } function clearPanelSearchHighlights() { const root = ui?.panel || document.querySelector(".panel"); for (const el of root?.querySelectorAll?.(".panel-search-match, .panel-search-current") || []) { el.classList.remove("panel-search-match", "panel-search-current"); } PANEL_SEARCH_STATE.matches = []; PANEL_SEARCH_STATE.index = -1; } function panelSearchHighlightTarget(el) { return el?.closest?.(".tool, .panel-card-toggle, .tool-category-toggle, .stat, .chart-series-toggle, .log-entry, .selected-category, .colony-setting-section, label, button, summary") || el; } function panelSearchElementText(el) { if (!el) return ""; const parts = [el.textContent, el.getAttribute?.("aria-label"), el.dataset?.tip]; if (typeof el.__tarinaiTipProvider === "function") { try { parts.push(el.__tarinaiTipProvider()); } catch (_) { /* Ignore a transient tooltip provider failure. */ } } for (const tipEl of el.querySelectorAll?.("[data-tip]") || []) parts.push(tipEl.dataset.tip); return normalizePanelSearchText(parts.filter(Boolean).join(" ")); } function panelSearchCandidates(root) { const selector = ".tool, .panel-card-toggle, .tool-category-toggle, .stat, .chart-series-toggle, .log-entry, .selected-category, .colony-setting-section, summary, label, button, option, h2, h3, p, small, strong, span"; const seen = new Set(); const candidates = []; for (const el of root?.querySelectorAll?.(selector) || []) { if (!el || el.closest(".panel-search-control")) continue; if (el.matches('.tool-category[data-tool-category="operate"] .tool-label')) continue; const target = panelSearchHighlightTarget(el); if (!target || seen.has(target)) continue; const text = panelSearchElementText(target); if (!text) continue; seen.add(target); candidates.push({ target, text }); } return candidates; } function runPanelSearch(query, { preserveIndex = false } = {}) { const root = ui?.panel || document.querySelector(".panel"); const normalized = normalizePanelSearchText(query); const previousIndex = PANEL_SEARCH_STATE.index; clearPanelSearchHighlights(); PANEL_SEARCH_STATE.query = normalized; const control = panelSearchControl(); const status = control?.querySelector?.(".panel-search-status"); const prev = control?.querySelector?.(".panel-search-prev"); const next = control?.querySelector?.(".panel-search-next"); if (!root || !normalized) { if (status) status.textContent = "0"; if (prev) prev.disabled = true; if (next) next.disabled = true; return []; } const matches = []; for (const candidate of panelSearchCandidates(root)) { if (!candidate.text.includes(normalized)) continue; candidate.target.classList.add("panel-search-match"); matches.push(candidate.target); } PANEL_SEARCH_STATE.matches = matches; PANEL_SEARCH_STATE.index = preserveIndex && matches.length ? Math.min(previousIndex, matches.length - 1) : -1; if (status) status.textContent = String(matches.length); if (prev) prev.disabled = matches.length === 0; if (next) next.disabled = matches.length === 0; return matches; } function revealPanelSearchTarget(target) { if (!target) return; for (const category of [target.closest?.(".tool-category")].filter(Boolean)) { category.classList.remove("collapsed"); category.querySelector(".tool-category-toggle")?.setAttribute("aria-expanded", "true"); } for (const card of [target.closest?.(".collapsible-card")].filter(Boolean)) { card.classList.remove("collapsed"); card.querySelector(".panel-card-toggle")?.setAttribute("aria-expanded", "true"); const state = card.querySelector(".panel-card-state"); if (state) state.textContent = "\u6298\u308a\u7573\u307f"; } for (const details of target.closest?.("details") ? [target.closest("details")] : []) details.open = true; } function movePanelSearchMatch(input, direction = 1) { const query = normalizePanelSearchText(input?.value || PANEL_SEARCH_STATE.query); const matches = runPanelSearch(query, { preserveIndex: true }); if (!matches.length) return; for (const el of matches) el.classList.remove("panel-search-current"); const step = direction < 0 ? -1 : 1; const base = PANEL_SEARCH_STATE.index < 0 ? (step > 0 ? -1 : 0) : PANEL_SEARCH_STATE.index; PANEL_SEARCH_STATE.index = (base + step + matches.length) % matches.length; const target = matches[PANEL_SEARCH_STATE.index]; revealPanelSearchTarget(target); target.classList.add("panel-search-current"); const status = panelSearchControl()?.querySelector?.(".panel-search-status"); if (status) status.textContent = `${PANEL_SEARCH_STATE.index + 1}/${matches.length}`; requestAnimationFrame(() => target.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest" })); } function bindPanelSearchControl() { const control = panelSearchControl(); const input = control?.querySelector?.(".panel-search-input"); const prev = control?.querySelector?.(".panel-search-prev"); const next = control?.querySelector?.(".panel-search-next"); if (!control || !input || control.dataset.searchBound === "1") return; control.dataset.searchBound = "1"; input.addEventListener("input", () => runPanelSearch(input.value)); input.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); movePanelSearchMatch(input, event.shiftKey ? -1 : 1); } if (event.key === "Escape") { input.value = ""; runPanelSearch(""); } }); prev?.addEventListener("click", () => movePanelSearchMatch(input, -1)); next?.addEventListener("click", () => movePanelSearchMatch(input, 1)); } function renderToolPalette() { if (!ui?.toolPalette || typeof toolCategories !== "function") return; bindPanelSearchControl(); ui.toolPalette.innerHTML = ""; const frag = document.createDocumentFragment(); for (const category of toolCategories()) { const section = document.createElement("section"); section.className = `tool-category ${category.themeClass || ""}`.trim(); section.dataset.toolCategory = category.id; const mobileCompact = Boolean(window.TarinaiInputMode.classification?.touchFirst || document.body.classList.contains("touch-mobile-ui")); const initiallyCollapsed = mobileCompact && category.id !== "operate"; if (initiallyCollapsed) section.classList.add("collapsed"); const toggle = document.createElement("button"); toggle.className = "tool-category-toggle"; toggle.type = "button"; toggle.setAttribute("aria-expanded", initiallyCollapsed ? "false" : "true"); toggle.textContent = category.label || category.id; const body = document.createElement("div"); body.className = "tool-grid tool-category-body"; for (const toolId of category.toolIds || []) { const def = toolDefinition(toolId); if (!def || def.simulationOnly || toolId === "observe") continue; const btn = document.createElement("button"); btn.className = "tool"; const hasPhysicsIcon = attachPhysicsToolIcon(btn, toolId); const hasTextIcon = !hasPhysicsIcon && applyToolTextIcon(btn, def, toolId); if (!hasPhysicsIcon && !hasTextIcon) applyToolImageIcon(btn, def); btn.dataset.tool = toolId; btn.dataset.toolCategory = category.id || ""; const label = document.createElement("span"); label.className = "tool-label"; label.textContent = def.label || toolId; btn.setAttribute("aria-label", label.textContent); btn.removeAttribute("title"); btn.appendChild(label); const shortcut = category.id === "operate" ? TOOL_SHORTCUTS[toolId] : ""; if (shortcut) { const badge = document.createElement("kbd"); badge.className = "tool-shortcut"; badge.textContent = shortcut; btn.setAttribute("aria-keyshortcuts", shortcut); btn.appendChild(badge); } if (!hasPhysicsIcon) attachToolSpritePreview(btn, def, toolId); if (toolId === world?.tool) btn.classList.add("selected"); body.appendChild(btn); if (category.id === "operate" && toolId === "clear_tool") body.appendChild(createToolPickModeControl()); } section.append(toggle, body); frag.appendChild(section); } ui.toolPalette.appendChild(frag); } function drawToolPreviewCanvas(canvas, itemType, { watermark = false } = {}) { if (!canvas || !itemType) return false; const ctx = canvas.getContext("2d"); if (!ctx) return false; ctx.clearRect(0, 0, canvas.width, canvas.height); if (typeof window.drawToolItemPreview !== "function") return false; return window.drawToolItemPreview(ctx, itemType, { width: canvas.width, height: canvas.height, compact: true, watermark }) !== false; } function createToolPreviewCanvas(className, size = 96) { const canvas = document.createElement("canvas"); canvas.className = className; canvas.width = size; canvas.height = size; canvas.setAttribute("aria-hidden", "true"); return canvas; } function attachToolSpritePreview(btn, def, toolId) { const itemType = def?.itemType || toolId; if (!btn || !itemType || !def?.placeable || TEXT_ICON_TOOL_IDS.has(toolId)) return; if (toolId === "reciprocator") return; if (toolId === "ball" || itemType === "ball") return; if (typeof window.drawToolItemPreview !== "function") return; const iconCanvas = createToolPreviewCanvas("tool-sprite-preview", 160); const iconDrawn = drawToolPreviewCanvas(iconCanvas, itemType); if (!iconDrawn) return; btn.classList.add("tool-has-canvas-icon"); btn.prepend(iconCanvas); } function toolSizeFor(tool) { if (!SCALABLE_TOOLS.has(tool)) return ""; return normalizedToolSizeFor(world, tool); } function cycleToolSize(tool) { if (!SCALABLE_TOOLS.has(tool)) return false; const result = window.TarinaiCommands.dispatch(world, { type: "tool.size.cycle", toolId: tool, order: SIZE_ORDER }); if (!result?.ok) return false; syncToolSizeBadges(); showToast(`\u30b5\u30a4\u30ba: ${SIZE_LABELS[result.size] || result.size}`); return true; } function cycleToolPickMode() { const result = window.TarinaiCommands.dispatch(world, { type: "tool.pick.cycle" }); if (!result?.ok) return false; syncToolSizeBadges(); showToast(`\u5438\u7740: ${PICK_MODE_LABELS[result.mode] || "\u81ea\u7531"}`); return true; } function syncToolPickModeControls() { const mode = normalizedToolPickMode(); for (const btn of ui?.toolPalette?.querySelectorAll?.("[data-pick-mode-control]") || []) { const value = btn.querySelector(".tool-pick-mode-value"); if (value) value.textContent = PICK_MODE_LABELS[mode] || "\u81ea\u7531"; btn.dataset.pickMode = mode; btn.setAttribute("aria-label", `\u5438\u7740\u30e2\u30fc\u30c9: ${PICK_MODE_NAMES[mode] || PICK_MODE_NAMES.free}\u3002\u62bc\u3059\u3068\u6b21\u306e\u30e2\u30fc\u30c9\u3078\u5207\u308a\u66ff\u3048\u307e\u3059\u3002`); btn.removeAttribute("title"); } } function syncToolSizeBadges() { for (const btn of uiCache.toolButtons || []) { const tool = btn.dataset.tool || ""; const old = btn.querySelector(".tool-size-badge"); if (!SCALABLE_TOOLS.has(tool)) { if (old) old.remove(); continue; } const size = toolSizeFor(tool); let badge = old; if (!badge) { badge = document.createElement("b"); badge.className = "tool-size-badge"; btn.appendChild(badge); } badge.textContent = SIZE_LABELS[size] || size; badge.title = "\u30b5\u30a4\u30ba\u3092\u5207\u308a\u66ff\u3048\u308b"; } syncToolPickModeControls(); }