unit G2App;

interface

uses
  Gen2MP,
  G2Utils,
  G2Types,
  G2Math,
  G2DataManager,
  G2PerlinNoise,
  G2Image,
  G2ImagePNG,
  Types,
  SysUtils,
  Math;

type
  TSound = object
  private
    function GetInst: TG2SoundInst;
  public
    Buffer: TG2SoundBuffer;
    Inst: TG2QuickList;
    procedure Load(const FileName: WideString); overload;
    procedure Load(const Buf: Pointer; const Size: Int64); overload;
    procedure Play;
    procedure Loop;
    procedure Free;
  end;
  PSound = ^TSound;

  TParticle = class (TG2GroupItem)
  private
    _TimeCreated: IntU32;
    _TimeToExpire: IntU32;
    _TimeDelay: IntU32;
  protected
    procedure Initialize; override;
  public
    t: Single;
    Pos: TG2Vec2;
    Vel: TG2Vec2;
    Size: Single;
    GravityCompliance: Single;
    property TimeToExpire: IntU32 read _TimeToExpire write _TimeToExpire;
    property TimeDelay: IntU32 read _TimeDelay write _TimeDelay;
    procedure Update; override;
  end;

  TPartImage = class (TParticle)
  protected
    procedure Initialize; override;
  public
    Tex: TG2Texture2D;
    Ang: Single;
    Scale: Single;
    Color: TG2Color;
    BlendMode: TG2BlendMode;
    procedure Render(const Pass: IntS32); override;
  end;

  TPartImageAnim = class (TPartImage)
  protected
    procedure Initialize; override;
  public
    FrameWidth: Integer;
    FrameHeight: Integer;
    CurFrame: Integer;
    procedure Render(const Pass: IntS32); override;
  end;

  TPartExplosion = class (TPartImage)
  protected
    procedure Initialize; override;
  public
    procedure Update; override;
  end;

  TPartWave = class (TParticle)
  protected
    procedure Initialize; override;
  public
    procedure Render(const Pass: IntS32); override;
  end;

  TPartFlare = class (TPartImage)
  protected
    procedure Initialize; override;
  public
    procedure Update; override;
  end;

  TPartFlame = class (TPartImageAnim)
  protected
    procedure Initialize; override;
  public
    procedure Update; override;
  end;

  CParticle = class of TParticle;

  TTrace = class (TG2GroupItem)
  private
    _TimeCreated: IntU32;
    _TimeToExpire: IntU32;
    _PosPrev: TG2Vec2;
    _Pos: TG2Vec2;
    _CurDistance: Single;
    procedure SetPos(const Value: TG2Vec2);
  protected
    procedure Initialize; override;
    procedure Finalize; override;
  public
    Vel: TG2Vec2;
    GravityCompliance: Single;
    Density: Single;
    Scatter: Single;
    ParticleClass: CParticle;
    property Pos: TG2Vec2 read _Pos write SetPos;
    property PosPrev: TG2Vec2 read _PosPrev;
    property TimeToExpire: IntU32 read _TimeToExpire write _TimeToExpire;
    procedure Update; override;
    procedure OnCreateParticle(const p: TParticle); virtual;
  end;

  TTraceFlame = class (TTrace)
  public
    procedure Update; override;
    procedure OnCreateParticle(const p: TParticle); override;
  end;

  TStripTracePoint = record
    Pos: TG2Vec2;
    Alpha: Single;
    Size: Single;
  end;
  PStripTracePoint = ^TStripTracePoint;

  TStripTracePointDraw = record
    Pos: TG2Vec2;
    Color: TG2Color;
  end;

  TStripTrace = class (TG2GroupItem)
  private
    TraceDraw: array of array[0..1] of TStripTracePointDraw;
    TraceArr: array of TStripTracePoint;
    TraceSize: Integer;
    DrawSize: Integer;
    TraceCur: Integer;
    CanDie: Boolean;
    procedure Initialize; override;
    procedure Finalize; override;
  public
    Pos: TG2Vec2;
    Tex: TG2Texture2D;
    procedure Render(const Pass: IntS32 = 0); override;
    procedure Update; override;
    procedure OnCreatePoint(const p: PStripTracePoint); virtual;
    procedure OnUpdatePoint(const p: PStripTracePoint); virtual;
    procedure AddTrace;
    procedure Cut;
  end;

  TShot = class
  public
    PosPrev: TG2Vec2;
    Pos: TG2Vec2;
    Vel: TG2Vec2;
    Dead: Boolean;
    Radius: Single;
    Damage: Integer;
    FactionID: Integer;
    CanHitFaction: Boolean;
    StartPos: TG2Vec2;
    constructor Create; virtual;
    destructor Destroy; override;
    procedure Render; virtual;
    procedure Update; virtual;
    procedure Die; virtual;
    procedure OnHit; virtual;
  end;

  TShotTrail = class (TShot)
  private
    Density: Single;
    CurDens: Single;
    Ang: Single;
    Scale: Single;
  public
    StripTrace: TStripTrace;
    constructor Create; override;
    procedure Render; override;
    procedure Update; override;
    procedure Die; override;
  end;

  TSentinel = object
    Pos: TG2Vec2;
    Vel: TG2Vec2;
    Size: Single;
    GunAng: Single;
    FactionID: Integer;
    Health: Integer;
    HealthMax: Integer;
    Dead: Boolean;
    PrevShot: TG2Vec2;
    procedure Init;
    procedure Update;
    procedure Shoot(const Power: Single);
    procedure Die;
  end;
  PSentinel = ^TSentinel;

  TGroundDamage = record
    Pos: TG2Vec2;
    Radius: Single;
  end;
  PGroundDamage = ^TGroundDamage;

  TGameMode = (gmObserve, gmPlay);

  TAIState = (aiSelectSentinel, aiAim, aiShoot);

  TGround = object
    Pos: TG2Vec2;
    Scale: Single;
    Width, Height: Integer;
    Collision: array of array of Boolean;
    Sentinels: TG2QuickList;
    SentinelsDead: TG2QuickList;
    Tex: TG2Texture2D;
    TexRT: TG2Texture2DRT;
    CopyRT: Boolean;
    Gravity: TG2Vec2;
    Damage: TG2QuickList;
    Shots: TG2QuickList;
    ShotsDead: TG2QuickList;
    Particles: TG2Group;
    Traces: TG2Group;
    CurFaction: Integer;
    GameMode: TGameMode;
    ModeTime: IntU32;
    Aiming: Boolean;
    AIState: TAIState;
    AITime: IntU32;
    AIPower: Single;
    procedure Initialize;
    procedure Finalize;
    procedure Generate;
    procedure Render;
    procedure Update;
    procedure BeginLevel; overload;
    procedure BeginLevel(const f0, f1: Integer); overload;
    procedure NextTurn;
    procedure DoAI;
    procedure MouseDown(const Button, x, y: Integer);
    procedure MouseUp(const Button, x, y: Integer);
    function AddSentinel(const PosX: Single; const Faction: Integer = 0): PSentinel;
    procedure DoDamange(const DamPos: TG2Vec2; const Radius: Single; const Amount: Integer);
    function Trace(const v0, v1: TG2Vec2; var Hit: TG2Vec2): Boolean;
    function CheckCollide(const x, y: Single): Boolean;
    procedure Explosion(const ExplPos: TG2Vec2; const Size: Single; const FlackCount: Integer);
  end;

  TMenuButton = record
    Tex0, Tex1: TG2Texture2D;
    Proc: TG2ProcObj;
  end;
  PMenuButton = ^TMenuButton;

  TMenu = object
  public
    Buttons: TG2QuickList;
    RTitle: TG2Rect;
    RButtons: TG2Rect;
    BtnX, BtnY, BtnW, BtnH: Single;
    TexTitle: TG2Texture2D;
    TexBtnNewGame0: TG2Texture2D;
    TexBtnNewGame1: TG2Texture2D;
    TexBtnQuit0: TG2Texture2D;
    TexBtnQuit1: TG2Texture2D;
    procedure Initialize;
    procedure Finalize;
    procedure Render;
    procedure Update;
    procedure AddButton(const Tex0, Tex1: TG2Texture2D; const Proc: TG2ProcObj);
    procedure MouseUp(const Button, x, y: Integer);
    function McInButton: Integer;
    function MdInButton: Integer;
    procedure BtnNewGame;
    procedure BtnQuit;
  end;

  TMapArea = record
    Geom: TG2PolyTriang;
    Center: TG2Vec2;
    Links: TG2QuickList;
    Faction: Integer;
  end;
  PMapArea = ^TMapArea;

  TMapAIState = (aiChooseMove, aiMove);

  TMapMode = (mmPlay, mmVictory, mmDefeat);

  TMap = object
  private
    CurArea: PMapArea;
    Pos: TG2Vec2;
    Scale: Single;
    CurFaction: Integer;
    AIState: TMapAIState;
    AITime: IntU32;
    AIMove0, AIMove1: PMapArea;
    AreaHit: PMapArea;
  public
    Mode: TMapMode;
    Areas: TG2QuickList;
    procedure Initialize;
    procedure Finalize;
    procedure Render;
    procedure Update;
    procedure DoAI;
    procedure MouseDown(const Button, x, y: Integer);
    procedure MouseUp(const Button, x, y: Integer);
    procedure AreaBegin;
    procedure AreaAdd(const v: TG2Vec2);
    procedure AreaEnd;
    procedure AreaLink(const a0, a1: PMapArea);
    procedure NextTurn;
    procedure GameBegin;
    procedure Winner(const Faction: Integer);
    function McInArea: PMapArea;
    function MdInArea: PMapArea;
  end;

  TGameState = (gsDebug, gsLoading, gsMenu, gsMap, gsGame);

  TG2App = object
    GameState: TGameState;
    CurTime: IntU32;
    TimeRem: G2Float;
    Pause: Boolean;
    gs: Single;
    Font1: TG2Font;
    Menu: TMenu;
    Map: TMap;
    Ground: TGround;
    Player: PSentinel;
    PlayerFaction: Integer;
    TexExplosion: array[0..3] of TG2Texture2D;
    TexWaveOutline: TG2Texture2D;
    TexFlare: TG2Texture2D;
    TexFlame: TG2Texture2D;
    TexClouds: TG2Texture2D;
    TexShotTrail: TG2Texture2D;
    TexMuzzleFlash: TG2Texture2D;
    TexTurretBase: array[0..5] of TG2Texture2D;
    TexTurretGun: TG2Texture2D;
    TexGroundDamage: TG2Texture2D;
    TexHealth: TG2Texture2D;
    TexTarget: TG2Texture2D;
    TexArrow: TG2Texture2D;
    TexSpot: TG2Texture2D;
    TexVictory: TG2Texture2D;
    TexDefeat: TG2Texture2D;
    procedure Initialize;
    procedure Finalize;
    procedure Update;
    procedure Render;
    procedure KeyDown(const Key: Integer);
    procedure KeyUp(const Key: Integer);
    procedure MouseDown(const Button, x, y: Integer);
    procedure MouseUp(const Button, x, y: Integer);
    function PosScreenToGround(const v: TG2Vec2): TG2Vec2;
    procedure DrawRing(
      const Pos: TG2Vec2; const Radius0, Radius1: Single; const Tex: TG2Texture2D; const Color: TG2Color;
      const BlendMode: TG2BlendMode = bmNormal; const TexSpan: Single = 1; const Segments: Integer = 32;
      const Angle: Single = 0
    );
    procedure DrawHealth(const Pos: TG2Vec2; const Radius0, Radius1, Amount: Single; const Color: TG2Color);
    procedure DrawArrow(const Pos0, Pos1: TG2Vec2; const Color: TG2Color; const Size: Single = 32);
    procedure RenderBox(const r: TG2Rect; const bs: Single; const Color: TG2Color; const Tex: TG2Texture2D; const BlendMode: TG2BlendMode = bmNormal); overload;
    procedure RenderBox(const x, y, w, h, bs: Single; const Color: TG2Color; const Tex: TG2Texture2D; const BlendMode: TG2BlendMode = bmNormal); overload;
    procedure RenderButton(const r: TG2Rect; const bs: Single; const Caption: AnsiString; const Tex0, Tex1, Tex2: TG2Texture2D); overload;
    procedure RenderButton(const x, y, w, h, bs: Single; const Caption: AnsiString; const Tex0, Tex1, Tex2: TG2Texture2D); overload;
    procedure TextFitRect(const TextFont: TG2Font; const R: TG2Rect; const Color: TG2Color; const Text: AnsiString);
  end;

