Version 1.0 #102
@@ -255,7 +255,6 @@ class MatchController extends Controller
|
|||||||
|
|
||||||
public function open(Request $request)
|
public function open(Request $request)
|
||||||
{
|
{
|
||||||
// FIX: Allow matches where P2 is NULL OR 'Ghost' (ID 1)
|
|
||||||
$query = MatchGame::where('status', 'Pending')
|
$query = MatchGame::where('status', 'Pending')
|
||||||
->where(function($q) {
|
->where(function($q) {
|
||||||
$q->whereNull('player2_user_id')
|
$q->whereNull('player2_user_id')
|
||||||
@@ -272,4 +271,54 @@ class MatchController extends Controller
|
|||||||
|
|
||||||
return response()->json($matches);
|
return response()->json($matches);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- FIX: Safely Delete Match by removing dependencies first ---
|
||||||
|
public function destroy(MatchGame $match)
|
||||||
|
{
|
||||||
|
$user = request()->user();
|
||||||
|
|
||||||
|
if ($match->player1_user_id !== $user->id) {
|
||||||
|
return response()->json(['message' => 'Unauthorized'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($match->status !== 'Pending') {
|
||||||
|
return response()->json(['message' => 'Cannot cancel a match that has already started'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($match, $user) {
|
||||||
|
// 1. Delete associated Games first (The FK constraint cause)
|
||||||
|
DB::table('games')->where('match_id', $match->id)->delete();
|
||||||
|
|
||||||
|
// 2. Unlink existing Coin Transactions (Set match_id to NULL)
|
||||||
|
CoinTransaction::where('match_id', $match->id)->update(['match_id' => null]);
|
||||||
|
|
||||||
|
// 3. Process Refund
|
||||||
|
if ($match->stake > 0) {
|
||||||
|
$user->increment('coins_balance', $match->stake);
|
||||||
|
|
||||||
|
$refundType = CoinTransactionType::firstOrCreate(
|
||||||
|
['name' => 'Refund'],
|
||||||
|
['type' => 'C']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create Refund Transaction with NO LINK to the deleted match
|
||||||
|
CoinTransaction::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'match_id' => null, // IMPORTANT: Must be null
|
||||||
|
'coin_transaction_type_id' => $refundType->id,
|
||||||
|
'transaction_datetime' => now(),
|
||||||
|
'coins' => $match->stake,
|
||||||
|
'custom' => "Refund for Match #{$match->id}"
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Finally delete the match
|
||||||
|
$match->delete();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Match canceled and refunded',
|
||||||
|
'balance' => $user->coins_balance
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ namespace App\Models;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
||||||
|
|
||||||
class CoinTransactionType extends Model
|
class CoinTransactionType extends Model
|
||||||
{
|
{
|
||||||
use HasFactory, SoftDeletes;
|
use HasFactory;
|
||||||
|
|
||||||
protected $table = 'coin_transaction_types';
|
// FIX: Disable auto-timestamps because the table doesn't have updated_at/created_at columns
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
protected $fillable = ['name', 'type', 'custom'];
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
protected $casts = [
|
'type',
|
||||||
'custom' => 'array',
|
'description' // Include description if your table has it
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -62,6 +62,7 @@ Route::prefix('v1')->group(function () {
|
|||||||
Route::get('open', [MatchController::class, 'open']);
|
Route::get('open', [MatchController::class, 'open']);
|
||||||
Route::post('/{match}/join', [MatchController::class, 'join']);
|
Route::post('/{match}/join', [MatchController::class, 'join']);
|
||||||
Route::post('/{match}/start', [MatchController::class, 'start']); // Changed to /start to be explicit
|
Route::post('/{match}/start', [MatchController::class, 'start']); // Changed to /start to be explicit
|
||||||
|
Route::delete('/{match}', [MatchController::class, 'destroy']);
|
||||||
Route::apiResource('/', MatchController::class)->parameters(['' => 'match']);
|
Route::apiResource('/', MatchController::class)->parameters(['' => 'match']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -189,7 +189,9 @@ class BiscaGame {
|
|||||||
this.players[winnerId].tricks += 1;
|
this.players[winnerId].tricks += 1;
|
||||||
this.currentTurn = winnerId;
|
this.currentTurn = winnerId;
|
||||||
|
|
||||||
if (this.type == 3) {
|
// --- CRITICAL FIX: Always draw if cards remain ---
|
||||||
|
// Removed "if (this.type == 3)" check
|
||||||
|
if (this.deck.length > 0 || this.trumpCard) {
|
||||||
this.drawCard(winnerId);
|
this.drawCard(winnerId);
|
||||||
this.drawCard(this.getOpponentId(winnerId));
|
this.drawCard(this.getOpponentId(winnerId));
|
||||||
}
|
}
|
||||||
@@ -221,7 +223,7 @@ class BiscaGame {
|
|||||||
? p2Obj.id
|
? p2Obj.id
|
||||||
: p1Obj.id;
|
: p1Obj.id;
|
||||||
|
|
||||||
return {
|
const result = {
|
||||||
action: "game_ended",
|
action: "game_ended",
|
||||||
trickResult,
|
trickResult,
|
||||||
winnerId: finalWinner,
|
winnerId: finalWinner,
|
||||||
@@ -233,6 +235,7 @@ class BiscaGame {
|
|||||||
player2_points: p2Obj.points,
|
player2_points: p2Obj.points,
|
||||||
totalTime: totalTime,
|
totalTime: totalTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.onGameEnd) {
|
if (this.onGameEnd) {
|
||||||
this.onGameEnd(result);
|
this.onGameEnd(result);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import axios from "axios";
|
|||||||
const API_URL = "http://localhost:8000/api/v1";
|
const API_URL = "http://localhost:8000/api/v1";
|
||||||
|
|
||||||
class BiscaMatch {
|
class BiscaMatch {
|
||||||
constructor(apiData, emitStateCallback, token) {
|
// FIX: Added autoPlayCallback parameter
|
||||||
|
constructor(apiData, emitStateCallback, token, autoPlayCallback) {
|
||||||
this.id = apiData.id;
|
this.id = apiData.id;
|
||||||
this.emitStateCallback = emitStateCallback;
|
this.emitStateCallback = emitStateCallback;
|
||||||
|
this.autoPlayCallback = autoPlayCallback;
|
||||||
this.token = token;
|
this.token = token;
|
||||||
this.stake = apiData.stake;
|
this.stake = apiData.stake;
|
||||||
this.type = apiData.type;
|
this.type = apiData.type;
|
||||||
@@ -14,6 +16,8 @@ class BiscaMatch {
|
|||||||
|
|
||||||
this.player1Id = apiData.player1_user_id;
|
this.player1Id = apiData.player1_user_id;
|
||||||
this.player2Id = apiData.player2_user_id;
|
this.player2Id = apiData.player2_user_id;
|
||||||
|
this.player1 = apiData.player1;
|
||||||
|
this.player2 = apiData.player2;
|
||||||
|
|
||||||
this.marks = {
|
this.marks = {
|
||||||
[this.player1Id]: apiData.player1_marks || 0,
|
[this.player1Id]: apiData.player1_marks || 0,
|
||||||
@@ -33,11 +37,12 @@ class BiscaMatch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async joinPlayer(userId) {
|
async joinPlayer(userId, user) {
|
||||||
if (this.player2Id !== this.GHOST_ID) return false;
|
if (this.player2Id !== this.GHOST_ID) return false;
|
||||||
if (userId === this.player1Id) return false;
|
if (userId === this.player1Id) return false;
|
||||||
|
|
||||||
this.player2Id = userId;
|
this.player2Id = userId;
|
||||||
|
this.player2 = user;
|
||||||
this.marks[userId] = 0;
|
this.marks[userId] = 0;
|
||||||
this.points[userId] = 0;
|
this.points[userId] = 0;
|
||||||
this.status = "Playing";
|
this.status = "Playing";
|
||||||
@@ -48,90 +53,83 @@ class BiscaMatch {
|
|||||||
|
|
||||||
getAuthHeaders() {
|
getAuthHeaders() {
|
||||||
return {
|
return {
|
||||||
headers: { Authorization: this.token },
|
headers: {
|
||||||
|
Authorization: this.token,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json"
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async startNewGame() {
|
async startNewGame() {
|
||||||
console.log(`[Match ${this.id}] A iniciar nova mão (Game DB)...`);
|
console.log(`[Match ${this.id}] Starting new round...`);
|
||||||
|
|
||||||
let newGameDbId = null;
|
let newGameDbId = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let payloadP1 = this.player1Id;
|
|
||||||
let payloadP2 = this.player2Id;
|
|
||||||
|
|
||||||
if (this.player2Id !== this.GHOST_ID) {
|
|
||||||
console.log(
|
|
||||||
"🔄 Multiplayer detetado: Trocando ordem para satisfazer a API..."
|
|
||||||
);
|
|
||||||
payloadP1 = this.player2Id;
|
|
||||||
payloadP2 = this.player1Id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
type: this.type,
|
type: this.type,
|
||||||
player1_user_id: payloadP1,
|
player1_user_id: this.player1Id,
|
||||||
player2_user_id: payloadP2,
|
player2_user_id: this.player2Id,
|
||||||
match_id: this.id,
|
match_id: this.id,
|
||||||
status: "Playing",
|
status: "Playing",
|
||||||
began_at: new Date().toISOString(),
|
began_at: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("📤 [Payload Adaptado]:", JSON.stringify(payload, null, 2));
|
const res = await axios.post(`${API_URL}/games`, payload, this.getAuthHeaders());
|
||||||
|
|
||||||
const res = await axios.post(
|
|
||||||
`${API_URL}/games`,
|
|
||||||
payload,
|
|
||||||
this.getAuthHeaders()
|
|
||||||
);
|
|
||||||
|
|
||||||
const gameData = res.data.game || res.data.data || res.data;
|
const gameData = res.data.game || res.data.data || res.data;
|
||||||
newGameDbId = gameData.id;
|
newGameDbId = gameData.id;
|
||||||
|
|
||||||
console.log(`[Match ${this.id}] Game DB criado: #${newGameDbId}`);
|
const p1Name = this.player1?.nickname || `User ${this.player1Id}`;
|
||||||
|
const p2Name = this.player2?.nickname || `User ${this.player2Id}`;
|
||||||
const apiP1 = gameData.player1_user_id;
|
|
||||||
const apiP2 = gameData.player2_user_id;
|
|
||||||
|
|
||||||
const p1 = { id: apiP1, name: `User ${apiP1}` };
|
|
||||||
const p2 = { id: apiP2, name: `User ${apiP2}` };
|
|
||||||
|
|
||||||
this.currentGame = new BiscaGame(
|
this.currentGame = new BiscaGame(
|
||||||
this.id,
|
this.id,
|
||||||
this.type,
|
this.type,
|
||||||
p1,
|
{ id: this.player1Id, name: p1Name },
|
||||||
p2,
|
{ id: this.player2Id, name: p2Name },
|
||||||
newGameDbId,
|
newGameDbId,
|
||||||
(result) => {
|
(result) => { this.handleGameEnd(result); }
|
||||||
this.handleGameEnd(result);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.currentGame.init();
|
this.currentGame.init();
|
||||||
|
|
||||||
|
// FIX: Start Timer & Connect Callback
|
||||||
|
this.currentGame.startTurnTimer((uid, cid) => {
|
||||||
|
if (this.autoPlayCallback) this.autoPlayCallback(uid, cid);
|
||||||
|
});
|
||||||
|
|
||||||
if (this.emitStateCallback) {
|
if (this.emitStateCallback) {
|
||||||
this.emitStateCallback("match:new-round", this.getState());
|
this.emitStateCallback("match:new-round-start", null);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.response) {
|
console.error(`❌ Start Game Error: ${e.message}`);
|
||||||
console.error(
|
|
||||||
`❌ Erro API (${e.response.status}):`,
|
|
||||||
JSON.stringify(e.response.data)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.error(`❌ Erro Código: ${e.message}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- FIX: Close DB Game row on surrender ---
|
||||||
|
async abortCurrentGame(loserId) {
|
||||||
|
if (this.currentGame && this.currentGame.dbId) {
|
||||||
|
console.log(`[Match ${this.id}] Aborting Game #${this.currentGame.dbId}`);
|
||||||
|
try {
|
||||||
|
// Determine winner for the DB record based on who DIDN'T lose
|
||||||
|
const winnerId = loserId == this.player1Id ? this.player2Id : this.player1Id;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
status: "Ended",
|
||||||
|
winner_user_id: winnerId,
|
||||||
|
loser_user_id: loserId,
|
||||||
|
is_draw: false,
|
||||||
|
ended_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
await axios.put(`${API_URL}/games/${this.currentGame.dbId}`, payload, this.getAuthHeaders());
|
||||||
|
} catch(e) { console.error("Error aborting game:", e.message); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleGameEnd(result) {
|
async handleGameEnd(result) {
|
||||||
console.log(
|
console.log(`[Match ${this.id}] Round ended. Winner: ${result.winnerId}`);
|
||||||
`[Match ${this.id}] Mão terminada. Vencedor: ${result.winnerId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
this.points[result.player1_id] += result.player1_points;
|
if (this.points[result.player1_id] !== undefined) this.points[result.player1_id] += result.player1_points;
|
||||||
this.points[result.player2_id] += result.player2_points;
|
if (this.points[result.player2_id] !== undefined) this.points[result.player2_id] += result.player2_points;
|
||||||
|
|
||||||
if (this.currentGame && this.currentGame.dbId) {
|
if (this.currentGame && this.currentGame.dbId) {
|
||||||
try {
|
try {
|
||||||
@@ -144,81 +142,50 @@ class BiscaMatch {
|
|||||||
player2_points: result.player2_points,
|
player2_points: result.player2_points,
|
||||||
ended_at: new Date().toISOString(),
|
ended_at: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
await axios.put(`${API_URL}/games/${this.currentGame.dbId}`, payload, this.getAuthHeaders());
|
||||||
console.log(
|
} catch (e) { console.error("DB Game Update Error:", e.message); }
|
||||||
`📤 A enviar update para Game #${this.currentGame.dbId}...`
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- CORREÇÃO IMPORTANTE: Forçar o header aqui ---
|
|
||||||
const config = {
|
|
||||||
headers: {
|
|
||||||
Authorization: this.token,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Accept: "application/json",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await axios.put(
|
|
||||||
`${API_URL}/games/${this.currentGame.dbId}`,
|
|
||||||
payload,
|
|
||||||
config
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`✅ Game #${this.currentGame.dbId} atualizado com sucesso.`
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
// --- LOG DETALHADO PARA VER O MOTIVO DO 403 ---
|
|
||||||
if (e.response) {
|
|
||||||
console.error(`❌ ERRO CRÍTICO API (${e.response.status}):`);
|
|
||||||
// Isto vai imprimir o JSON exato que o Laravel devolve
|
|
||||||
console.error(JSON.stringify(e.response.data, null, 2));
|
|
||||||
} else {
|
|
||||||
console.error("Erro Axios:", e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn("⚠️ Ignorado: Não há ID de jogo na DB para atualizar.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... (resto da lógica de atribuir marcas e finishMatch mantém-se igual)
|
|
||||||
let marksToAdd = 0;
|
let marksToAdd = 0;
|
||||||
let roundWinnerId = result.winnerId;
|
let roundWinnerId = result.winnerId;
|
||||||
|
|
||||||
if (roundWinnerId) {
|
if (roundWinnerId) {
|
||||||
const winningScore =
|
const winningScore = roundWinnerId == result.player1_id ? result.player1_points : result.player2_points;
|
||||||
roundWinnerId == result.player1_id
|
|
||||||
? result.player1_points
|
|
||||||
: result.player2_points;
|
|
||||||
if (winningScore === 120) marksToAdd = 4;
|
if (winningScore === 120) marksToAdd = 4;
|
||||||
else if (winningScore > 90) marksToAdd = 2;
|
else if (winningScore > 90) marksToAdd = 2;
|
||||||
else if (winningScore >= 60) marksToAdd = 1;
|
else if (winningScore >= 60) marksToAdd = 1;
|
||||||
this.marks[roundWinnerId] += marksToAdd;
|
|
||||||
|
if (this.marks[roundWinnerId] !== undefined) this.marks[roundWinnerId] += marksToAdd;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.emitStateCallback) {
|
||||||
|
this.emitStateCallback("match:round-ended", {
|
||||||
|
winnerId: roundWinnerId,
|
||||||
|
marksAdded: marksToAdd,
|
||||||
|
p1Points: result.player1_points,
|
||||||
|
p2Points: result.player2_points,
|
||||||
|
p1Marks: this.marks[this.player1Id],
|
||||||
|
p2Marks: this.marks[this.player2Id],
|
||||||
|
reason: result.reason || 'points'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.marks[this.player1Id] >= 4 || this.marks[this.player2Id] >= 4) {
|
if (this.marks[this.player1Id] >= 4 || this.marks[this.player2Id] >= 4) {
|
||||||
await this.finishMatch();
|
await this.finishMatch();
|
||||||
if (this.emitStateCallback) {
|
if (this.emitStateCallback) this.emitStateCallback("match:ended-signal", null);
|
||||||
this.emitStateCallback("match:ended", this.getState());
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.log(`[Match ${this.id}] Ninguém ganhou 4 marcas. A baralhar...`);
|
setTimeout(() => {
|
||||||
await this.startNewGame();
|
this.startNewGame();
|
||||||
|
}, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async finishMatch() {
|
async finishMatch() {
|
||||||
this.status = "Ended";
|
this.status = "Ended";
|
||||||
|
const winnerId = this.marks[this.player1Id] >= 4 ? this.player1Id : this.player2Id;
|
||||||
|
const loserId = winnerId == this.player1Id ? this.player2Id : this.player1Id;
|
||||||
|
|
||||||
const winnerId =
|
const payload = {
|
||||||
this.marks[this.player1Id] >= 4 ? this.player1Id : this.player2Id;
|
|
||||||
const loserId =
|
|
||||||
winnerId == this.player1Id ? this.player2Id : this.player1Id;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await axios.put(
|
|
||||||
`${API_URL}/matches/${this.id}`,
|
|
||||||
{
|
|
||||||
status: "Ended",
|
status: "Ended",
|
||||||
winner_user_id: winnerId,
|
winner_user_id: winnerId,
|
||||||
loser_user_id: loserId,
|
loser_user_id: loserId,
|
||||||
@@ -226,48 +193,45 @@ class BiscaMatch {
|
|||||||
player2_marks: this.marks[this.player2Id],
|
player2_marks: this.marks[this.player2Id],
|
||||||
player1_points: this.points[this.player1Id],
|
player1_points: this.points[this.player1Id],
|
||||||
player2_points: this.points[this.player2Id],
|
player2_points: this.points[this.player2Id],
|
||||||
},
|
};
|
||||||
this.getAuthHeaders()
|
|
||||||
);
|
try {
|
||||||
console.log(`[Match ${this.id}] Encerrado com sucesso na API.`);
|
await axios.put(`${API_URL}/matches/${this.id}`, payload, this.getAuthHeaders());
|
||||||
|
console.log(`[Match ${this.id}] Match Closed in DB.`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Erro ao fechar Match API: ${e.message}`);
|
console.error(`Match Close Error: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
playCard(userId, cardId) {
|
playCard(userId, cardId) {
|
||||||
if (!this.currentGame || this.status !== "Playing") return;
|
if (!this.currentGame || this.status !== "Playing") return;
|
||||||
return this.currentGame.playCard(userId, cardId);
|
|
||||||
|
const result = this.currentGame.playCard(userId, cardId);
|
||||||
|
|
||||||
|
// FIX: Restart timer if game continues
|
||||||
|
if (result && (result.action === 'next_turn' || result.action === 'trick_resolved')) {
|
||||||
|
this.currentGame.startTurnTimer((uid, cid) => {
|
||||||
|
if (this.autoPlayCallback) this.autoPlayCallback(uid, cid);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getState() {
|
return result;
|
||||||
let gameState = null;
|
|
||||||
|
|
||||||
if (this.currentGame) {
|
|
||||||
const p1Id = this.player1Id;
|
|
||||||
const p2Id = this.player2Id;
|
|
||||||
|
|
||||||
if (this.currentGame.players && this.currentGame.players[p1Id]) {
|
|
||||||
gameState = this.currentGame.getStateForPlayer(p1Id);
|
|
||||||
} else if (this.currentGame.players && this.currentGame.players[p2Id]) {
|
|
||||||
gameState = this.currentGame.getStateForPlayer(p2Id);
|
|
||||||
} else if (this.currentGame.players) {
|
|
||||||
const availableIds = Object.keys(this.currentGame.players);
|
|
||||||
if (availableIds.length > 0) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ [Match ${this.id}] IDs desalinhados! A usar vista do user ${availableIds[0]}`
|
|
||||||
);
|
|
||||||
gameState = this.currentGame.getStateForPlayer(availableIds[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getGameState(userId) {
|
||||||
|
if (!this.currentGame) return null;
|
||||||
|
if (this.currentGame.players[userId]) return this.currentGame.getStateForPlayer(userId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPublicState() {
|
||||||
return {
|
return {
|
||||||
matchId: this.id,
|
matchId: this.id,
|
||||||
status: this.status,
|
status: this.status,
|
||||||
marks: this.marks,
|
marks: this.marks,
|
||||||
points: this.points,
|
points: this.points,
|
||||||
game: gameState,
|
player1: this.player1,
|
||||||
|
player2: this.player2
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+104
-84
@@ -5,134 +5,154 @@ import axios from "axios";
|
|||||||
const API_URL = "http://localhost:8000/api/v1";
|
const API_URL = "http://localhost:8000/api/v1";
|
||||||
|
|
||||||
export default (io, socket) => {
|
export default (io, socket) => {
|
||||||
|
|
||||||
|
const broadcastMatchUpdate = (matchId, match) => {
|
||||||
|
const roomName = `match_${matchId}`;
|
||||||
|
const room = io.sockets.adapter.rooms.get(roomName);
|
||||||
|
if (room) {
|
||||||
|
for (const socketId of room) {
|
||||||
|
const clientSocket = io.sockets.sockets.get(socketId);
|
||||||
|
if (!clientSocket || !clientSocket.user) continue;
|
||||||
|
const userId = clientSocket.user.id;
|
||||||
|
const publicState = match.getPublicState();
|
||||||
|
const gameState = match.getGameState(userId);
|
||||||
|
clientSocket.emit("match:update", { ...publicState, game: gameState });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const createMatchEmitter = (matchId) => (eventName, payload) => {
|
const createMatchEmitter = (matchId) => (eventName, payload) => {
|
||||||
|
const match = getMatch(matchId);
|
||||||
|
if (!match) return;
|
||||||
|
|
||||||
|
if (eventName === 'match:new-round-start' || eventName === 'match:ended-signal') {
|
||||||
|
broadcastMatchUpdate(matchId, match);
|
||||||
|
} else if (eventName === 'match:round-ended') {
|
||||||
|
io.to(`match_${matchId}`).emit('match:round-ended', payload);
|
||||||
|
} else {
|
||||||
io.to(`match_${matchId}`).emit(eventName, payload);
|
io.to(`match_${matchId}`).emit(eventName, payload);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- FIX: Timer Callback ---
|
||||||
|
const handleAutoPlay = (matchId) => (userId, cardId) => {
|
||||||
|
const match = getMatch(matchId);
|
||||||
|
if (!match || match.status !== "Playing") return;
|
||||||
|
|
||||||
|
console.log(`[Match ${matchId}] Auto-playing for User ${userId}`);
|
||||||
|
const result = match.playCard(userId, cardId);
|
||||||
|
|
||||||
|
if (result && result.action === "trick_resolved") {
|
||||||
|
io.to(`match_${matchId}`).emit("game:trick-end", result.trickResult);
|
||||||
|
setTimeout(() => { broadcastMatchUpdate(matchId, match); }, 1500);
|
||||||
|
} else {
|
||||||
|
broadcastMatchUpdate(matchId, match);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.on("match:enter", async (data) => {
|
socket.on("match:enter", async (data) => {
|
||||||
const { matchId } = data;
|
const { matchId } = data;
|
||||||
|
if (!socket.user || !socket.user.id) return socket.emit("error", { message: "Auth failed" });
|
||||||
if (!socket.user || !socket.user.id) {
|
|
||||||
return socket.emit("error", { message: "User not authenticated" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = socket.user.id;
|
const userId = socket.user.id;
|
||||||
const room = `match_${matchId}`;
|
socket.activeMatchId = matchId;
|
||||||
|
|
||||||
|
const room = `match_${matchId}`;
|
||||||
let match = getMatch(matchId);
|
let match = getMatch(matchId);
|
||||||
|
|
||||||
if (!match) {
|
if (!match) {
|
||||||
try {
|
try {
|
||||||
console.log(`A pedir match ${matchId} à API...`);
|
|
||||||
const res = await axios.get(`${API_URL}/matches/${matchId}`, {
|
const res = await axios.get(`${API_URL}/matches/${matchId}`, {
|
||||||
headers: { Authorization: socket.token },
|
headers: { Authorization: socket.token || socket.handshake.auth.token },
|
||||||
});
|
});
|
||||||
|
|
||||||
const matchData = res.data.data || res.data;
|
const matchData = res.data.data || res.data;
|
||||||
|
|
||||||
|
// Pass handleAutoPlay to constructor
|
||||||
match = new BiscaMatch(
|
match = new BiscaMatch(
|
||||||
matchData,
|
matchData,
|
||||||
createMatchEmitter(matchId),
|
createMatchEmitter(matchId),
|
||||||
socket.token
|
socket.token,
|
||||||
|
handleAutoPlay(matchId)
|
||||||
);
|
);
|
||||||
addMatch(match);
|
addMatch(match);
|
||||||
console.log(`Match ${matchId} carregado em memória.`);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Erro ao carregar Match da API:", e.message);
|
return socket.emit("error", "Match not found");
|
||||||
socket.emit("error", "Match not found or API error");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
match.token = socket.token;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (match) {
|
|
||||||
match.token = socket.token;
|
|
||||||
}
|
|
||||||
|
|
||||||
match.emitStateCallback = createMatchEmitter(matchId);
|
match.emitStateCallback = createMatchEmitter(matchId);
|
||||||
|
if (userId !== match.player1Id && match.player2Id === 1) await match.joinPlayer(userId, socket.user);
|
||||||
if (userId !== match.player1Id && match.player2Id === 1) {
|
|
||||||
console.log(`User ${userId} a substituir o Ghost no Match ${matchId}`);
|
|
||||||
await match.joinPlayer(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.join(room);
|
socket.join(room);
|
||||||
console.log(`Socket ${socket.id} entrou na sala ${room}`);
|
const publicState = match.getPublicState();
|
||||||
|
const gameState = match.getGameState(userId);
|
||||||
if (match.status === "Playing") {
|
socket.emit("match:update", { ...publicState, game: gameState });
|
||||||
socket.emit("match:update", match.getState());
|
if (match.status === 'Playing') broadcastMatchUpdate(matchId, match);
|
||||||
} else {
|
|
||||||
socket.emit("match:waiting", { msg: "Waiting for opponent..." });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("game:play-card", (data) => {
|
socket.on("game:play-card", (data) => {
|
||||||
const { matchId, cardId } = data;
|
const { matchId, cardId } = data;
|
||||||
const match = getMatch(matchId);
|
const match = getMatch(matchId);
|
||||||
|
if (!match || match.status !== "Playing") return;
|
||||||
if (!match || match.status !== "Playing") {
|
|
||||||
console.log(
|
|
||||||
"Tentativa de jogar sem match ativo ou match não encontrado."
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = match.playCard(socket.user.id, cardId);
|
const result = match.playCard(socket.user.id, cardId);
|
||||||
|
if (result && result.error) return socket.emit("error", { message: result.error });
|
||||||
if (result && result.error) {
|
if (result && result.action === "trick_resolved") {
|
||||||
console.log(
|
io.to(`match_${matchId}`).emit("game:trick-end", result.trickResult);
|
||||||
`❌ [Erro Jogo] User ${socket.user.id} tentou jogar carta ${cardId} mas: ${result.error}`
|
setTimeout(() => { broadcastMatchUpdate(matchId, match); }, 1500);
|
||||||
);
|
} else {
|
||||||
|
broadcastMatchUpdate(matchId, match);
|
||||||
socket.emit("error", { message: result.error });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (match.currentGame) {
|
|
||||||
io.to(`match_${matchId}`).emit(
|
|
||||||
"game:update",
|
|
||||||
match.currentGame.getStateForPlayer(socket.user.id)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
socket.on("debug:end-game", async (data) => {
|
|
||||||
const { matchId } = data;
|
|
||||||
const match = getMatch(matchId);
|
|
||||||
if (match) {
|
|
||||||
console.log(`[DEBUG] User ${socket.user.id} a forçar fim de jogo.`);
|
|
||||||
// Simula que o user que clicou ganhou 120 pontos
|
|
||||||
const winnerId = socket.user.id;
|
|
||||||
const loserId =
|
|
||||||
winnerId == match.player1Id ? match.player2Id : match.player1Id;
|
|
||||||
|
|
||||||
// Objeto de resultado falso para fechar o jogo
|
socket.on("game:surrender", async ({ matchId }) => {
|
||||||
const fakeResult = {
|
const match = getMatch(matchId);
|
||||||
winnerId: winnerId,
|
if(!match || match.status !== "Playing") return;
|
||||||
loserId: loserId,
|
const userId = socket.user.id;
|
||||||
|
console.log(`[Match ${matchId}] User ${userId} surrendered ROUND.`);
|
||||||
|
const opponentId = match.player1Id == userId ? match.player2Id : match.player1Id;
|
||||||
|
const result = {
|
||||||
|
winnerId: opponentId,
|
||||||
|
loserId: userId,
|
||||||
isDraw: false,
|
isDraw: false,
|
||||||
player1_points: winnerId == match.player1Id ? 120 : 0,
|
|
||||||
player2_points: winnerId == match.player2Id ? 120 : 0,
|
|
||||||
player1_id: match.player1Id,
|
player1_id: match.player1Id,
|
||||||
player2_id: match.player2Id,
|
player2_id: match.player2Id,
|
||||||
|
player1_points: match.player1Id == opponentId ? 91 : 0,
|
||||||
|
player2_points: match.player2Id == opponentId ? 91 : 0,
|
||||||
|
reason: 'surrender'
|
||||||
};
|
};
|
||||||
|
await match.handleGameEnd(result);
|
||||||
await match.handleGameEnd(fakeResult);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// DEBUG: Forçar fim do Match Completo
|
socket.on("match:surrender", async ({ matchId }) => {
|
||||||
socket.on("debug:end-match", async (data) => {
|
|
||||||
const { matchId } = data;
|
|
||||||
const match = getMatch(matchId);
|
const match = getMatch(matchId);
|
||||||
if (match) {
|
if(!match || match.status !== "Playing") return;
|
||||||
console.log(`[DEBUG] User ${socket.user.id} a forçar fim de MATCH.`);
|
const userId = socket.user.id;
|
||||||
// Dá 4 marcas a quem clicou
|
|
||||||
match.marks[socket.user.id] = 4;
|
|
||||||
await match.finishMatch();
|
|
||||||
|
|
||||||
// Emite o estado final
|
// FIX: Abort current game to remove ghost
|
||||||
io.to(`match_${matchId}`).emit("match:ended", match.getState());
|
await match.abortCurrentGame(userId);
|
||||||
|
|
||||||
|
console.log(`[Match ${matchId}] User ${userId} surrendered MATCH.`);
|
||||||
|
const opponentId = match.player1Id == userId ? match.player2Id : match.player1Id;
|
||||||
|
match.marks[opponentId] = 4;
|
||||||
|
await match.finishMatch();
|
||||||
|
broadcastMatchUpdate(matchId, match);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("disconnect", async () => {
|
||||||
|
if (socket.activeMatchId) {
|
||||||
|
const match = getMatch(socket.activeMatchId);
|
||||||
|
if (match && match.status === 'Playing') {
|
||||||
|
const userId = socket.user?.id;
|
||||||
|
if (userId === match.player1Id || userId === match.player2Id) {
|
||||||
|
console.log(`[Match ${match.id}] Player ${userId} disconnected.`);
|
||||||
|
|
||||||
|
// FIX: Abort current game
|
||||||
|
await match.abortCurrentGame(userId);
|
||||||
|
|
||||||
|
const opponentId = match.player1Id == userId ? match.player2Id : match.player1Id;
|
||||||
|
match.marks[opponentId] = 4;
|
||||||
|
await match.finishMatch();
|
||||||
|
broadcastMatchUpdate(match.id, match);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user