#!/usr/bin/env python3
"""Run a WebKit browser smoke test for the title-to-map start flow.

Run it under Xvfb in headless VMs:

    xvfb-run -a python3 tools/verify_title_start_browser.py --base http://127.0.0.1:8013
"""
from __future__ import annotations

import argparse
import json
import os
import shutil
import socket
import subprocess
import time
from pathlib import Path
from typing import Any
from urllib.parse import urljoin
from urllib.request import Request, urlopen


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
TITLE_START_KEYS = ["start", "playStart", "startEncounters", "loadSavedat", "scanSavedat", "sampleSavedat", "routeAssist", "routeGoal", "home"]
TITLE_CONTINUE_KEYS = ["start", "playStart", "startEncounters", "continue", "loadSavedat", "scanSavedat", "sampleSavedat", "routeAssist", "routeGoal", "home"]
TRIAL_BLOCK_REASONS = [
    "no map1_01a->map2_02d transition-review row",
    "no strict map1_01a event transition",
    "coordinate refs are non-promotable",
    "variant coordinate scan found no strict source coordinate",
    "tile signature matches only as geometry/tile evidence",
]
TRIAL_BLOCKER_SHORT = "trial-only: no map1_01a->map2_02d transition-review row +4"
TRIAL_BLOCKER_FULL = "; ".join(TRIAL_BLOCK_REASONS)
TRIAL_CANDIDATE_TRANSITION_TEXT = f"후보 이동 map1_01a -> map2_02d · {TRIAL_BLOCKER_SHORT}"
ITEM_TEXT_TABLES = {
    "herb": {
        "itemNameSource": "exe-text-table-items",
        "itemTextTableKey": "items",
        "itemTextTableIndex": 0,
        "itemTextTableRefVaHex": "0x0048b984",
        "itemTextTableTextVaHex": "0x0048ba7c",
    },
    "item_2": {
        "itemNameSource": "exe-text-table-items",
        "itemTextTableKey": "items",
        "itemTextTableIndex": 1,
        "itemTextTableRefVaHex": "0x0048b98c",
        "itemTextTableTextVaHex": "0x0048ba8e",
    },
}


class WebDriverError(RuntimeError):
    pass


def browser_display_available() -> bool:
    return bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"))


def mini_browser_session_requested(path: str, payload: dict[str, Any] | None) -> bool:
    if path != "/session" or not isinstance(payload, dict):
        return False
    capabilities = payload.get("capabilities") or {}
    always_match = capabilities.get("alwaysMatch") or {}
    return always_match.get("browserName") == "MiniBrowser"


def item_text_provenance_matches(row: dict[str, Any], item_key: str, *, prefix: str = "item") -> bool:
    expected = ITEM_TEXT_TABLES[item_key]

    def key(name: str) -> str:
        return f"{prefix}{name[:1].upper()}{name[1:]}" if prefix else name

    return (
        row.get(key("nameSource")) == expected["itemNameSource"]
        and row.get(key("textTableKey")) == expected["itemTextTableKey"]
        and row.get(key("textTableIndex")) == expected["itemTextTableIndex"]
        and row.get(key("textTableRefVaHex")) == expected["itemTextTableRefVaHex"]
        and row.get(key("textTableTextVaHex")) == expected["itemTextTableTextVaHex"]
    )


def free_port() -> int:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.bind(("127.0.0.1", 0))
        return int(sock.getsockname()[1])


def request_json(port: int, method: str, path: str, payload: dict[str, Any] | None = None, timeout: float = 10) -> dict[str, Any] | None:
    if mini_browser_session_requested(path, payload) and not browser_display_available():
        raise WebDriverError(
            "MiniBrowser requires DISPLAY/WAYLAND_DISPLAY. "
            "Run this browser smoke test with `xvfb-run -a ...` in headless VMs."
        )
    data = None if payload is None else json.dumps(payload).encode("utf-8")
    request = Request(
        f"http://127.0.0.1:{port}{path}",
        data=data,
        method=method,
        headers={"Content-Type": "application/json"},
    )
    try:
        with urlopen(request, timeout=timeout) as response:
            body = response.read().decode("utf-8")
    except TimeoutError as exc:
        raise WebDriverError(f"WebDriver request timed out after {timeout}s: {method} {path}") from exc
    return json.loads(body) if body else None


def wait_for_driver(port: int, proc: subprocess.Popen[bytes], timeout: float = 8) -> None:
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        if proc.poll() is not None:
            raise WebDriverError(f"WebKitWebDriver exited early with status {proc.returncode}")
        try:
            status = request_json(port, "GET", "/status", timeout=0.5)
        except Exception:
            time.sleep(0.1)
            continue
        if status and status.get("value", {}).get("ready") is True:
            return
        time.sleep(0.1)
    raise WebDriverError("WebKitWebDriver did not become ready")


def execute_js(port: int, session_id: str, script: str, timeout: float = 8) -> Any:
    response = request_json(
        port,
        "POST",
        f"/session/{session_id}/execute/sync",
        {"script": script, "args": []},
        timeout=timeout,
    )
    if not response or "value" not in response:
        raise WebDriverError(f"bad execute/sync response: {response!r}")
    return response["value"]


def canvas_checksum_script() -> str:
    return """
const canvas = document.getElementById('screen');
const data = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height).data;
let checksum = 0;
for (let index = 0; index < data.length; index += 16) {
  checksum = (((checksum * 131) >>> 0) + data[index] + data[index + 1] * 3 + data[index + 2] * 7 + data[index + 3] * 11) >>> 0;
}
return checksum;
"""


def title_state_script() -> str:
    return """
const titleMenu = typeof titleMenuItems === 'function' ? titleMenuItems() : [];
return {
  readyState: document.readyState,
  href: location.href,
  search: location.search,
  hasScreen: !!document.getElementById('screen'),
  scene: typeof scene === 'undefined' ? '' : scene,
  mapName: typeof map === 'undefined' || !map ? '' : map.name,
  titleLoaded: !!images?.title?.complete && (images.title.naturalWidth || 0) > 0,
  quickLoadHidden: document.getElementById('quickLoadButton')?.hidden ?? null,
  quickLoadText: document.getElementById('quickLoadButton')?.textContent || '',
  playHudLines: typeof playHudLines === 'function' ? playHudLines() : (window.HWANSE_LAST_PLAY_HUD_LINES || []),
  titleMenuRender: window.HWANSE_LAST_TITLE_MENU_RENDER || null,
  titleSavedatScan: window.HWANSE_LAST_TITLE_SAVEDAT_SCAN || null,
  titleRouteGoal: window.HWANSE_LAST_TITLE_ROUTE_GOAL || null,
  titleMenuMode: typeof titleMenuMode === 'undefined' ? '' : titleMenuMode,
  titleMenuKeys: titleMenu.map((item) => item.key),
  titleMenuLabels: titleMenu.map((item) => item.label),
  selectedTitleMenuIndex: typeof selectedTitleMenuIndex === 'undefined' ? null : selectedTitleMenuIndex,
  routeGuideHidden: document.getElementById('routeGuideLink')?.hidden ?? null,
  mapReviewTileHidden: document.getElementById('mapReviewTileLink')?.hidden ?? null,
  bootError: window.HWANSE_BOOT_ERROR || null,
};
"""


def wait_for_title(port: int, session_id: str) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, title_state_script(), timeout=3)
        last_state = state
        if (
            state.get("readyState") == "complete"
            and state.get("hasScreen") is True
            and state.get("scene") == "title"
            and state.get("search") == "?game=1"
            and state.get("titleLoaded") is True
            and state.get("bootError") is None
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"title runtime did not load: {last_state!r}")


def press_title_enter_script() -> str:
    return """
window.dispatchEvent(new KeyboardEvent('keydown', {
  bubbles: true,
  cancelable: true,
  key: 'Enter',
  code: 'Enter',
  keyCode: 13,
  which: 13,
}));
const items = typeof titleMenuItems === 'function' ? titleMenuItems() : [];
return {
  ok: true,
  defaultTitleStart: true,
  selectedTitleMenuIndex: typeof selectedTitleMenuIndex === 'undefined' ? null : selectedTitleMenuIndex,
  keys: items.map((item) => item.key),
  labels: items.map((item) => item.label),
};
"""


def click_title_route_assist_script() -> str:
    return """
const button = document.getElementById('routeAssistButton');
if (!button) return false;
button.click();
return true;
"""


def click_title_menu_route_assist_script() -> str:
    return """
const canvas = document.getElementById('screen');
const rect = canvas.getBoundingClientRect();
if (typeof debugControlsOpen !== 'undefined' && !debugControlsOpen) {
  debugControlsOpen = true;
  if (typeof syncDebugControls === 'function') syncDebugControls();
}
const geometry = titleMenuGeometry();
const items = titleMenuItems();
const index = items.findIndex((item) => item.key === 'routeAssist');
if (index < 0) return { ok: false, reason: 'missing-routeAssist', keys: items.map((item) => item.key) };
const clientX = rect.left + ((geometry.x + geometry.width / 2) / canvas.width) * rect.width;
const clientY = rect.top + ((geometry.y + 10 + index * geometry.rowHeight + geometry.rowHeight / 2) / canvas.height) * rect.height;
canvas.dispatchEvent(new PointerEvent('pointerdown', {
  bubbles: true,
  cancelable: true,
  pointerId: 21,
  pointerType: 'mouse',
  isPrimary: true,
  clientX,
  clientY,
}));
return {
  ok: true,
  index,
  selectedTitleMenuIndex,
  keys: items.map((item) => item.key),
  clientX,
  clientY,
};
"""


def click_title_route_goal_script(target: str = "map2_18d") -> str:
    return f"""
const canvas = document.getElementById('screen');
const rect = canvas.getBoundingClientRect();
const geometry = titleMenuGeometry();
const items = titleMenuItems();
const index = items.findIndex((item) => item.key === 'routeGoal');
if (index < 0) {{
  return {{ ok: false, reason: 'missing-routeGoal', keys: items.map((item) => item.key), labels: items.map((item) => item.label) }};
}}
const clientX = rect.left + ((geometry.x + geometry.width / 2) / canvas.width) * rect.width;
const clientY = rect.top + ((geometry.y + 10 + index * geometry.rowHeight + geometry.rowHeight / 2) / canvas.height) * rect.height;
canvas.dispatchEvent(new PointerEvent('pointerdown', {{
  bubbles: true,
  cancelable: true,
  pointerId: 31,
  pointerType: 'mouse',
  isPrimary: true,
  clientX,
  clientY,
}}));
const routeGoalItems = titleMenuItems();
const targetIndex = routeGoalItems.findIndex((item) => item.routeTarget === '{target}');
if (targetIndex < 0) {{
  return {{
    ok: false,
    reason: 'missing-target',
    keys: items.map((item) => item.key),
    labels: items.map((item) => item.label),
    routeGoalKeys: routeGoalItems.map((item) => item.key),
    routeGoalLabels: routeGoalItems.map((item) => item.label),
    titleMenuMode,
  }};
}}
selectedTitleMenuIndex = targetIndex;
if (typeof render === 'function') render();
const routeGoalGeometry = titleMenuGeometry(routeGoalItems);
const routeGoalWindow = titleMenuWindow(routeGoalItems);
const visibleTargetIndex = targetIndex - routeGoalWindow.start;
if (visibleTargetIndex < 0 || visibleTargetIndex >= routeGoalWindow.end - routeGoalWindow.start) {{
  return {{
    ok: false,
    reason: 'target-not-visible',
    targetIndex,
    routeGoalWindow,
    routeGoalKeys: routeGoalItems.map((item) => item.key),
    routeGoalLabels: routeGoalItems.map((item) => item.label),
  }};
}}
const targetClientX = rect.left + ((routeGoalGeometry.x + routeGoalGeometry.width / 2) / canvas.width) * rect.width;
const targetClientY = rect.top + ((routeGoalGeometry.y + 10 + visibleTargetIndex * routeGoalGeometry.rowHeight + routeGoalGeometry.rowHeight / 2) / canvas.height) * rect.height;
canvas.dispatchEvent(new PointerEvent('pointerdown', {{
  bubbles: true,
  cancelable: true,
  pointerId: 32,
  pointerType: 'mouse',
  isPrimary: true,
  clientX: targetClientX,
  clientY: targetClientY,
}}));
return {{
  ok: window.HWANSE_LAST_TITLE_ROUTE_GOAL?.target === '{target}',
  index,
  targetIndex,
  visibleTargetIndex,
  selectedTitleMenuIndex,
  keys: items.map((item) => item.key),
  labels: items.map((item) => item.label),
  routeGoalKeys: routeGoalItems.map((item) => item.key),
  routeGoalLabels: routeGoalItems.map((item) => item.label),
  routeGoalWindow,
  request: window.HWANSE_LAST_TITLE_ROUTE_GOAL || null,
  clientX,
  clientY,
  targetClientX,
  targetClientY,
}};
"""


def click_title_encounter_start_script() -> str:
    return """
const canvas = document.getElementById('screen');
const rect = canvas.getBoundingClientRect();
const geometry = titleMenuGeometry();
const items = titleMenuItems();
const index = items.findIndex((item) => item.key === 'startEncounters');
if (index < 0) return { ok: false, reason: 'missing-startEncounters', keys: items.map((item) => item.key) };
window.HWANSE_FIELD_ENCOUNTER_FEEDBACK_LOG = [];
window.HWANSE_FIELD_ENCOUNTER_FEEDBACK_RENDER = [];
window.HWANSE_LAST_FIELD_ENCOUNTER_FEEDBACK = null;
window.HWANSE_LAST_FIELD_ENCOUNTER_FEEDBACK_RENDER = null;
if (typeof activeFieldEncounterFeedbacks !== 'undefined') activeFieldEncounterFeedbacks = [];
const clientX = rect.left + ((geometry.x + geometry.width / 2) / canvas.width) * rect.width;
const clientY = rect.top + ((geometry.y + 10 + index * geometry.rowHeight + geometry.rowHeight / 2) / canvas.height) * rect.height;
canvas.dispatchEvent(new PointerEvent('pointerdown', {
  bubbles: true,
  cancelable: true,
  pointerId: 22,
  pointerType: 'mouse',
  isPrimary: true,
  clientX,
  clientY,
}));
return {
  ok: true,
  index,
  selectedTitleMenuIndex,
  keys: items.map((item) => item.key),
  labels: items.map((item) => item.label),
  clientX,
  clientY,
};
"""


def click_title_play_start_script() -> str:
    return """
const canvas = document.getElementById('screen');
const rect = canvas.getBoundingClientRect();
const geometry = titleMenuGeometry();
const items = titleMenuItems();
const index = items.findIndex((item) => item.key === 'playStart');
if (index < 0) return { ok: false, reason: 'missing-playStart', keys: items.map((item) => item.key) };
const clientX = rect.left + ((geometry.x + geometry.width / 2) / canvas.width) * rect.width;
const clientY = rect.top + ((geometry.y + 10 + index * geometry.rowHeight + geometry.rowHeight / 2) / canvas.height) * rect.height;
canvas.dispatchEvent(new PointerEvent('pointerdown', {
  bubbles: true,
  cancelable: true,
  pointerId: 27,
  pointerType: 'mouse',
  isPrimary: true,
  clientX,
  clientY,
}));
return {
  ok: true,
  index,
  selectedTitleMenuIndex,
  keys: items.map((item) => item.key),
  labels: items.map((item) => item.label),
  clientX,
  clientY,
  request: window.HWANSE_LAST_TITLE_ROUTE_GOAL || null,
};
"""


def click_title_confirmed_start_script() -> str:
    return """
const canvas = document.getElementById('screen');
const rect = canvas.getBoundingClientRect();
const geometry = titleMenuGeometry();
const items = titleMenuItems();
const index = items.findIndex((item) => item.key === 'start');
if (index < 0) return { ok: false, reason: 'missing-start', keys: items.map((item) => item.key) };
const clientX = rect.left + ((geometry.x + geometry.width / 2) / canvas.width) * rect.width;
const clientY = rect.top + ((geometry.y + 10 + index * geometry.rowHeight + geometry.rowHeight / 2) / canvas.height) * rect.height;
canvas.dispatchEvent(new PointerEvent('pointerdown', {
  bubbles: true,
  cancelable: true,
  pointerId: 28,
  pointerType: 'mouse',
  isPrimary: true,
  clientX,
  clientY,
}));
return {
  ok: true,
  index,
  selectedTitleMenuIndex,
  keys: items.map((item) => item.key),
  labels: items.map((item) => item.label),
  clientX,
  clientY,
};
"""


def click_route_next_button_script() -> str:
    return """
const button = document.getElementById('routeNextButton');
if (!button || button.hidden || button.disabled) {
  return {
    ok: false,
    hidden: button?.hidden ?? null,
    disabled: button?.disabled ?? null,
    text: button?.textContent || '',
    title: button?.title || '',
    search: location.search,
  };
}
const before = {
  text: button.textContent || '',
  title: button.title || '',
  search: location.search,
  map: map?.name || '',
  routeGoal: typeof selectedRouteGoal === 'undefined' ? '' : selectedRouteGoal,
};
button.click();
return {
  ok: true,
  before,
};
"""


def route_input_transition_script(expected_source: str, expected_target: str, input_code: str = "ArrowUp") -> str:
    return f"""
window.__hwanseRouteInputTransition = null;
window.HWANSE_MAP_TRANSITION_FEEDBACK_LOG = [];
window.HWANSE_MAP_TRANSITION_FEEDBACK_RENDER = [];
window.HWANSE_LAST_MAP_TRANSITION_FEEDBACK = null;
window.HWANSE_LAST_MAP_TRANSITION_FEEDBACK_RENDER = null;
window.HWANSE_SOUND_COUNTS = {{}};
window.HWANSE_SOUND_LOG = [];
window.HWANSE_LAST_SOUND = null;
if (typeof activeMapTransitionFeedbacks !== 'undefined') activeMapTransitionFeedbacks = [];
const before = {{
  scene,
  mapName: map?.name || '',
  foot: footTile(),
  search: location.search,
  routeState: runtimeRouteStateSavePayload(),
  fieldEncounter: fieldEncounterSavePayload(),
  routeNextText: document.getElementById('routeNextButton')?.textContent || '',
  playHudLines: playHudLines(),
}};
const inputCode = '{input_code}';
window.__hwanseRouteInputTransition = {{
  pending: true,
  movementInput: true,
  inputCode,
  expectedSource: '{expected_source}',
  expectedTarget: '{expected_target}',
  before,
}};
window.dispatchEvent(new KeyboardEvent('keydown', {{
  bubbles: true,
  cancelable: true,
  code: inputCode,
  key: inputCode,
}}));
window.setTimeout(() => {{
  window.dispatchEvent(new KeyboardEvent('keyup', {{
    bubbles: true,
    cancelable: true,
    code: inputCode,
    key: inputCode,
  }}));
}}, 120);
const startedAt = performance.now();
const poll = () => {{
  const after = {{
    scene,
    mapName: map?.name || '',
    foot: footTile(),
    search: location.search,
    routeState: runtimeRouteStateSavePayload(),
    fieldEncounter: fieldEncounterSavePayload(),
    routeNextText: document.getElementById('routeNextButton')?.textContent || '',
    playHudLines: playHudLines(),
  }};
  const mapTransitionAutoSave = window.HWANSE_LAST_MAP_TRANSITION_AUTO_SAVE || null;
  const routeCandidateAutoSave = window.HWANSE_LAST_ROUTE_CANDIDATE_AUTO_SAVE || null;
  const mapTransitionFeedbackLog = window.HWANSE_MAP_TRANSITION_FEEDBACK_LOG || [];
  const mapTransitionFeedbackRender = window.HWANSE_MAP_TRANSITION_FEEDBACK_RENDER || [];
  const feedbackReady = mapTransitionFeedbackLog.some((entry) => (
    entry?.sourceMap === '{expected_source}' &&
    entry?.targetMap === '{expected_target}' &&
    entry?.browserMapTransitionFeedbackImplemented === true
  )) && mapTransitionFeedbackRender.some((entry) => (
    entry?.sourceMap === '{expected_source}' &&
    entry?.targetMap === '{expected_target}' &&
    entry?.browserMapTransitionFeedbackImplemented === true
  ));
  const payload = JSON.parse(localStorage.getItem(RUNTIME_SAVE_KEY) || 'null');
  if ((after.mapName === '{expected_target}' && feedbackReady) || performance.now() - startedAt > 3500) {{
    window.__hwanseRouteInputTransition = {{
      pending: false,
      movementInput: true,
      inputCode,
      expectedSource: '{expected_source}',
      expectedTarget: '{expected_target}',
      before,
      after,
      changedMap: before.mapName !== after.mapName,
      mapTransitionAutoSave,
      routeCandidateAutoSave,
      savedPayload: payload,
      confirmedTransition: window.HWANSE_LAST_CONFIRMED_EVENT_TRANSITION || null,
      progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
      mapTransitionFeedbackLog,
      mapTransitionFeedbackRender,
      mapTransitionFeedbackLast: window.HWANSE_LAST_MAP_TRANSITION_FEEDBACK || null,
      mapTransitionFeedbackLastRender: window.HWANSE_LAST_MAP_TRANSITION_FEEDBACK_RENDER || null,
      soundState: {{
        counts: {{ ...(window.HWANSE_SOUND_COUNTS || {{}}) }},
        last: window.HWANSE_LAST_SOUND || null,
        log: Array.isArray(window.HWANSE_SOUND_LOG) ? [...window.HWANSE_SOUND_LOG] : [],
      }},
      quickLoadHidden: document.getElementById('quickLoadButton')?.hidden ?? null,
      quickLoadText: document.getElementById('quickLoadButton')?.textContent || '',
      originalRoutePromotionImplemented: false,
    }};
  }} else {{
    window.setTimeout(poll, 50);
  }}
}};
window.setTimeout(poll, 120);
return true;
"""


def route_input_transition_state_script() -> str:
    return "return window.__hwanseRouteInputTransition || null;"


def click_title_load_savedat_script() -> str:
    return """
const canvas = document.getElementById('screen');
const rect = canvas.getBoundingClientRect();
const input = document.getElementById('saveFileInput');
const geometry = titleMenuGeometry();
const items = titleMenuItems();
const index = items.findIndex((item) => item.key === 'loadSavedat');
if (index < 0 || !input) {
  return { ok: false, reason: index < 0 ? 'missing-loadSavedat' : 'missing-input', keys: items.map((item) => item.key) };
}
const originalClick = input.click.bind(input);
input.click = () => {
  window.__hwanseTitleSavedatInputClicked = {
    clicked: true,
    accept: input.accept || '',
    scene,
    keys: titleMenuItems().map((item) => item.key),
  };
};
const clientX = rect.left + ((geometry.x + geometry.width / 2) / canvas.width) * rect.width;
const clientY = rect.top + ((geometry.y + 10 + index * geometry.rowHeight + geometry.rowHeight / 2) / canvas.height) * rect.height;
canvas.dispatchEvent(new PointerEvent('pointerdown', {
  bubbles: true,
  cancelable: true,
  pointerId: 23,
  pointerType: 'mouse',
  isPrimary: true,
  clientX,
  clientY,
}));
const clicked = window.__hwanseTitleSavedatInputClicked || null;
const request = window.HWANSE_LAST_SAVEDAT_FILE_REQUEST || null;
input.click = originalClick;
return {
  ok: !!clicked?.clicked && request?.source === 'title-menu',
  index,
  selectedTitleMenuIndex,
  keys: items.map((item) => item.key),
  labels: items.map((item) => item.label),
  clicked,
  request,
  clientX,
  clientY,
};
"""


def click_title_sample_savedat_script() -> str:
    return """
const canvas = document.getElementById('screen');
const rect = canvas.getBoundingClientRect();
const geometry = titleMenuGeometry();
const items = titleMenuItems();
const index = items.findIndex((item) => item.key === 'sampleSavedat');
if (index < 0) {
  return { ok: false, reason: 'missing-sampleSavedat', keys: items.map((item) => item.key) };
}
const clientX = rect.left + ((geometry.x + geometry.width / 2) / canvas.width) * rect.width;
const clientY = rect.top + ((geometry.y + 10 + index * geometry.rowHeight + geometry.rowHeight / 2) / canvas.height) * rect.height;
canvas.dispatchEvent(new PointerEvent('pointerdown', {
  bubbles: true,
  cancelable: true,
  pointerId: 24,
  pointerType: 'mouse',
  isPrimary: true,
  clientX,
  clientY,
}));
const sampleItems = titleMenuItems();
const sampleIndex = sampleItems.findIndex((item) => item.sampleKey === 'flack3r-savedat2');
if (sampleIndex < 0) {
  return {
    ok: false,
    reason: 'missing-flack3r-savedat2',
    keys: items.map((item) => item.key),
    labels: items.map((item) => item.label),
    sampleKeys: sampleItems.map((item) => item.key),
    sampleLabels: sampleItems.map((item) => item.label),
    titleMenuMode,
  };
}
const sampleGeometry = titleMenuGeometry(sampleItems);
const sampleClientX = rect.left + ((sampleGeometry.x + sampleGeometry.width / 2) / canvas.width) * rect.width;
const sampleClientY = rect.top + ((sampleGeometry.y + 10 + sampleIndex * sampleGeometry.rowHeight + sampleGeometry.rowHeight / 2) / canvas.height) * rect.height;
canvas.dispatchEvent(new PointerEvent('pointerdown', {
  bubbles: true,
  cancelable: true,
  pointerId: 26,
  pointerType: 'mouse',
  isPrimary: true,
  clientX: sampleClientX,
  clientY: sampleClientY,
}));
return {
  ok: window.HWANSE_LAST_TITLE_PUBLIC_SAVEDAT?.key === 'flack3r-savedat2',
  index,
  sampleIndex,
  selectedTitleMenuIndex,
  keys: items.map((item) => item.key),
  labels: items.map((item) => item.label),
  sampleKeys: sampleItems.map((item) => item.key),
  sampleLabels: sampleItems.map((item) => item.label),
  titleMenuMode,
  request: window.HWANSE_LAST_TITLE_PUBLIC_SAVEDAT || null,
  clientX,
  clientY,
  sampleClientX,
  sampleClientY,
};
"""


def click_title_scan_savedat_script() -> str:
    return """
const canvas = document.getElementById('screen');
const rect = canvas.getBoundingClientRect();
const geometry = titleMenuGeometry();
const items = titleMenuItems();
const index = items.findIndex((item) => item.key === 'scanSavedat');
if (index < 0) {
  return { ok: false, reason: 'missing-scanSavedat', keys: items.map((item) => item.key) };
}
const clientX = rect.left + ((geometry.x + geometry.width / 2) / canvas.width) * rect.width;
const clientY = rect.top + ((geometry.y + 10 + index * geometry.rowHeight + geometry.rowHeight / 2) / canvas.height) * rect.height;
canvas.dispatchEvent(new PointerEvent('pointerdown', {
  bubbles: true,
  cancelable: true,
  pointerId: 25,
  pointerType: 'mouse',
  isPrimary: true,
  clientX,
  clientY,
}));
return {
  ok: true,
  index,
  selectedTitleMenuIndex,
  keys: items.map((item) => item.key),
  labels: items.map((item) => item.label),
  request: window.HWANSE_LAST_TITLE_SAVEDAT_SCAN || null,
  clientX,
  clientY,
};
"""


def click_title_continue_script() -> str:
    return """
if (typeof updateRuntimeSaveControls === 'function') updateRuntimeSaveControls();
const items = typeof titleMenuItems === 'function' ? titleMenuItems() : [];
const button = document.getElementById('quickLoadButton');
if (!button || button.hidden || button.textContent !== '이어하기' || !items.some((item) => item.key === 'continue')) {
  return {
    ok: false,
    hidden: button?.hidden ?? null,
    text: button?.textContent || '',
    titleMenuKeys: items.map((item) => item.key),
  };
}
const index = items.findIndex((item) => item.key === 'continue');
selectedTitleMenuIndex = index;
activateSelectedTitleMenuItem();
return {
  ok: index >= 0,
  text: button.textContent,
  title: button.title,
  titleMenuKeys: items.map((item) => item.key),
  selectedTitleMenuIndex,
};
"""


def clear_runtime_save_script() -> str:
    return """
localStorage.removeItem(RUNTIME_SAVE_KEY);
if (typeof updateRuntimeSaveControls === 'function') updateRuntimeSaveControls();
return true;
"""


def quick_save_started_map_script() -> str:
    return """
window.HWANSE_RUNTIME_SAVE_FEEDBACK_LOG = [];
window.HWANSE_RUNTIME_SAVE_FEEDBACK_RENDER = [];
window.HWANSE_LAST_RUNTIME_SAVE_FEEDBACK = null;
window.HWANSE_LAST_RUNTIME_SAVE_FEEDBACK_RENDER = null;
if (typeof activeRuntimeSaveFeedbacks !== 'undefined') activeRuntimeSaveFeedbacks = [];
const saved = quickSaveRuntime({ feedback: true, feedbackSource: 'title-start-smoke-quick-save-feedback' });
const payload = JSON.parse(localStorage.getItem(RUNTIME_SAVE_KEY) || 'null');
if (typeof render === 'function') render();
return {
  saved,
  payloadMap: payload?.map || '',
  tile: payload?.tile || null,
  dir: payload?.dir ?? null,
  hasRuntimeState: !!payload?.runtimeState,
  runtimeMoney: payload?.runtimeState?.money ?? null,
  runtimeItems: (payload?.runtimeState?.items || []).map((item) => `${item.key}:${item.count}`),
  runtimeCharacters: (payload?.runtimeState?.characters || []).map((character) => character.name),
  hasLoadedSaveSummary: !!payload?.loadedSaveSummary,
  loadedSaveSummary: payload?.loadedSaveSummary ? {
    fileName: payload.loadedSaveSummary.fileName || '',
    selector: `${payload.loadedSaveSummary.group}:${payload.loadedSaveSummary.slot}`,
    group: payload.loadedSaveSummary.group,
    slot: payload.loadedSaveSummary.slot,
    x: payload.loadedSaveSummary.x,
    y: payload.loadedSaveSummary.y,
    money: payload.loadedSaveSummary.money,
    fieldMaps: payload.loadedSaveSummary.fieldMaps || [],
    routeEvidence: payload.loadedSaveSummary.routeEvidence || null,
  } : null,
  routeState: payload?.routeState || null,
  fieldEncounter: payload?.fieldEncounter || null,
  runtimeSaveFeedbackLog: window.HWANSE_RUNTIME_SAVE_FEEDBACK_LOG || [],
  runtimeSaveFeedbackRender: window.HWANSE_RUNTIME_SAVE_FEEDBACK_RENDER || [],
  runtimeSaveFeedbackLast: window.HWANSE_LAST_RUNTIME_SAVE_FEEDBACK || null,
  runtimeSaveFeedbackLastRender: window.HWANSE_LAST_RUNTIME_SAVE_FEEDBACK_RENDER || null,
  playHudLines: typeof playHudLines === 'function' ? playHudLines() : (window.HWANSE_LAST_PLAY_HUD_LINES || []),
  saveHudLines: window.HWANSE_LAST_SAVE_HUD_LINES || [],
};
"""


