Author Topic: Making a race tutorial. v1.1.2+  (Read 11936 times)

Ownz

  • Developer
  • Administrator
  • Hero Member
  • *****
  • Posts: 2437
  • chmod -R 777 *
    • View Profile
    • OwnageClan
Making a race tutorial. v1.1.2+
« on: July 10, 2010, 10:33:46 AM »
As of version 1.1.2, the race registration system has changed to allow a race to have different number of skills (although we still recommend 3 skill 1 ultimate races)


To get started making a race, you must meed ALL of the following REQUIREMENTS (yes its repetitive):
1 Know what sourcemod is.
2 Know how to compile a sourcemod plugin.
3 Know the sourcepawn programming language itself.
4 Know to use how sourcepawn/sourcemod to do specific tasks.
5 Able to create the race you are thinking of in a standalone plugin. This means that you are theoretically able to make a plugin that gives everyone the functions of that race without war3source, you demonstrate that you know how to hook events and set player properties.
6 You are not copy and pasting other races.

If you don't have all requirements met, you need to hang out more in the sourcemod forums, and ask scripting questions there.

This is NOT the place to ask: how do I set heath, how do I set ammo, how do I set gravity...etc. You should know how to set these before creating such a race.

This is also NOT the place to ask how to use War3Source to set things for you, post these questions in their own topics: how do I set health with War3Source, how do i set gravity with War3Source, how do i set speed with War3Source.


This tutorial teaches you the inner workings of the main War3Source plugin and allows you, the race developer, to run your race under the war3source plugin, and properly use the war3source interface (natives and forwards) to do tasks. After this tutorial, you are encouraged to use default packaged races as examples to help you create your new race.

Please follow the conventions given in this tutorial, such as variable names, it really helps us help you.

After you have read this tutorial, i have some recorded video sessions with beginner programmers. Download teamviewer (@teamviewer.com) and download the videos.
http://ownageclan.com/teamviewer/753%20789%20456%202010-07-29%2010.35.tvs building a copy cat race: low gravity, criticals, evasion, and flame strike.

If you have any specific questions about this tutorial, please do post a topic.

Lets get started!
« Last Edit: July 12, 2011, 08:43:49 PM by Ownz »

Ownz

  • Developer
  • Administrator
  • Hero Member
  • *****
  • Posts: 2437
  • chmod -R 777 *
    • View Profile
    • OwnageClan
Re: Making a race tutorial. v1.1.2+
« Reply #1 on: July 10, 2010, 10:47:55 AM »
You need to prepare a space for your programming files and .sp includes. We REALLY WANT YOU TO MAKE a separate folder inside the scripting folder (ie scripting/War3SourceProgramming)
From now on the "root" folder which i refer to will be War3SourceProgramming folder. My folder was named War3SourceP.

Dump all the packaged War3Source_XXXX.sp and the folder "W3SIncs" into root
You don't actually need all of it, but we want you to do it anyway because we it allows you to see inner workings of War3Source and use packaged races as examples.

compile.exe is there just for click and compile purposes, you can try to do the same: copy compile and spcomp.exe to this folder.

Go to the W3SIncs folder and read War3Source_Interface.inc
This is referred to as the war3source "API"
This file lists all the functions that are available to you (forwards, natives, and stocks)

In case you don't know:
Forward- a function call to you (and usually other plugins) and variables are passed to you if any.
Its like one plugins says "Hey everyone, this person's and this race and this skill level changed to this this and this, just letting u know! Thanks for tuning in".
You are not required to implement all forwards, the forward will not affect you if you did not implement it. However some are very important like the one below:
ie OnSkillLevelChanged(client,race,newlevel)

Native- a function call to another plugin (the plugin that registered it, War3Source registered War3_GetGame() so when u call War3_GetGame() War3Source handles it), you provide the variables to them if any. Sort of like a forward to a single plugin. Natives we use often to return values like War3_GetGame() returns you a number which indicates the current game (CS TF DODS).

Stock- A normal function provided by an include file. If unused, it won't be compiled.
« Last Edit: November 09, 2010, 11:06:13 PM by Ownz »

Ownz

  • Developer
  • Administrator
  • Hero Member
  • *****
  • Posts: 2437
  • chmod -R 777 *
    • View Profile
    • OwnageClan
Re: Making a race tutorial. v1.1.2+
« Reply #2 on: July 10, 2010, 10:52:40 AM »
Lets start coding shall we?



Create a new .sp file. Preferably name it War3Source_<race name here>.sp

