#!/usr/bin/env python3
"""Build a focused review for actor +0x62 battle result flag lifecycle.

The earlier status-transition report established actor +0x2a as the
mode/status id and identified several +0x62 bits.  This report narrows the
life cycle of those bits: which ones survive a hit unit, which ones are
per-hit result flags, and which routines consume/reset them.
"""
from __future__ import annotations

import html
import json
import re
import subprocess
from pathlib import Path
from typing import Any


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
EXE = ROOT / "Hwanse2.exe"


def esc(value: Any) -> str:
    return html.escape(str(value if value is not None else ""))


def hx(value: int) -> str:
    return f"0x{value:08x}"


def disassemble(start_va: int, stop_va: int) -> str:
    try:
        result = subprocess.run(
            [
                "objdump",
                "-Mintel",
                "-D",
                "-b",
                "pei-i386",
                f"--start-address=0x{start_va:08x}",
                f"--stop-address=0x{stop_va:08x}",
                str(EXE),
            ],
            check=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        )
    except (OSError, subprocess.CalledProcessError) as exc:
        return f"; disassembly unavailable: {exc}"
    return "\n".join(line for line in result.stdout.splitlines() if re.match(r"\s*[0-9a-f]{6,8}:", line))


def compact_lines(disasm: str, needles: list[str], *, context: int = 0) -> list[str]:
    lines = disasm.splitlines()
    picked: list[tuple[int, str]] = []
    for index, line in enumerate(lines):
        if any(needle in line for needle in needles):
            for i in range(max(0, index - context), min(len(lines), index + context + 1)):
                picked.append((i, lines[i]))
    seen: set[int] = set()
    result: list[str] = []
    for index, line in picked:
        if index not in seen:
            seen.add(index)
            result.append(line)
    return result


