Code of Units UGameItems to UTextures

Code of units UGameItems to UTextures of TowerOfArcher by George Wright

UGameItems

unit UGameItems;
{
    Copyright (c) 2015 George Wright

    Licensed under the Apache License, Version 2.0 (the "License"); you may not
    use this file except in compliance with the License, as described at
    http://www.apache.org/licenses/ and http://www.pp4s.co.uk/licenses/
}
interface

uses 
  UArrow, UEnemy;

var
  Arrows : array of TArrow;
  Enemies : array of TEnemy;

UGameVariables

unit UGameVariables;
{
    Copyright (c) 2015 George Wright

    Licensed under the Apache License, Version 2.0 (the "License"); you may not
    use this file except in compliance with the License, as described at
    http://www.apache.org/licenses/ and http://www.pp4s.co.uk/licenses/
}
interface

uses
  W3System;

const
  GRAVITY = 1.5;
  FLYING_SPEED_CHANGE = 0.1;
  FLYING_SPEED_MAX = 3;
  ARROW_FREEZE_DURATION_RANGE = 2000;

var
  MaxPower : float;
  ArrowDamage : integer;
  ArrowsFreeze : boolean;
  ArrowFreezeDuration : integer;
  TimeBetweenShots : integer;
  PauseButtonCoordinates : array [0 .. 3] of integer;
  RestartButtonCoordinates : array [0 .. 3] of integer;
  RestartClicked : boolean;
  Paused : boolean;
  Lives : integer;
  Money : integer;

function FloatMod(a, b : float) : integer; // A function to perform modulus operations on floats
function PauseButtonRect() : TRect;
function RestartButtonRect() : TRect;

implementation

function FloatMod(a, b : float) : integer;
begin
  exit(Trunc(a - b * Trunc(a / b)));
end;

function PauseButtonRect() : TRect;
begin
  exit(TRect.Create(PauseButtonCoordinates[0], PauseButtonCoordinates[1], PauseButtonCoordinates[2], PauseButtonCoordinates[3]));
end;

function RestartButtonRect() : TRect;
begin
  exit(TRect.Create(RestartButtonCoordinates[0], RestartButtonCoordinates[1], RestartButtonCoordinates[2], RestartButtonCoordinates[3]));
end;

end.

UGroundUnit

unit UGroundUnit;
{
    Copyright (c) 2015 George Wright

    Licensed under the Apache License, Version 2.0 (the "License"); you may not
    use this file except in compliance with the License, as described at
    http://www.apache.org/licenses/ and http://www.pp4s.co.uk/licenses/
}
interface

uses 
  W3System,
  UEnemy, UTextures;

type TGroundUnit = class(TEnemy)
  public
    constructor Create(newX, newY, newSpeed : float; newHealth, newMoneyValue : integer);
    function GetRect() : TRectF; override;
end;

implementation

constructor TGroundUnit.Create(newX, newY, newSpeed : float; newHealth, newMoneyValue : integer);
begin
  X := newX;
  Y := newY;
  Speed := newSpeed;
  Health := newHealth;
  MaxHealth := newHealth;
  MoneyValue := newMoneyValue;
  ApplyToEventHandler();
end;

function TGroundUnit.GetRect() : TRectF;
begin
  exit(TRectF.Create(X, Y, X + GroundUnitTexture.Handle.width, Y + GroundUnitTexture.Handle.height));
end;

end.

UMouseInputs

unit UMouseInputs;
{
    Copyright (c) 2015 George Wright

    Licensed under the Apache License, Version 2.0 (the "License"); you may not
    use this file except in compliance with the License, as described at
    http://www.apache.org/licenses/ and http://www.pp4s.co.uk/licenses/
}
interface

uses 
  W3System, W3Components, W3Touch,
  USpawner, UGameVariables, UGameItems, UPlayerData, UShop, UShopData, UScalingInfo;

procedure DownHandler(b : TMouseButton; x, y : integer);
procedure UpHandler(b : TMouseButton; x, y : integer);
procedure MoveHandler(x, y : integer);
procedure MouseDownHandler(sender : TObject; b : TMouseButton; t : TShiftState; x, y : integer);
procedure MouseUpHandler(sender : TObject; b : TMouseButton; t : TShiftState; x, y : integer);
procedure MouseMoveHandler(sender : TObject; ss : TShiftState; x, y : integer);
procedure TouchDownHandler(sender : TObject; td : TW3TouchData);
procedure TouchUpHandler(sender : TObject; td : TW3TouchData);
procedure TouchMoveHandler(sender : TObject; td : TW3TouchData);
procedure ChangeTimers(pause : boolean);

