FlashPunk Tutorial 07: Character Stats and Screen

Posted by dolgion on Friday Feb 11, 2011 Under Flashpunk
BOO! Hah, thought I’d never write another tutorial huh? Yeah, to be honest, for a while I thought so too. Taking a “break” of 2 months from a fairly big project like this can be a problem, because you have to get to know your code again. It’s like meeting an old aquaintance again, and it’s awkward at the beginning because you lost your comfortable familiarity for that aquaintance/source code. Even now, I’ve not completely re-figured out all the subsystems in the code, but that won’t be a problem for this tutorial.
As always, you can download the full source code with assets here, and also just try to play the game here.

The changes that have gone into the master branch since Tutorial 06 about Multiple Choice Dialog are numerous. We now have character stats such as experience points, strength, agility, spirituality stats and health and mana points. Also, there is a character screen that shows us these stats, along with a nice portrait of our hero. There are other additions which I won’t go into right now, but later. There are items, an inventory and chests which contain items that the player can take. So you see? It’s all getting more RPG-like, finally.


1. Player stats

So, of course the rule systems which are essential to RPGs can vary greatly from game to game. There are so many different ways to calculate damage in combat, to symbolize properties of heroes and monsters such as their strength and so on, depending on what kind of RPG one makes and what kind of gameplay experience is wanted. For JRPGs, complex rule systems and mechanics can be a central part of the game’s challenge and enjoyment. Tweaking your party to be at its most efficient against a certain boss can be a game in itself and often is incredibly rewarding.

I remember when playing FF7 as a kid, I was stuck at one boss for 2 days. I hadn’t given much thought about materia allocation and strategy. But with that boss, I was forced to take a step back and actually give a shit about the stuff going on behind the scenes, because my party’s set up directly hampered my chances in battle. So after a little bit of figuring out and thinking about a general tactic, I was able to beat the boss easily.

Anyways, my point is that even though I’m introducing a rule system here, it really is just a placeholder. You will most certainly decide that my rule system sucks and decide to implement your own, awesomer  one, and that is really the point of the whole thing. I’m writing this series so that you can have an easier time understanding the code so that you can take it apart and improve and customize it for your own game.
To be honest, I’ve never actually designed an RPG rule system, but I’ve played a lot of RPGs over the years, so I’m fairly comfortable with the concept. I’m gonna design a very, very simple rule system with very few variables that come into damage calculation and so on.
Our hero has 3 main stats: strength, agility and spirituality.
  • Strength is going to be a factor when calculating damage dealt by melee weapons. Also, some weapons and armor will only be equippable, if the player has a minimum amount of strength for the item. Strength factors into the amount of health point gains at level up.
  • Agility is a factor when evading melee and ranged non-magical attacks. Also, when attacking with a non-magical weapon, agility influences the accuracy of the strike. So you see, if the player’s agility is larger than the target’s agility, the player has a greater chance at a successful hit than the target has at evading it. Agility is also the measure to determine the amount of turns one gets, as a character with higher agility than another can perform more actions over the course of battle.
  • Spirituality influences the damage dealt by magical items, such as wands or staves. A minimum amount of spirituality is necessary to use some of these items, too. Spirituality also influences the amount of mana one gains with each level. Mana is then used to cast spells of course.
So this is the general idea for these 3 basic stats for now. They may be subject to change, as I haven’t set anything in stone. Then, we have as mentioned health and mana points. Their meaning should be obvious. Experience points will be gained as in most other RPGs, by defeating enemies in combat and by completing quests.
Our Player class is going to have these variables to represent those stats:

// character status variables
public var health:int;
public var maxHealth:int;
public var mana:int;
public var maxMana:int;

// character stats
public var experience:int;
public var strength:int; // determines what non-magical weapon and armor can be used
public var agility:int; // determines chance to hit and evade attacks
public var spirituality:int; // determines effectiveness of magical weapons and what spells can be used
Now, we don’t want to have the values for these variables hardcoded right there in the class, right? Let’s make it so that the values can be changed by editing the player_data.xml. I’ve added the necessary tags:
<player>
        ...
	<health>20</health>
	<maxHealth>100</maxHealth>
	<mana>50</mana>
	<maxMana>100</maxMana>
	<strength>10</strength>
	<agility>11</agility>
	<spirituality>12</spirituality>
	<experience>0</experience>
        ...
</player>
The content of the tags are then read in the DataLoader class we have created before. We modify the setupPlayer() function to also read the new tags:

