Jump to content
×
×
  • Create New...

[TUTORIAL] Your first script - Giant Slayer Part 2


Recommended Posts


Welcome to the 2nd part of my renowned "Your first script" series.

As I've mentioned in the previous article, this time we'll start with restructuring of the script.

Before we start, I just wanna say that I won't be mentioning every time you have to import a new class. You should be able to do it yourself from now on
(remember "alt+K" on a red text or red squiggly lines).


If you find something wrong with the code or in general please post in comments.
And if you have any suggestions for new features feel free to post in comments.


1) Task framework

Right now we're implementing all of our logic in the main class(GiantSlayer). That's not a bad thing by itself, especially since our script
is very simple, but as our script gets more complex it would get harder and harder to manage all of that code in just one class. 
Poll method would be the first one that gets extremely messy and hard to understand. 

Since we're using OOP language and principles, it might benefit us if we would try and encapsulate every functionality in a separate unit (class).
That way it gets a lot easier to manage and add or remove different parts of the script (banking, healing, looting, etc) without affecting other parts.

So lets start. Delete heal and attack function and remove everything inside of poll function so that final class looks like this.

public class GiantSlayer extends PollingScript<ClientContext> {

    private final static int FOOD_ID = 2309;

    @Override
    public void start() {

    }

    @Override
    public void poll() {

    }
}


Now create a new package inside of "yourname.GiantSlayer" package and name it "tasks".
My package looks like "toma.giantslayer.tasks".
Now in that package create a class named "Task". This will be our base abstract class that our concrete classes will extend.
We will not be creating instances of this class but rather classes that extend Task.
Now put this code into that class.

public abstract class Task <C extends ClientContext> extends ClientAccessor<C> {

    public Task(C ctx) {
        super(ctx);
    }

    public abstract boolean activate();

    public abstract void execute();
}


We have two methods, activate() and execute(). They are both abstract, that means we'll need to implement each of them in every child class.
The important thing to notice is that this class expects ClientContext in the constructor, ie. when we're creating a new class. We need it so we could interact 
with the powbot api. 

So how this will work is that for example, we will have a class called Eat that will be used for healing.
What we need to know is when do we want to heal and what to do when healing is needed.
As you may have figured it out, activate method will return true if healing criterion is met, and execute method will do the actual healing.

What we will then have is a list of tasks (attack, heal, loot, etc.) that we will constantly loop over and call activate and execute.

Before we start creating our tasks, lets first create a list of tasks in a main class and loop in the poll method.
So add this variable under our food constant.

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

And this for loop in poll() method.

for (Task t : taskList) {
    if (t.activate()) {
        t.execute();
        break;
    }
}


Note: It might not be obvious but break;  is very important. It will be explained after we have some tasks.


Alright, now we can start implementing our tasks.


2) Implementing tasks

2.1) Attack task

Lets create a new class in tasks package, name it "Attack".
Extend this class with a Task. You will need to put ClientContext inside of extended Task. Like so.

public class Attack extends  Task<ClientContext>

Now use autocomplete to implement missing funcitions. You should now have an empty Attack class with an Attack constructor and  execute and activate functions.

 

Lets start with activate. This will be very simple. We want to attack only if we're not already in combat.

public boolean activate() {
    // Find an npc that we're interacting with
    // If that npcs name is not "Hill Giant" return true so we could find another target, otherwise return false because we're in combat
    return (!ctx.players.local().interacting().name().equals("Hill Giant"));
}


Now a bit more complex, execute function. There's a bunch of new api functions but just follow comments and it should be clear enough.

public void execute() {

    // Find the nearest npc that is not interacting with any other player
    // or is interacting with us but for some reason we are not attacking it
    BasicQuery<Npc> npcQuery = ctx.npcs.select().name("Hill Giant").select(new Filter<Npc>() {
        @Override
        public boolean accept(Npc npc) {
            return (npc.interacting().equals(ctx.players.local())) || (!npc.interacting().valid() && npc.healthPercent() > 0);
        }
    }).nearest().limit(4);
    
    // Create a temp. variable for npc
    Npc currentNpc = npcQuery.peek();
    
    // Check if one of them is attacking us
    for (Npc n : npcQuery) {
        if (n.interacting().equals(ctx.players.local())) {
            // change currentNpc to the attacking one
            currentNpc = n;
        }
    }

    // If the npc exist and is in viewport, attack it, otherwise step towards the npc
    if (currentNpc.valid()) {
        if (currentNpc.inViewport()) { // If the npc is on the screen
            currentNpc.interact("Attack");
            // if we have successfully performed the Attack action
            if (ctx.game.crosshair() == Game.Crosshair.ACTION) {
                // Wait until we have started interacting
                Condition.wait(new Callable<Boolean>() {
                    @Override
                    public Boolean call() {
                        return currentNpc.interacting().equals(ctx.players.local());
                    }
                }, 450, 4);
            }
        }
        else {
            ctx.movement.step(currentNpc);
            ctx.camera.turnTo(currentNpc);

            Condition.wait(new Callable<Boolean>() {
                @Override
                public Boolean call() throws Exception {
                    return currentNpc.inViewport();
                }
            },250, 10);

        }
    }
}


