Jump to content
×
×
  • Create New...

[TUTORIAL] Your first script - Giant Slayer Part 3 (UPDATED)


Recommended Posts

Welcome to the (probably) final f2p part of this script.

Lets get right into business.

 

We will start with reorganizing some stuff and changes to existing classes.

1) Refactoring

First , lets change our Task class slightly. Since we'll  be adding a GUI with different options and removing predefined variables like food item , we won't be able to access them in a static way.

That's why we're adding reference to our main class (GiantSlayer) to our abstract Task class.  Create new variable of type GiantSlayer and change the constructor to include it.

Note: You will have to change constructors of all existing task classes to include reference to the main class

 protected GiantSlayer main;

    public Task(C ctx, GiantSlayer main) {
        super(ctx);
        this.main = main;
    }

 

Now lets create a new class for constants (static variables).  I named it GConstants. I created a new package for this class called "utility" (toma.giantslayer.utility).

Lets add some constants that we'll need. First I'll start with the array of Tiles that we'll be used to walk to and from bank. 

TIP: If you're wondering how I generate path tiles or areas, I use a great little tool called Explv's map. PowBot is not officially supported but you can use DreamBot setting and you should be good. It will save you a lot of time.

This is our final GConstants class. Most of the things should be self explanatory. There's two Areas , first one is small area with only 2 hill giants for safespotting, and the other one includes whole place.

public class GConstants {

    public final static Area GIANTS_AREA_SAFESPOT = new Area(new Tile(3104, 9840, 0), new Tile (3095, 9824, 0));

    public final static Area GIANTS_AREA = new Area(new Tile(3093, 9853, 0), new Tile(3128, 9822, 0));

    public final static Area BANK_AREA_WEST = new Area(new Tile(3179, 3448), new Tile( 3191, 3432));

    public final static Tile SAFESPOT = new Tile(3098,9837,0);

    public final static int BONES_ID = 532;

    public final static int BRASS_KEY = 983;

    public final static String[] FOOD_ITEMS = {"Shrimp", "Herring", "Trout", "Salmon", "Tuna", "Lobster", "Swordfish"};

}

 

Now lets change some stuff in the main class.  We're now using name of the food and not the id. We're getting eatAtHealth from GUI so we're not storing it directly in the Eat task anymore.

Variable startScript is used to stop poll() method from looping through tasks while our GUI is still open.  Most of this variables will be assigned through GUI.

I also added variables for keeping track of every combat skill that are used for showing exp. gains on paint. 

 

    public boolean startScript = false;
    public String foodName = "Tuna";
    public int foodAmount = 5;
    public int eatAtHealth = 15;
    public int ammoId = 888;
    public String targetName = "Hill Giant";
    public Area giantsArea;
    public boolean buryBones = true;
    public int minItemPrice = 400;

    // Tasks
    public List<Task> taskList = new ArrayList<Task>();

    // Vars for paint
    public long sAttackXp, sStrengthXp, sDefenceXp, sHitpointsXp, sRangeXp, sMagicXp;
    public long cAttackXp, cStrengthXp, cDefenceXp, cHitpointsXp, cRangeXp, cMagicXp;

Now just add if statement in poll() method so we don't loop tasks until we're done with GUI.

    public void poll() {
        if (startScript) {
            for (Task t : taskList) {
                if (t.activate()) {
                    t.execute();
                    break;
                }
            }
        }
    }

 

2) Creating GUI

Good scripts present users with many different options to meet their individual needs. We want user to be able to choose if they want to bank or use different healing items, loot items, etc. What's a better way than using a GUI.  Fortunately there's already a java library for that, called Swing. Now, I won't be teaching you Swing because that is not the point of this tutorial, and also I'm pretty bad at it. We'll be making a small,simple and functional GUI by hand. I won't be explaining much of the Swing classes and inner workings. There are also programs that you can use for creating Swing GUIs in drag&drop style (WYSIWYG) , like IntelliJ built-in swing designer or NetBeans swing designer.  So lets start.

Create a new class named GUI in utility package.

Extend GUI with JFrame. JFrame is a top level container for GUIs inside which you can place other gui elements like panels, labels, text fields, etc. All of the Swing classes start with J (JLabel, JCheckBox, etc.).

