"""re_hp_probe.py — pin the player's HP component in WASM memory by CORRELATING with vision HP.

The live HP entity reallocates and HP is a 0-1 FLOAT RATIO (not the displayed int), so blind scans
fail. This instead uses what we DO know each moment — the vision HP fraction — as the oracle:

  each round: scan all f32 in [vis-tol, vis+tol] whose i32 @+8 is a plausible maxHP (the +308/+316
              layout: HP ratio @entity+308, maxHP @entity+316 -> 8 bytes apart);
  keep the maxHP value(s) that KEEP having a ratio-matching address as vision HP CHANGES across
  rounds. That maxHP + layout survives reallocation (the address moves, the pattern persists).

RUN IT WHILE GRINDING ON THE THROWAWAY ACCOUNT so your HP goes up and down (take damage + heal).
The more your HP varies, the faster it converges.

  python re_hp_probe.py [rounds] [tol]      # default 12 rounds, tol 0.02
"""
import sys
import time
import urllib.request
from collections import defaultdict
from playwright.sync_api import sync_playwright
import grind_local as gl

PLAYER_PTR_ADDR = 1230588
SCAN_JS = """(a)=>{
  const lo=a.lo, hi=a.hi, mlo=a.mlo, mhi=a.mhi, cap=a.cap;
  const buf=HEAPU8.buffer, f=new Float32Array(buf), ii=new Int32Array(buf);
  const out=[]; const n=f.length-4;
  for (let i=0;i<n;i++){ const v=f[i];
    if (v>=lo && v<=hi){ const m=ii[i+2]; if (m>=mlo && m<=mhi){ out.push([i*4, m]); if(out.length>=cap) break; } } }
  return out;
}"""


def find_port():
    for p in range(9222, 9251):
        try:
            urllib.request.urlopen(f"http://127.0.0.1:{p}/json/version", timeout=0.3); return p
        except Exception:
            pass
    return None


def main():
    rounds = int(sys.argv[1]) if len(sys.argv) > 1 else 12
    tol = float(sys.argv[2]) if len(sys.argv) > 2 else 0.006
    port = find_port()
    if not port:
        print("No live game on 9222-9250 — launch the throwaway account in-game first."); return
    pw = sync_playwright().start()
    ctx = pw.chromium.connect_over_cdp(f"http://127.0.0.1:{port}").contexts[0]
    game = next((pg for pg in ctx.pages if "flyff" in (pg.url or "").lower()), ctx.pages[0])
    if not game.evaluate("typeof HEAPU8 !== 'undefined'"):
        print("HEAPU8 not on this page — is it the game, fully loaded?"); return
    print(f"connected (port {port}). Grind so HP varies (take damage + heal). tol={tol}\n")

    # maxHP -> how many CHANGED rounds it kept a ratio-matching address; + last seen address
    score = defaultdict(int); last_addr = {}; seen_ratios = []
    hp_box = (gl.HP_X0, gl.HP_Y0 - 2, gl.HP_X1, gl.HP_Y1 + 2)
    prev = None; done = 0
    while done < rounds:
        try:
            img = gl.grab(game)
            ocr = gl._ocr_pair_fraction(img, hp_box)     # EXACT cur/max oracle (precise) — if it reads right
            px = gl.hp_fraction(img)
            if ocr is not None and px is not None and abs(ocr - px) <= 0.10:
                vis, src = ocr, "ocr"                    # OCR agrees with pixel -> trust the precise value
            else:
                vis, src = px, "px"                      # OCR missing/garbled -> noisy pixel (looser tol)
        except Exception:
            vis = None
        if vis is None:
            time.sleep(0.7); continue
        rtol = tol if src == "ocr" else max(tol, 0.02)
        # only count a round when HP actually MOVED (so the correlation is meaningful)
        if prev is not None and abs(vis - prev) < 0.01:
            time.sleep(0.6); continue
        cands = game.evaluate(SCAN_JS, {"lo": vis - rtol, "hi": vis + rtol,
                                        "mlo": 50, "mhi": 30000, "cap": 20000})
        by_max = defaultdict(list)
        for addr, m in cands:
            by_max[m].append(addr)
        for m, addrs in by_max.items():
            score[m] += 1
            last_addr[m] = addrs[0]
        done += 1; prev = vis; seen_ratios.append(round(vis, 3))
        top = sorted(score.items(), key=lambda kv: -kv[1])[:6]
        print(f"round {done}/{rounds}  visHP={vis*100:4.0f}%  cands={len(cands)}  "
              f"top maxHP:[" + ", ".join(f"{m}:{c}/{done} hp~{int(m*vis)}" for m, c in top) + "]")
        time.sleep(0.5)

    print("\n=== RESULT — maxHP values that tracked vision HP across the most rounds ===")
    for m, c in sorted(score.items(), key=lambda kv: -kv[1])[:8]:
        if c >= 2:
            print(f"  maxHP={m:6d}  matched {c}/{done} rounds  last ratio@addr={last_addr[m]} "
                  f"(maxHP@{last_addr[m]+8})  -> read f32@addr × i32@addr+8 for live HP")
    print("\nThe winner is the maxHP equal to your character's real max HP, matched ~every round.")
    print("Next: from that addr, walk back to find the entity base (addr-308) and the registry that")
    print("holds it, so read_player_hp() can re-fetch it each frame despite reallocation.")


if __name__ == "__main__":
    main()
