Posted: 18th Nov 2021 1:20
the following is offered as a starting place for anyone who hasn't written a traditional Tower Defense game using a grid-based Tile system.

it uses a Waypoint system (sometimes referred to as "breadcrumbs") to guide Enemies down the Road.

for new users, please read up on User-Defined Types, V2 Arrays, and User Functions where the code relies upon them:
+ Code Snippet
// Project: TD Template 
// Created: 2021-11-09
// By: Virtual Nomad
// show all errors
SetErrorMode(2)

// set window properties
SetWindowTitle( "TD Template" )
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#			//and the Time the last shot was fired by the Tower so we know when to fire next.
EndType						//Add more like Upgrade Level, 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,0,0,0,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,0,0,0,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)

//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
	
	If GetPointerPressed() and Map[ThisX,ThisY] = 1 and Credits >= 100 //In this demo, Towers cost 100 Credits
		BuildTower(ThisX,ThisY)
	EndIf

	DoTowers() //Target an Enemy and Fire 
	
    If GetRawKeyState(27) then Exit    

    Sync()
loop

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#

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)
	
	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()
	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


[center][/center]
while i've used similar functionality before, i've never "put them all together" for something like this. so, please direct to any errors you might find; it's running fine on my end, but i expect it can be "broken"

and, i'm sure there are more-efficient methods but i'm trying to keep it simple for new users (and my own sanity in trying to get this out ASAP for the Comp).

11/21/21: Code updated with Tower Upgrades and more! - see below
01/09/22: HealthBars added - updated source and playable demo @ itch.io
10/23/22: Save/Load (Desktop & HTML) added @ HERE
Posted: 18th Nov 2021 1:45
Nice starting point there VN, hopefully pulls a few more entries in, although I don't really want to see clone upon clone it will certainly be interesting to see how many different directions this template can take. awesome stuff
Posted: 18th Nov 2021 1:58
300 lines?

Impressive

I'm at around three thousand lines in three different include files lol.

But I'm sure new people will take it and use it full force.

I will even look at it to learn new things.

I always learn something new from you.
Posted: 18th Nov 2021 2:49
found that i wasn't (also) using the Map[] properly to determine if a space was buildable. code fixed and updated above.

meanwhile, apparently wave ~25 is max where i'm out of real estate:
Posted: 18th Nov 2021 5:26
Ah, that is called a "negative feedback loop"... its one of game making biggest pitfalls, putting the player in a situation where he can not win, I want my game to detect this and snap a webcam pic the moment he rage quits .... lmao!!

I know... I'm a git!"

That's when you start upgrading towers, to kill more enemy's, they come faster, upgrade, rinse and repeat, or open up another section of the map, and again, rinse and repeat, looks like you got plenty of credits, make the upgrades expensive
Posted: 22nd Nov 2021 2:39
Tower Upgrades and more!



