Start with the MapInfo
I was the most confused by this. Mapinfo is not info for each specific level. It's pretty much general properties for your wad. The mod, map, etc. that you are working on is going to get all of its entrypoints from here, as well as some additional data on the game in general.
For HRPG I used the mapinfo.txt to reference a brand new file, similar to how it is done in the gzdoom pk3.
Code: Select all
include "mapinfo/hrpg.txt"
For the actual changes to the hrpg.txtfile, there are a few sections I'll highlight, but will not copy the whole thing here.
For my player class to have rpg elements, I've made a few player classes. Because there are more than 1, a class selection menu automatically opens like in Hexen. All of these classes inherit from HRpgPlayer.
Code: Select all
playerclasses = "HRpgHereticPlayer", "HRpgHeathenPlayer", "HRpgBlasphemerPlayer"
Code: Select all
statusbarclass = "HRpgStatusBar"
This is where HRPG makes each monster give Experience. Any thing that already spawns into a map in ZScript can be "replaced" by using the replace keyword. What this does is spawn your new version instead of the old version when the map loads. This trick gives you an entry point for overriding existing things. In this case, monsters.
The first thing I did was create an ExpSquishBag base class for monsters to inherit from. This means that it can override a few important virtual methods and any monster based on this base class will work like an experience giving bag of squishiness.
Here we will override the Take Special Damage method. I use the actual damage dealt because it helps with coop games. Everyone who scores a hit gets instant HP. It really beats all the lame logic to award coop XP, and really cool games like Amulets and Armor do it this way.
Code: Select all
const MAXXPHIT = 75;
//...
override int TakeSpecialDamage(Actor inflictor, Actor source, int damage, Name damagetype)
{
int xp = damage;
if (xp > MAXXPHIT)
xp = MAXXPHIT;
let hrpgPlayer = HRpgPlayer(source); //check if the source of the damage derives from HRpgPlayer. This means any of the classes that inherit from it can get XP.
if (hrpgPlayer)
{
hrpgPlayer.GiveXP(xp); //call the methods on the player directly
//...
}
return Super.TakeSpecialDamage(inflictor, source, damage, damagetype);
}
Code: Select all
class HRpgMummy : ExpSquishbag replaces Mummy
{
Default
{
Health 80;
Radius 22;
Height 62;
Mass 75;
Speed 12;
Painchance 128;
Monster;
//...
I do this for all of the monster classes except dsparil. There is no need to gain levels in the very last boss fight.
Note: This method requires that we replace every monster to get this bonus. It is not easily compatible with existing map packs and plugins. So if you want to try a different approach, then that may be better for your mod. One common suggestion is to add an inventory item to the player, and another is the do special damage override on the player object or the weapons, instead of the monster.
Create the Player Classes
I have HRpgPlayer and 3 player classes that inherit from it. You don't have to have the extra player classes. You could just make HRpgPlayer your only playable character and it would still work.
Code: Select all
const XPMULTI = 1000;
const HEALTHBASE = 100;
const STATNUM = 4;
class HRpgPlayer : HereticPlayer //This does not have to "replace" because it doesn't spawn in the map. We use the mapinfo to set the class
{
//Create the leveling variables
int expLevel;
int exp;
int expNext;
//Statistics
int brt; //Brutality (strength)
int trk; //Trickerty (Dexterity)
int crp; //Corruption (Intelligence)
//Expose them as properties
property ExpLevel : expLevel;
property Exp : exp;
property ExpNext : expNext;
property Brt : brt;
property Trk : trk;
property Crp : crp;
Default
{
//Defaults
HRpgPlayer.ExpLevel 1;
HRpgPlayer.Exp 0;
HRpgPlayer.ExpNext XPMULTI;
Player.MaxHealth HEALTHBASE;
Health HEALTHBASE;
//...
}
States
{
//...
}
//Methods for calculating statistics bonuses to attacks
double GetScaledMod(int stat)
//...
int GetModDamage(int damage, int stat, int scaled)
//...
int GetDamageForMelee(int damage)
//...
int GetDamageForWeapon(int damage)
//...
int GetDamageForMagic(int damage)
//...
void SetProjectileDamage(Actor proj, int stat)
//...
void SetProjectileDamageForMelee(Actor proj)
//...
void SetProjectileDamageForWeapon(Actor proj)
//...
void SetProjectileDamageForMagic(Actor proj)
//...
int CalcXPNeeded() //XP needed is current level times 1000
{
return ExpLevel * XPMULTI;
}
void GiveXP (int expEarned) //Method used to add XP to player. Called form monster's take special damage
{
Exp += expEarned;
while (Exp >= ExpNext) //Handles multiple level gains if massive XP is added
{
GainLevel();
}
}
//Leveling
void DoLevelGainBlend()//Make screen flash
{
let blendColor = Color(122, 122, 122, 122);
A_SetBlend(blendColor, 0.8, 40);
string lvlMsg = String.Format("You are now level %d", ExpLevel);
A_Print(lvlMsg);
}
//Gain a level
void GainLevel()
{
if (Exp < ExpNext)
return;
ExpLevel++;
Exp = Exp - ExpNext;
ExpNext = CalcXpNeeded();
//Distribute points randomly, giving weight to highest stats
int statPoints = STATNUM;
while (statPoints > 0)
{
int statStack = Brt + Trk + Crp;
double rand = random(1, statStack);
if (rand <= Brt)
{
Brt += 1;
}
else if (rand <= Brt + Trk)
{
Trk += 1;
}
else
{
Crp += 1;
}
statPoints--;
}
//BasicStatIncrease to call overrides in classes
BasicStatIncrease();
//health increases by random up to Brutality, min 5 (weighted for low end of flat scale)
int healthBonus = random(1, Brt);
if (healthBonus < 5)
healthbonus = 5;
int newHealth = MaxHealth + healthBonus;
MaxHealth = newHealth;
if (Health < MaxHealth)
A_SetHealth(MaxHealth);
DoLevelGainBlend();
}
}
Display that new stuff
Adding the new status bar HrpgStatusBar is pretty complicated, but mine is a copy of Heretics with minor changes.
Code: Select all
HUDFont mSmallFont;
//...
override void Init()
{
//...
fnt = "SMALLFONT";
mSmallFont = HUDFont.Create(fnt, fnt.GetCharWidth("0"), Mono_CellLeft);
//...
}
//...
override void Draw (int state, double TicFrac)
{
Super.Draw (state, TicFrac);
if (state == HUD_StatusBar)
{
BeginStatusBar();
DrawMainBar (TicFrac);
DrawExpStuff(0);
}
else if (state == HUD_Fullscreen)
{
BeginHUD();
DrawFullScreenStuff ();
DrawExpStuff(1);
}
}
//...
protected void DrawExpStuff (int isFullscreen)
{
let xPos = 0;
let yPos = 126;
let yStep = 8;
let xPosStats = 320;
if (isFullscreen)
{
xPos = 8;
yPos = 346;
xPosStats = 740;
}
let hrpgPlayer = HRpgPlayer(CPlayer.mo);
if (!hrpgPlayer)
return;
let text1 = String.Format("Level: %s", FormatNumber(hrpgPlayer.ExpLevel, 0));
let text2 = String.Format("XP: %s / %s", FormatNumber(hrpgPlayer.Exp, 0), FormatNumber(hrpgPlayer.ExpNext, 0));
//Exp
DrawString(mSmallFont, text1, (xPos, yPos), DI_TEXT_ALIGN_LEFT);
DrawString(mSmallFont, text2, (xPos, yPos + yStep), DI_TEXT_ALIGN_LEFT);
let statText1 = String.Format("Brutality: %s", FormatNumber(hrpgPlayer.Brt, 0));
let statText2 = String.Format("Trickery: %s", FormatNumber(hrpgPlayer.Trk, 0));
let statText3 = String.Format("Corruption: %s", FormatNumber(hrpgPlayer.Crp, 0));
//Stats
DrawString(mSmallFont, statText1, (xPosStats, yPos - yStep), DI_TEXT_ALIGN_RIGHT);
DrawString(mSmallFont, statText2, (xPosStats, yPos), DI_TEXT_ALIGN_RIGHT);
DrawString(mSmallFont, statText3, (xPosStats, yPos + yStep), DI_TEXT_ALIGN_RIGHT);
let bPlayer = HRpgBlasphemerPlayer(CPlayer.mo);
if (bPlayer && isFullscreen)
{
let text3 = String.Format("Mana: %s / %s", FormatNumber(bPlayer.Mana / MANA_SCALE_MOD, 0), FormatNumber(bPlayer.MaxMana / MANA_SCALE_MOD, 0));
DrawString(mSmallFont, text3, (xPos, yPos - yStep), DI_TEXT_ALIGN_LEFT);
}
}
And that's pretty much it for code changes. The next part is referencing those code files.
Referencing your new files
Now that you have all these new classes, ZScript has to know that they exist. That is done by adding them to the zscript.zs file.
Code: Select all
version "4.3.0"
#include "hrpgscripts/HRpgPlayer.zs"
//...
#include "hrpgscripts/HRpgStatusBar.zs"
//...
#include "hrpgscripts/ExpSquishbag.zs"
//...
#include "hrpgscripts/HRpgMummy.zs"