def field_encounter_step_autosave_script() -> str:
    return """
window.__hwanseFieldEncounterStepAutoSave = null;
window.HWANSE_FIELD_ENCOUNTER_FEEDBACK_LOG = [];
window.HWANSE_FIELD_ENCOUNTER_FEEDBACK_RENDER = [];
window.HWANSE_LAST_FIELD_ENCOUNTER_FEEDBACK = null;
window.HWANSE_LAST_FIELD_ENCOUNTER_FEEDBACK_RENDER = null;
if (typeof activeFieldEncounterFeedbacks !== 'undefined') activeFieldEncounterFeedbacks = [];
Promise.all([ensureBattleData()])
  .then(() => {
    const before = {
      scene,
      mapName: map?.name || '',
      foot: footTile(),
      fieldEncounter: fieldEncounterSavePayload(),
      playHudLines: playHudLines(),
      action: battleCandidateActionState(),
    };
    const inputCode = 'ArrowRight';
    const inputKey = 'ArrowRight';
    window.__hwanseFieldEncounterStepAutoSave = {
      pending: true,
      movementInput: true,
      inputCode,
      before,
    };
    window.dispatchEvent(new KeyboardEvent('keydown', {
      bubbles: true,
      cancelable: true,
      code: inputCode,
      key: inputKey,
    }));
    window.setTimeout(() => {
      window.dispatchEvent(new KeyboardEvent('keyup', {
        bubbles: true,
        cancelable: true,
        code: inputCode,
        key: inputKey,
      }));
    }, 80);
    const startedAt = performance.now();
    const poll = () => {
      const step = window.HWANSE_LAST_FIELD_ENCOUNTER_STEP || null;
      const autoSave = window.HWANSE_LAST_FIELD_ENCOUNTER_STEP_AUTO_SAVE || null;
      const payload = JSON.parse(localStorage.getItem(RUNTIME_SAVE_KEY) || 'null');
      const afterFoot = footTile();
      const tileChanged = before.foot?.x !== afterFoot?.x || before.foot?.y !== afterFoot?.y;
      if ((autoSave?.source === 'field-encounter-step' && !player.step) || performance.now() - startedAt > 2500) {
        if (typeof render === 'function') render();
        window.__hwanseFieldEncounterStepAutoSave = {
          pending: false,
          movementInput: true,
          inputCode,
          before,
          movement: {
            tileChanged,
            beforeFoot: before.foot || null,
            afterFoot,
            moving: !!player.moving,
            activeStep: !!player.step,
          },
          step,
          autoSave,
          savedPayload: payload,
          fieldEncounter: fieldEncounterSavePayload(),
          fieldEncounterMenuLabel: fieldEncounterMenuLabel(),
          encounterFeedbackLog: window.HWANSE_FIELD_ENCOUNTER_FEEDBACK_LOG || [],
          encounterFeedbackRender: window.HWANSE_FIELD_ENCOUNTER_FEEDBACK_RENDER || [],
          encounterFeedbackLast: window.HWANSE_LAST_FIELD_ENCOUNTER_FEEDBACK || null,
          encounterFeedbackLastRender: window.HWANSE_LAST_FIELD_ENCOUNTER_FEEDBACK_RENDER || null,
          quickLoadHidden: document.getElementById('quickLoadButton')?.hidden ?? null,
          quickLoadText: document.getElementById('quickLoadButton')?.textContent || '',
          playHudLines: playHudLines(),
        };
      } else {
        window.setTimeout(poll, 50);
      }
    };
    window.setTimeout(poll, 120);
  })
  .catch((error) => {
    window.__hwanseFieldEncounterStepAutoSave = { error: String(error && error.message || error) };
  });
return true;
"""


def field_encounter_step_autosave_state_script() -> str:
    return "return window.__hwanseFieldEncounterStepAutoSave || null;"


def field_encounter_disable_autosave_script() -> str:
    return """
window.__hwanseFieldEncounterDisableAutoSave = null;
window.HWANSE_FIELD_ENCOUNTER_FEEDBACK_LOG = [];
window.HWANSE_FIELD_ENCOUNTER_FEEDBACK_RENDER = [];
window.HWANSE_LAST_FIELD_ENCOUNTER_FEEDBACK = null;
window.HWANSE_LAST_FIELD_ENCOUNTER_FEEDBACK_RENDER = null;
if (typeof activeFieldEncounterFeedbacks !== 'undefined') activeFieldEncounterFeedbacks = [];
const before = {
  scene,
  mapName: map?.name || '',
  foot: footTile(),
  search: location.search,
  fieldEncounter: fieldEncounterSavePayload(),
  fieldEncounterMenuLabel: fieldEncounterMenuLabel(),
  playHudLines: playHudLines(),
};
menuOpen = true;
menuMode = 'main';
selectedMenuItemIndex = 0;
const items = menuItems();
const index = items.findIndex((item) => item.command === 'toggleFieldEncounters');
if (index < 0) {
  window.__hwanseFieldEncounterDisableAutoSave = {
    error: 'missing-toggleFieldEncounters',
    before,
    labels: items.map((item) => menuItemLabel(item)),
  };
  return true;
}
selectedMenuItemIndex = index;
const labelBefore = menuItemLabel(items[index]);
const commandResult = useSelectedMenuItem();
if (typeof render === 'function') render();
const payload = JSON.parse(localStorage.getItem(RUNTIME_SAVE_KEY) || 'null');
const nextItems = menuItems();
const nextItem = nextItems.find((item) => item.command === 'toggleFieldEncounters') || null;
window.__hwanseFieldEncounterDisableAutoSave = {
  commandResult,
  before,
  index,
  labelBefore,
  labelAfter: nextItem ? menuItemLabel(nextItem) : '',
  mode: window.HWANSE_LAST_FIELD_ENCOUNTER_MODE || null,
  autoSave: window.HWANSE_LAST_FIELD_ENCOUNTER_MODE_AUTO_SAVE || window.HWANSE_LAST_FIELD_ENCOUNTER_MODE?.autoSave || null,
  savedPayload: payload,
  fieldEncounter: fieldEncounterSavePayload(),
  fieldEncounterMenuLabel: fieldEncounterMenuLabel(),
  menuNotice,
  search: location.search,
  encounterFeedbackLog: window.HWANSE_FIELD_ENCOUNTER_FEEDBACK_LOG || [],
  encounterFeedbackRender: window.HWANSE_FIELD_ENCOUNTER_FEEDBACK_RENDER || [],
  encounterFeedbackLast: window.HWANSE_LAST_FIELD_ENCOUNTER_FEEDBACK || null,
  encounterFeedbackLastRender: window.HWANSE_LAST_FIELD_ENCOUNTER_FEEDBACK_RENDER || null,
  quickLoadHidden: document.getElementById('quickLoadButton')?.hidden ?? null,
  quickLoadText: document.getElementById('quickLoadButton')?.textContent || '',
  playHudLines: playHudLines(),
};
return true;
"""


def field_encounter_disable_autosave_state_script() -> str:
    return "return window.__hwanseFieldEncounterDisableAutoSave || null;"


def play_start_target_field_encounter_script() -> str:
    return """
window.__hwansePlayStartTargetFieldEncounter = null;
Promise.all([ensureBattleData()])
  .then(() => {
    const before = {
      scene,
      mapName: map?.name || '',
      foot: footTile(),
      routeState: runtimeRouteStateSavePayload(),
      fieldEncounter: fieldEncounterSavePayload(),
      playHudLines: playHudLines(),
      action: battleCandidateActionState(),
    };
    const movementInputs = [
      { code: 'ArrowRight', key: 'ArrowRight', dx: 1, dy: 0 },
      { code: 'ArrowLeft', key: 'ArrowLeft', dx: -1, dy: 0 },
      { code: 'ArrowUp', key: 'ArrowUp', dx: 0, dy: -1 },
      { code: 'ArrowDown', key: 'ArrowDown', dx: 0, dy: 1 },
    ];
    const selectedInput = movementInputs
      .map((input) => ({ ...input, movement: chooseMovementStep(before.foot, input.dx, input.dy) }))
      .find((input) => input.movement);
    if (!selectedInput) {
      window.__hwansePlayStartTargetFieldEncounter = {
        error: 'missing-passable-target-movement',
        before,
      };
      return;
    }
    fieldEncounterState = createFieldEncounterState({
      enabled: true,
      stepCount: FIELD_ENCOUNTER_STEP_THRESHOLD - 1,
      lastMap: map?.name || '',
    });
    syncMapQuery();
    updateBattleButton();
    refreshPlayHudLines();
    const armed = {
      fieldEncounter: fieldEncounterSavePayload(),
      search: location.search,
      buttonText: document.getElementById('battleButton')?.textContent || '',
      playHudLines: playHudLines(),
      action: battleCandidateActionState(),
    };
    window.__hwansePlayStartTargetFieldEncounter = {
      pending: true,
      movementInput: true,
      inputCode: selectedInput.code,
      before,
      armed,
      plannedMovement: {
        nextTile: selectedInput.movement.nextTile || null,
        target: selectedInput.movement.target || null,
      },
    };
    window.dispatchEvent(new KeyboardEvent('keydown', {
      bubbles: true,
      cancelable: true,
      code: selectedInput.code,
      key: selectedInput.key,
    }));
    window.setTimeout(() => {
      window.dispatchEvent(new KeyboardEvent('keyup', {
        bubbles: true,
        cancelable: true,
        code: selectedInput.code,
        key: selectedInput.key,
      }));
    }, 80);
    const startedAt = performance.now();
    const finish = (started, movement) => {
        const battleStartSnapshot = {
          scene,
          mapName: map?.name || '',
          summary: window.HWANSE_LAST_BATTLE_PROTOTYPE || null,
          battleCandidate: battleState?.candidate ? {
            id: battleState.candidate.id || '',
            blockId: battleState.candidate.blockId || '',
            battleBackground: battleState.candidate.battleBackground || '',
          } : null,
        };
        let victoryResult = null;
        let closeResult = null;
        let victorySummary = null;
        if (started && battleState) {
          battleState.enemy.hp = 1;
          victoryResult = useSelectedBattleCommand();
          victorySummary = window.HWANSE_LAST_BATTLE_PROTOTYPE || null;
          closeResult = useSelectedBattleCommand();
          updateBattleButton();
        }
        const victoryAutoSave = window.HWANSE_LAST_FIELD_ENCOUNTER_VICTORY_SAVE || null;
        let savedPayload = null;
        try {
          savedPayload = JSON.parse(localStorage.getItem(RUNTIME_SAVE_KEY) || 'null');
        } catch (error) {
          savedPayload = null;
        }
        const herb = (runtimeState?.items || []).find((item) => item.key === 'herb') || null;
        window.__hwansePlayStartTargetFieldEncounter = {
          pending: false,
          started,
          before,
          armed,
          movementInput: true,
          inputCode: selectedInput.code,
          plannedMovement: {
            nextTile: selectedInput.movement.nextTile || null,
            target: selectedInput.movement.target || null,
          },
          movement,
          startSnapshot: battleStartSnapshot,
          victoryResult,
          closeResult,
          victorySummary,
          victoryAutoSave,
          savedPayload,
          scene,
          mapName: map?.name || '',
          foot: footTile(),
          routeState: runtimeRouteStateSavePayload(),
          fieldEncounter: fieldEncounterSavePayload(),
          progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
          completion: publishPrototypeCompletionState(),
          buttonText: document.getElementById('battleButton')?.textContent || '',
          quickLoadHidden: document.getElementById('quickLoadButton')?.hidden ?? null,
          quickLoadText: document.getElementById('quickLoadButton')?.textContent || '',
          playHudLines: playHudLines(),
          runtimeState: runtimeState ? {
            money: runtimeState.money || 0,
            herbCount: herb?.count ?? null,
            items: (runtimeState.items || []).map((item) => `${item.key}:${item.count || 0}`),
          } : null,
          originalRoutePromotionImplemented: false,
          originalEncounterRuntimeImplemented: false,
        };
    };
    const poll = () => {
      const afterFoot = footTile();
      const movement = {
        tileChanged: before.foot?.x !== afterFoot?.x || before.foot?.y !== afterFoot?.y,
        beforeFoot: before.foot || null,
        afterFoot,
        moving: !!player.moving,
        activeStep: !!player.step,
        scene,
      };
      if (scene === 'battle' && battleState) {
        finish(true, movement);
      } else if (performance.now() - startedAt > 3500) {
        finish(false, movement);
      } else {
        window.setTimeout(poll, 50);
      }
    };
    window.setTimeout(poll, 120);
  })
  .catch((error) => {
    window.__hwansePlayStartTargetFieldEncounter = { error: String(error && error.message || error) };
  });
return true;
"""


def play_start_target_field_encounter_state_script() -> str:
    return "return window.__hwansePlayStartTargetFieldEncounter || null;"


def new_game_item_use_script() -> str:
    return """
window.__hwanseNewGameItemUse = null;
window.HWANSE_INVENTORY_ITEM_FEEDBACK_LOG = [];
window.HWANSE_INVENTORY_ITEM_FEEDBACK_RENDER = [];
window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK = null;
window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK_RENDER = null;
window.HWANSE_SOUND_COUNTS = {};
window.HWANSE_SOUND_LOG = [];
window.HWANSE_LAST_SOUND = null;
if (typeof activeInventoryItemFeedbacks !== 'undefined') activeInventoryItemFeedbacks = [];
const character = (runtimeState?.characters || []).find((row) => row.name === 'Ataho');
const herb = (runtimeState?.items || []).find((row) => row.key === 'herb');
if (!character || !herb) {
  window.__hwanseNewGameItemUse = {
    error: 'missing-new-game-runtime-state',
    characters: (runtimeState?.characters || []).map((row) => row.name),
    items: (runtimeState?.items || []).map((row) => `${row.key}:${row.count}`),
  };
  return true;
}
const selectRuntimeInventoryItemFromMenu = (itemKey) => {
  menuOpen = true;
  menuMode = 'main';
  pendingInventoryItem = null;
  selectedMenuItemIndex = 0;
  const mainItems = menuItems();
  const inventoryMenuIndex = mainItems.findIndex((item) => item.command === 'openInventoryItemMenu');
  if (inventoryMenuIndex < 0) {
    return {
      ok: false,
      error: 'missing-inventory-item-menu-command',
      labels: mainItems.map((item) => menuItemLabel(item)),
    };
  }
  selectedMenuItemIndex = inventoryMenuIndex;
  const menuOpenResult = useSelectedMenuItem();
  const inventoryOpenMarker = window.HWANSE_LAST_INVENTORY_ITEM_SELECT_MENU || null;
  const inventoryItems = menuItems();
  const itemIndex = inventoryItems.findIndex((item) => !item.command && item.key === itemKey);
  if (itemIndex < 0) {
    return {
      ok: false,
      error: `missing-inventory-item:${itemKey}`,
      menuOpenResult,
      inventoryOpenMarker,
      labels: inventoryItems.map((item) => menuItemLabel(item)),
      menuMode,
    };
  }
  selectedMenuItemIndex = itemIndex;
  const item = inventoryItems[itemIndex];
  const result = useSelectedMenuItem();
  return {
    ok: true,
    result,
    menuOpenResult,
    inventoryOpenMarker,
    inventorySelectionMarker: window.HWANSE_LAST_INVENTORY_ITEM_MENU_SELECTION || null,
    inventoryLabels: inventoryItems.map((entry) => menuItemLabel(entry)),
    itemIndex,
    label: menuItemLabel(item),
  };
};
const runNoTargetFailure = () => {
  character.hp = Math.max(1, Number(character.hpMax || 36));
  herb.count = 1;
  window.HWANSE_INVENTORY_ITEM_FEEDBACK_LOG = [];
  window.HWANSE_INVENTORY_ITEM_FEEDBACK_RENDER = [];
  window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK = null;
  window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK_RENDER = null;
  window.HWANSE_LAST_INVENTORY_ITEM_FAILURE = null;
  window.HWANSE_LAST_INVENTORY_ITEM_USE = null;
  window.HWANSE_LAST_INVENTORY_ITEM_USE_AUTO_SAVE = null;
  window.HWANSE_SOUND_COUNTS = {};
  window.HWANSE_SOUND_LOG = [];
  window.HWANSE_LAST_SOUND = null;
  if (typeof activeInventoryItemFeedbacks !== 'undefined') activeInventoryItemFeedbacks = [];
  const selection = selectRuntimeInventoryItemFromMenu('herb');
  if (!selection.ok) return selection;
  const result = selection.result;
  if (typeof render === 'function') render();
  return {
    result,
    inventoryOpenMarker: selection.inventoryOpenMarker,
    inventorySelectionMarker: selection.inventorySelectionMarker,
    inventoryLabels: selection.inventoryLabels,
    notice: menuNotice,
    hp: character.hp,
    herbCount: herb.count,
    failure: window.HWANSE_LAST_INVENTORY_ITEM_FAILURE || null,
    autoSave: window.HWANSE_LAST_INVENTORY_ITEM_USE_AUTO_SAVE || null,
    progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
    itemFeedbackLog: window.HWANSE_INVENTORY_ITEM_FEEDBACK_LOG || [],
    itemFeedbackRender: window.HWANSE_INVENTORY_ITEM_FEEDBACK_RENDER || [],
    itemFeedbackLast: window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK || null,
    itemFeedbackLastRender: window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK_RENDER || null,
    soundState: {
      counts: { ...(window.HWANSE_SOUND_COUNTS || {}) },
      last: window.HWANSE_LAST_SOUND || null,
      log: Array.isArray(window.HWANSE_SOUND_LOG) ? [...window.HWANSE_SOUND_LOG] : [],
    },
  };
};
const noTargetFailure = runNoTargetFailure();
const runNoStatusFailure = () => {
  const antidote = (runtimeState?.items || []).find((row) => row.key === 'item_2');
  if (!antidote) return { error: 'missing-antidote-runtime-item' };
  character.statuses = [];
  character.hp = Math.max(1, Number(character.hpMax || 36));
  antidote.count = 1;
  window.HWANSE_INVENTORY_ITEM_FEEDBACK_LOG = [];
  window.HWANSE_INVENTORY_ITEM_FEEDBACK_RENDER = [];
  window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK = null;
  window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK_RENDER = null;
  window.HWANSE_LAST_INVENTORY_ITEM_FAILURE = null;
  window.HWANSE_LAST_INVENTORY_ITEM_USE = null;
  window.HWANSE_LAST_INVENTORY_ITEM_USE_AUTO_SAVE = null;
  window.HWANSE_SOUND_COUNTS = {};
  window.HWANSE_SOUND_LOG = [];
  window.HWANSE_LAST_SOUND = null;
  if (typeof activeInventoryItemFeedbacks !== 'undefined') activeInventoryItemFeedbacks = [];
  const selection = selectRuntimeInventoryItemFromMenu('item_2');
  if (!selection.ok) return selection;
  const result = selection.result;
  if (typeof render === 'function') render();
  return {
    result,
    inventoryOpenMarker: selection.inventoryOpenMarker,
    inventorySelectionMarker: selection.inventorySelectionMarker,
    inventoryLabels: selection.inventoryLabels,
    notice: menuNotice,
    hp: character.hp,
    statuses: character.statuses || [],
    antidoteCount: antidote.count,
    failure: window.HWANSE_LAST_INVENTORY_ITEM_FAILURE || null,
    autoSave: window.HWANSE_LAST_INVENTORY_ITEM_USE_AUTO_SAVE || null,
    progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
    itemFeedbackLog: window.HWANSE_INVENTORY_ITEM_FEEDBACK_LOG || [],
    itemFeedbackRender: window.HWANSE_INVENTORY_ITEM_FEEDBACK_RENDER || [],
    itemFeedbackLast: window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK || null,
    itemFeedbackLastRender: window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK_RENDER || null,
    soundState: {
      counts: { ...(window.HWANSE_SOUND_COUNTS || {}) },
      last: window.HWANSE_LAST_SOUND || null,
      log: Array.isArray(window.HWANSE_SOUND_LOG) ? [...window.HWANSE_SOUND_LOG] : [],
    },
  };
};
const noStatusFailure = runNoStatusFailure();
const antidoteAfterFailure = (runtimeState?.items || []).find((row) => row.key === 'item_2');
if (antidoteAfterFailure) antidoteAfterFailure.count = 0;
window.HWANSE_INVENTORY_ITEM_FEEDBACK_LOG = [];
window.HWANSE_INVENTORY_ITEM_FEEDBACK_RENDER = [];
window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK = null;
window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK_RENDER = null;
window.HWANSE_SOUND_COUNTS = {};
window.HWANSE_SOUND_LOG = [];
window.HWANSE_LAST_SOUND = null;
if (typeof activeInventoryItemFeedbacks !== 'undefined') activeInventoryItemFeedbacks = [];
character.hp = Math.max(1, Math.max(1, Number(character.hpMax || 36)) - 12);
herb.count = 1;
menuOpen = true;
menuMode = 'main';
selectedMenuItemIndex = 0;
const itemSelection = selectRuntimeInventoryItemFromMenu('herb');
if (!itemSelection.ok) {
  window.__hwanseNewGameItemUse = {
    ...itemSelection,
  };
  return true;
}
const commandResult = itemSelection.result;
if (typeof render === 'function') render();
const itemFeedbackLog = window.HWANSE_INVENTORY_ITEM_FEEDBACK_LOG || [];
const itemFeedbackRender = window.HWANSE_INVENTORY_ITEM_FEEDBACK_RENDER || [];
const itemFeedbackLast = window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK || null;
const itemFeedbackLastRender = window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK_RENDER || null;
const soundState = {
  counts: { ...(window.HWANSE_SOUND_COUNTS || {}) },
  last: window.HWANSE_LAST_SOUND || null,
  log: Array.isArray(window.HWANSE_SOUND_LOG) ? [...window.HWANSE_SOUND_LOG] : [],
};
const marker = window.HWANSE_LAST_INVENTORY_ITEM_USE || null;
const autoSave = window.HWANSE_LAST_INVENTORY_ITEM_USE_AUTO_SAVE || marker?.autoSave || null;
const payloadBeforeMutation = JSON.parse(localStorage.getItem(RUNTIME_SAVE_KEY) || 'null');
character.hp = 1;
herb.count = 9;
quickLoadRuntime()
  .then((loaded) => {
    const restoredCharacter = (runtimeState?.characters || []).find((row) => row.name === 'Ataho') || {};
    const restoredHerb = (runtimeState?.items || []).find((row) => row.key === 'herb') || {};
    if (typeof activeDialogue !== 'undefined') activeDialogue = null;
    if (typeof menuOpen !== 'undefined') menuOpen = false;
    window.HWANSE_INVENTORY_ITEM_FEEDBACK_LOG = [];
    window.HWANSE_INVENTORY_ITEM_FEEDBACK_RENDER = [];
    window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK = null;
    window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK_RENDER = null;
    window.HWANSE_SOUND_COUNTS = {};
    window.HWANSE_SOUND_LOG = [];
    window.HWANSE_LAST_SOUND = null;
    if (typeof activeInventoryItemFeedbacks !== 'undefined') activeInventoryItemFeedbacks = [];
    window.HWANSE_LAST_OBJECTIVE_ACTION = null;
    window.HWANSE_LAST_ITEM_USE_COMPLETION_NOTICE = null;
    const objectiveBefore = typeof prototypeObjectiveState === 'function' ? prototypeObjectiveState() : null;
    const objectiveResult = typeof activatePrototypeObjectiveAction === 'function'
      ? activatePrototypeObjectiveAction()
      : false;
    if (typeof render === 'function') render();
    const objectiveAction = window.HWANSE_LAST_OBJECTIVE_ACTION || null;
    const objectiveCompletionNotice = window.HWANSE_LAST_ITEM_USE_COMPLETION_NOTICE || null;
    const objectiveItemFeedbackLog = window.HWANSE_INVENTORY_ITEM_FEEDBACK_LOG || [];
    const objectiveItemFeedbackRender = window.HWANSE_INVENTORY_ITEM_FEEDBACK_RENDER || [];
    const objectiveItemFeedbackLast = window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK || null;
    const objectiveItemFeedbackLastRender = window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK_RENDER || null;
    const objectiveSoundState = {
      counts: { ...(window.HWANSE_SOUND_COUNTS || {}) },
      last: window.HWANSE_LAST_SOUND || null,
      log: Array.isArray(window.HWANSE_SOUND_LOG) ? [...window.HWANSE_SOUND_LOG] : [],
    };
    const objectiveActiveDialogueBlock = activeDialogue
      ? {
          blockId: activeDialogue.block?.blockId || '',
          line: activeDialogue.lines?.[activeDialogue.index || 0] || '',
          lineCount: activeDialogue.lines?.length || 0,
        }
      : null;
    if (typeof activeDialogue !== 'undefined') activeDialogue = null;
    window.__hwanseNewGameItemUse = {
      commandResult,
      saveResult: autoSave?.saved === true,
      autoSave,
      loaded,
      noTargetFailure,
      noStatusFailure,
      marker,
      itemFeedbackLog,
      itemFeedbackRender,
      itemFeedbackLast,
      itemFeedbackLastRender,
      soundState,
      menuNotice,
      scene,
      mapName: map?.name || '',
      itemLabel: itemSelection.label,
      inventoryOpenMarker: itemSelection.inventoryOpenMarker,
      inventorySelectionMarker: itemSelection.inventorySelectionMarker,
      inventoryLabels: itemSelection.inventoryLabels,
      progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
      payloadBeforeMutation,
      restoredCharacter,
      restoredHerb,
      objectiveBefore,
      objectiveResult,
      objectiveAction,
      objectiveCompletionNotice,
      objectiveItemFeedbackLog,
      objectiveItemFeedbackRender,
      objectiveItemFeedbackLast,
      objectiveItemFeedbackLastRender,
      objectiveSoundState,
      objectiveActiveDialogueBlock,
      hasLoadedSaveSummary: !!loadedSaveSummary,
      playHudLines: window.HWANSE_LAST_PLAY_HUD_LINES || [],
    };
  })
  .catch((error) => {
    window.__hwanseNewGameItemUse = { error: String(error && error.message || error) };
  });
return true;
"""


def new_game_item_use_state_script() -> str:
    return "return window.__hwanseNewGameItemUse || null;"


