|
You chose the best - here they are
Favourite Game | Last Month | Change
1. Wetspot 2 1 <>
2. Dark Ages (t) 2(t) <>
2. Mono. Shooter (t) 2(t) <>
4. Of Hell & Magic (t) -- --
4. Shadow of Power (t) -- --
Comments: Two new titles crack the list, while cgi voting
means a large increase overall in terms of total votes.
Favourite Utility | Last Month | Change
1. DirectQB 1 <>
2. PP256 2(t) <>
3. GSlib (t) -- --
3. zsvga (t) -- --
3. QMIDI (t) -- --
3. Dash (t) -- --
Comments: DirectQB dominates, Dash and GSlib rejoin
the list, and PP256 does very well again.
Best Upcoming | Last Month | Change
1. Dark Ages 2 1(t) <>
2. ProjectRT 1(t) D1
3. Freelancer (t) 1(t) D2
3. Agent Squeak 2 (t) -- --
3. Glib (t) -- --
Comments: Dark Ages 2 is once again in sole possession
of 1st, while a variety of titles from Oham2 to
Senseless Violence to Groov 2.
|
must downloadsthe editor's top ten |
You need
this stuff! |
We've got a slightly different format in the Must Downloads this month. From
now on, we'll be displaying the 10 programs you *need*, and that's all. And the
ratings will go in terms of pure playability or usefulness, not nostalgia. So
here's the new list (in no particular order)! Oh, and if you see somethin' missing,
write in and tell me!
| |
| |
Absolute Assembly Petter Holmberg of Enhanced Creations's assembly
converter. By typing in normal asm, this proggy will convert the asm into
goods that qb will understand. Super-spiffy!
| | |
Dark Ages One of the most engaging QB games ever, as well
as one of the only complete rpg's. This was featured in PC Gamer! Check
it out!
| | |
Monospace Shooter
Gradius' 2 color side-scrolling space shooter. Featuring very
detailed enemies, flickerless animation and a devious AI, this
game is a classic.
| | |
Wetspot & Wetspot 2
Wetspot, the bubble-bobble like block-pushing action game was one
of the best QB games when it came out, but W2 is just incredible.
Super midi sound, great fast graphics, tons of variety, an insane
number of levels...everything you could want. GET this game. Now.
| | |
SFB2
The BEST qbasic fighter. Ever. Even though it's wireframe, it
has cool particles and smooth animation as well as rippin' gameplay.
| | |
PP256
Called the best tile editor in QB ever, PP256 has loads of tile-editing
options at your disposal. If you use tiles in your game, you can't live
without this.
| | |
DirectQB
The library that has it all: .fli players, sound, super-speedy graphics, special
effects, the works.
| | |
Shadow of Power
A rockin' new RPG from German group MasterCreating, this game has humour and SNES style
production value.
| | |
QMIDI4.1
This is the best version of Qmidi. Play .mid's in your game! The
new qmidi4.1 rules! It has tons of features and a "light" version.
Get it now!
| | |
Of Hell and Magic
An RPG by Progman, featuring smooth-scrolling, a fantasy story, and Zelda: Link to the Past
style gameplay.
| |
site of the monthgo here |
in-boxgood and bad (pasco edition) |
A R R A Y
|
Pete's QB Site
Sites usually don't do this. Y'know, go from nothing into gigantic in
just a few months. But Pete's done it. Files. Links. Tutorials. Daily
Updates. An easy to use design. It's *all* here.
The pure enormity of the site is what makes it so interesting. Literally
hundreds of links. Tons of programs, most of which have a short review
accompanying. Many of the visitors also submit reviews frequently for the
site. Go there. Now.
|
good
No html on the major webboards has meant less annoyance so far from
people writing huge lettered posts or bolding the next 10 pages.
bad
People still can be annoying in their posts. "Kum see my kikin gam!" 20
times over sucks.
|
A R R A Y
|
>>>Back to Top<<<
advertisement
By zkman
|
One year.
12 issues.
Too many articles to count. From assembly to RPG Scripting, to your first looks at DA2 and ProjectRt, among
others, to art, music, and more, we've covered a ton in that 1 year. And it's not just us that have brought
ya a lot in the last year: the qbasic community has been more active than at any point ever before. Don't
believe me? Let's take a walk down memory lane, and see what's gone on in the qb community each season since August 1998.
Fall 1998
Summer was winding down, qbasic.com was still being updated with regularity, and Angelo Mottola, recently
revered with his creation Wetspot 2 that spring, announced on his page that he had "big plans for a DirectQB library".
Blast! was then the most-used, and Dash was gaining subscribers, although no one could've guessed just how
pertinent to the future of qbasic those "big plans" would end up being.
SFB2, still the best fighter in qbasic, with a primitive-based graphics system, great particle support,
and a fab roto-zooming camera, became a splash in this season also. Danny Gump released a build of Super
Mario World Clone, showing that Super Nintendo quality games were by all means possible in qbasic today.
But all was not well in the qbasic community. The newly-christened (and short-lived) QBVoice
broke news of an attempted sabotage of Dash by implanting a virus into the file caused qb'ers to
question for the first time the safety of the .exe's compiled in qb on the internet. Tsugumo's
heralded graphical acheivement in TheGame and big plans for the game were sidetracked as he cancelled
the project. Apester disapeared from the qb scene, in all respects. And the IDSA sent letters to top qb sites including the qbt50 that they would no longer
be able to post links to or post quickbasic4.5 on threat of having the site license revoked. As
we know now, that threat never carried thru, but it made it hard to find for those who had "lost
their only copy" at the time.
Most saddening though, was the loss of arguably one of the greatest qbasic coders ever in Milo
Sedlacek, who passed away as a result of a chronic illness on September 24, 1998. Milo, aka Gradius,
is known for many of his programs, most notably Monospace Shooter and the never-completed RPGDemo4,
both amazing technology demos for qb in the pre-library days. But happier days would soon follow,
as we entered the frosty cold of...
Winter 1998/1999
As winter rolled along, all of America was focused on Slick Willy and the White House Three; all except
the qb community was hearing enough good news to "blow off" the allegations, so to speak. With Winter
beginning, DA2 production also started. But, almost as importantly as this, were the following
words from the November issue of the mag:
"More interestingly, we've picked up some rumours about Enhanced Creations future plans (cough...next game...cough)"
Of course, this game ended up being "Project RT", still one of the most hyped and most secretive qb games in the works (despite
the first screens which we procured for this spring's issue). Speculation about what the game was ranged from a Massive Multiplayer RPG
to Wetspot 3D to a Real Time Strategy title. Amazingly, what's known about the gameplay of the game is almost as vaulted over at
Enhanced now as it was then.
With Christmas break, us coders were enjoying the frosty snow...by looking out the window near the computer, of course. Who has
time to trim the tree when announcements such as "a new Vertical Scroller by Pasco", "a super-secret, rocking demo called Xeno", Puz
and DA2 shots were coming thru the pipes? Recollecting back, we'll also notice the December issue commented on a "secret graphics
library from LordQB". Could this be the LiquidGL from News this month?
And even with the holidays passing, new paradigms in qbasic coding such as uNESs, the first Nintendo emu in qbasic, or
LTPB, Interplay's new beginner level BASIC language proved that qbasic was far from dead. But the frivalty of the holidays
proved no match for...
Spring 1999
Spring is conventionally "lull-time" as far as qb code is concerned, and although no earth-shaking programs came out, there was
certainly nothing to complain about in terms of the qb community.
Most of the talk during this month focused on speculation about what would win at Sonicblue's Qlympics. The 1st Qlympics
were tainted by allegations of cheating, and Sonic assured us he was doing all he could to prevent that from happening. In the
end, there weren't many major upsets in any category, with big name titles like Dark Ages and Wetspot 2 easily conquering
their category, as had been suspected.
PP2, the Labryinth and MOTG Project all got their fair share of publicity, either due to the demos that were released or the
name recognition of their creators. And, as luck would have it, they've all panned out thus far to be equals in the regality they
recieved, most notably thus far The Labyrinth by Seav. The only major First Person Shooter in qb so far, progress continues on
it at a good clip, and it already plays very well.
Much chatter was also diverted to the future of the QBIDE. The memory and speed problems of qb have hindered the language
from being anything but a hobbyist language since it's creation, but plans such as QBCC by Leroy threatened to change that
distinction. QBCC is planned to work by taking qb code and "converting" it into a DJGPP file, which would give it muchisimo speed.
Not only that, there were, and still are, plans to make an alternate IDE that is Windows based, to give you the ability to have multiple
progs open at the same time.
All was not well, though. Viruses hit many qb coders, including Angelo Mottola and myself, which is why qb:tm reccomends
you never accept a file from someone you don't know except in .bas form. Nekro's game Killerz stirred up controversy among some,
much as WoS before it, because of it's content. Nekro, true to form, was not swayed by the commentary, standing up for his games
whenever possible.
Finally, our April Fools joke about Project RT being an Action Strategy RPG starring Cuby and Coby was not, err, picked up by some,
leading to boards being peppered questioning whether it was, in fact, an Action Strategy RPG.
Summer 1999
The summer of '99 is by no means over yet, but it has already become one of the greatest for qb enthusiasts. Shadow of Power
and Of Hell and Magic, two acclaimed RPG's, were both released, and both offered quality as high or higher than Dark Ages, the benchmark
for programs of that variety thus far.
Dmitry Brant and Petter Holmberg showed that they were the most 1337 of coders by displaying (relatively) fast jpeg viewing in qbasic.
Jpegs, are, of course, one of the most difficult types of files to decode, due to the complex compression they use. The future of Qb
also looked to have been shown by two libraries, and one future one, in Gslib, FutureLibrary and Glib.
QB's next year promises to be as great as the one before. The mainstream considers qbasic all but dead, but the last few pages have
shown what a group of quality coders can accomplish. As has been said before, the difference between good and bad code doesn't
hedge on the language. See ya next year!
|
A N N I V E R S A R Y !
|
  |
Did you think Pascal "rox qbee nyday!"? Lemme know. |
| |
>>>Back to Top<<<
V I E W B O X |
DA2 shows off layering from trees and a gorgeous hand-drawn border
|
V I E W B O X |
>>>Back to Top<<<
By Alias
A L T E R N A T E P R O J E C T I O N |
Wow, since our start 3 months ago we have covered literally most every aspect of voxel animation! There is but one area left I want to cover: translucency. By now, if you have followed us through you may have tried your hand a creating your own voxel program to suit your fancy, and if you've tried transparent things you may have encountered a problem. Because the drawn portions of voxels overlap, the drawing gets uneven and in some places can even look plaid or worse. Problem? You bet.
Voxel Render problems
- Solidity Make your voxel objects solid. This does not really fix the problem, but it can hurt framerates. Make your voxel model solid rather than hollow, if it isn't that way already. This can make the problem less apparent by making the voxel model look more or less transparent in different areas in a different way. Make it look more deliberate, and thus better.
- Hollowness If that didn't work or made the problem worse, you can hollow out the voxel model and make it more evenly transparent. This can have drawbacks too, because if it tries to be evenly translucent but has overlapping and has plaid spots, it is a lot more apparent than with solid models.
- Sprite caching This method is very difficult to do without a library like DirectQB, as you have to hide the drawing of the voxel model from the screen, and doing so in pure QB is kind of hard. What happens is you draw the model before hand, on a separate layer, and then you GET it into a separate array and blend it into the scene. This has the disadvantage of being slower, more memory-consuming, and much less versatile.
- Cuisineart Maneuver Change your blender map. I have found that using an additive or subtractive blender map instead of an average or weighted average map helps the appearance of blended voxel maps. This helps even more if you use it with the solid method above.
- Deal with it! Sometimes imperfection can be left in with no ill effects. Look at Quake. By all means, that game should not have left the shelf for all the graphical glitches. It is, mathematically, so incorrect that it's amazing it's even playable. Why does no one notice this? Because the graphics are done just barely well enough that the human eye can't see the errors unless you specifically look for them. If you make your action fast enough, it's possible that no one will ever notice. Michael Abrash (ASM programming God) wrote "Motion and fast action can surely cover for a multitude of graphics sins."
On to alternate projections
This article will deal exclusively with isometric projection because, of all the 3D projection methods out there, this is one of the best. It has the advantage of being quicker to calculate, and very easy to see and understand. The major downside is that it is very possible to make it look like something is in the wrong place - which is very annoying when you're trying to play a game! If you don't know how far you have to make the jump, you can't know if you're going to make the jump. I'll explain more later.
In the first installment of MA*SNART's 3D series, he gave us this algorithm for 3D projection:
x' = (((x - vx) / z) * 90) + vx
y' = (((y - vy) / z) * 90) + vy
(I will call this FPS projection from now on, as that is what it is primarily used for. That is, ALL FPS games use this formula.)
Where x' and y' are the projected coordinates of any point and vx and vy are the coordinates of the pixel in the middle of the screen. (Or they should be at least - if you ever have some free time you can play around with these and have some REAL fun.) The formula for isometric projection goes like this:
x' = (x - y) + vx
y' = ((x + y) / 2) + z
As you can see there is only one divide and everything else is add / subtract, making for a very fast algorithm. It's also amenable to all the same deformations FPS projection is - whatever you can do to a point there you can do to a point here and it generally looks just as cool. (The only thing I really miss is having the main character throw a missile right at the screen, and freeze-framing when the tip of the missile is so close to the nose of the player that my pentium's FPU can't tell they're apart at all so the missile is left there waiting to explode, and the player by this time has wet his pants because he _FINALLY_ made that big jump across the canyon and now, before he could hit quicksave, he's being destroyed by a pentium FP bug!!! But I digress.)
The problem with this I mentioned earlier - it often makes things appear in the wrong position. Take this example:
| |
That looks, in the isometric world, like a long line of blocks but below is what it really is |
| |
Screenshot taken from Attack of the Blobeteers |
| |
  |
Que? Believe it! The projection model has that one major flaw, that something in one position can appear to be in another. There is a very simple, very elegant way around it though - it's called rotation. Yes, it's true, if you rotate every point around the Z axis -8 degrees, it will make everything unmistakeable. Here's the formula to do that:
x' = cos(-8) * x - sin(-8)
y' = sin(-8) * y _ cos(-8)
Please note that the -8's in there need to be changed to -8 * (3.14159 / 180) because theta is in a different measure than degrees.
Now that you understand projection in Isometric form and everything there is to know about voxels, my work here is done. On the 23d of this month you will see a fine example of isometric perspective, that is, a demo of my game, Attack of the Blobeteers. Thanks for reading and I'll see you on the release date.
Alias doesn't know that rotated along the sine wave, z' = w* of the quaternion. |
| |
>>>Back to Top<<<<
By MA*SNART
I N D E X |
There you are: your cool new vertical shooter game is about done. You've used every trick you know about to get it where it is today. You have an array for the players bullets, another for the enemy's shots and yet another for the enemies themselves. But you have a problem... the frame rate, while consistant, is slow [even with the help of tons of assembly]. Well it's time to look at your game object routines...
A game object routine is just a catch all term for game specific routines that involve the interaction of the various game pieces. An example of such a routine would be the way by which you check for collision between enemy fighters and players bullets. And more specifically how enemies and bullets are 'spawned'.
An example of a 'spawning' player bullet routine would involve calculating where the bullet will be on screen then placing these and other values into the 'bullet array'. The problem is figuring out which array subscript is 'open' or 'good' in order to place the stats for the bullet. A typical 'bullet array' may have a variable that indicates if the particular bullet is 'alive'. The usual way to do this is by scanning through the array with a FOR/NEXT loop looking the first 'dead' one you find.
This will seem very fast if, say, you only have 10 possible bullets to check. But look again. For every frame you would need to 'scan' the array for all the 'alive' bullets just to be able to draw them! Then again for each enemy that might be in contact with them! And again just to move them for the next frame!
If only one enemy is onscreen and you limit the bullets to ten max but only have one bullet that is 'alive'... that means for every frame you have to 'scan' the array 3 times!. If you have 20 onscreen bad guys and still only one bullet... your scanning 22 times [or to put it another way... your checking 220 times per frame if a bullet is 'alive']! Now you know why your game engine is so slow!
But what to do about it? Well you could use several index lists.
Index Lists
An index list is a simple 'list' of 'array subscripts' [or 'indexes']. You can have a separate index list for all the 'alive' bullets, the 'dead' ones, 'alive' enemies, and 'dead' ones too. In fact you could have a separate index list for just about anything you want in relation to a particular array.
The great thing about index lists is that they contain all the array subscripts you need for a particular routine. You no longer need to 'scan' a whole array to find the 'alive' from the 'dead'. In the example above, one bullet and 20 enemies might mean that you have 'scanned' the array 220 times per frame. With a index list of 'alive' bullets and 'alive' enemies you are no longer 'scanning' but instead you are working with only the 'alive' bullets and enemies.
Great, so how do I make an index list in QB?
Pure and simple... variable length STRINGs. That and the assorted QB functions like MID$, CVI, ASC, and MKI$ [NOTE: use of PEEK and POKE will give faster results, but for the sake of clarity I'll use MID$ and the others for this article].
Okay here are some routines for a 'bullet' array:
CONST maxbullets = 10
'bullet data type that contains
'x/y location and kind of bullet
TYPE bulletdata
x AS INTEGER
y AS INTEGER
kind AS INTEGER
END TYPE
' here is the bullet array
DIM SHARED bullets(maxbullets) AS bulletdata
' here are the index lists.. one for "alive bullets" [balive]
' the other for "dead" ones [bdead]
DIM SHARED balive AS STRING
DIM SHARED bdead AS STRING
The first thing you want to do is "initialize" the "dead" array, like below.
This is done so that the index list is accurate in that all the bullets are
effectively "dead" when the game starts.
FOR i = 1 TO maxbullets
bdead = bdead + MKI$(i)
NEXT i
'
'your game code here :P
'
These subs are used to "spawn", "kill", "hit detect"[hdbullet] the bullets
SUB spawnbullet( x%, y%, k%)
' spawns a bullet... just pass the
'location x/y and kind of bullet and thats all you have to do
This works by grabbing and removing the first "dead" bullet from the bdead STRING. It then adds it to the end of the balive string, thus it becomes active
and it then enters the data into the array.
IF (LEN(balive)/2) < maxbullets THEN
index$ = MID$(bdead,1,2)
bdead = MID$(bdead,3,LEN(bdead))
balive = balive + index$
v% = CVI(index%)
bullets(v%).x = x%
bullets(v%).y = y%
bullets(v%).k = k%
END IF
END SUB
This removes a bullet from the balive STRING and adds it to bdead, thus
the particular bullet becomes inactive.
The variable passed to this SUB is the actual array subscript; however,
this SUB is only intended to be CALLed by the collision detection routine
named "hdbullet" or any routine [like movement] that will need to remove
the bullet for some reason.
SUB killbullet(v%)
index$ = MKI$(v%)
IF index$ = MID$(balive,1,2) THEN
balive = MID$(balive,3,LEN(balive))
ELSE
IF index$ = MID$(balive,(LEN(balive)-2),2) THEN
balive = MID$(balive,1,LEN(balive)-2))
ELSE
FOR i = 3 TO LEN(balive) STEP 2
test$ = MID$(balive,i,2)
IF index$ - test$ THEN
balive = MID$(balive,1,(i-1)) + MID$(balive, i+2, LEN(balive))
EXIT FOR
END IF
END IF
END IF
bdead = bdead + index$
END SUB
This sub is where you will do hit detection with the enemy array subscript
enum%, as most of this will really depend on the particular method you use for collision detection and etc..
My main reason for including this in the article is to show how to use a FOR/NEXT loop with the index list; by replacing the core "hit detection" routine inside the FOR/NEXT loop, you can replace it with sprite drawing routines,etc.. Just remember to use index% as the array subscript and not "i"
SUB hdbullet(enum%)
FOR i = 1 TO LEN(balive) STEP 2
index% = CVI( MID$(balive,i,2))
' IF bullets(index%) CONTACTS ENEMY(enum%) THEN
' CALL killbullet(index%)
' CALL killenemy(enum%) if need be....
' END IF
NEXT i
END SUB
Okay those more advanced programmers out there will see that you can have one large "generic" array for bullets, enemies, et al. And simply use a bunch of index list STRINGs [possibly even in an STRING array!]. Doing this will reduce your needed index list management subroutines to only a handful [spawn, remove, drawing, movement, and hit detection]! Mmm... less code equals less work, and less code to debug! :)
I've used a vertical shooter as a example game that would be helped out by index lists. But in truth any game could easily use this technique! If memory is a problem [but speed isn't] you could store the contents of your array in a RANDOM access file and use the index lists to GET/PUT one array record at a time. This would save a TON of memory as only the index list STRINGs would be needed! Another thing you can do with index list is to sort them based on some value held in the array. This is cool because only the order of characters in the STRING needs to be changed [and using a "radix" type sort programmed in Qbasic, it only takes less then a 10th of a second to sort 3,000 items on my P-150!].
So now you, hopefully, know some ways to optimize your code. If not, then I hope that you have learned of a new way to use STRINGs for things beside holding text...
Happy progging :)
Email MA*SNART telling him how you used a Reverse-culling bubble LZW Index in your game at
this address. |
| L I S T S |
>>>Back to Top<<<
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%
|
|
  |
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<<<
By Seav
P O W E R
P R O G R A M M I N G
2 |
Hello there! Here comes the second installment of my power programming article. This time around, I'll be teaching you of the many ancient and arcane ways of implement timing into your programs!
What the hell is timing? Let me answer by giving you some applications. For instance, you can insert precise delays while running cut scenes. You can also display your program's frame rate to see if it's competing on a level with snail races. You can also do a synchronization technique popularly used in low-level music programming.
QBasic commands that do time
There are a lot of commands and statements in QBasic that can be used for timing applications. But in my opinion, all you ever need is the TIMER function. I'll tell you why by degrading the other statements. =}
The ON TIMER statement, and it's sister statements, TIMER ON, TIMER OFF, and TIMER STOP, certainly makes timing very easy for newbies. But if you're advanced enough to actually bother with timing, then you should leave these statements alone. They slow down your program and when you compile it, they bloat the size up.
Another command that QBASIC has is the SLEEP statement. SLEEP is essentially a delaying statement, but it delays in only 1-second increments, and when a person presses a key, the timing is skipped. Also, the keystroke that ends a SLEEP isn't flushed from the keyboard buffer. So if you use SLEEP as a "Press any key to continue" command, you'll eventually get keyboard beeps, especially if you don't use INPUT$ or INKEY$ to relieve the keystrokes.
Don't bother anymore with TIME$. It's only useful purpose is to display and change the date. In order to do calculations with time using TIME$, you still have to mess around with string functions like MID$ and VAL (which can be very slow). Besides, you can easily compute the hour, minute, and second using TIMER.
My verdict? TIMER is possibly the best command to use for all your timing processes. One major problem with TIMER is that it only has a frequency of 18.2 Hz (ticks per second). This means that you can only time up to around five-hundredths of a second. This may seriously affect your timing, especially if you don't know about it.
Basic timing principle
Imagine that you have a broken ruler. The end with the zero mark was broken off by your younger brother, so that means that the ruler is useless, right? Well, you can use the ruler to spank your errant brother. Err...what I mean is, even if the end is broken, as long as the ruler is longer than the things you want to measure, you can still use it to measure the length of things. You just line up the object of choice along the ruler and record the points where the object starts and ends. The difference is the length.
The same principle is used for timing inside QBasic. You just record the time when the thing you want to time started, and record the time when it ended. The time elapsed is then the difference between the two recorded times.
Start! = TIMER
.
. Some part of the program you want to time
.
Finish! = TIMER
PRINT "Time Elapsed: "; Finish! - Start!
It's that simple. This basic principle is used in almost all applications that involve the TIMER command. You can use this principle to compare similar routines and see which is faster. This is called benchmarking.
Degree of error
Now I'll show you why the frequency of the TIMER is an important issue when you're dealing with timing applications. If the frequency or resolution of your timing device is very low, the larger the degree of error you may get when timing events.
I'll illustrate this point with an extreme example. Let's suppose that your digital watch only tells you the time in minutes (no seconds). And you wish to see how long a TV commercial lasted. When the commercial starts, you record the time on your watch: It's 8:25. When the commercial ends, you find out that the time is 8:27. So the commercial lasted 2 minutes, right? Wrong. The commercial lasted for 2 minutes, plus or minus 1 minute. Why plus or minus a whole minute? Well, the commercial could have actually started at 8:25:59 and ended at 8:27:00, in which case, the commercial actually lasted for 1 minute and 1 second. You could go the other way around. If the commercial started at 8:25:00 and ended at 8:27:59, then the commercial lasted for 2 minutes and 59 secondsalmost three minutes! So with your weird watch, you could say that the commercial lasted from around 1 minute to 3 minutes. See my point?
Your watch has a frequency of 1 tick per minute or a resolution one minute while the TIMER function has a frequency of around 18.2 Hz (ticks per second), or a resolution of around 5 hundredths of a second. You might say that since the frequency of the TIMER function is high, then our timing will be precise. Not quite. If the event you're timing lasts on the order of milliseconds, then TIMER isn't precise enough for that. Just remember that the time results you will obtain will have an possible error of around 0.1 seconds.
Midnight Rollover Issues
Another thing you should beware of is the midnight rollover. At midnight, the TIMER function would wrap around from 86399.91 seconds to 0 seconds. So time calculations that span midnight would be wrong. Make sure that all your timing routines work at midnight especially if it's in a game. Otherwise, just avoid running the program near midnight.
Using TIMER to delay
A popular application of TIMER is to implement it as a delaying subroutine. Most programmers do their own variation of it, and here's my version:
SUB Delay (Secs!)
' Get time when the delay ends
EndTime! = TIMER + Secs!
' Adjust for midnight
IF EndTime! > 86400 THEN
EndTime! = EndTime! - 86400
END IF
' Delaying loop
DO
LOOP UNTIL TIMER >= EndTime! AND (TIMER - EndTime!) < 1
END SUB
You should avoid using an empty FOR-NEXT loop to delay the program, since the length of the delay created by the loop would vary according to the speed of a computer. What would be a one-second delay on a 286 might not be a delay at all on a high-end Pentium. I really wouldn't recommend it even if you perform a speed test at the start of the program to find out the computer's speed.
Frame rates
Ah, frame rates. One of the most often asked question in discussion boards. How does one compute frame rates or the FPS (frames per second)? To quote Entropy, "however you want." There are many solutions to this problem, and which one you choose depends on your program's particular needs.
A frame is essentially a single loop of code (usually one that includes drawing to the screen). By computing the frame rate, you are, in effect, computing how many loops the computer can perform in a given amount of time.
The first method of getting the FPS is very simple. You just time your whole program (while counting the number of frames executed) then divide the number of frames by the time, that's your FPS.
Start! = TIMER
DO
.
. Program loop
.
Frames& = Frames& + 1
LOOP UNTIL Finished
Finish! = TIMER
ProgramTime! = Finish! - Start!
PRINT "FPS:"; Frames& / ProgramTime!
This method can be very accurate especially if you run the program for a while. The problem with this method is that it only prints the FPS when the program is through. You have no way to know where your program bogs down.
Obviously the answer to the first method's inadequacies is to display the frame rate while the program is running. One method would be to time each frame and derive the frame rate from that. This isn't very nice since you'd only get a maximum of 18.2 FPS for slow programs while faster programs would give the "Divide by Zero" error. Why is that? Recall that the TIMER has a resolution of 5-hundredths of a second, which would give us 18.2 FPS. If your frames are faster than .05 seconds, then the frame would have an elapsed time of 0 seconds, giving a Divide by Zero error.
To get around this, instead of timing every frame, you can time a specified number of frames, and compute the frame rate from that. It would be vastly better, however, to display the FPS on a per-second basis. That is, instead of timing frames, you just count the frames that has been performed every second.
TimeNow! = TIMER
DO
.
. Program loop
.
Frames = Frames + 1
IF TIMER - TimeNow! >= 1 THEN
PRINT "FPS:"; Frames
TimeNow! = TIMER
Frames = 0
END IF
LOOP UNTIL Finished
I hope that you can follow the logic of the program code above and see how it manages to display the frame rate every second.
An alternative to TIMER
Sadly, TIMER does not fulfill the requirements of quite a number of programmers. The most disappointing fact of it is that it only has a frequency of 18.2 Hz. Many have tried to get around this by changing the frequency which is done by programming the PIT chip (the chip that handles the system time). However, changing the frequency without disrupting the system clock is a messy process (which involves programming the PIT chip, and redirecting interrupts). Also, this method does not seem to work inside Windows!
Fortunately, I have found a workaround. I have devised a routine called CLOCK which can have a frequency greater than 18.2 Hz. In my game, The Labyrinth, I use a variant of the routine to give me a frequency of 291.2 Hz. This has a resolution of 3 milliseconds (very adequate for my purposes). This routine does not mess up the system time and is very fast, since it uses integer math. The routine, however, does not return the number of seconds that have elapsed since midnight, but instead returns the number of "ticks" that have elapsed since midnight. The frequency of the ticks corresponds to the frequency of the routine. (In my game, the value increases by 291 every second.) Here's the routine (which has a frequency of around 4660 Hz):
FUNCTION CLOCK&
' Get the number of timer ticks at
' 0000:046C
DEF SEG = 0
Ticks& = PEEK(&H46C)
Ticks& = Ticks& + PEEK(&H46D) * 256
Ticks& = Ticks& + PEEK(&H46E) * 65536
DEF SEG
' Latch the counter and obtain the PIT
' countdown status.
OUT &H43, &H4
LSB = INP(&H40)
HSB = 255 - INP(&H40)
' Compute the CLOCK& value
CLOCK& = Ticks& * 256 + HSB
END FUNCTION
I can't explain in detail how the routine works. Suffice it to say that it "peeks" at the PIT chip and system timer status.
To verify that the program works, try the program below with the function above. Here the program divides the CLOCK value by its frequency, 4660.859, to see that it indeed matches the value provided by TIMER. The actual difference varies from 1 to 16 milliseconds. That's because the CLOCK updates faster than TIMER. Notice that the difference doesn't exceed the resolution of TIMER which is 54 milliseconds.
CLS
DO
TimerVal! = TIMER
ClockVal! = CLOCK / 4660.859#
LOCATE 1
PRINT USING "CLOCK: #####.###"; ClockVal!
PRINT USING "TIMER: #####.###"; TimerVal!
LOOP UNTIL LEN(INKEY$)
It would be a good idea to convert the function to assembly and to compile it in include it in a library, or as a string to be executed by CALL ABSOLUTE.
It's the end of the article as we know it =}
Due to space constraints, I can't possibly include every possible application of timing and I can't address all the questions that you might have regarding the subject. If you have any questions, comments, or stuff, just send me some mail at the address below. For now, that's it, and always remember...Time is Gold...=}
What Seav doesn't know is that we like programming the PIT Chip to mess with
ppl's computers. Give
him a whompin'
for thinking otherwise. |
|
|
>>>Back to Top<<<<
By Gavan
|
Welcome to the third art tutorial, written by…Me. This tutorial can be
read independently of the previous two, although it is suggested that you read
the others anyway, because they were written by Me (and you can pick up the
conventions used in My tutorials). Is there a slight air of egotism in this
article? =) This tutorial steps out of shading concepts and into the wonderful
world of tiling. Tiles are (usually square) blocks of art that can be
repititiously blitted to the game screen to make a world. The advantage to
using tiles, as opposed to using very large bitmaps depicting entire areas, is
that tiles take up much less space, and are easier to work with from a
programming perspective (testing collision, for example).
The downside to
tiles is that they are not entirely flexible, and the final product they represent
tends to look "blocky" or repititious. There are several methods of tiling, but
first it is necessary to learn seamless tiling, the root of tiling techniques. One
thing to note in this tutorial is that some of the larger coordinate blocks are
depicted with cyan borders for the purpose of simplicity.
|
A R T 3
|
Mmm...examples =) |
| |
|
seamless tiling
For the purpose of making this tutorial less confusing (as if it were not
confusing enough to begin with |-) ), I have divided the example bitmap into
two parts: the first part shows the tiles used, the second part shows the tiles
being tiled in a two by two square. I will distinguish between the two
examples by using a prefix of "A" or "B" (the former refers to the first
bitmap, the latter to the second) in front of the bitmap coordinates. That said,
look at the asphalt tile in A(1,1). You can already tell this tile sucks =)…and
sure enough, looking how it tiles in B(1,1), you can see that it is definitely not
seamless.
What is that you ask, grasshopper? What is a seamless tile? A
seamless tile is a bitmap that can be copied and placed repeatedly in an
adjacent fashion such that the viewer can not tell where the origin of the
original bitmap is. In simpler terms, a seamless tile, when tiled, has no visible
edges. One bitmap should line right up with the next with no awkward lines
between if it is seamless. Look at A(2,1) for an example of a seamless tile,
and its tiled output in B(2,1).
Making a seamless tile can be very complex and frusterating, if you
only use trial and error. However, a fairly simple trick can save you lots of
work. I call it CAPEA, or "cut and paste edge alignment." It works like this:
first, a tile is made, without regard to its seamlessness. Next it is cut in half
horizontally…the upper half is place adjacently below the lower half. Then
the edge between the two is "fixed," such that no seam appears in the
currently visible tile. Then the lower half is placed in its original position,
and the tile is cut vertically, the right half is placed adjacent to the other side
of the left.
Finally, the seam between the left and right halves is fixed, and
the right half is placed back in its original position. This process may seem
awkward, but it is the single most efficient way I know of making a seamless
tile. To clear things up a bit, I made two examples. In the first example
A(1,2)-A(7,2), the goal was to create a checker pattern that would blend
completely along the edges, but not the inside (which is rather redundant for a
checker tile, but it was intended for an easily visualized example). The
resulting tiling can be seen in B(3,1). In the second example A(1,3)-A(7,3), a
(more real-world) random ellipse pattern was made; the goal of course, was
to have it tile seamlessly.
This required, in some instances, slightly reshaping
or resizing the ellipses. In the result B(4,1), there are several larger ellipses in
the center of each tile…this results in a conspicuous pattern (the viewer can
easily discern where each tile is, relative to the akward oval). In general, you
will want to avoid making any sections of your tiles too different from other
sections…no internal components of your tiles should be akwardly large,
small, colored, or greatly varied in any other way from the rest of the tile
components. I noticed in a certain game the bookshelf tile had a skull on it,
next to several books. The skull did add a bit of atmosphere, but when I saw
that every bookshelf in the game had the same skull on it…it made me
wonder =). To make lots of jargon short, avoid this mistake.
blending tile regions
Knowing how to make a seamless tile is only the first step…what if
you want to make one tile type blend with another type? This is particularly
useful for terrain…say if you wanted your grass tiles to gradually fade to your
sand tiles, or your rock tiles to your dirt tiles. We want to have tiles that fade
in every direction, so we can create any possible shape (imagine trying to
create a large, rectangular patch of grass on some sand: we would need
pieces that fade to each of the four sides, and the four corners). So let us
enumerate ALL possible transitions, so we can build any shape of transitions.
This can be done with some simple block tricks…if we divide a tile into four
quadrants, where each quadrant can be "on" (represented by black) or "off"
(represented by any shade of red) (think binary =) ).
There are sixteen
possible combinations…but we do not want the one that is completely blank
(all quadrants off); after all, how useful is a blank tile? Subtracting one, this
leaves us with fifteen possible combinations. If you align these possible
combinations A(1,4), you can easily distinguish that they form four sides, four
corners, four inverse corners, two touching corners, and a whole tile. This is
the base set we want to use for creating tiles that shift from one tile type to
another. From this set, we can create a blender mask, a mask that will dither
your base tile set onto any type of terrain. To create a blender mask, simply
fade the black dot region onto the red, using less and less frequent dot
dithering, and do the opposite with the red region A(2,4). Then, once you
have a base tile, you can just make fifteen copies of the tile and place this
blender mask over it A(3,4), dropping out the black color as a transparency.
Then only the red region has to be dropped out from the transitioning tiles
when they are placed on top of other tiles, as shown in B(1,5). As difficult as
I may have made that simple process seem, I guarantee that it works great,
and is very easy once you create the blender mask…or you can even copy
mine if you are lazy =).
| |
More tiles for ya |
| |
|
(not so?) trivial corrections from previous issues
I would like to correct some explanations of techniques that I have now
refined…the first is metallic shading for flat objects. A better way to do it is
use streaked regions that fade from dark to light, and then back to dark again
(like the gold ones in A(2,5). These regions are used in varied angles on each
face of the object. The second technique that I want to correct is pseudo-
refraction for flat faced objects…for each face, a random angle of the metallic
shading described above should be used. Good examples of this are the
jewels I ripped off of the DA2 border, shown in A(3,5).
More tips for making cool tiles
- Like I've said before, avoid making any areas of your tile too different
from the rest of the areas in the tile…unless…
- …you make multiple versions of one type of tile. Let's take a brick-filled
tile for example…you could have a large, awkward brick in one of the tiles,
another tile be slightly normal, and another tile have some cracks in it.
This way you could distribute the tiles randomly for a varied, not-so-
bland-and-uniform environment.
- Try as you might, you cannot shade an entire tile, because it will not tile
correctly (unless you shade darkly on all sides with a highlight in the
center, but damn that looks stupid). You can, however, shade components
of a tile…take a cobblestone pavement tile for example. You can shade
each individual cobblestone and still have the texture tile perfectly.
- The food pyramid you always see on cereal is boxes BS…nobody can eat
eleven servings of carbohydrates in a day…Its just a cheap ploy to get
more cereal consumers on the market.
- Don't run with scissors.
summary
This must be my most confusing, sloppy, and rushed article yet…so if
you have any questions, feel free to email me at gavbug@napanet.net, or
better yet, ICQ me at 17653643 (no authorization required). Just be so kind
as to not ask about the Spice Girls, Backstreet Boys, or any crap like that =)
Look out for the next article I will do (not necessarily next issue)…it goes
into quantum mechanics and the theory of the fourth dimension; well, it does
three dimensions, at least >8) .
Ask Gavan how you can seamlessly tile your blitted sprites at this address. |
| |
>>>Back to Top<<<
By Zkman
N E X T |
Hope you've enjoyed the all-new qb:tm. One year is past, and hopefully the next year will
exceed even this. Look forward to a few new in-between articles, including a GREAT Rpg tut from Darkdread, as well as more ripping content on 18 September.
Also, don't forget to register your QB company with the Visionaries Exchange. Till next time...
END
|
It's the end of the issue as we know it |
>>>Back to Top<<<
|