program sprites05;
// Example for creating sprites
//   - dynamic creation
//   - using sprite modifiers for movements
//   - simple collision detection
//
// This is a Space Invaders with simple level management
//

{$IFDEF FPC}
  {$MODE DELPHI}
{$ENDIF}

uses SysUtils,
     Math,
     
     fwLogging,
     fwCustomEngine,
     fwPlattform,
     fwResource,
     fwAnimation,
     fwSprite;

const
  SCREEN_WIDTH     = 320;
  SCREEN_HEIGHT    = 240;
  SCREEN_BPP       = 16;

  cStepX           = 2;
  cStepY           = 2;
  cAutoFire        = 500;

// -----------------------------------------------------------------------
// Create a on enemy list, which done all movements automaticly
type
  TEnemyList       = class(TfwSpriteList)
  protected
    fDirection       : Integer;
    fSpeed           : Integer;
    fNextRow         : Integer;
  public
    constructor Create(aEngine : TfwSpriteEngine; aAutoRegister : Boolean = True);
    procedure Move(aTime : Cardinal); override;
    property Speed : Integer read fSpeed write fSpeed;
  end;

constructor TEnemyList.Create(aEngine : TfwSpriteEngine; aAutoRegister : Boolean = True);
begin
  inherited Create(aEngine, aAutoRegister);

  fDirection := 1;
  fSpeed     := 1;
  fNextRow   := 0;
end;

procedure TEnemyList.Move(aTime : Cardinal);

  function CheckNextRow : Boolean;
  var
    iLoop            : Integer;
  begin
    Result := False;
    for iLoop := 0 to Pred(Count) do begin
      if (fDirection > 0) then begin
        if ((Items[iLoop].PosX + Items[iLoop].Width + fSpeed) >= SCREEN_WIDTH)
        then begin
          Result := True;
        end;
      end else if (fDirection < 0) then begin
        if ((Items[iLoop].PosX - fSpeed) < 0) then begin
          Result := True;
        end;
      end else begin
        Result := True;
      end;
    end;
  end;

var
  iLoop            : Integer;
begin
  if (fNextRow > 0) then begin
    Dec(fNextRow);
    for iLoop := 0 to Pred(Count) do begin
      if (Items[iLoop].State = stEnabled) then begin
        Items[iLoop].PosY := Items[iLoop].PosY + 1;
      end;
    end;

    if (fNextRow <= 0) then begin
      fDirection := fDirection * -1;
    end;
  end else begin
    if not CheckNextRow() then begin
      for iLoop := 0 to Pred(Count) do begin
        if (Items[iLoop].State = stEnabled) then begin
          Items[iLoop].PosX := Items[iLoop].PosX + (fDirection * fSpeed);
        end;
      end;
    end else begin
      fNextRow := 8;
    end;
  end;
end;

// -----------------------------------------------------------------------
// Define some globals
// -----------------------------------------------------------------------
var
  uLastPlayerBullet: Cardinal;
  uLastEnemyBullet : Cardinal;
  iSeconds         : Integer;
  iPoints          : Integer;
  iLevel           : Integer;
  iLives           : Integer;
  iKilled          : Integer;
  iRandom          : Integer;
  iLoop            : Integer;
  uEBulletTime     : Cardinal;
  Viewpoint        : TWorldRect;         // Screen width and height
  Screen           : TfwCESurface;       // drawing surface
  Resources        : TfwResourceManager; // contains all loaded/created objects
  GameImages       : TfwCESurface;       // game graphics
  ExplosionSound   : TfwCESound;
  ShootSoound      : TfwCESound;
  GameFont1        : TfwCEFont;
  GameFont2        : TfwCEFont;
  ShipAnim         : TfwAnimSequences;   // animation for ship
  Bullet1Anim      : TfwAnimSequences;   // animation for player bullet
  Bullet2Anim      : TfwAnimSequences;   // animation for enemy bullet
  EnemyAnim        : TfwAnimSequences;   // animation for enemy
  Bullet1Modifier  : TfwPosModifier;     // movement controller for player bullet
  Bullet2Modifier  : TfwPosModifier;     // movement controller for enmies bullet
  ShipSprite       : TfwSprite;          // players sprite
  BulletSprite     : TfwSprite;          // players or enmies tempory bullet sprite
  EnemySprite      : TfwSprite;
  Enemies          : TEnemyList;         // all created enemies
  PlayerBullets    : TfwSpriteList;      // all bullets from player
  EnemiesBulltes   : TfwSpriteList;      // all bullets from enemies
  SpriteEngine     : TfwSpriteEngine;    // the magic sprite engine

