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)); + } +}