Code of Units UAirUnit to UEnemy

Code of Units UAirUnit to UEnemy of TowerOfArcher by George Wright

UAirUnit

unit UAirUnit;
{
    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, UGameVariables, UTextures;

type TAirUnit = class(TEnemy)
  public
    MaxSpeed : float;
    YSpeed, YChanged, MaxYChange : float;
    MovingUp : boolean;
    constructor Create(newX, newY, newSpeed, newYChange : float; newHealth, newMoneyValue : integer);
    procedure Move(); override;
    function GetRect() : TRectF; override;
end;

implementation

constructor TAirUnit.Create(newX, newY, newSpeed, newYChange : float; newHealth, newMoneyValue : integer);
begin
  X := newX;
  Y := newY + newYChange / 2; // Makes the middle of bobbing at the y specified
  Speed := newSpeed;
  MaxSpeed := newSpeed;
  MaxHealth := newHealth;
  Health := newHealth;
  MoneyValue := newMoneyValue;
  YSpeed := 0;
  MaxYChange := newYChange;
  YChanged := 0;
  MovingUp := true;
  ApplyToEventHandler();
end;

procedure TAirUnit.Move();
begin
  inherited();
  Y += YSpeed;

  // Increase the speed if it is not moving at its max speed
  if Speed < MaxSpeed then
    begin
      Speed += FLYING_SPEED_CHANGE * 3;
    end;

  if MovingUp then
    begin
      // Increase the speed of moving up if it has not reached the max yet
      if -YSpeed < FLYING_SPEED_MAX then
        begin
          YSpeed -= FLYING_SPEED_CHANGE;
        end;

      if -YChanged > MaxYChange then
        begin
          // If it has moved its max, start going down again
          MovingUp := false;
        end
      else
        begin
          // Keep track on how much it has moved up
          YChanged += YSpeed;
        end;
    end
  else
    begin
      // Increase the speed of moving down if it has not reached the max yet
      if YSpeed < FLYING_SPEED_MAX then
        begin
          YSpeed += FLYING_SPEED_CHANGE;
        end;

      if YChanged > MaxYChange then
        begin
          // If it has moved its max, start going up again
          MovingUp := true;
        end
      else
        begin
          // Keep track on how much it has moved down
          YChanged += YSpeed;
        end;
    end;
end;

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

end.

UArcher

unit UArcher;
{
    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,
  USpawner, UGameVariables, UTextures, UScalingInfo;

type TArcher = class(TObject)
  public
    X, Y : float;
    XVol, YVol : float; // The predicted x and y velocities of shots
    CanShoot : boolean;
    constructor Create(newX, newY : float);
    procedure UpdateInformation(origX, origY, currX, currY : float); virtual;
    procedure Fire(); virtual;
    function ArrowSpawnPoint() : array [0 .. 1] of float;
    function Angle() : float;
    function Power() : float;
    procedure PauseTimer();
    procedure ResumeTimer();
  private
    Timer : TW3EventRepeater; // Timer for shots
    DelayHolder : integer;
    function HandleTimer(sender : TObject) : boolean;
end;

implementation

constructor TArcher.Create(newX, newY : float);
begin
  X := newX;
  Y := newY;
  XVol := 0;
  YVol := 0;
  CanShoot := true;
end;

procedure TArcher.UpdateInformation(origX, origY, currX, currY : float);
begin
  // Work out the x and y velocities
  XVol := (origX - currX) / (PIXELTOPOWERRATIO * (1 / Scale));
  YVol := (origY - currY) / (PIXELTOPOWERRATIO * (1 / Scale));
end;

procedure TArcher.Fire();
begin
  if CanShoot then
    begin
      SpawnArrow(XVol, YVol, ArrowSpawnPoint()[0], ArrowSpawnPoint()[1]);

      XVol := 0;
      YVol := 0;

      // Make the player unable to shoot and start the timer again
      CanShoot := false;
      Timer := TW3EventRepeater.Create(HandleTimer, TimeBetweenShots);
    end;
end;

function TArcher.ArrowSpawnPoint() : array [0 .. 1] of float;
var
  xPoint, yPoint : float;
begin
  // Get the x and y points using trigonometry
  xPoint := X + ArcherTexture.Handle.width / 2 + Cos(Angle()) * BowTexture.Handle.width;
  yPoint := Y + ArcherTexture.Handle.height / 3 + Sin(Angle()) * BowTexture.Handle.width;

  exit([xPoint, yPoint]);
end;

function TArcher.Angle() : float;
begin
  // Return the angle
  exit(ArcTan2(YVol, XVol));
end;

function TArcher.Power() : float;
var
  retVal : float;
begin
  // Return nothing if the archer cannot shoot
  if not CanShoot then
    begin
      exit(0);
    end;

  // Get the power
  retVal := Sqrt(Sqr(XVol) + Sqr(YVol));

  // Return max power if above max power, otherwise return the calculated power
  if retVal > MaxPower then
    begin
      exit(MaxPower);
    end
  else
    begin
      exit(retVal);
    end;
end;

procedure TArcher.PauseTimer();
begin
  // Store the delay then destroy the timer
  DelayHolder := Timer.Delay;
  Timer.Destroy();
end;

procedure TArcher.ResumeTimer();
begin
  // Recreate the timer then reset the delay holder
  Timer := TW3EventRepeater.Create(HandleTimer, DelayHolder);
  DelayHolder := 0;
end;

function TArcher.HandleTimer(sender : TObject) : boolean;
begin
  CanShoot := true;
  TW3EventRepeater(sender).Free();
  exit(true);
end;

end.

UArrow

unit UArrow;
{
    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, UGameVariables, UTextures, UScalingInfo;

type TArrow = class(TObject)
  public
    X, Y, XVol, YVol : float;
    Active : boolean;
    constructor Create(newX, newY, newXVol, newYVol : float);
    procedure Move();
    function GetAngle(deg : boolean = false) : float;
    function MaxX() : float;
    function MinX() : float;
    function MaxY() : float;
    function MinY() : float;
    function GetRect() : TRectF;
    procedure CheckCollisions(enemies : array of TEnemy; prevX, prevY : float);
  private
    function CheckCollision(enemy : TEnemy; prevX, prevY : float) : boolean; overload;
    function RectsIntersect(rect1, rect2 : TRectF) : boolean;
    function SidesOverlap(sides1, sides2 : array [0 .. 1] of float) : boolean;
end;

implementation

constructor TArrow.Create(newX, newY, newXVol, newYVol : float);
begin
  X := newX;
  Y := newY;
  XVol := newXVol;
  YVol := newYVol;
  Active := true;
end;

procedure TArrow.Move();
begin
  // Update x and y coordinates
  X += XVol;
  Y += YVol;

  // Add gravity affect
  YVol += GRAVITY;

  // Make the bullet inactive if off screen
  if (MaxX() < 0) or (MinX() > GAMEWIDTH) or (MinY() > GAMEHEIGHT) then
    begin
      Active := false;
    end;
end;

function TArrow.GetAngle(deg : boolean = false) : float;
var
  retVal : float;
begin
  // Get the angle from the velocity
  retVal := ArcTan2(YVol, XVol);

  // Convert to degrees if ordered to
  if deg then
    begin
      retVal *= 180 / Pi();
    end;

  exit(retVal);
end;

function TArrow.MaxX() : float;
begin
  // Get the current angle (stops us running the same method over and over again)
  var currAng := FloatMod(GetAngle(true), 360);

  // Work out the max x value
  if (currAng <= 90) or ((currAng > 180) and (currAng <= 270)) then
    begin
      exit(X + Cos((currAng mod 90) * Pi() / 180) * ArrowTexture.Handle.width);
    end
  else
    begin
      exit(X + Sin((currAng mod 90) * Pi() / 180) * ArrowTexture.Handle.width + Cos((currAng mod 90) * Pi() / 180) * ArrowTexture.Handle.height);
    end;
end;

function TArrow.MinX() : float;
begin
  // Get the current angle (stops us running the same method over and over again)
  var currAng := FloatMod(GetAngle(true), 360);

  // Work out the min x value
  if (currAng <= 90) or ((currAng > 180) and (currAng <= 270)) then
    begin
      exit(X - Sin((currAng mod 90) * Pi() / 180) * ArrowTexture.Handle.height);
    end
  else
    begin
      exit(X);
    end;
end;

function TArrow.MaxY() : float;
begin
  // Get the current angle (stops us running the same method over and over again)
  var currAng := FloatMod(GetAngle(true), 360);

  // Work out the max y value
  if (currAng <= 90) or ((currAng > 180) and (currAng <= 270)) then
    begin
      exit(Y + Cos((currAng mod 90) * Pi() / 180) * ArrowTexture.Handle.height + Sin((currAng mod 90) * Pi() / 180) * ArrowTexture.Handle.width);
    end
  else
    begin
      exit(Y + Sin((currAng mod 90) * Pi() / 180) * ArrowTexture.Handle.height);
    end;
end;

function TArrow.MinY() : float;
begin
  // Get the current angle (stops us running the same method over and over again)
  var currAng := FloatMod(GetAngle(true), 360);

  // Work out the min y value
  if (currAng <= 90) or ((currAng > 180) and (currAng <= 270)) then
    begin
      exit(Y);
    end
  else
    begin
      exit(Y - Cos((currAng mod 90) * Pi() / 180) * ArrowTexture.Handle.width);
    end;
end;

function TArrow.GetRect() : TRectF;
begin
  exit(TRectF.Create(MinX(), MinY(), MaxX(), MaxY()));
end;

procedure TArrow.CheckCollisions(enemies : array of TEnemy; prevX, prevY : float);
var
  pathRect : TRectF; // The rectangle of which the arrow has moved
begin
  // Create the path rect
  pathRect := TRectF.Create(prevX, prevY, MaxX(), MaxY());

  // Check over each enemy
  for var i := 0 to High(enemies) do
    begin
      // Only check enemies that are active with health
      if enemies[i].Health > 0 then
        begin
          // If the enemy was in the flight path of the arrow perform more detailed analysis
          if RectsIntersect(pathRect, enemies[i].GetRect()) then
            begin
              if CheckCollision(enemies[i], prevX, prevY) then
                begin
                  // If the arrow did actually hit the enemy run the hit procedure on it and exit the loop
                  enemies[i].Hit(ArrowDamage, XVol, YVol);

                  // Freeze the enemy if the arrows freeze enemies
                  if ArrowsFreeze then
                    begin
                      enemies[i].Freeze(ArrowFreezeDuration, ArrowFreezeDuration + ARROW_FREEZE_DURATION_RANGE);
                    end;

                  // Tell the arrow it is inactive now
                  Active := false;
                  break;
                end;
            end;
        end;
    end;
end;

function TArrow.CheckCollision(enemy : TEnemy; prevX, prevY : float) : boolean;
var
  distance : integer;
  arrowsInDistance : integer;
  xChangePerLoop, yChangePerLoop : float;
  testArrow : TArrow;
begin
  // Get the distance the arrow has travelled
  distance := Ceil(Sqrt(Sqr(MaxX() - prevX) + Sqr(MaxY - prevY)));

  // Get how many arrows fit in the distance (add 1 so it does at least 2 arrows)
  arrowsInDistance := Ceil(distance / ArrowTexture.Handle.width) + 1;

  // Use the distance as the divider
  xChangePerLoop := (MaxX() - prevX) / arrowsInDistance;
  yChangePerLoop := (MaxY() - prevY) / arrowsInDistance;

  // Create an arrow in the original position with the previous velocity
  testArrow := TArrow.Create(prevX, prevY, XVol, YVol - GRAVITY);

  // Move the arrow in small steps to see if it hits the enemy
  for var i := 0 to arrowsInDistance do
    begin
      // Test to see if it has collided
      if RectsIntersect(testArrow.GetRect(), enemy.GetRect()) then
        begin
          exit(true);
        end
      else
        begin
          // If it did not move the arrow by a small amout
          testArrow.X += xChangePerLoop;
          testArrow.Y += yChangePerLoop;
        end
    end;

  // If the arrow did not in fact hit return false
  exit(false);
end;

function TArrow.RectsIntersect(rect1, rect2 : TRectF) : boolean;
var
  xSides1, xSides2, ySides1, ySides2 : array [0 .. 1] of float;
begin
  // Get rect1's sides
  xSides1 := [rect1.Left, rect1.Right];
  ySides1 := [rect1.Top, rect1.Bottom];

  // Get rect2's sides
  xSides2 := [rect2.Left, rect2.Right];
  ySides2 := [rect2.Top, rect2.Bottom];

  // Return if the x sides and the y sides both overlap
  exit(SidesOverlap(xSides1, xSides2) and SidesOverlap(ySides1, ySides2));
end;

function TArrow.SidesOverlap(sides1, sides2 : array [0 .. 1] of float) : boolean;
var
  leftDoesntOverlap, rightDoesntOverlap : boolean;
begin
  // Work out if the left doesn't overlap
  leftDoesntOverlap := (sides2[0] < sides1[0]) and (sides2[1] < sides1[0]);

  // Work out if the left doesn't overlap
  rightDoesntOverlap := (sides2[0] > sides1[1]) and (sides2[1] > sides1[1]);

  // Return if the sides did overlap
  exit(not (leftDoesntOverlap or rightDoesntOverlap));
end;

end.

UDrawing

unit UDrawing;
{
    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, W3Graphics,
  UMouseInputs, UArrow, UArcher, UPlayer, UEnemy, UGroundUnit, UAirUnit, UGameVariables, UShop, UShopItem, UTextures, UScalingInfo;

procedure ClearScreen(canvas : TW3Canvas);
procedure DrawLoadingScreen(canvas : TW3Canvas);
procedure DrawScenery(canvas : TW3Canvas);
procedure DrawPlayer(player : TPlayer; canvas : TW3Canvas);
procedure DrawArcher(archer : TArcher; canvas : TW3Canvas);
procedure DrawArrow(arrows : array of TArrow; canvas : TW3Canvas); overload;
procedure DrawArrow(arrow : TArrow; canvas : TW3Canvas); overload;
procedure DrawEnemy(enemy : array of TEnemy; canvas : TW3Canvas); overload;
procedure DrawEnemy(enemy : TEnemy; canvas : TW3Canvas); overload;
procedure DrawMouseDragLine(player : TPlayer; canvas : TW3Canvas);
procedure DrawCanShoot(player : TPlayer; canvas : TW3Canvas);
procedure DrawHUD(canvas : TW3Canvas);
procedure DrawPauseScreen(canvas : TW3Canvas);
procedure DrawGameOver(canvas : TW3Canvas);
procedure RotateCanvas(angle, xChange, yChange : float; canvas : TW3Canvas);

implementation

procedure ClearScreen(canvas : TW3Canvas);
begin
  // Clear background
  canvas.FillStyle := "rgb(255, 255, 255)";
  canvas.FillRectF(0, 0, GAMEWIDTH * 2, GAMEHEIGHT * 2);

  // Draw border
  canvas.StrokeStyle := "rgb(0, 0, 0)";
  canvas.LineWidth := 4;
  canvas.StrokeRectF(0, 0, GAMEWIDTH, GAMEHEIGHT);
end;

procedure DrawLoadingScreen(canvas : TW3Canvas);
begin
  canvas.FillStyle := "blue";
  canvas.Font := "24pt verdana";
  canvas.TextAlign := "center";
  canvas.TextBaseLine := "middle";
  canvas.FillTextF("Loading Content...", GAMEWIDTH / 2, GAMEHEIGHT / 2, 275);
end;

procedure DrawScenery(canvas : TW3Canvas);
begin
  canvas.DrawImageF(TowerTexture.Handle, 0, GAMEHEIGHT - TowerTexture.Handle.height);

  // Draw the shop button
  canvas.StrokeStyle := "rgb(0, 0, 0)";
  canvas.LineWidth := 4;
  canvas.FillStyle := "rgb(130, 120, 140)";
  canvas.StrokeRect(PauseButtonRect());
  canvas.FillRect(PauseButtonRect());

  // Get the correct text
  var text := "Shop";
  if Paused then
    begin
      text := "Resume";
    end;

  // Put the text in the button
  canvas.Font := IntToStr(Round(PauseButtonRect().Width() / 4)) + "pt verdana";
  canvas.FillStyle := "rgb(0, 0, 0)";
  canvas.TextAlign := "center";
  canvas.TextBaseLine := "middle";
  canvas.FillTextF(text, PauseButtonRect().CenterPoint().X, PauseButtonRect().CenterPoint().Y, PauseButtonRect().Width() - 10);
end;

procedure DrawPlayer(player : TPlayer; canvas : TW3Canvas);
begin
  // Draw the player
  DrawArcher(player, canvas);

  // Draw the extra archers
  for var i := 0 to High(player.ExtraArchers) do
    begin
      DrawArcher(player.ExtraArchers[i], canvas);
    end;
end;

procedure DrawArcher(archer : TArcher; canvas : TW3Canvas);
begin
  // Draw the body of the archer
  canvas.DrawImageF(ArcherTexture.Handle, archer.X, archer.Y);

  // Rotate the canvas for the bow
  RotateCanvas(archer.Angle(), archer.X + ArcherTexture.Handle.width / 2, archer.Y + ArcherTexture.Handle.height / 3, canvas);

  // Draw the bow
  canvas.DrawImageF(BowTexture.Handle, archer.X + ArcherTexture.Handle.width / 2, archer.Y + ArcherTexture.Handle.height / 3 - BowTexture.Handle.height / 2);

  // Draw the string drawback
  canvas.StrokeStyle := "rgb(0, 0, 0)";
  canvas.LineWidth := 0.1;
  canvas.BeginPath();
  canvas.MoveToF(archer.X + ArcherTexture.Handle.width / 2 + BowTexture.Handle.width * 3 / 5, archer.Y + ArcherTexture.Handle.height / 3 - BowTexture.Handle.height / 2);
  canvas.LineToF(archer.X + ArcherTexture.Handle.width / 2 + BowTexture.Handle.width * 3 / 5 - archer.Power() / 3, archer.Y + ArcherTexture.Handle.height / 3);
  canvas.MoveToF(archer.X + ArcherTexture.Handle.width / 2 + BowTexture.Handle.width * 3 / 5, archer.Y + ArcherTexture.Handle.height / 3 + BowTexture.Handle.height / 2);
  canvas.LineToF(archer.X + ArcherTexture.Handle.width / 2 + BowTexture.Handle.width * 3 / 5 - archer.Power() / 3, archer.Y + ArcherTexture.Handle.height / 3);
  canvas.ClosePath();
  canvas.Stroke();

  // Un-rotate the canvas
  RotateCanvas(-archer.Angle(), archer.X + ArcherTexture.Handle.width / 2, archer.Y + ArcherTexture.Handle.height / 3, canvas);
end;

procedure DrawArrow(arrows : array of TArrow; canvas : TW3Canvas);
begin
  for var i := 0 to High(arrows) do
    begin
      if arrows[i].Active then
        begin
          DrawArrow(arrows[i], canvas);
        end;
    end;
end;

procedure DrawArrow(arrow : TArrow; canvas : TW3Canvas);
begin
  // Rotate the canvas
  RotateCanvas(arrow.GetAngle(), arrow.X, arrow.Y, canvas);

  // Draw the arrow
  canvas.DrawImageF(ArrowTexture.Handle, arrow.X, arrow.Y);

  // Rotate the canvas back
  RotateCanvas(-arrow.GetAngle(), arrow.X, arrow.Y, canvas);
end;

procedure DrawEnemy(enemy : array of TEnemy; canvas : TW3Canvas); overload;
begin
  for var i := 0 to High(enemy) do
    begin
      if not enemy[i].Dead then
        begin
          DrawEnemy(enemy[i], canvas);
        end;
    end;
end;

procedure DrawEnemy(enemy : TEnemy; canvas : TW3Canvas); overload;
var
  textureWidth : integer;
  greenWidth : float;
begin
  if (enemy is TGroundUnit) then
    begin
      // Draw the ground unit if it is one
      canvas.DrawImageF(GroundUnitTexture.Handle, enemy.X, enemy.Y);

      // Draw it frozen if it's meant to be
      if enemy.Frozen then
        begin
          canvas.DrawImageF(FrozenGroundUnitTexture.Handle, enemy.X, enemy.Y);
        end;

      // Store the texture's width
      textureWidth := GroundUnitTexture.Handle.width;
    end
  else if (enemy is TAirUnit) then
    begin
      // Draw the air unit if it is one
      canvas.DrawImageF(AirUnitTexture.Handle, enemy.X, enemy.Y);

      // Draw it frozen if it's meant to be
      if enemy.Frozen then
        begin
          canvas.DrawImageF(FrozenAirUnitTexture.Handle, enemy.X, enemy.Y);
        end;

      // Store the texture's width
      textureWidth := AirUnitTexture.Handle.width;
    end;

  // Draw the health bar's red underlay above enemy
  canvas.FillStyle := "rgb(200, 0, 0)";
  canvas.FillRectF(enemy.X, enemy.Y - 13, textureWidth, 5);

  // Draw the health bar's green overlay above the enemy
  greenWidth := (enemy.Health / enemy.MaxHealth) * textureWidth; // Get the percentage of health and multiply by the bar's width
  canvas.FillStyle := "rgb(0, 200, 0)";
  canvas.FillRectF(enemy.X, enemy.Y - 13, greenWidth, 5);
end;

procedure DrawMouseDragLine(player : TPlayer; canvas : TW3Canvas);
begin
  if MouseDown and not Paused then
    begin
      canvas.StrokeStyle := "rgba(0, 0, 0, 0.5)";
      canvas.LineWidth := 0.3;
      canvas.BeginPath();
      canvas.MoveToF(MouseDownX, MouseDownY);
      canvas.LineToF(CurrentMouseX, CurrentMouseY);
      canvas.ClosePath();
      canvas.Stroke();
    end;
end;

procedure DrawCanShoot(player : TPlayer; canvas : TW3Canvas);
var
  text : string; // Text to go in message for the colour blind
begin
  // Get red (can't shoot) or green (can shoot) fillers
  if player.CanShoot then
    begin
      canvas.FillStyle := "rgba(0, 200, 0, 0.5)";
      text := "Can shoot";
    end
  else
    begin
      canvas.FillStyle := "rgba(200, 0, 0, 0.5)";
      text := "Can't shoot";
    end;

  // Draw a circle around the mouse
  canvas.BeginPath();
  canvas.Ellipse(CurrentMouseX - 7, CurrentMouseY - 7, CurrentMouseX + 7, CurrentMouseY + 7);
  canvas.ClosePath();
  canvas.Fill();

  // Draw a message saying "can/can't shoot" for the colour blind
  canvas.Font := "10pt verdana";
  canvas.TextAlign := "center";
  canvas.TextBaseLine := "bottom";
  canvas.FillTextF(text, CurrentMouseX, CurrentMouseY - 12, MAX_INT);

  // Also draw this message again above the player for those on a mobile as they can't see through their thumbs
  canvas.FillTextF(text, player.X + ArcherTexture.Handle.width / 2, player.Y - 12, MAX_INT)
end;

procedure DrawPauseScreen(canvas : TW3Canvas);
begin
  // Draw shop
  Shop.Draw(canvas);

  // The x position to place instructions and the side padding
  var xPos := Shop.Items[0].X + SHOP_WIDTH + 30;
  var sidePadding := 30;

  // Draw the title
  canvas.FillStyle := "rgb(0, 0, 0)";
  canvas.TextAlign := "center";
  canvas.TextBaseLine := "top";
  canvas.FillTextF("Welcome to TowerOfArcher!", xPos + (GAMEWIDTH - xPos - sidePadding) / 2, 50, GAMEWIDTH - xPos - sidePadding);

  // Draw instructions
  canvas.Font := "16pt verdana";
  canvas.TextAlign := "left";
  canvas.TextBaseLine := "top";
  canvas.FillTextF("How to play:", xPos, 90, GAMEWIDTH - xPos);
  canvas.FillTextF("To aim, hold down the left mouse button and drag backwards.", xPos + 40, 130, GAMEWIDTH - xPos - 40 - sidePadding);
  canvas.FillTextF("Release the left mouse button to fire.", xPos + 40, 170, GAMEWIDTH - xPos - 40 - sidePadding);
  canvas.FillTextF("You can cancel the shot by right-clicking.", xPos + 40, 210, GAMEWIDTH - xPos - 40 - sidePadding);
  canvas.FillTextF("If 10 enemies reach your castle, you lose!", xPos + 40, 250, GAMEWIDTH - xPos - 40 - sidePadding);
  canvas.FillTextF("Don't forget to check the shop regularly for upgrades!", xPos + 40, 290, GAMEWIDTH - xPos - 40 - sidePadding);
end;

procedure DrawHUD(canvas : TW3Canvas);
begin
  canvas.FillStyle := "rgb(220, 20, 50)";
  canvas.Font := "15pt verdana";
  canvas.TextAlign := "right";
  canvas.TextBaseLine := "top";
  canvas.FillTextF("Lives: " + IntToStr(Lives), GAMEWIDTH - 20, 10, MAX_INT);
  canvas.FillStyle := "rgb(220, 220, 20)";
  canvas.FillTextF("Gold: $" + IntToStr(Money), GAMEWIDTH - 20, 40, MAX_INT);
end;

procedure DrawGameOver(canvas : TW3Canvas);
begin
  // Draw the text
  canvas.Font := "70pt verdana";
  canvas.FillStyle := "rgb(0, 0, 0)";
  canvas.TextAlign := "center";
  canvas.TextBaseLine := "top";
  canvas.FillTextF("Game Over!", GAMEWIDTH / 2, 50, MAX_INT);

  // Draw the button
  canvas.StrokeStyle := "rgb(0, 0, 0)";
  canvas.LineWidth := 4;
  canvas.FillStyle := "rgb(130, 120, 140)";
  canvas.StrokeRect(RestartButtonRect());
  canvas.FillRect(RestartButtonRect());

  // Put the text in the button
  canvas.Font := IntToStr(Round(RestartButtonRect().Width() / 4)) + "pt verdana";
  canvas.FillStyle := "rgb(0, 0, 0)";
  canvas.TextAlign := "center";
  canvas.TextBaseLine := "middle";
  canvas.FillTextF("Restart", RestartButtonRect().CenterPoint().X, RestartButtonRect().CenterPoint().Y, RestartButtonRect().Width() - 10);
end;

procedure RotateCanvas(angle, xChange, yChange : float; canvas : TW3Canvas);
begin
  // Translate the canvas so the 0,0 point is the centre of the object being rotated
  canvas.Translate(xChange, yChange);

  // Rotate the canvas
  canvas.Rotate(angle);

  // Detranslate the canvas so the 0,0 point is the normal one
  canvas.Translate(-xChange, -yChange);
end;

end.

UEnemy

unit UEnemy;
{
    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, W3Time,
  UGameVariables;

type ECrossedTowerEvent = procedure();

type TEnemy = class(TObject)
  public
    X, Y, Speed : float;
    Health, MaxHealth, MoneyValue : integer;
    Frozen, Dead : boolean;
    procedure Move(); virtual;
    function GetRect() : TRectF; virtual; abstract;
    procedure Hit(damage : integer; xArrowSpeed, yArrowSpeed : float);
    procedure Freeze(minDuration, maxDuration : integer);
    procedure PauseTimer();
    procedure ResumeTimer();
  protected
    Timer : TW3EventRepeater; // Timer for being frozen
    DelayHolder : integer;
    HasCrossedTower : boolean; // Whether the enemy has taken a life from the player by crossing the tower
    CrossedTowerEvent : ECrossedTowerEvent; // The event
    property OnPurchase : ECrossedTowerEvent read CrossedTowerEvent write CrossedTowerEvent; // The event handler
    procedure CrossedTower();
    procedure ApplyToEventHandler();
    function HandleTimer(sender : TObject) : boolean;
end;

procedure CrossedTowerEventHandler();

implementation

procedure TEnemy.Move();
begin
  // Move the enemy closer to the tower
  X -= Speed;

  // After the enemy has crossed the tower
  if X < 0 then
    begin
      // Take a life if it has not already done so
      if not HasCrossedTower then
        begin
          CrossedTower();
        end
      else
        begin
          // Kill the enemy if it's gone far beyond the end of the screen
          if X < -300 then
            begin
              Health := 0;
            end;
        end;
    end;
end;

procedure TEnemy.Hit(damage : integer; xArrowSpeed, yArrowSpeed : float);
begin
  // Multiply the speed of the arrow by the damage multiplier
  var damageWithSpeed := damage * Sqrt(Sqr(xArrowSpeed) + Sqr(yArrowSpeed));

  // Take the damage from the health
  Health -= Round(damageWithSpeed);

  if Health < 0 then
    begin
      Dead := true;
      Money += MoneyValue;
    end;
end;

procedure TEnemy.Freeze(minDuration, maxDuration : integer);
var
  duration : integer;
begin
  // Get a random duration from the range
  duration := RandomInt(maxDuration - minDuration) + minDuration;

  // Tell the enemy that it is frozen
  Frozen := true;

  // Set the timer
  Timer := TW3EventRepeater.Create(HandleTimer, duration);
end;

procedure TEnemy.PauseTimer();
begin
  // Try block to avoid issue with timer not being initialized before
  try
    // Store the delay then destroy the timer
    DelayHolder := Timer.Delay;
    Timer.Destroy();
  except
    on e: exception do;
  end;
end;

procedure TEnemy.ResumeTimer();
begin
  // Start the timer then reset the delay holder
  Timer := TW3EventRepeater.Create(HandleTimer, DelayHolder);
  DelayHolder := 0;
end;

procedure TEnemy.CrossedTower();
begin
  // Only run the handler if the event has one
  if Assigned(CrossedTowerEvent) then
    begin
      CrossedTowerEvent();
      HasCrossedTower := true;
    end;
end;

procedure TEnemy.ApplyToEventHandler();
begin
  CrossedTowerEvent := CrossedTowerEventHandler;
end;

function TEnemy.HandleTimer(sender : TObject) : boolean;
begin
  Frozen := false;
  TW3EventRepeater(sender).Free();
  exit(true);
end;

procedure CrossedTowerEventHandler();
begin
  Dec(Lives);
end;

end.

Programming - a skill for life!

by George Wright: Y10 Age ~14