diff --git a/.agents/rules/chat.md b/.agents/rules/chat.md index ce96df9..d4ec8a5 100644 --- a/.agents/rules/chat.md +++ b/.agents/rules/chat.md @@ -144,3 +144,7 @@ class ChatStateService } } ``` + +### 2.5 迁移文件注意事项 + +同时新建多个迁移文件时,要注意 是否有关联主键问题,主键所在表要先创建,所以迁移文件名称 要比被调用表文件名的靠前,否则执行迁移时会报错; diff --git a/GAMES_TODO.md b/GAMES_TODO.md index bdc5415..a2639b5 100644 --- a/GAMES_TODO.md +++ b/GAMES_TODO.md @@ -1,6 +1,6 @@ # 🎮 聊天室游戏开发进度 -> 更新时间:2026-03-01 +> 更新时间:2026-03-03 --- @@ -30,29 +30,23 @@ - **货币来源**:`CurrencySource::SLOT_SPIN` / `SLOT_WIN` / `SLOT_CURSE` - **后台配置**:`game_configs` 表,可配置每次消耗/每日次数上限/各赔率 ---- - -## 🕐 待开发(明天继续) - ### 📦 神秘箱子(Mystery Box) -**核心玩法**:系统定时或管理员手动投放神秘箱,最快发送暗号的用户开箱获奖 - -**待开发清单:** - -- [ ] 数据库:`mystery_boxes`(箱子记录)+ `mystery_box_claims`(领取日志) -- [ ] 模型:`MysteryBox` / `MysteryBoxClaim` -- [ ] 队列 Job:`DropMysteryBoxJob`(投放箱子 + 公屏广播暗号 + 定时关闭) -- [ ] 控制器:`MysteryBoxController`(`/mystery-box/claim` 领取接口) -- [ ] 调度器:`routes/console.php` 按配置间隔自动投放 -- [ ] 后台:管理员可手动投放(管理员面板新增"投放箱子"按钮) -- [ ] 前端:无需弹窗,用户直接在聊天框发送**暗号**(系统给的口令)领取 -- [ ] 货币来源:`CurrencySource::MYSTERY_BOX` -- [ ] 特殊类型:普通箱(500~2000)/ 稀有箱(5000~20000)/ 黑化箱(陷阱,倒扣) -- [ ] 配置参数:`auto_drop_enabled` / `auto_interval_hours` / `claim_window_seconds` / 各奖励范围 / `trap_chance_percent` +- **类型**:系统定时自动投放 + 管理员手动投放(即时广播暗号,先到先得) +- **数据库**:`mystery_boxes`(箱子记录)+ `mystery_box_claims`(领取日志) +- **模型**:`MysteryBox` / `MysteryBoxClaim` +- **队列 Job**:`DropMysteryBoxJob`(投放 + 公屏广播暗号 + 派发 ExpireJob)/ `ExpireMysteryBoxJob`(到期处理) +- **控制器**:`MysteryBoxController`(`/mystery-box/status` 状态查询 / `/mystery-box/claim` 领取) +- **前端**:`chat/partials/mystery-box.blade.php`(5秒轮询检测 + 可拖动FAB + 快捷输入面板) +- **领取方式**:① 聊天框直接输入暗号发送(前端拦截,不发普通消息)② 点击悬浮FAB打开面板输入 +- **箱子类型**:普通箱(500\~2000金)/ 稀有箱(5000\~20000金)/ 黑化箱(陷阱,倒扣200\~1000金) +- **货币来源**:`CurrencySource::MYSTERY_BOX` / `MYSTERY_BOX_TRAP`(含 `room_id` 流水记录) +- **后台配置**:`game_configs` 表,可配置开关/自动投放间隔/各奖励范围/陷阱概率;支持手动投放三种类型 --- +## 🕐 待开发 + ### 🐎 赛马竞猜(Horse Racing) **核心玩法**:定时举办赛马,用户押注马匹,按注池赔率结算,跑马过程 WebSocket 实时播报 @@ -98,7 +92,7 @@ --- -## 🔧 今日已修复的 Bug +## 🔧 已修复的 Bug 1. **百家乐广播频道**:`Channel` → `PresenceChannel`,解决前端收不到 WebSocket 事件 2. **百家乐余额检查**:`$user->gold` → `$user->jjb`(字段名错误) @@ -106,3 +100,8 @@ 4. **老虎机FAB**:支持拖动 + localStorage 位置持久化 5. **星海小博士随机事件**:改走 `UserCurrencyService.change()`,补写流水日志 6. **百家乐结算UI**:骰子改数字方块(跨平台);中奖/未中奖卡片重设计 +7. **全部 FAB 拖动统一**:百家乐 FAB 改为 Alpine.js `baccaratFab()` 组件,与老虎机 `slotFab()` 完全一致,位置持久化存 localStorage +8. **Alpine.js 初始化顺序**:`frame.blade.php` 中 Alpine CDN 补加 `defer`,解决所有组件 `is not defined` 错误 +9. **神秘箱子暗号领取**:改为主动尝试模式(不依赖5秒轮询),聊天框输入暗号即可触发领取;`claim()` 暗号统一转大写 +10. **神秘箱子流水记录**:`change()` 调用补上 `room_id` 参数,确保积分统计页面可按房间筛选 +11. **后台弹窗**:游戏管理页所有 `alert/confirm` 替换为全局 `window.adminDialog`(毛玻璃弹窗) diff --git a/app/Enums/CurrencySource.php b/app/Enums/CurrencySource.php index a570fd2..9433487 100644 --- a/app/Enums/CurrencySource.php +++ b/app/Enums/CurrencySource.php @@ -96,6 +96,12 @@ enum CurrencySource: string /** 领取礼包红包——经验(用户抢到经验礼包时收入) */ case RED_PACKET_RECV_EXP = 'red_packet_recv_exp'; + /** 神秘箱子——领取奖励(普通箱/稀有箱,正数金币) */ + case MYSTERY_BOX = 'mystery_box'; + + /** 神秘箱子——黑化陷阱(倒扣金币,负数) */ + case MYSTERY_BOX_TRAP = 'mystery_box_trap'; + /** * 返回该来源的中文名称,用于后台统计展示。 */ @@ -127,6 +133,8 @@ enum CurrencySource: string self::SLOT_CURSE => '老虎机诅咒', self::RED_PACKET_RECV => '领取礼包红包(金币)', self::RED_PACKET_RECV_EXP => '领取礼包红包(经验)', + self::MYSTERY_BOX => '神秘箱子奖励', + self::MYSTERY_BOX_TRAP => '神秘箱子陷阱', }; } } diff --git a/app/Http/Controllers/Admin/GameConfigController.php b/app/Http/Controllers/Admin/GameConfigController.php index 0f35ed5..70813d8 100644 --- a/app/Http/Controllers/Admin/GameConfigController.php +++ b/app/Http/Controllers/Admin/GameConfigController.php @@ -69,4 +69,36 @@ class GameConfigController extends Controller return back()->with('success', "「{$gameConfig->name}」参数已保存!"); } + + /** + * 管理员手动投放神秘箱子。 + * + * 立即分发 DropMysteryBoxJob 到队列,由 Horizon 执行箱子投放和公屏广播。 + */ + public function dropMysteryBox(Request $request): JsonResponse + { + if (! \App\Models\GameConfig::isEnabled('mystery_box')) { + return response()->json(['ok' => false, 'message' => '神秘箱子功能未开放,请先开启。']); + } + + $boxType = $request->input('box_type', 'normal'); + + if (! in_array($boxType, ['normal', 'rare', 'trap'], true)) { + return response()->json(['ok' => false, 'message' => '无效的箱子类型。']); + } + + // 检查是否有正在开放的箱子(避免同时多个) + if (\App\Models\MysteryBox::currentOpenBox()) { + return response()->json(['ok' => false, 'message' => '当前已有一个神秘箱子正在等待领取,请等它结束后再投放。']); + } + + \App\Jobs\DropMysteryBoxJob::dispatch($boxType, 1, null, (int) auth()->id()); + + $typeNames = ['normal' => '普通箱', 'rare' => '稀有箱', 'trap' => '黑化箱']; + + return response()->json([ + 'ok' => true, + 'message' => "✅ 已投放「{$typeNames[$boxType]}」到 #1 房间,暗号将实时发送到公屏!", + ]); + } } diff --git a/app/Http/Controllers/MysteryBoxController.php b/app/Http/Controllers/MysteryBoxController.php new file mode 100644 index 0000000..36e7399 --- /dev/null +++ b/app/Http/Controllers/MysteryBoxController.php @@ -0,0 +1,174 @@ +json(['active' => false]); + } + + $box = MysteryBox::currentOpenBox(); + + if (! $box) { + return response()->json(['active' => false]); + } + + // 计算剩余时间 + $secondsLeft = $box->expires_at ? max(0, now()->diffInSeconds($box->expires_at, false)) : null; + + return response()->json([ + 'active' => true, + 'box_id' => $box->id, + 'box_type' => $box->box_type, + 'type_name' => $box->typeName(), + 'type_emoji' => $box->typeEmoji(), + 'passcode' => $box->passcode, + 'seconds_left' => $secondsLeft, + ]); + } + + /** + * 用户用暗号领取箱子。 + */ + public function claim(Request $request): JsonResponse + { + if (! GameConfig::isEnabled('mystery_box')) { + return response()->json(['ok' => false, 'message' => '神秘箱子功能未开放。']); + } + + $passcode = strtoupper(trim((string) $request->input('passcode', ''))); + + if ($passcode === '') { + return response()->json(['ok' => false, 'message' => '请输入暗号。']); + } + + $user = $request->user(); + + return DB::transaction(function () use ($user, $passcode): JsonResponse { + // 查找匹配暗号的可领取箱子(加锁防并发) + $box = MysteryBox::query() + ->where('passcode', $passcode) + ->where('status', 'open') + ->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now())) + ->lockForUpdate() + ->first(); + + if (! $box) { + return response()->json(['ok' => false, 'message' => '暗号不正确,或箱子已被领走/已过期。']); + } + + // ① 随机奖励金额 + $reward = $box->rollReward(); + + // ② 货币变更 + $source = $reward >= 0 ? CurrencySource::MYSTERY_BOX : CurrencySource::MYSTERY_BOX_TRAP; + $remark = $reward >= 0 + ? "神秘箱子【{$box->typeName()}】奖励" + : "神秘箱子【黑化箱】陷阱扣除"; + + $this->currency->change($user, 'gold', $reward, $source, $remark, $box->room_id); + + // ③ 写领取记录 + 更新箱子状态 + MysteryBoxClaim::create([ + 'mystery_box_id' => $box->id, + 'user_id' => $user->id, + 'reward_amount' => $reward, + ]); + + $box->update(['status' => 'claimed']); + + // ④ 公屏广播结果 + $user->refresh(); + $this->broadcastResult($box, $user->username, $reward); + + return response()->json([ + 'ok' => true, + 'reward' => $reward, + 'balance' => $user->jjb ?? 0, + 'message' => $reward >= 0 + ? "🎉 恭喜!开箱获得 +{$reward} 金币!" + : "☠️ 中了黑化陷阱!扣除 " . abs($reward) . ' 金币!', + ]); + }); + } + + /** + * 公屏广播开箱结果。 + * + * @param MysteryBox $box 箱子实例 + * @param string $username 领取者用户名 + * @param int $reward 奖励金额(正/负) + */ + private function broadcastResult(MysteryBox $box, string $username, int $reward): void + { + $emoji = $box->typeEmoji(); + $typeName = $box->typeName(); + + if ($reward >= 0) { + $content = "{$emoji}【开箱播报】恭喜 【{$username}】 抢到了神秘{$typeName}!" + . "获得 🪙" . number_format($reward) . " 金币!"; + $color = $box->box_type === 'rare' ? '#c4b5fd' : '#34d399'; + } else { + $content = "☠️【黑化陷阱】haha!【{$username}】 中了神秘黑化箱的陷阱!" + . "被扣除 🪙" . number_format(abs($reward)) . " 金币!点背~"; + $color = '#f87171'; + } + + $msg = [ + 'id' => $this->chatState->nextMessageId(1), + 'room_id' => 1, + 'from_user' => '系统传音', + 'to_user' => '大家', + 'content' => $content, + 'is_secret' => false, + 'font_color' => $color, + 'action' => '大声宣告', + 'sent_at' => now()->toDateTimeString(), + ]; + + $this->chatState->pushMessage(1, $msg); + broadcast(new MessageSent(1, $msg)); + SaveMessageJob::dispatch($msg); + } +} diff --git a/app/Jobs/DropMysteryBoxJob.php b/app/Jobs/DropMysteryBoxJob.php new file mode 100644 index 0000000..41ae363 --- /dev/null +++ b/app/Jobs/DropMysteryBoxJob.php @@ -0,0 +1,122 @@ +params ?? []; + $claimWindow = (int) ($config['claim_window_seconds'] ?? 120); + $targetRoom = $this->roomId ?? 1; + + // 自动生成随机暗号(若未指定) + $passcode = $this->passcode ?? strtoupper(Str::random(6)); + + // 根据类型确定奖励范围 + [$rewardMin, $rewardMax] = match ($this->boxType) { + 'rare' => [ + (int) ($config['rare_reward_min'] ?? 5000), + (int) ($config['rare_reward_max'] ?? 20000), + ], + 'trap' => [ + (int) ($config['trap_penalty_min'] ?? 200), + (int) ($config['trap_penalty_max'] ?? 1000), + ], + default => [ + (int) ($config['normal_reward_min'] ?? 500), + (int) ($config['normal_reward_max'] ?? 2000), + ], + }; + + // 创建箱子记录 + $box = MysteryBox::create([ + 'box_type' => $this->boxType, + 'passcode' => $passcode, + 'reward_min' => $rewardMin, + 'reward_max' => $rewardMax, + 'status' => 'open', + 'expires_at' => now()->addSeconds($claimWindow), + 'dropped_by' => $this->droppedBy, + ]); + + // 公屏广播暗号提示 + $emoji = $box->typeEmoji(); + $typeName = $box->typeName(); + $source = $this->droppedBy ? '管理员' : '系统'; + + $content = "{$emoji}【{$typeName}】{$source}投放了一个神秘箱子!" + . "发送暗号「{$passcode}」即可开箱!限时 {$claimWindow} 秒,先到先得!"; + + $msg = [ + 'id' => $chatState->nextMessageId($targetRoom), + 'room_id' => $targetRoom, + 'from_user' => '系统传音', + 'to_user' => '大家', + 'content' => $content, + 'is_secret' => false, + 'font_color' => match ($this->boxType) { + 'rare' => '#c4b5fd', + 'trap' => '#f87171', + default => '#34d399', + }, + 'action' => '大声宣告', + 'sent_at' => now()->toDateTimeString(), + ]; + + $chatState->pushMessage($targetRoom, $msg); + broadcast(new MessageSent($targetRoom, $msg)); + SaveMessageJob::dispatch($msg); + + // 定时关闭任务:到期后将箱子标记为 expired + ExpireMysteryBoxJob::dispatch($box->id)->delay(now()->addSeconds($claimWindow + 5)); + } +} diff --git a/app/Jobs/ExpireMysteryBoxJob.php b/app/Jobs/ExpireMysteryBoxJob.php new file mode 100644 index 0000000..a8d440a --- /dev/null +++ b/app/Jobs/ExpireMysteryBoxJob.php @@ -0,0 +1,66 @@ +boxId); + + // 箱子不存在或已经被领取/过期,跳过 + if (! $box || $box->status !== 'open') { + return; + } + + // 标记为过期 + $box->update(['status' => 'expired']); + + // 公屏广播过期通知 + $msg = [ + 'id' => $chatState->nextMessageId(1), + 'room_id' => 1, + 'from_user' => '系统传音', + 'to_user' => '大家', + 'content' => "⏰ 神秘箱子(暗号:{$box->passcode})已超时,箱子消失了!下次要快哦~", + 'is_secret' => false, + 'font_color' => '#9ca3af', + 'action' => '大声宣告', + 'sent_at' => now()->toDateTimeString(), + ]; + + $chatState->pushMessage(1, $msg); + broadcast(new MessageSent(1, $msg)); + SaveMessageJob::dispatch($msg); + } +} diff --git a/app/Models/MysteryBox.php b/app/Models/MysteryBox.php new file mode 100644 index 0000000..5dc49dc --- /dev/null +++ b/app/Models/MysteryBox.php @@ -0,0 +1,124 @@ + 'integer', + 'reward_max' => 'integer', + 'expires_at' => 'datetime', + ]; + } + + // ─── 关联关系 ──────────────────────────────────────────────────── + + /** + * 领取记录(一个箱子只能被一人领取,但关联为 HasOne) + */ + public function claim(): HasOne + { + return $this->hasOne(MysteryBoxClaim::class); + } + + /** + * 所有领取记录(逻辑上只有一条,保留 HasMany 供统计使用) + */ + public function claims(): HasMany + { + return $this->hasMany(MysteryBoxClaim::class); + } + + // ─── 查询作用域 ────────────────────────────────────────────────── + + /** + * 当前可领取(open 状态 + 未过期)的箱子。 + */ + public static function currentOpenBox(): ?static + { + return static::query() + ->where('status', 'open') + ->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now())) + ->latest() + ->first(); + } + + // ─── 工具方法 ──────────────────────────────────────────────────── + + /** + * 返回箱子类型的 emoji 前缀。 + */ + public function typeEmoji(): string + { + return match ($this->box_type) { + 'normal' => '📦', + 'rare' => '💎', + 'trap' => '☠️', + default => '📦', + }; + } + + /** + * 返回箱子类型中文名称。 + */ + public function typeName(): string + { + return match ($this->box_type) { + 'normal' => '普通箱', + 'rare' => '稀有箱', + 'trap' => '黑化箱', + default => '神秘箱', + }; + } + + /** + * 随机生成奖励金额(trap 类型为负数)。 + */ + public function rollReward(): int + { + $amount = random_int( + min(abs($this->reward_min), abs($this->reward_max)), + max(abs($this->reward_min), abs($this->reward_max)), + ); + + // trap 类型:倒扣金币(负数) + return $this->box_type === 'trap' ? -$amount : $amount; + } + + /** + * 判断箱子是否已过期 + */ + public function isExpired(): bool + { + return $this->expires_at !== null && $this->expires_at->isPast(); + } +} diff --git a/app/Models/MysteryBoxClaim.php b/app/Models/MysteryBoxClaim.php new file mode 100644 index 0000000..8d1b04b --- /dev/null +++ b/app/Models/MysteryBoxClaim.php @@ -0,0 +1,53 @@ + 'integer', + ]; + } + + // ─── 关联关系 ──────────────────────────────────────────────────── + + /** + * 关联神秘箱子。 + */ + public function mysteryBox(): BelongsTo + { + return $this->belongsTo(MysteryBox::class); + } + + /** + * 关联领取用户。 + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/database/migrations/2026_03_03_175451_create_mystery_boxes_table.php b/database/migrations/2026_03_03_175451_create_mystery_boxes_table.php new file mode 100644 index 0000000..4fa2843 --- /dev/null +++ b/database/migrations/2026_03_03_175451_create_mystery_boxes_table.php @@ -0,0 +1,59 @@ +id(); + + // 箱子类型:normal=普通(500~2000) / rare=稀有(5000~20000) / trap=黑化(陷阱,倒扣) + $table->enum('box_type', ['normal', 'rare', 'trap'])->default('normal')->comment('箱子类型'); + + // 领取暗号(管理员设置或自动生成,4~8位随机字串) + $table->string('passcode', 20)->comment('领取暗号'); + + // 奖励金额范围(trap 类型此字段为负值上下界) + $table->integer('reward_min')->default(500)->comment('最低奖励金币'); + $table->integer('reward_max')->default(2000)->comment('最高奖励金币'); + + // 状态:open=可领取 / claimed=已被领取 / expired=已过期 + $table->enum('status', ['open', 'claimed', 'expired'])->default('open')->comment('箱子状态'); + + // 领取截止时间(投放后 N 秒内有效) + $table->timestamp('expires_at')->nullable()->comment('过期时间'); + + // 由哪个管理员用户投放(null=系统自动投放) + $table->unsignedBigInteger('dropped_by')->nullable()->comment('投放者用户ID,null=系统自动'); + + $table->timestamps(); + + $table->index('status'); + $table->index('expires_at'); + }); + } + + /** + * 回滚:删除神秘箱子表。 + */ + public function down(): void + { + Schema::dropIfExists('mystery_boxes'); + } +}; diff --git a/database/migrations/2026_03_03_175452_create_mystery_box_claims_table.php b/database/migrations/2026_03_03_175452_create_mystery_box_claims_table.php new file mode 100644 index 0000000..0e5eeda --- /dev/null +++ b/database/migrations/2026_03_03_175452_create_mystery_box_claims_table.php @@ -0,0 +1,51 @@ +id(); + + // 关联箱子 + $table->unsignedBigInteger('mystery_box_id')->comment('关联神秘箱子ID'); + $table->foreign('mystery_box_id')->references('id')->on('mystery_boxes')->cascadeOnDelete(); + + // 领取用户 + $table->unsignedBigInteger('user_id')->comment('领取用户ID'); + $table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete(); + + // 实际奖励(正数=获得,负数=被扣) + $table->integer('reward_amount')->comment('实际奖励金额(负数表示扣除)'); + + $table->timestamps(); + + $table->index('mystery_box_id'); + $table->index('user_id'); + }); + } + + /** + * 回滚:删除领取记录表。 + */ + public function down(): void + { + Schema::dropIfExists('mystery_box_claims'); + } +}; diff --git a/database/seeders/GameConfigSeeder.php b/database/seeders/GameConfigSeeder.php index e913e57..c278abc 100644 --- a/database/seeders/GameConfigSeeder.php +++ b/database/seeders/GameConfigSeeder.php @@ -70,14 +70,16 @@ class GameConfigSeeder extends Seeder 'description' => '管理员随时投放或系统定时自动投放神秘箱,最快发送暗号的用户开箱获得奖励。', 'enabled' => false, 'params' => [ - 'auto_drop_enabled' => false, // 是否自动定时投放 - 'auto_interval_hours' => 2, // 自动投放间隔(小时) + 'auto_drop_enabled' => false, // 是否自动定时投放 + 'auto_interval_hours' => 2, // 自动投放间隔(小时) 'claim_window_seconds' => 60, // 领取窗口(秒) - 'min_reward' => 500, // 普通箱最低奖励 - 'max_reward' => 2000, // 普通箱最高奖励 - 'rare_min_reward' => 5000, // 稀有箱最低奖励 - 'rare_max_reward' => 20000, // 稀有箱最高奖励 - 'trap_chance_percent' => 10, // 黑化箱触发概率(%) + 'normal_reward_min' => 500, // 普通箱最低奖励 + 'normal_reward_max' => 2000, // 普通箱最高奖励 + 'rare_reward_min' => 5000, // 稀有箱最低奖励 + 'rare_reward_max' => 20000, // 稀有箱最高奖励 + 'trap_penalty_min' => 200, // 黑化箱最低惩罚 + 'trap_penalty_max' => 1000, // 黑化箱最高惩罚 + 'trap_chance_percent' => 10, // 黑化箱触发概率(%) ], ], diff --git a/resources/views/admin/game-configs/index.blade.php b/resources/views/admin/game-configs/index.blade.php index 0ceb3df..e81bd30 100644 --- a/resources/views/admin/game-configs/index.blade.php +++ b/resources/views/admin/game-configs/index.blade.php @@ -100,6 +100,31 @@ 修改后立即生效(缓存60秒刷新) + + {{-- 神秘箱子:手动投放区域 --}} + @if ($game->game_key === 'mystery_box') +
+
🎯 手动投放箱子
+
+ + + + 直接向 #1 房间投放,立即广播暗号 +
+
+ @endif @endforeach @@ -153,10 +178,58 @@ header.classList.toggle('bg-gray-50', !enabled); } - // Toast 提示 - alert(data.message); + // 全局弹窗提示 + window.adminDialog.alert(data.message, enabled ? '游戏已开启' : '游戏已关闭', enabled ? '✅' : '⏸'); }); } + + /** + * 管理员手动投放神秘箱子 + * + * @param {string} boxType 箱子类型:normal | rare | trap + */ + function dropBox(boxType) { + const typeNames = { + normal: '普通箱', + rare: '稀有箱', + trap: '黑化箱' + }; + const typeIcons = { + normal: '📦', + rare: '💎', + trap: '☠️' + }; + const name = typeNames[boxType] || boxType; + const icon = typeIcons[boxType] || '📦'; + + window.adminDialog.confirm( + `确定要向 #1 房间 投放一个「${name}」吗?
箱子投放后将立即在公屏广播暗号,用户限时领取。`, + `投放${name}`, + () => { + fetch('/admin/mystery-box/drop', { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': '{{ csrf_token() }}', + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify({ + box_type: boxType + }), + }) + .then(r => r.json()) + .then(data => { + window.adminDialog.alert( + data.message || (data.ok ? '投放成功!' : '投放失败'), + data.ok ? '投放成功' : '投放失败', + data.ok ? icon : '❌' + ); + }) + .catch(() => window.adminDialog.alert('网络错误,请重试', '网络错误', '🌐')); + }, + icon + ); + } @endsection @@ -200,6 +273,14 @@ 'auto_drop_enabled' => ['label' => '自动定时投放', 'type' => 'boolean', 'unit' => ''], 'auto_interval_hours' => ['label' => '自动投放间隔', 'type' => 'number', 'unit' => '小时', 'min' => 1], 'claim_window_seconds' => ['label' => '领取窗口', 'type' => 'number', 'unit' => '秒', 'min' => 10], + // 新键名 + 'normal_reward_min' => ['label' => '普通箱最低奖励', 'type' => 'number', 'unit' => '金币', 'min' => 1], + 'normal_reward_max' => ['label' => '普通箱最高奖励', 'type' => 'number', 'unit' => '金币', 'min' => 1], + 'rare_reward_min' => ['label' => '稀有箱最低奖励', 'type' => 'number', 'unit' => '金币', 'min' => 1], + 'rare_reward_max' => ['label' => '稀有箱最高奖励', 'type' => 'number', 'unit' => '金币', 'min' => 1], + 'trap_penalty_min' => ['label' => '黑化箱最低惩罚', 'type' => 'number', 'unit' => '金币', 'min' => 1], + 'trap_penalty_max' => ['label' => '黑化箱最高惩罚', 'type' => 'number', 'unit' => '金币', 'min' => 1], + // 旧键名兼容(数据库中已存在的旧配置) 'min_reward' => ['label' => '普通箱最低奖励', 'type' => 'number', 'unit' => '金币', 'min' => 1], 'max_reward' => ['label' => '普通箱最高奖励', 'type' => 'number', 'unit' => '金币', 'min' => 1], 'rare_min_reward' => ['label' => '稀有箱最低奖励', 'type' => 'number', 'unit' => '金币', 'min' => 1], diff --git a/resources/views/admin/layouts/app.blade.php b/resources/views/admin/layouts/app.blade.php index f24c5df..c81e307 100644 --- a/resources/views/admin/layouts/app.blade.php +++ b/resources/views/admin/layouts/app.blade.php @@ -175,6 +175,136 @@ @yield('content') + + {{-- ══════════════════════════════════════════════════════════ + 全局弹窗组件:window.adminDialog.alert / window.adminDialog.confirm + 用法: + window.adminDialog.alert('操作成功!', '✅ 提示'); + window.adminDialog.confirm('确定要删除?', '⚠️ 确认', () => { ... }); + ══════════════════════════════════════════════════════════ --}} + + + diff --git a/resources/views/chat/frame.blade.php b/resources/views/chat/frame.blade.php index 4a5f423..7f8f866 100644 --- a/resources/views/chat/frame.blade.php +++ b/resources/views/chat/frame.blade.php @@ -84,7 +84,7 @@ }; @vite(['resources/css/app.css', 'resources/js/app.js', 'resources/js/chat.js']) - + @@ -141,6 +141,8 @@ @include('chat.partials.baccarat-panel') {{-- ═══════════ 老虎机游戏面板 ═══════════ --}} @include('chat.partials.slot-machine') + {{-- ═══════════ 神秘箱子游戏面板 ═══════════ --}} + @include('chat.partials.mystery-box') {{-- 全屏特效系统:管理员烟花/下雨/雷电/下雪 --}} diff --git a/resources/views/chat/partials/baccarat-panel.blade.php b/resources/views/chat/partials/baccarat-panel.blade.php index 0affe2e..6a6c4df 100644 --- a/resources/views/chat/partials/baccarat-panel.blade.php +++ b/resources/views/chat/partials/baccarat-panel.blade.php @@ -295,130 +295,78 @@ -{{-- ─── 骰子悬浮入口(游戏开启时常驻,支持拖拽) ─── --}} -
- + :style="dragging ? 'cursor:grabbing;' : 'cursor:grab;'" title="百家乐下注中(可拖动)">🎲
+ + diff --git a/resources/views/chat/partials/scripts.blade.php b/resources/views/chat/partials/scripts.blade.php index b52d0e1..f31da80 100644 --- a/resources/views/chat/partials/scripts.blade.php +++ b/resources/views/chat/partials/scripts.blade.php @@ -1262,6 +1262,59 @@ return; } + // ── 神秘箱子暗号拦截 ──────────────────────────────────── + // 当用户输入内容符合暗号格式(4-8位大写字母/数字)时,主动尝试领取 + // 不依赖轮询标志,服务端负责校验是否真有活跃箱子及暗号是否匹配 + const passcodePattern = /^[A-Z0-9]{4,8}$/; + if (passcodePattern.test(content.trim())) { + _isSending = false; + + try { + const claimRes = await fetch('/mystery-box/claim', { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify({ + passcode: content.trim() + }), + }); + const claimData = await claimRes.json(); + + if (claimData.ok) { + // ✅ 领取成功:清空输入框,不发送普通消息 + contentInput.value = ''; + contentInput.focus(); + + // 清除活跃箱子全局标志 + window._mysteryBoxActive = false; + window._mysteryBoxPasscode = null; + + // 弹出开箱结果卡片 + const isPositive = (claimData.reward ?? 1) >= 0; + window.chatDialog?.alert( + claimData.message || '开箱成功!', + isPositive ? '🎉 恭喜!' : '☠️ 中了陷阱!', + isPositive ? '#10b981' : '#ef4444', + ); + + // 更新全局金币余额显示 + if (window.__chatUser && claimData.balance !== undefined) { + window.__chatUser.jjb = claimData.balance; + } + + return; + } + + // ❌ 领取失败(暗号错误 / 无活跃箱子 / 已被领走) + // 静默回退到正常发送——不弹错误提示,让消息正常发出 + } catch (_) { + // 网络错误时同样静默回退正常发送 + } + } + submitBtn.disabled = true; try { diff --git a/resources/views/chat/partials/toolbar.blade.php b/resources/views/chat/partials/toolbar.blade.php index 92321c9..7f7edba 100644 --- a/resources/views/chat/partials/toolbar.blade.php +++ b/resources/views/chat/partials/toolbar.blade.php @@ -19,7 +19,7 @@
存点
娱乐
银行
-
呼叫
+
婚姻
好友
头像
设置 @@ -1051,3 +1051,280 @@ } })(); + +{{-- ═══════════ 婚姻状态弹窗 ═══════════ --}} + + + diff --git a/routes/console.php b/routes/console.php index 71c417e..6ce9a6a 100644 --- a/routes/console.php +++ b/routes/console.php @@ -61,3 +61,45 @@ Schedule::call(function () { \App\Jobs\OpenBaccaratRoundJob::dispatch(); } })->everyMinute()->name('baccarat:open-round')->withoutOverlapping(); + +// ──────────── 神秘箱子定时投放 ───────────────────────────────── + +// 每分钟:检查是否应自动投放一个新箱子 +Schedule::call(function () { + if (! \App\Models\GameConfig::isEnabled('mystery_box')) { + return; + } + + $config = \App\Models\GameConfig::forGame('mystery_box')?->params ?? []; + + // 自动投放开关 + if (! ($config['auto_drop_enabled'] ?? false)) { + return; + } + + // 当前已有可领取的箱子时跳过(一次只投放一个) + if (\App\Models\MysteryBox::currentOpenBox()) { + return; + } + + $intervalHours = (float) ($config['auto_interval_hours'] ?? 2); + + // 检查距上次投放时间 + $lastBox = \App\Models\MysteryBox::latest()->first(); + if ($lastBox && $lastBox->created_at->diffInHours(now()) < $intervalHours) { + return; + } + + // 按配置的陷阱概率决定箱子类型 + $trapChance = (int) ($config['trap_chance_percent'] ?? 10); + $rand = random_int(1, 100); + + $boxType = match (true) { + $rand <= $trapChance => 'trap', + $rand <= $trapChance + 15 => 'rare', + default => 'normal', + }; + + \App\Jobs\DropMysteryBoxJob::dispatch($boxType); +})->everyMinute()->name('mystery-box:auto-drop')->withoutOverlapping(); + diff --git a/routes/web.php b/routes/web.php index 49790aa..4c25053 100644 --- a/routes/web.php +++ b/routes/web.php @@ -132,6 +132,14 @@ Route::middleware(['chat.auth'])->group(function () { Route::get('/history', [\App\Http\Controllers\SlotMachineController::class, 'history'])->name('history'); }); + // ── 神秘箱子(前台)────────────────────────────────────────────── + Route::prefix('mystery-box')->name('mystery-box.')->group(function () { + // 查询当前可领取的箱子(前端轮询/显示悬浮提示用) + Route::get('/status', [\App\Http\Controllers\MysteryBoxController::class, 'status'])->name('status'); + // 用户发送暗号领取箱子 + Route::post('/claim', [\App\Http\Controllers\MysteryBoxController::class, 'claim'])->name('claim'); + }); + // ---- 第五阶段:具体房间内部聊天核心 ---- // 进入具体房间界面的初始化 Route::get('/room/{id}', [ChatController::class, 'init'])->name('chat.room'); @@ -350,6 +358,9 @@ Route::middleware(['chat.auth', 'chat.has_position'])->prefix('admin')->name('ad Route::post('/{gameConfig}/params', [\App\Http\Controllers\Admin\GameConfigController::class, 'updateParams'])->name('params'); }); + // 📦 神秘箱子:管理员手动投放 + Route::post('/mystery-box/drop', [\App\Http\Controllers\Admin\GameConfigController::class, 'dropMysteryBox'])->name('mystery-box.drop'); + // 🎣 钓鱼事件管理 Route::prefix('fishing')->name('fishing.')->group(function () { Route::get('/', [\App\Http\Controllers\Admin\FishingEventController::class, 'index'])->name('index');