This commit is contained in:
Diogo
2026-03-13 14:57:46 +00:00
parent cae3bbfe3b
commit 142f088763
4 changed files with 266 additions and 133 deletions

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
@@ -6,7 +7,7 @@ class ShotRecord {
final double relativeX;
final double relativeY;
final bool isMake;
final String playerName; // Bónus: Agora guardamos quem foi o jogador!
final String playerName;
ShotRecord({
required this.relativeX,
@@ -32,7 +33,6 @@ class PlacarController {
bool isLoading = true;
bool isSaving = false;
// 👇 TRINCO DE SEGURANÇA: Evita contar vitórias duas vezes se clicares no Guardar repetidamente!
bool gameWasAlreadyFinished = false;
int myScore = 0;
@@ -67,7 +67,12 @@ class PlacarController {
Timer? timer;
bool isRunning = false;
// --- 🔄 CARREGAMENTO COMPLETO (DADOS REAIS + ESTATÍSTICAS SALVAS) ---
// 👇 VARIÁVEIS DE CALIBRAÇÃO DO CAMPO (OS TEUS NÚMEROS!) 👇
bool isCalibrating = false;
double hoopBaseX = 0.088;
double arcRadius = 0.459;
double cornerY = 0.440;
Future<void> loadPlayers() async {
final supabase = Supabase.instance.client;
try {
@@ -95,7 +100,6 @@ class PlacarController {
opponentTimeoutsUsed = int.tryParse(gameResponse['opp_timeouts']?.toString() ?? '0') ?? 0;
currentQuarter = int.tryParse(gameResponse['current_quarter']?.toString() ?? '1') ?? 1;
// 👇 Verifica se o jogo já tinha acabado noutra sessão
gameWasAlreadyFinished = gameResponse['status'] == 'Terminado';
final teamsResponse = await supabase.from('teams').select('id, name').inFilter('name', [myTeam, opponentTeam]);
@@ -269,29 +273,34 @@ class PlacarController {
onUpdate();
}
void registerShotLocation(BuildContext context, Offset position, Size size) {
// =========================================================================
// 👇 A MÁGICA DOS PONTOS ACONTECE AQUI 👇
// =========================================================================
void registerShotLocation(BuildContext context, Offset position, Size size) {
if (pendingAction == null || pendingPlayer == null) return;
bool is3Pt = pendingAction!.contains("_3");
bool is2Pt = pendingAction!.contains("_2");
// O ÁRBITRO MATEMÁTICO COM AS TUAS VARIÁVEIS CALIBRADAS
if (is3Pt || is2Pt) {
bool isValid = _validateShotZone(position, size, is3Pt);
// SE A JOGADA FOI NO SÍTIO ERRADO
if (!isValid) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('🛑 Local incompatível com a pontuação.'), backgroundColor: Colors.red, duration: Duration(seconds: 2)));
return;
return; // <-- ESTE RETURN BLOQUEIA A GRAVAÇÃO DO PONTO!
}
}
// SE A JOGADA FOI VÁLIDA:
bool isMake = pendingAction!.startsWith("add_pts_");
// 👇 A MÁGICA DAS COORDENADAS RELATIVAS (0.0 a 1.0) 👇
double relX = position.dx / size.width;
double relY = position.dy / size.height;
// Extrai só o nome do jogador
String name = pendingPlayer!.replaceAll("player_my_", "").replaceAll("player_opp_", "");
// Guarda na lista!
matchShots.add(ShotRecord(
relativeX: relX,
relativeY: relY,
@@ -307,18 +316,36 @@ void registerShotLocation(BuildContext context, Offset position, Size size) {
onUpdate();
}
bool _validateShotZone(Offset pos, Size size, bool is3Pt) {
double w = size.width; double h = size.height;
Offset leftHoop = Offset(w * 0.12, h * 0.5);
Offset rightHoop = Offset(w * 0.88, h * 0.5);
double threePointRadius = w * 0.28;
Offset activeHoop = pos.dx < w / 2 ? leftHoop : rightHoop;
double distanceToHoop = (pos - activeHoop).distance;
bool isCorner3 = (pos.dy < h * 0.15 || pos.dy > h * 0.85) && (pos.dx < w * 0.20 || pos.dx > w * 0.80);
bool _validateShotZone(Offset position, Size size, bool is3Pt) {
double relX = position.dx / size.width;
double relY = position.dy / size.height;
if (is3Pt) return distanceToHoop >= threePointRadius || isCorner3;
else return distanceToHoop < threePointRadius && !isCorner3;
bool isLeftHalf = relX < 0.5;
double hoopX = isLeftHalf ? hoopBaseX : (1.0 - hoopBaseX);
double hoopY = 0.50;
double aspectRatio = size.width / size.height;
double distFromCenterY = (relY - hoopY).abs();
bool isInside2Pts;
// Lógica das laterais (Cantos)
if (distFromCenterY > cornerY) {
double distToBaseline = isLeftHalf ? relX : (1.0 - relX);
isInside2Pts = distToBaseline <= hoopBaseX;
}
// Lógica da Curva Frontal
else {
double dx = (relX - hoopX) * aspectRatio;
double dy = (relY - hoopY);
double distanceToHoop = math.sqrt((dx * dx) + (dy * dy));
isInside2Pts = distanceToHoop < arcRadius;
}
if (is3Pt) return !isInside2Pts;
return isInside2Pts;
}
// 👆 ===================================================================== 👆
void cancelShotLocation() {
isSelectingShotLocation = false; pendingAction = null; pendingPlayer = null; onUpdate();
@@ -368,7 +395,6 @@ void registerShotLocation(BuildContext context, Offset position, Size size) {
}
}
// --- 💾 FUNÇÃO PARA GUARDAR DADOS NA BD ---
Future<void> saveGameStats(BuildContext context) async {
final supabase = Supabase.instance.client;
isSaving = true;
@@ -378,14 +404,12 @@ void registerShotLocation(BuildContext context, Offset position, Size size) {
bool isGameFinishedNow = currentQuarter >= 4 && duration.inSeconds == 0;
String newStatus = isGameFinishedNow ? 'Terminado' : 'Pausado';
// 👇👇👇 0. CÉREBRO: CALCULAR OS LÍDERES E MVP DO JOGO 👇👇👇
String topPtsName = '---'; int maxPts = -1;
String topAstName = '---'; int maxAst = -1;
String topRbsName = '---'; int maxRbs = -1;
String topDefName = '---'; int maxDef = -1;
String mvpName = '---'; int maxMvpScore = -1;
// Passa por todos os jogadores e calcula a matemática
playerStats.forEach((playerName, stats) {
int pts = stats['pts'] ?? 0;
int ast = stats['ast'] ?? 0;
@@ -393,19 +417,16 @@ void registerShotLocation(BuildContext context, Offset position, Size size) {
int stl = stats['stl'] ?? 0;
int blk = stats['blk'] ?? 0;
int defScore = stl + blk; // Defesa: Roubos + Cortes
int mvpScore = pts + ast + rbs + defScore; // Impacto Total (MVP)
int defScore = stl + blk;
int mvpScore = pts + ast + rbs + defScore;
// Compara com o máximo atual e substitui se for maior
if (pts > maxPts && pts > 0) { maxPts = pts; topPtsName = '$playerName ($pts)'; }
if (ast > maxAst && ast > 0) { maxAst = ast; topAstName = '$playerName ($ast)'; }
if (rbs > maxRbs && rbs > 0) { maxRbs = rbs; topRbsName = '$playerName ($rbs)'; }
if (defScore > maxDef && defScore > 0) { maxDef = defScore; topDefName = '$playerName ($defScore)'; }
if (mvpScore > maxMvpScore && mvpScore > 0) { maxMvpScore = mvpScore; mvpName = playerName; } // MVP não leva nº à frente, fica mais limpo
if (mvpScore > maxMvpScore && mvpScore > 0) { maxMvpScore = mvpScore; mvpName = playerName; }
});
// 👆👆👆 FIM DO CÉREBRO 👆👆👆
// 1. Atualizar o Jogo na BD (Agora inclui os Reis da partida!)
await supabase.from('games').update({
'my_score': myScore,
'opponent_score': opponentScore,
@@ -414,8 +435,6 @@ void registerShotLocation(BuildContext context, Offset position, Size size) {
'opp_timeouts': opponentTimeoutsUsed,
'current_quarter': currentQuarter,
'status': newStatus,
// ENVIA A MATEMÁTICA PARA A TUA BASE DE DADOS
'top_pts_name': topPtsName,
'top_ast_name': topAstName,
'top_rbs_name': topRbsName,
@@ -423,7 +442,6 @@ void registerShotLocation(BuildContext context, Offset position, Size size) {
'mvp_name': mvpName,
}).eq('id', gameId);
// 2. LÓGICA DE VITÓRIAS, DERROTAS E EMPATES
if (isGameFinishedNow && !gameWasAlreadyFinished && myTeamDbId != null && oppTeamDbId != null) {
final teamsData = await supabase.from('teams').select('id, wins, losses, draws').inFilter('id', [myTeamDbId, oppTeamDbId]);
@@ -458,7 +476,6 @@ void registerShotLocation(BuildContext context, Offset position, Size size) {
gameWasAlreadyFinished = true;
}
// 3. Atualizar as Estatísticas dos Jogadores
List<Map<String, dynamic>> batchStats = [];
playerStats.forEach((playerName, stats) {
String? memberDbId = playerDbIds[playerName];