public function setupPlayer():Player
{
	...
	var stats:Array = new Array();
	...
	stats[GC.STATUS_HEALTH] = playerDataXML.player.health;
	stats[GC.STATUS_MAX_HEALTH] = playerDataXML.player.maxHealth;
	stats[GC.STATUS_MANA] = playerDataXML.player.mana;
	stats[GC.STATUS_MAX_MANA] = playerDataXML.player.maxMana;
	stats[GC.STATUS_STRENGTH] = playerDataXML.player.strength;
	stats[GC.STATUS_AGILITY] = playerDataXML.player.agility;
	stats[GC.STATUS_SPIRITUALITY] = playerDataXML.player.spirituality;
	stats[GC.STATUS_EXPERIENCE] = playerDataXML.player.experience;

	return new Player(new GlobalPosition(playerDataXML.player.mapIndex, playerDataXML.player.x, playerDataXML.player.y), playerDialogs, stats);
}
You can see that the values are stored in the new stats[] array. The indices for the elements are referred using some constants from the new GC class. I’ve gone through several refactor iterations and removed all static constants from all classes and put the into the GC class. This way, there is more clarity as to where a constant can be found, and I’ve even managed to get rid of some duplication (sometimes I would use a bunch of constants to basically mean the same thing another bunch of constants were used for in another class).

You can check out the GC class. It’s only gonna get bigger and bigger the more stuff I put in the game :P . The stats array is then passed into the constructor of our player. In the constructor, the stats array values are read and the character stat variables given their initial values, delivered all the way from our xml. :D