var App: TG2App;


const FactionColor: array[0..5] of IntU32 = (
  $ffff0000, $ff0000ff, $ff00ff00, $ffffff00, $ffff00ff, $ff00ffff
);
const SentinelSize = 8;

implementation

//TSound BEGIN
function TSound.GetInst: TG2SoundInst;
  var i: Integer;
begin
  Result := nil;
  for i := 0 to Inst.Count - 1 do
  if not TG2SoundInst(Inst[i]).IsPlaying then
  begin
    Result := TG2SoundInst(Inst[i]);
    Exit;
  end;
  Result := TG2SoundInst.Create(Buffer);
  Inst.Add(Result);
end;

procedure TSound.Load(const FileName: WideString);
begin
  Buffer := TG2SoundBuffer.Create;
  Buffer.Load(FileName);
  Inst.Clear;
end;

procedure TSound.Load(const Buf: Pointer; const Size: Int64);
begin
  Buffer := TG2SoundBuffer.Create;
  Buffer.Load(Buf, Size);
  Inst.Clear;
end;

procedure TSound.Play;
begin
  GetInst.Play;
end;

procedure TSound.Loop;
  var CurInst: TG2SoundInst;
begin
  CurInst := GetInst;
  CurInst.Loop := True;
  CurInst.Play;
end;

procedure TSound.Free;
  var i: Integer;
begin
  for i := 0 to Inst.Count - 1 do
  TG2SoundInst(Inst[i]).Free;
  Inst.Clear;
  Buffer.Free;
end;
//TSound END

//TParticle BEGIN
procedure TParticle.Initialize;
begin
  _TimeCreated := App.CurTime;
  _TimeToExpire := 1000;
  _TimeDelay := 0;
  t := 0;
  GravityCompliance := 1;
  Size := 1;
end;

procedure TParticle.Update;
begin
  t := (App.CurTime - _TimeCreated - _TimeDelay) / _TimeToExpire;
  if t < 0 then t := 0;
  if t >= 1 then Die;
  Vel := Vel + App.Ground.Gravity * GravityCompliance;
  Pos := Pos + Vel;
end;
//TParticle END

//TPartImage BEGIN
procedure TPartImage.Initialize;
begin
  Inherited Initialize;
  Pos.SetValue(0, 0);
  Vel.SetValue(0, 0);
  Ang := 0;
  Scale := 32;
  Color := $ffffffff;
  BlendMode := bmNormal;
end;

procedure TPartImage.Render(const Pass: IntS32);
  var s: Single;
begin
  s := Scale * App.Ground.Scale;
  g2.PicRect(
    Pos * App.Ground.Scale + App.Ground.Pos,
    1, 1, Color, 0.5, 0.5, s, s,
    Ang, False, False, Tex,
    Tex.Width, Tex.Height, 0,
    BlendMode, tfLinear
  );
end;
//TPartImage END

//TPartImageAnim BEGIN
procedure TPartImageAnim.Initialize;
begin
  inherited Initialize;
  FrameWidth := 0;
  FrameHeight := 0;
  CurFrame := 0;
end;

procedure TPartImageAnim.Render(const Pass: IntS32);
  var s: Single;
begin
  s := Scale * App.Ground.Scale;
  g2.PicRect(
    Pos * App.Ground.Scale + App.Ground.Pos,
    1, 1, Color, 0.5, 0.5, s, s,
    Ang, False, False, Tex,
    FrameWidth, FrameHeight, CurFrame,
    BlendMode, tfLinear
  );
end;
//TPartImageAnim END

//TPartExplosion BEGIN
procedure TPartExplosion.Initialize;
begin
  inherited Initialize;
  Sort := 2;
  Vel := G2RandomCirclePoint * Random(101) * 0.001;
  Tex := App.TexExplosion[Random(Length(App.TexExplosion))];
  Ang := G2Random2Pi;
  GravityCompliance := 0;
  TimeToExpire := 300;
  TimeDelay := Random(100);
end;

procedure TPartExplosion.Update;
begin
  inherited Update;
  if not Dead then
  begin
    Scale := Size * 0.6 * t + Size * 0.4;
    Color := G2Color($ff, $ff, $ff, Round(Cos(t * HalfPi) * $ff));
  end;
end;
//TPartExplosion END

//TPartWave BEGIN
procedure TPartWave.Initialize;
begin
  inherited Initialize;
  Sort := 3;
  GravityCompliance := 0;
  TimeToExpire := 200;
end;

procedure TPartWave.Render(const Pass: IntS32);
  var b: Byte;
  var c: TG2Color;
  var r0, r1: Single;
begin
  b := Round((1 - t) * 80);
  c := G2Color(b, b, b, $ff);
  r0 := G2LerpFloat(0.033 * Size, 0.83 * Size, t) * App.Ground.Scale;
  r1 := G2LerpFloat(0.3 * Size, Size, t) * App.Ground.Scale;
  App.DrawRing(
    Pos * App.Ground.Scale + App.Ground.Pos, r0, r1,
    App.TexWaveOutline,
    c, bmAdd, 2
  );
end;
//TPartWave END

//TPartFlare BEGIN
procedure TPartFlare.Initialize;
begin
  inherited Initialize;
  Sort := 4;
  Tex := App.TexFlare;
  Size := 30;
  Scale := Size;
  Ang := G2Random2Pi;
  GravityCompliance := 0;
  TimeToExpire := 100;
  BlendMode := bmAdd;
end;

procedure TPartFlare.Update;
  var b: Byte;
begin
  inherited Update;
  if not Dead then
  begin
    Scale := Size * t + Size;
    b := Round(Cos(t * HalfPi) * $cc);
    Color := G2Color(b, b, b, $ff);
  end;
end;
//TPartFlare END

//TPartFlame BEGIN
procedure TPartFlame.Initialize;
begin
  inherited Initialize;
  Sort := 1;
  Tex := App.TexFlame;
  FrameWidth := Tex.Width div 4;
  FrameHeight := Tex.Height div 4;
  Size := 20;
  Scale := Size;
  Ang := G2Random2Pi;
  GravityCompliance := 0;
end;

procedure TPartFlame.Update;
begin
  inherited Update;
  Scale := Size * (1 + t);
  CurFrame := Round(t * 15);
  Color := G2Color($ff, $ff, $ff, Round((1 - t) * $80));
end;
//TPartFlame END

//TTrace BEGIN
procedure TTrace.SetPos(const Value: TG2Vec2);
begin
  _Pos := Value;
  _PosPrev := Value;
end;

procedure TTrace.Initialize;
begin
  _TimeCreated := App.CurTime;
  _TimeToExpire := 1000;
  _CurDistance := 0;
  GravityCompliance := 1;
  Vel.SetValue(0, 0);
  Density := 20;
  Scatter := 0;
end;

procedure TTrace.Finalize;
begin

end;

procedure TTrace.Update;
  var i, n: Integer;
  var p: TParticle;
begin
  if App.CurTime - _TimeCreated > _TimeToExpire then Die
  else
  begin
    _PosPrev := _Pos;
    Vel := Vel + App.Ground.Gravity;
    _Pos := _Pos + Vel;
    _CurDistance := _CurDistance + (_Pos - _PosPrev).Len;
    n := Trunc(_CurDistance / Density);
    _CurDistance := _CurDistance - n * Density;
    for i := 0 to n - 1 do
    begin
      p := ParticleClass.Create(App.Ground.Particles);
      p.Pos := G2LerpVec2(_Pos, _PosPrev, i / n) + G2RandomCirclePoint * (Random(Round(Scatter * 1000)) * 0.001);
      OnCreateParticle(p);
    end;
  end;
end;

procedure TTrace.OnCreateParticle(const p: TParticle);
begin

end;
//TTrace END

//TTraceFlame BEIGN
procedure TTraceFlame.Update;
  var d, r, Hit: TG2Vec2;
  var i, n: Integer;
  var Coly: array[0..7] of Boolean;
  const Dirs: array[0..7] of TPoint = (
    (X: -1; Y: -1), (X: 0; Y: -1), (X: 1; Y: -1), (X: 1; Y: 0),
    (X: 1; Y: 1), (X: 0; Y: 1), (X: -1; Y: 1), (X: -1; Y: 0)
  );
begin
  inherited Update;
  if (PosPrev.x < 0)
  or (PosPrev.x > App.Ground.Width - 1)
  or (PosPrev.y < 0)
  or (PosPrev.y > App.Ground.Height - 1) then
  Exit;
  if App.Ground.Trace(PosPrev, Pos, Hit) then
  begin
    d := (Pos - PosPrev).Norm;
    n := 0;
    repeat
      Hit := Hit - d;
      Inc(n);
    until not App.Ground.CheckCollide(Hit.x, Hit.y) or (n > 10);
    for i := 0 to High(Dirs) do
    Coly[i] := App.Ground.CheckCollide(Hit.x + Dirs[i].x, Hit.y + Dirs[i].y);
    if Hit.x < 0 then Hit.x := 0 else if Hit.x > App.Ground.Width - 1 then Hit.x := App.Ground.Width - 1;
    while App.Ground.CheckCollide(Hit.x, Hit.y) do
    Hit.y := Hit.y - 1;
    Pos := Hit;
    r.SetValue(0, 0);
    if d.x < 0 then
    begin
      if d.y < 0 then
      begin
        if Coly[7] and Coly[1] then
        r := G2Vec2(1, 1).Norm
        else if Coly[7] then
        r := G2Vec2(1, 0)
        else if Coly[1] then
        r := G2Vec2(0, 1)
        else if Coly[0] then
        r := G2Vec2(1, 1).Norm;
      end
      else
      begin
        if Coly[7] and Coly[5] then
        r := G2Vec2(1, -1).Norm
        else if Coly[7] then
        r := G2Vec2(1, 0)
        else if Coly[5] then
        r := G2Vec2(0, -1)
        else if Coly[6] then
        r := G2Vec2(1, -1).Norm;
      end;
    end
    else //d.x > 0
    begin
      if d.y < 0 then
      begin
        if Coly[3] and Coly[1] then
        r := G2Vec2(-1, 1).Norm
        else if Coly[3] then
        r := G2Vec2(-1, 0)
        else if Coly[1] then
        r := G2Vec2(0, 1)
        else if Coly[2] then
        r := G2Vec2(-1, 1).Norm;
      end
      else
      begin
        if Coly[3] and Coly[5] then
        r := G2Vec2(-1, -1).Norm
        else if Coly[3] then
        r := G2Vec2(-1, 0)
        else if Coly[5] then
        r := G2Vec2(0, -1)
        else if Coly[4] then
        r := G2Vec2(-1, -1).Norm;
      end;
    end;
    if r.LenSq > 0.5 then
    Vel := Vel.Reflect(r) * 0.9;
  end;
end;

procedure TTraceFlame.OnCreateParticle(const p: TParticle);
begin
  TPartFlame(p).Ang := G2Random2Pi;
  TPartFlame(p).Size := 10 + Random(101) * 0.1;
end;
//TTraceFlame END

//TStripTrace BEGIN
procedure TStripTrace.Initialize;
begin
  Tex := App.TexShotTrail;
  SetLength(TraceArr, 64);
  SetLength(TraceDraw, 64);
  TraceCur := 0;
  TraceSize := 0;
  DrawSize := 0;
  CanDie := False;
end;

procedure TStripTrace.Finalize;
begin

end;