function LoadResources() : Boolean;
begin
  Result := False;

  // Load image for non moveable sprite as visible fixpoint
  // on screen
  GameImages := HAL.CreateSurface('spaceinvaders.bmp', $00FF00FF);
  if not Assigned(GameImages) then begin
    Exit;
  end;
  Resources.Add('GAMEDATA01', GameImages);

  ExplosionSound := HAL.CreateSound('explode1.wav');
  if not Assigned(ExplosionSound) then begin
    Exit;
  end;
  Resources.Add('EXPLOSION01', ExplosionSound);

  ShootSoound := HAL.CreateSound('shot.wav');
  if not Assigned(ShootSoound) then begin
    Exit;
  end;
  Resources.Add('SHOOT01', ShootSoound);

  GameFont1 := HAL.CreateFont('UNIVERSALFRUITCAKE.ttf', 24);
  if not Assigned(GameFont1) then begin
    Exit;
  end;
  Resources.Add('FONT01', GameFont1);

  GameFont2 := HAL.CreateFont('UNIVERSALFRUITCAKE.ttf', 10);
  if not Assigned(GameFont2) then begin
    Exit;
  end;
  Resources.Add('FONT02', GameFont2);

  // Creating animation for ship. Multiple sprites can sharing
  // one animation object.
  ShipAnim := TfwAnimSequences.Create;
  if not Assigned(ShipAnim) then begin
    Exit;
  end;
  Resources.Add('ANIM01', ShipAnim);

  // define one animation sequence. A sprite can have more
  // than one sequence. Each sequence contains one or
  // more sub images.
  ShipAnim.AddSequence.AddFrame( 0, 0, 18, 22);
  with ShipAnim.AddSequence do begin
    // Explosion animation ist bigger than players sprite, so add
    // negativ offset on animation drawing
    OffsetX       := -11;
    OffsetY       := -9;
    AnimSpeed     := 50;
    AutoDie       := True;
    AddFrame(142, 76, 38, 38);
    AddFrame(180,  0, 38, 38);
    AddFrame(180, 38, 38, 38);
    AddFrame(180, 76, 38, 38);
    AddFrame(218,  0, 38, 38);
    AddFrame(218, 38, 38, 38);
    AddFrame(218, 76, 38, 38);
  end;

  // Animation for play bullet
  Bullet1Anim := TfwAnimSequences.Create;
  if not Assigned(Bullet1Anim) then begin
    Exit;
  end;
  Resources.Add('ANIM02', Bullet1Anim);
  Bullet1Anim.AddSequence.AddFrame(2, 119, 4, 4);

  EnemyAnim := TfwAnimSequences.Create;
  if not Assigned(EnemyAnim) then begin
    Exit;
  end;
  Resources.Add('ANIM03', EnemyAnim);
  EnemyAnim.AddSequence.AddFrame(0, 32, 17, 20);
  // Add explosion animation
  with EnemyAnim.AddSequence do begin
    // Explosion animation ist bigger than enemy sprite, so add
    // negativ offset on animation drawing
    OffsetX       := -11;
    OffsetY       := -9;
    AnimSpeed     := 50;
    AutoDie       := True;
    AddFrame(142, 76, 38, 38);
    AddFrame(180,  0, 38, 38);
    AddFrame(180, 38, 38, 38);
    AddFrame(180, 76, 38, 38);
    AddFrame(218,  0, 38, 38);
    AddFrame(218, 38, 38, 38);
    AddFrame(218, 76, 38, 38);
  end;

  Bullet2Anim := TfwAnimSequences.Create;
  if not Assigned(Bullet2Anim) then begin
    Exit;
  end;
  Resources.Add('ANIM04', Bullet2Anim);
  Bullet2Anim.AddSequence.AddFrame(11, 120, 6, 6);

  // Creating the world for all sprites
  SpriteEngine := TfwSpriteEngine.Create;
  if not Assigned(SpriteEngine) then begin
    Exit;
  end;
  Resources.Add('SPRITEENINGE', SpriteEngine);

  // Create one sprite
  ShipSprite := TfwSprite.Create(SpriteEngine);
  if not Assigned(ShipSprite) then begin
    Exit;
  end;

  // Add ship to sprite engine
  SpriteEngine.Add(ShipSprite);

  // set image and animation stuff
  ShipSprite.Image     := GameImages;
  ShipSprite.AnimController.Animation := ShipAnim;
  ShipSprite.Show;

  // Position of ship sprite
  ShipSprite.PosX := 148;
  ShipSprite.PosY := 216;

  // movement controller for players shoots
  Bullet1Modifier := TfwPosModifier.Create(0, -3, 70);
  if Assigned(Bullet1Modifier) then begin
    SpriteEngine.Modifiers.Add(Bullet1Modifier);
  end;

  // Contains all active enemies
  Enemies := TEnemyList.Create(SpriteEngine);
  if not Assigned(Enemies) then begin
    Exit;
  end;

  // enmies bulltes controller
  Bullet2Modifier := TfwPosModifier.Create(0, +2, 110);
  if Assigned(Bullet2Modifier) then begin
    SpriteEngine.Modifiers.Add(Bullet2Modifier);
  end;

  // Contains all bullets fired by player
  PlayerBullets := TfwSpriteList.Create(SpriteEngine);
  if not Assigned(PlayerBullets) then begin
    Exit;
  end;

  EnemiesBulltes := TfwSpriteList.Create(SpriteEngine);
  if not Assigned(EnemiesBulltes) then begin
    Exit;
  end;

  Result := True;
