unit ant_world;

//
//  The main unit with world and objects.
//

{$MODE FPC}
{$MODESWITCH AUTODEREF}
{$MODESWITCH ADVANCEDRECORDS}
{$MODESWITCH DEFAULTPARAMETERS}
{$MODESWITCH OUT}
{$MODESWITCH RESULT}

interface

uses
  {$I deng_app_units.inc},
  dstrnum,
  dvector2f,
  dvector2i,
  dvector4f,
  drect2i,
  ant_hud,
  ant_resources,
  ant_text;

const
  FLAG_BASE       = 1 shl 0; // "ant hill"
  FLAG_PLAYER     = 1 shl 1;
  FLAG_FOOD       = 1 shl 2;
  FLAG_DEAD       = 1 shl 3;
  FLAG_LAST_STANDING = 1 shl 4;

  TASK_CHAOTIC = 0;
  TASK_EXPLORE = 1;
  TASK_ATTACK_BASE = 2;
  TASK_ATTACK_ANT = 3;
  TASK_FOOD = 4;

type
PEntity = ^TEntity;
PWorld = ^TWorld;

// An entity is heavy object with custom logic
TEntity = object
// Allocation data
private
  FSerialId: PtrInt;

// Hierarchy data
private
  World: PWorld;
  FParent: PEntity;
  FSubFirst, FNext: PEntity;
  FSubLast, FPrev: PEntity;

public const

// Actual game data
public
  Flags: UInt32;
  Pos: TVec2f;
  AntHillState: TAntHillState;
  LastDamageTime: Int32;

// Rendering data
public
  NextRender: PEntity; // rendering list

public
  procedure InitBase(const P: TVec2f; IsPlayer: Boolean);
  procedure InitFood(const P: TVec2f);
  function IsInsideTerrytory(const P: TVec2f): Boolean;
  function IsLastStanding: Boolean;
  procedure DamageBase(Damage: Int32);
  function DamagedRecently: Boolean;
  procedure Switch;
  procedure Update;
  procedure Render;
end;

PAnt = ^TAnt;
TAnt = record
  SerialId: Int32;
  Pos: TVec2f;
  Task: Int32;
  Owner: PEntity;
  EntityTarget: PEntity;
  EntityTargetId: Int32;
  AntTarget: PAnt;
  AntTargetId: Int32;
  Alive: Boolean;
  DeathTime: Int32;
  procedure Kill;
  procedure AttackBase(Base: PEntity);
  procedure AttackAnt(Ant: PAnt);
end;

TWorld = object
private
  Entities: array of PEntity;
public
  Player: PEntity;

  procedure Init;
  procedure Done;
  function Spawn: PEntity;
  function SpawnAnt(Owner: PEntity): PAnt;
  procedure Generate;
  procedure Update;
  procedure Render;
end;

var
  WonTime: Int32;

implementation


var
  TotalAntsCount: Int32;
  Ants: array of TAnt;

procedure TEntity.InitBase(const P: TVec2f; IsPlayer: Boolean);
begin
  Pos := P;
  Flags := FLAG_BASE;
  if IsPlayer then
    Flags := Flags or FLAG_PLAYER;
  AntHillState.Init;
  LastDamageTime := 0;
end;

procedure TEntity.InitFood(const P: TVec2f);
begin
  Pos := P;
  Flags := FLAG_FOOD;
end;

function TEntity.IsInsideTerrytory(const P: TVec2f): Boolean;
begin
  //Exit(((Pos.X - AntHillState.TerritorySize) < P.X) and ((Pos.X + AntHillState.TerritorySize) > P.X)
  // and ((Pos.Y - AntHillState.TerritorySize) < P.Y) and ((Pos.Y + AntHillState.TerritorySize) > P.Y));
  Exit(Dist2f(Pos, P) < AntHillState.TerritorySize);
end;

function TEntity.IsLastStanding: Boolean;
begin
  Exit(Flags and FLAG_LAST_STANDING <> 0);
end;

procedure TEntity.DamageBase(Damage: Int32);
begin
  AntHillState.Food := AntHillState.Food - Damage;
  if Eng.Time - LastDamageTime > 5000 then
    LastDamageTime := Eng.Time;
end;

function TEntity.DamagedRecently: Boolean;
begin
  Exit((Flags and FLAG_DEAD = 0) and (Eng.Time - LastDamageTime < 400));
end;

procedure TEntity.Switch;
begin
  AntHillState.State := (AntHillState.State + 1) mod 4;
  AntHillState.LastSwitchTime := Eng.Time;
end;

function FindNotAttackingAnt(Owner: PEntity): PAnt;
var
  I: Int32;
begin
  for I := 0 to High(Ants) do begin
    Result := @Ants[I];
    if (Result^.Owner = Owner) and (Result^.Task <> TASK_ATTACK_BASE) and (Result^.Task <> TASK_ATTACK_ANT) then begin
      Exit;
    end;
  end;
  Exit(nil);
