Qbasic: the Magazine
08.14.99
Issue 12

 

mod 1

By Darkdread

Download the related program

Hail!

Welcome to the second installment of the new RPG tutorials. Hopefully by now, most of the rust has worn off... and I'll be able to provide you all with a much better tutorial.

If you missed the previous one... It showed you how to write a scripting engine for your RPG. If you don't see it around here somewhere, ask zkman, I'm sure that he still has it. :)

This time around, I'll show you how to write a semi-active battle engine!

Before we start, let me point out that all of the code used in this tutorial is available in a zip file, along with some graphics, which demonstrate the engine in action. You can download it now, or read the tutorial first. If you're new to this, I would suggest reading the tutorial, as the code is not commented.

NOTE: This tutorial is aimed at a novice QB programmer. If you're a beginner, it shouldn't be too tough to figure out, But you may wish to brush up on your coding skills first.

Setting it all up.
First, we need to set up our variables, and the sub routines which we will use. Remember, always start with the two lines below. The $DYNAMIC meta- command is used to store arrays dynamically; This means that your array space is much more flexible. DEFINT A-Z defines all arrays as integers. This basically speeds up execution of the program.

'$DYNAMIC
DEFINT A-Z

Next, we delcare our sub routines. I'll explain what each of these is for once we get to them, so don't worry about it for now. You may also wish to know that this step is optional, as QB will automatically create these lines when you write the actual subroutines.

DECLARE SUB GetHandLocation ()
DECLARE SUB TimerDelay (Seconds!)
DECLARE SUB Battle ()
DECLARE SUB StatsBox ()
DECLARE SUB ChoiceBox (BoxType%)
DECLARE SUB DrawBattleScreen (ScreenType%)
DECLARE SUB InitBattle ()
DECLARE SUB InitRandomStats ()
DECLARE SUB InitSprites ()

Next, we must dimension our arrays, set up our global variables, and our constants as well. The first constants, are true and false. We will be using these for our 'flag' variables, to see check for certain happenings and tell the program to comtinue doing something if they are false, or to do something else if they are true.

CONST True = -1, False = NOT True

Next, we dimension the arrays we will use in the engine. These are needed to store our graphics. For this engine, there a four different enemies, and all are 32x32 in size. The two player characters are also 32x32, there are also three frames of animation for them. Next, our hand pointer is one 16x16 sprite. Finally, we need to allocate two arrays to hold parts of the graphic background which our sprites might be put over. This way, we can get what's behind the sprites, put or sprite on the screen, then, restore the background when we're done. Finally, we will also allocate space for masks in our sprite arrays.

Basically, This is the calculation I used to determine how big each array would have to be:

((SpriteXSize * SpriteYSize / 2) * NumberOfSprites) - 1

If you are not familiar how this all works, I would suggest reading a tutorial about sprites and graphics in SCREEN 13. It would help you understand more of what we're doing here.

DIM SHARED Hand%(258)
DIM SHARED Players%(4626)
DIM SHARED Enemies%(2570)
DIM SHARED BackSprite%(1028)
DIM SHARED BackHand%(129)

Next, we need to declare all the variables and dimension all of the arrays, which we will be using for our data. Basically, we need space to hold the players stats and enemy stats. Below, are the stats I deemed necessary for this engine to work. It's a good idea to give everything a name which will tell you what each variable is for; Ie. PlayerHP% is a much better variable name than, say A1% to hold the player's Hit Points.

DIM SHARED PlayerName$(1 TO 2), PlayerAlive%(1 TO 2), PlayerType%(1 TO 2)
DIM SHARED PlayerHP%(1 TO 2), PlayerMaxHP%(1 TO 2), PlayerMP%(1 TO 2), PlayerMaxMP%(1 TO 2)
DIM SHARED PlayerST%(1 TO 2), PlayerDF%(1 TO 2), PlayerAG%(1 TO 2)
DIM SHARED PlayerMS%(1 TO 2), PlayerMD%(1 TO 2)
DIM SHARED PlayerEXP&(1 TO 2), PlayerGold&
DIM SHARED PlayerX%(1 TO 2), PlayerY%(1 TO 2), PlayerGo%(1 TO 2)


DIM SHARED EnemyName$(1 TO 4), EnemyAlive%(1 TO 4), EnemyType%(1 TO 4)
DIM SHARED EnemyHP%(1 TO 4), EnemyMaxHP%(1 TO 4), EnemyMP%(1 TO 4), EnemyMaxMP%(1 TO 4)
DIM SHARED EnemyST%(1 TO 4), EnemyDF%(1 TO 4), EnemyAG%(1 TO 4)
DIM SHARED EnemyMS%(1 TO 4), EnemyMD%(1 TO 4)
DIM SHARED EnemyEXP%(1 TO 4), EnemyGold%(1 TO 4)
DIM SHARED EnemyX%(1 TO 4), EnemyY%(1 TO 4), EnemyGo%(1 TO 4), EnemyThere%

 