FLAG_ROWS = [
    {
        "bit": "0x01",
        "scope": "long-lived, one-turn",
        "name": "one-turn action/display latch",
        "status": "확정",
        "producer": "status results 1..3 and paralysis recovery set this as a temporary action-block marker.",
        "consumer": "0x40d95b battle-order cleanup restores +0x2a from +0x66, clears +0x64, then clears bit 0x01.",
        "engineMeaning": "넘어짐/휙 날아감/행동정지 같은 1턴 행동불가 결과를 다음 battle-order pass까지 유지한다.",
        "open": "UI label은 상태 id(+0x2a)가 담당한다. bit 0x01 자체는 표시명이 아니라 1턴 action/display latch다.",
    },
    {
        "bit": "0x02",
        "scope": "long-lived, timed",
        "name": "timed command/action lock",
        "status": "확정",
        "producer": "마비/졸림/푸쉬/곤드레만드레 계열이 +0x64 timer와 함께 세운다.",
        "consumer": "0x43512e decrements/checks +0x64, restores +0x2a from +0x66, clears +0x64 and bit 0x02 when recovery passes.",
        "engineMeaning": "action script gate 0x40db36 tests 0x83, so 0x02 actors skip/jump command flow while locked.",
        "open": "UI label은 상태 id(+0x2a)가 담당한다. bit 0x02 자체는 +0x64 timer/random recovery가 해제하는 command/action lock이다.",
    },
    {
        "bit": "0x04",
        "scope": "battle-flow request, per-hit reset",
        "name": "family 1 flow fan-out / escape marker",
        "status": "고신뢰",
        "producer": "result-family table 0x546970 index 1 calls 0x43485e, which ORs bit 0x04. Current extracted action records use family 1 only for 도주.",
        "consumer": "0x40dcf7 tests bit 0x04 and marks all eligible actors or the current actor with +0x5b=1, +0x5c=3, +0x60=6, then sets global 0x59e334=2. Hit-unit reset clears it with &= 0xc3.",
        "engineMeaning": "타격/크리티컬/상태 플래그가 아니라 도주 계열에서 battle-flow/display fan-out을 요청하는 마커다.",
        "open": "+0x5b/+0x5c/+0x60의 필드 역할은 battle_actor_display_state_review에서 각각 activation latch / display bitmask / phase selector로 확정됐다. 이 흐름에서는 +0x5c=0x3, +0x60=6인 도주 fan-out phase다.",
    },
    {
        "bit": "0x08",
        "scope": "per-hit result",
        "name": "guard/glancing chip-damage branch",
        "status": "확정",
        "producer": "0x434218 hit/avoid branch sets target bit 0x08 when avoid beats hit but not enough for full miss.",
        "consumer": "0x43414d turns it into random 1..3 damage and clears target +0x6c status effect id. Normal result sound/effect path also checks target 0x28.",
        "engineMeaning": "MISS는 아니지만 일반 damage formula 대신 1..3의 가드/스침성 chip damage로 처리되는 결과.",
        "open": "target result script 0x004545b4가 WLK id 11, helper 0x16(dec 22), btl_efc F56..F59, target frame 0을 선택한다. 사용자 관찰처럼 MISS 라벨 없이 normal frame에 1자리수 데미지와 약한 '팅' 계열 효과음만 뜬다.",
    },
    {
        "bit": "0x10",
        "scope": "per-hit result",
        "name": "critical presentation / alternate damage branch",
        "status": "확정",
        "producer": "0x4343db luck-side comparison and random threshold set attacker bit 0x10.",
        "consumer": "0x43414d selects alternate damage formula, 0x434737 may clear it if damage is too low, and 0x40ef1b consumes it for alternate sound/effect then clears it.",
        "engineMeaning": "크리티컬 presentation에 대응하는 1타성 결과 플래그. 일반 타격과 다른 데미지식/결과음/섬광성 이펙트 선택에 쓰인다.",
        "open": "전용 result sound/helper 경로, WLK id 14 선택, palette flash/fade child script는 확정됐다. 남은 것은 브라우저 러너에서 VM tick을 wall-clock ms로 환산하는 미세 타이밍이다.",
    },
    {
        "bit": "0x20",
        "scope": "per-hit result",
        "name": "full miss / HP-apply skip",
        "status": "고신뢰",
        "producer": "0x434218 hit/avoid branch sets target bit 0x20 when avoid greatly exceeds hit.",
        "consumer": "0x4340ae immediately returns before HP damage/recovery apply. Normal result sound/effect path also checks target 0x28.",
        "engineMeaning": "결과 적용 자체를 건너뛰는 완전 회피/미스 계열.",
        "open": "MISS 렌더링/사운드가 별도 VM object에서 붙는 지점은 별도 display layer 추적 필요.",
    },
    {
        "bit": "0x40",
        "scope": "long-lived battle state",
        "name": "HP zero / knockout",
        "status": "확정",
        "producer": "0x4353f8 HP damage apply sets bit 0x40 when current HP reaches zero.",
        "consumer": "0x434e84 maps it to +0x2a status id 27 기절. Full reset 0x4354eb preserves 0x40 with &= 0xc0. Refresh water 0x43583c and beast stone 0x43590d overwrite +0x62=0x01 when target has 0x80, clearing 0x40/0x80 together.",
        "engineMeaning": "기절/전투불능 상태. HP0 후 표시/비활성 phase에서 0x80과 결합되고, 부활 소모품에서 active one-turn flag 0x01로 재초기화된다.",
        "open": "전투 종료 후 전체 party 회복/여관/세이브포인트가 같은 helper를 쓰는지는 별도 추적 필요.",
    },
    {
        "bit": "0x80",
        "scope": "long-lived actor/display eligibility",
        "name": "display wait skip / target-excluded actor",
        "status": "고신뢰",
        "producer": "phase 4 display transition at 0x40d57f/0x40d61d sets bit 0x80 after scheduling +0x5b=1, +0x5c=0x20, +0x60=4. Script byte OR opcode can also propagate 0x80 to actor sets.",
        "consumer": "battle order, target picker, action script loops, display wait loop, and HP recovery result display skip actors with bit 0x80. Refresh water 0x43583c and beast stone 0x43590d clear it by overwriting +0x62=0x01. Hit/full resets preserve it.",
        "engineMeaning": "대상 선택/행동 순서 제외뿐 아니라 display wait 종료에도 직접 쓰이는 actor skip/completed bit. 부활 소모품에서는 비활성/기절 actor를 다시 active actor로 되돌리는 gate로 쓰인다.",
        "open": "소모품이 아닌 actor 생성/전투 종료/scene 전환에서 0x80을 재초기화하는 루틴은 추가 추적 대상이다.",
    },
]