var
  MouseDown : boolean;
  MouseDownX, MouseDownY, CurrentMouseX, CurrentMouseY : integer;

implementation

procedure DownHandler(b : TMouseButton; x, y : integer);
begin
  // Make the x and y values correct due to scaling
  x := Round(x * (1 / Scale));
  y := Round(y * (1 / Scale));

  // Only make the mouse down if the left mouse button is pressed
  if (b = TMouseButton.mbLeft) and (Lives > 0) then
    begin
      MouseDown := true;
      MouseDownX := x;
      MouseDownY := y;
      CurrentMouseX := x;
      CurrentMouseY := y;
    end;
end;

procedure UpHandler(b : TMouseButton; x, y : integer);
begin
  // Make the x and y values correct due to scaling
  x := Round(x * (1 / Scale));
  y := Round(y * (1 / Scale));

  // Only check the restart button if the player has lost
  if Lives <= 0 then
    begin
      if RestartButtonRect().ContainsPoint(TPoint.Create(x, y)) then
        begin
          // Tell the main program that the restart button was clicked
          RestartClicked := true;
        end;

      // Stop other mouse input checks
      exit;
    end;

  // Change whether the game is paused if the shop/resume button was clicked
  if (MouseDown) and (b = TMouseButton.mbLeft) and (PauseButtonRect().ContainsPoint(TPoint.Create(x, y))) then
    begin
      MouseDown := false;

      // Pause/Resume all timers
      ChangeTimers(Paused);

      // Invert the paused variable
      Paused := not Paused;

      // Clear the shop message
      PurchaseMessage := "";
    end
  else if not Paused then
    begin
      // Only fire if the left mouse button was clicked
      if (MouseDown) and (b = TMouseButton.mbLeft) then
        begin
          Player.Fire();
        end;

      // Set the player's velocities to 0 to avoid display issues
      Player.UpdateInformation(0, 0, 0, 0);
    end
  else
    begin
      // Check what has been clicked in the shop
      Shop.CheckClicked(x, y);
    end;

  MouseDown := false;
end;

procedure MoveHandler(x, y : integer);
begin
  // Make the x and y values correct due to scaling
  x := Round(x * (1 / Scale));
  y := Round(y * (1 / Scale));

  // Store mouse position
  CurrentMouseX := x;
  CurrentMouseY := y;

  if (MouseDown) and (not Paused) and (Lives > 0) then
    begin
      Player.UpdateInformation(MouseDownX, MouseDownY, CurrentMouseX, CurrentMouseY);
    end;
end;

procedure MouseDownHandler(sender : TObject; b : TMouseButton; t : TShiftState; x, y : integer);
begin
  DownHandler(b, x, y);
end;

procedure MouseUpHandler(sender : TObject; b : TMouseButton; t : TShiftState; x, y : integer);
begin
  UpHandler(b, x, y);
end;

procedure MouseMoveHandler(sender : TObject; ss : TShiftState; x, y : integer);
begin
  MoveHandler(x, y);
end;

procedure TouchDownHandler(sender : TObject; td : TW3TouchData);
begin
  DownHandler(TMouseButton.mbLeft, td.Touches.Touches[0].PageX, td.Touches.Touches[0].PageY);
end;

procedure TouchUpHandler(sender : TObject; td : TW3TouchData);
begin
  // Use previous x and y positions as the Touch has been removed
  UpHandler(TMouseButton.mbLeft, Round(CurrentMouseX * Scale), Round(CurrentMouseY * Scale));
end;

procedure TouchMoveHandler(sender : TObject; td : TW3TouchData);
begin
  MoveHandler(td.Touches.Touches[0].PageX, td.Touches.Touches[0].PageY);
end;

procedure ChangeTimers(pause : boolean);
begin
  if pause then
    begin
      // Resume timers if being un-paused
      Player.ResumeTimer();
      StartEnemySpawners();
      for var i := 0 to High(Enemies) do
        begin
          Enemies[i].ResumeTimer();
        end;
    end
  else
    begin
      // Pause timers if being paused
      Player.PauseTimer();
      PauseEnemySpawners();
      for var i := 0 to High(Enemies) do
        begin
          Enemies[i].PauseTimer();
        end;
    end;
end;

