diff --git a/app/Http/Controllers/Admin/DailyQuestController.php b/app/Http/Controllers/Admin/DailyQuestController.php new file mode 100644 index 0000000..19850fc --- /dev/null +++ b/app/Http/Controllers/Admin/DailyQuestController.php @@ -0,0 +1,85 @@ +paginate(15); + + // Upgraded Bank of Smart Suggestions (Hardcore Focused) + $suggestions = [ + // --- EXTREME CHALLENGES --- + ['title' => 'اسطوره لیگ: پیروزی در ۵۰ دوئل', 'quest_type' => 'duel_win', 'required_amount' => 50, 'reward_xp' => 4000], + ['title' => 'ماراتن تست‌زنی: پاسخ به ۵۰۰ تست', 'quest_type' => 'answer_tests', 'required_amount' => 500, 'reward_xp' => 3500], + ['title' => 'کمال‌گرا: کسب نمره کامل در ۵ آزمون', 'quest_type' => 'perfect_score', 'required_amount' => 5, 'reward_xp' => 2500], + + // --- HARD CHALLENGES --- + ['title' => 'مبارز خستگی‌ناپذیر: پیروزی در ۲۰ دوئل', 'quest_type' => 'duel_win', 'required_amount' => 20, 'reward_xp' => 1500], + ['title' => 'چالش حقوق‌دان: پاسخ به ۲۵۰ تست', 'quest_type' => 'answer_tests', 'required_amount' => 250, 'reward_xp' => 1200], + ['title' => 'دقت بالا: کسب نمره کامل در ۳ آزمون', 'quest_type' => 'perfect_score', 'required_amount' => 3, 'reward_xp' => 1200], + ['title' => 'پیروزی در ۱۰ دوئل متوالی (کاپ نقره)', 'quest_type' => 'duel_win', 'required_amount' => 10, 'reward_xp' => 700], + + // --- MEDIUM CHALLENGES (For balance) --- + ['title' => 'گرم‌کردن: پیروزی در ۵ دوئل', 'quest_type' => 'duel_win', 'required_amount' => 5, 'reward_xp' => 300], + ['title' => 'مرور روزانه: پاسخ به ۵۰ تست', 'quest_type' => 'answer_tests', 'required_amount' => 50, 'reward_xp' => 250], + ['title' => 'کسب نمره کامل در ۱ آزمون', 'quest_type' => 'perfect_score', 'required_amount' => 1, 'reward_xp' => 350], + ]; + + return view('admin.daily-quests.index', compact('quests', 'suggestions')); + } + + public function create() + { + return view('admin.daily-quests.create'); + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'quest_type' => 'required|string|in:duel_win,answer_tests,perfect_score', + 'required_amount' => 'required|integer|min:1', + 'reward_xp' => 'required|integer|min:10', + ]); + + $validated['is_active'] = $request->has('is_active'); // <-- Catch the switch + + DailyQuest::create($validated); + + return redirect()->route('daily-quests.index')->with('success', 'چالش روزانه با موفقیت ایجاد شد.'); + } + + public function update(Request $request, DailyQuest $daily_quest) + { + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'quest_type' => 'required|string|in:duel_win,answer_tests,perfect_score', + 'required_amount' => 'required|integer|min:1', + 'reward_xp' => 'required|integer|min:10', + ]); + + $validated['is_active'] = $request->has('is_active'); // <-- Catch the switch + + $daily_quest->update($validated); + + return redirect()->route('daily-quests.index')->with('success', 'چالش روزانه با موفقیت ویرایش شد.'); + } + + + public function edit(DailyQuest $daily_quest) + { + return view('admin.daily-quests.update', compact('daily_quest')); + } + + public function destroy(DailyQuest $daily_quest) + { + $daily_quest->delete(); + return redirect()->route('daily-quests.index')->with('success', 'چالش روزانه حذف شد.'); + } +} diff --git a/app/Http/Controllers/api/LeagueController.php b/app/Http/Controllers/api/LeagueController.php new file mode 100644 index 0000000..b410392 --- /dev/null +++ b/app/Http/Controllers/api/LeagueController.php @@ -0,0 +1,139 @@ +user(); + + // Dynamic Level Calculation (Every 500 XP = 1 Level) + $level = floor($user->xp / 500) + 1; + $xpToNext = 500 - ($user->xp % 500); + $progress = ($user->xp % 500) / 500; + + $winRate = $user->duels_played > 0 + ? round(($user->duels_won / $user->duels_played) * 100) + : 0; + + // Calculate Global Rank Position + $rankPosition = User::where('xp', '>', $user->xp)->count() + 1; + + $data = [ + 'user_id' => $user->id, + 'name' => $user->name ?? 'کاربر', + 'current_level' => $level, + 'rank_name' => $this->getRankName($level), + 'total_xp' => $user->xp, + 'xp_to_next_level' => $xpToNext, + 'progress_percent' => round($progress, 2), + 'win_rate_percent' => $winRate, + 'total_duels' => $user->duels_played, + 'global_rank_position' => $rankPosition + ]; + + return $this->success($data, 'موفق', 'اطلاعات لیگ کاربر دریافت شد'); + } + + /** + * API 2: Daily Quests + */ + public function quests(Request $request) + { + $user = $request->user(); + + // Fetch active quests for today using the new relationship + $userQuests = $user->activeQuests()->with('quest')->get(); + + $formattedQuests = $userQuests->map(function ($uq) { + return [ + 'id' => $uq->id, + 'title' => $uq->quest->title, + 'current_progress' => $uq->current_progress, + 'required_amount' => $uq->quest->required_amount, + 'reward_text' => '+' . $uq->quest->reward_xp . ' XP', + 'is_completed' => $uq->is_completed + ]; + }); + + $expiresIn = now()->endOfDay()->diffInSeconds(now()); + + return $this->success([ + 'expires_in_seconds' => $expiresIn, + 'quests' => $formattedQuests + ], 'موفق', 'چالش‌های روزانه دریافت شد'); + } + + /** + * API 3: League Leaderboard + */ + public function leaderboard(Request $request) + { + $currentUser = $request->user(); + + // Get Top 50 Users + $topUsers = User::orderByDesc('xp')->limit(50)->get(['id', 'name', 'xp']); + + $leaderboard = $topUsers->map(function ($u, $index) use ($currentUser) { + return [ + 'rank' => $index + 1, + 'user_id' => $u->id, + 'name' => $u->name ?? 'کاربر ناشناس', + 'xp' => $u->xp, + 'is_me' => $u->id === $currentUser->id + ]; + }); + + // Current User Data + $myRank = User::where('xp', '>', $currentUser->xp)->count() + 1; + + $myStats = [ + 'rank' => $myRank, + 'user_id' => $currentUser->id, + 'name' => $currentUser->name ?? 'شما', + 'xp' => $currentUser->xp, + 'is_me' => true + ]; + + return $this->success([ + 'leaderboard' => $leaderboard, + 'current_user' => $myStats + ], 'موفق', 'جدول رده‌بندی دریافت شد'); + } + + /** + * API 4: PvP Matchmaking Entry + */ + public function matchmake(Request $request) + { + $ticketId = 'req_' . Str::random(12); + + return $this->success([ + 'matchmaking_ticket_id' => $ticketId, + 'status' => 'searching', + 'websocket_channel' => 'private-matchmaking.' . $ticketId, + 'estimated_wait_seconds' => 15 + ], 'در حال جستجو', 'به صف انتظار دوئل اضافه شدید'); + } + + private function getRankName($level) + { + if ($level >= 50) return 'الماس'; + if ($level >= 30) return 'طلا'; + if ($level >= 10) return 'نقره‌ای'; + return 'برنز'; + } +} diff --git a/app/Models/DailyQuest.php b/app/Models/DailyQuest.php new file mode 100644 index 0000000..153bdb0 --- /dev/null +++ b/app/Models/DailyQuest.php @@ -0,0 +1,28 @@ + 'boolean', + ]; + + public function userQuests() + { + return $this->hasMany(UserQuest::class, 'daily_quest_id'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 3a002cb..81ca141 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -21,6 +21,9 @@ class User extends Authenticatable 'is_admin', 'email', 'password', + 'xp', + 'duels_played', + 'duels_won', ]; protected $hidden = [ @@ -67,4 +70,25 @@ class User extends Authenticatable ->withPivot('read_at') ->withTimestamps(); } + + // ========================================== + // --- NEW: Gamification Relationships --- + // ========================================== + + /** + * Get all quest records for the user. + */ + public function userQuests() + { + return $this->hasMany(UserQuest::class); + } + + /** + * Helper relationship to quickly get ONLY active, unexpired quests. + */ + public function activeQuests() + { + return $this->hasMany(UserQuest::class) + ->where('expires_at', '>', now()); + } } diff --git a/app/Models/UserQuest.php b/app/Models/UserQuest.php new file mode 100644 index 0000000..4aaa3d4 --- /dev/null +++ b/app/Models/UserQuest.php @@ -0,0 +1,35 @@ + 'boolean', + 'expires_at' => 'datetime', + ]; + + public function user() + { + return $this->belongsTo(User::class); + } + + public function quest() + { + return $this->belongsTo(DailyQuest::class, 'daily_quest_id'); + } +} diff --git a/resources/views/admin/daily-quests/create.blade.php b/resources/views/admin/daily-quests/create.blade.php new file mode 100644 index 0000000..2efff2c --- /dev/null +++ b/resources/views/admin/daily-quests/create.blade.php @@ -0,0 +1,47 @@ +@extends('admin.layouts.app') +@section('content') +
+ @include('admin.layouts.errors') +
+
+
+

