sync: restore source from Google Drive
Sync changed files to Google Drive / sync (push) Successful in 1h45m13s

This commit is contained in:
admin_d committed 2026-09-12 16:50:30 +05:00
1 parent e2d8247e91
commit 7063d1b1c9
993 files changed
+949976

No files matched your search

File diff suppressed because it is too large. Load diff
+37
View File
@@ -0,0 +1,37 @@
unit KM_UnitGroupTypes;
interface
uses
KM_Points;
type
// Unit Group types
TKMGroupInitialOrder = (gioNoOrder, gioSendGroup, gioAttackPosition);
TKMOrderWalkKind = (
wtokNone,
wtokPlayerOrder, //player order
wtokHaltOrder, //could be player order, or AI order (as part of OrderRepeat)
wtokFlagPoint, //after warrior get out of barracks/townhall
wtokAISquad, wtokAIGotoDefencePos, wtokAIAttackCustomPos, //initiated by AI
wtokMissionScript, wtokScript); //initiated by mission script or script
TKMOrderWalkKindSet = set of TKMOrderWalkKind;
//* Group order
TKMGroupOrder = (
goNone, // Last order was executed and now we have nothing to do
goWalkTo, // Ordered to walk somewhere or just change formation
goAttackHouse, // Attack house
goAttackUnit, // Attack specific unit
goStorm // Run forward
);
//MapEd allows to set order for a group that will be executed on mission start
TKMMapEdOrder = record
Order: TKMGroupInitialOrder;
Pos: TKMPointDir;
end;
implementation
end.
+177
View File
@@ -0,0 +1,177 @@
unit KM_UnitVisual;
{$I KaM_Remake.inc}
interface
uses
KM_ResTypes, KM_Points, KM_Defaults;
type
TKMUnitVisualState = record
public
PositionF: TKMPointF; // Precise unit position
Dir: TKMDirection;
SlideX, SlideY: Single;
Action: TKMUnitActionType;
IsActGoInOutStarted: Boolean;
InHouseType: TKMHouseType;
AnimStep: Integer;
AnimFraction: Single;
procedure SetFromUnit(aUnit: TObject);
end;
// Purely visual thing. Split from TKMUnit to aviod mixup of game-logic and render Positions
TKMUnitVisual = class
private
fUnit: TObject;
fCurr: TKMUnitVisualState;
fPrev: TKMUnitVisualState;
fPrevPrev: TKMUnitVisualState;
public
constructor Create(aUnit: TObject);
function GetLerp(aLag: Single): TKMUnitVisualState;
procedure UpdateState;
end;
implementation
uses
Math, SysUtils,
KromUtils,
KM_Units, KM_UnitActionGoInOut,
KM_Resource, KM_ResUnits, KM_ResHouses;
{ TKMUnitVisualState }
procedure TKMUnitVisualState.SetFromUnit(aUnit: TObject);
var
U: TKMUnit;
slide: TKMPointF;
begin
U := TKMUnit(aUnit);
PositionF := U.PositionF;
Dir := U.Direction;
slide := U.GetSlides;
SlideX := slide.X;
SlideY := slide.Y;
AnimStep := U.AnimStep;
AnimFraction := 0.0;
IsActGoInOutStarted := False;
if U.Action <> nil then
begin
Action := U.Action.ActionType;
if (U.Action is TKMUnitActionGoInOut)
and TKMUnitActionGoInOut(U.Action).IsStarted then
IsActGoInOutStarted := True;
end
else
Action := uaUnknown;
if U.InHouse <> nil then
InHouseType := U.InHouse.HouseType
else
InHouseType := htNone;
end;
{ TKMUnitVisual }
constructor TKMUnitVisual.Create(aUnit: TObject);
begin
inherited Create;
fUnit := TKMUnit(aUnit);
fPrev.SetFromUnit(fUnit);
fCurr.SetFromUnit(fUnit);
end;
function TKMUnitVisual.GetLerp(aLag: Single): TKMUnitVisualState;
var
animCount: Integer;
prevSlideX, prevSlideY: Single;
begin
prevSlideX := fPrev.SlideX;
prevSlideY := fPrev.SlideY;
// Special case for a unit who just started exiting the house
// In that case fPrev slide was not calculated with door slide consideration
// and thus fPrev.Slide would be 0 in the most cases (or in all cases)
// That will make unit 'jump' from fPrev.PositionF to fCurr.PositionF + fCurr.Slide, which looks very bad
if not fPrev.IsActGoInOutStarted
and fCurr.IsActGoInOutStarted
and (fPrev.InHouseType <> htNone) then
begin
// Just add doorway offset to the fPrev.Slide then
prevSlideX := prevSlideX + gRes.Houses[fPrev.InHouseType].GetDoorwayOffset(axX);
prevSlideY := prevSlideY + gRes.Houses[fPrev.InHouseType].GetDoorwayOffset(axY);
end;
Result.PositionF := KMLerp(fCurr.PositionF, fPrev.PositionF, aLag);
Result.SlideX := KromUtils.Lerp(fCurr.SlideX, prevSlideX, aLag);
Result.SlideY := KromUtils.Lerp(fCurr.SlideY, prevSlideY, aLag);
//If there's no lag, use the current state
if aLag = 0.0 then
begin
Result.Dir := fCurr.Dir;
Result.Action := fCurr.Action;
Result.AnimStep := fCurr.AnimStep;
Result.AnimFraction := 0.0;
end
else
begin
//Are we moving?
if fCurr.PositionF <> fPrev.PositionF then
begin
//Always interpolate the animation if the unit is moving
Result.AnimFraction := 1.0 - aLag;
//If we were still and just started moving
if fPrevPrev.PositionF = fPrev.PositionF then
begin
//Since the unit starts moving without warning we need to backwards interpolate
Result.Dir := fCurr.Dir;
Result.Action := fCurr.Action;
//Find the previous anim step
if fCurr.AnimStep = 0 then
begin
animCount := gRes.Units[TKMUnit(fUnit).UnitType].UnitAnim[Result.Action, Result.Dir].Count;
Result.AnimStep := animCount - 1;
end
else
Result.AnimStep := fCurr.AnimStep - 1;
end
else
begin
//Don't start a new action or change direction until the last one is 100% finished
Result.Dir := fPrev.Dir;
Result.Action := fPrev.Action;
Result.AnimStep := fPrev.AnimStep;
end;
end
else
begin
//Not moving. Don't start a new action or change direction until the last one is 100% finished
Result.Dir := fPrev.Dir;
Result.Action := fPrev.Action;
Result.AnimStep := fPrev.AnimStep;
//If action/dir/step is consistent we can interpolate the animation
if (fCurr.Action = fPrev.Action) and (fCurr.Dir = fPrev.Dir) and (fCurr.AnimStep - fPrev.AnimStep = 1) then
Result.AnimFraction := 1.0 - aLag
else
Result.AnimFraction := 0.0;
end;
end;
end;
procedure TKMUnitVisual.UpdateState;
begin
fPrevPrev := fPrev;
fPrev := fCurr;
fCurr.SetFromUnit(fUnit);
end;
end.
File diff suppressed because it is too large. Load diff
+690
View File
@@ -0,0 +1,690 @@
unit KM_UnitWorkPlan;
{$I KaM_Remake.inc}
interface
uses
KM_Defaults, KM_CommonClasses, KM_Points, KM_TerrainTypes, KM_Units,
KM_ResTypes;
const
MAX_WORKPLAN = 24;
type
TKMWorkPlanAllowedEvent = function(aProduct: TKMWareType): Boolean of object;
TKMUnitWorkPlan = class
private
fHome: TKMHouseType;
fIssued: Boolean;
function ChooseTree(const aLoc, aAvoid: TKMPoint; aRadius: Integer; aPlantAct: TKMPlantAct; aUnit: TKMUnit;
out Tree: TKMPointDir; out PlantAct: TKMPlantAct): Boolean;
procedure Clear;
procedure WalkStyle(const aLoc2: TKMPointDir; aTo, aWork: TKMUnitActionType; aCycles, aDelay: Byte; aFrom: TKMUnitActionType; aScript: TKMGatheringScript);
procedure SubActAdd(aAct: TKMHouseActionType; aCycles: Single);
procedure ResourcePlan(Res1: TKMWareType; Qty1: Byte; Res2: TKMWareType; Qty2: Byte; Prod1: TKMWareType; Prod2: TKMWareType = wtNone);
public
HasToWalk: Boolean;
Loc: TKMPoint;
ActionWalkTo: TKMUnitActionType;
ActionWorkType: TKMUnitActionType;
WorkCyc: Integer;
WorkDir: TKMDirection;
GatheringScript: TKMGatheringScript;
AfterWorkDelay: Integer;
ActionWalkFrom: TKMUnitActionType;
Resource1: TKMWareType; Count1: Byte;
Resource2: TKMWareType; Count2: Byte;
ActCount: Byte;
HouseAct: array [0..MAX_WORKPLAN - 1] of record
Act: TKMHouseActionType;
TimeToWork: Word;
end;
Product1: TKMWareType; ProdCount1: Byte;
Product2: TKMWareType; ProdCount2: Byte;
AfterWorkIdle: Integer;
ResourceDepleted: Boolean;
public
procedure FindPlan(aUnit: TKMUnit; aHome: TKMHouseType; aProduct: TKMWareType;
aLoc: TKMPoint; aPlantAct: TKMPlantAct);
function FindDifferentResource(aUnit: TKMUnit; aLoc: TKMPoint; const aAvoidLoc: TKMPoint): Boolean;
property IsIssued: Boolean read fIssued;
procedure Save(SaveStream: TKMemoryStream);
procedure Load(LoadStream: TKMemoryStream);
end;
implementation
uses
SysUtils,
KM_ResUnits, KM_Houses, KM_HouseWoodcutters,
KM_ResHouses,
KM_Terrain,
KM_Resource, KM_CommonUtils;
{Houses are only a place on map, they should not issue or perform tasks (except Training)
Everything should be issued by units
Where to go, which walking style, what to do on location, for how long
How to go back in case success, incase bad luck
What to take from supply, how much, take2, much2
What to do, Work/Cycles, What resource to add to Output, how much
E.g. CoalMine: Miner arrives at home and Idles for 5sec, then takes a work task (depending on ResOut count)
Since Loc is 0,0 he immidietely skips to Phase X where he switches house to Work1 (and self busy for same framecount)
Then Work2 and Work3 same way. Then adds resource to out and everything to Idle for 5sec.
E.g. Farmer arrives at home and Idles for 5sec, then takes a work task (depending on ResOut count, HouseType and Need to sow corn)
...... then switches house to Work1 (and self busy for same framecount)
Then Work2 and Work3 same way. Then adds resource to out and everything to Idle for 5sec.}
procedure TKMUnitWorkPlan.Clear;
begin
fIssued := False;
HasToWalk := False;
Loc := KMPOINT_ZERO;
ActionWalkTo := uaWalk;
ActionWorkType := uaWork;
WorkCyc := 0;
WorkDir := dirNA;
GatheringScript := gsNone;
AfterWorkDelay := 0;
ActionWalkFrom := uaWalk;
Resource1 := wtNone;
Count1 := 0;
Resource2 := wtNone;
Count2 := 0;
ActCount := 0;
Product1 := wtNone;
ProdCount1 := 0;
Product2 := wtNone;
ProdCount2 := 0;
AfterWorkIdle := 0;
ResourceDepleted := False;
end;
procedure TKMUnitWorkPlan.WalkStyle(const aLoc2: TKMPointDir; aTo, aWork: TKMUnitActionType; aCycles,aDelay: Byte; aFrom: TKMUnitActionType; aScript: TKMGatheringScript);
begin
Loc := aLoc2.Loc;
HasToWalk := True;
ActionWalkTo := aTo;
ActionWorkType := aWork;
WorkCyc := aCycles;
AfterWorkDelay := aDelay;
GatheringScript := aScript;
ActionWalkFrom := aFrom;
WorkDir := aLoc2.Dir;
end;
procedure TKMUnitWorkPlan.SubActAdd(aAct: TKMHouseActionType; aCycles: Single);
begin
HouseAct[ActCount].Act := aAct;
HouseAct[ActCount].TimeToWork := Round(gRes.Houses[fHome].Anim[aAct].Count * aCycles);
Inc(ActCount);
end;
procedure TKMUnitWorkPlan.ResourcePlan(Res1: TKMWareType; Qty1: Byte; Res2: TKMWareType; Qty2: Byte; Prod1: TKMWareType; Prod2: TKMWareType = wtNone);
begin
Resource1 := Res1; Count1 := Qty1;
Resource2 := Res2; Count2 := Qty2;
Product1 := Prod1; ProdCount1 := gRes.Houses[fHome].ResProductionX;
if Prod2=wtNone then exit;
Product2 := Prod2; ProdCount2 := gRes.Houses[fHome].ResProductionX;
end;
function TKMUnitWorkPlan.FindDifferentResource(aUnit: TKMUnit; aLoc: TKMPoint; const aAvoidLoc: TKMPoint): Boolean;
var
NewLoc: TKMPointDir;
PlantAct: TKMPlantAct;
Found: boolean;
HW: TKMHouseWoodcutters;
begin
if (GatheringScript = gsWoodCutterCut) or (GatheringScript = gsWoodCutterPlant) then
begin
HW := TKMHouseWoodcutters(aUnit.Home);
HW.ValidateFlagPoint; //Validate Cutting point. It will be set to a valid one if needed.
if HW.IsFlagPointSet then
aLoc := HW.FlagPoint;
end;
with gTerrain do
case GatheringScript of
gsStoneCutter: Found := FindStone(aLoc, gRes.Units[aUnit.UnitType].MiningRange, aAvoidLoc, False, NewLoc);
gsFarmerSow: Found := FindCornField(aLoc, gRes.Units[aUnit.UnitType].MiningRange, aAvoidLoc, taPlant, PlantAct, NewLoc);
gsFarmerCorn: begin
Found := FindCornField(aLoc, gRes.Units[aUnit.UnitType].MiningRange, aAvoidLoc, taAny, PlantAct, NewLoc);
if PlantAct = taPlant then
begin
GatheringScript := gsFarmerSow; //Switch to sowing corn rather than cutting
ActionWalkFrom := uaWalkTool; //Carry our scythe back (without the corn) as the player saw us take it out
ActionWorkType := uaWork1;
WorkCyc := 10;
Product1 := wtNone; //Don't produce corn
ProdCount1 := 0;
end;
end;
gsFarmerWine: begin
Found := FindWineField(aLoc, gRes.Units[aUnit.UnitType].MiningRange, aAvoidLoc, NewLoc);
NewLoc.Dir := dirN; //The animation for picking grapes is only defined for facing north
end;
gsFisherCatch: Found := FindFishWater(aLoc, gRes.Units[aUnit.UnitType].MiningRange, aAvoidLoc, False, NewLoc);
gsWoodCutterCut: Found := ChooseTree(aLoc, KMGetVertexTile(aAvoidLoc, WorkDir), gRes.Units[aUnit.UnitType].MiningRange, taCut, aUnit, NewLoc, PlantAct);
gsWoodCutterPlant: Found := ChooseTree(aLoc, aAvoidLoc, gRes.Units[aUnit.UnitType].MiningRange, taPlant, aUnit, NewLoc, PlantAct);
else
Found := False; //Can find a new resource for an unknown gathering script, so return with False
end;
if Found then
begin
Loc := NewLoc.Loc;
WorkDir := NewLoc.Dir;
Result := True;
end
else
Result := False;
end;
function TKMUnitWorkPlan.ChooseTree(const aLoc, aAvoid: TKMPoint; aRadius: Integer; aPlantAct: TKMPlantAct; aUnit: TKMUnit; out Tree: TKMPointDir; out PlantAct: TKMPlantAct): Boolean;
var
I: Integer;
T: TKMPoint;
treeList: TKMPointDirCenteredList;
bestToPlant, secondBestToPlant: TKMPointCenteredList;
begin
treeList := TKMPointDirCenteredList.Create(aLoc);
bestToPlant := TKMPointCenteredList.Create(aLoc);
secondBestToPlant := TKMPointCenteredList.Create(aLoc);
gTerrain.FindTree(aLoc, aRadius, aAvoid, aPlantAct, treeList, bestToPlant, secondBestToPlant);
//Convert taAny to either a Tree or a Spot
if (aPlantAct in [taCut, taAny])
and ((treeList.Count > 8) //Always chop the tree if there are many
or (bestToPlant.Count + secondBestToPlant.Count = 0)
or ((treeList.Count > 0) and (KaMRandom({$IFDEF DBG_RNG_SPY}'TKMUnitWorkPlan.ChooseTree'{$ENDIF}) < treeList.Count / (treeList.Count + (bestToPlant.Count + secondBestToPlant.Count)/15)))
) then
begin
PlantAct := taCut;
Result := treeList.GetWeightedRandom(Tree);
end
else
begin
PlantAct := taPlant;
//First try stumps list
for I := bestToPlant.Count - 1 downto 0 do
if not TKMUnitCitizen(aUnit).CanWorkAt(bestToPlant[I], gsWoodCutterPlant) then
bestToPlant.Delete(I);
Result := bestToPlant.GetWeightedRandom(T);
//Trees must always be planted facing north as that is the direction the animation uses
if Result then
Tree := KMPointDir(T, dirN)
else
begin
//Try empty places list
for I := secondBestToPlant.Count - 1 downto 0 do
if not TKMUnitCitizen(aUnit).CanWorkAt(secondBestToPlant[I], gsWoodCutterPlant) then
secondBestToPlant.Delete(I);
Result := secondBestToPlant.GetWeightedRandom(T);
//Trees must always be planted facing north as that is the direction the animation uses
if Result then
Tree := KMPointDir(T, dirN);
end;
end;
treeList.Free;
bestToPlant.Free;
secondBestToPlant.Free;
end;
procedure TKMUnitWorkPlan.FindPlan(aUnit: TKMUnit; aHome: TKMHouseType; aProduct: TKMWareType; aLoc: TKMPoint; aPlantAct: TKMPlantAct);
var
I: Integer;
tmp: TKMPointDir;
plantAct: TKMPlantAct;
HW: TKMHouseWoodcutters;
begin
Clear;
fHome := aHome;
AfterWorkIdle := gRes.Houses[aHome].WorkerRest * 10;
//Now we need to fill only specific properties
case aUnit.UnitType of
utWoodcutter: if aHome = htWoodcutters then
begin
HW := TKMHouseWoodcutters(aUnit.Home);
HW.ValidateFlagPoint; //Validate Cutting point. It will be set to a valid one if needed.
if HW.IsFlagPointSet then
aLoc := HW.FlagPoint;
fIssued := ChooseTree(aLoc, KMPOINT_ZERO, gRes.Units[aUnit.UnitType].MiningRange, aPlantAct, aUnit, tmp, plantAct);
if fIssued then
begin
case plantAct of
taCut: begin //Cutting uses DirNW,DirSW,DirSE,DirNE (1,3,5,7) of uaWork
ResourcePlan(wtNone,0,wtNone,0,wtTrunk);
WalkStyle(tmp, uaWalkBooty,uaWork,15,20,uaWalkTool2,gsWoodCutterCut);
end;
taPlant: begin //Planting uses DirN (0) of uaWork
WalkStyle(tmp, uaWalkTool,uaWork,12,0,uaWalk,gsWoodCutterPlant);
end;
else fIssued := False;
end;
end
else
case plantAct of
taCut: if not gTerrain.CanFindTree(aLoc, gRes.Units[aUnit.UnitType].MiningRange) then
ResourceDepleted := True; //No more trees to cut
taPlant: if HW.WoodcutterMode = wmPlant then
ResourceDepleted := True; //No place for trees to plant
end;
end;
utMiner: if aHome = htCoalMine then
begin
fIssued := gTerrain.FindOre(aLoc, wtCoal, tmp.Loc);
if fIssued then
begin
Loc := tmp.Loc;
ResourcePlan(wtNone,0,wtNone,0,wtCoal);
GatheringScript := gsCoalMiner;
SubActAdd(haWork1,1);
SubActAdd(haWork2,23);
SubActAdd(haWork5,1);
end else
ResourceDepleted := True;
end else
if aHome = htIronMine then
begin
fIssued := gTerrain.FindOre(aLoc, wtIronOre, tmp.Loc);
if fIssued then
begin
Loc := tmp.Loc;
ResourcePlan(wtNone,0,wtNone,0,wtIronOre);
GatheringScript := gsIronMiner;
SubActAdd(haWork1,1);
SubActAdd(haWork2,24);
SubActAdd(haWork5,1);
end else
ResourceDepleted := True;
end else
if aHome = htGoldMine then
begin
fIssued := gTerrain.FindOre(aLoc, wtGoldOre, tmp.Loc);
if fIssued then
begin
Loc := tmp.Loc;
ResourcePlan(wtNone,0,wtNone,0,wtGoldOre);
GatheringScript := gsGoldMiner;
SubActAdd(haWork1,1);
SubActAdd(haWork2,24);
SubActAdd(haWork5,1);
end else
ResourceDepleted := True;
end;
utAnimalBreeder: if aHome = htSwine then
begin
ResourcePlan(wtCorn,1,wtNone,0,wtPig,wtSkin);
GatheringScript := gsSwineBreeder;
for I := 0 to 3 do
begin
SubActAdd(haWork2,1);
SubActAdd(haWork3,1);
end;
SubActAdd(haWork2,1);
fIssued := True;
end else
if aHome = htStables then
begin
ResourcePlan(wtCorn,1,wtNone,0,wtHorse);
GatheringScript := gsHorseBreeder;
SubActAdd(haWork1,1);
SubActAdd(haWork2,1);
SubActAdd(haWork3,1);
SubActAdd(haWork4,1);
SubActAdd(haWork5,1);
fIssued := True;
end;
utFarmer: if aHome = htFarm then
begin
fIssued := gTerrain.FindCornField(aLoc, gRes.Units[aUnit.UnitType].MiningRange, KMPOINT_ZERO, aPlantAct, plantAct, tmp);
if fIssued then
case plantAct of
taCut: begin
ResourcePlan(wtNone,0,wtNone,0,wtCorn);
WalkStyle(tmp, uaWalkTool,uaWork,6,0,uaWalkBooty,gsFarmerCorn);
end;
taPlant: WalkStyle(tmp, uaWalk,uaWork1,10,0,uaWalk,gsFarmerSow);
else fIssued := False;
end;
end else
if aHome = htVineyard then
begin
fIssued := gTerrain.FindWineField(aLoc, gRes.Units[aUnit.UnitType].MiningRange, KMPOINT_ZERO, tmp);
if fIssued then
begin
ResourcePlan(wtNone,0,wtNone,0,wtWine);
WalkStyle(KMPointDir(tmp.Loc,dirN), uaWalkTool2,uaWork2,5,0,uaWalkBooty2,gsFarmerWine); //The animation for picking grapes is only defined for facing north
SubActAdd(haWork1,1);
SubActAdd(haWork2,11);
SubActAdd(haWork5,1);
end;
end;
utCarpenter: if aHome = htSawmill then
begin
ResourcePlan(wtTrunk,1,wtNone,0,wtTimber);
SubActAdd(haWork1,1);
SubActAdd(haWork2,25);
SubActAdd(haWork5,1);
fIssued := True;
end else
if (aHome = htArmorWorkshop) and (aProduct = wtLeatherArmor) then
begin
ResourcePlan(wtLeather,1,wtNone,0,wtLeatherArmor);
for I := 0 to 3 do
begin
SubActAdd(haWork2,1);
SubActAdd(haWork3,1);
SubActAdd(haWork4,1);
end;
SubActAdd(haWork2,0.25);
fIssued := True;
end else
if (aHome = htArmorWorkshop) and (aProduct = wtWoodenShield) then
begin
ResourcePlan(wtTimber,1,wtNone,0,wtWoodenShield);
for I := 0 to 3 do
begin
SubActAdd(haWork2,1);
SubActAdd(haWork3,1);
SubActAdd(haWork4,1);
end;
SubActAdd(haWork2,0.25);
fIssued := True;
end else
if (aHome = htWeaponWorkshop) and (aProduct = wtAxe) then
begin
ResourcePlan(wtTimber,2,wtNone,0,wtAxe);
SubActAdd(haWork1,1);
for I := 0 to 2 do
begin
SubActAdd(haWork2,1);
SubActAdd(haWork3,1);
SubActAdd(haWork4,1);
end;
SubActAdd(haWork5,1);
fIssued := True;
end else
if (aHome = htWeaponWorkshop) and (aProduct = wtLance) then
begin
ResourcePlan(wtTimber,2,wtNone,0,wtLance);
SubActAdd(haWork1,1);
for I := 0 to 2 do
begin
SubActAdd(haWork2,1);
SubActAdd(haWork3,1);
SubActAdd(haWork4,1);
end;
SubActAdd(haWork5,1);
fIssued := True;
end else
if (aHome = htWeaponWorkshop) and (aProduct = wtBow) then
begin
ResourcePlan(wtTimber,2,wtNone,0,wtBow);
SubActAdd(haWork1,1);
for I := 0 to 2 do
begin
SubActAdd(haWork2,1);
SubActAdd(haWork3,1);
SubActAdd(haWork4,1);
end;
SubActAdd(haWork5,1);
fIssued := True;
end;
utBaker: if aHome = htMill then
begin
ResourcePlan(wtCorn,1,wtNone,0,wtFlour);
SubActAdd(haWork2,47);
fIssued := True;
end else
if aHome = htBakery then
begin
ResourcePlan(wtFlour,1,wtNone,0,wtBread);
for I := 0 to 6 do
begin
SubActAdd(haWork2,1);
SubActAdd(haWork3,1);
end;
fIssued := True;
end;
utButcher: if aHome = htTannery then
begin
ResourcePlan(wtSkin,1,wtNone,0,wtLeather);
SubActAdd(haWork1,1);
SubActAdd(haWork2,29);
fIssued := True;
end else
if aHome = htButchers then
begin
ResourcePlan(wtPig,1,wtNone,0,wtSausage);
SubActAdd(haWork1,1);
for I := 0 to 5 do
begin
SubActAdd(haWork2,1);
SubActAdd(haWork4,1);
SubActAdd(haWork3,1);
end;
fIssued := True;
end;
utFisher: if aHome = htFishermans then
begin
fIssued := gTerrain.FindFishWater(aLoc, gRes.Units[aUnit.UnitType].MiningRange, KMPOINT_ZERO, False, tmp);
if fIssued then
begin
ResourcePlan(wtNone,0,wtNone,0,wtFish);
WalkStyle(tmp,uaWalk,uaWork2,10,0,uaWalkTool,gsFisherCatch);
end else
//We must check again this time ignoring working units since they don't indicate the resource is depleted
ResourceDepleted := not gTerrain.FindFishWater(aLoc, gRes.Units[aUnit.UnitType].MiningRange, KMPOINT_ZERO, True, tmp);
end;
utStonemason: if aHome = htQuarry then
begin
fIssued := gTerrain.FindStone(aLoc, gRes.Units[aUnit.UnitType].MiningRange, KMPOINT_ZERO, False, tmp);
if fIssued then
begin
ResourcePlan(wtNone,0,wtNone,0,wtStone);
WalkStyle(tmp, uaWalk,uaWork,8,0,uaWalkTool,gsStoneCutter);
SubActAdd(haWork1,1);
SubActAdd(haWork2,9);
SubActAdd(haWork5,1);
end else
//We must check again this time ignoring working units since they don't indicate the resource is depleted
ResourceDepleted := not gTerrain.FindStone(aLoc, gRes.Units[aUnit.UnitType].MiningRange, KMPOINT_ZERO, True, tmp);
end;
utSmith: if (aHome = htArmorSmithy) and (aProduct = wtIronShield) then
begin
ResourcePlan(wtIron,1,wtCoal,1,wtIronShield);
for I := 0 to 3 do
begin
SubActAdd(haWork2,1);
SubActAdd(haWork3,1);
SubActAdd(haWork4,1);
end;
SubActAdd(haWork2,1);
SubActAdd(haWork5,1);
fIssued := True;
end else
if (aHome = htArmorSmithy) and (aProduct = wtIronArmor) then
begin
ResourcePlan(wtIron,1,wtCoal,1,wtIronArmor);
for I := 0 to 3 do
begin
SubActAdd(haWork2,1);
SubActAdd(haWork3,1);
SubActAdd(haWork4,1);
end;
SubActAdd(haWork2,1);
SubActAdd(haWork5,1);
fIssued := True;
end else
if (aHome = htWeaponSmithy) and (aProduct = wtSword) then
begin
ResourcePlan(wtIron,1,wtCoal,1,wtSword);
SubActAdd(haWork1,1);
for I := 0 to 2 do
begin
SubActAdd(haWork2,1);
SubActAdd(haWork3,1);
SubActAdd(haWork4,1);
end;
SubActAdd(haWork5,1);
fIssued := True;
end else
if (aHome = htWeaponSmithy) and (aProduct = wtPike) then
begin
ResourcePlan(wtIron,1,wtCoal,1,wtPike);
SubActAdd(haWork1,1);
for I := 0 to 2 do
begin
SubActAdd(haWork2,1);
SubActAdd(haWork3,1);
SubActAdd(haWork4,1);
end;
SubActAdd(haWork5,1);
fIssued := True;
end else
if (aHome = htWeaponSmithy) and (aProduct = wtCrossbow) then
begin
ResourcePlan(wtIron,1,wtCoal,1,wtCrossbow);
SubActAdd(haWork1,1);
for I := 0 to 2 do
begin
SubActAdd(haWork2,1);
SubActAdd(haWork3,1);
SubActAdd(haWork4,1);
end;
SubActAdd(haWork5,1);
fIssued := True;
end;
utMetallurgist: if aHome = htIronSmithy then
begin
ResourcePlan(wtIronOre,1,wtCoal,1,wtIron);
for I := 0 to 3 do
begin
SubActAdd(haWork2,1);
SubActAdd(haWork3,1);
end;
SubActAdd(haWork2,1);
SubActAdd(haWork3,0.25);
fIssued := True;
end else
if aHome = htMetallurgists then
begin
ResourcePlan(wtGoldOre,1,wtCoal,1,wtGold);
for I := 0 to 3 do
begin
SubActAdd(haWork2,1);
SubActAdd(haWork3,1);
SubActAdd(haWork4,1);
end;
SubActAdd(haWork2,1);
SubActAdd(haWork3,0.1);
fIssued := True;
end;
else
raise Exception.Create('No work plan for ' +
gRes.Units[aUnit.UnitType].GUIName + ' in ' +
gRes.Houses[aHome].HouseName);
end;
end;
procedure TKMUnitWorkPlan.Load(LoadStream: TKMemoryStream);
var
I: Integer;
begin
LoadStream.CheckMarker('WorkPlan');
LoadStream.Read(fHome, SizeOf(fHome));
LoadStream.Read(fIssued);
//public
LoadStream.Read(HasToWalk);
LoadStream.Read(Loc);
LoadStream.Read(ActionWalkTo, SizeOf(ActionWalkTo));
LoadStream.Read(ActionWorkType, SizeOf(ActionWorkType));
LoadStream.Read(WorkCyc);
LoadStream.Read(WorkDir);
LoadStream.Read(GatheringScript, SizeOf(GatheringScript));
LoadStream.Read(AfterWorkDelay);
LoadStream.Read(ActionWalkFrom, SizeOf(ActionWalkFrom));
LoadStream.Read(Resource1, SizeOf(Resource1));
LoadStream.Read(Count1);
LoadStream.Read(Resource2, SizeOf(Resource2));
LoadStream.Read(Count2);
LoadStream.Read(ActCount);
for I := 0 to ActCount - 1 do //Write only assigned
begin
LoadStream.Read(HouseAct[I].Act, SizeOf(HouseAct[I].Act));
LoadStream.Read(HouseAct[I].TimeToWork);
end;
LoadStream.Read(Product1, SizeOf(Product1));
LoadStream.Read(ProdCount1);
LoadStream.Read(Product2, SizeOf(Product2));
LoadStream.Read(ProdCount2);
LoadStream.Read(AfterWorkIdle);
LoadStream.Read(ResourceDepleted);
end;
procedure TKMUnitWorkPlan.Save(SaveStream: TKMemoryStream);
var
I: Integer;
begin
SaveStream.PlaceMarker('WorkPlan');
SaveStream.Write(fHome, SizeOf(fHome));
SaveStream.Write(fIssued);
//public
SaveStream.Write(HasToWalk);
SaveStream.Write(Loc);
SaveStream.Write(ActionWalkTo, SizeOf(ActionWalkTo));
SaveStream.Write(ActionWorkType, SizeOf(ActionWorkType));
SaveStream.Write(WorkCyc);
SaveStream.Write(WorkDir);
SaveStream.Write(GatheringScript, SizeOf(GatheringScript));
SaveStream.Write(AfterWorkDelay);
SaveStream.Write(ActionWalkFrom, SizeOf(ActionWalkFrom));
SaveStream.Write(Resource1, SizeOf(Resource1));
SaveStream.Write(Count1);
SaveStream.Write(Resource2, SizeOf(Resource2));
SaveStream.Write(Count2);
SaveStream.Write(ActCount);
for I := 0 to ActCount - 1 do //Write only assigned
begin
SaveStream.Write(HouseAct[I].Act, SizeOf(HouseAct[I].Act));
SaveStream.Write(HouseAct[I].TimeToWork);
end;
SaveStream.Write(Product1, SizeOf(Product1));
SaveStream.Write(ProdCount1);
SaveStream.Write(Product2, SizeOf(Product2));
SaveStream.Write(ProdCount2);
SaveStream.Write(AfterWorkIdle);
SaveStream.Write(ResourceDepleted);
end;
end.
File diff suppressed because it is too large. Load diff
+415
View File
@@ -0,0 +1,415 @@
unit KM_UnitsCollection;
{$I KaM_Remake.inc}
interface
uses
Generics.Collections,
KM_CommonClasses, KM_Defaults, KM_Points,
KM_Units, KM_Houses;
type
TKMUnitsCollection = class
private
fUnits: TKMList;
function GetUnit(aIndex: Integer): TKMUnit; inline;
function GetCount: Integer;
public
constructor Create;
destructor Destroy; override;
function AddUnit(aOwner: TKMHandID; aUnitType: TKMUnitType; const aLoc: TKMPointDir; aAutoPlace: Boolean = True;
aRequiredWalkConnect: Byte = 0; aInHouse: TKMHouse = nil): TKMUnit;
procedure AddUnitToList(aUnit: TKMUnit);
property Count: Integer read GetCount;
property Units[aIndex: Integer]: TKMUnit read GetUnit; default; //Use instead of Items[.]
procedure RemoveUnit(aUnit: TKMUnit);
procedure RemoveAllUnits;
procedure RemoveUnitsOutOfBounds(const aInsetRect: TKMRect);
procedure DeleteUnitFromList(aUnit: TKMUnit);
procedure OwnerUpdate(aOwner: TKMHandID);
function HitTest(X, Y: Integer; const UT: TKMUnitType = utAny): TKMUnit;
function GetUnitByUID(aUID: Integer): TKMUnit;
function GetClosestUnit(const aPoint: TKMPoint; aTypes: TKMUnitTypeSet = [Low(TKMUnitType)..High(TKMUnitType)]): TKMUnit;
procedure GetUnitsInRect(const aRect: TKMRect; List: TList<TKMUnit>);
function GetTotalPointers: Integer;
procedure Clear;
procedure Save(SaveStream: TKMemoryStream);
procedure Load(LoadStream: TKMemoryStream);
procedure SyncLoad;
procedure UpdateState(aTick: Cardinal);
procedure UpdateVisualState;
procedure Paint(const aRect: TKMRect; aTickLag: Single);
end;
implementation
uses
Classes, Math, Types, SysUtils,
KM_Entity, KM_Terrain,
KM_Game, KM_GameParams, KM_HandsCollection, KM_Log, KM_Resource, KM_ResUnits, KM_UnitWarrior,
KM_UnitActionWalkTo, KM_GameUIDTracker,
KM_DevPerfLog, KM_DevPerfLogTypes,
KM_CommonExceptions;
{ TKMUnitsCollection }
constructor TKMUnitsCollection.Create;
begin
inherited Create;
fUnits := TKMList.Create;
end;
destructor TKMUnitsCollection.Destroy;
begin
//No need to free units individually since they are Freed by TKMList.Clear command in destructor
fUnits.Free;
inherited;
end;
procedure TKMUnitsCollection.Clear;
begin
fUnits.Clear;
end;
function TKMUnitsCollection.GetCount: Integer;
begin
Result := fUnits.Count;
end;
function TKMUnitsCollection.GetUnit(aIndex: Integer): TKMUnit;
begin
Result := fUnits[aIndex];
end;
//AutoPlace means we should try to find a spot for this unit instead of just placing it where we were told to
function TKMUnitsCollection.AddUnit(aOwner: TKMHandID; aUnitType: TKMUnitType; const aLoc: TKMPointDir;
aAutoPlace: Boolean = True; aRequiredWalkConnect: Byte = 0; aInHouse: TKMHouse = nil): TKMUnit;
var
uid: Integer;
placeTo: TKMPoint;
pointDir: TKMPointDir;
U: TKMUnit;
begin
if aAutoPlace then
begin
placeTo := KMPOINT_ZERO; // Will have 0:0 if no place found
if aRequiredWalkConnect = 0 then
aRequiredWalkConnect := gTerrain.GetWalkConnectID(aLoc.Loc);
var placeFound := gHands.FindPlaceForUnit(aLoc.Loc.X, aLoc.Loc.Y, aUnitType, placeTo, aRequiredWalkConnect);
if not placeFound then
begin
gLog.AddTime('Unable to find a place for a unit around ' + KM_Points.TypeToString(aLoc.Loc));
Exit(nil);
end;
end
else
placeTo := aLoc.Loc;
// Check if Pos is within map coords first, as other checks rely on this
if not gTerrain.TileInMapCoords(placeTo.X, placeTo.Y) then
begin
gLog.AddTime('Unable to add unit to ' + KM_Points.TypeToString(placeTo));
Exit(nil);
end;
if (aInHouse = nil) and gTerrain.HasUnit(placeTo) then
begin
U := TKMUnit(gTerrain.Land^[placeTo.Y,placeTo.X].IsUnit);
raise ELocError.Create(Format('No space for %s at %s, tile is already occupied by %s, ID = %d',
[gRes.Units[aUnitType].GUIName, aLoc.ToString, gRes.Units[U.UnitType].GUIName, U.UID]),
placeTo);
end;
pointDir := KMPointDir(placeTo, aLoc.Dir);
uid := gUIDTracker.GetNewUID;
case aUnitType of
utSerf: Result := TKMUnitSerf.Create(uid, aUnitType, pointDir, aOwner, aInHouse);
utBuilder: Result := TKMUnitWorker.Create(uid, aUnitType, pointDir, aOwner, aInHouse);
utWoodCutter..utFisher,
{utBuilder,}
utStonemason..utMetallurgist: Result := TKMUnitCitizen.Create(uid, aUnitType, pointDir, aOwner, aInHouse);
utRecruit: Result := TKMUnitRecruit.Create(uid, aUnitType, pointDir, aOwner, aInHouse);
WARRIOR_MIN..WARRIOR_MAX: Result := TKMUnitWarrior.Create(uid, aUnitType, pointDir, aOwner, aInHouse);
// Do not specify aAddInHouse, we want to call TKMUnitAnimal constructor
utWolf,
utWatersnake..utDuck: Result := TKMUnitAnimal.Create(uid, aUnitType, pointDir, aOwner);
utFish: Result := TKMUnitFish.Create(uid, pointDir, aOwner);
else
raise ELocError.Create('Add ' + gRes.Units[aUnitType].GUIName, pointDir.Loc);
end;
if Result <> nil then
fUnits.Add(Result);
end;
procedure TKMUnitsCollection.AddUnitToList(aUnit: TKMUnit);
begin
Assert(gGameParams.IsMapEditor); // Allow to add existing Unit directly only in MapEd
if aUnit <> nil then
fUnits.Add(aUnit);
end;
procedure TKMUnitsCollection.RemoveUnit(aUnit: TKMUnit);
begin
aUnit.CloseUnit; //Should free up the unit properly (freeing terrain usage and memory)
fUnits.Remove(aUnit); //Will free the unit
end;
procedure TKMUnitsCollection.RemoveUnitsOutOfBounds(const aInsetRect: TKMRect);
var
I: Integer;
newMapRect: TKMRect;
begin
Assert(gGameParams.IsMapEditor);
if Count <= 0 then Exit;
newMapRect := KMRectGrow(gTerrain.MapRect, aInsetRect);
for I := 0 to Count - 1 do
if not KMInRect(Units[I].Position, newMapRect) then
Units[I].CloseUnit;
end;
procedure TKMUnitsCollection.RemoveAllUnits;
var
I: Integer;
begin
if Count <= 0 then Exit;
for I := 0 to Count - 1 do
Units[I].CloseUnit;
fUnits.Clear;
end;
procedure TKMUnitsCollection.DeleteUnitFromList(aUnit: TKMUnit);
begin
Assert(gGameParams.IsMapEditor); // Allow to delete existing Unit directly only in MapEd
if (aUnit <> nil) then
fUnits.Extract(aUnit); // use Extract instead of Delete, cause Delete nils inner objects somehow
end;
procedure TKMUnitsCollection.OwnerUpdate(aOwner: TKMHandID);
var
I: Integer;
begin
for I := 0 to Count - 1 do
Units[I].Owner := aOwner;
end;
function TKMUnitsCollection.HitTest(X, Y: Integer; const UT: TKMUnitType = utAny): TKMUnit;
var
I: Integer;
begin
Result := nil;
for I := 0 to Count - 1 do
if Units[I].HitTest(X,Y,UT) and not Units[I].IsDead then
begin
Result := Units[I];
Exit;
end;
end;
function TKMUnitsCollection.GetUnitByUID(aUID: Integer): TKMUnit;
var
I: Integer;
begin
Result := nil;
for I := 0 to Count - 1 do
if aUID = Units[I].UID then
begin
Result := Units[I];
exit;
end;
end;
procedure TKMUnitsCollection.GetUnitsInRect(const aRect: TKMRect; List: TList<TKMUnit>);
var
I: Integer;
begin
for I := 0 to Count - 1 do
if KMInRect(Units[I].PositionF, aRect) and not Units[I].IsDeadOrDying then
List.Add(Units[I]);
end;
function TKMUnitsCollection.GetClosestUnit(const aPoint: TKMPoint; aTypes: TKMUnitTypeSet = [Low(TKMUnitType)..High(TKMUnitType)]): TKMUnit;
var
I: Integer;
bestDist, dist: Single;
begin
Result := nil;
bestDist := MaxSingle; //Any distance will be closer than that
for I := 0 to Count - 1 do
if not Units[I].IsDeadOrDying and Units[I].Visible and (Units[I].UnitType in aTypes) then
begin
dist := KMLengthSqr(Units[I].Position, aPoint);
if dist < bestDist then
begin
bestDist := dist;
Result := Units[I];
end;
end;
end;
function TKMUnitsCollection.GetTotalPointers: Integer;
var
I: Integer;
begin
Result := 0;
for I := 0 to Count - 1 do
Inc(Result, Units[I].PointerCount);
end;
procedure TKMUnitsCollection.Save(SaveStream: TKMemoryStream);
var
I: Integer;
begin
SaveStream.PlaceMarker('Units');
SaveStream.Write(Count);
for I := 0 to Count - 1 do
begin
//We save unit type to know which unit class to load
SaveStream.Write(Units[I].UnitType, SizeOf(Units[I].UnitType));
Units[I].Save(SaveStream);
end;
end;
procedure TKMUnitsCollection.Load(LoadStream: TKMemoryStream);
var
I, newCount: Integer;
unitType: TKMUnitType;
U: TKMUnit;
begin
LoadStream.CheckMarker('Units');
LoadStream.Read(newCount);
for I := 0 to newCount - 1 do
begin
LoadStream.Read(unitType, SizeOf(unitType));
case unitType of
utSerf: U := TKMUnitSerf.Load(LoadStream);
utBuilder: U := TKMUnitWorker.Load(LoadStream);
utWoodCutter..utFisher,
{utWorker,}
utStonemason..utMetallurgist:
U := TKMUnitCitizen.Load(LoadStream);
utRecruit: U := TKMUnitRecruit.Load(LoadStream);
WARRIOR_MIN..WARRIOR_MAX: U := TKMUnitWarrior.Load(LoadStream);
utWolf,
utWatersnake..utDuck: U := TKMUnitAnimal.Load(LoadStream);
utFish: U := TKMUnitFish.Load(LoadStream);
else
U := nil;
end;
if U <> nil then
fUnits.Add(U)
else
gLog.AddAssert('Unknown unit type in Savegame');
end;
end;
procedure TKMUnitsCollection.SyncLoad;
var
I: Integer;
begin
for I := 0 to Count - 1 do
Units[I].SyncLoad;
end;
procedure TKMUnitsCollection.UpdateVisualState;
var
I: Integer;
begin
Assert(gGameParams.IsMapEditor);
for I := Count - 1 downto 0 do
if not Units[I].IsDead then
Units[I].UpdateVisualState;
end;
procedure TKMUnitsCollection.UpdateState(aTick: Cardinal);
var
I: Integer;
begin
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionEnter(psUnits);
{$ENDIF}
try
for I := Count - 1 downto 0 do
if not Units[I].IsDead then
begin
Units[I].UpdateState;
Units[I].UpdateVisualState;
end
else
if FREE_POINTERS and (Units[I].PointerCount = 0) then
fUnits.Delete(I);
finally
{$IFDEF DBG_PERFLOG}
gPerfLogs.SectionLeave(psUnits);
{$ENDIF}
end;
// -- POINTER FREEING SYSTEM - DESCRIPTION -- //
// This system was implemented because unit and house objects cannot be freed until all pointers
// to them (in tasks, delivery queue, etc.) have been freed, otherwise we have pointer integrity
// issues.
// -- ROUGH OUTLINE -- //
// - Units and houses have fPointerCount, which is the number of pointers to them. (e.g. tasks,
// deliveries) This is kept up to date by the thing that is using the pointer. On create it uses
// GetPointer to get the pointer and increase the pointer count and on destroy it decreases
// it with ReleasePointer.
// - When a unit dies, the object is not destroyed. Instead a flag (boolean) is set to say that we
// want to destroy but can't because there still might be pointers to the unit. From then on
// every update state it checks to see if the pointer count is 0 yet. If it is then the unit is
// destroyed.
// - For each place that contains a pointer, it should check everytime the pointer is used to see
// if it has been destroy. If it has then we free the pointer and reduce the count.
// (and do any other action nececary due to the unit/house dying)
end;
procedure TKMUnitsCollection.Paint(const aRect: TKMRect; aTickLag: Single);
const
MARGIN = 2;
var
I: Integer;
growRect: TKMRect;
begin
//Add additional margin to compensate for units height
growRect := KMRectGrow(aRect, MARGIN);
for I := 0 to Count - 1 do
if (Units[I] <> nil)
and not Units[I].IsDead
and ((SHOW_UNIT_ROUTES
and (Units[I].Action is TKMUnitActionWalkTo)
and TKMUnitActionWalkTo(Units[I].Action).NeedToPaint(growRect))
or KMInRect(Units[I].PositionF, growRect)) then
Units[I].Paint(aTickLag);
end;
end.
@@ -0,0 +1,130 @@
unit KM_UnitActionAbandonWalk;
{$I KaM_Remake.inc}
interface
uses
Math,
KM_CommonClasses, KM_Defaults, KM_Units, KM_Points;
{Abandon the current walk, move onto next tile}
type
TKMUnitActionAbandonWalk = class(TKMUnitAction)
private
fWalkTo: TKMPoint;
fVertexOccupied: TKMPoint;
public
constructor Create(aUnit: TKMUnit; const LocB, aVertexOccupied: TKMPoint; aActionType: TKMUnitActionType);
constructor Load(LoadStream: TKMemoryStream); override;
destructor Destroy; override;
function ActName: TKMUnitActionName; override;
function CanBeInterrupted(aForced: Boolean = True): Boolean; override;
function GetExplanation: UnicodeString; override;
function Execute: TKMActionResult; override;
procedure Save(SaveStream: TKMemoryStream); override;
end;
implementation
uses
KM_Resource, KM_ResUnits;
{ TUnitActionAbandonWalk }
constructor TKMUnitActionAbandonWalk.Create(aUnit: TKMUnit; const LocB, aVertexOccupied: TKMPoint; aActionType: TKMUnitActionType);
begin
Assert(LocB.X*LocB.Y <> 0, 'Illegal WalkTo 0:0');
inherited Create(aUnit, aActionType, False);
fWalkTo := LocB;
fVertexOccupied := aVertexOccupied;
end;
destructor TKMUnitActionAbandonWalk.Destroy;
begin
if (fUnit <> nil) and not KMSamePoint(fVertexOccupied, KMPOINT_ZERO) then
begin
fUnit.VertexRem(fVertexOccupied); //Unoccupy vertex
fVertexOccupied := KMPOINT_ZERO;
end;
inherited;
end;
constructor TKMUnitActionAbandonWalk.Load(LoadStream: TKMemoryStream);
begin
inherited;
LoadStream.CheckMarker('UnitActionAbandonWalk');
LoadStream.Read(fWalkTo);
LoadStream.Read(fVertexOccupied);
end;
function TKMUnitActionAbandonWalk.ActName: TKMUnitActionName;
begin
Result := uanAbandonWalk;
end;
function TKMUnitActionAbandonWalk.GetExplanation: UnicodeString;
begin
Result := 'Abandoning walk';
end;
function TKMUnitActionAbandonWalk.Execute: TKMActionResult;
var
dx, dy: ShortInt;
walkX, walkY, distance: Single;
begin
Result := arActContinues;
//Execute the route in series of moves
// Use umtWalk move type here, since we just want to evaluate if we are close enough
distance := gRes.Units[fUnit.UnitType].GetEffectiveWalkSpeed(False);
//Check if unit has arrived on tile
if KMSamePointF(fUnit.PositionF, KMPointF(fWalkTo), distance/2) then
begin
fUnit.PositionF := KMPointF(fWalkTo); //Set precise position to avoid rounding errors
fUnit.IsExchanging := False; //Disable sliding (in case it was set in previous step)
if not KMSamePoint(fVertexOccupied, KMPOINT_ZERO) then
begin
fUnit.VertexRem(fVertexOccupied); //Unoccupy vertex
fVertexOccupied := KMPOINT_ZERO;
end;
StepDone := True;
Exit(arActDone);
end;
walkX := fWalkTo.X - fUnit.PositionF.X;
walkY := fWalkTo.Y - fUnit.PositionF.Y;
dx := sign(walkX); //-1,0,1
dy := sign(walkY); //-1,0,1
distance := gRes.Units[fUnit.UnitType].GetEffectiveWalkSpeed((dx <> 0) and (dy <> 0));
fUnit.PositionF := KMPointF(fUnit.PositionF.X + dx*Math.min(distance,abs(walkX)),
fUnit.PositionF.Y + dy*Math.min(distance,abs(walkY)));
Inc(fUnit.AnimStep);
end;
procedure TKMUnitActionAbandonWalk.Save(SaveStream: TKMemoryStream);
begin
inherited;
SaveStream.PlaceMarker('UnitActionAbandonWalk');
SaveStream.Write(fWalkTo);
SaveStream.Write(fVertexOccupied);
end;
function TKMUnitActionAbandonWalk.CanBeInterrupted(aForced: Boolean = True): Boolean;
begin
Result := StepDone and not Locked; //Abandon walk should never be abandoned, it will exit within 1 step anyway
end;
end.
@@ -0,0 +1,414 @@
unit KM_UnitActionFight;
{$I KaM_Remake.inc}
interface
uses
Classes, KM_CommonClasses, KM_Defaults, KM_CommonUtils, KromUtils, Math, SysUtils, KM_Units, KM_Points;
type
// Fight until we die or the opponent dies
TKMUnitActionFight = class(TKMUnitAction)
private
fFightDelay: Integer; //Pause for this many ticks before going onto the next Step
fOpponent: TKMUnit; //Who we are fighting with
fVertexOccupied: TKMPoint; //The diagonal vertex we are currently occupying
// Execute is broken up into multiple methods
function ExecuteValidateOpponent(Step: Byte): TKMActionResult;
function ExecuteProcessRanged(Step: Byte): Boolean;
function ExecuteProcessMelee(Step: Byte): Boolean;
function UpdateVertexUsage(const aFrom, aTo: TKMPoint): Boolean;
procedure IncVertex(const aFrom, aTo: TKMPoint);
procedure DecVertex;
procedure MakeSound(IsHit: Boolean);
public
constructor Create(aUnit: TKMUnit; aActionType: TKMUnitActionType; aOpponent: TKMUnit);
constructor Load(LoadStream: TKMemoryStream); override;
destructor Destroy; override;
function ActName: TKMUnitActionName; override;
function CanBeInterrupted(aForced: Boolean = True): Boolean; override;
function GetExplanation: UnicodeString; override;
procedure SyncLoad; override;
property GetOpponent: TKMUnit read fOpponent;
function Execute: TKMActionResult; override;
procedure Save(SaveStream: TKMemoryStream); override;
function ObjToStringShort(const aSeparator: String = '|'): String; override;
function ObjToString(const aSeparator: String = ' '): String; override;
end;
implementation
uses
KM_Entity,
KM_HandsCollection, KM_HandTypes, KM_HandEntity,
KM_ResSound, KM_Sound, KM_UnitWarrior, KM_Resource, KM_Projectiles,
KM_ResUnits, KM_Hand;
const
STRIKE_STEP = 5; //Melee units place hit on step 5
MeleeSoundsHit: array [0..14] of TKMSoundEffectOriginal = (
sfxMelee34, sfxMelee35, sfxMelee36, sfxMelee41, sfxMelee42,
sfxMelee44, sfxMelee45, sfxMelee46, sfxMelee47, sfxMelee48,
sfxMelee49, sfxMelee50, sfxMelee55, sfxMelee56, sfxMelee57);
MeleeSoundsMiss: array [0..8] of TKMSoundEffectOriginal = (
sfxMelee37, sfxMelee38, sfxMelee39,
sfxMelee40, sfxMelee43, sfxMelee51,
sfxMelee52, sfxMelee53, sfxMelee54);
{ TKMUnitActionFight }
constructor TKMUnitActionFight.Create(aUnit: TKMUnit; aActionType: TKMUnitActionType; aOpponent: TKMUnit);
begin
inherited Create(aUnit, aActionType, True);
Assert(aUnit is TKMUnitWarrior, 'Can''t create fight action for not Warrior unit');
fFightDelay := -1;
fOpponent := aOpponent.GetPointer;
aUnit.Direction := KMGetDirection(fUnit.PositionF, fOpponent.PositionF); //Face the opponent from the beginning
fVertexOccupied := KMPOINT_ZERO;
if KMStepIsDiag(fUnit.Position, fOpponent.Position) and not TKMUnitWarrior(fUnit).IsRanged then
IncVertex(fUnit.Position, fOpponent.Position);
end;
destructor TKMUnitActionFight.Destroy;
begin
gHands.CleanUpUnitPointer(fOpponent);
if not KMSamePoint(fVertexOccupied, KMPOINT_ZERO) then
DecVertex;
inherited;
end;
constructor TKMUnitActionFight.Load(LoadStream: TKMemoryStream);
begin
inherited;
LoadStream.CheckMarker('UnitActionFight');
LoadStream.Read(fOpponent, 4);
LoadStream.Read(fFightDelay);
LoadStream.Read(fVertexOccupied);
end;
procedure TKMUnitActionFight.SyncLoad;
begin
inherited;
fOpponent := gHands.GetUnitByUID(Integer(fOpponent));
end;
function TKMUnitActionFight.ActName: TKMUnitActionName;
begin
Result := uanFight;
end;
function TKMUnitActionFight.GetExplanation: UnicodeString;
begin
Result := 'Fighting';
end;
function TKMUnitActionFight.UpdateVertexUsage(const aFrom, aTo: TKMPoint): Boolean;
begin
Result := True;
if KMStepIsDiag(aFrom, aTo) then
begin
//If the new target has the same vertex as the old one, no change is needed
if KMSamePoint(KMGetDiagVertex(aFrom, aTo), fVertexOccupied) then Exit;
//Otherwise the new target's vertex is different to the old one, so remove old vertex usage and add new
DecVertex;
if fUnit.VertexUsageCompatible(aFrom, aTo) then
IncVertex(aFrom, aTo)
else
Result := False; //This vertex is being used so we can't fight
end
else
//The new target is not diagonal, make sure any old vertex usage is removed
DecVertex;
end;
procedure TKMUnitActionFight.IncVertex(const aFrom, aTo: TKMPoint);
begin
//Tell gTerrain that this vertex is being used so no other unit walks over the top of us
Assert(KMSamePoint(fVertexOccupied, KMPOINT_ZERO), 'Fight vertex in use');
fUnit.VertexAdd(aFrom, aTo);
fVertexOccupied := KMGetDiagVertex(aFrom,aTo);
end;
procedure TKMUnitActionFight.DecVertex;
begin
//Tell gTerrain that this vertex is not being used anymore
if KMSamePoint(fVertexOccupied, KMPOINT_ZERO) then exit;
fUnit.VertexRem(fVertexOccupied);
fVertexOccupied := KMPOINT_ZERO;
end;
procedure TKMUnitActionFight.MakeSound(IsHit: Boolean);
var
//Battlecry is the most noticable random sound, we would like to repeat it exactly the same in each replay (?)
makeBattleCry: Boolean;
begin
//Randomly make a battle cry. KaMRandom must always happen regardless of tile revelation
makeBattleCry := KaMRandom(20{$IFDEF DBG_RNG_SPY}, 'TKMUnitActionFight.MakeSound'{$ENDIF}) = 0;
//Do not play sounds if unit is invisible to gMySpectator
//We should not use KaMRandom below this line because sound playback depends on FOW and is individual for each player
if gMySpectator.FogOfWar.CheckTileRevelation(fUnit.Position.X, fUnit.Position.Y) < 255 then Exit;
if makeBattleCry then
gSoundPlayer.PlayWarrior(fUnit.UnitType, spBattleCry, fUnit.PositionF);
case fUnit.UnitType of
utCrossbowman: gSoundPlayer.Play(sfxCrossbowDraw, fUnit.PositionF); // Aiming
utBowman: gSoundPlayer.Play(sfxBowDraw, fUnit.PositionF); // Aiming
utRogue: gSoundPlayer.Play(sfxSlingerShoot, fUnit.PositionF);
else begin
if IsHit then
gSoundPlayer.Play(MeleeSoundsHit[Random(Length(MeleeSoundsHit))], fUnit.PositionF)
else
gSoundPlayer.Play(MeleeSoundsMiss[Random(Length(MeleeSoundsMiss))], fUnit.PositionF);
end;
end;
end;
function TKMUnitActionFight.ExecuteValidateOpponent(Step: Byte): TKMActionResult;
begin
Result := arActContinues;
//See if Opponent has walked away (i.e. Serf) or died
if fOpponent.IsDeadOrDying //Don't continue to fight dead units
or not fOpponent.Visible //Don't continue to fight units that have went into a house
or (gHands[fUnit.Owner].Alliances[fOpponent.Owner] = atAlly) //Unit could become ally from script
or not TKMUnitWarrior(fUnit).WithinFightRange(fOpponent.Position)
or (TKMUnitWarrior(fUnit).IsRanged // For ranged - ignore opponents who are under FOW
and (gHands[fUnit.Owner].FogOfWar.CheckTileRevelation(fOpponent.Position.X, fOpponent.Position.Y) <> 255))
or not fUnit.CanWalkDiagonally(fUnit.Position, fOpponent.Position) then //Might be a tree between us now
begin
//After killing an opponent there is a very high chance that there is another enemy to be fought immediately
//Try to start fighting that enemy by reusing this FightAction, rather than destroying it and making a new one
Locked := False; //Fight can be interrupted by FindEnemy, otherwise it will always return nil!
gHands.CleanUpUnitPointer(fOpponent); //We are finished with the old opponent
fOpponent := TKMUnitWarrior(fUnit).FindEnemy; //Find a new opponent
if fOpponent <> nil then
begin
//Start fighting this opponent by resetting the action
fOpponent.GetPointer; //Add to pointer count
TKMUnitWarrior(fUnit).OnPickedFight(TKMUnitWarrior(fUnit), fOpponent);
Locked := True;
fFightDelay := -1;
//Ranged units should turn to face the new opponent immediately
if TKMUnitWarrior(fUnit).IsRanged then
fUnit.Direction := KMGetDirection(fUnit.PositionF, fOpponent.PositionF)
else
//Melee: If we haven't yet placed our strike, reset the animation step
//Otherwise finish this strike then we can face the new opponent automatically
if Step <= STRIKE_STEP then
fUnit.AnimStep := 0; //Rest fight animation/sequence
end
else
begin
//No one else to fight, so we exit
Result := arActDone;
end;
end;
end;
//A result of True means exit from Execute
function TKMUnitActionFight.ExecuteProcessRanged(Step: Byte): Boolean;
var
W: TKMUnitWarrior;
begin
Result := False;
W := TKMUnitWarrior(fUnit);
if Step = 0 then
begin
if fFightDelay = -1 then //Initialize
fFightDelay := W.AimingDelay;
if fFightDelay > 0 then
begin
Dec(fFightDelay);
Result := True; //do not increment AnimStep, just exit;
Exit;
end;
end;
//Slingshot sound should happen a bit later
if Step = W.AimSoundDelay then
MakeSound(False);
if Step = W.FiringDelay then
begin
W.SetLastShootTime; //Record last time the warrior shot
// Fire the arrow
gProjectiles.AimTargetUnit(fUnit.PositionF, fOpponent, W.ProjectileType, fUnit, W.RangeMax, W.RangeMin);
fFightDelay := -1; //Reset
end;
end;
//A result of True means exit from Execute
function TKMUnitActionFight.ExecuteProcessMelee(Step: Byte): Boolean;
var
isHit: Boolean;
damage: Word;
begin
Result := False;
if Step = 1 then
begin
//Tell the Opponent we are attacking him
gHands[fOpponent.Owner].AI.UnitAttackNotification(fOpponent, TKMUnitWarrior(fUnit));
//Tell our AI that we are in a battle and might need assistance! (only for melee battles against warriors)
if (fOpponent is TKMUnitWarrior) and not TKMUnitWarrior(fUnit).IsRanged then
gHands[fUnit.Owner].AI.UnitAttackNotification(fUnit, TKMUnitWarrior(fOpponent), False);
end;
//Melee units place hit on this step
if Step = STRIKE_STEP then
begin
//Base damage is the unit attack strength + AttackHorse if the enemy is mounted
damage := gRes.Units[fUnit.UnitType].Attack;
if (fOpponent.UnitType in [Low(UNIT_TO_GROUP_TYPE) .. High(UNIT_TO_GROUP_TYPE)]) and (UNIT_TO_GROUP_TYPE[fOpponent.UnitType] = gtMounted) then
damage := damage + gRes.Units[fUnit.UnitType].AttackHorse;
damage := damage * (GetDirModifier(fUnit.Direction, fOpponent.Direction) + 1); // Direction modifier
//Defence modifier
damage := damage div Math.max(gRes.Units[fOpponent.UnitType].Defence, 1); //Not needed, but animals have 0 defence
isHit := (damage >= KaMRandom(101{$IFDEF DBG_RNG_SPY}, 'TKMUnitActionFight.ExecuteProcessMelee'{$ENDIF})); //Damage is a % chance to hit
if isHit then
fOpponent.HitPointsDecrease(1, fUnit);
MakeSound(isHit); //Different sounds for hit and for miss
end;
//In KaM melee units pause for 1 tick on Steps [0,3,6]. Made it random so troops are not striking in sync,
//plus it adds randomness to battles
if Step in [0,3,6] then
begin
if fFightDelay = -1 then //Initialize
fFightDelay := KaMRandom(2{$IFDEF DBG_RNG_SPY}, 'TKMUnitActionFight.ExecuteProcessMelee 2'{$ENDIF});
if fFightDelay > 0 then
begin
dec(fFightDelay);
Result := True; //Means exit from Execute
Exit;
end;
fFightDelay := -1; //Reset
end;
end;
function TKMUnitActionFight.Execute: TKMActionResult;
var
cycle, step: Byte;
W: TKMUnitWarrior;
begin
W := TKMUnitWarrior(fUnit);
cycle := Max(gRes.Units[fUnit.UnitType].UnitAnim[ActionType, fUnit.Direction].Count, 1);
step := fUnit.AnimStep mod cycle;
Result := ExecuteValidateOpponent(step);
if Result = arActDone then
begin
// Face back to our order direction when we have done fighting with our enemy
if W.IsRanged and (W.FaceDir <> dirNA) then
fUnit.Direction := W.FaceDir;
Exit;
end;
step := fUnit.AnimStep mod cycle; //Can be changed by ExecuteValidateOpponent, so recalculate it
//Opponent can walk next to us, keep facing him
if step = 0 then //Only change direction between strikes, otherwise it looks odd
fUnit.Direction := KMGetDirection(fUnit.PositionF, fOpponent.PositionF);
//If the vertex usage has changed we should update it
if not W.IsRanged then //Ranged units do not use verticies
if not UpdateVertexUsage(fUnit.Position, fOpponent.Position) then
begin
//The vertex is being used so we can't fight
Result := arActDone;
Exit;
end;
if W.IsRanged then
begin
if ExecuteProcessRanged(step) then
Exit;
end
else
if ExecuteProcessMelee(step) then
Exit;
//Aiming Archers and pausing melee may miss a few ticks, (exited above) so don't put anything critical below!
StepDone := (fUnit.AnimStep mod cycle = 0) or W.IsRanged; //Archers may abandon at any time as they need to walk off imediantly
Inc(fUnit.AnimStep);
end;
procedure TKMUnitActionFight.Save(SaveStream: TKMemoryStream);
begin
inherited;
SaveStream.PlaceMarker('UnitActionFight');
if fOpponent <> nil then
SaveStream.Write(fOpponent.UID) //Store ID, then substitute it with reference on SyncLoad
else
SaveStream.Write(Integer(0));
SaveStream.Write(fFightDelay);
SaveStream.Write(fVertexOccupied);
end;
function TKMUnitActionFight.CanBeInterrupted(aForced: Boolean = True): Boolean;
begin
Result := (TKMUnitWarrior(fUnit).IsRanged and aForced) or not Locked; //Only allowed to interupt ranged fights
end;
function TKMUnitActionFight.ObjToStringShort(const aSeparator: String = '|'): String;
begin
Result := inherited ObjToStringShort(aSeparator) +
Format('%sopponent = [%s]%svertexOccupied = %s',
[aSeparator,
fOpponent.ObjToStringBasic(aSeparator), aSeparator,
fVertexOccupied.ToString]);
end;
function TKMUnitActionFight.ObjToString(const aSeparator: String = ' '): String;
begin
Result := inherited ObjToString(aSeparator) +
Format('%sopponent = [%s]%svertexOccupied = %s',
[aSeparator,
fOpponent.ObjToStringBasic(aSeparator), aSeparator,
fVertexOccupied.ToString]);
end;
end.
@@ -0,0 +1,522 @@
unit KM_UnitActionGoInOut;
{$I KaM_Remake.inc}
interface
uses
Classes, KromUtils, SysUtils,
KM_CommonClasses, KM_CommonTypes, KM_Defaults, KM_Points,
KM_Houses, KM_Units;
type
TKMBestExit = (beNone, beLeft, beCenter, beRight);
{This is a [fairly :P] simple action making unit go inside/outside of house}
TKMUnitActionGoInOut = class(TKMUnitAction)
private
fStep: Single;
fHouse: TKMHouse;
fDirection: TKMGoInDirection;
fDoor: TKMPoint;
fStreet: TKMPoint;
fInitiated: Boolean;
fPushedUnit: TKMUnit;
fWaitingForPush: Boolean;
fUsedDoorway: Boolean;
procedure IncDoorway;
procedure DecDoorway;
function FindBestExit(const aLoc: TKMPoint): TKMBestExit;
function TileHasIdleUnitToPush(X,Y: Word): TKMUnit;
function TileHasUnitOnHouseEntrance: Boolean;
procedure WalkIn;
procedure WalkOut;
function GetIsStarted: Boolean;
public
OnWalkedOut: TKMEvent; //NOTE: Caller must sync these events after loading, used with caution
OnWalkedIn: TKMEvent;
constructor Create(aUnit: TKMUnit; aAction: TKMUnitActionType; aDirection: TKMGoInDirection; aHouse: TKMHouse);
constructor Load(LoadStream: TKMemoryStream); override;
procedure SyncLoad; override;
destructor Destroy; override;
function ActName: TKMUnitActionName; override;
function CanBeInterrupted(aForced: Boolean = True): Boolean; override;
function GetExplanation: UnicodeString; override;
property IsStarted: Boolean read GetIsStarted; // Is unit actually started exiting or going inside?
property Direction: TKMGoInDirection read fDirection;
function GetDoorwaySlide(aCheck: TKMCheckAxis): Single;
function GetDoorwaySlides: TKMPointF;
function Execute: TKMActionResult; override;
procedure Save(SaveStream: TKMemoryStream); override;
end;
implementation
uses
KM_Entity,
KM_HandsCollection, KM_HandTypes, KM_HandEntity,
KM_Resource, KM_Terrain, KM_UnitActionStay, KM_UnitActionWalkTo,
KM_HouseBarracks, KM_ResHouses, KM_ResUnits, KM_CommonUtils, KM_GameParams,
KM_ResTypes;
{ TKMUnitActionGoInOut }
constructor TKMUnitActionGoInOut.Create(aUnit: TKMUnit; aAction: TKMUnitActionType; aDirection: TKMGoInDirection; aHouse: TKMHouse);
begin
inherited Create(aUnit, aAction, True);
//We might stuck trying to exit when house gets destroyed (1)
//and we might be dying in destroyed house (2)
fHouse := aHouse.GetPointer;
fDirection := aDirection;
fInitiated := False;
fWaitingForPush := False;
if fDirection = gdGoInside then
fStep := 1 //go Inside (one cell up)
else
fStep := 0; //go Outside (one cell down)
end;
constructor TKMUnitActionGoInOut.Load(LoadStream: TKMemoryStream);
begin
inherited;
LoadStream.CheckMarker('UnitActionGoInOut');
LoadStream.Read(fStep);
LoadStream.Read(fHouse, 4);
LoadStream.Read(fPushedUnit, 4);
LoadStream.Read(fDirection, SizeOf(fDirection));
LoadStream.Read(fDoor);
LoadStream.Read(fStreet);
LoadStream.Read(fInitiated);
LoadStream.Read(fWaitingForPush);
LoadStream.Read(fUsedDoorway);
end;
procedure TKMUnitActionGoInOut.SyncLoad;
begin
inherited;
fHouse := gHands.GetHouseByUID(Integer(fHouse));
fPushedUnit := gHands.GetUnitByUID(Integer(fPushedUnit));
end;
destructor TKMUnitActionGoInOut.Destroy;
begin
if fUsedDoorway then
DecDoorway;
gHands.CleanUpHousePointer(fHouse);
gHands.CleanUpUnitPointer(fPushedUnit);
if (fUnit <> nil)
and fInitiated
and (gTerrain.Land^[fUnit.PositionNext.Y, fUnit.PositionNext.X].IsUnit = fUnit) then
begin
case fDirection of
//Clear terrain lock for house entrance that made while unit was entering the house
gdGoInside: gTerrain.UnitRem(fUnit.PositionNext);
//A bug can occur because this action is destroyed early when a unit is told to die.
//If we are still invisible then TTaskDie assumes we are inside and creates a new
//GoOut action. Therefore if we are invisible we do not occupy a tile.
gdGoOutside: if not fUnit.Visible then
begin
gTerrain.UnitRem(fUnit.PositionNext);
if not KMSamePoint(fDoor, KMPOINT_ZERO) then
fUnit.PositionF := KMPointF(fDoor); //Put us back inside the house
end;
end;
end;
if (fDirection = gdGoOutside) and fUnit.Visible then
fUnit.InHouse := nil; //We are not in any house now
inherited;
end;
function TKMUnitActionGoInOut.ActName: TKMUnitActionName;
begin
Result := uanGoInOut;
end;
function TKMUnitActionGoInOut.GetExplanation: UnicodeString;
begin
Result := 'Walking in/out';
end;
// Is unit actually started exiting or going inside?
function TKMUnitActionGoInOut.GetIsStarted: Boolean;
begin
Result := fInitiated and not fWaitingForPush;
end;
procedure TKMUnitActionGoInOut.IncDoorway;
begin
Assert(not fUsedDoorway, 'Inc doorway when already in use?');
if fHouse <> nil then Inc(fHouse.DoorwayUse);
fUsedDoorway := True;
end;
procedure TKMUnitActionGoInOut.DecDoorway;
begin
Assert(fUsedDoorway, 'Dec doorway when not in use?');
if fHouse<>nil then dec(fHouse.DoorwayUse);
fUsedDoorway := False;
end;
//Attempt to find a tile below the door (on the street) we can walk to
//We can push idle units away. Check center first
function TKMUnitActionGoInOut.FindBestExit(const aLoc: TKMPoint): TKMBestExit;
function ChooseBestExit(aL, aR: Boolean): TKMBestExit;
begin
//Choose randomly between left and right
if (aL and aR) then
begin
if KaMRandom(2{$IFDEF DBG_RNG_SPY}, 'TKMUnitActionGoInOut.FindBestExit.ChooseBestExit'{$ENDIF}) = 0 then
Result := beLeft
else
Result := beRight;
end
else
if aL then
Result := beLeft
else
if aR then
Result := beRight
else
Result := beNone;
end;
var
U, UC, UL, UR: TKMUnit;
L, R: Boolean;
begin
if fUnit.CanStepTo(aLoc.X, aLoc.Y, tpWalk) then
Result := beCenter
else
begin
L := fUnit.CanStepTo(aLoc.X-1, aLoc.Y, tpWalk);
R := fUnit.CanStepTo(aLoc.X+1, aLoc.Y, tpWalk);
Result := ChooseBestExit(L, R);
if Result = beNone then
begin
U := nil;
UL := nil;
UR := nil;
//U could be nil if tile is unwalkable for some reason
UC := TileHasIdleUnitToPush(aLoc.X, aLoc.Y);
if UC <> nil then
Result := beCenter
else
begin
UL := TileHasIdleUnitToPush(aLoc.X-1, aLoc.Y);
L := UL <> nil;
UR := TileHasIdleUnitToPush(aLoc.X+1, aLoc.Y);
R := UR <> nil;
Result := ChooseBestExit(L, R);
end;
case Result of
beCenter: U := UC;
beLeft: U := UL;
beRight: U := UR;
end;
if U <> nil then
begin
fPushedUnit := U.GetPointer;
fPushedUnit.SetActionWalkPushed(gTerrain.GetOutOfTheWay(U, KMPOINT_ZERO, tpWalk));
end;
end;
end;
end;
//Check that tile is walkable and there's no unit blocking it or that unit can be pushed away
function TKMUnitActionGoInOut.TileHasIdleUnitToPush(X,Y: Word): TKMUnit;
var
U: TKMUnit;
begin
Result := nil;
if gTerrain.TileInMapCoords(X,Y)
and (gTerrain.CheckPassability(KMPoint(X,Y), fUnit.DesiredPassability))
and (gTerrain.CanWalkDiagonally(fUnit.Position, X, Y))
and (gTerrain.Land^[Y,X].IsUnit <> nil) then //If there's some unit we need to do a better check on him
begin
U := gTerrain.UnitsHitTest(X,Y); //Let's see who is standing there
//Check that the unit is idling and not an enemy, so that we can push it away
if (U <> nil)
and not U.IsAnimal // Can't push animals
and (U.Action is TKMUnitActionStay)
and not TKMUnitActionStay(U.Action).Locked
and (gHands.CheckAlliance(U.Owner, fUnit.Owner) = atAlly) then
Result := U;
end;
end;
function TKMUnitActionGoInOut.TileHasUnitOnHouseEntrance: Boolean;
begin
if fHouse.IsDestroyed then Exit(False);
// There could be a unit walking in the house already,
// f.e. if serf was added at point below entrance by script and he went straight into the same house
Result := (gTerrain.Land^[fHouse.Entrance.Y, fHouse.Entrance.X].IsUnit <> nil);
end;
procedure TKMUnitActionGoInOut.WalkIn;
begin
fUnit.Direction := dirN; //one cell up
fUnit.PositionNext := KMPointAbove(fUnit.Position);
gTerrain.UnitRem(fUnit.Position); // Release tile at point below house entrance
// Unit occupy a tile on a terrain while he is walking inside house
// House could be destroyed while while unit was walking in it, so we need to have a tile occupied on terrain for that case
// After house was destroyed other unit could go there / smth could be placed with as script,
// but this unit is still walking into house so we have keep it consistent:
// unit occupied tile if he is standing on that tile or he is going to get onto that tile
gTerrain.UnitAdd(fUnit.PositionNext, fUnit);
//We are walking straight
if fStreet.X = fDoor.X then
IncDoorway;
end;
//Start walking out of the house. unit is no longer in the house
procedure TKMUnitActionGoInOut.WalkOut;
begin
fUnit.Direction := KMGetDirection(fDoor, fStreet);
fUnit.PositionNext := fStreet;
gTerrain.UnitAdd(fUnit.PositionNext, fUnit); //Unit was not occupying tile while inside
//Use InHouse instead of Home, since Home could be cleared via ProceedHouseClosedForWorker (f.e. when wGoingForEating = True)
if (fUnit.UnitType = utRecruit) //Recruit
and(fUnit.InHouse <> nil) //In some house
and (fUnit.InHouse.HouseType = htBarracks) //Recruit is in barracks
and (fUnit.InHouse = fHouse) then //And is the house we are walking from
TKMHouseBarracks(fHouse).RecruitsRemove(fUnit);
//We are walking straight
if fStreet.X = fDoor.X then
IncDoorway;
end;
function TKMUnitActionGoInOut.GetDoorwaySlide(aCheck: TKMCheckAxis): Single;
var
offset: Single;
begin
if (fHouse = nil) or not fInitiated then
Result := 0
else
begin
offset := gRes.Houses[fHouse.HouseType].GetDoorwayOffset(aCheck);
Result := Mix(0, offset, fStep);
end;
end;
function TKMUnitActionGoInOut.GetDoorwaySlides: TKMPointF;
begin
if (fHouse = nil) or not fInitiated then
Result := KMPointF(0, 0)
else
begin
Result.X := Mix(0, gRes.Houses[fHouse.HouseType].GetDoorwayOffset(axX), fStep);
Result.Y := Mix(0, gRes.Houses[fHouse.HouseType].GetDoorwayOffset(axY), fStep);
end;
end;
function TKMUnitActionGoInOut.Execute: TKMActionResult;
var
U: TKMUnit;
distance: Single;
begin
Result := arActContinues;
if not fInitiated then
begin
//Set Door and Street locations
fDoor := KMPoint(fUnit.Position.X, fUnit.Position.Y - Round(fStep));
fStreet := KMPoint(fUnit.Position.X, fUnit.Position.Y + 1 - Round(fStep));
case fDirection of
gdGoInside: // House could be destroyed on the same tick Action was created
// So we can't do the check before this very moment
// Do not allow to enter already destroyed houses
// Since we did not occupy entrance tile other units inside house could do that already
if fHouse.IsDestroyed then
Exit(arActCanNotStart)
else
// There could be a unit walking in the house already,
// f.e. if serf was added at point below entrance by script and he went straight into the same house
if TileHasUnitOnHouseEntrance then
Exit
else
WalkIn;
gdGoOutside: begin
case FindBestExit(fStreet) of
beLeft: fStreet.X := fStreet.X - 1;
beCenter: ;
beRight: fStreet.X := fStreet.X + 1;
beNone: Exit; //All street tiles are blocked by busy units. Do not exit the house, just wait
end;
//If we have pushed an idling unit, wait till it goes away
//Wait until our push request is dealt with before we move out
if (fPushedUnit <> nil) then
begin
fWaitingForPush := True;
fInitiated := True;
Exit;
end
else
WalkOut; //Otherwise we can walk out now
end;
end;
fInitiated := True;
end;
if fWaitingForPush then
begin
U := gTerrain.Land^[fStreet.Y,fStreet.X].IsUnit;
if (U = nil) then //Unit has walked away
begin
fWaitingForPush := False;
gHands.CleanUpUnitPointer(fPushedUnit);
WalkOut;
end
else
begin //There's still some unit - we can't go outside
if (U <> fPushedUnit) //The unit has switched places with another one, so we must start again
or not (U.Action is TKMUnitActionWalkTo) //Unit was interupted (no longer pushed), so start again
or not TKMUnitActionWalkTo(U.Action).WasPushed then
begin
fInitiated := False;
fWaitingForPush := False;
gHands.CleanUpUnitPointer(fPushedUnit);
end;
Exit;
end;
end;
//IsExchanging can be updated while we have completed less than 20% of the move. If it is changed after that
//the unit makes a noticable "jump". This needs to be updated after starting because we don't know about an
//exchanging unit until they have also started walking (otherwise only 1 of the units will have IsExchanging = True)
if (
((fDirection = gdGoOutside) and (fStep < 0.2)) or
((fDirection = gdGoInside) and (fStep > 0.8))
)
and (fStreet.X = fDoor.X) //We are walking straight
and (fHouse <> nil) then
fUnit.IsExchanging := (fHouse.DoorwayUse > 1);
Assert((fHouse = nil) or KMSamePoint(fDoor, fHouse.Entrance)); //Must always go in/out the entrance of the house
//Actual speed is slower if we are moving diagonally, due to the fact we are moving in X and Y
distance := gRes.Units[fUnit.UnitType].GetEffectiveWalkSpeed(fStreet.X - fDoor.X <> 0);
fStep := fStep - distance * ShortInt(fDirection);
fUnit.PositionF := KMLerp(fDoor, fStreet, fStep);
fUnit.Visible := (fHouse = nil) or (fHouse.IsDestroyed) or (fStep > 0); //Make unit invisible when it's inside of House
if (fStep <= 0) or (fStep >= 1) then
begin
Result := arActDone;
fUnit.IsExchanging := False;
if fUsedDoorway then DecDoorway;
if fDirection = gdGoInside then
begin
fUnit.PositionF := KMPointF(fDoor);
if (fUnit.Home <> nil)
and (fUnit.Home = fHouse)
and (fUnit.Home.HouseType = htBarracks) //Unit home is barracks
and not fUnit.Home.IsDestroyed then //And is the house we are walking into and it's not destroyed
TKMHouseBarracks(fUnit.Home).RecruitsAdd(fUnit); //Add the recruit once it is inside, otherwise it can be equipped while still walking in!
//Set us as inside even if the house is destroyed. In that case UpdateVisibility will sort things out.
//When any woodcutter returns home - add an Axe
//(this might happen when he walks home or when mining is done)
if (fUnit.UnitType = utWoodcutter)
and (fUnit.Home <> nil)
and (fUnit.Home.HouseType = htWoodcutters)
and (fUnit.Home = fHouse) then //And is the house we are walking from
fHouse.CurrentAction.SubActionAdd([haFlagpole]);
if Assigned(OnWalkedIn) then
OnWalkedIn;
if fHouse <> nil then
fUnit.InHouse := fHouse;
// Release tile, occupied under the house entrance
// Unit occupied tile, when he was walking inside house, because he could interact with terrain
// if house was destroyed while he was during walk-in process
// But when unit entered house he is not on terrain anymore, but is somewhere in the house and he can't interact with terrain
// Do not remove tile occupation if house was destroyed while unit was walking in it
// we want to keep this place for exact this unit, so he will be not teleported to some other loc under the house
// (do not allow other units inside house to get this place occasionaly on house destruction in FindPlaceForUnit method)
if (fHouse <> nil) and not fHouse.IsDestroyed then
gTerrain.UnitRem(fUnit.Position);
end
else
begin
fUnit.PositionF := KMPointF(fStreet);
fUnit.InHouse := nil; //We are not in a house any longer
if Assigned(OnWalkedOut) then
OnWalkedOut;
end;
end
else
Inc(fUnit.AnimStep);
end;
procedure TKMUnitActionGoInOut.Save(SaveStream: TKMemoryStream);
begin
inherited;
SaveStream.PlaceMarker('UnitActionGoInOut');
SaveStream.Write(fStep);
SaveStream.Write(fHouse.UID); //Store ID, then substitute it with reference on SyncLoad
SaveStream.Write(fPushedUnit.UID); //Store ID, then substitute it with reference on SyncLoad
SaveStream.Write(fDirection, SizeOf(fDirection));
SaveStream.Write(fDoor);
SaveStream.Write(fStreet);
SaveStream.Write(fInitiated);
SaveStream.Write(fWaitingForPush);
SaveStream.Write(fUsedDoorway);
end;
function TKMUnitActionGoInOut.CanBeInterrupted(aForced: Boolean = True): Boolean;
begin
Result := not Locked; //Never interupt leaving barracks
end;
end.
@@ -0,0 +1,153 @@
unit KM_UnitActionStay;
{$I KaM_Remake.inc}
interface
uses
Classes, KM_Defaults, KromUtils, KM_CommonClasses, KM_Units, SysUtils, Math, KM_Points;
type
{Stay in place for set time}
TKMUnitActionStay = class(TKMUnitAction)
private
StayStill:boolean;
TimeToStay:integer;
StillFrame:byte;
procedure MakeSound(Cycle, Step: Byte);
public
constructor Create(aUnit: TKMUnit; aTimeToStay: Integer; aActionType: TKMUnitActionType; aStayStill: Boolean;
aStillFrame: Byte; aLocked: Boolean);
constructor Load(LoadStream: TKMemoryStream); override;
function ActName: TKMUnitActionName; override;
function CanBeInterrupted(aForced: Boolean = True): Boolean; override;
function GetExplanation: UnicodeString; override;
function Execute: TKMActionResult; override;
procedure Save(SaveStream: TKMemoryStream); override;
function ObjToStringShort(const aSeparator: String = ' '): String; override;
end;
implementation
uses
KM_HandsCollection, KM_Sound, KM_ResSound, KM_Resource, KM_ResUnits;
{ TUnitActionStay }
constructor TKMUnitActionStay.Create(aUnit: TKMUnit; aTimeToStay: Integer; aActionType: TKMUnitActionType; aStayStill: Boolean;
aStillFrame: Byte; aLocked: Boolean);
begin
inherited Create(aUnit, aActionType, aLocked);
StayStill := aStayStill;
TimeToStay := aTimeToStay;
StillFrame := aStillFrame;
end;
constructor TKMUnitActionStay.Load(LoadStream: TKMemoryStream);
begin
inherited;
LoadStream.CheckMarker('UnitActionStay');
LoadStream.Read(StayStill);
LoadStream.Read(TimeToStay);
LoadStream.Read(StillFrame);
end;
function TKMUnitActionStay.ActName: TKMUnitActionName;
begin
Result := uanStay;
end;
function TKMUnitActionStay.GetExplanation: UnicodeString;
begin
Result := 'Staying';
end;
procedure TKMUnitActionStay.MakeSound(Cycle, Step: Byte);
begin
if SKIP_SOUND then Exit;
//Do not play sounds if unit is invisible to gMySpectator
if gMySpectator.FogOfWar.CheckTileRevelation(fUnit.Position.X, fUnit.Position.Y) < 255 then exit;
//Various UnitTypes and ActionTypes produce all the sounds
case fUnit.UnitType of
utBuilder: case ActionType of
uaWork: if Step = 3 then gSoundPlayer.Play(sfxHouseBuild, fUnit.PositionF);
uaWork1: if Step = 0 then gSoundPlayer.Play(sfxDig, fUnit.PositionF);
uaWork2: if Step = 8 then gSoundPlayer.Play(sfxPave, fUnit.PositionF);
end;
utFarmer: case ActionType of
uaWork: if Step = 8 then gSoundPlayer.Play(sfxCornCut, fUnit.PositionF);
uaWork1: if Step = 0 then gSoundPlayer.Play(sfxCornSow, fUnit.PositionF,True,0.6);
end;
utStonemason: if ActionType = uaWork then
if Step = 3 then gSoundPlayer.Play(sfxMinestone, fUnit.PositionF,True,1.4);
utWoodCutter: case ActionType of
uaWork: if (fUnit.AnimStep mod Cycle = 3) and (fUnit.Direction <> dirN) then gSoundPlayer.Play(sfxChopTree, fUnit.PositionF,True)
else if (fUnit.AnimStep mod Cycle = 0) and (fUnit.Direction = dirN) then gSoundPlayer.Play(sfxWoodcutterDig, fUnit.PositionF,True);
end;
end;
end;
function TKMUnitActionStay.Execute: TKMActionResult;
var
cycle, step: Byte;
begin
if not StayStill then
begin
cycle := Max(gRes.Units[fUnit.UnitType].UnitAnim[ActionType, fUnit.Direction].Count, 1);
step := fUnit.AnimStep mod cycle;
StepDone := fUnit.AnimStep mod cycle = 0;
if TimeToStay >= 1 then MakeSound(cycle, step);
Inc(fUnit.AnimStep);
end
else
begin
fUnit.AnimStep := StillFrame;
StepDone := True;
end;
Dec(TimeToStay);
if TimeToStay <= 0 then
Result := arActDone
else
Result := arActContinues;
end;
procedure TKMUnitActionStay.Save(SaveStream: TKMemoryStream);
begin
inherited;
SaveStream.PlaceMarker('UnitActionStay');
SaveStream.Write(StayStill);
SaveStream.Write(TimeToStay);
SaveStream.Write(StillFrame);
end;
function TKMUnitActionStay.CanBeInterrupted(aForced: Boolean = True): Boolean;
begin
Result := not Locked; //Initial pause before leaving barracks is locked
end;
function TKMUnitActionStay.ObjToStringShort(const aSeparator: String): String;
begin
Result := inherited + Format('%s[StayStill = %s%sTimeToStay = %d%sStillFrame = %d]', [
aSeparator,
BoolToStr(StayStill, True), aSeparator,
TimeToStay, aSeparator,
StillFrame]);
end;
end.
@@ -0,0 +1,209 @@
unit KM_UnitActionSteer;
{$I KaM_Remake.inc}
interface
uses
Classes, SysUtils, Math, KM_Defaults, KM_CommonClasses, KM_Units, KM_Points;
{Steer in place for set time}
type
TKMUnitActionSteer = class(TKMUnitAction)
private
fDesireToSteer, fStuckFor: Byte; //Likelihood of changing direction
fVertexOccupied: TKMPoint; //The diagonal vertex we are currently occupying
fNextPos: TKMPoint; //The tile we are currently walking to
procedure IncVertex(const aFrom, aTo: TKMPoint);
procedure DecVertex;
function ChooseNextStep(out Point: TKMPoint): Boolean;
public
constructor Create(aUnit: TKMUnit; aActionType: TKMUnitActionType; aLocked: Boolean);
constructor Load(LoadStream: TKMemoryStream); override;
destructor Destroy; override;
function ActName: TKMUnitActionName; override;
function GetExplanation: UnicodeString; override;
function Execute: TKMActionResult; override;
procedure Save(SaveStream: TKMemoryStream); override;
end;
implementation
uses
KM_CommonUtils, KM_Resource, KM_ResUnits;
{ TUnitActionSteer }
constructor TKMUnitActionSteer.Create(aUnit: TKMUnit; aActionType: TKMUnitActionType; aLocked: Boolean);
begin
inherited Create(aUnit, aActionType, aLocked);
Assert(aUnit is TKMUnitAnimal); //Only animals do steering
fVertexOccupied := KMPOINT_ZERO;
fNextPos := KMPOINT_ZERO;
end;
destructor TKMUnitActionSteer.Destroy;
begin
if not KMSamePoint(fVertexOccupied, KMPOINT_ZERO) then
DecVertex;
inherited;
end;
constructor TKMUnitActionSteer.Load(LoadStream: TKMemoryStream);
begin
inherited;
LoadStream.CheckMarker('UnitActionSteer');
LoadStream.Read(fDesireToSteer);
LoadStream.Read(fStuckFor);
LoadStream.Read(fVertexOccupied);
LoadStream.Read(fNextPos);
end;
function TKMUnitActionSteer.ActName: TKMUnitActionName;
begin
Result := uanSteer;
end;
function TKMUnitActionSteer.GetExplanation: UnicodeString;
begin
Result := 'Steering';
end;
procedure TKMUnitActionSteer.IncVertex(const aFrom, aTo: TKMPoint);
begin
//Tell gTerrain that this vertex is being used so no other unit walks over the top of us
Assert(KMSamePoint(fVertexOccupied, KMPOINT_ZERO), 'Steer vertex in use');
fUnit.VertexAdd(aFrom,aTo);
fVertexOccupied := KMGetDiagVertex(aFrom,aTo);
end;
procedure TKMUnitActionSteer.DecVertex;
begin
//Tell gTerrain that this vertex is not being used anymore
Assert(not KMSamePoint(fVertexOccupied, KMPOINT_ZERO), 'DecVertex 0:0 Steer');
fUnit.VertexRem(fVertexOccupied);
fVertexOccupied := KMPOINT_ZERO;
end;
function TKMUnitActionSteer.ChooseNextStep(out Point: TKMPoint): Boolean;
var
I,K,J: Integer;
loc: TKMPoint;
list: TKMPointList;
goodSpot: Boolean;
begin
Inc(fDesireToSteer);
//Default is the next tile in the direction we're going
loc := KMGetPointInDir(fUnit.Position, fUnit.Direction);
//Decide whether we should change direction or not
if (KaMRandom(10{$IFDEF DBG_RNG_SPY}, 'TKMUnitActionSteer.ChooseNextStep'{$ENDIF}) < fDesireToSteer)
or not fUnit.CanStepTo(loc.X, loc.Y, fUnit.DesiredPassability) then
begin
fDesireToSteer := 0; //Reset it
list := TKMPointList.Create;
loc := fUnit.Position;
for I:=-1 to 1 do
for K:=-1 to 1 do
if ((I<>0)or(K<>0)) and fUnit.CanStepTo(loc.X+I, loc.Y+K, fUnit.DesiredPassability) then
begin
//Directions next to our current one are preferable (looks nicer if animals don't make jarring direction changes often)
goodSpot := KMGetDirection(I, K) in [KMNextDirection(fUnit.Direction), KMPrevDirection(fUnit.Direction)];
for J:=0 to 5*Byte(goodSpot) do
list.Add(KMPoint(loc.X+I, loc.Y+K));
end;
Result := list.GetRandom(Point);
list.Free;
end
else
begin
Point := loc;
Result := True;
end;
end;
function TKMUnitActionSteer.Execute: TKMActionResult;
const
STUCK_MAX_TIME = 200;
var
dx, dy: Shortint;
walkX, walkY, distance: Single;
firstStep: Boolean;
begin
if KMSamePoint(fNextPos, KMPOINT_ZERO) then
begin
fNextPos := fUnit.Position; //Set fNextPos to current pos so it initializes on the first run
firstStep := True;
end
else
firstStep := False;
// Use umtWalk move type here, since we just want to evaluate if we are close enough
distance := gRes.Units[fUnit.UnitType].GetEffectiveWalkSpeed(False);
if KMSamePointF(fUnit.PositionF, KMPointF(fNextPos), distance/2) then
begin
//Set precise position to avoid rounding errors
fUnit.PositionF := KMPointF(fNextPos);
//No longer using previous vertex
if KMStepIsDiag(fUnit.PositionPrev, fUnit.PositionNext) and not firstStep and (fStuckFor = 0) then
DecVertex;
//Decide on next step
if not ChooseNextStep(fNextPos) then
begin
Inc(fStuckFor);
if fStuckFor > STUCK_MAX_TIME then
Result := arActAborted //We have been stuck for a while so abort and TKMUnitAnimal.UpdateState will kill us
else
Result := arActContinues;
Exit;
end;
fStuckFor := 0;
//Do some house keeping because we have now stepped on a new tile
fUnit.PositionNext := fNextPos;
fUnit.Walk(fUnit.PositionPrev, fUnit.PositionNext); //Pre-occupy next tile
if KMStepIsDiag(fUnit.PositionPrev,fUnit.PositionNext) then
IncVertex(fUnit.PositionPrev,fUnit.PositionNext);
//Update unit direction so we are facing the way we are going
fUnit.Direction := KMGetDirection(fUnit.PositionPrev, fUnit.PositionNext);
end;
walkX := fNextPos.X - fUnit.PositionF.X;
walkY := fNextPos.Y - fUnit.PositionF.Y;
dx := sign(walkX); //-1,0,1
dy := sign(walkY); //-1,0,1
distance := gRes.Units[fUnit.UnitType].GetEffectiveWalkSpeed((dx <> 0) and (dy <> 0));
fUnit.PositionF := KMPointF(fUnit.PositionF.X + dx*Math.min(distance,abs(walkX)),
fUnit.PositionF.Y + dy*Math.min(distance,abs(walkY)));
Inc(fUnit.AnimStep);
Result := arActContinues;
end;
procedure TKMUnitActionSteer.Save(SaveStream: TKMemoryStream);
begin
inherited;
SaveStream.PlaceMarker('UnitActionSteer');
SaveStream.Write(fDesireToSteer);
SaveStream.Write(fStuckFor);
SaveStream.Write(fVertexOccupied);
SaveStream.Write(fNextPos);
end;
end.
@@ -0,0 +1,222 @@
unit KM_UnitActionStormAttack;
{$I KaM_Remake.inc}
interface
uses
Classes, Math,
KM_CommonClasses, KM_Defaults, KM_Points, KM_CommonUtils,
KM_Units;
{Charge forwards until we are tired or hit an obstacle}
type
TKMUnitActionStormAttack = class(TKMUnitAction)
private
fDelay: Integer; //Delay before action starts
fTileSteps: Integer; //The number of tiles we have walked onto so far
fStamina: Integer; //How much stamina to run do we have
fNextPos: TKMPoint; //The tile we are currently walking to
fVertexOccupied: TKMPoint; //The diagonal vertex we are currently occupying
procedure IncVertex(const aFrom, aTo: TKMPoint);
procedure DecVertex;
public
constructor Create(aUnit: TKMUnit; aActionType: TKMUnitActionType; aRow: Integer);
constructor Load(LoadStream: TKMemoryStream); override;
destructor Destroy; override;
function ActName: TKMUnitActionName; override;
function CanBeInterrupted(aForced: Boolean = True): Boolean; override;
function GetExplanation: UnicodeString; override;
function GetSpeed: Single;
function Execute: TKMActionResult; override;
procedure Save(SaveStream: TKMemoryStream); override;
end;
implementation
uses
KM_Resource, KM_ResUnits, KM_UnitWarrior;
{ TUnitActionStormAttack }
constructor TKMUnitActionStormAttack.Create(aUnit: TKMUnit; aActionType: TKMUnitActionType; aRow: Integer);
const
//Tiles traveled measured in KaM TPR: Min 8, maximum 13
//We reduced the variation in order to make storm attack more useful
MIN_STAMINA = 12;
MAX_STAMINA = 13;
begin
inherited Create(aUnit, aActionType, True);
fTileSteps := -1; //-1 so the first initializing step makes it 0
fDelay := aRow * 5; //No delay for the first row
fStamina := MIN_STAMINA + KaMRandom(MAX_STAMINA - MIN_STAMINA + 1{$IFDEF DBG_RNG_SPY}, 'TKMUnitActionStormAttack.Create'{$ENDIF});
fNextPos := KMPOINT_ZERO;
fVertexOccupied := KMPOINT_ZERO;
end;
destructor TKMUnitActionStormAttack.Destroy;
begin
if not KMSamePoint(fVertexOccupied, KMPOINT_ZERO) then
DecVertex;
inherited;
end;
constructor TKMUnitActionStormAttack.Load(LoadStream: TKMemoryStream);
begin
inherited;
LoadStream.CheckMarker('UnitActionStormAttack');
LoadStream.Read(fDelay);
LoadStream.Read(fTileSteps);
LoadStream.Read(fStamina);
LoadStream.Read(fNextPos);
LoadStream.Read(fVertexOccupied);
end;
function TKMUnitActionStormAttack.ActName: TKMUnitActionName;
begin
Result := uanStormAttack;
end;
function TKMUnitActionStormAttack.GetExplanation: UnicodeString;
begin
Result := 'Storming';
end;
procedure TKMUnitActionStormAttack.IncVertex(const aFrom, aTo: TKMPoint);
begin
//Tell gTerrain that this vertex is being used so no other unit walks over the top of us
Assert(KMSamePoint(fVertexOccupied, KMPOINT_ZERO), 'Storm vertex in use');
//Assert(not gTerrain.HasVertexUnit(KMGetDiagVertex(aFrom,aTo)), 'Storm vertex blocked');
fUnit.VertexAdd(aFrom,aTo); //Running counts as walking
fVertexOccupied := KMGetDiagVertex(aFrom,aTo);
end;
procedure TKMUnitActionStormAttack.DecVertex;
begin
//Tell gTerrain that this vertex is not being used anymore
Assert(not KMSamePoint(fVertexOccupied, KMPOINT_ZERO), 'DecVertex 0:0 Storm');
fUnit.VertexRem(fVertexOccupied);
fVertexOccupied := KMPOINT_ZERO;
end;
function TKMUnitActionStormAttack.GetSpeed: Single;
begin
if (fTileSteps <= 0) or (fTileSteps >= fStamina-1) then
Result := gRes.Units[fUnit.UnitType].GetEffectiveWalkSpeed(DIAG_DIRECTION[fUnit.Direction])
else
Result := gRes.Units[fUnit.UnitType].GetEffectiveStormSpeed(DIAG_DIRECTION[fUnit.Direction]);
end;
function TKMUnitActionStormAttack.Execute: TKMActionResult;
var
dx, dy: ShortInt;
walkX, walkY, distance: Single;
isWalking, isDiag: Boolean;
begin
if KMSamePoint(fNextPos, KMPOINT_ZERO) then
fNextPos := fUnit.Position; //Set fNextPos to current pos so it initializes on the first run
//Walk for the first step before running
if fDelay > 0 then
begin
Dec(fDelay);
fUnit.AnimStep := UNIT_STILL_FRAMES[fUnit.Direction];
Exit(arActContinues);
end;
//Last step is walking, others are running (unit gets tired and slows at the end)
//In KaM the first step was also walking, but this makes it less useful/surprising
isWalking := (fTileSteps >= fStamina - 1);
if isWalking then
begin
// Use umtWalk move type here, since we just want to evaluate if we are close enough
distance := gRes.Units[fUnit.UnitType].GetEffectiveWalkSpeed(False);
fType := uaWalk;
end else begin
distance := gRes.Units[fUnit.UnitType].GetEffectiveStormSpeed(False);
fType := uaSpec;
end;
if KMSamePointF(fUnit.PositionF, KMPointF(fNextPos), distance/2) then
begin
Inc(fTileSteps); //We have stepped on a new tile
//Set precise position to avoid rounding errors
fUnit.PositionF := KMPointF(fNextPos);
//No longer using previous vertex
if KMStepIsDiag(fUnit.PositionPrev, fUnit.PositionNext) and (fTileSteps > 0) then
DecVertex;
//Check for units nearby to fight
Locked := False; //Unlock during this check only so CheckForEnemy can abandon our action
if (fUnit is TKMUnitWarrior) then
if TKMUnitWarrior(fUnit).CheckForEnemy then
begin
//If we've picked a fight it means this action no longer exists,
//so we must exit out (don't set ActDone as that will now apply to fight action)
Exit(arActContinues);
end;
Locked := True; //Finished CheckForEnemy, so lock again
//Begin the next step
fNextPos := KMGetPointInDir(fUnit.Position, fUnit.Direction);
//Action ends if: 1: Used up stamina. 2: There is an enemy to fight. 3: NextPos is an obsticle
if (fTileSteps >= fStamina) or not fUnit.CanStepTo(fNextPos.X, fNextPos.Y, fUnit.DesiredPassability) then
Exit(arActDone); //Finished run; Must exit right away as we might have changed this action to fight
//Do some house keeping because we have now stepped on a new tile
fUnit.PositionNext := fNextPos;
fUnit.Walk(fUnit.PositionPrev, fUnit.PositionNext); //Pre-occupy next tile
if KMStepIsDiag(fUnit.PositionPrev,fUnit.PositionNext) then
IncVertex(fUnit.PositionPrev,fUnit.PositionNext);
end;
walkX := fNextPos.X - fUnit.PositionF.X;
walkY := fNextPos.Y - fUnit.PositionF.Y;
dx := Sign(walkX); //-1,0,1
dy := Sign(walkY); //-1,0,1
isDiag := (dx <> 0) and (dy <> 0);
if isWalking then
distance := gRes.Units[fUnit.UnitType].GetEffectiveWalkSpeed(isDiag)
else
distance := gRes.Units[fUnit.UnitType].GetEffectiveStormSpeed(isDiag);
fUnit.PositionF := KMPointF(fUnit.PositionF.X + dx*Math.min(distance, Abs(walkX)),
fUnit.PositionF.Y + dy*Math.min(distance, Abs(walkY)));
Inc(fUnit.AnimStep);
StepDone := False; //We are not actually done because now we have just taken another step
Result := arActContinues;
end;
procedure TKMUnitActionStormAttack.Save(SaveStream: TKMemoryStream);
begin
inherited;
SaveStream.PlaceMarker('UnitActionStormAttack');
SaveStream.Write(fDelay);
SaveStream.Write(fTileSteps);
SaveStream.Write(fStamina);
SaveStream.Write(fNextPos);
SaveStream.Write(fVertexOccupied);
end;
function TKMUnitActionStormAttack.CanBeInterrupted(aForced: Boolean = True): Boolean;
begin
Result := not Locked; //Never interupt storm attack
end;
end.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,216 @@
unit KM_UnitTaskAttackHouse;
{$I KaM_Remake.inc}
interface
uses
Classes, SysUtils, Math,
KM_CommonClasses, KM_Defaults, KM_Houses, KM_Units, KM_UnitWarrior, KM_Points;
type
// Attack a house
TKMTaskAttackHouse = class(TKMUnitTask)
private
fHouse: TKMHouse;
public
constructor Create(aWarrior: TKMUnitWarrior; aHouse: TKMHouse);
constructor Load(LoadStream: TKMemoryStream); override;
procedure SyncLoad; override;
destructor Destroy; override;
property House: TKMHouse read fHouse;
function WalkShouldAbandon: Boolean; override;
function Execute: TKMTaskResult; override;
procedure Save(SaveStream: TKMemoryStream); override;
end;
implementation
uses
KM_Entity,
KM_HandsCollection,
KM_Resource, KM_ResSound, KM_ResUnits,
KM_Sound, KM_Projectiles, KM_GameParams;
const
MeleeSoundsHouse: array [0..12] of TKMSoundEffectOriginal = (
sfxMelee37, sfxMelee38, sfxMelee39, sfxMelee40, sfxMelee41,
sfxMelee42, sfxMelee43, sfxMelee47, sfxMelee51, sfxMelee52,
sfxMelee53, sfxMelee54, sfxMelee57
);
{ TKMTaskAttackHouse }
constructor TKMTaskAttackHouse.Create(aWarrior: TKMUnitWarrior; aHouse: TKMHouse);
begin
inherited Create(aWarrior);
fType := uttAttackHouse;
fHouse := aHouse.GetPointer;
end;
constructor TKMTaskAttackHouse.Load(LoadStream: TKMemoryStream);
begin
inherited;
LoadStream.CheckMarker('TaskAttackHouse');
LoadStream.Read(fHouse, 4);
end;
procedure TKMTaskAttackHouse.SyncLoad;
begin
inherited;
fHouse := gHands.GetHouseByUID(Integer(fHouse));
end;
destructor TKMTaskAttackHouse.Destroy;
begin
gHands.CleanUpHousePointer(fHouse);
inherited;
end;
function TKMTaskAttackHouse.WalkShouldAbandon: Boolean;
begin
Result := fHouse.IsDestroyed;
end;
function TKMTaskAttackHouse.Execute: TKMTaskResult;
var
AnimLength: Integer;
Delay, Cycle: Integer;
closest: TKMPoint;
begin
Result := trTaskContinues;
//If the house is destroyed drop the task
if WalkShouldAbandon then
begin
Result := trTaskDone;
Exit;
end;
with TKMUnitWarrior(fUnit) do
case fPhase of
0: if IsRanged then
if fHouse.GetDistance(Position) < GetFightMinRange then
//Archer is too close, try to step back to the minimum range
SetActionWalkFromHouse(fHouse, GetFightMinRange)
else
if fHouse.GetDistance(Position) > GetFightMaxRange then
SetActionWalkToHouse(fHouse, GetFightMaxRange)
else
SetActionStay(0, uaWalk)
else
SetActionWalkToHouse(fHouse, 1);
1: begin
if IsRanged then
begin
//Check if the walk failed
if (fHouse.GetDistance(Position) < GetFightMinRange) or (fHouse.GetDistance(Position) > GetFightMaxRange) then
begin
SetActionStay(0, uaWalk);
Result := trTaskDone;
Exit;
end;
//Calculate base aiming delay
Delay := AimingDelay;
//Prevent rate of fire exploit by making archers pause for longer if they shot recently
Cycle := Max(gRes.Units[UnitType].UnitAnim[uaWork, Direction].Count, 1) - FiringDelay;
if NeedsToReload(Cycle) then
Delay := Delay + Cycle - (gGameParams.Tick - LastShootTime);
SetActionLockedStay(Delay,uaWork); //Pretend to aim
if not KMSamePoint(Position, fHouse.GetClosestCell(Position)) then //Unbuilt houses can be attacked from within
Direction := KMGetDirection(PositionNext, fHouse.Entrance); //Look at house
if gMySpectator.FogOfWar.CheckTileRevelation(Round(PositionF.X), Round(PositionF.Y)) >= 255 then
case UnitType of
utCrossbowman: gSoundPlayer.Play(sfxCrossbowDraw, PositionF); //Aiming
utBowman: gSoundPlayer.Play(sfxBowDraw, PositionF); //Aiming
utRogue: ;
else raise Exception.Create('Unknown shooter');
end;
end
else
begin
//Check if the walk failed
if fHouse.GetDistance(Position) > GetFightMaxRange then
begin
SetActionStay(0, uaWalk);
Result := trTaskDone;
Exit;
end;
SetActionLockedStay(0,uaWork,False); //Melee units pause after the hit
closest := fHouse.GetClosestCell(PositionNext);
if not KMSamePoint(PositionNext, closest) then //Unbuilt houses can be attacked from within
Direction := KMGetDirection(PositionNext, closest); //Look at house
end;
end;
2: begin
//Special case - slingshot, he has AimSoundDelay
if UnitType = utRogue then
SetActionLockedStay(AimSoundDelay, uaWork, False) //Start shooting before sound
else
begin
Inc(fPhase); //Skip slingshot special phase
if IsRanged then
SetActionLockedStay(FiringDelay, uaWork, False) //Start shooting
else
SetActionLockedStay(6, uaWork, False); //Start the hit
end;
end;
3: begin
gSoundPlayer.Play(sfxSlingerShoot, PositionF);
SetActionLockedStay(FiringDelay - AimSoundDelay, uaWork, False, 0, AimSoundDelay) //Start shooting before sound
end;
4: begin
if IsRanged then
begin
//Launch the missile and forget about it
//Shooting range is not important now, houses don't walk (except Howl's Moving Castle perhaps)
//todo -cComplicated: Slingers (rogues) should launch rock part on SLINGSHOT_FIRING_DELAY like they do in ActionFight (animation looks wrong now)
gProjectiles.AimTargetHouse(PositionF, fHouse, ProjectileType, fUnit, RangeMax, RangeMin);
SetLastShootTime; //Record last time the warrior shot
AnimLength := gRes.Units[UnitType].UnitAnim[uaWork, Direction].Count;
SetActionLockedStay(AnimLength - FiringDelay, uaWork, False, 0, FiringDelay); //Reload for next attack
fPhase := 0; //Go for another shot (will be 1 after inc below)
end
else
begin
SetActionLockedStay(6, uaWork, False, 0, 6); // Pause for next attack
//All melee units do 2 damage per strike
fHouse.AddDamage(2, fUnit);
//Play a sound. We should not use KaMRandom here because sound playback depends on FOW and is individual for each player
if gMySpectator.FogOfWar.CheckTileRevelation(Position.X, Position.Y) >= 255 then
gSoundPlayer.Play(MeleeSoundsHouse[Random(Length(MeleeSoundsHouse))], PositionF);
fPhase := 1; //Go for another hit (will be 2 after inc below)
end;
end;
end;
Inc(fPhase);
end;
procedure TKMTaskAttackHouse.Save(SaveStream: TKMemoryStream);
begin
inherited;
SaveStream.PlaceMarker('TaskAttackHouse');
SaveStream.Write(fHouse.UID);
end;
end.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,607 @@
unit KM_UnitTaskDelivery;
{$I KaM_Remake.inc}
interface
uses
Classes, SysUtils,
KM_CommonClasses, KM_Defaults, KM_Points,
KM_Houses, KM_Units,
KM_ResTypes;
type
TKMDeliverKind = (dkToHouse, dkToConstruction, dkToUnit);
TKMDeliverStage = (
dsUnknown,
dsToFromHouse, // Serf is walking to the offer house
dsAtFromHouse, // Serf is getting in / out from offer house
dsToDestination, // Serf is walking to destination (unit/house)
dsAtDestination); // Serf is operating with destination
TKMTaskDeliver = class(TKMUnitTask)
private
fFrom: TKMHouse;
fToHouse: TKMHouse;
fToUnit: TKMUnit;
fWareType: TKMWareType;
fDeliverID: Integer;
fDeliverKind: TKMDeliverKind;
// Force delivery, even if fToHouse blocked ware from delivery.
// Used in exceptional situation, when ware was carried by serf and delivery demand was destroyed and no one new was found
fForceDelivery: Boolean;
procedure CheckForBetterDestination;
function FindBestDestination: Boolean;
function GetDeliverStage: TKMDeliverStage;
property FromHouse: TKMHouse read fFrom write fFrom;
property ToHouse: TKMHouse read fToHouse write fToHouse;
property ToUnit: TKMUnit read fToUnit write fToUnit;
function CanAbandonWalk: Boolean;
public
constructor Create(aSerf: TKMUnitSerf; aFrom: TKMHouse; aToHouse: TKMHouse; aWare: TKMWareType; aID: Integer); overload;
constructor Create(aSerf: TKMUnitSerf; aFrom: TKMHouse; aToUnit: TKMUnit; aWare: TKMWareType; aID: Integer); overload;
constructor Load(LoadStream: TKMemoryStream); override;
procedure SyncLoad; override;
destructor Destroy; override;
function WalkShouldAbandon: Boolean; override;
property DeliverKind: TKMDeliverKind read fDeliverKind;
property DeliverStage: TKMDeliverStage read GetDeliverStage;
procedure DelegateToOtherSerf(aToSerf: TKMUnitSerf);
function Execute: TKMTaskResult; override;
function CouldBeCancelled: Boolean; override;
function CanRestartAction(aLastActionResult: TKMActionResult): Boolean; override;
procedure Save(SaveStream: TKMemoryStream); override;
function ObjToString(const aSeparator: String = ', '): String; override;
procedure Paint; override; // Used only for debug so far
end;
implementation
uses
Math, TypInfo,
KM_Entity,
KM_HandsCollection, KM_Hand, KM_HandTypes, KM_HandEntity,
KM_UnitWarrior, KM_HouseInn,
KM_UnitTaskBuild, KM_Log, KM_RenderAux;
{ TKMTaskDeliver }
constructor TKMTaskDeliver.Create(aSerf: TKMUnitSerf; aFrom: TKMHouse; aToHouse: TKMHouse; aWare: TKMWareType; aID: Integer);
begin
inherited Create(aSerf);
fType := uttDeliver;
Assert((aFrom <> nil) and (aToHouse <> nil) and (aWare <> wtNone), 'Serf ' + IntToStr(fUnit.UID) + ': invalid delivery task');
if gLog.CanLogDelivery then
gLog.LogDelivery('Serf ' + IntToStr(fUnit.UID) + ' created delivery task ' + IntToStr(fDeliverID));
FromHouse := aFrom.GetPointer; // Also will set fPointBelowFromHouse
ToHouse := aToHouse.GetPointer; // Also will set fPointBelowToHouse
// Check it once to begin with as the house could become complete before the task exits (in rare circumstances when the task
// does not exit until long after the ware has been delivered due to walk interactions)
if aToHouse.IsComplete then
fDeliverKind := dkToHouse
else
fDeliverKind := dkToConstruction;
fWareType := aWare;
fDeliverID := aID;
end;
constructor TKMTaskDeliver.Create(aSerf: TKMUnitSerf; aFrom: TKMHouse; aToUnit: TKMUnit; aWare: TKMWareType; aID: Integer);
begin
inherited Create(aSerf);
fType := uttDeliver;
Assert((aFrom <> nil) and (aToUnit <> nil) and ((aToUnit is TKMUnitWarrior) or (aToUnit is TKMUnitWorker)) and (aWare <> wtNone), 'Serf '+inttostr(fUnit.UID)+': invalid delivery task');
if gLog.CanLogDelivery then
gLog.LogDelivery('Serf ' + IntToStr(fUnit.UID) + ' created delivery task ' + IntToStr(fDeliverID));
FromHouse := aFrom.GetPointer;
ToUnit := aToUnit.GetPointer;
fDeliverKind := dkToUnit;
fWareType := aWare;
fDeliverID := aID;
end;
constructor TKMTaskDeliver.Load(LoadStream: TKMemoryStream);
begin
inherited;
LoadStream.CheckMarker('TaskDeliver');
LoadStream.Read(fFrom, 4);
LoadStream.Read(fToHouse, 4);
LoadStream.Read(fToUnit, 4);
LoadStream.Read(fForceDelivery);
LoadStream.Read(fWareType, SizeOf(fWareType));
LoadStream.Read(fDeliverID);
LoadStream.Read(fDeliverKind, SizeOf(fDeliverKind));
end;
procedure TKMTaskDeliver.Save(SaveStream: TKMemoryStream);
begin
inherited;
SaveStream.PlaceMarker('TaskDeliver');
SaveStream.Write(fFrom.UID); // Store ID, then substitute it with reference on SyncLoad
SaveStream.Write(fToHouse.UID); // Store ID, then substitute it with reference on SyncLoad
SaveStream.Write(fToUnit.UID); // Store ID, then substitute it with reference on SyncLoad
SaveStream.Write(fForceDelivery);
SaveStream.Write(fWareType, SizeOf(fWareType));
SaveStream.Write(fDeliverID);
SaveStream.Write(fDeliverKind, SizeOf(fDeliverKind));
end;
procedure TKMTaskDeliver.SyncLoad;
begin
inherited;
fFrom := gHands.GetHouseByUID(Integer(fFrom));
fToHouse := gHands.GetHouseByUID(Integer(fToHouse));
fToUnit := gHands.GetUnitByUID(Integer(fToUnit));
end;
destructor TKMTaskDeliver.Destroy;
begin
if gLog.CanLogDelivery then
gLog.LogDelivery('Serf ' + IntToStr(fUnit.UID) + ' abandoned delivery task ' + IntToStr(fDeliverID) + ' at phase ' + IntToStr(fPhase));
if fUnit <> nil then
begin
if fDeliverID <> DELIVERY_NO_ID then
gHands[fUnit.Owner].Deliveries.Queue.AbandonDelivery(fDeliverID);
if TKMUnitSerf(fUnit).Carry <> wtNone then
begin
gHands[fUnit.Owner].Stats.WareConsumed(TKMUnitSerf(fUnit).Carry);
TKMUnitSerf(fUnit).CarryTake; // empty hands
end;
end;
gHands.CleanUpHousePointer(fFrom);
gHands.CleanUpHousePointer(fToHouse);
gHands.CleanUpUnitPointer(fToUnit);
inherited;
end;
// Note: Phase is -1 because it will have been increased at the end of last Execute
function TKMTaskDeliver.WalkShouldAbandon: Boolean;
begin
Result := False;
if not CanAbandonWalk then
Exit;
if not gHands[fUnit.Owner].Deliveries.Queue.IsDeliveryAlowed(fDeliverID) then
Exit(True);
// After step 2 we don't care if From is destroyed or doesn't have the ware
if fPhase <= 2 then
Result := Result
or fFrom.IsDestroyed
or fFrom.ShouldAbandonDeliveryFrom(fWareType)
or fFrom.ShouldAbandonDeliveryFromTo(fToHouse, fWareType, fPhase = 2); // Make immediate check only on Phase 2 (inside house)
Result := Result or CanRestartAction(fLastActionResult);
// do not abandon the delivery if target is destroyed/dead, we will find new target later
case fDeliverKind of
dkToHouse: Result := Result or fToHouse.IsDestroyed
or (not fForceDelivery and fToHouse.ShouldAbandonDeliveryTo(fWareType));
dkToConstruction: Result := Result or fToHouse.IsDestroyed;
dkToUnit: Result := Result or (fToUnit = nil) or fToUnit.IsDeadOrDying;
end;
end;
// We can restart some actions
function TKMTaskDeliver.CanRestartAction(aLastActionResult: TKMActionResult): Boolean;
begin
Result := (aLastActionResult = arActCanNotStart)
and (fDeliverKind = dkToHouse)
and (fPhase - 1 = 6); // Serf tried to get inside destination house
end;
procedure TKMTaskDeliver.CheckForBetterDestination;
var
NewToHouse: TKMHouse;
NewToUnit: TKMUnit;
begin
gHands[fUnit.Owner].Deliveries.Queue.CheckForBetterDemand(fDeliverID, NewToHouse, NewToUnit, TKMUnitSerf(fUnit));
gHands.CleanUpHousePointer(fToHouse);
gHands.CleanUpUnitPointer(fToUnit);
if NewToHouse <> nil then
begin
ToHouse := NewToHouse.GetPointer; // Use Setter here to set up fPointBelowToHouse
if fToHouse.IsComplete then
fDeliverKind := dkToHouse
else
fDeliverKind := dkToConstruction;
end
else
begin
ToUnit := NewToUnit.GetPointer; // Use Setter here to clean up fPointBelowToHouse
fDeliverKind := dkToUnit;
end;
end;
// Try to find best destination
function TKMTaskDeliver.FindBestDestination: Boolean;
var
NewToHouse: TKMHouse;
NewToUnit: TKMUnit;
begin
if fPhase <= 2 then
begin
Result := False;
Exit;
end else
if InRange(fPhase, 3, 3) then
begin
Result := True;
Exit;
end;
fForceDelivery := False; // Reset ForceDelivery from previous runs
gHands[fUnit.Owner].Deliveries.Queue.DeliveryFindBestDemand(TKMUnitSerf(fUnit), fDeliverID, fWareType, NewToHouse, NewToUnit, fForceDelivery);
gHands.CleanUpHousePointer(fToHouse);
gHands.CleanUpUnitPointer(fToUnit);
// New House
if (NewToHouse <> nil) and (NewToUnit = nil) then
begin
ToHouse := NewToHouse.GetPointer; // Use Setter here to set up fPointBelowToHouse
if fToHouse.IsComplete then
fDeliverKind := dkToHouse
else
fDeliverKind := dkToConstruction;
Result := True;
if fPhase > 4 then
fPhase := 4;
end
else
// New Unit
if (NewToHouse = nil) and (NewToUnit <> nil) then
begin
ToUnit := NewToUnit.GetPointer; // Use Setter here to clean up fPointBelowToHouse
fDeliverKind := dkToUnit;
Result := True;
if fPhase > 4 then
fPhase := 4;
end
else
// No alternative
if (NewToHouse = nil) and (NewToUnit = nil) then
Result := False
else
// Error
raise Exception.Create('Both destinations could not be');
end;
function TKMTaskDeliver.CouldBeCancelled: Boolean;
begin
// Allow cancel task only at walking phases
Result := ((fPhase - 1) // phase was increased at the end of execution
<= 0) // <= because fPhase is 0 when task is just created
or ((fPhase - 1) = 5);
end;
// Get Delivery stage
function TKMTaskDeliver.GetDeliverStage: TKMDeliverStage;
var
Phase: Integer;
begin
Result := dsUnknown;
Phase := fPhase - 1; // fPhase is increased at the phase end
case Phase of
-10..0,4: Result := dsToFromHouse;
1..3: Result := dsAtFromHouse;
else
case fDeliverKind of
dkToHouse: begin
case Phase of
5: Result := dsToDestination;
else Result := dsAtDestination;
end;
end;
dkToConstruction,
dkToUnit: begin
case Phase of
5,6: Result := dsToDestination;
else Result := dsAtDestination;
end;
end;
end;
end;
end;
function TKMTaskDeliver.CanAbandonWalk: Boolean;
begin
case fDeliverKind of
dkToHouse: Result := fPhase <= 8;
dkToConstruction: Result := fPhase <= 7;
dkToUnit: Result := fPhase <= 6;
else
raise Exception.Create('Unexpected type');
end;
end;
// Delegate delivery task to other serf
procedure TKMTaskDeliver.DelegateToOtherSerf(aToSerf: TKMUnitSerf);
begin
// Allow to delegate task only while serf is walking to From House
Assert(DeliverStage = dsToFromHouse, 'DeliverStage <> dsToFromHouse');
gHands.CleanUpUnitPointer(fUnit);
fUnit := aToSerf.GetPointer;
InitDefaultAction; // InitDefaultAction, otherwise serf will not have any action
end;
function TKMTaskDeliver.Execute: TKMTaskResult;
var
Worker: TKMUnit;
begin
Result := trTaskContinues;
if WalkShouldAbandon and fUnit.Visible and not FindBestDestination then
Exit(trTaskDone);
with TKMUnitSerf(fUnit) do
case fPhase of
0: begin
SetActionWalkToSpot(fFrom.PointBelowEntrance);
end;
1: begin
SetActionGoIn(uaWalk, gdGoInside, fFrom);
end;
2: begin
// Serf is inside house now.
// Barracks can consume the resource (by equipping) before we arrive
// All houses can have resources taken away by script at any moment
if fFrom.ShouldAbandonDeliveryFrom(fWareType)
or fFrom.ShouldAbandonDeliveryFromTo(fToHouse, fWareType, True) then // For store evacuation
begin
SetActionLockedStay(5, uaWalk); // Wait a moment inside
fPhase := 120; // Will get out of Barracks onthat Phase
Exit;
end;
SetActionLockedStay(5,uaWalk); // Wait a moment inside
CheckForBetterDestination; // Must run before TakenOffer and before taking ware so Offer is still valid
fFrom.WareTakeFromOut(fWareType);
CarryGive(fWareType);
gHands[Owner].Deliveries.Queue.TakenOffer(fDeliverID);
end;
3: begin
if fFrom.IsDestroyed then //We have the resource, so we don't care if house is destroyed
SetActionLockedStay(0, uaWalk)
else
SetActionGoIn(uaWalk, gdGoOutside, fFrom);
Inc(fPhase); // jump to phase 5 immediately
end;
4: begin
SetActionStay(5, uaWalk); // used only from FindBestDestination
Thought := thQuest;
end;
end;
if fPhase = 5 then
TKMUnitSerf(fUnit).Thought := thNone; // Clear possible '?' thought after 4th phase
// Get out barracks (special case after wait phase inside house, if resource is not available anymore)
with TKMUnitSerf(fUnit) do
if fPhase = 120 then
begin
SetActionGoIn(uaWalk, gdGoOutside, fFrom); //Step back out
fPhase := 99; // Exit next run
Exit;
end;
// Deliver into complete house
if (fDeliverKind = dkToHouse) then
with TKMUnitSerf(fUnit) do
case fPhase of
0..4:;
5: SetActionWalkToSpot(fToHouse.PointBelowEntrance);
6: SetActionGoIn(uaWalk, gdGoInside, fToHouse);
7: SetActionLockedStay(5, uaWalk); // wait a bit inside
8: begin
fToHouse.WareAddToIn(Carry);
CarryTake;
gHands[Owner].Deliveries.Queue.GaveDemand(fDeliverID);
gHands[Owner].Deliveries.Queue.AbandonDelivery(fDeliverID);
fDeliverID := DELIVERY_NO_ID; // So that it can't be abandoned if unit dies while trying to GoOut
// If serf bring smth into the Inn and he is hungry - let him eat immediately
if fUnit.IsHungry
and (fToHouse.HouseType = htInn)
and TKMHouseInn(fToHouse).HasFood
and TKMHouseInn(fToHouse).HasSpace
and TKMUnitSerf(fUnit).GoEat(TKMHouseInn(fToHouse), True) then
Exit // Exit immediately, since we created new task here and old task is destroyed!
// Changing any task fields here (f.e. Phase) could affect new task!
else
// Now look for another delivery from inside this house
// But only if we are not hungry!
// Otherwise there is a possiblity when he will go between houses until death
if not fUnit.IsHungry
and TKMUnitSerf(fUnit).TryDeliverFrom(fToHouse) then
Exit // Exit immediately, since we created new task here and old task is destroyed!
// Changing any task fields here (f.e. Phase) could affect new task!
else
// No delivery found then just step outside
SetActionGoIn(uaWalk, gdGoOutside, fToHouse);
end;
else Result := trTaskDone;
end;
// Deliver into wip house
if (fDeliverKind = dkToConstruction) then
with TKMUnitSerf(fUnit) do
case fPhase of
0..4:;
// First come close to point below house entrance
5: SetActionWalkToSpot(fToHouse.PointBelowEntrance, uaWalk, 1.42);
6: begin
// Then check if there is a worker hitting house just from the entrance
Worker := gHands[fUnit.Owner].UnitsHitTest(fToHouse.PointBelowEntrance, utBuilder);
if (Worker <> nil) and (Worker.Task <> nil)
and (Worker.Task is TKMTaskBuildHouse)
and (Worker.Task.Phase >= 1) then
// If so, then allow to bring resources diagonally
SetActionWalkToSpot(fToHouse.Entrance, uaWalk, 1.42)
else
// else ask serf to bring resources from point below entrance (not diagonally)
SetActionWalkToSpot(fToHouse.PointBelowEntrance);
end;
7: begin
Direction := KMGetDirection(PositionNext, fToHouse.Entrance);
fToHouse.WareAddToBuild(Carry);
gHands[Owner].Stats.WareConsumed(Carry);
CarryTake;
gHands[Owner].Deliveries.Queue.GaveDemand(fDeliverID);
gHands[Owner].Deliveries.Queue.AbandonDelivery(fDeliverID);
fDeliverID := DELIVERY_NO_ID; // So that it can't be abandoned if unit dies while staying
SetActionStay(1, uaWalk);
end;
else Result := trTaskDone;
end;
// Deliver to builder or soldier
if fDeliverKind = dkToUnit then
with TKMUnitSerf(fUnit) do
case fPhase of
0..4:;
5: SetActionWalkToUnit(fToUnit, 1.42, uaWalk); // When approaching from diagonal
6: begin
// See if the unit has moved. If so we must try again
if KMLengthDiag(fUnit.Position, fToUnit.Position) > 1.5 then
begin
SetActionWalkToUnit(fToUnit, 1.42, uaWalk); //Walk to unit again
fPhase := 6;
Exit;
end;
// Worker
if (fToUnit.UnitType = utBuilder) and (fToUnit.Task <> nil) then
begin
//todo -cPractical: Replace phase numbers with enums to avoid hardcoded magic numbers
// Check if worker is still digging
if ((fToUnit.Task is TKMTaskBuildWine) and (fToUnit.Task.Phase < 5))
or ((fToUnit.Task is TKMTaskBuildRoad) and (fToUnit.Task.Phase < 4)) then
begin
SetActionLockedStay(5, uaWalk); // Wait until worker finish digging process
fPhase := 6;
Exit;
end;
fToUnit.Task.Phase := fToUnit.Task.Phase + 1;
fToUnit.SetActionLockedStay(0, uaWork1); // Tell the worker to resume work by resetting his action (causes task to execute)
end;
// Warrior
if (fToUnit is TKMUnitWarrior) then
begin
fToUnit.Feed(UNIT_MAX_CONDITION); // Feed the warrior
TKMUnitWarrior(fToUnit).RequestedFood := False;
end;
gHands[Owner].Stats.WareConsumed(Carry);
CarryTake;
gHands[Owner].Deliveries.Queue.GaveDemand(fDeliverID);
gHands[Owner].Deliveries.Queue.AbandonDelivery(fDeliverID);
fDeliverID := DELIVERY_NO_ID; // So that it can't be abandoned if unit dies while staying
SetActionLockedStay(5, uaWalk); // Pause breifly (like we are handing over the ware/food)
end;
7: begin
// After feeding troops, serf should walk away, but ToUnit could be dead by now
if (fToUnit is TKMUnitWarrior) then
begin
if TKMUnitSerf(fUnit).IsHungry then
begin
// Serf is hungry, try to go to the Inn
if TKMUnitSerf(fUnit).CheckCondition then
Exit // Exit immediately, since we created new task here and old task is destroyed!
// Changing any task fields (f.e. Phase) here could affect new task!
else
// No Inn was found then just walk back to our From house
// even if it's destroyed, its location is still valid
SetActionWalkToHouse(fFrom, 5);
end
else
if TKMUnitSerf(fUnit).TryDeliverFrom(nil) then
Exit // Exit immediately, since we created new task here and old task is destroyed!
// Changing any task fields (f.e. Phase) here could affect new task!
else
// No delivery found then just walk back to our From house
// even if it's destroyed, its location is still valid
// Don't walk to spot as it doesn't really matter
SetActionWalkToHouse(fFrom, 5);
end else
SetActionStay(0, uaWalk); //If we're not feeding a warrior then ignore this step
end;
else Result := trTaskDone;
end;
Inc(fPhase);
end;
function TKMTaskDeliver.ObjToString(const aSeparator: String = ', '): String;
var
FromStr, ToUStr, ToHStr: String;
begin
if Self = nil then Exit('nil');
FromStr := 'nil';
ToHStr := 'nil';
ToUStr := 'nil';
if fFrom <> nil then
FromStr := fFrom.ObjToStringShort(',');
if fToHouse <> nil then
ToHStr := fToHouse.ObjToStringShort(',');
if fToUnit <> nil then
ToUStr := fToUnit.ObjToStringShort(',');
Result := inherited +
Format('%s|FromH = [%s]%s|ToH = [%s]%sFromU = [%s]%s|WareT = %s',
[aSeparator,
FromStr, aSeparator,
ToHStr, aSeparator,
ToUStr, aSeparator,
GetEnumName(TypeInfo(TKMWareType), Integer(fWareType)), aSeparator]);
end;
procedure TKMTaskDeliver.Paint;
begin
if SHOW_UNIT_ROUTES
and (gMySpectator.LastSelected = fUnit) then
begin
if fFrom <> nil then
gRenderAux.RenderWireTile(fFrom.PointBelowEntrance, icDarkBlue);
if fToHouse <> nil then
gRenderAux.RenderWireTile(fToHouse.PointBelowEntrance, icLightRed);
if fToUnit <> nil then
gRenderAux.RenderWireTile(fToUnit.Position, icRed);
end;
end;
end.
+119
View File
@@ -0,0 +1,119 @@
unit KM_UnitTaskDie;
{$I KaM_Remake.inc}
interface
uses
Classes, KM_CommonClasses, KM_Defaults, KM_Units, SysUtils;
type
{Yep, this is a Task}
TKMTaskDie = class(TKMUnitTask)
private
fShowAnimation: Boolean;
public
constructor Create(aUnit: TKMUnit; aShowAnimation: Boolean);
constructor Load(LoadStream: TKMemoryStream); override;
function Execute: TKMTaskResult; override;
procedure Save(SaveStream: TKMemoryStream); override;
end;
implementation
uses
KM_ResSound, KM_Sound,
KM_HandsCollection, KM_HandTypes, KM_HandEntity,
KM_Resource, KM_UnitWarrior, KM_ScriptingEvents,
KM_ResUnits;
{ TTaskDie }
constructor TKMTaskDie.Create(aUnit: TKMUnit; aShowAnimation: Boolean);
begin
inherited Create(aUnit);
fType := uttDie;
fShowAnimation := aShowAnimation;
//Shortcut to remove the pause before the dying animation which makes fights look odd
if aUnit.Visible then
begin
fPhase := 1; //Phase 0 can be skipped when the unit is visible
Execute;
end;
end;
constructor TKMTaskDie.Load(LoadStream: TKMemoryStream);
begin
inherited;
LoadStream.CheckMarker('TaskDie');
LoadStream.Read(fShowAnimation);
end;
procedure TKMTaskDie.Save(SaveStream: TKMemoryStream);
begin
inherited;
SaveStream.PlaceMarker('TaskDie');
SaveStream.Write(fShowAnimation);
end;
function TKMTaskDie.Execute: TKMTaskResult;
var
SequenceLength: SmallInt;
TempOwner: TKMHandID;
TempUnitType: TKMUnitType;
TempX, TempY: Word;
begin
Result := trTaskContinues;
with fUnit do
case fPhase of
0: if Visible then
SetActionLockedStay(0, uaWalk)
else
begin
if (Home <> nil) and not Home.IsDestroyed then
begin
Home.SetState(hstIdle);
Home.SetState(hstEmpty);
end;
SetActionGoIn(uaWalk, gdGoOutside, gHands.HousesHitTest(fUnit.PositionNext.X, fUnit.PositionNext.Y));
end;
1: begin
if not fShowAnimation or (fUnit is TKMUnitAnimal) then //Animals don't have a dying sequence. Can be changed later.
SetActionLockedStay(0, uaWalk, False)
else
begin
SequenceLength := gRes.Units[UnitType].UnitAnim[uaDie, Direction].Count;
SetActionLockedStay(SequenceLength, uaDie, False);
//Do not play sounds if unit is invisible to gMySpectator
//We should not use KaMRandom below this line because sound playback depends on FOW and is individual for each player
if gMySpectator.FogOfWar.CheckTileRevelation(fUnit.Position.X, fUnit.Position.Y) >= 255 then
begin
if fUnit is TKMUnitWarrior then
gSoundPlayer.PlayWarrior(fUnit.UnitType, spDeath, fUnit.PositionF)
else
gSoundPlayer.PlayCitizen(fUnit.UnitType, spDeath, fUnit.PositionF);
end;
end;
end;
else begin
//Store them before they get lost forever
TempOwner := fUnit.Owner;
TempUnitType := fUnit.UnitType;
TempX := fUnit.Position.X;
TempY := fUnit.Position.Y;
fUnit.CloseUnit; //This will FreeAndNil the Task and mark unit as "closed"
//Can't run this in CloseUnit since it is used from other places like barracks equipping and we only want it for normal deaths
//Notify the script that the unit is now gone from the game
gScriptEvents.EventUnitAfterDied(TempUnitType, TempOwner, TempX, TempY);
Result := trTaskContinues; //Running UpdateState will exit without further changes
Exit; //Next UpdateState won't happen cos unit is "closed"
end;
end;
Inc(fPhase);
end;
end.
+157
View File
@@ -0,0 +1,157 @@
unit KM_UnitTaskDismiss;
{$I KaM_Remake.inc}
interface
uses
Classes, SysUtils,
KM_Defaults, KM_Units, KM_Houses, KM_CommonClasses;
type
TKMTaskDismiss = class(TKMUnitTask)
private
fSchool: TKMHouse;
protected
procedure InitDefaultAction; override;
public
constructor Create(aUnit: TKMUnit);
constructor Load(LoadStream: TKMemoryStream); override;
destructor Destroy; override;
procedure SyncLoad; override;
procedure Save(SaveStream: TKMemoryStream); override;
function ShouldBeCancelled: Boolean;
function CouldBeCancelled: Boolean; override;
property School: TKMHouse read fSchool;
function FindNewSchool: TKMHouse;
function Execute: TKMTaskResult; override;
end;
implementation
uses
KM_Entity,
KM_HandsCollection, KM_Hand, KM_HandTypes, KM_HandEntity,
KM_ResTypes, KM_ScriptingEvents;
{ TTaskDismiss }
constructor TKMTaskDismiss.Create(aUnit: TKMUnit);
begin
Assert(aUnit is TKMCivilUnit, 'Only civil units are allowed to be dismissed');
inherited;
gHands[fUnit.Owner].Stats.UnitDismissed(fUnit.UnitType);
fType := uttDismiss;
FindNewSchool;
end;
constructor TKMTaskDismiss.Load(LoadStream: TKMemoryStream);
begin
inherited;
LoadStream.CheckMarker('TaskDismiss');
LoadStream.Read(fSchool, 4);
end;
destructor TKMTaskDismiss.Destroy;
begin
gHands.CleanUpHousePointer(fSchool);
fUnit.DismissInProgress := False; //Reset dismissInProgress Flag to show proper UI
gHands[fUnit.Owner].Stats.UnitDismissCanceled(fUnit.UnitType);
inherited;
end;
function TKMTaskDismiss.ShouldBeCancelled: Boolean;
begin
Result := (fSchool = nil) or fSchool.IsDestroyed;
end;
function TKMTaskDismiss.CouldBeCancelled: Boolean;
begin
Result := fPhase <= 1; //Allow cancel dismiss only while walking to the school point below entrance
end;
procedure TKMTaskDismiss.Save(SaveStream: TKMemoryStream);
begin
inherited;
SaveStream.PlaceMarker('TaskDismiss');
if fSchool <> nil then
SaveStream.Write(fSchool.UID) //Store ID, then substitute it with reference on SyncLoad
else
SaveStream.Write(Integer(0));
end;
procedure TKMTaskDismiss.SyncLoad;
begin
inherited;
fSchool := gHands[fUnit.Owner].Houses.GetHouseByUID(Integer(fSchool));
end;
function TKMTaskDismiss.FindNewSchool: TKMHouse;
var
S: TKMHouse;
begin
fSchool := nil;
S := gHands[fUnit.Owner].FindHouse(htSchool, fUnit.Position);
if (S <> nil) and fUnit.CanWalkTo(fUnit.Position, S.PointBelowEntrance, tpWalk, 0) then
fSchool := S.GetPointer;
Result := fSchool;
end;
procedure TKMTaskDismiss.InitDefaultAction;
begin
//Do nothing here, as we have to continue old action, until it could be interrupted
end;
function TKMTaskDismiss.Execute: TKMTaskResult;
begin
Result := trTaskContinues;
if (fSchool = nil) or fSchool.IsDestroyed then
begin
Result := trTaskDone;
Exit;
end;
with fUnit do
case fPhase of
0: SetActionWalkToSpot(fSchool.PointBelowEntrance, uaWalk, 0, fUnit.AnimStep); // Preserv current AnimStep
1: SetActionGoIn(uaWalk, gdGoInside, fSchool);
2: begin
//Note: we do not set trTaskDone here, as we are going to destroy this task and Close (delete) unit
//Setting to trTaskDone will force Unit.UpadateState to find new task/action for this unit
if gMySpectator.Selected = fUnit then
gMySpectator.Selected := nil; //Reset view, in case we were watching dismissed unit
gHands[fUnit.Owner].Stats.UnitLost(fUnit.UnitType);
gHands[fUnit.Owner].Stats.CitizenRetired(fUnit.UnitType);
gScriptEvents.ProcUnitDismissed(fUnit);
TKMCivilUnit(fUnit).KillInHouse; //Kill unit silently inside house
Exit; //Exit immediately, since we destroyed current task!
//Changing any task fields here (f.e. Phase) will try to change freed memory!
end;
else Result := trTaskDone;
end;
Inc(fPhase);
end;
end.
+209
View File
@@ -0,0 +1,209 @@
unit KM_UnitTaskGoEat;
{$I KaM_Remake.inc}
interface
uses
Classes, KM_CommonClasses, KM_Defaults, KM_Units, KM_Houses, KM_HouseInn, SysUtils;
type
//Go to eat
TKMTaskGoEat = class(TKMUnitTask)
private
fFeedCnt: Byte;
fInn: TKMHouseInn; //Inn in which we are going to eat
fPlace: ShortInt; //Units place in Inn
public
constructor Create(aInn: TKMHouseInn; aUnit: TKMUnit);
constructor Load(LoadStream: TKMemoryStream); override;
procedure SyncLoad; override;
destructor Destroy; override;
function Eating: Boolean;
function Execute: TKMTaskResult; override;
function CouldBeCancelled: Boolean; override;
function WalkShouldAbandon: Boolean; override;
procedure Save(SaveStream: TKMemoryStream); override;
end;
implementation
uses
KM_Entity,
KM_HandsCollection, KM_Hand, KM_HandTypes, KM_HandEntity,
KM_ResTypes;
{ TTaskGoEat }
constructor TKMTaskGoEat.Create(aInn: TKMHouseInn; aUnit: TKMUnit);
begin
inherited Create(aUnit);
fType := uttGoEat;
fInn := TKMHouseInn(aInn.GetPointer);
fPlace := -1;
fFeedCnt := 0;
end;
constructor TKMTaskGoEat.Load(LoadStream: TKMemoryStream);
begin
inherited;
LoadStream.CheckMarker('TaskGoEat');
LoadStream.Read(fInn, 4);
LoadStream.Read(fPlace);
LoadStream.Read(fFeedCnt);
end;
procedure TKMTaskGoEat.SyncLoad;
begin
inherited;
fInn := TKMHouseInn(gHands.GetHouseByUID(Integer(fInn)));
end;
function TKMTaskGoEat.WalkShouldAbandon: Boolean;
begin
Result := fInn.IsDestroyed;
end;
destructor TKMTaskGoEat.Destroy;
begin
//May happen when we die while desperatley trying to get some food
if (fInn <> nil) and Eating then
fInn.EatersGoesOut(fPlace);
gHands.CleanUpHousePointer(TKMHouse(fInn));
inherited;
end;
function TKMTaskGoEat.Eating: Boolean;
begin
Result := fPlace <> -1;
end;
function TKMTaskGoEat.CouldBeCancelled: Boolean;
begin
Result := (fPhase - 1) //phase was increased at the end of execution
= 1; //Allow cancel task only at walking phases
end;
function TKMTaskGoEat.Execute: TKMTaskResult;
const
MAX_FEED_CNT = 2; // Max number of times unit can eat different food at Inn during one visit
begin
Result := trTaskContinues;
if fInn.IsDestroyed then
Exit(trTaskDone);
with fUnit do
case fPhase of
0: begin
Thought := thEat;
if (Home <> nil) and not Home.IsDestroyed then
Home.SetState(hstEmpty);
if not Visible and (InHouse <> nil) and not InHouse.IsDestroyed then
SetActionGoIn(uaWalk, gdGoOutside, InHouse) //Walk outside the house
else
SetActionLockedStay(0, uaWalk); //Skip this step
end;
1: SetActionWalkToSpot(fInn.PointBelowEntrance);
2: SetActionGoIn(uaWalk, gdGoInside, fInn); //Enter Inn
3: begin
SetActionLockedStay(0, uaWalk);
fPlace := fInn.EaterGetsRandomEmptySlot(UnitType);
//If there's no free place in the Inn skip to the step where we go out hungry
if fPlace = -1 then
begin
fPhase := 7;
Exit;
end;
end;
4: //Typically when unit comes to Inn he is at 13%
//Order is Bread-Sausages-Wine-Fish
//We allow unit to eat foods until he is over 90% condition
if (Condition < UNIT_MAX_CONDITION * UNIT_CONDITION_FULL_THRESHOLD)
and (fInn.CheckWareIn(wtBread) > 0) then
begin
fInn.WareTakeFromIn(wtBread);
gHands[fUnit.Owner].Stats.WareConsumed(wtBread);
SetActionLockedStay(29*4, uaEat);
Feed(UNIT_MAX_CONDITION * BREAD_RESTORE);
fInn.UpdateEater(fPlace, wtBread);
Inc(fFeedCnt);
end else
SetActionLockedStay(0, uaWalk);
5: if (Condition < UNIT_MAX_CONDITION * UNIT_CONDITION_FULL_THRESHOLD)
and (fInn.CheckWareIn(wtSausage) > 0) then
begin
fInn.WareTakeFromIn(wtSausage);
gHands[fUnit.Owner].Stats.WareConsumed(wtSausage);
SetActionLockedStay(29*4, uaEat);
Feed(UNIT_MAX_CONDITION * SAUSAGE_RESTORE);
fInn.UpdateEater(fPlace, wtSausage);
Inc(fFeedCnt);
end else
SetActionLockedStay(0, uaWalk);
6: if (fFeedCnt < MAX_FEED_CNT) //Limit max number of times to eat in the Inn
and (Condition < UNIT_MAX_CONDITION * UNIT_CONDITION_FULL_THRESHOLD)
and (fInn.CheckWareIn(wtWine) > 0) then
begin
fInn.WareTakeFromIn(wtWine);
gHands[fUnit.Owner].Stats.WareConsumed(wtWine);
SetActionLockedStay(29*4, uaEat);
Feed(UNIT_MAX_CONDITION * WINE_RESTORE);
fInn.UpdateEater(fPlace, wtWine);
Inc(fFeedCnt);
end else
SetActionLockedStay(0, uaWalk);
7: if (fFeedCnt < MAX_FEED_CNT) //Limit max number of times to eat in the Inn
and (Condition < UNIT_MAX_CONDITION * UNIT_CONDITION_FULL_THRESHOLD)
and (fInn.CheckWareIn(wtFish) > 0) then
begin
fInn.WareTakeFromIn(wtFish);
gHands[fUnit.Owner].Stats.WareConsumed(wtFish);
SetActionLockedStay(29*4, uaEat);
Feed(UNIT_MAX_CONDITION * FISH_RESTORE);
fInn.UpdateEater(fPlace, wtFish);
end else
SetActionLockedStay(0, uaWalk);
8: begin
//Stop showing hungry if we no longer are,
//but if we are then walk out of the inn thinking hungry
//so that the player will know that we haven't been fed
if Condition < UNIT_MIN_CONDITION then
Thought := thEat
else
Thought := thNone;
SetActionGoIn(uaWalk, gdGoOutside, fInn); //Exit Inn
fInn.EatersGoesOut(fPlace);
fPlace := -1;
end;
else
Result := trTaskDone;
end;
Inc(fPhase);
end;
procedure TKMTaskGoEat.Save(SaveStream: TKMemoryStream);
begin
inherited;
SaveStream.PlaceMarker('TaskGoEat');
SaveStream.Write(fInn.UID); //Store ID, then substitute it with reference on SyncLoad
SaveStream.Write(fPlace);
SaveStream.Write(fFeedCnt);
end;
end.
+68
View File
@@ -0,0 +1,68 @@
unit KM_UnitTaskGoHome;
{$I KaM_Remake.inc}
interface
uses
Classes, SysUtils,
KM_Defaults, KM_Units;
type
TKMTaskGoHome = class(TKMUnitTask)
public
constructor Create(aUnit: TKMUnit);
function Execute: TKMTaskResult; override;
function CouldBeCancelled: Boolean; override;
end;
implementation
{ TTaskGoHome }
constructor TKMTaskGoHome.Create(aUnit: TKMUnit);
begin
inherited;
fType := uttGoHome;
aUnit.Thought := thHome;
end;
function TKMTaskGoHome.CouldBeCancelled: Boolean;
begin
Result := (fPhase - 1) //phase was increased at the end of execution
<= 0; //Allow cancel task only at walking phases
end;
function TKMTaskGoHome.Execute: TKMTaskResult;
begin
Result := trTaskContinues;
if fUnit.Home.IsDestroyed then
begin
Result := trTaskDone;
Exit;
end;
with fUnit do
case fPhase of
0: begin
Thought := thHome;
SetActionWalkToSpot(Home.PointBelowEntrance);
end;
1: SetActionGoIn(uaWalk, gdGoInside, Home);
2: begin
Thought := thNone; //Only stop thinking once we are right inside
Home.SetState(hstIdle);
SetActionStay(5, uaWalk);
end;
else Result := trTaskDone;
end;
Inc(fPhase);
end;
end.
@@ -0,0 +1,61 @@
unit KM_UnitTaskGoOutShowHungry;
{$I KaM_Remake.inc}
interface
uses
Classes, KM_Defaults, KM_Units, SysUtils;
type
TKMTaskGoOutShowHungry = class(TKMUnitTask)
public
constructor Create(aUnit: TKMUnit);
function Execute: TKMTaskResult; override;
end;
implementation
uses
KM_CommonUtils;
{ TTaskGoOutShowHungry }
constructor TKMTaskGoOutShowHungry.Create(aUnit: TKMUnit);
begin
inherited Create(aUnit);
fType := uttGoOutShowHungry;
end;
function TKMTaskGoOutShowHungry.Execute: TKMTaskResult;
begin
Result := trTaskContinues;
if fUnit.Home.IsDestroyed then
Exit(trTaskDone);
with fUnit do
case fPhase of
0: begin
Thought := thEat;
SetActionStay(20, uaWalk);
end;
1: begin
SetActionGoIn(uaWalk,gdGoOutside, fUnit.Home);
Home.SetState(hstEmpty);
end;
2: SetActionLockedStay(4,uaWalk);
3: SetActionWalkToSpot(fUnit.Home.PointBelowEntrance);
4: SetActionGoIn(uaWalk, gdGoInside, fUnit.Home);
5: begin
SetActionStay(20 + KaMRandom(10{$IFDEF DBG_RNG_SPY}, 'TKMTaskGoOutShowHungry.Execute'{$ENDIF}), uaWalk);
Home.SetState(hstIdle);
end;
else begin
Thought := thNone;
Result := trTaskDone;
end;
end;
inc(fPhase);
end;
end.
+442
View File
@@ -0,0 +1,442 @@
unit KM_UnitTaskMining;
{$I KaM_Remake.inc}
interface
uses
Math, SysUtils,
KM_CommonClasses, KM_Defaults, KM_Points,
KM_Units, KM_UnitWorkplan, KM_TerrainTypes,
KM_ResTypes;
type
// Resource mining task
TKMTaskMining = class(TKMUnitTask)
private
fBeastID: Byte;
fWorkPlan: TKMUnitWorkPlan;
fDistantResAcquired: Boolean; //Was distant resorce acquired ? (stone / fish / wood / corn / wine)
function ResourceExists: Boolean;
function ResourceTileIsLocked: Boolean;
function ChooseToCutOrPlant: TKMPlantAct;
procedure FindAnotherWorkPlan;
public
constructor Create(aUnit: TKMUnit; aWare: TKMWareType);
destructor Destroy; override;
function WalkShouldAbandon: Boolean; override;
constructor Load(LoadStream: TKMemoryStream); override;
function GetActivityText: UnicodeString;
property WorkPlan: TKMUnitWorkPlan read fWorkPlan;
function Execute: TKMTaskResult; override;
function CouldBeCancelled: Boolean; override;
procedure Save(SaveStream: TKMemoryStream); override;
end;
implementation
uses
KM_Houses, KM_HouseWoodcutters, KM_HouseSwineStable,
KM_HandsCollection, KM_HandTypes, KM_HandEntity,
KM_Resource, KM_ResMapElements, KM_ResTexts,
KM_Hand, KM_ResUnits, KM_ScriptingEvents, KM_Terrain;
{ TTaskMining }
constructor TKMTaskMining.Create(aUnit: TKMUnit; aWare: TKMWareType);
begin
inherited Create(aUnit);
fType := uttMining;
fWorkPlan := TKMUnitWorkPlan.Create;
fDistantResAcquired := False;
fBeastID := 0;
fWorkPlan.FindPlan( fUnit,
fUnit.Home.HouseType,
aWare,
aUnit.Home.PointBelowEntrance,
ChooseToCutOrPlant
);
end;
destructor TKMTaskMining.Destroy;
begin
// Make sure we don't abandon and leave our house with "working" animations
if (fUnit <> nil)
and not fUnit.Home.IsDestroyed
and (fUnit.Home.GetState = hstWork) then
fUnit.Home.SetState(hstIdle);
FreeAndNil(fWorkPlan);
inherited;
end;
//Note: Phase is -1 because it will have been increased at the end of last Execute
function TKMTaskMining.WalkShouldAbandon: Boolean;
begin
Result := False;
Assert(fUnit is TKMUnitCitizen);
if fPhase = 2 then //Unit is walking to mine-position
Result := ResourceTileIsLocked or //If someone takes our place
not ResourceExists or //Resource has gone
not TKMUnitCitizen(fUnit).CanWorkAt(WorkPlan.Loc, WorkPlan.GatheringScript);
end;
//Chose if we don't care or prefer specific activity
//depending on orders or clogged output
function TKMTaskMining.ChooseToCutOrPlant: TKMPlantAct;
begin
Result := taAny;
case fUnit.Home.HouseType of
htWoodcutters: case TKMHouseWoodcutters(fUnit.Home).WoodcutterMode of
wmChop: Result := taCut;
wmPlant: Result := taPlant;
wmChopAndPlant: if fUnit.Home.CheckWareOut(wtTrunk) >= MAX_WARES_IN_HOUSE then
Result := taPlant
else
Result := taAny;
end;
htFarm: if fUnit.Home.CheckWareOut(wtCorn) >= MAX_WARES_IN_HOUSE then
Result := taPlant
else
Result := taAny;
else Result := taAny; //We don't care since other housetypes don't have concurent activities
end;
end;
constructor TKMTaskMining.Load(LoadStream: TKMemoryStream);
begin
inherited;
LoadStream.CheckMarker('TaskMining');
fWorkPlan := TKMUnitWorkPlan.Create;
fWorkPlan.Load(LoadStream);
LoadStream.Read(fDistantResAcquired);
LoadStream.Read(fBeastID);
end;
function TKMTaskMining.GetActivityText: UnicodeString;
begin
case WorkPlan.GatheringScript of
gsStoneCutter: Result := gResTexts[TX_UNIT_TASK_STONE];
gsFarmerSow: Result := gResTexts[TX_UNIT_TASK_SOW_CORN];
gsFarmerCorn: Result := gResTexts[TX_UNIT_TASK_CUTTING_CORN];
gsFarmerWine: Result := gResTexts[TX_UNIT_TASK_GRAPES];
gsFisherCatch: Result := gResTexts[TX_UNIT_TASK_FISHING];
gsWoodCutterCut: Result := gResTexts[TX_UNIT_TASK_CUT_TREE];
gsWoodCutterPlant: Result := gResTexts[TX_UNIT_TASK_PLANT_TREE];
else Result := 'Unknown';
end;
end;
//Try to find alternative target for our WorkPlan
//Happens when we discover that resource is gone or is occupied by another busy unit
//Return False if new plan could not be found
procedure TKMTaskMining.FindAnotherWorkPlan;
var OldLoc: TKMPoint; OldDir: TKMDirection;
begin
OldLoc := WorkPlan.Loc;
OldDir := WorkPlan.WorkDir;
//Tell the work plan to find a new resource of the same gathering script
if WorkPlan.FindDifferentResource(fUnit, fUnit.Home.PointBelowEntrance, OldLoc) then
begin
//Must always give us a new location (or same location but different direction)
Assert((OldDir <> WorkPlan.WorkDir) or not KMSamePoint(OldLoc, WorkPlan.Loc));
fPhase := 0; //Set the walk again (Will become 1 after this loop)
fUnit.SetActionLockedStay(0, WorkPlan.ActionWalkTo);
end else
begin
fPhase := 99; //Abandon as there is no other work plan available (Exit the task on next update)
fUnit.SetActionLockedStay(0, WorkPlan.ActionWalkTo);
end;
end;
function TKMTaskMining.ResourceTileIsLocked: Boolean;
var
P: TKMPoint;
begin
if WorkPlan.GatheringScript = gsWoodCutterCut then
begin
P := KMGetVertexTile(WorkPlan.Loc, WorkPlan.WorkDir);
// Check all tiles around the tree, same as we do in TKMTerrain.FindTree
Result := not gTerrain.CanCutTreeAtVertex(fUnit.PositionNext, P);
end
else
Result := gTerrain.TileIsLocked(WorkPlan.Loc);
end;
function TKMTaskMining.ResourceExists: Boolean;
var P: TKMPoint;
begin
with gTerrain do
case WorkPlan.GatheringScript of
gsStoneCutter: Result := TileHasStone(WorkPlan.Loc.X, WorkPlan.Loc.Y-1); //Check stone deposit above Loc, which is walkable tile
gsFarmerSow: Result := TileIsCornField(WorkPlan.Loc) and (Land^[WorkPlan.Loc.Y, WorkPlan.Loc.X].FieldAge = 0);
gsFarmerCorn: begin
Result := TileIsCornField(WorkPlan.Loc) and (Land^[WorkPlan.Loc.Y, WorkPlan.Loc.X].FieldAge = CORN_AGE_MAX);
if Result then exit; //Resource still exists so exit
//If corn has been cut we can possibly plant new corn here to save time
Result := TileIsCornField(WorkPlan.Loc) and (Land^[WorkPlan.Loc.Y, WorkPlan.Loc.X].FieldAge = 0);
if Result then
with WorkPlan do
begin
GatheringScript := gsFarmerSow; //Switch to sowing corn rather than cutting
ActionWalkFrom := uaWalkTool; //Carry our scythe back (without the corn) as the player saw us take it out
ActionWorkType := uaWork1;
WorkCyc := 10;
Product1 := wtNone; //Don't produce corn
ProdCount1 := 0;
end;
end;
gsFarmerWine: Result := TileIsWineField(WorkPlan.Loc) and (Land^[WorkPlan.Loc.Y, WorkPlan.Loc.X].FieldAge = CORN_AGE_MAX);
gsFisherCatch: Result := CatchFish(KMPointDir(WorkPlan.Loc,WorkPlan.WorkDir),True);
gsWoodCutterPlant: Result := TileGoodToPlantTree(WorkPlan.Loc.X, WorkPlan.Loc.Y);
gsWoodCutterCut: begin
P := KMGetVertexTile(WorkPlan.Loc, WorkPlan.WorkDir);
Result := ObjectIsChopableTree(P, caAgeFull) and (Land^[P.Y, P.X].TreeAge >= TREE_AGE_FULL);
end;
else Result := True;
end;
end;
function TKMTaskMining.CouldBeCancelled: Boolean;
begin
Result := (fPhase - 1) //phase was increased at the end of execution
in [1, 7]; //Allow cancel task only at walking phases
end;
{This is execution of Resource mining}
function TKMTaskMining.Execute: TKMTaskResult;
const
// Shortcuts to skip certain Phases
SkipWalk = 9;
SKIP_WORK = 11 + MAX_WORKPLAN; //Skip to certain Phases
var
D: TKMDirection;
TimeToWork, StillFrame: Integer;
actWalkFrom: TKMUnitActionType;
ResAcquired: Boolean;
begin
Result := trTaskContinues;
//there's no point in doing a task if we can't return home
if (fUnit.Home <> nil) and fUnit.Home.IsDestroyed then
begin
Result := trTaskDone;
Exit;
end;
if fDistantResAcquired then
actWalkFrom := WorkPlan.ActionWalkFrom
else
actWalkFrom := WorkPlan.ActionWalkTo;
with fUnit do
case fPhase of
0: if WorkPlan.HasToWalk then
begin
fDistantResAcquired := False; // we will set distant resource as acquired when we gather it
Home.SetState(hstEmpty);
SetActionGoIn(WorkPlan.ActionWalkTo, gdGoOutside, Home); //Walk outside the house
//Woodcutter takes his axe with him when going to chop trees
if (WorkPlan.GatheringScript = gsWoodCutterCut) then
Home.CurrentAction.SubActionRem([haFlagpole]);
end
else
begin
fDistantResAcquired := True; // there is no distant resource, so its acquired by default
fPhase := SkipWalk; //Skip walking part if there's no need in it, e.g. CoalMiner or Baker
SetActionLockedStay(0, uaWalk);
Exit;
end;
1: //We cannot assume that the walk is still valid because the terrain could have changed while we were walking out of the house.
SetActionWalkToSpot(WorkPlan.Loc, WorkPlan.ActionWalkTo);
2: //Check if we are at the location. WalkTo could have failed or resource could have been exhausted
if not KMSamePoint(PositionNext, WorkPlan.Loc) or not ResourceExists or
not TKMUnitCitizen(fUnit).CanWorkAt(WorkPlan.Loc, WorkPlan.GatheringScript) then
FindAnotherWorkPlan
else
SetActionLockedStay(0, WorkPlan.ActionWalkTo);
3: //Before work tasks for specific mining jobs
if WorkPlan.GatheringScript = gsFisherCatch then
begin
Direction := WorkPlan.WorkDir;
SetActionLockedStay(13, uaWork1, False); //Throw the line out
end else
SetActionLockedStay(0, WorkPlan.ActionWalkTo);
4: //Choose direction and time to work
begin
if WorkPlan.WorkDir <> dirNA then
Direction := WorkPlan.WorkDir;
if gRes.Units[UnitType].UnitAnim[WorkPlan.ActionWorkType, Direction].Count < 1 then
for D := dirN to dirNW do
if gRes.Units[UnitType].UnitAnim[WorkPlan.ActionWorkType, D].Count > 1 then
begin
Direction := D;
Break;
end;
TimeToWork := WorkPlan.WorkCyc * Math.max(gRes.Units[UnitType].UnitAnim[WorkPlan.ActionWorkType, Direction].Count, 1);
SetActionLockedStay(TimeToWork, WorkPlan.ActionWorkType, False);
end;
5: //After work tasks for specific mining jobs
case WorkPlan.GatheringScript of
gsWoodCutterCut: SetActionLockedStay(10, WorkPlan.ActionWorkType, True, 5, 5); //Wait for the tree to start falling down
gsFisherCatch: SetActionLockedStay(15, uaWork, False); //Pull the line in
else SetActionLockedStay(0, WorkPlan.ActionWorkType);
end;
6: begin
StillFrame := 0;
case WorkPlan.GatheringScript of //Perform special tasks if required
gsStoneCutter: fDistantResAcquired := gTerrain.DecStoneDeposit(KMPoint(WorkPlan.Loc.X,WorkPlan.Loc.Y-1));
gsFarmerSow: gTerrain.SowCorn(WorkPlan.Loc);
gsFarmerCorn: fDistantResAcquired := gTerrain.CutCorn(WorkPlan.Loc);
gsFarmerWine: fDistantResAcquired := gTerrain.CutGrapes(WorkPlan.Loc);
gsFisherCatch: begin
fDistantResAcquired := gTerrain.CatchFish(KMPointDir(WorkPlan.Loc,WorkPlan.WorkDir));
if fDistantResAcquired then
WorkPlan.ActionWorkType := uaWalkTool;
end;
gsWoodCutterPlant: //If the player placed a house plan here while we were digging don't place the
//tree so the house plan isn't canceled. This is actually the same as TSK/TPR IIRC
if TKMUnitCitizen(fUnit).CanWorkAt(WorkPlan.Loc, gsWoodCutterPlant) then
begin
gTerrain.SetObject(WorkPlan.Loc, gTerrain.ChooseTreeToPlant(WorkPlan.Loc));
fDistantResAcquired := True;
end;
gsWoodCutterCut: begin
fDistantResAcquired := gTerrain.FallTree(KMGetVertexTile(WorkPlan.Loc, WorkPlan.WorkDir));
StillFrame := 5;
end;
end;
SetActionLockedStay(WorkPlan.AfterWorkDelay, WorkPlan.ActionWorkType, True, StillFrame, StillFrame);
end;
7: begin
//Removing the tree and putting a stump is handled in gTerrain.UpdateState from FallingTrees list
SetActionWalkToSpot(Home.PointBelowEntrance, actWalkFrom); //Go home
Thought := thHome;
end;
8: SetActionGoIn(actWalkFrom, gdGoInside, Home); //Go inside
{Unit back at home and can process its booty now}
9: begin
Thought := thNone;
fPhase2 := 0;
Home.SetState(hstWork);
//Take required resources
if WorkPlan.Resource1 <> wtNone then
Home.WareTakeFromIn(WorkPlan.Resource1, WorkPlan.Count1);
if WorkPlan.Resource2 <> wtNone then
Home.WareTakeFromIn(WorkPlan.Resource2, WorkPlan.Count2);
gHands[fUnit.Owner].Stats.WareConsumed(WorkPlan.Resource1, WorkPlan.Count1);
gHands[fUnit.Owner].Stats.WareConsumed(WorkPlan.Resource2, WorkPlan.Count2);
Home.CurrentAction.SubActionAdd([haSmoke]);
if fDistantResAcquired and (fPhase2 < WorkPlan.ActCount) then
begin
Home.CurrentAction.SubActionWork(WorkPlan.HouseAct[fPhase2].Act);
//Keep unit idling till next Phase, Idle time is -1 to compensate TaskExecution Phase
SetActionLockedStay(WorkPlan.HouseAct[fPhase2].TimeToWork-1, uaWalk);
end
else
begin
fPhase := SKIP_WORK; //Skip to work complete
SetActionLockedStay(0, uaWalk);
Exit;
end;
end;
10..10 + MAX_WORKPLAN:
begin
Inc(fPhase2);
//Keep on working
if fDistantResAcquired and (fPhase2 < WorkPlan.ActCount) then
begin
Home.CurrentAction.SubActionWork(WorkPlan.HouseAct[fPhase2].Act);
if fPhase < WorkPlan.ActCount then
SetActionLockedStay(WorkPlan.HouseAct[fPhase2].TimeToWork-1, uaWalk) //-1 to compensate units UpdateState run
else
SetActionLockedStay(WorkPlan.HouseAct[fPhase2].TimeToWork-2, uaWalk) //-2 to compensate 2 UpdateStates of a unit in last Act
end
else
begin
fPhase := SKIP_WORK; //Skip to step 31
SetActionLockedStay(0, uaWalk);
Exit;
end;
end;
11 + MAX_WORKPLAN:
begin
//Feed and take horses/swines after animal breeder will finish working to prevent abusing.
//If swine/horse will appear at start player can make animal breeder go out from house and let him in again and speed up house production.
if (WorkPlan.GatheringScript = gsSwineBreeder) or (WorkPlan.GatheringScript = gsHorseBreeder) then
begin
fBeastID := TKMHouseSwineStable(Home).FeedBeasts;
TKMHouseSwineStable(Home).TakeBeast(fBeastID);
end;
case WorkPlan.GatheringScript of
gsWoodCutterCut,
gsFarmerCorn,
gsFarmerWine,
gsStoneCutter,
gsFisherCatch: ResAcquired := fDistantResAcquired; // fResAcquired was set earlier when we tried to get resource
gsCoalMiner: ResAcquired := gTerrain.DecOreDeposit(WorkPlan.Loc, wtCoal);
gsGoldMiner: ResAcquired := gTerrain.DecOreDeposit(WorkPlan.Loc, wtGoldOre);
gsIronMiner: ResAcquired := gTerrain.DecOreDeposit(WorkPlan.Loc, wtIronOre);
gsSwineBreeder: ResAcquired := fBeastID <> 0;
gsHorseBreeder: ResAcquired := fBeastID <> 0;
else ResAcquired := True;
end;
if ResAcquired then
begin
Home.WareAddToOut(WorkPlan.Product1, WorkPlan.ProdCount1);
Home.WareAddToOut(WorkPlan.Product2, WorkPlan.ProdCount2);
gHands[fUnit.Owner].Stats.WareProduced(WorkPlan.Product1, WorkPlan.ProdCount1);
gHands[fUnit.Owner].Stats.WareProduced(WorkPlan.Product2, WorkPlan.ProdCount2);
gScriptEvents.ProcWareProduced(fUnit.Home, WorkPlan.Product1, WorkPlan.ProdCount1);
gScriptEvents.ProcWareProduced(fUnit.Home, WorkPlan.Product2, WorkPlan.ProdCount2);
end;
Home.SetState(hstIdle);
SetActionLockedStay(WorkPlan.AfterWorkIdle-1, uaWalk);
end;
else Result := trTaskDone;
end;
Inc(fPhase);
end;
procedure TKMTaskMining.Save(SaveStream: TKMemoryStream);
begin
inherited;
SaveStream.PlaceMarker('TaskMining');
fWorkPlan.Save(SaveStream);
SaveStream.Write(fDistantResAcquired);
SaveStream.Write(fBeastID);
end;
end.
@@ -0,0 +1,129 @@
unit KM_UnitTaskSelfTrain;
{$I KaM_Remake.inc}
interface
uses
Classes, SysUtils,
KM_CommonClasses, KM_Defaults, KM_Houses, KM_HouseSchool, KM_Units;
//Train citizen in school
type
TKMTaskSelfTrain = class(TKMUnitTask)
private
fSchool: TKMHouseSchool;
public
constructor Create(aUnit: TKMUnit; aSchool: TKMHouseSchool);
constructor Load(LoadStream: TKMemoryStream); override;
procedure SyncLoad; override;
destructor Destroy; override;
function Execute: TKMTaskResult; override;
procedure Save(SaveStream: TKMemoryStream); override;
end;
implementation
uses
KM_Entity,
KM_HandsCollection, KM_Game;
{ TTaskSelfTrain }
constructor TKMTaskSelfTrain.Create(aUnit: TKMUnit; aSchool: TKMHouseSchool);
begin
inherited Create(aUnit);
fType := uttSelfTrain;
fSchool := TKMHouseSchool(aSchool.GetPointer);
fUnit.Visible := False;
end;
constructor TKMTaskSelfTrain.Load(LoadStream: TKMemoryStream);
begin
inherited;
LoadStream.CheckMarker('TaskSelfTrain');
LoadStream.Read(fSchool, 4);
end;
procedure TKMTaskSelfTrain.SyncLoad;
begin
inherited;
fSchool := TKMHouseSchool(gHands.GetHouseByUID(Integer(fSchool)));
end;
destructor TKMTaskSelfTrain.Destroy;
begin
if (gGame = nil) or gGame.IsExiting then Exit; //fSchool will already be freed
// If we abandon for some reason, clear the school animation
if (fSchool <> nil) and (fPhase <= 5) and not fSchool.IsDestroyed then
fSchool.SetState(hstIdle);
gHands.CleanUpHousePointer(TKMHouse(fSchool));
inherited;
end;
function TKMTaskSelfTrain.Execute:TKMTaskResult;
begin
Result := trTaskContinues;
//If the school has been destroyed then this task should not be running (school frees it on CloseHouse)
//However, if we are past phase 6 (task ends on phase 7) then the school does not know about us (we have stepped outside)
if fSchool.IsDestroyed and (fPhase <= 6) then
//School will cancel the training on own destruction
raise Exception.Create('Unexpected error. Destoyed school erases the task');
with fUnit do
case fPhase of
0: begin
fSchool.SetState(hstWork);
fSchool.CurrentAction.SubActionWork(haWork1);
SetActionLockedStay(29, uaWalk);
end;
1: begin
fSchool.CurrentAction.SubActionWork(haWork2);
SetActionLockedStay(29, uaWalk);
end;
2: begin
fSchool.CurrentAction.SubActionWork(haWork3);
SetActionLockedStay(29, uaWalk);
end;
3: begin
fSchool.CurrentAction.SubActionWork(haWork4);
SetActionLockedStay(29, uaWalk);
end;
4: begin
fSchool.CurrentAction.SubActionWork(haWork5);
SetActionLockedStay(29, uaWalk);
end;
5: begin
fSchool.SetState(hstIdle);
SetActionLockedStay(9, uaWalk);
end;
6: begin
// Put him in the school, so if it is destroyed while he is looking for place to exit he is placed somewhere
fUnit.InHouse := fSchool;
SetActionGoIn(uaWalk, gdGoOutside, fSchool);
fSchool.UnitTrainingComplete(fUnit);
if Assigned(fUnit.OnUnitTrained) then
fUnit.OnUnitTrained(fUnit);
end;
else Result := trTaskDone;
end;
Inc(fPhase);
end;
procedure TKMTaskSelfTrain.Save(SaveStream: TKMemoryStream);
begin
inherited;
SaveStream.PlaceMarker('TaskSelfTrain');
SaveStream.Write(fSchool.UID);
end;
end.
@@ -0,0 +1,118 @@
unit KM_UnitTaskThrowRock;
{$I KaM_Remake.inc}
interface
uses
Classes, SysUtils,
KM_CommonClasses, KM_Defaults, KM_Units;
type
// Throw a rock
TKMTaskThrowRock = class(TKMUnitTask)
private
fTarget: TKMUnit;
fFlightTime: Word; //Thats how long it will take a stone to hit it's target
public
constructor Create(aUnit, aTarget: TKMUnit);
destructor Destroy; override;
constructor Load(LoadStream: TKMemoryStream); override;
procedure SyncLoad; override;
function Execute: TKMTaskResult; override;
procedure Save(SaveStream: TKMemoryStream); override;
end;
implementation
uses
KM_Entity,
KM_HandsCollection, KM_Hand, KM_HandTypes, KM_HandEntity,
KM_Projectiles,
KM_CommonGameTypes, KM_ResTypes;
{ TKMTaskThrowRock }
constructor TKMTaskThrowRock.Create(aUnit, aTarget: TKMUnit);
begin
inherited Create(aUnit);
fType := uttThrowRock;
fTarget := aTarget.GetPointer;
end;
destructor TKMTaskThrowRock.Destroy;
begin
if (fUnit <> nil)
and not fUnit.Home.IsDestroyed
and (fUnit.Home.GetState = hstWork) then
fUnit.Home.SetState(hstIdle); //Make sure we don't abandon and leave our tower with "working" animations
gHands.CleanUpUnitPointer(fTarget);
inherited;
end;
constructor TKMTaskThrowRock.Load(LoadStream: TKMemoryStream);
begin
inherited;
LoadStream.CheckMarker('TaskThrowRock');
LoadStream.Read(fTarget, 4);
LoadStream.Read(fFlightTime);
end;
procedure TKMTaskThrowRock.SyncLoad;
begin
inherited;
fTarget := gHands.GetUnitByUID(Integer(fTarget));
end;
function TKMTaskThrowRock.Execute: TKMTaskResult;
begin
Result := trTaskContinues;
//Target could have been killed by another Tower or in a fight
if fUnit.Home.IsDestroyed or ((fTarget<>nil) and fTarget.IsDeadOrDying) then
begin
Result := trTaskDone;
Exit;
end;
with fUnit do
case fPhase of
0: begin
Home.SetState(hstWork); //Set house to Work state
Home.CurrentAction.SubActionWork(haWork2); //show Recruits back
SetActionStay(2, uaWalk); //pretend to be taking the stone
end;
1: begin
Home.WareTakeFromIn(wtStone, 1);
gHands[Owner].Stats.WareConsumed(wtStone);
fFlightTime := gProjectiles.AimTargetUnit(PositionF, fTarget, ptTowerRock, fUnit, WATCHTOWER_RANGE_MAX, WATCHTOWER_RANGE_MIN);
gHands.CleanUpUnitPointer(fTarget); //We don't need it anymore
SetActionLockedStay(1, uaWalk);
end;
2: SetActionLockedStay(fFlightTime, uaWalk); //Pretend to look how it goes
3: begin
Home.SetState(hstIdle);
SetActionStay(20, uaWalk); //Idle before throwing another rock
end;
else Result := trTaskDone;
end;
Inc(fPhase);
end;
procedure TKMTaskThrowRock.Save(SaveStream: TKMemoryStream);
begin
inherited;
SaveStream.PlaceMarker('TaskThrowRock');
if fTarget <> nil then
SaveStream.Write(fTarget.UID) //Store ID, then substitute it with reference on SyncLoad
else
SaveStream.Write(Integer(0));
SaveStream.Write(fFlightTime);
end;
end.