R
P
G
 
B
A
T

T
L
E
S

  Next, we'll declare arrays to hold the X and Y location of our hand pointer. This will come in handy (No pun intended... really...) later.

DIM SHARED HandX%, HandY%

Now, we will initialize the random number seed. What is this? Well, we need the ability to generate random numbers, so that player and enemy damage isn't always the same. This line tells QB that we will be doing so later.

RANDOMIZE TIMER

Okay... Now we're more or less set up. The next parts of this tutorial will deal with individual parts of the engine. It is recommended that you read all of them carefully, as they are all intertwined closley together.

The rest of the main module.
This is how the rest of our main module would look. Note that some of this refers to subroutines I have not talked about yet... You may scroll down and read about these subroutines first... Then come back here.

Well, we have to tell QB to load our initialization routines next. To do this, we just call them by name like so:

InitSprites
InitRandomStats

Once we're initialized... Let's switch to screen 13 (320x200 resolution video mode, with 256 colours if you don't know) and call our main Battle sub to begin the fight:

SCREEN 13 Battle

Well... The battle's done. Now, the engine reverts to text mode and displays a short message and waits for a key press. After this, it exits. This last bit of code, you should leave out.

SCREEN 0: WIDTH 80
PRINT "Semi-Active Battle Engine. Created by DarkDread, 1999"
PRINT "You may use this in your programs... Just give me credit :)"
PRINT "Press any key to exit."
WHILE INKEY$ = "": WEND
END

The main battle sub.
This is the biggest sub routine in the engine, as it controls the flow of the battle. You may split this up into smaller routines, but for the sake of simplicity, I kept it this way for the tutorial.

Our subroutine starting code... QB will automatically do this for you, when you create a new sub, so you may skip this part if you wish.

REM $STATIC
SUB Battle

First, we must initialize our battle stats, then tell the program to draw the battle screen. It is done by calling the DrawBattleScreen. Note that we also pass a value of 1 to it. This way, the sub will know to draw a starting battle screen, and not an ending one.

InitBattle
DrawBattleScreen 1

Now, we begin our main battle loops.

DO

DO

These next lines, will calculate the agility of player and enemy characters that are still alive. This sub will take the agility of each character, and add it to a total every time it loops. Once a character's total is greater than 99... That character will be allowed a turn. Also, some of the player part of these lines, draws the little red and yellow status bar, which shows how much longer a player has to wait until their next turn.

FOR I = 1 TO 4
IF EnemyAlive%(I) THEN EnemyGo%(I) = EnemyGo%(I) + EnemyAG%(I)
IF EnemyGo%(I) > 99 THEN
EnemyGo%(I) = -1: GoThere% = True
END IF
IF EnemyGo%(I) = -1 THEN EXIT FOR
NEXT I
FOR I = 1 TO 2
IF PlayerAlive%(I) THEN PlayerGo%(I) = PlayerGo%(I) + PlayerAG%(I)
IF I = 1 THEN
Yellow = PlayerGo%(I) + 128
IF Yellow% > 228 THEN Yellow% = 228
IF Yellow% > 128 THEN LINE (128, 16)-(Yellow%, 18), 44, BF
IF Yellow% < 228 THEN LINE (Yellow% + 1, 16)-(228, 18), 40, BF
ELSEIF I = 2 THEN
Yellow = PlayerGo%(I) + 128
IF Yellow% > 228 THEN Yellow% = 228
IF Yellow% > 128 THEN LINE (128, 32)-(Yellow%, 34), 44, BF
IF Yellow% < 228 THEN LINE (Yellow% + 1, 32)-(228, 34), 40, BF
END IF
IF PlayerGo%(I) > 99 THEN
GoGo% = True
END IF
IF PlayerGo%(I) > 99 THEN EXIT FOR
NEXT I

Here, we have a little timer delay. This is to slow the battles down to a playable speed. If the delay wasn't here, you would notice that everyone attacks almost at once! The battles wouldn't be much fun then. You can raise this number to slow a battle down, and lower the number to speed it up.

TimerDelay .1

LOOP UNTIL GoThere% OR GoGo%

A character can now take a turn... If GoThere% is true, this means that it's an enemy's turn.

IF GoThere% THEN

Next, we check which enemy is allowed a turn, and if they are still alive or not... Just in case. Once we have found out, we draw the enemy mask on it's location. This will draw the enemy in all black, so the player knows which enemy is about to attack them.

FOR I = 1 TO 4
IF EnemyGo%(I) = -1 AND EnemyAlive%(I) THEN
PUT (EnemyX%(I), EnemyY%(I)), Enemies%(514 * 4), AND

This next line randomly selects which player the enemy will attack. This is where our random seed generator is put to work.

HitPlayer% = INT(RND * 2) + 1

After a player has been selected, these next lines check if that player is alive. Then a calculation is made based on the enemy's strength, the player's defense, and the help of a few random numbers, to determine how much damage the enemy has done. The random numbers are used just to add some variety to the damage. Instead of an enemy doing, say, 5 damage on a player all the time, they may do 3 sometimes, and 6 some other time.

Finally, we must take the damage done off of the player's HP. If the player's HP is less than one after this, it means they are dead. We then change the player's HP to 0 (So you don't see negative numbers in their stats) and make the PlayerAlive% variable of that player false. This way, the engine will know not to let that player have a turn, or to show their picture on the screen.

