diff --git a/app/Http/Controllers/Admin/FishingEventController.php b/app/Http/Controllers/Admin/FishingEventController.php new file mode 100644 index 0000000..0238f22 --- /dev/null +++ b/app/Http/Controllers/Admin/FishingEventController.php @@ -0,0 +1,108 @@ +orderBy('id')->get(); + $totalWeight = $events->where('is_active', true)->sum('weight'); + + return view('admin.fishing.index', compact('events', 'totalWeight')); + } + + /** + * 创建新钓鱼事件 + */ + public function store(Request $request): RedirectResponse + { + $data = $request->validate([ + 'emoji' => 'required|string|max:10', + 'name' => 'required|string|max:100', + 'message' => 'required|string|max:255', + 'exp' => 'required|integer', + 'jjb' => 'required|integer', + 'weight' => 'required|integer|min:1|max:9999', + 'sort' => 'required|integer|min:0', + 'is_active' => 'boolean', + ]); + + $data['is_active'] = $request->boolean('is_active', true); + FishingEvent::create($data); + + return redirect()->route('admin.fishing.index')->with('success', '钓鱼事件已添加!'); + } + + /** + * 更新钓鱼事件 + * + * @param FishingEvent $fishing 路由模型绑定 + */ + public function update(Request $request, FishingEvent $fishing): RedirectResponse + { + $data = $request->validate([ + 'emoji' => 'required|string|max:10', + 'name' => 'required|string|max:100', + 'message' => 'required|string|max:255', + 'exp' => 'required|integer', + 'jjb' => 'required|integer', + 'weight' => 'required|integer|min:1|max:9999', + 'sort' => 'required|integer|min:0', + 'is_active' => 'boolean', + ]); + + $data['is_active'] = $request->boolean('is_active'); + $fishing->update($data); + + return redirect()->route('admin.fishing.index')->with('success', "事件「{$fishing->name}」已更新!"); + } + + /** + * 切换事件启用/禁用状态(AJAX) + * + * @param FishingEvent $fishing 路由模型绑定 + */ + public function toggle(FishingEvent $fishing): JsonResponse + { + $fishing->update(['is_active' => ! $fishing->is_active]); + + return response()->json([ + 'ok' => true, + 'is_active' => $fishing->is_active, + 'message' => $fishing->is_active ? "「{$fishing->name}」已启用" : "「{$fishing->name}」已禁用", + ]); + } + + /** + * 删除钓鱼事件 + * + * @param FishingEvent $fishing 路由模型绑定 + */ + public function destroy(FishingEvent $fishing): RedirectResponse + { + $name = $fishing->name; + $fishing->delete(); + + return redirect()->route('admin.fishing.index')->with('success', "事件「{$name}」已删除!"); + } +} diff --git a/app/Http/Controllers/FishingController.php b/app/Http/Controllers/FishingController.php index bc4c95a..f1dd25c 100644 --- a/app/Http/Controllers/FishingController.php +++ b/app/Http/Controllers/FishingController.php @@ -18,6 +18,8 @@ namespace App\Http\Controllers; use App\Enums\CurrencySource; use App\Events\MessageSent; +use App\Models\FishingEvent; +use App\Models\GameConfig; use App\Models\Sysparam; use App\Services\ChatStateService; use App\Services\ShopService; @@ -56,6 +58,11 @@ class FishingController extends Controller return response()->json(['status' => 'error', 'message' => '请先登录'], 401); } + // 检查钓鱼全局开关 + if (! GameConfig::isEnabled('fishing')) { + return response()->json(['status' => 'error', 'message' => '钓鱼功能暂未开放。'], 403); + } + // 1. 检查冷却时间(Redis TTL) $cooldownKey = "fishing:cd:{$user->id}"; if (Redis::exists($cooldownKey)) { @@ -69,7 +76,7 @@ class FishingController extends Controller } // 2. 检查金币是否足够 - $cost = (int) Sysparam::getValue('fishing_cost', '5'); + $cost = (int) (GameConfig::param('fishing', 'fishing_cost') ?? Sysparam::getValue('fishing_cost', '5')); if (($user->jjb ?? 0) < $cost) { return response()->json([ 'status' => 'error', @@ -87,8 +94,8 @@ class FishingController extends Controller $user->refresh(); // 4. 生成一次性 token,存入 Redis(TTL = 等待时间 + 收竿窗口 + 缓冲) - $waitMin = (int) Sysparam::getValue('fishing_wait_min', '8'); - $waitMax = (int) Sysparam::getValue('fishing_wait_max', '15'); + $waitMin = (int) (GameConfig::param('fishing', 'fishing_wait_min') ?? Sysparam::getValue('fishing_wait_min', '8')); + $waitMax = (int) (GameConfig::param('fishing', 'fishing_wait_max') ?? Sysparam::getValue('fishing_wait_max', '15')); $waitTime = rand($waitMin, $waitMax); $token = Str::random(32); $tokenKey = "fishing:token:{$user->id}"; @@ -169,7 +176,7 @@ class FishingController extends Controller Redis::del($tokenKey); // 2. 设置冷却时间 - $cooldown = (int) Sysparam::getValue('fishing_cooldown', '300'); + $cooldown = (int) (GameConfig::param('fishing', 'fishing_cooldown') ?? Sysparam::getValue('fishing_cooldown', '300')); Redis::setex("fishing:cd:{$user->id}", $cooldown, time()); // 3. 随机决定钓鱼结果 @@ -229,57 +236,31 @@ class FishingController extends Controller } /** - * 随机钓鱼结果(复刻原版概率分布) + * 随机钓鱼结果(从数据库 fishing_events 加权随机抽取) + * + * 若数据库中无激活事件,回退到兜底结果。 * * @return array{emoji: string, message: string, exp: int, jjb: int} */ private function randomFishResult(): array { - $roll = rand(1, 100); + $event = FishingEvent::rollOne(); - return match (true) { - $roll <= 15 => [ - 'emoji' => '🦈', - 'message' => '钓到一条大鲨鱼!增加经验100、金币20', - 'exp' => 100, - 'jjb' => 20, - ], - $roll <= 30 => [ - 'emoji' => '🐟', - 'message' => '钓到一条娃娃鱼,到集市卖得30个金币', - 'exp' => 0, - 'jjb' => 30, - ], - $roll <= 50 => [ - 'emoji' => '🐠', - 'message' => '钓到一只大草鱼,吃下增加经验50', - 'exp' => 50, - 'jjb' => 0, - ], - $roll <= 70 => [ - 'emoji' => '🐡', - 'message' => '钓到一条小鲤鱼,增加经验50、金币10', - 'exp' => 50, - 'jjb' => 10, - ], - $roll <= 85 => [ - 'emoji' => '💧', - 'message' => '鱼没钓到,摔到河里经验减少50', - 'exp' => -50, - 'jjb' => 0, - ], - $roll <= 95 => [ - 'emoji' => '👊', - 'message' => '偷钓鱼塘被主人发现,一阵殴打!经验减少20、金币减少3', - 'exp' => -20, - 'jjb' => -3, - ], - default => [ - 'emoji' => '🎉', - 'message' => '运气爆棚!钓到大鲨鱼、大草鱼、小鲤鱼各一条!经验+150,金币+50!', - 'exp' => 150, - 'jjb' => 50, - ], - }; + // 数据库无事件时的兜底 + if (! $event) { + return [ + 'emoji' => '🐟', + 'message' => '钓到一条小鱼,获得金币10', + 'exp' => 0, + 'jjb' => 10, + ]; + } + + return [ + 'emoji' => $event->emoji, + 'message' => $event->message, + 'exp' => $event->exp, + 'jjb' => $event->jjb, + ]; } } diff --git a/app/Models/FishingEvent.php b/app/Models/FishingEvent.php new file mode 100644 index 0000000..a95fbf0 --- /dev/null +++ b/app/Models/FishingEvent.php @@ -0,0 +1,89 @@ + + */ + protected $fillable = [ + 'emoji', + 'name', + 'message', + 'exp', + 'jjb', + 'weight', + 'is_active', + 'sort', + ]; + + /** + * 字段类型转换 + * + * @return array + */ + protected function casts(): array + { + return [ + 'exp' => 'integer', + 'jjb' => 'integer', + 'weight' => 'integer', + 'is_active' => 'boolean', + 'sort' => 'integer', + ]; + } + + /** + * 作用域:只查询已启用的事件 + */ + public function scopeActive(Builder $query): Builder + { + return $query->where('is_active', true); + } + + /** + * 根据权重随机抽取一个激活的钓鱼事件 + * + * 实现加权随机:权重越大,被选中概率越高。 + * 若无任何激活事件,返回 null。 + */ + public static function rollOne(): ?static + { + $events = static::active()->orderBy('sort')->get(); + if ($events->isEmpty()) { + return null; + } + + // 计算总权重后加权随机 + $totalWeight = $events->sum('weight'); + $roll = random_int(1, max($totalWeight, 1)); + $cumulative = 0; + + foreach ($events as $event) { + $cumulative += $event->weight; + if ($roll <= $cumulative) { + return $event; + } + } + + // 兜底:返回最后一个 + return $events->last(); + } +} diff --git a/database/factories/FishingEventFactory.php b/database/factories/FishingEventFactory.php new file mode 100644 index 0000000..63effcf --- /dev/null +++ b/database/factories/FishingEventFactory.php @@ -0,0 +1,23 @@ + + */ +class FishingEventFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + // + ]; + } +} diff --git a/database/migrations/2026_03_03_163943_create_fishing_events_table.php b/database/migrations/2026_03_03_163943_create_fishing_events_table.php new file mode 100644 index 0000000..e273b0f --- /dev/null +++ b/database/migrations/2026_03_03_163943_create_fishing_events_table.php @@ -0,0 +1,46 @@ +id(); + $table->string('emoji', 10)->comment('事件表情符号'); + $table->string('name', 100)->comment('事件名称(后台展示用)'); + $table->string('message', 255)->comment('聊天室播报内容'); + $table->integer('exp')->default(0)->comment('经验变化(正为获得,负为扣除)'); + $table->integer('jjb')->default(0)->comment('金币变化(正为获得,负为扣除)'); + $table->unsignedSmallInteger('weight')->default(10)->comment('权重(权重越大概率越高)'); + $table->boolean('is_active')->default(true)->comment('是否启用'); + $table->unsignedTinyInteger('sort')->default(0)->comment('排序(越小越靠前)'); + $table->timestamps(); + }); + } + + /** + * 回滚:删除 fishing_events 表 + */ + public function down(): void + { + Schema::dropIfExists('fishing_events'); + } +}; diff --git a/database/seeders/FishingEventSeeder.php b/database/seeders/FishingEventSeeder.php new file mode 100644 index 0000000..3ea7f04 --- /dev/null +++ b/database/seeders/FishingEventSeeder.php @@ -0,0 +1,123 @@ + 0) { + $this->command->info('钓鱼事件已存在,跳过填充。'); + + return; + } + + $events = [ + [ + 'sort' => 1, + 'emoji' => '🦈', + 'name' => '大鲨鱼', + 'message' => '钓到一条大鲨鱼!获得经验30、金币50', + 'exp' => 30, + 'jjb' => 50, + 'weight' => 15, + 'is_active' => true, + ], + [ + 'sort' => 2, + 'emoji' => '🐟', + 'name' => '娃娃鱼', + 'message' => '钓到一条娃娃鱼,到集市卖得80个金币', + 'exp' => 0, + 'jjb' => 80, + 'weight' => 15, + 'is_active' => true, + ], + [ + 'sort' => 3, + 'emoji' => '🐠', + 'name' => '大草鱼', + 'message' => '钓到一只大草鱼,吃下增加经验20、金币30', + 'exp' => 20, + 'jjb' => 30, + 'weight' => 20, + 'is_active' => true, + ], + [ + 'sort' => 4, + 'emoji' => '🐡', + 'name' => '小鲤鱼', + 'message' => '钓到一条小鲤鱼,增加经验10、金币20', + 'exp' => 10, + 'jjb' => 20, + 'weight' => 20, + 'is_active' => true, + ], + [ + 'sort' => 5, + 'emoji' => '💧', + 'name' => '落水惨败', + 'message' => '鱼没钓到,摔到河里损失金币30', + 'exp' => 0, + 'jjb' => -30, + 'weight' => 15, + 'is_active' => true, + ], + [ + 'sort' => 6, + 'emoji' => '👊', + 'name' => '被抓殴打', + 'message' => '偷钓鱼塘被主人发现,一阵殴打!金币减少10', + 'exp' => 0, + 'jjb' => -10, + 'weight' => 10, + 'is_active' => true, + ], + [ + 'sort' => 7, + 'emoji' => '🎉', + 'name' => '超级大奖', + 'message' => '运气爆棚!钓到大鲨鱼、大草鱼、小鲤鱼各一条!经验+50,金币+200!', + 'exp' => 50, + 'jjb' => 200, + 'weight' => 5, + 'is_active' => true, + ], + ]; + + foreach ($events as $event) { + FishingEvent::create($event); + } + + $this->command->info('✅ 钓鱼事件已填充(7 条)'); + } +} diff --git a/database/seeders/GameConfigSeeder.php b/database/seeders/GameConfigSeeder.php index 4fc852e..e913e57 100644 --- a/database/seeders/GameConfigSeeder.php +++ b/database/seeders/GameConfigSeeder.php @@ -116,6 +116,21 @@ class GameConfigSeeder extends Seeder 'curse_chance' => 5, // 大凶签概率(%) ], ], + + // ─── 钓鱼小游戏 ────────────────────────────────────────────── + [ + 'game_key' => 'fishing', + 'name' => '钓鱼小游戏', + 'icon' => '🎣', + 'description' => '消耗金币抛竿,等待浮漂下沉后点击收竿,随机获得奖励或惩罚。持有自动钓鱼卡可自动循环。', + 'enabled' => false, + 'params' => [ + 'fishing_cost' => 5, // 每次抛竿消耗金币 + 'fishing_wait_min' => 8, // 浮漂等待最短秒数 + 'fishing_wait_max' => 15, // 浮漂等待最长秒数 + 'fishing_cooldown' => 300, // 收竿后冷却秒数 + ], + ], ]; foreach ($games as $game) { diff --git a/resources/views/admin/fishing/index.blade.php b/resources/views/admin/fishing/index.blade.php new file mode 100644 index 0000000..74488cf --- /dev/null +++ b/resources/views/admin/fishing/index.blade.php @@ -0,0 +1,281 @@ +@extends('admin.layouts.app') + +@section('title', '钓鱼事件管理') + +@section('content') +
+ + {{-- 页头 --}} +
+
+