end;

procedure TEntity.Update;
var
  Ant: PAnt;
  I, J: Int32;
  E: PEntity;
  F: Int32;
begin
  if Flags and FLAG_BASE <> 0 then begin
    if AntHillState.Food < 0 then begin
      AntHillState.Food := 0;
      Flags := Flags or FLAG_DEAD;
    end;
    if Flags and FLAG_DEAD <> 0 then begin
      for I := 0 to High(Ants) do begin
        if Ants[I].Owner = @Self then
          Ants[I].Kill;
      end;
      Exit;
    end;
    if AntHillState.State = STATE_REPRODUCTION then begin
      Ant := World^.SpawnAnt(@Self);
    end;
    if AntHillState.State = STATE_FOOD then begin
      Inc(AntHillState.Food);
      if AntHillState.Food > AntHillState.MaxFood then
        AntHillState.Food := AntHillState.MaxFood
    end else begin
      if AntHillState.AntsCount < 0 then
        Log('AntHillState.AntsCount = ' + IntToStr(AntHillState.AntsCount));
      Dec(AntHillState.Food, 1 + Abs(AntHillState.AntsCount) div 10); // passive drain
      if AntHillState.Food < 0 then begin
        Flags := Flags or FLAG_DEAD;
        AntHillState.Food := 0;
      end;
    end;
    if AntHillState.State = STATE_EXPANDING then begin
      AntHillState.TerritorySize := AntHillState.TerritorySize + 1;
    end;
    if AntHillState.State = STATE_FOOD then begin
      F := 0;
      for I := 0 to High(Ants) do begin
        if Ants[I].Alive and (Ants[I].Owner = @Self) then begin
          Inc(F);
          if F > 10 then
            break;
          for J := 0 to High(World^.Entities) do begin
            E := World^.Entities[J];
            if (E^.Flags and FLAG_FOOD <> 0) and IsInsideTerrytory(E^.Pos) then begin
              Ants[I].Task := TASK_FOOD;
              Ants[I].EntityTarget := E;
            end;
          end;
        end;
      end;
    end;
    if AntHillState.State = STATE_ATTACK then begin
      for I := 0 to High(World^.Entities) do begin
        E := World^.Entities[I];
        if (E <> @Self) and (E^.Flags and FLAG_BASE <> 0) and (E^.Flags and FLAG_DEAD = 0) and IsInsideTerrytory(E^.Pos) then begin
          Ant := FindNotAttackingAnt(@Self);
          if Ant = nil then
            break; // no free ants
          Ant^.AttackBase(E);
        end;
      end;
      for I := 0 to High(Ants) do begin
        if Ants[I].Alive and (Ants[I].Owner <> @Self) and IsInsideTerrytory(Ants[I].Pos) then begin
          Ant := FindNotAttackingAnt(@Self);
          if Ant = nil then
            break; // no free ants
          Ant^.AttackAnt(@Ants[I]);
        end;
      end;
    end;
    if AntHillState.State = STATE_EXPANDING then begin
      for I := 0 to High(Ants) do begin
        if Ants[I].Alive and (Ants[I].Owner = @Self) and (Ants[I].Task = TASK_CHAOTIC) then begin
          Ants[I].Task := TASK_EXPLORE;
        end;
      end;
    end;

    if Flags and FLAG_PLAYER = 0 then begin
      if AntHillState.AntsCount < 100 then begin
        AntHillState.State := STATE_REPRODUCTION;
      end else begin
        if AntHillState.Food < 0.1 * AntHillState.MaxFood then begin
          AntHillState.State := STATE_FOOD;
        end else begin
          if IsInsideTerrytory(World^.Player^.Pos) then begin
            AntHillState.State := STATE_ATTACK;
          end else
            AntHillState.State := STATE_EXPANDING;
        end;
      end;
    end;
  end;
end;

procedure TEntity.Render;
begin
  if Flags and FLAG_BASE <> 0 then begin
    if Flags and FLAG_PLAYER <> 0 then begin
      Eng.Draw.RGBA(145, 126, 4, 255);
      Eng.Draw.Circle(Pos, AntHillState.TerritorySize);
      //if IsLastStanding then // Congratulations
      //  Eng.Draw.RGBA(Random(255), Random(255), Random(255), 255);
    end else begin
      Eng.Draw.RGBA(Round(145/2), Round(126/2), Round(4/2), 255);
      //Eng.Draw.Circle(Pos, AntHillState.TerritorySize);
    end;
    if DamagedRecently then
      Eng.Draw.RGBA(255, 255, 255, 255);
    if Flags and FLAG_DEAD <> 0 then
      Eng.Draw.RGBA(80, 80, 80, 255);
    Eng.Draw.CircleFilled(Pos, Eng.Width / 40);
  end else if Flags and FLAG_FOOD <> 0 then begin
    Eng.Draw.RGBA(90, 0, 157, 255);
    Res.Icon.Resource.Draw(@Eng, Pos, Vec2f(Eng.Width / 100, Eng.Width / 100));
  end;
  Eng.Draw.RGBA(255, 255, 255, 255);
