From 63679a622f7d3cb77aab7884ab99a16b3409213a Mon Sep 17 00:00:00 2001 From: lkddi Date: Sun, 1 Mar 2026 16:19:45 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9A=E9=9A=8F=E6=9C=BA?= =?UTF-8?q?=E6=B5=AE=E6=BC=82=E9=92=93=E9=B1=BC=E9=98=B2=E6=8C=82=E6=9C=BA?= =?UTF-8?q?=20+=20=E5=95=86=E5=BA=97=E8=87=AA=E5=8A=A8=E9=92=93=E9=B1=BC?= =?UTF-8?q?=E5=8D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心变更: 1. FishingController 重写 - cast(): 生成随机浮漂坐标(x/y%) + 一次性 token - reel(): 必须携带 token 才能收竿(防脚本绕过) - 检测自动钓鱼卡剩余时间并返回给前端 2. 前端钓鱼逻辑重写 - 抛竿后显示随机位置 🪝 浮漂动画(全屏飘动) - 鱼上钩时浮漂「下沉」动画,8秒内点击浮漂才能收竿 - 超时未点击:鱼跑了,token 也失效 - 持有自动钓鱼卡:自动点击,紫色提示剩余时间 3. 商店新增「🎣 自动钓鱼卡」分组 - 3档:2h(800金)/8h(2500金)/24h(6000金) - 图标徽章显示剩余有效时间(紫色) - 购买后即时激活,无需手动操作 4. 数据库 - shop_items.type 加 auto_fishing 枚举 - shop_items.duration_minutes 新字段(分钟精度) - Seeder 写入 3 张卡数据 防挂机原理:按钮 → 浮漂随机位置,脚本无法固定坐标点击 --- app/Http/Controllers/FishingController.php | 82 +++++--- app/Http/Controllers/ShopController.php | 4 +- app/Models/ShopItem.php | 11 +- app/Services/ShopService.php | 54 ++++++ ..._fishing_card_type_to_shop_items_table.php | 31 +++ ...d_duration_minutes_to_shop_items_table.php | 35 ++++ database/seeders/AutoFishingCardSeeder.php | 68 +++++++ .../views/chat/partials/scripts.blade.php | 176 ++++++++++++++---- .../views/chat/partials/toolbar.blade.php | 33 +++- 9 files changed, 417 insertions(+), 77 deletions(-) create mode 100644 database/migrations/2026_03_01_161315_add_auto_fishing_card_type_to_shop_items_table.php create mode 100644 database/migrations/2026_03_01_161358_add_duration_minutes_to_shop_items_table.php create mode 100644 database/seeders/AutoFishingCardSeeder.php diff --git a/app/Http/Controllers/FishingController.php b/app/Http/Controllers/FishingController.php index acd4074..87ff46b 100644 --- a/app/Http/Controllers/FishingController.php +++ b/app/Http/Controllers/FishingController.php @@ -2,12 +2,16 @@ /** * 文件功能:钓鱼小游戏控制器 - * 复刻原版 ASP 聊天室 diaoyu/ 目录下的钓鱼功能 - * 简化掉鱼竿道具系统,用 Redis 控制冷却,随机奖惩经验/金币 + * + * 新增随机浮漂点击防挂机机制: + * - 抛竿时生成一次性 token 和随机浮漂坐标(百分比),返回给前端 + * - 前端显示漂浮动画,等待下沉后用户点击,将 token 随收竿请求提交 + * - 若持有有效「自动钓鱼卡」,前端直接自动点击,无需手动操作 + * - 服务端验证 token 有效性,防止脚本直接调用收竿接口 * * @author ChatRoom Laravel * - * @version 1.0.0 + * @version 2.0.0 */ namespace App\Http\Controllers; @@ -16,12 +20,14 @@ use App\Enums\CurrencySource; use App\Events\MessageSent; use App\Models\Sysparam; use App\Services\ChatStateService; +use App\Services\ShopService; use App\Services\UserCurrencyService; use App\Services\VipService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Redis; +use Illuminate\Support\Str; class FishingController extends Controller { @@ -29,10 +35,17 @@ class FishingController extends Controller private readonly ChatStateService $chatState, private readonly VipService $vipService, private readonly UserCurrencyService $currencyService, + private readonly ShopService $shopService, ) {} /** - * 抛竿 — 检查冷却和金币,扣除金币,返回随机等待时间 + * 抛竿 — 检查冷却和金币,扣除金币,生成浮漂 token 和随机坐标。 + * + * 返回: + * wait_time — 等待秒数(前端倒数后触发下沉动画) + * bobber_x/y — 浮漂随机位置(0-100 百分比) + * token — 本次钓鱼唯一令牌(收竿时必须携带) + * auto_fishing — 是否持有有效自动钓鱼卡(前端据此自动点击) * * @param int $id 房间ID */ @@ -64,36 +77,49 @@ class FishingController extends Controller ], 422); } - // 3. 扣除金币(通过统一积分服务记录流水) + // 3. 扣除金币 $this->currencyService->change( - $user, - 'gold', - -$cost, + $user, 'gold', -$cost, CurrencySource::FISHING_COST, "钓鱼抛竿消耗 {$cost} 金币", $id, ); - $user->refresh(); // 刷新本地模型(service 已原子更新) + $user->refresh(); - // 4. 设置"正在钓鱼"标记(防止重复抛竿,30秒后自动过期) - Redis::setex("fishing:active:{$user->id}", 30, time()); - - // 5. 计算随机等待时间 + // 4. 生成一次性 token,存入 Redis(TTL = 等待时间 + 收竿窗口 + 缓冲) $waitMin = (int) Sysparam::getValue('fishing_wait_min', '8'); $waitMax = (int) Sysparam::getValue('fishing_wait_max', '15'); $waitTime = rand($waitMin, $waitMax); + $token = Str::random(32); + $tokenKey = "fishing:token:{$user->id}"; + // token 有效期 = 等待时间 + 10秒点击窗口 + 5秒缓冲 + Redis::setex($tokenKey, $waitTime + 15, $token); + + // 5. 生成随机浮漂坐标(百分比,避开边缘) + $bobberX = rand(15, 85); // 左右 15%~85% + $bobberY = rand(20, 65); // 上下 20%~65% + + // 6. 检查是否持有有效自动钓鱼卡 + $autoFishingMinutes = $this->shopService->getActiveAutoFishingMinutesLeft($user); return response()->json([ 'status' => 'success', 'message' => "已花费 {$cost} 金币,鱼竿已抛出!等待鱼儿上钩...", 'wait_time' => $waitTime, + 'bobber_x' => $bobberX, + 'bobber_y' => $bobberY, + 'token' => $token, + 'auto_fishing' => $autoFishingMinutes > 0, + 'auto_fishing_minutes_left' => $autoFishingMinutes, 'cost' => $cost, 'jjb' => $user->jjb, ]); } /** - * 收竿 — 随机计算钓鱼结果,更新经验/金币,广播到聊天室 + * 收竿 — 验证浮漂 token,随机计算钓鱼结果,更新经验/金币,广播到聊天室。 + * + * 必须携带 token(从抛竿接口获取),否则判定为非法收竿。 * * @param int $id 房间ID */ @@ -104,17 +130,20 @@ class FishingController extends Controller return response()->json(['status' => 'error', 'message' => '请先登录'], 401); } - // 1. 检查是否有"正在钓鱼"标记 - $activeKey = "fishing:active:{$user->id}"; - if (! Redis::exists($activeKey)) { + // 1. 验证 token(防止脚本绕过浮漂直接收竿) + $tokenKey = "fishing:token:{$user->id}"; + $storedToken = Redis::get($tokenKey); + $clientToken = $request->input('token', ''); + + if (! $storedToken || $storedToken !== $clientToken) { return response()->json([ 'status' => 'error', - 'message' => '您还没有抛竿,或者鱼已经跑了!', + 'message' => '鱼儿跑了!浮漂已超时或令牌无效,请重新抛竿。', ], 422); } - // 清除钓鱼标记 - Redis::del($activeKey); + // 清除 token(一次性) + Redis::del($tokenKey); // 2. 设置冷却时间 $cooldown = (int) Sysparam::getValue('fishing_cooldown', '300'); @@ -123,7 +152,7 @@ class FishingController extends Controller // 3. 随机决定钓鱼结果 $result = $this->randomFishResult(); - // 4. 通过统一积分服务更新经验和金币,写入流水 + // 4. 通过统一积分服务更新经验和金币 $expMul = $this->vipService->getExpMultiplier($user); $jjbMul = $this->vipService->getJjbMultiplier($user); if ($result['exp'] !== 0) { @@ -140,7 +169,7 @@ class FishingController extends Controller "钓鱼收竿:{$result['message']}", $id, ); } - $user->refresh(); // 刷新获取最新余额 + $user->refresh(); // 5. 广播钓鱼结果到聊天室 $sysMsg = [ @@ -175,15 +204,6 @@ class FishingController extends Controller { $roll = rand(1, 100); - // 概率分布(总计 100%) - // 1-15: 大鲨鱼 (+100exp, +20金) - // 16-30: 娃娃鱼 (+0exp, +30金) - // 31-50: 大草鱼 (+50exp) - // 51-70: 小鲤鱼 (+50exp, +10金) - // 71-85: 落水 (-50exp) - // 86-95: 被打 (-20exp, -3金) - // 96-100:大丰收 (+150exp, +50金) - return match (true) { $roll <= 15 => [ 'emoji' => '🦈', diff --git a/app/Http/Controllers/ShopController.php b/app/Http/Controllers/ShopController.php index f7c17fb..a114cf2 100644 --- a/app/Http/Controllers/ShopController.php +++ b/app/Http/Controllers/ShopController.php @@ -42,6 +42,7 @@ class ShopController extends Controller 'price' => $item->price, 'type' => $item->type, 'duration_days' => $item->duration_days, + 'duration_minutes' => $item->duration_minutes, 'intimacy_bonus' => $item->intimacy_bonus, 'charm_bonus' => $item->charm_bonus, ]); @@ -61,7 +62,8 @@ class ShopController extends Controller 'user_jjb' => $user->jjb ?? 0, 'active_week_effect' => $this->shopService->getActiveWeekEffect($user), 'has_rename_card' => $this->shopService->hasRenameCard($user), - 'ring_counts' => $ringCounts, // [item_id => qty] + 'ring_counts' => $ringCounts, + 'auto_fishing_minutes_left' => $this->shopService->getActiveAutoFishingMinutesLeft($user), ]); } diff --git a/app/Models/ShopItem.php b/app/Models/ShopItem.php index c8d7745..501779a 100644 --- a/app/Models/ShopItem.php +++ b/app/Models/ShopItem.php @@ -17,7 +17,8 @@ class ShopItem extends Model protected $fillable = [ 'name', 'slug', 'description', 'icon', 'price', - 'type', 'duration_days', 'sort_order', 'is_active', + 'type', 'duration_days', 'duration_minutes', 'sort_order', 'is_active', + 'intimacy_bonus', 'charm_bonus', ]; protected $casts = [ @@ -32,6 +33,14 @@ class ShopItem extends Model return $this->hasMany(UserPurchase::class); } + /** + * 是否为自动钓鱼卡 + */ + public function isAutoFishingCard(): bool + { + return $this->type === 'auto_fishing'; + } + /** * 是否为特效类商品(instant 或 duration,slug 以 once_ 或 week_ 开头) */ diff --git a/app/Services/ShopService.php b/app/Services/ShopService.php index 5dbb74d..05e7adb 100644 --- a/app/Services/ShopService.php +++ b/app/Services/ShopService.php @@ -33,6 +33,7 @@ class ShopService 'duration' => $this->buyWeekCard($user, $item), 'one_time' => $this->buyRenameCard($user, $item), 'ring' => $this->buyRing($user, $item), + 'auto_fishing' => $this->buyAutoFishingCard($user, $item), default => ['ok' => false, 'message' => '未知商品类型'], }; } @@ -245,4 +246,57 @@ class ShopService ->whereHas('shopItem', fn ($q) => $q->where('slug', 'rename_card')) ->exists(); } + + /** + * 购买自动钓鱼卡:手刺金币,写入 active 记录,到期时间 = 现在 + duration_minutes。 + * + * @return array{ok:bool, message:string} + */ + public function buyAutoFishingCard(User $user, ShopItem $item): array + { + $minutes = (int) $item->duration_minutes; + if ($minutes <= 0) { + return ['ok' => false, 'message' => '该钓鱼卡配置异常,请联系管理员。']; + } + + DB::transaction(function () use ($user, $item, $minutes): void { + // 手刺金币 + $user->decrement('jjb', $item->price); + // 写入背包(active,刻起计时) + UserPurchase::create([ + 'user_id' => $user->id, + 'shop_item_id' => $item->id, + 'status' => 'active', + 'price_paid' => $item->price, + 'expires_at' => Carbon::now()->addMinutes($minutes), + ]); + }); + + $hours = round($minutes / 60, 1); + + return [ + 'ok' => true, + 'message' => "🎣 {$item->name}购买成功!{$hours}小时内鬼鱼自动收篼,尽情摆烂!", + ]; + } + + /** + * 获取用户当前有效的自动钓鱼卡剩余分钟数(没有则返回 0) + */ + public function getActiveAutoFishingMinutesLeft(User $user): int + { + $purchase = UserPurchase::where('user_id', $user->id) + ->where('status', 'active') + ->whereNotNull('expires_at') + ->whereHas('shopItem', fn ($q) => $q->where('type', 'auto_fishing')) + ->where('expires_at', '>', Carbon::now()) + ->orderByDesc('expires_at') // 取最晚过期的 + ->first(); + + if (! $purchase) { + return 0; + } + + return (int) Carbon::now()->diffInMinutes($purchase->expires_at, false); + } } diff --git a/database/migrations/2026_03_01_161315_add_auto_fishing_card_type_to_shop_items_table.php b/database/migrations/2026_03_01_161315_add_auto_fishing_card_type_to_shop_items_table.php new file mode 100644 index 0000000..faf731b --- /dev/null +++ b/database/migrations/2026_03_01_161315_add_auto_fishing_card_type_to_shop_items_table.php @@ -0,0 +1,31 @@ +unsignedSmallInteger('duration_minutes')->default(0)->after('duration_days')->comment('道具有效时长(分钟),0=不适用'); + }); + } + + /** + * 回滚:删除字段。 + */ + public function down(): void + { + Schema::table('shop_items', function (Blueprint $table): void { + $table->dropColumn('duration_minutes'); + }); + } +}; diff --git a/database/seeders/AutoFishingCardSeeder.php b/database/seeders/AutoFishingCardSeeder.php new file mode 100644 index 0000000..9230ce9 --- /dev/null +++ b/database/seeders/AutoFishingCardSeeder.php @@ -0,0 +1,68 @@ + 'auto_fishing_2h', + 'name' => '自动钓鱼卡(2小时)', + 'icon' => '🎣', + 'description' => '激活后2小时内,钓鱼无需手动点击浮漂,系统自动收竿。', + 'price' => 800, + 'type' => 'auto_fishing', + 'duration_minutes' => 120, + 'sort_order' => 201, + 'is_active' => true, + ], + [ + 'slug' => 'auto_fishing_8h', + 'name' => '自动钓鱼卡(8小时)', + 'icon' => '🎣', + 'description' => '激活后8小时内,钓鱼无需手动点击浮漂,系统自动收竿。超值之选!', + 'price' => 2500, + 'type' => 'auto_fishing', + 'duration_minutes' => 480, + 'sort_order' => 202, + 'is_active' => true, + ], + [ + 'slug' => 'auto_fishing_24h', + 'name' => '自动钓鱼卡(24小时)', + 'icon' => '🎣', + 'description' => '激活后24小时内,钓鱼无需手动点击浮漂,系统自动收竿。重度钓鱼爱好者必备!', + 'price' => 6000, + 'type' => 'auto_fishing', + 'duration_minutes' => 1440, + 'sort_order' => 203, + 'is_active' => true, + ], + ]; + + foreach ($cards as $card) { + ShopItem::updateOrCreate( + ['slug' => $card['slug']], + $card + ); + } + + $this->command->info('✅ 3 张自动钓鱼卡已写入 shop_items。'); + } +} diff --git a/resources/views/chat/partials/scripts.blade.php b/resources/views/chat/partials/scripts.blade.php index 5dc5f2b..da3394e 100644 --- a/resources/views/chat/partials/scripts.blade.php +++ b/resources/views/chat/partials/scripts.blade.php @@ -1619,16 +1619,74 @@ btn.disabled = false; btn.textContent = '确定更换'; } - // ── 钓鱼小游戏(复刻原版 diaoyu/ 功能)───────────── + // ── 钓鱼小游戏(随机浮漂版)───────────────────────── let fishingTimer = null; let fishingReelTimeout = null; + let _fishToken = null; // 当次钓鱼的 token /** - * 开始钓鱼 — 调用抛竿 API,花费金币,显示等待动画 + * 创建浮漂 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 0.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, { @@ -1643,85 +1701,124 @@ if (!res.ok || data.status !== 'success') { window.chatDialog.alert(data.message || '钓鱼失败', '操作失败', '#cc4444'); btn.disabled = false; + btn.textContent = '🎣 钓鱼'; return; } - // 在包厢窗口显示抛竿消息 - const now = new Date(); - const timeStr = now.getHours().toString().padStart(2, '0') + ':' + - now.getMinutes().toString().padStart(2, '0') + ':' + - now.getSeconds().toString().padStart(2, '0'); + // 保存本次 token(收竿时提交) + _fishToken = data.token; + // 聊天框提示 const castDiv = document.createElement('div'); castDiv.className = 'msg-line'; + const timeStr = new Date().toLocaleTimeString('zh-CN', { + hour12: false + }); castDiv.innerHTML = - `🎣【钓鱼】${data.message}(${timeStr})`; + `🎣【钓鱼】${data.message}(${timeStr})`; 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'; - hookDiv.innerHTML = - '🐟 鱼上钩了!快点击 [拉竿] 按钮!'; - container2.appendChild(hookDiv); - if (autoScroll) container2.scrollTop = container2.scrollHeight; - btn.textContent = '🎣 拉竿!'; - btn.disabled = false; - btn.onclick = reelFish; - - // 15 秒内不拉竿,鱼跑掉 - fishingReelTimeout = setTimeout(() => { - const missDiv = document.createElement('div'); - missDiv.className = 'msg-line'; - missDiv.innerHTML = - '💨 你反应太慢了,鱼跑掉了...'; - container2.appendChild(missDiv); + if (data.auto_fishing) { + // 自动钓鱼卡:在动画结束后自动收竿 + hookDiv.innerHTML = + `🎣 自动钓鱼卡生效!自动收竿中... (剩余${data.auto_fishing_minutes_left}分钟)`; + container2.appendChild(hookDiv); if (autoScroll) container2.scrollTop = container2.scrollHeight; - resetFishingBtn(); - }, 15000); + // 500ms 后自动收竿(等动画) + fishingReelTimeout = setTimeout(() => { + removeBobber(); + reelFish(); + }, 600); + } else { + // 手动模式:玩家需在 8 秒内点击浮漂 + hookDiv.innerHTML = + `🐟 鱼上钩了!快点击屏幕上的浮漂!`; + 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 = '💨 你反应太慢了,鱼跑掉了...'; + 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 = '🎣 钓鱼'; } } /** - * 拉竿 — 调用收竿 API,获取随机结果 + * 收竿 — 提交 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' - } + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + token + }) }); const data = await res.json(); - - const now = new Date(); - const timeStr = now.getHours().toString().padStart(2, '0') + ':' + - now.getMinutes().toString().padStart(2, '0') + ':' + - now.getSeconds().toString().padStart(2, '0'); + const timeStr = new Date().toLocaleTimeString('zh-CN', { + hour12: false + }); if (res.ok && data.status === 'success') { const r = data.result; @@ -1729,15 +1826,15 @@ const resultDiv = document.createElement('div'); resultDiv.className = 'msg-line'; resultDiv.innerHTML = - `${r.emoji}【钓鱼结果】${r.message}` + - ` (当前经验:${data.exp_num} 金币:${data.jjb})` + + `${r.emoji}【钓鱼结果】${r.message}` + + ` (经验:${data.exp_num} 金币:${data.jjb})` + `(${timeStr})`; container2.appendChild(resultDiv); } else { const errDiv = document.createElement('div'); errDiv.className = 'msg-line'; errDiv.innerHTML = - `【钓鱼】${data.message || '操作失败'}(${timeStr})`; + `【钓鱼】${data.message || '操作失败'}(${timeStr})`; container2.appendChild(errDiv); } if (autoScroll) container2.scrollTop = container2.scrollHeight; @@ -1758,6 +1855,7 @@ btn.onclick = startFishing; fishingTimer = null; fishingReelTimeout = null; + removeBobber(); } // ── AI 聊天机器人 ────────────────────────────────── diff --git a/resources/views/chat/partials/toolbar.blade.php b/resources/views/chat/partials/toolbar.blade.php index 6e2605a..6f6956e 100644 --- a/resources/views/chat/partials/toolbar.blade.php +++ b/resources/views/chat/partials/toolbar.blade.php @@ -799,6 +799,11 @@ desc: '存入背包,求婚时消耗(被拒则遗失)', type: 'ring' }, + { + label: '🎣 自动钓鱼卡', + desc: '激活后自动收篼,无需手动点击浮漂', + type: 'auto_fishing' + }, { label: '🎭 道具', desc: '', @@ -828,13 +833,30 @@ const card = document.createElement('div'); card.className = 'shop-card'; - // 顶部:图标 + 名称(戒指加持有数徽章) - const iconHtml = isRing && ownedQty > 0 ? - ` + // 顶部:图标 + 名称(戒指/自动钓鱼卡加徽章) + const isAutoFishing = item.type === 'auto_fishing'; + const autoFishLeft = isAutoFishing ? (data.auto_fishing_minutes_left || 0) : 0; + + let iconHtml; + if (isRing && ownedQty > 0) { + iconHtml = ` ${item.icon} ${ownedQty} - ` : - `${item.icon}`; + `; + } else if (isAutoFishing && autoFishLeft > 0) { + const hLeft = autoFishLeft >= 60 ? Math.floor(autoFishLeft / 60) + 'h' : + autoFishLeft + 'm'; + iconHtml = ` + ${item.icon} + ${hLeft} + `; + } else { + iconHtml = `${item.icon}`; + } + + const durationLabel = isAutoFishing && item.duration_minutes > 0 ? + `
⏱ 有效期 ${item.duration_minutes >= 60 ? Math.floor(item.duration_minutes / 60) + ' 小时' : item.duration_minutes + ' 分钟'}
` : + ''; card.innerHTML = `
@@ -847,6 +869,7 @@ ${item.intimacy_bonus > 0 ? `💞 亲密 +${item.intimacy_bonus}` : ''} ${item.charm_bonus > 0 ? `✨ 魅力 +${item.charm_bonus}` : ''}
` : ''} + ${durationLabel} `; // 按钮