// 聊天室钓鱼小游戏模块,管理抛竿、收竿、自动钓鱼循环和浮漂交互。 import { escapeHtml } from "./html.js"; let fishingEventsBound = false; let fishingTimer = null; let fishingReelTimeout = null; let fishToken = null; let autoFishing = false; let autoFishCooldownTimer = null; let autoFishCooldownCountdown = null; /** * 读取 CSRF Token。 * * @returns {string} */ function csrf() { return document.querySelector('meta[name="csrf-token"]')?.content || ""; } /** * 获取私聊消息容器,钓鱼提示沿用旧版展示位置。 * * @returns {HTMLElement|null} */ function messageContainer() { return document.getElementById("chat-messages-container2"); } /** * 判断当前是否允许自动滚动。 * * @returns {boolean} */ function shouldAutoScroll() { return typeof window.isChatAutoScrollEnabled === "function" ? window.isChatAutoScrollEnabled() : true; } /** * 追加一条钓鱼提示消息。 * * @param {string} html * @returns {void} */ function appendFishingMessage(html) { const container = messageContainer(); if (!container) { return; } const line = document.createElement("div"); line.className = "msg-line"; line.innerHTML = html; container.appendChild(line); if (shouldAutoScroll()) { container.scrollTop = container.scrollHeight; } } /** * 当前本地时间文本。 * * @returns {string} */ function timeText() { return new Date().toLocaleTimeString("zh-CN", { hour12: false }); } /** * 注入浮漂和自动钓鱼按钮需要的动画样式。 * * @returns {void} */ function ensureFishingStyles() { if (!document.getElementById("bobber-style")) { const style = document.createElement("style"); style.id = "bobber-style"; style.textContent = ` @keyframes bobberFloat { 0%,100% { transform: translateY(0) rotate(-8deg); } 50% { transform: translateY(-10px) rotate(8deg); } } @keyframes bobberSink { 0% { transform: translateY(0) scale(1); opacity:1; } 30% { transform: translateY(12px) scale(1.3); opacity:1; } 100% { transform: translateY(40px) scale(0.5); opacity:0; } } #fishing-bobber.sinking { animation: bobberSink 1.5s forwards !important; } `; document.head.appendChild(style); } if (!document.getElementById("auto-fish-stop-style")) { const style = document.createElement("style"); style.id = "auto-fish-stop-style"; style.textContent = ` @keyframes autoFishBtnPulse { 0%,100% { box-shadow: 0 4px 12px rgba(220,38,38,0.4); } 50% { box-shadow: 0 4px 20px rgba(220,38,38,0.7); } } #auto-fish-stop-btn { position: fixed; z-index: 10000; background: linear-gradient(135deg, #dc2626, #b91c1c); color: #fff; border: none; border-radius: 20px; padding: 8px 18px; font-size: 13px; font-weight: bold; cursor: grab; user-select: none; animation: autoFishBtnPulse 1.8s ease-in-out infinite; touch-action: none; } #auto-fish-stop-btn:active { cursor: grabbing; } #auto-fish-stop-btn .drag-hint { display: block; font-size: 9px; font-weight: normal; opacity: .65; margin-top: 1px; text-align: center; letter-spacing: .5px; } `; document.head.appendChild(style); } } /** * 创建浮漂 DOM 元素。 * * @param {number} x 水平百分比 * @param {number} y 垂直百分比 * @returns {HTMLElement} */ export function createBobber(x, y) { ensureFishingStyles(); const bobber = document.createElement("div"); bobber.id = "fishing-bobber"; bobber.style.cssText = ` position: fixed; left: ${Number(x) || 50}vw; top: ${Number(y) || 50}vh; font-size: 28px; cursor: pointer; z-index: 9999; animation: bobberFloat 1.2s ease-in-out infinite; filter: drop-shadow(0 2px 6px rgba(0,0,0,0.4)); user-select: none; transition: transform 0.3s; `; bobber.textContent = "🪝"; bobber.title = "鱼上钩了!快点击!"; return bobber; } /** * 移除当前浮漂。 * * @returns {void} */ export function removeBobber() { document.getElementById("fishing-bobber")?.remove(); } /** * 获取钓鱼按钮。 * * @returns {HTMLButtonElement|null} */ function fishingButton() { const button = document.getElementById("fishing-btn"); return button instanceof HTMLButtonElement ? button : null; } /** * 设置钓鱼按钮文案和禁用状态。 * * @param {string} text * @param {boolean} disabled * @returns {void} */ function setFishingButton(text, disabled) { const button = fishingButton(); if (!button) { return; } button.textContent = text; button.disabled = disabled; } /** * 启动自动钓鱼冷却倒计时(基于时间戳,不受浏览器后台节流影响)。 * * @param {number} cooldown 冷却秒数 * @returns {void} */ function startAutoFishingCooldown(cooldown) { const endTime = Date.now() + cooldown * 1000; setFishingButton(`⏳ 冷却 ${cooldown}s`, true); showAutoFishStopButton(cooldown); // 基于时间戳更新倒计时 UI — 后台节流后回来也能准确显示 autoFishCooldownCountdown = window.setInterval(() => { const remaining = Math.max(0, Math.ceil((endTime - Date.now()) / 1000)); setFishingButton(`⏳ 冷却 ${remaining}s`, true); if (remaining <= 0) { window.clearInterval(autoFishCooldownCountdown); autoFishCooldownCountdown = null; } }, 200); // 基于时间戳检测冷却结束 — 后台节流后立即触发 autoFishCooldownTimer = null; const checkEnd = () => { if (Date.now() >= endTime) { autoFishCooldownTimer = null; hideAutoFishStopButton(); if (autoFishing) { void startFishing(); } return; } autoFishCooldownTimer = window.setTimeout(checkEnd, 200); }; autoFishCooldownTimer = window.setTimeout(checkEnd, Math.min(cooldown * 1000, 200)); } /** * 展示可拖动的停止自动钓鱼按钮。 * * @param {number} cooldown * @returns {void} */ function showAutoFishStopButton(cooldown) { if (document.getElementById("auto-fish-stop-btn")) { return; } ensureFishingStyles(); const button = document.createElement("button"); button.id = "auto-fish-stop-btn"; button.innerHTML = `🛑 停止自动钓鱼冷却 ${Number(cooldown) || 0}s · 可拖动`; try { const saved = JSON.parse(window.localStorage.getItem("autoFishBtnPos") || "null"); if (saved) { button.style.left = `${Number(saved.left) || 0}px`; button.style.top = `${Number(saved.top) || 0}px`; } else { button.style.bottom = "80px"; button.style.right = "20px"; } } catch (error) { button.style.bottom = "80px"; button.style.right = "20px"; } bindAutoFishStopDrag(button); document.body.appendChild(button); } /** * 给停止自动钓鱼按钮绑定拖拽和点击停止事件。 * * @param {HTMLButtonElement} button * @returns {void} */ function bindAutoFishStopDrag(button) { let isDragging = false; let startX = 0; let startY = 0; let startLeft = 0; let startTop = 0; const dragStart = (event) => { const rect = button.getBoundingClientRect(); button.style.left = `${rect.left}px`; button.style.top = `${rect.top}px`; button.style.right = "auto"; button.style.bottom = "auto"; isDragging = false; const point = event.touches ? event.touches[0] : event; startX = point.clientX; startY = point.clientY; startLeft = rect.left; startTop = rect.top; document.addEventListener("mousemove", dragMove, { passive: false }); document.addEventListener("mouseup", dragEnd); document.addEventListener("touchmove", dragMove, { passive: false }); document.addEventListener("touchend", dragEnd); }; const dragMove = (event) => { event.preventDefault(); const point = event.touches ? event.touches[0] : event; const dx = point.clientX - startX; const dy = point.clientY - startY; if (Math.abs(dx) > 3 || Math.abs(dy) > 3) { isDragging = true; } if (!isDragging) { return; } const nextLeft = Math.max(0, Math.min(window.innerWidth - button.offsetWidth, startLeft + dx)); const nextTop = Math.max(0, Math.min(window.innerHeight - button.offsetHeight, startTop + dy)); button.style.left = `${nextLeft}px`; button.style.top = `${nextTop}px`; }; const dragEnd = () => { document.removeEventListener("mousemove", dragMove); document.removeEventListener("mouseup", dragEnd); document.removeEventListener("touchmove", dragMove); document.removeEventListener("touchend", dragEnd); if (isDragging) { window.localStorage.setItem("autoFishBtnPos", JSON.stringify({ left: Number.parseInt(button.style.left, 10), top: Number.parseInt(button.style.top, 10), })); } }; button.addEventListener("mousedown", dragStart); button.addEventListener("touchstart", dragStart, { passive: true }); button.addEventListener("click", () => { if (!isDragging) { stopAutoFishing(); } }); } /** * 隐藏停止自动钓鱼按钮。 * * @returns {void} */ function hideAutoFishStopButton() { document.getElementById("auto-fish-stop-btn")?.remove(); } /** * 开始钓鱼:调用抛竿接口并显示浮漂。 * * @returns {Promise} */ export async function startFishing() { setFishingButton("🎣 抛竿中...", true); try { const response = await fetch(window.chatContext.fishCastUrl, { method: "POST", headers: { "X-CSRF-TOKEN": csrf(), "Accept": "application/json", }, }); const data = await response.json(); if (!response.ok || data.status !== "success") { window.chatDialog?.alert?.(data.message || "钓鱼失败", "操作失败", "#cc4444"); setFishingButton("🎣 钓鱼", false); return; } fishToken = data.token; autoFishing = Boolean(data.auto_fishing); appendFishingMessage(`🎣【钓鱼】${escapeHtml(data.message)}(${timeText()})`); setFishingButton("🎣 等待中...", true); const bobber = createBobber(data.bobber_x, data.bobber_y); document.body.appendChild(bobber); fishingTimer = window.setTimeout(() => { bobber.classList.add("sinking"); bobber.textContent = "🐟"; if (data.auto_fishing) { appendFishingMessage(`🎣 自动钓鱼卡生效!自动收竿中... (剩余${Number(data.auto_fishing_minutes_left) || 0}分钟)`); fishingReelTimeout = window.setTimeout(() => { removeBobber(); void reelFish(); }, 1800); return; } appendFishingMessage('🐟 鱼上钩了!快点击屏幕上的浮漂!'); setFishingButton("🎣 点击浮漂!", true); bobber.addEventListener("click", () => { removeBobber(); if (fishingReelTimeout) { window.clearTimeout(fishingReelTimeout); fishingReelTimeout = null; } void reelFish(); }, { once: true }); fishingReelTimeout = window.setTimeout(() => { removeBobber(); fishToken = null; appendFishingMessage('💨 你反应太慢了,鱼跑掉了...'); resetFishingBtn(); }, 8000); }, Number(data.wait_time || 0) * 1000); } catch (error) { window.chatDialog?.alert?.(`网络错误:${error.message}`, "网络异常", "#cc4444"); removeBobber(); setFishingButton("🎣 钓鱼", false); } } /** * 收竿并提交本次钓鱼 token。 * * @returns {Promise} */ export async function reelFish() { setFishingButton("🎣 拉竿中...", true); if (fishingReelTimeout) { window.clearTimeout(fishingReelTimeout); fishingReelTimeout = null; } const token = fishToken; fishToken = null; try { const response = await fetch(window.chatContext.fishReelUrl, { method: "POST", headers: { "X-CSRF-TOKEN": csrf(), "Accept": "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ token }), }); const data = await response.json(); if (response.ok && data.status === "success") { const result = data.result || {}; const color = Number(result.exp || 0) >= 0 ? "#16a34a" : "#dc2626"; appendFishingMessage( `${escapeHtml(result.emoji || "🎣")}【钓鱼结果】${escapeHtml(result.message || "")}` + ` (经验:${Number(data.exp_num) || 0} 金币:${Number(data.jjb) || 0})` + `(${timeText()})`, ); if (autoFishing) { startAutoFishingCooldown(Number(data.cooldown_seconds) || 300); return; } } else { appendFishingMessage(`【钓鱼】${escapeHtml(data.message || "操作失败")}(${timeText()})`); if (autoFishing) { retryAutoFishing(); return; } autoFishing = false; } } catch (error) { if (autoFishing) { appendFishingMessage('⚠️ 网络异常,5秒后自动重试钓鱼...'); retryAutoFishing(); return; } window.chatDialog?.alert?.(`网络错误:${error.message}`, "网络异常", "#cc4444"); autoFishing = false; } resetFishingBtn(); } /** * 自动钓鱼异常时短暂等待后重试。 * * @returns {void} */ function retryAutoFishing() { setFishingButton("⏳ 重试中...", true); autoFishCooldownTimer = window.setTimeout(() => { autoFishCooldownTimer = null; if (autoFishing) { void startFishing(); } }, 5000); } /** * 手动停止自动钓鱼循环。 * * @returns {void} */ export function stopAutoFishing() { autoFishing = false; clearAutoFishingTimers(); hideAutoFishStopButton(); appendFishingMessage('🛑 已停止自动钓鱼。'); resetFishingBtn(); } /** * 清理自动钓鱼冷却计时器。 * * @returns {void} */ function clearAutoFishingTimers() { if (autoFishCooldownTimer) { window.clearTimeout(autoFishCooldownTimer); autoFishCooldownTimer = null; } if (autoFishCooldownCountdown) { window.clearInterval(autoFishCooldownCountdown); autoFishCooldownCountdown = null; } } /** * 重置钓鱼按钮和临时状态。 * * @returns {void} */ export function resetFishingBtn() { autoFishing = false; clearAutoFishingTimers(); hideAutoFishStopButton(); if (fishingTimer) { window.clearTimeout(fishingTimer); fishingTimer = null; } if (fishingReelTimeout) { window.clearTimeout(fishingReelTimeout); fishingReelTimeout = null; } setFishingButton("🎣 钓鱼", false); removeBobber(); } /** * 检查自动钓鱼卡状态并恢复自动循环。 * * @returns {void} */ export function checkAndAutoStartFishing() { const minutesLeft = Number(window.chatContext?.autoFishingMinutesLeft || 0); const initialCooldown = Number(window.chatContext?.fishingCooldownSeconds || 0); if (minutesLeft <= 0 || autoFishing) { return; } autoFishing = true; if (initialCooldown > 0) { console.log(`检测到自动钓鱼卡有效,恢复钓鱼状态,剩余冷却 ${initialCooldown}s`); startAutoFishingCooldown(initialCooldown); return; } console.log("检测到自动钓鱼卡有效,自动抛竿"); void startFishing(); } /** * 绑定钓鱼按钮、全局兼容入口和清屏恢复事件。 * * @returns {void} */ export function bindFishingControls() { if (typeof window === "undefined") { return; } window.createBobber = createBobber; window.removeBobber = removeBobber; window.startFishing = startFishing; window.reelFish = reelFish; window.stopAutoFishing = stopAutoFishing; window.resetFishingBtn = resetFishingBtn; window.checkAndAutoStartFishing = checkAndAutoStartFishing; if (fishingEventsBound || typeof document === "undefined") { return; } fishingEventsBound = true; document.addEventListener("click", (event) => { if (!(event.target instanceof Element) || !event.target.closest("[data-chat-fishing-start]")) { return; } event.preventDefault(); void startFishing(); }); document.addEventListener("DOMContentLoaded", () => { checkAndAutoStartFishing(); window.addEventListener("chat:screen-cleared", () => { if (!autoFishing) { checkAndAutoStartFishing(); } }); }); }