end;

procedure TAnt.Kill;
begin
  if not Alive then
    Exit;
  Alive := False;
  DeathTime := Eng.Time;
  Dec(Owner^.AntHillState.AntsCount);
  if Owner <> Owner^.World^.Player then begin
    Dec(Owner^.AntHillState.Food, 8); // balance
  end;
end;

procedure TAnt.AttackBase(Base: PEntity);
begin
  Task := TASK_ATTACK_BASE;
  EntityTarget := Base;
end;

procedure TAnt.AttackAnt(Ant: PAnt);
begin
  Task := TASK_ATTACK_ANT;
  AntTarget := Ant;
end;

procedure TWorld.Init;
var
  I: Int32;
begin
  SetLength(Ants, 10 * 1024);
  for I := 0 to High(Ants) do
    Ants[I].Alive := False;
end;

procedure TWorld.Done;
begin
end;

function TWorld.Spawn: PEntity;
begin
  SetLength(Entities, Length(Entities) + 1);
  New(Entities[High(Entities)]);
  Entities[High(Entities)]^.World := @Self;
  Exit(Entities[High(Entities)]);
end;

function TWorld.SpawnAnt(Owner: PEntity): PAnt;
var
  I: Int32;
begin
  for I := 0 to High(Ants) do begin
    if not Ants[I].Alive then begin
      Ants[I].Alive := True;
      Ants[I].Pos := Owner^.Pos;
      Ants[I].Task := TASK_CHAOTIC;
      Ants[I].Owner := Owner;
      Inc(Owner^.AntHillState.AntsCount);
      Exit(@Ants[I]);
    end;
  end;
  Exit(nil);
end;

procedure TWorld.Generate;
var
  I: Int32;
begin
  for I := 0 to High(Entities) do
    Dispose(Entities[I]);
  SetLength(Entities, 0);
  for I := 0 to High(Ants) do begin
    Ants[I].Alive := False;
    Ants[I].DeathTime := 0;
  end;

  Player := Spawn;
  Player^.InitBase(Vec2f(Eng.Width / 2, Eng.Height / 2), True);

  Spawn^.InitBase(Vec2f(Random() * Eng.Width / 5, Random(Eng.Height)), False);
  Spawn^.InitBase(Vec2f(Eng.Width - Random() * Eng.Width / 5, Random(Eng.Height)), False);
  Spawn^.InitBase(Vec2f(Random(Eng.Width), Random() * Eng.Height / 10), False);
  Spawn^.InitBase(Vec2f(Random(Eng.Width), Eng.Height - Random() * Eng.Height / 10), False);
  for I := 0 to 1 do
    case Random(4) of
    0: Spawn^.InitBase(Vec2f(Random() * Eng.Width / 5, Random(Eng.Height)), False);
    1: Spawn^.InitBase(Vec2f(Eng.Width - Random() * Eng.Width / 5, Random(Eng.Height)), False);
    2: Spawn^.InitBase(Vec2f(Random(Eng.Width), Random() * Eng.Height / 10), False);
    3: Spawn^.InitBase(Vec2f(Random(Eng.Width), Eng.Height - Random() * Eng.Height / 10), False);
    end;
  for I := 0 to 100 do
    Spawn^.InitFood(Vec2f(Random(Eng.Width), Random(Eng.Height)));

  WonTime := 0;
end;

procedure TWorld.Update;
var
  I: Int32;
  BasesNum: Int32;
