diff --git a/README.md b/README.md
index ddf3a4e..a2cfa95 100644
--- a/README.md
+++ b/README.md
@@ -27,28 +27,20 @@ Feel free to make a pull request if you have any improvements or create a issue
## Requirements & Dependencies
-- Tesseract v5.0+
- Python 3.10+
```
keyboard==0.13.5
mouse==0.7.1
-mss==6.1.0
-numpy==1.22.3
-opencv_python==4.5.5.64
-pytesseract==0.3.9
+mss==9.0.1
+numpy==1.25.1
+opencv_python==4.8.0.74
+Pymem==1.12.0
+pywin32==306
```
## Installation of dependencies:
-The script relies on tesseract (tested with v5.3.0) which can be installed using this [this](https://github.com/UB-Mannheim/tesseract/wiki) guide.
-(*If by any chance the tesseract installation directory is different from the directory specified in Bot.py you need to manually change that in the script. Otherwise the bot will not work!*)
-
-default path (all users tesseract installation):
-```py
-pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'
-```
-
The python library requirments can be installed using `python -m pip install -r requirements.txt` or by running `Install_Requirements.bat`
diff --git a/Run.bat b/Run.bat
index b949ad6..41e3194 100644
--- a/Run.bat
+++ b/Run.bat
@@ -1 +1 @@
-python src/main.py --gameplan_path "gameplans/Dark_Castle_Hard_Standard" --restart
\ No newline at end of file
+python src/main.py --gameplan_path "gameplans/Dark_Castle_Hard_Chimps"
\ No newline at end of file
diff --git a/gameplans/Dark_Castle_Hard_Chimps/notes.md b/gameplans/Dark_Castle_Hard_Chimps/notes.md
index d17ae7d..3141e57 100644
--- a/gameplans/Dark_Castle_Hard_Chimps/notes.md
+++ b/gameplans/Dark_Castle_Hard_Chimps/notes.md
@@ -18,6 +18,6 @@
...and 1 insta-monkey!
-# Credits to : Tanttinen#4001
+# Credits to : mirko93s
*Note: The strat that is being used has RNG because of the Alchemist, so the results may differ to you from what I got*
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 839e4f8..9de92be 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,7 @@
keyboard==0.13.5
mouse==0.7.1
-mss==6.1.0
-numpy==1.22.3
-opencv_python==4.5.5.64
-pytesseract==0.3.9
+mss==9.0.1
+numpy==1.25.1
+opencv_python==4.8.0.74
+Pymem==1.12.0
+pywin32==306
diff --git a/src/Bot.py b/src/Bot.py
index 2f61702..6f46aa1 100644
--- a/src/Bot.py
+++ b/src/Bot.py
@@ -9,12 +9,13 @@
from pathlib import Path
# Local imports
-import ocr
import recognition
import simulatedinput
import monitor
import gameplan
from logger import logger as log
+import pymem
+from winreg import *
# Definently fix this
class Bot():
@@ -37,8 +38,6 @@ def __init__(self,
# Something to do with how python handles copying objects
self._game_plan_copy = copy.deepcopy(self.game_plan)
####
-
- self.round_area = None
self.DEBUG = debug_mode
self.RESTART = restart_mode
@@ -308,7 +307,6 @@ def set_static_target(self, tower_position, target_pos):
def remove_tower(self, position):
simulatedinput.click(position)
simulatedinput.send_key("backspace")
- simulatedinput.send_key("esc")
def execute_instruction(self, instruction):
"""Handles instructions from the gameplan"""
@@ -401,33 +399,17 @@ def check_for_collection_crates(self):
# Can this be done better?
if self.checkFor("diamond_case"):
log.debug("easter collection detected")
-
- simulatedinput.click("EASTER_COLLECTION") #DUE TO EASTER EVENT:
- time.sleep(1)
- simulatedinput.click("LEFT_INSTA") # unlock insta
- time.sleep(1)
- simulatedinput.click("LEFT_INSTA") # collect insta
- time.sleep(1)
- simulatedinput.click("RIGHT_INSTA") # unlock r insta
- time.sleep(1)
- simulatedinput.click("RIGHT_INSTA") # collect r insta
- time.sleep(1)
- simulatedinput.click("F_LEFT_INSTA")
- time.sleep(1)
- simulatedinput.click("F_LEFT_INSTA")
- time.sleep(1)
- simulatedinput.click("MID_INSTA") # unlock insta
- time.sleep(1)
- simulatedinput.click("MID_INSTA") # collect insta
+ #c lick collect button
+ simulatedinput.click("EVENT_COLLECTION", timeout=1.5)
+ # collect instas
+ simulatedinput.click("LEFT_INSTA", amount=2, timeout=1.5)
+ simulatedinput.click("RIGHT_INSTA", amount=2, timeout=1.5)
+ simulatedinput.click("F_LEFT_INSTA", amount=2, timeout=1.5)
+ simulatedinput.click("MID_INSTA", amount=2, timeout=1.5)
+ simulatedinput.click("F_RIGHT_INSTA", amount=2, timeout=1.5)
+ # click continue and exit to main menu
time.sleep(1)
- simulatedinput.click("F_RIGHT_INSTA")
- time.sleep(1)
- simulatedinput.click("F_RIGHT_INSTA")
- time.sleep(1)
-
- time.sleep(1)
- simulatedinput.click("EASTER_CONTINUE")
-
+ simulatedinput.click("EVENT_CONTINUE",timeout=1)
simulatedinput.send_key("esc")
# select hero if not selected
@@ -524,94 +506,23 @@ def wait_for_loading(self):
still_loading = self.checkFor("loading_screen")
log.debug("Out of loading screen, continuing..")
-
- def getRoundArea(self):
- # Init round area dict with width and height of the round area
- round_area = {
- "width": 200,
- "height": 42
- }
-
- # Search for round text, returns (1484,13) on 1080p
- area = self.checkFor("round", return_cords=True, center_on_found=False)
- log.debug("this should be only printed once, getting round area")
- log.debug(f"Round area found at {area}, applying offsetts")
-
- if area:
- log.info("Found round area!")
-
- # set round area to the found area + offset
- x, y, roundwidth, roundheight = area
-
- # Fiddled offset, do not tuch
- # Offset from ROUND text to round number
- xOffset = roundwidth + 10
- yOffset = int(roundheight * 3) - 40
-
- round_area["top"] = y + yOffset
- round_area["left"] = x - xOffset
-
- return round_area
-
- # If it cant find anything
- log.warning("Could not find round area, setting default values")
- default_round_area_scaled = monitor.scaling([0.7083333333333333, 0.0277777777777778]) # Use default values, (1360,30) on 1080p
-
- # left = x, top = y
- round_area["left"] = default_round_area_scaled[0]
- round_area["top"] = default_round_area_scaled[1]
- return round_area
-
+
def getRound(self):
- # If round area is not located yet
- if self.round_area is None:
- self.round_area = self.getRoundArea()
-
- # Setting up screen capture area
- # The screen part to capture
- screenshot_dimensions = {
- 'top': self.round_area["top"],
- 'left': self.round_area["left"],
- 'width': self.round_area["width"],
- 'height': self.round_area["height"] + 50
- }
-
- # Take Screenshot
- with mss.mss() as screenshotter:
- screenshot = screenshotter.grab(screenshot_dimensions)
- found_text, _ocrImage = ocr.getTextFromImage(screenshot)
-
- if self.DEBUG:
- from cv2 import imwrite, IMWRITE_PNG_COMPRESSION
- def get_valid_filename(s):
- s = str(s).strip().replace(' ', '_')
- return re.sub(r'(?u)[^-\w.]', '', s)
- imwrite(f"./DEBUG/OCR_DONE_FOUND_{get_valid_filename(found_text)}_{str(time.time())}.png", _ocrImage, [IMWRITE_PNG_COMPRESSION, 0])
-
- # Get only the first number/group so we don't need to replace anything in the string
- if re.search(r"(\d+/\d+)", found_text):
- found_text = re.search(r"(\d+)", found_text)
- return int(found_text.group(0))
- else:
- # If the found text does not match the regex requirements, Debug and save image
- log.warning("Found text '{}' does not match regex requirements".format(found_text))
-
- try:
- file_path = Path(__file__).resolve().parent.parent/ "DEBUG"
- if not file_path.exists():
- Path.mkdir(file_path)
+ add = 0x03096928
+ offsets = [0xB8, 0x0, 0xC0, 0x120, 0x20, 0x20]
- with open(file_path/f"GETROUND_IMAGE_{str(time.time())}.png", "wb") as output_file:
- output_file.write(mss.tools.to_png(screenshot.rgb, screenshot.size))
-
- log.warning("Saved screenshot of what was found")
+ pm = pymem.Pymem('BloonsTD6.exe')
+ gameModule = pymem.process.module_from_name(pm.process_handle, "GameAssembly.dll").lpBaseOfDll
+ address = pm.read_longlong(gameModule+add)
- except Exception as e:
- log.error(e)
- log.warning("Could not save screenshot of what was found")
+ for offset in offsets:
+ try:
+ address = pm.read_longlong(address + offset)
+ except Exception as e:
+ print(e)
- return None
+ return address + offsets[-1] -32
def waitForRound(self, round) -> None:
"""Wait for a specific round to start, use this to wait until to execute a gameplan instruction"""
@@ -660,6 +571,59 @@ def checkFor(self,
return_cords,
center_on_found
)
+
+ def findStore(self):
+ # check if game is installed on Steam
+ reg = ConnectRegistry(None,HKEY_CURRENT_USER)
+ game_on_Steam = False
+ try:
+ key = OpenKey(reg, r"SOFTWARE\Valve\Steam\Apps\960090")
+ if key:
+ try:
+ isInstalled = QueryValueEx(key, "Installed")[0]
+ # Other useful values are "Running" and "Updating"
+ if isInstalled == 1:
+ game_on_Steam = True
+ print("Detected Steam installation.")
+ except WindowsError:
+ # game is not installed but in the Steam library ???
+ pass
+ CloseKey(key)
+ except WindowsError:
+ # game is not installed on Steam
+ pass
+ CloseKey(reg)
+
+ # if game is not installed on Steam check Epic Games
+ reg = ConnectRegistry(None,HKEY_LOCAL_MACHINE)
+ game_on_EpicGames = False
+ try:
+ key = OpenKey(reg, r"SOFTWARE\WOW6432Node\Epic Games\EpicGamesLauncher")
+ if key:
+ try:
+ epicgamesLauncherPath = QueryValueEx(key, "AppDataPath")[0]
+ if epicgamesLauncherPath:
+ path = re.search(r".+Epic\\", epicgamesLauncherPath).group(0) + "UnrealEngineLauncher\LauncherInstalled.dat"
+ try:
+ with open(path) as f:
+ data = json.load(f)
+ for x in data["InstallationList"]:
+ if x["NamespaceId"] == "6a8dfa6e441e4f2f9048a98776c6077d":
+ game_on_EpicGames = True
+ print("Detected Epic Games installation.")
+ except:
+ # game is not installed on Epic Games
+ pass
+ except WindowsError:
+ # couldn't find Epic Games launcher path
+ pass
+ CloseKey(key)
+ except WindowsError:
+ # Epic Games launcher is not installed
+ pass
+ CloseKey(reg)
+
+ return(game_on_Steam,game_on_EpicGames)
if __name__ == "__main__":
import time
diff --git a/src/assets/admiral_brickell_3.png b/src/assets/admiral_brickell_3.png
new file mode 100644
index 0000000..d2782a0
Binary files /dev/null and b/src/assets/admiral_brickell_3.png differ
diff --git a/src/assets/captain_churchill_3.png b/src/assets/captain_churchill_3.png
new file mode 100644
index 0000000..bc9e9fb
Binary files /dev/null and b/src/assets/captain_churchill_3.png differ
diff --git a/src/assets/instamonkey.png b/src/assets/instamonkey.png
index e49875c..feeccdb 100644
Binary files a/src/assets/instamonkey.png and b/src/assets/instamonkey.png differ
diff --git a/src/assets/pat_fusty_3.png b/src/assets/pat_fusty_3.png
new file mode 100644
index 0000000..4484d8c
Binary files /dev/null and b/src/assets/pat_fusty_3.png differ
diff --git a/src/assets/sauda_3.png b/src/assets/sauda_3.png
new file mode 100644
index 0000000..f4bb19d
Binary files /dev/null and b/src/assets/sauda_3.png differ
diff --git a/src/assets/startup.png b/src/assets/startup.png
new file mode 100644
index 0000000..c3fec89
Binary files /dev/null and b/src/assets/startup.png differ
diff --git a/src/main.py b/src/main.py
index 2a916c9..f1df38a 100644
--- a/src/main.py
+++ b/src/main.py
@@ -7,8 +7,11 @@
from Failsafe import FailSafe
import mouse
import simulatedinput
+import monitor
from logger import logger as log
import os
+import win32gui
+import subprocess
def main(arg_parser):
@@ -60,15 +63,52 @@ def no_gameplan_exception():
print("Gamemode:", bot.settings["GAMEMODE"].replace("_", " ").title())
print("="*25)
- print("Waiting for Home screen. Please switch to the Bloons TD 6 window.")
+ print("Finding game process.")
- # Wait for btd6 home screen
+ # set mouse starting position to the bottom right corner
+ simulatedinput.move_mouse((monitor.width,monitor.height))
+
+ waiting_for_game = True
+ hwnd = win32gui.FindWindow(None, 'BloonsTD6')
+
+ # game is not open
+ if not hwnd:
+ print("Game process not found. Finding game installations.")
+
+ Steam, EpicGames = bot.findStore()
+
+ if Steam:
+ print("Starting the game through Steam. Please wait...")
+ subprocess.run("start steam://run/960090", shell=True, check=True)
+
+ elif EpicGames:
+ print("Starting the game through Epic Games. Please wait...")
+ subprocess.run("start com.epicgames.launcher://apps/6a8dfa6e441e4f2f9048a98776c6077d%3A49c4bf5c6fd24259b87d0bcc96b6009f%3A7786b355a13b47a6b3915335117cd0b2?action=launch", shell=True, check=True)
+
+ else:
+ # We can exit the program here if we don't want cracked users to use the bot
+ # sys.exit(1)
+ print("Please start the game manually.")
+
+ while waiting_for_game:
+ time.sleep(0.2)
+ hwnd = win32gui.FindWindow(None, 'BloonsTD6')
+ # game process found, focus its window
+ if hwnd:
+ waiting_for_game = False
+ win32gui.SetForegroundWindow(hwnd)
+ print("Game found. Switching to game window.")
+
+ # Wait for btd6 home screen or startup screen
waiting_for_home = False
log.info("Waiting for home screen..")
while waiting_for_home is False:
time.sleep(0.2) # add a short timeout to avoid spamming the cpu
- waiting_for_home = bot.checkFor("home_menu")
+ waiting_for_startup, waiting_for_home = bot.checkFor(["startup","home_menu"], return_raw=True)
+ if waiting_for_startup:
+ log.info("Startup screen detected")
+ simulatedinput.click("STARTUP")
log.info("Home screen detected")
print("Starting bot..\nIf you want to stop the bot, move your mouse to the upper left corner of your screen or press ctrl+c in the terminal")
@@ -109,7 +149,7 @@ def no_gameplan_exception():
parser.add_argument('-p', '--path', '--gameplan_path', type=str, help='Path to the gameplan directory', required=True)
parser.add_argument('-d', '--debug', action='store_true', help='Enable debug mode')
- parser.add_argument('-r', '--restart', action='store_true', help='automatically restarts the game when finished, instead of going to home screen')
+ parser.add_argument('-r', '--restart', action='store_true', help='automatically restarts the game when finished, instead of going to home screen \(games don\'t count towards event progression if you don\'t go back to home)')
parser.add_argument('-s', '--sandbox', action='store_true', help='Try put gameplan in sandbox mode without waiting for specific rounds')
# Start the bot on a seperate thread
diff --git a/src/ocr.py b/src/ocr.py
deleted file mode 100644
index e25d4b4..0000000
--- a/src/ocr.py
+++ /dev/null
@@ -1,90 +0,0 @@
-# width, height, top, left
-
-import pytesseract
-
-import numpy as np
-import cv2
-import sys
-import time
-
-if sys.platform == "win32":
- pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'
-
-
-def formatImageOCR(originalScreenshot):
- screenshot = np.array(originalScreenshot, dtype=np.uint8)
- # Get local maximum:
- kernelSize = 5
- maxKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernelSize, kernelSize))
- localMax = cv2.morphologyEx(screenshot, cv2.MORPH_CLOSE, maxKernel, None, None, 1, cv2.BORDER_REFLECT101)
- # Perform gain division
- # print(screenshot, localMax)
- gainDivision = np.where(localMax == 0, 0, (screenshot / localMax))
- # Clip the values to [0,255]
- gainDivision = np.clip((255 * gainDivision), 0, 255)
- # Convert the mat type from float to uint8:
- gainDivision = gainDivision.astype("uint8")
- # Convert RGB to grayscale:
- grayscaleImage = cv2.cvtColor(gainDivision, cv2.COLOR_BGR2GRAY)
- # Resize image to improve the quality
- grayscaleImage = cv2.resize(grayscaleImage,(0,0), fx=3.0, fy=3.0)
- # Get binary image via Otsu:
- _, final_image = cv2.threshold(grayscaleImage, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
- # cv2.imwrite(f"./DEBUG/OCR_FORMAT_BINARY_IMAGE_{str(time.time())}.png", final_image, [cv2.IMWRITE_PNG_COMPRESSION, 0])
-
- # Set kernel (structuring element) size:
- kernelSize = 3
- # Set morph operation iterations:
- opIterations = 1
- # Get the structuring element:
- morphKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernelSize, kernelSize))
- # Perform closing:
- final_image = cv2.morphologyEx( final_image, cv2.MORPH_CLOSE, morphKernel, None, None, opIterations, cv2.BORDER_REFLECT101 )
- # cv2.imwrite(f"./DEBUG/OCR_FORMAT_BEFORE_FLOOD_{str(time.time())}.png", final_image, [cv2.IMWRITE_PNG_COMPRESSION, 0])
-
- # Flood fill (white + black):
- cv2.floodFill(final_image, mask=None, seedPoint=(int(0), int(0)), newVal=(255))
- # Invert image so target blobs are colored in white:
- final_image = 255 - final_image
- # Find the blobs on the binary image:
- contours, hierarchy = cv2.findContours(final_image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
- # Process the contours:
- for i, c in enumerate(contours):
- # Get contour hierarchy:
- currentHierarchy = hierarchy[0][i][3]
- # Look only for children contours (the holes):
- if currentHierarchy != -1:
- # Get the contour bounding rectangle:
- boundRect = cv2.boundingRect(c)
- # Get the dimensions of the bounding rect:
- rectX = boundRect[0]
- rectY = boundRect[1]
- rectWidth = boundRect[2]
- rectHeight = boundRect[3]
- # Get the center of the contour the will act as
- # seed point to the Flood-Filling:
- fx = rectX + 0.5 * rectWidth
- fy = rectY + 0.5 * rectHeight
- # Fill the hole:
- cv2.floodFill(final_image, mask=None, seedPoint=(int(fx), int(fy)), newVal=(0))
- # cv2.imwrite(f"./DEBUG/OCR_FLOOD_{i}_{str(time.time())}.png", final_image, [cv2.IMWRITE_PNG_COMPRESSION, 0])
-
- return final_image
-
-
-# Change to https://stackoverflow.com/questions/66334737/pytesseract-is-very-slow-for-real-time-ocr-any-way-to-optimise-my-code
-# or https://www.reddit.com/r/learnpython/comments/kt5zzw/how_to_speed_up_pytesseract_ocr_processing/
-
-def getTextFromImage(image):
- """ returns text from image """
- imageCandidate = formatImageOCR(image)
- # Write result to disk:
-
- # DEBUG log round to disk
- # import time
- # cv2.imwrite(f"./DEBUG/{str(time.time())}.png", imageCandidate, [cv2.IMWRITE_PNG_COMPRESSION, 0])
-
- # NOTE: This part seems to be buggy
- # Get current round from screenshot with tesseract
- return pytesseract.image_to_string(imageCandidate, config='--psm 7').replace("\n", ""), imageCandidate
-
diff --git a/src/simulatedinput.py b/src/simulatedinput.py
index 0acac76..c68be85 100644
--- a/src/simulatedinput.py
+++ b/src/simulatedinput.py
@@ -58,5 +58,11 @@ def click(location: tuple | tuple, amount=1, timeout=0.5, move_timeout=0.1, hold
"""
if amount > 1:
sleep(timeout)
+
+ # keep this if or it will wait twice (double the time) if amount is 2+
+ if amount == 1:
+ sleep(timeout)
+
+ # Move mouse on the bottom right corner so we don't cover the screen with the cursor
+ move_mouse((monitor.width,monitor.height))
- sleep(timeout)
\ No newline at end of file
diff --git a/src/static.py b/src/static.py
index 8d3fbe0..f166ccd 100644
--- a/src/static.py
+++ b/src/static.py
@@ -49,14 +49,14 @@ class Map (Difficulty, Gamemode):
"VICTORY_HOME" : [ 0.366796875 , 0.7805555555555556 ],
"DEFEAT_HOME" : [ 0.29453125 , 0.7520833333333333 ],
"DEFEAT_HOME_NO_CONTINUE" : [ 0.36328125 , 0.7520833333333333 ],
- "EASTER_COLLECTION" : [ 0.499609375 , 0.6326388888888889 ],
+ "EVENT_COLLECTION" : [ 0.499609375 , 0.6326388888888889 ],
"F_LEFT_INSTA" : [ 0.3390625 , 0.5013888888888889 ],
"F_RIGHT_INSTA" : [ 0.65625 , 0.5013888888888889 ],
"LEFT_INSTA" : [ 0.41953125 , 0.5034722222222222 ],
"RIGHT_INSTA" : [ 0.577734375 , 0.5027777777777778 ],
"MID_INSTA" : [ 0.4984375 , 0.5048611111111111 ],
- "EASTER_CONTINUE" : [ 0.5 , 0.9236111111111112 ],
- "EASTER_EXIT" : [ 0.0390625 , 0.06458333333333334 ],
+ "EVENT_CONTINUE" : [ 0.5 , 0.9236111111111112 ],
+ "EVENT_EXIT" : [ 0.0390625 , 0.06458333333333334 ],
"QUIT_HOME" : [ 0.43984375 , 0.7881944444444444 ],
"HERO_SELECT" : [ 0.312109375 , 0.8833333333333333 ],
"CONFIRM_HERO" : [ 0.587890625 , 0.5722222222222222 ],
@@ -72,7 +72,8 @@ class Map (Difficulty, Gamemode):
"CONFIRM_CHIMPS" : [ 0.500390625 , 0.6805555555555556 ],
"SETTINGS" : [ 0.037500 , 0.191667],
"LANGUAGE" : [ 0.555208 , 0.659259],
- "ENGLISH" : [ 0.224479 , 0.192593]
+ "ENGLISH" : [ 0.224479 , 0.192593],
+ "STARTUP" : [ 0.5, 0.925]
}
hero_positions = {
@@ -138,69 +139,79 @@ class Map (Difficulty, Gamemode):
# "NAME" : [PAGE, INDEX]
maps = {
- "MONKEY_MEADOW" : [1, 1],
- "TREE_STUMP" : [1, 2],
- "TOWN_CENTER" : [1, 3],
- "SCRAPYARD" : [1, 4],
- "THE_CABIN" : [1, 5],
- "RESORT" : [1, 6],
- "SKATES" : [2, 1],
- "LOTUS_ISLAND" : [2, 2],
- "CANDY_FALLS" : [2, 3],
- "WINTER_PARK" : [2, 4],
- "CARVED" : [2, 5],
- "PARK_PATH" : [2, 6],
- "ALPINE_RUN" : [3, 1],
- "FROZEN_OVER" : [3, 2],
- "IN_THE_LOOP" : [3, 3],
- "CUBISM" : [3, 4],
- "FOUR_CIRCLES" : [3, 5],
- "HEDGE" : [3, 6],
- "END_OF_THE_ROAD" : [4, 1],
- "LOGS" : [4, 2],
- "COVERED_GARDEN" : [5, 1],
- "QUARRY" : [5, 2],
- "QUIET_STREET" : [5, 3],
- "BLOONARIUS_PRIME" : [5, 4],
- "BALANCE" : [5, 5],
- "ENCRYPTED" : [5, 6],
- "BAZAAR" : [6, 1],
- "ADORAS_TEMPLE" : [6, 2],
- "SPRING_SPRING" : [6, 3],
- "KARTSNDARTS" : [6, 4],
- "MOON_LANDING" : [6, 5],
- "HAUNTED" : [6, 6],
- "DOWNSTREAM" : [7, 1],
- "FIRING_RANGE" : [7, 2],
- "CRACKED" : [7, 3],
- "STREAMBED" : [7, 4],
- "CHUTES" : [7, 5],
- "RAKE" : [7, 6],
- "SPICE_ISLANDS" : [8, 1],
- "MIDNIGHT_MANSION" : [9, 1],
- "SUNKEN_COLUMNS" : [9, 2],
- "XFACTOR" : [9, 3],
- "MESA" : [9, 4],
- "GEARED" : [9, 5],
- "SPILLWAY" : [9, 6],
- "CARGO" : [10, 1],
- "PATS_POND" : [10, 2],
- "PENINSULA" : [10, 3],
- "HIGH_FINANCE" : [10, 4],
- "ANOTHER_BRICK" : [10, 5],
- "OFF_THE_COAST" : [10, 6],
- "CORNFIELD" : [11, 1],
- "UNDERGROUND" : [11, 2],
- "SANCTUARY" : [12, 1],
- "RAVINE" : [12, 2],
- "FLOODED_VALLEY" : [12, 3],
- "INFERNAL" : [12, 4],
- "BLOODY_PUDDLES" : [12, 5],
- "WORKSHOP" : [12, 6],
- "QUAD" : [13, 1],
- "DARK_CASTLE" : [13, 2],
- "MUDDY_PUDDLES" : [13, 3],
- "OUCH" : [13, 4]
+ # BEGINNER
+ "MONKEY_MEADOW" : [1, 1],
+ "TREE_STUMP" : [1, 2],
+ "TOWN_CENTER" : [1, 3],
+ "MIDDLE_OF_THE_ROAD": [1, 4],
+ "ONE_TWO_THREE" : [1, 5],
+ "SCRAPYARD" : [1, 6],
+ "THE_CABIN" : [2, 1],
+ "RESORT" : [2, 2],
+ "SKATES" : [2, 3],
+ "LOTUS_ISLAND" : [2, 4],
+ "CANDY_FALLS" : [2, 5],
+ "WINTER_PARK" : [2, 6],
+ "CARVED" : [3, 1],
+ "PARK_PATH" : [3, 2],
+ "ALPINE_RUN" : [3, 3],
+ "FROZEN_OVER" : [3, 4],
+ "IN_THE_LOOP" : [3, 5],
+ "CUBISM" : [3, 6],
+ "FOUR_CIRCLES" : [4, 1],
+ "HEDGE" : [4, 2],
+ "END_OF_THE_ROAD" : [4, 3],
+ "LOGS" : [4, 4],
+ # INTERMEDIATE
+ "WATER_PARK" : [5, 1],
+ "POLYPHEMUS" : [5, 2],
+ "COVERED_GARDEN" : [5, 3],
+ "QUARRY" : [5, 4],
+ "QUIET_STREET" : [5, 5],
+ "BLOONARIUS_PRIME" : [5, 6],
+ "BALANCE" : [6, 1],
+ "ENCRYPTED" : [6, 2],
+ "BAZAAR" : [6, 3],
+ "ADORAS_TEMPLE" : [6, 4],
+ "SPRING_SPRING" : [6, 5],
+ "KARTSNDARTS" : [6, 6],
+ "MOON_LANDING" : [7, 1],
+ "HAUNTED" : [7, 2],
+ "DOWNSTREAM" : [7, 3],
+ "FIRING_RANGE" : [7, 4],
+ "CRACKED" : [7, 5],
+ "STREAMBED" : [7, 6],
+ "CHUTES" : [8, 1],
+ "RAKE" : [8, 2],
+ "SPICE_ISLANDS" : [8, 3],
+ # ADVANCED
+ "EROSION" : [9, 1],
+ "MIDNIGHT_MANSION" : [9, 2],
+ "SUNKEN_COLUMNS" : [9, 3],
+ "X_FACTOR" : [9, 4],
+ "MESA" : [9, 5],
+ "GEARED" : [9, 6],
+ "SPILLWAY" : [10, 1],
+ "CARGO" : [10, 2],
+ "PATS_POND" : [10, 3],
+ "PENINSULA" : [10, 4],
+ "HIGH_FINANCE" : [10, 5],
+ "ANOTHER_BRICK" : [10, 6],
+ "OFF_THE_COAST" : [11, 1],
+ "CORNFIELD" : [11, 2],
+ "UNDERGROUND" : [11, 3],
+ # EXPERT
+ "DARK_DUNGEONS" : [12, 1],
+ "SANCTUARY" : [12, 2],
+ "RAVINE" : [12, 3],
+ "FLOODED_VALLEY" : [12, 4],
+ "INFERNAL" : [12, 5],
+ "BLOODY_PUDDLES" : [12, 6],
+ "WORKSHOP" : [13, 1],
+ "QUAD" : [13, 2],
+ "DARK_CASTLE" : [13, 3],
+ "MUDDY_PUDDLES" : [13, 4],
+ "OUCH" : [13, 5]
}
upgrade_keybinds = {
@@ -211,4 +222,4 @@ class Map (Difficulty, Gamemode):
# Index, regular targets, spike factory targets
target_order_regular = [ "FIRST", "LAST", "CLOSE", "STRONG" ]
-target_order_spike = [ "NORMAL", "CLOSE", "FAR", "SMART" ]
\ No newline at end of file
+target_order_spike = [ "NORMAL", "CLOSE", "FAR", "SMART" ]