From 9359184e382b23f29bcc09541eb8167bfb78fdb6 Mon Sep 17 00:00:00 2001 From: lkddi Date: Sun, 1 Mar 2026 21:00:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=80=81=E8=99=8E=E6=9C=BA?= =?UTF-8?q?=E6=B8=B8=E6=88=8F=EF=BC=9A=E2=91=A0slot=5Fmachine=5Flogs?= =?UTF-8?q?=E8=A1=A8+=E6=A8=A1=E5=9E=8B(8=E7=A7=8D=E6=9D=83=E9=87=8D?= =?UTF-8?q?=E5=9B=BE=E6=A1=88/=E5=88=A4=E5=A5=96)=20=E2=91=A1SlotMachineCo?= =?UTF-8?q?ntroller(=E6=89=A3=E8=B4=B9/=E9=9A=8F=E6=9C=BA/=E8=B5=94?= =?UTF-8?q?=E4=BB=98/=E8=AF=85=E5=92=92/=E4=B8=897=E5=85=A8=E6=9C=8D?= =?UTF-8?q?=E5=B9=BF=E6=92=AD)=20=E2=91=A2=E5=89=8D=E5=8F=B0=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF(=E4=B8=89=E5=88=97=E6=BB=9A=E8=BD=AE=E5=8A=A8?= =?UTF-8?q?=E7=94=BB/=E9=80=90=E5=88=97=E5=81=9C=E6=AD=A2/=E8=B5=94?= =?UTF-8?q?=E7=8E=87=E8=AF=B4=E6=98=8E/=E5=8E=86=E5=8F=B2=E8=AE=B0?= =?UTF-8?q?=E5=BD=95)=20=E2=91=A3CurrencySource=E4=B8=89=E4=B8=AA=E6=9E=9A?= =?UTF-8?q?=E4=B8=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Enums/CurrencySource.php | 12 + .../Controllers/SlotMachineController.php | 249 ++++++++++ app/Models/SlotMachineLog.php | 129 +++++ ..._205542_create_slot_machine_logs_table.php | 43 ++ resources/views/chat/frame.blade.php | 2 + .../chat/partials/slot-machine.blade.php | 460 ++++++++++++++++++ routes/web.php | 13 +- 7 files changed, 905 insertions(+), 3 deletions(-) create mode 100644 app/Http/Controllers/SlotMachineController.php create mode 100644 app/Models/SlotMachineLog.php create mode 100644 database/migrations/2026_03_01_205542_create_slot_machine_logs_table.php create mode 100644 resources/views/chat/partials/slot-machine.blade.php diff --git a/app/Enums/CurrencySource.php b/app/Enums/CurrencySource.php index b841c71..2d545a0 100644 --- a/app/Enums/CurrencySource.php +++ b/app/Enums/CurrencySource.php @@ -81,6 +81,15 @@ enum CurrencySource: string /** 星海小博士随机事件(好运/坏运/经验/金币奖惩) */ case AUTO_EVENT = 'auto_event'; + /** 老虎机转动消耗金币 */ + case SLOT_SPIN = 'slot_spin'; + + /** 老虎机中奖赔付(含本金返还) */ + case SLOT_WIN = 'slot_win'; + + /** 老虎机诅咒额外扣除 */ + case SLOT_CURSE = 'slot_curse'; + /** * 返回该来源的中文名称,用于后台统计展示。 */ @@ -107,6 +116,9 @@ enum CurrencySource: string self::BACCARAT_BET => '百家乐下注', self::BACCARAT_WIN => '百家乐赢钱', self::AUTO_EVENT => '随机事件(星海小博士)', + self::SLOT_SPIN => '老虎机转动', + self::SLOT_WIN => '老虎机中奖', + self::SLOT_CURSE => '老虎机诅咒', }; } } diff --git a/app/Http/Controllers/SlotMachineController.php b/app/Http/Controllers/SlotMachineController.php new file mode 100644 index 0000000..6e70ae7 --- /dev/null +++ b/app/Http/Controllers/SlotMachineController.php @@ -0,0 +1,249 @@ +json(['enabled' => false]); + } + + $config = GameConfig::forGame('slot_machine')?->params ?? []; + $user = $request->user(); + $dailyLimit = (int) ($config['daily_limit'] ?? 0); + $usedToday = 0; + + if ($dailyLimit > 0) { + $usedToday = SlotMachineLog::query() + ->where('user_id', $user->id) + ->whereDate('created_at', today()) + ->count(); + } + + return response()->json([ + 'enabled' => true, + 'cost_per_spin' => (int) ($config['cost_per_spin'] ?? 100), + 'daily_limit' => $dailyLimit, + 'used_today' => $usedToday, + 'remaining' => $dailyLimit > 0 ? max(0, $dailyLimit - $usedToday) : null, + 'symbols' => collect(SlotMachineLog::symbols())->map(fn ($s) => $s['emoji']), + ]); + } + + /** + * 执行一次转动。 + * + * 流程:检查可玩 → 扣费 → 摇号 → 赔付 → 写日志 → 全服广播(三7)→ 返回结果 + */ + public function spin(Request $request): JsonResponse + { + if (! GameConfig::isEnabled('slot_machine')) { + return response()->json(['ok' => false, 'message' => '老虎机未开放。']); + } + + $config = GameConfig::forGame('slot_machine')?->params ?? []; + $cost = (int) ($config['cost_per_spin'] ?? 100); + $dailyLimit = (int) ($config['daily_limit'] ?? 0); + + $user = $request->user(); + + // 金币余额检查 + if (($user->jjb ?? 0) < $cost) { + return response()->json(['ok' => false, 'message' => "金币不足,每次转动需 {$cost} 金币。"]); + } + + // 每日次数限制检查 + if ($dailyLimit > 0) { + $usedToday = SlotMachineLog::query() + ->where('user_id', $user->id) + ->whereDate('created_at', today()) + ->count(); + + if ($usedToday >= $dailyLimit) { + return response()->json(['ok' => false, 'message' => "今日已转动 {$dailyLimit} 次,明日再来!"]); + } + } + + return DB::transaction(function () use ($user, $cost, $config): JsonResponse { + // ① 扣费 + $this->currency->change( + $user, + 'gold', + -$cost, + CurrencySource::SLOT_SPIN, + '老虎机转动消耗', + ); + + // ② 摇号 + $r1 = SlotMachineLog::randomSymbol(); + $r2 = SlotMachineLog::randomSymbol(); + $r3 = SlotMachineLog::randomSymbol(); + + $resultType = SlotMachineLog::judgeResult($r1, $r2, $r3); + $symbols = SlotMachineLog::symbols(); + + // ③ 计算赔付金额 + $payout = $this->calcPayout($resultType, $cost, $config); + + // ④ 赔付金币 + if ($payout > 0) { + $this->currency->change( + $user, + 'gold', + $payout, + CurrencySource::SLOT_WIN, + "老虎机 {$resultType} 中奖", + ); + } elseif ($resultType === 'curse' && ($config['curse_enabled'] ?? true)) { + // 诅咒:再扣一倍本金 + $cursePenalty = $cost; + if (($user->jjb ?? 0) >= $cursePenalty) { + $this->currency->change( + $user, + 'gold', + -$cursePenalty, + CurrencySource::SLOT_CURSE, + '老虎机三骷髅诅咒额外扣除', + ); + $payout = -$cursePenalty; // 净损失 = 本金+惩罚 + } + } + + // ⑤ 写游戏日志 + SlotMachineLog::create([ + 'user_id' => $user->id, + 'reel1' => $r1, + 'reel2' => $r2, + 'reel3' => $r3, + 'result_type' => $resultType, + 'cost' => $cost, + 'payout' => $payout, + ]); + + // ⑥ 三个7:全服公屏广播 + if ($resultType === 'jackpot') { + $this->broadcastJackpot($user->username, $payout, $cost); + } + + $user->refresh(); + + return response()->json([ + 'ok' => true, + 'reels' => [$r1, $r2, $r3], + 'emojis' => [ + $symbols[$r1]['emoji'], + $symbols[$r2]['emoji'], + $symbols[$r3]['emoji'], + ], + 'result_type' => $resultType, + 'result_label' => SlotMachineLog::resultLabel($resultType), + 'payout' => $payout, // 净变化(正=赢,负=额外亏) + 'net_change' => $payout - $cost, // 相对本金的盈亏(debug 用) + 'balance' => $user->jjb ?? 0, + ]); + }); + } + + /** + * 查询最近10条个人记录。 + */ + public function history(Request $request): JsonResponse + { + $logs = SlotMachineLog::query() + ->where('user_id', $request->user()->id) + ->orderByDesc('id') + ->limit(10) + ->get(['id', 'reel1', 'reel2', 'reel3', 'result_type', 'cost', 'payout', 'created_at']); + + $symbols = SlotMachineLog::symbols(); + + return response()->json([ + 'history' => $logs->map(fn ($l) => [ + 'emojis' => [$symbols[$l->reel1]['emoji'], $symbols[$l->reel2]['emoji'], $symbols[$l->reel3]['emoji']], + 'result_label' => SlotMachineLog::resultLabel($l->result_type), + 'payout' => $l->payout, + 'created_at' => $l->created_at->format('H:i'), + ]), + ]); + } + + /** + * 计算赔付金额(赢时返还 = 本金 × 赔率)。 + * + * @return int 正数=赢得金额(含本金返还),0=不赔付 + */ + private function calcPayout(string $resultType, int $cost, array $config): int + { + $multiplier = match ($resultType) { + 'jackpot' => (int) ($config['jackpot_payout'] ?? 100), + 'triple_gem' => (int) ($config['triple_payout'] ?? 50), + 'triple' => (int) ($config['same_payout'] ?? 10), + 'pair' => (int) ($config['pair_payout'] ?? 2), + default => 0, + }; + + return $multiplier > 0 ? $cost * $multiplier : 0; + } + + /** + * 三个7全服公屏广播。 + */ + private function broadcastJackpot(string $username, int $payout, int $cost): void + { + $net = $payout - $cost; + $content = "🎰🎉【老虎机大奖】恭喜 【{$username}】 转出三个7️⃣!" + .'狂揽 🪙'.number_format($net).' 金币!全服见证奇迹!'; + + $msg = [ + 'id' => $this->chatState->nextMessageId(1), + 'room_id' => 1, + 'from_user' => '系统传音', + 'to_user' => '大家', + 'content' => $content, + 'is_secret' => false, + 'font_color' => '#f59e0b', + 'action' => '大声宣告', + 'sent_at' => now()->toDateTimeString(), + ]; + + $this->chatState->pushMessage(1, $msg); + broadcast(new MessageSent(1, $msg)); + SaveMessageJob::dispatch($msg); + } +} diff --git a/app/Models/SlotMachineLog.php b/app/Models/SlotMachineLog.php new file mode 100644 index 0000000..0be10d3 --- /dev/null +++ b/app/Models/SlotMachineLog.php @@ -0,0 +1,129 @@ + 'integer', + 'payout' => 'integer', + ]; + } + + /** + * 关联玩家。 + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** + * 全部图案符号定义(key => emoji)。 + * 顺序影响权重:越靠前出现概率越高(摇奖时按权重随机)。 + * + * @return array + */ + public static function symbols(): array + { + return [ + 'cherry' => ['emoji' => '🍒', 'weight' => 30], + 'lemon' => ['emoji' => '🍋', 'weight' => 25], + 'orange' => ['emoji' => '🍊', 'weight' => 20], + 'grape' => ['emoji' => '🍇', 'weight' => 15], + 'bell' => ['emoji' => '🔔', 'weight' => 8], + 'gem' => ['emoji' => '💎', 'weight' => 4], + 'skull' => ['emoji' => '💀', 'weight' => 3], + 'seven' => ['emoji' => '7️⃣', 'weight' => 1], + ]; + } + + /** + * 按权重随机抽取一个图案 key。 + */ + public static function randomSymbol(): string + { + $symbols = static::symbols(); + $totalWeight = array_sum(array_column($symbols, 'weight')); + $rand = random_int(1, $totalWeight); + $cumulative = 0; + + foreach ($symbols as $key => $item) { + $cumulative += $item['weight']; + if ($rand <= $cumulative) { + return $key; + } + } + + return 'cherry'; // fallback + } + + /** + * 根据三列图案判断结果类型。 + * + * @return string 'jackpot' | 'triple_gem' | 'triple' | 'pair' | 'curse' | 'miss' + */ + public static function judgeResult(string $r1, string $r2, string $r3): string + { + // 三个7 → 大奖 + if ($r1 === 'seven' && $r2 === 'seven' && $r3 === 'seven') { + return 'jackpot'; + } + // 三个💎 → 豪华奖 + if ($r1 === 'gem' && $r2 === 'gem' && $r3 === 'gem') { + return 'triple_gem'; + } + // 三个💀 → 诅咒(亏双倍) + if ($r1 === 'skull' && $r2 === 'skull' && $r3 === 'skull') { + return 'curse'; + } + // 三同(其他) + if ($r1 === $r2 && $r2 === $r3) { + return 'triple'; + } + // 两同 + if ($r1 === $r2 || $r2 === $r3 || $r1 === $r3) { + return 'pair'; + } + + return 'miss'; + } + + /** + * 结果类型中文标签。 + */ + public static function resultLabel(string $type): string + { + return match ($type) { + 'jackpot' => '🎉 三个7大奖!', + 'triple_gem' => '💎 三钻豪华奖', + 'triple' => '✨ 三同奖', + 'pair' => '🎁 两同小奖', + 'curse' => '☠️ 三骷髅诅咒', + 'miss' => '😔 未中奖', + }; + } +} diff --git a/database/migrations/2026_03_01_205542_create_slot_machine_logs_table.php b/database/migrations/2026_03_01_205542_create_slot_machine_logs_table.php new file mode 100644 index 0000000..b7cc788 --- /dev/null +++ b/database/migrations/2026_03_01_205542_create_slot_machine_logs_table.php @@ -0,0 +1,43 @@ +id(); + $table->unsignedBigInteger('user_id')->comment('玩家 ID'); + $table->string('reel1', 10)->comment('第一列图案 key'); + $table->string('reel2', 10)->comment('第二列图案 key'); + $table->string('reel3', 10)->comment('第三列图案 key'); + $table->enum('result_type', ['jackpot', 'triple_gem', 'triple', 'pair', 'curse', 'miss']) + ->comment('结果类型'); + $table->unsignedInteger('cost')->comment('消耗金币'); + $table->integer('payout')->default(0)->comment('净赔付金额(正=赢,负=输)'); + $table->timestamps(); + + $table->index(['user_id', 'created_at']); + }); + } + + /** + * 回滚迁移。 + */ + public function down(): void + { + Schema::dropIfExists('slot_machine_logs'); + } +}; diff --git a/resources/views/chat/frame.blade.php b/resources/views/chat/frame.blade.php index feae5c2..e73e934 100644 --- a/resources/views/chat/frame.blade.php +++ b/resources/views/chat/frame.blade.php @@ -139,6 +139,8 @@ @include('chat.partials.holiday-modal') {{-- ═══════════ 百家乐游戏面板 ═══════════ --}} @include('chat.partials.baccarat-panel') + {{-- ═══════════ 老虎机游戏面板 ═══════════ --}} + @include('chat.partials.slot-machine') {{-- 全屏特效系统:管理员烟花/下雨/雷电/下雪 --}} diff --git a/resources/views/chat/partials/slot-machine.blade.php b/resources/views/chat/partials/slot-machine.blade.php new file mode 100644 index 0000000..b596f51 --- /dev/null +++ b/resources/views/chat/partials/slot-machine.blade.php @@ -0,0 +1,460 @@ +{{-- + 文件功能:老虎机游戏前台面板组件 + + 聊天室内老虎机游戏: + - 悬浮按钮 🎰 入口(游戏开启时显示) + - 三列滚轮动画(CSS 逐列延迟停止) + - 权重随机图案、多种赔率(三7全服广播) + - 每日次数限制、金币余额显示 + - 最近记录展示 +--}} + +{{-- ─── 老虎机悬浮按钮 ─── --}} +
+ +
+ +{{-- ─── 老虎机主面板 ─── --}} +
+
+ +
+ + {{-- ─── 顶部标题 ─── --}} +
+
+
+
🎰 老虎机
+
+ 每次消耗 金币 +
+
+
+
🪙
+
+
+
+
+ + {{-- ─── 滚轮区域 ─── --}} +
+ + {{-- 三列转轮 --}} +
+
+ {{-- 第一列 --}} +
+
+
+
+ {{-- 分隔 --}} +
|
+ {{-- 第二列 --}} +
+
+
+
+ {{-- 分隔 --}} +
|
+ {{-- 第三列 --}} +
+
+
+
+
+ + {{-- 中间射线指示条 --}} +
+
+
+ + {{-- 结果提示 --}} +
+
+
+
+ 正在转动中… +
+
+ + {{-- 盈亏显示 --}} +
+
+
+
+
+
+ 损失 金币 +
+
+ + {{-- 转动按钮 --}} + + + {{-- 赔率说明 --}} +
+
+
7️⃣7️⃣7️⃣
+
×100
+
+
+
💎💎💎
+
×50
+
+
+
三同
+
×10
+
+
+
两同
+
×2
+
+
+ + {{-- 历史记录 --}} +
+
最近记录
+
+ +
+
+
+ + {{-- ─── 底部关闭 ─── --}} +
+ +
+
+
+
+ + + + diff --git a/routes/web.php b/routes/web.php index 1b56b35..a5d929c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -117,14 +117,21 @@ Route::middleware(['chat.auth'])->group(function () { // ── 百家乐(前台)──────────────────────────────────────────────── Route::prefix('baccarat')->name('baccarat.')->group(function () { - // 获取当前局次信息 Route::get('/current', [\App\Http\Controllers\BaccaratController::class, 'currentRound'])->name('current'); - // 提交下注 Route::post('/bet', [\App\Http\Controllers\BaccaratController::class, 'bet'])->name('bet'); - // 查询历史记录 Route::get('/history', [\App\Http\Controllers\BaccaratController::class, 'history'])->name('history'); }); + // ── 老虎机(前台)──────────────────────────────────────────────── + Route::prefix('slot')->name('slot.')->group(function () { + // 获取配置及今日剩余次数 + Route::get('/info', [\App\Http\Controllers\SlotMachineController::class, 'info'])->name('info'); + // 执行一次转动 + Route::post('/spin', [\App\Http\Controllers\SlotMachineController::class, 'spin'])->name('spin'); + // 个人历史记录 + Route::get('/history', [\App\Http\Controllers\SlotMachineController::class, 'history'])->name('history'); + }); + // ---- 第五阶段:具体房间内部聊天核心 ---- // 进入具体房间界面的初始化 Route::get('/room/{id}', [ChatController::class, 'init'])->name('chat.room');