We'll need checkboxes for banking, burying bones, looting arrows, safespotting; text fields for food withdraw amount, minimum item price (for looting); combobox for healing items; slider for when to eat; some labels and button for starting script. I declared all of those in the class. I'm using GridBagLayout layout manager. Layout managers help you define how elements are placed in the window and how they scale relative to other elements.

If using GridBagLayout, for every element that goes in that layout you can define a set of variables (GridBagConstraints) that will tell the manager where to place that element and how it'll behave.

So that's what I do for our elements. You can play around with different values if you want. If you want to learn more about swing or layout managers you can check oracle's site .

You don't need to worry about all of that code. Most important part is start button and finishSetup() function. 

Swing elements are not just passive , they can react and change when you click on them (or type). Almost all of them have many different events like actionPerformed, stateChanged and other similar events that you can "subscribe" to by calling different listener methods on those elements.

In our case we're only interested in what happens when a user clicks on the start button. When a user clicks on that button, we want to call our own function that will extract information from gui elements and define our script variables based on that info. So we create a new action listener on that button that will call finishSetup() whenever that action is performed. 

Note: I'm using lambdas here. You can also use anonymous class or you can even create a new ActionListener class and send it to this function.

startBtn.addActionListener((event)-> finishSetup());

In finishSetup() function we add tasks and define script variables based on selected options in the gui. Eveything should be understandable.  The only newish thing is what we do if pickupAmmo is selected.

ctx.game.tab(Game.Tab.EQUIPMENT);  // Opens equipment tab
if (ctx.equipment.itemAt(Equipment.Slot.QUIVER).valid()) { // Check if there's an item in the quiver slot
  main.ammoId = ctx.equipment.itemAt(Equipment.Slot.QUIVER).id(); // Set ammoId to id from equipment slot
}
Condition.sleep(350); // Sleep for good measure
ctx.game.tab(Game.Tab.INVENTORY, true); // Go back to inventory

First we open equipment tab and then check if there's valid item in the quiver slot, if there is we save the id of that item in ammoId. ctx.game.tab() can also take optional boolean argument for hotkey usage.

That's all we have to do in the GUI class. I know that most of those tasks don't exist yet. That is what we're gonna do next.

If you wanna test the GUI. You should initialize it in start() function of main class.

// Start the GUI
SwingUtilities.invokeLater((() -> {
	GUI gui = new GUI(ctx, this );
}));

Finished gui should look something like this.

hillGui.png.d0096e9382236b5f085f91d945492014.png

 

Here's a link for full GUI class

 

3) Tasks

3.1) Refactoring Attack and Eat task

As I've said before, since we changed our Task class we need to change all existing tasks to include reference to our main class.

So constructor should look like this.

public Eat(ClientContext ctx, GiantSlayer main) {
        super(ctx, main);
}

Also , activate() will now have reference to eatAtHealth from main class.

return Integer.parseInt(healthComp.text()) < main.eatAtHealth;

In execute(), I added one new line, right at the start of the function. This will make sure that inventory is open when we try to interact with food.

 // Open inventory tab if it's not already open
ctx.game.tab(Game.Tab.INVENTORY , true);

And we don't use id of the food anymore but the name that was selected by a user in the gui. You can change that yourself.

 

Now for Attack class.

In activate() we now use variable for hill giant name instead of string literal, and we also have one more condition for activation.

 return (!ctx.players.local().interacting().name().equals(main.targetName)
         && main.giantsArea.contains(ctx.players.local()));

Area class has a method contains() that takes any object that implements Locatable  and checks if that object is inside of that area. So in addition to not being in combat, we also must be inside of defined area to search for npcs.

3.2) MoveToSafespot - support for safespotting

Lets add support for safespotting. Create a new task class and name it MoveToSafespot.

We want to move to a safe spot only if we're in combat and we aren't already standing in the safe spot. So activate() will look like this.

public boolean activate() {
	return ctx.players.local().interacting().name().equals(main.targetName) && !ctx.players.local().tile().equals(GConstants.SAFESPOT);
}

Now for execute().  We don't want to use ctx.movement.step() unless safespot tile is not in viewport because step() will always use the minimap for moving and that's not very humanlike if a tile is really close.

For interacting with tiles we have to use matrix() property of Tile class.  

If tile is visible on screen, we click on it, otherwise we use step() to click on the minimap. And then we wait until we're on that tile.