Open it with your text editor for .sp files
(I use the defunct PawnStudio which still crashes alot, but IntelliSense-autocompletion is great for new programmers)

Add the following if it does not exist (probably not because we made a new blank file)

Code: [Select]
#pragma semicolon 1    ///WE RECOMMEND THE SEMICOLON

#include <sourcemod>
#include "W3SIncs/War3Source_Interface"  

public Plugin:myinfo =
{
name = "War3Source Race - BLAH",
author = "YOUR NAME HERE",
description = "The BLAH race for War3Source.",
version = "1.0",
url = "http://example.com"
};

Declare global variables before OnPluginStart() as we go... ie new Handle:ultCooldownCvar;
Every race needs a thisRaceID, it stores what race number itself is registered to. Since forwards call every plugin, forwards with the race parameter will be checked against this variable and to the appropriate action.
Code: [Select]
new thisRaceID;thisRaceID will be > 0 if registered successfully.

As the awesome sourcepawn programmer you are, you would know every plugin needs to register itself with sourcemod in OnPluginStart...meanwhile do the stuff you need to do in here (dont just copy everything).
Code: [Select]
new Handle:ultCooldownCvar;
public OnPluginStart()
{
//Create race specific Cvars here
ultCooldownCvar=CreateConVar("war3_<shortname>_ult_cooldown","20","Cooldown between <name> (ultimate)");

//Create Repeating timers here
CreateTimer(1.0,SecondTimer,_,TIMER_REPEAT);

//Hook offsets if appropriate
AmmoOffset=FindSendPropOffs("CBasePlayer","m_iAmmo");

//Hook events here!!!!
HookEvent("player_hurt",PlayerHurtEvent);
//War3Source provides some event forwards such as Spawn Death.....
}
« Last Edit: July 12, 2011, 08:46:28 PM by Ownz »

Ownz

  • Developer
  • Administrator
  • Hero Member
  • *****
  • Posts: 2437
  • chmod -R 777 *
    • View Profile
    • OwnageClan
Re: Making a race tutorial. v1.1.2+
« Reply #3 on: July 10, 2010, 11:25:23 AM »
Now we need to register race and skills with War3Source so the big man can track the race and skill levels etc
*The term "skill" is generic. There are: passive skills, abilities (active skills), and ultimates (usually active) all refered to as a "skill" in W3S programming sense.

declare skill variables at the top
Code: [Select]
new SKILL_LEECH,SKILL_SPEED,SKILL_LOWGRAV,SKILL_SUICIDE;then we register our race, and skills. DO NOT USE weird characters in names such as ' and " and `
Code: [Select]
public OnWar3PluginReady(){
thisRaceID=War3_CreateNewRace("Undead Scourge","undead");
SKILL_LEECH=War3_AddRaceSkill(thisRaceID,"Vampiric Aura","You gain health after a successful attack",false,4);
SKILL_SPEED=War3_AddRaceSkill(thisRaceID,"Unholy Aura","You move faster",false,4);
SKILL_LOWGRAV=War3_AddRaceSkill(thisRaceID,"Levitation","You can jump higher",false,4);
SKILL_SUICIDE=War3_AddRaceSkill(thisRaceID,"Suicide Bomber","You explode when you die, can be manually activated",true,4);
War3_CreateRaceEnd(thisRaceID); ///DO NOT FORGET THE END!!!
}
OnWar3PluginReady is called (forward) when war3source is ready to have races registered, it was the old way of register races and had problem of race order on linux machines. there is a new forward called OnWar3LoadRaceOrItemOrdered(num) which passes a number and you register at a particular number. THIS SHOULD BE USED BY DEFAULT PACKAGED RACES ONLY!!
Custom races shall use OnWar3PluginReady() or OnWar3LoadRaceOrItemOrdered2(num) in order to allow default races to be loaded first.

New race makers out there should implement one skill at a time, meaning you register one skill only, implement that skill and fully test it before registering another skill.

Lets talk about these functions parameters:

thisRaceID=War3_CreateNewRace("race name","race short name")
Returns the race id! better store it in your thisRaceID!!!

SKILL_VARIABLE=War3_AddRaceSkill(thisRaceID,"skill name","skill description", is this an ultimate? use true/false,  whats the max level of this skill ? default 4 please!   );
you need to pass your thisRaceID back
This function returns the skill number, better store it in your skill variable!

War3_CreateRaceEnd(thisRaceID); finalizes race creation, writes to the database to create a table for xp storage.

