462 lines
18 KiB
PHP
462 lines
18 KiB
PHP
|
|
<script>
|
|||
|
|
// ── 钓鱼小游戏(随机浮漂版)─────────────────────────
|
|||
|
|
let fishingTimer = null;
|
|||
|
|
let fishingReelTimeout = null;
|
|||
|
|
let _fishToken = null; // 当次钓鱼的 token
|
|||
|
|
let _autoFishing = false; // 是否处于自动钓鱼循环中
|
|||
|
|
let _autoFishCdTimer = null; // 自动钓鱼冷却计时器
|
|||
|
|
let _autoFishCdCountdown = null; // 冷却倒计时 interval
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 创建浮漂 DOM 元素(绝对定位在聊天框上层)
|
|||
|
|
* @param {number} x 水平百分比 0-100
|
|||
|
|
* @param {number} y 垂直百分比 0-100
|
|||
|
|
* @returns {HTMLElement}
|
|||
|
|
*/
|
|||
|
|
function createBobber(x, y) {
|
|||
|
|
const el = document.createElement('div');
|
|||
|
|
el.id = 'fishing-bobber';
|
|||
|
|
el.style.cssText = `
|
|||
|
|
position: fixed;
|
|||
|
|
left: ${x}vw;
|
|||
|
|
top: ${y}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;
|
|||
|
|
`;
|
|||
|
|
el.textContent = '🪝';
|
|||
|
|
el.title = '鱼上钩了!快点击!';
|
|||
|
|
// 注入动画
|
|||
|
|
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; }
|
|||
|
|
}
|
|||
|
|
@keyframes bobberPulse {
|
|||
|
|
0%,100% { box-shadow: 0 0 0 0 rgba(220,38,38,0.6); }
|
|||
|
|
50% { box-shadow: 0 0 0 14px rgba(220,38,38,0); }
|
|||
|
|
}
|
|||
|
|
#fishing-bobber.sinking {
|
|||
|
|
animation: bobberSink 1.5s forwards !important;
|
|||
|
|
}
|
|||
|
|
`;
|
|||
|
|
document.head.appendChild(style);
|
|||
|
|
}
|
|||
|
|
return el;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 移除浮漂 */
|
|||
|
|
function removeBobber() {
|
|||
|
|
const el = document.getElementById('fishing-bobber');
|
|||
|
|
if (el) el.remove();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 开始钓鱼:调用抛竿 API,随机显示浮漂位置
|
|||
|
|
*/
|
|||
|
|
async function startFishing() {
|
|||
|
|
const btn = document.getElementById('fishing-btn');
|
|||
|
|
btn.disabled = true;
|
|||
|
|
btn.textContent = '🎣 抛竿中...';
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const res = await fetch(window.chatContext.fishCastUrl, {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {
|
|||
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
|||
|
|
'Accept': 'application/json'
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
const data = await res.json();
|
|||
|
|
|
|||
|
|
if (!res.ok || data.status !== 'success') {
|
|||
|
|
window.chatDialog.alert(data.message || '钓鱼失败', '操作失败', '#cc4444');
|
|||
|
|
btn.disabled = false;
|
|||
|
|
btn.textContent = '🎣 钓鱼';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 保存本次 token(收竿时提交)
|
|||
|
|
_fishToken = data.token;
|
|||
|
|
_autoFishing = !!data.auto_fishing; // 持有自动钓鱼卡则开启循环模式
|
|||
|
|
|
|||
|
|
// 聊天框提示
|
|||
|
|
const castDiv = document.createElement('div');
|
|||
|
|
castDiv.className = 'msg-line';
|
|||
|
|
const timeStr = new Date().toLocaleTimeString('zh-CN', {
|
|||
|
|
hour12: false
|
|||
|
|
});
|
|||
|
|
castDiv.innerHTML =
|
|||
|
|
`<span style="color:#2563eb;font-weight:bold;">🎣【钓鱼】</span>${data.message}<span class="msg-time">(${timeStr})</span>`;
|
|||
|
|
container2.appendChild(castDiv);
|
|||
|
|
if (autoScroll) container2.scrollTop = container2.scrollHeight;
|
|||
|
|
|
|||
|
|
btn.textContent = '🎣 等待中...';
|
|||
|
|
|
|||
|
|
// 创建浮漂(浮漂在随机位置)
|
|||
|
|
const bobber = createBobber(data.bobber_x, data.bobber_y);
|
|||
|
|
document.body.appendChild(bobber);
|
|||
|
|
|
|||
|
|
// 等待 wait_time 秒后浮漂「下沉」
|
|||
|
|
fishingTimer = setTimeout(() => {
|
|||
|
|
// 播放下沉动画
|
|||
|
|
bobber.classList.add('sinking');
|
|||
|
|
bobber.textContent = '🐟';
|
|||
|
|
|
|||
|
|
const hookDiv = document.createElement('div');
|
|||
|
|
hookDiv.className = 'msg-line';
|
|||
|
|
|
|||
|
|
if (data.auto_fishing) {
|
|||
|
|
// 自动钓鱼卡:在动画结束后自动收竿
|
|||
|
|
hookDiv.innerHTML =
|
|||
|
|
`<span style="color:#7c3aed;font-weight:bold;">🎣 自动钓鱼卡生效!自动收竿中... <span style="font-size:10px;opacity:0.7">(剩余${data.auto_fishing_minutes_left}分钟)</span></span>`;
|
|||
|
|
container2.appendChild(hookDiv);
|
|||
|
|
if (autoScroll) container2.scrollTop = container2.scrollHeight;
|
|||
|
|
|
|||
|
|
// 500ms 后自动收竿(等动画)
|
|||
|
|
fishingReelTimeout = setTimeout(() => {
|
|||
|
|
removeBobber();
|
|||
|
|
reelFish();
|
|||
|
|
}, 1800);
|
|||
|
|
} else {
|
|||
|
|
// 手动模式:玩家需在 8 秒内点击浮漂
|
|||
|
|
hookDiv.innerHTML =
|
|||
|
|
`<span style="color:#d97706;font-weight:bold;font-size:14px;">🐟 鱼上钩了!快点击屏幕上的浮漂!</span>`;
|
|||
|
|
container2.appendChild(hookDiv);
|
|||
|
|
if (autoScroll) container2.scrollTop = container2.scrollHeight;
|
|||
|
|
|
|||
|
|
btn.textContent = '🎣 点击浮漂!';
|
|||
|
|
|
|||
|
|
// 浮漂点击事件
|
|||
|
|
bobber.onclick = () => {
|
|||
|
|
removeBobber();
|
|||
|
|
if (fishingReelTimeout) {
|
|||
|
|
clearTimeout(fishingReelTimeout);
|
|||
|
|
fishingReelTimeout = null;
|
|||
|
|
}
|
|||
|
|
reelFish();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 8 秒内不点击 → 鱼跑了(token 过期服务端也会拒绝)
|
|||
|
|
fishingReelTimeout = setTimeout(() => {
|
|||
|
|
removeBobber();
|
|||
|
|
_fishToken = null;
|
|||
|
|
const missDiv = document.createElement('div');
|
|||
|
|
missDiv.className = 'msg-line';
|
|||
|
|
missDiv.innerHTML = '<span style="color:#999;">💨 你反应太慢了,鱼跑掉了...</span>';
|
|||
|
|
container2.appendChild(missDiv);
|
|||
|
|
if (autoScroll) container2.scrollTop = container2.scrollHeight;
|
|||
|
|
resetFishingBtn();
|
|||
|
|
}, 8000);
|
|||
|
|
}
|
|||
|
|
}, data.wait_time * 1000);
|
|||
|
|
|
|||
|
|
} catch (e) {
|
|||
|
|
window.chatDialog.alert('网络错误:' + e.message, '网络异常', '#cc4444');
|
|||
|
|
removeBobber();
|
|||
|
|
btn.disabled = false;
|
|||
|
|
btn.textContent = '🎣 钓鱼';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 收竿 — 提交 token 到后端,获取随机结果
|
|||
|
|
*/
|
|||
|
|
async function reelFish() {
|
|||
|
|
const btn = document.getElementById('fishing-btn');
|
|||
|
|
btn.disabled = true;
|
|||
|
|
btn.textContent = '🎣 拉竿中...';
|
|||
|
|
|
|||
|
|
if (fishingReelTimeout) {
|
|||
|
|
clearTimeout(fishingReelTimeout);
|
|||
|
|
fishingReelTimeout = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const token = _fishToken;
|
|||
|
|
_fishToken = null;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const res = await fetch(window.chatContext.fishReelUrl, {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {
|
|||
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
|||
|
|
'Accept': 'application/json',
|
|||
|
|
'Content-Type': 'application/json'
|
|||
|
|
},
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
token
|
|||
|
|
})
|
|||
|
|
});
|
|||
|
|
const data = await res.json();
|
|||
|
|
const timeStr = new Date().toLocaleTimeString('zh-CN', {
|
|||
|
|
hour12: false
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (res.ok && data.status === 'success') {
|
|||
|
|
const r = data.result;
|
|||
|
|
const color = r.exp >= 0 ? '#16a34a' : '#dc2626';
|
|||
|
|
const resultDiv = document.createElement('div');
|
|||
|
|
resultDiv.className = 'msg-line';
|
|||
|
|
resultDiv.innerHTML =
|
|||
|
|
`<span style="color:${color};font-weight:bold;">${r.emoji}【钓鱼结果】</span>${r.message}` +
|
|||
|
|
` <span style="color:#666;font-size:11px;">(经验:${data.exp_num} 金币:${data.jjb})</span>` +
|
|||
|
|
`<span class="msg-time">(${timeStr})</span>`;
|
|||
|
|
container2.appendChild(resultDiv);
|
|||
|
|
|
|||
|
|
// 自动钓鱼卡循环:等冷却时间后自动再次抛竿
|
|||
|
|
if (_autoFishing) {
|
|||
|
|
const cooldown = data.cooldown_seconds || 300;
|
|||
|
|
const btn = document.getElementById('fishing-btn');
|
|||
|
|
btn.disabled = true;
|
|||
|
|
btn.textContent = `⏳ 冷却 ${cooldown}s`;
|
|||
|
|
btn.onclick = null;
|
|||
|
|
|
|||
|
|
// 显示停止按钮
|
|||
|
|
_showAutoFishStopBtn(cooldown);
|
|||
|
|
|
|||
|
|
// 倒计时更新文字
|
|||
|
|
let remaining = cooldown;
|
|||
|
|
_autoFishCdCountdown = setInterval(() => {
|
|||
|
|
remaining--;
|
|||
|
|
const b = document.getElementById('fishing-btn');
|
|||
|
|
if (b) b.textContent = `⏳ 冷却 ${remaining}s`;
|
|||
|
|
if (remaining <= 0) {
|
|||
|
|
clearInterval(_autoFishCdCountdown);
|
|||
|
|
_autoFishCdCountdown = null;
|
|||
|
|
}
|
|||
|
|
}, 1000);
|
|||
|
|
|
|||
|
|
// 冷却结束后自动抛竿
|
|||
|
|
_autoFishCdTimer = setTimeout(() => {
|
|||
|
|
_autoFishCdTimer = null;
|
|||
|
|
_hideAutoFishStopBtn();
|
|||
|
|
if (_autoFishing) startFishing(); // 仍未停止 → 继续
|
|||
|
|
}, cooldown * 1000);
|
|||
|
|
|
|||
|
|
if (autoScroll) container2.scrollTop = container2.scrollHeight;
|
|||
|
|
return; // 不走 resetFishingBtn
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
const errDiv = document.createElement('div');
|
|||
|
|
errDiv.className = 'msg-line';
|
|||
|
|
errDiv.innerHTML =
|
|||
|
|
`<span style="color:red;">【钓鱼】${data.message || '操作失败'}</span><span class="msg-time">(${timeStr})</span>`;
|
|||
|
|
container2.appendChild(errDiv);
|
|||
|
|
_autoFishing = false; // 出错时停止循环
|
|||
|
|
}
|
|||
|
|
if (autoScroll) container2.scrollTop = container2.scrollHeight;
|
|||
|
|
} catch (e) {
|
|||
|
|
window.chatDialog.alert('网络错误:' + e.message, '网络异常', '#cc4444');
|
|||
|
|
_autoFishing = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
resetFishingBtn();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 显示「停止自动钓鱼」悬浮按钮(支持拖拽移动)
|
|||
|
|
* @param {number} cooldown 冷却秒数(用于倒计时提示)
|
|||
|
|
*/
|
|||
|
|
function _showAutoFishStopBtn(cooldown) {
|
|||
|
|
if (document.getElementById('auto-fish-stop-btn')) return;
|
|||
|
|
|
|||
|
|
// 注入动画样式
|
|||
|
|
if (!document.getElementById('auto-fish-stop-style')) {
|
|||
|
|
const s = document.createElement('style');
|
|||
|
|
s.id = 'auto-fish-stop-style';
|
|||
|
|
s.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(s);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const btn = document.createElement('button');
|
|||
|
|
btn.id = 'auto-fish-stop-btn';
|
|||
|
|
btn.innerHTML = '🛑 停止自动钓鱼<span class="drag-hint">⠿ 可拖动</span>';
|
|||
|
|
|
|||
|
|
// 从 localStorage 恢复上次位置,默认右下角
|
|||
|
|
const saved = (() => {
|
|||
|
|
try {
|
|||
|
|
return JSON.parse(localStorage.getItem('autoFishBtnPos'));
|
|||
|
|
} catch {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
})();
|
|||
|
|
if (saved) {
|
|||
|
|
btn.style.left = saved.left + 'px';
|
|||
|
|
btn.style.top = saved.top + 'px';
|
|||
|
|
} else {
|
|||
|
|
btn.style.bottom = '80px';
|
|||
|
|
btn.style.right = '20px';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 拖拽逻辑(鼠标 + 触摸) ──────────────────────────────────
|
|||
|
|
let isDragging = false;
|
|||
|
|
let startX, startY, startLeft, startTop;
|
|||
|
|
|
|||
|
|
function onDragStart(e) {
|
|||
|
|
// 将 right/bottom 转为 left/top 绝对坐标,便于拖拽计算
|
|||
|
|
const rect = btn.getBoundingClientRect();
|
|||
|
|
btn.style.left = rect.left + 'px';
|
|||
|
|
btn.style.top = rect.top + 'px';
|
|||
|
|
btn.style.right = 'auto';
|
|||
|
|
btn.style.bottom = 'auto';
|
|||
|
|
|
|||
|
|
isDragging = false;
|
|||
|
|
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
|||
|
|
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
|||
|
|
startX = clientX;
|
|||
|
|
startY = clientY;
|
|||
|
|
startLeft = rect.left;
|
|||
|
|
startTop = rect.top;
|
|||
|
|
|
|||
|
|
document.addEventListener('mousemove', onDragMove, {
|
|||
|
|
passive: false
|
|||
|
|
});
|
|||
|
|
document.addEventListener('mouseup', onDragEnd);
|
|||
|
|
document.addEventListener('touchmove', onDragMove, {
|
|||
|
|
passive: false
|
|||
|
|
});
|
|||
|
|
document.addEventListener('touchend', onDragEnd);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onDragMove(e) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
|||
|
|
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
|||
|
|
const dx = clientX - startX;
|
|||
|
|
const dy = clientY - startY;
|
|||
|
|
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) {
|
|||
|
|
isDragging = true;
|
|||
|
|
}
|
|||
|
|
if (!isDragging) return;
|
|||
|
|
|
|||
|
|
const newLeft = Math.max(0, Math.min(window.innerWidth - btn.offsetWidth, startLeft + dx));
|
|||
|
|
const newTop = Math.max(0, Math.min(window.innerHeight - btn.offsetHeight, startTop + dy));
|
|||
|
|
btn.style.left = newLeft + 'px';
|
|||
|
|
btn.style.top = newTop + 'px';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onDragEnd() {
|
|||
|
|
document.removeEventListener('mousemove', onDragMove);
|
|||
|
|
document.removeEventListener('mouseup', onDragEnd);
|
|||
|
|
document.removeEventListener('touchmove', onDragMove);
|
|||
|
|
document.removeEventListener('touchend', onDragEnd);
|
|||
|
|
|
|||
|
|
// 持久化位置
|
|||
|
|
if (isDragging) {
|
|||
|
|
localStorage.setItem('autoFishBtnPos', JSON.stringify({
|
|||
|
|
left: parseInt(btn.style.left),
|
|||
|
|
top: parseInt(btn.style.top),
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
btn.addEventListener('mousedown', onDragStart);
|
|||
|
|
btn.addEventListener('touchstart', onDragStart, {
|
|||
|
|
passive: true
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 拖拽时不触发 click;非拖拽时才停止钓鱼
|
|||
|
|
btn.addEventListener('click', () => {
|
|||
|
|
if (!isDragging) stopAutoFishing();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.body.appendChild(btn);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 隐藏停止按钮 */
|
|||
|
|
function _hideAutoFishStopBtn() {
|
|||
|
|
const el = document.getElementById('auto-fish-stop-btn');
|
|||
|
|
if (el) el.remove();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 手动停止自动钓鱼循环
|
|||
|
|
*/
|
|||
|
|
function stopAutoFishing() {
|
|||
|
|
_autoFishing = false;
|
|||
|
|
if (_autoFishCdTimer) {
|
|||
|
|
clearTimeout(_autoFishCdTimer);
|
|||
|
|
_autoFishCdTimer = null;
|
|||
|
|
}
|
|||
|
|
if (_autoFishCdCountdown) {
|
|||
|
|
clearInterval(_autoFishCdCountdown);
|
|||
|
|
_autoFishCdCountdown = null;
|
|||
|
|
}
|
|||
|
|
_hideAutoFishStopBtn();
|
|||
|
|
|
|||
|
|
const noticeDiv = document.createElement('div');
|
|||
|
|
noticeDiv.className = 'msg-line';
|
|||
|
|
noticeDiv.innerHTML = '<span style="color:#6b7280;">🛑 已停止自动钓鱼。</span>';
|
|||
|
|
container2.appendChild(noticeDiv);
|
|||
|
|
if (autoScroll) container2.scrollTop = container2.scrollHeight;
|
|||
|
|
|
|||
|
|
resetFishingBtn();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 重置钓鱼按钮状态(停止自动循环后调用)
|
|||
|
|
*/
|
|||
|
|
function resetFishingBtn() {
|
|||
|
|
_autoFishing = false;
|
|||
|
|
_hideAutoFishStopBtn();
|
|||
|
|
if (_autoFishCdTimer) {
|
|||
|
|
clearTimeout(_autoFishCdTimer);
|
|||
|
|
_autoFishCdTimer = null;
|
|||
|
|
}
|
|||
|
|
if (_autoFishCdCountdown) {
|
|||
|
|
clearInterval(_autoFishCdCountdown);
|
|||
|
|
_autoFishCdCountdown = null;
|
|||
|
|
}
|
|||
|
|
const btn = document.getElementById('fishing-btn');
|
|||
|
|
btn.textContent = '🎣 钓鱼';
|
|||
|
|
btn.disabled = false;
|
|||
|
|
btn.onclick = startFishing;
|
|||
|
|
fishingTimer = null;
|
|||
|
|
fishingReelTimeout = null;
|
|||
|
|
removeBobber();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
</script>
|