procedure TStripTrace.Render(const Pass: IntS32 = 0);
  var i, j: Integer;
begin
  if not Dead then
  begin
    if DrawSize > 1 then
    begin
      g2.Gfx.Poly2D.PolyBegin(ptTriangles, Tex, bmNormal, tfLinear);
      for i := 0 to DrawSize - 2 do
      begin
        j := i + 1;
        g2.Gfx.Poly2D.PolyAdd(TraceDraw[i][0].Pos.x * App.Ground.Scale + App.Ground.Pos.x, TraceDraw[i][0].Pos.y * App.Ground.Scale + App.Ground.Pos.y, 0, 0.5, TraceDraw[i][0].Color);
        g2.Gfx.Poly2D.PolyAdd(TraceDraw[i][1].Pos.x * App.Ground.Scale + App.Ground.Pos.x, TraceDraw[i][1].Pos.y * App.Ground.Scale + App.Ground.Pos.y, 1, 0.5, TraceDraw[i][1].Color);
        g2.Gfx.Poly2D.PolyAdd(TraceDraw[j][0].Pos.x * App.Ground.Scale + App.Ground.Pos.x, TraceDraw[j][0].Pos.y * App.Ground.Scale + App.Ground.Pos.y, 0, 0.5, TraceDraw[j][0].Color);
        g2.Gfx.Poly2D.PolyAdd(TraceDraw[i][1].Pos.x * App.Ground.Scale + App.Ground.Pos.x, TraceDraw[i][1].Pos.y * App.Ground.Scale + App.Ground.Pos.y, 1, 0.5, TraceDraw[i][1].Color);
        g2.Gfx.Poly2D.PolyAdd(TraceDraw[j][1].Pos.x * App.Ground.Scale + App.Ground.Pos.x, TraceDraw[j][1].Pos.y * App.Ground.Scale + App.Ground.Pos.y, 1, 0.5, TraceDraw[j][1].Color);
        g2.Gfx.Poly2D.PolyAdd(TraceDraw[j][0].Pos.x * App.Ground.Scale + App.Ground.Pos.x, TraceDraw[j][0].Pos.y * App.Ground.Scale + App.Ground.Pos.y, 0, 0.5, TraceDraw[j][0].Color);
      end;
      g2.Gfx.Poly2D.PolyEnd;
    end;
  end;
end;

procedure TStripTrace.Update;
  var i, j, k, ind: Integer;
  var pp: TStripTracePoint;
  var v, n0, n1, n: TG2Vec2;
begin
  if CanDie and (TraceSize < 1) then Die;
  if not Dead then
  begin
    j := TraceCur;
    for i := 0 to TraceSize - 1 do
    begin
      j := j - 1;
      if j < 0 then j := High(TraceArr);
      OnUpdatePoint(@TraceArr[j]);
    end;
    DrawSize := 0;
    if TraceSize > 1 then
    begin
      if CanDie then
      begin
        j := TraceCur - 1;
        if j < 0 then j := High(TraceArr);
        pp := TraceArr[j];
        j := j - 1;
        ind := 1;
      end
      else
      begin
        j := TraceCur - 1;
        pp.Pos := Pos;
        OnCreatePoint(@pp);
        ind := 0;
      end;
      if j < 0 then j := High(TraceArr);
      v := pp.Pos - TraceArr[j].Pos;
      if v.LenSq < 0.5 then
      begin
        j := TraceCur - 1;
        if j < 0 then j := High(TraceArr);
        pp := TraceArr[j];
        j := j - 1;
        ind := 1;
        if j < 0 then j := High(TraceArr);
        v := pp.Pos - TraceArr[j].Pos;
      end;
      n0 := v.Perp.Norm;
      TraceDraw[DrawSize][0].Pos := pp.Pos - n0 * pp.Size;
      TraceDraw[DrawSize][0].Color := G2Color($ff, $ff, $ff, Round($ff * pp.Alpha));
      TraceDraw[DrawSize][1].Pos := pp.Pos + n0 * pp.Size;
      TraceDraw[DrawSize][1].Color := G2Color($ff, $ff, $ff, Round($ff * pp.Alpha));
      Inc(DrawSize);
      pp := TraceArr[j];
      for i := ind to TraceSize - 2 do
      begin
        j := j - 1;
        if j < 0 then j := High(TraceArr);
        v := pp.Pos - TraceArr[j].Pos;
        n1 := v.Perp.Norm;
        n := (n0 + n1) * 0.5;
        TraceDraw[DrawSize][0].Pos := pp.Pos - n * pp.Size;
        TraceDraw[DrawSize][0].Color := G2Color($ff, $ff, $ff, Round($ff * pp.Alpha));
        TraceDraw[DrawSize][1].Pos := pp.Pos + n * pp.Size;
        TraceDraw[DrawSize][1].Color := G2Color($ff, $ff, $ff, Round($ff * pp.Alpha));
        Inc(DrawSize);
        pp := TraceArr[j];
        n0 := n1;
      end;
      TraceDraw[DrawSize][0].Pos := pp.Pos - n0 * pp.Size;
      TraceDraw[DrawSize][0].Color := G2Color($ff, $ff, $ff, Round($ff * pp.Alpha));
      TraceDraw[DrawSize][1].Pos := pp.Pos + n0 * pp.Size;
      TraceDraw[DrawSize][1].Color := G2Color($ff, $ff, $ff, Round($ff * pp.Alpha));
      Inc(DrawSize);
    end;
  end;
end;

procedure TStripTrace.OnCreatePoint(const p: PStripTracePoint);
begin
  p^.Alpha := 0.7;
  p^.Size := 6;
end;

procedure TStripTrace.OnUpdatePoint(const p: PStripTracePoint);
begin
  p^.Alpha := p^.Alpha - 0.03;
  p^.Size := p^.Size - 0.2;
  if (p^.Alpha < 0)
  or (p^.Size < 1) then
  Dec(TraceSize);
end;

procedure TStripTrace.AddTrace;
begin
  TraceArr[TraceCur].Pos := Pos;
  OnCreatePoint(@TraceArr[TraceCur]);
  TraceCur := (TraceCur + 1) mod Length(TraceArr);
  if TraceSize < High(TraceArr) - 1 then
  Inc(TraceSize);
end;

procedure TStripTrace.Cut;
begin
  CanDie := True;
end;
//TStripTrace END

//TShot BEGIN
constructor TShot.Create;
begin
  inherited Create;
  Dead := False;
  Radius := 20;
  Damage := 50;
  CanHitFaction := False;
end;

destructor TShot.Destroy;
begin
  inherited Destroy;
end;

procedure TShot.Render;
  var sp: TG2Vec2;
begin
  sp := Pos * App.Ground.Scale + App.Ground.Pos;
  g2.PrimCircleCol(sp, 4 * App.Ground.Scale, $ffffff00, $ffffff00, 8);
end;

procedure TShot.Update;
  var v, Hit: TG2Vec2;
  var i: Integer;
  var p0, p1: PG2Vec2;
begin
  if not Dead then
  begin
    PosPrev := Pos;
    Vel := Vel + App.Ground.Gravity;
    Pos := Pos + Vel;
    if not CanHitFaction and ((PosPrev - StartPos).LenSq > Sqr(SentinelSize * 2)) then CanHitFaction := True;
    for i := 0 to App.Ground.Sentinels.Count - 1 do
    if ((PSentinel(App.Ground.Sentinels[i])^.FactionID <> FactionID) or CanHitFaction)
    and G2LineVsCircle(PosPrev, Pos, PSentinel(App.Ground.Sentinels[i])^.Pos, SentinelSize, p0, p1) then
    begin
      if p1 <> nil then
      begin
        v := (Pos - PosPrev).Norm;
        if v.Dot(p1^) < v.Dot(p0^) then
        Pos := p1^
        else
        Pos := p0^;
        Dispose(p1);
      end
      else
      begin
        if p0 = nil then
        G2BreakPoint;
        Pos := p0^;
      end;
      Dispose(p0);
      OnHit;
      Die;
      Exit;
    end;
    if App.Ground.Trace(PosPrev, Pos, Hit) then
    begin
      Pos := Hit;
      OnHit;
      Die;
      Exit;
    end;
    if (Pos.x < 0) or (Pos.x > App.Ground.Width - 1) or (Pos.y > App.Ground.Height - 1) then
    Die;
  end;
end;

procedure TShot.Die;
begin
  if not Dead then
  begin
    Dead := True;
    App.Ground.ShotsDead.Add(Self);
  end;
end;

procedure TShot.OnHit;
begin
  App.Ground.DoDamange(Pos, Radius, Damage);
  App.Ground.Explosion(Pos, Radius, Round(Radius * 0.1 * (Random(2) + 1)));
end;
//TShot END

//TShotTrail BEGIN
constructor TShotTrail.Create;
begin
  inherited Create;
  Density := 10;
  CurDens := Density;
  Ang := 0;
  Scale := 1;
  StripTrace := TStripTrace.Create(App.Ground.Particles);
end;

procedure TShotTrail.Render;
begin
  g2.PicRect(
    Pos * App.Ground.Scale + App.Ground.Pos, 70, 35,
    $aaffffff, 0.3, 0.5, Scale, Scale, Ang + Pi, False, False,
    App.TexMuzzleFlash, App.TexMuzzleFlash.Width, App.TexMuzzleFlash.Height, 0,
    bmAdd, tfLinear
  );
end;

procedure TShotTrail.Update;
  var v: TG2Vec2;
begin
  inherited Update;
  v := Pos - PosPrev;
  if v.LenSq > 0.1 then
  Ang := v.AngleOX;
  Scale := 0.5 + Abs(Sin(G2PiTime(50))) * 0.5;
  StripTrace.Pos := Pos;
  CurDens := CurDens + (Pos - PosPrev).Len;
  if CurDens >= Density then
  begin
    StripTrace.AddTrace;
    CurDens := 0;
  end;
end;

procedure TShotTrail.Die;
begin
  inherited Die;
  StripTrace.Cut;
end;
//TShotTrail END

//TSentinel BEGIN
procedure TSentinel.Init;
begin
  PrevShot.SetValue(0, 0);
  Vel.SetValue(0, 0);
  Size := SentinelSize;
  GunAng := -HalfPi;
  FactionID := Random(6);
  HealthMax := 50;
  Health := HealthMax;
  Dead := False;
end;

procedure TSentinel.Update;
  var x, y, i: Integer;
  var ColX, ColY: Boolean;
begin
  if Health < 0 then Health := 0;
  if Health = 0 then Die;
  if not Dead then
  begin
    Vel := Vel + App.Ground.Gravity;
    Pos := Pos + Vel;

    if Pos.x < 1 + Size then Pos.x := 1 + Size;
    if Pos.x > App.Ground.Width - 2 - Size then Pos.x := App.Ground.Width - 2 - Size;

    ColY := False;
    x := Round(Pos.x);
    y := Round(Pos.y + Size);
    if y > App.Ground.Height - 1 then
    begin
      y := App.Ground.Height - 1;
      ColY := True;
    end;
    if App.Ground.Collision[x, y] then
    ColY := True;
    while App.Ground.Collision[x, y] and (y > 0) do
    y := y - 1;
    if ColY then
    begin
      Pos.y := y - Size;
      Vel.y := 0;
      Vel.x := 0;
    end;

    ColX := False;
    x := Round(Pos.x - Size);
    y := Round(Pos.y);
    if x < 0 then
    begin
      x := 0;
      ColX := True;
    end;
    for i := Round(Pos.x) downto x do
    if App.Ground.Collision[x, y] then
    begin
      ColX := True;
      x := i;
    end;
    while App.Ground.Collision[x, y] and (x < App.Ground.Width - 1 - Size * 2) do
    x := x + 1;
    if ColX then
    begin
      Pos.x := x;
      Vel.x := -Vel.x * 0.7;
    end;

    ColX := False;
    x := Round(Pos.x + Size);
    y := Round(Pos.y);
    if x > App.Ground.Width - 1 then
    begin
      x := App.Ground.Width - 1;
      ColX := True;
    end;
    for i := Round(Pos.x) to x do
    if App.Ground.Collision[x, y] then
    begin
      ColX := True;
      x := i;
    end;
    while App.Ground.Collision[x, y] and (x > Size * 2) do
    x := x - 1;
    if ColX then
    begin
      Pos.x := x;
      Vel.x := -Vel.x * 0.7;
    end;
  end;
