"use strict"; function bindUI() { renderToolPalette(); loadLogPushSettings(); syncLogPushControls(); syncAudioControls(); applyToolTips(); renderEcologyCards(); window.TarinaiSaveSystem?.bindSaveSystem?.(); window.TarinaiFreezeSystem?.bindFreezeSystem?.(); uiCache.toolButtons = Array.from(ui.toolPalette?.querySelectorAll(".tool") || []); syncToolSizeBadges(); for (const toggle of document.querySelectorAll(".panel-card-toggle")) { toggle.addEventListener("click", () => { const card = toggle.closest(".collapsible-card"); if (!card) return; audio.uiFold?.(); const collapsed = card.classList.toggle("collapsed"); toggle.setAttribute("aria-expanded", collapsed ? "false" : "true"); const state = toggle.querySelector(".panel-card-state"); if (state) state.textContent = collapsed ? "\u5c55\u958b" : "\u6298\u308a\u7573\u307f"; }); } ui.colonyChartTabs?.addEventListener("click", (e) => { const btn = e.target.closest("button[data-chart]"); if (!btn) return; uiCache.chartMode = btn.dataset.chart || "population"; for (const b of ui.colonyChartTabs.querySelectorAll("button")) b.classList.toggle("active", b === btn); uiCache.lastChartDraw = ""; uiCache.lastChartDrawAt = 0; renderStats(); }); syncToolSizeBadges(); ui.logPushEnabled?.addEventListener("change", () => { uiCache.logPushEnabled = Boolean(ui.logPushEnabled.checked); audio.notify?.(); saveLogPushSettings(); showToast(uiCache.logPushEnabled ? "\u30ed\u30b0\u306e\u30d7\u30c3\u30b7\u30e5\u901a\u77e5\u3092ON\u306b\u3057\u307e\u3057\u305f\u3002" : "\u30ed\u30b0\u306e\u30d7\u30c3\u30b7\u30e5\u901a\u77e5\u3092OFF\u306b\u3057\u307e\u3057\u305f\u3002"); }); ui.logPushControls?.addEventListener("change", (e) => { const input = e.target.closest("[data-push-kind]"); if (!input) return; const kind = input.dataset.pushKind || ""; audio.uiClick?.(); if (input.checked) uiCache.logPushKinds.add(kind); else uiCache.logPushKinds.delete(kind); saveLogPushSettings(); }); ui.soundCategoryControls?.addEventListener("change", (e) => { const input = e.target.closest("[data-sound-category]"); if (!input) return; const category = input.dataset.soundCategory || ""; audio.setCategory?.(category, Boolean(input.checked)); if (audio.enabled && input.checked) audio.uiClick?.(); syncAudioControls(); showToast(`${audio.soundCategories?.[category] || category} ${input.checked ? "ON" : "OFF"}`); }); ui.archiveContent?.addEventListener("scroll", scheduleArchiveWindowRender, { passive: true }); window.addEventListener("scroll", schedulePendingArchiveRender, { passive: true }); window.addEventListener("resize", schedulePendingArchiveRender, { passive: true }); const bindArchiveHorizontal = (root) => { if (!root) return; root.addEventListener("wheel", (e) => { const scroller = e.target.closest(".lineage-family-tree, .lineage-main-content"); if (!scroller) return; const canX = scroller.scrollWidth > scroller.clientWidth + 4; if (!canX) return; const dominant = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : (e.shiftKey ? e.deltaY : 0); if (!dominant) return; e.preventDefault(); scroller.scrollLeft += dominant; }, { passive: false }); }; bindArchiveHorizontal(ui.archiveContent); ui.archiveContent?.addEventListener("click", (e) => { const node = e.target.closest("[data-family-tarinai-id]"); if (!node) return; const id = node.getAttribute("data-family-tarinai-id"); const t = world.tarinai.find(x => (x.id === id || x.familyKey === id) && !x.dead); if (!t) { showToast("\u305d\u306e\u500b\u4f53\u306f\u3082\u3046\u9078\u629e\u3067\u304d\u307e\u305b\u3093\u3002"); return; } world.selected = t; t.focusPulseTimer = Math.max(t.focusPulseTimer || 0, 1.15); selectTool("observe"); if (Number.isFinite(t.x) && Number.isFinite(t.y)) { world.cameraX = t.x - (world.visibleWorldW ? world.visibleWorldW() : (world.viewportW || world.w)) / 2; world.cameraY = t.y - (world.visibleWorldH ? world.visibleWorldH() : (world.viewportH || world.h)) / 2; world.clampCamera?.(); } renderStats(); showToast(`${t.name}\u3092\u9078\u629e\u3057\u307e\u3057\u305f\u3002`); }); if (!uiCache.archiveAutoRefreshTimer && typeof window !== "undefined") { uiCache.archiveAutoRefreshTimer = window.setInterval(() => { if (!ui.archiveContent || !archiveAutoUpdateEnabled()) return; scheduleArchiveWindowRender(); }, 5000); } ui.selectedInfo?.addEventListener("click", (e) => { const categoryToggle = e.target.closest(".selected-category-toggle"); if (categoryToggle) { const category = categoryToggle.closest(".selected-category"); const id = category?.dataset?.selectedCategory || ""; if (!category || !id) return; if (!uiCache.selectedCollapsedCategories) uiCache.selectedCollapsedCategories = new Set(); audio.uiFold?.(); category.classList.toggle("collapsed"); const collapsed = category.classList.contains("collapsed"); if (collapsed) uiCache.selectedCollapsedCategories.add(id); else uiCache.selectedCollapsedCategories.delete(id); categoryToggle.setAttribute("aria-expanded", collapsed ? "false" : "true"); return; } const focusBtn = e.target.closest("[data-focus-tarinai-id]"); if (focusBtn) { e.preventDefault(); focusTarinaiById(focusBtn.getAttribute("data-focus-tarinai-id")); return; } const btn = e.target.closest("[data-selected-action=\"favorite\"]"); if (!btn || !world.selected || world.selected.dead) return; world.selected.favorite = !world.selected.favorite; uiCache.selectedSnapshot = ""; renderSelected(); render(); showToast(world.selected.favorite ? "\u304a\u6c17\u306b\u5165\u308a\u306b\u3057\u307e\u3057\u305f\u3002" : "\u304a\u6c17\u306b\u5165\u308a\u3092\u89e3\u9664\u3057\u307e\u3057\u305f\u3002"); }); ui.selectedInfo?.addEventListener("change", (e) => { const input = e.target.closest("[data-selected-name]"); if (!input) return; renameSelectedTarinai(input.value); }); ui.selectedInfo?.addEventListener("keydown", (e) => { const input = e.target.closest("[data-selected-name]"); if (!input) return; if (e.key === "Enter") { e.preventDefault(); input.blur(); } if (e.key === "Escape") { e.preventDefault(); input.value = world.selected?.name || ""; input.blur(); } }); ui.selectedCloseBtn?.addEventListener("click", () => { world.selected = null; uiCache.selectedSnapshot = ""; uiCache.selectedEmpty = false; renderSelected(); render(); }); ui.pauseBtn?.addEventListener("click", () => { audio.uiClick?.(); world.paused = !world.paused; const archiveStale = uiCache.archiveFamilyVersion !== (world.familyVersion || 0); if (archiveAutoUpdateEnabled() && world.paused && (uiCache.archivePendingWhileRunning || archiveStale)) { uiCache.archivePendingWhileRunning = false; scheduleArchiveWindowRender(); } syncTopButtons(); }); ui.speedBtn?.addEventListener("click", () => { audio.uiClick?.(); const speeds = [1, 2, 4]; const i = speeds.indexOf(world.speed); world.speed = speeds[(i + 1) % speeds.length]; syncTopButtons(); }); ui.ecologyBtn?.addEventListener("click", () => { audio.uiClick?.(); openEcologyDialog(); }); ui.soundBtn?.addEventListener("click", (e) => { e.stopPropagation(); toggleSoundPanel(); }); ui.soundPanel?.addEventListener("click", (e) => e.stopPropagation()); ui.soundMasterToggle?.addEventListener("change", () => { const on = audio.setEnabled ? audio.setEnabled(ui.soundMasterToggle.checked) : audio.toggle(); if (on) audio.uiClick?.(); syncAudioControls(); showToast(on ? "\u52b9\u679c\u97f3\u3092ON\u306b\u3057\u307e\u3057\u305f\u3002" : "\u52b9\u679c\u97f3\u3092OFF\u306b\u3057\u307e\u3057\u305f\u3002"); }); document.addEventListener("click", (e) => { if (!ui.soundMenu || ui.soundMenu.contains(e.target)) return; closeSoundPanel(); }); ui.creditsBtn?.addEventListener("click", () => { audio.uiClick?.(); openCreditsDialog(); }); ui.creditsCloseBtn?.addEventListener("click", closeCreditsDialog); ui.creditsDialog?.addEventListener("click", (e) => { if (e.target === ui.creditsDialog) closeCreditsDialog(); }); ui.resetBtn?.addEventListener("click", () => { audio.uiClick?.(); openFieldDialog(); }); ui.fieldCancelBtn?.addEventListener("click", closeFieldDialog); ui.fieldDialog?.addEventListener("click", (e) => { if (e.target === ui.fieldDialog) { closeFieldDialog(); return; } const btn = e.target.closest("[data-field-type]"); if (!btn) return; resetWithField(btn.dataset.fieldType || "garden"); }); ui.ecologyCloseBtn?.addEventListener("click", closeEcologyDialog); ui.ecologyDialog?.addEventListener("click", (e) => { if (e.target === ui.ecologyDialog) closeEcologyDialog(); }); ui.archiveUpdateToggleBtn?.addEventListener("click", () => { audio.uiClick?.(); setArchiveUpdateEnabled(!archiveAutoUpdateEnabled()); }); syncArchiveUpdateButton(); ui.archiveResetBtn?.addEventListener("click", () => { audio.uiClick?.(); openArchiveResetDialog(); }); ui.archiveResetCancelBtn?.addEventListener("click", closeArchiveResetDialog); ui.archiveResetDialog?.addEventListener("click", (e) => { if (e.target === ui.archiveResetDialog) closeArchiveResetDialog(); }); ui.archiveResetConfirmBtn?.addEventListener("click", () => { world.resetFamilyTree?.(); window.resetArchiveRenderState?.(); closeArchiveResetDialog(); renderArchive(); renderSelected(); showToast("\u5bb6\u7cfb\u56f3\u3092\u30ea\u30bb\u30c3\u30c8\u3057\u307e\u3057\u305f\u3002"); }); ui.toolPalette?.addEventListener("click", (e) => { const toggle = e.target.closest(".tool-category-toggle"); if (toggle) { const category = toggle.closest(".tool-category"); if (!category) return; audio.uiFold?.(); category.classList.toggle("collapsed"); toggle.setAttribute("aria-expanded", category.classList.contains("collapsed") ? "false" : "true"); return; } const btn = e.target.closest(".tool"); if (!btn) return; const tool = btn.dataset.tool; audio.uiClick?.(); if (world.tool === tool && cycleToolSize(tool)) return; selectTool(tool); }); const chainScroll = (el) => { if (!el) return; el.addEventListener("wheel", (e) => { if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return; const atTop = el.scrollTop <= 0; const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 1; if ((e.deltaY < 0 && atTop) || (e.deltaY > 0 && atBottom)) { const page = document.scrollingElement || document.documentElement; page.scrollTop += e.deltaY; } }, { passive: true }); }; const transferEdgeScroll = (child, parent) => { if (!child || !parent) return; child.addEventListener("wheel", (e) => { if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return; const childCanScroll = child.scrollHeight > child.clientHeight + 1; const atTop = child.scrollTop <= 0; const atBottom = child.scrollTop + child.clientHeight >= child.scrollHeight - 1; const movingInside = childCanScroll && ((e.deltaY < 0 && !atTop) || (e.deltaY > 0 && !atBottom)); e.preventDefault(); e.stopPropagation(); if (movingInside) { child.scrollTop += e.deltaY; return; } parent.scrollTop += e.deltaY; }, { passive: false }); }; const rightPanel = document.querySelector(".panel"); chainScroll(rightPanel); chainScroll(ui.archiveContent); transferEdgeScroll(ui.log, rightPanel); const inputModeManager = window.TarinaiInputMode; const setMobileInputMode = (mode = "auto") => { mode = inputModeManager?.setMode?.(mode) || (["auto", "camera", "tool", "family"].includes(mode) ? mode : "auto"); uiCache.mobileInputMode = mode; ui.mobileModeControls?.querySelectorAll("[data-mobile-mode]").forEach(btn => btn.classList.toggle("active", btn.dataset.mobileMode === mode)); document.body.classList.toggle("mobile-mode-camera", mode === "camera"); document.body.classList.toggle("mobile-mode-tool", mode === "tool"); document.body.classList.toggle("mobile-mode-family", mode === "family"); }; const updateTouchMobileUiClass = () => { const enabled = Boolean(inputModeManager?.refresh?.().touchFirst); document.body.classList.toggle("touch-mobile-ui", enabled); if (ui.mobileModeControls) ui.mobileModeControls.hidden = !enabled; }; const mobileInputMode = () => inputModeManager?.currentMode || uiCache.mobileInputMode || "auto"; updateTouchMobileUiClass(); window.addEventListener("resize", updateTouchMobileUiClass, { passive: true }); window.matchMedia?.("(pointer: coarse)")?.addEventListener?.("change", updateTouchMobileUiClass); setMobileInputMode(uiCache.mobileInputMode || "auto"); ui.mobileModeControls?.addEventListener("click", (e) => { const btn = e.target.closest("[data-mobile-mode]"); if (!btn) return; audio.uiClick?.(); setMobileInputMode(btn.dataset.mobileMode || "auto"); showToast(`\u30b9\u30de\u30db\u64cd\u4f5c: ${btn.textContent || btn.dataset.mobileMode}`); }); const touchPoint = (touches, index = 0) => { const t = touches?.[index]; if (!t) return null; return { clientX: t.clientX, clientY: t.clientY }; }; const touchCenter = (touches) => { const a = touchPoint(touches, 0); const b = touchPoint(touches, 1); if (!a) return null; if (!b) return a; return { clientX: (a.clientX + b.clientX) / 2, clientY: (a.clientY + b.clientY) / 2 }; }; const touchDistance = (touches) => { const a = touchPoint(touches, 0); const b = touchPoint(touches, 1); if (!a || !b) return 0; return Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY); }; const screenToWorldClient = (clientX, clientY) => { const rect = canvas.getBoundingClientRect(); const sx = clientX - rect.left; const sy = clientY - rect.top; return world.screenToWorld ? world.screenToWorld(sx, sy) : { x: sx, y: sy, inside: sx >= 0 && sy >= 0 }; }; const clientToWorldRaw = (clientX, clientY) => { const rect = canvas.getBoundingClientRect(); const sx = clientX - rect.left; const sy = clientY - rect.top; const scale = world.viewScale ? world.viewScale() : 1; const off = world.fieldScreenOffset ? world.fieldScreenOffset() : { x: 0, y: 0 }; const rawX = (sx - off.x) / scale + (world.cameraX || 0); const rawY = (sy - off.y) / scale + (world.cameraY || 0); return { x: rawX, y: rawY, inside: rawX >= 0 && rawY >= 0 && rawX <= world.w && rawY <= world.h }; }; const setZoomAroundClientPoint = (clientX, clientY, nextZoom) => { if (!world.setFieldZoom) return false; const rect = canvas.getBoundingClientRect(); const sx = clientX - rect.left; const sy = clientY - rect.top; const focus = world.screenToWorld ? world.screenToWorld(sx, sy) : { x: sx, y: sy }; if (!world.setFieldZoom(nextZoom)) return false; const off = world.fieldScreenOffset ? world.fieldScreenOffset() : { x: 0, y: 0 }; const scale = world.viewScale ? world.viewScale() : 1; world.cameraX = focus.x - (sx - off.x) / scale; world.cameraY = focus.y - (sy - off.y) / scale; world.clampCamera?.(); return true; }; function isClientOverFrozenPanel(clientX, clientY) { const panel = document.getElementById("frozenPanel"); if (!panel) return false; const r = panel.getBoundingClientRect(); return clientX >= r.left && clientX <= r.right && clientY >= r.top && clientY <= r.bottom; } function rememberGrabOrigin(target) { if (!target || uiCache.grabKind !== "tarinai") return; uiCache.grabOrigin = { x: target.x, y: target.y, vx: target.vx || 0, vy: target.vy || 0 }; } function finishTarinaiGrab(target, clientX, clientY) { if (!target || uiCache.grabKind !== "tarinai") return false; if (isClientOverFrozenPanel(clientX, clientY) && window.TarinaiFreezeSystem?.freezeTarinai) { window.TarinaiFreezeSystem.freezeTarinai(target, world); uiCache.grabOrigin = null; return true; } const p = screenToWorldClient(clientX, clientY); if (!p.inside && uiCache.grabOrigin) { target.x = uiCache.grabOrigin.x; target.y = uiCache.grabOrigin.y; target.vx = uiCache.grabOrigin.vx || 0; target.vy = uiCache.grabOrigin.vy || 0; target.thought = "フィールドに戻された"; target.pinchThoughtTimer = Math.max(target.pinchThoughtTimer || 0, 0.8); showToast("フィールド外なので元の位置に戻しました。"); uiCache.grabOrigin = null; return true; } uiCache.grabOrigin = null; return false; } const beginTouchGrab = (p, e) => { const target = world.findGrabTargetAt ? world.findGrabTargetAt(p.x, p.y) : null; if (!target) return false; audio.grab?.(); uiCache.grabbing = true; uiCache.grabMoved = false; uiCache.grabTarget = target; uiCache.grabKind = target instanceof Tarinai ? "tarinai" : "item"; rememberGrabOrigin(target); uiCache.grabLastWorldX = p.x; uiCache.grabLastWorldY = p.y; uiCache.grabStartX = e.touches?.[0]?.clientX || 0; uiCache.grabStartY = e.touches?.[0]?.clientY || 0; if (uiCache.grabKind === "tarinai") { target.sleeping = false; if (target.state === "sleep" || target.state === "seek_bed") target.goIdle?.("つままれている"); target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.18); target.thought = "つままれている"; target.pinchThoughtTimer = Math.max(target.pinchThoughtTimer || 0, 1.2); } else { if (isPinType(target.type) && target.pinState === "lodged" && target.detachPushpin) target.detachPushpin(world, "pinch"); target.dropTimer = 0; target.dropMax = 0; target.dropImpactDone = true; } uiCache.touchMode = "grab"; renderStats(); return true; }; canvas?.addEventListener("touchstart", (e) => { if (!e.touches?.length) return; e.preventDefault(); uiCache.touchMoved = false; const mode = mobileInputMode(); if ((mode === "camera" || mode === "family") && e.touches.length === 1) { const t = e.touches[0]; uiCache.touchStartX = t.clientX; uiCache.touchStartY = t.clientY; uiCache.touchLastX = t.clientX; uiCache.touchLastY = t.clientY; uiCache.touchMode = "cameraPan"; return; } if (e.touches.length >= 2) { const c = touchCenter(e.touches); uiCache.touchMode = "pinchZoom"; uiCache.pinchStartDistance = Math.max(1, touchDistance(e.touches)); uiCache.pinchStartZoom = world.fieldZoom || 1; uiCache.pinchLastCenterX = c?.clientX || 0; uiCache.pinchLastCenterY = c?.clientY || 0; return; } const t = e.touches[0]; const p = screenToWorldClient(t.clientX, t.clientY); uiCache.touchStartX = t.clientX; uiCache.touchStartY = t.clientY; uiCache.touchLastX = t.clientX; uiCache.touchLastY = t.clientY; uiCache.touchLastWorldX = p.x; uiCache.touchLastWorldY = p.y; if (mode === "tool" && world.tool === "observe") { uiCache.touchMode = "toolTapOnly"; return; } if (world.tool === "water_hose" && p.inside) { audio.waterHose?.(); uiCache.hosing = true; uiCache.hoseMoved = false; uiCache.hoseLastWorldX = p.x; uiCache.hoseLastWorldY = p.y; uiCache.hoseStartX = t.clientX; uiCache.hoseStartY = t.clientY; world.applyWaterHose?.(p.x, p.y, 0, 0, 0.10); uiCache.touchMode = "hose"; render(); return; } if (world.tool === "pinch" && p.inside && beginTouchGrab(p, e)) return; uiCache.touchMode = mode === "tool" ? "toolTapOnly" : "panOrTap"; }, { passive: false }); canvas?.addEventListener("touchmove", (e) => { if (!e.touches?.length || !uiCache.touchMode) return; e.preventDefault(); if (uiCache.touchMode === "pinchZoom" && e.touches.length >= 2) { const c = touchCenter(e.touches); const distNow = Math.max(1, touchDistance(e.touches)); const ratio = distNow / Math.max(1, uiCache.pinchStartDistance || distNow); setZoomAroundClientPoint(c.clientX, c.clientY, (uiCache.pinchStartZoom || 1) * ratio); const dx = c.clientX - (uiCache.pinchLastCenterX || c.clientX); const dy = c.clientY - (uiCache.pinchLastCenterY || c.clientY); uiCache.pinchLastCenterX = c.clientX; uiCache.pinchLastCenterY = c.clientY; if (Math.hypot(dx, dy) > 0.1) world.panCamera?.(-dx, -dy); uiCache.touchMoved = true; render(); return; } const t = e.touches[0]; const p = screenToWorldClient(t.clientX, t.clientY); const screenDx = t.clientX - (uiCache.touchLastX || t.clientX); const screenDy = t.clientY - (uiCache.touchLastY || t.clientY); const totalMove = Math.hypot(t.clientX - (uiCache.touchStartX || t.clientX), t.clientY - (uiCache.touchStartY || t.clientY)); uiCache.touchLastX = t.clientX; uiCache.touchLastY = t.clientY; if (uiCache.touchMode === "cameraPan") { if (totalMove > 2) uiCache.touchMoved = true; world.panCamera?.(-screenDx, -screenDy); render(); return; } if (uiCache.touchMode === "toolTapOnly") { if (totalMove > 8) uiCache.touchMoved = true; return; } if (uiCache.touchMode === "hose" && uiCache.hosing) { const dx = p.x - (uiCache.hoseLastWorldX ?? p.x); const dy = p.y - (uiCache.hoseLastWorldY ?? p.y); uiCache.hoseLastWorldX = p.x; uiCache.hoseLastWorldY = p.y; if (totalMove > 3) { uiCache.hoseMoved = true; uiCache.touchMoved = true; } if (p.inside) world.applyWaterHose?.(p.x, p.y, dx, dy, 0.10); render(); return; } if (uiCache.touchMode === "grab" && uiCache.grabbing) { const target = uiCache.grabTarget; if (!target || target.dead) { uiCache.grabbing = false; uiCache.grabTarget = null; uiCache.touchMode = ""; return; } const dx = p.x - (uiCache.grabLastWorldX ?? p.x); const dy = p.y - (uiCache.grabLastWorldY ?? p.y); uiCache.grabLastWorldX = p.x; uiCache.grabLastWorldY = p.y; if (totalMove > 3) { uiCache.grabMoved = true; uiCache.touchMoved = true; } const pad = target.radius || target.r || 16; if (uiCache.grabKind === "tarinai" && !p.inside) { const raw = clientToWorldRaw(e.touches?.[0]?.clientX || uiCache.touchLastX || 0, e.touches?.[0]?.clientY || uiCache.touchLastY || 0); target.x = raw.x; target.y = raw.y; } else { target.x = clamp(p.x, CONFIG.worldPadding + pad * 0.15, world.w - CONFIG.worldPadding - pad * 0.15); target.y = clamp(p.y, CONFIG.worldPadding + pad * 0.15, world.h - CONFIG.worldPadding - pad * 0.15); } target.vx = clamp(dx * 16, -160, 160); target.vy = clamp(dy * 16, -160, 160); if (uiCache.grabKind === "tarinai") { target.goIdle?.("つままれている"); target.pinchThoughtTimer = Math.max(target.pinchThoughtTimer || 0, 0.8); } else if (target.type === "ball") { target.prevX = target.x - dx; target.prevY = target.y - dy; target.spinVelocity = clamp((target.spinVelocity || 0) + dx * 0.06, -38, 38); } world.drawListDirty = true; const now = performance.now(); if (!uiCache.grabLastSpatialAt || now - uiCache.grabLastSpatialAt > 80) { world.rebuildSpatial?.(); uiCache.grabLastSpatialAt = now; } render(); return; } if (uiCache.touchMode === "panOrTap") { if (totalMove > 7) { uiCache.touchMoved = true; world.panCamera?.(-screenDx, -screenDy); render(); } } }, { passive: false }); canvas?.addEventListener("touchend", (e) => { e.preventDefault(); if (uiCache.hosing) { uiCache.hosing = false; world.rebuildSpatial?.(); renderStats(); } if (uiCache.grabbing) { const target = uiCache.grabTarget; if (target) { if (uiCache.grabKind === "tarinai") { target.target = null; target.targetKey = ""; target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.15); finishTarinaiGrab(target, e.changedTouches?.[0]?.clientX || uiCache.touchLastX || 0, e.changedTouches?.[0]?.clientY || uiCache.touchLastY || 0); } else if (target.type === "ball") { target.prevX = target.x; target.prevY = target.y; } } audio.drop?.(); uiCache.grabbing = false; uiCache.grabTarget = null; uiCache.grabKind = ""; world.rebuildSpatial?.(); uiCache.grabLastSpatialAt = 0; renderStats(); } else if (uiCache.touchMode === "toolTapOnly" && !uiCache.touchMoved) { const p = screenToWorldClient(uiCache.touchLastX || uiCache.touchStartX, uiCache.touchLastY || uiCache.touchStartY); if (p.inside) { world.handleClick(p.x, p.y); renderStats(); } } else if (uiCache.touchMode === "panOrTap" && !uiCache.touchMoved) { const p = screenToWorldClient(uiCache.touchLastX || uiCache.touchStartX, uiCache.touchLastY || uiCache.touchStartY); if (p.inside) { world.handleClick(p.x, p.y); renderStats(); } } if (!e.touches?.length) { uiCache.touchMode = ""; uiCache.touchMoved = false; } }, { passive: false }); canvas?.addEventListener("touchcancel", () => { uiCache.touchMode = ""; uiCache.touchMoved = false; uiCache.hosing = false; uiCache.grabbing = false; uiCache.grabTarget = null; uiCache.grabKind = ""; }, { passive: true }); canvas?.addEventListener("contextmenu", (e) => e.preventDefault()); canvas?.addEventListener("mousedown", (e) => { if (e.button === 0 && world.tool === "water_hose") { audio.waterHose?.(); const p = screenToWorld(e); if (!p.inside) return; e.preventDefault(); uiCache.hosing = true; uiCache.hoseMoved = false; uiCache.hoseLastWorldX = p.x; uiCache.hoseLastWorldY = p.y; uiCache.hoseStartX = e.clientX; uiCache.hoseStartY = e.clientY; world.applyWaterHose?.(p.x, p.y, 0, 0, 0.10); render(); return; } if (e.button === 0 && world.tool === "pinch") { const p = screenToWorld(e); if (!p.inside) return; const target = world.findGrabTargetAt ? world.findGrabTargetAt(p.x, p.y) : null; if (!target) { showToast("\u3064\u307e\u3081\u308b\u5bfe\u8c61\u304c\u3042\u308a\u307e\u305b\u3093\u3002"); return; } e.preventDefault(); audio.grab?.(); uiCache.grabbing = true; uiCache.grabMoved = false; uiCache.grabTarget = target; uiCache.grabKind = target instanceof Tarinai ? "tarinai" : "item"; rememberGrabOrigin(target); uiCache.grabLastWorldX = p.x; uiCache.grabLastWorldY = p.y; uiCache.grabStartX = e.clientX; uiCache.grabStartY = e.clientY; if (uiCache.grabKind === "tarinai") { target.sleeping = false; if (target.state === "sleep" || target.state === "seek_bed") target.goIdle?.("つままれている"); target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.18); target.thought = "つままれている"; target.pinchThoughtTimer = Math.max(target.pinchThoughtTimer || 0, 1.2); } else { if (isPinType(target.type) && target.pinState === "lodged" && target.detachPushpin) target.detachPushpin(world, "pinch"); target.dropTimer = 0; target.dropMax = 0; target.dropImpactDone = true; } canvas.style.cursor = "grabbing"; renderStats(); return; } if (e.button !== 2) return; e.preventDefault(); uiCache.panning = true; uiCache.panMoved = false; uiCache.panButton = e.button; uiCache.panStartX = e.clientX; uiCache.panStartY = e.clientY; uiCache.panLastX = e.clientX; uiCache.panLastY = e.clientY; }); window.addEventListener("mouseup", (e) => { if (uiCache.hosing) { uiCache.hosing = false; canvas.style.cursor = world.tool === "water_hose" ? "crosshair" : (world.tool === "observe" ? "default" : (world.tool === "pinch" ? "grab" : (world.tool === "poke" || world.tool === "delete" ? "pointer" : "crosshair"))); world.rebuildSpatial?.(); renderStats(); return; } if (uiCache.grabbing) { const target = uiCache.grabTarget; if (target) { if (uiCache.grabKind === "tarinai") { target.target = null; target.targetKey = ""; target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.15); finishTarinaiGrab(target, e.clientX, e.clientY); } else if (target.type === "ball") { target.prevX = target.x; target.prevY = target.y; } } audio.drop?.(); uiCache.grabbing = false; uiCache.grabTarget = null; uiCache.grabKind = ""; world.rebuildSpatial?.(); uiCache.grabLastSpatialAt = 0; canvas.style.cursor = world.tool === "pinch" ? "grab" : (world.tool === "observe" ? "default" : (world.tool === "poke" || world.tool === "delete" ? "pointer" : "crosshair")); renderStats(); return; } if (!uiCache.panning) return; if (uiCache.panButton !== null && e.button !== uiCache.panButton) return; uiCache.panning = false; uiCache.panButton = null; canvas.style.cursor = world.tool === "observe" ? "default" : (world.tool === "pinch" ? "grab" : (world.tool === "poke" || world.tool === "delete" ? "pointer" : "crosshair")); }); window.addEventListener("mousemove", (e) => { if (uiCache.hosing) { const p = screenToWorld(e); const dx = p.x - (uiCache.hoseLastWorldX ?? p.x); const dy = p.y - (uiCache.hoseLastWorldY ?? p.y); uiCache.hoseLastWorldX = p.x; uiCache.hoseLastWorldY = p.y; const totalMove = Math.hypot(e.clientX - (uiCache.hoseStartX || e.clientX), e.clientY - (uiCache.hoseStartY || e.clientY)); if (totalMove > 3) uiCache.hoseMoved = true; if (p.inside) world.applyWaterHose?.(p.x, p.y, dx, dy, 0.10); canvas.style.cursor = "crosshair"; render(); return; } if (uiCache.grabbing) { const target = uiCache.grabTarget; if (!target || target.dead) { uiCache.grabbing = false; uiCache.grabTarget = null; canvas.style.cursor = world.tool === "pinch" ? "grab" : "default"; return; } const p = screenToWorld(e); const dx = p.x - (uiCache.grabLastWorldX ?? p.x); const dy = p.y - (uiCache.grabLastWorldY ?? p.y); uiCache.grabLastWorldX = p.x; uiCache.grabLastWorldY = p.y; const totalMove = Math.hypot(e.clientX - (uiCache.grabStartX || e.clientX), e.clientY - (uiCache.grabStartY || e.clientY)); if (totalMove > 3) uiCache.grabMoved = true; const pad = target.radius || target.r || 16; if (uiCache.grabKind === "tarinai" && !p.inside) { const raw = clientToWorldRaw(e.clientX, e.clientY); target.x = raw.x; target.y = raw.y; } else { target.x = clamp(p.x, CONFIG.worldPadding + pad * 0.15, world.w - CONFIG.worldPadding - pad * 0.15); target.y = clamp(p.y, CONFIG.worldPadding + pad * 0.15, world.h - CONFIG.worldPadding - pad * 0.15); } target.vx = clamp(dx * 16, -160, 160); target.vy = clamp(dy * 16, -160, 160); if (uiCache.grabKind === "tarinai") { target.goIdle?.("つままれている"); } else if (target.type === "ball") { target.prevX = target.x - dx; target.prevY = target.y - dy; target.spinVelocity = clamp((target.spinVelocity || 0) + dx * 0.06, -38, 38); } world.drawListDirty = true; const now = performance.now(); if (!uiCache.grabLastSpatialAt || now - uiCache.grabLastSpatialAt > 80) { world.rebuildSpatial?.(); uiCache.grabLastSpatialAt = now; } canvas.style.cursor = "grabbing"; return; } if (!uiCache.panning) return; const dx = e.clientX - uiCache.panLastX; const dy = e.clientY - uiCache.panLastY; uiCache.panLastX = e.clientX; uiCache.panLastY = e.clientY; const totalMove = Math.hypot(e.clientX - uiCache.panStartX, e.clientY - uiCache.panStartY); if (totalMove > 8) { canvas.style.cursor = "grabbing"; world.panCamera?.(-dx, -dy); uiCache.panMoved = true; render(); } }); canvas?.addEventListener("click", (e) => { if (uiCache.hoseMoved) { uiCache.hoseMoved = false; return; } if (uiCache.grabMoved) { uiCache.grabMoved = false; return; } if (uiCache.panMoved) { uiCache.panMoved = false; return; } const p = screenToWorld(e); if (!p.inside) return; world.handleClick(p.x, p.y); renderStats(); }); canvas?.addEventListener("mousemove", (e) => { const p = screenToWorld(e); const prevX = world.pointer.x; const prevY = world.pointer.y; world.pointer.x = p.x; world.pointer.y = p.y; world.pointer.inside = !!p.inside; const delta = Math.hypot((p.x || 0) - (prevX || 0), (p.y || 0) - (prevY || 0)); world.pointer.motion = clamp(delta / 18, 0, 2); world.pointer.movedAt = performance.now(); }); canvas?.addEventListener("wheel", resizeFieldFromWheel, { passive: false }); canvas?.addEventListener("mouseleave", () => { world.pointer.inside = false; world.pointer.motion = 0; }); window.addEventListener("keydown", (e) => { if (e.key !== "Escape") return; closeFieldDialog(); closeEcologyDialog(); closeArchiveResetDialog(); closeCreditsDialog(); }); window.addEventListener("resize", resizeCanvas); }