end;

procedure ShowMain;
begin
  while HAL.Controls.KeyPressed(FIRE_BUTTON) do begin
    HAL.Idle();
    HAL.UpdateInput();
    Sleep(10);
  end;

  // -------------------------------------------------------------
  // show a simple main screen
  // -------------------------------------------------------------
  while (not HAL().ExitKeyPressed()) do begin
    // paint background black
    HAL.ClearScreen($00000000);

    GameFont2.RenderTo (Screen, 30,  60, 'fwGameEngine - Sprite demo', $00FFFFFF);
    GameFont1.RenderTo (Screen, 30, 100, 'Space Invaders', $00FFFFFF);
    GameFont2.RenderTo (Screen, 30, 160, 'Press fire key to play', $00FFFFFF);

    GameFont2.RenderTo (Screen, 30, 200, 'Last points: ' + IntToStr(iPoints), $00FFFFFF);
    GameFont2.RenderTo (Screen, 30, 220, 'Last level:  ' + IntToStr(iLevel), $00FFFFFF);

    // show graphics
    HAL.Flip(Screen);

    // update control all
    HAL.Idle();
    HAL.UpdateInput();
    if HAL.Controls.KeyPressed(FIRE_BUTTON) then begin
      Break;
    end;
  end;
end;

procedure ShowCounter;
var
  uTimer           : Cardinal;
begin
  while HAL.Controls.KeyPressed(FIRE_BUTTON) do begin
    HAL.Idle();
    HAL.UpdateInput();
    Sleep(10);
  end;

  // -----------------------------------------------------------
  // show counter screen
  // -----------------------------------------------------------
  iSeconds := 5;
  uTimer   := HAL().GetTicks();
  while (not HAL().ExitKeyPressed()) and (iSeconds > 0)
  do begin
    // paint background black
    HAL.ClearScreen($00000000);

    GameFont1.RenderTo (Screen, 80, 70, 'Level : ' + IntToStr(iLevel), $00FFFFFF);
    GameFont1.RenderTo (Screen, 50, 120, 'Starting in ' + IntToStr(iSeconds), $00FFFFFF);

    // show graphics
    HAL.Flip(Screen);

    // update control all
    HAL.Idle();
    HAL.UpdateInput();
    if HAL.Controls.KeyPressed(FIRE_BUTTON) then begin
      while HAL.Controls.KeyPressed(FIRE_BUTTON) do begin
        HAL.Idle();
        HAL.UpdateInput();
        Sleep(10);
      end;
      Break;
    end;

    if ((HAL().GetTicks() - uTimer) > 1000)
    then begin
      Dec(iSeconds);
      uTimer := HAL().GetTicks();
    end;
  end;
