Jump to content
×
×
  • Create New...

[TUTORIAL] Your first script - Giant Slayer Part 3


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. 

If you're wondering how I generate path tiles and 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 Tile[] PATH_TO_GIANTS = {
            new Tile(3182, 3440, 0),
            new Tile(3182, 3446, 0),
            new Tile(3181, 3453, 0),
            new Tile(3174, 3453, 0),
            new Tile(3165, 3453, 0),
            new Tile(3158, 3451, 0),
            new Tile(3153, 3446, 0),
            new Tile(3149, 3443, 0),
            new Tile(3143, 3439, 0),
            new Tile(3136, 3440, 0),
            new Tile(3129, 3441, 0),
            new Tile(3123, 3443, 0),
            new Tile(3115, 3448, 0),
            new Tile(3115, 3451, 0),
            new Tile(3116, 9849, 0),
            new Tile(3114, 9843, 0),
            new Tile(3111, 9836, 0),
            new Tile(3098, 9835, 0)
    };

    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 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. Any 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 small and simple, albeit 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.select(10).id(main.ammoId).sort((o1, o2) -> o2.stackSize() - o1.stackSize()).peek();
    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 with ammoId, then I sort them  by stackSize,  from high to low, and then I  take the one with the biggest stack size.

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.select().id(main.ammoId).sort((o1, o2) -> { return o2.stackSize() - o1.stackSize();}).poll();
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.select().id(main.ammoId).count(true) >= Random.nextInt(10,45)) { // Compare ammo in inventory
    if (ctx.inventory.poll().interact("Wield")) {
        Condition.wait(()-> ctx.inventory.select().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.

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

                                                -  currently used area contains that item

We're also limiting to radius of 12.

public boolean activate() {
    // Query ground items only if player is not in combat
    if (!ctx.players.local().interacting().valid()) {
		GroundItems items = (GroundItems) ctx.groundItems.select(12).select((groundItem) ->
                          (GeItem.getPrice(groundItem.id()) > main.minItemPrice
                          || (main.buryBones && groundItem.id() == GConstants.BONES_ID && groundItem.inViewport()))
                          && main.giantsArea.contains(groundItem)).nearest();
		return ctx.inventory.select().size() < 28 && !items.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.nearest().poll();
if (item.valid()) {
  if (item.inViewport()) {
    if (item.interact("Take")) {
      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 query for bones in inventory, 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) {
  ItemQuery<Item> bonesInv = ctx.inventory.select().id(GConstants.BONES_ID);
  if (bonesInv.size() >= 3) {
    for (Item b : bonesInv) {
      Condition.wait(()->  b.interact("Bury") && !b.valid() , 250, 5);
    }
  }
}

 

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

 

3.5) Banking support

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 is 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.select().name(main.foodName).isEmpty();
}

 

We can't just use api for traversing tile paths because our paths contain obstacles. We need to do some things manually. 

Two main functions are nextTile() and handleObstacles(). They are both implemented inside of our main class so we wouldn't have to duplicate them for each walking class. They will be explained shortly after this.

So first we get the best next tile in the path by calling nextTile() and giving it a path. If that tile is reachable ( no obstacle in the path) we move towards it using step(), and then we wait until nextTile has changed to something else. Btw, there are definitely optimizations to do here.

If it's not reachable it means that there's  an obstacle in the way, so we must call handleObstacle()

 

 public void execute() {
   Player p = ctx.players.local();

   // Find a next tile and check if it's reachable
   Tile nextTile = main.nextTile(ctx.movement.newTilePath(GConstants.PATH_TO_GIANTS));        
   if (nextTile.matrix(ctx).reachable()) {
     ctx.movement.step(nextTile);
     Condition.wait(() -> !ctx.movement.destination().equals(main.nextTile(ctx.movement.newTilePath(GConstants.PATH_TO_GIANTS))), 250,10);
   }
   else {
     main.handleObstacle(nextTile); // Handle door or ladder
   }
 }

Lets now quickly go through nextTile() and handleObstacle(). Both implemented in GiantSlayer class.

First we get Tile array from TilePath class so we could loop through all the tiles. Then we save next tile as suggested by the api to variable nx. 

We start from end of the path and go through each tile. If we have come to the tile that was suggested by the api, that means that we should either take that tile or the next tile is not reachable by the api and in that case we must manually correct it and take the next one. Otherwise we just check if the distance to current tile in the loop is less than 8, if it is, take that tile.

public Tile nextTile(TilePath path) {
  Tile[] aPath = path.toArray();
  Tile nx = path.next();
  Player p = ctx.players.local();

  int index = 0;
  for (int i = aPath.length - 1; i >= 0; i--) {
    if (aPath[i].equals(nx)) {
      if (i + 1 <= aPath.length - 1 && nx.distanceTo(p) < 3) { // Next tile might be unreachable by the api so we must check if it exists
        index = i + 1;
        break;
      }
      index = i;
      break;
    } else if (aPath[i].distanceTo(p) < 8){
      index = i;
      break;
    }
  }
  return aPath[index];
}

 

 

Now for handling obstacles. I made this very simple since we know there's only door and ladder in our path. If you haven't already noticed Edgeville dungeon has the same Z coordinate as the ( z=0 ) as the surface level.

We have only 2 cases.

In first case we check if distance is relatively small (100 randomly chosen) and if it we must be going through door , so we query for nearest door and interact with it.

In second case distance to next tile is huge (ex. Tile(3115, 3451, 0) -> Tile(3116, 9849, 0) ). We must be going down the ladder ( or up the ladder), so we just query for nearest ladder and interact with it.

I used filter in case of ladders because for going down it's "Climb-down" and for going up it's "Climb-up". So I just filtered for actions that contain "Climb".

After interacting we check if it was successful and if it was we wait until animation goes to idle and we have stopped moving.

 

public void handleObstacle(Tile t) {
  Player p = ctx.players.local();
  GameObject obstacle = null;

  // Only two options
  // distance to next tile is relatively small (door in between)
  // or distance to next tile is huge (ladder in between)
  // 100 should suffice

  if (p.tile().distanceTo(t) < 100D) { // Handle door
    obstacle = ctx.objects.select(5).name("Door").nearest().poll();
    obstacle.interact("Open");
  }
  else { // Handle ladder
    obstacle = ctx.objects.select(5).name("Ladder").nearest().poll();
    obstacle.interact((f)-> f.action.contains("Climb") );
  }

  if (ctx.game.crosshair() == Game.Crosshair.ACTION) {
    Condition.wait(() -> p.animation() == -1 && !p.inMotion(), 1000, 4);
  }
}

 

 

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.select().name(main.foodName).isEmpty() && !GConstants.BANK_AREA_WEST.contains(ctx.players.local());
}

 

Execute() is almost the same as in WalkToGiants. Only difference is that we have to reverse the path because I defined our path to start at the bank.

public void execute() {
    Player p = ctx.players.local();

    // Find a next tile and check if it's reachable
    Tile nextTile = main.nextTile(ctx.movement.newTilePath(GConstants.PATH_TO_GIANTS).reverse());
    if (nextTile.matrix(ctx).reachable()) {
        ctx.movement.step(nextTile);
        Condition.wait(() -> !ctx.movement.destination().equals(main.nextTile(ctx.movement.newTilePath(GConstants.PATH_TO_GIANTS).reverse())), 250,10);
    }
    else {
        main.handleObstacle(nextTile);
    }
}

 

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.select().name(main.foodName).isEmpty() || ctx.inventory.select().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.select().name(main.foodName).isEmpty() || ctx.inventory.select().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.select().name(main.foodName).isEmpty()) {
    ctx.bank.withdraw(main.foodName, main.foodAmount);
    Condition.wait(() -> !ctx.inventory.select().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 : ) 

 

  • Like 3
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

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.