Added quiz new features
This commit is contained in:
9
.env
Normal file
9
.env
Normal file
@@ -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
|
||||||
32
app/Events/MatchEnded.php
Normal file
32
app/Events/MatchEnded.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||||
|
use Illuminate\Broadcasting\PrivateChannel;
|
||||||
|
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class MatchEnded implements ShouldBroadcastNow
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||||
|
|
||||||
|
public $matchId;
|
||||||
|
|
||||||
|
public function __construct($matchId)
|
||||||
|
{
|
||||||
|
$this->matchId = $matchId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function broadcastOn()
|
||||||
|
{
|
||||||
|
// ارسال پیام به کانال پرایوت بازی که هر دو نفر داخل آن هستند
|
||||||
|
return new PrivateChannel('pvp-match.' . $this->matchId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function broadcastAs()
|
||||||
|
{
|
||||||
|
return 'match.ended';
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/Events/MatchStarted.php
Normal file
35
app/Events/MatchStarted.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||||
|
use Illuminate\Broadcasting\PrivateChannel;
|
||||||
|
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class MatchStarted implements ShouldBroadcastNow
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||||
|
|
||||||
|
public $matchData;
|
||||||
|
public $ticketId;
|
||||||
|
|
||||||
|
public function __construct($matchData, $ticketId)
|
||||||
|
{
|
||||||
|
$this->matchData = $matchData;
|
||||||
|
$this->ticketId = $ticketId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function broadcastOn()
|
||||||
|
{
|
||||||
|
// ارسال پیام به کانال پرایوت مختص تیکت کاربری که در صف انتظار بود
|
||||||
|
return new PrivateChannel('matchmaking.' . $this->ticketId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function broadcastAs()
|
||||||
|
{
|
||||||
|
// نام ایونتی که کاتلین باید به آن گوش دهد
|
||||||
|
return 'match.started';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ use App\Models\PvpMatch;
|
|||||||
use App\Traits\BaseApiResponse;
|
use App\Traits\BaseApiResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class PvPQuizController extends Controller
|
class PvPQuizController extends Controller
|
||||||
{
|
{
|
||||||
@@ -19,12 +20,13 @@ class PvPQuizController extends Controller
|
|||||||
public function findMatch(Request $request)
|
public function findMatch(Request $request)
|
||||||
{
|
{
|
||||||
$userId = auth()->id();
|
$userId = auth()->id();
|
||||||
|
$ticketId = $request->input('ticket_id') ?? 'req_' . Str::random(12);
|
||||||
|
|
||||||
// 1. Check if anyone else is waiting in the cache queue
|
// 1. Check if anyone else is waiting in the cache queue
|
||||||
$waitingPlayer = Cache::get('pvp_waiting_player');
|
$waitingPlayer = Cache::get('pvp_waiting_player');
|
||||||
|
|
||||||
// Ensure we don't match the user with themselves
|
// 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
|
// MATCH FOUND! Remove the waiting player from the queue
|
||||||
Cache::forget('pvp_waiting_player');
|
Cache::forget('pvp_waiting_player');
|
||||||
@@ -37,7 +39,7 @@ class PvPQuizController extends Controller
|
|||||||
|
|
||||||
// Add both players to the match
|
// Add both players to the match
|
||||||
$match->players()->createMany([
|
$match->players()->createMany([
|
||||||
['user_id' => $waitingPlayer, 'score' => 0],
|
['user_id' => $waitingPlayer['user_id'], 'score' => 0],
|
||||||
['user_id' => $userId, 'score' => 0],
|
['user_id' => $userId, 'score' => 0],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -50,23 +52,33 @@ class PvPQuizController extends Controller
|
|||||||
// Attach questions to the match
|
// Attach questions to the match
|
||||||
$match->questions()->attach($questions);
|
$match->questions()->attach($questions);
|
||||||
|
|
||||||
// TODO (Next Step): Fire a WebSocket Event here to tell Player 1 the match started!
|
// Prepare the payload for both players
|
||||||
|
$matchData = [
|
||||||
return $this->success([
|
|
||||||
'match_id' => $match->id,
|
'match_id' => $match->id,
|
||||||
'status' => 'matched',
|
'status' => 'matched',
|
||||||
'opponent_id' => $waitingPlayer,
|
|
||||||
'questions' => $match->questions()->with('options')->get()->makeHidden(['solution_explanation'])->each(function($q) {
|
'questions' => $match->questions()->with('options')->get()->makeHidden(['solution_explanation'])->each(function($q) {
|
||||||
$q->options->each->makeHidden(['is_correct']);
|
$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.
|
// NO MATCH FOUND YET. Put Player A in the waiting queue and save their Ticket ID
|
||||||
Cache::put('pvp_waiting_player', $userId, 30);
|
Cache::put('pvp_waiting_player', [
|
||||||
|
'user_id' => $userId,
|
||||||
|
'ticket_id' => $ticketId
|
||||||
|
], 30);
|
||||||
|
|
||||||
return $this->success([
|
return $this->success([
|
||||||
'status' => 'waiting'
|
'status' => 'waiting',
|
||||||
|
'ticket_id' => $ticketId
|
||||||
], 'در حال جستجو', 'در حال یافتن حریف...');
|
], 'در حال جستجو', 'در حال یافتن حریف...');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,28 +181,59 @@ class PvPQuizController extends Controller
|
|||||||
'winner_id' => $winnerId
|
'winner_id' => $winnerId
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// --- NEW: UPDATE GAMIFICATION STATS ---
|
// --- UPDATE GAMIFICATION STATS ---
|
||||||
$user1 = \App\Models\User::find($player1->user_id);
|
$user1 = \App\Models\User::find($player1->user_id);
|
||||||
$user2 = \App\Models\User::find($player2->user_id);
|
$user2 = \App\Models\User::find($player2->user_id);
|
||||||
|
|
||||||
$user1->increment('duels_played');
|
$user1->increment('duels_played');
|
||||||
$user2->increment('duels_played');
|
$user2->increment('duels_played');
|
||||||
|
|
||||||
// Give XP and Wins
|
// Give XP, Wins, and update "Win Duel" Quests
|
||||||
if ($winnerId) {
|
if ($winnerId) {
|
||||||
$winner = \App\Models\User::find($winnerId);
|
$winner = \App\Models\User::find($winnerId);
|
||||||
$winner->increment('duels_won');
|
$winner->increment('duels_won');
|
||||||
$winner->increment('xp', 50); // Winner gets 50 XP
|
$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
|
// Loser/Draw gets participation XP
|
||||||
$loserId = $winnerId === $user1->id ? $user2->id : $user1->id;
|
$loserId = $winnerId === $user1->id ? $user2->id : $user1->id;
|
||||||
\App\Models\User::find($loserId)->increment('xp', 10);
|
\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
|
* STEP 4: Get Final Match Results
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use Illuminate\Support\Facades\Event;
|
|||||||
class EventServiceProvider extends ServiceProvider
|
class EventServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The event to listener mappings for the application.
|
* The Events to listener mappings for the application.
|
||||||
*
|
*
|
||||||
* @var array<class-string, array<int, class-string>>
|
* @var array<class-string, array<int, class-string>>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ return [
|
|||||||
*/
|
*/
|
||||||
App\Providers\AppServiceProvider::class,
|
App\Providers\AppServiceProvider::class,
|
||||||
App\Providers\AuthServiceProvider::class,
|
App\Providers\AuthServiceProvider::class,
|
||||||
// App\Providers\BroadcastServiceProvider::class,
|
App\Providers\BroadcastServiceProvider::class,
|
||||||
App\Providers\EventServiceProvider::class,
|
App\Providers\EventServiceProvider::class,
|
||||||
App\Providers\RouteServiceProvider::class,
|
App\Providers\RouteServiceProvider::class,
|
||||||
App\Providers\FortifyServiceProvider::class,
|
App\Providers\FortifyServiceProvider::class,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ return [
|
|||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| This option controls the default broadcaster that will be used by the
|
| 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.
|
| any of the connections defined in the "connections" array below.
|
||||||
|
|
|
|
||||||
| Supported: "pusher", "ably", "redis", "log", "null"
|
| Supported: "pusher", "ably", "redis", "log", "null"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Broadcast;
|
|||||||
| Broadcast Channels
|
| 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
|
| application supports. The given channel authorization callbacks are
|
||||||
| used to check if an authenticated user can listen to the channel.
|
| 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) {
|
Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
|
||||||
return (int) $user->id === (int) $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();
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user