
Hello, and welcome to Chapter 25!
This is a big day! Congratulations if you've made it this far.
For today's tutorial, we're going to transform our game from "Almost" to "Wow!"
We've got a lot to do, but I'll be making this as painless as I possibly can.
Here's what we need to do to take this from "Almost" to "Wow!"

Remember that EndGame "Too Bad" line that we added in Chapter 23?
Sure would be nice if our game wrote "Too Bad" to the screen when we died.
That way, as well as "just blowing up", they get a big Game Over style text.

We should keep the previous high-scores, make a table.
What about saving those scores, too?
Store them in the browser's cookie the same way it stores our program, so that the next time you run the game, all of the highscores are still there.
GotoJSE has a lot of functions for loading and saving a certain amount of "Game" information along with our programs, and highscore tables are one of those things.

We're going to need a better titlescreen to show all the highscores.
A nice new sidebar on the titlescreen with the highscores listed, alongside a list of the last 10 player's scores.
We'll add a circular menu to choose between Starting a game, and opening the Options screen.
Ooh, do we need an Option's Screen?

We should probably add a little volume slider, and maybe even add one for Music, so that later on if we add Music (!), the player can change that volume too.
That sounds like something that could be useful.

We've a giant todo list of things we're going to add to our game.
We've made a list, and checked it twice.
It's time for the big event.
// My Game
// by Mr Green
// Created 2025/12/16
Symbol 0,"2__0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0P?0_0/P?0!P0,P0_0/P0,P0?P,00P0_0/P00P,0.P,00P0_0/P00P,0.P,00P0_0/P00P,0.P,00P!0!P!00P,0_0?P@0_0_0_0_0_0/P!0.P!0_0_0_0_0_0.P?0PP0P?0_0_0!PP0_0_0/P.0PP0P.0_0_0/PP0_0_0_0,PP0_0@P_P,0PP0P_P,0.P0_0_0@P0.P0_0_0@P0.P0_0_0@P0.P0@P0@P0@P0.PP0/P0@P0/PP0?P0/P0P!0P0/P0!P?0,P0@P0,P?0@P,0,P0@P0,P,0_0?P,0@P,";
Symbol 1,"2__0_0_0_0_0_0_0_0_0_0_0_0_0_0_00P,0_0_0@P_P,0_0?P_P,0?P0_0?P0?P,0@P0_0?P0_0/P0_0?P0_0/P?0!P?0_0_00P0!P0_0_0!P@0_0_0_0_0_0/P!0.P!0_0_0_0_0_0.P?0PP0P?0_0_0!PP0_0_0/P.0PP0P.0_0_0/PP0_0_0_0,PP0_0!P_P?0PP0P_P!0_0_0_0,PP0_0_0_0,PP0_0_0_0,PP0_0P0@P0_0PP0_0P0@P0_0PP0_0P0P!0P0_0PP0_0P0P0.P0P0_0PP0_0P0P0.P0P0_0P,0/P,0P!0P,0/P.0_0_0_0P.0_0_0_0PP";
Symbol 2,"2__0_0_0_0_0_0_0_0_0_0_0_0_0NN00NN0_0_0/NPQNNQPN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0?NPPQ.PPN0_0_0.NPPQ.PPN0_0_0.NPPQ.PPN0_0_0.NPPQ.PPN0_0_0.NPQ!PN0_0_0.NPQ!PN0_0_0.NPQ!PN0_0_0,NPPQ!PPN0_0_00NPPQ!PPN0_0_00NPPQ!PPN0_0_00NPPQ!PPN0_0,P_P_P_P0,PS_S_S_SP0PS_S_S_S,PPSSR,S,R,S,RRS,R,S,R,SSP0S_S_S_S,0,S_S_S_S";
Symbol 3,"2__P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P/";
Symbol 4,"2__0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_z0_0_0_0020,20_0_0_00;0_0_0_0z0;1;0z0_0_0_0;0_0_0_0020,20_0_0_00z";
Symbol 5,"2__0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0?Q_Q_Q_0?Q_Q_Q_";
AntiAlias Off // Turn off AntiAlias, for Crispy Invaders
// Arrays
Dim EnemyAlive(20)
Dim EnemyBullet(50,2)
// Particle Setup
// Clear out settings
ResetParticle
// Setting for Particle type
SetParticleImage 5
SetParticleDirection 0,360
SetParticleSpeed 0.1,0.3
// Save particle style to Variable
StoreParticle ExplodingPiece
// And don't forget to reset, afterwards
ResetParticle
// Main Loop
GameRun=0
Repeat
If GameRun==0 then Gosub .StartScreen
If GameRun==1 then Gosub .InGame
Flip
Forever
// This is our Titlescreen
.StartScreen
CLS
ResetDraw
ShowInvader=1
If Wrap(Frames,0,20)<10 Then ShowInvader=0
DrawImg 320,160,ShowInvader
SetFontSize 32
Text 320,240,"Titlescreen!",1
SetFontSize 16
Text 320,360,"Press Fire to Play",1
If GamePad(ButtonA)
Gosub StartGame
GameRun=1
Endif
Return
// This resets the current level
.NextLevel
Level=Level+1
DropDown=480
For BadGuy=1 to 20
EnemyAlive(BadGuy)=1
Next
// New variables to multiply the EnemyX and Y Angles
WaveX=Rand(1,3)
WaveY=Rand(1,3)
Gap=Rand(3,15)
Return
// This prepares our game when the A Button is hit
.StartGame
Score=0
Dead=0
Level=0
PlayerX=320 // Middle of the screen
PlayerY=440 // Bottom of the screen
BulletX=0 // Reset player's bullet
BulletY=0
XPosition=1
YPosition=2
For b=0 to 50
EnemyBullet(b,1)=0
EnemyBullet(b,2)=1000
Next
EnemyBulletTo=0 // We increase this when an enemy fires a bullet.
EnemyBulletDelay=0 // We'll have a delay between every bullet drop.
Gosub NextLevel
Return
// This is our ingame loop
.InGame
CLS
ResetDraw
// Starfield using Symbol 4
Starfield 1,0,4
// Player
If Dead==0
PlayerCollide=DrawImg(PlayerX,PlayerY,2)
Speed=4
If GamePad(ButtonLeft)>0.5 then PlayerX=PlayerX-Speed
If GamePad(ButtonRight)>0.5 then PlayerX=PlayerX+Speed
// Using >0.5 allows players to use a thumbstick, too!
PlayerX=Limit(PlayerX,32,640-32)
// Player's Bullet
// Keep the Bullet going upwards
BulletSpeed=16
BulletY=Limit(BulletY-BulletSpeed, -100,480)
EndIf
// Draw the Bullet
SetScale 0.2,0.5
BulletCollide=DrawImg(BulletX,BulletY,3)
// Handle Enemy Bullets
For b=1 to 50
If EnemyBullet(b, YPosition)<480
EnemyBullet(b, YPosition)=EnemyBullet(b, YPosition)+6
ThisCollide=DrawImg(EnemyBullet(b, XPosition),EnemyBullet(b, YPosition),3)
If Collide(ThisCollide,PlayerCollide) and Dead==0 then Dead=1
EndIf
Next
// And reset things afterwards
ResetDraw
If BulletY<0 And GamePad(ButtonA) and Dead==0
BulletX=PlayerX
BulletY=PlayerY
PlaySFX("Lazer_2")
EndIf
// Enemy Section
// Reuse this to keep the invaders animated
ShowInvader=1
if Wrap(Frames,0,20)<10 then ShowInvader=0
CircleSize=160
// New Variable for Circle Position
CircleX=320+Sin(Frames)*160
CircleY=200-DropDown
DropDown=Limit( DropDown-5 ,0,480)
// Why not try changing this value, too.
// The higher the number the faster the drop.
// Make it slower to have the invaders creep into view!
// Bullet Delay workings
EnemyBulletDelay=Limit(EnemyBulletDelay-Level, 0,200)
BaddyCount=0
// A loop for 20 Bad Guys
For BadGuy=1 to 20
// If Alive
If EnemyAlive(BadGuy)==1
BaddyCount=BaddyCount+1
// Each enemy should be further around the loop.
PlusAngle=BadGuy*Gap
Angle=Wrap(Frames + PlusAngle,0,360)
EnemyX=CircleX+Cos(Angle*WaveX)*CircleSize
EnemyY=CircleY+Sin(Angle*WaveY)*CircleSize
EnemyColour=Wrap(BadGuy,0,7)
// New collision code
EnemyCollide=DrawImg(EnemyX,EnemyY,ShowInvader,EnemyColour)
If Collide(BulletCollide,EnemyCollide) And BulletY>0
// If the bullet hits the Enemy
// Turn off the Enemy
EnemyAlive(BadGuy)=0
// Make a Bang sound
PlaySFX("Explode_Low_2")
// Gimme 5 points!
Gimme 5,EnemyX,EnemyY
SpawnParticles EnemyX,EnemyY,ExplodingPiece,10
// Move the Bullet off the screen
BulletY=-100
EndIf // Collision
// Dropping Bullets
If Rand(0,10)==1 and EnemyY<240 and EnemyBulletDelay==0
EnemyBulletTo=Wrap(EnemyBulletTo+1,1,50)
// Wrap the value between 1 and 50
// Then position the Bullet
EnemyBullet(EnemyBulletTo, XPosition)=EnemyX
EnemyBullet(EnemyBulletTo, YPosition)=EnemyY
// Reset the Delay
EnemyBulletDelay=100
// And go "peow!"
PlaySFX("Lazer_1")
EndIf
EndIf // Alive
Next // Each BadGuy
If BaddyCount<1 then Gosub NextLevel
// Quit button (Return)
If GamePad(ButtonStart) then GameRun=0
// We can hit the Return key to quit
If Dead>0
If Dead==1
PlaySFX("Explode_High_4")
SpawnParticles PlayerX,PlayerY,ExplodingPiece,100
EndIf
Dead=Dead+1
EndGame "Too Bad"
// We'll talk about this in Chapter 25!
If Dead>120 Then GameRun=0
EndIf
SetFontSize 16
Text 320,16,Score,1
Return
Head to the space just above our original // Main Loop, and place the following line of code
GameHandler "My Space Game"
GameHandler will take over from our Main Loop, and do everything I just described.
.. and that'll do.