# monitor.log 요약 — 스킬별 시전 수, 시전 뒤 window 초 안에 들어온 플레이어 피해, 예고 시간(설정) 대비 실제 첫 타격 시각 import io import json import re import sys sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") log_path = sys.argv[1] names_path = sys.argv[2] window = float(sys.argv[3]) if len(sys.argv) > 3 else 5.0 since = float(sys.argv[4]) if len(sys.argv) > 4 else 0.0 config_path = sys.argv[5] if len(sys.argv) > 5 else None names = {} for row in json.load(open(names_path, encoding="utf-8-sig")): names[row["Index"]] = row.get("Desc", "").replace("[BT] ", "") telegraphs = {} if config_path: for boss in json.load(open(config_path, encoding="utf-8-sig")).get("bosses", []): for t in boss.get("timings") or []: telegraphs[t["skillId"]] = t casts = [] # (t, skill, owner, dist) hits = [] # (t, count delta) damages = [] # (t, delta) states = [] positions = [] notices = [] anims = [] for line in open(log_path, encoding="utf-8", errors="replace"): m = re.match(r"([\d.]+) CAST (\d+) by (\d+) play=(\d+) dist=([\d.]+)", line) if m: t = float(m.group(1)) if t >= since: casts.append((t, int(m.group(2)), int(m.group(3)), float(m.group(5)))) continue m = re.match(r"([\d.]+) PLAYERHIT count (-?\d+) -> (-?\d+)", line) if m: t = float(m.group(1)) if t >= since: hits.append((t, int(m.group(3)) - int(m.group(2)))) continue m = re.match(r"([\d.]+) PLAYER hp (-?\d+) -> (-?\d+) \((-?\d+)\)", line) if m: t = float(m.group(1)) if t >= since: damages.append((t, int(m.group(4)))) continue m = re.match(r"([\d.]+) PLAYERPOS (.*)", line) if m and float(m.group(1)) >= since: positions.append((float(m.group(1)), m.group(2).strip())) continue m = re.match(r"([\d.]+) NOTICE (.*)", line) if m and float(m.group(1)) >= since: notices.append((float(m.group(1)), m.group(2).strip())) continue m = re.match(r"([\d.]+) ANIM (\d+) id=(-?\d+) speed=([\d.]+) at=([\d.]+)", line) if m and float(m.group(1)) >= since: anims.append((float(m.group(1)), int(m.group(3)), float(m.group(4)), float(m.group(5)))) continue m = re.match(r"([\d.]+) BOSS#(\d+) hp=(-?\d+) idx=(\d+) init=(\d) dead=(\d) phase=(\d+) inv=(\d+) abn=(\d) gauge=(\d+) groggy=(\d) stun=(\d)(.*)", line) if m and float(m.group(1)) >= since: rest = m.group(13) out = re.search(r" out=(\d)", rest) air = re.search(r" air=(\d)", rest) mask = re.search(r" mask=(\d+)", rest) states.append((float(m.group(1)), int(m.group(4)), int(m.group(7)), int(m.group(8)), int(m.group(10)), int(m.group(11)), int(m.group(12)), int(m.group(3)), int(out.group(1)) if out else 0, int(air.group(1)) if air else 0, int(mask.group(1)) if mask else 0)) per = {} order = [] for i, (t, skill, owner, dist) in enumerate(casts): # 같은 스킬의 다음 시전 전까지만, 그리고 window 안 nxt = min([c[0] for c in casts[i + 1:] if c[1] == skill] + [t + window]) end = min(t + window, nxt) mine = [(ht, d) for (ht, d) in hits if t <= ht < end] n_hits = sum(d for (_, d) in mine) dmg = [d for (dt, d) in damages if t <= dt < end and d < 0] if skill not in per: per[skill] = {"casts": 0, "hit_casts": 0, "requests": 0, "damage": [], "dist": [], "first": [], "speeds": []} order.append(skill) p = per[skill] p["casts"] += 1 p["requests"] += n_hits if n_hits > 0 or dmg: p["hit_casts"] += 1 if mine: p["first"].append(mine[0][0] - t) p["damage"] += dmg p["dist"].append(dist) # 시전 뒤 예고 시간 동안의 동작 속도 변화(박자) tele = telegraphs.get(skill, {}).get("telegraph", 0) if tele: p["speeds"].append([f"{a[0] - t:.2f}s:{a[2]:.2f}@{a[3]:.2f}" for a in anims if t <= a[0] <= t + tele + 0.6 and 3 <= a[1] <= 7][:5]) print("스킬 | 이름 | 시전 | 맞은 시전 | 피해 요청 | 체력 변화(최대 6개) | 시전 거리 | 예고(설정) | 첫 타격(실측 평균) | 차이") for skill in sorted(order): p = per[skill] dist = sum(p["dist"]) / len(p["dist"]) tele = telegraphs.get(skill, {}).get("telegraph") first = sum(p["first"]) / len(p["first"]) if p["first"] else None tele_s = f"{tele:.2f}" if tele is not None else "-" first_s = f"{first:.2f}" if first is not None else "-" diff = f"{first - tele:+.2f}" if (tele is not None and first is not None) else "-" print(f"{skill} | {names.get(skill, '?')} | {p['casts']} | {p['hit_casts']} | {p['requests']} | {p['damage'][:6]} | {dist:.1f} | {tele_s} | {first_s} | {diff}") if p["speeds"] and p["speeds"][0]: print(f" 박자(시전 뒤 시각:재생 속도@동작 위치): {p['speeds'][0]}") if states: phases = [] last = None for s in states: key = (s[1], s[2]) if key != last: phases.append(f"{s[0]:.1f}s idx {s[1]} P{s[2]} hp={s[7]}") last = key print("페이즈 흐름:", " → ".join(phases)) inv = [s for s in states if s[3] > 0] if inv: print(f"전환 무적 관측: {inv[0][0]:.1f}s ~ (무적 표시 {len(inv)}회)") gauges = sorted(set(s[4] for s in states)) stun = [s[0] for s in states if s[5] == 1 or s[6] == 1] print("그로기 게이지 관측값:", gauges, "· 기절/그로기 표시 시각:", [f"{x:.1f}" for x in stun[:6]]) print("전장 밖:", sum(1 for s in states if s[8]), "· 공중:", sum(1 for s in states if s[9]), "· 안전 지점 마스크:", sorted(set(s[10] for s in states))) if notices: print("안내:", " / ".join(f"{t:.0f}s {text}" for t, text in notices[:12]), f"(총 {len(notices)}건)") if positions: print("플레이어 위치 변화:", " → ".join(f"{t:.0f}s({p})" for t, p in positions[:12]), f"(총 {len(positions)}회)") total_damage = sum(d for _, d in damages if d < 0) print(f"플레이어 피해 합계 {total_damage} · 피해 요청 합계 {sum(d for _, d in hits)}")