From 76d9246e0e6e393f25f0b235453f34ac2fd498a8 Mon Sep 17 00:00:00 2001 From: "m.razaghi" Date: Thu, 20 Aug 2026 16:21:03 +0330 Subject: [PATCH] Added quiz new features --- .env | 9 +++ app/Events/MatchEnded.php | 32 +++++++++ app/Events/MatchStarted.php | 35 +++++++++ .../Controllers/api/PvPQuizController.php | 71 +++++++++++++++---- app/Providers/EventServiceProvider.php | 2 +- config/app.php | 2 +- config/broadcasting.php | 2 +- routes/channels.php | 21 +++++- 8 files changed, 156 insertions(+), 18 deletions(-) create mode 100644 .env create mode 100644 app/Events/MatchEnded.php create mode 100644 app/Events/MatchStarted.php diff --git a/.env b/.env new file mode 100644 index 0000000..771fc59 --- /dev/null +++ b/.env @@ -0,0 +1,9 @@ +BROADCAST_DRIVER=pusher + +PUSHER_APP_ID=2184469 +PUSHER_APP_KEY=ecd88d278bd51427da98 +PUSHER_APP_SECRET=3b1cb32eb9ddda591f65 +PUSHER_HOST= +PUSHER_PORT=443 +PUSHER_SCHEME=https +PUSHER_APP_CLUSTER=eu diff --git a/app/Events/MatchEnded.php b/app/Events/MatchEnded.php new file mode 100644 index 0000000..354337d --- /dev/null +++ b/app/Events/MatchEnded.php @@ -0,0 +1,32 @@ +matchId = $matchId; + } + + public function broadcastOn() + { + // ارسال پیام به کانال پرایوت بازی که هر دو نفر داخل آن هستند + return new PrivateChannel('pvp-match.' . $this->matchId); + } + + public function broadcastAs() + { + return 'match.ended'; + } +} diff --git a/app/Events/MatchStarted.php b/app/Events/MatchStarted.php new file mode 100644 index 0000000..0ceef6f --- /dev/null +++ b/app/Events/MatchStarted.php @@ -0,0 +1,35 @@ +matchData = $matchData; + $this->ticketId = $ticketId; + } + + public function broadcastOn() + { + // ارسال پیام به کانال پرایوت مختص تیکت کاربری که در صف انتظار بود + return new PrivateChannel('matchmaking.' . $this->ticketId); + } + + public function broadcastAs() + { + // نام ایونتی که کاتلین باید به آن گوش دهد + return 'match.started'; + } +} diff --git a/app/Http/Controllers/api/PvPQuizController.php b/app/Http/Controllers/api/PvPQuizController.php index 97741e0..c255646 100644 --- a/app/Http/Controllers/api/PvPQuizController.php +++ b/app/Http/Controllers/api/PvPQuizController.php @@ -8,6 +8,7 @@ use App\Models\PvpMatch; use App\Traits\BaseApiResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Str; class PvPQuizController extends Controller { @@ -19,12 +20,13 @@ class PvPQuizController extends Controller public function findMatch(Request $request) { $userId = auth()->id(); + $ticketId = $request->input('ticket_id') ?? 'req_' . Str::random(12); // 1. Check if anyone else is waiting in the cache queue $waitingPlayer = Cache::get('pvp_waiting_player'); // Ensure we don't match the user with themselves - if ($waitingPlayer && $waitingPlayer != $userId) { + if ($waitingPlayer && $waitingPlayer['user_id'] != $userId) { // MATCH FOUND! Remove the waiting player from the queue Cache::forget('pvp_waiting_player'); @@ -37,7 +39,7 @@ class PvPQuizController extends Controller // Add both players to the match $match->players()->createMany([ - ['user_id' => $waitingPlayer, 'score' => 0], + ['user_id' => $waitingPlayer['user_id'], 'score' => 0], ['user_id' => $userId, 'score' => 0], ]); @@ -50,23 +52,33 @@ class PvPQuizController extends Controller // Attach questions to the match $match->questions()->attach($questions); - // TODO (Next Step): Fire a WebSocket Event here to tell Player 1 the match started! - - return $this->success([ + // Prepare the payload for both players + $matchData = [ 'match_id' => $match->id, 'status' => 'matched', - 'opponent_id' => $waitingPlayer, 'questions' => $match->questions()->with('options')->get()->makeHidden(['solution_explanation'])->each(function($q) { $q->options->each->makeHidden(['is_correct']); }) - ], 'موفق', 'حریف پیدا شد! بازی شروع می‌شود.'); + ]; + + // FIRE THE WEBSOCKET EVENT TO PLAYER A! + event(new \App\Events\MatchStarted($matchData, $waitingPlayer['ticket_id'])); + + // Return the same payload to Player B via standard HTTP response + $matchData['opponent_id'] = $waitingPlayer['user_id']; + + return $this->success($matchData, 'موفق', 'حریف پیدا شد! بازی شروع می‌شود.'); } - // NO MATCH FOUND YET. Put this user in the waiting queue for 30 seconds. - Cache::put('pvp_waiting_player', $userId, 30); + // NO MATCH FOUND YET. Put Player A in the waiting queue and save their Ticket ID + Cache::put('pvp_waiting_player', [ + 'user_id' => $userId, + 'ticket_id' => $ticketId + ], 30); return $this->success([ - 'status' => 'waiting' + 'status' => 'waiting', + 'ticket_id' => $ticketId ], 'در حال جستجو', 'در حال یافتن حریف...'); } @@ -169,28 +181,59 @@ class PvPQuizController extends Controller 'winner_id' => $winnerId ]); - // --- NEW: UPDATE GAMIFICATION STATS --- + // --- UPDATE GAMIFICATION STATS --- $user1 = \App\Models\User::find($player1->user_id); $user2 = \App\Models\User::find($player2->user_id); $user1->increment('duels_played'); $user2->increment('duels_played'); - // Give XP and Wins + // Give XP, Wins, and update "Win Duel" Quests if ($winnerId) { $winner = \App\Models\User::find($winnerId); $winner->increment('duels_won'); $winner->increment('xp', 50); // Winner gets 50 XP - // NOTE: Here we will later trigger the "duel_win" Daily Quest progress! + $duelQuest = $winner->activeQuests() + ->whereHas('quest', function ($q) { + $q->where('quest_type', 'duel_win'); + })->where('is_completed', false)->first(); + + if ($duelQuest) { + $duelQuest->increment('current_progress'); + + if ($duelQuest->current_progress >= $duelQuest->quest->required_amount) { + $duelQuest->update(['is_completed' => true]); + $winner->increment('xp', $duelQuest->quest->reward_xp); + } + } } // Loser/Draw gets participation XP $loserId = $winnerId === $user1->id ? $user2->id : $user1->id; \App\Models\User::find($loserId)->increment('xp', 10); - // TODO: Fire a WebSocket Event to push "Game Over" screen! + // Update "Answer Tests" Quest Progress for BOTH players + foreach ([$user1, $user2] as $player) { + $testQuest = $player->activeQuests() + ->whereHas('quest', function ($q) { + $q->where('quest_type', 'answer_tests'); + })->where('is_completed', false)->first(); + + if ($testQuest) { + $testQuest->increment('current_progress', 5); // They both answered 5 questions + + if ($testQuest->current_progress >= $testQuest->quest->required_amount) { + $testQuest->update(['is_completed' => true]); + $player->increment('xp', $testQuest->quest->reward_xp); + } + } + } + + // FIRE THE WEBSOCKET EVENT TO END MATCH! + event(new \App\Events\MatchEnded($match->id)); } + /** * STEP 4: Get Final Match Results */ diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 2d65aac..8d97a95 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -10,7 +10,7 @@ use Illuminate\Support\Facades\Event; class EventServiceProvider extends ServiceProvider { /** - * The event to listener mappings for the application. + * The Events to listener mappings for the application. * * @var array> */ diff --git a/config/app.php b/config/app.php index 343bf6f..8cdb290 100644 --- a/config/app.php +++ b/config/app.php @@ -165,7 +165,7 @@ return [ */ App\Providers\AppServiceProvider::class, App\Providers\AuthServiceProvider::class, - // App\Providers\BroadcastServiceProvider::class, + App\Providers\BroadcastServiceProvider::class, App\Providers\EventServiceProvider::class, App\Providers\RouteServiceProvider::class, App\Providers\FortifyServiceProvider::class, diff --git a/config/broadcasting.php b/config/broadcasting.php index 2410485..1d46c28 100644 --- a/config/broadcasting.php +++ b/config/broadcasting.php @@ -8,7 +8,7 @@ return [ |-------------------------------------------------------------------------- | | This option controls the default broadcaster that will be used by the - | framework when an event needs to be broadcast. You may set this to + | framework when an Events needs to be broadcast. You may set this to | any of the connections defined in the "connections" array below. | | Supported: "pusher", "ably", "redis", "log", "null" diff --git a/routes/channels.php b/routes/channels.php index 5d451e1..9afc718 100644 --- a/routes/channels.php +++ b/routes/channels.php @@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Broadcast; | Broadcast Channels |-------------------------------------------------------------------------- | -| Here you may register all of the event broadcasting channels that your +| Here you may register all of the Events broadcasting channels that your | application supports. The given channel authorization callbacks are | used to check if an authenticated user can listen to the channel. | @@ -16,3 +16,22 @@ use Illuminate\Support\Facades\Broadcast; Broadcast::channel('App.Models.User.{id}', function ($user, $id) { return (int) $user->id === (int) $id; }); + + + +// کانال انتظار برای پیدا شدن حریف +Broadcast::channel('matchmaking.{ticketId}', function ($user, $ticketId) { + // در این مرحله، هر کاربری که لاگین باشد و تیکت خودش را داشته باشد اجازه دسترسی دارد + return true; +}); + +// کانال داخل بازی (برای ارسال پایان بازی یا تغییرات زنده) +Broadcast::channel('pvp-match.{matchId}', function ($user, $matchId) { + $match = \App\Models\PvpMatch::find($matchId); + if (!$match) { + return false; + } + + // فقط اگر این کاربر یکی از دو بازیکنِ این مسابقه باشد، اجازه دسترسی به کانال را دارد + return $match->players()->where('user_id', $user->id)->exists(); +});