Files
my-botty/tools/quest_plan.md
alexpolo1 c04c60ead7 release: v0.9.1 — production cleanup
- Restore FIRST-RUN.md onboarding guide
- Pin environment.yml to requirements.txt (was loose 24 packages)
- Remove debug screenshots from git (debug_*.png -> .gitignore)
- Remove .hermes/ dev plans from git (already in .gitignore)
- Archive botty_next/ test harness to alexpolo1/my-botty-tools
- Archive docs/legacy-go/ to alexpolo1/my-botty-tools
- Move 14 dev tools from repo root to tools/ directory
- Extend .gitignore to prevent dev artifacts re-entering root
- Promote stable branch with all bug fixes from main
- Bump version 0.8.4 -> 0.9.1
2026-08-08 09:59:31 +02:00

12 KiB

Botty Auto-Quest: Implementation Plan

1. Architecture

src/quest/
    __init__.py
    quest_manager.py      # State machine, quest sequencing, persistence
    quest_state.py         # JSON-based quest progress tracker
    quest_npc.py           # Generic NPC interaction utilities (talk to NPC, dialogue selection)
    quest_items.py         # Quest item detection and management
    quest_combat.py        # Combat helpers for quest-specific fights
    a1/
        __init__.py        # Act 1 quest runner
        q1a1_smith.py      # Q1: The Search for the Smith
        q1a2_cain.py        # Q2: Tools of the Trade
        q1a3_sacrifice.py  # Q3: Sacrifice
        q1a4_town_portal.py# Q4: The Summoner
        q1a5_skeleton_king.py # Q5: The Shepherd
        q1a6_andariel.py   # Q6: The Fallen Angel
    a2/
        __init__.py        # Act 2 quest runner
        q2a1_jerhyn.py     # Q1: Radament
        q2a2_atheistic.py  # Q2: The Horadric Staff
        q2a3_hephalon.py   # Q3: Tyrael's Breath
        q2a4_atalai.py     # Q4: Secrets
        q2a5_tarbedit.py   # Q5: The Summoner
        q2a6_nihlathak.py  # Q6: The Seven Tombs
        q2a7_duriel.py     # Q7: The Fallen Angel
    a3/
        __init__.py        # Act 3 quest runner
        q3a1_larzuk.py     # Q1: The Forgotten Tower
        q3a2_cain.py        # Q2: The Quest for the Horizon
        q3a3_kaelthas.py   # Q3: The Hellforge
        q3a4_hellgate.py   # Q4: The Hellgate
        q3a5_mephisto.py   # Q5: The Prime Evil
    a4/
        __init__.py        # Act 4 quest runner
        q4a1_izual.py      # Q1: The Fallen Angel
        q4a2_harumony.py   # Q2: The Fallen Angel
        q4a3_diablo.py     # Q3: The Prime Evil
    a5/
        __init__.py        # Act 5 quest runner
        q5a1_ancients.py   # Q1: The Fallen Angel
        q5a2_cain.py        # Q2: The Quest for the Horizon
        q5a3_baal.py       # Q3: The Prime Evil

2. Quest Manager Design

# quest/quest_manager.py

from enum import Enum
import json, os
from config import Config

class QuestStatus(Enum):
    PENDING = "pending"
    IN_PROGRESS = "in_progress"
    COMPLETED = "completed"
    SKIPPED = "skipped"

