Files
PlayMaker/lib/controllers/placar_controller.dart
2026-04-30 10:41:10 +01:00

837 lines
32 KiB
Dart

import 'dart:async';
import 'dart:math' as math;
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ShotRecord {
final double relativeX;
final double relativeY;
final bool isMake;
final String playerId;
final String playerName;
final String? zone;
final int? points;
ShotRecord({
required this.relativeX,
required this.relativeY,
required this.isMake,
required this.playerId,
required this.playerName,
this.zone,
this.points,
});
Map<String, dynamic> toJson() => {
'relativeX': relativeX, 'relativeY': relativeY, 'isMake': isMake,
'playerId': playerId, 'playerName': playerName, 'zone': zone, 'points': points,
};
factory ShotRecord.fromJson(Map<String, dynamic> json) => ShotRecord(
relativeX: json['relativeX'], relativeY: json['relativeY'], isMake: json['isMake'],
playerId: json['playerId'], playerName: json['playerName'], zone: json['zone'], points: json['points'],
);
}
class PlacarController extends ChangeNotifier {
final String gameId;
final String myTeam;
final String opponentTeam;
PlacarController({
required this.gameId,
required this.myTeam,
required this.opponentTeam,
});
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<String> myCourt = [];
List<String> myBench = [];
List<String> oppCourt = [];
List<String> oppBench = [];
Map<String, String> playerNames = {};
Map<String, String> playerNumbers = {};
Map<String, Map<String, int>> playerStats = {};
bool showMyBench = false;
bool showOppBench = false;
bool isSelectingShotLocation = false;
String? pendingAction;
String? pendingPlayerId;
List<ShotRecord> matchShots = [];
List<String> playByPlay = [];
ValueNotifier<Duration> durationNotifier = ValueNotifier(const Duration(minutes: 10));
Timer? timer;
bool isRunning = false;
bool isCalibrating = false;
double hoopBaseX = 0.088;
double arcRadius = 0.459;
double cornerY = 0.440;
Timer? _autoSaveTimer;
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();
playerNames.clear(); playerStats.clear(); playerNumbers.clear();
matchShots.clear(); playByPlay.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;
durationNotifier.value = 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';
if (gameResponse['play_by_play'] != null) {
playByPlay = List<String>.from(gameResponse['play_by_play']);
} else {
playByPlay = [];
}
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<dynamic> myPlayers = myTeamDbId != null ? await supabase.from('members').select().eq('team_id', myTeamDbId!).eq('type', 'Jogador') : [];
List<dynamic> 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<String, dynamic> 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];
_loadSavedPlayerStats(dbId, s);
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];
_loadSavedPlayerStats(dbId, s);
opponentFouls += (s['fls'] as int? ?? 0);
}
}
_padTeam(oppCourt, oppBench, "Adversário", isMyTeam: false);
final shotsResponse = await supabase.from('shot_locations').select().eq('game_id', gameId);
for (var shotData in shotsResponse) {
matchShots.add(ShotRecord(
relativeX: double.parse(shotData['relative_x'].toString()),
relativeY: double.parse(shotData['relative_y'].toString()),
isMake: shotData['is_make'] == true,
playerId: shotData['member_id'].toString(),
playerName: shotData['player_name'].toString(),
zone: shotData['zone']?.toString(),
points: shotData['points'] != null ? int.parse(shotData['points'].toString()) : null,
));
}
await _loadLocalBackup();
isLoading = false;
notifyListeners();
} catch (e) {
debugPrint("Erro ao retomar jogo: $e");
isLoading = false;
notifyListeners();
}
}
void _loadSavedPlayerStats(String dbId, Map<String, dynamic> s) {
playerStats[dbId] = {
"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,
"p2m": s['p2m'] ?? 0, "p2a": s['p2a'] ?? 0, "p3m": s['p3m'] ?? 0, "p3a": s['p3a'] ?? 0,
"so": s['so'] ?? 0, "il": s['il'] ?? 0, "li": s['li'] ?? 0,
"pa": s['pa'] ?? 0, "tres_seg": s['tres_seg'] ?? 0, "dr": s['dr'] ?? 0,
"min": (s['minutos_jogados'] ?? 0) ~/ 60,
"sec": s['minutos_jogados'] ?? 0,
};
}
void _registerPlayer({required String name, required String number, String? dbId, required bool isMyTeam, required bool isCourt}) {
String id = dbId ?? "fake_${DateTime.now().millisecondsSinceEpoch}_${math.Random().nextInt(9999)}";
playerNames[id] = name;
playerNumbers[id] = number;
playerStats[id] = {
"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,
"p2m": 0, "p2a": 0, "p3m": 0, "p3a": 0,
"so": 0, "il": 0, "li": 0, "pa": 0, "tres_seg": 0, "dr": 0,
"min": 0, "sec": 0
};
if (isMyTeam) {
if (isCourt) myCourt.add(id); else myBench.add(id);
} else {
if (isCourt) oppCourt.add(id); else oppBench.add(id);
}
}
void _padTeam(List<String> court, List<String> 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 _scheduleAutoSave() {
_autoSaveTimer?.cancel();
_autoSaveTimer = Timer(const Duration(milliseconds: 1500), () {
_saveLocalBackup();
});
}
Future<void> _saveLocalBackup() async {
try {
final prefs = await SharedPreferences.getInstance();
final backupData = {
'myScore': myScore, 'opponentScore': opponentScore,
'myFouls': myFouls, 'opponentFouls': opponentFouls,
'currentQuarter': currentQuarter, 'duration': durationNotifier.value.inSeconds,
'myTimeoutsUsed': myTimeoutsUsed, 'opponentTimeoutsUsed': opponentTimeoutsUsed,
'playerStats': playerStats,
'myCourt': myCourt, 'myBench': myBench, 'oppCourt': oppCourt, 'oppBench': oppBench,
'matchShots': matchShots.map((s) => s.toJson()).toList(),
'playByPlay': playByPlay,
};
await prefs.setString('backup_$gameId', jsonEncode(backupData));
} catch (e) {
debugPrint("Erro no Auto-Save: $e");
}
}
Future<void> _loadLocalBackup() async {
try {
final prefs = await SharedPreferences.getInstance();
final String? backupString = prefs.getString('backup_$gameId');
if (backupString != null) {
final data = jsonDecode(backupString);
myScore = data['myScore']; opponentScore = data['opponentScore'];
myFouls = data['myFouls']; opponentFouls = data['opponentFouls'];
currentQuarter = data['currentQuarter']; durationNotifier.value = Duration(seconds: data['duration']);
myTimeoutsUsed = data['myTimeoutsUsed']; opponentTimeoutsUsed = data['opponentTimeoutsUsed'];
myCourt = List<String>.from(data['myCourt']); myBench = List<String>.from(data['myBench']);
oppCourt = List<String>.from(data['oppCourt']); oppBench = List<String>.from(data['oppBench']);
Map<String, dynamic> decodedStats = data['playerStats'];
playerStats = decodedStats.map((k, v) => MapEntry(k, Map<String, int>.from(v)));
List<dynamic> decodedShots = data['matchShots'];
matchShots = decodedShots.map((s) => ShotRecord.fromJson(s)).toList();
playByPlay = List<String>.from(data['playByPlay'] ?? []);
}
} catch (e) {
debugPrint("Erro ao carregar Auto-Save: $e");
}
}
void toggleTimer(BuildContext context) {
if (isRunning) {
timer?.cancel();
_scheduleAutoSave();
} else {
timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (durationNotifier.value.inSeconds > 0) {
void addTimeToCourt(List<String> court) {
for (String id in court) {
if (playerStats.containsKey(id)) {
int currentSec = playerStats[id]!["sec"] ?? 0;
playerStats[id]!["sec"] = currentSec + 1;
playerStats[id]!["min"] = (currentSec + 1) ~/ 60;
}
}
}
addTimeToCourt(myCourt);
addTimeToCourt(oppCourt);
durationNotifier.value -= const Duration(seconds: 1);
} else {
timer.cancel();
isRunning = false;
if (currentQuarter < 4) {
currentQuarter++;
durationNotifier.value = const Duration(minutes: 10);
myFouls = 0; opponentFouls = 0;
myTimeoutsUsed = 0; opponentTimeoutsUsed = 0;
_scheduleAutoSave();
}
notifyListeners();
}
});
}
isRunning = !isRunning;
notifyListeners();
}
void useTimeout(bool isOpponent) {
if (isOpponent) {
if (opponentTimeoutsUsed < 3) opponentTimeoutsUsed++;
} else {
if (myTimeoutsUsed < 3) myTimeoutsUsed++;
}
isRunning = false;
timer?.cancel();
_scheduleAutoSave();
notifyListeners();
}
void handleActionDrag(BuildContext context, String action, String playerData) {
String playerId = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", "");
final stats = playerStats[playerId]!;
final name = playerNames[playerId]!;
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;
pendingPlayerId = playerData;
isSelectingShotLocation = true;
} else {
commitStat(action, playerData);
}
notifyListeners();
}
void handleSubbing(BuildContext context, String action, String courtPlayerId, bool isOpponent) {
if (action.startsWith("bench_my_") && !isOpponent) {
String benchPlayerId = action.replaceAll("bench_my_", "");
if (playerStats[benchPlayerId]!["fls"]! >= 5) return;
int courtIndex = myCourt.indexOf(courtPlayerId);
int benchIndex = myBench.indexOf(benchPlayerId);
myCourt[courtIndex] = benchPlayerId;
myBench[benchIndex] = courtPlayerId;
showMyBench = false;
}
if (action.startsWith("bench_opp_") && isOpponent) {
String benchPlayerId = action.replaceAll("bench_opp_", "");
if (playerStats[benchPlayerId]!["fls"]! >= 5) return;
int courtIndex = oppCourt.indexOf(courtPlayerId);
int benchIndex = oppBench.indexOf(benchPlayerId);
oppCourt[courtIndex] = benchPlayerId;
oppBench[benchIndex] = courtPlayerId;
showOppBench = false;
}
_scheduleAutoSave();
notifyListeners();
}
void registerShotFromPopup(BuildContext context, String action, String targetPlayer, String zone, int points, double relativeX, double relativeY) {
String playerId = targetPlayer.replaceAll("player_my_", "").replaceAll("player_opp_", "");
bool isMake = action.startsWith("add_");
String name = playerNames[playerId] ?? "Jogador";
matchShots.add(ShotRecord(
relativeX: relativeX, relativeY: relativeY, isMake: isMake,
playerId: playerId, playerName: name, zone: zone, points: points
));
String finalAction = isMake ? "add_pts_$points" : "miss_$points";
commitStat(finalAction, targetPlayer);
notifyListeners();
}
void registerShotLocation(BuildContext context, Offset position, Size size) {
if (pendingAction == null || pendingPlayerId == null) return;
bool is3Pt = pendingAction!.contains("_3");
bool is2Pt = pendingAction!.contains("_2");
if (is3Pt || is2Pt) {
bool isValid = _validateShotZone(position, size, is3Pt);
if (!isValid) return;
}
bool isMake = pendingAction!.startsWith("add_pts_");
double relX = position.dx / size.width;
double relY = position.dy / size.height;
String pId = pendingPlayerId!.replaceAll("player_my_", "").replaceAll("player_opp_", "");
matchShots.add(ShotRecord(relativeX: relX, relativeY: relY, isMake: isMake, playerId: pId, playerName: playerNames[pId]!));
commitStat(pendingAction!, pendingPlayerId!);
isSelectingShotLocation = false; pendingAction = null; pendingPlayerId = null;
_scheduleAutoSave();
notifyListeners();
}
bool _validateShotZone(Offset position, Size size, bool is3Pt) {
double relX = position.dx / size.width;
double relY = position.dy / size.height;
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;
if (distFromCenterY > cornerY) {
double distToBaseline = isLeftHalf ? relX : (1.0 - relX);
isInside2Pts = distToBaseline <= hoopBaseX;
} 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; pendingPlayerId = null; notifyListeners();
}
void registerFoul(String committerData, String foulType, String victimData) {
bool isOpponent = committerData.startsWith("player_opp_");
String committerId = committerData.replaceAll("player_my_", "").replaceAll("player_opp_", "");
final committerStats = playerStats[committerId]!;
final committerName = playerNames[committerId] ?? "Jogador";
committerStats["fls"] = committerStats["fls"]! + 1;
if (isOpponent) opponentFouls++; else myFouls++;
if (foulType == "Desqualificante") {
committerStats["fls"] = 5;
}
String logText = "cometeu Falta $foulType";
if (victimData.isNotEmpty) {
String victimId = victimData.replaceAll("player_my_", "").replaceAll("player_opp_", "");
final victimStats = playerStats[victimId]!;
final victimName = playerNames[victimId] ?? "Jogador";
victimStats["so"] = victimStats["so"]! + 1;
logText += " sobre $victimName ⚠️";
} else {
logText += " (Equipa/Banco) ⚠️";
}
String time = "${durationNotifier.value.inMinutes.toString().padLeft(2, '0')}:${durationNotifier.value.inSeconds.remainder(60).toString().padLeft(2, '0')}";
playByPlay.insert(0, "P$currentQuarter - $time: $committerName $logText");
_scheduleAutoSave();
notifyListeners();
}
void commitStat(String action, String playerData) {
bool isOpponent = playerData.startsWith("player_opp_");
String playerId = playerData
.replaceAll("player_my_", "")
.replaceAll("player_opp_", "");
final stats = playerStats[playerId]!;
final name = playerNames[playerId] ?? "Jogador";
String logText = "";
// ── PONTOS ──────────────────────────────────────────────────────────────
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) {
stats["fgm"] = stats["fgm"]! + 1;
stats["fga"] = stats["fga"]! + 1;
stats["p2m"] = stats["p2m"]! + 1;
stats["p2a"] = stats["p2a"]! + 1;
}
if (pts == 3) {
stats["fgm"] = stats["fgm"]! + 1;
stats["fga"] = stats["fga"]! + 1;
stats["p3m"] = stats["p3m"]! + 1;
stats["p3a"] = stats["p3a"]! + 1;
}
if (pts == 1) {
stats["ftm"] = stats["ftm"]! + 1;
stats["fta"] = stats["fta"]! + 1;
}
logText = "marcou $pts pontos 🏀";
}
// ── ANULAR PONTOS ────────────────────────────────────────────────────────
else if (action.startsWith("sub_pts_")) {
int ptsToAnul = int.parse(action.split("_").last);
int lastShotIndex = matchShots.lastIndexWhere((s) =>
s.playerId == playerId && s.isMake == true && s.points == ptsToAnul);
if (lastShotIndex != -1) {
matchShots.removeAt(lastShotIndex);
if (isOpponent)
opponentScore -= ptsToAnul;
else
myScore -= ptsToAnul;
stats["pts"] = stats["pts"]! - ptsToAnul;
if (ptsToAnul == 2) {
if (stats["p2m"]! > 0) stats["p2m"] = stats["p2m"]! - 1;
if (stats["p2a"]! > 0) stats["p2a"] = stats["p2a"]! - 1;
if (stats["fgm"]! > 0) stats["fgm"] = stats["fgm"]! - 1;
if (stats["fga"]! > 0) stats["fga"] = stats["fga"]! - 1;
} else if (ptsToAnul == 3) {
if (stats["p3m"]! > 0) stats["p3m"] = stats["p3m"]! - 1;
if (stats["p3a"]! > 0) stats["p3a"] = stats["p3a"]! - 1;
if (stats["fgm"]! > 0) stats["fgm"] = stats["fgm"]! - 1;
if (stats["fga"]! > 0) stats["fga"] = stats["fga"]! - 1;
} else if (ptsToAnul == 1) {
if (stats["ftm"]! > 0) stats["ftm"] = stats["ftm"]! - 1;
if (stats["fta"]! > 0) stats["fta"] = stats["fta"]! - 1;
}
logText = "anulou cesto de $ptsToAnul pts ⏪";
} else {
return;
}
}
// ── FALHAS ───────────────────────────────────────────────────────────────
else if (action == "miss_1") {
stats["fta"] = stats["fta"]! + 1;
logText = "falhou lance livre ❌";
} else if (action == "miss_2") {
stats["fga"] = stats["fga"]! + 1;
stats["p2a"] = stats["p2a"]! + 1;
logText = "falhou lançamento de 2 ❌";
} else if (action == "miss_3") {
stats["fga"] = stats["fga"]! + 1;
stats["p3a"] = stats["p3a"]! + 1;
logText = "falhou lançamento de 3 ❌";
}
// ── RESSALTOS ─────────────────────────────────────────────────────────────
else if (action == "add_orb") {
stats["orb"] = stats["orb"]! + 1;
stats["rbs"] = stats["rbs"]! + 1;
logText = "ganhou ressalto ofensivo 🔄";
} else if (action == "add_drb") {
stats["drb"] = stats["drb"]! + 1;
stats["rbs"] = stats["rbs"]! + 1;
logText = "ganhou ressalto defensivo 🛡️";
}
// ── ASSISTÊNCIA ───────────────────────────────────────────────────────────
else if (action == "add_ast") {
stats["ast"] = stats["ast"]! + 1;
logText = "fez uma assistência 🤝";
}
// ── SOFRIDAS ──────────────────────────────────────────────────────────────
else if (action == "add_so") {
stats["so"] = stats["so"]! + 1;
logText = "sofreu uma falta 🤕";
}
// ══════════════════════════════════════════════════════════════════════════
// STEAL — ROUBO DE BOLA
// ══════════════════════════════════════════════════════════════════════════
else if (action == "add_stl" || action == "stl_steal") {
stats["stl"] = stats["stl"]! + 1;
logText = "roubou a bola 🥷";
} else if (action == "stl_intercept") {
stats["stl"] = stats["stl"]! + 1;
stats["il"] = stats["il"]! + 1;
logText = "intercetou um lançamento 🛑";
}
// ══════════════════════════════════════════════════════════════════════════
// BLOCK — DESARME
// ══════════════════════════════════════════════════════════════════════════
else if (action == "add_blk" || action == "blk_made") {
stats["blk"] = stats["blk"]! + 1;
logText = "fez um desarme (bloco) ✋";
} else if (action == "blk_suffered") {
stats["li"] = stats["li"]! + 1;
logText = "sofreu um desarme 🚫";
}
// Ações independentes legadas
else if (action == "add_il") {
stats["il"] = stats["il"]! + 1;
logText = "intercetou um lançamento 🛑";
} else if (action == "add_li") {
stats["li"] = stats["li"]! + 1;
logText = "teve o lançamento intercetado 🚫";
}
// ══════════════════════════════════════════════════════════════════════════
// TURNOVER — PERDE DE BOLA E INFRAÇÕES
// ══════════════════════════════════════════════════════════════════════════
else if (action == "add_tov" || action == "tov_badpass") {
stats["tov"] = stats["tov"]! + 1;
logText = "fez um passe ruim 🤦";
} else if (action == "tov_3s") {
stats["tres_seg"] = stats["tres_seg"]! + 1; // SOMA AOS 3 SEGUNDOS
stats["tov"] = stats["tov"]! + 1; // SOMA AO TURNOVER GERAL
logText = "violação de 3 segundos ⏱️";
} else if (action == "tov_clock") {
stats["tov"] = stats["tov"]! + 1;
logText = "violação de 24 segundos ⏱️";
} else if (action == "tov_travel") {
stats["pa"] = stats["pa"]! + 1; // SOMA AOS PASSOS
stats["tov"] = stats["tov"]! + 1; // SOMA AO TURNOVER GERAL
logText = "cometeu passos 🚶";
} else if (action == "tov_double") {
stats["dr"] = stats["dr"]! + 1; // SOMA AOS DRIBLES DUPLOS
stats["tov"] = stats["tov"]! + 1; // SOMA AO TURNOVER GERAL
logText = "fez drible duplo 🏀";
}
// ── ANULAR FALTA ──────────────────────────────────────────────────────────
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--;
}
logText = "teve falta anulada 🔄";
}
if (logText.isNotEmpty) {
String time =
"${durationNotifier.value.inMinutes.toString().padLeft(2, '0')}:${durationNotifier.value.inSeconds.remainder(60).toString().padLeft(2, '0')}";
playByPlay.insert(0, "P$currentQuarter - $time: $name $logText");
}
_scheduleAutoSave();
notifyListeners();
}
@override
void dispose() {
timer?.cancel();
_autoSaveTimer?.cancel();
super.dispose();
}
Future<void> saveGameStats(BuildContext context) async {
final supabase = Supabase.instance.client;
isSaving = true;
notifyListeners();
try {
bool isGameFinishedNow =
currentQuarter >= 4 && durationNotifier.value.inSeconds == 0;
String newStatus = isGameFinishedNow ? 'Terminado' : 'Pausado';
String topPtsName = '---';
int maxPts = -1;
String topAstName = '---';
int maxAst = -1;
String topRbsName = '---';
int maxRbs = -1;
String mvpName = '---';
double maxMvpScore = -999.0;
playerStats.forEach((playerId, stats) {
int pts = stats['pts'] ?? 0;
int ast = stats['ast'] ?? 0;
int rbs = stats['rbs'] ?? 0;
double minJogados = (stats['sec'] ?? 0) / 60.0;
if (minJogados <= 0) minJogados = 40.0;
int tr = rbs;
int br = stats['stl'] ?? 0;
int bp = stats['tov'] ?? 0;
int lFalhados = (stats['fga'] ?? 0) - (stats['fgm'] ?? 0);
int llFalhados = (stats['fta'] ?? 0) - (stats['ftm'] ?? 0);
double mvpScore =
((pts * 0.30) + (tr * 0.20) + (ast * 0.35) + (br * 0.15)) -
((bp * 0.35) + (lFalhados * 0.30) + (llFalhados * 0.35));
mvpScore = mvpScore * (minJogados / 40.0);
String pName = playerNames[playerId] ?? '---';
if (pts > maxPts && pts > 0) {
maxPts = pts;
topPtsName = '$pName ($pts)';
}
if (ast > maxAst && ast > 0) {
maxAst = ast;
topAstName = '$pName ($ast)';
}
if (rbs > maxRbs && rbs > 0) {
maxRbs = rbs;
topRbsName = '$pName ($rbs)';
}
if (mvpScore > maxMvpScore) {
maxMvpScore = mvpScore;
mvpName = '$pName (${mvpScore.toStringAsFixed(1)})';
}
});
// 1. Atualizar o Jogo
await supabase.from('games').update({
'my_score': myScore,
'opponent_score': opponentScore,
'remaining_seconds': durationNotifier.value.inSeconds,
'my_timeouts': myTimeoutsUsed,
'opp_timeouts': opponentTimeoutsUsed,
'current_quarter': currentQuarter,
'status': newStatus,
'top_pts_name': topPtsName,
'top_ast_name': topAstName,
'top_rbs_name': topRbsName,
'mvp_name': mvpName,
'play_by_play': playByPlay,
}).eq('id', gameId);
// 2. Preparar as Estatísticas dos Jogadores
List<Map<String, dynamic>> batchStats = [];
playerStats.forEach((playerId, stats) {
if (!playerId.startsWith("fake_")) {
bool isMyTeamPlayer =
myCourt.contains(playerId) || myBench.contains(playerId);
batchStats.add({
'game_id': gameId,
'member_id': playerId,
'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'],
'p2m': stats['p2m'],
'p2a': stats['p2a'],
'p3m': stats['p3m'],
'p3a': stats['p3a'],
'so': stats['so'],
'il': stats['il'],
'li': stats['li'],
'pa': stats['pa'],
'tres_seg': stats['tres_seg'],
'dr': stats['dr'],
'minutos_jogados': stats['sec'],
});
}
});
// 3. Preparar os Locais dos Lançamentos (MAPA DE CALOR) - O QUE FALTAVA
List<Map<String, dynamic>> batchShots = [];
for (var shot in matchShots) {
if (!shot.playerId.startsWith("fake_")) {
batchShots.add({
'game_id': gameId,
'member_id': shot.playerId,
'player_name': shot.playerName,
'relative_x': shot.relativeX,
'relative_y': shot.relativeY,
'is_make': shot.isMake,
'zone': shot.zone,
'points': shot.points,
});
}
}
// Guardar na BD: Apaga as antigas e insere as atualizadas
await supabase.from('player_stats').delete().eq('game_id', gameId);
if (batchStats.isNotEmpty) {
await supabase.from('player_stats').insert(batchStats);
}
// Guardar mapa de calor na BD
await supabase.from('shot_locations').delete().eq('game_id', gameId);
if (batchShots.isNotEmpty) {
await supabase.from('shot_locations').insert(batchShots);
}
// Limpar backup local
final prefs = await SharedPreferences.getInstance();
await prefs.remove('backup_$gameId');
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('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));
}
} finally {
isSaving = false;
notifyListeners();
}
}
}