Tower Upgrades and more!
review the
original code and spot the changes (mostly //noted) in the new code:
// Project: TD Template Upgrade 1
// Created: 2021-11-21
// By: Virtual Nomad
// show all errors
SetErrorMode(2)
// set window properties
SetWindowTitle( "TD Template - Upgrade 1" )
SetWindowSize( 640,384,0 )
SetWindowAllowResize( 1 )
CenterWindow()
MaximizeWindow()
// set display properties
SetVirtualResolution( 640,384)
SetOrientationAllowed( 1, 1, 1, 1 )
SetSyncRate( 30, 0 )
SetScissor( 0,0,0,0 )
UseNewDefaultFonts( 1 )
//The Map will be broken down into Tile- or Grid-based squares.
GLOBAL TileSize = 64 : GLOBAL Half = 32 //We'll be centering some Sprites within a Tile while others, like Ground & Castle Tiles
//will be positioned using their top-left corner.
//The following will establish some UDTs or User Defined Types (of variables) that can hold many related values inside.
Type Tower //Tower Characteristics:
ID, TargetID, Range# //Record SpriteID, a Tower's Target (Sprite ID, if any), its Range, Rate of Fire
RoF#, LastShot#, LVL //and the Time the last shot was fired by the Tower so we know when to fire next.
EndType //Add more like Upgrade Level (ADDED!), Damage (here we'll only do a Damage of 1, Area of Effect?
GLOBAL Towers as Tower [] //When a Tower is built, add it to this array for easy reference.
GLOBAL Waypoints as Integer [] //We'll save Sprite IDs for the Waypoints here. Once we place them (in the BuildMap() Function below),
//we can refer to the Sprites themselves for X/Y coordinates that Enemies will move toward.
//Note, when we .Insert them into this array, we will do so sequentially: 0,1,2,3...
//The Waypoints are visible in this guide so you can, well, visualize their placement :)
Type Enemy //Hold Enemy characteristics, too.
ID, Health, Drop, NextWP //Sprite ID, Enemy Health, Credits (In-game Currency) "Dropped" on death, along with its next Waypoint.
EndType //Consider adding a variable MoveRate and whatever else you might want to change between Enemies.
GLOBAL Enemies as Enemy [] //When an Enemy Spawns, we'll add it here. If it reaches the final Waypoint, it will
//be removed from the queue and damage the Castle.
GLOBAL Map as Integer [20,30] //Used to track whether or not player can build a Tower in the GridSpace
//Set to the Maximum dimensions a Map could be. We'll only use a portion
//for this Template. See BuildMap() below.
GLOBAL MapSprites as Integer [] //Record any Spirtes (IDs) used for this Map so we can delete them later.
//Not needed in this Template since we are only using 1 map but get in the habit of Handling your Sprites.
BuildMap() //See Function below
//Establish some starting variables and provide for some of them to be displayed on screen
GLOBAL Credits = 100 //Just enough to buy 1 Tower. Waste no time in placing it!
GLOBAL CredTXT as Integer : CredTXT = CreateText("Credits: " + STR(Credits)) //Update this as we go
SetTextPosition(CredTXT,630,0) : SetTextSize(CredTXT,36) : SetTextAlignment(CredTXT,3)
SetTextColor(CredTXT,255,255,255,255) : SetTextDepth(CredTXT,0)
GLOBAL SpawnRate# = 3.0 //Every 3 Seconds, another will spawn. Play with this value
GLOBAL Wave = 1 //We'll base some things on the current Wave of Enemies.
GLOBAL TotalKills = 0 //Every 10 Kills, we'll INC or Increase the Wave by 1
GLOBAL WaveTXT as Integer : WaveTXT = CreateText("Wave: " + STR(Wave))
SetTextPosition(WaveTXT,320,0) : SetTextSize(WaveTXT,36) : SetTextAlignment(WaveTXT,1)
SetTextColor(WaveTXT,255,255,255,255) : SetTextDepth(WaveTXT,0)
GLOBAL CastleStrength = 100 //If an Enemy reaches the final Waypoint, it will Damage the Castle (see DoEnemies() below)
GLOBAL CastleTXT as Integer : CastleTXT = CreateText("Castle: " + STR(CastleStrength))
SetTextPosition(CastleTXT,352,340) : SetTextSize(CastleTXT,36) : SetTextAlignment(CastleTXT,1)
SetTextColor(CastleTXT,0,0,0,255) : SetTextDepth(CastleTXT,0)
//NEW! Hover over a Tower see if it can be Upgraded! See HoverTower() and UpgradeTower()
GLOBAL LvlTXT as Integer : CreateText(LvlTXT,"") : SetTextSize(LvlTXT,24) : SetTextAlignment(LvlTXT,1)
SetTextDepth(LvlTXT,0) : SetTextBold(LvlTXT,1)
//We'll draw red lines to indicate Tower shots when we DoTowers()
GLOBAL Red : Red = MakeColor(255,0,0)
do
//Determine which GridSpace the Pointer currently occupies
ThisX = ROUND(GetPointerX())/TileSize : ThisY = ROUND(GetPointerY())/TileSize
If LastSpawn# + SpawnRate# <= Timer()
SpawnEnemy()
LastSpawn# = Timer()
Endif
DoEnemies() //Move enemies toward their next Waypoint
//NEW!
//Check to see if Pointer is over a Tower
If GetSpriteHitGroup(100,GetPointerX(),GetPointerY()) > 0 //Hovering over a Tower (Sprite Group 100)
HoverTower( FindTowerIndex(GetSpriteHitGroup(100,GetPointerX(),GetPointerY()) )) //Use the SpriteID to find Index in Towers array
SetTextVisible(LvlTXT,1)
Else
SetTextVisible(LvlTXT,0)
EndIf
//Upgrade or Build Tower?
If GetPointerPressed()
If GetSpriteHitGroup(100,GetPointerX(),GetPointerY()) > 0 //Pointer over Tower = Upgrade
UpgradeTower(FindTowerIndex(GetSpriteHitGroup(100,GetPointerX(),GetPointerY())))
ElseIf Map[ThisX,ThisY] = 1 and Credits >= 100 //POiner over Buildable land? NEW Towers cost 100 Credits
BuildTower(ThisX,ThisY)
Endif
EndIf
DoTowers() //Target an Enemy and Fire when ready
If GetRawKeyState(27) and GetDeviceBaseName() <> "html5" then Exit
If GetPaused() = 0 then Sync()
loop
Function HoverTower(Index)
//Find a Tower's current Level of Upgrade to determine Next, or NOT Upgradnle if already Level 5
NextLevel = Towers[Index].LVL + 1
If NextLevel < 10
SetTextString(LvlTXT, "UPGRADE" + CHR(10) + STR(NextLevel*100))
Else
SetTextString(LvlTXT, "MAXED!")
EndIf
SetTextPosition(LvlTXT,GetPointerX(),GetPointerY() ) //Show Upgrade Status @ Pointer X/Y
EndFunction
Function UpgradeTower(Index)
//Towers now Upgradable to Level 5. Each Upgrade makes the Tower Darker to indicate Level
//Upgrades Increase the Rate of Fire and Range
//With the Towers[Index] passed in using the FindTowerIndex() function below,
//retrrieve the Sprite ID and Tower Level(LVL)
ThisID = Towers[Index].ID : NextLevel = Towers[Index].LVL + 1
If NextLevel <= 9 //Max LVL 10
If Credits >= NextLevel*100 //Can you afford it?
Credits = Credits - NextLevel*100 : SetTextString(CredTXT, "Credits: " + STR(Credits))
SetSpriteColor(ThisID,192/NextLevel, 192/NextLevel, 192/NextLevel, 255)
Towers[Index].LVL = NextLevel : Towers[Index].RoF# = Towers[Index].RoF#*0.9
Towers[Index].Range# = Towers[Index].Range#*1.10
EndIf
EndIf
EndFunction
Function BuildMap() // We'll add 6 Rows (0-5) and 10 Columns (0-9) of Tiles for this demo Map
AddRow(0 ,"2122222222") //Row, TileType signified by a 1 (Road) or 2 (Ground or Buildable space for Towers)
AddRow(1 ,"2111111112") //3 = Castle Walls where to Towers can be built. Don't forget the size of the Map[] array. Anything we don't
AddRow(2 ,"2222222212") //specify here will = "0". Note that with Arrays, the first Index is 0. Hence, Row 0, 1, 2, etc.
AddRow(3 ,"2222222212") //Rows will determine the Y on the Map while each character (read Column) in the String
AddRow(4 ,"2221111112") //will determine the X coordinate (both multiplied by our TileSize (64).
AddRow(5 ,"2331333222") //Note, when we later use MID(), the Characters returned begin at 1 (not 0) so we'll
//need to account for that when we place the Tile where we want the top-left corner of the map to be 0,0.
//Waypoints for the Enemies to follow. We added 6 Rows (0-5) and 10 Columns (0-9) of Tiles above.
AddWP(1,-1) : AddWP(1,1) : AddWP(8,1) //Note that WP(1,-1) is "off the Map" as is WP(3,6)
AddWP(8,4) : AddWP(3,4) : AddWP(3,6) //Same as above, the Top Left corner is 0,0 (* TileSize)
//We can use WayPoints.Length to determine if an Enemy has reached the last Waypoint.
EndFunction
Function AddRow(Row,Data$) //to the Map
//remember the Row determines the Y coordinate in Gridspace while the x in the following For/NExt loop will be used for the X coordinate
for x = 1 to LEN(Data$) //LEN gives you the # of characters in a String. IE, LEN("2122222222") = 10
ThisTile = CreateSprite(0) //Create a 64x64 Sprite:
SetSpriteSize(ThisTile,TileSize,TileSize)
ThisTileType$ = MID(Data$,x,1) //Retrieve Charater (x) in the String to determine Tile "Type": Road, Land, or Castle
//Remember, MID(String$) begins at 1. we need to convert it to GridSpace which begins at 0
If ThisTileType$ = "1" //Road
SetSpriteColor(ThisTile, 255,192,128,255)
Map[x-1,Row] = 0 //0 signifies that the player CANNOT build a tower on this tile
Endif
If ThisTileType$ = "2" //Land
SetSpriteColor(ThisTile,10,128,0,255)
Map[x-1,Row] = 1 //1 signifies that the player CAN build a tower on this tile
EndIf
If ThisTileType$ = "3" //Castle
SetSpriteColor(ThisTile,192,192,192,255)
Map[x-1,Row] = 0 //Can't build here, either.
EndIf
SetSpritePosition(ThisTile,(x-1)*TileSize, Row*TileSize ) //Remember the first MID in a String is 1 so we need to subtract 1 to place
SetSpriteDepth(ThisTile,100) //Below everything else visually.
MapSprites.Insert(ThisTile) //Record the Sprite ID so we can delete them between Maps.
//Not actually used in this Template since we only have 1 Map
next x
EndFunction
Function AddWP(x,y) //Center a small sprite in the center of a tile to indicate Waypoint
WP = CreateSprite(0) : WayPoints.Insert(WP) //Add it to the (sequential) queue.
SetSpritePositionByOffset(WP,(x*TileSize)+HALF, (y*TileSize)+HALF) //center in gridspace
SetSpriteDepth(WP,90) //just above the Ground (@ depth 100)
MapSprites.Insert(WP)
EndFunction
Function SpawnEnemy()
//Set everything we need for a new Enemy. See Type Enemy above.
x = GetSpriteXByOffset(Waypoints[0]) //This first Waypoint (0) was added: AddWP(1,-1) * TileSize (64) + HALF
y = GetSpriteYByOffset(Waypoints[0]) //we already Centered the Waypoint in the Tile so we can center the Enemy on it.
ID = CreateSprite(0) : SetSpriteSize(ID,24,24) : SetSpriteColor(ID,0,0,255,255)
SetSpritePositionByOffset(ID, x, y)
SetSpriteDepth(ID,50)
ThisEnemy as Enemy //Create the Enemy based on the current Wave so they get stronger as the game progresses:
ThisEnemy.ID = ID //the Sprite ID we created above
ThisEnemy.Drop = Wave*13 //Credits dropped on death
ThisEnemy.Health = Wave*2 //play with these values to help balance the game
ThisEnemy.NextWP = 1 //it's already at WP1.
Enemies.Insert(ThisEnemy) //Add it to the queue
EndFunction
Function DoEnemies()
//The following will iterate through the queue of Enemies that have spawned:
For x = Enemies.Length to 0 Step -1 //Go thru the list backward because we may .Remove array elements.
//if we don't, indices will get skipped in the loop.
ThisEnemy = Enemies[x].ID //Enemy Sprite ID
ThisWP = Waypoints[Enemies[x].NextWP] //Waypoint Sprite ID
//Get Enemy position in the World
EX# = GetSpriteXByOffset(ThisEnemy) : EY# = GetSpriteYByOffset(ThisEnemy)
//...and Waypoint position to base Enemy's NEW position on below
WPX# = GetSpriteXByOffset(ThisWP) : WPY# = GetSpriteYByOffset(ThisWP)
If DistanceToSprite(ThisEnemy,ThisWP) <= 1.0 //Our (default) MoveRate# for all Enemies, here.
//Feel free to adjust/change in YOUR game :)
If Enemies[x].NextWP = WayPoints.Length //If Enemy has reached the LAST Waypoint and breached the Castle...
DeleteSprite(ThisEnemy) //Enemies[x].ID above
Enemies.Remove(x) //Remove reference to the Enemy in the arrawy.
//...damage the Castle:
CastleStrength = CastleStrength - 10 : SetTextString(CastleTXT,"Castle: " + STR(CastleStrength))
//We're doing nothing special if the Castle collapses (when its strength = 0); I'll leave that to you :)
Else
SetSpritePositionByOffset(ThisEnemy,WPX#,WPY#)
INC Enemies[x].NextWP
EndIf
Else //If the Enemy is NOT going to reach .NextWP, instead:
ThisAngle# = AngleToWP(x) //We'll use some basic Trig to help simplify new position for this Enemy.
//Getting the SIN and COS of an angle will return a range between -1.0 and 1.0 on respective axis (x,y)
//which we can multiply times the MoveRate# of an Enemy.
//Note, to move Up within the World we Subtract the COS of the Angle the Enemy is moving.
//So, original position plus any change to the x/y
SetSpritePositionByOffset(ThisEnemy, EX#+SIN(ThisAngle#), EY#-COS(ThisAngle#))
EndIf
Next x
EndFunction
Function DoTowers()
//Loop thru each Tower, see if it has a valid Target (within Range) and fire at it based on the Tower's Rate of Fire:
For x = 0 to Towers.Length
If Timer() - Towers[x].LastShot# >= Towers[x].RoF# //If it's time to fire, else skip this Tower
ThisTower = Towers[x].ID //Get Tower Sprite ID for reference
TX# = GetSpriteXByOffset(ThisTower) : TY# = GetSpriteYByOffset(ThisTower)
HasTarget = 0 //used as a Flag to trigger fire if the Tower has a Target in range
ThisTarget = Towers[x].TargetID //If the Tower's last Target was killed by another Tower or
//it never had one, this will be 0; See BuildTower() below.
If ThisTarget > 0 and GetSpriteExists(ThisTarget) //If it has a vaid target...
If DistanceToSprite(ThisTower,ThisTarget) <= Towers[x].Range# ///and it's within Range
//Get Enemy Coords, trip the HastTarget flag...
EX# = GetSpriteXByOffset(ThisTarget) : EY# = GetSpriteYByOffset(ThisTarget)
HasTarget = 1
E = FindEnemyIndex(ThisTarget) //...and determine WHERE in the Enemies array it is.
EndIf
Endif
///If the Tower has no Target...
If HasTarget = 0 //Find New Target by searching all Enemies to determine if one is within .Range#
For E = 0 to Enemies.Length //We may .Remove the Enemy if we kill it but, unlike in DoEnemies(), we
//won't be continuing iteration of the loop.
ThisEnemy = Enemies[E].ID //Enemy Sprite ID
If DistanceToSprite(ThisTower, ThisEnemy) <= Towers[x].Range# //See DistanceToSprite() function below
Towers[x].TargetID = ThisEnemy //Note, since Enemies were added when they were Spawned, this will be the
//"oldest" enemy within Range.
EX# = GetSpriteXByOffset(ThisEnemy) : EY# = GetSpriteYByOffset(ThisEnemy)
HasTarget = 1
Exit //...the "For E" loop since we've found a Target.
//E, the Index refered to in the Enemies array is about to be used a few lines down
Endif
Next E
EndIf
If HasTarget = 1
DEC Enemies[E].Health //Damage the Enemy (by 1)
If Enemies[E].Health <= 0 //The Enemy was killed
Towers[x].TargetID = 0 //Set to 0 so, the next time through, the Tower find a new Target.
//Collect its Credits amd record the Kill
INC Credits, Enemies[E].Drop : INC TotalKills
SetTextString(CredTXT,"Credits: " + STR(Credits))
If TotalKills > 0 and MOD(TotalKills,7) = 0 //Every 7 Kills, Enemies will get tougher. See SpawnEnemy()
INC Wave : SetTextString(WaveTXT,"Wave: " + STR(Wave))
EndIf
DeleteSprite(Enemies[E].ID)
Enemies.Remove(E)
Endif
DrawLine(TX#,TY#,EX#,EY#,Red,Red) //Show the Shot
Towers[x].LastShot# = Timer() //and Record the time
Endif
EndIf
Next x
EndFunction
Function DistanceToSprite(ThisSprite,ThisTarget) //Center to Center
GX# = GetSpriteXByOffset(ThisSprite) : GY# = GetSpriteYByOffset(ThisSprite)
PX# = GetSpriteXByOffset(ThisTarget) : PY# = GetSpriteYByOffset(ThisTarget)
ThisDistance# = SQRT( (GX#-PX#)^2 + (GY#-PY#)^2 )
EndFunction ThisDistance#
Function AngleToWP(ThisEnemy) //Center to Center
EX# = GetSpriteXByOffset(Enemies[ThisEnemy].ID)
EY# = GetSpriteYByOffset(Enemies[ThisEnemy].ID)
ThisWP = WayPoints[Enemies[ThisEnemy].NextWP]
WPX# = GetSpriteXByOffset(ThisWP)
WPY# = GetSpriteYByOffset(ThisWP)
ThisAngle# = ATanFull(WPX#-EX#, WPY#-EY#)*1.0 // ??
EndFunction ThisAngle#
//Note Towers.LVL added to allow Upgrades and Tower Sprite Group (100) to detect Tower and facilitate.
Function BuildTower(x,y) //in GridSpace
Map[x,y] = 0 //0 signifies that the player can no longer build a tower on this tile
ID = CreateSprite(0) : SetSpriteSize(ID,36,36) : SetSpriteColor(ID,192,192,192,255)
x = x*TileSize + HALF //Center it within the Tile
y = y*TileSize + HALF
SetSpritePositionByOffset(ID, x, y)
SetSpriteDepth(ID,50)
SetSpriteGroup(ID,100) // we'll need this to detect Tower selection for UpgradeTower()
Credits = Credits - 100 //Static cost. Add DIFFERENT Towers to yours with their own Costs.
SetTextString(CredTXT, "Credits: " + STR(Credits))
ThisTower as Tower
ThisTower.ID = ID : ThisTower.Range# = 100.0 : ThisTower.RoF# = 2.0
ThisTower.LastShot# = 0.0 //Ready to Fire on Build
ThisTower.TargetID = 0 //to indicate Tower needs a new Target in DoTowers()
ThisTower.LVL = 1
Towers.Insert(ThisTower)
EndFunction
Function CenterWindow()
SetWindowPosition(GetMaxDeviceWidth()/2-GetWindowWidth()/2, GetMaxDeviceHeight()/2-GetWindowHeight()/2)
EndFunction
Function FindEnemyIndex(ID)
For Index = 0 to Enemies.Length
If Enemies[Index].ID = ID
ThisIndex = Index
Exit
EndIf
Next Index
EndFunction ThisIndex
Function FindTowerIndex(ID)
For Index = 0 to Towers.Length
If Towers[Index].ID = ID
ThisIndex = Index
Exit
EndIf
Next Index
EndFunction ThisIndex
i'm calling Wave 50 MAX (made it to 48 before i found i was accidentally limiting Towers to Level 9
).
is anyone (beside me) using this for
the comp?
i'd like to know!