public void execute() {
    if (GConstants.SAFESPOT.tile().matrix(ctx).inViewport()) {
    	GConstants.SAFESPOT.tile().matrix(ctx).interact("Walk here");
    }
    else {
    	ctx.movement.step(GConstants.SAFESPOT);
    }

    Condition.wait(new Callable<Boolean>() {
    	@Override
    	public Boolean call() throws Exception {
    	return ctx.players.local().tile().equals(GConstants.SAFESPOT);
    	}
  	}, 250, 10);
}

Now we have support for safespotting, niceeee. Lets move on.

3.3) PickAmmo - loot and equip ammo

Create a new task class PickAmmo.  You could implement this in different ways, but I decided that it should activate only if we're not in combat.

So activate() looks like this.

public boolean activate() {
	GroundItem ammo =  ctx.groundItems.toStream().within(10)
               .filter((gi)-> gi.id() == main.ammoId)
               .sorted(Comparator.comparingInt(GroundItem::stackSize).reversed()).first();
	return !ctx.players.local().interacting().valid() && ammo.stackSize() >= 5;
 }

For interacting with ground items we need to use GroundItem class.  So first I select ground items in radius of 10 , I use a filter to keep items with id ammoId, then I sort them  by stackSize,  from high to low, and then I  take the one with the biggest stack size. Using Comparator class for sorting by stackSize,  .reveresed() because we want max to min ( default is min to max).

Our activate condition is that we're not in combat and that the stack size of our polled ground item is >= 5. I chose 5 arbitrarily ( you could perhaps have it as an option in the gui).

For execute() we have two parts.  In first part, we find that ground item again, check if it's valid and in viewport, if it is we interact with it and wait until it's gone, if it's not in viewport we step() towards it.

GroundItem ammo =  ctx.groundItems.toStream().within(10)
                .filter((gi)-> gi.id() == main.ammoId)
                .sorted(Comparator.comparingInt(GroundItem::stackSize).reversed()).first();
if (ammo.valid()) {
  if (ammo.inViewport()) {
    if (ammo.interact("Take")) {
      Condition.wait(new Callable<Boolean>() {
        @Override
          public Boolean call() throws Exception {
          return !ammo.valid();
        }
      }, 500, 5);
    }
  }
  else {
    ctx.movement.step(ammo);
  }
}

And in next part we compare stack size of our ammo , this time in inventory, so we could equip it if it's over some arbitrary amount ( I use random number between 10 and 45, but you can put whatever you want).

if (ctx.inventory.toStream().id(main.ammoId).count(true) >= Random.nextInt(10,45)) { // Compare ammo in inventory
	if (ctx.inventory.toStream().id(main.ammoId).first().interact("Wield")) {
		Condition.wait(()-> ctx.inventory.toStream().id(main.ammoId).isEmpty(), 450, 5);
	}
 }

We need to use bool true in count() because we're dealing with a stackable item. If we have enough ammo we then interact with it and if the interact was success we wait until there's no more ammo in inventory.

So far I've used lambdas and anonymous functions interchangeably, but from now on I'll be using lambdas exclusively. If you want to learn more about that you can check this tutorial here.

3.4) Looting stuff

For simplicity (or complexity?) I will have one task that loots anything and optionally buries bones.

Create a new task, name it PickLoot. I created a variable for the ground item filter so the code would look nicer. We define it in the constructor.

private final Filter<GroundItem> itemFilter;

public PickLoot(ClientContext ctx, GiantSlayer main) {
	super(ctx, main);

	itemFilter = new Filter<GroundItem>() {
    		@Override
    		public boolean accept(GroundItem item) {
    			return (GeItem.getPrice(item.id()) > main.minItemPrice
        		|| (main.buryBones && item.id() == GConstants.BONES_ID && item.inViewport())
        		&& main.giantsArea.contains(item));}};
}

For activate() we want to return true if player is not in combat, inventory is not full and there are items on the ground that we want to loot.

Filters for items to loot are :  -  item price must be greater than minItemPrice (assigned through gui) OR buryBones is enabled and item id equals big bones id and bones are in viewport - GeItem class  is used for gettng item price

                                                AND  an item is inside the giants area

 

public boolean activate() {
	// Filter items only if not in combat and we have free inventory space
	if (!ctx.players.local().interacting().valid() && ctx.inventory.toStream().count() < 28) {
		return !ctx.groundItems.toStream().filter(itemFilter).nearest().isEmpty();
	}
	return  false;
}

 

