import 'dart:async'; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; class ShotRecord { final double relativeX; final double relativeY; final bool isMake; final String playerName; ShotRecord({ required this.relativeX, required this.relativeY, required this.isMake, required this.playerName }); } class PlacarController { final String gameId; final String myTeam; final String opponentTeam; final VoidCallback onUpdate; PlacarController({ required this.gameId, required this.myTeam, required this.opponentTeam, required this.onUpdate }); bool isLoading = true; bool isSaving = false; bool gameWasAlreadyFinished = false; int myScore = 0; int opponentScore = 0; int myFouls = 0; int opponentFouls = 0; int currentQuarter = 1; int myTimeoutsUsed = 0; int opponentTimeoutsUsed = 0; String? myTeamDbId; String? oppTeamDbId; List myCourt = []; List myBench = []; List oppCourt = []; List oppBench = []; Map playerNumbers = {}; Map> playerStats = {}; Map playerDbIds = {}; bool showMyBench = false; bool showOppBench = false; bool isSelectingShotLocation = false; String? pendingAction; String? pendingPlayer; List matchShots = []; Duration duration = const Duration(minutes: 10); Timer? timer; bool isRunning = false; // OS TEUS NÚMEROS DE OURO DO TABLET bool isCalibrating = false; double hoopBaseX = 0.000; double arcRadius = 0.500; double cornerY = 0.443; Future 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; 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; gameWasAlreadyFinished = gameResponse['status'] == 'Terminado'; final teamsResponse = await supabase.from('teams').select('id, name').inFilter('name', [myTeam, opponentTeam]); for (var t in teamsResponse) { if (t['name'] == myTeam) myTeamDbId = t['id']; if (t['name'] == opponentTeam) oppTeamDbId = t['id']; } List myPlayers = myTeamDbId != null ? await supabase.from('members').select().eq('team_id', myTeamDbId!).eq('type', 'Jogador') : []; List oppPlayers = oppTeamDbId != null ? await supabase.from('members').select().eq('team_id', oppTeamDbId!).eq('type', 'Jogador') : []; final statsResponse = await supabase.from('player_stats').select().eq('game_id', gameId); final Map savedStats = { for (var item in statsResponse) item['member_id'].toString(): item }; 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 }; myFouls += (s['fls'] as int? ?? 0); } } _padTeam(myCourt, myBench, "Jogador", isMyTeam: true); 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 }; 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"); isLoading = false; onUpdate(); } } void _registerPlayer({required String name, required String number, String? dbId, required bool isMyTeam, required bool isCourt}) { 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); } } void _padTeam(List court, List bench, String prefix, {required bool isMyTeam}) { while (court.length < 5) { _registerPlayer(name: "Sem $prefix ${court.length + 1}", number: "0", dbId: null, isMyTeam: isMyTeam, isCourt: true); } } void toggleTimer(BuildContext context) { if (isRunning) { timer?.cancel(); } else { timer = Timer.periodic(const Duration(seconds: 1), (timer) { if (duration.inSeconds > 0) { duration -= const Duration(seconds: 1); } else { timer.cancel(); isRunning = false; if (currentQuarter < 4) { currentQuarter++; duration = const Duration(minutes: 10); myFouls = 0; opponentFouls = 0; myTimeoutsUsed = 0; opponentTimeoutsUsed = 0; onUpdate(); } } onUpdate(); }); } isRunning = !isRunning; onUpdate(); } void useTimeout(bool isOpponent) { if (isOpponent) { if (opponentTimeoutsUsed < 3) opponentTimeoutsUsed++; } else { if (myTimeoutsUsed < 3) myTimeoutsUsed++; } isRunning = false; timer?.cancel(); onUpdate(); } String formatTime() => "${duration.inMinutes.toString().padLeft(2, '0')}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}"; void handleActionDrag(BuildContext context, String action, String playerData) { String name = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", ""); final stats = playerStats[name]!; if (stats["fls"]! >= 5 && action != "sub_foul") { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('🛑 $name atingiu 5 faltas e está expulso!'), backgroundColor: Colors.red)); return; } if (action == "add_pts_2" || action == "add_pts_3" || action == "miss_2" || action == "miss_3") { pendingAction = action; pendingPlayer = playerData; isSelectingShotLocation = true; } else { commitStat(action, playerData); } onUpdate(); } void handleSubbing(BuildContext context, String action, String courtPlayerName, bool isOpponent) { if (action.startsWith("bench_my_") && !isOpponent) { String benchPlayer = action.replaceAll("bench_my_", ""); if (playerStats[benchPlayer]!["fls"]! >= 5) return; int courtIndex = myCourt.indexOf(courtPlayerName); int benchIndex = myBench.indexOf(benchPlayer); myCourt[courtIndex] = benchPlayer; myBench[benchIndex] = courtPlayerName; showMyBench = false; } if (action.startsWith("bench_opp_") && isOpponent) { String benchPlayer = action.replaceAll("bench_opp_", ""); if (playerStats[benchPlayer]!["fls"]! >= 5) return; int courtIndex = oppCourt.indexOf(courtPlayerName); int benchIndex = oppBench.indexOf(benchPlayer); oppCourt[courtIndex] = benchPlayer; oppBench[benchIndex] = courtPlayerName; showOppBench = false; } onUpdate(); } // ============================================================== // 🎯 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 isInside2Pts = _validateShotZone(position, size, isOpponent); // Bloqueio silencioso (sem notificações chamas) if ((is2Pt && !isInside2Pts) || (is3Pt && isInside2Pts)) { cancelShotLocation(); return; } } bool isMake = pendingAction!.startsWith("add_pts_"); double relX = position.dx / size.width; double relY = position.dy / size.height; String name = pendingPlayer!.replaceAll("player_my_", "").replaceAll("player_opp_", ""); matchShots.add(ShotRecord(relativeX: relX, relativeY: relY, isMake: isMake, playerName: name)); commitStat(pendingAction!, pendingPlayer!); isSelectingShotLocation = false; pendingAction = null; pendingPlayer = null; onUpdate(); } // ============================================================== // 📐 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; 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() { isSelectingShotLocation = false; pendingAction = null; pendingPlayer = null; onUpdate(); } void commitStat(String action, String playerData) { bool isOpponent = playerData.startsWith("player_opp_"); String name = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", ""); final stats = playerStats[name]!; if (action.startsWith("add_pts_")) { int pts = int.parse(action.split("_").last); if (isOpponent) opponentScore += pts; else myScore += pts; stats["pts"] = stats["pts"]! + pts; if (pts == 2 || pts == 3) { stats["fgm"] = stats["fgm"]! + 1; stats["fga"] = stats["fga"]! + 1; } if (pts == 1) { stats["ftm"] = stats["ftm"]! + 1; stats["fta"] = stats["fta"]! + 1; } } else if (action.startsWith("sub_pts_")) { int pts = int.parse(action.split("_").last); if (isOpponent) { opponentScore = (opponentScore - pts < 0) ? 0 : opponentScore - pts; } else { myScore = (myScore - pts < 0) ? 0 : myScore - pts; } stats["pts"] = (stats["pts"]! - pts < 0) ? 0 : stats["pts"]! - pts; if (pts == 2 || pts == 3) { if (stats["fgm"]! > 0) stats["fgm"] = stats["fgm"]! - 1; if (stats["fga"]! > 0) stats["fga"] = stats["fga"]! - 1; } if (pts == 1) { if (stats["ftm"]! > 0) stats["ftm"] = stats["ftm"]! - 1; if (stats["fta"]! > 0) stats["fta"] = stats["fta"]! - 1; } } else if (action == "miss_1") { stats["fta"] = stats["fta"]! + 1; } else if (action == "miss_2" || action == "miss_3") { stats["fga"] = stats["fga"]! + 1; } else if (action == "add_orb") { stats["orb"] = stats["orb"]! + 1; stats["rbs"] = stats["rbs"]! + 1; } else if (action == "add_drb") { stats["drb"] = stats["drb"]! + 1; stats["rbs"] = stats["rbs"]! + 1; } else if (action == "add_ast") { stats["ast"] = stats["ast"]! + 1; } else if (action == "add_stl") { stats["stl"] = stats["stl"]! + 1; } else if (action == "add_tov") { stats["tov"] = stats["tov"]! + 1; } else if (action == "add_blk") { stats["blk"] = stats["blk"]! + 1; } else if (action == "add_foul") { stats["fls"] = stats["fls"]! + 1; if (isOpponent) { opponentFouls++; } else { myFouls++; } } else if (action == "sub_foul") { if (stats["fls"]! > 0) stats["fls"] = stats["fls"]! - 1; if (isOpponent) { if (opponentFouls > 0) opponentFouls--; } else { if (myFouls > 0) myFouls--; } } } Future saveGameStats(BuildContext context) async { final supabase = Supabase.instance.client; isSaving = true; onUpdate(); try { bool isGameFinishedNow = (currentQuarter >= 4 && duration.inSeconds == 0); String newStatus = isGameFinishedNow ? 'Terminado' : 'Pausado'; 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; 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; 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; } }); 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, 'top_pts_name': topPtsName, 'top_ast_name': topAstName, 'top_rbs_name': topRbsName, 'top_def_name': topDefName, 'mvp_name': mvpName, }).eq('id', gameId); // 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]); for(var t in teamsData) { 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!); } } gameWasAlreadyFinished = true; } // Salvar Estatísticas Gerais List> batchStats = []; playerStats.forEach((playerName, stats) { String? memberDbId = playerDbIds[playerName]; if (memberDbId != null && stats.values.any((val) => val > 0)) { bool isMyTeamPlayer = myCourt.contains(playerName) || myBench.contains(playerName); batchStats.add({ 'game_id': gameId, 'member_id': memberDbId, 'team_id': isMyTeamPlayer ? myTeamDbId! : oppTeamDbId!, 'pts': stats['pts'], 'rbs': stats['rbs'], 'ast': stats['ast'], 'stl': stats['stl'], 'blk': stats['blk'], 'tov': stats['tov'], 'fls': stats['fls'], 'fgm': stats['fgm'], 'fga': stats['fga'], 'ftm': stats['ftm'], 'fta': stats['fta'], 'orb': stats['orb'], 'drb': stats['drb'], }); } }); await supabase.from('player_stats').delete().eq('game_id', gameId); if (batchStats.isNotEmpty) await supabase.from('player_stats').insert(batchStats); // =============================================== // 🔥 GRAVAR COORDENADAS PARA O HEATMAP // =============================================== List> 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('Tudo guardado com Sucesso!'), backgroundColor: Colors.green)); } } catch (e) { if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Erro ao guardar: $e'), backgroundColor: Colors.red)); } finally { isSaving = false; onUpdate(); } } void dispose() { timer?.cancel(); } }