🎣 钓鱼事件管理

+

+ 管理钓鱼随机事件。权重越大被触发概率越高。 + 当前激活事件总权重:{{ $totalWeight }} +

+
+ + ⚙️ 钓鱼参数设置 + +
+ + {{-- Flash --}} + @if (session('success')) +
+ ✅ {{ session('success') }} +
+ @endif + + {{-- 钓鱼事件列表 --}} +
+ + + + + + + + + + + + + + + + + @foreach ($events as $event) + + + + + + + + + + + + + @endforeach + +
排序符号名称播报内容经验金币权重概率状态操作
{{ $event->sort }}{{ $event->emoji }}{{ $event->name }}{{ $event->message }} + {{ $event->exp > 0 ? '+' : '' }}{{ $event->exp }} + + {{ $event->jjb > 0 ? '+' : '' }}{{ $event->jjb }} + {{ $event->weight }} + @if ($totalWeight > 0 && $event->is_active) + {{ number_format(($event->weight / $totalWeight) * 100, 1) }}% + @else + — + @endif + + + + +
+ @csrf @method('DELETE') + +
+
+
+ + {{-- 新增事件卡片 --}} +
+
+

➕ 新增钓鱼事件

+
+
+ @csrf +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ +
+ + {{-- 编辑弹窗 --}} + + + +@endsection diff --git a/resources/views/admin/game-configs/index.blade.php b/resources/views/admin/game-configs/index.blade.php index 5feae73..0ceb3df 100644 --- a/resources/views/admin/game-configs/index.blade.php +++ b/resources/views/admin/game-configs/index.blade.php @@ -248,6 +248,12 @@ 'max' => 100, ], ], + 'fishing' => [ + 'fishing_cost' => ['label' => '每次抛竿消耗', 'type' => 'number', 'unit' => '金币', 'min' => 1], + 'fishing_wait_min' => ['label' => '浮漂等待最短', 'type' => 'number', 'unit' => '秒', 'min' => 1], + 'fishing_wait_max' => ['label' => '浮漂等待最长', 'type' => 'number', 'unit' => '秒', 'min' => 1], + 'fishing_cooldown' => ['label' => '收竿后冷却时间', 'type' => 'number', 'unit' => '秒', 'min' => 10], + ], default => [], }; } diff --git a/resources/views/admin/layouts/app.blade.php b/resources/views/admin/layouts/app.blade.php index 7c133e7..f24c5df 100644 --- a/resources/views/admin/layouts/app.blade.php +++ b/resources/views/admin/layouts/app.blade.php @@ -82,6 +82,10 @@ class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.game-configs.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}"> {!! '🎮 游戏管理' !!} + + {!! '🎣 钓鱼事件' !!} + {!! '🏛️ 部门管理' !!} diff --git a/routes/web.php b/routes/web.php index d285711..49790aa 100644 --- a/routes/web.php +++ b/routes/web.php @@ -349,6 +349,15 @@ Route::middleware(['chat.auth', 'chat.has_position'])->prefix('admin')->name('ad Route::post('/{gameConfig}/toggle', [\App\Http\Controllers\Admin\GameConfigController::class, 'toggle'])->name('toggle'); Route::post('/{gameConfig}/params', [\App\Http\Controllers\Admin\GameConfigController::class, 'updateParams'])->name('params'); }); + + // 🎣 钓鱼事件管理 + Route::prefix('fishing')->name('fishing.')->group(function () { + Route::get('/', [\App\Http\Controllers\Admin\FishingEventController::class, 'index'])->name('index'); + Route::post('/', [\App\Http\Controllers\Admin\FishingEventController::class, 'store'])->name('store'); + Route::put('/{fishing}', [\App\Http\Controllers\Admin\FishingEventController::class, 'update'])->name('update'); + Route::post('/{fishing}/toggle', [\App\Http\Controllers\Admin\FishingEventController::class, 'toggle'])->name('toggle'); + Route::delete('/{fishing}', [\App\Http\Controllers\Admin\FishingEventController::class, 'destroy'])->name('destroy'); + }); }); // ──────────────────────────────────────────────────────────────