Ok, so first, lets start with a BasicQuery<Npc>. As I've said before most of the query functions will return a modified query (other than poll and peek).
Since we use ctx.npcs.select() to find a list of npcs that we can attack, our query will be of type Npc.
First we repopulate the query with select().name("Hill Giant") and then we use a new query function select(Filter<K>).
What this will do is run accept method from Filter<Npc> for each Npc in the query. It will keep only npcs for which accept returns true.
We want a list of all the npcs that are either not interacting with anyone OR are interacting with us but we're not interacting with them (for example if we have Auto Retaliate off).
So we want to return:
npc.interacting().equals(ctx.players.local()) || (!npc.interacting().valid() && npc.healthPercent() > 0)

npc.healthPercent() > 0  is just so we don't try to attack npcs that are about to die.
We will then sort them by distance(nearest) and then limit to only first 4 (you can play with this limit).

Once we have the list, we'll save the nearest npc in the variable. After that we want to check if there's an npc that's already attacking.
So we loop through the list and if we find the one that's interacting with us we assign it to that previous variable.

Now we check if the npc is valid (if it exists), then if it's in the viewport (visible on screen), if both are true we try to attack it.
We can then test if the mouse was clicked on something interactable(crosshair will be red if there was something interactable under the mouse).
And if that is also true, we then wait until we have started interacting with the npc.

If the nearest npc is not visible, we have to move our character.
For movement we can use ctx.movement.step(), it will try to step somewhere near the npc. For good measure we also rotate the camera towards the npc.
So we don't spam click while moving we can write another conditional wait. We will wait until our new target is visible, for the maximum 250*10 = 2500 ms = 2.5 seconds.

And that's it for Attack class.

Now we can go to the main class and add our new class to the list. We do this in start function.

public void start() {

    taskList.add(new Attack(ctx));
    
}


If you have done everything correctly,  you can compile and run the script and see if it works.


2.2) Eat task and Widgets

So now that we have attacking out of the way we can (re)implement healing.
In the first part of the tutorial, for checking player health we were using ctx.players.local.healthPercent(). That method sucks, to say the least.
From now on we will use Widgets to get our players current health.


2.2.1) Widgets api
What are Widgets? Widgets are interactive display windows that store some kind of data and Components . Essentially everything you see on the screen is part of a Widget, 
be it bank window, inventory, chat window, minimap button, etc.
Components are subparts of Widgets that can also contain more Components. It works like a hierarchy.    
Components are the ones that contain actual data that we want.

Widget0
    Component0
    Component1
        Component0
    Component2
Widget1
    Component0

        

If you look at the game screen you can see the little health globe next to a minimap that shows your current health. That is a Widget, or rather a Component of a widget.
We want to extract the text that shows the current health.

healthWidget.png.df03273e5269ca62803917e9b725762c.png

 


The api for interacting with widgets looks like this:

ctx.widgets.widget(INDEX_OF_WIDGET).component(INDEX_OF_COMPONENT)

To find index of a widget or a component you can use Widget explorer tool inside of the client (View -> Widgets).
When you select a widget or a component , a yellow rectangle will be placed around the widget/component (that's the most help you'll get).
Unless the component has some kind of a text, searching by filter won't help much, so sometimes it can take a while before you find what you're looking for.
Fortunately I know both indexes.

Widget160->Component5

widgetExplorer.thumb.png.b07c2934a77951115da2b8fd288a6a0b.png

 

2.2.2) Eat task 

Now lets create a new class "Eat" in tasks package. Extend  Eat with Task and implement empty methods as in previous task.

Create a new variable in Eat  class that will be used for activating eating (you would usually have this kind of variables in a main class or some other class that you can then pass to tasks).

private final int eatAtHealth = (int) (0.5 * ctx.skills.realLevel(Constants.SKILLS_HITPOINTS));


What ctx.skills.realLevel() does is it returns as it says, real level of a skill, aka. level prior to boosts or status decrease.
Number 0.5 stands for percentage (50%). If you're hitpoints level is 60 eatAtHealth will be -> 0.5 * 60 = 30
We cast it to int so it rounds up. EatAtHealth will be compared to our current health in activate function.

We need one more variable and that is our widget component.

private final Component healthComp = ctx.widgets.widget(160).component(5);

Now we're ready to finish our task.

Activate function is very simple. Just compare our current health to our eatAtHealth.
We use text method from a component object that returns our current health as a string. Because we can't compare string to an int we need to cast it to int.
If our current health is less than eatAtHealth we activate Eat task.
As you may or may not have noticed, I don't use any temporary variable for a food item.
That is because as I've said before, query is persistent. I repopulate the query with food items when checking if it's not empty.
Then on the next line I just use ctx.inventory.peek() (without select()) to get a head of the query and then .interact() to interact with it.

