perl
Building Browsergames: Healing your players (Perl)
We’ve built a banking and combat system, but we haven’t built anything yet that will allow players to heal themselves after combat. Today, we’re going to build that piece of functionality.
Amazingly, we don’t actually need add any new stats to our game - we’ve already got all we need with the Maximum HP and Current HP stats from earlier. That means that we can start building our templates right away, without needing to do any SQL - starting with adding a link to index.tmpl:
17 | <p><a href='healer.cgi'>The Healer</a></p> |
Now that that’s done, we can create the template for our healing page. There isn’t really anything fancy on it - this is what the entire healer.tmpl template looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <html>
<head>
<title>The Healer</title>
</head>
<body>
<p>Welcome to the healer. You currently have <strong><!--tmpl_var name='curhp'--></strong> HP out of a maximum of <strong><!--tmpl_var name='maxhp'--></strong>.</p>
<p>You have <strong><!--tmpl_var name='gold'--></strong> gold to heal yourself with, and it will cost you <strong>1 gold per HP healed</strong> to heal yourself.</p>
<tmpl_if name='healed'>
<p>You have been healed for <strong><!--tmpl_var name='healed'--></strong> HP.</p>
</tmpl_if>
<form action='healer.cgi' method='post'>
<input type='text' name='amount' id='amount' /><br />
<input type='submit' name='action' value='Heal Me' />
</form>
<p><a href='index.cgi'>Back to main</a></p>
<script type='text/javascript'>
document.getElementById('amount').focus();
</script>
</body>
</html> |
If you’ve been following along so far, then chances are good that you can take one look at that template and tell exactly what we’re going to be doing with it - but I’ll let you in on the code anwyays. Here’s our starter code to load in the template and populate it with some values:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | #!/usr/bin/perl -w use strict; use CGI qw(:cgi); use HTML::Template; use DBI; use config; use stats; my $query = new CGI; my %arguments = $query->Vars; my $cookie = $query->cookie('username+password'); my $template = HTML::Template->new( filename => 'healer.tmpl', ); my %parameters; my ($username) = split(/\+/,$cookie); my $dbh = DBI->connect("DBI:mysql:$dbname:$dbhost",$dbuser,$dbpass,{RaiseError => 1}); my $sth = $dbh->prepare("SELECT id FROM users WHERE UPPER(username) = UPPER(?)"); $sth->execute($username); my $userID; $sth->bind_columns(\$userID); $sth->fetch; $parameters{gold} = stats::getStat('gc',$userID); $parameters{curhp} = stats::getStat('curhp',$userID); $parameters{maxhp} = stats::getStat('maxhp',$userID); $template->param(%parameters); print $query->header(), $template->output; |
With this code in place, now we can write the code that makes our page different from any other page that loads and displays a template. We’re going to add some handling for when a user enters how much they want to get healed. Just like we did for our bank page, we’ll assume that ‘weird’ values mean ‘use the maximum available’ - and then either heal the user to full, or as much as we can with their current gold on hand. Here’s our code to handle healing the user:
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | if(%arguments) { my $amount = $arguments{amount}; my $gold = stats::getStat('gc',$userID); my $needed = stats::getStat('maxhp',$userID) - stats::getStat('curhp',$userID); if($amount > $needed || $amount eq '') { $amount = $needed; } if($amount > $gold) { $amount = $gold; } stats::setStat('gc',$userID,stats::getStat('gc',$userID) - $amount); stats::setStat('curhp',$userID,stats::getStat('curhp',$userID) + $amount); $parameters{healed} = $amount; } |
For all that we’ve implemented similar functionality to our banking page from earlier in terms of checking what the user enters, you might notice a slight difference in the tests we perform based on what the user entered. That’s because we first need to make sure that they don’t heal themselves for more than their maximum HP, and then we need to make sure that they can afford to heal themselves for however much they are attempting to. Once the two checks are done, we heal the player for whatever amount possible, and then display a helpful message to show them they’ve been healed - in addition to subtracting the amount healed from their current gold in hand.
And that’s the healer page! People playing the game that we’ve been building can now fight monsters, put their gold in the bank, and heal themselves whenever they get injured. Here’s the code for the healer page, all in one place:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | #!/usr/bin/perl -w use strict; use CGI qw(:cgi); use HTML::Template; use DBI; use config; use stats; my $query = new CGI; my %arguments = $query->Vars; my $cookie = $query->cookie('username+password'); my $template = HTML::Template->new( filename => 'healer.tmpl', ); my %parameters; my ($username) = split(/\+/,$cookie); my $dbh = DBI->connect("DBI:mysql:$dbname:$dbhost",$dbuser,$dbpass,{RaiseError => 1}); my $sth = $dbh->prepare("SELECT id FROM users WHERE UPPER(username) = UPPER(?)"); $sth->execute($username); my $userID; $sth->bind_columns(\$userID); $sth->fetch; if(%arguments) { my $amount = $arguments{amount}; my $gold = stats::getStat('gc',$userID); my $needed = stats::getStat('maxhp',$userID) - stats::getStat('curhp',$userID); if($amount > $needed || $amount eq '') { $amount = $needed; } if($amount > $gold) { $amount = $gold; } stats::setStat('gc',$userID,stats::getStat('gc',$userID) - $amount); stats::setStat('curhp',$userID,stats::getStat('curhp',$userID) + $amount); $parameters{healed} = $amount; } $parameters{gold} = stats::getStat('gc',$userID); $parameters{curhp} = stats::getStat('curhp',$userID); $parameters{maxhp} = stats::getStat('maxhp',$userID); $template->param(%parameters); print $query->header(), $template->output; |
Building Browsergames: Creating the bank (Perl)
The other day, we built a combat system that would allow players to fight monsters, and get gold when they defeated those monsters. Today, we’re going to be building the bank for users to put their gold into.
To start off, we’ll add a link to our index page to bank.cgi, which will handle all of the logic for our users to do their banking.
16 | <p><a href='bank.cgi'>The Bank</a></p> |
And now that the link is there, we’ll create the template for that page. As usual, we’ll start off with just the basics:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <html> <head> <title>The Bank</title> </head> <body> <p>Welcome to the bank. You currently have <strong><!--tmpl_var name='inbank'--></strong> gold in the bank, and <strong><!--tmpl_var name='gold'--></strong> gold in hand.</p> <form action='bank.cgi' method='post'> <input type='text' name='amount' /><br /> <input type='submit' name='action' value='Deposit' /> or <input type='submit' name='action' value='Withdraw' /> </form> <p><a href='index.cgi'>Back to main</a></p> </body> </html> |
Save that file as bank.tmpl.
Even though our players have a gold stat to keep track of their gold in hand, we still need to add another one: gold in the bank. We’ll add another stat to our stats table, so that we can keep track of how much gold the player currently has in the bank. Here’s the relevant SQL query:
INSERT INTO stats(display_name,short_name) VALUES ('Gold In Bank','bankgc');
And now that we’ve inserted the stat, we’ll start writing the basic code behind our banking page. Because the amount of gold that a player has in the bank starts at 0, we won’t need any special code to make sure that we default it or anything - our stats code default behavior of returning 0 will work perfectly for us in this case.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | #!/usr/bin/perl -w use strict; use CGI qw(:cgi); use HTML::Template; use DBI; use config; use stats; my $query = new CGI; my $cookie = $query->cookie('username+password'); my $template = HTML::Template->new( filename => 'bank.tmpl', ); my %parameters; my ($username) = split(/\+/,$cookie); my $dbh = DBI->connect("DBI:mysql:$dbname:$dbhost",$dbuser,$dbpass,{RaiseError => 1}); my $sth = $dbh->prepare("SELECT id FROM users WHERE UPPER(username) = UPPER(?)"); $sth->execute($username); my $userID; $sth->bind_columns(\$userID); $sth->fetch; $parameters{inbank} = stats::getStat('bankgc',$userID); $parameters{gold} = stats::getStat('gc',$userID); $template->param(%parameters); print $query->header(), $template->output; |
And if you visit your bank page as it is right now, it will show you that you have no gold in the bank, along with however much you have in your character’s inventory. Now that that’s finished, we can start writing the logic to handle what happens when a player presses one of the action buttons on our page. We’re first going to customize our template slightly, so that it displays two different messages for deposits and withdrawals:
7 8 9 10 11 12 | <tmpl_if name='deposited'> <p>You deposited <strong><!--tmpl_var name='deposited'--></strong> gold into your bank account. Your total in the bank is now <strong><!--tmpl_var name='inbank'--></strong>.</p> </tmpl_if> <tmpl_if name='withdrawn'> <p>You withdraw <strong><!--tmpl_var name='withdrawn'--></strong> gold from your bank account. Your total gold in hand is now <strong><!--tmpl_var name='gold'--></strong>.</p> </tmpl_if> |
We’re also going to make a small tweak to the area where users enter the amount that they want to deposit/withdraw, by adding an ID to the field and then using that ID to make it auto-focus:
14 15 16 17 18 19 20 21 | <input type='text' name='amount' id='amount' /><br />
<input type='submit' name='action' value='Deposit' /> or
<input type='submit' name='action' value='Withdraw' />
</form>
<p><a href='index.cgi'>Back to main</a></p>
<script type='text/javascript'>
document.getElementById('amount').focus();
</script> |
And with that finished, we can start working on the code to handle the interactions on this page.
One thing that gets handled differently in every browsergame that I come accross is how forms handle values they don’t expect - things like trying to deposit ‘wq’ gold, trying to deposit or withdraw too much or too little, or how the form handles it when a user submits the form without entering a value at all. Some games stop and tell you that you made a mistake, forcing you to try to re-enter the correct value - but one approach that I’ve encountered and prefer is assuming that a value that’s ‘weird’(unexpected) means ‘use the maximum’ - so the game will just quietly shift my ‘wq2′ into ‘100′, or the ‘200′ into ‘100′(because it’s all I have). I like this option, so that’s how I’m going to write the following logic - but you’re allowed to do whatever you want with your game, and it shouldn’t be too hard to customize the following logic to display an error message if you prefer that way.
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | my $gold = stats::getStat('gc',$userID); my %arguments = $query->Vars; if(%arguments) { my $amount = $arguments{amount}; if($arguments{action} eq 'Deposit') { if($amount > $gold || $amount eq '') { # weird input - assume maximum $amount = $gold; } stats::setStat('gc',$userID,stats::getStat('gc',$userID) - $amount); stats::setStat('bankgc',$userID,stats::getStat('bankgc',$userID) + $amount); $parameters{deposited} = $amount; } else { my $bankGold = stats::getStat('bankgc',$userID); if($amount > $bankGold || $amount == '') { # weird input - assume maximum $amount = $bankGold; } stats::setStat('gc',$userID,stats::getStat('gc',$userID) + $amount); stats::setStat('bankgc',$userID,stats::getStat('bankgc',$userID) - $amount); $parameters{withdrawn} = $amount; } } |
And with that code written, our logic is finished - there isn’t anything else to worry about in our simple banking system. We check whether the user wants to make a deposit or a withdrawal, and then we handle each case individually - although as you can see, the code is virtually the same. The code is so similar that I’m sure someone could re-implement in a less repetitious way, via a creative use of references - but I’ll leave that as an exercise for a reader.
Here’s all the banking code in one place(albeit with some slight organizational tweaks):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | #!/usr/bin/perl -w use strict; use CGI qw(:cgi); use HTML::Template; use DBI; use config; use stats; my $query = new CGI; my %arguments = $query->Vars; my $cookie = $query->cookie('username+password'); my $template = HTML::Template->new( filename => 'bank.tmpl', ); my %parameters; my ($username) = split(/\+/,$cookie); my $dbh = DBI->connect("DBI:mysql:$dbname:$dbhost",$dbuser,$dbpass,{RaiseError => 1}); my $sth = $dbh->prepare("SELECT id FROM users WHERE UPPER(username) = UPPER(?)"); $sth->execute($username); my $userID; $sth->bind_columns(\$userID); $sth->fetch; my $gold = stats::getStat('gc',$userID); if(%arguments) { my $amount = $arguments{amount}; if($arguments{action} eq 'Deposit') { if($amount > $gold || $amount eq '') { # weird input - assume maximum $amount = $gold; } stats::setStat('gc',$userID,stats::getStat('gc',$userID) - $amount); stats::setStat('bankgc',$userID,stats::getStat('bankgc',$userID) + $amount); $parameters{deposited} = $amount; } else { my $bankGold = stats::getStat('bankgc',$userID); if($amount > $bankGold || $amount == '') { # weird input - assume maximum $amount = $bankGold; } stats::setStat('gc',$userID,stats::getStat('gc',$userID) + $amount); stats::setStat('bankgc',$userID,stats::getStat('bankgc',$userID) - $amount); $parameters{withdrawn} = $amount; } } $parameters{inbank} = stats::getStat('bankgc',$userID); $parameters{gold} = stats::getStat('gc',$userID); $template->param(%parameters); print $query->header(), $template->output; |
Building Browsergames: a simple combat system (Perl)
One thing that virtually all browsergames have is some sort of combat system - users enounter monsters, and then they fight those monsters in order to gain gold, resources, and so on. And that’s what we’re going to build today.
To begin with, we’ll need to make a few database changes - adding a few tables, and a little bit more data for our combat system to work with. We’ll start with a monsters table, for keeping track of our monsters:
CREATE TABLE monsters ( id int NOT NULL AUTO_INCREMENT, name text, PRIMARY KEY(id) );
We’ll also need to add two more stats to our game - Maximum HP, and Current HP. These two stats are fairly self-explanatory. Monsters are goingt o use the same stats as players do, using a table we’ll create called monster_stats:
INSERT INTO stats(display_name,short_name) VALUES ('Maximum HP','maxhp'); INSERT INTO stats(display_name,short_name) VALUES ('Current HP','curhp'); CREATE TABLE monster_stats ( id int NOT NULL AUTO_INCREMENT, monster_id int NOT NULL, stat_id int, value text, PRIMARY KEY(id) );
And at this point, we can add a couple of monsters to our database, along with giving them some starting stats:
INSERT INTO monsters(name) VALUES ('Crazy Eric'); INSERT INTO monster_stats(monster_id,stat_id,value) VALUES ((SELECT id FROM monsters WHERE name = 'Crazy Eric'),(SELECT id FROM stats WHERE short_name = 'atk'),2); INSERT INTO monster_stats(monster_id,stat_id,value) VALUES ((SELECT id FROM monsters WHERE name = 'Crazy Eric'),(SELECT id FROM stats WHERE short_name = 'def'),2); INSERT INTO monster_stats(monster_id,stat_id,value) VALUES ((SELECT id FROM monsters WHERE name = 'Crazy Eric'),(SELECT id FROM stats WHERE short_name = 'maxhp'),8); INSERT INTO monster_stats(monster_id,stat_id,value) VALUES ((SELECT id FROM monsters WHERE name = 'Crazy Eric'),(SELECT id FROM stats WHERE short_name = 'gc'),5); INSERT INTO monsters(name) VALUES ('Lazy Russell'); INSERT INTO monster_stats(monster_id,stat_id,value) VALUES ((SELECT id FROM monsters WHERE name = 'Lazy Russell'),(SELECT id FROM stats WHERE short_name = 'atk'),1); INSERT INTO monster_stats(monster_id,stat_id,value) VALUES ((SELECT id FROM monsters WHERE name = 'Lazy Russell'),(SELECT id FROM stats WHERE short_name = 'def'),0); INSERT INTO monster_stats(monster_id,stat_id,value) VALUES ((SELECT id FROM monsters WHERE name = 'Lazy Russell'),(SELECT id FROM stats WHERE short_name = 'maxhp'),4); INSERT INTO monster_stats(monster_id,stat_id,value) VALUES ((SELECT id FROM monsters WHERE name = 'Lazy Russell'),(SELECT id FROM stats WHERE short_name = 'gc'),20); INSERT INTO monsters(name) VALUES ('Hard Hitting Louis'); INSERT INTO monster_stats(monster_id,stat_id,value) VALUES ((SELECT id FROM monsters WHERE name = 'Hard Hitting Louis'),(SELECT id FROM stats WHERE short_name = 'atk'),4); INSERT INTO monster_stats(monster_id,stat_id,value) VALUES ((SELECT id FROM monsters WHERE name = 'Hard Hitting Louis'),(SELECT id FROM stats WHERE short_name = 'def'),3); INSERT INTO monster_stats(monster_id,stat_id,value) VALUES ((SELECT id FROM monsters WHERE name = 'Hard Hitting Louis'),(SELECT id FROM stats WHERE short_name = 'maxhp'),10); INSERT INTO monster_stats(monster_id,stat_id,value) VALUES ((SELECT id FROM monsters WHERE name = 'Hard Hitting Louis'),(SELECT id FROM stats WHERE short_name = 'gc'),5);
There are now three monsters within our database, complete with some basic stats - attack, defence, maximum hp, and gold. Attack, defence, and maximum HP all make sense on their own - but if you were wondering what the ‘gold’ stat is for with a monster, it’s so that we know how much gold to give the player after they manage to kill the monster. These stats(for now) will be the basis behind our combat system.
The first thing we’ll do to prepare for our combat system is modify our main page, so that it displays the user’s current HP and has a link to the ‘Forest’ page, so that the user can fight monsters. Here’s what the new index.tmpl looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <html> <head> <title>Index Page</title> </head> <body> <p>Hello, <!--tmpl_var name='username'-->!</p> <ul> <li>Attack: <strong><!--tmpl_var name='attack'--></strong></li> <li>Defence: <strong><!--tmpl_var name='defence'--></strong></li> <li>Magic: <strong><!--tmpl_var name='magic'--></strong></li> <li>Gold in hand: <strong><!--tmpl_var name='gold'--></strong></li> <li>Current HP: <strong><!--tmpl_var name='current_hp'-->/<!--tmpl_var name='maximum_hp'--></strong></li> </ul> <p><a href='logout.cgi'>Logout</a></p> <p><a href='forest.cgi'>The Forest</a></p> </body> </html> |
We also make a minor change to index.cgi, so that it retrieves and then displays the user’s current HP and maximum HP stats:
25 26 | $stats{current_hp} = stats::getStat('curhp',$userID); $stats{maximum_hp} = stats::getStat('maxhp',$userID); |
Now, if you login and visit your main page, you’ll notice that both of your HP values are currently set to 0 - not exactly setting you up for success.
There are a few options available to you when introducing a new stat to your game. You could go through your database, and hand-insert the new default values for every user who signed up before the stat was introduced, in addition to modifying your registration page to also set up the new defaults - or you could do it the easier way, and use a second stat, that exists solely to check to see if the player has had the stat’s default value set yet.
Are you wondering how that works? We’re taking advantage of the fact that our stats library returns 0 and inserts a new row for any stat we try to retrieve that doesn’t have a value yet. All we do is check to see if the ’set HP value’ stat has a value of 0 - if it does, we know that the user hasn’t had their HP set up, and we can set it. To start the process off, we’ll insert the stat into the database:
INSERT INTO stats(display_name,short_name) VALUES ('Set Default HP Values','sethp');
And then, we just quickly check the value on our main page(although you could do it on any page - just after a successful login on the login page would probably be a good spot):
25 26 27 28 29 30 31 32 33 | my $setHP = stats::getStat('sethp',$userID); if($setHP == 0) { # haven't set up the user's HP - set to defaults stats::setStat('curhp',$userID,10); stats::setStat('maxhp',$userID,10); stats::setStat('sethp',$userID,1); } $stats{current_hp} = stats::getStat('curhp',$userID); $stats{maximum_hp} = stats::getStat('maxhp',$userID); |
And that code will handle setting up our player’s default HP values for us. All it does is check to see if their HP has been set - and if it isn’t, it sets it for them. It also updates the stat ’sethp’, so that we don’t accidentally reset the user’s HP values to the defaults the next time they visit the page. Now that we’ve gotten that all working, we can start building our forest page.
To start with, we’ll need a template. The template will display the monster that a player encountered, and two options - ‘Attack’, or ‘Run Away’. By clicking on ‘Attack’ users can go through a few rounds of combat with the monster, using their attack and defence stats to figure out whether the player or monster wins the fight. Clicking on ‘Run Away’ will redirect the player back to the index page. Here’s our starting template, called forest.tmpl:
<html> <head> <title>The Forest</title> </head> <body> <p>You've encountered a <!--tmpl_var name='monster'-->!</p> <form action='forest.cgi' method='post'> <input type='submit' name='action' value='Attack' /> or <input type='submit' name='action' value='Run Away' /> <input type='hidden' name='monster' value='<!--tmpl_var name='monster'-->' /> </form> </body> </html>
We’ve placed a hidden field called ‘monster’ in our template so that we can keep track of which monster the player is about to fight - when they click a button on this page, we’ll be able to see which monster they are about to interact with. The next thing we need to do is create forest.cgi, which is the page that will display our forest.tmpl template, and select a random monster for the player to fight.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #!/usr/bin/perl -w use strict; use CGI qw(:cgi); use HTML::Template; my $query = new CGI; use DBI; use config; my $dbh = DBI->connect("DBI:mysql:$dbname:$dbhost",$dbuser,$dbpass,{RaiseError => 1}); my $sth = $dbh->prepare("SELECT name FROM monsters ORDER BY RAND() LIMIT 1"); $sth->execute(); my $monster; $sth->bind_columns(\$monster); $sth->fetch; my %parameters; $parameters{monster} = $monster; my $template = HTML::Template->new( filename => 'forest.tmpl', ); $template->param(%parameters); print $query->header(), $template->output; |
We haven’t made our page actually handle it when any of our buttons are clicked just yet - for now, all this code is doing is retrieving a random monster’s name and displaying it to the user.
I wanted to explain the SQL query that we’re using to retrieve the monster information, as you may not have seen it before. The ORDER BY RAND() query is the easiest way to retrieve random rows in MySQL. Instead of telling MySQL to order by a specific column, we tell it to use a generated random number to order things - meaning that the order of the rows retrieved will be random. We use the LIMIT 1 clause at the end to make sure we only retrieve one random row. If you’re so inclined, you can also use the WHERE clause to limit what rows you are going to be selecting from randomly - we’ll use that later when we add some more functionality to our game.
Because we gave both of the buttons on our page the name of ‘action’, we can easily switch off based on which one was pressed:
8 9 10 11 12 13 14 15 16 17 18 | my $query = new CGI; my %arguments = $query->Vars; if(%arguments) { if($arguments{action} eq 'Attack') { # fighting the monster } else { # running away - back to the index page! print $query->redirect('index.cgi'); } } |
In order to handle the fight with our monster, we’re going to customize our template a little bit and add an area to display the results of the fight, along with hiding the “you’ve encountered a <___>!” message. Here’s the relevant portion of our new template:
6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <tmpl_unless name='combat'> <p>You've encountered a <!--tmpl_var name='monster'-->!</p> <form action='forest.cgi' method='post'> <input type='submit' name='action' value='Attack' /> or <input type='submit' name='action' value='Run Away' /> <input type='hidden' name='monster' value='<!--tmpl_var name='monster'-->' /> </form> <tmpl_else> <ul> <tmpl_loop name='combat'> <li><strong><!--tmpl_var name='attacker'--></strong> attacks <!--tmpl_var name='defender'--> for <!--tmpl_var name='damage'--> damage!</li> </tmpl_loop> </ul> </tmpl_unless> |
By customizing our template this way, we can display the results of the fight when a user decides to fight a monster, without accidentally re-displaying the ‘you encountered a monster!’ message.
If that HTML::Template code looks a little new, that’s because it is - we’ve never used the <tmpl_loop> or <tmpl_unless> tags before. <tmpl_loop> allows us to create simple(and sometimes not-so-simple) loops which we can display in our template, and the <tmpl_unless> tag says “show what’s before the <tmpl_else> if my variable doesn’t have anything in it or evaluates to false - otherwise, show what’s inside the <tmpl_else> block.
Next we need to actually add the combat logic. But before we can add that, we’ll need a way to retrieve our monster’s stats - we haven’t built anything to do that yet. We’ll quickly modify our player stats code to work off of our monster_stats table, and take slightly different arguments:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | package monsterstats; use DBI; sub getMonsterStat { my ($statName,$monsterID) = @_; use config; my $dbh = DBI->connect("DBI:mysql:$dbname:$dbhost",$dbuser,$dbpass,{RaiseError => 1}); createMonsterStatIfNotExists($statName,$monsterID); my $sth = $dbh->prepare("SELECT value FROM monster_stats WHERE stat_id = (SELECT id FROM stats WHERE display_name = ? OR short_name = ?) AND monster_id = ?"); $sth->execute($statName,$statName,$monsterID); my $value; $sth->bind_columns(\$value); $sth->fetch; return $value; } sub createMonsterStatIfNotExists { my ($statName, $monsterID) = @_; use config; my $dbh = DBI->connect("DBI:mysql:$dbname:$dbhost",$dbuser,$dbpass,{RaiseError => 1}); my $sth = $dbh->prepare("SELECT count(value) FROM monster_stats WHERE stat_id = (SELECT id FROM stats WHERE display_name = ? OR short_name = ?) AND monster_id = ?"); $sth->execute($statName,$statName,$monsterID); my $count; $sth->bind_columns(\$count); $sth->fetch; if($count == 0) { # no entry for that stat/user combination - insert one with a value of 0 $sth = $dbh->prepare("INSERT INTO monster_stats(stat_id,monster_id,value) VALUES ((SELECT id FROM stats WHERE display_name = ? OR short_name = ?),?,?)"); $sth->execute($statName,$statName,$monsterID,0); } } 1; |
You can save this file as monsterstats.pm - as you can see, there’s very little changed from the code we use to retrieve player stats. Basically, we changed the function names so that there aren’t any naming collisions(and we can clearly see what stat we’re retrieving), and we’ve modified the SQL queries getting run so that they work off of the monster_stats table. We removed the setStat() function, because players aren’t supposed to be able to actually set a monster’s stats - they only need to be retrieved for combat.
Now that that’s finished, we can get back to writing our actual combat code, inside forest.cgi. First off, we’ll use our stats and monster stats code to retrieve the monster and player stats and store them into two hashes, %player and %monster. Because we’re now using a database connection in two areas of our code, I’ve also re-organized it a little bit - moving the database connection code to the top of the program. Here’s the new code:
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | my $dbh = DBI->connect("DBI:mysql:$config{dbName}:$config{dbHost}",$config{dbUser},$config{dbPass},{RaiseError => 1}); my $sth; my %parameters; if(%arguments) { if($arguments{action} eq 'Attack') { # fighting the monster use stats; use monsterstats; 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 %player = ( name => $username, attack => stats::getStat('atk',$userID), defence => stats::getStat('def',$userID), curhp => stats::getStat('curhp',$userID) ); $sth = $dbh->prepare("SELECT id FROM monsters WHERE name = ?"); $sth->execute($arguments{monster}); my $monsterID; $sth->bind_columns(\$monsterID); $sth->fetch; my %monster = ( name => $arguments{monster}, attack => monsterstats::getMonsterStat('atk',$monsterID), defence => monsterstats::getMonsterStat('def',$monsterID), curhp => monsterstats::getMonsterStat('maxhp',$monsterID) ); |
What we’ve done here is retrieved the IDs of our user and our monster, and then used those in conjunction with our stats code to retrieve their stats. By doing this, we can refer to specific stats by using something like $monster{attack}, which is much easier then having a whole handful of confusingly-named variables.
Now that we’ve retrieved the stats for both the player and the monster, we can start with the actual combat in the system. It’s actually very simple, and boils down to a simple while loop:
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | my @combat; my $turns = 0; my ($attacker,$defender); while($player{curhp} > 0 && $monster{curhp} > 0) { my %attack; if($turns % 2 != 0) { $attacker = \%monster; $defender = \%player; } else { $attacker = \%player; $defender = \%monster; } my $damage = 0; if($attacker->{attack} > $defender->{defence}) { $damage = $attacker->{attack} - $defender->{defence}; } my %attack = ( attacker => $attacker->{name}, defender => $defender->{name}, damage => $damage ); $defender->{curhp} -= $damage; push @combat, \%attack; $turns++; } stats::setStat(' |