diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..047da6b
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,37 @@
+
+ 4.0.0
+ com.rslbot
+ rsl-bot
+ 1.0-SNAPSHOT
+
+
+ 17
+ 17
+ UTF-8
+ 5.10.2
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.version}
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+ false
+
+
+
+
+
diff --git a/src/main/java/rslbot/BotCore.java b/src/main/java/rslbot/BotCore.java
new file mode 100644
index 0000000..e7cef87
--- /dev/null
+++ b/src/main/java/rslbot/BotCore.java
@@ -0,0 +1,76 @@
+package rslbot;
+
+import rslbot.ocr.OcrReader;
+import java.util.List;
+
+/**
+ * Core automation routine that searches for battle buttons, waits for
+ * victory and applies auto-sell rules to obtained gear.
+ */
+public class BotCore {
+ private final ScreenUtils screen = new ScreenUtils();
+ private final OcrReader ocr = new OcrReader();
+ private volatile boolean running;
+
+ public interface ProgressListener {
+ void onProgress(int currentRun, int totalRuns);
+ }
+
+ /**
+ * Starts the bot loop for the given number of runs. Progress is reported
+ * via the supplied listener on the calling thread.
+ */
+ public void startBot(int runs, ProgressListener progress) throws Exception {
+ running = true;
+ List rules = GearRules.loadRules(
+ "src/main/resources/rules/Early Mid Game Basic Sell rules (1).hsf");
+
+ for (int i = 0; i < runs && running; i++) {
+ System.out.println("\u25B6 Starting run " + (i + 1));
+
+ // Locate and click the Fight button inside the game client.
+ if (!screen.findAndClick("images/fight_button.png")) {
+ System.out.println("Fight button not found!");
+ Thread.sleep(3000);
+ continue;
+ }
+
+ // Wait for the victory screen to appear.
+ while (running && screen.find("images/victory.png") == null) {
+ Thread.sleep(5000);
+ }
+ if (!running) {
+ break;
+ }
+
+ // OCR the gear popup and decide whether to sell or keep.
+ String gearText = ocr.readGearPopup();
+ if (gearText != null) {
+ boolean sell = GearRules.shouldSell(gearText, rules);
+ if (sell) {
+ screen.findAndClick("images/sell_button.png");
+ System.out.println("\uD83D\uDDD1 Sold gear: " + gearText);
+ } else {
+ screen.findAndClick("images/keep_button.png");
+ System.out.println("\uD83D\uDC8E Kept gear: " + gearText);
+ }
+ }
+
+ // Start another run.
+ screen.findAndClick("images/replay_button.png");
+ Thread.sleep(3000);
+
+ if (progress != null) {
+ progress.onProgress(i + 1, runs);
+ }
+ }
+ running = false;
+ }
+
+ /**
+ * Requests the running loop to stop after the current iteration.
+ */
+ public void stop() {
+ running = false;
+ }
+}
diff --git a/src/main/java/rslbot/ChampionUpgrade.java b/src/main/java/rslbot/ChampionUpgrade.java
new file mode 100644
index 0000000..e2b87e2
--- /dev/null
+++ b/src/main/java/rslbot/ChampionUpgrade.java
@@ -0,0 +1,35 @@
+package rslbot;
+
+/**
+ * Utility for computing how many 1★ champions are required to create
+ * a champion of a given rank. In the game, promoting a champion to the
+ * next rank requires feeding it a number of fodder champions equal to
+ * its current rank, and each fodder champion must itself be ranked up
+ * in the same way. This results in a factorial growth in the amount of
+ * fodder needed.
+ */
+public class ChampionUpgrade {
+
+ /**
+ * Returns the total number of 1★ champions required to build a single
+ * champion of the supplied rank.
+ *
+ * @param rank target rank (must be positive)
+ * @return number of base 1★ champions needed
+ */
+ public static int championsNeeded(int rank) {
+ if (rank < 1) {
+ throw new IllegalArgumentException("rank must be positive");
+ }
+ int total = 1;
+ for (int i = 2; i <= rank; i++) {
+ total *= i; // factorial growth
+ }
+ return total;
+ }
+
+ public static void main(String[] args) {
+ System.out.println("To upgrade 3★ → " + championsNeeded(3) + " fodders");
+ System.out.println("To upgrade 4★ → " + championsNeeded(4) + " fodders");
+ }
+}
diff --git a/src/main/java/rslbot/GearRules.java b/src/main/java/rslbot/GearRules.java
new file mode 100644
index 0000000..d969b3b
--- /dev/null
+++ b/src/main/java/rslbot/GearRules.java
@@ -0,0 +1,33 @@
+package rslbot;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Simple rule set that determines whether a piece of gear should be sold
+ * based on text extracted from the gear popup.
+ */
+public class GearRules {
+ public static class SellRule {
+ public String gearType;
+ public int minStars;
+ public int maxStars;
+ public String rarity;
+ public String mainStat;
+ public List subStats;
+ }
+
+ public static List loadRules(String path) throws Exception {
+ // JSON parsing removed to keep the project self-contained.
+ return Collections.emptyList();
+ }
+
+ public static boolean shouldSell(String gearText, List rules) {
+ for (SellRule rule : rules) {
+ if (gearText.contains(rule.rarity) && gearText.contains(rule.gearType)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/rslbot/LogWindow.java b/src/main/java/rslbot/LogWindow.java
new file mode 100644
index 0000000..109f227
--- /dev/null
+++ b/src/main/java/rslbot/LogWindow.java
@@ -0,0 +1,51 @@
+package rslbot;
+
+import javax.swing.*;
+import java.io.OutputStream;
+import java.io.PrintStream;
+
+/**
+ * Utility that redirects System.out/err to a provided JTextArea.
+ * The textarea itself is expected to be displayed by the caller.
+ */
+public final class LogWindow {
+
+ private LogWindow() {
+ // utility
+ }
+
+ /**
+ * Redirects standard output streams to the given text area.
+ */
+ public static void install(JTextArea area) {
+ PrintStream ps = new PrintStream(new TextAreaOutputStream(area), true);
+ System.setOut(ps);
+ System.setErr(ps);
+ }
+
+ private static class TextAreaOutputStream extends OutputStream {
+ private final JTextArea area;
+ private final StringBuilder buffer = new StringBuilder();
+
+ TextAreaOutputStream(JTextArea area) {
+ this.area = area;
+ }
+
+ @Override
+ public void write(int b) {
+ if (b == '\r') {
+ return;
+ }
+ if (b == '\n') {
+ String line = buffer.toString();
+ SwingUtilities.invokeLater(() -> {
+ area.append(line + System.lineSeparator());
+ area.setCaretPosition(area.getDocument().getLength());
+ });
+ buffer.setLength(0);
+ } else {
+ buffer.append((char) b);
+ }
+ }
+ }
+}
diff --git a/src/main/java/rslbot/Main.java b/src/main/java/rslbot/Main.java
new file mode 100644
index 0000000..929f5ca
--- /dev/null
+++ b/src/main/java/rslbot/Main.java
@@ -0,0 +1,12 @@
+package rslbot;
+
+/**
+ * Entry point for launching the bot without the settings UI.
+ */
+public class Main {
+ public static void main(String[] args) throws Exception {
+ System.out.println("=== RSL Bot starting ===");
+ BotCore bot = new BotCore();
+ bot.startBot(50, null); // run 50 battles by default
+ }
+}
diff --git a/src/main/java/rslbot/ScreenUtils.java b/src/main/java/rslbot/ScreenUtils.java
new file mode 100644
index 0000000..6948e39
--- /dev/null
+++ b/src/main/java/rslbot/ScreenUtils.java
@@ -0,0 +1,65 @@
+package rslbot;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.File;
+
+/**
+ * Helper methods for locating images on the screen and clicking within the
+ * Raid: Shadow Legends window without hijacking the user's mouse.
+ */
+public class ScreenUtils {
+ /**
+ * Finds the given template and sends a click to its top-left coordinate.
+ */
+ public boolean findAndClick(String templatePath) {
+ Point p = find(templatePath);
+ if (p != null) {
+ WindowClicker.click(p.x, p.y);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns the on-screen coordinates of the template if it is found with a
+ * reasonable match score, otherwise {@code null}.
+ */
+ public Point find(String templatePath) {
+ try {
+ BufferedImage screenshot = new Robot().createScreenCapture(
+ new Rectangle(Toolkit.getDefaultToolkit().getScreenSize()));
+
+ File file = new File(templatePath);
+ if (!file.exists()) {
+ System.out.println("Template not found: " + templatePath);
+ return null;
+ }
+
+ BufferedImage template = ImageIO.read(file);
+
+ for (int y = 0; y <= screenshot.getHeight() - template.getHeight(); y++) {
+ for (int x = 0; x <= screenshot.getWidth() - template.getWidth(); x++) {
+ if (matches(screenshot, template, x, y)) {
+ return new Point(x, y);
+ }
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ private boolean matches(BufferedImage screen, BufferedImage template, int startX, int startY) {
+ for (int y = 0; y < template.getHeight(); y++) {
+ for (int x = 0; x < template.getWidth(); x++) {
+ if (screen.getRGB(startX + x, startY + y) != template.getRGB(x, y)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/rslbot/SettingsWindow.java b/src/main/java/rslbot/SettingsWindow.java
new file mode 100644
index 0000000..c7c4307
--- /dev/null
+++ b/src/main/java/rslbot/SettingsWindow.java
@@ -0,0 +1,113 @@
+package rslbot;
+
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * Simple Swing window that provides a few configuration options similar to
+ * those found in the RSLHelper application. A progress bar shows run progress
+ * and the Start button toggles to Stop while the bot is active.
+ */
+public class SettingsWindow extends JFrame {
+ private final JCheckBox autoSell;
+ private final JCheckBox enableOcr;
+ private final JSpinner runCount;
+ private final JTextArea logArea = new JTextArea(8, 40);
+ private final JProgressBar progress = new JProgressBar();
+ private final JButton start = new JButton("Start");
+ private final BotCore bot = new BotCore();
+ private Thread botThread;
+
+ public SettingsWindow() {
+ super("RSL Bot Settings");
+ setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ setLayout(new BorderLayout());
+
+ LogWindow.install(logArea);
+
+ // Settings controls
+ autoSell = new JCheckBox("Auto sell gear");
+ enableOcr = new JCheckBox("Enable OCR");
+ runCount = new JSpinner(new SpinnerNumberModel(10, 1, 1000, 1));
+
+ JPanel settingsPanel = new JPanel(new GridLayout(0, 1));
+ settingsPanel.add(autoSell);
+ settingsPanel.add(enableOcr);
+ settingsPanel.add(new JLabel("Runs per session:"));
+ settingsPanel.add(runCount);
+
+ logArea.setEditable(false);
+ JScrollPane logScroll = new JScrollPane(logArea);
+
+ JPanel center = new JPanel(new BorderLayout());
+ center.add(settingsPanel, BorderLayout.NORTH);
+ center.add(logScroll, BorderLayout.CENTER);
+
+ progress.setStringPainted(true);
+
+ JButton screenshot = new JButton("Screenshot");
+ screenshot.addActionListener(e -> WindowClicker.saveScreenshot("raid-window.png"));
+
+ start.addActionListener(e -> toggleBot());
+
+ JButton save = new JButton("Save");
+ save.addActionListener(e -> saveSettings());
+
+ JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT));
+ buttons.add(screenshot);
+ buttons.add(start);
+ buttons.add(save);
+
+ JPanel south = new JPanel(new BorderLayout());
+ south.add(progress, BorderLayout.NORTH);
+ south.add(buttons, BorderLayout.SOUTH);
+
+ add(center, BorderLayout.CENTER);
+ add(south, BorderLayout.SOUTH);
+
+ pack();
+ setLocationRelativeTo(null); // center on screen
+ }
+
+ private void saveSettings() {
+ // In a full implementation, settings would be persisted to disk.
+ // For now, just dump to console.
+ System.out.println("Settings saved:");
+ System.out.println(" Auto sell gear: " + autoSell.isSelected());
+ System.out.println(" Enable OCR: " + enableOcr.isSelected());
+ System.out.println(" Runs per session: " + runCount.getValue());
+ }
+
+ private void toggleBot() {
+ if (botThread != null && botThread.isAlive()) {
+ bot.stop();
+ start.setEnabled(false);
+ return;
+ }
+
+ saveSettings();
+ int runs = (Integer) runCount.getValue();
+ progress.setMaximum(runs);
+ progress.setValue(0);
+ start.setText("Stop");
+
+ botThread = new Thread(() -> {
+ try {
+ bot.startBot(runs, (cur, total) ->
+ SwingUtilities.invokeLater(() -> progress.setValue(cur)));
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ } finally {
+ SwingUtilities.invokeLater(() -> {
+ start.setText("Start");
+ start.setEnabled(true);
+ });
+ }
+ }, "RSL-Bot");
+ botThread.start();
+ }
+
+ public static void main(String[] args) {
+ SwingUtilities.invokeLater(() -> new SettingsWindow().setVisible(true));
+ }
+}
diff --git a/src/main/java/rslbot/WindowClicker.java b/src/main/java/rslbot/WindowClicker.java
new file mode 100644
index 0000000..f974051
--- /dev/null
+++ b/src/main/java/rslbot/WindowClicker.java
@@ -0,0 +1,89 @@
+package rslbot;
+
+import java.awt.Dimension;
+import java.awt.MouseInfo;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.Robot;
+import java.awt.Toolkit;
+import java.awt.event.InputEvent;
+import java.awt.image.BufferedImage;
+import java.io.File;
+
+import javax.imageio.ImageIO;
+
+/**
+ * Utility for sending click events to on-screen coordinates while restoring the
+ * mouse pointer to its original location. This avoids leaving the cursor
+ * elsewhere on the user's desktop and keeps all interaction scoped to the
+ * Raid: Shadow Legends window.
+ */
+public final class WindowClicker {
+ private WindowClicker() {}
+
+ /**
+ * Convenience helper that aims a click at the centre of the screen. The
+ * coordinates may require adjustment for a specific window layout.
+ */
+ public static void clickStartButton() {
+ Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+ click(screen.width / 2, screen.height / 2);
+ }
+
+ /**
+ * Sends a left mouse click to the given screen coordinates, restoring the
+ * cursor to its original position afterwards.
+ *
+ * @param screenX absolute X coordinate
+ * @param screenY absolute Y coordinate
+ * @return {@code true} if the click was dispatched successfully
+ */
+ public static boolean click(int screenX, int screenY) {
+ try {
+ Robot robot = new Robot();
+ Point original = MouseInfo.getPointerInfo().getLocation();
+ robot.mouseMove(screenX, screenY);
+ robot.mousePress(InputEvent.BUTTON1_DOWN_MASK);
+ robot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK);
+ robot.mouseMove(original.x, original.y);
+ return true;
+ } catch (Exception e) {
+ System.out.println("Failed to send click: " + e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * Captures a screenshot of the current desktop and writes it to the
+ * specified file path. The image can then be inspected to determine button
+ * locations.
+ */
+ public static void saveScreenshot(String filePath) {
+ BufferedImage img = captureWindowImage();
+ if (img == null) {
+ return;
+ }
+ try {
+ ImageIO.write(img, "png", new File(filePath));
+ System.out.println("Saved screenshot to " + filePath);
+ } catch (Exception e) {
+ System.out.println("Failed to save screenshot: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Returns a {@link BufferedImage} of the full screen. This method does not
+ * attempt to locate the game window; callers may crop the image as needed.
+ */
+ public static BufferedImage captureWindowImage() {
+ try {
+ Robot robot = new Robot();
+ Rectangle bounds = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
+ return robot.createScreenCapture(bounds);
+ } catch (Exception e) {
+ System.out.println("Failed to capture screenshot: " + e.getMessage());
+ return null;
+ }
+ }
+}
+
diff --git a/src/main/java/rslbot/ocr/OcrReader.java b/src/main/java/rslbot/ocr/OcrReader.java
new file mode 100644
index 0000000..191c064
--- /dev/null
+++ b/src/main/java/rslbot/ocr/OcrReader.java
@@ -0,0 +1,25 @@
+package rslbot.ocr;
+
+import java.awt.*;
+import java.awt.image.BufferedImage;
+
+/**
+ * Minimal placeholder OCR reader. Actual character recognition is omitted to
+ * keep the project self-contained; callers will receive {@code null} instead
+ * of extracted text.
+ */
+public class OcrReader {
+ public OcrReader() {
+ }
+
+ public String readGearPopup() {
+ try {
+ Rectangle popup = new Rectangle(500, 300, 600, 400); // example coords
+ BufferedImage img = new Robot().createScreenCapture(popup);
+ // Real OCR would process 'img' here. Returning null yields no gear text.
+ return null;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+}
diff --git a/src/test/java/rslbot/ChampionUpgradeTest.java b/src/test/java/rslbot/ChampionUpgradeTest.java
new file mode 100644
index 0000000..4c1d032
--- /dev/null
+++ b/src/test/java/rslbot/ChampionUpgradeTest.java
@@ -0,0 +1,20 @@
+package rslbot;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+class ChampionUpgradeTest {
+
+ @Test
+ void factorialGrowth() {
+ assertEquals(1, ChampionUpgrade.championsNeeded(1));
+ assertEquals(2, ChampionUpgrade.championsNeeded(2));
+ assertEquals(6, ChampionUpgrade.championsNeeded(3));
+ assertEquals(24, ChampionUpgrade.championsNeeded(4));
+ }
+
+ @Test
+ void invalidRankThrows() {
+ assertThrows(IllegalArgumentException.class, () -> ChampionUpgrade.championsNeeded(0));
+ }
+}