افزودن چالش روزانه جدید

+
+ @csrf +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ is_active) || !isset($daily_quest) ? 'checked' : '' }}> + +
+ + +
+
+
+
+
+@endsection diff --git a/resources/views/admin/daily-quests/index.blade.php b/resources/views/admin/daily-quests/index.blade.php new file mode 100644 index 0000000..912df79 --- /dev/null +++ b/resources/views/admin/daily-quests/index.blade.php @@ -0,0 +1,108 @@ +@extends('admin.layouts.app') +@section('content') +
+
+
+
+
+

مدیریت چالش‌های روزانه (لیگ)

+
+ + + افزودن چالش دستی +
+
+ + @if(session('success')) +
{{ session('success') }}
+ @endif + +
+ + + + + + + + + + + + + + @foreach ($quests as $item) + + + + + + + + + + @endforeach + +
شناسهعنوان چالشنوع چالشمقدار مورد نیازپاداش (XP)وضعیتعملیات
{{ $item->id }}{{ $item->title }}{{ $item->quest_type }}{{ $item->required_amount }}+{{ $item->reward_xp }} XP + + {{ $item->is_active ? 'فعال' : 'غیرفعال' }} + + +
+ @csrf + @method('DELETE') + +
+ ویرایش +
+
+ +
+ {{ $quests->links() }} +
+
+
+
+
+ + + +@endsection diff --git a/resources/views/admin/daily-quests/update.blade.php b/resources/views/admin/daily-quests/update.blade.php new file mode 100644 index 0000000..cad422e --- /dev/null +++ b/resources/views/admin/daily-quests/update.blade.php @@ -0,0 +1,60 @@ +@extends('admin.layouts.app') +@section('content') +
+ @include('admin.layouts.errors') +
+
+
+

