DL-Broadcast-Tool/dashleague_cast_tool.py

1710 lines
No EOL
68 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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("<<ComboboxSelected>>", 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("<<ComboboxSelected>>", 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("<<ListboxSelect>>", lambda e: self.enforce_rolling_five(e, "A"))
self.team_b_listbox.bind("<<ListboxSelect>>", 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("<<ComboboxSelected>>", 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)
# 1. Text display for the GUI window
lines = []
lines.append(f"ROOKIE LEADERBOARD — Season {season}")
lines.append("Rookies: < 50 maps and < 1000 kills.")
lines.append("")
lines.append(f"{'Rank':<5} {'Name':<18} {'Team':<12} {'Score':<10} {'KD':<6} {'Maps':<6} {'Kills':<8}")
lines.append("-" * 75)
for r in rookies:
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("800x600")
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")
# 2. HTML Export function for OBS Browser Source
def export_html():
html_rows = ""
for r in rookies:
html_rows += f"""
<tr>
<td class="rank">#{r['rank']}</td>
<td class="name">{r['name']}</td>
<td class="team">{r['team']}</td>
<td class="score">{int(r['score']):,}</td>
<td class="kd">{r['kd']:.2f}</td>
<td class="kills">{int(r['kills'])}</td>
<td class="maps">{int(r['maps'])}</td>
</tr>
"""
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<style>
body {{
background-color: rgba(10, 10, 15, 0.95);
color: #ffffff;
font-family: 'Segoe UI', Arial, sans-serif;
margin: 30px;
padding: 20px;
border-radius: 12px;
border: 2px solid #0d86f4;
}}
h1 {{
text-align: center;
color: #0d86f4;
font-size: 32px;
margin-bottom: 5px;
text-transform: uppercase;
letter-spacing: 3px;
}}
.subtitle {{
text-align: center;
color: #aaaaaa;
font-size: 16px;
margin-bottom: 30px;
}}
table {{
width: 100%;
border-collapse: collapse;
font-size: 20px;
}}
th {{
border-bottom: 4px solid #f40d35;
padding: 15px;
text-align: left;
color: #aaaaaa;
font-weight: bold;
text-transform: uppercase;
}}
td {{
padding: 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}}
.rank {{ font-weight: bold; color: #0d86f4; width: 8%; }}
.name {{ font-weight: bold; width: 25%; }}
.team {{ color: #aaaaaa; width: 17%; }}
.score {{ text-align: right; width: 15%; font-family: monospace; font-weight: bold; }}
.kd {{ text-align: right; width: 10%; font-family: monospace; }}
.kills {{ text-align: right; width: 10%; font-family: monospace; color: #f40d35; }}
.maps {{ text-align: right; width: 15%; font-family: monospace; }}
</style>
</head>
<body>
<h1>Rookie Leaderboard</h1>
<div class="subtitle">Season {season} | &lt; 50 maps and &lt; 1000 kills</div>
<table>
<tr>
<th class="rank">Rank</th>
<th class="name">Name</th>
<th class="team">Team</th>
<th class="score">Score</th>
<th class="kd">KD</th>
<th class="kills">Kills</th>
<th class="maps">Maps</th>
</tr>
{html_rows}
</table>
</body>
</html>
"""
# Save to exports folder
html_path = os.path.join(OBS_EXPORT_DIR, "rookie_leaderboard.html")
with open(html_path, "w", encoding="utf-8") as f:
f.write(html_content)
self.set_status("Exported Rookie HTML (incl. Kills)")
messagebox.showinfo("Exported", f"HTML updated\nLocation: {html_path}")
# Add button to pop-up window
ttk.Button(win, text="Export HTML for OBS Browser Source", command=export_html).pack(pady=10)
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()