def field_item_target_selection_script() -> str:
    return """
window.__hwanseFieldItemTargetSelection = null;
localStorage.removeItem(RUNTIME_SAVE_KEY);
manualPartyModeIndex = partyModeIndexForNames(['rinshan', 'smash']);
manualPartyMembers = partyMembersForMode(manualPartyModeIndex);
runtimeState = createPrototypeRuntimeState({
  money: 60,
  items: [{ key: 'herb', name: '약초', count: 2 }],
  characters: [
    { key: 'ataho', name: 'Ataho', level: 1, hp: 24, hpMax: 36, mp: 8, mpMax: 8, statuses: [] },
    { key: 'rinshan', name: 'Rinshan', level: 1, hp: 8, hpMax: 38, mp: 6, mpMax: 12, statuses: [] },
    { key: 'smash', name: 'Smashu', level: 1, hp: 42, hpMax: 42, mp: 5, mpMax: 5, statuses: [] },
  ],
});
runtimeState.characters = [
  { key: 'ataho', name: 'Ataho', level: 1, hp: 24, hpMax: 36, mp: 8, mpMax: 8, statuses: [] },
  { key: 'rinshan', name: 'Rinshan', level: 1, hp: 8, hpMax: 38, mp: 6, mpMax: 12, statuses: [] },
  { key: 'smash', name: 'Smashu', level: 1, hp: 42, hpMax: 42, mp: 5, mpMax: 5, statuses: [] },
];
prototypeProgress = createPrototypeProgressState();
publishPrototypeProgress();
loadedSaveSummary = null;
window.HWANSE_INVENTORY_ITEM_TARGET_SELECTION_LOG = [];
window.HWANSE_LAST_INVENTORY_ITEM_TARGET_SELECTION = null;
window.HWANSE_INVENTORY_ITEM_FEEDBACK_LOG = [];
window.HWANSE_INVENTORY_ITEM_FEEDBACK_RENDER = [];
window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK = null;
window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK_RENDER = null;
window.HWANSE_LAST_INVENTORY_ITEM_USE = null;
window.HWANSE_LAST_INVENTORY_ITEM_USE_AUTO_SAVE = null;
window.HWANSE_LAST_INVENTORY_ITEM_SELECT_MENU = null;
window.HWANSE_LAST_INVENTORY_ITEM_MENU_SELECTION = null;
window.HWANSE_SOUND_COUNTS = {};
window.HWANSE_SOUND_LOG = [];
window.HWANSE_LAST_SOUND = null;
if (typeof activeInventoryItemFeedbacks !== 'undefined') activeInventoryItemFeedbacks = [];
menuOpen = true;
menuMode = 'main';
pendingInventoryItem = null;
selectedMenuItemIndex = 0;
const before = {
  characters: (runtimeState.characters || []).map((row) => ({
    name: row.name,
    hp: row.hp,
    hpMax: row.hpMax,
	  })),
	  herbCount: (runtimeState.items || []).find((row) => row.key === 'herb')?.count ?? null,
	};
	const mainItems = menuItems();
	const inventoryMenuIndex = mainItems.findIndex((item) => item.command === 'openInventoryItemMenu');
	if (inventoryMenuIndex < 0) {
	  window.__hwanseFieldItemTargetSelection = {
	    error: 'missing-inventory-item-menu-command',
	    labels: mainItems.map((item) => menuItemLabel(item)),
	  };
	  return true;
	}
	selectedMenuItemIndex = inventoryMenuIndex;
	const inventoryOpenResult = useSelectedMenuItem();
	const inventoryOpenMarker = window.HWANSE_LAST_INVENTORY_ITEM_SELECT_MENU || null;
	const inventoryItems = menuItems();
	const herbIndex = inventoryItems.findIndex((item) => !item.command && item.key === 'herb');
	if (herbIndex < 0) {
	  window.__hwanseFieldItemTargetSelection = {
	    error: 'missing-herb-menu-item',
	    inventoryOpenResult,
	    inventoryOpenMarker,
	    labels: inventoryItems.map((item) => menuItemLabel(item)),
	    menuMode,
	  };
	  return true;
	}
	selectedMenuItemIndex = herbIndex;
	const openResult = useSelectedMenuItem();
	const inventorySelectionMarker = window.HWANSE_LAST_INVENTORY_ITEM_MENU_SELECTION || null;
	const openMarker = window.HWANSE_LAST_INVENTORY_ITEM_TARGET_SELECTION || null;
	const targetItems = menuItems();
	const rinshanIndex = targetItems.findIndex((item) => item.command === 'selectInventoryItemTarget' && item.member?.name === 'Rinshan');
	if (rinshanIndex < 0) {
	  window.__hwanseFieldItemTargetSelection = {
	    error: 'missing-rinshan-target',
	    inventoryOpenResult,
	    inventoryOpenMarker,
	    inventorySelectionMarker,
	    openResult,
	    openMarker,
	    targetLabels: targetItems.map((item) => menuItemLabel(item)),
    menuMode,
    pendingInventoryItem,
  };
  return true;
}
selectedMenuItemIndex = rinshanIndex;
const selectResult = useSelectedMenuItem();
if (typeof render === 'function') render();
const marker = window.HWANSE_LAST_INVENTORY_ITEM_USE || null;
const autoSave = window.HWANSE_LAST_INVENTORY_ITEM_USE_AUTO_SAVE || marker?.autoSave || null;
const savedPayload = JSON.parse(localStorage.getItem(RUNTIME_SAVE_KEY) || 'null');
const rinshan = (runtimeState.characters || []).find((row) => row.name === 'Rinshan') || {};
const ataho = (runtimeState.characters || []).find((row) => row.name === 'Ataho') || {};
const herb = (runtimeState.items || []).find((row) => row.key === 'herb') || {};
	window.__hwanseFieldItemTargetSelection = {
	  inventoryOpenResult,
	  inventoryOpenMarker,
	  inventorySelectionMarker,
	  inventoryMenuLabels: inventoryItems.map((item) => menuItemLabel(item)),
	  openResult,
	  selectResult,
  before,
  openMarker,
  selectedMarker: window.HWANSE_LAST_INVENTORY_ITEM_TARGET_SELECTION || null,
  selectionLog: window.HWANSE_INVENTORY_ITEM_TARGET_SELECTION_LOG || [],
  marker,
  autoSave,
  savedPayload,
  menuState: {
    menuOpen,
    menuMode,
    pendingInventoryItem: pendingInventoryItem || null,
    selectedMenuItemIndex,
    notice: menuNotice,
  },
  after: {
    atahoHp: ataho.hp ?? null,
    rinshanHp: rinshan.hp ?? null,
    rinshanHpMax: rinshan.hpMax ?? null,
    herbCount: herb.count ?? null,
  },
  itemFeedbackLog: window.HWANSE_INVENTORY_ITEM_FEEDBACK_LOG || [],
  itemFeedbackRender: window.HWANSE_INVENTORY_ITEM_FEEDBACK_RENDER || [],
  itemFeedbackLast: window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK || null,
  itemFeedbackLastRender: window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK_RENDER || null,
  soundState: {
    counts: { ...(window.HWANSE_SOUND_COUNTS || {}) },
    last: window.HWANSE_LAST_SOUND || null,
    log: Array.isArray(window.HWANSE_SOUND_LOG) ? [...window.HWANSE_SOUND_LOG] : [],
  },
  progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
  scene,
  mapName: map?.name || '',
  loadedSaveSummary: loadedSaveSummary || null,
};
return true;
"""


def field_item_target_selection_state_script() -> str:
    return "return window.__hwanseFieldItemTargetSelection || null;"


def item_use_objective_capture_script() -> str:
    return """
if (typeof activeDialogue !== 'undefined') activeDialogue = null;
if (typeof menuOpen !== 'undefined') menuOpen = false;
window.HWANSE_INVENTORY_ITEM_FEEDBACK_LOG = [];
window.HWANSE_INVENTORY_ITEM_FEEDBACK_RENDER = [];
window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK = null;
window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK_RENDER = null;
window.HWANSE_SOUND_COUNTS = {};
window.HWANSE_SOUND_LOG = [];
window.HWANSE_LAST_SOUND = null;
if (typeof activeInventoryItemFeedbacks !== 'undefined') activeInventoryItemFeedbacks = [];
window.HWANSE_LAST_OBJECTIVE_ACTION = null;
window.HWANSE_LAST_ITEM_USE_COMPLETION_NOTICE = null;
const objectiveBefore = typeof prototypeObjectiveState === 'function' ? prototypeObjectiveState() : null;
const objectiveResult = typeof activatePrototypeObjectiveAction === 'function'
  ? activatePrototypeObjectiveAction()
  : false;
if (typeof render === 'function') render();
const objectiveAction = window.HWANSE_LAST_OBJECTIVE_ACTION || null;
const objectiveCompletionNotice = window.HWANSE_LAST_ITEM_USE_COMPLETION_NOTICE || null;
const objectiveItemFeedbackLog = window.HWANSE_INVENTORY_ITEM_FEEDBACK_LOG || [];
const objectiveItemFeedbackRender = window.HWANSE_INVENTORY_ITEM_FEEDBACK_RENDER || [];
const objectiveItemFeedbackLast = window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK || null;
const objectiveItemFeedbackLastRender = window.HWANSE_LAST_INVENTORY_ITEM_FEEDBACK_RENDER || null;
const objectiveSoundState = {
  counts: { ...(window.HWANSE_SOUND_COUNTS || {}) },
  last: window.HWANSE_LAST_SOUND || null,
  log: Array.isArray(window.HWANSE_SOUND_LOG) ? [...window.HWANSE_SOUND_LOG] : [],
};
const objectiveActiveDialogueBlock = activeDialogue
  ? {
      blockId: activeDialogue.block?.blockId || '',
      line: activeDialogue.lines?.[activeDialogue.index || 0] || '',
      lineCount: activeDialogue.lines?.length || 0,
    }
  : null;
if (typeof activeDialogue !== 'undefined') activeDialogue = null;
return {
  scene,
  mapName: map?.name || '',
  foot: typeof footTile === 'function' ? footTile() : null,
  objectiveBefore,
  objectiveResult,
  objectiveAction,
  objectiveCompletionNotice,
  objectiveItemFeedbackLog,
  objectiveItemFeedbackRender,
  objectiveItemFeedbackLast,
  objectiveItemFeedbackLastRender,
  objectiveSoundState,
  objectiveActiveDialogueBlock,
  progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
  completion: window.HWANSE_LAST_PROTOTYPE_COMPLETION || null,
  runtimeItems: (runtimeState?.items || []).map((item) => `${item.key}:${item.count}`),
  originalStoryFlagRuntimeImplemented: false,
};
"""


def verify_item_use_completion_objective(
    state: dict[str, Any],
    *,
    target_name: str = "Ataho",
    hp_before: int = 24,
    hp_after: int = 36,
    count_before: int = 1,
    count_after: int = 0,
) -> None:
    objective_before = state.get("objectiveBefore") or {}
    objective_action = state.get("objectiveAction") or {}
    objective_notice = state.get("objectiveCompletionNotice") or {}
    objective_dialogue = state.get("objectiveActiveDialogueBlock") or {}
    notice_lines = " ".join(objective_notice.get("lines") or [])
    completion = objective_notice.get("completion") or {}
    detail = f"{target_name} 약초"
    hp_line = f"HP {hp_before} -> {hp_after}"
    count_line = f"약초 {count_before} -> {count_after}"
    if (
        state.get("objectiveResult") is not True
        or objective_before.get("title") != "후보 도구 완료 map1_02b"
        or objective_before.get("nextAction") != "완료 알림 확인"
        or objective_before.get("detail") != detail
        or objective_before.get("source") != "prototype-item-use-completion"
        or objective_action.get("action") != "item-use-completion-notice"
        or objective_action.get("activeId") != "item-use-complete:map1_02b"
        or objective_action.get("prototypeItemUseImplemented") is not True
        or objective_action.get("originalItemEffectFormulaImplemented") is not False
        or objective_action.get("originalStoryFlagRuntimeImplemented") is not False
        or objective_notice.get("blockId") != "item-use-complete:map1_02b"
        or objective_notice.get("map") != "map1_02b"
        or "도구 사용 완료 1/1" not in notice_lines
        or detail not in notice_lines
        or hp_line not in notice_lines
        or count_line not in notice_lines
        or completion.get("completed") is not True
        or completion.get("eventCount") != 1
        or completion.get("itemName") != "약초"
        or completion.get("targetName") != target_name
        or completion.get("source") != "prototype-item-effect"
        or completion.get("prototypeItemUseImplemented") is not True
        or completion.get("originalItemEffectFormulaImplemented") is not False
        or completion.get("originalStoryFlagRuntimeImplemented") is not False
        or objective_notice.get("prototypeItemUseImplemented") is not True
        or objective_notice.get("originalItemEffectFormulaImplemented") is not False
        or objective_notice.get("originalStoryFlagRuntimeImplemented") is not False
        or objective_dialogue.get("blockId") != "item-use-complete:map1_02b"
        or objective_dialogue.get("line") != "도구 사용 완료 1/1"
        or objective_dialogue.get("lineCount", 0) < 5
    ):
        raise WebDriverError(f"unexpected item-use completion objective state: {state!r}")
    verify_item_use_completion_notice_feedback(
        state,
        target_name=target_name,
        hp_before=hp_before,
        hp_after=hp_after,
        count_before=count_before,
        count_after=count_after,
    )


def inventory_item_feedback_entries(state: dict[str, Any], *, rendered: bool = False) -> list[dict[str, Any]]:
    key = "itemFeedbackRender" if rendered else "itemFeedbackLog"
    return [entry for entry in (state.get(key) or []) if isinstance(entry, dict)]


def item_use_objective_feedback_entries(state: dict[str, Any], *, rendered: bool = False) -> list[dict[str, Any]]:
    key = "objectiveItemFeedbackRender" if rendered else "objectiveItemFeedbackLog"
    return [entry for entry in (state.get(key) or []) if isinstance(entry, dict)]


def verify_item_use_completion_notice_feedback(
    state: dict[str, Any],
    *,
    target_name: str = "Ataho",
    hp_before: int = 24,
    hp_after: int = 36,
    count_before: int = 1,
    count_after: int = 0,
) -> None:
    feedback_log = item_use_objective_feedback_entries(state)
    feedback_render_log = item_use_objective_feedback_entries(state, rendered=True)
    feedback = state.get("objectiveItemFeedbackLast") or {}
    feedback_render = state.get("objectiveItemFeedbackLastRender") or {}
    sound_state = state.get("objectiveSoundState") or {}
    sound_counts = sound_state.get("counts") or {}
    if (
        len(feedback_log) < 1
        or len(feedback_render_log) < 1
        or int(sound_counts.get("menuConfirm") or 0) < 1
        or feedback.get("source") != "inventory-item-completion-notice-feedback"
        or feedback.get("text") != "도구 사용 완료 1/1"
        or feedback.get("map") != "map1_02b"
        or feedback.get("itemKey") != "herb"
        or feedback.get("itemName") != "약초"
        or feedback.get("targetName") != target_name
        or feedback.get("hpBefore") != hp_before
        or feedback.get("hpAfter") != hp_after
        or feedback.get("countBefore") != count_before
        or feedback.get("countAfter") != count_after
        or feedback.get("inventoryItemSound") != "menuConfirm"
        or not str(feedback.get("inventoryItemSoundSrc") or "").endswith("/extract_wlk/04.wav")
        or feedback.get("inventoryItemSoundPlayed") is not True
        or feedback.get("durationMs") != 1100
        or feedback.get("browserInventoryItemFeedbackImplemented") is not True
        or feedback.get("prototypeItemUseImplemented") is not True
        or feedback.get("originalItemEffectFormulaImplemented") is not False
        or feedback.get("originalStoryFlagRuntimeImplemented") is not False
        or feedback_render.get("active") is not True
        or feedback_render.get("source") != "inventory-item-completion-notice-feedback"
        or feedback_render.get("text") != "도구 사용 완료 1/1"
        or feedback_render.get("map") != "map1_02b"
        or feedback_render.get("itemKey") != "herb"
        or feedback_render.get("itemName") != "약초"
        or feedback_render.get("targetName") != target_name
        or feedback_render.get("hpBefore") != hp_before
        or feedback_render.get("hpAfter") != hp_after
        or feedback_render.get("countBefore") != count_before
        or feedback_render.get("countAfter") != count_after
        or feedback_render.get("inventoryItemSound") != "menuConfirm"
        or not str(feedback_render.get("inventoryItemSoundSrc") or "").endswith("/extract_wlk/04.wav")
        or feedback_render.get("inventoryItemSoundPlayed") is not True
        or feedback_render.get("durationMs") != 1100
        or feedback_render.get("browserInventoryItemFeedbackImplemented") is not True
        or feedback_render.get("prototypeItemUseImplemented") is not True
        or feedback_render.get("originalItemEffectFormulaImplemented") is not False
        or feedback_render.get("originalStoryFlagRuntimeImplemented") is not False
    ):
        raise WebDriverError(f"item-use completion notice feedback is incomplete: {state!r}")


def item_use_completion_notice_feedback_summary(state: dict[str, Any]) -> str:
    feedback = state.get("objectiveItemFeedbackLast") or {}
    rendered = state.get("objectiveItemFeedbackLastRender") or {}
    return (
        f"itemNoticeFeedback={feedback.get('source')}:{feedback.get('text')} "
        f"itemNoticeFeedbackRender={rendered.get('active')} "
        f"itemNoticeSound={feedback.get('inventoryItemSound')}:"
        f"{feedback.get('inventoryItemSoundSrc')}:{feedback.get('inventoryItemSoundPlayed')}"
    )


def inventory_item_feedback_match(
    state: dict[str, Any],
    *,
    source: str,
    item_key: str,
    text: str,
    rendered: bool = False,
) -> dict[str, Any]:
    for entry in inventory_item_feedback_entries(state, rendered=rendered):
        if (
            entry.get("source") == source
            and entry.get("itemKey") == item_key
            and entry.get("text") == text
        ):
            return entry
    return {}


def inventory_item_feedback_summary(state: dict[str, Any]) -> str:
    return ",".join(
        f"{entry.get('source')}:{entry.get('itemKey')}:{entry.get('text')}"
        for entry in inventory_item_feedback_entries(state)
    )


def inventory_item_feedback_rendered(state: dict[str, Any]) -> bool:
    return any(
        entry.get("browserInventoryItemFeedbackImplemented") is True
        for entry in inventory_item_feedback_entries(state, rendered=True)
    )


def inventory_item_feedback_sound_summary(state: dict[str, Any]) -> str:
    feedback = state.get("itemFeedbackLast") or {}
    if not feedback:
        entries = inventory_item_feedback_entries(state)
        feedback = entries[-1] if entries else {}
    return ":".join(
        [
            str(feedback.get("inventoryItemSound") or ""),
            str(feedback.get("inventoryItemSoundSrc") or ""),
            str(feedback.get("inventoryItemSoundPlayed")),
        ]
    )


def verify_inventory_item_sound_state(state: dict[str, Any]) -> None:
    sound_state = state.get("soundState") or {}
    counts = sound_state.get("counts") or {}
    log = sound_state.get("log") or []
    last = sound_state.get("last") or {}
    if (
        int(counts.get("item") or 0) < 1
        or last.get("key") != "item"
        or not str(last.get("src") or "").endswith("/extract_wlk/11.wav")
        or not any(isinstance(entry, dict) and entry.get("key") == "item" for entry in log)
    ):
        raise WebDriverError(f"missing inventory WLK item sound state: {sound_state!r}")


def verify_inventory_item_feedback_sound_fields(feedback: dict[str, Any]) -> None:
    if (
        feedback.get("inventoryItemSound") != "item"
        or not str(feedback.get("inventoryItemSoundSrc") or "").endswith("/extract_wlk/11.wav")
        or feedback.get("inventoryItemSoundPlayed") is not True
    ):
        raise WebDriverError(f"missing inventory feedback WLK item sound marker: {feedback!r}")


def verify_inventory_item_failure_state(state: dict[str, Any]) -> None:
    failure = state.get("failure") or {}
    feedback = failure.get("itemFeedback") or {}
    progress_counts = ((state.get("progress") or {}).get("counts") or {})
    sound_counts = ((state.get("soundState") or {}).get("counts") or {})
    failure_feedback = inventory_item_feedback_match(
        state,
        source="inventory-item-failed-feedback",
        item_key="herb",
        text="약초 대상 없음",
    )
    failure_feedback_render = inventory_item_feedback_match(
        state,
        source="inventory-item-failed-feedback",
        item_key="herb",
        text="약초 대상 없음",
        rendered=True,
    )
    if (
        state.get("error") is not None
        or state.get("result") is not True
        or state.get("notice") != "약초 효과 대상이 없습니다."
        or state.get("hp") != 36
        or state.get("herbCount") != 1
        or state.get("autoSave") is not None
        or int(progress_counts.get("item-use-prototype") or 0) != 0
        or int(sound_counts.get("item") or 0) != 0
        or failure.get("failed") is not True
        or failure.get("failureReason") != "no-target"
        or failure.get("itemKey") != "herb"
        or failure.get("itemName") != "약초"
        or failure.get("countBefore") != 1
        or failure.get("countAfter") != 1
        or failure.get("targetName") != ""
        or failure.get("hpBefore") != 0
        or failure.get("hpAfter") != 0
        or failure.get("source") != "prototype-item-effect"
        or failure.get("originalItemEffectFormulaImplemented") is not False
        or failure.get("originalStoryFlagRuntimeImplemented") is not False
        or not item_text_provenance_matches(failure, "herb")
        or feedback.get("source") != "inventory-item-failed-feedback"
        or feedback.get("failed") is not True
        or feedback.get("failureReason") != "no-target"
        or feedback.get("text") != "약초 대상 없음"
        or failure_feedback.get("browserInventoryItemFeedbackImplemented") is not True
        or failure_feedback.get("failed") is not True
        or failure_feedback.get("failureReason") != "no-target"
        or failure_feedback.get("durationMs") != 1100
        or not item_text_provenance_matches(failure_feedback, "herb")
        or failure_feedback_render.get("browserInventoryItemFeedbackImplemented") is not True
        or failure_feedback_render.get("failed") is not True
        or failure_feedback_render.get("text") != "약초 대상 없음"
        or not item_text_provenance_matches(failure_feedback_render, "herb")
    ):
        raise WebDriverError(f"inventory item failure state is incomplete: {state!r}")


def verify_inventory_status_item_failure_state(state: dict[str, Any]) -> None:
    failure = state.get("failure") or {}
    feedback = failure.get("itemFeedback") or {}
    progress_counts = ((state.get("progress") or {}).get("counts") or {})
    sound_counts = ((state.get("soundState") or {}).get("counts") or {})
    failure_feedback = inventory_item_feedback_match(
        state,
        source="inventory-item-failed-feedback",
        item_key="item_2",
        text="해독초 회복 대상 없음",
    )
    failure_feedback_render = inventory_item_feedback_match(
        state,
        source="inventory-item-failed-feedback",
        item_key="item_2",
        text="해독초 회복 대상 없음",
        rendered=True,
    )
    if (
        state.get("error") is not None
        or state.get("result") is not True
        or state.get("notice") != "해독초 해독할 상태가 없습니다."
        or state.get("hp") != 36
        or state.get("statuses") != []
        or state.get("antidoteCount") != 1
        or state.get("autoSave") is not None
        or int(progress_counts.get("item-use-prototype") or 0) != 0
        or int(sound_counts.get("item") or 0) != 0
        or failure.get("failed") is not True
        or failure.get("failureReason") != "no-status"
        or failure.get("itemKey") != "item_2"
        or failure.get("itemName") != "해독초"
        or failure.get("countBefore") != 1
        or failure.get("countAfter") != 1
        or failure.get("source") != "prototype-item-effect"
        or failure.get("originalItemEffectFormulaImplemented") is not False
        or failure.get("originalStoryFlagRuntimeImplemented") is not False
        or not item_text_provenance_matches(failure, "item_2")
        or feedback.get("source") != "inventory-item-failed-feedback"
        or feedback.get("failed") is not True
        or feedback.get("failureReason") != "no-status"
        or feedback.get("text") != "해독초 회복 대상 없음"
        or failure_feedback.get("browserInventoryItemFeedbackImplemented") is not True
        or failure_feedback.get("failed") is not True
        or failure_feedback.get("failureReason") != "no-status"
        or failure_feedback.get("durationMs") != 1100
        or not item_text_provenance_matches(failure_feedback, "item_2")
        or failure_feedback_render.get("browserInventoryItemFeedbackImplemented") is not True
        or failure_feedback_render.get("failed") is not True
        or failure_feedback_render.get("text") != "해독초 회복 대상 없음"
        or not item_text_provenance_matches(failure_feedback_render, "item_2")
    ):
        raise WebDriverError(f"inventory status item failure state is incomplete: {state!r}")


def field_encounter_feedback_entries(state: dict[str, Any], *, rendered: bool = False) -> list[dict[str, Any]]:
    key = "encounterFeedbackRender" if rendered else "encounterFeedbackLog"
    return [entry for entry in (state.get(key) or []) if isinstance(entry, dict)]


def field_encounter_feedback_summary(state: dict[str, Any]) -> str:
    return ",".join(
        f"{entry.get('source')}:{entry.get('text')}"
        for entry in field_encounter_feedback_entries(state)
    )


def field_encounter_feedback_rendered(state: dict[str, Any]) -> bool:
    return any(
        entry.get("browserFieldEncounterFeedbackImplemented") is True
        for entry in field_encounter_feedback_entries(state, rendered=True)
    )


def field_encounter_feedback_sound_summary(state: dict[str, Any]) -> str:
    return ",".join(
        f"{entry.get('fieldEncounterSound')}:{entry.get('fieldEncounterSoundSrc')}:{entry.get('fieldEncounterSoundPlayed')}"
        for entry in field_encounter_feedback_entries(state)
        if entry.get("fieldEncounterSound")
    )


def runtime_save_feedback_entries(state: dict[str, Any], *, rendered: bool = False) -> list[dict[str, Any]]:
    key = "runtimeSaveFeedbackRender" if rendered else "runtimeSaveFeedbackLog"
    return [entry for entry in (state.get(key) or []) if isinstance(entry, dict)]


def runtime_save_feedback_summary(state: dict[str, Any]) -> str:
    return ",".join(
        f"{entry.get('source')}:{entry.get('text')}"
        for entry in runtime_save_feedback_entries(state)
    )


def runtime_save_feedback_rendered(state: dict[str, Any]) -> bool:
    return any(
        entry.get("browserRuntimeSaveFeedbackImplemented") is True
        for entry in runtime_save_feedback_entries(state, rendered=True)
    )


def runtime_save_feedback_sound_summary(state: dict[str, Any]) -> str:
    return ",".join(
        f"{entry.get('runtimeSaveSound')}:{entry.get('runtimeSaveSoundSrc')}:{entry.get('runtimeSaveSoundPlayed')}"
        for entry in runtime_save_feedback_entries(state)
        if entry.get("runtimeSaveSound")
    )


def runtime_save_feedback_has_menu_confirm_sound(entry: dict[str, Any]) -> bool:
    return (
        entry.get("runtimeSaveSound") == "menuConfirm"
        and entry.get("runtimeSaveSoundSrc") == "../extract_wlk/04.wav"
        and entry.get("runtimeSaveSoundPlayed") is True
    )


def map_transition_feedback_entries(state: dict[str, Any], *, rendered: bool = False) -> list[dict[str, Any]]:
    key = "mapTransitionFeedbackRender" if rendered else "mapTransitionFeedbackLog"
    return [entry for entry in (state.get(key) or []) if isinstance(entry, dict)]


def has_trial_blocker_fields(entry: dict[str, Any]) -> bool:
    return (
        entry.get("blockReasons") == TRIAL_BLOCK_REASONS
        and entry.get("blockReasonCount") == len(TRIAL_BLOCK_REASONS)
        and entry.get("stepBlockerText") == f"trial-only: {TRIAL_BLOCKER_FULL}"
        and entry.get("routeBlockerShortText") == TRIAL_BLOCKER_SHORT
        and entry.get("strictHotspotStatus") == "blocked"
        and entry.get("promotionBlockSummary") == TRIAL_BLOCKER_FULL
        and entry.get("routePromotionStatus") == "trial-only"
    )


def map_transition_feedback_summary(state: dict[str, Any]) -> str:
    return ",".join(
        f"{entry.get('source')}:{entry.get('text')}"
        for entry in map_transition_feedback_entries(state)
    )


def map_transition_feedback_sound_summary(state: dict[str, Any]) -> str:
    return ",".join(
        f"{entry.get('transitionSound')}:{entry.get('transitionSoundSrc')}:{entry.get('transitionSoundPlayed')}"
        for entry in map_transition_feedback_entries(state)
        if entry.get("transitionSound")
    )


def map_transition_feedback_rendered(state: dict[str, Any]) -> bool:
    return any(
        entry.get("browserMapTransitionFeedbackImplemented") is True
        for entry in map_transition_feedback_entries(state, rendered=True)
    )


def wait_for_new_game_item_use(port: int, session_id: str) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, new_game_item_use_state_script(), timeout=3)
        last_state = state if isinstance(state, dict) else {}
        marker = last_state.get("marker") or {}
        auto_save = last_state.get("autoSave") or {}
        auto_save_state = auto_save.get("runtimeState") or {}
        auto_save_ataho = next((row for row in auto_save_state.get("characters") or [] if row.get("name") == "Ataho"), {})
        auto_save_herb = next((row for row in auto_save_state.get("items") or [] if row.get("key") == "herb"), {})
        auto_save_progress = auto_save.get("progress") or {}
        progress_detail = ((marker.get("progressEvent") or {}).get("detail") or {})
        progress = last_state.get("progress") or {}
        inventory_menu_marker = last_state.get("inventoryOpenMarker") or {}
        inventory_selection_marker = last_state.get("inventorySelectionMarker") or {}
        item_feedback = inventory_item_feedback_match(
            last_state,
            source="inventory-item-use",
            item_key="herb",
            text="약초 HP +12",
        )
        item_feedback_render = inventory_item_feedback_match(
            last_state,
            source="inventory-item-use",
            item_key="herb",
            text="약초 HP +12",
            rendered=True,
        )
        if (
            last_state
            and last_state.get("error") is None
            and last_state.get("commandResult") is True
            and last_state.get("saveResult") is True
            and auto_save.get("saved") is True
            and auto_save.get("source") == "item-use-prototype"
            and auto_save.get("payloadMap") == "map1_02b"
            and auto_save.get("itemKey") == "herb"
            and auto_save.get("countBefore") == 1
            and auto_save.get("countAfter") == 0
            and auto_save.get("hpBefore") == 24
            and auto_save.get("hpAfter") == 36
            and auto_save_state.get("money") == 60
            and auto_save_ataho.get("hp") == 36
            and auto_save_herb.get("count") == 0
            and (auto_save_progress.get("counts") or {}).get("item-use-prototype") == 1
            and auto_save.get("prototypeItemUseImplemented") is True
            and auto_save.get("originalItemEffectFormulaImplemented") is False
            and auto_save.get("originalStoryFlagRuntimeImplemented") is False
            and inventory_menu_marker.get("source") == "prototype-inventory-item-menu"
            and inventory_menu_marker.get("count") == 1
            and inventory_selection_marker.get("source") == "prototype-inventory-item-menu"
            and inventory_selection_marker.get("itemKey") == "herb"
            and inventory_selection_marker.get("countBefore") == 1
            and inventory_selection_marker.get("targetSelectionOpened") is False
            and inventory_selection_marker.get("afterMenuMode") == "main"
            and inventory_selection_marker.get("originalInventoryMenuRuntimeImplemented") is False
            and last_state.get("loaded") is True
            and marker.get("itemKey") == "herb"
            and marker.get("countBefore") == 1
            and marker.get("countAfter") == 0
            and marker.get("hpBefore") == 24
            and marker.get("hpAfter") == 36
            and item_text_provenance_matches(marker, "herb")
            and item_text_provenance_matches(auto_save, "herb")
            and item_text_provenance_matches(progress_detail, "herb")
            and item_feedback.get("browserInventoryItemFeedbackImplemented") is True
            and item_feedback.get("prototypeItemUseImplemented") is True
            and item_feedback.get("originalItemEffectFormulaImplemented") is False
            and item_feedback.get("originalStoryFlagRuntimeImplemented") is False
            and item_text_provenance_matches(item_feedback, "herb")
            and item_feedback.get("targetName") == "Ataho"
            and item_feedback.get("hpGain") == 12
            and item_feedback.get("mpGain") == 0
            and item_feedback.get("countBefore") == 1
            and item_feedback.get("countAfter") == 0
            and item_feedback.get("durationMs") == 1100
            and item_feedback_render.get("browserInventoryItemFeedbackImplemented") is True
            and item_text_provenance_matches(item_feedback_render, "herb")
            and item_feedback_render.get("text") == "약초 HP +12"
            and (progress.get("counts") or {}).get("item-use-prototype") == 1
            and (last_state.get("restoredCharacter") or {}).get("hp") == 36
            and (last_state.get("restoredHerb") or {}).get("count") == 0
        ):
            verify_inventory_item_failure_state(last_state.get("noTargetFailure") or {})
            verify_inventory_status_item_failure_state(last_state.get("noStatusFailure") or {})
            verify_inventory_item_sound_state(last_state)
            verify_inventory_item_feedback_sound_fields(item_feedback)
            verify_inventory_item_feedback_sound_fields(item_feedback_render)
            verify_item_use_completion_objective(last_state)
            return last_state
        time.sleep(0.2)
    raise WebDriverError(f"new-game item use did not save/restore: {last_state!r}")


