From 5d230bbd150f4443a7a838c8c50788ae6efe761d Mon Sep 17 00:00:00 2001 From: Jimmy Gross Date: Tue, 20 May 2025 12:43:12 -0400 Subject: [PATCH 1/6] version 0.8 - still in debug, but after a very small amount of manual effort at combat instructor, completes from start to finish. Noted bugs QUEST GUIDE - Sometimes doesn't open the door, patching. Combat Instructor - Sometimes gets stuck in a state loop, enhancing checks. Mining Guide - Need to extend delay for the bronze dagger, currently random so will do it until the random amount is enough to complete the action --- .../JG-example-plugin.gradle.kts | 20 + .../examples/plugin/JGExampleConfig.java | 49 ++ .../examples/plugin/JGExamplePlugin.java | 59 ++ .../JG-tutorial-island.gradle.kts | 20 + .../JGTutorialIslandOverlay.java | 117 ++++ .../JGTutorialIslandPlugin.java | 99 ++++ .../tutorialisland/JGTutorialIslandState.java | 15 + .../src/main/java/steps/BankerStep.java | 340 ++++++++++++ .../main/java/steps/CombatInstructorStep.java | 429 +++++++++++++++ .../main/java/steps/GielinorGuideStep.java | 253 +++++++++ .../main/java/steps/MagicInstructorStep.java | 342 ++++++++++++ .../src/main/java/steps/MasterChefStep.java | 262 +++++++++ .../src/main/java/steps/MiningGuideStep.java | 351 ++++++++++++ .../main/java/steps/PrayerInstructorStep.java | 270 +++++++++ .../src/main/java/steps/QuestGuideStep.java | 305 ++++++++++ .../main/java/steps/SurvivalExpertStep.java | 520 ++++++++++++++++++ .../src/main/java/steps/TutorialStep.java | 7 + .../src/main/java/util/PathingUtil.java | 5 + .../src/main/java/util/WidgetUtil.java | 5 + build.gradle.kts | 2 +- settings.gradle.kts | 3 +- 21 files changed, 3471 insertions(+), 2 deletions(-) create mode 100644 JG-example-plugin/JG-example-plugin.gradle.kts create mode 100644 JG-example-plugin/src/main/java/net/storm/plugins/examples/plugin/JGExampleConfig.java create mode 100644 JG-example-plugin/src/main/java/net/storm/plugins/examples/plugin/JGExamplePlugin.java create mode 100644 JG-tutorial-island/JG-tutorial-island.gradle.kts create mode 100644 JG-tutorial-island/src/main/java/net/storm/plugins/tutorialisland/JGTutorialIslandOverlay.java create mode 100644 JG-tutorial-island/src/main/java/net/storm/plugins/tutorialisland/JGTutorialIslandPlugin.java create mode 100644 JG-tutorial-island/src/main/java/net/storm/plugins/tutorialisland/JGTutorialIslandState.java create mode 100644 JG-tutorial-island/src/main/java/steps/BankerStep.java create mode 100644 JG-tutorial-island/src/main/java/steps/CombatInstructorStep.java create mode 100644 JG-tutorial-island/src/main/java/steps/GielinorGuideStep.java create mode 100644 JG-tutorial-island/src/main/java/steps/MagicInstructorStep.java create mode 100644 JG-tutorial-island/src/main/java/steps/MasterChefStep.java create mode 100644 JG-tutorial-island/src/main/java/steps/MiningGuideStep.java create mode 100644 JG-tutorial-island/src/main/java/steps/PrayerInstructorStep.java create mode 100644 JG-tutorial-island/src/main/java/steps/QuestGuideStep.java create mode 100644 JG-tutorial-island/src/main/java/steps/SurvivalExpertStep.java create mode 100644 JG-tutorial-island/src/main/java/steps/TutorialStep.java create mode 100644 JG-tutorial-island/src/main/java/util/PathingUtil.java create mode 100644 JG-tutorial-island/src/main/java/util/WidgetUtil.java diff --git a/JG-example-plugin/JG-example-plugin.gradle.kts b/JG-example-plugin/JG-example-plugin.gradle.kts new file mode 100644 index 0000000..9ba7d6a --- /dev/null +++ b/JG-example-plugin/JG-example-plugin.gradle.kts @@ -0,0 +1,20 @@ +version = "0.0.1" + +project.extra["PluginName"] = "JG Looped Plugin" +project.extra["PluginDescription"] = "First attempt at a torm 2 example" + +tasks { + jar { + manifest { + attributes( + mapOf( + "Plugin-Version" to project.version, + "Plugin-Id" to nameToId(project.extra["PluginName"] as String), + "Plugin-Provider" to project.extra["PluginProvider"], + "Plugin-Description" to project.extra["PluginDescription"], + "Plugin-License" to project.extra["PluginLicense"] + ) + ) + } + } +} diff --git a/JG-example-plugin/src/main/java/net/storm/plugins/examples/plugin/JGExampleConfig.java b/JG-example-plugin/src/main/java/net/storm/plugins/examples/plugin/JGExampleConfig.java new file mode 100644 index 0000000..f902d75 --- /dev/null +++ b/JG-example-plugin/src/main/java/net/storm/plugins/examples/plugin/JGExampleConfig.java @@ -0,0 +1,49 @@ +package net.storm.plugins.examples.plugin; + + +import net.storm.api.plugins.SoxExclude; +import net.storm.api.plugins.config.Config; +import net.storm.api.plugins.config.ConfigGroup; +import net.storm.api.plugins.config.ConfigItem; + +@ConfigGroup(JGExampleConfig.GROUP) +@SoxExclude // Exclude from obfuscation +public interface JGExampleConfig extends Config { + String GROUP = "JG-example-plugin"; + + @ConfigItem( + keyName = "npcName", + name = "NPC Name", + description = "Name of the NPC to attack" + ) + default String npcName() { + return "Goblin"; + } + + @ConfigItem( + keyName = "eatFood", + name = "Eat Food?", + description = "Eat food when low hp?" + ) + default boolean eatFood() { + return true; + } + + @ConfigItem( + keyName = "foodName", + name = "Food Name", + description = "Name of the food to eat" + ) + default String foodName() { + return "Lobster"; + } + + @ConfigItem( + keyName = "foodHp", + name = "Food HP %", + description = "HP percentage to eat food at" + ) + default int foodHp() { + return 50; + } +} diff --git a/JG-example-plugin/src/main/java/net/storm/plugins/examples/plugin/JGExamplePlugin.java b/JG-example-plugin/src/main/java/net/storm/plugins/examples/plugin/JGExamplePlugin.java new file mode 100644 index 0000000..6fe24d5 --- /dev/null +++ b/JG-example-plugin/src/main/java/net/storm/plugins/examples/plugin/JGExamplePlugin.java @@ -0,0 +1,59 @@ +package net.storm.plugins.examples.plugin; + +import net.storm.plugins.examples.plugin.JGExampleConfig; +import com.google.inject.Inject; +import com.google.inject.Provides; +import net.storm.api.plugins.PluginDescriptor; +import net.storm.api.plugins.config.ConfigManager; +import net.storm.sdk.entities.NPCs; +import net.storm.sdk.entities.Players; +import net.storm.sdk.game.Combat; +import net.storm.sdk.items.Inventory; +import net.storm.sdk.plugins.LoopedPlugin; +import org.pf4j.Extension; + +/* + * A very basic example of a looped plugin. + * + * Important notes: look at the imports! The class names are similar to RuneLite's API, but they are not the same. + * Always use the Storm SDK's classes when developing plugins. + * + * Ensure that your package names start with net.storm.plugins, or your plugin will not be compatible with the SDN. + */ +@PluginDescriptor( + name = "Jimmy Example Plugin", + description = "A simple demonstration plugin.", + enabledByDefault = false +) +@Extension +public class JGExamplePlugin extends LoopedPlugin { + @Inject + private JGExampleConfig config; + + @Override + protected int loop() { + if (config.eatFood() && Combat.getHealthPercent() < config.foodHp()) { + var food = Inventory.getFirst(config.foodName()); + if (food != null) { + food.interact("Eat"); + return 1200; // Eat, do not execute any other actions if eating, and wait for 1200 milliseconds + } + } + + var localPlayer = Players.getLocal(); + var npc = NPCs.query() + .names(config.npcName()) + .results() + .nearest(localPlayer); + if (npc != null && !localPlayer.isInteracting() && npc.isInteractable()) { + npc.interact("Attack"); + } + + return 1000; // Sleep for 1000 milliseconds + } + + @Provides + JGExampleConfig provideConfig(ConfigManager configManager) { + return configManager.getConfig(JGExampleConfig.class); + } +} diff --git a/JG-tutorial-island/JG-tutorial-island.gradle.kts b/JG-tutorial-island/JG-tutorial-island.gradle.kts new file mode 100644 index 0000000..9ae60d7 --- /dev/null +++ b/JG-tutorial-island/JG-tutorial-island.gradle.kts @@ -0,0 +1,20 @@ +version = "0.0.1" + +project.extra["PluginName"] = "Tutorial Island Bot" +project.extra["PluginDescription"] = "Automates Tutorial Island from scratch." + +tasks { + jar { + manifest { + attributes( + mapOf( + "Plugin-Version" to project.version, + "Plugin-Id" to nameToId(project.extra["PluginName"] as String), + "Plugin-Provider" to project.extra["PluginProvider"], + "Plugin-Description" to project.extra["PluginDescription"], + "Plugin-License" to project.extra["PluginLicense"] + ) + ) + } + } +} diff --git a/JG-tutorial-island/src/main/java/net/storm/plugins/tutorialisland/JGTutorialIslandOverlay.java b/JG-tutorial-island/src/main/java/net/storm/plugins/tutorialisland/JGTutorialIslandOverlay.java new file mode 100644 index 0000000..2fb0d48 --- /dev/null +++ b/JG-tutorial-island/src/main/java/net/storm/plugins/tutorialisland/JGTutorialIslandOverlay.java @@ -0,0 +1,117 @@ +package net.storm.plugins.tutorialisland; + +import net.runelite.client.ui.overlay.OverlayPanel; +import net.runelite.client.ui.overlay.OverlayLayer; +import net.runelite.client.ui.overlay.OverlayPriority; +import net.runelite.client.ui.overlay.components.LineComponent; +import net.runelite.client.ui.overlay.components.PanelComponent; +import net.storm.api.plugins.Task; +import net.storm.sdk.game.Combat; +import steps.*; + +import javax.inject.Inject; +import java.awt.*; + +public class JGTutorialIslandOverlay extends OverlayPanel +{ + private final JGTutorialIslandPlugin plugin; + + @Inject + public JGTutorialIslandOverlay(JGTutorialIslandPlugin plugin) + { + this.plugin = plugin; + setLayer(OverlayLayer.ABOVE_WIDGETS); // or ALWAYS_ON_TOP + setPriority(OverlayPriority.LOW); + } + + @Override + public Dimension render(Graphics2D graphics) + { + // Basic box, but you can style as you like + panelComponent.getChildren().clear(); + panelComponent.setPreferredSize(new Dimension(220, 0)); + + panelComponent.getChildren().add(LineComponent.builder() + .left("Tutorial Island Bot") + .right("Debug") + .build()); + + panelComponent.getChildren().add(LineComponent.builder() + .left("Current Step") + .right(plugin.getCurrentState().toString()) + .build()); + + panelComponent.getChildren().add(LineComponent.builder() + .left("Tick") + .right(Integer.toString(plugin.getTickCount())) + .build()); + + for (Task task : plugin.getTasks()) { + if (plugin.getCurrentState() == JGTutorialIslandState.SURVIVAL_EXPERT && task instanceof SurvivalExpertStep) { + SurvivalExpertStep s = (SurvivalExpertStep) task; + panelComponent.getChildren().add(LineComponent.builder() + .left("Substate") + .right(String.valueOf(s.getSubState())) + .build()); + } + if (plugin.getCurrentState() == JGTutorialIslandState.GIELINOR_GUIDE && task instanceof GielinorGuideStep) { + GielinorGuideStep g = (GielinorGuideStep) task; + panelComponent.getChildren().add(LineComponent.builder() + .left("Substate") + .right(String.valueOf(g.getSubState())) + .build()); + } + if (plugin.getCurrentState() == JGTutorialIslandState.MASTER_CHEF && task instanceof MasterChefStep) { + MasterChefStep c = (MasterChefStep) task; + panelComponent.getChildren().add(LineComponent.builder() + .left("Substate") + .right(String.valueOf(c.getSubState())) + .build()); + } + if (plugin.getCurrentState() == JGTutorialIslandState.QUEST_GUIDE && task instanceof QuestGuideStep) { + QuestGuideStep q = (QuestGuideStep) task; + panelComponent.getChildren().add(LineComponent.builder() + .left("Substate") + .right(String.valueOf(q.getSubState())) + .build()); + } + if (plugin.getCurrentState() == JGTutorialIslandState.MINING_INSTRUCTOR && task instanceof MiningGuideStep) { + MiningGuideStep m = (MiningGuideStep) task; + panelComponent.getChildren().add(LineComponent.builder() + .left("Substate") + .right(String.valueOf(m.getSubState())) + .build()); + } + if (plugin.getCurrentState() == JGTutorialIslandState.COMBAT_INSTRUCTOR && task instanceof CombatInstructorStep) { + CombatInstructorStep cb = (CombatInstructorStep) task; + panelComponent.getChildren().add(LineComponent.builder() + .left("Substate") + .right(String.valueOf(cb.getSubState())) + .build()); + } + if (plugin.getCurrentState() == JGTutorialIslandState.BANKER && task instanceof BankerStep) { + BankerStep b = (BankerStep) task; + panelComponent.getChildren().add(LineComponent.builder() + .left("Substate") + .right(String.valueOf(b.getSubState())) + .build()); + } + if (plugin.getCurrentState() == JGTutorialIslandState.PRAYER_INSTRUCTOR && task instanceof PrayerInstructorStep) { + PrayerInstructorStep p = (PrayerInstructorStep) task; + panelComponent.getChildren().add(LineComponent.builder() + .left("Substate") + .right(String.valueOf(p.getSubState())) + .build()); + } + if (plugin.getCurrentState() == JGTutorialIslandState.MAGIC_INSTRUCTOR && task instanceof MagicInstructorStep) { + MagicInstructorStep m = (MagicInstructorStep) task; + panelComponent.getChildren().add(LineComponent.builder() + .left("Substate") + .right(String.valueOf(m.getSubState())) + .build()); + } + } + + return super.render(graphics); + } +} diff --git a/JG-tutorial-island/src/main/java/net/storm/plugins/tutorialisland/JGTutorialIslandPlugin.java b/JG-tutorial-island/src/main/java/net/storm/plugins/tutorialisland/JGTutorialIslandPlugin.java new file mode 100644 index 0000000..21c0124 --- /dev/null +++ b/JG-tutorial-island/src/main/java/net/storm/plugins/tutorialisland/JGTutorialIslandPlugin.java @@ -0,0 +1,99 @@ +package net.storm.plugins.tutorialisland; + +import com.google.inject.Inject; +import lombok.Getter; +import net.storm.api.plugins.PluginDescriptor; +import net.storm.api.plugins.Task; +import net.storm.sdk.plugins.TaskPlugin; +import net.storm.sdk.script.paint.DefaultPaint; +import net.runelite.client.ui.overlay.OverlayManager; +import org.pf4j.Extension; +import steps.*; + +@PluginDescriptor( + name = "Tutorial Island Bot", + description = "Automates Tutorial Island from scratch", + enabledByDefault = false +) +@Extension +public class JGTutorialIslandPlugin extends TaskPlugin { + @Getter + private JGTutorialIslandState currentState = JGTutorialIslandState.GIELINOR_GUIDE; + @Getter + private int tickCount = 0; + + @Inject + private JGTutorialIslandOverlay overlay; + + @Inject + private OverlayManager overlayManager; + + // --- PERSISTENT STEP INSTANCES --- + private final GielinorGuideStep gielinorGuideStep = new GielinorGuideStep(this); + private final SurvivalExpertStep survivalExpertStep = new SurvivalExpertStep(this); + private final MasterChefStep masterChefStep = new MasterChefStep(this); + private final QuestGuideStep questGuideStep = new QuestGuideStep(this); + private final MiningGuideStep miningGuideStep = new MiningGuideStep(this); + private final CombatInstructorStep combatInstructorStep = new CombatInstructorStep(this); + private final BankerStep bankerStep = new BankerStep(this); + private final PrayerInstructorStep prayerInstructorStep = new PrayerInstructorStep(this); + private final MagicInstructorStep magicInstructorStep = new MagicInstructorStep(this); + @Override + public void startUp() { + overlayManager.add(overlay); + setCurrentState(JGTutorialIslandState.GIELINOR_GUIDE); + } + + @Override + public void shutDown() { + overlayManager.remove(overlay); + } + + // Return the steps only ONCE; but you will only run the ACTIVE one + @Override + public Task[] getTasks() { + return new Task[] { + gielinorGuideStep, + survivalExpertStep, + masterChefStep, + questGuideStep, + miningGuideStep, + combatInstructorStep, + bankerStep, + prayerInstructorStep, + magicInstructorStep, + + + }; + } + + public void incrementTick() { + tickCount++; + } + + public void resetTickCount() { + tickCount = 0; + } + + public void setCurrentState(JGTutorialIslandState newState) { + if (this.currentState != newState) { + System.out.println("Tutorial Island State changed: " + this.currentState + " → " + newState); + this.currentState = newState; + resetTickCount(); + // Optionally: update overlay, log, trigger events, etc. + } + } + + // --- Only run the active step --- + @Override + protected int loop() { + // Only execute the step where validate() is true (should only be one) + for (Task task : getTasks()) { + if (task instanceof steps.TutorialStep && ((steps.TutorialStep) task).validate()) { + return task.execute(); + } + } + // If no step matched, just sleep + return 600; + } +} diff --git a/JG-tutorial-island/src/main/java/net/storm/plugins/tutorialisland/JGTutorialIslandState.java b/JG-tutorial-island/src/main/java/net/storm/plugins/tutorialisland/JGTutorialIslandState.java new file mode 100644 index 0000000..05a051c --- /dev/null +++ b/JG-tutorial-island/src/main/java/net/storm/plugins/tutorialisland/JGTutorialIslandState.java @@ -0,0 +1,15 @@ +package net.storm.plugins.tutorialisland; + +public enum JGTutorialIslandState { + GIELINOR_GUIDE, + SURVIVAL_EXPERT, + MASTER_CHEF, + QUEST_GUIDE, + MINING_INSTRUCTOR, + COMBAT_INSTRUCTOR, + BANKER, + PRAYER_INSTRUCTOR, + IRONMAN_INSTRUCTOR, + MAGIC_INSTRUCTOR, + COMPLETE +} diff --git a/JG-tutorial-island/src/main/java/steps/BankerStep.java b/JG-tutorial-island/src/main/java/steps/BankerStep.java new file mode 100644 index 0000000..212c51e --- /dev/null +++ b/JG-tutorial-island/src/main/java/steps/BankerStep.java @@ -0,0 +1,340 @@ +package steps; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.storm.api.domain.actors.INPC; +import net.storm.plugins.tutorialisland.JGTutorialIslandPlugin; +import net.storm.plugins.tutorialisland.JGTutorialIslandState; +import net.storm.sdk.entities.NPCs; +import net.storm.sdk.entities.Players; +import net.storm.sdk.movement.Movement; +import net.storm.sdk.widgets.Dialog; +import net.storm.sdk.widgets.Widgets; +import net.storm.api.domain.widgets.IWidget; +import net.storm.api.domain.tiles.ITileObject; + +@Slf4j +public class BankerStep implements TutorialStep { + private final JGTutorialIslandPlugin plugin; + + // TODO: Replace all these with real IDs and coords + private static final int BANK_BOOTH_ID = 10083; + private static final int POLL_BOOTH_ID = 26815; + private static final int ACCOUNT_GUIDE_ID = 3310; + private static final int BANK_BOOTH_X = 3122, BANK_BOOTH_Y = 3123, PLANE = 0; + private static final int POLL_BOOTH_X = 3120, POLL_BOOTH_Y = 3121; + private static final int ACCOUNT_GUIDE_X = 3125, ACCOUNT_GUIDE_Y = 3124; + + // Widget IDs for Bank and Poll Booth interfaces (fill these in from DevTools if you want to check they're open/close) + private static final int ACCOUNT_MANAGE_PARENT = 164, ACCOUNT_MANAGE_CHILD = 39; + + private enum SubState { + WALK_TO_BANK, + OPEN_BANK, + CLOSE_BANK_IF_OPEN, + WALK_TO_POLL_BOOTH, + OPEN_POLL_BOOTH, + CLOSE_POLL_IF_OPEN, + WALK_TO_ACCOUNT_GUIDE, + TALK_ACCOUNT_GUIDE, + CLICK_ACCOUNT_MANAGEMENT, + FINAL_DIALOGUE, + MOVE_ON + } + @Getter + private SubState subState; + + public BankerStep(JGTutorialIslandPlugin plugin) { + this.plugin = plugin; + this.subState = SubState.WALK_TO_BANK; + } + + private void setSubState(SubState next) { + if (subState != next) log.info("Changing substate: {} -> {}", subState, next); + subState = next; + } + + @Override + public boolean validate() { + boolean valid = plugin.getCurrentState() == JGTutorialIslandState.BANKER; + log.info("Validate called: currentState={}, valid={}", plugin.getCurrentState(), valid); + return valid; + } + + @Override + public int execute() { + plugin.incrementTick(); + if (!Dialog.isOpen() && !Players.getLocal().isInteracting()) { + SubState inferred = inferSubState(); + if (inferred != subState) setSubState(inferred); + } + log.info("---- BankerStep Tick ---- Current subState: {}", subState); + + switch (subState) { + case WALK_TO_BANK: { + if (!isAtBankBooth()) { + Movement.walkTo(BANK_BOOTH_X, BANK_BOOTH_Y, PLANE); + log.info("Walking to Bank booth."); + return randomDelay(1000); + } + setSubState(SubState.OPEN_BANK); + return randomDelay(500); + } + case OPEN_BANK: { + ITileObject bankBooth = getNearestObject(BANK_BOOTH_ID); + if (bankBooth != null) { + bankBooth.interact("Use"); + log.info("Interacting with Bank booth."); + setSubState(SubState.CLOSE_BANK_IF_OPEN); + return randomDelay(1200); + } + log.warn("Couldn't find bank booth!"); + return randomDelay(600); + } + case CLOSE_BANK_IF_OPEN: { + // Optionally, detect if the bank interface is open and close it, or just wait a second + if (isBankWidgetOpen()) { + closeBankWidget(); + log.info("Closing bank interface."); + setSubState(SubState.WALK_TO_POLL_BOOTH); + return randomDelay(1000); + } + setSubState(SubState.WALK_TO_POLL_BOOTH); + return randomDelay(500); + } + case WALK_TO_POLL_BOOTH: { + if (!isAtPollBooth()) { + Movement.walkTo(POLL_BOOTH_X, POLL_BOOTH_Y, PLANE); + log.info("Walking to Poll booth."); + return randomDelay(1000); + } + setSubState(SubState.OPEN_POLL_BOOTH); + return randomDelay(500); + } + case OPEN_POLL_BOOTH: { + ITileObject pollBooth = getNearestObject(POLL_BOOTH_ID); + if (pollBooth != null) { + pollBooth.interact("Use"); + log.info("Interacting with Poll booth."); + setSubState(SubState.CLOSE_POLL_IF_OPEN); + return randomDelay(1200); + } + log.warn("Couldn't find poll booth!"); + return randomDelay(600); + } + case CLOSE_POLL_IF_OPEN: { + if (isPollWidgetOpen()) { + closePollWidget(); + log.info("Closing poll booth interface."); + setSubState(SubState.WALK_TO_ACCOUNT_GUIDE); + return randomDelay(1000); + } + setSubState(SubState.WALK_TO_ACCOUNT_GUIDE); + return randomDelay(500); + } + case WALK_TO_ACCOUNT_GUIDE: { + if (!isAtAccountGuide()) { + Movement.walkTo(ACCOUNT_GUIDE_X, ACCOUNT_GUIDE_Y, PLANE); + log.info("Walking to Account Guide."); + return randomDelay(1000); + } + setSubState(SubState.TALK_ACCOUNT_GUIDE); + return randomDelay(500); + } + case TALK_ACCOUNT_GUIDE: { + // If dialog options are up, pick first option + if (getWidgetSafe(263,1,0).getText().toLowerCase().contains( + "continue through the" + )){ + setSubState(SubState.MOVE_ON); + } + if (getWidgetSafe(263,1,0).getText().toLowerCase() + .contains("click on the flashing icon to open your account management")) + {setSubState((SubState.CLICK_ACCOUNT_MANAGEMENT));} + // After dialog fully ends, THEN move to next substate + if (!Dialog.isOpen() && !Dialog.isViewingOptions()) { + setSubState(SubState.CLICK_ACCOUNT_MANAGEMENT); // or whatever is next + log.info("Dialog with Account Guide finished, moving to ACCOUNT_MANAGEMENT step."); + return randomDelay(600); + } + if (Dialog.isViewingOptions()) { + Dialog.chooseOption(1); + log.info("Choosing dialog option 1 for Account Guide."); + return randomDelay(400); + } + // If dialog open, just continue + if (Dialog.isOpen() && Dialog.canContinue()) { + Dialog.continueSpace(); + log.info("Continuing dialog with Account Guide."); + return randomDelay(200); + } + // Only try to talk if NOT already in dialog, and you are at the Account Guide + INPC guide = getAccountGuide(); + if (guide != null && !Players.getLocal().isInteracting() && isAt(guide)) { + guide.interact("Talk-to"); + log.info("Talking to Account Guide."); + return randomDelay(1200); + } + return randomDelay(600); + } + case CLICK_ACCOUNT_MANAGEMENT: { + // Check for widget and click if required (account management menu) + IWidget manageWidget = getWidgetSafe(ACCOUNT_MANAGE_PARENT, ACCOUNT_MANAGE_CHILD); + if (manageWidget != null && manageWidget.isVisible()) { + manageWidget.click(); + log.info("Clicked Account Management widget."); + setSubState(SubState.FINAL_DIALOGUE); + return randomDelay(800); + } + setSubState(SubState.FINAL_DIALOGUE); + return randomDelay(500); + } + case FINAL_DIALOGUE: { + if (getWidgetSafe(263,1,0).getText().toLowerCase().contains( + "continue through the" + )){ + setSubState(SubState.MOVE_ON); + } + if (Dialog.isViewingOptions()) { + Dialog.chooseOption(1); + log.info("Choosing dialog option 1 for Account Guide."); + return randomDelay(400); + } + // If dialog open, just continue + if (Dialog.isOpen() && Dialog.canContinue()) { + Dialog.continueSpace(); + log.info("Continuing dialog with Account Guide."); + return randomDelay(200); + } + INPC guide = getAccountGuide(); + if (guide != null && !Players.getLocal().isInteracting() && isAt(guide)) { + guide.interact("Talk-to"); + log.info("Talking to Account Guide."); + return randomDelay(1200); + } + setSubState(SubState.MOVE_ON); + return randomDelay(600); + } + case MOVE_ON: { + log.info("Completed Banker step! Setting plugin state to next step."); + plugin.setCurrentState(JGTutorialIslandState.PRAYER_INSTRUCTOR); // Set whatever comes next + return randomDelay(600); + } + } + return randomDelay(600); + } + + // --- Utility: get nearest bank/poll booth, or account guide NPC --- + private ITileObject getNearestObject(int id) { + try { + return net.storm.sdk.entities.TileObjects.getNearest(id); + } catch (Exception e) { + log.warn("getNearestObject({}): {}", id, e.toString()); + return null; + } + } + + private INPC getAccountGuide() { + try { + return NPCs.query().ids(ACCOUNT_GUIDE_ID).results().nearest(Players.getLocal()); + } catch (Exception e) { + log.warn("getAccountGuide exception: {}", e.toString()); + return null; + } + } + + // --- At-position checks --- + private boolean isAtBankBooth() { + if (Players.getLocal() == null || Players.getLocal().getWorldLocation() == null) + return false; + int px = Players.getLocal().getWorldLocation().getX(); + int py = Players.getLocal().getWorldLocation().getY(); + return (Math.abs(px - BANK_BOOTH_X) <= 1 && Math.abs(py - BANK_BOOTH_Y) <= 1); + } + + private boolean isAtPollBooth() { + if (Players.getLocal() == null || Players.getLocal().getWorldLocation() == null) + return false; + int px = Players.getLocal().getWorldLocation().getX(); + int py = Players.getLocal().getWorldLocation().getY(); + return (Math.abs(px - POLL_BOOTH_X) <= 1 && Math.abs(py - POLL_BOOTH_Y) <= 1); + } + + private boolean isAtAccountGuide() { + if (Players.getLocal() == null || Players.getLocal().getWorldLocation() == null) + return false; + int px = Players.getLocal().getWorldLocation().getX(); + int py = Players.getLocal().getWorldLocation().getY(); + return (Math.abs(px - ACCOUNT_GUIDE_X) <= 1 && Math.abs(py - ACCOUNT_GUIDE_Y) <= 1); + } + + private boolean isAt(INPC npc) { + try { + return npc != null && Players.getLocal() != null && + Players.getLocal().getWorldLocation() != null && npc.getWorldLocation() != null && + Players.getLocal().getWorldLocation().distanceTo(npc.getWorldLocation()) <= 2; + } catch (Exception e) { + return false; + } + } + + // --- Widget helpers --- + private IWidget getWidgetSafe(int parent, int child) { + try { return Widgets.get(parent, child); } + catch (Exception e) { return null; } + } + private IWidget getWidgetSafe(int parent, int child, int index) { + try { return Widgets.get(parent, child, index); } + catch (Exception e) { return null; } + } + + // Check if bank interface is open + private boolean isBankWidgetOpen() { + IWidget bankWidget = getWidgetSafe(12, 0); + return bankWidget != null && bankWidget.isVisible(); + } + + private void closeBankWidget() { + IWidget closeButton = getWidgetSafe(12, 11); + if (closeButton != null && closeButton.isVisible()) closeButton.click(); + } + + // Check if poll booth widget is open (usually 310, 2 or 310, 3) + private boolean isPollWidgetOpen() { + IWidget pollWidget = getWidgetSafe(310, 2); + return pollWidget != null && pollWidget.isVisible(); + } + + private void closePollWidget() { + IWidget closeButton = getWidgetSafe(310, 7); // Poll booth close button usually here + if (closeButton != null && closeButton.isVisible()) closeButton.click(); + } + + // --- Core state inference, structure as usual --- + private SubState inferSubState() { + // Example of sequencing logic + if (!isAtBankBooth()) + return SubState.WALK_TO_BANK; + if (!isBankWidgetOpen()) + return SubState.OPEN_BANK; + if (isBankWidgetOpen()) + return SubState.CLOSE_BANK_IF_OPEN; + // ... etc + return subState; + } + + // --- Misc --- +// Add a base random delay for human-like timing + private int randomDelay(int base) { + return base + (int) (Math.random() * 200) - 100; + } + + // --- Completion --- + @Override + public boolean isComplete() { + // Done if plugin has moved to next state + boolean complete = plugin.getCurrentState() != JGTutorialIslandState.BANKER; + log.info("isComplete called: currentState={}, complete={}", plugin.getCurrentState(), complete); + return complete; + } +} diff --git a/JG-tutorial-island/src/main/java/steps/CombatInstructorStep.java b/JG-tutorial-island/src/main/java/steps/CombatInstructorStep.java new file mode 100644 index 0000000..9c5eb47 --- /dev/null +++ b/JG-tutorial-island/src/main/java/steps/CombatInstructorStep.java @@ -0,0 +1,429 @@ +package steps; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.storm.api.domain.actors.INPC; +import net.storm.plugins.tutorialisland.JGTutorialIslandPlugin; +import net.storm.plugins.tutorialisland.JGTutorialIslandState; +import net.storm.sdk.game.Combat; +import net.storm.sdk.entities.NPCs; +import net.storm.sdk.entities.Players; +import net.storm.sdk.items.Equipment; +import net.storm.sdk.items.Inventory; +import net.storm.sdk.movement.Movement; +import net.storm.sdk.widgets.Dialog; +import net.storm.sdk.widgets.Widgets; +import net.storm.api.domain.items.IInventoryItem; +import net.storm.api.domain.widgets.IWidget; +import net.storm.api.domain.tiles.ITileObject; + +/** + * Handles all substeps for the Combat Instructor segment of Tutorial Island. + * Strongly debounced, safe to run at any point, and immune to most state/step bugs. + */ +@Slf4j +public class CombatInstructorStep implements TutorialStep { + private final JGTutorialIslandPlugin plugin; + + // --- Game constants --- + private static final int INSTRUCTOR_ID = 3307; + private static final int GIANT_RAT_ID = 3313; + private static final int DAGGER_ID = 1205, SWORD_ID = 1277, SHIELD_ID = 1171, BOW_ID = 841, ARROW_ID = 882; + private static final int INSTRUCTOR_X = 3105, INSTRUCTOR_Y = 9507, INSTRUCTOR_PLANE = 0; + + private static final int EQUIPMENT_TAB_PARENT = 164, EQUIPMENT_TAB_CHILD = 63; + private static final int EQUIP_STATS_TAB_PARENT = 387, EQUIP_STATS_TAB_CHILD = 1; + private static final int COMBAT_STYLES_WIDGET_PARENT = 164, COMBAT_STYLES_WIDGET_CHILD = 52; + + private static final int RAT_AREA_X = 3104; + private static final int RAT_AREA_Y = 9518; + private static final int RAT_AREA_VAR = 1; // ±1 tile for human-like movement + private static final int RAT_DOOR_ID = 9720; + + private enum SubState { + WALK_TO_INSTRUCTOR, TALK_INSTRUCTOR_1, OPEN_EQUIPMENT, OPEN_EQUIP_STATS, EQUIP_DAGGER, + TALK_INSTRUCTOR_2, OPEN_EQUIPMENT_2, EQUIP_SWORD_SHIELD, OPEN_COMBAT_STYLES, + WALK_TO_RAT, KILL_MELEE_RAT, TALK_INSTRUCTOR_3, EQUIP_BOW_ARROWS, + KILL_RANGE_RAT, FINAL_DIALOGUE, MOVE_ON + } + @Getter + private SubState subState; + + public CombatInstructorStep(JGTutorialIslandPlugin plugin) { + this.plugin = plugin; + this.subState = SubState.WALK_TO_INSTRUCTOR; + } + + private void setSubState(SubState next) { + if (subState != next) log.info("Changing substate: {} -> {}", subState, next); + subState = next; + } + + @Override + public boolean validate() { + boolean valid = plugin.getCurrentState() == JGTutorialIslandState.COMBAT_INSTRUCTOR; + log.info("Validate called: currentState={}, valid={}", plugin.getCurrentState(), valid); + return valid; + } + + @Override + public int execute() { + plugin.incrementTick(); + + // Infer and process substate (no return on substate change) + boolean inferredAndProcessed; + do { + inferredAndProcessed = false; + if (!Dialog.isOpen() && !Players.getLocal().isInteracting()) { + SubState inferred = inferSubState(); + if (inferred != subState) { + setSubState(inferred); + inferredAndProcessed = true; // Reprocess below + } + } + } while (inferredAndProcessed); + + log.info("---- CombatInstructorStep Tick ---- Current subState: {}", subState); + + switch (subState) { + case WALK_TO_INSTRUCTOR: { + INPC instructor = getInstructorSafe(); + if (instructor == null || !isAt(instructor)) { + log.info("Walking to Combat Instructor at {},{},{}", INSTRUCTOR_X, INSTRUCTOR_Y, INSTRUCTOR_PLANE); + Movement.walkTo(INSTRUCTOR_X, INSTRUCTOR_Y, INSTRUCTOR_PLANE); + return randomDelay(1000); + } + if (hasKilledMeleeRat()) { + setSubState(SubState.TALK_INSTRUCTOR_3); + return 0; + } + setSubState(SubState.TALK_INSTRUCTOR_1); + return randomDelay(600); + } + + case TALK_INSTRUCTOR_1: { + if (handleDialog()) return randomDelay(200); + + if (hasWidgetText("equipping items")) { + setSubState(SubState.OPEN_EQUIPMENT); + return 0; + } + if (hasSwordAnywhere() && hasShieldAnywhere()) { + setSubState(SubState.OPEN_EQUIPMENT_2); + return randomDelay(600); + } + INPC instructor = getInstructorSafe(); + if (instructor != null && !Players.getLocal().isInteracting() && isAt(instructor) + && !hasWidgetText("equipping items")) { + instructor.interact("Talk-to"); + return randomDelay(1200); + } + return randomDelay(600); + } + + case OPEN_EQUIPMENT: { + IWidget equipTab = getWidgetSafe(EQUIPMENT_TAB_PARENT, EQUIPMENT_TAB_CHILD); + if (equipTab != null && equipTab.isVisible()) { + equipTab.click(); + setSubState(SubState.OPEN_EQUIP_STATS); + return randomDelay(600); + } + return randomDelay(600); + } + case OPEN_EQUIP_STATS: { + IWidget statsTab = getWidgetSafe(EQUIP_STATS_TAB_PARENT, EQUIP_STATS_TAB_CHILD); + if (statsTab != null && statsTab.isVisible()) { + statsTab.click(); + setSubState(SubState.EQUIP_DAGGER); + return randomDelay(600); + } + return randomDelay(600); + } + case EQUIP_DAGGER: { + if (!isAt(getInstructorSafe())) { + setSubState(SubState.WALK_TO_INSTRUCTOR); + return randomDelay(600); + } + if (isBlockedByMiningWidget()) { + setSubState(SubState.TALK_INSTRUCTOR_1); + return randomDelay(600); + } + if (Equipment.contains(DAGGER_ID)) { + setSubState(SubState.TALK_INSTRUCTOR_2); + return randomDelay(600); + } + IInventoryItem dagger = Inventory.getFirst(DAGGER_ID); + if (dagger != null) { + dagger.interact("Wield"); + return randomDelay(800); + } + setSubState(SubState.TALK_INSTRUCTOR_1); + return randomDelay(600); + } + + case TALK_INSTRUCTOR_2: { + if (handleDialog()) return randomDelay(200); + + if (hasSwordAnywhere() && hasShieldAnywhere()) { + setSubState(SubState.OPEN_EQUIPMENT_2); + return randomDelay(600); + } + INPC instructor = getInstructorSafe(); + if (instructor != null && !Players.getLocal().isInteracting() && isAt(instructor)) { + instructor.interact("Talk-to"); + return randomDelay(1200); + } + return randomDelay(600); + } + + case OPEN_EQUIPMENT_2: { + IWidget equipTab = getWidgetSafe(EQUIPMENT_TAB_PARENT, EQUIPMENT_TAB_CHILD); + if (equipTab != null && equipTab.isVisible()) { + equipTab.click(); + setSubState(SubState.EQUIP_SWORD_SHIELD); + return randomDelay(600); + } + return randomDelay(600); + } + case EQUIP_SWORD_SHIELD: { + if (Equipment.contains(SWORD_ID) && Equipment.contains(SHIELD_ID)) { + setSubState(SubState.OPEN_COMBAT_STYLES); + return randomDelay(600); + } + IInventoryItem sword = Inventory.getFirst(SWORD_ID); + IInventoryItem shield = Inventory.getFirst(SHIELD_ID); + if (sword != null && !Equipment.contains(SWORD_ID)) { + sword.interact("Wield"); + return randomDelay(600); + } + if (shield != null && !Equipment.contains(SHIELD_ID)) { + shield.interact("Wield"); + return randomDelay(600); + } + return randomDelay(600); + } + + case OPEN_COMBAT_STYLES: { + if (hasKilledMeleeRat()) { + setSubState(SubState.TALK_INSTRUCTOR_3); + return randomDelay(600); + } + IWidget combatTab = getWidgetSafe(COMBAT_STYLES_WIDGET_PARENT, COMBAT_STYLES_WIDGET_CHILD); + if (combatTab != null && combatTab.isVisible()) { + combatTab.click(); + setSubState(SubState.WALK_TO_RAT); + return randomDelay(700); + } + return randomDelay(600); + } + + case WALK_TO_RAT: { + if (hasKilledRangeRat()) { + setSubState(SubState.MOVE_ON); + return randomDelay(600); + } + if (isDoorBlockingRatArea()) { + ITileObject door = getNearestDoor(); + if (door != null && door.isInteractable()) { + door.interact("Open"); + return randomDelay(1000); + } + } + int x = RAT_AREA_X + (int) (Math.random() * (RAT_AREA_VAR * 2 + 1)) - RAT_AREA_VAR; + int y = RAT_AREA_Y + (int) (Math.random() * (RAT_AREA_VAR * 2 + 1)) - RAT_AREA_VAR; + Movement.walkTo(x, y, INSTRUCTOR_PLANE); + setSubState(SubState.KILL_MELEE_RAT); + return randomDelay(1000); + } + + case KILL_MELEE_RAT: { + if (hasWidgetText("i can't reach that")) { + setSubState(SubState.WALK_TO_RAT); + return randomDelay(1000); + } + if (hasKilledMeleeRat()) { + setSubState(SubState.TALK_INSTRUCTOR_3); + return randomDelay(600); + } + if (isPlayerInCombat()) { + return randomDelay(1000); + } + INPC rat = getRatSafe(); + if (rat != null && !rat.isDead() && !rat.isInteracting()) { + rat.interact("Attack"); + return randomDelay(1800); + } + return randomDelay(1000); + } + + case TALK_INSTRUCTOR_3: { + if (handleDialog()) return randomDelay(200); + + if (hasBow() && hasArrows()) { + setSubState(SubState.EQUIP_BOW_ARROWS); + return randomDelay(600); + } + INPC instructor = getInstructorSafe(); + if (instructor != null && !Players.getLocal().isInteracting() && isAt(instructor)) { + instructor.interact("Talk-to"); + return randomDelay(1200); + } + setSubState(SubState.WALK_TO_INSTRUCTOR); // Safety + return randomDelay(600); + } + + case EQUIP_BOW_ARROWS: { + if (Equipment.contains(BOW_ID) && Equipment.contains(ARROW_ID)) { + setSubState(SubState.KILL_RANGE_RAT); + return randomDelay(600); + } + IInventoryItem bow = Inventory.getFirst(BOW_ID); + IInventoryItem arrows = Inventory.getFirst(ARROW_ID); + if (bow != null && !Equipment.contains(BOW_ID)) { + bow.interact("Wield"); + return randomDelay(800); + } + if (arrows != null && !Equipment.contains(ARROW_ID)) { + arrows.interact("Wield"); + return randomDelay(800); + } + return randomDelay(600); + } + case KILL_RANGE_RAT: { + if (hasKilledRangeRat()) { + setSubState(SubState.MOVE_ON); + return randomDelay(600); + } + if (hasWidgetText("i can't reach that")) { + Movement.walkTo(3106, 9510); + return randomDelay(600); + } + INPC rat = getRatSafe(); + if (rat != null && !rat.isDead() && !rat.isInteracting()) { + rat.interact("Attack"); + return randomDelay(1800); + } + return randomDelay(800); + } + case FINAL_DIALOGUE: { + if (handleDialog()) return randomDelay(200); + + INPC instructor = getInstructorSafe(); + if (instructor != null && !Players.getLocal().isInteracting() && isAt(instructor)) { + instructor.interact("Talk-to"); + return randomDelay(1200); + } + IWidget moveOnWidget = getWidgetSafe(263, 1, 0); + if (moveOnWidget != null && moveOnWidget.isVisible()) { + setSubState(SubState.MOVE_ON); + return randomDelay(600); + } + return randomDelay(600); + } + case MOVE_ON: { + plugin.setCurrentState(JGTutorialIslandState.BANKER); + return randomDelay(600); + } + } + return randomDelay(600); + } + + // --- State Inference Logic --- + private SubState inferSubState() { + INPC instructor = getInstructorSafe(); + if (instructor == null || !isAt(instructor)) return SubState.WALK_TO_INSTRUCTOR; + IWidget moveOnWidget = getWidgetSafe(263, 1, 0); + String moveOnText = (moveOnWidget != null && moveOnWidget.isVisible()) ? moveOnWidget.getText() : null; + if (isBlockedByMiningWidget()) return SubState.TALK_INSTRUCTOR_1; + if (moveOnText != null && moveOnText.toLowerCase().contains("moving on")) return SubState.MOVE_ON; + if (Equipment.contains(BOW_ID) && Equipment.contains(ARROW_ID) && !hasKilledRangeRat()) + return SubState.KILL_RANGE_RAT; + if (hasBow() && hasArrows() && (!Equipment.contains(BOW_ID) || !Equipment.contains(ARROW_ID))) + return SubState.EQUIP_BOW_ARROWS; + if (hasKilledRangeRat()) return SubState.FINAL_DIALOGUE; + if (Equipment.contains(SWORD_ID) && Equipment.contains(SHIELD_ID) && !hasKilledMeleeRat()) + return SubState.OPEN_COMBAT_STYLES; + if (hasSword() && hasShield() && (!Equipment.contains(SWORD_ID) || !Equipment.contains(SHIELD_ID))) + return SubState.EQUIP_SWORD_SHIELD; + if (hasKilledMeleeRat()) return SubState.TALK_INSTRUCTOR_3; + if (Equipment.contains(DAGGER_ID)) return SubState.TALK_INSTRUCTOR_2; + if (Inventory.contains(DAGGER_ID) && !Equipment.contains(DAGGER_ID)) return SubState.OPEN_EQUIPMENT; + return SubState.WALK_TO_INSTRUCTOR; + } + + // --- Helpers --- + /** Handles dialog continue/choose for all NPC steps */ + private boolean handleDialog() { + if (Dialog.isViewingOptions()) { + Dialog.chooseOption(1); return true; + } + if (Dialog.isOpen() && Dialog.canContinue()) { + Dialog.continueSpace(); return true; + } + return false; + } + /** Null-safe widget text substring check */ + private boolean hasWidgetText(String snippet) { + IWidget w = getWidgetSafe(263, 1, 0); + return w != null && w.isVisible() && w.getText() != null && w.getText().toLowerCase().contains(snippet.toLowerCase()); + } + + private boolean isPlayerInCombat() { return Players.getLocal() != null && Players.getLocal().isInteracting(); } + private boolean hasSwordAnywhere() { return Inventory.contains(SWORD_ID) || Equipment.contains(SWORD_ID); } + private boolean hasShieldAnywhere() { return Inventory.contains(SHIELD_ID) || Equipment.contains(SHIELD_ID); } + private INPC getInstructorSafe() { + try { return NPCs.query().ids(INSTRUCTOR_ID).results().nearest(Players.getLocal()); } + catch (Exception e) { log.warn("getInstructorSafe exception: {}", e.toString()); return null; } + } + private boolean isAt(INPC npc) { + try { + return npc != null && Players.getLocal() != null && + Players.getLocal().getWorldLocation() != null && npc.getWorldLocation() != null && + Players.getLocal().getWorldLocation().distanceTo(npc.getWorldLocation()) <= 2; + } catch (Exception e) { return false; } + } + private INPC getRatSafe() { + try { return Combat.getAttackableNPC(GIANT_RAT_ID); } + catch (Exception e) { return null; } + } + private IWidget getWidgetSafe(int parent, int child) { + try { return Widgets.get(parent, child); } + catch (Exception e) { return null; } + } + private IWidget getWidgetSafe(int parent, int child, int array) { + try { return Widgets.get(parent, child, array); } + catch (Exception e) { return null; } + } + private boolean hasSword() { return Inventory.contains(SWORD_ID); } + private boolean hasShield() { return Inventory.contains(SHIELD_ID); } + private boolean hasBow() { return Inventory.contains(BOW_ID); } + private boolean hasArrows() { return Inventory.contains(ARROW_ID); } + private boolean hasKilledMeleeRat() { + return hasWidgetText("well done, you've made your first kill"); + } + private boolean hasKilledRangeRat() { + return hasWidgetText("you have completed the tasks here"); + } + private boolean isBlockedByMiningWidget() { + IWidget moveOnWidget = getWidgetSafe(263, 1); + String moveOnText = (moveOnWidget != null && moveOnWidget.isVisible()) ? moveOnWidget.getText() : null; + return moveOnText != null && moveOnText.toLowerCase().contains("congratulations, you've made your first weapon"); + } + private boolean isDoorBlockingRatArea() { + ITileObject door = getNearestDoor(); + return door != null && door.isInteractable(); + } + private ITileObject getNearestDoor() { + try { return net.storm.sdk.entities.TileObjects.getNearest(RAT_DOOR_ID); } + catch (Exception e) { return null; } + } + private int randomDelay(int base) { + return base + (int) (Math.random() * 200) - 100; + } + @Override + public boolean isComplete() { + boolean complete = plugin.getCurrentState() != JGTutorialIslandState.COMBAT_INSTRUCTOR; + log.info("isComplete called: currentState={}, complete={}", plugin.getCurrentState(), complete); + return complete; + } +} diff --git a/JG-tutorial-island/src/main/java/steps/GielinorGuideStep.java b/JG-tutorial-island/src/main/java/steps/GielinorGuideStep.java new file mode 100644 index 0000000..2e18527 --- /dev/null +++ b/JG-tutorial-island/src/main/java/steps/GielinorGuideStep.java @@ -0,0 +1,253 @@ +package steps; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.storm.plugins.tutorialisland.JGTutorialIslandPlugin; +import net.storm.plugins.tutorialisland.JGTutorialIslandState; +import net.storm.sdk.entities.NPCs; +import net.storm.sdk.entities.Players; +import net.storm.sdk.widgets.Dialog; +import net.storm.sdk.widgets.Widgets; +import net.storm.sdk.game.Client; +import net.storm.api.domain.actors.INPC; + +@Slf4j +public class GielinorGuideStep implements TutorialStep { + private final JGTutorialIslandPlugin plugin; + + private static final int GIELINOR_GUIDE_ID = 3308; + private static final int SURVIVAL_EXPERT_ID = 8503; + private static final int SETTINGS_TAB_PARENT = 164, SETTINGS_TAB_CHILD = 41; + private static final int MOVEON_WIDGET_PARENT = 263, MOVEON_WIDGET_CHILD1 = 1, MOVEON_WIDGET_CHILD2 = 0; + private static final int NEAR_DISTANCE = 4; + + private enum SubState { + TALK_GUIDE_1, + OPEN_SETTINGS, + TALK_GUIDE_2, + MOVE_ON + } + @Getter + private SubState subState; + + public GielinorGuideStep(JGTutorialIslandPlugin plugin) { + this.plugin = plugin; + // Always start at a safe default; infer true state after world is loaded + this.subState = SubState.TALK_GUIDE_2; + } + + private void setSubState(SubState next) { + if (subState != next) log.info("Changing substate: {} -> {}", subState, next); + subState = next; + } + + @Override + public boolean validate() { + boolean valid = plugin.getCurrentState() == JGTutorialIslandState.GIELINOR_GUIDE; + log.info("Validate called: currentState={}, valid={}", plugin.getCurrentState(), valid); + return valid; + } + + @Override + public int execute() { + plugin.incrementTick(); + + // Only re-infer state when not in dialog or interacting (and world is loaded) + if (!Dialog.isOpen() && !Players.getLocal().isInteracting()) { + SubState inferred = inferSubState(); + if (inferred != subState) setSubState(inferred); + } + + log.info("---- GielinorGuideStep Tick ---- Current subState: {}", subState); + + switch (subState) { + case TALK_GUIDE_1: { + if (readyToMoveOn()) { + log.info("Guide done, ready to move on."); + setSubState(SubState.MOVE_ON); + } + // If in dialog, continue as usual + if (Dialog.isViewingOptions()) { + log.info("Dialogue options detected: choosing option 1"); + Dialog.chooseOption(1); + return randomDelay(400); + } + if (Dialog.isOpen() && Dialog.canContinue()) { + log.info("Continuing dialogue..."); + Dialog.continueSpace(); + return randomDelay(200); + } + + // If the settings widget is visible, transition to open settings! + var w = Widgets.get(MOVEON_WIDGET_PARENT, MOVEON_WIDGET_CHILD1, MOVEON_WIDGET_CHILD2); + if (w != null && w.isVisible() && w.getText() != null && w.getText().toLowerCase().contains("spanner icon")) { + log.info("Prompt to open settings detected."); + setSubState(SubState.OPEN_SETTINGS); + return randomDelay(400); + } + + // If hint arrow is gone and we're not in dialog, WAIT for widget prompt, don't keep talking + if (dialogueJustFinished()) { + log.info("First dialogue done, waiting for open settings prompt."); + // Do nothing, just idle until widget prompt appears + return randomDelay(600); + } + + // Only talk if arrow is present and we're not already interacting + INPC arrowNpc = Client.getHintArrowNpc(); + INPC guide = getGuide(); + if ((arrowNpc != null && arrowNpc.getId() == GIELINOR_GUIDE_ID && !Players.getLocal().isInteracting()) || + (arrowNpc == null && isAt(guide) && !Players.getLocal().isInteracting())) { + log.info("Interacting with Gielinor Guide..."); + if (arrowNpc != null) arrowNpc.interact("Talk-to"); + else if (guide != null) guide.interact("Talk-to"); + return randomDelay(1200); + } + break; + } + + case OPEN_SETTINGS: { + var settingsTab = Widgets.get(SETTINGS_TAB_PARENT, SETTINGS_TAB_CHILD); + if (settingsTab != null && settingsTab.isVisible()) { + log.info("Opening settings tab ({},{})", SETTINGS_TAB_PARENT, SETTINGS_TAB_CHILD); + settingsTab.click(); + if (Dialog.isOpen() && Dialog.canContinue()) { + log.info("Continue after opening settings."); + Dialog.continueSpace(); + return randomDelay(200); + } + setSubState(SubState.TALK_GUIDE_2); + return randomDelay(600); + } else { + log.info("Settings tab not found or not visible. Waiting..."); + } + break; + } + case TALK_GUIDE_2: { + if (Dialog.isViewingOptions()) { + log.info("Dialogue options (2nd) detected: choosing option 1"); + Dialog.chooseOption(1); + return randomDelay(400); + } + if (Dialog.isOpen() && Dialog.canContinue()) { + log.info("Continuing dialogue (2nd)..."); + Dialog.continueSpace(); + return randomDelay(200); + } + if (readyToMoveOn()) { + log.info("Guide done, ready to move on."); + setSubState(SubState.MOVE_ON); + } + INPC arrowNpc2 = Client.getHintArrowNpc(); + INPC guide = getGuide(); + if ((arrowNpc2 != null && arrowNpc2.getId() == GIELINOR_GUIDE_ID && !Players.getLocal().isInteracting()) || + (arrowNpc2 == null && isAt(guide) && !Players.getLocal().isInteracting())) { + log.info("Interacting with Gielinor Guide again..."); + if (arrowNpc2 != null) arrowNpc2.interact("Talk-to"); + else if (guide != null) guide.interact("Talk-to"); + return randomDelay(1200); + } + if (readyToMoveOn()) { + log.info("Guide done, ready to move on."); + setSubState(SubState.MOVE_ON); + } + break; + } + case MOVE_ON: + log.info("Set plugin state to SURVIVAL_EXPERT"); + plugin.setCurrentState(JGTutorialIslandState.SURVIVAL_EXPERT); + break; + } + + return randomDelay(600); + } + + private INPC getGuide() { + try { + return NPCs.query().ids(GIELINOR_GUIDE_ID).results().nearest(Players.getLocal()); + } catch (Exception e) { + log.warn("NPC query failed (likely world not ready): {}", e.toString()); + return null; + } + } + + private boolean isAt(INPC npc) { + return npc != null && + Players.getLocal() != null && + Players.getLocal().getWorldLocation() != null && + npc.getWorldLocation() != null && + Players.getLocal().getWorldLocation().distanceTo(npc.getWorldLocation()) <= NEAR_DISTANCE; + } + + private boolean dialogueJustFinished() { + INPC arrowNpc = Client.getHintArrowNpc(); + if (arrowNpc == null) { + log.info("No hint arrow present after first guide dialogue."); + return true; + } + return false; + } + + private boolean readyToMoveOn() { + var w = Widgets.get(MOVEON_WIDGET_PARENT, MOVEON_WIDGET_CHILD1, MOVEON_WIDGET_CHILD2); + if (w != null && w.isVisible()) { + String t = w.getText(); + if (t != null && (t.toLowerCase().contains("moving on") || t.toLowerCase().contains("catch some shrimp"))) { + log.info("Ready to move on: widget text = {}", t); + return true; + } + } + INPC arrowNpc = Client.getHintArrowNpc(); + if (arrowNpc != null && arrowNpc.getId() == SURVIVAL_EXPERT_ID) { + log.info("Ready to move on: hint arrow on Survival Expert."); + return true; + } + INPC guide = getGuide(); + if (guide == null) { + log.info("No Gielinor Guide found (possibly already moved on)."); + return true; + } + if (Players.getLocal() == null || Players.getLocal().getWorldLocation() == null || guide.getWorldLocation() == null) { + log.warn("Null player or guide world location detected; cannot compute distance. Not moving on."); + return false; + } + int distance = Players.getLocal().getWorldLocation().distanceTo(guide.getWorldLocation()); + if (distance > 10) { + log.info("Ready to move on: player far from guide."); + return true; + } + return false; + } + + private SubState inferSubState() { + var w = Widgets.get(MOVEON_WIDGET_PARENT, MOVEON_WIDGET_CHILD1, MOVEON_WIDGET_CHILD2); + if (w != null && w.isVisible()) { + String t = w.getText(); + if (t != null) { + t = t.toLowerCase(); + if (t.contains("catch some shrimp") || t.contains("moving on") || t.contains("proceed")) { + log.info("Infer: Widget says 'move on', setting subState=MOVE_ON"); + return SubState.MOVE_ON; + } + if (t.contains("open your settings") || t.contains("you've been given an item")) { + log.info("Infer: Widget says 'open settings' or 'got item', setting subState=OPEN_SETTINGS"); + return SubState.OPEN_SETTINGS; + } + } + } + if (readyToMoveOn()) return SubState.MOVE_ON; + log.info("Infer: Defaulting to TALK_GUIDE_1"); + return SubState.TALK_GUIDE_1; + } + + private int randomDelay(int base) { + return base + (int) (Math.random() * 200) - 100; + } + + @Override + public boolean isComplete() { + boolean complete = plugin.getCurrentState() != JGTutorialIslandState.GIELINOR_GUIDE; + log.info("isComplete called: currentState={}, complete={}", plugin.getCurrentState(), complete); + return complete; + } +} diff --git a/JG-tutorial-island/src/main/java/steps/MagicInstructorStep.java b/JG-tutorial-island/src/main/java/steps/MagicInstructorStep.java new file mode 100644 index 0000000..3923f41 --- /dev/null +++ b/JG-tutorial-island/src/main/java/steps/MagicInstructorStep.java @@ -0,0 +1,342 @@ +package steps; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.storm.api.domain.actors.INPC; +import net.storm.api.domain.actors.IPlayer; +import net.storm.api.magic.Spell; +import net.storm.api.magic.SpellBook; +import net.storm.plugins.tutorialisland.JGTutorialIslandPlugin; +import net.storm.plugins.tutorialisland.JGTutorialIslandState; +import net.storm.sdk.entities.NPCs; +import net.storm.sdk.entities.Players; +import net.storm.sdk.movement.Movement; +import net.storm.sdk.widgets.Dialog; +import net.storm.sdk.widgets.Widgets; +import net.storm.api.domain.widgets.IWidget; + +@Slf4j +public class MagicInstructorStep implements TutorialStep { + private final JGTutorialIslandPlugin plugin; + + // --- Constants (replace widget/coords as needed) --- + private static final int MAGIC_INSTRUCTOR_ID = 3309; + private static final int CHICKEN_ID = 3316; + private static final int AIR_RUNE_ID = 556, MIND_RUNE_ID = 558; + private static final int INSTRUCTOR_X = 3142, INSTRUCTOR_Y = 3085, PLANE = 0; + + // Widgets (placeholder, replace if different in your client) + private static final int SPELLBOOK_TAB_PARENT = 164, SPELLBOOK_TAB_CHILD = 58; + private static final int TEXT_BOX_PARENT = 263, TEXT_BOX_CHILD = 1, TEXT_BOX_INDEX = 0; + + // Spell (using API, no menu manipulation needed) + private static final Spell WIND_STRIKE = SpellBook.Standard.WIND_STRIKE; + + private enum SubState { + WALK_TO_INSTRUCTOR, + TALK_INSTRUCTOR_1, + OPEN_SPELLBOOK, + TALK_INSTRUCTOR_2, + CAST_WIND_STRIKE, + FINAL_DIALOGUE, + MOVE_ON + } + @Getter + private SubState subState; + + public MagicInstructorStep(JGTutorialIslandPlugin plugin) { + this.plugin = plugin; + this.subState = SubState.WALK_TO_INSTRUCTOR; + } + + private void setSubState(SubState next) { + if (subState != next) log.info("Changing substate: {} -> {}", subState, next); + subState = next; + } + + @Override + public boolean validate() { + boolean valid = plugin.getCurrentState() == JGTutorialIslandState.MAGIC_INSTRUCTOR; + log.info("Validate called: currentState={}, valid={}", plugin.getCurrentState(), valid); + return valid; + } + + @Override + public int execute() { + plugin.incrementTick(); + + // Re-infer substate for crash/restart safety + if (!Players.getLocal().isInteracting()) { + SubState inferred = inferSubState(); + if (inferred != subState) setSubState(inferred); + } + + log.info("---- MagicInstructorStep Tick ---- Current subState: {}", subState); + + switch (subState) { + case WALK_TO_INSTRUCTOR: { + if (!isAtInstructor()) { + Movement.walkTo(INSTRUCTOR_X, INSTRUCTOR_Y, PLANE); + log.info("Walking to Magic Instructor."); + return randomDelay(1000); + } + setSubState(SubState.TALK_INSTRUCTOR_1); + log.info("Arrived at Magic Instructor."); + return randomDelay(600); + } + case TALK_INSTRUCTOR_1: { + String txt = getWidgetText(); + // Defensive: only move on if dialog is over *and* we have both runes or the right textbox + boolean dialogDone = !Dialog.isOpen() && !Dialog.isViewingOptions(); + boolean hasRunes = hasRunes(); + boolean shouldAdvance = (txt.contains("final menu") || hasRunes); + + if (shouldAdvance) { + setSubState(SubState.OPEN_SPELLBOOK); + log.info("Done with dialog + runes received or prompt seen, going to OPEN_SPELLBOOK."); + return randomDelay(400); + } + if (txt.contains("cast wind strike")) { + setSubState(SubState.CAST_WIND_STRIKE); + return randomDelay(400); + } + if (txt.contains("congratulations, you have completed")) { + setSubState(SubState.FINAL_DIALOGUE); + return randomDelay(400); + } + // Always continue dialog if open + if (Dialog.isViewingOptions()) { + Dialog.chooseOption(1); + log.info("Choosing dialog option 1 (Magic Instructor, intro)."); + return randomDelay(400); + } + if (Dialog.isOpen() && Dialog.canContinue()) { + Dialog.continueSpace(); + log.info("Continuing dialog (Magic Instructor, intro)."); + return randomDelay(200); + } + INPC instructor = getInstructor(); + // Only start dialog if not already talking or interacting + if (instructor != null && isAt(instructor) && !Players.getLocal().isInteracting()) { + instructor.interact("Talk-to"); + log.info("Talking to Magic Instructor."); + return randomDelay(1200); + } + return randomDelay(600); + } + + case TALK_INSTRUCTOR_2: { + String txt = getWidgetText(); + // If the dialog is prompting to cast Wind Strike, move on + if (txt.contains("you now have some runes")) { + setSubState(SubState.CAST_WIND_STRIKE); + return randomDelay(400); + } + // Always continue dialog if open + if (Dialog.isViewingOptions()) { + Dialog.chooseOption(1); + log.info("Choosing dialog option 1 (Magic Instructor, post-spellbook)."); + return randomDelay(400); + } + if (Dialog.isOpen() && Dialog.canContinue()) { + Dialog.continueSpace(); + log.info("Continuing dialog (Magic Instructor, post-spellbook)."); + return randomDelay(200); + } + INPC instructor = getInstructor(); + if (instructor != null && isAt(instructor) && !Players.getLocal().isInteracting()) { + instructor.interact("Talk-to"); + log.info("Talking to Magic Instructor (after spellbook)."); + return randomDelay(1200); + } + // If dialog is closed and text box says to cast, move on + if (!Dialog.isViewingOptions() && txt.contains("cast wind strike")) { + setSubState(SubState.CAST_WIND_STRIKE); + log.info("Dialog done and cast prompt detected, moving to CAST_WIND_STRIKE."); + return randomDelay(400); + } + return randomDelay(600); + } + + case OPEN_SPELLBOOK: { + IWidget spellbookTab = getWidgetSafe(SPELLBOOK_TAB_PARENT, SPELLBOOK_TAB_CHILD); + if (spellbookTab != null && spellbookTab.isVisible()) { + spellbookTab.click(); + log.info("Clicked spellbook tab."); + setSubState(SubState.TALK_INSTRUCTOR_2); + return randomDelay(700); + } + log.info("Spellbook tab not found, waiting..."); + return randomDelay(600); + } + case CAST_WIND_STRIKE: { + if (!WIND_STRIKE.canCast()) { + log.warn("Cannot cast Wind Strike (missing runes or interface not ready?)"); + // Optional: interact with instructor for more runes + return randomDelay(800); + } + INPC chicken = getNearestChicken(); + if (chicken != null && !chicken.isDead() && !chicken.isInteracting()) { + WIND_STRIKE.castOn(chicken); + log.info("Casting Wind Strike on chicken."); + setSubState(SubState.FINAL_DIALOGUE); + return randomDelay(1600); + } + log.info("No available chicken found. Waiting..."); + return randomDelay(1000); + } + case FINAL_DIALOGUE: { + INPC adventurerJonh = null; + try { + adventurerJonh = NPCs.query().ids(9244).results().nearest(Players.getLocal()); + } catch (Exception e) { + } + if (adventurerJonh != null && adventurerJonh.getWorldLocation() != null) { + log.info("Adventurer Jonh detected nearby! ({} {}) Tutorial Island is complete.", + adventurerJonh.getWorldLocation().getX(), adventurerJonh.getWorldLocation().getY()); + setSubState(SubState.MOVE_ON); + plugin.setCurrentState(JGTutorialIslandState.COMPLETE); + return randomDelay(400); + } + if (Dialog.isViewingOptions()) { + if (getWidgetSafe(219,1,3).getText().toLowerCase().contains( + "i'm not planning to do that" + )){ + Dialog.chooseOption(3); + } + Dialog.chooseOption(1); + log.info("Choosing dialog option 1 (Magic Instructor, final wrap-up)."); + return randomDelay(400); + } + if (Dialog.isOpen() && Dialog.canContinue()) { + Dialog.continueSpace(); + log.info("Continuing dialog (Magic Instructor, final wrap-up)."); + return randomDelay(200); + } + INPC instructor = getInstructor(); + if (instructor != null && isAt(instructor) && !Players.getLocal().isInteracting()) { + instructor.interact("Talk-to"); + log.info("Talking to Magic Instructor (final wrap-up)."); + return randomDelay(1200); + } + String txt = getWidgetText(); + if (txt.contains("congratulations, you have completed")) { + setSubState(SubState.MOVE_ON); + log.info("Tutorial complete, moving on!"); + return randomDelay(600); + } + if (!Dialog.isOpen() && !Dialog.isViewingOptions()) { + setSubState(SubState.MOVE_ON); + log.info("Dialog closed, moving on."); + return randomDelay(600); + } + return randomDelay(600); + } + case MOVE_ON: { + log.info("Completed Magic Instructor step! Setting plugin state to TUTORIAL_ISLAND_DONE."); + plugin.setCurrentState(JGTutorialIslandState.COMPLETE); + return randomDelay(600); + } + } + return randomDelay(600); + } + + // --- State inference logic for crash/restart robustness --- + private SubState inferSubState() { + // 1. If Adventurer Jonh (ID 9244) is near, player is in Lumbridge: tutorial is complete + INPC adventurerJonh = null; + try { + adventurerJonh = NPCs.query().ids(9244).results().nearest(Players.getLocal()); + } catch (Exception e) { + // ignore + } + if (adventurerJonh != null && adventurerJonh.getWorldLocation() != null) { + log.info("inferSubState: Detected Adventurer Jonh nearby. Completing Tutorial Island."); + return SubState.MOVE_ON; + } + + // 2. [Optional backup] Location check for Lumbridge (if you want) + if (Players.getLocal() != null && Players.getLocal().getWorldLocation() != null) { + int x = Players.getLocal().getWorldLocation().getX(); + int y = Players.getLocal().getWorldLocation().getY(); + int plane = Players.getLocal().getWorldLocation().getPlane(); + if (plane == 0 && Math.abs(x - 3222) <= 3 && Math.abs(y - 3218) <= 3) { + log.info("inferSubState: Player is at Lumbridge arrival area. Completing Tutorial Island."); + return SubState.MOVE_ON; + } + } + String txt = getWidgetText(); + boolean dialogDone = !Dialog.isOpen() && !Dialog.isViewingOptions(); + boolean hasRunes = hasRunes(); + // Now check for spellbook just opened: if spellbook is open but NOT cast prompt, go to TALK_INSTRUCTOR_2 + if (isSpellbookOpen() && txt.contains("this is your magic interface") && dialogDone) { + return SubState.TALK_INSTRUCTOR_2; + } + if ((txt.contains("magic menu") || hasRunes) && dialogDone && !isSpellbookOpen()) { + return SubState.OPEN_SPELLBOOK; + } + if (txt.contains("cast wind strike")) { + return SubState.CAST_WIND_STRIKE; + } + if (txt.contains("congratulations, you have completed")) { + return SubState.MOVE_ON; + } + if (!isSpellbookOpen() && dialogDone && hasRunes) { + return SubState.OPEN_SPELLBOOK; + } + return subState; + } + + // --- Helpers --- + private INPC getInstructor() { + try { return NPCs.query().ids(MAGIC_INSTRUCTOR_ID).results().nearest(Players.getLocal()); } + catch (Exception e) { log.warn("getInstructor exception: {}", e.toString()); return null; } + } + private boolean isAtInstructor() { + if (Players.getLocal() == null || Players.getLocal().getWorldLocation() == null) + return false; + int px = Players.getLocal().getWorldLocation().getX(); + int py = Players.getLocal().getWorldLocation().getY(); + return Math.abs(px - INSTRUCTOR_X) <= 7 && Math.abs(py - INSTRUCTOR_Y) <= 7; + } + private boolean isAt(INPC npc) { + try { + return npc != null && Players.getLocal() != null && + Players.getLocal().getWorldLocation() != null && npc.getWorldLocation() != null && + Players.getLocal().getWorldLocation().distanceTo(npc.getWorldLocation()) <= 2; + } catch (Exception e) { return false; } + } + private INPC getNearestChicken() { + try { return NPCs.query().ids(CHICKEN_ID).results().nearest(Players.getLocal()); } + catch (Exception e) { return null; } + } + private IWidget getWidgetSafe(int parent, int child) { + try { return Widgets.get(parent, child); } + catch (Exception e) { return null; } + } + private IWidget getWidgetSafe(int parent, int child, int idx) { + try { return Widgets.get(parent, child, idx); } + catch (Exception e) { return null; } + } + private boolean isSpellbookOpen() { + IWidget spellTab = getWidgetSafe(SPELLBOOK_TAB_PARENT, SPELLBOOK_TAB_CHILD); + return spellTab != null && spellTab.isVisible(); + } + private String getWidgetText() { + IWidget w = getWidgetSafe(TEXT_BOX_PARENT, TEXT_BOX_CHILD, TEXT_BOX_INDEX); + return (w != null && w.isVisible() && w.getText() != null) ? w.getText().toLowerCase() : ""; + } + private boolean hasRunes() { + // You could also add inventory checks via Inventory.contains(), or leave as is for just dialog/textbox + return net.storm.sdk.items.Inventory.contains(AIR_RUNE_ID) && net.storm.sdk.items.Inventory.contains(MIND_RUNE_ID); + } + private int randomDelay(int base) { + return base + (int) (Math.random() * 200) - 100; + } + @Override + public boolean isComplete() { + boolean complete = plugin.getCurrentState() != JGTutorialIslandState.MAGIC_INSTRUCTOR; + log.info("isComplete called: currentState={}, complete={}", plugin.getCurrentState(), complete); + return complete; + } +} diff --git a/JG-tutorial-island/src/main/java/steps/MasterChefStep.java b/JG-tutorial-island/src/main/java/steps/MasterChefStep.java new file mode 100644 index 0000000..8e76b80 --- /dev/null +++ b/JG-tutorial-island/src/main/java/steps/MasterChefStep.java @@ -0,0 +1,262 @@ +package steps; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.storm.plugins.tutorialisland.JGTutorialIslandPlugin; +import net.storm.plugins.tutorialisland.JGTutorialIslandState; +import net.storm.sdk.entities.NPCs; +import net.storm.sdk.entities.Players; +import net.storm.sdk.entities.TileObjects; +import net.storm.api.domain.actors.INPC; +import net.storm.api.domain.tiles.ITileObject; +import net.storm.sdk.widgets.Dialog; +import net.storm.sdk.widgets.Widgets; +import net.storm.sdk.movement.Movement; +import net.storm.sdk.items.Inventory; +import net.storm.api.domain.items.IInventoryItem; + +@Slf4j +public class MasterChefStep implements TutorialStep { + private final JGTutorialIslandPlugin plugin; + + private static final int MASTER_CHEF_ID = 3305; // Confirm with Dev Tools + private static final int RANGE_ID = 9736; // Confirm with Dev Tools + private static final int MASTER_CHEF_TILE_X = 3075; + private static final int MASTER_CHEF_TILE_Y = 3085; + + private static final String[] FLOUR_NAMES = {"Pot of flour"}; + private static final String[] WATER_NAMES = {"Bucket of water"}; + private static final String[] DOUGH_NAMES = {"Bread dough"}; + private static final String[] BREAD_NAMES = {"Bread"}; + + private static final int MUSIC_WIDGET_PARENT = 261; + private static final int MUSIC_WIDGET_CHILD = 1; + + private enum SubState { + WALK_TO_CHEF, + TALK_CHEF, + GET_INGREDIENTS, + MIX_DOUGH, + BAKE_BREAD, + MUSIC_PLAYER, + MOVE_ON + } + @Getter + private SubState subState; + + public MasterChefStep(JGTutorialIslandPlugin plugin) { + this.plugin = plugin; + // Default to TALK_CHEF, never query the world here! + this.subState = SubState.WALK_TO_CHEF; + } + + private void setSubState(SubState next) { + if (subState != next) log.info("Changing substate: {} -> {}", subState, next); + subState = next; + } + + @Override + public boolean validate() { + boolean valid = plugin.getCurrentState() == JGTutorialIslandState.MASTER_CHEF; + log.info("Validate called: currentState={}, valid={}", plugin.getCurrentState(), valid); + return valid; + } + + @Override + public int execute() { + plugin.incrementTick(); + + // Only re-infer when not in dialog or using an item (robust restarts) + if (!Dialog.isOpen() && !Players.getLocal().isInteracting()) { + SubState newInferred = inferSubState(); + if (newInferred != subState) { + log.info("Auto-correcting subState: {} -> {}", subState, newInferred); + setSubState(newInferred); + } + } + log.info("---- MasterChefStep Tick ---- Current subState: {}", subState); + + switch (subState) { + case WALK_TO_CHEF: { + INPC chef = getChef(); + if (chef == null) { + log.warn("Can't find Master Chef NPC, walking to fallback tile ({}, {})", MASTER_CHEF_TILE_X, MASTER_CHEF_TILE_Y); + Movement.walkTo(MASTER_CHEF_TILE_X, MASTER_CHEF_TILE_Y); + return randomDelay(1000); + } + if (!isAt(chef)) { + Movement.walkTo(chef.getWorldLocation()); + log.info("Walking to Master Chef at {}", chef.getWorldLocation()); + return randomDelay(1000); + } + log.info("Arrived at Master Chef. Switching to TALK_CHEF"); + setSubState(SubState.TALK_CHEF); + return randomDelay(600); + } + + case TALK_CHEF: { + INPC chefTalk = getChef(); + // If we have both flour and water, proceed + if (hasFlour() && hasWater()) { + setSubState(SubState.MIX_DOUGH); + log.info("Both flour and water detected; switching to MIX_DOUGH."); + return randomDelay(600); + } + // If options/dialogue, handle as normal + if (Dialog.isViewingOptions()) { + Dialog.chooseOption(1); + log.info("Choosing dialog option 1 for chef."); + return randomDelay(400); + } + if (Dialog.isOpen() && Dialog.canContinue()) { + Dialog.continueSpace(); + log.info("Continuing dialog with chef."); + return randomDelay(200); + } + // If either flour or water missing, try to talk + if (chefTalk != null && isAt(chefTalk)) { + chefTalk.interact("Talk-to"); + log.info("Talking to Master Chef for bread instructions/ingredients."); + return randomDelay(1200); + } + return randomDelay(600); + } + + case GET_INGREDIENTS: { + // Only talk if missing any required item + if (!hasFlour() || !hasWater()) { + INPC chef2 = getChef(); + if (chef2 != null && isAt(chef2)) { + chef2.interact("Talk-to"); + log.info("Getting more flour/water from chef."); + return randomDelay(1200); + } + } else { + setSubState(SubState.MIX_DOUGH); + log.info("Obtained flour and water. Switching to MIX_DOUGH."); + } + return randomDelay(600); + } + + case MIX_DOUGH: { + // If missing dough, try to combine ingredients + if (!hasDough()) { + if (!hasFlour() || !hasWater()) { + log.info("Missing flour/water, switching to GET_INGREDIENTS."); + setSubState(SubState.GET_INGREDIENTS); + return randomDelay(400); + } + // Combine ingredients: use one on the other + IInventoryItem flour = getInventoryFirst(FLOUR_NAMES); + IInventoryItem water = getInventoryFirst(WATER_NAMES); + if (flour != null && water != null) { + log.info("Mixing flour and water to make dough."); + flour.useOn(water); + return randomDelay(1200); + } + log.info("Flour or water not found for mixing."); + } else { + setSubState(SubState.BAKE_BREAD); + log.info("Bread dough in inventory; switching to BAKE_BREAD."); + } + return randomDelay(600); + } + + case BAKE_BREAD: { + // If we lose the dough or fail to get bread, fallback to remixing + if (!hasDough() && !hasBread()) { + log.warn("No bread dough and no bread found, possible burn/loss. Switching to GET_INGREDIENTS."); + setSubState(SubState.GET_INGREDIENTS); + return randomDelay(400); + } + // Use dough on range to bake bread + if (hasDough() && !hasBread()) { + IInventoryItem dough = getInventoryFirst(DOUGH_NAMES); + ITileObject range = getRange(); + if (dough != null && range != null) { + log.info("Baking bread by using dough on range."); + dough.useOn(range); + return randomDelay(1800); + } + log.info("Range or dough missing for baking."); + } else if (hasBread()) { + setSubState(SubState.MOVE_ON); // skip MUSIC_PLAYER for now + log.info("Bread detected, proceeding to MOVE_ON."); + } + return randomDelay(600); + } + + case MOVE_ON: { + log.info("Master Chef complete. Advancing to QUEST_GUIDE step."); + plugin.setCurrentState(JGTutorialIslandState.QUEST_GUIDE); + return randomDelay(600); + } + } + return randomDelay(600); + } + + // ---- Inventory/Widget/NPC null-safe helpers ---- + private boolean hasFlour() { try { return Inventory.contains(FLOUR_NAMES); } catch (Exception e) { return false; } } + private boolean hasWater() { try { return Inventory.contains(WATER_NAMES); } catch (Exception e) { return false; } } + private boolean hasDough() { try { return Inventory.contains(DOUGH_NAMES); } catch (Exception e) { return false; } } + private boolean hasBread() { try { return Inventory.contains(BREAD_NAMES); } catch (Exception e) { return false; } } + + private IInventoryItem getInventoryFirst(String[] names) { + try { return Inventory.getFirst(names); } catch (Exception e) { return null; } + } + private INPC getChef() { + try { return NPCs.query().ids(MASTER_CHEF_ID).results().nearest(Players.getLocal()); } catch (Exception e) { return null; } + } + private boolean isAt(INPC npc) { + try { + return npc != null && + Players.getLocal() != null && + Players.getLocal().getWorldLocation() != null && + npc.getWorldLocation() != null && + Players.getLocal().getWorldLocation().distanceTo(npc.getWorldLocation()) <= 2; + } catch (Exception e) { + return false; + } + } + private ITileObject getRange() { + try { return TileObjects.getNearest(RANGE_ID); } catch (Exception e) { return null; } + } + + // --- Substate inference for safe restarts --- + private SubState inferSubState() { + try { + if (hasBread()) { + log.info("Infer: Bread in inventory, ready to MOVE_ON."); + return SubState.MOVE_ON; + } + if (hasDough()) { + log.info("Infer: Bread dough in inventory, ready to bake."); + return SubState.BAKE_BREAD; + } + if (hasFlour() && hasWater()) { + log.info("Infer: Flour and water present, ready to MIX_DOUGH."); + return SubState.MIX_DOUGH; + } + INPC chef = getChef(); + if (chef == null || !isAt(chef)) { + log.info("Infer: Need to walk to chef."); + return SubState.WALK_TO_CHEF; + } + } catch (Exception e) { + log.warn("Exception in inferSubState: {}", e.toString()); + } + log.info("Infer: Defaulting to TALK_CHEF."); + return SubState.TALK_CHEF; + } + + private int randomDelay(int base) { + return base + (int) (Math.random() * 200) - 100; + } + + @Override + public boolean isComplete() { + boolean complete = plugin.getCurrentState() != JGTutorialIslandState.MASTER_CHEF; + log.info("isComplete called: currentState={}, complete={}", plugin.getCurrentState(), complete); + return complete; + } +} diff --git a/JG-tutorial-island/src/main/java/steps/MiningGuideStep.java b/JG-tutorial-island/src/main/java/steps/MiningGuideStep.java new file mode 100644 index 0000000..d7eac19 --- /dev/null +++ b/JG-tutorial-island/src/main/java/steps/MiningGuideStep.java @@ -0,0 +1,351 @@ +package steps; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.storm.api.domain.actors.INPC; +import net.storm.api.domain.tiles.ITileObject; +import net.storm.plugins.tutorialisland.JGTutorialIslandPlugin; +import net.storm.plugins.tutorialisland.JGTutorialIslandState; +import net.storm.sdk.entities.NPCs; +import net.storm.sdk.entities.Players; +import net.storm.sdk.entities.TileObjects; +import net.storm.sdk.items.Inventory; +import net.storm.sdk.movement.Movement; +import net.storm.sdk.widgets.Dialog; +import net.storm.sdk.widgets.Widgets; +import net.storm.api.domain.items.IInventoryItem; +import net.storm.api.domain.widgets.IWidget; + +@Slf4j +public class MiningGuideStep implements TutorialStep { + private final JGTutorialIslandPlugin plugin; + + // --- Constants --- + private static final int INSTRUCTOR_ID = 3311; + private static final int TIN_ROCK_ID = 10080, COPPER_ROCK_ID = 10079, FURNACE_ID = 10082, ANVIL_ID = 2097; + private static final int INSTRUCTOR_X = 3081, INSTRUCTOR_Y = 9506, INSTRUCTOR_PLANE = 0; + + // Item IDs + private static final int PICKAXE_ID = 1265, HAMMER_ID = 2347, TIN_ORE_ID = 438, COPPER_ORE_ID = 436, + BRONZE_BAR_ID = 2349, BRONZE_DAGGER_ID = 1205; + + private enum SubState { + WALK_TO_INSTRUCTOR, + TALK_INSTRUCTOR_1, + MINE_TIN, + MINE_COPPER, + SMELT_BAR, + TALK_INSTRUCTOR_2, + SMITH_DAGGER, + MOVE_ON + } + + @Getter + private SubState subState; + + public MiningGuideStep(JGTutorialIslandPlugin plugin) { + this.plugin = plugin; + // Do NOT inferSubState here; just pick a safe default + this.subState = SubState.WALK_TO_INSTRUCTOR; + } + + private void setSubState(SubState next) { + if (subState != next) log.info("Changing substate: {} -> {}", subState, next); + subState = next; + } + + @Override + public boolean validate() { + boolean valid = plugin.getCurrentState() == JGTutorialIslandState.MINING_INSTRUCTOR; + log.info("Validate called: currentState={}, valid={}", plugin.getCurrentState(), valid); + return valid; + } + + @Override + public int execute() { + plugin.incrementTick(); + + // Only infer at runtime, not in constructor + if (!Dialog.isOpen() && !Players.getLocal().isInteracting()) { + SubState inferred = inferSubState(); + if (inferred != subState) setSubState(inferred); + } + + log.info("---- MiningGuideStep Tick ---- Current subState: {}", subState); + + switch (subState) { + case WALK_TO_INSTRUCTOR: { + INPC instructor = getInstructor(); + if (instructor == null) { + log.warn("Can't find Mining Instructor, walking to fallback tile ({}, {}, {})", INSTRUCTOR_X, INSTRUCTOR_Y, INSTRUCTOR_PLANE); + Movement.walkTo(INSTRUCTOR_X, INSTRUCTOR_Y, INSTRUCTOR_PLANE); + return randomDelay(1000); + } + if (!isAt(instructor)) { + Movement.walkTo(instructor.getWorldLocation()); + log.info("Walking to Mining Instructor at {}", instructor.getWorldLocation()); + return randomDelay(1000); + } + log.info("Arrived at Mining Instructor. Switching to TALK_INSTRUCTOR_1"); + setSubState(SubState.TALK_INSTRUCTOR_1); + return randomDelay(600); + } + case TALK_INSTRUCTOR_1: { + if (hasPickaxe()) { + setSubState(SubState.MINE_TIN); + log.info("Received pickaxe. Proceeding to MINE_TIN."); + return randomDelay(600); + } + if (Dialog.isViewingOptions()) { + Dialog.chooseOption(1); + log.info("Choosing dialog option 1 for instructor."); + return randomDelay(400); + } + if (Dialog.isOpen() && Dialog.canContinue()) { + Dialog.continueSpace(); + log.info("Continuing dialog with instructor."); + return randomDelay(200); + } + INPC instructor = getInstructor(); + if (instructor != null && !Players.getLocal().isInteracting() && isAt(instructor)) { + instructor.interact("Talk-to"); + log.info("Talking to Mining Instructor for pickaxe."); + return randomDelay(1200); + } + return randomDelay(600); + } + case MINE_TIN: { + if (hasTinOre()) { + setSubState(SubState.MINE_COPPER); + log.info("Tin ore acquired. Switching to MINE_COPPER."); + return randomDelay(600); + } + ITileObject tinRock = getTileObjectSafe(TIN_ROCK_ID); + if (tinRock != null && tinRock.isInteractable()) { + tinRock.interact("Mine"); + log.info("Mining Tin rock."); + return randomDelay(1800); + } + log.info("Tin rock not found or not interactable."); + return randomDelay(600); + } + case MINE_COPPER: { + if (hasCopperOre()) { + setSubState(SubState.SMELT_BAR); + log.info("Copper ore acquired. Switching to SMELT_BAR."); + return randomDelay(600); + } + ITileObject copperRock = getTileObjectSafe(COPPER_ROCK_ID); + if (copperRock != null && copperRock.isInteractable()) { + copperRock.interact("Mine"); + log.info("Mining Copper rock."); + return randomDelay(1800); + } + log.info("Copper rock not found or not interactable."); + return randomDelay(600); + } + case SMELT_BAR: { + if (hasBronzeBar()) { + setSubState(SubState.TALK_INSTRUCTOR_2); + log.info("Bronze bar acquired. Switching to TALK_INSTRUCTOR_2."); + return randomDelay(600); + } + if (!hasTinOre() || !hasCopperOre()) { + setSubState(hasTinOre() ? SubState.MINE_COPPER : SubState.MINE_TIN); + log.info("Missing tin/copper ore. Resetting mining substates."); + return randomDelay(400); + } + ITileObject furnace = getTileObjectSafe(FURNACE_ID); + if (furnace != null) { + IInventoryItem tinOre = Inventory.getFirst(TIN_ORE_ID); + if (tinOre != null) { + tinOre.useOn(furnace); + log.info("Smelting ores at furnace."); + return randomDelay(1600); + } + } + log.info("Furnace not found."); + return randomDelay(600); + } + case TALK_INSTRUCTOR_2: { + if (hasHammer()) { + setSubState(SubState.SMITH_DAGGER); + log.info("Hammer acquired (mid-state); advancing to SMITH_DAGGER."); + return randomDelay(600); + } + INPC instructor = getInstructor(); + if (Dialog.isViewingOptions()) { + Dialog.chooseOption(1); + log.info("Choosing dialog option 1 for instructor (get hammer)."); + return randomDelay(400); + } + if (Dialog.isOpen() && Dialog.canContinue()) { + Dialog.continueSpace(); + log.info("Continuing dialog with instructor (get hammer)."); + return randomDelay(200); + } + if (instructor != null && !Players.getLocal().isInteracting() && isAt(instructor)) { + instructor.interact("Talk-to"); + log.info("Talking to Mining Instructor for hammer."); + return randomDelay(1200); + } + if (instructor == null || !isAt(instructor)) { + Movement.walkTo(INSTRUCTOR_X, INSTRUCTOR_Y, INSTRUCTOR_PLANE); + log.info("Instructor not in range, walking to fallback location."); + return randomDelay(1000); + } + log.info("stuck, unsure what to do right now- DEBUGGG"); + return randomDelay(600); + } + case SMITH_DAGGER: { + if (hasDagger()) { + setSubState(SubState.MOVE_ON); + log.info("Bronze dagger acquired. Ready to move on."); + return randomDelay(600); + } + IWidget daggerWidget = getWidgetSafe(312, 9, 2); + if (daggerWidget != null && daggerWidget.isVisible()) { + daggerWidget.click(); + log.info("Smithing menu open, clicking dagger widget (312,9,2)."); + return randomDelay(2000); + } + if (!hasBronzeBar()) { + setSubState(SubState.SMELT_BAR); + log.info("No bronze bar for smithing. Returning to SMELT_BAR."); + return randomDelay(600); + } + ITileObject anvil = getTileObjectSafe(ANVIL_ID); + IInventoryItem bronzeBar = Inventory.getFirst(BRONZE_BAR_ID); + if (anvil != null && bronzeBar != null) { + bronzeBar.useOn(anvil); + log.info("Smithing bronze dagger at anvil (opening smithing menu)."); + return randomDelay(1600); + } + log.info("Anvil or bronze bar missing for smithing."); + return randomDelay(600); + } + case MOVE_ON: { + log.info("Completed Mining Instructor step! Setting plugin state to COMBAT_INSTRUCTOR."); + plugin.setCurrentState(JGTutorialIslandState.COMBAT_INSTRUCTOR); + return randomDelay(600); + } + } + return randomDelay(600); + } + + // --------- Safe SubState Inference --------- + private SubState inferSubState() { + try { + IWidget movingOnWidget = getWidgetSafe(263, 1, 0); + if (movingOnWidget != null && movingOnWidget.isVisible()) { + String text = movingOnWidget.getText(); + if (text != null && text.toLowerCase().contains("combat instructor")) { + log.info("Infer: 'Combat Instructor' text present. Substate = MOVE_ON."); + return SubState.MOVE_ON; + } + } + if (hasDagger()) { + log.info("Infer: Bronze dagger present. Substate = MOVE_ON."); + return SubState.MOVE_ON; + } + if (hasHammer()) { + if (hasBronzeBar()) { + log.info("Infer: Have hammer and bar. Substate = SMITH_DAGGER."); + return SubState.SMITH_DAGGER; + } + log.info("Infer: Have hammer but no bar. Substate = SMELT_BAR."); + return SubState.SMELT_BAR; + } + if (hasBronzeBar()) { + log.info("Infer: Bronze bar present, ready to get hammer. Substate = TALK_INSTRUCTOR_2."); + return SubState.TALK_INSTRUCTOR_2; + } + if (hasTinOre() && hasCopperOre()) { + log.info("Infer: Have both ores. Substate = SMELT_BAR."); + return SubState.SMELT_BAR; + } + INPC instructor = getInstructor(); + boolean atInstructor = instructor != null && isAt(instructor); + + if (hasPickaxe() && !hasHammer() && (!hasTinOre() || !hasCopperOre())) { + if (Dialog.isOpen() || Dialog.isViewingOptions() || (atInstructor && !Players.getLocal().isInteracting())) { + log.info("Infer: Have pickaxe but not hammer/ores; talking to instructor for proper progression."); + return SubState.TALK_INSTRUCTOR_1; + } + } + if (hasPickaxe() && !hasTinOre()) { + log.info("Infer: Pickaxe present, not in dialog, ready to mine tin. Substate = MINE_TIN."); + return SubState.MINE_TIN; + } + if (hasPickaxe() && !hasCopperOre()) { + log.info("Infer: Pickaxe present, not in dialog, ready to mine copper. Substate = MINE_COPPER."); + return SubState.MINE_COPPER; + } + if (instructor == null || !atInstructor) { + log.info("Infer: Not at instructor, need to WALK_TO_INSTRUCTOR."); + return SubState.WALK_TO_INSTRUCTOR; + } + } catch (Exception e) { + log.warn("Exception in inferSubState: {}", e.toString()); + } + log.info("Infer: At instructor, ready to TALK_INSTRUCTOR_1."); + return SubState.TALK_INSTRUCTOR_1; + } + + // --------- Utility Methods --------- + private INPC getInstructor() { + try { + return NPCs.query().ids(INSTRUCTOR_ID).results().nearest(Players.getLocal()); + } catch (Exception e) { + log.warn("getInstructor() exception: {}", e.toString()); + return null; + } + } + private boolean isAt(INPC npc) { + try { + return npc != null && Players.getLocal() != null && + Players.getLocal().getWorldLocation() != null && npc.getWorldLocation() != null && + Players.getLocal().getWorldLocation().distanceTo(npc.getWorldLocation()) <= 2; + } catch (Exception e) { + return false; + } + } + private ITileObject getTileObjectSafe(int id) { + try { + return TileObjects.getNearest(id); + } catch (Exception e) { + return null; + } + } + private IWidget getWidgetSafe(int parent, int child) { + try { + return Widgets.get(parent, child); + } catch (Exception e) { + return null; + } + } + private IWidget getWidgetSafe(int parent, int child, int subchild) { + try { + return Widgets.get(parent, child, subchild); + } catch (Exception e) { + return null; + } + } + private boolean hasPickaxe() { return Inventory.contains(PICKAXE_ID); } + private boolean hasHammer() { return Inventory.contains(HAMMER_ID); } + private boolean hasTinOre() { return Inventory.contains(TIN_ORE_ID); } + private boolean hasCopperOre() { return Inventory.contains(COPPER_ORE_ID); } + private boolean hasBronzeBar() { return Inventory.contains(BRONZE_BAR_ID); } + private boolean hasDagger() { return Inventory.contains(BRONZE_DAGGER_ID); } + + private int randomDelay(int base) { + return base + (int) (Math.random() * 200) - 100; + } + + @Override + public boolean isComplete() { + boolean complete = plugin.getCurrentState() != JGTutorialIslandState.MINING_INSTRUCTOR; + log.info("isComplete called: currentState={}, complete={}", plugin.getCurrentState(), complete); + return complete; + } +} diff --git a/JG-tutorial-island/src/main/java/steps/PrayerInstructorStep.java b/JG-tutorial-island/src/main/java/steps/PrayerInstructorStep.java new file mode 100644 index 0000000..8511889 --- /dev/null +++ b/JG-tutorial-island/src/main/java/steps/PrayerInstructorStep.java @@ -0,0 +1,270 @@ +package steps; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.storm.api.domain.actors.INPC; +import net.storm.plugins.tutorialisland.JGTutorialIslandPlugin; +import net.storm.plugins.tutorialisland.JGTutorialIslandState; +import net.storm.sdk.entities.NPCs; +import net.storm.sdk.entities.Players; +import net.storm.sdk.movement.Movement; +import net.storm.sdk.widgets.Dialog; +import net.storm.sdk.widgets.Widgets; +import net.storm.api.domain.widgets.IWidget; + +@Slf4j +public class PrayerInstructorStep implements TutorialStep { + private final JGTutorialIslandPlugin plugin; + + // ---- Constants ---- + private static final int PRAYER_INSTRUCTOR_ID = 3319; + private static final int CHAPEL_X = 3123, CHAPEL_Y = 3106, PLANE = 0; + private static final int PRAYER_TAB_PARENT = 164, PRAYER_TAB_CHILD = 57; + private static final int FRIENDS_TAB_PARENT = 164, FRIENDS_TAB_CHILD = 40; + private static final int TEXT_BOX_PARENT = 263, TEXT_BOX_CHILD = 1, TEXT_BOX_INDEX = 0; + + private enum SubState { + WALK_TO_CHAPEL, + TALK_INSTRUCTOR_1, + OPEN_PRAYER_TAB, + TALK_INSTRUCTOR_2, + OPEN_FRIENDS_TAB, + FINAL_DIALOGUE, + MOVE_ON + } + @Getter + private SubState subState; + + public PrayerInstructorStep(JGTutorialIslandPlugin plugin) { + this.plugin = plugin; + this.subState = SubState.WALK_TO_CHAPEL; + } + + private void setSubState(SubState next) { + if (subState != next) log.info("Changing substate: {} -> {}", subState, next); + subState = next; + } + + @Override + public boolean validate() { + boolean valid = plugin.getCurrentState() == JGTutorialIslandState.PRAYER_INSTRUCTOR; + log.info("Validate called: currentState={}, valid={}", plugin.getCurrentState(), valid); + return valid; + } + + @Override + public int execute() { + plugin.incrementTick(); + + // Robust state re-eval: keep running until state stabilizes (no change) + boolean inferredAndProcessed; + do { + inferredAndProcessed = false; + if (!Dialog.isOpen() && !Players.getLocal().isInteracting()) { + SubState inferred = inferSubState(); + if (inferred != subState) { + setSubState(inferred); + inferredAndProcessed = true; // process again! + } + } + } while (inferredAndProcessed); + + log.info("---- PrayerInstructorStep Tick ---- Current subState: {}", subState); + + switch (subState) { + case WALK_TO_CHAPEL: { + if (!isAtChapel()) { + Movement.walkTo(CHAPEL_X, CHAPEL_Y, PLANE); + log.info("Walking to chapel for Prayer Instructor."); + return randomDelay(1000); + } + setSubState(SubState.TALK_INSTRUCTOR_1); + return randomDelay(600); + } + + case TALK_INSTRUCTOR_1: { + if (handleDialog()) return randomDelay(200); + if (hasTutorialText("your final instructor")) { + setSubState(SubState.MOVE_ON); return randomDelay(400); + } + if (hasTutorialText("prayer menu")) { + setSubState(SubState.OPEN_PRAYER_TAB); return randomDelay(400); + } + if (hasTutorialText("friends and ignore")) { + setSubState(SubState.OPEN_FRIENDS_TAB); return randomDelay(400); + } + INPC brace = getPrayerInstructor(); + if (brace != null && isAt(brace) && !Players.getLocal().isInteracting()) { + brace.interact("Talk-to"); + log.info("Talking to Prayer Instructor (Brother Brace)."); + return randomDelay(1200); + } + // Fallback: If dialog is closed, try prayer tab + if (!Dialog.isOpen() && !Dialog.isViewingOptions()) { + setSubState(SubState.OPEN_PRAYER_TAB); + return randomDelay(600); + } + return randomDelay(600); + } + + case OPEN_PRAYER_TAB: { + IWidget prayerTab = getWidgetSafe(PRAYER_TAB_PARENT, PRAYER_TAB_CHILD); + if (prayerTab != null && prayerTab.isVisible()) { + prayerTab.click(); + setSubState(SubState.TALK_INSTRUCTOR_2); + return randomDelay(700); + } + log.info("Prayer tab not found, waiting..."); + return randomDelay(600); + } + + case TALK_INSTRUCTOR_2: { + if (handleDialog()) return randomDelay(200); + if (hasTutorialText("friends and ignore")) { + setSubState(SubState.OPEN_FRIENDS_TAB); return randomDelay(400); + } + INPC brace = getPrayerInstructor(); + if (brace != null && isAt(brace) && !Players.getLocal().isInteracting()) { + brace.interact("Talk-to"); + log.info("Talking to Prayer Instructor (after prayer tab)."); + return randomDelay(1200); + } + // Defensive: If dialog is closed and we're stuck, open friends tab + if (!Dialog.isOpen() && !Dialog.isViewingOptions()) { + setSubState(SubState.OPEN_FRIENDS_TAB); + return randomDelay(600); + } + return randomDelay(600); + } + + case OPEN_FRIENDS_TAB: { + IWidget friendsTab = getWidgetSafe(FRIENDS_TAB_PARENT, FRIENDS_TAB_CHILD); + if (friendsTab != null && friendsTab.isVisible()) { + friendsTab.click(); + setSubState(SubState.FINAL_DIALOGUE); + return randomDelay(700); + } + log.info("Friends tab not found, waiting..."); + return randomDelay(600); + } + + case FINAL_DIALOGUE: { + // Special "ready to move on" dialogue option check + IWidget moveOnOption = getWidgetSafe(219, 1, 3); + if (moveOnOption != null && moveOnOption.isVisible() && moveOnOption.getText() != null + && moveOnOption.getText().toLowerCase().contains("ready to move on")) { + Dialog.chooseOption(3); + log.info("Selected special 'ready to move on' dialog option (219,1,3)."); + setSubState(SubState.MOVE_ON); + return randomDelay(400); + } + if (handleDialog()) return randomDelay(200); + if (hasTutorialText("your final instructor")) { + setSubState(SubState.MOVE_ON); return randomDelay(400); + } + INPC brace = getPrayerInstructor(); + if (brace != null && isAt(brace) && !Players.getLocal().isInteracting()) { + brace.interact("Talk-to"); + log.info("Talking to Prayer Instructor (final wrap-up)."); + return randomDelay(1200); + } + // Defensive: If dialog is closed and we're stuck, move on + if (!Dialog.isOpen() && !Dialog.isViewingOptions()) { + setSubState(SubState.MOVE_ON); + return randomDelay(600); + } + return randomDelay(600); + } + + + case MOVE_ON: { + log.info("Completed Prayer Instructor step! Setting plugin state to MAGIC_INSTRUCTOR."); + plugin.setCurrentState(JGTutorialIslandState.MAGIC_INSTRUCTOR); + return randomDelay(600); + } + } + return randomDelay(600); + } + + // --- State inference logic for crash/restart robustness --- + private SubState inferSubState() { + INPC brace = getPrayerInstructor(); + String msg = getTutorialText(); + if (brace == null || !isAt(brace)) return SubState.WALK_TO_CHAPEL; + if (msg.contains("your final instructor")) return SubState.MOVE_ON; + if (msg.contains("prayer menu")) return SubState.OPEN_PRAYER_TAB; + if (msg.contains("friends and ignore")) return SubState.OPEN_FRIENDS_TAB; + if (Dialog.isOpen() || Dialog.isViewingOptions()) { + if (subState == SubState.TALK_INSTRUCTOR_1 || subState == SubState.TALK_INSTRUCTOR_2 || subState == SubState.FINAL_DIALOGUE) + return subState; + } + if (!isPrayerTabOpen()) return SubState.OPEN_PRAYER_TAB; + if (!isFriendsTabOpen() && subState != SubState.OPEN_FRIENDS_TAB) return SubState.TALK_INSTRUCTOR_2; + if (!isFriendsTabOpen()) return SubState.OPEN_FRIENDS_TAB; + if (!Dialog.isOpen() && !Dialog.isViewingOptions()) return SubState.MOVE_ON; + return subState; + } + + // --- Helper/utility methods --- + private INPC getPrayerInstructor() { + try { return NPCs.query().ids(PRAYER_INSTRUCTOR_ID).results().nearest(Players.getLocal()); } + catch (Exception e) { log.warn("getPrayerInstructor exception: {}", e.toString()); return null; } + } + private boolean isAtChapel() { + if (Players.getLocal() == null || Players.getLocal().getWorldLocation() == null) + return false; + int px = Players.getLocal().getWorldLocation().getX(); + int py = Players.getLocal().getWorldLocation().getY(); + return Math.abs(px - CHAPEL_X) <= 2 && Math.abs(py - CHAPEL_Y) <= 2; + } + private boolean isAt(INPC npc) { + try { + return npc != null && Players.getLocal() != null && + Players.getLocal().getWorldLocation() != null && npc.getWorldLocation() != null && + Players.getLocal().getWorldLocation().distanceTo(npc.getWorldLocation()) <= 2; + } catch (Exception e) { return false; } + } + private IWidget getWidgetSafe(int parent, int child) { + try { return Widgets.get(parent, child); } + catch (Exception e) { return null; } + } + private IWidget getWidgetSafe(int parent, int child, int index) { + try { return Widgets.get(parent, child, index); } + catch (Exception e) { return null; } + } + // Null-safe and lowercase + private String getTutorialText() { + IWidget w = getWidgetSafe(TEXT_BOX_PARENT, TEXT_BOX_CHILD, TEXT_BOX_INDEX); + return (w != null && w.isVisible() && w.getText() != null) ? w.getText().toLowerCase() : ""; + } + // Null-safe substring check for tutorial text + private boolean hasTutorialText(String snippet) { + return getTutorialText().contains(snippet.toLowerCase()); + } + private boolean isPrayerTabOpen() { + IWidget prayerTab = getWidgetSafe(PRAYER_TAB_PARENT, PRAYER_TAB_CHILD); + return prayerTab != null && prayerTab.isVisible(); + } + private boolean isFriendsTabOpen() { + IWidget friendsTab = getWidgetSafe(FRIENDS_TAB_PARENT, FRIENDS_TAB_CHILD); + return friendsTab != null && friendsTab.isVisible(); + } + private boolean handleDialog() { + if (Dialog.isViewingOptions()) { + Dialog.chooseOption(1); return true; + } + if (Dialog.isOpen() && Dialog.canContinue()) { + Dialog.continueSpace(); return true; + } + return false; + } + private int randomDelay(int base) { + return base + (int) (Math.random() * 200) - 100; + } + @Override + public boolean isComplete() { + boolean complete = plugin.getCurrentState() != JGTutorialIslandState.PRAYER_INSTRUCTOR; + log.info("isComplete called: currentState={}, complete={}", plugin.getCurrentState(), complete); + return complete; + } +} diff --git a/JG-tutorial-island/src/main/java/steps/QuestGuideStep.java b/JG-tutorial-island/src/main/java/steps/QuestGuideStep.java new file mode 100644 index 0000000..35f5b51 --- /dev/null +++ b/JG-tutorial-island/src/main/java/steps/QuestGuideStep.java @@ -0,0 +1,305 @@ +package steps; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.widgets.Widget; +import net.storm.api.domain.actors.INPC; +import net.storm.api.domain.tiles.ITileObject; +import net.storm.api.domain.widgets.IWidget; +import net.storm.plugins.tutorialisland.JGTutorialIslandPlugin; +import net.storm.plugins.tutorialisland.JGTutorialIslandState; +import net.storm.sdk.entities.NPCs; +import net.storm.sdk.entities.Players; +import net.storm.sdk.entities.TileObjects; +import net.storm.sdk.movement.Movement; +import net.storm.sdk.widgets.Dialog; +import net.storm.sdk.widgets.Widgets; + +@Slf4j +public class QuestGuideStep implements TutorialStep { + private final JGTutorialIslandPlugin plugin; + + private static final int QUEST_GUIDE_ID = 3312; // Confirm with Dev Tools + private static final int DOOR_ID = 9721; // Confirm + private static final int LADDER_ID = 9726; // Confirm + private static final int QUEST_TAB_BUTTON_PARENT = 164, QUEST_TAB_BUTTON_CHILD = 54; + private static final int QUEST_TAB_PARENT = 399, QUEST_TAB_CHILD = 7; + private static final int MUSIC_WIDGET_PARENT = 261, MUSIC_WIDGET_CHILD = 1; + private static final int QUEST_GUIDE_TILE_X = 3088, QUEST_GUIDE_TILE_Y = 3124; + + private enum SubState { + WALK_TO_GUIDE, + OPEN_DOOR, + TALK_GUIDE_1, + OPEN_QUESTS_TAB, + TALK_GUIDE_2, + MUSIC_PLAYER, + MOVE_ON + } + + @Getter + private SubState subState; + + public QuestGuideStep(JGTutorialIslandPlugin plugin) { + this.plugin = plugin; + // Default to TALK_GUIDE_1 (do NOT call inferSubState here) + this.subState = SubState.WALK_TO_GUIDE; + } + + private void setSubState(SubState next) { + if (subState != next) log.info("Changing substate: {} -> {}", subState, next); + subState = next; + } + + private boolean openQuestGuideDoorIfNecessary() { + try { + ITileObject door = TileObjects.getNearest(DOOR_ID); + if (door != null && door.isInteractable()) { + door.interact("Open"); + log.info("Opening Quest Guide room door..."); + return true; + } + } catch (Exception e) { + log.warn("Error while trying to open quest guide door: {}", e.toString()); + } + return false; + } + + private boolean goDownLadderIfNecessary() { + try { + ITileObject ladder = TileObjects.getNearest(LADDER_ID); + if (ladder != null && ladder.isInteractable()) { + ladder.interact("Climb-down"); + log.info("Climbing down ladder to caves..."); + return true; + } + } catch (Exception e) { + log.warn("Error while trying to go down ladder: {}", e.toString()); + } + return false; + } + + private INPC getGuide() { + try { + return NPCs.query().ids(QUEST_GUIDE_ID).results().nearest(Players.getLocal()); + } catch (Exception e) { + log.warn("getGuide() exception: {}", e.toString()); + return null; + } + } + + private boolean isAt(INPC npc) { + try { + return npc != null && + Players.getLocal() != null && + Players.getLocal().getWorldLocation() != null && + npc.getWorldLocation() != null && + Players.getLocal().getWorldLocation().distanceTo(npc.getWorldLocation()) <= 2; + } catch (Exception e) { + return false; + } + } + + private boolean isInMiningCave() { + try { + return Players.getLocal() != null && + Players.getLocal().getWorldLocation().getY() > 9000; + } catch (Exception e) { + return false; + } + } + + @Override + public boolean validate() { + if (isInMiningCave()) { + log.warn("QuestGuideStep: Player is in mining cave; step should be skipped/completed."); + setSubState(SubState.MOVE_ON); + } + boolean valid = plugin.getCurrentState() == JGTutorialIslandState.QUEST_GUIDE; + log.info("Validate called: currentState={}, valid={}", plugin.getCurrentState(), valid); + return valid; + } + + @Override + public int execute() { + if (isInMiningCave()) { + log.warn("QuestGuideStep: Player is in mining cave; skipping step."); + plugin.setCurrentState(JGTutorialIslandState.MINING_INSTRUCTOR); + return 0; + } + plugin.incrementTick(); + + // Safe auto-infer substate at runtime, never in constructor + if (!Dialog.isOpen() && !Players.getLocal().isInteracting()) { + SubState inferred = inferSubState(); + if (inferred != subState) setSubState(inferred); + } + + log.info("---- QuestGuideStep Tick ---- Current subState: {}", subState); + + switch (subState) { + case WALK_TO_GUIDE: { + if (openQuestGuideDoorIfNecessary()) return randomDelay(1200); + INPC guide = getGuide(); + if (guide == null) { + log.warn("Can't find Quest Guide NPC"); + Movement.walkTo(QUEST_GUIDE_TILE_X, QUEST_GUIDE_TILE_Y); + return randomDelay(1000); + } + if (!isAt(guide)) { + Movement.walkTo(guide.getWorldLocation()); + log.info("Walking to Quest Guide at {}", guide.getWorldLocation()); + return randomDelay(1000); + } + log.info("Arrived at Quest Guide. Switching to TALK_GUIDE_1"); + setSubState(SubState.TALK_GUIDE_1); + return randomDelay(600); + } + case TALK_GUIDE_1: { + INPC guide = getGuide(); + if (Dialog.isViewingOptions()) { + Dialog.chooseOption(1); + log.info("Choosing dialog option 1 for guide."); + return randomDelay(400); + } + if (Dialog.isOpen() && Dialog.canContinue()) { + Dialog.continueSpace(); + log.info("Continuing dialog with guide."); + return randomDelay(200); + } + var questTabButton = getWidgetSafe(QUEST_TAB_BUTTON_PARENT, QUEST_TAB_BUTTON_CHILD); + if (questTabButton != null && questTabButton.isVisible()) { + setSubState(SubState.OPEN_QUESTS_TAB); + log.info("Prompt to open quest journal tab detected, switching to OPEN_QUESTS_TAB."); + return randomDelay(600); + } + if (guide != null && isAt(guide)) { + guide.interact("Talk-to"); + log.info("Talking to Quest Guide."); + return randomDelay(1200); + } + return randomDelay(600); + } + case OPEN_QUESTS_TAB: { + var questTabButton = getWidgetSafe(QUEST_TAB_BUTTON_PARENT, QUEST_TAB_BUTTON_CHILD); + if (questTabButton != null && questTabButton.isVisible()) { + questTabButton.click(); + log.info("Clicked quest journal tab button."); + setSubState(SubState.TALK_GUIDE_2); + return randomDelay(600); + } + log.info("Quest tab button not visible, waiting..."); + return randomDelay(600); + } + case TALK_GUIDE_2: { + var textWidget = getWidgetSafe(263, 1, 0); + if (textWidget != null && textWidget.isVisible()) { + String text = textWidget.getText(); + if (text != null && text.toLowerCase().contains("mining and smithing") || text.toLowerCase().contains("moving on")) { + log.info("Detected 'Mining and Smithing' in TALK_GUIDE_2; ready to move on."); + setSubState(SubState.MOVE_ON); + return randomDelay(600); + } + } + INPC guide = getGuide(); + if (Dialog.isViewingOptions()) { + Dialog.chooseOption(1); + log.info("Dialog options open, picking option 1 (TALK_GUIDE_2)."); + return randomDelay(400); + } + if (Dialog.isOpen() && Dialog.canContinue()) { + Dialog.continueSpace(); + log.info("Dialog open, continuing (TALK_GUIDE_2)."); + return randomDelay(200); + } + var questTab = getWidgetSafe(QUEST_TAB_PARENT, QUEST_TAB_CHILD); + if ((questTab != null && questTab.isVisible()) && guide != null && isAt(guide)) { + guide.interact("Talk-to"); + log.info("Talking to Quest Guide (2nd dialog)."); + return randomDelay(1200); + } + log.info("Waiting for dialog/journal completion before moving on."); + return randomDelay(600); + } + case MUSIC_PLAYER: { + var musicWidget = getWidgetSafe(MUSIC_WIDGET_PARENT, MUSIC_WIDGET_CHILD); + if (musicWidget != null && musicWidget.isVisible()) { + musicWidget.interact(); + log.info("Interacting with music player widget."); + setSubState(SubState.MOVE_ON); + return randomDelay(1000); + } + log.info("MUSIC_PLAYER step - widget not visible or not required, skipping."); + setSubState(SubState.MOVE_ON); + return randomDelay(1000); + } + case MOVE_ON: { + if (goDownLadderIfNecessary() || isInMiningCave()) return randomDelay(1200); + log.info("Completed Quest Guide step! Setting plugin state to next step."); + plugin.setCurrentState(JGTutorialIslandState.MINING_INSTRUCTOR); + return randomDelay(600); + } + } + return randomDelay(600); + } + + private IWidget getWidgetSafe(int parent, int child) { + try { return Widgets.get(parent, child); } catch (Exception e) { return null; } + } + private IWidget getWidgetSafe(int parent, int child, int subchild) { + try { return Widgets.get(parent, child, subchild); } catch (Exception e) { return null; } + } + + private int randomDelay(int base) { + return base + (int) (Math.random() * 200) - 100; + } + + // Safe runtime substate inference + private SubState inferSubState() { + try { + INPC guide = getGuide(); + if (guide == null || !isAt(guide)) { + log.info("Infer: Not at Quest Guide, need to WALK_TO_GUIDE."); + return SubState.WALK_TO_GUIDE; + } + var miningWidget = getWidgetSafe(263, 1, 0); + if (miningWidget != null && miningWidget.isVisible()) { + String text = miningWidget.getText(); + if (text != null && text.toLowerCase().contains("mining and smithing")) { + log.info("Infer: 'Mining and Smithing' text present. Substate = MOVE_ON."); + return SubState.MOVE_ON; + } + } + var musicWidget = getWidgetSafe(MUSIC_WIDGET_PARENT, MUSIC_WIDGET_CHILD); + if (musicWidget != null && musicWidget.isVisible()) { + log.info("Infer: Music player widget detected, switching to MUSIC_PLAYER."); + return SubState.MUSIC_PLAYER; + } + if (Dialog.isOpen() || Dialog.isViewingOptions()) { + log.info("Infer: Dialog open or options present, substate = TALK_GUIDE_2."); + return SubState.TALK_GUIDE_2; + } + var questTab = getWidgetSafe(QUEST_TAB_PARENT, QUEST_TAB_CHILD); + if (questTab != null && questTab.isVisible()) { + log.info("Infer: Quest journal open, substate = TALK_GUIDE_2."); + return SubState.TALK_GUIDE_2; + } + var questTabButton = getWidgetSafe(QUEST_TAB_BUTTON_PARENT, QUEST_TAB_BUTTON_CHILD); + if (questTabButton != null && questTabButton.isVisible()) { + log.info("Infer: Quest journal tab button visible, substate = OPEN_QUESTS_TAB."); + return SubState.OPEN_QUESTS_TAB; + } + } catch (Exception e) { + log.warn("Exception in inferSubState: {}", e.toString()); + } + log.info("Infer: At Quest Guide, ready to TALK_GUIDE_1."); + return SubState.TALK_GUIDE_1; + } + + @Override + public boolean isComplete() { + boolean complete = plugin.getCurrentState() != JGTutorialIslandState.QUEST_GUIDE; + log.info("isComplete called: currentState={}, complete={}", plugin.getCurrentState(), complete); + return complete; + } +} diff --git a/JG-tutorial-island/src/main/java/steps/SurvivalExpertStep.java b/JG-tutorial-island/src/main/java/steps/SurvivalExpertStep.java new file mode 100644 index 0000000..bfd55d5 --- /dev/null +++ b/JG-tutorial-island/src/main/java/steps/SurvivalExpertStep.java @@ -0,0 +1,520 @@ +package steps; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.storm.api.domain.items.IInventoryItem; +import net.storm.plugins.tutorialisland.JGTutorialIslandPlugin; +import net.storm.plugins.tutorialisland.JGTutorialIslandState; +import net.storm.sdk.entities.NPCs; +import net.storm.sdk.entities.Players; +import net.storm.sdk.entities.TileObjects; +import net.storm.api.domain.actors.INPC; +import net.storm.api.domain.tiles.ITileObject; +import net.storm.sdk.widgets.Dialog; +import net.storm.sdk.widgets.Widgets; +import net.storm.sdk.game.Client; +import net.storm.sdk.movement.Movement; +import net.storm.sdk.items.Inventory; + +@Slf4j +public class SurvivalExpertStep implements TutorialStep { + private final JGTutorialIslandPlugin plugin; + + private static final int SURVIVAL_EXPERT_ID = 8503; + private static final int FISHING_SPOT_ID = 3317; + private static final int TREE_ID = 9730; + private static final int DOOR_ID = 9398; + private static final int PROXIMITY_RADIUS = 2; + private static final int FIREMAKING_ANIMATION = 733; + + private static final int INVENTORY_TAB_PARENT = 164, INVENTORY_TAB_CHILD = 55; + private static final int SKILLS_TAB_PARENT = 164, SKILLS_TAB_CHILD = 53; + private static final int TUTORIAL_WIDGET_PARENT = 263, TUTORIAL_WIDGET_CHILD1 = 1, TUTORIAL_WIDGET_CHILD2 = 0; + + private static final String[] RAW_SHRIMP_NAMES = {"Raw shrimps", "Raw shrimp"}; + private static final String[] COOKED_SHRIMP_NAMES = {"Shrimps", "Shrimp"}; + private static final String[] LOG_NAMES = {"Logs"}; + private static final String[] AXE_NAMES = {"Bronze axe"}; + private static final String[] TINDERBOX_NAMES = {"Tinderbox"}; + + private boolean choppedTreeForLogs = false; // Set to true only if YOU chopped the tree + + private enum SubState { + WALK_TO_EXPERT, + TALK_EXPERT_1, + OPEN_INVENTORY, + FISH, + OPEN_SKILLS, + TALK_EXPERT_2, + CHOP_TREE, + LIGHT_FIRE, + COOK_SHRIMP, + MOVE_ON + } + + @Getter + private SubState subState; + + public SurvivalExpertStep(JGTutorialIslandPlugin plugin) { + this.plugin = plugin; + this.subState = SubState.WALK_TO_EXPERT; + } + + private void setSubState(SubState next) { + if (subState != next) log.info("Changing substate: {} -> {}", subState, next); + subState = next; + } + + private int randomDelay(int base) { + return base + (int) (Math.random() * 200) - 100; + } + + @Override + public boolean validate() { + boolean valid = plugin.getCurrentState() == JGTutorialIslandState.SURVIVAL_EXPERT; + log.info("Validate called: currentState={}, valid={}", plugin.getCurrentState(), valid); + return valid; + } + + @Override + public int execute() { + plugin.incrementTick(); + + // Re-infer state if not talking/interacting (don’t return, let switch run) + if (!Dialog.isOpen() && !Players.getLocal().isInteracting()) { + SubState inferred = inferSubState(); + if (inferred != subState) setSubState(inferred); + } + + log.info("---- SurvivalExpertStep Tick ---- Current subState: {}", subState); + + switch (subState) { + case WALK_TO_EXPERT: { + if (openStartingDoorIfNecessary()) return randomDelay(1200); + INPC expert = getExpert(); + if (expert == null) { + log.warn("Can't find Survival Expert NPC"); + return randomDelay(600); + } + if (!isAt(expert)) { + Movement.walkTo(expert.getWorldLocation()); + log.info("Walking to Survival Expert at {}", expert.getWorldLocation()); + return randomDelay(1000); + } + log.info("Arrived at Survival Expert. Switching to TALK_EXPERT_1"); + setSubState(SubState.TALK_EXPERT_1); + return randomDelay(600); + } + case TALK_EXPERT_1: { + if (handleDialog()) return randomDelay(200); + + if (hasShrimp()) { + log.info("Detected shrimp in inventory, skipping to OPEN_SKILLS."); + setSubState(SubState.OPEN_SKILLS); + return randomDelay(600); + } + if (hasTutorialText("you've been given an item")) { + setSubState(SubState.OPEN_INVENTORY); + log.info("Infer: Open inventory"); + return randomDelay(600); + } + INPC arrowNpc1 = Client.getHintArrowNpc(); + INPC expert1 = getExpert(); + if ((arrowNpc1 != null && arrowNpc1.getId() == SURVIVAL_EXPERT_ID && !Players.getLocal().isInteracting()) || + (arrowNpc1 == null && isAt(expert1) && !Players.getLocal().isInteracting())) { + log.info("Hint arrow or fallback: Interacting with Survival Expert for first talk..."); + if (arrowNpc1 != null) arrowNpc1.interact("Talk-to"); + else if (expert1 != null) expert1.interact("Talk-to"); + return randomDelay(1200); + } + if (dialogueJustFinished()) { + log.info("Finished initial Survival Expert dialogue. Advancing to OPEN_INVENTORY."); + setSubState(SubState.OPEN_INVENTORY); + return randomDelay(600); + } + return randomDelay(600); + } + case OPEN_INVENTORY: { + var invTab = Widgets.get(INVENTORY_TAB_PARENT, INVENTORY_TAB_CHILD); + if (invTab != null && invTab.isVisible()) { + invTab.click(); + log.info("Clicked inventory tab ({},{})", INVENTORY_TAB_PARENT, INVENTORY_TAB_CHILD); + setSubState(SubState.FISH); + return randomDelay(600); + } + if (hasShrimp()) { + setSubState(SubState.OPEN_SKILLS); + log.info("Already have shrimp, skipping to OPEN_SKILLS."); + return randomDelay(600); + } + return randomDelay(600); + } + case FISH: { + if (hasTutorialText("you've gained some experience")) { + log.info("Widget shows 'you've gained some experience'—skipping to OPEN_SKILLS."); + setSubState(SubState.OPEN_SKILLS); + return randomDelay(600); + } + if (handleDialog()) return randomDelay(200); + + if (!hasShrimp()) { + INPC fishingSpot = getFishingSpot(); + if (fishingSpot != null && !Players.getLocal().isInteracting()) { + fishingSpot.interact("Net"); + log.info("Fishing at spot..."); + return randomDelay(2000); + } + log.info("Fishing spot not found."); + } else { + setSubState(SubState.OPEN_SKILLS); + log.info("Caught shrimp. Proceeding to OPEN_SKILLS."); + } + return randomDelay(600); + } + case OPEN_SKILLS: { + var skillsTab = Widgets.get(SKILLS_TAB_PARENT, SKILLS_TAB_CHILD); + if (skillsTab != null && skillsTab.isVisible()) { + skillsTab.click(); + log.info("Clicked skills tab ({},{})", SKILLS_TAB_PARENT, SKILLS_TAB_CHILD); + setSubState(SubState.TALK_EXPERT_2); + return randomDelay(600); + } + if (hasAxeAndTinderbox()) { + setSubState(SubState.CHOP_TREE); + log.info("Already have axe/tinderbox, skipping to CHOP_TREE."); + return randomDelay(600); + } + return randomDelay(600); + } + case TALK_EXPERT_2: { + if (handleDialog()) return randomDelay(200); + + if (hasAxeAndTinderbox()) { + log.info("Axe and tinderbox detected, skipping to CHOP_TREE."); + setSubState(SubState.CHOP_TREE); + return randomDelay(600); + } + INPC expert2 = getExpert(); + INPC arrowNpc2 = Client.getHintArrowNpc(); + if ((arrowNpc2 != null && arrowNpc2.getId() == SURVIVAL_EXPERT_ID && !Players.getLocal().isInteracting()) || + (arrowNpc2 == null && isAt(expert2) && !Players.getLocal().isInteracting())) { + log.info("Interacting with Survival Expert for axe/tinderbox (TALK_EXPERT_2)."); + if (arrowNpc2 != null) arrowNpc2.interact("Talk-to"); + else if (expert2 != null) expert2.interact("Talk-to"); + return randomDelay(1200); + } + log.info("Waiting for axe/tinderbox. (TALK_EXPERT_2)"); + return randomDelay(600); + } + case CHOP_TREE: { + // Defensive: must have axe and tinderbox + if (!hasAxeAndTinderbox()) { + log.info("Lost axe or tinderbox! Reverting to TALK_EXPERT_2."); + setSubState(SubState.TALK_EXPERT_2); + choppedTreeForLogs = false; + return randomDelay(600); + } + if (!hasShrimp()) { + log.info("Lost shrimp before chopping tree! Returning to FISH."); + setSubState(SubState.FISH); + choppedTreeForLogs = false; + return randomDelay(600); + } + if (handleDialog()) return randomDelay(200); + + // Only proceed to LIGHT_FIRE if the logs in inventory are from chopping *this* tree + if (!hasLogs()) { + ITileObject tree = getTree(); + if (tree != null && !Players.getLocal().isInteracting()) { + tree.interact("Chop down"); + choppedTreeForLogs = true; // Mark: you did the chop, next state must own these logs! + log.info("Chopping tree for logs..."); + return randomDelay(1800); + } + log.info("Tree not found."); + } else if (choppedTreeForLogs) { + setSubState(SubState.LIGHT_FIRE); + log.info("Obtained logs from your own chop. Proceeding to LIGHT_FIRE."); + } else { + log.info("Logs found, but not from your chop. Ensuring you perform the chop before moving on."); + } + return randomDelay(600); + } + case LIGHT_FIRE: { + if (!hasAxeAndTinderbox()) { + log.info("Lost axe/tinderbox before lighting fire. Returning to TALK_EXPERT_2."); + setSubState(SubState.TALK_EXPERT_2); + choppedTreeForLogs = false; + return randomDelay(600); + } + if (!hasShrimp()) { + log.info("Lost shrimp before lighting fire! Returning to FISH."); + setSubState(SubState.FISH); + choppedTreeForLogs = false; + return randomDelay(600); + } + if (!hasLogs() && !isFiremaking()) { + log.info("No logs and not firemaking during LIGHT_FIRE, switching to CHOP_TREE."); + setSubState(SubState.CHOP_TREE); + return randomDelay(400); + } + if (handleDialog()) return randomDelay(200); + + ITileObject fireToCook = getFire(); + if (fireToCook == null) { + if (useTinderboxOnLogs()) { + log.info("Using tinderbox on logs to light fire..."); + choppedTreeForLogs = false; // Consumed logs + return randomDelay(1500); + } + log.info("Failed to light fire (check logic)"); + } else { + setSubState(SubState.COOK_SHRIMP); + log.info("Fire detected. Proceeding to COOK_SHRIMP."); + } + return randomDelay(600); + } + case COOK_SHRIMP: { + if (!hasAxeAndTinderbox()) { + log.info("Lost axe/tinderbox before cooking. Returning to TALK_EXPERT_2."); + setSubState(SubState.TALK_EXPERT_2); + return randomDelay(600); + } + if (!hasShrimp()) { + log.info("No shrimp found during COOK_SHRIMP, need to FISH again."); + setSubState(SubState.FISH); + return randomDelay(400); + } + if (handleDialog()) return randomDelay(200); + + if (!hasCookedShrimp()) { + ITileObject fireToCook = getFire(); + if (fireToCook != null && useShrimpOnFire()) { + log.info("Cooking shrimp on fire..."); + return randomDelay(1500); + } else if (fireToCook == null) { + log.info("No fire detected! Returning to LIGHT_FIRE."); + setSubState(SubState.LIGHT_FIRE); + return randomDelay(600); + } + log.info("Failed to cook shrimp (missing shrimp or fire?)"); + } else { + setSubState(SubState.MOVE_ON); + log.info("Cooked shrimp. Ready to move on."); + } + return randomDelay(600); + } + case MOVE_ON: { + log.info("Completed Survival Expert step! Setting plugin state to MASTER_CHEF"); + plugin.setCurrentState(JGTutorialIslandState.MASTER_CHEF); + return randomDelay(600); + } + } + return randomDelay(600); + } + + // ---- Null-safe helpers ---- + private boolean hasShrimp() { + try { return Inventory.contains(RAW_SHRIMP_NAMES); } catch (Exception e) { return false; } + } + private boolean hasAxeAndTinderbox() { + try { return Inventory.containsAll(AXE_NAMES) && Inventory.containsAll(TINDERBOX_NAMES); } catch (Exception e) { return false; } + } + private boolean hasLogs() { + try { return Inventory.contains(LOG_NAMES); } catch (Exception e) { return false; } + } + private boolean hasCookedShrimp() { + try { return Inventory.contains(COOKED_SHRIMP_NAMES); } catch (Exception e) { return false; } + } + private boolean useTinderboxOnLogs() { + IInventoryItem tinderbox = null, logs = null; + try { + tinderbox = Inventory.getFirst(TINDERBOX_NAMES); + logs = Inventory.getFirst(LOG_NAMES); + } catch (Exception ignore) {} + if (tinderbox != null && logs != null) { + log.info("Using tinderbox on logs..."); + tinderbox.useOn(logs); + return true; + } + return false; + } + private boolean useShrimpOnFire() { + IInventoryItem shrimp = null; + ITileObject fire = null; + try { + shrimp = Inventory.getFirst(RAW_SHRIMP_NAMES); + fire = getFire(); + } catch (Exception ignore) {} + if (shrimp != null && fire != null) { + log.info("Using shrimp on fire..."); + shrimp.useOn(fire); + return true; + } + return false; + } + private boolean isFiremaking() { + try { return Players.getLocal() != null && Players.getLocal().getAnimation() == FIREMAKING_ANIMATION; } catch (Exception e) { return false; } + } + private INPC getExpert() { + try { return NPCs.query().ids(SURVIVAL_EXPERT_ID).results().nearest(Players.getLocal()); } catch (Exception e) { return null; } + } + private INPC getFishingSpot() { + try { return NPCs.query().ids(FISHING_SPOT_ID).results().nearest(Players.getLocal()); } catch (Exception e) { return null; } + } + private ITileObject getTree() { + try { return TileObjects.getNearest(TREE_ID); } catch (Exception e) { return null; } + } + private ITileObject getFire() { + try { return TileObjects.getNearest("Fire"); } catch (Exception e) { return null; } + } + private boolean isAt(INPC npc) { + try { + return npc != null && + Players.getLocal() != null && + Players.getLocal().getWorldLocation() != null && + npc.getWorldLocation() != null && + Players.getLocal().getWorldLocation().distanceTo(npc.getWorldLocation()) <= PROXIMITY_RADIUS; + } catch (Exception e) { + return false; + } + } + private boolean openStartingDoorIfNecessary() { + try { + INPC guide = NPCs.query().ids(3308).results().nearest(Players.getLocal()); + if (guide != null && guide.getWorldLocation() != null && Players.getLocal() != null && + Players.getLocal().getWorldLocation() != null && + guide.getWorldLocation().distanceTo(Players.getLocal().getWorldLocation()) < 6) { + ITileObject door = TileObjects.getNearest(DOOR_ID); + if (door != null && door.isInteractable()) { + door.interact("Open"); + log.info("Opening starting door..."); + return true; + } + } + } catch (Exception e) { + log.warn("Exception in openStartingDoorIfNecessary: {}", e.toString()); + } + return false; + } + + // Unified dialog handler returns true if dialog was handled + private boolean handleDialog() { + try { + if (Dialog.isViewingOptions()) { + Dialog.chooseOption(1); + log.info("Choosing first dialog option."); + return true; + } + if (Dialog.isOpen() && Dialog.canContinue()) { + Dialog.continueSpace(); + log.info("Continuing dialog."); + return true; + } + } catch (Exception ignore) {} + return false; + } + + // Widget cues for dialogue completion + private boolean dialogueJustFinished() { + try { + var w = Widgets.get(TUTORIAL_WIDGET_PARENT, TUTORIAL_WIDGET_CHILD1, TUTORIAL_WIDGET_CHILD2); + if (w != null && w.isVisible()) { + String t = w.getText(); + if (t != null && (t.toLowerCase().contains("you've been given an item") || + t.toLowerCase().contains("open your inventory"))) { + log.info("Detected inventory widget after expert dialogue: '{}'", t); + return true; + } + } + } catch (Exception ignore) {} + return false; + } + + private boolean hasTutorialText(String search) { + try { + var w = Widgets.get(TUTORIAL_WIDGET_PARENT, TUTORIAL_WIDGET_CHILD1, TUTORIAL_WIDGET_CHILD2); + if (w != null && w.isVisible() && w.getText() != null) { + return w.getText().toLowerCase().contains(search.toLowerCase()); + } + } catch (Exception ignore) {} + return false; + } + + // Substate inference: always null-safe, inventory always overrides widget! + private SubState inferSubState() { + try { + var w = Widgets.get(TUTORIAL_WIDGET_PARENT, TUTORIAL_WIDGET_CHILD1, TUTORIAL_WIDGET_CHILD2); + if (hasCookedShrimp()) { + log.info("Infer: Cooked shrimp in inventory, ready to MOVE_ON."); + return SubState.MOVE_ON; + } + if (w != null && w.isVisible() && w.getText().toLowerCase().contains("you've been given an item")) { + log.info("Infer: Open inventory"); + return SubState.OPEN_INVENTORY; + } + if (!hasAxeAndTinderbox()) { + log.info("Infer: Missing axe or tinderbox, need to TALK_EXPERT_2."); + return SubState.TALK_EXPERT_2; + } + if (!hasShrimp()) { + log.info("Infer: Missing shrimp, need to FISH."); + return SubState.FISH; + } + // Only move to LIGHT_FIRE if logs were chopped by player + if (hasLogs() && choppedTreeForLogs) { + log.info("Infer: Has logs (from own chop), ready to LIGHT_FIRE."); + return SubState.LIGHT_FIRE; + } + if (!hasLogs()) { + log.info("Infer: Missing logs, need to CHOP_TREE."); + return SubState.CHOP_TREE; + } + // Widget cues + if (w != null && w.isVisible()) { + String t = w.getText(); + log.info("Widget text: '{}'", t); + if (t != null) { + t = t.toLowerCase(); + if (t.contains("catch some shrimp")) { + log.info("Infer: Widget says 'catch some shrimp', subState=FISH"); + return SubState.FISH; + } + if (t.contains("you've gained some experience") || t.contains("check your skills") || t.contains("view the skills")) { + log.info("Infer: Widget says 'gained experience' or 'check your skills', subState=OPEN_SKILLS"); + return SubState.OPEN_SKILLS; + } + if (t.contains("on this menu you can view your skills")) { + log.info("Infer: Widget says 'talk to your instructor', subState=TALK_EXPERT_2"); + return SubState.TALK_EXPERT_2; + } + if (t.contains("light a fire")) { + log.info("Infer: Widget says 'light a fire', subState=LIGHT_FIRE"); + return SubState.LIGHT_FIRE; + } + if (t.contains("woodcutting")) { + log.info("Infer: Widget says 'woodcutting', subState=CHOP_TREE"); + return SubState.CHOP_TREE; + } + } + } + // Fallback + INPC expert = getExpert(); + if (expert == null || !isAt(expert)) { + log.info("Infer: Not at expert, need to walk there."); + return SubState.WALK_TO_EXPERT; + } + } catch (Exception e) { + log.warn("Exception in inferSubState: {}", e.toString()); + } + log.info("Infer: Defaulting to TALK_EXPERT_1."); + return SubState.TALK_EXPERT_1; + } + + @Override + public boolean isComplete() { + boolean complete = plugin.getCurrentState() != JGTutorialIslandState.SURVIVAL_EXPERT; + log.info("isComplete called: currentState={}, complete={}", plugin.getCurrentState(), complete); + return complete; + } +} diff --git a/JG-tutorial-island/src/main/java/steps/TutorialStep.java b/JG-tutorial-island/src/main/java/steps/TutorialStep.java new file mode 100644 index 0000000..5048737 --- /dev/null +++ b/JG-tutorial-island/src/main/java/steps/TutorialStep.java @@ -0,0 +1,7 @@ +package steps; + +import net.storm.api.plugins.Task; + +public interface TutorialStep extends Task { +boolean isComplete(); +} \ No newline at end of file diff --git a/JG-tutorial-island/src/main/java/util/PathingUtil.java b/JG-tutorial-island/src/main/java/util/PathingUtil.java new file mode 100644 index 0000000..1cbef42 --- /dev/null +++ b/JG-tutorial-island/src/main/java/util/PathingUtil.java @@ -0,0 +1,5 @@ +package util; + +public class PathingUtil { + // Pathfinding helpers will go here +} \ No newline at end of file diff --git a/JG-tutorial-island/src/main/java/util/WidgetUtil.java b/JG-tutorial-island/src/main/java/util/WidgetUtil.java new file mode 100644 index 0000000..fac8dba --- /dev/null +++ b/JG-tutorial-island/src/main/java/util/WidgetUtil.java @@ -0,0 +1,5 @@ +package util; + +public class WidgetUtil { + // Dialog/click helpers will go here +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 5536e04..ccfd675 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -30,7 +30,7 @@ subprojects { create("basic") } credentials { - username = "" + username = "JG" password = System.getenv("GITHUB_PACKAGES_PAT") } } diff --git a/settings.gradle.kts b/settings.gradle.kts index b0b0539..9d148a1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,7 +4,8 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") include( "example-looped-plugin", - "example-task-plugin", + "JG-example-plugin", + "JG-tutorial-island", ) for (project in rootProject.children) { From dc05dd9163ca6a16e721060b3e012c1c179887d0 Mon Sep 17 00:00:00 2001 From: Jimmy Gross Date: Tue, 20 May 2025 13:11:43 -0400 Subject: [PATCH 2/6] version 0.8 - still in debug, but after a very small amount of manual effort at combat instructor, completes from start to finish. Noted bugs QUEST GUIDE - Sometimes doesn't open the door, patching. Combat Instructor - Sometimes gets stuck in a state loop, enhancing checks. Mining Guide - Need to extend delay for the bronze dagger, currently random so will do it until the random amount is enough to complete the action --- JG-tutorial-island/src/main/java/steps/BankerStep.java | 1 - 1 file changed, 1 deletion(-) diff --git a/JG-tutorial-island/src/main/java/steps/BankerStep.java b/JG-tutorial-island/src/main/java/steps/BankerStep.java index 212c51e..10ddca2 100644 --- a/JG-tutorial-island/src/main/java/steps/BankerStep.java +++ b/JG-tutorial-island/src/main/java/steps/BankerStep.java @@ -17,7 +17,6 @@ public class BankerStep implements TutorialStep { private final JGTutorialIslandPlugin plugin; - // TODO: Replace all these with real IDs and coords private static final int BANK_BOOTH_ID = 10083; private static final int POLL_BOOTH_ID = 26815; private static final int ACCOUNT_GUIDE_ID = 3310; From fba94cc3822a47bda1d05b3d867023b7b9295e02 Mon Sep 17 00:00:00 2001 From: JamesGross99 Date: Tue, 20 May 2025 13:35:33 -0400 Subject: [PATCH 3/6] Create TODO --- TODO | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 TODO diff --git a/TODO b/TODO new file mode 100644 index 0000000..bff0dfd --- /dev/null +++ b/TODO @@ -0,0 +1,2 @@ +Enable Ironman selection support +Currently works by creating character, and naming it. Once out of that interface, start plugin. From d956a416380a460aac2da19bac20981acd4c9f64 Mon Sep 17 00:00:00 2001 From: Jimmy Gross Date: Tue, 20 May 2025 14:34:50 -0400 Subject: [PATCH 4/6] Redact code --- README.md | Bin 97 -> 22 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/README.md b/README.md index 72308683a3199c2e75555a8ff564433b018a3c6f..3b13a46c8f3261276d5461e614917ab3891288de 100644 GIT binary patch literal 22 acmezWFNh(PA%!84A(^2B%;II>VgLYA+66HH literal 97 zcmY#Z2rkJl%2hB@aIHwpEyzh#2*@c-&&=a;%u`5(isY8&lw|7W=BJbbrPGTNQ-D-a hYC(Q+CQxgoLRx;2LQ-jFPD*B8I^47ZuqnlQTmX%>BCY@c From 73cb3afd43975b6ebe602fdae39b656cbafcc13f Mon Sep 17 00:00:00 2001 From: JGDevelopment Date: Tue, 20 May 2025 16:07:43 -0400 Subject: [PATCH 5/6] Create redactcode --- redactcode | 1 + 1 file changed, 1 insertion(+) create mode 100644 redactcode diff --git a/redactcode b/redactcode new file mode 100644 index 0000000..7990a28 --- /dev/null +++ b/redactcode @@ -0,0 +1 @@ +n/a From 89468c1722d1895d86d8754d258b1dea648f07ea Mon Sep 17 00:00:00 2001 From: Jimmy Gross Date: Tue, 20 May 2025 19:07:39 -0400 Subject: [PATCH 6/6] version 0.8 - still in debug, but after a very small amount of manual effort at combat instructor, completes from start to finish. Noted bugs QUEST GUIDE - Sometimes doesn't open the door, patching. Combat Instructor - Sometimes gets stuck in a state loop, enhancing checks. Mining Guide - Need to extend delay for the bronze dagger, currently random so will do it until the random amount is enough to complete the action --- TODO | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO b/TODO index bff0dfd..1d75906 100644 --- a/TODO +++ b/TODO @@ -1,2 +1,4 @@ Enable Ironman selection support Currently works by creating character, and naming it. Once out of that interface, start plugin. + +just checkingg this update