Files
kam-mobile/vendor/kam_remake/src/hands/KM_Hand.pas
T
admin_d 7063d1b1c9
Sync changed files to Google Drive / sync (push) Successful in 1h45m13s
sync: restore source from Google Drive
2026-09-12 16:50:30 +05:00

2320 lines
72 KiB
ObjectPascal

unit KM_Hand;
{$I KaM_Remake.inc}
interface
uses
Generics.Collections,
KM_AI,
KM_Units, KM_UnitsCollection, KM_UnitGroup, KM_UnitWarrior,
KM_Houses, KM_HouseCollection, KM_HouseInn,
KM_HandLogistics, KM_HandLocks, KM_HandStats, KM_GameTypes,
KM_FogOfWar, KM_HandConstructions, KM_MessageLog, KM_ResHouses,
KM_CommonClasses, KM_CommonTypes, KM_Defaults, KM_Points,
KM_HandEntity, KM_HandTypes, KM_CommonClassesExt,
KM_ResTypes, KM_ResFonts;
type
TKMChooseLoc = record
Allowed, Placed: Boolean;
Resources: array[WARE_MIN..WARE_MAX] of Word;
Units: array[CITIZEN_MIN..CITIZEN_MAX] of Byte;
end;
//Player manages its assets
TKMHandCommon = class abstract
private
fID: TKMHandID; //Index of this hand in gHands
fUnits: TKMUnitsCollection;
public
constructor Create(aHandIndex: TKMHandID);
destructor Destroy; override;
property ID: TKMHandID read fID;
property Units: TKMUnitsCollection read fUnits;
function AddUnit(aUnitType: TKMUnitType; const aLoc: TKMPoint): TKMUnit;
function RemUnit(const Position: TKMPoint): Boolean; overload;
function RemUnit(const Position: TKMPoint; out aUnitType: TKMUnitType): Boolean; overload;
function UnitsHitTest(const aLoc: TKMPoint; const UT: TKMUnitType = utAny): TKMUnit; overload;
function UnitsHitTest(X, Y: Integer; const UT: TKMUnitType = utAny): TKMUnit; overload;
procedure Save(SaveStream: TKMemoryStream); virtual;
procedure Load(LoadStream: TKMemoryStream); virtual;
procedure SyncLoad; virtual;
procedure UpdateState(aTick: Cardinal); virtual;
procedure UpdateVisualState;
procedure Paint(const aRect: TKMRect; aTickLag: Single); virtual;
end;
TKMHand = class(TKMHandCommon)
private
fAI: TKMHandAI;
fEnabled: Boolean;
fConstructions: TKMHandConstructions;
fDeliveries: TKMHandLogistics;
fFogOfWar: TKMFogOfWar; //Stores FOW info for current player, which includes
fHouses: TKMHousesCollection;
fLocks: TKMHandLocks;
fRoadsList: TKMPointList; //Used only once to speedup mission loading, then freed
fStats: TKMHandStats;
fUnitGroups: TKMUnitGroups;
fMessageLog: TKMMessageLog;
fOwnerNickname: AnsiString; //Multiplayer owner nickname
fHandType: TKMHandType;
fCanBeHuman: Boolean;
fCanBeAITypes: TKMAITypeSet;
fFlagColor: Cardinal;
fTeam: Integer;
fTeamColor: Cardinal;
fCenterScreen: TKMPoint;
fChooseLocation: TKMChooseLoc;
fAlliances: array [0 .. MAX_HANDS - 1] of TKMAllianceType;
fShareFOW: array [0 .. MAX_HANDS - 1] of Boolean;
fShareBeacons: array [0 .. MAX_HANDS - 1] of Boolean;
// Overlays
fOverlayText: UnicodeString; //Needed for replays. Not saved since it's translated
fOverlayMarkup: AnsiString;
fOverlayParams: TKMVarValueList;
fOverlayTextSettings: TKMOverlayTextSettings;
//House sketch fields, used to GetNextHouseWSameType
fHSketch: TKMHouseSketchEdit;
fFirstHSketch: TKMHouseSketchEdit;
fFoundHSketch: TKMHouseSketchEdit;
fOnAllianceChange: TKMEvent;
function IsDisabled: Boolean;
function GetColorIndex: Byte;
function GetAlliances(aIndex: Integer): TKMAllianceType; inline;
procedure SetAlliances(aIndex: Integer; aValue: TKMAllianceType); inline;
function GetShareFOW(aIndex: Integer): Boolean;
procedure SetShareFOW(aIndex: Integer; aValue: Boolean);
function GetShareBeacons(aIndex: Integer): Boolean;
procedure SetShareBeacons(aIndex: Integer; aValue: Boolean);
procedure EntityDestroyed(aEntity: TKMHandEntity);
procedure GroupDied(aGroup: TKMUnitGroup);
procedure HouseDestroyed(aHouse: TKMHouse; aFrom: TKMHandID);
procedure UnitDied(aUnit: TKMUnit);
procedure UnitTrained(aUnit: TKMUnit);
procedure WarriorWalkedOut(aWarrior: TKMUnitWarrior);
function LocHasNoAllyPlans(const aLoc: TKMPoint): Boolean;
function GetGameFlagColor: Cardinal;
function GetOwnerNicknameU: UnicodeString;
procedure ChooseFirstStorehouse();
function GetAI: TKMHandAI;
procedure SetFlagColor(const Value: Cardinal);
procedure SetOwnerNickname(const aName: AnsiString);
function GetDeliveries: TKMHandLogistics;
procedure SetHandType(const Value: TKMHandType);
procedure SetEnabled(const Value: Boolean);
function GetFlagTextColor: Cardinal;
public
InCinematic: Boolean;
//Used for syncing hotkeys in multiplayer saves only. UI keeps local value to avoid GIP delays
SelectionHotkeys: array[0..DYNAMIC_HOTKEYS_NUM-1] of Integer;
constructor Create(aHandIndex: TKMHandID; aOnAllianceChange: TKMEvent);
destructor Destroy; override;
property AI: TKMHandAI read GetAI;
property Constructions: TKMHandConstructions read fConstructions;
property Deliveries: TKMHandLogistics read GetDeliveries;
property Houses: TKMHousesCollection read fHouses;
property Locks: TKMHandLocks read fLocks;
property Stats: TKMHandStats read fStats;
property FogOfWar: TKMFogOfWar read fFogOfWar;
property UnitGroups: TKMUnitGroups read fUnitGroups;
property MessageLog: TKMMessageLog read fMessageLog;
property Enabled: Boolean read fEnabled write SetEnabled;
property Disabled: Boolean read IsDisabled;
procedure SetHandIndex(aNewIndex: TKMHandID);
property OwnerNickname: AnsiString read fOwnerNickname write SetOwnerNickname; //MP owner nickname (empty in SP)
property OwnerNicknameU: UnicodeString read GetOwnerNicknameU;
function CalcOwnerName: UnicodeString; //Universal owner name
function OwnerName(aNumberedAIs: Boolean = True; aLocalized: Boolean = True): UnicodeString; //Universal owner name
function GetHandOwnerName(aIsHuman, aIsAdvAI: Boolean; aNumberedAIs: Boolean = True; aLocalized: Boolean = True): UnicodeString;
function GetOwnerName: UnicodeString;
function GetOwnerNameColored: AnsiString;
function GetOwnerNameColoredU: UnicodeString;
function HasAssets: Boolean;
property HandType: TKMHandType read fHandType write SetHandType; //Is it Human or AI
property CanBeHuman: Boolean read fCanBeHuman write fCanBeHuman;
property CanBeAITypes: TKMAITypeSet read fCanBeAITypes;
property FlagColor: Cardinal read fFlagColor write SetFlagColor;
property FlagTextColor: Cardinal read GetFlagTextColor;
property Team: Integer read fTeam write fTeam;
property TeamColor: Cardinal read fTeamColor write fTeamColor;
property GameFlagColor: Cardinal read GetGameFlagColor;
property FlagColorIndex: Byte read GetColorIndex;
property Alliances[aIndex: Integer]: TKMAllianceType read GetAlliances write SetAlliances;
property ShareFOW[aIndex: Integer]: Boolean read GetShareFOW write SetShareFOW;
property ShareBeacons[aIndex: Integer]: Boolean read GetShareBeacons write SetShareBeacons;
property CenterScreen: TKMPoint read fCenterScreen write fCenterScreen;
property ChooseLocation: TKMChooseLoc read fChooseLocation write fChooseLocation;
property OverlayText: UnicodeString read fOverlayText write fOverlayText;
property OverlayTextSettings: TKMOverlayTextSettings read fOverlayTextSettings;
property OverlayMarkup: AnsiString read fOverlayMarkup write fOverlayMarkup;
property OverlayParams: TKMVarValueList read fOverlayParams;
procedure SetOverlayTextWordWrap(aWordWrap: Boolean);
procedure SetOverlayTextFont(aFont: TKMFont);
procedure AddAIType(aHandAIType: TKMAIType);
procedure PostLoadMission;
function IsAnimal: Boolean;
function IsHuman: Boolean;
function IsComputer: Boolean;
function IsAdvancedAI: Boolean;
function IsClassicAI: Boolean;
function CanBeAI: Boolean;
procedure AfterMissionInit(aFlattenRoads: Boolean);
function AddUnit(aUnitType: TKMUnitType; const aLoc: TKMPoint; aAutoPlace: Boolean = True;
aRequiredWalkConnect: Byte = 0; aCheat: Boolean = False): TKMUnit; reintroduce; overload;
function AddUnit(aUnitType: TKMUnitType; const aLoc: TKMPointDir; aAutoPlace: Boolean = True;
aRequiredWalkConnect: Byte = 0; aCheat: Boolean = False): TKMUnit; reintroduce; overload;
function AddUnitGroup(aUnitType: TKMUnitType; const Position: TKMPoint; aDir: TKMDirection; aUnitPerRow, aCount: Word): TKMUnitGroup;
function TrainUnit(aUnitType: TKMUnitType; aInHouse: TKMHouse): TKMUnit;
function GetNextHouseWSameType(aHouseType: TKMHouseType; aStartFromUID: Integer): TKMHouse; overload;
function GetNextHouseWSameType(aHouseType: TKMHouseType; aStartFromUID: Integer;
out aHouseSketch: TKMHouseSketchEdit;
aSketchTypesSet: TKMHouseSketchTypeSet = [hstHouse]): TKMHouse; overload;
function GetNextHouseWSameType(aHouseType: TKMHouseType; aStartFromUID: Integer;
out aHouseSketch: TKMHouseSketchEdit;
aSketchTypesSet: TKMHouseSketchTypeSet;
aVerifySketch: TAnonHouseSketchBoolFn;
aVerifySketchBoolParam: Boolean): TKMHouse; overload;
function GetNextUnitWSameType(aUnitType: TKMUnitType; aStartFromUID: Integer): TKMUnit;
function GetNextGroupWSameType(aUnitType: TKMUnitType; aStartFromUID: Integer): TKMUnitGroup;
function CanAddFieldPlan(const aLoc: TKMPoint; aFieldType: TKMFieldType): Boolean;
function CanAddFakeFieldPlan(const aLoc: TKMPoint; aFieldType: TKMFieldType): Boolean;
function CanRemFakeFieldPlan(const aLoc: TKMPoint; aFieldType: TKMFieldType): Boolean;
function CanAddHousePlan(const aLoc: TKMPoint; aHouseType: TKMHouseType): Boolean;
function CanAddHousePlanAI(aX, aY: Word; aHouseType: TKMHouseType; aCheckInfluence: Boolean): Boolean;
procedure AddFirstStorehouse(aEntrance: TKMPoint);
procedure ResetChooseLocation;
function NeedToChooseFirstStorehouse: Boolean;
function NeedToChooseFirstStorehouseInGame: Boolean;
procedure AddRoadToList(const aLoc: TKMPoint);
procedure AddRoad(const aLoc: TKMPoint);
procedure AddField(const aLoc: TKMPoint; aFieldType: TKMFieldType; aStage: Byte = 0; aKeepOldObject: Boolean = False;
aRemoveOverlay: Boolean = True);
procedure ToggleFieldPlan(const aLoc: TKMPoint; aFieldType: TKMFieldType; aMakeSound: Boolean);
procedure ToggleFakeFieldPlan(const aLoc: TKMPoint; aFieldType: TKMFieldType);
function AddHouse(aHouseType: TKMHouseType; PosX, PosY: Word; RelativeEntrace: Boolean): TKMHouse;
procedure AddHousePlan(aHouseType: TKMHouseType; const aLoc: TKMPoint);
function HasHousePlan(const aLoc: TKMPoint): Boolean;
function AddHouseWIP(aHouseType: TKMHouseType; const aLoc: TKMPoint): TKMHouse;
function RemGroup(const Position: TKMPoint): Boolean;
procedure RemHouse(const Position: TKMPoint; DoSilent: Boolean; IsEditor: Boolean = False);
procedure RemHousePlan(const Position: TKMPoint);
procedure RemFieldPlan(const Position: TKMPoint; aMakeSound:Boolean);
procedure RemFakeFieldPlan(const Position: TKMPoint);
function FindInn(Loc: TKMPoint; aUnit: TKMUnit; UnitIsAtHome: Boolean = False): TKMHouseInn;
function FindHouse(aType: TKMHouseType; const aPosition: TKMPoint; Index: Byte = 1): TKMHouse; overload;
function FindHouse(aType: TKMHouseType; Index: Byte=1): TKMHouse; overload;
function FindHousesInRadius(const aLoc: TKMPoint; aSqrRadius: Single; aTypes: TKMHouseTypeSet = HOUSES_VALID; aOnlyCompleted: Boolean = True): TKMHouseArray;
function FindCityCenter: TKMPoint;
function HitTest(X,Y: Integer): TObject;
function HousesHitTest(X, Y: Integer): TKMHouse;
function GroupsHitTest(X, Y: Integer): TKMUnitGroup;
function ObjectByUID(aUID: Integer): TObject;
procedure GetHouseMarks(const aLoc: TKMPoint; aHouseType: TKMHouseType; aList: TKMPointTagList; aIgnoreFOW: Boolean = False);
function GetFieldsCount: Integer;
procedure GetFieldPlans(aList: TKMPointTagList; const aRect: TKMRect; aIncludeFake: Boolean);
procedure GetHousePlans(aList: TKMPointDirList; const aRect: TKMRect);
procedure GetPlansTablets(aList: TKMPointTagList; const aRect: TKMRect);
function CanDoStatsUpdate(aTick: Cardinal): Boolean;
function DoCheckGoals: Boolean;
procedure Save(SaveStream: TKMemoryStream); override;
procedure Load(LoadStream: TKMemoryStream); override;
procedure SyncLoad; override;
procedure IncAnimStep;
procedure UpdateState(aTick: Cardinal); override;
procedure Paint(const aRect: TKMRect; aTickLag: Single); override;
function ObjToString(aSeparator: String = ' '): String;
end;
TKMHandAnimals = class(TKMHandCommon)
public
function GetFishInWaterBody(aWaterID: Byte; FindHighestCount: Boolean = True): TKMUnitFish;
end;
function GetStatsUpdatePeriod: Integer;
implementation
uses
Classes, SysUtils, KromUtils, Math, TypInfo,
KM_Entity,
KM_Cursor, KM_Game, KM_GameParams, KM_Terrain,
KM_HandsCollection, KM_Sound, KM_AIFields, KM_MapEdTypes,
KM_Resource, KM_ResSound, KM_ResTexts, KM_ResMapElements, KM_ScriptingEvents, KM_ResUnits,
KM_CommonUtils, KM_GameSettings,
KM_UnitGroupTypes,
KM_MapTypes;
const
TIME_TO_SET_FIRST_STOREHOUSE = 10*60*2; //We give 2 minutes to set first storehouse, otherwise player will be defeated
{ TKMHandCommon }
constructor TKMHandCommon.Create(aHandIndex: TKMHandID);
begin
inherited Create;
fID := aHandIndex;
fUnits := TKMUnitsCollection.Create;
end;
destructor TKMHandCommon.Destroy;
begin
FreeThenNil(fUnits);
inherited;
end;
function TKMHandCommon.AddUnit(aUnitType: TKMUnitType; const aLoc: TKMPoint): TKMUnit;
begin
//Animals are autoplaced by default
Result := fUnits.AddUnit(fID, aUnitType, KMPointDir(aLoc, dirS), True);
end;
procedure TKMHandCommon.Paint(const aRect: TKMRect; aTickLag: Single);
begin
if mlUnits in gGameParams.VisibleLayers then
fUnits.Paint(aRect, aTickLag);
end;
function TKMHandCommon.RemUnit(const Position: TKMPoint): Boolean;
var
UnitType: TKMUnitType;
begin
Result := RemUnit(Position, UnitType);
end;
function TKMHandCommon.RemUnit(const Position: TKMPoint; out aUnitType: TKMUnitType): Boolean;
var
U: TKMUnit;
begin
Assert(gGameParams.IsMapEditor);
U := fUnits.HitTest(Position.X, Position.Y);
Result := U <> nil;
if Result then
begin
aUnitType := U.UnitType;
fUnits.RemoveUnit(U);
end;
end;
procedure TKMHandCommon.Save(SaveStream: TKMemoryStream);
begin
SaveStream.PlaceMarker('HandCommon');
fUnits.Save(SaveStream);
end;
procedure TKMHandCommon.Load(LoadStream: TKMemoryStream);
begin
LoadStream.CheckMarker('HandCommon');
fUnits.Load(LoadStream);
end;
procedure TKMHandCommon.SyncLoad;
begin
fUnits.SyncLoad;
end;
function TKMHandCommon.UnitsHitTest(const aLoc: TKMPoint; const UT: TKMUnitType = utAny): TKMUnit;
begin
Result := UnitsHitTest(aLoc.X, aLoc.Y, UT);
end;
function TKMHandCommon.UnitsHitTest(X, Y: Integer; const UT: TKMUnitType = utAny): TKMUnit;
begin
Result := fUnits.HitTest(X, Y, UT);
end;
procedure TKMHandCommon.UpdateState(aTick: Cardinal);
begin
fUnits.UpdateState(aTick);
end;
procedure TKMHandCommon.UpdateVisualState;
begin
Assert(gGameParams.IsMapEditor);
fUnits.UpdateVisualState;
end;
{ TKMHand }
constructor TKMHand.Create(aHandIndex: TKMHandID; aOnAllianceChange: TKMEvent);
var
I: Integer;
begin
inherited Create(aHandIndex);
Enabled := True;
fOnAllianceChange := aOnAllianceChange;
fAI := TKMHandAI.Create(fID);
fFogOfWar := TKMFogOfWar.Create(gTerrain.MapX, gTerrain.MapY, gGameParams.DynamicFOW);
fLocks := TKMHandLocks.Create;
fStats := TKMHandStats.Create;
fRoadsList := TKMPointList.Create;
fHouses := TKMHousesCollection.Create;
fDeliveries := TKMHandLogistics.Create(fID);
fConstructions:= TKMHandConstructions.Create;
fUnitGroups := TKMUnitGroups.Create;
fMessageLog := TKMMessageLog.Create;
fOwnerNickname := '';
fHandType := hndComputer;
fCanBeHuman := False;
fCanBeAITypes := [];
for I := 0 to MAX_HANDS - 1 do
begin
fShareFOW[I] := True; //Share FOW between allies by default (it only affects allied players)
fShareBeacons[I] := True; //Share beacons between allies by default (it only affects allied players)
end;
for I := 0 to 9 do
SelectionHotkeys[I] := -1; //Not set
fAlliances[fID] := atAlly; //Others are set to enemy by default
fFlagColor := DEFAULT_PLAYERS_COLORS[fID]; //Init with default color, later replaced by Script
fTeamColor := fFlagColor;
fTeam := NO_TEAM;
fOverlayParams := TKMVarValueList.Create;
fOverlayTextSettings.WordWrap := False;
fOverlayTextSettings.Font := fntMetal;
fHSketch := TKMHouseSketchEdit.Create;
fFirstHSketch := TKMHouseSketchEdit.Create;
fFoundHSketch := TKMHouseSketchEdit.Create;
ResetChooseLocation;
end;
//Destruction order is important as Houses and Units need to access
//Stats/Deliveries and other collection in their Destroy/Abandon/Demolish methods
destructor TKMHand.Destroy;
begin
FreeAndNil(fHSketch);
FreeAndNil(fFirstHSketch);
FreeAndNil(fFoundHSketch);
FreeAndNil(fOverlayParams);
//Groups freed before units since we need to release pointers they have to units
FreeThenNil(fUnitGroups);
FreeThenNil(fMessageLog);
//Free units
inherited;
FreeThenNil(fRoadsList);
FreeThenNil(fHouses);
//Should be freed after Houses and Units, as they write Stats on Destroy
FreeThenNil(fLocks);
FreeThenNil(fStats);
FreeThenNil(fFogOfWar);
FreeThenNil(fDeliveries);
FreeThenNil(fConstructions);
FreeThenNil(fAI);
end;
procedure TKMHand.ResetChooseLocation;
begin
if Self = nil then Exit;
fChooseLocation.Allowed := False;
fChooseLocation.Placed := False;
end;
function TKMHand.AddUnit(aUnitType: TKMUnitType; const aLoc: TKMPoint; aAutoPlace: Boolean = True;
aRequiredWalkConnect: Byte = 0; aCheat: Boolean = False): TKMUnit;
begin
Result := AddUnit(aUnitType, KMPointDir(aLoc, dirS), aAutoPlace, aRequiredWalkConnect, aCheat);
end;
//Place unit of aUnitType to aLoc via script
//AutoPlace - add unit to nearest available spot if aLoc is already taken (or unwalkable)
function TKMHand.AddUnit(aUnitType: TKMUnitType; const aLoc: TKMPointDir; aAutoPlace: Boolean = True;
aRequiredWalkConnect: Byte = 0; aCheat: Boolean = False): TKMUnit;
var
G: TKMUnitGroup;
begin
Result := fUnits.AddUnit(fID, aUnitType, aLoc, aAutoPlace, aRequiredWalkConnect);
//Unit failed to add, that happens
if Result = nil then Exit;
if gGameParams.IsMapEditor then
Exit;
Result.OnUnitDied := UnitDied;
Result.OnUnitTrained := UnitTrained; //Used for debug Scout placed by a cheat
if Result is TKMUnitWarrior then
TKMUnitWarrior(Result).OnWarriorWalkOut := WarriorWalkedOut;
if Result is TKMUnitWorker then
fConstructions.AddWorker(TKMUnitWorker(Result));
if Result is TKMUnitSerf then
fDeliveries.AddSerf(TKMUnitSerf(Result));
if not aCheat then
fStats.UnitCreated(aUnitType, False)
else
if Result is TKMUnitWarrior then
begin
//When we place a cheat Scout we want to rig it immediately
//It's simpler to count cheat scouts as initial, rather than trained.
//Then we don't need to mess with initial recruits.
fStats.UnitCreated(aUnitType, False);
G := fUnitGroups.WarriorTrained(TKMUnitWarrior(Result));
Assert(G <> nil, 'It is certain that equipped warrior creates or finds some group to join to');
G.OnGroupDied := GroupDied;
//Scripting doesn't care about scouts added this way.
//It could cause issues if the scripter assumes the warrior came from the player's barracks.
//The event is "OnWarriorEquipped" not "OnWarriorCreated".
//fScriptingESA.ProcWarriorEquipped(Result, G);
end;
end;
//Start training unit in School/Barracks
//User can cancel the training, so we don't add unit to stats just yet
function TKMHand.TrainUnit(aUnitType: TKMUnitType; aInHouse: TKMHouse): TKMUnit;
begin
Assert(aInHouse <> nil, 'House to train unit in could not be nil');
// Add unit and specify that its made inside house (so there is no need to occupy terrain tile)
Result := fUnits.AddUnit(fID, aUnitType, KMPointDir(aInHouse.Entrance, dirS), False, 0, aInHouse);
Result.OnUnitDied := UnitDied;
Result.OnUnitTrained := UnitTrained;
if Result is TKMUnitWarrior then
TKMUnitWarrior(Result).OnWarriorWalkOut := WarriorWalkedOut;
//Do not add unit to statistic just yet, wait till it's training complete
end;
procedure TKMHand.UnitTrained(aUnit: TKMUnit);
begin
if aUnit.UnitType = utBuilder then
fConstructions.AddWorker(TKMUnitWorker(aUnit));
if aUnit.UnitType = utSerf then
fDeliveries.AddSerf(TKMUnitSerf(aUnit));
//Warriors don't trigger "OnTrained" event, they trigger "WarriorEquipped" in WarriorWalkedOut below
if not (aUnit is TKMUnitWarrior) then
gScriptEvents.ProcUnitTrained(aUnit);
fStats.UnitCreated(aUnit.UnitType, True{, (aUnit.InHouse <> nil) and (aUnit.InHouse.HouseType = htTownHall)});
end;
procedure TKMHand.WarriorWalkedOut(aWarrior: TKMUnitWarrior);
var
G: TKMUnitGroup;
H: TKMHouse;
HWFP: TKMHouseWFlagPoint;
begin
// Warrior could be killed before he walked out, f.e. by script OnTick ---> Actions.UnitKill
// Then group will be assigned to invalid warrior and never gets removed from game
if (aWarrior = nil) or aWarrior.IsDeadOrDying then
Exit;
G := fUnitGroups.WarriorTrained(aWarrior);
Assert(G <> nil, 'It is certain that equipped warrior creates or finds some group to join to');
G.OnGroupDied := GroupDied;
if IsComputer then
begin
if AI.Setup.NewAI then
AI.ArmyManagement.WarriorEquipped(G)
else
AI.General.WarriorEquipped(aWarrior);
G := UnitGroups.GetGroupByMember(aWarrior); //AI might assign warrior to different group
end
else
if G.Count = 1 then
begin
//If player is human and this is the first warrior in the group, send it to the rally point
H := HousesHitTest(aWarrior.Position.X, aWarrior.Position.Y-1);
if (H is TKMHouseWFlagPoint) then
begin
HWFP := TKMHouseWFlagPoint(H);
HWFP.ValidateFlagPoint; // Validate Flag point first. It will set it to a proper walkable position
if HWFP.IsFlagPointSet
and G.CanWalkTo(HWFP.FlagPoint, 0) then
G.OrderWalk(HWFP.FlagPoint, True, wtokFlagPoint);
end;
end;
gScriptEvents.ProcWarriorEquipped(aWarrior, G);
end;
function TKMHand.AddUnitGroup(aUnitType: TKMUnitType; const Position: TKMPoint; aDir: TKMDirection; aUnitPerRow, aCount: Word): TKMUnitGroup;
var
I: Integer;
begin
Assert(aDir <> dirNA);
Result := nil;
if aUnitType in [CITIZEN_MIN..CITIZEN_MAX] then
for I := 0 to aCount - 1 do
AddUnit(aUnitType, Position)
else
if aUnitType in [WARRIOR_MIN..WARRIOR_MAX] then
Result := fUnitGroups.AddGroup(fID, aUnitType, Position.X, Position.Y, aDir, aUnitPerRow, aCount);
//Group can be nil if it fails to be placed on terrain (e.g. because of terrain height passability)
if Result <> nil then
Result.OnGroupDied := GroupDied;
//Units will be added to statistic inside the function for some units may not fit on map
end;
//When adding roads from script we want to batch them all into one list to save time
//on WalkConnect and other calculations
procedure TKMHand.AddRoadToList(const aLoc: TKMPoint);
begin
Assert(fRoadsList <> nil);
//Sometimes maps can have roads placed outside of map bounds - ignore them
//(on 80x80 map Loc range is 1..79, which is not obvious when placing roads manually in script)
if gTerrain.TileInMapCoords(aLoc.X, aLoc.Y) then
fRoadsList.Add(aLoc);
end;
function TKMHand.IsHuman: Boolean;
begin
Result := fHandType = hndHuman;
end;
function TKMHand.IsAnimal: Boolean;
begin
Result := fID = HAND_ANIMAL;
end;
function TKMHand.IsComputer: Boolean;
begin
Result := fHandType = hndComputer;
end;
function TKMHand.IsAdvancedAI: Boolean;
begin
Result := IsComputer and fAI.Setup.NewAI;
end;
function TKMHand.IsClassicAI: Boolean;
begin
Result := IsComputer and not fAI.Setup.NewAI;
end;
function TKMHand.CanBeAI: Boolean;
begin
Result := (fCanBeAITypes <> []) and (fCanBeAITypes <> [aitNone]);
end;
//Lay out all roads at once to save time on Terrain lighting/passability recalculations
procedure TKMHand.AfterMissionInit(aFlattenRoads: Boolean);
begin
if (Self = nil) or not Enabled then Exit;
Assert(fRoadsList <> nil);
gTerrain.SetRoads(fRoadsList, fID, not aFlattenRoads); //If we are flattening roads that will update WalkConnect anyway
if aFlattenRoads then
gTerrain.FlattenTerrain(fRoadsList);
if not gGameParams.IsMapEditor then
fAI.AfterMissionInit(fCanBeAITypes);
end;
function TKMHand.GetNextHouseWSameType(aHouseType: TKMHouseType; aStartFromUID: Integer): TKMHouse;
begin
Result := GetNextHouseWSameType(aHouseType, aStartFromUID, TKMHouseSketchEdit.DummyHouseSketch);
end;
function TKMHand.GetNextHouseWSameType(aHouseType: TKMHouseType; aStartFromUID: Integer;
out aHouseSketch: TKMHouseSketchEdit;
aSketchTypesSet: TKMHouseSketchTypeSet = [hstHouse]): TKMHouse;
begin
Result := GetNextHouseWSameType(aHouseType, aStartFromUID, aHouseSketch, aSketchTypesSet, nil, False);
end;
function TKMHand.GetNextHouseWSameType(aHouseType: TKMHouseType; aStartFromUID: Integer;
out aHouseSketch: TKMHouseSketchEdit;
aSketchTypesSet: TKMHouseSketchTypeSet;
aVerifySketch: TAnonHouseSketchBoolFn;
aVerifySketchBoolParam: Boolean): TKMHouse;
var
resultSet: Boolean;
procedure FillHSketchByHouse(out aHouseSketchTmp: TKMHouseSketchEdit; aHouse: TKMHouse);
begin
if not aHouse.IsDestroyed // not destroyed
and (aHouse.HouseType = aHouseType)
{and (not aConsiderHousePlan or aHouse.IsComplete)} then
begin
aHouseSketchTmp.Owner := fID;
aHouseSketchTmp.SetHouseUID(aHouse.UID);
aHouseSketchTmp.SetHouseType(aHouse.HouseType);
aHouseSketchTmp.SetPosition(aHouse.Position);
end;
end;
procedure FillHSketchByHPlan(out aHouseSketchTmp: TKMHouseSketchEdit; aHousePlan: TKMHousePlan);
begin
if not aHousePlan.IsEmpty
and (aHousePlan.HouseType = aHouseType) then
begin
aHouseSketchTmp.Owner := fID;
aHouseSketchTmp.SetHouseUID(aHousePlan.UID);
aHouseSketchTmp.SetHouseType(aHousePlan.HouseType);
aHouseSketchTmp.SetPosition(aHousePlan.Loc);
end;
end;
function GetNextHSketch(aIndex: Integer; out aHouseSketchTmp: TKMHouseSketchEdit): Boolean;
var
sketch2Verify: TKMHouseSketch;
begin
aHouseSketchTmp.Clear;
sketch2Verify := nil;
if (hstHouse in aSketchTypesSet) and (aIndex < fHouses.Count) then
begin
FillHSketchByHouse(aHouseSketchTmp, fHouses[aIndex]);
sketch2Verify := fHouses[aIndex];
end else if (hstHousePlan in aSketchTypesSet) then
begin
FillHSketchByHPlan(aHouseSketchTmp, fConstructions.HousePlanList.Plans[aIndex - Byte(hstHouse in aSketchTypesSet)*fHouses.Count]);
sketch2Verify := aHouseSketchTmp;
end;
Result := not aHouseSketchTmp.IsEmpty
and (not Assigned(aVerifySketch) or aVerifySketch(sketch2Verify, aVerifySketchBoolParam));
end;
procedure FillResult(aIndex: Integer; aHSketch: TKMHouseSketchEdit);
begin
resultSet := True;
if aHouseSketch <> nil then
aHSketch.CopyTo(aHouseSketch);
if aIndex < fHouses.Count then
Result := fHouses[aIndex];
end;
var
found: Boolean; //Flag when we find house sketch (House or HousePlan) with specified Starting UID
I, firstHSketchI, foundSketchI, cnt: Integer;
begin
Result := nil;
aHouseSketch.Clear;
found := False;
resultSet := False;
cnt := Byte(hstHouse in aSketchTypesSet)*fHouses.Count
+ Byte(hstHousePlan in aSketchTypesSet)*fConstructions.HousePlanList.Count;
I := 0;
firstHSketchI := 0;
foundSketchI := 0;
fHSketch.Clear;
fFirstHSketch.Clear;
fFoundHSketch.Clear;
while I < cnt do
begin
try
if not GetNextHSketch(I, fHSketch) then
Continue;
//Just find any first house
if (aStartFromUID = 0) then
begin
FillResult(I, fHSketch);
Break;
end;
//Find first house from specified UID
if (fHSketch.UID = aStartFromUID) then
begin
found := True; // Mark that we found our house
fHSketch.CopyTo(fFoundHSketch);
foundSketchI := I;
end
else if found then
begin
FillResult(I, fHSketch); // Save the next house after Found to Result and Break
Break;
end else if fFirstHSketch.IsEmpty then
begin
fHSketch.CopyTo(fFirstHSketch); // Save 1st house in list in case our house is the last one
firstHSketchI := I;
end;
finally
Inc(I);
end;
end;
if not resultSet then // Found should be always True here
begin
if found then
begin
if fFirstHSketch.IsEmpty then
FillResult(foundSketchI, fFoundHSketch) //Could happen, when we have only 1 house with that type...
else
FillResult(firstHSketchI, fFirstHSketch);
end else if not fFirstHSketch.IsEmpty then
FillResult(firstHSketchI, fFirstHSketch);
end;
end;
function TKMHand.GetNextUnitWSameType(aUnitType: TKMUnitType; aStartFromUID: Integer): TKMUnit;
var
I: Integer;
U, firstU, lastU: TKMUnit;
found: Boolean;
begin
Result := nil;
found := False;
firstU := nil;
lastU := nil;
for I := 0 to fUnits.Count - 1 do
begin
U := fUnits[I];
if (U = nil)
or U.IsDeadOrDying
or (U.UnitType <> aUnitType)
or not U.Visible then
Continue;
// Just find any first unit
if (aStartFromUID = 0) then
Exit(U);
lastU := U;
if U.UID = aStartFromUID then
found := True // Mark that we found our unit
else if found then
Exit(U) // Save the next unit after Found to Result and Break
else
if firstU = nil then
firstU := U; // Save 1st unit in list in case our unit is the last one
end;
if found then // Found should be always True here
begin
if firstU = nil then //Could happen, when we have only 1 unit with that type...
Result := lastU
else
Result := firstU;
end;
end;
function TKMHand.GetNextGroupWSameType(aUnitType: TKMUnitType; aStartFromUID: Integer): TKMUnitGroup;
var
I: Integer;
group, firstG, lastG: TKMUnitGroup;
found: Boolean;
begin
Result := nil;
found := False;
firstG := nil;
lastG := nil;
for I := 0 to UnitGroups.Count - 1 do
begin
group := UnitGroups[I];
if (group = nil)
or group.IsDead //check if group is dead
or not group.HasUnitType(aUnitType) then // we are interested in groups with the same type only
Continue;
//Just find any first house
if (aStartFromUID = 0) then
begin
Result := group;
Break;
end;
lastG := group;
if group.UID = aStartFromUID then
found := True // Mark that we found our group
else if found then
begin
Result := group; // Save the next group after Found to Result and Break
Break;
end else if firstG = nil then
firstG := group; // Save 1st group in list in case our group is the last one
end;
if (Result = nil) and found then // Found should be always True here
begin
if firstG = nil then //Could happen, when we have only 1 group with that type...
Result := lastG
else
Result := firstG;
end;
end;
procedure TKMHand.SetHandIndex(aNewIndex: TKMHandID);
begin
fID := aNewIndex;
fUnits.OwnerUpdate(aNewIndex);
fHouses.OwnerUpdate(aNewIndex);
fAI.OwnerUpdate(aNewIndex);
end;
procedure TKMHand.SetHandType(const Value: TKMHandType);
begin
fHandType := Value;
end;
procedure TKMHand.SetOverlayTextFont(aFont: TKMFont);
begin
fOverlayTextSettings.Font := aFont;
end;
procedure TKMHand.SetOverlayTextWordWrap(aWordWrap: Boolean);
begin
fOverlayTextSettings.WordWrap := aWordWrap;
end;
procedure TKMHand.SetOwnerNickname(const aName: AnsiString); //MP owner nickname (empty in SP)
begin
fOwnerNickname := aName;
end;
procedure TKMHand.AddRoad(const aLoc: TKMPoint);
begin
gTerrain.SetRoad(aLoc, fID);
end;
procedure TKMHand.AddField(const aLoc: TKMPoint; aFieldType: TKMFieldType; aStage: Byte = 0;
aKeepOldObject: Boolean = False; aRemoveOverlay: Boolean = True);
var
isFieldSet: Boolean;
obj: Word;
begin
isFieldSet := False;
obj := gTerrain.Land^[aLoc.Y,aLoc.X].Obj;
//If we have corn/wine object on that tile, set appropriate field/wine stage
if (aFieldType = ftCorn) and not gTerrain.TileIsCornField(aLoc) then
begin
if ObjectIsCorn(obj) then
begin
gTerrain.SetField(aLoc, fID, aFieldType, obj - 54, True, aKeepOldObject, aRemoveOverlay);
isFieldSet := True;
end;
end else if (aFieldType = ftWine) and not gTerrain.TileIsWineField(aLoc) then
begin
if ObjectIsWine(obj) then
begin
gTerrain.SetField(aLoc, fID, aFieldType, obj - 54, True, aKeepOldObject, aRemoveOverlay);
isFieldSet := True;
end;
end;
if not isFieldSet then
gTerrain.SetField(aLoc, fID, aFieldType, aStage, True, aKeepOldObject, aRemoveOverlay);
end;
function TKMHand.LocHasNoAllyPlans(const aLoc: TKMPoint): Boolean;
var
I: Integer;
begin
Result := True;
//Don't allow placing on allies plans either
for I := 0 to gHands.Count - 1 do
if (I <> fID) and (fAlliances[I] = atAlly) then
Result := Result and (gHands[i].fConstructions.FieldworksList.HasField(aLoc) = ftNone)
and not gHands[i].fConstructions.HousePlanList.HasPlan(aLoc);
end;
function TKMHand.GetGameFlagColor: Cardinal;
begin
Result := fFlagColor;
if gGame = nil then Exit;
case gGameSettings.PlayersColorMode of
pcmAllyEnemy: begin
if ID = gMySpectator.HandID then
Result := gGameSettings.PlayerColorSelf
else if (Alliances[gMySpectator.HandID] = atAlly) then
Result := gGameSettings.PlayerColorAlly
else
Result := gGameSettings.PlayerColorEnemy;
end;
pcmTeams: Result := fTeamColor;
end;
end;
function TKMHand.GetOwnerNicknameU: UnicodeString;
begin
Result := UnicodeString(fOwnerNickname);
end;
function TKMHand.IsDisabled: Boolean;
begin
Result := not Enabled;
end;
//See comment on CanAddFakeFieldPlan
function TKMHand.CanAddFieldPlan(const aLoc: TKMPoint; aFieldType: TKMFieldType): Boolean;
begin
Result := gTerrain.CanAddField(aLoc.X, aLoc.Y, aFieldType)
and (fConstructions.FieldworksList.HasField(aLoc) = ftNone)
and not fConstructions.HousePlanList.HasPlan(aLoc)
and LocHasNoAllyPlans(aLoc);
end;
//This differs from above only in that it uses HasFakeField instead of HasField.
//We need it because the user expects to be blocked by fake field plans, but the gameplay should not.
//When the result effects the outcome of the game, the above function should be used instead.
function TKMHand.CanAddFakeFieldPlan(const aLoc: TKMPoint; aFieldType: TKMFieldType): Boolean;
begin
Result := gTerrain.CanAddField(aLoc.X, aLoc.Y, aFieldType)
and (fConstructions.FieldworksList.HasFakeField(aLoc) = ftNone)
and not fConstructions.HousePlanList.HasPlan(aLoc)
and LocHasNoAllyPlans(aLoc);
end;
// Same as for CanAddFakeFieldPlan, but check can we delete plan
function TKMHand.CanRemFakeFieldPlan(const aLoc: TKMPoint; aFieldType: TKMFieldType): Boolean;
begin
Result := (fConstructions.FieldworksList.HasFakeField(aLoc) = aFieldType)
and LocHasNoAllyPlans(aLoc);
end;
function TKMHand.CanAddHousePlan(const aLoc: TKMPoint; aHouseType: TKMHouseType): Boolean;
var
I, K, J, S, T, Tx, Ty: Integer;
HA: TKMHouseArea;
begin
Result := gTerrain.CanPlaceHouse(aLoc, aHouseType);
if not Result then Exit;
HA := gRes.Houses[aHouseType].BuildArea;
for I := 1 to 4 do
for K := 1 to 4 do
if HA[I,K] <> 0 then
begin
Tx := aLoc.X - gRes.Houses[aHouseType].EntranceOffsetX + K - 3;
Ty := aLoc.Y + I - 4;
//AI ignores FOW (this function is used from scripting)
Result := Result and gTerrain.TileInMapCoords(Tx, Ty, 1)
and (IsComputer
or (NeedToChooseFirstStorehouseInGame and fFogOfWar.CheckTileInitialRevelation(Tx, Ty)) //Use initial revelation for first storehouse
or (not NeedToChooseFirstStorehouseInGame and (fFogOfWar.CheckTileRevelation(Tx, Ty) > 0)));
//This checks below require Tx;Ty to be within the map so exit immediately if they are not
if not Result then exit;
//This tile must not contain fields/houses of allied players or self
for J := 0 to gHands.Count - 1 do
if fAlliances[J] = atAlly then
begin
Result := Result and (gHands[J].fConstructions.FieldworksList.HasField(KMPoint(Tx,Ty)) = ftNone);
//Surrounding tiles must not be a house
for S := -1 to 1 do
for T := -1 to 1 do
Result := Result and not gHands[J].fConstructions.HousePlanList.HasPlan(KMPoint(Tx+S,Ty+T));
end;
end;
end;
function TKMHand.CanAddHousePlanAI(aX, aY: Word; aHouseType: TKMHouseType; aCheckInfluence: Boolean): Boolean;
var
I, K, J, S, T, Tx, Ty: Integer;
HA: TKMHouseArea;
enterOff: ShortInt;
terOwner: TKMHandID;
begin
Result := False;
//Check if we can place house on terrain, this also makes sure the house is
//at least 1 tile away from map border (skip that below)
if not gTerrain.CanPlaceHouse(KMPoint(aX, aY), aHouseType) then
Exit;
//Perform additional cheks for AI
HA := gRes.Houses[aHouseType].BuildArea;
enterOff := gRes.Houses[aHouseType].EntranceOffsetX;
for I := 1 to 4 do
for K := 1 to 4 do
if HA[I,K] <> 0 then
begin
Tx := aX + K - 3 - enterOff;
Ty := aY + I - 4;
//Make sure we don't block existing roads
if gTerrain.CheckPassability(KMPoint(Tx, Ty), tpWalkRoad) then
Exit;
//Check with influence maps
if aCheckInfluence and FEAT_AI_GENERATE_INFLUENCE then
begin
//Check if tile's blocked
if (gAIFields.Influences.AvoidBuilding[Ty, Tx] > 0) then
Exit;
//Check ownership for entrance (good enough since it does not changes that fast)
if (HA[I,K] = 2) then
begin
terOwner := gAIFields.Influences.GetBestOwner(Tx,Ty);
if ((terOwner <> fID) and (terOwner <> HAND_NONE)) then
Exit;
end;
end;
//Avoid placing houses in choke-points _/house\_ by checking upper corners
if not (aHouseType in [htGoldMine, htIronMine]) then
if (gTerrain.Land^[Ty-1, Tx - 1].Passability * [tpMakeRoads, tpWalkRoad] = [])
or (gTerrain.Land^[Ty-1, Tx + 1].Passability * [tpMakeRoads, tpWalkRoad] = [])
then
Exit;
//Make sure we can add road below house, full width + 1 on each side
//Terrain already checked we are 1 tile away from map edge
if (I = 4) and not (aHouseType in [htGoldMine, htIronMine]) then
if (gTerrain.Land^[Ty+1, Tx - 1].Passability * [tpMakeRoads, tpWalkRoad] = [])
or (gTerrain.Land^[Ty+1, Tx ].Passability * [tpMakeRoads, tpWalkRoad] = [])
or (gTerrain.Land^[Ty+1, Tx + 1].Passability * [tpMakeRoads, tpWalkRoad] = [])
then
Exit;
//This tile must not contain fields/houseplans of allied players or self
for J := 0 to gHands.Count - 1 do
if fAlliances[J] = atAlly then
begin
if (gHands[J].fConstructions.FieldworksList.HasField(KMPoint(Tx,Ty)) <> ftNone) then
Exit;
//Surrounding tiles must not be a house
for S := -1 to 1 do
for T := -1 to 1 do
if gHands[J].fConstructions.HousePlanList.HasPlan(KMPoint(Tx+S,Ty+T)) then
Exit;
end;
end;
Result := True;
end;
//Due to lag there could be already plans placed by user in previous ticks
//Check if Plan can be placed once again, as we might have conflicting commands caused by lag
//This is called by GIP when a place field command is processed
procedure TKMHand.ToggleFieldPlan(const aLoc: TKMPoint; aFieldType: TKMFieldType; aMakeSound: Boolean);
var
plan: TKMFieldType;
begin
Assert(aFieldType in [ftRoad, ftCorn, ftWine], 'Placing wrong FieldType');
plan := fConstructions.FieldworksList.HasField(aLoc);
if aFieldType = plan then //Same plan - remove it
RemFieldPlan(aLoc,aMakeSound)
else
if CanAddFieldPlan(aLoc, aFieldType) then
begin
if aMakeSound and not gGameParams.IsReplayOrSpectate
and (ID = gMySpectator.HandID) then
gSoundPlayer.Play(sfxPlacemarker);
fConstructions.FieldworksList.AddField(aLoc, aFieldType);
case aFieldType of
ftRoad: gScriptEvents.ProcPlanRoadPlaced(fID, aLoc.X, aLoc.Y);
ftCorn: gScriptEvents.ProcPlanFieldPlaced(fID, aLoc.X, aLoc.Y);
ftWine: gScriptEvents.ProcPlanWinefieldPlaced(fID, aLoc.X, aLoc.Y);
else
raise Exception.Create('Unknown aFieldType');
end;
end
else
begin
if aMakeSound and not gGameParams.IsReplayOrSpectate
and (ID = gMySpectator.HandID) then
gSoundPlayer.Play(sfxCantPlace, 4);
if plan = ftNone then //If we can't build because there's some other plan, that's ok
begin
//Can't build here anymore because something changed between click and command processing, so remove any fake plans
fConstructions.FieldworksList.RemFakeField(aLoc);
fConstructions.FieldworksList.RemFakeDeletedField(aLoc);
end;
end;
end;
//This procedure does not effect gameplay, it only changes fake field plans to make it look better for the user
//It is called when the user clicks to place a field plan
procedure TKMHand.ToggleFakeFieldPlan(const aLoc: TKMPoint; aFieldType: TKMFieldType);
var
plan: TKMFieldType;
begin
Assert(aFieldType in [ftRoad, ftCorn, ftWine], 'Placing wrong fake FieldType');
plan := fConstructions.FieldworksList.HasFakeField(aLoc);
if aFieldType = plan then //Same plan - remove it
begin
fConstructions.FieldworksList.RemFakeField(aLoc); //Remove our fake marker which is shown to the user
fConstructions.FieldworksList.AddFakeDeletedField(aLoc); //This will hide the real field until it is deleted from game
if ID = gMySpectator.HandID then gSoundPlayer.Play(sfxClick);
end
else
if CanAddFakeFieldPlan(aLoc, aFieldType) then
begin
fConstructions.FieldworksList.AddFakeField(aLoc, aFieldType);
if ID = gMySpectator.HandID then
gSoundPlayer.Play(sfxPlacemarker);
end
else
if ID = gMySpectator.HandID then
gSoundPlayer.Play(sfxCantPlace, 4);
end;
//Used mainly for testing purposes
{procedure TKMHand.AddRoadConnect(LocA,LocB: TKMPoint);
var
NodeList: TKMPointList;
RoadExists: Boolean;
I: Integer;
begin
NodeList := TKMPointList.Create;
try
RoadExists := fTerrain.PathFinding.Route_Make(LocA, LocB, CanMakeRoads, 0, nil, NodeList);
if RoadExists then
for I := 1 to NodeList.Count do
AddField(NodeList.List[i], ftRoad);
finally
FreeAndNil(NodeList);
end;
end;}
function TKMHand.AddHouse(aHouseType: TKMHouseType; PosX, PosY: Word; RelativeEntrace: Boolean): TKMHouse;
begin
Result := fHouses.AddHouse(aHouseType, PosX, PosY, fID, RelativeEntrace);
Result.OnDestroyed := HouseDestroyed;
end;
//Add plan of a house, house is not created until after worker flattens the terrain
procedure TKMHand.AddHousePlan(aHouseType: TKMHouseType; const aLoc: TKMPoint);
var
loc: TKMPoint;
begin
loc.X := aLoc.X - gRes.Houses[aHouseType].EntranceOffsetX;
loc.Y := aLoc.Y;
fConstructions.HousePlanList.AddPlan(aHouseType, loc);
fStats.HousePlanned(aHouseType);
gScriptEvents.EventHousePlanPlaced(fID, loc.X, loc.Y, aHouseType);
if (ID = gMySpectator.HandID) and not gGameParams.IsReplayOrSpectate then
gSoundPlayer.Play(sfxPlacemarker);
end;
function TKMHand.HasHousePlan(const aLoc: TKMPoint): Boolean;
begin
Result := fConstructions.HousePlanList.HasPlan(aLoc);
end;
function TKMHand.AddHouseWIP(aHouseType: TKMHouseType; const aLoc: TKMPoint): TKMHouse;
begin
Result := fHouses.AddHouseWIP(aHouseType, aLoc.X, aLoc.Y, fID);
Result.OnDestroyed := HouseDestroyed;
fStats.HouseStarted(aHouseType);
end;
//Player wants to remove own house
procedure TKMHand.RemHouse(const Position: TKMPoint; DoSilent: Boolean; IsEditor: Boolean = False);
var
H: TKMHouse;
begin
//Sound is handled in Demolish
H := fHouses.HitTest(Position.X, Position.Y);
if H = nil then Exit; //Due to network delays the house might have already been destroyed by now
H.Demolish(fID, IsEditor);
end;
procedure TKMHand.RemHousePlan(const Position: TKMPoint);
var
hPlan: TKMHousePlan;
begin
if not fConstructions.HousePlanList.TryGetPlan(Position, hPlan) then //Due to network delays house might not exist now
Exit;
fConstructions.HousePlanList.RemPlan(Position);
fStats.HousePlanRemoved(hPlan.HouseType);
gScriptEvents.EventHousePlanRemoved(fID, hPlan.Loc.X, hPlan.Loc.Y, hPlan.HouseType);
if (ID = gMySpectator.HandID) and not gGameParams.IsReplayOrSpectate then
gSoundPlayer.Play(sfxClick);
end;
//This is called by the GIP when an erase command is processed
procedure TKMHand.RemFieldPlan(const Position: TKMPoint; aMakeSound: Boolean);
var
fieldType: TKMFieldType;
begin
fieldType := fConstructions.FieldworksList.HasField(Position);
if fieldType = ftNone then Exit; //Can happen due to network delays
fConstructions.FieldworksList.RemFieldPlan(Position);
case fieldType of
ftRoad: gScriptEvents.ProcPlanRoadRemoved(fID, Position.X, Position.Y);
ftCorn: gScriptEvents.ProcPlanFieldRemoved(fID, Position.X, Position.Y);
ftWine: gScriptEvents.ProcPlanWinefieldRemoved(fID, Position.X, Position.Y);
else
raise Exception.Create('Unknown fieldType');
end;
if aMakeSound and not gGameParams.IsReplayOrSpectate
and (ID = gMySpectator.HandID) then
gSoundPlayer.Play(sfxClick);
end;
function TKMHand.RemGroup(const Position: TKMPoint): Boolean;
var
group: TKMUnitGroup;
begin
Assert(gGameParams.IsMapEditor);
group := fUnitGroups.HitTest(Position.X, Position.Y);
Result := group <> nil;
if Result then
fUnitGroups.RemGroup(group);
end;
//This is called immediately when the user clicks erase on a field plan.
//We know that an erase command is queued and will be processed in some ticks,
//so we AddFakeDeletedField which lets the user think the field was removed,
//while the game does not know the difference.
procedure TKMHand.RemFakeFieldPlan(const Position: TKMPoint);
begin
fConstructions.FieldworksList.RemFakeField(Position); //Remove our fake marker which is shown to the user
fConstructions.FieldworksList.AddFakeDeletedField(Position); //This will hide the real field until it is deleted from game
if ID = gMySpectator.HandID then gSoundPlayer.Play(sfxClick);
end;
function TKMHand.FindHouse(aType: TKMHouseType; const aPosition: TKMPoint; Index: Byte=1): TKMHouse;
begin
Result := fHouses.FindHouse(aType, aPosition.X, aPosition.Y, Index);
end;
// Very rough but fast way to find approximate city center
function TKMHand.FindCityCenter: TKMPoint;
const
IMPORTANT_HOUSES: array[0..4] of TKMHouseType = (htStore, htInn, htSchool, htBarracks, htTownhall);
var
I: Integer;
H: TKMHouse;
begin
for I := 0 to High(IMPORTANT_HOUSES) do
begin
H := FindHouse(IMPORTANT_HOUSES[I]);
if (H <> nil) and not H.IsDestroyed then
Exit(H.Entrance);
end;
// Very rough approach. We suggest there will be at least 1 of the important houses 99.99% of times
// Find any house then
for I := 0 to fHouses.Count - 1 do
begin
H := fHouses[I];
if (H <> nil) and not H.IsDestroyed then
Exit(H.Entrance);
end;
end;
function TKMHand.FindHouse(aType: TKMHouseType; Index: Byte=1): TKMHouse;
begin
Result := fHouses.FindHouse(aType, 0, 0, Index);
end;
function TKMHand.FindHousesInRadius(const aLoc: TKMPoint; aSqrRadius: Single; aTypes: TKMHouseTypeSet = HOUSES_VALID; aOnlyCompleted: Boolean = True): TKMHouseArray;
begin
Result := fHouses.FindHousesInRadius(aLoc, aSqrRadius, aTypes, aOnlyCompleted);
end;
function TKMHand.FindInn(Loc: TKMPoint; aUnit: TKMUnit; UnitIsAtHome: Boolean = False): TKMHouseInn;
var
H: TKMHouseInn;
I: Integer;
dist, bestMatch: Single;
begin
//This function will return the best inn for a unit at Loc, base on distance, food available and space available.
//Will return nil if no suitable inn is available
Result := nil;
I := 1;
bestMatch := MaxSingle;
if UnitIsAtHome then Inc(Loc.Y); //From outside the door of the house
H := TKMHouseInn(FindHouse(htInn));
repeat
//First make sure that it is valid
if (H <> nil) and H.HasFood and H.HasSpace
and aUnit.CanWalkTo(Loc, H.PointBelowEntrance, tpWalk, 0) then
begin
//Take the closest inn out of the ones that are suitable
dist := KMLengthSqr(H.Position, Loc);
if dist < bestMatch then
begin
Result := H;
bestMatch := dist;
end;
end;
inc(I);
H := TKMHouseInn(FindHouse(htInn, I));
until(H = nil);
end;
//Does the player has any assets (without assets player is harmless)
function TKMHand.HasAssets: Boolean;
begin
if Self = nil then Exit(False);
Result := (Houses.Count > 0) or (Units.Count > 0) or (GetFieldsCount > 0)
or NeedToChooseFirstStorehouse; //RMG - added ChooseLocation.Allowed option as a valid player
end;
// usually AIType is added by mission parser
procedure TKMHand.AddAIType(aHandAIType: TKMAIType);
begin
Include(fCanBeAITypes, aHandAIType);
AI.CanBeAITypes := fCanBeAITypes;
end;
procedure TKMHand.PostLoadMission;
var
I: Integer;
begin
if (Self = nil) or not Enabled then Exit;
for I := 0 to fHouses.Count - 1 do
fHouses[I].PostLoadMission;
end;
function TKMHand.HitTest(X, Y: Integer): TObject;
var
H: TKMHouse;
U: TKMUnit;
G: TKMUnitGroup;
begin
//Houses have priority over units, so you can't select an occupant
//Selection priority is as follows:
//BuiltHouses > UnitGroups > Units > IncompleteHouses
H := HousesHitTest(X,Y);
if (H <> nil) and (H.BuildingState in [hbsStone, hbsDone]) then
Result := H
else
begin
G := GroupsHitTest(X,Y);
if (G <> nil) then
Result := G
else
begin
U := UnitsHitTest(X,Y);
if (U <> nil) and (not U.IsDeadOrDying) then
Result := U
else
Result := H; //Incomplete house or nil
end;
end;
end;
// Some entity was destroyed, but gMySpectator could have pointer to this entity, we have to nil it then
procedure TKMHand.EntityDestroyed(aEntity: TKMHandEntity);
begin
//gMySpectator is nil during loading, when houses can be destroyed at the start
if gMySpectator = nil then Exit;
if gMySpectator.Selected = aEntity then
gMySpectator.Selected := nil;
if gMySpectator.LastSelected = aEntity then
gMySpectator.NilLastSelected;
if gMySpectator.Highlight = aEntity then
gMySpectator.Highlight := nil;
if gMySpectator.HighlightDebug.Entity = aEntity then
gMySpectator.HighlightDebug.Reset;
if gMySpectator.HighlightDebug2.Entity = aEntity then
gMySpectator.HighlightDebug2.Reset;
if gMySpectator.HighlightDebug3.Entity = aEntity then
gMySpectator.HighlightDebug3.Reset;
if gMySpectator.HighlightRoute.Entity = aEntity then
gMySpectator.HighlightRoute.Reset;
end;
//Which house whas destroyed and by whom
procedure TKMHand.HouseDestroyed(aHouse: TKMHouse; aFrom: TKMHandID);
begin
//Dispose of delivery tasks performed in DeliverQueue unit
if aHouse.BuildingState in [hbsWood .. hbsDone] then
begin
Deliveries.Queue.RemAllOffers(aHouse);
Deliveries.Queue.RemDemand(aHouse);
end;
//Only Done houses are treated as Self-Destruct, Lost, Destroyed
if aHouse.BuildingState in [hbsNoGlyph .. hbsStone] then
begin
fStats.HouseEnded(aHouse.HouseType);
// Mark houses BuildEnded only for houses where ready to build
if aHouse.IsReadyToBeBuilt then
fStats.HouseBuildEnded(aHouse.HouseType);
end
else
begin
//We have to consider destroyed closed house as actually opened, otherwise closed houses stats will be corrupted
if aHouse.IsClosedForWorker and not gGameParams.IsMapEditor then
fStats.HouseClosed(False, aHouse.HouseType);
//Distribute honors
if aFrom = fID then
fStats.HouseSelfDestruct(aHouse.HouseType)
else
begin
Stats.HouseLost(aHouse.HouseType);
if aFrom <> HAND_NONE then
gHands[aFrom].Stats.HouseDestroyed(aHouse.HouseType);
end;
end;
//Scripting events happen AFTER updating statistics
gScriptEvents.ProcHouseDestroyed(aHouse, aFrom);
EntityDestroyed(aHouse);
end;
function TKMHand.HousesHitTest(X, Y: Integer): TKMHouse;
begin
Result:= fHouses.HitTest(X, Y);
end;
function TKMHand.GroupsHitTest(X, Y: Integer): TKMUnitGroup;
begin
Result:= fUnitGroups.HitTest(X, Y);
end;
function TKMHand.ObjectByUID(aUID: Integer): TObject;
var
I: Integer;
begin
Result := nil;
for I := 0 to Units.Count - 1 do
if aUID = Units[I].UID then
begin
Result := Units[I];
Exit;
end;
end;
function TKMHand.GetColorIndex: Byte;
var
I: Integer;
begin
Result := 3; //3 = Black which can be the default when a non-palette 32 bit color value is used
for I := 0 to 255 do
if gRes.Palettes.DefaultPalette.Color32(I) = fFlagColor then
Result := I;
end;
function TKMHand.GetDeliveries: TKMHandLogistics;
begin
if Self = nil then Exit(nil);
Result := fDeliveries;
end;
function TKMHand.CalcOwnerName: UnicodeString;
var
numberedAIs: Boolean;
begin
numberedAIs := not gGameParams.IsSingleplayer;
//Default names
if IsHuman then
Result := gResTexts[TX_PLAYER_YOU]
else
if AI.Setup.NewAI then
begin
if numberedAIs then
Result := Format(gResTexts[TX_ADVANCED_AI_PLAYER_SHORT_X], [fID + 1])
else
Result := gResTexts[TX_AI_PLAYER_ADVANCED_SHORT];
end else begin
if numberedAIs then
Result := Format(gResTexts[TX_CLASSIC_AI_PLAYER_SHORT_X], [fID + 1])
else
Result := gResTexts[TX_AI_PLAYER_CLASSIC_SHORT];
end;
//Try to take player name from mission text if we are in SP
//Do not use names in MP to avoid confusion of AI players with real player nicknames
if gGameParams.Mode in [gmSingle, gmCampaign, gmMapEd, gmReplaySingle] then
if gGame.TextMission.HasHandNameText(fID) then
if IsHuman then
Result := gResTexts[TX_PLAYER_YOU] + ' (' + gGame.TextMission.GetHandNameText(fID) + ')'
else
Result := gGame.TextMission.GetHandNameText(fID);
//If this location is controlled by an MP player - show his nik
if (fOwnerNickname <> '') and IsHuman then
Result := UnicodeString(fOwnerNickname);
end;
function TKMHand.OwnerName(aNumberedAIs: Boolean = True; aLocalized: Boolean = True): UnicodeString;
begin
Result := GetHandOwnerName(IsHuman, AI.Setup.NewAI, aNumberedAIs, aLocalized);
end;
function TKMHand.GetHandOwnerName(aIsHuman, aIsAdvAI: Boolean; aNumberedAIs: Boolean = True; aLocalized: Boolean = True): UnicodeString;
function GetText(aId: Word; aLocalized: Boolean): UnicodeString; inline;
begin
if aLocalized then
Result := gResTexts[aId]
else
Result := gResTexts.DefaultTexts[aId];
end;
begin
//If this location is controlled by an MP player - show his nik
if (fOwnerNickname <> '')
and aIsHuman then //we could ask AI to play on ex human loc, so fOwnerNickname will be still some human name
Exit(UnicodeString(fOwnerNickname));
//Try to take player name from mission text if we are in SP
//Do not use names in MP to avoid confusion of AI players with real player nicknames
if (gGameParams.Mode in [gmSingle, gmCampaign, gmMapEd, gmReplaySingle])
and gGame.TextMission.HasHandNameText(fID) then
begin
if aIsHuman then
Result := GetText(TX_PLAYER_YOU, aLocalized) + ' (' + gGame.TextMission.GetHandNameText(fID) + ')'
else
Result := gGame.TextMission.GetHandNameText(fID);
Exit;
end;
//Default names
if aIsHuman then
Result := GetText(TX_PLAYER_YOU, aLocalized)
else
if aIsAdvAI then
begin
if aNumberedAIs then
Result := Format(GetText(TX_ADVANCED_AI_PLAYER_SHORT_X, aLocalized), [fID + 1])
else
Result := GetText(TX_AI_PLAYER_ADVANCED_SHORT, aLocalized);
end else begin
if aNumberedAIs then
Result := Format(GetText(TX_CLASSIC_AI_PLAYER_SHORT_X, aLocalized), [fID + 1])
else
Result := GetText(TX_AI_PLAYER_CLASSIC_SHORT, aLocalized);
end;
end;
function TKMHand.GetOwnerName: UnicodeString;
begin
Result := OwnerName(not gGameParams.IsSingleplayer);
end;
function TKMHand.GetOwnerNameColored: AnsiString;
begin
Result := WrapColorA(AnsiString(GetOwnerName), FlagColorToTextColor(FlagColor));
end;
function TKMHand.GetOwnerNameColoredU: UnicodeString;
begin
Result := WrapColor(GetOwnerName, FlagColorToTextColor(FlagColor));
end;
function TKMHand.GetAI: TKMHandAI;
begin
if Self = nil then Exit(nil);
Result := fAI;
end;
function TKMHand.GetAlliances(aIndex: Integer): TKMAllianceType;
begin
Result := fAlliances[aIndex];
end;
procedure TKMHand.SetAlliances(aIndex: Integer; aValue: TKMAllianceType);
begin
if Self = nil then Exit;
fAlliances[aIndex] := aValue;
gAIFields.Supervisor.UpdateAlliances();
if Assigned(fOnAllianceChange) then
fOnAllianceChange;
end;
procedure TKMHand.SetEnabled(const Value: Boolean);
begin
fEnabled := Value;
end;
procedure TKMHand.SetFlagColor(const Value: Cardinal);
begin
if Self = nil then Exit;
fFlagColor := Value;
end;
function TKMHand.GetShareFOW(aIndex: Integer): Boolean;
begin
Result := fShareFOW[aIndex];
end;
procedure TKMHand.SetShareFOW(aIndex: Integer; aValue: Boolean);
begin
if Self = nil then Exit;
fShareFOW[aIndex] := aValue;
end;
function TKMHand.GetShareBeacons(aIndex: Integer): Boolean;
begin
Result := fShareBeacons[aIndex];
end;
procedure TKMHand.SetShareBeacons(aIndex: Integer; aValue: Boolean);
begin
if Self = nil then Exit;
fShareBeacons[aIndex] := aValue;
end;
{ See if player owns any Fields/Roads/Walls (has any assets on Terrain)
If Player has none and no Units/Houses we can assume it's empty and does not needs to be saved
Queried by MapEditor.SaveDAT;
Might also be used to show Players strength (or builder/warrior balance) in Tavern }
function TKMHand.GetFieldsCount: Integer;
var
I, K: Integer;
begin
Result := 0;
for I := 1 to gTerrain.MapY do
for K := 1 to gTerrain.MapX do
if gTerrain.Land^[I,K].TileOwner = fID then
Inc(Result);
end;
function TKMHand.GetFlagTextColor: Cardinal;
begin
Result := FlagColorToTextColor(fFlagColor);
end;
procedure TKMHand.GetFieldPlans(aList: TKMPointTagList; const aRect: TKMRect; aIncludeFake: Boolean);
var
I: TKMHandID;
begin
//Include self and allies
for I := 0 to gHands.Count - 1 do
if gHands[fID].Alliances[I] = atAlly then
gHands[I].Constructions.FieldworksList.GetFields(aList, aRect, aIncludeFake);
end;
procedure TKMHand.GetHousePlans(aList: TKMPointDirList; const aRect: TKMRect);
var
I: TKMHandID;
begin
//Include self and allies
for I := 0 to gHands.Count - 1 do
if gHands[fID].Alliances[I] = atAlly then
gHands[I].Constructions.HousePlanList.GetOutlines(aList, aRect);
end;
procedure TKMHand.GetPlansTablets(aList: TKMPointTagList; const aRect: TKMRect);
var
I: TKMHandID;
begin
//Include self and allies
for I := 0 to gHands.Count - 1 do
if gHands[fID].Alliances[I] = atAlly then
gHands[I].Constructions.HousePlanList.GetTablets(aList, aRect);
end;
procedure TKMHand.GetHouseMarks(const aLoc: TKMPoint; aHouseType: TKMHouseType; aList: TKMPointTagList; aIgnoreFOW: Boolean = False);
//Replace existing icon with a Block
procedure BlockPoint(const aPoint: TKMPoint; aID: Integer);
var I: Integer;
begin
//Remove all existing marks on this tile (entrance can have 2 entries)
for I := aList.Count - 1 downto 0 do
if KMSamePoint(aList[I], aPoint) then
aList.Remove(aPoint);
aList.Add(aPoint, aID);
end;
var
I, K, J, S, T: Integer;
P2: TKMPoint;
allowBuild: Boolean;
HA: TKMHouseArea;
begin
//Get basic Marks
gTerrain.GetHouseMarks(aLoc, aHouseType, aList);
//Override marks if there are House/FieldPlans (only we know about our plans) and or FogOfWar
HA := gRes.Houses[aHouseType].BuildArea;
for I := 1 to 4 do
for K := 1 to 4 do
if (HA[I,K] <> 0)
and gTerrain.TileInMapCoords(aLoc.X + K - 3 - gRes.Houses[aHouseType].EntranceOffsetX, aLoc.Y + I - 4, 1) then
begin
//This can't be done earlier since values can be off-map
P2 := KMPoint(aLoc.X + K - 3 - gRes.Houses[aHouseType].EntranceOffsetX, aLoc.Y + I - 4);
//Forbid planning on unrevealed areas and fieldplans
allowBuild := aIgnoreFOW
or (NeedToChooseFirstStorehouseInGame and fFogOfWar.CheckTileInitialRevelation(P2.X, P2.Y)) //Use initial revelation for first storehouse
or (not NeedToChooseFirstStorehouseInGame and (fFogOfWar.CheckTileRevelation(P2.X, P2.Y) > 0));
//This tile must not contain fields/houses of allied players or self
if allowBuild then
for J := 0 to gHands.Count - 1 do
if (gHands[fID].Alliances[J] = atAlly)
and ((gHands[J].fConstructions.FieldworksList.HasField(P2) <> ftNone)
or gHands[J].fConstructions.HousePlanList.HasPlan(P2)) then
allowBuild := False;
//Check surrounding tiles in +/- 1 range for other houses pressence
for S := -1 to 1 do
for T := -1 to 1 do
if (S <> 0) or (T <> 0) then //This is a surrounding tile, not the actual tile
for J := 0 to gHands.Count - 1 do
if (gHands[fID].Alliances[J] = atAlly)
and gHands[J].fConstructions.HousePlanList.HasPlan(KMPoint(P2.X + S, P2.Y + T)) then
begin
BlockPoint(KMPoint(P2.X + S, P2.Y + T), TC_BLOCK); //Block surrounding points
allowBuild := False;
end;
//Mark the tile according to previous check results
if not allowBuild then
if HA[I,K] = 2 then
BlockPoint(P2, TC_BLOCK_ENTRANCE)
else
if aHouseType in [htGoldMine, htIronMine] then
BlockPoint(P2, TC_BLOCK_MINE)
else
BlockPoint(P2, TC_BLOCK);
end;
end;
procedure TKMHand.Save(SaveStream: TKMemoryStream);
begin
SaveStream.PlaceMarker('Hand');
SaveStream.Write(fEnabled);
SaveStream.Write(InCinematic);
if not Enabled then Exit;
inherited;
fAI.Save(SaveStream);
fConstructions.Save(SaveStream);
fDeliveries.Save(SaveStream);
fFogOfWar.Save(SaveStream);
fHouses.Save(SaveStream);
fLocks.Save(SaveStream);
fStats.Save(SaveStream);
fUnitGroups.Save(SaveStream);
fMessageLog.Save(SaveStream);
SaveStream.Write(fID);
SaveStream.WriteA(fOwnerNickname);
SaveStream.Write(fHandType, SizeOf(fHandType));
SaveStream.Write(fCanBeHuman, SizeOf(fCanBeHuman));
SaveStream.Write(fCanBeAITypes, SizeOf(fCanBeAITypes));
SaveStream.Write(fAlliances, SizeOf(fAlliances));
SaveStream.Write(fShareFOW, SizeOf(fShareFOW));
SaveStream.Write(fShareBeacons, SizeOf(fShareBeacons));
SaveStream.Write(fCenterScreen);
SaveStream.Write(fFlagColor);
SaveStream.Write(fTeam);
SaveStream.Write(SelectionHotkeys, SizeOf(SelectionHotkeys));
SaveStream.Write(fChooseLocation, SizeOf(TKMChooseLoc));
// Overlay
SaveStream.WriteA(fOverlayMarkup);
SaveStream.Write(fOverlayTextSettings, SizeOf(fOverlayTextSettings));
fOverlayParams.Save(SaveStream);
end;
procedure TKMHand.Load(LoadStream: TKMemoryStream);
begin
LoadStream.CheckMarker('Hand');
LoadStream.Read(fEnabled);
LoadStream.Read(InCinematic);
if not Enabled then Exit;
inherited;
fAI.Load(LoadStream);
fConstructions.Load(LoadStream);
fDeliveries.Load(LoadStream);
fFogOfWar.Load(LoadStream);
fHouses.Load(LoadStream);
fLocks.Load(LoadStream);
fStats.Load(LoadStream);
fUnitGroups.Load(LoadStream);
fMessageLog.Load(LoadStream);
LoadStream.Read(fID);
LoadStream.ReadA(fOwnerNickname);
LoadStream.Read(fHandType, SizeOf(fHandType));
LoadStream.Read(fCanBeHuman, SizeOf(fCanBeHuman));
LoadStream.Read(fCanBeAITypes, SizeOf(fCanBeAITypes));
LoadStream.Read(fAlliances, SizeOf(fAlliances));
LoadStream.Read(fShareFOW, SizeOf(fShareFOW));
LoadStream.Read(fShareBeacons, SizeOf(fShareBeacons));
LoadStream.Read(fCenterScreen);
LoadStream.Read(fFlagColor);
LoadStream.Read(fTeam);
LoadStream.Read(SelectionHotkeys, SizeOf(SelectionHotkeys));
LoadStream.Read(fChooseLocation, SizeOf(TKMChooseLoc));
// Overlay
LoadStream.ReadA(fOverlayMarkup);
LoadStream.Read(fOverlayTextSettings, SizeOf(fOverlayTextSettings));
fOverlayParams.Clear;
fOverlayParams.Load(LoadStream);
end;
procedure TKMHand.SyncLoad;
var
I: Integer;
begin
if not Enabled then Exit;
inherited;
//Assign event handler after load
for I := 0 to fUnits.Count - 1 do
begin
fUnits[I].OnUnitDied := UnitDied;
fUnits[I].OnUnitTrained := UnitTrained;
if fUnits[I] is TKMUnitWarrior then
TKMUnitWarrior(fUnits[I]).OnWarriorWalkOut := WarriorWalkedOut;
end;
fUnitGroups.SyncLoad;
//Assign event handler after load
for I := 0 to fUnitGroups.Count - 1 do
fUnitGroups[I].OnGroupDied := GroupDied;
fHouses.SyncLoad;
//Assign event handler after load
for I := 0 to fHouses.Count - 1 do
fHouses[I].OnDestroyed := HouseDestroyed;
fDeliveries.SyncLoad;
fConstructions.SyncLoad;
fAI.SyncLoad;
end;
procedure TKMHand.IncAnimStep;
begin
if not Enabled then Exit;
fHouses.IncAnimStep;
end;
procedure TKMHand.UnitDied(aUnit: TKMUnit);
begin
Stats.UnitLost(aUnit.UnitType);
if aUnit.KilledBy <> HAND_NONE then
gHands[aUnit.KilledBy].Stats.UnitKilled(aUnit.UnitType);
//Demands: food for soldiers / stone or wood for workers
Deliveries.Queue.RemDemand(aUnit);
if aUnit is TKMUnitWarrior then
AI.General.WarriorDied(TKMUnitWarrior(aUnit));
//Call script event after updating statistics
gScriptEvents.ProcUnitDied(aUnit, aUnit.KilledBy);
EntityDestroyed(aUnit);
end;
procedure TKMHand.GroupDied(aGroup: TKMUnitGroup);
begin
EntityDestroyed(aGroup);
end;
procedure TKMHand.UpdateState(aTick: Cardinal);
begin
if not Enabled then Exit;
//Player has to place first storehouse at some point or will be defeated
if NeedToChooseFirstStorehouse and (aTick > TIME_TO_SET_FIRST_STOREHOUSE) then
AI.Defeat;
//Update Groups logic before Units
fUnitGroups.UpdateState;
inherited;
fHouses.UpdateState(aTick);
fFogOfWar.UpdateState(gGameParams.DynamicFOW); //We might optimize it for AI somehow, to make it work coarse and faster
//Distribute AI updates among different Ticks to avoid slowdowns
if (aTick mod gHands.Count) = fID then
begin
fConstructions.UpdateState;
fDeliveries.UpdateState(aTick);
end;
//AI update takes care of it's own interleaving, so run it every tick
fAI.UpdateState(aTick, gHands.DoCheckGoals);
//if (aTick + Byte(fPlayerIndex)) mod 20 = 0 then
//fArmyEval.UpdateState;
if CanDoStatsUpdate(aTick) then
fStats.UpdateState;
if not gGameParams.IsMapEditor //Do not place first storehouse in map editor etc
and fChooseLocation.Allowed
and not fChooseLocation.Placed then
ChooseFirstStorehouse();
end;
function TKMHand.NeedToChooseFirstStorehouse: Boolean;
begin
Result := fChooseLocation.Allowed and not fChooseLocation.Placed;
end;
function TKMHand.NeedToChooseFirstStorehouseInGame: Boolean;
begin
Result := not gGameParams.IsMapEditor and NeedToChooseFirstStorehouse;
end;
procedure TKMHand.ChooseFirstStorehouse();
var
I: Integer;
entrance: TKMPoint;
begin
if IsComputer then
fChooseLocation.Placed := True
// Check if storehouse has been placed
else
if (Stats.GetHouseTotal(htStore) > 0) then
begin
for I := 0 to fConstructions.HousePlanList.Count - 1 do
with fConstructions.HousePlanList.Plans[I] do
if (HouseType = htStore) then
begin
entrance := KMPointAdd( Loc, KMPoint(gRes.Houses[HouseType].EntranceOffsetX,0) );
RemHousePlan(entrance);
if CanAddFieldPlan(KMPoint(entrance.X, entrance.Y+1), ftRoad) then
AddFirstStorehouse(entrance);
end;
end;
// Preselect storehouse
if not gGameParams.IsReplayOrSpectate
and (gMySpectator.HandID = ID)
and not fChooseLocation.Placed then
begin
gCursor.Mode := cmHouses;
gCursor.Tag1 := Byte(htStore);
end;
end;
procedure TKMHand.AddFirstStorehouse(aEntrance: TKMPoint);
// Place road and return True if it is possible
function AddRoad(aPoint: TKMPoint): Boolean;
begin
Result := CanAddFieldPlan(KMPoint(aPoint.X, aPoint.Y), ftRoad);
if Result then
begin
gTerrain.SetRoad(aPoint, fID);
//Terrain under roads is flattened (fields are not)
gTerrain.FlattenTerrain(aPoint);
if gMapElements[gTerrain.Land^[aPoint.Y,aPoint.X].Obj].WineOrCorn then
gTerrain.RemoveObject(aPoint);
end;
end;
var
I: Integer;
H: TKMHouse;
WT: TKMWareType;
UT: TKMUnitType;
begin
// Add Storehouse
H := AddHouse(htStore, aEntrance.X, aEntrance.Y, True);
// Add Wares
for WT := Low(fChooseLocation.Resources) to High(fChooseLocation.Resources) do
if H.WareCanAddToIn(WT) OR H.WareCanAddToOut(WT) then
begin
Stats.WareInitial(WT, fChooseLocation.Resources[WT]);
H.WareAddToEitherFromScript(WT, fChooseLocation.Resources[WT]);
end;
// Add Roads
AddRoad( KMPoint(aEntrance.X, aEntrance.Y+1) );
AddRoad( KMPoint(aEntrance.X-1,aEntrance.Y+1) );
AddRoad( KMPoint(aEntrance.X+1,aEntrance.Y+1) );
// Add Units
for UT := Low(fChooseLocation.Units) to High(fChooseLocation.Units) do
for I := 0 to fChooseLocation.Units[UT] - 1 do
AddUnit(UT, KMPoint(aEntrance.X,aEntrance.Y+1));
// Finish action
fChooseLocation.Placed := True;
gCursor.Mode := cmNone; // Reset cursor
end;
function TKMHand.CanDoStatsUpdate(aTick: Cardinal): Boolean;
begin
Result := (aTick mod GetStatsUpdatePeriod = 0) or (aTick = 1);
end;
function TKMHand.DoCheckGoals: Boolean;
begin
Result := not fChooseLocation.Allowed
or (fChooseLocation.Allowed and fChooseLocation.Placed);
end;
procedure TKMHand.Paint(const aRect: TKMRect; aTickLag: Single);
begin
if not Enabled then Exit;
inherited;
if mlUnits in gGameParams.VisibleLayers then
fUnitGroups.Paint(aRect, aTickLag);
if mlHouses in gGameParams.VisibleLayers then
fHouses.Paint(aRect);
if not SKIP_RENDER AND OVERLAY_DEFENCES AND not fAI.Setup.NewAI then
if gMySpectator.SelectedHandID = fID then
fAI.General.DefencePositions.Paint;
if not SKIP_RENDER AND fAI.Setup.NewAI then
begin
if OVERLAY_AI_BUILD then
begin
fAI.CityManagement.Builder.Paint();
fAI.CityManagement.Builder.Planner.Paint();
end;
if OVERLAY_AI_COMBAT then
fAI.ArmyManagement.Paint();
end;
end;
function TKMHand.ObjToString(aSeparator: String = ' '): String;
begin
Result := Format('Enabled = %5s%sID = %d%sAI: [%s]%sOwner = %s%sHandType = %s',
[BoolToStr(Enabled, True), aSeparator,
fID, aSeparator,
AI.ObjToString, aSeparator,
OwnerName, aSeparator,
GetEnumName(TypeInfo(TKMHandType), Integer(fHandType))]);
end;
{ TKMHandAnimals }
function TKMHandAnimals.GetFishInWaterBody(aWaterID: Byte; FindHighestCount: Boolean = True): TKMUnitFish;
var
I, highestGroupCount: Integer;
U: TKMUnit;
begin
Result := nil;
if aWaterID = 0 then Exit; //Fish should always be in valid water
highestGroupCount := 0;
for I := 0 to fUnits.Count - 1 do
begin
U := fUnits[I]; //Store locally
if (U <> nil)
and (U.UnitType = utFish)
and not U.IsDeadOrDying //Fish are killed when they are caught or become stuck
and (gTerrain.Land^[U.Position.Y, U.Position.X].WalkConnect[wcFish] = aWaterID)
and (TKMUnitFish(U).FishCount > highestGroupCount) then
begin
Result := TKMUnitFish(U);
//This is for time saving when we don't actually care which group is returned
if not FindHighestCount then Exit;
highestGroupCount := Result.FishCount;
end;
end;
end;
//-----------
function GetStatsUpdatePeriod: Integer;
begin
Result := 1000;
case gGameParams.MissionMode of
mmBuilding: Result := CHARTS_SAMPLING_FOR_ECONOMY;
mmFighting: Result := CHARTS_SAMPLING_FOR_TACTICS;
end;
end;
end.