2. The Status Screen
So…we now have some character stats, yay. Let’s go and have celebratory beers!…..No wait, we want to see these stats displayed in a character status screen in-game. Therefore, we’ll create the new class StatusScreen.as. This class is basically a whole bunch of DisplayText instances put together in a room with a bunch of bullies to command them around. The bullies are the functions in the class and control what the DisplayText instances actually display. They read relevant data in and so forth.
package utility
{
	import entities.DisplayText;
	import entities.PlayerPortrait;
	import entities.TextBox;
	import net.flashpunk.Entity;

	/**
	 * ...
	 * @author dolgion
	 */
	public class StatusScreen
	{
		public var background:TextBox;
		public var portrait:PlayerPortrait;
		public var displayTexts:Array = new Array();
		public var experienceDisplay:DisplayText;
		public var strengthDisplay:DisplayText;
		public var agilityDisplay:DisplayText;
		public var spiritualityDisplay:DisplayText;
		public var damageRatingDisplay:DisplayText;
		public var damageTypeDisplay:DisplayText;
		public var attackTypeDisplay:DisplayText;
		public var armorRatingDisplay:DisplayText;
		public var healthDisplay:DisplayText;
		public var manaDisplay:DisplayText;

		private var visibility:Boolean = false;

		public function StatusScreen()
		{
			background = new TextBox(10, 10, 3, 4.5);
			portrait = new PlayerPortrait(50, 50);

			experienceDisplay = new DisplayText(GC.EXPERIENCE_STRING + ": ", 245, 40, "default", GC.STATUS_SCREEN_DEFAULT_FONT_SIZE, 0xFFFFFF, 500);
			strengthDisplay = new DisplayText(GC.STRENGTH_STRING + ": ", 245, 55, "default", GC.STATUS_SCREEN_DEFAULT_FONT_SIZE, 0xFFFFFF, 500);
			agilityDisplay = new DisplayText(GC.AGILITY_STRING + ": ", 245, 70, "default", GC.STATUS_SCREEN_DEFAULT_FONT_SIZE, 0xFFFFFF, 500);
			spiritualityDisplay = new DisplayText(GC.SPIRITUALITY_STRING + ": ", 245, 85, "default", GC.STATUS_SCREEN_DEFAULT_FONT_SIZE, 0xFFFFFF, 500);
			damageRatingDisplay = new DisplayText(GC.DAMAGE_STRING + ": ", 245, 110, "default", GC.STATUS_SCREEN_DEFAULT_FONT_SIZE, 0xFFFFFF, 500);
			damageTypeDisplay = new DisplayText(GC.DAMAGE_TYPE_STRING + ": ", 245, 125, "default", GC.STATUS_SCREEN_DEFAULT_FONT_SIZE, 0xFFFFFF, 500);
			attackTypeDisplay = new DisplayText(GC.ATTACK_TYPE_STRING + ": ", 245, 140, "default", GC.STATUS_SCREEN_DEFAULT_FONT_SIZE, 0xFFFFFF, 500);
			armorRatingDisplay = new DisplayText(GC.ARMOR_STRING + ": ", 245, 155, "default", GC.STATUS_SCREEN_DEFAULT_FONT_SIZE, 0xFFFFFF, 500);
			healthDisplay = new DisplayText(GC.HEALTH_STRING + ": ", 55, 170, "default", GC.STATUS_SCREEN_DEFAULT_FONT_SIZE, 0xFFFFFF, 500);
			manaDisplay = new DisplayText(GC.MANA_STRING + ": ", 55, 185, "default", GC.STATUS_SCREEN_DEFAULT_FONT_SIZE, 0xFFFFFF, 500);

			displayTexts.push(experienceDisplay);
			displayTexts.push(strengthDisplay);
			displayTexts.push(agilityDisplay);
			displayTexts.push(spiritualityDisplay);
			displayTexts.push(damageRatingDisplay);
			displayTexts.push(damageTypeDisplay);
			displayTexts.push(attackTypeDisplay);
			displayTexts.push(armorRatingDisplay);
			displayTexts.push(healthDisplay);
			displayTexts.push(manaDisplay);
		}

		public function set stats(_stats:Array):void
		{
			healthDisplay.displayText.text = GC.HEALTH_STRING + ": " + _stats[GC.STATUS_HEALTH] + "/" + _stats[GC.STATUS_MAX_HEALTH];
			manaDisplay.displayText.text = GC.MANA_STRING + ": " + _stats[GC.STATUS_MANA] + "/" + _stats[GC.STATUS_MAX_MANA];
			strengthDisplay.displayText.text = GC.STRENGTH_STRING + ": " + _stats[GC.STATUS_STRENGTH];
			agilityDisplay.displayText.text = GC.AGILITY_STRING + ": " + _stats[GC.STATUS_AGILITY];
			spiritualityDisplay.displayText.text = GC.SPIRITUALITY_STRING + ": " + _stats[GC.STATUS_SPIRITUALITY];
			experienceDisplay.displayText.text = GC.EXPERIENCE_STRING + ": " + _stats[GC.STATUS_EXPERIENCE];

			damageRatingDisplay.displayText.text = GC.DAMAGE_STRING + ": " + _stats[GC.STATUS_DAMAGE];
			damageTypeDisplay.displayText.text = GC.DAMAGE_TYPE_STRING + ": " + Weapon.getDamageType(_stats[GC.STATUS_DAMAGE_TYPE]);
			attackTypeDisplay.displayText.text = GC.ATTACK_TYPE_STRING + ": " + Weapon.getAttackType(_stats[GC.STATUS_ATTACK_TYPE]);
			armorRatingDisplay.displayText.text = GC.ARMOR_STRING + ": " + _stats[GC.STATUS_ARMOR];
		}

		public function get visible():Boolean
		{
			return visibility;
		}

		public function set visible(_visible:Boolean):void
		{
			visibility = _visible;
			background.visible = _visible;
			portrait.visible = _visible;
			for each (var d:DisplayText in displayTexts)
			{
				d.visible = _visible;
			}
		}
	}

}
Where to start…well, first of all, we have a whole lot of DisplayTexts. They are there to become the text shown in the status screen at the end of the day. For example, strengthDisplay:DisplayText will show something like “Strength: 15″. We also have the same background as in the dialog boxes from the previous tutorial, only scaled with different multipliers. A portrait, which is simply an entity with a portrait graphic of our hero is also added. Then, we have a displayTexts[] array, which will contain all of the DisplayText instances. The reason will become apparent soon. You see, in the Game class, we want to open the character status screen upon pressing the “c” key. If you look at the defineInputKeys() function in the Game class, you can see the allocation of the “c” key with this brand new usage of our GC constants. Basically we map the “c” key to the string stored as BUTTON_STATUS_SCREEN in the GC class.
public function defineInputKeys():void
{
	Input.define(GC.BUTTON_UP, Key.W, Key.UP);
	Input.define(GC.BUTTON_DOWN, Key.S, Key.DOWN);
	Input.define(GC.BUTTON_LEFT, Key.A, Key.LEFT);
	Input.define(GC.BUTTON_RIGHT, Key.D, Key.RIGHT);
	Input.define(GC.BUTTON_MAP, Key.M);
	Input.define(GC.BUTTON_CANCEL, Key.X);
	Input.define(GC.BUTTON_EXIT, Key.ESCAPE);
	Input.define(GC.BUTTON_ACTION, Key.SPACE);
	Input.define(GC.BUTTON_STATUS_SCREEN, Key.C);
	Input.define(GC.BUTTON_INVENTORY_SCREEN, Key.I);
}
Then, in the processGeneralInput() function, we check for the status screen button press if the game is in NORMAL_MODE. If that is indeed the case, we call the new openStatusScreen() function.
public function processGeneralInput():void
{
	...
	else if (gameMode == NORMAL_MODE)
	{
		// Check for input that opens or closes the world map
		if (Input.pressed(GC.BUTTON_MAP))
		{
			openWorldMap();
		}
		else if (Input.pressed(GC.BUTTON_STATUS_SCREEN))
		{
			openStatusScreen();
		}
		else if (Input.pressed(GC.BUTTON_INVENTORY_SCREEN))
		{
			openInventoryScreen();
		}
		...
	}
}
public function openStatusScreen():void
{
	gameMode = STATUS_SCREEN_MODE;
	statusScreen.visible = true;
	statusScreen.stats = player.stats;
}
Here we can see that the game mode is set to STATUS_SCREEN_MODE which amongst other things stops the game from continuing. NPCs will stop in their tracks, the game time will stand still etc. Also, we call the visible() function of our status screen instance. Wait…this looks like we’re just setting a member variable called visible…or are we? visible is a function – actually it’s two functions – of our status screen, but of the getter and setter kind. This means when we say statusScreen.visible = true, what we’re actually doing is calling a function called visible with the parameter of type boolean. The setter function looks like this:
public function set visible(_visible:Boolean):void
{
	visibility = _visible;
	background.visible = _visible;
	portrait.visible = _visible;
	for each (var d:DisplayText in displayTexts)
	{
		d.visible = _visible;
	}
}
We set the flag called visibility to the boolean passed by parameter and basically set the visibility of all the display texts of our status_screen to that boolean, effectively making our entire status screen either invisible, or visible. The getter function for visible simply returns the visibility flag. You can see in the Game class that when we add the StatusScreen instance to the stage, we also call the visible setter, but setting it to false, because we don’t want to be in status screen mode at the beginning of the game. We also add the DisplayTexts to the stage.
public function loadMap():void
{
        ...
	// status screen creation
	statusScreen = new StatusScreen();
	statusScreen.visible = false;
	add(statusScreen.background);
	add(statusScreen.portrait);
	addList(statusScreen.displayTexts);
	...
}
So when we press the “c” key in normal mode, we set the DisplayTexts of our StatusScreen instance to true, effectively showing the DisplayTexts. The next thing needed is to actually tell the status screen what the stats are that must be displayed. In the openStatusScreen() function, we call another setter, the stats setter. You know the drill. The value being assigned to the setter is passed as a parameter. Look at the setter code in the StatusScreen instance:
public function set stats(_stats:Array):void
{
	healthDisplay.displayText.text = GC.HEALTH_STRING + ": " + _stats[GC.STATUS_HEALTH] + "/" + _stats[GC.STATUS_MAX_HEALTH];
	manaDisplay.displayText.text = GC.MANA_STRING + ": " + _stats[GC.STATUS_MANA] + "/" + _stats[GC.STATUS_MAX_MANA];
	strengthDisplay.displayText.text = GC.STRENGTH_STRING + ": " + _stats[GC.STATUS_STRENGTH];
	agilityDisplay.displayText.text = GC.AGILITY_STRING + ": " + _stats[GC.STATUS_AGILITY];
	spiritualityDisplay.displayText.text = GC.SPIRITUALITY_STRING + ": " + _stats[GC.STATUS_SPIRITUALITY];
	experienceDisplay.displayText.text = GC.EXPERIENCE_STRING + ": " + _stats[GC.STATUS_EXPERIENCE];

	damageRatingDisplay.displayText.text = GC.DAMAGE_STRING + ": " + _stats[GC.STATUS_DAMAGE];
	damageTypeDisplay.displayText.text = GC.DAMAGE_TYPE_STRING + ": " + Weapon.getDamageType(_stats[GC.STATUS_DAMAGE_TYPE]);
	attackTypeDisplay.displayText.text = GC.ATTACK_TYPE_STRING + ": " + Weapon.getAttackType(_stats[GC.STATUS_ATTACK_TYPE]);
	armorRatingDisplay.displayText.text = GC.ARMOR_STRING + ": " + _stats[GC.STATUS_ARMOR];
}
All we do here is read the stats given to us in the array and construct the text properties of our display texts accordingly. Once again, we use the constans in our GC class to access the strings so that we don’t have to hard code them here. It might look a bit overcomplicated, but that is down to the long-ass names I gave to the constants lol. And that is pretty much it for the status screen. Next up will be game items and then the inventory screen. These features are already in the engine, so you can check them out already! I’ll be writing the tutorials for them soon.
Share Share

3 Responses to “FlashPunk Tutorial 07: Character Stats and Screen”

  1. marcin Says:

    Thanks for this. I was really perplexed as to how to display variable text on the screen. I was originally looking for a quick way to throw up a coordinate grid – I’m debugging my pathfinding and got tired of counting tiles – but with this, I just went ahead and included it properly as a way to do stats and inventory as well!

    So yeah, massive thanks for demystifying Text!

  2. dolgion Says:

    Glad that it helped you out!

  3. Kimimondo Says:

    How about a sim date like game? would this work there? if not know a good link to a proper tut?

Leave a Reply