RESET_ROWS = [
    {
        "va": 0x00433F0E,
        "routine": "hit-unit setup",
        "operation": "actor+0x62 &= 0xc3",
        "keeps": "0x01, 0x02, 0x40, 0x80",
        "clears": "0x04, 0x08, 0x10, 0x20",
        "meaning": "매 hit unit 시작마다 per-hit/result 비트만 지운다. 장기 상태/대상 제외 플래그는 유지한다.",
    },
    {
        "va": 0x0040D95B,
        "routine": "battle-order one-turn cleanup",
        "operation": "if +0x62&0x01: restore +0x2a, clear +0x66/+0x64, +0x62 &= 0xfe",
        "keeps": "all except 0x01",
        "clears": "0x01",
        "meaning": "1턴 행동불가/결과 pose를 다음 순회에서 해제한다.",
    },
    {
        "va": 0x0043512E,
        "routine": "timed status recovery",
        "operation": "timer/random check, restore +0x2a, clear +0x66/+0x64, +0x62 &= 0xfd",
        "keeps": "all except 0x02",
        "clears": "0x02",
        "meaning": "마비/졸림/고유 모드 같은 timer 상태를 해제한다.",
    },
    {
        "va": 0x004354EB,
        "routine": "full status/control reset",
        "operation": "restore +0x2a, clear +0x66/+0x64/+0x69, +0x62 &= 0xc0",
        "keeps": "0x40, 0x80",
        "clears": "0x01, 0x02, 0x04, 0x08, 0x10, 0x20",
        "meaning": "상태 제어 플래그는 초기화하되 기절/비활성 actor 상태는 보존한다.",
    },
    {
        "va": 0x0043583C,
        "routine": "revive consumable reset",
        "operation": "if +0x62&0x80: restore +0x2a from +0x66, clear +0x66/+0x64, +0x62 = 0x01",
        "keeps": "none of previous high flags",
        "clears": "0x40, 0x80, 0x02 and all per-hit flags by overwrite",
        "meaning": "리프레시 워터와 마수석의 공통 부활 전처리. 0x40/0x80을 지운 뒤 one-turn display/result flag 0x01만 세운다.",
    },
    {
        "va": 0x0040EF1B,
        "routine": "alternate result sound/effect consumer",
        "operation": "if +0x62&0x10: play alternate path, +0x62 &= 0xef",
        "keeps": "all except 0x10",
        "clears": "0x10",
        "meaning": "critical-like 결과 사운드/이펙트를 1회 소비한 뒤 per-hit alternate flag를 지운다.",
    },
]


