Files
chatroom/app/Http/Controllers/SlotMachineController.php

289 lines
9.9 KiB
PHP
Raw Normal View History

<?php
/**
* 文件功能:老虎机游戏前台控制器
*
* 提供老虎机转动 API
* - 检查游戏开关、每日限制
* - 扣除金币、生成三列图案
* - 判断结果、赔付金币、写流水
* - 三个7时全服公屏广播
* - 返回结果供前端播放动画
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace App\Http\Controllers;
use App\Enums\CurrencySource;
use App\Events\MessageSent;
use App\Jobs\SaveMessageJob;
use App\Models\GameConfig;
use App\Models\SlotMachineLog;
use App\Services\ChatStateService;
use App\Services\UserCurrencyService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class SlotMachineController extends Controller
{
public function __construct(
private readonly UserCurrencyService $currency,
private readonly ChatStateService $chatState,
) {}
/**
* 获取老虎机配置信息(图案表、赔率、今日剩余次数)。
*/
public function info(Request $request): JsonResponse
{
if (! GameConfig::isEnabled('slot_machine')) {
return response()->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);
}
}