public boolean activate() {
    return Integer.parseInt(healthComp.text()) < eatAtHealth;
}

Now for execute function.

public void execute() {
    // Save our current health so we can compare later
    int startHealth = Integer.parseInt(healthComp.text());
    // Check if inventory contains at least 1 food item 
    // Instead of checking if size of returned query is more than 0 , we check if it's NOT empty
    if (!ctx.inventory.select().id(GiantSlayer.FOOD_ID).isEmpty()) {
        // We found some food, lets eat it, interact returns true if it clicked and false if it failed to cli
        if (ctx.inventory.peek().interact("Eat")) {
            //We clicked on food, lets wait until our current health is higher than our startHealth variable
            Condition.wait(new Callable<>() {
                @Override
                public Boolean call() {
                    return Integer.parseInt(healthComp.text()) > startHealth;
                }
            }, 350, 5);
        }
    }
    else {
        // Oops, we're out of food
        // For now we will just stop the script
       ctx.controller.stop();
    }
}


We're now done with this task.
You can now add this task to the list of tasks inside of stop function in main class, but be careful of the order of insertion.
You can look at the poll function and then try to see why the order matters.

Not all tasks are born equal. Some of them are more important, like our Eat task.
What happens if you put Attack task before Eat? Well, nothing really.
But imagine if you had a bunch of tasks before Eat task that take a significant time to process. If you fail to eat for some reason (missclick), it will then break from the loop and start again from the first task.                        
We would rather have our Eat task repeated instead, that's why I'd put break; in the first place. What would be even worse is if it couldn't get to our crucial healing task in time because it keeps breaking out of the loop.
In contrast, if we put Eat task at first place in the list, and it fails to eat, it will break as usual and start from the beginning. But now the first task is Eat task so it will try to eat again and that's exactly what we'd wanted. 

Alright, now you can compile and run your script. See if everything works correctly.


3) Simple info paint

Last thing we will do in this part is adding simple informational paint on our game window. For that we will be using PaintListener interface.
PaintListener interface has only one method, repaint(). It essentially works like poll(), that means that repaint() is called every frame until we stop the script.
repaint() has one parameter of type Graphics which we'll use to draw different elements onto a screen.

So implement PaintListener in our main class and implement missing repaint() method.

public class GiantSlayer extends PollingScript<ClientContext> implements PaintListener

We'll start with combat experience and script run time. For now we'll just hardcode a skill we're training, you can add whichever you want. I'm gonna do defense.
    
Ok so declare a new variable in our main class. This we'll be our starting experience.

public int startXp;

Now define it in our start() method. We're using experience() method in ctx.skills to get current experience in a skill.

startXp = ctx.skills.experience(Constants.SKILLS_DEFENSE);

I also defined a new font right before repaint()

private final Font helveticaFont = new Font("Helvetica", 0, 12);

Now lets add some stuff to repaint()

public void repaint(Graphics g) {

    long currXp = ctx.skills.experience(Constants.SKILLS_DEFENSE);
    long gainedXp = currXp - startXp;

    // Paint window
    g.setColor(new Color(0, 0, 0,255));
    g.drawRect(5,5,250,100);
    g.setColor(new Color(33, 33, 33, 230));
    g.fillRect(5,5,250,100);

    // Info text
    g.setFont(helveticaFont);
    g.setColor(new Color(172, 172, 172,255));
    g.drawString("Combat exp. gained: " + gainedXp + " (" + getPerHour(gainedXp, this.getRuntime()) +"/pH)", 8, 25);
    g.drawString("Time running: " + formatTime((int)this.getRuntime()/1000), 8, 60);

}

First we get our current experience and then we compute gained exp. by subtracting it from starting experience.
Whenever you change any field(font, color, stroke width, etc) in Graphics variable it will stay like that until you change it to something else.
So we use setColor() to change color and then drawRect() to draw "window" (unfilled rectangle) and then fillRect() to fill that window.
After that we change to custom font and change color again for our text.
For drawing text we use drawString() that takes a string and x, y parameters.
As you can see I'm drawing gained experience and exp/hour which I calculate with a helper function that you can either implement yourself or check the spoiler.
For time running we can use getRuntime() method that our main class inherits from PollingScript. It returns script runtime in milliseconds.
I also use helper function to format time into something more readable. 

 

private long getPerHour(long in, long time) {
    return (int) ((in) * 3600000D / time);
}

private String formatTime(long time) {
    return String.format("%d:%02d:%02d", time / 3600, (time % 3600) / 60, (time % 60));

}

 

Now if you run your script you should see a small info paint.
My looks like this.

infoPaint.png.433cbd07466a409bb961a8bfabb18fc2.png

 

 


This is it for now. If you have any problems or questions don't hesitate to post below.
In the next part, we'll be adding item looting, burying bones, safespot support and banking.
 

  • Like 5
  • Thanks 1
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.