class QuestManager:
    _state_file = "config/quest_state.json"

    def __init__(self):
        self.state = self._load_state()
        self.char = None  # reference to IChar
        self.pather = None

    def _load_state(self):
        if os.path.exists(self._state_file):
            with open(self._state_file) as f:
                return json.load(f)
        return self._default_state()

    def _default_state(self):
        # All 25 quests tracked by (act, quest_number)
        return {
            "current_act": 1,
            "quests": {
                "1-1": QuestStatus.PENDING, "1-2": QuestStatus.PENDING,
                "1-3": QuestStatus.PENDING, "1-4": QuestStatus.PENDING,
                "1-5": QuestStatus.PENDING, "1-6": QuestStatus.PENDING,
                "2-1": QuestStatus.PENDING, "2-2": QuestStatus.PENDING,
                "2-3": QuestStatus.PENDING, "2-4": QuestStatus.PENDING,
                "2-5": QuestStatus.PENDING, "2-6": QuestStatus.PENDING,
                "2-7": QuestStatus.PENDING,
                "3-1": QuestStatus.PENDING, "3-2": QuestStatus.PENDING,
                "3-3": QuestStatus.PENDING, "3-4": QuestStatus.PENDING,
                "3-5": QuestStatus.PENDING,
                "4-1": QuestStatus.PENDING, "4-2": QuestStatus.PENDING,
                "4-3": QuestStatus.PENDING,
                "5-1": QuestStatus.PENDING, "5-2": QuestStatus.PENDING,
                "5-3": QuestStatus.PENDING,
            }
        }

    def save(self):
        with open(self._state_file, 'w') as f:
            json.dump(self.state, f, indent=2)

    def next_quest(self) -> tuple | None:
        """Returns (act, quest_num) of next pending quest, or None if all done."""
        current = self.state["current_act"]
        for qn in range(1, 8):  # max 7 quests per act
            key = f"{current}-{qn}"
            if key in self.state["quests"] and self.state["quests"][key] == QuestStatus.PENDING:
                return (current, qn)
        return None

    def mark_complete(self, act, qn):
        self.state["quests"][f"{act}-{qn}"] = QuestStatus.COMPLETED

    def is_act_complete(self, act):
        for qn in range(1, 8):
            key = f"{act}-{qn}"
            if key in self.state["quests"] and self.state["quests"][key] == QuestStatus.PENDING:
                return False
        return True

    def advance_to_next_act(self):
        """Called when all quests in current act are done."""
        current = self.state["current_act"]
        # ... handle act transition (talk to quest NPC to unlock next act)
        self.state["current_act"] = current + 1
        self.save()

    def run_next_quest(self):
        """Dispatches to the appropriate quest implementation."""
        nxt = self.next_quest()
        if not nxt:
            return
        act, qn = nxt
        self.state["quests"][f"{act}-{qn}"] = QuestStatus.IN_PROGRESS
        # Route to quest implementation
        # ... call quest module

3. Quest NPC Interaction

Every quest requires talking to NPCs. The existing npc_manager.py has talk_to_npc() which we extend.

# quest/quest_npc.py
# Utilities for NPC dialogue selection

from utils.custom_mouse import mouse
from utils.misc import wait
from template_finder import search_and_wait
from screen import grab

def talk_and_select(dialogue_choice: str, npc_name: str):
    """
    Talk to NPC and select a specific dialogue option.
    botty already detects NPC and opens dialogue. We need to click
    the specific dialogue button.
    """
    from npc_manager import talk_to_npc
    talk_to_npc(npc_name)
    wait(1)
    # D2R shows dialogue options in a box at bottom center.
    # Detect the text of each option using OCR and click the matching one.
    click_dialogue_option(dialogue_choice)

def click_dialogue_option(text: str):
    """
    Use OCR to read dialogue options and click the one containing the given text.
    """
    from d2r_image.ocr import image_to_text
    img = grab()
    roi = (300, 530, 680, 190)  # dialogue box area
    result = image_to_text(cut_roi(img, roi), psm=6)
    for line in result.text.split('\n'):
        if text.lower() in line.lower():
            # Click near the center of this text line
            # ... (use OCR bounding box to find click position)
            break

def check_quest_item_on_screen() -> bool:
    """Detect if a quest item tooltip is visible (gold item name)."""
    # Quest items have a gold/purple glow. Detect by color.
    img = grab()
    roi = (400, 400, 200, 200)  # quest item pickup area
    # Check for gold-colored pixels indicating a quest item
    ...

4. Quest Item Tracking

Quest items (keys, scrolls, weapons) need to be tracked. We extend the inventory system.

# quest/quest_items.py

QUEST_ITEMS = {
    "stone_of_jah": "Stone of Jah",
    "hephaestons_key": "Hephaston's Key",
    "tal_rashas_will": "Tal Rasha's Will",
    "keys_to_the_crypt": "Keys to the Crypt",
    "harumony": "Harumony",
    "ancients_battle_order": "Ancient's Battle Order",
    "horadric_cube": "Horadric Cube",
    "horadric_staff": "Horadric Staff",
    "amulet_of_the_vipers": "Amulet of the Vipers",
}

def has_quest_item(name: str) -> bool:
    """Check if quest item is in inventory."""
    # Use OCR to scan inventory for item name
    # OR check by item template matching (faster)
    ...

def equip_quest_item(name: str):
    """Click on quest item in inventory to equip it."""
    ...

def pickup_quest_item():
    """Detect and pickup quest items on ground (gold glow)."""
    # Quest items have a gold/purple glow around them
    # Detect with color thresholding
    ...

5. Quest Combat

Most quests involve fighting a boss or clearing a path. Botty already has combat logic.

# quest/quest_combat.py

from char.i_char import IChar
from pather import Pather

def kill_boss(boss_name: str, atk_len: float, char: IChar, pather: Pather):
    """
    Navigate to boss, fight until dead.
    Reuses botty's existing kill_* methods from run/*.py
    """
    # 1. Find boss on screen
    # 2. Move to boss position
    # 3. Attack until dead (same as run/trav.py)
    # 4. Return to town
    ...