Races are registered every map change, but races that already exists (by shortname) will not be registered again or rewrite to the database and waste some time (this is not an error, its intentional).
However things will go wrong if there are 2 races with the same shortnames!
« Last Edit: July 12, 2011, 08:47:58 PM by Ownz »

Ownz

  • Developer
  • Administrator
  • Hero Member
  • *****
  • Posts: 2437
  • chmod -R 777 *
    • View Profile
    • OwnageClan
Re: Making a race tutorial. v1.1.2+
« Reply #4 on: July 10, 2010, 12:04:25 PM »
After you registered your race and skills, you basically start implementing what they do. Start hooking events or use War3Source functions to modify their properties. HOWEVER, you must only do it if the person has a relation to your race. this is when thisRaceID and War3_GetRace comes in.

Following the example of the Undead race

when the player dies, we make some explosion effects and deal damage to enemies
IF: he is our race, and he has our ultimate skill leveled, he hasnt already suicided since last spawn (exploit check)
Code: [Select]
public OnWar3EventDeath(victim,attacker)
{
if(!bSuicided[victim])
{
new race=War3_GetRace(victim);
new skill=War3_GetSkillLevel(victim,thisRaceID,SKILL_SUICIDE);
if(race==thisRaceID && skill>0)
{
bSuicided[victim]=true;
GetClientAbsOrigin(victim,SuicideLocation[victim]); //this is a global variable we are storing to, so he doesnt bomb in a position different than where he died.
CreateTimer(0.15,DelayedBomber,victim); ///we want a delay in the bombing
}
}
}
War3_GetSkillLevel( client, race, what skill id of this race) returns what level the player is at this skill.
For now we just check if skill level is > 0, to say that he has a level in this skill.

//DelayedBomber expires, it just calls SuicideBomber. DelayedBomber is what i call a "timer helper function" lol.
Code: [Select]
public Action:DelayedBomber(Handle:h,any:client){
if(War3_ValidPlayer(client)){
SuicideBomber(client,War3_GetSkillLevel(client,thisRaceID,SKILL_SUICIDE));
}
}
public SuicideBomber(client,level)
{
     ///bombing effects here...kinda long, see race file
}

When player spawns he should be able to suicide again (if hes our race lol):
Code: [Select]
public OnWar3EventSpawn(client)
{
new race=War3_GetRace(client);
if(race==thisRaceID)
{
InitSkills(client);
}
else{
bSuicided[client]=true; //kludge, not to allow some other race switch to this race and explode on death (ultimate)
}
}

And whats this InitSkills(client)??
It grants passive abilities to the player: speed and low gravity.
You will get a lesson in "buffs and debuffs" later.
Code: [Select]
public InitSkills(client){
if(War3_GetRace(client)==thisRaceID)
{
bSuicided[client]=false;
new skilllevel_unholy=War3_GetSkillLevel(client,thisRaceID,SKILL_SPEED);
if(skilllevel_unholy)
{
new Float:speed=UnholySpeed[skilllevel_unholy];
War3_SetBuff(client,fMaxSpeed,thisRaceID,speed);
}
new skilllevel_levi=War3_GetSkillLevel(client,thisRaceID,SKILL_LOWGRAV);
if(skilllevel_levi)
{
new Float:gravity=LevitationGravity[skilllevel_levi];
War3_SetBuff(client,fLowGravitySkill,thisRaceID,gravity);
}
}
}

We also set his buff on race selection (instead of waiting at next spawn to grant his powers)
But we also have to be aware if he switches to another race, we need to cancel his buffs!
Code: [Select]
public OnRaceSelected(client,newrace)
{
if(newrace!=thisRaceID) ///new race aint ours!!!!! cancel his buffs
{
War3_SetBuff(client,fMaxSpeed,thisRaceID,1.0);
War3_SetBuff(client,fLowGravitySkill,thisRaceID,1.0);
}
else   ///ok then he is our race, grant powers if any
{
if(IsPlayerAlive(client)){
InitSkills(client);
}
}
}

If he leveled up (or leveled down by an admin), we can re-initiate his skills, but with different levels since InitSkills gives buffs based on his current skill level.
Code: [Select]
public OnSkillLevelChanged(client,race,skill,newskilllevel)
{
if(War3_GetRace(client)==thisRaceID) ///why check race again? cuz the "race" parameter is what race's skills changed, not nessarily that the client is on this race. This is kinda redundant cuz InitSkills checks for it again LOL.
{
InitSkills(client);
}
}
We could have combined InitSkills to check if he is this race, and if he is not just remove all buffs.
This is separated just to let you know in which situations you should deal with passive skills, otherwise if not done right the player gets to double stack a race's skill with another race or do some exploits!
« Last Edit: November 09, 2010, 11:13:32 PM by Ownz »

