#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import shutil
import subprocess
import time
from pathlib import Path
from urllib.parse import urlencode, urljoin

from verify_mobile_browser_controls import (
    WebDriverError,
    canvas_checksum_script,
    execute_js,
    free_port,
    item_text_provenance_matches,
    request_json,
    wait_for_driver,
    wait_for_map_runtime,
    wait_for_page,
)

ROOT = Path(__file__).resolve().parents[1]

EXPECTED_LINKED_DIALOGUE_VM_REPLAYS = {
    "event-dialogue-block-004": {
        "lineCount": 14,
        "traceCommandCount": 16,
        "separatorEventCount": 9,
        "renderEventCount": 6,
        "literalTextEventCount": 9,
    },
    "event-dialogue-block-032": {
        "lineCount": 14,
        "traceCommandCount": 13,
        "separatorEventCount": 9,
        "renderEventCount": 3,
        "literalTextEventCount": 4,
    },
    "event-dialogue-block-041": {
        "lineCount": 13,
        "traceCommandCount": 14,
        "separatorEventCount": 8,
        "renderEventCount": 5,
        "literalTextEventCount": 8,
    },
}

EXPECTED_OBJECT_LINKED_DIALOGUE_BATTLE = {
    "map": "map2_03l",
    "candidateId": "event-dialogue-block-041:btl_l1",
    "blockId": "event-dialogue-block-041",
    "battleBackground": "btl_l1",
    "enemySpriteCandidate": "zjk_byk.cns",
    "enemySpriteAssetKey": "zjk_byk",
    "enemySpriteRefVaHex": "0x004bdb3c",
    "battleBackgroundRefVaHex": "0x004bd850",
    "enemyName": "백호 괴수",
    "enemyHp": 50,
    "enemyAtk": 5,
    "enemyDef": 1,
    "enemyActionName": "돌진",
    "enemyActionIndex": 8,
    "enemyRewardExp": 9,
    "enemyDropKey": "item_2",
    "enemyDropName": "해독초",
    "enemyDropCount": 1,
}

EXPECTED_SCENE_LINKED_Z_OBJECTS = [
    {
        "map": "map1_02b",
        "asset": "zjk_suz",
        "linked": "zjk_suz.cns",
        "sceneIdHex": "0x0618",
        "eventKind": 93,
        "anchor": {"x": 11, "y": 11},
    },
    {
        "map": "map2_03l",
        "assets": ["zm_2", "zg_kni", "zs_rg"],
        "linked": ["zm_2.cns", "zg_kni.cns", "zs_rg.cns"],
        "sceneIdHex": "0x0918",
        "eventKind": 93,
        "anchor": {"x": 11, "y": 11},
    },
    {
        "map": "map8_18o",
        "asset": "zs_dd",
        "linked": "zs_dd.cns",
        "sceneIdHex": "0x9e18",
        "eventKind": 94,
        "anchor": {"x": 11, "y": 11},
    },
    {
        "map": "map8_32q",
        "asset": "zsa_iwa",
        "linked": "zsa_iwa.cns",
        "sceneIdHex": "0xac18",
        "eventKind": 94,
        "anchor": {"x": 11, "y": 11},
    },
    {
        "map": "map9_01e",
        "asset": "zi_ana",
        "linked": "zi_ana.cns",
        "sceneIdHex": "0xad18",
        "eventKind": 94,
        "anchor": {"x": 11, "y": 11},
    },
]


def load_map(base: str, port: int, session_id: str, params: dict[str, str]) -> str:
    query = urlencode({**params, "_": str(time.time_ns())})
    url = urljoin(base.rstrip("/") + "/", f"/web/game.html?{query}")
    request_json(port, "POST", f"/session/{session_id}/url", {"url": url}, timeout=30)
    wait_for_page(port, session_id)
    wait_for_map_runtime(port, session_id, params["map"])
    return url


def prepare_object_button_script() -> str:
    return """
window.__hwanseCandidateObjectButton = null;
ensureSceneEvents().then(() => {
  updateObjectButton();
  const button = document.getElementById('objectButton');
  window.__hwanseCandidateObjectButton = {
    hidden: button?.hidden ?? null,
    text: button?.textContent || '',
    title: button?.title || '',
    map: map?.name || '',
    candidates: eventObjectCandidatesForMap().map((candidate) => ({
      key: candidate.key,
      linked: candidate.linked || '',
      anchor: candidate.anchor,
      sceneIdHex: candidate.record?.sceneIdHex || '',
    })),
  };
}).catch((error) => {
  window.__hwanseCandidateObjectButton = { error: String(error && error.message || error) };
});
return true;
"""


def object_button_state_script() -> str:
    return "return window.__hwanseCandidateObjectButton || null;"


def object_candidate_menu_selection_script() -> str:
    return """
window.__hwanseObjectCandidateMenuSelection = null;
localStorage.removeItem(RUNTIME_SAVE_KEY);
prototypeProgress = createPrototypeProgressState();
activeDialogue = null;
menuOpen = false;
menuMode = 'main';
selectedMenuItemIndex = 0;
window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_LOG = [];
window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER = [];
window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK = null;
window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER = null;
if (typeof activeEventObjectInspectFeedbacks !== 'undefined') activeEventObjectInspectFeedbacks = [];
Promise.all([ensureSceneEvents(), ensureEventDialogueBlocks()]).then(() =>
  Promise.all(eventObjectCandidatesForMap().map((candidate) => ensureImage(candidate.key))).then(() => {
    updateObjectButton();
    render();
    const opened = openObjectCandidateMenu();
    const items = typeof objectCandidateMenuItems === 'function' ? objectCandidateMenuItems() : [];
    const targetIndex = items.findIndex((item) =>
      item.command === 'selectObjectCandidate' && item.objectCandidate?.key === 'zs_rg'
    );
    const before = {
      opened,
      menuMode,
      selectedMenuItemIndex,
      count: items.length,
      labels: items.map((item) => item.name),
      targetIndex,
      targetKey: items[targetIndex]?.objectCandidate?.key || '',
      targetName: items[targetIndex]?.name || '',
      menuMarker: window.HWANSE_LAST_OBJECT_CANDIDATE_MENU || null,
    };
    if (!opened || targetIndex < 0) {
      window.__hwanseObjectCandidateMenuSelection = { ok: false, before };
      return;
    }
    selectedMenuItemIndex = targetIndex;
    const commandResult = useSelectedMenuItem();
    render();
    const payload = JSON.parse(localStorage.getItem(RUNTIME_SAVE_KEY) || '{}');
    window.__hwanseObjectCandidateMenuSelection = {
      ok: commandResult === true && activeDialogue?.block?.blockId === 'event-object-candidates:map2_03l',
      before,
      commandResult,
      afterMenuMode: menuMode,
      afterMenuOpen: menuOpen,
      selection: window.HWANSE_LAST_OBJECT_CANDIDATE_MENU_SELECTION || null,
      inspect: window.HWANSE_LAST_EVENT_OBJECT_INSPECT || null,
      inspectFeedbackLog: window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_LOG || [],
      inspectFeedbackRender: window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER || [],
      inspectFeedbackLast: window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK || null,
      inspectFeedbackLastRender: window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER || null,
      activeId: activeDialogue?.block?.blockId || '',
      activeLines: activeDialogue?.lines?.slice(0, 16) || [],
      progress: prototypeProgress || null,
      savedPayload: payload || null,
      render: window.HWANSE_LAST_EVENT_OBJECT_RENDER || null,
      scene,
      map: map?.name || '',
      showEventObjectPrototypes,
    };
  })
).catch((error) => {
  window.__hwanseObjectCandidateMenuSelection = { ok: false, error: String(error && error.message || error) };
});
return true;
"""


def object_candidate_menu_state_script() -> str:
    return "return window.__hwanseObjectCandidateMenuSelection || null;"


def reset_runtime_progress_script() -> str:
    return """
localStorage.removeItem(RUNTIME_SAVE_KEY);
prototypeProgress = createPrototypeProgressState();
activeDialogue = null;
menuOpen = false;
menuMode = 'main';
selectedMenuItemIndex = 0;
pendingInventoryItem = null;
menuNotice = '';
window.HWANSE_LAST_EVENT_OBJECT_INSPECT = null;
window.HWANSE_LAST_EVENT_OBJECT_INSPECT_AUTO_SAVE = null;
window.HWANSE_LAST_OBJECT_CANDIDATE_MENU = null;
window.HWANSE_LAST_OBJECT_CANDIDATE_MENU_SELECTION = null;
if (typeof activeEventObjectInspectFeedbacks !== 'undefined') activeEventObjectInspectFeedbacks = [];
updateObjectButton();
render();
return true;
"""


def passive_object_render_state_script() -> str:
    return """
const objectButton = document.getElementById('objectButton');
return {
  scene,
  map: map?.name || '',
  sceneEventsLoaded,
  showEventHotspots,
  showEventObjectPrototypes,
  render: window.HWANSE_LAST_EVENT_OBJECT_RENDER || null,
  objectButtonText: objectButton?.textContent || '',
  objectButtonTitle: objectButton?.title || '',
  objectButtonHidden: objectButton?.hidden ?? null,
};
"""


def click_object_button_script() -> str:
    return """
const button = document.getElementById('objectButton');
window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_LOG = [];
window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER = [];
window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK = null;
window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER = null;
if (typeof activeEventObjectInspectFeedbacks !== 'undefined') activeEventObjectInspectFeedbacks = [];
const before = {
  hidden: button?.hidden ?? null,
  text: button?.textContent || '',
  title: button?.title || '',
};
button?.click();
return { ok: !!button, before };
"""


def action_object_script() -> str:
    return """
window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_LOG = [];
window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER = [];
window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK = null;
window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER = null;
if (typeof activeEventObjectInspectFeedbacks !== 'undefined') activeEventObjectInspectFeedbacks = [];
const before = {
  scene,
  map: map?.name || '',
  candidates: eventObjectCandidatesForMap().length,
  near: eventObjectCandidateSelection({ nearOnly: true }).candidates.length,
};
const activated = activateHotspotAtFoot();
return { before, activated };
"""


def linked_dialogue_battle_action_script() -> str:
    return """
window.__hwanseObjectLinkedDialogueBattleAction = null;
Promise.all([ensureSceneEvents(), ensureEventDialogueBlocks(), ensureBattleData()]).then(() => {
  activeDialogue = null;
  menuOpen = false;
  updateObjectButton();
  updateDialogueButton();
  publishPrototypeCompletionState(eventObjectCandidateSelection());
  render();
  drawActionPrompt();
  const actionPromptBefore = window.HWANSE_LAST_ACTION_PROMPT || null;
  const link = typeof dialogueBattleLinkState === 'function' ? dialogueBattleLinkState() : null;
  const linkBefore = link ? {
    pending: link.pending === true,
    id: link.id || '',
    blockId: link.blockId || '',
    candidateId: link.candidate?.id || '',
    battleBackground: link.battleBackground || '',
    progressEventKind: link.progressEvent?.kind || '',
    progressEventId: link.progressEvent?.id || '',
    prototypeDialogueBattleLinkImplemented: link.prototypeDialogueBattleLinkImplemented === true,
    originalEventDrivenBattleEntry: link.originalEventDrivenBattleEntry === true,
    originalEventVmRuntimeImplemented: link.originalEventVmRuntimeImplemented === true,
    originalStoryFlagRuntimeImplemented: link.originalStoryFlagRuntimeImplemented === true,
  } : null;
  const objectCompletion = eventObjectPrototypeCompletionState(eventObjectCandidateSelection());
  const dialogueCompletion = dialoguePrototypeCompletionState();
  const progressBefore = window.HWANSE_LAST_PROTOTYPE_PROGRESS || null;
  window.HWANSE_LAST_DIALOGUE_BATTLE_LINK_ACTION = null;
  window.HWANSE_BATTLE_START_FEEDBACK_LOG = [];
  window.HWANSE_BATTLE_START_FEEDBACK_RENDER = [];
  window.HWANSE_LAST_BATTLE_START_FEEDBACK = null;
  window.HWANSE_LAST_BATTLE_START_FEEDBACK_RENDER = null;
  if (typeof activeBattleStartFeedbacks !== 'undefined') activeBattleStartFeedbacks = [];
  const actionResult = activateHotspotAtFoot();
  window.__hwanseObjectLinkedDialogueBattleAction = {
    actionPromptBefore,
    linkBefore,
    objectCompletion,
    dialogueCompletion,
    progressBefore,
    actionResult,
    startedAtScene: scene,
    startedAtMap: map?.name || '',
  };
}).catch((error) => {
  window.__hwanseObjectLinkedDialogueBattleAction = { error: String(error && error.message || error) };
});
return true;
"""


def linked_dialogue_battle_action_state_script() -> str:
    return """
if (typeof render === 'function') render();
const action = window.__hwanseObjectLinkedDialogueBattleAction || null;
return {
  ...(action || {}),
  scene,
  map: map?.name || '',
  summary: window.HWANSE_LAST_BATTLE_PROTOTYPE || null,
  enemyVisual: battleState?.enemy?.visual || null,
  enemyProfile: battleState?.enemy?.profile || null,
  enemyAction: battleState?.enemy?.action || null,
  enemyName: battleState?.enemy?.name || '',
  enemyHpMax: battleState?.enemy?.hpMax ?? null,
  enemyAtk: battleState?.enemy?.atk ?? null,
  enemyDef: battleState?.enemy?.def ?? null,
  enemyProfileSource: battleState?.enemy?.profileSource || '',
  enemyOriginalEnemyRowBound: battleState?.enemy?.originalEnemyRowBound ?? null,
  enemyOriginalStatsOrRewardsBound: battleState?.enemy?.originalStatsOrRewardsBound ?? null,
  dialogueBattleLinkAction: window.HWANSE_LAST_DIALOGUE_BATTLE_LINK_ACTION || null,
  battleStartFeedbackLog: window.HWANSE_BATTLE_START_FEEDBACK_LOG || [],
  battleStartFeedbackRender: window.HWANSE_BATTLE_START_FEEDBACK_RENDER || [],
  battleStartFeedbackLast: window.HWANSE_LAST_BATTLE_START_FEEDBACK || null,
  battleStartFeedbackLastRender: window.HWANSE_LAST_BATTLE_START_FEEDBACK_RENDER || null,
  progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
};
"""


def linked_dialogue_battle_victory_script() -> str:
    return """
window.__hwanseObjectLinkedDialogueBattleVictory = null;
Promise.resolve().then(() => {
  if (scene !== 'battle' || !battleState) {
    throw new Error(`linked battle is not active: ${scene}`);
  }
  window.HWANSE_BATTLE_REWARD_EFFECT_LOG = [];
  window.HWANSE_BATTLE_REWARD_EFFECT_RENDER = [];
  window.HWANSE_LAST_BATTLE_REWARD_EFFECT = null;
  window.HWANSE_LAST_BATTLE_REWARD_EFFECT_RENDER = null;
  if (typeof activeBattleRewardEffect !== 'undefined') activeBattleRewardEffect = null;
  const before = {
    scene,
    map: map?.name || '',
    candidateId: battleState?.candidate?.id || '',
    blockId: battleState?.candidate?.blockId || '',
    battleBackground: battleState?.background?.name || '',
    enemyName: battleState?.enemy?.name || '',
    enemyHp: battleState?.enemy?.hp ?? null,
  };
  battleState.enemy.hp = 1;
  const attackResult = useSelectedBattleCommand();
  const finishedAfterAttack = battleState?.finished === true;
  const rewardGranted = battleState?.rewardGranted === true;
  if (!finishedAfterAttack || !rewardGranted) {
    throw new Error('linked battle did not finish after forced low HP attack');
  }
  if (typeof render === 'function') render();
  const victorySummary = window.HWANSE_LAST_BATTLE_PROTOTYPE || null;
  const victoryAutoSave = window.HWANSE_LAST_BATTLE_VICTORY_SAVE || null;
  const rewardEffect = window.HWANSE_LAST_BATTLE_REWARD_EFFECT || null;
  const rewardEffectRender = window.HWANSE_LAST_BATTLE_REWARD_EFFECT_RENDER || null;
  const rewardEffectLog = window.HWANSE_BATTLE_REWARD_EFFECT_LOG || [];
  const progressAfterVictory = window.HWANSE_LAST_PROTOTYPE_PROGRESS || null;
  let savedPayload = null;
  try {
    savedPayload = JSON.parse(localStorage.getItem(RUNTIME_SAVE_KEY) || 'null');
  } catch (error) {
    savedPayload = null;
  }
  const finishResult = useSelectedBattleCommand();
  updateObjectButton();
  updateDialogueButton();
  updateBattleButton();
  publishPrototypeCompletionState(eventObjectCandidateSelection());
  refreshPlayHudLines();
  if (typeof render === 'function') render();
  drawActionPrompt();
  const linkAfter = typeof dialogueBattleLinkState === 'function' ? dialogueBattleLinkState() : null;
  const runtimeItems = (runtimeState?.items || []).map((item) => ({
    key: item.key,
    name: item.name,
    count: item.count || 0,
  }));
  window.__hwanseObjectLinkedDialogueBattleVictory = {
    before,
    attackResult,
    finishedAfterAttack,
    rewardGranted,
    finishResult,
    scene,
    map: map?.name || '',
    victorySummary,
    victoryAutoSave,
    rewardEffect,
    rewardEffectRender,
    rewardEffectLog,
    progressAfterVictory,
    savedPayload,
    progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
    completion: window.HWANSE_LAST_PROTOTYPE_COMPLETION || null,
    objective: typeof prototypeObjectiveState === 'function' ? prototypeObjectiveState() : null,
    actionPromptAfter: window.HWANSE_LAST_ACTION_PROMPT || null,
    linkAfter: linkAfter ? {
      pending: linkAfter.pending === true,
      blockId: linkAfter.blockId || '',
      candidateId: linkAfter.candidate?.id || '',
      battleBackground: linkAfter.battleBackground || '',
    } : null,
    battleButtonText: document.getElementById('battleButton')?.textContent || '',
    battleButtonTitle: document.getElementById('battleButton')?.title || '',
    dialogueButtonText: document.getElementById('dialogueButton')?.textContent || '',
    objectButtonText: document.getElementById('objectButton')?.textContent || '',
    playHudLines: window.HWANSE_LAST_PLAY_HUD_LINES || [],
    runtimeMoney: runtimeState?.money ?? null,
    runtimeItems,
    originalEventDrivenBattleEntry: false,
    originalStoryFlagRuntimeImplemented: false,
  };
}).catch((error) => {
  window.__hwanseObjectLinkedDialogueBattleVictory = { error: String(error && error.message || error) };
});
return true;
"""


def linked_dialogue_battle_victory_state_script() -> str:
    return "return window.__hwanseObjectLinkedDialogueBattleVictory || null;"


def linked_dialogue_battle_chain_completion_script() -> str:
    return """
window.__hwanseObjectLinkedDialogueBattleChainCompletion = null;
Promise.resolve().then(async () => {
  if (scene !== 'map') throw new Error(`linked battle chain expected map scene, got ${scene}`);
  await ensureBattleData();
  const steps = [];
  let guard = 0;
  while (guard < 5) {
    activeDialogue = null;
    menuOpen = false;
    publishPrototypeCompletionState(eventObjectCandidateSelection());
    refreshPlayHudLines();
    if (typeof render === 'function') render();
    drawActionPrompt();
    const promptBefore = window.HWANSE_LAST_ACTION_PROMPT || null;
    const link = typeof dialogueBattleLinkState === 'function' ? dialogueBattleLinkState() : null;
    if (!link?.pending) break;
    const before = {
      prompt: promptBefore,
      blockId: link.blockId || '',
      candidateId: link.candidate?.id || '',
      battleBackground: link.battleBackground || '',
      progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
    };
    window.HWANSE_BATTLE_REWARD_EFFECT_LOG = [];
    window.HWANSE_BATTLE_REWARD_EFFECT_RENDER = [];
    window.HWANSE_LAST_BATTLE_REWARD_EFFECT = null;
    window.HWANSE_LAST_BATTLE_REWARD_EFFECT_RENDER = null;
    if (typeof activeBattleRewardEffect !== 'undefined') activeBattleRewardEffect = null;
    const startResult = await startBattlePrototype({
      candidate: link.candidate,
      background: link.background,
      dialogueBattleLink: link,
    });
    if (!startResult || scene !== 'battle' || !battleState) {
      throw new Error(`could not start linked battle ${before.candidateId}`);
    }
    const battleSummary = window.HWANSE_LAST_BATTLE_PROTOTYPE || null;
    battleState.enemy.hp = 1;
    const attackResult = useSelectedBattleCommand();
    const finishedAfterAttack = battleState?.finished === true;
    const rewardGranted = battleState?.rewardGranted === true;
    if (!finishedAfterAttack || !rewardGranted) {
      throw new Error(`linked battle did not finish ${before.candidateId}`);
    }
    if (typeof render === 'function') render();
    const victorySummary = window.HWANSE_LAST_BATTLE_PROTOTYPE || null;
    const victoryAutoSave = window.HWANSE_LAST_BATTLE_VICTORY_SAVE || null;
    const rewardEffect = window.HWANSE_LAST_BATTLE_REWARD_EFFECT || null;
    const rewardEffectRender = window.HWANSE_LAST_BATTLE_REWARD_EFFECT_RENDER || null;
    const finishResult = useSelectedBattleCommand();
    updateObjectButton();
    updateDialogueButton();
    updateBattleButton();
    publishPrototypeCompletionState(eventObjectCandidateSelection());
    refreshPlayHudLines();
    if (typeof render === 'function') render();
    drawActionPrompt();
    steps.push({
      before,
      startResult,
      battleSummary,
      attackResult,
      finishedAfterAttack,
      rewardGranted,
      finishResult,
      scene,
      map: map?.name || '',
      victorySummary,
      victoryAutoSave,
      rewardEffect,
      rewardEffectRender,
      progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
      completion: window.HWANSE_LAST_PROTOTYPE_COMPLETION || null,
      objective: typeof prototypeObjectiveState === 'function' ? prototypeObjectiveState() : null,
      actionPromptAfter: window.HWANSE_LAST_ACTION_PROMPT || null,
      linkAfter: (() => {
        const next = typeof dialogueBattleLinkState === 'function' ? dialogueBattleLinkState() : null;
        return next ? {
          pending: next.pending === true,
          blockId: next.blockId || '',
          candidateId: next.candidate?.id || '',
          battleBackground: next.battleBackground || '',
        } : null;
      })(),
      runtimeMoney: runtimeState?.money ?? null,
      runtimeItems: (runtimeState?.items || []).map((item) => ({
        key: item.key,
        name: item.name,
        count: item.count || 0,
      })),
    });
    guard += 1;
  }
  activeDialogue = null;
  menuOpen = false;
  publishPrototypeCompletionState(eventObjectCandidateSelection());
  refreshPlayHudLines();
	  if (typeof render === 'function') render();
	  drawActionPrompt();
	  const finalPromptBeforeNotice = window.HWANSE_LAST_ACTION_PROMPT || null;
	  const completionNoticeCountsBefore = { ...((window.HWANSE_LAST_PROTOTYPE_PROGRESS || {}).counts || {}) };
	  window.HWANSE_LAST_BATTLE_COMPLETION_NOTICE = null;
	  const completionNoticeActionResult = activateHotspotAtFoot();
	  if (typeof render === 'function') render();
	  const completionNotice = window.HWANSE_LAST_BATTLE_COMPLETION_NOTICE || null;
	  const completionNoticeDialogue = activeDialogue ? {
	    blockId: activeDialogue.block?.blockId || '',
	    lineCount: activeDialogue.lines?.length || 0,
	    firstLine: activeDialogue.lines?.[0] || '',
	    lines: [...(activeDialogue.lines || [])],
	  } : null;
	  const completionNoticeProgressAfter = window.HWANSE_LAST_PROTOTYPE_PROGRESS || null;
	  const completionNoticeCountsAfter = { ...((completionNoticeProgressAfter || {}).counts || {}) };
	  const completionNoticeDuplicateProgress =
	    JSON.stringify(completionNoticeCountsBefore) === JSON.stringify(completionNoticeCountsAfter);
	  const battleCompletionFeedbackLog = window.HWANSE_BATTLE_COMPLETION_FEEDBACK_LOG || [];
	  const battleCompletionFeedbackRender = window.HWANSE_BATTLE_COMPLETION_FEEDBACK_RENDER || [];
	  const battleCompletionFeedbackLast = window.HWANSE_LAST_BATTLE_COMPLETION_FEEDBACK || null;
	  const battleCompletionFeedbackLastRender = window.HWANSE_LAST_BATTLE_COMPLETION_FEEDBACK_RENDER || null;
	  let savedPayload = null;
	  try {
	    savedPayload = JSON.parse(localStorage.getItem(RUNTIME_SAVE_KEY) || 'null');
  } catch (error) {
    savedPayload = null;
  }
  window.__hwanseObjectLinkedDialogueBattleChainCompletion = {
    steps,
    stepCount: steps.length,
    scene,
    map: map?.name || '',
    progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
    completion: window.HWANSE_LAST_PROTOTYPE_COMPLETION || null,
	    objective: typeof prototypeObjectiveState === 'function' ? prototypeObjectiveState() : null,
	    actionPromptAfter: window.HWANSE_LAST_ACTION_PROMPT || null,
	    finalPromptBeforeNotice,
	    completionNoticeActionResult,
	    completionNotice,
	    completionNoticeDialogue,
	    completionNoticeCountsBefore,
	    completionNoticeProgressAfter,
	    completionNoticeDuplicateProgress,
	    battleCompletionFeedbackLog,
	    battleCompletionFeedbackRender,
	    battleCompletionFeedbackLast,
	    battleCompletionFeedbackLastRender,
	    linkAfter: (() => {
      const next = typeof dialogueBattleLinkState === 'function' ? dialogueBattleLinkState() : null;
      return next ? {
        pending: next.pending === true,
        blockId: next.blockId || '',
        candidateId: next.candidate?.id || '',
        battleBackground: next.battleBackground || '',
      } : null;
    })(),
    savedPayload,
    runtimeMoney: runtimeState?.money ?? null,
    runtimeItems: (runtimeState?.items || []).map((item) => ({
      key: item.key,
      name: item.name,
      count: item.count || 0,
    })),
    battleButtonText: document.getElementById('battleButton')?.textContent || '',
    battleButtonTitle: document.getElementById('battleButton')?.title || '',
    playHudLines: window.HWANSE_LAST_PLAY_HUD_LINES || [],
    originalEventDrivenBattleEntry: false,
    originalStoryFlagRuntimeImplemented: false,
  };
}).catch((error) => {
  window.__hwanseObjectLinkedDialogueBattleChainCompletion = { error: String(error && error.message || error) };
});
return true;
"""