end.

UPlayer

unit UPlayer;
{
    Copyright (c) 2015 George Wright

    Licensed under the Apache License, Version 2.0 (the "License"); you may not
    use this file except in compliance with the License, as described at
    http://www.apache.org/licenses/ and http://www.pp4s.co.uk/licenses/
}
interface

uses 
  UArcher;

type TPlayer = class(TArcher)
  public
    ExtraArchers : array of TArcher;
    procedure UpdateInformation(origX, origY, currX, currY : float); override;
    procedure Fire(); override;
    procedure UpdateY(newPos : float);
end;

implementation

procedure TPlayer.UpdateInformation(origX, origY, currX, currY : float);
begin
  inherited(origX, origY, currX, currY);

  // Update information for the extra archers
  for var i := 0 to High(ExtraArchers) do
    begin
      ExtraArchers[i].UpdateInformation(origX, origY, currX, currY);

      // Set the archer's ability to shoot to that of the player
      if not ExtraArchers[i].CanShoot = CanShoot then
        begin
          ExtraArchers[i].CanShoot := CanShoot;
        end;
    end;
end;

procedure TPlayer.Fire();
begin
  // Make extra archers also fire if the player can
  if CanShoot then
    begin
      for var i := 0 to High(ExtraArchers) do
        begin
          ExtraArchers[i].Fire();
        end;
    end;

  // Then fire the player
  inherited();
end;

procedure TPlayer.UpdateY(newPos : float);
var
  posChange : float;
begin
  // Get the change in y position
  posChange := Y - newPos;

  // Update the player's and extra archer's y position
  Y -= posChange;
  for var i := 0 to High(ExtraArchers) do
    begin
      ExtraArchers[i].Y -= posChange;
    end;
end;

end.

UPlayerData

unit UPlayerData;
{
    Copyright (c) 2015 George Wright

    Licensed under the Apache License, Version 2.0 (the "License"); you may not
    use this file except in compliance with the License, as described at
    http://www.apache.org/licenses/ and http://www.pp4s.co.uk/licenses/
}
interface

uses 
  UPlayer;

var
  Player : TPlayer;

UScalingInfo

unit UScalingInfo;
{
    Copyright (c) 2015 George Wright

    Licensed under the Apache License, Version 2.0 (the "License"); you may not
    use this file except in compliance with the License, as described at
    http://www.apache.org/licenses/ and http://www.pp4s.co.uk/licenses/
}
interface

uses 
  W3Graphics;

procedure ScaleCanvas(newScreenWidth, newScreenHeight : integer);

const
  GAMEWIDTH = 1366;
  GAMEHEIGHT = 598;
  PIXELTOPOWERRATIO = 12;

var
  Scale : float;

implementation

procedure ScaleCanvas(newScreenWidth, newScreenHeight : integer);
var
  gameLength, gameDepth : float;
begin
  // Get the new x and y lengths
  gameLength := newScreenWidth;
  gameDepth := (gameLength / 16) * 7; // Using 16:7 aspect ratio

  // If the new game size is too tall use the height as the base of the new scale
  if gameDepth >= newScreenHeight then
    begin
      gameDepth := newScreenHeight;
      gameLength := (gameDepth / 7) * 16;
    end;

  // Get the new scale
  Scale := gameLength / GAMEWIDTH;
end;

end.

UShop

unit UShop;
{
    Copyright (c) 2015 George Wright

    Licensed under the Apache License, Version 2.0 (the "License"); you may not
    use this file except in compliance with the License, as described at
    http://www.apache.org/licenses/ and http://www.pp4s.co.uk/licenses/
}
interface

uses 
  W3Image, W3Graphics,
  UShopItem, UShopData;

type TShop = class(TObject)
  public
    Items : array [0 .. 4] of TShopItem;
    Loaded : boolean;
    constructor Create();
    procedure ResetItems();
    procedure Draw(canvas : TW3Canvas);
    procedure CheckClicked(xPos, yPos : integer);
  private
    ContentLoaded : array [0 .. 4] of boolean;
    procedure CheckLoadedContent();
end;

var
  Shop : TShop;

implementation

constructor TShop.Create();
var
  textures : array [0 .. 4] of TW3Image;