IF PlayerAlive%(HitPlayer%) THEN
Damage% = (EnemyST%(I) * 2 + (INT(RND * 4))) - PlayerDF%(HitPlayer%)
IF Damage% < 1 THEN Damage% = INT(RND * 2) + 1
PlayerHP%(HitPlayer%) = PlayerHP%(HitPlayer%) - Damage%
IF PlayerHP%(HitPlayer%) < 1 THEN
PlayerHP%(HitPlayer%) = 0: PlayerAlive%(HitPlayer%) = False
END IF
ELSE
IF HitPlayer% = 1 THEN HitPlayer% = 2 ELSE HitPlayer% = 1
Damage% = (EnemyST%(I) * 2 + (INT(RND * 4))) - PlayerDF%(HitPlayer%)
IF Damage% < 1 THEN Damage% = INT(RND * 2) + 1
PlayerHP%(HitPlayer%) = PlayerHP%(HitPlayer%) - Damage%
IF PlayerHP%(HitPlayer%) < 1 THEN
PlayerHP%(HitPlayer%) = 0: PlayerAlive%(HitPlayer%) = False
END IF
END IF

Next, we show the damage done on the appropriate player, and delay for 3 seconds. The delay is necessary to allow us to see how much damamge the enemy did, and on who.

IF HitPlayer% = 1 THEN
LOCATE 12, 36: PRINT Damage%
TimerDelay 3
ELSEIF HitPlayer% = 2 THEN
LOCATE 18, 36: PRINT Damage%
TimerDelay 3
END IF

Now that the enemy's turn is done, their agility meter must be set back to 0. We must also make GoThere% false, so the engine knows to return to the pervious loop. Finally, we must make our DrawNeed% variable equal to 1. We will use this variable later when calling the DrawBattleScreen so we know what parts of the battle we will need to redraw.

EnemyGo%(I) = 0: GoThere% = False: DrawNeed% = 1
END IF
NEXT I
END IF

Next, our routine checks if it was a player's turn right after the enemy's. If so, we must redraw the screen, using our DrawNeed% variable from before.

IF DrawNeed% = 1 AND GoGo% THEN DrawBattleScreen DrawNeed%

Now, we check if it's the player's turn to go. If so, we check which player will be going. Then, we draw a choice box for them which will give the player their options (RUN, ATTACK) and set up our hand pointer X and Y values. We then draw the player's mask over the player, this way, we will know which character's turn it is. We also draw our hand pointer next to the first choice in our choice box.

IF GoGo% THEN
FOR I = 1 TO 2
IF PlayerGo%(I) > 99 AND PlayerAlive%(I) THEN
ChoiceBox 1
HandY% = 5: HandX% = 10
PUT (PlayerX%(I), PlayerY%(I)), Players%(514 * 6), AND
PUT (HandX%, HandY%), Hand%(0), PSET
ChoiceMade% = False

Now, we have to create a loop. Then, we trap the keyboard buffer using INKEY$ to check which keys the player has pressed. This is all used so that the routine knows to move the hand pointer up or down, and which choice the player has selected when they hit enter.

DO
SELECT CASE INKEY$
CASE CHR$(0) + CHR$(72)
IF HandY% = 5 THEN
HandY% = 29
ELSEIF HandY% = 29 THEN
HandY% = 5
END IF
LINE (5, 7)-(24, 45), 0, BF
PUT (HandX%, HandY%), Hand%, PSET
CASE CHR$(0) + CHR$(80)
IF HandY% = 5 THEN
HandY% = 29
ELSEIF HandY% = 29 THEN
HandY% = 5
END IF
LINE (5, 7)-(24, 45), 0, BF
PUT (HandX%, HandY%), Hand%, PSET
CASE CHR$(13)