def wait_for_field_item_target_selection(port: int, session_id: str) -> dict[str, Any]:
    deadline = time.monotonic() + 8
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, field_item_target_selection_state_script(), timeout=3)
        last_state = state if isinstance(state, dict) else {}
        marker = last_state.get("marker") or {}
        auto_save = last_state.get("autoSave") or {}
        saved_payload = last_state.get("savedPayload") or {}
        saved_runtime = saved_payload.get("runtimeState") or {}
        saved_rinshan = next((row for row in saved_runtime.get("characters") or [] if row.get("name") == "Rinshan"), {})
        saved_ataho = next((row for row in saved_runtime.get("characters") or [] if row.get("name") == "Ataho"), {})
        saved_herb = next((row for row in saved_runtime.get("items") or [] if row.get("key") == "herb"), {})
        progress_counts = ((last_state.get("progress") or {}).get("counts") or {})
        inventory_menu_marker = last_state.get("inventoryOpenMarker") or {}
        inventory_menu_choices = inventory_menu_marker.get("choices") or []
        inventory_selection_marker = last_state.get("inventorySelectionMarker") or {}
        open_marker = last_state.get("openMarker") or {}
        selected_marker = last_state.get("selectedMarker") or {}
        phases = [entry.get("phase") for entry in (last_state.get("selectionLog") or []) if isinstance(entry, dict)]
        after = last_state.get("after") or {}
        menu_state = last_state.get("menuState") or {}
        feedback = inventory_item_feedback_match(
            last_state,
            source="inventory-item-use",
            item_key="herb",
            text="약초 HP +30",
        )
        feedback_render = inventory_item_feedback_match(
            last_state,
            source="inventory-item-use",
            item_key="herb",
            text="약초 HP +30",
            rendered=True,
        )
        if (
            last_state
            and last_state.get("error") is None
            and last_state.get("inventoryOpenResult") is True
            and last_state.get("openResult") is True
            and last_state.get("selectResult") is True
            and last_state.get("scene") == "map"
            and last_state.get("mapName") == "map1_02b"
            and last_state.get("loadedSaveSummary") is None
            and inventory_menu_marker.get("source") == "prototype-inventory-item-menu"
            and inventory_menu_marker.get("count") == 1
            and inventory_menu_marker.get("prototypeInventoryItemMenuImplemented") is True
            and inventory_menu_marker.get("originalInventoryMenuRuntimeImplemented") is False
            and inventory_menu_marker.get("originalStoryFlagRuntimeImplemented") is False
            and len(inventory_menu_choices) == 1
            and (inventory_menu_choices[0] or {}).get("itemKey") == "herb"
            and (inventory_menu_choices[0] or {}).get("itemName") == "약초"
            and (inventory_menu_choices[0] or {}).get("count") == 2
            and (inventory_menu_choices[0] or {}).get("label") == "약초 x2"
            and inventory_selection_marker.get("source") == "prototype-inventory-item-menu"
            and inventory_selection_marker.get("itemKey") == "herb"
            and inventory_selection_marker.get("itemName") == "약초"
            and inventory_selection_marker.get("countBefore") == 2
            and inventory_selection_marker.get("label") == "약초 x2"
            and inventory_selection_marker.get("menuCandidateIndex") == 0
            and inventory_selection_marker.get("menuCandidateCount") == 1
            and inventory_selection_marker.get("targetCount") == 3
            and inventory_selection_marker.get("targetSelectionOpened") is True
            and inventory_selection_marker.get("opensMenuMode") == "item-target"
            and inventory_selection_marker.get("afterMenuMode") == "item-target"
            and inventory_selection_marker.get("prototypeInventoryItemMenuImplemented") is True
            and inventory_selection_marker.get("originalInventoryMenuRuntimeImplemented") is False
            and inventory_selection_marker.get("originalItemEffectFormulaImplemented") is False
            and inventory_selection_marker.get("originalStoryFlagRuntimeImplemented") is False
            and open_marker.get("source") == "inventory-item-target-selection"
            and open_marker.get("phase") == "open"
            and open_marker.get("itemKey") == "herb"
            and open_marker.get("targetCount") == 3
            and selected_marker.get("source") == "inventory-item-target-selection"
            and selected_marker.get("phase") == "selected"
            and selected_marker.get("selectedTargetName") == "Rinshan"
            and selected_marker.get("targetCount") == 3
            and selected_marker.get("prototypeInventoryItemTargetSelectionImplemented") is True
            and selected_marker.get("originalItemEffectFormulaImplemented") is False
            and phases[-2:] == ["open", "selected"]
            and marker.get("source") == "prototype-item-effect"
            and marker.get("itemKey") == "herb"
            and marker.get("itemName") == "약초"
            and marker.get("targetName") == "Rinshan"
            and marker.get("countBefore") == 2
            and marker.get("countAfter") == 1
            and marker.get("hpBefore") == 8
            and marker.get("hpAfter") == 38
            and marker.get("mpBefore") == 6
            and marker.get("mpAfter") == 6
            and item_text_provenance_matches(marker, "herb")
            and auto_save.get("saved") is True
            and auto_save.get("source") == "item-use-prototype"
            and auto_save.get("payloadMap") == "map1_02b"
            and auto_save.get("itemKey") == "herb"
            and auto_save.get("targetName") == "Rinshan"
            and auto_save.get("hpBefore") == 8
            and auto_save.get("hpAfter") == 38
            and auto_save.get("countBefore") == 2
            and auto_save.get("countAfter") == 1
            and item_text_provenance_matches(auto_save, "herb")
            and saved_payload.get("map") == "map1_02b"
            and saved_rinshan.get("hp") == 38
            and saved_ataho.get("hp") == 24
            and saved_herb.get("count") == 1
            and progress_counts.get("item-use-prototype") == 1
            and after.get("rinshanHp") == 38
            and after.get("atahoHp") == 24
            and after.get("herbCount") == 1
            and menu_state.get("menuOpen") is True
            and menu_state.get("menuMode") == "main"
            and menu_state.get("pendingInventoryItem") is None
            and feedback.get("browserInventoryItemFeedbackImplemented") is True
            and feedback.get("targetName") == "Rinshan"
            and feedback.get("hpBefore") == 8
            and feedback.get("hpAfter") == 38
            and feedback.get("hpGain") == 30
            and feedback.get("countBefore") == 2
            and feedback.get("countAfter") == 1
            and item_text_provenance_matches(feedback, "herb")
            and feedback_render.get("browserInventoryItemFeedbackImplemented") is True
            and feedback_render.get("targetName") == "Rinshan"
            and feedback_render.get("hpAfter") == 38
            and item_text_provenance_matches(feedback_render, "herb")
        ):
            verify_inventory_item_sound_state(last_state)
            verify_inventory_item_feedback_sound_fields(feedback)
            verify_inventory_item_feedback_sound_fields(feedback_render)
            return last_state
        time.sleep(0.2)
    raise WebDriverError(f"field item target selection did not finish: {last_state!r}")


def return_to_title_menu_command_script() -> str:
    return """
trialTransitions = 'routeAssist';
selectedRouteGoal = 'map2_02d';
runtimeState = { money: 999, items: [], characters: [] };
loadedSaveSummary = { fileName: 'stale savedat', group: 2, slot: 0, x: 9, y: 9, fieldMaps: ['map1_01a'] };
menuOpen = true;
menuMode = 'main';
const items = typeof menuItems === 'function' ? menuItems() : [];
const index = items.findIndex((item) => item.command === 'returnToTitle');
if (index < 0) {
  return {
    ok: false,
    reason: 'missing-returnToTitle',
    labels: items.map((item) => typeof menuItemLabel === 'function' ? menuItemLabel(item) : (item.name || item.key || '')),
  };
}
selectedMenuItemIndex = index;
const menuWindow = typeof runtimeMenuWindow === 'function' ? runtimeMenuWindow(items) : null;
const visibleItems = menuWindow ? items.slice(menuWindow.start, menuWindow.end) : [];
const geometry = RUNTIME_MENU_GEOMETRY;
const visibleIndex = index - menuWindow.start;
const canvas = document.getElementById('screen');
const rect = canvas.getBoundingClientRect();
const clientX = rect.left + ((geometry.x + geometry.itemX + geometry.itemWidth / 2) / canvas.width) * rect.width;
const clientY = rect.top + ((geometry.rowTop + visibleIndex * geometry.rowHeight + geometry.rowHeight / 2) / canvas.height) * rect.height;
const pointerHitIndex = typeof runtimeMenuItemIndexAtPoint === 'function'
  ? runtimeMenuItemIndexAtPoint(clientX, clientY)
  : null;
canvas.dispatchEvent(new PointerEvent('pointerdown', {
  bubbles: true,
  cancelable: true,
  pointerId: 37,
  pointerType: 'mouse',
  isPrimary: true,
  clientX,
  clientY,
}));
const titleMenu = typeof titleMenuItems === 'function' ? titleMenuItems() : [];
return {
  ok: scene === 'title',
  scene,
  search: location.search,
  menuOpen,
  menuMode,
  mapName: typeof map === 'undefined' || !map ? '' : map.name,
  quickLoadHidden: document.getElementById('quickLoadButton')?.hidden ?? null,
  quickLoadText: document.getElementById('quickLoadButton')?.textContent || '',
  trialTransitions: typeof trialTransitions === 'undefined' ? '' : trialTransitions,
  selectedRouteGoal: typeof selectedRouteGoal === 'undefined' ? '' : selectedRouteGoal,
  titleMenuKeys: titleMenu.map((item) => item.key),
  selectedTitleMenuIndex,
  commandIndex: index,
  menuWindow,
  pointerHitIndex,
  clickedClientX: clientX,
  clickedClientY: clientY,
  visibleLabels: visibleItems.map((item) => typeof menuItemLabel === 'function' ? menuItemLabel(item) : (item.name || item.key || '')),
  labels: items.map((item) => typeof menuItemLabel === 'function' ? menuItemLabel(item) : (item.name || item.key || '')),
};
"""


def runtime_submenu_cancel_script() -> str:
    return """
menuOpen = true;
menuMode = 'main';
selectedMenuItemIndex = 0;
menuNotice = '';
const opened = openShopBuyMenu();
const before = {
  opened,
  menuOpen,
  menuMode,
  selectedMenuItemIndex,
  labels: menuItems().map((item) => typeof menuItemLabel === 'function' ? menuItemLabel(item) : (item.name || item.key || '')),
};
window.dispatchEvent(new KeyboardEvent('keydown', {
  bubbles: true,
  cancelable: true,
  key: 'Escape',
  code: 'Escape',
  keyCode: 27,
  which: 27,
}));
const afterBack = {
  menuOpen,
  menuMode,
  selectedMenuItemIndex,
  labels: menuItems().map((item) => typeof menuItemLabel === 'function' ? menuItemLabel(item) : (item.name || item.key || '')),
};
window.dispatchEvent(new KeyboardEvent('keydown', {
  bubbles: true,
  cancelable: true,
  key: 'Escape',
  code: 'Escape',
  keyCode: 27,
  which: 27,
}));
const afterClose = {
  menuOpen,
  menuMode,
  selectedMenuItemIndex,
  labels: menuItems().map((item) => typeof menuItemLabel === 'function' ? menuItemLabel(item) : (item.name || item.key || '')),
};
const mobileButton = document.getElementById('virtualMenuButton');
const mobileOpened = openShopBuyMenu();
const mobileBefore = {
  opened: mobileOpened,
  hidden: mobileButton?.hidden ?? null,
  menuOpen,
  menuMode,
  selectedMenuItemIndex,
  labels: menuItems().map((item) => typeof menuItemLabel === 'function' ? menuItemLabel(item) : (item.name || item.key || '')),
};
mobileButton?.dispatchEvent(new PointerEvent('pointerup', {
  bubbles: true,
  cancelable: true,
  pointerId: 47,
  pointerType: 'touch',
  isPrimary: true,
}));
const afterMobileBack = {
  menuOpen,
  menuMode,
  selectedMenuItemIndex,
  labels: menuItems().map((item) => typeof menuItemLabel === 'function' ? menuItemLabel(item) : (item.name || item.key || '')),
};
mobileButton?.dispatchEvent(new PointerEvent('pointerup', {
  bubbles: true,
  cancelable: true,
  pointerId: 48,
  pointerType: 'touch',
  isPrimary: true,
}));
const afterMobileClose = {
  menuOpen,
  menuMode,
  selectedMenuItemIndex,
  labels: menuItems().map((item) => typeof menuItemLabel === 'function' ? menuItemLabel(item) : (item.name || item.key || '')),
};
const cancelButton = document.getElementById('virtualCancelButton');
const cancelOpened = openShopBuyMenu();
const cancelBefore = {
  opened: cancelOpened,
  hidden: cancelButton?.hidden ?? null,
  text: cancelButton?.textContent || '',
  menuOpen,
  menuMode,
  selectedMenuItemIndex,
  labels: menuItems().map((item) => typeof menuItemLabel === 'function' ? menuItemLabel(item) : (item.name || item.key || '')),
};
cancelButton?.dispatchEvent(new PointerEvent('pointerup', {
  bubbles: true,
  cancelable: true,
  pointerId: 57,
  pointerType: 'touch',
  isPrimary: true,
}));
const afterCancelBack = {
  menuOpen,
  menuMode,
  selectedMenuItemIndex,
  labels: menuItems().map((item) => typeof menuItemLabel === 'function' ? menuItemLabel(item) : (item.name || item.key || '')),
};
cancelButton?.dispatchEvent(new PointerEvent('pointerup', {
  bubbles: true,
  cancelable: true,
  pointerId: 58,
  pointerType: 'touch',
  isPrimary: true,
}));
const afterCancelClose = {
  menuOpen,
  menuMode,
  selectedMenuItemIndex,
  labels: menuItems().map((item) => typeof menuItemLabel === 'function' ? menuItemLabel(item) : (item.name || item.key || '')),
};
return {
  before,
  afterBack,
  afterClose,
  mobileBefore,
  afterMobileBack,
  afterMobileClose,
  cancelBefore,
  afterCancelBack,
  afterCancelClose,
};
"""


def map_state_script() -> str:
    return """
if (typeof render === 'function') render();
const foot = typeof footTile === 'function' ? footTile() : null;
const pathOptions = [...(document.getElementById('routePathSelect')?.options || [])]
  .map((option) => option.textContent || '');
const commandItems = typeof commandMenuItems === 'function'
  ? commandMenuItems().map((item) => ({
      key: item.key || '',
      name: item.name || '',
      command: item.command || '',
      usable: item.usable !== false,
      routeTarget: item.routeTarget || '',
      routeNextTarget: item.routeNextTarget || '',
    }))
  : [];
const ataho = (runtimeState?.characters || []).find((character) => character.name === 'Ataho') || null;
const herb = (runtimeState?.items || []).find((item) => item.key === 'herb') || null;
return {
  readyState: document.readyState,
  href: location.href,
  search: location.search,
  hasScreen: !!document.getElementById('screen'),
  scene: typeof scene === 'undefined' ? '' : scene,
  mapName: typeof map === 'undefined' || !map ? '' : map.name,
  foot,
  trialTransitions: typeof trialTransitions === 'undefined' ? '' : trialTransitions,
  selectedRouteGoal: typeof selectedRouteGoal === 'undefined' ? '' : selectedRouteGoal,
  routePathHidden: document.getElementById('routePathSelect')?.hidden ?? null,
  routePathValue: document.getElementById('routePathSelect')?.value ?? '',
  routePathOptions: pathOptions,
  commandItems,
  routeNextHidden: document.getElementById('routeNextButton')?.hidden ?? null,
  routeNextText: document.getElementById('routeNextButton')?.textContent || '',
  routeNextTitle: document.getElementById('routeNextButton')?.title || '',
  quickLoadHidden: document.getElementById('quickLoadButton')?.hidden ?? null,
  quickLoadText: document.getElementById('quickLoadButton')?.textContent || '',
  playHudLines: typeof playHudLines === 'function' ? playHudLines() : (window.HWANSE_LAST_PLAY_HUD_LINES || []),
  saveHudLines: window.HWANSE_LAST_SAVE_HUD_LINES || [],
  hasRuntimeState: !!runtimeState,
  runtimeMoney: runtimeState?.money ?? null,
  runtimeItems: (runtimeState?.items || []).map((item) => `${item.key}:${item.count}`),
  runtimeCharacters: (runtimeState?.characters || []).map((character) => character.name),
  runtimeCharactersDetailed: (runtimeState?.characters || []).map((character) => ({
    name: character.name,
    hp: character.hp,
    hpMax: character.hpMax,
    mp: character.mp,
    mpMax: character.mpMax,
  })),
  atahoHp: ataho?.hp ?? null,
  atahoHpMax: ataho?.hpMax ?? null,
  herbCount: herb?.count ?? null,
  prototypeProgress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
  loadedSaveSummary: loadedSaveSummary ? {
    fileName: loadedSaveSummary.fileName || '',
    selector: `${loadedSaveSummary.group}:${loadedSaveSummary.slot}`,
    group: loadedSaveSummary.group,
    slot: loadedSaveSummary.slot,
    x: loadedSaveSummary.x,
    y: loadedSaveSummary.y,
    fieldMaps: loadedSaveSummary.fieldMaps || [],
    routeEvidence: loadedSaveSummary.routeEvidence || null,
  } : null,
  publicSaveValue: document.getElementById('publicSaveSelect')?.value || '',
  titlePublicSavedat: window.HWANSE_LAST_TITLE_PUBLIC_SAVEDAT || null,
  titleRouteGoal: window.HWANSE_LAST_TITLE_ROUTE_GOAL || null,
  routeAssistAutoSave: window.HWANSE_LAST_ROUTE_ASSIST_AUTO_SAVE || null,
  fieldEncounter: typeof fieldEncounterSavePayload === 'function' ? fieldEncounterSavePayload() : null,
  fieldEncounterMenuLabel: typeof fieldEncounterMenuLabel === 'function' ? fieldEncounterMenuLabel() : '',
  fieldEncounterMode: window.HWANSE_LAST_FIELD_ENCOUNTER_MODE || null,
  fieldEncounterModeAutoSave: window.HWANSE_LAST_FIELD_ENCOUNTER_MODE_AUTO_SAVE || null,
  encounterFeedbackLog: window.HWANSE_FIELD_ENCOUNTER_FEEDBACK_LOG || [],
  encounterFeedbackRender: window.HWANSE_FIELD_ENCOUNTER_FEEDBACK_RENDER || [],
  encounterFeedbackLast: window.HWANSE_LAST_FIELD_ENCOUNTER_FEEDBACK || null,
  encounterFeedbackLastRender: window.HWANSE_LAST_FIELD_ENCOUNTER_FEEDBACK_RENDER || null,
  runtimeSaveFeedbackLog: window.HWANSE_RUNTIME_SAVE_FEEDBACK_LOG || [],
  runtimeSaveFeedbackRender: window.HWANSE_RUNTIME_SAVE_FEEDBACK_RENDER || [],
  runtimeSaveFeedbackLast: window.HWANSE_LAST_RUNTIME_SAVE_FEEDBACK || null,
  runtimeSaveFeedbackLastRender: window.HWANSE_LAST_RUNTIME_SAVE_FEEDBACK_RENDER || null,
  fieldEncounterStep: window.HWANSE_LAST_FIELD_ENCOUNTER_STEP || null,
  fieldEncounterStepAutoSave: window.HWANSE_LAST_FIELD_ENCOUNTER_STEP_AUTO_SAVE || null,
  routeGuideHidden: document.getElementById('routeGuideLink')?.hidden ?? null,
  mapReviewTileHidden: document.getElementById('mapReviewTileLink')?.hidden ?? null,
  transitionCount: Array.isArray(map?.transitions) ? map.transitions.length : 0,
};
"""


def wait_for_started_map(port: int, session_id: str) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, map_state_script(), timeout=3)
        last_state = state
        foot = state.get("foot") or {}
        if (
            state.get("readyState") == "complete"
            and state.get("hasScreen") is True
            and state.get("scene") == "map"
            and state.get("mapName") == "map1_02b"
            and foot.get("x") == 11
            and foot.get("y") == 12
            and "map=map1_02b" in str(state.get("search") or "")
            and "startTile=11%2C12" in str(state.get("search") or "")
            and any("map1_02b 11,12" in str(line) for line in (state.get("playHudLines") or []))
            and any("Ataho HP" in str(line) for line in (state.get("playHudLines") or []))
            and any("저장 없음" in str(line) for line in (state.get("playHudLines") or []))
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"title Enter did not start the confirmed map route: {last_state!r}")


def wait_for_encounter_started_map(port: int, session_id: str) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, map_state_script(), timeout=3)
        last_state = state
        foot = state.get("foot") or {}
        field = state.get("fieldEncounter") or {}
        mode = state.get("fieldEncounterMode") or {}
        mode_auto_save = state.get("fieldEncounterModeAutoSave") or mode.get("autoSave") or {}
        mode_auto_field = mode_auto_save.get("fieldEncounter") or {}
        mode_feedback = mode.get("modeFeedback") or {}
        mode_auto_feedback = mode_auto_save.get("modeFeedback") or {}
        feedback_entries = [
            entry for entry in (state.get("encounterFeedbackLog") or [])
            if isinstance(entry, dict)
            and entry.get("source") == "field-encounter-enable-feedback"
            and entry.get("text") == "전투 탐색 켬 0/6"
            and entry.get("mode") == "enabled"
            and entry.get("modeSource") == "title-start"
            and entry.get("enabled") is True
            and entry.get("stepCount") == 0
            and entry.get("threshold") == 6
            and entry.get("triggered") is False
            and entry.get("durationMs") == 900
            and entry.get("browserFieldEncounterFeedbackImplemented") is True
            and entry.get("originalEncounterTableMapped") is False
            and entry.get("originalEncounterRuntimeImplemented") is False
            and entry.get("originalStoryFlagRuntimeImplemented") is False
        ]
        feedback_render_entries = [
            entry for entry in (state.get("encounterFeedbackRender") or [])
            if isinstance(entry, dict)
            and entry.get("source") == "field-encounter-enable-feedback"
            and entry.get("text") == "전투 탐색 켬 0/6"
            and entry.get("mode") == "enabled"
            and entry.get("modeSource") == "title-start"
            and entry.get("enabled") is True
            and entry.get("stepCount") == 0
            and entry.get("threshold") == 6
            and entry.get("triggered") is False
            and entry.get("durationMs") == 900
            and entry.get("active") is True
            and entry.get("browserFieldEncounterFeedbackImplemented") is True
        ]
        if (
            state.get("readyState") == "complete"
            and state.get("hasScreen") is True
            and state.get("scene") == "map"
            and state.get("mapName") == "map1_02b"
            and foot.get("x") == 11
            and foot.get("y") == 12
            and state.get("quickLoadHidden") is False
            and state.get("quickLoadText") == "임시 불러오기"
            and "map=map1_02b" in str(state.get("search") or "")
            and "startTile=11%2C12" in str(state.get("search") or "")
            and "encounter=1" in str(state.get("search") or "")
            and field.get("enabled") is True
            and field.get("stepCount") == 0
            and field.get("source") == "prototype-field-encounter"
            and field.get("originalEncounterTableMapped") is False
            and field.get("originalEncounterRuntimeImplemented") is False
            and mode.get("source") == "title-start"
            and mode.get("enabled") is True
            and mode_feedback.get("source") == "field-encounter-enable-feedback"
            and mode_feedback.get("text") == "전투 탐색 켬 0/6"
            and mode_feedback.get("mode") == "enabled"
            and mode_feedback.get("modeSource") == "title-start"
            and mode_feedback.get("browserFieldEncounterFeedbackImplemented") is True
            and state.get("fieldEncounterMenuLabel") == "전투 탐색 끄기 0/6"
            and any("전투 0/6" in str(line) for line in (state.get("playHudLines") or []))
            and any("저장 있음" in str(line) for line in (state.get("playHudLines") or []))
            and mode_auto_save.get("saved") is True
            and mode_auto_save.get("source") == "field-encounter-enable"
            and mode_auto_save.get("scope") == "field-encounter-mode"
            and mode_auto_save.get("modeSource") == "title-start"
            and mode_auto_save.get("payloadMap") == "map1_02b"
            and mode_auto_feedback.get("source") == "field-encounter-enable-feedback"
            and mode_auto_feedback.get("text") == "전투 탐색 켬 0/6"
            and mode_auto_feedback.get("mode") == "enabled"
            and mode_auto_feedback.get("modeSource") == "title-start"
            and mode_auto_field.get("enabled") is True
            and mode_auto_field.get("stepCount") == 0
            and mode_auto_field.get("source") == "prototype-field-encounter"
            and bool(feedback_entries)
            and bool(feedback_render_entries)
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"title encounter start did not enable field encounters: {last_state!r}")


def wait_for_field_encounter_step_autosave(port: int, session_id: str) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, field_encounter_step_autosave_state_script(), timeout=3)
        last_state = state if isinstance(state, dict) else {}
        step = last_state.get("step") or {}
        auto_save = last_state.get("autoSave") or {}
        saved_payload = last_state.get("savedPayload") or {}
        saved_field = saved_payload.get("fieldEncounter") or {}
        field = last_state.get("fieldEncounter") or {}
        before = last_state.get("before") or {}
        before_field = before.get("fieldEncounter") or {}
        before_foot = before.get("foot") or {}
        movement = last_state.get("movement") or {}
        after_foot = movement.get("afterFoot") or {}
        step_feedback = step.get("feedback") or {}
        feedback_entries = [
            entry for entry in (last_state.get("encounterFeedbackLog") or [])
            if isinstance(entry, dict)
            and entry.get("source") == "field-encounter-step-feedback"
            and entry.get("text") == "전투 탐색 1/6"
            and entry.get("stepCount") == 1
            and entry.get("threshold") == 6
            and entry.get("triggered") is False
            and entry.get("durationMs") == 900
            and entry.get("browserFieldEncounterFeedbackImplemented") is True
            and entry.get("originalEncounterTableMapped") is False
            and entry.get("originalEncounterRuntimeImplemented") is False
            and entry.get("originalStoryFlagRuntimeImplemented") is False
        ]
        feedback_render_entries = [
            entry for entry in (last_state.get("encounterFeedbackRender") or [])
            if isinstance(entry, dict)
            and entry.get("source") == "field-encounter-step-feedback"
            and entry.get("text") == "전투 탐색 1/6"
            and entry.get("stepCount") == 1
            and entry.get("threshold") == 6
            and entry.get("triggered") is False
            and entry.get("durationMs") == 900
            and entry.get("active") is True
            and entry.get("browserFieldEncounterFeedbackImplemented") is True
        ]
        if (
            last_state
            and last_state.get("error") is None
            and last_state.get("pending") is False
            and last_state.get("movementInput") is True
            and last_state.get("inputCode") == "ArrowRight"
            and before_field.get("enabled") is True
            and before_field.get("stepCount") == 0
            and before_foot.get("x") == 11
            and before_foot.get("y") == 12
            and movement.get("tileChanged") is True
            and after_foot.get("x") != before_foot.get("x")
            and after_foot.get("y") == before_foot.get("y")
            and movement.get("activeStep") is False
            and step.get("triggered") is False
            and step.get("stepCount") == 1
            and step.get("candidateId")
            and step.get("battleBackground")
            and step_feedback.get("source") == "field-encounter-step-feedback"
            and step_feedback.get("text") == "전투 탐색 1/6"
            and step_feedback.get("stepCount") == 1
            and step_feedback.get("threshold") == 6
            and step_feedback.get("triggered") is False
            and step_feedback.get("fieldEncounterSound") == "step"
            and str(step_feedback.get("fieldEncounterSoundSrc") or "").endswith("/extract_wlk/00.wav")
            and step_feedback.get("fieldEncounterSoundPlayed") is True
            and step_feedback.get("browserFieldEncounterFeedbackImplemented") is True
            and bool(feedback_entries)
            and bool(feedback_render_entries)
            and auto_save.get("saved") is True
            and auto_save.get("source") == "field-encounter-step"
            and auto_save.get("scope") == "field-encounter-step"
            and auto_save.get("payloadMap") == "map1_02b"
            and (auto_save.get("payloadTile") or {}).get("x") == after_foot.get("x")
            and (auto_save.get("payloadTile") or {}).get("y") == after_foot.get("y")
            and (auto_save.get("fieldEncounter") or {}).get("enabled") is True
            and (auto_save.get("fieldEncounter") or {}).get("stepCount") == 1
            and (auto_save.get("fieldEncounter") or {}).get("lastMap") == "map1_02b"
            and auto_save.get("stepCount") == 1
            and auto_save.get("threshold") == 6
            and saved_payload.get("map") == "map1_02b"
            and (saved_payload.get("tile") or {}).get("x") == after_foot.get("x")
            and (saved_payload.get("tile") or {}).get("y") == after_foot.get("y")
            and saved_field.get("enabled") is True
            and saved_field.get("stepCount") == 1
            and saved_field.get("lastMap") == "map1_02b"
            and field.get("enabled") is True
            and field.get("stepCount") == 1
            and field.get("source") == "prototype-field-encounter"
            and last_state.get("fieldEncounterMenuLabel") == "전투 탐색 끄기 1/6"
            and last_state.get("quickLoadHidden") is False
            and last_state.get("quickLoadText") == "임시 불러오기"
            and any("전투 1/6" in str(line) for line in (last_state.get("playHudLines") or []))
        ):
            return last_state
        time.sleep(0.2)
    raise WebDriverError(f"field encounter step did not auto-save: {last_state!r}")


