Using Items (Perl)

Our game is still missing something really important: items that users can actually use!

Today, we’ll be writing the code that allows our users to use the items in their inventory. To start off, check out a copy of the code for our project:

svn checkout http://building-browsergames-tutorial.googlecode.com/svn/trunk/perl/pbbg tutorial -r 38
And then we’re ready to go!

To start off, we’ll build a basic template for our inventory page, and put it inside inventory.tmpl:

 

<html>
<head>
	<title>Your Inventory</title>
</head>
<body>
	<p>This is your inventory. Check out all the stuff you've got!</p>
	<tmpl_if name='message'>
		<p><!--tmpl_var name='message'--></p>
	</tmpl_if>
	<ul>
		<tmpl_loop name='inventory'>
		<li>
			<strong><!--tmpl_var name='name'--> x <!--tmpl_var name='quantity'--></strong>
			<form action='inventory.cgi' method='post'>
				<input type='hidden' name='item-id' value='<!--tmpl_var name="id"-->' />
				<input type='submit' value='Use' />
			</form>
		</li>
		</tmpl_loop>
	</ul>
</body>
</html>

With our template built, we can start writing the code inside inventory.cgi:

 

#!/usr/bin/perl -w
 
use strict;
use CGI qw(:cgi);
use CGI::Carp qw(fatalsToBrowser warningsToBrowser);
use HTML::Template;
use DBI;
use config;
 
my $query = new CGI;
my %arguments = $query->Vars;
 
my $dbh = DBI->connect("DBI:mysql:$config{dbName}:$config{dbHost}",$config{dbUser},$config{dbPass},{RaiseError => 1});
my $sth;
my %parameters;
 
use stats;
 
my $cookie = $query->cookie('username+password');
my ($username) = split(/\+/,$cookie);
$sth = $dbh->prepare("SELECT id FROM users WHERE UPPER(username) = UPPER(?)");
$sth->execute($username);
my $userID;
$sth->bind_columns(\$userID);
$sth->fetch;
 
$sth = $dbh->prepare("SELECT item_id, quantity FROM user_items WHERE user_id = ?");
$sth->execute($userID);
my @inventory = ();
while(my $row = $sth->fetchrow_hashref) {
	my $sth2 = $dbh->prepare("SELECT name FROM items WHERE id = ?");
	$sth2->execute($row->{item_id});
	$sth2->bind_columns(\$row->{name});
	$sth2->fetch;
	push @inventory, $row;
}
 
$parameters{inventory} = \@inventory;
 
my $template = HTML::Template->new(
		filename	=>	'inventory.tmpl',
		associate	=>	$query,
		die_on_bad_params => 0,
	);
$template->param(%parameters);
print $query->header(), $template->output;

This code is almost entirely from our item shop code – so there isn’t really anything you haven’t seen before. The one new piece is the die_on_bad_params parameter that we are passing to HTML::Template->new() – we do this so that it won’t complain about the fact that our @inventory variable has the value ‘item_id’ inside of it, but the template doesn’t.

Our code is still missing something, however: the ability to actually use items!

If you remember the initial database preparations that we did earlier, you’ll remember that we created a new stat called ‘Item Use Token’ – and now we’ll be putting that stat to use. We will be using the ‘Item Use Token’ stat in conjunction with what’s known as a Dispatch Table to easily write the code that will run when our items are used.

For now, we’ll insert a single item with the token ‘potion’:

INSERT INTO items(name,type,price) VALUES (‘Red Potion’,’Usable’,10);
INSERT INTO entity_stats(stat_id,entity_id,value,entity_type) VALUES ((SELECT id FROM stats WHERE short_name=’token’),(SELECT id FROM items WHERE name=’Red Potion’),’potion’,’Item’);
With that stat set up, we can buckle down and start on our dispatch table.

Dispatch tables are one of the more useful tools available to you as a programmer; they allow you to create a hash of functions(instead of values), and then call functions by key. This makes them perfect for our items system, because using dispatch tables will allow us to easily add new code for items in the most efficient manner possible. Let’s write some code to define and interact with a dispatch table:

 

my %actions = (
	'potion' => \&use_potion,
);
 
sub use_potion {
	$parameters{message} = 'This is code that would run if the user used a potion.';
}
 
if(%arguments) {
	if($arguments{'item-id'}) {
		use items;
		$sth = $dbh->prepare("SELECT item_id FROM user_items WHERE user_id = ? AND id = ?");
		$sth->execute($userID,$arguments{'item-id'});
		my $itemID;
		$sth->bind_columns(\$itemID);
		$sth->fetch;
		my $token = items::getStat('token',$itemID);
		$actions{$token}->();
	}
}

If you visit your inventory page and click on ‘Use’ underneath the item in your character’s inventory, you should be treated to the simple message ‘This is code that would run when the user used a potion’. Neat, eh?