Ownz

  • Developer
  • Administrator
  • Hero Member
  • *****
  • Posts: 2437
  • chmod -R 777 *
    • View Profile
    • OwnageClan
Re: Making a race tutorial. v1.1.2+
« Reply #5 on: July 10, 2010, 12:29:24 PM »
Alright what about passive but event triggered skills? ie LEECH

When the event is triggered,
you check to see if the person involved is our race,
you check to see if he has leveled the skill associated with this event,
and you do what ever you do in the event.

from ORC: critical grenades (dont need to read it all, just get the gist)
Code: [Select]

public PlayerHurtEvent(Handle:event,const String:name[],bool:dontBroadcast)
{
new userid=GetEventInt(event,"userid");
new attacker_userid=GetEventInt(event,"attacker");
new dmg=GetEventInt(event,"dmg_health");
if(War3_GetGame()==Game_TF)
dmg=GetEventInt(event,"damageamount");
if(userid&&attacker_userid&&userid!=attacker_userid)
{
new index=GetClientOfUserId(userid);
new attacker_index=GetClientOfUserId(attacker_userid);
if(index>0&&attacker_index>0)
{
new skill_cg_attacker = War3_GetSkillLevel(attacker_index,race_attacker,SKILL_NADE_INVIS);
if(race_attacker==thisRaceID && skill_cg_attacker>0 && War3_GetGame()!=Game_TF)
{
decl String:weapon[64];
GetEventString(event,"weapon",weapon,63);
if(StrEqual(weapon,"hegrenade",false) && !War3_GetImmunity(index,Immunity_Skills))
{
new Float:percent=CriticalGrenadePercent[skill_cg_attacker];
new health_take=RoundFloat((float(dmg)*percent));
War3_DealDamage(index,health_take,attacker_index,_,"criticalnade");
PrintHintText(attacker_index,"CRITICAL GRENADE! +%d DAMAGE",health_take);
War3_FlashScreen(index,RGBA_COLOR_RED);
}
}
}
}
}
War3_DealDamage is the mother of all damage function, it allows you to deal damage to a player easily with many optional parameters. (see War3Source_Interface)
Usually the first 4 parameters is enough for generic damages, but the extra parameters make this function very powerful.

The damage you passed to War3_DealDamage wont always be the actual damage dealt. CS has armor, and warcraft has their own magic and physical armor.  Use War3_GetWar3DamageDealt() to get the the actual amount of damage dealt. You will use this when you are printing damage messages.


« Last Edit: July 28, 2010, 02:59:53 PM by Ownz »

Ownz

  • Developer
  • Administrator
  • Hero Member
  • *****
  • Posts: 2437
  • chmod -R 777 *
    • View Profile
    • OwnageClan
Re: Making a race tutorial. v1.1.2+
« Reply #6 on: July 28, 2010, 03:06:17 PM »
Active skills (abilities and ultimates)

There are forwards when the player presses the ability or ultimate button. You usually check to see if the player is valid, if they are your race, and see if they have the skill that you are interested in activating.

You dont always have to check the "ability" argument, its for if you have multiple abilities, the one would be activated via +ability (number==0) and the second one activated by +ability1 (number==1)
Code: [Select]
public OnAbilityCommand(client,number,bool:pressed)
{
if(War3_GetRace(client)==thisRaceID && number==0 && pressed && IsPlayerAlive(client))
{
new skill_level=War3_GetSkillLevel(client,thisRaceID,SKILL_VARIABLE);
if(skill_level>0)
{
//do your stuff here, like check cooldown, find targets, create cooldown...
}
}
}
Same concept with ultimates. OnUltimateCommand passes a "race" variable (this is very old but we didnt bother changing it). You can for now use it to compare it to your own thisRaceID.
Code: [Select]
public OnUltimateCommand(client,race,bool:pressed)
{
if(race==thisRaceID && pressed && IsPlayerAlive(client))
{
new skill=War3_GetSkillLevel(client,race,ULT_VARIABLE);
if(skill>0)
{
//you know the drill
}
}
}
« Last Edit: November 09, 2010, 11:14:11 PM by Ownz »

Ownz

  • Developer
  • Administrator
  • Hero Member
  • *****
  • Posts: 2437
  • chmod -R 777 *
    • View Profile
    • OwnageClan
