2026-04-15 06:15:07 +00:00
|
|
|
|
from utils.safe_tag import safe_pct, safe_tier, safe_avg
|
|
|
|
|
|
|
2026-04-21 08:55:26 +00:00
|
|
|
|
# Caster‑friendly display names
|
|
|
|
|
|
DISPLAY_NAMES = {
|
|
|
|
|
|
"slayer": "Slayer",
|
|
|
|
|
|
"objective_payload": "Payload Specialist",
|
2026-09-29 00:59:21 +00:00
|
|
|
|
"objective_domination": "Domination Specialist",
|
2026-04-21 08:55:26 +00:00
|
|
|
|
"sharpshooter": "Sharpshooter",
|
|
|
|
|
|
"consistency": "Consistency",
|
|
|
|
|
|
"clutch": "Clutch",
|
|
|
|
|
|
"anchor": "Anchor Specialist",
|
|
|
|
|
|
"breaker": "Breaker",
|
|
|
|
|
|
}
|
2026-04-15 06:15:07 +00:00
|
|
|
|
|
|
|
|
|
|
|
2026-09-29 00:59:21 +00:00
|
|
|
|
def summarize_team_tags(team_players, map_type="Payload"):
|
2026-04-10 08:08:47 +00:00
|
|
|
|
"""
|
|
|
|
|
|
Aggregates tag tiers + percentiles for a team.
|
2026-09-29 00:59:21 +00:00
|
|
|
|
Only the objective tag for the mode actually being played (map_type)
|
|
|
|
|
|
is included — not both Payload and Domination — since the other one
|
|
|
|
|
|
isn't relevant to this match.
|
2026-04-10 08:08:47 +00:00
|
|
|
|
Returns a dict with:
|
|
|
|
|
|
- avg_pct
|
|
|
|
|
|
- top_tier
|
|
|
|
|
|
- tier counts
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2026-09-29 00:59:21 +00:00
|
|
|
|
objective_tag = "objective_domination" if map_type == "Domination" else "objective_payload"
|
|
|
|
|
|
|
|
|
|
|
|
tags = ["slayer", objective_tag, "sharpshooter",
|
2026-04-21 08:55:26 +00:00
|
|
|
|
"consistency", "clutch", "anchor", "breaker"]
|
|
|
|
|
|
|
2026-04-10 08:08:47 +00:00
|
|
|
|
summary = {}
|
|
|
|
|
|
|
|
|
|
|
|
for tag in tags:
|
|
|
|
|
|
pcts = []
|
|
|
|
|
|
tiers = []
|
|
|
|
|
|
|
|
|
|
|
|
for p in team_players:
|
|
|
|
|
|
tag_data = p.get(tag, {})
|
2026-04-15 06:15:07 +00:00
|
|
|
|
pcts.append(safe_pct(tag_data.get("pct")))
|
2026-04-21 08:55:26 +00:00
|
|
|
|
tiers.append(safe_tier(tag_data.get("tier"))) # guaranteed valid tier
|
2026-04-10 08:08:47 +00:00
|
|
|
|
|
|
|
|
|
|
summary[tag] = {
|
2026-04-21 08:55:26 +00:00
|
|
|
|
"avg_pct": safe_avg(pcts), # guaranteed numeric
|
2026-04-10 08:08:47 +00:00
|
|
|
|
"top_tier": max(tiers, key=lambda t: "SABCD".index(t)),
|
|
|
|
|
|
"count_S": tiers.count("S"),
|
|
|
|
|
|
"count_A": tiers.count("A"),
|
|
|
|
|
|
"count_B": tiers.count("B"),
|
|
|
|
|
|
"count_C": tiers.count("C"),
|
|
|
|
|
|
"count_D": tiers.count("D"),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return summary
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-29 00:59:21 +00:00
|
|
|
|
def classify_team_style(summary, map_type="Payload"):
|
2026-04-10 08:08:47 +00:00
|
|
|
|
"""
|
|
|
|
|
|
Produces a high-level team style classification based on tag strengths.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2026-09-29 00:59:21 +00:00
|
|
|
|
objective_tag = "objective_domination" if map_type == "Domination" else "objective_payload"
|
|
|
|
|
|
objective_label = "Objective-focused"
|
|
|
|
|
|
|
2026-04-10 08:08:47 +00:00
|
|
|
|
slayer = summary["slayer"]["avg_pct"]
|
2026-09-29 00:59:21 +00:00
|
|
|
|
objective = summary[objective_tag]["avg_pct"]
|
2026-04-10 08:08:47 +00:00
|
|
|
|
sharpshooter = summary["sharpshooter"]["avg_pct"]
|
|
|
|
|
|
consistency = summary["consistency"]["avg_pct"]
|
|
|
|
|
|
clutch = summary["clutch"]["avg_pct"]
|
2026-04-21 08:55:26 +00:00
|
|
|
|
anchor = summary["anchor"]["avg_pct"]
|
2026-04-10 08:08:47 +00:00
|
|
|
|
|
2026-04-21 08:55:26 +00:00
|
|
|
|
# Primary style selection
|
2026-04-10 08:08:47 +00:00
|
|
|
|
primary = max(
|
|
|
|
|
|
{
|
|
|
|
|
|
"Slayer-heavy": slayer,
|
2026-09-29 00:59:21 +00:00
|
|
|
|
objective_label: objective,
|
2026-04-10 08:08:47 +00:00
|
|
|
|
"Precision/Sharpshooter": sharpshooter,
|
|
|
|
|
|
"Stable/Consistent": consistency,
|
|
|
|
|
|
"High-pressure/Clutch": clutch,
|
2026-04-21 08:55:26 +00:00
|
|
|
|
"Anchor/Support": anchor,
|
2026-04-10 08:08:47 +00:00
|
|
|
|
}.items(),
|
|
|
|
|
|
key=lambda x: x[1]
|
|
|
|
|
|
)[0]
|
|
|
|
|
|
|
2026-04-21 08:55:26 +00:00
|
|
|
|
# Weakness detection
|
2026-04-10 08:08:47 +00:00
|
|
|
|
weaknesses = []
|
|
|
|
|
|
if slayer < 40:
|
|
|
|
|
|
weaknesses.append("low elimination pressure")
|
2026-09-29 00:59:21 +00:00
|
|
|
|
if objective < 40:
|
2026-04-10 08:08:47 +00:00
|
|
|
|
weaknesses.append("weak objective presence")
|
|
|
|
|
|
if sharpshooter < 40:
|
|
|
|
|
|
weaknesses.append("below-average accuracy")
|
|
|
|
|
|
if consistency < 40:
|
|
|
|
|
|
weaknesses.append("inconsistent match-to-match output")
|
|
|
|
|
|
if clutch < 40:
|
|
|
|
|
|
weaknesses.append("poor high-pressure performance")
|
2026-04-21 08:55:26 +00:00
|
|
|
|
if anchor < 40:
|
|
|
|
|
|
weaknesses.append("weak defensive backbone / low anchor stability")
|
2026-04-10 08:08:47 +00:00
|
|
|
|
|
|
|
|
|
|
return primary, weaknesses
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-29 00:59:21 +00:00
|
|
|
|
def generate_team_identity_block(team_players, team_name="Team", map_type="Payload"):
|
2026-04-10 08:08:47 +00:00
|
|
|
|
"""
|
|
|
|
|
|
Returns a structured identity block dict for a team.
|
|
|
|
|
|
GUI or OBS formatter will convert this into text.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2026-09-29 00:59:21 +00:00
|
|
|
|
summary = summarize_team_tags(team_players, map_type)
|
|
|
|
|
|
primary_style, weaknesses = classify_team_style(summary, map_type)
|
2026-04-10 08:08:47 +00:00
|
|
|
|
|
2026-04-21 08:55:26 +00:00
|
|
|
|
# Strengths list
|
2026-04-10 08:08:47 +00:00
|
|
|
|
strengths = []
|
|
|
|
|
|
for tag, data in summary.items():
|
|
|
|
|
|
if data["avg_pct"] >= 60:
|
2026-04-21 08:55:26 +00:00
|
|
|
|
display = DISPLAY_NAMES.get(tag, tag.replace("_", " ").title())
|
2026-04-10 08:08:47 +00:00
|
|
|
|
strengths.append(
|
2026-04-21 08:55:26 +00:00
|
|
|
|
f"Strong {display} ({data['top_tier']} Tier)"
|
2026-04-10 08:08:47 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-04-21 08:55:26 +00:00
|
|
|
|
# Tag breakdown
|
2026-04-10 08:08:47 +00:00
|
|
|
|
tag_block = {}
|
|
|
|
|
|
for tag, data in summary.items():
|
|
|
|
|
|
tag_block[tag] = {
|
|
|
|
|
|
"avg_pct": data["avg_pct"],
|
|
|
|
|
|
"tier": data["top_tier"],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"team_name": team_name,
|
|
|
|
|
|
"team_style": primary_style,
|
|
|
|
|
|
"strengths": strengths,
|
|
|
|
|
|
"weaknesses": weaknesses,
|
|
|
|
|
|
"tags": tag_block,
|
|
|
|
|
|
}
|
2026-04-21 08:55:26 +00:00
|
|
|
|
|
|
|
|
|
|
|
2026-04-10 08:08:47 +00:00
|
|
|
|
def format_team_identity(identity_data):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Converts the identity block dict into a readable text block.
|
|
|
|
|
|
Used by GUI and OBS exporters.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
lines = []
|
|
|
|
|
|
|
|
|
|
|
|
lines.append(f"Team: {identity_data['team_name']}")
|
|
|
|
|
|
lines.append(f"Playstyle: {identity_data['team_style']}")
|
|
|
|
|
|
lines.append("")
|
|
|
|
|
|
|
|
|
|
|
|
# Strengths
|
2026-04-21 08:55:26 +00:00
|
|
|
|
lines.append("Strengths:")
|
2026-04-10 08:08:47 +00:00
|
|
|
|
if identity_data["strengths"]:
|
|
|
|
|
|
for s in identity_data["strengths"]:
|
|
|
|
|
|
lines.append(f" - {s}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
lines.append(" - None identified")
|
|
|
|
|
|
|
|
|
|
|
|
lines.append("")
|
|
|
|
|
|
|
|
|
|
|
|
# Weaknesses
|
2026-04-21 08:55:26 +00:00
|
|
|
|
lines.append("Weaknesses:")
|
2026-04-10 08:08:47 +00:00
|
|
|
|
if identity_data["weaknesses"]:
|
|
|
|
|
|
for w in identity_data["weaknesses"]:
|
|
|
|
|
|
lines.append(f" - {w}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
lines.append(" - None identified")
|
|
|
|
|
|
|
|
|
|
|
|
lines.append("")
|
|
|
|
|
|
lines.append("Tag Breakdown:")
|
|
|
|
|
|
|
|
|
|
|
|
for tag, data in identity_data["tags"].items():
|
2026-04-21 08:55:26 +00:00
|
|
|
|
name = DISPLAY_NAMES.get(tag, tag.replace("_", " ").title())
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
f" - {name}: {data['tier']} Tier (avg {data['avg_pct']:.1f} percentile)"
|
|
|
|
|
|
)
|
2026-04-10 08:08:47 +00:00
|
|
|
|
|
|
|
|
|
|
return "\n".join(lines)
|