Hello Rune-Server, today I made my first NPC from scratch, and I decided to turn it into a slightly larger tutorial! I'm going to be covering a pretty large surface with this tutorial, ranging from client commands, the NPCDef class, to NPC definitions on the server-side and CombatStrategies!
What I'll be showing you:
- Tools and Commands
- Designing the NPC
- Creating a CombatStrategy
- Advanced CombatStrategy
So let's get down and dirty, with step 1:
Tools and Commands
[SPOIL]For this tutorial, I used quite a few debugging tools and commands to help me on my way.
Dump NPC defs by Linus - BEWARE, this will take some renaming. Just change entityDef to npc in Ruse, you should also remove the recolor features (That's what I did at least, if you can figure out how it works in Ruse be my guest lol)
RSMV (RS Model Viewer) - You can find this just about anywhere, not gonna link lol
The data from your cache - If you can't unpack this, search up like 474 model dats, should find what you need.
Also, this command in your client:
Code:
case "modelinf":
sendConsoleMessage("Model ID: " + ItemDef.forID(Integer.parseInt(args[1])).modelID, true);
sendConsoleMessage("Wear: " + ItemDef.forID(Integer.parseInt(args[1])).maleEquip1, true);
sendConsoleMessage("Zoom: " + ItemDef.forID(Integer.parseInt(args[1])).modelZoom, true);
sendConsoleMessage("RotX: " + ItemDef.forID(Integer.parseInt(args[1])).rotationX, true);
sendConsoleMessage("RotY: " + ItemDef.forID(Integer.parseInt(args[1])).rotationY, true);
break;
All of these things should give you everything you need to identify model IDs, dump NPC defs so they can be added to NPCDef class, and create your own custom NPC!
PS, use modelinf in the console to get the model of armor or weps to equip your NPC/boss
[/SPOIL]
Designing the NPC
[SPOIL]This isn't a very complicated task, and it's all up to personal preference, for this segment, I'll just provide you with the NPCDef case of my first NPC.
What I did was review the dump.txt that was written to my desktop by Linus's dump method, then in RSMV I checked out the array of models of a humanoid NPC I picked (In my case, it was NPC 200 since he isn't ingame and I really doubt I'll ever use him). After looking at all of the models in RSMV, I labeled which ones were which. I don't think the order REALLY matters, but then again this was my first attempt with this and I haven't done much experimenting.
NPCDef case for my NPC: Rex Goliath
Code:
case 200:
npc.name = "Rex Goliath";
npc.walkAnim = 1660;
npc.standAnim = 11973;
npc.models = new int[9];
npc.models[0] = 230; //HEAD
npc.models[1] = 246; //JAW
npc.models[2] = 49352; //CHEST
npc.models[3] = 10313; //CAPE
npc.models[4] = 49347; //ARM
npc.models[5] = 49349; //HAND
npc.models[6] = 5409; //WEP
npc.models[7] = 49351; //LEG
npc.models[8] = 49348; //BOOT
npc.actions = new String[5];
npc.actions[0] = "Examine";
npc.actions[1] = "Attack";
npc.actions[2] = null;
npc.actions[3] = null;
npc.actions[4] = null;
break;
As you review these models in RSMV, you'll notice the wep slot has the model for a whip. This is why I set his walk and stand anims to what I did, if he's wielding a standard wep like a scimitar or sword, you'll need a different stand anim.
Notice the examine button is before the attack button, that's switched if you're a high enough level to attack him with a left click. actions[0] should always be examine when making a combat capable NPC.
Then, on the server-side, you'll need to change his npc definition in /src/data/def/json/npc_definitions.json
Search for "id": 200 and replace that case with this:
Code:
{
"id": 200,
"name": "Rex Goliath",
"examine": "A mighty warrior!",
"combat": 138,
"size": 1,
"attackable": true,
"aggressive": true,
"retreats": false,
"poisonous": false,
"respawn": 10,
"maxHit": 970,
"hitpoints": 10000,
"attackSpeed": 7,
"attackAnim": 11968,
"defenceAnim": 11974,
"deathAnim": 2304,
"attackBonus": 500,
"defenceMelee": 350,
"defenceRange": 350,
"defenceMage": 150
},
Most of this is self explanatory, but remember this is the constitution system so hitpoints and maxHit are multiplied by 10. I chose attackSpeed 7 for this NPC because it seemed like a reasonable number, you can experiment with faster and slower enemies. Beware that this will be overridden when you create the CombatStrategy in the next step![/SPOIL]
Creating the CombatStrategy
[SPOIL]Creating the CombatStrategy for Rex Goliath is a simple, yet daunting task. For those who have never used the CombatStrategy class in Ruse before, please check out my other tutorial on making a simple Combat Stone Here
Let's start with the basic CombatStrategy class, first create a new class in com.ruse.world.content.combat.strategy.impl, we'll call it RexGoliath to keep things simple. Then, you're going to implement CombatStrategy like this:
Code:
package com.ruseps.world.content.combat.strategy.impl;
import com.ruseps.world.content.combat.CombatContainer;
import com.ruseps.world.content.combat.CombatType;
import com.ruseps.world.content.combat.strategy.CombatStrategy;
import com.ruseps.world.entity.impl.Character;
public class RexGoliath implements CombatStrategy{
Hopefully you're using Eclipse, because this is going to throw an error. Hover over the class name and implement the methods.
Your class should now look like this:
Code:
package com.ruseps.world.content.combat.strategy.impl;
import com.ruseps.world.content.combat.CombatContainer;
import com.ruseps.world.content.combat.CombatType;
import com.ruseps.world.content.combat.strategy.CombatStrategy;
import com.ruseps.world.entity.impl.Character;
public class Example implements CombatStrategy{
@Override
public boolean canAttack(Character entity, Character victim) {
// TODO Auto-generated method stub
return false;
}
@Override
public CombatContainer attack(Character entity, Character victim) {
// TODO Auto-generated method stub
return null;
}
@Override
public boolean customContainerAttack(Character entity, Character victim) {
// TODO Auto-generated method stub
return false;
}
@Override
public int attackDelay(Character entity) {
// TODO Auto-generated method stub
return 0;
}
@Override
public int attackDistance(Character entity) {
// TODO Auto-generated method stub
return 0;
}
@Override
public CombatType getCombatType() {
// TODO Auto-generated method stub
return null;
}
}
Now, after looking at the code, hovering over the different methods you're blindly overriding and figuring out what they're for, you're going to set up the boss.
Somewhere in the top, you'll need to declare his main attack animation, and since he wields a whip, it will look like this:
Code:
Animation attackAnim = new Animation(11968);
After this, we need to give him a basic attack, since we'll be adding other cases in the next step.
Looking at the customContainerAttack method, it's safe to assume that'll go here. Keeping it simple, we'll start his animation then we'll create a CombatBuilder() and declare his hook, his victim, his hit amount, the delay, the type of attack, and if it should check the accuracy or not inside of a CombatContainer(). Simple, right? More like too simple for a CombatStrategy at this point!
Code:
@Override
public boolean customContainerAttack(Character entity, Character victim) {
NPC rex = (NPC)entity;
rex.performAnimation(attackAnim);
rex.getCombatBuilder().setContainer(new CombatContainer(rex, victim, 1, 6, CombatType.MELEE, true));
return false;
}
Then we should set up the other methods, so they return SOMETHING, though these values are pretty generic.
Code:
@Override
public int attackDelay(Character entity) {
// TODO Auto-generated method stub
return entity.getAttackSpeed();
}
@Override
public int attackDistance(Character entity) {
// TODO Auto-generated method stub
return 2;
}
@Override
public CombatType getCombatType() {
// TODO Auto-generated method stub
return CombatType.MIXED;
}
You should now have a class that looks like this:
Code:
package com.ruseps.world.content.combat.strategy.impl;
import com.ruseps.model.Animation;
import com.ruseps.world.content.combat.CombatContainer;
import com.ruseps.world.content.combat.CombatType;
import com.ruseps.world.content.combat.strategy.CombatStrategy;
import com.ruseps.world.entity.impl.Character;
import com.ruseps.world.entity.impl.npc.NPC;
public class RexGoliath implements CombatStrategy{
Animation attackAnim = new Animation(11968);
@Override
public boolean canAttack(Character entity, Character victim) {
// TODO Auto-generated method stub
return false;
}
@Override
public CombatContainer attack(Character entity, Character victim) {
// TODO Auto-generated method stub
return null;
}
@Override
public boolean customContainerAttack(Character entity, Character victim) {
NPC rex = (NPC)entity;
rex.performAnimation(attackAnim);
rex.getCombatBuilder().setContainer(new CombatContainer(rex, victim, 1, 6, CombatType.MELEE, true));
return false;
}
@Override
public int attackDelay(Character entity) {
// TODO Auto-generated method stub
return entity.getAttackSpeed();
}
@Override
public int attackDistance(Character entity) {
// TODO Auto-generated method stub
return 2;
}
@Override
public CombatType getCombatType() {
// TODO Auto-generated method stub
return CombatType.MIXED;
}
}
Now there's nothing left to do but add the CombatStrategy hook in com.ruseps.world.content.combat.strategy.CombatStr ategies like this:
Code:
STRATEGIES.put(200, new RexGoliath());//NPC id, strategy
[/SPOIL]
Advanced CombatStrategies!
[SPOIL]I created a PvP style boss while writing this tutorial, and here's the source code! I'll explain what it does, but not in too much detail since the tutorial already explains MOST of it. There's a random int generated between 1 and 10, and if it rolls 2, he'll "eat a shark", doing the eat anim and gaining 20 hp, while also saying a random line from an array of forcedChat strings. Otherwise, he'll do the normal attack we wrote just before. If he heals, it subtracts from healsLeft, so he can't heal infinitely, I have it set where he can only heal 7 times. His attack distance is also significantly higher, so people can't run as easily
Code:
package com.ruseps.world.content.combat.strategy.impl;
import java.util.Random;
import com.ruseps.engine.task.Task;
import com.ruseps.engine.task.TaskManager;
import com.ruseps.model.Animation;
import com.ruseps.model.Graphic;
import com.ruseps.model.Locations;
import com.ruseps.model.Projectile;
import com.ruseps.world.content.combat.CombatContainer;
import com.ruseps.world.content.combat.CombatType;
import com.ruseps.world.content.combat.strategy.CombatStrategy;
import com.ruseps.world.entity.impl.Character;
import com.ruseps.world.entity.impl.npc.NPC;
public class RexGoliath implements CombatStrategy {
Animation attackAnim = new Animation(11968);
int healsAllowed = 7;
String[] chat = { "Not so fast!", "I still have " + healsAllowed + " sharks left!", "Food left?" };
// Graphic fireGfx = new Graphic(393);
@Override
public boolean canAttack(Character entity, Character victim) {
// TODO Auto-generated method stub
return true;
}
@Override
public CombatContainer attack(Character entity, Character victim) {
return null;
}
@Override
public boolean customContainerAttack(Character entity, Character victim) {
NPC rex= (NPC) entity;
if (Locations.goodDistance(rex.getPosition().getX(), rex.getPosition().getY(), victim.getPosition().getX(),
victim.getPosition().getY(), 14)) {
rex.setChargingAttack(true);
Random rand = new Random();
switch (rand.nextInt(10)) {
case 2:
if (healsAllowed >= 1) {
rex.performAnimation(new Animation(829));
rex.setConstitution(rex.getConstitution() + 50);
rex.forceChat(chat[rand.nextInt(chat.length)]);
healsAllowed--;
}
break;
}
rex.getCombatBuilder().setContainer(new CombatContainer(rex, victim, 1, 6, CombatType.MELEE, true));
rex.performAnimation(attackAnim);
// victim.
}
return false;
}
@Override
public int attackDelay(Character entity) {
// TODO Auto-generated method stub
return entity.getAttackSpeed();
}
@Override
public int attackDistance(Character entity) {
// TODO Auto-generated method stub
return 7;
}
@Override
public CombatType getCombatType() {
// TODO Auto-generated method stub
return CombatType.MIXED;
}
}
[/SPOIL]
Thanks for the read, guys! Hope you found this useful. Sorry it's so messy, I did my best to organize it but it's a lot for one post haha.
TL;DR: I put the finished classes at the end of the last two parts, and the NPCDef and server definitions in part two lol
Edit: Afterthought, seems the healsAllowed doesn't work, iunno