EVIDENCE_SPECS = [
    {
        "label": "hit-unit setup preserves long flags and clears per-hit flags",
        "range": (0x00433F0E, 0x00433F38),
        "needles": ["[eax+0x62]", "and    cl,0xc3"],
        "context": 1,
    },
    {
        "label": "hit/avoid produces 0x20 full miss or 0x08 guard/glancing branch",
        "range": (0x00434218, 0x004343DB),
        "needles": ["or     cl,0x20", "or     cl,0x8"],
        "context": 4,
    },
    {
        "label": "alternate/critical flag gate sets 0x10",
        "range": (0x004343DB, 0x004344AB),
        "needles": ["cmp    eax,0x96", "or     cl,0x10"],
        "context": 4,
    },
    {
        "label": "result dispatcher skips HP apply on 0x20 and handles HP/recovery modes",
        "range": (0x004340AE, 0x0043414D),
        "needles": ["test   cl,0x20", "call   0x4353f8", "call   0x434e84", "call   0x435295"],
        "context": 2,
    },
    {
        "label": "alternate damage may clear 0x10 when damage falls below threshold",
        "range": (0x00434737, 0x0043485E),
        "needles": ["cmp    eax,0x3", "and    cl,0xef"],
        "context": 4,
    },
    {
        "label": "bit 0x04 producer candidate",
        "range": (0x0043485E, 0x0043487A),
        "needles": ["or     cl,0x4"],
        "context": 4,
    },
    {
        "label": "one-turn 0x01 cleanup restores prior mode and clears only bit 0x01",
        "range": (0x0040D95B, 0x0040DA00),
        "needles": ["test   cl,0x1", "[eax+0x66]", "[ecx+0x2a]", "[eax+0x64]", "and    cl,0xfe"],
        "context": 1,
    },
    {
        "label": "action script gate skips actors with 0x01/0x02/0x80",
        "range": (0x0040DB36, 0x0040DB8C),
        "needles": ["test   cl,0x83"],
        "context": 5,
    },
    {
        "label": "display wait loop skips actors with 0x80",
        "range": (0x0040E0C2, 0x0040E150),
        "needles": ["0x4576e8", "[eax+0x62]", "test   cl,0x80", "0x42177f", "or     DWORD PTR [ebp-0x4],ecx"],
        "context": 4,
    },
    {
        "label": "action script can OR arbitrary flag byte into +0x62",
        "range": (0x0040DB8C, 0x0040DC65),
        "needles": ["or     cl,BYTE PTR [eax+0x2]", "BYTE PTR [eax+0x62]"],
        "context": 3,
    },
    {
        "label": "phase 4 display transition sets 0x80 after scheduling display state",
        "range": (0x0040D520, 0x0040D640),
        "needles": ["[eax+0x5b]", "[eax+0x5c]", "[eax+0x60]", "or     cl,0x80"],
        "context": 3,
    },
    {
        "label": "timed 0x02 recovery restores prior mode and clears bit 0x02",
        "range": (0x0043512E, 0x004351DA),
        "needles": ["[eax+0x64]", "[eax+0x66]", "[ecx+0x2a]", "and    cl,0xfd"],
        "context": 1,
    },
    {
        "label": "HP zero sets 0x40 and full reset preserves only 0x40/0x80",
        "range": (0x004353F8, 0x00435538),
        "needles": ["or     cl,0x40", "and    cl,0xc0", "[eax+0x69]"],
        "context": 4,
    },
    {
        "label": "revive consumables clear 0x40/0x80 by overwriting +0x62 with 0x01",
        "range": (0x0043583C, 0x00435992),
        "needles": ["test   cl,0x80", "BYTE PTR [eax+0x62],0x1", "0xc8", "[eax+0xa]", "[eax+0x10]"],
        "context": 4,
    },
    {
        "label": "alternate sound/effect consumes 0x10, normal branch checks target 0x28",
        "range": (0x0040EF16, 0x0040F01E),
        "needles": ["test   cl,0x10", "and    cl,0xef", "test   cl,0x28"],
        "context": 4,
    },
    {
        "label": "bit 0x04 consumer/fan-out candidate",
        "range": (0x0040DCF7, 0x0040DD86),
        "needles": ["test   cl,0x4", "[eax+0x5b]", "[eax+0x5c]", "[eax+0x60]", "test   cl,0x80"],
        "context": 3,
    },
]


def build_data() -> dict[str, Any]:
    evidence: list[dict[str, Any]] = []
    for spec in EVIDENCE_SPECS:
        start, stop = spec["range"]
        disasm = disassemble(start, stop)
        evidence.append(
            {
                "label": spec["label"],
                "startVa": start,
                "stopVa": stop,
                "startVaHex": hx(start),
                "stopVaHex": hx(stop),
                "lines": compact_lines(disasm, spec["needles"], context=spec.get("context", 0)),
            }
        )
    return {
        "title": "전투 결과 플래그 생명주기",
        "actorField": "+0x62",
        "summary": [
            "actor +0x62는 상태이상 전용 필드가 아니라 long-lived actor flags와 per-hit result flags가 섞인 바이트다.",
            "hit-unit setup의 &=0xc3 마스크가 핵심이다. 0x01/0x02/0x40/0x80은 보존되고, 0x04/0x08/0x10/0x20은 매 hit마다 지워진다.",
            "따라서 전투 러너/엔진에서는 0x08/0x10/0x20을 데미지/사운드/결과 표시의 1타성 플래그로 취급하고, 0x01/0x02/0x40/0x80은 actor 상태 생명주기로 별도 관리해야 한다.",
            "0x10은 MISS처럼 텍스트를 띄우는 경로가 아니라 alternate damage, 전용 결과음/helper, 섬광성 presentation으로 소비되는 critical branch로 승격됐다.",
            "bit 0x10 critical branch는 정적 역할, WLK id 14 runtime 선택, palette flash/fade child script까지 확정됐다. 남은 wall-clock ms 환산은 EXE 의미가 아니라 browser runner timing 보정이다.",
            "0x80은 target/order 제외뿐 아니라 display wait loop의 완료/스킵 조건으로도 쓰인다.",
            "리프레시 워터와 마수석은 0x80 actor를 대상으로 +0x62=0x01을 써서 기절 0x40과 비활성/스킵 0x80을 함께 해제한다.",
        ],
        "flags": FLAG_ROWS,
        "resets": RESET_ROWS,
        "evidence": evidence,
        "openQuestions": [],
    }


