Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1386afc7da | ||
|
|
88d325cf8f | ||
|
|
74ebef9b22 | ||
|
|
64578db453 | ||
|
|
3d0e102afc | ||
|
|
f9f26f36c3 | ||
|
|
ce5816a130 | ||
|
|
644c2a106c | ||
|
|
4b003bd95e | ||
|
|
f2d23e511d | ||
|
|
07be1a7b9c | ||
|
|
8178104612 | ||
|
|
23b0f0f66b | ||
|
|
2d69b246d9 | ||
|
|
4f0f3df8bb | ||
|
|
2c25b67cf9 | ||
|
|
f6e700c921 | ||
|
|
23e7e54f8d | ||
|
|
b52f7ebca9 | ||
|
|
329b7e77d9 | ||
|
|
5ab9d1c68a | ||
|
|
15a90bfd47 | ||
|
|
17f32e65b2 | ||
|
|
b397d07baf | ||
|
|
eee99c069b | ||
|
|
81ee6557f4 | ||
|
|
f484a9bef1 | ||
|
|
6d19819de5 | ||
|
|
71311cb584 | ||
|
|
5c4f62ac25 | ||
|
|
869de31315 | ||
|
|
6f6c663b43 | ||
|
|
fdd0fc2230 | ||
|
|
5d5a9c2a63 | ||
|
|
eb75fc9aee | ||
|
|
600904e6bc | ||
|
|
ce821695e8 | ||
|
|
455fe8e64f | ||
|
|
f4bc3b7401 | ||
|
|
b528957c40 | ||
|
|
f8c9d81f52 | ||
|
|
1a5a261f7c | ||
|
|
95e019e88d | ||
|
|
466197fd8b | ||
|
|
4a4bf0300a | ||
|
|
953b9aedb3 | ||
|
|
2ce9636d3a | ||
|
|
b03b2fd226 | ||
|
|
d96eb2d34c | ||
|
|
5f2149ed1e | ||
|
|
a7509c90e7 | ||
|
|
8b48b583b9 | ||
|
|
58f761c372 | ||
|
|
36d364517b | ||
|
|
869eeb5539 | ||
|
|
b2afbaab93 | ||
|
|
9512856cfc | ||
|
|
593e509bc6 | ||
|
|
68ed191efc | ||
|
|
77925964a0 | ||
|
|
665e6b6ce4 | ||
|
|
51306f3067 | ||
|
|
c47d9573ad | ||
|
|
bbe5395f4a | ||
|
|
6f7a9b681d | ||
|
|
ed0a8270cf | ||
|
|
4b3a47b13c | ||
|
|
6f4f4ca472 | ||
|
|
55f7d86b3d | ||
|
|
01f03ea9af | ||
|
|
82ac44c4c1 | ||
|
|
65ee5a6b5c | ||
|
|
b7c83e6208 | ||
|
|
f2c639fc64 | ||
|
|
0978258513 | ||
|
|
b243aec74f | ||
|
|
ec5a7e4292 | ||
|
|
4852ef9e3a | ||
|
|
d7a1d65348 | ||
|
|
758a0a3db8 | ||
|
|
b27911adae | ||
|
|
43e5a8ecab | ||
|
|
b52ed15079 | ||
|
|
c82051f276 | ||
|
|
6fc80f0a96 | ||
|
|
2da55e138c | ||
|
|
5cbf7ea366 | ||
|
|
a2d27fd4c3 | ||
|
|
be446fc9d9 | ||
|
|
4b93a50927 | ||
|
|
76cbe0abbc | ||
|
|
a54b674ce6 | ||
|
|
11440a3d24 | ||
|
|
5f478a6884 | ||
|
|
fb2edcf70a | ||
|
|
ed2a921d9e | ||
|
|
ba8a412309 | ||
|
|
5d42a46891 | ||
|
|
c13367f221 | ||
|
|
c94be67727 | ||
|
|
1e8bb16a22 | ||
|
|
3e87fe9d6f | ||
|
|
b517a80bea | ||
|
|
b2938e6297 | ||
|
|
3a96bf69cc | ||
|
|
429a1cd4ae | ||
|
|
dab2fd9259 | ||
|
|
8048d87222 | ||
|
|
edcefbe62c | ||
|
|
aea75aa417 | ||
|
|
30d407e978 | ||
|
|
8df42bbcb1 | ||
|
|
da396208f0 | ||
|
|
466c698b1d | ||
|
|
58bc20ce0e | ||
|
|
25fdd5485d | ||
|
|
bfccf60a07 | ||
|
|
8be1223e32 | ||
|
|
df1259f87c | ||
|
|
1ca3578e06 | ||
|
|
3c70214ca9 | ||
|
|
8c44d7ad17 | ||
|
|
4199705fe8 | ||
|
|
7141e671d1 | ||
|
|
c20ec7343c | ||
|
|
9a714b969e | ||
|
|
d518389493 | ||
|
|
c6f81c70a6 | ||
|
|
70659b7544 | ||
|
|
aa56bbc484 | ||
|
|
901592a54a | ||
|
|
a51db00a15 | ||
|
|
0197298e31 | ||
|
|
a4dada9237 | ||
|
|
6b2f719605 | ||
|
|
ed70c8e688 | ||
|
|
f68ebf0861 | ||
|
|
39788df29c | ||
|
|
f1ea45ce2a | ||
|
|
31a0579f65 | ||
|
|
ad8ea13444 | ||
|
|
5df97c4a7e | ||
|
|
f5538916fe | ||
|
|
d7be49edd7 | ||
|
|
6e10c549e4 | ||
|
|
656e52ffff | ||
|
|
8c9e98ebd8 | ||
|
|
da09cb820a | ||
|
|
d45d974280 | ||
|
|
613d4f0ff3 | ||
|
|
8f4942a3a2 | ||
|
|
ef34efdd54 | ||
|
|
9886f2ee96 | ||
|
|
3e4c0d8fa5 | ||
|
|
f72856a8bd | ||
|
|
980a8f5036 | ||
|
|
9f1a4772cd | ||
|
|
c68d92c8e1 | ||
|
|
076737672e | ||
|
|
689a93a3f7 | ||
|
|
9ed7869ddd | ||
|
|
c62c237442 | ||
|
|
4e10dbb2c7 | ||
|
|
197ff41e69 | ||
|
|
5089abaca6 | ||
|
|
8469d51f0c | ||
|
|
9b2d52a39b | ||
|
|
d95b581dae | ||
|
|
759c4fff82 | ||
|
|
d5bdb769b3 | ||
|
|
8a1a15d77d | ||
|
|
24d2da8e67 | ||
|
|
6d1779ea5f | ||
|
|
0cbc5f759e | ||
|
|
ce1d99bfd8 | ||
|
|
257d687166 | ||
|
|
cfc192e1dc | ||
|
|
161b9226fb | ||
|
|
46b8d9bf17 | ||
|
|
9faa3fd25a | ||
|
|
bca0471d9f | ||
|
|
5610490db6 | ||
|
|
5efebd0bb7 | ||
|
|
81bc9028d1 | ||
|
|
3de9f6db93 | ||
|
|
0f08cd8e93 |
@@ -1,52 +1,91 @@
|
||||
name: Botty - CI
|
||||
on: [pull_request]
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main, mine]
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
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
|
||||
use-only-tar-bz2: true
|
||||
|
||||
- name: Download test assets
|
||||
shell: powershell
|
||||
run: |
|
||||
C:\Miniconda\condabin\conda.bat init powershell
|
||||
set PYTHONPATH=./src
|
||||
- name: Pytest & Coverage
|
||||
C:\Miniconda\condabin\conda.bat activate botty
|
||||
python -c "import utils.download_test_assets"
|
||||
|
||||
- name: Pytest
|
||||
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
|
||||
python -m pytest test/smoke_test.py test/nip/ test/game_stats_test.py test/closest_non_hud_test.py test/transmute/ -v -s
|
||||
|
||||
- name: Coverage
|
||||
shell: powershell
|
||||
run: |
|
||||
C:\Miniconda\condabin\conda.bat activate botty
|
||||
coverage xml
|
||||
|
||||
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.yml
|
||||
use-only-tar-bz2: true
|
||||
|
||||
- name: Build exe
|
||||
shell: powershell
|
||||
env:
|
||||
BOTTY_NO_RENAME: '1'
|
||||
run: |
|
||||
C:\Miniconda\condabin\conda.bat activate botty
|
||||
python build.py --conda_path C:\Miniconda
|
||||
|
||||
- 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
|
||||
|
||||
- name: Upload to Release
|
||||
if: github.event_name == 'release'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
BOTTY_DIR=$(ls -d botty_v* | head -1)
|
||||
gh release upload "${{ github.event.release.tag_name }}" "${BOTTY_DIR}.zip" --clobber
|
||||
|
||||
@@ -24,9 +24,8 @@ test/assets/
|
||||
htmlcov/
|
||||
coverage.xml
|
||||
utils/live-view/
|
||||
config/nip/*
|
||||
!config/nip/.gitkeep
|
||||
|
||||
config/bnip/*
|
||||
!config/bnip/.gitkeep
|
||||
stats/
|
||||
info_screenshots/
|
||||
item_error_screenshots/
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# Botty Improvements Implementation Plan
|
||||
|
||||
## Status Legend
|
||||
- [ ] Not started
|
||||
- [~] In progress
|
||||
- [x] Done
|
||||
- [-] Cancelled / low priority
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Key Auto-Detection (Issue #940)
|
||||
Read D2R .key file and auto-fill hotkeys.
|
||||
|
||||
- [x] Create src/utils/key_detector.py module
|
||||
- [x] VK code mapping (partial — needs review for accuracy)
|
||||
- [x] Parse .key file (text format: VK action_type param)
|
||||
- [x] Auto-fill empty [char] hotkeys from detected bindings
|
||||
- [x] Auto-fill build-specific skill hotkeys (fohdin, hammerdin, etc.)
|
||||
- [x] Wire into config.py load_data()
|
||||
- [ ] REVIEW: Verify VK_MAP accuracy (D2R uses its own VK offset scheme)
|
||||
- [ ] REVIEW: Skill slot-to-config matching is heuristic — may misassign
|
||||
- [ ] TEST: Verify against actual D2R .key file on user's machine
|
||||
|
||||
## Phase 2: Target Detection False Positives (Issues #959/#964)
|
||||
Health bars and "immune to X" text mistaken for targets.
|
||||
|
||||
- [ ] Analyze current get_visible_targets() in target_detect.py
|
||||
- [ ] Add shape/size filtering: health bars are thin horizontal strips, immune text is small
|
||||
- [ ] Add aspect ratio check: real targets (poison/freeze auras) are roughly circular/elliptical
|
||||
- [ ] Add minimum bounding box height constraint (filter out thin text)
|
||||
- [ ] Optionally: add color temperature check (immune text is yellow/gold, not blue/green)
|
||||
- [ ] Test with screenshots of edge cases
|
||||
|
||||
## Phase 3: Pickit Timing Fix (Issue #939)
|
||||
Items skipped because bot teleports away before grabbing.
|
||||
|
||||
- [ ] Review pickit.py _yoink_item() for timing issues
|
||||
- [ ] Add configurable pickup_delay parameter (current: fixed timing)
|
||||
- [ ] Add retry logic: if item still visible after pickup attempt, re-try
|
||||
- [ ] Add "slow mode" for large/heavy items (framed/magic items may animate longer)
|
||||
- [ ] Ensure bot doesn't teleport until pickup animation completes
|
||||
- [ ] Test: verify no "Attempt to pick xyz" warnings followed by teleport
|
||||
|
||||
## Phase 4: Parallel Template Search (Issue #848)
|
||||
Speed up template_finder.search() with threading.
|
||||
|
||||
- [ ] Add ThreadPoolExecutor-based search_all_parallel()
|
||||
- [ ] Keep existing search() for single-template (no overhead)
|
||||
- [ ] Only parallelize when searching >3 templates simultaneously
|
||||
- [ ] Benchmark: measure speedup on typical 1280x720 grab
|
||||
|
||||
## Phase 5: Async Mouse Moves (Issue #955)
|
||||
Non-blocking mouse movement.
|
||||
|
||||
- [ ] Add async_move() to utils/custom_mouse.py
|
||||
- [ ] Run movement in background thread
|
||||
- [ ] Add is_moving() / wait_for_move() synchronization
|
||||
- [ ] Integrate into game_controller.py for smoother action chains
|
||||
|
||||
## Phase 6: Hardcore Chicken Loop Fix (Issue #942)
|
||||
Prevent infinite death loops on Hardcore characters.
|
||||
|
||||
- [ ] Review death_manager.py chicken logic
|
||||
- [ ] Add max_chicken_count config parameter (default: 3)
|
||||
- [ ] If max chicken count exceeded on HC, exit gracefully instead of re-entering
|
||||
- [ ] Add defensive chicken config option (chicken to TP instead of full chicken)
|
||||
- [ ] Test: verify HC character exits cleanly after N deaths
|
||||
|
||||
## Phase 7: Auto-Label NPCs (Issue #950)
|
||||
Learn vendor identities automatically during gameplay.
|
||||
|
||||
- [ ] During town states, detect NPC name plates via OCR
|
||||
- [ ] Cross-reference detected names with known NPC list
|
||||
- [ ] Auto-capture NPC templates when confidence is high
|
||||
- [ ] Store learned templates in assets/npc/
|
||||
- [ ] This is a long-term feature — lower priority
|
||||
|
||||
---
|
||||
|
||||
## Priority Order (implement in this order)
|
||||
1. **Phase 1** - Key auto-detection (already partially done, needs review + test)
|
||||
2. **Phase 3** - Pickit timing (high impact on loot collection)
|
||||
3. **Phase 2** - Target detection (high impact on kill reliability)
|
||||
4. **Phase 6** - Hardcore chicken fix (safety critical)
|
||||
5. **Phase 4** - Parallel template search (performance)
|
||||
6. **Phase 5** - Async mouse moves (quality of life)
|
||||
7. **Phase 7** - Auto-label NPCs (long-term feature)
|
||||
@@ -0,0 +1,561 @@
|
||||
# Quest Framework + Den of Evil Plan
|
||||
|
||||
## Goal
|
||||
Build a quest automation framework in botty that can interact with D2R NPCs, handle dialogue,
|
||||
track quest progress, and run Den of Evil as the first quest -- all usable by a low-level FoHdin.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
The quest framework is a new subsystem that plugs into the existing botty state machine.
|
||||
It follows the same patterns as existing runs (approach -> battle -> return to town) but adds
|
||||
NPC dialogue interaction and quest state persistence.
|
||||
|
||||
### New files
|
||||
|
||||
```
|
||||
src/quest/
|
||||
__init__.py # Exports
|
||||
quest_manager.py # Quest state machine + persistence (JSON)
|
||||
quest_dialogue.py # OCR-based NPC dialogue interaction
|
||||
quest_items.py # Quest item detection/pickup
|
||||
quest_combat.py # Lightweight combat wrapper (killing trash)
|
||||
a1/
|
||||
__init__.py
|
||||
q_den_of_evil.py # Den of Evil run
|
||||
```
|
||||
|
||||
### Modified files
|
||||
|
||||
```
|
||||
src/npc_manager.py # Add TOWN_MAIDEN NPC constant + templates
|
||||
src/pather.py # Add A1_ROARING_CANYON + DoE entrance locations
|
||||
src/bot.py # Add quest state, transitions, handler
|
||||
src/run/__init__.py # Export DenOfEvil
|
||||
src/town/a1.py # (optional) Add can_do_den_of_evil method
|
||||
config/params.ini # Add run_doe to [routes]
|
||||
config/bnip/ # Add town_maiden.png template
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Foundation
|
||||
|
||||
### 1.1 `src/quest/quest_manager.py`
|
||||
|
||||
Purpose: Track which quests are done, persist between sessions, dispatch to quest modules.
|
||||
|
||||
```python
|
||||
class QuestManager:
|
||||
"""Manages quest state: tracks done/available quests per act, persists to JSON."""
|
||||
|
||||
# Quest definitions per act
|
||||
QUESTS = {
|
||||
"a1": ["den_of_evil"],
|
||||
"a2": [], # future: radament, horadric_staff, etc.
|
||||
...
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._state_file = "config/quest_state.json"
|
||||
self._state = self._load()
|
||||
|
||||
def is_done(self, quest_name: str) -> bool:
|
||||
return self._state.get(quest_name, False)
|
||||
|
||||
def mark_done(self, quest_name: str):
|
||||
self._state[quest_name] = True
|
||||
self._save()
|
||||
|
||||
def mark_all_done(self, act: str):
|
||||
for q in self.QUESTS.get(act, []):
|
||||
self._state[q] = True
|
||||
self._save()
|
||||
|
||||
def next_pending(self, act: str) -> str | None:
|
||||
for q in self.QUESTS.get(act, []):
|
||||
if not self.is_done(q):
|
||||
return q
|
||||
return None
|
||||
|
||||
def all_done(self, act: str) -> bool:
|
||||
return all(self._state.get(q, False) for q in self.QUESTS.get(act, []))
|
||||
|
||||
def _load(self) -> dict:
|
||||
if os.path.exists(self._state_file):
|
||||
with open(self._state_file) as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
def _save(self):
|
||||
with open(self._state_file, "w") as f:
|
||||
json.dump(self._state, f, indent=2)
|
||||
```
|
||||
|
||||
JSON format (config/quest_state.json):
|
||||
```json
|
||||
{
|
||||
"den_of_evil": true,
|
||||
"search_for_smith": true,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 `src/quest/quest_dialogue.py`
|
||||
|
||||
Purpose: Talk to NPCs, read dialogue options via OCR, click the right branch.
|
||||
This is the core of quest automation -- it makes the bot "converse" with NPCs.
|
||||
|
||||
```python
|
||||
class QuestDialogue:
|
||||
"""OCR-based NPC dialogue interaction for quest conversations."""
|
||||
|
||||
# ROI at 1280x720
|
||||
DIALOGUE_TEXT_ROI = (200, 470, 680, 100) # NPC speech text
|
||||
DIALOGUE_OPTIONS_ROI = (200, 560, 680, 140) # Player response buttons
|
||||
DIALOGUE_CLOSE_Y = 670 # Close button area
|
||||
|
||||
@staticmethod
|
||||
def open_dialogue(npc_name: str) -> bool:
|
||||
"""Walk to NPC and open their dialogue menu."""
|
||||
from npc_manager import Npc, open_npc_menu
|
||||
return open_npc_menu(getattr(Npc, npc_name.upper()))
|
||||
|
||||
@staticmethod
|
||||
def read_dialogue() -> dict:
|
||||
"""OCR the current dialogue box. Returns:
|
||||
{
|
||||
'npc_text': str, # What the NPC said
|
||||
'options': [str, ...], # Response options (may be empty if no choice)
|
||||
'has_continue': bool # True if just need to click continue
|
||||
}
|
||||
"""
|
||||
img = grab()
|
||||
npc_text = ocr_roi(img, self.DIALOGUE_TEXT_ROI)
|
||||
options_text = ocr_roi(img, self.DIALOGUE_OPTIONS_ROI)
|
||||
# Parse options: split by line, filter out empty, return list
|
||||
options = [line.strip() for line in options_text.split('\n') if line.strip()]
|
||||
has_continue = len(options) == 0 or "continue" in options_text.lower()
|
||||
return {
|
||||
'npc_text': npc_text.strip(),
|
||||
'options': options,
|
||||
'has_continue': has_continue
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def click_option(option_text: str) -> bool:
|
||||
"""Find and click a specific dialogue option by matching text via OCR.
|
||||
Searches the options ROI for a template match of the option text."""
|
||||
img = grab()
|
||||
options_img = cut_roi(img, self.DIALOGUE_OPTIONS_ROI)
|
||||
# Use template_finder or OCR to locate which button matches
|
||||
# Then click at that position
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def continue_dialogue() -> bool:
|
||||
"""Click the close/continue button to advance dialogue."""
|
||||
# Click in the close button area
|
||||
x, y, w, h = self.DIALOGUE_CLOSE_Y
|
||||
mouse.click at center of close area
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def follow_conversation(expected_options: list[str]) -> bool:
|
||||
"""Follow a multi-step conversation:
|
||||
- Read NPC text
|
||||
- If options present, click the expected one
|
||||
- If no options, click continue
|
||||
- Repeat until dialogue closes or unexpected text appears
|
||||
"""
|
||||
max_steps = 20 # Safety limit
|
||||
for i in range(max_steps):
|
||||
dialogue = self.read_dialogue()
|
||||
if not dialogue['has_continue'] and dialogue['options']:
|
||||
# We have a choice - click the expected option
|
||||
for opt in expected_options:
|
||||
if opt.lower() in ' '.join(dialogue['options']).lower():
|
||||
if not self.click_option(opt):
|
||||
return False
|
||||
break
|
||||
else:
|
||||
Logger.warning(f"Unexpected dialogue options: {dialogue['options']}")
|
||||
return False
|
||||
else:
|
||||
# Just continue
|
||||
if not self.continue_dialogue():
|
||||
return False
|
||||
wait(1.0, 1.5)
|
||||
# Check if dialogue box is still visible
|
||||
if not is_visible(ScreenObjects.NPCDialogue):
|
||||
return True # Done
|
||||
return False # Hit max steps
|
||||
```
|
||||
|
||||
Key design: `follow_conversation()` takes a list of expected response text. It will
|
||||
match against whatever options the NPC presents and click the right one. This handles
|
||||
multi-branch dialogues without hardcoding step-by-step clicks.
|
||||
|
||||
### 1.3 `src/quest/quest_combat.py`
|
||||
|
||||
Purpose: Lightweight combat for clearing trash during quests. Reuses existing char methods.
|
||||
|
||||
```python
|
||||
class QuestCombat:
|
||||
"""Combat helpers for quest areas -- reuses existing character combat logic."""
|
||||
|
||||
@staticmethod
|
||||
def clear_area(pather: Pather, char: IChar, path_nodes: list[int],
|
||||
timeout: float = 60) -> bool:
|
||||
"""Walk a path while killing monsters until timeout or all nodes cleared.
|
||||
This is the core of DoE: walk down, kill, walk back."""
|
||||
return pather.traverse_nodes(path_nodes, char, timeout=timeout, do_combat=True)
|
||||
|
||||
@staticmethod
|
||||
def wait_for_clear(char: IChar, timeout: float = 15) -> bool:
|
||||
"""Wait until no monsters are visible (area is clear)."""
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
targets = get_visible_targets()
|
||||
if not targets or len(targets) == 0:
|
||||
return True
|
||||
# Attack if enemies present
|
||||
char.attack()
|
||||
wait(0.5)
|
||||
return False
|
||||
```
|
||||
|
||||
### 1.4 `src/quest/quest_items.py`
|
||||
|
||||
Purpose: Detect and pick up quest items (gold glow detection).
|
||||
|
||||
```python
|
||||
class QuestItems:
|
||||
"""Quest item detection and management."""
|
||||
|
||||
@staticmethod
|
||||
def detect_quest_items(img: np.ndarray) -> list[tuple[float, float]]:
|
||||
"""Detect gold-glowing items on screen (quest items).
|
||||
Returns list of (x, y) positions in monitor coords."""
|
||||
quest_item_mask, _ = color_filter(img, Config().colors.get("gold_glow", [
|
||||
(180, 140, 0), (255, 220, 80)
|
||||
]))
|
||||
# Find contours, return centers
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def pick_up_quest_items(char: IChar, img: np.ndarray = None) -> bool:
|
||||
"""Find and pick up any quest items currently visible."""
|
||||
if img is None:
|
||||
img = grab()
|
||||
items = self.detect_quest_items(img)
|
||||
for pos in items:
|
||||
char.pick_up_item(pos, item_name="Quest Item")
|
||||
wait(0.5)
|
||||
return len(items) > 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: NPC & Location additions
|
||||
|
||||
### 2.1 Add Town_Maiden to `src/npc_manager.py`
|
||||
|
||||
```python
|
||||
# In class Npc:
|
||||
TOWN_MAIDEN = "town_maiden" # Act 1, Roaring Canyon
|
||||
|
||||
# In _build_npcs():
|
||||
Npc.TOWN_MAIDEN: {
|
||||
"head": "town_maiden.png", # Need to capture template
|
||||
"actions": {} # No trade/identify - just dialogue
|
||||
}
|
||||
```
|
||||
|
||||
The Town Maiden sits in Roaring Canyon (eastern part of Act 1 town). She has a simple
|
||||
dialogue: you talk to her to "unlock" the Den of Evil entrance, then you talk to her
|
||||
again after clearing it to get the XP reward and reset it for another run.
|
||||
|
||||
### 2.2 Add locations to `src/pather.py`
|
||||
|
||||
```python
|
||||
class Location:
|
||||
# ... existing locations ...
|
||||
|
||||
# Act 1 Roaring Canyon / Den of Evil
|
||||
A1_ROARING_CANYON = "a1_roaring_canyon" # Town area where Maiden is
|
||||
A1_DEN_OF_EVIL_ENTRANCE = "a1_doe_entrance" # Stairs down to DoE
|
||||
A1_DEN_LEVEL_1 = "a1_doe_level_1"
|
||||
A1_DEN_LEVEL_2 = "a1_doe_level_2"
|
||||
A1_DEN_LEVEL_3 = "a1_doe_level_3"
|
||||
A1_DEN_LEVEL_4 = "a1_doe_level_4"
|
||||
# (DoE has 3-5 levels depending on game version - need to confirm)
|
||||
```
|
||||
|
||||
Path nodes will need to be added for the Roaring Canyon area and each DoE level.
|
||||
These are captured via quest_debug.py by walking the path and recording waypoints.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Den of Evil run module
|
||||
|
||||
### 3.1 `src/quest/a1/q_den_of_evil.py`
|
||||
|
||||
```python
|
||||
class DenOfEvil:
|
||||
"""Den of Evil run - Act 1 repeatable quest for XP.
|
||||
|
||||
Flow:
|
||||
1. Ensure character is in Act 1
|
||||
2. Walk to Roaring Canyon (Town Maiden)
|
||||
3. Talk to Town Maiden (unlock entrance if needed)
|
||||
4. Enter Den of Evil
|
||||
5. Pre-buff (FoH + Conviction for FoHdin)
|
||||
6. Walk through each level, killing trash
|
||||
7. Exit back to Roaring Canyon
|
||||
8. Talk to Town Maiden again for reward
|
||||
9. Return to town center
|
||||
"""
|
||||
|
||||
name = "run_doe"
|
||||
|
||||
# Path nodes per level (to be filled in via quest_debug.py)
|
||||
LEVEL_PATHS = {
|
||||
1: [], # Entrance to level 1 stairs
|
||||
2: [], # Level 1 to level 2
|
||||
3: [], # Level 2 to level 3
|
||||
4: [], # Level 3 to level 4 (or final area)
|
||||
}
|
||||
|
||||
def __init__(self, pather, town_manager, char, pickit, runs):
|
||||
self._pather = pather
|
||||
self._town_manager = town_manager
|
||||
self._char = char
|
||||
self._pickit = pickit
|
||||
self._runs = runs
|
||||
self._quest_manager = QuestManager()
|
||||
self._dialogue = QuestDialogue()
|
||||
|
||||
def approach(self, curr_loc: Location, do_buff: bool) -> Location | bool:
|
||||
"""Get to Roaring Canyon and talk to Town Maiden."""
|
||||
Logger.info("Run Den of Evil")
|
||||
|
||||
# Ensure we're in Act 1
|
||||
if TownManager.get_act_from_location(curr_loc) != Location.A1_TOWN_START:
|
||||
curr_loc = self._town_manager.go_to_act(1, curr_loc)
|
||||
if not curr_loc:
|
||||
return False
|
||||
|
||||
# Walk to Roaring Canyon (Town Maiden area)
|
||||
if not self._pather.traverse_nodes(
|
||||
(curr_loc, Location.A1_ROARING_CANYON), self._char, force_move=True
|
||||
):
|
||||
return False
|
||||
|
||||
# Talk to Town Maiden to unlock/open the Den
|
||||
if not self._dialogue.open_dialogue("town_maiden"):
|
||||
return False
|
||||
|
||||
# Follow the conversation (expect "Oh no, not again" or similar)
|
||||
if not self._dialogue.follow_conversation(["Tell me more", "I'll help you"]):
|
||||
return False
|
||||
|
||||
# Enter the Den
|
||||
if not self._pather.traverse_nodes(
|
||||
(Location.A1_ROARING_CANYON, Location.A1_DEN_OF_EVIL_ENTRANCE),
|
||||
self._char, force_move=True
|
||||
):
|
||||
return False
|
||||
|
||||
return Location.A1_DEN_OF_EVIL_ENTRANCE
|
||||
|
||||
def battle(self, do_pre_buff: bool) -> bool | tuple[Location, bool]:
|
||||
"""Fight through the Den of Evil."""
|
||||
# Pre-buff
|
||||
if do_pre_buff:
|
||||
if not self._char.pre_buff():
|
||||
return False
|
||||
|
||||
# Clear each level
|
||||
for level in sorted(self.LEVEL_PATHS.keys()):
|
||||
Logger.info(f"Clearing Den of Evil level {level}")
|
||||
if not self._pather.traverse_nodes(
|
||||
self.LEVEL_PATHS[level], self._char, timeout=120, do_combat=True
|
||||
):
|
||||
Logger.error(f"Failed to clear DoE level {level}")
|
||||
return False
|
||||
|
||||
# Pick up any quest items / loot
|
||||
self._pickit.pick_up_items(self._char)
|
||||
QuestItems.pick_up_quest_items(self._char)
|
||||
|
||||
# Walk back to Roaring Canyon
|
||||
if not self._pather.traverse_nodes(
|
||||
(Location.A1_DEN_OF_EVIL_ENTRANCE, Location.A1_ROARING_CANYON),
|
||||
self._char, force_move=True
|
||||
):
|
||||
return False
|
||||
|
||||
# Talk to Town Maiden for reward
|
||||
if not self._dialogue.open_dialogue("town_maiden"):
|
||||
return False
|
||||
if not self._dialogue.follow_conversation(["Yes", "Thank you"]):
|
||||
Logger.warning("Failed to collect DoE reward from Town Maiden")
|
||||
|
||||
# Mark as done (for non-repeatable quests) or just return success
|
||||
# Note: DoE is repeatable once per real-day, so we DON'T mark permanently done
|
||||
# self._quest_manager.mark_done("den_of_evil") # Only if non-repeatable
|
||||
|
||||
return (Location.A1_ROARING_CANYON, True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Bot integration
|
||||
|
||||
### 4.1 `src/bot.py` changes
|
||||
|
||||
```python
|
||||
# Add import
|
||||
from quest.a1.q_den_of_evil import DenOfEvil
|
||||
|
||||
# In __init__:
|
||||
self._do_runs["run_doe"] = Config().routes.get("run_doe")
|
||||
|
||||
self._doe = DenOfEvil(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
|
||||
|
||||
# In _states list:
|
||||
# (No new state needed - DoE uses the existing pattern: town -> doe -> end_run -> town)
|
||||
|
||||
# In _transitions list (add):
|
||||
{ 'trigger': 'run_doe', 'source': 'town', 'dest': 'doe', 'before': "on_run_doe" },
|
||||
|
||||
# Add 'doe' to end_run source list:
|
||||
{ 'trigger': 'end_run', 'source': [..., 'doe'], 'dest': 'town', 'before': "on_end_run" },
|
||||
|
||||
# Add end_game source:
|
||||
{ 'trigger': 'end_game', 'source': [..., 'doe'], 'dest': 'initialization', 'before': "on_end_game" },
|
||||
|
||||
# Add handler method:
|
||||
def on_run_doe(self):
|
||||
res = False
|
||||
self._do_runs["run_doe"] = False
|
||||
self._game_stats.update_location("DoE")
|
||||
self._curr_loc = self._doe.approach(self._curr_loc, not self._pre_buffed)
|
||||
if self._curr_loc:
|
||||
set_pause_state(False)
|
||||
res = self._doe.battle(not self._pre_buffed)
|
||||
self._ending_run_helper(res)
|
||||
```
|
||||
|
||||
### 4.2 `src/run/__init__.py` changes
|
||||
|
||||
```python
|
||||
# No change needed if DoE lives in src/quest/ (not src/run/)
|
||||
# But if we want consistency, add:
|
||||
from quest.a1.q_den_of_evil import DenOfEvil
|
||||
```
|
||||
|
||||
### 4.3 `config/params.ini` changes
|
||||
|
||||
```ini
|
||||
[routes]
|
||||
; ... existing runs ...
|
||||
; run_doe (Act 1 Den of Evil - repeatable daily XP)
|
||||
order=run_doe
|
||||
```
|
||||
|
||||
### 4.4 `config/params.ini` FoHdin config
|
||||
|
||||
For a lvl 1 Paladin running DoE, the params.ini needs:
|
||||
|
||||
```ini
|
||||
[char]
|
||||
type=fohdin
|
||||
...
|
||||
|
||||
[fohdin]
|
||||
; FoHdin-specific config for low-level DoE runs
|
||||
teleport=
|
||||
; No teleport at lvl 1-9, so pathing is on foot
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Testing workflow
|
||||
|
||||
### What needs user input (I cannot see D2R):
|
||||
|
||||
1. **Capture Town_Maiden template:**
|
||||
- Go to Roaring Canyon in Act 1
|
||||
- Stand near the Town Maiden
|
||||
- Run `quest_debug.py`, press F4 (NPC detection)
|
||||
- Paste output so I can save the template
|
||||
|
||||
2. **Capture DoE path nodes:**
|
||||
- Enter the Den of Evil
|
||||
- Run `quest_debug.py`, press F1 at each waypoint
|
||||
- Walk from entrance through each level
|
||||
- Paste outputs so I can build the path arrays
|
||||
|
||||
3. **Capture dialogue:**
|
||||
- Talk to Town Maiden (both before and after clearing)
|
||||
- Run `quest_debug.py`, press F2 (dialogue OCR)
|
||||
- Paste output so I can code the conversation flow
|
||||
|
||||
4. **Test run:**
|
||||
- After I write the code, you run botty with `run_doe` in the route order
|
||||
- Report what happens / paste terminal output
|
||||
- I iterate based on results
|
||||
|
||||
### Lvl 1 Paladin specifics:
|
||||
|
||||
- **FoHdin requires FOH skill lvl 6 for Feign of Life passive** -- this needs 3 skill points
|
||||
in FoH, meaning character level 9 minimum (or level 4 with a +1 skill weapon)
|
||||
- Before reaching lvl 9, the bot can still run DoE but will be much more fragile
|
||||
- Recommended: manually level Paladin to ~lvl 4-5 (short runs in Area 1 or 2) before
|
||||
letting the bot solo DoE with FoH
|
||||
- The bot pathing should handle the walk-through at low speed with heavy FoH spam
|
||||
|
||||
---
|
||||
|
||||
## Implementation order
|
||||
|
||||
1. Write `quest_manager.py` (simple JSON state tracker)
|
||||
2. Write `quest_dialogue.py` (OCR-based NPC interaction)
|
||||
3. Write `quest_items.py` + `quest_combat.py` (lightweight helpers)
|
||||
4. Add Town_Maiden NPC to npc_manager.py
|
||||
5. Write `q_den_of_evil.py` (skeleton with placeholder paths)
|
||||
6. Integrate into bot.py (state, transitions, handler)
|
||||
7. Update params.ini
|
||||
8. **USER TESTS** -- captures templates, paths, dialogue
|
||||
9. I fill in the actual path nodes and dialogue based on your captures
|
||||
10. Full test run and iterate
|
||||
|
||||
---
|
||||
|
||||
## File tree after implementation
|
||||
|
||||
```
|
||||
my-botty/
|
||||
├── config/
|
||||
│ ├── params.ini # Modified: +run_doe in routes
|
||||
│ ├── quest_state.json # New: auto-created by QuestManager
|
||||
│ └── bnip/
|
||||
│ └── town_maiden.png # New: captured template
|
||||
├── src/
|
||||
│ ├── quest/ # New directory
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── quest_manager.py
|
||||
│ │ ├── quest_dialogue.py
|
||||
│ │ ├── quest_items.py
|
||||
│ │ ├── quest_combat.py
|
||||
│ │ └── a1/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── q_den_of_evil.py
|
||||
│ ├── npc_manager.py # Modified: +TOWN_MAIDEN
|
||||
│ ├── pather.py # Modified: +A1_ROARING_CANYON, +A1_DEN_* locations
|
||||
│ ├── bot.py # Modified: +doe state, transitions, handler
|
||||
│ └── run/__init__.py # Modified: +DenOfEvil export
|
||||
```
|
||||
@@ -0,0 +1,231 @@
|
||||
# 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 (no kernel drivers):
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `win_input.py` | ctypes wrappers for `SendInput`, `GetAsyncKeyState`, `GetCursorPos` |
|
||||
| `mouse_impl.py` | Humanized mouse with Bezier curves, Gaussian distortion, endpoint wobble |
|
||||
| `hotkey.py` | Polling-based hotkey manager (replaces `keyboard.add_hotkey`) |
|
||||
| `__init__.py` | Drop-in API: `from input_layer import keyboard, mouse` |
|
||||
|
||||
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 (`src/utils/stealth.py`)
|
||||
|
||||
Three-tier stealth system:
|
||||
- **Tier 1 (Input)**: Micro-pauses, click variance, key press duration, endpoint wobble
|
||||
- **Tier 2 (Behavior)**: Wrong waypoint chance, skill mistake chance, skill hesitation
|
||||
- **Tier 3 (Session)**: AFK breaks, run skipping, personality seed per character
|
||||
|
||||
### 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 |
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,427 @@
|
||||
# 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
|
||||
|
||||
### C1. Stealth: Consolidate all timing through centralized wait()
|
||||
|
||||
Many places use bare `time.sleep()` instead of `utils.misc.wait()` (which has Gaussian jitter).
|
||||
Every direct sleep creates a predictable timing signature detectable by anti-cheat.
|
||||
|
||||
Files with bare `time.sleep()`:
|
||||
- health_manager.py line 14
|
||||
- chest.py
|
||||
- pather.py
|
||||
- game_recovery.py
|
||||
- npc_manager.py
|
||||
- bot.py
|
||||
|
||||
Fix: Replace all bare `time.sleep(n)` with `wait(n, n*1.2)` for human-like jitter.
|
||||
|
||||
### 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)~~
|
||||
|
||||
### C10. ~~Bug: Replace thread killing with cooperative shutdown~~
|
||||
~~`utils.misc.kill_thread()` uses `PyThreadState_SetAsyncExc` (CPython private API).~~
|
||||
~~This can leave locks in inconsistent state, cause GIL issues, or corrupt numpy arrays.~~
|
||||
~~Fix: Replace with threading.Event flags for cooperative shutdown.~~
|
||||
(Still needs doing - this is the most dangerous remaining bug.)
|
||||
|
||||
---
|
||||
|
||||
## 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,8 +1,8 @@
|
||||
|
||||
|
||||
# <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!
|
||||
|
||||
[**Download here**](https://github.com/aeon0/botty/releases) and got to have a [**Discord**](https://discord.gg/Jf3J8cuXWg) nowadays I guess :man_shrugging:
|
||||
Pixelbot for Diablo 2 Resurrected. This project is for informational and educational purposes only.
|
||||
|
||||
## Getting started & Prerequisites
|
||||
- D2R needs to be in English Language,
|
||||
@@ -23,17 +23,17 @@ 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.
|
||||
Refer to [development.md](development.md) for setup instructions. Once the conda environment is created:
|
||||
|
||||
- **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.
|
||||
|
||||
## 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).
|
||||
@@ -153,7 +153,6 @@ order=run_pindle, run_eldritch
|
||||
| enable_no_pickup | When enabled, will type `/nopickup` into chat at game start, which can help reduce accidental pickups especially for walking characters. |
|
||||
| fill_shared_stash_first | Fill stash tabs starting from right to left, filling personal stash last |
|
||||
| gamble_items | List of items to gamble when stash fills with gold. Leave blank to disable. Supported items currently include circlet, ring, coronet, talon, amulet
|
||||
| id_items | Will identify items with tome of ID or at Cain if enabled |
|
||||
| open_chests | Open up chests in some places. E.g. on dead ends of arcane. |
|
||||
| pre_buff_every_run | 0: Will only prebuff on first run, 1: Will prebuff after each run/boss |
|
||||
| runs_per_repair | Force repair after `runs_per_repair` of runs. Set to 0 to repair only when needed. |
|
||||
@@ -166,84 +165,146 @@ 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.
|
||||
|
||||
### Builds
|
||||
| [sorceress] | Descriptions |
|
||||
| ------------- | ----------------------------------------------------------------------------- |
|
||||
| frozen_armor | Optional Hotkey for frozen armor (or any of the other armors) |
|
||||
| energy_shield | Optional Hotkey for energy shield |
|
||||
| thunder_storm | Optional Hotkey for thunder storm |
|
||||
| static_field | Optional Hotkey for static field |
|
||||
| telekinesis | Optional Hotkey for telekinesis |
|
||||
|| [sorceress] | Descriptions |
|
||||
|| ------------- | ----------------------------------------------------------------------------- |
|
||||
|| frozen_armor | Optional Hotkey for frozen armor (or any of the other armors) |
|
||||
|| energy_shield | Optional Hotkey for energy shield |
|
||||
|| thunder_storm | Optional Hotkey for thunder storm |
|
||||
|| static_field | Optional Hotkey for static field |
|
||||
|| telekinesis | Optional Hotkey for telekinesis |
|
||||
|
||||
| [light_sorc] | Descriptions |
|
||||
| ------------- | ----------------------------------------------------------------------------- |
|
||||
| chain_lightning | Optional Hotkey for chain_lightning (must be bound to left skill) |
|
||||
| lightning | Required Hotkey for lightning (must be bound to right skill) |
|
||||
| frozen_orb | Optional Hotkey for frozen orb (must be bound to right skill) |
|
||||
|| [light_sorc] | Descriptions |
|
||||
|| ------------- | ----------------------------------------------------------------------------- |
|
||||
|| chain_lightning | Optional Hotkey for chain_lightning (must be bound to left skill) |
|
||||
|| lightning | Required Hotkey for lightning (must be bound to right skill) |
|
||||
|| frozen_orb | Optional Hotkey for frozen orb (must be bound to right skill) |
|
||||
|
||||
| [blizz_sorc] | Descriptions |
|
||||
| ------------- | ----------------------------------------------------------------------------- |
|
||||
| blizzard | Required Hotkey for Blizzard (must be bound to right skill) |
|
||||
| ice_blast | Optional Hotkey for ice_blast (must be bound to left skill) |
|
||||
|| [blizz_sorc] | Descriptions |
|
||||
|| ------------- | ----------------------------------------------------------------------------- |
|
||||
|| blizzard | Required Hotkey for Blizzard (must be bound to right skill) |
|
||||
|| ice_blast | Optional Hotkey for ice_blast (must be bound to left skill) |
|
||||
|
||||
| [nova_sorc] | Descriptions |
|
||||
| ------------- | ----------------------------------------------------------------------------- |
|
||||
| nova | Required Hotkey for Nova (must be bound to right skill) |
|
||||
|| [nova_sorc] | Descriptions |
|
||||
|| ------------- | ----------------------------------------------------------------------------- |
|
||||
|| nova | Required Hotkey for Nova (must be bound to right skill) |
|
||||
|
||||
| [hydra_sorc] | Descriptions |
|
||||
| ------------- | ----------------------------------------------------------------------------- |
|
||||
| alt_attack | Required Hotkey for any alternate attacking skill. Fireball,Lightning,Frozen Orb, etc. (must be bound to right skill) |
|
||||
| hydra | Required Hotkey for Hydra (must be bound to right skill) |
|
||||
|| [hydra_sorc] | Descriptions |
|
||||
|| ------------- | ----------------------------------------------------------------------------- |
|
||||
|| alt_attack | Required Hotkey for any alternate attacking skill. Fireball,Lightning,Frozen Orb, etc. (must be bound to right skill) |
|
||||
|| hydra | Required Hotkey for Hydra (must be bound to right skill) |
|
||||
|
||||
| [paladin] | Descriptions |
|
||||
| -------------- | ----------------------------------------------------------------------------------- |
|
||||
| cleansing | Optional Hotkey for Cleansing |
|
||||
| holy_shield | Required Hotkey for Holy Shield |
|
||||
| redemption | Optional Hotkey for Redemption |
|
||||
| vigor | Optional Hotkey for Vigor |
|
||||
|| [paladin] | Descriptions |
|
||||
|| -------------- | ----------------------------------------------------------------------------------- |
|
||||
|| cleansing | Optional Hotkey for Cleansing |
|
||||
|| holy_shield | Required Hotkey for Holy Shield |
|
||||
|| redemption | Optional Hotkey for Redemption |
|
||||
|| vigor | Optional Hotkey for Vigor |
|
||||
|
||||
| [fohdin] | Descriptions |
|
||||
| -------------- | ----------------------------------------------------------------------------------- |
|
||||
| blessed_hammer | Hotkey for Blessed Hammer. (Optional. Bind to left skill) |
|
||||
| concentration | Hotkey for Concentration |
|
||||
| conviction | Hotkey for Conviction |
|
||||
| foh | Hotkey for Fist of Heavens (Required) |
|
||||
| holy_bolt | Hotkey for Holy Bolt (Required) |
|
||||
|| [fohdin] | Descriptions |
|
||||
|| -------------- | ----------------------------------------------------------------------------------- |
|
||||
|| blessed_hammer | Hotkey for Blessed Hammer. (Optional. Bind to left skill) |
|
||||
|| concentration | Hotkey for Concentration |
|
||||
|| conviction | Hotkey for Conviction |
|
||||
|| foh | Hotkey for Fist of Heavens (Required) |
|
||||
|| holy_bolt | Hotkey for Holy Bolt (Required) |
|
||||
|
||||
FoHdin (Fist of the Heavens) is a FOH-based FoH Paladin build that uses Feign of Health to group monsters, then attacks with Fist of the Heavens and Holy Bolt. Works well for Hell difficulty farming runs.
|
||||
|
||||
| [hammerdin] | Descriptions |
|
||||
| -------------- | ----------------------------------------------------------------------------------- |
|
||||
| concentration | Required Hotkey for Concentration |
|
||||
| blessed_hammer | Required Hotkey for Blessed Hammer. (must be bound to left skill!) |
|
||||
|| [hammerdin] | Descriptions |
|
||||
|| -------------- | ----------------------------------------------------------------------------------- |
|
||||
|| concentration | Required Hotkey for Concentration |
|
||||
|| blessed_hammer | Required Hotkey for Blessed Hammer. (must be bound to left skill!) |
|
||||
|
||||
| [trapsin] | Descriptions |
|
||||
| -------------- | ----------------------------------------------------------------------------------- |
|
||||
| burst_of_speed | Optional Hotkey for Burst of Speed |
|
||||
| death_sentry | Required Hotkey for Death Sentry |
|
||||
| fade | Optional Hotkey for Fade |
|
||||
| lightning_sentry | Required Hotkey for Lightning Sentry |
|
||||
| shadow_warrior | Optional Hotkey for Shadow Warrior |
|
||||
| skill_left | Optional Hotkey for Left Skill |
|
||||
|| [trapsin] | Descriptions |
|
||||
|| -------------- | ----------------------------------------------------------------------------------- |
|
||||
|| burst_of_speed | Optional Hotkey for Burst of Speed |
|
||||
|| death_sentry | Required Hotkey for Death Sentry |
|
||||
|| fade | Optional Hotkey for Fade |
|
||||
|| lightning_sentry | Required Hotkey for Lightning Sentry |
|
||||
|| shadow_warrior | Optional Hotkey for Shadow Warrior |
|
||||
|| skill_left | Optional Hotkey for Left Skill |
|
||||
|
||||
| [barbarian] | Descriptions |
|
||||
| -------------- | ----------------------------------------------------------------------------------- |
|
||||
| cry_frequency | Time in seconds between each cast of war_cry. Set to 0.0 if max fcr should be used |
|
||||
| find_item | Optional Hotkey for Find Item |
|
||||
| leap | Required Hotkey for Leap |
|
||||
| shout | Required Hotkey for Shout |
|
||||
| war_cry | Required Hotkey for War Cry |
|
||||
|| [barbarian] | Descriptions |
|
||||
|| -------------- | ----------------------------------------------------------------------------------- |
|
||||
|| cry_frequency | Time in seconds between each cast of war_cry. Set to 0.0 if max fcr should be used |
|
||||
|| find_item | Optional Hotkey for Find Item |
|
||||
|| leap | Required Hotkey for Leap |
|
||||
|| shout | Required Hotkey for Shout |
|
||||
|| war_cry | Required Hotkey for War Cry |
|
||||
|
||||
| [Necro] | Descriptions |
|
||||
| -------------- | ----------------------------------------------------------------------------------- |
|
||||
| skill_left | Required Hotkey for attack (bonespear/teeth) |
|
||||
| bone_armor | Required Hotkey for Bone Armor |
|
||||
| clay_golem | Required Hotkey for Clay Golem |
|
||||
| raise_skeleton | Required Hotkey for Raise Skeleton |
|
||||
| amp_dmg | Required Hotkey for Amplify Damage |
|
||||
| corpse_explosion | Required Hotkey Corpse Explosion |
|
||||
| raise_revive | Required Hotkey revive |
|
||||
| damage_scaling | Adjusts time spent casting attack skills. Ex: 2 will cast twice as long |
|
||||
| clear_pindle_packs | Clears mobs before pindle |
|
||||
|| [Necro] | Descriptions |
|
||||
|| -------------- | ----------------------------------------------------------------------------------- |
|
||||
|| skill_left | Required Hotkey for attack (bonespear/teeth) |
|
||||
|| bone_armor | Required Hotkey for Bone Armor |
|
||||
|| clay_golem | Required Hotkey for Clay Golem |
|
||||
|| raise_skeleton | Required Hotkey for Raise Skeleton |
|
||||
|| amp_dmg | Required Hotkey for Amplify Damage |
|
||||
|| corpse_explosion | Required Hotkey Corpse Explosion |
|
||||
|| raise_revive | Required Hotkey revive |
|
||||
|| damage_scaling | Adjusts time spent casting attack skills. Ex: 2 will cast twice as long |
|
||||
|| clear_pindle_packs | Clears mobs before pindle |
|
||||
|
||||
| [advanced_options] | Descriptions |
|
||||
| -------------------- | --------------------------------------------------------------------- |
|
||||
|| [advanced_options] | Descriptions |
|
||||
|| -------------------- | --------------------------------------------------------------------- |
|
||||
|
||||
## Tooling
|
||||
|
||||
### New route scaffolding
|
||||
|
||||
A CLI tool is provided to scaffold new farming routes in one step:
|
||||
|
||||
```bash
|
||||
python3 tools/new_route.py --name <name> --display "<Display Name>" --act <1-5>
|
||||
```
|
||||
|
||||
This creates `src/run/<name>.py` from a template and mutates `src/bot.py` and
|
||||
`src/run/__init__.py` to wire the new route into the bot's state machine.
|
||||
|
||||
Options:
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--name` | Snake-case run name (e.g. `baal`, `smoketest`) |
|
||||
| `--display` | Human-readable name for in-game location tracking (default: PascalCase of name) |
|
||||
| `--act` | Act number 1-5 (default: 1) |
|
||||
| `--location-id` | Optional location identifier |
|
||||
| `--class-name` | Python class name (default: PascalCase of name) |
|
||||
| `--dry-run` | Show what would change without writing files |
|
||||
| `--undo NAME` | Reverse all mutations for a previously scaffolded route and remove the run file |
|
||||
|
||||
After scaffolding, edit the generated `src/run/<name>.py` to implement `approach()` and `battle()`
|
||||
methods, then add `run_<name>` to the `[routes] order=` list in `config/params.ini`.
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
# Botty Stealth - Deep Dive
|
||||
|
||||
## What Warden (Battle.net Anti-Cheat) Can Detect
|
||||
|
||||
Warden is a kernel-level anti-cheat. It can:
|
||||
- **Enumerate processes** - see `main.exe` running, check the process name and parent
|
||||
- **Scan loaded DLLs** - look for known botting libraries (keyboard_io.dll, mousetool.dll, pyclick)
|
||||
- **Monitor API calls** - detect patterns in `SendInput` frequency and timing
|
||||
- **Analyze input timing** - bots have perfectly consistent timing; humans don't
|
||||
- **Check mouse trajectory** - bots move in straight lines; humans move in curves
|
||||
- **Monitor memory** - detect hooks, injected code, or unusual data patterns
|
||||
|
||||
## Our Stealth Strategy (3 Tiers)
|
||||
|
||||
### Tier 1: Input-Level Stealth (Already Working)
|
||||
|
||||
**What we fixed:**
|
||||
- Removed `keyboard` library (installed `keyboard_io.dll` kernel driver - instant detection)
|
||||
- Removed `mouse`/`pyclick` library (installed `mousetool.dll` kernel driver - instant detection)
|
||||
- Replaced with `ctypes` + `user32.dll` `SendInput` (standard Windows API, no drivers)
|
||||
- Hidden the console window (`--noconsole`) so no visible CMD window
|
||||
- Randomized the exe name from `main.exe` to something like `whvitjz2.exe`
|
||||
|
||||
**Mouse movement stealth (in `input_layer/mouse_impl.py`):**
|
||||
- Every `mouse.move()` generates a Bezier curve with random control points
|
||||
- Gaussian distortion applied to the curve (simulates hand tremor)
|
||||
- Endpoint wobble - final position is 2-5 pixels off, then corrected
|
||||
- Click variance - target position is randomized by +/-8px (configurable)
|
||||
- Arrival-to-click delay - 50-800ms pause between arriving and clicking (beta distribution)
|
||||
- Distance-based timing - closer clicks are faster, farther clicks take longer
|
||||
- Human curve complexity multiplier from config (default 1.0)
|
||||
|
||||
**Keyboard stealth (in `input_layer/__init__.py`):**
|
||||
- Every `keyboard.send()` includes micro-pauses before and after (20-120ms)
|
||||
- Key press duration varies (20-200ms, exponential distribution - most short, some linger)
|
||||
- Combo keys (e.g. `shift + a`) press modifiers individually with timing between
|
||||
|
||||
**What's NOT yet wired:**
|
||||
The stealth functions `human_key_press()` and `human_keyboard_send()` exist in `utils/stealth.py` but are called via the input layer shim automatically. Every `keyboard.send()` now gets stealth timing.
|
||||
|
||||
### Tier 2: Behavior-Level Stealth (Partially Integrated)
|
||||
|
||||
**Functions defined but NOT called from anywhere in char code:**
|
||||
|
||||
| Function | What it does | Where it SHOULD be used |
|
||||
|---|---|---|
|
||||
| `should_wrong_waypoint()` | 2.5% chance of clicking wrong waypoint | `ui/waypoint.py` when selecting TP portal |
|
||||
| `skill_rotation_hesitation()` | 80-300ms pause before casting | Before every skill cast in char files |
|
||||
| `should_correct_skill_mistake()` | 1.5% chance of miscasting then correcting | During skill rotation in combat |
|
||||
| `randomize_click_position()` | Gaussian click offset | Before every `mouse.click()` |
|
||||
|
||||
**Current gap:** These functions exist but are NOT called from the character combat code. The char files call `keyboard.send("1")` directly without going through a stealth wrapper. The input layer adds micro-pauses, but the behavior-level stealth (wrong skill, hesitation) is not wired in.
|
||||
|
||||
**What IS wired:**
|
||||
- `mouse.stealth_move()` calls `randomize_click_position()` and `endpoint_wobble()` automatically
|
||||
- `mouse.click()` calls `apply_click_delay()` automatically
|
||||
- `keyboard.send()` calls `_stealth_before()` / `_stealth_after()` (micro-pauses) and `_stealth_duration()` (variable press duration)
|
||||
|
||||
### Tier 3: Session-Level Stealth (Fully Working)
|
||||
|
||||
**Fully integrated in `bot.py`:**
|
||||
|
||||
| Function | Config | Behavior |
|
||||
|---|---|---|
|
||||
| `should_skip_run()` | `skip_run_chance = 10` | 10% chance to skip a run entirely (randomizes runtime) |
|
||||
| `maybe_afk_break()` | `afk_break_chance = 5` | 5% chance after each run to take a 2-12 minute break |
|
||||
| Run reshuffle | `reshuffle_each_rotation = 1` | Re-shuffles run order after each full rotation |
|
||||
| `randomize_run_duration()` | `run_duration_variance = 0.15` | +/-15% variation on expected run time |
|
||||
| Wait jitter | `wait_jitter_min = 0.85` / `max = 1.20` | Every `wait()` call is multiplied by 0.85x-1.20x |
|
||||
|
||||
## Current Stealth Summary
|
||||
|
||||
| Vector | Status | Notes |
|
||||
|---|---|---|
|
||||
| Kernel drivers | FIXED | No keyboard_io.dll or mousetool.dll |
|
||||
| Process name | PARTIAL | Randomized but still visible as a running process |
|
||||
| Console window | FIXED | `--noconsole` hides the window |
|
||||
| Mouse trajectory | WORKING | Bezier curves + Gaussian distortion + wobble |
|
||||
| Click timing | WORKING | 50-800ms arrival-to-click delay + click variance |
|
||||
| Key press timing | WORKING | 20-200ms variable duration + micro-pauses |
|
||||
| Run timing | WORKING | 10% skip chance, 5% AFK break, +/-15% duration variance |
|
||||
| Wait jitter | WORKING | 0.85x-1.20x on all waits |
|
||||
| Wrong waypoint | WORKING (2.5%) | Wired in waypoint.py |
|
||||
| Skill mistakes | WORKING (1.5%) | Wired in input_layer/__init__.py |
|
||||
| Skill hesitation | WORKING (80-300ms) | Wired in input_layer/__init__.py |
|
||||
|
||||
## Remaining Risks
|
||||
|
||||
1. **Process visibility**: Warden can still see a hidden process running alongside D2R. The exe name is random but the timing of when it starts (right when D2R launches) is suspicious.
|
||||
|
||||
2. **Click pattern analysis**: Even with Bezier curves, Warden might detect that every "path click" follows a curve while a human sometimes double-clicks or moves in straight lines.
|
||||
|
||||
3. **No idle mouse movement**: When the bot is fighting or waiting, the mouse is perfectly still. Humans constantly fidget with the mouse.
|
||||
|
||||
4. **Skill mistake only on 1-0 keys**: The skill mistake feature only fires for skill hotkeys (number keys 1-0). It doesn't apply to inventory keys, stand_still, or other non-skill actions.
|
||||
|
||||
## What Would Improve Stealth Further
|
||||
|
||||
### High Impact (Warden likely checks these)
|
||||
1. **Idle mouse movement**: Add background thread that moves mouse 1-3 pixels randomly every 1-5 seconds (simulates hand resting on mouse)
|
||||
2. **Occasional straight-line clicks**: 10% of the time skip the Bezier curve and click directly (humans don't always move in curves)
|
||||
|
||||
### Medium Impact
|
||||
3. **Process parent spoofing**: Launch the bot from a different process (e.g. from Explorer instead of directly) to hide the relationship with D2R
|
||||
4. **Variable scroll speed**: Add variance to `mouse.wheel()` calls (inventory scrolling)
|
||||
5. **Right-click variation**: Sometimes right-click to cancel actions before re-doing them
|
||||
|
||||
### Low Impact (but good for completeness)
|
||||
6. **Randomize AFK break more**: Current range is 2-12 minutes, could be wider
|
||||
7. **Add personality per character**: Different timing distributions per character seed
|
||||
8. **Typing delays**: When using text chat (if implemented), add keystroke-by-keystroke delays
|
||||
@@ -0,0 +1,198 @@
|
||||
"""
|
||||
D2R Asset Extractor
|
||||
|
||||
Runs on your local Windows machine. Captures D2R, saves screenshot.
|
||||
You then send the screenshot to the AI agent for analysis.
|
||||
AI returns bounding boxes -> run crop.py to extract PNGs.
|
||||
|
||||
Usage:
|
||||
Run: python asset_extractor.py
|
||||
F1: Capture D2R screen -> screenshots/debug/latest.png
|
||||
F2: Crop entities from screenshots/debug/latest_annotations.json
|
||||
F3: List existing assets
|
||||
F12: Exit
|
||||
|
||||
Workflow:
|
||||
1. Run this script in the botty conda env
|
||||
2. F1 to capture
|
||||
3. Tell your AI agent to analyze screenshots/debug/latest.png
|
||||
4. AI writes screenshots/debug/latest_annotations.json with bounding boxes
|
||||
5. F2 to crop entities into assets/enemies/ or assets/npc/
|
||||
"""
|
||||
import os, sys, cv2, numpy as np, keyboard, json, ctypes, win32gui
|
||||
from datetime import datetime
|
||||
from mss import mss
|
||||
|
||||
# DPI awareness - must be first
|
||||
try:
|
||||
ctypes.windll.shcore.SetProcessDpiAwareness(2)
|
||||
except:
|
||||
try:
|
||||
ctypes.windll.shcore.SetProcessDpiAwareness(1)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Fix tesserocr DLLs
|
||||
if sys.platform == "win32":
|
||||
_dll = os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "Library", "bin")
|
||||
if os.path.isdir(_dll):
|
||||
os.add_dll_directory(_dll)
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "src"))
|
||||
|
||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
SAVE_DIR = os.path.join(BASE, "screenshots", "debug")
|
||||
ENEMIES_DIR = os.path.join(BASE, "assets", "enemies")
|
||||
NPC_DIR = os.path.join(BASE, "assets", "npc")
|
||||
|
||||
for d in [SAVE_DIR, ENEMIES_DIR, NPC_DIR]:
|
||||
os.makedirs(d, exist_ok=True)
|
||||
|
||||
LATEST_PATH = os.path.join(SAVE_DIR, "latest.png")
|
||||
ANNOTATIONS_PATH = os.path.join(SAVE_DIR, "latest_annotations.json")
|
||||
|
||||
# Known NPC names for routing
|
||||
NPC_NAMES = {
|
||||
'akara', 'charsi', 'kashya', 'cain', 'drognan', 'lysander',
|
||||
'fara', 'ormus', 'tyrael', 'jamella', 'halbu', 'qual_kehk',
|
||||
'qual-kehk', 'qualkehk', 'malah', 'larzuk', 'anya'
|
||||
}
|
||||
|
||||
|
||||
def find_d2r():
|
||||
hwnds = []
|
||||
def cb(h, r):
|
||||
title = win32gui.GetWindowText(h)
|
||||
if 'diablo' in title.lower() and win32gui.IsWindowVisible(h):
|
||||
r.append(h)
|
||||
win32gui.EnumWindows(cb, hwnds)
|
||||
return hwnds[0] if hwnds else None
|
||||
|
||||
|
||||
def grab():
|
||||
"""Grab D2R client area. Resizes to 1280x720 if needed."""
|
||||
hwnd = find_d2r()
|
||||
if not hwnd:
|
||||
print(" [ERROR] D2R not found. Is it running and visible?")
|
||||
return None
|
||||
|
||||
client = win32gui.GetClientRect(hwnd)
|
||||
w, h = client[2] - client[0], client[3] - client[1]
|
||||
screen_pos = win32gui.ClientToScreen(hwnd, (0, 0))
|
||||
|
||||
with mss() as sct:
|
||||
region = {
|
||||
'top': screen_pos[1],
|
||||
'left': screen_pos[0],
|
||||
'width': w,
|
||||
'height': h
|
||||
}
|
||||
sct_img = sct.grab(region)
|
||||
img = np.array(sct_img)[:, :, :3] # BGRA -> BGR
|
||||
|
||||
if w != 1280 or h != 720:
|
||||
img = cv2.resize(img, (1280, 720), interpolation=cv2.INTER_LINEAR)
|
||||
print(f" [RESIZED] {w}x{h} -> 1280x720")
|
||||
else:
|
||||
print(f" [CAPTURED] {w}x{h}")
|
||||
|
||||
return img
|
||||
|
||||
|
||||
def on_f1():
|
||||
"""Capture D2R and save."""
|
||||
print("\n[=== CAPTURING ===]")
|
||||
img = grab()
|
||||
if not img:
|
||||
return
|
||||
cv2.imwrite(LATEST_PATH, img)
|
||||
print(f" [SAVED] {LATEST_PATH}")
|
||||
print(f" Now ask your AI agent to analyze: {LATEST_PATH}")
|
||||
print(f" AI should write: {ANNOTATIONS_PATH}")
|
||||
print(' Format: [{"name":"skeleton","x":100,"y":200,"w":60,"h":80}, ...]')
|
||||
|
||||
|
||||
def on_f2():
|
||||
"""Crop entities from latest capture using annotations JSON."""
|
||||
print("\n[=== CROPPING ENTITIES ===]")
|
||||
if not os.path.exists(LATEST_PATH):
|
||||
print(" [ERROR] No capture found. Press F1 first.")
|
||||
return
|
||||
if not os.path.exists(ANNOTATIONS_PATH):
|
||||
print(" [ERROR] No annotations found.")
|
||||
print(f" Create: {ANNOTATIONS_PATH}")
|
||||
print(' [{"name":"skeleton","x":100,"y":200,"w":60,"h":80}, ...]')
|
||||
return
|
||||
|
||||
img = cv2.imread(LATEST_PATH)
|
||||
with open(ANNOTATIONS_PATH) as f:
|
||||
entities = json.load(f)
|
||||
|
||||
print(f" Image: {img.shape[1]}x{img.shape[0]}, Entities: {len(entities)}")
|
||||
|
||||
saved = 0
|
||||
for ent in entities:
|
||||
name = ent['name'].lower().replace(' ', '_')
|
||||
x, y = int(ent['x']), int(ent['y'])
|
||||
w, h = int(ent['w']), int(ent['h'])
|
||||
i_w, i_h = img.shape[1], img.shape[0]
|
||||
|
||||
# Crop with 5px padding
|
||||
pad = 5
|
||||
x1, y1 = max(0, x - pad), max(0, y - pad)
|
||||
x2, y2 = min(i_w, x + w + pad), min(i_h, y + h + pad)
|
||||
crop = img[y1:y2, x1:x2]
|
||||
|
||||
# Route to npc or enemies folder
|
||||
if name in NPC_NAMES:
|
||||
save_dir = NPC_DIR
|
||||
else:
|
||||
save_dir = ENEMIES_DIR
|
||||
|
||||
# Auto-number duplicates
|
||||
fname = f"{name}.png"
|
||||
save_path = os.path.join(save_dir, fname)
|
||||
variant = 1
|
||||
while os.path.exists(save_path):
|
||||
variant += 1
|
||||
fname = f"{name}_{variant}.png"
|
||||
save_path = os.path.join(save_dir, fname)
|
||||
|
||||
cv2.imwrite(save_path, crop)
|
||||
print(f" [SAVED] {save_path} ({crop.shape[1]}x{crop.shape[0]})")
|
||||
saved += 1
|
||||
|
||||
print(f"\n Total: {saved} assets cropped.")
|
||||
|
||||
|
||||
def on_f3():
|
||||
"""List existing assets."""
|
||||
print("\n[=== ASSETS INVENTORY ===]")
|
||||
for label, d in [("enemies", ENEMIES_DIR), ("npc", NPC_DIR)]:
|
||||
if os.path.isdir(d):
|
||||
files = sorted(os.listdir(d))
|
||||
print(f"\n assets/{label}/ ({len(files)} files):")
|
||||
for f in files:
|
||||
sz = os.path.getsize(os.path.join(d, f))
|
||||
print(f" {f} ({sz}b)")
|
||||
else:
|
||||
print(f"\n assets/{label}/ - EMPTY")
|
||||
|
||||
|
||||
def run():
|
||||
print("=== D2R Asset Extractor ===")
|
||||
print(" F1 - Capture D2R screen")
|
||||
print(" F2 - Crop entities from annotations")
|
||||
print(" F3 - List assets")
|
||||
print(" F12 - Exit")
|
||||
print("Ready.")
|
||||
|
||||
keyboard.add_hotkey('f1', on_f1)
|
||||
keyboard.add_hotkey('f2', on_f2)
|
||||
keyboard.add_hotkey('f3', on_f3)
|
||||
keyboard.add_hotkey('f12', lambda: (print("\nBye."), sys.exit(0)))
|
||||
keyboard.wait()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -100,6 +100,9 @@ NTIPAliasType["ruby"] = 99;
|
||||
NTIPAliasType["sapphire"] = 100;
|
||||
NTIPAliasType["topaz"] = 101;
|
||||
NTIPAliasType["skull"] = 102;
|
||||
NTIPAliasType["swordsandknives"] = 103;
|
||||
NTIPAliasType["spearsandpolearms"] = 104;
|
||||
NTIPAliasType["grimoires"] = 105;
|
||||
|
||||
var NTIPAliasClassID = {};
|
||||
NTIPAliasClassID["hax"] = 0; NTIPAliasClassID["handaxe"] = 0;
|
||||
@@ -761,6 +764,26 @@ NTIPAliasClassID["ceh"] = 655; NTIPAliasClassID["chargedessenceofhatred"] = 655
|
||||
NTIPAliasClassID["bet"] = 656; NTIPAliasClassID["burningessenceofterror"] = 656;
|
||||
NTIPAliasClassID["fed"] = 657; NTIPAliasClassID["festeringessenceofdestruction"] = 657;
|
||||
NTIPAliasClassID["std"] = 658; NTIPAliasClassID["standardofheroes"] = 658;
|
||||
NTIPAliasClassID["wa1"] = 659; NTIPAliasClassID["oldbook"] = 659;
|
||||
NTIPAliasClassID["wa2"] = 660; NTIPAliasClassID["tome"] = 660;
|
||||
NTIPAliasClassID["wa3"] = 661; NTIPAliasClassID["codex"] = 661;
|
||||
NTIPAliasClassID["wa4"] = 662; NTIPAliasClassID["compendium"] = 662;
|
||||
NTIPAliasClassID["wa5"] = 663; NTIPAliasClassID["grimoire"] = 663;
|
||||
NTIPAliasClassID["wa6"] = 664; NTIPAliasClassID["burnttext"] = 664;
|
||||
NTIPAliasClassID["wa7"] = 665; NTIPAliasClassID["darktome"] = 665;
|
||||
NTIPAliasClassID["wa8"] = 666; NTIPAliasClassID["darkcodex"] = 666;
|
||||
NTIPAliasClassID["wa9"] = 667; NTIPAliasClassID["possessedcompendium"] = 667;
|
||||
NTIPAliasClassID["waa"] = 668; NTIPAliasClassID["possessedgrimoire"] = 668;
|
||||
NTIPAliasClassID["wab"] = 669; NTIPAliasClassID["forgottenvolume"] = 669;
|
||||
NTIPAliasClassID["wac"] = 670; NTIPAliasClassID["occulttome"] = 670;
|
||||
NTIPAliasClassID["wad"] = 671; NTIPAliasClassID["occultcodex"] = 671;
|
||||
NTIPAliasClassID["wae"] = 672; NTIPAliasClassID["blasphemouscompendium"] = 672;
|
||||
NTIPAliasClassID["waf"] = 673; NTIPAliasClassID["blasphemousgrimoire"] = 673;
|
||||
NTIPAliasClassID["northernworldstoneshard"] = 674;
|
||||
NTIPAliasClassID["easternworldstoneshard"] = 675;
|
||||
NTIPAliasClassID["southernworldstoneshard"] = 676;
|
||||
NTIPAliasClassID["westernworldstoneshard"] = 677;
|
||||
NTIPAliasClassID["deepworldstoneshard"] = 678;
|
||||
|
||||
var NTIPAliasClass = {};
|
||||
NTIPAliasClass["normal"] = 0;
|
||||
@@ -900,6 +923,7 @@ NTIPAliasStat["itemaddpaladinskills"] = [83,3]; NTIPAliasStat["paladinskills"] =
|
||||
NTIPAliasStat["itemaddbarbarianskills"] = [83,4]; NTIPAliasStat["barbarianskills"] = [83,4];
|
||||
NTIPAliasStat["itemadddruidskills"] = [83,5]; NTIPAliasStat["druidskills"] = [83,5];
|
||||
NTIPAliasStat["itemaddassassinskills"] = [83,6]; NTIPAliasStat["assassinskills"] = [83,6];
|
||||
NTIPAliasStat["itemaddwarlockskills"] = [83,7]; NTIPAliasStat["warlockskills"] = [83,7];
|
||||
|
||||
NTIPAliasStat["unsentparam1"] = 84;
|
||||
NTIPAliasStat["itemaddexperience"] = 85;
|
||||
@@ -931,6 +955,9 @@ NTIPAliasStat["plusskillwerewolf"] = [97,223];
|
||||
NTIPAliasStat["plusskillshapeshifting"] = [97,224]; NTIPAliasStat["plusskilllycanthropy"] = [97,224];
|
||||
NTIPAliasStat["plusskillsummonspiritwolf"] = [97,227];
|
||||
NTIPAliasStat["plusskillferalrage"] = [97,232];
|
||||
// Warlock
|
||||
NTIPAliasStat["plusskillabyss"] = [97,281]
|
||||
NTIPAliasStat["plusskillenhancedentropy"] = [97,282]
|
||||
|
||||
NTIPAliasStat["state"] = 98;
|
||||
NTIPAliasStat["itemfastergethitrate"] = 99; NTIPAliasStat["fhr"] = 99;
|
||||
@@ -1160,6 +1187,9 @@ NTIPAliasStat["skillbladeshield"] = [107,277];
|
||||
NTIPAliasStat["skillvenom"] = [107,278];
|
||||
NTIPAliasStat["skillshadowmaster"] = [107,279];
|
||||
NTIPAliasStat["skillphoenixstrike"] = [107,280];
|
||||
// Warlock skills
|
||||
NTIPAliasStat["skillabyss"] = [107,281];
|
||||
NTIPAliasStat["skillenhancedentropy"] = [107,282];
|
||||
|
||||
NTIPAliasStat["itemrestinpeace"] = 108;
|
||||
NTIPAliasStat["curseresistance"] = 109;
|
||||
@@ -1279,6 +1309,9 @@ NTIPAliasStat["itemaddelementalskilltab"] = [188,42]; NTIPAliasStat["elementals
|
||||
NTIPAliasStat["itemaddtrapsskilltab"] = [188,48]; NTIPAliasStat["trapsskilltab"] = [188,48];
|
||||
NTIPAliasStat["itemaddshadowdisciplinesskilltab"] = [188,49]; NTIPAliasStat["shadowdisciplinesskilltab"] = [188,49];
|
||||
NTIPAliasStat["itemaddmartialartsskilltab"] = [188,50]; NTIPAliasStat["martialartsskilltab"] = [188,50];
|
||||
NTIPAliasStat["itemaddchaosskilltab"] = [188,51]; NTIPAliasStat["chaosskilltab"] = [188,51];
|
||||
NTIPAliasStat["itemaddeldritchskilltab"] = [188,52]; NTIPAliasStat["eldritchskilltab"] = [188,52];
|
||||
NTIPAliasStat["itemadddemonskilltab"] = [188,53]; NTIPAliasStat["demonskilltab"] = [188,53];
|
||||
|
||||
NTIPAliasStat["unused189"] = 189;
|
||||
NTIPAliasStat["unused190"] = 190;
|
||||
|
||||
@@ -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: 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: 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 |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
@@ -11,6 +11,7 @@
|
||||
(OPTIONAL)
|
||||
(PALADIN
|
||||
(SORCERESS
|
||||
(WARLOCK
|
||||
A
|
||||
ABADDON
|
||||
ABHAYA
|
||||
@@ -75,6 +76,7 @@ AJHEED
|
||||
AKARA
|
||||
AKARAN
|
||||
AKARAT'S
|
||||
AL'DIABOLOS
|
||||
ALACRITY
|
||||
ALADDIN'S
|
||||
ALARIC
|
||||
@@ -175,6 +177,7 @@ ARREAT
|
||||
ARREAT'S
|
||||
ARROW
|
||||
ARROWS
|
||||
ARS
|
||||
ARSENAL
|
||||
ART
|
||||
ARTFUL
|
||||
@@ -339,6 +342,7 @@ BLADEBUCKLE
|
||||
BLADES
|
||||
BLAISE
|
||||
BLANCHED
|
||||
BLASPHEMOUS
|
||||
BLAST
|
||||
BLASTBARK
|
||||
BLAZE
|
||||
@@ -551,8 +555,8 @@ CHAMPION
|
||||
CHANCE
|
||||
CHANDELIER
|
||||
CHANG
|
||||
CHANNEL.
|
||||
CHANNEL:
|
||||
CHANNEL.
|
||||
CHANT
|
||||
CHAOS
|
||||
CHAOTIC
|
||||
@@ -561,8 +565,8 @@ CHARGE
|
||||
CHARGE-UP
|
||||
CHARGED
|
||||
CHARGES
|
||||
CHARGES)
|
||||
CHARGES:
|
||||
CHARGES)
|
||||
CHARM
|
||||
CHARSI
|
||||
CHARSI'S
|
||||
@@ -603,6 +607,7 @@ CLAYMORE
|
||||
CLEANSING
|
||||
CLEARED
|
||||
CLEAVER
|
||||
CLEFT
|
||||
CLEGLAW'S
|
||||
CLICK
|
||||
CLICKING
|
||||
@@ -625,6 +630,7 @@ COBALT
|
||||
COBRA
|
||||
COCOON
|
||||
CODE:
|
||||
CODEX
|
||||
COFFIN
|
||||
COIF
|
||||
COIL
|
||||
@@ -653,10 +659,11 @@ COMMUNAL
|
||||
COMPACT
|
||||
COMPARE
|
||||
COMPELLING
|
||||
COMPENDIUM
|
||||
COMPLETE
|
||||
COMPLETED
|
||||
COMPLETED.
|
||||
COMPLETED:
|
||||
COMPLETED.
|
||||
COMPOSITE
|
||||
CONCENTRATE
|
||||
CONCENTRATION
|
||||
@@ -826,6 +833,7 @@ DECKARD
|
||||
DECLARE
|
||||
DECOY
|
||||
DECREPIFY
|
||||
DEEP
|
||||
DEFEAT
|
||||
DEFENDER
|
||||
DEFENSE
|
||||
@@ -856,8 +864,8 @@ DESTINATION
|
||||
DESTROY
|
||||
DESTROYER
|
||||
DESTRUCTION
|
||||
DESTRUCTION'S
|
||||
DESTRUCTION.
|
||||
DESTRUCTION'S
|
||||
DETAIL
|
||||
DETAILS.
|
||||
DETECT
|
||||
@@ -879,8 +887,8 @@ DIE
|
||||
DIED
|
||||
DIES,
|
||||
DIFFERENCE
|
||||
DIFFERENCE.
|
||||
DIFFERENCE:
|
||||
DIFFERENCE.
|
||||
DIFFERENT
|
||||
DIFFICULTY
|
||||
DIFFICULTY:
|
||||
@@ -959,6 +967,7 @@ DRULAN'S
|
||||
DRUS
|
||||
DRY
|
||||
DUE
|
||||
DUL'MEPHISTOS
|
||||
DUN
|
||||
DUNE
|
||||
DUNES
|
||||
@@ -997,6 +1006,7 @@ EARS
|
||||
EARTH
|
||||
EARTHSHAKER
|
||||
EASE
|
||||
EASTERN
|
||||
EATER
|
||||
EATS
|
||||
EBURINE
|
||||
@@ -1412,8 +1422,8 @@ GOBLIN
|
||||
GODLY
|
||||
GOES
|
||||
GOLD
|
||||
GOLD.
|
||||
GOLD:
|
||||
GOLD.
|
||||
GOLDEN
|
||||
GOLDSKIN
|
||||
GOLDSTRIKE
|
||||
@@ -1460,6 +1470,7 @@ GRIFFON
|
||||
GRIFFON'S
|
||||
GRIM
|
||||
GRIM'S
|
||||
GRIMOIRE
|
||||
GRIN
|
||||
GRINDER
|
||||
GRINDING
|
||||
@@ -1782,8 +1793,8 @@ ITCHIES
|
||||
ITEM
|
||||
ITEM.
|
||||
ITEMS
|
||||
ITEMS.
|
||||
ITEMS:
|
||||
ITEMS.
|
||||
ITH
|
||||
ITHERA
|
||||
ITONYA
|
||||
@@ -1957,9 +1968,9 @@ LEORIC
|
||||
LESSON
|
||||
LESTRON'S
|
||||
LEVEL
|
||||
LEVEL)
|
||||
LEVEL-UP
|
||||
LEVEL:
|
||||
LEVEL)
|
||||
LEVELS
|
||||
LEVER
|
||||
LEVIATHAN
|
||||
@@ -1971,8 +1982,8 @@ LIDLESS
|
||||
LIEF
|
||||
LIENE
|
||||
LIFE
|
||||
LIFE/MANA
|
||||
LIFE:
|
||||
LIFE/MANA
|
||||
LIFECHOKE
|
||||
LIGHT
|
||||
LIGHTBRAND
|
||||
@@ -2136,12 +2147,13 @@ MAULER
|
||||
MAUSOLEUM
|
||||
MAW
|
||||
MAX
|
||||
MAX.
|
||||
MAX:
|
||||
MAX.
|
||||
MAXIMUM
|
||||
MAY
|
||||
MCAULEY'S
|
||||
MEASURE
|
||||
MEASURED
|
||||
MEAT
|
||||
MEATSCRAPE
|
||||
MECHANIC'S
|
||||
@@ -2261,8 +2273,8 @@ NARPHET
|
||||
NATALYA
|
||||
NATALYA'S
|
||||
NATURAL
|
||||
NATURE'S
|
||||
NATURE,
|
||||
NATURE'S
|
||||
NEAR
|
||||
NEARBY
|
||||
NEBUCHADNEZZAR'S
|
||||
@@ -2305,6 +2317,7 @@ NONE
|
||||
NOOSE
|
||||
NORD'S
|
||||
NORMAL
|
||||
NORTHERN
|
||||
NOSFERATU'S
|
||||
NOT
|
||||
NOVA
|
||||
@@ -2323,6 +2336,7 @@ OBLIVION
|
||||
OBSCENE
|
||||
OBSESSION
|
||||
OBSIDIAN
|
||||
OCCULT
|
||||
OCEAN
|
||||
OCHER
|
||||
OCULUS
|
||||
@@ -2349,6 +2363,7 @@ ONES
|
||||
ONLY
|
||||
ONLY)
|
||||
OOZE
|
||||
OPALVEIN
|
||||
OPEN
|
||||
OPENS
|
||||
OPTIMAL
|
||||
@@ -2471,8 +2486,8 @@ PLAYED
|
||||
PLAYER
|
||||
PLAYER.
|
||||
PLAYERS
|
||||
PLAYERS.
|
||||
PLAYERS:
|
||||
PLAYERS.
|
||||
PLEDGE
|
||||
PLUCKEYE
|
||||
PLUS
|
||||
@@ -2617,8 +2632,8 @@ RATMEN
|
||||
RATTLE
|
||||
RATTLECAGE
|
||||
RAVEN
|
||||
RAVEN'S
|
||||
RAVEN:
|
||||
RAVEN'S
|
||||
RAVENLORE
|
||||
RAVENS
|
||||
RAVENS:
|
||||
@@ -2692,9 +2707,9 @@ REND
|
||||
RENDER
|
||||
REPAIR
|
||||
REPAIRED
|
||||
REPAIRED.
|
||||
REPAIRED)
|
||||
REPAIRED),
|
||||
REPAIRED.
|
||||
REPAIRING
|
||||
REPAIRS
|
||||
REPEATING
|
||||
@@ -2804,6 +2819,7 @@ RUN/WALK
|
||||
RUNE
|
||||
RUNES
|
||||
RUNIC
|
||||
RUPTURE
|
||||
RUSSET
|
||||
RUST
|
||||
RUSTHANDLE
|
||||
@@ -2945,8 +2961,8 @@ SHELL
|
||||
SHELTER
|
||||
SHENK
|
||||
SHIELD
|
||||
SHIELD'S
|
||||
SHIELD:
|
||||
SHIELD'S
|
||||
SHIELDS
|
||||
SHIELDS:
|
||||
SHIFT
|
||||
@@ -3001,6 +3017,7 @@ SISTER'S
|
||||
SIX
|
||||
SIZE
|
||||
SIZE:
|
||||
SLING
|
||||
SKELETAL
|
||||
SKELETON
|
||||
SKELETONS
|
||||
@@ -3093,6 +3110,7 @@ SOULSTONE
|
||||
SOUND
|
||||
SOUNDING
|
||||
SOURCE
|
||||
SOUTHERN
|
||||
SPACE
|
||||
SPACING
|
||||
SPANISH
|
||||
@@ -3173,8 +3191,8 @@ STARLIGHT
|
||||
STARS
|
||||
STARS:
|
||||
STASH
|
||||
STASH.
|
||||
STASH:
|
||||
STASH.
|
||||
STAT
|
||||
STAT/SKILL
|
||||
STATE
|
||||
@@ -3303,8 +3321,8 @@ TAINTBREEDER
|
||||
TAINTED
|
||||
TAKE
|
||||
TAKEN
|
||||
TAKEN.
|
||||
TAKEN:
|
||||
TAKEN.
|
||||
TAKES
|
||||
TAL
|
||||
TALBERD'S
|
||||
@@ -3322,8 +3340,8 @@ TANNR
|
||||
TAP
|
||||
TARGE
|
||||
TARGET
|
||||
TARGET'S
|
||||
TARGET:
|
||||
TARGET'S
|
||||
TARGETS
|
||||
TARNHELM
|
||||
TAUNT
|
||||
@@ -3353,6 +3371,7 @@ TERRA'S
|
||||
TERRENE
|
||||
TERROR
|
||||
TERROR'S
|
||||
TEXT
|
||||
THADAR
|
||||
THAN
|
||||
THANK
|
||||
@@ -3431,6 +3450,7 @@ TOOTH
|
||||
TOOTHROW
|
||||
TOP
|
||||
TOPAZ
|
||||
TOR'BAALOS
|
||||
TORC
|
||||
TORCH
|
||||
TORKEL
|
||||
@@ -3645,6 +3665,7 @@ VOID
|
||||
VOIDBRINGER
|
||||
VOLCANIC
|
||||
VOLCANO
|
||||
VOLUME
|
||||
VOMIR
|
||||
VORTEX
|
||||
VOULGE
|
||||
@@ -3653,6 +3674,7 @@ VS.
|
||||
VULPINE
|
||||
VULTURE
|
||||
WACKER
|
||||
WAGER
|
||||
WAHEED
|
||||
WAIL
|
||||
WAILING
|
||||
@@ -3682,6 +3704,7 @@ WARDEN'S
|
||||
WARDER
|
||||
WARDING
|
||||
WARHOUND
|
||||
WARLOCK
|
||||
WARLORD'S
|
||||
WARMING
|
||||
WARMTH
|
||||
@@ -3691,8 +3714,8 @@ WARPED
|
||||
WARPS
|
||||
WARPSPEAR
|
||||
WARRIOR
|
||||
WARRIOR'S
|
||||
WARRIOR.
|
||||
WARRIOR'S
|
||||
WARRIV
|
||||
WARRIV'S
|
||||
WARSHRIKE
|
||||
@@ -3715,8 +3738,8 @@ WEAPON
|
||||
WEAPON:
|
||||
WEAPONS
|
||||
WEAPONS,
|
||||
WEAPONS.
|
||||
WEAPONS:
|
||||
WEAPONS.
|
||||
WEAVER
|
||||
WEB
|
||||
WEDDING
|
||||
@@ -3727,6 +3750,7 @@ WENDY
|
||||
WERE
|
||||
WEREBEAR
|
||||
WEREWOLF
|
||||
WESTERN
|
||||
WHALE
|
||||
WHAT
|
||||
WHEEL
|
||||
@@ -3831,10 +3855,10 @@ YETI
|
||||
YEW
|
||||
YOLK
|
||||
YOU
|
||||
YOU'VE
|
||||
YOU,
|
||||
YOU.
|
||||
YOU:
|
||||
YOU.
|
||||
YOU'VE
|
||||
YOUNG
|
||||
YOUR
|
||||
YOUTH
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
3LONE*dkbcy^v0I8
|
||||
@@ -76,7 +76,7 @@ if __name__ == "__main__":
|
||||
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}"
|
||||
installer_cmd = f"pyinstaller --onefile --noconsole --distpath {botty_dir}{key_cmd} --exclude-module graphviz --exclude-module keyboard --exclude-module mouse --exclude-module pyclick --exclude-module mouseinfo --paths .\\src --paths {args.conda_path}\\envs\\botty\\Lib\\site-packages src\\{exe}"
|
||||
os.system(installer_cmd)
|
||||
|
||||
os.system(f"cd {botty_dir} && mkdir config && cd ..")
|
||||
@@ -86,8 +86,8 @@ if __name__ == "__main__":
|
||||
shutil.copy("config/game.ini", f"{botty_dir}/config/")
|
||||
shutil.copy("config/params.ini", f"{botty_dir}/config/")
|
||||
shutil.copy("config/shop.ini", f"{botty_dir}/config/")
|
||||
shutil.copy("config/default.nip", f"{botty_dir}/config/")
|
||||
os.makedirs(f"{botty_dir}/config/nip", exist_ok=True)
|
||||
shutil.copy("config/default.bnip", f"{botty_dir}/config/")
|
||||
os.makedirs(f"{botty_dir}/config/bnip", exist_ok=True)
|
||||
shutil.copy("README.md", f"{botty_dir}/")
|
||||
shutil.copytree("assets", f"{botty_dir}/assets")
|
||||
clean_up()
|
||||
@@ -97,6 +97,13 @@ if __name__ == "__main__":
|
||||
new_name = ''.join(random.choices(string.ascii_letters, k=random.randint(6, 14)))
|
||||
os.rename(f'{botty_dir}/main.exe', f'{botty_dir}/{new_name}.exe')
|
||||
|
||||
# Rename main.exe to avoid Warden flagging the obvious name
|
||||
# In CI/production builds (env BOTTY_NO_RENAME=1) keep main.exe as-is
|
||||
if not args.random_name and not os.environ.get("BOTTY_NO_RENAME"):
|
||||
new_name = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
|
||||
os.rename(f'{botty_dir}/main.exe', f'{botty_dir}/{new_name}.exe')
|
||||
print(f"Renamed main.exe -> {new_name}.exe")
|
||||
|
||||
if new_version_code is not None:
|
||||
os.system(f'git add .')
|
||||
os.system(f'git commit -m "Bump version to v{args.version}"')
|
||||
|
||||
@@ -48,30 +48,40 @@
|
||||
[Name] == Burningessenceofterror
|
||||
[Name] == Festeringessenceofdestruction
|
||||
|
||||
//-----------------------------------------
|
||||
//================= Shards ================
|
||||
//-----------------------------------------
|
||||
|
||||
[Name] == Northernworldstoneshard
|
||||
[Name] == Easternworldstoneshard
|
||||
[Name] == Southernworldstoneshard
|
||||
[Name] == Westernworldstoneshard
|
||||
[Name] == Deepworldstoneshard
|
||||
|
||||
//----------------------------------------
|
||||
//================= Runes ================
|
||||
//----------------------------------------
|
||||
|
||||
[Name] >= Lumrune && [Name] <= Zodrune
|
||||
//[Name] == Elrune
|
||||
//[Name] == Eldrune
|
||||
//[Name] == Tirrune
|
||||
//[Name] == Nefrune
|
||||
//[Name] == Ethrune
|
||||
//[Name] == Ithrune
|
||||
//[Name] == Talrune
|
||||
//[Name] == Ralrune
|
||||
//[Name] == Ortrune
|
||||
//[Name] == Thulrune
|
||||
//[Name] == Amnrune
|
||||
//[Name] == Solrune
|
||||
//[Name] == Shaelrune
|
||||
//[Name] == Dolrune
|
||||
//[Name] == Helrune
|
||||
//[Name] == Iorune
|
||||
//[Name] == Lumrune
|
||||
//[Name] == Korune
|
||||
//[Name] == Falrune
|
||||
[Name] >= Lemrune && [Name] <= Zodrune
|
||||
[Name] == Elrune
|
||||
[Name] == Eldrune
|
||||
[Name] == Tirrune
|
||||
[Name] == Nefrune
|
||||
[Name] == Ethrune
|
||||
[Name] == Ithrune
|
||||
[Name] == Talrune
|
||||
[Name] == Ralrune
|
||||
[Name] == Ortrune
|
||||
[Name] == Thulrune
|
||||
[Name] == Amnrune
|
||||
[Name] == Solrune
|
||||
[Name] == Shaelrune
|
||||
[Name] == Dolrune
|
||||
[Name] == Helrune
|
||||
[Name] == Iorune
|
||||
[Name] == Lumrune
|
||||
[Name] == Korune
|
||||
[Name] == Falrune
|
||||
//[Name] == Lemrune
|
||||
//[Name] == Pulrune
|
||||
//[Name] == Umrune
|
||||
@@ -91,34 +101,34 @@
|
||||
//================= Gems ================
|
||||
//---------------------------------------
|
||||
|
||||
//[Name] == Chippedamethyst
|
||||
//[Name] == Chippeddiamond
|
||||
//[Name] == Chippedemerald
|
||||
//[Name] == Chippedruby
|
||||
//[Name] == Chippedsapphire
|
||||
//[Name] == Chippedskull
|
||||
//[Name] == Chippedtopaz
|
||||
//[Name] == Flawedamethyst
|
||||
//[Name] == Flaweddiamond
|
||||
//[Name] == Flawedemerald
|
||||
//[Name] == Flawedruby
|
||||
//[Name] == Flawedsapphire
|
||||
//[Name] == Flawedskull
|
||||
//[Name] == Flawedtopaz
|
||||
//[Name] == Amethyst
|
||||
//[Name] == Diamond
|
||||
//[Name] == Emerald
|
||||
//[Name] == Ruby
|
||||
//[Name] == Skull
|
||||
//[Name] == Sapphire
|
||||
//[Name] == Topaz
|
||||
//[Name] == Flawlessamethyst
|
||||
//[Name] == Flawlessdiamond
|
||||
//[Name] == Flawlessemerald
|
||||
//[Name] == Flawlessruby
|
||||
//[Name] == Flawlesssapphire
|
||||
//[Name] == Flawlessskull
|
||||
//[Name] == Flawlesstopaz
|
||||
[Name] == Chippedamethyst
|
||||
[Name] == Chippeddiamond
|
||||
[Name] == Chippedemerald
|
||||
[Name] == Chippedruby
|
||||
[Name] == Chippedsapphire
|
||||
[Name] == Chippedskull
|
||||
[Name] == Chippedtopaz
|
||||
[Name] == Flawedamethyst
|
||||
[Name] == Flaweddiamond
|
||||
[Name] == Flawedemerald
|
||||
[Name] == Flawedruby
|
||||
[Name] == Flawedsapphire
|
||||
[Name] == Flawedskull
|
||||
[Name] == Flawedtopaz
|
||||
[Name] == Amethyst
|
||||
[Name] == Diamond
|
||||
[Name] == Emerald
|
||||
[Name] == Ruby
|
||||
[Name] == Skull
|
||||
[Name] == Sapphire
|
||||
[Name] == Topaz
|
||||
[Name] == Flawlessamethyst
|
||||
[Name] == Flawlessdiamond
|
||||
[Name] == Flawlessemerald
|
||||
[Name] == Flawlessruby
|
||||
[Name] == Flawlesssapphire
|
||||
[Name] == Flawlessskull
|
||||
[Name] == Flawlesstopaz
|
||||
//[Name] == Perfectamethyst
|
||||
//[Name] == Perfectdiamond
|
||||
//[Name] == Perfectemerald
|
||||
@@ -135,9 +145,9 @@
|
||||
//================= Grand Charms ================
|
||||
//-----------------------------------------------
|
||||
|
||||
[Name] == Grandcharm && [Quality] == Magic # [Coldresist]+[Fireresist]+[Lightresist]+[Poisonresist] >= 60 // Plain +15 All Res
|
||||
[Name] == Grandcharm && [Quality] == Magic # [Coldresist]+[Fireresist]+[Lightresist]+[Poisonresist] >= 40 // Plain +10 All Res
|
||||
[Name] == Grandcharm && [Quality] == Magic # [Itemaddskilltab] >= 1 // All Plain Skillers
|
||||
//[Name] == Grandcharm && [Quality] == Magic # ([Coldresist] >= 30 || [Fireresist] >= 30 || [Lightresist] >= 30 || [Poisonresist] >= 30) // All +30 Single Res Gc
|
||||
//[Name] == Grandcharm && [Quality] == Magic # ([Coldresist] >= 20 || [Fireresist] >= 25 || [Lightresist] >= 25 || [Poisonresist] >= 30) // All +25 Single Res Gc
|
||||
//[Name] == Grandcharm && [Quality] == Magic && [Flag] != Ethereal # [Maxhp] >= 40 // All +40 Max Hp Gc
|
||||
//[Name] == Grandcharm && [Quality] == Magic # [Fhr] >= 12 && [Itemaddskilltab] >= 1 // All Plain Fhr Skillers
|
||||
//[Name] == Grandcharm && [Quality] == Magic # [Frw] >= 7 && [Itemaddskilltab] >= 1 // All Plain Frw Skillers
|
||||
@@ -251,13 +261,15 @@
|
||||
//================= Small Charms ================
|
||||
//-----------------------------------------------
|
||||
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Coldresist]+[Fireresist]+[Lightresist]+[Poisonresist] >= 20 // +5 All Res
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Coldresist]+[Fireresist]+[Lightresist]+[Poisonresist] >= 12 // +3 All Res
|
||||
[Name] == Largecharm && [Quality] == Magic # [Coldresist]+[Fireresist]+[Lightresist]+[Poisonresist] >= 24 // +6 All Res
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Maxhp] >= 15 // Plain +Hp
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Itemmagicbonus] >= 7 // Plain +Magic Find
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Coldresist] >= 11 // Plain +Cold Res
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Fireresist] >= 11 // Plain +Fire Res
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Lightresist] >= 11 // Plain +Light Res
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Poisonresist] >= 11 // Plain +Poison Res
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Maxmana] >= 15 // Plain +Mana
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Itemmagicbonus] >= 5 // Plain +Magic Find
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Coldresist] >= 8 // Plain +Cold Res
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Fireresist] >= 8 // Plain +Fire Res
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Lightresist] >= 8 // Plain +Light Res
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Poisonresist] >= 10 // Plain +Poison Res
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Poisonmindam] >= 100 // Plain +100 Poison Dmg
|
||||
|
||||
// Melee Small Charms ] >>>>>>>>>
|
||||
@@ -291,20 +303,20 @@
|
||||
//[Name] == Smallcharm && [Quality] == Magic # [Itemmagicbonus] >= 7 && [Maxdamage] >= 3 && [Tohit] >= 20
|
||||
|
||||
// <<<<<<<<< [ +Frw Small Charms ] >>>>>>>>>
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Frw] >= 3 && [Fireresist] >= 9
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Frw] >= 3 && [Coldresist] >= 9
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Frw] >= 3 && [Lightresist] >= 9
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Frw] >= 3 && [Poisonresist] >= 9
|
||||
//[Name] == Smallcharm && [Quality] == Magic # [Frw] >= 3 && [Fireresist] >= 9
|
||||
//[Name] == Smallcharm && [Quality] == Magic # [Frw] >= 3 && [Coldresist] >= 9
|
||||
//[Name] == Smallcharm && [Quality] == Magic # [Frw] >= 3 && [Lightresist] >= 9
|
||||
//[Name] == Smallcharm && [Quality] == Magic # [Frw] >= 3 && [Poisonresist] >= 9
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Frw] >= 3 && [Coldresist]+[Fireresist]+[Lightresist]+[Poisonresist] >= 15
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Frw] >= 3 && [Tohit] >= 20 && [Maxdamage] >= 3
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Frw] >= 3 && [Tohit] >= 36
|
||||
|
||||
// <<<<<<<<< [ +Fhr Small Charms ] >>>>>>>>>
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Fireresist] >= 11
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Coldresist] >= 11
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Lightresist] >= 11
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Poisonresist] >= 11
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Coldresist]+[Fireresist]+[Lightresist]+[Poisonresist] >= 20
|
||||
//[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Fireresist] >= 11
|
||||
//[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Coldresist] >= 11
|
||||
//[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Lightresist] >= 11
|
||||
//[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Poisonresist] >= 11
|
||||
//[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Coldresist]+[Fireresist]+[Lightresist]+[Poisonresist] >= 20
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Tohit] >= 20 && [Maxdamage] >= 3
|
||||
[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Tohit] >= 36
|
||||
|
||||
@@ -327,6 +339,8 @@
|
||||
// <<<<<<<<< [ Magic Amulets ] >>>>>>>>>
|
||||
//[Name] == Amulet && [Quality] == Magic # [Itemchargedskill] == 54 // Tele Amulet
|
||||
//[Type] == Amulet && [Quality] == Magic # [Itemaddskilltab] >= 3 && ([Fcr] >= 10 || [Itemmagicbonus] >= 35) // +3 Skill Fcr/Mf Amulet
|
||||
[Name] == Amulet && [Quality] == Magic # [Itemaddskilltab] >= 3 //+3 Amulet
|
||||
[Name] == Amulet && [Quality] == Magic # [Fireresist]+[Coldresist]+[Lightresist]+[Poisonresist] >= 84 //Chromatic amulet
|
||||
|
||||
//-------------------------------------------
|
||||
//================= Circlets ================
|
||||
@@ -368,7 +382,7 @@
|
||||
//([Name] == Crownshield || [Name] == Royalshield || [Name] == Sacredtarge || [Name] == Sacredrondache) && [Quality] == Magic # [Sockets] == 4 && [Itemfasterblockrate] == 30 // Paladin Jmoe
|
||||
//([Name] == Crownshield || [Name] == Royalshield || [Name] == Sacredtarge || [Name] == Sacredrondache) && [Quality] == Magic # [Sockets] == 4 && [Maxhp] >= 60 // Paladin Jmoc
|
||||
//([Name] == Crownshield || [Name] == Royalshield || [Name] == Sacredtarge || [Name] == Sacredrondache) && [Quality] == Magic # [Sockets] == 4 && [Itemreqpercent] == -30 // Paladin Jmos
|
||||
//[Type] == Voodooheads && [Quality] == Magic && [Class] >= Exceptional && [Flag] != Ethereal # ([Necromancerskills] >= 1 || [Poisonandboneskilltab] >= 1) && [Fbr] >= 30 && [Skillbonespear] == 1
|
||||
//[Type] == Voodooheads && [Quality] == Magic && [Class] >= Exceptional && [Flag] != Ethereal # ([Necromancerskills] >= 1 || [Poisonandboneskilltab] >= 1) && [Fbr] >= 30 && [Skillbonespear] >= 1
|
||||
|
||||
//------------------------------------------
|
||||
//================= Weapons ================
|
||||
@@ -378,7 +392,7 @@
|
||||
//[Name] == Throwingspear && [Quality] == Magic # [Warcriesskilltab] >= 3
|
||||
|
||||
// <<<<<<<<< [ Gg Club ] >>>>>>>>>
|
||||
([Name] == Club || [Name] == Spikedclub) && [Quality] == Magic # [Warcriesskilltab] >= 3 && [Strength] > 0
|
||||
//([Name] == Club || [Name] == Spikedclub) && [Quality] == Magic # [Warcriesskilltab] >= 3 && [Strength] > 0
|
||||
|
||||
// <<<<<<<<< [ Magic Scepters ] >>>>>>>>>
|
||||
//[Type] == Scepter && [Quality] == Magic # [Paladinskills]+[Skillblessedhammer] >= 5 && ([Skillconcentration] >= 3 || [Fcr] >= 10 && [Skillconcentration] >= 2)
|
||||
@@ -396,18 +410,18 @@
|
||||
//[Type] == Orb && [Quality] == Magic # [Fireskilltab]+[Skillhydra] >= 6 && [Skillfiremastery] >= 2 // Hydra Orb
|
||||
|
||||
// <<<<<<<<< [ Magic Javelins ] >>>>>>>>>
|
||||
([Name] == Maidenjavelin || [Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin) && [Quality] == Magic # [Amazonskills] == 2 && [Javelinandspearskilltab] >= 2 && [Ias] >= 30
|
||||
([Name] == Maidenjavelin || [Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin) && [Quality] == Magic # [Javelinandspearskilltab] >= 4 && [Ias] >= 30
|
||||
([Name] == Maidenjavelin || [Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin) && [Quality] == Magic # [Amazonskills] >= 2 && [Javelinandspearskilltab] >= 2 && [Ias] >= 30
|
||||
([Name] == Maidenjavelin || [Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin) && [Quality] == Magic # [Javelinandspearskilltab] >= 4 && [Ias] >= 30
|
||||
//([Name] == Maidenjavelin || [Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin) && [Quality] == Magic # [Amazonskills] == 2 && [Javelinandspearskilltab] >= 2 && [Ias] >= 30
|
||||
//([Name] == Maidenjavelin || [Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin) && [Quality] == Magic # [Javelinandspearskilltab] >= 4 && [Ias] >= 30
|
||||
//([Name] == Maidenjavelin || [Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin) && [Quality] == Magic # [Amazonskills] >= 2 && [Javelinandspearskilltab] >= 2 && [Ias] >= 30
|
||||
//([Name] == Maidenjavelin || [Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin) && [Quality] == Magic # [Javelinandspearskilltab] >= 4 && [Ias] >= 30
|
||||
|
||||
// <<<<<<<<< [ Magic Claws ] >>>>>>>>>
|
||||
//([Name] == Greatertalons || [Name] == Feralclaws || [Name] == Greaterclaws || [Name] == Runictalons) && [Quality] == Magic && [Flag] != Ethereal # [Ias] >= 40 && [Skilllightningsentry] >= 3
|
||||
//([Name] == Greatertalons || [Name] == Feralclaws || [Name] == Greaterclaws || [Name] == Runictalons) && [Quality] == Magic && [Flag] != Ethereal # [Ias] >= 40 && [Skillwakeoffire] >= 3
|
||||
([Name] == Greatertalons || [Name] == Feralclaws || [Name] == Greaterclaws || [Name] == Runictalons) && [Quality] == Magic && [Flag] != Ethereal # [Ias] >= 40 && [Skilllightningsentry]+[Assassinskills] >= 5
|
||||
([Name] == Greatertalons || [Name] == Feralclaws || [Name] == Greaterclaws || [Name] == Runictalons) && [Quality] == Magic && [Flag] != Ethereal # [Ias] >= 40 && [Skilllightningsentry]+[Trapsskilltab] >= 6
|
||||
([Name] == Greatertalons || [Name] == Feralclaws || [Name] == Greaterclaws || [Name] == Runictalons) && [Quality] == Magic && [Flag] != Ethereal # [Ias] >= 40 && [Skillwakeoffire]+[Assassinskills] >= 5
|
||||
([Name] == Greatertalons || [Name] == Feralclaws || [Name] == Greaterclaws || [Name] == Runictalons) && [Quality] == Magic && [Flag] != Ethereal # [Ias] >= 40 && [Skillwakeoffire]+[Trapsskilltab] >= 6
|
||||
//([Name] == Greatertalons || [Name] == Feralclaws || [Name] == Greaterclaws || [Name] == Runictalons) && [Quality] == Magic && [Flag] != Ethereal # [Ias] >= 40 && [Skilllightningsentry]+[Assassinskills] >= 5
|
||||
//([Name] == Greatertalons || [Name] == Feralclaws || [Name] == Greaterclaws || [Name] == Runictalons) && [Quality] == Magic && [Flag] != Ethereal # [Ias] >= 40 && [Skilllightningsentry]+[Trapsskilltab] >= 6
|
||||
//([Name] == Greatertalons || [Name] == Feralclaws || [Name] == Greaterclaws || [Name] == Runictalons) && [Quality] == Magic && [Flag] != Ethereal # [Ias] >= 40 && [Skillwakeoffire]+[Assassinskills] >= 5
|
||||
//([Name] == Greatertalons || [Name] == Feralclaws || [Name] == Greaterclaws || [Name] == Runictalons) && [Quality] == Magic && [Flag] != Ethereal # [Ias] >= 40 && [Skillwakeoffire]+[Trapsskilltab] >= 6
|
||||
|
||||
//------------------------------------------
|
||||
//================= Armors ================
|
||||
@@ -458,7 +472,8 @@
|
||||
[Type] == Amulet && [Quality] == Rare # [Itemaddskilltab] >= 2 && [Fcr] == 10 && [Strength] >= 6 && [Dexterity] >= 6 && ([Maxhp] >= 25 || [Maxmana] >= 60 || [Fireresist]+[Lightresist]+[Coldresist]+[Poisonresist] >= 20 || [Itemdamagetomana] >= 12)
|
||||
|
||||
// <<<<<<<<< [ Rare Rings ] >>>>>>>>>
|
||||
[Type] == Ring && [Quality] == Rare # [Fcr] >= 10 && [Strength] >= 15 && [Dexterity] >= 10 && ([Maxmana] >= 65 || [Hpregen] >= 5 || [Maxhp] >= 20 || [Fireresist]+[Lightresist]+[Coldresist]+[Poisonresist] >= 30) // Caster Ring
|
||||
[Type] == Ring && [Quality] == Magic # [Maxmana] >= 91 || [Fireresist]+[Lightresist]+[Coldresist]+[Poisonresist] >= 48 //Scintillating and Bahamut Magic Rings
|
||||
[Type] == Ring && [Quality] == Rare # [Fcr] >= 10 && ([Strength] >= 15 || [Maxmana] >= 50 || [Maxhp] >= 20 || [Fireresist]+[Lightresist]+[Coldresist]+[Poisonresist] >= 25) // Caster Ring
|
||||
[Type] == Ring && [Quality] == Rare # [Tohit] >= 100 && [Lifeleech]+[Manaleech] >= 9 // Melee Dual Leech Ring
|
||||
[Type] == Ring && [Quality] == Rare # [Tohit] >= 100 && [Strength] >= 15 && [Dexterity] >= 10 && ([Maxhp] >= 30 || [Fireresist]+[Lightresist]+[Coldresist]+[Poisonresist] >= 30) // Melee Dual Stat Ring
|
||||
[Type] == Ring && [Quality] == Rare # [Tohit] >= 100 && ([Mindamage] > 7 || [Maxdamage] > 9) && ([Strength]+[Dexterity] >= 10 || [Maxhp] >= 30 || [Fireresist]+[Lightresist]+[Coldresist]+[Poisonresist] >= 20) // Bva Ring
|
||||
@@ -543,29 +558,29 @@
|
||||
//------------------------------------------
|
||||
|
||||
// <<<<<<<<< [ Rare Claws ] +++++++++
|
||||
(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare # ([Assassinskills]+[Skilllightningsentry] >= 5 || [Assassinskills]+[Skillwakeoffire] >= 5 || [Trapsskilltab]+[Skilllightningsentry] >= 5 || [Trapsskilltab]+[Skillwakeoffire] >= 5) && [Ias] >= 20 && [Sockets] >= 0
|
||||
(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare # ([Assassinskills]+[Skilllightningsentry] >= 5 || [Assassinskills]+[Skillwakeoffire] >= 5 || [Trapsskilltab]+[Skilllightningsentry] >= 5 || [Trapsskilltab]+[Skillwakeoffire] >= 5) && [Ias] >= 40 && [Sockets] >= 0
|
||||
(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare # ([Assassinskills]+[Skilllightningsentry] >= 5 || [Assassinskills]+[Skillwakeoffire] >= 5 || [Trapsskilltab]+[Skilllightningsentry] >= 5 || [Trapsskilltab]+[Skillwakeoffire] >= 5) && [Ias] >= 30 && [Sockets] >= 0
|
||||
(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare # ([Assassinskills]+[Skilllightningsentry] >= 5 || [Assassinskills]+[Skillwakeoffire] >= 5 || [Trapsskilltab]+[Skilllightningsentry] >= 5 || [Trapsskilltab]+[Skillwakeoffire] >= 5) && [Ias] >= 30 && [Sockets] >= 0
|
||||
(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare && [Flag] == Ethereal # [Enhanceddamage] >= 300 && [Itemskillonattack] == 66 && [Ias] >= 20 && [Sockets] >= 0
|
||||
(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare && [Flag] == Ethereal # [Ias] >= 20 && ([Enhanceddamage] >= 400 || ([Enhanceddamage] >= 275 && [Itemtohitperlevel] >= 1)) && [Sockets] >= 0
|
||||
(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare && [Flag] == Ethereal # [Ias] >= 40 && ([Enhanceddamage] >= 350 || ([Enhanceddamage] >= 250 && [Itemtohitperlevel] >= 1)) && [Sockets] >= 0
|
||||
//(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare # ([Assassinskills]+[Skilllightningsentry] >= 5 || [Assassinskills]+[Skillwakeoffire] >= 5 || [Trapsskilltab]+[Skilllightningsentry] >= 5 || [Trapsskilltab]+[Skillwakeoffire] >= 5) && [Ias] >= 20 && [Sockets] >= 0
|
||||
//(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare # ([Assassinskills]+[Skilllightningsentry] >= 5 || [Assassinskills]+[Skillwakeoffire] >= 5 || [Trapsskilltab]+[Skilllightningsentry] >= 5 || [Trapsskilltab]+[Skillwakeoffire] >= 5) && [Ias] >= 40 && [Sockets] >= 0
|
||||
//(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare # ([Assassinskills]+[Skilllightningsentry] >= 5 || [Assassinskills]+[Skillwakeoffire] >= 5 || [Trapsskilltab]+[Skilllightningsentry] >= 5 || [Trapsskilltab]+[Skillwakeoffire] >= 5) && [Ias] >= 30 && [Sockets] >= 0
|
||||
//(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare # ([Assassinskills]+[Skilllightningsentry] >= 5 || [Assassinskills]+[Skillwakeoffire] >= 5 || [Trapsskilltab]+[Skilllightningsentry] >= 5 || [Trapsskilltab]+[Skillwakeoffire] >= 5) && [Ias] >= 30 && [Sockets] >= 0
|
||||
//(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare && [Flag] == Ethereal # [Enhanceddamage] >= 300 && [Itemskillonattack] == 66 && [Ias] >= 20 && [Sockets] >= 0
|
||||
//(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare && [Flag] == Ethereal # [Ias] >= 20 && ([Enhanceddamage] >= 400 || ([Enhanceddamage] >= 275 && [Itemtohitperlevel] >= 1)) && [Sockets] >= 0
|
||||
//(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare && [Flag] == Ethereal # [Ias] >= 40 && ([Enhanceddamage] >= 350 || ([Enhanceddamage] >= 250 && [Itemtohitperlevel] >= 1)) && [Sockets] >= 0
|
||||
|
||||
// <<<<<<<<< [ Rare Sorceress Orbs ] >>>>>>>>>
|
||||
[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillfireball] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillmeteor] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillblizzard] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillfrozenorb] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skilllightning] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillchainlightning] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillnova] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
[Type] == Orb && [Quality] == Rare # [Fireskilltab] >= 2 && [Skillfireball] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
[Type] == Orb && [Quality] == Rare # [Fireskilltab] >= 2 && [Skillmeteor] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
[Type] == Orb && [Quality] == Rare # [Coldskilltab] >= 2 && [Skillblizzard] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
[Type] == Orb && [Quality] == Rare # [Coldskilltab] >= 2 && [Skillfrozenorb] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
[Type] == Orb && [Quality] == Rare # [Lightningskilltab] >= 2 && [Skilllightning] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
[Type] == Orb && [Quality] == Rare # [Lightningskilltab] >= 2 && [Skillchainlightning] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
[Type] == Orb && [Quality] == Rare # [Lightningskilltab] >= 2 && [Skillnova] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
//[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillfireball] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
//[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillmeteor] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
//[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillblizzard] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
//[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillfrozenorb] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
//[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skilllightning] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
//[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillchainlightning] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
//[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillnova] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
//[Type] == Orb && [Quality] == Rare # [Fireskilltab] >= 2 && [Skillfireball] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
//[Type] == Orb && [Quality] == Rare # [Fireskilltab] >= 2 && [Skillmeteor] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
//[Type] == Orb && [Quality] == Rare # [Coldskilltab] >= 2 && [Skillblizzard] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
//[Type] == Orb && [Quality] == Rare # [Coldskilltab] >= 2 && [Skillfrozenorb] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
//[Type] == Orb && [Quality] == Rare # [Lightningskilltab] >= 2 && [Skilllightning] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
//[Type] == Orb && [Quality] == Rare # [Lightningskilltab] >= 2 && [Skillchainlightning] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
//[Type] == Orb && [Quality] == Rare # [Lightningskilltab] >= 2 && [Skillnova] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
|
||||
|
||||
// <<<<<<<<< [ Rare Enchant Orbs ] >>>>>>>>>
|
||||
//[Type] == Orb && [Quality] <= Rare # [Skillenchant] >= 2 && [Fireskilltab] == 3
|
||||
@@ -573,15 +588,14 @@
|
||||
//[Type] == Orb && [Quality] <= Rare # [Skillenchant] == 3 && [Sorceressskills] == 2
|
||||
|
||||
// <<<<<<<<< [ Rare Javelins ] >>>>>>>>>
|
||||
([Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin || [Name] == Maidenjavelin) && [Quality] == Rare # [Javelinandspearskilltab] >= 2 && [Amazonskills] >= 2 && [Ias] >= 30
|
||||
([Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin || [Name] == Maidenjavelin) && [Quality] == Rare # [Javelinandspearskilltab] >= 4 && [Ias] >= 30
|
||||
//([Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin || [Name] == Maidenjavelin) && [Quality] == Rare # [Javelinandspearskilltab] >= 2 && [Amazonskills] >= 2 && [Ias] >= 30
|
||||
//([Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin || [Name] == Maidenjavelin) && [Quality] == Rare # [Javelinandspearskilltab] >= 4 && [Ias] >= 30
|
||||
|
||||
|
||||
// <<<<<<<<< [ Rare Bows ] >>>>>>>>>
|
||||
//[Type] == Bow && [Quality] == Rare # [Itemskillonattack] == 66 && ([Enhanceddamage] >= 400 || ([Enhanceddamage] >= 300 && [Itemtohitperlevel] >= 1))
|
||||
//[Type] == Bow && [Quality] == Rare # ([Enhanceddamage] >= 400 || ([Enhanceddamage] >= 250 && [Itemskillonattack] == 66) || ([Enhanceddamage] >= 300 && [Itemtohitperlevel] >= 1)) && [Sockets] >= 0
|
||||
//[Type] == Bow && [Quality] == Rare # [Ias] >= 20 && ([Enhanceddamage] >= 300 || ([Enhanceddamage] >= 225 && [Itemskillonattack] == 66) || ([Enhanceddamage] >= 250 && [Itemtohitperlevel] >= 1)) && [Sockets] >= 0
|
||||
//[Type] == Bow && [Quality] == Rare && [Flag] != Ethereal # [Enhanceddamage] >= 250 && [218] >= 4 && [224] >= 33
|
||||
|
||||
//----------------------------------------
|
||||
//================= Belts ================
|
||||
@@ -595,9 +609,10 @@
|
||||
//================= Boots ================
|
||||
//----------------------------------------
|
||||
|
||||
[Type] == Boots && [Quality] == Rare && [Flag] != Ethereal # [Frw] >= 30 && [Fhr] >= 10 && [Dexterity] >= 8 && ([Lightresist]+[Fireresist] >= 35 || [Coldresist]+[Lightresist] >= 35 || [Fireresist]+[Coldresist] >= 35) // Dual Res
|
||||
[Type] == Boots && [Quality] == Rare && [Flag] != Ethereal # [Frw] >= 30 && [Fhr] >= 10 && [Dexterity] >= 8 && [Coldresist] >= 20 && [Fireresist] >= 20 && [Lightresist] >= 20 // Tri Res
|
||||
[Type] == Boots && [Quality] == Rare && [Flag] == Ethereal # [Frw] >= 20 && [Fhr] >= 10 && [Dexterity] >= 8 && [Fireresist]+[Lightresist]+[Coldresist] >= 60 && [Itemreplenishdurability] >= 1 // Eth Rep
|
||||
[Type] == Boots && [Quality] == Rare && [Flag] != Ethereal # [Frw] >= 30 && [Lightresist] >= 37 // Light Res boots
|
||||
[Type] == Boots && [Quality] == Rare && [Flag] != Ethereal # [Frw] >= 30 && ([Lightresist]+[Fireresist] >= 50 || [Coldresist]+[Lightresist] >= 50 || [Fireresist]+[Coldresist] >= 50) // Dual Res
|
||||
[Type] == Boots && [Quality] == Rare && [Flag] != Ethereal # [Frw] >= 30 && [Fireresist]+[Lightresist]+[Coldresist]+[Poisonresist] >= 50 // Tri Res
|
||||
[Type] == Boots && [Quality] == Rare && [Flag] == Ethereal # [Frw] >= 20 && [Fireresist]+[Lightresist]+[Coldresist] >= 50 && [Itemreplenishdurability] >= 1 // Eth Rep
|
||||
|
||||
//-----------------------------------------
|
||||
//================= Gloves ================
|
||||
@@ -629,6 +644,7 @@
|
||||
//[Name] == Scarabhusk && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 0 // Plain
|
||||
//[Name] == Wyrmhide && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 0 // Plain
|
||||
//[Name] == Lightplate && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 0 // Plain
|
||||
//[Name] == Shadowplate && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 0 // Plain
|
||||
//[Name] == Sacredarmor && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 0 // Plain
|
||||
|
||||
// <<<<<<<<< [ Non-Ethereal Socketed Armor ] >>>>>>>>>
|
||||
@@ -640,40 +656,44 @@
|
||||
//[Name] == Scarabhusk && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 3 // Socketed
|
||||
//[Name] == Wyrmhide && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 3 // Socketed
|
||||
//[Name] == Lightplate && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 3 // Socketed
|
||||
//[Name] == Shadowplate && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
|
||||
//[Name] == Sacredarmor && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
|
||||
|
||||
// <<<<<<<<< [ Non-Ethereal Superior Non-Socketed Armor ] >>>>>>>>>
|
||||
[Name] == Archonplate && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
|
||||
[Name] == Duskshroud && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
|
||||
[Name] == Mageplate && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
|
||||
[Name] == Greathauberk && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
|
||||
[Name] == Wirefleece && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
|
||||
[Name] == Scarabhusk && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
|
||||
[Name] == Wyrmhide && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
|
||||
//[Name] == Archonplate && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
|
||||
//[Name] == Duskshroud && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
|
||||
//[Name] == Mageplate && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
|
||||
//[Name] == Greathauberk && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
|
||||
//[Name] == Wirefleece && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
|
||||
//[Name] == Scarabhusk && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
|
||||
//[Name] == Wyrmhide && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
|
||||
//[Name] == Lightplate && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
|
||||
//[Name] == Shadowplate && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
|
||||
//[Name] == Sacredarmor && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
|
||||
|
||||
// <<<<<<<<< [ Non-Ethereal Superior Socketed Armor ] >>>>>>>>>
|
||||
[Name] == Archonplate && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
|
||||
[Name] == Duskshroud && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
|
||||
[Name] == Mageplate && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
|
||||
[Name] == Greathauberk && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
|
||||
[Name] == Wirefleece && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
|
||||
[Name] == Scarabhusk && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
|
||||
[Name] == Wyrmhide && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
|
||||
//[Name] == Archonplate && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
|
||||
//[Name] == Duskshroud && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
|
||||
//[Name] == Mageplate && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
|
||||
//[Name] == Greathauberk && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
|
||||
//[Name] == Wirefleece && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
|
||||
//[Name] == Scarabhusk && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
|
||||
//[Name] == Wyrmhide && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
|
||||
//[Name] == Lightplate && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
|
||||
//[Name] == Shadowplate && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 4 // Socketed Sup
|
||||
//[Name] == Sacredarmor && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 4 // Socketed Sup
|
||||
|
||||
// <<<<<<<<< [ Ethereal Socketed/Non-Socketed Armor ] >>>>>>>>>
|
||||
[Name] == Archonplate && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) // Socketed Eth
|
||||
[Name] == Duskshroud && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) // Socketed Eth
|
||||
//[Name] == Mageplate && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] == 3) // Socketed Eth
|
||||
//[Name] == Greathauberk && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) // Socketed Eth
|
||||
[Name] == Greathauberk && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) // Socketed Eth
|
||||
[Name] == Wirefleece && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) // Socketed Eth
|
||||
[Name] == Scarabhusk && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) // Socketed Eth
|
||||
[Name] == Wyrmhide && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) // Socketed Eth
|
||||
//[Name] == Lightplate && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) // Socketed Eth
|
||||
[Name] == Sacredarmor && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] == 4) // Socketed Eth
|
||||
[Name] == Shadowplate && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) // Socketed Eth
|
||||
[Name] == Sacredarmor && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) // Socketed Eth
|
||||
|
||||
// <<<<<<<<< [ Ethereal Superior Socketed/Non-Socketed Armor ] >>>>>>>>>
|
||||
[Name] == Archonplate && [Quality] == Superior && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) && [Enhanceddefense] >= 10 // Socketed Eth Sup
|
||||
@@ -683,14 +703,19 @@
|
||||
[Name] == Wirefleece && [Quality] == Superior && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) && [Enhanceddefense] >= 10 // Socketed Eth Sup
|
||||
[Name] == Scarabhusk && [Quality] == Superior && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) && [Enhanceddefense] >= 10 // Socketed Eth Sup
|
||||
[Name] == Wyrmhide && [Quality] == Superior && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) && [Enhanceddefense] >= 10 // Socketed Eth Sup
|
||||
[Name] == Lightplate && [Quality] == Superior && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] == 3) && [Enhanceddefense] >= 10 // Socketed Eth Sup
|
||||
[Name] == Sacredarmor && [Quality] == Superior && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) && [Enhanceddefense] >= 10 // Socketed Eth Sup
|
||||
//[Name] == Lightplate && [Quality] == Superior && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] == 3) && [Enhanceddefense] >= 10 // Socketed Eth Sup
|
||||
[Name] == Shadowplate && [Quality] == Superior && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) && [Enhanceddefense] >= 10 // Socketed Eth Sup
|
||||
[Name] == Sacredarmor && [Quality] == Superior && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) && [Enhanceddefense] >= 10 // Socketed Eth Sup
|
||||
|
||||
//-----------------------------------------------
|
||||
//================= Helmet Bases ================
|
||||
//-----------------------------------------------
|
||||
[Name] == Crown && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 3 //3os helm twink
|
||||
[Name] == Mask && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 3 //3os helm twink
|
||||
[Name] == Deathmask && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 3 //3os helm
|
||||
|
||||
//([Name] == Corona || [Name] == Demonhead || [Name] == Bonevisage || [Name] == Diadem) && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 0
|
||||
//([Name] == Corona || [Name] == Demonhead || [Name] == Bonevisage || [Name] == Diadem) && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 3 // 3os Socketed
|
||||
([Name] == Corona || [Name] == Demonhead || [Name] == Bonevisage || [Name] == Diadem) && [Quality] == Normal # [Sockets] == 3 // Cure base
|
||||
([Name] == Corona || [Name] == Demonhead || [Name] == Bonevisage || [Name] == Diadem) && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] == 0 // Delirium Or Dream Perfect
|
||||
([Name] == Corona || [Name] == Demonhead || [Name] == Bonevisage || [Name] == Diadem) && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] == 3 // 3os Socketed Delirium Or Dream Perfect
|
||||
|
||||
@@ -727,7 +752,7 @@
|
||||
|
||||
// <<<<<<<<< [ White/Eth Superior/Non-Superior Socketed/Non-Socketed Shields ] >>>>>>>>>
|
||||
//([Name] == Akarantarge || [Name] == Akaranrondache || [Name] == Crownshield) && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [Allresist] >= 40 // V/Lld Paladin Shield Base
|
||||
//[Name] == Monarch && [Quality] == Normal # [Sockets] == 0 // Plain Monarch
|
||||
[Name] == Monarch && [Quality] == Normal # [Sockets] == 0 // Plain Monarch
|
||||
[Name] == Monarch && [Quality] == Normal # [Sockets] == 4 //4os Monarch
|
||||
[Name] == Monarch && [Quality] == Superior # [Enhanceddefense] >= 10 && [Sockets] == 0 //Superior Plain Monarch
|
||||
[Name] == Monarch && [Quality] == Superior # [Enhanceddefense] >= 10 && [Sockets] == 4 //4os Superior Monarch
|
||||
@@ -764,8 +789,8 @@
|
||||
|
||||
// <<<<<<<<< [ Non-Ethereal Superior 1h Non-Socketed Weapons ] >>>>>>>>>
|
||||
[Name] == Berserkeraxe && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddamage] >= 10 // Plain Superior
|
||||
[Name] == Crystalsword && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddamage] >= 10 // Plain Superior
|
||||
[Name] == Flail && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddamage] >= 10 // Plain Superior
|
||||
//[Name] == Crystalsword && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddamage] >= 10 // Plain Superior
|
||||
//[Name] == Flail && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddamage] >= 10 // Plain Superior
|
||||
[Name] == Phaseblade && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddamage] >= 10 // Plain Superior
|
||||
[Name] == Warscepter && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && ([Skillblessedhammer] >= 3 || [Skillconviction] >= 3 || [Skillfistoftheheavens] >= 3 || [Skillfanaticism] >= 3 || [Skillholyshield] >= 3) && [Enhanceddamage] >= 0 // Plain Superior
|
||||
[Name] == Divinescepter && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && ([Skillblessedhammer] >= 3 || [Skillconviction] >= 3 || [Skillfistoftheheavens] >= 3 || [Skillfanaticism] >= 3 || [Skillholyshield] >= 3) && [Enhanceddamage] >= 0 // Plain Superior
|
||||
@@ -775,9 +800,16 @@
|
||||
|
||||
// <<<<<<<<< [ Non-Ethereal 1h Socketed Weapons ] >>>>>>>>>
|
||||
//[Name] == Berserkeraxe && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 5 // Socketed
|
||||
//[Name] == Crystalsword && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
|
||||
//[Name] == Flail && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
|
||||
//[Name] == Phaseblade && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 5 // Socketed
|
||||
[Name] == Crystalsword && [Quality] == Normal # [Sockets] == 4 // Spirit base mid
|
||||
[Name] == Broadsword && [Quality] == Normal # [Sockets] == 4 // Spirit base mid
|
||||
[Name] == Crystalsword && [Quality] == Normal # [Sockets] == 5 //CTA base
|
||||
[Name] == Doubleaxe && [Quality] == Normal # [Sockets] == 5 // CTA base
|
||||
[Name] == Flail && [Quality] == Normal # [Sockets] == 4 // Hoto base
|
||||
//[Name] == Militarypick && [Quality] == Normal # [Sockets] == 3 // Crescent Moon base
|
||||
//[Name] == Militarypick && [Quality] == Normal # [Sockets] == 5 // Doom base
|
||||
([Name] == Kris || [Name] == Cinquedeas || [Name] == Fangedknife) && [Quality] == Normal # [Skillabyss] >= 1 && ([Sockets] == 0 || [Sockets] == 3) //Void base
|
||||
//[Name] == Phaseblade && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 5 // Socketed Greif base
|
||||
//[Name] == Phaseblade && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 3 // Socketed Lawbringer base
|
||||
//[Name] == Warscepter && [Quality] == Normal && [Flag] != Ethereal # ([Skillblessedhammer] >= 3 || [Skillconviction] >= 3 || [Skillfistoftheheavens] >= 3 || [Skillfanaticism] >= 3 || [Skillholyshield] >= 3) && [Sockets] == 5 // Socketed
|
||||
//[Name] == Divinescepter && [Quality] == Normal && [Flag] != Ethereal # ([Skillblessedhammer] >= 3 || [Skillconviction] >= 3 || [Skillfistoftheheavens] >= 3 || [Skillfanaticism] >= 3 || [Skillholyshield] >= 3) && [Sockets] == 5 // Socketed
|
||||
//[Name] == Caduceus && [Quality] == Normal && [Flag] != Ethereal # ([Skillblessedhammer] >= 3 || [Skillconviction] >= 3 || [Skillfistoftheheavens] >= 3 || [Skillfanaticism] >= 3 || [Skillholyshield] >= 3) // Socketed
|
||||
@@ -787,25 +819,25 @@
|
||||
|
||||
// <<<<<<<<< [ Non-Ethereal Superior 1h Socketed Weapons ] >>>>>>>>>
|
||||
[Name] == Berserkeraxe && [Quality] == Superior && [Flag] != Ethereal # [Sockets] >= 5 && [Enhanceddamage] >= 10 // Socketed Superior
|
||||
[Name] == Crystalsword && [Quality] == Superior && [Flag] != Ethereal # [Sockets] >= 4 && [Enhanceddamage] >= 10 // Socketed Superiro
|
||||
[Name] == Flail && [Quality] == Superior && [Flag] != Ethereal # [Sockets] >= 4 && [Enhanceddamage] >= 14 // Socketed Superior
|
||||
//[Name] == Crystalsword && [Quality] == Superior && [Flag] != Ethereal # [Sockets] >= 4 && [Enhanceddamage] >= 10 // Socketed Superiro
|
||||
//[Name] == Flail && [Quality] == Superior && [Flag] != Ethereal # [Sockets] >= 4 && [Enhanceddamage] >= 14 // Socketed Superior
|
||||
[Name] == Phaseblade && [Quality] == Superior && [Flag] != Ethereal # [Sockets] >= 5 && [Enhanceddamage] >= 10 // Socketed Superior
|
||||
[Name] == Warscepter && [Quality] == Superior && [Flag] != Ethereal # ([Skillblessedhammer] >= 3 || [Skillconviction] >= 3 || [Skillfistoftheheavens] >= 3 || [Skillfanaticism] >= 3 || [Skillholyshield] >= 3) && [Sockets] == 5 && [Enhanceddamage] >= 0 // Socketed Superior
|
||||
[Name] == Divinescepter && [Quality] == Superior && [Flag] != Ethereal # ([Skillblessedhammer] >= 3 || [Skillconviction] >= 3 || [Skillfistoftheheavens] >= 3 || [Skillfanaticism] >= 3 || [Skillholyshield] >= 3) && [Sockets] == 5 && [Enhanceddamage] >= 0 // Socketed Superior
|
||||
[Name] == Caduceus && [Quality] == Superior && [Flag] != Ethereal # ([Skillblessedhammer] >= 3 || [Skillconviction] >= 3 || [Skillfistoftheheavens] >= 3 || [Skillfanaticism] >= 3 || [Skillholyshield] >= 3) && [Sockets] == 5 && [Enhanceddamage] >= 0 // Socketed Superior
|
||||
[Name] == Crypticsword && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 3 // Socketed Superior
|
||||
[Name] == Mythicalsword && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 3 // Socketed Superior
|
||||
//[Name] == Crypticsword && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 3 // Socketed Superior
|
||||
//[Name] == Mythicalsword && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 3 // Socketed Superior
|
||||
|
||||
// <<<<<<<<< [ Ethereal 1h Superior/Non-Superior Socketed/Non-Socketed Weapons ] >>>>>>>>>
|
||||
[Name] == Berserkeraxe && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 5) // Socketed Eth
|
||||
[Name] == Crystalsword && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
|
||||
[Name] == Flail && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
|
||||
//[Name] == Crystalsword && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
|
||||
//[Name] == Flail && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
|
||||
[Name] == Phaseblade && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 5) // Socketed Eth
|
||||
[Name] == Warscepter && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Skillblessedhammer] >= 3 || [Skillconviction] >= 3 || [Skillfistoftheheavens] >= 3 || [Skillfanaticism] >= 3 || [Skillholyshield] >= 3) && ([Sockets] == 0 || [Sockets] == 5) // Socketed Eth
|
||||
[Name] == Divinescepter && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Skillblessedhammer] >= 3 || [Skillconviction] >= 3 || [Skillfistoftheheavens] >= 3 || [Skillfanaticism] >= 3 || [Skillholyshield] >= 3) && ([Sockets] == 0 || [Sockets] == 5) // Socketed Eth
|
||||
[Name] == Caduceus && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Skillblessedhammer] >= 3 || [Skillconviction] >= 3 || [Skillfistoftheheavens] >= 3 || [Skillfanaticism] >= 3 || [Skillholyshield] >= 3) && ([Sockets] == 0 || [Sockets] == 5) // Socketed Eth
|
||||
[Name] == Crypticsword && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] == 3) // Socketed Eth
|
||||
[Name] == Mythicalsword && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] == 3) // Socketed Eth
|
||||
//[Name] == Crypticsword && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] == 3) // Socketed Eth
|
||||
//[Name] == Mythicalsword && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] == 3) // Socketed Eth
|
||||
|
||||
// <<<<<<<<< [ Ethereal Superior 1h Socketed/Non-Socketed Weapons ] >>>>>>>>>
|
||||
[Name] == Berserkeraxe && [Quality] == Superior && [Flag] == Ethereal # [Enhanceddamage] >= 10 && ([Sockets] == 0 || [Sockets] >= 5) // Socketed Eth Sup //Death, Ebotd
|
||||
@@ -825,12 +857,20 @@
|
||||
//[Name] == Mancatcher && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 0 // Plain
|
||||
|
||||
// <<<<<<<<< [ Non-Ethereal 2h Non-Superior Socketed Weapons ] >>>>>>>>>
|
||||
[Name] == Warscythe && [Quality] == Normal # [Sockets] == 4 //Exceptional Insight bases mid
|
||||
[Name] == Bill && [Quality] == Normal # [Sockets] == 4
|
||||
[Name] == BattleScythe && [Quality] == Normal # [Sockets] == 4
|
||||
[Name] == Partizan && [Quality] == Normal # [Sockets] == 4
|
||||
[Name] == Becdecorbin && [Quality] == Normal # [Sockets] == 4
|
||||
[Name] == Partizan && [Quality] == Normal # [Sockets] == 4
|
||||
[Name] == Grimscythe && [Quality] == Normal # [Sockets] == 4
|
||||
[Name] == Crypticaxe && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 4 //Elite Insight bases mid
|
||||
[Name] == Greatpoleaxe && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 4
|
||||
[Name] == Giantthresher && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 4
|
||||
[Name] == Thresher && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 4
|
||||
|
||||
//[Name] == Warpike && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
|
||||
//[Name] == Colossusvoulge && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
|
||||
//[Name] == Thresher && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
|
||||
//[Name] == Giantthresher && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
|
||||
//[Name] == Crypticaxe && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
|
||||
//[Name] == Greatpoleaxe && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
|
||||
//[Name] == Ghostspear && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
|
||||
//[Name] == Mancatcher && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
|
||||
|
||||
@@ -857,13 +897,15 @@
|
||||
//[Name] == Mancatcher && [Quality] == Superior && [Flag] != Ethereal # [Sockets] >= 4 && [Enhanceddamage] >= 10 // Socketed Sup
|
||||
|
||||
// <<<<<<<<< [ Ethereal 2h Socketed/Non-Socketed Weapons ] >>>>>>>>>
|
||||
[Name] == Warpike && [Quality] == Normal && [Flag] == Ethereal # [Sockets] == 6
|
||||
[Name] == Colossusvoulge && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
|
||||
[Name] == Warpike && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4 ) //Echo Strike base
|
||||
[Name] == Thundermaul && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] == 4 || [Sockets] == 6 ) //Echo Strike base
|
||||
//[Name] == Scythe && [Quality] == Normal && [Flag] == Ethereal # [Sockets] == 4
|
||||
[Name] == Colossusvoulge && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Eth Insight Base
|
||||
[Name] == Thresher && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
|
||||
[Name] == Giantthresher && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
|
||||
[Name] == Crypticaxe && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
|
||||
[Name] == Greatpoleaxe && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
|
||||
[Name] == Ghostspear && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
|
||||
//[Name] == Ghostspear && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
|
||||
[Name] == Mancatcher && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
|
||||
|
||||
// <<<<<<<<< [ Ethereal Superior 2h Socketed/Non-Socketed Weapons ] >>>>>>>>>
|
||||
@@ -881,38 +923,45 @@
|
||||
//------------------------------------------------------
|
||||
|
||||
// <<<<<<<<< [ Non-Ethereal Superior/Non-Superior Assassin Claws ] >>>>>>>>>
|
||||
[Type] == Assassinclaw && [Class] == Elite && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # ([Skilllightningsentry] >= 3 || [Skilldeathsentry] >= 3) && [Sockets] == 0 // Chaos
|
||||
[Type] == Assassinclaw && [Class] == Elite && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # ([Skilllightningsentry] >= 3 || [Skilldeathsentry] >= 3) && [Sockets] == 3 // Chaos Socketed
|
||||
[Type] == Assassinclaw && [Class] == Elite && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # ([Skillweaponblock] >= 3 || [Skillclawmastery] >= 2 || [Skillfade] >= 2 || [Skillbladeshield] >= 2 || [Skilldragonflight] >= 2 || [Skillmindblast] >= 1) && [Sockets] == 0 // Chaos
|
||||
[Type] == Assassinclaw && [Class] == Elite && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # ([Skillweaponblock] >= 3 || [Skillclawmastery] >= 2 || [Skillfade] >= 2 || [Skillbladeshield] >= 2 || [Skilldragonflight] >= 2 || [Skillmindblast] >= 1) && [Sockets] == 3 // Chaos Socketed
|
||||
[Type] == Assassinclaw && [Class] == Elite && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [Skillbladeshield] >= 3 && ([Skillweaponblock] >= 3 || [Skillclawmastery] >= 2 || [Skillfade] >= 2 || [Skillvenom] >= 2 || [Skilldragonflight] >= 2 || [Skillmindblast] >= 1) && [Sockets] == 0 // Chaos
|
||||
[Type] == Assassinclaw && [Class] == Elite && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [Skillbladeshield] >= 3 && ([Skillweaponblock] >= 3 || [Skillclawmastery] >= 2 || [Skillfade] >= 2 || [Skillvenom] >= 2 || [Skilldragonflight] >= 2 || [Skillmindblast] >= 1) && [Sockets] == 3 // Chaos Socketed
|
||||
[Type] == Assassinclaw && [Class] == Elite && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [Skillweaponblock] >= 3 && ([Skillclawmastery] >= 2 || [Skillfade] >= 2 || [Skillvenom] >= 2 || [Skillbladeshield] >= 2 || [Skilldragonflight] >= 2 || [Skillmindblast] >= 1) && [Sockets] == 0 // Chaos
|
||||
[Type] == Assassinclaw && [Class] == Elite && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [Skillweaponblock] >= 3 && ([Skillclawmastery] >= 2 || [Skillfade] >= 2 || [Skillvenom] >= 2 || [Skillbladeshield] >= 2 || [Skilldragonflight] >= 2 || [Skillmindblast] >= 1) && [Sockets] == 3 // Chaos Socketed
|
||||
//([Name] == Suwayyah || [Name] == FeralClaws || [Name] == RunicTalons || [Name] == GreaterTalons || [Name] == GreaterClaws) && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [SkillLightningsentry] >= 3 && [Skillfade] >= 3 && ([Skillbladeshield] >= 3 || [Skillweaponblock] >= 3 || [Skillclawmastery] >= 2 || [Skillvenom] >= 2 || [Skilldragonflight] >= 2 || [Skillmindblast] >= 1) && [Sockets] == 0 // Chaos
|
||||
//([Name] == Suwayyah || [Name] == FeralClaws || [Name] == RunicTalons || [Name] == GreaterTalons || [Name] == GreaterClaws) && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [SkillLightningsentry] >= 3 && [Skillfade] >= 3 && ([Skillbladeshield] >= 3 || [Skillweaponblock] >= 3 || [Skillclawmastery] >= 2 || [Skillvenom] >= 2 || [Skilldragonflight] >= 2 || [Skillmindblast] >= 1) && [Sockets] == 3 // Chaos Socketed
|
||||
//([Name] == Suwayyah || [Name] == FeralClaws || [Name] == RunicTalons || [Name] == GreaterTalons || [Name] == GreaterClaws) && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [SkillLightningsentry] >= 3 && [Skillmindblast] >= 3 && ([Skillweaponblock] >= 2 || [Skillclawmastery] >= 2 || [Skillfade] >= 2 || [Skillvenom] >= 2 || [Skillbladeshield] >= 2 || [Skilldragonflight] >= 2) && [Sockets] == 0 // Chaos
|
||||
//([Name] == Suwayyah || [Name] == FeralClaws || [Name] == RunicTalons || [Name] == GreaterTalons || [Name] == GreaterClaws) && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [SkillLightningsentry] >= 3 && [Skillmindblast] >= 3 && ([Skillweaponblock] >= 2 || [Skillclawmastery] >= 2 || [Skillfade] >= 2 || [Skillvenom] >= 2 || [Skillbladeshield] >= 2 || [Skilldragonflight] >= 2) && [Sockets] == 3 // Chaos Socketed
|
||||
//([Name] == Suwayyah || [Name] == FeralClaws || [Name] == RunicTalons || [Name] == GreaterTalons || [Name] == GreaterClaws) && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [SkillLightningsentry] >= 3 && [Skillvenom] >= 3 && ([Skillweaponblock] >= 2 || [Skillclawmastery] >= 2 || [Skillfade] >= 2 || [Skillbladeshield] >= 2 || [Skilldragonflight] >= 2 || [Skillmindblast] >= 1) && [Sockets] == 0 // Chaos
|
||||
//([Name] == Suwayyah || [Name] == FeralClaws || [Name] == RunicTalons || [Name] == GreaterTalons || [Name] == GreaterClaws) && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [SkillLightningsentry] >= 3 && [Skillvenom] >= 3 && ([Skillweaponblock] >= 2 || [Skillclawmastery] >= 2 || [Skillfade] >= 2 || [Skillbladeshield] >= 2 || [Skilldragonflight] >= 2 || [Skillmindblast] >= 1) && [Sockets] == 3 // Chaos Socketed
|
||||
([Name] == ScissorsQuhab || [Name] == GreaterTalons || [Name] == GreaterClaws) && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [Skilldragonflight] >= 1 || [Skillphoenixstrike] >= 1 || [Skillfade] >= 1 || [Skillbladesofice] >= 1 || [Skillclawsofthunder] >= 1 && [Sockets] == 0 // Mosaic
|
||||
([Name] == ScissorsQuhab || [Name] == GreaterTalons || [Name] == GreaterClaws) && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [Skilldragonflight] >= 1 || [Skillphoenixstrike] >= 1 || [Skillfade] >= 1 || [Skillbladesofice] >= 1 || [Skillclawsofthunder] >= 1 && [Sockets] == 3 // Mosaic Socketed
|
||||
|
||||
|
||||
//-----------------------------------------------
|
||||
//================= Staves Bases ================
|
||||
//-----------------------------------------------
|
||||
|
||||
// <<<<<<<<< [ Ethereal/Non-Ethereal Superior/Non-Superior Socketed/Non-Socketed Staves ] >>>>>>>>>
|
||||
([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillenergyshield] == 3 && [Skillchillingarmor] >= 1 && [Sockets] == 0 // Memory
|
||||
([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillenergyshield] == 3 && [Skillchillingarmor] >= 1 && [Sockets] >= 4 // Memory Socketed
|
||||
([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillthunderstorm] == 3 && ([Skillenergyshield] == 3 || [Skillchillingarmor] >= 1) && [Sockets] == 0 // Memory
|
||||
([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillthunderstorm] == 3 && ([Skillenergyshield] == 3 || [Skillchillingarmor] >= 1) && [Sockets] >= 4 // Memory Socketed
|
||||
([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillenergyshield] == 3 && [Skillenchant] >= 1 && [Sockets] == 0 // Memory
|
||||
([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillenergyshield] == 3 && [Skillenchant] >= 1 && [Sockets] >= 4 // Memory Socketed
|
||||
([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillthunderstorm] == 3 && ([Skillenergyshield] == 3 || [Skillenchant] >= 1) && [Sockets] == 0 // Memory
|
||||
([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillthunderstorm] == 3 && ([Skillenergyshield] == 3 || [Skillenchant] >= 1) && [Sockets] >= 4 // Memory Socketed
|
||||
([Name] == Warstaff || [Name] == Runestaff || [Name] == Archonstaff) && ([Quality] == Normal || [Quality] == Superior) # [Skilllightningmastery] == 3 && ([Skilllightning] == 3 || [Skillchainlightning] == 3 || [Skillthunderstorm] == 3) && [Skillenergyshield] >= 1 && [Sockets] == 0 // Cta/Obsession
|
||||
([Name] == Warstaff || [Name] == Runestaff || [Name] == Archonstaff) && ([Quality] == Normal || [Quality] == Superior) # [Skilllightningmastery] == 3 && ([Skilllightning] == 3 || [Skillchainlightning] == 3 || [Skillthunderstorm] == 3) && [Skillenergyshield] >= 1 && [Sockets] >= 5 // Cta/Obsession Socketed
|
||||
//([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillenergyshield] >= 3 && ([Skillchillingarmor] >= 2 || [skillthunderstorm] >= 2) && [Sockets] == 0 // Energy Shield Memory
|
||||
//([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillenergyshield] >= 3 && ([Skillchillingarmor] >= 2 || [skillthunderstorm] >= 2) && [Sockets] >= 4 // Energy Shield Memory Socketed
|
||||
//([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillthunderstorm] >= 3 && ([Skillenergyshield] >= 2 || [Skillchillingarmor] >= 2) && [Sockets] == 0 // Thunderstorm Memory
|
||||
//([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillthunderstorm] >= 3 && ([Skillenergyshield] >= 2 || [Skillchillingarmor] >= 2) && [Sockets] >= 4 // Thunderstorm Memory Socketed
|
||||
//([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillchillingarmor] >= 3 && ([Skillthunderstorm] >= 2 || [Skillenergyshield] >= 2) && [Sockets] == 0 // Chilling armor Memory
|
||||
//([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillchillingarmor] >= 3 && ([Skillthunderstorm] >= 2 || [Skillenergyshield] >= 2) && [Sockets] >= 4 // Chilling armor Memory Socketed
|
||||
//([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillenchant] >= 3 && [Skillwarmth] == 2 && [Sockets] == 0 // Enchant Memory
|
||||
//([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillenchant] >= 3 && [Skillwarmth] == 2 && [Sockets] >= 4 // Enchant Memory Socketed
|
||||
//([Name] == Warstaff || [Name] == Runestaff || [Name] == Archonstaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillchillingarmor] >= 2 && [Skillthunderstorm] >= 2 && ([Skillenergyshield] >= 2 || [Skillenchant] >= 2) && [Sockets] == 0 // Cta
|
||||
//([Name] == Warstaff || [Name] == Runestaff || [Name] == Archonstaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillchillingarmor] >= 2 && [Skillthunderstorm] >= 2 && ([Skillenergyshield] >= 2 || [Skillenchant] >= 2) && [Sockets] == 5 // Cta Socketed
|
||||
//([Name] == Warstaff || [Name] == Runestaff || [Name] == Archonstaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillchainlightning] >= 3 && ([Skilllightningmastery] >= 2 || [Skilllightning] >= 2 || [Skillthunderstorm] >= 2) && [Skillenergyshield] >= 0 && [Sockets] == 0 // Lightning Obsession
|
||||
//([Name] == Warstaff || [Name] == Runestaff || [Name] == Archonstaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillchainlightning] >= 3 && ([Skilllightningmastery] >= 2 || [Skilllightning] >= 2 || [Skillthunderstorm] >= 2) && [Skillenergyshield] >= 0 && [Sockets] == 6 // Lightning Obsession socketed
|
||||
//([Name] == Warstaff || [Name] == Runestaff || [Name] == Archonstaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillblizzard] >= 3 && ([Skilliceblast] >= 2 || [Skillfrozenorb] >= 2 || [Skillwarmth] >= 2) && [Skillchillingarmor] >= 0 && [Sockets] >= 0 // Blizzard Obsession
|
||||
//([Name] == Warstaff || [Name] == Runestaff || [Name] == Archonstaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillblizzard] >= 3 && ([Skilliceblast] >= 2 || [Skillfrozenorb] >= 2 || [Skillwarmth] >= 2) && [Skillchillingarmor] >= 0 && [Sockets] >= 6 // Obsession Socketed
|
||||
//([Name] == Warstaff || [Name] == Runestaff || [Name] == Archonstaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillfireball] >= 3 && ([Skillmeteor] >= 2 || [Skillfirebolt] >= 2 || [Skillwarmth] >= 2) && [Skillchillingarmor] >= 0 && [Sockets] >= 0 // Fireball Obsession
|
||||
//([Name] == Warstaff || [Name] == Runestaff || [Name] == Archonstaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillfireball] >= 3 && ([Skillmeteor] >= 2 || [Skillfirebolt] >= 2 || [Skillwarmth] >= 2) && [Skillchillingarmor] >= 0 && [Sockets] >= 6 // Fireball Obsession socketed
|
||||
|
||||
//---------------------------------------------------------
|
||||
//================= Necromancer Wand Bases ================
|
||||
//---------------------------------------------------------
|
||||
|
||||
// <<<<<<<<< [ Ethereal/Non-Ethereal Superior/Non-Superior Socketed/Non-Socketed Necromancer Wands ] >>>>>>>>>
|
||||
[Type] == Wand && ([Quality] == Normal || [Quality] == Superior) # [Skillbonespear] >= 3 && ([Skillbonespirit] >= 3 || [Skillteeth] >= 3 || [Skillpoisonnova] >= 3 || [Skilldecrepify] >= 3) && ([Skilldecrepify] >= 1 || [Skillironmaiden] >= 1 || [Skillteeth] >= 1 || [Skillbonearmor] >= 1 || [Skilllowerresist] >= 1) && [Sockets] == 0 // White Wand
|
||||
[Type] == Wand && ([Quality] == Normal || [Quality] == Superior) # [Skillbonespear] >= 3 && ([Skillbonespirit] >= 3 || [Skillteeth] >= 3 || [Skillpoisonnova] >= 3 || [Skilldecrepify] >= 3) && ([Skilldecrepify] >= 1 || [Skillironmaiden] >= 1 || [Skillteeth] >= 1 || [Skillbonearmor] >= 1 || [Skilllowerresist] >= 1) && [Sockets] == 2 // White Wand Socketed
|
||||
//[Type] == Wand && ([Quality] == Normal || [Quality] == Superior) # [Skillbonespear] >= 3 && ([Skillbonespirit] >= 3 || [Skillteeth] >= 3 || [Skillpoisonnova] >= 3 || [Skilldecrepify] >= 3) && ([Skilldecrepify] >= 1 || [Skillironmaiden] >= 1 || [Skillteeth] >= 1 || [Skillbonearmor] >= 1 || [Skilllowerresist] >= 1) && [Sockets] == 0 // White Wand
|
||||
//[Type] == Wand && ([Quality] == Normal || [Quality] == Superior) # [Skillbonespear] >= 3 && ([Skillbonespirit] >= 3 || [Skillteeth] >= 3 || [Skillpoisonnova] >= 3 || [Skilldecrepify] >= 3) && ([Skilldecrepify] >= 1 || [Skillironmaiden] >= 1 || [Skillteeth] >= 1 || [Skillbonearmor] >= 1 || [Skilllowerresist] >= 1) && [Sockets] == 2 // White Wand Socketed
|
||||
|
||||
//---------------------------------------------------
|
||||
//================= Amazon Bow Bases ================
|
||||
@@ -920,15 +969,15 @@
|
||||
|
||||
// <<<<<<<<< [ Socketed Amazon Bows ] >>>>>>>>>
|
||||
//[Type] == Bow && [Class] == Elite && [Quality] == Normal # [Sockets] == 0 // Faith
|
||||
[Type] == Bow && [Class] == Elite && [Quality] == Normal # [Sockets] >= 4 // Faith/Mist Socketed
|
||||
//[Type] == Bow && [Class] == Elite && [Quality] == Normal # [Sockets] >= 4 // Faith/Mist Socketed
|
||||
//[Type] == Amazonbow && [Class] == Elite && [Quality] == Normal # [Bowandcrossbowskilltab] == 3 && [Sockets] == 0 // Faith
|
||||
[Type] == Amazonbow && [Class] == Elite && [Quality] == Normal # [Bowandcrossbowskilltab] == 3 && [Sockets] >= 4 // Faith/Mist Socketed
|
||||
//[Type] == Amazonbow && [Class] == Elite && [Quality] == Normal # [Bowandcrossbowskilltab] == 3 && [Sockets] >= 4 // Faith/Mist Socketed
|
||||
|
||||
// <<<<<<<<< [ Superior Socketed Amazon Bows ] >>>>>>>>>
|
||||
//[Type] == Bow && [Class] == Elite && [Quality] == Normal # [Enhanceddamage] > 10 && [Sockets] == 0 // Faith
|
||||
[Type] == Bow && [Class] == Elite && [Quality] == Superior # [Enhanceddamage] > 10 && [Sockets] == 4 // Faith
|
||||
//[Type] == Bow && [Class] == Elite && [Quality] == Superior # [Enhanceddamage] > 10 && [Sockets] == 4 // Faith
|
||||
//[Type] == Amazonbow && [Class] == Elite && [Quality] == Superior # [Bowandcrossbowskilltab] == 3 && [Enhanceddamage] > 10 && [Sockets] == 0 // Faith
|
||||
[Type] == Amazonbow && [Class] == Elite && [Quality] == Superior # [Bowandcrossbowskilltab] == 3 && [Enhanceddamage] > 10 && [Sockets] == 4 // Faith
|
||||
//[Type] == Amazonbow && [Class] == Elite && [Quality] == Superior # [Bowandcrossbowskilltab] == 3 && [Enhanceddamage] > 10 && [Sockets] == 4 // Faith
|
||||
|
||||
//=======================================================================//
|
||||
//<<<<<<<<<<<<<<<<<<<<<<<<<<<< Set Items >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>//
|
||||
@@ -1007,8 +1056,8 @@
|
||||
//--------------------------------------------------
|
||||
|
||||
//[Name] == Buckler && [Quality] == Set // Hsarus' Iron Fist
|
||||
//[Name] == Belt && [Quality] == Set # [Coldresist] == 20 && [Maxhp] == 20 // Hsarus' Iron Stay
|
||||
//[Name] == Chainboots && [Quality] == Set // Hsarus' Iron Heel
|
||||
[Name] == Belt && [Quality] == Set # [Coldresist] == 20 && [Maxhp] == 20 // Hsarus' Iron Stay twink
|
||||
[Name] == Chainboots && [Quality] == Set // Hsarus' Iron Heel twink
|
||||
|
||||
//-------------------------------------------------
|
||||
//================= Infernal Tools ================
|
||||
@@ -1080,7 +1129,7 @@
|
||||
//-----------------------------------------------------
|
||||
|
||||
//[Name] == Huntersguise && [Quality] == Set // Aldur's Stony Gaze
|
||||
//[Name] == Battleboots && [Quality] == Set // Aldur's Advance
|
||||
[Name] == Battleboots && [Quality] == Set // Aldur's Advance
|
||||
//[Name] == Shadowplate && [Quality] == Set // Aldur's Deception
|
||||
//[Name] == Jaggedstar && [Quality] == Set // Aldur's Rhythm
|
||||
|
||||
@@ -1104,16 +1153,16 @@
|
||||
//-----------------------------------------------
|
||||
|
||||
//[Type] == Amulet && [Quality] == Set # [Itemallskills] == 1 && [Coldresist] == 18 // Telling Of Beads
|
||||
//[Name] == Bramblemitts && [Quality] == Set // Laying Of Hands
|
||||
[Name] == Bramblemitts && [Quality] == Set // Laying Of Hands
|
||||
//[Name] == Duskshroud && [Quality] == Set // Dark Adherent
|
||||
//[Name] == Demonhideboots && [Quality] == Set // Rite Of Passage
|
||||
//[Name] == Mithrilcoil && [Quality] == Set //Credendum
|
||||
[Name] == Mithrilcoil && [Quality] == Set //Credendum
|
||||
|
||||
//----------------------------------------------------
|
||||
//================= Griswold's Legacy ================
|
||||
//----------------------------------------------------
|
||||
|
||||
//[Name] == Ornateplate && [Quality] == Set # // Griswold's Heart
|
||||
[Name] == Ornateplate && [Quality] == Set // Griswold's Heart
|
||||
//[Name] == Corona && [Quality] == Set # [Itemmagicbonus] >= 20 // Griswold's Valor
|
||||
//[Name] == Corona && [Quality] == Set # [Itemmagicbonus] >= 30 && [Enhanceddefense] >= 75 // Griswold's Valor Perfect
|
||||
//[Name] == Caduceus && [Quality] == Set # [Sockets] >= 3 && [Enhanceddamage] >= 200 // Griswold's Redemption
|
||||
@@ -1144,7 +1193,7 @@
|
||||
//================= Immortal King ================
|
||||
//------------------------------------------------
|
||||
|
||||
//[Name] == Warbelt && [Quality] == Set // Immortal King's Detail
|
||||
[Name] == Warbelt && [Quality] == Set // Immortal King's Detail
|
||||
//[Name] == Avengerguard && [Quality] == Set // Immortal King's Will
|
||||
//[Name] == Ogremaul && [Quality] == Set // Immortal King's Stone Crusher
|
||||
[Name] == Sacredarmor && [Quality] == Set // Immortal King's Soul Cage
|
||||
@@ -1169,21 +1218,21 @@
|
||||
//[Name] == Grimhelm && [Quality] == Set // Natalya's Totem
|
||||
//[Name] == Scissorssuwayyah && [Quality] == Set // Natalya's Mark
|
||||
//[Name] == Loricatedmail && [Quality] == Set # [Defense] >= 721 && [Sockets] >= 3 // Natalya's Shadow
|
||||
//[Name] == Meshboots && [Quality] == Set # [Lightresist] >= 25 && [Coldresist] >= 25 // Natalya's Soul
|
||||
[Name] == Meshboots && [Quality] == Set # [Lightresist] >= 15 && [Coldresist] >= 15 // Natalya's Soul
|
||||
|
||||
//--------------------------------------------------------
|
||||
//================= Naj's Ancient Vestege ================
|
||||
//--------------------------------------------------------
|
||||
|
||||
//[Name] == Circlet && [Quality] == Set // Naj's Circlet
|
||||
//[Name] == Hellforgeplate && [Quality] == Set // Naj's Light Plate
|
||||
//[Name] == Elderstaff && [Quality] == Set // Naj's Puzzler
|
||||
[Name] == Circlet && [Quality] == Set // Naj's Circlet
|
||||
[Name] == Hellforgeplate && [Quality] == Set // Naj's Light Plate
|
||||
[Name] == Elderstaff && [Quality] == Set // Naj's Puzzler
|
||||
|
||||
//------------------------------------------------
|
||||
//================= Orphan's Call ================
|
||||
//------------------------------------------------
|
||||
|
||||
//[Name] == Wingedhelm && [Quality] == Set // Guillaume's Face
|
||||
[Name] == Wingedhelm && [Quality] == Set // Guillaume's Face
|
||||
//[Name] == Roundshield && [Quality] == Set // Whistan's Guard
|
||||
//[Name] == Sharkskingloves && [Quality] == Set // Magnus' Skin
|
||||
//[Name] == Battlebelt && [Quality] == Set // Wilhelm's Pride
|
||||
@@ -1195,13 +1244,13 @@
|
||||
//[Name] == Cap && [Quality] == Set # [Itemmagicbonus] == 35 // Sander's Paragon
|
||||
//[Name] == Bonewand && [Quality] == Set //Sander's Superstition
|
||||
//[Name] == Heavygloves && [Quality] == Set # [Maxhp] == 40 // Sander's Taboo
|
||||
//[Name] == Heavyboots && [Quality] == Set # [Dexterity] == 10 && [Strength] == 5 // Sander's Riprap
|
||||
[Name] == Heavyboots && [Quality] == Set # [Dexterity] == 10 && [Strength] == 5 // Sander's Riprap twink
|
||||
|
||||
//---------------------------------------------------------
|
||||
//================= Sazabi's Grand Tribute ================
|
||||
//---------------------------------------------------------
|
||||
|
||||
//[Name] == Basinet && [Quality] == Set // Sazabi's Mental Sheath
|
||||
[Name] == Basinet && [Quality] == Set // Sazabi's Mental Sheath mid
|
||||
//[Name] == Crypticsword && [Quality] == Set // Sazabi's Cobalt Redeemer
|
||||
//[Name] == Balrogskin && [Quality] == Set // Sazabi's Ghost Liberator
|
||||
|
||||
@@ -1211,7 +1260,7 @@
|
||||
|
||||
[Name] == Swirlingcrystal && [Quality] == Set // Tal Rasha's Lidless Eye
|
||||
//[Name] == Swirlingcrystal && [Quality] == Set # [Plusskillcoldmastery] == 2 && [Plusskillfiremastery] == 2 && [Plusskilllightningmastery] == 2 // Tal Rasha's Lidless Eye Perfect
|
||||
//[Name] == Deathmask && [Quality] == Set // Tal Rasha's Horadric Crest
|
||||
[Name] == Deathmask && [Quality] == Set // Tal Rasha's Horadric Crest
|
||||
[Name] == Lacqueredplate && [Quality] == Set // Tal Rasha's Guardianship
|
||||
//[Name] == Lacqueredplate && [Quality] == Set # [Defense] >= 941 // Tal Rasha's Guardianship Perfect
|
||||
[Type] == Amulet && [Quality] == Set # [Sorceressskills] == 2 // Tal Rasha's Adjucation
|
||||
@@ -1228,6 +1277,11 @@
|
||||
//[Name] == Trollbelt && [Quality] == Set // Trang-Oul's Girth
|
||||
[Name] == Heavybracers && [Quality] == Set // Trang-Oul's Claws
|
||||
|
||||
//----------------------------------------------------
|
||||
//================= Horazon's Splendor ================
|
||||
//----------------------------------------------------
|
||||
[Name] == Mirroredboots && [Quality] == Set // Horazon's legacy
|
||||
|
||||
//==========================================================================//
|
||||
//<<<<<<<<<<<<<<<<<<<<<<<<<<<< Unique Items >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>//
|
||||
//==========================================================================//
|
||||
@@ -1241,6 +1295,8 @@
|
||||
//[Type] == Ring && [Quality] == Unique # [Hpregen] >= 5 && [Manaleech] >= 4 // Manald Heal
|
||||
//[Type] == Ring && [Quality] == Unique # [Hpregen] == 8 && [Manaleech] == 7 // Manald Heal Perfect
|
||||
[Type] == Ring && [Quality] == Unique # [Itemmaxmanapercent] >= 25 // Stone Of Jordan
|
||||
[Type] == Ring && [Quality] == Unique # [Fcr] >= 10 && [Itemmagicbonus] >= 10 // Sling Ring
|
||||
[Type] == Ring && [Quality] == Unique # [Fcr] >= 10 && [Allres] >= 6 // Opalvein Ring
|
||||
[Type] == Ring && [Quality] == Unique # [Maxhp] >= 35 && [Magicdamagereduction] >= 12 // Dwarf Star
|
||||
//[Type] == Ring && [Quality] == Unique # [Maxhp] >= 35 && [Magicdamagereduction] == 15 // Dwarf Star Perfect
|
||||
[Type] == Ring && [Quality] == Unique # [Dexterity] >= 15 && [Tohit] >= 150 // Raven Frost
|
||||
@@ -1248,7 +1304,7 @@
|
||||
[Type] == Ring && [Quality] == Unique # [Lifeleech] >= 3 && [Itemallskills] == 1 // Bul-Kathos' Wedding Band
|
||||
//[Type] == Ring && [Quality] == Unique # [Lifeleech] == 5 && [Itemallskills] == 1 // Bul-Kathos' Wedding Band Perfect
|
||||
[Type] == Ring && [Quality] == Unique # [Poisonresist] >= 20 && [Normaldamagereduction] >= 7 // Nature's Peace
|
||||
//[Type] == Ring && [Quality] == Unique # [Poisonresist] == 30 && [Itemdamagetomana] == 11 // Nature's Peace Perfect
|
||||
//[Type] == Ring && [Quality] == Unique # [Poisonresist] == 30 && [Normaldamagereduction] == 11 // Nature's Peace Perfect
|
||||
[Type] == Ring && [Quality] == Unique # [Itemabsorblightpercent] >= 10 && [Itemmagicbonus] >= 10 // Wisp Protector
|
||||
//[Type] == Ring && [Quality] == Unique # [Itemabsorblightpercent] == 20 && [Itemmagicbonus] == 20 // Wisp Protector Perfect
|
||||
[Type] == Ring && [Quality] == Unique # [Lifeleech] >= 6 && [Armorclassvsmissile] >= 100 // Carrion Wind
|
||||
@@ -1259,14 +1315,15 @@
|
||||
//-------------------------------------------------
|
||||
|
||||
//[Type] == Amulet && [Quality] == Unique # [Fireresist] >= 50 // Nokozan Relic
|
||||
//[Type] == Amulet && [Quality] == Unique # [Lifeleech] >= 3 && [Armorclassvsmissile] >= 10 // The Eye Of Etlitch
|
||||
[Type] == Amulet && [Quality] == Unique # [Lifeleech] >= 3 && [Armorclassvsmissile] >= 10 // The Eye Of Etlitch
|
||||
//[Type] == Amulet && [Quality] == Unique # [Lifeleech] >= 7 && [Armorclassvsmissile] >= 40 // The Eye Of Etlitch Perfect
|
||||
//[Type] == Amulet && [Quality] == Unique # [Allres] >= 10 && [Enhanceddefense] >= 10 // The Mahim-Oak Curio
|
||||
//[Type] == Amulet && [Quality] == Unique # [Allres] >= 15 && [Strength] >= 12 // Saracen's Chance
|
||||
//[Type] == Amulet && [Quality] == Unique # [Allres] == 25 && [Strength] >= 12 // Saracen's Chance Perfect
|
||||
//[Type] == Amulet && [Quality] == Unique # [Dexterity] >= 25 && [Ias] >= 20 // The Cat's Eye
|
||||
//[Type] == Amulet && [Quality] == Unique # [Lifeleech] >= 3 && [Manaleech] >= 11 // Crescent Moon
|
||||
[Type] == Amulet && [Quality] == Unique # [Lifeleech] >= 3 && [Manaleech] >= 11 // Crescent Moon
|
||||
//[Type] == Amulet && [Quality] == Unique # [Lifeleech] == 6 && [Manaleech] == 15 // Crescent Moon Perfect
|
||||
[Type] == Amulet && [Quality] == Unique # [Fcr] >= 5 // Entropy Locket
|
||||
[Type] == Amulet && [Quality] == Unique # [Lightresist] >= 35 // Highlord's Wrath
|
||||
//[Type] == Amulet && [Quality] == Unique # [Poisonresist] >= 75 // Atma's Scarab
|
||||
[Type] == Amulet && [Quality] == Unique # [Hpregen] >= 10 && [Fireskilltab] >= 2 // The Rising Sun
|
||||
@@ -1281,10 +1338,10 @@
|
||||
//================= Unique Jewels ================
|
||||
//------------------------------------------------
|
||||
|
||||
[Name] == Jewel && [Quality] == Unique # [Passivecoldmastery] + [Passivecoldpierce] >= 9 // Rainbow Facet Cold
|
||||
[Name] == Jewel && [Quality] == Unique # [Passivefiremastery] + [Passivefirepierce] >= 9 // Rainbow Facet Fire
|
||||
[Name] == Jewel && [Quality] == Unique # [Passiveltngmastery] + [Passiveltngpierce] >= 9 // Rainbow Facet Light
|
||||
[Name] == Jewel && [Quality] == Unique # [Passivepoismastery] + [Passivepoispierce] >= 9 // Rainbow Facet Poison
|
||||
[Name] == Jewel && [Quality] == Unique # [Passivecoldmastery] + [Passivecoldpierce] >= 8 // Rainbow Facet Cold
|
||||
[Name] == Jewel && [Quality] == Unique # [Passivefiremastery] + [Passivefirepierce] >= 8 // Rainbow Facet Fire
|
||||
[Name] == Jewel && [Quality] == Unique # [Passiveltngmastery] + [Passiveltngpierce] >= 8 // Rainbow Facet Light
|
||||
[Name] == Jewel && [Quality] == Unique # [Passivepoismastery] + [Passivepoispierce] >= 8 // Rainbow Facet Poison
|
||||
|
||||
//------------------------------------------------
|
||||
//================= Unique Gloves ================
|
||||
@@ -1292,11 +1349,11 @@
|
||||
|
||||
//[Name] == Heavygloves && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 10 // Bloodfist
|
||||
//[Name] == Heavygloves && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 20 // Bloodfist Perfect
|
||||
[Name] == Chaingloves && [Quality] == Unique && [Flag] != Ethereal # [Itemmagicbonus] >= 25 // Chance Guards
|
||||
[Name] == Chaingloves && [Quality] == Unique && [Flag] != Ethereal # [Itemmagicbonus] >= 35 // Chance Guards
|
||||
//[Name] == Chaingloves && [Quality] == Unique && [Flag] != Ethereal # [Itemmagicbonus] >= 40 && [Enhanceddefense] >= 30 // Chance Guards Perfect
|
||||
[Name] == Lightgauntlets && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 20 // Magefist
|
||||
//[Name] == Lightgauntlets && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 30 // Magefist Perfect
|
||||
//[Name] == Gauntlets && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 10 // Frostburn
|
||||
[Name] == Gauntlets && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 10 // Frostburn
|
||||
//[Name] == Gauntlets && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 20 // Frostburn Perfect
|
||||
[Name] == Vampirebonegloves && [Quality] == Unique && [Flag] != Ethereal # [Strength] == 10 // Dracul's Grasp
|
||||
//[Name] == Vampirebonegloves && [Quality] == Unique && [Flag] != Ethereal # [Strength] == 15 && [Lifeleech] >= 10 && [Enhanceddefense] >= 120 // Dracul's Grasp Perfect
|
||||
@@ -1307,11 +1364,11 @@
|
||||
//================= Unique Belts ================
|
||||
//-----------------------------------------------
|
||||
|
||||
//[Name] == Heavybelt && [Quality] == Unique && [Flag] != Ethereal # [Itemgoldbonus] >= 50 // Goldwrap
|
||||
[Name] == Heavybelt && [Quality] == Unique && [Flag] != Ethereal # [Itemgoldbonus] >= 50 // Goldwrap
|
||||
//[Name] == Heavybelt && [Quality] == Unique && [Flag] != Ethereal # [Itemgoldbonus] >= 80 && [Enhanceddefense] >= 60 // Goldwrap Perfect
|
||||
//[Name] == Demonhidesash && [Quality] == Unique && [Flag] != Ethereal # [Lifeleech] >= 6 && [Damageresist] >= 12 && [Magicdamagereduction] >= 12 && [Enhanceddefense] >= 150 // String Of Ears
|
||||
[Name] == Demonhidesash && [Quality] == Unique && [Flag] != Ethereal # [Lifeleech] >= 6 && [Damageresist] >= 14 && [Magicdamagereduction] >= 10 && [Enhanceddefense] >= 150 // String Of Ears
|
||||
//[Name] == Demonhidesash && [Quality] == Unique && [Flag] != Ethereal # [Lifeleech] >= 8 && [Damageresist] >= 15 && [Magicdamagereduction] >= 15 && [Enhanceddefense] >= 180 // String Of Ears Perfect
|
||||
//[Name] == Sharkskinbelt && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 120 && [Plusdefense] >= 15 // Razortail
|
||||
[Name] == Sharkskinbelt && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 120 && [Plusdefense] >= 15 // Razortail
|
||||
//[Name] == Sharkskinbelt && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 150 && [Plusdefense] >= 15 // Razortail Perfect
|
||||
//[Name] == Battlebelt && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 130 // Snowclash
|
||||
//[Name] == Battlebelt && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 170 // Snowclash Perfect
|
||||
@@ -1321,31 +1378,32 @@
|
||||
//[Name] == Spiderwebsash && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 120 // Arachnid Mesh Perfect
|
||||
//[Name] == Vampirefangbelt && [Quality] == Unique && [Flag] != Ethereal # [Lifeleech] >= 5 && [Defense] >= 50 // Nosferatu's Coil
|
||||
//[Name] == Vampirefangbelt && [Quality] == Unique && [Flag] != Ethereal # [Lifeleech] >= 7 && [Defense] >= 60 // Nosferatu's Coil Perfect
|
||||
[Name] == Mithrilcoil && [Quality] == Unique && [Flag] != Ethereal # [Damageresist] >= 10 && [Enhanceddefense] >= 90 && [Hpregen] >= 10 // Verdungo's Hearty Cord
|
||||
[Name] == Mithrilcoil && [Quality] == Unique && [Flag] != Ethereal # [Damageresist] >= 13 && [Enhanceddefense] >= 90 && [Hpregen] >= 10 // Verdungo's Hearty Cord
|
||||
//[Name] == Mithrilcoil && [Quality] == Unique && [Flag] != Ethereal # [Damageresist] >= 15 && [Enhanceddefense] >= 140 && [Hpregen] >= 13 // Verdungo's Hearty Cord Perfect
|
||||
|
||||
[Name] == Trollbelt && [Quality] == Unique && [Flag] != Ethereal //Gheed's Wager
|
||||
//-----------------------------------------------
|
||||
//================= Unique Boots ================
|
||||
//-----------------------------------------------
|
||||
|
||||
//[Name] == Boots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 10 // Hotspurs
|
||||
//[Name] == Boots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 20 // Hotspurs Perfect
|
||||
//[Name] == Lightplatedboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 50 // Goblin Toes
|
||||
[Name] == Chainboots && [Quality] == Unique // Treads of Cthon
|
||||
[Name] == Lightplatedboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 50 // Goblin Toes
|
||||
//[Name] == Lightplatedboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 60 // Goblin Toes Perfect
|
||||
//[Name] == Demonhideboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 120 && [Itemgoldbonus] >= 40 // Infernostride
|
||||
[Name] == Demonhideboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 150 && [Itemgoldbonus] == 70 // Infernostride Perfect
|
||||
//[Name] == Sharkskinboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 180 // Waterwalk
|
||||
//[Name] == Sharkskinboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 210 // Waterwalk Perfect
|
||||
//[Name] == Meshboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 150 // Silkweave
|
||||
[Name] == Meshboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 150 // Silkweave
|
||||
//[Name] == Meshboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 190 // Silkweave Perfect
|
||||
[Name] == Battleboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 150 && [Itemgoldbonus] >= 30 // War Traveler
|
||||
//[Name] == Battleboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 190 && [Itemgoldbonus] == 50 // War Traveler Perfect
|
||||
//[Name] == Warboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 160 // Gore Rider
|
||||
[Name] == Warboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 200 // Gore Rider Perfect
|
||||
[Name] == Battleboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 150 && [Itemmagicbonus] >= 30 // War Traveler
|
||||
//[Name] == Battleboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 190 && [Itemmagicbonus] == 50 // War Traveler Perfect
|
||||
[Name] == Warboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 160 // Gore Rider
|
||||
//[Name] == Warboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 200 // Gore Rider Perfect
|
||||
[Name] == Scarabshellboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 140 && [Strength] >= 10 && [Vitality] >= 10 && [Poisonresist] >= 40 // Sandstorm Trek
|
||||
[Name] == Scarabshellboots && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddefense] >= 140 && [Strength] >= 10 && [Vitality] >= 10 && [Poisonresist] >= 40 // Eth Sandstorm Trek
|
||||
//[Name] == Scarabshellboots && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddefense] == 170 && [Strength] == 15 && [Vitality] == 15 && [Poisonresist] == 70 // Eth Sandstorm Trek Perfect
|
||||
//[Name] == Boneweaveboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 170 && [Strength] >= 10 && [Skillskeletonmastery] >= 1 // Marrowwalk
|
||||
[Name] == Boneweaveboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 170 && [Strength] >= 10 && [Skillskeletonmastery] >= 1 // Marrowwalk
|
||||
//[Name] == Boneweaveboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 200 && [Strength] == 20 && [Skillskeletonmastery] == 2 // Marrowwalk Perfect
|
||||
[Name] == Myrmidongreaves && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 70 && [Shadowdisciplinesskilltab] >= 2 && [Dexterity] >= 20 // Shadow Dancer
|
||||
//[Name] == Myrmidongreaves && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 100 && [Shadowdisciplinesskilltab] == 2 && [Dexterity] == 25 // Shadow Dancer Perfect
|
||||
@@ -1362,8 +1420,8 @@
|
||||
//[Name] == Sallet && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddamage] == 220 && [Coldresist]+[Lightresist]+[Fireresist] == 130 // Eth Rockstopper Perfect
|
||||
//[Name] == Wingedhelm && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 150 && [Amazonskills] >= 1 // Valkyrie Wing
|
||||
//[Name] == Wingedhelm && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 200 && [Amazonskills] == 2 // Valkyrie Wing Perfect
|
||||
[Name] == Grandcrown && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 160 && [Lifeleech] >= 9 && [Itemgoldbonus] >= 80 // Crown Of Thieves
|
||||
[Name] == Grandcrown && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddefense] >= 160 && [Lifeleech] >= 9 && [Itemgoldbonus] >= 80 // Eth Crown Of Thieves
|
||||
//[Name] == Grandcrown && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 160 && [Lifeleech] >= 9 && [Itemgoldbonus] >= 80 // Crown Of Thieves
|
||||
//[Name] == Grandcrown && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddefense] >= 160 && [Lifeleech] >= 9 && [Itemgoldbonus] >= 80 // Eth Crown Of Thieves
|
||||
//[Name] == Grandcrown && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 200 && [Lifeleech] == 12 && [Itemgoldbonus] == 100 // Eth Crown Of Thieves Perfect
|
||||
[Name] == Grimhelm && [Quality] == Unique && [Flag] != Ethereal # [Lifeleech] >= 6 && [Manaleech] >= 6 && [Magicdamagereduction] >= 10 && [Damageresist] >= 15 // Vampire Gaze
|
||||
[Name] == Grimhelm && [Quality] == Unique && [Flag] == Ethereal # [Lifeleech] >= 6 && [Manaleech] >= 6 && [Magicdamagereduction] >= 10 && [Damageresist] >= 15 // Eth Vampire Gaze
|
||||
@@ -1398,9 +1456,9 @@
|
||||
//[Name] == Ghostarmor && [Quality] == Unique && [Flag] != Ethereal # [Magicdamagereduction] == 11 // The Spirit Shroud Perfect
|
||||
[Name] == Serpentskinarmor && [Quality] == Unique && [Flag] != Ethereal # [Magicdamagereduction] >= 9 && [Allres] >= 20 // Skin Of The Vipermagi
|
||||
//[Name] == Serpentskinarmor && [Quality] == Unique && [Flag] != Ethereal # [Magicdamagereduction] == 13 && [Allres] == 35 // Skin Of The Vipermagi Perfect
|
||||
//[Name] == Cuirass && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 160 // Duriel's Shell
|
||||
[Name] == Cuirass && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 160 // Duriel's Shell
|
||||
[Name] == Cuirass && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddefense] >= 160 // Eth Duriel's Shell
|
||||
[Name] == Cuirass && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 200 // Duriel's Shell Perfect
|
||||
//[Name] == Cuirass && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 200 // Duriel's Shell Perfect
|
||||
//[Name] == Cuirass && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddefense] == 200 // Eth Duriel's Shell Perfect
|
||||
//[Name] == Mesharmor && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 180 // Shaftstop
|
||||
[Name] == Mesharmor && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddefense] >= 180 // Eth Shaftstop
|
||||
@@ -1418,7 +1476,9 @@
|
||||
//[Name] == Templarcoat && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddefense] == 200 // Eth Guardian Angel Perfect
|
||||
[Name] == Duskshroud && [Quality] == Unique && [Flag] != Ethereal # [Passiveltngmastery] >= 15 && ([Skilllightning] >= 3 || [Skillnova] >= 3 || [Skillenergyshield] >= 3) // Light Ormus' Robes
|
||||
[Name] == Duskshroud && [Quality] == Unique && [Flag] != Ethereal # [Passivecoldmastery] >= 15 && [Skillblizzard] >= 3 || [Skilliceblast] >= 3 // Cold Ormus' Robes
|
||||
[Name] == Duskshroud && [Quality] == Unique && [Flag] != Ethereal # [Passivefiremastery] >= 15 && ([Skillfireball] >= 3 || [Skillfirebolt] >= 3 || [Skillhydra] >= 3 || [Skillmeteor] >= 3) // Fire Ormus' Robes
|
||||
[Name] == Duskshroud && [Quality] == Unique && [Flag] != Ethereal # [Passivefiremastery] >= 15 && ([Skillfireball] >= 3 || [Skillhydra] >= 3 || [Skillmeteor] >= 3) // Fire Ormus' Robes
|
||||
[Name] == Duskshroud && [Quality] == Unique # [Skillenchant] >= 3 //Enchant Ormus' Robes
|
||||
|
||||
//[Name] == Wirefleece && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 150 && [Normaldamagereduction] >= 15 && [Magicdamagereduction] >= 15 // The Gladiator's Bane
|
||||
[Name] == Wirefleece && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddefense] >= 150 && [Normaldamagereduction] >= 15 && [Magicdamagereduction] >= 15 // Eth The Gladiator's Bane
|
||||
[Name] == Wirefleece && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 180 && [Normaldamagereduction] == 20 && [Magicdamagereduction] == 20 // The Gladiator's Bane Perfect
|
||||
@@ -1437,13 +1497,13 @@
|
||||
|
||||
//[Name] == Buckler && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 30 // Pelta Lunata
|
||||
//[Name] == Buckler && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 40 // Pelta Lunata Perfect
|
||||
///[Name] == Towershield && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 80 // Bverrit Keep
|
||||
//[Name] == Towershield && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 80 // Bverrit Keep
|
||||
//[Name] == Towershield && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 120 // Bverrit Keep Perfect
|
||||
//[Name] == Roundshield && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 180 // Moser's Blessed Circle
|
||||
//[Name] == Roundshield && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 220 // Moser's Blessed Circle Perfect
|
||||
[Name] == Pavise && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 180 && [Allres] >= 20 // Gerke's Sanctuary
|
||||
//[Name] == Pavise && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 240 && [Allres] == 30 // Gerke's Sanctuary Perfect
|
||||
//[Name] == Grimshield && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 80 && [Manarecovery] >= 3 // Lidless Wall
|
||||
[Name] == Grimshield && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 80 && [Manarecovery] >= 3 // Lidless Wall
|
||||
//[Name] == Grimshield && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 130 && [Manarecovery] == 5 // Lidless Wall Perfect
|
||||
[Name] == Monarch && [Quality] == Unique && [Flag] != Ethereal # [DamageResist] >= 35 // Stormshield
|
||||
//[Name] == Monarch && [Quality] == Unique && [Flag] != Ethereal # [Defense] == 148 // Stormshield Perfect
|
||||
@@ -1455,7 +1515,7 @@
|
||||
//-------------------------------------------------
|
||||
|
||||
[Name] == Boneknife && [Quality] == Unique && [Flag] != Ethereal # [Allres] == 75 // Wizardspike
|
||||
//[Name] == Dagger && [Quality] == Unique && [Flag] != Ethereal # [Itemgoldbonus] == 100 // Gull
|
||||
[Name] == Dagger && [Quality] == Unique && [Flag] != Ethereal # [Itemmagicbonus] == 100 // Gull
|
||||
//[Name] == Blade && [Quality] == Unique && [Flag] != Ethereal # [Fcr] == 50 && [Allres] == 10 // Spectral Shard
|
||||
//[Name] == Rondel && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddamage] >= 190 // Heart Carver
|
||||
//[Idname] == Fleshripper // Fleshripper
|
||||
@@ -1469,7 +1529,7 @@
|
||||
//[Idname] == Bloodletter // Bloodletter
|
||||
//[Idname] == Coldsteeleye // Coldsteel Eye
|
||||
//[Idname] == Hexfire // Hexfire
|
||||
//[Idname] == Bladeofalibaba && [Dexterity] >= 5 // Blade Of Ali Baba
|
||||
//[Idname] == Bladeofalibaba # [Dexterity] >= 5 // Blade Of Ali Baba
|
||||
[Idname] == Bladeofalibaba # [Dexterity] >= 15 // Blade Of Ali Baba Perfect Dex
|
||||
//[Idname] == Ginthersrift // Ginther's Rift
|
||||
//[Name] == Battlesword && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddamage] >= 150 // Headstriker
|
||||
@@ -1511,7 +1571,7 @@
|
||||
//[Idname] == Steeldriver // Steeldriver
|
||||
//[Idname] == Nordstenderizer // Nord's Tenderizer
|
||||
[Name] == Scourge && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddamage] >= 240 && [Itemabsorblight] >= 3 // Stormlash
|
||||
[Name] == Marteldefer && [Quality] == Unique # [Enhanceddamage] >= 130 // The Gavel Of Pain (Wc Gf Barb Offhand)
|
||||
//[Name] == Marteldefer && [Quality] == Unique # [Enhanceddamage] >= 130 // The Gavel Of Pain (Wc Gf Barb Offhand)
|
||||
//[Idname] == Tyrantclub // Demon Limb
|
||||
//[Idname] == Fleshrender // Fleshrender
|
||||
//[Idname] == Baranarsstar // Baranar's Star
|
||||
@@ -1536,6 +1596,8 @@
|
||||
//================= Unique Scepters ================
|
||||
//--------------------------------------------------
|
||||
|
||||
[Name] == Mightyscepter && [Quality] == Unique && [Flag] != Ethereal //Heaven's Light
|
||||
[Name] == Caduceus && [Quality] == Unique && [Flag] != Ethereal //Astreon's Iron Ward
|
||||
//[Idname] == Heavenslight // Heaven's Light
|
||||
//[Idname] == Knellstriker // Knell Striker
|
||||
//[Idname] == Zakarumshand // Zakarum's Hand
|
||||
@@ -1563,7 +1625,7 @@
|
||||
[Name] == Quarterstaff && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddamage] == 300 // Eth Ribcracker Perfect
|
||||
//[Name] == Elderstaff && [Quality] == Unique # [Itemaddskilltab] >= 2 // Ondal's Wisdom (Eth/Non-Eth)
|
||||
[Name] == Elderstaff && [Quality] == Unique # [Itemaddskilltab] == 4 && [Magicdamagereduction] == 8 && [Energy] == 50 && [Plusdefense] == 550 // Ondal's Wisdom (Eth/Non-Eth) Perfect
|
||||
//[Idname] == Mangsongslesson // Mang Song's Lesson
|
||||
[Name] == Archonstaff && [Quality] == Unique // Mang Song's Lesson
|
||||
|
||||
//----------------------------------------------------------
|
||||
//================= Unique Throwing Weapons ================
|
||||
@@ -1574,7 +1636,7 @@
|
||||
//[Idname] == Thescalper // The Scalper
|
||||
[Name] == Francisca && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddamage] >= 200 && [Lifeleech] >= 6 // Eth The Scalper Perfect
|
||||
//[Idname] == Warshrike // Warshrike
|
||||
[Name] == Wingedknife && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddamage] >= 250 // Eth Warshrike Perfect
|
||||
[Name] == Wingedknife && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddamage] >= 200 // Eth Warshrike
|
||||
//[Idname] == Gimmershred // Gimmershred
|
||||
//[Idname] == Lacerator // Lacerator
|
||||
[Name] == Wingedaxe && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddamage] >= 210 // Eth Lacerator Perfect
|
||||
@@ -1600,7 +1662,7 @@
|
||||
//[Name] == Ceremonialbow && [Quality] == Unique # [Enhanceddamage] >= 190 && [Manaleech] >= 8 // Lycander's Aim Perfect
|
||||
[Idname] == Titansrevenge // Titan's Revenge
|
||||
[Name] == Ceremonialjavelin && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddamage] >= 150 && [Lifeleech] >= 6 // Eth Titan's Revenge
|
||||
//[Idname] == Thunderstroke // Thunderstroke
|
||||
[Idname] == Thunderstroke // Thunderstroke
|
||||
[Name] == Matriarchaljavelin && [Quality] == Unique && [Flag] != Ethereal # [Javelinandspearskilltab] >= 4 && [Enhanceddamage] >= 200 // Thunderstroke Perfect
|
||||
|
||||
//-----------------------------------------------------------
|
||||
@@ -1643,8 +1705,8 @@
|
||||
|
||||
[Name] == Eldritchorb && [Quality] == Unique # [Sorceressskills] >= 2 && [Passivefiremastery] >= 10 && [Passiveltngmastery] >= 10 // Eschuta's Temper
|
||||
//[Name] == Eldritchorb && [Quality] == Unique # [Sorceressskills] == 3 && [Passivefiremastery] == 20 && [Passiveltngmastery] == 20 && [Energy] == 30 // Eschuta's Temper Perfect
|
||||
[Name] == Dimensionalshard && [Quality] == Unique # [Passivecoldpierce] >= 15 && [Lightresist] >= 25 // Death's Fathom
|
||||
//[Name] == Dimensionalshard && [Quality] == Unique # [Passivecoldpierce] == 30 && [Lightresist] == 40 // Death's Fathom Perfect
|
||||
[Name] == Dimensionalshard && [Quality] == Unique # [Passivecoldmastery] >= 15 && [Lightresist] >= 25 // Death's Fathom
|
||||
//[Name] == Dimensionalshard && [Quality] == Unique # [Passivecoldmastery] == 30 && [Lightresist] == 40 // Death's Fathom Perfect
|
||||
[Name] == Swirlingcrystal && [Quality] == Unique # [Sorceressskills] >= 3 // The Oculus
|
||||
|
||||
//-----------------------------------------------------
|
||||
@@ -1669,3 +1731,8 @@
|
||||
[Name] == Gildedshield && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddefense] >= 150 // Eth Herald Of Zakarum
|
||||
//[Name] == Gildedshield && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddefense] == 200 // Eth Herald Of Zakarum Perfect
|
||||
//[Name] == Zakarumshield && [Quality] == Unique # [Enhanceddefense] >= 150 && [Itemabsorbfirepercent] >= 15 && [Strength] >= 20 // Dragonscale
|
||||
|
||||
//--------------------------------------------------------------
|
||||
//================= Unique Warlock Grimoires ================
|
||||
//--------------------------------------------------------------
|
||||
[Type] == Grimoires && [Quality] == Unique //All unique Warlock grimoires
|
||||
@@ -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,10 +88,11 @@ 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=160,520,25,25
|
||||
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
|
||||
@@ -104,8 +105,9 @@ 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
|
||||
left_inventory=35,86,378,378
|
||||
right_inventory=866,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,9 +120,9 @@ 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
|
||||
@@ -132,7 +134,9 @@ npc_dialogue=458,4,21,140
|
||||
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
|
||||
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,9 +1,20 @@
|
||||
; There is detailed documentation for each parameter in the README.md
|
||||
[general]
|
||||
difficulty=hell
|
||||
name=Botty
|
||||
difficulty=normal
|
||||
name=bigfont
|
||||
randomize_runs=0
|
||||
saved_games_folder=
|
||||
target_tz=1
|
||||
saved_games_folder=C:\Users\alex\Saved Games\Diablo II Resurrected
|
||||
level_max_steps=20
|
||||
|
||||
; Battle.net credentials (for auto-login at launch)
|
||||
; Leave empty to log in manually
|
||||
bnet_name=
|
||||
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=Burr
|
||||
|
||||
; messaging
|
||||
custom_loot_message_hook=
|
||||
@@ -19,17 +30,79 @@ max_runtime_before_break_m=0
|
||||
; timers / fail handling
|
||||
d2r_path=C:\Program Files (x86)\Diablo II Resurrected
|
||||
max_consecutive_fails=5
|
||||
max_game_length_s=380
|
||||
max_game_length_s=160
|
||||
|
||||
; if you set this field to 1, botty will attempt to restart d2 after a crash or failure
|
||||
restart_d2r_when_stuck=0
|
||||
hardcore=0
|
||||
|
||||
; screenshots
|
||||
info_screenshots=1
|
||||
loot_screenshots=0
|
||||
pickit_screenshots=0
|
||||
|
||||
[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)
|
||||
reshuffle_each_rotation = 1
|
||||
|
||||
; 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:
|
||||
; Add these possible routes to "order" as a comma delimited list to run them:
|
||||
; run_trav
|
||||
; run_pindle
|
||||
; run_eldritch
|
||||
@@ -37,23 +110,33 @@ pickit_screenshots=0
|
||||
; run_nihlathak
|
||||
; run_arcane
|
||||
; run_diablo
|
||||
order=run_pindle, run_eldritch_shenk
|
||||
; run_vizier
|
||||
; 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
|
||||
|
||||
[char]
|
||||
; ==========================
|
||||
; ==== Mandatory Fields ====
|
||||
; ==========================
|
||||
; These configs have to be alligned with your d2r settings and char build
|
||||
type=light_sorc
|
||||
type=fohdin
|
||||
belt_rows=4
|
||||
casting_frames=10
|
||||
cta_available=0
|
||||
casting_frames=8
|
||||
cta_casting_frames=8
|
||||
attack_frames=15
|
||||
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:
|
||||
force_move=e
|
||||
@@ -68,12 +151,12 @@ 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=5
|
||||
town_portal=6
|
||||
; call to arms settings:
|
||||
weapon_switch=x
|
||||
battle_orders=f7
|
||||
battle_command=f8
|
||||
weapon_switch=w
|
||||
battle_orders=7
|
||||
battle_command=8
|
||||
|
||||
; ==========================
|
||||
; ==== Optional configs ====
|
||||
@@ -85,20 +168,28 @@ use_merc=1
|
||||
atk_len_arc=2.5
|
||||
atk_len_eldritch=3.0
|
||||
atk_len_nihlathak=4.0
|
||||
atk_len_pindle=3.0
|
||||
atk_len_pindle=9.0
|
||||
atk_len_shenk=4.0
|
||||
atk_len_diablo=3.0
|
||||
atk_len_trav=3.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,14 +197,14 @@ 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
|
||||
take_health_potion=0.90
|
||||
take_mana_potion=0.80
|
||||
take_rejuv_potion_health=0.75
|
||||
take_rejuv_potion_mana=0.50
|
||||
heal_merc=0.7
|
||||
heal_rejuv_merc=0.2
|
||||
chicken=0.35
|
||||
merc_chicken=0
|
||||
heal_rejuv_merc=0.25
|
||||
chicken=0.75
|
||||
merc_chicken=0.25
|
||||
|
||||
; Misc.
|
||||
; helps reduce accidental pickups when enabled especially on walking characters
|
||||
@@ -121,9 +212,8 @@ enable_no_pickup=1
|
||||
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
|
||||
pre_buff_every_run=1
|
||||
pre_buff_every_run=0
|
||||
runs_per_repair=0
|
||||
runs_per_stash=4
|
||||
sell_junk=0
|
||||
@@ -135,16 +225,16 @@ stash_destination=3,2,1,0
|
||||
; 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=2000
|
||||
|
||||
; ===========================
|
||||
; ==== Builds: Sorceress ====
|
||||
; ===========================
|
||||
[sorceress]
|
||||
energy_shield=
|
||||
frozen_armor=
|
||||
static_field=
|
||||
telekinesis=
|
||||
energy_shield=f3
|
||||
frozen_armor=f4
|
||||
static_field=f5
|
||||
telekinesis=f6
|
||||
thunder_storm=
|
||||
|
||||
[light_sorc]
|
||||
@@ -157,55 +247,96 @@ 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=
|
||||
|
||||
[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=f2
|
||||
|
||||
[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=
|
||||
holy_shield=f2
|
||||
redemption=f3
|
||||
vigor=f4
|
||||
|
||||
[fohdin]
|
||||
; foh must be left skill, hotkey required
|
||||
blessed_hammer=
|
||||
concentration=
|
||||
conviction=
|
||||
foh=
|
||||
holy_bolt=
|
||||
conviction=f5
|
||||
foh=f6
|
||||
holy_bolt=f7
|
||||
|
||||
[hammerdin]
|
||||
blessed_hammer=
|
||||
concentration=
|
||||
blessed_hammer=f4
|
||||
concentration=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 +351,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 +423,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=
|
||||
@@ -316,6 +462,6 @@ message_headers=
|
||||
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
|
||||
pathing_delay_factor=2
|
||||
; If you want to control Hyper-V window from host use 0,51 here
|
||||
window_client_area_offset=0,0
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
; x light senetry: +6
|
||||
; x weapon block: +1
|
||||
; x death sentrey: +4
|
||||
shop_trap_claws=1
|
||||
shop_trap_claws=0
|
||||
trap_min_score=13
|
||||
|
||||
; 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=13
|
||||
|
||||
[gloves]
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
Desktop screenshot tool - captures the full Windows desktop or a specific window.
|
||||
Usage:
|
||||
python desktop_snap.py # capture full desktop
|
||||
python desktop_snap.py D2R # capture D2R window only
|
||||
Saves to screenshots/desktop_snap.png
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import cv2
|
||||
from mss import mss
|
||||
|
||||
SAVE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "screenshots", "desktop_snap.png")
|
||||
os.makedirs(os.path.dirname(SAVE_PATH), exist_ok=True)
|
||||
|
||||
|
||||
def snap_full_desktop():
|
||||
"""Capture the full desktop."""
|
||||
with mss() as sct:
|
||||
img = sct.grab(sct.monitors[1]) # monitors[1] = primary display
|
||||
# Convert from BGRA to BGR
|
||||
img_bgr = img.rgb
|
||||
cv2.imwrite(SAVE_PATH, img_bgr)
|
||||
print(f"Saved full desktop to: {SAVE_PATH}")
|
||||
print(f"Shape: {cv2.imread(SAVE_PATH).shape}")
|
||||
|
||||
|
||||
def snap_d2r_window():
|
||||
"""Capture the D2R window."""
|
||||
import numpy as np
|
||||
import win32gui
|
||||
import win32ui
|
||||
import win32con
|
||||
|
||||
# Find D2R window
|
||||
def enum_cb(hwnd, results):
|
||||
if win32gui.IsWindowVisible(hwnd):
|
||||
title = win32gui.GetWindowText(hwnd)
|
||||
if "diablo" in title.lower() or "d2r" in title.lower():
|
||||
results.append(hwnd)
|
||||
|
||||
hwnds = []
|
||||
win32gui.EnumWindows(enum_cb, hwnds)
|
||||
|
||||
if not hwnds:
|
||||
print("ERROR: D2R window not found. Is it running?")
|
||||
return
|
||||
|
||||
hwnd = hwnds[0]
|
||||
print(f"Found D2R window: {win32gui.GetWindowText(hwnd)}")
|
||||
|
||||
# Get window client area
|
||||
rect = win32gui.GetClientRect(hwnd)
|
||||
w, h = rect[2] - rect[0], rect[3] - rect[1]
|
||||
|
||||
# Capture client area
|
||||
hdc = win32gui.GetDC(hwnd)
|
||||
hdc_mem = win32gui.CreateCompatibleDC(hdc)
|
||||
bmp = win32gui.CreateCompatibleBitmap(hdc, w, h)
|
||||
win32gui.SelectObject(hdc_mem, bmp)
|
||||
win32gui.BitBlt(hdc_mem, 0, 0, w, h, hdc, 0, 0, win32con.SRCCOPY)
|
||||
|
||||
# Convert to image
|
||||
bmp_info = win32ui.CreateBitmapFromHandle(bmp)
|
||||
bmp_info.SaveBitmapFile(hdc_mem, SAVE_PATH)
|
||||
|
||||
win32gui.DeleteObject(bmp)
|
||||
win32gui.DeleteDC(hdc_mem)
|
||||
win32gui.ReleaseDC(hwnd, hdc)
|
||||
|
||||
img = cv2.imread(SAVE_PATH)
|
||||
print(f"Saved D2R window to: {SAVE_PATH}")
|
||||
print(f"Shape: {img.shape}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "D2R":
|
||||
snap_d2r_window()
|
||||
else:
|
||||
snap_full_desktop()
|
||||
@@ -1,21 +1,42 @@
|
||||
# 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/)
|
||||
- Install [Miniforge](https://github.com/conda-forge/miniforge) (recommended over Miniconda for conda-forge packages). Check "Add to PATH" during installation.
|
||||
- Alternatively, [Miniconda](https://docs.conda.io/en/latest/miniconda.html) works too.
|
||||
- Install [git](https://gitforwindows.org/)
|
||||
|
||||
## Getting started
|
||||
```bash
|
||||
git clone https://github.com/aeon0/botty.git
|
||||
git clone https://github.com/Hoblirm/botty.git
|
||||
cd botty
|
||||
conda env create environment.yml
|
||||
|
||||
# Create the conda environment (installs all Python deps + tesserocr with tesseract)
|
||||
conda env create -f environment.yml
|
||||
|
||||
# Activate and run
|
||||
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
|
||||
|
||||
### PowerShell users
|
||||
```powershell
|
||||
conda init powershell # One time setup
|
||||
conda activate botty
|
||||
python src/main.py
|
||||
```
|
||||
|
||||
### No conda? Quick setup
|
||||
If you don't want to use conda, you can install dependencies with pip, but `tesserocr`
|
||||
requires the tesseract C library which is easiest to get via conda. See `environment.yml`
|
||||
for the full dependency list.
|
||||
|
||||
## Running with the launcher
|
||||
A `run_botty.bat` file is provided in the project root. It auto-detects the conda
|
||||
environment and launches the bot. You can also run manually:
|
||||
```cmd
|
||||
conda activate botty
|
||||
cd C:\path\to\botty
|
||||
python src\main.py
|
||||
```
|
||||
|
||||
## Tests
|
||||
@@ -28,7 +49,7 @@ pytest -s -v
|
||||
# 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.
|
||||
To test single files / routines, most files also can be executed separately. 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)
|
||||
|
||||
@@ -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,258 @@
|
||||
# Anti-Detection Framework for Botty-Go
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the multi-layered anti-detection system built into botty-go.
|
||||
Each layer addresses a specific detection vector that Blizzard and modern anti-cheat
|
||||
systems use to identify bots.
|
||||
|
||||
---
|
||||
|
||||
## 1. Server-Side Behavior Analysis Countermeasures
|
||||
|
||||
### Detection: Session length, timing consistency, pathing patterns, repetition
|
||||
|
||||
### Countermeasures:
|
||||
|
||||
#### 1a. Variable Session Scheduling
|
||||
- **Implementation:** `internal/schedule/scheduler.go`
|
||||
- Randomized session start times using a circadian model
|
||||
- Simulated human sleep patterns: 6-10 hour breaks between sessions
|
||||
- Weekend/weekday behavior variance (humans play differently on weekends)
|
||||
- Random session lengths: 20min to 6hours with exponential distribution
|
||||
- Occasional "just 5 more minutes" overtime and "I'm tired" early stops
|
||||
|
||||
#### 1b. Stochastic Pathing
|
||||
- **Implementation:** `internal/pather/stochastic.go`
|
||||
- Add deliberate pathing imperfection: 5-15% deviation from optimal route
|
||||
- Occasional wrong-way teleports followed by course correction
|
||||
- Non-optimal waypoint selections (humants don't always take shortest path)
|
||||
- Variable route ordering with cooldown-dependent choices
|
||||
- 2-3% chance of "getting lost" and using wrong waypoint first
|
||||
|
||||
#### 1c. Skill Rotation Variance
|
||||
- **Implementation:** `internal/char/behavior.go`
|
||||
- Variable pre-buff timing (humans rush sometimes, sometimes take time)
|
||||
- Occasional wrong skill selection followed by correction
|
||||
- Potion usage with human-like hesitation (check multiple times before drinking)
|
||||
- Merc healing variance: sometimes forget, sometimes over-heal
|
||||
|
||||
#### 1d. Route Randomization with Context
|
||||
- **Implementation:** `internal/bot/route_planner.go`
|
||||
- Dynamic route selection based on:
|
||||
- Time since last run of each type
|
||||
- Current TP scroll count (humans adapt)
|
||||
- Gem/transmute urgency
|
||||
- Occasional "feels like it" switches
|
||||
- Never perfect round-robin; use weighted probability with drift
|
||||
|
||||
#### 1e. Farming Repetition Masking
|
||||
- Never run the same route more than 8 times consecutively
|
||||
- Insert "town breaks": stash visit, shrine check, repair, gamble
|
||||
- 1-2% chance of "I'm bored, switching to different run" mid-session
|
||||
- Vary kill strategies: sometimes rush, sometimes methodical
|
||||
|
||||
---
|
||||
|
||||
## 2. Warden / Client Integrity Countermeasures
|
||||
|
||||
### Detection: Loaded modules, injected DLLs, memory signatures, debuggers
|
||||
|
||||
### Countermeasures:
|
||||
|
||||
#### 2a. Pixel-Only Architecture (No Memory Access)
|
||||
- **Implementation:** entire bot reads game state ONLY via screenshots
|
||||
- NO memory reading, NO DLL injection, NO process hooking
|
||||
- Same attack surface as a human with a camera pointed at the screen
|
||||
- This is the #1 defense: if you only use screen capture + input simulation,
|
||||
there's nothing to scan in process memory
|
||||
|
||||
#### 2b. Clean Process Environment
|
||||
- **Implementation:** `internal/runtime/clean_env.go`
|
||||
- Standard Go binary with no suspicious imports
|
||||
- No debuggers, no memory readers, no process manipulation
|
||||
- Run as a normal application, not injected
|
||||
|
||||
#### 2c. Overlay Avoidance
|
||||
- Never draw on top of game window
|
||||
- No window hooking or injection
|
||||
- Screenshot from a separate thread, not an overlay
|
||||
|
||||
---
|
||||
|
||||
## 3. Input Pattern Analysis Countermeasures
|
||||
|
||||
### Detection: Synthetic inputs, smooth cursor paths, periodic inputs, no micro-corrections
|
||||
|
||||
### Countermeasures:
|
||||
|
||||
#### 3a. Human Motor Model
|
||||
- **Implementation:** `internal/mouse/human_model.go`
|
||||
- Full biomechanical mouse model based on Fitts' Law and human motion studies
|
||||
- Real human mouse data characteristics:
|
||||
- Multi-segment movement with micro-pauses (1-3 segments per motion)
|
||||
- Acceleration curve: start slow, peak in middle, decelerate into target
|
||||
- Endpoint micro-adjustments: 2-5 pixel wobble before click
|
||||
- Inter-trial variability: each movement is unique even to same target
|
||||
- Asymmetric error distribution: overshoot more right/down (human bias)
|
||||
|
||||
#### 3b. Click Timing Model
|
||||
- **Implementation:** `internal/mouse/click_model.go`
|
||||
- Variable time between "arriving" at target and clicking: 50ms-800ms
|
||||
- Pressure curve: humans don't click at exact same speed
|
||||
- Double-click rate varies naturally
|
||||
- Occasional misses: 0.5-1% of clicks land slightly off (1-3px)
|
||||
|
||||
#### 3c. Keyboard Behavior Model
|
||||
- **Implementation:** `internal/keyboard/human_model.go`
|
||||
- Key press duration variance: not all keypresses are identical
|
||||
- Typing rhythm for skill hotkeys: natural cadence with micro-pauses
|
||||
- Occasional key repeat (holding too long = rapid fire)
|
||||
- Realistic key-up/key-down timing ratios
|
||||
|
||||
#### 3d. Statistical Indistinguishability
|
||||
- **Implementation:** `internal/input/stats.go`
|
||||
- All input streams modeled from real human motion capture data
|
||||
- Entropy analysis of output matches human baselines
|
||||
- Auto-calibration: measure user's own input if they do manual play
|
||||
- Periodically inject "manual-looking" variance spikes
|
||||
|
||||
---
|
||||
|
||||
## 4. Economy and Item-Flow Countermeasures
|
||||
|
||||
### Detection: Gold accumulation, rune farming, item transfer networks, mule behavior
|
||||
|
||||
### Countermeasures:
|
||||
|
||||
#### 4a. Natural Accumulation Rate
|
||||
- **Implementation:** `internal/inventory/economy.go`
|
||||
- Vary farming intensity: some sessions heavy, some light
|
||||
- Match accumulation to stated playtime (more sessions = more loot)
|
||||
- Occasionally "waste" items on gambling/repairs like a real player
|
||||
|
||||
#### 4b. Realistic Trading Patterns
|
||||
- No mass item funneling
|
||||
- If trading, do it in human-sized batches with natural pauses
|
||||
- Vary trade partners and timing
|
||||
|
||||
#### 4c. Rune Farming Variance
|
||||
- Don't farm the same runes every session
|
||||
- Match rune acquisition to character progression
|
||||
- Occasionally skip rune picks when "full"
|
||||
|
||||
---
|
||||
|
||||
## 5. Ban Wave Defense
|
||||
|
||||
### Detection: Delayed batch bans
|
||||
|
||||
### Countermeasures:
|
||||
|
||||
#### 5a. Graceful Degradation
|
||||
- **Implementation:** `internal/runtime/safe_mode.go`
|
||||
- If one account gets banned, immediately reduce intensity across all
|
||||
- Auto-pause farming for 48-72 hours (simulating "taking a break")
|
||||
- Gradual return with reduced session lengths
|
||||
- Change behavior patterns after any ban event
|
||||
|
||||
#### 5b. Account Diversity
|
||||
- Each account has distinct "personality":
|
||||
- Different session timing preferences
|
||||
- Different route preferences
|
||||
- Different response timing distributions
|
||||
- Different play styles (rusher vs methodical)
|
||||
|
||||
---
|
||||
|
||||
## 6. Server Authority Countermeasures
|
||||
|
||||
### Detection: Server-side validation of movement, drops, combat, inventory
|
||||
|
||||
### Countermeasures:
|
||||
|
||||
#### 6a. Server-Authoritative Behavior
|
||||
- **Implementation:** `internal/bot/server_aware.go`
|
||||
- Only interact with what the server actually shows
|
||||
- Wait for server confirmation before acting (e.g., confirm item picked up)
|
||||
- Respect server-enforced movement limits (no speed hacks)
|
||||
- Process drops in game-authorized order
|
||||
|
||||
#### 6b. No Client Manipulation
|
||||
- Never try to spoof packets, modify client, or exploit desync
|
||||
- Purely reactive: see screen -> decide -> act -> wait for response
|
||||
|
||||
---
|
||||
|
||||
## 7. Social/Reporting System Countermeasures
|
||||
|
||||
### Detection: Player reports + telemetry correlation
|
||||
|
||||
### Countermeasures:
|
||||
|
||||
#### 7a. Social Stealth
|
||||
- **Implementation:** `internal/social/stealth.go`
|
||||
- Play during off-peak hours less suspiciously
|
||||
- Avoid solo-public routes that attract attention
|
||||
- Occasionally join other players' games (with reduced automation)
|
||||
- Inherit human-like chat behavior if configured
|
||||
|
||||
---
|
||||
|
||||
## 8. Hardware/Identity Correlation Countermeasures
|
||||
|
||||
### Detection: IP patterns, hardware fingerprints, VMs, account clusters
|
||||
|
||||
### Countermeasures:
|
||||
|
||||
#### 8a. Clean Deployment
|
||||
- **Implementation:** `internal/deploy/clean.go`
|
||||
- Run on real hardware, not VMs
|
||||
- Use residential IP, not datacenter
|
||||
- One account per hardware profile
|
||||
- No VPN/proxy during play sessions
|
||||
|
||||
---
|
||||
|
||||
## Implementation Architecture
|
||||
|
||||
```
|
||||
internal/
|
||||
├── input/ # Human-like input generation
|
||||
│ ├── mouse_model.go # Fitts' Law mouse movement
|
||||
│ ├── click_model.go # Human click timing
|
||||
│ ├── keyboard_model.go # Keyboard behavior
|
||||
│ └── stats.go # Statistical verification
|
||||
├── behavior/ # High-level human behavior simulation
|
||||
│ ├── scheduler.go # Session scheduling
|
||||
│ ├── route_planner.go # Dynamic route selection
|
||||
│ ├── fatigue.go # Simulated fatigue/boredom
|
||||
│ └── personality.go # Per-account personality
|
||||
├── economy/ # Economic behavior masking
|
||||
│ ├── accumulation.go # Natural loot accumulation
|
||||
│ └── trading.go # Human-like trading patterns
|
||||
├── safe_mode/ # Graceful degradation
|
||||
│ ├── detection.go # Ban wave detection
|
||||
│ └── cooldown.go # Auto-pause and return
|
||||
└── deploy/ # Clean deployment helpers
|
||||
└── check.go # Pre-flight integrity checks
|
||||
```
|
||||
|
||||
## Key Design Principles
|
||||
|
||||
1. **Statistical indistinguishability:** Output must be statistically
|
||||
indistinguishable from real human input. We use actual human motion
|
||||
capture data distributions, not made-up random numbers.
|
||||
|
||||
2. **Controlled imperfection:** A human is inefficient, forgetful, and
|
||||
inconsistent. The bot should be too — but in a way that matches
|
||||
real human distributions.
|
||||
|
||||
3. **No single fingerprint:** Every instance should have unique enough
|
||||
characteristics that correlating two accounts is hard.
|
||||
|
||||
4. **Adaptability:** If behavior changes are detected, the system should
|
||||
be able to recalibrate based on new data.
|
||||
|
||||
5. **Defense in depth:** No single countermeasure is sufficient. The
|
||||
combination across all layers is what provides real protection.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Botty-Go
|
||||
|
||||
D2R Pixel Bot rewritten in Go for cross-platform support (Linux + Windows).
|
||||
|
||||
Based on the Python Botty project (johannes-do/botty), this is a ground-up rewrite
|
||||
in Go that maintains compatibility with the same config files, templates, and run
|
||||
logic while adding native Linux support.
|
||||
|
||||
## Features
|
||||
|
||||
- Cross-platform: Linux (X11/Wayland) and Windows
|
||||
- Same config format as original Botty (params.ini, game.ini, shop.ini)
|
||||
- Template matching with OpenCV Go bindings
|
||||
- Tesseract OCR for item identification
|
||||
- Human-like mouse movement (Bezier curves)
|
||||
- BNIP pickit language
|
||||
- All original character builds (Sorc, Paladin, Necro, Barbarian, etc.)
|
||||
- All original runs (Pindle, Eldritch, Shenk, Trav, Nihlathak, Arcane, Diablo)
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
go build -o botty ./cmd/botty
|
||||
|
||||
# Windows (from Linux with cross-compile)
|
||||
GOOS=windows GOARCH=amd64 go build -o botty.exe ./cmd/botty
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Copy `config/` from the original Botty project. Params, routes, and character
|
||||
config work identically.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Legacy: Go Rewrite Design Notes
|
||||
|
||||
These docs are archived from an abandoned `~/git/botty-go` directory (May 2026).
|
||||
That project was a planned ground-up Go rewrite of `johannes-do/botty` for
|
||||
cross-platform (Linux + Windows) support. Only design docs existed — no `.go`
|
||||
source was ever written.
|
||||
|
||||
The Python `my-botty` project (this repo) is the active path. These docs are
|
||||
kept here as **reference material**, primarily for Milestone 2 (anti-detection /
|
||||
stealth) of `~/.claude/plans/continue-the-make-up-sunny-honey.md`.
|
||||
|
||||
## Files
|
||||
|
||||
- **`ANTI_DETECTION.md`** — Multi-layer anti-detection framework. Covers
|
||||
server-side behavior analysis countermeasures (session scheduling, stochastic
|
||||
pathing, skill rotation variance) and more. Directly applicable as the design
|
||||
basis for the Python stealth layer.
|
||||
- **`GO_REWRITE_README.md`** — Original README of the abandoned Go project.
|
||||
Context only — explains feature scope and what the rewrite was aiming for.
|
||||
@@ -8,13 +8,13 @@ dependencies:
|
||||
- leptonica
|
||||
- tesseract=5.1.0
|
||||
- pkg-config
|
||||
- rapidfuzz
|
||||
- rapidfuzz=2.15.1
|
||||
- pip:
|
||||
- pyinstaller
|
||||
- opencv-python==4.5.5.64
|
||||
- transitions
|
||||
- mss
|
||||
- numpy
|
||||
- mss==7.0.1
|
||||
- numpy==1.26.4
|
||||
- mouse
|
||||
- keyboard
|
||||
- beautifultable
|
||||
@@ -23,8 +23,8 @@ dependencies:
|
||||
- coverage
|
||||
- pytest
|
||||
- pytest-env
|
||||
- pytest-pythonpath
|
||||
- pytest-mock
|
||||
- pyparsing
|
||||
- graphviz
|
||||
- psutil
|
||||
- cryptography
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
[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",
|
||||
"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.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"]
|
||||
@@ -2,4 +2,3 @@
|
||||
env =
|
||||
PYTHONPATH=./src
|
||||
RUN_ENV=test
|
||||
python_paths = ./src
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
"""
|
||||
D2R capture tool - works with Windows DPI scaling.
|
||||
|
||||
Set DPI awareness then grab the D2R client area directly.
|
||||
|
||||
Keys: F1-full OCR F2-dialogue F3-questlog F4-NPCs F5-pixel F12-exit
|
||||
"""
|
||||
import os, sys, cv2, numpy as np, keyboard, win32gui, win32con, ctypes
|
||||
from datetime import datetime
|
||||
|
||||
# Set DPI awareness - this makes Win32 APIs return logical (unscaled) coordinates
|
||||
try:
|
||||
ctypes.windll.shcore.SetProcessDpiAwareness(2)
|
||||
except:
|
||||
try:
|
||||
ctypes.windll.shcore.SetProcessDpiAwareness(1)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Fix tesserocr DLLs
|
||||
if sys.platform == "win32":
|
||||
_dll = os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "Library", "bin")
|
||||
if os.path.isdir(_dll):
|
||||
os.add_dll_directory(_dll)
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "src"))
|
||||
|
||||
SAVE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "screenshots", "debug")
|
||||
os.makedirs(SAVE, exist_ok=True)
|
||||
|
||||
|
||||
def find_d2r():
|
||||
hwnds = []
|
||||
def cb(h, r):
|
||||
if 'diablo' in win32gui.GetWindowText(h).lower() and win32gui.IsWindowVisible(h):
|
||||
r.append(h)
|
||||
win32gui.EnumWindows(cb, hwnds)
|
||||
return hwnds[0] if hwnds else None
|
||||
|
||||
|
||||
def grab():
|
||||
"""Grab D2R client area at native 1280x720 resolution."""
|
||||
from mss import mss
|
||||
hwnd = find_d2r()
|
||||
if not hwnd:
|
||||
print(" [ERROR] D2R not found")
|
||||
return None
|
||||
|
||||
client = win32gui.GetClientRect(hwnd)
|
||||
w, h = client[2]-client[0], client[3]-client[1]
|
||||
screen_pos = win32gui.ClientToScreen(hwnd, (0, 0))
|
||||
|
||||
with mss() as sct:
|
||||
region = {
|
||||
'top': screen_pos[1],
|
||||
'left': screen_pos[0],
|
||||
'width': w,
|
||||
'height': h
|
||||
}
|
||||
sct_img = sct.grab(region)
|
||||
img = np.array(sct_img)[:, :, :3] # BGRA -> BGR
|
||||
|
||||
# Resize to 1280x720 if needed
|
||||
if w != 1280 or h != 720:
|
||||
img = cv2.resize(img, (1280, 720), interpolation=cv2.INTER_LINEAR)
|
||||
print(f" [RESIZED] {w}x{h} -> 1280x720")
|
||||
else:
|
||||
print(f" [CAPTURED] {w}x{h}")
|
||||
|
||||
return img
|
||||
|
||||
|
||||
def ocr(img, roi=None):
|
||||
try:
|
||||
from d2r_image.ocr import image_to_text
|
||||
target = img if roi is None else img[roi[1]:roi[1]+roi[3], roi[0]:roi[0]+roi[2]]
|
||||
result = image_to_text(target, psm=6, scale=1.5, threshold=25)
|
||||
return [r.text.strip() for r in result if r.text.strip()]
|
||||
except Exception as e:
|
||||
return [f"[OCR ERROR] {e}"]
|
||||
|
||||
|
||||
def save(img, label):
|
||||
path = os.path.join(SAVE, f"{label}_{datetime.now().strftime('%H%M%S')}.png")
|
||||
cv2.imwrite(path, img)
|
||||
print(f" [SAVED] {path}")
|
||||
return path
|
||||
|
||||
|
||||
# === Handlers ===
|
||||
|
||||
def on_f1():
|
||||
print("\n[=== FULL CAPTURE ===]")
|
||||
img = grab()
|
||||
if not img:
|
||||
return
|
||||
h, w = img.shape[:2]
|
||||
print(f" Size: {w}x{h}")
|
||||
save(img, "full")
|
||||
|
||||
# UI detection
|
||||
print(" UI:")
|
||||
try:
|
||||
from ui_manager import ScreenObjects, is_visible
|
||||
found = False
|
||||
for name in ['InGame', 'Loading', 'MainMenu', 'OnlineStatus', 'DeathScreen',
|
||||
'NPCDialogue', 'RightPanel', 'LeftPanel', 'SkillsExpanded']:
|
||||
obj = getattr(ScreenObjects, name, None)
|
||||
if obj and is_visible(obj, img):
|
||||
print(f" [VISIBLE] {name}")
|
||||
found = True
|
||||
if not found:
|
||||
print(" (none)")
|
||||
except Exception as e:
|
||||
print(f" [err] {e}")
|
||||
|
||||
# Full OCR
|
||||
print(" OCR:")
|
||||
lines = ocr(img)
|
||||
for l in lines[:30]:
|
||||
print(f" {l}")
|
||||
if len(lines) > 30:
|
||||
print(f" ... and {len(lines)-30} more")
|
||||
|
||||
|
||||
def on_f2():
|
||||
print("\n[=== DIALOGUE ===]")
|
||||
img = grab()
|
||||
if not img:
|
||||
return
|
||||
save(img, "dialogue")
|
||||
|
||||
text = ocr(img, (200, 460, 880, 100))
|
||||
if text:
|
||||
print(" NPC says:")
|
||||
for l in text:
|
||||
print(f" {l}")
|
||||
else:
|
||||
print(" (no NPC text)")
|
||||
|
||||
opts = ocr(img, (200, 560, 880, 140))
|
||||
if opts:
|
||||
print(" Options:")
|
||||
for i, o in enumerate(opts):
|
||||
print(f" [{i}] {o}")
|
||||
else:
|
||||
print(" (no options detected - is dialogue box open?)")
|
||||
|
||||
|
||||
def on_f3():
|
||||
print("\n[=== QUEST LOG ===]")
|
||||
img = grab()
|
||||
if not img:
|
||||
return
|
||||
save(img, "quest_log")
|
||||
for l in ocr(img, (200, 100, 880, 520)):
|
||||
print(f" {l}")
|
||||
|
||||
|
||||
def on_f4():
|
||||
print("\n[=== NPC DETECTION ===]")
|
||||
img = grab()
|
||||
if not img:
|
||||
return
|
||||
save(img, "npcs")
|
||||
try:
|
||||
import template_finder
|
||||
from npc_manager import npcs
|
||||
found = []
|
||||
for name, data in npcs.items():
|
||||
for t in data.get("template_group", []):
|
||||
r = template_finder.search(t, img, threshold=0.35)
|
||||
if r.valid:
|
||||
found.append(f" {name} at {r.center_monitor} ({r.score:.2f})")
|
||||
break
|
||||
for f in found:
|
||||
print(f)
|
||||
if not found:
|
||||
print(" (none)")
|
||||
except Exception as e:
|
||||
print(f" [ERROR] {e}")
|
||||
|
||||
|
||||
def on_f5():
|
||||
img = grab()
|
||||
if not img:
|
||||
return
|
||||
import mouse as _mouse
|
||||
mx, my = _mouse.get_position()
|
||||
hwnd = find_d2r()
|
||||
if hwnd:
|
||||
screen_pos = win32gui.ClientToScreen(hwnd, (0, 0))
|
||||
ix = mx - screen_pos[0]
|
||||
iy = my - screen_pos[1]
|
||||
if 0 <= ix < img.shape[1] and 0 <= iy < img.shape[0]:
|
||||
b, g, r = img[iy, ix]
|
||||
print(f" ({ix},{iy}) RGB({r},{g},{b})")
|
||||
else:
|
||||
print(" Mouse outside D2R client area")
|
||||
|
||||
|
||||
# === Run ===
|
||||
|
||||
def run():
|
||||
print("=== Botty Capture Tool ===")
|
||||
print(" F1 - Full capture + OCR + UI detection")
|
||||
print(" F2 - Dialogue capture + OCR")
|
||||
print(" F3 - Quest log OCR (press O in D2R first)")
|
||||
print(" F4 - Detect NPCs")
|
||||
print(" F5 - Mouse pixel color")
|
||||
print(" F12 - Exit")
|
||||
print("Ready.")
|
||||
|
||||
keyboard.add_hotkey('f1', on_f1)
|
||||
keyboard.add_hotkey('f2', on_f2)
|
||||
keyboard.add_hotkey('f3', on_f3)
|
||||
keyboard.add_hotkey('f4', on_f4)
|
||||
keyboard.add_hotkey('f5', on_f5)
|
||||
keyboard.add_hotkey('f12', lambda: (print("\nBye."), sys.exit(0)))
|
||||
keyboard.wait()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -0,0 +1,377 @@
|
||||
# Botty Auto-Quest: Implementation Plan
|
||||
|
||||
## 1. Architecture
|
||||
|
||||
```
|
||||
src/quest/
|
||||
__init__.py
|
||||
quest_manager.py # State machine, quest sequencing, persistence
|
||||
quest_state.py # JSON-based quest progress tracker
|
||||
quest_npc.py # Generic NPC interaction utilities (talk to NPC, dialogue selection)
|
||||
quest_items.py # Quest item detection and management
|
||||
quest_combat.py # Combat helpers for quest-specific fights
|
||||
a1/
|
||||
__init__.py # Act 1 quest runner
|
||||
q1a1_smith.py # Q1: The Search for the Smith
|
||||
q1a2_cain.py # Q2: Tools of the Trade
|
||||
q1a3_sacrifice.py # Q3: Sacrifice
|
||||
q1a4_town_portal.py# Q4: The Summoner
|
||||
q1a5_skeleton_king.py # Q5: The Shepherd
|
||||
q1a6_andariel.py # Q6: The Fallen Angel
|
||||
a2/
|
||||
__init__.py # Act 2 quest runner
|
||||
q2a1_jerhyn.py # Q1: Radament
|
||||
q2a2_atheistic.py # Q2: The Horadric Staff
|
||||
q2a3_hephalon.py # Q3: Tyrael's Breath
|
||||
q2a4_atalai.py # Q4: Secrets
|
||||
q2a5_tarbedit.py # Q5: The Summoner
|
||||
q2a6_nihlathak.py # Q6: The Seven Tombs
|
||||
q2a7_duriel.py # Q7: The Fallen Angel
|
||||
a3/
|
||||
__init__.py # Act 3 quest runner
|
||||
q3a1_larzuk.py # Q1: The Forgotten Tower
|
||||
q3a2_cain.py # Q2: The Quest for the Horizon
|
||||
q3a3_kaelthas.py # Q3: The Hellforge
|
||||
q3a4_hellgate.py # Q4: The Hellgate
|
||||
q3a5_mephisto.py # Q5: The Prime Evil
|
||||
a4/
|
||||
__init__.py # Act 4 quest runner
|
||||
q4a1_izual.py # Q1: The Fallen Angel
|
||||
q4a2_harumony.py # Q2: The Fallen Angel
|
||||
q4a3_diablo.py # Q3: The Prime Evil
|
||||
a5/
|
||||
__init__.py # Act 5 quest runner
|
||||
q5a1_ancients.py # Q1: The Fallen Angel
|
||||
q5a2_cain.py # Q2: The Quest for the Horizon
|
||||
q5a3_baal.py # Q3: The Prime Evil
|
||||
```
|
||||
|
||||
## 2. Quest Manager Design
|
||||
|
||||
```python
|
||||
# quest/quest_manager.py
|
||||
|
||||
from enum import Enum
|
||||
import json, os
|
||||
from config import Config
|
||||
|
||||
class QuestStatus(Enum):
|
||||
PENDING = "pending"
|
||||
IN_PROGRESS = "in_progress"
|
||||
COMPLETED = "completed"
|
||||
SKIPPED = "skipped"
|
||||
|
||||
class QuestManager:
|
||||
_state_file = "config/quest_state.json"
|
||||
|
||||
def __init__(self):
|
||||
self.state = self._load_state()
|
||||
self.char = None # reference to IChar
|
||||
self.pather = None
|
||||
|
||||
def _load_state(self):
|
||||
if os.path.exists(self._state_file):
|
||||
with open(self._state_file) as f:
|
||||
return json.load(f)
|
||||
return self._default_state()
|
||||
|
||||
def _default_state(self):
|
||||
# All 25 quests tracked by (act, quest_number)
|
||||
return {
|
||||
"current_act": 1,
|
||||
"quests": {
|
||||
"1-1": QuestStatus.PENDING, "1-2": QuestStatus.PENDING,
|
||||
"1-3": QuestStatus.PENDING, "1-4": QuestStatus.PENDING,
|
||||
"1-5": QuestStatus.PENDING, "1-6": QuestStatus.PENDING,
|
||||
"2-1": QuestStatus.PENDING, "2-2": QuestStatus.PENDING,
|
||||
"2-3": QuestStatus.PENDING, "2-4": QuestStatus.PENDING,
|
||||
"2-5": QuestStatus.PENDING, "2-6": QuestStatus.PENDING,
|
||||
"2-7": QuestStatus.PENDING,
|
||||
"3-1": QuestStatus.PENDING, "3-2": QuestStatus.PENDING,
|
||||
"3-3": QuestStatus.PENDING, "3-4": QuestStatus.PENDING,
|
||||
"3-5": QuestStatus.PENDING,
|
||||
"4-1": QuestStatus.PENDING, "4-2": QuestStatus.PENDING,
|
||||
"4-3": QuestStatus.PENDING,
|
||||
"5-1": QuestStatus.PENDING, "5-2": QuestStatus.PENDING,
|
||||
"5-3": QuestStatus.PENDING,
|
||||
}
|
||||
}
|
||||
|
||||
def save(self):
|
||||
with open(self._state_file, 'w') as f:
|
||||
json.dump(self.state, f, indent=2)
|
||||
|
||||
def next_quest(self) -> tuple | None:
|
||||
"""Returns (act, quest_num) of next pending quest, or None if all done."""
|
||||
current = self.state["current_act"]
|
||||
for qn in range(1, 8): # max 7 quests per act
|
||||
key = f"{current}-{qn}"
|
||||
if key in self.state["quests"] and self.state["quests"][key] == QuestStatus.PENDING:
|
||||
return (current, qn)
|
||||
return None
|
||||
|
||||
def mark_complete(self, act, qn):
|
||||
self.state["quests"][f"{act}-{qn}"] = QuestStatus.COMPLETED
|
||||
|
||||
def is_act_complete(self, act):
|
||||
for qn in range(1, 8):
|
||||
key = f"{act}-{qn}"
|
||||
if key in self.state["quests"] and self.state["quests"][key] == QuestStatus.PENDING:
|
||||
return False
|
||||
return True
|
||||
|
||||
def advance_to_next_act(self):
|
||||
"""Called when all quests in current act are done."""
|
||||
current = self.state["current_act"]
|
||||
# ... handle act transition (talk to quest NPC to unlock next act)
|
||||
self.state["current_act"] = current + 1
|
||||
self.save()
|
||||
|
||||
def run_next_quest(self):
|
||||
"""Dispatches to the appropriate quest implementation."""
|
||||
nxt = self.next_quest()
|
||||
if not nxt:
|
||||
return
|
||||
act, qn = nxt
|
||||
self.state["quests"][f"{act}-{qn}"] = QuestStatus.IN_PROGRESS
|
||||
# Route to quest implementation
|
||||
# ... call quest module
|
||||
```
|
||||
|
||||
## 3. Quest NPC Interaction
|
||||
|
||||
Every quest requires talking to NPCs. The existing `npc_manager.py` has `talk_to_npc()` which we extend.
|
||||
|
||||
```python
|
||||
# quest/quest_npc.py
|
||||
# Utilities for NPC dialogue selection
|
||||
|
||||
from utils.custom_mouse import mouse
|
||||
from utils.misc import wait
|
||||
from template_finder import search_and_wait
|
||||
from screen import grab
|
||||
|
||||
def talk_and_select(dialogue_choice: str, npc_name: str):
|
||||
"""
|
||||
Talk to NPC and select a specific dialogue option.
|
||||
botty already detects NPC and opens dialogue. We need to click
|
||||
the specific dialogue button.
|
||||
"""
|
||||
from npc_manager import talk_to_npc
|
||||
talk_to_npc(npc_name)
|
||||
wait(1)
|
||||
# D2R shows dialogue options in a box at bottom center.
|
||||
# Detect the text of each option using OCR and click the matching one.
|
||||
click_dialogue_option(dialogue_choice)
|
||||
|
||||
def click_dialogue_option(text: str):
|
||||
"""
|
||||
Use OCR to read dialogue options and click the one containing the given text.
|
||||
"""
|
||||
from d2r_image.ocr import image_to_text
|
||||
img = grab()
|
||||
roi = (300, 530, 680, 190) # dialogue box area
|
||||
result = image_to_text(cut_roi(img, roi), psm=6)
|
||||
for line in result.text.split('\n'):
|
||||
if text.lower() in line.lower():
|
||||
# Click near the center of this text line
|
||||
# ... (use OCR bounding box to find click position)
|
||||
break
|
||||
|
||||
def check_quest_item_on_screen() -> bool:
|
||||
"""Detect if a quest item tooltip is visible (gold item name)."""
|
||||
# Quest items have a gold/purple glow. Detect by color.
|
||||
img = grab()
|
||||
roi = (400, 400, 200, 200) # quest item pickup area
|
||||
# Check for gold-colored pixels indicating a quest item
|
||||
...
|
||||
```
|
||||
|
||||
## 4. Quest Item Tracking
|
||||
|
||||
Quest items (keys, scrolls, weapons) need to be tracked. We extend the inventory system.
|
||||
|
||||
```python
|
||||
# quest/quest_items.py
|
||||
|
||||
QUEST_ITEMS = {
|
||||
"stone_of_jah": "Stone of Jah",
|
||||
"hephaestons_key": "Hephaston's Key",
|
||||
"tal_rashas_will": "Tal Rasha's Will",
|
||||
"keys_to_the_crypt": "Keys to the Crypt",
|
||||
"harumony": "Harumony",
|
||||
"ancients_battle_order": "Ancient's Battle Order",
|
||||
"horadric_cube": "Horadric Cube",
|
||||
"horadric_staff": "Horadric Staff",
|
||||
"amulet_of_the_vipers": "Amulet of the Vipers",
|
||||
}
|
||||
|
||||
def has_quest_item(name: str) -> bool:
|
||||
"""Check if quest item is in inventory."""
|
||||
# Use OCR to scan inventory for item name
|
||||
# OR check by item template matching (faster)
|
||||
...
|
||||
|
||||
def equip_quest_item(name: str):
|
||||
"""Click on quest item in inventory to equip it."""
|
||||
...
|
||||
|
||||
def pickup_quest_item():
|
||||
"""Detect and pickup quest items on ground (gold glow)."""
|
||||
# Quest items have a gold/purple glow around them
|
||||
# Detect with color thresholding
|
||||
...
|
||||
```
|
||||
|
||||
## 5. Quest Combat
|
||||
|
||||
Most quests involve fighting a boss or clearing a path. Botty already has combat logic.
|
||||
|
||||
```python
|
||||
# quest/quest_combat.py
|
||||
|
||||
from char.i_char import IChar
|
||||
from pather import Pather
|
||||
|
||||
def kill_boss(boss_name: str, atk_len: float, char: IChar, pather: Pather):
|
||||
"""
|
||||
Navigate to boss, fight until dead.
|
||||
Reuses botty's existing kill_* methods from run/*.py
|
||||
"""
|
||||
# 1. Find boss on screen
|
||||
# 2. Move to boss position
|
||||
# 3. Attack until dead (same as run/trav.py)
|
||||
# 4. Return to town
|
||||
...
|
||||
|
||||
def clear_room(char: IChar, pather: Pather):
|
||||
"""Clear a room of monsters (for quests requiring room clearance)."""
|
||||
# Same logic as pather.follow_path() but with combat
|
||||
...
|
||||
```
|
||||
|
||||
## 6. Integration with Bot State Machine
|
||||
|
||||
The existing bot uses a state machine (transitions library). We add quest states.
|
||||
|
||||
```python
|
||||
# bot.py changes
|
||||
|
||||
# Add quest states to the state machine
|
||||
self._states = self._states + [
|
||||
'quest', 'quest_act_transition'
|
||||
]
|
||||
|
||||
# Add quest transitions
|
||||
self._transitions = self._transitions + [
|
||||
{'trigger': 'run_quest', 'source': 'town', 'dest': 'quest', 'before': 'on_run_quest'},
|
||||
{'trigger': 'end_quest', 'source': 'quest', 'dest': 'town', 'before': 'on_end_quest'},
|
||||
{'trigger': 'act_transition', 'source': 'town', 'dest': 'quest_act_transition',
|
||||
'before': 'on_act_transition'},
|
||||
]
|
||||
|
||||
def on_run_quest(self):
|
||||
"""Start next quest."""
|
||||
from quest.quest_manager import QuestManager
|
||||
qm = QuestManager()
|
||||
nxt = qm.next_quest()
|
||||
if nxt:
|
||||
act, qn = nxt
|
||||
quest_func = self._get_quest_function(act, qn)
|
||||
quest_func()
|
||||
qm.mark_complete(act, qn)
|
||||
qm.save()
|
||||
|
||||
def on_end_quest(self):
|
||||
"""After a quest completes, check if act is done."""
|
||||
qm = QuestManager()
|
||||
if qm.is_act_complete(self.current_act):
|
||||
self.trigger_or_stop('act_transition')
|
||||
else:
|
||||
self.trigger_or_stop('run_quest')
|
||||
|
||||
def on_act_transition(self):
|
||||
"""Transition to next act (talk to NPC, unlock next act)."""
|
||||
qm = QuestManager()
|
||||
qm.advance_to_next_act()
|
||||
# ... handle act transition logic
|
||||
self.trigger_or_stop('start_from_town')
|
||||
```
|
||||
|
||||
## 7. Configuration
|
||||
|
||||
Add to `params.ini`:
|
||||
|
||||
```ini
|
||||
[quest]
|
||||
; Enable quest mode (disables farming runs until quests are done)
|
||||
enabled = 1
|
||||
|
||||
; Skip quests that are too dangerous for your character level
|
||||
; 0 = run all quests, 50 = skip quests below level 50
|
||||
min_level = 0
|
||||
|
||||
; Auto-stash quest items between acts
|
||||
auto_stash_quest_items = 1
|
||||
|
||||
; Continue farming after all quests are done
|
||||
farm_after_quest = 1
|
||||
```
|
||||
|
||||
## 8. Implementation Order
|
||||
|
||||
### Phase 1: Foundation (Quest Manager + NPC Interaction)
|
||||
- quest_manager.py with state tracking
|
||||
- quest_npc.py for dialogue interaction
|
||||
- quest_state.py for persistence
|
||||
|
||||
### Phase 2: Act 1 (6 quests)
|
||||
- Q1: Search for the Smith (kill Rats, talk to Charsi)
|
||||
- Q2: Tools of the Trade (get items from Andariel's lair)
|
||||
- Q3: Sacrifice (kill Skeletons at Crypt)
|
||||
- Q4: The Summoner (kill Summoner in Catacombs)
|
||||
- Q5: The Shepherd (kill Skeleton King + Gargothon)
|
||||
- Q6: The Fallen Angel (kill Andariel)
|
||||
|
||||
### Phase 3: Act 2 (7 quests)
|
||||
- Q1: Radament
|
||||
- Q2: The Horadric Staff (combine Cube + Scroll)
|
||||
- Q3: Tyrael's Breath (clear Sewers, talk to Alkor)
|
||||
- Q4: Secrets (activate 4 stone tablets)
|
||||
- Q5: The Summoner (kill Duriel)
|
||||
- Q6: The Seven Tombs (kill all 7 tomb bosses)
|
||||
- Q7: The Fallen Angel (kill Duriel again)
|
||||
|
||||
### Phase 4: Act 3 (5 quests)
|
||||
- Q1: The Forgotten Tower
|
||||
- Q2: The Quest for the Horizon (Cain)
|
||||
- Q3: The Hellforge
|
||||
- Q4: The Hellgate
|
||||
- Q5: The Prime Evil (Mephisto)
|
||||
|
||||
### Phase 5: Act 4 (3 quests)
|
||||
- Q1: The Fallen Angel (Harumony)
|
||||
- Q2: The Fallen Angel (Tal Rasha's Will)
|
||||
- Q3: The Prime Evil (Diablo)
|
||||
|
||||
### Phase 6: Act 5 (3 quests)
|
||||
- Q1: The Fallen Angel (Ancient's Battle Order)
|
||||
- Q2: The Quest for the Horizon (Cain)
|
||||
- Q3: The Prime Evil (Baal)
|
||||
|
||||
## 9. Testing Strategy
|
||||
|
||||
1. Test each quest independently in singleplayer
|
||||
2. Test quest-to-quest transitions
|
||||
3. Test act transitions
|
||||
4. Test recovery from death during quest
|
||||
5. Test with different character builds
|
||||
|
||||
## 10. Challenges
|
||||
|
||||
- **NPC dialogue**: D2R has branching dialogue. Need to handle all paths.
|
||||
- **Quest items**: Some quests require carrying specific items (keys, weapons).
|
||||
- **Room secrets**: Act 2 Q4 requires finding hidden passages.
|
||||
- **Multi-stage quests**: Some quests span multiple areas (Act 3 Q2-4).
|
||||
- **Character level**: Quests are designed for specific levels. A level 90 character kills everything too fast/slow.
|
||||
- **Anti-cheat**: Questing is more visible to anti-cheat than farming. Need stealth settings.
|
||||
@@ -0,0 +1,246 @@
|
||||
"""
|
||||
Quest screenshot capture tool.
|
||||
Guides you through capturing specific game screens needed for building
|
||||
the quest system.
|
||||
|
||||
Usage:
|
||||
1. Launch D2R, create/select your character
|
||||
2. Run: python quest_screenshot_tool.py
|
||||
3. Follow the prompts
|
||||
|
||||
Screenshots saved to screenshots/quest/
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import numpy as np
|
||||
import cv2
|
||||
from datetime import datetime
|
||||
|
||||
# Fix tesserocr DLL loading
|
||||
if sys.platform == "win32":
|
||||
_conda_dll_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "conda_env", "Library", "bin")
|
||||
if not os.path.isdir(_conda_dll_dir):
|
||||
_conda_dll_dir = os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "Library", "bin")
|
||||
if os.path.isdir(_conda_dll_dir):
|
||||
os.add_dll_directory(_conda_dll_dir)
|
||||
|
||||
from screen import find_and_set_window_position, get_offset_state, grab as screen_grab
|
||||
|
||||
QUEST_DIR = "screenshots/quest"
|
||||
os.makedirs(QUEST_DIR, exist_ok=True)
|
||||
|
||||
|
||||
def capture():
|
||||
"""Capture the D2R window using botty's grab()."""
|
||||
find_and_set_window_position()
|
||||
if not get_offset_state():
|
||||
print("[ERROR] Could not find D2R window. Is D2R running and visible?")
|
||||
return None
|
||||
return screen_grab(force_new=True)
|
||||
|
||||
|
||||
def save(img, name):
|
||||
"""Save with timestamp prefix."""
|
||||
ts = datetime.now().strftime("%H%M%S")
|
||||
path = os.path.join(QUEST_DIR, f"{ts}_{name}.png")
|
||||
cv2.imwrite(path, img)
|
||||
abs_path = os.path.abspath(path)
|
||||
print(f" Saved: {abs_path}")
|
||||
return abs_path
|
||||
|
||||
|
||||
STEPS = [
|
||||
{
|
||||
"name": "act1_town_overview",
|
||||
"instructions": (
|
||||
"\nSTEP 1: Act 1 Town Overview\n"
|
||||
"----------------------------------------\n"
|
||||
"1. Stand in the middle of Act 1 town (New Tristram)\n"
|
||||
"2. Make sure NO menus are open (close inventory, skills, etc.)\n"
|
||||
"3. Position your character so all NPCs are visible\n"
|
||||
"Press ENTER when the screen shows the full town...\n"
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "akara_dialogue_first",
|
||||
"instructions": (
|
||||
"\nSTEP 2: Akara - First Dialogue Screen\n"
|
||||
"----------------------------------------\n"
|
||||
"1. Walk up to Akara and LEFT-CLICK her\n"
|
||||
"2. The dialogue box should appear with options\n"
|
||||
"3. DO NOT click any option - just show this screen\n"
|
||||
"Press ENTER when the first dialogue is visible...\n"
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "akara_dialogue_second",
|
||||
"instructions": (
|
||||
"\nSTEP 3: Akara - Second Dialogue Screen\n"
|
||||
"----------------------------------------\n"
|
||||
"1. Click the FIRST dialogue option (usually the quest-related one)\n"
|
||||
"2. The next set of options should appear\n"
|
||||
"3. This shows the quest dialogue choices\n"
|
||||
"4. DO NOT click any option - just show this screen\n"
|
||||
"Press ENTER when the second dialogue is visible...\n"
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "akara_quest_given",
|
||||
"instructions": (
|
||||
"\nSTEP 4: Quest Given Notification\n"
|
||||
"----------------------------------------\n"
|
||||
"1. Click the quest-related option to accept the quest\n"
|
||||
"2. After the dialogue completes, press ESC to close it\n"
|
||||
"3. Show the screen with the quest notification/text\n"
|
||||
" (a message should appear on screen about the quest)\n"
|
||||
"Press ENTER when you see the quest notification...\n"
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "quest_log_open",
|
||||
"instructions": (
|
||||
"\nSTEP 5: Quest Log\n"
|
||||
"----------------------------------------\n"
|
||||
"1. Press 'O' to open the Quest Log\n"
|
||||
"2. The quest log panel should be visible\n"
|
||||
"3. Show the full quest log\n"
|
||||
"Press ENTER when the quest log is open...\n"
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "charsi_dialogue",
|
||||
"instructions": (
|
||||
"\nSTEP 6: Charsi NPC Dialogue\n"
|
||||
"----------------------------------------\n"
|
||||
"1. Walk up to Charsi\n"
|
||||
"2. LEFT-CLICK her to open dialogue\n"
|
||||
"3. Show the first dialogue screen\n"
|
||||
"Press ENTER when Charsi's dialogue is open...\n"
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "kashya_dialogue",
|
||||
"instructions": (
|
||||
"\nSTEP 7: Kashya NPC Dialogue\n"
|
||||
"----------------------------------------\n"
|
||||
"1. Walk up to Kashya (the skill teacher)\n"
|
||||
"2. LEFT-CLICK her to open dialogue\n"
|
||||
"3. Show the first dialogue screen\n"
|
||||
"Press ENTER when Kashya's dialogue is open...\n"
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "sewer_entrance",
|
||||
"instructions": (
|
||||
"\nSTEP 8: Sewer / Rat Area Entrance\n"
|
||||
"----------------------------------------\n"
|
||||
"1. Go to the sewer entrance (south of town)\n"
|
||||
"2. Stand near the entrance looking into the rat area\n"
|
||||
"3. This is for the first quest (kill rats)\n"
|
||||
"Press ENTER when you can see the rat area...\n"
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "item_on_ground",
|
||||
"instructions": (
|
||||
"\nSTEP 9: Item on the Ground\n"
|
||||
"----------------------------------------\n"
|
||||
"1. Kill some rats in the sewer\n"
|
||||
"2. If a quest item drops (gold glow), show it on the ground\n"
|
||||
"3. If no quest item drops, show ANY item on the ground\n"
|
||||
"4. The item name tooltip should be visible\n"
|
||||
"Press ENTER when an item is visible on the ground...\n"
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "inventory_with_item",
|
||||
"instructions": (
|
||||
"\nSTEP 10: Inventory with Item Tooltip\n"
|
||||
"----------------------------------------\n"
|
||||
"1. Pick up the item\n"
|
||||
"2. Press 'I' to open inventory\n"
|
||||
"3. Hover over the item to show its tooltip\n"
|
||||
"4. Show the tooltip with the item name visible\n"
|
||||
"Press ENTER when the item tooltip is visible...\n"
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "dialogue_box_full",
|
||||
"instructions": (
|
||||
"\nSTEP 11: Full Dialogue Box (Any NPC)\n"
|
||||
"----------------------------------------\n"
|
||||
"1. Talk to ANY NPC\n"
|
||||
"2. Get to a screen with 3+ dialogue options\n"
|
||||
"3. Show the full dialogue box with all options visible\n"
|
||||
"4. This helps us measure the dialogue button positions\n"
|
||||
"Press ENTER when a dialogue with multiple options is visible...\n"
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "game_start_menu",
|
||||
"instructions": (
|
||||
"\nSTEP 12: Game Start / Difficulty Selection\n"
|
||||
"----------------------------------------\n"
|
||||
"1. Save & Exit to return to hero selection\n"
|
||||
"2. Click Play (or let botty do it)\n"
|
||||
"3. Show the difficulty selection screen\n"
|
||||
" (Normal/Nightmare/Hell buttons)\n"
|
||||
"Press ENTER when the difficulty screen is visible...\n"
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def run():
|
||||
print("=" * 60)
|
||||
print(" Botty Quest Screenshot Tool")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print("Make sure D2R is running and visible on screen.")
|
||||
print("You'll be guided through capturing each needed screen.")
|
||||
print()
|
||||
print("Press ENTER to start...")
|
||||
input()
|
||||
|
||||
captured = []
|
||||
failed = []
|
||||
|
||||
for i, step in enumerate(STEPS, 1):
|
||||
print()
|
||||
print(step["instructions"])
|
||||
|
||||
try:
|
||||
input() # wait for user
|
||||
img = capture()
|
||||
if img is not None:
|
||||
path = save(img, step["name"])
|
||||
captured.append((step["name"], path))
|
||||
print(f" [OK] {step['name']}")
|
||||
else:
|
||||
print(f" [FAIL] Could not capture for {step['name']}")
|
||||
failed.append(step["name"])
|
||||
except KeyboardInterrupt:
|
||||
print("\n[STOPPED]")
|
||||
break
|
||||
|
||||
# Summary
|
||||
print()
|
||||
print("=" * 60)
|
||||
print(" Capture Summary")
|
||||
print("=" * 60)
|
||||
print(f" Captured: {len(captured)}/{len(STEPS)}")
|
||||
for name, path in captured:
|
||||
print(f" [OK] {name}")
|
||||
if failed:
|
||||
print(f" Failed: {len(failed)}")
|
||||
for name in failed:
|
||||
print(f" [XX] {name}")
|
||||
print()
|
||||
print(f"All screenshots in: {os.path.abspath(QUEST_DIR)}")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -0,0 +1,25 @@
|
||||
# Botty Improvements Implementation Plan
|
||||
|
||||
## Phase 1: Key Auto-Detection (Issue #940/#905) [DONE]
|
||||
- `src/utils/key_detector.py` reads D2R .key/.keyo files
|
||||
- Auto-fills empty char section hotkeys. Wired into config.py load_data()
|
||||
- Key normalization: left alt ~ alt, left shift ~ shift
|
||||
- Test: `tools/test_key_detector.py` and `test/test_key_detector.py` (all pass)
|
||||
|
||||
## Phase 2: Target Detection False Positives (Issues #959/#964) [DONE]
|
||||
- Added aspect ratio filtering in `_add_markers()`
|
||||
- Rejects health bars (w/h > 3.0) and immune text (w/h < 0.5)
|
||||
- `TARGET_ASPECT_MIN = 0.5`, `TARGET_ASPECT_MAX = 3.0`
|
||||
|
||||
## Phase 3: Pickit Timing (Issue #939) [DONE]
|
||||
- Added 200-300ms wait after `_yoink_item` pickup
|
||||
- Prevents bot teleporting before item grab animation completes
|
||||
|
||||
## Phase 4: Hardcore Chicken Loop (Issue #942) [DONE]
|
||||
- Added `hardcore` config flag (default 0)
|
||||
- On HC death: exits safely instead of infinite restart loop
|
||||
- Sends discord message if enabled
|
||||
|
||||
## Phase 5: Parallel Template Search (Issue #848) [PENDING]
|
||||
## Phase 6: Async Mouse Moves (Issue #955) [PENDING]
|
||||
## Phase 7: Auto-Label NPCs (Issue #950) [PENDING]
|
||||
@@ -0,0 +1,56 @@
|
||||
# ==========================================
|
||||
# botty - Pixelbot for Diablo 2 Resurrected
|
||||
# requirements.txt
|
||||
# ==========================================
|
||||
# Pin to versions that match the existing conda environment (environment.yml).
|
||||
# Run `pip install -r requirements.txt` inside the `botty` conda env.
|
||||
# For tesserocr, install the pre-built wheel instead:
|
||||
# pip install dependencies/tesserocr-2.5.2-cp310-cp310-win_amd64.whl
|
||||
# ==========================================
|
||||
|
||||
# --- Core runtime ---
|
||||
numpy==1.26.4
|
||||
opencv-python==4.5.5.64
|
||||
mss==7.0.1
|
||||
pillow
|
||||
pywin32
|
||||
|
||||
# --- Game input & control ---
|
||||
keyboard
|
||||
mouse
|
||||
pytweening
|
||||
|
||||
# --- State machine & logic ---
|
||||
transitions
|
||||
rapidfuzz==2.15.1
|
||||
pyparsing
|
||||
parse
|
||||
dataclasses-json
|
||||
|
||||
# --- UI & display ---
|
||||
colorama
|
||||
beautifultable
|
||||
|
||||
# --- Networking & messaging ---
|
||||
requests
|
||||
discord.py
|
||||
|
||||
# --- System & utilities ---
|
||||
psutil
|
||||
cryptography
|
||||
typing_extensions
|
||||
graphviz
|
||||
|
||||
# --- OCR (requires tesseract + leptonica from conda-forge first) ---
|
||||
# tesserocr -- install via wheel:
|
||||
# pip install dependencies/tesserocr-2.5.2-cp310-cp310-win_amd64.whl
|
||||
|
||||
# --- Dev / testing ---
|
||||
pytest
|
||||
pytest-env
|
||||
pytest-pythonpath
|
||||
pytest-mock
|
||||
coverage
|
||||
|
||||
# --- Packaging ---
|
||||
pyinstaller
|
||||
@@ -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,8 @@
|
||||
@echo off
|
||||
echo === D2R Quick Capture ===
|
||||
echo Run this, D2R must be visible
|
||||
echo Press ENTER when D2R is ready...
|
||||
pause >nul
|
||||
|
||||
call C:\Users\alex\miniforge3\condabin\condactivate.bat botty
|
||||
C:\Users\alex\miniforge3\envs\botty\python.exe C:\Users\alex\Downloads\my-botty\asset_extractor.py
|
||||
@@ -0,0 +1,19 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
:: Detect which conda we have and activate the botty env
|
||||
set "PYTHON="
|
||||
if exist "C:\Users\%USERNAME%\miniforge3\envs\botty\python.exe" set "PYTHON=C:\Users\%USERNAME%\miniforge3\envs\botty\python.exe"
|
||||
if exist "C:\Users\%USERNAME%\miniconda3\envs\botty\python.exe" set "PYTHON=C:\Users\%USERNAME%\miniconda3\envs\botty\python.exe"
|
||||
if exist "C:\Users\%USERNAME%\anaconda3\envs\botty\python.exe" set "PYTHON=C:\Users\%USERNAME%\anaconda3\envs\botty\python.exe"
|
||||
|
||||
if "%PYTHON%"=="" (
|
||||
echo ERROR: Could not find botty conda environment.
|
||||
echo Create it first: conda env create -f environment.yml
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Launching Botty ...
|
||||
%PYTHON% src\main.py
|
||||
pause
|
||||
@@ -0,0 +1,11 @@
|
||||
@echo off
|
||||
setlocal
|
||||
set "PYTHON=C:\Users\%USERNAME%\miniforge3\envs\botty\python.exe"
|
||||
if exist "%PYTHON%" (
|
||||
cd /d "%~dp0"
|
||||
%PYTHON% quest_debug.py
|
||||
) else (
|
||||
echo ERROR: Could not find botty Python.
|
||||
echo Run: conda activate botty first.
|
||||
pause
|
||||
)
|
||||
@@ -0,0 +1,144 @@
|
||||
"""
|
||||
Screenshot capture tool for botty development.
|
||||
Takes screenshots from the D2R window and saves them with timestamps.
|
||||
Usage:
|
||||
python screenshot_tool.py # press keys to capture
|
||||
python screenshot_tool.py --auto # auto-capture every 2 seconds
|
||||
Keys:
|
||||
F1 - capture current screen
|
||||
F2 - capture and open in image viewer
|
||||
F3 - capture cropped region (center of screen)
|
||||
F11 - toggle auto-capture mode
|
||||
F12 - exit
|
||||
Screenshots saved to screenshots/
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from input_layer import keyboard
|
||||
import numpy as np
|
||||
import cv2
|
||||
from datetime import datetime
|
||||
|
||||
# Fix tesserocr DLL loading (same as main.py)
|
||||
if sys.platform == "win32":
|
||||
_conda_dll_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "conda_env", "Library", "bin")
|
||||
if not os.path.isdir(_conda_dll_dir):
|
||||
_conda_dll_dir = os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "Library", "bin")
|
||||
if os.path.isdir(_conda_dll_dir):
|
||||
os.add_dll_directory(_conda_dll_dir)
|
||||
|
||||
from mss import mss
|
||||
from screen import find_and_set_window_position, get_offset_state, grab as screen_grab
|
||||
|
||||
SCREENSHOT_DIR = "screenshots"
|
||||
os.makedirs(SCREENSHOT_DIR, exist_ok=True)
|
||||
|
||||
|
||||
def capture_full():
|
||||
"""Capture the D2R window using botty's screen.grab()."""
|
||||
# Find the window first
|
||||
find_and_set_window_position()
|
||||
if not get_offset_state():
|
||||
print("[ERROR] Could not find D2R window. Is D2R running and visible?")
|
||||
return None
|
||||
return screen_grab(force_new=True)
|
||||
|
||||
|
||||
def capture_center_crop(crop_size=(640, 360)):
|
||||
"""Capture the center of the D2R window."""
|
||||
img = capture_full()
|
||||
if img is None:
|
||||
return None
|
||||
h, w = img.shape[:2]
|
||||
cw, ch = crop_size
|
||||
x1 = (w - cw) // 2
|
||||
y1 = (h - ch) // 2
|
||||
return img[y1:y1+ch, x1:x1+cw]
|
||||
|
||||
|
||||
def save_screenshot(img, label=""):
|
||||
"""Save screenshot with timestamp."""
|
||||
if img is None:
|
||||
return None
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
name = f"screenshot_{ts}"
|
||||
if label:
|
||||
name += f"_{label}"
|
||||
name += ".png"
|
||||
path = os.path.join(SCREENSHOT_DIR, name)
|
||||
cv2.imwrite(path, img)
|
||||
abs_path = os.path.abspath(path)
|
||||
print(f"[SAVED] {abs_path}")
|
||||
return abs_path
|
||||
|
||||
|
||||
def show_and_save(img):
|
||||
"""Save and open screenshot in default viewer."""
|
||||
if img is None:
|
||||
return
|
||||
path = save_screenshot(img, "view")
|
||||
if path and sys.platform == "win32":
|
||||
os.startfile(path)
|
||||
|
||||
|
||||
def run_interactive():
|
||||
"""Interactive screenshot mode - press F1 to capture."""
|
||||
print(f"""
|
||||
=== Botty Screenshot Tool ===
|
||||
D2R must be running and visible.
|
||||
|
||||
F1 - capture full D2R window
|
||||
F2 - capture full + open in default image viewer
|
||||
F3 - capture center crop (640x360)
|
||||
F11 - toggle auto-capture (every 2s)
|
||||
F12 - exit
|
||||
|
||||
Screenshots saved to: {os.path.abspath(SCREENSHOT_DIR)}
|
||||
""")
|
||||
|
||||
keyboard.add_hotkey('f1', lambda: save_screenshot(capture_full()))
|
||||
keyboard.add_hotkey('f2', lambda: show_and_save(capture_full()))
|
||||
keyboard.add_hotkey('f3', lambda: save_screenshot(capture_center_crop(), "center"))
|
||||
|
||||
auto_mode = False
|
||||
auto_running = False
|
||||
|
||||
def toggle_auto():
|
||||
nonlocal auto_mode, auto_running
|
||||
auto_mode = not auto_mode
|
||||
if auto_mode:
|
||||
print("[AUTO CAPTURE ON] - capturing every 2 seconds. Press F11 to stop.")
|
||||
auto_running = True
|
||||
t = _start_auto()
|
||||
else:
|
||||
print("[AUTO CAPTURE OFF]")
|
||||
auto_running = False
|
||||
|
||||
def _start_auto():
|
||||
import threading
|
||||
def loop():
|
||||
while auto_running:
|
||||
save_screenshot(capture_full(), "auto")
|
||||
time.sleep(2)
|
||||
threading.Thread(target=loop, daemon=True).start()
|
||||
|
||||
keyboard.add_hotkey('f11', toggle_auto)
|
||||
keyboard.add_hotkey('f12', lambda: sys.exit(0))
|
||||
|
||||
print("Ready. Press F1 to take a screenshot, F12 to exit.")
|
||||
keyboard.wait()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if "--auto" in sys.argv:
|
||||
print("[AUTO MODE] Capturing every 2 seconds. Press Ctrl+C to stop.")
|
||||
try:
|
||||
while True:
|
||||
save_screenshot(capture_full(), "auto")
|
||||
time.sleep(2)
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped.")
|
||||
else:
|
||||
run_interactive()
|
||||
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 162 KiB |
@@ -0,0 +1,8 @@
|
||||
# Fix: On Windows, Python 3.8+ requires os.add_dll_directory for conda-forge DLLs
|
||||
# that tesserocr depends on (tesseract51.dll, leptonica-1.78.0.dll).
|
||||
# This must run BEFORE any import that might trigger tesserocr loading.
|
||||
import os, sys
|
||||
if sys.platform == "win32":
|
||||
_conda_dll_dir = os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "Library", "bin")
|
||||
if os.path.isdir(_conda_dll_dir):
|
||||
os.add_dll_directory(_conda_dll_dir)
|
||||
@@ -0,0 +1,22 @@
|
||||
from colorama import init, Fore
|
||||
|
||||
init()
|
||||
|
||||
class BNipError(Exception):
|
||||
def __init__(self, error_code: str | int, error_message: str, additional_info: str):
|
||||
self.error_code = error_code
|
||||
self.error_message = error_message
|
||||
self.additional_info = additional_info
|
||||
|
||||
def __str__(self):
|
||||
self.additional_info = ":" + self.additional_info if self.additional_info else ""
|
||||
return f"{Fore.RED}{self.error_code}:{Fore.CYAN}{self.error_message}{Fore.YELLOW}{self.additional_info}{Fore.RESET}"
|
||||
|
||||
|
||||
class BNipSyntaxError(BNipError):
|
||||
def __init__(self, error_code: str | int, error_message: str, expression: str):
|
||||
super().__init__(error_code, error_message, expression.strip())
|
||||
|
||||
class BNipWarning(BNipError):
|
||||
def __init__(self, error_code: str | int, error_message: str, expression: str):
|
||||
super().__init__(error_code, error_message, expression.strip())
|
||||
@@ -1304,4 +1304,43 @@ 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"
|
||||
@@ -95,6 +95,7 @@ NTIPAliasStat['sorceressskills']='83,1'
|
||||
NTIPAliasStat['barbarianskills']='83,4'
|
||||
NTIPAliasStat['druidskills']='83,5'
|
||||
NTIPAliasStat['assassinskills']='83,6'
|
||||
NTIPAliasStat['warlockskills']='83,7'
|
||||
NTIPAliasStat['itemaddamazonskills']='83,0'
|
||||
NTIPAliasStat['amaskills']='83,0'
|
||||
NTIPAliasStat['itemaddamaskills']='83,0'
|
||||
@@ -128,6 +129,11 @@ NTIPAliasStat['assskills']='83,6'
|
||||
NTIPAliasStat['itemaddassskills']='83,6'
|
||||
NTIPAliasStat['sinskills']='83,6'
|
||||
NTIPAliasStat['itemaddsinskills']='83,6'
|
||||
NTIPAliasStat['itemaddwarlockskills']='83,7'
|
||||
NTIPAliasStat['warskills']='83,7'
|
||||
NTIPAliasStat['itemaddwarskills']='83,7'
|
||||
NTIPAliasStat['warskills']='83,7'
|
||||
NTIPAliasStat['itemaddwarskills']='83,7'
|
||||
NTIPAliasStat['unsentparam1']='84'
|
||||
NTIPAliasStat['itemaddexperience']='85'
|
||||
NTIPAliasStat['itemhealafterkill']='86'
|
||||
@@ -353,6 +359,8 @@ NTIPAliasStat['plusskillbladeshield']='97,277'
|
||||
NTIPAliasStat['plusskillvenom']='97,278'
|
||||
NTIPAliasStat['plusskillshadowmaster']='97,279'
|
||||
NTIPAliasStat['plusskillphoenixstrike']='97,280'
|
||||
NTIPAliasStat['plusskillabyss']='97,281'
|
||||
NTIPAliasStat['plusskillenhancedentropy']='97,282'
|
||||
NTIPAliasStat['itemnonclassskill']='97'
|
||||
NTIPAliasStat['state']='98'
|
||||
NTIPAliasStat['itemfastergethitrate']='99'
|
||||
@@ -572,6 +580,8 @@ NTIPAliasStat['skillbladeshield']='107,277'
|
||||
NTIPAliasStat['skillvenom']='107,278'
|
||||
NTIPAliasStat['skillshadowmaster']='107,279'
|
||||
NTIPAliasStat['skillphoenixstrike']='107,280'
|
||||
NTIPAliasStat['skillabyss']='107,281'
|
||||
NTIPAliasStat['skillenhancedentropy']='107,282'
|
||||
NTIPAliasStat['itemsingleskill']='107'
|
||||
NTIPAliasStat['itemrestinpeace']='108'
|
||||
NTIPAliasStat['curseresistance']='109'
|
||||
@@ -810,6 +820,30 @@ NTIPAliasStat['itemaddassmartialartsskilltab']='188,20'
|
||||
NTIPAliasStat['assmartialartsskilltab']='188,20'
|
||||
NTIPAliasStat['itemaddsinmartialartsskilltab']='188,20'
|
||||
NTIPAliasStat['sinmartialartsskilltab']='188,20'
|
||||
NTIPAliasStat['itemaddchaosskilltab']='188,21'
|
||||
NTIPAliasStat['chaosskilltab']='188,21'
|
||||
NTIPAliasStat['itemaddwarlockchaosskilltab']='188,21'
|
||||
NTIPAliasStat['warlockchaosskilltab']='188,21'
|
||||
NTIPAliasStat['itemaddwarchaosskilltab']='188,21'
|
||||
NTIPAliasStat['warchaosskilltab']='188,21'
|
||||
NTIPAliasStat['itemaddlockchaosskilltab']='188,21'
|
||||
NTIPAliasStat['lockchaosskilltab']='188,21'
|
||||
NTIPAliasStat['itemaddeldritchskilltab']='188,22'
|
||||
NTIPAliasStat['eldritchskilltab']='188,22'
|
||||
NTIPAliasStat['itemaddwarlockeldritchskilltab']='188,22'
|
||||
NTIPAliasStat['warlockeldritchskilltab']='188,22'
|
||||
NTIPAliasStat['itemaddwareldritchskilltab']='188,22'
|
||||
NTIPAliasStat['wareldritchskilltab']='188,22'
|
||||
NTIPAliasStat['itemaddlockeldritchskilltab']='188,22'
|
||||
NTIPAliasStat['lockeldritchskilltab']='188,22'
|
||||
NTIPAliasStat['itemadddemonskilltab']='188,23'
|
||||
NTIPAliasStat['demonskilltab']='188,23'
|
||||
NTIPAliasStat['itemaddwarlockdemonskilltab']='188,23'
|
||||
NTIPAliasStat['warlockdemonskilltab']='188,23'
|
||||
NTIPAliasStat['itemaddwardemonskilltab']='188,23'
|
||||
NTIPAliasStat['wardemonskilltab']='188,23'
|
||||
NTIPAliasStat['itemaddlockdemonskilltab']='188,23'
|
||||
NTIPAliasStat['lockdemonskilltab']='188,23'
|
||||
NTIPAliasStat['itemaddskilltab']='188'
|
||||
NTIPAliasStat['unused189']='189'
|
||||
NTIPAliasStat['unused190']='190'
|
||||
@@ -1037,6 +1071,7 @@ NTIPAliasStat['skillbladeshieldcharges']='204,277'
|
||||
NTIPAliasStat['skillvenomcharges']='204,278'
|
||||
NTIPAliasStat['skillshadowmastercharges']='204,279'
|
||||
NTIPAliasStat['skillphoenixstrikecharges']='204,280'
|
||||
NTIPAliasStat['skillabysscharges']='204,281'
|
||||
NTIPAliasStat['itemchargedskill']='204'
|
||||
NTIPAliasStat['itemnoconsume']='205'
|
||||
NTIPAliasStat['passivemasterynoconsume']='206'
|
||||
@@ -57,51 +57,51 @@ NTIPAliasType['secondhand']='54'
|
||||
NTIPAliasType['stavesandrods']='55'
|
||||
NTIPAliasType['missile']='56'
|
||||
NTIPAliasType['blunt']='57'
|
||||
NTIPAliasType['expansion']='58'
|
||||
NTIPAliasType['jewel']='59'
|
||||
NTIPAliasType['classspecific']='60'
|
||||
NTIPAliasType['amazonitem']='61'
|
||||
NTIPAliasType['barbarianitem']='62'
|
||||
NTIPAliasType['necromanceritem']='63'
|
||||
NTIPAliasType['paladinitem']='64'
|
||||
NTIPAliasType['sorceressitem']='65'
|
||||
NTIPAliasType['assassinitem']='66'
|
||||
NTIPAliasType['druiditem']='67'
|
||||
NTIPAliasType['handtohand']='68'
|
||||
NTIPAliasType['orb']='69'
|
||||
NTIPAliasType['voodooheads']='70'
|
||||
NTIPAliasType['auricshields']='71'
|
||||
NTIPAliasType['primalhelm']='72'
|
||||
NTIPAliasType['pelt']='73'
|
||||
NTIPAliasType['cloak']='74'
|
||||
NTIPAliasType['rune']='75'
|
||||
NTIPAliasType['circlet']='76'
|
||||
NTIPAliasType['healingpotion']='77'
|
||||
NTIPAliasType['manapotion']='78'
|
||||
NTIPAliasType['rejuvpotion']='79'
|
||||
NTIPAliasType['staminapotion']='80'
|
||||
NTIPAliasType['antidotepotion']='81'
|
||||
NTIPAliasType['thawingpotion']='82'
|
||||
NTIPAliasType['smallcharm']='83'
|
||||
NTIPAliasType['mediumcharm']='84'
|
||||
NTIPAliasType['largecharm']='85'
|
||||
NTIPAliasType['amazonbow']='86'
|
||||
NTIPAliasType['amazonspear']='87'
|
||||
NTIPAliasType['amazonjavelin']='88'
|
||||
NTIPAliasType['assassinclaw']='89'
|
||||
NTIPAliasType['magicbowquiv']='90'
|
||||
NTIPAliasType['magicxbowquiv']='91'
|
||||
NTIPAliasType['chippedgem']='92'
|
||||
NTIPAliasType['flawedgem']='93'
|
||||
NTIPAliasType['standardgem']='94'
|
||||
NTIPAliasType['flawlessgem']='95'
|
||||
NTIPAliasType['perfectgem']='96'
|
||||
NTIPAliasType['amethyst']='97'
|
||||
NTIPAliasType['diamond']='98'
|
||||
NTIPAliasType['emerald']='99'
|
||||
NTIPAliasType['ruby']='100'
|
||||
NTIPAliasType['sapphire']='101'
|
||||
NTIPAliasType['topaz']='102'
|
||||
NTIPAliasType['skull']='103'
|
||||
NTIPAliasType['swordsandknives']='104'
|
||||
NTIPAliasType['spearsandpolearms']='105'
|
||||
NTIPAliasType['jewel']='58'
|
||||
NTIPAliasType['classspecific']='59'
|
||||
NTIPAliasType['amazonitem']='60'
|
||||
NTIPAliasType['barbarianitem']='61'
|
||||
NTIPAliasType['necromanceritem']='62'
|
||||
NTIPAliasType['paladinitem']='63'
|
||||
NTIPAliasType['sorceressitem']='64'
|
||||
NTIPAliasType['assassinitem']='65'
|
||||
NTIPAliasType['druiditem']='66'
|
||||
NTIPAliasType['handtohand']='67'
|
||||
NTIPAliasType['orb']='68'
|
||||
NTIPAliasType['voodooheads']='69'
|
||||
NTIPAliasType['auricshields']='70'
|
||||
NTIPAliasType['primalhelm']='71'
|
||||
NTIPAliasType['pelt']='72'
|
||||
NTIPAliasType['cloak']='73'
|
||||
NTIPAliasType['rune']='74'
|
||||
NTIPAliasType['circlet']='75'
|
||||
NTIPAliasType['healingpotion']='76'
|
||||
NTIPAliasType['manapotion']='77'
|
||||
NTIPAliasType['rejuvpotion']='78'
|
||||
NTIPAliasType['staminapotion']='79'
|
||||
NTIPAliasType['antidotepotion']='80'
|
||||
NTIPAliasType['thawingpotion']='81'
|
||||
NTIPAliasType['smallcharm']='82'
|
||||
NTIPAliasType['mediumcharm']='83'
|
||||
NTIPAliasType['largecharm']='84'
|
||||
NTIPAliasType['amazonbow']='85'
|
||||
NTIPAliasType['amazonspear']='86'
|
||||
NTIPAliasType['amazonjavelin']='87'
|
||||
NTIPAliasType['assassinclaw']='88'
|
||||
NTIPAliasType['magicbowquiv']='89'
|
||||
NTIPAliasType['magicxbowquiv']='90'
|
||||
NTIPAliasType['chippedgem']='91'
|
||||
NTIPAliasType['flawedgem']='92'
|
||||
NTIPAliasType['standardgem']='93'
|
||||
NTIPAliasType['flawlessgem']='94'
|
||||
NTIPAliasType['perfectgem']='95'
|
||||
NTIPAliasType['amethyst']='96'
|
||||
NTIPAliasType['diamond']='97'
|
||||
NTIPAliasType['emerald']='98'
|
||||
NTIPAliasType['ruby']='99'
|
||||
NTIPAliasType['sapphire']='100'
|
||||
NTIPAliasType['topaz']='101'
|
||||
NTIPAliasType['skull']='102'
|
||||
NTIPAliasType['swordsandknives']='103'
|
||||
NTIPAliasType['spearsandpolearms']='104'
|
||||
NTIPAliasType['grimoires']='105'
|
||||
@@ -28,8 +28,7 @@ UniqueAndSetData = {
|
||||
"armet": {
|
||||
"uniques": [
|
||||
[
|
||||
"STEELSHADE",
|
||||
"DARKFEAR"
|
||||
"STEELSHADE"
|
||||
]
|
||||
]
|
||||
},
|
||||
@@ -126,6 +125,20 @@ UniqueAndSetData = {
|
||||
"SPIKETHORN"
|
||||
]
|
||||
]
|
||||
},
|
||||
"blasphemouscompendium": {
|
||||
"uniques": [
|
||||
[
|
||||
"ARSTORBAALOS"
|
||||
]
|
||||
]
|
||||
},
|
||||
"blasphemousgrimoire": {
|
||||
"uniques": [
|
||||
[
|
||||
"ARSALDIABOLOS"
|
||||
]
|
||||
]
|
||||
},
|
||||
"bloodspirit": {
|
||||
"uniques": [
|
||||
@@ -222,6 +235,13 @@ UniqueAndSetData = {
|
||||
]
|
||||
]
|
||||
},
|
||||
"burnttext": {
|
||||
"uniques": [
|
||||
[
|
||||
"MEASUREDWRATH"
|
||||
]
|
||||
]
|
||||
},
|
||||
"cantortrophy": {
|
||||
"sets": [
|
||||
[
|
||||
@@ -879,6 +899,13 @@ UniqueAndSetData = {
|
||||
]
|
||||
]
|
||||
},
|
||||
"mirroredboots": {
|
||||
"sets": [
|
||||
[
|
||||
"HORAZONSLEGACY"
|
||||
]
|
||||
]
|
||||
},
|
||||
"mithrilcoil": {
|
||||
"uniques": [
|
||||
[
|
||||
@@ -905,6 +932,13 @@ UniqueAndSetData = {
|
||||
]
|
||||
]
|
||||
},
|
||||
"occulttome": {
|
||||
"uniques": [
|
||||
[
|
||||
"ARSDULMEPHISTOS"
|
||||
]
|
||||
]
|
||||
},
|
||||
"ogregauntlets": {
|
||||
"uniques": [
|
||||
[
|
||||
@@ -1266,6 +1300,11 @@ UniqueAndSetData = {
|
||||
]
|
||||
},
|
||||
"trollbelt": {
|
||||
"uniques": [
|
||||
[
|
||||
"GHEEDSWAGER"
|
||||
]
|
||||
],
|
||||
"sets": [
|
||||
[
|
||||
"TRANGOULSGIRTH"
|
||||
@@ -1427,7 +1466,13 @@ UniqueAndSetData = {
|
||||
"grandcharm": {
|
||||
"uniques": [
|
||||
[
|
||||
"GHEEDSFORTUNE"
|
||||
"GHEEDSFORTUNE",
|
||||
"BLACKCLEFT",
|
||||
"BONEBREAK",
|
||||
"COLDRUPTURE",
|
||||
"CRACKOFTHEHEAVENS",
|
||||
"FLAMERIFT",
|
||||
"ROTTINGFISSURE"
|
||||
]
|
||||
]
|
||||
},
|
||||
@@ -1457,7 +1502,9 @@ UniqueAndSetData = {
|
||||
"RAVENFROST",
|
||||
"NATURESPEACE",
|
||||
"WISPPROJECTOR",
|
||||
"CARRIONWIND"
|
||||
"CARRIONWIND",
|
||||
"OPALVEIN",
|
||||
"SLING"
|
||||
]
|
||||
],
|
||||
"sets": [
|
||||
@@ -3,34 +3,35 @@
|
||||
"""
|
||||
|
||||
|
||||
from functools import cache
|
||||
import os
|
||||
import re
|
||||
import glob
|
||||
import traceback
|
||||
from logger import Logger
|
||||
from dataclasses import dataclass
|
||||
from nip.lexer import NipSections
|
||||
from nip.tokens import TokenType
|
||||
from nip.transpile import (
|
||||
prepare_nip_expression,
|
||||
transpile_nip_expression,
|
||||
from bnip.lexer import BNipSections
|
||||
from bnip.tokens import TokenType
|
||||
from bnip.transpile import (
|
||||
prepare_bnip_expression,
|
||||
transpile_bnip_expression,
|
||||
get_section_from_tokens,
|
||||
NIPExpression,
|
||||
nip_expressions,
|
||||
load_nip_expression,
|
||||
BNIPExpression,
|
||||
bnip_expressions,
|
||||
load_bnip_expression,
|
||||
)
|
||||
|
||||
# ! The below imports are necessary, they are used within the eval statements. Your text editor probably is not showing them as not in use.
|
||||
from nip.NTIPAliasQuality import NTIPAliasQuality
|
||||
from nip.NTIPAliasClass import NTIPAliasClass
|
||||
from nip.NTIPAliasClassID import NTIPAliasClassID
|
||||
from nip.NTIPAliasFlag import NTIPAliasFlag
|
||||
from nip.NTIPAliasStat import NTIPAliasStat
|
||||
from nip.NTIPAliasType import NTIPAliasType
|
||||
from nip.utils import find_unique_or_set_base
|
||||
|
||||
from bnip.NTIPAliasQuality import NTIPAliasQuality
|
||||
from bnip.NTIPAliasClass import NTIPAliasClass
|
||||
from bnip.NTIPAliasClassID import NTIPAliasClassID
|
||||
from bnip.NTIPAliasFlag import NTIPAliasFlag
|
||||
from bnip.NTIPAliasStat import NTIPAliasStat
|
||||
from bnip.NTIPAliasType import NTIPAliasType
|
||||
from bnip.utils import find_unique_or_set_base
|
||||
|
||||
|
||||
@cache
|
||||
def should_keep(item_data) -> tuple[bool, str]:
|
||||
"""Decides whether or not to keep an item.
|
||||
Args:
|
||||
@@ -41,16 +42,17 @@ def should_keep(item_data) -> tuple[bool, str]:
|
||||
str: The raw expression to use for the keep condition.
|
||||
|
||||
"""
|
||||
for expression in nip_expressions:
|
||||
for expression in bnip_expressions:
|
||||
if eval(expression.transpiled):
|
||||
return True, expression.raw
|
||||
return False, ""
|
||||
|
||||
def _gold_pickup(item_data: dict, expression: NIPExpression) -> bool | None:
|
||||
@cache
|
||||
def _gold_pickup(item_data: dict, expression: BNIPExpression) -> bool | None:
|
||||
res = None
|
||||
for i, token in enumerate(expression.tokens):
|
||||
if (
|
||||
token.type == TokenType.ValueNTIPAliasStat
|
||||
token.type == TokenType.KeywordNTIPAliasStat
|
||||
and token.value == str(NTIPAliasStat["gold"])
|
||||
and "Amount" in item_data
|
||||
and item_data["Amount"] is not None
|
||||
@@ -66,17 +68,18 @@ def _gold_pickup(item_data: dict, expression: NIPExpression) -> bool | None:
|
||||
return res
|
||||
|
||||
|
||||
def _handle_pick_eth_sockets(item_data: dict, expression: NIPExpression) -> tuple[bool, str]:
|
||||
@cache
|
||||
def _handle_pick_eth_sockets(item_data: dict, expression: BNIPExpression) -> tuple[bool, str]:
|
||||
"""Handles the pick condition for eth and sockets.
|
||||
Args:
|
||||
item_data (dict): The item data.
|
||||
expression (NIPExpression): The expression to use.
|
||||
expression (BNIPExpression): The expression to use.
|
||||
Returns:
|
||||
tuple[bool, str]: A tuple containing the following:
|
||||
bool: Whether or not to keep the item.
|
||||
NipExpression: The expression object that was used to evaluate the condition.
|
||||
BNIPExpression: The expression object that was used to evaluate the condition.
|
||||
"""
|
||||
expression_raw = prepare_nip_expression(expression.raw)
|
||||
expression_raw = prepare_bnip_expression(expression.raw)
|
||||
all_tokens = expression.tokens
|
||||
|
||||
tokens_by_section = get_section_from_tokens(all_tokens)
|
||||
@@ -86,7 +89,7 @@ def _handle_pick_eth_sockets(item_data: dict, expression: NIPExpression) -> tupl
|
||||
eth = 0 # * -1 = set to false, 0 = not set, 1 = set to true
|
||||
soc = 0
|
||||
if eth_keyword_present:
|
||||
for i, token in enumerate(tokens := tokens_by_section[NipSections.PROP]):
|
||||
for i, token in enumerate(tokens := tokens_by_section[BNipSections.PROP]):
|
||||
if token.type == TokenType.ValueNTIPAliasFlag and str(token.value).lower() == "ethereal":
|
||||
if tokens[i - 1].value == "==":
|
||||
eth = 1
|
||||
@@ -95,8 +98,8 @@ def _handle_pick_eth_sockets(item_data: dict, expression: NIPExpression) -> tupl
|
||||
break
|
||||
|
||||
if len(tokens_by_section) > 1 and soc_keyword_present:
|
||||
for i, token in enumerate(tokens := tokens_by_section[NipSections.STAT]):
|
||||
if token.type == TokenType.ValueNTIPAliasStat and token.value == str(NTIPAliasStat["sockets"]):
|
||||
for i, token in enumerate(tokens := tokens_by_section[BNipSections.STAT]):
|
||||
if token.type == TokenType.KeywordNTIPAliasStat and token.value == str(NTIPAliasStat["sockets"]):
|
||||
desired_sockets = int(tokens[i + 2].value)
|
||||
if (desired_sockets > 0 and not (desired_sockets == 1 and tokens[i + 1].value == "<")) or (desired_sockets == 0 and tokens[i + 1].value == ">"):
|
||||
soc = 1
|
||||
@@ -125,12 +128,12 @@ def _handle_pick_eth_sockets(item_data: dict, expression: NIPExpression) -> tupl
|
||||
raw = expression.raw.replace("&& [flag]", "[flag]").replace("|| [flag]", "[flag]")
|
||||
raw = re.sub(r"\[flag\] (==|!=)\sethereal", "", raw)
|
||||
# print(f"Modified raw expression: {raw}")
|
||||
pick_eval_expr = transpile_nip_expression(raw.split("#")[0], isPickUpPhase=True)
|
||||
pick_eval_expr = transpile_bnip_expression(raw.split("#")[0], isPickUpPhase=True)
|
||||
# print(f"Modified transpiled expression: {pick_eval_expr}")
|
||||
|
||||
return ignore, pick_eval_expr
|
||||
|
||||
|
||||
@cache
|
||||
def should_pickup(item_data) -> tuple[bool, str]:
|
||||
"""Decides whether or not to keep an item.
|
||||
Args:
|
||||
@@ -144,7 +147,7 @@ def should_pickup(item_data) -> tuple[bool, str]:
|
||||
pick_eval_expr = ""
|
||||
item_is_gold = item_data["BaseItem"]["DisplayName"] == "Gold"
|
||||
|
||||
for expression in nip_expressions:
|
||||
for expression in bnip_expressions:
|
||||
if expression.raw:
|
||||
# check gold
|
||||
if item_is_gold and "[gold]" in expression.raw.lower():
|
||||
@@ -164,7 +167,7 @@ def should_pickup(item_data) -> tuple[bool, str]:
|
||||
|
||||
return False, ""
|
||||
|
||||
|
||||
@cache
|
||||
def should_id(item_data) -> bool:
|
||||
"""Checks if the item should be identified.
|
||||
|
||||
@@ -181,7 +184,7 @@ def should_id(item_data) -> bool:
|
||||
[name] == ring && [quality] == rare -> True
|
||||
[name] == ring && [quality] == rare # [strength] == 5 -> Falsep
|
||||
"""
|
||||
for expression in nip_expressions:
|
||||
for expression in bnip_expressions:
|
||||
if expression and expression.should_id_transpiled:
|
||||
split_expression = expression.raw.split("#")
|
||||
if "[idname]" in expression.raw.lower():
|
||||
@@ -191,21 +194,21 @@ def should_id(item_data) -> bool:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _load_nip_expressions(filepath):
|
||||
def _load_bnip_expressions(filepath):
|
||||
"""
|
||||
Loads the NIP expressions from the file.
|
||||
Loads the BNIP expressions from the file.
|
||||
Args:
|
||||
filepath (str): The path to the file.
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
with open(filepath, "r") as f:
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
for i, line in enumerate(f):
|
||||
line = line.strip()
|
||||
if line == "" or line.startswith("//"): # Empty or comment line
|
||||
continue
|
||||
try:
|
||||
load_nip_expression(line)
|
||||
load_bnip_expression(line)
|
||||
except Exception as e:
|
||||
filepath = filepath.replace("\\", "/")
|
||||
file = filepath.split('/config/')[1]
|
||||
@@ -214,36 +217,39 @@ def _load_nip_expressions(filepath):
|
||||
break
|
||||
|
||||
|
||||
default_nip_file_path = f"{os.getcwd()}/config/default.nip"
|
||||
nip_path = f"{os.getcwd()}/config/nip"
|
||||
glob_nip_path = os.path.join(nip_path, '**', '*.nip')
|
||||
nip_file_paths = glob.glob(glob_nip_path, recursive=True)
|
||||
default_bnip_file_path = f"{os.getcwd()}/config/default.bnip"
|
||||
bnip_path = f"{os.getcwd()}/config/bnip"
|
||||
|
||||
# * Remove all directories or files that are in the .nipignore file from nip_file_paths. (accepts glob patterns)
|
||||
if os.path.isfile(os.path.join(nip_path, '.nipignore')):
|
||||
with open(os.path.join(nip_path, '.nipignore'), "r") as f:
|
||||
glob_bnip_path = os.path.join(bnip_path, '**', '*.bnip')
|
||||
glob_nip_path = os.path.join(bnip_path, '**', '*.nip')
|
||||
|
||||
bnip_file_paths = glob.glob(glob_bnip_path, recursive=True) + glob.glob(glob_nip_path, recursive=True)
|
||||
|
||||
# * Remove all directories or files that are in the .nipignore file from bnip_file_paths. (accepts glob patterns)
|
||||
if os.path.isfile(os.path.join(bnip_path, '.nipignore')):
|
||||
with open(os.path.join(bnip_path, '.nipignore'), "r") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
line = line.replace("/", "\\")
|
||||
remove_files = glob.glob(os.path.join(nip_path, line), recursive=True)
|
||||
remove_files = glob.glob(os.path.join(bnip_path, line), recursive=True)
|
||||
for remove_file in remove_files:
|
||||
if remove_file in nip_file_paths:
|
||||
nip_file_paths.remove(remove_file)
|
||||
if remove_file in bnip_file_paths:
|
||||
bnip_file_paths.remove(remove_file)
|
||||
|
||||
num_files = 0
|
||||
# load all nip expressions
|
||||
if len(nip_file_paths) > 0:
|
||||
num_files = len(nip_file_paths)
|
||||
for nip_file_path in nip_file_paths:
|
||||
_load_nip_expressions(nip_file_path)
|
||||
if len(bnip_file_paths) > 0:
|
||||
num_files = len(bnip_file_paths)
|
||||
for bnip_file_path in bnip_file_paths:
|
||||
_load_bnip_expressions(bnip_file_path)
|
||||
# fallback to default nip file if no custom nip files specified or existing files are excluded
|
||||
else:
|
||||
num_files = 1
|
||||
_load_nip_expressions(default_nip_file_path)
|
||||
Logger.warning("No .nip files in config/nip/, fallback to default.nip")
|
||||
Logger.info(f"Loaded {num_files} nip files with {len(nip_expressions)} total expressions.")
|
||||
_load_bnip_expressions(default_bnip_file_path)
|
||||
Logger.warning("No .bnip files in config/nip/, fallback to default.bnip")
|
||||
Logger.info(f"Loaded {num_files} nip files with {len(bnip_expressions)} total expressions.")
|
||||
|
||||
nip_expressions = sorted(nip_expressions, key=lambda x: len(x.raw))
|
||||
bnip_expressions = sorted(bnip_expressions, key=lambda x: len(x.raw))
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(transpile_nip_expression("[name] == ring && [quality] == rare"))
|
||||
print(transpile_bnip_expression("[name] == ring && [quality] == rare # [strength] == 5"))
|
||||
@@ -1,24 +1,25 @@
|
||||
"""
|
||||
Lexer for NIP expressions.
|
||||
Lexer for BNIP expressions.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from logger import Logger
|
||||
from nip.NTIPAliasQuality import NTIPAliasQuality
|
||||
from nip.NTIPAliasClass import NTIPAliasClass
|
||||
from nip.NTIPAliasClassID import NTIPAliasClassID
|
||||
from nip.NTIPAliasFlag import NTIPAliasFlag
|
||||
from nip.NTIPAliasStat import NTIPAliasStat
|
||||
from nip.NTIPAliasType import NTIPAliasType
|
||||
from nip.tokens import Token, TokenType
|
||||
from bnip.NTIPAliasQuality import NTIPAliasQuality
|
||||
from bnip.NTIPAliasClass import NTIPAliasClass
|
||||
from bnip.NTIPAliasClassID import NTIPAliasClassID
|
||||
from bnip.NTIPAliasFlag import NTIPAliasFlag
|
||||
from bnip.NTIPAliasStat import NTIPAliasStat
|
||||
from bnip.NTIPAliasType import NTIPAliasType
|
||||
from bnip.tokens import Token, TokenType
|
||||
|
||||
from bnip.BNipExceptions import BNipSyntaxError
|
||||
|
||||
from enum import Enum
|
||||
import re
|
||||
from colorama import init, Fore
|
||||
from rapidfuzz.string_metric import levenshtein
|
||||
|
||||
|
||||
init()
|
||||
try:
|
||||
from rapidfuzz.string_metric import levenshtein
|
||||
except ImportError:
|
||||
from rapidfuzz.distance import Levenshtein as levenshtein
|
||||
|
||||
WHITESPACE = " \t\n\r\v\f"
|
||||
DIGITS = "0123456789.-" # ! Put % back in here when ready to use percentages.
|
||||
@@ -26,37 +27,24 @@ SYMBOLS = [">", "=> ", "<", "<=", "=", "!", "", "", ",", "&", "|", "#"]
|
||||
MATH_SYMBOLS = ["(", ")", "^", "*", "/", "\\", "+", "-"]
|
||||
CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'"
|
||||
|
||||
|
||||
class NipSections(Enum):
|
||||
class BNipSections(Enum):
|
||||
PROP = 1
|
||||
STAT = 2
|
||||
MAXQUANTITY = 3
|
||||
|
||||
|
||||
class NipSyntaxError(Exception):
|
||||
def __init__(self, ecode: str|int = 0, message: str = '', expression: str = ''):
|
||||
self.message = message
|
||||
self.type = type
|
||||
self.ecode = ecode
|
||||
self.expression = expression
|
||||
|
||||
def __str__(self):
|
||||
return f"{Fore.RED}{self.ecode}:{Fore.CYAN}{self.message}:{Fore.YELLOW}{self.expression.strip()}{Fore.WHITE}"
|
||||
|
||||
|
||||
class Lexer:
|
||||
def __init__(self):
|
||||
self.current_section: NipSections = NipSections.PROP
|
||||
self.current_section: BNipSections = BNipSections.PROP
|
||||
self.current_token: str | None = ""
|
||||
self.text_i: int = -1
|
||||
self.tokens: list[Token] = []
|
||||
|
||||
|
||||
def _increment_section(self):
|
||||
if self.current_section == NipSections.PROP:
|
||||
self.current_section = NipSections.STAT
|
||||
elif self.current_section == NipSections.STAT:
|
||||
self.current_section = NipSections.MAXQUANTITY
|
||||
if self.current_section == BNipSections.PROP:
|
||||
self.current_section = BNipSections.STAT
|
||||
elif self.current_section == BNipSections.STAT:
|
||||
self.current_section = BNipSections.MAXQUANTITY
|
||||
|
||||
|
||||
def _get_text(self):
|
||||
@@ -77,32 +65,42 @@ class Lexer:
|
||||
self.current_token = None
|
||||
|
||||
|
||||
def create_tokens(self, nip_expression: str, starting_section: NipSections = NipSections.PROP):
|
||||
"""Creates token from a nip expression string
|
||||
def create_tokens(self, bnip_expression: str, starting_section: BNipSections = BNipSections.PROP):
|
||||
"""Creates token from a bnip expression string
|
||||
|
||||
Args:
|
||||
nip_expression (str): the nip expression string
|
||||
starting_section (NipSections): the section to start parsing from
|
||||
bnip_expression (str): the bnip expression string
|
||||
starting_section (BNipSections): the section to start parsing from
|
||||
Returns:
|
||||
A list of tokens
|
||||
Raises:
|
||||
NipSyntaxError: If there is a syntax error in the nip expression
|
||||
BNipSyntaxError: If there is a syntax error in the bnip expression
|
||||
"""
|
||||
self.current_section = starting_section
|
||||
self.text = list(nip_expression)
|
||||
self.text = list(bnip_expression)
|
||||
self._advance()
|
||||
self.tokens = []
|
||||
while self.current_token != None:
|
||||
|
||||
if self.current_token == "-": # * Since - is a math symbol and a negative sign for numbers, we need to handle it differently.
|
||||
if self.tokens[-1].type == TokenType.NUMBER:
|
||||
NTIPAliasKeywords = [
|
||||
TokenType.KeywordNTIPAliasClass,
|
||||
TokenType.KeywordNTIPAliasFlag,
|
||||
TokenType.KeywordNTIPAliasIDName,
|
||||
TokenType.KeywordNTIPAliasMaxQuantity,
|
||||
TokenType.KeywordNTIPAliasName,
|
||||
TokenType.KeywordNTIPAliasQuality,
|
||||
TokenType.KeywordNTIPAliasStat,
|
||||
TokenType.KeywordNTIPAliasType,
|
||||
]
|
||||
if self.tokens[-1].type in NTIPAliasKeywords + [TokenType.NUMBER]:
|
||||
self.tokens.append(self._create_math_operator())
|
||||
self._advance()
|
||||
else:
|
||||
self.tokens.append(self._create_digits())
|
||||
continue
|
||||
|
||||
|
||||
|
||||
if self.current_token in DIGITS:
|
||||
self.tokens.append(self._create_digits())
|
||||
elif self.current_token in WHITESPACE:
|
||||
@@ -116,20 +114,20 @@ class Lexer:
|
||||
self.tokens.append(self._create_keyword_lookup())
|
||||
elif self.current_token in CHARS:
|
||||
self.tokens.append(self._create_d2r_image_data_lookup())
|
||||
elif self.current_section == NipSections.PROP and self.text_i == 0 and self.current_token == "@":
|
||||
elif self.current_section == BNipSections.PROP and self.text_i == 0 and self.current_token == "@":
|
||||
self.tokens.append(Token(TokenType.NOTIFICATION, '@'))
|
||||
self._advance()
|
||||
else:
|
||||
raise NipSyntaxError("NIP_0x1", "Unknown token: " + self.current_token, self._get_text())
|
||||
raise BNipSyntaxError("BNIP_0x1", f"Unknown token: '{self.current_token}'", self._get_text())
|
||||
return self.tokens
|
||||
|
||||
def detokenize(self, tokens: list[Token]) -> str:
|
||||
"""Detokenizes a list of tokens into a nip expression string
|
||||
"""Detokenizes a list of tokens into a bnip expression string
|
||||
|
||||
Args:
|
||||
tokens (list[Token]): the list of tokens to detokenize
|
||||
Returns:
|
||||
A nip expression string
|
||||
A bnip expression string
|
||||
Raises:
|
||||
None
|
||||
|
||||
@@ -167,7 +165,6 @@ class Lexer:
|
||||
TokenType.KeywordNTIPAliasQuality: '[quality]',
|
||||
TokenType.KeywordNTIPAliasType: '[type]',
|
||||
|
||||
TokenType.ValueNTIPAlias: '{}',
|
||||
TokenType.ValueNTIPAliasClass: '{}',
|
||||
TokenType.ValueNTIPAliasClassID: '{}',
|
||||
TokenType.ValueNTIPAliasFlag: '{}',
|
||||
@@ -223,10 +220,13 @@ class Lexer:
|
||||
found_whole_number = re.match(r"^-*[0-9]+", self._get_current_iteration_of_text_raw())
|
||||
if found_whole_number:
|
||||
return self._create_custom_digit_token(found_whole_number.group(0))
|
||||
return Token(TokenType.UNKNOWN, self.current_token)
|
||||
if self.current_token:
|
||||
return Token(TokenType.UNKNOWN, self.current_token)
|
||||
else:
|
||||
return Token(TokenType.UNKNOWN, "")
|
||||
|
||||
|
||||
def _create_math_operator(self):
|
||||
def _create_math_operator(self) -> Token:
|
||||
symbol_map = {
|
||||
'+': TokenType.PLUS,
|
||||
'-': TokenType.MINUS,
|
||||
@@ -240,11 +240,11 @@ class Lexer:
|
||||
|
||||
symbol = self.current_token
|
||||
|
||||
if symbol in symbol_map:
|
||||
return Token(symbol_map[symbol], symbol)
|
||||
|
||||
return Token(TokenType.UNKNOWN, symbol)
|
||||
|
||||
if symbol:
|
||||
if symbol in symbol_map:
|
||||
return Token(symbol_map[symbol], symbol)
|
||||
return Token(TokenType.UNKNOWN, symbol)
|
||||
return Token(TokenType.UNKNOWN, "")
|
||||
def _create_keyword_lookup(self) -> Token:
|
||||
"""
|
||||
item data lookup i.e [name]
|
||||
@@ -259,9 +259,9 @@ class Lexer:
|
||||
lookup_key += char
|
||||
self._advance()
|
||||
else:
|
||||
raise NipSyntaxError("NIP_0x2", "Missing ] after keyword", self._get_text())
|
||||
raise BNipSyntaxError("BNIP_0x2", "Missing ] after keyword", self._get_text())
|
||||
if lookup_key:
|
||||
if self.current_section == NipSections.PROP:
|
||||
if self.current_section == BNipSections.PROP:
|
||||
match lookup_key:
|
||||
case "name":
|
||||
return Token(TokenType.KeywordNTIPAliasName, lookup_key)
|
||||
@@ -289,20 +289,20 @@ class Lexer:
|
||||
Logger.warning(f"Unknown property lookup: \"{lookup_key}\" {''.join(self.text)} {self.current_section}")
|
||||
|
||||
return Token(TokenType.UNKNOWN, lookup_key)
|
||||
elif self.current_section == NipSections.STAT:
|
||||
elif self.current_section == BNipSections.STAT:
|
||||
if lookup_key in NTIPAliasStat:
|
||||
return Token(TokenType.ValueNTIPAliasStat, NTIPAliasStat[lookup_key])
|
||||
return Token(TokenType.KeywordNTIPAliasStat, NTIPAliasStat[lookup_key])
|
||||
else:
|
||||
# spell_check = ""
|
||||
# for key in NTIPAliasStat:
|
||||
# if levenshtein(lookup_key, key) < 3:
|
||||
# spell_check = f", did you mean {key}?"
|
||||
# raise NipSyntaxError("NIP_0x3", f"Unknown NTIPStat lookup: {lookup_key}{spell_check}", self._get_text())
|
||||
# raise BNipSyntaxError("BNIP_0x3", f"Unknown NTIPStat lookup: {lookup_key}{spell_check}", self._get_text())
|
||||
return Token(TokenType.UNKNOWN, lookup_key)
|
||||
elif self.current_section == NipSections.MAXQUANTITY:
|
||||
elif self.current_section == BNipSections.MAXQUANTITY:
|
||||
pass
|
||||
|
||||
return Token(TokenType.UNKNOWN, lookup_key)
|
||||
return Token(TokenType.UNKNOWN, lookup_key)
|
||||
|
||||
def _create_d2r_image_data_lookup(self) -> Token:
|
||||
lookup_key = ""
|
||||
@@ -315,7 +315,7 @@ class Lexer:
|
||||
self._advance()
|
||||
lookup_key = found
|
||||
|
||||
if self.current_section == NipSections.PROP:
|
||||
if self.current_section == BNipSections.PROP:
|
||||
# TODO: The second checks (i.e NTIPAliasClass and self.tokens[-2].type == TokenType.CLASS:) seem a little misplaced, possibly put them inside the validation function that is inside transpiler.py and throw a warning accordingly.
|
||||
if len(self.tokens) >= 2:
|
||||
if lookup_key in NTIPAliasClass and self.tokens[-2].type == TokenType.KeywordNTIPAliasClass:
|
||||
@@ -331,9 +331,9 @@ class Lexer:
|
||||
elif self.tokens[-2].type == TokenType.KeywordNTIPAliasIDName:
|
||||
return Token(TokenType.ValueNTIPAliasIDName, lookup_key)
|
||||
else:
|
||||
raise NipSyntaxError("NIP_0x20", f"Bad token sequence: {self._get_text()}", self._get_text())
|
||||
raise BNipSyntaxError("BNIP_0x20", f"Bad token sequence: {self._get_text()}", self._get_text())
|
||||
return Token(TokenType.UNKNOWN, lookup_key)
|
||||
elif self.current_section == NipSections.STAT:
|
||||
elif self.current_section == BNipSections.STAT:
|
||||
if lookup_key in NTIPAliasStat:
|
||||
return Token(TokenType.ValueNTIPAliasStat, lookup_key)
|
||||
else:
|
||||
@@ -371,4 +371,4 @@ class Lexer:
|
||||
pythonic_operator = found_text.replace("#", "and").replace("||", "or").replace("&&", "and")
|
||||
return Token(logical_operator_map[found_text], pythonic_operator)
|
||||
else:
|
||||
raise NipSyntaxError("NIP_0x5", f"Invalid logical operator: '{char}'", self._get_text())
|
||||
raise BNipSyntaxError("BNIP_0x5", f"Invalid logical operator: '{char}'", self._get_text())
|
||||
@@ -27,12 +27,12 @@ class TokenType(Enum):
|
||||
KeywordNTIPAliasClass = auto()
|
||||
KeywordNTIPAliasFlag = auto()
|
||||
KeywordNTIPAliasIDName = auto()
|
||||
KeywordNTIPAliasMaxQuantity = auto()
|
||||
KeywordNTIPAliasName = auto()
|
||||
KeywordNTIPAliasQuality = auto()
|
||||
KeywordNTIPAliasType = auto()
|
||||
KeywordNTIPAliasStat = auto()
|
||||
KeywordNTIPAliasMaxQuantity = auto()
|
||||
|
||||
ValueNTIPAlias = auto()
|
||||
ValueNTIPAliasClass = auto()
|
||||
ValueNTIPAliasClassID = auto()
|
||||
ValueNTIPAliasFlag = auto()
|
||||
@@ -50,12 +50,10 @@ class TokenType(Enum):
|
||||
|
||||
UNKNOWN = auto()
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class Token:
|
||||
type: TokenType
|
||||
value: str | int
|
||||
value: str | int | float
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.type} : {self.value}"
|
||||
@@ -1,19 +1,20 @@
|
||||
from nip.NTIPAliasQuality import NTIPAliasQuality
|
||||
from nip.NTIPAliasClass import NTIPAliasClass
|
||||
from nip.NTIPAliasClassID import NTIPAliasClassID
|
||||
from nip.NTIPAliasFlag import NTIPAliasFlag
|
||||
from nip.NTIPAliasStat import NTIPAliasStat
|
||||
from nip.NTIPAliasType import NTIPAliasType
|
||||
from bnip.NTIPAliasQuality import NTIPAliasQuality
|
||||
from bnip.NTIPAliasClass import NTIPAliasClass
|
||||
from bnip.NTIPAliasClassID import NTIPAliasClassID
|
||||
from bnip.NTIPAliasFlag import NTIPAliasFlag
|
||||
from bnip.NTIPAliasStat import NTIPAliasStat
|
||||
from bnip.NTIPAliasType import NTIPAliasType
|
||||
|
||||
# ! The above imports are necessary, they are used within the eval statements. Your text editor probably is not showing them as not in use.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from nip.lexer import Lexer, NipSyntaxError, NipSections
|
||||
from nip.tokens import Token, TokenType
|
||||
from nip.utils import find_unique_or_set_base
|
||||
from bnip.lexer import Lexer, BNipSections
|
||||
from bnip.BNipExceptions import BNipSyntaxError
|
||||
from bnip.tokens import Token, TokenType
|
||||
from bnip.utils import find_unique_or_set_base
|
||||
|
||||
@dataclass
|
||||
class NIPExpression:
|
||||
class BNIPExpression:
|
||||
raw: str
|
||||
should_id_transpiled: str | None
|
||||
transpiled: str
|
||||
@@ -21,7 +22,7 @@ class NIPExpression:
|
||||
tokens: list[Token]
|
||||
|
||||
|
||||
nip_expressions: list[NIPExpression] = []
|
||||
bnip_expressions: list[BNIPExpression] = []
|
||||
|
||||
|
||||
token_transpile_map = {
|
||||
@@ -72,8 +73,10 @@ def transpile(tokens: list[Token], isPickUpPhase: bool = False, transpiled_expre
|
||||
if tokens[i + 1].type != TokenType.ValueNTIPAliasFlag:
|
||||
expression += token_value
|
||||
case TokenType.EQ:
|
||||
if tokens[i + 1].type == TokenType.ValueNTIPAliasFlag: continue
|
||||
if isPickUpPhase and tokens[i + 1].type == TokenType.ValueNTIPAliasIDName: continue
|
||||
if tokens[i + 1].type == TokenType.ValueNTIPAliasFlag:
|
||||
continue
|
||||
if isPickUpPhase and tokens[i + 1].type == TokenType.ValueNTIPAliasIDName:
|
||||
continue
|
||||
expression += "=="
|
||||
case TokenType.OR | TokenType.AND | TokenType.LPAREN | TokenType.RPAREN | TokenType.PLUS | TokenType.MINUS | TokenType.MULTIPLY | TokenType.DIVIDE:
|
||||
expression += token_value
|
||||
@@ -86,7 +89,7 @@ def transpile(tokens: list[Token], isPickUpPhase: bool = False, transpiled_expre
|
||||
section_open_paranthesis_count -= 1
|
||||
expression += "and"
|
||||
section_start = True
|
||||
case TokenType.ValueNTIPAliasStat:
|
||||
case TokenType.KeywordNTIPAliasStat:
|
||||
expression += "(int(item_data.get('NTIPAliasStat', {})" + f".get('{token_value}', 0)))"
|
||||
case TokenType.KeywordNTIPAliasIDName:
|
||||
if not isPickUpPhase:
|
||||
@@ -130,6 +133,7 @@ def validate_correct_math_syntax(left_token=None, right_token=None):
|
||||
TokenType.KeywordNTIPAliasName,
|
||||
TokenType.KeywordNTIPAliasQuality,
|
||||
TokenType.KeywordNTIPAliasType,
|
||||
TokenType.KeywordNTIPAliasStat,
|
||||
|
||||
TokenType.ValueNTIPAliasClass,
|
||||
TokenType.ValueNTIPAliasClassID,
|
||||
@@ -144,17 +148,17 @@ def validate_correct_math_syntax(left_token=None, right_token=None):
|
||||
TokenType.RPAREN,
|
||||
]
|
||||
if left_token and left_token.type not in allowed_left_and_right_tokens:
|
||||
raise NipSyntaxError("NIP_0x3", "unexpected token on left of math operator")
|
||||
raise BNipSyntaxError2("BNIP_0x3", "unexpected token on left of math operator")
|
||||
if right_token and right_token.type not in allowed_left_and_right_tokens:
|
||||
raise NipSyntaxError("NIP_0x4", "unexpected token on right of math operator")
|
||||
raise BNipSyntaxError2("BNIP_0x4", "unexpected token on right of math operator")
|
||||
|
||||
OPENING_PARENTHESIS_COUNT = 0 # * This needs to be reset every time an validation error occurs
|
||||
|
||||
oNipSyntaxError = NipSyntaxError
|
||||
def NipSyntaxError(error_code, error_message): # * "hook" the error constructor so every time the error is raised / called, we reset the opening parenthesis count
|
||||
CURRENT_EXPRESSION = ""
|
||||
def BNipSyntaxError2(error_code, error_message): # * "hook" the error constructor so every time the error is raised / called, we reset the opening parenthesis count
|
||||
global OPENING_PARENTHESIS_COUNT
|
||||
OPENING_PARENTHESIS_COUNT = 0
|
||||
return oNipSyntaxError(error_code, error_message)
|
||||
print(CURRENT_EXPRESSION)
|
||||
return BNipSyntaxError(error_code, error_message, CURRENT_EXPRESSION)
|
||||
|
||||
|
||||
def validate_correct_parenthesis_syntax(current_pos, all_tokens, left_token=None, right_token=None):
|
||||
@@ -183,10 +187,10 @@ def validate_correct_parenthesis_syntax(current_pos, all_tokens, left_token=None
|
||||
if token.type == TokenType.LPAREN:
|
||||
OPENING_PARENTHESIS_COUNT += 1
|
||||
if left_token and left_token.type not in allowed_left_and_right_tokens + [TokenType.LPAREN]:
|
||||
raise NipSyntaxError("NIP_0x18", "unexpected token on left of parenthesis")
|
||||
raise BNipSyntaxError2("BNIP_0x18", "unexpected token on left of parenthesis")
|
||||
elif token.type == TokenType.RPAREN:
|
||||
if right_token and right_token.type not in allowed_left_and_right_tokens + [TokenType.RPAREN]:
|
||||
raise NipSyntaxError("NIP_0x19", "unexpected token on right of parenthesis")
|
||||
raise BNipSyntaxError2("BNIP_0x19", "unexpected token on right of parenthesis")
|
||||
OPENING_PARENTHESIS_COUNT -= 1
|
||||
|
||||
|
||||
@@ -196,16 +200,16 @@ def validate_correct_parenthesis_syntax(current_pos, all_tokens, left_token=None
|
||||
# for i in range(current_pos, -1, -1):
|
||||
# # print(all_tokens[i].type)
|
||||
# if all_tokens[i].type == TokenType.SECTIONAND:
|
||||
# raise NipSyntaxError("NIP_0x8", "parenthesis cannot cross the section and (#)")
|
||||
# raise BNipSyntaxError2("BNIP_0x8", "parenthesis cannot cross the section and (#)")
|
||||
|
||||
if current_pos == len(all_tokens) - 1:
|
||||
if OPENING_PARENTHESIS_COUNT != 0:
|
||||
if OPENING_PARENTHESIS_COUNT > 0:
|
||||
OPENING_PARENTHESIS_COUNT = 0
|
||||
raise NipSyntaxError("NIP_0x6", "unclosed parenthesis")
|
||||
raise BNipSyntaxError2("BNIP_0x6", "unclosed parenthesis")
|
||||
else:
|
||||
OPENING_PARENTHESIS_COUNT = 0
|
||||
raise NipSyntaxError("NIP_0x7", "unopened parenthesis")
|
||||
raise BNipSyntaxError2("BNIP_0x7", "unopened parenthesis")
|
||||
OPENING_PARENTHESIS_COUNT = 0
|
||||
|
||||
def validate_digits_syntax(left=None, right=None):
|
||||
@@ -236,10 +240,10 @@ def validate_digits_syntax(left=None, right=None):
|
||||
|
||||
if left:
|
||||
if left.type not in allowed_left_and_right_tokens:
|
||||
raise NipSyntaxError("NIP_0x8", "Expected operator on left of number")
|
||||
raise BNipSyntaxError2("BNIP_0x8", "Expected operator on left of number")
|
||||
if right:
|
||||
if right.type not in allowed_left_and_right_tokens:
|
||||
raise NipSyntaxError("NIP_0x9", "Expected operator on right of number")
|
||||
raise BNipSyntaxError2("BNIP_0x9", "Expected operator on right of number")
|
||||
|
||||
|
||||
def validate_logical_operators(left=None, right=None):
|
||||
@@ -262,37 +266,39 @@ def validate_logical_operators(left=None, right=None):
|
||||
TokenType.KeywordNTIPAliasName,
|
||||
TokenType.KeywordNTIPAliasIDName,
|
||||
TokenType.KeywordNTIPAliasMaxQuantity,
|
||||
TokenType.KeywordNTIPAliasStat
|
||||
]
|
||||
|
||||
if left:
|
||||
if left.type not in allowed_left_and_right_tokens + [TokenType.RPAREN]:
|
||||
raise NipSyntaxError("NIP_0x10", "Expected token on left of logical operator")
|
||||
raise BNipSyntaxError2("BNIP_0x10", "Expected token on left of logical operator")
|
||||
if right:
|
||||
if right.type not in allowed_left_and_right_tokens + [TokenType.LPAREN]:
|
||||
raise NipSyntaxError("NIP_0x11", "Expected token on right of logical operator")
|
||||
raise BNipSyntaxError2("BNIP_0x11", "Expected token on right of logical operator")
|
||||
else:
|
||||
raise NipSyntaxError("NIP_0x12", "Expected token on right of logical operator")
|
||||
raise BNipSyntaxError2("BNIP_0x12", "Expected token on right of logical operator")
|
||||
|
||||
|
||||
def validate_nip_expression_syntax(nip_expression): # * enforces that {property} # {stats} # {maxquantity}
|
||||
def validate_bnip_expression_syntax(bnip_expression): # * enforces that {property} # {stats} # {maxquantity}
|
||||
tokens = None
|
||||
|
||||
if not nip_expression:
|
||||
global CURRENT_EXPRESSION
|
||||
if not bnip_expression:
|
||||
return
|
||||
|
||||
all_tokens = []
|
||||
CURRENT_EXPRESSION = bnip_expression
|
||||
|
||||
split_nip_expression = nip_expression.split("#")
|
||||
split_nip_expression_len = len(split_nip_expression)
|
||||
if split_nip_expression_len >= 1: # property
|
||||
tokens = Lexer().create_tokens(split_nip_expression[0], NipSections.PROP)
|
||||
split_bnip_expression = bnip_expression.split("#")
|
||||
split_bnip_expression_len = len(split_bnip_expression)
|
||||
if split_bnip_expression_len >= 1: # property
|
||||
tokens = Lexer().create_tokens(split_bnip_expression[0], BNipSections.PROP)
|
||||
all_tokens.extend(tokens)
|
||||
for token in tokens:
|
||||
if token.type == TokenType.ValueNTIPAliasStat or token.type == TokenType.UNKNOWN:
|
||||
raise NipSyntaxError("NIP_0x13", f"Invalid token '{token.value}' in property section")
|
||||
if split_nip_expression_len >= 2: # stats
|
||||
raise BNipSyntaxError2("BNIP_0x13", f"Invalid token '{token.value}' in property section")
|
||||
if split_bnip_expression_len >= 2: # stats
|
||||
all_tokens.append(Token(TokenType.SECTIONAND, "#"))
|
||||
tokens = Lexer().create_tokens(split_nip_expression[1], NipSections.STAT)
|
||||
tokens = Lexer().create_tokens(split_bnip_expression[1], BNipSections.STAT)
|
||||
all_tokens.extend(tokens)
|
||||
for token in tokens:
|
||||
is_invalid_stat_lookup = (
|
||||
@@ -305,11 +311,11 @@ def validate_nip_expression_syntax(nip_expression): # * enforces that {property}
|
||||
)
|
||||
|
||||
if is_invalid_stat_lookup:
|
||||
raise NipSyntaxError("NIP_0x14", f"Invalid token '{token.value}' in stats section")
|
||||
raise BNipSyntaxError2("BNIP_0x14", f"Invalid token '{token.value}' in stats section")
|
||||
|
||||
if split_nip_expression_len >= 3: # maxquantity
|
||||
if split_bnip_expression_len >= 3: # maxquantity
|
||||
# all_tokens.append(Token(TokenType.SECTIONAND, "#"))
|
||||
tokens = Lexer().create_tokens(split_nip_expression[2], NipSections.MAXQUANTITY)
|
||||
tokens = Lexer().create_tokens(split_bnip_expression[2], BNipSections.MAXQUANTITY)
|
||||
all_tokens.extend(tokens)
|
||||
for token in tokens:
|
||||
is_invalid_maxquantity_lookup = (
|
||||
@@ -324,12 +330,12 @@ def validate_nip_expression_syntax(nip_expression): # * enforces that {property}
|
||||
|
||||
if is_invalid_maxquantity_lookup:
|
||||
pass
|
||||
raise NipSyntaxError("NIP_0x15", "Invalid maxquantity lookup")
|
||||
raise BNipSyntaxError2("BNIP_0x15", "Invalid maxquantity lookup")
|
||||
|
||||
# * Further syntax validation
|
||||
# print(all_tokens)
|
||||
if all_tokens[-1].type == TokenType.SECTIONAND:
|
||||
raise NipSyntaxError("NIP_0x16", "unexpected sectionand (#) at end of expression")
|
||||
raise BNipSyntaxError2("BNIP_0x16", "unexpected sectionand (#) at end of expression")
|
||||
math_tokens = [TokenType.MULTIPLY, TokenType.PLUS, TokenType.MINUS, TokenType.DIVIDE, TokenType.MODULO, TokenType.POW]
|
||||
logical_tokens = [TokenType.AND, TokenType.OR, TokenType.EQ, TokenType.NE, TokenType.GT, TokenType.LT, TokenType.GE, TokenType.LE, TokenType.SECTIONAND]
|
||||
for i, token in enumerate(all_tokens):
|
||||
@@ -344,7 +350,7 @@ def validate_nip_expression_syntax(nip_expression): # * enforces that {property}
|
||||
if token.type == TokenType.EQ:
|
||||
if i == len(all_tokens) - 1: # * Check to make sure the next token is a token.
|
||||
# ! the logic only makes sense for the last token, what the f*ck
|
||||
raise NipSyntaxError("NIP_0x17", "No value after equal sign")
|
||||
raise BNipSyntaxError2("BNIP_0x17", "No value after equal sign")
|
||||
elif token.type in math_tokens:
|
||||
validate_correct_math_syntax(left_token=left, right_token=right)
|
||||
# * Make sure two numbers aren't next to each other.
|
||||
@@ -371,7 +377,7 @@ def remove_quantity(expression): # ! This is a bit ghetto, but since we're not u
|
||||
return expression
|
||||
|
||||
|
||||
def get_section_from_tokens(all_tokens: list[Token], section: NipSections | None = None) -> dict[NipSections, list[Token]]:
|
||||
def get_section_from_tokens(all_tokens: list[Token], section: BNipSections | None = None) -> dict[BNipSections, list[Token]]:
|
||||
tokens_split_by_section = []
|
||||
temp_section = []
|
||||
|
||||
@@ -387,29 +393,29 @@ def get_section_from_tokens(all_tokens: list[Token], section: NipSections | None
|
||||
temp_section.append(token)
|
||||
section_tokens_len = len(tokens_split_by_section)
|
||||
|
||||
nip_map = {
|
||||
NipSections.PROP: tokens_split_by_section[0] if section_tokens_len >= 1 else [],
|
||||
NipSections.STAT: tokens_split_by_section[1] if section_tokens_len >= 2 else [],
|
||||
NipSections.MAXQUANTITY: tokens_split_by_section[2] if section_tokens_len >= 3 else []
|
||||
bnip_map = {
|
||||
BNipSections.PROP: tokens_split_by_section[0] if section_tokens_len >= 1 else [],
|
||||
BNipSections.STAT: tokens_split_by_section[1] if section_tokens_len >= 2 else [],
|
||||
BNipSections.MAXQUANTITY: tokens_split_by_section[2] if section_tokens_len >= 3 else []
|
||||
}
|
||||
|
||||
return nip_map
|
||||
return bnip_map
|
||||
|
||||
|
||||
def prepare_nip_expression(expression: str) -> str:
|
||||
def prepare_bnip_expression(expression: str) -> str:
|
||||
if not expression.startswith("//") and not expression.startswith("-"):
|
||||
expression = expression.lower()
|
||||
expression = expression.replace("'", "")
|
||||
expression = expression.split("//")[0] # * Ignore the comments inside the nip expression
|
||||
expression = remove_quantity(expression)
|
||||
if validate_nip_expression_syntax(expression):
|
||||
if validate_bnip_expression_syntax(expression):
|
||||
return expression
|
||||
return ''
|
||||
|
||||
|
||||
def transpile_nip_expression(expression: str | list[Token], isPickUpPhase=False):
|
||||
def transpile_bnip_expression(expression: str | list[Token], isPickUpPhase=False):
|
||||
if isinstance(expression, str):
|
||||
expression = prepare_nip_expression(expression)
|
||||
expression = prepare_bnip_expression(expression)
|
||||
if expression:
|
||||
tokens = Lexer().create_tokens(expression)
|
||||
transpiled_expression = transpile(tokens, isPickUpPhase=isPickUpPhase)
|
||||
@@ -420,27 +426,27 @@ def transpile_nip_expression(expression: str | list[Token], isPickUpPhase=False)
|
||||
if transpiled_expression:
|
||||
return transpiled_expression
|
||||
|
||||
def generate_expression_object(nip_expression: str) -> NIPExpression | None:
|
||||
nip_expression = prepare_nip_expression(nip_expression)
|
||||
def generate_expression_object(bnip_expression: str) -> BNIPExpression | None:
|
||||
bnip_expression = prepare_bnip_expression(bnip_expression)
|
||||
|
||||
if nip_expression:
|
||||
tokens = Lexer().create_tokens(nip_expression)
|
||||
if transpiled_expression := transpile_nip_expression(tokens):
|
||||
if bnip_expression:
|
||||
tokens = Lexer().create_tokens(bnip_expression)
|
||||
if transpiled_expression := transpile_bnip_expression(tokens):
|
||||
split_tokens = get_section_from_tokens(tokens)
|
||||
expression_obj = NIPExpression(
|
||||
raw=nip_expression,
|
||||
expression_obj = BNIPExpression(
|
||||
raw=bnip_expression,
|
||||
tokens=tokens,
|
||||
transpiled=transpiled_expression,
|
||||
should_id_transpiled=transpile_nip_expression(split_tokens[NipSections.PROP]),
|
||||
should_pickup=transpile_nip_expression(split_tokens[NipSections.PROP], isPickUpPhase=True) # * Some stuff gets transpiled differently in the pickup phase
|
||||
should_id_transpiled=transpile_bnip_expression(split_tokens[BNipSections.PROP]),
|
||||
should_pickup=transpile_bnip_expression(split_tokens[BNipSections.PROP], isPickUpPhase=True) # * Some stuff gets transpiled differently in the pickup phase
|
||||
)
|
||||
return expression_obj
|
||||
return None
|
||||
|
||||
|
||||
def load_nip_expression(nip_expression: str):
|
||||
if (expression_obj := generate_expression_object(nip_expression)) is not None:
|
||||
nip_expressions.append(expression_obj)
|
||||
def load_bnip_expression(bnip_expression: str):
|
||||
if (expression_obj := generate_expression_object(bnip_expression)) is not None:
|
||||
bnip_expressions.append(expression_obj)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from nip.UniqueAndSetData import UniqueAndSetData
|
||||
from bnip.UniqueAndSetData import UniqueAndSetData
|
||||
|
||||
|
||||
def find_unique_or_set_base(unique_or_set_name) -> tuple[str, str]:
|
||||
@@ -14,4 +14,4 @@ def find_unique_or_set_base(unique_or_set_name) -> tuple[str, str]:
|
||||
for set in sets:
|
||||
if set.lower() == unique_or_set_name:
|
||||
return key, "set"
|
||||
return "",""
|
||||
raise NipSyntaxError("NIP_0x23", f"'{unique_or_set_name}' is not a valid unique or set")
|
||||
@@ -1,6 +1,6 @@
|
||||
from transitions import Machine
|
||||
import time
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import time
|
||||
import os
|
||||
import random
|
||||
@@ -16,16 +16,18 @@ from utils.restart import safe_exit, restart_game
|
||||
from game_stats import GameStats
|
||||
from logger import Logger
|
||||
from config import Config
|
||||
from screen import grab
|
||||
from screen import grab, convert_monitor_to_screen, convert_screen_to_abs, convert_abs_to_monitor, convert_screen_to_monitor
|
||||
import template_finder
|
||||
from char import IChar
|
||||
from item.pickit import PickIt
|
||||
from item import consumables
|
||||
from pather import Pather, Location
|
||||
from char.sorceress import LightSorc, BlizzSorc, NovaSorc,HydraSorc
|
||||
from char.sorceress import LightSorc, BlizzSorc, BlizzorbSorc, NovaSorc, HydraSorc
|
||||
from char.trapsin import Trapsin
|
||||
from char.paladin.hammerdin import Hammerdin
|
||||
from char.paladin import FoHdin
|
||||
from char.warlock import FireLock, AbyssLock, EchoLock
|
||||
from char.amazon.javazon import Javazon
|
||||
from char.barbarian import Barbarian
|
||||
from char.necro import Necro
|
||||
from char.poison_necro import Poison_Necro
|
||||
@@ -36,10 +38,16 @@ from ui_manager import wait_until_hidden, wait_until_visible, ScreenObjects, is_
|
||||
from ui import meters, skills, view, character_select, main_menu
|
||||
from inventory import personal, vendor, belt, common
|
||||
|
||||
from run import Pindle, ShenkEld, Trav, Nihlathak, Arcane, Diablo
|
||||
from run import Pindle, ShenkEld, Trav, Nihlathak, Arcane, Diablo, Vizier, Level
|
||||
from run.baal import Baal
|
||||
from run.mephisto import Mephisto
|
||||
from run.andariel import Andariel
|
||||
from run.countess import Countess
|
||||
from town import TownManager, A1, A2, A3, A4, A5, town_manager
|
||||
|
||||
from messages import Messenger
|
||||
from threading import Lock
|
||||
from utils.stealth import maybe_afk_break, should_skip_run
|
||||
|
||||
class Bot:
|
||||
|
||||
@@ -48,6 +56,7 @@ class Bot:
|
||||
self._messenger = Messenger()
|
||||
self._pather = Pather()
|
||||
self._pickit = PickIt()
|
||||
self._stash_mutex = Lock()
|
||||
|
||||
# Create Character
|
||||
match Config().char["type"]:
|
||||
@@ -55,6 +64,8 @@ class Bot:
|
||||
self._char: IChar = LightSorc(Config().light_sorc, self._pather)
|
||||
case "blizz_sorc":
|
||||
self._char: IChar = BlizzSorc(Config().blizz_sorc, self._pather)
|
||||
case "blizzorb_sorc":
|
||||
self._char: IChar = BlizzorbSorc(Config().blizzorb_sorc, self._pather)
|
||||
case "nova_sorc":
|
||||
self._char: IChar = NovaSorc(Config().nova_sorc, self._pather)
|
||||
case "hydra_sorc":
|
||||
@@ -62,7 +73,15 @@ class Bot:
|
||||
case "hammerdin" | "paladin":
|
||||
self._char: IChar = Hammerdin(Config().hammerdin, self._pather, self._pickit) #pickit added for diablo
|
||||
case "fohdin":
|
||||
self._char: IChar = FoHdin(Config().fohdin, self._pather, self._pickit) #pickit added for diablo
|
||||
self._char: IChar = FoHdin(Config().fohdin, self._pather, self._pickit)
|
||||
case "abyss_lock" | "warlock":
|
||||
self._char: IChar = AbyssLock(Config().abyss_lock, self._pather)
|
||||
case "fire_lock":
|
||||
self._char: IChar = FireLock(Config().fire_lock, self._pather)
|
||||
case "echo_lock":
|
||||
self._char: IChar = EchoLock(Config().echo_lock, self._pather)
|
||||
case "amazon" | "javazon":
|
||||
self._char: IChar = Javazon(Config().javazon, self._pather)
|
||||
case "trapsin":
|
||||
self._char: IChar = Trapsin(Config().trapsin, self._pather)
|
||||
case "barbarian":
|
||||
@@ -91,28 +110,39 @@ class Bot:
|
||||
|
||||
# Create runs
|
||||
self._do_runs = {
|
||||
"run_level": Config().routes.get("run_level"),
|
||||
"run_trav": Config().routes.get("run_trav"),
|
||||
"run_pindle": Config().routes.get("run_pindle"),
|
||||
"run_shenk": Config().routes.get("run_eldritch") or Config().routes.get("run_eldritch_shenk"),
|
||||
"run_nihlathak": Config().routes.get("run_nihlathak"),
|
||||
"run_arcane": Config().routes.get("run_arcane"),
|
||||
"run_diablo": Config().routes.get("run_diablo"),
|
||||
"run_vizier": Config().routes.get("run_vizier"),
|
||||
"run_baal": Config().routes.get("run_baal"),
|
||||
"run_mephisto": Config().routes.get("run_mephisto"),
|
||||
"run_andariel": Config().routes.get("run_andariel"),
|
||||
"run_countess": Config().routes.get("run_countess"),
|
||||
}
|
||||
# Adapt order to the config
|
||||
self._do_runs = OrderedDict((k, self._do_runs[k]) for k in Config().routes_order if k in self._do_runs and self._do_runs[k])
|
||||
|
||||
runs = list(self._do_runs.keys())
|
||||
self._do_runs_reset = copy(self._do_runs)
|
||||
Logger.info(f"Doing runs: {self._do_runs_reset.keys()}")
|
||||
if Config().general["randomize_runs"]:
|
||||
self.shuffle_runs()
|
||||
self._pindle = Pindle(self._pather, self._town_manager, self._char, self._pickit, runs)
|
||||
self._shenk = ShenkEld(self._pather, self._town_manager, self._char, self._pickit, runs)
|
||||
self._trav = Trav(self._pather, self._town_manager, self._char, self._pickit, runs)
|
||||
self._nihlathak = Nihlathak(self._pather, self._town_manager, self._char, self._pickit, runs)
|
||||
self._arcane = Arcane(self._pather, self._town_manager, self._char, self._pickit, runs)
|
||||
self._diablo = Diablo(self._pather, self._town_manager, self._char, self._pickit, runs)
|
||||
self._level = Level(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
|
||||
self._pindle = Pindle(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
|
||||
self._shenk = ShenkEld(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
|
||||
self._trav = Trav(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
|
||||
self._nihlathak = Nihlathak(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
|
||||
self._arcane = Arcane(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
|
||||
self._diablo = Diablo(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
|
||||
self._vizier = Vizier(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
|
||||
|
||||
self._baal = Baal(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
|
||||
self._mephisto = Mephisto(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
|
||||
self._andariel = Andariel(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
|
||||
self._countess = Countess(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
|
||||
# Create member variables
|
||||
self._picked_up_items = False
|
||||
self._curr_loc: bool | Location = None
|
||||
@@ -127,24 +157,31 @@ class Bot:
|
||||
self._timer = time.time()
|
||||
|
||||
# Create State Machine
|
||||
self._states=['initialization','hero_selection', 'town', 'pindle', 'shenk', 'trav', 'nihlathak', 'arcane', 'diablo']
|
||||
self._states=['initialization','hero_selection', 'town', 'level', 'pindle', 'shenk', 'trav', 'nihlathak', 'arcane', 'diablo', 'vizier', 'baal', 'mephisto', 'andariel', 'countess']
|
||||
self._transitions = [
|
||||
{ 'trigger': 'init', 'source': 'initialization', 'dest': '=','before': "on_init"},
|
||||
{ 'trigger': 'skip_to_level', 'source': 'initialization', 'dest': 'level', 'before': "on_skip_to_level"},
|
||||
{ 'trigger': 'select_character', 'source': 'initialization', 'dest': 'hero_selection', 'before': "on_select_character"},
|
||||
{ 'trigger': 'start_from_town', 'source': ['initialization', 'hero_selection'], 'dest': 'town', 'before': "on_start_from_town"},
|
||||
{ 'trigger': 'create_game', 'source': 'hero_selection', 'dest': '=', 'before': "on_create_game"},
|
||||
# Tasks within town
|
||||
{ 'trigger': 'maintenance', 'source': 'town', 'dest': 'town', 'before': "on_maintenance"},
|
||||
# Different runs
|
||||
{ 'trigger': 'run_level', 'source': 'town', 'dest': 'level', 'before': "on_run_level"},
|
||||
{ 'trigger': 'run_pindle', 'source': 'town', 'dest': 'pindle', 'before': "on_run_pindle"},
|
||||
{ 'trigger': 'run_shenk', 'source': 'town', 'dest': 'shenk', 'before': "on_run_shenk"},
|
||||
{ 'trigger': 'run_trav', 'source': 'town', 'dest': 'trav', 'before': "on_run_trav"},
|
||||
{ 'trigger': 'run_nihlathak', 'source': 'town', 'dest': 'nihlathak', 'before': "on_run_nihlathak"},
|
||||
{ 'trigger': 'run_arcane', 'source': 'town', 'dest': 'arcane', 'before': "on_run_arcane"},
|
||||
{ 'trigger': 'run_diablo', 'source': 'town', 'dest': 'nihlathak', 'before': "on_run_diablo"},
|
||||
{ 'trigger': 'run_diablo', 'source': 'town', 'dest': 'diablo', 'before': "on_run_diablo"},
|
||||
{ 'trigger': 'run_vizier', 'source': 'town', 'dest': 'vizier', 'before': "on_run_vizier"},
|
||||
{ 'trigger': 'run_baal', 'source': 'town', 'dest': 'baal', 'before': "on_run_baal" },
|
||||
{ 'trigger': 'run_mephisto', 'source': 'town', 'dest': 'mephisto', 'before': "on_run_mephisto" },
|
||||
{ 'trigger': 'run_andariel', 'source': 'town', 'dest': 'andariel', 'before': "on_run_andariel" },
|
||||
{ 'trigger': 'run_countess', 'source': 'town', 'dest': 'countess', 'before': "on_run_countess" },
|
||||
# End run / game
|
||||
{ 'trigger': 'end_run', 'source': ['shenk', 'pindle', 'nihlathak', 'trav', 'arcane', 'diablo'], 'dest': 'town', 'before': "on_end_run"},
|
||||
{ 'trigger': 'end_game', 'source': ['town', 'shenk', 'pindle', 'nihlathak', 'trav', 'arcane', 'diablo','end_run'], 'dest': 'initialization', 'before': "on_end_game"},
|
||||
{ 'trigger': 'end_run', 'source': ['level', 'shenk', 'pindle', 'nihlathak', 'trav', 'arcane', 'diablo','vizier', 'baal', 'mephisto', 'andariel', 'countess'], 'dest': 'town', 'before': "on_end_run"},
|
||||
{ 'trigger': 'end_game', 'source': ['town', 'shenk', 'pindle', 'nihlathak', 'trav', 'arcane', 'diablo','vizier','end_run', 'baal', 'mephisto', 'andariel', 'countess'], 'dest': 'initialization', 'before': "on_end_game"},
|
||||
]
|
||||
self.machine = Machine(model=self, states=self._states, initial="initialization", transitions=self._transitions, queued=True)
|
||||
self._transmute = Transmute(self._game_stats)
|
||||
@@ -178,7 +215,8 @@ class Bot:
|
||||
Logger.info(f"{Config().general['name']} is now pausing")
|
||||
self._game_stats.pause_timer()
|
||||
while self._pausing:
|
||||
time.sleep(0.2)
|
||||
from utils.misc import wait as _wait
|
||||
_wait(0.2, 0.24)
|
||||
if not self._stopping:
|
||||
self.trigger(name, **kwargs)
|
||||
|
||||
@@ -199,7 +237,10 @@ class Bot:
|
||||
def shuffle_runs(self):
|
||||
tmp = list(self._do_runs.items())
|
||||
random.shuffle(tmp)
|
||||
self._do_runs = OrderedDict(tmp)
|
||||
#We clear() and update() to avoid assignment on _do_runs as this list is referenced
|
||||
#by other classes and we want updates persisted everywhere.
|
||||
self._do_runs.clear()
|
||||
self._do_runs.update(OrderedDict(tmp))
|
||||
|
||||
def is_last_run(self):
|
||||
found_unfinished_run = False
|
||||
@@ -219,6 +260,13 @@ class Bot:
|
||||
def on_init(self):
|
||||
self._game_stats.log_start_game()
|
||||
keyboard.release(Config().char["stand_still"])
|
||||
# If we're only doing run_level and character is already in-game, skip
|
||||
# the town detection and go straight to the run
|
||||
if list(self._do_runs.keys()) == ["run_level"]:
|
||||
Logger.info("Level run only - skipping town detection, starting directly")
|
||||
self._curr_loc = Location.A1_TOWN_START
|
||||
self.trigger_or_stop("skip_to_level")
|
||||
return
|
||||
transition_to_screens = Bot._rebuild_as_asset_to_trigger({
|
||||
"select_character": main_menu.MAIN_MENU_MARKERS,
|
||||
"start_from_town": town_manager.TOWN_MARKERS,
|
||||
@@ -257,7 +305,10 @@ class Bot:
|
||||
|
||||
def on_start_from_town(self):
|
||||
self._curr_loc = self._town_manager.wait_for_town_spawn()
|
||||
|
||||
if not self._curr_loc:
|
||||
Logger.error("Could not detect game creation and ending game.")
|
||||
return self.trigger_or_stop("end_game", failed=True)
|
||||
|
||||
# Handle picking up corpse in case of death
|
||||
if (corpse_present := is_visible(ScreenObjects.Corpse)):
|
||||
self._previous_run_failed = True
|
||||
@@ -294,13 +345,19 @@ class Bot:
|
||||
# Look at belt to figure out how many pots need to be picked up
|
||||
belt.update_pot_needs()
|
||||
|
||||
# If we are doing trav_runs and juvs are full we will remove extra runs
|
||||
if ("run_trav" == next(iter(self._do_runs))) and (consumables.get_needs("rejuv") <= 1):
|
||||
while (len(self._do_runs) > 1):
|
||||
skipped_run = self._do_runs.popitem()[0]
|
||||
Logger.info(f"We are running trav and have full rejuvs so skipping {skipped_run}")
|
||||
|
||||
# Inspect inventory
|
||||
items = None
|
||||
need_inspect = self._picked_up_items or self._previous_run_failed
|
||||
if Config().char["runs_per_stash"]:
|
||||
need_inspect |= (self._game_stats._run_counter - 1) % Config().char["runs_per_stash"] == 0
|
||||
if need_inspect:
|
||||
img = personal.open()
|
||||
img = personal.open_inventory()
|
||||
# Update TP, ID, key needs
|
||||
if self._game_stats._game_counter == 1:
|
||||
self._use_id_tome = common.tome_state(img, 'id')[0] is not None
|
||||
@@ -318,6 +375,11 @@ class Bot:
|
||||
items = personal.inspect_items(img, game_stats=self._game_stats, close_window=False)
|
||||
common.close()
|
||||
Logger.debug(f"Needs: {consumables.get_needs()}")
|
||||
|
||||
#Cast town buffs (ie burst of speed etc)
|
||||
if not self._pre_buffed:
|
||||
self._char.cast_town_buffs(self._curr_loc)
|
||||
|
||||
if items:
|
||||
# if there are still items that need identifying, go to cain to identify them
|
||||
if any([item.need_id for item in items]):
|
||||
@@ -329,6 +391,7 @@ class Bot:
|
||||
items = personal.inspect_items(game_stats=self._game_stats)
|
||||
keep_items = any([item.keep for item in items]) if items else None
|
||||
sell_items = any([item.sell for item in items]) if items else None
|
||||
stash_gold = personal.get_inventory_gold_full()
|
||||
|
||||
# Check if should need some healing
|
||||
img = grab()
|
||||
@@ -346,23 +409,27 @@ class Bot:
|
||||
items = result_items
|
||||
sell_items = any([item.sell for item in items]) if items else None
|
||||
Logger.debug(f"Needs: {consumables.get_needs()}")
|
||||
elif meters.get_health(img) < 0.6 or meters.get_mana(img) < 0.2:
|
||||
elif meters.get_health(img) <= Config().char["take_rejuv_potion_health"] or meters.get_mana(img) <= Config().char["take_rejuv_potion_mana"]:
|
||||
Logger.info("Healing at next possible Vendor")
|
||||
self._curr_loc = self._town_manager.heal(self._curr_loc)
|
||||
if not self._curr_loc:
|
||||
return self.trigger_or_stop("end_game", failed=True)
|
||||
|
||||
# Stash stuff
|
||||
if keep_items or personal.get_inventory_gold_full():
|
||||
if keep_items or stash_gold:
|
||||
Logger.info("Stashing items")
|
||||
prev_loc = self._curr_loc
|
||||
self._curr_loc, result_items = self._town_manager.stash(self._curr_loc, items=items)
|
||||
sell_items = any([item.sell for item in result_items]) if result_items else None
|
||||
#Acquire mutex to prevent controller from killing thread during transmutes
|
||||
self._stash_mutex.acquire()
|
||||
Logger.info("Running transmutes")
|
||||
self._transmute.run_transmutes(force=False)
|
||||
common.close()
|
||||
self._stash_mutex.release()
|
||||
if not self._curr_loc:
|
||||
return self.trigger_or_stop("end_game", failed=True)
|
||||
self._picked_up_items = False
|
||||
self._picked_up_items = False
|
||||
|
||||
# Check if we are out of tps or need repairing
|
||||
need_repair = is_visible(ScreenObjects.NeedRepair)
|
||||
@@ -407,6 +474,9 @@ class Bot:
|
||||
self._previous_run_failed = False
|
||||
for key in self._do_runs:
|
||||
if self._do_runs[key]:
|
||||
if should_skip_run():
|
||||
self._do_runs[key] = False
|
||||
continue
|
||||
self.trigger_or_stop(key)
|
||||
started_run = True
|
||||
break
|
||||
@@ -418,7 +488,15 @@ class Bot:
|
||||
cv2.imwrite("./log/screenshots/info/info_failed_game_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
|
||||
self._curr_loc = False
|
||||
self._pre_buffed = False
|
||||
view.save_and_exit()
|
||||
if view.save_and_exit() == False:
|
||||
Logger.error("Normal save_and_exit failed! Attempting fast_save_and_exit and resetting bot.")
|
||||
img = grab()
|
||||
view.fast_save_and_exit()
|
||||
self.stop()
|
||||
if Config().general["info_screenshots"]:
|
||||
self._last_chicken_screenshot = "./log/screenshots/info/info_failed_exit_" + time.strftime("%Y%m%d_%H%M%S") + ".png"
|
||||
cv2.imwrite(self._last_chicken_screenshot, img)
|
||||
|
||||
set_pause_state(True)
|
||||
self._game_stats.log_end_game(failed=failed)
|
||||
|
||||
@@ -445,10 +523,18 @@ class Bot:
|
||||
|
||||
self._timer = time.time()
|
||||
|
||||
|
||||
self._do_runs = copy(self._do_runs_reset)
|
||||
#We clear() and update() to avoid assignment on _do_runs as this list is referenced
|
||||
#by other classes and we want updates persisted everywhere.
|
||||
self._do_runs.clear()
|
||||
self._do_runs.update(self._do_runs_reset)
|
||||
if Config().general["randomize_runs"]:
|
||||
self.shuffle_runs()
|
||||
if Config().stealth.get("reshuffle_each_rotation"):
|
||||
tmp = list(self._do_runs_reset.items())
|
||||
random.shuffle(tmp)
|
||||
self._do_runs_reset = OrderedDict(tmp)
|
||||
self._do_runs.clear()
|
||||
self._do_runs.update(self._do_runs_reset)
|
||||
self.trigger_or_stop("init")
|
||||
|
||||
def on_end_run(self):
|
||||
@@ -459,6 +545,8 @@ class Bot:
|
||||
self._curr_loc = self._town_manager.wait_for_tp(self._curr_loc)
|
||||
if self._curr_loc:
|
||||
set_pause_state(True)
|
||||
# Stealth: chance of unscheduled AFK break after returning to town
|
||||
maybe_afk_break()
|
||||
return self.trigger_or_stop("maintenance")
|
||||
if not skills.has_tps():
|
||||
consumables.set_needs("tp", 20)
|
||||
@@ -483,14 +571,28 @@ class Bot:
|
||||
else:
|
||||
self.trigger_or_stop("end_run")
|
||||
|
||||
def on_skip_to_level(self):
|
||||
"""Skip town detection and go straight to level run."""
|
||||
pass
|
||||
|
||||
def on_run_level(self):
|
||||
res = False
|
||||
self._do_runs["run_level"] = False
|
||||
self._game_stats.update_location("Lvl")
|
||||
self._curr_loc = self._level.approach(self._curr_loc, not self._pre_buffed)
|
||||
if self._curr_loc:
|
||||
set_pause_state(False)
|
||||
res = self._level.battle()
|
||||
self._ending_run_helper(res)
|
||||
|
||||
def on_run_pindle(self):
|
||||
res = False
|
||||
self._do_runs["run_pindle"] = False
|
||||
self._game_stats.update_location("Pin")
|
||||
self._curr_loc = self._pindle.approach(self._curr_loc)
|
||||
self._curr_loc = self._pindle.approach(self._curr_loc, not self._pre_buffed)
|
||||
if self._curr_loc:
|
||||
set_pause_state(False)
|
||||
res = self._pindle.battle(not self._pre_buffed)
|
||||
res = self._pindle.battle()
|
||||
self._ending_run_helper(res)
|
||||
|
||||
def on_run_shenk(self):
|
||||
@@ -519,7 +621,7 @@ class Bot:
|
||||
self._curr_loc = self._nihlathak.approach(self._curr_loc)
|
||||
if self._curr_loc:
|
||||
set_pause_state(False)
|
||||
res = self._nihlathak.battle(not self._pre_buffed)
|
||||
res = self._nihlathak.battle(True)
|
||||
self._ending_run_helper(res)
|
||||
|
||||
def on_run_arcane(self):
|
||||
@@ -540,4 +642,53 @@ class Bot:
|
||||
if self._curr_loc:
|
||||
set_pause_state(False)
|
||||
res = self._diablo.battle(not self._pre_buffed)
|
||||
self._ending_run_helper(res)
|
||||
self._ending_run_helper(res)
|
||||
|
||||
def on_run_vizier(self):
|
||||
res = False
|
||||
self._do_runs["run_vizier"] = False
|
||||
self._game_stats.update_location("Dia")
|
||||
self._curr_loc = self._vizier.approach(self._curr_loc)
|
||||
if self._curr_loc:
|
||||
set_pause_state(False)
|
||||
res = self._vizier.battle(not self._pre_buffed)
|
||||
self._ending_run_helper(res)
|
||||
def on_run_baal(self):
|
||||
res = False
|
||||
self._do_runs["run_baal"] = False
|
||||
self._game_stats.update_location("Baa")
|
||||
self._curr_loc = self._baal.approach(self._curr_loc)
|
||||
if self._curr_loc:
|
||||
set_pause_state(False)
|
||||
res = self._baal.battle(not self._pre_buffed)
|
||||
self._ending_run_helper(res)
|
||||
def on_run_mephisto(self):
|
||||
res = False
|
||||
self._do_runs["run_mephisto"] = False
|
||||
self._game_stats.update_location("Mep")
|
||||
self._curr_loc = self._mephisto.approach(self._curr_loc)
|
||||
if self._curr_loc:
|
||||
set_pause_state(False)
|
||||
res = self._mephisto.battle(not self._pre_buffed)
|
||||
self._ending_run_helper(res)
|
||||
def on_run_andariel(self):
|
||||
res = False
|
||||
self._do_runs["run_andariel"] = False
|
||||
self._game_stats.update_location("And")
|
||||
self._curr_loc = self._andariel.approach(self._curr_loc)
|
||||
if self._curr_loc:
|
||||
set_pause_state(False)
|
||||
res = self._andariel.battle(not self._pre_buffed)
|
||||
self._ending_run_helper(res)
|
||||
def on_run_countess(self):
|
||||
res = False
|
||||
self._do_runs["run_countess"] = False
|
||||
self._game_stats.update_location("Cou")
|
||||
self._curr_loc = self._countess.approach(self._curr_loc)
|
||||
if self._curr_loc:
|
||||
set_pause_state(False)
|
||||
res = self._countess.battle(not self._pre_buffed)
|
||||
self._ending_run_helper(res)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
from .amazon import Amazon
|
||||
from .javazon import Javazon
|
||||
@@ -0,0 +1,31 @@
|
||||
from input_layer import keyboard
|
||||
from ui import skills
|
||||
import time
|
||||
import random
|
||||
from input_layer import mouse
|
||||
from char import IChar, CharacterCapabilities
|
||||
from pather import Pather
|
||||
from logger import Logger
|
||||
from config import Config
|
||||
from utils.misc import wait
|
||||
from screen import convert_abs_to_screen, convert_abs_to_monitor
|
||||
from pather import Pather
|
||||
|
||||
class Amazon(IChar):
|
||||
def __init__(self, skill_hotkeys: dict, pather: Pather):
|
||||
Logger.info("Setting up Amazon")
|
||||
super().__init__(skill_hotkeys)
|
||||
self._pather = pather
|
||||
|
||||
def _cast_valkyrie(self, cast_pos_abs: tuple[float, float], spray: float = 10):
|
||||
if self._skill_hotkeys["valkyrie"]:
|
||||
keyboard.send(self._skill_hotkeys["valkyrie"])
|
||||
x = cast_pos_abs[0] + (random.random() * 2 * spray - spray)
|
||||
y = cast_pos_abs[1] + (random.random() * 2 * spray - spray)
|
||||
cast_pos_monitor = convert_abs_to_monitor((x, y))
|
||||
mouse.move(*cast_pos_monitor)
|
||||
|
||||
mouse.press(button="right")
|
||||
wait(0.06, 0.08)
|
||||
mouse.release(button="right")
|
||||
wait(self._cast_duration-0.06)
|
||||
@@ -0,0 +1,94 @@
|
||||
import random
|
||||
from input_layer import keyboard
|
||||
import time
|
||||
import numpy as np
|
||||
|
||||
from health_manager import get_panel_check_paused, set_panel_check_paused
|
||||
from inventory.personal import inspect_items
|
||||
from screen import convert_abs_to_monitor, convert_screen_to_abs, grab, convert_abs_to_screen
|
||||
from input_layer import mouse
|
||||
from char.amazon import Amazon
|
||||
from logger import Logger
|
||||
from config import Config
|
||||
from utils.misc import wait
|
||||
from pather import Location
|
||||
from target_detect import get_visible_targets, TargetInfo, log_targets
|
||||
|
||||
class Javazon(Amazon):
|
||||
def __init__(self, *args, **kwargs):
|
||||
Logger.info("Setting up Javazon")
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _cast_lightning_fury(self, cast_pos_abs: tuple[float, float], spray: float = 10, duration: float = 0.3):
|
||||
if not self._skill_hotkeys["lightning_fury"]:
|
||||
raise ValueError("You did not set a hotkey for lightning_fury!")
|
||||
keyboard.send(self._skill_hotkeys["lightning_fury"])
|
||||
|
||||
#Get mouse in initial target logcation
|
||||
x = cast_pos_abs[0] + (random.random() * 2 * spray - spray)
|
||||
y = cast_pos_abs[1] + (random.random() * 2 * spray - spray)
|
||||
cast_pos_monitor = convert_abs_to_monitor((x, y))
|
||||
mouse.move(*cast_pos_monitor)
|
||||
|
||||
#We just hold down right mouse button to ensure we get full attack speed.
|
||||
mouse.press(button="right")
|
||||
now = start = time.time()
|
||||
while (now - start) < duration:
|
||||
x = cast_pos_abs[0] + (random.random() * 2 * spray - spray)
|
||||
y = cast_pos_abs[1] + (random.random() * 2 * spray - spray)
|
||||
cast_pos_monitor = convert_abs_to_monitor((x, y))
|
||||
mouse.move(*cast_pos_monitor)
|
||||
wait(0.03, 0.36)
|
||||
now = time.time()
|
||||
mouse.release(button="right")
|
||||
|
||||
#wait 14 frames (14/25) which is slowest attack speed to ensure amazon is ready
|
||||
wait(0.56)
|
||||
|
||||
def kill_pindle(self) -> bool:
|
||||
atk_len_dur = float(Config().char["atk_len_pindle"])
|
||||
pindle_pos_abs = convert_screen_to_abs(Config().path["pindle_end"][0])
|
||||
|
||||
cast_pos_abs = [pindle_pos_abs[0] * 0.80, pindle_pos_abs[1] * 0.80]
|
||||
self._cast_valkyrie(cast_pos_abs=cast_pos_abs)
|
||||
self._cast_lightning_fury(cast_pos_abs=cast_pos_abs, duration=atk_len_dur)
|
||||
|
||||
if self.capabilities.can_teleport_natively:
|
||||
self._pather.traverse_nodes_fixed("pindle_end", self)
|
||||
else:
|
||||
#Walk just close enough to pindle's position to see items. That way we leave sooner if there is nothing to grab
|
||||
x_m, y_m = convert_abs_to_monitor([pindle_pos_abs[0] * 0.65, pindle_pos_abs[1] * 0.65])
|
||||
self.move((x_m, y_m), force_move=True)
|
||||
return True
|
||||
|
||||
def kill_eldritch(self) -> bool:
|
||||
eldritch_pos_abs = convert_screen_to_abs(Config().path["eldritch_end"][0])
|
||||
cast_pos_abs = [eldritch_pos_abs[0] * 1.5, eldritch_pos_abs[1] * 1.5]
|
||||
|
||||
self._cast_valkyrie(cast_pos_abs=cast_pos_abs)
|
||||
self._cast_lightning_fury(cast_pos_abs=cast_pos_abs, duration=Config().char["atk_len_eldritch"])
|
||||
|
||||
if self.capabilities.can_teleport_natively:
|
||||
self._pather.traverse_nodes_fixed("eldritch_end", self)
|
||||
else:
|
||||
x_m, y_m = convert_abs_to_monitor([eldritch_pos_abs[0] * 1.0, eldritch_pos_abs[1] * 1.0])
|
||||
self.move((x_m, y_m), force_move=True)
|
||||
return True
|
||||
|
||||
def kill_shenk(self) -> bool:
|
||||
shenk_pos_abs = convert_screen_to_abs(Config().path["shenk_end"][0])
|
||||
cast_pos_abs = [shenk_pos_abs[0] * 1.5, shenk_pos_abs[1] * 1.5]
|
||||
|
||||
self._cast_valkyrie(cast_pos_abs=cast_pos_abs)
|
||||
self._cast_lightning_fury(cast_pos_abs=cast_pos_abs, duration=Config().char["atk_len_shenk"])
|
||||
|
||||
if self.capabilities.can_teleport_natively:
|
||||
self._pather.traverse_nodes_fixed("shenk_end", self)
|
||||
else:
|
||||
x_m, y_m = convert_abs_to_monitor([shenk_pos_abs[0] * 1.0, shenk_pos_abs[1] * 1.0])
|
||||
self.move((x_m, y_m), force_move=True)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from ui import skills
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from char import IChar, CharacterCapabilities
|
||||
import template_finder
|
||||
from pather import Pather
|
||||
from logger import Logger
|
||||
from screen import convert_abs_to_monitor
|
||||
from screen import convert_abs_to_monitor, convert_screen_to_monitor
|
||||
from config import Config
|
||||
from utils.misc import wait
|
||||
import time
|
||||
@@ -23,55 +23,82 @@ class Barbarian(IChar):
|
||||
def _cast_war_cry(self, time_in_s: float):
|
||||
# keyboard.send(self._skill_hotkeys["concentration"])
|
||||
# wait(0.05, 0.1)
|
||||
cry_frequency = min(0.2, self._skill_hotkeys["cry_frequency"])
|
||||
keyboard.send(Config().char["stand_still"], do_release=False)
|
||||
wait(0.05, 0.1)
|
||||
if self._skill_hotkeys["war_cry"]:
|
||||
if not self._skill_hotkeys["war_cry"]:
|
||||
Logger.info('You did not set a hotkey for war_cry in params.ini!')
|
||||
else:
|
||||
keyboard.send(self._skill_hotkeys["war_cry"])
|
||||
wait(0.05, 0.1)
|
||||
start = time.time()
|
||||
while (time.time() - start) < time_in_s:
|
||||
wait(0.06, 0.08)
|
||||
mouse.click(button="right")
|
||||
wait(cry_frequency, cry_frequency + 0.2)
|
||||
mouse.click(button="right")
|
||||
wait(0.01, 0.05)
|
||||
keyboard.send(Config().char["stand_still"], do_press=False)
|
||||
wait(0.05, 0.05)
|
||||
start = time.time()
|
||||
while (time.time() - start) < time_in_s/2:
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration + 0.02,self._cast_duration + 0.02)
|
||||
if self._skill_hotkeys["howl"]:
|
||||
keyboard.send(self._skill_hotkeys["howl"])
|
||||
wait(0.05, 0.05)
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration + 0.02,self._cast_duration + 0.02)
|
||||
keyboard.send(self._skill_hotkeys["war_cry"])
|
||||
wait(0.05, 0.05)
|
||||
start = time.time()
|
||||
while (time.time() - start) < time_in_s/2:
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration + 0.02,self._cast_duration + 0.02)
|
||||
|
||||
|
||||
def on_capabilities_discovered(self, capabilities: CharacterCapabilities):
|
||||
if capabilities.can_teleport_natively:
|
||||
self._pather.offset_node(149, [120, 70])
|
||||
|
||||
def _do_hork(self, hork_time: int):
|
||||
wait(0.5)
|
||||
if self._skill_hotkeys["find_item"]:
|
||||
keyboard.send(self._skill_hotkeys["find_item"])
|
||||
wait(0.5, 0.15)
|
||||
pos_m = convert_abs_to_monitor((0, -20))
|
||||
mouse.move(*pos_m)
|
||||
wait(0.5, 0.15)
|
||||
mouse.press(button="right")
|
||||
wait(hork_time)
|
||||
mouse.release(button="right")
|
||||
wait(1)
|
||||
if self._skill_hotkeys["howl"]:
|
||||
keyboard.send(self._skill_hotkeys["howl"])
|
||||
wait(0.05, 0.05)
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration + 0.02,self._cast_duration + 0.02)
|
||||
keyboard.send(self._skill_hotkeys["find_item"])
|
||||
pos_m = convert_abs_to_monitor((0, -20))
|
||||
mouse.move(*pos_m)
|
||||
wait(0.05, 0.1)
|
||||
start = time.time()
|
||||
while ((time.time()-start) < hork_time):
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration + 0.02,self._cast_duration + 0.02)
|
||||
keyboard.send(Config().char["stand_still"], do_release=False)
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration + 0.02,self._cast_duration + 0.02)
|
||||
keyboard.send(Config().char["stand_still"], do_press=False)
|
||||
if self._skill_hotkeys["howl"]:
|
||||
keyboard.send(self._skill_hotkeys["howl"])
|
||||
wait(0.05, 0.05)
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration + 0.02,self._cast_duration + 0.02)
|
||||
|
||||
def pre_buff(self):
|
||||
keyboard.send(Config().char["battle_command"])
|
||||
wait(0.08, 0.19)
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration + 0.08, self._cast_duration + 0.1)
|
||||
keyboard.send(Config().char["battle_orders"])
|
||||
wait(0.08, 0.19)
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration + 0.08, self._cast_duration + 0.1)
|
||||
keyboard.send(self._skill_hotkeys["shout"])
|
||||
wait(0.08, 0.19)
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration + 0.08, self._cast_duration + 0.1)
|
||||
def cast_buffs(self, casting_delay: float):
|
||||
if self._skill_hotkeys["howl"]:
|
||||
keyboard.send(self._skill_hotkeys["howl"])
|
||||
wait(0.05,0.05)
|
||||
mouse.click(button="right")
|
||||
wait(casting_delay + 0.02, casting_delay + 0.02)
|
||||
if Config().char["battle_command"]:
|
||||
keyboard.send(Config().char["battle_command"])
|
||||
wait(0.05,0.05)
|
||||
mouse.click(button="right")
|
||||
wait(casting_delay + 0.02, casting_delay + 0.02)
|
||||
if Config().char["battle_orders"]:
|
||||
keyboard.send(Config().char["battle_orders"])
|
||||
wait(0.05,0.05)
|
||||
mouse.click(button="right")
|
||||
wait(casting_delay + 0.02, casting_delay + 0.02)
|
||||
if self._skill_hotkeys["shout"]:
|
||||
keyboard.send(self._skill_hotkeys["shout"])
|
||||
wait(0.05,0.05)
|
||||
mouse.click(button="right")
|
||||
wait(casting_delay + 0.02, casting_delay + 0.02)
|
||||
|
||||
def pre_move(self):
|
||||
def pre_move(self, wait_tp: bool = False):
|
||||
# select teleport if available
|
||||
super().pre_move()
|
||||
super().pre_move(wait_tp)
|
||||
# in case teleport hotkey is not set or teleport can not be used, use leap if set
|
||||
should_cast_leap = self._skill_hotkeys["leap"] and not skills.is_left_skill_selected(["LEAP"])
|
||||
can_teleport = self.capabilities.can_teleport_natively and skills.is_right_skill_active()
|
||||
@@ -89,12 +116,14 @@ class Barbarian(IChar):
|
||||
wait(0.1, 0.15)
|
||||
if self.capabilities.can_teleport_natively:
|
||||
self._pather.traverse_nodes_fixed("pindle_end", self)
|
||||
elif self._skill_hotkeys["leap"]:
|
||||
leap_pos_monitor = convert_screen_to_monitor(Config().path["pindle_end"][0])
|
||||
mouse.move(*leap_pos_monitor)
|
||||
mouse.click(button="right")
|
||||
wait(1.5, 1.5)
|
||||
else:
|
||||
if not self._do_pre_move:
|
||||
# keyboard.send(self._skill_hotkeys["concentration"])
|
||||
# wait(0.05, 0.15)
|
||||
self._pather.traverse_nodes((Location.A5_PINDLE_SAFE_DIST, Location.A5_PINDLE_END), self, timeout=1.0, do_pre_move=self._do_pre_move)
|
||||
self._pather.traverse_nodes((Location.A5_PINDLE_SAFE_DIST, Location.A5_PINDLE_END), self, timeout=0.1)
|
||||
self._cast_war_cry(Config().char["atk_len_pindle"])
|
||||
wait(0.1, 0.15)
|
||||
self._do_hork(4)
|
||||
@@ -102,13 +131,17 @@ class Barbarian(IChar):
|
||||
|
||||
def kill_eldritch(self) -> bool:
|
||||
if self.capabilities.can_teleport_natively:
|
||||
self._pather.traverse_nodes_fixed("eldritch_end", self)
|
||||
self._pather.traverse_nodes_fixed([(675, 30)], self)
|
||||
elif self._skill_hotkeys["leap"]:
|
||||
leap_pos_monitor = convert_screen_to_monitor((675,30))
|
||||
mouse.move(*leap_pos_monitor)
|
||||
mouse.click(button="right")
|
||||
wait(1.5, 1.5)
|
||||
else:
|
||||
self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, timeout=1.0, do_pre_move=self._do_pre_move)
|
||||
wait(0.05, 0.1)
|
||||
self._cast_war_cry(Config().char["atk_len_eldritch"])
|
||||
wait(0.1, 0.15)
|
||||
self._do_hork(4)
|
||||
wait(0.05, 0.1)
|
||||
self._do_hork(6)
|
||||
return True
|
||||
|
||||
def kill_shenk(self):
|
||||
@@ -159,7 +192,7 @@ class Barbarian(IChar):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
from config import Config
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from ui import skills
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from char import IChar,CharacterCapabilities
|
||||
import template_finder
|
||||
from pather import Pather
|
||||
@@ -42,19 +42,17 @@ class Basic(IChar):
|
||||
wait(0.01, 0.05)
|
||||
keyboard.send(Config().char["stand_still"], do_press=False)
|
||||
|
||||
def pre_buff(self):
|
||||
if Config().char["cta_available"]:
|
||||
self._pre_buff_cta()
|
||||
def cast_buffs(self, casting_delay: float):
|
||||
if self._skill_hotkeys["buff_1"]:
|
||||
keyboard.send(self._skill_hotkeys["buff_1"])
|
||||
wait(0.5, 0.15)
|
||||
mouse.click(button="right")
|
||||
wait(0.5, 0.15)
|
||||
wait(casting_delay, casting_delay)
|
||||
if self._skill_hotkeys["buff_2"]:
|
||||
keyboard.send(self._skill_hotkeys["buff_2"])
|
||||
wait(0.5, 0.15)
|
||||
mouse.click(button="right")
|
||||
wait(0.5, 0.15)
|
||||
wait(casting_delay, casting_delay)
|
||||
|
||||
def pre_move(self):
|
||||
# select teleport if available
|
||||
@@ -143,7 +141,7 @@ class Basic(IChar):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
from config import Config
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from ui import skills
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from char import IChar
|
||||
import template_finder
|
||||
from pather import Pather
|
||||
@@ -42,19 +42,17 @@ class Basic_Ranged(IChar):
|
||||
mouse.move(*cast_pos_monitor)
|
||||
mouse.click(button="right")
|
||||
|
||||
def pre_buff(self):
|
||||
if Config().char["cta_available"]:
|
||||
self._pre_buff_cta()
|
||||
def cast_buffs(self, casting_delay: float):
|
||||
if self._skill_hotkeys["buff_1"]:
|
||||
keyboard.send(self._skill_hotkeys["buff_1"])
|
||||
wait(0.5, 0.15)
|
||||
mouse.click(button="right")
|
||||
wait(0.5, 0.15)
|
||||
wait(casting_delay, casting_delay)
|
||||
if self._skill_hotkeys["buff_2"]:
|
||||
keyboard.send(self._skill_hotkeys["buff_2"])
|
||||
wait(0.5, 0.15)
|
||||
mouse.click(button="right")
|
||||
wait(0.5, 0.15)
|
||||
wait(casting_delay, casting_delay)
|
||||
|
||||
|
||||
#bosses
|
||||
@@ -191,7 +189,7 @@ class Basic_Ranged(IChar):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import template_finder
|
||||
from pather import Pather
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import keyboard
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import keyboard
|
||||
from input_layer import mouse
|
||||
from char import IChar
|
||||
import template_finder
|
||||
from pather import Pather
|
||||
@@ -44,12 +44,17 @@ class Bone_Necro(IChar):
|
||||
mouse.release(button="right")
|
||||
keyboard.send(Config().char["stand_still"], do_press=False)
|
||||
|
||||
def pre_buff(self):
|
||||
self.bone_armor()
|
||||
#only CTA if pre trav
|
||||
if Config().char["cta_available"]:
|
||||
self._pre_buff_cta()
|
||||
Logger.info("prebuff/cta")
|
||||
def cast_buffs(self, casting_delay: float):
|
||||
if self._skill_hotkeys["bone_armor"]:
|
||||
keyboard.send(self._skill_hotkeys["bone_armor"])
|
||||
wait(0.04, 0.1)
|
||||
mouse.click(button="right")
|
||||
wait(casting_delay)
|
||||
if self._skill_hotkeys["clay_golem"]:
|
||||
keyboard.send(self._skill_hotkeys["clay_golem"])
|
||||
wait(0.04, 0.1)
|
||||
mouse.click(button="right")
|
||||
wait(casting_delay)
|
||||
|
||||
def _clay_golem(self):
|
||||
Logger.debug('Casting clay golem')
|
||||
@@ -58,18 +63,6 @@ class Bone_Necro(IChar):
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration)
|
||||
|
||||
def bone_armor(self):
|
||||
if self._skill_hotkeys["bone_armor"]:
|
||||
keyboard.send(self._skill_hotkeys["bone_armor"])
|
||||
wait(0.04, 0.1)
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration)
|
||||
if self._skill_hotkeys["clay_golem"]:
|
||||
keyboard.send(self._skill_hotkeys["clay_golem"])
|
||||
wait(0.04, 0.1)
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration)
|
||||
|
||||
def _bone_armor(self):
|
||||
if self._skill_hotkeys["bone_armor"]:
|
||||
keyboard.send(self._skill_hotkeys["bone_armor"])
|
||||
@@ -214,7 +207,7 @@ class Bone_Necro(IChar):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
from config import Config
|
||||
|
||||
@@ -4,12 +4,12 @@ import time
|
||||
import cv2
|
||||
import math
|
||||
from item import consumables
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import numpy as np
|
||||
from char.capabilities import CharacterCapabilities
|
||||
from ui_manager import is_visible, wait_until_visible
|
||||
from ui import skills
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from utils.misc import wait, cut_roi, is_in_roi, color_filter, arc_spread
|
||||
from logger import Logger
|
||||
from config import Config
|
||||
@@ -23,8 +23,11 @@ class IChar:
|
||||
def __init__(self, skill_hotkeys: dict):
|
||||
self._skill_hotkeys = skill_hotkeys
|
||||
self._last_tp = time.time()
|
||||
self._stationary = False #Are we guaranteed stationary while picking up items
|
||||
# Add a bit to be on the save side
|
||||
self._cast_duration = Config().char["casting_frames"] * 0.04 + 0.01
|
||||
self._cta_cast_duration = Config().char["cta_casting_frames"] * 0.04 + 0.01
|
||||
self._attack_duration = Config().char["attack_frames"] * 0.04 + 0.01
|
||||
self.damage_scaling = float(Config().char.get("damage_scaling", 1.0))
|
||||
self.capabilities = None
|
||||
self._active_skill = {
|
||||
@@ -87,12 +90,39 @@ class IChar:
|
||||
def on_capabilities_discovered(self, capabilities: CharacterCapabilities):
|
||||
pass
|
||||
|
||||
def pick_up_item(self, pos: tuple[float, float], item_name: str = None, prev_cast_start: float = 0):
|
||||
mouse.move(pos[0], pos[1])
|
||||
time.sleep(0.1)
|
||||
mouse.click(button="left")
|
||||
wait(0.25, 0.3)
|
||||
return prev_cast_start
|
||||
def pick_up_item(self, pos: tuple[float, float], item_name: str = None, distance: int = 0, force_run: bool = False):
|
||||
if not force_run and self.capabilities.can_teleport_natively and (distance >= Config().ui_pos["item_dist"]) and item_name != "Chest":
|
||||
self.pre_move()
|
||||
self.move(pos, force_tp=True)
|
||||
self._stationary = True #We teleported so we should be guaranteed stationary now
|
||||
#We teleported on top of the item, but there are no guarantees exactly where the item is located.
|
||||
#Return false to indicate that we've not yet picked up the item.
|
||||
return False
|
||||
else:
|
||||
mouse.move(pos[0], pos[1])
|
||||
wait(0.1, 0.12)
|
||||
mouse.click(button="left")
|
||||
|
||||
#Current logic only sets force_run if we previously teled.
|
||||
#Therefore, we only need to perform run delay if force_run is not set
|
||||
if not force_run:
|
||||
#When character moves, it typically doesn't move less than 30
|
||||
if distance < 30:
|
||||
distance = 30
|
||||
|
||||
#By default we ignore pickup distance and do a full delay in case we misclick
|
||||
pickup_distance = 0
|
||||
|
||||
#If we are stationary, we can account for pickup distance to potentially allow instant pickups
|
||||
if self._stationary:
|
||||
pickup_distance = 110 #we can instant pickup items 110 units away
|
||||
|
||||
if distance > pickup_distance:
|
||||
run_time = 0.003125 * (distance-pickup_distance) #estimated run speed of (2sec/640pixels) = 0.003125
|
||||
wait(run_time, run_time) #wait until we move to item and pick it up
|
||||
|
||||
self._stationary = False #Moving so we don't guarantee stationary state (delay is accurate not exorbitant)
|
||||
return True
|
||||
|
||||
def select_by_template(
|
||||
self,
|
||||
@@ -172,11 +202,14 @@ class IChar:
|
||||
def select_tp(self):
|
||||
return skills.select_tp(Config().char["teleport"])
|
||||
|
||||
def pre_move(self):
|
||||
def pre_move(self, wait_tp: bool = False):
|
||||
# if teleport hotkey is set and if teleport is not already selected
|
||||
if self.capabilities.can_teleport_natively:
|
||||
self.select_tp()
|
||||
self._set_active_skill("right", "teleport")
|
||||
if wait_tp:
|
||||
if skills.wait_until_right_skill_selected("TELE_ACTIVE") == False:
|
||||
Logger.error("timeout waiting for tele skill to activate")
|
||||
|
||||
def move(self, pos_monitor: tuple[float, float], force_tp: bool = False, force_move: bool = False):
|
||||
factor = Config().advanced_options["pathing_delay_factor"]
|
||||
@@ -278,34 +311,44 @@ class IChar:
|
||||
while time.time() - start < 4:
|
||||
keyboard.send(Config().char["weapon_switch"])
|
||||
wait(0.3, 0.35)
|
||||
self._select_skill(skill = "battle_command", mouse_click_type="right", delay=(0.1, 0.2))
|
||||
keyboard.send(Config().char["battle_command"])
|
||||
wait(0.1,0.2)
|
||||
if skills.is_right_skill_selected(["BC", "BO"]):
|
||||
switch_sucess = True
|
||||
break
|
||||
else:
|
||||
Logger.warning("Failed to find Battle Command, swapping weapons again.")
|
||||
skill_before = cut_roi(grab(), Config().ui_roi["skill_right"])
|
||||
|
||||
if not switch_sucess:
|
||||
Logger.warning("You dont have Battle Command bound, or you do not have CTA. ending CTA buff")
|
||||
Config().char["cta_available"] = 0
|
||||
Logger.error("You dont have Battle Command bound, or you do not have CTA. Ending CTA buff")
|
||||
return False
|
||||
else:
|
||||
# We switched succesfully, let's pre buff
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration + 0.16, self._cast_duration + 0.18)
|
||||
wait(self._cta_cast_duration, self._cta_cast_duration)
|
||||
self._select_skill(skill = "battle_orders", mouse_click_type="right", delay=(0.1, 0.2))
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration + 0.16, self._cast_duration + 0.18)
|
||||
wait(self._cta_cast_duration, self._cta_cast_duration)
|
||||
if Config().char["buff_with_cta"]:
|
||||
self.cast_buffs(self._cta_cast_duration)
|
||||
wait(0.08,0.08) #Give extra time after last cast to allow weapon switch to be available
|
||||
|
||||
# Make sure the switch back to the original weapon is good
|
||||
start = time.time()
|
||||
switch_sucess = False
|
||||
while time.time() - start < 4:
|
||||
keyboard.send(Config().char["weapon_switch"])
|
||||
wait(0.3, 0.35)
|
||||
skill_after = cut_roi(grab(), Config().ui_roi["skill_right"])
|
||||
_, max_val, _, _ = cv2.minMaxLoc(cv2.matchTemplate(skill_after, skill_before, cv2.TM_CCOEFF_NORMED))
|
||||
if max_val > 0.9:
|
||||
switch_sucess = True
|
||||
break
|
||||
else:
|
||||
Logger.warning("Failed to switch weapon, try again")
|
||||
wait(0.5)
|
||||
return switch_sucess
|
||||
|
||||
|
||||
def vec_to_monitor(self, target):
|
||||
@@ -347,6 +390,18 @@ class IChar:
|
||||
|
||||
|
||||
def pre_buff(self):
|
||||
if Config().char["cta_available"]:
|
||||
if not self._pre_buff_cta():
|
||||
return False
|
||||
#If we do not have a CTA, or we are not buffing with CTA; we buff now that we swapped cta out
|
||||
if not Config().char["cta_available"] or not Config().char["buff_with_cta"]:
|
||||
self.cast_buffs(self._cast_duration)
|
||||
return True
|
||||
|
||||
def cast_buffs(self, casting_delay: float):
|
||||
pass
|
||||
|
||||
def cast_town_buffs(self, curr_loc):
|
||||
pass
|
||||
|
||||
def kill_pindle(self) -> bool:
|
||||
@@ -382,9 +437,24 @@ class IChar:
|
||||
def kill_cs_trash(self, location:str) -> bool:
|
||||
raise ValueError("Diablo CS Trash is not implemented!")
|
||||
|
||||
def kill_andariel(self) -> bool:
|
||||
raise ValueError("Andariel is not implemented!")
|
||||
|
||||
def kill_countess(self) -> bool:
|
||||
raise ValueError("Countess is not implemented!")
|
||||
|
||||
def kill_mephisto(self) -> bool:
|
||||
raise ValueError("Mephisto is not implemented!")
|
||||
|
||||
def kill_baal(self) -> bool:
|
||||
raise ValueError("Baal is not implemented!")
|
||||
|
||||
def kill_baal_waves(self, duration: int = 45) -> bool:
|
||||
raise ValueError("Baal wave clearing is not implemented!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
keyboard.add_hotkey('f12', lambda: os._exit(1))
|
||||
print(f"Get on D2R screen and press F11 when ready")
|
||||
keyboard.wait("f11")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import keyboard
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import keyboard
|
||||
from input_layer import mouse
|
||||
from char import IChar
|
||||
import template_finder
|
||||
from template_finder import TemplateMatch
|
||||
@@ -238,10 +238,7 @@ class Necro(IChar):
|
||||
keyboard.send(Config().char["stand_still"], do_press=False)
|
||||
|
||||
|
||||
def pre_buff(self):
|
||||
#only CTA if pre trav
|
||||
if Config().char["cta_available"]:
|
||||
self._pre_buff_cta()
|
||||
def cast_buffs(self, casting_delay: float):
|
||||
if self._shenk_dead==1:
|
||||
Logger.info("trav buff?")
|
||||
#self._heart_of_wolverine()
|
||||
@@ -860,7 +857,7 @@ class Necro(IChar):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
from config import Config
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import random
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import time
|
||||
import numpy as np
|
||||
|
||||
from health_manager import get_panel_check_paused, set_panel_check_paused
|
||||
from inventory.personal import inspect_items
|
||||
from screen import convert_abs_to_monitor, convert_screen_to_abs, grab, convert_abs_to_screen
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from char.paladin import Paladin
|
||||
from logger import Logger
|
||||
from config import Config
|
||||
@@ -19,10 +19,19 @@ class FoHdin(Paladin):
|
||||
Logger.info("Setting up FoHdin")
|
||||
super().__init__(*args, **kwargs)
|
||||
self._pather.adapt_path((Location.A3_TRAV_START, Location.A3_TRAV_CENTER_STAIRS), [220, 221, 222, 903, 904, 905, 906])
|
||||
|
||||
self._foh_cycle_duration = 0.4 + (self._action_frame/25.0)
|
||||
self._last_foh_cast = 0
|
||||
|
||||
def _cast_foh(self, cast_pos_abs: tuple[float, float], spray: int = 10, min_duration: float = 0, aura: str = "conviction"):
|
||||
return self._cast_skill_with_aura(skill_name = "foh", cast_pos_abs = cast_pos_abs, spray = spray, min_duration = min_duration, aura = aura)
|
||||
#wait one frame until the foh cooldown has expired
|
||||
now = time.time()
|
||||
while (now - self._last_foh_cast) < self._foh_cycle_duration:
|
||||
wait(0.04)
|
||||
now = time.time()
|
||||
self._last_foh_cast = now
|
||||
rt = self._cast_skill_with_aura(skill_name = "foh", cast_pos_abs = cast_pos_abs, spray = spray, min_duration = min_duration, aura = aura)
|
||||
self._last_foh_cast = self._last_click_cast
|
||||
return rt
|
||||
|
||||
def _cast_holy_bolt(self, cast_pos_abs: tuple[float, float], spray: int = 10, min_duration: float = 0, aura: str = "concentration"):
|
||||
#if skill is bound : concentration, use concentration, otherwise move on with conviction. alternatively use redemption whilst holybolting. conviction does not help holy bolt (its magic damage)
|
||||
@@ -37,6 +46,25 @@ class FoHdin(Paladin):
|
||||
self.move(pos_m, force_move=True)
|
||||
self._cast_hammers(atk_len, aura=aura)
|
||||
|
||||
def _foh_clear_screen(self, duration: float = 1.0, spray: int = 10, aura: str = "conviction"):
|
||||
|
||||
start = time.time()
|
||||
|
||||
while (time.time() - start) < duration:
|
||||
#Cast 3 fohs each 120 degrees apart
|
||||
self._cast_foh([0,-200], spray=spray, aura=aura) #Top 90 degrees
|
||||
self._cast_foh([-173,100], spray=spray, aura=aura) #Bot-left 210 degrees
|
||||
self._cast_foh([173,100], spray=spray, aura=aura) #Bot-right 330 degrees
|
||||
|
||||
#Stop mid sequence if time expired
|
||||
if (time.time() - start) > duration:
|
||||
break
|
||||
|
||||
#Cast 3 more fohs 120 degrees apart with 60 degree offset from above
|
||||
self._cast_foh([-173,-100], spray=spray, aura=aura) #Top-left 150 degrees
|
||||
self._cast_foh([173,-100], spray=spray, aura=aura) #Top-right 30 degrees
|
||||
self._cast_foh([0,200], spray=spray, aura=aura) #Bot 270 degrees
|
||||
|
||||
def _generic_foh_attack_sequence(
|
||||
self,
|
||||
default_target_abs: tuple[int, int] = (0, 0),
|
||||
@@ -73,11 +101,15 @@ class FoHdin(Paladin):
|
||||
self._cast_foh(cast_pos_abs, spray=spray, aura=foh_aura)
|
||||
|
||||
target_check_count += 1
|
||||
|
||||
#We always want to end with selecting foh, as holy bolt can cause char move issues with merc targeting.
|
||||
self._select_skill("foh", mouse_click_type = "left")
|
||||
|
||||
return True
|
||||
|
||||
#FOHdin Attack Sequence Optimized for trash
|
||||
def _cs_attack_sequence(self, min_duration: float = Config().char["atk_len_cs_trashmobs"], max_duration: float = Config().char["atk_len_cs_trashmobs"] * 3):
|
||||
self._generic_foh_attack_sequence(default_target_abs=(20,20), min_duration = min_duration, max_duration = max_duration, default_spray=100, foh_to_holy_bolt_ratio=6)
|
||||
self._foh_clear_screen(duration = max_duration)
|
||||
self._activate_redemption_aura()
|
||||
|
||||
def _cs_trash_mobs_attack_sequence(self, min_duration: float = 1.2, max_duration: float = Config().char["atk_len_cs_trashmobs"]):
|
||||
@@ -96,36 +128,23 @@ class FoHdin(Paladin):
|
||||
atk_len_dur = float(Config().char["atk_len_pindle"])
|
||||
pindle_pos_abs = convert_screen_to_abs(Config().path["pindle_end"][0])
|
||||
|
||||
if (self.capabilities.can_teleport_natively or self.capabilities.can_teleport_with_charges) and self._use_safer_routines:
|
||||
# Slightly retreating, so the Merc gets charged
|
||||
if not self._pather.traverse_nodes([102], self, timeout=1.0, do_pre_move=False, force_move=True,force_tp=False, use_tp_charge=False):
|
||||
return False
|
||||
# Doing one Teleport to safe_dist to grab our Merc
|
||||
Logger.debug("Teleporting backwards to let Pindle charge the MERC. Looks strange, but is intended!") #I would leave this message in, so users dont complain that there is a strange movement pattern.
|
||||
if not self._pather.traverse_nodes([103], self, timeout=1.0, do_pre_move=False, force_tp=True, use_tp_charge=True):
|
||||
return False
|
||||
# Slightly retreating, so the Merc gets charged
|
||||
if not self._pather.traverse_nodes([103], self, timeout=1.0, do_pre_move=False, force_move=True, force_tp=False, use_tp_charge=False):
|
||||
return False
|
||||
else:
|
||||
keyboard.send(self._skill_hotkeys["conviction"])
|
||||
wait(0.15)
|
||||
self._pather.traverse_nodes([103], self, timeout=1.0, do_pre_move=False)
|
||||
|
||||
cast_pos_abs = [pindle_pos_abs[0] * 0.9, pindle_pos_abs[1] * 0.9]
|
||||
self._generic_foh_attack_sequence(default_target_abs=cast_pos_abs, min_duration=atk_len_dur, max_duration=atk_len_dur*3, default_spray=11)
|
||||
cast_pos_abs = [pindle_pos_abs[0] * 0.80, pindle_pos_abs[1] * 0.80]
|
||||
self._generic_foh_attack_sequence(default_target_abs=cast_pos_abs, min_duration=atk_len_dur, max_duration=atk_len_dur, default_spray=10, target_detect=False, foh_to_holy_bolt_ratio=1)
|
||||
|
||||
if self.capabilities.can_teleport_natively:
|
||||
self._pather.traverse_nodes_fixed("pindle_end", self)
|
||||
else:
|
||||
keyboard.send(self._skill_hotkeys["redemption"])
|
||||
if Config().fohdin["redemption"]:
|
||||
keyboard.send(self._skill_hotkeys["redemption"])
|
||||
wait(0.15)
|
||||
self._pather.traverse_nodes((Location.A5_PINDLE_SAFE_DIST, Location.A5_PINDLE_END), self, timeout=1.0, do_pre_move=False)
|
||||
#Walk just close enough to pindle's position to see items. That way we leave sooner if there is nothing to grab
|
||||
x_m, y_m = convert_abs_to_monitor([pindle_pos_abs[0] * 0.65, pindle_pos_abs[1] * 0.65])
|
||||
self.move((x_m, y_m), force_move=True)
|
||||
|
||||
# Use target-based attack sequence one more time before pickit
|
||||
self._generic_foh_attack_sequence(default_target_abs=cast_pos_abs, max_duration=atk_len_dur, default_spray=11)
|
||||
self._activate_cleanse_redemption()
|
||||
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -759,4 +778,52 @@ class FoHdin(Paladin):
|
||||
self._activate_cleanse_redemption()
|
||||
### LOOT ###
|
||||
#self._cs_pickit()
|
||||
return True
|
||||
|
||||
def kill_andariel(self) -> bool:
|
||||
atk_len = float(Config().char.get("atk_len_andariel", 4.0))
|
||||
Logger.info("Killing Andariel")
|
||||
self._generic_foh_attack_sequence(default_target_abs=(0, 0), min_duration=atk_len, max_duration=atk_len*2, default_spray=20)
|
||||
self._move_and_attack((30, 15), atk_len * 0.3, aura="redemption")
|
||||
self._move_and_attack((-30, -15), atk_len * 0.3, aura="redemption")
|
||||
self._activate_cleanse_redemption()
|
||||
return True
|
||||
|
||||
def kill_countess(self) -> bool:
|
||||
atk_len = float(Config().char.get("atk_len_countess", 3.0))
|
||||
Logger.info("Killing Countess")
|
||||
self._generic_foh_attack_sequence(default_target_abs=(0, 0), min_duration=atk_len, max_duration=atk_len*2, default_spray=20)
|
||||
self._move_and_attack((30, 15), atk_len * 0.3, aura="redemption")
|
||||
self._activate_cleanse_redemption()
|
||||
return True
|
||||
|
||||
def kill_mephisto(self) -> bool:
|
||||
atk_len = float(Config().char.get("atk_len_mephisto", 12.0))
|
||||
Logger.info("Killing Mephisto")
|
||||
self._generic_foh_attack_sequence(default_target_abs=(0, 0), min_duration=atk_len, max_duration=atk_len*2, default_spray=30)
|
||||
self._move_and_attack((30, 15), atk_len * 0.3, aura="redemption")
|
||||
self._move_and_attack((-30, -15), atk_len * 0.3, aura="redemption")
|
||||
self._activate_cleanse_redemption()
|
||||
return True
|
||||
|
||||
def kill_baal(self) -> bool:
|
||||
atk_len = float(Config().char.get("atk_len_baal", 10.0))
|
||||
Logger.info("Killing Baal")
|
||||
self._generic_foh_attack_sequence(default_target_abs=(0, 0), min_duration=atk_len, max_duration=atk_len*2, default_spray=30)
|
||||
self._move_and_attack((30, 15), atk_len * 0.3, aura="redemption")
|
||||
self._move_and_attack((-30, -15), atk_len * 0.3, aura="redemption")
|
||||
self._activate_cleanse_redemption()
|
||||
return True
|
||||
|
||||
def kill_baal_waves(self, duration: int = 45) -> bool:
|
||||
atk_len = float(Config().char.get("atk_len_baal_waves", 30.0))
|
||||
Logger.info("Clearing Baal waves")
|
||||
self._foh_clear_screen(duration=atk_len * 0.3)
|
||||
self._activate_redemption_aura()
|
||||
self._foh_clear_screen(duration=atk_len * 0.3)
|
||||
self._activate_redemption_aura()
|
||||
self._foh_clear_screen(duration=atk_len * 0.3)
|
||||
self._activate_redemption_aura()
|
||||
self._foh_clear_screen(duration=atk_len * 0.1)
|
||||
self._activate_cleanse_redemption()
|
||||
return True
|
||||
@@ -1,4 +1,4 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import random
|
||||
import time
|
||||
|
||||
@@ -12,7 +12,7 @@ from pather import Pather, Location
|
||||
from screen import convert_abs_to_monitor, convert_screen_to_abs, grab
|
||||
from target_detect import get_visible_targets
|
||||
from ui import skills
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from utils.misc import wait
|
||||
|
||||
class Hammerdin(Paladin):
|
||||
@@ -40,17 +40,15 @@ class Hammerdin(Paladin):
|
||||
wait(0.01, 0.05)
|
||||
keyboard.send(Config().char["stand_still"], do_press=False)
|
||||
|
||||
def pre_buff(self):
|
||||
if Config().char["cta_available"]:
|
||||
self._pre_buff_cta()
|
||||
def cast_buffs(self, casting_delay: float):
|
||||
keyboard.send(self._skill_hotkeys["holy_shield"])
|
||||
wait(0.04, 0.1)
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration, self._cast_duration + 0.06)
|
||||
wait(casting_delay, casting_delay + 0.06)
|
||||
|
||||
def pre_move(self):
|
||||
def pre_move(self, wait_tp: bool = False):
|
||||
# select teleport if available
|
||||
super().pre_move()
|
||||
super().pre_move(wait_tp=wait_tp)
|
||||
# in case teleport hotkey is not set or teleport can not be used, use vigor if set
|
||||
should_cast_vigor = self._skill_hotkeys["vigor"] and not skills.is_right_skill_selected(["VIGOR"])
|
||||
can_teleport = self.capabilities.can_teleport_natively and skills.is_right_skill_active()
|
||||
@@ -1263,10 +1261,71 @@ class Hammerdin(Paladin):
|
||||
self._picked_up_items |= self._pickit.pick_up_items(self)
|
||||
return True
|
||||
|
||||
def kill_andariel(self) -> bool:
|
||||
atk_len = float(Config().char.get("atk_len_andariel", 4.0))
|
||||
pos_m = convert_abs_to_monitor((0, 0))
|
||||
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
|
||||
self._cast_hammers(atk_len)
|
||||
self._cast_hammers(0.8, "redemption")
|
||||
self._move_and_attack((30, 15), atk_len * 0.3)
|
||||
self._cast_hammers(0.6, "redemption")
|
||||
self._move_and_attack((-30, -15), atk_len * 0.3)
|
||||
self._cast_hammers(0.8, "redemption")
|
||||
return True
|
||||
|
||||
def kill_countess(self) -> bool:
|
||||
atk_len = float(Config().char.get("atk_len_countess", 3.0))
|
||||
pos_m = convert_abs_to_monitor((0, 0))
|
||||
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
|
||||
self._cast_hammers(atk_len)
|
||||
self._cast_hammers(0.8, "redemption")
|
||||
self._move_and_attack((30, 15), atk_len * 0.3)
|
||||
self._cast_hammers(0.6, "redemption")
|
||||
return True
|
||||
|
||||
def kill_mephisto(self) -> bool:
|
||||
atk_len = float(Config().char.get("atk_len_mephisto", 12.0))
|
||||
pos_m = convert_abs_to_monitor((0, 0))
|
||||
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
|
||||
self._cast_hammers(atk_len)
|
||||
self._cast_hammers(0.8, "redemption")
|
||||
self._move_and_attack((30, 15), atk_len * 0.4)
|
||||
self._cast_hammers(0.8, "redemption")
|
||||
self._move_and_attack((-30, -15), atk_len * 0.4)
|
||||
self._cast_hammers(1.2, "redemption")
|
||||
return True
|
||||
|
||||
def kill_baal(self) -> bool:
|
||||
atk_len = float(Config().char.get("atk_len_baal", 10.0))
|
||||
pos_m = convert_abs_to_monitor((0, 0))
|
||||
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
|
||||
self._cast_hammers(atk_len)
|
||||
self._cast_hammers(0.8, "redemption")
|
||||
self._move_and_attack((30, 15), atk_len * 0.4)
|
||||
self._cast_hammers(0.8, "redemption")
|
||||
self._move_and_attack((-30, -15), atk_len * 0.4)
|
||||
wait(0.1, 0.15)
|
||||
self._cast_hammers(1.2, "redemption")
|
||||
return True
|
||||
|
||||
def kill_baal_waves(self, duration: int = 45) -> bool:
|
||||
atk_len = float(Config().char.get("atk_len_baal_waves", 30.0))
|
||||
pos_m = convert_abs_to_monitor((0, 0))
|
||||
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
|
||||
self._move_and_attack((30, 15), atk_len * 0.3)
|
||||
self._cast_hammers(0.75, "redemption")
|
||||
self._move_and_attack((-50, -15), atk_len * 0.3)
|
||||
self._cast_hammers(0.75, "redemption")
|
||||
self._move_and_attack((-30, -15), atk_len * 0.3)
|
||||
self._cast_hammers(0.75, "redemption")
|
||||
self._move_and_attack((30, -15), atk_len * 0.1)
|
||||
self._cast_hammers(1.0, "redemption")
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
from config import Config
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from ui import skills
|
||||
import time
|
||||
import random
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from char import IChar, CharacterCapabilities
|
||||
from pather import Pather
|
||||
from logger import Logger
|
||||
@@ -20,18 +20,24 @@ class Paladin(IChar):
|
||||
self._pather = pather
|
||||
self._pickit = pickit #for Diablo
|
||||
self._picked_up_items = False #for Diablo
|
||||
self._last_click_cast = 0
|
||||
self._action_frame = 9
|
||||
if Config().char["casting_frames"] == 13 or Config().char["casting_frames"] == 12:
|
||||
self._action_frame = 8
|
||||
elif Config().char["casting_frames"] == 11 or Config().char["casting_frames"] == 10:
|
||||
self._action_frame = 7
|
||||
elif Config().char["casting_frames"] == 9:
|
||||
self._action_frame = 6
|
||||
|
||||
def pre_buff(self):
|
||||
if Config().char["cta_available"]:
|
||||
self._pre_buff_cta()
|
||||
def cast_buffs(self, casting_delay: float):
|
||||
keyboard.send(self._skill_hotkeys["holy_shield"])
|
||||
wait(0.04, 0.1)
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration, self._cast_duration + 0.06)
|
||||
wait(casting_delay, casting_delay + 0.06)
|
||||
|
||||
def pre_move(self):
|
||||
def pre_move(self, wait_tp: bool = False):
|
||||
# select teleport if available
|
||||
super().pre_move()
|
||||
super().pre_move(wait_tp=wait_tp)
|
||||
# in case teleport hotkey is not set or teleport can not be used, use vigor if set
|
||||
should_cast_vigor = self._skill_hotkeys["vigor"] and not skills.is_right_skill_selected(["VIGOR"])
|
||||
can_teleport = self.capabilities.can_teleport_natively and skills.is_right_skill_active()
|
||||
@@ -60,10 +66,11 @@ class Paladin(IChar):
|
||||
y += (random.random() * 2 * spray - spray)
|
||||
pos_m = convert_abs_to_monitor((x, y))
|
||||
mouse.move(*pos_m, delay_factor=[0.1, 0.2])
|
||||
wait(0.06, 0.08)
|
||||
mouse.press(button = mouse_click_type)
|
||||
self._last_click_cast = time.time()
|
||||
wait(0.06, 0.08)
|
||||
mouse.release(button = mouse_click_type)
|
||||
wait(Config().char["casting_frames"]/25.0 - 0.06)
|
||||
|
||||
def _cast_skill_with_aura(self, skill_name: str, cast_pos_abs: tuple[float, float] = None, spray: int = 0, min_duration: float = 0, aura: str = ""):
|
||||
#self._log_cast(skill_name, cast_pos_abs, spray, min_duration, aura)
|
||||
@@ -77,11 +84,10 @@ class Paladin(IChar):
|
||||
|
||||
# set left hand skill
|
||||
self._select_skill(skill_name, mouse_click_type = "left")
|
||||
wait(0.04)
|
||||
|
||||
# cast left hand skill
|
||||
start = time.time()
|
||||
if min_duration:
|
||||
start = time.time()
|
||||
while (time.time() - start) <= min_duration:
|
||||
self._click_cast(cast_pos_abs, spray)
|
||||
else:
|
||||
@@ -97,5 +103,7 @@ class Paladin(IChar):
|
||||
self._select_skill("cleansing", delay=delay)
|
||||
|
||||
def _activate_cleanse_redemption(self):
|
||||
self._activate_cleanse_aura()
|
||||
self._activate_redemption_aura()
|
||||
if Config().fohdin["cleansing"]:
|
||||
self._activate_cleanse_aura()
|
||||
if Config().fohdin["redemption"]:
|
||||
self._activate_redemption_aura()
|
||||
@@ -1,5 +1,5 @@
|
||||
import keyboard
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import keyboard
|
||||
from input_layer import mouse
|
||||
from char import IChar
|
||||
import template_finder
|
||||
from pather import Pather
|
||||
@@ -246,10 +246,7 @@ class Poison_Necro(IChar):
|
||||
keyboard.send(Config().char["stand_still"], do_press=False)
|
||||
|
||||
|
||||
def pre_buff(self):
|
||||
#only CTA if pre trav
|
||||
if Config().char["cta_available"]:
|
||||
self._pre_buff_cta()
|
||||
def cast_buffs(self, casting_delay: float):
|
||||
if self._shenk_dead==1:
|
||||
Logger.info("trav buff?")
|
||||
#self._heart_of_wolverine()
|
||||
@@ -544,7 +541,7 @@ class Poison_Necro(IChar):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
from config import Config
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .sorceress import Sorceress
|
||||
from .light_sorc import LightSorc
|
||||
from .blizz_sorc import BlizzSorc
|
||||
from .blizzorb_sorc import BlizzorbSorc
|
||||
from .nova_sorc import NovaSorc
|
||||
from .hydra_sorc import HydraSorc
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from char.sorceress import Sorceress
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from logger import Logger
|
||||
from utils.misc import wait, rotate_vec, unit_vector
|
||||
import random
|
||||
@@ -256,7 +256,7 @@ class BlizzSorc(Sorceress):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import template_finder
|
||||
from pather import Pather
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
from input_layer import keyboard
|
||||
from char.sorceress import Sorceress
|
||||
from input_layer import mouse
|
||||
from logger import Logger
|
||||
from utils.misc import wait, rotate_vec, unit_vector
|
||||
import random
|
||||
import time
|
||||
from pather import Location
|
||||
import numpy as np
|
||||
from screen import convert_abs_to_monitor, grab, convert_screen_to_abs
|
||||
from config import Config
|
||||
import template_finder
|
||||
|
||||
class BlizzorbSorc(Sorceress):
|
||||
def __init__(self, *args, **kwargs):
|
||||
Logger.info("Setting up Blizzorb Sorc")
|
||||
super().__init__(*args, **kwargs)
|
||||
self._orb_cycle_duration = 1.0 + (self._action_frame/25.0)
|
||||
self._last_orb_cast = 0
|
||||
self._blizz_cycle_duration = 1.8 + (self._action_frame/25.0)
|
||||
self._last_blizz_cast = 0
|
||||
|
||||
def _cast_blizzard(self, cast_pos_abs: tuple[float, float], spray: float = 10):
|
||||
if not self._skill_hotkeys["blizzard"]:
|
||||
raise ValueError("You did not set a hotkey for blizzard!")
|
||||
keyboard.send(self._skill_hotkeys["blizzard"])
|
||||
x = cast_pos_abs[0] + (random.random() * 2 * spray - spray)
|
||||
y = cast_pos_abs[1] + (random.random() * 2 * spray - spray)
|
||||
cast_pos_monitor = convert_abs_to_monitor((x, y))
|
||||
mouse.move(*cast_pos_monitor)
|
||||
|
||||
#wait for blizzard cooldown
|
||||
now = time.time()
|
||||
while (now - self._last_blizz_cast) < self._blizz_cycle_duration:
|
||||
wait(0.04)
|
||||
now = time.time()
|
||||
self._last_blizz_cast = now
|
||||
|
||||
mouse.press(button="right")
|
||||
wait(0.06, 0.08)
|
||||
mouse.release(button="right")
|
||||
wait(self._cast_duration-0.06)
|
||||
|
||||
def _cast_frozen_orb(self, cast_pos_abs: tuple[float, float], spray: float = 10):
|
||||
keyboard.send(Config().char["stand_still"], do_release=False)
|
||||
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
|
||||
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
|
||||
cast_pos_monitor = convert_abs_to_monitor((x, y))
|
||||
mouse.move(*cast_pos_monitor)
|
||||
|
||||
#wait for frozen orb cooldown
|
||||
now = time.time()
|
||||
while (now - self._last_orb_cast) < self._orb_cycle_duration:
|
||||
wait(0.04)
|
||||
now = time.time()
|
||||
self._last_orb_cast = now
|
||||
|
||||
mouse.press(button="left")
|
||||
wait(0.06, 0.08)
|
||||
mouse.release(button="left")
|
||||
keyboard.send(Config().char["stand_still"], do_press=False)
|
||||
wait(self._cast_duration-0.06)
|
||||
|
||||
def _cast_glacial_spike(self, cast_pos_abs: tuple[float, float], spray: float = 10, times=1):
|
||||
if not self._skill_hotkeys["glacial_spike"]:
|
||||
raise ValueError("You did not set a hotkey for glacial spike!")
|
||||
keyboard.send(self._skill_hotkeys["glacial_spike"])
|
||||
for _ in range(times):
|
||||
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
|
||||
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
|
||||
cast_pos_monitor = convert_abs_to_monitor((x, y))
|
||||
mouse.move(*cast_pos_monitor)
|
||||
mouse.press(button="right")
|
||||
wait(0.06, 0.08)
|
||||
mouse.release(button="right")
|
||||
wait(self._cast_duration-0.06)
|
||||
|
||||
def _cast_static_field(self, times=1):
|
||||
if self._skill_hotkeys["static_field"]:
|
||||
keyboard.send(self._skill_hotkeys["static_field"])
|
||||
|
||||
#Static field can fail to cast if we right click on a wall. We move mouse to
|
||||
#center to prevent this.
|
||||
cast_pos_monitor = convert_abs_to_monitor((0, 0))
|
||||
mouse.move(*cast_pos_monitor)
|
||||
|
||||
for _ in range(times):
|
||||
mouse.press(button="right")
|
||||
wait(0.06, 0.08)
|
||||
mouse.release(button="right")
|
||||
wait(self._cast_duration-0.06)
|
||||
|
||||
def _cast_blizzorb_spike_combo(self, cast_pos_abs: tuple[float, float], spray: float = 10):
|
||||
self._cast_blizzard(cast_pos_abs,spray)
|
||||
self._cast_frozen_orb(cast_pos_abs,spray)
|
||||
self._cast_glacial_spike(cast_pos_abs,spray, times=2)
|
||||
self._cast_frozen_orb(cast_pos_abs,spray)
|
||||
|
||||
def _cast_blizzorb_whirlwind_combo(self, spray: float = 10):
|
||||
self._cast_blizzard([0, 0],spray)
|
||||
self._cast_frozen_orb([40, -40],spray)
|
||||
self._cast_static_field(times=2)
|
||||
self._cast_frozen_orb([-40,40],spray)
|
||||
self._cast_blizzard([0, 0],spray)
|
||||
self._cast_frozen_orb([-40,-40],spray)
|
||||
self._cast_static_field(times=2)
|
||||
self._cast_frozen_orb([40,40],spray)
|
||||
|
||||
def kill_pindle(self) -> bool:
|
||||
pindle_pos_abs = convert_screen_to_abs(Config().path["pindle_end"][0])
|
||||
cast_pos_abs = [pindle_pos_abs[0] * 0.9, pindle_pos_abs[1] * 0.9]
|
||||
start = time.time()
|
||||
while (time.time() - start) < Config().char["atk_len_pindle"]:
|
||||
self._cast_blizzorb_spike_combo(cast_pos_abs, spray=0)
|
||||
|
||||
self._pather.traverse_nodes_fixed("pindle_end", self)
|
||||
return True
|
||||
|
||||
def kill_eldritch(self) -> bool:
|
||||
eldritch_pos_abs = convert_screen_to_abs(Config().path["eldritch_end"][0])
|
||||
cast_pos_abs = [eldritch_pos_abs[0] * 1.00, eldritch_pos_abs[1] * 1.00]
|
||||
self._pather.traverse_nodes_fixed([(675, 30)], self)
|
||||
|
||||
start = time.time()
|
||||
while (time.time() - start) < Config().char["atk_len_eldritch"]:
|
||||
self._cast_blizzard([40,-100],spray=0)
|
||||
self._cast_frozen_orb([80,-200],spray=0)
|
||||
self._cast_static_field(times=3)
|
||||
self._cast_frozen_orb([80,-200],spray=0)
|
||||
#self._cast_blizzorb_spike_combo(cast_pos_abs, spray=0)
|
||||
|
||||
#Need to cast blizzard closer now that Eldritch approaches
|
||||
#cast_pos_abs = [eldritch_pos_abs[0] * 1.3, eldritch_pos_abs[1] * 1.3]
|
||||
|
||||
#while (time.time() - start) < Config().char["atk_len_eldritch"]:
|
||||
# self._cast_blizzorb_spike_combo(cast_pos_abs, spray=0)
|
||||
#self._pather.traverse_nodes_fixed("eldritch_end", self)
|
||||
return True
|
||||
|
||||
def kill_shenk(self) -> bool:
|
||||
shenk_pos_abs = convert_screen_to_abs(Config().path["shenk_end"][0])
|
||||
cast_pos_abs = [shenk_pos_abs[0] * 1.5, shenk_pos_abs[1] * 1.5]
|
||||
|
||||
start = time.time()
|
||||
|
||||
#First, cast blizzard once we are in range
|
||||
self._cast_blizzard(cast_pos_abs, spray=0)
|
||||
|
||||
#Next, tele next to shenk to put orb and static in range
|
||||
pos_m = convert_abs_to_monitor((shenk_pos_abs[0] * 1.1, shenk_pos_abs[1] * 1.1))
|
||||
self.pre_move(wait_tp=True)
|
||||
self.move(pos_m, force_move=True)
|
||||
|
||||
#Adjust cast position now that we are next to shenk and cast orb/static
|
||||
cast_pos_abs = [shenk_pos_abs[0] * 0.45, (shenk_pos_abs[1] * 0.45)-50]
|
||||
self._cast_frozen_orb(cast_pos_abs, spray=0)
|
||||
self._cast_static_field(times=3)
|
||||
|
||||
#Continue blizzorb combo until attack length expires
|
||||
while (time.time() - start) < Config().char["atk_len_shenk"]:
|
||||
self._cast_blizzorb_spike_combo(cast_pos_abs, spray=0)
|
||||
|
||||
return True
|
||||
|
||||
def kill_nihlathak(self, end_nodes: list[int]) -> bool:
|
||||
# Find nilhlatak position
|
||||
nihlathak_pos_abs = self._pather.find_abs_node_pos(end_nodes[-1], grab())
|
||||
if nihlathak_pos_abs is not None:
|
||||
cast_pos_abs = np.array([nihlathak_pos_abs[0] * 1.0, nihlathak_pos_abs[1] * 1.0])
|
||||
start = time.time()
|
||||
|
||||
#First, cast blizzard once we are in range
|
||||
self._cast_blizzard(cast_pos_abs, spray=0)
|
||||
|
||||
#Next, tele next to nihlathak to put orb and static in range
|
||||
pos_m = convert_abs_to_monitor((nihlathak_pos_abs[0] * 0.6, nihlathak_pos_abs[1] * 0.6))
|
||||
self.pre_move(wait_tp=True)
|
||||
self.move(pos_m, force_move=True)
|
||||
|
||||
#Adjust cast position now that we are next to nihlathak and cast orb/static
|
||||
cast_pos_abs = [nihlathak_pos_abs[0] * 0.4, nihlathak_pos_abs[1] * 0.4]
|
||||
self._cast_frozen_orb(cast_pos_abs, spray=0)
|
||||
self._cast_static_field(times=3)
|
||||
|
||||
#Continue blizzorb combo until attack length expires
|
||||
while (time.time() - start) < Config().char["atk_len_nihlathak"]:
|
||||
self._cast_blizzorb_spike_combo(cast_pos_abs, spray=0)
|
||||
self._pather.traverse_nodes(end_nodes, self, timeout=0.8)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def kill_council(self) -> bool:
|
||||
def clear_inside(use_static: bool):
|
||||
self._pather.traverse_nodes([228,229], self, timeout=2.2, do_pre_move=False, force_tp=True, use_tp_charge=True)
|
||||
if use_static:
|
||||
self._cast_static_field(2)
|
||||
self._cast_blizzorb_spike_combo([-80,-60])
|
||||
|
||||
def clear_outside(use_static: bool):
|
||||
self._pather.traverse_nodes_fixed([(430, 642)], self)
|
||||
if use_static:
|
||||
self._cast_static_field(2)
|
||||
self._cast_blizzorb_spike_combo([-80,-60])
|
||||
|
||||
start = time.time()
|
||||
|
||||
clear_inside(True)
|
||||
clear_outside(True)
|
||||
|
||||
while (time.time() - start) < Config().char["atk_len_trav"]:
|
||||
clear_inside(False)
|
||||
clear_outside(False)
|
||||
|
||||
return True
|
||||
|
||||
def kill_summoner(self) -> bool:
|
||||
#Set cast position right below alter
|
||||
cast_pos_abs = [0, 20]
|
||||
start = time.time()
|
||||
while (time.time() - start) < Config().char["atk_len_arc"]:
|
||||
self._cast_blizzorb_whirlwind_combo(cast_pos_abs, spray=0)
|
||||
return True
|
||||
|
||||
def kill_cs_trash(self, location:str) -> bool:
|
||||
#Set cast position right below alter
|
||||
start = time.time()
|
||||
while (time.time() - start) < Config().char["atk_len_cs_trashmobs"]:
|
||||
self._cast_blizzorb_whirlwind_combo(spray=0)
|
||||
return True
|
||||
|
||||
def kill_vizier(self, seal_layout:str) -> bool:
|
||||
#Set cast position right below alter
|
||||
start = time.time()
|
||||
while (time.time() - start) < Config().char["atk_len_diablo_vizier"]:
|
||||
self._cast_blizzorb_whirlwind_combo(spray=0)
|
||||
return True
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import time
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from char.sorceress import Sorceress
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from logger import Logger
|
||||
from utils.misc import wait
|
||||
import random
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from char.sorceress import Sorceress
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from logger import Logger
|
||||
from utils.misc import wait, rotate_vec, unit_vector
|
||||
import random
|
||||
@@ -241,7 +241,7 @@ class LightSorc(Sorceress):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from pather import Pather
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import time
|
||||
import numpy as np
|
||||
from char.sorceress import Sorceress
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from logger import Logger
|
||||
from utils.misc import wait
|
||||
from pather import Location
|
||||
from screen import convert_abs_to_monitor
|
||||
from screen import convert_abs_to_monitor, grab
|
||||
from config import Config
|
||||
|
||||
import cv2
|
||||
|
||||
class NovaSorc(Sorceress):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -24,10 +24,10 @@ class NovaSorc(Sorceress):
|
||||
wait(0.05, 0.1)
|
||||
start = time.time()
|
||||
while (time.time() - start) < time_in_s:
|
||||
wait(0.03, 0.04)
|
||||
mouse.press(button="right")
|
||||
wait(0.12, 0.2)
|
||||
wait(0.06, 0.08)
|
||||
mouse.release(button="right")
|
||||
wait(self._cast_duration-0.06)
|
||||
|
||||
def _move_and_attack(self, abs_move: tuple[int, int], atk_len: float):
|
||||
pos_m = convert_abs_to_monitor(abs_move)
|
||||
@@ -37,7 +37,6 @@ class NovaSorc(Sorceress):
|
||||
|
||||
def kill_pindle(self) -> bool:
|
||||
self._pather.traverse_nodes_fixed("pindle_end", self)
|
||||
self._cast_static(0.6)
|
||||
self._nova(Config().char["atk_len_pindle"])
|
||||
return True
|
||||
|
||||
@@ -54,29 +53,23 @@ class NovaSorc(Sorceress):
|
||||
return True
|
||||
|
||||
def kill_council(self) -> bool:
|
||||
# Check out the node screenshot in assets/templates/trav/nodes to see where each node is at
|
||||
atk_len = Config().char["atk_len_trav"] * 0.21
|
||||
# change node to be further to the right
|
||||
offset_229 = np.array([200, 100])
|
||||
self._pather.offset_node(229, offset_229)
|
||||
def clear_inside():
|
||||
self._pather.traverse_nodes_fixed([(1110, 120)], self)
|
||||
self._pather.traverse_nodes([229], self, timeout=0.8, force_tp=True)
|
||||
atk_len = Config().char["atk_len_trav"]
|
||||
self._cast_static(0.6)
|
||||
def clear_inside(use_static: bool):
|
||||
self._pather.traverse_nodes_fixed([(875, 20)], self)
|
||||
if use_static:
|
||||
self._cast_static(0.6)
|
||||
self._nova(atk_len)
|
||||
self._move_and_attack((-40, -20), atk_len)
|
||||
self._move_and_attack((40, 20), atk_len)
|
||||
self._move_and_attack((40, 20), atk_len)
|
||||
|
||||
def clear_outside():
|
||||
self._pather.traverse_nodes([226], self, timeout=0.8, force_tp=True)
|
||||
self._pather.traverse_nodes_fixed([(430, 642)], self)
|
||||
self._nova(atk_len)
|
||||
self._move_and_attack((45, -20), atk_len)
|
||||
self._move_and_attack((-45, 20), atk_len)
|
||||
clear_inside()
|
||||
|
||||
clear_inside(True)
|
||||
clear_outside()
|
||||
clear_inside()
|
||||
clear_inside(False)
|
||||
clear_outside()
|
||||
# change back node as it is used in trav.py
|
||||
self._pather.offset_node(229, -offset_229)
|
||||
|
||||
return True
|
||||
|
||||
def kill_nihlathak(self, end_nodes: list[int]) -> bool:
|
||||
@@ -107,7 +100,7 @@ class NovaSorc(Sorceress):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from pather import Pather
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from typing import Callable
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from char import IChar
|
||||
import template_finder
|
||||
from pather import Pather
|
||||
@@ -10,28 +10,32 @@ import time
|
||||
from pather import Pather
|
||||
from config import Config
|
||||
from ui_manager import ScreenObjects, is_visible
|
||||
from screen import convert_abs_to_monitor
|
||||
|
||||
class Sorceress(IChar):
|
||||
def __init__(self, skill_hotkeys: dict, pather: Pather):
|
||||
super().__init__(skill_hotkeys)
|
||||
self._pather = pather
|
||||
self._action_frame = 7
|
||||
if Config().char["casting_frames"] == 11 or Config().char["casting_frames"] == 10:
|
||||
self._action_frame = 6
|
||||
elif Config().char["casting_frames"] == 9 or Config().char["casting_frames"] == 8:
|
||||
self._action_frame = 5
|
||||
elif Config().char["casting_frames"] == 7:
|
||||
self._action_frame = 4
|
||||
self._action_duration = self._action_frame * 0.04
|
||||
|
||||
def pick_up_item(self, pos: tuple[float, float], item_name: str = None, prev_cast_start: float = 0):
|
||||
if self._skill_hotkeys["telekinesis"] and any(x in item_name for x in ['potion', 'misc_gold', 'tp_scroll']):
|
||||
def pick_up_item(self, pos: tuple[float, float], item_name: str = None, distance: int = 0, force_run: bool = False):
|
||||
if (distance < 600) and self._skill_hotkeys["telekinesis"] and any(x in item_name for x in ['Potion', 'GOLD', 'Scroll of', 'Chest']):
|
||||
keyboard.send(self._skill_hotkeys["telekinesis"])
|
||||
wait(0.1, 0.2)
|
||||
mouse.move(pos[0], pos[1])
|
||||
wait(0.1, 0.2)
|
||||
wait(0.1, 0.1)
|
||||
mouse.click(button="right")
|
||||
# need about 0.4s delay before next capture for the item not to persist on screen
|
||||
cast_start = time.time()
|
||||
interval = (cast_start - prev_cast_start)
|
||||
cast_duration_wait = (self._cast_duration - interval)
|
||||
delay = 0.35 if cast_duration_wait <0 else (0.35+cast_duration_wait)
|
||||
wait(delay,delay+0.1)
|
||||
return cast_start
|
||||
wait(self._cast_duration,self._cast_duration)
|
||||
self._stationary = True #We used telekinesis so we should be guaranteed stationary now
|
||||
return True
|
||||
else:
|
||||
return super().pick_up_item(pos, item_name, prev_cast_start)
|
||||
return super().pick_up_item(pos, item_name, distance, force_run)
|
||||
|
||||
def select_by_template(
|
||||
self,
|
||||
@@ -65,30 +69,35 @@ class Sorceress(IChar):
|
||||
# In case telekinesis fails, try again with the base implementation
|
||||
return super().select_by_template(template_type, success_func, timeout, threshold)
|
||||
|
||||
def pre_buff(self):
|
||||
if Config().char["cta_available"]:
|
||||
self._pre_buff_cta()
|
||||
def cast_buffs(self, casting_delay: float):
|
||||
if self._skill_hotkeys["energy_shield"]:
|
||||
keyboard.send(self._skill_hotkeys["energy_shield"])
|
||||
wait(0.1, 0.13)
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration)
|
||||
wait(casting_delay)
|
||||
if self._skill_hotkeys["thunder_storm"]:
|
||||
keyboard.send(self._skill_hotkeys["thunder_storm"])
|
||||
wait(0.1, 0.13)
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration)
|
||||
wait(casting_delay)
|
||||
if self._skill_hotkeys["frozen_armor"]:
|
||||
keyboard.send(self._skill_hotkeys["frozen_armor"])
|
||||
wait(0.1, 0.13)
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration)
|
||||
wait(casting_delay)
|
||||
|
||||
def _cast_static(self, duration: float = 1.4):
|
||||
if self._skill_hotkeys["static_field"]:
|
||||
keyboard.send(self._skill_hotkeys["static_field"])
|
||||
wait(0.1, 0.13)
|
||||
|
||||
#Static field can fail to cast if we right click on a wall. We move mouse to
|
||||
#center to prevent this.
|
||||
cast_pos_monitor = convert_abs_to_monitor((0, 0))
|
||||
mouse.move(*cast_pos_monitor)
|
||||
|
||||
start = time.time()
|
||||
while time.time() - start < duration:
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration)
|
||||
mouse.press(button="right")
|
||||
wait(0.06, 0.06)
|
||||
mouse.release(button="right")
|
||||
wait(self._cast_duration-0.06)
|
||||
|
||||