melhorar o sensor de calor

This commit is contained in:
2026-03-13 18:08:15 +00:00
parent cae3bbfe3b
commit 0369b5376c
10 changed files with 1053 additions and 926 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,
@@ -31,8 +32,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,35 +66,31 @@ class PlacarController {
Timer? timer;
bool isRunning = false;
// --- 🔄 CARREGAMENTO COMPLETO (DADOS REAIS + ESTATÍSTICAS SALVAS) ---
// OS TEUS NÚMEROS DE OURO DO TABLET
bool isCalibrating = false;
double hoopBaseX = 0.000;
double arcRadius = 0.500;
double cornerY = 0.443;
Future<void> loadPlayers() async {
final supabase = Supabase.instance.client;
try {
await Future.delayed(const Duration(milliseconds: 1500));
myCourt.clear();
myBench.clear();
oppCourt.clear();
oppBench.clear();
playerStats.clear();
playerNumbers.clear();
playerDbIds.clear();
myFouls = 0;
opponentFouls = 0;
myCourt.clear(); myBench.clear(); oppCourt.clear(); oppBench.clear();
playerStats.clear(); playerNumbers.clear(); playerDbIds.clear();
myFouls = 0; opponentFouls = 0;
final gameResponse = await supabase.from('games').select().eq('id', gameId).single();
myScore = int.tryParse(gameResponse['my_score']?.toString() ?? '0') ?? 0;
opponentScore = int.tryParse(gameResponse['opponent_score']?.toString() ?? '0') ?? 0;
int totalSeconds = int.tryParse(gameResponse['remaining_seconds']?.toString() ?? '600') ?? 600;
duration = Duration(seconds: totalSeconds);
myTimeoutsUsed = int.tryParse(gameResponse['my_timeouts']?.toString() ?? '0') ?? 0;
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]);
@@ -115,17 +110,10 @@ class PlacarController {
for (int i = 0; i < myPlayers.length; i++) {
String dbId = myPlayers[i]['id'].toString();
String name = myPlayers[i]['name'].toString();
_registerPlayer(name: name, number: myPlayers[i]['number']?.toString() ?? "0", dbId: dbId, isMyTeam: true, isCourt: i < 5);
if (savedStats.containsKey(dbId)) {
var s = savedStats[dbId];
playerStats[name] = {
"pts": s['pts'] ?? 0, "rbs": s['rbs'] ?? 0, "ast": s['ast'] ?? 0,
"stl": s['stl'] ?? 0, "tov": s['tov'] ?? 0, "blk": s['blk'] ?? 0,
"fls": s['fls'] ?? 0, "fgm": s['fgm'] ?? 0, "fga": s['fga'] ?? 0,
"ftm": s['ftm'] ?? 0, "fta": s['fta'] ?? 0, "orb": s['orb'] ?? 0, "drb": s['drb'] ?? 0,
};
playerStats[name] = { "pts": s['pts'] ?? 0, "rbs": s['rbs'] ?? 0, "ast": s['ast'] ?? 0, "stl": s['stl'] ?? 0, "tov": s['tov'] ?? 0, "blk": s['blk'] ?? 0, "fls": s['fls'] ?? 0, "fgm": s['fgm'] ?? 0, "fga": s['fga'] ?? 0, "ftm": s['ftm'] ?? 0, "fta": s['fta'] ?? 0, "orb": s['orb'] ?? 0, "drb": s['drb'] ?? 0 };
myFouls += (s['fls'] as int? ?? 0);
}
}
@@ -134,28 +122,28 @@ class PlacarController {
for (int i = 0; i < oppPlayers.length; i++) {
String dbId = oppPlayers[i]['id'].toString();
String name = oppPlayers[i]['name'].toString();
_registerPlayer(name: name, number: oppPlayers[i]['number']?.toString() ?? "0", dbId: dbId, isMyTeam: false, isCourt: i < 5);
if (savedStats.containsKey(dbId)) {
var s = savedStats[dbId];
playerStats[name] = {
"pts": s['pts'] ?? 0, "rbs": s['rbs'] ?? 0, "ast": s['ast'] ?? 0,
"stl": s['stl'] ?? 0, "tov": s['tov'] ?? 0, "blk": s['blk'] ?? 0,
"fls": s['fls'] ?? 0, "fgm": s['fgm'] ?? 0, "fga": s['fga'] ?? 0,
"ftm": s['ftm'] ?? 0, "fta": s['fta'] ?? 0, "orb": s['orb'] ?? 0, "drb": s['drb'] ?? 0,
};
playerStats[name] = { "pts": s['pts'] ?? 0, "rbs": s['rbs'] ?? 0, "ast": s['ast'] ?? 0, "stl": s['stl'] ?? 0, "tov": s['tov'] ?? 0, "blk": s['blk'] ?? 0, "fls": s['fls'] ?? 0, "fgm": s['fgm'] ?? 0, "fga": s['fga'] ?? 0, "ftm": s['ftm'] ?? 0, "fta": s['fta'] ?? 0, "orb": s['orb'] ?? 0, "drb": s['drb'] ?? 0 };
opponentFouls += (s['fls'] as int? ?? 0);
}
}
_padTeam(oppCourt, oppBench, "Adversário", isMyTeam: false);
// Carregar Shots salvos para o HeatMap
final shotsResponse = await supabase.from('game_shots').select().eq('game_id', gameId);
matchShots = (shotsResponse as List).map((s) => ShotRecord(
relativeX: (s['relative_x'] as num).toDouble(),
relativeY: (s['relative_y'] as num).toDouble(),
isMake: s['is_make'] as bool,
playerName: s['player_name'],
)).toList();
isLoading = false;
onUpdate();
} catch (e) {
debugPrint("Erro ao retomar jogo: $e");
_padTeam(myCourt, myBench, "Falha", isMyTeam: true);
_padTeam(oppCourt, oppBench, "Falha Opp", isMyTeam: false);
isLoading = false;
onUpdate();
}
@@ -165,17 +153,9 @@ class PlacarController {
if (playerNumbers.containsKey(name)) name = "$name (Opp)";
playerNumbers[name] = number;
if (dbId != null) playerDbIds[name] = dbId;
playerStats[name] = {
"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0,
"fls": 0, "fgm": 0, "fga": 0, "ftm": 0, "fta": 0, "orb": 0, "drb": 0
};
if (isMyTeam) {
if (isCourt) myCourt.add(name); else myBench.add(name);
} else {
if (isCourt) oppCourt.add(name); else oppBench.add(name);
}
playerStats[name] = { "pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0, "ftm": 0, "fta": 0, "orb": 0, "drb": 0 };
if (isMyTeam) { if (isCourt) myCourt.add(name); else myBench.add(name); }
else { if (isCourt) oppCourt.add(name); else oppBench.add(name); }
}
void _padTeam(List<String> court, List<String> bench, String prefix, {required bool isMyTeam}) {
@@ -194,17 +174,12 @@ class PlacarController {
} else {
timer.cancel();
isRunning = false;
if (currentQuarter < 4) {
currentQuarter++;
duration = const Duration(minutes: 10);
myFouls = 0;
opponentFouls = 0;
myTimeoutsUsed = 0;
opponentTimeoutsUsed = 0;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Período $currentQuarter iniciado. Faltas e Timeouts resetados!'), backgroundColor: Colors.blue));
} else {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('FIM DO JOGO! Clica em Guardar para fechar a partida.'), backgroundColor: Colors.red));
}
if (currentQuarter < 4) {
currentQuarter++;
duration = const Duration(minutes: 10);
myFouls = 0; opponentFouls = 0; myTimeoutsUsed = 0; opponentTimeoutsUsed = 0;
onUpdate();
}
}
onUpdate();
});
@@ -214,11 +189,8 @@ class PlacarController {
}
void useTimeout(bool isOpponent) {
if (isOpponent) {
if (opponentTimeoutsUsed < 3) opponentTimeoutsUsed++;
} else {
if (myTimeoutsUsed < 3) myTimeoutsUsed++;
}
if (isOpponent) { if (opponentTimeoutsUsed < 3) opponentTimeoutsUsed++; }
else { if (myTimeoutsUsed < 3) myTimeoutsUsed++; }
isRunning = false;
timer?.cancel();
onUpdate();
@@ -254,7 +226,6 @@ class PlacarController {
myCourt[courtIndex] = benchPlayer;
myBench[benchIndex] = courtPlayerName;
showMyBench = false;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Sai $courtPlayerName, Entra $benchPlayer')));
}
if (action.startsWith("bench_opp_") && isOpponent) {
String benchPlayer = action.replaceAll("bench_opp_", "");
@@ -264,41 +235,36 @@ class PlacarController {
oppCourt[courtIndex] = benchPlayer;
oppBench[benchIndex] = courtPlayerName;
showOppBench = false;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Sai $courtPlayerName, Entra $benchPlayer')));
}
onUpdate();
}
void registerShotLocation(BuildContext context, Offset position, Size size) {
// ==============================================================
// 🎯 REGISTO DO TOQUE (INTELIGENTE E SILENCIOSO)
// ==============================================================
void registerShotLocation(BuildContext context, Offset position, Size size) {
if (pendingAction == null || pendingPlayer == null) return;
bool isOpponent = pendingPlayer!.startsWith("player_opp_");
bool is3Pt = pendingAction!.contains("_3");
bool is2Pt = pendingAction!.contains("_2");
if (is3Pt || is2Pt) {
bool isValid = _validateShotZone(position, size, is3Pt);
if (!isValid) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('🛑 Local incompatível com a pontuação.'), backgroundColor: Colors.red, duration: Duration(seconds: 2)));
bool isInside2Pts = _validateShotZone(position, size, isOpponent);
// Bloqueio silencioso (sem notificações chamas)
if ((is2Pt && !isInside2Pts) || (is3Pt && isInside2Pts)) {
cancelShotLocation();
return;
}
}
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,
isMake: isMake,
playerName: name
));
matchShots.add(ShotRecord(relativeX: relX, relativeY: relY, isMake: isMake, playerName: name));
commitStat(pendingAction!, pendingPlayer!);
isSelectingShotLocation = false;
@@ -307,17 +273,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);
// ==============================================================
// 📐 MATEMÁTICA PURA: LÓGICA DE MEIO-CAMPO ATACANTE (SOLUÇÃO DIVIDIDA)
// ==============================================================
bool _validateShotZone(Offset position, Size size, bool isOpponent) {
double relX = position.dx / size.width;
double relY = position.dy / size.height;
if (is3Pt) return distanceToHoop >= threePointRadius || isCorner3;
else return distanceToHoop < threePointRadius && !isCorner3;
double hX = hoopBaseX;
double radius = arcRadius;
double cY = cornerY;
// A Minha Equipa defende na Esquerda (0.0), logo ataca o cesto da Direita (1.0)
// O Adversário defende na Direita (1.0), logo ataca o cesto da Esquerda (0.0)
double hoopX = isOpponent ? hX : (1.0 - hX);
double hoopY = 0.50;
double aspectRatio = size.width / size.height;
double distFromCenterY = (relY - hoopY).abs();
// Descobre se o toque foi feito na metade atacante daquela equipa
bool isAttackingHalf = isOpponent ? (relX < 0.5) : (relX > 0.5);
if (isAttackingHalf && distFromCenterY > cY) {
return false; // É 3 pontos (Zona dos Cantos)
} else {
double dx = (relX - hoopX) * aspectRatio;
double dy = (relY - hoopY);
double distanceToHoop = math.sqrt((dx * dx) + (dy * dy));
return distanceToHoop <= radius;
}
}
void cancelShotLocation() {
@@ -368,97 +353,63 @@ 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;
onUpdate();
try {
bool isGameFinishedNow = currentQuarter >= 4 && duration.inSeconds == 0;
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;
int rbs = stats['rbs'] ?? 0;
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)
// Compara com o máximo atual e substitui se for maior
int defScore = stl + blk;
int mvpScore = pts + ast + rbs + defScore;
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,
'remaining_seconds': duration.inSeconds,
'my_timeouts': myTimeoutsUsed,
'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,
'top_def_name': topDefName,
'mvp_name': mvpName,
'my_score': myScore, 'opponent_score': opponentScore, 'remaining_seconds': duration.inSeconds,
'my_timeouts': myTimeoutsUsed, 'opp_timeouts': opponentTimeoutsUsed, 'current_quarter': currentQuarter,
'status': newStatus, 'top_pts_name': topPtsName, 'top_ast_name': topAstName, 'top_rbs_name': topRbsName,
'top_def_name': topDefName, 'mvp_name': mvpName,
}).eq('id', gameId);
// 2. LÓGICA DE VITÓRIAS, DERROTAS E EMPATES
// Atualiza Vitórias/Derrotas se o jogo terminou
if (isGameFinishedNow && !gameWasAlreadyFinished && myTeamDbId != null && oppTeamDbId != null) {
final teamsData = await supabase.from('teams').select('id, wins, losses, draws').inFilter('id', [myTeamDbId, oppTeamDbId]);
Map<String, dynamic> myTeamUpdate = {};
Map<String, dynamic> oppTeamUpdate = {};
for(var t in teamsData) {
if(t['id'].toString() == myTeamDbId) myTeamUpdate = Map.from(t);
if(t['id'].toString() == oppTeamDbId) oppTeamUpdate = Map.from(t);
if(t['id'].toString() == myTeamDbId) {
int w = (t['wins'] ?? 0) + (myScore > opponentScore ? 1 : 0);
int l = (t['losses'] ?? 0) + (myScore < opponentScore ? 1 : 0);
int d = (t['draws'] ?? 0) + (myScore == opponentScore ? 1 : 0);
await supabase.from('teams').update({'wins': w, 'losses': l, 'draws': d}).eq('id', myTeamDbId!);
} else {
int w = (t['wins'] ?? 0) + (opponentScore > myScore ? 1 : 0);
int l = (t['losses'] ?? 0) + (opponentScore < myScore ? 1 : 0);
int d = (t['draws'] ?? 0) + (opponentScore == myScore ? 1 : 0);
await supabase.from('teams').update({'wins': w, 'losses': l, 'draws': d}).eq('id', oppTeamDbId!);
}
}
if (myScore > opponentScore) {
myTeamUpdate['wins'] = (myTeamUpdate['wins'] ?? 0) + 1;
oppTeamUpdate['losses'] = (oppTeamUpdate['losses'] ?? 0) + 1;
} else if (myScore < opponentScore) {
myTeamUpdate['losses'] = (myTeamUpdate['losses'] ?? 0) + 1;
oppTeamUpdate['wins'] = (oppTeamUpdate['wins'] ?? 0) + 1;
} else {
myTeamUpdate['draws'] = (myTeamUpdate['draws'] ?? 0) + 1;
oppTeamUpdate['draws'] = (oppTeamUpdate['draws'] ?? 0) + 1;
}
await supabase.from('teams').update({
'wins': myTeamUpdate['wins'], 'losses': myTeamUpdate['losses'], 'draws': myTeamUpdate['draws']
}).eq('id', myTeamDbId!);
await supabase.from('teams').update({
'wins': oppTeamUpdate['wins'], 'losses': oppTeamUpdate['losses'], 'draws': oppTeamUpdate['draws']
}).eq('id', oppTeamDbId!);
gameWasAlreadyFinished = true;
}
// 3. Atualizar as Estatísticas dos Jogadores
// Salvar Estatísticas Gerais
List<Map<String, dynamic>> batchStats = [];
playerStats.forEach((playerName, stats) {
String? memberDbId = playerDbIds[playerName];
@@ -470,21 +421,32 @@ void registerShotLocation(BuildContext context, Offset position, Size size) {
});
}
});
await supabase.from('player_stats').delete().eq('game_id', gameId);
if (batchStats.isNotEmpty) {
await supabase.from('player_stats').insert(batchStats);
if (batchStats.isNotEmpty) await supabase.from('player_stats').insert(batchStats);
// ===============================================
// 🔥 GRAVAR COORDENADAS PARA O HEATMAP
// ===============================================
List<Map<String, dynamic>> shotsData = [];
for (var shot in matchShots) {
bool isMyTeamPlayer = myCourt.contains(shot.playerName) || myBench.contains(shot.playerName);
shotsData.add({
'game_id': gameId,
'team_id': isMyTeamPlayer ? myTeamDbId! : oppTeamDbId!,
'player_name': shot.playerName,
'relative_x': shot.relativeX,
'relative_y': shot.relativeY,
'is_make': shot.isMake,
});
}
await supabase.from('game_shots').delete().eq('game_id', gameId);
if (shotsData.isNotEmpty) await supabase.from('game_shots').insert(shotsData);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Estatísticas e Resultados guardados com Sucesso!'), backgroundColor: Colors.green));
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Tudo guardado com Sucesso!'), backgroundColor: Colors.green));
}
} catch (e) {
debugPrint("Erro ao gravar estatísticas: $e");
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Erro ao guardar: $e'), backgroundColor: Colors.red));
}
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Erro ao guardar: $e'), backgroundColor: Colors.red));
} finally {
isSaving = false;
onUpdate();
@@ -494,4 +456,4 @@ void registerShotLocation(BuildContext context, Offset position, Size size) {
void dispose() {
timer?.cancel();
}
}
}