Execute() is akin to PickAmmo() in that both consist of 2 similar parts.

First we loot the item (if it's still valid)

GroundItem item =  ctx.groundItems.toStream().filter(itemFilter).nearest().first();
if (item.valid()) {
  if (item.inViewport()) {
    if (item.interact("Take", item.name())) {
      Condition.wait( () -> !item.valid(), 500, 5);
    }
  }
  else {
    ctx.movement.step(item);
    ctx.camera.turnTo(item);
  }
}

And then we check if we need to buryBones is enabled. If it is, filter out  bones in inventory, and if there are more than 3 (arbitrarily chosen), loop through them and bury them. You can play with Condition.wait() frequency here.

if (main.buryBones) {
	List<Item> bonesInv =  ctx.inventory.toStream().id(GConstants.BONES_ID).collect(Collectors.toList());
	if (bonesInv.size() >= 3)
		bonesInv.forEach((bone) -> { Condition.wait(()->  bone.interact("Bury") && !bone.valid() , 250, 5);});
}

We use forEach() stream method to quickly and elegantly loop through stream elements (using lambda as an input function).

Now we're able to loot any item. Woohoo.

 

3.5) Banking support (revised to use a webwalker)

Lets now implement the most important option, support for banking. Starting with tasks for walking to and from bank.

We have two different combat areas, defined in GConstants. Smaller one is used for safespotting and the big one is used when safespotting is not checked.

combtAreas.png.93ed3e614576c1ab952bce1478f2d181.png

3.5.1) Walking to giants area

To keep it simple, main criteria for activating walking tasks will be amount of food in inventory.

So create a new task class , name it WalkToGiants.

Activate() is simple. We want to walk to giants are if we're not already there and we have some food left.

public boolean activate() {
    return !main.giantsArea.contains(ctx.players.local()) && !ctx.inventory.toStream().name(main.foodName).isEmpty();
}

 

We're gonna use WebWalker API to walk between the bank and the hill giants area. WebWalker automatically handles obstacles. This makes it incredibly easy to use.

All we need to do is call one simple function - ctx.movement.moveTo()

First argument specifies end Tile (actually anything that implements Locatable). We take a random tile from giantsArea.

Second argument is false because we don't want to refresh quests (quests are not needed for this path), and the final argument is to force it to use WebWalk API through the whole path (instead of local pathing).

And that's all. Easy.

public void execute() {
	ctx.movement.moveTo(main.giantsArea.getRandomTile(), false, true);
	ctx.game.tab(Game.Tab.INVENTORY);
 }

 

 

3.5.2) Walking to bank

Create a new task, name it WalkToBank.

For walking to bank we check if there's no more food left and we're not already in the bank area.

public boolean activate() {
	return ctx.inventory.toStream().name(main.foodName).isEmpty() && !GConstants.BANK_AREA_WEST.contains(ctx.players.local());
}

 

Execute() is almost the same as in WalkToGiants. Only difference is that now we use random tile rom bank area.

public void execute() {
	ctx.movement.moveTo(GConstants.BANK_AREA_WEST.getRandomTile(), false, true);
	ctx.game.tab(Game.Tab.INVENTORY);
}

And that's all for walking.

 

3.5.3) Opening bank 

Create another task, name is OpenBank.

 

For opening bank we have a few conditions. Player must be in the bank area, bank is not already open, and we have no food in inventory or no brass key.

public boolean activate() {
  return GConstants.BANK_AREA_WEST.contains(ctx.players.local()) && !ctx.bank.opened()
    && (ctx.inventory.toStream().name(main.foodName).isEmpty() || ctx.inventory.toStream().id(GConstants.BRASS_KEY).isEmpty());
}

 

In execute()  we first check if there's a bank in viewport, if not walk towards one. If there's bank in viewport try to open it and wait until it's open. Easy peasy.

public void execute() {
  if (ctx.bank.inViewport()) {
    // Find neareast bank and try to open it
    if (ctx.bank.open()) {
      Condition.wait(()-> ctx.bank.opened(), 1000, 2);
    }
  } else {
    // Bank not in viewport, lets walk towards it
    ctx.movement.step(ctx.bank.nearest().tile());
    ctx.camera.turnTo(ctx.bank.nearest());
  }
}

 

