Files
kam-mobile/vendor/kam_remake/src/ai/newAI/KM_ArmyAttack.pas
T

1784 lines
58 KiB
ObjectPascal
Raw Normal View History

2026-09-12 16:50:30 +05:00
{
Artificial intelligence
@author: Martin Toupal
@e-mail: poznamenany@gmail.com
}
unit KM_ArmyAttack;
{$I KaM_Remake.inc}
interface
uses
Classes, KromUtils, Math, SysUtils,
KM_Units, KM_UnitGroup,
KM_CommonClasses, KM_CommonTypes, KM_Defaults, KM_Points,
KM_Houses, KM_ResHouses, KM_ResWares, KM_NavMeshPathFinding,
KM_Eye;
type
TAISquad = class; // Combat group (orders to specific group: walk / attack)
TAICompany = class; // Company (selection of targets, positions etc.)
TKMArmyAttack = class; // Attack (time distribution, company initialization)
TKMSquadList = array[GROUP_TYPE_MIN..GROUP_TYPE_MAX] of TKMList;
TKMSquadsArray = array[GROUP_TYPE_MIN..GROUP_TYPE_MAX] of record
Count: Word;
Squads: array of TAISquad;
TargetUnit: TKMUnitArray;
TargetHouse: TKMHouseArray;
end;
TKMTargetSelection = array of record
Index: Word;
CenterPoint: TKMPoint;
CloseThreat, DistantThreat: Single;
end;
TKMCompanyMode = (cmAttack, cmDefence, cmDestruction);
TKMCompanyState = (csAttack, csWalking, csIdle);
TAISquad = class
private
fGroup: TKMUnitGroup;
fOnPlace, fTargetChanged: Boolean;
fFinalPosition: TKMPointDir;
fTargetHouse: TKMHouse;
fTargetUnit: TKMUnit;
fWalkTimeLimit, fAttackTimeLimit: Cardinal;
function SquadInFight(): Boolean; inline;
function GetGroupPosition(): TKMPoint; inline;
function PlanPath(aTick: Cardinal; var aActualPosition, aTargetPosition: TKMPoint; aOrderAttack: Boolean = False; aOrderDestroy: Boolean = False): Boolean;
procedure SetTargetHouse(aHouse: TKMHouse);
procedure SetTargetUnit(aUnit: TKMUnit);
procedure SetTargetPosition(aLoc: TKMPointDir);
public
{$IFDEF DEBUG_NewAI}
DEBUGPointPath: TKMPointArray;
{$ENDIF}
constructor Create(aGroup: TKMUnitGroup);
constructor Load(LoadStream: TKMemoryStream);
destructor Destroy; override;
procedure Save(SaveStream: TKMemoryStream);
procedure SyncLoad();
property Group: TKMUnitGroup read fGroup;
property OnPlace: Boolean read fOnPlace;
property InFight: Boolean read SquadInFight;
property FinalPosition: TKMPointDir read fFinalPosition write SetTargetPosition;
property Position: TKMPoint read GetGroupPosition;
property WalkTimeLimit: Cardinal read fWalkTimeLimit write fWalkTimeLimit;
property AttackTimeLimit: Cardinal read fAttackTimeLimit write fAttackTimeLimit;
property TargetHouse: TKMHouse read fTargetHouse write SetTargetHouse;
property TargetUnit: TKMUnit read fTargetUnit write SetTargetUnit;
procedure UpdateState(aTick: Cardinal);
end;
TAICompany = class
private
fOwner: TKMHandID;
fPathPosition: TKMPoint;
fScanPosition: TKMPoint;
fCompanyMode: TKMCompanyMode;
fTargetOwner: TKMHandID;
fTargetPoint: TKMPoint;
fTargetHouse: TKMHouse;
fTargetUnit: TKMUnit;
fState: TKMCompanyState;
fSquads: TKMSquadList;
function GetPosition(): TKMPoint; overload;
function GetPosition(var aSQRRadius: Single): TKMPoint; overload;
function GetTargetPosition(): TKMPoint;
function OrderToAttack(aActualPosition: TKMPoint; UA: TKMUnitArray; UGA: TKMUnitGroupArray; HA: TKMHouseArray): Boolean;
function OrderMove(aTick: Cardinal; aActualPosition: TKMPoint): Boolean;
public
{$IFDEF DEBUG_NewAI}
DEBUGScanRad: Single;
DEBUGTargetU: TKMTargetSelection;
DEBUGPointPath, DEBUG_UA_POINTS, DEBUG_UGA_POINTS: TKMPointArray;
{$ENDIF}
constructor Create(aOwner: TKMHandID; aCompanyMode: TKMCompanyMode);
constructor Load(LoadStream: TKMemoryStream);
destructor Destroy(); override;
procedure Save(SaveStream: TKMemoryStream);
procedure SyncLoad();
property Owner: TKMHandID read fOwner write fOwner;
property PathPosition: TKMPoint read fPathPosition;
property ScanPosition: TKMPoint read fScanPosition;
property CompanyMode: TKMCompanyMode read fCompanyMode write fCompanyMode;
property TargetPoint: TKMPoint read fTargetPoint write fTargetPoint;
property TargetPosition: TKMPoint read GetTargetPosition;
property State: TKMCompanyState read fState;
property Squads: TKMSquadList read fSquads;
procedure InitCompany();
procedure UpdateState(aTick: Cardinal);
procedure AddSquad(aGroup: TKMUnitGroup);
procedure DeleteSquad(aGT: TKMGroupType; aIdx: Integer);
function SquadCnt(aTypes: TKMGroupTypeSet = GROUP_TYPES_VALID): Word;
function WarriorCnt(aTypes: TKMGroupTypeSet = GROUP_TYPES_VALID): Word;
function IsGroupInCompany(aGroup: TKMUnitGroup): Boolean;
function SetTarget(aHouse: TKMHouse; aUnit: TKMUnit = nil): Boolean;
function ActualizeTarget(aInitPosition: TKMPoint; var aTargetHouse: TKMHouse; var aTargetUnit: TKMUnit; aAimCivilians: Boolean = True): Boolean;
end;
TKMArmyAttack = class
private
fOwner: TKMHandID;
fCompanies: TKMList;
function GetCount(): Integer;
function GetCompany(aIdx: Integer): TAICompany;
public
constructor Create(aOwner: TKMHandID);
destructor Destroy(); override;
procedure Save(SaveStream: TKMemoryStream);
procedure Load(LoadStream: TKMemoryStream);
procedure SyncLoad();
property Count: Integer read GetCount;
property Company[aIdx: Integer]: TAICompany read GetCompany; default;
procedure AfterMissionInit();
procedure UpdateState(aTick: Cardinal);
procedure OwnerUpdate(aPlayer: TKMHandID);
procedure CreateCompany(aTargetPoint: TKMPoint; aGroups: TKMUnitGroupArray; aCompanyMode: TKMCompanyMode = cmAttack);
function IsGroupInAction(aGroup: TKMUnitGroup): Boolean;
procedure LogStatus(var aBalanceText: UnicodeString);
procedure Paint();
end;
const
// Houses in TARGET_HOUSES will be selected as a primary target (so the company will come to this point but it will not attack it because of this list)
TARGET_HOUSES: TKMHouseTypeSet = [htBarracks, htStore, htSchool, htTownhall];
// Houses in SCAN_HOUSES will be destroyed when they are in radius (it should also contain TARGET_HOUSES)
SCAN_HOUSES: TKMHouseTypeSet = [htWatchTower, htBarracks, htStore, htSchool, htTownhall];
// All houses for final stage of attack algorithm
ALL_HOUSES: TKMHouseTypeSet = HOUSES_VALID;
implementation
uses
Types,
KM_HandsCollection, KM_Terrain, KM_AIFields,
KM_NavMesh, KM_RenderAux,
KM_UnitWarrior, KM_AIParameters;
{ TAISquad }
constructor TAISquad.Create(aGroup: TKMUnitGroup);
begin
inherited Create;
fGroup := aGroup.GetPointer();
fOnPlace := True;
fTargetChanged := True;
fWalkTimeLimit := 0;
fAttackTimeLimit := 0;
fTargetHouse := nil;
fTargetUnit := nil;
//fFinalPosition := KMPointDir(KMPOINT_ZERO, dirNA);
end;
destructor TAISquad.Destroy();
begin
gHands.CleanUpGroupPointer(fGroup);
gHands.CleanUpUnitPointer(fTargetUnit);
gHands.CleanUpHousePointer(fTargetHouse);
inherited;
end;
constructor TAISquad.Load(LoadStream: TKMemoryStream);
begin
inherited Create;
LoadStream.CheckMarker('Squad');
LoadStream.Read(fOnPlace);
LoadStream.Read(fTargetChanged);
LoadStream.Read(fFinalPosition);
LoadStream.Read(fWalkTimeLimit, SizeOf(fWalkTimeLimit));
LoadStream.Read(fAttackTimeLimit, SizeOf(fAttackTimeLimit));
//Subst on syncload
LoadStream.Read(fGroup, 4);
LoadStream.Read(fTargetUnit, 4);
LoadStream.Read(fTargetHouse, 4);
end;
procedure TAISquad.Save(SaveStream: TKMemoryStream);
begin
SaveStream.PlaceMarker('Squad');
SaveStream.Write(fOnPlace);
SaveStream.Write(fTargetChanged);
SaveStream.Write(fFinalPosition);
SaveStream.Write(fWalkTimeLimit, SizeOf(fWalkTimeLimit));
SaveStream.Write(fAttackTimeLimit, SizeOf(fAttackTimeLimit));
if (fGroup <> nil) then
SaveStream.Write(fGroup.UID) //Store ID
else
SaveStream.Write(Integer(0));
if (fTargetUnit <> nil) then
SaveStream.Write(fTargetUnit.UID) //Store ID
else
SaveStream.Write(Integer(0));
if (fTargetHouse <> nil) then
SaveStream.Write(fTargetHouse.UID) //Store ID
else
SaveStream.Write(Integer(0));
end;
procedure TAISquad.SyncLoad();
begin
fGroup := gHands.GetGroupByUID(Integer(fGroup) );
fTargetUnit := gHands.GetUnitByUID(Integer(fTargetUnit) );
fTargetHouse := gHands.GetHouseByUID(Integer(fTargetHouse) );
end;
procedure TAISquad.SetTargetUnit(aUnit: TKMUnit);
begin
if (fTargetUnit = aUnit) then
Exit;
// Archers will reaim (+ reset animation) only in case that new target is in specific distance from existing target
if (aUnit <> nil) AND not aUnit.IsDeadOrDying then
begin
if (fGroup.GroupType = gtRanged)
AND (fTargetUnit <> nil)
//AND not fTargetUnit.IsDeadOrDying
AND (
(
(fTargetUnit is TKMUnitWarrior)
AND (aUnit is TKMUnitWarrior)
AND (TKMUnitWarrior(fTargetUnit).Group = TKMUnitWarrior(aUnit).Group)
) OR (
(KMDistanceSqr(fTargetUnit.CurrPosition, aUnit.CurrPosition) < sqr(AI_Par[ATTACK_SQUAD_ChangeTarget_DistTolerance]))
)
) then
Exit;
fTargetChanged := True;
gHands.CleanUpUnitPointer(fTargetUnit);
fTargetUnit := aUnit.GetPointer;
end
else
gHands.CleanUpUnitPointer(fTargetUnit); // aUnit = nil case
end;
procedure TAISquad.SetTargetPosition(aLoc: TKMPointDir);
begin
fFinalPosition := aLoc;
end;
procedure TAISquad.SetTargetHouse(aHouse: TKMHouse);
begin
if (aHouse <> nil) then
SetTargetUnit(nil); // House have lower priority so if there is request for attacking house unit must be nil
if (fTargetHouse = aHouse) then
Exit;
fTargetChanged := True;
gHands.CleanUpHousePointer(fTargetHouse);
if (aHouse <> nil) then
fTargetHouse := aHouse.GetPointer;
end;
function TAISquad.SquadInFight(): Boolean;
begin
Result := not fGroup.CanTakeOrders;
end;
function TAISquad.GetGroupPosition(): TKMPoint;
begin
Result := fGroup.Position;
end;
// Update state of squad (group orders)
procedure TAISquad.UpdateState(aTick: Cardinal);
function GetTargetPosition(): TKMPoint;
var
K: Integer;
Dist, BestDist: Single;
BestTgt: TKMUnit;
G: TKMUnitGroup;
begin
Result := fTargetUnit.CurrPosition;
// Get closest warrior in enemy group if the squad is ranged
if (fGroup.GroupType = gtRanged) AND (fTargetUnit is TKMUnitWarrior) then
begin
BestTgt := nil;
G := TKMUnitWarrior(fTargetUnit).Group;
//Group will be nil if we target a warrior still walking out of barracks
if G <> nil then
begin
BestDist := 1E10;
for K := 0 to G.Count - 1 do
if not G.Members[K].IsDeadOrDying then
begin
Dist := KMDistanceSqr(fGroup.Position,G.Members[K].CurrPosition);
if (Dist < BestDist) then
begin
BestTgt := G.Members[K];
BestDist := Dist;
end;
end;
if (BestTgt <> nil) then
begin
gHands.CleanUpUnitPointer(fTargetUnit);
fTargetUnit := BestTgt.GetPointer;
end;
end;
end;
end;
var
ActPos, FinPos: TKMPoint;
begin
fOnPlace := False;
// Check group status and possibility to give order
if (fGroup = nil) OR fGroup.IsDead OR InFight then
Exit;
// Check targets
if (fTargetHouse <> nil) AND (fTargetHouse.IsDestroyed) then
gHands.CleanUpHousePointer(fTargetHouse);
if (fTargetUnit <> nil) AND (fTargetUnit.IsDead) then
gHands.CleanUpUnitPointer(fTargetUnit);
// Do order
ActPos := fGroup.Position;
if (fTargetUnit <> nil) then
begin
FinPos := GetTargetPosition();
if PlanPath(aTick, ActPos, FinPos, True, False) then // Check if squad is in the place
Group.OrderWalk(FinPos, True, wtokAISquad, FinalPosition.Dir)
else if
fTargetChanged // Target has been changed
OR (fGroup.GroupType <> gtRanged) // If not ranged then send a command to attack every update
OR (KMDistanceSqr(FinPos,ActPos) > 12*12) // If ranged send attack command only until shoot distance to prevent reset of the shooting animation
OR (fGroup.Order in [goNone, goWalkTo]) // If ranged send attack command also if target is already in range but previous command was not attack something
OR (fAttackTimeLimit < aTick) // If ranged send attack command every X ticks to force at least first line of archers to shoot
then
begin
fAttackTimeLimit := aTick + Round(AI_Par[ATTACK_SQUAD_ChangeTarget_Delay]);
fTargetChanged := False;
Group.OrderAttackUnit(fTargetUnit, True);
end;
fOnPlace := True;
end
else if (fTargetHouse <> nil) then
begin
FinPos := fTargetHouse.Position;
if PlanPath(aTick, ActPos, FinPos, False, True) then
Group.OrderWalk(FinPos, True, wtokAISquad, FinalPosition.Dir)
else if (fGroup.GroupType <> gtRanged) OR fTargetChanged OR (fAttackTimeLimit < aTick) then
begin
fAttackTimeLimit := aTick + Round(AI_Par[ATTACK_SQUAD_ChangeTarget_Delay]);
fTargetChanged := False;
Group.OrderAttackHouse(fTargetHouse, True);
end;
fOnPlace := True;
end
else
begin
FinPos := FinalPosition.Loc;
if PlanPath(aTick, ActPos, FinPos, False, False) then
Group.OrderWalk(FinPos, True, wtokAISquad, FinalPosition.Dir)
else if not KMSamePoint(Group.Position, FinalPosition.Loc) then // Dont repeat order and let archers fire
Group.OrderWalk(FinalPosition.Loc, True, wtokAISquad, FinalPosition.Dir);
end;
end;
function TAISquad.PlanPath(aTick: Cardinal; var aActualPosition, aTargetPosition: TKMPoint; aOrderAttack: Boolean = False; aOrderDestroy: Boolean = False): Boolean;
var
InitPolygon, ClosestPolygon, Distance: Word;
I: Integer;
SQRDist: Single;
PointPath: TKMPointArray;
begin
Result := False;
fOnPlace := False;
SQRDist := KMDistanceSqr(aActualPosition, aTargetPosition);
// Time limit (time limit MUST be always set by higher rank (platoon))
if (not (aOrderAttack OR aOrderDestroy) AND (fWalkTimeLimit < aTick)) // Time limit is set to 0 in case that unit attack something
// Target position is reached
OR (SQRDist < sqr(AI_Par[ATTACK_SQUAD_TargetReached_Position]))
// Target unit is close
OR (aOrderAttack AND (SQRDist < sqr(AI_Par[ATTACK_SQUAD_TargetReached_Unit])))
// Target house is close
OR (aOrderDestroy AND (SQRDist < sqr(AI_Par[ATTACK_SQUAD_TargetReached_House])))
// Archers should start fire as soon as possible
OR ((aOrderAttack OR aOrderDestroy) AND (fGroup.GroupType = gtRanged) AND (SQRDist < sqr(AI_Par[ATTACK_SQUAD_TargetReached_RangedSquad]))) then
begin
fOnPlace := True;
Exit;
end;
// Plan path with respect to enemy presence
if gAIFields.NavMesh.Pathfinding.AvoidEnemyRoute(Group.Owner, Group.GroupType, aActualPosition, aTargetPosition, Distance, PointPath) then
begin
if (Distance < 5) then // Just to be sure (default value of pathfinding, if is group stuck it should not influence platoon)
begin
fOnPlace := True;
Exit;
end
else
begin
InitPolygon := gAIFields.NavMesh.KMPoint2Polygon[aActualPosition];
I := Length(PointPath)-2; // Skip next polygon -> fluent movement
repeat
aTargetPosition := PointPath[ Max(0, I) ];
ClosestPolygon := gAIFields.NavMesh.KMPoint2Polygon[ aTargetPosition ];
I := I - 1;
until (I < 0) OR ( (InitPolygon <> ClosestPolygon)
AND (tpWalk in gTerrain.Land^[aTargetPosition.Y, aTargetPosition.X].Passability)
AND (aOrderAttack OR (KMDistanceSqr(aActualPosition, aTargetPosition) > sqr(AI_Par[ATTACK_SQUAD_MinWalkingDistance]))) );
{$IFDEF DEBUG_NewAI}
DEBUGPointPath := PointPath;
{$ENDIF}
end;
end;
Result := True;
end;
{ TAICompany }
constructor TAICompany.Create(aOwner: TKMHandID; aCompanyMode: TKMCompanyMode);
var
GT: TKMGroupType;
begin
inherited Create;
fOwner := aOwner;
fCompanyMode := aCompanyMode;
fTargetPoint := KMPOINT_ZERO;
fTargetOwner := -1;
fTargetHouse := nil;
fTargetUnit := nil;
for GT := GROUP_TYPE_MIN to GROUP_TYPE_MAX do
fSquads[GT] := TKMList.Create();
end;
destructor TAICompany.Destroy();
var
GT: TKMGroupType;
begin
gHands.CleanUpUnitPointer(fTargetUnit);
gHands.CleanUpHousePointer(fTargetHouse);
for GT := GROUP_TYPE_MIN to GROUP_TYPE_MAX do
fSquads[GT].Free;
inherited;
end;
constructor TAICompany.Load(LoadStream: TKMemoryStream);
var
I,Cnt: Integer;
GT: TKMGroupType;
begin
inherited Create;
LoadStream.CheckMarker('Company');
LoadStream.Read(fOwner);
LoadStream.Read(fPathPosition);
LoadStream.Read(fScanPosition);
LoadStream.Read(fCompanyMode, SizeOf(TKMCompanyMode));
LoadStream.Read(fTargetPoint);
LoadStream.Read(fTargetOwner);
LoadStream.Read(fTargetUnit, 4);
LoadStream.Read(fTargetHouse, 4);
for GT := GROUP_TYPE_MIN to GROUP_TYPE_MAX do
begin
fSquads[GT] := TKMList.Create();
LoadStream.Read(Cnt);
for I := 0 to Cnt - 1 do
fSquads[GT].Add( TAISquad.Load(LoadStream) );
end;
end;
procedure TAICompany.Save(SaveStream: TKMemoryStream);
var
I,Cnt: Integer;
GT: TKMGroupType;
begin
SaveStream.PlaceMarker('Company');
SaveStream.Write(fOwner);
SaveStream.Write(fPathPosition);
SaveStream.Write(fScanPosition);
SaveStream.Write(fCompanyMode, SizeOf(TKMCompanyMode));
SaveStream.Write(fTargetPoint);
SaveStream.Write(fTargetOwner);
if (fTargetUnit <> nil) then
SaveStream.Write(fTargetUnit.UID) //Store ID
else
SaveStream.Write(Integer(0));
if (fTargetHouse <> nil) then
SaveStream.Write(fTargetHouse.UID) //Store ID
else
SaveStream.Write(Integer(0));
for GT := GROUP_TYPE_MIN to GROUP_TYPE_MAX do
begin
Cnt := fSquads[GT].Count;
SaveStream.Write(Cnt);
for I := 0 to Cnt - 1 do
TAISquad( Squads[GT].Items[I] ).Save(SaveStream);
end;
end;
procedure TAICompany.SyncLoad();
var
I: Integer;
GT: TKMGroupType;
begin
fTargetUnit := gHands.GetUnitByUID(Integer(fTargetUnit) );
fTargetHouse := gHands.GetHouseByUID(Integer(fTargetHouse) );
for GT := GROUP_TYPE_MIN to GROUP_TYPE_MAX do
for I := 0 to fSquads[GT].Count - 1 do
TAISquad( Squads[GT].Items[I] ).SyncLoad();
end;
function TAICompany.SetTarget(aHouse: TKMHouse; aUnit: TKMUnit = nil): Boolean;
begin
Result := False;
if (aHouse = nil) AND (aUnit <> nil) then
begin
gHands.CleanUpUnitPointer(fTargetUnit);
fTargetUnit := aUnit.GetPointer;
fTargetOwner := aUnit.Owner;
Result := True;
end
else if (aHouse <> nil) AND (aUnit = nil) then
begin
gHands.CleanUpHousePointer(fTargetHouse);
fTargetHouse := aHouse.GetPointer;
fTargetOwner := aHouse.Owner;
Result := True;
end;
end;
function TAICompany.GetTargetPosition(): TKMPoint;
begin
Result := KMPOINT_ZERO;
if (fTargetHouse <> nil) AND not fTargetHouse.IsDestroyed then
Result := fTargetHouse.Position
else if (fTargetUnit <> nil) AND not fTargetUnit.IsDead then
Result := fTargetUnit.CurrPosition;
end;
// Update state of company, detect targets, call functions for orders
procedure TAICompany.UpdateState(aTick: Cardinal);
// Check if target still exist
function CheckPrimaryTarget(): Boolean;
begin
Result := False;
if (fTargetHouse <> nil) then
if fTargetHouse.IsDestroyed then
gHands.CleanUpHousePointer(fTargetHouse)
else
Result := True;
if (fTargetUnit <> nil) then
if fTargetUnit.IsDead then
gHands.CleanUpUnitPointer(fTargetUnit)
else
Result := True;
end;
// Check if are all squads in position and we can continue
function SquadsInPosition(): Boolean;
var
I: Integer;
GT: TKMGroupType;
Squad: TAISquad;
begin
Result := True;
for GT := GROUP_TYPE_MIN to GROUP_TYPE_MAX do
for I := fSquads[GT].Count - 1 downto 0 do
begin
Squad := fSquads[GT].Items[I];
if (Squad = nil) OR (Squad.Group = nil) OR Squad.Group.IsDead then
fSquads[GT].Delete(I)
else
Result := Result AND Squad.OnPlace;
end;
end;
// Filter target houses according to house type and influence
procedure FilterTargetHouses(var aHA: TKMHouseArray);
var
K, Cnt: Integer;
begin
if (fCompanyMode = cmDestruction) then
Exit;
Cnt := 0;
for K := Low(aHA) to High(aHA) do
if (aHA[K].HouseType in SCAN_HOUSES) OR
(gAIFields.Influences.OwnPoint[fOwner, aHA[K].Position] > 100) OR
(gAIFields.Influences.GetBestAllianceOwnership(fOwner, aHA[K].Position.X, aHA[K].Position.Y, atAlly) > 50) then
begin
aHA[Cnt] := aHA[K];
Cnt := Cnt + 1;
end;
SetLength(aHA,Cnt);
end;
// Update state of Squads
procedure UpdateSquadsState();
var
I: Integer;
GT: TKMGroupType;
begin
for GT := GROUP_TYPE_MIN to GROUP_TYPE_MAX do
for I := fSquads[GT].Count - 1 downto 0 do
TAISquad( fSquads[GT].Items[I] ).UpdateState(aTick);
end;
var
InPosition: Boolean;
{$IFDEF DEBUG_NewAI}
I: Integer;
{$ENDIF}
ScanRad, SQRRadius: Single;
HA: TKMHouseArray;// Target houses in radius
UA: TKMUnitArray; // Target closest soldiers in radius
UGA: TKMUnitGroupArray; // Group of closest soldiers in radius (reference to UA)
ClosestHouse: TKMHouse;
ClosestUnit: TKMUnit;
begin
if (SquadCnt = 0) then
Exit;
InPosition := SquadsInPosition();
// Check target
ClosestHouse := nil;
ClosestUnit := nil;
fScanPosition := GetPosition(SQRRadius);
ScanRad := sqr(AI_Par[ATTACK_COMPANY_AttackRadius]) + Min(50*50, SQRRadius);
HA := gHands.GetHousesInRadius(ScanPosition, sqr(AI_Par[ATTACK_COMPANY_AttackRadius]), fOwner, atEnemy, ALL_HOUSES, False);
UA := gHands.GetGroupsMemberInRadius(ScanPosition, ScanRad, fOwner, atEnemy, UGA);
FilterTargetHouses(HA);
{$IFDEF DEBUG_NewAI}
DEBUGScanRad := ScanRad;
SetLength(DEBUG_UA_POINTS, Length(UA));
SetLength(DEBUG_UGA_POINTS, Length(UA));
for I := 0 to Length(UA) - 1 do
begin
DEBUG_UA_POINTS[I] := UA[I].CurrPosition;
DEBUG_UGA_POINTS[I] := UGA[I].Position;
end;
{$ENDIF}
// Actualize target
if not CheckPrimaryTarget() then
begin
if (Length(UA) > 0) then
ClosestUnit := UA[0];
if (Length(HA) > 0) then
ClosestHouse := HA[0];
if not SetTarget(ClosestHouse, ClosestUnit) then
begin
if ActualizeTarget(fScanPosition, ClosestHouse, ClosestUnit, False) then
begin
if not gAIFields.Supervisor.FFA
OR ((ClosestHouse <> nil) AND (ClosestHouse.Owner = fTargetOwner))
OR ((ClosestUnit <> nil) AND (ClosestUnit.Owner = fTargetOwner)) then
SetTarget(ClosestHouse, ClosestUnit)
else
fState := csIdle;
end;
end;
end;
// Give new orders
if OrderToAttack(ScanPosition, UA, UGA, HA) then
fState := csAttack
else
begin
fState := csWalking;
if InPosition AND not OrderMove(aTick, PathPosition) then
fState := csIdle;
end;
UpdateSquadsState();
end;
function TAICompany.OrderToAttack(aActualPosition: TKMPoint; UA: TKMUnitArray; UGA: TKMUnitGroupArray; HA: TKMHouseArray): Boolean;
type
TCompanyInfo = record
GTCnt: array[GROUP_TYPE_MIN..GROUP_TYPE_MAX] of Word; // Count of soldiers sorted by GroupType
end;
var
AvailableSquads: TKMSquadsArray;
CompanyInfo: TCompanyInfo;
TargetU: TKMTargetSelection;
// Find only available squads (we can give orders to them) and return if there are any
function FindAvailableSquads(var aSquadsInCombat: Boolean): Boolean;
var
Output: Boolean;
I: Integer;
GT: TKMGroupType;
Squad: TAISquad;
begin
Output := False;
aSquadsInCombat := False;
FillChar(CompanyInfo, SizeOf(CompanyInfo), #0);
for GT := GROUP_TYPE_MIN to GROUP_TYPE_MAX do
begin
AvailableSquads[GT].Count := 0;
SetLength(AvailableSquads[GT].Squads, fSquads[GT].Count);
end;
for GT := GROUP_TYPE_MIN to GROUP_TYPE_MAX do
for I := 0 to fSquads[GT].Count - 1 do
begin
Squad := fSquads[GT].Items[I];
Inc(CompanyInfo.GTCnt[GT], Squad.Group.Count);
if not Squad.InFight then
begin
Output := True;
// Make sure that unit will not hunt target over the whole map and better stay inside company
if (GT <> gtRanged) then // ranged units are fixed in Squad class(set / reset target cause that they dont shoot and just reset animation)
begin
Squad.TargetUnit := nil;
//Squad.TargetHouse := nil; // This cause same problem like with archers
end;
AvailableSquads[GT].Squads[ AvailableSquads[GT].Count ] := Squad;
Inc(AvailableSquads[GT].Count);
end
else
aSquadsInCombat := True;
end;
Result := Output;
end;
// Calculate evaluation to enemy groups in specific radius
procedure EvalEnemyGroupsInRadius();
const
INIT_DIST = 1000000;
SQR_MAX_RANGE_INTEREST = 12*12;
SQR_RANGE_OF_PROJECTILES = 11*11; // Sqr( Max( Max(RANGE_BOWMAN_MAX,RANGE_ARBALETMAN_MAX),RANGE_SLINGSHOT_MAX ) );
var
Polygon: Word;
I,K,L: Integer;
SqrDist, SqrClosestDist, SqrClosestDistToRanged, CloseCombatProtection: Single;
GT: TKMGroupType;
Squad: TAISquad;
GroupsInFightArr: TKMUnitGroupArray;
begin
SetLength(TargetU, Length(UA));
for I := 0 to Length(TargetU) - 1 do
begin
// Get closest distance to Ranged groups and all groups
SqrClosestDist := INIT_DIST;
SqrClosestDistToRanged := INIT_DIST;
for GT := GROUP_TYPE_MIN to GROUP_TYPE_MAX do
for K := 0 to fSquads[GT].Count - 1 do
begin
Squad := fSquads[GT].Items[K];
L := 0;
while (L < Squad.Group.Count) do
begin
SqrDist := KMDistanceSqr(Squad.Group.Members[L].CurrPosition, UA[I].CurrPosition);
if (SqrDist < SqrClosestDist) then
SqrClosestDist := SqrDist;
if (GT = gtRanged) AND (SqrDist < SqrClosestDistToRanged) then
SqrClosestDistToRanged := SqrDist;
L := L + 3;
end;
end;
// Calculate threat level
with TargetU[I] do
begin
Index := I;
CenterPoint := UGA[I].Position;
CloseThreat := 0;
DistantThreat := 0;
if (SqrClosestDist < SQR_MAX_RANGE_INTEREST) then
begin
CloseThreat := UGA[I].Count; // Threat level for close combat units
DistantThreat := CloseThreat; // Threat level for ranged group type
if (UGA[I].GroupType = gtRanged) then
begin
// Calculate distant threat level (determine whether archers are shooting at our troops)
CloseThreat := CloseThreat * AI_Par[ATTACK_COMPANY_AttackRangedGain];
DistantThreat := DistantThreat * Byte(SQR_RANGE_OF_PROJECTILES > SqrClosestDist) * AI_Par[ATTACK_COMPANY_AttackRangedGain];
// Close threat level is computed with using influences
CloseCombatProtection := 0;
Polygon := gAIFields.NavMesh.KMPoint2Polygon[ UGA[I].Position ];
for GT := GROUP_TYPE_MIN to GROUP_TYPE_MAX do
if (GT <> gtRanged) then // Skip ranged units so AI have a tendency to walk through them if they are alone
CloseCombatProtection := CloseCombatProtection + gAIFields.Influences.EnemyGroupPresence[fOwner, Polygon, GT]; // Tune parameters
if (DistantThreat < CloseCombatProtection) then // Ranged units are well protected -> try shoot them
CloseThreat := Byte(SqrClosestDistToRanged <= SQR_RANGE_OF_PROJECTILES) // Lowest priority (=1) but dont ignore them (if we have no archers call infantry)
else // Ranged units are not protected -> attack with close combat units
DistantThreat := Byte(SqrClosestDist <= SQR_RANGE_OF_PROJECTILES); // Lowest priority but dont ignore them (if they are in range of archers shoot at them)
end
else
begin
// Close combat threat level (in case that group already fight agaist more soldiers there is 0 threat)
// In case that group kills ranged units threat must be increased
if UGA[I].InFightAgaistGroups(GroupsInFightArr) then
begin
for K := 0 to Length(GroupsInFightArr) - 1 do
if (GroupsInFightArr[K].GroupType = gtRanged) then
CloseThreat := CloseThreat + GroupsInFightArr[K].Count
else
CloseThreat := CloseThreat - GroupsInFightArr[K].Count;
end
// If group does not fight check how far is from ranged units
else
CloseThreat := CloseThreat + Max(0, (sqr(AI_Par[ATTACK_COMPANY_ProtectRangedRadius]) - SqrClosestDistToRanged) * AI_Par[ATTACK_COMPANY_ProtectRangedGain] );
end;
end;
end;
end;
end;
// Distribute available groups agaist enemies
function SelectTargetGroups(): Boolean;
const
INIT_THREAT = -10000000;
INIT_DISTANCE = 10000000;
BEST_TARGET: array[GROUP_TYPE_MIN..GROUP_TYPE_MAX] of array[0..3] of TKMGroupType = (
(gtMelee, gtRanged, gtMounted, gtAntiHorse), // against gtMelee
(gtMelee, gtRanged, gtAntiHorse, gtMounted), // against gtAntiHorse
(gtMounted, gtRanged, gtMelee, gtAntiHorse), // against gtRanged
(gtAntiHorse, gtRanged, gtMounted, gtMelee) // against gtMounted
);
var
Output: Boolean;
I,K,L,BestPrioIdx, TargetIdx, ThreatIdx, Cnt: Integer;
Threat, HighestThreat, Dist, BestDist, BestPrioCoeff: Single;
GT, BestGT: TKMGroupType;
Squad: TAISquad;
begin
Output := False;
if (Length(TargetU) <= 0) then
Exit;
// Ranged groups view: our ranged unit -> select target => each unit should fire
GT := gtRanged;
ThreatIdx := 0; // Only for compiler
for I := AvailableSquads[GT].Count - 1 downto 0 do
begin
Squad := AvailableSquads[GT].Squads[I];
HighestThreat := INIT_THREAT;
//BestDist := 0;
Dist := 0;
for K := 0 to Length(TargetU) - 1 do
if (TargetU[K].DistantThreat > 0) then
begin
Dist := KMDistanceSqr(TAISquad(fSquads[GT].Items[I]).Position, UA[ TargetU[K].Index ].CurrPosition);
Threat := TargetU[K].DistantThreat - Dist;
if (Threat > HighestThreat) then
begin
//BestDist := Dist;
HighestThreat := Threat;
ThreatIdx := K;
end;
end;
if (HighestThreat <> INIT_THREAT) then
begin
Output := True;
Squad.TargetUnit := UA[ TargetU[ThreatIdx].Index ];
if (Dist >= sqr(AI_Par[ATTACK_COMPANY_ProtectRangedAllInDist])) then // If is enemy too cloose aim him but dont decrease threat level so next part of code can call close combat support
TargetU[ThreatIdx].DistantThreat := TargetU[ThreatIdx].DistantThreat - Squad.Group.Count;
// Unit will be targeted by Ranged group -> if there was minimal priority for close combat then remove it
if (TargetU[ThreatIdx].CloseThreat = 1) then
TargetU[ThreatIdx].CloseThreat := 0;
Dec(AvailableSquads[GT].Count);
AvailableSquads[GT].Squads[I] := AvailableSquads[GT].Squads[ AvailableSquads[GT].Count ];
end
else
Squad.TargetUnit := nil;
end;
// Close combat groups view: enemy units -> select oponent => keep something in reserve
Cnt := Length(TargetU);
BestGT := gtMelee;
TargetIdx := 0; // For compiler
BestPrioIdx := 0; // For compiler
BestPrioCoeff := 1; // For compiler
repeat
// Find top threat
HighestThreat := INIT_THREAT;
for I := Cnt - 1 downto 0 do
begin
if (TargetU[I].CloseThreat <= 0) then
begin
Cnt := Cnt - 1;
TargetU[I] := TargetU[Cnt];
end
else if (TargetU[I].CloseThreat > HighestThreat) then
begin
HighestThreat := TargetU[I].CloseThreat;
ThreatIdx := I;
end;
end;
if (Cnt <= 0) then
break;
// Find best close combat opponent
BestDist := INIT_DISTANCE;
for K := 0 to 3 do
begin
GT := BEST_TARGET[ UGA[ TargetU[ThreatIdx].Index ].GroupType, K ];
for L := 0 to AvailableSquads[GT].Count - 1 do
begin
Dist := KMDistanceSqr(AvailableSquads[GT].Squads[L].Position, UA[ TargetU[ThreatIdx].Index ].CurrPosition) + K * AI_Par[ATTACK_COMPANY_GroupTypePenalization];
if (Dist < BestDist) then
begin
BestDist := Dist;
TargetIdx := L;
BestGT := GT;
BestPrioIdx := K;
end;
end;
end;
// Set target
if (BestDist < INIT_DISTANCE) then
begin
Output := True;
Squad := AvailableSquads[BestGT].Squads[TargetIdx];
Squad.TargetUnit := UA[ TargetU[ThreatIdx].Index ];
// Update threat level and call another squad if needed
case BestPrioIdx of
0: BestPrioCoeff := AI_Par[ATTACK_COMPANY_DecreaseThreat_Prio1];
1: BestPrioCoeff := AI_Par[ATTACK_COMPANY_DecreaseThreat_Prio2];
2: BestPrioCoeff := AI_Par[ATTACK_COMPANY_DecreaseThreat_Prio3];
3: BestPrioCoeff := AI_Par[ATTACK_COMPANY_DecreaseThreat_Prio4];
end;
TargetU[ThreatIdx].CloseThreat := TargetU[ThreatIdx].CloseThreat - Squad.Group.Count * BestPrioCoeff;
Dec(AvailableSquads[BestGT].Count);
AvailableSquads[BestGT].Squads[TargetIdx] := AvailableSquads[BestGT].Squads[ AvailableSquads[BestGT].Count ];
end;
until (Cnt <= 0) OR (BestDist >= INIT_DISTANCE);
Result := Output;
end;
function OrderAttackHouse(): Boolean;
const
INIT_THREAT = 1000000;
LIMIT_RANGED_DESTROY_ALL_HOUSES = 6; // Archers will start to shoot at other houses if there is not enough close combat units in company
CLOSE_COMBAT_DISTANCE_LIMIT = 12; // Order to attack house starts at this distance (close combat groups; watchtowers have exception)
MAX_SOLDIERS_VS_HOUSE = 12;
MAX_ARCHERS_VS_TOWER = 9;
ATTACK_WATCHTOWER_WITH_CLOSE_COMBAT_DIST = 5; // Attack watchtower with close combat units
MAX_CLOSE_COMBAT_VS_UNIT_DIST = 3; // Kill citizens in this radius around house (workers tries to repair house)
var
Output: Boolean;
TargetIdx: Word;
I,K: Integer;
Dist, BestDist: Single;
GT: TKMGroupType;
U: TKMUnit;
GroupAttackCnt: TKMWordArray;
begin
Output := False;
SetLength(GroupAttackCnt, Length(HA));
FillChar(GroupAttackCnt[0], SizeOf(GroupAttackCnt[0]) * Length(GroupAttackCnt), #0);
// Target watchtowers with archers
GT := gtRanged;
TargetIdx := 0; // Only for compiler
for I := AvailableSquads[GT].Count - 1 downto 0 do
begin
BestDist := INIT_THREAT;
for K := 0 to Length(HA) - 1 do
if (HA[K].HouseType = htWatchTower)
AND (GroupAttackCnt[K] < MAX_ARCHERS_VS_TOWER)
AND (HA[K].CheckWareIn(wtStone) > 1)
AND HA[K].HasOwner then // Ignore towers without stone and without recruit inside
begin
Dist := KMDistanceSqr(AvailableSquads[GT].Squads[I].Position, HA[K].Position);
if (Dist < BestDist) then
begin
BestDist := Dist;
TargetIdx := K;
end;
end;
if (BestDist <> INIT_THREAT) then
begin
Output := True;
Inc(GroupAttackCnt[TargetIdx], AvailableSquads[GT].Squads[I].Group.Count);
// Find and kill workers who want to repair house
U := gAIFields.Eye.GetClosestUnitAroundHouse(HA[TargetIdx].HouseType, HA[TargetIdx].Position, AvailableSquads[GT].Squads[I].Position);
if (U <> nil) then
AvailableSquads[GT].Squads[I].TargetUnit := U
else
AvailableSquads[GT].Squads[I].TargetHouse := HA[TargetIdx];
Dec(AvailableSquads[GT].Count);
AvailableSquads[GT].Squads[I] := AvailableSquads[GT].Squads[ AvailableSquads[GT].Count ];
end;
end;
// Target everything else with close combat units
for GT := GROUP_TYPE_MIN to GROUP_TYPE_MAX do
begin
if GT = gtRanged then // Ignore ranged units if we have enough close combat support (avoid friendly fire)
with CompanyInfo do
if (GTCnt[gtMelee] + GTCnt[gtAntiHorse] + GTCnt[gtMounted] > LIMIT_RANGED_DESTROY_ALL_HOUSES) then
continue;
for I := AvailableSquads[GT].Count - 1 downto 0 do
begin
BestDist := INIT_THREAT;
for K := 0 to Length(HA) - 1 do
if (
(HA[K].HouseType <> htWatchTower)
OR ( KMDistanceSqr(AvailableSquads[GT].Squads[I].Position, HA[K].Position) < sqr(ATTACK_WATCHTOWER_WITH_CLOSE_COMBAT_DIST))
)
AND (GroupAttackCnt[K] < MAX_SOLDIERS_VS_HOUSE) then
begin
Dist := KMDistanceSqr(AvailableSquads[GT].Squads[I].Position, HA[K].Position);
if (Dist < BestDist) AND (Dist <= sqr(CLOSE_COMBAT_DISTANCE_LIMIT)) then
begin
BestDist := Dist;
TargetIdx := K;
end;
end;
if (BestDist <> INIT_THREAT) then
begin
Output := True;
GroupAttackCnt[TargetIdx] := GroupAttackCnt[TargetIdx] + AvailableSquads[GT].Squads[I].Group.Count;
// Find and kill workers who want to repair house
U := gAIFields.Eye.GetClosestUnitAroundHouse(HA[TargetIdx].HouseType, HA[TargetIdx].Position, AvailableSquads[GT].Squads[I].Position);
if (U <> nil)
AND ((GT = gtRanged) OR (KMDistanceSqr(U.CurrPosition, AvailableSquads[GT].Squads[I].Position) < sqr(MAX_CLOSE_COMBAT_VS_UNIT_DIST))) then
AvailableSquads[GT].Squads[I].TargetUnit := U
else
AvailableSquads[GT].Squads[I].TargetHouse := HA[TargetIdx];
Dec(AvailableSquads[GT].Count);
AvailableSquads[GT].Squads[I] := AvailableSquads[GT].Squads[ AvailableSquads[GT].Count ];
end;
end;
end;
Result := Output;
end;
procedure Regroup();
var
Cnt: Word;
I: Integer;
GT: TKMGroupType;
Squad: TAISquad;
Positions: TKMPointArray;
InitPolygons: TKMWordArray;
begin
Cnt := 0;
for GT := GROUP_TYPE_MIN to GROUP_TYPE_MAX do
Cnt := Cnt + AvailableSquads[GT].Count;
if (Cnt = 0) then
Exit;
// Get positions around center of scan area
SetLength(InitPolygons,1);
InitPolygons[0] := gAIFields.NavMesh.KMPoint2Polygon[ fScanPosition ];
gAIFields.NavMesh.Positioning.FindPositions(Cnt, Round(AI_Par[ATTACK_COMPANY_MinCombatSpacing]), InitPolygons, Positions);
Cnt := 0;
for GT := GROUP_TYPE_MIN to GROUP_TYPE_MAX do
for I := 0 to AvailableSquads[GT].Count - 1 do
begin
Squad := AvailableSquads[GT].Squads[I];
Squad.FinalPosition := KMPointDir( Positions[Cnt], KMGetDirection( Squad.Position, Positions[Cnt] ) );
// Time limit does not make sence for unit in combat
//Squad.TimeLimit := gGameParams.GameTick + KMDistanceAbs(Position, Squads[ClosestIdx].Position) * TIME_PER_A_TILE;
Cnt := Cnt + 1;
if (Cnt >= Length(Positions)) then
Exit;
end;
end;
var
FreeSquads, SquadsInCombat, AttackGroup, AttackHouse: Boolean;
begin
// Find Available squads (groups who obey orders / are not in combat)
FreeSquads := FindAvailableSquads(SquadsInCombat);
AttackGroup := False;
AttackHouse := False;
// Attack groups in radius
if (Length(UA) > 0) AND FreeSquads then
begin
// Calculate threat level of each enemy group in radius
EvalEnemyGroupsInRadius();
// Attack hostile units in radius
AttackGroup := SelectTargetGroups();
end;
// Attack houses in radius
if (Length(HA) > 0) AND FreeSquads then
AttackHouse := OrderAttackHouse();
// Move troops in reserve into new center point
if AttackGroup OR AttackHouse then
Regroup();
Result := AttackGroup OR AttackHouse OR SquadsInCombat;
end;
function TAICompany.OrderMove(aTick: Cardinal; aActualPosition: TKMPoint): Boolean;
function GetInitPolygons(aCnt: Integer; var aPointPath: TKMPointArray): TKMWordArray;
var
InitPolygon: Word;
I, Idx: Integer;
InitPolygons: TKMWordArray;
begin
// Get initial point on the path (it must be in specific distance from actual position to secure smooth moving of the company)
I := Length(aPointPath)-1;
while (I >= 0) AND (KMDistanceAbs(aActualPosition, aPointPath[I]) < AI_Par[ATTACK_COMPANY_MinimumMovement]) do
I := I - 1;
// Make sure that platoon will not start in actual polygon but position will be moved forward
InitPolygon := gAIFields.NavMesh.KMPoint2Polygon[ aActualPosition ];
repeat
fPathPosition := aPointPath[ Max(0, I) ];
I := I - 1;
until (InitPolygon <> gAIFields.NavMesh.KMPoint2Polygon[ fPathPosition ]) OR (I < 0);
I := Max(0,I + 1); // I = 0 we are in polygon of our target
// Get several init polygons
SetLength(InitPolygons, Min(Length(aPointPath) - I, Max(1, Round(AI_Par[ATTACK_COMPANY_Positioning_InitPolyCnt]))) );
Idx := 0;
while (I >= 0) AND (Idx < Length(InitPolygons)) do
begin
InitPolygons[Idx] := gAIFields.NavMesh.KMPoint2Polygon[ aPointPath[I] ];
Idx := Idx + 1;
I := I - 2;
end;
if (Idx <> Length(InitPolygons)) then
SetLength(InitPolygons, Idx);
Result := InitPolygons;
end;
procedure SetOrders(aCnt: Integer; aTargetPosition: TKMPoint; var aPositions: TKMPointArray);
const
INFLUENCE_DANGER = 20;
INIT_DIST = 1000;
var
I, K, Dist, ClosestDist, ClosestIdx, SelectedTime: Integer;
Dir: TKMDirection;
Position: TKMPoint;
GT: TKMGroupType;
Squads: array of TAISquad;
AvailableSquads: TBooleanArray;
TagPositions: TKMPointTagList;
begin
// Get influence and set speed of move
SelectedTime := Round(AI_Par[ATTACK_COMPANY_TimePerATile_Slow]);
if (gAIFields.Influences.GetBestAllianceOwnership(fOwner, gAIFields.NavMesh.KMPoint2Polygon[aActualPosition], atEnemy) < INFLUENCE_DANGER) then
SelectedTime := Round(AI_Par[ATTACK_COMPANY_TimePerATile_Fast]);
// Get formations and set orders
// Sort points according to aTargetPosition (it should work better than aActualPosition)
TagPositions := TKMPointTagList.Create;
try
for I := 0 to Min(aCnt, High(aPositions)) do
TagPositions.Add(aPositions[I], KMDistanceAbs(aTargetPosition, aPositions[I]));
TagPositions.SortByTag();
//fScanPosition := TagPositions.Items[TagPositions.Count - 1];
//fPathPosition := TagPositions.Items[(TagPositions.Count - 1) div 2];
Dir := KMGetDirection( aActualPosition, fPathPosition );
SetLength(Squads, aCnt);
SetLength(AvailableSquads, aCnt);
K := 0;
for GT := GROUP_TYPE_MIN to GROUP_TYPE_MAX do
for I := 0 to fSquads[GT].Count - 1 do
begin
Squads[K] := fSquads[GT].Items[I];
AvailableSquads[K] := True;
K := K + 1;
end;
ClosestIdx := 0; // Only for compiler
for I := TagPositions.Count - 1 downto 0 do
begin
Position := TagPositions.Items[I];
ClosestDist := INIT_DIST;
for K := 0 to Length(Squads) - 1 do
if AvailableSquads[K] then
begin
Dist := KMDistanceAbs(Position, Squads[K].Position);
if (Dist < ClosestDist) then
begin
ClosestDist := Dist;
ClosestIdx := K;
end;
end;
if (ClosestDist = INIT_DIST) then
break;
Squads[ClosestIdx].TargetUnit := nil;
Squads[ClosestIdx].TargetHouse := nil;
Squads[ClosestIdx].FinalPosition := KMPointDir(Position, Dir);
Squads[ClosestIdx].WalkTimeLimit := aTick + KMDistanceAbs(Position, Squads[ClosestIdx].Position) * SelectedTime;
AvailableSquads[ClosestIdx] := False;
end;
finally
TagPositions.Free;
end;
end;
var
Distance: Word;
Cnt: Integer;
TargetPoint: TKMPoint;
PointPath, Positions: TKMPointArray;
InitPolygons: TKMWordArray;
begin
if (fCompanyMode = cmDefence) then
begin
TargetPoint := fTargetPoint;
Result := not KMSamePoint(TargetPoint, aActualPosition);
end
else
begin
TargetPoint := GetTargetPosition();
Result := not KMSamePoint(TargetPoint, KMPOINT_ZERO);
end;
if not Result then
Exit;
// Try find path to the target point
//if gAIFields.NavMesh.Pathfinding.ShortestRoute(aActualPosition, TargetPoint, Distance, PointPath) then
if gAIFields.NavMesh.Pathfinding.AvoidTrafficRoute(fOwner, aActualPosition, TargetPoint, Distance, PointPath) then
begin
Cnt := SquadCnt(); // Count of groups in company
// Get init polygons -> polygons on road which are base for army positioning (groups will get order to walk there)
InitPolygons := GetInitPolygons(Cnt, PointPath);
// Get positions around init polygons
gAIFields.NavMesh.Positioning.FindPositions(Cnt, Round(AI_Par[ATTACK_COMPANY_MinWalkSpacing]), InitPolygons, Positions);
// Compute distance of new positions and select the closest group to walk there
SetOrders(Cnt, TargetPoint, Positions);
end;
{$IFDEF DEBUG_NewAI}
DEBUGPointPath := PointPath;
{$ENDIF}
end;
function TAICompany.SquadCnt(aTypes: TKMGroupTypeSet = GROUP_TYPES_VALID): Word;
var
GT: TKMGroupType;
begin
Result := 0;
for GT in aTypes do
Result := Result + fSquads[GT].Count;
end;
function TAICompany.WarriorCnt(aTypes: TKMGroupTypeSet = GROUP_TYPES_VALID): Word;
var
K: Integer;
G: TKMUnitGroup;
GT: TKMGroupType;
begin
Result := 0;
for GT in aTypes do
for K := 0 to fSquads[GT].Count - 1 do
begin
G := TAISquad(fSquads[GT].Items[K]).Group;
if (G <> nil) then
Result := Result + G.Count;
end;
end;
procedure TAICompany.AddSquad(aGroup: TKMUnitGroup);
begin
fSquads[ aGroup.GroupType ].Add( TAISquad.Create(aGroup) );
end;
procedure TAICompany.DeleteSquad(aGT: TKMGroupType; aIdx: Integer);
var
Squad: TAISquad;
begin
Squad := fSquads[aGT].Items[aIdx];
Squad.Free;
fSquads[aGT].Delete(aIdx);
end;
procedure TAICompany.InitCompany();
begin
fPathPosition := GetPosition();
fScanPosition := fPathPosition;
end;
function TAICompany.GetPosition(): TKMPoint;
var
I, Count: Integer;
Output: TKMPoint;
G: TKMGroupType;
begin
Output := KMPOINT_ZERO;
Count := 0;
for G := GROUP_TYPE_MIN to GROUP_TYPE_MAX do
for I := 0 to fSquads[G].Count-1 do
begin
Output := KMPointAdd(Output, TAISquad(fSquads[G].Items[I]).Group.Position);
Count := Count + 1;
end;
if (Count > 0) then
begin
Output.X := Round( Output.X / Count );
Output.Y := Round( Output.Y / Count );
end;
// If we cannot walk there choose first group instead
I := gAIFields.NavMesh.KMPoint2Polygon[Output];
if not (gTerrain.CheckPassability(Output, tpWalk)
AND ((gAIFields.Influences.PresenceAllGroups[ fOwner, I ] > 0)
//OR (gAIFields.Influences.ArmyTraffic[ fOwner, I ] > 0)
)
) then
for G := GROUP_TYPE_MIN to GROUP_TYPE_MAX do
if (fSquads[G].Count > 0) then
begin
Output := TAISquad(fSquads[G].Items[0]).Group.Position;
break;
end;
Result := Output;
end;
function TAICompany.GetPosition(var aSQRRadius: Single): TKMPoint;
var
I: Integer;
G: TKMGroupType;
begin
Result := GetPosition();
aSQRRadius := 0;
for G := GROUP_TYPE_MIN to GROUP_TYPE_MAX do
for I := 0 to fSquads[G].Count-1 do
aSQRRadius := Max(aSQRRadius, KMDistanceSqr(Result, TAISquad(fSquads[G].Items[I]).Group.Position));
end;
function TAICompany.ActualizeTarget(aInitPosition: TKMPoint; var aTargetHouse: TKMHouse; var aTargetUnit: TKMUnit; aAimCivilians: Boolean = True): Boolean;
var
TargetGroup: TKMUnitGroup;
begin
Result := False;
aTargetUnit := nil;
aTargetHouse := gHands.GetClosestHouse(aInitPosition, fOwner, atEnemy, TARGET_HOUSES, True);
if (aTargetHouse = nil) then
begin
TargetGroup := gHands.GetClosestGroup(aInitPosition, fOwner, atEnemy);
if (TargetGroup = nil) then
begin
if not aAimCivilians then
begin
aTargetHouse := gHands.GetClosestHouse(aInitPosition, fOwner, atEnemy, ALL_HOUSES, True);
if (aTargetHouse <> nil) then
fCompanyMode := cmDestruction;
end
else
begin
aTargetUnit := gHands.GetClosestUnit(aInitPosition, fOwner, atEnemy);
if (aTargetUnit = nil) then
Exit;
end;
end
else
aTargetUnit := TargetGroup.GetAliveMember;
end;
Result := True;
end;
function TAICompany.IsGroupInCompany(aGroup: TKMUnitGroup): Boolean;
var
I: Integer;
G: TKMGroupType;
begin
Result := True;
for G := GROUP_TYPE_MIN to GROUP_TYPE_MAX do
for I := 0 to fSquads[G].Count-1 do
if (TAISquad(fSquads[G].Items[I]).Group = aGroup) then
Exit;
Result := False;
end;
{ TKMArmyAttack }
constructor TKMArmyAttack.Create(aOwner: TKMHandID);
begin
inherited Create;
fCompanies := TKMList.Create();
fOwner := aOwner;
end;
destructor TKMArmyAttack.Destroy;
begin
fCompanies.Free;
inherited;
end;
procedure TKMArmyAttack.Save(SaveStream: TKMemoryStream);
var
I: Integer;
begin
SaveStream.PlaceMarker('ArmyAttack');
SaveStream.Write(fOwner);
SaveStream.Write( Integer(Count) );
for I := 0 to Count - 1 do
Company[I].Save(SaveStream);
end;
procedure TKMArmyAttack.Load(LoadStream: TKMemoryStream);
var
I, NewCount: Integer;
begin
LoadStream.CheckMarker('ArmyAttack');
LoadStream.Read(fOwner);
LoadStream.Read(NewCount);
for I := 0 to NewCount - 1 do
fCompanies.Add( TAICompany.Load(LoadStream) );
end;
procedure TKMArmyAttack.SyncLoad();
var
I: Integer;
begin
for I := 0 to Count - 1 do
Company[I].SyncLoad();
end;
procedure TKMArmyAttack.AfterMissionInit();
begin
end;
procedure TKMArmyAttack.UpdateState(aTick: Cardinal);
function DetectEnemyPresence(aPoint: TKMPoint): Boolean;
var
Idx: Integer;
GT: TKMGroupType;
begin
Result := True;
Idx := gAIFields.NavMesh.KMPoint2Polygon[aPoint];
for GT := GROUP_TYPE_MIN to GROUP_TYPE_MAX do
if (gAIFields.Influences.EnemyGroupPresence[ fOwner, Idx, GT ] > 0) then
Exit;
Result := False;
end;
var
I: Integer;
Company: TAICompany;
begin
for I := Count - 1 downto 0 do
begin
Company := fCompanies.Items[I];
Company.UpdateState(aTick);
if ( (Company.SquadCnt = 0) OR (Company.State = csIdle) ) then
//AND ( (Company.CompanyMode in [cmAttack, cmDestruction]) OR not DetectEnemyPresence(Company.TargetPoint) ) then
fCompanies.Remove( Company );
end;
end;
procedure TKMArmyAttack.OwnerUpdate(aPlayer: TKMHandID);
var
I: Integer;
begin
fOwner := aPlayer;
for I := Count - 1 downto 0 do
Company[I].Owner := aPlayer;
end;
function TKMArmyAttack.GetCount(): Integer;
begin
Result := fCompanies.Count;
end;
function TKMArmyAttack.GetCompany(aIdx: Integer): TAICompany;
begin
Result := fCompanies.Items[aIdx];
end;
function TKMArmyAttack.IsGroupInAction(aGroup: TKMUnitGroup): Boolean;
var
I: Integer;
begin
Result := True;
for I := 0 to Count - 1 do
if Company[I].IsGroupInCompany(aGroup) then
Exit;
Result := False;
end;
procedure TKMArmyAttack.CreateCompany(aTargetPoint: TKMPoint; aGroups: TKMUnitGroupArray; aCompanyMode: TKMCompanyMode = cmAttack);
procedure PrepareCompany(var aCompany: TAICompany; aTargetHouse: TKMHouse; aTargetUnit: TKMUnit);
var
I: Integer;
begin
aCompany.SetTarget(aTargetHouse, aTargetUnit);
for I := 0 to Length(aGroups) - 1 do
aCompany.AddSquad( aGroups[I] );
aCompany.InitCompany();
end;
var
Company: TAICompany;
TargetHouse: TKMHouse;
TargetUnit: TKMUnit;
begin
TargetHouse := nil;
TargetUnit := nil;
Company := TAICompany.Create(fOwner, aCompanyMode);
fCompanies.Add( Company );
if (aCompanyMode = cmAttack) AND Company.ActualizeTarget(aTargetPoint, TargetHouse, TargetUnit) then
begin
PrepareCompany(Company, TargetHouse, TargetUnit);
end
else if (aCompanyMode = cmDefence) then
begin
Company.TargetPoint := aTargetPoint;
PrepareCompany(Company, TargetHouse, TargetUnit);
end
else
fCompanies.Remove( Company );
end;
procedure TKMArmyAttack.LogStatus(var aBalanceText: UnicodeString);
begin
//aBalanceText := '';
end;
procedure TKMArmyAttack.Paint;
const
COLOR_WHITE = $FFFFFF;
COLOR_BLACK = $000000;
COLOR_GREEN = $00FF00;
COLOR_RED = $0000FF;
COLOR_YELLOW = $00FFFF;
COLOR_BLUE = $FF0000;
var
CompanySelected: Boolean;
K,L: Integer;
{$IFDEF DEBUG_NewAI}
M: Integer;
{$ENDIF}
Col, CompOpacity1, CompOpacity2, GroupOpacity1, GroupOpacity2: Cardinal;
Position: TKMPoint;
GT: TKMGroupType;
Company: TAICompany;
Squad: TAISquad;
begin
//if (fOwner <> gMySpectator.HandID) then // Show just 1 player (it prevents notification to be mess)
// Exit;
//if (fOwner <> 1) then // Show just 1 player (it prevents notification to be mess)
// Exit;
CompanySelected := False;
for K := 0 to Count - 1 do
begin
// Company status log
Company := fCompanies.Items[K];
CompOpacity1 := $09000000;
CompOpacity2 := $33000000;
// Check if company is selected
for GT := GROUP_TYPE_MIN to GROUP_TYPE_MAX do
for L := Company.Squads[GT].Count - 1 downto 0 do
if ((gMySpectator.Selected is TKMUnitGroup) AND (gMySpectator.Selected = TAISquad(Company.Squads[GT].Items[L]).Group)) then
begin
CompanySelected := True;
CompOpacity1 := $20000000;
CompOpacity2 := $FF000000;
end;
// Pathfinding (squads) + targets
if CompanySelected then
for GT := GROUP_TYPE_MIN to GROUP_TYPE_MAX do
for L := Company.Squads[GT].Count - 1 downto 0 do
begin
Squad := Company.Squads[GT].Items[L];
// Highlight selected group and squad
GroupOpacity1 := $20000000;
GroupOpacity2 := $30000000;
if (gMySpectator.Selected is TKMUnitGroup) AND (gMySpectator.Selected = Squad.Group) then
begin
GroupOpacity1 := $99000000;
GroupOpacity2 := $FF000000;
end;
// Order position of group
Position := Squad.Group.OrderLoc.Loc;
gRenderAux.CircleOnTerrain(Position.X, Position.Y, 0.25, $FF000000 OR COLOR_YELLOW, $FF000000 OR COLOR_YELLOW);
// Position
Position := Squad.Position;
gRenderAux.CircleOnTerrain(Position.X, Position.Y, 0.75, GroupOpacity2 OR COLOR_GREEN, GroupOpacity2 OR COLOR_GREEN);
// Target unit
if (Squad.TargetUnit <> nil) then
begin
if not Squad.TargetUnit.IsDeadOrDying then
begin
gRenderAux.LineOnTerrain(Position, Squad.TargetUnit.CurrPosition, GroupOpacity2 OR COLOR_RED);
{$IFDEF DEBUG_NewAI}
if (Length(Squad.DEBUGPointPath) > 0) then
for M := Length(Squad.DEBUGPointPath)-2 downto 0 do
gRenderAux.LineOnTerrain(Squad.DEBUGPointPath[M+1], Squad.DEBUGPointPath[M], GroupOpacity1 OR COLOR_BLUE);
{$ENDIF}
end;
end
// Target house
else if (Squad.TargetHouse <> nil) then
if not Squad.TargetHouse.IsDestroyed then
begin
gRenderAux.LineOnTerrain(Position, Squad.TargetHouse.Position, GroupOpacity2 OR COLOR_RED);
{$IFDEF DEBUG_NewAI}
if (Length(Squad.DEBUGPointPath) > 0) then
for M := Length(Squad.DEBUGPointPath)-2 downto 0 do
gRenderAux.LineOnTerrain(Squad.DEBUGPointPath[M+1], Squad.DEBUGPointPath[M], GroupOpacity1 OR COLOR_BLUE);
{$ENDIF}
end;
// Pathfinding
if not KMSamePoint(Squad.FinalPosition.Loc,KMPOINT_ZERO) then
begin
//gRenderAux.LineOnTerrain(Position, Squad.FinalPosition.Loc, $AAFF5555);
gRenderAux.CircleOnTerrain(Squad.FinalPosition.Loc.X, Squad.FinalPosition.Loc.Y, 1, 0, GroupOpacity1 OR COLOR_BLUE);
{$IFDEF DEBUG_NewAI}
if (Length(Squad.DEBUGPointPath) > 0) then
for M := Length(Squad.DEBUGPointPath)-2 downto 0 do
gRenderAux.LineOnTerrain(Squad.DEBUGPointPath[M+1], Squad.DEBUGPointPath[M], GroupOpacity1 OR COLOR_BLUE);
{$ENDIF}
end;
end;
// Change color according to state
Col := 0; // For compiler
case Company.State of
csAttack: Col := COLOR_RED;
csWalking: Col := COLOR_BLUE;
csIdle: Col := COLOR_BLACK;
end;
// Mark company
Position := Company.ScanPosition;
{$IFDEF DEBUG_NewAI}
//gRenderAux.CircleOnTerrain(Position.X, Position.Y, Sqrt(Company.DEBUGScanRad), CompOpacity1 OR Col, CompOpacity2 OR Col);
{$ENDIF}
gRenderAux.CircleOnTerrain(Position.X, Position.Y, 3, CompOpacity1 OR Col, CompOpacity2 OR Col);
Position := Company.PathPosition;
gRenderAux.CircleOnTerrain(Position.X, Position.Y, 2, CompOpacity1 OR COLOR_GREEN, CompOpacity2 OR COLOR_GREEN);
// Target aim
{$IFDEF DEBUG_NewAI}
for L := 0 to Length(Company.DEBUGTargetU) - 1 do
with Company.DEBUGTargetU[L] do
begin
Position := CenterPoint;
// Close threath
Col := (Min($FF, Max(0,Round(CloseThreat)) ) shl 24) OR COLOR_RED;
gRenderAux.CircleOnTerrain(Position.X, Position.Y, 0.5, Col, $FF000000 OR COLOR_RED );
// Distant threath
Col := (Min($FF, Max(0,Round(DistantThreat)) ) shl 24) OR COLOR_BLACK;
gRenderAux.CircleOnTerrain(Position.X, Position.Y+1, 0.5, Col, $FF000000 OR COLOR_BLACK );
end;
{$ENDIF}
// Pathfinding (company)
{$IFDEF DEBUG_NewAI}
if (Length(Company.DEBUGPointPath) > 0) then
for L := Length(Company.DEBUGPointPath)-2 downto 0 do
gRenderAux.LineOnTerrain(Company.DEBUGPointPath[L+1], Company.DEBUGPointPath[L], CompOpacity2 OR COLOR_YELLOW);
{$ENDIF}
end;
//gRenderAux.LineOnTerrain(P1, P2, $CCFF2222);
//gRenderAux.CircleOnTerrain(CenterPlatoon.X, CenterPlatoon.Y, 5, $09FFFFFF, $99FFFFFF);
//gRenderAux.Quad(Loc.X, Loc.Y, Color);
end;
// JUNK
{
function SelectClosestTarget(aInitPoint: TKMPoint; var aTargetHouse: TKMHouse; var aTargetUnit: TKMUnit; var aTargetGroup: TKMUnitGroup): Boolean;
function TKMArmyAttack.SelectClosestTarget(aInitPoint: TKMPoint; var aTargetHouse: TKMHouse; var aTargetUnit: TKMUnit; var aTargetGroup: TKMUnitGroup): Boolean;
begin
Result := False;
aTargetHouse := nil;
aTargetUnit := nil;
aTargetGroup := nil;
// Compute common stuf first
aTargetHouse := gHands.GetClosestHouse(aInitPoint, fOwner, atEnemy, TARGET_HOUSES, False);
if (aTargetHouse = nil) then
begin
aTargetGroup := gHands.GetClosestGroup(aInitPoint, fOwner, atEnemy);
if (aTargetGroup = nil) then
begin
aTargetUnit := gHands.GetClosestUnit(aInitPoint, fOwner, atEnemy);
if (aTargetUnit = nil) then
Exit;
end;
end;
Result := True;
end;
// Try find new target house in ownership area
function SelectNewTargetHouse(aPosition: TKMPoint): TKMHouse;
var
PLIdx: Word;
Enemies: TKMHandIndexArray;
HT: TKMHouseType;
H: TKMHouse;
begin
Result := nil;
Enemies := gAIFields.Influences.GetAllAllianceOwnership(fOwner, aPosition.X, aPosition.Y, atEnemy);
for PLIdx := 0 to Length(Enemies) - 1 do
for HT in TARGET_HOUSES do
begin
H := gHands[ Enemies[PLIdx] ].Houses.FindHouse(HT, aPosition.X, aPosition.Y);
if (H <> nil) then
begin
Result := H;
Exit;
end;
end;
end;
//}
end.