unit uSprites;

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

interface

uses SysUtils,
     Classes,
     ContNrs,

     fwTypes,
     fwCustomEngine,
     fwAnimation,

     uCustomSprites,
     uWorldData,
     uWorld;

type
  TAnimSprite      = class;

  TModifierData    = packed record
    Sequence         : TSequenceID;
    Frame            : Integer;
    DifX             : Integer;
    DifY             : Integer;
  end;

  TSpriteMovement  = class(TObject)
  protected
    fMovements       : array of TModifierData;
    fCount           : Integer;
  public
    constructor Create;
    destructor Destroy; override;
    procedure Add(aSequenceID : TSequenceID; aFrame, aDifX, aDifY : Integer); overload;
    procedure Add(aSequenceID : TSequenceID; aFrameFrom, aFrameTo, aDifX, aDifY : Integer); overload;
    procedure Update(aSprite : TAnimSprite);
  end;

  TAnimSprite      = class(TCustomSprite)
  protected
    fAnimOwner       : Boolean;
    fAnimations      : TfwAnimSequences;
    fSequence        : TfwAnimSequence;
    fMovement        : TSpriteMovement;
    fFrame           : Integer;
    fAnimDone        : Boolean;
    fFrameTime       : Cardinal;
    fLastAnim        : Cardinal;
    fDirection       : TAnimDirection;
    fPathPos         : Integer;
    fZeroPointX      : Integer;
    fZeroPointY      : Integer;
    function  GetSortOrder : Integer; override;
    procedure SetAnimation(aAnimation : TfwAnimSequences);
  public
    constructor Create(aImage : TfwCEDrawable; aZeroPointX, aZeroPointY : Integer; aLayer : Integer = 0);
    destructor Destroy; override;

    procedure GetBounds(var aRect : TCERect); override;

    procedure AddSequence(aSequenceID : TSequenceID; aPosX, aPosY, aWidth, aHeight, aCount, aStepX, aStepY : Integer); overload;
    procedure AddSequence(aSequenceID : TSequenceID; aDirection : TAnimDirection; aPosX, aPosY, aWidth, aHeight, aCount, aStepX, aStepY : Integer); overload;
    procedure SwitchToSquence(aSequence : Integer); overload;
    function  SwitchToSquence(aSequence : TSequenceID) : Boolean; overload;
    procedure SetFrameTime(aFrameTime : Cardinal);
    function  TimeOut : Boolean;
    procedure NextFrame(aIncrement : Boolean = True);
    procedure Update(); override;

    procedure Draw(var aWorld : TWorldData; aScreen : TfwCESurface; aOffsetX, aOffsetY : Integer); override;
    procedure Draw(var aWorld : TWorldData; aDrawList : TDrawList); override;

    property Animations : TfwAnimSequences read fAnimations write SetAnimation;
    property AnimDone : Boolean read fAnimDone;
    property FrameTime : Cardinal read fFrameTime;
    property Direction : TAnimDirection read fDirection;
    property Movements : TSpriteMovement read fMovement write fMovement;
    property PathPos : Integer read fPathPos;
    property Sequence : TfwAnimSequence read fSequence;
    property ZeroPointX : Integer read fZeroPointX;
    property ZeroPointY : Integer read fZeroPointY;
  end;

  TFixedSprite     = class(TCustomSprite)
  public
    procedure Draw(var aWorld : TWorldData; aScreen : TfwCESurface; aOffsetX, aOffsetY : Integer); override;
  end;

implementation

// TSpriteMovement -------------------------------------------------------
constructor TSpriteMovement.Create;
begin
  inherited Create;

  fCount := 0;
  SetLength(fMovements, 0);
end;

destructor TSpriteMovement.Destroy;
begin
  SetLength(fMovements, 0);

  inherited Destroy;
end;

procedure TSpriteMovement.Add(aSequenceID : TSequenceID; aFrame, aDifX, aDifY : Integer);
begin
  SetLength(fMovements, fCount + 1);
  fMovements[fCount].Sequence := aSequenceID;
  fMovements[fCount].Frame    := aFrame;
  fMovements[fCount].DifX     := aDifX;
  fMovements[fCount].DifY     := aDifY;
  Inc(fCount);
end;

procedure TSpriteMovement.Add(aSequenceID : TSequenceID; aFrameFrom, aFrameTo, aDifX, aDifY : Integer);
var
  iLoop            : Integer;
  iNext            : Integer;