begin
  Loaded := false;
  ContentLoaded := [ false, false, false, false, false ];

  // Load textures
  textures[0] := TW3Image.Create(nil);
  textures[0].LoadFromURL("res\ArcherThumbnail.png");
  textures[1] := TW3Image.Create(nil);
  textures[1].LoadFromURL("res\DamageThumbnail.png");
  textures[2] := TW3Image.Create(nil);
  textures[2].LoadFromURL("res\IceThumbnail.png");
  textures[3] := TW3Image.Create(nil);
  textures[3].LoadFromURL("res\RangeThumbnail.png");
  textures[4] := TW3Image.Create(nil);
  textures[4].LoadFromURL("res\SpeedThumbnail.png");

  // Apply to OnLoad event
  textures[0].OnLoad :=  procedure(o : TObject) begin ContentLoaded[0] := true;  CheckLoadedContent(); end;
  textures[1].OnLoad :=  procedure(o : TObject) begin ContentLoaded[1] := true;  CheckLoadedContent(); end;
  textures[2].OnLoad :=  procedure(o : TObject) begin ContentLoaded[2] := true;  CheckLoadedContent(); end;
  textures[3].OnLoad :=  procedure(o : TObject) begin ContentLoaded[3] := true;  CheckLoadedContent(); end;
  textures[4].OnLoad :=  procedure(o : TObject) begin ContentLoaded[4] := true;  CheckLoadedContent(); end;

  // Create each shop item
  Items[0] := TShopItem.Create(150, 20, 5, 1000, 1.3, "Extra Archers", textures[0], AddArcher);
  Items[1] := TShopItem.Create(150, 80, -1, 100, 1.3, "Increase Damage", textures[1], IncreaseDamage);
  Items[2] := TShopItem.Create(150, 140, 5, 700, 1.6, "Freeze", textures[2], IncreaseIce);
  Items[3] := TShopItem.Create(150, 200, 10, 100, 1.2, "Increase Max Power", textures[3], IncreaseRange);
  Items[4] := TShopItem.Create(150, 260, 8, 300, 1.4, "Decrease Reload Speed", textures[4], DecreaseReload);
end;

procedure TShop.ResetItems();
begin
  for var i := 0 to 4 do
    begin
      Items[i].Reset();
    end;
end;

procedure TShop.Draw(canvas : TW3Canvas);
begin
  for var i := 0 to 4 do
    begin
      Items[i].Draw(canvas);
    end;

  // Draw the message
  canvas.FillStyle := "rgb(110, 0, 0)";
  canvas.Font := "24pt verdana";
  canvas.TextAlign := "center";
  canvas.TextBaseLine := "top";
  canvas.FillTextF(PurchaseMessage, 150 + SHOP_WIDTH / 2, 320, SHOP_WIDTH * 2);
end;

procedure TShop.CheckClicked(xPos, yPos : integer);
begin
  for var i := 0 to High(Items) do
    begin
      // Purchase the item if it was clicked
      if Items[i].IsInButton(xPos, yPos) then
        begin
          Items[i].Purchase();
          break;
        end;
    end;
end;

procedure TShop.CheckLoadedContent();
begin
  // Check each content is loaded
  for var i := 0 to High(ContentLoaded) do
    begin
      if not ContentLoaded[i] then
        begin
          exit
        end;
    end;

  // If they are all loaded state all content is loaded
  Loaded := true;
end;

end.

UShopData

unit UShopData;
{
    Copyright (c) 2015 George Wright

    Licensed under the Apache License, Version 2.0 (the "License"); you may not
    use this file except in compliance with the License, as described at
    http://www.apache.org/licenses/ and http://www.pp4s.co.uk/licenses/
}
interface

uses 
  UArcher, UGameVariables, UPlayerData, UTextures;

var
  PurchaseMessage : string;

// Purchase event handlers for shop items
procedure AddArcher();
procedure IncreaseDamage();
procedure IncreaseRange();
procedure DecreaseReload();
procedure IncreaseIce();

implementation

procedure AddArcher();
begin
  if Length(Player.ExtraArchers) = 0 then
    begin
      // Create the first extra archer for the player
      Player.ExtraArchers[0] := TArcher.Create(Player.X - ArcherTexture.Handle.width - 30, Player.Y + 20);
    end
  else
    begin
      // Get the y change compared to the last archer
      var yChange := 20;
      if Length(Player.ExtraArchers) mod 2 = 0 then
        begin
          // If there in an odd number of archers the next archer is above the previous
          yChange *= -1;
        end;

      // Get the y and x position
      var xPos := Player.ExtraArchers[High(Player.ExtraArchers)].X - ArcherTexture.Handle.width - 30;
      var yPos := Player.ExtraArchers[High(Player.ExtraArchers)].Y + yChange;

      // Create the new archer
      Player.ExtraArchers[Length(Player.ExtraArchers)] := TArcher.Create(xPos, yPos);
    end;