Re: Making a race tutorial. v1.1.2+
« Reply #7 on: July 28, 2010, 03:10:23 PM »
The cooldown manager!

Every race used to implement their own cooldowns, lame timers, their own prints statements, which was quite cumbersome

We implemented a cooldown manager that allows us to simplify everything.

Each race times each skill is allowed to be managed by the cooldown manager. the race id and skill num is used to specifically identify a cooldown. See examples and you should understand.

Lets see the API
native War3_CooldownMGR(client,Float:cooldownTime,raceid,skillNum,bool:resetOnSpawn=true,
     bool:resetOnDeath=true,bool:printMsgOnExpireByTime=true,String:CooldownPrintMsgName[]="");  
Mostly self explainatory. You create a cooldown, with many options such as the duration, does it expire on spawn and death, should it print to the client if it expires via timer, and pass the name of this skill so it prints the name instead of generic "Ability Ready".
If you call it on the same raceid and skill number twice, the last one will overwrite it.

native bool:War3_SkillNotInCooldown(client,raceid,skillNum,bool:printTextIfNotReady=false);  
Your race can just call this function to see if that skill is in cooldown or not, optionally print to the client a "not ready" message.

The following are not used very much
native War3_CooldownRemaining(client,raceid,skillNum);  
What if i really wanted to know how much time is remaining on this cooldown? Call this.

native War3_CooldownReset(client,raceid,skillNum);  
Forcibly reset the cooldown (take it out of cooldown). its the same thing as overwriting it as a cooldown that expires in 0 seconds. War3_CooldownMGR(client,0.0,....);

native War3_PrintSkillIsNotReady(client,raceid,skillNum);
Well yea it prints a "skill not ready" message for that cooldown. War3_SkillNotInCooldown already has an option to do this for you so War3_PrintSkillIsNotReady is basically defunct.

« Last Edit: August 19, 2010, 06:46:21 PM by Ownz »

Ownz

  • Developer
  • Administrator
  • Hero Member
  • *****
  • Posts: 2437
  • chmod -R 777 *
    • View Profile
    • OwnageClan
Re: Making a race tutorial. v1.1.2+
« Reply #8 on: July 29, 2010, 10:01:15 AM »
The Buff System (you must be joking if you don't know what a buff is)

War3Source has a buff system (and debuff but its the same system) which removes many conflicts between races. Like one race wants to set the gravity to X and another wants to set the gravity to Y, and the two races don't know each other. To solve this problem we have all the races that want to set gravity pass on their intended value to War3Source buff system and the buff system has the final say on what gravity the player gets.

The buff system also helps in denying buffs (debuffs). If one races disables low gravity for other races, that that race can set a debuff on the client in the buff system, instead of telling every other race to cancel their gravity.

All the available buffs are listed in the constants.inc
sample here:
Code: [Select]
enum {
dummy=0,
bBuffDenyAll, //DENY=not allowed to have any buffs, aka "purge"

fLowGravitySkill, //0.4 ish?
fLowGravityItem, //0.4 ish?
bLowGravityDenyAll,

fInvisibilitySkill, //0.4 ish?
fInvisibilityItem, //0.4 ish?
bInvisibilityDenyAll,
bInvisibilityDenySkill,

fMaxSpeed, //for increasing speeds only! MUST BE MORE THAN 1.0
fSlow, //for decreeasing speeds only! MUST BE LESS THAN 1.0
fSlow2, //
bReapplySpeed, //bool

.....and so on
Each race only has ONE say about EACH buff, so if you have two skills that modifies the same buff, you need to keep track of the stacking yourself! We made fSlow and fSlow2 specifically to allow your race to have stacking slows, but this wont always be the case.
The last buff value set for that property overwrites the previous value for your own race. So if u set client gravity to 0.4 then 0.6, 0.6 is the final value registered on the buff system.

The syntax:
Code: [Select]
native War3_SetBuff(client,buffindex,raceid,any:value);
set his low gravity
Code: [Select]
War3_SetBuff(client,fLowGravitySkill,thisRaceID,0.6);
remove his low gravity
Code: [Select]
War3_SetBuff(client,fLowGravitySkill,thisRaceID,1.0);
force someone to lose their gravity buffs (debuff)
Code: [Select]
War3_SetBuff(client,bLowGravityDenyAll,thisRaceID,true);
See races for more examples!
« Last Edit: August 19, 2010, 06:49:33 PM by Ownz »