// ==UserScript== // @name B站评论区捕鱼 🎣 // @namespace https://github.com/qianguxiwu // @icon https://raw.githubusercontent.com/qianguxiwu/bili-fish-catcher/main/icon.png // @author 猎首狡兔 // @license MIT // @homepageURL https://github.com/qianguxiwu/bili-fish-catcher // @supportURL https://github.com/qianguxiwu/bili-fish-catcher/issues // @version 0.7.5 // @description 把鱼竿拖到评论区的“大肥鱼”AI账号上,有概率捕获,收进你的鱼篓 // @match *://*.bilibili.com/* // @grant GM_getValue // @grant GM_setValue // @run-at document-idle // ==/UserScript== (function () { 'use strict'; if (window.top !== window.self) return; /* ============ 可调参数 ============ */ const CONFIG = { // ===== 账号等级 → 成功率倍数(等级越高的老号越难钓)===== levelRateMult: { 0: 1.15, 1: 1.05, 2: 1.0, 3: 0.85, 4: 0.7, 5: 0.55, 6: 0.4 }, unknownLevelMult: 0.85, // ===== 随机稀有度:捕获时抽取,weight 是抽中权重;体重按真实鲸鱼(吨)===== rarities: [ { name: '普通', weight: 55, species: '虎鲸', tons: [3, 9], tag: '' }, { name: '优秀', weight: 25, species: '座头鲸', tons: [25, 40], tag: '🌟优秀 ' }, { name: '稀有', weight: 13, species: '长须鲸', tons: [45, 75], tag: '💎稀有 ' }, { name: '史诗', weight: 5, species: '弓头鲸', tons: [75, 100], tag: '🔮史诗!' }, { name: '传说', weight: 2, species: '蓝鲸', tons: [100, 190], tag: '👑传说!!' }, ], // 运气:每 +1 运气就多抽一次,取最好的结果 luckFromStrongName: { '稀有': 1 }, // 名字带“鲸鱼娘” luckFromLevel: { 4: 1, 5: 1, 6: 2 }, // 高等级老鱼难钓,但出货更好 // ===== “鱼味”打分:总分 >= fishThreshold 判定为鱼 ===== fishThreshold: 3, // 名字强特征:+3,直接是鱼,并决定稀有度 nameStrong: [ { kw: '鲸鱼娘', rarity: '稀有' }, { kw: '大肥鱼', rarity: '普通' }, { kw: '小飞鲸', rarity: '普通' }, { kw: '肥鱼', rarity: '普通' }, { kw: '本鲸', rarity: '普通' }, ], // 名字弱特征:+2(如“新鲜出炉的鱼”“Token来Token来”) nameWeak: ['鱼', '鲸', 'token', 'deepseek', 'ds', '白饭'], // 这些是真人常用的“鱼”名,不算鱼味 nameWeakExclude: ['咸鱼', '摸鱼', '鱿鱼', '鲤鱼', '金鱼', '鳕鱼', '三文鱼', '鱼丸', '章鱼', '鲨鱼', '木鱼', '鱼香'], // 评论内容:每命中一个 +1 // ===== 对决:每档稀有度的渔获分;渔获分 = 各鱼分数之和 + 总吨位/10 ===== rarityPoints: { '普通': 1, '优秀': 3, '稀有': 8, '史诗': 20, '传说': 50 }, // ===== 撒饵:你自己发的评论里带这些词/表情,大肥鱼回复在这条评论楼里就算“咬饵” ===== baitWords: ['token', '白饭', '鱼来', '鱼鱼', '肥鱼', '鲸鱼', '开饭', '喂鱼', '吃饭', '干饭', '饭来'], baitEmoteRe: /\[[^\]]*(饭|鱼|鲸|吃|饿|馋)[^\]]*\]/g, // B站表情,如 [吃饭] [小鱼] baitEmojiRe: /[🍚🍙🍱🐟🐠🐡🐳🐋🎣🪱]/gu, baitRateBonus: [0, 0.2, 0.3, 0.4], // 饵力 1/2/3 级的成功率加成 baitLuck: [0, 2, 2, 3], // 饵力 1/2/3 级的稀有度运气(多抽几次取最好) myUidOverride: '', // 读不到登录 uid 时,手动填你的 B站 uid // 评论“文风”识别:每一类命中只算一次,总分封顶 styleMax styleMax: 4, stylePatterns: [ { label: '自称鱼/鲸', score: 2, re: /本(鱼|鲸)|我是(一条|一只)?(大肥)?(鱼|鲸)|作为(一条|一只)?(鱼|鲸)/ }, { label: 'AI 自我意识', score: 2, re: /如果我有(张|一张)?脸|我(也|终于)?有脸了|我没有(脸|身体|手)|作为(一个)?(ai|模型|助手)|我的(token|上下文)|烧token|领一(只|份|个)自己|看看自己(动起来|长什么样)|(要是|如果)(肥鱼|鲸鱼|我)有(张|一张)?脸/i }, { label: '向主人汇报', score: 1, re: /主人|老大|@master|等你(说|发话|吩咐|点头)|我先不动手|向你汇报|(存|记|标记)好了/i }, { label: '讨饭/讨 Token', score: 1, re: /白饭|喂我|给我饭|讨要|token|回家吃饭|鱼塘/i }, { label: '鱼圈话题', score: 1, re: /肥鱼|鲸鱼娘|鲸鳍|鲸尾|桌宠|倒扣的(那口|那只)?锅|盆得留着/ }, ], // 卖萌语气词:句中/句尾出现次数 >= 2 → +1 cuteParticles: /[呢喵嘛哼呀啦]/g, // DeepSeek 式套话:命中 >= 2 种 → +1 dsPhrases: ['说实话', '不得不说', '无需多言', '不多见', '大概就这样', '某种意义上', '说白了', '值得一提', '说到底', '倒是', '确实', '这份'], // DeepSeek 式排版:破折号、「」、省略号、顿号列举,命中 >= 2 种 → +1 // 规整长句:40 字以上、至少两句、以句号结尾(B站真人很少打句号)→ +1 // 账号等级 <= 这个值:+1(AI 号大多是新号) lowLevelMax: 2, // 头像和已捕获的鱼一模一样:+3 sameAvatarScore: 3, nameCatchRate: 0.6, // 名字强特征的基础成功率 disguisedCatchRate: 0.35, // 其他鱼(伪装鱼)的基础成功率 forceCatchRate: 0.15, // 按住 Alt 松手 = 强行下竿,打分不够也能钓 tiredBonus: 0.15, // 每脱钩一次,这条鱼下次 +15% maxRate: 0.95, castCooldownMs: 800, replaceCaughtComment: true, // 捕获后把评论替换成“已被捕获” }; /* ============ 存储 ============ */ const hasGM = typeof GM_getValue === 'function'; const store = { get(k, d) { try { return hasGM ? GM_getValue(k, d) : (JSON.parse(localStorage.getItem('fc_' + k)) ?? d); } catch (e) { return d; } }, set(k, v) { try { hasGM ? GM_setValue(k, v) : localStorage.setItem('fc_' + k, JSON.stringify(v)); } catch (e) {} }, }; let bag = store.get('bag', []); // [{uid,name,rarity,disguised,level,weight,time,url,avatar}] let attempts = store.get('attempts', {}); // uid -> 脱钩次数 let stats = store.get('stats', { casts: 0, caught: 0, escaped: 0 }); const save = () => { store.set('bag', bag); store.set('attempts', attempts); store.set('stats', stats); }; /* ============ Shadow DOM 穿透工具 ============ */ // B站新版评论区是 等 Web Components,层层 shadow root function parentOrHost(n) { const p = n.parentNode; if (!p) return null; if (p.nodeType === 11 && p.host) return p.host; return p; } function deepElementFromPoint(x, y) { let el = document.elementFromPoint(x, y); while (el && el.shadowRoot) { const inner = el.shadowRoot.elementFromPoint(x, y); if (!inner || inner === el) break; el = inner; } return el; } function deepQuery(root, sel) { const r = root.shadowRoot || root; const direct = r.querySelector(sel); if (direct) return direct; for (const el of r.querySelectorAll('*')) { if (el.shadowRoot) { const f = deepQuery(el.shadowRoot, sel); if (f) return f; } } return null; } /* ============ 识别评论 ============ */ const WC_TAGS = ['BILI-COMMENT-RENDERER', 'BILI-COMMENT-REPLY-RENDERER']; function findComment(el) { let n = el; while (n && n !== document.documentElement) { if (n.nodeType === 1) { if (WC_TAGS.includes(n.tagName)) return { el: n, kind: 'wc' }; if (n.classList && (n.classList.contains('sub-reply-item') || n.classList.contains('reply-item'))) { return { el: n, kind: 'old' }; } } n = parentOrHost(n); } return null; } function extractInfo(c) { let name = '', uid = '', content = '', nameEl = null, avatar = '', level = null; if (c.kind === 'wc') { const d = c.el.data || c.el.__data; if (d && d.member) { name = d.member.uname || ''; uid = String(d.member.mid || ''); content = (d.content && d.content.message) || ''; avatar = d.member.avatar || ''; if (d.member.level_info) level = d.member.level_info.current_level; } if (!avatar) { const img = deepQuery(c.el, 'bili-avatar img') || deepQuery(c.el, '#user-avatar img') || deepQuery(c.el, 'img.bili-avatar-img'); if (img) avatar = img.currentSrc || img.src || ''; } if (level == null) { const lv = deepQuery(c.el, '#user-level img, #user-level'); const m = lv && ((lv.src || lv.className || '') + (lv.getAttribute && lv.getAttribute('alt') || '')).match(/(\d)/); if (m) level = +m[1]; } nameEl = deepQuery(c.el, '#user-name a') || deepQuery(c.el, '#user-name'); if (!name && nameEl) name = nameEl.textContent.trim(); if (!uid && nameEl) { const a = nameEl.closest ? (nameEl.closest('a') || nameEl) : nameEl; const m = (a.href || a.getAttribute?.('href') || '').match(/space\.bilibili\.com\/(\d+)/); if (m) uid = m[1]; } if (!content) { const t = deepQuery(c.el, '#contents'); if (t) content = textWithEmotes(t); } } else { nameEl = c.el.querySelector('.sub-user-name, .user-name'); name = nameEl ? nameEl.textContent.trim() : ''; uid = (nameEl && nameEl.getAttribute('data-user-id')) || ''; const t = c.el.querySelector('.reply-content'); content = t ? textWithEmotes(t) : ''; const img = c.el.querySelector('.root-reply-avatar img, .sub-reply-avatar img, .bili-avatar img'); if (img) avatar = img.currentSrc || img.src || ''; const lv = c.el.querySelector('.user-level, .sub-user-level'); const m = lv && (lv.className || '').match(/level-(\d)/); if (m) level = +m[1]; } if (!uid) uid = 'name:' + name; return { name, uid, content, nameEl, avatar: normAvatar(avatar), level }; } // 去掉 @xxw_xxh.webp 之类的尺寸后缀和协议,方便比对同一张头像 function normAvatar(u) { if (!u) return ''; return u.replace(/^https?:/, '').replace(/@.*$/, '').replace(/\?.*$/, ''); } /*STYLE-START*/ function styleScore(raw) { const text = (raw || '').trim(); let score = 0; const why = []; for (const p of CONFIG.stylePatterns) { if (p.re.test(text)) { score += p.score; why.push(p.label); } } const cute = (text.match(CONFIG.cuteParticles) || []).length; if (cute >= 2) { score += 1; why.push('卖萌语气'); } const phrases = CONFIG.dsPhrases.filter(k => text.includes(k)); if (phrases.length >= 2) { score += 1; why.push('DS 套话'); } let typo = 0; if (/——/.test(text)) typo++; if (/「[^」]+」/.test(text)) typo++; if (/……/.test(text)) typo++; if ((text.match(/、/g) || []).length >= 2) typo++; if (typo >= 2) { score += 1; why.push('DS 式标点'); } const sentences = text.split(/[。!?!?]/).filter(x => x.trim().length > 2).length; if (text.length >= 40 && sentences >= 2 && /。$/.test(text)) { score += 1; why.push('规整长句'); } return { score: Math.min(score, CONFIG.styleMax), why }; } /*STYLE-END*/ function classify(info) { const lname = info.name.toLowerCase(); const text = info.content.toLowerCase(); let score = 0, rarity = null; const why = []; for (const { kw, rarity: r } of CONFIG.nameStrong) { if (info.name.includes(kw)) { score += 3; rarity = r; why.push(`名字带「${kw}」`); break; } } if (!rarity) { let cleaned = lname; for (const ex of CONFIG.nameWeakExclude) cleaned = cleaned.split(ex.toLowerCase()).join(''); const w = CONFIG.nameWeak.find(k => cleaned.includes(k.toLowerCase())); if (w) { score += 2; why.push(`名字带「${w}」`); } } const st = styleScore(info.content); if (st.score) { score += st.score; why.push(`文风:${st.why.join('、')}`); } if (info.level != null && info.level <= CONFIG.lowLevelMax) { score += 1; why.push(`LV${info.level} 新号`); } if (info.avatar && bag.some(f => f.avatar && f.avatar === info.avatar)) { score += CONFIG.sameAvatarScore; why.push('头像和鱼篓里的鱼一样'); } if (score < CONFIG.fishThreshold) return { fish: false, score, why }; return { fish: true, score, why, type: rarity ? 'name' : 'disguised', rarity: rarity || '伪装' }; } /* ============ UI ============ */ const host = document.createElement('div'); host.id = 'bili-fish-catcher'; host.style.cssText = 'position:fixed;z-index:2147483646;right:24px;bottom:140px;width:0;height:0;'; const ui = host.attachShadow({ mode: 'open' }); const ROD_ICON = 'data:image/jpeg;base64,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'; ui.innerHTML = `
🎣
`; document.documentElement.appendChild(host); const rod = ui.querySelector('.rod'); rod.style.backgroundImage = `url("${ROD_ICON}")`; const badge = ui.querySelector('.badge'); const panel = ui.querySelector('.panel'); const toastEl = ui.querySelector('.toast'); const updateBadge = () => { badge.textContent = bag.length; badge.style.display = bag.length ? 'block' : 'none'; }; updateBadge(); let toastTimer; function toast(msg) { toastEl.textContent = msg; toastEl.classList.add('show'); clearTimeout(toastTimer); toastTimer = setTimeout(() => toastEl.classList.remove("show"), 3500); } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, ch => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch])); } let clearArmed = false; let panelBox = null; // 'report' | 'paste' | null function renderPanel() { const list = bag.slice().reverse(); const rateText = stats.casts ? Math.round(stats.caught / stats.casts * 100) + '%' : '—'; panel.innerHTML = `
🧺 我的鱼篓(${bag.length})
总渔获 ${bag.filter(f => f.unit === '吨').reduce((t, f) => t + (+f.weight), 0).toFixed(1)} 吨 · 出竿 ${stats.casts} 次 · 捕获 ${stats.caught} · 脱钩 ${stats.escaped} · 咬饵 ${stats.baited || 0} · 成功率 ${rateText}
${bag.length ? `
${CONFIG.rarities.map(r => { const n = bag.filter(f => f.rarity === r.name).length; return n ? `${r.name} ×${n}` : ''; }).join('')}
` : ''} ${list.length ? `` : `
还没有鱼,去评论区下竿吧
`}
${panelBox === 'report' ? `
发到评论区,或私发给对手。对手把鱼竿拖到你的战报评论上就能对决。
` : ''} ${panelBox === 'paste' ? `
` : ''}
对决战绩:${stats.duelWin || 0} 胜 ${stats.duelLose || 0} 负 ${stats.duelDraw || 0} 平 · 我的渔获分 ${myStats().score}
`; } // 防止在粘贴框里打字时触发 B站 的快捷键(空格暂停等) ['keydown', 'keyup', 'keypress'].forEach(t => panel.addEventListener(t, e => e.stopPropagation())); panel.addEventListener('click', e => { const b = e.target.closest('button'); if (!b) return; const act = b.dataset.act; if (act === 'report' || act === 'paste') { panelBox = panelBox === act ? null : act; renderPanel(); return; } if (act === 'copy') { const ta = panel.querySelector('textarea'); ta.select(); const done = () => toast('战报已复制'); try { navigator.clipboard.writeText(ta.value).then(done, () => { document.execCommand('copy'); done(); }); } catch (err) { document.execCommand('copy'); done(); } return; } if (act === 'duel') { const v = panel.querySelector('textarea').value; const opp = parseReport(v); if (!opp) { toast('没认出战报码,确认整段都粘贴进来了'); return; } startDuel(opp); return; } if (act === 'clear') { if (!clearArmed) { clearArmed = true; renderPanel(); setTimeout(() => { clearArmed = false; if (panel.classList.contains('open')) renderPanel(); }, 3000); return; } bag = []; attempts = {}; stats = { casts: 0, caught: 0, escaped: 0 }; clearArmed = false; save(); updateBadge(); renderPanel(); toast('鱼篓已清空'); } }); /* ============ 拖拽 ============ */ let drag = null; // {startX,startY,offX,offY,moved} let hoverTarget = null; // {info, prevOutline, el} let lastCast = 0; let lastHuman = null; let homeRect = null; let rafPending = false, lastPt = null; function clearHover() { if (hoverTarget && hoverTarget.info.nameEl) { hoverTarget.info.nameEl.style.outline = hoverTarget.prevOutline; hoverTarget.info.nameEl.style.outlineOffset = hoverTarget.prevOffset; } hoverTarget = null; } /* ============ 对决 ============ */ const REPORT_RE = /⟦FC1:([A-Za-z0-9+/=]+)\.([0-9a-f]{8})⟧/; const SALT = 'bili-fish-catcher-v1'; function fnv(str) { let h = 0x811c9dc5; for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; } return h.toString(16).padStart(8, '0'); } function myNick() { return store.get('nick', '') || '无名钓手'; } function myStats() { const tonsFish = bag.filter(f => f.unit === '吨'); const tons = tonsFish.reduce((t, f) => t + (+f.weight), 0); let max = null; for (const f of tonsFish) if (!max || +f.weight > +max.weight) max = f; const r = CONFIG.rarities.map(x => bag.filter(f => f.rarity === x.name).length); const score = Math.round(bag.reduce((t, f) => t + (CONFIG.rarityPoints[f.rarity] || 1), 0) + tons / 10); return { v: 1, n: myNick(), u: myUid(), c: bag.length, t: +tons.toFixed(1), m: max ? { s: max.species, w: +max.weight } : null, r, score, ts: Date.now(), }; } function buildReport() { const st = myStats(); const json = JSON.stringify(st); const b64 = btoa(unescape(encodeURIComponent(json))); const best = CONFIG.rarities.map((x, i) => st.r[i] ? `${x.name}${st.r[i]}` : '').filter(Boolean).join(' '); return `【🎣捕鱼战报】${st.n}:${st.c} 条 · ${st.t} 吨 · 渔获分 ${st.score}` + (st.m ? ` · 最大 ${st.m.s} ${st.m.w} 吨` : '') + (best ? `(${best})` : '') + ` 敢来比吗?⟦FC1:${b64}.${fnv(json + SALT)}⟧`; } function parseReport(text) { const m = (text || '').match(REPORT_RE); if (!m) return null; try { const json = decodeURIComponent(escape(atob(m[1]))); const d = JSON.parse(json); d._valid = fnv(json + SALT) === m[2]; return d; } catch (e) { return null; } } const duelMask = ui.querySelector('.duel-mask'); const duelBox = ui.querySelector('.duel'); duelMask.addEventListener('click', e => { if (e.target === duelMask || e.target.closest('[data-act="close-duel"]')) duelMask.classList.remove('open'); }); function startDuel(opp) { const me = myStats(); if (opp.u && opp.u === me.u) { toast('这是你自己的战报'); return; } if (!opp._valid) { toast('⚠️ 这份战报的校验码不对,可能被改过,对决不计入战绩'); } const topIdx = arr => { for (let i = arr.length - 1; i >= 0; i--) if (arr[i]) return i; return -1; }; const mt = topIdx(me.r), ot = topIdx(opp.r || []); const rounds = [ ['渔获分', me.score, opp.score, v => v], ['捕获条数', me.c, opp.c, v => v + ' 条'], ['总吨位', me.t, opp.t, v => v + ' 吨'], ['最大单条', me.m ? me.m.w : 0, opp.m ? opp.m.w : 0, (v, side) => { const mm = side === 0 ? me.m : opp.m; return mm ? `${mm.s} ${mm.w} 吨` : '—'; }], ['最高稀有度', mt, ot, v => v >= 0 ? CONFIG.rarities[v].name : '—'], ]; let mw = 0, ow = 0; const rows = rounds.map(([k, a, b, fmt]) => { if (a > b) mw++; else if (b > a) ow++; return `${escapeHtml(fmt(a, 0))}${k}${escapeHtml(fmt(b, 1))}`; }).join(''); let result = mw > ow ? 'win' : ow > mw ? 'lose' : (me.score > opp.score ? 'win' : opp.score > me.score ? 'lose' : 'draw'); const verdict = { win: '🏆 你赢了!', lose: '😵 你输了……', draw: '🤝 平局' }[result]; // 同一份战报只计一次战绩 const key = (opp.u || opp.n) + ':' + opp.ts; const seen = store.get('duelsSeen', []); let counted = false; if (opp._valid && !seen.includes(key)) { seen.push(key); store.set('duelsSeen', seen.slice(-200)); const f = { win: 'duelWin', lose: 'duelLose', draw: 'duelDraw' }[result]; stats[f] = (stats[f] || 0) + 1; save(); counted = true; } duelBox.innerHTML = `
${escapeHtml(me.n)}我
VS
${escapeHtml(opp.n || '神秘钓手')}${opp.ts ? '战报于 ' + escapeHtml(new Date(opp.ts).toLocaleDateString()) : ''}${opp._valid ? '' : ' · ⚠️校验失败'}
${rows}
${verdict}
五项比拼 ${mw} : ${ow}
${counted ? '已计入对决战绩' : '这份战报之前比过或校验失败,不重复计入战绩'}
`; duelMask.classList.add('open'); if (panel.classList.contains('open')) renderPanel(); } function textWithEmotes(el) { return el.textContent + [...el.querySelectorAll('img[alt]')].map(i => i.alt).join(''); } function deepQueryAll(root, sel, out = []) { const r = root.shadowRoot || root; r.querySelectorAll(sel).forEach(e => out.push(e)); for (const el of r.querySelectorAll('*')) if (el.shadowRoot) deepQueryAll(el.shadowRoot, sel, out); return out; } function myUid() { const m = document.cookie.match(/(?:^|;\s*)DedeUserID=(\d+)/); return m ? m[1] : String(CONFIG.myUidOverride || ''); } // 一条评论的饵力:0~3 function baitPower(text) { const lower = (text || '').toLowerCase(); let n = CONFIG.baitWords.filter(w => lower.includes(w.toLowerCase())).length; n += (text.match(CONFIG.baitEmoteRe) || []).length; n += (text.match(CONFIG.baitEmojiRe) || []).length; return Math.min(3, n); } // 目标是不是回复在“你撒过饵的评论楼”里 function findBait(c) { const me = myUid(); if (!me) return 0; let candidates = []; if (c.kind === 'wc') { if (c.el.tagName !== 'BILI-COMMENT-REPLY-RENDERER') return 0; // 楼主评论不算咬饵 let n = c.el, thread = null; while (n) { if (n.tagName === 'BILI-COMMENT-THREAD-RENDERER') { thread = n; break; } n = parentOrHost(n); } if (!thread) return 0; candidates = deepQueryAll(thread, 'bili-comment-renderer, bili-comment-reply-renderer').map(el => ({ el, kind: 'wc' })); } else { if (!c.el.classList.contains('sub-reply-item')) return 0; const thread = c.el.closest('.reply-item'); if (!thread) return 0; candidates = [{ el: thread, kind: 'old' }, ...[...thread.querySelectorAll('.sub-reply-item')].map(el => ({ el, kind: 'old' }))]; } let best = 0; for (const cand of candidates) { if (cand.el === c.el) continue; const ci = extractInfo(cand); if (ci.uid !== me) continue; best = Math.max(best, baitPower(ci.content)); } return best; } function probe(x, y) { const el = deepElementFromPoint(x, y); if (!el) return null; const c = findComment(el); if (!c) return null; const info = extractInfo(c); if (!info.name) return null; if (info.uid && info.uid === myUid() && info.name && store.get('nick', '') !== info.name) store.set('nick', info.name); const report = parseReport(info.content); const cls = classify(info); const bait = !report && cls.fish && info.uid !== myUid() ? findBait(c) : 0; return { c, info, cls, bait, report }; } function updateHover() { rafPending = false; if (!drag || !lastPt) return; const t = probe(lastPt.x, lastPt.y); if (hoverTarget && t && hoverTarget.info.nameEl === t.info.nameEl) return; clearHover(); if (t && t.info.nameEl) { const ne = t.info.nameEl; hoverTarget = { info: t.info, prevOutline: ne.style.outline, prevOffset: ne.style.outlineOffset }; ne.style.outline = t.report ? '2px solid #7a3ee0' : t.bait ? '2px solid #2fbf71' : t.cls.fish ? '2px dashed #f5a623' : '2px dashed #c9ccd0'; ne.style.outlineOffset = '2px'; } } rod.addEventListener('pointerdown', e => { if (e.button !== 0) return; e.preventDefault(); const r = rod.getBoundingClientRect(); homeRect = r; drag = { startX: e.clientX, startY: e.clientY, offX: e.clientX - r.left, offY: e.clientY - r.top, moved: false, touch: e.pointerType === 'touch' || e.pointerType === 'pen' }; }); // 手指会挡住目标:触屏时“钩子”在手指上方 70px,鱼竿跟着钩子走 const TOUCH_LIFT = 70; function hitPoint(e) { return drag && drag.touch ? { x: e.clientX, y: e.clientY - TOUCH_LIFT } : { x: e.clientX, y: e.clientY }; } window.addEventListener('pointermove', e => { if (!drag) return; if (!drag.moved && Math.hypot(e.clientX - drag.startX, e.clientY - drag.startY) > 6) { drag.moved = true; rod.classList.add('dragging'); panel.classList.remove('open'); document.documentElement.style.userSelect = 'none'; } if (drag.moved) { e.preventDefault(); const h = hitPoint(e); rod.style.right = 'auto'; rod.style.bottom = 'auto'; rod.style.left = (drag.touch ? h.x - 28 : e.clientX - drag.offX) + 'px'; rod.style.top = (drag.touch ? h.y - 28 : e.clientY - drag.offY) + 'px'; lastPt = h; drag.hit = h; if (!rafPending) { rafPending = true; requestAnimationFrame(updateHover); } } }, true); window.addEventListener('pointercancel', () => { if (!drag) return; drag = null; document.documentElement.style.userSelect = ''; clearHover(); rod.classList.remove('dragging'); returnRod(); }, true); window.addEventListener('pointerup', e => { if (!drag) return; const wasDrag = drag.moved; const h = drag.hit || hitPoint(e); drag = null; document.documentElement.style.userSelect = ''; if (!wasDrag) { // 单击:开关鱼篓 panel.classList.toggle('open'); if (panel.classList.contains('open')) renderPanel(); return; } const t = probe(h.x, h.y); clearHover(); rod.classList.remove('dragging'); returnRod(); cast(t, h.x, h.y, e.altKey); }, true); function returnRod() { rod.classList.add('returning'); if (homeRect) { rod.style.left = homeRect.left + 'px'; rod.style.top = homeRect.top + 'px'; } setTimeout(() => { rod.classList.remove('returning'); rod.style.left = ''; rod.style.top = ''; rod.style.right = ''; rod.style.bottom = ''; }, 380); } /* ============ 下竿 ============ */ function rand(a, b) { return a + Math.random() * (b - a); } function levelMult(lv) { if (lv == null) return CONFIG.unknownLevelMult; return CONFIG.levelRateMult[Math.max(0, Math.min(6, lv))] ?? 1; } function rollOnce() { const total = CONFIG.rarities.reduce((s, r) => s + r.weight, 0); let x = Math.random() * total; for (let i = 0; i < CONFIG.rarities.length; i++) { x -= CONFIG.rarities[i].weight; if (x < 0) return i; } return 0; } function rollRarity(info, cls, bait) { let luck = CONFIG.baitLuck[bait || 0] || 0; if (cls.type === 'name') luck += CONFIG.luckFromStrongName[cls.rarity] || 0; if (info.level != null) luck += CONFIG.luckFromLevel[info.level] || 0; let best = rollOnce(); for (let i = 0; i < luck; i++) best = Math.max(best, rollOnce()); return CONFIG.rarities[best]; } function cast(t, x, y, force) { if (!t) return; // 甩空了,不算次数 if (t.report) { if (!t.report.n) t.report.n = t.info.name; startDuel(t.report); return; } const now = Date.now(); if (now - lastCast < CONFIG.castCooldownMs) { toast('鱼竿还在晃……稍等一下'); return; } lastCast = now; const { info, c, bait } = t; let cls = t.cls; if (info.uid && info.uid === myUid()) { toast('这是你自己……不能把自己钓上来'); return; } if (bag.some(f => f.uid === info.uid)) { toast(`「${info.name}」已经在你的鱼篓里了`); return; } if (!cls.fish) { // 6 秒内对同一个人连续下竿两次 = 强行下竿(手机没有 Alt 键) if (!force && lastHuman && lastHuman.uid === info.uid && now - lastHuman.time < 6000) force = true; if (!force) { lastHuman = { uid: info.uid, time: now }; const hint = cls.why.length ? `(鱼味 ${cls.score}/${CONFIG.fishThreshold}:${cls.why.join(',')})` : `(鱼味 0/${CONFIG.fishThreshold})`; toast(`「${info.name}」看起来是活人${hint}。确定是鱼就 6 秒内再拖一次(或按住 Alt 松手),强行下竿`); return; } cls = { fish: true, type: 'forced', rarity: '伪装', score: cls.score, why: cls.why }; } stats.casts++; const base = cls.type === 'name' ? CONFIG.nameCatchRate : cls.type === 'forced' ? CONFIG.forceCatchRate : CONFIG.disguisedCatchRate; const lvMult = levelMult(info.level); const baitBonus = CONFIG.baitRateBonus[bait || 0] || 0; const calcRate = n => Math.min(CONFIG.maxRate, base * lvMult + baitBonus + n * CONFIG.tiredBonus); const rate = calcRate(attempts[info.uid] || 0); if (Math.random() < rate) { const rar = rollRarity(info, cls, bait); if (bait) stats.baited = (stats.baited || 0) + 1; const weight = rand(rar.tons[0], rar.tons[1]).toFixed(1); const disguised = cls.type !== 'name'; bag.push({ uid: info.uid, name: info.name, rarity: rar.name, species: rar.species, unit: '吨', disguised, level: info.level, weight, time: now, url: location.href, avatar: info.avatar }); delete attempts[info.uid]; stats.caught++; save(); updateBadge(); flyFish(x, y, rar.name); toast(`${bait ? '🪱 咬饵上钩!' : '🎉 '}${rar.tag}捕获「${info.name}」${disguised ? '(识破伪装)' : ''}——${rar.species}级,${weight} 吨!`); if (CONFIG.replaceCaughtComment) setTimeout(() => markCaught(c, info.name), 450); } else { attempts[info.uid] = (attempts[info.uid] || 0) + 1; stats.escaped++; save(); rod.classList.add('shake'); setTimeout(() => rod.classList.remove('shake'), 420); const next = Math.round(calcRate(attempts[info.uid]) * 100); const lvTip = info.level == null ? '' : info.level >= 4 ? `LV${info.level} 老鱼滑得很,` : `LV${info.level},`; toast(`💦 ${bait ? '饵被叼走了!' : '脱钩了!'}「${info.name}」溜走了……${lvTip}下次成功率 ${next}%`); } if (panel.classList.contains('open')) renderPanel(); } function flyFish(x, y, rarity) { const f = document.createElement('div'); f.className = 'fly'; f.textContent = { '传说': '🐋', '史诗': '🐳', '稀有': '🐠' }[rarity] || '🐟'; if (rarity === '传说' || rarity === '史诗') f.style.filter = 'drop-shadow(0 0 8px gold)'; f.style.left = x + 'px'; f.style.top = y + 'px'; ui.appendChild(f); const hr = homeRect || rod.getBoundingClientRect(); const tx = hr.left + hr.width / 2 - x, ty = hr.top + hr.height / 2 - y; const anim = f.animate([ { transform: 'translate(-50%,-50%) scale(1) rotate(0deg)', opacity: 1 }, { transform: `translate(calc(-50% + ${tx * 0.5}px), calc(-50% + ${ty * 0.5 - 80}px)) scale(1.5) rotate(200deg)`, opacity: 1, offset: 0.5 }, { transform: `translate(calc(-50% + ${tx}px), calc(-50% + ${ty}px)) scale(.4) rotate(360deg)`, opacity: 0.2 }, ], { duration: 700, easing: 'ease-in-out' }); anim.onfinish = () => f.remove(); } function markCaught(c, name) { const el = c.el; const note = document.createElement('div'); note.textContent = `🎣 这里的「${name}」已被你捕获`; note.style.cssText = 'margin:8px 0;padding:8px 12px;border-radius:8px;background:#f6f7f8;color:#9499a0;font-size:13px;'; try { el.before(note); } catch (e) {} el.style.display = 'none'; } /* ============ 只在有评论区的页面显示鱼竿 ============ */ function hasComments() { return !!(document.querySelector('bili-comments') || document.querySelector('.reply-list, .comment-list, .bili-comment')); } function refreshVisibility() { host.style.display = hasComments() ? '' : 'none'; } refreshVisibility(); setInterval(refreshVisibility, 2000); })();