end;

procedure TSentinel.Shoot(const Power: Single);
  var Shot: TShot;
  var s, c, p: Single;
begin
  p := Power * 22;
  G2SinCos(GunAng, s, c);
  PrevShot.SetValue(c * Power * 100, s * Power * 100);
  Shot := TShotTrail.Create;
  Shot.Pos := Pos;
  Shot.PosPrev := Pos;
  Shot.StartPos := Pos;
  Shot.FactionID := FactionID;
  Shot.Vel.SetValue(c * p, s * p);
  Shot.Update;
  App.Ground.Shots.Add(Shot);
end;

procedure TSentinel.Die;
begin
  if not Dead then
  begin
    if App.Player = @Self then
    App.Player := nil;
    Dead := True;
    App.Ground.SentinelsDead.Add(@Self);
  end;
end;
//TSentinel END

//TGround BEGIN
procedure TGround.Initialize;
begin
  Pos.SetValue(0, 0);
  Scale := 1;
  Width := 1024; Height := 576;
  //Width := 512; Height := 288;
  //Width := 480; Height := 320;
  SetLength(Collision, Width, Height);
  Tex := TG2Texture2D.Create;
  TexRT := TG2Texture2DRT.Create;
  TexRT.Make(Width, Height);
  Shots.Clear;
  ShotsDead.Clear;
  Sentinels.Clear;
  SentinelsDead.Clear;
  Particles := TG2Group.Create;
  Traces := TG2Group.Create;
  Gravity.SetValue(0, 0.4);
end;

procedure TGround.Finalize;
  var i: Integer;
begin
  for i := 0 to Shots.Count - 1 do
  TShot(Shots[i]).Free;
  Shots.Clear;
  for i := 0 to Sentinels.Count - 1 do
  Dispose(PSentinel(Sentinels[i]));
  Sentinels.Clear;
  TexRT.Free;
  Tex.Free;
  Traces.Free;
  Particles.Free;
end;

procedure TGround.Generate;
  var i, j, n, h: Integer;
  var NoiseArr: array of Integer;
  var pn: TG2PerlinNoise;
  var TexGround: TG2Texture2D;
  var Img: TG2Image;
  var ImgGround0, ImgGround1: TG2ImagePNG;
  var c, c1: TG2ColorR8G8B8A8;
  var g: Single;
begin
  Randomize;
  SetLength(NoiseArr, Width);
  pn := TG2PerlinNoise.Create;
  for i := 0 to Width - 1 do
  begin
    n := Round((pn.PerlinNoise2D(i, 0, 2, 0.3, 0.03 * (1024 / Width), 1, RandSeed) * 0.5 + 0.5) * (Height - 1));
    n := Min(Max(n, 0), Height - 1);
    NoiseArr[i] := n;
    for j := 0 to n - 1 do
    Collision[i, j] := False;
    for j := n to Height - 1 do
    Collision[i, j] := True;
  end;
  ImgGround0 := TG2ImagePNG.Create;
  ImgGround0.Load('data\gfx\Ground04.png');
  ImgGround1 := TG2ImagePNG.Create;
  ImgGround1.Load('data\gfx\Ground05.png');
  Img := TG2Image.Create;
  Img.Allocate(G2IF_R8G8B8A8, Width, Height);
  for j := 0 to Height - 1 do
  for i := 0 to Width - 1 do
  begin
    if Collision[i, j] then
    begin
      c := ImgGround0.Pixels[i mod ImgGround0.Width, j mod ImgGround0.Height];
    end
    else
    begin
      PLongWord(@c)^ := 0;
    end;
    Img.Pixels[i, j] := c;
  end;
  ImgGround0.Free;
  Randomize;
  for i := 0 to Width - 1 do
  begin
    h := Round(Min(Max(pn.PerlinNoise2D(i, 0, 4, 0.5, 0.1 * (1024 / Width), 1, RandSeed) * 0.5 + 0.5, 0), 1) * 40 + 11);
    for j := NoiseArr[i] to Min(Height - 1, NoiseArr[i] + h) do
    begin
      n := j - NoiseArr[i];
      c := ImgGround1.Pixels[i mod ImgGround1.Width, n mod ImgGround1.Height];
      if n > h - 10 then
      begin
        c1 := Img.Pixels[i, j];
        g := (n - h + 10) / 10;
        if g > 1 then g := 1;
        c.r := Round(G2LerpFloat(c.r, c1.r, g));
        c.g := Round(G2LerpFloat(c.g, c1.g, g));
        c.b := Round(G2LerpFloat(c.b, c1.b, g));
        c.a := Round(G2LerpFloat(c.a, c1.a, g));
      end;
      Img.Pixels[i, j] := c;
    end;
  end;
  pn.Free;
  Tex.Load(Img, tuUsage2D);
  Img.Free;
  CopyRT := True;
end;

procedure TGround.Render;
  var i: Integer;
  var Sent: PSentinel;
  var gd: PGroundDamage;
  var s, c, casp, l: Single;
  var sp, v: TG2Vec2;
  var BMDamage: TG2BlendMode;
begin
  if CopyRT then
  begin
    g2.Gfx.StateChange.StateRenderTargetTexture2D(TexRT);
    g2.Gfx.StateChange.StateClear($00000000);
    g2.PicRect(0.5, 0.5, TexRT.Width, TexRT.Height, $ffffffff, Tex);
    g2.Gfx.StateChange.StateRenderTargetDefault;
    CopyRT := False;
  end;
  if Damage.Count > 0 then
  begin
    BMDamage.ColorSrc := boSrcAlpha;
    BMDamage.ColorDst := boInvSrcAlpha;
    BMDamage.AlphaSrc := boZero;
    BMDamage.AlphaDst := boOne;
    g2.Gfx.StateChange.StateRenderTargetTexture2D(TexRT);
    for i := 0 to Damage.Count - 1 do
    begin
      gd := PGroundDamage(Damage[i]);
      g2.PrimCircleCol(gd^.Pos, gd^.Radius, 0, 0, 16, bmDisable);
      App.DrawRing(
        gd^.Pos, gd^.Radius - 1, gd^.Radius + 15,
        App.TexGroundDamage, $ffffffff, BMDamage, 10
      );
      Dispose(gd);
    end;
    g2.Gfx.StateChange.StateRenderTargetDefault;
    Damage.Clear;
  end;
  g2.PrimRectCol(
    Pos.x, Pos.y, Width * Scale, Height * Scale,
    $ffc5e7ff, $ffc5e7ff, $ff2f99e3, $ff2f99e3
  );
  casp := Width / Height;
  g2.PicRectCol(
    Pos.x, Pos.y, Width * Scale, Height * Scale,
    $ff808080, $ff808080,
    $ff000000, $ff000000,
    0, 0, casp, 1,
    App.TexClouds, bmAdd, tfLinear
  );
  g2.PicRect(Pos, Width * Scale, Height * Scale, $ffffffff, TexRT, bmNormal, tfLinear);
  Particles.Render();
  for i := 0 to Shots.Count - 1 do
  TShot(Shots[i]).Render;
  for i := 0 to Sentinels.Count - 1 do
  begin
    Sent := PSentinel(Sentinels[i]);
    sp.x := Pos.x + Round(Sent^.Pos.x) * Scale;
    sp.y := Pos.y + Round(Sent^.Pos.y) * Scale;
    if (CurFaction = App.PlayerFaction) then
    begin
      if (Sent = App.Player)
      and Aiming then
      begin
        v := App.PosScreenToGround(g2.MousePos) - Sent^.Pos;
        l := v.Len;
        if l > 100 then
        begin
          v := v.Norm * 100;
          l := 100;
        end;
        if Sent^.PrevShot.LenSq > 1 then
        App.DrawArrow(sp, sp + Sent^.PrevShot * Scale, $40ffffff);
        App.DrawArrow(sp, sp + v * Scale, G2LerpColor($ffffff00, $ffff0000, l / 100));
      end;
    end
    else
    begin
      if (Sent = App.Player)
      and (AIState = aiShoot) then
      begin
        G2SinCos(Sent^.GunAng, v.y, v.x);
        v := v * (AIPower * 100);
        App.DrawArrow(sp, sp + v * Scale, G2LerpColor($ffffff00, $ffff0000, l / 100));
      end;
    end;
    App.DrawHealth(sp, Sent^.Size * 3, Sent^.Size * 4, Sent^.Health / Sent^.HealthMax, FactionColor[Sent^.FactionID]);
    g2.PicRect(
      sp, Sent^.Size, Sent^.Size,
      $ffffffff, 0.5, 0.5, Scale * 3, Scale * 3,
      0, False, False, App.TexTurretBase[Sent^.FactionID],
      App.TexTurretBase[Sent^.FactionID].Width, App.TexTurretBase[Sent^.FactionID].Height, 0,
      bmNormal, tfLinear
    );
    g2.PicRect(
      sp, Sent^.Size * 3, Sent^.Size * 1.5,
      $ffffffff, 0.2, 0.5, Scale, Scale,
      Sent^.GunAng, False, False, App.TexTurretGun,
      App.TexTurretGun.Width, App.TexTurretGun.Height, 0,
      bmNormal, tfLinear
    );
    if (App.Player = nil) and (App.PlayerFaction = CurFaction) and (Sent^.FactionID = CurFaction) and (GameMode = gmPlay) then
    App.DrawRing(sp, SentinelSize * 4, SentinelSize * 6, App.TexTarget, $ff0000ff, bmNormal, 4, 32, G2PiTime());
  end;
  g2.PrimRect(0, 0, Pos.x, g2.Params.Height, $ff000000);
  g2.PrimRect(Pos.x + Width * Scale, 0, Pos.x, g2.Params.Height, $ff000000);
  g2.PrimRect(0, 0, g2.Params.Width, Pos.y, $ff000000);
  g2.PrimRect(0, g2.Params.Height - Pos.y, g2.Params.Width, Pos.y, $ff000000);
end;

procedure TGround.Update;
  var i: Integer;
  var mc: TG2Vec2;
begin
  if (GameMode = gmObserve)
  and (Shots.Count = 0)
  and (App.CurTime - ModeTime > 3000) then
  begin
    GameMode := gmPlay;
    NextTurn;
  end;
  if GameMode = gmPlay then
  begin
    if (CurFaction = App.PlayerFaction) then
    begin
      if (App.Player <> nil) then
      begin
        mc := G2Vec2(g2.MousePos.x, g2.MousePos.y);
        mc := App.PosScreenToGround(G2Vec2(g2.MousePos.x, g2.MousePos.y));
        App.Player^.GunAng := (mc - G2Vec2(Round(App.Player^.Pos.x), Round(App.Player^.Pos.y))).AngleOX;
      end;
    end
    else
    begin
      DoAI;
    end;
  end;
  for i := 0 to Sentinels.Count - 1 do
  PSentinel(Sentinels[i])^.Update;
  for i := 0 to SentinelsDead.Count - 1 do
  begin
    Sentinels.Remove(SentinelsDead[i]);
    Dispose(PSentinel(SentinelsDead[i]));
  end;
  SentinelsDead.Clear;
  for i := 0 to Shots.Count - 1 do
  if not TShot(Shots[i]).Dead then
  TShot(Shots[i]).Update;
  for i := 0 to ShotsDead.Count - 1 do
  begin
    Shots.Remove(ShotsDead[i]);
    TShot(ShotsDead[i]).Free;
  end;
  ShotsDead.Clear;
  Traces.Update;
  Particles.Update;
end;

procedure TGround.BeginLevel;
  var i: Integer;