The player has hit enter. Now, we check where the hand pointer was when they pressed the enter key. If it was at Attack (HandY% = 5), then we allow the player to choose which enemy they wish to attack. This is done with the hand pointer, and another loop while checking the keyboard. You will also notice that before the handpointer is moved, the routine checks if which enemy is still alive and which one isn't. This is so that the hand pointer is never pointing to an enemy that is no longer there.

You will also notice that this is where we use our array to hold the back- ground graphics were the hand pointer is put. This is necessary because once the player moves the hand pointer, we need to restore the graphics that were there before the pointer was drawn.

After the player hits enter here... We determine which enemy they have decided to attack. Then, we make a calculation based on the player's strength and the enemy's defense... As usual, we throw in a few random numbers to add variety.

Finally, we show an animation of the player attacking (We use our second array to hold background graphics here) and display the damage done on the enemy they've attacked. Again, we pause after this for 3 seconds, so that we may see who was attacked by whom, and how much damage was done.

IF HandY% = 5 THEN
ChoiceBox 0
GetHandLocation
GET (HandX%, HandY%)-(HandX% + 15, HandY% + 15), BackHand%
PUT (HandX%, HandY%), Hand%(130), AND
PUT (HandX%, HandY%), Hand%(0), XOR
ChoiceMade2% = False
DO

SELECT CASE INKEY$
CASE CHR$(0) + CHR$(72), CHR$(0) + CHR$(80)
PUT (HandX%, HandY%), BackHand%, PSET
IF HandX% = 8 AND HandY% = 85 THEN
IF EnemyAlive%(3) THEN
HandX% = 8: HandY% = 135
ELSEIF EnemyAlive%(4) THEN
HandX% = 114: HandY% = 135
ELSEIF EnemyAlive%(2) THEN
HandX% = 114: HandY% = 85
END IF
ELSEIF HandX% = 114 AND HandY% = 85 THEN
IF EnemyAlive%(4) THEN
HandX% = 114: HandY% = 135
ELSEIF EnemyAlive%(3) THEN
HandX% = 8: HandY% = 135
ELSEIF EnemyAlive%(1) THEN
HandX% = 8: HandY% = 85
END IF
ELSEIF HandX% = 8 AND HandY% = 135 THEN
IF EnemyAlive%(1) THEN
HandX% = 8: HandY% = 85
ELSEIF EnemyAlive%(2) THEN
HandX% = 114: HandY% = 85
ELSEIF EnemyAlive%(4) THEN
HandX% = 114: HandY% = 135
END IF
ELSEIF HandX% = 114 AND HandY% = 135 THEN
IF EnemyAlive%(2) THEN
HandX% = 114: HandY% = 85
ELSEIF EnemyAlive%(1) THEN
HandX% = 8: HandY% = 85
ELSEIF EnemyAlive%(3) THEN
HandX% = 8: HandY% = 135
END IF
END IF
GET (HandX%, HandY%)-(HandX% + 15, HandY% + 15), BackHand%
PUT (HandX%, HandY%), Hand%(130), AND
PUT (HandX%, HandY%), Hand%(0), XOR
CASE CHR$(0) + CHR$(75), CHR$(0) + CHR$(77)
PUT (HandX%, HandY%), BackHand%, PSET
IF HandX% = 8 AND HandY% = 85 THEN
IF EnemyAlive%(2) THEN
HandX% = 114: HandY% = 85
ELSEIF EnemyAlive%(4) THEN
HandX% = 114: HandY% = 135
ELSEIF EnemyAlive%(3) THEN
HandX% = 8: HandY% = 135
END IF
ELSEIF HandX% = 114 AND HandY% = 85 THEN
IF EnemyAlive%(1) THEN
HandX% = 8: HandY% = 85
ELSEIF EnemyAlive%(3) THEN
HandX% = 8: HandY% = 135
ELSEIF EnemyAlive%(4) THEN
HandX% = 114: HandY% = 135
END IF
ELSEIF HandX% = 8 AND HandY% = 135 THEN
IF EnemyAlive%(4) THEN
HandX% = 114: HandY% = 135
ELSEIF EnemyAlive%(2) THEN
HandX% = 114: HandY% = 85
ELSEIF EnemyAlive%(1) THEN
HandX% = 8: HandY% = 85
END IF
ELSEIF HandX% = 114 AND HandY% = 135 THEN
IF EnemyAlive%(3) THEN
HandX% = 8: HandY% = 135
ELSEIF EnemyAlive%(1) THEN
HandX% = 8: HandY% = 85
ELSEIF EnemyAlive%(2) THEN
HandX% = 114: HandY% = 85
END IF
END IF
GET (HandX%, HandY%)-(HandX% + 15, HandY% + 15), BackHand%
PUT (HandX%, HandY%), Hand%(130), AND
PUT (HandX%, HandY%), Hand%(0), XOR
CASE CHR$(13)
IF HandX% = 8 AND HandY% = 85 THEN
EnemySelect% = 1
ELSEIF HandX% = 114 AND HandY% = 85 THEN
EnemySelect% = 2
ELSEIF HandX% = 8 AND HandY% = 135 THEN
EnemySelect% = 3
ELSEIF HandX% = 114 AND HandY% = 135 THEN
EnemySelect% = 4
END IF
ChoiceMade2% = True
END SELECT
LOOP UNTIL ChoiceMade2%