def clear_room(char: IChar, pather: Pather):
    """Clear a room of monsters (for quests requiring room clearance)."""
    # Same logic as pather.follow_path() but with combat
    ...

6. Integration with Bot State Machine

The existing bot uses a state machine (transitions library). We add quest states.

# bot.py changes

# Add quest states to the state machine
self._states = self._states + [
    'quest', 'quest_act_transition'
]

# Add quest transitions
self._transitions = self._transitions + [
    {'trigger': 'run_quest', 'source': 'town', 'dest': 'quest', 'before': 'on_run_quest'},
    {'trigger': 'end_quest', 'source': 'quest', 'dest': 'town', 'before': 'on_end_quest'},
    {'trigger': 'act_transition', 'source': 'town', 'dest': 'quest_act_transition',
     'before': 'on_act_transition'},
]

def on_run_quest(self):
    """Start next quest."""
    from quest.quest_manager import QuestManager
    qm = QuestManager()
    nxt = qm.next_quest()
    if nxt:
        act, qn = nxt
        quest_func = self._get_quest_function(act, qn)
        quest_func()
        qm.mark_complete(act, qn)
        qm.save()

def on_end_quest(self):
    """After a quest completes, check if act is done."""
    qm = QuestManager()
    if qm.is_act_complete(self.current_act):
        self.trigger_or_stop('act_transition')
    else:
        self.trigger_or_stop('run_quest')

def on_act_transition(self):
    """Transition to next act (talk to NPC, unlock next act)."""
    qm = QuestManager()
    qm.advance_to_next_act()
    # ... handle act transition logic
    self.trigger_or_stop('start_from_town')

7. Configuration

Add to params.ini:

[quest]
; Enable quest mode (disables farming runs until quests are done)
enabled = 1

; Skip quests that are too dangerous for your character level
; 0 = run all quests, 50 = skip quests below level 50
min_level = 0

; Auto-stash quest items between acts
auto_stash_quest_items = 1

; Continue farming after all quests are done
farm_after_quest = 1

8. Implementation Order

Phase 1: Foundation (Quest Manager + NPC Interaction)

  • quest_manager.py with state tracking
  • quest_npc.py for dialogue interaction
  • quest_state.py for persistence

Phase 2: Act 1 (6 quests)

  • Q1: Search for the Smith (kill Rats, talk to Charsi)
  • Q2: Tools of the Trade (get items from Andariel's lair)
  • Q3: Sacrifice (kill Skeletons at Crypt)
  • Q4: The Summoner (kill Summoner in Catacombs)
  • Q5: The Shepherd (kill Skeleton King + Gargothon)
  • Q6: The Fallen Angel (kill Andariel)

Phase 3: Act 2 (7 quests)

  • Q1: Radament
  • Q2: The Horadric Staff (combine Cube + Scroll)
  • Q3: Tyrael's Breath (clear Sewers, talk to Alkor)
  • Q4: Secrets (activate 4 stone tablets)
  • Q5: The Summoner (kill Duriel)
  • Q6: The Seven Tombs (kill all 7 tomb bosses)
  • Q7: The Fallen Angel (kill Duriel again)

Phase 4: Act 3 (5 quests)

  • Q1: The Forgotten Tower
  • Q2: The Quest for the Horizon (Cain)
  • Q3: The Hellforge
  • Q4: The Hellgate
  • Q5: The Prime Evil (Mephisto)

Phase 5: Act 4 (3 quests)

  • Q1: The Fallen Angel (Harumony)
  • Q2: The Fallen Angel (Tal Rasha's Will)
  • Q3: The Prime Evil (Diablo)

Phase 6: Act 5 (3 quests)

  • Q1: The Fallen Angel (Ancient's Battle Order)
  • Q2: The Quest for the Horizon (Cain)
  • Q3: The Prime Evil (Baal)

9. Testing Strategy

  1. Test each quest independently in singleplayer
  2. Test quest-to-quest transitions
  3. Test act transitions
  4. Test recovery from death during quest
  5. Test with different character builds

10. Challenges

  • NPC dialogue: D2R has branching dialogue. Need to handle all paths.
  • Quest items: Some quests require carrying specific items (keys, weapons).
  • Room secrets: Act 2 Q4 requires finding hidden passages.
  • Multi-stage quests: Some quests span multiple areas (Act 3 Q2-4).
  • Character level: Quests are designed for specific levels. A level 90 character kills everything too fast/slow.
  • Anti-cheat: Questing is more visible to anti-cheat than farming. Need stealth settings.