新增老虎机游戏:①slot_machine_logs表+模型(8种权重图案/判奖) ②SlotMachineController(扣费/随机/赔付/诅咒/三7全服广播) ③前台面板(三列滚轮动画/逐列停止/赔率说明/历史记录) ④CurrencySource三个枚举
This commit is contained in:
249
app/Http/Controllers/SlotMachineController.php
Normal file
249
app/Http/Controllers/SlotMachineController.php
Normal file
@@ -0,0 +1,249 @@
|
||||
<?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; // 净损失 = 本金+惩罚
|
||||
}
|
||||
}
|
||||
|
||||
// ⑤ 写游戏日志
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user