Damage% = (PlayerST%(I) * 2 + INT(RND * 4) + 1) - EnemyDF%(EnemySelect%)
IF Damage% = 0 THEN Damage% = INT(RND * 2) + 1
EnemyHP%(EnemySelect%) = EnemyHP%(EnemySelect%) - Damage%
IF EnemyHP%(EnemySelect%) < 1 THEN EnemyAlive%(EnemySelect%) = False
FOR J = 7 TO 8
PUT (PlayerX%(I), PlayerY%(I)), BackSprite%(514 * (I - 1)), PSET
PUT (PlayerX%(I), PlayerY%(I)), Players%(514 * J), AND
IF J = 7 THEN
PUT (PlayerX%(I), PlayerY%(I)), Players%(514 * (PlayerType%(I) + 1)), XOR
ELSEIF J = 8 THEN
PUT (PlayerX%(I), PlayerY%(I)), Players%(514 * (PlayerType%(I) + 2)), XOR
END IF
TimerDelay .5
NEXT J
IF EnemySelect% = 1 THEN
LOCATE 12, 4: PRINT USING "###"; Damage%
ELSEIF EnemySelect% = 2 THEN
LOCATE 12, 17: PRINT USING "###"; Damage%
ELSEIF EnemySelect% = 3 THEN
LOCATE 18, 4: PRINT USING "###"; Damage%
ELSEIF EnemySelect% = 4 THEN
LOCATE 18, 17: PRINT USING "###"; Damage%
END IF
TimerDelay 3
ChoiceMade% = True: DrawNeed% = 1

 

A pic of the engine

 
  This is the rest of our first choice box. Here, if the player has chosen to run away (HandY% = 29) then we do a calculation based on the player's agility and the agility of a random enemy to detrmine if the player can succesfully run, or not. Why a random enemy? Simple, we want to allow the player to have a fair chance from running from any group of enemies that isn't based on the agility of the quickest or slowest enemy in the group. If the player can run, then RanAway is made true.

ELSEIF HandY% = 29 THEN
RandomEnemy% = INT(RND * 4) + 1
IF INT(RND * PlayerAG%(I)) + PlayerAG%(I) / 2 > INT(RND * EnemyAG%(RandomEnemy%)) + EnemyAG%(RandomEnemy%) / 2 THEN RanAway = True
ChoiceMade% = True
END IF
END SELECT
LOOP UNTIL ChoiceMade%

Well... A player's turn has occured, so now we must reset their agility meter to 0 and make GoGo% false so that our program knows to return to the previous loop.

PlayerGo%(I) = 0: GoGo% = False
END IF
NEXT I
END IF

After a player or an enemy has had their turn, this routine checks to see if either the enemies or the party have been wiped out. If the players are dead, then Lost is made true. If the enemies are dead, then Won is made true.

IF EnemyAlive%(1) = False AND EnemyAlive%(2) = False AND EnemyAlive%(3) = False AND EnemyAlive%(4) = False THEN
Won = True
ELSEIF PlayerAlive%(1) = False AND PlayerAlive%(2) = False THEN
Lost = True
END IF

Next, we check the situation (Players won, lost, ran away, or still fighting) and redraw the battle screen based on what has happened. After this is done, or DrawNeed% variable is reset to 0 again.

IF Lost = False AND Won = False AND RanAway = False THEN
DrawBattleScreen DrawNeed%
ELSE
DrawBattleScreen 2
END IF
DrawNeed% = 0

Finally... This whole battle loop continues until something occurs to end the battle. This will be the players winning, losing, or running away. If any of these happen. The engine exits the loop.

LOOP UNTIL Lost OR Won OR RanAway

Once the loop is exited, the engine determines what has happened. If the players lost, a losing message is displayed. If they ran away, a message is displayed saying so. If they won, a message is displayed saying so, then the total gold and experience they won is calculated, displayed, and added to the living characters totals.