end;

procedure ShowGameOver;
begin
  while HAL.Controls.KeyPressed(FIRE_BUTTON) do begin
    HAL.Idle();
    HAL.UpdateInput();
    Sleep(10);
  end;

  // -----------------------------------------------------------
  // show game over screen
  // -----------------------------------------------------------
  while (not HAL().ExitKeyPressed()) do begin
    // paint background black
    HAL.ClearScreen($00000000);

    GameFont1.RenderTo (Screen,  80,  70, 'GAME OVER', $00FFFFFF);
    GameFont2.RenderTo (Screen, 115, 110, 'press fire key', $00FFFFFF);

    // show graphics
    HAL.Flip(Screen);

    // update control all
    HAL.Idle();
    HAL.UpdateInput();
    if HAL.Controls.KeyPressed(FIRE_BUTTON) then begin
      while HAL.Controls.KeyPressed(FIRE_BUTTON) do begin
        HAL.Idle();
        HAL.UpdateInput();
        Sleep(10);
      end;
      Break;
    end;
  end;
end;

// -----------------------------------------------------------------------
// Collision detection
// -----------------------------------------------------------------------
procedure EnemyCollide(aItem1, aItem2 : TfwSprite);
begin
  // Remove both sprite from spritelist
  if aItem1.HasFrames then begin
    aItem1.Die(aItem1.AnimController.DieSequence);
  end else begin
    aItem1.Die();
  end;

  // play explosion animation
  if aItem2.HasFrames then begin
    aItem2.Die(aItem2.AnimController.DieSequence);
  end else begin
    aItem2.Die();
  end;
  ExplosionSound.Play;
end;

procedure PlayerCollide(aItem1, aItem2 : TfwSprite);
begin
  // Remove both sprite from spritelist
  if aItem1.HasFrames then begin
    aItem1.Die(aItem1.AnimController.DieSequence);
  end else begin
    aItem1.Die();
  end;

  if aItem2.HasFrames then begin
    aItem2.Die(aItem2.AnimController.DieSequence);
  end else begin
    aItem2.Die();
  end;
  ExplosionSound.Play;
end;

// -----------------------------------------------------------------------
// Simple helper function for adding enemies
// -----------------------------------------------------------------------
function AddEnemy(aSpriteEngine : TfwSpriteEngine; aList : TEnemyList; aXPos, aYPos : Integer; aImage : TfwCESurface; aAnimations : TfwAnimSequences) : TfwSprite;
begin
  Result := TfwSprite.Create(aSpriteEngine);
  if Assigned(Result) then begin
    Result.AutoRemove := True;
    Result.Image      := aImage;
    Result.PosX       := aXPos;
    Result.PosY       := aYPos;
    Result.AnimController.Animation := aAnimations;
    Result.Show;

    aSpriteEngine.Add(Result);
    aList.Add(Result);
  end;
end;

function AddEnemyRow(aSpriteEngine : TfwSpriteEngine; aList : TEnemyList; aImage : TfwCESurface; aAnimations : TfwAnimSequences; aRow : Integer) : TfwSprite;
var
  iLoop            : Integer;
begin
  for iLoop := 0 to 9 do begin
    Result := AddEnemy(aSpriteEngine, aList, iLoop * 24, aRow * 32, aImage, aAnimations);
  end;
end;