def status_class(status: str) -> str:
    if status == "확정":
        return "good"
    if status == "고신뢰":
        return "warn"
    return "candidate"


def render_html(data: dict[str, Any]) -> str:
    flag_rows = "\n".join(
        f"""
        <tr>
          <td class="mono">{esc(row['bit'])}</td>
          <td>{esc(row['scope'])}</td>
          <td><strong>{esc(row['name'])}</strong><br><span class="muted">{esc(row['engineMeaning'])}</span></td>
          <td><span class="tag {status_class(row['status'])}">{esc(row['status'])}</span></td>
          <td>{esc(row['producer'])}</td>
          <td>{esc(row['consumer'])}</td>
          <td>{esc(row['open'])}</td>
        </tr>"""
        for row in data["flags"]
    )
    reset_rows = "\n".join(
        f"""
        <tr>
          <td class="mono">{esc(hx(row['va']))}</td>
          <td>{esc(row['routine'])}</td>
          <td class="mono">{esc(row['operation'])}</td>
          <td>{esc(row['keeps'])}</td>
          <td>{esc(row['clears'])}</td>
          <td>{esc(row['meaning'])}</td>
        </tr>"""
        for row in data["resets"]
    )
    evidence_blocks = "\n".join(
        f"""
        <details>
          <summary>{esc(item['label'])} <span class="mono">{esc(item['startVaHex'])}..{esc(item['stopVaHex'])}</span></summary>
          <pre>{esc(chr(10).join(item['lines']) if item['lines'] else '; no compact evidence lines selected')}</pre>
        </details>"""
        for item in data["evidence"]
    )
    summary_items = "\n".join(f"<li>{esc(line)}</li>" for line in data["summary"])
    open_items = "\n".join(f"<li>{esc(line)}</li>" for line in data["openQuestions"])
    return f"""<!doctype html>
<html lang="ko">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>{esc(data['title'])}</title>
    <style>
      :root {{
        --bg:#f6f7f9;
        --panel:#ffffff;
        --line:#d8dde6;
        --text:#1d2733;
        --muted:#687487;
        --head:#eef2f7;
        --good:#15803d;
        --warn:#b7791f;
        --candidate:#4f46e5;
      }}
      body {{ margin:0; background:var(--bg); color:var(--text); font-family:system-ui,-apple-system,Segoe UI,sans-serif; }}
      main {{ max-width:1280px; margin:0 auto; padding:24px; }}
      header, section {{ background:var(--panel); border:1px solid var(--line); border-radius:8px; margin-bottom:16px; }}
      header {{ padding:20px; }}
      section {{ padding:16px; }}
      h1 {{ margin:0 0 8px; font-size:26px; }}
      h2 {{ margin:0 0 12px; font-size:18px; }}
      p, li {{ line-height:1.55; }}
      nav {{ display:flex; flex-wrap:wrap; gap:8px; margin-top:12px; }}
      a {{ color:#2457a6; text-decoration:none; }}
      a:hover {{ text-decoration:underline; }}
      table {{ width:100%; border-collapse:collapse; font-size:13px; }}
      th, td {{ border:1px solid var(--line); padding:8px; vertical-align:top; }}
      th {{ background:var(--head); text-align:left; }}
      pre {{ margin:0; padding:12px; overflow:auto; background:#111827; color:#d1d5db; border-radius:6px; font-size:12px; line-height:1.45; }}
      details {{ border:1px solid var(--line); border-radius:8px; margin:8px 0; background:white; }}
      summary {{ cursor:pointer; padding:10px 12px; font-weight:700; background:var(--head); }}
      details pre {{ border-radius:0 0 8px 8px; }}
      .mono {{ font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }}
      .muted {{ color:var(--muted); }}
      .tag {{ display:inline-block; border-radius:999px; padding:2px 8px; color:white; font-size:12px; }}
      .tag.good {{ background:var(--good); }}
      .tag.warn {{ background:var(--warn); }}
      .tag.candidate {{ background:var(--candidate); }}
      @media (max-width: 760px) {{
        main {{ padding:12px; }}
        th:nth-child(5), td:nth-child(5), th:nth-child(6), td:nth-child(6) {{ display:none; }}
      }}
    </style>
  </head>
  <body>
    <main>
      <header>
        <h1>{esc(data['title'])}</h1>
        <p class="muted">actor <span class="mono">{esc(data['actorField'])}</span> 비트를 hit-unit reset, 상태 회복, 결과 사운드 소비 지점으로 다시 묶은 검토 자료.</p>
        <nav>
          <a href="../web/index.html">관리 홈</a>
          <a href="battle_result_flag_lifecycle_review.json">JSON</a>
          <a href="battle_result_flag_lifecycle_review.md">MD</a>
          <a href="battle_result_family_dispatch_review.html">result-family dispatch</a>
          <a href="battle_status_transition_review.html">상태 전환</a>
          <a href="battle_stat_flag_status_semantics_review.html">스탯/플래그/상태</a>
          <a href="../web/battle_simulator.html">전투 기술 실행</a>
        </nav>
      </header>
      <section>
        <h2>결론</h2>
        <ul>{summary_items}</ul>
      </section>
      <section>
        <h2>비트별 생명주기</h2>
        <table>
          <thead><tr><th>bit</th><th>scope</th><th>역할</th><th>상태</th><th>생성</th><th>소비/해제</th><th>남은 점</th></tr></thead>
          <tbody>{flag_rows}</tbody>
        </table>
      </section>
      <section>
        <h2>Reset/소비 마스크</h2>
        <table>
          <thead><tr><th>VA</th><th>루틴</th><th>연산</th><th>보존</th><th>해제</th><th>의미</th></tr></thead>
          <tbody>{reset_rows}</tbody>
        </table>
      </section>
      <section>
        <h2>근거 라인</h2>
        {evidence_blocks}
      </section>
      <section>
        <h2>미확정</h2>
        <ul>{open_items}</ul>
      </section>
    </main>
  </body>
</html>
"""