def linked_dialogue_battle_chain_completion_state_script() -> str:
    return "return window.__hwanseObjectLinkedDialogueBattleChainCompletion || null;"


def facing_object_action_script() -> str:
    return """
window.__hwanseCandidateObjectFacingAction = null;
window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_LOG = [];
window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER = [];
window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK = null;
window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER = null;
if (typeof activeEventObjectInspectFeedbacks !== 'undefined') activeEventObjectInspectFeedbacks = [];
Promise.all([ensureSceneEvents(), ensureEventDialogueBlocks()]).then(() => {
  setPlayerPosition(11 * map.tileSize - 16, 12 * map.tileSize - 31);
  player.dir = 3;
  updateObjectButton();
  render();
  drawActionPrompt();
  const promptBefore = window.HWANSE_LAST_ACTION_PROMPT || null;
  const selectionBefore = eventObjectCandidateSelection({ nearOnly: true });
  render();
  const promptRender = window.HWANSE_LAST_EVENT_OBJECT_RENDER || null;
  const activated = activateHotspotAtFoot();
  render();
  window.__hwanseCandidateObjectFacingAction = {
    promptBefore,
    promptRender,
    selectionBefore: {
      scope: selectionBefore.scope,
      count: selectionBefore.candidates.length,
      assets: selectionBefore.candidates.map((candidate) => candidate.key),
      anchors: selectionBefore.candidates.map((candidate) => ({ x: candidate.anchor.x, y: candidate.anchor.y })),
    },
    activated,
    scene,
    map: map?.name || '',
    dir: player.dir,
    tile: footTile(),
    showEventHotspots,
    showEventObjectPrototypes,
    inspect: window.HWANSE_LAST_EVENT_OBJECT_INSPECT || null,
    render: window.HWANSE_LAST_EVENT_OBJECT_RENDER || null,
    inspectFeedbackLog: window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_LOG || [],
    inspectFeedbackRender: window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER || [],
    inspectFeedbackLast: window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK || null,
    inspectFeedbackLastRender: window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER || null,
    activeId: activeDialogue?.block?.blockId || '',
    activeLines: activeDialogue?.lines?.slice(0, 16) || [],
  };
}).catch((error) => {
  window.__hwanseCandidateObjectFacingAction = { error: String(error && error.message || error) };
});
return true;
"""


def facing_object_action_state_script() -> str:
    return "return window.__hwanseCandidateObjectFacingAction || null;"


def object_collision_script() -> str:
    return """
window.__hwanseCandidateObjectCollision = null;
Promise.all([ensureSceneEvents(), ensureEventDialogueBlocks()]).then(() =>
  Promise.all(eventObjectCandidatesForMap().map((candidate) => ensureImage(candidate.key))).then(() => {
    setPlayerPosition(11 * map.tileSize - 16, 12 * map.tileSize - 31);
    player.dir = 3;
    updateObjectButton();
    render();
    const before = {
      tile: footTile(),
      objectCollisionEnabled,
      anchors: eventObjectCollisionAnchors(),
      render: window.HWANSE_LAST_EVENT_OBJECT_RENDER || null,
    };
    const target = playerPositionForTile(11, 11);
    window.HWANSE_LAST_EVENT_OBJECT_COLLISION = null;
    const directCanMove = canMoveToTarget(target.x, target.y, 0, -1);
    const directCollision = window.HWANSE_LAST_EVENT_OBJECT_COLLISION || null;
    window.HWANSE_LAST_EVENT_OBJECT_COLLISION = null;
    const primary = movementTargetFromTile(footTile(), 0, -1);
    const movementCollision = window.HWANSE_LAST_EVENT_OBJECT_COLLISION || null;
    render();
    drawActionPrompt();
    const actionPrompt = window.HWANSE_LAST_ACTION_PROMPT || null;
    window.__hwanseCandidateObjectCollision = {
      scene,
      map: map?.name || '',
      before,
      targetTile: { x: 11, y: 11 },
      target,
      directCanMove,
      directCollision,
      primary,
      movementCollision,
      collision: movementCollision || directCollision,
      actionPrompt,
      render: window.HWANSE_LAST_EVENT_OBJECT_RENDER || null,
      foot: footTile(),
      dir: player.dir,
      objectCollisionEnabled,
    };
  })
).catch((error) => {
  window.__hwanseCandidateObjectCollision = { error: String(error && error.message || error) };
});
return true;
"""


def object_collision_state_script() -> str:
    return "return window.__hwanseCandidateObjectCollision || null;"


def object_collision_overlay_script() -> str:
    return """
window.__hwanseCandidateObjectCollisionOverlay = null;
ensureSceneEvents().then(() =>
  Promise.all(eventObjectCandidatesForMap().map((candidate) => ensureImage(candidate.key))).then(() => {
    setPlayerPosition(11 * map.tileSize - 16, 12 * map.tileSize - 31);
    player.dir = 3;
    showCollisionOverlay = true;
    render();
    window.__hwanseCandidateObjectCollisionOverlay = {
      scene,
      map: map?.name || '',
      showCollisionOverlay,
      objectCollisionEnabled,
      foot: footTile(),
      overlay: window.HWANSE_LAST_COLLISION_OVERLAY || null,
      render: window.HWANSE_LAST_EVENT_OBJECT_RENDER || null,
      tiles: eventObjectCollisionTiles(),
      anchors: eventObjectCollisionAnchors(),
    };
  })
).catch((error) => {
  window.__hwanseCandidateObjectCollisionOverlay = { error: String(error && error.message || error) };
});
return true;
"""


def object_collision_overlay_state_script() -> str:
    return "return window.__hwanseCandidateObjectCollisionOverlay || null;"


def object_collision_disabled_script() -> str:
    return """
window.__hwanseCandidateObjectCollisionDisabled = null;
Promise.all([ensureSceneEvents(), ensureEventDialogueBlocks()]).then(() =>
  Promise.all(eventObjectCandidatesForMap().map((candidate) => ensureImage(candidate.key))).then(() => {
    setPlayerPosition(11 * map.tileSize - 16, 12 * map.tileSize - 31);
    player.dir = 3;
    updateObjectButton();
    render();
    const before = {
      tile: footTile(),
      objectCollisionEnabled,
      anchors: eventObjectCollisionAnchors(),
      render: window.HWANSE_LAST_EVENT_OBJECT_RENDER || null,
    };
    const target = playerPositionForTile(11, 11);
    window.HWANSE_LAST_EVENT_OBJECT_COLLISION = null;
    const directCanMove = canMoveToTarget(target.x, target.y, 0, -1);
    const directCollision = window.HWANSE_LAST_EVENT_OBJECT_COLLISION || null;
    window.HWANSE_LAST_EVENT_OBJECT_COLLISION = null;
    const primary = movementTargetFromTile(footTile(), 0, -1);
    const movementCollision = window.HWANSE_LAST_EVENT_OBJECT_COLLISION || null;
    render();
    drawActionPrompt();
    const actionPrompt = window.HWANSE_LAST_ACTION_PROMPT || null;
    window.__hwanseCandidateObjectCollisionDisabled = {
      scene,
      map: map?.name || '',
      before,
      targetTile: { x: 11, y: 11 },
      target,
      directCanMove,
      directCollision,
      primary,
      movementCollision,
      collision: movementCollision || directCollision,
      actionPrompt,
      render: window.HWANSE_LAST_EVENT_OBJECT_RENDER || null,
      foot: footTile(),
      dir: player.dir,
      objectCollisionEnabled,
    };
  })
).catch((error) => {
  window.__hwanseCandidateObjectCollisionDisabled = { error: String(error && error.message || error) };
});
return true;
"""


def object_collision_disabled_state_script() -> str:
    return "return window.__hwanseCandidateObjectCollisionDisabled || null;"


def pointer_object_script() -> str:
    return """
window.__hwanseCandidateObjectPointer = null;
window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_LOG = [];
window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER = [];
window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK = null;
window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER = null;
if (typeof activeEventObjectInspectFeedbacks !== 'undefined') activeEventObjectInspectFeedbacks = [];
Promise.all([ensureSceneEvents(), ensureEventDialogueBlocks()]).then(() => {
  updateObjectButton();
  render();
  const canvas = document.getElementById('screen');
  const canvasRect = canvas.getBoundingClientRect();
  const renderState = window.HWANSE_LAST_EVENT_OBJECT_RENDER || {};
  const object = (renderState.objects || []).find((row) =>
    row.visible && row.loaded && row.screenWidth > 0 && row.screenHeight > 0
  );
  if (!object) {
    window.__hwanseCandidateObjectPointer = {
      ok: false,
      reason: 'missing-visible-object',
      render: renderState,
    };
    return;
  }
  const clientX = canvasRect.left + ((object.screenX + object.screenWidth / 2) / canvas.width) * canvasRect.width;
  const clientY = canvasRect.top + ((object.screenY + object.screenHeight / 2) / canvas.height) * canvasRect.height;
  canvas.dispatchEvent(new PointerEvent('pointermove', {
    bubbles: true,
    cancelable: true,
    pointerId: 82,
    pointerType: 'mouse',
    isPrimary: true,
    clientX,
    clientY,
  }));
  const hoverFeedback = window.HWANSE_LAST_CANVAS_POINTER_FEEDBACK || null;
  const hoverCursor = canvas.style.cursor || '';
  const hoverRender = window.HWANSE_LAST_EVENT_OBJECT_RENDER || null;
  const hitBefore = eventObjectRenderHitAtPoint(clientX, clientY);
  const selectionBefore = eventObjectCandidateSelectionAtPoint(clientX, clientY);
  const pointerDispatched = Boolean(hitBefore && selectionBefore.candidates.length);
  if (pointerDispatched) {
    canvas.dispatchEvent(new PointerEvent('pointerdown', {
      bubbles: true,
      cancelable: true,
      pointerId: 83,
      pointerType: 'mouse',
      isPrimary: true,
      clientX,
      clientY,
    }));
  }
  window.setTimeout(() => {
    render();
    window.__hwanseCandidateObjectPointer = {
      ok: pointerDispatched && activeDialogue?.block?.blockId === 'event-object-candidates:map2_03l',
      pointerDispatched,
      clientX,
      clientY,
      object,
      hoverFeedback,
      hoverCursor,
      hoverRender,
      hitBefore,
      selectionBefore: {
        scope: selectionBefore.scope,
        count: selectionBefore.candidates.length,
        assets: selectionBefore.candidates.map((candidate) => candidate.key),
        anchors: selectionBefore.candidates.map((candidate) => ({ x: candidate.anchor.x, y: candidate.anchor.y })),
      },
      pointer: window.HWANSE_LAST_EVENT_OBJECT_POINTER || null,
      scene,
      map: map?.name || '',
      showEventHotspots,
      showEventObjectPrototypes,
      inspect: window.HWANSE_LAST_EVENT_OBJECT_INSPECT || null,
      render: window.HWANSE_LAST_EVENT_OBJECT_RENDER || null,
      inspectFeedbackLog: window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_LOG || [],
      inspectFeedbackRender: window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER || [],
      inspectFeedbackLast: window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK || null,
      inspectFeedbackLastRender: window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER || null,
      activeId: activeDialogue?.block?.blockId || '',
      activeLines: activeDialogue?.lines?.slice(0, 16) || [],
    };
  }, 0);
}).catch((error) => {
  window.__hwanseCandidateObjectPointer = { ok: false, error: String(error && error.message || error) };
});
return true;
"""


def pointer_object_state_script() -> str:
    return "return window.__hwanseCandidateObjectPointer || null;"


def object_inspect_state_script() -> str:
    return """
if (typeof render === 'function') render();
return {
  scene,
  map: map?.name || '',
  showEventHotspots,
  showEventObjectPrototypes,
  inspect: window.HWANSE_LAST_EVENT_OBJECT_INSPECT || null,
  inspectFeedbackLog: window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_LOG || [],
  inspectFeedbackRender: window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER || [],
  inspectFeedbackLast: window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK || null,
  inspectFeedbackLastRender: window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER || null,
  render: window.HWANSE_LAST_EVENT_OBJECT_RENDER || null,
  activeId: activeDialogue?.block?.blockId || '',
  activeLines: activeDialogue?.lines?.slice(0, 16) || [],
};
"""


def descriptor_field_object_script() -> str:
    return """
window.__hwanseDescriptorFieldObject = null;
window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_LOG = [];
window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER = [];
window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK = null;
window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER = null;
if (typeof activeEventObjectInspectFeedbacks !== 'undefined') activeEventObjectInspectFeedbacks = [];
Promise.all([ensureSceneEvents(), ensureEventDialogueBlocks()]).then(() =>
  Promise.all(eventObjectCandidatesForMap().map((candidate) => ensureImage(candidate.key))).then(() => {
    setPlayerPosition(11 * map.tileSize - 16, 11 * map.tileSize - 16);
    player.dir = 3;
    updateObjectButton();
    render();
    const button = document.getElementById('objectButton');
    const beforeRender = window.HWANSE_LAST_EVENT_OBJECT_RENDER || null;
    const candidates = eventObjectCandidatesForMap();
    const selectionBefore = eventObjectCandidateSelection({ nearOnly: true });
    const activated = activateEventObjectCandidateSelection(() => eventObjectCandidateSelection({ nearOnly: true }));
    window.setTimeout(() => {
      render();
      const inspect = window.HWANSE_LAST_EVENT_OBJECT_INSPECT || null;
      const feedback = window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK || null;
      const feedbackRender = window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER || null;
      window.__hwanseDescriptorFieldObject = {
        scene,
        map: map?.name || '',
        expectedDescriptorTable: '0x00442d95',
        expectedDescriptorRow: 10,
        expectedDescriptorAsset: 'zsa_iwa.cns',
        expectedRuntimeSurface: 'field-object-overlay-candidate',
        beforeRender,
        candidates: candidates.map((candidate) => ({
          key: candidate.key,
          linked: candidate.linked || '',
          anchor: candidate.anchor,
          sceneIdHex: candidate.record?.sceneIdHex || '',
        })),
        selectionBefore: {
          scope: selectionBefore.scope,
          count: selectionBefore.candidates.length,
          assets: selectionBefore.candidates.map((candidate) => candidate.key),
          anchors: selectionBefore.candidates.map((candidate) => ({ x: candidate.anchor.x, y: candidate.anchor.y })),
        },
        activated,
        activeId: activeDialogue?.block?.blockId || '',
        activeLine: activeDialogue?.lines?.[activeDialogue.index] || '',
        activeLineCount: activeDialogue?.lines?.length || 0,
        inspect,
        inspectFeedback: feedback,
        inspectFeedbackRender: feedbackRender,
        render: window.HWANSE_LAST_EVENT_OBJECT_RENDER || null,
        objectButtonText: button?.textContent || '',
        objectButtonTitle: button?.title || '',
        originalLinkedDialogueRuntimeImplemented: inspect?.originalLinkedDialogueRuntimeImplemented ?? null,
        originalEventObjectRuntimeImplemented: inspect?.originalEventObjectRuntimeImplemented ?? null,
        originalStoryFlagRuntimeImplemented: inspect?.autoSave?.originalStoryFlagRuntimeImplemented ?? null,
      };
    }, 0);
  })
).catch((error) => {
  window.__hwanseDescriptorFieldObject = { error: String(error && error.message || error) };
});
return true;
"""


def descriptor_field_object_state_script() -> str:
    return "return window.__hwanseDescriptorFieldObject || null;"


def scene_linked_z_object_script(expected: dict) -> str:
    expected_json = json.dumps(expected, ensure_ascii=False)
    return f"""
const expected = {expected_json};
window.__hwanseSceneLinkedZObject = null;
localStorage.removeItem(RUNTIME_SAVE_KEY);
prototypeProgress = createPrototypeProgressState();
activeDialogue = null;
menuOpen = false;
menuMode = 'main';
selectedMenuItemIndex = 0;
window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_LOG = [];
window.HWANSE_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER = [];
window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK = null;
window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER = null;
window.HWANSE_LAST_EVENT_OBJECT_INSPECT = null;
if (typeof activeEventObjectInspectFeedbacks !== 'undefined') activeEventObjectInspectFeedbacks = [];
Promise.all([ensureSceneEvents(), ensureEventDialogueBlocks()]).then(() =>
  Promise.all(eventObjectCandidatesForMap().map((candidate) => ensureImage(candidate.key))).then(() => {{
    const anchor = expected.anchor || {{ x: 11, y: 11 }};
    setPlayerPosition(anchor.x * map.tileSize - 16, anchor.y * map.tileSize - 16);
    player.dir = 3;
    updateObjectButton();
    render();
    const button = document.getElementById('objectButton');
    const beforeRender = window.HWANSE_LAST_EVENT_OBJECT_RENDER || null;
    const candidates = eventObjectCandidatesForMap();
    const selectionBefore = eventObjectCandidateSelection({{ nearOnly: true }});
    const activated = activateEventObjectCandidateSelection(() => eventObjectCandidateSelection({{ nearOnly: true }}));
    window.setTimeout(() => {{
      render();
      const inspect = window.HWANSE_LAST_EVENT_OBJECT_INSPECT || null;
      const feedback = window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK || null;
      const feedbackRender = window.HWANSE_LAST_EVENT_OBJECT_INSPECT_FEEDBACK_RENDER || null;
      const renderState = window.HWANSE_LAST_EVENT_OBJECT_RENDER || null;
      window.__hwanseSceneLinkedZObject = {{
        expected,
        scene,
        map: map?.name || '',
        sceneEventsLoaded,
        beforeRender,
        render: renderState,
        candidates: candidates.map((candidate) => ({{
          key: candidate.key,
          linked: candidate.linked || '',
          anchor: candidate.anchor,
          sceneIdHex: candidate.record?.sceneIdHex || '',
          eventKind: candidate.record?.eventKind ?? null,
        }})),
        selectionBefore: {{
          scope: selectionBefore.scope,
          count: selectionBefore.candidates.length,
          assets: selectionBefore.candidates.map((candidate) => candidate.key),
          anchors: selectionBefore.candidates.map((candidate) => ({{ x: candidate.anchor.x, y: candidate.anchor.y }})),
          sceneIds: selectionBefore.candidates.map((candidate) => candidate.record?.sceneIdHex || ''),
        }},
        activated,
        activeId: activeDialogue?.block?.blockId || '',
        activeLine: activeDialogue?.lines?.[activeDialogue.index] || '',
        activeLineCount: activeDialogue?.lines?.length || 0,
        inspect,
        inspectFeedback: feedback,
        inspectFeedbackRender: feedbackRender,
        objectButtonText: button?.textContent || '',
        objectButtonTitle: button?.title || '',
        originalLinkedDialogueRuntimeImplemented: inspect?.originalLinkedDialogueRuntimeImplemented ?? null,
        originalEventObjectRuntimeImplemented: inspect?.originalEventObjectRuntimeImplemented ?? null,
        originalStoryFlagRuntimeImplemented: inspect?.autoSave?.originalStoryFlagRuntimeImplemented ?? null,
      }};
    }}, 0);
  }})
).catch((error) => {{
  window.__hwanseSceneLinkedZObject = {{ expected, error: String(error && error.message || error) }};
}});
return true;
"""


def scene_linked_z_object_state_script() -> str:
    return "return window.__hwanseSceneLinkedZObject || null;"