begin
  iNext := fCount;
  SetLength(fMovements, fCount + (aFrameTo - aFrameFrom) + 1);
  for iLoop := aFrameFrom to aFrameTo do begin
    fMovements[iNext].Sequence := aSequenceID;
    fMovements[iNext].Frame    := iLoop;
    fMovements[iNext].DifX     := aDifX;
    fMovements[iNext].DifY     := aDifY;
    Inc(iNext);
  end;
  fCount := Length(fMovements);
end;

procedure TSpriteMovement.Update(aSprite : TAnimSprite);
var
  iNext            : Integer;
  LastSequence     : TSequenceID;
begin
  LastSequence := siNone;
  if (fCount > 0) then begin
    if aSprite.TimeOut then begin
      iNext := aSprite.fPathPos;
      if Assigned(aSprite.Sequence) then begin
        LastSequence := aSprite.Sequence.SequenceID;
      end;
      
      Inc(iNext);
      if (iNext >= fCount) then begin
        iNext := 0;
      end;
      aSprite.fPathPos  := iNext;
      aSprite.fLastAnim := HAL().GetTicks;
      with fMovements[iNext] do begin
        if (LastSequence <> fMovements[iNext].Sequence) then begin
          aSprite.SwitchToSquence(fMovements[iNext].Sequence);
        end;
        aSprite.fFrame := Frame;
        aSprite.Move(DifX, DifY);
      end;
    end;
  end;
end;

// TAnimSprite -----------------------------------------------------------
function TAnimSprite.GetSortOrder : Integer;
begin
  Result := fY + fZeroPointY;
end;

procedure TAnimSprite.SetAnimation(aAnimation : TfwAnimSequences);
begin
  if (aAnimation <> fAnimations) then begin
    fSequence := nil;
    fFrame    := -1;
    if fAnimOwner then begin
      FreeAndNil(fAnimations);
    end;

    fAnimOwner  := False;
    fAnimations := aAnimation;
  end;
end;

constructor TAnimSprite.Create(aImage : TfwCEDrawable; aZeroPointX, aZeroPointY : Integer; aLayer : Integer = 0);
begin
  inherited Create(aImage, aLayer);

  fMovement   := nil;
  fAnimOwner  := True;
  fAnimations := TfwAnimSequences.Create;
  fZeroPointX := aZeroPointX;
  fZeroPointY := aZeroPointY;
  fPathPos    := -1;
  fSequence   := nil;
  fFrame      := -1;
  fFrameTime  := 50;
  fLastAnim   := HAL().GetTicks;
end;

destructor TAnimSprite.Destroy;
begin
  if fAnimOwner then begin
    FreeAndNil(fAnimations);
    fAnimOwner := False;
  end;

  inherited Destroy;
end;

procedure TAnimSprite.GetBounds(var aRect : TCERect);
begin
  if Assigned(fSurface) then begin
    if Assigned(fSequence) then begin
      if (fFrame >= 0) and (fFrame < fSequence.Count) then begin
        aRect        := fSequence[fFrame];
        aRect.X      := fX - fZeroPointX;
        aRect.Y      := fY - fZeroPointY;
        aRect.Width  := fWidth;
        aRect.Height := fHeight;
      end;
    end else begin
      aRect.X      := fX - fZeroPointX;
      aRect.Y      := fY - fZeroPointY;
      aRect.Width  := fWidth;
      aRect.Height := fHeight;
    end;
  end;
end;

procedure TAnimSprite.AddSequence(aSequenceID : TSequenceID; aPosX, aPosY, aWidth, aHeight, aCount, aStepX, aStepY : Integer);
begin
  AddSequence(aSequenceID, adNone, aPosX, aPosY, aWidth, aHeight, aCount, aStepX, aStepY);
end;

procedure TAnimSprite.AddSequence(aSequenceID : TSequenceID; aDirection : TAnimDirection; aPosX, aPosY, aWidth, aHeight, aCount, aStepX, aStepY : Integer);
begin
  with fAnimations.AddSequence(aSequenceID, aDirection) do begin
    AddFrames(aPosX, aPosY, aWidth, aHeight, aCount, aStepX, aStepY);
  end;
end;

procedure TAnimSprite.SwitchToSquence(aSequence : Integer);
begin
  if (aSequence >= 0) and (aSequence < fAnimations.Count) then begin
    fFrame    := -1;
    fSequence := fAnimations[aSequence];
    fAnimDone := False;
    if Assigned(fSequence) then begin
      if (fSequence.Count > 0) then begin
        fFrame := 0;
      end;
    end;
  end;
end;

function TAnimSprite.SwitchToSquence(aSequence : TSequenceID) : Boolean;
var
  iLoop            : Integer;
  Sequence         : TfwAnimSequence;