IF Lost THEN
LOCATE 2, 2: PRINT "Annhiliated..."
ELSEIF RanAway THEN
LOCATE 2, 2: PRINT "You got away!"
ELSEIF Won THEN
LOCATE 2, 2: PRINT "Victory!"
WHILE INKEY$ = "": WEND
TotalEXP% = EnemyEXP%(1) + EnemyEXP%(2) + EnemyEXP%(3) + EnemyEXP%(4)
FOR I = 1 TO 2
IF PlayerAlive%(I) THEN PlayerEXP&(I) = PlayerEXP&(I) + TotalEXP%
NEXT I
DrawBattleScreen 2
LOCATE 2, 2: PRINT "Gained"; TotalEXP%; "experience."
WHILE INKEY$ = "": WEND
TotalGold% = EnemyGold%(1) + EnemyGold%(2) + EnemyGold%(3) + EnemyGold%(4)
DrawBattleScreen 2
LOCATE 2, 2: PRINT "Gained"; TotalGold%; "gold."
END IF
WHILE INKEY$ = "": WEND
END SUB

The ChoiceBox sub.
This sub is used to draw the box in the upper left corner of the battle screen. It is used to show the names of the enemies that are alive and also used to create the menu for choosing a character's course of action.

Notice that a BoxType% variable is passed to it. This tells the sub which type of box to draw. If a box with the enemy names is needed, it is called like this: ChoiceBox 0 If a box with the player's choices is needed, it is called like this: ChoiceBox 1

SUB ChoiceBox (BoxType%)

LINE (0, 0)-(120, 50), 23, B
LINE (1, 1)-(119, 49), 25, B
LINE (2, 2)-(118, 48), 24, B
LINE (3, 3)-(117, 47), 0, BF

IF BoxType% = 0 THEN
IF EnemyAlive%(1) THEN
LOCATE 2, 2: PRINT EnemyName$(1)
END IF
IF EnemyAlive%(2) THEN
LOCATE 3, 2: PRINT EnemyName$(2)
END IF
IF EnemyAlive%(3) THEN
LOCATE 4, 2: PRINT EnemyName$(3)
END IF
IF EnemyAlive%(4) THEN
LOCATE 5, 2: PRINT EnemyName$(4)
END IF
ELSEIF BoxType% = 1 THEN
LOCATE 2, 5: PRINT "ATTACK"
LOCATE 5, 5: PRINT "RUN"
END IF

END SUB

The DrawBattleScreen sub.
This sub is used to draw the battle screen, and all of the characters which are currently on it.

Notice that the ScreenType% variable is passed to it. This is used to determine what parts of the screen to draw.

SUB DrawBattleScreen (ScreenType%)

DEF SEG = &HA000
IF ScreenType% = 0 THEN
ChoiceBox 0
StatsBox
ELSEIF ScreenType% = 1 THEN
BLOAD "back1.bsv"
GET (PlayerX%(1), PlayerY%(1))-(PlayerX%(1) + 31, PlayerY%(1) + 31), BackSprite%(0)
GET (PlayerX%(2), PlayerY%(2))-(PlayerX%(2) + 31, PlayerY%(2) + 31), BackSprite%(514)
ChoiceBox 0
StatsBox
ELSEIF ScreenType% = 2 THEN
BLOAD "back2.bsv"
END IF
DEF SEG

IF ScreenType% < 2 THEN
Yellow = PlayerGo%(1) + 128
IF Yellow% > 228 THEN Yellow% = 228
IF Yellow% > 128 THEN LINE (128, 16)-(Yellow%, 18), 44, BF
IF Yellow% < 228 THEN LINE (Yellow% + 1, 16)-(228, 18), 40, BF
Yellow = PlayerGo%(2) + 128
IF Yellow% > 228 THEN Yellow% = 228
IF Yellow% > 128 THEN LINE (128, 32)-(Yellow%, 34), 44, BF
IF Yellow% < 228 THEN LINE (Yellow% + 1, 32)-(228, 34), 40, BF
END IF

FOR I = 1 TO 4
IF EnemyAlive%(I) THEN
PUT (EnemyX%(I), EnemyY%(I)), Enemies%(514 * 4), AND
PUT (EnemyX%(I), EnemyY%(I)), Enemies%(514 * EnemyType%(I)), XOR
END IF
NEXT I

FOR I = 1 TO 2
IF PlayerAlive%(I) THEN
PUT (PlayerX%(I), PlayerY%(I)), Players%(514 * 6), AND
PUT (PlayerX%(I), PlayerY%(I)), Players%(514 * PlayerType%(I)), XOR
END IF
NEXT I