begin
  for i := 0 to Sentinels.Count - 1 do
  Dispose(PSentinel(Sentinels[i]));
  Sentinels.Clear;
  Generate;
  App.PlayerFaction := 0;
  AddSentinel(Random(Width), 0);
  AddSentinel(Random(Width), 1);
  AddSentinel(Random(Width), 2);
  AddSentinel(Random(Width), 3);
  AddSentinel(Random(Width), 4);
  AddSentinel(Random(Width), 5);
  CurFaction := -1;
  NextTurn;
end;

procedure TGround.BeginLevel(const f0, f1: Integer);
  var i, j, n, w: Integer;
  var b: Boolean;
  var Sent: PSentinel;
begin
  for i := 0 to Sentinels.Count - 1 do
  Dispose(PSentinel(Sentinels[i]));
  Sentinels.Clear;
  Generate;
  App.PlayerFaction := 0;
  w := Round(Width * 0.4);
  for i := 0 to 2 do
  begin
    repeat
      b := True;
      n := Random(w) + SentinelSize;
      for j := 0 to Sentinels.Count - 1 do
      if Abs(PSentinel(Sentinels[j])^.Pos.x - n) < SentinelSize * 2 then
      begin
        b := False;
        Break;
      end;
    until b;
    AddSentinel(n, f0);
  end;
  for i := 0 to 2 do
  begin
    repeat
      b := True;
      n := Width - 1 - Random(w) - SentinelSize;
      for j := 0 to Sentinels.Count - 1 do
      if Abs(PSentinel(Sentinels[j])^.Pos.x - n) < SentinelSize * 2 then
      begin
        b := False;
        Break;
      end;
    until b;
    AddSentinel(n, f1);
  end;
  CurFaction := -1;
  NextTurn;
end;

procedure TGround.NextTurn;
  var n, i: Integer;
  var r: Boolean;
begin
  if Sentinels.Count > 0 then
  begin
    n := CurFaction;
    repeat
      CurFaction := (CurFaction + 1) mod 6;
      r := False;
      for i := 0 to Sentinels.Count - 1 do
      if PSentinel(Sentinels[i])^.FactionID = CurFaction then
      begin
        r := True;
        Break;
      end;
    until r;
    if CurFaction = n then
    begin
      App.Map.Winner(CurFaction);
      App.GameState := gsMap;
    end
    else
    begin
      GameMode := gmPlay;
      Aiming := False;
      App.Player := nil;
      AITime := App.CurTime;
      AIState := aiSelectSentinel;
    end;
  end
  else
  begin
    App.Map.Winner(-1);
    App.GameState := gsMap;
  end;
end;

procedure TGround.DoAI;
  var i: Integer;
  var lst: TG2QuickList;
  var Sent: PSentinel;
  var Pos0, Pos1, V: TG2Vec2;
  var n, n1: Single;
begin
  if App.CurTime - AITime < 1000 then Exit;
  AITime := App.CurTime;
  case AIState of
    aiSelectSentinel:
    begin
      lst.Clear;
      for i := 0 to Sentinels.Count - 1 do
      if PSentinel(Sentinels[i])^.FactionID = CurFaction then
      lst.Add(Sentinels[i]);
      if lst.Count > 0 then
      begin
        App.Player := PSentinel(lst[Random(lst.Count)]);
        AIState := aiAim;
      end;
    end;
    aiAim:
    begin
      lst.Clear;
      for i := 0 to Sentinels.Count - 1 do
      if PSentinel(Sentinels[i])^.FactionID <> CurFaction then
      lst.Add(Sentinels[i]);
      if lst.Count > 0 then
      begin
        Sent := PSentinel(lst[Random(lst.Count)]);
        Pos0 := App.Player^.Pos;
        Pos1 := Sent^.Pos;
        n := (Pos1 - Pos0).Len * 0.5;
        V := Pos1 / n - Pos0 / n - App.Ground.Gravity * n * 0.5;
        n1 := V.Len;
        i := 0;
        while (n1 > 22) and (i < 200) do
        begin
          n := n * 22 / n1;
          V := Pos1 / n - Pos0 / n - App.Ground.Gravity * n * 0.5;
          n1 := V.Len;
          Inc(i);
        end;
        App.Player^.GunAng := V.AngleOX;
        AIPower := V.Len / 22;
        if AIPower > 1 then AIPower := 1;
        AIState := aiShoot;
      end;
    end;
    aiShoot:
    begin
      AIPower := AIPower + (Random(201) - 100) * 0.001;
      if AIPower < 0.01 then AIPower := 0.01;
      App.Player^.Shoot(AIPower);
      App.Player := nil;
      GameMode := gmObserve;
      ModeTime := App.CurTime;
    end;
  end;
end;

procedure TGround.MouseDown(const Button, x, y: Integer);
begin
  case GameMode of
    gmPlay:
    begin
      if Button = G2MB_Left then
      begin
        if CurFaction = App.PlayerFaction then
        begin
          if (App.Player <> nil) then
          begin
            Aiming := True;
          end;
        end;
      end;
    end;
  end;
end;

procedure TGround.MouseUp(const Button, x, y: Integer);
  var mc: TG2Vec2;
  var i: Integer;
  var d: Single;
begin
  case GameMode of
    gmPlay:
    begin
      if Button = G2MB_Left then
      begin
        if CurFaction = App.PlayerFaction then
        begin
          if (App.Player = nil) then
          begin
            mc := App.PosScreenToGround(G2Vec2(x, y));
            for i := 0 to Sentinels.Count - 1 do
            if ((PSentinel(Sentinels[i])^.Pos - mc).Len < SentinelSize * 2)
            and ((PSentinel(Sentinels[i])^.Pos - App.PosScreenToGround(g2.MouseDownPos[G2MB_Left])).Len < SentinelSize * 2)then
            begin
              App.Player := PSentinel(Sentinels[i]);
              Break;
            end;
          end
          else
          begin
            if Aiming then
            begin
              mc := App.PosScreenToGround(G2Vec2(x, y));
              d := (mc - App.Player^.Pos).Len;
              if d > 100 then d := 100;
              App.Player^.Shoot(d * 0.01);
              App.Player := nil;
              GameMode := gmObserve;
              ModeTime := App.CurTime;
              Aiming := False;
            end;
          end;
        end;
      end;
    end;
  end;
end;

function TGround.AddSentinel(const PosX: Single; const Faction: Integer = 0): PSentinel;
  var i, j: Integer;
begin
  i := Trunc(PosX);
  for j := SentinelSize to Height - 1 do
  begin
    if Collision[i, j] then
    begin
      New(Result);
      Result^.Pos.SetValue(i, j - SentinelSize);
      Result^.Init;
      Result^.FactionID := Faction;
      Sentinels.Add(Result);
      Exit;
    end;
  end;
  Result := nil;
end;

procedure TGround.DoDamange(const DamPos: TG2Vec2; const Radius: Single; const Amount: Integer);
  var gd: PGroundDamage;
  var i, j, xl, xh, yl, yh: Integer;
  var rsq, d: Single;
  var v: TG2Vec2;
begin
  for i := 0 to Sentinels.Count - 1 do
  begin
    v := PSentinel(Sentinels[i])^.Pos - DamPos;
    d := v.Len / (Radius * 2);
    if d > 1 then d := 1;
    if d < 1 then
    begin
      d := Cos(d * HalfPi);
      PSentinel(Sentinels[i])^.Health := PSentinel(Sentinels[i])^.Health - Round(Amount * d);
      PSentinel(Sentinels[i])^.Vel := PSentinel(Sentinels[i])^.Vel + v.Norm * (Amount * d * 0.05);
      if PSentinel(Sentinels[i])^.Health < 0 then PSentinel(Sentinels[i])^.Health := 0;
    end;
  end;
  New(gd);
  gd^.Pos := DamPos;
  gd^.Radius := Radius;
  Damage.Add(gd);
  xl := Round(DamPos.x - Radius); if xl < 0 then xl := 0;
  yl := Round(DamPos.y - Radius); if yl < 0 then yl := 0;
  xh := Round(DamPos.x + Radius); if xh > Width - 1 then xh := Width - 1;
  yh := Round(DamPos.y + Radius); if yh > Height - 1 then yh := Height - 1;
  rsq := Radius * Radius;
  for j := yl to yh do
  for i := xl to xh do
  if (G2Vec2(i, j) - DamPos).LenSq <= rsq then
  Collision[i, j] := False;
end;

function TGround.Trace(const v0, v1: TG2Vec2; var Hit: TG2Vec2): Boolean;
  var d, p: TG2Vec2;
  var i, n, x, y: Integer;
begin
  d := v1 - v0;
  n := Round(d.Len);
  d := d.Norm;
  p := v0;
  for i := 0 to n - 1 do
  begin
    p := p + d;
    x := Trunc(p.x);
    y := Trunc(p.y);
    if (x >= 0) and (x < Width)
    and (y >= 0) and (y < Height)
    and Collision[x, y] then
    begin
      Result := True;
      Hit := p;
      Exit;
    end;
  end;
  Result := False;
end;

function TGround.CheckCollide(const x, y: Single): Boolean;
  var xi, yi: Integer;
begin
  xi := Trunc(x);
  yi := Trunc(y);
  if (xi < 0) or (xi > Width - 1) or (yi > Height - 1) then
  begin
    Result := True;
    Exit;
  end;
  if yi < 0 then
  begin
    Result := False;
    Exit;
  end;
  Result := Collision[xi, yi];
end;

procedure TGround.Explosion(const ExplPos: TG2Vec2; const Size: Single; const FlackCount: Integer);
  var e: TPartExplosion;
  var w: TPartWave;
  var f: TPartFlare;
  var tr: TTraceFlame;
  var i: Integer;
begin
  for i := 0 to 2 do
  begin
    e := TPartExplosion.Create(App.Ground.Particles);
    e.Size := 4 * Size;
    e.Pos := ExplPos + G2RandomCirclePoint * Random(101) * 0.05;
    e.Update;
  end;
  w := TPartWave.Create(App.Ground.Particles);
  w.Size := 3 * Size;
  w.Pos := ExplPos;
  w.Update;
  f := TPartFlare.Create(App.Ground.Particles);
  f.Size := 3 * Size;
  f.Pos := ExplPos;
  f.Update;
  for i := 0 to FlackCount - 1 do
  begin
    tr := TTraceFlame.Create(App.Ground.Traces);
    tr.ParticleClass := TPartFlame;
    tr.Pos := ExplPos;
    tr.Vel := (G2RandomCirclePoint + G2Vec2(0, -1)).Norm * ((Random(31) * 0.01 * Size + 0.3 * Size));
    tr.Density := 8;
    tr.Scatter := 3;
    tr.TimeToExpire := 800 + Random(201);
    tr.Update;
  end;
end;
//TGround END

//TMenu BEGIN
procedure TMenu.Initialize;
  var w, h, s: Single;
begin
  TexTitle := TG2Texture2D.Create;
  TexTitle.Load('data\gfx\Title.png', tuUsage2D);
  TexBtnNewGame0 := TG2Texture2D.Create;
  TexBtnNewGame0.Load('data\gfx\MBNewGame0.png', tuUsage2D);
  TexBtnNewGame1 := TG2Texture2D.Create;
  TexBtnNewGame1.Load('data\gfx\MBNewGame1.png', tuUsage2D);
  TexBtnQuit0 := TG2Texture2D.Create;
  TexBtnQuit0.Load('data\gfx\MBQuit0.png', tuUsage2D);
  TexBtnQuit1 := TG2Texture2D.Create;
  TexBtnQuit1.Load('data\gfx\MBQuit1.png', tuUsage2D);
  Buttons.Clear;
  AddButton(TexBtnNewGame0, TexBtnNewGame1, @BtnNewGame);
  AddButton(TexBtnQuit0, TexBtnQuit1, @BtnQuit);
  RTitle.h := g2.Params.Height * 0.2;
  RTitle.w := TexTitle.Width * (RTitle.h / TexTitle.Height);
  RTitle.x := (g2.Params.Width - RTitle.w) * 0.5;
  RTitle.y := 0;
  RButtons.h := g2.Params.Height * 0.5;
  RButtons.w := 600 * RButtons.h / 500;
  RButtons.x := (g2.Params.Width - RButtons.w) * 0.5;
  RButtons.y := (g2.Params.Height - RButtons.h) * 0.5;
  w := RButtons.w / TexBtnNewGame0.Width;
  h := RButtons.h / Buttons.Count / TexBtnNewGame0.Height;
  if w < h then s := w else s := h;
  BtnW := TexBtnNewGame0.Width * s;
  BtnH := TexBtnNewGame0.Height * s;
  BtnX := RButtons.x + (RButtons.w - BtnW) * 0.5;
  BtnY := RButtons.y + (RButtons.h - BtnH * Buttons.Count) * 0.5;