begin
  Result := False;
  if (aSequence > siNone) then begin
    if Assigned(fSequence) then begin
      if (fSequence.SequenceID = aSequence) then begin
        Exit;
      end;
    end;

    for iLoop := 0 to Pred(fAnimations.Count) do begin
      Sequence := fAnimations[iLoop];
      if Assigned(Sequence) then begin
        if (Sequence.SequenceID = aSequence) then begin
          fDirection := Sequence.Direction;        
          if (Sequence.Count > 0) then begin
            fSequence := Sequence;
            fFrame    := 0;
            fAnimDone := False;
            fWidth    := fSequence[0].Width;
            fHeight   := fSequence[0].Height;
            Result    := True;
            Exit;
          end;
        end;
      end;
    end;
  end;
end;

procedure TAnimSprite.SetFrameTime(aFrameTime : Cardinal);
begin
  if (aFrameTime > 0) then begin
    fFrameTime := aFrameTime;
  end;
end;

function TAnimSprite.TimeOut : Boolean;
begin
  Result := False;
  if Assigned(fSequence) then begin
    if ((HAL().GetTicks - fLastAnim) >= fFrameTime) then begin
      Result := True;
    end;
  end;
end;

procedure TAnimSprite.NextFrame(aIncrement : Boolean = True);
begin
  if Assigned(fSequence) then begin
    if not TimeOut() then begin
      Exit;
    end;
    fLastAnim := HAL().GetTicks;

    if (fSequence.Count > 0) then begin
      if aIncrement then begin
        Inc(fFrame);
        if (fFrame >= fSequence.Count) then begin
          fFrame    := 0;
          fAnimDone := True;
        end;
      end else begin
        Dec(fFrame);
        if (fFrame < 0) then begin
          fFrame    := Pred(fSequence.Count);
          fAnimDone := True
        end;
      end;

      if (fFrame >= 0) and (fFrame < fSequence.Count)
      then begin
        fWidth    := fSequence[fFrame].Width;
        fHeight   := fSequence[fFrame].Height;
      end;
    end;
  end;
end;

procedure TAnimSprite.Update();
begin
  if Assigned(fMovement) then begin
    fMovement.Update(Self);
  end;
end;

procedure TAnimSprite.Draw(var aWorld : TWorldData; aScreen : TfwCESurface; aOffsetX, aOffsetY : Integer);
var
  Rect             : TCERect;
  iX, iY           : Integer;
begin
  if Assigned(fSurface) then begin
    if Assigned(fSequence) then begin
      if (fFrame >= 0) and (fFrame < fSequence.Count) then begin
        WorldXYToScreen(aWorld, fX, fY, iX, iY);
        Rect := fSequence[fFrame];
        iX   := iX - fZeroPointX;
        iY   := iY - fZeroPointY;
        fSurface.Blit(aScreen, aOffsetX + iX, aOffsetY + iY, Rect.X, Rect.Y, Rect.Width, Rect.Height);
      end;
    end;
  end;
end;

procedure TAnimSprite.Draw(var aWorld : TWorldData; aDrawList : TDrawList);
var
  Rect             : TCERect;
  iX, iY           : Integer;
begin
  if Assigned(fSurface) then begin
    WorldXYToScreen(aWorld, fX, fY, iX, iY);
    iX   := iX - fZeroPointX;
    iY   := iY - fZeroPointY;
    if Assigned(fSequence) then begin
      if (fFrame >= 0) and (fFrame < fSequence.Count) then begin
        Rect := fSequence[fFrame];
        // aDrawList.Add(TDrawListItem.Create(fSurface, iX, iY, Rect.X, Rect.Y, Rect.Width, Rect.Height, fLayer, SortOrder - fZeroPointY, Self));
        aDrawList.Add(TDrawListItem.Create(fSurface, iX, iY, Rect.X, Rect.Y, Rect.Width, Rect.Height, fLayer, iY + fZeroPointY, Self));
      end;
    end else begin
      aDrawList.Add(TDrawListItem.Create(fSurface, iX, iY, 0, 0, Width, Height, fLayer, iY + fZeroPointY, Self));
    end;
  end;
end;

// TFixedSprite ----------------------------------------------------------
procedure TFixedSprite.Draw(var aWorld : TWorldData; aScreen : TfwCESurface; aOffsetX, aOffsetY : Integer);
begin
  if Assigned(fSurface) then begin
    fSurface.Blit(aScreen, fX, fY, 0, 0, fWidth, fHeight);
  end;
end;

end.