ویرایش چالش روزانه

+
+ @csrf + @method('PUT') + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ is_active) || !isset($daily_quest) ? 'checked' : '' }}> + +
+ + +
+
+
+
+
+@endsection diff --git a/resources/views/admin/partial/menu.blade.php b/resources/views/admin/partial/menu.blade.php index 77adc7e..ef43a3d 100644 --- a/resources/views/admin/partial/menu.blade.php +++ b/resources/views/admin/partial/menu.blade.php @@ -67,6 +67,16 @@ +
  • + + + لیگ و چالش‌ها + + +
  • diff --git a/routes/api.php b/routes/api.php index babb01b..58070a7 100644 --- a/routes/api.php +++ b/routes/api.php @@ -23,6 +23,7 @@ use App\Http\Controllers\SuggestionController; use Illuminate\Support\Facades\Route; use App\Http\Controllers\api\QuizController; use App\Http\Controllers\api\PvPQuizController; +use App\Http\Controllers\api\LeagueController; Route::prefix('v1')->group(function () { Route::post('login', [AuthController::class, 'login']); @@ -165,5 +166,12 @@ Route::prefix('v1')->middleware('auth:sanctum')->group(function () { Route::get('market/gold', [CurrencyController::class, 'goldPrices']); + // PvP & League Gamification Routes + Route::prefix('league')->group(function () { + Route::get('/stats', [LeagueController::class, 'stats']); + Route::get('/quests', [LeagueController::class, 'quests']); + Route::get('/leaderboard', [LeagueController::class, 'leaderboard']); + Route::post('/pvp/matchmake', [LeagueController::class, 'matchmake']); + }); }); diff --git a/routes/web.php b/routes/web.php index abd07ee..0c90338 100644 --- a/routes/web.php +++ b/routes/web.php @@ -34,6 +34,7 @@ use App\Http\Controllers\Admin\DashboardController; use App\Http\Controllers\Admin\QuizController; use App\Http\Controllers\Admin\QuizCategoryController; use App\Http\Controllers\Admin\QuizSubjectController; +use App\Http\Controllers\Admin\DailyQuestController; Route::get('payment/callback', [PayController::class, 'callback']); @@ -89,5 +90,9 @@ Route::middleware(['auth', config('jetstream.auth_session')])->group(function () Route::post('/users/revoke-subscription', [\App\Http\Controllers\Admin\UsersController::class, 'revokeSubscription']) ->name('admin.subscriptions.revoke'); + Route::prefix('admin')->middleware(['auth', 'admin'])->group(function () { + // ... your other admin routes + Route::resource('daily-quests', DailyQuestController::class)->except(['show']); + }); });