def linked_dialogue_followup_script() -> str:
    return """
const fromId = activeDialogue?.block?.blockId || '';
const lineCount = activeDialogue?.lines?.length || 0;
for (let i = 0; i < lineCount && activeDialogue; i += 1) {
  advanceDialogue();
}
const vmReplay = typeof eventVmReplaySummaryForBlock === 'function'
  ? eventVmReplaySummaryForBlock(activeDialogue?.block?.blockId || '')
  : null;
return {
  fromId,
  lineCount,
  activeId: activeDialogue?.block?.blockId || '',
  activeLine: activeDialogue?.lines?.[activeDialogue.index] || '',
  vmReplay,
  linkedStart: window.HWANSE_LAST_EVENT_OBJECT_LINKED_DIALOGUE_START || null,
  progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
};
"""


def linked_dialogue_completion_script() -> str:
    return """
const startId = activeDialogue?.block?.blockId || '';
const startLineCount = activeDialogue?.lines?.length || 0;
let guard = 0;
while (activeDialogue && guard < 128) {
  advanceDialogue();
  guard += 1;
}
const vmReplay = typeof eventVmReplaySummaryForBlock === 'function'
  ? eventVmReplaySummaryForBlock(startId)
  : null;
updateDialogueButton();
render();
const autoSave = window.HWANSE_LAST_DIALOGUE_COMPLETE_AUTO_SAVE || null;
const saved = autoSave?.saved === true;
const payload = JSON.parse(localStorage.getItem(RUNTIME_SAVE_KEY) || '{}');
const button = document.getElementById('dialogueButton');
const progress = window.HWANSE_LAST_PROTOTYPE_PROGRESS || null;
const progressVmReplay = progress?.lastEvent?.detail?.vmReplay || null;
const savedEvents = payload.prototypeProgress?.events || [];
const savedProgressVmReplay = savedEvents.find((event) => event?.id === startId && event?.kind === 'dialogue-complete')?.detail?.vmReplay || null;
return {
  startId,
  startLineCount,
  advancedCount: guard,
  activeId: activeDialogue?.block?.blockId || '',
  vmReplay,
  progressVmReplay,
  savedProgressVmReplay,
  buttonText: button?.textContent || '',
  buttonTitle: button?.title || '',
  menuLabels: menuItems().map((item) => menuItemLabel(item)),
  nextCandidateId: activeActionDialogueCandidate()?.blockId || '',
  actionPrompt: window.HWANSE_LAST_ACTION_PROMPT || null,
  saved,
  autoSave,
  completion: window.HWANSE_LAST_PROTOTYPE_COMPLETION || null,
  progress,
  savedProgress: payload.prototypeProgress || null,
};
"""


def linked_dialogue_all_completion_script() -> str:
    return """
const completedIds = [];
const openedIds = [];
const completedVmReplays = [];
for (let step = 0; step < 4; step += 1) {
  const started = startDialogueCandidateAction();
  const openedId = activeDialogue?.block?.blockId || '';
  if (!started) break;
  if (openedId.startsWith('dialogue-complete:')) {
    window.__hwanseObjectLinkedDialogueCompletionNotice = {
      blockId: openedId,
      line: activeDialogue?.lines?.[activeDialogue.index] || '',
      lineCount: activeDialogue?.lines?.length || 0,
    };
    break;
  }
  openedIds.push(openedId);
  let guard = 0;
  while (activeDialogue && guard < 128) {
    advanceDialogue();
    guard += 1;
  }
  const vmReplay = typeof eventVmReplaySummaryForBlock === 'function'
    ? eventVmReplaySummaryForBlock(openedId)
    : null;
  if (vmReplay) completedVmReplays.push(vmReplay);
  completedIds.push(openedId);
}
updateDialogueButton();
render();
const allCompleteActionResult = startDialogueCandidateAction();
const completionNotice = window.HWANSE_LAST_DIALOGUE_COMPLETION_NOTICE || null;
const completionNoticeBlock = activeDialogue ? {
  blockId: activeDialogue.block?.blockId || '',
  line: activeDialogue.lines?.[activeDialogue.index] || '',
  lineCount: activeDialogue.lines?.length || 0,
} : null;
const progressAfterNotice = window.HWANSE_LAST_PROTOTYPE_PROGRESS || null;
const autoSave = window.HWANSE_LAST_DIALOGUE_COMPLETE_AUTO_SAVE || null;
const saved = autoSave?.saved === true;
const payload = JSON.parse(localStorage.getItem(RUNTIME_SAVE_KEY) || '{}');
const button = document.getElementById('dialogueButton');
const progress = window.HWANSE_LAST_PROTOTYPE_PROGRESS || null;
const progressEvents = typeof ensurePrototypeProgressState === 'function'
  ? (ensurePrototypeProgressState().events || [])
  : (progress?.events || []);
const progressVmReplays = progressEvents
  .filter((event) => event?.kind === 'dialogue-complete' && (event?.detail?.vmReplay))
  .map((event) => event.detail.vmReplay);
const savedProgressVmReplays = (payload.prototypeProgress?.events || [])
  .filter((event) => event?.kind === 'dialogue-complete' && (event?.detail?.vmReplay))
  .map((event) => event.detail.vmReplay);
return {
  openedIds,
  completedIds,
  completedVmReplays,
  progressVmReplays,
  savedProgressVmReplays,
  allCompleteActionResult,
  completionNotice,
  completionNoticeBlock,
  progressAfterNotice,
  saved,
  autoSave,
  buttonText: button?.textContent || '',
  buttonTitle: button?.title || '',
  menuLabels: menuItems().map((item) => menuItemLabel(item)),
  nextCandidateId: activeActionDialogueCandidate()?.blockId || '',
  completion: window.HWANSE_LAST_PROTOTYPE_COMPLETION || null,
  progress,
  savedProgress: payload.prototypeProgress || null,
};
"""


def start_linked_dialogue_restore_script() -> str:
    return """
window.__hwanseObjectLinkedDialogueRestore = null;
quickLoadRuntime()
  .then((loaded) => Promise.all([ensureSceneEvents(), ensureEventDialogueBlocks()]).then(() => {
    updateObjectButton();
    updateDialogueButton();
    publishPrototypeCompletionState(eventObjectCandidateSelection());
    render();
    const dialogueButton = document.getElementById('dialogueButton');
    const objectButton = document.getElementById('objectButton');
    const labels = menuItems().map((item) => menuItemLabel(item));
    const actionPrompt = window.HWANSE_LAST_ACTION_PROMPT || null;
    window.HWANSE_LAST_DIALOGUE_COMPLETION_NOTICE = null;
    return activateDialogueButton().then((dialogueButtonResult) => {
      const completionNotice = window.HWANSE_LAST_DIALOGUE_COMPLETION_NOTICE || null;
      const completionNoticeBlock = activeDialogue ? {
        blockId: activeDialogue.block?.blockId || '',
        line: activeDialogue.lines?.[activeDialogue.index] || '',
        lineCount: activeDialogue.lines?.length || 0,
      } : null;
      activeDialogue = null;
      window.HWANSE_LAST_EVENT_OBJECT_COMPLETION_NOTICE = null;
      const menuEntries = menuItems();
      const menuDirectObjectIndex = menuEntries.findIndex((item) => item.command === 'startObjectCandidate');
      const menuObjectIndex = menuEntries.findIndex((item) => item.command === 'openObjectCandidateMenu');
      const menuObjectName = menuEntries[menuObjectIndex]?.name || '';
      selectedMenuItemIndex = menuObjectIndex;
      const menuObjectResult = useSelectedMenuItem();
      window.setTimeout(() => {
        const menuCandidateEntries = menuItems();
        window.__hwanseObjectLinkedDialogueRestore = {
          loaded,
          scene,
          map: map?.name || '',
          dialogueButtonText: dialogueButton?.textContent || '',
          dialogueButtonTitle: dialogueButton?.title || '',
          objectButtonText: objectButton?.textContent || '',
          objectButtonTitle: objectButton?.title || '',
          labels,
          labelsAfterDialogueButton: menuItems().map((item) => menuItemLabel(item)),
          actionPrompt,
          dialogueButtonResult,
          completionNotice,
          completionNoticeBlock,
          menuDirectObjectIndex,
          menuObjectName,
          menuObjectResult,
          menuObjectModeAfterOpen: menuMode,
          menuObjectOpenAfterOpen: menuOpen,
          menuObjectCandidateCount: menuCandidateEntries.filter((item) => item.command === 'selectObjectCandidate').length,
          menuObjectCandidateNames: menuCandidateEntries.map((item) => menuItemLabel(item)),
          completion: window.HWANSE_LAST_PROTOTYPE_COMPLETION || null,
          progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
          showEventObjectPrototypes,
          showDialogueCandidates,
        };
      }, 0);
    });
  }))
  .catch((error) => {
    window.__hwanseObjectLinkedDialogueRestore = { error: String(error && error.message || error) };
  });
return true;
"""


def linked_dialogue_restore_state_script() -> str:
    return "return window.__hwanseObjectLinkedDialogueRestore || null;"


def title_state_script() -> str:
    return """
const titleMenu = typeof titleMenuItems === 'function' ? titleMenuItems() : [];
return {
  readyState: document.readyState,
  search: location.search,
  hasScreen: !!document.getElementById('screen'),
  scene: typeof scene === 'undefined' ? '' : scene,
  titleLoaded: !!images?.title?.complete && (images.title.naturalWidth || 0) > 0,
  quickLoadHidden: document.getElementById('quickLoadButton')?.hidden ?? null,
  quickLoadText: document.getElementById('quickLoadButton')?.textContent || '',
  titleMenuKeys: titleMenu.map((item) => item.key),
  titleMenuLabels: titleMenu.map((item) => item.label),
};
"""


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 continued_object_map_state_script() -> str:
    return """
const foot = typeof footTile === 'function' ? footTile() : null;
return {
  readyState: document.readyState,
  search: location.search,
  hasScreen: !!document.getElementById('screen'),
  scene: typeof scene === 'undefined' ? '' : scene,
  map: typeof map === 'undefined' || !map ? '' : map.name,
  foot,
  quickLoadHidden: document.getElementById('quickLoadButton')?.hidden ?? null,
  quickLoadText: document.getElementById('quickLoadButton')?.textContent || '',
  playHudLines: window.HWANSE_LAST_PLAY_HUD_LINES || [],
  progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
};
"""


def start_title_continue_restore_script() -> str:
    return """
window.__hwanseObjectTitleContinueRestore = null;
Promise.all([ensureSceneEvents(), ensureEventDialogueBlocks()]).then(() => {
  updateObjectButton();
  updateDialogueButton();
  publishPrototypeCompletionState(eventObjectCandidateSelection());
  render();
  const dialogueButton = document.getElementById('dialogueButton');
  const objectButton = document.getElementById('objectButton');
  const labels = menuItems().map((item) => menuItemLabel(item));
  const actionPrompt = window.HWANSE_LAST_ACTION_PROMPT || null;
  const titleContinueMap = {
    search: location.search,
    quickLoadText: document.getElementById('quickLoadButton')?.textContent || '',
    playHudLines: window.HWANSE_LAST_PLAY_HUD_LINES || [],
    foot: typeof footTile === 'function' ? footTile() : null,
  };
  window.HWANSE_LAST_DIALOGUE_COMPLETION_NOTICE = null;
  return activateDialogueButton().then((dialogueButtonResult) => {
    const completionNotice = window.HWANSE_LAST_DIALOGUE_COMPLETION_NOTICE || null;
    const completionNoticeBlock = activeDialogue ? {
      blockId: activeDialogue.block?.blockId || '',
      line: activeDialogue.lines?.[activeDialogue.index] || '',
      lineCount: activeDialogue.lines?.length || 0,
    } : null;
    activeDialogue = null;
    window.HWANSE_LAST_EVENT_OBJECT_COMPLETION_NOTICE = null;
    const menuEntries = menuItems();
    const menuDirectObjectIndex = menuEntries.findIndex((item) => item.command === 'startObjectCandidate');
    const menuObjectIndex = menuEntries.findIndex((item) => item.command === 'openObjectCandidateMenu');
    const menuObjectName = menuEntries[menuObjectIndex]?.name || '';
    selectedMenuItemIndex = menuObjectIndex;
    const menuObjectResult = useSelectedMenuItem();
    window.setTimeout(() => {
      const menuCandidateEntries = menuItems();
      window.__hwanseObjectTitleContinueRestore = {
        titleContinue: true,
        scene,
        map: map?.name || '',
        foot: typeof footTile === 'function' ? footTile() : null,
        titleContinueMap,
        dialogueButtonText: dialogueButton?.textContent || '',
        dialogueButtonTitle: dialogueButton?.title || '',
        objectButtonText: objectButton?.textContent || '',
        objectButtonTitle: objectButton?.title || '',
        labels,
        labelsAfterDialogueButton: menuItems().map((item) => menuItemLabel(item)),
        actionPrompt,
        dialogueButtonResult,
        completionNotice,
        completionNoticeBlock,
        menuDirectObjectIndex,
        menuObjectName,
        menuObjectResult,
        menuObjectModeAfterOpen: menuMode,
        menuObjectOpenAfterOpen: menuOpen,
        menuObjectCandidateCount: menuCandidateEntries.filter((item) => item.command === 'selectObjectCandidate').length,
        menuObjectCandidateNames: menuCandidateEntries.map((item) => menuItemLabel(item)),
        completion: window.HWANSE_LAST_PROTOTYPE_COMPLETION || null,
        progress: window.HWANSE_LAST_PROTOTYPE_PROGRESS || null,
        showEventObjectPrototypes,
        showDialogueCandidates,
      };
    }, 0);
  });
}).catch((error) => {
  window.__hwanseObjectTitleContinueRestore = { error: String(error && error.message || error) };
});
return true;
"""


def title_continue_restore_state_script() -> str:
    return "return window.__hwanseObjectTitleContinueRestore || null;"


def wait_for_continue_ready_title(port: int, session_id: str, timeout: float = 8) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, title_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        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 "continue" in (state.get("titleMenuKeys") or [])
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"title continue button did not become ready: {state!r}")


def wait_for_continued_object_map(port: int, session_id: str, timeout: float = 8) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, continued_object_map_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        foot = state.get("foot") or {}
        counts = ((state.get("progress") or {}).get("counts") or {})
        if (
            state.get("readyState") == "complete"
            and state.get("hasScreen") is True
            and state.get("scene") == "map"
            and state.get("map") == "map2_03l"
            and foot.get("x") == 11
            and foot.get("y") == 11
            and state.get("quickLoadHidden") is False
            and state.get("quickLoadText") == "임시 불러오기"
            and state.get("search") == "?map=map2_03l&startTile=11%2C11"
            and counts.get("object-inspect") == 1
            and counts.get("dialogue-candidate") == 3
            and counts.get("dialogue-complete") == 3
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"title continue did not restore object map: {state!r}")