def wait_for_field_encounter_disable_autosave(port: int, session_id: str) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, field_encounter_disable_autosave_state_script(), timeout=3)
        last_state = state if isinstance(state, dict) else {}
        before = last_state.get("before") or {}
        before_field = before.get("fieldEncounter") or {}
        auto_save = last_state.get("autoSave") or {}
        auto_field = auto_save.get("fieldEncounter") or {}
        saved_payload = last_state.get("savedPayload") or {}
        saved_field = saved_payload.get("fieldEncounter") or {}
        field = last_state.get("fieldEncounter") or {}
        before_foot = before.get("foot") or {}
        mode = last_state.get("mode") or {}
        mode_feedback = mode.get("modeFeedback") or {}
        auto_feedback = auto_save.get("modeFeedback") or {}
        feedback_entries = [
            entry for entry in (last_state.get("encounterFeedbackLog") or [])
            if isinstance(entry, dict)
            and entry.get("source") == "field-encounter-disable-feedback"
            and entry.get("text") == "전투 탐색 끔 1/6"
            and entry.get("mode") == "disabled"
            and entry.get("modeSource") == "menu"
            and entry.get("enabled") is False
            and entry.get("stepCount") == 1
            and entry.get("threshold") == 6
            and entry.get("triggered") is False
            and entry.get("durationMs") == 900
            and entry.get("browserFieldEncounterFeedbackImplemented") is True
            and entry.get("originalEncounterTableMapped") is False
            and entry.get("originalEncounterRuntimeImplemented") is False
            and entry.get("originalStoryFlagRuntimeImplemented") is False
        ]
        feedback_render_entries = [
            entry for entry in (last_state.get("encounterFeedbackRender") or [])
            if isinstance(entry, dict)
            and entry.get("source") == "field-encounter-disable-feedback"
            and entry.get("text") == "전투 탐색 끔 1/6"
            and entry.get("mode") == "disabled"
            and entry.get("modeSource") == "menu"
            and entry.get("enabled") is False
            and entry.get("stepCount") == 1
            and entry.get("threshold") == 6
            and entry.get("triggered") is False
            and entry.get("durationMs") == 900
            and entry.get("active") is True
            and entry.get("browserFieldEncounterFeedbackImplemented") is True
        ]
        if (
            last_state
            and last_state.get("error") is None
            and last_state.get("commandResult") is True
            and before_field.get("enabled") is True
            and before_field.get("stepCount") == 1
            and before_foot.get("x") is not None
            and before_foot.get("y") is not None
            and last_state.get("labelBefore") == "전투 탐색 끄기 1/6"
            and last_state.get("labelAfter") == "전투 탐색 켜기"
            and mode_feedback.get("source") == "field-encounter-disable-feedback"
            and mode_feedback.get("text") == "전투 탐색 끔 1/6"
            and mode_feedback.get("mode") == "disabled"
            and mode_feedback.get("modeSource") == "menu"
            and mode_feedback.get("browserFieldEncounterFeedbackImplemented") is True
            and auto_save.get("saved") is True
            and auto_save.get("source") == "field-encounter-disable"
            and auto_save.get("scope") == "field-encounter-mode"
            and auto_save.get("modeSource") == "menu"
            and auto_save.get("payloadMap") == "map1_02b"
            and auto_feedback.get("source") == "field-encounter-disable-feedback"
            and auto_feedback.get("text") == "전투 탐색 끔 1/6"
            and auto_feedback.get("mode") == "disabled"
            and auto_feedback.get("modeSource") == "menu"
            and (auto_save.get("payloadTile") or {}).get("x") == before_foot.get("x")
            and (auto_save.get("payloadTile") or {}).get("y") == before_foot.get("y")
            and auto_field.get("enabled") is False
            and auto_field.get("stepCount") == 0
            and auto_field.get("source") == "prototype-field-encounter"
            and saved_payload.get("map") == "map1_02b"
            and (saved_payload.get("tile") or {}).get("x") == before_foot.get("x")
            and (saved_payload.get("tile") or {}).get("y") == before_foot.get("y")
            and saved_field.get("enabled") is False
            and saved_field.get("stepCount") == 0
            and field.get("enabled") is False
            and field.get("stepCount") == 0
            and field.get("source") == "prototype-field-encounter"
            and last_state.get("fieldEncounterMenuLabel") == "전투 탐색 켜기"
            and "encounter=1" not in str(last_state.get("search") or "")
            and last_state.get("quickLoadHidden") is False
            and last_state.get("quickLoadText") == "임시 불러오기"
            and bool(feedback_entries)
            and bool(feedback_render_entries)
            and any("저장 있음" in str(line) for line in (last_state.get("playHudLines") or []))
            and not any(
                ("전투 1/6" in str(line) or "필드 전투" in str(line))
                for line in (last_state.get("playHudLines") or [])
            )
        ):
            return last_state
        time.sleep(0.2)
    raise WebDriverError(f"field encounter disable did not auto-save: {last_state!r}")


def wait_for_direct_url_map(port: int, session_id: str) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, map_state_script(), timeout=3)
        last_state = state
        foot = state.get("foot") or {}
        if (
            state.get("readyState") == "complete"
            and state.get("hasScreen") is True
            and state.get("scene") == "map"
            and state.get("mapName") == "map1_02b"
            and foot.get("x") == 11
            and foot.get("y") == 12
            and "map=map1_02b" in str(state.get("search") or "")
            and "startTile=11%2C12" in str(state.get("search") or "")
            and state.get("hasRuntimeState") is True
            and state.get("runtimeMoney") == 60
            and "Ataho" in (state.get("runtimeCharacters") or [])
            and "herb:1" in (state.get("runtimeItems") or [])
            and any("소지금 60" in str(line) for line in (state.get("playHudLines") or []))
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"direct URL map did not create prototype runtime state: {last_state!r}")


def public_sample_route_evidence_ready(route_evidence: dict[str, Any]) -> bool:
    missing = set(route_evidence.get("missingEvidence") or [])
    return (
        route_evidence.get("routeProofCandidate") is False
        and route_evidence.get("realRouteProofCandidate") is False
        and route_evidence.get("syntheticDiagnostic") is False
        and route_evidence.get("status") == "not-current-selector"
        and route_evidence.get("requiredSelector") == "2:0"
        and route_evidence.get("requiredSelectedPointer") == "0x00540714"
        and route_evidence.get("requiredSourceMap") == "map1_01a"
        and route_evidence.get("requiredTargetMap") == "map2_02d"
        and route_evidence.get("externalProofInputId") == "real-selector-2-0-save"
        and route_evidence.get("browserSavedatRouteEvidenceChecklistImplemented") is True
        and route_evidence.get("missingEvidenceCount") == 3
        and missing == {
            "selector 2:0",
            "selected pointer 0x00540714",
            "map1_01a->map2_02d route pair",
        }
    )


def public_sample_route_marker_ready(marker: dict[str, Any]) -> bool:
    missing = set(marker.get("missingEvidence") or [])
    return (
        marker.get("routeProofCandidate") is False
        and marker.get("realRouteProofCandidate") is False
        and marker.get("syntheticDiagnostic") is False
        and marker.get("routeStatus") == "not-current-selector"
        and marker.get("externalProofInputId") == "real-selector-2-0-save"
        and marker.get("browserSavedatRouteEvidenceChecklistImplemented") is True
        and marker.get("missingEvidenceCount") == 3
        and missing == {
            "selector 2:0",
            "selected pointer 0x00540714",
            "map1_01a->map2_02d route pair",
        }
    )


def public_sample_route_hud_ready(lines: list[Any]) -> bool:
    return any("route missing selector 2:0" in str(line) for line in lines)


def wait_for_title_sample_savedat_map(port: int, session_id: str) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, map_state_script(), timeout=3)
        last_state = state
        foot = state.get("foot") or {}
        summary = state.get("loadedSaveSummary") or {}
        marker = state.get("titlePublicSavedat") or {}
        route_evidence = summary.get("routeEvidence") or {}
        if (
            state.get("readyState") == "complete"
            and state.get("hasScreen") is True
            and state.get("scene") == "map"
            and state.get("mapName") == "map2_02d"
            and foot.get("x") == 5
            and foot.get("y") == 14
            and "publicSave=flack3r-savedat2" in str(state.get("search") or "")
            and "map=map2_02d" in str(state.get("search") or "")
            and "startTile=5%2C14" in str(state.get("search") or "")
            and state.get("publicSaveValue") == "flack3r-savedat2"
            and summary.get("selector") == "1:0"
            and summary.get("group") == 1
            and summary.get("slot") == 0
            and summary.get("x") == 5
            and summary.get("y") == 15
            and "map2_02d" in (summary.get("fieldMaps") or [])
            and public_sample_route_evidence_ready(route_evidence)
            and state.get("runtimeMoney") == 983062
            and "herb:3" in (state.get("runtimeItems") or [])
            and {"Ataho", "Rinshan", "Smashu"}.issubset(set(state.get("runtimeCharacters") or []))
            and marker.get("source") == "title-menu"
            and marker.get("key") == "flack3r-savedat2"
            and marker.get("loaded") is True
            and marker.get("loadedMap") == "map2_02d"
            and marker.get("selector") == "1:0"
            and public_sample_route_marker_ready(marker)
            and marker.get("originalRoutePromotionImplemented") is False
            and public_sample_route_hud_ready(state.get("saveHudLines") or [])
            and any("map2_02d 5,14" in str(line) for line in (state.get("playHudLines") or []))
            and any("소지금 983062" in str(line) for line in (state.get("playHudLines") or []))
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"title sample savedat did not load public save: {last_state!r}")


def wait_for_continued_sample_savedat_map(port: int, session_id: str) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, map_state_script(), timeout=3)
        last_state = state
        foot = state.get("foot") or {}
        summary = state.get("loadedSaveSummary") or {}
        route_evidence = summary.get("routeEvidence") or {}
        search = str(state.get("search") or "")
        if (
            state.get("readyState") == "complete"
            and state.get("hasScreen") is True
            and state.get("scene") == "map"
            and state.get("mapName") == "map2_02d"
            and foot.get("x") == 5
            and foot.get("y") == 14
            and state.get("quickLoadHidden") is False
            and state.get("quickLoadText") == "임시 불러오기"
            and "map=map2_02d" in search
            and "startTile=5%2C14" in search
            and summary.get("selector") == "1:0"
            and summary.get("group") == 1
            and summary.get("slot") == 0
            and summary.get("x") == 5
            and summary.get("y") == 14
            and "map2_02d" in (summary.get("fieldMaps") or [])
            and public_sample_route_evidence_ready(route_evidence)
            and state.get("runtimeMoney") == 983062
            and "herb:3" in (state.get("runtimeItems") or [])
            and {"Ataho", "Rinshan", "Smashu"}.issubset(set(state.get("runtimeCharacters") or []))
            and any("map2_02d 5,14" in str(line) for line in (state.get("playHudLines") or []))
            and public_sample_route_hud_ready(state.get("saveHudLines") or [])
            and any("소지금 983062" in str(line) for line in (state.get("playHudLines") or []))
            and any("저장 있음" in str(line) for line in (state.get("playHudLines") or []))
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"title continue did not restore quick-saved public savedat sample: {last_state!r}")


def wait_for_title_scan_no_saves(port: int, session_id: str) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, title_state_script(), timeout=3)
        last_state = state
        scan = state.get("titleSavedatScan") or {}
        render = state.get("titleMenuRender") or {}
        if (
            state.get("readyState") == "complete"
            and state.get("hasScreen") is True
            and state.get("scene") == "title"
            and state.get("search") == "?savedatScan=1"
            and scan.get("source") == "title-menu"
            and scan.get("requestScene") == "title"
            and scan.get("loaded") is False
            and scan.get("foundCount") == 0
            and scan.get("loadedMap") == ""
            and scan.get("originalRoutePromotionImplemented") is False
            and "SAVEDATA/savedat1-9 dat/zip not found" in str(scan.get("menuNotice") or "")
            and "SAVEDATA/savedat1-9 dat/zip not found" in str(render.get("notice") or "")
            and render.get("keys") == TITLE_START_KEYS
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"title savedat scan did not expose no-save state: {last_state!r}")


def wait_for_route_assist_started_map(port: int, session_id: str) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, map_state_script(), timeout=3)
        last_state = state
        foot = state.get("foot") or {}
        search = str(state.get("search") or "")
        if (
            state.get("readyState") == "complete"
            and state.get("hasScreen") is True
            and state.get("scene") == "map"
            and state.get("mapName") == "map1_02b"
            and foot.get("x") == 11
            and foot.get("y") == 12
            and state.get("trialTransitions") == "routeAssist"
            and state.get("selectedRouteGoal") == "map2_02d"
            and state.get("routePathHidden") is False
            and state.get("routePathValue") == "map2_02d"
            and state.get("routeNextHidden") is False
            and state.get("routeNextText") == "다음"
            and "map=map1_02b" in search
            and "startTile=11%2C12" in search
            and "trialTransitions=routeAssist" in search
            and "routeGoal=map2_02d" in search
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"title routeAssist button did not start route-assisted map route: {last_state!r}")


def wait_for_play_started_map(
    port: int,
    session_id: str,
    *,
    expected_source: str = "title-play-start",
    require_play_query: bool = False,
) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, map_state_script(), timeout=3)
        last_state = state
        foot = state.get("foot") or {}
        search = str(state.get("search") or "")
        field = state.get("fieldEncounter") or {}
        mode = state.get("fieldEncounterMode") or {}
        marker = state.get("titleRouteGoal") or {}
        auto_saved_field = marker.get("autoSavedFieldEncounter") or {}
        hud = " ".join(str(line) for line in (state.get("playHudLines") or []))
        if (
            state.get("readyState") == "complete"
            and state.get("hasScreen") is True
            and state.get("scene") == "map"
            and state.get("mapName") == "map1_02b"
            and foot.get("x") == 11
            and foot.get("y") == 12
            and state.get("quickLoadHidden") is False
            and state.get("quickLoadText") == "임시 불러오기"
            and state.get("trialTransitions") == "routeAssist"
            and state.get("selectedRouteGoal") == "map2_02d"
            and state.get("routePathHidden") is False
            and state.get("routePathValue") == "map2_02d"
            and state.get("routeNextHidden") is False
            and state.get("routeNextText") == "다음"
            and "map=map1_02b" in search
            and "startTile=11%2C12" in search
            and "trialTransitions=routeAssist" in search
            and "routeGoal=map2_02d" in search
            and "encounter=1" in search
            and field.get("enabled") is True
            and field.get("stepCount") == 0
            and field.get("source") == "prototype-field-encounter"
            and mode.get("source") == expected_source
            and mode.get("enabled") is True
            and marker.get("source") == expected_source
            and marker.get("fieldEncounters") is True
            and marker.get("target") == "map2_02d"
            and marker.get("autoSaved") is True
            and marker.get("autoSavedMap") == "map1_02b"
            and marker.get("autoSavedRouteGoal") == "map2_02d"
            and auto_saved_field.get("enabled") is True
            and auto_saved_field.get("stepCount") == 0
            and auto_saved_field.get("source") == "prototype-field-encounter"
            and marker.get("originalRoutePromotionImplemented") is False
            and "후보 map2_02d" in hud
            and "전투 0/6" in hud
            and "저장 있음" in hud
            and (not require_play_query or "play=1" in search)
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"title/query play start did not enable routeAssist plus field encounters: {last_state!r}")


def wait_for_continued_play_start_map(port: int, session_id: str) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, map_state_script(), timeout=3)
        last_state = state
        foot = state.get("foot") or {}
        search = str(state.get("search") or "")
        field = state.get("fieldEncounter") or {}
        play_hud = " ".join(str(line) for line in (state.get("playHudLines") or []))
        if (
            state.get("readyState") == "complete"
            and state.get("hasScreen") is True
            and state.get("scene") == "map"
            and state.get("mapName") == "map1_02b"
            and foot.get("x") == 11
            and foot.get("y") == 12
            and state.get("quickLoadHidden") is False
            and state.get("quickLoadText") == "임시 불러오기"
            and state.get("trialTransitions") == "routeAssist"
            and state.get("selectedRouteGoal") == "map2_02d"
            and state.get("routePathHidden") is False
            and state.get("routePathValue") == "map2_02d"
            and state.get("routeNextHidden") is False
            and state.get("routeNextText") == "다음"
            and "map=map1_02b" in search
            and "startTile=11%2C12" in search
            and "trialTransitions=routeAssist" in search
            and "routeGoal=map2_02d" in search
            and "encounter=1" in search
            and field.get("enabled") is True
            and field.get("stepCount") == 0
            and field.get("source") == "prototype-field-encounter"
            and field.get("originalEncounterTableMapped") is False
            and field.get("originalEncounterRuntimeImplemented") is False
            and "후보 map2_02d" in play_hud
            and "전투 0/6" in play_hud
            and "저장 있음" in play_hud
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"title play start continue did not restore routeAssist plus field encounters: {last_state!r}")


def wait_for_play_start_route_next_map(port: int, session_id: str) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, map_state_script(), timeout=3)
        last_state = state
        foot = state.get("foot") or {}
        search = str(state.get("search") or "")
        field = state.get("fieldEncounter") or {}
        auto_save = state.get("routeAssistAutoSave") or {}
        auto_save_field = auto_save.get("fieldEncounter") or {}
        auto_save_route = auto_save.get("routeState") or {}
        continuation_item = next(
            (row for row in (state.get("commandItems") or []) if row.get("command") == "continueRouteAssistPath"),
            {},
        )
        play_hud = " ".join(str(line) for line in (state.get("playHudLines") or []))
        if (
            state.get("readyState") == "complete"
            and state.get("hasScreen") is True
            and state.get("scene") == "map"
            and state.get("mapName") == "map1_01a"
            and foot.get("x") == 11
            and foot.get("y") == 11
            and state.get("quickLoadHidden") is False
            and state.get("quickLoadText") == "임시 불러오기"
            and state.get("trialTransitions") == "routeAssist"
            and state.get("selectedRouteGoal") == "map2_02d"
            and state.get("routePathHidden") is False
            and state.get("routePathValue") == "map2_02d"
            and state.get("routeNextHidden") is False
            and state.get("routeNextText") == "후보"
            and "map=map1_01a" in search
            and "startTile=11%2C11" in search
            and "trialTransitions=routeAssist" in search
            and "routeGoal=map2_02d" in search
            and "routeAutoSave=1" in search
            and "encounter=1" in search
            and field.get("enabled") is True
            and field.get("stepCount") == 0
            and field.get("lastMap") == "map1_01a"
            and field.get("source") == "prototype-field-encounter"
            and auto_save.get("saved") is True
            and auto_save.get("source") == "routeAutoSave"
            and auto_save.get("map") == "map1_01a"
            and auto_save.get("payloadMap") == "map1_01a"
            and (auto_save.get("payloadTile") or {}).get("x") == 11
            and (auto_save.get("payloadTile") or {}).get("y") == 11
            and auto_save_route.get("trialTransitions") == "routeAssist"
            and auto_save_route.get("selectedRouteGoal") == "map2_02d"
            and auto_save_field.get("enabled") is True
            and auto_save_field.get("stepCount") == 0
            and auto_save_field.get("lastMap") == "map1_01a"
            and "후보 map2_02d" in play_hud
            and "전투 0/6" in play_hud
            and "저장 있음" in play_hud
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"title play start route next did not preserve encounter/auto-save: {last_state!r}")


def wait_for_play_start_route_focus_map(port: int, session_id: str) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, map_state_script(), timeout=3)
        last_state = state
        foot = state.get("foot") or {}
        search = str(state.get("search") or "")
        field = state.get("fieldEncounter") or {}
        auto_save = state.get("routeAssistAutoSave") or {}
        auto_save_field = auto_save.get("fieldEncounter") or {}
        auto_save_route = auto_save.get("routeState") or {}
        continuation_item = next(
            (row for row in (state.get("commandItems") or []) if row.get("command") == "continueRouteAssistPath"),
            {},
        )
        play_hud = " ".join(str(line) for line in (state.get("playHudLines") or []))
        if (
            state.get("readyState") == "complete"
            and state.get("hasScreen") is True
            and state.get("scene") == "map"
            and state.get("mapName") == "map1_01a"
            and foot.get("x") == 18
            and foot.get("y") == 0
            and state.get("quickLoadHidden") is False
            and state.get("quickLoadText") == "임시 불러오기"
            and state.get("trialTransitions") == "routeAssist"
            and state.get("selectedRouteGoal") == "map2_02d"
            and state.get("routePathHidden") is False
            and state.get("routePathValue") == "map2_02d"
            and state.get("routeNextHidden") is False
            and state.get("routeNextText") == "진입"
            and "map=map1_01a" in search
            and "startTile=18%2C0" in search
            and "focusTile=18%2C0" in search
            and "transitionTarget=map2_02d" in search
            and "trialTransitions=routeAssist" in search
            and "routeGoal=map2_02d" in search
            and "routeAutoSave=1" in search
            and "encounter=1" in search
            and field.get("enabled") is True
            and field.get("stepCount") in (0, 1)
            and field.get("lastMap") == "map1_01a"
            and auto_save.get("saved") is True
            and auto_save.get("map") == "map1_01a"
            and auto_save.get("payloadMap") == "map1_01a"
            and (auto_save.get("payloadTile") or {}).get("x") == 18
            and (auto_save.get("payloadTile") or {}).get("y") == 0
            and auto_save_route.get("trialTransitions") == "routeAssist"
            and auto_save_route.get("selectedRouteGoal") == "map2_02d"
            and auto_save_field.get("enabled") is True
            and auto_save_field.get("lastMap") == "map1_01a"
            and "후보 map2_02d" in play_hud
            and ("전투 0/6" in play_hud or "전투 1/6" in play_hud)
            and "저장 있음" in play_hud
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"title play start route focus did not preserve encounter/auto-save: {last_state!r}")


def wait_for_play_start_confirmed_input_transition(port: int, session_id: str) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, route_input_transition_state_script(), timeout=3)
        last_state = state if isinstance(state, dict) else {}
        before = last_state.get("before") or {}
        after = last_state.get("after") or {}
        after_foot = after.get("foot") or {}
        auto_save = last_state.get("mapTransitionAutoSave") or {}
        auto_route = auto_save.get("routeState") or {}
        auto_field = auto_save.get("fieldEncounter") or {}
        auto_tile = auto_save.get("payloadTile") or {}
        payload = last_state.get("savedPayload") or {}
        payload_route = payload.get("routeState") or {}
        payload_field = payload.get("fieldEncounter") or {}
        payload_tile = payload.get("tile") or {}
        progress_counts = ((last_state.get("progress") or {}).get("counts") or {})
        sound_state = last_state.get("soundState") or {}
        sound_counts = sound_state.get("counts") or {}
        sound_transition = next(
            (
                entry
                for entry in (sound_state.get("log") or [])
                if entry.get("key") == "transition"
                and str(entry.get("src") or "").endswith("/extract_wlk/07.wav")
            ),
            {},
        )
        transition_feedback = next(
            (
                entry
                for entry in map_transition_feedback_entries(last_state)
                if entry.get("source") == "map-transition-feedback"
                and entry.get("text") == "맵 이동 map1_02b -> map1_01a"
            ),
            {},
        )
        transition_feedback_render = next(
            (
                entry
                for entry in map_transition_feedback_entries(last_state, rendered=True)
                if entry.get("source") == "map-transition-feedback"
                and entry.get("text") == "맵 이동 map1_02b -> map1_01a"
            ),
            {},
        )
        hud = " ".join(str(line) for line in (after.get("playHudLines") or []))
        if (
            last_state
            and last_state.get("pending") is False
            and last_state.get("movementInput") is True
            and last_state.get("inputCode") == "ArrowUp"
            and last_state.get("changedMap") is True
            and before.get("mapName") == "map1_02b"
            and (before.get("foot") or {}).get("x") == 11
            and (before.get("foot") or {}).get("y") == 12
            and after.get("scene") == "map"
            and after.get("mapName") == "map1_01a"
            and after_foot.get("x") == 11
            and after_foot.get("y") == 11
            and after.get("routeNextText") == "후보"
            and after.get("routeState", {}).get("trialTransitions") == "routeAssist"
            and after.get("routeState", {}).get("selectedRouteGoal") == "map2_02d"
            and after.get("fieldEncounter", {}).get("enabled") is True
            and after.get("fieldEncounter", {}).get("stepCount") == 0
            and auto_save.get("saved") is True
            and auto_save.get("source") == "map-transition"
            and auto_save.get("payloadMap") == "map1_01a"
            and auto_tile.get("x") == 11
            and auto_tile.get("y") == 11
            and auto_route.get("trialTransitions") == "routeAssist"
            and auto_route.get("selectedRouteGoal") == "map2_02d"
            and auto_field.get("enabled") is True
            and auto_field.get("stepCount") == 0
            and auto_field.get("lastMap") == "map1_01a"
            and payload.get("map") == "map1_01a"
            and payload_tile.get("x") == 11
            and payload_tile.get("y") == 11
            and payload_route.get("trialTransitions") == "routeAssist"
            and payload_route.get("selectedRouteGoal") == "map2_02d"
            and payload_field.get("enabled") is True
            and payload_field.get("stepCount") == 0
            and payload_field.get("lastMap") == "map1_01a"
            and progress_counts.get("map-transition") == 1
            and transition_feedback.get("kind") == "map-transition"
            and transition_feedback.get("sourceMap") == "map1_02b"
            and transition_feedback.get("targetMap") == "map1_01a"
            and transition_feedback.get("trigger") == "movement"
            and transition_feedback.get("transitionKind") == "event-transition"
            and transition_feedback.get("transitionReviewState") == "confirmed"
            and transition_feedback.get("durationMs") == 1000
            and transition_feedback.get("transitionSound") == "transition"
            and str(transition_feedback.get("transitionSoundSrc") or "").endswith("/extract_wlk/07.wav")
            and transition_feedback.get("transitionSoundPlayed") is True
            and transition_feedback.get("browserMapTransitionFeedbackImplemented") is True
            and transition_feedback.get("originalTransitionTextRuntimeImplemented") is False
            and transition_feedback.get("originalStoryFlagRuntimeImplemented") is False
            and transition_feedback_render.get("active") is True
            and transition_feedback_render.get("transitionSound") == "transition"
            and str(transition_feedback_render.get("transitionSoundSrc") or "").endswith("/extract_wlk/07.wav")
            and transition_feedback_render.get("transitionSoundPlayed") is True
            and transition_feedback_render.get("browserMapTransitionFeedbackImplemented") is True
            and sound_counts.get("transition") == 1
            and sound_transition.get("key") == "transition"
            and "후보 map2_02d" in hud
            and "전투 0/6" in hud
            and "저장 있음" in hud
            and last_state.get("originalRoutePromotionImplemented") is False
        ):
            return last_state
        time.sleep(0.2)
    raise WebDriverError(f"play start confirmed input transition did not finish/save: {last_state!r}")


def wait_for_play_start_candidate_input_transition(port: int, session_id: str) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, route_input_transition_state_script(), timeout=3)
        last_state = state if isinstance(state, dict) else {}
        before = last_state.get("before") or {}
        after = last_state.get("after") or {}
        after_foot = after.get("foot") or {}
        auto_save = last_state.get("routeCandidateAutoSave") or {}
        auto_route = auto_save.get("routeState") or {}
        auto_field = auto_save.get("fieldEncounter") or {}
        auto_tile = auto_save.get("payloadTile") or {}
        payload = last_state.get("savedPayload") or {}
        payload_route = payload.get("routeState") or {}
        payload_field = payload.get("fieldEncounter") or {}
        payload_tile = payload.get("tile") or {}
        progress_counts = ((last_state.get("progress") or {}).get("counts") or {})
        sound_state = last_state.get("soundState") or {}
        sound_counts = sound_state.get("counts") or {}
        sound_transition = next(
            (
                entry
                for entry in (sound_state.get("log") or [])
                if entry.get("key") == "transition"
                and str(entry.get("src") or "").endswith("/extract_wlk/07.wav")
            ),
            {},
        )
        transition_feedback = next(
            (
                entry
                for entry in map_transition_feedback_entries(last_state)
                if entry.get("source") == "route-candidate-feedback"
                and entry.get("text") == TRIAL_CANDIDATE_TRANSITION_TEXT
            ),
            {},
        )
        transition_feedback_render = next(
            (
                entry
                for entry in map_transition_feedback_entries(last_state, rendered=True)
                if entry.get("source") == "route-candidate-feedback"
                and entry.get("text") == TRIAL_CANDIDATE_TRANSITION_TEXT
            ),
            {},
        )
        hud = " ".join(str(line) for line in (after.get("playHudLines") or []))
        if (
            last_state
            and last_state.get("pending") is False
            and last_state.get("movementInput") is True
            and last_state.get("inputCode") == "ArrowUp"
            and last_state.get("changedMap") is True
            and before.get("mapName") == "map1_01a"
            and (before.get("foot") or {}).get("x") == 18
            and (before.get("foot") or {}).get("y") == 0
            and before.get("routeNextText") == "진입"
            and after.get("scene") == "map"
            and after.get("mapName") == "map2_02d"
            and after_foot.get("x") == 47
            and after_foot.get("y") == 47
            and after.get("routeNextText") in {"도착", "완료", "이어가기"}
            and after.get("routeState", {}).get("trialTransitions") == "routeAssist"
            and after.get("routeState", {}).get("selectedRouteGoal") == "map2_02d"
            and after.get("fieldEncounter", {}).get("enabled") is True
            and after.get("fieldEncounter", {}).get("stepCount") == 0
            and auto_save.get("saved") is True
            and auto_save.get("source") == "route-candidate"
            and auto_save.get("payloadMap") == "map2_02d"
            and auto_tile.get("x") == 47
            and auto_tile.get("y") == 47
            and auto_route.get("trialTransitions") == "routeAssist"
            and auto_route.get("selectedRouteGoal") == "map2_02d"
            and auto_field.get("enabled") is True
            and auto_field.get("stepCount") == 0
            and auto_field.get("lastMap") == "map2_02d"
            and auto_save.get("originalRoutePromotionImplemented") is False
            and payload.get("map") == "map2_02d"
            and payload_tile.get("x") == 47
            and payload_tile.get("y") == 47
            and payload_route.get("trialTransitions") == "routeAssist"
            and payload_route.get("selectedRouteGoal") == "map2_02d"
            and payload_field.get("enabled") is True
            and payload_field.get("stepCount") == 0
            and payload_field.get("lastMap") == "map2_02d"
            and progress_counts.get("route-candidate") == 1
            and transition_feedback.get("kind") == "route-candidate"
            and transition_feedback.get("sourceMap") == "map1_01a"
            and transition_feedback.get("targetMap") == "map2_02d"
            and transition_feedback.get("trigger") == "movement"
            and transition_feedback.get("transitionKind") == "route-candidate"
            and transition_feedback.get("durationMs") == 1000
            and transition_feedback.get("transitionSound") == "transition"
            and str(transition_feedback.get("transitionSoundSrc") or "").endswith("/extract_wlk/07.wav")
            and transition_feedback.get("transitionSoundPlayed") is True
            and transition_feedback.get("browserMapTransitionFeedbackImplemented") is True
            and transition_feedback.get("originalTransitionTextRuntimeImplemented") is False
            and transition_feedback.get("originalRoutePromotionImplemented") is False
            and transition_feedback.get("originalStoryFlagRuntimeImplemented") is False
            and transition_feedback_render.get("active") is True
            and has_trial_blocker_fields(transition_feedback)
            and has_trial_blocker_fields(transition_feedback_render)
            and transition_feedback_render.get("transitionSound") == "transition"
            and str(transition_feedback_render.get("transitionSoundSrc") or "").endswith("/extract_wlk/07.wav")
            and transition_feedback_render.get("transitionSoundPlayed") is True
            and transition_feedback_render.get("browserMapTransitionFeedbackImplemented") is True
            and sound_counts.get("transition") == 1
            and sound_transition.get("key") == "transition"
            and "후보 map2_02d" in hud
            and "전투 0/6" in hud
            and "저장 있음" in hud
            and last_state.get("quickLoadHidden") is False
            and last_state.get("quickLoadText") == "임시 불러오기"
            and last_state.get("originalRoutePromotionImplemented") is False
        ):
            return last_state
        time.sleep(0.2)
    raise WebDriverError(f"play start candidate input transition did not finish/save: {last_state!r}")


