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; // 净损失 = 本金+惩罚 } } // ⑤ 写游戏日志 $resultLabel = SlotMachineLog::resultLabel($resultType); SlotMachineLog::create([ 'user_id' => $user->id, 'reel1' => $r1, 'reel2' => $r2, 'reel3' => $r3, 'result_type' => $resultType, 'cost' => $cost, 'payout' => $payout, ]); // ⑥ 广播通知 $e1 = $symbols[$r1]['emoji']; $e2 = $symbols[$r2]['emoji']; $e3 = $symbols[$r3]['emoji']; if ($resultType === 'jackpot') { // 三个7:全服公屏广播 $this->broadcastJackpot($user->username, $payout, $cost); } elseif (in_array($resultType, ['triple_gem', 'triple', 'pair'], true)) { // 普通中奖:仅向本人发送聊天室系统通知 $net = $payout - $cost; $content = "🎰 {$resultLabel}!{$e1}{$e2}{$e3} 赢得 +💰".number_format($net).' 金币'; $this->broadcastPersonal($user->username, $content); } elseif ($resultType === 'curse') { // 诅咒:通知本人 $content = "☠️ 三骷髅诅咒!{$e1}{$e2}{$e3} 额外扣除 💰".number_format($cost).' 金币!'; $this->broadcastPersonal($user->username, $content); } $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); } /** * 向特定用户发送聊天室私人系统通知(仅该用户可见)。 * * @param string $toUsername 接收用户名 * @param string $content 消息内容 */ private function broadcastPersonal(string $toUsername, string $content): void { $msg = [ 'id' => $this->chatState->nextMessageId(1), 'room_id' => 1, 'from_user' => '系统传音', 'to_user' => $toUsername, 'content' => $content, 'is_secret' => true, 'font_color' => '#f59e0b', 'action' => '', 'sent_at' => now()->toDateTimeString(), ]; broadcast(new MessageSent(1, $msg)); SaveMessageJob::dispatch($msg); } }