END SUB

The GetHandLocation sub.
This sub is used to determine where to draw the hand pointer when the player chooses to attack an enemy. Notice that, it will check to see which enemy is alive before giving values to HandX% and HandY%. This is done so that the hand is never pointing to an enemy which is no longer there.

SUB GetHandLocation

IF EnemyAlive%(1) THEN
HandX% = 8: HandY% = 85
ELSEIF EnemyAlive%(2) THEN
HandX% = 114: HandY% = 85
ELSEIF EnemyAlive%(3) THEN
HandX% = 8: HandY% = 135
ELSEIF EnemyAlive%(4) THEN
HandX% = 114: HandY% = 135
END IF

END SUB

The InitBattle sub.
This sub is used to determine four enemies for the battle and to load their stats into the respective variables. Here, you can change the enemy names, as well as their statistics. You can make tough enemies, or weak enemies using this sub.

REM $DYNAMIC
SUB InitBattle

FOR I = 1 TO 4
EnemyType%(I) = INT(RND * 4)
IF EnemyType%(I) = 0 THEN
EnemyName$(I) = "Green Goblin"
EnemyMaxHP%(I) = 20: EnemyMaxMP%(I) = 5
EnemyST%(I) = 3: EnemyDF%(I) = 2: EnemyAG%(I) = 3
EnemyMS%(I) = 1: EnemyMD%(I) = 3
EnemyEXP%(I) = 10: EnemyGold%(I) = 4
EnemyAlive%(I) = True
ELSEIF EnemyType%(I) = 1 THEN
EnemyName$(I) = "Blue Goblin"
EnemyMaxHP%(I) = 20: EnemyMaxMP%(I) = 25
EnemyST%(I) = 2: EnemyDF%(I) = 1: EnemyAG%(I) = 4
EnemyMS%(I) = 5: EnemyMD%(I) = 4
EnemyEXP%(I) = 14: EnemyGold%(I) = 6
EnemyAlive%(I) = True
ELSEIF EnemyType%(I) = 2 THE

Quick! Spot the stoner rock referance in the next line! :)

EnemyName$(I) = "Orange Goblin"
EnemyMaxHP%(I) = 40: EnemyMaxMP%(I) = 0
EnemyST%(I) = 5: EnemyDF%(I) = 3: EnemyAG%(I) = 2
EnemyMS%(I) = 1: EnemyMD%(I) = 1
EnemyEXP%(I) = 18: EnemyGold%(I) = 10
EnemyAlive%(I) = True
ELSEIF EnemyType%(I) = 3 THEN
EnemyName$(I) = "Purple Goblin"
EnemyMaxHP%(I) = 25: EnemyMaxMP%(I) = 15
EnemyST%(I) = 3: EnemyDF%(I) = 3: EnemyAG%(I) = 3
EnemyMS%(I) = 3: EnemyMD%(I) = 3
EnemyEXP%(I) = 16: EnemyGold%(I) = 8
EnemyAlive%(I) = True
END IF
EnemyHP%(I) = EnemyMaxHP%(I): EnemyMP%(I) = EnemyMaxMP%(I)
NEXT I

END SUB

The InitRandomStats sub.
This sub is used to create random stats for the players just for the puprose of this engine. Most of these stats you will not need to use for your battle engine. They are there simply to give some usable character statistics in battle. You will want to replace these with your own character stats.

SUB InitRandomStats

PlayerName$(1) = "DarkDread": PlayerName$(2) = "Tyranny"
PlayerAlive%(1) = True: PlayerAlive%(2) = True

PlayerMaxHP%(1) = 52: PlayerMaxHP%(2) = 37
PlayerMaxMP%(1) = 11: PlayerMaxMP%(2) = 31

PlayerHP%(1) = PlayerMaxHP%(1): PlayerHP%(2) = PlayerMaxHP%(2)
PlayerMP%(1) = PlayerMaxMP%(1): PlayerMP%(2) = PlayerMaxMP%(2)

PlayerST%(1) = 7: PlayerST%(2) = 5
PlayerDF%(1) = 4: PlayerDF%(2) = 0
PlayerAG%(1) = 4: PlayerAG%(2) = 3
PlayerMS%(1) = 4: PlayerMS%(2) = 8
PlayerMD%(1) = 5: PlayerMD%(2) = 9

PlayerType%(1) = 0: PlayerType%(2) = 3

These next lines, you may wish to keep. They tell the engine where to put each enemy and character when drawing them.