end;

procedure IncreaseDamage();
begin
  ArrowDamage += 10;
end;

procedure IncreaseRange();
begin
  MaxPower += 2;
end;

procedure DecreaseReload();
begin
  TimeBetweenShots -= 200;
end;

procedure IncreaseIce();
begin
  ArrowsFreeze := true;
  ArrowFreezeDuration += 500;
end;

end.

UShopItem

unit UShopItem;

interface

uses 
  W3System, W3Graphics, W3Image,
  UGameVariables, UShopData;

type EPurchasedEvent = procedure();

type TShopItem = class(TObject)
  public
    X, Y : integer;
    Price, OrigPrice, UnitsSold, MaxUnitsSold : integer;
    PriceAfterPurchaseMultiplier : float;
    ItemName : string;
    Thumbnail : TW3Image;
    constructor Create(newX, newY, newMaxUnitsToSell, newPrice : integer; newPriceAfterPurchaseMultiplier : float; newName : string; newThumbnail : TW3Image; purchaseHandler : procedure);
    procedure Reset();
    procedure Purchase();
    procedure Draw(canvas : TW3Canvas);
    function IsInButton(xPos, yPos : integer) : boolean;
  private
    PurchaseEvent : EPurchasedEvent;
    property OnPurchase : EPurchasedEvent read PurchaseEvent write PurchaseEvent;
end;

const
  SHOP_WIDTH = 360;

implementation

constructor TShopItem.Create(newX, newY, newMaxUnitsToSell, newPrice : integer; newPriceAfterPurchaseMultiplier : float; newName : string; newThumbnail : TW3Image; purchaseHandler : procedure);
begin
  X := newX;
  Y := newY;
  UnitsSold := 0;
  MaxUnitsSold := newMaxUnitsToSell;
  Price := newPrice;
  OrigPrice := Price;
  PriceAfterPurchaseMultiplier := newPriceAfterPurchaseMultiplier;
  ItemName := newName;
  Thumbnail := newThumbnail;
  OnPurchase := purchaseHandler;
end;

procedure TShopItem.Purchase();
begin
  // Only run the handler if the event has one
  if Assigned(PurchaseEvent) then
    begin
      // Check if it is affordable or if more can be sold
      if (not MaxUnitsSold <= -1) and (UnitsSold >= MaxUnitsSold) then
        begin
          PurchaseMessage := "No more of this item can be purchased!";
        end
      else if Money < Price then
        begin
          PurchaseMessage := "You cannot afford that!";
        end
      else
        begin
          // If the item can be purchased...
          Inc(UnitsSold); // Increment the amount of units sold
          Money -= Price; // Take away the appropriate amount of money
          PurchaseEvent(); // Run the event handler
          PurchaseMessage := "Item purchased!"; // Tell the player the item has been brought

          // Increase the price by the price multiplier
          Price := Round(Price * PriceAfterPurchaseMultiplier);
        end;
    end
  else
    begin
      // If there is no event handler the item cannot be purchased
      PurchaseMessage := "This item cannot be purchased!";
    end;
end;

procedure TShopItem.Reset();
begin
  // Reset price and how many has been sold
  Price := OrigPrice;
  UnitsSold := 0;
end;