def wait_for_title_continue_restore_state(port: int, session_id: str, timeout: float = 8) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, title_continue_restore_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        dialogue = ((state.get("completion") or {}).get("dialogue") or {})
        if (
            state
            and state.get("error") is None
            and state.get("titleContinue") is True
            and state.get("map") == "map2_03l"
            and dialogue.get("completedCount") == 3
            and (state.get("completionNotice") or {}).get("blockId") == "dialogue-complete:map2_03l"
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate object title continue restore did not finish: {state!r}")


def wait_for_linked_dialogue_battle_action_state(port: int, session_id: str, timeout: float = 8) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    expected = EXPECTED_OBJECT_LINKED_DIALOGUE_BATTLE
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, linked_dialogue_battle_action_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        summary = state.get("summary") or {}
        if (
            state
            and state.get("error") is None
            and state.get("actionResult") is True
            and state.get("scene") == "battle"
            and summary.get("candidateId") == expected["candidateId"]
            and summary.get("battleBackground") == expected["battleBackground"]
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate object linked dialogue battle action did not start: {state!r}")


def wait_for_linked_dialogue_battle_victory_state(port: int, session_id: str, timeout: float = 8) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, linked_dialogue_battle_victory_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        counts = ((state.get("progress") or {}).get("counts") or {})
        if (
            state
            and state.get("error") is None
            and state.get("attackResult") is True
            and state.get("finishResult") is True
            and state.get("scene") == "map"
            and state.get("map") == "map2_03l"
            and counts.get("battle-victory") == 1
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate object linked dialogue battle victory did not finish: {state!r}")


def wait_for_linked_dialogue_battle_chain_completion_state(port: int, session_id: str, timeout: float = 10) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, linked_dialogue_battle_chain_completion_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        counts = ((state.get("progress") or {}).get("counts") or {})
        if (
            state
            and state.get("error") is None
            and state.get("scene") == "map"
            and state.get("map") == "map2_03l"
            and state.get("stepCount") == 2
            and counts.get("battle-victory") == 3
            and state.get("linkAfter") is None
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate object linked dialogue battle chain did not complete: {state!r}")


def wait_for_button_state(port: int, session_id: str, timeout: float = 6) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, object_button_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        if state and state.get("error") is None:
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate object button did not become ready: {state!r}")


def wait_for_passive_object_render_state(port: int, session_id: str, timeout: float = 8) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, passive_object_render_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        render = state.get("render") or {}
        if (
            state.get("scene") == "map"
            and state.get("map") == "map2_03l"
            and state.get("sceneEventsLoaded") is True
            and render.get("enabled") is True
            and render.get("passiveVisible") is True
            and render.get("prototypeVisible") is False
            and render.get("debugOverlay") is False
            and render.get("candidateCount") == 3
            and render.get("loadedCount") == 3
            and render.get("renderedCount", 0) >= 1
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate object passive render did not become ready: {state!r}")


def wait_for_object_candidate_menu_state(port: int, session_id: str, timeout: float = 6) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, object_candidate_menu_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        inspect = state.get("inspect") or {}
        render = state.get("render") or {}
        if (
            state.get("ok") is True
            and state.get("activeId") == "event-object-candidates:map2_03l"
            and inspect.get("scope") == "menu-object"
            and inspect.get("count") == 1
            and inspect.get("objectAssets") == ["zs_rg"]
            and render.get("enabled") is True
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate object menu selection did not become ready: {state!r}")


def wait_for_inspect_state(port: int, session_id: str, timeout: float = 6) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, object_inspect_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        inspect = state.get("inspect") or {}
        render = state.get("render") or {}
        if (
            state.get("activeId") == "event-object-candidates:map2_03l"
            and inspect.get("count") == 3
            and render.get("enabled") is True
            and render.get("prototypeVisible") is True
            and render.get("loadedCount") == 3
            and render.get("renderedCount", 0) >= 1
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate object inspect did not become ready: {state!r}")


def wait_for_descriptor_field_object_state(port: int, session_id: str, timeout: float = 6) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, descriptor_field_object_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        inspect = state.get("inspect") or {}
        before_render = state.get("beforeRender") or {}
        if (
            state
            and state.get("error") is None
            and state.get("map") == "map8_32q"
            and state.get("activeId") == "event-object-candidates:map8_32q"
            and inspect.get("scope") == "near-foot"
            and inspect.get("count") == 1
            and inspect.get("objectAssets") == ["zsa_iwa"]
            and before_render.get("candidateCount") == 1
            and before_render.get("loadedCount") == 1
            and before_render.get("renderedCount", 0) >= 1
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"descriptor field object inspect did not become ready: {state!r}")


def expected_scene_linked_assets(expected: dict) -> list[str]:
    assets = expected.get("assets")
    if isinstance(assets, list):
        return [str(asset) for asset in assets]
    asset = expected.get("asset")
    return [str(asset)] if asset else []


def expected_scene_linked_names(expected: dict) -> list[str]:
    linked = expected.get("linked")
    if isinstance(linked, list):
        return [str(name) for name in linked]
    return [str(linked)] if linked else []


def wait_for_scene_linked_z_object_state(port: int, session_id: str, expected: dict, timeout: float = 6) -> dict:
    expected_assets = set(expected_scene_linked_assets(expected))
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, scene_linked_z_object_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        inspect = state.get("inspect") or {}
        before_render = state.get("beforeRender") or {}
        if (
            state
            and state.get("error") is None
            and state.get("map") == expected.get("map")
            and set(inspect.get("objectAssets") or []) == expected_assets
            and before_render.get("candidateCount") == len(expected_assets)
            and before_render.get("loadedCount") == len(expected_assets)
            and before_render.get("renderedCount", 0) >= len(expected_assets)
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"scene-linked z object inspect did not become ready: {state!r}")


def wait_for_pointer_object_state(port: int, session_id: str, timeout: float = 6) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, pointer_object_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        inspect = state.get("inspect") or {}
        pointer = state.get("pointer") or {}
        if (
            state.get("ok") is True
            and state.get("activeId") == "event-object-candidates:map2_03l"
            and inspect.get("scope") == "pointer-object"
            and pointer.get("scope") == "pointer-object"
            and pointer.get("count") == 3
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate object pointer did not start inspect: {state!r}")


def wait_for_facing_action_state(port: int, session_id: str, timeout: float = 6) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, facing_object_action_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        inspect = state.get("inspect") or {}
        if (
            state.get("activated") is True
            and state.get("activeId") == "event-object-candidates:map2_03l"
            and inspect.get("scope") == "facing-object"
            and inspect.get("count") == 3
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate object facing action did not start inspect: {state!r}")


def wait_for_object_collision_state(port: int, session_id: str, timeout: float = 6) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, object_collision_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        collision = state.get("collision") or {}
        before_render = ((state.get("before") or {}).get("render") or {})
        prompt = state.get("actionPrompt") or {}
        if (
            state
            and state.get("error") is None
            and state.get("scene") == "map"
            and state.get("map") == "map2_03l"
            and state.get("directCanMove") is False
            and state.get("primary") is None
            and collision.get("blocked") is True
            and collision.get("anchorCount") == 3
            and before_render.get("collisionAnchorCount") == 3
            and prompt.get("kind") == "object-inspect"
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate object collision did not block movement: {state!r}")


def wait_for_object_collision_overlay_state(port: int, session_id: str, timeout: float = 6) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, object_collision_overlay_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        overlay = state.get("overlay") or {}
        if (
            state
            and state.get("error") is None
            and state.get("scene") == "map"
            and state.get("map") == "map2_03l"
            and state.get("showCollisionOverlay") is True
            and overlay.get("enabled") is True
            and overlay.get("objectAnchorCount") == 3
            and overlay.get("objectTileCount") == 1
            and overlay.get("visibleObjectTileCount") == 1
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate object collision overlay did not render object blocks: {state!r}")


def wait_for_object_collision_disabled_state(port: int, session_id: str, timeout: float = 6) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, object_collision_disabled_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        collision = state.get("collision") or {}
        before = state.get("before") or {}
        primary = state.get("primary") or {}
        prompt = state.get("actionPrompt") or {}
        if (
            state
            and state.get("error") is None
            and state.get("scene") == "map"
            and state.get("map") == "map2_03l"
            and state.get("objectCollisionEnabled") is False
            and state.get("directCanMove") is True
            and primary.get("nextTile") == {"x": 11, "y": 11}
            and collision.get("enabled") is False
            and collision.get("blocked") is False
            and collision.get("anchorCount") == 0
            and before.get("anchors") == []
            and prompt.get("kind") == "object-inspect"
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate object collision opt-out did not allow movement: {state!r}")


def wait_for_linked_dialogue_restore_state(port: int, session_id: str, timeout: float = 8) -> dict:
    deadline = time.monotonic() + timeout
    state: dict = {}
    while time.monotonic() < deadline:
        value = execute_js(port, session_id, linked_dialogue_restore_state_script(), timeout=3)
        state = value if isinstance(value, dict) else {}
        dialogue = ((state.get("completion") or {}).get("dialogue") or {})
        if (
            state
            and state.get("error") is None
            and state.get("loaded") is True
            and dialogue.get("completedCount") == 3
            and (state.get("completionNotice") or {}).get("blockId") == "dialogue-complete:map2_03l"
        ):
            return state
        time.sleep(0.2)
    raise WebDriverError(f"candidate object linked dialogue restore did not finish: {state!r}")


def verify_object_inspect_auto_save(auto_save: dict, expected_scope: str, expected_tile: dict) -> None:
    progress = auto_save.get("progress") or {}
    progress_counts = progress.get("counts") or {}
    progress_event = auto_save.get("progressEvent") or {}
    progress_detail = progress_event.get("detail") or {}
    if (
        auto_save.get("saved") is not True
        or auto_save.get("source") != "object-inspect"
        or auto_save.get("map") != "map2_03l"
        or auto_save.get("payloadMap") != "map2_03l"
        or (auto_save.get("tile") or {}) != expected_tile
        or (auto_save.get("payloadTile") or {}) != expected_tile
        or auto_save.get("id") != "event-object-candidates:map2_03l"
        or auto_save.get("scope") != expected_scope
        or auto_save.get("count") != 3
        or set(auto_save.get("objectAssets") or []) != {"zm_2", "zg_kni", "zs_rg"}
        or auto_save.get("linkedDialogueScope") != "tileset-family"
        or auto_save.get("linkedDialogueCount") != 3
        or set(auto_save.get("linkedDialogueBlockIds") or []) != {
            "event-dialogue-block-004",
            "event-dialogue-block-032",
            "event-dialogue-block-041",
        }
        or progress_counts.get("object-inspect") != 1
        or progress_event.get("kind") != "object-inspect"
        or progress_event.get("id") != "event-object-candidates:map2_03l"
        or progress_detail.get("originalLinkedDialogueRuntimeImplemented") is not False
        or progress_detail.get("originalEventObjectRuntimeImplemented") is not False
        or auto_save.get("originalEventVmRuntimeImplemented") is not False
        or auto_save.get("originalStoryFlagRuntimeImplemented") is not False
    ):
        raise WebDriverError(f"candidate object inspect auto-save is incomplete: {auto_save!r}")


def verify_object_candidate_menu_state(state: dict) -> None:
    before = state.get("before") or {}
    menu_marker = before.get("menuMarker") or {}
    selection = state.get("selection") or {}
    inspect = state.get("inspect") or {}
    auto_save = inspect.get("autoSave") or {}
    progress = state.get("progress") or {}
    progress_counts = progress.get("counts") or {}
    progress_event = inspect.get("progressEvent") or {}
    progress_detail = progress_event.get("detail") or {}
    saved_payload = state.get("savedPayload") or {}
    saved_counts = ((saved_payload.get("prototypeProgress") or {}).get("counts") or {})
    feedback_log = state.get("inspectFeedbackLog") or []
    feedback = state.get("inspectFeedbackLast") or {}
    feedback_render = state.get("inspectFeedbackLastRender") or {}
    linked_dialogue = inspect.get("linkedDialogue") or {}
    lines = "\n".join(state.get("activeLines") or [])
    render = state.get("render") or {}
    if (
        state.get("ok") is not True
        or state.get("scene") != "map"
        or state.get("map") != "map2_03l"
        or before.get("opened") is not True
        or before.get("menuMode") != "object-candidate"
        or before.get("count") != 4
        or before.get("targetKey") != "zs_rg"
        or not any("zs_rg" in str(label) for label in (before.get("labels") or []))
        or menu_marker.get("source") != "prototype-object-candidate-menu"
        or menu_marker.get("map") != "map2_03l"
        or menu_marker.get("count") != 3
        or not any((choice or {}).get("key") == "zs_rg" for choice in (menu_marker.get("choices") or []))
        or menu_marker.get("originalLinkedDialogueRuntimeImplemented") is not False
        or menu_marker.get("originalEventObjectRuntimeImplemented") is not False
        or menu_marker.get("originalStoryFlagRuntimeImplemented") is not False
        or state.get("commandResult") is not True
        or state.get("afterMenuMode") != "main"
        or state.get("afterMenuOpen") is not False
        or selection.get("source") != "prototype-object-candidate-menu"
        or selection.get("map") != "map2_03l"
        or selection.get("key") != "zs_rg"
        or selection.get("index") != 2
        or selection.get("count") != 3
        or selection.get("originalLinkedDialogueRuntimeImplemented") is not False
        or selection.get("originalEventObjectRuntimeImplemented") is not False
        or selection.get("originalStoryFlagRuntimeImplemented") is not False
        or state.get("activeId") != "event-object-candidates:map2_03l"
        or state.get("showEventObjectPrototypes") is not True
        or inspect.get("map") != "map2_03l"
        or inspect.get("scope") != "menu-object"
        or inspect.get("selectionSource") != "prototype-object-candidate-menu"
        or inspect.get("menuCandidateIndex") != 2
        or inspect.get("menuCandidateCount") != 3
        or inspect.get("count") != 1
        or inspect.get("objectAssets") != ["zs_rg"]
        or inspect.get("anchors") != [{"x": 11, "y": 11}]
        or inspect.get("originalLinkedDialogueRuntimeImplemented") is not False
        or inspect.get("originalEventObjectRuntimeImplemented") is not False
        or linked_dialogue.get("scope") != "tileset-family"
        or linked_dialogue.get("count") != 3
        or set(linked_dialogue.get("blockIds") or []) != {
            "event-dialogue-block-004",
            "event-dialogue-block-032",
            "event-dialogue-block-041",
        }
        or auto_save.get("saved") is not True
        or auto_save.get("source") != "object-inspect"
        or auto_save.get("scope") != "menu-object"
        or auto_save.get("selectionSource") != "prototype-object-candidate-menu"
        or auto_save.get("menuCandidateIndex") != 2
        or auto_save.get("menuCandidateCount") != 3
        or auto_save.get("count") != 1
        or auto_save.get("objectAssets") != ["zs_rg"]
        or progress_counts.get("object-inspect") != 1
        or saved_counts.get("object-inspect") != 1
        or progress_event.get("kind") != "object-inspect"
        or progress_detail.get("selectionSource") != "prototype-object-candidate-menu"
        or progress_detail.get("menuCandidateIndex") != 2
        or progress_detail.get("menuCandidateCount") != 3
        or progress_detail.get("originalLinkedDialogueRuntimeImplemented") is not False
        or progress_detail.get("originalEventObjectRuntimeImplemented") is not False
        or len(feedback_log) < 1
        or feedback.get("source") != "event-object-inspect-feedback"
        or feedback.get("inspectSource") != "object-inspect"
        or feedback.get("text") != "조사 완료 1"
        or feedback.get("scope") != "menu-object"
        or feedback.get("count") != 1
        or feedback.get("objectAssets") != ["zs_rg"]
        or feedback.get("inspectSound") != "menuConfirm"
        or not str(feedback.get("inspectSoundSrc") or "").endswith("/extract_wlk/04.wav")
        or feedback.get("inspectSoundPlayed") is not True
        or feedback_render.get("source") != "event-object-inspect-feedback"
        or feedback_render.get("text") != "조사 완료 1"
        or feedback_render.get("scope") != "menu-object"
        or feedback_render.get("count") != 1
        or feedback_render.get("objectAssets") != ["zs_rg"]
        or feedback_render.get("inspectSoundPlayed") is not True
        or render.get("prototypeVisible") is not True
        or render.get("loadedCount") != 3
        or "오브젝트 후보 map2_03l: 1개 (menu-object)" not in lines
        or "zs_rg" not in lines
        or "원본 NPC/오브젝트 스크립트 실행이 아니라 프로토타입 조사입니다." not in lines
    ):
        raise WebDriverError(f"candidate object menu selection is incomplete: {state!r}")


def verify_dialogue_complete_auto_save(
    auto_save: dict,
    expected_block_id: str,
    expected_dialogue_candidates: int,
    expected_dialogue_completions: int,
) -> None:
    progress = auto_save.get("progress") or {}
    progress_counts = progress.get("counts") or {}
    progress_event = auto_save.get("progressEvent") or {}
    progress_detail = progress_event.get("detail") or {}
    expected_tile = {"x": 11, "y": 11}
    if (
        auto_save.get("saved") is not True
        or auto_save.get("source") != "dialogue-complete"
        or auto_save.get("map") != "map2_03l"
        or auto_save.get("payloadMap") != "map2_03l"
        or (auto_save.get("tile") or {}) != expected_tile
        or (auto_save.get("payloadTile") or {}) != expected_tile
        or auto_save.get("blockId") != expected_block_id
        or progress_counts.get("object-inspect") != 1
        or progress_counts.get("dialogue-candidate") != expected_dialogue_candidates
        or progress_counts.get("dialogue-complete") != expected_dialogue_completions
        or progress_event.get("kind") != "dialogue-complete"
        or progress_event.get("id") != expected_block_id
        or progress_event.get("map") != "map2_03l"
        or progress_detail.get("originalEventVmRuntimeImplemented") is not False
        or auto_save.get("originalEventVmRuntimeImplemented") is not False
        or auto_save.get("originalStoryFlagRuntimeImplemented") is not False
    ):
        raise WebDriverError(f"candidate object dialogue completion auto-save is incomplete: {auto_save!r}")


def verify_linked_dialogue_vm_replay(vm_replay: dict, expected_block_id: str) -> None:
    expected = EXPECTED_LINKED_DIALOGUE_VM_REPLAYS.get(expected_block_id) or {}
    if (
        not isinstance(vm_replay, dict)
        or vm_replay.get("blockId") != expected_block_id
        or vm_replay.get("source") != "partial-opcode-replay"
        or vm_replay.get("traceCommandCount") != expected.get("traceCommandCount")
        or vm_replay.get("executedOpcodeCount") != expected.get("traceCommandCount")
        or vm_replay.get("separatorCommandCount") != expected.get("separatorEventCount")
        or vm_replay.get("separatorEventCount") != expected.get("separatorEventCount")
        or vm_replay.get("renderEventCount") != expected.get("renderEventCount")
        or vm_replay.get("vmDrivenLineCount") != expected.get("renderEventCount")
        or vm_replay.get("renderCoverageStatus") != "partial-render-prefix"
        or vm_replay.get("firstRenderTextSourceValueHex") != "0x00030000"
        or vm_replay.get("literalTextCommandCount") != expected.get("literalTextEventCount")
        or vm_replay.get("literalTextEventCount") != expected.get("literalTextEventCount")
        or vm_replay.get("routeLinkedEventVmExecution") is not False
        or vm_replay.get("browserEventVmPartialReplayImplemented") is not True
        or vm_replay.get("browserEventVmFullImplementation") is not False
        or vm_replay.get("originalEventVmRuntimeImplemented") is not False
        or vm_replay.get("originalStoryFlagRuntimeImplemented") is not False
    ):
        raise WebDriverError(f"linked dialogue VM replay is incomplete for {expected_block_id}: {vm_replay!r}")


def linked_dialogue_vm_replay_for(replays: list[dict], block_id: str) -> dict:
    return next(
        (
            row
            for row in replays
            if isinstance(row, dict) and row.get("blockId") == block_id
        ),
        {},
    )


def verify_linked_dialogue_vm_replays(replays: list[dict], expected_block_ids: list[str]) -> None:
    for block_id in expected_block_ids:
        verify_linked_dialogue_vm_replay(linked_dialogue_vm_replay_for(replays, block_id), block_id)


def linked_dialogue_vm_report(replays: list[dict]) -> str:
    rows = [row for row in replays if isinstance(row, dict)]
    return (
        f"vmBlocks={','.join(str(row.get('blockId') or '') for row in rows)} "
        f"vmPartial={','.join(str(row.get('browserEventVmPartialReplayImplemented')) for row in rows)} "
        f"vmFull={','.join(str(row.get('browserEventVmFullImplementation')) for row in rows)} "
        f"vmRender={','.join(str(row.get('renderEventCount')) for row in rows)} "
        f"vmSeparator={','.join(str(row.get('separatorEventCount')) for row in rows)} "
        f"vmLiteral={','.join(str(row.get('literalTextEventCount')) for row in rows)} "
        f"vmSource={','.join(str(row.get('firstRenderTextSourceValueHex')) for row in rows)}"
    )


def verify_passive_object_render(state: dict) -> None:
    render = state.get("render") or {}
    objects = render.get("objects") or []
    assets = {row.get("assetKey") for row in objects}
    visible_assets = {row.get("assetKey") for row in objects if row.get("visible")}
    if (
        state.get("scene") != "map"
        or state.get("map") != "map2_03l"
        or state.get("sceneEventsLoaded") is not True
        or state.get("showEventHotspots") is not False
        or state.get("showEventObjectPrototypes") is not False
        or state.get("objectButtonHidden") is not False
        or state.get("objectButtonText") != "조사 3"
        or "오브젝트 후보 3" not in str(state.get("objectButtonTitle") or "")
        or render.get("enabled") is not True
        or render.get("passiveVisible") is not True
        or render.get("prototypeVisible") is not False
        or render.get("debugOverlay") is not False
        or render.get("annotated") is not False
        or render.get("candidateCount") != 3
        or render.get("loadedCount") != 3
        or render.get("renderedCount", 0) < 1
        or render.get("objectCollisionEnabled") is not True
        or render.get("collisionAnchorCount") != 3
        or any(row.get("collisionBlocks") is not True for row in objects)
        or any(row.get("collisionTile") != {"x": 11, "y": 11} for row in objects)
        or assets != {"zm_2", "zg_kni", "zs_rg"}
        or not visible_assets
    ):
        raise WebDriverError(f"candidate object passive render is incomplete: {state!r}")


def verify_descriptor_field_object_state(state: dict) -> None:
    before_render = state.get("beforeRender") or {}
    render = state.get("render") or {}
    inspect = state.get("inspect") or {}
    feedback = state.get("inspectFeedback") or {}
    feedback_render = state.get("inspectFeedbackRender") or {}
    candidates = state.get("candidates") or []
    selection_before = state.get("selectionBefore") or {}
    lines = state.get("activeLine", "")
    if (
        state.get("scene") != "map"
        or state.get("map") != "map8_32q"
        or state.get("expectedDescriptorTable") != "0x00442d95"
        or state.get("expectedDescriptorRow") != 10
        or state.get("expectedDescriptorAsset") != "zsa_iwa.cns"
        or state.get("expectedRuntimeSurface") != "field-object-overlay-candidate"
        or len(candidates) != 1
        or candidates[0].get("key") != "zsa_iwa"
        or candidates[0].get("linked") != "zsa_iwa.cns"
        or candidates[0].get("anchor") != {"x": 11, "y": 11}
        or candidates[0].get("sceneIdHex") != "0xac18"
        or selection_before.get("scope") != "near-foot"
        or selection_before.get("count") != 1
        or selection_before.get("assets") != ["zsa_iwa"]
        or state.get("activated") is not True
        or state.get("activeId") != "event-object-candidates:map8_32q"
        or state.get("objectButtonText") != "조사 완료 1"
        or "오브젝트 후보 1개 조사 완료" not in str(state.get("objectButtonTitle") or "")
        or before_render.get("enabled") is not True
        or before_render.get("passiveVisible") is not True
        or before_render.get("candidateCount") != 1
        or before_render.get("loadedCount") != 1
        or before_render.get("renderedCount", 0) < 1
        or before_render.get("collisionAnchorCount") != 1
        or before_render.get("objectCollisionEnabled") is not True
        or inspect.get("scope") != "near-foot"
        or inspect.get("count") != 1
        or inspect.get("objectAssets") != ["zsa_iwa"]
        or inspect.get("anchors") != [{"x": 11, "y": 11}]
        or inspect.get("sceneIds") != ["0xac18"]
        or inspect.get("originalLinkedDialogueRuntimeImplemented") is not False
        or inspect.get("originalEventObjectRuntimeImplemented") is not False
        or ((inspect.get("autoSave") or {}).get("originalStoryFlagRuntimeImplemented")) is not False
        or feedback.get("source") != "event-object-inspect-feedback"
        or feedback.get("text") != "조사 완료 1"
        or feedback.get("objectAssets") != ["zsa_iwa"]
        or feedback.get("inspectSound") != "menuConfirm"
        or feedback.get("inspectSoundPlayed") is not True
        or feedback_render.get("text") != "조사 완료 1"
        or feedback_render.get("objectAssets") != ["zsa_iwa"]
        or render.get("prototypeVisible") is not True
        or render.get("loadedCount") != 1
        or state.get("originalLinkedDialogueRuntimeImplemented") is not False
        or state.get("originalEventObjectRuntimeImplemented") is not False
        or state.get("originalStoryFlagRuntimeImplemented") is not False
        or "오브젝트 후보 map8_32q: 1개" not in lines
    ):
        raise WebDriverError(f"descriptor field object smoke is incomplete: {state!r}")


def verify_scene_linked_z_object_state(state: dict, expected: dict) -> None:
    expected_assets = set(expected_scene_linked_assets(expected))
    expected_linked = set(expected_scene_linked_names(expected))
    expected_count = len(expected_assets)
    expected_anchor = expected.get("anchor") or {"x": 11, "y": 11}
    before_render = state.get("beforeRender") or {}
    render = state.get("render") or {}
    inspect = state.get("inspect") or {}
    feedback = state.get("inspectFeedback") or {}
    feedback_render = state.get("inspectFeedbackRender") or {}
    candidates = state.get("candidates") or []
    candidate_assets = {row.get("key") for row in candidates}
    candidate_linked = {row.get("linked") for row in candidates}
    selection_before = state.get("selectionBefore") or {}
    if (
        state.get("error") is not None
        or state.get("scene") != "map"
        or state.get("map") != expected.get("map")
        or state.get("sceneEventsLoaded") is not True
        or len(candidates) != expected_count
        or candidate_assets != expected_assets
        or candidate_linked != expected_linked
        or any(row.get("anchor") != expected_anchor for row in candidates)
        or any(row.get("sceneIdHex") != expected.get("sceneIdHex") for row in candidates)
        or any(row.get("eventKind") != expected.get("eventKind") for row in candidates)
        or selection_before.get("scope") != "near-foot"
        or selection_before.get("count") != expected_count
        or set(selection_before.get("assets") or []) != expected_assets
        or state.get("activated") is not True
        or state.get("activeId") != f"event-object-candidates:{expected.get('map')}"
        or state.get("objectButtonText") != f"조사 완료 {expected_count}"
        or before_render.get("enabled") is not True
        or before_render.get("passiveVisible") is not True
        or before_render.get("candidateCount") != expected_count
        or before_render.get("loadedCount") != expected_count
        or before_render.get("renderedCount", 0) < expected_count
        or before_render.get("collisionAnchorCount") != expected_count
        or before_render.get("objectCollisionEnabled") is not True
        or render.get("prototypeVisible") is not True
        or render.get("loadedCount") != expected_count
        or render.get("renderedCount", 0) < expected_count
        or inspect.get("scope") != "near-foot"
        or inspect.get("count") != expected_count
        or set(inspect.get("objectAssets") or []) != expected_assets
        or not all(anchor == expected_anchor for anchor in (inspect.get("anchors") or []))
        or set(inspect.get("sceneIds") or []) != {expected.get("sceneIdHex")}
        or inspect.get("originalLinkedDialogueRuntimeImplemented") is not False
        or inspect.get("originalEventObjectRuntimeImplemented") is not False
        or ((inspect.get("autoSave") or {}).get("originalStoryFlagRuntimeImplemented")) is not False
        or feedback.get("source") != "event-object-inspect-feedback"
        or feedback.get("text") != f"조사 완료 {expected_count}"
        or set(feedback.get("objectAssets") or []) != expected_assets
        or feedback.get("inspectSound") != "menuConfirm"
        or feedback.get("inspectSoundPlayed") is not True
        or feedback_render.get("text") != f"조사 완료 {expected_count}"
        or set(feedback_render.get("objectAssets") or []) != expected_assets
        or state.get("originalLinkedDialogueRuntimeImplemented") is not False
        or state.get("originalEventObjectRuntimeImplemented") is not False
        or state.get("originalStoryFlagRuntimeImplemented") is not False
    ):
        raise WebDriverError(f"scene-linked z object smoke is incomplete: {state!r}")


def scene_linked_z_object_report(states: list[dict]) -> str:
    rows = []
    loaded = 0
    rendered = 0
    inspected = 0
    anchors = 0
    checksums = []
    for state in states:
        expected = state.get("expected") or {}
        assets = sorted(expected_scene_linked_assets(expected))
        scene_id = expected.get("sceneIdHex") or ""
        rows.append(f"{state.get('map')}:{','.join(assets)}@{scene_id}")
        before_render = state.get("beforeRender") or {}
        inspect = state.get("inspect") or {}
        loaded += int(before_render.get("loadedCount") or 0)
        rendered += int(before_render.get("renderedCount") or 0)
        inspected += int(inspect.get("count") or 0)
        anchors += int(before_render.get("collisionAnchorCount") or 0)
        if state.get("checksum") is not None:
            checksums.append(str(state.get("checksum")))
    expected_total = sum(len(expected_scene_linked_assets(row)) for row in EXPECTED_SCENE_LINKED_Z_OBJECTS)
    return (
        f"verified={inspected}/{expected_total} "
        f"maps={';'.join(rows)} "
        f"loaded={loaded} rendered={rendered} inspected={inspected} anchors={anchors} "
        "scope=near-foot "
        "originalLinkedDialogueRuntimeImplemented=False "
        "originalEventObjectRuntimeImplemented=False "
        "originalStoryFlagRuntimeImplemented=False "
        f"checksums={','.join(checksums)}"
    )


def verify_button_case(port: int, session_id: str) -> tuple[dict, dict, int, dict, dict, dict]:
    execute_js(port, session_id, prepare_object_button_script(), timeout=3)
    button = wait_for_button_state(port, session_id)
    candidate_keys = {row.get("key") for row in button.get("candidates") or []}
    if (
        button.get("hidden") is not False
        or button.get("text") != "조사 3"
        or "오브젝트 후보 3" not in str(button.get("title") or "")
        or candidate_keys != {"zm_2", "zg_kni", "zs_rg"}
    ):
        raise WebDriverError(f"candidate object button did not expose expected candidates: {button!r}")

    clicked = execute_js(port, session_id, click_object_button_script(), timeout=3)
    if not clicked.get("ok") or clicked.get("before", {}).get("text") != "조사 3":
        raise WebDriverError(f"candidate object button click failed: {clicked!r}")
    state = wait_for_inspect_state(port, session_id)
    checksum = int(execute_js(port, session_id, canvas_checksum_script(), timeout=3))
    if checksum == 0:
        raise WebDriverError("candidate object inspect rendered a blank canvas")
    verify_inspect_state(state)
    followup = execute_js(port, session_id, linked_dialogue_followup_script(), timeout=5)
    verify_linked_dialogue_followup(followup)
    completion = execute_js(port, session_id, linked_dialogue_completion_script(), timeout=5)
    verify_linked_dialogue_completion(completion)
    all_completion = execute_js(port, session_id, linked_dialogue_all_completion_script(), timeout=5)
    verify_linked_dialogue_all_completion(all_completion)
    return button, state, checksum, followup, completion, all_completion


def verify_action_case(port: int, session_id: str) -> tuple[dict, int]:
    execute_js(port, session_id, prepare_object_button_script(), timeout=3)
    wait_for_button_state(port, session_id)
    activated = execute_js(port, session_id, action_object_script(), timeout=5)
    if (
        activated.get("activated") is not True
        or activated.get("before", {}).get("candidates") != 3
        or activated.get("before", {}).get("near") != 3
    ):
        raise WebDriverError(f"candidate object action did not start: {activated!r}")
    state = wait_for_inspect_state(port, session_id)
    checksum = int(execute_js(port, session_id, canvas_checksum_script(), timeout=3))
    if checksum == 0:
        raise WebDriverError("candidate object action rendered a blank canvas")
    verify_inspect_state(state)
    return state, checksum


def verify_pointer_case(port: int, session_id: str) -> tuple[dict, int]:
    execute_js(port, session_id, pointer_object_script(), timeout=5)
    state = wait_for_pointer_object_state(port, session_id)
    selection = state.get("selectionBefore") or {}
    pointer = state.get("pointer") or {}
    hit = state.get("hitBefore") or {}
    hover = state.get("hoverFeedback") or {}
    hover_render = state.get("hoverRender") or {}
    if (
        state.get("pointerDispatched") is not True
        or state.get("hoverCursor") != "pointer"
        or hover.get("kind") != "event-object"
        or hover.get("cursor") != "pointer"
        or (hover.get("hit") or {}).get("assetKey") not in {"zm_2", "zg_kni", "zs_rg"}
        or hover_render.get("hoveredCount") != 1
        or hover_render.get("hoverAssetKey") not in {"zm_2", "zg_kni", "zs_rg"}
        or selection.get("scope") != "pointer-object"
        or selection.get("count") != 3
        or set(selection.get("assets") or []) != {"zm_2", "zg_kni", "zs_rg"}
        or not all(anchor == {"x": 11, "y": 11} for anchor in (selection.get("anchors") or []))
        or pointer.get("scope") != "pointer-object"
        or pointer.get("count") != 3
        or set(pointer.get("objectAssets") or []) != {"zm_2", "zg_kni", "zs_rg"}
        or hit.get("assetKey") not in {"zm_2", "zg_kni", "zs_rg"}
    ):
        raise WebDriverError(f"candidate object pointer selection is incomplete: {state!r}")
    checksum = int(execute_js(port, session_id, canvas_checksum_script(), timeout=3))
    if checksum == 0:
        raise WebDriverError("candidate object pointer rendered a blank canvas")
    verify_inspect_state(state)
    return state, checksum


def verify_facing_action_case(port: int, session_id: str) -> tuple[dict, int]:
    execute_js(port, session_id, facing_object_action_script(), timeout=5)
    state = wait_for_facing_action_state(port, session_id)
    prompt = state.get("promptBefore") or {}
    prompt_render = state.get("promptRender") or {}
    selection = state.get("selectionBefore") or {}
    if (
        prompt.get("kind") != "object-inspect"
        or prompt.get("scope") != "facing-object"
        or prompt.get("text") != "Enter -> 조사 zg_kni 3개"
        or (prompt.get("target") or {}).get("assetKey") != "zg_kni"
        or (prompt.get("target") or {}).get("anchor") != {"x": 11, "y": 11}
        or prompt_render.get("actionHighlightedCount") != 1
        or prompt_render.get("actionAssetKey") != "zg_kni"
        or selection.get("scope") != "facing-object"
        or selection.get("count") != 3
        or set(selection.get("assets") or []) != {"zm_2", "zg_kni", "zs_rg"}
        or not all(anchor == {"x": 11, "y": 11} for anchor in (selection.get("anchors") or []))
        or state.get("dir") != 3
        or state.get("tile") != {"x": 11, "y": 12}
    ):
        raise WebDriverError(f"candidate object facing selection is incomplete: {state!r}")
    checksum = int(execute_js(port, session_id, canvas_checksum_script(), timeout=3))
    if checksum == 0:
        raise WebDriverError("candidate object facing action rendered a blank canvas")
    verify_inspect_state(state)
    return state, checksum


def verify_object_collision_case(port: int, session_id: str) -> dict:
    execute_js(port, session_id, object_collision_script(), timeout=5)
    state = wait_for_object_collision_state(port, session_id)
    collision = state.get("collision") or {}
    direct_collision = state.get("directCollision") or {}
    movement_collision = state.get("movementCollision") or {}
    before = state.get("before") or {}
    before_render = before.get("render") or {}
    render = state.get("render") or {}
    prompt = state.get("actionPrompt") or {}
    objects = before_render.get("objects") or []
    blocked_anchor = collision.get("blockedAnchor") or {}
    target_tiles = set(collision.get("targetTiles") or [])
    if (
        state.get("scene") != "map"
        or state.get("map") != "map2_03l"
        or state.get("objectCollisionEnabled") is not True
        or before.get("objectCollisionEnabled") is not True
        or state.get("foot") != {"x": 11, "y": 12}
        or state.get("dir") != 3
        or state.get("targetTile") != {"x": 11, "y": 11}
        or state.get("directCanMove") is not False
        or state.get("primary") is not None
        or collision.get("enabled") is not True
        or direct_collision.get("blocked") is not True
        or movement_collision.get("blocked") is not True
        or collision.get("blocked") is not True
        or collision.get("anchorCount") != 3
        or blocked_anchor != {"x": 11, "y": 11}
        or "11,11" not in target_tiles
        or collision.get("blockedAssetKey") not in {"zm_2", "zg_kni", "zs_rg"}
        or collision.get("originalEventObjectRuntimeImplemented") is not False
        or before_render.get("objectCollisionEnabled") is not True
        or before_render.get("collisionAnchorCount") != 3
        or render.get("objectCollisionEnabled") is not True
        or render.get("collisionAnchorCount") != 3
        or not objects
        or any(row.get("collisionBlocks") is not True for row in objects)
        or any(row.get("collisionTile") != {"x": 11, "y": 11} for row in objects)
        or prompt.get("kind") != "object-inspect"
        or prompt.get("scope") != "facing-object"
        or prompt.get("text") != "Enter -> 조사 zg_kni 3개"
        or (prompt.get("target") or {}).get("assetKey") != "zg_kni"
        or (prompt.get("target") or {}).get("anchor") != {"x": 11, "y": 11}
    ):
        raise WebDriverError(f"candidate object collision state is incomplete: {state!r}")
    return state


def verify_object_collision_overlay_case(port: int, session_id: str) -> tuple[dict, int]:
    execute_js(port, session_id, object_collision_overlay_script(), timeout=5)
    state = wait_for_object_collision_overlay_state(port, session_id)
    overlay = state.get("overlay") or {}
    tiles = state.get("tiles") or []
    anchors = state.get("anchors") or []
    render = state.get("render") or {}
    overlay_tiles = overlay.get("objectTiles") or []
    first_tile = tiles[0] if tiles else {}
    overlay_tile = overlay_tiles[0] if overlay_tiles else {}
    if (
        state.get("scene") != "map"
        or state.get("map") != "map2_03l"
        or state.get("showCollisionOverlay") is not True
        or state.get("objectCollisionEnabled") is not True
        or state.get("foot") != {"x": 11, "y": 12}
        or len(anchors) != 3
        or overlay.get("enabled") is not True
        or overlay.get("map") != "map2_03l"
        or overlay.get("objectCollisionEnabled") is not True
        or overlay.get("objectAnchorCount") != 3
        or overlay.get("objectTileCount") != 1
        or overlay.get("visibleObjectTileCount") != 1
        or overlay.get("originalEventObjectRuntimeImplemented") is not False
        or len(tiles) != 1
        or first_tile.get("x") != 11
        or first_tile.get("y") != 11
        or first_tile.get("count") != 3
        or set(first_tile.get("assets") or []) != {"zm_2", "zg_kni", "zs_rg"}
        or overlay_tile.get("x") != 11
        or overlay_tile.get("y") != 11
        or overlay_tile.get("count") != 3
        or set(overlay_tile.get("assets") or []) != {"zm_2", "zg_kni", "zs_rg"}
        or render.get("objectCollisionEnabled") is not True
        or render.get("collisionAnchorCount") != 3
    ):
        raise WebDriverError(f"candidate object collision overlay state is incomplete: {state!r}")
    checksum = int(execute_js(port, session_id, canvas_checksum_script(), timeout=3))
    if checksum == 0:
        raise WebDriverError("candidate object collision overlay rendered a blank canvas")
    return state, checksum


def verify_object_collision_disabled_case(port: int, session_id: str) -> dict:
    execute_js(port, session_id, object_collision_disabled_script(), timeout=5)
    state = wait_for_object_collision_disabled_state(port, session_id)
    collision = state.get("collision") or {}
    direct_collision = state.get("directCollision") or {}
    movement_collision = state.get("movementCollision") or {}
    before = state.get("before") or {}
    before_render = before.get("render") or {}
    render = state.get("render") or {}
    prompt = state.get("actionPrompt") or {}
    primary = state.get("primary") or {}
    target = primary.get("target") or {}
    if (
        state.get("scene") != "map"
        or state.get("map") != "map2_03l"
        or state.get("objectCollisionEnabled") is not False
        or before.get("objectCollisionEnabled") is not False
        or before.get("anchors") != []
        or state.get("foot") != {"x": 11, "y": 12}
        or state.get("dir") != 3
        or state.get("targetTile") != {"x": 11, "y": 11}
        or state.get("directCanMove") is not True
        or primary.get("nextTile") != {"x": 11, "y": 11}
        or target.get("x") != state.get("target", {}).get("x")
        or target.get("y") != state.get("target", {}).get("y")
        or collision.get("enabled") is not False
        or direct_collision.get("blocked") is not False
        or movement_collision.get("blocked") is not False
        or collision.get("blocked") is not False
        or collision.get("anchorCount") != 0
        or collision.get("blockedAssetKey") != ""
        or collision.get("blockedAnchor") is not None
        or collision.get("originalEventObjectRuntimeImplemented") is not False
        or before_render.get("objectCollisionEnabled") is not False
        or before_render.get("collisionAnchorCount") != 0
        or render.get("objectCollisionEnabled") is not False
        or render.get("collisionAnchorCount") != 0
        or prompt.get("kind") != "object-inspect"
        or prompt.get("scope") != "facing-object"
        or prompt.get("text") != "Enter -> 조사 zg_kni 3개"
        or (prompt.get("target") or {}).get("assetKey") != "zg_kni"
        or (prompt.get("target") or {}).get("anchor") != {"x": 11, "y": 11}
    ):
        raise WebDriverError(f"candidate object collision opt-out state is incomplete: {state!r}")
    return state


def verify_inspect_state(state: dict) -> None:
    inspect = state.get("inspect") or {}
    render = state.get("render") or {}
    feedback_log = state.get("inspectFeedbackLog") or []
    feedback = state.get("inspectFeedbackLast") or {}
    feedback_render = state.get("inspectFeedbackLastRender") or {}
    linked_dialogue = inspect.get("linkedDialogue") or {}
    linked_block_ids = set(linked_dialogue.get("blockIds") or [])
    progress_detail = ((inspect.get("progressEvent") or {}).get("detail") or {})
    assets = set(inspect.get("objectAssets") or [])
    feedback_assets = set(feedback.get("objectAssets") or [])
    feedback_render_assets = set(feedback_render.get("objectAssets") or [])
    anchors = inspect.get("anchors") or []
    lines = "\n".join(state.get("activeLines") or [])
    expected_tile = state.get("tile") or {"x": 11, "y": 11}
    inspect_scope = inspect.get("scope") or ""
    verify_object_inspect_auto_save(inspect.get("autoSave") or {}, inspect.get("scope") or "", expected_tile)
    if (
        state.get("scene") != "map"
        or state.get("map") != "map2_03l"
        or state.get("showEventHotspots") is not False
        or state.get("showEventObjectPrototypes") is not True
        or inspect.get("map") != "map2_03l"
        or inspect_scope not in {"near-foot", "current-map", "pointer-object", "facing-object"}
        or inspect.get("count") != 3
        or assets != {"zm_2", "zg_kni", "zs_rg"}
        or not all(anchor == {"x": 11, "y": 11} for anchor in anchors)
        or linked_dialogue.get("scope") != "tileset-family"
        or linked_dialogue.get("count") != 3
        or linked_block_ids != {"event-dialogue-block-004", "event-dialogue-block-032", "event-dialogue-block-041"}
        or linked_dialogue.get("originalEventVmRuntimeImplemented") is not False
        or inspect.get("originalLinkedDialogueRuntimeImplemented") is not False
        or inspect.get("originalEventObjectRuntimeImplemented") is not False
        or progress_detail.get("linkedDialogueScope") != "tileset-family"
        or progress_detail.get("linkedDialogueCount") != 3
        or set(progress_detail.get("linkedDialogueBlockIds") or []) != linked_block_ids
        or progress_detail.get("originalLinkedDialogueRuntimeImplemented") is not False
        or render.get("debugOverlay") is not False
        or render.get("prototypeVisible") is not True
        or render.get("candidateCount") != 3
        or render.get("loadedCount") != 3
        or render.get("renderedCount", 0) < 1
        or len(feedback_log) < 1
        or feedback.get("source") != "event-object-inspect-feedback"
        or feedback.get("inspectSource") != "object-inspect"
        or feedback.get("text") != "조사 완료 3"
        or feedback.get("map") != "map2_03l"
        or feedback.get("tile") != expected_tile
        or feedback.get("scope") != inspect_scope
        or feedback.get("count") != 3
        or feedback_assets != {"zm_2", "zg_kni", "zs_rg"}
        or feedback.get("linkedDialogueCount") != 3
        or feedback.get("inspectSound") != "menuConfirm"
        or not str(feedback.get("inspectSoundSrc") or "").endswith("/extract_wlk/04.wav")
        or feedback.get("inspectSoundPlayed") is not True
        or feedback.get("durationMs") != 1100
        or feedback.get("browserEventObjectInspectFeedbackImplemented") is not True
        or feedback.get("prototypeEventObjectInspectImplemented") is not True
        or feedback.get("originalLinkedDialogueRuntimeImplemented") is not False
        or feedback.get("originalEventObjectRuntimeImplemented") is not False
        or feedback.get("originalStoryFlagRuntimeImplemented") is not False
        or feedback_render.get("active") is not True
        or feedback_render.get("source") != "event-object-inspect-feedback"
        or feedback_render.get("inspectSource") != "object-inspect"
        or feedback_render.get("text") != "조사 완료 3"
        or feedback_render.get("map") != "map2_03l"
        or feedback_render.get("tile") != expected_tile
        or feedback_render.get("scope") != inspect_scope
        or feedback_render.get("count") != 3
        or feedback_render_assets != {"zm_2", "zg_kni", "zs_rg"}
        or feedback_render.get("linkedDialogueCount") != 3
        or feedback_render.get("inspectSound") != "menuConfirm"
        or not str(feedback_render.get("inspectSoundSrc") or "").endswith("/extract_wlk/04.wav")
        or feedback_render.get("inspectSoundPlayed") is not True
        or feedback_render.get("durationMs") != 1100
        or feedback_render.get("browserEventObjectInspectFeedbackImplemented") is not True
        or feedback_render.get("prototypeEventObjectInspectImplemented") is not True
        or feedback_render.get("originalLinkedDialogueRuntimeImplemented") is not False
        or feedback_render.get("originalEventObjectRuntimeImplemented") is not False
        or feedback_render.get("originalStoryFlagRuntimeImplemented") is not False
        or "오브젝트 후보 map2_03l: 3개" not in lines
        or "원본 NPC/오브젝트 스크립트 실행이 아니라 프로토타입 조사입니다." not in lines
        or "연결 대사 후보 3개 (tileset-family)" not in lines
        or "event-dialogue-block-004" not in lines
    ):
        raise WebDriverError(f"candidate object inspect state is incomplete: {state!r}")


def verify_linked_dialogue_followup(state: dict) -> None:
    linked_start = state.get("linkedStart") or {}
    progress = state.get("progress") or {}
    counts = progress.get("counts") or {}
    verify_linked_dialogue_vm_replay(state.get("vmReplay") or {}, "event-dialogue-block-004")
    if (
        state.get("fromId") != "event-object-candidates:map2_03l"
        or state.get("lineCount", 0) < 10
        or state.get("activeId") != "event-dialogue-block-004"
        or state.get("activeLine") != "아타호"
        or linked_start.get("fromBlockId") != "event-object-candidates:map2_03l"
        or linked_start.get("blockId") != "event-dialogue-block-004"
        or linked_start.get("sampleText") != "아타호"
        or linked_start.get("originalLinkedDialogueRuntimeImplemented") is not False
        or linked_start.get("originalEventVmRuntimeImplemented") is not False
        or counts.get("object-inspect") != 1
        or counts.get("dialogue-candidate") != 1
        or progress.get("originalStoryFlagRuntimeImplemented") is not False
    ):
        raise WebDriverError(f"candidate object linked dialogue follow-up is incomplete: {state!r}")


def verify_linked_dialogue_completion(state: dict) -> None:
    completion = state.get("completion") or {}
    dialogue_completion = completion.get("dialogue") or {}
    progress = state.get("progress") or {}
    progress_counts = progress.get("counts") or {}
    saved_progress = state.get("savedProgress") or {}
    saved_counts = saved_progress.get("counts") or {}
    labels = state.get("menuLabels") or []
    action_prompt = state.get("actionPrompt") or {}
    auto_save = state.get("autoSave") or {}
    verify_dialogue_complete_auto_save(auto_save, "event-dialogue-block-004", 1, 1)
    verify_linked_dialogue_vm_replay(state.get("vmReplay") or {}, "event-dialogue-block-004")
    verify_linked_dialogue_vm_replay(state.get("progressVmReplay") or {}, "event-dialogue-block-004")
    verify_linked_dialogue_vm_replay(state.get("savedProgressVmReplay") or {}, "event-dialogue-block-004")
    verify_linked_dialogue_vm_replay(auto_save.get("vmReplay") or {}, "event-dialogue-block-004")
    if (
        state.get("startId") != "event-dialogue-block-004"
        or state.get("startLineCount", 0) < 10
        or state.get("activeId") != ""
        or state.get("buttonText") != "대사 완료 1/3"
        or "현재 맵 대사 후보 3개 중 1개 완료" not in str(state.get("buttonTitle") or "")
        or "대사 선택 3" not in labels
        or state.get("nextCandidateId") != "event-dialogue-block-032"
        or action_prompt.get("kind") != "dialogue-candidate"
        or action_prompt.get("text") != "Enter -> 대사 2/3"
        or action_prompt.get("blockId") != "event-dialogue-block-032"
        or action_prompt.get("candidateIndex") != 1
        or action_prompt.get("completed") is not False
        or action_prompt.get("completedCount") != 1
        or state.get("saved") is not True
        or dialogue_completion.get("count") != 3
        or dialogue_completion.get("completedCount") != 1
        or dialogue_completion.get("remainingCount") != 2
        or dialogue_completion.get("completed") is not False
        or "event-dialogue-block-004" not in (dialogue_completion.get("completedIds") or [])
        or progress_counts.get("object-inspect") != 1
        or progress_counts.get("dialogue-candidate") != 1
        or progress_counts.get("dialogue-complete") != 1
        or saved_counts.get("object-inspect") != 1
        or saved_counts.get("dialogue-candidate") != 1
        or saved_counts.get("dialogue-complete") != 1
        or progress.get("originalStoryFlagRuntimeImplemented") is not False
        or saved_progress.get("originalStoryFlagRuntimeImplemented") is not False
    ):
        raise WebDriverError(f"candidate object linked dialogue completion is incomplete: {state!r}")


def verify_linked_dialogue_all_completion(state: dict) -> None:
    expected_remaining = ["event-dialogue-block-032", "event-dialogue-block-041"]
    completion = state.get("completion") or {}
    dialogue_completion = completion.get("dialogue") or {}
    progress = state.get("progress") or {}
    progress_counts = progress.get("counts") or {}
    saved_progress = state.get("savedProgress") or {}
    saved_counts = saved_progress.get("counts") or {}
    notice = state.get("completionNotice") or {}
    notice_block = state.get("completionNoticeBlock") or {}
    progress_after_notice = state.get("progressAfterNotice") or {}
    labels = state.get("menuLabels") or []
    auto_save = state.get("autoSave") or {}
    verify_dialogue_complete_auto_save(auto_save, "event-dialogue-block-041", 3, 3)
    verify_linked_dialogue_vm_replays(state.get("completedVmReplays") or [], expected_remaining)
    verify_linked_dialogue_vm_replays(
        state.get("progressVmReplays") or [],
        ["event-dialogue-block-004", "event-dialogue-block-032", "event-dialogue-block-041"],
    )
    verify_linked_dialogue_vm_replays(
        state.get("savedProgressVmReplays") or [],
        ["event-dialogue-block-004", "event-dialogue-block-032", "event-dialogue-block-041"],
    )
    verify_linked_dialogue_vm_replay(auto_save.get("vmReplay") or {}, "event-dialogue-block-041")
    if (
        state.get("openedIds") != expected_remaining
        or state.get("completedIds") != expected_remaining
        or state.get("allCompleteActionResult") is not True
        or notice.get("blockId") != "dialogue-complete:map2_03l"
        or notice.get("originalEventVmRuntimeImplemented") is not False
        or notice.get("originalStoryFlagRuntimeImplemented") is not False
        or notice_block.get("blockId") != "dialogue-complete:map2_03l"
        or notice_block.get("line") != "대사 완료 3/3"
        or state.get("buttonText") != "대사 완료 3"
        or "현재 맵 대사 후보 3개 완료" not in str(state.get("buttonTitle") or "")
        or "대사 선택 3" not in labels
        or state.get("nextCandidateId") not in {"event-dialogue-block-004", "event-dialogue-block-041", ""}
        or state.get("saved") is not True
        or dialogue_completion.get("count") != 3
        or dialogue_completion.get("completedCount") != 3
        or dialogue_completion.get("remainingCount") != 0
        or dialogue_completion.get("completed") is not True
        or set(dialogue_completion.get("completedIds") or []) != {
            "event-dialogue-block-004",
            "event-dialogue-block-032",
            "event-dialogue-block-041",
        }
        or progress_counts.get("object-inspect") != 1
        or progress_counts.get("dialogue-candidate") != 3
        or progress_counts.get("dialogue-complete") != 3
        or saved_counts.get("object-inspect") != 1
        or saved_counts.get("dialogue-candidate") != 3
        or saved_counts.get("dialogue-complete") != 3
        or (progress_after_notice.get("counts") or {}).get("dialogue-complete") != 3
        or progress.get("originalStoryFlagRuntimeImplemented") is not False
        or saved_progress.get("originalStoryFlagRuntimeImplemented") is not False
    ):
        raise WebDriverError(f"candidate object linked dialogue all-completion is incomplete: {state!r}")


def verify_linked_dialogue_restore(state: dict) -> None:
    completion = state.get("completion") or {}
    dialogue_completion = completion.get("dialogue") or {}
    object_completion = completion.get("objectInspect") or {}
    progress = state.get("progress") or {}
    progress_counts = progress.get("counts") or {}
    notice = state.get("completionNotice") or {}
    notice_block = state.get("completionNoticeBlock") or {}
    labels = state.get("labels") or []
    action_prompt = state.get("actionPrompt") or {}
    if (
        state.get("loaded") is not True
        or state.get("scene") != "map"
        or state.get("map") != "map2_03l"
        or state.get("dialogueButtonText") != "대사 완료 3"
        or "현재 맵 대사 후보 3개 완료" not in str(state.get("dialogueButtonTitle") or "")
        or state.get("objectButtonText") != "조사 완료 3"
        or "현재 맵 오브젝트 후보 3개 조사 완료" not in str(state.get("objectButtonTitle") or "")
        or "대사 선택 3" not in labels
        or "조사 선택 3" not in labels
        or action_prompt.get("kind") != "dialogue-battle-link"
        or action_prompt.get("text") != "Enter -> 대사 전투 btl_l1"
        or action_prompt.get("blockId") != "event-dialogue-block-041"
        or action_prompt.get("candidateId") != "event-dialogue-block-041:btl_l1"
        or action_prompt.get("battleBackground") != "btl_l1"
        or action_prompt.get("source") != "prototype-dialogue-battle-link"
        or action_prompt.get("prototypeDialogueBattleLinkImplemented") is not True
        or state.get("dialogueButtonResult") is not True
        or notice.get("blockId") != "dialogue-complete:map2_03l"
        or notice.get("originalEventVmRuntimeImplemented") is not False
        or notice.get("originalStoryFlagRuntimeImplemented") is not False
        or notice_block.get("blockId") != "dialogue-complete:map2_03l"
        or notice_block.get("line") != "대사 완료 3/3"
        or state.get("menuDirectObjectIndex") != -1
        or state.get("menuObjectName") != "조사 선택 3"
        or state.get("menuObjectResult") is not True
        or state.get("menuObjectModeAfterOpen") != "object-candidate"
        or state.get("menuObjectOpenAfterOpen") is not True
        or state.get("menuObjectCandidateCount") != 3
        or "완료 1/3" not in " ".join(state.get("menuObjectCandidateNames") or [])
        or "완료 2/3" not in " ".join(state.get("menuObjectCandidateNames") or [])
        or "완료 3/3" not in " ".join(state.get("menuObjectCandidateNames") or [])
        or dialogue_completion.get("count") != 3
        or dialogue_completion.get("completedCount") != 3
        or dialogue_completion.get("remainingCount") != 0
        or dialogue_completion.get("completed") is not True
        or set(dialogue_completion.get("completedIds") or []) != {
            "event-dialogue-block-004",
            "event-dialogue-block-032",
            "event-dialogue-block-041",
        }
        or object_completion.get("completed") is not True
        or object_completion.get("completedCount") != 3
        or progress_counts.get("object-inspect") != 1
        or progress_counts.get("dialogue-candidate") != 3
        or progress_counts.get("dialogue-complete") != 3
        or progress_counts.get("dialogue-battle-link") != 3
        or progress.get("originalStoryFlagRuntimeImplemented") is not False
        or state.get("showEventObjectPrototypes") is not True
        or state.get("showDialogueCandidates") is not True
    ):
        raise WebDriverError(f"candidate object linked dialogue restore is incomplete: {state!r}")


def verify_title_continue_restore(state: dict) -> None:
    completion = state.get("completion") or {}
    dialogue_completion = completion.get("dialogue") or {}
    object_completion = completion.get("objectInspect") or {}
    progress = state.get("progress") or {}
    progress_counts = progress.get("counts") or {}
    notice = state.get("completionNotice") or {}
    notice_block = state.get("completionNoticeBlock") or {}
    title_map = state.get("titleContinueMap") or {}
    labels = state.get("labels") or []
    action_prompt = state.get("actionPrompt") or {}
    foot = state.get("foot") or {}
    if (
        state.get("titleContinue") is not True
        or state.get("scene") != "map"
        or state.get("map") != "map2_03l"
        or foot.get("x") != 11
        or foot.get("y") != 11
        or title_map.get("quickLoadText") != "임시 불러오기"
        or title_map.get("search") != "?map=map2_03l&startTile=11%2C11"
        or state.get("dialogueButtonText") != "대사 완료 3"
        or "현재 맵 대사 후보 3개 완료" not in str(state.get("dialogueButtonTitle") or "")
        or state.get("objectButtonText") != "조사 완료 3"
        or "현재 맵 오브젝트 후보 3개 조사 완료" not in str(state.get("objectButtonTitle") or "")
        or "대사 선택 3" not in labels
        or "조사 선택 3" not in labels
        or action_prompt.get("kind") != "dialogue-battle-link"
        or action_prompt.get("text") != "Enter -> 대사 전투 btl_l1"
        or action_prompt.get("blockId") != "event-dialogue-block-041"
        or action_prompt.get("candidateId") != "event-dialogue-block-041:btl_l1"
        or action_prompt.get("battleBackground") != "btl_l1"
        or action_prompt.get("source") != "prototype-dialogue-battle-link"
        or action_prompt.get("prototypeDialogueBattleLinkImplemented") is not True
        or state.get("dialogueButtonResult") is not True
        or notice.get("blockId") != "dialogue-complete:map2_03l"
        or notice.get("originalEventVmRuntimeImplemented") is not False
        or notice.get("originalStoryFlagRuntimeImplemented") is not False
        or notice_block.get("blockId") != "dialogue-complete:map2_03l"
        or notice_block.get("line") != "대사 완료 3/3"
        or state.get("menuDirectObjectIndex") != -1
        or state.get("menuObjectName") != "조사 선택 3"
        or state.get("menuObjectResult") is not True
        or state.get("menuObjectModeAfterOpen") != "object-candidate"
        or state.get("menuObjectOpenAfterOpen") is not True
        or state.get("menuObjectCandidateCount") != 3
        or "완료 1/3" not in " ".join(state.get("menuObjectCandidateNames") or [])
        or "완료 2/3" not in " ".join(state.get("menuObjectCandidateNames") or [])
        or "완료 3/3" not in " ".join(state.get("menuObjectCandidateNames") or [])
        or dialogue_completion.get("count") != 3
        or dialogue_completion.get("completedCount") != 3
        or dialogue_completion.get("remainingCount") != 0
        or dialogue_completion.get("completed") is not True
        or object_completion.get("completed") is not True
        or object_completion.get("completedCount") != 3
        or progress_counts.get("object-inspect") != 1
        or progress_counts.get("dialogue-candidate") != 3
        or progress_counts.get("dialogue-complete") != 3
        or progress_counts.get("dialogue-battle-link") != 3
        or progress.get("originalStoryFlagRuntimeImplemented") is not False
        or state.get("showEventObjectPrototypes") is not True
        or state.get("showDialogueCandidates") is not True
    ):
        raise WebDriverError(f"candidate object title continue restore is incomplete: {state!r}")


def verify_linked_dialogue_battle_action(state: dict) -> None:
    expected = EXPECTED_OBJECT_LINKED_DIALOGUE_BATTLE
    prompt = state.get("actionPromptBefore") or {}
    link = state.get("linkBefore") or {}
    object_completion = state.get("objectCompletion") or {}
    dialogue_completion = state.get("dialogueCompletion") or {}
    link_action = state.get("dialogueBattleLinkAction") or {}
    summary = state.get("summary") or {}
    progress = state.get("progress") or {}
    progress_counts = progress.get("counts") or {}
    progress_before_counts = ((state.get("progressBefore") or {}).get("counts") or {})
    visual = state.get("enemyVisual") or {}
    profile = state.get("enemyProfile") or {}
    action = state.get("enemyAction") or {}
    feedback_log = state.get("battleStartFeedbackLog") or []
    feedback_render = state.get("battleStartFeedbackRender") or []
    feedback_last = state.get("battleStartFeedbackLast") or {}
    feedback_last_render = state.get("battleStartFeedbackLastRender") or {}
    feedback = next(
        (
            entry
            for entry in feedback_log
            if isinstance(entry, dict)
            and entry.get("source") == "battle-start-feedback"
            and entry.get("battleBackground") == expected["battleBackground"]
        ),
        {},
    )
    rendered_feedback = next(
        (
            entry
            for entry in feedback_render
            if isinstance(entry, dict)
            and entry.get("source") == "battle-start-feedback"
            and entry.get("battleBackground") == expected["battleBackground"]
        ),
        {},
    )
    if (
        prompt.get("kind") != "dialogue-battle-link"
        or prompt.get("text") != "Enter -> 대사 전투 btl_l1"
        or prompt.get("blockId") != expected["blockId"]
        or prompt.get("candidateId") != expected["candidateId"]
        or prompt.get("battleBackground") != expected["battleBackground"]
        or prompt.get("source") != "prototype-dialogue-battle-link"
        or prompt.get("prototypeDialogueBattleLinkImplemented") is not True
        or link.get("pending") is not True
        or link.get("blockId") != expected["blockId"]
        or link.get("candidateId") != expected["candidateId"]
        or link.get("battleBackground") != expected["battleBackground"]
        or link.get("progressEventKind") != "dialogue-battle-link"
        or link.get("progressEventId") != expected["blockId"]
        or link.get("prototypeDialogueBattleLinkImplemented") is not True
        or link.get("originalEventDrivenBattleEntry") is not False
        or link.get("originalEventVmRuntimeImplemented") is not False
        or link.get("originalStoryFlagRuntimeImplemented") is not False
        or object_completion.get("completed") is not True
        or object_completion.get("completedCount") != 3
        or dialogue_completion.get("completed") is not True
        or dialogue_completion.get("completedCount") != 3
        or progress_before_counts.get("dialogue-battle-link") != 3
        or state.get("actionResult") is not True
        or link_action.get("action") != "dialogue-battle-action"
        or link_action.get("handled") is not True
        or link_action.get("activeId") != expected["blockId"]
        or link_action.get("candidateId") != expected["candidateId"]
        or link_action.get("battleBackground") != expected["battleBackground"]
        or link_action.get("source") != "prototype-dialogue-battle-link"
        or link_action.get("prototypeDialogueBattleLinkImplemented") is not True
        or link_action.get("originalEventDrivenBattleEntry") is not False
        or state.get("scene") != "battle"
        or state.get("map") != expected["map"]
        or summary.get("map") != expected["map"]
        or summary.get("candidateId") != expected["candidateId"]
        or summary.get("blockId") != expected["blockId"]
        or summary.get("battleBackground") != expected["battleBackground"]
        or summary.get("dialogueBattleLink") is not True
        or summary.get("dialogueBlockId") != expected["blockId"]
        or summary.get("prototypeDialogueBattleLinkImplemented") is not True
        or summary.get("enemySpriteCandidate") != expected["enemySpriteCandidate"]
        or summary.get("enemySpriteAssetKey") != expected["enemySpriteAssetKey"]
        or summary.get("enemySpriteRefVaHex") != expected["enemySpriteRefVaHex"]
        or summary.get("battleBackgroundRefVaHex") != expected["battleBackgroundRefVaHex"]
        or summary.get("enemyName") != expected["enemyName"]
        or summary.get("enemyHp") != expected["enemyHp"]
        or summary.get("enemyAtk") != expected["enemyAtk"]
        or summary.get("enemyDef") != expected["enemyDef"]
        or summary.get("enemyActionName") != expected["enemyActionName"]
        or summary.get("enemyActionIndex") != expected["enemyActionIndex"]
        or summary.get("enemyActionSource") != "prototype-enemy-action"
        or summary.get("enemyRewardExp") != expected["enemyRewardExp"]
        or summary.get("enemyDropKey") != expected["enemyDropKey"]
        or summary.get("enemyDropName") != expected["enemyDropName"]
        or summary.get("enemyDropCount") != expected["enemyDropCount"]
        or not item_text_provenance_matches(summary, expected["enemyDropKey"], prefix="enemyDropItem")
        or summary.get("enemyProfileSource") != "prototype-enemy-profile"
        or summary.get("originalEnemyRowBound") is not False
        or summary.get("originalStatsOrRewardsBound") is not False
        or summary.get("originalEventDrivenBattleEntry") is not False
        or state.get("enemyName") != expected["enemyName"]
        or state.get("enemyHpMax") != expected["enemyHp"]
        or state.get("enemyAtk") != expected["enemyAtk"]
        or state.get("enemyDef") != expected["enemyDef"]
        or state.get("enemyProfileSource") != "prototype-enemy-profile"
        or state.get("enemyOriginalEnemyRowBound") is not False
        or state.get("enemyOriginalStatsOrRewardsBound") is not False
        or visual.get("enemyCns") != expected["enemySpriteCandidate"]
        or visual.get("enemyAssetKey") != expected["enemySpriteAssetKey"]
        or visual.get("enemyRefVaHex") != expected["enemySpriteRefVaHex"]
        or visual.get("battleBackgroundRefVaHex") != expected["battleBackgroundRefVaHex"]
        or profile.get("name") != expected["enemyName"]
        or profile.get("hp") != expected["enemyHp"]
        or profile.get("atk") != expected["enemyAtk"]
        or profile.get("def") != expected["enemyDef"]
        or profile.get("dropKey") != expected["enemyDropKey"]
        or profile.get("dropName") != expected["enemyDropName"]
        or profile.get("dropCount") != expected["enemyDropCount"]
        or not item_text_provenance_matches(profile.get("dropItemTextTable") or {}, expected["enemyDropKey"])
        or action.get("name") != expected["enemyActionName"]
        or action.get("index") != expected["enemyActionIndex"]
        or action.get("source") != "prototype-enemy-action"
        or progress_counts.get("object-inspect") != 1
        or progress_counts.get("dialogue-candidate") != 3
        or progress_counts.get("dialogue-complete") != 3
        or progress_counts.get("dialogue-battle-link") != 3
        or progress_counts.get("battle-start") != 1
        or feedback.get("text") != "전투 시작 btl_l1"
        or feedback.get("candidateId") != expected["candidateId"]
        or feedback.get("blockId") != expected["blockId"]
        or feedback.get("durationMs") != 1000
        or feedback.get("sourceBacked") is not True
        or feedback.get("battleStartSound") != "battleStart"
        or feedback.get("battleStartSoundSrc") != "../extract_wlk/08.wav"
        or feedback.get("battleStartSoundPlayed") is not True
        or feedback.get("browserBattleStartFeedbackImplemented") is not True
        or feedback.get("prototypeBattleStartImplemented") is not True
        or rendered_feedback.get("text") != "전투 시작 btl_l1"
        or rendered_feedback.get("candidateId") != expected["candidateId"]
        or rendered_feedback.get("blockId") != expected["blockId"]
        or rendered_feedback.get("durationMs") != 1000
        or rendered_feedback.get("battleStartSound") != "battleStart"
        or rendered_feedback.get("battleStartSoundSrc") != "../extract_wlk/08.wav"
        or rendered_feedback.get("battleStartSoundPlayed") is not True
        or rendered_feedback.get("browserBattleStartFeedbackImplemented") is not True
        or rendered_feedback.get("prototypeBattleStartImplemented") is not True
        or feedback_last.get("battleBackground") != expected["battleBackground"]
        or feedback_last.get("battleStartSound") != "battleStart"
        or feedback_last.get("battleStartSoundSrc") != "../extract_wlk/08.wav"
        or feedback_last.get("battleStartSoundPlayed") is not True
        or feedback_last_render.get("battleBackground") != expected["battleBackground"]
        or feedback_last_render.get("battleStartSound") != "battleStart"
        or feedback_last_render.get("battleStartSoundSrc") != "../extract_wlk/08.wav"
        or feedback_last_render.get("battleStartSoundPlayed") is not True
    ):
        raise WebDriverError(f"candidate object linked dialogue battle action is incomplete: {state!r}")


def verify_linked_dialogue_battle_victory(state: dict) -> None:
    expected = EXPECTED_OBJECT_LINKED_DIALOGUE_BATTLE
    before = state.get("before") or {}
    victory_summary = state.get("victorySummary") or {}
    victory_auto_save = state.get("victoryAutoSave") or {}
    reward_effect = state.get("rewardEffect") or {}
    reward_effect_render = state.get("rewardEffectRender") or {}
    progress = state.get("progress") or {}
    progress_counts = progress.get("counts") or {}
    progress_after_victory_counts = ((state.get("progressAfterVictory") or {}).get("counts") or {})
    saved_progress_counts = (((state.get("savedPayload") or {}).get("prototypeProgress") or {}).get("counts") or {})
    completion = state.get("completion") or {}
    objective = state.get("objective") or {}
    action_prompt = state.get("actionPromptAfter") or {}
    link_after = state.get("linkAfter") or {}
    progress_event = victory_summary.get("progressEvent") or {}
    progress_detail = progress_event.get("detail") or {}
    item_drop = victory_summary.get("itemDrop") or {}
    auto_save_progress_counts = ((victory_auto_save.get("progress") or {}).get("counts") or {})
    if (
        before.get("candidateId") != expected["candidateId"]
        or before.get("blockId") != expected["blockId"]
        or before.get("battleBackground") != expected["battleBackground"]
        or before.get("enemyName") != expected["enemyName"]
        or state.get("attackResult") is not True
        or state.get("finishedAfterAttack") is not True
        or state.get("rewardGranted") is not True
        or state.get("finishResult") is not True
        or state.get("scene") != "map"
        or state.get("map") != expected["map"]
        or victory_summary.get("candidateId") != expected["candidateId"]
        or victory_summary.get("blockId") != expected["blockId"]
        or victory_summary.get("battleBackground") != expected["battleBackground"]
        or victory_summary.get("enemyName") != expected["enemyName"]
        or victory_summary.get("rewardGranted") is not True
        or progress_event.get("kind") != "battle-victory"
        or progress_event.get("id") != expected["blockId"]
        or progress_detail.get("battleBackground") != expected["battleBackground"]
        or progress_detail.get("blockId") != expected["blockId"]
        or progress_detail.get("enemyName") != expected["enemyName"]
        or progress_detail.get("exp") != expected["enemyRewardExp"]
        or progress_detail.get("itemKey") != expected["enemyDropKey"]
        or progress_detail.get("itemName") != expected["enemyDropName"]
        or progress_detail.get("dropCountGranted") != expected["enemyDropCount"]
        or progress_detail.get("source") != "candidate-battle"
        or progress_detail.get("originalEventDrivenBattleEntry") is not False
        or progress_detail.get("originalRewardTableMapped") is not False
        or progress_detail.get("originalDropTableMapped") is not False
        or not item_text_provenance_matches(progress_detail, expected["enemyDropKey"])
        or item_drop.get("itemKey") != expected["enemyDropKey"]
        or item_drop.get("itemName") != expected["enemyDropName"]
        or item_drop.get("countGranted") != expected["enemyDropCount"]
        or item_drop.get("source") != "prototype-enemy-drop"
        or not item_text_provenance_matches(item_drop, expected["enemyDropKey"])
        or victory_auto_save.get("saved") is not True
        or victory_auto_save.get("source") != "battle-victory"
        or victory_auto_save.get("map") != expected["map"]
        or victory_auto_save.get("payloadMap") != expected["map"]
        or victory_auto_save.get("battleBackground") != expected["battleBackground"]
        or victory_auto_save.get("battleId") != expected["blockId"]
        or not item_text_provenance_matches(victory_auto_save, expected["enemyDropKey"])
        or auto_save_progress_counts.get("battle-victory") != 1
        or saved_progress_counts.get("battle-victory") != 1
        or progress_after_victory_counts.get("battle-victory") != 1
        or progress_counts.get("battle-victory") != 1
        or progress_counts.get("object-inspect") != 1
        or progress_counts.get("dialogue-candidate") != 3
        or progress_counts.get("dialogue-complete") != 3
        or progress_counts.get("dialogue-battle-link") != 3
        or progress_counts.get("battle-start") != 1
        or objective.get("title") != "후보 대사 전투 btl_l2"
        or objective.get("nextAction") != "전투 시작"
        or objective.get("source") != "prototype-dialogue-battle-link"
        or action_prompt.get("kind") != "dialogue-battle-link"
        or action_prompt.get("text") != "Enter -> 대사 전투 btl_l2"
        or action_prompt.get("completed") is not False
        or action_prompt.get("blockId") != "event-dialogue-block-032"
        or action_prompt.get("candidateId") != "event-dialogue-block-032:btl_l2"
        or action_prompt.get("battleBackground") != "btl_l2"
        or action_prompt.get("source") != "prototype-dialogue-battle-link"
        or action_prompt.get("prototypeDialogueBattleLinkImplemented") is not True
        or link_after.get("pending") is not True
        or link_after.get("blockId") != "event-dialogue-block-032"
        or link_after.get("candidateId") != "event-dialogue-block-032:btl_l2"
        or link_after.get("battleBackground") != "btl_l2"
        or reward_effect.get("source") != "battle-victory"
        or reward_effect.get("battleBackground") != expected["battleBackground"]
        or reward_effect.get("enemyName") != expected["enemyName"]
        or reward_effect.get("browserBattleRewardFeedbackImplemented") is not True
        or reward_effect.get("battleRewardSound") != "victory"
        or reward_effect.get("battleRewardSoundSrc") != "../extract_wlk/12.wav"
        or reward_effect.get("battleRewardSoundPlayed") is not True
        or not item_text_provenance_matches(reward_effect.get("itemDrop") or {}, expected["enemyDropKey"])
        or reward_effect_render.get("active") is not True
        or reward_effect_render.get("battleRewardSound") != "victory"
        or reward_effect_render.get("battleRewardSoundSrc") != "../extract_wlk/12.wav"
        or reward_effect_render.get("battleRewardSoundPlayed") is not True
        or not item_text_provenance_matches(reward_effect_render, expected["enemyDropKey"])
        or state.get("runtimeMoney") is None
        or next((item for item in state.get("runtimeItems") or [] if item.get("key") == expected["enemyDropKey"]), {}).get("count") != expected["enemyDropCount"]
        or state.get("originalEventDrivenBattleEntry") is not False
        or state.get("originalStoryFlagRuntimeImplemented") is not False
    ):
        raise WebDriverError(f"candidate object linked dialogue battle victory is incomplete: {state!r}")


def verify_linked_dialogue_battle_chain_completion(state: dict) -> None:
    steps = state.get("steps") or []
    progress = state.get("progress") or {}
    progress_counts = progress.get("counts") or {}
    saved_counts = (((state.get("savedPayload") or {}).get("prototypeProgress") or {}).get("counts") or {})
    completion = state.get("completion") or {}
    battle_completion = completion.get("battle") or {}
    objective = state.get("objective") or {}
    action_prompt = state.get("actionPromptAfter") or {}
    final_prompt = state.get("finalPromptBeforeNotice") or {}
    completion_notice = state.get("completionNotice") or {}
    completion_notice_dialogue = state.get("completionNoticeDialogue") or {}
    completion_notice_lines = completion_notice.get("lines") or []
    completion_notice_counts_after = ((state.get("completionNoticeProgressAfter") or {}).get("counts") or {})
    completion_feedback_log = state.get("battleCompletionFeedbackLog") or []
    completion_feedback_render = state.get("battleCompletionFeedbackRender") or []
    completion_feedback_last = state.get("battleCompletionFeedbackLast") or {}
    completion_feedback_last_render = state.get("battleCompletionFeedbackLastRender") or {}
    expected_steps = [
        ("event-dialogue-block-032:btl_l2", "event-dialogue-block-032", "btl_l2", "event-dialogue-block-004:btl_l1"),
        ("event-dialogue-block-004:btl_l1", "event-dialogue-block-004", "btl_l1", None),
    ]
    if len(steps) != len(expected_steps):
        raise WebDriverError(f"candidate object linked battle chain step count mismatch: {state!r}")
    for index, (candidate_id, block_id, background, next_candidate_id) in enumerate(expected_steps):
        step = steps[index] or {}
        before = step.get("before") or {}
        victory_summary = step.get("victorySummary") or {}
        victory_auto_save = step.get("victoryAutoSave") or {}
        reward_effect = step.get("rewardEffect") or {}
        reward_effect_render = step.get("rewardEffectRender") or {}
        progress_detail = ((victory_summary.get("progressEvent") or {}).get("detail") or {})
        item_drop = victory_summary.get("itemDrop") or {}
        link_after = step.get("linkAfter")
        step_counts = ((step.get("progress") or {}).get("counts") or {})
        if (
            before.get("candidateId") != candidate_id
            or before.get("blockId") != block_id
            or before.get("battleBackground") != background
            or step.get("startResult") is not True
            or step.get("attackResult") is not True
            or step.get("finishedAfterAttack") is not True
            or step.get("rewardGranted") is not True
            or step.get("finishResult") is not True
            or step.get("scene") != "map"
            or step.get("map") != "map2_03l"
            or victory_summary.get("candidateId") != candidate_id
            or victory_summary.get("blockId") != block_id
            or victory_summary.get("battleBackground") != background
            or victory_summary.get("rewardGranted") is not True
            or ((victory_summary.get("progressEvent") or {}).get("kind")) != "battle-victory"
            or ((victory_summary.get("progressEvent") or {}).get("id")) != block_id
            or victory_auto_save.get("saved") is not True
            or victory_auto_save.get("source") != "battle-victory"
            or victory_auto_save.get("battleId") != block_id
            or victory_auto_save.get("battleBackground") != background
            or reward_effect.get("source") != "battle-victory"
            or reward_effect.get("battleBackground") != background
            or reward_effect.get("enemyName") != EXPECTED_OBJECT_LINKED_DIALOGUE_BATTLE["enemyName"]
            or reward_effect.get("browserBattleRewardFeedbackImplemented") is not True
            or reward_effect.get("battleRewardSound") != "victory"
            or reward_effect.get("battleRewardSoundSrc") != "../extract_wlk/12.wav"
            or reward_effect.get("battleRewardSoundPlayed") is not True
            or reward_effect_render.get("battleRewardSound") != "victory"
            or reward_effect_render.get("battleRewardSoundSrc") != "../extract_wlk/12.wav"
            or reward_effect_render.get("battleRewardSoundPlayed") is not True
            or not item_text_provenance_matches(progress_detail, EXPECTED_OBJECT_LINKED_DIALOGUE_BATTLE["enemyDropKey"])
            or not item_text_provenance_matches(item_drop, EXPECTED_OBJECT_LINKED_DIALOGUE_BATTLE["enemyDropKey"])
            or not item_text_provenance_matches(victory_auto_save, EXPECTED_OBJECT_LINKED_DIALOGUE_BATTLE["enemyDropKey"])
            or not item_text_provenance_matches(reward_effect.get("itemDrop") or {}, EXPECTED_OBJECT_LINKED_DIALOGUE_BATTLE["enemyDropKey"])
            or not item_text_provenance_matches(reward_effect_render, EXPECTED_OBJECT_LINKED_DIALOGUE_BATTLE["enemyDropKey"])
            or step_counts.get("battle-victory") != index + 2
            or step_counts.get("battle-start") != index + 2
            or next((item for item in step.get("runtimeItems") or [] if item.get("key") == EXPECTED_OBJECT_LINKED_DIALOGUE_BATTLE["enemyDropKey"]), {}).get("count") != index + 2
        ):
            raise WebDriverError(f"candidate object linked battle chain step is incomplete: {step!r}")
        if next_candidate_id:
            if not isinstance(link_after, dict) or link_after.get("candidateId") != next_candidate_id:
                raise WebDriverError(f"candidate object linked battle chain did not advance to next link: {step!r}")
        elif link_after is not None:
            raise WebDriverError(f"candidate object linked battle chain still has a pending link: {step!r}")
    if (
        state.get("scene") != "map"
        or state.get("map") != "map2_03l"
        or state.get("stepCount") != 2
        or progress_counts.get("object-inspect") != 1
        or progress_counts.get("dialogue-candidate") != 3
        or progress_counts.get("dialogue-complete") != 3
        or progress_counts.get("dialogue-battle-link") != 3
        or progress_counts.get("battle-start") != 3
        or progress_counts.get("battle-victory") != 3
        or saved_counts.get("battle-victory") != 3
        or state.get("linkAfter") is not None
        or battle_completion.get("completed") is not True
        or battle_completion.get("completedCount") != 1
        or battle_completion.get("id") != "event-dialogue-block-004"
        or battle_completion.get("battleBackground") != "btl_l1"
        or objective.get("title") != "후보 전투 완료 btl_l1"
        or objective.get("nextAction") != "완료 알림 확인"
        or objective.get("source") != "prototype-battle-completion"
        or action_prompt.get("kind") != "battle-candidate"
        or action_prompt.get("text") != "Enter -> 전투 완료 btl_l1"
        or action_prompt.get("completed") is not True
        or action_prompt.get("blockId") != "event-dialogue-block-004"
        or action_prompt.get("battleBackground") != "btl_l1"
        or final_prompt.get("text") != "Enter -> 전투 완료 btl_l1"
        or state.get("completionNoticeActionResult") is not True
        or completion_notice.get("blockId") != "battle-complete:event-dialogue-block-004"
        or completion_notice_dialogue.get("blockId") != "battle-complete:event-dialogue-block-004"
        or completion_notice_dialogue.get("lineCount", 0) < 4
        or "전투 완료 btl_l1" not in completion_notice_lines
        or "event-dialogue-block-004" not in completion_notice_lines
        or not any("전투 보상 소지금 114" in str(line) for line in completion_notice_lines)
        or not any("전투 드롭 해독초 3" in str(line) for line in completion_notice_lines)
        or len(completion_feedback_log) < 1
        or not any(
            isinstance(entry, dict)
            and entry.get("source") == "battle-completion-feedback"
            and entry.get("text") == "전투 완료 btl_l1"
            and entry.get("blockId") == "battle-complete:event-dialogue-block-004"
            and entry.get("battleBackground") == "btl_l1"
            and entry.get("durationMs") == 1100
            and entry.get("battleCompletionSound") == "menuConfirm"
            and entry.get("battleCompletionSoundSrc") == "../extract_wlk/04.wav"
            and entry.get("battleCompletionSoundPlayed") is True
            and entry.get("browserBattleCompletionFeedbackImplemented") is True
            for entry in completion_feedback_log
        )
        or not any(
            isinstance(entry, dict)
            and entry.get("source") == "battle-completion-feedback"
            and entry.get("text") == "전투 완료 btl_l1"
            and entry.get("battleCompletionSound") == "menuConfirm"
            and entry.get("battleCompletionSoundSrc") == "../extract_wlk/04.wav"
            and entry.get("battleCompletionSoundPlayed") is True
            and entry.get("browserBattleCompletionFeedbackImplemented") is True
            for entry in completion_feedback_render
        )
        or completion_feedback_last.get("text") != "전투 완료 btl_l1"
        or completion_feedback_last.get("battleCompletionSound") != "menuConfirm"
        or completion_feedback_last.get("battleCompletionSoundSrc") != "../extract_wlk/04.wav"
        or completion_feedback_last.get("battleCompletionSoundPlayed") is not True
        or completion_feedback_last_render.get("text") != "전투 완료 btl_l1"
        or completion_feedback_last_render.get("battleCompletionSound") != "menuConfirm"
        or completion_feedback_last_render.get("battleCompletionSoundSrc") != "../extract_wlk/04.wav"
        or completion_feedback_last_render.get("battleCompletionSoundPlayed") is not True
        or state.get("completionNoticeDuplicateProgress") is not True
        or completion_notice_counts_after.get("battle-victory") != 3
        or next((item for item in state.get("runtimeItems") or [] if item.get("key") == EXPECTED_OBJECT_LINKED_DIALOGUE_BATTLE["enemyDropKey"]), {}).get("count") != 3
        or state.get("battleButtonText") != "전투 완료 btl_l1"
        or state.get("originalEventDrivenBattleEntry") is not False
        or state.get("originalStoryFlagRuntimeImplemented") is not False
    ):
        raise WebDriverError(f"candidate object linked dialogue battle chain completion is incomplete: {state!r}")


def write_report(report: dict) -> None:
    out_dir = ROOT / "out"
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "candidate_event_object_browser_smoke.json").write_text(
        json.dumps(report, ensure_ascii=False, separators=(",", ":")) + "\n",
        encoding="utf-8",
    )
    lines = [
        "# Candidate Event Object Browser Smoke",
        "",
        f"- status: `{report.get('status')}`",
        f"- passive render: `{report.get('passiveRender')}`",
        f"- object candidate menu: `{report.get('objectCandidateMenu')}`",
        f"- descriptor field object: `{report.get('descriptorFieldObject')}`",
        f"- scene-linked z objects: `{report.get('sceneLinkedZObjects')}`",
        f"- pointer inspect: `{report.get('pointerInspect')}`",
        f"- facing inspect: `{report.get('facingInspect')}`",
        f"- object collision: `{report.get('objectCollision')}`",
        f"- object collision overlay: `{report.get('objectCollisionOverlay')}`",
        f"- object collision disabled: `{report.get('objectCollisionDisabled')}`",
        f"- button inspect: `{report.get('buttonInspect')}`",
        f"- linked dialogue follow-up: `{report.get('linkedDialogueFollowup')}`",
        f"- linked dialogue completion: `{report.get('linkedDialogueCompletion')}`",
        f"- linked dialogue all-complete: `{report.get('linkedDialogueAllComplete')}`",
        f"- linked dialogue restore: `{report.get('linkedDialogueRestore')}`",
        f"- title continue restore: `{report.get('titleContinueRestore')}`",
        f"- linked dialogue battle action: `{report.get('linkedDialogueBattleAction')}`",
        f"- linked dialogue battle victory: `{report.get('linkedDialogueBattleVictory')}`",
        f"- linked dialogue battle chain: `{report.get('linkedDialogueBattleChain')}`",
        "",
    ]


def verify_browser(base: str) -> 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 = ROOT / "out" / "candidate_event_object_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["value"]["sessionId"])
            request_json(
                port,
                "POST",
                f"/session/{session_id}/window/rect",
                {"x": 0, "y": 0, "width": 390, "height": 844},
                timeout=8,
            )

            menu_url = load_map(base, port, session_id, {"map": "map2_03l", "startTile": "11,11"})
            execute_js(port, session_id, object_candidate_menu_selection_script(), timeout=3)
            object_menu_state = wait_for_object_candidate_menu_state(port, session_id)
            verify_object_candidate_menu_state(object_menu_state)
            execute_js(port, session_id, reset_runtime_progress_script(), timeout=3)

            descriptor_field_object_url = load_map(base, port, session_id, {"map": "map8_32q", "startTile": "11,11"})
            execute_js(port, session_id, descriptor_field_object_script(), timeout=3)
            descriptor_field_object = wait_for_descriptor_field_object_state(port, session_id)
            verify_descriptor_field_object_state(descriptor_field_object)
            descriptor_field_object_checksum = int(execute_js(port, session_id, canvas_checksum_script(), timeout=3))
            if descriptor_field_object_checksum == 0:
                raise WebDriverError("descriptor field object smoke rendered a blank canvas")

            scene_linked_z_object_urls = []
            scene_linked_z_object_states = []
            for expected in EXPECTED_SCENE_LINKED_Z_OBJECTS:
                anchor = expected.get("anchor") or {"x": 11, "y": 11}
                scene_linked_z_object_url = load_map(
                    base,
                    port,
                    session_id,
                    {"map": expected["map"], "startTile": f"{anchor['x']},{anchor['y']}"},
                )
                execute_js(port, session_id, scene_linked_z_object_script(expected), timeout=3)
                scene_linked_z_object = wait_for_scene_linked_z_object_state(port, session_id, expected)
                verify_scene_linked_z_object_state(scene_linked_z_object, expected)
                scene_linked_z_object_checksum = int(execute_js(port, session_id, canvas_checksum_script(), timeout=3))
                if scene_linked_z_object_checksum == 0:
                    raise WebDriverError(
                        f"scene-linked z object smoke rendered a blank canvas: {expected.get('map')}"
                    )
                scene_linked_z_object["checksum"] = scene_linked_z_object_checksum
                scene_linked_z_object_urls.append(scene_linked_z_object_url)
                scene_linked_z_object_states.append(scene_linked_z_object)
            execute_js(port, session_id, reset_runtime_progress_script(), timeout=3)

            pointer_url = load_map(base, port, session_id, {"map": "map2_03l", "startTile": "11,11"})
            passive_render = wait_for_passive_object_render_state(port, session_id)
            verify_passive_object_render(passive_render)
            pointer_state, pointer_checksum = verify_pointer_case(port, session_id)

            facing_url = load_map(base, port, session_id, {"map": "map2_03l", "startTile": "11,12"})
            facing_state, facing_checksum = verify_facing_action_case(port, session_id)

            collision_url = load_map(base, port, session_id, {"map": "map2_03l", "startTile": "11,12"})
            object_collision = verify_object_collision_case(port, session_id)

            collision_overlay_url = load_map(
                base,
                port,
                session_id,
                {"map": "map2_03l", "startTile": "11,12", "collision": "1"},
            )
            object_collision_overlay, object_collision_overlay_checksum = verify_object_collision_overlay_case(port, session_id)

            collision_disabled_url = load_map(
                base,
                port,
                session_id,
                {"map": "map2_03l", "startTile": "11,12", "objectCollision": "0"},
            )
            object_collision_disabled = verify_object_collision_disabled_case(port, session_id)

            button_url = load_map(base, port, session_id, {"map": "map2_03l", "startTile": "11,11"})
            (
                button,
                button_state,
                button_checksum,
                linked_followup,
                linked_completion,
                linked_all_completion,
            ) = verify_button_case(port, session_id)

            restore_url = load_map(base, port, session_id, {"map": "map2_03l", "startTile": "11,11"})
            execute_js(port, session_id, start_linked_dialogue_restore_script(), timeout=8)
            linked_restore = wait_for_linked_dialogue_restore_state(port, session_id)
            verify_linked_dialogue_restore(linked_restore)

            title_url = urljoin(base.rstrip("/") + "/", "/web/game.html?game=1")
            request_json(port, "POST", f"/session/{session_id}/url", {"url": title_url}, timeout=30)
            wait_for_page(port, session_id)
            continue_title = wait_for_continue_ready_title(port, session_id)
            if not any("이어하기 map2_03l 11,11" in str(label) for label in (continue_title.get("titleMenuLabels") or [])):
                raise WebDriverError(f"title continue row did not summarize object quick save: {continue_title!r}")
            title_continue_click = execute_js(port, session_id, click_title_continue_script(), timeout=3)
            if not isinstance(title_continue_click, dict) or title_continue_click.get("ok") is not True:
                raise WebDriverError(f"title continue button was not usable: {title_continue_click!r}")
            continued_object_map = wait_for_continued_object_map(port, session_id)
            execute_js(port, session_id, start_title_continue_restore_script(), timeout=8)
            title_continue_restore = wait_for_title_continue_restore_state(port, session_id)
            verify_title_continue_restore(title_continue_restore)

            action_url = urljoin(base.rstrip("/") + "/", "/web/game.html?map=map2_03l&startTile=11%2C11")
            execute_js(port, session_id, linked_dialogue_battle_action_script(), timeout=3)
            linked_battle_action = wait_for_linked_dialogue_battle_action_state(port, session_id)
            verify_linked_dialogue_battle_action(linked_battle_action)
            linked_battle_action_checksum = int(execute_js(port, session_id, canvas_checksum_script(), timeout=3))
            if linked_battle_action_checksum == 0:
                raise WebDriverError("candidate object linked dialogue battle action rendered a blank canvas")
            execute_js(port, session_id, linked_dialogue_battle_victory_script(), timeout=3)
            linked_battle_victory = wait_for_linked_dialogue_battle_victory_state(port, session_id)
            verify_linked_dialogue_battle_victory(linked_battle_victory)
            linked_battle_victory_checksum = int(execute_js(port, session_id, canvas_checksum_script(), timeout=3))
            if linked_battle_victory_checksum == 0:
                raise WebDriverError("candidate object linked dialogue battle victory rendered a blank canvas")
            execute_js(port, session_id, linked_dialogue_battle_chain_completion_script(), timeout=3)
            linked_battle_chain = wait_for_linked_dialogue_battle_chain_completion_state(port, session_id)
            verify_linked_dialogue_battle_chain_completion(linked_battle_chain)
            linked_battle_chain_checksum = int(execute_js(port, session_id, canvas_checksum_script(), timeout=3))
            if linked_battle_chain_checksum == 0:
                raise WebDriverError("candidate object linked dialogue battle chain completion rendered a blank canvas")

            button_assets = ",".join((button_state.get("inspect") or {}).get("objectAssets") or [])
            pointer_assets = ",".join((pointer_state.get("inspect") or {}).get("objectAssets") or [])
            facing_assets = ",".join((facing_state.get("inspect") or {}).get("objectAssets") or [])
            button_dialogue = ((button_state.get("inspect") or {}).get("linkedDialogue") or {})
            pointer_dialogue = ((pointer_state.get("inspect") or {}).get("linkedDialogue") or {})
            facing_dialogue = ((facing_state.get("inspect") or {}).get("linkedDialogue") or {})
            button_dialogue_blocks = ",".join(button_dialogue.get("blockIds") or [])
            pointer_dialogue_blocks = ",".join(pointer_dialogue.get("blockIds") or [])
            facing_dialogue_blocks = ",".join(facing_dialogue.get("blockIds") or [])
            passive_render_detail = passive_render.get("render") or {}
            title_continue_label = next(
                (str(label) for label in (continue_title.get("titleMenuLabels") or []) if str(label).startswith("이어하기")),
                "",
            )
            title_continue_map = title_continue_restore.get("titleContinueMap") or {}
            title_continue_progress_counts = ((title_continue_restore.get("progress") or {}).get("counts") or {})
            linked_all_vm_replays = [
                linked_dialogue_vm_replay_for(linked_all_completion.get("progressVmReplays") or [], block_id)
                for block_id in [
                    "event-dialogue-block-004",
                    "event-dialogue-block-032",
                    "event-dialogue-block-041",
                ]
            ]
            report = {
                "status": "passed",
                "base": base,
                "objectMenuUrl": menu_url,
                "descriptorFieldObjectUrl": descriptor_field_object_url,
                "sceneLinkedZObjectUrls": scene_linked_z_object_urls,
                "pointerUrl": pointer_url,
                "facingUrl": facing_url,
                "collisionUrl": collision_url,
                "collisionOverlayUrl": collision_overlay_url,
                "collisionDisabledUrl": collision_disabled_url,
                "buttonUrl": button_url,
                "restoreUrl": restore_url,
                "titleUrl": title_url,
                "actionUrl": action_url,
                "continueTitle": continue_title,
                "titleContinueClick": title_continue_click,
                "continuedObjectMap": continued_object_map,
                "passiveRender": (
                    f"map={passive_render.get('map')} "
                    f"button={passive_render.get('objectButtonText')} "
                    f"passiveVisible={passive_render_detail.get('passiveVisible')} "
                    f"prototype={passive_render_detail.get('prototypeVisible')} "
                    f"annotated={passive_render_detail.get('annotated')} "
                    f"candidates={passive_render_detail.get('candidateCount')} "
                    f"loaded={passive_render_detail.get('loadedCount')} "
                    f"rendered={passive_render_detail.get('renderedCount')}"
                ),
                "objectCandidateMenu": (
                    f"menuCount={(object_menu_state.get('before') or {}).get('count')} "
                    f"selected={((object_menu_state.get('selection') or {}).get('key') or '')} "
                    f"scope={(object_menu_state.get('inspect') or {}).get('scope')} "
                    f"active={object_menu_state.get('activeId')} "
                    f"assets={','.join((object_menu_state.get('inspect') or {}).get('objectAssets') or [])} "
                    f"selectionSource={(object_menu_state.get('inspect') or {}).get('selectionSource')} "
                    f"commandResult={object_menu_state.get('commandResult')} "
                    f"afterMenuOpen={object_menu_state.get('afterMenuOpen')} "
                    f"inspectFeedback={((object_menu_state.get('inspectFeedbackLast') or {}).get('source') or '')}:"
                    f"{((object_menu_state.get('inspectFeedbackLast') or {}).get('text') or '')} "
                    f"inspectSound={((object_menu_state.get('inspectFeedbackLast') or {}).get('inspectSound') or '')}:"
                    f"{((object_menu_state.get('inspectFeedbackLast') or {}).get('inspectSoundSrc') or '')}:"
                    f"{((object_menu_state.get('inspectFeedbackLast') or {}).get('inspectSoundPlayed'))} "
                    f"linkedDialogueCount={((object_menu_state.get('inspect') or {}).get('linkedDialogue') or {}).get('count')} "
                    f"originalLinkedDialogueRuntimeImplemented="
                    f"{(object_menu_state.get('inspect') or {}).get('originalLinkedDialogueRuntimeImplemented')} "
                    f"originalEventObjectRuntimeImplemented="
                    f"{(object_menu_state.get('inspect') or {}).get('originalEventObjectRuntimeImplemented')} "
                    f"originalStoryFlagRuntimeImplemented="
                    f"{(object_menu_state.get('selection') or {}).get('originalStoryFlagRuntimeImplemented')}"
                ),
                "descriptorFieldObject": (
                    f"map={descriptor_field_object.get('map')} "
                    f"descriptorTable={descriptor_field_object.get('expectedDescriptorTable')} "
                    f"descriptorRow={descriptor_field_object.get('expectedDescriptorRow')} "
                    f"asset={descriptor_field_object.get('expectedDescriptorAsset')} "
                    f"runtimeSurface={descriptor_field_object.get('expectedRuntimeSurface')} "
                    f"active={descriptor_field_object.get('activeId')} "
                    f"button={descriptor_field_object.get('objectButtonText')} "
                    f"passiveVisible={(descriptor_field_object.get('beforeRender') or {}).get('passiveVisible')} "
                    f"candidates={(descriptor_field_object.get('beforeRender') or {}).get('candidateCount')} "
                    f"loaded={(descriptor_field_object.get('beforeRender') or {}).get('loadedCount')} "
                    f"rendered={(descriptor_field_object.get('beforeRender') or {}).get('renderedCount')} "
                    f"anchors={(descriptor_field_object.get('beforeRender') or {}).get('collisionAnchorCount')} "
                    f"scope={(descriptor_field_object.get('inspect') or {}).get('scope')} "
                    f"assets={','.join((descriptor_field_object.get('inspect') or {}).get('objectAssets') or [])} "
                    f"sceneIds={','.join((descriptor_field_object.get('inspect') or {}).get('sceneIds') or [])} "
                    f"inspectFeedback={((descriptor_field_object.get('inspectFeedback') or {}).get('source') or '')}:"
                    f"{((descriptor_field_object.get('inspectFeedback') or {}).get('text') or '')} "
                    f"inspectSound={((descriptor_field_object.get('inspectFeedback') or {}).get('inspectSound') or '')}:"
                    f"{((descriptor_field_object.get('inspectFeedback') or {}).get('inspectSoundSrc') or '')}:"
                    f"{((descriptor_field_object.get('inspectFeedback') or {}).get('inspectSoundPlayed'))} "
                    f"originalEventObjectRuntimeImplemented="
                    f"{descriptor_field_object.get('originalEventObjectRuntimeImplemented')} "
                    f"originalStoryFlagRuntimeImplemented="
                    f"{descriptor_field_object.get('originalStoryFlagRuntimeImplemented')} "
                    f"checksum={descriptor_field_object_checksum}"
                ),
                "sceneLinkedZObjects": scene_linked_z_object_report(scene_linked_z_object_states),
                "pointerInspect": (
                    f"{pointer_state.get('activeId')} scope={(pointer_state.get('inspect') or {}).get('scope')} "
                    f"hoverCursor={pointer_state.get('hoverCursor')} "
                    f"hoverAsset={(pointer_state.get('hoverRender') or {}).get('hoverAssetKey')} "
                    f"hit={((pointer_state.get('pointer') or {}).get('hit') or {}).get('assetKey')} "
                    f"assets={pointer_assets} prototype={pointer_state.get('showEventObjectPrototypes')} "
                    f"dialogueLinks={pointer_dialogue.get('count')} dialogueScope={pointer_dialogue.get('scope')} "
                    f"dialogueBlocks={pointer_dialogue_blocks} "
                    f"canvasPointer=True checksum={pointer_checksum}"
                ),
                "facingInspect": (
                    f"{facing_state.get('activeId')} scope={(facing_state.get('inspect') or {}).get('scope')} "
                    f"prompt={(facing_state.get('promptBefore') or {}).get('text')} "
                    f"actionHighlight={(facing_state.get('promptRender') or {}).get('actionAssetKey')} "
                    f"assets={facing_assets} prototype={facing_state.get('showEventObjectPrototypes')} "
                    f"dialogueLinks={facing_dialogue.get('count')} dialogueScope={facing_dialogue.get('scope')} "
                    f"dialogueBlocks={facing_dialogue_blocks} "
                    f"checksum={facing_checksum}"
                ),
                "objectCollision": (
                    f"objectCollision={object_collision.get('map')} "
                    f"blocked={(object_collision.get('collision') or {}).get('blocked')} "
                    f"anchors={(object_collision.get('collision') or {}).get('anchorCount')} "
                    f"target={(object_collision.get('targetTile') or {}).get('x')},{(object_collision.get('targetTile') or {}).get('y')} "
                    f"primary={object_collision.get('primary')} "
                    f"directCanMove={object_collision.get('directCanMove')} "
                    f"blockedAsset={(object_collision.get('collision') or {}).get('blockedAssetKey')} "
                    f"prompt={(object_collision.get('actionPrompt') or {}).get('text')} "
                    f"objectCollisionEnabled={object_collision.get('objectCollisionEnabled')} "
                    f"renderAnchors={((object_collision.get('before') or {}).get('render') or {}).get('collisionAnchorCount')} "
                    f"originalEventObjectRuntimeImplemented="
                    f"{(object_collision.get('collision') or {}).get('originalEventObjectRuntimeImplemented')}"
                ),
                "objectCollisionOverlay": (
                    f"collisionOverlay={object_collision_overlay.get('map')} "
                    f"enabled={(object_collision_overlay.get('overlay') or {}).get('enabled')} "
                    f"objectTiles={(object_collision_overlay.get('overlay') or {}).get('objectTileCount')} "
                    f"objectAnchors={(object_collision_overlay.get('overlay') or {}).get('objectAnchorCount')} "
                    f"visible={(object_collision_overlay.get('overlay') or {}).get('visibleObjectTileCount')} "
                    f"tile={((object_collision_overlay.get('tiles') or [{}])[0]).get('x')},"
                    f"{((object_collision_overlay.get('tiles') or [{}])[0]).get('y')} "
                    f"assets={','.join(((object_collision_overlay.get('tiles') or [{}])[0]).get('assets') or [])} "
                    f"objectCollisionEnabled={object_collision_overlay.get('objectCollisionEnabled')} "
                    f"originalEventObjectRuntimeImplemented="
                    f"{(object_collision_overlay.get('overlay') or {}).get('originalEventObjectRuntimeImplemented')} "
                    f"checksum={object_collision_overlay_checksum}"
                ),
                "objectCollisionDisabled": (
                    f"objectCollisionDisabled={object_collision_disabled.get('map')} "
                    f"enabled={object_collision_disabled.get('objectCollisionEnabled')} "
                    f"directCanMove={object_collision_disabled.get('directCanMove')} "
                    f"primaryTile={(object_collision_disabled.get('primary') or {}).get('nextTile', {}).get('x')},"
                    f"{(object_collision_disabled.get('primary') or {}).get('nextTile', {}).get('y')} "
                    f"anchors={(object_collision_disabled.get('collision') or {}).get('anchorCount')} "
                    f"blocked={(object_collision_disabled.get('collision') or {}).get('blocked')} "
                    f"prompt={(object_collision_disabled.get('actionPrompt') or {}).get('text')} "
                    f"renderAnchors={((object_collision_disabled.get('before') or {}).get('render') or {}).get('collisionAnchorCount')} "
                    f"originalEventObjectRuntimeImplemented="
                    f"{(object_collision_disabled.get('collision') or {}).get('originalEventObjectRuntimeImplemented')}"
                ),
                "buttonInspect": (
                    f"{button_state.get('activeId')} scope={(button_state.get('inspect') or {}).get('scope')} "
                    f"assets={button_assets} prototype={button_state.get('showEventObjectPrototypes')} "
                    f"dialogueLinks={button_dialogue.get('count')} dialogueScope={button_dialogue.get('scope')} "
                    f"dialogueBlocks={button_dialogue_blocks} "
                    f"inspectFeedback={((button_state.get('inspectFeedbackLast') or {}).get('source') or '')}:"
                    f"{((button_state.get('inspectFeedbackLast') or {}).get('text') or '')} "
                    f"inspectFeedbackRender={bool(button_state.get('inspectFeedbackRender') or [])} "
                    f"inspectSound={((button_state.get('inspectFeedbackLast') or {}).get('inspectSound') or '')} "
                    f"inspectSoundSrc={((button_state.get('inspectFeedbackLast') or {}).get('inspectSoundSrc') or '')} "
                    f"inspectSoundPlayed={((button_state.get('inspectFeedbackLast') or {}).get('inspectSoundPlayed'))} "
                    f"checksum={button_checksum}"
                ),
                "linkedDialogueFollowup": (
                    f"{linked_followup.get('fromId')} -> {linked_followup.get('activeId')} "
                    f"line={linked_followup.get('activeLine')} "
                    f"dialogueCandidateCount={(linked_followup.get('progress') or {}).get('counts', {}).get('dialogue-candidate')} "
                    f"originalLinkedDialogueRuntimeImplemented="
                    f"{(linked_followup.get('linkedStart') or {}).get('originalLinkedDialogueRuntimeImplemented')} "
                    f"{linked_dialogue_vm_report([linked_followup.get('vmReplay') or {}])}"
                ),
                "linkedDialogueCompletion": (
                    f"{linked_completion.get('startId')} completed "
                    f"button={linked_completion.get('buttonText')} "
                    f"next={linked_completion.get('nextCandidateId')} "
                    f"dialogueCompleteCount={(linked_completion.get('progress') or {}).get('counts', {}).get('dialogue-complete')} "
                    f"savedDialogueCompleteCount={(linked_completion.get('savedProgress') or {}).get('counts', {}).get('dialogue-complete')} "
                    f"autoSaved={(linked_completion.get('autoSave') or {}).get('saved')} "
                    f"autoSource={(linked_completion.get('autoSave') or {}).get('source')} "
                    f"prompt={(linked_completion.get('actionPrompt') or {}).get('text')} "
                    f"{linked_dialogue_vm_report([linked_completion.get('vmReplay') or {}])}"
                ),
                "linkedDialogueAllComplete": (
                    f"completed={','.join(linked_all_completion.get('completedIds') or [])} "
                    f"button={linked_all_completion.get('buttonText')} "
                    f"notice={(linked_all_completion.get('completionNotice') or {}).get('blockId')} "
                    f"dialogueCandidateCount={(linked_all_completion.get('progress') or {}).get('counts', {}).get('dialogue-candidate')} "
                    f"dialogueCompleteCount={(linked_all_completion.get('progress') or {}).get('counts', {}).get('dialogue-complete')} "
                    f"savedDialogueCompleteCount={(linked_all_completion.get('savedProgress') or {}).get('counts', {}).get('dialogue-complete')} "
                    f"autoSaved={(linked_all_completion.get('autoSave') or {}).get('saved')} "
                    f"autoSource={(linked_all_completion.get('autoSave') or {}).get('source')} "
                    f"{linked_dialogue_vm_report(linked_all_vm_replays)}"
                ),
                "linkedDialogueRestore": (
                    f"loaded={linked_restore.get('loaded')} "
                    f"button={linked_restore.get('dialogueButtonText')} "
                    f"objectButton={linked_restore.get('objectButtonText')} "
                    f"notice={(linked_restore.get('completionNotice') or {}).get('blockId')} "
                    f"dialogueButtonResult={linked_restore.get('dialogueButtonResult')} "
                    f"menuObject={linked_restore.get('menuObjectName')} "
                    f"directMenuIndex={linked_restore.get('menuDirectObjectIndex')} "
                    f"menuMode={linked_restore.get('menuObjectModeAfterOpen')} "
                    f"menuCandidateCount={linked_restore.get('menuObjectCandidateCount')} "
                    f"dialogueCompleteCount={(linked_restore.get('progress') or {}).get('counts', {}).get('dialogue-complete')} "
                    f"objectInspectCount={(linked_restore.get('progress') or {}).get('counts', {}).get('object-inspect')} "
                    f"prompt={(linked_restore.get('actionPrompt') or {}).get('text')}"
                ),
                "titleContinueRestore": (
                    f"titleContinue={title_continue_restore.get('titleContinue')} "
                    f"label={title_continue_label} "
                    f"map={title_continue_restore.get('map')}@{(title_continue_restore.get('foot') or {}).get('x')},{(title_continue_restore.get('foot') or {}).get('y')} "
                    f"quickLoadText={title_continue_map.get('quickLoadText')} "
                    f"button={title_continue_restore.get('dialogueButtonText')} "
                    f"objectButton={title_continue_restore.get('objectButtonText')} "
                    f"dialogueButtonResult={title_continue_restore.get('dialogueButtonResult')} "
                    f"menuObject={title_continue_restore.get('menuObjectName')} "
                    f"directMenuIndex={title_continue_restore.get('menuDirectObjectIndex')} "
                    f"menuMode={title_continue_restore.get('menuObjectModeAfterOpen')} "
                    f"menuCandidateCount={title_continue_restore.get('menuObjectCandidateCount')} "
                    f"dialogueCompleteCount={title_continue_progress_counts.get('dialogue-complete')} "
                    f"objectInspectCount={title_continue_progress_counts.get('object-inspect')} "
                    f"prompt={(title_continue_restore.get('actionPrompt') or {}).get('text')}"
                ),
                "linkedDialogueBattleAction": (
                    f"{(linked_battle_action.get('linkBefore') or {}).get('candidateId')} "
                    f"prompt={(linked_battle_action.get('actionPromptBefore') or {}).get('text')} "
                    f"actionResult={linked_battle_action.get('actionResult')} "
                    f"linkAction={(linked_battle_action.get('dialogueBattleLinkAction') or {}).get('action')} "
                    f"scene={linked_battle_action.get('scene')} "
                    f"battleBackground={(linked_battle_action.get('summary') or {}).get('battleBackground')} "
                    f"enemyName={(linked_battle_action.get('summary') or {}).get('enemyName')} "
                    f"enemySprite={(linked_battle_action.get('summary') or {}).get('enemySpriteCandidate')} "
                    f"enemyDropItemTextRef={(linked_battle_action.get('summary') or {}).get('enemyDropItemTextTableRefVaHex')} "
                    f"enemyDropItemTextVa={(linked_battle_action.get('summary') or {}).get('enemyDropItemTextTableTextVaHex')} "
                    f"battleStartFeedback={((linked_battle_action.get('battleStartFeedbackLast') or {}).get('source') or '')}:"
                    f"{((linked_battle_action.get('battleStartFeedbackLast') or {}).get('text') or '')} "
                    f"battleStartFeedbackRender={bool(linked_battle_action.get('battleStartFeedbackRender') or [])} "
                    f"battleStartFeedbackSound={((linked_battle_action.get('battleStartFeedbackLast') or {}).get('battleStartSound') or '')}:"
                    f"{((linked_battle_action.get('battleStartFeedbackLast') or {}).get('battleStartSoundSrc') or '')}:"
                    f"{((linked_battle_action.get('battleStartFeedbackLast') or {}).get('battleStartSoundPlayed'))} "
                    f"battleStartSound={((linked_battle_action.get('battleStartFeedbackLast') or {}).get('battleStartSound') or '')} "
                    f"battleStartSoundSrc={((linked_battle_action.get('battleStartFeedbackLast') or {}).get('battleStartSoundSrc') or '')} "
                    f"dialogueBattleLinkCount={(linked_battle_action.get('progress') or {}).get('counts', {}).get('dialogue-battle-link')} "
                    f"battleStartCount={(linked_battle_action.get('progress') or {}).get('counts', {}).get('battle-start')} "
                    f"checksum={linked_battle_action_checksum}"
                ),
                "linkedDialogueBattleVictory": (
                    f"{((linked_battle_victory.get('victorySummary') or {}).get('progressEvent') or {}).get('id')} "
                    f"attackResult={linked_battle_victory.get('attackResult')} "
                    f"finishResult={linked_battle_victory.get('finishResult')} "
                    f"scene={linked_battle_victory.get('scene')} "
                    f"battleBackground={(linked_battle_victory.get('victorySummary') or {}).get('battleBackground')} "
                    f"rewardGranted={(linked_battle_victory.get('victorySummary') or {}).get('rewardGranted')} "
                    f"autoSaved={(linked_battle_victory.get('victoryAutoSave') or {}).get('saved')} "
                    f"autoSource={(linked_battle_victory.get('victoryAutoSave') or {}).get('source')} "
                    f"battleVictoryCount={(linked_battle_victory.get('progress') or {}).get('counts', {}).get('battle-victory')} "
                    f"objective={(linked_battle_victory.get('objective') or {}).get('title')} "
                    f"prompt={(linked_battle_victory.get('actionPromptAfter') or {}).get('text')} "
                    f"nextLink={(linked_battle_victory.get('linkAfter') or {}).get('candidateId')} "
                    f"drop={((linked_battle_victory.get('victorySummary') or {}).get('itemDrop') or {}).get('itemName')} "
                    f"dropCount={((linked_battle_victory.get('victorySummary') or {}).get('itemDrop') or {}).get('countAfter')} "
                    f"dropItemTextRef={((linked_battle_victory.get('victorySummary') or {}).get('itemDrop') or {}).get('itemTextTableRefVaHex')} "
                    f"dropItemTextVa={((linked_battle_victory.get('victorySummary') or {}).get('itemDrop') or {}).get('itemTextTableTextVaHex')} "
                    f"rewardEffect={((linked_battle_victory.get('rewardEffect') or {}).get('source') or '')}:"
                    f"{((linked_battle_victory.get('rewardEffect') or {}).get('enemyName') or '')} "
                    f"rewardDropItemTextRef={((linked_battle_victory.get('rewardEffect') or {}).get('itemDrop') or {}).get('itemTextTableRefVaHex')} "
                    f"rewardRenderDropItemTextRef={(linked_battle_victory.get('rewardEffectRender') or {}).get('itemTextTableRefVaHex')} "
                    f"rewardEffectRender={bool(linked_battle_victory.get('rewardEffectRender') or {})} "
                    f"rewardEffectSound={((linked_battle_victory.get('rewardEffect') or {}).get('battleRewardSound') or '')}:"
                    f"{((linked_battle_victory.get('rewardEffect') or {}).get('battleRewardSoundSrc') or '')}:"
                    f"{((linked_battle_victory.get('rewardEffect') or {}).get('battleRewardSoundPlayed'))} "
                    f"checksum={linked_battle_victory_checksum}"
                ),
                "linkedDialogueBattleChain": (
                    f"steps={linked_battle_chain.get('stepCount')} "
                    f"order={','.join((step.get('before') or {}).get('candidateId') or '' for step in (linked_battle_chain.get('steps') or []))} "
                    f"scene={linked_battle_chain.get('scene')} "
                    f"battleStartCount={(linked_battle_chain.get('progress') or {}).get('counts', {}).get('battle-start')} "
                    f"battleVictoryCount={(linked_battle_chain.get('progress') or {}).get('counts', {}).get('battle-victory')} "
                    f"pendingLink={linked_battle_chain.get('linkAfter')} "
                    f"objective={(linked_battle_chain.get('objective') or {}).get('title')} "
                    f"prompt={(linked_battle_chain.get('actionPromptAfter') or {}).get('text')} "
                    f"dropCount={next((item.get('count') for item in (linked_battle_chain.get('runtimeItems') or []) if item.get('key') == EXPECTED_OBJECT_LINKED_DIALOGUE_BATTLE['enemyDropKey']), None)} "
                    f"stepDropItemTextRefs={','.join((((step.get('victorySummary') or {}).get('itemDrop') or {}).get('itemTextTableRefVaHex') or '') for step in (linked_battle_chain.get('steps') or []))} "
                    f"rewardDropItemTextRefs={','.join((((step.get('rewardEffect') or {}).get('itemDrop') or {}).get('itemTextTableRefVaHex') or '') for step in (linked_battle_chain.get('steps') or []))} "
                    f"rewardRenderDropItemTextRefs={','.join(((step.get('rewardEffectRender') or {}).get('itemTextTableRefVaHex') or '') for step in (linked_battle_chain.get('steps') or []))} "
                    f"rewardEffectSounds={','.join(((step.get('rewardEffect') or {}).get('battleRewardSound') or '') for step in (linked_battle_chain.get('steps') or []))} "
                    f"rewardEffectSoundSrcs={','.join(((step.get('rewardEffect') or {}).get('battleRewardSoundSrc') or '') for step in (linked_battle_chain.get('steps') or []))} "
                    f"rewardEffectSoundsPlayed={','.join(str((step.get('rewardEffect') or {}).get('battleRewardSoundPlayed')) for step in (linked_battle_chain.get('steps') or []))} "
                    f"autoSource={(((linked_battle_chain.get('steps') or [{}])[-1].get('victoryAutoSave') or {}).get('source')) if (linked_battle_chain.get('steps') or []) else None} "
                    f"noticeResult={linked_battle_chain.get('completionNoticeActionResult')} "
	                    f"notice={(linked_battle_chain.get('completionNotice') or {}).get('blockId')} "
	                    f"noticeActive={(linked_battle_chain.get('completionNoticeDialogue') or {}).get('blockId')} "
                    f"completionFeedback={((linked_battle_chain.get('battleCompletionFeedbackLast') or {}).get('source') or '')}:"
                    f"{((linked_battle_chain.get('battleCompletionFeedbackLast') or {}).get('text') or '')} "
                    f"completionFeedbackRender={bool(linked_battle_chain.get('battleCompletionFeedbackRender') or [])} "
                    f"completionFeedbackSound={((linked_battle_chain.get('battleCompletionFeedbackLast') or {}).get('battleCompletionSound') or '')}:"
                    f"{((linked_battle_chain.get('battleCompletionFeedbackLast') or {}).get('battleCompletionSoundSrc') or '')}:"
                    f"{((linked_battle_chain.get('battleCompletionFeedbackLast') or {}).get('battleCompletionSoundPlayed'))} "
	                    f"duplicateProgress={linked_battle_chain.get('completionNoticeDuplicateProgress')} "
	                    f"checksum={linked_battle_chain_checksum}"
	                ),
                "snapshots": {
                    "objectCandidateMenu": object_menu_state,
                    "descriptorFieldObject": descriptor_field_object,
                    "sceneLinkedZObjects": scene_linked_z_object_states,
                    "passiveRender": passive_render,
                    "pointerInspect": pointer_state,
                    "facingInspect": facing_state,
                    "objectCollision": object_collision,
                    "objectCollisionOverlay": object_collision_overlay,
                    "objectCollisionDisabled": object_collision_disabled,
                    "button": button,
                    "buttonInspect": button_state,
                    "linkedDialogueFollowup": linked_followup,
                    "linkedDialogueCompletion": linked_completion,
                    "linkedDialogueAllComplete": linked_all_completion,
                    "linkedDialogueRestore": linked_restore,
                    "continueTitle": continue_title,
                    "titleContinueClick": title_continue_click,
                    "continuedObjectMap": continued_object_map,
                    "titleContinueRestore": title_continue_restore,
                    "linkedDialogueBattleAction": linked_battle_action,
                    "linkedDialogueBattleVictory": linked_battle_victory,
                    "linkedDialogueBattleChain": linked_battle_chain,
                },
            }
            write_report(report)
            print(
                "ok candidate event object browser "
                f"menu={report['objectCandidateMenu']} "
                f"passive={report['passiveRender']} pointer={report['pointerInspect']} "
                f"facing={report['facingInspect']} "
                f"collision={report['objectCollision']} "
                f"overlay={report['objectCollisionOverlay']} "
                f"disabled={report['objectCollisionDisabled']} "
                f"button={report['buttonInspect']} "
                f"restore={report['linkedDialogueRestore']} "
                f"titleContinue={report['titleContinueRestore']} "
                f"battleAction={report['linkedDialogueBattleAction']} "
                f"battleVictory={report['linkedDialogueBattleVictory']} "
                f"battleChain={report['linkedDialogueBattleChain']} "
                f"sceneLinkedZObjects={report['sceneLinkedZObjects']}"
            )
        finally:
            if session_id:
                try:
                    request_json(port, "DELETE", f"/session/{session_id}", timeout=5)
                except Exception:
                    pass
            proc.terminate()
            try:
                proc.wait(timeout=5)
            except subprocess.TimeoutExpired:
                proc.kill()
                proc.wait(timeout=5)


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


if __name__ == "__main__":
    main()
