Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2a2d1af8c | ||
|
|
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 | ||
|
|
ea77ac5fe7 | ||
|
|
1df9e0a28f | ||
|
|
54316a1eac | ||
|
|
76b911f50f | ||
|
|
5b9c56a2c6 | ||
|
|
2b2de1baf3 | ||
|
|
0e800cc3e2 | ||
|
|
42639c8273 | ||
|
|
9a5ee8cd5e | ||
|
|
e6b65a6a10 | ||
|
|
5597df55ca | ||
|
|
dc75cce5dc | ||
|
|
4a0396fe81 | ||
|
|
0f34b4951c | ||
|
|
d7f776ad37 | ||
|
|
723a75e672 | ||
|
|
541810f2e3 | ||
|
|
e9b5cf6dc5 | ||
|
|
4b2e1f7059 | ||
|
|
6d43868b4d | ||
|
|
1c07b74f79 | ||
|
|
4deed0867a | ||
|
|
99579bfda4 | ||
|
|
cb1f443d23 | ||
|
|
e890be4b38 | ||
|
|
2d3deb74ae | ||
|
|
0acd6d8cc9 | ||
|
|
d77ad0dc7c | ||
|
|
8bc909eb72 | ||
|
|
329f4fcb5c | ||
|
|
5fd89b5b6b | ||
|
|
f9780c5942 | ||
|
|
d895dd0b40 | ||
|
|
a786ba4f08 | ||
|
|
9d4c8681ca |
@@ -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,167 @@
|
||||
name: Botty - CI
|
||||
on: [pull_request]
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main, mine]
|
||||
# Pushing a version tag (e.g. `git tag v0.8.5 && git push --tags`) builds,
|
||||
# smoke-tests, then creates the GitHub release with the zip attached — all
|
||||
# in one run. If the build fails, no release is ever created.
|
||||
tags: ['v*']
|
||||
|
||||
# Default GITHUB_TOKEN is read-only; the build job's release step needs
|
||||
# contents:write to create the release and attach the built zip
|
||||
# (else HTTP 403 "Resource not accessible by integration").
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
test:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Miniconda Python 3.10
|
||||
uses: actions/setup-python@v2
|
||||
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
|
||||
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
|
||||
env:
|
||||
# Increase this value to reset cache if environment.yml has not changed
|
||||
CACHE_NUMBER: 1
|
||||
with:
|
||||
path: ~/pip
|
||||
key: pip-${{ env.CACHE_NUMBER }}-${{ hashFiles('environment.yml') }}
|
||||
- name: Install Miniconda
|
||||
uses: conda-incubator/setup-miniconda@v2
|
||||
uses: conda-incubator/setup-miniconda@v3
|
||||
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
|
||||
environment-file: environment-win11.yml
|
||||
use-only-tar-bz2: false
|
||||
|
||||
- name: Python version
|
||||
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
|
||||
|
||||
- name: Syntax check
|
||||
shell: powershell
|
||||
env:
|
||||
PYTHONPATH: ./src
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
C:\Miniconda\condabin\conda.bat activate botty
|
||||
python -m compileall -q src tools test scripts
|
||||
|
||||
- name: Tests
|
||||
shell: powershell
|
||||
env:
|
||||
PYTHONPATH: ./src
|
||||
RUN_ENV: test
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
C:\Miniconda\condabin\conda.bat activate botty
|
||||
python -m coverage run -m pytest -v
|
||||
|
||||
- name: Coverage report
|
||||
shell: powershell
|
||||
env:
|
||||
PYTHONPATH: ./src
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
C:\Miniconda\condabin\conda.bat activate botty
|
||||
python -m coverage xml --ignore-errors
|
||||
|
||||
build:
|
||||
needs: test
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Miniconda Python 3.10
|
||||
uses: conda-incubator/setup-miniconda@v3
|
||||
with:
|
||||
python-version: '3.10'
|
||||
activate-environment: botty
|
||||
channel-priority: strict
|
||||
environment-file: environment-win11.yml
|
||||
use-only-tar-bz2: false
|
||||
|
||||
- name: Install Tesseract (bundled into the release for click-and-run OCR)
|
||||
shell: powershell
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
choco install tesseract --no-progress -y
|
||||
if (-not (Test-Path "C:\Program Files\Tesseract-OCR\tesseract.exe")) {
|
||||
throw "Tesseract install did not produce tesseract.exe"
|
||||
}
|
||||
|
||||
- name: Build exe
|
||||
shell: powershell
|
||||
env:
|
||||
BOTTY_NO_RENAME: '1'
|
||||
PYTHONPATH: ./src
|
||||
run: |
|
||||
C:\Miniconda\condabin\conda.bat activate botty
|
||||
coverage xml
|
||||
python build.py --conda_path C:\Miniconda
|
||||
|
||||
- name: Verify Tesseract was bundled
|
||||
shell: powershell
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$BOTTY_DIR = Get-ChildItem -Directory -Name | Where-Object { $_ -match '^botty_v' } | Select-Object -First 1
|
||||
$tess = Join-Path $BOTTY_DIR "tesseract\tesseract.exe"
|
||||
if (-not (Test-Path $tess)) { throw "Tesseract was not bundled into $BOTTY_DIR" }
|
||||
Write-Host "Bundled: $tess"
|
||||
|
||||
- name: Launch smoke test (built executables)
|
||||
shell: powershell
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$BOTTY_DIR = Get-ChildItem -Directory -Name | Where-Object { $_ -match '^botty_v' } | Select-Object -First 1
|
||||
if (-not $BOTTY_DIR) { throw "No botty_v* build directory found." }
|
||||
|
||||
$mainExe = Join-Path $BOTTY_DIR "main.exe"
|
||||
$shopperExe = Join-Path $BOTTY_DIR "shopper.exe"
|
||||
if (-not (Test-Path $mainExe)) { throw "Missing $mainExe" }
|
||||
if (-not (Test-Path $shopperExe)) { throw "Missing $shopperExe" }
|
||||
|
||||
$procs = @()
|
||||
try {
|
||||
$mainProc = Start-Process -FilePath $mainExe -PassThru -WindowStyle Hidden
|
||||
Start-Sleep -Seconds 6
|
||||
if ($mainProc.HasExited) { throw "main.exe exited early with code $($mainProc.ExitCode)" }
|
||||
$procs += $mainProc
|
||||
|
||||
$shopperProc = Start-Process -FilePath $shopperExe -PassThru -WindowStyle Hidden
|
||||
Start-Sleep -Seconds 6
|
||||
if ($shopperProc.HasExited) { throw "shopper.exe exited early with code $($shopperProc.ExitCode)" }
|
||||
$procs += $shopperProc
|
||||
}
|
||||
finally {
|
||||
foreach ($p in $procs) {
|
||||
if ($p -and -not $p.HasExited) {
|
||||
Stop-Process -Id $p.Id -Force
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- name: Prepare release zip
|
||||
shell: powershell
|
||||
run: |
|
||||
$BOTTY_DIR = Get-ChildItem -Directory -Name | Where-Object { $_ -match '^botty_v' } | Select-Object -First 1
|
||||
Write-Host "Botty dir: $BOTTY_DIR"
|
||||
Get-ChildItem -Path $BOTTY_DIR -Recurse | Select-Object -Property FullName, Length
|
||||
$ZIP = "${BOTTY_DIR}.zip"
|
||||
Compress-Archive -Path "${BOTTY_DIR}\*" -DestinationPath $ZIP -Force
|
||||
Write-Host "Release zip: $ZIP"
|
||||
Get-Item $ZIP | Select-Object -Property FullName, Length
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: botty-build
|
||||
path: botty_v*/
|
||||
retention-days: 7
|
||||
|
||||
# On a version-tag push, create the release (if absent) and attach the
|
||||
# zip atomically. Runs only after the build + smoke test above succeed.
|
||||
- name: Create release and upload zip
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: botty_v*.zip
|
||||
fail_on_unmatched_files: true
|
||||
generate_release_notes: true
|
||||
|
||||
@@ -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,29 @@ 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/
|
||||
|
||||
@@ -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,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).
|
||||
@@ -51,6 +94,8 @@ Poison damage is no longer calculated, but is now read as it's raw value. Exampl
|
||||
|
||||
`[idname]` can now be used for unique / set items. For example, `[idname] == thestoneofjordan` will pick up SoJ. Keep in mind, however, this forces the item to be ID'd so be careful if you want to keep unid items.
|
||||
|
||||
`[maxquantity]` is not supported (yet). You can leave those tags in but they'll do nothing.
|
||||
|
||||
If you have the discord webhook hooked up to alert you when the bot keeps items, you can suppress these alerts by adding a "@" in front of your expression.
|
||||
|
||||
@[type] == ring = no notification
|
||||
@@ -97,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 |
|
||||
| ------------------ | -------------------------------------------------------------------------------------------------|
|
||||
@@ -151,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. |
|
||||
@@ -164,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
|
||||
@@ -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,
|
||||
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 9.3 KiB |
@@ -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
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from src.version import __version__
|
||||
import argparse
|
||||
import getpass
|
||||
import random
|
||||
from cryptography.fernet import Fernet
|
||||
import string
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(description="Build Botty")
|
||||
parser.add_argument(
|
||||
"-v" , "--version",
|
||||
type=str,
|
||||
help="New release version e.g. 0.4.2",
|
||||
default=""
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c", "--conda_path",
|
||||
type=str,
|
||||
help="Path to local conda e.g. C:\\Users\\USER\\miniconda3",
|
||||
default=f"C:\\Users\\{getpass.getuser()}\\miniconda3")
|
||||
parser.add_argument(
|
||||
"-r", "--random_name",
|
||||
action='store_true',
|
||||
help="Will generate a random name for the botty exe")
|
||||
parser.add_argument(
|
||||
"-k", "--use_key",
|
||||
action='store_true',
|
||||
help="Will build with encryption key")
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
# clean up
|
||||
def clean_up():
|
||||
# pyinstaller
|
||||
if os.path.exists("build"):
|
||||
shutil.rmtree("build")
|
||||
if os.path.exists("main.spec"):
|
||||
os.remove("main.spec")
|
||||
if os.path.exists("health_manager.spec"):
|
||||
os.remove("health_manager.spec")
|
||||
if os.path.exists("shopper.spec"):
|
||||
os.remove("shopper.spec")
|
||||
|
||||
if __name__ == "__main__":
|
||||
new_version_code = None
|
||||
if args.version != "":
|
||||
print(f"Releasing new version: {args.version}")
|
||||
os.system(f"git checkout -b new-release-v{args.version}")
|
||||
botty_dir = f"botty_v{args.version}"
|
||||
version_code = ""
|
||||
with open('src/version.py', 'r') as f:
|
||||
version_code = f.read()
|
||||
version_code = version_code.split("=")
|
||||
new_version_code = f"{version_code[0]}= '{args.version}'"
|
||||
with open('src/version.py', 'w') as f:
|
||||
f.write(new_version_code)
|
||||
else:
|
||||
botty_dir = f"botty_v{__version__}"
|
||||
print(f"Building version: {__version__}")
|
||||
|
||||
clean_up()
|
||||
|
||||
if os.path.exists(botty_dir):
|
||||
for path in Path(botty_dir).glob("**/*"):
|
||||
if path.is_file():
|
||||
os.remove(path)
|
||||
elif path.is_dir():
|
||||
shutil.rmtree(path)
|
||||
shutil.rmtree(botty_dir)
|
||||
|
||||
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)
|
||||
|
||||
os.system(f"cd {botty_dir} && mkdir config && cd ..")
|
||||
|
||||
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("README.md", f"{botty_dir}/")
|
||||
shutil.copytree("assets", f"{botty_dir}/assets")
|
||||
clean_up()
|
||||
|
||||
if args.random_name:
|
||||
print("Generate random names")
|
||||
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')
|
||||
|
||||
if new_version_code is not None:
|
||||
os.system(f'git add .')
|
||||
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=fistman
|
||||
; 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
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
# Dev Docu
|
||||
|
||||
## Dependencies
|
||||
- Install latest miniconda (https://docs.conda.io/en/latest/miniconda.html). Note: You will have to check "Add conda to my PATH environment variable" in order to access the conda command in the cmd.
|
||||
- Install git (https://gitforwindows.org/)
|
||||
|
||||
## Getting started
|
||||
```bash
|
||||
git clone https://github.com/aeon0/botty.git
|
||||
cd botty
|
||||
conda env create environment.yml
|
||||
conda activate botty
|
||||
python src/main.py
|
||||
```
|
||||
Important info for Powershell users:
|
||||
```bash
|
||||
# for powershell you have to init conda before using any conda commands:
|
||||
conda init powershell
|
||||
```
|
||||
|
||||
## Tests
|
||||
All automated tests can be found within the **/test/*** folder. The file and folder structure is supposed to mimic the src folder.
|
||||
```bash
|
||||
conda activate botty
|
||||
# To run all tests: (-s to see stdout, -v for verbose)
|
||||
pytest -s -v
|
||||
# To see std output:
|
||||
# To run a specific test:
|
||||
pytest test/smoke_test.py
|
||||
```
|
||||
To test single files / routines, most files also can be executed seperatly. E.g. running `python src/pickit.py` -> going to d2r window -> throw stuff on the groudn -> press f11, will test the pickit.
|
||||
|
||||
## Adding Items
|
||||
To add items you can check the **assets/items** folder. Screenshot whatever you want to pick up in the same way (all settings must be as if you ran the bot). Then add the filename to the param.ini [items] section (e.g. if boots_rare.png add boots_rare=1)
|
||||
|
||||
## Folder Structure
|
||||
**/**</br>
|
||||
The root contains docu, param files and development specific stuff such as .gitignore</br>
|
||||
|
||||
**assets**</br>
|
||||
Contains all data for the project that is not source code</br>
|
||||
**assets/docs**</br>
|
||||
Images you can see in the .md files and logos</br>
|
||||
**assets/items**</br>
|
||||
Screenshot of item names that should be picked up. The filename must then be added to the param.ini</br>
|
||||
**assets/npc**</br>
|
||||
Templates of npcs in different poses</br>
|
||||
**assets/templates**</br>
|
||||
Templates for different UIs and key points. Also contains folders of "pathes" that were generated with the utils/node_creator.py</br>
|
||||
|
||||
**src**</br>
|
||||
All python source files go here</br>
|
||||
**src/char**</br>
|
||||
Want to implement a new char or build. Check this folder out. You will have to inherit from IChar and go from there</br>
|
||||
**src/char**</br>
|
||||
Utilities functions and scripts e.g. for easily creating templates to traverse nodes and automatically generate code for it</br>
|
||||
|
||||
## Code routine
|
||||
main.py contains the main() function and is the entry point for botty. It will start 3 threads: death monitoring (death_manager.py), health monitoring (health_manager.py) and the bot (bot.py) itself. Whenever the two monitors either detect a player's death or chicken out of the game, the bot thread will be killed and restarted.</br>
|
||||
In bot.py is a state machine in its core an executes different actions based on the current state. The goal is to remove as much implementation details as possible from bot.py and "hide" them in different manager classes (e.g. pickit.py, pather.py, npc_manager.py, etc.)
|
||||
|
||||
## State Diagram
|
||||
The core logic of the bot is determined by a state machine with these states and transations. The bot.py which contains all of the transitions should have little implementation code which should be hidden as much as possible in the "manager" classes.
|
||||
<img src="assets/docs/state_diagram.png" width="550"/>
|
||||
|
||||
## Coordinate System
|
||||
There are different coordinate systems used and I tried my best to add these to the variable names.</br>
|
||||
**Monitor**: It will have the origin at the top left of the first monitor</br>
|
||||
**Screen**: Same as monitor for single monitor setups, otherwise origin at top left of the screen </br>
|
||||
**Absolute**: Has its origin at the center of the screen, thus at the footpoint of your char </br>
|
||||
**Relative**: Relative coordinates as the name suggest are relative to something. It is mostly used to express relative coordinates in relation to a tempalte that is found </br>
|
||||
<img src="assets/docs/coordinate_systems.png" width="550"/>
|
||||
|
||||
## Release process
|
||||
If you installed your miniconda in another location you will of course have ot change it for that one.
|
||||
```bash
|
||||
# Adapt new version with x.x.x, build .exe and bundeling all needed resource into one folder
|
||||
python release.py x.x.x
|
||||
```
|
||||
For changelog run: `git log <PREVIOUS_TAG>..HEAD --oneline --decorate`
|
||||
@@ -0,0 +1,102 @@
|
||||
# Adding a New Farming Route
|
||||
|
||||
## Quick Start (Scaffolding Tool)
|
||||
|
||||
```bash
|
||||
python3 tools/new_route.py --name my_target --act 1 --location "Black Marsh"
|
||||
```
|
||||
|
||||
This generates `src/run/my_target.py` and prints every line you need to add to `bot.py`, `params.ini`, and `pather.py`. The tool also runs `ruff check` on the generated file.
|
||||
|
||||
For a simpler inline version (no ruff check):
|
||||
|
||||
```bash
|
||||
python3 src/utils/new_route.py --name my_target --act 1 --location "Black Marsh"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step Manual Guide
|
||||
|
||||
### 1. Create the run file: `src/run/my_target.py`
|
||||
|
||||
Every run has two methods:
|
||||
|
||||
**`approach(start_loc, do_pre_buff)`** — gets the character to the run start location:
|
||||
- Navigate to the right act's WP via `traverse_nodes_automap()`
|
||||
- Use `pather.go_to_area("Area Name", "WP")` to take the waypoint
|
||||
- Return a `Location` constant on success, `False` on failure
|
||||
|
||||
**`battle()`** — kills the target and picks items:
|
||||
- Navigate to the target via recorded path nodes
|
||||
- Call `char.kill_X()` (add to `IChar` / your char class if missing)
|
||||
- Call `pickit.pick_up_items(char)`
|
||||
- Return `(Location, picked_up_bool)` on success, `False` on failure
|
||||
|
||||
### 2. Add path node constants to `src/pather.py`
|
||||
|
||||
In `class Location`, add string constants for each nav point:
|
||||
```python
|
||||
MY_TARGET_SAFE_DIST = "my_target_safe_dist"
|
||||
```
|
||||
|
||||
### 3. Record path nodes in-game
|
||||
|
||||
Requires: **D2R game running**, **bot window active**, **game window NOT in fullscreen**
|
||||
|
||||
```bash
|
||||
python3 src/utils/node_recorder.py
|
||||
```
|
||||
|
||||
Enter the run name (e.g. `andariel`) when prompted. A debug visualization window will appear.
|
||||
|
||||
#### Recording workflow:
|
||||
|
||||
1. **F8** — Record a visual template: click top-left corner of a distinctive UI element or landmark, press F8, then click bottom-right corner, press F8 again. This creates a reusable reference point.
|
||||
2. **F9** — Record a navigation node at the current mouse cursor position (relative to the templates you recorded).
|
||||
3. **F10** — Update all recorded nodes with currently visible templates.
|
||||
4. **F12** — Exit recorder.
|
||||
|
||||
Recorded templates go to `log/screenshots/generated/templates/<run_name>/`.
|
||||
The generated pather code is written to `log/screenshots/generated/pather_generated.py`.
|
||||
|
||||
#### Level requirements for boss areas (need campaign progress):
|
||||
|
||||
| Boss | Act | Minimum Level | Area |
|
||||
|------|-----|---------------|------|
|
||||
| Countess | 1 | 1 | Forgotten Tower |
|
||||
| Andariel | 1 | 6 | Catacombs Level 4 |
|
||||
| Duriel | 2 | 15 | Sewers Level 3 |
|
||||
| Mephisto | 3 | 30 | Durance of Hate Level 3 |
|
||||
| Baal | 5 | 60 | Throne of Destruction |
|
||||
|
||||
You need a character that has progressed through the campaign to access these areas. If you're starting from level 1, you'll need to play through each act to unlock the areas, then record nodes as you go.
|
||||
|
||||
### 4. Register the route in `src/bot.py`
|
||||
|
||||
Four additions (the scaffold tool prints the exact lines):
|
||||
1. Import: `from run import ..., MyTarget`
|
||||
2. `_do_runs` dict entry: `"run_my_target": Config().routes.get("run_my_target"),`
|
||||
3. Instance: `self._my_target = MyTarget(...)`
|
||||
4. State machine state + transition + handler method
|
||||
|
||||
### 5. Document in `config/params.ini`
|
||||
|
||||
Add `; run_my_target` to the routes comment block so users know it exists.
|
||||
|
||||
### 6. Enable the route
|
||||
|
||||
In your `config/params.ini` (or `config/custom.ini`):
|
||||
```ini
|
||||
[routes]
|
||||
order=run_pindle, run_my_target
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stealth Tips for New Routes
|
||||
|
||||
- Use `wait(min, max)` for all pauses — the stealth jitter config multiplies these automatically
|
||||
- Use `stealth_move(x, y)` instead of `mouse.move(x, y)` for clicks in the new run
|
||||
- Keep path node counts low (3-5 nodes) — more nodes = more predictable pathing patterns
|
||||
- Vary which areas you teleport through using random sub-paths if the route allows it
|
||||
@@ -0,0 +1,74 @@
|
||||
# BNIP Guide
|
||||
|
||||
This guide explains how to edit `config/default.bnip` safely and predictably.
|
||||
|
||||
## What BNIP Does
|
||||
|
||||
BNIP rules decide which items Botty keeps.
|
||||
Each line is a filter expression evaluated against detected item data.
|
||||
|
||||
## Rule Shape
|
||||
|
||||
Typical rule format:
|
||||
|
||||
```text
|
||||
[Name] == Ring && [Quality] == Rare # [Fcr] >= 10 && [Allres] >= 15
|
||||
```
|
||||
|
||||
- Left side (`[Name]`, `[Type]`, `[Quality]`, etc.) narrows item identity.
|
||||
- Right side after `#` checks stats/rolls.
|
||||
|
||||
## Enable or Disable Rules
|
||||
|
||||
- Enabled: line starts with `[...`
|
||||
- Disabled: line starts with `//`
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
//[Name] == Lemrune
|
||||
[Name] == Pulrune
|
||||
```
|
||||
|
||||
## Safe Editing Workflow
|
||||
|
||||
1. Copy an existing nearby rule.
|
||||
2. Keep your new rule commented out initially (`//`).
|
||||
3. Enable one new rule at a time.
|
||||
4. Run a few games and verify behavior before adding more.
|
||||
|
||||
## Rule Ordering
|
||||
|
||||
BNIP files are easier to maintain when ordered from specific to broad.
|
||||
|
||||
- Put strict high-value rules first.
|
||||
- Put broad catch-all rules later.
|
||||
- Avoid duplicated broad rules in multiple sections.
|
||||
|
||||
## Common Fields You Will Use
|
||||
|
||||
- `[Name]`
|
||||
- `[Type]`
|
||||
- `[Quality]`
|
||||
- `[Flag]` (for ethereal/sockets behavior)
|
||||
- Stat aliases like `[Fcr]`, `[Allres]`, `[Enhanceddefense]`, `[Enhanceddamage]`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If a desired item is not kept:
|
||||
|
||||
1. Confirm the rule is enabled (no `//`).
|
||||
2. Relax one condition at a time.
|
||||
3. Check for typos in stat aliases.
|
||||
4. Ensure no local BNIP file in `config/bnip/` is overriding expectations.
|
||||
|
||||
If too much junk is kept:
|
||||
|
||||
1. Tighten broad rules.
|
||||
2. Disable catch-all rules first.
|
||||
3. Add stricter stat thresholds.
|
||||
|
||||
## Recommended Local Customization
|
||||
|
||||
Keep `config/default.bnip` as the team baseline.
|
||||
Put personal experiments in separate local `.bnip` files under `config/bnip/` and test there first.
|
||||
@@ -0,0 +1,142 @@
|
||||
# Broken Runs - Setup Guide
|
||||
|
||||
## Current Status
|
||||
|
||||
All 4 boss runs (Countess, Andariel, Mephisto, Baal) have **code structure in place** but require **path recording** before they can run.
|
||||
|
||||
| Run | Code | Guards | Path Coords | Templates | Walking Fallback |
|
||||
|---|---|---|---|---|---|
|
||||
| Countess | OK | (0,0) check | 0,0 placeholder | Empty dir | Nodes 1000-1004 |
|
||||
| Andariel | OK | (0,0) check | 0,0 placeholder | Empty dir | Nodes 1010-1012 |
|
||||
| Mephisto | OK | (0,0) check | 0,0 placeholder | Empty dir | Nodes 1020-1021 |
|
||||
| Baal | OK | (0,0) check | 0,0 placeholder | Empty dir | Nodes 1030-1031 |
|
||||
|
||||
## What's Done
|
||||
|
||||
- **Run classes**: `countess.py`, `andariel.py`, `mephisto.py`, `baal.py` - all wired in `bot.py`
|
||||
- **Kill methods**: `kill_countess()`, `kill_andariel()`, `kill_mephisto()`, `kill_baal()`, `kill_baal_waves()` - implemented in Hammerdin/FoHdin/Warlock
|
||||
- **Path guards**: All 4 runs check for `(0,0)` paths and refuse to run with a clear error message
|
||||
- **Walking fallback**: `pather.py` has node definitions (1000-1031) and path routes for walking fallback
|
||||
- **Template dirs**: `assets/templates/countess/`, `andariel/`, `mephisto/`, `baal/` created (empty)
|
||||
|
||||
## What's Needed to Enable Each Run
|
||||
|
||||
### Option A: Teleport (faster, recommended)
|
||||
|
||||
Record path coordinates in `config/game.ini` using `node_recorder.py`:
|
||||
|
||||
```bash
|
||||
cd C:\Users\alex\Downloads\my-botty
|
||||
python src/utils/node_recorder.py
|
||||
```
|
||||
|
||||
Then follow the on-screen instructions:
|
||||
- **F8**: Capture template ROI (top-left, then bottom-right) -> saves PNG
|
||||
- **F9**: Record node position at cursor -> generates path coordinates
|
||||
- **F10**: Update all nodes with visible templates
|
||||
|
||||
### Option B: Walking (slower, needs templates + nodes)
|
||||
|
||||
Same as above, but also requires creating template PNGs for each landmark.
|
||||
|
||||
---
|
||||
|
||||
## 1. COUNTESS (Act 1 Forgotten Tower)
|
||||
|
||||
**game.ini keys to record (5):**
|
||||
|
||||
| Key | What to Record |
|
||||
|---|---|
|
||||
| `a1_tower_level2_enter` | First click inside tower after entering from Black Marsh |
|
||||
| `a1_tower_level3_enter` | Top of stairs from L2 to L3 |
|
||||
| `a1_tower_level4_enter` | Top of stairs from L3 to L4 |
|
||||
| `a1_tower_level5_enter` | Top of stairs from L4 to L5 |
|
||||
| `a1_countess_safe_dist` | Position near Countess at safe hammer range |
|
||||
|
||||
**Templates for walking fallback (in `assets/templates/countess/`):**
|
||||
|
||||
| Template File | Template Key | Purpose |
|
||||
|---|---|---|
|
||||
| `countess_tower_l2.png` | COUNTESS_TOWER_L2 | Tower L2 entrance |
|
||||
| `countess_tower_l3.png` | COUNTESS_TOWER_L3 | Stairs L2->L3 |
|
||||
| `countess_tower_l4.png` | COUNTESS_TOWER_L4 | Stairs L3->L4 |
|
||||
| `countess_tower_l5.png` | COUNTESS_TOWER_L5 | Stairs L4->L5 |
|
||||
| `countess_boss.png` | COUNTESS_BOSS | Countess boss area |
|
||||
|
||||
**Nodes in pather.py:** 1000-1004 (placeholder coords `(0,0)` - update after recording)
|
||||
|
||||
---
|
||||
|
||||
## 2. ANDARIEL (Act 1 Catacombs)
|
||||
|
||||
**game.ini keys to record (3):**
|
||||
|
||||
| Key | What to Record |
|
||||
|---|---|
|
||||
| `a1_andy_level3_enter` | Catacombs L3 entrance (from L2) |
|
||||
| `a1_andy_level4_enter` | Catacombs L4 entrance (from L3) |
|
||||
| `a1_andy_safe_dist` | Position near Andariel's cage at safe hammer range |
|
||||
|
||||
**Templates for walking fallback (in `assets/templates/andariel/`):**
|
||||
|
||||
| Template File | Template Key | Purpose |
|
||||
|---|---|---|
|
||||
| `andy_l3_stairs.png` | ANDY_L3_STAIRS | Stairs from L2 to L3 |
|
||||
| `andy_l4_stairs.png` | ANDY_L4_STAIRS | Stairs from L3 to L4 |
|
||||
| `andy_cage.png` | ANDY_CAGE | Andariel cage area |
|
||||
|
||||
**Nodes in pather.py:** 1010-1012 (placeholder coords `(0,0)` - update after recording)
|
||||
|
||||
---
|
||||
|
||||
## 3. MEPHISTO (Act 3 Durance of Hate)
|
||||
|
||||
**game.ini keys to record (2):**
|
||||
|
||||
| Key | What to Record |
|
||||
|---|---|
|
||||
| `a3_meph_level3_enter` | Durance of Hate L3 entrance (from L2) |
|
||||
| `a3_meph_safe_dist` | Position near Mephisto's cage at safe hammer range |
|
||||
|
||||
**Templates for walking fallback (in `assets/templates/mephisto/`):**
|
||||
|
||||
| Template File | Template Key | Purpose |
|
||||
|---|---|---|
|
||||
| `meph_l3_stairs.png` | MEPH_L3_STAIRS | Stairs from L2 to L3 |
|
||||
| `meph_cage.png` | MEPH_CAGE | Mephisto cage area |
|
||||
|
||||
**Nodes in pather.py:** 1020-1021 (placeholder coords `(0,0)` - update after recording)
|
||||
|
||||
---
|
||||
|
||||
## 4. BAAL (Act 5 Throne of Destruction)
|
||||
|
||||
**game.ini keys to record (2):**
|
||||
|
||||
| Key | What to Record |
|
||||
|---|---|
|
||||
| `a5_baal_throne_entry` | Throne of Destruction entrance (from Worldstone Keep L2) |
|
||||
| `a5_baal_safe_dist` | Position near Baal at safe hammer range (after wave clear) |
|
||||
|
||||
**Templates for walking fallback (in `assets/templates/baal/`):**
|
||||
|
||||
| Template File | Template Key | Purpose |
|
||||
|---|---|---|
|
||||
| `baal_throne_entry.png` | BAAL_THRONE_ENTRY | Entrance to Throne area |
|
||||
| `baal_arena.png` | BAAL_ARENA | Baal arena / boss position |
|
||||
|
||||
**Nodes in pather.py:** 1030-1031 (placeholder coords `(0,0)` - update after recording)
|
||||
|
||||
---
|
||||
|
||||
## Quick Start: Record Paths for One Run
|
||||
|
||||
1. Launch D2R and navigate to the boss area
|
||||
2. Run `python src/utils/node_recorder.py` from the botty directory
|
||||
3. Enter the run name when prompted (e.g., `countess`)
|
||||
4. Position your cursor at the target location in-game
|
||||
5. Press F8 to capture a template, F9 to record a node
|
||||
6. Press F10 to update all nodes with visible templates
|
||||
7. Copy the generated path coordinates to `config/game.ini`
|
||||
8. Copy the generated template PNGs to `assets/templates/<run>/`
|
||||
9. Update `pather.py` node definitions with real coordinates from the recorder output
|
||||
@@ -0,0 +1,18 @@
|
||||
# D2R Window and Input Troubleshooting
|
||||
|
||||
Botty expects Diablo II: Resurrected to expose a stable 1280x720 client area. If
|
||||
the live D2R window reports a slightly different client size, template matching
|
||||
can still succeed while mouse clicks land at the wrong monitor coordinate.
|
||||
|
||||
At startup and before creating a game, Botty now resizes and positions the D2R
|
||||
client area to the configured `config/game.ini` dimensions. The expected size is
|
||||
`1280x720`, matching `assets/d2r_settings.json` and the template assets.
|
||||
|
||||
If a UI click is detected but the cursor does not visibly move, the native input
|
||||
layer first tries `SendInput` and then verifies the cursor position. When
|
||||
`SendInput` misses the target coordinate, Botty falls back to `SetCursorPos` and
|
||||
logs the fallback.
|
||||
|
||||
If movement or clicks still do not reach D2R, check that D2R and the bot process
|
||||
are running at the same privilege level. Windows can block input from a
|
||||
non-admin process into an elevated game window.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Diablo Waypoint Recovery Notes
|
||||
|
||||
Date: 2026-06-07
|
||||
|
||||
## What changed
|
||||
|
||||
- Diablo is now first in the configured run order, followed by Pindle only.
|
||||
- After using the Act 4 River of Flame waypoint, the Diablo approach now closes the waypoint panel, resets stale health-manager panel detections, and verifies that River of Flame templates are visible.
|
||||
- If the first River of Flame waypoint attempt leaves the bot in town, the approach retries from the Act 4 town start before failing the run.
|
||||
- River of Flame and Pentagram traversal now include template-based verification before continuing into Chaos Sanctuary logic.
|
||||
- Diablo now has a reusable `_search_and_log()` wrapper for template searches that can report match confidence during route debugging.
|
||||
- Health manager reset now clears `_count_panel_detects` so detections from a previous game do not immediately chicken the next game.
|
||||
- `config/fg_daily_estimates.json` was regenerated with the improved offline estimator output, including skipped-topic counts and trimmed price statistics.
|
||||
|
||||
## Why
|
||||
|
||||
Same-act waypoint use can leave the waypoint panel open without a loading screen. That stale panel could combine with the CTA weapon-swap panel during pre-buffing and trigger the health manager panel-detection chicken path. The bot could also silently remain in town after a missed waypoint click and continue as if it had reached River of Flame.
|
||||
|
||||
The new checks make Diablo startup state-based: the bot confirms River of Flame and Pentagram markers before continuing. Failed waypoint transitions are retried once, then reported as approach failures instead of drifting into later pathing.
|
||||
|
||||
## Validation
|
||||
|
||||
Run a focused route order:
|
||||
|
||||
```ini
|
||||
order=run_diablo, run_pindle
|
||||
```
|
||||
|
||||
Expected log behavior:
|
||||
|
||||
- `_verify_in_rof: confirmed in River of Flame ...` after the Act 4 waypoint.
|
||||
- `CS: Calibrated at PENTAGRAM` after Pentagram traversal.
|
||||
- No immediate chicken caused by the waypoint panel plus CTA weapon-swap panel sequence.
|
||||
|
||||
If River of Flame or Pentagram verification fails repeatedly, refresh the affected templates:
|
||||
|
||||
- `DIABLO_ROF_WP_0`
|
||||
- `DIABLO_ROF_WP_1`
|
||||
- `DIABLO_ENTRANCE_50` through `DIABLO_ENTRANCE_55`
|
||||
- `DIA_NEW_PENT_TP`
|
||||
- `DIA_NEW_PENT_0` through `DIA_NEW_PENT_2`
|
||||
@@ -0,0 +1,44 @@
|
||||
# FG Market Scraper (Day 1-14)
|
||||
|
||||
This tool estimates FG prices per ladder day by scraping public trade topics from:
|
||||
- https://forums.d2jsp.org/forum.php?f=271
|
||||
|
||||
## Script
|
||||
- `tools/fg_market_scraper.py`
|
||||
|
||||
## What it does
|
||||
1. Scans forum listing pages for topic links.
|
||||
2. Fetches topic pages (rate-limited).
|
||||
3. Detects known item/rune keywords.
|
||||
4. Extracts `fg` prices from post text.
|
||||
5. Buckets prices by ladder day index (Day 1..Day N).
|
||||
6. Writes median estimates + sample counts.
|
||||
|
||||
## Usage
|
||||
From repo root:
|
||||
|
||||
```powershell
|
||||
python tools/fg_market_scraper.py --ladder-start-date 2026-05-23 --days 14
|
||||
```
|
||||
|
||||
Output:
|
||||
- `config/fg_daily_estimates.json`
|
||||
|
||||
## Tuning
|
||||
- `--max-forum-pages`: listing pages to scan (default 40)
|
||||
- `--max-topics`: hard cap on fetched topics (default 800)
|
||||
- `--delay-s`: delay between requests (default 0.35s)
|
||||
|
||||
Example heavier run:
|
||||
|
||||
```powershell
|
||||
python tools/fg_market_scraper.py --ladder-start-date 2026-05-23 --days 14 --max-forum-pages 120 --max-topics 2400 --delay-s 0.5
|
||||
```
|
||||
|
||||
## Notes
|
||||
- This is a heuristic estimator, not a full market engine.
|
||||
- Accuracy depends on post format quality and keyword matches.
|
||||
- Keep request rate polite to avoid stressing the forum.
|
||||
- Some environments/IPs will receive HTTP `403` from d2jsp. In that case use:
|
||||
- `config/fg_day_estimates.json` (Day 1-14 estimated multiplier model from Day 3 snapshot),
|
||||
- and update `config/fg_prices.json` manually from your current market sample.
|
||||
@@ -0,0 +1,87 @@
|
||||
# GEMS-tab transmute flow (rewritten in commit 48d8445)
|
||||
|
||||
`src/transmute/transmute.py` — `Transmute.convert_all_gems()` and helpers.
|
||||
|
||||
## What changed and why
|
||||
|
||||
The old flow drove gem upgrades through the **Horadric Cube UI**: it required the cube to live in
|
||||
the PERSONAL stash tab, opened the cube, ctrl+shift+right-clicked 3 gems into it, clicked the cube's
|
||||
Transmute button, then pulled the result back out. This was fragile — it depended on cube placement,
|
||||
cube-open detection (`ScreenObjects.CubeOpened` via `wait_until_visible`), and a pre-flight
|
||||
"empty the cube" pass.
|
||||
|
||||
The rewrite uses D2R's **native GEMS-tab convert panel** instead of the cube. Gems are moved
|
||||
directly between the GEMS stack grid and the convert panel, and conversion uses the GEMS tab's own
|
||||
convert button. The cube is no longer required to be in the stash and, if present in the character
|
||||
inventory, is left untouched.
|
||||
|
||||
## Fixed stack-coordinate grid
|
||||
|
||||
`GEMS_TAB_STACK_COORDS` is a new module-level dict mapping every gem template
|
||||
(`INVENTORY_<FAMILY>_<TIER>`) to a fixed `(x, y)` **screen** coordinate at 1280×720. The grid is:
|
||||
|
||||
- **Columns by family** (x): diamond 85, emerald 132, ruby 180, topaz 227, amethyst 275,
|
||||
sapphire 322, skull 370.
|
||||
- **Rows by tier** (y): chipped 154, flawed 195, standard 234, flawless 273, perfect 311.
|
||||
|
||||
Because gem stacks always render at these fixed slots on the GEMS tab, the flow can click them by
|
||||
coordinate instead of template-searching for them each time. `_gems_stack_monitor_for(template)`
|
||||
converts the screen coord to monitor coords (returns `None` if the template isn't in the grid, so
|
||||
the caller can fall back to a template search).
|
||||
|
||||
## Panel constants (1280×720)
|
||||
|
||||
| Constant | Value | Meaning |
|
||||
|----------|-------|---------|
|
||||
| `GEMS_TAB_X` | `223` | GEMS tab click X (was `200`) |
|
||||
| `GEMS_CONVERT_BUTTON` | `(225, 500)` | native convert button in the GEMS panel |
|
||||
| `GEMS_CONVERT_PANEL_ROI` | `(160, 296, 128, 160)` | ROI to search for the converted result |
|
||||
| `GEMS_CONVERT_FIRST_SLOT` | `(181, 318)` | fallback slot to grab the result if the search misses |
|
||||
|
||||
## Per-transmute flow (new)
|
||||
|
||||
For each gem type/tier with enough gems to convert:
|
||||
|
||||
1. **Switch to GEMS tab.**
|
||||
2. **Load 3 gems** — `_ctrl_shift_left_click_monitor` the stack slot ×3 to move 3 gems into the
|
||||
convert panel. If exactly 3 weren't loaded, abort that batch.
|
||||
3. **Convert** — `_click_gems_tab_convert_button()` clicks the panel's native convert button.
|
||||
4. **Recover the result** — template-search `gems_out` within `GEMS_CONVERT_PANEL_ROI`; if found,
|
||||
ctrl+shift+left-click it back onto the GEMS tab. If not found, fall back to ctrl+shift+left-click
|
||||
on `GEMS_CONVERT_FIRST_SLOT` (instead of the old behavior of clearing the cube and bailing).
|
||||
5. **Leave the inventory cube untouched** and return to the GEMS tab for the next iteration.
|
||||
|
||||
The old pre-flight "open cube → empty 12 cube slots → reopen stash" pass and the per-iteration
|
||||
"close cube / reopen stash" steps are **removed**.
|
||||
|
||||
## Helper changes
|
||||
|
||||
- `_open_cube_from_stash()` / `_open_cube_from_gems_tab()` / `_open_available_cube_for_gems_tab()`
|
||||
now operate from the **GEMS** tab rather than PERSONAL, and can open the cube from character
|
||||
inventory (`_open_cube_from_inventory`) when the GEMS tab is active. These remain for older
|
||||
inventory flows; the new gem flow doesn't need them.
|
||||
- `_locate_cube()` can now return `'opened'` (cube UI already open on the GEMS tab) in addition to
|
||||
`'stash'` / `'inventory'`.
|
||||
- `_ensure_cube_available()` now treats `inventory` as usable ("it will stay there") instead of an
|
||||
error — the cube no longer has to be moved to PERSONAL.
|
||||
- `_ensure_cube_in_stash()` dropped the loot-column safety guard: it now ctrl+clicks any positive
|
||||
`CubeInventory` match, so the cube can be moved from reserved inventory columns too.
|
||||
- **Keyboard modifier handling** switched from the low-level
|
||||
`win_input.key_down/key_up(_get_vk(...))` to `keyboard.send(..., do_release=False)` /
|
||||
`keyboard.release(...)` wrapped in `try/finally`, so ctrl+shift are always released even if the
|
||||
click raises. Applies to both `_ctrl_shift_click_monitor` (right) and
|
||||
`_ctrl_shift_left_click_monitor` (left).
|
||||
- `_empty_cube_to_gems_tab()` now uses `_ctrl_click_monitor` (plain ctrl) rather than ctrl+shift.
|
||||
|
||||
## Gem counting (`_count_gems_by_ocr`)
|
||||
|
||||
Counting no longer template-searches the left-inventory ROI for each gem. It now reads each fixed
|
||||
slot from `GEMS_TAB_STACK_COORDS`: crop the slot icon, and **skip empty slots** by checking the
|
||||
95th-percentile grayscale brightness (`< 45` ⇒ no gem present). Occupied slots still OCR the count
|
||||
badge in the bottom-right quadrant. This is faster and immune to template-match drift, at the cost
|
||||
of depending on the fixed 1280×720 grid.
|
||||
|
||||
## Resolution assumption
|
||||
|
||||
The whole flow is hard-coded to **1280×720**. All coordinates above are screen coordinates at that
|
||||
resolution, converted to monitor coordinates at click time via `convert_screen_to_monitor`.
|
||||
@@ -0,0 +1,112 @@
|
||||
# Recovery & Error Logging
|
||||
|
||||
This document describes the failure-recovery and diagnostics features that let the
|
||||
bot survive broken runs and makes every failure reviewable after the fact.
|
||||
|
||||
## 1. Comprehensive failure logging
|
||||
|
||||
Every town action (`src/town/a1.py` … `a5.py`) and every run `approach()` method
|
||||
(`src/run/*.py`) now logs the **specific step that failed** via `Logger.error`
|
||||
instead of silently returning `False`. Examples of what now gets logged:
|
||||
|
||||
- `A1 resurrect: open_npc_menu(KASHYA) failed — NPC not found or menu did not open`
|
||||
- `A4 open_wp: waypoint template A4_WP/A4_WP_2 not found — WP may be obscured or template mismatch`
|
||||
- `Vizier approach: use_wp('River of Flame') failed`
|
||||
- `Mephisto approach: go_to_act(3) failed`
|
||||
- `gamble: go_to_act(4) failed from A1_TOWN_START — cannot reach Jamella`
|
||||
|
||||
Town-manager fallbacks (`resurrect`, `identify`, `open_stash`, `gamble`, `stash`,
|
||||
`heal`) also log when their `go_to_act` recovery hop fails, so you can tell whether
|
||||
the problem was reaching the NPC, opening the menu, pressing the button, or the
|
||||
panel never opening.
|
||||
|
||||
This means that "if Cain is missing, if the WP is not enabled, if some other game
|
||||
factor is not in place" the reason ends up in the log.
|
||||
|
||||
## 2. Error screenshots
|
||||
|
||||
When a run fails, the bot saves a screenshot so logs and visuals can be compared.
|
||||
|
||||
- **Where:** `log/screenshots/error/`
|
||||
- **Filename:**
|
||||
`error_<run_name>_<reason>_g<game#>_r<run#>_<YYYYMMDD_HHMMSS>.png`
|
||||
e.g. `error_run_vizier_approach_failed_g12_r47_20260607_143501.png`
|
||||
- **When:** captured on three failure types inside `Bot._run_wrapper`:
|
||||
- `approach_failed` — couldn't reach the boss
|
||||
- `battle_failed` — boss fight didn't complete
|
||||
- `exception_<ExceptionType>` — an uncaught exception during battle
|
||||
- The `g<game#>_r<run#>` in the filename matches the `game`/`run` fields in the
|
||||
structured event log (`log/stats/events_*.jsonl`) and the `Logger` output, so a
|
||||
screenshot can be lined up with its log lines.
|
||||
|
||||
### Discord delivery
|
||||
|
||||
When a run fails the bot also sends the **error message and the screenshot** to
|
||||
Discord (as a red embed with the screenshot attached). For the generic webhook API
|
||||
a text-only message is sent (the screenshot still lands on disk). This is on by
|
||||
default and can be turned off two ways:
|
||||
|
||||
- `[general] discord_log_errors=0` — master switch for error → Discord
|
||||
- `[discord_events] error=0` — fine-grained event toggle (Discord API only)
|
||||
|
||||
### Config
|
||||
|
||||
`config/params.ini` → `[general]`:
|
||||
|
||||
```ini
|
||||
; save a screenshot every time a run fails (falls back to info_screenshots if unset)
|
||||
error_screenshots=1
|
||||
; also send the error message + screenshot to Discord on each run failure
|
||||
discord_log_errors=1
|
||||
```
|
||||
|
||||
`config/params.ini` → `[discord_events]`:
|
||||
|
||||
```ini
|
||||
; send run-failure notifications (message + error screenshot)
|
||||
error=1
|
||||
```
|
||||
|
||||
Set `error_screenshots=0` to turn off error screenshots entirely. Old screenshots
|
||||
are size-rotated using the `info_*` limits in `[log_rotation]` (or optional
|
||||
`error_max_files` / `error_max_mb` if you add them).
|
||||
|
||||
## 3. Auto-disable broken runs (recovery)
|
||||
|
||||
Previously, repeated failures of one run could stall the whole bot. Now each run
|
||||
has its own **consecutive failure counter**:
|
||||
|
||||
- Every failed attempt of a run increments its counter; a single success resets it.
|
||||
- When a run reaches the threshold of consecutive failures, it is **disabled for
|
||||
the rest of the session**. The bot keeps doing the other runs instead of stopping.
|
||||
- Disabling is logged and sent to Discord (if enabled):
|
||||
`DISABLING run_vizier for this session: it failed 5 consecutive times. …`
|
||||
- The disable is **session-only / in-memory** — it does not edit `params.ini`.
|
||||
Restarting the bot re-enables the run. This is intentional so a transient problem
|
||||
(e.g. a game hiccup) doesn't permanently change your config.
|
||||
- If **all** runs end up disabled there are no routes left, so the bot saves a
|
||||
session report and stops cleanly (kills the game and exits) rather than looping
|
||||
over empty games or pointlessly restarting D2R.
|
||||
|
||||
### Config
|
||||
|
||||
`config/params.ini` → `[general]`:
|
||||
|
||||
```ini
|
||||
; after this many CONSECUTIVE failures of the same run, disable just that run
|
||||
; for the rest of the session and keep doing the others. A success resets the count.
|
||||
disable_run_after_failures=5
|
||||
```
|
||||
|
||||
Set this high (e.g. `9999`) to effectively disable the auto-disable behaviour.
|
||||
|
||||
## Implementation notes
|
||||
|
||||
- `src/bot.py`
|
||||
- `__init__`: `_run_failure_counts`, `_disabled_runs`, `_max_run_failures`
|
||||
- `_record_run_result(run_name, failed)`: counts failures, disables runs, resets on success
|
||||
- `_save_error_screenshot(run_name, reason)`: writes the diagnostic screenshot
|
||||
- `_run_wrapper(...)`: calls both on each failure path (approach / battle / exception)
|
||||
- `src/config.py`: parses `error_screenshots` and `disable_run_after_failures`
|
||||
(both via `_select_optional`, so existing configs keep working).
|
||||
- `src/utils/log_rotation.py`: routes the `error/` directory through rotation.
|
||||
@@ -0,0 +1,71 @@
|
||||
# Run Countess Route Plan
|
||||
|
||||
## Goal
|
||||
Record path data and templates for the Countess run (Act 1 Forgotten Tower) so it executes reliably with Hammerdin.
|
||||
|
||||
## Current State
|
||||
- `src/run/countess.py` exists and is wired into bot.py
|
||||
- `Location` constants defined in `pather.py`: A1_TOWER_LEVEL2_ENTER through A1_TOWER_LEVEL5_ENTER, A1_COUNTESS_SAFE_DIST
|
||||
- `config/game.ini` has 5 path entries — all set to `0,0` (placeholders)
|
||||
- Run is disabled in `params.ini` (`order=run_arcane, run_pindle`)
|
||||
|
||||
## Paths Needed (5 entries in game.ini)
|
||||
|
||||
| Config Key | Description | How to Record |
|
||||
|---|---|---|
|
||||
| `a1_tower_level2_enter` | First click point inside tower after entering from Black Marsh | Walk into tower, record first position on L2 |
|
||||
| `a1_tower_level3_enter` | Click point at top of stairs to L3 | Walk up stairs, record position at L3 entrance |
|
||||
| `a1_tower_level4_enter` | Click point at top of stairs to L4 | Walk up stairs, record position at L4 entrance |
|
||||
| `a1_tower_level5_enter` | Click point at top of stairs to L5 | Walk up stairs, record position at L5 entrance |
|
||||
| `a1_countess_safe_dist` | Click point near Countess (safe hammer range) | Walk to Countess, step back to hammer range |
|
||||
|
||||
## Recording Steps
|
||||
|
||||
### 1. Record path coordinates
|
||||
1. Start botty, let it waypoint to Black Marsh
|
||||
2. Enter the tower manually (walk through the door)
|
||||
3. Run `static_run_recorder.py`:
|
||||
```
|
||||
conda activate botty
|
||||
cd src/utils
|
||||
python static_run_recorder.py
|
||||
```
|
||||
4. Right-click each target position in game order (L2 enter → L3 enter → L4 enter → L5 enter → Countess safe distance)
|
||||
5. Copy the printed coordinate list
|
||||
|
||||
### 2. Update game.ini
|
||||
Replace the `0,0` values with recorded coordinates. Format: `x,y` per line, or comma-separated for multi-point paths.
|
||||
|
||||
Example:
|
||||
```ini
|
||||
a1_tower_level2_enter=400,300
|
||||
a1_tower_level3_enter=410,280
|
||||
a1_tower_level4_enter=420,260
|
||||
a1_tower_level5_enter=430,240
|
||||
a1_countess_safe_dist=450,200
|
||||
```
|
||||
|
||||
### 3. Test the run
|
||||
1. Add `run_countess` back to `order=` in `params.ini`
|
||||
2. Start botty and verify it navigates through all tower levels
|
||||
3. Adjust coordinates if it misses stairs or clicks in wrong spots
|
||||
|
||||
## Hammerdin-Specific Notes
|
||||
- `atk_len_countess=3.0` is already set in `params.ini`
|
||||
- Hammerdin uses `kill_countess()` method — ensure conviction aura is active before engaging
|
||||
- Safe distance should account for hammer swing range (~3-4 game units)
|
||||
|
||||
## Path Recorder Alternative
|
||||
If `static_run_recorder.py` is inconvenient, use `node_recorder.py`:
|
||||
```
|
||||
cd src/utils
|
||||
python node_recorder.py
|
||||
```
|
||||
Follow on-screen prompts to record nodes, then copy the generated path coordinates.
|
||||
|
||||
## Acceptance Criteria
|
||||
- Botty enters Black Marsh via waypoint
|
||||
- Navigates L2 → L3 → L4 → L5 without getting stuck
|
||||
- Reaches Countess and engages
|
||||
- Completes kill and returns to town
|
||||
- No false template matches on tower stairs
|
||||
@@ -0,0 +1,225 @@
|
||||
# Run Cows Route Plan
|
||||
|
||||
## Goal
|
||||
Add a stable `run_cows` route that can:
|
||||
- create/open Secret Cow Level portal in Act 1,
|
||||
- clear cows with safe movement and loot handling,
|
||||
- exit cleanly and continue normal run rotation.
|
||||
|
||||
## Scope
|
||||
This plan is for implementation and testing in this repo only.
|
||||
No behavior changes to existing routes unless needed for shared helpers.
|
||||
|
||||
## Constraints
|
||||
- Keep it compatible with current config style (`[routes]`, `[char]`, per-build sections).
|
||||
- Prioritize stability over speed for first release.
|
||||
- Avoid forcing hard dependencies on OCR for core route progression.
|
||||
|
||||
## Route Design
|
||||
1. Town prep in Act 1:
|
||||
- go to stash/cube workflow position.
|
||||
- ensure cube available.
|
||||
2. Cow portal creation:
|
||||
- verify Wirt's Leg + Tome of Town Portal availability.
|
||||
- transmute in Act 1 town.
|
||||
- detect red portal reliably.
|
||||
3. Entry and combat:
|
||||
- enter portal.
|
||||
- execute deterministic clear pattern (clockwise + center sweep).
|
||||
- run character-specific attack loop with timeout guard.
|
||||
4. Loot and exit:
|
||||
- reuse existing pickit flow.
|
||||
- return through TP or save/exit based on safety state.
|
||||
|
||||
## Code Changes (Planned)
|
||||
1. Add new run module:
|
||||
- `src/run/cows.py` (`name = "run_cows"`)
|
||||
- methods: `approach()`, `battle()`
|
||||
2. Wire route registration:
|
||||
- include `run_cows` in route loader/dispatcher used by `bot.py`.
|
||||
3. Town/cube helpers:
|
||||
- add minimal helper(s) for cube transmute flow if missing.
|
||||
4. Pathing/templates:
|
||||
- add cow portal/town waypoint templates only as needed.
|
||||
- add robust fallback checks for portal detect/select.
|
||||
5. Config/docs:
|
||||
- document `run_cows` in `config/params.ini` route comments.
|
||||
- add usage notes to `README.md`.
|
||||
|
||||
## Config Additions (Planned)
|
||||
In `[char]` or dedicated route section:
|
||||
- `cows_clear_timeout_s` (default safe value, e.g. 90-150)
|
||||
- `cows_use_tp_exit` (1/0)
|
||||
- `cows_repair_before_run` (1/0, default 1)
|
||||
|
||||
Keep defaults conservative.
|
||||
|
||||
## Safety & Recovery
|
||||
- If portal creation fails: log once, abort current cow attempt, continue next route.
|
||||
- If portal click fails: retry from known anchor position once.
|
||||
- If in-combat timeout reached: force exit sequence and mark run failed.
|
||||
- If inventory full during cows: run pickit stop logic and exit route early.
|
||||
|
||||
## Character Compatibility Strategy
|
||||
Phase 1 support:
|
||||
- Blizz Sorc
|
||||
- Hammerdin
|
||||
- FoHdin
|
||||
|
||||
Phase 2 support:
|
||||
- remaining builds with route-specific combat tuning.
|
||||
|
||||
## Telemetry / Stats
|
||||
Track in `game_stats`:
|
||||
- `run_cows` attempts
|
||||
- success/fail count
|
||||
- elapsed duration
|
||||
- rune/valuable item outcomes (already covered by global item tracking)
|
||||
|
||||
## Test Plan
|
||||
1. Unit-ish tests:
|
||||
- route registration includes `run_cows`
|
||||
- config parsing accepts route key without crash
|
||||
2. Integration smoke (manual):
|
||||
- portal creation success path
|
||||
- missing Wirt’s Leg failure path
|
||||
- missing tome failure path
|
||||
- portal click retry path
|
||||
3. Stability run:
|
||||
- 25+ games mixed with existing routes
|
||||
- verify no regression in Pindle/Trav/Diablo flows
|
||||
|
||||
## Assets / Inputs Needed
|
||||
- Screenshot set for:
|
||||
- Act 1 stash/cube interaction points
|
||||
- successful cow portal in town
|
||||
- entry location inside cows
|
||||
- Optional:
|
||||
- map movement recording for an efficient clear loop.
|
||||
|
||||
## Screenshot + Coordinates SOP (Detailed)
|
||||
|
||||
### Why this matters
|
||||
Cow route reliability depends on visual matching and deterministic interaction points.
|
||||
Bad captures (cursor overlap, wrong UI state, mismatched lighting) are the #1 reason for unstable routes.
|
||||
|
||||
### Environment lock before capture
|
||||
1. Use the same D2R setup that bot runtime uses:
|
||||
- same resolution,
|
||||
- same UI scale,
|
||||
- same graphics preset,
|
||||
- same window mode.
|
||||
2. Apply Botty auto settings before capture.
|
||||
3. Keep panel state consistent:
|
||||
- inventory closed unless the step explicitly needs it,
|
||||
- item labels in known state,
|
||||
- no obstructing chat/quest panels.
|
||||
|
||||
### Capture tools in this repo
|
||||
- `screenshot_tool.py`
|
||||
- `quest_screenshot_tool.py`
|
||||
- `quest_debug.py`
|
||||
- `desktop_snap.py`
|
||||
- Graphic debugger hotkey from `config/params.ini` (`advanced_options.graphic_debugger_key`)
|
||||
|
||||
### Capture types required per target
|
||||
For every actionable object, collect both:
|
||||
1. Full-context screenshot (for debugging and reproducibility)
|
||||
2. Tight template crop (for matching)
|
||||
|
||||
Template crop rules:
|
||||
- include only the unique visual feature + small margin,
|
||||
- avoid cursor overlap,
|
||||
- avoid tooltip overlap unless tooltip itself is the detection target,
|
||||
- avoid over-cropping edges that change with animation.
|
||||
|
||||
### Coordinate types to record
|
||||
For each target interaction, record:
|
||||
1. Screen/monitor click point used when interaction succeeds.
|
||||
2. Whether template-center click worked.
|
||||
3. Optional fallback click point if template-center is unreliable.
|
||||
4. ROI used (if narrowed in code/tests).
|
||||
5. Confidence range seen in quick manual validation.
|
||||
|
||||
### Mandatory cows capture checklist (in order)
|
||||
1. Act 1 town anchor (post-spawn)
|
||||
- full screenshot
|
||||
- possible anchor templates
|
||||
2. Stash interaction
|
||||
- full screenshot near stash
|
||||
- stash template crop(s)
|
||||
- successful click coordinate(s)
|
||||
3. Cube interaction/transmute context
|
||||
- inventory+cube state screenshot
|
||||
- transmute UI screenshot references
|
||||
4. Cow portal in town (opened)
|
||||
- full screenshot with portal visible
|
||||
- portal template crops (2-3 variants)
|
||||
- portal click success coordinates
|
||||
5. Cow level entry area
|
||||
- immediate post-load screenshot(s)
|
||||
- safe first-move anchor capture
|
||||
6. Exit path reference
|
||||
- screenshot where exit action is normally triggered
|
||||
- any portal/escape interaction target needed
|
||||
|
||||
### Variants to capture for robustness
|
||||
Per critical template (stash, portal, entry anchor), capture:
|
||||
- 3-5 samples with slight camera/position variation,
|
||||
- at least one “busy background” sample,
|
||||
- one sample with nearby NPC/player clutter if possible.
|
||||
|
||||
### Validation pass before route coding
|
||||
For each template candidate:
|
||||
1. Verify it matches across the sample set at intended threshold.
|
||||
2. Verify it does not false-match to nearby unrelated objects.
|
||||
3. Verify click-at-center triggers the intended interaction.
|
||||
4. Record accepted threshold and failure notes.
|
||||
|
||||
Reject templates that:
|
||||
- only match at very low confidence,
|
||||
- require exact pixel-perfect camera alignment,
|
||||
- frequently collide with non-target objects.
|
||||
|
||||
### File naming convention
|
||||
Use deterministic names aligned with route semantics, e.g.:
|
||||
- `A1_COW_PORTAL_0`
|
||||
- `A1_COW_PORTAL_1`
|
||||
- `A1_STASH_REF_0`
|
||||
- `COW_ENTRY_ANCHOR_0`
|
||||
|
||||
Keep source full screenshots in a dated debug folder and final template crops in the templates location used by the project.
|
||||
|
||||
### Capture log format (what testers should write down)
|
||||
For each target:
|
||||
- `template_name`
|
||||
- capture timestamp
|
||||
- location/context
|
||||
- successful click point(s)
|
||||
- confidence range observed
|
||||
- notes on failed attempts
|
||||
|
||||
### Inputs needed from testers (you + friend)
|
||||
Provide:
|
||||
1. ZIP/folder of full screenshots + template crops.
|
||||
2. Short markdown/text capture log using format above.
|
||||
3. Character/build used during capture (Blizz Sorc / FoH / Hammerdin).
|
||||
4. Any observed instability notes (miss-clicks, wrong target matches, etc.).
|
||||
|
||||
### Acceptance criteria for capture package
|
||||
Capture package is ready for implementation when:
|
||||
- all mandatory checklist targets are present,
|
||||
- each critical target has multi-sample variants,
|
||||
- at least one tester validated successful interaction per target,
|
||||
- naming is consistent and unambiguous.
|
||||
|
||||
## Rollout
|
||||
1. Land Phase 1 behind route opt-in (`order=... ,run_cows`).
|
||||
2. Keep disabled by default in examples until 10+ stable sessions.
|
||||
3. Promote to documented standard route after stability threshold.
|
||||
|
||||
## Definition of Done
|
||||
- `run_cows` can run repeatedly without manual intervention.
|
||||
- Failure modes recover without crashing whole bot loop.
|
||||
- Route is documented in `params.ini` and `README.md`.
|
||||
- CI/lint/tests pass for changed files.
|
||||
@@ -0,0 +1,11 @@
|
||||
name: botty
|
||||
channels:
|
||||
- conda-forge
|
||||
dependencies:
|
||||
- python=3.10
|
||||
- pywin32
|
||||
- pip
|
||||
- pkg-config
|
||||
- rapidfuzz=2.15.1
|
||||
- pip:
|
||||
- -r requirements-win10.txt
|
||||
@@ -0,0 +1,11 @@
|
||||
name: botty
|
||||
channels:
|
||||
- conda-forge
|
||||
dependencies:
|
||||
- python=3.10
|
||||
- pywin32
|
||||
- pip
|
||||
- pkg-config
|
||||
- rapidfuzz=2.15.1
|
||||
- pip:
|
||||
- -r requirements-win11.txt
|
||||
@@ -5,16 +5,14 @@ dependencies:
|
||||
- python=3.10
|
||||
- pywin32
|
||||
- pip
|
||||
- leptonica
|
||||
- tesseract=5.1.0
|
||||
- pkg-config
|
||||
- rapidfuzz
|
||||
- rapidfuzz=2.15.1
|
||||
- pip:
|
||||
- pyinstaller
|
||||
- opencv-python
|
||||
- opencv-python==4.5.5.64
|
||||
- transitions
|
||||
- mss
|
||||
- numpy
|
||||
- mss==7.0.1
|
||||
- numpy==1.26.4
|
||||
- mouse
|
||||
- keyboard
|
||||
- beautifultable
|
||||
@@ -23,15 +21,14 @@ dependencies:
|
||||
- coverage
|
||||
- pytest
|
||||
- pytest-env
|
||||
- pytest-pythonpath
|
||||
- pytest-mock
|
||||
- pyparsing
|
||||
- graphviz
|
||||
- psutil
|
||||
- cryptography
|
||||
- pillow
|
||||
- discord.py
|
||||
- parse
|
||||
- dependencies/tesserocr-2.5.2-cp310-cp310-win_amd64.whl
|
||||
- typing_extensions
|
||||
- colorama
|
||||
- dataclasses-json
|
||||
- dataclasses-json
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
@echo off
|
||||
:: Shared conda environment detection for Botty
|
||||
:: Source this file from your .bat scripts: call find_python.bat
|
||||
:: On return, %PYTHON% will be set to the botty env python.exe or the script will exit.
|
||||
|
||||
if defined PYTHON goto :_find_python_done
|
||||
|
||||
for %%C in (
|
||||
"%LOCALAPPDATA%\miniforge3\envs\botty\python.exe"
|
||||
"%LOCALAPPDATA%\miniconda3\envs\botty\python.exe"
|
||||
"%USERPROFILE%\miniforge3\envs\botty\python.exe"
|
||||
"%USERPROFILE%\miniconda3\envs\botty\python.exe"
|
||||
"%USERPROFILE%\anaconda3\envs\botty\python.exe"
|
||||
"%USERPROFILE%\.conda\envs\botty\python.exe"
|
||||
"C:\ProgramData\miniforge3\envs\botty\python.exe"
|
||||
"C:\ProgramData\miniconda3\envs\botty\python.exe"
|
||||
"C:\ProgramData\anaconda3\envs\botty\python.exe"
|
||||
) do (
|
||||
if exist %%C (
|
||||
set "PYTHON=%%~C"
|
||||
goto :_find_python_done
|
||||
)
|
||||
)
|
||||
|
||||
echo ERROR: Could not find botty conda environment.
|
||||
echo Run install.bat first.
|
||||
pause
|
||||
exit /b 1
|
||||
|
||||
:_find_python_done
|
||||
@@ -0,0 +1,386 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
:: Always run from the folder this script lives in
|
||||
cd /d "%~dp0"
|
||||
|
||||
echo ============================================
|
||||
echo Botty - Dependency Installer
|
||||
echo ============================================
|
||||
echo.
|
||||
|
||||
:: --- Detect Windows version ---
|
||||
for /f "tokens=3" %%A in ('reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion" /v CurrentBuild 2^>nul') do set "WIN_BUILD=%%A"
|
||||
if not defined WIN_BUILD (
|
||||
echo WARNING: Could not detect Windows build number.
|
||||
set "WIN_BUILD=0"
|
||||
)
|
||||
echo Detected Windows build: %WIN_BUILD%
|
||||
set /a "WIN_BUILD_NUM=%WIN_BUILD%" 2>nul || set "WIN_BUILD_NUM=0"
|
||||
if %WIN_BUILD_NUM% GEQ 22000 (
|
||||
echo Detected Windows 11 -- using relative mouse mode ^(no ABSOLUTE flag^)
|
||||
set "WIN11=1"
|
||||
set "REQ_FILE=requirements-win11.txt"
|
||||
set "ENV_FILE=environment-win11.yml"
|
||||
) else (
|
||||
echo Detected Windows 10 or earlier — using absolute mouse mode
|
||||
set "WIN11=0"
|
||||
set "REQ_FILE=requirements-win10.txt"
|
||||
set "ENV_FILE=environment-win10.yml"
|
||||
)
|
||||
if not exist "%REQ_FILE%" set "REQ_FILE=requirements.txt"
|
||||
if not exist "%ENV_FILE%" set "ENV_FILE=environment.yml"
|
||||
echo Requirements profile: %REQ_FILE%
|
||||
echo Environment profile: %ENV_FILE%
|
||||
echo.
|
||||
|
||||
:: --- Find conda ---
|
||||
:: Miniforge3 JustMe default: %LOCALAPPDATA%\miniforge3 (newer) or
|
||||
:: %USERPROFILE%\miniforge3 (older)
|
||||
set "CONDA_EXE="
|
||||
for %%C in (
|
||||
"%LOCALAPPDATA%\miniforge3\Scripts\conda.exe"
|
||||
"%USERPROFILE%\miniforge3\Scripts\conda.exe"
|
||||
"%LOCALAPPDATA%\miniconda3\Scripts\conda.exe"
|
||||
"%USERPROFILE%\miniconda3\Scripts\conda.exe"
|
||||
"%USERPROFILE%\anaconda3\Scripts\conda.exe"
|
||||
"%ProgramData%\miniforge3\Scripts\conda.exe"
|
||||
"%ProgramData%\miniconda3\Scripts\conda.exe"
|
||||
"%ProgramData%\anaconda3\Scripts\conda.exe"
|
||||
) do (
|
||||
if exist %%C (
|
||||
set "CONDA_EXE=%%~C"
|
||||
goto :found_conda
|
||||
)
|
||||
)
|
||||
|
||||
echo conda not found — installing Miniforge3 automatically...
|
||||
echo This is a one-time setup (~100 MB). Please wait.
|
||||
echo.
|
||||
|
||||
:: --- Method 1: winget (cleanest; available on Win10 1709+ and all Win11) ---
|
||||
winget --version >nul 2>&1
|
||||
if %errorlevel% equ 0 (
|
||||
echo Installing via winget...
|
||||
winget install --id CondaForge.Miniforge3 --exact --silent ^
|
||||
--accept-package-agreements --accept-source-agreements
|
||||
if !errorlevel! equ 0 goto :rescan_conda
|
||||
echo winget install failed — falling back to direct download.
|
||||
echo.
|
||||
)
|
||||
|
||||
:: --- Method 2: direct download + silent NSIS install ---
|
||||
:: Uses curl (built into Win10/11) with PowerShell as fallback.
|
||||
:: $ProgressPreference=SilentlyContinue is critical — without it PowerShell's
|
||||
:: progress bar drops download speed from ~50 MB/s to ~1 MB/s.
|
||||
:: start /wait "" is required: plain "installer /S" can return before install
|
||||
:: finishes. /D= intentionally omitted — NSIS /D= breaks on paths with spaces.
|
||||
set "MF_INSTALLER=%TEMP%\Miniforge3-installer.exe"
|
||||
set "MF_URL=https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Windows-x86_64.exe"
|
||||
|
||||
echo Downloading Miniforge3...
|
||||
curl -Lk --progress-bar "%MF_URL%" -o "%MF_INSTALLER%" 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo curl failed, trying PowerShell...
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
|
||||
"$ProgressPreference='SilentlyContinue'; try { Invoke-WebRequest -Uri '%MF_URL%' -OutFile '%MF_INSTALLER%' -UseBasicParsing; exit 0 } catch { Write-Host $_.Exception.Message; exit 1 }"
|
||||
)
|
||||
if not exist "%MF_INSTALLER%" (
|
||||
echo.
|
||||
echo ERROR: Could not download Miniforge3. Check your internet connection, or
|
||||
echo install it manually then re-run install.bat:
|
||||
echo https://github.com/conda-forge/miniforge/releases/latest
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Installing Miniforge3 (this takes ~1 minute)...
|
||||
start /wait "" "%MF_INSTALLER%" /S /InstallationType=JustMe /AddToPath=0 /RegisterPython=0 /NoShortcuts=1 /NoRegistry=1
|
||||
if %errorlevel% neq 0 (
|
||||
echo.
|
||||
echo ERROR: Miniforge3 installer failed (exit code %errorlevel%).
|
||||
echo Try running it manually: %MF_INSTALLER%
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
del /q "%MF_INSTALLER%" >nul 2>&1
|
||||
echo Miniforge3 installed.
|
||||
|
||||
:rescan_conda
|
||||
echo.
|
||||
|
||||
:: Re-scan for conda now that Miniforge is installed
|
||||
for %%C in (
|
||||
"%LOCALAPPDATA%\miniforge3\Scripts\conda.exe"
|
||||
"%USERPROFILE%\miniforge3\Scripts\conda.exe"
|
||||
"%LOCALAPPDATA%\miniconda3\Scripts\conda.exe"
|
||||
"%USERPROFILE%\miniconda3\Scripts\conda.exe"
|
||||
"%USERPROFILE%\anaconda3\Scripts\conda.exe"
|
||||
"%ProgramData%\miniforge3\Scripts\conda.exe"
|
||||
"%ProgramData%\miniconda3\Scripts\conda.exe"
|
||||
"%ProgramData%\anaconda3\Scripts\conda.exe"
|
||||
) do (
|
||||
if exist %%C (
|
||||
set "CONDA_EXE=%%~C"
|
||||
goto :found_conda
|
||||
)
|
||||
)
|
||||
echo ERROR: Miniforge3 installed but conda.exe still not found. Please restart
|
||||
echo the installer or contact support.
|
||||
pause
|
||||
exit /b 1
|
||||
|
||||
:found_conda
|
||||
echo Found conda: %CONDA_EXE%
|
||||
echo.
|
||||
|
||||
:: --- Test conda works ---
|
||||
echo Testing conda...
|
||||
"%CONDA_EXE%" --version >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo.
|
||||
echo ERROR: conda found but failed to run. Your conda installation may be broken.
|
||||
echo Try re-installing Miniforge: https://github.com/conda-forge/miniforge
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo conda is working.
|
||||
echo.
|
||||
|
||||
:: --- Find botty Python ---
|
||||
set "PYTHON="
|
||||
for %%C in (
|
||||
"%LOCALAPPDATA%\miniforge3\envs\botty\python.exe"
|
||||
"%LOCALAPPDATA%\miniconda3\envs\botty\python.exe"
|
||||
"%USERPROFILE%\miniforge3\envs\botty\python.exe"
|
||||
"%USERPROFILE%\miniconda3\envs\botty\python.exe"
|
||||
"%USERPROFILE%\anaconda3\envs\botty\python.exe"
|
||||
"%ProgramData%\miniforge3\envs\botty\python.exe"
|
||||
"%ProgramData%\miniconda3\envs\botty\python.exe"
|
||||
"%ProgramData%\anaconda3\envs\botty\python.exe"
|
||||
) do (
|
||||
if exist %%C (
|
||||
set "PYTHON=%%~C"
|
||||
goto :check_env
|
||||
)
|
||||
)
|
||||
|
||||
:: --- Create botty env ---
|
||||
echo Creating 'botty' conda environment...
|
||||
"%CONDA_EXE%" env create -f "%ENV_FILE%"
|
||||
if %errorlevel% neq 0 (
|
||||
echo.
|
||||
echo ERROR: conda env create failed. See output above.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:check_env
|
||||
:: Re-scan for Python after env creation
|
||||
set "PYTHON="
|
||||
for %%C in (
|
||||
"%LOCALAPPDATA%\miniforge3\envs\botty\python.exe"
|
||||
"%LOCALAPPDATA%\miniconda3\envs\botty\python.exe"
|
||||
"%USERPROFILE%\miniforge3\envs\botty\python.exe"
|
||||
"%USERPROFILE%\miniconda3\envs\botty\python.exe"
|
||||
"%USERPROFILE%\anaconda3\envs\botty\python.exe"
|
||||
"%ProgramData%\miniforge3\envs\botty\python.exe"
|
||||
"%ProgramData%\miniconda3\envs\botty\python.exe"
|
||||
"%ProgramData%\anaconda3\envs\botty\python.exe"
|
||||
) do (
|
||||
if exist %%C (
|
||||
set "PYTHON=%%~C"
|
||||
goto :env_ready
|
||||
)
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ERROR: botty env was created but python.exe was not found.
|
||||
pause
|
||||
exit /b 1
|
||||
|
||||
:env_ready
|
||||
echo Botty Python: %PYTHON%
|
||||
|
||||
:: --- Install pip requirements ---
|
||||
:: --progress-bar off: pip's progress bar uses \r to redraw in place; when this
|
||||
:: script's output is redirected to a file (e.g. run_install_capture.bat) those
|
||||
:: redraws become millions of lines and the log balloons to many GB. Turning the
|
||||
:: bar off keeps the install log small without hiding warnings/errors.
|
||||
echo.
|
||||
echo Installing Botty requirements from %REQ_FILE%...
|
||||
"%CONDA_EXE%" run -n botty python -m pip install --progress-bar off -r "%REQ_FILE%"
|
||||
if %errorlevel% neq 0 (
|
||||
echo.
|
||||
echo ERROR: pip requirements install failed for %REQ_FILE%.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Botty osdetect:
|
||||
"%CONDA_EXE%" run -n botty python -c "import sys; sys.path.insert(0, 'src'); from utils.os_detect import describe_current_os; print(describe_current_os())"
|
||||
|
||||
:: --- OCR setup ---
|
||||
:: Two OCR backends, tried in order at runtime:
|
||||
:: 1. tesserocr — C extension calling tesseract DLLs directly (fast)
|
||||
:: 2. pytesseract — subprocess calling tesseract.exe (always works if exe exists)
|
||||
:: At least one must work. Install.bat sets up both and reports which is ready.
|
||||
echo.
|
||||
echo Setting up OCR...
|
||||
|
||||
:: Find the botty env directory
|
||||
set "BOTTY_ENV_DIR="
|
||||
for %%C in (
|
||||
"%LOCALAPPDATA%\miniforge3\envs\botty"
|
||||
"%LOCALAPPDATA%\miniconda3\envs\botty"
|
||||
"%USERPROFILE%\miniforge3\envs\botty"
|
||||
"%USERPROFILE%\miniconda3\envs\botty"
|
||||
"%USERPROFILE%\anaconda3\envs\botty"
|
||||
"%ProgramData%\miniforge3\envs\botty"
|
||||
"%ProgramData%\miniconda3\envs\botty"
|
||||
"%ProgramData%\anaconda3\envs\botty"
|
||||
) do (
|
||||
if exist %%C (
|
||||
set "BOTTY_ENV_DIR=%%~C"
|
||||
goto :env_found
|
||||
)
|
||||
)
|
||||
echo ERROR: Could not find botty env directory.
|
||||
pause
|
||||
exit /b 1
|
||||
|
||||
:env_found
|
||||
set "TESS_PATH=%BOTTY_ENV_DIR%\Library\bin;%BOTTY_ENV_DIR%\Library\lib;%BOTTY_ENV_DIR%\DLLs;%BOTTY_ENV_DIR%\Scripts"
|
||||
|
||||
:: --- Backend 1: tesserocr (fast path, best-effort) ---
|
||||
:: tesserocr uses the C++ API of tesseract and needs an MSVC-compiled tesseract DLL.
|
||||
:: The bundled dependencies folder provides:
|
||||
:: tesseract52.dll — MSVC tesseract 5.2.0 (conda-forge, compiled vs2019)
|
||||
:: tesserocr.cp310-...pyd — MSVC tesserocr 2.5.2 linked against tesseract52.dll
|
||||
:: (both require leptonica-1.78.0.dll from the conda tesseract=4.x install below)
|
||||
:: If any step fails the bot still works via pytesseract below.
|
||||
|
||||
:: Remove any existing pip/wheel tesserocr
|
||||
"%PYTHON%" -m pip uninstall tesserocr -y >nul 2>&1
|
||||
|
||||
:: conda tesseract 4.x provides leptonica-1.78.0.dll (MSVC) which tesseract52.dll needs
|
||||
"%CONDA_EXE%" install -n botty "tesseract=4.*" -c conda-forge -y >nul 2>&1
|
||||
|
||||
:: conda tesseract.exe crashes on Win10 and Win11 — disable it, keep the DLLs
|
||||
if exist "%BOTTY_ENV_DIR%\Library\bin\tesseract.exe" (
|
||||
rename "%BOTTY_ENV_DIR%\Library\bin\tesseract.exe" "tesseract_disabled.exe" >nul 2>&1
|
||||
)
|
||||
|
||||
:: Deploy the MSVC tesseract52.dll (tesserocr.pyd imports exactly this name)
|
||||
if exist "dependencies\tesseract52.dll" (
|
||||
copy /y "dependencies\tesseract52.dll" "%BOTTY_ENV_DIR%\Library\bin\tesseract52.dll" >nul
|
||||
)
|
||||
|
||||
:: Deploy the conda-forge tesserocr .pyd (compiled against tesseract52.dll)
|
||||
if exist "dependencies\tesserocr.cp310-win_amd64.pyd" (
|
||||
copy /y "dependencies\tesserocr.cp310-win_amd64.pyd" "%BOTTY_ENV_DIR%\lib\site-packages\tesserocr.cp310-win_amd64.pyd" >nul
|
||||
) else if exist "dependencies\tesserocr-2.5.2-cp310-cp310-win_amd64.whl" (
|
||||
"%CONDA_EXE%" run -n botty python -m pip install --quiet --force-reinstall --no-deps "dependencies\tesserocr-2.5.2-cp310-cp310-win_amd64.whl" >nul 2>&1
|
||||
)
|
||||
|
||||
:: --- Backend 2: pytesseract (reliable fallback) ---
|
||||
:: Needs tesseract.exe from winget. Works on any Python version.
|
||||
:: On Win10: winget may not be available -- if install fails, offer manual link.
|
||||
"%CONDA_EXE%" run -n botty python -m pip install --progress-bar off pytesseract >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo WARNING: Could not install pytesseract Python wrapper.
|
||||
)
|
||||
|
||||
if not exist "C:\Program Files\Tesseract-OCR\tesseract.exe" (
|
||||
echo Installing Tesseract OCR via winget...
|
||||
winget install --id tesseract-ocr.tesseract --silent --accept-package-agreements --accept-source-agreements 2>nul
|
||||
if !errorlevel! neq 0 (
|
||||
echo winget failed -- on Windows 10 you may need to install manually:
|
||||
echo https://github.com/tesseract-ocr/tesseract/releases
|
||||
echo Download the win64 installer, run it, keep the default install path.
|
||||
)
|
||||
)
|
||||
|
||||
:: --- Verify OCR: at least one backend must work ---
|
||||
echo.
|
||||
echo Checking OCR backends...
|
||||
set "PATH=%TESS_PATH%;%PATH%"
|
||||
set "OCR_READY=0"
|
||||
|
||||
"%CONDA_EXE%" run -n botty python -c "import os; os.add_dll_directory(r'%BOTTY_ENV_DIR%\Library\bin'); from tesserocr import PyTessBaseAPI, OEM; PyTessBaseAPI(psm=7,oem=OEM.LSTM_ONLY,path='assets/tessdata',lang='hover-eng_inconsolata_inv_th_fast').End()" >nul 2>&1
|
||||
if %errorlevel% == 0 (
|
||||
echo tesserocr: OK ^(fast path^)
|
||||
set "OCR_READY=1"
|
||||
) else (
|
||||
echo tesserocr: not available ^(DLL issue -- bot will use pytesseract instead^)
|
||||
)
|
||||
|
||||
if exist "C:\Program Files\Tesseract-OCR\tesseract.exe" (
|
||||
"%CONDA_EXE%" run -n botty python -c "import pytesseract; pytesseract.pytesseract.tesseract_cmd=r'C:\Program Files\Tesseract-OCR\tesseract.exe'; pytesseract.get_tesseract_version()" >nul 2>&1
|
||||
if !errorlevel! == 0 (
|
||||
echo pytesseract: OK ^(reliable fallback^)
|
||||
set "OCR_READY=1"
|
||||
) else (
|
||||
echo pytesseract: tesseract.exe found but call failed
|
||||
)
|
||||
) else (
|
||||
echo pytesseract: tesseract.exe not found -- install from link above if needed
|
||||
)
|
||||
|
||||
if "%OCR_READY%"=="0" (
|
||||
echo.
|
||||
echo WARNING: No OCR backend is working. Item hover text will not be read.
|
||||
echo Install Tesseract OCR: https://github.com/tesseract-ocr/tesseract/releases
|
||||
echo Then re-run install.bat.
|
||||
)
|
||||
|
||||
:: --- Verify all dependencies ---
|
||||
echo.
|
||||
echo Verifying all dependencies...
|
||||
set "ALL_OK=1"
|
||||
for %%M in (cv2 mss numpy transitions rapidfuzz pydantic pytesseract yaml) do (
|
||||
set "PATH=%TESS_PATH%;%PATH%"
|
||||
"%CONDA_EXE%" run -n botty python -c "import %%M" >nul 2>&1
|
||||
if !errorlevel! neq 0 (
|
||||
echo FAILED: %%M
|
||||
set "ALL_OK=0"
|
||||
) else (
|
||||
echo %%M: OK
|
||||
)
|
||||
)
|
||||
:: discord.py imports aiohttp which calls ssl.create_default_context() at import
|
||||
:: time. A malformed cert in the Windows store triggers ASN1 NOT_ENOUGH_DATA.
|
||||
:: The bot patches this at runtime via _patch_ssl(); check import with the patch.
|
||||
set "PATH=%TESS_PATH%;%PATH%"
|
||||
"%CONDA_EXE%" run -n botty python -c "import ssl; ssl.SSLContext.load_default_certs = lambda *a, **kw: None; import discord" >nul 2>&1
|
||||
if !errorlevel! neq 0 (
|
||||
echo FAILED: discord
|
||||
set "ALL_OK=0"
|
||||
) else (
|
||||
echo discord: OK
|
||||
)
|
||||
if "%OCR_READY%"=="0" set "ALL_OK=0"
|
||||
|
||||
if "%ALL_OK%"=="1" (
|
||||
echo.
|
||||
echo All dependencies verified.
|
||||
) else (
|
||||
echo.
|
||||
echo WARNING: Some dependencies failed above.
|
||||
echo This is often a PATH issue. Try running from a fresh CMD/PowerShell:
|
||||
echo conda activate botty
|
||||
echo python -c "import cv2"
|
||||
echo.
|
||||
)
|
||||
|
||||
:install_done
|
||||
echo.
|
||||
echo ============================================
|
||||
echo Installation complete!
|
||||
echo Run botty with: run_botty.bat
|
||||
echo ============================================
|
||||
echo.
|
||||
@@ -0,0 +1,45 @@
|
||||
[project]
|
||||
name = "botty"
|
||||
version = "0.8.1"
|
||||
description = "Pixelbot for Diablo 2 Resurrected"
|
||||
requires-python = ">=3.10,<3.11"
|
||||
dependencies = [
|
||||
"pywin32",
|
||||
"opencv-python==4.5.5.64",
|
||||
"transitions",
|
||||
"mss==7.0.1",
|
||||
"numpy==1.26.4",
|
||||
"pydantic",
|
||||
"PyYAML",
|
||||
"beautifultable",
|
||||
"pytweening",
|
||||
"requests",
|
||||
"rapidfuzz==2.15.1",
|
||||
"pyparsing",
|
||||
"graphviz",
|
||||
"psutil",
|
||||
"cryptography",
|
||||
"pillow",
|
||||
"discord.py",
|
||||
"parse",
|
||||
"typing_extensions",
|
||||
"colorama",
|
||||
"dataclasses-json",
|
||||
# tesserocr must be installed separately - see development.md
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
botty-next = "botty_next.cli:main"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest",
|
||||
"pytest-env",
|
||||
# pytest-pythonpath is deprecated; use [tool.pytest.ini_options] pythonpath instead
|
||||
"pytest-mock",
|
||||
"coverage",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = ["./src"]
|
||||
testpaths = ["test"]
|
||||
@@ -1,5 +1,6 @@
|
||||
[pytest]
|
||||
pythonpath =
|
||||
src
|
||||
env =
|
||||
PYTHONPATH=./src
|
||||
RUN_ENV=test
|
||||
python_paths = ./src
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Botty dependency profile for Windows 10.
|
||||
#
|
||||
# Windows 10 uses Botty's absolute SendInput mouse mode.
|
||||
# Keep package versions aligned with requirements.txt and environment.yml.
|
||||
-r requirements.txt
|
||||
@@ -0,0 +1,6 @@
|
||||
# Botty dependency profile for Windows 11.
|
||||
#
|
||||
# Windows 11 uses Botty's relative SendInput mouse mode because absolute
|
||||
# SendInput movement is unreliable on recent Windows 11 builds.
|
||||
# Keep package versions aligned with requirements.txt and environment.yml.
|
||||
-r requirements.txt
|
||||
@@ -0,0 +1,49 @@
|
||||
aiohappyeyeballs==2.6.2
|
||||
aiohttp==3.14.1
|
||||
aiosignal==1.4.0
|
||||
async-timeout==5.0.1
|
||||
attrs==26.1.0
|
||||
beautifultable==1.1.0
|
||||
certifi==2026.6.17
|
||||
cffi==2.0.0
|
||||
charset-normalizer==3.4.7
|
||||
colorama==0.4.6
|
||||
cryptography==49.0.0
|
||||
dataclasses-json==0.6.7
|
||||
discord.py==2.7.1
|
||||
frozenlist==1.5.0
|
||||
idna==3.10
|
||||
keyboard==0.13.5; sys_platform=="win32"
|
||||
marshmallow==3.26.2
|
||||
mouse==0.7.1; sys_platform=="win32"
|
||||
mss==7.0.1
|
||||
multidict==6.7.1
|
||||
numpy==1.26.4
|
||||
opencv-python==4.5.5.64
|
||||
packaging==25.0
|
||||
parse==1.22.1
|
||||
pillow==12.2.0
|
||||
psutil==7.2.2
|
||||
pycparser==2.22
|
||||
pyparsing==3.3.2
|
||||
pytesseract==0.3.13
|
||||
pytweening==1.2.0
|
||||
PyYAML==6.0.3
|
||||
rapidfuzz==3.12.1
|
||||
requests==2.34.2
|
||||
six==1.17.0
|
||||
transitions==0.9.3
|
||||
typing-inspect==0.9.0
|
||||
typing_extensions==4.15.0
|
||||
urllib3==2.7.0
|
||||
wcwidth==0.2.13
|
||||
python-dotenv==1.2.2
|
||||
graphviz==0.21
|
||||
Pygments==2.20.0
|
||||
pydantic==2.12.5
|
||||
coverage==7.14.1
|
||||
pytest==9.1.1
|
||||
pytest-env==1.6.0
|
||||
pytest-mock==3.15.1
|
||||
pytest-pythonpath==0.7.3
|
||||
pyinstaller==6.21.0
|
||||
@@ -0,0 +1,57 @@
|
||||
# ruff.toml — botty project linting configuration
|
||||
|
||||
target-version = "py310"
|
||||
line-length = 120
|
||||
|
||||
[lint]
|
||||
select = [
|
||||
"E", # pycodestyle errors
|
||||
"F", # Pyflakes
|
||||
"I", # isort
|
||||
"UP", # pyupgrade
|
||||
"SIM", # simplify
|
||||
"RUF", # ruff-specific rules
|
||||
]
|
||||
ignore = [
|
||||
"E501", # line too long (handled by line-length)
|
||||
"F401", # unused imports (common in __init__.py and game bots)
|
||||
"F403", # star imports (used for convenience modules)
|
||||
"E722", # bare except (intentional in game automation)
|
||||
"RUF012", # mutable class attributes (State machine patterns)
|
||||
]
|
||||
|
||||
[lint.per-file-ignores]
|
||||
"__init__.py" = ["F401", "F403"]
|
||||
"**/test/**/*.py" = ["F401"]
|
||||
"**/mocks/**/*.py" = ["F401"]
|
||||
"tools/*.py" = ["E402"]
|
||||
"build.py" = ["E402"]
|
||||
|
||||
[lint.isort]
|
||||
force-single-line = false
|
||||
known-first-party = [
|
||||
"screen",
|
||||
"config",
|
||||
"logger",
|
||||
"bot",
|
||||
"pather",
|
||||
"template_finder",
|
||||
"input_layer",
|
||||
"ui",
|
||||
"ui_manager",
|
||||
"game_controller",
|
||||
"game_stats",
|
||||
"health_manager",
|
||||
"death_manager",
|
||||
"target_detect",
|
||||
"char",
|
||||
"shop",
|
||||
"item",
|
||||
"transmute",
|
||||
"bnip",
|
||||
"d2r_image",
|
||||
"messages",
|
||||
"npc_manager",
|
||||
"utils",
|
||||
"version",
|
||||
]
|
||||
@@ -1,34 +1,4 @@
|
||||
@ECHO OFF
|
||||
CLS
|
||||
:MENU
|
||||
ECHO.
|
||||
ECHO ...............................................
|
||||
ECHO PRESS 1, 2, 3, 4 or 5 to select your task
|
||||
ECHO ...............................................
|
||||
ECHO.
|
||||
ECHO 1 - Install Env
|
||||
ECHO 2 - Update Env
|
||||
ECHO 3 - Compile
|
||||
ECHO 4 - Run Botty
|
||||
Echo 5 - Exit
|
||||
ECHO.
|
||||
SET /P M=Type 1, 2, 3, 4 or 5 then press ENTER:
|
||||
IF %M%==1 GOTO INSTALL
|
||||
IF %M%==2 GOTO UPDATE
|
||||
IF %M%==3 GOTO COMPILE
|
||||
IF %M%==4 GOTO RUN
|
||||
IF %M%==5 GOTO EXIT
|
||||
:INSTALL
|
||||
start cmd /c conda env create environment.yml
|
||||
GOTO MENU
|
||||
:UPDATE
|
||||
start cmd /c conda env update environment.yml
|
||||
GOTO MENU
|
||||
:COMPILE
|
||||
python ./build.py
|
||||
GOTO MENU
|
||||
:RUN
|
||||
conda activate botty && python src/main.py
|
||||
GOTO MENU
|
||||
:EXIT
|
||||
EXIT
|
||||
@echo off
|
||||
REM Legacy launcher - redirects to the new run_botty.bat
|
||||
cd /d "%~dp0"
|
||||
call run_botty.bat
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set "BOTTY_DIR=%~dp0"
|
||||
cd /d "%BOTTY_DIR%"
|
||||
|
||||
call "%BOTTY_DIR%find_python.bat"
|
||||
|
||||
:: Derive the conda env root from python.exe:
|
||||
:: envs\botty\python.exe -> envs\botty
|
||||
for %%F in ("%PYTHON%") do set "_ENV=%%~dpF"
|
||||
set "_ENV=%_ENV:~0,-1%"
|
||||
|
||||
:: Match an activated conda environment closely enough for native DLL loading.
|
||||
set "PATH=%_ENV%;%_ENV%\Scripts;%_ENV%\Library\bin;%_ENV%\Library\mingw-w64\bin;%_ENV%\Library\usr\bin;%_ENV%\DLLs;%PATH%"
|
||||
set "CONDA_PREFIX=%_ENV%"
|
||||
set "PYTHONUTF8=1"
|
||||
set "PYTHONIOENCODING=utf-8"
|
||||
set "SSL_CERT_DIR="
|
||||
:: Use winget tesseract 5.5.0 (conda tesseract crashes with access violation)
|
||||
set "TESSDATA_PREFIX=%_ENV%\Library\share"
|
||||
set "PYTESSERACT_TESSERACT_CMD=C:\Program Files\Tesseract-OCR\tesseract.exe"
|
||||
|
||||
echo Launching Botty ...
|
||||
"%PYTHON%" "%BOTTY_DIR%src\main.py"
|
||||
pause
|
||||
@@ -0,0 +1,35 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
rem Chipped-only GEMS-tab converter.
|
||||
rem Requirements:
|
||||
rem - D2R is open
|
||||
rem - character is in-game
|
||||
rem - stash UI is open on or near the GEMS tab
|
||||
rem This script does not open or click the character-inventory cube.
|
||||
|
||||
set "BOTTY_DIR=%~dp0"
|
||||
cd /d "%BOTTY_DIR%"
|
||||
|
||||
call "%BOTTY_DIR%find_python.bat"
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
for %%F in ("%PYTHON%") do set "_ENV=%%~dpF"
|
||||
set "_ENV=%_ENV:~0,-1%"
|
||||
|
||||
set "PATH=%_ENV%;%_ENV%\Scripts;%_ENV%\Library\bin;%_ENV%\Library\mingw-w64\bin;%_ENV%\Library\usr\bin;%_ENV%\DLLs;%PATH%"
|
||||
set "CONDA_PREFIX=%_ENV%"
|
||||
set "PYTHONUTF8=1"
|
||||
set "PYTHONIOENCODING=utf-8"
|
||||
set "SSL_CERT_DIR="
|
||||
set "TESSDATA_PREFIX=%_ENV%\Library\share"
|
||||
set "PYTESSERACT_TESSERACT_CMD=C:\Program Files\Tesseract-OCR\tesseract.exe"
|
||||
|
||||
echo Running chipped-only GEMS converter...
|
||||
echo Before/after screenshots and a JSON summary will be written to fixtures\screenshots.
|
||||
echo.
|
||||
|
||||
"%PYTHON%" "%BOTTY_DIR%tools\convert_chipped_gems.py" %*
|
||||
set "RC=%ERRORLEVEL%"
|
||||
pause
|
||||
exit /b %RC%
|
||||
@@ -0,0 +1,31 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set "BOTTY_DIR=%~dp0"
|
||||
cd /d "%BOTTY_DIR%"
|
||||
|
||||
call "%BOTTY_DIR%find_python.bat"
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
for %%F in ("%PYTHON%") do set "_ENV=%%~dpF"
|
||||
set "_ENV=%_ENV:~0,-1%"
|
||||
|
||||
set "PATH=%_ENV%;%_ENV%\Scripts;%_ENV%\Library\bin;%_ENV%\Library\mingw-w64\bin;%_ENV%\Library\usr\bin;%_ENV%\DLLs;%PATH%"
|
||||
set "CONDA_PREFIX=%_ENV%"
|
||||
set "PYTHONUTF8=1"
|
||||
set "PYTHONIOENCODING=utf-8"
|
||||
set "SSL_CERT_DIR="
|
||||
set "TESSDATA_PREFIX=%_ENV%\Library\share"
|
||||
set "PYTESSERACT_TESSERACT_CMD=C:\Program Files\Tesseract-OCR\tesseract.exe"
|
||||
|
||||
echo Running gem transmute testbed...
|
||||
echo Usage examples:
|
||||
echo run_gems_all.bat
|
||||
echo run_gems_all.bat flawless
|
||||
echo run_gems_all.bat flawless diamond --max 1
|
||||
echo.
|
||||
|
||||
"%PYTHON%" "%BOTTY_DIR%tools\testbed.py" gems_all %*
|
||||
set "RC=%ERRORLEVEL%"
|
||||
pause
|
||||
exit /b %RC%
|
||||
@@ -0,0 +1,2 @@
|
||||
@echo off
|
||||
call "%~dp0install.bat" > "%~dp0install_log.txt" 2>&1
|
||||
@@ -0,0 +1,8 @@
|
||||
@echo off
|
||||
setlocal
|
||||
set "BOTTY_DIR=%~dp0"
|
||||
cd /d "%BOTTY_DIR%"
|
||||
|
||||
call "%BOTTY_DIR%find_python.bat"
|
||||
|
||||
%PYTHON% "%BOTTY_DIR%quest_debug.py"
|
||||