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

@@ -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.