- Read character .key file from Saved Games / D2R install dir / APPDATA - Auto-fill empty hotkeys in [char] section (inventory, belt, potions, etc.) - Match skill slots to configured skills for validation - Normalize key variants (left alt ~ alt, left shift ~ shift) - params.ini values always take priority over detected bindings - Warn on genuine mismatches between config and .key file
12 KiB
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
- Test each quest independently in singleplayer
- Test quest-to-quest transitions
- Test act transitions
- Test recovery from death during quest
- 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.