3.5.4) Depositing and withdrawing

Now once bank window is open we have to interact with it.

Create a new task, name it Withdraw.

We want this task to activate only if bank window is open and there's no food or brass key in inventory.

public boolean activate() {
  return ctx.bank.opened()
    && (ctx.inventory.toStream().name(main.foodName).isEmpty() || ctx.inventory.toStream().id(GConstants.BRASS_KEY).isEmpty());
}

For simplicity and speed I went with depositInventory() option. You can use depositAllExcept() function but if you have lots of different items it will take ages to deposit all of them.

First we deposit everything in our inventory (including brass key), then we withdraw brass key : ). After that we check if there's food in bank , and if there is we withdraw the amount that the user put in the gui. If there's no more food left, we logout and stop the script.

public void execute() {
  ctx.bank.depositInventory();

  ctx.bank.withdraw(GConstants.BRASS_KEY, 1); 
  // Check if there is food in bank
  if (!ctx.bank.toStream().name(main.foodName).isEmpty()) {
    ctx.bank.withdraw(main.foodName, main.foodAmount);
    Condition.wait(() -> !ctx.inventory.toStream().name(main.foodName).isEmpty(), 450, 6);

  } else { // No food left in the bank , stop the script
    ctx.game.logout();
    ctx.controller.stop();
  }
}

 

Aaaand baam, we're done with banking stuff.

 

4) Getting rid of level-up message

You may have noticed that if you're safespotting and npcs can't get to you and you happen to level up your range or mage, you will get stuck on the level-up message. That is because poll() method stops looping once that message has been received. So the only way to get rid of it is to have a function that will check if there's a level up message and clear it. But that function must be running on a different thread so it doesn't get stuck with everything else. I'm not really a master of Java concurrency so this will be quick and dirty.

We will need another class , but this time it won't extend Task, but rather Runnable interface. So create another class in "utility" package and name it MessageClearer. This is the whole class.

package toma.giantslayer.utility;

import org.powerbot.script.rt4.ClientContext;

public class MessageClearer implements Runnable {

    private ClientContext ctx;
    public MessageClearer(ClientContext ctx) {
        this.ctx = ctx;
    }
    @Override
    public void run() {
        while (!ctx.controller.isStopping()) {
            if (ctx.chat.canContinue()) {
                ctx.input.send(" ");
            }

            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Runnable interface requires of us to to implement run() method. That method will be called once we create and start the new thread in our main class.

In our cause,  it will not exit from run() until our script has stopped running. We use ctx.chat api to query if there's a valid chat window that has continue as an option, which most(?) chat windows have, including our level-up window. In case that's true, we simulate "space" button input and that should clear the message. After that conditional we have a sleep() for a good measure.

Now all we need to do is create an instance of this class in the main class and run that function on a new thread.

So add this 2 new variables to a GiantSlayer class :

private Thread cThread;
private MessageClearer messageClearer;

And now on the top of the start() function:

// Init and start message clearer thread
cThread = new Thread(messageClearer, "MessageCleaner");
cThread.start();

 

And now you shouldn't have problems with level-up messages.


Here's a link to GiantSlayer class.

THE END

This is the end of this part. If you have any suggestions, critique or questions about the code feel free to post below.

Thanks for reading : ) 

 

Edited by Toma
  • Like 4
Link to post
Share on other sites

Damn do people actually write their own GUI classes instead of using some generator, my brain would go numb lol

 

Serious question: Does Powbot cache the GE prices now? Previously (hadn't scripted in a while) we had to cache the price lookups on our own. From your code, I assume this is handled by the client now?

GeItem.getPrice(groundItem.id()) > main.minItemPrice
Link to post
Share on other sites
1 hour ago, Dad said:

Damn do people actually write their own GUI classes instead of using some generator, my brain would go numb lol

 

Serious question: Does Powbot cache the GE prices now? Previously (hadn't scripted in a while) we had to cache the price lookups on our own. From your code, I assume this is handled by the client now?

GeItem.getPrice(groundItem.id()) > main.minItemPrice

No way am I doing it manually for anything more complex , haha.


Yep.  Prices get updated hourly.

Link to post
Share on other sites
  • Toma changed the title to [TUTORIAL] Your first script - Giant Slayer Part 3 (UPDATED)

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.