procedure AddPlayerBullet();
begin
  BulletSprite      := TfwSprite.Create(SpriteEngine);
  if Assigned(BulletSprite) then begin
    BulletSprite.Image      := GameImages;
    BulletSprite.AutoRemove := True;
    BulletSprite.AnimController.Animation := Bullet1Anim;
    BulletSprite.AnimController.Sequence  := 0;

    BulletSprite.PosX := ShipSprite.PosX + (ShipSprite.Width div 2) - (BulletSprite.Width div 2);
    BulletSprite.PosY := ShipSprite.PosY - BulletSprite.Height;
    BulletSprite.Show;

    // add bulltes to separat list for faster collision
    // checks
    PlayerBullets.Add(BulletSprite);
    SpriteEngine.Add(BulletSprite);
    ShootSoound.Play();

    if Assigned(Bullet1Modifier) then begin
      BulletSprite.SetModifier(Bullet1Modifier);
    end;
  end;
end;

procedure AddEnemyBullet();
begin
  if (EnemiesBulltes.Count < 4) then begin
    // randomize shooting enmies
    iRandom := Random(Enemies.Count);
    if (iRandom < Enemies.Count) then begin
      EnemySprite := Enemies[iRandom];
      BulletSprite := TfwSprite.Create(SpriteEngine);
      if Assigned(BulletSprite) then begin
        BulletSprite.AutoRemove := True;
        BulletSprite.Image      := GameImages;
        BulletSprite.AnimController.Animation := Bullet2Anim;
        BulletSprite.AnimController.Sequence  := 0;

        BulletSprite.PosX := EnemySprite.PosX + (ShipSprite.Width div 2) - (BulletSprite.Width div 2);
        BulletSprite.PosY := EnemySprite.PosY + BulletSprite.Height;
        BulletSprite.Show;

        // add bulltes to separat list for faster collision
        // checks
        EnemiesBulltes.Add(BulletSprite);
        SpriteEngine.Add(BulletSprite);
        ShootSoound.Play();

        if Assigned(Bullet2Modifier) then begin
          BulletSprite.SetModifier(Bullet2Modifier);
        end;
      end;
    end;
  end;
end;

label ShowMainScreen;
label RestartLevel;
begin
  iPoints := 0;
  iLevel  := 0;
  
  // setup graphic engine
  SetHAL(TfwEngine.Create(false, false));
  try
    // setup stand path to game resources 
    HAL().MediaPath := '../Media/';

    HAL().Caption := 'SpaceInvaders (c) Thomas Seban';
    Screen := HAL.Init(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP);
    if Assigned(Screen) then begin
      // video, audio and control init done with default values for
      // each plattform.

      // Setup world viewport
      Viewpoint.Left   := 0;
      Viewpoint.Top    := 0;
      Viewpoint.Right  := 319;
      Viewpoint.Bottom := 239;

      // Adding resource manager, which free all loaded stuff at
      // end of program.
      Resources := TfwResourceManager.Create();
      try
        // Load and create all static resources
        if not LoadResources() then begin
          Exit;
        end;

        while (not HAL().ExitKeyPressed()) do begin
ShowMainScreen:
          ShowMain();

          uEBulletTime  := 1250;
          iPoints       := 0;
          iLevel        := 1;
          iLives        := 3;
          Enemies.Speed := 1;

          // go into game loop
          while (not HAL().ExitKeyPressed()) do begin
