Files
PlayMaker/lib/controllers/placar_controller.dart
2026-03-09 15:05:14 +00:00

430 lines
16 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class ShotRecord {
final Offset position;
final bool isMake;
ShotRecord(this.position, this.isMake);
}
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;
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> playerNumbers = {};
Map<String, Map<String, int>> playerStats = {};
Map<String, String> playerDbIds = {};
bool showMyBench = false;
bool showOppBench = false;
bool isSelectingShotLocation = false;
String? pendingAction;
String? pendingPlayer;
List<ShotRecord> matchShots = [];
Duration duration = const Duration(minutes: 10);
Timer? timer;
bool isRunning = false;
// --- 🔄 CARREGAMENTO COMPLETO (DADOS REAIS + ESTATÍSTICAS SALVAS) ---
Future<void> loadPlayers() async {
final supabase = Supabase.instance.client;
try {
await Future.delayed(const Duration(milliseconds: 1500));
// 1. Limpar estados para evitar duplicação
myCourt.clear();
myBench.clear();
oppCourt.clear();
oppBench.clear();
playerStats.clear();
playerNumbers.clear();
playerDbIds.clear();
myFouls = 0;
opponentFouls = 0;
// 2. Buscar dados básicos do JOGO
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;
// 3. Buscar os IDs das equipas
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'];
}
// 4. Buscar os Jogadores
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') : [];
// 5. BUSCAR ESTATÍSTICAS JÁ SALVAS
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
};
// 6. Registar a tua equipa
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,
};
myFouls += (s['fls'] as int? ?? 0);
}
}
_padTeam(myCourt, myBench, "Jogador", isMyTeam: true);
// 7. Registar a equipa adversária
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,
};
opponentFouls += (s['fls'] as int? ?? 0);
}
}
_padTeam(oppCourt, oppBench, "Adversário", isMyTeam: false);
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();
}
}
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};
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}) {
while (court.length < 5) {
_registerPlayer(name: "Sem $prefix ${court.length + 1}", number: "0", dbId: null, isMyTeam: isMyTeam, isCourt: true);
}
}
// --- TEMPO E TIMEOUTS ---
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;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Período $currentQuarter iniciado. Faltas resetadas!'), backgroundColor: Colors.blue));
} else {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('FIM DO JOGO!'), backgroundColor: Colors.red));
}
}
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')}";
// --- LÓGICA DE JOGO & VALIDAÇÃO GEOMÉTRICA DE ZONAS ---
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;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Sai $courtPlayerName, Entra $benchPlayer')));
}
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;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Sai $courtPlayerName, Entra $benchPlayer')));
}
onUpdate();
}
// AGORA RECEBE CONTEXT E SIZE PARA A MATEMÁTICA
void registerShotLocation(BuildContext context, Offset position, Size size) {
if (pendingAction == null || pendingPlayer == null) return;
bool is3Pt = pendingAction!.contains("_3");
bool is2Pt = pendingAction!.contains("_2");
// Validação
if (is3Pt || is2Pt) {
bool isValid = _validateShotZone(position, size, is3Pt);
if (!isValid) {
// Se a validação falhar, fudeo. Bloqueia.
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(' Local de lançamento incompatível com a pontuação.'),
backgroundColor: Colors.red,
duration: Duration(seconds: 2),
)
);
return; // Aborta!
}
}
bool isMake = pendingAction!.startsWith("add_pts_");
matchShots.add(ShotRecord(position, isMake));
commitStat(pendingAction!, pendingPlayer!);
isSelectingShotLocation = false;
pendingAction = null;
pendingPlayer = null;
onUpdate();
}
// A MATEMÁTICA DA ZONA
bool _validateShotZone(Offset pos, Size size, bool is3Pt) {
double w = size.width;
double h = size.height;
// Ajusta o 0.12 e 0.88 se os teus cestos na imagem estiverem mais para o lado
Offset leftHoop = Offset(w * 0.12, h * 0.5);
Offset rightHoop = Offset(w * 0.88, h * 0.5);
// O raio da linha de 3 pontos (Brinca com este 0.28 se a área ficar muito grande ou pequena)
double threePointRadius = w * 0.28;
Offset activeHoop = pos.dx < w / 2 ? leftHoop : rightHoop;
double distanceToHoop = (pos - activeHoop).distance;
// Zonas de canto (onde a linha de 3 é reta)
bool isCorner3 = (pos.dy < h * 0.15 || pos.dy > h * 0.85) &&
(pos.dx < w * 0.20 || pos.dx > w * 0.80);
if (is3Pt) {
return distanceToHoop >= threePointRadius || isCorner3;
} else {
return distanceToHoop < threePointRadius && !isCorner3;
}
}
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; }
}
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;
}
}
else if (action == "miss_2" || action == "miss_3") { stats["fga"] = stats["fga"]! + 1; }
else if (action == "add_rbs") { 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--; }
}
}
// --- 💾 FUNÇÃO PARA GUARDAR DADOS NA BD ---
Future<void> saveGameStats(BuildContext context) async {
final supabase = Supabase.instance.client;
isSaving = true;
onUpdate();
try {
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': currentQuarter >= 4 && duration.inSeconds == 0 ? 'Terminado' : 'Pausado',
}).eq('id', gameId);
List<Map<String, dynamic>> 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);
String teamId = isMyTeamPlayer ? myTeamDbId! : oppTeamDbId!;
batchStats.add({
'game_id': gameId,
'member_id': memberDbId,
'team_id': teamId,
'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'],
});
}
});
await supabase.from('player_stats').delete().eq('game_id', gameId);
if (batchStats.isNotEmpty) {
await supabase.from('player_stats').insert(batchStats);
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Estatísticas guardadas 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;
onUpdate();
}
}
void dispose() {
timer?.cancel();
}
}