EnemyX%(1) = 25: EnemyY%(1) = 75
EnemyX%(2) = 125: EnemyY%(2) = 75
EnemyX%(3) = 25: EnemyY%(3) = 125
EnemyX%(4) = 125: EnemyY%(4) = 125

PlayerX%(1) = 275: PlayerY%(1) = 75
PlayerX%(2) = 275: PlayerY%(2) = 125

END SUB

The InitSprites sub.
This sub is used to load the enemy, player, and hand pointer graphics into the arrays. These arrays are then used to display the necessary graphics by the battle engine.

SUB InitSprites

DEF SEG = VARSEG(Players%(0)): BLOAD "players.spr", VARPTR(Players%(0)): DEF SEG
DEF SEG = VARSEG(Enemies%(0)): BLOAD "enemies.spr", VARPTR(Enemies%(0)): DEF SEG
DEF SEG = VARSEG(Hand%(0)): BLOAD "handy.spr", VARPTR(Hand%(0)): DEF SEG

END SUB

The StatsBox sub.
This sub draws the player statistic box in the upper right corner of the screen. It is called from the DrawBattleScreen sub when the player's HP/MP have changed and need to be displayed. You'll also notice a calculation which checks to see if a player has 25% or less HP and changes the colour of the HP/MaxHP to a bright red if this is so. It is done this way to give the player a warning when the HP of one of the characters is falling to dangerous levels. You will also notice that the MP is displayed in a bright green colour. This is to easily differentiate between the HP and MP. Handy no?

REM $STATIC
SUB StatsBox

LINE (121, 0)-(319, 50), 23, B
LINE (122, 1)-(318, 49), 25, B
LINE (123, 2)-(317, 48), 24, B
LINE (124, 3)-(316, 47), 0, BF

LOCATE 2, 17: PRINT PlayerName$(1)
IF PlayerHP%(1) / PlayerMaxHP%(1) < .25 THEN COLOR 40
LOCATE 2, 30: PRINT USING "HP ###/"; PlayerHP%(1)
LOCATE 2, 37: PRINT USING "###"; PlayerMaxHP%(1)
COLOR 47
LOCATE 3, 30: PRINT USING "MP ###/"; PlayerMP%(1)
LOCATE 3, 37: PRINT USING "###"; PlayerMaxMP%(1)

COLOR 15
LOCATE 4, 17: PRINT PlayerName$(2)
IF PlayerHP%(2) / PlayerMaxHP%(2) < .25 THEN COLOR 40
LOCATE 4, 30: PRINT USING "HP ###/"; PlayerHP%(2)
LOCATE 4, 37: PRINT USING "###"; PlayerMaxHP%(2)
COLOR 47
LOCATE 5, 30: PRINT USING "MP ###/"; PlayerMP%(2)
LOCATE 5, 37: PRINT USING "###"; PlayerMaxMP%(2)
COLOR 15

END SUB

The TimerDelay sub.
This sub is used as a delay sub. It is much more precise than the SLEEP command, as is not system dependant like a FOR...NEXT delay. Of course, there are much more precise delay routines which you can do, but this one is good enough for the battle. One thing you may wish to add to this, is a small routine which checks for midnight rollover. I've left it up to you if you wish to include the midnight rollover check.

SUB TimerDelay (Seconds!)

CurrentTime! = TIMER
WHILE CurrentTime! + Seconds! > TIMER: WEND

END SUB

Putting it all together.
Well... Now that you have an idea of how this engine works, it's time to put it all together. If you look at the battle.bas file from the zip included with this routine, you'll see how it all looks in QB.

Basically, what you'll want to do, is take the stuff that's in the main part of the battle.bas file, and copy it over to your RPG. Of course, don't forget to change anything you may need to change. Then, copy all of the subs to your RPG as well. Now you're one step away from having semi-active battles in your RPG!

This is the easy part... Below, I've given you an example of how to make the random battles work in your RPG. You will likely have to change some lines, but this is the basic idea. Just put this code in your main loop:

RANDOMIZE TIMER
Fight% = INT(RND * 10) + 1
IF Fight% = 5 THEN
Battle
END IF

There you go. That's all you need to get started. Should you have any problems with the engine, feel free to e-mail me at darkdread@hotmail.com and I'll be happy to help you out. Good luck with your RPG!

Next time (and there WILL be a next time, mwuahahah!).

Well... These tutorials are written for YOU. So tell me what you want to read about next. Do you want to know how to create an Eye of the Beholder/ Legend of Lith 2 style 3D engine... or something else?

Incidentally, if you wish to check out the Serenity homepage for some free RPGs written in QB, you can go here

Cheers!

Do you suggest that DD cover the FREE Semi-Active Modulating Action battle engine? Tell him here.

 

>>>Back to Top<<<