end;

procedure TMenu.Finalize;
  var i: Integer;
begin
  for i := 0 to Buttons.Count - 1 do
  Dispose(PMenuButton(Buttons[i]));
  Buttons.Clear;
end;

procedure TMenu.Render;
  var i, n: Integer;
  var b: PMenuButton;
  var Tex: TG2Texture2D;
begin
  App.RenderBox(
    RButtons, 32 * RButtons.h / 500,
    $80ffffff, App.TexSpot, bmNormal
  );
  g2.PicRect(
    RTitle.x, RTitle.y, RTitle.w, RTitle.h,
    $ffffffff, TexTitle, bmNormal, tfLinear
  );
  n := McInButton;
  for i := 0 to Buttons.Count - 1 do
  begin
    b := PMenuButton(Buttons[i]);
    if i = n then
    Tex := b^.Tex1
    else
    Tex := b^.Tex0;
    g2.PicRect(BtnX, BtnY + i * BtnH, BtnW, BtnH, $ffffffff, Tex, bmNormal, tfLinear);
  end;
end;

procedure TMenu.Update;
begin

end;

procedure TMenu.AddButton(const Tex0, Tex1: TG2Texture2D; const Proc: TG2ProcObj);
  var pb: PMenuButton;
begin
  New(pb);
  pb^.Tex0 := Tex0;
  pb^.Tex1 := Tex1;
  pb^.Proc := Proc;
  Buttons.Add(pb);
end;

procedure TMenu.MouseUp(const Button, x, y: Integer);
  var i: Integer;
  var b: PMenuButton;
begin
  i := McInButton;
  if i = MdInButton then
  begin
    b := PMenuButton(Buttons[i]);
    if Assigned(b^.Proc) then b^.Proc;
  end;
end;

function TMenu.McInButton: Integer;
  var i: Integer;
  var mc: TPoint;
  var r: TG2Rect;
begin
  mc := g2.MousePos;
  for i := 0 to Buttons.Count - 1 do
  begin
    r := G2Rect(BtnX, BtnY + BtnH * i, BtnW, BtnH);
    if PtInRect(r, mc) then
    begin
      Result := i;
      Exit;
    end;
  end;
  Result := -1;
end;

function TMenu.MdInButton: Integer;
  var i: Integer;
  var r: TG2Rect;
begin
  for i := 0 to Buttons.Count - 1 do
  begin
    r := G2Rect(BtnX, BtnY + BtnH * i, BtnW, BtnH);
    if PtInRect(r, g2.MouseDownPos[G2MB_Left]) then
    begin
      Result := i;
      Exit;
    end;
  end;
  Result := -1;
end;

procedure TMenu.BtnNewGame;
begin
  //App.Ground.BeginLevel;
  App.GameState := gsMap;
  App.Map.GameBegin;
end;

procedure TMenu.BtnQuit;
begin
  g2.Stop;
end;
//TMenu END

//TMap BEGIN
procedure TMap.Initialize;
  var AreaArr: array[0..12] of PMapArea;
  var i, j: Integer;
  var MinV, MaxV: TG2Vec2;
  var w, h: Single;
begin
  AreaBegin; AreaArr[0] := CurArea;
  AreaAdd(G2Vec2(266, 87)); AreaAdd(G2Vec2(294, 103)); AreaAdd(G2Vec2(292, 135)); AreaAdd(G2Vec2(265, 149)); AreaAdd(G2Vec2(239, 135)); AreaAdd(G2Vec2(239, 103));
  AreaEnd;

  AreaBegin; AreaArr[1] := CurArea;
  AreaAdd(G2Vec2(177, 137)); AreaAdd(G2Vec2(204, 154)); AreaAdd(G2Vec2(204, 184)); AreaAdd(G2Vec2(177, 201)); AreaAdd(G2Vec2(149, 185)); AreaAdd(G2Vec2(149, 154));
  AreaEnd;
  AreaBegin; AreaArr[2] := CurArea;
  AreaAdd(G2Vec2(237, 137)); AreaAdd(G2Vec2(264, 154)); AreaAdd(G2Vec2(264, 184)); AreaAdd(G2Vec2(237, 201)); AreaAdd(G2Vec2(209, 185)); AreaAdd(G2Vec2(209, 154));
  AreaEnd;
  AreaBegin; AreaArr[3] := CurArea;
  AreaAdd(G2Vec2(296, 137)); AreaAdd(G2Vec2(323, 154)); AreaAdd(G2Vec2(323, 184)); AreaAdd(G2Vec2(296, 201)); AreaAdd(G2Vec2(267, 185)); AreaAdd(G2Vec2(267, 154));
  AreaEnd;
  AreaBegin; AreaArr[4] := CurArea;
  AreaAdd(G2Vec2(356, 137)); AreaAdd(G2Vec2(380, 154)); AreaAdd(G2Vec2(380, 184)); AreaAdd(G2Vec2(356, 201)); AreaAdd(G2Vec2(325, 185)); AreaAdd(G2Vec2(325, 154));
  AreaEnd;

  AreaBegin; AreaArr[5] := CurArea;
  AreaAdd(G2Vec2(207, 187)); AreaAdd(G2Vec2(234, 203)); AreaAdd(G2Vec2(234, 235)); AreaAdd(G2Vec2(207, 250)); AreaAdd(G2Vec2(179, 235)); AreaAdd(G2Vec2(179, 204));
  AreaEnd;
  AreaBegin; AreaArr[6] := CurArea;
  AreaAdd(G2Vec2(266, 187)); AreaAdd(G2Vec2(292, 203)); AreaAdd(G2Vec2(292, 235)); AreaAdd(G2Vec2(266, 250)); AreaAdd(G2Vec2(237, 235)); AreaAdd(G2Vec2(237, 204));
  AreaEnd;
  AreaBegin; AreaArr[7] := CurArea;
  AreaAdd(G2Vec2(324, 187)); AreaAdd(G2Vec2(352, 203)); AreaAdd(G2Vec2(352, 235)); AreaAdd(G2Vec2(324, 250)); AreaAdd(G2Vec2(297, 235)); AreaAdd(G2Vec2(297, 204));
  AreaEnd;

  AreaBegin; AreaArr[8] := CurArea;
  AreaAdd(G2Vec2(177, 239)); AreaAdd(G2Vec2(203, 255)); AreaAdd(G2Vec2(203, 286)); AreaAdd(G2Vec2(177, 301)); AreaAdd(G2Vec2(149, 286)); AreaAdd(G2Vec2(149, 255));
  AreaEnd;
  AreaBegin; AreaArr[9] := CurArea;
  AreaAdd(G2Vec2(237, 239)); AreaAdd(G2Vec2(264, 255)); AreaAdd(G2Vec2(264, 286)); AreaAdd(G2Vec2(237, 301)); AreaAdd(G2Vec2(208, 286)); AreaAdd(G2Vec2(208, 255));
  AreaEnd;
  AreaBegin; AreaArr[10] := CurArea;
  AreaAdd(G2Vec2(294, 239)); AreaAdd(G2Vec2(321, 255)); AreaAdd(G2Vec2(321, 286)); AreaAdd(G2Vec2(294, 301)); AreaAdd(G2Vec2(266, 286)); AreaAdd(G2Vec2(266, 255));
  AreaEnd;
  AreaBegin; AreaArr[11] := CurArea;
  AreaAdd(G2Vec2(353, 239)); AreaAdd(G2Vec2(381, 255)); AreaAdd(G2Vec2(381, 286)); AreaAdd(G2Vec2(353, 301)); AreaAdd(G2Vec2(325, 286)); AreaAdd(G2Vec2(325, 255));
  AreaEnd;

  AreaBegin; AreaArr[12] := CurArea;
  AreaAdd(G2Vec2(265, 290)); AreaAdd(G2Vec2(292, 306)); AreaAdd(G2Vec2(292, 337)); AreaAdd(G2Vec2(265, 354)); AreaAdd(G2Vec2(237, 337)); AreaAdd(G2Vec2(237, 305));
  AreaEnd;

  AreaLink(AreaArr[0], AreaArr[2]); AreaLink(AreaArr[0], AreaArr[3]);
  AreaLink(AreaArr[1], AreaArr[2]); AreaLink(AreaArr[1], AreaArr[5]);
  AreaLink(AreaArr[2], AreaArr[3]); AreaLink(AreaArr[2], AreaArr[5]); AreaLink(AreaArr[2], AreaArr[6]);
  AreaLink(AreaArr[3], AreaArr[4]); AreaLink(AreaArr[3], AreaArr[6]); AreaLink(AreaArr[3], AreaArr[7]);
  AreaLink(AreaArr[4], AreaArr[7]);
  AreaLink(AreaArr[5], AreaArr[6]); AreaLink(AreaArr[5], AreaArr[8]); AreaLink(AreaArr[5], AreaArr[9]);
  AreaLink(AreaArr[6], AreaArr[7]); AreaLink(AreaArr[6], AreaArr[9]); AreaLink(AreaArr[6], AreaArr[10]);
  AreaLink(AreaArr[7], AreaArr[10]); AreaLink(AreaArr[7], AreaArr[11]);
  AreaLink(AreaArr[8], AreaArr[9]);
  AreaLink(AreaArr[9], AreaArr[10]); AreaLink(AreaArr[9], AreaArr[12]);
  AreaLink(AreaArr[10], AreaArr[11]); AreaLink(AreaArr[10], AreaArr[12]);

  AreaArr[0]^.Faction := 0;
  AreaArr[1]^.Faction := 1;
  AreaArr[4]^.Faction := 2;
  AreaArr[8]^.Faction := 3;
  AreaArr[11]^.Faction := 4;
  AreaArr[12]^.Faction := 5;

  MinV := AreaArr[0]^.Geom.v[0];
  MaxV := MinV;
  for i := 0 to Areas.Count - 1 do
  for j := 0 to High(PMapArea(Areas[i])^.Geom.v) do
  begin
    if PMapArea(Areas[i])^.Geom.v[j].x < MinV.x then MinV.x := PMapArea(Areas[i])^.Geom.v[j].x;
    if PMapArea(Areas[i])^.Geom.v[j].y < MinV.y then MinV.y := PMapArea(Areas[i])^.Geom.v[j].y;
    if PMapArea(Areas[i])^.Geom.v[j].x > MaxV.x then MaxV.x := PMapArea(Areas[i])^.Geom.v[j].x;
    if PMapArea(Areas[i])^.Geom.v[j].y > MaxV.y then MaxV.y := PMapArea(Areas[i])^.Geom.v[j].y;
  end;
  w := (g2.Params.Width * 0.9) / (MaxV.x - MinV.x);
  h := (g2.Params.Height * 0.9) / (MaxV.y - MinV.y);
  if w < h then Scale := w else Scale := h;
  Pos.x := -MinV.x * Scale + (g2.Params.Width - (MaxV.x - MinV.x) * Scale) * 0.5;
  Pos.y := -MinV.y * Scale + (g2.Params.Height - (MaxV.y - MinV.y) * Scale) * 0.5;

  CurFaction := -1;
end;

procedure TMap.Finalize;
  var i: Integer;
