🪙 在多数平台/字体上渲染为银灰色,与「金币」语义不符; 💰 各平台均渲染为金黄色,更直观传达金币概念。 涉及文件(43处): - app/Jobs:百家乐、赛马结算广播 - app/Http/Controllers:管理员命令、红包、老虎机、神秘箱子 - app/Listeners - resources/views:聊天室各游戏面板、商店、toolbar、后台页面等
289 lines
9.9 KiB
PHP
289 lines
9.9 KiB
PHP
<?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);
|
||
}
|
||
}
|