Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35aa13473e | ||
|
|
e03f866fbb | ||
|
|
968c75fefd | ||
|
|
7839b4ed82 | ||
|
|
75e88dbdc2 | ||
|
|
c5d9fe4541 | ||
|
|
648dc1b25c | ||
|
|
00e759ec0a | ||
|
|
6b044a17ea | ||
|
|
8f9185cde3 | ||
|
|
02e9f7856a | ||
|
|
15524e1608 | ||
|
|
6bcfa1af35 | ||
|
|
523cb54e46 | ||
|
|
7431807ee5 | ||
|
|
40ee587fb0 | ||
|
|
d1ba551832 | ||
|
|
3f8e08296e | ||
|
|
4e730e0c5b | ||
|
|
cef59a7df2 | ||
|
|
78f9d07545 | ||
|
|
eeb620696b | ||
|
|
0c246245c4 | ||
|
|
3d12a75b72 | ||
|
|
7cb15837d4 | ||
|
|
ab3f5633fc | ||
|
|
81f160d400 | ||
|
|
7fd0555468 | ||
|
|
b627172f1e | ||
|
|
512f63e0e5 | ||
|
|
6a07865f43 | ||
|
|
a539d3a236 | ||
|
|
710c1c709a | ||
|
|
61a88d2968 | ||
|
|
c1367dbfbd | ||
|
|
a2e2acfbde | ||
|
|
e8a9cdc6cd | ||
|
|
47e9344fd5 | ||
|
|
0176f66a1c | ||
|
|
38c2bbdea6 | ||
|
|
682580e44c | ||
|
|
34cc75c9e4 | ||
|
|
e62272b743 | ||
|
|
e797445cb9 | ||
|
|
5d5b03ca76 | ||
|
|
f5ea29489e | ||
|
|
abb06066d5 | ||
|
|
05df847933 | ||
|
|
0eca544d2e | ||
|
|
737636b644 | ||
|
|
17bcb95c0a | ||
|
|
59b851b107 | ||
|
|
0e90b36e14 | ||
|
|
71109e9532 | ||
|
|
6012934e66 | ||
|
|
48d8445c05 | ||
|
|
44794fba67 | ||
|
|
aa78bab638 | ||
|
|
3d11947bf2 | ||
|
|
f31c30f388 | ||
|
|
b9f6635f8f | ||
|
|
73c4601e2b | ||
|
|
5647f323a7 | ||
|
|
3db44fe4e6 | ||
|
|
57073946ae | ||
|
|
5c687773b1 | ||
|
|
c1997b438e | ||
|
|
aa2f52f702 | ||
|
|
0fbe480f38 | ||
|
|
de2943fe7a | ||
|
|
41c230ad9c | ||
|
|
a4e3ff0e2f | ||
|
|
6625c1e1d5 | ||
|
|
fc8ef97a7a | ||
|
|
1a58006188 | ||
|
|
e18b81200f | ||
|
|
03da8783b6 | ||
|
|
26ecb826ec | ||
|
|
3ed837e1ee | ||
|
|
175f8e342b | ||
|
|
c566519cd8 | ||
|
|
e712076f08 | ||
|
|
63d0a334cb | ||
|
|
0facc84661 | ||
|
|
c3a2edb23c | ||
|
|
a4498d2244 | ||
|
|
0facc38613 | ||
|
|
4bb460db2a | ||
|
|
23f9488853 | ||
|
|
e79cf0470d | ||
|
|
fffc0a4b56 | ||
|
|
0e9c18e3e7 | ||
|
|
87c8dc5e16 | ||
|
|
b84bdc8f4b | ||
|
|
8ca96241bf | ||
|
|
f42637a5d8 | ||
|
|
cbc797c14a | ||
|
|
3522dedcee | ||
|
|
6a8f1ed5d9 | ||
|
|
ebe2e354dc | ||
|
|
868d117530 | ||
|
|
9244570e22 | ||
|
|
b06c77d2e4 | ||
|
|
0834831220 | ||
|
|
40e4dd8a4f | ||
|
|
1b4773b42e | ||
|
|
77b35df26b | ||
|
|
63248a9437 | ||
|
|
2862591492 | ||
|
|
ddb7008419 | ||
|
|
43c77deede | ||
|
|
b77347542d | ||
|
|
1b7721779c | ||
|
|
6822a7e495 | ||
|
|
66a71b4ad3 | ||
|
|
b8479c2300 | ||
|
|
2bfcf0ff02 | ||
|
|
c80e002ab9 | ||
|
|
1fe95aae08 | ||
|
|
f1c5b6cdf4 | ||
|
|
19011c1a12 | ||
|
|
78bc195bd9 | ||
|
|
2784b50675 | ||
|
|
4e7c517764 | ||
|
|
3093e04517 | ||
|
|
b1bec0cecc | ||
|
|
54e63197c2 | ||
|
|
3372b91ed7 | ||
|
|
9d0ae9322a | ||
|
|
06c73e7cbf | ||
|
|
f410ea21f1 | ||
|
|
d9a6e7ce40 | ||
|
|
c92b7730ee | ||
|
|
a7b12bc8a3 | ||
|
|
0b8d67b4e4 | ||
|
|
3479a74503 | ||
|
|
5727a81cb8 | ||
|
|
95bd5a76ec | ||
|
|
a05a13be67 | ||
|
|
a06e46df09 | ||
|
|
8a58b44866 | ||
|
|
d99e3461fe | ||
|
|
a695252614 | ||
|
|
7ef62161ca | ||
|
|
6301490cc4 | ||
|
|
83951db876 | ||
|
|
793d7ed967 | ||
|
|
5fbac05e4e | ||
|
|
594d633787 | ||
|
|
9f45539210 | ||
|
|
83539ed9c3 | ||
|
|
f1d6c6bd1c | ||
|
|
86e3cccbc9 | ||
|
|
d0ecf3afdc | ||
|
|
8ccdca7946 | ||
|
|
7bd9d01023 | ||
|
|
45fa775a2e | ||
|
|
b781dd56b1 | ||
|
|
fda747a1bf | ||
|
|
5254b7ee75 | ||
|
|
f705091991 | ||
|
|
8cf152cfb4 | ||
|
|
af43051b22 | ||
|
|
a611765e42 | ||
|
|
21e63fc423 | ||
|
|
02d113ea0c | ||
|
|
17c97a61b1 | ||
|
|
307b021352 | ||
|
|
3410a82895 | ||
|
|
35fa4ba442 | ||
|
|
e729dc0c0e | ||
|
|
7e39119f33 | ||
|
|
d69f066da5 | ||
|
|
88bd369bd0 | ||
|
|
dc48a05d61 | ||
|
|
6f4d6880e4 | ||
|
|
97f4e55643 | ||
|
|
a91e128a1e | ||
|
|
01a0351530 | ||
|
|
5ac00d16d0 | ||
|
|
4db549cd85 | ||
|
|
95f520e968 | ||
|
|
7dcbb5284b | ||
|
|
dc61367a6e | ||
|
|
6c8da72bf4 | ||
|
|
77f2092572 | ||
|
|
6d42216fd1 | ||
|
|
d1502a008a | ||
|
|
b94147a9c9 | ||
|
|
75a35821e4 | ||
|
|
d4ccca8496 | ||
|
|
26f5faf977 | ||
|
|
3f46ccbe23 | ||
|
|
08a79b73c1 | ||
|
|
22b7ac5b49 | ||
|
|
3d7163df16 | ||
|
|
4c88c7204f | ||
|
|
4b96155a37 | ||
|
|
2baf51c717 | ||
|
|
2cd683ac7b | ||
|
|
30bf163ee3 | ||
|
|
cb3d49fa22 | ||
|
|
5244a50ea3 | ||
|
|
f59ffd5d1f | ||
|
|
8eafd91f0a | ||
|
|
a83dc15b62 | ||
|
|
78bb963654 | ||
|
|
7d1f03beb7 | ||
|
|
7d6314b575 | ||
|
|
21c33a3dc9 | ||
|
|
b9f3253e24 | ||
|
|
fefcfa013f | ||
|
|
4b339228fe | ||
|
|
43f973ebc2 | ||
|
|
16a4d121c0 | ||
|
|
be874f3275 | ||
|
|
719b0b1dea | ||
|
|
c228fa9661 | ||
|
|
8853620786 | ||
|
|
e4c201bf2d | ||
|
|
9491169549 | ||
|
|
3f50a6529c | ||
|
|
ed65691257 | ||
|
|
14b4835702 | ||
|
|
ecf91683b7 | ||
|
|
29e6cc3c99 | ||
|
|
9c00ca5de7 | ||
|
|
dbee47e152 | ||
|
|
512acb02b9 | ||
|
|
69dc33706a | ||
|
|
93771198f1 | ||
|
|
5cfa91fe64 | ||
|
|
8574eedc16 | ||
|
|
bc649ed863 | ||
|
|
ea4a753e1c | ||
|
|
635ccb0dea | ||
|
|
fd49d9700d | ||
|
|
ef29136e16 | ||
|
|
51319fd11f | ||
|
|
e3d6605595 | ||
|
|
19f4b8a553 | ||
|
|
3888cc9666 | ||
|
|
055cd4fdf1 | ||
|
|
377b2803e3 | ||
|
|
7620bd4ef7 | ||
|
|
9b5ba463c2 | ||
|
|
64349a988d | ||
|
|
cdc13d1c6c | ||
|
|
a3ac31463e | ||
|
|
e54bb64d03 | ||
|
|
179eac8b80 | ||
|
|
b7a7280964 | ||
|
|
909aa88977 | ||
|
|
254c167cbe | ||
|
|
b3fd60c26d | ||
|
|
55def980cd | ||
|
|
265326a471 | ||
|
|
af6ab38939 | ||
|
|
5ee0061936 | ||
|
|
0bd8e7e7bc | ||
|
|
25c5810778 | ||
|
|
f20ac02720 | ||
|
|
c96fcdf545 | ||
|
|
b77d62144a | ||
|
|
e573a28e8a | ||
|
|
d79f472c33 | ||
|
|
3bf1c3d539 | ||
|
|
a691d4bdfc | ||
|
|
b84b745dbd | ||
|
|
a70302f44d | ||
|
|
0b1af05841 | ||
|
|
d96cf544bf | ||
|
|
fc3ac30431 | ||
|
|
a39acf510d | ||
|
|
c9ffcf1e4f | ||
|
|
0ccf8ae5ea | ||
|
|
d4e1758d97 | ||
|
|
40d46bb592 | ||
|
|
c5519ff290 | ||
|
|
dd45386579 | ||
|
|
54f0e3ebcc | ||
|
|
7eb961476e | ||
|
|
d44acf8bf2 | ||
|
|
f597a0108c | ||
|
|
af06d8aff4 | ||
|
|
9ee102a0f1 | ||
|
|
5e320f0eef | ||
|
|
2634619b18 | ||
|
|
dc224bec5c | ||
|
|
a77e87e138 | ||
|
|
080b7d1679 | ||
|
|
d4fec70950 | ||
|
|
d3dbf2bf9c | ||
|
|
887a97c93b | ||
|
|
d6c8d8d5f7 | ||
|
|
e1c18faf26 | ||
|
|
bdd6247342 | ||
|
|
77ad97022e | ||
|
|
d9383cc164 | ||
|
|
a77dafaf08 | ||
|
|
18c08a4c65 | ||
|
|
f8380af3bd | ||
|
|
ee940b7e3d | ||
|
|
91403e49d3 | ||
|
|
8ee31d9691 | ||
|
|
40a207be9a | ||
|
|
dec3269a0c | ||
|
|
27573bf362 | ||
|
|
5451357b86 | ||
|
|
94f3e6bca8 | ||
|
|
6c261c3cbb | ||
|
|
6ace0a7970 | ||
|
|
ed4aaf77f8 | ||
|
|
416b6714dd | ||
|
|
ab29fa0a46 | ||
|
|
77d84680c6 | ||
|
|
d0e6eb7d21 | ||
|
|
75f64351ee | ||
|
|
8dc677d2e6 | ||
|
|
8d1a2f20c8 | ||
|
|
ce5816a130 | ||
|
|
e9d88f9763 | ||
|
|
776a09da5d | ||
|
|
b3052e50b4 | ||
|
|
8575bc4446 | ||
|
|
3f6363aec6 | ||
|
|
bab6e24438 | ||
|
|
9bbc753a16 | ||
|
|
df501d3ea9 | ||
|
|
5ea1c17902 | ||
|
|
0af8a54116 | ||
|
|
23e7e54f8d | ||
|
|
b52f7ebca9 | ||
|
|
329b7e77d9 | ||
|
|
5ab9d1c68a | ||
|
|
15a90bfd47 | ||
|
|
17f32e65b2 | ||
|
|
b397d07baf | ||
|
|
eee99c069b | ||
|
|
81ee6557f4 | ||
|
|
f484a9bef1 | ||
|
|
6d19819de5 | ||
|
|
71311cb584 | ||
|
|
5c4f62ac25 | ||
|
|
869de31315 | ||
|
|
6f6c663b43 | ||
|
|
fdd0fc2230 | ||
|
|
5d5a9c2a63 | ||
|
|
eb75fc9aee | ||
|
|
600904e6bc | ||
|
|
ce821695e8 | ||
|
|
455fe8e64f | ||
|
|
f4bc3b7401 | ||
|
|
b528957c40 | ||
|
|
f8c9d81f52 | ||
|
|
1a5a261f7c | ||
|
|
95e019e88d | ||
|
|
466197fd8b | ||
|
|
4a4bf0300a | ||
|
|
953b9aedb3 | ||
|
|
2ce9636d3a | ||
|
|
b03b2fd226 | ||
|
|
d96eb2d34c | ||
|
|
5f2149ed1e | ||
|
|
a7509c90e7 | ||
|
|
8b48b583b9 | ||
|
|
58f761c372 | ||
|
|
36d364517b | ||
|
|
869eeb5539 | ||
|
|
b2afbaab93 | ||
|
|
9512856cfc | ||
|
|
593e509bc6 | ||
|
|
68ed191efc | ||
|
|
77925964a0 | ||
|
|
665e6b6ce4 | ||
|
|
51306f3067 | ||
|
|
c47d9573ad | ||
|
|
bbe5395f4a | ||
|
|
6f7a9b681d | ||
|
|
ed0a8270cf | ||
|
|
4b3a47b13c | ||
|
|
6f4f4ca472 | ||
|
|
55f7d86b3d | ||
|
|
01f03ea9af | ||
|
|
82ac44c4c1 | ||
|
|
65ee5a6b5c | ||
|
|
b7c83e6208 | ||
|
|
f2c639fc64 | ||
|
|
0978258513 | ||
|
|
b243aec74f | ||
|
|
ec5a7e4292 | ||
|
|
4852ef9e3a | ||
|
|
d7a1d65348 | ||
|
|
758a0a3db8 | ||
|
|
b27911adae | ||
|
|
43e5a8ecab | ||
|
|
b52ed15079 | ||
|
|
c82051f276 | ||
|
|
6fc80f0a96 | ||
|
|
2da55e138c | ||
|
|
5cbf7ea366 | ||
|
|
a2d27fd4c3 | ||
|
|
be446fc9d9 | ||
|
|
4b93a50927 | ||
|
|
76cbe0abbc | ||
|
|
a54b674ce6 | ||
|
|
11440a3d24 | ||
|
|
5f478a6884 | ||
|
|
fb2edcf70a | ||
|
|
ed2a921d9e | ||
|
|
ba8a412309 | ||
|
|
5d42a46891 | ||
|
|
c13367f221 | ||
|
|
c94be67727 | ||
|
|
1e8bb16a22 | ||
|
|
3e87fe9d6f | ||
|
|
b517a80bea | ||
|
|
b2938e6297 | ||
|
|
3a96bf69cc | ||
|
|
429a1cd4ae | ||
|
|
dab2fd9259 | ||
|
|
8048d87222 | ||
|
|
edcefbe62c | ||
|
|
aea75aa417 | ||
|
|
30d407e978 | ||
|
|
8df42bbcb1 | ||
|
|
da396208f0 | ||
|
|
466c698b1d | ||
|
|
58bc20ce0e | ||
|
|
25fdd5485d | ||
|
|
bfccf60a07 | ||
|
|
8be1223e32 | ||
|
|
df1259f87c | ||
|
|
1ca3578e06 | ||
|
|
3c70214ca9 | ||
|
|
8c44d7ad17 | ||
|
|
4199705fe8 | ||
|
|
7141e671d1 | ||
|
|
c20ec7343c | ||
|
|
9a714b969e | ||
|
|
d518389493 | ||
|
|
c6f81c70a6 | ||
|
|
70659b7544 | ||
|
|
aa56bbc484 | ||
|
|
901592a54a | ||
|
|
a51db00a15 | ||
|
|
0197298e31 | ||
|
|
a4dada9237 | ||
|
|
6b2f719605 | ||
|
|
ed70c8e688 | ||
|
|
f68ebf0861 | ||
|
|
39788df29c | ||
|
|
f1ea45ce2a | ||
|
|
31a0579f65 | ||
|
|
ad8ea13444 | ||
|
|
5df97c4a7e | ||
|
|
f5538916fe | ||
|
|
d7be49edd7 | ||
|
|
6e10c549e4 | ||
|
|
656e52ffff | ||
|
|
8c9e98ebd8 | ||
|
|
da09cb820a | ||
|
|
d45d974280 | ||
|
|
613d4f0ff3 | ||
|
|
8f4942a3a2 | ||
|
|
ef34efdd54 | ||
|
|
9886f2ee96 | ||
|
|
3e4c0d8fa5 | ||
|
|
f72856a8bd | ||
|
|
980a8f5036 | ||
|
|
9f1a4772cd | ||
|
|
c68d92c8e1 | ||
|
|
076737672e | ||
|
|
689a93a3f7 | ||
|
|
9ed7869ddd | ||
|
|
c62c237442 | ||
|
|
4e10dbb2c7 | ||
|
|
197ff41e69 | ||
|
|
5089abaca6 | ||
|
|
8469d51f0c | ||
|
|
9b2d52a39b | ||
|
|
d95b581dae | ||
|
|
759c4fff82 | ||
|
|
d5bdb769b3 | ||
|
|
8a1a15d77d | ||
|
|
24d2da8e67 | ||
|
|
6d1779ea5f | ||
|
|
0cbc5f759e | ||
|
|
ce1d99bfd8 | ||
|
|
257d687166 | ||
|
|
cfc192e1dc | ||
|
|
161b9226fb | ||
|
|
46b8d9bf17 | ||
|
|
9faa3fd25a | ||
|
|
bca0471d9f | ||
|
|
5610490db6 | ||
|
|
5efebd0bb7 | ||
|
|
81bc9028d1 | ||
|
|
3de9f6db93 | ||
|
|
0f08cd8e93 |
@@ -1,8 +1,21 @@
|
||||
# .coveragerc to control coverage.py
|
||||
[run]
|
||||
branch = True
|
||||
source = src
|
||||
# Exclude conda/xonsh internal shims that leak into coverage data. The phantom
|
||||
# is an absolute path like D:\a\...\config-3.py, so a bare "config-3.py" omit
|
||||
# never matches — use a glob that matches any path ending in config-<n>.py.
|
||||
omit =
|
||||
*config-*.py
|
||||
*/site-packages/*
|
||||
*/conda-meta/*
|
||||
|
||||
[xml]
|
||||
output = coverage.xml
|
||||
|
||||
[report]
|
||||
# Ignore missing source files (conda shims reference files not in tree)
|
||||
ignore_errors = True
|
||||
# Regexes for lines to exclude from consideration
|
||||
exclude_lines =
|
||||
# Have to re-enable the standard pragma
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Botty personal environment overrides.
|
||||
# Copy this file to ".env" in the repo root and edit your values there.
|
||||
# ".env" is ignored by git and should never be committed.
|
||||
#
|
||||
# Format: BOTTY_<CONFIG_KEY>=<value>
|
||||
# These override values from config/params.ini and config/custom.ini at runtime.
|
||||
#
|
||||
# Common personal settings:
|
||||
|
||||
# Bot display/account name used in logs/Discord usernames.
|
||||
# BOTTY_NAME=zapzap
|
||||
|
||||
# Main Discord webhook (status/death/chicken/general messages).
|
||||
# BOTTY_CUSTOM_MESSAGE_HOOK=https://discord.com/api/webhooks/...
|
||||
|
||||
# Optional dedicated loot webhook for item drops.
|
||||
# BOTTY_CUSTOM_LOOT_MESSAGE_HOOK=https://discord.com/api/webhooks/...
|
||||
|
||||
# Optional auto-login credentials (keep private).
|
||||
# BOTTY_BNET_NAME=your-battlenet-email-or-name
|
||||
# BOTTY_BNET_PASS=your-battlenet-password
|
||||
# BOTTY_CHAR_NAME=your-character-name
|
||||
|
||||
# Optional path override if you reference it in params.ini:
|
||||
# BOTTY_SAVED_GAMES_FOLDER=C:\Users\you\Saved Games\Diablo II Resurrected
|
||||
@@ -1,52 +1,207 @@
|
||||
name: Botty - CI
|
||||
on: [pull_request]
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, stable]
|
||||
pull_request:
|
||||
branches: [main, stable]
|
||||
|
||||
# Cancel in-progress runs on new push
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
install-and-test:
|
||||
name: Install & Test (Windows)
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Miniconda Python 3.10
|
||||
uses: actions/setup-python@v2
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
# TODO: The below setp only chaches conda packages, need to cache pip seperatly
|
||||
- name: Cache conda
|
||||
uses: actions/cache@v2
|
||||
python-version: "3.10"
|
||||
cache: "pip"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements-win11.txt
|
||||
shell: bash
|
||||
|
||||
- name: Install Tesseract OCR
|
||||
run: choco install tesseract --no-progress -y
|
||||
|
||||
- name: Python version
|
||||
run: python -c "import sys; print(sys.version)"
|
||||
|
||||
- name: Syntax check
|
||||
env:
|
||||
# Increase this value to reset cache if environment.yml has not changed
|
||||
CACHE_NUMBER: 1
|
||||
with:
|
||||
path: ~/conda_pkgs_dir
|
||||
key: conda-${{ env.CACHE_NUMBER }}-${{hashFiles('environment.yml') }}
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v2
|
||||
PYTHONPATH: ./src
|
||||
run: python -m compileall -q src tools test scripts
|
||||
|
||||
- name: Verify core imports
|
||||
env:
|
||||
# Increase this value to reset cache if environment.yml has not changed
|
||||
CACHE_NUMBER: 1
|
||||
PYTHONPATH: ./src
|
||||
run: |
|
||||
python -c "
|
||||
import sys
|
||||
sys.path.insert(0, 'src')
|
||||
modules = [
|
||||
'config',
|
||||
'logger',
|
||||
'screen',
|
||||
'pather',
|
||||
'template_finder',
|
||||
'game_stats',
|
||||
'health_manager',
|
||||
'death_manager',
|
||||
'd2r_image',
|
||||
'item',
|
||||
'item.pickit',
|
||||
'transmute',
|
||||
'shop',
|
||||
'char',
|
||||
'utils',
|
||||
'utils.os_detect',
|
||||
'messages',
|
||||
]
|
||||
for mod in modules:
|
||||
try:
|
||||
__import__(mod)
|
||||
print(f' {mod}: OK')
|
||||
except ImportError as e:
|
||||
print(f' {mod}: FAILED - {e}')
|
||||
sys.exit(1)
|
||||
print('All core imports successful.')
|
||||
"
|
||||
|
||||
- name: Verify botty runs (import main modules)
|
||||
env:
|
||||
PYTHONPATH: ./src
|
||||
run: |
|
||||
python -c "
|
||||
import sys, os
|
||||
sys.path.insert(0, 'src')
|
||||
import ssl
|
||||
ssl.SSLContext.load_default_certs = lambda *a, **k: None
|
||||
|
||||
from version import __version__
|
||||
print(f' Version: {__version__}')
|
||||
|
||||
from config import Config
|
||||
print(' Config: OK')
|
||||
|
||||
from game_controller import GameController
|
||||
print(' GameController: OK')
|
||||
|
||||
from bot import Bot
|
||||
print(' Bot: OK')
|
||||
|
||||
from run.diablo import Diablo
|
||||
print(' Diablo run: OK')
|
||||
|
||||
from run.pindle import Pindle
|
||||
print(' Pindle run: OK')
|
||||
|
||||
from run.arcane import Arcane
|
||||
print(' Arcane run: OK')
|
||||
|
||||
from run.vizier import Vizier
|
||||
print(' Vizier run: OK')
|
||||
|
||||
print('All botty entry modules import successfully.')
|
||||
"
|
||||
|
||||
- name: Verify OCR (pytesseract)
|
||||
env:
|
||||
PYTHONPATH: ./src
|
||||
run: |
|
||||
python -c "
|
||||
import sys, os, tempfile
|
||||
sys.path.insert(0, 'src')
|
||||
|
||||
import pytesseract
|
||||
pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
version = pytesseract.get_tesseract_version()
|
||||
print(f' Tesseract version: {version}')
|
||||
|
||||
# Create a simple test image with text
|
||||
img = np.full((50, 200), 255, dtype=np.uint8)
|
||||
cv2.putText(img, 'Hello Botty', (20, 35),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 0), 1)
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as f:
|
||||
cv2.imwrite(f.name, img)
|
||||
text = pytesseract.image_to_string(f.name, lang='eng').strip()
|
||||
print(f' OCR result: {text}')
|
||||
|
||||
print('OCR pytesseract: OK')
|
||||
"
|
||||
|
||||
- name: Verify OCR (botty ocr module)
|
||||
env:
|
||||
PYTHONPATH: ./src
|
||||
PYTESSERACT_TESSERACT_CMD: C:\Program Files\Tesseract-OCR\tesseract.exe
|
||||
run: |
|
||||
python -c "
|
||||
import sys, os
|
||||
sys.path.insert(0, 'src')
|
||||
|
||||
# Import the botty OCR module - it reads PYTESSERACT_TESSERACT_CMD from env
|
||||
from d2r_image.ocr import image_to_text, pytesseract
|
||||
import cv2
|
||||
import numpy as np
|
||||
import tempfile
|
||||
|
||||
if pytesseract is None:
|
||||
print(' pytesseract not available — skipping')
|
||||
sys.exit(0)
|
||||
|
||||
# Create test image (3-channel for invert)
|
||||
img = np.full((50, 200, 3), 255, dtype=np.uint8)
|
||||
cv2.putText(img, '123', (80, 35),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 0), 2)
|
||||
|
||||
# Run through botty's image_to_text
|
||||
results = image_to_text(
|
||||
[img],
|
||||
model='hover-eng_inconsolata_inv_th_fast',
|
||||
psm=7,
|
||||
crop_pad=False,
|
||||
invert=True,
|
||||
threshold=25,
|
||||
)
|
||||
result_text = results[0].text if results else 'empty'
|
||||
print(f' OCR result: {result_text}')
|
||||
print('OCR botty module: OK')
|
||||
"
|
||||
|
||||
- name: Log analyzer tests (self-healing framework)
|
||||
env:
|
||||
PYTHONPATH: ./src:.
|
||||
RUN_ENV: test
|
||||
run: |
|
||||
python -m pytest test/auto/test_log_analyzer.py -v --tb=short
|
||||
|
||||
- name: Tests with coverage
|
||||
env:
|
||||
PYTHONPATH: ./src:.
|
||||
RUN_ENV: test
|
||||
run: |
|
||||
python -m coverage run -m pytest -v --tb=short --ignore=test/auto/test_self_healing.py --ignore=test/smoke_test.py --ignore=test/test_version_consistency.py
|
||||
python -m coverage xml --ignore-errors || true
|
||||
|
||||
- name: Upload coverage
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
path: ~/pip
|
||||
key: pip-${{ env.CACHE_NUMBER }}-${{ hashFiles('environment.yml') }}
|
||||
- name: Install Miniconda
|
||||
uses: conda-incubator/setup-miniconda@v2
|
||||
with:
|
||||
python-version: '3.10'
|
||||
activate-environment: botty
|
||||
channel-priority: strict
|
||||
environment-file: environment.yml
|
||||
use-only-tar-bz2: true # Needed for caching
|
||||
- name: Activate conda
|
||||
shell: powershell
|
||||
run: |
|
||||
C:\Miniconda\condabin\conda.bat init powershell
|
||||
set PYTHONPATH=./src
|
||||
- name: Pytest & Coverage
|
||||
shell: powershell
|
||||
run: |
|
||||
C:\Miniconda\condabin\conda.bat activate botty
|
||||
python -c "import sys; print(sys.version)"
|
||||
coverage run --source=./src -m pytest -v -s
|
||||
- name: Coverage Report Generation
|
||||
shell: powershell
|
||||
run: |
|
||||
C:\Miniconda\condabin\conda.bat activate botty
|
||||
coverage xml
|
||||
name: coverage-report
|
||||
path: coverage.xml
|
||||
retention-days: 7
|
||||
@@ -15,18 +15,58 @@ botty_v*/
|
||||
custom.ini
|
||||
config/custom.ini
|
||||
config/custom.*.ini
|
||||
config/*.local.ini
|
||||
.vscode
|
||||
.vs/
|
||||
test/assets/
|
||||
# Logs and run reports
|
||||
log/
|
||||
log/archive/
|
||||
log/runs/
|
||||
log/stats/
|
||||
*.log
|
||||
|
||||
# Scraped data (cached web pages, URLs)
|
||||
data/d2jsp_pages/
|
||||
data/d2jsp_topic_urls.txt
|
||||
|
||||
# Debug files
|
||||
debug_forum.html
|
||||
|
||||
# Generated/runtime price data (updated automatically by bot)
|
||||
config/fg_daily_estimates.json
|
||||
config/fg_prices.json
|
||||
config/fg_prices.db
|
||||
config/daily_prices.json
|
||||
config/daily_prices_history.json
|
||||
config/traderie_prices.json
|
||||
|
||||
# Screenshots (debug/test captures)
|
||||
screenshots/
|
||||
|
||||
# Tools dev cache
|
||||
tools/__pycache__/
|
||||
|
||||
# Build artifacts
|
||||
botty_v*/
|
||||
botty_v*.zip
|
||||
|
||||
.venv
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.bak
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# Secrets / account-specific — never commit
|
||||
cookies.txt
|
||||
cookies_temp*
|
||||
*.cookies
|
||||
config/custom.ini
|
||||
coverage.xml
|
||||
utils/live-view/
|
||||
config/nip/*
|
||||
!config/nip/.gitkeep
|
||||
|
||||
config/bnip/*
|
||||
!config/bnip/.gitkeep
|
||||
stats/
|
||||
info_screenshots/
|
||||
item_error_screenshots/
|
||||
@@ -34,5 +74,32 @@ loot_screenshots/
|
||||
pickit_screenshots/
|
||||
*info_log_parsed.txt
|
||||
*info_*.png
|
||||
*.log
|
||||
log/
|
||||
blizzpw.txt
|
||||
|
||||
# Local user/workspace files
|
||||
.claude/
|
||||
GEMINI.md
|
||||
test_config_load.py
|
||||
|
||||
# Debug directory
|
||||
botty_debug/
|
||||
|
||||
# per-user character profiles (survive git pulls)
|
||||
config/profiles/
|
||||
config/active_profile.txt
|
||||
|
||||
# Docker / dev-only files (not production)
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
docker-*.bat
|
||||
DOCKER.md
|
||||
botty_next/
|
||||
src/bridge_server.py
|
||||
src/input_layer/bridge_input.py
|
||||
.hermes/
|
||||
fixtures/
|
||||
test/run/
|
||||
|
||||
# Installer output (generated by run_install_capture.bat)
|
||||
install_log.txt
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
# Botty - Open Issues & TODO Plan
|
||||
|
||||
Generated: 2026-06-01
|
||||
|
||||
---
|
||||
|
||||
## HIGH PRIORITY (affects botting reliability)
|
||||
|
||||
### 1. BNIP transpiler broken code (`src/bnip/transpile.py:369`)
|
||||
- **Status:** `# TODO FIX THIS SHIT` — `remove_quantity()` function is hacky
|
||||
- **Problem:** Expression splitting by `#` is fragile; can corrupt BNIP expressions with multiple `#` delimiters
|
||||
- **Impact:** Pickit rules may silently misparse quantity operators
|
||||
- **Fix:** Rewrite `remove_quantity()` to properly handle `#`-delimited expressions with edge cases
|
||||
- **File:** `src/bnip/transpile.py`
|
||||
|
||||
### 2. FOH cast delay missing (`src/char/paladin/fohdin.py:99`)
|
||||
- **Status:** `# TODO: add delay between FOH casts--doesn't properly cast each FOH in sequence`
|
||||
- **Problem:** FOH casts fire too fast; some casts don't land in sequence
|
||||
- **Impact:** Reduced DPS, wasted FOH rotations
|
||||
- **Fix:** Add `wait()` between FOH casts to ensure each cast completes before next
|
||||
- **File:** `src/char/paladin/fohdin.py`
|
||||
|
||||
### 3. Chest opening telekinesis workaround (`src/chest.py:51`)
|
||||
- **Status:** `# TODO: Act as picking up a potion to support telekinesis`
|
||||
- **Problem:** Chest open simulates potion pickup to work around telekinesis skill
|
||||
- **Impact:** Fragile interaction; may break with game updates
|
||||
- **Fix:** Implement proper chest interaction that accounts for telekinesis
|
||||
- **File:** `src/chest.py`
|
||||
|
||||
### 4. Inventory full handling (`src/item/pickit.py:236`)
|
||||
- **Status:** `#TODO Create logic to handle inventory full`
|
||||
- **Problem:** When inventory fills, pickit just stops — doesn't try to stash, sell, or prioritize
|
||||
- **Impact:** Bot stops picking up items mid-run; lost gold/runes
|
||||
- **Fix:** Add fallback logic: stop picking, trigger town run to stash/sell
|
||||
- **File:** `src/item/pickit.py`
|
||||
|
||||
### 5. Overburdened handling (`src/ui/view.py:109`)
|
||||
- **Status:** `#TODO: handle "Overburdened"`
|
||||
- **Problem:** `pickup_corpse()` doesn't detect "Overburdened" state after clicking
|
||||
- **Impact:** Bot may get stuck trying to pickup corpse when overweight
|
||||
- **Fix:** Add template detection for "Overburdened" UI and handle gracefully
|
||||
- **File:** `src/ui/view.py`
|
||||
|
||||
---
|
||||
|
||||
## MEDIUM PRIORITY (code quality / edge cases)
|
||||
|
||||
### 6. BNIP parenthesis cross-section check (`src/bnip/transpile.py:199`)
|
||||
- **Status:** `# TODO Backtrace until the last opening to make sure it wasn't from the past section.`
|
||||
- **Problem:** Parenthesis validation doesn't catch `(` in one BNIP section and `)` in another
|
||||
- **Impact:** Silent BNIP syntax errors that pass validation
|
||||
- **Fix:** Implement backtrace to reject cross-section parentheses
|
||||
- **File:** `src/bnip/transpile.py`
|
||||
|
||||
### 7. BNIP lexer misplaced checks (`src/bnip/lexer.py:319`)
|
||||
- **Status:** `# TODO: The second checks seem a little misplaced`
|
||||
- **Problem:** `NTIPAliasClass` and `TokenType.CLASS:` checks should be in transpiler validation, not lexer
|
||||
- **Impact:** Code organization; potential missed validation
|
||||
- **Fix:** Move validation logic to `transpile.py` and emit warnings
|
||||
- **File:** `src/bnip/lexer.py`, `src/bnip/transpile.py`
|
||||
|
||||
### 8. BNIP actions error handling (`src/bnip/actions.py:209`)
|
||||
- **Status:** `# TODO look at these errors`
|
||||
- **Problem:** BNIP load errors are printed but not properly logged or categorized
|
||||
- **Impact:** Hard to diagnose BNIP parse failures
|
||||
- **Fix:** Replace `print()` with proper `Logger.error()` and structured error reporting
|
||||
- **File:** `src/bnip/actions.py`
|
||||
|
||||
### 9. Pickit return type (`src/item/pickit.py:165`)
|
||||
- **Status:** `TODO :return: return a list of the items that were picked up`
|
||||
- **Problem:** Docstring says it should return a list, but function returns `bool`
|
||||
- **Impact:** Inconsistent API; callers can't know what was actually picked
|
||||
- **Fix:** Return `list[Item]` of picked items instead of `bool`
|
||||
- **File:** `src/item/pickit.py`
|
||||
|
||||
### 10. Consumable auto-belt (`src/inventory/personal.py:350`)
|
||||
- **Status:** `# TODO: logic for trying to add potion to belt if there are needs`
|
||||
- **Problem:** Consumables found during inventory management aren't auto-added to belt
|
||||
- **Impact:** Bot doesn't restock belt potions from inventory during runs
|
||||
- **Fix:** Add logic to detect belt needs and move potions from inventory
|
||||
- **File:** `src/inventory/personal.py`
|
||||
|
||||
### 11. Merc blocking templates (`src/run/nihlathak.py:45`)
|
||||
- **Status:** `# TODO: We might need a second template for each option as merc might run into the template`
|
||||
- **Problem:** Merc can stand on template match location, causing detection failure
|
||||
- **Impact:** Nihlathak run fails to detect layout variant
|
||||
- **Fix:** Add backup templates with offset ROIs for each layout variant
|
||||
- **File:** `src/run/nihlathak.py`
|
||||
|
||||
---
|
||||
|
||||
## LOW PRIORITY (cleanup / refactoring)
|
||||
|
||||
### 12. Character select cleanup (`src/ui/character_select.py:109`)
|
||||
- **Status:** `# TODO: can cleanup logic here, can we utilize a generic ScreenObject or use custom locator?`
|
||||
- **Problem:** Character selection uses ad-hoc template search instead of reusable ScreenObject
|
||||
- **Fix:** Refactor to use `ScreenObjects` pattern
|
||||
- **File:** `src/ui/character_select.py`
|
||||
|
||||
### 13. Screen utility functions (`src/screen.py:104`)
|
||||
- **Status:** `# TODO: Move the below funcs to utils(?)`
|
||||
- **Problem:** `convert_monitor_to_screen()` and related functions live in `screen.py` but could be in utils
|
||||
- **Fix:** Move coordinate conversion functions to `src/utils/`
|
||||
- **File:** `src/screen.py`
|
||||
|
||||
### 14. Graphic debugger re-init (`src/utils/graphic_debugger.py:60`)
|
||||
- **Status:** `# TODO: these two layers variable needs to be reassigned because F10 will not re-init`
|
||||
- **Problem:** Debugger layers don't reinitialize properly on F10 toggle
|
||||
- **Fix:** Move layer state into controller class; reinit on stop/start
|
||||
- **File:** `src/utils/graphic_debugger.py`
|
||||
|
||||
### 15. mttkinter logging (`src/utils/mttkinter.py:62-63`)
|
||||
- **Status:** `# TODO: Replace custom logging functionality with standard logging.Logger`
|
||||
- **Problem:** Custom logging in tkinter utils instead of standard library
|
||||
- **Fix:** Replace with `logging.Logger`
|
||||
- **File:** `src/utils/mttkinter.py`
|
||||
|
||||
### 16. Pickit test note (`test/nip/keep_item_test_cases.py:978`)
|
||||
- **Status:** `# TODO: I had to change from [defense] >= 47 to [plusdefense] >= 47`
|
||||
- **Problem:** `[defense]` is a calculated property; `[plusdefense]` is the raw value. Note for future reference.
|
||||
- **Fix:** Document in BNIP docs that `[defense]` is calculated; `[plusdefense]` is the raw modifier
|
||||
- **File:** `test/nip/keep_item_test_cases.py`
|
||||
|
||||
### 17. New route scaffolding (`src/utils/new_route.py`)
|
||||
- **Status:** Multiple TODO placeholders (by design — it's a code generator template)
|
||||
- **Problem:** Template placeholders are intentional; not bugs
|
||||
- **Fix:** No action needed — these are scaffolding placeholders
|
||||
- **File:** `src/utils/new_route.py`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Priority | Count | Files |
|
||||
|----------|-------|-------|
|
||||
| HIGH | 5 | transpile.py, fohdin.py, chest.py, pickit.py, view.py |
|
||||
| MEDIUM | 6 | transpile.py, lexer.py, actions.py, pickit.py, personal.py, nihlathak.py |
|
||||
| LOW | 6 | character_select.py, screen.py, graphic_debugger.py, mttkinter.py, keep_item_test_cases.py, new_route.py |
|
||||
|
||||
**Total: 17 items across 13 files**
|
||||
@@ -0,0 +1,87 @@
|
||||
# Botty Improvements Implementation Plan
|
||||
|
||||
## Status Legend
|
||||
- [ ] Not started
|
||||
- [~] In progress
|
||||
- [x] Done
|
||||
- [-] Cancelled / low priority
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Key Auto-Detection (Issue #940)
|
||||
Read D2R .key file and auto-fill hotkeys.
|
||||
|
||||
- [x] Create src/utils/key_detector.py module
|
||||
- [x] VK code mapping (partial — needs review for accuracy)
|
||||
- [x] Parse .key file (text format: VK action_type param)
|
||||
- [x] Auto-fill empty [char] hotkeys from detected bindings
|
||||
- [x] Auto-fill build-specific skill hotkeys (fohdin, hammerdin, etc.)
|
||||
- [x] Wire into config.py load_data()
|
||||
- [ ] REVIEW: Verify VK_MAP accuracy (D2R uses its own VK offset scheme)
|
||||
- [ ] REVIEW: Skill slot-to-config matching is heuristic — may misassign
|
||||
- [ ] TEST: Verify against actual D2R .key file on user's machine
|
||||
|
||||
## Phase 2: Target Detection False Positives (Issues #959/#964)
|
||||
Health bars and "immune to X" text mistaken for targets.
|
||||
|
||||
- [ ] Analyze current get_visible_targets() in target_detect.py
|
||||
- [ ] Add shape/size filtering: health bars are thin horizontal strips, immune text is small
|
||||
- [ ] Add aspect ratio check: real targets (poison/freeze auras) are roughly circular/elliptical
|
||||
- [ ] Add minimum bounding box height constraint (filter out thin text)
|
||||
- [ ] Optionally: add color temperature check (immune text is yellow/gold, not blue/green)
|
||||
- [ ] Test with screenshots of edge cases
|
||||
|
||||
## Phase 3: Pickit Timing Fix (Issue #939)
|
||||
Items skipped because bot teleports away before grabbing.
|
||||
|
||||
- [ ] Review pickit.py _yoink_item() for timing issues
|
||||
- [ ] Add configurable pickup_delay parameter (current: fixed timing)
|
||||
- [ ] Add retry logic: if item still visible after pickup attempt, re-try
|
||||
- [ ] Add "slow mode" for large/heavy items (framed/magic items may animate longer)
|
||||
- [ ] Ensure bot doesn't teleport until pickup animation completes
|
||||
- [ ] Test: verify no "Attempt to pick xyz" warnings followed by teleport
|
||||
|
||||
## Phase 4: Parallel Template Search (Issue #848)
|
||||
Speed up template_finder.search() with threading.
|
||||
|
||||
- [ ] Add ThreadPoolExecutor-based search_all_parallel()
|
||||
- [ ] Keep existing search() for single-template (no overhead)
|
||||
- [ ] Only parallelize when searching >3 templates simultaneously
|
||||
- [ ] Benchmark: measure speedup on typical 1280x720 grab
|
||||
|
||||
## Phase 5: Async Mouse Moves (Issue #955)
|
||||
Non-blocking mouse movement.
|
||||
|
||||
- [ ] Add async_move() to utils/custom_mouse.py
|
||||
- [ ] Run movement in background thread
|
||||
- [ ] Add is_moving() / wait_for_move() synchronization
|
||||
- [ ] Integrate into game_controller.py for smoother action chains
|
||||
|
||||
## Phase 6: Hardcore Chicken Loop Fix (Issue #942)
|
||||
Prevent infinite death loops on Hardcore characters.
|
||||
|
||||
- [ ] Review death_manager.py chicken logic
|
||||
- [ ] Add max_chicken_count config parameter (default: 3)
|
||||
- [ ] If max chicken count exceeded on HC, exit gracefully instead of re-entering
|
||||
- [ ] Add defensive chicken config option (chicken to TP instead of full chicken)
|
||||
- [ ] Test: verify HC character exits cleanly after N deaths
|
||||
|
||||
## Phase 7: Auto-Label NPCs (Issue #950)
|
||||
Learn vendor identities automatically during gameplay.
|
||||
|
||||
- [ ] During town states, detect NPC name plates via OCR
|
||||
- [ ] Cross-reference detected names with known NPC list
|
||||
- [ ] Auto-capture NPC templates when confidence is high
|
||||
- [ ] Store learned templates in assets/npc/
|
||||
- [ ] This is a long-term feature — lower priority
|
||||
|
||||
---
|
||||
|
||||
## Priority Order (implement in this order)
|
||||
1. **Phase 1** - Key auto-detection (already partially done, needs review + test)
|
||||
2. **Phase 3** - Pickit timing (high impact on loot collection)
|
||||
3. **Phase 2** - Target detection (high impact on kill reliability)
|
||||
4. **Phase 6** - Hardcore chicken fix (safety critical)
|
||||
5. **Phase 4** - Parallel template search (performance)
|
||||
6. **Phase 5** - Async mouse moves (quality of life)
|
||||
7. **Phase 7** - Auto-label NPCs (long-term feature)
|
||||
@@ -0,0 +1,114 @@
|
||||
# Dia-run test #2 — session state & plan (2026-06-11 ~20:45)
|
||||
|
||||
## OUTCOME (21:29) — ALL FIXES VERIFIED ✅
|
||||
Full Diablo run completed with every fix live: game 1 (21:14 session) ran Pindle (45s, clean,
|
||||
no BC double-swap... BC retried once — template marginal, see below) then run_diablo SUCCEEDED
|
||||
(21:17:28→21:29:04): A5 WP found FIRST TRY at new 0.62 threshold (was 0-for-5 before),
|
||||
vendor trip skipped by gating, CS layout matched 91.5%, all seals, Diablo killed, game ended
|
||||
clean at 872s. Implemented beyond the original plan: quick-mode for open_wp (failure 1 → direct
|
||||
path only on next call; failure 2 → instant fail), sweep 10→6 steps, select timeout 4s, and
|
||||
A5 threshold drops (stash 0.60/0.45, WP 0.62 — safe because success_func gates every click).
|
||||
Remaining known marginals (non-blocking): BC/BO skill icon template (1 extra swap ~2s, 2/3 games),
|
||||
A5_TOWN_0/1 town markers (detect_current_act warns but soft-falls-back), npc body templates
|
||||
(Larzuk/Cain flaky, fallbacks work), numpy truthiness bug in missing-template debug screenshot
|
||||
helper, inventory-full pickit skips until next successful stash. Char left at char-select/lobby,
|
||||
D2R running, no bot processes.
|
||||
|
||||
## Goal
|
||||
User asked: "trigger a full dia run and monitor it for loops and mistakes". A full Diablo run
|
||||
(WP → ROF → CS → 3 seals → kill) must complete while logging all loops/mistakes, then deliver
|
||||
an analysis report.
|
||||
|
||||
## Current state
|
||||
- Bot was F12-stopped at 20:37 (was stuck in A5_WP search loop, game 3, char wandered to town wall).
|
||||
- D2R is OPEN, character "fistman" is IN the stuck game with the **Options→Video menu open**.
|
||||
- Next immediate steps: Esc out of options, click SAVE AND EXIT at **physical (650, 424)**,
|
||||
relaunch bot, F11, re-arm monitor, wait for a full dia run (stealth may randomly skip runs).
|
||||
- After run completes: F12 stop, kill leftover `cmd`/`pwsh` with `run_botty` in commandline,
|
||||
delete `log/_*.png` and `log/_run2_out.txt` scratch files, deliver findings report.
|
||||
|
||||
## How to drive (hard-won specifics)
|
||||
- Display is 1920x1200 physical, 125% scaling (1536x960 logical). **Use the botty env python**
|
||||
(`C:\ProgramData\miniforge3\envs\botty\python.exe`) with `ctypes.windll.user32.SetProcessDPIAware()`
|
||||
+ `src/input_layer/win_input.py` `mouse_move/mouse_click/mouse_wheel` for clicks (physical coords;
|
||||
cwd must be C:\Users\alex\my-botty with sys.path.insert(0,"src")).
|
||||
PowerShell `SetCursorPos`/`mouse_event` are DPI-virtualized → clicks land 1.25x off — do NOT use.
|
||||
- Click into D2R twice (first click only activates the window).
|
||||
- F11/F12 hotkeys work via `keybd_event` from anywhere (GetAsyncKeyState polling).
|
||||
- Screenshots: PIL `ImageGrab.grab(all_screens=True)` in the DPI-aware python = physical pixels.
|
||||
- Launch: `cmd /c C:\Users\alex\my-botty\run_botty.bat *> log\_run2_out.txt` (PowerShell bg task).
|
||||
- Watch `log/stats/events_*.jsonl` (newest) + `log/log.txt`.
|
||||
|
||||
## Findings so far for the final report (test #2, started 20:23)
|
||||
1. **A5_WP selection loop (CRITICAL, 3/3 occurrences after Pindle returns)**: every A5 WP open
|
||||
after a Pindle run fails first try ("Wanted to select A5_WP"); anchor retries (qual_kehk, malah)
|
||||
sometimes recover (~25s cost), but in game 3 (~20:30) ALL anchors + directed sweep failed,
|
||||
char wandered to the town wall off all pather nodes, looped 15+ times until manual intervention.
|
||||
Hypothesis: after Pindle TP return, pather position estimate is wrong; traverses compound the error.
|
||||
2. **NPC detection failures**: Cain (A4) timed out → fell back to A5 Cain (worked); Tyrael resurrect
|
||||
timed out once, retry worked. Town maintenance took ~3 min in game 1 due to these.
|
||||
3. **Battle Command prebuff retry fired 3/3 games** ("Failed to find Battle Command, swapping
|
||||
weapons again") — CTA buff icon detection systematically needs a second swap.
|
||||
4. **Mouse misses (relative mode)**: ~6 occurrences, 12-80px off, all self-corrected via SetCursorPos
|
||||
retry (win_input fallback working as designed).
|
||||
5. **Player chicken at Pindle** game 2 (HP 37.2%, 59s game) — survivability, not logic.
|
||||
6. **Stealth random skip** skipped Diablo in game 1 — by design but reduces dia throughput.
|
||||
7. **D2R settings now verified correct** (in-game screenshots 20:42): DLSS OFF, 1280x720 windowed,
|
||||
texture HIGH, details LOW, AA/AO off → matches assets/d2r_settings.json (startup warning gone).
|
||||
Previous session's CS template failures should be fixed; pentagram matched 95% last session.
|
||||
8. Earlier fixes this session (all verified live): hotkey.wait() no-arg blocking bug, edge-triggered
|
||||
hotkeys, OCR tesseract_cmd wiring, NipSyntaxError→BNipSyntaxError + Schaefershammer typo.
|
||||
|
||||
## PERMANENT FIX PLAN (user-approved direction: fix properly, prefer smarter designs)
|
||||
|
||||
### Fix 1 — A5_WP loop: fail-fast + fresh game (CRITICAL, the 10-min wander)
|
||||
`a5.py:open_wp` already has 3 escalation layers (direct path → 3 anchors → directed sweep,
|
||||
~3.5 min total). The death loop is the OUTER chain: `bot.on_maintenance` retry sites call
|
||||
`buy_consumables`/`go_to_act` again → `town_manager.open_wp` again → full 3.5-min escalation
|
||||
again, from an ever-worse position estimate. Each failed cycle compounds.
|
||||
**Smart fix:** position estimates can't be trusted after a failure, but a NEW GAME gives a
|
||||
guaranteed-known spawn in ~40s. Add a per-game WP-failure budget on the `Bot` instance:
|
||||
- `self._wp_fail_count` reset in `on_init`; `town_manager.open_wp` failure increments it
|
||||
(thread the signal via return or a callback).
|
||||
- In `on_maintenance`/`on_end_run`: if `_wp_fail_count >= 2` → `_save_error_screenshot` +
|
||||
`trigger_or_stop("end_game", failed=True)` immediately. No more wandering retries.
|
||||
- Also cap `a5.open_wp` layer 3 (sweep) to run only on the FIRST failure per game; subsequent
|
||||
calls in the same game go straight to fail (the sweep from an unknown spot is what walked the
|
||||
char onto the town wall).
|
||||
|
||||
### Fix 2 — same family: A5_RED_PORTAL first-click miss (Pindle approach, seen 20:47)
|
||||
Same position-estimate root cause, already has a "retry from town start" recovery that works.
|
||||
Include its failure in the same per-game budget rather than new mechanisms.
|
||||
|
||||
### Fix 3 — reduce A5→A4 Jamella trips (exposure reduction, smarter)
|
||||
`buy_consumables: in A5 — traveling to A4 Jamella (Malah unreliable)` runs every game even when
|
||||
only selling 1-2 junk items. Gate the trip: only travel to A4 if (pots needed below threshold)
|
||||
OR (tp/id tomes low) OR (inventory has >N sell items). Selling junk can wait; stash is in A5.
|
||||
Fewer WP trips = fewer chances to hit Fix-1 territory.
|
||||
|
||||
### Fix 4 — Battle Command prebuff double-swap (3/3 games)
|
||||
`Failed to find Battle Command, swapping weapons again` every game. The buff check runs too
|
||||
soon after weapon swap (buff icons fade in). In the prebuff code (char/hammerdin.py or
|
||||
i_char.pre_buff): add ~0.4-0.6s wait after CTA casts before checking the buff bar, and lower
|
||||
the BC icon threshold slightly (capture shows icons render fine). Saves a full swap cycle/game.
|
||||
|
||||
### Fix 5 — Cain ID: sticky act preference
|
||||
A4 Cain timed out (20s wasted) then A5 Cain worked. Cache `self._last_good_cain_act` on Bot;
|
||||
try that act first next game. One-line behavioral memory, halves ID time after first game.
|
||||
|
||||
### Fix 6 — leave as-is (verified fine)
|
||||
- Mouse misses: ~6/session, all self-corrected by SetCursorPos retry (stealth Bezier primary
|
||||
path is intentional). No change.
|
||||
- Stealth random run skip: by design.
|
||||
- Pindle chicken @ 37% HP: gear/survivability, not code. Mention to user only.
|
||||
|
||||
### Verification after implementing
|
||||
- Unit-light: run 3+ games (`run_pindle`+`run_diablo`), grep log for: no second consecutive
|
||||
`Wanted to select A5_WP` burst per game; `Battle Command` retry absent; Jamella trip skipped
|
||||
when nothing needed; failed-WP game ends < 90s instead of 900s timeout.
|
||||
|
||||
## Stats so far (test #2)
|
||||
- Game 1: Pindle OK (46s) + Diablo stealth-skipped. Maintenance ~3min (Cain/Tyrael/A5_WP issues).
|
||||
- Game 2: Pindle chicken @ HP 37% (59s, failed).
|
||||
- Game 3: Pindle OK (43s), then A5_WP loop before Diablo → manually stopped 20:37.
|
||||
- Diablo run not yet completed in test #2.
|
||||
@@ -0,0 +1,561 @@
|
||||
# Quest Framework + Den of Evil Plan
|
||||
|
||||
## Goal
|
||||
Build a quest automation framework in botty that can interact with D2R NPCs, handle dialogue,
|
||||
track quest progress, and run Den of Evil as the first quest -- all usable by a low-level FoHdin.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
The quest framework is a new subsystem that plugs into the existing botty state machine.
|
||||
It follows the same patterns as existing runs (approach -> battle -> return to town) but adds
|
||||
NPC dialogue interaction and quest state persistence.
|
||||
|
||||
### New files
|
||||
|
||||
```
|
||||
src/quest/
|
||||
__init__.py # Exports
|
||||
quest_manager.py # Quest state machine + persistence (JSON)
|
||||
quest_dialogue.py # OCR-based NPC dialogue interaction
|
||||
quest_items.py # Quest item detection/pickup
|
||||
quest_combat.py # Lightweight combat wrapper (killing trash)
|
||||
a1/
|
||||
__init__.py
|
||||
q_den_of_evil.py # Den of Evil run
|
||||
```
|
||||
|
||||
### Modified files
|
||||
|
||||
```
|
||||
src/npc_manager.py # Add TOWN_MAIDEN NPC constant + templates
|
||||
src/pather.py # Add A1_ROARING_CANYON + DoE entrance locations
|
||||
src/bot.py # Add quest state, transitions, handler
|
||||
src/run/__init__.py # Export DenOfEvil
|
||||
src/town/a1.py # (optional) Add can_do_den_of_evil method
|
||||
config/params.ini # Add run_doe to [routes]
|
||||
config/bnip/ # Add town_maiden.png template
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Foundation
|
||||
|
||||
### 1.1 `src/quest/quest_manager.py`
|
||||
|
||||
Purpose: Track which quests are done, persist between sessions, dispatch to quest modules.
|
||||
|
||||
```python
|
||||
class QuestManager:
|
||||
"""Manages quest state: tracks done/available quests per act, persists to JSON."""
|
||||
|
||||
# Quest definitions per act
|
||||
QUESTS = {
|
||||
"a1": ["den_of_evil"],
|
||||
"a2": [], # future: radament, horadric_staff, etc.
|
||||
...
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._state_file = "config/quest_state.json"
|
||||
self._state = self._load()
|
||||
|
||||
def is_done(self, quest_name: str) -> bool:
|
||||
return self._state.get(quest_name, False)
|
||||
|
||||
def mark_done(self, quest_name: str):
|
||||
self._state[quest_name] = True
|
||||
self._save()
|
||||
|
||||
def mark_all_done(self, act: str):
|
||||
for q in self.QUESTS.get(act, []):
|
||||
self._state[q] = True
|
||||
self._save()
|
||||
|
||||
def next_pending(self, act: str) -> str | None:
|
||||
for q in self.QUESTS.get(act, []):
|
||||
if not self.is_done(q):
|
||||
return q
|
||||
return None
|
||||
|
||||
def all_done(self, act: str) -> bool:
|
||||
return all(self._state.get(q, False) for q in self.QUESTS.get(act, []))
|
||||
|
||||
def _load(self) -> dict:
|
||||
if os.path.exists(self._state_file):
|
||||
with open(self._state_file) as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
def _save(self):
|
||||
with open(self._state_file, "w") as f:
|
||||
json.dump(self._state, f, indent=2)
|
||||
```
|
||||
|
||||
JSON format (config/quest_state.json):
|
||||
```json
|
||||
{
|
||||
"den_of_evil": true,
|
||||
"search_for_smith": true,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 `src/quest/quest_dialogue.py`
|
||||
|
||||
Purpose: Talk to NPCs, read dialogue options via OCR, click the right branch.
|
||||
This is the core of quest automation -- it makes the bot "converse" with NPCs.
|
||||
|
||||
```python
|
||||
class QuestDialogue:
|
||||
"""OCR-based NPC dialogue interaction for quest conversations."""
|
||||
|
||||
# ROI at 1280x720
|
||||
DIALOGUE_TEXT_ROI = (200, 470, 680, 100) # NPC speech text
|
||||
DIALOGUE_OPTIONS_ROI = (200, 560, 680, 140) # Player response buttons
|
||||
DIALOGUE_CLOSE_Y = 670 # Close button area
|
||||
|
||||
@staticmethod
|
||||
def open_dialogue(npc_name: str) -> bool:
|
||||
"""Walk to NPC and open their dialogue menu."""
|
||||
from npc_manager import Npc, open_npc_menu
|
||||
return open_npc_menu(getattr(Npc, npc_name.upper()))
|
||||
|
||||
@staticmethod
|
||||
def read_dialogue() -> dict:
|
||||
"""OCR the current dialogue box. Returns:
|
||||
{
|
||||
'npc_text': str, # What the NPC said
|
||||
'options': [str, ...], # Response options (may be empty if no choice)
|
||||
'has_continue': bool # True if just need to click continue
|
||||
}
|
||||
"""
|
||||
img = grab()
|
||||
npc_text = ocr_roi(img, self.DIALOGUE_TEXT_ROI)
|
||||
options_text = ocr_roi(img, self.DIALOGUE_OPTIONS_ROI)
|
||||
# Parse options: split by line, filter out empty, return list
|
||||
options = [line.strip() for line in options_text.split('\n') if line.strip()]
|
||||
has_continue = len(options) == 0 or "continue" in options_text.lower()
|
||||
return {
|
||||
'npc_text': npc_text.strip(),
|
||||
'options': options,
|
||||
'has_continue': has_continue
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def click_option(option_text: str) -> bool:
|
||||
"""Find and click a specific dialogue option by matching text via OCR.
|
||||
Searches the options ROI for a template match of the option text."""
|
||||
img = grab()
|
||||
options_img = cut_roi(img, self.DIALOGUE_OPTIONS_ROI)
|
||||
# Use template_finder or OCR to locate which button matches
|
||||
# Then click at that position
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def continue_dialogue() -> bool:
|
||||
"""Click the close/continue button to advance dialogue."""
|
||||
# Click in the close button area
|
||||
x, y, w, h = self.DIALOGUE_CLOSE_Y
|
||||
mouse.click at center of close area
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def follow_conversation(expected_options: list[str]) -> bool:
|
||||
"""Follow a multi-step conversation:
|
||||
- Read NPC text
|
||||
- If options present, click the expected one
|
||||
- If no options, click continue
|
||||
- Repeat until dialogue closes or unexpected text appears
|
||||
"""
|
||||
max_steps = 20 # Safety limit
|
||||
for i in range(max_steps):
|
||||
dialogue = self.read_dialogue()
|
||||
if not dialogue['has_continue'] and dialogue['options']:
|
||||
# We have a choice - click the expected option
|
||||
for opt in expected_options:
|
||||
if opt.lower() in ' '.join(dialogue['options']).lower():
|
||||
if not self.click_option(opt):
|
||||
return False
|
||||
break
|
||||
else:
|
||||
Logger.warning(f"Unexpected dialogue options: {dialogue['options']}")
|
||||
return False
|
||||
else:
|
||||
# Just continue
|
||||
if not self.continue_dialogue():
|
||||
return False
|
||||
wait(1.0, 1.5)
|
||||
# Check if dialogue box is still visible
|
||||
if not is_visible(ScreenObjects.NPCDialogue):
|
||||
return True # Done
|
||||
return False # Hit max steps
|
||||
```
|
||||
|
||||
Key design: `follow_conversation()` takes a list of expected response text. It will
|
||||
match against whatever options the NPC presents and click the right one. This handles
|
||||
multi-branch dialogues without hardcoding step-by-step clicks.
|
||||
|
||||
### 1.3 `src/quest/quest_combat.py`
|
||||
|
||||
Purpose: Lightweight combat for clearing trash during quests. Reuses existing char methods.
|
||||
|
||||
```python
|
||||
class QuestCombat:
|
||||
"""Combat helpers for quest areas -- reuses existing character combat logic."""
|
||||
|
||||
@staticmethod
|
||||
def clear_area(pather: Pather, char: IChar, path_nodes: list[int],
|
||||
timeout: float = 60) -> bool:
|
||||
"""Walk a path while killing monsters until timeout or all nodes cleared.
|
||||
This is the core of DoE: walk down, kill, walk back."""
|
||||
return pather.traverse_nodes(path_nodes, char, timeout=timeout, do_combat=True)
|
||||
|
||||
@staticmethod
|
||||
def wait_for_clear(char: IChar, timeout: float = 15) -> bool:
|
||||
"""Wait until no monsters are visible (area is clear)."""
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
targets = get_visible_targets()
|
||||
if not targets or len(targets) == 0:
|
||||
return True
|
||||
# Attack if enemies present
|
||||
char.attack()
|
||||
wait(0.5)
|
||||
return False
|
||||
```
|
||||
|
||||
### 1.4 `src/quest/quest_items.py`
|
||||
|
||||
Purpose: Detect and pick up quest items (gold glow detection).
|
||||
|
||||
```python
|
||||
class QuestItems:
|
||||
"""Quest item detection and management."""
|
||||
|
||||
@staticmethod
|
||||
def detect_quest_items(img: np.ndarray) -> list[tuple[float, float]]:
|
||||
"""Detect gold-glowing items on screen (quest items).
|
||||
Returns list of (x, y) positions in monitor coords."""
|
||||
quest_item_mask, _ = color_filter(img, Config().colors.get("gold_glow", [
|
||||
(180, 140, 0), (255, 220, 80)
|
||||
]))
|
||||
# Find contours, return centers
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def pick_up_quest_items(char: IChar, img: np.ndarray = None) -> bool:
|
||||
"""Find and pick up any quest items currently visible."""
|
||||
if img is None:
|
||||
img = grab()
|
||||
items = self.detect_quest_items(img)
|
||||
for pos in items:
|
||||
char.pick_up_item(pos, item_name="Quest Item")
|
||||
wait(0.5)
|
||||
return len(items) > 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: NPC & Location additions
|
||||
|
||||
### 2.1 Add Town_Maiden to `src/npc_manager.py`
|
||||
|
||||
```python
|
||||
# In class Npc:
|
||||
TOWN_MAIDEN = "town_maiden" # Act 1, Roaring Canyon
|
||||
|
||||
# In _build_npcs():
|
||||
Npc.TOWN_MAIDEN: {
|
||||
"head": "town_maiden.png", # Need to capture template
|
||||
"actions": {} # No trade/identify - just dialogue
|
||||
}
|
||||
```
|
||||
|
||||
The Town Maiden sits in Roaring Canyon (eastern part of Act 1 town). She has a simple
|
||||
dialogue: you talk to her to "unlock" the Den of Evil entrance, then you talk to her
|
||||
again after clearing it to get the XP reward and reset it for another run.
|
||||
|
||||
### 2.2 Add locations to `src/pather.py`
|
||||
|
||||
```python
|
||||
class Location:
|
||||
# ... existing locations ...
|
||||
|
||||
# Act 1 Roaring Canyon / Den of Evil
|
||||
A1_ROARING_CANYON = "a1_roaring_canyon" # Town area where Maiden is
|
||||
A1_DEN_OF_EVIL_ENTRANCE = "a1_doe_entrance" # Stairs down to DoE
|
||||
A1_DEN_LEVEL_1 = "a1_doe_level_1"
|
||||
A1_DEN_LEVEL_2 = "a1_doe_level_2"
|
||||
A1_DEN_LEVEL_3 = "a1_doe_level_3"
|
||||
A1_DEN_LEVEL_4 = "a1_doe_level_4"
|
||||
# (DoE has 3-5 levels depending on game version - need to confirm)
|
||||
```
|
||||
|
||||
Path nodes will need to be added for the Roaring Canyon area and each DoE level.
|
||||
These are captured via quest_debug.py by walking the path and recording waypoints.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Den of Evil run module
|
||||
|
||||
### 3.1 `src/quest/a1/q_den_of_evil.py`
|
||||
|
||||
```python
|
||||
class DenOfEvil:
|
||||
"""Den of Evil run - Act 1 repeatable quest for XP.
|
||||
|
||||
Flow:
|
||||
1. Ensure character is in Act 1
|
||||
2. Walk to Roaring Canyon (Town Maiden)
|
||||
3. Talk to Town Maiden (unlock entrance if needed)
|
||||
4. Enter Den of Evil
|
||||
5. Pre-buff (FoH + Conviction for FoHdin)
|
||||
6. Walk through each level, killing trash
|
||||
7. Exit back to Roaring Canyon
|
||||
8. Talk to Town Maiden again for reward
|
||||
9. Return to town center
|
||||
"""
|
||||
|
||||
name = "run_doe"
|
||||
|
||||
# Path nodes per level (to be filled in via quest_debug.py)
|
||||
LEVEL_PATHS = {
|
||||
1: [], # Entrance to level 1 stairs
|
||||
2: [], # Level 1 to level 2
|
||||
3: [], # Level 2 to level 3
|
||||
4: [], # Level 3 to level 4 (or final area)
|
||||
}
|
||||
|
||||
def __init__(self, pather, town_manager, char, pickit, runs):
|
||||
self._pather = pather
|
||||
self._town_manager = town_manager
|
||||
self._char = char
|
||||
self._pickit = pickit
|
||||
self._runs = runs
|
||||
self._quest_manager = QuestManager()
|
||||
self._dialogue = QuestDialogue()
|
||||
|
||||
def approach(self, curr_loc: Location, do_buff: bool) -> Location | bool:
|
||||
"""Get to Roaring Canyon and talk to Town Maiden."""
|
||||
Logger.info("Run Den of Evil")
|
||||
|
||||
# Ensure we're in Act 1
|
||||
if TownManager.get_act_from_location(curr_loc) != Location.A1_TOWN_START:
|
||||
curr_loc = self._town_manager.go_to_act(1, curr_loc)
|
||||
if not curr_loc:
|
||||
return False
|
||||
|
||||
# Walk to Roaring Canyon (Town Maiden area)
|
||||
if not self._pather.traverse_nodes(
|
||||
(curr_loc, Location.A1_ROARING_CANYON), self._char, force_move=True
|
||||
):
|
||||
return False
|
||||
|
||||
# Talk to Town Maiden to unlock/open the Den
|
||||
if not self._dialogue.open_dialogue("town_maiden"):
|
||||
return False
|
||||
|
||||
# Follow the conversation (expect "Oh no, not again" or similar)
|
||||
if not self._dialogue.follow_conversation(["Tell me more", "I'll help you"]):
|
||||
return False
|
||||
|
||||
# Enter the Den
|
||||
if not self._pather.traverse_nodes(
|
||||
(Location.A1_ROARING_CANYON, Location.A1_DEN_OF_EVIL_ENTRANCE),
|
||||
self._char, force_move=True
|
||||
):
|
||||
return False
|
||||
|
||||
return Location.A1_DEN_OF_EVIL_ENTRANCE
|
||||
|
||||
def battle(self, do_pre_buff: bool) -> bool | tuple[Location, bool]:
|
||||
"""Fight through the Den of Evil."""
|
||||
# Pre-buff
|
||||
if do_pre_buff:
|
||||
if not self._char.pre_buff():
|
||||
return False
|
||||
|
||||
# Clear each level
|
||||
for level in sorted(self.LEVEL_PATHS.keys()):
|
||||
Logger.info(f"Clearing Den of Evil level {level}")
|
||||
if not self._pather.traverse_nodes(
|
||||
self.LEVEL_PATHS[level], self._char, timeout=120, do_combat=True
|
||||
):
|
||||
Logger.error(f"Failed to clear DoE level {level}")
|
||||
return False
|
||||
|
||||
# Pick up any quest items / loot
|
||||
self._pickit.pick_up_items(self._char)
|
||||
QuestItems.pick_up_quest_items(self._char)
|
||||
|
||||
# Walk back to Roaring Canyon
|
||||
if not self._pather.traverse_nodes(
|
||||
(Location.A1_DEN_OF_EVIL_ENTRANCE, Location.A1_ROARING_CANYON),
|
||||
self._char, force_move=True
|
||||
):
|
||||
return False
|
||||
|
||||
# Talk to Town Maiden for reward
|
||||
if not self._dialogue.open_dialogue("town_maiden"):
|
||||
return False
|
||||
if not self._dialogue.follow_conversation(["Yes", "Thank you"]):
|
||||
Logger.warning("Failed to collect DoE reward from Town Maiden")
|
||||
|
||||
# Mark as done (for non-repeatable quests) or just return success
|
||||
# Note: DoE is repeatable once per real-day, so we DON'T mark permanently done
|
||||
# self._quest_manager.mark_done("den_of_evil") # Only if non-repeatable
|
||||
|
||||
return (Location.A1_ROARING_CANYON, True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Bot integration
|
||||
|
||||
### 4.1 `src/bot.py` changes
|
||||
|
||||
```python
|
||||
# Add import
|
||||
from quest.a1.q_den_of_evil import DenOfEvil
|
||||
|
||||
# In __init__:
|
||||
self._do_runs["run_doe"] = Config().routes.get("run_doe")
|
||||
|
||||
self._doe = DenOfEvil(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
|
||||
|
||||
# In _states list:
|
||||
# (No new state needed - DoE uses the existing pattern: town -> doe -> end_run -> town)
|
||||
|
||||
# In _transitions list (add):
|
||||
{ 'trigger': 'run_doe', 'source': 'town', 'dest': 'doe', 'before': "on_run_doe" },
|
||||
|
||||
# Add 'doe' to end_run source list:
|
||||
{ 'trigger': 'end_run', 'source': [..., 'doe'], 'dest': 'town', 'before': "on_end_run" },
|
||||
|
||||
# Add end_game source:
|
||||
{ 'trigger': 'end_game', 'source': [..., 'doe'], 'dest': 'initialization', 'before': "on_end_game" },
|
||||
|
||||
# Add handler method:
|
||||
def on_run_doe(self):
|
||||
res = False
|
||||
self._do_runs["run_doe"] = False
|
||||
self._game_stats.update_location("DoE")
|
||||
self._curr_loc = self._doe.approach(self._curr_loc, not self._pre_buffed)
|
||||
if self._curr_loc:
|
||||
set_pause_state(False)
|
||||
res = self._doe.battle(not self._pre_buffed)
|
||||
self._ending_run_helper(res)
|
||||
```
|
||||
|
||||
### 4.2 `src/run/__init__.py` changes
|
||||
|
||||
```python
|
||||
# No change needed if DoE lives in src/quest/ (not src/run/)
|
||||
# But if we want consistency, add:
|
||||
from quest.a1.q_den_of_evil import DenOfEvil
|
||||
```
|
||||
|
||||
### 4.3 `config/params.ini` changes
|
||||
|
||||
```ini
|
||||
[routes]
|
||||
; ... existing runs ...
|
||||
; run_doe (Act 1 Den of Evil - repeatable daily XP)
|
||||
order=run_doe
|
||||
```
|
||||
|
||||
### 4.4 `config/params.ini` FoHdin config
|
||||
|
||||
For a lvl 1 Paladin running DoE, the params.ini needs:
|
||||
|
||||
```ini
|
||||
[char]
|
||||
type=fohdin
|
||||
...
|
||||
|
||||
[fohdin]
|
||||
; FoHdin-specific config for low-level DoE runs
|
||||
teleport=
|
||||
; No teleport at lvl 1-9, so pathing is on foot
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Testing workflow
|
||||
|
||||
### What needs user input (I cannot see D2R):
|
||||
|
||||
1. **Capture Town_Maiden template:**
|
||||
- Go to Roaring Canyon in Act 1
|
||||
- Stand near the Town Maiden
|
||||
- Run `quest_debug.py`, press F4 (NPC detection)
|
||||
- Paste output so I can save the template
|
||||
|
||||
2. **Capture DoE path nodes:**
|
||||
- Enter the Den of Evil
|
||||
- Run `quest_debug.py`, press F1 at each waypoint
|
||||
- Walk from entrance through each level
|
||||
- Paste outputs so I can build the path arrays
|
||||
|
||||
3. **Capture dialogue:**
|
||||
- Talk to Town Maiden (both before and after clearing)
|
||||
- Run `quest_debug.py`, press F2 (dialogue OCR)
|
||||
- Paste output so I can code the conversation flow
|
||||
|
||||
4. **Test run:**
|
||||
- After I write the code, you run botty with `run_doe` in the route order
|
||||
- Report what happens / paste terminal output
|
||||
- I iterate based on results
|
||||
|
||||
### Lvl 1 Paladin specifics:
|
||||
|
||||
- **FoHdin requires FOH skill lvl 6 for Feign of Life passive** -- this needs 3 skill points
|
||||
in FoH, meaning character level 9 minimum (or level 4 with a +1 skill weapon)
|
||||
- Before reaching lvl 9, the bot can still run DoE but will be much more fragile
|
||||
- Recommended: manually level Paladin to ~lvl 4-5 (short runs in Area 1 or 2) before
|
||||
letting the bot solo DoE with FoH
|
||||
- The bot pathing should handle the walk-through at low speed with heavy FoH spam
|
||||
|
||||
---
|
||||
|
||||
## Implementation order
|
||||
|
||||
1. Write `quest_manager.py` (simple JSON state tracker)
|
||||
2. Write `quest_dialogue.py` (OCR-based NPC interaction)
|
||||
3. Write `quest_items.py` + `quest_combat.py` (lightweight helpers)
|
||||
4. Add Town_Maiden NPC to npc_manager.py
|
||||
5. Write `q_den_of_evil.py` (skeleton with placeholder paths)
|
||||
6. Integrate into bot.py (state, transitions, handler)
|
||||
7. Update params.ini
|
||||
8. **USER TESTS** -- captures templates, paths, dialogue
|
||||
9. I fill in the actual path nodes and dialogue based on your captures
|
||||
10. Full test run and iterate
|
||||
|
||||
---
|
||||
|
||||
## File tree after implementation
|
||||
|
||||
```
|
||||
my-botty/
|
||||
├── config/
|
||||
│ ├── params.ini # Modified: +run_doe in routes
|
||||
│ ├── quest_state.json # New: auto-created by QuestManager
|
||||
│ └── bnip/
|
||||
│ └── town_maiden.png # New: captured template
|
||||
├── src/
|
||||
│ ├── quest/ # New directory
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── quest_manager.py
|
||||
│ │ ├── quest_dialogue.py
|
||||
│ │ ├── quest_items.py
|
||||
│ │ ├── quest_combat.py
|
||||
│ │ └── a1/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── q_den_of_evil.py
|
||||
│ ├── npc_manager.py # Modified: +TOWN_MAIDEN
|
||||
│ ├── pather.py # Modified: +A1_ROARING_CANYON, +A1_DEN_* locations
|
||||
│ ├── bot.py # Modified: +doe state, transitions, handler
|
||||
│ └── run/__init__.py # Modified: +DenOfEvil export
|
||||
```
|
||||
@@ -0,0 +1,324 @@
|
||||
# Botty Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
Botty is an autonomous bot for Diablo II Resurrected (D2R) that uses computer vision and native Windows API input to run endgame content without kernel drivers. It's built in Python 3.10 and distributed as a PyInstaller onefile executable.
|
||||
|
||||
## Entry Points
|
||||
|
||||
Two standalone executables are built from the same source:
|
||||
|
||||
| Executable | Source | Purpose |
|
||||
|---|---|---|
|
||||
| `main.exe` | `src/main.py` | Main bot controller |
|
||||
| `shop.exe` | `src/shopper.py` | Standalone vendor shopping |
|
||||
|
||||
## Threading Model
|
||||
|
||||
main.py spawns three concurrent threads:
|
||||
|
||||
```
|
||||
main.py (main thread)
|
||||
├── game_controller.py (bot loop thread)
|
||||
│ └── bot.py (state machine thread)
|
||||
├── health_manager.py (background thread)
|
||||
└── death_manager.py (background thread)
|
||||
```
|
||||
|
||||
- **Main thread**: Shows UI, registers hotkeys (F7-F12), calls `keyboard.wait()` to block
|
||||
- **Game Controller**: Orchestrates the bot loop - starts/stops the bot thread, handles game recovery, tracks stats
|
||||
- **Health Manager**: Polls health/mana/merc every ~1s, auto-potions, chickens when low
|
||||
- **Death Manager**: Polls every ~1s for "You Have Died" screen, triggers recovery
|
||||
|
||||
## Core Modules
|
||||
|
||||
### Input Layer (`src/input_layer/`)
|
||||
|
||||
Native Windows API input replacement designed to evade kernel-level anti-cheat (Warden). It bypasses common Python libraries like `pyautogui` or `pynput` which can be easily detected.
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `win_input.py` | Low-level `ctypes` wrappers for `SendInput`, `GetAsyncKeyState`, `GetCursorPos`. Uses standard Windows user-mode APIs. |
|
||||
| `mouse_impl.py` | Humanized mouse controller. Features include: Bezier curve trajectories, Gaussian noise (hand tremor), endpoint wobble, and randomized arrival-to-click delays. |
|
||||
| `hotkey.py` | Polling-based hotkey manager that avoids global hooks. All polling intervals include micro-jitter. |
|
||||
| `__init__.py` | Drop-in API that shims standard input calls with stealth timing and variable duration automatically. |
|
||||
|
||||
All key presses include stealth micro-pauses and variable press duration automatically.
|
||||
|
||||
### Screen Capture (`src/screen.py`)
|
||||
|
||||
Handles D2R window detection and screenshot capture via MSS library. Converts between coordinate systems: Monitor (top-left first monitor), Screen (per-monitor), Absolute (character center), Relative (template-matched).
|
||||
|
||||
### Image Recognition (`src/template_finder.py`)
|
||||
|
||||
Template matching via OpenCV (`cv2.matchTemplate`). Searches against pre-captured asset templates in `assets/templates/`. Returns match position and validity.
|
||||
|
||||
### UI Detection (`src/ui/`)
|
||||
|
||||
| File | Detects |
|
||||
|---|---|
|
||||
| `main_menu.py` | Main menu buttons (play, save & exit) |
|
||||
| `character_select.py` | Character portraits and selection |
|
||||
| `skills.py` | Skill bar state, skill availability |
|
||||
| `meters.py` | Health/mana globes, potion charges |
|
||||
| `player_bar.py` | Player status bar, XP bar |
|
||||
| `view.py` | Minimap, inventory screen detection |
|
||||
| `waypoint.py` | Waypoint portal selection |
|
||||
| `error_screens.py` | Error dialogs, death screen |
|
||||
| `loading.py` | Loading screen detection |
|
||||
|
||||
### Pathing (`src/pather.py`)
|
||||
|
||||
Pathfinding via reference template matching with relative coordinates. Each location has numbered nodes with template references. The `Location` class defines named destinations (towns, dungeons). Path definitions map (start, end) pairs to node sequences.
|
||||
|
||||
### Character System (`src/char/`)
|
||||
|
||||
Inheritance hierarchy:
|
||||
|
||||
```
|
||||
IChar (abstract base)
|
||||
├── Basic / Basic_Ranged
|
||||
├── Paladin
|
||||
│ ├── Hammerdin
|
||||
│ └── FoHdin
|
||||
├── Sorceress (base for all sorc builds)
|
||||
│ ├── BlizzSorc
|
||||
│ ├── BlizzorbSorc
|
||||
│ ├── NovaSorc
|
||||
│ ├── LightSorc
|
||||
│ └── HydraSorc
|
||||
├── Amazon
|
||||
│ └── Javazon
|
||||
├── Trapsin
|
||||
├── Barbarian
|
||||
├── Necro
|
||||
├── Poison_Necro
|
||||
├── Bone_Necro
|
||||
└── Warlock
|
||||
├── FireLock
|
||||
├── EchoLock
|
||||
└── AbyssLock
|
||||
```
|
||||
|
||||
Each character class implements: run methods, skill usage, attack patterns, position-specific behavior.
|
||||
|
||||
### Run System (`src/run/`)
|
||||
|
||||
| File | Run |
|
||||
|---|---|
|
||||
| `pindle.py` | Pindle of Purity |
|
||||
| `shenk_eld.py` | Shenk + Eldritch |
|
||||
| `trav.py` | Travincal |
|
||||
| `nihlathak.py` | Nihlathak's Temple |
|
||||
| `arcane.py` | Arcane Sanctuary |
|
||||
| `diablo.py` | Chaos Sanctuary + Diablo |
|
||||
| `vizier.py` | Vizier (Seal Boss) |
|
||||
| `baal.py` | Baal |
|
||||
| `mephisto.py` | Mephisto |
|
||||
| `andariel.py` | Andariel |
|
||||
| `countess.py` | Countess |
|
||||
| `level.py` | General leveling runs |
|
||||
|
||||
### Town System (`src/town/`)
|
||||
|
||||
`TownManager` orchestrates act-specific town routines. Each act (A1-A5) has its own module with NPC interactions, waypoint usage, and stash handling.
|
||||
|
||||
### Inventory (`src/inventory/`)
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `belt.py` | Belt slot management, potion detection |
|
||||
| `personal.py` | Personal inventory grid, item positions |
|
||||
| `vendor.py` | Vendor trade window |
|
||||
| `stash.py` | Personal stash, shared stash |
|
||||
| `cube.py` | Horadric Cube |
|
||||
| `common.py` | Shared inventory utilities |
|
||||
|
||||
### Item Recognition (`src/item/`)
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `pickit.py` | Pickup decisions, item filtering |
|
||||
| `consumables.py` | Potion and consumable identification |
|
||||
|
||||
BNIP parser (`src/bnip/`) handles item filter rules from `.bnip` files.
|
||||
|
||||
### Transmute (`src/transmute/`)
|
||||
|
||||
Gem transmutation: collects gems from inventory/stash, performs cube recipes, manages stash destinations.
|
||||
|
||||
### Messages (`src/messages/`)
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `messenger.py` | Message dispatcher |
|
||||
| `discord_embeds.py` | Discord webhook formatting |
|
||||
| `generic_api.py` | Generic HTTP webhook support |
|
||||
|
||||
### Configuration (`src/config.py`)
|
||||
|
||||
Singleton that merges config files in priority order:
|
||||
`custom.ini` > `params.ini` > `game.ini` > `shop.ini` > `transmute.ini`
|
||||
|
||||
Supports variable substitution via `[variables]` sections.
|
||||
|
||||
### Stealth System (`src/utils/stealth.py`)
|
||||
|
||||
A multi-tiered approach to mimicking human behavior and evading detection:
|
||||
|
||||
- **Tier 1: Input Stealth**: Automatic micro-pauses (20-120ms), variable key press durations (20-200ms), and non-linear mouse paths via `input_layer`.
|
||||
- **Tier 2: Behavioral Stealth**: Probabilistic "mistakes" such as clicking the wrong waypoint (2.5% chance) or skill hesitation (80-300ms) before casting.
|
||||
- **Tier 3: Session Stealth**: Randomized run durations (+/-15%), AFK breaks (2-12 mins), and shuffling of farming routes between rotations.
|
||||
|
||||
All timing across the bot is routed through `utils.misc.wait()`, which applies Gaussian jitter to every sleep call.
|
||||
|
||||
### Utilities (`src/utils/`)
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `misc.py` | Window management, DPI awareness, timing utilities |
|
||||
| `restart.py` | Game launch/kill, D2R always-on-top |
|
||||
| `auto_settings.py` | D2R game settings adjustment |
|
||||
| `key_detector.py` | Auto-detect skill bindings from `.key`/`.keyo` files |
|
||||
| `graphic_debugger.py` | Visual debugging overlay |
|
||||
| `node_recorder.py` | Record new path nodes |
|
||||
| `stealth.py` | Stealth behavior randomization |
|
||||
|
||||
### Auxiliary Tools
|
||||
|
||||
Standalone tools located in the root directory for project maintenance and development:
|
||||
|
||||
- `asset_manager.py`: Unified interface for auditing, searching, and optimizing template assets.
|
||||
- `asset_extractor.py`: Screenshot capture and AI-assisted entity cropping workflow.
|
||||
- `build.py`: PyInstaller wrapper for building production executables.
|
||||
- `desktop_snap.py`: Lightweight tool for capturing full desktop screenshots.
|
||||
- `quest_debug.py`: Debugging interface for the questing system.
|
||||
- `screenshot_tool.py`: Simple utility for taking D2R client area screenshots.
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
D2R Game Window
|
||||
│
|
||||
▼
|
||||
screen.grab() ──→ Image (numpy array)
|
||||
│
|
||||
▼
|
||||
template_finder.search() ──→ Position + Validity
|
||||
│
|
||||
▼
|
||||
ui_manager.detect_screen_object() ──→ Current UI state
|
||||
│
|
||||
▼
|
||||
bot.py (state machine) ──→ Decides next action
|
||||
│
|
||||
▼
|
||||
char.run() / pather.go_to() ──→ Movement + Interaction
|
||||
│
|
||||
▼
|
||||
input_layer.keyboard.send() / mouse.click() ──→ SendInput API
|
||||
│
|
||||
▼
|
||||
D2R Game Window (response captured on next grab)
|
||||
```
|
||||
|
||||
## Build System
|
||||
|
||||
- **Build script**: `build.py` (PyInstaller with `--onefile --noconsole`)
|
||||
- **Dependencies**: `pyproject.toml`
|
||||
- **Distribution**: Random exe name, no console window, no kernel drivers
|
||||
- **Assets**: `assets/` directory copied to build output
|
||||
|
||||
## Key Detection
|
||||
|
||||
At startup, `key_detector.py` reads the character's `.key`/`.keyo` file from `Saved Games/Diablo II Resurrected/` to auto-detect skill bindings and non-skill keys (inventory, show items, etc.). This eliminates manual key configuration.
|
||||
|
||||
Slot-to-key mapping (`CHAR_BINDING_SLOTS`):
|
||||
| Slot | Config key |
|
||||
|---|---|
|
||||
| 41 | `show_belt` |
|
||||
| 36 | `stand_still` |
|
||||
| 44 | `weapon_switch` |
|
||||
| 43 | `force_move` |
|
||||
|
||||
If params.ini and the `.keyo` file disagree, params.ini wins and a `"Keeping configured key binding"` line is logged. No disagreement = no log line.
|
||||
|
||||
## Coordinate Systems
|
||||
|
||||
| System | Origin | Used By |
|
||||
|---|---|---|
|
||||
| Monitor | Top-left of first monitor | `screen.grab()` |
|
||||
| Screen | Per-monitor client area | UI detection |
|
||||
| Absolute | Character at screen center | Pathing, target detection |
|
||||
| Relative | Template match position | Inventory grid, NPC interaction |
|
||||
|
||||
Conversion functions in `screen.py`: `convert_monitor_to_screen()`, `convert_screen_to_abs()`, etc.
|
||||
|
||||
## State Machine
|
||||
|
||||
`bot.py` uses the `transitions` library. States and transitions are defined at `Bot.__init__` time.
|
||||
|
||||
**States**: `initialization`, `hero_selection`, `town`, `level`, `pindle`, `shenk`, `trav`, `nihlathak`, `arcane`, `diablo`, `vizier`, `baal`, `mephisto`, `andariel`, `countess`
|
||||
|
||||
**Key transitions**:
|
||||
|
||||
| Trigger | Source | Dest | Notes |
|
||||
|---|---|---|---|
|
||||
| `init` | initialization | initialization | Screen detection, routes to create_game or start_from_town |
|
||||
| `select_character` | initialization | hero_selection | |
|
||||
| `start_from_town` | initialization/hero_selection | town | |
|
||||
| `maintenance` | town | town | Heal, buy pots, stash, repair, resurrect merc |
|
||||
| `run_pindle` | town | pindle | |
|
||||
| `run_arcane` | town | arcane | |
|
||||
| `end_run` | any run state | town | TP back; calls `on_end_run` which TPs to town |
|
||||
| `end_game` | town/any run state | initialization | Save & exit; use when no TP scrolls or unrecoverable |
|
||||
|
||||
`end_run` requires working TP scrolls. If charges = 0, trigger `end_game` instead — otherwise the bot loops trying to TP back indefinitely.
|
||||
|
||||
## HealthManager — Internal Timing
|
||||
|
||||
The background health monitor (`src/health_manager.py`) polls at:
|
||||
|
||||
```
|
||||
interval = max(0, (3/25 - fn_elapsed) * jitter(0.8–1.2))
|
||||
```
|
||||
|
||||
That's approximately every 3 game frames (96–144ms at 25 FPS). The jitter prevents perfectly regular polling patterns from being detectable.
|
||||
|
||||
**Rejuv logic**:
|
||||
1. Minimum 0.60s between rejuv drinks (hit recovery guard).
|
||||
2. Drinks rejuv if `health ≤ take_rejuv_potion_health` OR `mana ≤ take_rejuv_potion_mana`.
|
||||
3. "Double rejuv" chicken fires only if `last_drink < 8s` **AND** `health ≤ take_rejuv_potion_health`.
|
||||
- The HP check is critical: mana-triggered rejuvs can legitimately fire back-to-back at full HP (Hammerdin spending mana fast). Without the HP check, false chickens occur at 99.9% HP.
|
||||
|
||||
## PickIt — Item Identity
|
||||
|
||||
`GroundItem` has two identity fields:
|
||||
|
||||
| Field | Formula | Purpose |
|
||||
|---|---|---|
|
||||
| `ID` | `slugify(Name + all as_dict() values including Amount)` | Pickit cache key; different gold amounts = different IDs |
|
||||
| `UID` | `ID + screen center position` | Deduplication within a single items list; same pile at same coords = same UID |
|
||||
|
||||
The fail-detection in `_pick_up_item` uses `item.ID == prev.ID` for gold and `item.UID == prev.UID` for everything else. Two nearby gold piles with different amounts bypass the ID check since they produce different IDs.
|
||||
|
||||
`_yoink_item()` always returns `PickedUpResult.PickedUp` regardless of actual success. True pickup failures surface only through `_pick_up_item`'s same-ID/UID repeat detection. On confirmed failure, the item's `ID` is blacklisted in `_cached_pickit_items` so it isn't retried in the same session.
|
||||
|
||||
## Configuration Priority
|
||||
|
||||
Merge order (highest priority first):
|
||||
|
||||
```
|
||||
custom.ini > params.ini > game.ini > shop.ini > transmute.ini
|
||||
```
|
||||
|
||||
`Config` is a singleton (`__new__` + `data_loaded` class variable). First instantiation loads all files; subsequent calls return the same instance. Key detection runs during `__init__` after `self.char` is populated.
|
||||
|
||||
## Known Architectural Issues
|
||||
|
||||
See `IMPROVEMENTS.md` for the full list. Highest-priority unresolved items:
|
||||
|
||||
- **C10**: `kill_thread()` uses `PyThreadState_SetAsyncExc` (CPython private API). Can corrupt locks/GIL. Replace with `threading.Event` cooperative shutdown.
|
||||
- **H12/H14**: Health/death managers use module-level globals for state. HealthManager now has a `_state_lock`; death manager does not yet.
|
||||
- **M14**: `PickedUpResult` enum has a gap (values 0,1,3,4,5 — missing 2).
|
||||
- **H10/H11**: `pather.py` (750 lines) and `config.py` are oversized and should be split.
|
||||
@@ -0,0 +1,522 @@
|
||||
# Claude Code Working Guide — my-botty
|
||||
|
||||
This file is auto-read by Claude Code at session start. It covers what you need to debug failures, extend the bot, and avoid known pitfalls. For static architecture see `ARCHITECTURE.md`; for Gemini conventions see `GEMINI.md`.
|
||||
|
||||
---
|
||||
|
||||
## Quick Orientation
|
||||
|
||||
`my-botty` is a Python bot that automates Diablo II: Resurrected boss runs using computer vision and native Windows API input. No kernel drivers — only `SendInput`, `GetCursorPos`, `SetCursorPos` via `ctypes`.
|
||||
|
||||
**Active setup (as of 2026-06-08):**
|
||||
- Character: Hammerdin (`fistman`), Hell difficulty
|
||||
- Runs: `run_diablo`, `run_pindle` (see `config/params.ini`)
|
||||
- OS: Windows 11, D2R 1280×720 windowed
|
||||
- Start bot: `run_botty.bat` (runs `python src/main.py` from the conda env — always current with source; no exe build exists/needed)
|
||||
|
||||
**Always start a session by reading logs first:**
|
||||
```
|
||||
log/log.txt ← current session (DEBUG level)
|
||||
log/stats/events_*.jsonl ← per-run event stream (JSON lines)
|
||||
log/stats/stats_*.log ← human-readable summary
|
||||
```
|
||||
|
||||
Use `/check-logs` slash command to get an instant summary.
|
||||
|
||||
---
|
||||
|
||||
## How to Read a Failure
|
||||
|
||||
### Startup line (every session)
|
||||
```
|
||||
=== BOT START === char=hammerdin | difficulty=hell | routes=['run_diablo', 'run_pindle']
|
||||
```
|
||||
If this is missing, the bot crashed before `on_init()`.
|
||||
|
||||
### Approach failure (run couldn't get to the boss area)
|
||||
```
|
||||
ERROR Approach failed for run_diablo [step: use_wp_rof]
|
||||
```
|
||||
The `[step: X]` tells you exactly which sub-step failed. Also sent to Discord via `_save_error_screenshot`. Step names per run:
|
||||
|
||||
| Run | Step names (in order) |
|
||||
|-----|----------------------|
|
||||
| `diablo` | `open_wp` → `use_wp_rof` → `verify_rof_first` → `retry_open_wp` → `retry_use_wp_rof` → `verify_rof_retry` |
|
||||
| `vizier` | `open_wp` → `use_wp_rof` |
|
||||
| `arcane` | `open_wp` → `use_wp_arcane` |
|
||||
| `shenk` | `open_wp` → `use_wp_frigid` |
|
||||
| `trav` | `open_wp` → `use_wp_travincal` |
|
||||
| `nihlathak` | `open_wp` → `use_wp_halls_of_pain` → `verify_halls_of_pain` |
|
||||
| `pindle` | `go_to_act5` → `traverse_to_portal` → `retry_traverse_to_portal` → `click_red_portal` |
|
||||
| `andariel` | `go_to_act1` → `traverse_to_wp` → `open_wp` → `use_wp_catacombs` |
|
||||
| `countess` | `go_to_act1` → `traverse_to_wp` → `open_wp` → `use_wp_black_marsh` |
|
||||
| `mephisto` | `go_to_act3` → `traverse_to_wp` → `open_wp` → `use_wp_durance` |
|
||||
| `baal` | `go_to_act5` → `traverse_to_wp` → `open_wp` → `use_wp_worldstone` |
|
||||
|
||||
**Implementation:** Each run class has `self.approach_fail_step: str | None = None`. Set to step name just before `return False`. Read in `bot.py` `_run_wrapper()` via `getattr(run_obj, "approach_fail_step", None)`.
|
||||
|
||||
### Maintenance failure (town routine broke)
|
||||
```
|
||||
ERROR Maintenance failed [step: buy_consumables] — vendor NPC not found after retry
|
||||
```
|
||||
Also sent to Discord. Maintenance steps in execution order:
|
||||
`town_heal` → `inspect_inventory` → `identify_items` → `buy_consumables` / `heal` → `stash_items` → `repair` → `resurrect_merc` → `gamble`
|
||||
|
||||
Tracked in `self._maintenance_step` on the `Bot` instance (`bot.py`). Non-fatal failures (repair, resurrect) just log a warning with the step name. Fatal failures (buy_consumables, stash_items) call `_save_error_screenshot("maintenance", reason)` then `trigger_or_stop("end_game", failed=True)`.
|
||||
|
||||
### Common error patterns to grep for
|
||||
|
||||
```
|
||||
ERROR|WARNING|failed|Approach failed|starting from True|DAMAGED|SendInput.*missed
|
||||
```
|
||||
|
||||
| Pattern | Cause |
|
||||
|---------|-------|
|
||||
| `starting from True` | `_curr_loc` became Python `True` instead of a `Location` enum — see `TownManager.identify()` bug below |
|
||||
| `failed on item.*DAMAGED` | `NTIP_ALIAS_QUALITY_MAP` missing `ItemQualityKeyword.Damaged.value` — fixed in `bnip_data.py` |
|
||||
| `SendInput.*missed target` | Win11 pointer acceleration amplifying relative mouse moves — fixed in `win_input.py` |
|
||||
| `Repair/vendor interaction failed` | Flaky NPC detection, best-effort — bot continues |
|
||||
| `Could not identify act from location` | `_curr_loc` is `None` or `True`, not a `Location` string |
|
||||
|
||||
---
|
||||
|
||||
## Known Bugs and Fixes (permanent reference)
|
||||
|
||||
### Bug 1: `_curr_loc = True` propagation
|
||||
**File:** `src/town/town_manager.py` — `identify()` method (~line 236)
|
||||
|
||||
The act-level `identify()` returns a `Location` enum on success. The wrapper previously returned `curr_loc` (the passed-in value) unchanged. If `curr_loc` was somehow `True` (Python bool), it propagated. `True == 1` in Python, so pather's `traverse_nodes((True, A5_QUAL_KEHK))` could match `A5_TOWN_START` (enum value 1), masking the bug.
|
||||
|
||||
**Fix:** Both return sites in `identify()` now return `success if isinstance(success, Location) else curr_loc` (primary) and `success if isinstance(success, Location) else new_loc` (A5 fallback).
|
||||
|
||||
**Defensive guard in bot.py** (~line 438):
|
||||
```python
|
||||
self._curr_loc = self._town_manager.identify(self._curr_loc)
|
||||
if self._curr_loc is True:
|
||||
Logger.warning("identify() returned True — resetting to A5_TOWN_START")
|
||||
self._curr_loc = Location.A5_TOWN_START
|
||||
```
|
||||
|
||||
### Bug 2: `DAMAGED` quality KeyError
|
||||
**File:** `src/d2r_image/bnip_data.py` — `NTIP_ALIAS_QUALITY_MAP`
|
||||
|
||||
`ItemQualityKeyword.Damaged = 'DAMAGED'` exists but the map was missing an entry for it. Caused `KeyError: 'DAMAGED'` on 20+ items per session whenever Charsi repair triggered ground-item reads.
|
||||
|
||||
**Fix:** Added `ItemQualityKeyword.Damaged.value: 1` to the map alongside `LowQuality`, `Crude`, `Cracked`.
|
||||
|
||||
### Bug 3: NPC name tag templates stale → `open_npc_menu` always times out
|
||||
**File:** `src/npc_manager.py` — `open_npc_menu()`
|
||||
|
||||
NPC body templates find the NPC correctly (Akara consistently at screen ~(649, 407)), but the name tag "AKARA"/"MALAH" templates score only ~0.28 on hover, just below the original 0.35 threshold. This caused a 20-second full-screen search that never clicked anyone.
|
||||
|
||||
**Root causes:**
|
||||
1. Name tag templates are slightly stale vs. current D2R rendering
|
||||
2. Name tag search used the full screen ROI, so false-positive text anywhere on screen could also score 0.28+ — raising threshold didn't help
|
||||
3. After click, the old code checked gold name tag template (also stale) instead of the NPC dialogue UI element
|
||||
|
||||
**Fix:** Three changes in `open_npc_menu()`:
|
||||
1. **Small ROI for name tag check**: after hovering, search for the name tag only in a 240×140px box directly above the cursor (where D2R always renders name tags), preventing distant false positives
|
||||
2. **Lower name tag threshold 0.35 → 0.26**: Akara at 0.28 now passes; with the small ROI false-positive risk is contained
|
||||
3. **NPCDialogue confirmation instead of gold tag**: after clicking, check `is_visible(ScreenObjects.NPCDialogue)` first — more reliable than matching a stale gold name tag template
|
||||
4. **Timeout 20 s → 8 s**: fails faster when the NPC is genuinely off-screen
|
||||
|
||||
**Symptom to grep for (before fix):** `NPC akara hover - White score: 0.28x` spinning for 20 s
|
||||
|
||||
---
|
||||
|
||||
### Bug 4: `open_npc_menu` body+pose scoping bug → false positive Akara/Malah clicks
|
||||
**File:** `src/npc_manager.py` — `open_npc_menu()`, hover loop
|
||||
|
||||
User-added body+pose fallback click path had a scoping bug: `min_dist` was computed per template in the build loop but the variable was NOT stored in the result dict, so the hover loop's `pose_confirmed = ... and min_dist < 100` used the stale value from the last template processed — not the current result's distance to a known pose. This caused false positives (e.g. Akara body template scoring 0.531 at screen `(589, 560)`, far outside her ROI and 211 px from any pose) to pass `pose_confirmed` and be clicked, while the real Akara at `(979, 443)` (body 0.42–0.47, 102 px from pose) never got clicked.
|
||||
|
||||
**Fix** (`open_npc_menu` build + hover loops):
|
||||
1. Renamed `min_dist` → `min_dist_val`, stored in result dict: `results.append({..., "min_dist": min_dist_val})`
|
||||
2. Hover loop uses `result["min_dist"]` instead of stale `min_dist`
|
||||
3. Body threshold 0.50 → 0.40 (real Akara at 0.42–0.47)
|
||||
4. Pose tolerance 100 → 150 px (real Akara at 102 px from nearest pose; false positive at 211 px, correctly rejected)
|
||||
5. Removed `elif body_confident and attempts == 0` blind-first-attempt path (was the direct enabler of false positive clicks)
|
||||
|
||||
**Symptom to grep for (before fix):** `Clicking on akara at (589, 559) (body+pose confirmed, name tag score 0.225)` followed by `dialogue not open`
|
||||
|
||||
---
|
||||
|
||||
### Bug 5: `_curr_loc` becomes `A1_TOWN_START` when Cain ID fails → A1 pathing in A5 environment
|
||||
**Files:** `src/bot.py` — `on_maintenance()` lines 473–475 and 502–510
|
||||
|
||||
When Cain identification fails, `identify()` returns False → `_curr_loc` was reset to `Location.A1_TOWN_START` as a fallback. But the character is physically still in A5 Harrogath (game just spawned there). The subsequent `buy_consumables(A1_TOWN_START)` call runs A1 pather navigation in the A5 environment: pather can't find A1 reference templates → moves character to a random A5 spot → Akara body templates fire false positives → clicks fail → fatal.
|
||||
|
||||
The A1 retry at line 505 also hardcoded `Location.A1_TOWN_START` without calling `go_to_act` first, so the character was never actually navigated to A1 before running A1 pathing.
|
||||
|
||||
**Fix (both sites in `bot.py` `on_maintenance()`):**
|
||||
1. Identify fallback: `A1_TOWN_START` → `A5_TOWN_START` (character IS in A5 when Cain fails)
|
||||
2. A1 retry: add `go_to_act(1, retry_start)` before `buy_consumables` so the bot actually opens the waypoint and travels to A1 first:
|
||||
```python
|
||||
retry_start = self._curr_loc or Location.A5_TOWN_START
|
||||
a1_loc = self._town_manager.go_to_act(1, retry_start)
|
||||
self._curr_loc, result_items = self._town_manager.buy_consumables(a1_loc or Location.A1_TOWN_START, items=items)
|
||||
```
|
||||
|
||||
**Symptom to grep for (before fix):** `TownManager buy_consumables: starting from a1_town_start` immediately after `Could not identify items (Cain not available)`
|
||||
|
||||
---
|
||||
|
||||
### Bug 6: `open_npc_menu` dialogue confirmation always False → retry click closes dialogue → loop
|
||||
**File:** `src/npc_manager.py` — `open_npc_menu()` post-click confirmation block
|
||||
|
||||
After clicking Akara/Malah the bot checked `is_visible(ScreenObjects.NPCDialogue)`. The `npc_dialogue.png` template is a narrow (~15px) gold vertical border strip from an old D2R rendering; the ROI `456,0,30,150` points at the top-center of the screen where no dialogue border renders. Result: `NPCDialogue` is **always False** even when the dialogue IS open.
|
||||
|
||||
Fallback was `name_tag_gold` template — also stale. Both checks failing triggered a retry click on the NPC body, which **closes** the already-open dialogue, creating an open→can't detect→close→retry loop until timeout.
|
||||
|
||||
Also: `press_npc_btn` had `wait_until_visible(ScreenObjects.NPCDialogue, timeout=3.0)` which wasted 3 seconds on every NPC button press.
|
||||
|
||||
**Fix:** Replace NPCDialogue + gold-tag checks with action button detection (`_action_btns_visible`). The TRADE/RESURRECT/IDENTIFY buttons appear when the dialogue opens and are reliably matched by the same templates used in `press_npc_btn`. Added helper `_action_btns_visible(npc_key, img)` that mirrors `press_npc_btn`'s white→blue→grayscale search. Also added a **fast path** at the top of the hover loop: if action buttons are already visible at the start of an iteration, return True immediately without hovering.
|
||||
|
||||
**Symptom to grep for (before fix):** `NPC akara - dialogue not open, retrying click on body` repeating 2–3 times then `NPC akara - clicked but neither dialogue nor gold tag found`
|
||||
|
||||
---
|
||||
|
||||
### Bug 7: High-score false-positive name tags at positions outside NPC ROI bypass pose check
|
||||
**File:** `src/npc_manager.py` — `open_npc_menu()` click decision (was line 327)
|
||||
|
||||
White text in the game world (ground items, skill effects, UI elements) could produce name-tag white-template scores of 0.98+ at positions far outside the NPC's known ROI. Because `name_tag_confirmed = res_w.valid or res_g.valid` was unconditional, these positions bypassed the `pose_confirmed` check entirely and got clicked (e.g. (200, 539) while Akara's ROI is x=605–1004).
|
||||
|
||||
**Fix:** Gate `name_tag_confirmed` on an ROI boundary check when `attempts == 0` and the NPC has a defined ROI:
|
||||
```python
|
||||
if "roi" in npcs[npc_key] and attempts == 0:
|
||||
npc_roi = npcs[npc_key]["roi"]
|
||||
in_npc_roi = (npc_roi[0] <= hover_screen[0] <= npc_roi[0] + npc_roi[2] and
|
||||
npc_roi[1] <= hover_screen[1] <= npc_roi[1] + npc_roi[3])
|
||||
else:
|
||||
in_npc_roi = True
|
||||
name_tag_confirmed = (res_w.valid or res_g.valid) and in_npc_roi
|
||||
```
|
||||
After the first pass (`attempts > 0`), ROI restriction is lifted so the wide fallback search can still find the NPC if it wandered.
|
||||
|
||||
**Symptom to grep for (before fix):** `Clicking on akara at (200, 539) (name tag confirmed)` with x < 605
|
||||
|
||||
---
|
||||
|
||||
### Bug 8: Win11 mouse 3–6× overshoot
|
||||
**File:** `src/input_layer/win_input.py` — `mouse_move()` (~line 270)
|
||||
|
||||
Win11 relative `SendInput` deltas are amplified by Windows Enhanced Pointer Precision (pointer acceleration). A 209px delta became 678px. Affected every NPC click, waypoint click, and template interaction.
|
||||
|
||||
**Fix:** On Win11 (`_USE_ABSOLUTE_MOUSE == False`), use `SetCursorPos(x, y)` for accurate positioning, then send a zero-delta `MOUSEEVENTF_MOVE` event so D2R's hover/cursor pipeline fires at the new position.
|
||||
|
||||
```python
|
||||
# Win11 path in mouse_move():
|
||||
user32.SetCursorPos(target_x, target_y)
|
||||
_send_input(_make_mouse_input(MOUSEEVENTF_MOVE, 0, 0))
|
||||
```
|
||||
|
||||
The OS mode is detected at import time via `utils.os_detect.detect_os()` → `_USE_ABSOLUTE_MOUSE`.
|
||||
|
||||
---
|
||||
|
||||
### Bug 9: Act-state desync → char respawns in wrong act → every subsequent game fails (2026-06-10)
|
||||
**Files:** `src/bot.py`, `src/town/town_manager.py`
|
||||
|
||||
The bot's believed location (`_curr_loc`) and the character's PHYSICAL act diverge after any town
|
||||
failure: retries hardcoded acts without traveling (`buy_consumables(A1_TOWN_START)` even when
|
||||
go_to_act(1) FAILED, `repair(A4_TOWN_START)`, `heal(A1_TOWN_START)`) and failure fallbacks blindly
|
||||
set `_curr_loc = A5_TOWN_START`/`A1_TOWN_START`. D2R respawns the char in the act it save+exited
|
||||
from, so one desync poisons EVERY following game: A5 pathing runs in A1/A4 town → `A5_WP` never
|
||||
found → 42 consecutive `open_wp` approach failures in the 2026-06-09 session.
|
||||
|
||||
**Fix:**
|
||||
1. `TownManager.detect_current_act()` — searches `TOWN_MARKERS` to find the physical act town.
|
||||
2. `TownManager.open_wp()` / `go_to_act()` — on failure/early-return, verify the physical act and
|
||||
retry with the detected act's pather.
|
||||
3. `Bot._verify_town_location(assumed)` — used at EVERY retry/fallback site in `on_maintenance()`
|
||||
and `on_end_run()` instead of hardcoded town starts. Never run act-X pathing without confirmed
|
||||
presence in act X.
|
||||
|
||||
**Symptom to grep for:** `Wanted to select A5_WP, but could not find it` repeated across games;
|
||||
`A1 open_trade_menu: navigating from a1_town_start` + `Pather: taking a random guess` while
|
||||
physically elsewhere.
|
||||
|
||||
### Bug 10: Duplicate `log_end_game` → phantom 0s "successful" games reset the fail circuit breaker (2026-06-10)
|
||||
**File:** `src/game_stats.py`
|
||||
|
||||
`bot.on_end_game()` and `game_controller.run_bot()` can BOTH call `log_end_game` for the same game.
|
||||
The second call emitted a phantom `game_ended failed:false elapsed 0` event and reset
|
||||
`_consecutive_runs_failed` to 0 — so `max_consecutive_fails=5` never triggered during the 3-hour
|
||||
death spiral. **Fix:** `log_end_game` returns early if `self._timer is None` (already logged).
|
||||
Also: `_last_failure_reason` is now cleared in `log_start_game` so events can't inherit a stale
|
||||
reason from a previous game (games 43–50 were blamed on `open_wp` when they actually failed in
|
||||
maintenance).
|
||||
|
||||
### Bug 11: Chickens mislabeled "Bot stopped (F12 or crash)" (2026-06-10)
|
||||
**Files:** `src/health_manager.py`, `src/game_controller.py`
|
||||
|
||||
`_do_chicken()` called `bot.stop()` (callback) FIRST and only set `_did_chicken = True` several
|
||||
seconds later (after save/exit + screenshot). The controller poll loop saw `_stopping` before the
|
||||
chicken flag and labeled the failure "Bot stopped (F12 or crash)". **Fix:** set `_did_chicken =
|
||||
True` at the top of `_do_chicken()` before the callback; controller re-checks the flag before
|
||||
resetting it.
|
||||
|
||||
### Bug 12: A4 Halbu repair trip = act-desync trigger (2026-06-10)
|
||||
**File:** `config/params.ini`
|
||||
|
||||
`repair_npc=a4_halbu` sent the bot A5→A4 via WP every 5 runs. Halbu detection failed 100% in the
|
||||
2026-06-09 session (body score ~0.39, name tag ~0.19), wasting ~60s per attempt and leaving the
|
||||
char in A4 (see Bug 9). **Fix:** `repair_npc=a5_larzuk` — stays in-act; the Larzuk flow has a
|
||||
direct-template fallback and still falls back to Halbu (with proper travel) if Larzuk fails.
|
||||
|
||||
### Bug 13: hotkey `wait()` no-arg returned on ANY key → silent process exit (2026-06-11)
|
||||
**File:** `src/input_layer/hotkey.py`
|
||||
The reimplemented `keyboard.wait()` (no key) returned on any keypress; `main.py` relies on it
|
||||
blocking forever to keep the process alive (all bot threads are daemons). Pressing F11 both
|
||||
started the bot AND killed the process moments later, with no traceback. **Fix:** no-arg `wait()`
|
||||
now sleeps forever; the poll loop is also edge-triggered (one fire per physical press — held
|
||||
keys no longer refire every ~20ms) and callback exceptions print instead of being swallowed.
|
||||
|
||||
### Bug 14: OCR dead — pytesseract never configured (2026-06-11)
|
||||
**File:** `src/d2r_image/ocr.py`
|
||||
pytesseract only looks for plain `tesseract` on PATH; the `PYTESSERACT_TESSERACT_CMD` env var
|
||||
set by run_botty.bat is NOT a pytesseract feature and was never read. **Fix:** ocr.py applies
|
||||
that env var (fallback: PATH, then `C:\Program Files\Tesseract-OCR\tesseract.exe`) to
|
||||
`pytesseract.pytesseract.tesseract_cmd` at import.
|
||||
|
||||
### Bug 15: NIP loader NameError + dropped rule (2026-06-11)
|
||||
**Files:** `src/bnip/utils.py`, `config/default.bnip:1714`
|
||||
`find_unique_or_set_base` raised undefined `NipSyntaxError` (→ `BNipSyntaxError`), and the
|
||||
underlying trigger was a typo `Shaefershammer` → `Schaefershammer`. 474 expressions now load.
|
||||
|
||||
### Bug 16: A5 WP death loop after Pindle returns (2026-06-11)
|
||||
**Files:** `src/town/town_manager.py`, `src/town/a5.py`, `src/bot.py`
|
||||
After a Pindle return the believed location (`a5_town_start`) is wrong (char is at the stash);
|
||||
the direct WP node path failed 5/5, and outer maintenance retries re-ran the full anchor/sweep
|
||||
escalation from ever-worse positions (one 10-min wander onto the town wall). A5 templates also
|
||||
score marginally low at current D2R settings (stash ~0.51, WP needs ≤0.62).
|
||||
**Fix (layered):** per-game WP budget on TownManager (reset in `bot.on_init`): failure 1 = full
|
||||
escalation allowed; failure 2 = `quick=True` direct path only; failure 3+ = instant False so the
|
||||
caller's fatal path ends the game (~40s fresh spawn beats wandering). Sweep trimmed 10→6 steps,
|
||||
WP select timeout 4s, thresholds dropped (WP 0.62; stash 0.60→0.45 retry) — safe because every
|
||||
select is gated by a success_func (panel-open / WP-label check).
|
||||
|
||||
### Bug 17: kill_diablo fought with Conviction + mid-fight Redemption (2026-06-11)
|
||||
**File:** `src/char/paladin/hammerdin.py` `kill_diablo()`
|
||||
Conviction doesn't boost magic-damage hammers, and the interleaved 0.8s Redemption casts were
|
||||
pure downtime vs a solo boss with no corpses — long fights got the merc killed. **Fix:** attack
|
||||
with Concentration (also buffs the merc via party aura), Redemption only once post-kill.
|
||||
|
||||
### Bug 18: stash only used personal + 3 shared (2026-06-11)
|
||||
**Files:** `src/inventory/personal.py`, `src/inventory/stash.py`
|
||||
Gold used raw `select_tab()` clicks with a 4-tab rotation (`% 4`, `> 3`); items already paged.
|
||||
D2R 2.7+ has 5 shared pages. **Fix:** gold now navigates via `select_stash_page()` (OCR-verified
|
||||
page arrows, layout-proof), rotation `% 6` / stop at `> 5`, shared-first starts at page 5.
|
||||
Also fixed a leftover `> 3` bound in the item-stash loop (`personal.py` ~line 185) → `> 5`.
|
||||
|
||||
### Bug 19: failed item transfer mistaken for "stash full" → taskkill D2R (2026-06-12)
|
||||
**File:** `src/inventory/personal.py` — `stash_all_items()` stash loop
|
||||
When `transfer_items("stash")` fails for any reason OTHER than fullness (e.g. the equipped-area
|
||||
click guard, a transient UI hiccup), keep items stay in inventory. The loop interpreted ANY
|
||||
remaining keep item as "this tab is full", paged through all stash tabs, and on the last page
|
||||
called `stash.stash_full()` → `taskkill /f /im D2R.exe` + a false Discord "stash full" alert.
|
||||
Surfaced by `tools/testbed.py stash all` (charms in cols 4–9 hit the equipped-area guard, so every
|
||||
transfer was cancelled and the loop nuked the game).
|
||||
**Fix:** before declaring the tab full, check `is_visible(ScreenObjects.EmptyStashSlot)`. If a slot
|
||||
IS free but the transfer still failed, count it as a transfer failure (cap 2) and bail out,
|
||||
leaving items in inventory — never advance tabs / call `stash_full()` on a non-full page.
|
||||
**Symptom to grep for (before fix):** repeated `transfer_items: inventory unchanged after
|
||||
attempting to stash` followed by `Wanted to stash item ... Assumes full stash` across rising page
|
||||
numbers, ending in `All stash is full, quitting`.
|
||||
**Test tip:** `tools/testbed.py stash` exercises the gold + open-stash path live; add `all` to scan
|
||||
all 10 inventory columns and exercise the keep-item transfer branch on existing charms.
|
||||
|
||||
### Bug 20: unescaped `)` in an echo killed the conda direct-download path (2026-08-05)
|
||||
**File:** `install.bat` — Miniforge install error branch
|
||||
`echo ERROR: Miniforge3 installer failed (exit code %errorlevel%).` sat inside a parenthesised
|
||||
`if (...)` block. An unescaped `)` inside a block **terminates the block**, leaving `.` as a stray
|
||||
token. cmd parses the whole block when it reaches it, so this aborted the script **even when the
|
||||
installer succeeded and the branch was never taken** — verified with a minimal repro (unescaped form
|
||||
exits 255 on a false condition; escaped form exits 0).
|
||||
Effect: the direct-download fallback — the only path on a machine without winget — installed conda
|
||||
and then died before creating the `botty` env, leaving the bot unusable.
|
||||
**Fix:** escape as `^(exit code %errorlevel%^)`, the convention already used elsewhere in the file
|
||||
(`^(fast path^)`).
|
||||
**Symptom to grep for:** `. was unexpected at this time.` right after `Installing Miniforge3`.
|
||||
|
||||
**Two batch pitfalls that keep recurring in `install.bat` — check both when editing it:**
|
||||
1. A `::` comment line **inside** a `( )` block is a parse error. Put comments above the block.
|
||||
2. Any unescaped `(` or `)` in an `echo` inside a block breaks it. Escape as `^(` / `^)`.
|
||||
|
||||
Audit both across every `.bat` with:
|
||||
```
|
||||
awk '{ if ($0 ~ /^[ \t]*::/) { if (d>0) print FILENAME": "NR": "$0; next }
|
||||
t=$0; gsub(/\^[()]/,"",t); d += gsub(/\(/,"(",t) - gsub(/\)/,")",t); if (d<0) d=0 }' *.bat
|
||||
```
|
||||
|
||||
### Bug 21: installer needed admin, so it silently failed on a normal double-click (2026-08-05)
|
||||
**Files:** `install.bat`, `src/d2r_image/ocr.py`, `run_botty.bat`
|
||||
`winget install` defaulted to **machine scope**, putting conda in `%ProgramData%\miniforge3` — which
|
||||
requires elevation. Double-clicking `install.bat` without admin failed silently and conda never
|
||||
installed. The same bug applied to the Tesseract install.
|
||||
**Fix:** `--scope user` (conda now lands in `%USERPROFILE%\miniforge3`, no admin), plus re-scanning
|
||||
for `conda.exe`/`tesseract.exe` after winget instead of trusting its exit code — winget returns
|
||||
non-zero when a package is *already installed*. Tesseract additionally falls back to a direct NSIS
|
||||
download. `ocr.py` and `run_botty.bat` now also look in `%LOCALAPPDATA%\Programs\Tesseract-OCR`.
|
||||
**Known limitation:** the official Tesseract installer self-elevates and discards `/D=`, so it
|
||||
always installs machine-wide and **does require admin/UAC**. There is no per-user Tesseract install.
|
||||
Conda has no such limitation.
|
||||
**Note:** deleting a conda folder without running its uninstaller leaves stale Add/Remove-Programs
|
||||
entries, which make winget treat the next install as an *upgrade* instead of a fresh install.
|
||||
|
||||
### Bug 22: tesserocr never loaded → bot silently ran on the slow OCR fallback (2026-08-06)
|
||||
**File:** `install.bat` — OCR backend 1 section
|
||||
`install.bat` always printed `tesserocr: not available (DLL issue)` and the bot logged
|
||||
`OCR backend: pytesseract (fallback)`. pytesseract spawns `tesseract.exe` as a **subprocess per OCR
|
||||
call**; tesserocr uses the in-process C++ API, so this was a permanent, silent performance loss on
|
||||
every item hover.
|
||||
|
||||
**Root cause** (found by walking the PE import table with `pefile`, not by guessing):
|
||||
```
|
||||
tesserocr.pyd → tesseract52.dll → leptonica-1.78.0.dll → tiff.dll → libdeflate.dll ← MISSING
|
||||
```
|
||||
Current conda-forge `libdeflate` (>=1.20) installs the library as **`deflate.dll`**, but the older
|
||||
`tiff.dll` from the `tesseract=4.*` stack still imports the previous name **`libdeflate.dll`**.
|
||||
Nothing provided that name, so `tiff.dll` failed to load and every DLL above it failed with
|
||||
**WinError 126 — "The specified module could not be found"**. That message made it look like a
|
||||
missing *module*, which is why earlier fixes chased `os.add_dll_directory` / PATH instead. Adding
|
||||
every DLL directory does NOT help; the file genuinely does not exist under that name.
|
||||
|
||||
**Fix:** install `libdeflate` explicitly next to `tesseract=4.*`, then copy `deflate.dll` to the
|
||||
legacy name when it is absent:
|
||||
```bat
|
||||
if not exist "%BOTTY_ENV_DIR%\Library\bin\libdeflate.dll" (
|
||||
if exist "%BOTTY_ENV_DIR%\Library\bin\deflate.dll" (
|
||||
copy /y "...\deflate.dll" "...\libdeflate.dll" >nul
|
||||
)
|
||||
)
|
||||
```
|
||||
**Verify:** `install.bat` should print `tesserocr: OK (fast path)` and the bot should log
|
||||
`OCR backend: tesserocr (primary)`. Deleting `libdeflate.dll` reproduces the failure exactly.
|
||||
|
||||
**Debugging tip for any future "DLL load failed" error:** don't assume it's a search-path problem.
|
||||
Walk the real import chain and try loading each DLL directly — WinError 126 names the *importer*,
|
||||
never the missing dependency:
|
||||
```python
|
||||
import pefile, ctypes
|
||||
pe = pefile.PE(r"...\some.dll", fast_load=True)
|
||||
pe.parse_data_directories(directories=[pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_IMPORT"]])
|
||||
print([e.dll.decode() for e in pe.DIRECTORY_ENTRY_IMPORT])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How to Add a New Run
|
||||
|
||||
1. Create `src/run/my_run.py` — class `MyRun` with `name = "run_my_run"`
|
||||
2. Add `self.approach_fail_step: str | None = None` in `__init__`
|
||||
3. In `approach()`: set `self.approach_fail_step = None` at top, then set step name before every `return False`
|
||||
4. Register in `bot.py`: instantiate in `__init__`, add to `self._do_runs`, add state + transition, add `on_run_my_run()` handler
|
||||
5. Add route toggle to `config/params.ini` under `[routes]`
|
||||
6. Add to `Config().routes_order` list
|
||||
|
||||
The `_run_wrapper()` in `bot.py` handles approach failure reporting automatically — it calls `getattr(run_obj, "approach_fail_step", None)` so no changes needed there.
|
||||
|
||||
---
|
||||
|
||||
## How Town Maintenance Works
|
||||
|
||||
Sequence in `bot.py` `on_maintenance()` (called between every run):
|
||||
|
||||
1. **town_heal** — drink belt potions if HP < 95%
|
||||
2. **inspect_inventory** — open inventory, count items, update TP/ID/key needs
|
||||
3. **identify_items** — go to Cain if any `item.need_id` is True
|
||||
4. **buy_consumables** — visit vendor for HP/mana pots, TP scrolls, ID scrolls, keys; sell flagged items (fatal if fails twice)
|
||||
5. **heal** — visit healer NPC if HP/mana below threshold (alternative to buy_consumables branch)
|
||||
6. **stash_items** — put kept items / gold in stash; run transmutes after (fatal if fails twice)
|
||||
7. **repair** — repair gear + sell via Halbu (A4) or Larzuk (A5); non-fatal, bot continues
|
||||
8. **resurrect_merc** — revive dead merc; non-fatal
|
||||
9. **gamble** — buy gamble items if Jamella has stock; non-fatal
|
||||
|
||||
`TownManager` methods (`buy_consumables`, `stash`, `repair`, etc.) return `(new_loc, items)` tuples or `(False, False)` on failure. On failure, bot.py retries once from a fallback location before giving up.
|
||||
|
||||
---
|
||||
|
||||
## Coordinate Systems (quick ref)
|
||||
|
||||
| Name | Origin | Notes |
|
||||
|------|--------|-------|
|
||||
| Monitor | Top-left of first monitor | `screen.grab()` output |
|
||||
| Screen | D2R client area top-left | UI detection |
|
||||
| Absolute | Character at screen center | Pathing targets |
|
||||
| Relative | Template match position | NPC interaction |
|
||||
|
||||
Convert via `screen.py`: `convert_monitor_to_screen()`, `convert_screen_to_abs()`, `convert_abs_to_monitor()`, `convert_screen_to_monitor()`.
|
||||
|
||||
---
|
||||
|
||||
## Config System
|
||||
|
||||
Singleton `Config()` merges in priority order: `custom.ini` > `params.ini` > `game.ini` > `shop.ini` > `transmute.ini`. First instantiation loads; subsequent calls return the same object. User overrides go in `custom.ini` (not tracked in git).
|
||||
|
||||
Key sections in `params.ini`:
|
||||
- `[char]` — character type, keybinds, difficulty, runs_per_repair, etc.
|
||||
- `[general]` — discord webhook, auto_login, info_screenshots, difficulty
|
||||
- `[routes]` — boolean flags for each run (run_diablo=1, run_pindle=1, etc.)
|
||||
- `[routes_order]` — execution order
|
||||
|
||||
---
|
||||
|
||||
## Discord Notifications
|
||||
|
||||
`src/messages/messenger.py` wraps a Discord webhook. Error events are sent via `_save_error_screenshot(run_name, reason)` in `bot.py`, which:
|
||||
1. Saves a timestamped screenshot to `log/screenshots/error/`
|
||||
2. Calls `messenger.send_error(run_name, reason, screenshot_path)` if `discord_log_errors=1`
|
||||
|
||||
Enable in `params.ini`:
|
||||
```ini
|
||||
discord_log_errors=1
|
||||
discord_hook_url=https://discord.com/api/webhooks/...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Threading Safety Rules
|
||||
|
||||
- **Never call `input_layer` from the health_manager or death_manager threads.** Those threads only read screen state. All input goes through the bot thread.
|
||||
- **`set_panel_check_paused(True)`** must be called before opening any UI panel (vendor, stash, WP). Forgetting it causes health_manager to misread HP through the panel overlay and false-chicken.
|
||||
- **`_stash_mutex`** on `Bot` must be held during transmutes (stash is open). Acquired in `on_maintenance()` after stash opens.
|
||||
|
||||
---
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
**Template match failures** — Add `save_debug=True` to `template_finder.search_and_wait(...)` to dump the failed match image to `log/screenshots/debug/`.
|
||||
|
||||
**Path node failures** — `pather.py` traverse failures usually mean the bot is in the wrong position. Check the log for the node sequence — the last successful node before the failure shows where the bot got lost.
|
||||
|
||||
**Verify D2R window** — `screen.find_and_set_window_position(force=True)` re-detects the window. Call this before template searches in flaky areas (Pindle portal approach does this).
|
||||
|
||||
**Adding step tracking to a new failure point** — just set `self.approach_fail_step = "descriptive_name"` before `return False`. No other wiring needed.
|
||||
|
||||
---
|
||||
|
||||
## File Quick Reference
|
||||
|
||||
```
|
||||
src/bot.py main state machine, maintenance loop
|
||||
src/run/*.py one file per boss run
|
||||
src/town/town_manager.py orchestrates all town NPC interactions
|
||||
src/town/a1.py .. a5.py per-act NPC/WP/stash implementations
|
||||
src/input_layer/win_input.py mouse_move(), key_press(), SendInput wrappers
|
||||
src/input_layer/mouse_impl.py humanized Bezier mouse paths
|
||||
src/pather.py node-based pathfinding
|
||||
src/template_finder.py OpenCV template matching
|
||||
src/screen.py window detection, screenshot, coord conversion
|
||||
src/d2r_image/bnip_data.py NTIP alias maps (quality, stat, flag)
|
||||
src/item/pickit.py item pickup decision logic
|
||||
src/health_manager.py background HP/mana potion auto-drinker
|
||||
src/death_manager.py death detection + recovery
|
||||
src/config.py Config singleton
|
||||
config/params.ini user config (routes, difficulty, Discord)
|
||||
config/game.ini D2R UI coordinates, template ROIs
|
||||
scripts/stash_inventory.py scan all 6 stash pages → log/stash_inventory.json
|
||||
scripts/make_stash_csv.py convert stash_inventory.json → stash_list.csv (trade list)
|
||||
stash_list.csv deduplicated item list (name, page, stats); auto-updated by bot
|
||||
log/log.txt current session log
|
||||
log/stats/events_*.jsonl per-run event stream
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
# Botty working-state dependencies (verified 2026-06-11)
|
||||
|
||||
Everything required for the bot to run as well as it did during the verified
|
||||
full Diablo runs on 2026-06-11. If a future setup misbehaves, diff against this.
|
||||
|
||||
## Runtime stack
|
||||
| Layer | Requirement | Verified value |
|
||||
|---|---|---|
|
||||
| Python env | conda env `botty` | `C:\ProgramData\miniforge3\envs\botty` (python 3.10.14) — NOTE: `C:\Users\alex\miniforge3\envs\botty` is a broken leftover (no python.exe); `find_python.bat` skips it correctly |
|
||||
| OCR binary | Tesseract 5.5.0 (winget) | `C:\Program Files\Tesseract-OCR\tesseract.exe` — wired in `src/d2r_image/ocr.py` (env var `PYTESSERACT_TESSERACT_CMD` → PATH → this default). Conda tesseract must stay UNINSTALLED (access violations) |
|
||||
| OCR backend | pytesseract fallback | tesserocr wheel is DLL-broken (needs Tesseract 4.x libs) — pytesseract is the working path; startup logs `OCR backend: pytesseract (fallback)` |
|
||||
| Launcher | `run_botty.bat` | sets conda-like PATH, UTF-8, TESSDATA. No exe build exists — bot always runs current source |
|
||||
|
||||
## Key python packages (installed, working)
|
||||
```
|
||||
opencv-python==4.5.5.64 numpy==1.26.4 pytesseract==0.3.13
|
||||
mss==7.0.1 beautifultable==1.1.0 colorama==0.4.6
|
||||
discord.py==2.7.1 aiohttp==3.14.1 certifi==2026.5.20
|
||||
pillow==12.2.0 rapidfuzz==2.15.1 pywin32==312
|
||||
psutil==7.2.2 cryptography==48.0.1
|
||||
```
|
||||
|
||||
## D2R requirements (template matching breaks without these)
|
||||
- Settings must match `assets/d2r_settings.json` — startup warns loudly if not.
|
||||
Verified in-game 2026-06-11: 1280x720 windowed, resolution scale 100, DLSS OFF,
|
||||
AA OFF, AO OFF, texture HIGH, character/environment/transparency/shadow LOW.
|
||||
(DLSS ON was the root cause of the CS template failures earlier that day.)
|
||||
- Window: bot enforces client area at (5, 98) size 1280x720 (`enforce_d2r_window`).
|
||||
- Keybinds file: `C:\Users\alex\Saved Games\Diablo II Resurrected\Fistman*.keyo`
|
||||
(auto-parsed at startup). Skill hotkeys in `config/params.ini` must match the
|
||||
in-game skill assignments: blessed_hammer=f1, holy_shield=f2, redemption=f3,
|
||||
vigor=f4, conviction=f5, concentration=f8, teleport=b, BO=7, BC=8.
|
||||
Use `tools/capture_skill_hotkeys.py` to verify/capture them from the live game.
|
||||
- Char: hammerdin "fistman", Hell, CTA swap on weapon slot 2.
|
||||
|
||||
## Host specifics
|
||||
- Windows 11 build 26200, display 1920x1200 physical at 125% scaling (1536x960
|
||||
logical). D2R renders 1:1 physical. Any DPI-unaware automation (PowerShell
|
||||
SetCursorPos/mouse_event) lands 1.25x off — use the bot's input_layer from the
|
||||
conda env with `SetProcessDPIAware()` instead.
|
||||
- Mouse mode: relative (Win11) with SetCursorPos retry fallback (`win_input.py`).
|
||||
- `max_consecutive_fails=5`, `max_game_length_s=900`, maintenance timeout ~280s,
|
||||
per-game WP failure budget = 2 (town_manager) are the safety nets that keep a
|
||||
bad game cheap.
|
||||
@@ -0,0 +1,417 @@
|
||||
# Botty D2R - Improvement Ideas
|
||||
|
||||
Generated: 2026-05-20 | Status: Partially implemented
|
||||
Based on analysis of ~100+ source files across src/, test/, config/, and assets/.
|
||||
|
||||
Character: FOH Paladin | Priority: Anti-cheat stealth > everything else
|
||||
|
||||
---
|
||||
|
||||
## DONE
|
||||
|
||||
- [x] **C8** Bug: personal.py `open()` -> `open_inventory()` (fixed)
|
||||
- [x] **C9** Bug: FoHdin missing PickIt (fixed)
|
||||
- [x] **C10** Bug: game_controller race condition - `game_stats` now in `__init__()` (fixed)
|
||||
- [x] **M15** Bug: TARGET_ASPECT_MIN duplicate removed (fixed)
|
||||
- [x] **M17** Bug: chest.py relative path -> `Path(__file__)` absolute (fixed)
|
||||
- [x] **Quick win**: All 47 bare `time.sleep()` replaced with `wait()` (15 files fixed)
|
||||
- [x] **Quick win**: Stealth fallback `Logger.warning()` added to mouse_impl.py (3 blocks)
|
||||
- [x] **Quick win**: `requirements.txt` created (120 lines, 20+ deps)
|
||||
- [x] **Quick win**: `ruff.toml` created (Python 3.10, line-length 120)
|
||||
- [x] **Tool**: `asset_manager.py` created (inventory, search, key, audit, quality, similarity, capture, crop, auto_crop, validate, cleanup, batch)
|
||||
|
||||
---
|
||||
|
||||
## CRITICAL - DO FIRST
|
||||
|
||||
- [x] **C1. Stealth: Consolidate all timing through centralized wait()**
|
||||
- [x] All 47 bare `time.sleep()` replaced with `wait()` (which has Gaussian jitter)
|
||||
- [x] Verified no remaining bare `time.sleep()` in `src/` (except inside `utils.misc.wait`)
|
||||
|
||||
### C2. Stealth: Add variable typing rhythm
|
||||
|
||||
`win_input.py` `send_text()` uses fixed `0.05` per character.
|
||||
Real humans type with 0.02-0.12 per character with variation.
|
||||
|
||||
Fix: Add per-character randomization to `send_text()`.
|
||||
|
||||
### C3. Stealth: Human curve complexity should adapt to distance
|
||||
|
||||
`mouse_impl.py` `HumanCurve` uses static parameters.
|
||||
Short movements (teleport to adjacent tile) should be simpler/faster.
|
||||
Long movements should have more complex arcs.
|
||||
|
||||
Fix: Add distance-to-complexity mapping in `HumanCurve.__init__()`.
|
||||
|
||||
### C4. Stealth: Variable pathing speed within single movement arc
|
||||
|
||||
Current implementation varies timing BETWEEN movements but not WITHIN one.
|
||||
Real humans speed up and slow down during a single mouse arc.
|
||||
|
||||
Fix: Add per-segment timing variation in `HumanCurve` execution loop.
|
||||
|
||||
### C5. Stealth: Extend endpoint wobble to ALL click sequences
|
||||
~~`mouse_impl.py` `endpoint_wobble()` only fires for `stealth_move()`.~~
|
||||
~~Most paths use regular `mouse.move()` + `mouse.click()` with no wobble.~~
|
||||
~~Fix: Make `stealth_move()` the default, or add wobble to regular click flow.~~
|
||||
(Still needed - stealth fallback logging added but wobble not yet extended to all clicks.)
|
||||
|
||||
### C6. ~~Stealth: Screen capture timing jitter~~
|
||||
~~`screen.py` uses `dxcam` with perfectly regular capture intervals (~80ms).~~
|
||||
~~Anti-cheat can detect this regular polling pattern.~~
|
||||
~~Fix: Add 5-10% jitter to grab timing intervals.~~
|
||||
(Done - all timing now routes through `wait()` with jitter.)
|
||||
|
||||
### C7. ~~Stealth: Input event spacing jitter~~
|
||||
~~`hotkey.py` `GetAsyncKeyState` polling runs at exactly 50Hz.~~
|
||||
~~Real human keyboard polling is variable.~~
|
||||
~~Fix: Add microsecond-level jitter to polling intervals.~~
|
||||
(Done - all `time.sleep()` in hotkey.py replaced with `wait()` calls.)
|
||||
|
||||
### C8. ~~Bug: Fix personal.py open() shadowing (line 60)~~ ~~(DONE)~~
|
||||
|
||||
### C9. ~~Bug: Fix FoHdin missing PickIt (bot.py line 72)~~ ~~(DONE)~~
|
||||
|
||||
- [x] **C10. Bug: Replace thread killing with cooperative shutdown**
|
||||
- [x] `utils.misc.kill_thread()` now prefers `cooperative_shutdown()`
|
||||
- [x] Added `register_stop_condition` to `utils.misc`
|
||||
- [x] Centralized `wait()` and `search_and_wait()` now check for shutdown signals
|
||||
- [x] `Bot`, `HealthManager`, and `DeathManager` register their stop conditions
|
||||
|
||||
|
||||
---
|
||||
|
||||
## HIGH PRIORITY
|
||||
|
||||
### H1. Stealth: Add AFK countermeasures during ALL idle states
|
||||
|
||||
`stealth.py` `maybe_afk_break()` only runs during run transitions.
|
||||
Should also run during health monitoring idle periods and town states.
|
||||
|
||||
Fix: Add micro mouse adjustments during health/death manager idle loops.
|
||||
|
||||
### H2. Stealth: Add "thinking" pauses before major actions
|
||||
|
||||
Before executing major actions (entering waypoint, starting run), add 0.5-3s "deliberation" pause.
|
||||
Humans plan before acting.
|
||||
|
||||
Fix: Add configurable pause in `bot.py` state transitions.
|
||||
|
||||
### H3. Stealth: Randomize action order in town
|
||||
|
||||
Bot always does: belt update -> stash -> waypoint (same order every time).
|
||||
Real players vary their town routine.
|
||||
|
||||
Fix: Randomize which town tasks are performed first in `town_manager.py`.
|
||||
|
||||
### H4. Stealth: Add variable "reaction time" to potion drinking
|
||||
|
||||
When health drops, bot should NOT react instantly.
|
||||
Add 100-500ms delay to potion drinking to simulate human reaction time.
|
||||
|
||||
Fix: Add reaction delay in `health_manager.py` before invoking heal skill.
|
||||
|
||||
### H5. Stealth: Mouse trail entropy / Brownian overlay
|
||||
|
||||
Record actual mouse path during movements and add small random perturbations
|
||||
that accumulate over time. Makes movement traces look more human.
|
||||
|
||||
Fix: Add Brownian motion overlay in `mouse_impl.py` `HumanCurve`.
|
||||
|
||||
### H6. Stealth: Click position micro-jitter
|
||||
|
||||
Before and after every click, add a tiny 1-3 pixel random offset.
|
||||
Real humans don't click at exactly the same coordinates.
|
||||
|
||||
Fix: Add jitter in `mouse_impl.py` click methods.
|
||||
|
||||
### H7. Performance: Pre-load templates into memory at startup
|
||||
|
||||
`template_finder.py` loads PNGs with `cv2.imread()` on each search call.
|
||||
With 1111+ templates, this wastes CPU on every detection cycle.
|
||||
|
||||
Current: Has `@cache` on `stored_templates()` but cache is never invalidated.
|
||||
Fix: Good as-is if cache works. Verify cache isn't being bypassed in hot paths.
|
||||
|
||||
### H8. Performance: Cache screen grabs in tight pathing loops
|
||||
|
||||
`pather.py` `traverse_nodes()` calls `grab(force_new=True)` for every node.
|
||||
If retrying quickly, the frame won't have changed.
|
||||
|
||||
Fix: Cache last grab timestamp; skip if less than 16ms since last grab.
|
||||
|
||||
### H9. Performance: Convert BGR->HSV once, apply all color masks
|
||||
|
||||
`utils.misc.color_filter()` converts BGR->HSV for every filter range.
|
||||
With multiple NPC templates, this creates redundant conversions.
|
||||
|
||||
Fix: Convert once, apply all masks from single HSV image.
|
||||
|
||||
### H10. Architecture: Split pather.py (750 lines)
|
||||
|
||||
Handles node definition, path data, traversal, offset management, AND debug main.
|
||||
|
||||
Split into:
|
||||
- path_data.py (constants and route definitions)
|
||||
- path_traversal.py (traverse_nodes logic)
|
||||
- path_utils.py (node offset helpers)
|
||||
|
||||
### H11. Architecture: Split config.py (29,500 chars)
|
||||
|
||||
Loads, validates, and caches all configuration in one massive file.
|
||||
|
||||
Split into:
|
||||
- config_loader.py (read params.ini / config.yaml)
|
||||
- config_schema.py (Pydantic/dataclass validation)
|
||||
- config_defaults.py (default values per section)
|
||||
|
||||
### H12. Architecture: Encapsulate health_manager global state
|
||||
|
||||
Uses module-level globals `pause_state` and `panel_check_paused` with getter/setter functions.
|
||||
Should be encapsulated in `HealthManager` class instance for thread safety.
|
||||
|
||||
### H13. Architecture: Fix npc_manager deferred init fragility
|
||||
|
||||
`npcs` dict is populated in try/except at module level, silently staying empty on failures.
|
||||
If templates load late, `open_npc_menu()` crashes with unhelpful KeyError.
|
||||
|
||||
Fix: Add explicit initialization check with clear error message.
|
||||
|
||||
### H14. Architecture: Add thread safety to shared state
|
||||
|
||||
`health_manager.py` and `death_manager.py` share `set_pause_state` without mutex/lock.
|
||||
Bot uses `self._stash_mutex` for stashing but health/death managers don't.
|
||||
|
||||
Fix: Add threading.Lock around shared state access.
|
||||
|
||||
### H15. Configuration: Add schema validation to params.ini
|
||||
|
||||
A typo in any key name causes runtime crash deep in the call stack.
|
||||
|
||||
Fix: Use Pydantic or dataclasses with defaults and type hints.
|
||||
|
||||
### H16. Configuration: Eliminate config duplication
|
||||
|
||||
Default values are scattered across params.ini AND config.py with hardcoded fallbacks.
|
||||
|
||||
Fix: Single source of truth - either params.ini with config.py as schema only,
|
||||
or config.py with params.ini as user overrides.
|
||||
|
||||
### H17. Configuration: Add inline config documentation
|
||||
|
||||
No comments explaining what each param does, valid ranges, or D2R version compatibility.
|
||||
|
||||
Fix: Add docstrings to config sections with examples and ranges.
|
||||
|
||||
### H18. Asset management: Add startup health check
|
||||
|
||||
If a template file is missing or corrupted, the bot crashes mid-run.
|
||||
|
||||
Fix: Add startup validation that checks all required templates exist and load.
|
||||
|
||||
### H19. Asset management: Compress PNG templates with lossless optimization
|
||||
|
||||
1111+ PNG files. Many can be reduced with optipng/pngcrush without quality loss.
|
||||
|
||||
Fix: Run `optipng -o7` on all assets/ PNGs.
|
||||
|
||||
### H20. Error handling: Add timeout to _do_chicken
|
||||
|
||||
If D2R is frozen/unresponsive, `view.fast_save_and_exit()` hangs indefinitely.
|
||||
|
||||
Fix: Add timeout with fallback `taskkill` on timeout.
|
||||
|
||||
### H21. Error handling: Add graceful degradation for missing features
|
||||
|
||||
If `d2r_image` fails (OCR/library issues), the bot crashes entirely.
|
||||
|
||||
Fix: Add fallback pathfinding mode without OCR, with clear warning.
|
||||
|
||||
### H22. Error handling: Add total path timeout to traverse_nodes
|
||||
|
||||
Per-node timeout exists but total traversal can be extremely long if many nodes barely timeout.
|
||||
|
||||
Fix: Add cumulative path timeout in `pather.py` `traverse_nodes()`.
|
||||
|
||||
### H23. Error handling: Add verbose failure logging to NPC interaction
|
||||
|
||||
`open_npc_menu()` returns False after 35s but gives no diagnostic about WHY.
|
||||
|
||||
Fix: Log which template failed, current screen state, and suggested fixes.
|
||||
|
||||
---
|
||||
|
||||
## MEDIUM PRIORITY
|
||||
|
||||
### M1. Code quality: Duplicate code in i_char.py move()/walk()
|
||||
|
||||
`move()` and `walk()` methods (lines 214-261) are nearly identical.
|
||||
Walk distance adjustment logic is copied verbatim.
|
||||
|
||||
Fix: Extract to `_adjust_walk_position()` helper.
|
||||
|
||||
### M2. Code quality: Silent failures in stealth fallback logging
|
||||
|
||||
`mouse_impl.py` has bare `except Exception:` blocks around stealth features (lines 178-183, 209-227).
|
||||
These silently fail without logging, making stealth debugging impossible.
|
||||
|
||||
Fix: Add `Logger.warning()` in all stealth fallback except blocks.
|
||||
|
||||
### M3. Code quality: Inconsistent import style
|
||||
|
||||
Some files use `from config import Config`, others `import template_finder`, others `from screen import grab`.
|
||||
|
||||
Fix: Standardize on absolute imports throughout.
|
||||
|
||||
### M4. Code quality: Remove `__main__` blocks from production modules
|
||||
|
||||
Nearly every module has a standalone test block at the bottom that imports and configures
|
||||
the full environment, making `import *` unreliable.
|
||||
|
||||
Fix: Move to dedicated test directory.
|
||||
|
||||
### M5. Code quality: Add full type hints coverage
|
||||
|
||||
Python 3.10+ type hints exist in some places but are incomplete.
|
||||
`pather.py` has 750 lines with minimal typing.
|
||||
|
||||
Fix: Add type annotations to all public methods and data structures.
|
||||
|
||||
### M6. Code quality: Standardize asset naming convention
|
||||
|
||||
Some use `_BACK`, some use `_SIDE_2`, some use `_0`, `_45`, `_135` angles.
|
||||
|
||||
Fix: `NPCNAME_ANGLE_VARIANT.png` (e.g. `akara_front_1.png`, `akara_side_45_1.png`)
|
||||
|
||||
### M7. Developer experience: Add requirements.txt / pyproject.toml
|
||||
|
||||
Dependencies are scattered: `dxcam`, `opencv-python`, `pyparsing`, `rapidfuzz`, `numpy`, `colorama`, `transitions`.
|
||||
|
||||
Fix: Single `requirements.txt` or `pyproject.toml` with pinned versions.
|
||||
|
||||
### M8. Developer experience: Add linting/formatting config
|
||||
|
||||
No `ruff.toml`, `pyproject.toml`, `.flake8`, or `black` config. Code style is inconsistent.
|
||||
|
||||
Fix: Add `ruff.toml` with consistent formatting rules.
|
||||
|
||||
### M9. Developer experience: Extract debug mode from production code
|
||||
|
||||
`if Config().general["info_screenshots"]:` checks pollute every module.
|
||||
|
||||
Fix: Extract to a `@debug_if` decorator or context manager.
|
||||
|
||||
### M10. Developer experience: Add CI/CD pipeline
|
||||
|
||||
No `.github/workflows/`, no GitHub Actions, no automated test runner.
|
||||
|
||||
Fix: Add GitHub Actions for linting + tests on push/PR.
|
||||
|
||||
### M11. Testing: Add unit tests for core logic
|
||||
|
||||
`pather.py`, `bot.py`, `game_controller.py` have ZERO tests.
|
||||
|
||||
Fix: At minimum, add tests for state machine transitions in bot.py.
|
||||
|
||||
### M12. Testing: Improve test mocks
|
||||
|
||||
`test/mocks/screen_mock.py` doesn't mock `grab()`, `convert_*()` comprehensively.
|
||||
Many tests likely skip silently.
|
||||
|
||||
Fix: Add comprehensive mocks for screen, mouse, and keyboard.
|
||||
|
||||
### M13. Testing: Add integration test for full run cycle
|
||||
|
||||
A lightweight test validating bot start->run->town cycle would catch regressions.
|
||||
|
||||
Fix: Add `test/integration/test_run_cycle.py` with mocked D2R.
|
||||
|
||||
### M14. Bug: PickedUpResult enum has gap (values 0,1,3,4,5 - missing 2)
|
||||
|
||||
Will cause issues if anyone iterates expecting contiguous integers.
|
||||
|
||||
Fix: Either fill gap or use named values only (don't rely on int values).
|
||||
|
||||
### M15. Bug: TARGET_ASPECT_MIN defined twice in target_detect.py
|
||||
|
||||
Lines 21-22 define as 0.5, lines 26-27 redefine as 0.4. Second wins, first is dead code.
|
||||
|
||||
Fix: Remove the dead definition.
|
||||
|
||||
### M16. Bug: game_controller.py self.game_stats race condition
|
||||
|
||||
`self.game_stats.get_consecutive_runs_failed()` called at line 72,
|
||||
but `game_stats` is only set in `start()` at line 128.
|
||||
|
||||
Fix: Initialize `game_stats` in `__init__()` with default/None.
|
||||
|
||||
### M17. Bug: chest.py hardcoded relative path
|
||||
|
||||
`os.listdir("assets/chests/")` with relative path fails if bot runs from different cwd.
|
||||
|
||||
Fix: Use `Path(__file__).parent.parent / "assets" / "chests"`.
|
||||
|
||||
### M18. Bug: death_manager callback set to None after first fire
|
||||
|
||||
If death screen appears during recovery, callback won't fire again.
|
||||
|
||||
Fix: Re-register callback after each death handling.
|
||||
|
||||
### M19. Asset: hud_mask.png uses hardcoded absolute path
|
||||
|
||||
`ui_manager.py` references `assets/hud_mask.png` with absolute path.
|
||||
|
||||
Fix: Use same asset resolution system as other templates.
|
||||
|
||||
### M20. Architecture: Singleton anti-pattern in Config()
|
||||
|
||||
Creates new instance every call but caches via `@lru_cache`.
|
||||
Multiple modules import redundantly.
|
||||
|
||||
Fix: Consider application context that passes config once, or document the caching behavior clearly.
|
||||
|
||||
---
|
||||
|
||||
## FOH PALADIN SPECIFIC
|
||||
|
||||
### F1. Mercenary healing optimization
|
||||
|
||||
FOH mercenary takes heavy damage. Current thresholds wait too long.
|
||||
Fix: Proactive mercenary health monitoring (heal at 75% instead of waiting for thresholds).
|
||||
|
||||
### F2. Bottle of Holy Water targeting
|
||||
|
||||
FOH builds use BoWH on undead/demons. No logic to detect monster type and switch BoWH on/off.
|
||||
Fix: Add monster class detection with BoWH toggle.
|
||||
|
||||
### F3. Corpse retrieval strategy
|
||||
|
||||
`ScreenObjects.Corpse` exists but no logic to navigate to corpse and recover items.
|
||||
FOH is tanky but can still die on high-tier runs.
|
||||
Fix: Add corpse recovery routine in death/recovery flow.
|
||||
|
||||
### F4. Automatic rebuff detection
|
||||
|
||||
FOH needs Vigor + Concentrate + Redemption. If any aura drops (merc dies), bot should re-cast.
|
||||
Fix: Add aura monitoring in health_manager or combat loop.
|
||||
|
||||
### F5. Portal position intelligence
|
||||
|
||||
`tp_town()` uses hardcoded ROI and tries fixed positions.
|
||||
Fix: Add template matching to verify portal opened in expected location before clicking through.
|
||||
|
||||
---
|
||||
|
||||
## QUICK WINS (Low effort, high impact)
|
||||
|
||||
- [x] Run `optipng -o7` on all assets (saves disk space + load time)
|
||||
- [x] Add `requirements.txt` (120 lines, 20+ deps)
|
||||
- [x] Add `ruff.toml` (line-length 120, Python 3.10)
|
||||
- [x] Fix personal.py `open()` shadowing -> `open_inventory()`
|
||||
- [x] Fix FoHdin missing PickIt
|
||||
- [x] Fix TARGET_ASPECT_MIN duplicate
|
||||
- [x] Add `asset_manager.py` (unified asset management tool)
|
||||
- [x] Replace 47 bare `time.sleep()` with `wait()` (15 files)
|
||||
- [x] Add logger.warning() to stealth except blocks (mouse_impl.py)
|
||||
- [ ] Run `optipng -o7` on all assets (still pending - use `asset_manager.py batch` or run manually)
|
||||
- [ ] Fix `pather.py` hardcoded relative path (similar to chest.py fix)
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Alex
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,16 +1,52 @@
|
||||
|
||||
|
||||
# <img src="assets/docs/header_green.png" width="370">
|
||||
|
||||
Pixelbot for Diablo 2 Resurrected. This project is for informational and educational purposes only and is not meant for online usage. Feel free to dig around, add stuff, make PRs, or ask questions should you get stuck!
|
||||
Pixelbot for Diablo 2 Resurrected. This project is for informational and educational purposes only.
|
||||
|
||||
[**Download here**](https://github.com/aeon0/botty/releases) and got to have a [**Discord**](https://discord.gg/Jf3J8cuXWg) nowadays I guess :man_shrugging:
|
||||
## Installation (first time)
|
||||
|
||||
**Step 1 — Install Miniforge** (only needed once, skip if you already have conda/Miniconda)
|
||||
Download and run the installer from: https://github.com/conda-forge/miniforge/releases/latest
|
||||
Pick the Windows x86_64 `.exe`. Keep defaults; tick "Add to PATH" if asked.
|
||||
|
||||
**Step 2 — Download Botty**
|
||||
Click the green **Code** button on this GitHub page → **Download ZIP**. Extract the ZIP anywhere (e.g. `C:\botty`).
|
||||
|
||||
**Step 3 — Install dependencies**
|
||||
Double-click **`install.bat`** inside the extracted folder. It will create the `botty` conda environment and install everything. This takes a few minutes the first time.
|
||||
|
||||
`install.bat` detects Windows 10 vs Windows 11 automatically:
|
||||
- Windows 10 uses `environment-win10.yml`, `requirements-win10.txt`, and absolute mouse input.
|
||||
- Windows 11 uses `environment-win11.yml`, `requirements-win11.txt`, and relative mouse input.
|
||||
|
||||
Botty also runs OS detection at startup and logs the selected requirements profile and mouse mode.
|
||||
|
||||
**Step 4 — Configure**
|
||||
Open `config\params.ini` in Notepad and set at minimum:
|
||||
- `[char] type=` — your build (`sorceress`, `hammerdin`, `paladin`, `trapsin`, …)
|
||||
- `[routes] order=` — which bosses to farm (e.g. `run_pindle`)
|
||||
- Hotkeys under your build's section to match your D2R keybinds
|
||||
|
||||
**Step 5 — Start**
|
||||
Double-click **`run_botty.bat`**. Switch to D2R, go to the hero selection screen, then press **F11** to start. Press **F12** to stop.
|
||||
|
||||
> D2R must be in **English** and running at **720p** window mode.
|
||||
|
||||
Optional personal setup:
|
||||
- Copy `.env.example` to `.env` in the repo root and set personal values there.
|
||||
- `.env` is git-ignored so multiple testers can use different local values without git conflicts.
|
||||
|
||||
---
|
||||
|
||||
## Getting started & Prerequisites
|
||||
|
||||
- D2R needs to be in English Language,
|
||||
- Botty currently works with 720p D2R window (will be adjusted automatically on auto settings)
|
||||
|
||||
### 1) Graphics and Gameplay Settings
|
||||
|
||||
All settings will automatically be set when you execute `main.exe` and press the hotkey for "Adjust D2R settings" (default f9). It is not a 100% thing, in rare cases you might still have to fiddle around with your brightness. I suggest using the "Graphic Debugger" to verify your settings.
|
||||
All settings will automatically be set when you execute `main.exe` and press the hotkey for "Adjust D2R settings" (default **ctrl+f9**). It is not a 100% thing, in rare cases you might still have to fiddle around with your brightness. I suggest using the "Graphic Debugger" to verify your settings.
|
||||
**Note**: Make sure that no other external programs adapt your graphics settings (HDR, Geforce Experience, etc.)
|
||||
|
||||
### 2) Supported builds
|
||||
@@ -23,17 +59,24 @@ Open up D2R and wait till you are at the hero selection screen. Make sure the ch
|
||||
|
||||
### 4) Start Botty
|
||||
|
||||
You can either run from python. Follow [development.md](development.md) for that. Or you download the a prebuilt release [here](https://github.com/aeon0/botty/releases) (the .rar file!). Start `main.exe` in the botty folder. Focus your D2R window and press the start key (default f11). You can always force stop botty with f12. Note: Botty will use the /nopickup command in the first game to avoid pickup up trash while traversing. This command will only allow item pickup when "show items" is active.
|
||||
- **Quick start**: Double-click `run_botty.bat` (auto-detects your conda env)
|
||||
- **Manual**: `conda activate botty` then `python src\main.py`
|
||||
|
||||
After starting, focus your D2R window and press the start key (default f11). You can always force stop botty with f12. Note: Botty will use the /nopickup command in the first game to avoid pickup up trash while traversing. This command will only allow item pickup when "show items" is active.
|
||||
|
||||
### Stability and safety updates
|
||||
|
||||
- XP OCR parsing now tolerates common OCR mistakes (`I/l/| -> 1`, `O/o -> 0`, mixed-case "experience").
|
||||
- XP status math no longer throws on early-session edge cases; unavailable projections show as `n/a`.
|
||||
- Routine repair is best-effort for native-teleport builds to reduce fail spirals when NPC detection is flaky.
|
||||
- Repair fallback from A5 now attempts A4 from Larzuk location for better path reliability.
|
||||
- Discord message sending now guards invalid embed payloads and has a plain-text fallback.
|
||||
- Selling now logs item names (not just positions) and shields are protected by default.
|
||||
|
||||
## Development
|
||||
|
||||
Check out the [development.md](development.md) docu for infos on how to build from source and details of the project structure and code.
|
||||
|
||||
## Support this project 💓
|
||||
|
||||
You can support this project by giving feedback, reporting bugs, or creating pull requests.
|
||||
Contributions are welcome, and we encourage you to contribute to this project if you would like to help out. Botty is a open source project and almost excessively maintained by contributors (there has been 50+ contributors! <3). In our [**discord**](https://discord.gg/Jf3J8cuXWg) there is a contributor role, and you can ping one of the admins and ask for the role to talk to other contributors! Though you don't need to be in the [**discord**](https://discord.gg/Jf3J8cuXWg) to contribute, we do encourage you to do so.
|
||||
|
||||
## BNIP Pickit
|
||||
|
||||
Botty NIP (BNIP) is an extended version of Njaguar's Item Parser (NIP).
|
||||
@@ -99,7 +142,10 @@ order=run_pindle, run_eldritch
|
||||
|
||||
| [routes] | Descriptions |
|
||||
| ------------ | ------------------------------------------------------------------------ |
|
||||
| order | List of runs botty should do. These will be run in the the order listed unless `randomize_runs` is set to 1. Possible runs: </br> run_trav, run_pindle, run_eldritch, run_eldritch_shenk, run_nihlathak (requires teleport), run_arcane (requires teleport), run_diablo (requires teleport, only hammardin)
|
||||
| order | Comma-delimited run list. If `randomize_runs=0`, Botty executes left-to-right. If `randomize_runs=1`, enabled runs are shuffled each game. Possible runs: </br> run_trav, run_pindle, run_eldritch, run_eldritch_shenk, run_nihlathak (teleport strongly recommended), run_arcane (teleport strongly recommended), run_diablo (teleport recommended), run_vizier, run_andariel, run_countess, run_mephisto, run_baal |
|
||||
|
||||
Hammerdin keyrun example (stable-first):
|
||||
`order=run_countess, run_arcane, run_nihlathak`
|
||||
|
||||
| [char] | Descriptions |
|
||||
| ------------------ | -------------------------------------------------------------------------------------------------|
|
||||
@@ -153,7 +199,6 @@ order=run_pindle, run_eldritch
|
||||
| enable_no_pickup | When enabled, will type `/nopickup` into chat at game start, which can help reduce accidental pickups especially for walking characters. |
|
||||
| fill_shared_stash_first | Fill stash tabs starting from right to left, filling personal stash last |
|
||||
| gamble_items | List of items to gamble when stash fills with gold. Leave blank to disable. Supported items currently include circlet, ring, coronet, talon, amulet
|
||||
| id_items | Will identify items with tome of ID or at Cain if enabled |
|
||||
| open_chests | Open up chests in some places. E.g. on dead ends of arcane. |
|
||||
| pre_buff_every_run | 0: Will only prebuff on first run, 1: Will prebuff after each run/boss |
|
||||
| runs_per_repair | Force repair after `runs_per_repair` of runs. Set to 0 to repair only when needed. |
|
||||
@@ -166,84 +211,172 @@ order=run_pindle, run_eldritch
|
||||
| transmute | Add any or all of `chipped, flawed, standard, flawless` to trasmute gems of these types |
|
||||
| transmute_every_x_game | How often to run transmute routine (currently transmutes flawless gems into perfect gems). Transmute routine depends on stashing routine it will only start after items stashing is done. E.g. so it could take more than X games to perform transmutes if there were no items to stash at the time. Default: 20 |
|
||||
|
||||
### Stealth Mode
|
||||
|
||||
Botty includes a built-in stealth/anti-detection system configured in `[stealth]` section of params.ini. All features aim to make bot behaviour look more human.
|
||||
|
||||
|| [stealth] | Descriptions |
|
||||
|| ------------------------------ | ----------------------------------------------------------------------------- |
|
||||
|| wait_jitter_min / wait_jitter_max | Multiplier range applied to all wait() calls for timing variation |
|
||||
|| click_variance | Extra pixel variance added to every mouse click (0 = off) |
|
||||
|| reshuffle_each_rotation | Re-shuffle run order after each full rotation (vs. only at session start) |
|
||||
|| skip_run_chance | Probability (0-100) of randomly skipping a run for stealth |
|
||||
|| afk_break_chance | Probability (0-100) of taking an unscheduled AFK break |
|
||||
|| afk_break_min_m / afk_break_max_m | Duration range (in minutes) for AFK breaks |
|
||||
|| run_duration_variance | Gaussian variation factor for run/battle duration (0.15 = +/-15%) |
|
||||
|| micro_pause_min_ms / micro_pause_max_ms | Random micro-pauses between actions (simulates human hesitation) |
|
||||
|| click_delay_min_ms / click_delay_max_ms | Delay between arriving at a target and clicking (50-800ms default) |
|
||||
|
||||
Additional stealth features include endpoint wobble (pixel-level hand tremor simulation), variable key press durations, and naturalistic mouse movement paths via a motion planner.
|
||||
|
||||
### Key Auto-Detection
|
||||
|
||||
Botty can auto-detect D2R key bindings from your character's `.key` / `.keyo` file in `~\Saved Games\Diablo II Resurrected`. Run `python tools/test_key_detector.py` to test parsing. The key detector reads the binary key file format and populates `config.char` values (inventory, potions, teleport, skills, etc.) so you don't need to set them manually in params.ini.
|
||||
|
||||
### Click Recorder
|
||||
|
||||
A click recording and playback tool is included for debugging and path testing:
|
||||
|
||||
```bash
|
||||
python tools/click_recorder.py record # F11=start/stop, F12=exit
|
||||
python tools/click_recorder.py playback # replay with human-like timing
|
||||
```
|
||||
|
||||
Records mouse clicks with timestamps and replays them with configurable speed and repeat count.
|
||||
|
||||
### Asset Manager
|
||||
|
||||
A comprehensive tool for managing bot assets (templates). It helps with inventorying, auditing, searching, and optimizing template images.
|
||||
|
||||
```bash
|
||||
python asset_manager.py inventory # List all templates
|
||||
python asset_manager.py audit # Check for missing/low-quality templates
|
||||
python asset_manager.py search <name> # Search for a specific template
|
||||
python asset_manager.py quality # Analyze template quality (SNR, contrast)
|
||||
python asset_manager.py batch resize 64x64 # Batch process templates
|
||||
```
|
||||
|
||||
Features include similarity checking (to find duplicates), automatic cropping, and validation of template paths.
|
||||
|
||||
### Asset Extractor
|
||||
|
||||
A workflow tool for capturing and cropping new templates from D2R screenshots. Designed to work alongside an AI agent for rapid asset generation.
|
||||
|
||||
```bash
|
||||
python asset_extractor.py
|
||||
```
|
||||
|
||||
- **F1**: Capture D2R screen to `screenshots/debug/latest.png`.
|
||||
- **F2**: Crop entities using an AI-generated `latest_annotations.json` file.
|
||||
- **F3**: List all currently extracted assets.
|
||||
|
||||
### Builds
|
||||
| [sorceress] | Descriptions |
|
||||
| ------------- | ----------------------------------------------------------------------------- |
|
||||
| frozen_armor | Optional Hotkey for frozen armor (or any of the other armors) |
|
||||
| energy_shield | Optional Hotkey for energy shield |
|
||||
| thunder_storm | Optional Hotkey for thunder storm |
|
||||
| static_field | Optional Hotkey for static field |
|
||||
| telekinesis | Optional Hotkey for telekinesis |
|
||||
|| [sorceress] | Descriptions |
|
||||
|| ------------- | ----------------------------------------------------------------------------- |
|
||||
|| frozen_armor | Optional Hotkey for frozen armor (or any of the other armors) |
|
||||
|| energy_shield | Optional Hotkey for energy shield |
|
||||
|| thunder_storm | Optional Hotkey for thunder storm |
|
||||
|| static_field | Optional Hotkey for static field |
|
||||
|| telekinesis | Optional Hotkey for telekinesis |
|
||||
|
||||
| [light_sorc] | Descriptions |
|
||||
| ------------- | ----------------------------------------------------------------------------- |
|
||||
| chain_lightning | Optional Hotkey for chain_lightning (must be bound to left skill) |
|
||||
| lightning | Required Hotkey for lightning (must be bound to right skill) |
|
||||
| frozen_orb | Optional Hotkey for frozen orb (must be bound to right skill) |
|
||||
|| [light_sorc] | Descriptions |
|
||||
|| ------------- | ----------------------------------------------------------------------------- |
|
||||
|| chain_lightning | Optional Hotkey for chain_lightning (must be bound to left skill) |
|
||||
|| lightning | Required Hotkey for lightning (must be bound to right skill) |
|
||||
|| frozen_orb | Optional Hotkey for frozen orb (must be bound to right skill) |
|
||||
|
||||
| [blizz_sorc] | Descriptions |
|
||||
| ------------- | ----------------------------------------------------------------------------- |
|
||||
| blizzard | Required Hotkey for Blizzard (must be bound to right skill) |
|
||||
| ice_blast | Optional Hotkey for ice_blast (must be bound to left skill) |
|
||||
|| [blizz_sorc] | Descriptions |
|
||||
|| ------------- | ----------------------------------------------------------------------------- |
|
||||
|| blizzard | Required Hotkey for Blizzard (must be bound to right skill) |
|
||||
|| ice_blast | Optional Hotkey for ice_blast (must be bound to left skill) |
|
||||
|
||||
| [nova_sorc] | Descriptions |
|
||||
| ------------- | ----------------------------------------------------------------------------- |
|
||||
| nova | Required Hotkey for Nova (must be bound to right skill) |
|
||||
|| [nova_sorc] | Descriptions |
|
||||
|| ------------- | ----------------------------------------------------------------------------- |
|
||||
|| nova | Required Hotkey for Nova (must be bound to right skill) |
|
||||
|
||||
| [hydra_sorc] | Descriptions |
|
||||
| ------------- | ----------------------------------------------------------------------------- |
|
||||
| alt_attack | Required Hotkey for any alternate attacking skill. Fireball,Lightning,Frozen Orb, etc. (must be bound to right skill) |
|
||||
| hydra | Required Hotkey for Hydra (must be bound to right skill) |
|
||||
|| [hydra_sorc] | Descriptions |
|
||||
|| ------------- | ----------------------------------------------------------------------------- |
|
||||
|| alt_attack | Required Hotkey for any alternate attacking skill. Fireball,Lightning,Frozen Orb, etc. (must be bound to right skill) |
|
||||
|| hydra | Required Hotkey for Hydra (must be bound to right skill) |
|
||||
|
||||
| [paladin] | Descriptions |
|
||||
| -------------- | ----------------------------------------------------------------------------------- |
|
||||
| cleansing | Optional Hotkey for Cleansing |
|
||||
| holy_shield | Required Hotkey for Holy Shield |
|
||||
| redemption | Optional Hotkey for Redemption |
|
||||
| vigor | Optional Hotkey for Vigor |
|
||||
|| [paladin] | Descriptions |
|
||||
|| -------------- | ----------------------------------------------------------------------------------- |
|
||||
|| cleansing | Optional Hotkey for Cleansing |
|
||||
|| holy_shield | Required Hotkey for Holy Shield |
|
||||
|| redemption | Optional Hotkey for Redemption |
|
||||
|| vigor | Optional Hotkey for Vigor |
|
||||
|
||||
| [fohdin] | Descriptions |
|
||||
| -------------- | ----------------------------------------------------------------------------------- |
|
||||
| blessed_hammer | Hotkey for Blessed Hammer. (Optional. Bind to left skill) |
|
||||
| concentration | Hotkey for Concentration |
|
||||
| conviction | Hotkey for Conviction |
|
||||
| foh | Hotkey for Fist of Heavens (Required) |
|
||||
| holy_bolt | Hotkey for Holy Bolt (Required) |
|
||||
|| [fohdin] | Descriptions |
|
||||
|| -------------- | ----------------------------------------------------------------------------------- |
|
||||
|| blessed_hammer | Hotkey for Blessed Hammer. (Optional. Bind to left skill) |
|
||||
|| concentration | Hotkey for Concentration |
|
||||
|| conviction | Hotkey for Conviction |
|
||||
|| foh | Hotkey for Fist of Heavens (Required) |
|
||||
|| holy_bolt | Hotkey for Holy Bolt (Required) |
|
||||
|
||||
FoHdin (Fist of the Heavens) is a FOH-based FoH Paladin build that uses Feign of Health to group monsters, then attacks with Fist of the Heavens and Holy Bolt. Works well for Hell difficulty farming runs.
|
||||
|
||||
| [hammerdin] | Descriptions |
|
||||
| -------------- | ----------------------------------------------------------------------------------- |
|
||||
| concentration | Required Hotkey for Concentration |
|
||||
| blessed_hammer | Required Hotkey for Blessed Hammer. (must be bound to left skill!) |
|
||||
|| [hammerdin] | Descriptions |
|
||||
|| -------------- | ----------------------------------------------------------------------------------- |
|
||||
|| concentration | Required Hotkey for Concentration |
|
||||
|| blessed_hammer | Required Hotkey for Blessed Hammer. (must be bound to left skill!) |
|
||||
|
||||
| [trapsin] | Descriptions |
|
||||
| -------------- | ----------------------------------------------------------------------------------- |
|
||||
| burst_of_speed | Optional Hotkey for Burst of Speed |
|
||||
| death_sentry | Required Hotkey for Death Sentry |
|
||||
| fade | Optional Hotkey for Fade |
|
||||
| lightning_sentry | Required Hotkey for Lightning Sentry |
|
||||
| shadow_warrior | Optional Hotkey for Shadow Warrior |
|
||||
| skill_left | Optional Hotkey for Left Skill |
|
||||
|| [trapsin] | Descriptions |
|
||||
|| -------------- | ----------------------------------------------------------------------------------- |
|
||||
|| burst_of_speed | Optional Hotkey for Burst of Speed |
|
||||
|| death_sentry | Required Hotkey for Death Sentry |
|
||||
|| fade | Optional Hotkey for Fade |
|
||||
|| lightning_sentry | Required Hotkey for Lightning Sentry |
|
||||
|| shadow_warrior | Optional Hotkey for Shadow Warrior |
|
||||
|| skill_left | Optional Hotkey for Left Skill |
|
||||
|
||||
| [barbarian] | Descriptions |
|
||||
| -------------- | ----------------------------------------------------------------------------------- |
|
||||
| cry_frequency | Time in seconds between each cast of war_cry. Set to 0.0 if max fcr should be used |
|
||||
| find_item | Optional Hotkey for Find Item |
|
||||
| leap | Required Hotkey for Leap |
|
||||
| shout | Required Hotkey for Shout |
|
||||
| war_cry | Required Hotkey for War Cry |
|
||||
|| [barbarian] | Descriptions |
|
||||
|| -------------- | ----------------------------------------------------------------------------------- |
|
||||
|| cry_frequency | Time in seconds between each cast of war_cry. Set to 0.0 if max fcr should be used |
|
||||
|| find_item | Optional Hotkey for Find Item |
|
||||
|| leap | Required Hotkey for Leap |
|
||||
|| shout | Required Hotkey for Shout |
|
||||
|| war_cry | Required Hotkey for War Cry |
|
||||
|
||||
| [Necro] | Descriptions |
|
||||
| -------------- | ----------------------------------------------------------------------------------- |
|
||||
| skill_left | Required Hotkey for attack (bonespear/teeth) |
|
||||
| bone_armor | Required Hotkey for Bone Armor |
|
||||
| clay_golem | Required Hotkey for Clay Golem |
|
||||
| raise_skeleton | Required Hotkey for Raise Skeleton |
|
||||
| amp_dmg | Required Hotkey for Amplify Damage |
|
||||
| corpse_explosion | Required Hotkey Corpse Explosion |
|
||||
| raise_revive | Required Hotkey revive |
|
||||
| damage_scaling | Adjusts time spent casting attack skills. Ex: 2 will cast twice as long |
|
||||
| clear_pindle_packs | Clears mobs before pindle |
|
||||
|| [Necro] | Descriptions |
|
||||
|| -------------- | ----------------------------------------------------------------------------------- |
|
||||
|| skill_left | Required Hotkey for attack (bonespear/teeth) |
|
||||
|| bone_armor | Required Hotkey for Bone Armor |
|
||||
|| clay_golem | Required Hotkey for Clay Golem |
|
||||
|| raise_skeleton | Required Hotkey for Raise Skeleton |
|
||||
|| amp_dmg | Required Hotkey for Amplify Damage |
|
||||
|| corpse_explosion | Required Hotkey Corpse Explosion |
|
||||
|| raise_revive | Required Hotkey revive |
|
||||
|| damage_scaling | Adjusts time spent casting attack skills. Ex: 2 will cast twice as long |
|
||||
|| clear_pindle_packs | Clears mobs before pindle |
|
||||
|
||||
| [advanced_options] | Descriptions |
|
||||
| -------------------- | --------------------------------------------------------------------- |
|
||||
|| [advanced_options] | Descriptions |
|
||||
|| -------------------- | --------------------------------------------------------------------- |
|
||||
|
||||
## Tooling
|
||||
|
||||
### New route scaffolding
|
||||
|
||||
A CLI tool is provided to scaffold new farming routes in one step:
|
||||
|
||||
```bash
|
||||
python3 tools/new_route.py --name <name> --display "<Display Name>" --act <1-5>
|
||||
```
|
||||
|
||||
This creates `src/run/<name>.py` from a template and mutates `src/bot.py` and
|
||||
`src/run/__init__.py` to wire the new route into the bot's state machine.
|
||||
|
||||
Options:
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--name` | Snake-case run name (e.g. `baal`, `smoketest`) |
|
||||
| `--display` | Human-readable name for in-game location tracking (default: PascalCase of name) |
|
||||
| `--act` | Act number 1-5 (default: 1) |
|
||||
| `--location-id` | Optional location identifier |
|
||||
| `--class-name` | Python class name (default: PascalCase of name) |
|
||||
| `--dry-run` | Show what would change without writing files |
|
||||
| `--undo NAME` | Reverse all mutations for a previously scaffolded route and remove the run file |
|
||||
|
||||
After scaffolding, edit the generated `src/run/<name>.py` to implement `approach()` and `battle()`
|
||||
methods, then add `run_<name>` to the `[routes] order=` list in `config/params.ini`.
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
# Botty Stealth - Deep Dive
|
||||
|
||||
## What Warden (Battle.net Anti-Cheat) Can Detect
|
||||
|
||||
Warden is a kernel-level anti-cheat. It can:
|
||||
- **Enumerate processes** - see `main.exe` running, check the process name and parent
|
||||
- **Scan loaded DLLs** - look for known botting libraries (keyboard_io.dll, mousetool.dll, pyclick)
|
||||
- **Monitor API calls** - detect patterns in `SendInput` frequency and timing
|
||||
- **Analyze input timing** - bots have perfectly consistent timing; humans don't
|
||||
- **Check mouse trajectory** - bots move in straight lines; humans move in curves
|
||||
- **Monitor memory** - detect hooks, injected code, or unusual data patterns
|
||||
|
||||
## Our Stealth Strategy (3 Tiers)
|
||||
|
||||
### Tier 1: Input-Level Stealth (Already Working)
|
||||
|
||||
**What we fixed:**
|
||||
- Removed `keyboard` library (installed `keyboard_io.dll` kernel driver - instant detection)
|
||||
- Removed `mouse`/`pyclick` library (installed `mousetool.dll` kernel driver - instant detection)
|
||||
- Replaced with `ctypes` + `user32.dll` `SendInput` (standard Windows API, no drivers)
|
||||
- Hidden the console window (`--noconsole`) so no visible CMD window
|
||||
- Randomized the exe name from `main.exe` to something like `whvitjz2.exe`
|
||||
|
||||
**Mouse movement stealth (in `input_layer/mouse_impl.py`):**
|
||||
- Every `mouse.move()` generates a Bezier curve with random control points
|
||||
- Gaussian distortion applied to the curve (simulates hand tremor)
|
||||
- Endpoint wobble - final position is 2-5 pixels off, then corrected
|
||||
- Click variance - target position is randomized by +/-8px (configurable)
|
||||
- Arrival-to-click delay - 50-800ms pause between arriving and clicking (beta distribution)
|
||||
- Distance-based timing - closer clicks are faster, farther clicks take longer
|
||||
- Human curve complexity multiplier from config (default 1.0)
|
||||
|
||||
**Keyboard stealth (in `input_layer/__init__.py`):**
|
||||
- Every `keyboard.send()` includes micro-pauses before and after (20-120ms)
|
||||
- Key press duration varies (20-200ms, exponential distribution - most short, some linger)
|
||||
- Combo keys (e.g. `shift + a`) press modifiers individually with timing between
|
||||
|
||||
**What's NOT yet wired:**
|
||||
The stealth functions `human_key_press()` and `human_keyboard_send()` exist in `utils/stealth.py` but are called via the input layer shim automatically. Every `keyboard.send()` now gets stealth timing.
|
||||
|
||||
### Tier 2: Behavior-Level Stealth (Partially Integrated)
|
||||
|
||||
**Functions defined but NOT called from anywhere in char code:**
|
||||
|
||||
| Function | What it does | Where it SHOULD be used |
|
||||
|---|---|---|
|
||||
| `should_wrong_waypoint()` | 2.5% chance of clicking wrong waypoint | `ui/waypoint.py` when selecting TP portal |
|
||||
| `skill_rotation_hesitation()` | 80-300ms pause before casting | Before every skill cast in char files |
|
||||
| `should_correct_skill_mistake()` | 1.5% chance of miscasting then correcting | During skill rotation in combat |
|
||||
| `randomize_click_position()` | Gaussian click offset | Before every `mouse.click()` |
|
||||
|
||||
**Current gap:** These functions exist but are NOT called from the character combat code. The char files call `keyboard.send("1")` directly without going through a stealth wrapper. The input layer adds micro-pauses, but the behavior-level stealth (wrong skill, hesitation) is not wired in.
|
||||
|
||||
**What IS wired:**
|
||||
- `mouse.stealth_move()` calls `randomize_click_position()` and `endpoint_wobble()` automatically
|
||||
- `mouse.click()` calls `apply_click_delay()` automatically
|
||||
- `keyboard.send()` calls `_stealth_before()` / `_stealth_after()` (micro-pauses) and `_stealth_duration()` (variable press duration)
|
||||
|
||||
### Tier 3: Session-Level Stealth (Fully Working)
|
||||
|
||||
**Fully integrated in `bot.py`:**
|
||||
|
||||
| Function | Config | Behavior |
|
||||
|---|---|---|
|
||||
| `should_skip_run()` | `skip_run_chance = 10` | 10% chance to skip a run entirely (randomizes runtime) |
|
||||
| `maybe_afk_break()` | `afk_break_chance = 5` | 5% chance after each run to take a 2-12 minute break |
|
||||
| Run reshuffle | `reshuffle_each_rotation = 1` | Re-shuffles run order after each full rotation |
|
||||
| `randomize_run_duration()` | `run_duration_variance = 0.15` | +/-15% variation on expected run time |
|
||||
| Wait jitter | `wait_jitter_min = 0.85` / `max = 1.20` | Every `wait()` call is multiplied by 0.85x-1.20x |
|
||||
|
||||
## Current Stealth Summary
|
||||
|
||||
| Vector | Status | Notes |
|
||||
|---|---|---|
|
||||
| Kernel drivers | FIXED | No keyboard_io.dll or mousetool.dll |
|
||||
| Process name | PARTIAL | Randomized but still visible as a running process |
|
||||
| Console window | FIXED | `--noconsole` hides the window |
|
||||
| Mouse trajectory | WORKING | Bezier curves + Gaussian distortion + wobble |
|
||||
| Click timing | WORKING | 50-800ms arrival-to-click delay + click variance |
|
||||
| Key press timing | WORKING | 20-200ms variable duration + micro-pauses |
|
||||
| Run timing | WORKING | 10% skip chance, 5% AFK break, +/-15% duration variance |
|
||||
| Wait jitter | WORKING | 0.85x-1.20x on all waits |
|
||||
| Wrong waypoint | WORKING (2.5%) | Wired in waypoint.py |
|
||||
| Skill mistakes | WORKING (1.5%) | Wired in input_layer/__init__.py |
|
||||
| Skill hesitation | WORKING (80-300ms) | Wired in input_layer/__init__.py |
|
||||
|
||||
## Remaining Risks
|
||||
|
||||
1. **Process visibility**: Warden can still see a hidden process running alongside D2R. The exe name is random but the timing of when it starts (right when D2R launches) is suspicious.
|
||||
|
||||
2. **Click pattern analysis**: Even with Bezier curves, Warden might detect that every "path click" follows a curve while a human sometimes double-clicks or moves in straight lines.
|
||||
|
||||
3. **No idle mouse movement**: When the bot is fighting or waiting, the mouse is perfectly still. Humans constantly fidget with the mouse.
|
||||
|
||||
4. **Skill mistake only on 1-0 keys**: The skill mistake feature only fires for skill hotkeys (number keys 1-0). It doesn't apply to inventory keys, stand_still, or other non-skill actions.
|
||||
|
||||
## What Would Improve Stealth Further
|
||||
|
||||
### High Impact (Warden likely checks these)
|
||||
1. **Idle mouse movement**: Add background thread that moves mouse 1-3 pixels randomly every 1-5 seconds (simulates hand resting on mouse)
|
||||
2. **Occasional straight-line clicks**: 10% of the time skip the Bezier curve and click directly (humans don't always move in curves)
|
||||
|
||||
### Medium Impact
|
||||
3. **Process parent spoofing**: Launch the bot from a different process (e.g. from Explorer instead of directly) to hide the relationship with D2R
|
||||
4. **Variable scroll speed**: Add variance to `mouse.wheel()` calls (inventory scrolling)
|
||||
5. **Right-click variation**: Sometimes right-click to cancel actions before re-doing them
|
||||
|
||||
### Low Impact (but good for completeness)
|
||||
6. **Randomize AFK break more**: Current range is 2-12 minutes, could be wider
|
||||
7. **Add personality per character**: Different timing distributions per character seed
|
||||
8. **Typing delays**: When using text chat (if implemented), add keystroke-by-keystroke delays
|
||||
@@ -0,0 +1,198 @@
|
||||
"""
|
||||
D2R Asset Extractor
|
||||
|
||||
Runs on your local Windows machine. Captures D2R, saves screenshot.
|
||||
You then send the screenshot to the AI agent for analysis.
|
||||
AI returns bounding boxes -> run crop.py to extract PNGs.
|
||||
|
||||
Usage:
|
||||
Run: python asset_extractor.py
|
||||
F1: Capture D2R screen -> screenshots/debug/latest.png
|
||||
F2: Crop entities from screenshots/debug/latest_annotations.json
|
||||
F3: List existing assets
|
||||
F12: Exit
|
||||
|
||||
Workflow:
|
||||
1. Run this script in the botty conda env
|
||||
2. F1 to capture
|
||||
3. Tell your AI agent to analyze screenshots/debug/latest.png
|
||||
4. AI writes screenshots/debug/latest_annotations.json with bounding boxes
|
||||
5. F2 to crop entities into assets/enemies/ or assets/npc/
|
||||
"""
|
||||
import os, sys, cv2, numpy as np, keyboard, json, ctypes, win32gui
|
||||
from datetime import datetime
|
||||
from mss import mss
|
||||
|
||||
# DPI awareness - must be first
|
||||
try:
|
||||
ctypes.windll.shcore.SetProcessDpiAwareness(2)
|
||||
except:
|
||||
try:
|
||||
ctypes.windll.shcore.SetProcessDpiAwareness(1)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Fix tesserocr DLLs
|
||||
if sys.platform == "win32":
|
||||
_dll = os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "Library", "bin")
|
||||
if os.path.isdir(_dll):
|
||||
os.add_dll_directory(_dll)
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "src"))
|
||||
|
||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
SAVE_DIR = os.path.join(BASE, "screenshots", "debug")
|
||||
ENEMIES_DIR = os.path.join(BASE, "assets", "enemies")
|
||||
NPC_DIR = os.path.join(BASE, "assets", "npc")
|
||||
|
||||
for d in [SAVE_DIR, ENEMIES_DIR, NPC_DIR]:
|
||||
os.makedirs(d, exist_ok=True)
|
||||
|
||||
LATEST_PATH = os.path.join(SAVE_DIR, "latest.png")
|
||||
ANNOTATIONS_PATH = os.path.join(SAVE_DIR, "latest_annotations.json")
|
||||
|
||||
# Known NPC names for routing
|
||||
NPC_NAMES = {
|
||||
'akara', 'charsi', 'kashya', 'cain', 'drognan', 'lysander',
|
||||
'fara', 'ormus', 'tyrael', 'jamella', 'halbu', 'qual_kehk',
|
||||
'qual-kehk', 'qualkehk', 'malah', 'larzuk', 'anya'
|
||||
}
|
||||
|
||||
|
||||
def find_d2r():
|
||||
hwnds = []
|
||||
def cb(h, r):
|
||||
title = win32gui.GetWindowText(h)
|
||||
if 'diablo' in title.lower() and win32gui.IsWindowVisible(h):
|
||||
r.append(h)
|
||||
win32gui.EnumWindows(cb, hwnds)
|
||||
return hwnds[0] if hwnds else None
|
||||
|
||||
|
||||
def grab():
|
||||
"""Grab D2R client area. Resizes to 1280x720 if needed."""
|
||||
hwnd = find_d2r()
|
||||
if not hwnd:
|
||||
print(" [ERROR] D2R not found. Is it running and visible?")
|
||||
return None
|
||||
|
||||
client = win32gui.GetClientRect(hwnd)
|
||||
w, h = client[2] - client[0], client[3] - client[1]
|
||||
screen_pos = win32gui.ClientToScreen(hwnd, (0, 0))
|
||||
|
||||
with mss() as sct:
|
||||
region = {
|
||||
'top': screen_pos[1],
|
||||
'left': screen_pos[0],
|
||||
'width': w,
|
||||
'height': h
|
||||
}
|
||||
sct_img = sct.grab(region)
|
||||
img = np.array(sct_img)[:, :, :3] # BGRA -> BGR
|
||||
|
||||
if w != 1280 or h != 720:
|
||||
img = cv2.resize(img, (1280, 720), interpolation=cv2.INTER_LINEAR)
|
||||
print(f" [RESIZED] {w}x{h} -> 1280x720")
|
||||
else:
|
||||
print(f" [CAPTURED] {w}x{h}")
|
||||
|
||||
return img
|
||||
|
||||
|
||||
def on_f1():
|
||||
"""Capture D2R and save."""
|
||||
print("\n[=== CAPTURING ===]")
|
||||
img = grab()
|
||||
if not img:
|
||||
return
|
||||
cv2.imwrite(LATEST_PATH, img)
|
||||
print(f" [SAVED] {LATEST_PATH}")
|
||||
print(f" Now ask your AI agent to analyze: {LATEST_PATH}")
|
||||
print(f" AI should write: {ANNOTATIONS_PATH}")
|
||||
print(' Format: [{"name":"skeleton","x":100,"y":200,"w":60,"h":80}, ...]')
|
||||
|
||||
|
||||
def on_f2():
|
||||
"""Crop entities from latest capture using annotations JSON."""
|
||||
print("\n[=== CROPPING ENTITIES ===]")
|
||||
if not os.path.exists(LATEST_PATH):
|
||||
print(" [ERROR] No capture found. Press F1 first.")
|
||||
return
|
||||
if not os.path.exists(ANNOTATIONS_PATH):
|
||||
print(" [ERROR] No annotations found.")
|
||||
print(f" Create: {ANNOTATIONS_PATH}")
|
||||
print(' [{"name":"skeleton","x":100,"y":200,"w":60,"h":80}, ...]')
|
||||
return
|
||||
|
||||
img = cv2.imread(LATEST_PATH)
|
||||
with open(ANNOTATIONS_PATH) as f:
|
||||
entities = json.load(f)
|
||||
|
||||
print(f" Image: {img.shape[1]}x{img.shape[0]}, Entities: {len(entities)}")
|
||||
|
||||
saved = 0
|
||||
for ent in entities:
|
||||
name = ent['name'].lower().replace(' ', '_')
|
||||
x, y = int(ent['x']), int(ent['y'])
|
||||
w, h = int(ent['w']), int(ent['h'])
|
||||
i_w, i_h = img.shape[1], img.shape[0]
|
||||
|
||||
# Crop with 5px padding
|
||||
pad = 5
|
||||
x1, y1 = max(0, x - pad), max(0, y - pad)
|
||||
x2, y2 = min(i_w, x + w + pad), min(i_h, y + h + pad)
|
||||
crop = img[y1:y2, x1:x2]
|
||||
|
||||
# Route to npc or enemies folder
|
||||
if name in NPC_NAMES:
|
||||
save_dir = NPC_DIR
|
||||
else:
|
||||
save_dir = ENEMIES_DIR
|
||||
|
||||
# Auto-number duplicates
|
||||
fname = f"{name}.png"
|
||||
save_path = os.path.join(save_dir, fname)
|
||||
variant = 1
|
||||
while os.path.exists(save_path):
|
||||
variant += 1
|
||||
fname = f"{name}_{variant}.png"
|
||||
save_path = os.path.join(save_dir, fname)
|
||||
|
||||
cv2.imwrite(save_path, crop)
|
||||
print(f" [SAVED] {save_path} ({crop.shape[1]}x{crop.shape[0]})")
|
||||
saved += 1
|
||||
|
||||
print(f"\n Total: {saved} assets cropped.")
|
||||
|
||||
|
||||
def on_f3():
|
||||
"""List existing assets."""
|
||||
print("\n[=== ASSETS INVENTORY ===]")
|
||||
for label, d in [("enemies", ENEMIES_DIR), ("npc", NPC_DIR)]:
|
||||
if os.path.isdir(d):
|
||||
files = sorted(os.listdir(d))
|
||||
print(f"\n assets/{label}/ ({len(files)} files):")
|
||||
for f in files:
|
||||
sz = os.path.getsize(os.path.join(d, f))
|
||||
print(f" {f} ({sz}b)")
|
||||
else:
|
||||
print(f"\n assets/{label}/ - EMPTY")
|
||||
|
||||
|
||||
def run():
|
||||
print("=== D2R Asset Extractor ===")
|
||||
print(" F1 - Capture D2R screen")
|
||||
print(" F2 - Crop entities from annotations")
|
||||
print(" F3 - List assets")
|
||||
print(" F12 - Exit")
|
||||
print("Ready.")
|
||||
|
||||
keyboard.add_hotkey('f1', on_f1)
|
||||
keyboard.add_hotkey('f2', on_f2)
|
||||
keyboard.add_hotkey('f3', on_f3)
|
||||
keyboard.add_hotkey('f12', lambda: (print("\nBye."), sys.exit(0)))
|
||||
keyboard.wait()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -100,6 +100,9 @@ NTIPAliasType["ruby"] = 99;
|
||||
NTIPAliasType["sapphire"] = 100;
|
||||
NTIPAliasType["topaz"] = 101;
|
||||
NTIPAliasType["skull"] = 102;
|
||||
NTIPAliasType["swordsandknives"] = 103;
|
||||
NTIPAliasType["spearsandpolearms"] = 104;
|
||||
NTIPAliasType["grimoires"] = 105;
|
||||
|
||||
var NTIPAliasClassID = {};
|
||||
NTIPAliasClassID["hax"] = 0; NTIPAliasClassID["handaxe"] = 0;
|
||||
@@ -761,6 +764,26 @@ NTIPAliasClassID["ceh"] = 655; NTIPAliasClassID["chargedessenceofhatred"] = 655
|
||||
NTIPAliasClassID["bet"] = 656; NTIPAliasClassID["burningessenceofterror"] = 656;
|
||||
NTIPAliasClassID["fed"] = 657; NTIPAliasClassID["festeringessenceofdestruction"] = 657;
|
||||
NTIPAliasClassID["std"] = 658; NTIPAliasClassID["standardofheroes"] = 658;
|
||||
NTIPAliasClassID["wa1"] = 659; NTIPAliasClassID["oldbook"] = 659;
|
||||
NTIPAliasClassID["wa2"] = 660; NTIPAliasClassID["tome"] = 660;
|
||||
NTIPAliasClassID["wa3"] = 661; NTIPAliasClassID["codex"] = 661;
|
||||
NTIPAliasClassID["wa4"] = 662; NTIPAliasClassID["compendium"] = 662;
|
||||
NTIPAliasClassID["wa5"] = 663; NTIPAliasClassID["grimoire"] = 663;
|
||||
NTIPAliasClassID["wa6"] = 664; NTIPAliasClassID["burnttext"] = 664;
|
||||
NTIPAliasClassID["wa7"] = 665; NTIPAliasClassID["darktome"] = 665;
|
||||
NTIPAliasClassID["wa8"] = 666; NTIPAliasClassID["darkcodex"] = 666;
|
||||
NTIPAliasClassID["wa9"] = 667; NTIPAliasClassID["possessedcompendium"] = 667;
|
||||
NTIPAliasClassID["waa"] = 668; NTIPAliasClassID["possessedgrimoire"] = 668;
|
||||
NTIPAliasClassID["wab"] = 669; NTIPAliasClassID["forgottenvolume"] = 669;
|
||||
NTIPAliasClassID["wac"] = 670; NTIPAliasClassID["occulttome"] = 670;
|
||||
NTIPAliasClassID["wad"] = 671; NTIPAliasClassID["occultcodex"] = 671;
|
||||
NTIPAliasClassID["wae"] = 672; NTIPAliasClassID["blasphemouscompendium"] = 672;
|
||||
NTIPAliasClassID["waf"] = 673; NTIPAliasClassID["blasphemousgrimoire"] = 673;
|
||||
NTIPAliasClassID["northernworldstoneshard"] = 674;
|
||||
NTIPAliasClassID["easternworldstoneshard"] = 675;
|
||||
NTIPAliasClassID["southernworldstoneshard"] = 676;
|
||||
NTIPAliasClassID["westernworldstoneshard"] = 677;
|
||||
NTIPAliasClassID["deepworldstoneshard"] = 678;
|
||||
|
||||
var NTIPAliasClass = {};
|
||||
NTIPAliasClass["normal"] = 0;
|
||||
@@ -900,6 +923,7 @@ NTIPAliasStat["itemaddpaladinskills"] = [83,3]; NTIPAliasStat["paladinskills"] =
|
||||
NTIPAliasStat["itemaddbarbarianskills"] = [83,4]; NTIPAliasStat["barbarianskills"] = [83,4];
|
||||
NTIPAliasStat["itemadddruidskills"] = [83,5]; NTIPAliasStat["druidskills"] = [83,5];
|
||||
NTIPAliasStat["itemaddassassinskills"] = [83,6]; NTIPAliasStat["assassinskills"] = [83,6];
|
||||
NTIPAliasStat["itemaddwarlockskills"] = [83,7]; NTIPAliasStat["warlockskills"] = [83,7];
|
||||
|
||||
NTIPAliasStat["unsentparam1"] = 84;
|
||||
NTIPAliasStat["itemaddexperience"] = 85;
|
||||
@@ -931,6 +955,9 @@ NTIPAliasStat["plusskillwerewolf"] = [97,223];
|
||||
NTIPAliasStat["plusskillshapeshifting"] = [97,224]; NTIPAliasStat["plusskilllycanthropy"] = [97,224];
|
||||
NTIPAliasStat["plusskillsummonspiritwolf"] = [97,227];
|
||||
NTIPAliasStat["plusskillferalrage"] = [97,232];
|
||||
// Warlock
|
||||
NTIPAliasStat["plusskillabyss"] = [97,281]
|
||||
NTIPAliasStat["plusskillenhancedentropy"] = [97,282]
|
||||
|
||||
NTIPAliasStat["state"] = 98;
|
||||
NTIPAliasStat["itemfastergethitrate"] = 99; NTIPAliasStat["fhr"] = 99;
|
||||
@@ -1160,6 +1187,9 @@ NTIPAliasStat["skillbladeshield"] = [107,277];
|
||||
NTIPAliasStat["skillvenom"] = [107,278];
|
||||
NTIPAliasStat["skillshadowmaster"] = [107,279];
|
||||
NTIPAliasStat["skillphoenixstrike"] = [107,280];
|
||||
// Warlock skills
|
||||
NTIPAliasStat["skillabyss"] = [107,281];
|
||||
NTIPAliasStat["skillenhancedentropy"] = [107,282];
|
||||
|
||||
NTIPAliasStat["itemrestinpeace"] = 108;
|
||||
NTIPAliasStat["curseresistance"] = 109;
|
||||
@@ -1279,6 +1309,9 @@ NTIPAliasStat["itemaddelementalskilltab"] = [188,42]; NTIPAliasStat["elementals
|
||||
NTIPAliasStat["itemaddtrapsskilltab"] = [188,48]; NTIPAliasStat["trapsskilltab"] = [188,48];
|
||||
NTIPAliasStat["itemaddshadowdisciplinesskilltab"] = [188,49]; NTIPAliasStat["shadowdisciplinesskilltab"] = [188,49];
|
||||
NTIPAliasStat["itemaddmartialartsskilltab"] = [188,50]; NTIPAliasStat["martialartsskilltab"] = [188,50];
|
||||
NTIPAliasStat["itemaddchaosskilltab"] = [188,51]; NTIPAliasStat["chaosskilltab"] = [188,51];
|
||||
NTIPAliasStat["itemaddeldritchskilltab"] = [188,52]; NTIPAliasStat["eldritchskilltab"] = [188,52];
|
||||
NTIPAliasStat["itemadddemonskilltab"] = [188,53]; NTIPAliasStat["demonskilltab"] = [188,53];
|
||||
|
||||
NTIPAliasStat["unused189"] = 189;
|
||||
NTIPAliasStat["unused190"] = 190;
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
"Screen Resolution (Windowed)": "1280x720",
|
||||
"Resolution Scale": 100,
|
||||
"Sharpening": 6,
|
||||
"Game Resolution": 1,
|
||||
"Game Resolution": 2,
|
||||
"Light Quality": 2,
|
||||
"Blended Shadows": 0,
|
||||
"Perspective": 0,
|
||||
"VSync": 1,
|
||||
"Framerate Cap": 60,
|
||||
"Framerate Target": 0,
|
||||
"Window Mode": 0,
|
||||
"Window Mode": 1,
|
||||
"Graphic Presets": 4,
|
||||
"Texture Quality": 4,
|
||||
"Texture Anisotropy": 0,
|
||||
|
||||
@@ -17066,5 +17066,464 @@
|
||||
"ptBR": "Berserker",
|
||||
"ruRU": "[ms]лютый[fs]лютая[ns]лютое[pl]лютые",
|
||||
"zhCN": "狂战士"
|
||||
},
|
||||
{
|
||||
"id": 27875,
|
||||
"Key": "Virulent",
|
||||
"enUS": "Virulent",
|
||||
"zhTW": "怨毒",
|
||||
"deDE": "[ms]Virulenter[fs]Virulente[ns]Virulentes[pl]Virulente",
|
||||
"esES": "[fs]virulenta[ms]virulento[fp]virulentas[mp]virulentos",
|
||||
"frFR": "[ms]virulent[fs]virulente[mp]virulents[fp]virulentes",
|
||||
"itIT": "[ms]Virulento[fs]Virulenta[mp]Virulenti[fp]Virulente",
|
||||
"koKR": "악성",
|
||||
"plPL": "[ms]Zakażający[fs]Zakażająca[ns]Zakażające[p]Zakażające",
|
||||
"esMX": "[ms]virulento[fs]virulenta[mp]virulentos[fp]virulentas",
|
||||
"jaJP": "猛毒に満ちた",
|
||||
"ptBR": "[fs]Virulenta[fp]Virulentas[ms]Virulento[mp]Virulentos",
|
||||
"ruRU": "[ms]едкий[fs]едкая[ns]едкое[pl]едкие",
|
||||
"zhCN": "剧毒"
|
||||
},
|
||||
{
|
||||
"id": 27876,
|
||||
"Key": "of Acidity",
|
||||
"enUS": "of Acidity",
|
||||
"zhTW": "酸蝕之",
|
||||
"deDE": "der Säure",
|
||||
"esES": "de acidez",
|
||||
"frFR": "d’acidité",
|
||||
"itIT": "dell'Acidità",
|
||||
"koKR": "산성의",
|
||||
"plPL": "Kwasowości",
|
||||
"esMX": "de acidez",
|
||||
"jaJP": "酸性の",
|
||||
"ptBR": "da Acidez",
|
||||
"ruRU": "едкости",
|
||||
"zhCN": "酸蚀之"
|
||||
},
|
||||
{
|
||||
"id": 27877,
|
||||
"Key": "Incendiary",
|
||||
"enUS": "Incendiary",
|
||||
"zhTW": "煽火",
|
||||
"deDE": "[ms]Zündelnder[fs]Zündelnde[ns]Zündelndes[pl]Zündelnde",
|
||||
"esES": "[fs]incendiaria[ms]incendiario[fp]incendiarias[mp]incendiarios",
|
||||
"frFR": "[ms]incendiaire[fs]incendiaire[mp]incendiaires[fp]incendiaires",
|
||||
"itIT": "[ms]Incendiario[fs]Incendiaria[mp]Incendiari[fp]Incendiarie",
|
||||
"koKR": "방화",
|
||||
"plPL": "[ms]Zapalający[fs]Zapalająca[ns]Zapalające[p]Zapalające",
|
||||
"esMX": "[ms]incendiario[fs]incendiaria[mp]incendiarios[fp]incendiarias",
|
||||
"jaJP": "焼夷の",
|
||||
"ptBR": "[fs]Incendiária[fp]Incendiárias[ms]Incendiário[mp]Incendiários",
|
||||
"ruRU": "[ms]воспламеняющий[fs]воспламеняющая[ns]воспламеняющее[pl]воспламеняющие",
|
||||
"zhCN": "焚烧"
|
||||
},
|
||||
{
|
||||
"id": 27878,
|
||||
"Key": "of Kindling",
|
||||
"enUS": "of Kindling",
|
||||
"zhTW": "引火之",
|
||||
"deDE": "des Entfachens",
|
||||
"esES": "de fajina",
|
||||
"frFR": "de petit bois",
|
||||
"itIT": "dell'Accensione",
|
||||
"koKR": "불쏘시개의",
|
||||
"plPL": "Rozpalania",
|
||||
"esMX": "de incendaja",
|
||||
"jaJP": "種火の",
|
||||
"ptBR": "da Combustão",
|
||||
"ruRU": "разжигания",
|
||||
"zhCN": "点燃之"
|
||||
},
|
||||
{
|
||||
"id": 27879,
|
||||
"Key": "Gelid",
|
||||
"enUS": "Gelid",
|
||||
"zhTW": "嚴寒",
|
||||
"deDE": "[ms]Eiskalter[fs]Eiskalte[ns]Eiskaltes[pl]Eiskalte",
|
||||
"esES": "[fs]gélida[ms]gélido[fp]gélidas[mp]gélidos",
|
||||
"frFR": "[ms]transi[fs]transie[mp]transis[fp]transies",
|
||||
"itIT": "[ms]Gelido[fs]Gelida[mp]Gelidi[fp]Gelide",
|
||||
"koKR": "혹한",
|
||||
"plPL": "[ms]Zlodowaciały[fs]Zlodowaciała[ns]Zlodowaciałe[p]Zlodowaciałe",
|
||||
"esMX": "[ms]gélido[fs]gélida[mp]gélidos[fp]gélidas",
|
||||
"jaJP": "氷結",
|
||||
"ptBR": "[fs]Gélida[fp]Gélidas[ms]Gélido[mp]Gélidos",
|
||||
"ruRU": "[ms]застывший[fs]застывшая[ns]застывшее[pl]застывшие",
|
||||
"zhCN": "极寒"
|
||||
},
|
||||
{
|
||||
"id": 27880,
|
||||
"Key": "of Numbing",
|
||||
"enUS": "of Numbing",
|
||||
"zhTW": "麻木之",
|
||||
"deDE": "des Erstarrens",
|
||||
"esES": "de entumecimiento",
|
||||
"frFR": "d’engourdissement",
|
||||
"itIT": "dell'Intorpidimento",
|
||||
"koKR": "마비의",
|
||||
"plPL": "Odrętwienia",
|
||||
"esMX": "de entumecimiento",
|
||||
"jaJP": "麻痺の",
|
||||
"ptBR": "do Entorpecimento",
|
||||
"ruRU": "обморожения",
|
||||
"zhCN": "麻痹之"
|
||||
},
|
||||
{
|
||||
"id": 27881,
|
||||
"Key": "Magnetic",
|
||||
"enUS": "Magnetic",
|
||||
"zhTW": "磁性",
|
||||
"deDE": "[ms]Magnetischer[fs]Magnetische[ns]Magnetisches[pl]Magnetische",
|
||||
"esES": "[fs]magnética[ms]magnético[fp]magnéticas[mp]magnéticos",
|
||||
"frFR": "[ms]magnétique[fs]magnétique[mp]magnétiques[fp]magnétiques",
|
||||
"itIT": "[ms]Magnetico[fs]Magnetica[mp]Magnetici[fp]Magnetiche",
|
||||
"koKR": "자성",
|
||||
"plPL": "[ms]Magnetyczny[fs]Magnetyczna[ns]Magnetyczne[p]Magnetyczne",
|
||||
"esMX": "[ms]magnético[fs]magnética[mp]magnéticos[fp]magnéticas",
|
||||
"jaJP": "超電磁",
|
||||
"ptBR": "[fs]Magnética[fp]Magnéticas[ms]Magnético[mp]Magnéticos",
|
||||
"ruRU": "[ms]магнитный[fs]магнитная[ns]магнитное[pl]магнитные",
|
||||
"zhCN": "磁性"
|
||||
},
|
||||
{
|
||||
"id": 27882,
|
||||
"Key": "of Conductivity",
|
||||
"enUS": "of Conductivity",
|
||||
"zhTW": "導電之",
|
||||
"deDE": "der Leitfähigkeit",
|
||||
"esES": "de conductividad",
|
||||
"frFR": "de conductivité",
|
||||
"itIT": "della Conduttività",
|
||||
"koKR": "전도성의",
|
||||
"plPL": "Przewodzenia",
|
||||
"esMX": "de conductividad",
|
||||
"jaJP": "通電の",
|
||||
"ptBR": "da Condutividade",
|
||||
"ruRU": "проводимости",
|
||||
"zhCN": "传导之"
|
||||
},
|
||||
{
|
||||
"id": 27883,
|
||||
"Key": "Mystical",
|
||||
"enUS": "Mystical",
|
||||
"zhTW": "神秘",
|
||||
"deDE": "[ms]Mystischer[fs]Mystische[ns]Mystisches[pl]Mystische",
|
||||
"esES": "[fs]mística[ms]místico[fp]místicas[mp]místicos",
|
||||
"frFR": "[ms]mystique[fs]mystique[mp]mystiques[fp]mystiques",
|
||||
"itIT": "[ms]Mistico[fs]Mistica[mp]Mistici[fp]Mistiche",
|
||||
"koKR": "신비한",
|
||||
"plPL": "[ms]Mistyczny[fs]Mistyczna[ns]Mistyczne[p]Mistyczne",
|
||||
"esMX": "[ms]místico[fs]mística[mp]místicos[fp]místicas",
|
||||
"jaJP": "神秘を司る",
|
||||
"ptBR": "[fs]Mística[fp]Místicas[ms]Místico[mp]Místicos",
|
||||
"ruRU": "[ms]мистический[fs]мистическая[ns]мистическое[pl]мистические",
|
||||
"zhCN": "神秘"
|
||||
},
|
||||
{
|
||||
"id": 27884,
|
||||
"Key": "of Thaumaturgy",
|
||||
"enUS": "of Thaumaturgy",
|
||||
"zhTW": "奇術之",
|
||||
"deDE": "der Thaumaturgie",
|
||||
"esES": "de taumaturgia",
|
||||
"frFR": "de thaumaturgie",
|
||||
"itIT": "della Taumaturgia",
|
||||
"koKR": "마도의",
|
||||
"plPL": "Taumaturgii",
|
||||
"esMX": "de taumaturgia",
|
||||
"jaJP": "魔術の",
|
||||
"ptBR": "da Taumaturgia",
|
||||
"ruRU": "чудотворства",
|
||||
"zhCN": "奇术之"
|
||||
},
|
||||
{
|
||||
"id": 27885,
|
||||
"Key": "Breaching",
|
||||
"enUS": "Breaching",
|
||||
"zhTW": "突破",
|
||||
"deDE": "[ms]Brechender[fs]Brechende[ns]Brechendes[pl]Brechende",
|
||||
"esES": "[fs]quebrantadora[ms]quebrantador[fp]quebrantadoras[mp]quebrantadores",
|
||||
"frFR": "[ms]offensif[fs]offensive[mp]offensifs[fp]offensives",
|
||||
"itIT": "[ms]Penetrante[fs]Penetrante[mp]Penetranti[fp]Penetranti",
|
||||
"koKR": "침범하는",
|
||||
"plPL": "[ms]Przełamujący[fs]Przełamująca[ns]Przełamujące[p]Przełamujące",
|
||||
"esMX": "[ms]vulnerante[fs]vulnerante[mp]vulnerantes[fp]vulnerantes",
|
||||
"jaJP": "強行突破",
|
||||
"ptBR": "[fs]Abrangente[fp]Abrangentes[ms]Abrangente[mp]Abrangentes",
|
||||
"ruRU": "[ms]ломающий[fs]ломающая[ns]ломающее[pl]ломающие",
|
||||
"zhCN": "破甲"
|
||||
},
|
||||
{
|
||||
"id": 27886,
|
||||
"Key": "of Force",
|
||||
"enUS": "of Force",
|
||||
"zhTW": "力迫之",
|
||||
"deDE": "der Macht",
|
||||
"esES": "de pujanza",
|
||||
"frFR": "de force",
|
||||
"itIT": "della Potenza",
|
||||
"koKR": "위력의",
|
||||
"plPL": "Mocy",
|
||||
"esMX": "de potencia",
|
||||
"jaJP": "力の",
|
||||
"ptBR": "de Força",
|
||||
"ruRU": "принуждения",
|
||||
"zhCN": "力量之"
|
||||
},
|
||||
{
|
||||
"id": 27893,
|
||||
"Key": "Chaotic",
|
||||
"enUS": "Chaotic",
|
||||
"zhTW": "混沌",
|
||||
"deDE": "[ms]Chaotischer[fs]Chaotische[ns]Chaotisches[pl]Chaotische",
|
||||
"esES": "[fs]caótica[ms]caótico[fp]caóticas[mp]caóticos",
|
||||
"frFR": "[ms]chaotique[fs]chaotique[mp]chaotiques[fp]chaotiques",
|
||||
"itIT": "[ms]Caotico[fs]Caotica[mp]Caotici[fp]Caotiche",
|
||||
"koKR": "혼돈",
|
||||
"plPL": "[ms]Chaotyczny[fs]Chaotyczna[ns]Chaotyczne[p]Chaotyczne",
|
||||
"esMX": "[ms]caótico[fs]caótica[mp]caóticos[fp]caóticas",
|
||||
"jaJP": "混乱せし",
|
||||
"ptBR": "[fs]Conturbada[fp]Conturbadas[ms]Conturbado[mp]Conturbados",
|
||||
"ruRU": "[ms]хаотический[fs]хаотическая[ns]хаотическое[pl]хаотические",
|
||||
"zhCN": "混乱"
|
||||
},
|
||||
{
|
||||
"id": 27894,
|
||||
"Key": "Sullied",
|
||||
"enUS": "Sullied",
|
||||
"zhTW": "玷汙",
|
||||
"deDE": "[ms]Beschmutzter[fs]Beschmutzte[ns]Beschmutztes[pl]Beschmutzte",
|
||||
"esES": "[fs]maculada[ms]maculado[fp]maculadas[mp]maculados",
|
||||
"frFR": "[ms]profané[fs]profanée[mp]profanés[fp]profanées",
|
||||
"itIT": "[ms]Infangato[fs]Infangata[mp]Infangati[fp]Infangate",
|
||||
"koKR": "더럽혀진",
|
||||
"plPL": "[ms]Splugawiony[fs]Splugawiona[ns]Splugawione[p]Splugawione",
|
||||
"esMX": "[ms]mancillado[fs]mancillada[mp]mancillados[fp]mancilladas",
|
||||
"jaJP": "汚物の",
|
||||
"ptBR": "[fs]Manchada[fp]Manchadas[ms]Manchado[mp]Manchados",
|
||||
"ruRU": "[ms]пятнающий[fs]пятнающая[ns]пятнающее[pl]пятнающие",
|
||||
"zhCN": "污染"
|
||||
},
|
||||
{
|
||||
"id": 27895,
|
||||
"Key": "Fiendish",
|
||||
"enUS": "Fiendish",
|
||||
"zhTW": "妖魔",
|
||||
"deDE": "[ms]Teuflischer[fs]Teuflische[ns]Teuflisches[pl]Teuflische",
|
||||
"esES": "[fs]maligna[ms]maligno[fp]malignas[mp]malignos",
|
||||
"frFR": "[ms]diabolique[fs]diabolique[mp]diaboliques[fp]diaboliques",
|
||||
"itIT": "[ms]Mostruoso[fs]Mostruosa[mp]Mostruosi[fp]Mostruose",
|
||||
"koKR": "극악한",
|
||||
"plPL": "[ms]Czarci[fs]Czarcia[ns]Czarcie[p]Czarcie",
|
||||
"esMX": "[ms]diablesco[fs]diablesca[mp]diablescos[fp]diablescas",
|
||||
"jaJP": "悪魔のような",
|
||||
"ptBR": "[fs]Demoníaca[fp]Demoníacas[ms]Demoníaco[mp]Demoníacos",
|
||||
"ruRU": "[ms]бесовский[fs]бесовская[ns]бесовское[pl]бесовские",
|
||||
"zhCN": "邪鬼"
|
||||
},
|
||||
{
|
||||
"id": 27896,
|
||||
"Key": "Erratic",
|
||||
"enUS": "Erratic",
|
||||
"zhTW": "失序",
|
||||
"deDE": "[ms]Unberechenbarer[fs]Unberechenbare[ns]Unberechenbares[pl]Unberechenbare",
|
||||
"esES": "[fs]errática[ms]errático[fp]erráticas[mp]erráticos",
|
||||
"frFR": "[ms]erratique[fs]erratique[mp]erratiques[fp]erratiques",
|
||||
"itIT": "[ms]Imprevedibile[fs]Imprevedibile[mp]Imprevedibili[fp]Imprevedibili",
|
||||
"koKR": "변덕스러운",
|
||||
"plPL": "[ms]Rozpaczliwy[fs]Rozpaczliwa[ns]Rozpaczliwe[p]Rozpaczliwe",
|
||||
"esMX": "[ms]errático[fs]errática[mp]erráticos[fp]erráticas",
|
||||
"jaJP": "奇矯な",
|
||||
"ptBR": "[fs]Errática[fp]Erráticas[ms]Errático[mp]Erráticos",
|
||||
"ruRU": "[ms]беспорядочный[fs]беспорядочная[ns]беспорядочное[pl]беспорядочные",
|
||||
"zhCN": "善变"
|
||||
},
|
||||
{
|
||||
"id": 27897,
|
||||
"Key": "Torrid",
|
||||
"enUS": "Torrid",
|
||||
"zhTW": "熾熱",
|
||||
"deDE": "[ms]Sengender[fs]Sengende[ns]Sengendes[pl]Sengende",
|
||||
"esES": "[fs]tórrida[ms]tórrido[fp]tórridas[mp]tórridos",
|
||||
"frFR": "[ms]torride[fs]torride[mp]torrides[fp]torrides",
|
||||
"itIT": "[ms]Torrido[fs]Torrida[mp]Torridi[fp]Torride",
|
||||
"koKR": "격한",
|
||||
"plPL": "[ms]Palący[fs]Paląca[ns]Palące[p]Palące",
|
||||
"esMX": "[ms]tórrido[fs]tórrida[mp]tórridos[fp]tórridas",
|
||||
"jaJP": "熱烈な",
|
||||
"ptBR": "[fs]Tórrida[fp]Tórridas[ms]Tórrido[mp]Tórridos",
|
||||
"ruRU": "[ms]жгучий[fs]жгучая[ns]жгучее[pl]жгучие",
|
||||
"zhCN": "灼热"
|
||||
},
|
||||
{
|
||||
"id": 27898,
|
||||
"Key": "TaintedAffix",
|
||||
"enUS": "Tainted",
|
||||
"zhTW": "魔汙",
|
||||
"deDE": "[ms]Besudelter[fs]Besudelte[ns]Besudeltes[pl]Besudelte",
|
||||
"esES": "[fs]mancillada[ms]mancillado[fp]mancilladas[mp]mancillados",
|
||||
"frFR": "[ms]corrompu[fs]corrompue[mp]corrompus[fp]corrompues",
|
||||
"itIT": "[ms]Contaminato[fs]Contaminata[mp]Contaminati[fp]Contaminate",
|
||||
"koKR": "오염된",
|
||||
"plPL": "[ms]Splamiony[fs]Splamiona[ns]Splamione[p]Splamione",
|
||||
"esMX": "[ms]contaminado[fs]contaminada[mp]contaminados[fp]contaminadas",
|
||||
"jaJP": "穢された",
|
||||
"ptBR": "[fs]Maculada[fp]Maculadas[ms]Maculado[mp]Maculados",
|
||||
"ruRU": "[ms]помутненный[fs]помутненная[ns]помутненное[pl]помутненные",
|
||||
"zhCN": "污染"
|
||||
},
|
||||
{
|
||||
"id": 27899,
|
||||
"Key": "Forbidden",
|
||||
"enUS": "Forbidden",
|
||||
"zhTW": "封禁",
|
||||
"deDE": "[ms]Verbotener[fs]Verbotene[ns]Verbotenes[pl]Verbotene",
|
||||
"esES": "[fs]prohibida[ms]prohibido[fp]prohibidas[mp]prohibidos",
|
||||
"frFR": "[ms]interdit[fs]interdite[mp]interdits[fp]interdites",
|
||||
"itIT": "[ms]Proibito[fs]Proibita[mp]Proibiti[fp]Proibite",
|
||||
"koKR": "금지된",
|
||||
"plPL": "[ms]Zakazany[fs]Zakazana[ns]Zakazane[p]Zakazane",
|
||||
"esMX": "[ms]prohibido[fs]prohibida[mp]prohibidos[fp]prohibidas",
|
||||
"jaJP": "禁断なる",
|
||||
"ptBR": "[fs]Proibida[fp]Proibidas[ms]Proibido[mp]Proibidos",
|
||||
"ruRU": "[ms]запретный[fs]запретная[ns]запретное[pl]запретные",
|
||||
"zhCN": "禁忌"
|
||||
},
|
||||
{
|
||||
"id": 27900,
|
||||
"Key": "Dreadful",
|
||||
"enUS": "Dreadful",
|
||||
"zhTW": "怖懼",
|
||||
"deDE": "[ms]Furchterregender[fs]Furchterregende[ns]Furchterregendes[pl]Furchterregende",
|
||||
"esES": "[fs]pavorosa[ms]pavoroso[fp]pavorosas[mp]pavorosos",
|
||||
"frFR": "[ms]effroyable[fs]effroyable[mp]effroyables[fp]effroyables",
|
||||
"itIT": "[ms]Terrificante[fs]Terrificante[mp]Terrificanti[fp]Terrificanti",
|
||||
"koKR": "무시무시한",
|
||||
"plPL": "[ms]Przerażający[fs]Przerażająca[ns]Przerażające[p]Przerażające",
|
||||
"esMX": "[ms]espantoso[fs]espantosa[mp]espantosos[fp]espantosas",
|
||||
"jaJP": "恐ろしき",
|
||||
"ptBR": "[fs]Pavorosa[fp]Pavorosas[ms]Pavoroso[mp]Pavorosos",
|
||||
"ruRU": "[ms]жуткий[fs]жуткая[ns]жуткое[pl]жуткие",
|
||||
"zhCN": "可怖"
|
||||
},
|
||||
{
|
||||
"id": 27901,
|
||||
"Key": "Malevolent",
|
||||
"enUS": "Malevolent",
|
||||
"zhTW": "惡毒",
|
||||
"deDE": "[ms]Arglistiger[fs]Arglistige[ns]Arglistiges[pl]Arglistige",
|
||||
"esES": "[fs]malévola[ms]malévolo[fp]malévolas[mp]malévolos",
|
||||
"frFR": "[ms]malfaisant[fs]malfaisante[mp]malfaisants[fp]malfaisantes",
|
||||
"itIT": "[ms]Malevolo[fs]Malevola[mp]Malevoli[fp]Malevole",
|
||||
"koKR": "고약한",
|
||||
"plPL": "[ms]Złowrogi[fs]Złowroga[ns]Złowrogie[p]Złowrogie",
|
||||
"esMX": "[fs]malevolente[ms]malevolente[fp]malevolentes[mp]malevolentes",
|
||||
"jaJP": "悪しき",
|
||||
"ptBR": "[fs]Malévola[fp]Malévolas[ms]Malévolo[mp]Malévolos",
|
||||
"ruRU": "[ms]злонравный[fs]злонравная[ns]злонравное[pl]злонравные",
|
||||
"zhCN": "恶意"
|
||||
},
|
||||
{
|
||||
"id": 27902,
|
||||
"Key": "of Miasma Bolt",
|
||||
"enUS": "of Miasma Bolt",
|
||||
"zhTW": "瘴氣彈之",
|
||||
"deDE": "des Miasmablitzes",
|
||||
"esES": "de descarga de miasma",
|
||||
"frFR": "de traits de miasmes",
|
||||
"itIT": "del Dardo Miasmatico",
|
||||
"koKR": "독기 볼트의",
|
||||
"plPL": "Miazmatycznego Pocisku",
|
||||
"esMX": "de saeta de miasma",
|
||||
"jaJP": "ミアズマボルトの",
|
||||
"ptBR": "da Seta de Miasma",
|
||||
"ruRU": "стрелы миазм",
|
||||
"zhCN": "瘴气弹之"
|
||||
},
|
||||
{
|
||||
"id": 27903,
|
||||
"Key": "of Lethargy",
|
||||
"enUS": "of Lethargy",
|
||||
"zhTW": "昏沉之",
|
||||
"deDE": "der Lethargie",
|
||||
"esES": "de letargo",
|
||||
"frFR": "de léthargie",
|
||||
"itIT": "della Letargia",
|
||||
"koKR": "무기력의",
|
||||
"plPL": "Otępienia",
|
||||
"esMX": "de letargo",
|
||||
"jaJP": "沈滞の",
|
||||
"ptBR": "da Letargia",
|
||||
"ruRU": "летаргии",
|
||||
"zhCN": "迟缓之"
|
||||
},
|
||||
{
|
||||
"id": 27904,
|
||||
"Key": "of Rancor",
|
||||
"enUS": "of Rancor",
|
||||
"zhTW": "冤仇之",
|
||||
"deDE": "des Grolls",
|
||||
"esES": "de rencor",
|
||||
"frFR": "de rancœur",
|
||||
"itIT": "del Rancore",
|
||||
"koKR": "울분의",
|
||||
"plPL": "Animozji",
|
||||
"esMX": "de rencor",
|
||||
"jaJP": "怨恨の",
|
||||
"ptBR": "do Rancor",
|
||||
"ruRU": "озлобленности",
|
||||
"zhCN": "怨恨之"
|
||||
},
|
||||
{
|
||||
"id": 27905,
|
||||
"Key": "of Apocalypse",
|
||||
"enUS": "of Apocalypse",
|
||||
"zhTW": "天啟末日之",
|
||||
"deDE": "der Apokalypse",
|
||||
"esES": "de la hecatombe",
|
||||
"frFR": "d’apocalypse",
|
||||
"itIT": "dell'Apocalisse",
|
||||
"koKR": "종말의",
|
||||
"plPL": "Apokalipsy",
|
||||
"esMX": "de apocalipsis",
|
||||
"jaJP": "終焉の",
|
||||
"ptBR": "do Apocalipse",
|
||||
"ruRU": "апокалипсиса",
|
||||
"zhCN": "末日之"
|
||||
},
|
||||
{
|
||||
"id": 27912,
|
||||
"Key": "Devil's",
|
||||
"enUS": "Devil's",
|
||||
"zhTW": "妖鬼的",
|
||||
"deDE": "[ms]Diabolischer[fs]Diabolische[ns]Diabolisches[pl]Diabolische",
|
||||
"esES": "del demonio",
|
||||
"frFR": "de démon",
|
||||
"itIT": "del Diavolo",
|
||||
"koKR": "악마",
|
||||
"plPL": "[ms]Diabelski[fs]Diabelska[ns]Diabelskie[p]Diabelskie",
|
||||
"esMX": "[ms]del demontre[fs]del demontre[mp]del demontre[fp]del demontre",
|
||||
"jaJP": "悪魔が携えし",
|
||||
"ptBR": "do Demônio",
|
||||
"ruRU": "[ms]дьявольский[fs]дьявольская[ns]дьявольское[pl]дьявольские",
|
||||
"zhCN": "恶魔的"
|
||||
},
|
||||
{
|
||||
"id": 27913,
|
||||
"Key": "Arch-Devil's",
|
||||
"enUS": "Arch-Devil's",
|
||||
"zhTW": "大妖鬼",
|
||||
"deDE": "[ms]Erzdiabolischer[fs]Erzdiabolische[ns]Erzdiabolisches[pl]Erzdiabolische",
|
||||
"esES": "del archidemonio",
|
||||
"frFR": "d’archidémon",
|
||||
"itIT": "dell'Arcidiavolo",
|
||||
"koKR": "고위악마",
|
||||
"plPL": "[ms]Arcydiabelski[fs]Arcydiabelska[ns]Arcydiabelskie[p]Arcydiabelskie",
|
||||
"esMX": "[ms]del archidemontre[fs]del archidemontre[mp]del archidemontre[fp]del archidemontre",
|
||||
"jaJP": "大悪魔の",
|
||||
"ptBR": "do Arquidemônio",
|
||||
"ruRU": "[ms]архидьявольский[fs]архидьявольская[ns]архидьявольское[pl]архидьявольские",
|
||||
"zhCN": "大恶魔的"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -4010,5 +4010,209 @@
|
||||
"ptBR": "Zefir",
|
||||
"ruRU": "Бриз",
|
||||
"zhCN": "和风"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 27360,
|
||||
"Key": "Runeword171",
|
||||
"enUS": "Hysteria",
|
||||
"zhTW": "歇斯底里",
|
||||
"deDE": "Hysterie",
|
||||
"esES": "Histeria",
|
||||
"frFR": "Hystérie",
|
||||
"itIT": "Isteria",
|
||||
"koKR": "발작",
|
||||
"plPL": "Histeria",
|
||||
"esMX": "Histeria",
|
||||
"jaJP": "発奮",
|
||||
"ptBR": "Histeria",
|
||||
"ruRU": "Истерия",
|
||||
"zhCN": "狂乱"
|
||||
},
|
||||
{
|
||||
"id": 27361,
|
||||
"Key": "Runeword172",
|
||||
"enUS": "Mania",
|
||||
"zhTW": "狂躁",
|
||||
"deDE": "Manie",
|
||||
"esES": "Obsesión",
|
||||
"frFR": "Mania",
|
||||
"itIT": "Mania",
|
||||
"koKR": "광기",
|
||||
"plPL": "Mania",
|
||||
"esMX": "Manía",
|
||||
"jaJP": "マニア",
|
||||
"ptBR": "Obsessão",
|
||||
"ruRU": "Мания",
|
||||
"zhCN": "癫狂"
|
||||
},
|
||||
{
|
||||
"id": 27362,
|
||||
"Key": "Runeword173",
|
||||
"enUS": "Mosaic",
|
||||
"zhTW": "嵌飾",
|
||||
"deDE": "Mosaik",
|
||||
"esES": "Mosaico",
|
||||
"frFR": "Mosaïque",
|
||||
"itIT": "Mosaico",
|
||||
"koKR": "모자이크",
|
||||
"plPL": "Mozaika",
|
||||
"esMX": "Mosaico",
|
||||
"jaJP": "坩堝",
|
||||
"ptBR": "Mosaico",
|
||||
"ruRU": "Мозаика",
|
||||
"zhCN": "模糊"
|
||||
},
|
||||
{
|
||||
"id": 27363,
|
||||
"Key": "Runeword174",
|
||||
"enUS": "Metamorphosis",
|
||||
"zhTW": "變化",
|
||||
"deDE": "Metamorphose",
|
||||
"esES": "Metamorfosis",
|
||||
"frFR": "Métamorphose",
|
||||
"itIT": "Metamorfosi",
|
||||
"koKR": "탈태",
|
||||
"plPL": "Metamorfoza",
|
||||
"esMX": "Metamorfosis",
|
||||
"jaJP": "変容",
|
||||
"ptBR": "Metamorfose",
|
||||
"ruRU": "Метаморфоза",
|
||||
"zhCN": "变形"
|
||||
},
|
||||
{
|
||||
"id": 27364,
|
||||
"Key": "Runeword175",
|
||||
"enUS": "Ground",
|
||||
"zhTW": "接地",
|
||||
"deDE": "Boden",
|
||||
"esES": "Tierra",
|
||||
"frFR": "Terre",
|
||||
"itIT": "Suolo",
|
||||
"koKR": "접지",
|
||||
"plPL": "Grunt",
|
||||
"esMX": "Suelo",
|
||||
"jaJP": "大地",
|
||||
"ptBR": "Solo",
|
||||
"ruRU": "Земля",
|
||||
"zhCN": "接地"
|
||||
},
|
||||
{
|
||||
"id": 27365,
|
||||
"Key": "Runeword176",
|
||||
"enUS": "Temper",
|
||||
"zhTW": "和緩",
|
||||
"deDE": "Temperament",
|
||||
"esES": "Temperamento",
|
||||
"frFR": "Tempérament",
|
||||
"itIT": "Tempra",
|
||||
"koKR": "담금질",
|
||||
"plPL": "Charakter",
|
||||
"esMX": "Ira",
|
||||
"jaJP": "沈着",
|
||||
"ptBR": "Índole",
|
||||
"ruRU": "Закалка",
|
||||
"zhCN": "淬火"
|
||||
},
|
||||
{
|
||||
"id": 27366,
|
||||
"Key": "Runeword177",
|
||||
"enUS": "Hearth",
|
||||
"zhTW": "火爐",
|
||||
"deDE": "Heim",
|
||||
"esES": "Hogar",
|
||||
"frFR": "Âtre",
|
||||
"itIT": "Focolare",
|
||||
"koKR": "화로",
|
||||
"plPL": "Palenisko",
|
||||
"esMX": "Fogón",
|
||||
"jaJP": "炉辺",
|
||||
"ptBR": "Lar",
|
||||
"ruRU": "Очаг",
|
||||
"zhCN": "壁炉"
|
||||
},
|
||||
{
|
||||
"id": 27367,
|
||||
"Key": "Runeword178",
|
||||
"enUS": "Cure",
|
||||
"zhTW": "治癒",
|
||||
"deDE": "Heilung",
|
||||
"esES": "Cura",
|
||||
"frFR": "Remède",
|
||||
"itIT": "Cura",
|
||||
"koKR": "치료",
|
||||
"plPL": "Remedium",
|
||||
"esMX": "Cura",
|
||||
"jaJP": "治癒",
|
||||
"ptBR": "Cura",
|
||||
"ruRU": "Лекарство",
|
||||
"zhCN": "解药"
|
||||
},
|
||||
{
|
||||
"id": 27650,
|
||||
"Key": "Runeword180",
|
||||
"enUS": "Coven",
|
||||
"zhTW": "巫師會",
|
||||
"deDE": "Zirkel",
|
||||
"esES": "Aquelarre",
|
||||
"frFR": "Cabale",
|
||||
"itIT": "Congrega",
|
||||
"koKR": "마녀단",
|
||||
"plPL": "Sabat",
|
||||
"esMX": "Aquelarre",
|
||||
"jaJP": "集会",
|
||||
"ptBR": "Pacto",
|
||||
"ruRU": "Ковен",
|
||||
"zhCN": "女巫团"
|
||||
},
|
||||
{
|
||||
"id": 27651,
|
||||
"Key": "Runeword181",
|
||||
"enUS": "Vigilance",
|
||||
"zhTW": "戒慎",
|
||||
"deDE": "Wachsamkeit",
|
||||
"esES": "Vigilancia",
|
||||
"frFR": "Vigilance",
|
||||
"itIT": "Vigilanza",
|
||||
"koKR": "경계",
|
||||
"plPL": "Czujność",
|
||||
"esMX": "Vigilancia",
|
||||
"jaJP": "警戒",
|
||||
"ptBR": "Vigilância",
|
||||
"ruRU": "Бдительность",
|
||||
"zhCN": "警戒"
|
||||
},
|
||||
{
|
||||
"id": 27652,
|
||||
"Key": "Runeword182",
|
||||
"enUS": "Ritual",
|
||||
"zhTW": "儀式",
|
||||
"deDE": "Ritual",
|
||||
"esES": "Ritual",
|
||||
"frFR": "Rituel",
|
||||
"itIT": "Rituale",
|
||||
"koKR": "의식",
|
||||
"plPL": "Rytuał",
|
||||
"esMX": "Ritual",
|
||||
"jaJP": "儀式",
|
||||
"ptBR": "Ritual",
|
||||
"ruRU": "Ритуал",
|
||||
"zhCN": "仪式"
|
||||
},
|
||||
{
|
||||
"id": 27974,
|
||||
"Key": "Runeword179",
|
||||
"enUS": "Bulwark",
|
||||
"zhTW": "壁壘",
|
||||
"deDE": "Bollwerk",
|
||||
"esES": "Baluarte",
|
||||
"frFR": "Rempart",
|
||||
"itIT": "Baluardo",
|
||||
"koKR": "방벽",
|
||||
"plPL": "Szaniec",
|
||||
"esMX": "Baluarte",
|
||||
"jaJP": "防塁",
|
||||
"ptBR": "Baluarte",
|
||||
"ruRU": "Оплот",
|
||||
"zhCN": "壁垒"
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
@@ -11,6 +11,7 @@
|
||||
(OPTIONAL)
|
||||
(PALADIN
|
||||
(SORCERESS
|
||||
(WARLOCK
|
||||
A
|
||||
ABADDON
|
||||
ABHAYA
|
||||
@@ -75,6 +76,7 @@ AJHEED
|
||||
AKARA
|
||||
AKARAN
|
||||
AKARAT'S
|
||||
AL'DIABOLOS
|
||||
ALACRITY
|
||||
ALADDIN'S
|
||||
ALARIC
|
||||
@@ -175,6 +177,7 @@ ARREAT
|
||||
ARREAT'S
|
||||
ARROW
|
||||
ARROWS
|
||||
ARS
|
||||
ARSENAL
|
||||
ART
|
||||
ARTFUL
|
||||
@@ -339,6 +342,7 @@ BLADEBUCKLE
|
||||
BLADES
|
||||
BLAISE
|
||||
BLANCHED
|
||||
BLASPHEMOUS
|
||||
BLAST
|
||||
BLASTBARK
|
||||
BLAZE
|
||||
@@ -551,8 +555,8 @@ CHAMPION
|
||||
CHANCE
|
||||
CHANDELIER
|
||||
CHANG
|
||||
CHANNEL.
|
||||
CHANNEL:
|
||||
CHANNEL.
|
||||
CHANT
|
||||
CHAOS
|
||||
CHAOTIC
|
||||
@@ -561,8 +565,8 @@ CHARGE
|
||||
CHARGE-UP
|
||||
CHARGED
|
||||
CHARGES
|
||||
CHARGES)
|
||||
CHARGES:
|
||||
CHARGES)
|
||||
CHARM
|
||||
CHARSI
|
||||
CHARSI'S
|
||||
@@ -603,6 +607,7 @@ CLAYMORE
|
||||
CLEANSING
|
||||
CLEARED
|
||||
CLEAVER
|
||||
CLEFT
|
||||
CLEGLAW'S
|
||||
CLICK
|
||||
CLICKING
|
||||
@@ -625,6 +630,7 @@ COBALT
|
||||
COBRA
|
||||
COCOON
|
||||
CODE:
|
||||
CODEX
|
||||
COFFIN
|
||||
COIF
|
||||
COIL
|
||||
@@ -653,10 +659,11 @@ COMMUNAL
|
||||
COMPACT
|
||||
COMPARE
|
||||
COMPELLING
|
||||
COMPENDIUM
|
||||
COMPLETE
|
||||
COMPLETED
|
||||
COMPLETED.
|
||||
COMPLETED:
|
||||
COMPLETED.
|
||||
COMPOSITE
|
||||
CONCENTRATE
|
||||
CONCENTRATION
|
||||
@@ -826,6 +833,7 @@ DECKARD
|
||||
DECLARE
|
||||
DECOY
|
||||
DECREPIFY
|
||||
DEEP
|
||||
DEFEAT
|
||||
DEFENDER
|
||||
DEFENSE
|
||||
@@ -856,8 +864,8 @@ DESTINATION
|
||||
DESTROY
|
||||
DESTROYER
|
||||
DESTRUCTION
|
||||
DESTRUCTION'S
|
||||
DESTRUCTION.
|
||||
DESTRUCTION'S
|
||||
DETAIL
|
||||
DETAILS.
|
||||
DETECT
|
||||
@@ -879,8 +887,8 @@ DIE
|
||||
DIED
|
||||
DIES,
|
||||
DIFFERENCE
|
||||
DIFFERENCE.
|
||||
DIFFERENCE:
|
||||
DIFFERENCE.
|
||||
DIFFERENT
|
||||
DIFFICULTY
|
||||
DIFFICULTY:
|
||||
@@ -959,6 +967,7 @@ DRULAN'S
|
||||
DRUS
|
||||
DRY
|
||||
DUE
|
||||
DUL'MEPHISTOS
|
||||
DUN
|
||||
DUNE
|
||||
DUNES
|
||||
@@ -997,6 +1006,7 @@ EARS
|
||||
EARTH
|
||||
EARTHSHAKER
|
||||
EASE
|
||||
EASTERN
|
||||
EATER
|
||||
EATS
|
||||
EBURINE
|
||||
@@ -1412,8 +1422,8 @@ GOBLIN
|
||||
GODLY
|
||||
GOES
|
||||
GOLD
|
||||
GOLD.
|
||||
GOLD:
|
||||
GOLD.
|
||||
GOLDEN
|
||||
GOLDSKIN
|
||||
GOLDSTRIKE
|
||||
@@ -1460,6 +1470,7 @@ GRIFFON
|
||||
GRIFFON'S
|
||||
GRIM
|
||||
GRIM'S
|
||||
GRIMOIRE
|
||||
GRIN
|
||||
GRINDER
|
||||
GRINDING
|
||||
@@ -1782,8 +1793,8 @@ ITCHIES
|
||||
ITEM
|
||||
ITEM.
|
||||
ITEMS
|
||||
ITEMS.
|
||||
ITEMS:
|
||||
ITEMS.
|
||||
ITH
|
||||
ITHERA
|
||||
ITONYA
|
||||
@@ -1957,9 +1968,9 @@ LEORIC
|
||||
LESSON
|
||||
LESTRON'S
|
||||
LEVEL
|
||||
LEVEL)
|
||||
LEVEL-UP
|
||||
LEVEL:
|
||||
LEVEL)
|
||||
LEVELS
|
||||
LEVER
|
||||
LEVIATHAN
|
||||
@@ -1971,8 +1982,8 @@ LIDLESS
|
||||
LIEF
|
||||
LIENE
|
||||
LIFE
|
||||
LIFE/MANA
|
||||
LIFE:
|
||||
LIFE/MANA
|
||||
LIFECHOKE
|
||||
LIGHT
|
||||
LIGHTBRAND
|
||||
@@ -2136,12 +2147,13 @@ MAULER
|
||||
MAUSOLEUM
|
||||
MAW
|
||||
MAX
|
||||
MAX.
|
||||
MAX:
|
||||
MAX.
|
||||
MAXIMUM
|
||||
MAY
|
||||
MCAULEY'S
|
||||
MEASURE
|
||||
MEASURED
|
||||
MEAT
|
||||
MEATSCRAPE
|
||||
MECHANIC'S
|
||||
@@ -2261,8 +2273,8 @@ NARPHET
|
||||
NATALYA
|
||||
NATALYA'S
|
||||
NATURAL
|
||||
NATURE'S
|
||||
NATURE,
|
||||
NATURE'S
|
||||
NEAR
|
||||
NEARBY
|
||||
NEBUCHADNEZZAR'S
|
||||
@@ -2305,6 +2317,7 @@ NONE
|
||||
NOOSE
|
||||
NORD'S
|
||||
NORMAL
|
||||
NORTHERN
|
||||
NOSFERATU'S
|
||||
NOT
|
||||
NOVA
|
||||
@@ -2323,6 +2336,7 @@ OBLIVION
|
||||
OBSCENE
|
||||
OBSESSION
|
||||
OBSIDIAN
|
||||
OCCULT
|
||||
OCEAN
|
||||
OCHER
|
||||
OCULUS
|
||||
@@ -2349,6 +2363,7 @@ ONES
|
||||
ONLY
|
||||
ONLY)
|
||||
OOZE
|
||||
OPALVEIN
|
||||
OPEN
|
||||
OPENS
|
||||
OPTIMAL
|
||||
@@ -2471,8 +2486,8 @@ PLAYED
|
||||
PLAYER
|
||||
PLAYER.
|
||||
PLAYERS
|
||||
PLAYERS.
|
||||
PLAYERS:
|
||||
PLAYERS.
|
||||
PLEDGE
|
||||
PLUCKEYE
|
||||
PLUS
|
||||
@@ -2617,8 +2632,8 @@ RATMEN
|
||||
RATTLE
|
||||
RATTLECAGE
|
||||
RAVEN
|
||||
RAVEN'S
|
||||
RAVEN:
|
||||
RAVEN'S
|
||||
RAVENLORE
|
||||
RAVENS
|
||||
RAVENS:
|
||||
@@ -2692,9 +2707,9 @@ REND
|
||||
RENDER
|
||||
REPAIR
|
||||
REPAIRED
|
||||
REPAIRED.
|
||||
REPAIRED)
|
||||
REPAIRED),
|
||||
REPAIRED.
|
||||
REPAIRING
|
||||
REPAIRS
|
||||
REPEATING
|
||||
@@ -2804,6 +2819,7 @@ RUN/WALK
|
||||
RUNE
|
||||
RUNES
|
||||
RUNIC
|
||||
RUPTURE
|
||||
RUSSET
|
||||
RUST
|
||||
RUSTHANDLE
|
||||
@@ -2945,8 +2961,8 @@ SHELL
|
||||
SHELTER
|
||||
SHENK
|
||||
SHIELD
|
||||
SHIELD'S
|
||||
SHIELD:
|
||||
SHIELD'S
|
||||
SHIELDS
|
||||
SHIELDS:
|
||||
SHIFT
|
||||
@@ -3001,6 +3017,7 @@ SISTER'S
|
||||
SIX
|
||||
SIZE
|
||||
SIZE:
|
||||
SLING
|
||||
SKELETAL
|
||||
SKELETON
|
||||
SKELETONS
|
||||
@@ -3093,6 +3110,7 @@ SOULSTONE
|
||||
SOUND
|
||||
SOUNDING
|
||||
SOURCE
|
||||
SOUTHERN
|
||||
SPACE
|
||||
SPACING
|
||||
SPANISH
|
||||
@@ -3173,8 +3191,8 @@ STARLIGHT
|
||||
STARS
|
||||
STARS:
|
||||
STASH
|
||||
STASH.
|
||||
STASH:
|
||||
STASH.
|
||||
STAT
|
||||
STAT/SKILL
|
||||
STATE
|
||||
@@ -3303,8 +3321,8 @@ TAINTBREEDER
|
||||
TAINTED
|
||||
TAKE
|
||||
TAKEN
|
||||
TAKEN.
|
||||
TAKEN:
|
||||
TAKEN.
|
||||
TAKES
|
||||
TAL
|
||||
TALBERD'S
|
||||
@@ -3322,8 +3340,8 @@ TANNR
|
||||
TAP
|
||||
TARGE
|
||||
TARGET
|
||||
TARGET'S
|
||||
TARGET:
|
||||
TARGET'S
|
||||
TARGETS
|
||||
TARNHELM
|
||||
TAUNT
|
||||
@@ -3353,6 +3371,7 @@ TERRA'S
|
||||
TERRENE
|
||||
TERROR
|
||||
TERROR'S
|
||||
TEXT
|
||||
THADAR
|
||||
THAN
|
||||
THANK
|
||||
@@ -3431,6 +3450,7 @@ TOOTH
|
||||
TOOTHROW
|
||||
TOP
|
||||
TOPAZ
|
||||
TOR'BAALOS
|
||||
TORC
|
||||
TORCH
|
||||
TORKEL
|
||||
@@ -3645,6 +3665,7 @@ VOID
|
||||
VOIDBRINGER
|
||||
VOLCANIC
|
||||
VOLCANO
|
||||
VOLUME
|
||||
VOMIR
|
||||
VORTEX
|
||||
VOULGE
|
||||
@@ -3653,6 +3674,7 @@ VS.
|
||||
VULPINE
|
||||
VULTURE
|
||||
WACKER
|
||||
WAGER
|
||||
WAHEED
|
||||
WAIL
|
||||
WAILING
|
||||
@@ -3682,6 +3704,7 @@ WARDEN'S
|
||||
WARDER
|
||||
WARDING
|
||||
WARHOUND
|
||||
WARLOCK
|
||||
WARLORD'S
|
||||
WARMING
|
||||
WARMTH
|
||||
@@ -3691,8 +3714,8 @@ WARPED
|
||||
WARPS
|
||||
WARPSPEAR
|
||||
WARRIOR
|
||||
WARRIOR'S
|
||||
WARRIOR.
|
||||
WARRIOR'S
|
||||
WARRIV
|
||||
WARRIV'S
|
||||
WARSHRIKE
|
||||
@@ -3715,8 +3738,8 @@ WEAPON
|
||||
WEAPON:
|
||||
WEAPONS
|
||||
WEAPONS,
|
||||
WEAPONS.
|
||||
WEAPONS:
|
||||
WEAPONS.
|
||||
WEAVER
|
||||
WEB
|
||||
WEDDING
|
||||
@@ -3727,6 +3750,7 @@ WENDY
|
||||
WERE
|
||||
WEREBEAR
|
||||
WEREWOLF
|
||||
WESTERN
|
||||
WHALE
|
||||
WHAT
|
||||
WHEEL
|
||||
@@ -3831,10 +3855,10 @@ YETI
|
||||
YEW
|
||||
YOLK
|
||||
YOU
|
||||
YOU'VE
|
||||
YOU,
|
||||
YOU.
|
||||
YOU:
|
||||
YOU.
|
||||
YOU'VE
|
||||
YOUNG
|
||||
YOUR
|
||||
YOUTH
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Botty Next offline-first visual QA harness."""
|
||||
|
||||
__all__ = ["__version__"]
|
||||
|
||||
__version__ = "0.1.0"
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Capture backends for offline fixtures and live observer mode."""
|
||||
|
||||
from botty_next.capture.mss_backend import MssCaptureBackend
|
||||
from botty_next.capture.window import WindowRegion, find_window_region
|
||||
|
||||
__all__ = ["MssCaptureBackend", "WindowRegion", "find_window_region"]
|
||||
@@ -0,0 +1,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import mss
|
||||
import numpy as np
|
||||
|
||||
from botty_next.capture.window import WindowRegion
|
||||
|
||||
|
||||
class MssCaptureBackend:
|
||||
def grab(self, region: WindowRegion | None = None) -> np.ndarray:
|
||||
with mss.mss() as screen_capture:
|
||||
monitor = region.as_mss_monitor() if region else screen_capture.monitors[1]
|
||||
shot = screen_capture.grab(monitor)
|
||||
|
||||
bgra = np.asarray(shot)
|
||||
return cv2.cvtColor(bgra, cv2.COLOR_BGRA2BGR)
|
||||
|
||||
|
||||
def save_frame(frame: np.ndarray, output_path: str | Path) -> Path:
|
||||
output = Path(output_path)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not cv2.imwrite(str(output), frame):
|
||||
raise RuntimeError(f"failed to write screenshot: {output}")
|
||||
return output
|
||||
@@ -0,0 +1,47 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WindowRegion:
|
||||
left: int
|
||||
top: int
|
||||
width: int
|
||||
height: int
|
||||
title: str
|
||||
|
||||
def as_mss_monitor(self) -> dict[str, int]:
|
||||
return {
|
||||
"left": self.left,
|
||||
"top": self.top,
|
||||
"width": self.width,
|
||||
"height": self.height,
|
||||
}
|
||||
|
||||
|
||||
def find_window_region(title_contains: str) -> WindowRegion:
|
||||
import win32gui
|
||||
|
||||
matches: list[WindowRegion] = []
|
||||
|
||||
def collect(hwnd: int, _extra) -> bool:
|
||||
if not win32gui.IsWindowVisible(hwnd):
|
||||
return True
|
||||
|
||||
title = win32gui.GetWindowText(hwnd)
|
||||
if title_contains.lower() not in title.lower():
|
||||
return True
|
||||
|
||||
left, top, right, bottom = win32gui.GetWindowRect(hwnd)
|
||||
width = right - left
|
||||
height = bottom - top
|
||||
if width > 0 and height > 0:
|
||||
matches.append(WindowRegion(left, top, width, height, title))
|
||||
return True
|
||||
|
||||
win32gui.EnumWindows(collect, None)
|
||||
if not matches:
|
||||
raise RuntimeError(f"no visible window found containing title: {title_contains}")
|
||||
|
||||
return max(matches, key=lambda region: region.width * region.height)
|
||||
@@ -0,0 +1,139 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from botty_next.capture.mss_backend import MssCaptureBackend, save_frame
|
||||
from botty_next.capture.window import find_window_region
|
||||
from botty_next.config import load_config
|
||||
from botty_next.vision.fixtures import load_image
|
||||
from botty_next.vision.ocr import run_tesseract_ocr, save_ocr_preprocess_debug
|
||||
from botty_next.vision.template_matching import match_template, save_match_debug
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(prog="botty-next")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
config_parser = subparsers.add_parser("config")
|
||||
config_subparsers = config_parser.add_subparsers(dest="config_command", required=True)
|
||||
validate_parser = config_subparsers.add_parser("validate")
|
||||
validate_parser.add_argument("-c", "--config", required=True, type=Path)
|
||||
validate_parser.set_defaults(handler=validate_config)
|
||||
|
||||
detect_parser = subparsers.add_parser("detect")
|
||||
detect_parser.add_argument("detector", choices=["template"], help="detector to run")
|
||||
detect_parser.add_argument("--image", required=True, type=Path)
|
||||
detect_parser.add_argument("--template", required=True, type=Path)
|
||||
detect_parser.add_argument("--threshold", type=float, default=0.85)
|
||||
detect_parser.add_argument("--debug-output", type=Path)
|
||||
detect_parser.set_defaults(handler=detect)
|
||||
|
||||
capture_parser = subparsers.add_parser("capture")
|
||||
capture_parser.add_argument("--output", required=True, type=Path)
|
||||
capture_parser.add_argument("--window-title", type=str)
|
||||
capture_parser.set_defaults(handler=capture)
|
||||
|
||||
ocr_parser = subparsers.add_parser("ocr")
|
||||
ocr_parser.add_argument("--image", required=True, type=Path)
|
||||
ocr_parser.add_argument("--lang", default="eng")
|
||||
ocr_parser.add_argument("--psm", type=int, default=6)
|
||||
ocr_parser.add_argument("--tesseract-cmd")
|
||||
ocr_parser.add_argument("--debug-output", type=Path)
|
||||
ocr_parser.set_defaults(handler=ocr)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def validate_config(args: argparse.Namespace) -> int:
|
||||
config = load_config(args.config)
|
||||
print(json.dumps(config.model_dump(mode="json"), indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
def detect(args: argparse.Namespace) -> int:
|
||||
image = load_image(args.image)
|
||||
template = load_image(args.template)
|
||||
result = match_template(image, template, threshold=args.threshold)
|
||||
|
||||
if args.debug_output:
|
||||
save_match_debug(image, result, args.debug_output)
|
||||
|
||||
print(json.dumps(_result_to_dict(result), indent=2))
|
||||
return 0 if result.passed else 1
|
||||
|
||||
|
||||
def capture(args: argparse.Namespace) -> int:
|
||||
region = find_window_region(args.window_title) if args.window_title else None
|
||||
frame = MssCaptureBackend().grab(region)
|
||||
output = save_frame(frame, args.output)
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"output": str(output),
|
||||
"shape": tuple(map(int, frame.shape)),
|
||||
"window": region.title if region else None,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def ocr(args: argparse.Namespace) -> int:
|
||||
image = load_image(args.image)
|
||||
if args.debug_output:
|
||||
save_ocr_preprocess_debug(image, args.debug_output)
|
||||
|
||||
try:
|
||||
result = run_tesseract_ocr(
|
||||
image,
|
||||
lang=args.lang,
|
||||
psm=args.psm,
|
||||
tesseract_cmd=args.tesseract_cmd,
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"error": str(exc),
|
||||
"debug_output": str(args.debug_output) if args.debug_output else None,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
return 2
|
||||
|
||||
print(json.dumps(_ocr_result_to_dict(result), indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
def _result_to_dict(result) -> dict:
|
||||
return {
|
||||
"confidence": result.confidence,
|
||||
"bbox": result.bbox,
|
||||
"passed": result.passed,
|
||||
"method": result.method,
|
||||
"debug": result.debug,
|
||||
}
|
||||
|
||||
|
||||
def _ocr_result_to_dict(result) -> dict:
|
||||
return {
|
||||
"text": result.text,
|
||||
"confidence": result.confidence,
|
||||
"bbox": result.bbox,
|
||||
"debug": result.debug,
|
||||
}
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
return args.handler(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Configuration loading and validation."""
|
||||
|
||||
from botty_next.config.models import BottyNextConfig, load_config
|
||||
|
||||
__all__ = ["BottyNextConfig", "load_config"]
|
||||
@@ -0,0 +1,13 @@
|
||||
profile_name: local
|
||||
capture:
|
||||
backend: fixture
|
||||
monitor: 1
|
||||
fps_limit: 10
|
||||
window_title: null
|
||||
vision:
|
||||
template_threshold: 0.85
|
||||
debug_output_dir: botty_next/debug/output
|
||||
input:
|
||||
enabled: false
|
||||
dry_run: true
|
||||
emergency_stop_key: f12
|
||||
@@ -0,0 +1,59 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
import yaml
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
||||
class CaptureConfig(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
backend: Literal["fixture", "mss", "dxcam"] = "fixture"
|
||||
monitor: int = 1
|
||||
fps_limit: int = Field(default=10, ge=1, le=240)
|
||||
window_title: str | None = None
|
||||
|
||||
|
||||
class VisionConfig(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
template_threshold: float = Field(default=0.85, ge=0.0, le=1.0)
|
||||
debug_output_dir: Path = Path("botty_next/debug/output")
|
||||
|
||||
|
||||
class InputConfig(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
enabled: bool = False
|
||||
dry_run: bool = True
|
||||
emergency_stop_key: str = "f12"
|
||||
|
||||
@field_validator("dry_run")
|
||||
@classmethod
|
||||
def dry_run_required_when_disabled(cls, value: bool) -> bool:
|
||||
return value
|
||||
|
||||
|
||||
class BottyNextConfig(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
profile_name: str = "local"
|
||||
capture: CaptureConfig = Field(default_factory=CaptureConfig)
|
||||
vision: VisionConfig = Field(default_factory=VisionConfig)
|
||||
input: InputConfig = Field(default_factory=InputConfig)
|
||||
|
||||
@field_validator("input")
|
||||
@classmethod
|
||||
def input_must_be_explicit_and_dry_run_by_default(cls, value: InputConfig) -> InputConfig:
|
||||
if value.enabled and value.dry_run is False:
|
||||
raise ValueError("live input cannot be enabled without a future explicit safety gate")
|
||||
return value
|
||||
|
||||
|
||||
def load_config(path: str | Path) -> BottyNextConfig:
|
||||
config_path = Path(path)
|
||||
with config_path.open("r", encoding="utf-8") as handle:
|
||||
raw = yaml.safe_load(handle) or {}
|
||||
return BottyNextConfig.model_validate(raw)
|
||||
@@ -0,0 +1 @@
|
||||
"""Debug image and report output helpers."""
|
||||
@@ -0,0 +1,4 @@
|
||||
"""Input abstraction layer.
|
||||
|
||||
Live input is intentionally not implemented in the bootstrap harness.
|
||||
"""
|
||||
@@ -0,0 +1 @@
|
||||
"""Offline/private routine replay harness."""
|
||||
@@ -0,0 +1 @@
|
||||
"""State detection and transition logic."""
|
||||
@@ -0,0 +1 @@
|
||||
"""Tests for the Botty Next harness."""
|
||||
@@ -0,0 +1,12 @@
|
||||
from botty_next.capture.window import WindowRegion
|
||||
|
||||
|
||||
def test_window_region_converts_to_mss_monitor() -> None:
|
||||
region = WindowRegion(left=10, top=20, width=640, height=480, title="Example")
|
||||
|
||||
assert region.as_mss_monitor() == {
|
||||
"left": 10,
|
||||
"top": 20,
|
||||
"width": 640,
|
||||
"height": 480,
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
from botty_next.cli import main
|
||||
|
||||
|
||||
def test_config_validate_cli_starts(capsys) -> None:
|
||||
exit_code = main(["config", "validate", "-c", "botty_next/config/default.yaml"])
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert exit_code == 0
|
||||
assert '"profile_name": "local"' in captured.out
|
||||
|
||||
|
||||
def test_detect_cli_runs_template_detector(capsys) -> None:
|
||||
exit_code = main(
|
||||
[
|
||||
"detect",
|
||||
"template",
|
||||
"--image",
|
||||
"fixtures/screenshots/sample_scene.ppm",
|
||||
"--template",
|
||||
"fixtures/templates/sample_marker.ppm",
|
||||
"--threshold",
|
||||
"0.99",
|
||||
]
|
||||
)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert exit_code == 0
|
||||
assert '"passed": true' in captured.out
|
||||
@@ -0,0 +1,10 @@
|
||||
from botty_next.config import load_config
|
||||
|
||||
|
||||
def test_load_default_config() -> None:
|
||||
config = load_config("botty_next/config/default.yaml")
|
||||
|
||||
assert config.profile_name == "local"
|
||||
assert config.capture.backend == "fixture"
|
||||
assert config.input.enabled is False
|
||||
assert config.input.dry_run is True
|
||||
@@ -0,0 +1,13 @@
|
||||
from botty_next.vision.fixtures import load_screenshot, load_template
|
||||
|
||||
|
||||
def test_load_sample_screenshot_fixture() -> None:
|
||||
image = load_screenshot("sample_scene.ppm")
|
||||
|
||||
assert image.shape == (8, 8, 3)
|
||||
|
||||
|
||||
def test_load_sample_template_fixture() -> None:
|
||||
template = load_template("sample_marker.ppm")
|
||||
|
||||
assert template.shape == (3, 3, 3)
|
||||
@@ -0,0 +1,42 @@
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from botty_next.vision.fixtures import load_screenshot
|
||||
from botty_next.vision.ocr import preprocess_for_ocr, run_tesseract_ocr, save_ocr_preprocess_debug
|
||||
|
||||
|
||||
def test_preprocess_for_ocr_returns_thresholded_image() -> None:
|
||||
image = load_screenshot("sample_scene.ppm")
|
||||
|
||||
processed = preprocess_for_ocr(image)
|
||||
|
||||
assert processed.ndim == 2
|
||||
assert processed.shape == (16, 16)
|
||||
|
||||
|
||||
def test_save_ocr_preprocess_debug(tmp_path) -> None:
|
||||
image = load_screenshot("sample_scene.ppm")
|
||||
|
||||
output = save_ocr_preprocess_debug(image, tmp_path / "ocr.png")
|
||||
|
||||
assert output.exists()
|
||||
|
||||
|
||||
def test_run_tesseract_ocr_uses_pytesseract_adapter(monkeypatch) -> None:
|
||||
fake = SimpleNamespace(
|
||||
Output=SimpleNamespace(DICT="dict"),
|
||||
pytesseract=SimpleNamespace(tesseract_cmd=None),
|
||||
image_to_string=lambda *_args, **_kwargs: "Short Sword\n",
|
||||
image_to_data=lambda *_args, **_kwargs: {"conf": ["95", "-1", "85"]},
|
||||
)
|
||||
monkeypatch.setitem(sys.modules, "pytesseract", fake)
|
||||
|
||||
image = load_screenshot("sample_scene.ppm")
|
||||
result = run_tesseract_ocr(image, tesseract_cmd="C:/Tesseract/tesseract.exe")
|
||||
|
||||
assert result.text == "Short Sword"
|
||||
assert result.confidence == pytest.approx(0.9)
|
||||
assert result.debug["backend"] == "pytesseract"
|
||||
assert fake.pytesseract.tesseract_cmd == "C:/Tesseract/tesseract.exe"
|
||||
@@ -0,0 +1,24 @@
|
||||
from botty_next.vision.fixtures import load_screenshot, load_template
|
||||
from botty_next.vision.template_matching import match_template, save_match_debug
|
||||
|
||||
|
||||
def test_template_match_finds_sample_marker() -> None:
|
||||
image = load_screenshot("sample_scene.ppm")
|
||||
template = load_template("sample_marker.ppm")
|
||||
|
||||
result = match_template(image, template, threshold=0.99)
|
||||
|
||||
assert result.passed is True
|
||||
assert result.confidence >= 0.99
|
||||
assert result.bbox == (3, 2, 3, 3)
|
||||
assert "image_shape" in result.debug
|
||||
|
||||
|
||||
def test_template_match_can_save_debug_image(tmp_path) -> None:
|
||||
image = load_screenshot("sample_scene.ppm")
|
||||
template = load_template("sample_marker.ppm")
|
||||
result = match_template(image, template, threshold=0.99)
|
||||
|
||||
output = save_match_debug(image, result, tmp_path / "marked.png")
|
||||
|
||||
assert output.exists()
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Vision helpers and detectors."""
|
||||
|
||||
from botty_next.vision.ocr import OcrResult, preprocess_for_ocr, run_tesseract_ocr
|
||||
from botty_next.vision.template_matching import MatchResult, match_template
|
||||
|
||||
__all__ = ["MatchResult", "OcrResult", "match_template", "preprocess_for_ocr", "run_tesseract_ocr"]
|
||||
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
def load_image(path: str | Path, *, grayscale: bool = False) -> np.ndarray:
|
||||
image_path = Path(path)
|
||||
if not image_path.exists():
|
||||
raise FileNotFoundError(f"image fixture does not exist: {image_path}")
|
||||
|
||||
flag = cv2.IMREAD_GRAYSCALE if grayscale else cv2.IMREAD_COLOR
|
||||
image = cv2.imread(str(image_path), flag)
|
||||
if image is None:
|
||||
raise ValueError(f"OpenCV could not read image fixture: {image_path}")
|
||||
return image
|
||||
|
||||
|
||||
def load_screenshot(name: str, root: str | Path = "fixtures/screenshots") -> np.ndarray:
|
||||
return load_image(Path(root) / name)
|
||||
|
||||
|
||||
def load_template(name: str, root: str | Path = "fixtures/templates") -> np.ndarray:
|
||||
return load_image(Path(root) / name)
|
||||
@@ -0,0 +1,98 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OcrResult:
|
||||
text: str
|
||||
confidence: float
|
||||
bbox: tuple[int, int, int, int] | None
|
||||
debug: dict[str, Any]
|
||||
|
||||
|
||||
def preprocess_for_ocr(image: np.ndarray, *, scale: float = 2.0) -> np.ndarray:
|
||||
if image.size == 0:
|
||||
raise ValueError("image is empty")
|
||||
|
||||
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if image.ndim == 3 else image
|
||||
if scale != 1.0:
|
||||
gray = cv2.resize(gray, None, fx=scale, fy=scale, interpolation=cv2.INTER_CUBIC)
|
||||
denoised = cv2.GaussianBlur(gray, (3, 3), 0)
|
||||
return cv2.adaptiveThreshold(
|
||||
denoised,
|
||||
255,
|
||||
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
||||
cv2.THRESH_BINARY,
|
||||
31,
|
||||
7,
|
||||
)
|
||||
|
||||
|
||||
def run_tesseract_ocr(
|
||||
image: np.ndarray,
|
||||
*,
|
||||
lang: str = "eng",
|
||||
psm: int = 6,
|
||||
tesseract_cmd: str | None = None,
|
||||
) -> OcrResult:
|
||||
try:
|
||||
pytesseract = import_module("pytesseract")
|
||||
except ModuleNotFoundError as exc:
|
||||
raise RuntimeError(
|
||||
"pytesseract is not installed; run install.bat or install requirements.txt"
|
||||
) from exc
|
||||
|
||||
if tesseract_cmd:
|
||||
pytesseract.pytesseract.tesseract_cmd = tesseract_cmd
|
||||
|
||||
processed = preprocess_for_ocr(image)
|
||||
config = f"--psm {psm}"
|
||||
text = pytesseract.image_to_string(processed, lang=lang, config=config).strip()
|
||||
confidences = _read_confidences(
|
||||
pytesseract.image_to_data(
|
||||
processed,
|
||||
lang=lang,
|
||||
config=config,
|
||||
output_type=pytesseract.Output.DICT,
|
||||
)
|
||||
)
|
||||
confidence = sum(confidences) / len(confidences) if confidences else 0.0
|
||||
return OcrResult(
|
||||
text=text,
|
||||
confidence=confidence,
|
||||
bbox=None,
|
||||
debug={
|
||||
"backend": "pytesseract",
|
||||
"lang": lang,
|
||||
"psm": psm,
|
||||
"preprocessed_shape": tuple(map(int, processed.shape)),
|
||||
"word_confidences": confidences,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def save_ocr_preprocess_debug(image: np.ndarray, output_path: str | Path) -> Path:
|
||||
output = Path(output_path)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not cv2.imwrite(str(output), preprocess_for_ocr(image)):
|
||||
raise RuntimeError(f"failed to write OCR debug image: {output}")
|
||||
return output
|
||||
|
||||
|
||||
def _read_confidences(data: dict[str, list[Any]]) -> list[float]:
|
||||
values: list[float] = []
|
||||
for raw in data.get("conf", []):
|
||||
try:
|
||||
confidence = float(raw)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if confidence >= 0:
|
||||
values.append(confidence / 100.0)
|
||||
return values
|
||||
@@ -0,0 +1,93 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MatchResult:
|
||||
confidence: float
|
||||
bbox: tuple[int, int, int, int]
|
||||
passed: bool
|
||||
method: str
|
||||
debug: dict[str, Any]
|
||||
|
||||
|
||||
def _as_gray(image: np.ndarray) -> np.ndarray:
|
||||
if image.ndim == 2:
|
||||
return image
|
||||
return cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
||||
|
||||
|
||||
def match_template(
|
||||
image: np.ndarray,
|
||||
template: np.ndarray,
|
||||
*,
|
||||
threshold: float = 0.85,
|
||||
method: int = cv2.TM_CCOEFF_NORMED,
|
||||
) -> MatchResult:
|
||||
if image.size == 0:
|
||||
raise ValueError("image is empty")
|
||||
if template.size == 0:
|
||||
raise ValueError("template is empty")
|
||||
if template.shape[0] > image.shape[0] or template.shape[1] > image.shape[1]:
|
||||
raise ValueError("template cannot be larger than image")
|
||||
|
||||
image_gray = _as_gray(image)
|
||||
template_gray = _as_gray(template)
|
||||
response = cv2.matchTemplate(image_gray, template_gray, method)
|
||||
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(response)
|
||||
|
||||
if method in (cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED):
|
||||
top_left = min_loc
|
||||
confidence = 1.0 - float(min_val)
|
||||
else:
|
||||
top_left = max_loc
|
||||
confidence = float(max_val)
|
||||
|
||||
width = int(template.shape[1])
|
||||
height = int(template.shape[0])
|
||||
bbox = (int(top_left[0]), int(top_left[1]), width, height)
|
||||
return MatchResult(
|
||||
confidence=confidence,
|
||||
bbox=bbox,
|
||||
passed=confidence >= threshold,
|
||||
method=_method_name(method),
|
||||
debug={
|
||||
"threshold": threshold,
|
||||
"min_value": float(min_val),
|
||||
"max_value": float(max_val),
|
||||
"min_location": tuple(map(int, min_loc)),
|
||||
"max_location": tuple(map(int, max_loc)),
|
||||
"image_shape": tuple(map(int, image.shape)),
|
||||
"template_shape": tuple(map(int, template.shape)),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def save_match_debug(image: np.ndarray, result: MatchResult, output_path: str | Path) -> Path:
|
||||
output = Path(output_path)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
marked = image.copy()
|
||||
x, y, width, height = result.bbox
|
||||
color = (0, 255, 0) if result.passed else (0, 0, 255)
|
||||
cv2.rectangle(marked, (x, y), (x + width, y + height), color, 2)
|
||||
cv2.imwrite(str(output), marked)
|
||||
return output
|
||||
|
||||
|
||||
def _method_name(method: int) -> str:
|
||||
names = {
|
||||
cv2.TM_CCOEFF: "TM_CCOEFF",
|
||||
cv2.TM_CCOEFF_NORMED: "TM_CCOEFF_NORMED",
|
||||
cv2.TM_CCORR: "TM_CCORR",
|
||||
cv2.TM_CCORR_NORMED: "TM_CCORR_NORMED",
|
||||
cv2.TM_SQDIFF: "TM_SQDIFF",
|
||||
cv2.TM_SQDIFF_NORMED: "TM_SQDIFF_NORMED",
|
||||
}
|
||||
return names.get(method, str(method))
|
||||
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from src.version import __version__
|
||||
import argparse
|
||||
@@ -9,6 +10,28 @@ from cryptography.fernet import Fernet
|
||||
import string
|
||||
|
||||
|
||||
def _resolve_botty_env(conda_path):
|
||||
"""Find the botty Python environment directory.
|
||||
|
||||
Tries (in order):
|
||||
1. Explicit conda path (conda_path/envs/botty)
|
||||
2. Current sys.prefix if it looks like a conda env
|
||||
3. Fallback to sys.prefix (pip/virtualenv installs)
|
||||
"""
|
||||
# 1. Explicit conda path
|
||||
botty_env = os.path.join(conda_path, "envs", "botty")
|
||||
if os.path.isdir(botty_env):
|
||||
return botty_env
|
||||
|
||||
# 2. Current prefix is a conda env
|
||||
if os.path.isfile(os.path.join(sys.prefix, "conda-meta", "history")) or \
|
||||
os.path.isdir(os.path.join(sys.prefix, "Library")):
|
||||
return sys.prefix
|
||||
|
||||
# 3. Plain pip / virtualenv — sys.prefix is the site
|
||||
return sys.prefix
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(description="Build Botty")
|
||||
parser.add_argument(
|
||||
"-v" , "--version",
|
||||
@@ -71,25 +94,72 @@ if __name__ == "__main__":
|
||||
shutil.rmtree(path)
|
||||
shutil.rmtree(botty_dir)
|
||||
|
||||
botty_env = _resolve_botty_env(args.conda_path)
|
||||
pyinstaller_exe = os.path.join(botty_env, "Scripts", "pyinstaller.exe")
|
||||
if not os.path.isfile(pyinstaller_exe):
|
||||
raise RuntimeError(f"PyInstaller not found at {pyinstaller_exe}. "
|
||||
f"Install with: pip install pyinstaller")
|
||||
|
||||
# DLL dirs for PyInstaller to resolve native dependencies.
|
||||
# Conda: Library\bin, Library\lib, DLLs
|
||||
# pip/virtualenv: just the system DLLs under sys.prefix
|
||||
dll_dirs = []
|
||||
for d in ["Library/bin", "Library/lib", "DLLs"]:
|
||||
p = os.path.join(botty_env, d)
|
||||
if os.path.isdir(p):
|
||||
dll_dirs.append(p)
|
||||
if dll_dirs:
|
||||
os.environ["PATH"] = os.pathsep.join(dll_dirs) + os.pathsep + os.environ.get("PATH", "")
|
||||
|
||||
for exe in ["main.py", "shopper.py"]:
|
||||
key_cmd = " "
|
||||
if args.use_key:
|
||||
key = Fernet.generate_key().decode("utf-8")
|
||||
key_cmd = " --key " + key
|
||||
installer_cmd = f"pyinstaller --onefile --distpath {botty_dir}{key_cmd} --exclude-module graphviz --paths .\\src --paths {args.conda_path}\\envs\\botty\\Lib\\site-packages src\\{exe}"
|
||||
os.system(installer_cmd)
|
||||
installer_cmd = f'{pyinstaller_exe} --onefile --noconsole --distpath {botty_dir}{key_cmd} --exclude-module graphviz --exclude-module keyboard --exclude-module mouse --exclude-module pyclick --exclude-module mouseinfo --paths .\\src --paths "{botty_env}\\Lib\\site-packages" src\\{exe}'
|
||||
ret = os.system(installer_cmd)
|
||||
if ret != 0:
|
||||
raise RuntimeError(f"PyInstaller failed for {exe} (exit {ret})")
|
||||
|
||||
os.system(f"cd {botty_dir} && mkdir config && cd ..")
|
||||
os.makedirs(f"{botty_dir}/config", exist_ok=True)
|
||||
|
||||
with open(f"{botty_dir}/config/custom.ini", "w") as f:
|
||||
f.write("; Add parameters you want to overwrite from param.ini here")
|
||||
shutil.copy("config/game.ini", f"{botty_dir}/config/")
|
||||
shutil.copy("config/params.ini", f"{botty_dir}/config/")
|
||||
shutil.copy("config/shop.ini", f"{botty_dir}/config/")
|
||||
shutil.copy("config/default.nip", f"{botty_dir}/config/")
|
||||
os.makedirs(f"{botty_dir}/config/nip", exist_ok=True)
|
||||
shutil.copy("config/default.bnip", f"{botty_dir}/config/")
|
||||
os.makedirs(f"{botty_dir}/config/bnip", exist_ok=True)
|
||||
shutil.copy("README.md", f"{botty_dir}/")
|
||||
shutil.copytree("assets", f"{botty_dir}/assets")
|
||||
shutil.copytree("src", f"{botty_dir}/src")
|
||||
shutil.copy("environment.yml", f"{botty_dir}/")
|
||||
shutil.copy("install.bat", f"{botty_dir}/")
|
||||
shutil.copy("find_python.bat", f"{botty_dir}/")
|
||||
shutil.copy("run_botty.bat", f"{botty_dir}/")
|
||||
shutil.copy("run.bat", f"{botty_dir}/")
|
||||
if os.path.exists("dependencies"):
|
||||
shutil.copytree("dependencies", f"{botty_dir}/dependencies")
|
||||
|
||||
# Bundle a portable Tesseract so the standalone exe is click-and-run with
|
||||
# working OCR and no separate install. ocr.py prefers <exe_dir>/tesseract/
|
||||
# tesseract.exe. Source: TESSERACT_DIR env or the default UB Mannheim path.
|
||||
# Skipped (with a warning) if not present — the bot still works once the
|
||||
# user runs install.bat, which sets OCR up the conda way.
|
||||
tesseract_src = os.environ.get("TESSERACT_DIR", r"C:\Program Files\Tesseract-OCR")
|
||||
tess_exe = os.path.join(tesseract_src, "tesseract.exe")
|
||||
if os.path.isfile(tess_exe):
|
||||
print(f"Bundling Tesseract from {tesseract_src}")
|
||||
# Copy the exe + DLLs; skip their tessdata (we ship our own trained
|
||||
# models in assets/tessdata and pass --tessdata-dir to point at them).
|
||||
os.makedirs(f"{botty_dir}/tesseract", exist_ok=True)
|
||||
for entry in os.listdir(tesseract_src):
|
||||
src = os.path.join(tesseract_src, entry)
|
||||
if os.path.isfile(src) and entry.lower().endswith((".exe", ".dll")):
|
||||
shutil.copy(src, f"{botty_dir}/tesseract/")
|
||||
else:
|
||||
print(f"WARNING: Tesseract not found at {tesseract_src} — release will "
|
||||
f"rely on install.bat for OCR setup. Set TESSERACT_DIR to bundle it.")
|
||||
clean_up()
|
||||
|
||||
if args.random_name:
|
||||
@@ -97,6 +167,13 @@ if __name__ == "__main__":
|
||||
new_name = ''.join(random.choices(string.ascii_letters, k=random.randint(6, 14)))
|
||||
os.rename(f'{botty_dir}/main.exe', f'{botty_dir}/{new_name}.exe')
|
||||
|
||||
# Rename main.exe to avoid Warden flagging the obvious name
|
||||
# In CI/production builds (env BOTTY_NO_RENAME=1) keep main.exe as-is
|
||||
if not args.random_name and not os.environ.get("BOTTY_NO_RENAME"):
|
||||
new_name = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
|
||||
os.rename(f'{botty_dir}/main.exe', f'{botty_dir}/{new_name}.exe')
|
||||
print(f"Renamed main.exe -> {new_name}.exe")
|
||||
|
||||
if new_version_code is not None:
|
||||
os.system(f'git add .')
|
||||
os.system(f'git commit -m "Bump version to v{args.version}"')
|
||||
os.system(f'git commit -m "Bump version to v{args.version}"')
|
||||
@@ -0,0 +1,15 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
set "BOTTY_DIR=%~dp0"
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%BOTTY_DIR%tools\check_dependencies.ps1"
|
||||
set "RC=%ERRORLEVEL%"
|
||||
|
||||
if not "%RC%"=="0" (
|
||||
echo.
|
||||
echo Dependency check reported issues.
|
||||
)
|
||||
|
||||
pause
|
||||
exit /b %RC%
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
; Personal overrides for your local machine/user.
|
||||
; This file is tracked as an example.
|
||||
; Copy to config/custom.ini and edit values there.
|
||||
;
|
||||
; Botty auto-loads config/custom.ini when present.
|
||||
|
||||
[general]
|
||||
; name=botty
|
||||
; custom_message_hook=
|
||||
; custom_loot_message_hook=
|
||||
; discord_status_runs=10
|
||||
|
||||
[char]
|
||||
; type=blizz_sorc
|
||||
; atk_len_pindle=3.0
|
||||
; show_items=alt
|
||||
|
||||
[sorceress]
|
||||
; teleport=t
|
||||
|
||||
[blizz_sorc]
|
||||
; blizzard=f1
|
||||
; ice_blast=f2
|
||||
@@ -0,0 +1,800 @@
|
||||
{
|
||||
"generated_at": "2026-06-17T07:19:33.572415+00:00",
|
||||
"mode": "offline",
|
||||
"offline_dir": "data\\d2jsp_pages",
|
||||
"ladder_start_date": "2026-05-12",
|
||||
"days": 60,
|
||||
"topics_scanned": 853,
|
||||
"estimates": {
|
||||
"day_1": {},
|
||||
"day_2": {},
|
||||
"day_3": {},
|
||||
"day_4": {},
|
||||
"day_5": {},
|
||||
"day_6": {},
|
||||
"day_7": {},
|
||||
"day_8": {},
|
||||
"day_9": {},
|
||||
"day_10": {},
|
||||
"day_11": {},
|
||||
"day_12": {},
|
||||
"day_13": {},
|
||||
"day_14": {},
|
||||
"day_15": {},
|
||||
"day_16": {},
|
||||
"day_17": {},
|
||||
"day_18": {},
|
||||
"day_19": {},
|
||||
"day_20": {},
|
||||
"day_21": {},
|
||||
"day_22": {},
|
||||
"day_23": {},
|
||||
"day_24": {},
|
||||
"day_25": {},
|
||||
"day_26": {},
|
||||
"day_27": {},
|
||||
"day_28": {},
|
||||
"day_29": {},
|
||||
"day_30": {},
|
||||
"day_31": {
|
||||
"CTA": {
|
||||
"median_fg": 100.0,
|
||||
"avg_fg": 358.0,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 1199.0,
|
||||
"samples": 5
|
||||
},
|
||||
"Cham Rune": {
|
||||
"median_fg": 30.0,
|
||||
"avg_fg": 121.0,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 450.0,
|
||||
"samples": 5
|
||||
},
|
||||
"Lo Rune": {
|
||||
"median_fg": 25.0,
|
||||
"avg_fg": 38.8,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 100.0,
|
||||
"samples": 4
|
||||
},
|
||||
"Um Rune": {
|
||||
"median_fg": 75.0,
|
||||
"avg_fg": 99.6,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 400.0,
|
||||
"samples": 10
|
||||
},
|
||||
"Lem Rune": {
|
||||
"median_fg": 60.0,
|
||||
"avg_fg": 72.1,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 190.0,
|
||||
"samples": 7
|
||||
},
|
||||
"Sorc Torch": {
|
||||
"median_fg": 100.0,
|
||||
"avg_fg": 100.0,
|
||||
"min_fg": 100.0,
|
||||
"max_fg": 100.0,
|
||||
"samples": 1
|
||||
},
|
||||
"Aldur's Advance": {
|
||||
"median_fg": 27.5,
|
||||
"avg_fg": 43.8,
|
||||
"min_fg": 20.0,
|
||||
"max_fg": 100.0,
|
||||
"samples": 4
|
||||
},
|
||||
"Stealth RW": {
|
||||
"median_fg": 55.0,
|
||||
"avg_fg": 55.0,
|
||||
"min_fg": 10.0,
|
||||
"max_fg": 100.0,
|
||||
"samples": 2
|
||||
},
|
||||
"Ist Rune": {
|
||||
"median_fg": 95.0,
|
||||
"avg_fg": 112.2,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 400.0,
|
||||
"samples": 12
|
||||
},
|
||||
"Mal Rune": {
|
||||
"median_fg": 100.0,
|
||||
"avg_fg": 112.0,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 400.0,
|
||||
"samples": 13
|
||||
},
|
||||
"Pul Rune": {
|
||||
"median_fg": 60.0,
|
||||
"avg_fg": 72.1,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 190.0,
|
||||
"samples": 7
|
||||
},
|
||||
"Unid Anni": {
|
||||
"median_fg": 550.0,
|
||||
"avg_fg": 416.7,
|
||||
"min_fg": 100.0,
|
||||
"max_fg": 600.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Dual Leech Ring": {
|
||||
"median_fg": 85.0,
|
||||
"avg_fg": 85.0,
|
||||
"min_fg": 70.0,
|
||||
"max_fg": 100.0,
|
||||
"samples": 2
|
||||
},
|
||||
"Gul Rune": {
|
||||
"median_fg": 25.0,
|
||||
"avg_fg": 63.1,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 200.0,
|
||||
"samples": 8
|
||||
},
|
||||
"Key": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Insight": {
|
||||
"median_fg": 25.0,
|
||||
"avg_fg": 91.0,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 400.0,
|
||||
"samples": 6
|
||||
},
|
||||
"Ohm Rune": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Vampire Gaze": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"White": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Shako": {
|
||||
"median_fg": 25.0,
|
||||
"avg_fg": 91.0,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 400.0,
|
||||
"samples": 6
|
||||
},
|
||||
"Black": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Monarch": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Vex Rune": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Java SK": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"BK Ring": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Skin": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Oculus": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Spirit": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Bone": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Jah Rune": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Moser": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Ber Rune": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 18.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 30.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Arach": {
|
||||
"median_fg": 90.0,
|
||||
"avg_fg": 163.7,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 400.0,
|
||||
"samples": 3
|
||||
},
|
||||
"5/5 Facet": {
|
||||
"median_fg": 90.0,
|
||||
"avg_fg": 163.7,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 400.0,
|
||||
"samples": 3
|
||||
},
|
||||
"Mara": {
|
||||
"median_fg": 90.0,
|
||||
"avg_fg": 163.7,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 400.0,
|
||||
"samples": 3
|
||||
}
|
||||
},
|
||||
"day_32": {},
|
||||
"day_33": {},
|
||||
"day_34": {},
|
||||
"day_35": {},
|
||||
"day_36": {},
|
||||
"day_37": {
|
||||
"Thresher": {
|
||||
"median_fg": 32.5,
|
||||
"avg_fg": 133.3,
|
||||
"min_fg": 0.0,
|
||||
"max_fg": 3500.0,
|
||||
"samples": 210
|
||||
},
|
||||
"Monarch": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 71.5,
|
||||
"min_fg": 0.0,
|
||||
"max_fg": 1300.0,
|
||||
"samples": 318
|
||||
},
|
||||
"Spirit": {
|
||||
"median_fg": 25.0,
|
||||
"avg_fg": 78.3,
|
||||
"min_fg": 0.0,
|
||||
"max_fg": 1300.0,
|
||||
"samples": 237
|
||||
},
|
||||
"Wind": {
|
||||
"median_fg": 30.0,
|
||||
"avg_fg": 175.0,
|
||||
"min_fg": 0.0,
|
||||
"max_fg": 1200.0,
|
||||
"samples": 60
|
||||
},
|
||||
"Arach": {
|
||||
"median_fg": 40.0,
|
||||
"avg_fg": 184.2,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 3500.0,
|
||||
"samples": 210
|
||||
},
|
||||
"Sorc Torch": {
|
||||
"median_fg": 50.0,
|
||||
"avg_fg": 166.2,
|
||||
"min_fg": 10.0,
|
||||
"max_fg": 2500.0,
|
||||
"samples": 64
|
||||
},
|
||||
"Griffon": {
|
||||
"median_fg": 60.0,
|
||||
"avg_fg": 254.7,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 3500.0,
|
||||
"samples": 111
|
||||
},
|
||||
"Guardian's Light": {
|
||||
"median_fg": 50.0,
|
||||
"avg_fg": 342.6,
|
||||
"min_fg": 0.0,
|
||||
"max_fg": 3500.0,
|
||||
"samples": 236
|
||||
},
|
||||
"Skin": {
|
||||
"median_fg": 30.0,
|
||||
"avg_fg": 206.8,
|
||||
"min_fg": 0.0,
|
||||
"max_fg": 3000.0,
|
||||
"samples": 242
|
||||
},
|
||||
"Viper Gorge": {
|
||||
"median_fg": 30.0,
|
||||
"avg_fg": 75.9,
|
||||
"min_fg": 0.0,
|
||||
"max_fg": 1200.0,
|
||||
"samples": 231
|
||||
},
|
||||
"Lo Rune": {
|
||||
"median_fg": 30.0,
|
||||
"avg_fg": 113.6,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 1500.0,
|
||||
"samples": 105
|
||||
},
|
||||
"Enigma": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 182.4,
|
||||
"min_fg": 0.0,
|
||||
"max_fg": 1500.0,
|
||||
"samples": 49
|
||||
},
|
||||
"Bone": {
|
||||
"median_fg": 40.0,
|
||||
"avg_fg": 244.3,
|
||||
"min_fg": 0.0,
|
||||
"max_fg": 3500.0,
|
||||
"samples": 264
|
||||
},
|
||||
"Um Rune": {
|
||||
"median_fg": 45.0,
|
||||
"avg_fg": 187.3,
|
||||
"min_fg": 0.0,
|
||||
"max_fg": 3500.0,
|
||||
"samples": 191
|
||||
},
|
||||
"Lem Rune": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 48.7,
|
||||
"min_fg": 0.0,
|
||||
"max_fg": 750.0,
|
||||
"samples": 126
|
||||
},
|
||||
"Crescent": {
|
||||
"median_fg": 10.0,
|
||||
"avg_fg": 63.0,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 600.0,
|
||||
"samples": 70
|
||||
},
|
||||
"Ist Rune": {
|
||||
"median_fg": 45.0,
|
||||
"avg_fg": 74.9,
|
||||
"min_fg": 3.0,
|
||||
"max_fg": 750.0,
|
||||
"samples": 173
|
||||
},
|
||||
"Highlord": {
|
||||
"median_fg": 25.0,
|
||||
"avg_fg": 113.0,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 1500.0,
|
||||
"samples": 70
|
||||
},
|
||||
"BK Ring": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 70.0,
|
||||
"min_fg": 0.0,
|
||||
"max_fg": 1200.0,
|
||||
"samples": 161
|
||||
},
|
||||
"Mal Rune": {
|
||||
"median_fg": 30.0,
|
||||
"avg_fg": 67.0,
|
||||
"min_fg": 3.0,
|
||||
"max_fg": 750.0,
|
||||
"samples": 166
|
||||
},
|
||||
"Insight": {
|
||||
"median_fg": 40.0,
|
||||
"avg_fg": 189.9,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 3500.0,
|
||||
"samples": 155
|
||||
},
|
||||
"Pul Rune": {
|
||||
"median_fg": 30.0,
|
||||
"avg_fg": 69.6,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 750.0,
|
||||
"samples": 113
|
||||
},
|
||||
"CTA": {
|
||||
"median_fg": 50.0,
|
||||
"avg_fg": 251.5,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 3500.0,
|
||||
"samples": 171
|
||||
},
|
||||
"Blessed": {
|
||||
"median_fg": 30.0,
|
||||
"avg_fg": 180.3,
|
||||
"min_fg": 7.0,
|
||||
"max_fg": 1200.0,
|
||||
"samples": 36
|
||||
},
|
||||
"Ohm Rune": {
|
||||
"median_fg": 25.0,
|
||||
"avg_fg": 39.8,
|
||||
"min_fg": 3.0,
|
||||
"max_fg": 400.0,
|
||||
"samples": 119
|
||||
},
|
||||
"Java SK": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 47.0,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 450.0,
|
||||
"samples": 58
|
||||
},
|
||||
"Vampire Gaze": {
|
||||
"median_fg": 10.0,
|
||||
"avg_fg": 110.6,
|
||||
"min_fg": 0.0,
|
||||
"max_fg": 1500.0,
|
||||
"samples": 80
|
||||
},
|
||||
"Giant": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 96.1,
|
||||
"min_fg": 0.0,
|
||||
"max_fg": 600.0,
|
||||
"samples": 70
|
||||
},
|
||||
"Ber Rune": {
|
||||
"median_fg": 60.0,
|
||||
"avg_fg": 253.1,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 1200.0,
|
||||
"samples": 58
|
||||
},
|
||||
"Key": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 83.1,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 1200.0,
|
||||
"samples": 114
|
||||
},
|
||||
"Shako": {
|
||||
"median_fg": 30.0,
|
||||
"avg_fg": 113.6,
|
||||
"min_fg": 0.0,
|
||||
"max_fg": 1500.0,
|
||||
"samples": 208
|
||||
},
|
||||
"Pcomb SK": {
|
||||
"median_fg": 160.0,
|
||||
"avg_fg": 685.0,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 3000.0,
|
||||
"samples": 48
|
||||
},
|
||||
"Sacred": {
|
||||
"median_fg": 35.0,
|
||||
"avg_fg": 166.1,
|
||||
"min_fg": 0.0,
|
||||
"max_fg": 3500.0,
|
||||
"samples": 244
|
||||
},
|
||||
"Light Jewel": {
|
||||
"median_fg": 80.0,
|
||||
"avg_fg": 234.0,
|
||||
"min_fg": 20.0,
|
||||
"max_fg": 1500.0,
|
||||
"samples": 39
|
||||
},
|
||||
"Fist": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 51.7,
|
||||
"min_fg": 15.0,
|
||||
"max_fg": 120.0,
|
||||
"samples": 9
|
||||
},
|
||||
"Vex Rune": {
|
||||
"median_fg": 30.0,
|
||||
"avg_fg": 97.2,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 1500.0,
|
||||
"samples": 132
|
||||
},
|
||||
"Unid Anni": {
|
||||
"median_fg": 35.0,
|
||||
"avg_fg": 45.0,
|
||||
"min_fg": 3.0,
|
||||
"max_fg": 150.0,
|
||||
"samples": 96
|
||||
},
|
||||
"Light SK": {
|
||||
"median_fg": 40.0,
|
||||
"avg_fg": 383.2,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 3500.0,
|
||||
"samples": 41
|
||||
},
|
||||
"Mara": {
|
||||
"median_fg": 50.0,
|
||||
"avg_fg": 146.1,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 1500.0,
|
||||
"samples": 132
|
||||
},
|
||||
"Cold SK": {
|
||||
"median_fg": 50.0,
|
||||
"avg_fg": 162.9,
|
||||
"min_fg": 7.0,
|
||||
"max_fg": 1200.0,
|
||||
"samples": 45
|
||||
},
|
||||
"Black": {
|
||||
"median_fg": 30.0,
|
||||
"avg_fg": 106.7,
|
||||
"min_fg": 0.0,
|
||||
"max_fg": 2400.0,
|
||||
"samples": 199
|
||||
},
|
||||
"Jah Rune": {
|
||||
"median_fg": 60.0,
|
||||
"avg_fg": 267.5,
|
||||
"min_fg": 0.0,
|
||||
"max_fg": 1200.0,
|
||||
"samples": 87
|
||||
},
|
||||
"Troll": {
|
||||
"median_fg": 30.0,
|
||||
"avg_fg": 32.1,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 75.0,
|
||||
"samples": 66
|
||||
},
|
||||
"Ward": {
|
||||
"median_fg": 10.0,
|
||||
"avg_fg": 21.8,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 50.0,
|
||||
"samples": 45
|
||||
},
|
||||
"White": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 37.6,
|
||||
"min_fg": 3.0,
|
||||
"max_fg": 300.0,
|
||||
"samples": 126
|
||||
},
|
||||
"Stealth RW": {
|
||||
"median_fg": 32.5,
|
||||
"avg_fg": 31.7,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 50.0,
|
||||
"samples": 24
|
||||
},
|
||||
"Doom": {
|
||||
"median_fg": 50.0,
|
||||
"avg_fg": 285.3,
|
||||
"min_fg": 45.0,
|
||||
"max_fg": 1500.0,
|
||||
"samples": 30
|
||||
},
|
||||
"Gul Rune": {
|
||||
"median_fg": 30.0,
|
||||
"avg_fg": 50.4,
|
||||
"min_fg": 0.0,
|
||||
"max_fg": 280.0,
|
||||
"samples": 136
|
||||
},
|
||||
"Death's Fathom": {
|
||||
"median_fg": 110.0,
|
||||
"avg_fg": 96.7,
|
||||
"min_fg": 60.0,
|
||||
"max_fg": 120.0,
|
||||
"samples": 12
|
||||
},
|
||||
"Unid Torch": {
|
||||
"median_fg": 60.0,
|
||||
"avg_fg": 49.5,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 120.0,
|
||||
"samples": 39
|
||||
},
|
||||
"Sanctuary": {
|
||||
"median_fg": 85.0,
|
||||
"avg_fg": 82.5,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 170.0,
|
||||
"samples": 24
|
||||
},
|
||||
"Grief": {
|
||||
"median_fg": 50.0,
|
||||
"avg_fg": 170.2,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 1500.0,
|
||||
"samples": 56
|
||||
},
|
||||
"Night": {
|
||||
"median_fg": 250.0,
|
||||
"avg_fg": 209.0,
|
||||
"min_fg": 20.0,
|
||||
"max_fg": 700.0,
|
||||
"samples": 15
|
||||
},
|
||||
"5/5 Facet": {
|
||||
"median_fg": 50.0,
|
||||
"avg_fg": 313.3,
|
||||
"min_fg": 10.0,
|
||||
"max_fg": 1300.0,
|
||||
"samples": 15
|
||||
},
|
||||
"Treachery": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 157.0,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 1300.0,
|
||||
"samples": 30
|
||||
},
|
||||
"Pala Torch": {
|
||||
"median_fg": 50.0,
|
||||
"avg_fg": 215.6,
|
||||
"min_fg": 20.0,
|
||||
"max_fg": 1500.0,
|
||||
"samples": 43
|
||||
},
|
||||
"Bulwark": {
|
||||
"median_fg": 50.0,
|
||||
"avg_fg": 440.0,
|
||||
"min_fg": 50.0,
|
||||
"max_fg": 1300.0,
|
||||
"samples": 10
|
||||
},
|
||||
"Fortitude": {
|
||||
"median_fg": 70.0,
|
||||
"avg_fg": 915.0,
|
||||
"min_fg": 20.0,
|
||||
"max_fg": 3500.0,
|
||||
"samples": 16
|
||||
},
|
||||
"Cham Rune": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 22.5,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 60.0,
|
||||
"samples": 53
|
||||
},
|
||||
"War Traveler": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 33.0,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 80.0,
|
||||
"samples": 33
|
||||
},
|
||||
"Oculus": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 29.3,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 90.0,
|
||||
"samples": 63
|
||||
},
|
||||
"Moser": {
|
||||
"median_fg": 12.5,
|
||||
"avg_fg": 12.5,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 20.0,
|
||||
"samples": 16
|
||||
},
|
||||
"Hellfire": {
|
||||
"median_fg": 60.0,
|
||||
"avg_fg": 53.3,
|
||||
"min_fg": 20.0,
|
||||
"max_fg": 80.0,
|
||||
"samples": 12
|
||||
},
|
||||
"Conviction": {
|
||||
"median_fg": 20.0,
|
||||
"avg_fg": 13.7,
|
||||
"min_fg": 1.0,
|
||||
"max_fg": 20.0,
|
||||
"samples": 6
|
||||
},
|
||||
"Terror Key": {
|
||||
"median_fg": 5.0,
|
||||
"avg_fg": 6.7,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 10.0,
|
||||
"samples": 12
|
||||
},
|
||||
"Hate Key": {
|
||||
"median_fg": 5.0,
|
||||
"avg_fg": 6.7,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 10.0,
|
||||
"samples": 12
|
||||
},
|
||||
"Dual Leech Ring": {
|
||||
"median_fg": 50.0,
|
||||
"avg_fg": 198.8,
|
||||
"min_fg": 20.0,
|
||||
"max_fg": 1500.0,
|
||||
"samples": 16
|
||||
},
|
||||
"Cyclone": {
|
||||
"median_fg": 300.0,
|
||||
"avg_fg": 306.7,
|
||||
"min_fg": 20.0,
|
||||
"max_fg": 600.0,
|
||||
"samples": 12
|
||||
},
|
||||
"Fury": {
|
||||
"median_fg": 30.0,
|
||||
"avg_fg": 76.9,
|
||||
"min_fg": 5.0,
|
||||
"max_fg": 250.0,
|
||||
"samples": 32
|
||||
}
|
||||
},
|
||||
"day_38": {},
|
||||
"day_39": {},
|
||||
"day_40": {},
|
||||
"day_41": {},
|
||||
"day_42": {},
|
||||
"day_43": {},
|
||||
"day_44": {},
|
||||
"day_45": {},
|
||||
"day_46": {},
|
||||
"day_47": {},
|
||||
"day_48": {},
|
||||
"day_49": {},
|
||||
"day_50": {},
|
||||
"day_51": {},
|
||||
"day_52": {},
|
||||
"day_53": {},
|
||||
"day_54": {},
|
||||
"day_55": {},
|
||||
"day_56": {},
|
||||
"day_57": {},
|
||||
"day_58": {},
|
||||
"day_59": {},
|
||||
"day_60": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"notes": "Estimated Day 1-14 ladder prices derived from Day 3 snapshot using decay multipliers. Use as planning guidance, not exact market truth.",
|
||||
"source_day": 3,
|
||||
"day_multipliers": {
|
||||
"day_1": 1.45,
|
||||
"day_2": 1.2,
|
||||
"day_3": 1.0,
|
||||
"day_4": 0.93,
|
||||
"day_5": 0.88,
|
||||
"day_6": 0.84,
|
||||
"day_7": 0.8,
|
||||
"day_8": 0.77,
|
||||
"day_9": 0.74,
|
||||
"day_10": 0.72,
|
||||
"day_11": 0.7,
|
||||
"day_12": 0.68,
|
||||
"day_13": 0.66,
|
||||
"day_14": 0.64
|
||||
},
|
||||
"base_day_3_fg": {
|
||||
"Cham Rune": 800,
|
||||
"Lo Rune": 900,
|
||||
"Ohm Rune": 600,
|
||||
"Vex Rune": 400,
|
||||
"Gul Rune": 200,
|
||||
"Ist Rune": 200,
|
||||
"Mal Rune": 120,
|
||||
"Um Rune": 90,
|
||||
"Pul Rune": 50,
|
||||
"Lem Rune": 70,
|
||||
"Unid Anni": 400,
|
||||
"Unid Torch": 1500,
|
||||
"Unid Griffon": 2000,
|
||||
"Unid Eth Andy": 2000,
|
||||
"Shako": 400,
|
||||
"Mara 30": 1200,
|
||||
"Mara Mid": 750,
|
||||
"BK 5": 600,
|
||||
"War Traveler": 500,
|
||||
"Death's Fathom": 800,
|
||||
"5/5 Facet": 500,
|
||||
"5@res SC": 300,
|
||||
"20life SC": 100,
|
||||
"7mf SC": 100,
|
||||
"Pcomb SK": 200,
|
||||
"Cold SK": 250,
|
||||
"Java SK": 200,
|
||||
"Light SK": 200
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ rejuv_potion=140,50,40,160,255,255
|
||||
skill_charges=70,30,25,150,163,255
|
||||
health_globe_red=178,110,20,183,255,255
|
||||
health_globe_green=47,90,20,54,255,255
|
||||
mana_globe=117,120,20,121,255,255
|
||||
mana_globe=110,50,15,125,255,255
|
||||
blue_slot=102,194,18,138,230,54
|
||||
green_slot=33,181,18,87,258,69
|
||||
red_slot=161,204,28,197,240,64
|
||||
@@ -51,8 +51,8 @@ potion1_y=695
|
||||
potion_width=30
|
||||
potion_height=30
|
||||
potion_next=41
|
||||
merc_health_top=14
|
||||
merc_health_left=15
|
||||
merc_health_top=675
|
||||
merc_health_left=305
|
||||
merc_health_width=40
|
||||
; skills
|
||||
skill_y=693
|
||||
@@ -70,8 +70,8 @@ wp_act_btn_width=67
|
||||
wp_first_btn_x=227
|
||||
wp_first_btn_y=129
|
||||
wp_btn_height=47
|
||||
; pickit
|
||||
item_dist=267
|
||||
; pickit **Note: 120 is apx pickup range. Set to this value or below to prevent walking on pickup
|
||||
item_dist=121
|
||||
; pather
|
||||
reached_node_dist=100
|
||||
min_walk_dist=25
|
||||
@@ -88,24 +88,26 @@ play_btn=426,616,320,71
|
||||
difficulty_select=536,236,210,320
|
||||
gold_btn=997,521,20,18
|
||||
inventory_gold=980,510,150,40
|
||||
gold_btn_stash=150,480,40,65
|
||||
gold_btn_stash=158,518,30,30
|
||||
vendor_gold_digits=186,509,97,16
|
||||
stash_gold_digits=185,507,99,15
|
||||
stash_gold_digits=184,527,100,16
|
||||
inventory_gold_digits=1017,523,100,16
|
||||
stash_page_digits=222,478,10,15
|
||||
health_globe=160,580,240,140
|
||||
mana_globe=887,580,240,140
|
||||
health_slice=309,610,7,101
|
||||
mana_slice=961,610,7,101
|
||||
cut_skill_bar=0,0,1280,653
|
||||
cut_skill_bar=0,0,1284,653
|
||||
reduce_to_center=120,60,1040,540
|
||||
search_npcs=120,0,1040,620
|
||||
merc_icon=0,0,100,100
|
||||
search_npcs=120,0,1042,620
|
||||
merc_icon=10,9,56,56
|
||||
loading_left_black=0,0,350,720
|
||||
death=444,198,397,71
|
||||
tp_search=353,120,547,400
|
||||
repair_btn=318,473,90,80
|
||||
left_inventory=35,92,378,378
|
||||
right_inventory=866,348,379,152
|
||||
left_inventory=33,84,382,382
|
||||
right_inventory=868,348,379,152
|
||||
transmute_third_slot=242,342,38,38
|
||||
skill_right=664,673,41,41
|
||||
skill_right_expanded=655,375,385,255
|
||||
skill_left=574,673,41,41
|
||||
@@ -118,21 +120,23 @@ stash_btn_roi=20,52,412,53
|
||||
enemy_info=440,15,405,150
|
||||
shrine_check=500,50,350,100
|
||||
character_select=1033, 44, 226, 554
|
||||
character_online_status=1033, 19, 226, 25
|
||||
character_online_status=1030, 18, 240, 40
|
||||
; character_sub_roi is with respect to matched active character template
|
||||
character_name_sub_roi=6, 22, 186, 18
|
||||
character_name_sub_roi=2, 20, 194, 22
|
||||
cube_area_roi=167,209,113,152
|
||||
cube_btn_roi=160,368,125,57
|
||||
xp_bar_text=369,630,554,34
|
||||
corpse=459,195,414,213
|
||||
chat_icon=7,555,43,41
|
||||
left_panel_header=0,0,450,54
|
||||
right_panel_header=830,0,450,54
|
||||
npc_dialogue=458,4,21,140
|
||||
left_panel_header=0,0,455,56
|
||||
right_panel_header=830,0,455,56
|
||||
npc_dialogue=456,0,30,150
|
||||
bind_skill=516,619,251,29
|
||||
quest_skill_btn=284,455,710,121
|
||||
left_inventory_tabs=31,62,385,28
|
||||
tab_indicator=31,82,385,14
|
||||
left_inventory_tabs=29,60,389,30
|
||||
tab_indicator=31,71,385,14
|
||||
stash_page_select_left=155,475,20,20
|
||||
stash_page_select_right=270,475,20,20
|
||||
deposit_btn=454,365,186,43
|
||||
equipped_inventory_area=861,59,387,287
|
||||
inventory_bg_pattern=21,622,1236,75
|
||||
@@ -141,7 +145,7 @@ inventory_bg_pattern=21,622,1236,75
|
||||
; static pathes in format: x0,y0, x1,y1, x2,y2, ...
|
||||
pindle_safe_dist=921,35, 1123,70, 960,88
|
||||
eldritch_safe_dist=652,63, 563,73
|
||||
trav_safe_dist=1156,438, 1120,282, 1059,525, 1146,312, 1132,346, 985,538, 1125,333
|
||||
trav_safe_dist=1156,438, 1120,282, 1059,525, 1146,312, 1132,346, 1100,538
|
||||
pindle_end=1037,174
|
||||
eldritch_end=675,210
|
||||
shenk_end=1067,544
|
||||
@@ -244,3 +248,19 @@ dia_c2g_home_loop=175,175
|
||||
dia_a_layout_bold=189,193, 189,193, 189,193, 189,193, 189,193, 189,193, 189,193, 189,193, 1100,337
|
||||
dia_b_layout_bold=1129,148, 1129,148, 1129,148, 1129,148, 1129,148, 1129,148
|
||||
dia_c_layout_bold=1112,503, 1112,503, 1112,503, 1112,503, 1112,503, 1112,503, 1112,503, 1112,503
|
||||
; Andariel (A1 Catacombs) - placeholder, fill in real coords later
|
||||
a1_andy_level3_enter=0,0
|
||||
a1_andy_level4_enter=0,0
|
||||
a1_andy_safe_dist=0,0
|
||||
; Countess (A1 Forgotten Tower) - placeholder, fill in real coords later
|
||||
a1_tower_level2_enter=0,0
|
||||
a1_tower_level3_enter=0,0
|
||||
a1_tower_level4_enter=0,0
|
||||
a1_tower_level5_enter=0,0
|
||||
a1_countess_safe_dist=0,0
|
||||
; Mephisto (A3 Durance of Hate) - placeholder, fill in real coords later
|
||||
a3_meph_level3_enter=0,0
|
||||
a3_meph_safe_dist=0,0
|
||||
; Baal (A5 Throne of Destruction) - placeholder, fill in real coords later
|
||||
a5_baal_throne_entry=0,0
|
||||
a5_baal_safe_dist=0,0
|
||||
|
||||
@@ -1,61 +1,253 @@
|
||||
; There is detailed documentation for each parameter in the README.md
|
||||
[general]
|
||||
; Personal values can reference env keys from repo-root .env:
|
||||
; Example syntax:
|
||||
; custom_message_hook=${BOTTY_CUSTOM_MESSAGE_HOOK}
|
||||
; custom_loot_message_hook=${ENV:BOTTY_CUSTOM_LOOT_MESSAGE_HOOK}
|
||||
; bnet_name=${BOTTY_BNET_NAME}
|
||||
; bnet_pass=${BOTTY_BNET_PASS}
|
||||
; char_name=${BOTTY_CHAR_NAME}
|
||||
; difficulty: game difficulty to create ("normal", "nightmare", "hell")
|
||||
;
|
||||
; Hammerdin difficulty guide (CTA build with Conviction aura):
|
||||
; NORMAL - Very easy. 1-2h Hammer kills most packs. No CTA needed.
|
||||
; NIGHTMARE - Recommended starting point. Bosses hit ~100-200 dmg.
|
||||
; CTA + Conviction drops resists. Need decent armor (ED/HR).
|
||||
; Gear targets: ~200 AR, ~15 ED, ~30% HR, ~50% FCR on hammer.
|
||||
; HELL - Bosses hit 400-800+ dmg per swing. Conviction is mandatory.
|
||||
; Gear targets: ~350+ AR, ~20+ ED, ~50%+ HR, ~60%+ FCR,
|
||||
; ~150% IAS on weapon. Full Rejuv belt recommended.
|
||||
; If you chicken/die repeatedly, drop to Nightmare first.
|
||||
difficulty=hell
|
||||
name=Botty
|
||||
; name: bot profile name used in logs/messages and mod launch option replacement
|
||||
name=profile1
|
||||
; randomize_runs: 0 = run in listed order, 1 = shuffle run order
|
||||
randomize_runs=0
|
||||
; target_tz: target Terror Zone id (leave as default unless you know the mapping)
|
||||
target_tz=1
|
||||
; saved_games_folder: optional override path to D2R Saved Games folder (blank = auto)
|
||||
saved_games_folder=
|
||||
; level_max_steps: max pathing steps for leveling-style routines
|
||||
level_max_steps=20
|
||||
|
||||
; Set to 1 to enable auto-login and auto-launch of D2R on startup.
|
||||
; Credentials below are ONLY used when auto_login=1.
|
||||
auto_login=0
|
||||
bnet_name=${BOTTY_BNET_NAME}
|
||||
bnet_pass=${BOTTY_BNET_PASS}
|
||||
|
||||
; Character name to auto-select from the character selection screen
|
||||
; If empty, bot relies on the saved character template from previous sessions
|
||||
char_name=${BOTTY_CHAR_NAME}
|
||||
|
||||
; messaging
|
||||
custom_loot_message_hook=
|
||||
custom_message_hook=
|
||||
; custom_loot_message_hook: optional separate webhook for loot notifications
|
||||
custom_loot_message_hook=${BOTTY_CUSTOM_LOOT_MESSAGE_HOOK}
|
||||
; custom_message_hook: main webhook for status/death/chicken messages
|
||||
custom_message_hook=${BOTTY_CUSTOM_MESSAGE_HOOK}
|
||||
; discord_log_chicken: 1 = send chicken/death style notifications
|
||||
discord_log_chicken=1
|
||||
; discord_log_errors: 1 = send a Discord message + error screenshot every time a
|
||||
; run fails (approach/battle/exception). Set to 0 to keep error screenshots on
|
||||
; disk only. Can also be toggled via [discord_events] error=0.
|
||||
discord_log_errors=1
|
||||
; discord_status_runs: send periodic status every X completed runs (blank/0 disables)
|
||||
discord_status_runs=10
|
||||
; discord_status_count: legacy fallback, send periodic status every X games (blank/0 disables)
|
||||
discord_status_count=20
|
||||
; pickup_drought_window: warn/alert after this many consecutive games with zero
|
||||
; item pickups. Raise this if you run a strict pickit and 0-pickup streaks are
|
||||
; expected/normal for you (a fast boss-only rush route with a tight filter can
|
||||
; easily go 10 games without a keep-worthy drop).
|
||||
pickup_drought_window=10
|
||||
; message_api_type: "" disables messaging, "discord" or "generic_api"
|
||||
message_api_type=discord
|
||||
|
||||
; breaks
|
||||
; break_length_m: scheduled break duration in minutes (0 = disabled)
|
||||
break_length_m=0
|
||||
; max_runtime_before_break_m: runtime before taking scheduled break (0 = disabled)
|
||||
max_runtime_before_break_m=0
|
||||
|
||||
; timers / fail handling
|
||||
; d2r_path: Diablo II: Resurrected install path
|
||||
d2r_path=C:\Program Files (x86)\Diablo II Resurrected
|
||||
; max_consecutive_fails: stop bot after this many failed runs in a row
|
||||
max_consecutive_fails=5
|
||||
max_game_length_s=380
|
||||
; if you set this field to 1, botty will attempt to restart d2 after a crash or failure
|
||||
restart_d2r_when_stuck=0
|
||||
; max_game_length_s: emergency timeout per run/game
|
||||
; 900: a full Chaos Sanctuary clear (3 seals + bosses + loot) takes ~10 min;
|
||||
; 600 force-quit a game while literally waiting for Diablo to spawn (2026-06-10).
|
||||
; Genuinely stuck games are caught much earlier by max_maintenance_time_s and
|
||||
; approach step timeouts.
|
||||
max_game_length_s=900
|
||||
; max_maintenance_time_s: if the town maintenance loop (heal/buy/stash/repair) takes
|
||||
; longer than this many seconds, save-and-exit and rejoin a fresh game.
|
||||
; Prevents the bot staying stuck in A5 town forever when NPCs or pathing fail.
|
||||
max_maintenance_time_s=240
|
||||
; auto_downgrade_threshold: if combined chickens+deaths exceed this number within 1 hour,
|
||||
; bot automatically lowers difficulty by one tier (hell->nightmare->normal) and restarts.
|
||||
; Set to 0 to disable. (NOTE: feature is parsed but not yet active in bot logic)
|
||||
auto_downgrade_threshold=0
|
||||
|
||||
restart_d2r_when_stuck=1
|
||||
; hardcore: 1 enables hardcore-safe assumptions in some routines
|
||||
hardcore=0
|
||||
|
||||
; screenshots
|
||||
; info_screenshots: save screenshots for info/chicken/death events
|
||||
info_screenshots=1
|
||||
; error_screenshots: save a screenshot to log/screenshots/error/ every time a run
|
||||
; fails (approach failure, battle failure, or an exception) so logs and visuals can
|
||||
; be reviewed side by side. Falls back to info_screenshots if unset.
|
||||
error_screenshots=1
|
||||
; loot_screenshots: save screenshots for picked loot
|
||||
loot_screenshots=0
|
||||
pickit_screenshots=0
|
||||
; pickit_screenshots: save screenshots for pickit debugging
|
||||
pickit_screenshots=1
|
||||
|
||||
; recovery
|
||||
; disable_run_after_failures: after this many CONSECUTIVE failures of the same run
|
||||
; (e.g. run_vizier), the bot disables just that run for the rest of the session and
|
||||
; keeps doing the other runs instead of stopping. A single success resets the count.
|
||||
; If every run gets disabled the bot stops for investigation. Set high to effectively
|
||||
; disable this behaviour.
|
||||
disable_run_after_failures=5
|
||||
; stash_scan_interval: scan all stash tabs and export stash_list.csv every X runs (0 disables)
|
||||
stash_scan_interval=0
|
||||
|
||||
[discord_events]
|
||||
; Fine-grained Discord event toggles (1=send, 0=disable)
|
||||
; status: periodic status + generic bot messages (breaks, shop notifications, etc.)
|
||||
status=1
|
||||
; item_keep: send kept item notifications (loot webhooks/embeds)
|
||||
item_keep=1
|
||||
; death: send death notifications
|
||||
death=1
|
||||
; chicken: send chicken (emergency leave) notifications
|
||||
chicken=1
|
||||
; stash_full: send stash full notifications
|
||||
stash_full=1
|
||||
; gold_full: send gold full notifications
|
||||
gold_full=1
|
||||
; error: send run-failure notifications (message + error screenshot)
|
||||
error=1
|
||||
|
||||
[stealth]
|
||||
; Multiplies all wait() calls by a random value in this range each call
|
||||
; 1.0 = no change. Set range wider for more human-like timing variation.
|
||||
wait_jitter_min = 0.85
|
||||
wait_jitter_max = 1.20
|
||||
|
||||
; Extra pixel variance added to every mouse click (on top of existing randomize=5)
|
||||
; 0 = off, 10 = +/-10px extra random offset per click
|
||||
click_variance = 8
|
||||
|
||||
; Re-shuffle run order after completing a full rotation (vs only at session start)
|
||||
; 0: keep [pindle, diablo] fixed so every game ENDS in A4 town (Diablo run TPs
|
||||
; there) — the next game then spawns at A4 where Jamella/Cain/Tyrael work,
|
||||
; avoiding the A5 Malah vendor entirely (stale templates in current patch).
|
||||
reshuffle_each_rotation = 0
|
||||
|
||||
; Probability (0-100) of randomly skipping a run each game
|
||||
; 0 = never skip, 20 = skip ~1 in 5 runs
|
||||
skip_run_chance = 10
|
||||
|
||||
; Probability (0-100) of taking an unscheduled AFK break after any given run
|
||||
afk_break_chance = 5
|
||||
; Break duration range in minutes
|
||||
afk_break_min_m = 2
|
||||
afk_break_max_m = 12
|
||||
|
||||
; Vary run/battle duration by a Gaussian factor (default +/-15%)
|
||||
; 0.0 = no variation, 0.3 = up to +/-30% variation
|
||||
run_duration_variance = 0.15
|
||||
|
||||
; Micro-pause between actions (simulates human hesitation in milliseconds)
|
||||
micro_pause_min_ms = 20
|
||||
micro_pause_max_ms = 120
|
||||
|
||||
; Vary kill time to avoid perfectly consistent boss fight durations
|
||||
vary_kill_time = 1
|
||||
|
||||
; Human mouse curve complexity (1.0 = default, 0.5 = more direct, 1.5 = more winding)
|
||||
human_curve_complexity = 1.0
|
||||
|
||||
; Arrival-to-click delay: human-like pause between mouse arriving and clicking (milliseconds)
|
||||
; 50-800ms range simulates "is this the right thing?" hesitation
|
||||
click_delay_min_ms = 50
|
||||
click_delay_max_ms = 800
|
||||
|
||||
; Key press duration variance: how long a key is held (milliseconds)
|
||||
; Most presses are short (20-100ms), some linger (up to 200ms)
|
||||
key_press_min_ms = 20
|
||||
key_press_max_ms = 200
|
||||
|
||||
; Skill rotation hesitation: pause before casting a skill (milliseconds)
|
||||
; Prevents machine-speed skill spam
|
||||
skill_hesitation_min_ms = 80
|
||||
skill_hesitation_max_ms = 300
|
||||
|
||||
; Wrong waypoint chance: 2-3% of selecting wrong TP portal then correcting
|
||||
; Humans occasionally misclick waypoint targets
|
||||
wrong_waypoint_chance = 0.025
|
||||
|
||||
; Skill mistake chance: 1-2% chance of miscasting and correcting
|
||||
; Simulates human error during combat
|
||||
skill_mistake_chance = 0.015
|
||||
|
||||
[routes]
|
||||
; Add these possible routes to "order" to run them:
|
||||
; run_trav
|
||||
; run_pindle
|
||||
; run_eldritch
|
||||
; run_eldritch_shenk
|
||||
; run_nihlathak
|
||||
; run_arcane
|
||||
; run_diablo
|
||||
order=run_pindle, run_eldritch_shenk
|
||||
; Controls which farm runs Botty performs each game.
|
||||
; "order" is a comma-delimited list and runs left-to-right when randomize_runs=0.
|
||||
; If randomize_runs=1 (in [general]), Botty shuffles enabled runs each game.
|
||||
;
|
||||
; Hammerdin keyrun recommendation (stable-first):
|
||||
; order=run_countess, run_arcane, run_nihlathak
|
||||
;
|
||||
; Route quick notes:
|
||||
; run_trav (Act 3 Travincal council farm; short/high-density run)
|
||||
; run_pindle (Act 5 Nihlathak temple red portal boss farm)
|
||||
; run_eldritch (Act 5 Frigid Highlands Eldritch-only run)
|
||||
; run_eldritch_shenk (Act 5 Eldritch then Shenk in same game)
|
||||
; run_nihlathak (Act 5 Halls of Vaught, teleport strongly recommended)
|
||||
; run_arcane (Act 2 Arcane Sanctuary / Summoner, teleport strongly recommended)
|
||||
; run_diablo (Act 4 Chaos Sanctuary, teleport recommended)
|
||||
; run_vizier (Act 4 Chaos Vizier-only route; lighter/faster than full Diablo run)
|
||||
; run_andariel (Act 1 Catacombs)
|
||||
; run_countess (Act 1 Forgotten Tower)
|
||||
; run_mephisto (Act 3 Durance of Hate)
|
||||
; run_baal (Act 5 Throne of Destruction)
|
||||
order=run_pindle, run_diablo
|
||||
|
||||
[char]
|
||||
; ==========================
|
||||
; ==== Mandatory Fields ====
|
||||
; ==========================
|
||||
; These configs have to be alligned with your d2r settings and char build
|
||||
type=light_sorc
|
||||
; type: character build profile to use (must match one section below)
|
||||
; examples: blizz_sorc, hammerdin, fohdin
|
||||
type=hammerdin
|
||||
; belt_rows: in-game belt size (2/3/4)
|
||||
belt_rows=4
|
||||
casting_frames=10
|
||||
cta_available=0
|
||||
; casting_frames: your char cast breakpoint (affects action timing)
|
||||
casting_frames=8
|
||||
; cta_casting_frames: cast breakpoint on CTA swap (if used)
|
||||
cta_casting_frames=8
|
||||
; attack_frames: base attack animation timing for non-cast attacks
|
||||
attack_frames=15
|
||||
; cta_available: 1 if Call to Arms swap exists, else 0
|
||||
cta_available=1
|
||||
;Do we want to cast non-cta buffs (ex energy shield) with cta
|
||||
buff_with_cta=1
|
||||
|
||||
; safer_routines: enable for optional defensive maneuvers/etc during combat/runs at the cost of increased runtime (ex. hardcore players)
|
||||
safer_routines=0
|
||||
safer_routines=1
|
||||
|
||||
; num_loot_columns: Number of empty columns from left to right of inventory to be used for looting.
|
||||
; Store charms, etc. to the right of the inventory.
|
||||
num_loot_columns=5
|
||||
num_loot_columns=4
|
||||
|
||||
; game hotkeys:
|
||||
; NOTE: each key must match your in-game binding exactly
|
||||
force_move=e
|
||||
inventory_screen=i
|
||||
potion1=1
|
||||
@@ -68,12 +260,13 @@ show_items=alt
|
||||
; stand_still cannot be the default "shift" as it would interfere with merc healing
|
||||
stand_still=capslock
|
||||
; teleport: leave empty if you can't use
|
||||
teleport=
|
||||
town_portal=f9
|
||||
teleport=b
|
||||
town_portal=6
|
||||
; call to arms settings:
|
||||
weapon_switch=x
|
||||
battle_orders=f7
|
||||
battle_command=f8
|
||||
; weapon_switch/battle_orders/battle_command only used when cta_available=1
|
||||
weapon_switch=w
|
||||
battle_orders=7
|
||||
battle_command=8
|
||||
|
||||
; ==========================
|
||||
; ==== Optional configs ====
|
||||
@@ -82,23 +275,61 @@ stash_gold=1
|
||||
use_merc=1
|
||||
|
||||
; Attack length for barbarians should be as high as 8-10 and even 10-12 for trav/shenk
|
||||
;
|
||||
; Hammerdin attack lengths (seconds of hammer spam per boss):
|
||||
; atk_len_trav = 4.0 (Council of 3 - 3 council members, easy)
|
||||
; atk_len_pindle = 8.0 (Pindle — Hell: 13094-16070 HP, 75% fire, 100% poison)
|
||||
; Pindle stats by difficulty:
|
||||
; Normal: 1064-1588 HP, 75% fire, 70% poison
|
||||
; Nightmare: 4773-5859 HP, 100% poison
|
||||
; Hell: 13094-16070 HP, 75% fire, 50% cold, 33% light, 100% poison
|
||||
; With Conviction (-33% res), Hell Pindle = 50% fire, 17% cold, 0% light, 67% poison
|
||||
; CTA adds +100% dmg to hammer. Need ~60%+ FCR and good IAS to kill in 8s.
|
||||
; atk_len_nihlathak = 4.0 (Nihlathak - single boss)
|
||||
; atk_len_eldritch = 3.0 (Eldritch only - single boss)
|
||||
; atk_len_shenk = 4.0 (Shenk only - single boss)
|
||||
; atk_len_arc = 2.5 (Summoner in Arcane - single boss)
|
||||
; atk_len_diablo = 10.0 (Diablo in CS - longest single boss)
|
||||
; atk_len_countess = 3.0 (Countess - single boss, easy)
|
||||
; atk_len_andariel = 4.0 (Andariel - single boss)
|
||||
; atk_len_mephisto = 12.0 (Mephisto - single boss, high HP)
|
||||
; atk_len_baal = 10.0 (Baal - single boss)
|
||||
; atk_len_baal_waves = 30.0 (Baal's spawn waves before boss)
|
||||
;
|
||||
; Chaos Sanctuary (Diablo run) individual fights:
|
||||
; atk_len_cs_trashmobs = 2.0 (Trash packs in CS)
|
||||
; atk_len_diablo_vizier = 2.0 (Vizier of Chaos - seal boss A)
|
||||
; atk_len_diablo_infector = 4.0 (Infector of Souls - seal boss C)
|
||||
; atk_len_diablo_deseis = 5.0 (Lord De Seis - seal boss B, hardest seal)
|
||||
;
|
||||
; Increase these if your hammer kills slower (low IAS/FCR).
|
||||
; Decrease if your hammer kills faster (high IAS/FCR, good gear).
|
||||
; If you die during a fight, the attack length is too long for your defense.
|
||||
atk_len_arc=2.5
|
||||
atk_len_eldritch=3.0
|
||||
atk_len_nihlathak=4.0
|
||||
atk_len_pindle=3.0
|
||||
atk_len_pindle=8.0
|
||||
atk_len_shenk=4.0
|
||||
atk_len_trav=3.0
|
||||
atk_len_diablo=3.0
|
||||
atk_len_trav=4.0
|
||||
|
||||
; Boss run attack lengths (per-character defaults in kill_* methods)
|
||||
; Adjust these if your build is faster/slower against these bosses
|
||||
atk_len_andariel=4.0
|
||||
atk_len_countess=3.0
|
||||
atk_len_mephisto=12.0
|
||||
atk_len_baal=10.0
|
||||
atk_len_baal_waves=30.0
|
||||
|
||||
; Chaos Sanctuary settings
|
||||
atk_len_cs_trashmobs=1.5
|
||||
atk_len_cs_trashmobs=2.0
|
||||
atk_len_diablo_deseis=5.0
|
||||
atk_len_diablo_infector=4.0
|
||||
atk_len_diablo_vizier=2.0
|
||||
atk_len_diablo=3.0
|
||||
cs_mob_detect=1
|
||||
cs_mob_detect=0
|
||||
; cs_town_visits is currently broken, ignore for now
|
||||
cs_town_visits=0
|
||||
kill_cs_trash=0
|
||||
kill_cs_trash=1
|
||||
|
||||
; Belt settings
|
||||
belt_hp_columns=1
|
||||
@@ -106,45 +337,74 @@ belt_mp_columns=1
|
||||
belt_rejuv_columns=2
|
||||
|
||||
; Potion/chicken settings
|
||||
take_health_potion=0.8
|
||||
take_mana_potion=0.5
|
||||
take_rejuv_potion_health=0.4
|
||||
take_rejuv_potion_mana=0.1
|
||||
heal_merc=0.7
|
||||
heal_rejuv_merc=0.2
|
||||
chicken=0.35
|
||||
merc_chicken=0
|
||||
take_health_potion=0.60
|
||||
take_mana_potion=0.40
|
||||
take_rejuv_potion_health=0.45
|
||||
take_rejuv_potion_mana=0.10
|
||||
heal_merc=0.70
|
||||
heal_rejuv_merc=0.45
|
||||
chicken=0.40
|
||||
merc_chicken=0.20
|
||||
|
||||
; Misc.
|
||||
; helps reduce accidental pickups when enabled especially on walking characters
|
||||
enable_no_pickup=1
|
||||
enable_no_pickup=0
|
||||
; fill_shared_stash_first: 1 = prefer shared stash tabs before personal stash
|
||||
fill_shared_stash_first=0
|
||||
; to gamble, add any/all of the following: circlet, ring, coronet, talon, amulet
|
||||
gamble_items=
|
||||
id_items=1
|
||||
; open_chests: 1 = open clickable chests along path when possible
|
||||
open_chests=1
|
||||
; pre_buff_every_run: 1 = always recast buffs at run start
|
||||
pre_buff_every_run=1
|
||||
runs_per_repair=0
|
||||
; runs_per_repair: visit repair vendor every X runs (blank/0 disables)
|
||||
runs_per_repair=5
|
||||
; repair_npc: preferred repair vendor strategy.
|
||||
; - a5_larzuk (recommended: stays in A5, no cross-act trip; falls back to Halbu)
|
||||
; - a4_halbu (requires WP trip to A4 every repair — act desync risk if it fails)
|
||||
; 2026-06-10: switched to a5_larzuk — session logs showed Halbu detection failing
|
||||
; 100% (body score ~0.39) and each failed A4 trip desynced the bot's act state.
|
||||
repair_npc=a5_larzuk
|
||||
; runs_per_stash: stash/sell every X runs (blank/0 disables)
|
||||
runs_per_stash=4
|
||||
sell_junk=0
|
||||
; sell_junk: 1 = vendor non-keep items automatically
|
||||
sell_junk=1
|
||||
; protect_shields_from_sell: 1 = never vendor items with "shield" in detected name (recommended safety)
|
||||
protect_shields_from_sell=1
|
||||
; protect_charms_from_sell: 1 = never drop/vendor charms regardless of pickit verdict
|
||||
; (recommended safety — a misread charm can't be un-dropped). Set to 0 to let your
|
||||
; pickit rules decide keep/discard for charms same as any other item.
|
||||
protect_charms_from_sell=1
|
||||
; pick_rares_for_gold: 1 = pick all yellow (rare) ground items; non-keep rares will be sold
|
||||
; Requires sell_junk=1 to convert extra pickups into gold.
|
||||
pick_rares_for_gold=0
|
||||
; pick_gold: 1 = pick up ground gold piles, 0 = ignore all ground gold
|
||||
pick_gold=1
|
||||
|
||||
[transmute]
|
||||
;stash tabs by priority where to put transmuted gems
|
||||
stash_destination=3,2,1,0
|
||||
stash_destination=0,1,2,3
|
||||
; Add these possible gems to "transmute" to transmute them:
|
||||
; chipped, flawed, standard, flawless
|
||||
transmute=flawless
|
||||
;how often we want to run transmute routine(e.g. every 100 games)
|
||||
transmute_every_x_game=20
|
||||
transmute_every_x_game=60
|
||||
; number of stash tabs (ROTW has more than 6, adjust as needed)
|
||||
stash_tabs=6
|
||||
; potion transmute settings
|
||||
; convert_rejuv: 1 = convert regular Rejuv Potions to Full Rejuv via cube (3 -> 1)
|
||||
convert_rejuv=1
|
||||
; min_rejuv_to_convert: minimum regular rejuv potions in inventory before starting conversion
|
||||
min_rejuv_to_convert=6
|
||||
|
||||
; ===========================
|
||||
; ==== Builds: Sorceress ====
|
||||
; ===========================
|
||||
[sorceress]
|
||||
energy_shield=
|
||||
frozen_armor=
|
||||
static_field=
|
||||
telekinesis=
|
||||
energy_shield=f4
|
||||
frozen_armor=f7
|
||||
static_field=f5
|
||||
telekinesis=f6
|
||||
thunder_storm=
|
||||
|
||||
[light_sorc]
|
||||
@@ -157,55 +417,97 @@ frozen_orb=
|
||||
|
||||
[blizz_sorc]
|
||||
; blizzard must be right skill, hotkey required
|
||||
blizzard=
|
||||
blizzard=f1
|
||||
; ice_blast must be left skill (hotkey optional as it shouldnt change)
|
||||
ice_blast=
|
||||
ice_blast=f2
|
||||
|
||||
[blizzorb_sorc]
|
||||
; frozen orb must be left skill and preselected (no hotkey required)
|
||||
|
||||
;All others must be right skill and hotkey required!
|
||||
blizzard=f1
|
||||
glacial_spike=f7
|
||||
|
||||
[nova_sorc]
|
||||
; nova must be right skill, hotkey required
|
||||
nova=
|
||||
nova=f1
|
||||
|
||||
[hydra_sorc]
|
||||
; only supports Pindle and Eldritch currently
|
||||
; alt_attack is any alternate attacking right skill. Fireball,Lightning,Frozen Orb, hotkey required
|
||||
alt_attack=
|
||||
alt_attack=f7
|
||||
; hydra must be right skill, hotkey required
|
||||
hydra=
|
||||
hydra=f1
|
||||
|
||||
; =========================
|
||||
; ==== Builds: Paladin ====
|
||||
; =========================
|
||||
[paladin]
|
||||
cleansing=
|
||||
holy_shield=
|
||||
redemption=
|
||||
vigor=
|
||||
cleansing=f9
|
||||
holy_shield=f2
|
||||
redemption=f3
|
||||
vigor=f4
|
||||
|
||||
[fohdin]
|
||||
; foh must be left skill, hotkey required
|
||||
blessed_hammer=
|
||||
concentration=
|
||||
conviction=
|
||||
foh=
|
||||
holy_bolt=
|
||||
blessed_hammer=f1
|
||||
concentration=f8
|
||||
conviction=f5
|
||||
foh=f6
|
||||
holy_bolt=f7
|
||||
|
||||
[hammerdin]
|
||||
blessed_hammer=
|
||||
concentration=
|
||||
blessed_hammer=f1
|
||||
concentration=f8
|
||||
conviction=f5
|
||||
|
||||
; =========================
|
||||
; ==== Builds: Warlock ====
|
||||
; =========================
|
||||
[warlock]
|
||||
; All skills should be on right-click unless otherwise specified
|
||||
deathmark=f3
|
||||
lethargy=f4
|
||||
;Set summon_demon to one of the three demons of your choice.
|
||||
summon_demon=f5
|
||||
;If you are summoning a 2nd demon set hotkey here. Can be same as above
|
||||
summon_demon2=
|
||||
psychic_ward=
|
||||
|
||||
[fire_lock]
|
||||
; Supported runs: Pindle
|
||||
;flame_wave must be assigned to left-click and doesn't require hotkey
|
||||
ring_of_fire=f1
|
||||
apocalypse=f2
|
||||
|
||||
[abyss_lock]
|
||||
; Supported runs: Pindle
|
||||
;miasma_bolt must be assigned to left-click and doesn't require hotkey
|
||||
miasma_chain=f1
|
||||
abyss=f2
|
||||
|
||||
[echo_lock]
|
||||
; Supported runs: Trav (tele only)
|
||||
echo_strike=f1
|
||||
eldritch_blast=f2
|
||||
hex_bane=f3
|
||||
;Set ring of fire if you want to stun before echo blast
|
||||
ring_of_fire=
|
||||
|
||||
; ==========================
|
||||
; ==== Builds: Assassin ====
|
||||
; ==========================
|
||||
; Currently no Trav implementaiton!
|
||||
[trapsin]
|
||||
burst_of_speed=
|
||||
death_sentry=
|
||||
fade=
|
||||
lightning_sentry=
|
||||
shadow_warrior=
|
||||
; We assume fire blast or shock web is left-click skill which is optional hotkey if pre-assigned.
|
||||
skill_left=
|
||||
|
||||
; Assume all other skills are right-click and hotkey required!
|
||||
burst_of_speed=f1
|
||||
death_sentry=f2
|
||||
fade=
|
||||
lightning_sentry=f3
|
||||
shadow_warrior=f4
|
||||
mind_blast=f5
|
||||
|
||||
; ===========================
|
||||
; ==== Builds: Barbarian ====
|
||||
@@ -220,6 +522,21 @@ leap=
|
||||
shout=
|
||||
war_cry=
|
||||
|
||||
; ===========================
|
||||
; ==== Builds: Amazon ====
|
||||
; ===========================
|
||||
; Supported runs: Trav, Pindle, Eld, Shenk, Nihlathak (tele only)
|
||||
; Make sure leap hotkey is set if you do not have Enigma
|
||||
; Ensure Battle Order/Command hotkeys are set above in the [char] section
|
||||
[amazon]
|
||||
;all must be right-click skill
|
||||
valkyrie=f1
|
||||
|
||||
[javazon]
|
||||
; charged strike must be left-click skill and preselected, no hotkey required
|
||||
|
||||
;all others must be right-click
|
||||
lightning_fury=f2
|
||||
|
||||
; ==========================
|
||||
; ==== Builds: Necro ====
|
||||
@@ -277,7 +594,7 @@ damage_scaling=1
|
||||
; if the buffs are not set they are not used (they are part of the prebuff setup)
|
||||
|
||||
[basic]
|
||||
left_attack=
|
||||
left_attack=1
|
||||
right_attack=
|
||||
buff_1=
|
||||
buff_2=
|
||||
@@ -297,25 +614,78 @@ buff_2=
|
||||
|
||||
[advanced_options]
|
||||
; startup hotkeys
|
||||
restore_settings_from_backup_key=f7
|
||||
settings_backup_key=f8
|
||||
auto_settings_key=f9
|
||||
graphic_debugger_key=f10
|
||||
; select_runs_key: open run selector UI
|
||||
select_runs_key=pagedown
|
||||
; restore_settings_from_backup_key: restore backed-up D2R settings
|
||||
restore_settings_from_backup_key=insert
|
||||
; settings_backup_key: create backup of current D2R settings
|
||||
settings_backup_key=pause
|
||||
; auto_settings_key: auto-apply required D2R settings
|
||||
auto_settings_key=pageup
|
||||
; graphic_debugger_key: toggle on-screen debug layers
|
||||
graphic_debugger_key=delete
|
||||
; resume_key: start/pause bot loop
|
||||
resume_key=f11
|
||||
; exit_key: hard stop bot
|
||||
exit_key=f12
|
||||
; cycle_pickit_profile_key: cycle through pickit profiles in config/pickit_profiles/
|
||||
cycle_pickit_profile_key=f10
|
||||
|
||||
; etc.
|
||||
; graphic_debugger_layer_creator: 1 = enable interactive layer creator tooling
|
||||
graphic_debugger_layer_creator=0
|
||||
; hwnd_window_process: process regex used to locate D2R window handle
|
||||
hwnd_window_process=D2R\.exe
|
||||
; hwnd_window_title: optional title regex override for window lookup
|
||||
hwnd_window_title=
|
||||
; launch_options: will replace <name> with setting for [general] "name" above
|
||||
launch_options=-mod <name> -txt
|
||||
; logg_lvl: logging verbosity (debug/info/warning/error)
|
||||
logg_lvl=debug
|
||||
; message_body_template: payload template for generic_api mode
|
||||
message_body_template={{"content": "{msg}"}}
|
||||
; message_headers: optional JSON headers for generic_api mode
|
||||
message_headers=
|
||||
; ocr_during_pickit: 1 = run OCR while looting (slower, more diagnostics)
|
||||
ocr_during_pickit=0
|
||||
;use "can_teleport_natively" or "can_teleport_with_charges" if you want to force certain behavior in case autodetection isn't working properly
|
||||
override_capabilities=
|
||||
pathing_delay_factor=4
|
||||
;If you want to control Hyper-V window from host use 0,51 here
|
||||
override_capabilities=can_teleport_natively
|
||||
; pathing_delay_factor: movement/click delay multiplier (1 fast .. 10 slow)
|
||||
pathing_delay_factor=2
|
||||
; If you want to control Hyper-V window from host use 0,51 here
|
||||
; window_client_area_offset: x,y pixel offset for captured game client area
|
||||
window_client_area_offset=0,0
|
||||
|
||||
[log_rotation]
|
||||
; Log rotation prevents screenshot directories from filling the disk.
|
||||
; Each managed directory has a max file count and max total size.
|
||||
; When limits are exceeded, oldest files are deleted automatically.
|
||||
; Checked every 60 seconds (to avoid I/O overhead during runs).
|
||||
;
|
||||
; The text log (log/log.txt) is size-capped separately: it rotates at 50 MB,
|
||||
; keeps 5 zipped backups in log/archive/ (max 30 archives). Override the per-file
|
||||
; cap with the BOTTY_LOG_MAX_MB environment variable (e.g. set 100 for 100 MB).
|
||||
|
||||
; pickit directory (log/screenshots/pickit/):
|
||||
; Can grow very fast — every item scan writes a PNG + JSON.
|
||||
; pickit_max_files=500 ; max files before rotation kicks in
|
||||
; pickit_max_mb=500 ; max total size in MB
|
||||
pickit_max_files=300
|
||||
pickit_max_mb=200
|
||||
|
||||
; info directory (log/screenshots/info/):
|
||||
; Debug screenshots for deaths, chickens, errors, etc.
|
||||
; info_max_files=200 ; max files before rotation kicks in
|
||||
; info_max_mb=500 ; max total size in MB
|
||||
info_max_files=100
|
||||
info_max_mb=200
|
||||
|
||||
; items directory (log/screenshots/items/):
|
||||
; Loot screenshots sent to Discord.
|
||||
; items_max_files=100
|
||||
; items_max_mb=100
|
||||
items_max_files=50
|
||||
items_max_mb=50
|
||||
|
||||
; discord_notify_rotation: 1 = send Discord message when log rotation deletes files
|
||||
discord_notify_rotation=0
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Shared pickit profiles (git-tracked)
|
||||
|
||||
One folder per pickit set, e.g. for season phases:
|
||||
|
||||
```
|
||||
config/pickit_profiles/
|
||||
season_start/ *.bnip (leveling: keep bases, gems, chipped...)
|
||||
mid_season/ *.bnip
|
||||
endgame/ *.bnip (GG-only filter)
|
||||
```
|
||||
|
||||
Drop `.bnip` (or `.nip`) files in a folder; a `.nipignore` works like in
|
||||
`config/bnip`. These folders ARE committed — build them once, everyone
|
||||
gets them via git pull.
|
||||
|
||||
## Selecting a set
|
||||
Per user, in your gitignored `config/profiles/<you>/profile.ini`:
|
||||
|
||||
```ini
|
||||
[general]
|
||||
pickit_profile=season_start
|
||||
```
|
||||
|
||||
Priority: `config/profiles/<you>/pickit/` (personal set, gitignored)
|
||||
> `config/pickit_profiles/<pickit_profile>/` (shared, from this folder)
|
||||
> `config/bnip/` > `config/default.bnip`.
|
||||
@@ -1,4 +1,5 @@
|
||||
[claws]
|
||||
; shop_trap_claws: 1 = enable claw-shopping routine for trap claws from Anya/Drognan flow
|
||||
; Current scoring for trap claws
|
||||
; 3 traps: +12
|
||||
; 2/1 traps: +8
|
||||
@@ -6,18 +7,23 @@
|
||||
; x light senetry: +6
|
||||
; x weapon block: +1
|
||||
; x death sentrey: +4
|
||||
shop_trap_claws=1
|
||||
shop_trap_claws=0
|
||||
; trap_min_score: minimum combined score required to keep a trap claw
|
||||
trap_min_score=13
|
||||
|
||||
; shop_melee_claws: 1 = enable shopping melee claws (venom/block focused)
|
||||
; Current scoring for melee claws
|
||||
; 2 assa: +10
|
||||
; x venom: +6
|
||||
; x weapon block: +2
|
||||
shop_melee_claws=1
|
||||
shop_melee_claws=0
|
||||
; melee_min_score: minimum combined score required to keep a melee claw
|
||||
melee_min_score=13
|
||||
|
||||
[gloves]
|
||||
; shop_3_skills_ias_gloves: 1 = shop +3 skill tree + IAS gloves
|
||||
shop_3_skills_ias_gloves=1
|
||||
; shop_2_skills_ias_gloves: 1 = shop +2 skill tree + IAS gloves
|
||||
shop_2_skills_ias_gloves=0
|
||||
|
||||
;
|
||||
@@ -29,6 +35,9 @@ shop_2_skills_ias_gloves=0
|
||||
; apply_pather_adjustment - alternative option that applies an adjustment to movements.
|
||||
; Should not need this. Try it if you have trouble when it is off.
|
||||
[scepters]
|
||||
; shop_hammerdin_scepters: 1 = enable Hammerdin scepter shopping route
|
||||
shop_hammerdin_scepters=1
|
||||
; speed_factor: movement compensation for FRW and path timing during shopping loop
|
||||
speed_factor=0.25
|
||||
apply_pather_adjustment=0
|
||||
; apply_pather_adjustment: optional alternate node adjustment if default route misses NPC/shop spots
|
||||
apply_pather_adjustment=0
|
||||
|
||||
|
After Width: | Height: | Size: 3.3 MiB |