def wait_for_play_start_route_target_map(port: int, session_id: str) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, map_state_script(), timeout=3)
        last_state = state
        foot = state.get("foot") or {}
        search = str(state.get("search") or "")
        field = state.get("fieldEncounter") or {}
        auto_save = state.get("routeAssistAutoSave") or {}
        auto_save_field = auto_save.get("fieldEncounter") or {}
        auto_save_route = auto_save.get("routeState") or {}
        auto_save_detail = (auto_save.get("progressEvent") or {}).get("detail") or {}
        route_progress_feedback = auto_save.get("routeProgressFeedback") or {}
        continuation_item = next(
            (row for row in (state.get("commandItems") or []) if row.get("command") == "continueRouteAssistPath"),
            {},
        )
        play_hud = " ".join(str(line) for line in (state.get("playHudLines") or []))
        if (
            state.get("readyState") == "complete"
            and state.get("hasScreen") is True
            and state.get("scene") == "map"
            and state.get("mapName") == "map2_02d"
            and foot.get("x") == 47
            and foot.get("y") == 47
            and state.get("quickLoadHidden") is False
            and state.get("quickLoadText") == "임시 불러오기"
            and state.get("trialTransitions") == "routeAssist"
            and state.get("selectedRouteGoal") == "map2_02d"
            and state.get("routePathHidden") is False
            and state.get("routePathValue") == "map2_02d"
            and state.get("routeNextHidden") is False
            and state.get("routeNextText") in {"도착", "완료", "이어가기"}
            and "map=map2_02d" in search
            and "startTile=47%2C47" in search
            and "trialTransitions=routeAssist" in search
            and "routeGoal=map2_02d" in search
            and "routeAutoSave=1" in search
            and "encounter=1" in search
            and field.get("enabled") is True
            and field.get("stepCount") == 0
            and field.get("lastMap") == "map2_02d"
            and auto_save.get("saved") is True
            and auto_save.get("source") == "routeAutoSave"
            and auto_save.get("map") == "map2_02d"
            and auto_save.get("payloadMap") == "map2_02d"
            and auto_save.get("sourceMap") == "map1_01a"
            and auto_save.get("targetMap") == "map2_02d"
            and auto_save.get("candidateKind") == "routeAssist-blocker"
            and auto_save.get("directTargetUrl") is True
            and (auto_save.get("payloadTile") or {}).get("x") == 47
            and (auto_save.get("payloadTile") or {}).get("y") == 47
            and auto_save_route.get("trialTransitions") == "routeAssist"
            and auto_save_route.get("selectedRouteGoal") == "map2_02d"
            and auto_save_field.get("enabled") is True
            and auto_save_field.get("lastMap") == "map2_02d"
            and has_trial_blocker_fields(auto_save)
            and has_trial_blocker_fields(auto_save_detail)
            and has_trial_blocker_fields(route_progress_feedback)
            and route_progress_feedback.get("text") == f"후보 완료 1/1 map2_02d · {TRIAL_BLOCKER_SHORT}"
            and route_progress_feedback.get("routeAutoSave") is True
            and route_progress_feedback.get("directTargetUrl") is True
            and "후보 map2_02d" in play_hud
            and "다음 목표 map2_09g" in play_hud
            and continuation_item.get("name") == "후보 이어가기 map2_09g"
            and continuation_item.get("routeTarget") == "map2_09g"
            and continuation_item.get("routeNextTarget") == "map2_09g"
            and continuation_item.get("usable") is True
            and "전투 0/6" in play_hud
            and "저장 있음" in play_hud
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"title play start route target did not preserve encounter/auto-save: {last_state!r}")


def wait_for_continued_play_start_route_target_map(port: int, session_id: str) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, map_state_script(), timeout=3)
        last_state = state
        foot = state.get("foot") or {}
        search = str(state.get("search") or "")
        field = state.get("fieldEncounter") or {}
        continuation_item = next(
            (row for row in (state.get("commandItems") or []) if row.get("command") == "continueRouteAssistPath"),
            {},
        )
        play_hud = " ".join(str(line) for line in (state.get("playHudLines") or []))
        if (
            state.get("readyState") == "complete"
            and state.get("hasScreen") is True
            and state.get("scene") == "map"
            and state.get("mapName") == "map2_02d"
            and foot.get("x") == 47
            and foot.get("y") == 47
            and state.get("quickLoadHidden") is False
            and state.get("quickLoadText") == "임시 불러오기"
            and state.get("trialTransitions") == "routeAssist"
            and state.get("selectedRouteGoal") == "map2_02d"
            and state.get("routePathHidden") is not True
            and state.get("routePathValue") == "map2_02d"
            and state.get("routeNextHidden") is False
            and state.get("routeNextText") in {"도착", "완료", "이어가기"}
            and "map=map2_02d" in search
            and "startTile=47%2C47" in search
            and "trialTransitions=routeAssist" in search
            and "routeGoal=map2_02d" in search
            and "encounter=1" in search
            and field.get("enabled") is True
            and field.get("stepCount") == 0
            and field.get("lastMap") == "map2_02d"
            and field.get("source") == "prototype-field-encounter"
            and "후보 map2_02d" in play_hud
            and "다음 목표 map2_09g" in play_hud
            and continuation_item.get("name") == "후보 이어가기 map2_09g"
            and continuation_item.get("routeTarget") == "map2_09g"
            and continuation_item.get("routeNextTarget") == "map2_09g"
            and continuation_item.get("usable") is True
            and "전투 0/6" in play_hud
            and "저장 있음" in play_hud
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"title continue did not restore play start route target: {last_state!r}")


def wait_for_play_start_target_field_encounter(port: int, session_id: str) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, play_start_target_field_encounter_state_script(), timeout=3)
        last_state = state if isinstance(state, dict) else {}
        field = last_state.get("fieldEncounter") or {}
        route = last_state.get("routeState") or {}
        progress = last_state.get("progress") or {}
        counts = progress.get("counts") or {}
        runtime = last_state.get("runtimeState") or {}
        saved_payload = last_state.get("savedPayload") or {}
        saved_route = saved_payload.get("routeState") or {}
        saved_field = saved_payload.get("fieldEncounter") or {}
        saved_tile = saved_payload.get("tile") or {}
        auto_save = last_state.get("victoryAutoSave") or {}
        auto_save_field = auto_save.get("fieldEncounter") or {}
        auto_save_route = auto_save.get("routeState") or {}
        auto_save_tile = auto_save.get("payloadTile") or {}
        movement = last_state.get("movement") or {}
        before_foot = movement.get("beforeFoot") or {}
        after_foot = movement.get("afterFoot") or {}
        if (
            last_state
            and last_state.get("error") is None
            and last_state.get("pending") is False
            and last_state.get("movementInput") is True
            and last_state.get("inputCode")
            and movement.get("tileChanged") is True
            and before_foot.get("x") == 47
            and before_foot.get("y") == 47
            and after_foot.get("x") is not None
            and after_foot.get("y") is not None
            and last_state.get("started") is True
            and last_state.get("scene") == "map"
            and last_state.get("mapName") == "map2_02d"
            and (last_state.get("foot") or {}).get("x") == after_foot.get("x")
            and (last_state.get("foot") or {}).get("y") == after_foot.get("y")
            and last_state.get("victoryResult") is True
            and last_state.get("closeResult") is True
            and last_state.get("buttonText")
            and field.get("enabled") is True
            and field.get("stepCount") == 0
            and field.get("lastMap") == "map2_02d"
            and route.get("trialTransitions") == "routeAssist"
            and route.get("selectedRouteGoal") == "map2_02d"
            and counts.get("battle-start") == 1
            and counts.get("route-candidate") == 1
            and counts.get("field-encounter") == 1
            and counts.get("field-encounter-victory") == 1
            and counts.get("battle-victory", 0) == 0
            and runtime.get("money") == 98
            and runtime.get("herbCount") == 1
            and "refresh_water:1" in (runtime.get("items") or [])
            and auto_save.get("saved") is True
            and auto_save.get("source") == "field-encounter-victory"
            and auto_save.get("payloadMap") == "map2_02d"
            and auto_save_tile.get("x") == after_foot.get("x")
            and auto_save_tile.get("y") == after_foot.get("y")
            and auto_save_field.get("enabled") is True
            and auto_save_field.get("stepCount") == 0
            and auto_save_field.get("lastMap") == "map2_02d"
            and auto_save_route.get("trialTransitions") == "routeAssist"
            and auto_save_route.get("selectedRouteGoal") == "map2_02d"
            and saved_payload.get("map") == "map2_02d"
            and saved_tile.get("x") == after_foot.get("x")
            and saved_tile.get("y") == after_foot.get("y")
            and saved_route.get("trialTransitions") == "routeAssist"
            and saved_route.get("selectedRouteGoal") == "map2_02d"
            and saved_field.get("enabled") is True
            and saved_field.get("stepCount") == 0
            and saved_field.get("lastMap") == "map2_02d"
            and last_state.get("quickLoadHidden") is False
            and last_state.get("quickLoadText") == "임시 불러오기"
            and last_state.get("originalRoutePromotionImplemented") is False
            and last_state.get("originalEncounterRuntimeImplemented") is False
        ):
            return last_state
        time.sleep(0.2)
    raise WebDriverError(f"play start target field encounter did not finish/save: {last_state!r}")


def wait_for_continued_play_start_target_encounter_map(port: int, session_id: str, expected_tile: dict[str, Any]) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    expected_x = expected_tile.get("x")
    expected_y = expected_tile.get("y")
    expected_search = f"startTile={expected_x}%2C{expected_y}"
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, map_state_script(), timeout=3)
        last_state = state
        foot = state.get("foot") or {}
        search = str(state.get("search") or "")
        field = state.get("fieldEncounter") or {}
        counts = ((state.get("prototypeProgress") or {}).get("counts") or {})
        play_hud = " ".join(str(line) for line in (state.get("playHudLines") or []))
        if (
            state.get("readyState") == "complete"
            and state.get("hasScreen") is True
            and state.get("scene") == "map"
            and state.get("mapName") == "map2_02d"
            and foot.get("x") == expected_x
            and foot.get("y") == expected_y
            and state.get("quickLoadHidden") is False
            and state.get("quickLoadText") == "임시 불러오기"
            and state.get("trialTransitions") == "routeAssist"
            and state.get("selectedRouteGoal") == "map2_02d"
            and state.get("routePathHidden") is not True
            and state.get("routePathValue") == "map2_02d"
            and state.get("routeNextHidden") is False
            and state.get("routeNextText") in {"도착", "완료", "이어가기"}
            and "map=map2_02d" in search
            and expected_search in search
            and "trialTransitions=routeAssist" in search
            and "routeGoal=map2_02d" in search
            and "encounter=1" in search
            and field.get("enabled") is True
            and field.get("stepCount") == 0
            and field.get("lastMap") == "map2_02d"
            and counts.get("battle-start") == 1
            and counts.get("field-encounter") == 1
            and counts.get("field-encounter-victory") == 1
            and counts.get("battle-victory", 0) == 0
            and state.get("runtimeMoney") == 98
            and state.get("herbCount") == 1
            and "refresh_water:1" in (state.get("runtimeItems") or [])
            and "후보 map2_02d" in play_hud
            and "전투 0/6" in play_hud
            and "진행 4" in play_hud
            and "저장 있음" in play_hud
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"title continue did not restore play start target encounter reward: {last_state!r}")


def wait_for_route_assist_goal_started_map(port: int, session_id: str, target: str) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, map_state_script(), timeout=3)
        last_state = state
        foot = state.get("foot") or {}
        search = str(state.get("search") or "")
        marker = state.get("titleRouteGoal") or {}
        play_hud = " ".join(str(line) for line in (state.get("playHudLines") or []))
        if (
            state.get("readyState") == "complete"
            and state.get("hasScreen") is True
            and state.get("scene") == "map"
            and state.get("mapName") == "map1_02b"
            and foot.get("x") == 11
            and foot.get("y") == 12
            and state.get("trialTransitions") == "routeAssist"
            and state.get("selectedRouteGoal") == target
            and state.get("routePathHidden") is False
            and state.get("routePathValue") == target
            and state.get("routeNextHidden") is False
            and state.get("routeNextText") == "다음"
            and marker.get("source") == "title-route-goal"
            and marker.get("target") == target
            and marker.get("originalRoutePromotionImplemented") is False
            and "map=map1_02b" in search
            and "startTile=11%2C12" in search
            and "trialTransitions=routeAssist" in search
            and f"routeGoal={target}" in search
            and f"후보 {target}" in play_hud
            and "다음 map1_01a" in play_hud
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"title routeGoal menu did not start route-assisted {target} map route: {last_state!r}")


def wait_for_continue_ready_title(port: int, session_id: str) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, title_state_script(), timeout=3)
        last_state = state
        if (
            state.get("readyState") == "complete"
            and state.get("hasScreen") is True
            and state.get("scene") == "title"
            and state.get("titleLoaded") is True
            and state.get("quickLoadHidden") is False
            and state.get("quickLoadText") == "이어하기"
            and state.get("titleMenuKeys") == TITLE_CONTINUE_KEYS
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"title continue button did not become ready: {last_state!r}")


def wait_for_continued_map(port: int, session_id: str) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, map_state_script(), timeout=3)
        last_state = state
        foot = state.get("foot") or {}
        progress_counts = ((state.get("prototypeProgress") or {}).get("counts") or {})
        play_hud = " ".join(str(line) for line in (state.get("playHudLines") or []))
        if (
            state.get("readyState") == "complete"
            and state.get("scene") == "map"
            and state.get("mapName") == "map1_02b"
            and foot.get("x") == 11
            and foot.get("y") == 12
            and state.get("quickLoadHidden") is False
            and state.get("quickLoadText") == "임시 불러오기"
            and "map=map1_02b" in str(state.get("search") or "")
            and "startTile=11%2C12" in str(state.get("search") or "")
            and state.get("hasRuntimeState") is True
            and state.get("runtimeMoney") == 60
            and "herb:0" in (state.get("runtimeItems") or [])
            and state.get("atahoHp") == 36
            and state.get("atahoHpMax") == 36
            and state.get("herbCount") == 0
            and progress_counts.get("item-use-prototype") == 1
            and state.get("loadedSaveSummary") is None
            and "Ataho HP 36/36" in play_hud
            and "소지금 60" in play_hud
            and "저장 있음" in play_hud
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"title continue did not restore quick-saved map: {last_state!r}")


def wait_for_continued_encounter_map(port: int, session_id: str, expected_tile: dict[str, Any]) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    expected_x = expected_tile.get("x")
    expected_y = expected_tile.get("y")
    expected_search = f"startTile={expected_x}%2C{expected_y}"
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, map_state_script(), timeout=3)
        last_state = state
        foot = state.get("foot") or {}
        field = state.get("fieldEncounter") or {}
        search = str(state.get("search") or "")
        if (
            state.get("readyState") == "complete"
            and state.get("hasScreen") is True
            and state.get("scene") == "map"
            and state.get("mapName") == "map1_02b"
            and foot.get("x") == expected_x
            and foot.get("y") == expected_y
            and state.get("quickLoadHidden") is False
            and state.get("quickLoadText") == "임시 불러오기"
            and "map=map1_02b" in search
            and expected_search in search
            and "encounter=1" in search
            and field.get("enabled") is True
            and field.get("stepCount") == 1
            and field.get("source") == "prototype-field-encounter"
            and field.get("originalEncounterTableMapped") is False
            and field.get("originalEncounterRuntimeImplemented") is False
            and state.get("fieldEncounterMenuLabel") == "전투 탐색 끄기 1/6"
            and any("전투 1/6" in str(line) for line in (state.get("playHudLines") or []))
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"title continue did not restore quick-saved field encounters: {last_state!r}")


def wait_for_continued_disabled_encounter_map(port: int, session_id: str, expected_tile: dict[str, Any]) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    expected_x = expected_tile.get("x")
    expected_y = expected_tile.get("y")
    expected_search = f"startTile={expected_x}%2C{expected_y}"
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, map_state_script(), timeout=3)
        last_state = state
        foot = state.get("foot") or {}
        field = state.get("fieldEncounter") or {}
        search = str(state.get("search") or "")
        play_hud = " ".join(str(line) for line in (state.get("playHudLines") or []))
        if (
            state.get("readyState") == "complete"
            and state.get("hasScreen") is True
            and state.get("scene") == "map"
            and state.get("mapName") == "map1_02b"
            and foot.get("x") == expected_x
            and foot.get("y") == expected_y
            and state.get("quickLoadHidden") is False
            and state.get("quickLoadText") == "임시 불러오기"
            and "map=map1_02b" in search
            and expected_search in search
            and "encounter=1" not in search
            and field.get("enabled") is False
            and field.get("stepCount") == 0
            and field.get("source") == "prototype-field-encounter"
            and field.get("originalEncounterTableMapped") is False
            and field.get("originalEncounterRuntimeImplemented") is False
            and state.get("fieldEncounterMenuLabel") == "전투 탐색 켜기"
            and "저장 있음" in play_hud
            and "전투 1/6" not in play_hud
            and "필드 전투" not in play_hud
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"title continue did not restore disabled field encounters: {last_state!r}")


def wait_for_continued_route_goal_map(port: int, session_id: str, target: str) -> dict[str, Any]:
    deadline = time.monotonic() + 10
    last_state: dict[str, Any] = {}
    while time.monotonic() < deadline:
        state = execute_js(port, session_id, map_state_script(), timeout=3)
        last_state = state
        foot = state.get("foot") or {}
        search = str(state.get("search") or "")
        play_hud = " ".join(str(line) for line in (state.get("playHudLines") or []))
        if (
            state.get("readyState") == "complete"
            and state.get("hasScreen") is True
            and state.get("scene") == "map"
            and state.get("mapName") == "map1_02b"
            and foot.get("x") == 11
            and foot.get("y") == 12
            and state.get("quickLoadHidden") is False
            and state.get("quickLoadText") == "임시 불러오기"
            and state.get("trialTransitions") == "routeAssist"
            and state.get("selectedRouteGoal") == target
            and state.get("routePathHidden") is False
            and state.get("routePathValue") == target
            and state.get("routeNextHidden") is False
            and state.get("routeNextText") == "다음"
            and "map=map1_02b" in search
            and "startTile=11%2C12" in search
            and "trialTransitions=routeAssist" in search
            and f"routeGoal={target}" in search
            and f"후보 {target}" in play_hud
            and "다음 map1_01a" in play_hud
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"title continue did not restore quick-saved route goal {target}: {last_state!r}")