procedure TShopItem.Draw(canvas : TW3Canvas);
begin
  // Draw the background rectangle
  canvas.FillStyle := "rgb(110, 0, 0)";
  canvas.FillRect(X, Y, SHOP_WIDTH, Thumbnail.Handle.height + 6);

  // Draw item icon
  canvas.DrawImageF(Thumbnail.Handle, X + 3, Y + 3);

  // Draw the label and price
  canvas.FillStyle := "rgb(0, 0, 0)";
  canvas.Font := "14pt verdana";
  canvas.TextAlign := "left";
  canvas.TextBaseLine := "middle";
  canvas.FillTextF(ItemName, X + 10 + Thumbnail.Handle.width, Y + 6 + Thumbnail.Handle.height / 4, 140);
  canvas.TextAlign := "right";
  canvas.FillText("$" + IntToStr(Price), X + SHOP_WIDTH - 76, Y + 6 + Thumbnail.Handle.height / 2, 60);

  // Draw the level
  canvas.Font := "10pt verdana";
  canvas.TextAlign := "left";
  canvas.TextBaseLine := "bottom";
  canvas.FillTextF("Level " + IntToStr(UnitsSold + 1), X + 10 + Thumbnail.Handle.width, Y + Thumbnail.Handle.height, 140);

  // Draw the button
  canvas.StrokeStyle := "rgb(0, 0, 0)";
  canvas.LineWidth := 3;
  canvas.FillStyle := "rgb(130, 120, 140)";
  canvas.StrokeRectF(X + SHOP_WIDTH - 73, Y + 3, 70, Thumbnail.Handle.height);
  canvas.FillRectF(X + SHOP_WIDTH - 73, Y + 3, 70, Thumbnail.Handle.height);

  // Draw the text inside the button
  canvas.FillStyle := "rgb(0, 0, 0)";
  canvas.Font := "16pt verdana";
  canvas.TextAlign := "center";
  canvas.TextBaseLine := "middle";
  canvas.FillTextF("Purchase", X + SHOP_WIDTH - 38, Y + 3 + Thumbnail.Handle.height / 2, 60);

  // Put a line through the item if no more can be bought
  if (not MaxUnitsSold <= -1) and (UnitsSold >= MaxUnitsSold) then
    begin
      canvas.StrokeStyle := "rgb(0, 0, 0)";
      canvas.LineWidth := 10;
      canvas.BeginPath();
      canvas.MoveToF(X - 10, Y + 3 + Thumbnail.Handle.height / 2);
      canvas.LineToF(X + SHOP_WIDTH + 10, Y + 3 + Thumbnail.Handle.height / 2);
      canvas.ClosePath();
      canvas.Stroke();
    end;
end;

function TShopItem.IsInButton(xPos, yPos : integer) : boolean;
var
  buttonRect : TRectF;
begin
  // Make the button a TRect
  buttonRect := TRectF.Create(X + SHOP_WIDTH - 73, Y + 3, X + SHOP_WIDTH - 3, Y + 3 + Thumbnail.Handle.height);

  // Return if the point is in the button
  exit(buttonRect.ContainsPoint(TPointF.Create(xPos, yPos)))
end;

end.	

USpawner

unit USpawner;
{
    Copyright (c) 2015 George Wright

    Licensed under the Apache License, Version 2.0 (the "License"); you may not
    use this file except in compliance with the License, as described at
    http://www.apache.org/licenses/ and http://www.pp4s.co.uk/licenses/
}
interface

uses 
  W3Time,
  UArrow, UGroundUnit, UAirUnit, UGameVariables, UGameItems, UScalingInfo, UTextures;

procedure SpawnArrow(xVol, yVol, x, y : float);
procedure StartGroundTimer(delay : integer);
procedure StartAirTimer(delay : integer);
procedure StartEnemySpawners();
procedure PauseEnemySpawners();
procedure SpawnGroundUnit();
procedure SpawnAirUnit();
procedure IncreaseDifficulty();
function HandleGroundTimer(sender : TObject) : boolean;
function HandleAirTimer(sender : TObject) : boolean;
function GetNextEnemyIndex() : integer;

var
  GroundTimer, AirTimer : TW3EventRepeater; // Timers for enemy spawn
  GroundDelay, AirDelay, GroundDelayHolder, AirDelayHolder : integer; // Delays and delay holders for the timers
  Difficulty : integer; // The difficulty level

implementation

procedure SpawnArrow(xVol, yVol, x, y : float);
begin
  // Change the x and y velocity if they exceed the max power
  if (Sqrt(Sqr(xVol) + Sqr(yVol)) > MaxPower) then
    begin
      // Work out the angle
      var ang := ArcTan2(yVol, xVol);

      // Change the velocities to match the angle at max power
      xVol := Cos(ang) * MaxPower;
      yVol := Sin(ang) * MaxPower;
    end;

  for var i := 0 to High(Arrows) do
    begin
      // If the arrow is inactive spawn one at this index
      if not Arrows[i].Active then
        begin
          Arrows[i] := TArrow.Create(x, y, xVol, yVol);
          exit;
        end;
    end;

  // Spawn an arrow if an inactive one wasn't found
  Arrows[High(Arrows) + 1] := TArrow.Create(x, y, xVol, yVol);
end;

procedure StartGroundTimer(delay : integer);
begin
  GroundTimer := TW3EventRepeater.Create(HandleGroundTimer, delay);
