diff --git a/README.md b/README.md
index f3098b5..a51a6a3 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,43 @@
-> [!WARNING]
-> This script is designed for 1920x1080 display. If you are using a higher resolution display please set it to 1920x1080 in windows' display settings.
+# AFK Script for Dead by Daylight
+
+This script is designed to automatically farm XP and a few bloodpoints while avoiding detection:
+
+- **Anti-AFK Proof**: Ensures consistent actions to prevent AFK kicks.
+- **Banner-Proof**: Handles cases where survivors disconnect during loading screens.
+- **Killer Behaviors**: Implements various killer strategies to maximize bloodpoints gain.
+- **XP Gain**: Achieves up to 4000 XP per hour (excluding queue times).
+- **Configurable Settings**: Includes settings for customization (more to come).
+- **Testing Results**: Tested for 5 hours, yielding ~11,000 XP (queue times took half of that).
+
+
+
+
+## Statistics
+
+(still gathering)
+
+| | Per game | Per hour (in-game) |
+|-------------------------|----------|--------------------|
+| XP (ex. 1st game bonus) | | |
+| Bloodpoints as OTHER | | |
+| Bloodpoints as TRAPPER | | |
+
+
+## Behaviors
+
+All killers follow a basic movement pattern by moving back and forth. This helps bypass the anti-AFK system introduced by BHVR, which disconnects you from the game if no movement is detected. You can still interact with survivors (e.g., hit them), but without any movement, after the game ends you'll be treated like you disconnected (no xp, and matchmaking ban).
+
+
+| Killer | Actions |
+|---------|--------------------------------|
+| TRAPPER | - Placing and picking up traps |
-# An AFK Script for Dead by Daylight
## Setup
+
+> [!WARNING]
+> This script is designed for 1920x1080 display. If you are using a higher resolution display please set it to 1920x1080 in windows' display settings.
+
1. Download and unpack this repository
2. Install python 3.11+
3. Install python requirements (setup.bat, first time only):
@@ -15,18 +49,20 @@ pip install -r requirements.txt
python run.py
```
-## Features
-- [X] Queueing
-- [X] Attacking
-- [X] Random Movement
-- [X] Statistics
-- [X] Different behaviour for different killers (for bloodpoints)
-- [ ] Settings (ui + saving)
+## Settings
+
+You can configure the script's behavior in the `src/config.py` file.
+
+| Variable | Description | Values |
+|-------------|----------------------------------------------------------|----------------|
+| killer | Select the killer you're using (with custom behavior) | OTHER, TRAPPER |
+| xp_limit | Script will exit after gaining this much XP | 0 to disable |
+| games_limit | Script will exit after playing this many games | 0 to disable |
+
+
+## Bans
-# Behaviors
-All killers walk forward-backward and attack. Some time ago BHVR added an anti-afk system which make you "DC" after the game (so you won't get xp and bloodpoints). You can even hit survivors, but if you didn't move an inch you will get kicked.
+There's no widespread evidence of players getting banned for AFK farming. Survivors often benefit from it as well (through healing, totems, etc.).
-| Killer | Actions |
-|-|-|
-| | - Placing and picking up traps
\ No newline at end of file
+However, **I am not responsible for any bans** that may occur.
diff --git a/run.py b/run.py
deleted file mode 100644
index 98edc26..0000000
--- a/run.py
+++ /dev/null
@@ -1,221 +0,0 @@
-# TODO:
-# - More killers: Dracula,
-
-import mss
-import pyautogui
-import easyocr
-import time
-from PIL import Image
-import os
-from enum import Enum
-import rich
-from rich.console import Console
-from rich.text import Text
-import random
-import datetime
-
-Killer = Enum('Killer', ['OTHER', "TRAPPER"])
-
-# CONFIGURATION
-
-killer = Killer.OTHER # From the list above
-stop_after_games = 100 # Script plays about 4-5 games per hour (10 minutes in game, 5 minutes in lobby)
-stop_after_xp = 20000 # Script gets around 2000xp per hour
-
-# END OF CONFIGURATION
-
-pyautogui.FAILSAFE = False
-
-lobby_button = {
- "top": 927,
- "left": 1580,
- "width": 235,
- "height": 35
-}
-
-endgame_button = {
- "top": 1000,
- "left": 1650,
- "width": 235,
- "height": 35
-}
-
-State = Enum('State', ['INGAME', 'INLOBBY', 'INQUEUE'])
-
-games = 0
-xp = 0
-total_time_in_game = 0
-
-def click_image():
- try:
- pos = pyautogui.locateOnScreen('screenshot.png', confidence=(0.8))
- if pos != None:
- pyautogui.click(pos[0] + 20, pos[1])
- time.sleep(0.5)
- except:
- pass
-
-def take_and_read_screenshot(area):
- screenshot = sct.grab(area)
- image = Image.frombytes("RGB", screenshot.size, screenshot.bgra, "raw", "BGRX")
- image.save("screenshot.png")
- return reader.readtext("screenshot.png", detail=0)
-
-def take_and_read_xp_screenshot():
- screenshot = sct.grab({
- "top": 700,
- "left": 540,
- "width": 75,
- "height": 35
- })
- image = Image.frombytes("RGB", screenshot.size, screenshot.bgra, "raw", "BGRX")
- image.save("last_game_xp.png")
- return reader.readtext("last_game_xp.png", detail=0)
-
-def perform_ingame_action():
- def walk_and_attack():
- pyautogui.keyDown("w")
- time.sleep(0.5)
- pyautogui.keyUp("w")
-
- pyautogui.keyDown("s")
- time.sleep(0.5)
- pyautogui.keyUp("s")
-
- # Coords of the "banner" (survivor disconneted etc.); if no banner is present, it just attacks like normal
- pyautogui.click(x=1379, y=658)
- pyautogui.click(x=1379, y=658)
- pyautogui.click(x=1379, y=658)
-
- def attack():
- pyautogui.mouseDown()
- time.sleep(0.5)
- pyautogui.mouseUp()
-
- console.log(Text("Performing random movement as ") + Text(f"{killer.name}", style="bold red"))
-
- if (killer == Killer.OTHER):
- walk_and_attack()
- elif (killer == Killer.TRAPPER):
- random_action = random.randint(0, 1)
- if random_action == 0:
- walk_and_attack()
- else:
- pyautogui.mouseDown(button="secondary")
- time.sleep(4)
- pyautogui.mouseUp(button="secondary")
- time.sleep(2)
- pyautogui.press("space")
- time.sleep(10)
-
-def print_stats():
- console.print("\n\n")
- console.log(Text(f" Game Time: ~{time_in_game / 60} minutes ", style="black on aqua"))
- console.log(Text(f" XP Earned: {xp} ({xp / games} average) ", style="black on yellow"))
- console.log(Text(f" Games Played: {games} ", style="black on green"))
- console.log(Text(f" Running Time: {str(datetime.timedelta(seconds=int(time.time() - script_start_time)))} ({str(datetime.timedelta(seconds=int(total_time_in_game)))} in games; {str(datetime.timedelta(seconds=int((time.time() - script_start_time) - total_time_in_game)))} in lobby)"))
- console.print("\n\n")
-
-console = Console()
-
-console.set_window_title("DBD Auto AFK by Pazdikan")
-console.clear()
-console.log(Text("Initializing..."))
-console.log(Text("! IGNORE ALL ERRORS BELOW !", style="bold black on red"))
-
-if __name__ == '__main__':
- script_start_time = time.time()
- current_state = State.INLOBBY
-
- game_started_at = None
-
- reader = easyocr.Reader(['en'])
-
- with mss.mss() as sct:
- console.clear()
- console.log(Text("Initialized!", style="green"))
- while True:
- if (games >= stop_after_games or xp >= stop_after_xp):
- console.log(Text("Stopping script due XP or Games limit set in the configuration", style="green"))
- print_stats()
- break
-
- if (current_state == State.INGAME):
- ocr = take_and_read_screenshot(endgame_button)
-
- if any("CONTINUE" in string for string in ocr):
- time_in_game = time.time() - game_started_at
-
- pyautogui.click(x=465, y=871)
- pyautogui.click(x=465, y=871)
- pyautogui.click(x=465, y=871)
-
- time.sleep(15)
-
- current_xp = take_and_read_xp_screenshot()
- if (len(current_xp) > 0):
- xp += int(current_xp[0])
- else:
- xp += time_in_game * 0.8 # 1 second in game = 1 xp; but the time isnt perfect so reduce the xp
-
- time.sleep(5)
-
- click_image()
- click_image()
- click_image()
- console.log("Clicking CONTINUE (endgame)")
- current_state = State.INLOBBY
-
- games += 1
- total_time_in_game += time_in_game
-
- print_stats()
-
- console.log("Setting state to INLOBBY")
- console.log("Waiting 30 seconds")
- time.sleep(30)
- console.log("Finished waiting")
- else:
- perform_ingame_action()
- else:
- # When you level up your rift, the right expandable menu will cover the play button
- # This moves the mouse to top left corner to close it
- pyautogui.moveTo(0, 0)
-
- ocr = take_and_read_screenshot(lobby_button)
-
- if any("PLAY" in string for string in ocr):
- click_image()
- click_image()
- click_image()
- console.log("Clicking PLAY in main menu")
-
- elif any("READY" in string for string in ocr):
- click_image()
- click_image()
- click_image()
- console.log("Clicking READY in found lobby")
- current_state = State.INGAME
- console.log("Setting state to INGAME")
- console.log("Waiting 120 seconds")
- time.sleep(120) # Loading from lobby to game (+ missing players etc.)
- console.log("Finished waiting")
- game_started_at = time.time()
-
- elif not ocr:
- ocr = take_and_read_screenshot(endgame_button)
-
- # Sometimes the game doesn't register the click
- # Script thinks it's in main menu but game is still in endgame
- if any("CONTINUE" in string for string in ocr):
- click_image()
- click_image()
- click_image()
- console.log("Clicking CONTINUE (endgame)")
-
- console.log("Waiting 30 seconds")
- time.sleep(30)
- console.log("Finished waiting")
-
- console.log("Waiting 5 seconds")
- time.sleep(5)
\ No newline at end of file
diff --git a/src/config.py b/src/config.py
new file mode 100644
index 0000000..f46a3df
--- /dev/null
+++ b/src/config.py
@@ -0,0 +1,29 @@
+from time import time
+from enum import Enum
+
+# THIS IS NOT THE CONFIGURATION YOU ARE LOOKING FOR
+# SCROLL DOWN TO FIND THE CONFIGURATION FOR USERS
+
+ss = None
+
+State = Enum('State', ['INGAME', 'INLOBBY', 'INQUEUE'])
+Killer = Enum('Killer', ['OTHER', "TRAPPER"])
+
+current_state = State.INLOBBY
+
+games = 0
+xp = 0
+total_time_in_game = 0
+
+script_start_time = time()
+game_started_at = None
+
+# CONFIGURATION FOR USERS
+# EDITING STUFF BETWEEN COMMENTS BELOW IS SAFE AND WILL NOT BREAK THE SCRIPT
+
+killer = Killer.TRAPPER # From the list above
+
+xp_limit = 0 # SET TO 0 TO DISABLE
+games_limit = 0 # SET TO 0 TO DISABLE
+
+# END OF CONFIGURATION
\ No newline at end of file
diff --git a/src/core/behavior.py b/src/core/behavior.py
new file mode 100644
index 0000000..12c719d
--- /dev/null
+++ b/src/core/behavior.py
@@ -0,0 +1,17 @@
+import config
+from random import randint
+
+import killer.universal as universal
+import killer.trapper as trapper
+
+def perform_ingame_action():
+ if (config.killer == config.Killer.OTHER):
+ universal.walk_and_attack()
+
+ elif (config.killer == config.Killer.TRAPPER):
+ random_action = randint(0, 5)
+
+ if random_action == 0:
+ universal.walk_and_attack()
+ else:
+ trapper.place_and_pick_trap()
\ No newline at end of file
diff --git a/src/core/brain.py b/src/core/brain.py
new file mode 100644
index 0000000..d0f2493
--- /dev/null
+++ b/src/core/brain.py
@@ -0,0 +1,102 @@
+import config
+from util.console import console, print_stats
+from rich.text import Text
+from time import time, sleep
+import pyautogui
+import core.behavior as behavior
+
+def check_if_limit_reached():
+ if (config.xp_limit > 0 and config.xp >= config.xp_limit):
+ console.log(Text("XP limit reached. The script will now turn off", style="green"))
+ return True
+
+ elif (config.games_limit > 0 and config.games >= config.games_limit):
+ console.log(Text("Games Played limit reached. The script will now turn off", style="green"))
+ return True
+
+ return False
+
+def loop():
+ if (check_if_limit_reached()):
+ quit()
+
+ if (config.current_state == config.State.INGAME):
+ ocr = config.ss.take_and_read_screenshot(config.ss.endgame_button)
+
+ if any("CONTINUE" in string for string in ocr):
+ time_in_game = time() - config.game_started_at
+
+ pyautogui.click(x=465, y=871)
+ pyautogui.click(x=465, y=871)
+ pyautogui.click(x=465, y=871)
+
+ sleep(15)
+ current_xp = config.ss.take_and_read_xp_screenshot()
+
+ if (len(current_xp) > 0):
+ config.xp += int(current_xp[0])
+ else:
+ config.xp += time_in_game * 0.8 # "Predicted" XP gain when the OCR fails
+
+ sleep(5)
+
+ config.ss.click_image()
+ config.ss.click_image()
+ config.ss.click_image()
+
+ console.log("Clicking CONTINUE (endgame)")
+ config.current_state = config.State.INLOBBY
+ config.games += 1
+ config.total_time_in_game += time_in_game
+
+ console.log(f"{Text('Game Finished! Earned XP: {current_xp}', style='green')}")
+ print_stats()
+
+ console.log("Setting state to INLOBBY")
+ console.log("Waiting 30 seconds")
+ sleep(30)
+ console.log("Finished waiting")
+ else:
+ behavior.perform_ingame_action()
+
+ else:
+ # When you level up your rift, the right expandable menu will cover the play button
+ # This moves the mouse to top left corner to close it
+ pyautogui.moveTo(0, 0)
+
+ ocr = config.ss.take_and_read_screenshot(config.ss.lobby_button)
+ if any("PLAY" in string for string in ocr):
+ config.ss.click_image()
+ config.ss.click_image()
+ config.ss.click_image()
+
+ console.log("Clicking PLAY in main menu")
+ elif any("READY" in string for string in ocr):
+ config.ss.click_image()
+ config.ss.click_image()
+ config.ss.click_image()
+
+ console.log("Clicking READY in found lobby")
+ config.current_state = config.State.INGAME
+ console.log("Setting state to INGAME")
+ console.log("Waiting 120 seconds")
+
+ sleep(120) # Loading from lobby to game (+ missing players etc.)
+ console.log("Finished waiting")
+
+ config.game_started_at = time()
+ elif not ocr:
+ ocr = config.ss.take_and_read_screenshot(config.ss.endgame_button)
+ # Sometimes the game doesn't register the click
+ # Script thinks it's in main menu but game is still in endgame
+ if any("CONTINUE" in string for string in ocr):
+ config.ss.click_image()
+ config.ss.click_image()
+ config.ss.click_image()
+
+ console.log("Clicking CONTINUE (endgame)")
+ console.log("Waiting 30 seconds")
+ sleep(30)
+ console.log("Finished waiting")
+ console.log("Waiting 5 seconds")
+ sleep(5)
\ No newline at end of file
diff --git a/src/killer/trapper.py b/src/killer/trapper.py
new file mode 100644
index 0000000..e7f1cbc
--- /dev/null
+++ b/src/killer/trapper.py
@@ -0,0 +1,13 @@
+import pyautogui
+from time import sleep
+from util.console import console
+
+def place_and_pick_trap():
+ console.log("Trapper action: Place and pick trap")
+
+ pyautogui.mouseDown(button="secondary")
+ sleep(4)
+ pyautogui.mouseUp(button="secondary")
+ sleep(2)
+ pyautogui.press("space")
+ sleep(5)
\ No newline at end of file
diff --git a/src/killer/universal.py b/src/killer/universal.py
new file mode 100644
index 0000000..8e6b651
--- /dev/null
+++ b/src/killer/universal.py
@@ -0,0 +1,20 @@
+import pyautogui
+from time import sleep
+from util.console import console
+from rich.text import Text
+
+def walk_and_attack():
+ console.log("Universal action: Walk and attack")
+
+ pyautogui.keyDown("w")
+ sleep(0.5)
+ pyautogui.keyUp("w")
+
+ pyautogui.keyDown("s")
+ sleep(0.5)
+ pyautogui.keyUp("s")
+
+ # Coords of the "banner" (survivor disconneted etc.); if no banner is present, it just attacks like normal
+ pyautogui.click(x=1379, y=658)
+ pyautogui.click(x=1379, y=658)
+ pyautogui.click(x=1379, y=658)
\ No newline at end of file
diff --git a/src/main.py b/src/main.py
new file mode 100644
index 0000000..496b158
--- /dev/null
+++ b/src/main.py
@@ -0,0 +1,20 @@
+from util.console import console, print_stats
+from rich.text import Text
+import config
+from mss import mss
+import core.brain as brain
+
+from util.screenshot import Screenshot
+
+if __name__ == '__main__':
+ console.print(Text("\n! IGNORE ALL ERRORS ABOVE !\n", style="bold black on red"))
+ console.log(Text("Initializing...", style="green"))
+
+
+ with mss() as sct:
+ config.ss = Screenshot(sct)
+ console.clear()
+ console.log(Text("Initialized!", style="green"))
+
+ while True:
+ brain.loop()
\ No newline at end of file
diff --git a/src/util/console.py b/src/util/console.py
new file mode 100644
index 0000000..9d7bfd0
--- /dev/null
+++ b/src/util/console.py
@@ -0,0 +1,14 @@
+from rich.console import Console
+from rich.text import Text
+import datetime
+from time import time
+import config
+
+console = Console()
+
+def print_stats():
+ console.print("\n\n")
+ console.print(Text(f" XP: {config.xp} total ({config.xp / config.games} avg per game)", style="black on yellow"))
+ console.print(Text(f" Games: {config.games} played ", style="black on green"))
+ console.print(Text(f" Running Time: {str(datetime.timedelta(seconds=int(time() - config.script_start_time)))} ({str(datetime.timedelta(seconds=int(config.total_time_in_game)))} in games; {str(datetime.timedelta(seconds=int((time() - config.script_start_time) - config.total_time_in_game)))} in lobby)"), style="white on purple")
+ console.print("\n\n")
\ No newline at end of file
diff --git a/src/util/screenshot.py b/src/util/screenshot.py
new file mode 100644
index 0000000..1eea8b0
--- /dev/null
+++ b/src/util/screenshot.py
@@ -0,0 +1,54 @@
+import pyautogui
+import easyocr
+from time import sleep
+from PIL import Image
+
+pyautogui.FAILSAFE = False
+
+reader = easyocr.Reader(['en'])
+
+
+class Screenshot:
+ def __init__(self, sct):
+ self.sct = sct
+
+
+ lobby_button = {
+ "top": 927,
+ "left": 1580,
+ "width": 235,
+ "height": 35
+ }
+
+ endgame_button = {
+ "top": 1000,
+ "left": 1650,
+ "width": 235,
+ "height": 35
+ }
+
+ def click_image(self):
+ try:
+ pos = pyautogui.locateOnScreen('screenshot.png', confidence=(0.8))
+ if pos != None:
+ pyautogui.click(pos[0] + 20, pos[1])
+ sleep(0.5)
+ except:
+ pass
+
+ def take_and_read_screenshot(self, area):
+ screenshot = self.sct.grab(area)
+ image = Image.frombytes("RGB", screenshot.size, screenshot.bgra, "raw", "BGRX")
+ image.save("screenshot.png")
+ return reader.readtext("screenshot.png", detail=0)
+
+ def take_and_read_xp_screenshot(self):
+ screenshot = self.sct.grab({
+ "top": 700,
+ "left": 540,
+ "width": 75,
+ "height": 35
+ })
+ image = Image.frombytes("RGB", screenshot.size, screenshot.bgra, "raw", "BGRX")
+ image.save("last_game_xp.png")
+ return reader.readtext("last_game_xp.png", detail=0)
\ No newline at end of file
diff --git a/start.bat b/start.bat
index 44cc5d6..b364247 100644
--- a/start.bat
+++ b/start.bat
@@ -1,2 +1 @@
-pip install -r requirements.txt
-python run.py
\ No newline at end of file
+python src/main.py
\ No newline at end of file