begin
  BasesNum := 0;
  for I := 0 to High(Entities) do begin
    Entities[I].Update;
    if (Entities[I].Flags and FLAG_BASE <> 0) and (Entities[I].Flags and FLAG_DEAD = 0) then begin
      Inc(BasesNum);
    end;
  end;
  if BasesNum = 1 then begin
    for I := 0 to High(Entities) do begin
      if (Entities[I].Flags and FLAG_BASE <> 0) and (Entities[I].Flags and FLAG_DEAD = 0) then begin
        if (Entities[I].Flags and FLAG_LAST_STANDING = 0) then
          WonTime := Eng.Time;
        Entities[I].Flags := Entities[I].Flags or FLAG_LAST_STANDING;
      end;
    end;
  end;
  for I := 0 to High(Ants) do begin
    if not Ants[I].Alive then
      continue;
    case Ants[I].Task of
    TASK_CHAOTIC: begin
        Ants[I].Pos.X := Ants[I].Pos.X + 5 * (Random - 0.5);
        Ants[I].Pos.Y := Ants[I].Pos.Y + 5 * (Random - 0.5);
        if not Ants[I].Owner^.IsInsideTerrytory(Ants[I].Pos) then
          Ants[I].Pos := Ants[I].Owner^.Pos + Mul2f(Norm2f(Sub2f(Ants[I].Owner^.Pos, Ants[I].Pos)), Ants[I].Owner^.AntHillState.TerritorySize);
      end;
    TASK_EXPLORE: begin
        Ants[I].Pos.X := Ants[I].Pos.X + 5 * (Random - 0.5);
        Ants[I].Pos.Y := Ants[I].Pos.Y + 5 * (Random - 0.5);
        if Dist2f(Ants[I].Pos, Ants[I].Owner^.Pos) < 0.8 * Ants[I].Owner^.AntHillState.TerritorySize then
          Ants[I].Pos := Ants[I].Pos + Mul2f(Norm2f(Sub2f(Ants[I].Pos, Ants[I].Owner^.Pos)), 1);
        if not Ants[I].Owner^.IsInsideTerrytory(Ants[I].Pos) then
          Ants[I].Pos := Ants[I].Owner^.Pos + Mul2f(Norm2f(Sub2f(Ants[I].Owner^.Pos, Ants[I].Pos)), Ants[I].Owner^.AntHillState.TerritorySize);
      end;
    TASK_FOOD: begin
        if Ants[I].EntityTarget^.Flags and FLAG_FOOD = 0 then begin
          Ants[I].Task := TASK_CHAOTIC;
          continue;
        end;
        if Dist2f(Ants[I].Pos, Ants[I].EntityTarget^.Pos) < 5 then begin
          if Dist2f(Ants[I].Owner^.Pos, Ants[I].Pos) < 5 then begin
            Ants[I].EntityTarget^.Flags := 0;
            Ants[I].Owner^.AntHillState.Food := Ants[I].Owner^.AntHillState.MaxFood;
          end else begin
            Ants[I].Pos := Add2f(Ants[I].Pos, Mul2f(Norm2f(Sub2f(Ants[I].Owner^.Pos, Ants[I].Pos)), 5));
            Ants[I].EntityTarget^.Pos := Ants[I].Pos;
          end;
        end else begin
          Ants[I].Pos := Add2f(Ants[I].Pos, Mul2f(Norm2f(Sub2f(Ants[I].EntityTarget^.Pos, Ants[I].Pos)), 5));
        end;
      end;
    TASK_ATTACK_BASE: begin
        if Ants[I].Owner^.IsInsideTerrytory(Ants[I].EntityTarget^.Pos) then begin
          Ants[I].Pos := Add2f(Ants[I].Pos, Mul2f(Norm2f(Sub2f(Ants[I].EntityTarget^.Pos, Ants[I].Pos)), 5));
          if Dist2f(Ants[I].EntityTarget^.Pos, Ants[I].Pos) < 5 then begin
            Ants[I].EntityTarget^.DamageBase(10);
            Ants[I].Pos := Ants[I].Owner^.Pos + Vec2f(Random(50), Random(50)); // mission accomplished, return to base
            Ants[I].Task := TASK_CHAOTIC;
            if Ants[I].Owner <> Player then
              Ants[I].Kill; // balance...
          end;
        end else
          Ants[I].Task := TASK_CHAOTIC;
      end;
    TASK_ATTACK_ANT: begin
        if Ants[I].Owner^.IsInsideTerrytory(Ants[I].AntTarget^.Pos) then begin
          Ants[I].Pos := Add2f(Ants[I].Pos, Mul2f(Norm2f(Sub2f(Ants[I].AntTarget^.Pos, Ants[I].Pos)), 5));
          if Dist2f(Ants[I].AntTarget^.Pos, Ants[I].Pos) < 5 then begin
            Ants[I].AntTarget^.Kill;
            Ants[I].Task := TASK_CHAOTIC;
          end;
        end else
          Ants[I].Task := TASK_CHAOTIC;
      end;
    end;
  end;
end;

procedure TWorld.Render;
var
  I: Int32;
begin

  for I := 0 to High(Entities) do
    Entities[I].Render;

  for I := 0 to High(Ants) do begin
    if not Ants[I].Alive then begin
      if Eng.Time - Ants[I].DeathTime < 200 then begin
        Eng.Draw.RGBA(255, 255, 255, 255);
      end else
        continue;
    end else if Ants[I].Owner = Player then begin
      Eng.Draw.RGBA(0, 0, 0, 255);
    end else
      Eng.Draw.RGBA(160, 0, 0, 255);
    Eng.Draw.RectFilled(drect2i.Rect2i(Round2f(Ants[I].Pos), Vec2i(3, 3)));
  end;
end;

end.