end;

procedure StartAirTimer(delay : integer);
begin
  AirTimer := TW3EventRepeater.Create(HandleAirTimer, delay);
end;

procedure StartEnemySpawners();
begin
  // Start the ground timer
  if GroundDelayHolder <= 0 then
    begin
      StartGroundTimer(GroundDelay);
    end
  else
    begin
      StartGroundTimer(GroundDelayHolder);
    end;

  // Start the air timer
  if AirDelayHolder <= 0 then
    begin
      StartAirTimer(AirDelay);
    end
  else
    begin
      StartAirTimer(AirDelayHolder);
    end;

  // Clear delay holders
  GroundDelayHolder := 0;
  AirDelayHolder := 0;
end;

procedure PauseEnemySpawners();
begin
  // Record the delays
  GroundDelayHolder := GroundTimer.Delay;
  AirDelayHolder := AirTimer.Delay;

  // Destroy the timers
  GroundTimer.Destroy();
  AirTimer.Destroy();
end;

procedure SpawnGroundUnit();
var
  speed : float;
  health : integer;
  moneyVal : integer;
begin
  // Get a random speed
  speed := RandomInt(3500) / 1000; // Max speed is 3.5

  // Make the health multiplied by the speed equal the difficulty
  health := Round(Difficulty / speed);

  // Generate a money value
  moneyVal := (Difficulty div 25 + RandomInt(Difficulty div 25)) div 2;

  // Spawn the enemy
  Enemies[GetNextEnemyIndex()] := TGroundUnit.Create(GAMEWIDTH, GAMEHEIGHT - GroundUnitTexture.Handle.height, speed, health, moneyVal);

  // Increase the difficulty
  IncreaseDifficulty();
end;

procedure SpawnAirUnit();
var
  speed : float;
  yChange, y, health : integer;
  moneyVal : integer;
begin
  // Get the y change
  yChange := RandomInt(GAMEHEIGHT div 4);

  // Generate a starting y position from the y change
  y := RandomInt(GAMEHEIGHT - yChange * 2 - AirUnitTexture.Handle.height * 2) + yChange;

  // Get speed and make health indirectly proportionate to the speed
  speed := RandomInt(3500) / 1000; // Max speed is 3.5
  health := Round((Difficulty - yChange) / speed);

  // Generate a money value
  moneyVal := (Difficulty div 33 + RandomInt(Difficulty div 33)) div 2;

  // Spawn the flying enemy
  Enemies[GetNextEnemyIndex()] := TAirUnit.Create(GAMEWIDTH, y, speed, yChange, health, moneyVal);

  // Increase the difficulty
  IncreaseDifficulty();
end;

procedure IncreaseDifficulty();
begin
  Difficulty += 150;
end;

function HandleGroundTimer(sender : TObject) : boolean;
begin
  SpawnGroundUnit();

  // Release the timer then restart it
  TW3EventRepeater(sender).Free();
  StartGroundTimer(RandomInt(GroundDelay) + GroundDelay);
  exit(true);
end;

function HandleAirTimer(sender : TObject) : boolean;
begin
  SpawnAirUnit();

  // Release the timer then restart it
  TW3EventRepeater(sender).Free();
  StartAirTimer(RandomInt(AirDelay) + AirDelay);
  exit(true);
end;

function GetNextEnemyIndex() : integer;
begin
  // Check for dead enemies
  for var i := 0 to High(Enemies) do
    begin
      if Enemies[i].Dead then
        begin
          exit(i); // Return the first index of a dead enemy
        end;
    end;

  // If no dead enemies were found create a new index
  exit(Length(Enemies));
end;

end.

UTextures

unit UTextures;
{
    Copyright (c) 2015 George Wright

    Licensed under the Apache License, Version 2.0 (the "License"); you may not
    use this file except in compliance with the License, as described at
    http://www.apache.org/licenses/ and http://www.pp4s.co.uk/licenses/
}
interface

uses 
  W3Image;

var
  GroundUnitTexture : TW3Image;
  FrozenGroundUnitTexture : TW3Image;
  AirUnitTexture : TW3Image;
  FrozenAirUnitTexture : TW3Image;
  BowTexture : TW3Image;
  ArrowTexture : TW3Image;
  ArcherTexture : TW3Image;
  TowerTexture : TW3Image;
Programming - a skill for life!

by George Wright: Y10 Age ~14