def write_report(payload: dict[str, Any]) -> None:
    OUT.mkdir(parents=True, exist_ok=True)
    (OUT / "title_start_browser_smoke.json").write_text(
        json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + "\n",
        encoding="utf-8",
    )
    lines = [
        "# Title Start Browser Smoke",
        "",
        f"- status: `{payload.get('status')}`",
        f"- url: `{payload.get('url')}`",
        f"- title checksum: `{payload.get('titleChecksum')}`",
        f"- map checksum: `{payload.get('mapChecksum')}`",
        f"- query play: map `{payload['queryPlay']['mapName']}` `{payload['queryPlay']['foot']['x']},{payload['queryPlay']['foot']['y']}` goal `{payload['queryPlay']['selectedRouteGoal']}` marker `{payload['queryPlay'].get('titleRouteGoal')}` field `{payload['queryPlay'].get('fieldEncounter')}` HUD `{payload['queryPlay'].get('playHudLines')}`",
        f"- play start: click `{payload['playStartClick']}` map `{payload['playStart']['mapName']}` `{payload['playStart']['foot']['x']},{payload['playStart']['foot']['y']}` goal `{payload['playStart']['selectedRouteGoal']}` marker `{payload['playStart'].get('titleRouteGoal')}` field `{payload['playStart'].get('fieldEncounter')}` HUD `{payload['playStart'].get('playHudLines')}`",
        f"- play start continue: title `{payload['continuePlayStartTitle'].get('quickLoadText')}` labels `{payload['continuePlayStartTitle'].get('titleMenuLabels')}` click `{payload['continuePlayStartClick']}` map `{payload['continuePlayStart']['mapName']}` `{payload['continuePlayStart']['foot']['x']},{payload['continuePlayStart']['foot']['y']}` goal `{payload['continuePlayStart']['selectedRouteGoal']}` field `{payload['continuePlayStart'].get('fieldEncounter')}` HUD `{payload['continuePlayStart'].get('playHudLines')}`",
        f"- play start route next: click `{payload['playStartRouteNextClick']}` map `{payload['playStartRouteNext']['mapName']}` `{payload['playStartRouteNext']['foot']['x']},{payload['playStartRouteNext']['foot']['y']}` goal `{payload['playStartRouteNext']['selectedRouteGoal']}` auto `{payload['playStartRouteNext'].get('routeAssistAutoSave')}` field `{payload['playStartRouteNext'].get('fieldEncounter')}` HUD `{payload['playStartRouteNext'].get('playHudLines')}`",
        f"- play start route focus: click `{payload['playStartRouteFocusClick']}` map `{payload['playStartRouteFocus']['mapName']}` `{payload['playStartRouteFocus']['foot']['x']},{payload['playStartRouteFocus']['foot']['y']}` goal `{payload['playStartRouteFocus']['selectedRouteGoal']}` auto `{payload['playStartRouteFocus'].get('routeAssistAutoSave')}` field `{payload['playStartRouteFocus'].get('fieldEncounter')}` HUD `{payload['playStartRouteFocus'].get('playHudLines')}`",
        f"- play start route target: click `{payload['playStartRouteTargetClick']}` map `{payload['playStartRouteTarget']['mapName']}` `{payload['playStartRouteTarget']['foot']['x']},{payload['playStartRouteTarget']['foot']['y']}` goal `{payload['playStartRouteTarget']['selectedRouteGoal']}` auto `{payload['playStartRouteTarget'].get('routeAssistAutoSave')}` field `{payload['playStartRouteTarget'].get('fieldEncounter')}` HUD `{payload['playStartRouteTarget'].get('playHudLines')}`",
        f"- play start route target continue: title `{payload['continuePlayStartRouteTargetTitle'].get('quickLoadText')}` labels `{payload['continuePlayStartRouteTargetTitle'].get('titleMenuLabels')}` click `{payload['continuePlayStartRouteTargetClick']}` map `{payload['continuePlayStartRouteTarget']['mapName']}` `{payload['continuePlayStartRouteTarget']['foot']['x']},{payload['continuePlayStartRouteTarget']['foot']['y']}` goal `{payload['continuePlayStartRouteTarget']['selectedRouteGoal']}` field `{payload['continuePlayStartRouteTarget'].get('fieldEncounter')}` HUD `{payload['continuePlayStartRouteTarget'].get('playHudLines')}`",
        f"- play start target encounter: input `{payload['playStartTargetEncounter'].get('inputCode')}` movement `{payload['playStartTargetEncounter'].get('movement')}` started `{payload['playStartTargetEncounter'].get('started')}` map `{payload['playStartTargetEncounter'].get('mapName')}` `{payload['playStartTargetEncounter'].get('foot', {}).get('x')},{payload['playStartTargetEncounter'].get('foot', {}).get('y')}` route `{payload['playStartTargetEncounter'].get('routeState')}` auto `{payload['playStartTargetEncounter'].get('victoryAutoSave')}` progress `{(payload['playStartTargetEncounter'].get('progress') or {}).get('counts')}` runtime `{payload['playStartTargetEncounter'].get('runtimeState')}`",
        f"- play start target encounter continue: title `{payload['continuePlayStartTargetEncounterTitle'].get('quickLoadText')}` labels `{payload['continuePlayStartTargetEncounterTitle'].get('titleMenuLabels')}` click `{payload['continuePlayStartTargetEncounterClick']}` map `{payload['continuePlayStartTargetEncounter']['mapName']}` `{payload['continuePlayStartTargetEncounter']['foot']['x']},{payload['continuePlayStartTargetEncounter']['foot']['y']}` goal `{payload['continuePlayStartTargetEncounter']['selectedRouteGoal']}` field `{payload['continuePlayStartTargetEncounter'].get('fieldEncounter')}` progress `{(payload['continuePlayStartTargetEncounter'].get('prototypeProgress') or {}).get('counts')}` money `{payload['continuePlayStartTargetEncounter'].get('runtimeMoney')}` herb `{payload['continuePlayStartTargetEncounter'].get('herbCount')}` HUD `{payload['continuePlayStartTargetEncounter'].get('playHudLines')}`",
        f"- play start input confirmed transition: input `{payload['playStartConfirmedInput'].get('inputCode')}` before `{payload['playStartConfirmedInput'].get('before')}` after `{payload['playStartConfirmedInput'].get('after')}` auto `{payload['playStartConfirmedInput'].get('mapTransitionAutoSave')}` progress `{(payload['playStartConfirmedInput'].get('progress') or {}).get('counts')}` transitionFeedback `{map_transition_feedback_summary(payload['playStartConfirmedInput'])}` transitionFeedbackRender `{map_transition_feedback_rendered(payload['playStartConfirmedInput'])}` transitionSound `{map_transition_feedback_sound_summary(payload['playStartConfirmedInput'])}`",
        f"- play start input candidate transition: input `{payload['playStartCandidateInput'].get('inputCode')}` before `{payload['playStartCandidateInput'].get('before')}` after `{payload['playStartCandidateInput'].get('after')}` auto `{payload['playStartCandidateInput'].get('routeCandidateAutoSave')}` progress `{(payload['playStartCandidateInput'].get('progress') or {}).get('counts')}` transitionFeedback `{map_transition_feedback_summary(payload['playStartCandidateInput'])}` transitionFeedbackRender `{map_transition_feedback_rendered(payload['playStartCandidateInput'])}` transitionSound `{map_transition_feedback_sound_summary(payload['playStartCandidateInput'])}`",
        f"- play start input continue: title `{payload['continuePlayStartInputTitle'].get('quickLoadText')}` labels `{payload['continuePlayStartInputTitle'].get('titleMenuLabels')}` click `{payload['continuePlayStartInputClick']}` map `{payload['continuePlayStartInput']['mapName']}` `{payload['continuePlayStartInput']['foot']['x']},{payload['continuePlayStartInput']['foot']['y']}` goal `{payload['continuePlayStartInput']['selectedRouteGoal']}` field `{payload['continuePlayStartInput'].get('fieldEncounter')}` HUD `{payload['continuePlayStartInput'].get('playHudLines')}`",
        f"- title savedat: `{payload['titleSavedatRequest'].get('request')}` clicked `{payload['titleSavedatRequest'].get('clicked')}` labels `{payload['titleSavedatRequest'].get('labels')}`",
        f"- title savedat scan: click `{payload['titleSavedatScanClick']}` state `{payload['titleSavedatScan'].get('titleSavedatScan')}` render `{payload['titleSavedatScan'].get('titleMenuRender')}`",
        f"- title public savedat: click `{payload['titleSampleSavedatClick']}` loaded `{payload['titleSampleSavedat'].get('titlePublicSavedat')}` map `{payload['titleSampleSavedat']['mapName']}` `{payload['titleSampleSavedat']['foot']['x']},{payload['titleSampleSavedat']['foot']['y']}` summary `{payload['titleSampleSavedat'].get('loadedSaveSummary')}`",
        f"- title public savedat quick save: `{payload['titleSampleSavedatQuickSave'].get('loadedSaveSummary')}` HUD `{payload['titleSampleSavedatQuickSave'].get('playHudLines')}`",
        f"- title public savedat continue: title `{payload['continueSampleSavedatTitle'].get('quickLoadText')}` labels `{payload['continueSampleSavedatTitle'].get('titleMenuLabels')}` click `{payload['continueSampleSavedatClick']}` map `{payload['continueSampleSavedat']['mapName']}` `{payload['continueSampleSavedat']['foot']['x']},{payload['continueSampleSavedat']['foot']['y']}` summary `{payload['continueSampleSavedat'].get('loadedSaveSummary')}` HUD `{payload['continueSampleSavedat'].get('playHudLines')}`",
        f"- started map: `{payload['map']['mapName']}` `{payload['map']['foot']['x']},{payload['map']['foot']['y']}`",
        f"- play HUD: `{payload['map'].get('playHudLines')}`",
        f"- quick save HUD: `{payload['quickSave'].get('playHudLines')}` saveFeedback `{runtime_save_feedback_summary(payload['quickSave'])}` saveFeedbackRender `{runtime_save_feedback_rendered(payload['quickSave'])}` saveFeedbackSound `{runtime_save_feedback_sound_summary(payload['quickSave'])}`",
        f"- direct URL runtime: `{payload['directUrl']['mapName']}` money `{payload['directUrl'].get('runtimeMoney')}` items `{payload['directUrl'].get('runtimeItems')}`",
        f"- new game item: `{payload['itemUse'].get('itemLabel')}` marker `{payload['itemUse'].get('marker')}` itemTextTable `{(payload['itemUse'].get('marker') or {}).get('itemTextTableKey')}` itemTextRef `{(payload['itemUse'].get('marker') or {}).get('itemTextTableRefVaHex')}` itemTextVa `{(payload['itemUse'].get('marker') or {}).get('itemTextTableTextVaHex')}` itemFeedback `{inventory_item_feedback_summary(payload['itemUse'])}` itemFeedbackRender `{inventory_item_feedback_rendered(payload['itemUse'])}` itemFeedbackSound `{((payload['itemUse'].get('itemFeedbackLast') or {}).get('inventoryItemSound'))}` itemFeedbackSoundSrc `{((payload['itemUse'].get('itemFeedbackLast') or {}).get('inventoryItemSoundSrc'))}` itemFeedbackSoundPlayed `{((payload['itemUse'].get('itemFeedbackLast') or {}).get('inventoryItemSoundPlayed'))}` itemFailFeedback `{inventory_item_feedback_summary(payload['itemUse'].get('noTargetFailure') or {})}` itemFailFeedbackRender `{inventory_item_feedback_rendered(payload['itemUse'].get('noTargetFailure') or {})}` statusFailFeedback `{inventory_item_feedback_summary(payload['itemUse'].get('noStatusFailure') or {})}` statusFailFeedbackRender `{inventory_item_feedback_rendered(payload['itemUse'].get('noStatusFailure') or {})}` itemFailSoundItemCount `{((((payload['itemUse'].get('noTargetFailure') or {}).get('soundState') or {}).get('counts') or {}).get('item'))}` statusFailSoundItemCount `{((((payload['itemUse'].get('noStatusFailure') or {}).get('soundState') or {}).get('counts') or {}).get('item'))}` itemSoundItemCount `{(((payload['itemUse'].get('soundState') or {}).get('counts') or {}).get('item'))}` objective `{(payload['itemUse'].get('objectiveBefore') or {}).get('title')}` action `{(payload['itemUse'].get('objectiveAction') or {}).get('action')}` active `{(payload['itemUse'].get('objectiveAction') or {}).get('activeId')}` {item_use_completion_notice_feedback_summary(payload['itemUse'])}",
        f"- field item target: inventoryMenu `{payload['fieldItemTarget'].get('inventoryOpenMarker')}` inventorySelection `{payload['fieldItemTarget'].get('inventorySelectionMarker')}` open `{(payload['fieldItemTarget'].get('openMarker') or {}).get('phase')}` selected `{(payload['fieldItemTarget'].get('selectedMarker') or {}).get('selectedTargetName')}` openMarker `{payload['fieldItemTarget'].get('openMarker')}` selectedMarker `{payload['fieldItemTarget'].get('selectedMarker')}` marker `{payload['fieldItemTarget'].get('marker')}` after `{payload['fieldItemTarget'].get('after')}` auto `{payload['fieldItemTarget'].get('autoSave')}` feedback `{inventory_item_feedback_summary(payload['fieldItemTarget'])}` feedbackRender `{inventory_item_feedback_rendered(payload['fieldItemTarget'])}` sound `{inventory_item_feedback_sound_summary(payload['fieldItemTarget'])}` objective `{(payload['fieldItemTargetObjective'].get('objectiveBefore') or {}).get('title')}` objectiveDetail `{(payload['fieldItemTargetObjective'].get('objectiveBefore') or {}).get('detail')}` objectiveAction `{(payload['fieldItemTargetObjective'].get('objectiveAction') or {}).get('action')}` objectiveActive `{(payload['fieldItemTargetObjective'].get('objectiveAction') or {}).get('activeId')}` {item_use_completion_notice_feedback_summary(payload['fieldItemTargetObjective'])}",
        f"- new game item continue: title `{payload['continueTitle'].get('quickLoadText')}` labels `{payload['continueTitle'].get('titleMenuLabels')}` map `{payload['continue']['mapName']}` `{payload['continue']['foot']['x']},{payload['continue']['foot']['y']}` hp `{payload['continue'].get('atahoHp')}/{payload['continue'].get('atahoHpMax')}` herb `{payload['continue'].get('herbCount')}` progress `{(payload['continue'].get('prototypeProgress') or {}).get('counts')}` loadFeedback `{runtime_save_feedback_summary(payload['continue'])}` loadFeedbackRender `{runtime_save_feedback_rendered(payload['continue'])}` loadFeedbackSound `{runtime_save_feedback_sound_summary(payload['continue'])}` objective `{(payload['itemUseTitleObjective'].get('objectiveBefore') or {}).get('title')}` action `{(payload['itemUseTitleObjective'].get('objectiveAction') or {}).get('action')}` active `{(payload['itemUseTitleObjective'].get('objectiveAction') or {}).get('activeId')}` {item_use_completion_notice_feedback_summary(payload['itemUseTitleObjective'])} HUD `{payload['continue'].get('playHudLines')}`",
        f"- submenu cancel: `{payload['submenuCancel'].get('before')}` -> `{payload['submenuCancel'].get('afterBack')}` -> `{payload['submenuCancel'].get('afterClose')}`",
        f"- mobile menu cancel: `{payload['submenuCancel'].get('mobileBefore')}` -> `{payload['submenuCancel'].get('afterMobileBack')}` -> `{payload['submenuCancel'].get('afterMobileClose')}`",
        f"- mobile B cancel: `{payload['submenuCancel'].get('cancelBefore')}` -> `{payload['submenuCancel'].get('afterCancelBack')}` -> `{payload['submenuCancel'].get('afterCancelClose')}`",
        f"- return title: scene `{payload['returnTitle']['scene']}` menu `{payload['returnTitle']['titleMenuKeys']}` visible `{payload['returnTitle'].get('visibleLabels')}` pointer `{payload['returnTitle'].get('pointerHitIndex')}`",
        f"- encounter start: `{payload['encounterStart']['mapName']}` `{payload['encounterStart']['foot']['x']},{payload['encounterStart']['foot']['y']}` field `{payload['encounterStart'].get('fieldEncounter')}` modeAutoSource `{(payload['encounterStart'].get('fieldEncounterModeAutoSave') or {}).get('source')}` modeFeedback `{field_encounter_feedback_summary(payload['encounterStart'])}` modeFeedbackRender `{field_encounter_feedback_rendered(payload['encounterStart'])}`",
        f"- encounter step auto-save: input `{payload['encounterStepAutoSave'].get('inputCode')}` movement `{payload['encounterStepAutoSave'].get('movement')}` source `{(payload['encounterStepAutoSave'].get('autoSave') or {}).get('source')}` scope `{(payload['encounterStepAutoSave'].get('autoSave') or {}).get('scope')}` field `{(payload['encounterStepAutoSave'].get('autoSave') or {}).get('fieldEncounter')}` encounterFeedback `{field_encounter_feedback_summary(payload['encounterStepAutoSave'])}` encounterFeedbackRender `{field_encounter_feedback_rendered(payload['encounterStepAutoSave'])}` encounterFeedbackSound `{field_encounter_feedback_sound_summary(payload['encounterStepAutoSave'])}` HUD `{payload['encounterStepAutoSave'].get('playHudLines')}`",
        f"- encounter continue: title `{payload['continueEncounterTitle'].get('quickLoadText')}` labels `{payload['continueEncounterTitle'].get('titleMenuLabels')}` click `{payload['continueEncounterClick']}` map `{payload['continueEncounter']['mapName']}` `{payload['continueEncounter']['foot']['x']},{payload['continueEncounter']['foot']['y']}` field `{payload['continueEncounter'].get('fieldEncounter')}` HUD `{payload['continueEncounter'].get('playHudLines')}`",
        f"- encounter disable: source `{(payload['encounterDisable'].get('autoSave') or {}).get('source')}` label `{payload['encounterDisable'].get('labelBefore')}` -> `{payload['encounterDisable'].get('labelAfter')}` field `{payload['encounterDisable'].get('fieldEncounter')}` modeFeedback `{field_encounter_feedback_summary(payload['encounterDisable'])}` modeFeedbackRender `{field_encounter_feedback_rendered(payload['encounterDisable'])}` HUD `{payload['encounterDisable'].get('playHudLines')}`",
        f"- encounter disabled continue: title `{payload['continueDisabledEncounterTitle'].get('quickLoadText')}` labels `{payload['continueDisabledEncounterTitle'].get('titleMenuLabels')}` click `{payload['continueDisabledEncounterClick']}` map `{payload['continueDisabledEncounter']['mapName']}` `{payload['continueDisabledEncounter']['foot']['x']},{payload['continueDisabledEncounter']['foot']['y']}` field `{payload['continueDisabledEncounter'].get('fieldEncounter')}` HUD `{payload['continueDisabledEncounter'].get('playHudLines')}`",
        f"- routeAssist start: `{payload['routeAssist']['mapName']}` `{payload['routeAssist']['foot']['x']},{payload['routeAssist']['foot']['y']}` goal `{payload['routeAssist']['selectedRouteGoal']}` next `{payload['routeAssist']['routeNextText']}`",
        f"- routeAssist menu: `{payload['routeAssistMenu']['mapName']}` `{payload['routeAssistMenu']['foot']['x']},{payload['routeAssistMenu']['foot']['y']}` goal `{payload['routeAssistMenu']['selectedRouteGoal']}`",
        f"- title route goal: click `{payload['routeGoalClick']}` map `{payload['routeGoalTitle']['mapName']}` `{payload['routeGoalTitle']['foot']['x']},{payload['routeGoalTitle']['foot']['y']}` goal `{payload['routeGoalTitle']['selectedRouteGoal']}` marker `{payload['routeGoalTitle'].get('titleRouteGoal')}`",
        f"- route goal quick save: `{payload['routeGoalQuickSave'].get('routeState')}` HUD `{payload['routeGoalQuickSave'].get('playHudLines')}`",
        f"- route goal continue: title `{payload['continueRouteGoalTitle'].get('quickLoadText')}` labels `{payload['continueRouteGoalTitle'].get('titleMenuLabels')}` click `{payload['continueRouteGoalClick']}` map `{payload['continueRouteGoal']['mapName']}` `{payload['continueRouteGoal']['foot']['x']},{payload['continueRouteGoal']['foot']['y']}` goal `{payload['continueRouteGoal']['selectedRouteGoal']}` HUD `{payload['continueRouteGoal'].get('playHudLines')}`",
        f"- title deep route goal: click `{payload['deepRouteGoalClick']}` map `{payload['deepRouteGoalTitle']['mapName']}` `{payload['deepRouteGoalTitle']['foot']['x']},{payload['deepRouteGoalTitle']['foot']['y']}` goal `{payload['deepRouteGoalTitle']['selectedRouteGoal']}` marker `{payload['deepRouteGoalTitle'].get('titleRouteGoal')}`",
        f"- deep route goal quick save: `{payload['deepRouteGoalQuickSave'].get('routeState')}` HUD `{payload['deepRouteGoalQuickSave'].get('playHudLines')}`",
        f"- deep route goal continue: title `{payload['continueDeepRouteGoalTitle'].get('quickLoadText')}` labels `{payload['continueDeepRouteGoalTitle'].get('titleMenuLabels')}` click `{payload['continueDeepRouteGoalClick']}` map `{payload['continueDeepRouteGoal']['mapName']}` `{payload['continueDeepRouteGoal']['foot']['x']},{payload['continueDeepRouteGoal']['foot']['y']}` goal `{payload['continueDeepRouteGoal']['selectedRouteGoal']}` HUD `{payload['continueDeepRouteGoal'].get('playHudLines')}`",
        f"- continue: `{payload['continue']['mapName']}` `{payload['continue']['foot']['x']},{payload['continue']['foot']['y']}` button `{payload['continueTitle']['quickLoadText']}` labels `{payload['continueTitle'].get('titleMenuLabels')}`",
        "",
    ]


