Files
chatroom/resources/js/chat-room/fishing.js
T

633 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 聊天室钓鱼小游戏模块,管理抛竿、收竿、自动钓鱼循环和浮漂交互。
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 = `🛑 停止自动钓鱼<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;
}
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();
}
});
});
}