def render_md(data: dict[str, Any]) -> str:
    lines: list[str] = [
        f"# {data['title']}",
        "",
        "## 결론",
    ]
    for item in data["summary"]:
        lines.append(f"- {item}")
    lines.extend(["", "## 비트별 생명주기", "", "| bit | scope | 역할 | 상태 | 생성 | 소비/해제 | 남은 점 |", "|---|---|---|---|---|---|---|"])
    for row in data["flags"]:
        lines.append(
            f"| {row['bit']} | {row['scope']} | {row['name']} - {row['engineMeaning']} | {row['status']} | {row['producer']} | {row['consumer']} | {row['open']} |"
        )
    lines.extend(["", "## Reset/소비 마스크", "", "| VA | 루틴 | 연산 | 보존 | 해제 | 의미 |", "|---|---|---|---|---|---|"])
    for row in data["resets"]:
        lines.append(
            f"| {hx(row['va'])} | {row['routine']} | `{row['operation']}` | {row['keeps']} | {row['clears']} | {row['meaning']} |"
        )
    lines.extend(["", "## 근거 라인"])
    for item in data["evidence"]:
        lines.extend(["", f"### {item['label']} ({item['startVaHex']}..{item['stopVaHex']})", "", "```asm"])
        lines.extend(item["lines"] or ["; no compact evidence lines selected"])
        lines.append("```")
    lines.extend(["", "## 미확정"])
    if data["openQuestions"]:
        for item in data["openQuestions"]:
            lines.append(f"- {item}")
    else:
        lines.append("- 없음")
    lines.append("")
    return "\n".join(lines)


def main() -> None:
    OUT.mkdir(exist_ok=True)
    data = build_data()
    (OUT / "battle_result_flag_lifecycle_review.json").write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
    (OUT / "battle_result_flag_lifecycle_review.html").write_text(render_html(data), encoding="utf-8")
    (OUT / "battle_result_flag_lifecycle_review.md").write_text(render_md(data), encoding="utf-8")
    print("wrote battle_result_flag_lifecycle_review")


if __name__ == "__main__":
    main()