review the original code and spot the changes (mostly //noted) in the new code:
+ Code Snippet
// 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!
Posted: 22nd Nov 2021 4:40
meanwhile, apparently wave ~25 is max where i'm out of real estate:

Make a bigger map and zoom out!
Posted: 22nd Nov 2021 5:06
Make a bigger map and zoom out!

in time
Posted: 4th Dec 2021 19:48
Hmm I must be doing something wrong, it just crashes?
Posted: 4th Dec 2021 20:37
You are accessing a invalid array item (-1)

Show the block of code, its impossible to debug from a error dialog, line 91 in Vn's code is a EndIf ... that would not generate that error, we need to see your array code

Edit: or first example???

this??
+ Code Snippet
    If GetPointerPressed() and Map[ThisX,ThisY] = 1 and Credits >= 100 //In this demo, Towers cost 100 Credits
        BuildTower(ThisX,ThisY)
    EndIf


if so, change to this
+ Code Snippet
if ThisX <> -1 and ThisY <> -1
    If GetPointerPressed() and Map[ThisX,ThisY] = 1 and Credits >= 100 //In this demo, Towers cost 100 Credits
        BuildTower(ThisX,ThisY)
    EndIf
EndIF
Posted: 5th Dec 2021 2:42
i'll take a look at it but the array length 30 in the error is referring to the y-size of the map array: GLOBAL Map as Integer [20,30]

so, you must have somehow been able to click above y = 0 to return -1?

again, its reference -1 so: if ThisX <> -1 and ThisY <> -1 is still going to let you click < 0. if anything, wrap it in if ThisX > -1 and ThisY > -1 which still leaves it susceptible to x > 20 and y > 30 (which i didn't worry about since i hadn't implemented view offset yet.

meanwhile, i'm assuming the error number is off because i tend to enter 2+ lines of code in line 1. for example:
+ Code Snippet
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) 

i'm not sure if the compiler counts that as 7 lines of code or not <shrugs> but i don't remember ever being "that far off".

@RC01, this is my first attempt at a tutorial. are the comments throughout "enough" to follow what i've been trying to show? any feedback from a newcomer along those lines is appreciated incase i try to do another sometime.

finally, i have health bars implemented. i may add something else this weekend and post an update. when i do, i'll make each line "its own line" for clarity purposes/scenarios like this. no need to perpetuate all of my bad habits
Posted: 5th Dec 2021 11:00
is still going to let you click


You are correct, the best practice is to check the range (if ThisX >= 0 and ThisX <= 20) etc, but as you say with no view offset it should always be within range.

i'm not sure if the compiler counts that as 7 lines of code


Yes, when ':' is used the compiler sees 1 line, I just did a quick test and it appears to report the correct line number
Posted: 5th Dec 2021 16:32
just noted the crash is with the first example (where the text was black). i tried re-creating the scenario (building tower in the top-right corner) and didn't receive the error in the updated code and it's probably to do with my not adding proper safety nets to ensure clicking within the map (only).

@ RC01, was that the case? had you somehow clicked off the map?
Posted: 10th Jan 2022 1:29
small update to this with HealthBars added:

playable demo and updated source code added @ itch.io

btw, i'm calling 68 MAX level
Posted: 24th Oct 2022 5:39
updated to include "single string" Save/Load functionality for both Desktop & HTML via some simple string Building and Parsing that some might find useful: https://virtual-nomad.itch.io/td-template.

The Core (although a number of other changes were made):
+ Code Snippet
Function SaveGame()
	//We're going to build a single string that holds all Save data so that it can also be used as a Shared Variable
	//in HTML. We'll be using String Tokens to Parse the data during Load so we need something to break up sets of
	//variables called Delimeters. a Pipe | will separate each Entry and Colons and Commas within each Set.
	Save$ = "Creds:" + STR(Credits)			//We will not include TotalKills or Enemy Data
	Save$ = Save$ + "|Wave:" + STR(Wave)	//since we will be Restarting the Current Wave on Reload.
	Save$ = Save$ + "|Castle:" + STR(CastleStrength)	//Note the Pipes | to separate from previous Entries.

	//We will now add each Tower's basic details where remaining Stats will be derived from it's Level on Reload
	If Towers.Length &gt; -1 //Are there any Towers?
		For x = 0 to Towers.Length
			ID = Towers[x].ID	//Not Saved; Used only to get the Sprite's X,Y Coordinates.
			TX = GetSpriteX(ID)/TileSize 	//We want want Map coordinates so we can use our BuildTower(x,y) function
			TY = GetSpriteY(ID)/TileSize 	//on Reload. Actual Placement will occur during BuildTower()
			Coords$ = STR(TX)+","+STR(TY) 	//Remember, we're building a String; Hence STR() here and above to
											//convert Integers to Strings.
			
			//When we Load &amp; Parse the Save string, we'll use the first Token in each set of data to determine what
			//*kind* of data it is. Above, "Creds", "Wave" &amp; "Castle" are separated from their settings with a Colon :
			LVL$ = STR(Towers[x].LVL) //Here, we'll further seperate Settings with a Comma ,. Ie: Lvl, X, Y
			ThisTower$ = "|Tower:" + LVL$ + "," + Coords$ //Remember this order (Level, x, y) for LoadGame()
			Save$ = Save$ + ThisTower$ //Add it to the end of the Save$ String.
		Next x
	EndIf
	
	If GetDeviceBaseName() = "html5"
		SaveSharedVariable("Save",Save$)
	Else
		OpenToWrite(1,"Save.dat") 	//For Desktop, we're saving as a human-readable String while you observe
			WriteLine(1,Save$)		//what's being written for yourself via [ENTER} during the game. You may want to
		CloseFile(1)				//user WriteString() instead in yours so the player can't easily alter the Save file.
	EndIf
EndFunction


Function LoadGame()
	//Since native HTML exports cannot Write files to the Hard Drive, we'll used Cookies, instead.
	If GetDeviceBaseName() = "html5"
		SaveString$ = LoadSharedVariable("Save","-1") //Look for the Save cookie. Returns -1 if not found
	Else
		If GetFileExists("Save.dat")
			OpenToRead(1,"Save.dat")
				SaveString$ = ReadLine(1) 	//Remember, it's a Single string saved as a readable Line of text
			CloseFile(1)					//See SaveGame() where you may change this to ReadString() if WriteString()
		Else								//was utilized.
			SaveString$ = "-1"
		EndIf
	EndIf

	If SaveString$ &lt;&gt; "-1" //If Save exists, Parse the string:
		Total = CountStringTokens(SaveString$,"|")	//Get the number of Entries within the String
		For x = 1 to Total //String Tokens start at 1, not 0
			ThisEntry$ = GetStringToken(SaveString$,"|",x)	//Read the Entry (where each is eeparated by a Pipe |
			Var$ = GetStringToken(ThisEntry$,":",1)		//Let's determine what it is
			Value$ = GetStringToken(ThisEntry$,":",2)	//and it's Setting/pertinent data, all separated by Colons :
			
			If Var$ = "Wave"	//Wave will be Restarted so there won't be a TotalKills value
				Wave = VAL( Value$ )
				SetTextString(WaveTXT, "Wave: " + Value$)
			EndIf
			
			If Var$ = "Creds"				//We won't set Credits yet since we'll possibly be building Towers
				CredHolder = VAL( Value$ )	//later using our existing BuildTower() function which costs Credits
			EndIf							//so, we'll Hold onto it for now and set it later.
			
			If Var$ = "Castle" //Castle health
				CastleStrength = VAL( Value$ )
				SetTextString(CastleTXT, "Castle: " + Value$)
			EndIf

			If Var$ = "Tower" 								//If it's a Tower entry
				LVL = 	VAL( GetStringToken(Value$,",",1) )	//get its Level
				TX = 	VAL( GetStringToken(Value$,",",2) ) //and Map[x,y]
				TY = 	VAL( GetStringToken(Value$,",",3) )

				BuildTower(TX,TY) //Build a new Tower at TX, TY
				
				//Find it's Index in the Towers[] array
				ThisTower = Towers.Find(GetSpriteHitGroup(100,TX*TileSize+Half,TY*TileSize+Half))
				
				While Towers[ThisTower].LVL &lt; LVL	//Repeatedly Upgrade the Tower until
					SetTower(ThisTower)				//it's the correct Level. 
				EndWhile							//This saves us some compound math.
			EndIf
		Next x

		//Since any Towers are now built, we can set the current Credits
		Credits = CredHolder
		SetTextString(CredTXT, "Credits: " + STR(CredHolder) )

		//The above is not as efficient as it could be since Loading and Saving was not implemented in the initial
		//Tutorial and I didn't want to change too much around for anyone following earlier versions.
	EndIF
EndFunction


Make sure to modify the Player for same-site Cookie support.