begin
  for i := 0 to Areas.Count - 1 do
  Dispose(PMapArea(Areas[i]));
  Areas.Clear;
end;

procedure TMap.Render;
  var i, j: Integer;
  var a: PMapArea;
  var Col: TG2Color;
  var v0, v1: TG2Vec2;
begin
  for i := 0 to Areas.Count - 1 do
  begin
    a := PMapArea(Areas[i]);
    if a^.Faction > -1 then
    Col := FactionColor[a^.Faction]
    else
    Col := $80ffffff;
    for j := 0 to High(a^.Geom.Triangles) do
    begin
      g2.PrimTriCol(
        Pos + a^.Geom.v[a^.Geom.Triangles[j][0]] * Scale,
        Pos + a^.Geom.v[a^.Geom.Triangles[j][1]] * Scale,
        Pos + a^.Geom.v[a^.Geom.Triangles[j][2]] * Scale,
        Col, Col, Col
      );
    end;
  end;
  if CurFaction = App.PlayerFaction then
  begin
    for i := 0 to Areas.Count - 1 do
    if PMapArea(Areas[i])^.Faction = CurFaction then
    begin
      a := PMapArea(Areas[i]);
      for j := 0 to a^.Links.Count - 1 do
      if PMapArea(a^.Links[j])^.Faction <> CurFaction then
      begin
        v0 := (PMapArea(a^.Links[j])^.Center - a^.Center) * 0.25;
        v1 := (PMapArea(a^.Links[j])^.Center + a^.Center) * 0.5;
        App.DrawArrow(
          Pos + (v1 - v0) * Scale, Pos + (v1 + v0 * 0.5) * Scale, $ff000000, 32 * Scale
        );
      end;
    end;
  end
  else if AIState = aiMove then
  begin
    v0 := (AIMove1^.Center - AIMove0^.Center) * 0.25;
    v1 := (AIMove1^.Center + AIMove0^.Center) * 0.5;
    App.DrawArrow(
      Pos + (v1 - v0) * Scale, Pos + (v1 + v0 * 0.5) * Scale, $ff000000, 32 * Scale
    );
  end;
  case Mode of
    mmVictory:
    begin
      g2.PicRect(
        g2.Params.Width * 0.5, g2.Params.Height * 0.5,
        App.TexVictory.Width, App.TexVictory.Height,
        $ffffffff, 0.5, 0.5, Scale, Scale, 0, False, False,
        App.TexVictory, App.TexVictory.Width, App.TexVictory.Height, 0,
        bmNormal, tfLinear
      );
    end;
    mmDefeat:
    begin
      g2.PicRect(
        g2.Params.Width * 0.5, g2.Params.Height * 0.5,
        App.TexDefeat.Width, App.TexDefeat.Height,
        $ffffffff, 0.5, 0.5, Scale, Scale, 0, False, False,
        App.TexDefeat, App.TexDefeat.Width, App.TexDefeat.Height, 0,
        bmNormal, tfLinear
      );
    end;
  end;
end;

procedure TMap.Update;
begin
  case Mode of
    mmPlay:
    begin
      if CurFaction <> App.PlayerFaction then
      DoAI;
    end;
  end;
end;

procedure TMap.DoAI;
  var i, j: Integer;
  var lst: TG2QuickList;
  var lst1: TG2QuickSortListIntS32;
  var a: PMapArea;
begin
  if App.CurTime - AITime < 1000 then Exit;
  AITime := App.CurTime;
  case AIState of
    aiChooseMove:
    begin
      lst.Clear;
      for i := 0 to Areas.Count - 1 do
      if PMapArea(Areas[i])^.Faction = CurFaction then
      begin
        for j := 0 to PMapArea(Areas[i])^.Links.Count - 1 do
        if PMapArea(PMapArea(Areas[i])^.Links[j])^.Faction <> CurFaction then
        begin
          lst.Add(Areas[i]);
          Break;
        end;
      end;
      lst1.Clear;
      a := PMapArea(lst[Random(lst.Count)]);
      for i := 0 to a^.Links.Count - 1 do
      if PMapArea(a^.Links[i])^.Faction <> CurFaction then
      begin
        if PMapArea(a^.Links[i])^.Faction = -1 then
        j := 0
        else
        j := 1 + Random(10);
        lst1.Add(i, j);
      end;
      AIMove0 := a;
      AIMove1 := PMapArea(a^.Links[lst1[0]]);
      AIState := aiMove;
    end;
    aiMove:
    begin
      if AIMove1^.Faction = -1 then
      begin
        AIMove1^.Faction := CurFaction;
        NextTurn;
      end
      else
      begin
        if AIMove1^.Faction = App.PlayerFaction then
        begin
          AreaHit := AIMove1;
          App.Ground.BeginLevel(CurFaction, AIMove1^.Faction);
          App.GameState := gsGame;
        end
        else
        begin
          if Random(2) = 1 then
          AIMove1^.Faction := CurFaction;
          NextTurn;
        end;
      end;
    end;
  end;
end;

procedure TMap.MouseDown(const Button, x, y: Integer);
begin

end;

procedure TMap.MouseUp(const Button, x, y: Integer);
  var i: Integer;
  var a: PMapArea;
begin
  case Mode of
    mmVictory, mmDefeat:
    begin
      App.GameState := gsMenu;
    end;
    mmPlay:
    begin
      if CurFaction = App.PlayerFaction then
      begin
        a := McInArea;
        if (a <> nil) and (MdInArea = a) then
        begin
          if a^.Faction = -1 then
          begin
            a^.Faction := CurFaction;
            NextTurn;
          end
          else
          begin
            AreaHit := a;
            App.Ground.BeginLevel(CurFaction, a^.Faction);
            App.GameState := gsGame;
          end;
        end;
      end;
    end;
  end;
end;

procedure TMap.AreaBegin;
begin
  New(CurArea);
  CurArea^.Geom.v := nil;
  CurArea^.Links.Clear;
  CurArea^.Faction := -1;
end;

procedure TMap.AreaAdd(const v: TG2Vec2);
begin
  if CurArea = nil then Exit;
  SetLength(CurArea^.Geom.v, Length(CurArea^.Geom.v) + 1);
  CurArea^.Geom.v[High(CurArea^.Geom.v)] := v;
end;

procedure TMap.AreaEnd;
  var i: Integer;
begin
  CurArea^.Center := CurArea^.Geom.v[0];
  for i := 1 to High(CurArea^.Geom.v) do
  CurArea^.Center := CurArea^.Center + CurArea^.Geom.v[i];
  CurArea^.Center := CurArea^.Center / Length(CurArea^.Geom.v);
  G2PolyTriangulate(@CurArea^.Geom);
  Areas.Add(CurArea);
  CurArea := nil;
end;

procedure TMap.AreaLink(const a0, a1: PMapArea);
begin
  a0^.Links.Add(a1);
  a1^.Links.Add(a0);
end;

procedure TMap.NextTurn;
  var i, n: Integer;
  var b: Boolean;
begin
  n := CurFaction;
  repeat
    CurFaction := (CurFaction + 1) mod 6;
    b := False;
    for i := 0 to Areas.Count - 1 do
    if PMapArea(Areas[i])^.Faction = CurFaction then
    begin
      b := True;
      Break;
    end;
  until b or (CurFaction = n);
  if CurFaction = n then
  begin
    if n = App.PlayerFaction then
    begin
      Mode := mmVictory;
    end
    else
    begin
      Mode := mmDefeat;
    end;
  end
  else
  begin
    AITime := App.CurTime;
    AIState := aiChooseMove;
  end;
end;

procedure TMap.GameBegin;
begin
  CurFaction := App.PlayerFaction;
  Mode := mmPlay;
end;

procedure TMap.Winner(const Faction: Integer);
begin
  if Faction > -1 then
  AreaHit^.Faction := Faction;
  NextTurn;
end;

function TMap.McInArea: PMapArea;
  var mc: TG2Vec2;
  var i: Integer;
begin
  mc := g2.MousePos;
  mc := (mc - Pos) / Scale;
  for i := 0 to Areas.Count - 1 do
  begin
    if G2Vec2InPoly(
      mc, @PMapArea(Areas[i])^.Geom.v[0],
      Length(PMapArea(Areas[i])^.Geom.v)
    ) then
    begin
      Result := PMapArea(Areas[i]);
      Exit;
    end;
  end;
  Result := nil;
end;

function TMap.MdInArea: PMapArea;
  var mc: TG2Vec2;
  var i: Integer;
begin
  mc := g2.MouseDownPos[G2MB_Left];
  mc := (mc - Pos) / Scale;
  for i := 0 to Areas.Count - 1 do
  begin
    if G2Vec2InPoly(
      mc, @PMapArea(Areas[i])^.Geom.v[0],
      Length(PMapArea(Areas[i])^.Geom.v)
    ) then
    begin
      Result := PMapArea(Areas[i]);
      Exit;
    end;
  end;
  Result := nil;
end;
//TMap END

//TG2App BEGIN
procedure TG2App.Initialize;
  var ws, hs: Single;
  var i: Integer;
begin
  //GameState := gsDebug;
  CurTime := 0;
  TimeRem := 0;
  Pause := False;
  g2.Window.Caption := 'template v1.0';
  GameState := gsLoading;
  ws := g2.Params.Width / 480;
  hs := g2.Params.Height / 320;
  if Abs(1 - ws) > Abs(1 - hs) then gs := ws else gs := hs;
  Font1 := TG2Font.Create;
  Font1.Make(24);
  g2.PackLinker.LinkPack('data.g2pk', 'Gen2 Key');
  for i := 0 to High(TexExplosion) do
  begin
    TexExplosion[i] := TG2Texture2D.Create;
    TexExplosion[i].Load('data\gfx\Explosion' + IntToStr(i) + '.png', tuUsage2D);
  end;
  TexWaveOutline := TG2Texture2D.Create;
  TexWaveOutline.Load('data\gfx\WaveOutline.png', tuUsage2D);
  TexFlare := TG2Texture2D.Create;
  TexFlare.Load('data\gfx\Flare.png', tuUsage2D);
  TexFlame := TG2Texture2D.Create;
  TexFlame.Load('data\gfx\Flame.png', tuUsage2D);
  TexClouds := TG2Texture2D.Create;
  TexClouds.Load('data\gfx\Clouds.png', tuUsage2D);
  TexShotTrail := TG2Texture2D.Create;
  TexShotTrail.Load('data\gfx\ShotTrail.png', tuUsage2D);
  TexMuzzleFlash := TG2Texture2D.Create;
  TexMuzzleFlash.Load('data\gfx\MuzzleFlash.png', tuUsage2D);
  for i := 0 to High(TexTurretBase) do
  begin
    TexTurretBase[i] := TG2Texture2D.Create;
    TexTurretBase[i].Load('data\gfx\Base' + IntToStr(i) + '.png', tuUsage2D);
  end;
  TexTurretGun := TG2Texture2D.Create;
  TexTurretGun.Load('data\gfx\Gun0.png', tuUsage2D);
  TexGroundDamage := TG2Texture2D.Create;
  TexGroundDamage.Load('data\gfx\GroundDamage.png', tuUsage2D);
  TexHealth := TG2Texture2D.Create;
  TexHealth.Load('data\gfx\Health.png', tuUsage2D);
  TexTarget := TG2Texture2D.Create;
  TexTarget.Load('data\gfx\Target.png', tuUsage2D);
  TexArrow := TG2Texture2D.Create;
  TexArrow.Load('data\gfx\Arrow.png', tuUsage2D);
  TexSpot := TG2Texture2D.Create;
  TexSpot.Load('data\gfx\Spot.png', tuUsage2D);
  TexVictory := TG2Texture2D.Create;
  TexVictory.Load('data\gfx\Victory.png', tuUsage2D);
  TexDefeat := TG2Texture2D.Create;
  TexDefeat.Load('data\gfx\Defeat.png', tuUsage2D);
  Menu.Initialize;
  Map.Initialize;
  Ground.Initialize;
  ws := g2.Params.Width / Ground.Width;
  hs := g2.Params.Height / Ground.Height;
  if ws < hs then Ground.Scale := ws else Ground.Scale := hs;
  Ground.Pos.x := (g2.Params.Width - Ground.Width * Ground.Scale) * 0.5;
  Ground.Pos.y := (g2.Params.Height - Ground.Height * Ground.Scale) * 0.5;
  GameState := gsMenu;
  g2.Gfx.StateChange.StateDepthEnable(False);
  if GameState = gsDebug then Exit;