def verify_browser(base: str, keep_log: bool = False) -> None:
    driver_path = shutil.which("WebKitWebDriver")
    if not driver_path:
        raise WebDriverError("WebKitWebDriver is not installed; install webkit2gtk-driver and run under Xvfb")

    port = free_port()
    log_path = OUT / "title_start_webkitdriver.log"
    log_path.parent.mkdir(parents=True, exist_ok=True)
    with log_path.open("wb") as log:
        proc = subprocess.Popen(
            [
                driver_path,
                "--host=127.0.0.1",
                f"--port={port}",
                "--replace-on-new-session",
            ],
            stdout=log,
            stderr=subprocess.STDOUT,
        )
        session_id = ""
        try:
            wait_for_driver(port, proc)
            session = request_json(
                port,
                "POST",
                "/session",
                {"capabilities": {"alwaysMatch": {"browserName": "MiniBrowser"}}},
                timeout=30,
            )
            session_id = str((session or {}).get("value", {}).get("sessionId") or "")
            if not session_id:
                raise WebDriverError(f"could not create WebKit session: {session!r}")
            request_json(
                port,
                "POST",
                f"/session/{session_id}/window/rect",
                {"x": 0, "y": 0, "width": 860, "height": 620},
                timeout=8,
            )
            url = urljoin(base.rstrip("/") + "/", "/web/game.html?game=1")
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            wait_for_title(port, session_id)
            execute_js(port, session_id, clear_runtime_save_script())
            title = execute_js(port, session_id, title_state_script(), timeout=3)
            title_labels = title.get("titleMenuLabels") or []
            if (
                not isinstance(title, dict)
                or title.get("titleMenuKeys") != TITLE_START_KEYS
                or title.get("selectedTitleMenuIndex") != 1
                or title_labels[:2] != ["확정 루트", "처음부터"]
            ):
                raise WebDriverError(f"title menu did not reset to play-first start menu: {title!r}")
            title_checksum = execute_js(port, session_id, canvas_checksum_script())
            if not isinstance(title_checksum, int) or title_checksum == 0:
                raise WebDriverError(f"title canvas checksum was blank: {title_checksum!r}")
            query_play_url = urljoin(base.rstrip("/") + "/", "/web/game.html?play=1")
            request_json(port, "POST", f"/session/{session_id}/url", {"url": query_play_url}, timeout=30)
            query_play_map = wait_for_play_started_map(
                port,
                session_id,
                expected_source="query-play-start",
                require_play_query=True,
            )
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            wait_for_title(port, session_id)
            execute_js(port, session_id, clear_runtime_save_script())
            play_start_click = execute_js(port, session_id, press_title_enter_script(), timeout=3)
            if (
                not isinstance(play_start_click, dict)
                or play_start_click.get("ok") is not True
                or play_start_click.get("selectedTitleMenuIndex") != 1
                or (play_start_click.get("keys") or [None, None])[1] != "playStart"
            ):
                raise WebDriverError(f"title default Enter/A did not dispatch play start: {play_start_click!r}")
            play_start_map = wait_for_play_started_map(port, session_id)
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            continue_play_start_title = wait_for_continue_ready_title(port, session_id)
            if not any("이어하기 map1_02b 11,12 전투 후보 map2_02d" in str(label) for label in (continue_play_start_title.get("titleMenuLabels") or [])):
                raise WebDriverError(f"title play start continue row did not summarize auto save: {continue_play_start_title!r}")
            continue_play_start_click = execute_js(port, session_id, click_title_continue_script())
            if not isinstance(continue_play_start_click, dict) or continue_play_start_click.get("ok") is not True:
                raise WebDriverError(f"title play start continue button was not usable: {continue_play_start_click!r}")
            continued_play_start_map = wait_for_continued_play_start_map(port, session_id)
            play_start_route_next_click = execute_js(port, session_id, click_route_next_button_script(), timeout=3)
            if not isinstance(play_start_route_next_click, dict) or play_start_route_next_click.get("ok") is not True:
                raise WebDriverError(f"title play start route next button was not usable: {play_start_route_next_click!r}")
            play_start_route_next_map = wait_for_play_start_route_next_map(port, session_id)
            play_start_route_focus_click = execute_js(port, session_id, click_route_next_button_script(), timeout=3)
            if not isinstance(play_start_route_focus_click, dict) or play_start_route_focus_click.get("ok") is not True:
                raise WebDriverError(f"title play start route focus button was not usable: {play_start_route_focus_click!r}")
            play_start_route_focus_map = wait_for_play_start_route_focus_map(port, session_id)
            play_start_route_target_click = execute_js(port, session_id, click_route_next_button_script(), timeout=3)
            if not isinstance(play_start_route_target_click, dict) or play_start_route_target_click.get("ok") is not True:
                raise WebDriverError(f"title play start route target button was not usable: {play_start_route_target_click!r}")
            play_start_route_target_map = wait_for_play_start_route_target_map(port, session_id)
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            continue_play_start_route_target_title = wait_for_continue_ready_title(port, session_id)
            if not any("이어하기 map2_02d 47,47 전투 후보 map2_02d" in str(label) for label in (continue_play_start_route_target_title.get("titleMenuLabels") or [])):
                raise WebDriverError(f"title play start route target continue row did not summarize auto save: {continue_play_start_route_target_title!r}")
            continue_play_start_route_target_click = execute_js(port, session_id, click_title_continue_script())
            if not isinstance(continue_play_start_route_target_click, dict) or continue_play_start_route_target_click.get("ok") is not True:
                raise WebDriverError(f"title play start route target continue button was not usable: {continue_play_start_route_target_click!r}")
            continued_play_start_route_target_map = wait_for_continued_play_start_route_target_map(port, session_id)
            execute_js(port, session_id, play_start_target_field_encounter_script(), timeout=3)
            play_start_target_encounter = wait_for_play_start_target_field_encounter(port, session_id)
            play_start_target_encounter_tile = (play_start_target_encounter.get("savedPayload") or {}).get("tile") or {}
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            continue_play_start_target_encounter_title = wait_for_continue_ready_title(port, session_id)
            play_start_target_encounter_label = (
                f"이어하기 map2_02d {play_start_target_encounter_tile.get('x')},"
                f"{play_start_target_encounter_tile.get('y')} 전투 후보 map2_02d"
            )
            if not any(play_start_target_encounter_label in str(label) for label in (continue_play_start_target_encounter_title.get("titleMenuLabels") or [])):
                raise WebDriverError(
                    "title play start target encounter continue row did not summarize battle reward save: "
                    f"{continue_play_start_target_encounter_title!r}"
                )
            continue_play_start_target_encounter_click = execute_js(port, session_id, click_title_continue_script())
            if (
                not isinstance(continue_play_start_target_encounter_click, dict)
                or continue_play_start_target_encounter_click.get("ok") is not True
            ):
                raise WebDriverError(
                    "title play start target encounter continue button was not usable: "
                    f"{continue_play_start_target_encounter_click!r}"
                )
            continued_play_start_target_encounter_map = wait_for_continued_play_start_target_encounter_map(
                port,
                session_id,
                play_start_target_encounter_tile,
            )
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            wait_for_title(port, session_id)
            execute_js(port, session_id, clear_runtime_save_script())
            play_start_input_click = execute_js(port, session_id, click_title_play_start_script(), timeout=3)
            if not isinstance(play_start_input_click, dict) or play_start_input_click.get("ok") is not True:
                raise WebDriverError(f"title play start input menu row was not clickable: {play_start_input_click!r}")
            play_start_input_start = wait_for_play_started_map(port, session_id)
            execute_js(
                port,
                session_id,
                route_input_transition_script("map1_02b", "map1_01a", "ArrowUp"),
                timeout=3,
            )
            play_start_confirmed_input = wait_for_play_start_confirmed_input_transition(port, session_id)
            play_start_input_focus_click = execute_js(port, session_id, click_route_next_button_script(), timeout=3)
            if not isinstance(play_start_input_focus_click, dict) or play_start_input_focus_click.get("ok") is not True:
                raise WebDriverError(f"title play start input route focus button was not usable: {play_start_input_focus_click!r}")
            play_start_input_focus = wait_for_play_start_route_focus_map(port, session_id)
            execute_js(
                port,
                session_id,
                route_input_transition_script("map1_01a", "map2_02d", "ArrowUp"),
                timeout=3,
            )
            play_start_candidate_input = wait_for_play_start_candidate_input_transition(port, session_id)
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            continue_play_start_input_title = wait_for_continue_ready_title(port, session_id)
            if not any("이어하기 map2_02d 47,47 전투 후보 map2_02d" in str(label) for label in (continue_play_start_input_title.get("titleMenuLabels") or [])):
                raise WebDriverError(
                    "title play start input transition continue row did not summarize target save: "
                    f"{continue_play_start_input_title!r}"
                )
            continue_play_start_input_click = execute_js(port, session_id, click_title_continue_script())
            if not isinstance(continue_play_start_input_click, dict) or continue_play_start_input_click.get("ok") is not True:
                raise WebDriverError(
                    f"title play start input transition continue button was not usable: {continue_play_start_input_click!r}"
                )
            continued_play_start_input = wait_for_continued_play_start_route_target_map(port, session_id)
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            wait_for_title(port, session_id)
            execute_js(port, session_id, clear_runtime_save_script())
            title_savedat_request = execute_js(port, session_id, click_title_load_savedat_script(), timeout=3)
            savedat_request = (title_savedat_request or {}).get("request") or {}
            savedat_clicked = (title_savedat_request or {}).get("clicked") or {}
            if (
                not isinstance(title_savedat_request, dict)
                or title_savedat_request.get("ok") is not True
                or savedat_request.get("source") != "title-menu"
                or savedat_request.get("scene") != "title"
                or ".dat" not in str(savedat_request.get("accept") or "")
                or ".zip" not in str(savedat_request.get("accept") or "")
                or savedat_request.get("originalSavedataParserAvailable") is not True
                or savedat_request.get("originalRoutePromotionImplemented") is not False
                or savedat_clicked.get("clicked") is not True
                or savedat_clicked.get("scene") != "title"
                or "loadSavedat" not in (savedat_clicked.get("keys") or [])
            ):
                raise WebDriverError(f"title savedat file request did not use the hidden input: {title_savedat_request!r}")
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            wait_for_title(port, session_id)
            execute_js(port, session_id, clear_runtime_save_script())
            title_savedat_scan_click = execute_js(port, session_id, click_title_scan_savedat_script(), timeout=3)
            if not isinstance(title_savedat_scan_click, dict) or title_savedat_scan_click.get("ok") is not True:
                raise WebDriverError(f"title savedat scan menu row was not clickable: {title_savedat_scan_click!r}")
            title_savedat_scan = wait_for_title_scan_no_saves(port, session_id)
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            wait_for_title(port, session_id)
            execute_js(port, session_id, clear_runtime_save_script())
            title_sample_savedat_click = execute_js(port, session_id, click_title_sample_savedat_script(), timeout=3)
            if not isinstance(title_sample_savedat_click, dict) or title_sample_savedat_click.get("ok") is not True:
                raise WebDriverError(f"title sample savedat menu row was not clickable: {title_sample_savedat_click!r}")
            title_sample_savedat_map = wait_for_title_sample_savedat_map(port, session_id)
            title_sample_savedat_quick_save = execute_js(port, session_id, quick_save_started_map_script())
            sample_quick_summary = (title_sample_savedat_quick_save or {}).get("loadedSaveSummary") or {}
            if (
                not isinstance(title_sample_savedat_quick_save, dict)
                or title_sample_savedat_quick_save.get("saved") is not True
                or title_sample_savedat_quick_save.get("payloadMap") != "map2_02d"
                or (title_sample_savedat_quick_save.get("tile") or {}).get("x") != 5
                or (title_sample_savedat_quick_save.get("tile") or {}).get("y") != 14
                or title_sample_savedat_quick_save.get("hasRuntimeState") is not True
                or title_sample_savedat_quick_save.get("runtimeMoney") != 983062
                or "herb:3" not in (title_sample_savedat_quick_save.get("runtimeItems") or [])
                or not {"Ataho", "Rinshan", "Smashu"}.issubset(set(title_sample_savedat_quick_save.get("runtimeCharacters") or []))
                or title_sample_savedat_quick_save.get("hasLoadedSaveSummary") is not True
                or sample_quick_summary.get("selector") != "1:0"
                or sample_quick_summary.get("group") != 1
                or sample_quick_summary.get("slot") != 0
                or sample_quick_summary.get("x") != 5
                or sample_quick_summary.get("y") != 14
                or "map2_02d" not in (sample_quick_summary.get("fieldMaps") or [])
                or not public_sample_route_evidence_ready(sample_quick_summary.get("routeEvidence") or {})
                or not public_sample_route_hud_ready(title_sample_savedat_quick_save.get("saveHudLines") or [])
                or not any("저장 있음" in str(line) for line in (title_sample_savedat_quick_save.get("playHudLines") or []))
            ):
                raise WebDriverError(f"title sample savedat quick save did not persist savedat state: {title_sample_savedat_quick_save!r}")
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            continue_sample_savedat_title = wait_for_continue_ready_title(port, session_id)
            if not any("이어하기 map2_02d 5,14 세이브 1:0" in str(label) for label in (continue_sample_savedat_title.get("titleMenuLabels") or [])):
                raise WebDriverError(f"title sample savedat continue row did not summarize savedat state: {continue_sample_savedat_title!r}")
            continue_sample_savedat_click = execute_js(port, session_id, click_title_continue_script())
            if not isinstance(continue_sample_savedat_click, dict) or continue_sample_savedat_click.get("ok") is not True:
                raise WebDriverError(f"title sample savedat continue button was not usable: {continue_sample_savedat_click!r}")
            continued_sample_savedat_map = wait_for_continued_sample_savedat_map(port, session_id)
            direct_url = urljoin(base.rstrip("/") + "/", "/web/game.html?map=map1_02b&startTile=11%2C12")
            request_json(port, "POST", f"/session/{session_id}/url", {"url": direct_url}, timeout=30)
            direct_url_map = wait_for_direct_url_map(port, session_id)
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            wait_for_title(port, session_id)
            execute_js(port, session_id, clear_runtime_save_script())
            confirmed_start_click = execute_js(port, session_id, click_title_confirmed_start_script(), timeout=3)
            if not isinstance(confirmed_start_click, dict) or confirmed_start_click.get("ok") is not True:
                raise WebDriverError(f"title confirmed route menu row was not clickable: {confirmed_start_click!r}")
            started_map = wait_for_started_map(port, session_id)
            submenu_cancel = execute_js(port, session_id, runtime_submenu_cancel_script(), timeout=3)
            if (
                not isinstance(submenu_cancel, dict)
                or (submenu_cancel.get("before") or {}).get("opened") is not True
                or (submenu_cancel.get("before") or {}).get("menuOpen") is not True
                or (submenu_cancel.get("before") or {}).get("menuMode") != "shop-buy"
                or not any("구매 약초" in str(label) for label in ((submenu_cancel.get("before") or {}).get("labels") or []))
                or (submenu_cancel.get("afterBack") or {}).get("menuOpen") is not True
                or (submenu_cancel.get("afterBack") or {}).get("menuMode") != "main"
                or not any("상점 구매" in str(label) for label in ((submenu_cancel.get("afterBack") or {}).get("labels") or []))
                or (submenu_cancel.get("afterClose") or {}).get("menuOpen") is not False
                or (submenu_cancel.get("afterClose") or {}).get("menuMode") != "main"
                or (submenu_cancel.get("mobileBefore") or {}).get("opened") is not True
                or (submenu_cancel.get("mobileBefore") or {}).get("hidden") is not False
                or (submenu_cancel.get("mobileBefore") or {}).get("menuOpen") is not True
                or (submenu_cancel.get("mobileBefore") or {}).get("menuMode") != "shop-buy"
                or not any("구매 약초" in str(label) for label in ((submenu_cancel.get("mobileBefore") or {}).get("labels") or []))
                or (submenu_cancel.get("afterMobileBack") or {}).get("menuOpen") is not True
                or (submenu_cancel.get("afterMobileBack") or {}).get("menuMode") != "main"
                or not any("상점 구매" in str(label) for label in ((submenu_cancel.get("afterMobileBack") or {}).get("labels") or []))
                or (submenu_cancel.get("afterMobileClose") or {}).get("menuOpen") is not False
                or (submenu_cancel.get("afterMobileClose") or {}).get("menuMode") != "main"
                or (submenu_cancel.get("cancelBefore") or {}).get("opened") is not True
                or (submenu_cancel.get("cancelBefore") or {}).get("hidden") is not False
                or (submenu_cancel.get("cancelBefore") or {}).get("text") != "B"
                or (submenu_cancel.get("cancelBefore") or {}).get("menuOpen") is not True
                or (submenu_cancel.get("cancelBefore") or {}).get("menuMode") != "shop-buy"
                or not any("구매 약초" in str(label) for label in ((submenu_cancel.get("cancelBefore") or {}).get("labels") or []))
                or (submenu_cancel.get("afterCancelBack") or {}).get("menuOpen") is not True
                or (submenu_cancel.get("afterCancelBack") or {}).get("menuMode") != "main"
                or not any("상점 구매" in str(label) for label in ((submenu_cancel.get("afterCancelBack") or {}).get("labels") or []))
                or (submenu_cancel.get("afterCancelClose") or {}).get("menuOpen") is not False
                or (submenu_cancel.get("afterCancelClose") or {}).get("menuMode") != "main"
            ):
                raise WebDriverError(f"runtime submenu Escape should step back before closing: {submenu_cancel!r}")
            return_title = execute_js(port, session_id, return_to_title_menu_command_script())
            if (
                not isinstance(return_title, dict)
                or return_title.get("ok") is not True
                or return_title.get("scene") != "title"
                or return_title.get("search") != ""
                or return_title.get("menuOpen") is not False
                or return_title.get("titleMenuKeys") != TITLE_START_KEYS
                or return_title.get("trialTransitions") != ""
                or return_title.get("selectedRouteGoal") != ""
                or (return_title.get("menuWindow") or {}).get("start", 0) <= 0
                or (return_title.get("menuWindow") or {}).get("end", 0) <= return_title.get("commandIndex", 0)
                or return_title.get("pointerHitIndex") != return_title.get("commandIndex")
                or "제목" not in (return_title.get("visibleLabels") or [])
            ):
                raise WebDriverError(f"runtime menu did not return to the title screen: {return_title!r}")
            confirmed_start_click = execute_js(port, session_id, click_title_confirmed_start_script(), timeout=3)
            if not isinstance(confirmed_start_click, dict) or confirmed_start_click.get("ok") is not True:
                raise WebDriverError(f"title confirmed route menu row was not clickable: {confirmed_start_click!r}")
            started_map = wait_for_started_map(port, session_id)
            save_state = execute_js(port, session_id, quick_save_started_map_script())
            quick_save_feedback = next(
                (
                    entry for entry in runtime_save_feedback_entries(save_state if isinstance(save_state, dict) else {})
                    if entry.get("source") == "title-start-smoke-quick-save-feedback"
                    and entry.get("text") == "임시 저장 map1_02b 11,12"
                ),
                {},
            )
            quick_save_feedback_render = next(
                (
                    entry for entry in runtime_save_feedback_entries(save_state if isinstance(save_state, dict) else {}, rendered=True)
                    if entry.get("source") == "title-start-smoke-quick-save-feedback"
                    and entry.get("text") == "임시 저장 map1_02b 11,12"
                ),
                {},
            )
            if (
                not isinstance(save_state, dict)
                or save_state.get("saved") is not True
                or save_state.get("payloadMap") != "map1_02b"
                or (save_state.get("tile") or {}).get("x") != 11
                or (save_state.get("tile") or {}).get("y") != 12
                or save_state.get("hasRuntimeState") is not True
                or save_state.get("runtimeMoney") != 60
                or "Ataho" not in (save_state.get("runtimeCharacters") or [])
                or "herb:1" not in (save_state.get("runtimeItems") or [])
                or save_state.get("hasLoadedSaveSummary") is not False
                or (save_state.get("routeState") or {}).get("trialTransitions") != ""
                or quick_save_feedback.get("type") != "save"
                or quick_save_feedback.get("saved") is not True
                or quick_save_feedback.get("durationMs") != 1000
                or quick_save_feedback.get("browserRuntimeSaveFeedbackImplemented") is not True
                or quick_save_feedback.get("originalSavePointRuntimeImplemented") is not False
                or quick_save_feedback.get("originalStoryFlagRuntimeImplemented") is not False
                or not runtime_save_feedback_has_menu_confirm_sound(quick_save_feedback)
                or quick_save_feedback_render.get("active") is not True
                or quick_save_feedback_render.get("browserRuntimeSaveFeedbackImplemented") is not True
                or not runtime_save_feedback_has_menu_confirm_sound(quick_save_feedback_render)
                or not any("저장 있음" in str(line) for line in (save_state.get("playHudLines") or []))
            ):
                raise WebDriverError(f"title start quick save did not persist start state: {save_state!r}")
            execute_js(port, session_id, new_game_item_use_script(), timeout=3)
            item_use_state = wait_for_new_game_item_use(port, session_id)
            item_use_payload = item_use_state.get("payloadBeforeMutation") or {}
            item_use_payload_character = ((item_use_payload.get("runtimeState") or {}).get("characters") or [{}])[0]
            item_use_payload_herb = next(
                (
                    row for row in ((item_use_payload.get("runtimeState") or {}).get("items") or [])
                    if row.get("key") == "herb"
                ),
                {},
            )
            if (
                item_use_state.get("hasLoadedSaveSummary") is not False
                or item_use_state.get("scene") != "map"
                or item_use_state.get("mapName") != "map1_02b"
                or (item_use_state.get("progress") or {}).get("total") != 1
                or item_use_payload.get("map") != "map1_02b"
                or item_use_payload_character.get("hp") != 36
                or item_use_payload_herb.get("count") != 0
                or ((item_use_state.get("marker") or {}).get("source")) != "prototype-item-effect"
                or ((item_use_state.get("marker") or {}).get("originalStoryFlagRuntimeImplemented")) is not False
            ):
                raise WebDriverError(f"new-game item use marker/payload was incomplete: {item_use_state!r}")
            map_checksum = execute_js(port, session_id, canvas_checksum_script())
            if not isinstance(map_checksum, int) or map_checksum == 0 or map_checksum == title_checksum:
                raise WebDriverError(
                    f"title start did not change canvas: title={title_checksum!r} map={map_checksum!r}"
                )
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            continue_title = wait_for_continue_ready_title(port, session_id)
            if not any("이어하기 map1_02b 11,12" in str(label) for label in (continue_title.get("titleMenuLabels") or [])):
                raise WebDriverError(f"title continue row did not summarize the quick save: {continue_title!r}")
            continue_click = execute_js(port, session_id, click_title_continue_script())
            if not isinstance(continue_click, dict) or continue_click.get("ok") is not True:
                raise WebDriverError(f"title continue button was not usable: {continue_click!r}")
            continued_map = wait_for_continued_map(port, session_id)
            continued_progress_counts = ((continued_map.get("prototypeProgress") or {}).get("counts") or {})
            quick_load_feedback = next(
                (
                    entry for entry in runtime_save_feedback_entries(continued_map)
                    if entry.get("source") == "title-continue-feedback"
                    and entry.get("text") == "임시 불러오기 map1_02b 11,12"
                ),
                {},
            )
            quick_load_feedback_render = next(
                (
                    entry for entry in runtime_save_feedback_entries(continued_map, rendered=True)
                    if entry.get("source") == "title-continue-feedback"
                    and entry.get("text") == "임시 불러오기 map1_02b 11,12"
                ),
                {},
            )
            if (
                continued_map.get("atahoHp") != 36
                or continued_map.get("herbCount") != 0
                or continued_progress_counts.get("item-use-prototype") != 1
                or continued_map.get("loadedSaveSummary") is not None
                or quick_load_feedback.get("type") != "load"
                or quick_load_feedback.get("loaded") is not True
                or quick_load_feedback.get("durationMs") != 1000
                or quick_load_feedback.get("browserRuntimeSaveFeedbackImplemented") is not True
                or quick_load_feedback.get("originalSavePointRuntimeImplemented") is not False
                or quick_load_feedback.get("originalStoryFlagRuntimeImplemented") is not False
                or not runtime_save_feedback_has_menu_confirm_sound(quick_load_feedback)
                or quick_load_feedback_render.get("active") is not True
                or quick_load_feedback_render.get("browserRuntimeSaveFeedbackImplemented") is not True
                or not runtime_save_feedback_has_menu_confirm_sound(quick_load_feedback_render)
            ):
                raise WebDriverError(f"title continue did not restore new-game item state: {continued_map!r}")
            item_use_title_objective = execute_js(port, session_id, item_use_objective_capture_script(), timeout=3)
            if not isinstance(item_use_title_objective, dict):
                raise WebDriverError(f"title item-use objective capture did not return state: {item_use_title_objective!r}")
            verify_item_use_completion_objective(item_use_title_objective)
            execute_js(port, session_id, field_item_target_selection_script(), timeout=3)
            field_item_target = wait_for_field_item_target_selection(port, session_id)
            field_item_target_objective = execute_js(port, session_id, item_use_objective_capture_script(), timeout=3)
            if not isinstance(field_item_target_objective, dict):
                raise WebDriverError(
                    f"field item target objective capture did not return state: {field_item_target_objective!r}"
                )
            verify_item_use_completion_objective(
                field_item_target_objective,
                target_name="Rinshan",
                hp_before=8,
                hp_after=38,
                count_before=2,
                count_after=1,
            )
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            wait_for_title(port, session_id)
            execute_js(port, session_id, clear_runtime_save_script())
            encounter_click = execute_js(port, session_id, click_title_encounter_start_script(), timeout=3)
            if not isinstance(encounter_click, dict) or encounter_click.get("ok") is not True:
                raise WebDriverError(f"title encounter start menu row was not clickable: {encounter_click!r}")
            encounter_start = wait_for_encounter_started_map(port, session_id)
            execute_js(port, session_id, field_encounter_step_autosave_script(), timeout=3)
            encounter_step_auto_save = wait_for_field_encounter_step_autosave(port, session_id)
            encounter_auto_save = encounter_step_auto_save.get("autoSave") or {}
            encounter_step_tile = (encounter_step_auto_save.get("savedPayload") or {}).get("tile") or {}
            encounter_quick_save = {
                **encounter_auto_save,
                "movementInput": encounter_step_auto_save.get("movementInput"),
                "movement": encounter_step_auto_save.get("movement") or {},
                "step": encounter_step_auto_save.get("step") or {},
                "savedPayload": encounter_step_auto_save.get("savedPayload") or {},
                "playHudLines": encounter_step_auto_save.get("playHudLines") or [],
                "fieldEncounterMenuLabel": encounter_step_auto_save.get("fieldEncounterMenuLabel") or "",
                "encounterFeedbackLog": encounter_step_auto_save.get("encounterFeedbackLog") or [],
                "encounterFeedbackRender": encounter_step_auto_save.get("encounterFeedbackRender") or [],
                "encounterFeedbackLast": encounter_step_auto_save.get("encounterFeedbackLast") or {},
                "encounterFeedbackLastRender": encounter_step_auto_save.get("encounterFeedbackLastRender") or {},
            }
            encounter_payload = encounter_auto_save.get("fieldEncounter") or {}
            if (
                encounter_auto_save.get("saved") is not True
                or encounter_auto_save.get("source") != "field-encounter-step"
                or encounter_auto_save.get("payloadMap") != "map1_02b"
                or encounter_payload.get("enabled") is not True
                or encounter_payload.get("stepCount") != 1
                or encounter_payload.get("source") != "prototype-field-encounter"
                or "전투 1/6" not in " ".join(str(line) for line in (encounter_step_auto_save.get("playHudLines") or []))
            ):
                raise WebDriverError(f"title encounter step auto-save did not persist field encounters: {encounter_step_auto_save!r}")
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            continue_encounter_title = wait_for_continue_ready_title(port, session_id)
            encounter_continue_label = f"이어하기 map1_02b {encounter_step_tile.get('x')},{encounter_step_tile.get('y')} 전투"
            if not any(encounter_continue_label in str(label) for label in (continue_encounter_title.get("titleMenuLabels") or [])):
                raise WebDriverError(f"title encounter continue row did not summarize field encounters: {continue_encounter_title!r}")
            continue_encounter_click = execute_js(port, session_id, click_title_continue_script())
            if not isinstance(continue_encounter_click, dict) or continue_encounter_click.get("ok") is not True:
                raise WebDriverError(f"title encounter continue button was not usable: {continue_encounter_click!r}")
            continued_encounter_map = wait_for_continued_encounter_map(port, session_id, encounter_step_tile)
            execute_js(port, session_id, field_encounter_disable_autosave_script(), timeout=3)
            encounter_disable = wait_for_field_encounter_disable_autosave(port, session_id)
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            continue_disabled_encounter_title = wait_for_continue_ready_title(port, session_id)
            disabled_labels = [str(label) for label in (continue_disabled_encounter_title.get("titleMenuLabels") or [])]
            encounter_disabled_label = f"이어하기 map1_02b {encounter_step_tile.get('x')},{encounter_step_tile.get('y')}"
            if (
                not any(encounter_disabled_label in label for label in disabled_labels)
                or any(f"{encounter_disabled_label} 전투" in label for label in disabled_labels)
            ):
                raise WebDriverError(
                    "title encounter disabled continue row did not summarize disabled field encounters: "
                    f"{continue_disabled_encounter_title!r}"
                )
            continue_disabled_encounter_click = execute_js(port, session_id, click_title_continue_script())
            if not isinstance(continue_disabled_encounter_click, dict) or continue_disabled_encounter_click.get("ok") is not True:
                raise WebDriverError(f"title encounter disabled continue button was not usable: {continue_disabled_encounter_click!r}")
            continued_disabled_encounter_map = wait_for_continued_disabled_encounter_map(port, session_id, encounter_step_tile)
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            wait_for_title(port, session_id)
            execute_js(port, session_id, clear_runtime_save_script())
            menu_click = execute_js(port, session_id, click_title_menu_route_assist_script())
            if not isinstance(menu_click, dict) or menu_click.get("ok") is not True:
                raise WebDriverError(f"title routeAssist menu row was not clickable: {menu_click!r}")
            route_assist_menu_map = wait_for_route_assist_started_map(port, session_id)
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            wait_for_title(port, session_id)
            execute_js(port, session_id, clear_runtime_save_script())
            route_goal_click = execute_js(port, session_id, click_title_route_goal_script("map2_18d"), timeout=3)
            if not isinstance(route_goal_click, dict) or route_goal_click.get("ok") is not True:
                raise WebDriverError(f"title routeGoal menu row was not clickable: {route_goal_click!r}")
            route_goal_title_map = wait_for_route_assist_goal_started_map(port, session_id, "map2_18d")
            route_goal_quick_save = execute_js(port, session_id, quick_save_started_map_script())
            route_goal_saved_state = (route_goal_quick_save or {}).get("routeState") or {}
            if (
                not isinstance(route_goal_quick_save, dict)
                or route_goal_quick_save.get("saved") is not True
                or route_goal_quick_save.get("payloadMap") != "map1_02b"
                or route_goal_saved_state.get("trialTransitions") != "routeAssist"
                or route_goal_saved_state.get("selectedRouteGoal") != "map2_18d"
                or "후보 map2_18d" not in " ".join(str(line) for line in (route_goal_quick_save.get("playHudLines") or []))
            ):
                raise WebDriverError(f"title routeGoal quick save did not persist route state: {route_goal_quick_save!r}")
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            continue_route_goal_title = wait_for_continue_ready_title(port, session_id)
            if not any("이어하기 map1_02b 11,12 후보 map2_18d" in str(label) for label in (continue_route_goal_title.get("titleMenuLabels") or [])):
                raise WebDriverError(f"title routeGoal continue row did not summarize route state: {continue_route_goal_title!r}")
            continue_route_goal_click = execute_js(port, session_id, click_title_continue_script())
            if not isinstance(continue_route_goal_click, dict) or continue_route_goal_click.get("ok") is not True:
                raise WebDriverError(f"title routeGoal continue button was not usable: {continue_route_goal_click!r}")
            continued_route_goal_map = wait_for_continued_route_goal_map(port, session_id, "map2_18d")
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            wait_for_title(port, session_id)
            execute_js(port, session_id, clear_runtime_save_script())
            deep_route_goal_click = execute_js(port, session_id, click_title_route_goal_script("map5_40f"), timeout=3)
            if not isinstance(deep_route_goal_click, dict) or deep_route_goal_click.get("ok") is not True:
                raise WebDriverError(f"title deep routeGoal menu row was not clickable: {deep_route_goal_click!r}")
            deep_route_goal_title_map = wait_for_route_assist_goal_started_map(port, session_id, "map5_40f")
            deep_route_goal_quick_save = execute_js(port, session_id, quick_save_started_map_script())
            deep_route_goal_saved_state = (deep_route_goal_quick_save or {}).get("routeState") or {}
            if (
                not isinstance(deep_route_goal_quick_save, dict)
                or deep_route_goal_quick_save.get("saved") is not True
                or deep_route_goal_quick_save.get("payloadMap") != "map1_02b"
                or deep_route_goal_saved_state.get("trialTransitions") != "routeAssist"
                or deep_route_goal_saved_state.get("selectedRouteGoal") != "map5_40f"
                or "후보 map5_40f" not in " ".join(str(line) for line in (deep_route_goal_quick_save.get("playHudLines") or []))
            ):
                raise WebDriverError(f"title deep routeGoal quick save did not persist route state: {deep_route_goal_quick_save!r}")
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            continue_deep_route_goal_title = wait_for_continue_ready_title(port, session_id)
            if not any("이어하기 map1_02b 11,12 후보 map5_40f" in str(label) for label in (continue_deep_route_goal_title.get("titleMenuLabels") or [])):
                raise WebDriverError(f"title deep routeGoal continue row did not summarize route state: {continue_deep_route_goal_title!r}")
            continue_deep_route_goal_click = execute_js(port, session_id, click_title_continue_script())
            if not isinstance(continue_deep_route_goal_click, dict) or continue_deep_route_goal_click.get("ok") is not True:
                raise WebDriverError(f"title deep routeGoal continue button was not usable: {continue_deep_route_goal_click!r}")
            continued_deep_route_goal_map = wait_for_continued_route_goal_map(port, session_id, "map5_40f")
            request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
            wait_for_title(port, session_id)
            execute_js(port, session_id, clear_runtime_save_script())
            if execute_js(port, session_id, click_title_route_assist_script()) is not True:
                raise WebDriverError("title routeAssist button was not clickable")
            route_assist_map = wait_for_route_assist_started_map(port, session_id)
            payload = {
                "status": "ok",
                "url": url,
                "title": title,
                "queryPlay": query_play_map,
                "playStartClick": play_start_click,
                "playStart": play_start_map,
                "continuePlayStartTitle": continue_play_start_title,
                "continuePlayStartClick": continue_play_start_click,
                "continuePlayStart": continued_play_start_map,
                "playStartRouteNextClick": play_start_route_next_click,
                "playStartRouteNext": play_start_route_next_map,
                "playStartRouteFocusClick": play_start_route_focus_click,
                "playStartRouteFocus": play_start_route_focus_map,
                "playStartRouteTargetClick": play_start_route_target_click,
                "playStartRouteTarget": play_start_route_target_map,
                "continuePlayStartRouteTargetTitle": continue_play_start_route_target_title,
                "continuePlayStartRouteTargetClick": continue_play_start_route_target_click,
                "continuePlayStartRouteTarget": continued_play_start_route_target_map,
                "playStartTargetEncounter": play_start_target_encounter,
                "continuePlayStartTargetEncounterTitle": continue_play_start_target_encounter_title,
                "continuePlayStartTargetEncounterClick": continue_play_start_target_encounter_click,
                "continuePlayStartTargetEncounter": continued_play_start_target_encounter_map,
                "playStartInputClick": play_start_input_click,
                "playStartInputStart": play_start_input_start,
                "playStartConfirmedInput": play_start_confirmed_input,
                "playStartInputFocusClick": play_start_input_focus_click,
                "playStartInputFocus": play_start_input_focus,
                "playStartCandidateInput": play_start_candidate_input,
                "continuePlayStartInputTitle": continue_play_start_input_title,
                "continuePlayStartInputClick": continue_play_start_input_click,
                "continuePlayStartInput": continued_play_start_input,
                "titleSavedatRequest": title_savedat_request,
                "titleSavedatScanClick": title_savedat_scan_click,
                "titleSavedatScan": title_savedat_scan,
                "titleSampleSavedatClick": title_sample_savedat_click,
                "titleSampleSavedat": title_sample_savedat_map,
                "titleSampleSavedatQuickSave": title_sample_savedat_quick_save,
                "continueSampleSavedatTitle": continue_sample_savedat_title,
                "continueSampleSavedatClick": continue_sample_savedat_click,
                "continueSampleSavedat": continued_sample_savedat_map,
                "directUrl": direct_url_map,
                "map": started_map,
                "submenuCancel": submenu_cancel,
                "returnTitle": return_title,
                "quickSave": save_state,
                "itemUse": item_use_state,
                "itemUseTitleObjective": item_use_title_objective,
                "fieldItemTarget": field_item_target,
                "fieldItemTargetObjective": field_item_target_objective,
                "continueTitle": continue_title,
                "continue": continued_map,
                "encounterStartClick": encounter_click,
                "encounterStart": encounter_start,
                "encounterStepAutoSave": encounter_step_auto_save,
                "encounterQuickSave": encounter_quick_save,
                "continueEncounterTitle": continue_encounter_title,
                "continueEncounterClick": continue_encounter_click,
                "continueEncounter": continued_encounter_map,
                "encounterDisable": encounter_disable,
                "continueDisabledEncounterTitle": continue_disabled_encounter_title,
                "continueDisabledEncounterClick": continue_disabled_encounter_click,
                "continueDisabledEncounter": continued_disabled_encounter_map,
                "routeAssist": route_assist_map,
                "routeAssistMenu": route_assist_menu_map,
                "routeGoalClick": route_goal_click,
                "routeGoalTitle": route_goal_title_map,
                "routeGoalQuickSave": route_goal_quick_save,
                "continueRouteGoalTitle": continue_route_goal_title,
                "continueRouteGoalClick": continue_route_goal_click,
                "continueRouteGoal": continued_route_goal_map,
                "deepRouteGoalClick": deep_route_goal_click,
                "deepRouteGoalTitle": deep_route_goal_title_map,
                "deepRouteGoalQuickSave": deep_route_goal_quick_save,
                "continueDeepRouteGoalTitle": continue_deep_route_goal_title,
                "continueDeepRouteGoalClick": continue_deep_route_goal_click,
                "continueDeepRouteGoal": continued_deep_route_goal_map,
                "titleChecksum": title_checksum,
                "mapChecksum": map_checksum,
            }
            write_report(payload)
            foot = started_map.get("foot") or {}
            assist_foot = route_assist_map.get("foot") or {}
            play_start_foot = play_start_map.get("foot") or {}
            query_play_foot = query_play_map.get("foot") or {}
            continued_play_start_foot = continued_play_start_map.get("foot") or {}
            play_start_route_next_foot = play_start_route_next_map.get("foot") or {}
            play_start_route_focus_foot = play_start_route_focus_map.get("foot") or {}
            play_start_route_target_foot = play_start_route_target_map.get("foot") or {}
            continued_play_start_route_target_foot = continued_play_start_route_target_map.get("foot") or {}
            play_start_target_encounter_foot = play_start_target_encounter.get("foot") or {}
            play_start_target_encounter_movement = play_start_target_encounter.get("movement") or {}
            continued_play_start_target_encounter_foot = continued_play_start_target_encounter_map.get("foot") or {}
            play_start_confirmed_input_after = (play_start_confirmed_input.get("after") or {}).get("foot") or {}
            play_start_candidate_input_after = (play_start_candidate_input.get("after") or {}).get("foot") or {}
            continued_play_start_input_foot = continued_play_start_input.get("foot") or {}
            assist_menu_foot = route_assist_menu_map.get("foot") or {}
            route_goal_foot = route_goal_title_map.get("foot") or {}
            continued_route_goal_foot = continued_route_goal_map.get("foot") or {}
            deep_route_goal_foot = deep_route_goal_title_map.get("foot") or {}
            continued_deep_route_goal_foot = continued_deep_route_goal_map.get("foot") or {}
            sample_foot = title_sample_savedat_map.get("foot") or {}
            continued_sample_foot = continued_sample_savedat_map.get("foot") or {}
            continued_encounter_foot = continued_encounter_map.get("foot") or {}
            continued_encounter_field = continued_encounter_map.get("fieldEncounter") or {}
            continued_disabled_encounter_foot = continued_disabled_encounter_map.get("foot") or {}
            continued_disabled_encounter_field = continued_disabled_encounter_map.get("fieldEncounter") or {}
            encounter_mode_auto = encounter_start.get("fieldEncounterModeAutoSave") or {}
            encounter_step_auto = encounter_step_auto_save.get("autoSave") or {}
            encounter_step_movement = encounter_step_auto_save.get("movement") or {}
            encounter_disable_auto = encounter_disable.get("autoSave") or {}
            savedat_scan_marker = title_savedat_scan.get("titleSavedatScan") or {}
            title_sample_savedat_route = (title_sample_savedat_map.get("loadedSaveSummary") or {}).get("routeEvidence") or {}
            print(
                "ok title start browser "
                f"title={title_checksum} map={map_checksum} "
                f"started={started_map.get('mapName')}@{foot.get('x')},{foot.get('y')} "
                f"queryPlay={query_play_map.get('selectedRouteGoal')}@{query_play_map.get('mapName')}:{query_play_foot.get('x')},{query_play_foot.get('y')} "
                f"playStart={play_start_map.get('selectedRouteGoal')}@{play_start_map.get('mapName')}:{play_start_foot.get('x')},{play_start_foot.get('y')} "
                f"continuePlayStart={continued_play_start_map.get('selectedRouteGoal')}@{continued_play_start_map.get('mapName')}:{continued_play_start_foot.get('x')},{continued_play_start_foot.get('y')} "
                f"playStartNext={play_start_route_next_map.get('selectedRouteGoal')}@{play_start_route_next_map.get('mapName')}:{play_start_route_next_foot.get('x')},{play_start_route_next_foot.get('y')} "
                f"playStartFocus={play_start_route_focus_map.get('selectedRouteGoal')}@{play_start_route_focus_map.get('mapName')}:{play_start_route_focus_foot.get('x')},{play_start_route_focus_foot.get('y')} "
                f"playStartTarget={play_start_route_target_map.get('selectedRouteGoal')}@{play_start_route_target_map.get('mapName')}:{play_start_route_target_foot.get('x')},{play_start_route_target_foot.get('y')} "
                f"continuePlayStartTarget={continued_play_start_route_target_map.get('selectedRouteGoal')}@{continued_play_start_route_target_map.get('mapName')}:{continued_play_start_route_target_foot.get('x')},{continued_play_start_route_target_foot.get('y')} "
                f"playStartTargetEncounter={play_start_target_encounter.get('routeState', {}).get('selectedRouteGoal')}@{play_start_target_encounter.get('mapName')}:{play_start_target_encounter_foot.get('x')},{play_start_target_encounter_foot.get('y')} "
                f"playStartTargetEncounterInput={play_start_target_encounter.get('inputCode')} "
                f"playStartTargetEncounterMoved={play_start_target_encounter_movement.get('beforeFoot')}->{play_start_target_encounter_movement.get('afterFoot')} "
                f"continuePlayStartTargetEncounter={continued_play_start_target_encounter_map.get('selectedRouteGoal')}@{continued_play_start_target_encounter_map.get('mapName')}:{continued_play_start_target_encounter_foot.get('x')},{continued_play_start_target_encounter_foot.get('y')} "
                f"playStartConfirmedInput={play_start_confirmed_input.get('inputCode')}@{(play_start_confirmed_input.get('after') or {}).get('mapName')}:{play_start_confirmed_input_after.get('x')},{play_start_confirmed_input_after.get('y')} "
                f"confirmedTransitionSound={map_transition_feedback_sound_summary(play_start_confirmed_input)} "
                f"playStartCandidateInput={play_start_candidate_input.get('inputCode')}@{(play_start_candidate_input.get('after') or {}).get('mapName')}:{play_start_candidate_input_after.get('x')},{play_start_candidate_input_after.get('y')} "
                f"candidateTransitionSound={map_transition_feedback_sound_summary(play_start_candidate_input)} "
                f"continuePlayStartInput={continued_play_start_input.get('selectedRouteGoal')}@{continued_play_start_input.get('mapName')}:{continued_play_start_input_foot.get('x')},{continued_play_start_input_foot.get('y')} "
                f"savedatRequest={savedat_request.get('source')} "
                f"savedatScan={savedat_scan_marker.get('foundCount')}/{savedat_scan_marker.get('loaded')} "
                f"sampleSavedat={title_sample_savedat_map.get('mapName')}@{sample_foot.get('x')},{sample_foot.get('y')} "
                f"sampleSavedatRouteMissing={title_sample_savedat_route.get('missingEvidenceCount')} "
                f"sampleSavedatRouteExternalInput={title_sample_savedat_route.get('externalProofInputId')} "
                f"continueSampleSavedat={continued_sample_savedat_map.get('mapName')}@{continued_sample_foot.get('x')},{continued_sample_foot.get('y')} "
                f"submenuCancel={(submenu_cancel.get('before') or {}).get('menuMode')}->{(submenu_cancel.get('afterBack') or {}).get('menuMode')}->{(submenu_cancel.get('afterClose') or {}).get('menuOpen')} "
                f"mobileMenuCancel={(submenu_cancel.get('mobileBefore') or {}).get('menuMode')}->{(submenu_cancel.get('afterMobileBack') or {}).get('menuMode')}->{(submenu_cancel.get('afterMobileClose') or {}).get('menuOpen')} "
                f"mobileBCancel={(submenu_cancel.get('cancelBefore') or {}).get('menuMode')}->{(submenu_cancel.get('afterCancelBack') or {}).get('menuMode')}->{(submenu_cancel.get('afterCancelClose') or {}).get('menuOpen')} "
                f"returnTitle={return_title.get('scene')} "
                f"continue={continued_map.get('mapName')}@{(continued_map.get('foot') or {}).get('x')},{(continued_map.get('foot') or {}).get('y')} "
                f"encounterStart={encounter_start.get('mapName')}@{(encounter_start.get('foot') or {}).get('x')},{(encounter_start.get('foot') or {}).get('y')} "
                f"encounterModeAutoSource={encounter_mode_auto.get('source')} "
                f"encounterStepAutoSource={encounter_step_auto.get('source')} "
                f"encounterStepInput={encounter_step_auto_save.get('inputCode')} "
                f"encounterStepMoved={encounter_step_movement.get('beforeFoot')}->{encounter_step_movement.get('afterFoot')} "
                f"continueEncounter={continued_encounter_map.get('mapName')}@{continued_encounter_foot.get('x')},{continued_encounter_foot.get('y')} "
                f"fieldEncounter={continued_encounter_field.get('enabled')}/{continued_encounter_field.get('stepCount')} "
                f"encounterDisableAutoSource={encounter_disable_auto.get('source')} "
                f"continueDisabledEncounter={continued_disabled_encounter_map.get('mapName')}@{continued_disabled_encounter_foot.get('x')},{continued_disabled_encounter_foot.get('y')} "
                f"disabledFieldEncounter={continued_disabled_encounter_field.get('enabled')}/{continued_disabled_encounter_field.get('stepCount')} "
                f"routeAssistMenu={route_assist_menu_map.get('mapName')}@{assist_menu_foot.get('x')},{assist_menu_foot.get('y')} "
                f"routeGoalTitle={route_goal_title_map.get('selectedRouteGoal')}@{route_goal_title_map.get('mapName')}:{route_goal_foot.get('x')},{route_goal_foot.get('y')} "
                f"continueRouteGoal={continued_route_goal_map.get('selectedRouteGoal')}@{continued_route_goal_map.get('mapName')}:{continued_route_goal_foot.get('x')},{continued_route_goal_foot.get('y')} "
                f"deepRouteGoalTitle={deep_route_goal_title_map.get('selectedRouteGoal')}@{deep_route_goal_title_map.get('mapName')}:{deep_route_goal_foot.get('x')},{deep_route_goal_foot.get('y')} "
                f"continueDeepRouteGoal={continued_deep_route_goal_map.get('selectedRouteGoal')}@{continued_deep_route_goal_map.get('mapName')}:{continued_deep_route_goal_foot.get('x')},{continued_deep_route_goal_foot.get('y')} "
                f"routeAssist={route_assist_map.get('mapName')}@{assist_foot.get('x')},{assist_foot.get('y')}"
            )
        finally:
            if session_id:
                try:
                    request_json(port, "DELETE", f"/session/{session_id}", timeout=3)
                except Exception:
                    pass
            proc.terminate()
            try:
                proc.wait(timeout=5)
            except subprocess.TimeoutExpired:
                proc.kill()
                proc.wait(timeout=5)
            if not keep_log:
                log_path.unlink(missing_ok=True)


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--base", default="http://127.0.0.1:8013")
    parser.add_argument("--keep-log", action="store_true")
    args = parser.parse_args()
    verify_browser(args.base, keep_log=args.keep_log)


if __name__ == "__main__":
    main()