While we’ve built the functionality for a single item, that’s not really our use-case – we need to see how useful this system will be if we want to add another item without writing too much extra code. Ideally, we would only need to write the code that makes this item special – and thankfully, dispatch tables let us do just that. First, we’ll add another item, with the token ‘crystal_ball’:

INSERT INTO items(name,type,price) VALUES (‘Crystal Ball’,’Usable’,10);
INSERT INTO entity_stats(stat_id,entity_id,value,entity_type) VALUES ((SELECT id FROM stats WHERE short_name=’token’),(SELECT id FROM items WHERE name=’Crystal Ball’),’crystal_ball’,’Item’);
And now that we’ve added the item, we’ll add the code to run when an item with that token gets used:

 

my %actions = (
	'potion' => \&use_potion,
	'crystal_ball'	=>	\&use_crystal_ball,
);
 
sub use_crystal_ball {
	$parameters{message} = 'This is code that would run if the user used a crystal ball.';
}

And if you use the crystal ball now that we’ve written that code, you’ll see the message ‘This is code that would run if the user used a crystal ball.’.

The ability to keep our code organized in this way is what makes dispatch tables so useful – it doesn’t take us long at all to write more code to support any new items we want to add, and it’s as simple as just adding a new token(which serves as a key in the dispatch table), and then writing the function that will be called(you could even map multiple tokens to the same function, if you wanted to – although in that situation you should just have two items with the same token).

And with that, our inventory page is complete! Users can now use any items that you have defined tokens and written the code for – and you can run any code that should be executed no matter what the item’s token is by adding it just after the line that says:

$actions{$token}->();
Here’s the (slightly re-organized) code for inventory.cgi:

#!/usr/bin/perl -w
 
use strict;
use CGI qw(:cgi);
use CGI::Carp qw(fatalsToBrowser warningsToBrowser);
use HTML::Template;
use DBI;
use config;
 
my $query = new CGI;
my %arguments = $query->Vars;
 
my $dbh = DBI->connect("DBI:mysql:$config{dbName}:$config{dbHost}",$config{dbUser},$config{dbPass},{RaiseError => 1});
my $sth;
my %parameters;
 
use stats;
 
my $cookie = $query->cookie('username+password');
my ($username) = split(/\+/,$cookie);
$sth = $dbh->prepare("SELECT id FROM users WHERE UPPER(username) = UPPER(?)");
$sth->execute($username);
my $userID;
$sth->bind_columns(\$userID);
$sth->fetch;
 
my %actions = (
	'potion' => \&use_potion,
	'crystal_ball'	=>	\&use_crystal_ball,
);
 
sub use_crystal_ball {
	$parameters{message} = 'This is code that would run if the user used a crystal ball.';
}
 
sub use_potion {
	$parameters{message} = 'This is code that would run if the user used a potion.';
}
 
if(%arguments) {
	if($arguments{'item-id'}) {
		use items;
		$sth = $dbh->prepare("SELECT item_id FROM user_items WHERE user_id = ? AND id = ?");
		$sth->execute($userID,$arguments{'item-id'});
		my $itemID;
		$sth->bind_columns(\$itemID);
		$sth->fetch;
		my $token = items::getStat('token',$itemID);
		$actions{$token}->();
	}
}
 
$sth = $dbh->prepare("SELECT id, item_id, quantity FROM user_items WHERE user_id = ?");
$sth->execute($userID);
my @inventory = ();
while(my $row = $sth->fetchrow_hashref) {
	my $sth2 = $dbh->prepare("SELECT name FROM items WHERE id = ?");
	$sth2->execute($row->{item_id});
	$sth2->bind_columns(\$row->{name});
	$sth2->fetch;
	push @inventory, $row;
}
 
$parameters{inventory} = \@inventory;
 
my $template = HTML::Template->new(
		filename	=>	'inventory.tmpl',
		associate	=>	$query,
		die_on_bad_params => 0,
	);
$template->param(%parameters);
print $query->header(), $template->output;

And here’s inventory.tmpl:

 

<html>
<head>
	<title>Your Inventory</title>
</head>
<body>
	<p>This is your inventory. Check out all the stuff you've got!</p>
	<tmpl_if name='message'>
		<p><!--tmpl_var name='message'--></p>
	</tmpl_if>
	<ul>
		<tmpl_loop name='inventory'>
		<li>
			<strong><!--tmpl_var name='name'--> x <!--tmpl_var name='quantity'--></strong>
			<form action='inventory.cgi' method='post'>
				<input type='hidden' name='item-id' value='<!--tmpl_var name="id"-->' />
				<input type='submit' value='Use' />
			</form>
		</li>
		</tmpl_loop>
	</ul>
</body>
</html>

Extra Credit

  • Re-factor the weapons and armor systems, so that they are all sold from the item-shop – and when you use them from your inventory, the items are equipped.