import os import json import math from collections import defaultdict import tkinter as tk from tkinter import ttk, messagebox, filedialog import requests # --------------------------------------------------------- # Internal modules — clean, correct, no duplicates # --------------------------------------------------------- # Career DB builder from data.career_db import build_career_database # Career rankings (used for global leaderboards) from rankings import ( top_players_by_tag, split_two_columns, rank_match_players_by_tag, ) # Team identity + storyline + predictions from analysis.team_identity_v2 import generate_team_identity_block from analysis.storylines import generate_storyline, matchup_storyline from analysis.predictions_v2 import generate_prediction from analysis.rookie_leaderboard import rank_rookies_by_tag # OBS export from analysis.obs_export_v2 import export_all_obs # Domination objective (match + career) from tags.objective_domination import ( compute_dom_objective_for_career_player, compute_dom_objective_for_match_player, compute_dom_objective_team_scores, ) # Prediction engine (legacy but still used) from predictions.prediction_engine import generate_predictions # Match-based tag engines from match_engine import ( rank_match_players_slayer, rank_match_players_objpl, rank_match_players_objdom, rank_match_players_sharpshooter, rank_match_players_clutch, compute_slayer_prediction, rank_match_players_consistency_career, load_career_db, attach_career_tags_to_match_player, ) from data.tag_framework import percentile_rank, tier_from_percentile # --------------------------------------------------------- # PATHS + CONFIG # --------------------------------------------------------- BASE_DIR = os.path.dirname(os.path.abspath(__file__)) CONFIG_PATH = os.path.join(BASE_DIR, "config.json") SCREENS_DIR = os.path.join(BASE_DIR, "screens") # Load config (or default) if os.path.exists(CONFIG_PATH): with open(CONFIG_PATH, "r", encoding="utf-8") as f: config = json.load(f) else: config = {} # Default output folder (outside repo) DEFAULT_OUTPUT_ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", "stats")) # Allow override from config.json output_root = config.get("output_root", DEFAULT_OUTPUT_ROOT) # Build runtime directories STATS_DIR = output_root PLAYERS_DIR = os.path.join(STATS_DIR, "players") OBS_EXPORT_DIR = os.path.join(STATS_DIR, "obs_exports") os.makedirs(PLAYERS_DIR, exist_ok=True) os.makedirs(OBS_EXPORT_DIR, exist_ok=True) API_BASE = "https://dashleague.games/api/v1" def export_to_obs(filename, text): path = os.path.join(OBS_EXPORT_DIR, filename) with open(path, "w", encoding="utf-8") as f: f.write(text) return path # --------------------------------------------------------- # Breaker (match-based) # --------------------------------------------------------- def compute_breaker_for_match_player(p): kills = p.get("kills", 0) or 0 damage = p.get("damage", 0) or 0 dom_counters = p.get("DOM_Counters", 0) or 0 dom_caps = p.get("DOM_Captures", 0) or 0 cp_caps = p.get("CP_Captures", 0) or 0 raw = ( dom_counters * 1.0 + dom_caps * 0.6 + cp_caps * 0.4 + kills * 0.05 + damage / 20000.0 ) return max(raw, 0.0) # --------------------------------------------------------- # Team Identity Formatter (Standalone Helper) # --------------------------------------------------------- def format_team_identity(identity): """ Converts the identity block dict into readable caster-friendly text. """ lines = [] lines.append(f"Team Style: {identity.get('team_style', 'Unknown')}") lines.append("") lines.append("Strengths:") for s in identity.get("strengths", []): lines.append(f" - {s}") lines.append("") lines.append("Weaknesses:") for w in identity.get("weaknesses", []): lines.append(f" - {w}") lines.append("") lines.append("Tag Breakdown:") for tag, data in identity.get("tags", {}).items(): lines.append( f" - {tag.replace('_', ' ').title()}: " f"{data['tier']} Tier (avg {data['avg_pct']:.1f} percentile)" ) return "\n".join(lines) def fetch_json(url): resp = requests.get(url, timeout=10) resp.raise_for_status() return resp.json() def ensure_player_slots(): os.makedirs(PLAYERS_DIR, exist_ok=True) for i in range(10): slot_dir = os.path.join(PLAYERS_DIR, f"p{i}") os.makedirs(slot_dir, exist_ok=True) def write_text(path, value): with open(path, "w", encoding="utf-8") as f: f.write(str(value)) class DashLeagueGUI: def __init__(self, root): self.root = root self.root.title("DashLeague Casting Tool") self.config = {} self.current_season = None self.team_a_var = tk.StringVar() self.team_b_var = tk.StringVar() self.team_a_players = [] self.team_b_players = [] self.stats_by_id = {} self.status_var = tk.StringVar(value="Ready") self.load_config() ensure_player_slots() self._build_ui() self.current_match_players = [] def load_config(self): if not os.path.exists(CONFIG_PATH): messagebox.showerror("Error", f"config.json not found at {CONFIG_PATH}") self.config = {} return with open(CONFIG_PATH, "r", encoding="utf-8") as f: self.config = json.load(f) self.current_season = self.config.get("current_season", 10) def _build_ui(self): main_frame = ttk.Frame(self.root, padding=10) main_frame.grid(row=0, column=0, sticky="nsew") self.team_a_var = tk.StringVar() self.team_b_var = tk.StringVar() self.root.columnconfigure(0, weight=1) self.root.rowconfigure(0, weight=1) main_frame.columnconfigure(0, weight=1) main_frame.columnconfigure(1, weight=1) top_frame = ttk.Frame(main_frame) top_frame.grid(row=0, column=0, columnspan=2, sticky="ew", pady=(0, 10)) self.build_db_button = ttk.Button(top_frame, text="Build Career Database", command=self.on_build_career_db) self.build_db_button.grid(row=0, column=0, padx=(0, 10)) self.sync_button = ttk.Button(top_frame, text="Sync Stats", command=self.on_sync_stats) self.sync_button.grid(row=0, column=1, padx=(0, 10)) self.team_id_button = ttk.Button(top_frame, text="Team Identity", command=self.on_team_identity) self.team_id_button.grid(row=0, column=2, padx=(0, 10)) self.storyline_button = ttk.Button(top_frame, text="Match Storyline", command=self.on_match_storyline) self.storyline_button.grid(row=0, column=3, padx=(0, 10)) self.rankings_button = ttk.Button(top_frame, text="Rankings", command=self.on_rankings) self.rankings_button.grid(row=0, column=4, padx=(0, 10)) self.rookie_button = ttk.Button(top_frame, text="Rookie Leaderboard", command=self.on_rookie_leaderboard) self.rookie_button.grid(row=0, column=5, padx=(0, 10)) self.output_folder_button = ttk.Button( top_frame, text="Select Output Folder", command=self.choose_output_folder ) self.output_folder_button.grid(row=0, column=7, padx=(0, 10)) self.map_var = tk.StringVar() self.map_var.set("Payload") # default teams = self.config.get("teams", []) left_frame = ttk.LabelFrame(main_frame, text="Team A") left_frame.grid(row=1, column=0, sticky="nsew", padx=(0, 5)) right_frame = ttk.LabelFrame(main_frame, text="Team B") right_frame.grid(row=1, column=1, sticky="nsew", padx=(5, 0)) main_frame.rowconfigure(1, weight=1) left_frame.rowconfigure(1, weight=1) right_frame.rowconfigure(1, weight=1) ttk.Label(left_frame, text="Team:").grid(row=0, column=0, sticky="w") self.team_a_combo = ttk.Combobox(left_frame, textvariable=self.team_a_var, values=teams, state="readonly") self.team_a_combo.grid(row=0, column=1, sticky="ew", pady=2) self.team_a_combo.bind("<>", lambda e: self.on_team_select("A")) ttk.Label(right_frame, text="Team:").grid(row=0, column=0, sticky="w") self.team_b_combo = ttk.Combobox(right_frame, textvariable=self.team_b_var, values=teams, state="readonly") self.team_b_combo.grid(row=0, column=1, sticky="ew", pady=2) self.team_b_combo.bind("<>", lambda e: self.on_team_select("B")) self.team_a_listbox = tk.Listbox(left_frame, selectmode=tk.MULTIPLE, exportselection=False) self.team_a_listbox.grid(row=1, column=0, columnspan=2, sticky="nsew", pady=(5, 0)) self.team_b_listbox = tk.Listbox(right_frame, selectmode=tk.MULTIPLE, exportselection=False) self.team_b_listbox.grid(row=1, column=0, columnspan=2, sticky="nsew", pady=(5, 0)) self.team_a_listbox.bind("<>", lambda e: self.enforce_rolling_five(e, "A")) self.team_b_listbox.bind("<>", lambda e: self.enforce_rolling_five(e, "B")) bottom_frame = ttk.Frame(main_frame) bottom_frame.grid(row=2, column=0, columnspan=2, sticky="ew", pady=(10, 0)) self.generate_button = ttk.Button(bottom_frame, text="Generate Player Slots", command=self.on_generate_slots) self.generate_button.grid(row=0, column=0, sticky="ew") ttk.Label(bottom_frame, text="Map:").grid(row=0, column=1, padx=(10, 5)) ttk.OptionMenu(bottom_frame, self.map_var, "Payload", "Payload", "Domination").grid(row=0, column=2) # Create a specific section for the 4K Graphics broadcast_frame = ttk.LabelFrame(main_frame, text="4K Broadcast Graphics", padding=10) broadcast_frame.grid(row=3, column=0, columnspan=2, sticky="ew", pady=(10, 0)) ttk.Button(broadcast_frame, text="Player Spotlight", command=self.on_player_spotlight).grid(row=0, column=0, padx=5) ttk.Button(broadcast_frame, text="Tale of the Tape (PvP)", command=self.on_tale_of_the_tape).grid(row=0, column=1, padx=5) ttk.Button(broadcast_frame, text="Team vs Team (DNA)", command=self.on_team_comparison).grid(row=0, column=2, padx=5) ttk.Button(broadcast_frame, text="Win Probability", command=self.on_win_probability).grid(row=0, column=3, padx=5) status_frame = ttk.Frame(self.root) status_frame.grid(row=1, column=0, sticky="ew") status_label = ttk.Label(status_frame, textvariable=self.status_var, anchor="w") status_label.grid(row=0, column=0, sticky="ew") status_frame.columnconfigure(0, weight=1) def set_status(self, text): self.status_var.set(text) self.root.update_idletasks() # Add these methods inside the DashLeagueGUI class def get_team_roster_data(self, team_name): """Helper to get full career data for the current 5-man roster.""" career_path = os.path.join(BASE_DIR, "career_stats.json") with open(career_path, "r", encoding="utf-8") as f: db = json.load(f) roster_data = [] # Filter match players for this specific team team_players = [p for p in self.current_match_players if p.get("team") == team_name] for p in team_players: cid = p.get("canonical") if cid in db["players"]: # Merge match team info with career stats p_data = db["players"][cid] p_data["current_team"] = team_name roster_data.append(p_data) return roster_data def on_tale_of_the_tape(self): if not self.current_match_players: messagebox.showerror("Error", "Generate Player Slots first.") return team_a = self.team_a_var.get() team_b = self.team_b_var.get() roster_a = self.get_team_roster_data(team_a) roster_b = self.get_team_roster_data(team_b) from utils.graphics_engine import generate_tale_of_the_tape try: generate_tale_of_the_tape(roster_a, roster_b, team_a, team_b) self.set_status("Generated 4K Tale of the Tape (PvP)") messagebox.showinfo("Success", "PvP Graphic generated in obs_exports.") except Exception as e: messagebox.showerror("Graphics Error", str(e)) def on_team_comparison(self): if not self.current_match_players: messagebox.showerror("Error", "Generate Player Slots first.") return team_a = self.team_a_var.get() team_b = self.team_b_var.get() roster_a = self.get_team_roster_data(team_a) roster_b = self.get_team_roster_data(team_b) from utils.graphics_engine import generate_team_dna try: generate_team_dna(roster_a, roster_b, team_a, team_b) self.set_status("Generated 4K Team DNA Graphic") messagebox.showinfo("Success", "Team DNA Graphic generated in obs_exports.") except Exception as e: messagebox.showerror("Graphics Error", str(e)) def on_win_probability(self): if not self.current_match_players: messagebox.showerror("Error", "Generate Player Slots first.") return team_a = self.team_a_var.get() team_b = self.team_b_var.get() roster_a = self.get_team_roster_data(team_a) roster_b = self.get_team_roster_data(team_b) from utils.graphics_engine import generate_win_probability try: generate_win_probability(roster_a, roster_b, team_a, team_b) self.set_status("Generated 4K Win Probability Graphic") messagebox.showinfo("Success", "Win Probability Graphic generated in obs_exports.") except Exception as e: messagebox.showerror("Graphics Error", str(e)) def on_sync_stats(self): try: self.set_status(f"Syncing stats for season {self.current_season}...") url = f"{API_BASE}/stats?season={self.current_season}" data = fetch_json(url) stats = data.get("data", []) print("DEBUG: Checking raw API teams...") for p in stats: print("TEAM FIELD:", repr(p.get("team"))) # --------------------------------------------------------- # Load PIR + Career DB # --------------------------------------------------------- from analysis.player_identity_registry import PlayerIdentityRegistry pir = PlayerIdentityRegistry() career_path = os.path.join(BASE_DIR, "career_stats.json") with open(career_path, "r", encoding="utf-8") as f: career_db = json.load(f) players_db = career_db.setdefault("players", {}) # --------------------------------------------------------- # BUILD stats_by_id (canonical → enriched player object) # --------------------------------------------------------- new_stats_by_id = {} for p in stats: raw_id = p.get("id") if not raw_id: continue # Resolve canonical + stamp seasons canonical = pir.resolve(raw_id, season=self.current_season) canonical = pir.resolve(raw_id, season=self.current_season) # Update Registry and History with clean names clean_name = p.get("name") clean_team = (p.get("team") or "").strip() pir.update_name(canonical, clean_name) if clean_team: pir.update_team(canonical, clean_team) # Ensure career_stats has this history if canonical in players_db: players_db[canonical]["team_history"] = pir.data["canonical"][canonical]["team_history"] if not canonical: continue # Ensure first_season is set if missing entry = pir.data["canonical"].get(canonical) #if entry is not None and entry.get("first_season") is None: # entry["first_season"] = self.current_season # Normalize DOM fields from API (for DomObj) p["DOM_Captures"] = p.get("DOM_captures", p.get("DOM_Captures", 0)) p["DOM_Counters"] = p.get("DOM_counters", p.get("DOM_Counters", 0)) merged = { "canonical": canonical, "name": p.get("name"), "team": (p.get("team") or "").strip(), "id": raw_id, "kills": p.get("kills", 0), "deaths": p.get("deaths", 0), "damage": p.get("damage", 0), "shots": p.get("shots", 0), "shots_hit": p.get("shots_hit", 0), "match_stats": p, "seasons_played": p.get("seasons_played") or p.get("seasons") or "", } # Ensure player exists in career DB (so rookies are present) if canonical not in players_db: players_db[canonical] = { "name": merged["name"], "team_history": [merged["team"]] if merged["team"] else [], "seasons_played": merged.get("seasons_played"), "slayer": {}, "sharpshooter": {}, "objective_payload": {}, "objective_domination": {}, "consistency": {}, "clutch": {}, "anchor": {}, "breaker": {}, } pdata = players_db.setdefault(canonical, {}) pdata["seasons_played"] = merged["seasons_played"] print("DEBUG: seasons fields:") #for p in stats: # print(p.get("name"), "seasons:", p.get("seasons"), "seasons_played:", p.get("seasons_played")) # Store basic stats into career DB so rookies have real data pdata["kills"] = merged.get("kills", 0) pdata["deaths"] = merged.get("deaths", 0) pdata["damage"] = merged.get("damage", 0) pdata["shots"] = merged.get("shots", 0) pdata["shots_hit"] = merged.get("shots_hit", 0) # Score is inside match_stats ms = merged.get("match_stats", {}) pdata["score"] = ms.get("score", 0) # KD if pdata["deaths"] > 0: pdata["kd"] = pdata["kills"] / pdata["deaths"] else: pdata["kd"] = pdata["kills"] # Keep seasons_played updated #if merged.get("seasons_played"): # pdata["seasons_played"] = merged["seasons_played"] #pdata["seasons_played"] = merged.get("seasons_played") # Attach career tags for tag in ( "slayer", "sharpshooter", "objective_payload", "objective_domination", "consistency", "clutch", "anchor", "breaker", ): merged[tag] = pdata.get(tag, {}) new_stats_by_id[canonical] = merged # Write merged stats into career DB players_db[canonical]["kills"] = merged["kills"] players_db[canonical]["deaths"] = merged["deaths"] players_db[canonical]["damage"] = merged["damage"] players_db[canonical]["shots"] = merged["shots"] players_db[canonical]["shots_hit"] = merged["shots_hit"] players_db[canonical]["match_stats"] = merged["match_stats"] players_db[canonical]["seasons_played"] = merged["seasons_played"] # Compute KD d = merged["deaths"] players_db[canonical]["kd"] = merged["kills"] / d if d > 0 else merged["kills"] # Score (inside match_stats) players_db[canonical]["score"] = merged["match_stats"].get("score", 0) # --------------------------------------------------------- # Save updated career DB (rookies now included) # --------------------------------------------------------- with open(career_path, "w", encoding="utf-8") as f: json.dump(career_db, f, indent=2) # --------------------------------------------------------- # Save PIR (first_season now persisted) # --------------------------------------------------------- pir.save() # --------------------------------------------------------- # Replace stats_by_id with enriched version # --------------------------------------------------------- self.stats_by_id = new_stats_by_id # --------------------------------------------------------- # Debug: show teams detected # --------------------------------------------------------- teams_in_api = sorted({p["team"] for p in new_stats_by_id.values()}) print("TEAMS IN API:", teams_in_api) self.set_status("Stats synced.") messagebox.showinfo("Success", f"Synced {len(stats)} player stats.") except Exception as e: messagebox.showerror("Error", f"Failed to sync stats:\n{e}") self.set_status("Sync failed.") # Re-trigger team selection so listboxes repopulate if self.team_a_var.get(): self.on_team_select("A") if self.team_b_var.get(): self.on_team_select("B") def enforce_rolling_five(self, event, side): listbox = self.team_a_listbox if side == "A" else self.team_b_listbox selection = listbox.curselection() # If 5 or fewer selected, nothing to do if len(selection) <= 5: return # More than 5 selected → rolling behavior: # Deselect the *oldest* selected index oldest = selection[0] listbox.selection_clear(oldest) def on_team_select(self, side): team_name = self.team_a_var.get() if side == "A" else self.team_b_var.get() print("TEAM SELECT FIRED:", side, "team_name=", team_name) # Debug: show how many players match this team matching = [] for p in self.stats_by_id.values(): if p.get("team") == team_name or p.get("TeamUUID") == team_name: matching.append(p) print(f"DEBUG: Found {len(matching)} players for team {team_name}") for p in matching: print(" PLAYER:", p.get("name"), "| TEAM:", p.get("team")) # Now assign to UI if side == "A": self.team_a_players = matching self.team_a_listbox.delete(0, tk.END) for p in matching: self.team_a_listbox.insert(tk.END, p.get("name", "Unknown")) else: self.team_b_players = matching self.team_b_listbox.delete(0, tk.END) for p in matching: self.team_b_listbox.insert(tk.END, p.get("name", "Unknown")) def normalize_player(self, p): return { "id": p.get("id"), "name": p.get("name"), "team": p.get("team"), "KD": p.get("KD", 0), "kills": p.get("kills", 0), "deaths": p.get("deaths", 0), "headshots": p.get("headshots", 0), "accuracy": p.get("accuracy", 0), "PAY_PushTime": p.get("PAY_PushTime", 0), "DOM_captures": p.get("DOM_captures", 0), "DOM_counters": p.get("DOM_counters", 0), } def on_generate_slots(self): try: ensure_player_slots() # --------------------------------------------------------- # 1. Collect selected players # --------------------------------------------------------- selected_a = [self.team_a_players[i] for i in self.team_a_listbox.curselection()] selected_b = [self.team_b_players[i] for i in self.team_b_listbox.curselection()] if not selected_a: selected_a = self.team_a_players if not selected_b: selected_b = self.team_b_players # Limit to 5 per team selected_a = selected_a[:5] selected_b = selected_b[:5] all_players = selected_a + selected_b print("=== DEBUG MATCH_STATS SAMPLE ===") print(all_players[0].get("match_stats", {})) # --------------------------------------------------------- # 2. Normalize stat keys BEFORE attaching tags # --------------------------------------------------------- for p in all_players: ms = p.get("match_stats", {}) # DOM p["DOM_Captures"] = ( p.get("DOM_Captures") or p.get("DOM_captures") or ms.get("DOM_Captures") or ms.get("DOM_captures") or 0 ) p["DOM_Counters"] = ( p.get("DOM_Counters") or p.get("DOM_counters") or ms.get("DOM_Counters") or ms.get("DOM_counters") or 0 ) # --------------------------------------------------------- # 3. Resolve canonical IDs AND normalize player ID # --------------------------------------------------------- from analysis.player_identity_registry import PlayerIdentityRegistry pir = PlayerIdentityRegistry() for p in all_players: # Resolve canonical ID first raw_uuid = ( p.get("id") or p.get("PlayerUUID") or p.get("uuid") ) if raw_uuid: p["canonical"] = pir.resolve(raw_uuid, season=self.current_season) # Normalize ID (AFTER canonical resolution) pid = ( p.get("id") or p.get("PlayerUUID") or p.get("uuid") or p.get("canonical") or p.get("name") ) p["id"] = pid # --------------------------------------------------------- # 4. Compute match-based Breaker # --------------------------------------------------------- for p in all_players: breaker_raw = compute_breaker_for_match_player(p) p["breaker"] = {"raw": breaker_raw} # --------------------------------------------------------- # 5. Load career DB # --------------------------------------------------------- career_db = None career_path = os.path.join(BASE_DIR, "career_stats.json") if os.path.exists(career_path): with open(career_path, "r", encoding="utf-8") as f: career_db = json.load(f) # --------------------------------------------------------- # 6. Compute match-based Consistency (raw only) # --------------------------------------------------------- print("\n=== DEBUG: Player objects BEFORE consistency ===") for p in all_players: print(p.get("name"), "| id:", p.get("id"), "| uuid:", p.get("uuid"), "| PlayerUUID:", p.get("PlayerUUID"), "| canonical:", p.get("canonical_id")) consistency_rows = rank_match_players_consistency_career(all_players) consistency_map = {row["id"]: row["score_raw"] for row in consistency_rows} for p in all_players: raw = consistency_map.get(p["id"], 0.0) p["consistency"] = {"raw": raw} # --------------------------------------------------------- # 7. Build match-based consistency distribution # --------------------------------------------------------- consistency_distribution = [p["consistency"]["raw"] for p in all_players] # --------------------------------------------------------- # 8. Compute percentile + tier for match-based Consistency # --------------------------------------------------------- from data.tag_framework import percentile_rank, tier_from_percentile for p in all_players: raw = p["consistency"]["raw"] pct = percentile_rank(raw, consistency_distribution) tier = tier_from_percentile(pct) p["consistency"]["pct"] = pct p["consistency"]["tier"] = tier p["consistency"]["summary"] = f"{tier} Tier ({pct:.1f} percentile)" # --------------------------------------------------------- # 9. Attach career tags (DO NOT overwrite match consistency) # --------------------------------------------------------- if career_db is not None: for p in all_players: cid = p.get("canonical") if not cid: continue pdata = career_db["players"].get(cid) if not pdata: continue p["slayer"] = pdata.get("slayer", {}) p["sharpshooter"] = pdata.get("sharpshooter", {}) p["objective_payload"] = pdata.get("objective_payload", {}) p["clutch"] = pdata.get("clutch", {}) p["anchor"] = pdata.get("anchor", {}) p["breaker"] = pdata.get("breaker", {}) # Do NOT overwrite match-based DomObj if "objective_domination" not in p or not p["objective_domination"]: p["objective_domination"] = pdata.get("objective_domination", {}) # --------------------------------------------------------- # 10. Compute match-based Domination Objective # --------------------------------------------------------- for p in all_players: dom = compute_dom_objective_for_match_player(p) caps = dom.get("captures", 0) counters = dom.get("counters", 0) dom_score = caps * 0.4 + counters * 0.6 print("DOM DEBUG:", p["name"], "caps=", caps, "counters=", counters) p["objective_domination"] = { "raw": dom_score, "pct": dom_score, # <- match panel can now read this "tier": "-", # <- neutral tier for single-match view "captures": caps, "counters": counters, "summary": "", # optional, keeps shape consistent } # 11. Ensure stats_by_id contains all enriched tags for p in all_players: cid = p.get("canonical") if not cid: continue enriched = self.stats_by_id.get(cid) if not enriched: continue # Copy match fields into stats_by_id enriched["team"] = p.get("team") enriched["name"] = p.get("name") enriched["id"] = p.get("id") # --------------------------------------------------------- # 12. Update current_match_players # --------------------------------------------------------- self.current_match_players = all_players # --------------------------------------------------------- # 13. Write OBS slot files # --------------------------------------------------------- for idx, p in enumerate(all_players): slot_dir = os.path.join(PLAYERS_DIR, f"p{idx}") os.makedirs(slot_dir, exist_ok=True) write_text(os.path.join(slot_dir, "IGN.txt"), p.get("name", "")) write_text(os.path.join(slot_dir, "Team.txt"), p.get("team", "")) write_text(os.path.join(slot_dir, "KD.txt"), p.get("KD", "")) write_text(os.path.join(slot_dir, "Kills.txt"), p.get("kills", "")) write_text(os.path.join(slot_dir, "Deaths.txt"), p.get("deaths", "")) write_text(os.path.join(slot_dir, "Headshots.txt"), p.get("headshots", "")) write_text(os.path.join(slot_dir, "Accuracy.txt"), p.get("accuracy", "")) write_text(os.path.join(slot_dir, "PushTime.txt"), p.get("PAY_PushTime", "")) write_text(os.path.join(slot_dir, "Captures.txt"), p.get("DOM_Captures", "")) write_text(os.path.join(slot_dir, "Counters.txt"), p.get("DOM_Counters", "")) # Consistency write_text(os.path.join(slot_dir, "ConsistencyRaw.txt"), p["consistency"]["raw"]) write_text(os.path.join(slot_dir, "ConsistencyPct.txt"), p["consistency"]["pct"]) write_text(os.path.join(slot_dir, "ConsistencyTier.txt"), p["consistency"]["tier"]) self.set_status("Player slots generated.") messagebox.showinfo("Success", "Player slots generated into p0–p9.") except Exception as e: messagebox.showerror("Error", f"Failed to generate slots:\n{e}") self.set_status("Generate failed.") self.export_all_obs_files() def on_build_career_db(self): try: self.set_status("Building career database...") # Use the same file the rest of the tool reads output_path = os.path.join(BASE_DIR, "career_stats.json") build_career_database(output_path) #print("DEBUG CAREER DB KEYS:", career_db.keys()) #print("DEBUG PLAYER COUNT:", len(career_db.get("players", {}))) self.set_status("Career database built successfully.") messagebox.showinfo("Success", "Career database has been built.") except Exception as e: import traceback traceback.print_exc() messagebox.showerror("Error", f"Failed to build career database:\n{e}") self.set_status("Career DB build failed.") def on_player_spotlight(self): if not self.current_match_players: messagebox.showerror("Error", "Please generate player slots first.") return # Prepare the list of players and names players = self.current_match_players names = [p["name"] for p in players] self.spotlight_idx = 0 # Load the career DB for Trend/Tag data career_path = os.path.join(BASE_DIR, "career_stats.json") with open(career_path, "r", encoding="utf-8") as f: db = json.load(f) win = tk.Toplevel(self.root) win.title("Spotlight Operator") win.geometry("500x700") ttk.Label(win, text="Active Spotlight Player:").pack(pady=5) # Dropdown selection combo_var = tk.StringVar() combo = ttk.Combobox(win, textvariable=combo_var, values=names, state="readonly") combo.pack(fill="x", padx=20) output = tk.Text(win, wrap="word", font=("Courier", 10), height=20) output.pack(expand=True, fill="both", padx=20, pady=10) def update_spotlight(idx): self.spotlight_idx = idx % len(players) p_match = players[self.spotlight_idx] selected_name = p_match["name"] combo_var.set(selected_name) # Get full career data cid = p_match.get("canonical") player_data = db["players"].get(cid) if not player_data: output.delete("1.0", tk.END) output.insert(tk.END, f"Data missing for {selected_name}") return # Determine Side team_b_names = [self.team_b_listbox.get(i) for i in range(self.team_b_listbox.size())] side = "Red" if selected_name in team_b_names else "Blue" # Generate 4K PNG from utils.graphics_engine import generate_player_card try: generate_player_card(player_data, team_side=side) self.set_status(f"Updated 4K Graphic: {selected_name}") except Exception as e: print(f"Graphics Error: {e}") # Update Text Preview career = player_data["career"] text = f"PLAYER: {selected_name}\n" text += f"Team: {p_match.get('team', 'Unknown')}\n" text += f"Side: {side}\n" text += "-"*30 + "\n" text += f"Kills: {int(career.get('kills',0)):,}\n" text += f"Maps: {int(career.get('maps',0))}\n" text += f"KD: {career.get('KD',0):.2f}\n\n" text += "SKILL PERCENTILES:\n" for tag in ["slayer", "sharpshooter", "consistency", "clutch", "anchor"]: t_obj = player_data.get(tag, {}) text += f"{tag.title():<14}: {t_obj.get('pct',0):.0f}%\n" output.delete("1.0", tk.END) output.insert(tk.END, text) # Button logic btn_frame = ttk.Frame(win) btn_frame.pack(pady=10) ttk.Button(btn_frame, text="<< Prev", command=lambda: update_spotlight(self.spotlight_idx - 1)).grid(row=0, column=0, padx=5) ttk.Button(btn_frame, text="Next >>", command=lambda: update_spotlight(self.spotlight_idx + 1)).grid(row=0, column=1, padx=5) # Sync dropdown selection to buttons combo.bind("<>", lambda e: update_spotlight(names.index(combo.get()))) # Initialize first player update_spotlight(0) def on_team_identity(self): team = self.team_a_var.get() if not team: messagebox.showerror("Error", "Select a team first.") return team_players = [p for p in self.current_match_players if p.get("team") == team] if len(team_players) == 0: messagebox.showerror("Error", "No players selected for this team.") return identity_data = generate_team_identity_block(team_players, team) identity_text = format_team_identity(identity_data) win = tk.Toplevel(self.root) win.title(f"Team Identity — {team}") win.geometry("500x600") output = tk.Text(win, wrap="word") output.pack(expand=True, fill="both", padx=10, pady=10) output.insert(tk.END, identity_text) def export(): content = output.get("1.0", tk.END).strip() if not content: messagebox.showerror("Error", "No team identity text to export.") return filename = f"team_identity_{team}.txt" path = export_to_obs(filename, content) messagebox.showinfo("Exported", f"Team identity exported to:\n{path}") ttk.Button(win, text="Export to OBS", command=export).pack(pady=5) def generate_player_tags(self, player_entry, map_type): tags_out = [] # --------------------------------------------------------- # Slayer (career) # --------------------------------------------------------- slayer = player_entry.get("slayer", {}) slayer_strength = slayer.get("strength") or player_entry.get("slayer_strength") if isinstance(slayer_strength, str): tags_out.append(slayer_strength) # --------------------------------------------------------- # Consistency (career - hybrid system) # --------------------------------------------------------- consistency = player_entry.get("consistency", {}) if isinstance(consistency, dict): score = consistency.get("raw") if isinstance(score, (int, float)): tags_out.append(f"Cons {score:.2f}") # --------------------------------------------------------- # Payload Objective Specialist (match-based) # --------------------------------------------------------- if map_type == "Payload": pl_info = player_entry.get("objective_payload_components", {}) pl_score = pl_info.get("payload_score_raw", 0.0) tags_out.append(f"ObjPL {pl_score:.2f}") # --------------------------------------------------------- # Domination Objective Specialist (match-based) # --------------------------------------------------------- if map_type == "Domination": dom_info = player_entry.get("objective_domination", {}) dom_score = dom_info.get("raw", 0.0) tags_out.append(f"ObjDOM {dom_score:.2f}") # --------------------------------------------------------- # Sharpshooter (career) # --------------------------------------------------------- sharp = player_entry.get("sharpshooter", {}).get("score") or player_entry.get("sharpshooter_score") if isinstance(sharp, (int, float)): tags_out.append(f"Sharp {sharp:.2f}") return ", ".join(tags_out) def on_match_storyline(self): team_a = self.team_a_var.get() team_b = self.team_b_var.get() if not team_a or not team_b: messagebox.showerror("Error", "Select both Team A and Team B.") return # --------------------------------------------------------- # 1. Attach career tags to ALL match players (CRITICAL) # --------------------------------------------------------- from match_engine import load_career_db, attach_career_tags_to_match_player career_db = load_career_db() # self.stats_by_id keys ARE canonical IDs for cid, p in self.stats_by_id.items(): if cid in career_db["players"]: attach_career_tags_to_match_player(p, career_db["players"][cid]) # --------------------------------------------------------- # 2. Extract players for each team (using enriched objects) # --------------------------------------------------------- teamA_players = [] teamB_players = [] for p in self.current_match_players: cid = p.get("canonical") # <-- correct key if not cid: continue enriched = self.stats_by_id.get(cid) if not enriched: continue if enriched.get("team") == team_a: teamA_players.append(enriched) elif enriched.get("team") == team_b: teamB_players.append(enriched) if not teamA_players or not teamB_players: messagebox.showerror("Error", "No players selected for one or both teams.") return # --------------------------------------------------------- # 3. Generate storyline + identity blocks # --------------------------------------------------------- storyline_text = matchup_storyline(teamA_players, teamB_players) identity_data_a = generate_team_identity_block(teamA_players, team_a) identity_text_a = format_team_identity(identity_data_a) identity_data_b = generate_team_identity_block(teamB_players, team_b) identity_text_b = format_team_identity(identity_data_b) full_story = ( storyline_text + "\n\nTEAM IDENTITY SUMMARY – " + team_a + "\n" + identity_text_a + "\n\nTEAM IDENTITY SUMMARY – " + team_b + "\n" + identity_text_b ) # --------------------------------------------------------- # 4. Popup window # --------------------------------------------------------- win = tk.Toplevel(self.root) win.title("Match Storyline") win.geometry("600x700") output = tk.Text(win, wrap="word") output.pack(expand=True, fill="both", padx=10, pady=10) output.insert(tk.END, full_story) # --------------------------------------------------------- # 5. Export to OBS # --------------------------------------------------------- def export(): content = output.get("1.0", tk.END).strip() if not content: messagebox.showerror("Error", "No storyline text to export.") return teamA_name = self.team_a_var.get() teamB_name = self.team_b_var.get() export_all_obs( OBS_EXPORT_DIR, teamA_players, teamB_players, teamA_name, teamB_name, ) messagebox.showinfo("Exported", f"OBS files exported to:\n{OBS_EXPORT_DIR}") ttk.Button(win, text="Export to OBS", command=export).pack(pady=5) def on_rankings(self): if not self.current_match_players: messagebox.showerror("Error", "Generate player slots first.") return win = tk.Toplevel(self.root) win.title("Rankings") win.geometry("800x600") tags = [ "Slayer", "Payload Objective Specialist", "Sharpshooter", "Domination Objective Specialist", "Consistency", "Clutch", "Anchor", "Breaker", ] ttk.Label(win, text="Select Tag:").pack(anchor="w", padx=10, pady=5) combo = ttk.Combobox(win, values=tags, state="readonly") combo.pack(fill="x", padx=10) output = tk.Text(win, wrap="word") output.pack(expand=True, fill="both", padx=10, pady=10) def show(): tag = combo.get() if not tag: return from match_engine import load_career_db, attach_career_tags_to_match_player career_db = load_career_db() if career_db: for p in self.current_match_players: cid = p.get("canonical") if cid and cid in career_db["players"]: attach_career_tags_to_match_player(p, career_db["players"][cid]) team_a = self.team_a_var.get() team_b = self.team_b_var.get() if not team_a or not team_b: messagebox.showerror("Error", "Select both teams first.") return if tag == "Slayer": ranked = rank_match_players_slayer(self.current_match_players) elif tag == "Sharpshooter": ranked = rank_match_players_sharpshooter(self.current_match_players) elif tag == "Payload Objective Specialist": ranked = rank_match_players_objpl(self.current_match_players) elif tag == "Domination Objective Specialist": ranked = rank_match_players_objdom(self.current_match_players) elif tag == "Consistency": ranked = rank_match_players_by_tag(self.current_match_players, "consistency") elif tag == "Clutch": ranked = rank_match_players_clutch(self.current_match_players) elif tag == "Anchor": ranked = rank_match_players_by_tag(self.current_match_players, "anchor") elif tag == "Breaker": ranked = rank_match_players_by_tag(self.current_match_players, "breaker") elif tag == "Rookie": ranked = compute_rookie_leaderboard(career_db, tag="slayer") # convert rookie rows to the same format as other rankings else: messagebox.showerror("Error", f"Unknown tag: {tag}") return if not ranked: output.delete("1.0", tk.END) output.insert(tk.END, "No data available for this tag.") return left, right = split_two_columns(ranked, team_a, team_b) text = f"TOP {tag.upper()} PLAYERS (THIS MATCH):\n\n" # Two-column layout, but keep global rank max_rows = max(len(left), len(right)) text += f"{team_a:<24}{team_b}\n" text += "-" * 48 + "\n" for i in range(max_rows): left_str = "" right_str = "" if i < len(left): lp = left[i] #print("DEBUG LP ROW:", lp) # <--- add this pct = lp.get("pct") if pct is not None: pct_str = f"{pct:.0f}%" left_str = f"{lp['rank']:>2}. {lp['name']:<16} {lp['score_display']:>6} ({pct_str})" else: left_str = f"{lp['rank']:>2}. {lp['name']:<16} {lp['score_display']:>6}" if i < len(right): rp = right[i] pct = rp.get("pct") if pct is not None: pct_str = f"{pct:.0f}%" right_str = f"{rp['rank']:>2}. {rp['name']:<16} {rp['score_display']:>6} ({pct_str})" else: right_str = f"{rp['rank']:>2}. {rp['name']:<16} {rp['score_display']:>6}" text += f"{left_str:<24}{right_str}\n" # ----------------------------- # TEAM TOTALS & AVERAGES (COLUMN FORMAT) # ----------------------------- teamA_total = sum(p["score_raw"] for p in left) teamB_total = sum(p["score_raw"] for p in right) teamA_avg = teamA_total / max(1, len(left)) teamB_avg = teamB_total / max(1, len(right)) text += "-" * 48 + "\n" text += f"{team_a} Total: {teamA_total:.2f}".ljust(24) text += f"{team_b} Total: {teamB_total:.2f}\n" text += f"{team_a} Avg: {teamA_avg:.2f}".ljust(24) text += f"{team_b} Avg: {teamB_avg:.2f}\n" # ----------------------------- # DELTA (relative strength) # ----------------------------- delta = teamB_avg - teamA_avg if delta > 0: fav_delta = team_b elif delta < 0: fav_delta = team_a else: fav_delta = "Neither team" delta_mag = abs(delta) text += f"Delta: {delta_mag:.2f} in favour of {fav_delta}\n" stronger_avg = max(teamA_avg, teamB_avg) if stronger_avg > 0: rel_adv = (delta_mag / stronger_avg) * 100 else: rel_adv = 0.0 text += f"Relative Advantage: {rel_adv:.1f}%\n" # ----------------------------- # WIN CHANCE + SWING # ----------------------------- if tag == "Slayer": pred = compute_slayer_prediction(left, right) teamA_win = pred["teamA_win"] teamB_win = pred["teamB_win"] else: # ObjPL win factor (simple logistic) if max(teamA_avg, teamB_avg) == 0: edge = 0 else: edge = (teamB_avg - teamA_avg) / max(teamA_avg, teamB_avg) k = 1.1 pB = 1 / (1 + math.exp(-k * edge)) pA = 1 - pB teamA_win = round(pA * 100) teamB_win = round(pB * 100) swing = abs(teamA_win - teamB_win) fav = team_a if teamA_win > teamB_win else team_b text += f"Win Chance: {team_a} {teamA_win}% — {team_b} {teamB_win}%\n" text += f"Win Swing: {swing}% in favour of {fav}\n" # ----------------------------- # FORMULA EXPLANATION # ----------------------------- if tag == "Slayer": text += "\nSlayer Formula:\n" text += "Slayer Score = (Damage per Map × 0.40) + (Kills per Map × 0.40) + (KD × 0.20)\n" elif tag == "Sharpshooter": text += "\nSharpshooter Formula:\n" text += "Sharpshooter = (Accuracy × 0.40) + (Headshot Rate × 0.40) + (Damage per Shot × 0.20)\n" text += "Where:\n" text += "- Accuracy = Shots Hit / Shots Fired\n" text += "- Headshot Rate = Headshots / Shots Hit\n" text += "- Damage per Shot = Damage / Shots Fired\n" elif tag == "Payload Objective Specialist": text += "\nPayload Objective Formula:\n" text += "ObjPL = (Presence × 0.40) + (PushTime Normalized × 0.40) + (PSI × 0.20)\n" text += "Where:\n" text += "- Presence = Player PushTime / Team PushTime\n" text += "- PSI = Damage / (Deaths + 1) × 1 / sqrt(PushTime + 1)\n" elif tag == "Domination Objective Specialist": text += "\nDomination Objective Formula:\n" text += "ObjDOM = (CapRate × 0.40) + (CounterRate × 0.60)\n" text += "Where:\n" text += "- CapRate = log(1 + DOM_captures) / log(21)\n" text += "- CounterRate = log(1 + DOM_counters) / log(21)\n" text += "Counters are weighted more heavily because they prevent enemy scoring.\n" elif tag == "Consistency": text += "\nConsistency Formula (Hybrid Career Tag):\n" text += "Consistency = (Floor × 0.40) + (Stability × 0.40) + (Average Performance × 0.20)\n" text += "Where:\n" text += "- Floor = lowest per-match performance score\n" text += "- Stability = 1 / (1 + Adjusted Variance)\n" text += "- Adjusted Variance = Variance × (1 + 1 / Match Count)\n" text += "- Average Performance = mean per-match performance score\n" elif tag == "Clutch": text += "\nClutch Formula:\n" text += "Clutch = (Pressure Efficiency × 0.40) + (Collapse Avoidance × 0.40) + (Conversion Rate × 0.20)\n" text += "Where:\n" text += "- Pressure Efficiency = Damage / (Deaths + 1)\n" text += "- Collapse Avoidance = Player's lowest per-map performance score\n" text += "- Conversion Rate = (Accuracy × 0.50) + (Headshot Rate × 0.50)\n" elif tag == "Anchor": text += "\nAnchor Formula:\n" text += "Anchor = (Survivability × 0.45) + (DefensivePresence × 0.30) + (Consistency × 0.25)\n" text += "Where:\n" text += "- Survivability = (LeagueAvgDeathsPerMap / PlayerDeathsPerMap), clamped to [0.25, 1.50] then normalized to 0–1\n" text += "- DefensivePresence = (DamagePerMap / LeagueAvgDamagePerMap), clamped to [0, 1.5] then normalized to 0–1\n" text += "- Consistency = normalized 0–1 stability score from the Consistency tag\n" text += "Survivability reflects how hard the player is to remove; Defensive Presence measures baseline contribution; Consistency rewards stable defensive performance.\n" elif tag == "Breaker": text += "\nBreaker Formula:\n" text += "Breaker = (DamageNorm × 0.60) + (KillsNorm × 0.40)\n" text += "Where:\n" text += "- DamageNorm = Damage per map normalized against league average\n" text += "- KillsNorm = Kills per map normalized against league average\n" text += "Breaker measures disruptive offensive pressure through high damage output and aggressive fragging.\n" output.delete("1.0", tk.END) output.insert(tk.END, text) def export(): content = output.get("1.0", tk.END).strip() if not content: messagebox.showerror("Error", "No rankings text to export.") return tag = combo.get() or "rankings" filename = f"rankings_{tag}.txt" path = export_to_obs(filename, content) messagebox.showinfo("Exported", f"Rankings exported to:\n{path}") ttk.Button(win, text="Show Rankings", command=show).pack(pady=5) # dashleague_cast_tool.py -> on_rookie_leaderboard() def on_rookie_leaderboard(self): from analysis.rookie_leaderboard import rank_rookies_by_tag season = self.current_season tag = "slayer" rookies = rank_rookies_by_tag(season, tag) lines = [] lines.append(f"ROOKIE LEADERBOARD — Season {season}") lines.append("Rookies: < 50 maps and < 1000 kills.") lines.append("") # Updated Header lines.append(f"{'Rank':<5} {'Name':<18} {'Team':<12} {'Score':<10} {'KD':<6} {'Maps':<6} {'Kills':<8}") lines.append("-" * 75) for r in rookies: # Format thousands for score (e.g. 96,107) score_str = f"{int(r['score']):,}" kd_str = f"{r['kd']:.2f}" maps_str = f"{int(r['maps'])}" kills_str = f"{int(r['kills'])}" lines.append(f"{r['rank']:<5} {r['name']:<18} {r['team']:<12} {score_str:<10} {kd_str:<6} {maps_str:<6} {kills_str:<8}") text = "\n".join(lines) win = tk.Toplevel(self.root) win.title("Rookie Standings") win.geometry("800x550") output = tk.Text(win, wrap="none", font=("Courier", 10)) output.pack(expand=True, fill="both", padx=10, pady=10) output.insert(tk.END, text) output.config(state="disabled") def export_all_obs_files(self): """ Unified export function that writes all OBS text files based on the current map type and current match players. """ map_type = self.map_var.get() team_a = self.team_a_var.get() team_b = self.team_b_var.get() if not map_type or not team_a or not team_b: return # --- Extract selected players for each team --- teamA_players = [p for p in self.current_match_players if p.get("team") == team_a] teamB_players = [p for p in self.current_match_players if p.get("team") == team_b] if len(teamA_players) == 0 or len(teamB_players) == 0: print("OBS export skipped: no selected players for one or both teams.") return # Define names for identity block teamA_name = team_a teamB_name = team_b # --- STORYLINE + TEAM IDENTITY --- try: storyline_text = matchup_storyline(teamA_players, teamB_players) identity_data_a = generate_team_identity_block(teamA_players, teamA_name) identity_data_b = generate_team_identity_block(teamB_players, teamB_name) identity_text_a = format_team_identity(identity_data_a) identity_text_b = format_team_identity(identity_data_b) full_story = ( storyline_text + f"\n\nTEAM IDENTITY SUMMARY — {teamA_name}\n" + identity_text_a + f"\n\nTEAM IDENTITY SUMMARY — {teamB_name}\n" + identity_text_b ) write_text(os.path.join(OBS_EXPORT_DIR, "storyline.txt"), full_story) except Exception as e: print("Storyline export failed:", e) # --- PREDICTIONS --- try: if hasattr(self, "generate_predictions"): predictions = self.generate_predictions(teamA_players, teamB_players) write_text(os.path.join(OBS_EXPORT_DIR, "predictions.txt"), predictions) except Exception as e: print("Prediction export failed:", e) # --- PLAYER TAG EXPORTS --- try: for idx, p in enumerate(self.current_match_players): # p IS the enriched match player object tags_text = self.generate_player_tags(p, map_type) slot_path = os.path.join(PLAYERS_DIR, f"p{idx}", "Tags.txt") write_text(slot_path, tags_text) except Exception as e: print("Tag export failed:", e) print(f"OBS files updated for {map_type}") def refresh_output_dirs(self): global STATS_DIR, PLAYERS_DIR, OBS_EXPORT_DIR STATS_DIR = self.output_root PLAYERS_DIR = os.path.join(STATS_DIR, "players") OBS_EXPORT_DIR = os.path.join(STATS_DIR, "obs_exports") os.makedirs(PLAYERS_DIR, exist_ok=True) os.makedirs(OBS_EXPORT_DIR, exist_ok=True) def choose_output_folder(self): folder = filedialog.askdirectory(title="Select output folder for OBS + players") if not folder: return self.output_root = os.path.abspath(folder) config["output_root"] = self.output_root self.refresh_output_dirs() with open(CONFIG_PATH, "w", encoding="utf-8") as f: json.dump(config, f, indent=4) messagebox.showinfo( "Output Folder Set", f"Output folder set to:\n{self.output_root}\n\n" "Point OBS to this folder for players and exports." ) def on_match_prediction(self): team_a = self.team_a_var.get() team_b = self.team_b_var.get() if not team_a or not team_b: messagebox.showerror("Error", "Select both Team A and Team B.") return teamA_players = [p for p in self.current_match_players if p.get("team") == team_a] teamB_players = [p for p in self.current_match_players if p.get("team") == team_b] if not teamA_players or not teamB_players: messagebox.showerror("Error", "No players selected for one or both teams.") return prediction_text = generate_prediction(teamA_players, teamB_players, team_a, team_b) win = tk.Toplevel(self.root) win.title("Match Prediction") win.geometry("600x600") output = tk.Text(win, wrap="word") output.pack(expand=True, fill="both", padx=10, pady=10) output.insert(tk.END, prediction_text) def export(): content = output.get("1.0", tk.END).strip() if not content: messagebox.showerror("Error", "No prediction text to export.") return path = export_to_obs("match_prediction.txt", content) messagebox.showinfo("Exported", f"Prediction exported to:\n{path}") ttk.Button(win, text="Export to OBS", command=export).pack(pady=5) def on_export_obs_all(self): team_a = self.team_a_var.get() team_b = self.team_b_var.get() if not team_a or not team_b: messagebox.showerror("Error", "Select both Team A and Team B.") return teamA_players = [p for p in self.current_match_players if p.get("team") == team_a] teamB_players = [p for p in self.current_match_players if p.get("team") == team_b] if not teamA_players or not teamB_players: messagebox.showerror("Error", "No players selected for one or both teams.") return output_folder = os.path.join(os.getcwd(), "obs_output") export_all_obs(output_folder, teamA_players, teamB_players, team_a, team_b) messagebox.showinfo("Exported", f"OBS files exported to:\n{output_folder}") def main(): root = tk.Tk() app = DashLeagueGUI(root) root.mainloop() if __name__ == "__main__": main()