RestartLevel:
            // -----------------------------------------------------------
            // show counter screen
            // -----------------------------------------------------------
            ShowCounter();

            // -----------------------------------------------------------
            // init level
            // -----------------------------------------------------------

            // in higher level, they are maybe some bullets from previous
            // level, remove them
            SpriteEngine.RemoveSprites(PlayerBullets);
            SpriteEngine.RemoveSprites(EnemiesBulltes);
            SpriteEngine.RemoveSprites(Enemies);

            // set image, animation and players position
            ShipSprite.Image      := GameImages;
            ShipSprite.AutoRemove := False;
            ShipSprite.PosX       := (SCREEN_WIDTH - ShipSprite.Width) div 2;
            ShipSprite.PosY       := SCREEN_HEIGHT - ShipSprite.Height - 10;
            ShipSprite.AnimController.Animation := ShipAnim;
            ShipSprite.AnimController.Sequence  := 0;
            ShipSprite.SetState(stEnabled);
            ShipSprite.Show;

            // Add enemies
            for iLoop := 1 to iLevel do begin
              if (iLoop > 4) then begin
                Break;
              end;
              AddEnemyRow(SpriteEngine, Enemies, GameImages, EnemyAnim, iLoop);
            end;

            uLastPlayerBullet := HAL().GetTicks - cAutoFire;
            uLastEnemyBullet  := HAL().GetTicks;
            Randomize;
            while (not HAL().ExitKeyPressed()) do begin
              // paint background black
              HAL.ClearScreen($00000000);

              // show same game data
              GameFont2.RenderTo(Screen, 10, 2, 'Level: ' + IntToStr(iLevel), $00FFFFFF);
              GameFont2.RenderTo(Screen, 110, 2, 'Lives: ' + IntToStr(iLives), $00FFFFFF);
              GameFont2.RenderTo(Screen, 210, 2, 'Points: ' + IntToStr(iPoints), $00FFFFFF);

              // render all sprite to screen
              SpriteEngine.Render(Screen, Viewpoint);

              // show all graphics
              HAL.Flip(Screen);

              if (ShipSprite.State = stDied) then begin
                // Oh, player collides with enemies, this is not good
                Dec(iLives);
                if (iLives > 0) then begin
                  goto RestartLevel;
                end else begin
                   ShowGameOver();
                  goto ShowMainScreen;
                end;
              end;

              // update all controls
              HAL.Idle();
              HAL.UpdateInput();

              // check player movement
              if HAL.Controls.KeyPressed(LEFT_BUTTON) then begin
                // check collision
                if ((ShipSprite.PosX - cStepX) > 0) then begin
                  // no collision, move player
                  ShipSprite.PosX := ShipSprite.PosX - cStepX;
                end;
              end;

              if HAL.Controls.KeyPressed(RIGHT_BUTTON) then begin
                // check collision
                if ((ShipSprite.PosX + cStepX) < 304) then begin
                  // no collision, move player
                  ShipSprite.PosX := ShipSprite.PosX + cStepX;
                end;
              end;

              if HAL.Controls.KeyPressed(FIRE_BUTTON) then begin
                // fire key pressed, check time last pressed
                if ((HAL().GetTicks - uLastPlayerBullet) > cAutoFire) then begin
                  if (PlayerBullets.Count < 3) then begin
                    // only three bullets at same time
                    uLastPlayerBullet := HAL().GetTicks;
                    AddPlayerBullet();
                  end;
                end;
              end else if ((HAL().GetTicks - uLastPlayerBullet) > (cAutoFire div 2))
              then begin
                uLastPlayerBullet := HAL().GetTicks - cAutoFire;
              end;

              // make some enemie shoots
              if (HAL.GetTicks() > (uLastEnemyBullet + uEBulletTime))
              then begin
                AddEnemyBullet();
                uLastEnemyBullet := HAL.GetTicks();
              end;

              SpriteEngine.Move(HAL.FrameTime);
              Enemies.Move(HAL.FrameTime);

              // do all animations
              iKilled := PlayerBullets.CheckCollision(Enemies, EnemyCollide);
              if (iKilled > 0) then begin
                iPoints := iPoints + iKilled;
              end;

              Enemies.CheckCollision(ShipSprite, PlayerCollide);
              EnemiesBulltes.CheckCollision(ShipSprite, PlayerCollide);

              // remove all killed sprites
              SpriteEngine.RemoveSprites();

              if (Enemies.Count = 0) then begin
                // All enemies killed
                if (uEBulletTime > 500) then begin
                  uEBulletTime := uEBulletTime - 50;
                end;
                Inc(iLevel);
                Break;
              end;

            end;
          end;
        end;
      finally
        FreeAndNil(Resources);
      end;
    end;
  except
    on E : Exception do begin
      fwLog(E.Message);
    end;
  end;
end.
