迁移钓鱼游戏脚本

This commit is contained in:
2026-04-25 14:43:33 +08:00
parent dac8adfc5a
commit 9f61dcc619
4 changed files with 631 additions and 575 deletions
+17 -3
View File
@@ -31,7 +31,7 @@
* - holiday-modal.js:处理节日福利弹窗和系统消息入口。
* - initial-state.js:恢复首屏历史消息、欢迎消息、入场特效和挂起婚姻事件。
* - bank-modal.js:处理银行弹窗、转账、排行和标签切换。
* - fishing.js:处理钓鱼入口与自动钓鱼相关交互
* - fishing.js:处理钓鱼抛竿、收竿、浮漂和自动钓鱼循环
* - fortune-panel.js:提供神秘占卜 fortunePanel Alpine 组件。
* - profile-controls.js:处理用户资料和资料相关按钮。
* - shop-controls.js:处理商店弹窗的基础按钮事件。
@@ -106,7 +106,7 @@ export {
switchBankTab,
toggleBankRankSort,
} from "./chat-room/bank-modal.js";
export { bindFishingControls } from "./chat-room/fishing.js";
export { bindFishingControls, checkAndAutoStartFishing, createBobber, reelFish, removeBobber, resetFishingBtn, startFishing, stopAutoFishing } from "./chat-room/fishing.js";
export { bindFortunePanelControls, fortunePanel } from "./chat-room/fortune-panel.js";
export { bindProfileControls } from "./chat-room/profile-controls.js";
export { bindShopControls } from "./chat-room/shop-controls.js";
@@ -200,7 +200,7 @@ import {
switchBankTab,
toggleBankRankSort,
} from "./chat-room/bank-modal.js";
import { bindFishingControls } from "./chat-room/fishing.js";
import { bindFishingControls, checkAndAutoStartFishing, createBobber, reelFish, removeBobber, resetFishingBtn, startFishing, stopAutoFishing } from "./chat-room/fishing.js";
import { bindFortunePanelControls, fortunePanel } from "./chat-room/fortune-panel.js";
import { bindProfileControls } from "./chat-room/profile-controls.js";
import { bindShopControls } from "./chat-room/shop-controls.js";
@@ -314,6 +314,13 @@ if (typeof window !== "undefined") {
switchBankTab,
toggleBankRankSort,
bindFishingControls,
checkAndAutoStartFishing,
createBobber,
reelFish,
removeBobber,
resetFishingBtn,
startFishing,
stopAutoFishing,
bindFortunePanelControls,
fortunePanel,
bindMarriageStatusControls,
@@ -408,6 +415,13 @@ if (typeof window !== "undefined") {
window.openLotteryPanel = openLotteryPanel;
window.openBankModal = openBankModal;
window.showLotteryMsg = showLotteryMsg;
window.checkAndAutoStartFishing = checkAndAutoStartFishing;
window.createBobber = createBobber;
window.reelFish = reelFish;
window.removeBobber = removeBobber;
window.resetFishingBtn = resetFishingBtn;
window.startFishing = startFishing;
window.stopAutoFishing = stopAutoFishing;
window.buyVip = buyVip;
window.closeVipModal = closeVipModal;
window.openVipModal = openVipModal;
+603 -6
View File
@@ -1,13 +1,605 @@
// 聊天室钓鱼入口事件绑定,先兼容存量全局 startFishing 实现
// 聊天室钓鱼小游戏模块,管理抛竿、收竿、自动钓鱼循环和浮漂交互
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) {
let remaining = cooldown;
setFishingButton(`⏳ 冷却 ${remaining}s`, true);
showAutoFishStopButton(cooldown);
autoFishCooldownCountdown = window.setInterval(() => {
remaining -= 1;
setFishingButton(`⏳ 冷却 ${remaining}s`, true);
if (remaining <= 0) {
window.clearInterval(autoFishCooldownCountdown);
autoFishCooldownCountdown = null;
}
}, 1000);
autoFishCooldownTimer = window.setTimeout(() => {
autoFishCooldownTimer = null;
hideAutoFishStopButton();
if (autoFishing) {
void startFishing();
}
}, cooldown * 1000);
}
/**
* 展示可拖动的停止自动钓鱼按钮。
*
* @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 = `🛑 停止自动钓鱼<span class="drag-hint">冷却 ${Number(cooldown) || 0}s · 可拖动</span>`;
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<void>}
*/
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(`<span style="color:#2563eb;font-weight:bold;">🎣【钓鱼】</span>${escapeHtml(data.message)}<span class="msg-time">(${timeText()})</span>`);
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(`<span style="color:#7c3aed;font-weight:bold;">🎣 自动钓鱼卡生效!自动收竿中... <span style="font-size:10px;opacity:0.7">(剩余${Number(data.auto_fishing_minutes_left) || 0}分钟)</span></span>`);
fishingReelTimeout = window.setTimeout(() => {
removeBobber();
void reelFish();
}, 1800);
return;
}
appendFishingMessage('<span style="color:#d97706;font-weight:bold;font-size:14px;">🐟 鱼上钩了!快点击屏幕上的浮漂!</span>');
setFishingButton("🎣 点击浮漂!", true);
bobber.addEventListener("click", () => {
removeBobber();
if (fishingReelTimeout) {
window.clearTimeout(fishingReelTimeout);
fishingReelTimeout = null;
}
void reelFish();
}, { once: true });
fishingReelTimeout = window.setTimeout(() => {
removeBobber();
fishToken = null;
appendFishingMessage('<span style="color:#999;">💨 你反应太慢了,鱼跑掉了...</span>');
resetFishingBtn();
}, 8000);
}, Number(data.wait_time || 0) * 1000);
} catch (error) {
window.chatDialog?.alert?.(`网络错误:${error.message}`, "网络异常", "#cc4444");
removeBobber();
setFishingButton("🎣 钓鱼", false);
}
}
/**
* 收竿并提交本次钓鱼 token。
*
* @returns {Promise<void>}
*/
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(
`<span style="color:${color};font-weight:bold;">${escapeHtml(result.emoji || "🎣")}【钓鱼结果】</span>${escapeHtml(result.message || "")}` +
` <span style="color:#666;font-size:11px;">(经验:${Number(data.exp_num) || 0} 金币:${Number(data.jjb) || 0}</span>` +
`<span class="msg-time">(${timeText()})</span>`,
);
if (autoFishing) {
startAutoFishingCooldown(Number(data.cooldown_seconds) || 300);
return;
}
} else {
appendFishingMessage(`<span style="color:red;">【钓鱼】${escapeHtml(data.message || "操作失败")}</span><span class="msg-time">(${timeText()})</span>`);
if (autoFishing) {
retryAutoFishing();
return;
}
autoFishing = false;
}
} catch (error) {
if (autoFishing) {
appendFishingMessage('<span style="color:#d97706;">⚠️ 网络异常,5秒后自动重试钓鱼...</span>');
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('<span style="color:#6b7280;">🛑 已停止自动钓鱼。</span>');
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;
}
@@ -19,10 +611,15 @@ export function bindFishingControls() {
}
event.preventDefault();
void startFishing();
});
// 钓鱼完整流程仍在 fishing-panel.blade.php,当前模块只统一按钮事件入口。
if (typeof window.startFishing === "function") {
window.startFishing();
}
document.addEventListener("DOMContentLoaded", () => {
checkAndAutoStartFishing();
window.addEventListener("chat:screen-cleared", () => {
if (!autoFishing) {
checkAndAutoStartFishing();
}
});
});
}
+7
View File
@@ -30,6 +30,11 @@
$operatorActivePosition = Auth::user()->activePosition?->load('position.department')->position;
$operatorDepartmentRank = (int) ($operatorActivePosition?->department?->rank ?? 0);
$operatorPositionRank = (int) ($operatorActivePosition?->rank ?? 0);
// 自动钓鱼状态下发给 Vite 模块,避免钓鱼面板继续在 Blade 内写业务脚本。
$autoFishingMinutesLeft = app(\App\Services\ShopService::class)->getActiveAutoFishingMinutesLeft(Auth::user());
$fishingCooldownKey = 'fishing:cd:'.Auth::id();
$fishingCooldownSeconds = \Illuminate\Support\Facades\Redis::ttl($fishingCooldownKey);
$fishingCooldownSeconds = $fishingCooldownSeconds > 0 ? $fishingCooldownSeconds : 0;
@endphp
<script>
window.chatContext = {
@@ -49,6 +54,8 @@
heartbeatUrl: "{{ route('chat.heartbeat', $room->id) }}",
fishCastUrl: "{{ route('fishing.cast', $room->id) }}",
fishReelUrl: "{{ route('fishing.reel', $room->id) }}",
autoFishingMinutesLeft: {{ (int) $autoFishingMinutesLeft }},
fishingCooldownSeconds: {{ (int) $fishingCooldownSeconds }},
chatBotUrl: "{{ route('chatbot.chat') }}",
chatBotClearUrl: "{{ route('chatbot.clear') }}",
@php
@@ -1,566 +1,4 @@
<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);
// 核心修复:如果是自动钓鱼中遇到超时或接口报错,不要中断,而是休眠5秒后重试抛竿
if (_autoFishing) {
const btn = document.getElementById('fishing-btn');
if (btn) {
btn.disabled = true;
btn.textContent = '⏳ 重试中...';
}
_autoFishCdTimer = setTimeout(() => {
_autoFishCdTimer = null;
if (_autoFishing) startFishing();
}, 5000);
if (autoScroll) container2.scrollTop = container2.scrollHeight;
return; // 阻止后续重置逻辑
}
_autoFishing = false; // 非自动模式出错时停止
}
if (autoScroll) container2.scrollTop = container2.scrollHeight;
} catch (e) {
// 网络断开或超时
if (_autoFishing) {
const noticeDiv = document.createElement('div');
noticeDiv.className = 'msg-line';
noticeDiv.innerHTML = `<span style="color:#d97706;">⚠️ 网络异常,5秒后自动重试钓鱼...</span>`;
container2.appendChild(noticeDiv);
if (autoScroll) container2.scrollTop = container2.scrollHeight;
const btn = document.getElementById('fishing-btn');
if (btn) {
btn.disabled = true;
btn.textContent = '⏳ 重试中...';
}
_autoFishCdTimer = setTimeout(() => {
_autoFishCdTimer = null;
if (_autoFishing) startFishing();
}, 5000);
return; // 阻止后续重置逻辑
}
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');
if (btn) {
btn.textContent = '🎣 钓鱼';
btn.disabled = false;
}
fishingTimer = null;
fishingReelTimeout = null;
removeBobber();
}
/**
* 检查并自动开启钓鱼
*/
function checkAndAutoStartFishing() {
@if(auth()->check())
@php
$autoFishingMinutesLeft = app(\App\Services\ShopService::class)->getActiveAutoFishingMinutesLeft(auth()->user());
$cdKey = "fishing:cd:" . auth()->id();
$cooldown = \Illuminate\Support\Facades\Redis::ttl($cdKey);
$cooldown = $cooldown > 0 ? $cooldown : 0;
@endphp
const minutesLeft = {{ $autoFishingMinutesLeft }};
const initialCooldown = {{ $cooldown }};
if (minutesLeft > 0 && !_autoFishing) {
_autoFishing = true;
if (initialCooldown > 0) {
// 如果还在冷却中,恢复冷却倒计时和停止按钮
console.log(`检测到自动钓鱼卡有效,恢复钓鱼状态,剩余冷却 ${initialCooldown}s`);
const btn = document.getElementById('fishing-btn');
if (btn) {
btn.disabled = true;
btn.textContent = `⏳ 冷却 ${initialCooldown}s`;
btn.onclick = null;
}
_showAutoFishStopBtn(initialCooldown);
let remaining = initialCooldown;
_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();
}, initialCooldown * 1000);
} else {
// 无冷却,直接开始
console.log('检测到自动钓鱼卡有效,自动抛竿');
startFishing();
}
}
@endif
}
document.addEventListener('DOMContentLoaded', () => {
// 页面初始化时检查
checkAndAutoStartFishing();
// 监听全员清屏事件(清屏后由于 DOM 可能会导致部分提示被清,确保自动钓鱼能继续,不过由于上面已是闭环,这里只是做个保险调用)
// 主要是如果用户在开着自动钓鱼,清屏不会终止 JS 变量,所以不需要完全重置。
// 但如果要求“全员清屏后也能自动开启”(指可能因为某种原因刷新了聊天流),我们可以额外保障一下。
window.addEventListener('chat:screen-cleared', () => {
if (!_autoFishing) {
checkAndAutoStartFishing();
}
});
});
</script>
{{--
钓鱼小游戏业务脚本已迁移到 resources/js/chat-room/fishing.js。
自动钓鱼初始状态由 resources/views/chat/frame.blade.php 下发到 window.chatContext。
--}}