"""Markdown report helpers for TL DPS analyzer outputs."""
from __future__ import annotations
from statistics import fmean, median
from typing import Dict, Iterable, List
Payload = Dict[str, object]
def _fmt_int(value: int | float) -> str:
return f"{int(round(value)):,}"
def _fmt_float(value: float, decimals: int = 1) -> str:
return f"{value:.{decimals}f}"
def _mean(values: Iterable[float]) -> float:
values = list(values)
return fmean(values) if values else 0.0
def _median(values: Iterable[float]) -> float:
values = list(values)
return median(values) if values else 0.0
def _build_aggregate_section(runs: List[Dict[str, object]], summary: Dict[str, object]) -> List[str]:
run_dps = [float(run.get("dps", 0.0)) for run in runs]
mean_dps = float(summary.get("mean_dps", _mean(run_dps))) if summary else _mean(run_dps)
median_dps = float(summary.get("median_dps", _median(run_dps))) if summary else _median(run_dps)
mean_dpm = float(summary.get("mean_dpm", mean_dps * 60.0))
median_dpm = float(summary.get("median_dpm", median_dps * 60.0))
total_runs = int(summary.get("total_runs", len(runs)))
total_damage = int(summary.get("total_damage", 0))
lines = ["## Aggregate Summary", "", "| Metric | Value |", "| --- | --- |"]
lines.append(f"| Runs | {total_runs} |")
lines.append(f"| Total Damage | {_fmt_int(total_damage)} |")
lines.append(f"| Mean DPS | {_fmt_float(mean_dps)} |")
lines.append(f"| Median DPS | {_fmt_float(median_dps)} |")
lines.append(f"| Mean DPM | {_fmt_float(mean_dpm)} |")
lines.append(f"| Median DPM | {_fmt_float(median_dpm)} |")
lines.append("")
return lines
def _build_run_table(runs: List[Dict[str, object]]) -> List[str]:
lines = ["## Per-Run Overview", "", "| Run | Duration (s) | DPS | DPM | Hits | Crit % | Heavy % |", "| --- | --- | --- | --- | --- | --- | --- |"]
if not runs:
lines.append("| _ | _ | _ | _ | _ | _ | _ |")
else:
for run in runs:
duration = float(run.get("duration_seconds", 0.0))
hits = int(run.get("total_hits", 0))
crit = float(run.get("crit_rate_pct", 0.0))
heavy = float(run.get("heavy_rate_pct", 0.0))
lines.append(
"| {run_id} | {duration:.2f} | {dps} | {dpm} | {hits} | {crit:.1f}% | {heavy:.1f}% |".format(
run_id=run.get("run_id", "?"),
duration=duration,
dps=_fmt_float(float(run.get("dps", 0.0))),
dpm=_fmt_float(float(run.get("dpm", 0.0))),
hits=_fmt_int(hits),
crit=crit,
heavy=heavy,
)
)
lines.append("")
return lines
def _build_top_skills_table(summary: Dict[str, object]) -> List[str]:
total_damage = int(summary.get("total_damage", 0))
skills = summary.get("top_skills_by_damage") or summary.get("top_skills") or []
lines = ["## Top Skills (by total damage)", "", "| Skill | Total Damage | Damage Share % | Avg Crit % |", "| --- | --- | --- | --- |"]
if not skills:
lines.append("| _ | _ | _ | _ |")
else:
for skill in skills:
damage = int(skill.get("total_damage", 0))
share = (damage / total_damage * 100.0) if total_damage else 0.0
crit = float(skill.get("crit_rate_pct", 0.0))
lines.append(
f"| {skill.get('skill', '?')} | {_fmt_int(damage)} | {_fmt_float(share)}% | {_fmt_float(crit)}% |"
)
lines.append("")
return lines
def _build_chatbot_prompts(summary: Dict[str, object], runs: List[Dict[str, object]]) -> List[str]:
mean_dps = _mean(float(run.get("dps", 0.0)) for run in runs)
overall_crit = float(summary.get("overall_crit_rate_pct", 0.0))
skills = summary.get("top_skills_by_damage") or summary.get("top_skills") or []
top_skill_name = skills[0].get("skill") if skills else "your highest-damage skill"
top_skill_damage = _fmt_int(int(skills[0].get("total_damage", 0))) if skills else "that skill"
prompts = [
f"What rotational tweaks would push average DPS beyond {_fmt_float(mean_dps)}?",
f"How can I raise overall crit rate above {overall_crit:.1f}%?",
f"Which cooldown windows keep {top_skill_name} contributing around {top_skill_damage} damage per run?",
]
lines = ["## Chatbot Prompts", ""]
lines.extend(f"- {prompt}" for prompt in prompts)
lines.append("")
return lines
def build_markdown_report(payload: Payload) -> str:
runs = payload.get("runs", []) or []
summary = payload.get("summary", {}) or {}
runs_list: List[Dict[str, object]] = [dict(run) for run in runs] # shallow copy for safety
summary_dict: Dict[str, object] = dict(summary)
lines: List[str] = ["# TL DPS Report", ""]
generated_at = payload.get("generated_at")
source = payload.get("source")
if generated_at or source:
lines.append(
f"Generated {generated_at or 'n/a'} from `{source or 'n/a'}`"
)
lines.append("")
lines.extend(_build_aggregate_section(runs_list, summary_dict))
lines.extend(_build_run_table(runs_list))
lines.extend(_build_top_skills_table(summary_dict))
lines.extend(_build_chatbot_prompts(summary_dict, runs_list))
return "\n".join(lines).strip() + "\n"