Files
Max BittkerandClaude Opus 5 e5fb1e9d07 Make the agent API self-describing and add regression CI (#43)
* Improve agent API docs and validation

* Drop the wiki search tool

Removes wiki/search.ts and its test. The index only covered names, aliases,
summaries and Sources lines, so it had worse recall than grep on body text
(e.g. "cow hide" missed skills/crafting.md), and normalize() stripped
underscores so map-square queries matched every neighbouring square
(m50_50 returned m50_53 NPCs). The computed mapSquareBounds were only
indexed as text, so coordinate lookups — the one thing grep can't do —
never worked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Regenerate the wiki from server/content

Adds the coordinate-samples column to NPC location tables and picks up
content drift the checked-in pages had missed (807 NPC, 117 shop, 1573 item
pages; 335 of them new). Examples: Aubury now stocks 1000 chaos/death runes
rather than 500, and shop rows point at "Vial of water"/"Bolts" instead of
the stale "Vial"/"Bolt" names.

Also stops all three generators from crashing on their final step. Each one
read wiki/README.md unconditionally, but that file has never existed, so the
index update threw ENOENT on every run. Skip it when absent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-24 17:52:51 -04:00

3.9 KiB

Dialogs & UI

Successful patterns for handling game dialogs and interfaces.

Dismissing Level-Up Dialogs

Level-up dialogs block all actions. Dismiss them immediately:

if (sdk.getState()?.dialog.isOpen) {
  await sdk.sendClickDialog(0);
  continue; // Skip rest of loop iteration
}

Dismiss at Arc Start

All BotActions methods automatically dismiss blocking UI before executing. Manual dismissal is only needed when using low-level sdk methods directly:

await bot.dismissBlockingUI();

Checking Dialog State

const dialog = sdk.getState()?.dialog;

// Is any dialog open?
if (dialog.isOpen) { ... }

// Is dialog waiting for input?
if (dialog.isOpen && !dialog.isWaiting) { ... }

// Get available options
for (const opt of dialog.options) {
    console.log(`${opt.index}: ${opt.text}`);
}

Navigating Multi-Step Dialogs

For NPC conversations with choices:

// Click through until specific option appears
for (let i = 0; i < 20; i++) {
  const s = sdk.getState();
  if (!s?.dialog.isOpen) {
    await new Promise((r) => setTimeout(r, 150));
    continue;
  }

  // Look for target option
  const targetOpt = s.dialog.options.find((o) => /yes/i.test(o.text));
  if (targetOpt) {
    await sdk.sendClickDialog(targetOpt.index);
    break;
  }

  // Otherwise click to continue
  await sdk.sendClickDialog(0);
  await new Promise((r) => setTimeout(r, 200));
}

Shop Interfaces

const state = sdk.getState();
if (!state) throw new Error('No world state');

// Check if shop is open
if (state.shop.isOpen) { ... }

// Check if any interface is open (bank, shop, etc.)
if (state.interface?.isOpen) { ... }

sendClickDialog Index Behavior

  • sendClickDialog(0) = special "continue" action (RESUME_PAUSEBUTTON)
  • sendClickDialog(1-5) = click actual option buttons (1-based indices)

Always use 0 as fallback for "Click here to continue" screens.

Al Kharid Toll Gate Pattern

Requires 10gp. Position west of gate: (3267, 3228).

const gate = sdk.getState()?.nearbyLocs.find((l) => /gate/i.test(l.name));
if (!gate) throw new Error('No gate nearby');
await sdk.sendInteractLoc(gate.x, gate.z, gate.id, 1);
await sdk.waitForTicks(2);

// Click through dialog: 0 = continue, or pick "Yes" when available
for (let i = 0; i < 10; i++) {
  const yesOpt = sdk
    .getState()
    ?.dialog?.options.find((o) => /yes/i.test(o.text));
  await sdk.sendClickDialog(yesOpt?.index ?? 0);
  await sdk.waitForTicks(1);
}

await bot.walkTo(3277, 3227); // Walk through to Al Kharid

Buying Kebabs from the Kebab Seller (Al Kharid)

Sells kebabs via dialog (not a shop interface). Location: (3273, 3180), spawns at (3272, 3182).

// Walk to kebab seller
await bot.walkTo(3273, 3180);

// Find the kebab seller
const seller = sdk
  .getState()
  ?.nearbyNpcs.find((n) => /kebab/i.test(n.name));
if (!seller) throw new Error('No kebab seller nearby');
const talkOpt = seller.optionsWithIndex.find((o) => /talk/i.test(o.text));
if (!talkOpt) throw new Error('Kebab seller has no Talk-to option');
await sdk.sendInteractNpc(seller.index, talkOpt.opIndex);
await new Promise((r) => setTimeout(r, 1000));

// Handle dialog to buy kebab (1gp each)
for (let i = 0; i < 15; i++) {
  const s = sdk.getState();
  if (!s?.dialog.isOpen) {
    await new Promise((r) => setTimeout(r, 200));
    continue;
  }
  const buyOpt = s.dialog.options.find((o) => /yes/i.test(o.text));
  if (buyOpt) {
    await sdk.sendClickDialog(buyOpt.index);
    break;
  }
  await sdk.sendClickDialog(0);
  await new Promise((r) => setTimeout(r, 300));
}

Kebabs cost 1gp and heal 1-19 HP (random). Good cheap food for training.

Detecting Stuck in Dialog

If your script makes no progress, check for stuck dialogs:

// In your main loop
if (sdk.getState()?.dialog.isOpen) {
  console.log("Dialog open, dismissing...");
  await sdk.sendClickDialog(0);
  continue;
}