end;

procedure TG2App.Finalize;
begin
  Ground.Finalize;
  Map.Finalize;
  Menu.Finalize;
  Font1.Free;
end;

procedure TG2App.Update;
  var mc: TG2Vec2;
begin
  if not Pause then
  begin
    TimeRem := TimeRem + 1000 / g2.Params.TargetUPS;
    CurTime := CurTime + Trunc(TimeRem);
    TimeRem := Frac(TimeRem);
  end;
  case GameState of
    gsMenu: Menu.Update;
    gsMap: Map.Update;
    gsGame: Ground.Update;
  end;
end;

procedure TG2App.Render;
begin
  case GameState of
    gsMenu: Menu.Render;
    gsMap: Map.Render;
    gsGame: Ground.Render;
  end;
  Font1.Print(0, 0, 'Res: ' + IntToStr(g2.Params.Width) + 'x' + IntToStr(g2.Params.Height) + '; FPS: ' + IntToStr(g2.FPS));
end;

procedure TG2App.KeyDown(const Key: Integer);
begin
  if Key = G2K_Escape then
  begin
    case GameState of
      gsMenu: g2.Stop;
      gsMap: GameState := gsMenu;
      gsGame: GameState := gsMenu;
      else g2.Stop;
    end;
  end;
end;

procedure TG2App.KeyUp(const Key: Integer);
begin

end;

procedure TG2App.MouseDown(const Button, x, y: Integer);
begin
  case GameState of
    gsMap: Map.MouseDown(Button, x, y);
    gsGame: Ground.MouseDown(Button, x, y);
  end;
end;

procedure TG2App.MouseUp(const Button, x, y: Integer);
begin
  case GameState of
    gsMenu: Menu.MouseUp(Button, x, y);
    gsMap: Map.MouseUp(Button, x, y);
    gsGame: Ground.MouseUp(Button, x, y);
  end;
end;

function TG2App.PosScreenToGround(const v: TG2Vec2): TG2Vec2;
begin
  Result := (v - Ground.Pos) / Ground.Scale;
end;

procedure TG2App.DrawRing(
  const Pos: TG2Vec2; const Radius0, Radius1: Single; const Tex: TG2Texture2D; const Color: TG2Color;
  const BlendMode: TG2BlendMode = bmNormal; const TexSpan: Single = 1; const Segments: Integer = 32;
  const Angle: Single = 0
);
  var i, n: Integer;
  var Ang, tc, tc0, tc1: Single;
  var Pos0, Pos1, Pos2, Pos3: TG2Vec2;
begin
  g2.Gfx.Poly2D.PolyBegin(ptTriangles, Tex, BlendMode, tfLinear);
  Ang := TwoPi / Segments;
  tc := TexSpan / Segments;
  G2SinCos(Angle, Pos1.y, Pos1.x);
  Pos0 := Pos + Pos1 * Radius0;
  Pos1 := Pos + Pos1 * Radius1;
  tc0 := 0;
  for i := 1 to Segments do
  begin
    G2SinCos(Angle + Ang * i, Pos2.y, Pos2.x);
    Pos3 := Pos + Pos2 * Radius1;
    Pos2 := Pos + Pos2 * Radius0;
    tc1 := tc0 + tc;
    g2.Gfx.Poly2D.PolyAdd(Pos0.x, Pos0.y, tc0, 1, Color);
    g2.Gfx.Poly2D.PolyAdd(Pos1.x, Pos1.y, tc0, 0, Color);
    g2.Gfx.Poly2D.PolyAdd(Pos2.x, Pos2.y, tc1, 1, Color);
    g2.Gfx.Poly2D.PolyAdd(Pos2.x, Pos2.y, tc1, 1, Color);
    g2.Gfx.Poly2D.PolyAdd(Pos1.x, Pos1.y, tc0, 0, Color);
    g2.Gfx.Poly2D.PolyAdd(Pos3.x, Pos3.y, tc1, 0, Color);
    Pos0 := Pos2;
    Pos1 := Pos3;
    tc0 := tc1;
  end;
  g2.Gfx.Poly2D.PolyEnd;
end;

procedure TG2App.DrawHealth(const Pos: TG2Vec2; const Radius0, Radius1, Amount: Single; const Color: TG2Color);
  var i, n, Segments: Integer;
  var Ang: Single;
  var Pos0, Pos1, Pos2, Pos3: TG2Vec2;
begin
  DrawRing(Pos, Radius0 - 2, Radius1 + 2, TexHealth, $88888888, bmNormal);
  g2.Gfx.Poly2D.PolyBegin(ptTriangles, TexHealth, bmNormal, tfLinear);
  Segments := Round(64 * Amount);
  if Segments < 1 then Segments := 1;
  Ang := TwoPi * Amount / Segments;
  G2SinCos(-HalfPi, Pos1.y, Pos1.x);
  Pos0 := Pos + Pos1 * Radius0;
  Pos1 := Pos + Pos1 * Radius1;
  for i := 1 to Segments do
  begin
    G2SinCos(-HalfPi + Ang * i, Pos2.y, Pos2.x);
    Pos3 := Pos + Pos2 * Radius1;
    Pos2 := Pos + Pos2 * Radius0;
    g2.Gfx.Poly2D.PolyAdd(Pos0.x, Pos0.y, 0.5, 1, Color);
    g2.Gfx.Poly2D.PolyAdd(Pos1.x, Pos1.y, 0.5, 0, Color);
    g2.Gfx.Poly2D.PolyAdd(Pos2.x, Pos2.y, 0.5, 1, Color);
    g2.Gfx.Poly2D.PolyAdd(Pos2.x, Pos2.y, 0.5, 1, Color);
    g2.Gfx.Poly2D.PolyAdd(Pos1.x, Pos1.y, 0.5, 0, Color);
    g2.Gfx.Poly2D.PolyAdd(Pos3.x, Pos3.y, 0.5, 0, Color);
    Pos0 := Pos2;
    Pos1 := Pos3;
  end;
  g2.Gfx.Poly2D.PolyEnd;
end;

procedure TG2App.DrawArrow(const Pos0, Pos1: TG2Vec2; const Color: TG2Color; const Size: Single = 32);
  const tc = 1 / 32;
  var v, n, v0, v1, v2, v3, v4, v5: TG2Vec2;
  var hs: Single;
begin
  if (Pos0 - Pos1).LenSq < 1 then Exit;
  hs := Size * 0.5;
  v := (Pos1 - Pos0).Norm;
  n := v.Perp;
  v0 := Pos1 + v * hs;
  v1 := v0 + n * hs; v0 := v0 - n * hs;
  v2 := Pos1 - n * hs; v3 := Pos1 + n * hs;
  v4 := Pos0 - n * hs; v5 := Pos0 + n * hs;
  g2.PicQuad(
    v0, v1, v2, v3,
    G2Vec2(0, tc), G2Vec2(1, tc), G2Vec2(0, 1 - tc), G2Vec2(1, 1 - tc),
    Color, TexArrow, bmNormal, tfLinear
  );
  g2.PicQuad(
    v2, v3, v4, v5,
    G2Vec2(0, 1 - tc), G2Vec2(1, 1 - tc), G2Vec2(0, 1 - tc), G2Vec2(1, 1 - tc),
    Color, TexArrow, bmNormal, tfLinear
  );
end;

procedure TG2App.RenderBox(const r: TG2Rect; const bs: Single; const Color: TG2Color; const Tex: TG2Texture2D; const BlendMode: TG2BlendMode = bmNormal);
begin
  RenderBox(r.x, r.y, r.w, r.h, bs, Color, Tex, BlendMode);
end;

procedure TG2App.RenderBox(const x, y, w, h, bs: Single; const Color: TG2Color; const Tex: TG2Texture2D; const BlendMode: TG2BlendMode = bmNormal);
  var x0, x1, x2, y0, y1, y2, bw, bh: Single;
begin
  bw := w - bs * 2;
  bh := h - bs * 2;
  x0 := x; x1 := x + bs; x2 := x1 + bw;
  y0 := y; y1 := y + bs; y2 := y1 + bh;
  g2.PicRect(x0, y0, bs, bs, 0, 0, 0.5, 0.5, Color, Tex, BlendMode, tfLinear);
  g2.PicRect(x1, y0, bw, bs, 0.5, 0, 0.5, 0.5, Color, Tex, BlendMode, tfLinear);
  g2.PicRect(x2, y0, bs, bs, 0.5, 0, 1, 0.5, Color, Tex, BlendMode, tfLinear);
  g2.PicRect(x0, y1, bs, bh, 0, 0.5, 0.5, 0.5, Color, Tex, BlendMode, tfLinear);
  g2.PicRect(x2, y1, bs, bh, 0.5, 0.5, 1, 0.5, Color, Tex, BlendMode, tfLinear);
  g2.PicRect(x0, y2, bs, bs, 0, 0.5, 0.5, 1, Color, Tex, BlendMode, tfLinear);
  g2.PicRect(x1, y2, bw, bs, 0.5, 0.5, 0.5, 1, Color, Tex, BlendMode, tfLinear);
  g2.PicRect(x2, y2, bs, bs, 0.5, 0.5, 1, 1, Color, Tex, BlendMode, tfLinear);
  g2.PicRect(x1, y1, bw, bh, 0.5, 0.5, 0.5, 0.5, Color, Tex, BlendMode, tfLinear);
end;

procedure TG2App.RenderButton(const r: TG2Rect; const bs: Single; const Caption: AnsiString; const Tex0, Tex1, Tex2: TG2Texture2D);
begin
  RenderButton(r.x, r.y, r.w, r.h, bs, Caption, Tex0, Tex1, Tex2);
end;

procedure TG2App.RenderButton(const x, y, w, h, bs: Single; const Caption: AnsiString; const Tex0, Tex1, Tex2: TG2Texture2D);
  var R: TRect;
  var TextRect: TG2Rect;
  var Tex: TG2Texture2D;
  var TextPress: Single;
begin
  R.Left := Round(x);
  R.Top := Round(y);
  R.Right := Round(x + w);
  R.Bottom := Round(y + h);
  TextPress := 0;
  if PtInRect(R, g2.MousePos) then
  begin
    if g2.MouseDown[G2MB_Left]
    and PtInRect(R, g2.MouseDownPos[G2MB_Left]) then
    begin
      Tex := Tex2;
      TextPress := App.gs;
    end
    else
    Tex := Tex1;
  end
  else
  Tex := Tex0;
  RenderBox(x, y, w, h, bs, $ffffffff, Tex);
  if Length(Caption) > 0 then
  begin
    TextRect.l := x + 2 * App.gs;
    TextRect.t := y + 2 * App.gs + TextPress;
    TextRect.r := x + w - 2 * App.gs;
    TextRect.b := y + h - 2 * App.gs + TextPress;
    TextFitRect(App.Font1, TextRect, $ff000000, Caption);
  end;
end;

procedure TG2App.TextFitRect(const TextFont: TG2Font; const R: TG2Rect; const Color: TG2Color; const Text: AnsiString);
  var tw, th, sw, sh, t: Single;
begin
  tw := TextFont.TextWidth(Text); th := TextFont.TextHeight(Text);
  sw := R.w / tw; sh := R.h / th; if sw < sh then t := sw else t := sh;
  tw := tw * t; th := th * t;
  TextFont.Print(
    R.x + (R.w - tw) * 0.5,
    R.y + (R.h - th) * 0.5,
    t, t, Color, Text,
    bmNormal, tfLinear
  );
end;
//TG2App END

end.
