Complete BigTask
Sync changed files to Google Drive / sync (push) Successful in 48s

This commit is contained in:
admin_d committed 2026-09-17 20:47:56 +05:00
1 parent daa3eec27a
commit 1a5b20ca9f
4 files changed
+1726 -2

No files matched your search

+3 -1
View File
@@ -9,7 +9,9 @@
"src/utils/KM_Utils.pas", "src/utils/KM_Utils.pas",
"src/terrain/KM_TerrainUtils.pas", "src/terrain/KM_TerrainUtils.pas",
"src/res/KM_ResSprites.pas", "src/res/KM_ResSprites.pas",
"src/utils/io/KM_IoPNG.pas" "src/utils/io/KM_IoPNG.pas",
"src/res/KM_ResUnits.pas",
"src/res/KM_ResHouses.pas"
], ],
"source_paths": [ "source_paths": [
"code/resource" "code/resource"
+3 -1
View File
@@ -2,6 +2,8 @@
"schema": "kam-runtime-resources/1", "schema": "kam-runtime-resources/1",
"entries": [ "entries": [
{"kind": "file", "path": "Maps/CatchTheSnake/CatchTheSnake.map"}, {"kind": "file", "path": "Maps/CatchTheSnake/CatchTheSnake.map"},
{"kind": "file", "path": "data/Sprites/Tileset.rxx"} {"kind": "file", "path": "data/Sprites/Tileset.rxx"},
{"kind": "file", "path": "data/defines/unit.dat"},
{"kind": "file", "path": "data/defines/houses.dat"}
] ]
} }
@@ -0,0 +1,995 @@
unit KM_ResHouses;
{$I KaM_Remake.inc}
interface
uses
Classes, Math, SysUtils,
KM_CommonClasses, KM_CommonTypes, KM_Defaults,
KM_ResTypes;
const
HOUSE_MIN = htArmorSmithy;
HOUSE_MAX = htWoodcutters;
HOUSES_VALID = [HOUSE_MIN..HOUSE_MAX];
HOUSES_CNT = Integer(HOUSE_MAX) - Integer(HOUSE_MIN) + 1;
type
THouseAnim = array [TKMHouseActionType] of TKMAnimLoop;
TKMHouseBuildSupply = array [1..2,1..6] of packed record MoveX, MoveY: Integer; end;
TKMHouseSupply = array [1..4, 1..5] of SmallInt;
//House fields as they are in a DAT file
TKMHouseSpecLegacy = packed record
StonePic, WoodPic, WoodPal, StonePal: SmallInt;
SupplyIn: TKMHouseSupply;
SupplyOut: TKMHouseSupply;
Anim: THouseAnim;
WoodPicSteps, StonePicSteps: Word;
a1: SmallInt;
EntranceOffsetX, EntranceOffsetY: ShortInt;
EntranceOffsetXpx, EntranceOffsetYpx: ShortInt; //When entering house units go for the door, which is offset by these values
BuildArea: array [1..10,1..10] of ShortInt;
WoodCost,StoneCost: Byte;
BuildSupply: TKMHouseBuildSupply;
a5,SizeArea: SmallInt;
SizeX,SizeY,sx2,sy2: ShortInt;
WorkerWork,WorkerRest: SmallInt;
WareInput, WareOutput: array [1..4] of ShortInt; //KaM_Remake will use its own tables for this matter
ResProductionX: ShortInt;
MaxHealth,Sight: SmallInt;
WorkerType: ShortInt;
Foot1: array [1..12] of ShortInt; //Sound indices
Foot2: array [1..12] of SmallInt; //vs sprite ID
end;
TKMHouseArea = array [1..4, 1..4] of Byte;
TKMWareType4 = array [1..4] of TKMWareType;
// This class wraps KaM House info
// it hides unused fields and adds new ones
TKMHouseSpec = class
private
fHouseType: TKMHouseType; //Our class
fNameTextID: Integer;
fHouseDat: TKMHouseSpecLegacy;
function GetArea: TKMHouseArea;
function GetDoesOrders: Boolean;
function GetGUIIcon: Word;
function GetHouseName: UnicodeString;
function GetWareInput: TKMWareType4;
function GetWareOutput: TKMWareType4;
function GetWorkerType: TKMUnitType;
function GetReleasedBy: TKMHouseType;
function GetTabletIcon: Word;
function GetSnowPic: SmallInt;
function GetUnoccupiedMsgId: SmallInt;
function GetGroundVisibleArea: TKMHouseArea;
public
constructor Create(aHouseType: TKMHouseType);
procedure LoadFromStream(Stream: TMemoryStream);
// Property accessors:
// Derived from KaM
property StonePic: Smallint read fHouseDat.StonePic;
property WoodPic: Smallint read fHouseDat.WoodPic;
property WoodPal: Smallint read fHouseDat.WoodPal;
property StonePal: Smallint read fHouseDat.StonePal;
property SupplyIn: TKMHouseSupply read fHouseDat.SupplyIn;
property SupplyOut: TKMHouseSupply read fHouseDat.SupplyOut;
property Anim: THouseAnim read fHouseDat.Anim;
property WoodPicSteps: Word read fHouseDat.WoodPicSteps;
property StonePicSteps: Word read fHouseDat.StonePicSteps;
property EntranceOffsetX: ShortInt read fHouseDat.EntranceOffsetX;
property EntranceOffsetXpx: ShortInt read fHouseDat.EntranceOffsetXpx;
property EntranceOffsetYpx: ShortInt read fHouseDat.EntranceOffsetYpx;
property WoodCost: Byte read fHouseDat.WoodCost;
property StoneCost: Byte read fHouseDat.StoneCost;
property BuildSupply: TKMHouseBuildSupply read fHouseDat.BuildSupply;
property WorkerRest: Smallint read fHouseDat.WorkerRest;
property ResProductionX: ShortInt read fHouseDat.ResProductionX;
property Sight: Smallint read fHouseDat.Sight;
property WorkerType: TKMUnitType read GetWorkerType;
function CanHasWorker: Boolean;
// Additional properties added by Remake
property BuildArea: TKMHouseArea read GetArea;
property GroundVisibleArea: TKMHouseArea read GetGroundVisibleArea;
property DoesOrders: Boolean read GetDoesOrders;
property GUIIcon: Word read GetGUIIcon;
property HouseName: UnicodeString read GetHouseName;
property HouseNameTextID: Integer read fNameTextID;
property ReleasedBy: TKMHouseType read GetReleasedBy;
property WareInput: TKMWareType4 read GetWareInput;
property WareOutput: TKMWareType4 read GetWareOutput;
property TabletIcon: Word read GetTabletIcon;
property UnoccupiedMsgId: SmallInt read GetUnoccupiedMsgId;
property SnowPic: SmallInt read GetSnowPic;
// Functions
function AcceptsWares: Boolean;
function MaxHealth: Word;
function ProducesWares: Boolean;
function IsWorkshop: Boolean;
function FlagPointTexId: Word;
procedure Outline(aList: TKMPointList);
function GetDoorwayOffset(aCheck: TKMCheckAxis): Single;
end;
TKMResHouses = class
private
fCRC: Cardinal;
fItems: array [HOUSE_MIN..HOUSE_MAX] of TKMHouseSpec;
//Swine&Horses, 5 beasts in each house, 3 ages for each beast
fBeastAnim: array [1..2,1..5,1..3] of TKMAnimLoop;
fMarketBeastAnim: array [1..3] of TKMAnimLoop;
function LoadHouseDat(const aPath: string): Cardinal;
function GetHouse(aType: TKMHouseType): TKMHouseSpec; inline;
function GetBeastAnim(aType: TKMHouseType; aBeast, aAge:integer): TKMAnimLoop;
public
constructor Create;
destructor Destroy; override;
function IsValid(aType: TKMHouseType): Boolean;
property Houses[aType: TKMHouseType]: TKMHouseSpec read GetHouse; default;
property BeastAnim[aType: TKMHouseType; aBeast, aAge: Integer]: TKMAnimLoop read GetBeastAnim;
property CRC: Cardinal read fCRC; //Return hash of all values
procedure ExportCSV(const aPath: string);
end;
const
//Sprites in the marketplace
MARKET_WARES_OFF_X = -93;
MARKET_WARES_OFF_Y = -88;
MARKET_WARES_TEX_CNT = 323;
MARKET_WARES_TEX_START = 1724; //ID of where market ware sprites start. Allows us to relocate them easily.
MARKET_WARES: array[TKMWareType] of record
TexStart: Integer; //Tex ID for first sprite
Count: Integer; //Total sprites for this resource
end
= (
(TexStart: 0; Count: 0;), //rtNone
(TexStart: MARKET_WARES_TEX_START+237; Count: 20;), //rtTrunk
(TexStart: MARKET_WARES_TEX_START+47; Count: 36;), //rtStone
(TexStart: MARKET_WARES_TEX_START+94; Count: 19;), //rtWood
(TexStart: MARKET_WARES_TEX_START+113; Count: 11;), //rtIronOre
(TexStart: MARKET_WARES_TEX_START+135; Count: 12;), //rtGoldOre
(TexStart: MARKET_WARES_TEX_START+207; Count: 11;), //rtCoal
(TexStart: MARKET_WARES_TEX_START+130; Count: 5;), //rtSteel
(TexStart: MARKET_WARES_TEX_START+147; Count: 9;), //rtGold
(TexStart: MARKET_WARES_TEX_START+1; Count: 23;), //rtWine
(TexStart: MARKET_WARES_TEX_START+24; Count: 23;), //rtCorn
(TexStart: MARKET_WARES_TEX_START+218; Count: 12;), //rtBread
(TexStart: MARKET_WARES_TEX_START+186; Count: 12;), //rtFlour
(TexStart: MARKET_WARES_TEX_START+156; Count: 9;), //rtLeather
(TexStart: MARKET_WARES_TEX_START+283; Count: 16;), //rtSausages
(TexStart: MARKET_WARES_TEX_START+299; Count: 6;), //rtPig
(TexStart: MARKET_WARES_TEX_START+230; Count: 7;), //rtSkin
(TexStart: MARKET_WARES_TEX_START+85; Count: 9;), //rtShield
(TexStart: MARKET_WARES_TEX_START+127; Count: 3;), //rtMetalShield
(TexStart: MARKET_WARES_TEX_START+165; Count: 6;), //rtArmor
(TexStart: MARKET_WARES_TEX_START+124; Count: 3;), //rtMetalArmor
(TexStart: MARKET_WARES_TEX_START+201; Count: 6;), //rtAxe
(TexStart: MARKET_WARES_TEX_START+183; Count: 3;), //rtSword
(TexStart: MARKET_WARES_TEX_START+171; Count: 6;), //rtPike
(TexStart: MARKET_WARES_TEX_START+198; Count: 3;), //rtHallebard
(TexStart: MARKET_WARES_TEX_START+177; Count: 6;), //rtBow
(TexStart: MARKET_WARES_TEX_START+83; Count: 2;), //rtArbalet
(TexStart: 0; Count: 2;), //rtHorse (defined in fMarketBeastAnim)
(TexStart: MARKET_WARES_TEX_START+305; Count: 19;), //rtFish
(TexStart: 0; Count: 0;), //rtAll
(TexStart: 0; Count: 0;), //rtWarfare
(TexStart: 0; Count: 0;) //rtFood
);
//These tables are used to convert between KaM script IDs and Remake enums
HOUSE_DAT_COUNT = 30;
//KaM scripts and HouseDat address houses in this order
HOUSE_ID_TO_TYPE: array [0 .. HOUSE_DAT_COUNT - 1] of TKMHouseType = (
htSawmill, htIronSmithy, htWeaponSmithy, htCoalMine, htIronMine,
htGoldMine, htFishermans, htBakery, htFarm, htWoodcutters,
htArmorSmithy, htStore, htStables, htSchool, htQuarry,
htMetallurgists, htSwine, htWatchTower, htTownHall, htWeaponWorkshop,
htArmorWorkshop, htBarracks, htMill, htSiegeWorkshop, htButchers,
htTannery, htNone, htInn, htVineyard, htMarket);
//TKMHouseType corresponds to this index in KaM scripts and libs
//KaM scripts are 0 based, so we must use HouseTypeToIndex[H]-1 in script usage. Other cases are 1 based.
HOUSE_TYPE_TO_ID: array [TKMHouseType] of Byte = (0, 0,
11, 21, 8, 22, 25,
4, 9, 7, 6, 28,
5, 2, 30, 16, 23,
15, 1, 14, 24, 13,
12, 17, 26, 19, 18,
3, 20, 29, 10);
implementation
uses
TypInfo, KromUtils,
KM_Outline, KM_Points, KM_PolySimplify, KM_ResTexts, KM_ResUnits, KaMResourceInput;
type
//Additional house info appended to classic format
THouseInfo = record
PlanYX: TKMHouseArea;
NeedsPlayerOrder: Boolean; //Does house output needs to be ordered by Player or it's producing by itself
BuildIcon: Word; //Icon in GUI
TabletSpriteId: Word; //House area WIP tablet
Input: TKMWareType4;
Output: TKMWareType4;
UnlockedByHouse: TKMHouseType; //Which house type allows to build this house type
SnowSpriteId: SmallInt;
GroundArea: TKMHouseArea; //Ground that is visible on house tiles (and a bit near). With weight of 'how much' is ground visible
end;
const
// Remake stores additional house properties here. This looks like House.Dat, but hardcoded.
HOUSE_DAT_X: array [HOUSE_MIN..HOUSE_MAX] of THouseInfo = (
( //Armor smithy
PlanYX: ((0,0,0,0), (0,1,1,0), (1,1,1,1), (1,2,1,1));
NeedsPlayerOrder: True;
BuildIcon: 311;
TabletSpriteId: 261;
Input: (wtCoal, wtIron, wtNone, wtNone);
Output: (wtIronArmor, wtIronShield,wtNone, wtNone);
UnlockedByHouse: htIronSmithy;
SnowSpriteId: 2074;
GroundArea: ((0,0,0,0), (1,2,2,1), (3,1,0,2), (3,1,2,3));
),
( //Armor workshop
PlanYX: ((0,0,0,0), (0,1,1,0), (0,1,1,1), (0,2,1,1));
NeedsPlayerOrder: True;
BuildIcon: 321;
TabletSpriteId: 271;
Input: (wtLeather, wtTimber, wtNone, wtNone);
Output: (wtWoodenShield, wtLeatherArmor, wtNone, wtNone);
UnlockedByHouse: htTannery;
SnowSpriteId: 2067;
GroundArea: ((0,0,0,0), (0,3,1,0), (0,0,0,1), (0,1,2,3));
),
( //Bakery
PlanYX: ((0,0,0,0), (0,1,1,1), (0,1,1,1), (0,1,1,2));
NeedsPlayerOrder: False;
BuildIcon: 308;
TabletSpriteId: 258;
Input: (wtFlour, wtNone, wtNone, wtNone);
Output: (wtBread, wtNone, wtNone, wtNone);
UnlockedByHouse: htMill;
SnowSpriteId: 2054;
GroundArea: ((0,0,0,0), (0,3,1,2), (0,1,0,1), (0,3,2,1));
),
( //Barracks
PlanYX: ((1,1,1,1), (1,1,1,1), (1,1,1,1), (1,2,1,1));
NeedsPlayerOrder: False;
BuildIcon: 322;
TabletSpriteId: 272;
Input: (wtWarfare, wtNone, wtNone, wtNone);
Output: (wtNone, wtNone, wtNone, wtNone);
UnlockedByHouse: htSawmill;
SnowSpriteId: 2075;
GroundArea: ((2,0,0,1), (0,0,0,0), (1,0,0,1), (4,1,2,2));
),
( //Butchers
PlanYX: ((0,0,0,0), (0,1,1,0), (0,1,1,1), (0,1,1,2));
NeedsPlayerOrder: False;
BuildIcon: 325;
TabletSpriteId: 275;
Input: (wtPig, wtNone, wtNone, wtNone);
Output: (wtSausage, wtNone, wtNone, wtNone);
UnlockedByHouse: htSwine;
SnowSpriteId: 2066;
GroundArea: ((0,0,0,0), (0,2,2,3), (0,0,0,1), (0,2,2,1));
),
( //Coal mine
PlanYX: ((0,0,0,0), (0,0,0,0), (1,1,1,0), (1,2,1,0));
NeedsPlayerOrder: False;
BuildIcon: 304;
TabletSpriteId: 254;
Input: (wtNone, wtNone, wtNone, wtNone);
Output: (wtCoal, wtNone, wtNone, wtNone);
UnlockedByHouse: htSawmill;
SnowSpriteId: 2070;
GroundArea: ((0,0,0,0), (0,0,0,0), (2,1,1,0), (3,1,3,0));
),
( //Farm
PlanYX: ((0,0,0,0), (1,1,1,1), (1,1,1,1), (1,2,1,1));
NeedsPlayerOrder: False;
BuildIcon: 309;
TabletSpriteId: 259;
Input: (wtNone, wtNone, wtNone, wtNone);
Output: (wtCorn, wtNone, wtNone, wtNone);
UnlockedByHouse: htSawmill;
SnowSpriteId: 2055;
GroundArea: ((0,0,0,0), (3,2,2,3), (2,0,0,2), (3,1,3,3));
),
( //Fisher hut
PlanYX: ((0,0,0,0), (0,0,0,0), (0,1,1,0), (0,2,1,1));
NeedsPlayerOrder: False;
BuildIcon: 307;
TabletSpriteId: 257;
Input: (wtNone, wtNone, wtNone, wtNone);
Output: (wtFish, wtNone, wtNone, wtNone);
UnlockedByHouse: htSawmill;
SnowSpriteId: 2053;
GroundArea: ((0,0,0,0), (0,0,0,0), (0,2,0,1), (0,1,1,3));
),
( //Gold mine
PlanYX: ((0,0,0,0), (0,0,0,0), (0,0,0,0), (0,1,2,0));
NeedsPlayerOrder: False;
BuildIcon: 306;
TabletSpriteId: 256;
Input: (wtNone, wtNone, wtNone, wtNone);
Output: (wtGoldOre, wtNone, wtNone, wtNone);
UnlockedByHouse: htSawmill;
SnowSpriteId: 2073;
GroundArea: ((0,0,0,0), (0,0,0,0), (0,0,0,0), (0,2,1,0));
),
( //Inn
PlanYX: ((0,0,0,0), (0,1,1,1), (1,1,1,1), (1,2,1,1));
NeedsPlayerOrder: False;
BuildIcon: 328;
TabletSpriteId: 278;
Input: (wtBread, wtSausage, wtWine, wtFish);
Output: (wtNone, wtNone, wtNone, wtNone);
UnlockedByHouse: htStore;
SnowSpriteId: 2063;
GroundArea: ((0,0,0,0), (1,0,0,2), (2,0,0,2), (4,1,2,3));
),
( //Iron mine
PlanYX: ((0,0,0,0), (0,0,0,0), (0,0,0,0), (0,1,2,1));
NeedsPlayerOrder: False;
BuildIcon: 305;
TabletSpriteId: 255;
Input: (wtNone, wtNone, wtNone, wtNone);
Output: (wtIronOre, wtNone, wtNone, wtNone);
UnlockedByHouse: htSawmill;
SnowSpriteId: 2052;
GroundArea: ((0,0,0,0), (0,0,0,0), (0,0,0,0), (0,3,1,3));
),
( //Iron smithy
PlanYX: ((0,0,0,0), (0,0,0,0), (1,1,1,1), (1,1,2,1));
NeedsPlayerOrder: False;
BuildIcon: 302;
TabletSpriteId: 252;
Input: (wtIronOre, wtCoal, wtNone, wtNone);
Output: (wtIron, wtNone, wtNone, wtNone);
UnlockedByHouse: htIronMine;
SnowSpriteId: 2051;
GroundArea: ((0,0,0,0), (0,0,0,0), (3,0,0,2), (3,2,1,3));
),
( //Marketplace
PlanYX: ((0,0,0,0), (0,1,1,1), (1,1,1,1), (1,1,1,2));
NeedsPlayerOrder: False;
BuildIcon: 327;
TabletSpriteId: 277;
Input: (wtNone, wtNone, wtNone, wtNone);
Output: (wtNone, wtNone, wtNone, wtNone);
UnlockedByHouse: htSawmill;
SnowSpriteId: 2072;
GroundArea: ((0,0,0,0), (2,2,1,1), (0,0,0,0), (2,2,2,1));
),
( //Metallurgist
PlanYX: ((0,0,0,0), (1,1,1,0), (1,1,1,0), (1,2,1,0));
NeedsPlayerOrder: False;
BuildIcon: 316;
TabletSpriteId: 266;
Input: (wtGoldOre, wtCoal, wtNone, wtNone);
Output: (wtGold, wtNone, wtNone, wtNone);
UnlockedByHouse: htGoldMine;
SnowSpriteId: 2068;
GroundArea: ((0,0,0,0), (3,0,2,0), (3,1,2,0), (3,1,3,0));
),
( //Mill
PlanYX: ((0,0,0,0), (0,0,0,0), (0,1,1,1), (0,1,2,1));
NeedsPlayerOrder: False;
BuildIcon: 323;
TabletSpriteId: 273;
Input: (wtCorn, wtNone, wtNone, wtNone);
Output: (wtFlour, wtNone, wtNone, wtNone);
UnlockedByHouse: htFarm;
SnowSpriteId: 2062;
GroundArea: ((0,0,0,0), (0,0,0,0), (0,2,1,3), (0,3,1,4));
),
( //Quarry
PlanYX: ((0,0,0,0), (0,0,0,0), (0,1,1,1), (0,1,2,1));
NeedsPlayerOrder: False;
BuildIcon: 315;
TabletSpriteId: 265;
Input: (wtNone, wtNone, wtNone, wtNone);
Output: (wtStone, wtNone, wtNone, wtNone);
UnlockedByHouse: htSchool;
SnowSpriteId: 2058;
GroundArea: ((0,0,0,0), (0,0,0,0), (0,2,0,1), (0,3,1,3));
),
( //Sawmill
PlanYX: ((0,0,0,0), (0,0,0,0), (1,1,1,1), (1,2,1,1));
NeedsPlayerOrder: False;
BuildIcon: 301;
TabletSpriteId: 251;
Input: (wtTrunk, wtNone, wtNone, wtNone);
Output: (wtTimber, wtNone, wtNone, wtNone);
UnlockedByHouse: htWoodcutters;
SnowSpriteId: 2050;
GroundArea: ((0,0,0,0), (0,0,0,0), (3,1,0,2), (4,1,1,3));
),
( //School
PlanYX: ((0,0,0,0), (1,1,1,0), (1,1,1,0), (1,2,1,0));
NeedsPlayerOrder: False;
BuildIcon: 314;
TabletSpriteId: 264;
Input: (wtGold, wtNone, wtNone, wtNone);
Output: (wtNone, wtNone, wtNone, wtNone);
UnlockedByHouse: htStore;
SnowSpriteId: 2059;
GroundArea: ((0,0,0,0), (1,0,1,0), (1,0,0,0), (3,1,3,0));
),
( //Siege workshop
PlanYX: ((0,0,0,0), (0,0,0,0), (0,1,1,1), (0,2,1,1));
NeedsPlayerOrder: True;
BuildIcon: 324;
TabletSpriteId: 274;
Input: (wtTimber, wtIron, wtNone, wtNone);
Output: (wtNone, wtNone, wtNone, wtNone);
UnlockedByHouse: htIronSmithy;
SnowSpriteId: 2078;
GroundArea: ((0,0,0,0), (0,0,0,0), (0,1,0,2), (0,1,2,3));
),
( //Stables
PlanYX: ((0,0,0,0), (1,1,1,1), (1,1,1,1), (1,1,2,1));
NeedsPlayerOrder: False;
BuildIcon: 313;
TabletSpriteId: 263;
Input: (wtCorn, wtNone, wtNone, wtNone);
Output: (wtHorse, wtNone, wtNone, wtNone);
UnlockedByHouse: htFarm;
SnowSpriteId: 2071;
GroundArea: ((0,0,0,0), (2,1,0,2), (1,0,0,1), (3,2,1,4));
),
( //Store
PlanYX: ((0,0,0,0), (1,1,1,0), (1,1,1,0), (1,2,1,0));
NeedsPlayerOrder: False;
BuildIcon: 312;
TabletSpriteId: 262;
Input: (wtAll, wtNone, wtNone, wtNone);
Output: (wtAll, wtNone, wtNone, wtNone);
UnlockedByHouse: htNone;
SnowSpriteId: 2056;
GroundArea: ((0,0,0,0), (3,0,2,0), (2,0,2,0), (3,1,3,0));
),
( //Swine
PlanYX: ((0,0,0,0), (0,1,1,1), (1,1,1,1), (1,1,1,2));
NeedsPlayerOrder: False;
BuildIcon: 317;
TabletSpriteId: 267;
Input: (wtCorn, wtNone, wtNone, wtNone);
Output: (wtPig, wtSkin, wtNone, wtNone);
UnlockedByHouse: htFarm;
SnowSpriteId: 2064;
GroundArea: ((0,0,0,0), (2,2,0,2), (2,1,0,2), (4,4,3,1));
),
( //Tannery
PlanYX: ((0,0,0,0), (0,0,0,0), (0,1,1,1), (0,1,2,1));
NeedsPlayerOrder: False;
BuildIcon: 326;
TabletSpriteId: 276;
Input: (wtSkin, wtNone, wtNone, wtNone);
Output: (wtLeather, wtNone, wtNone, wtNone);
UnlockedByHouse: htSwine;
SnowSpriteId: 2076;
GroundArea: ((0,0,0,0), (0,0,0,0), (0,0,0,2), (0,3,1,3));
),
( //Town hall
PlanYX: ((0,0,0,0), (1,1,1,1), (1,1,1,1), (1,2,1,1));
NeedsPlayerOrder: False;
BuildIcon: 319;
TabletSpriteId: 269;
Input: (wtGold, wtNone, wtNone, wtNone);
Output: (wtNone, wtNone, wtNone, wtNone);
UnlockedByHouse: htMetallurgists;
SnowSpriteId: 2077;
GroundArea: ((0,0,0,0), (1,0,0,2), (2,0,0,2), (3,1,1,2));
),
( //Watch tower
PlanYX: ((0,0,0,0), (0,0,0,0), (0,1,1,0), (0,1,2,0));
NeedsPlayerOrder: False;
BuildIcon: 318;
TabletSpriteId: 268;
Input: (wtStone, wtNone, wtNone, wtNone);
Output: (wtNone, wtNone, wtNone, wtNone);
UnlockedByHouse: htQuarry;
SnowSpriteId: 2060;
GroundArea: ((0,0,0,0), (0,0,0,0), (0,1,2,0), (0,3,1,0));
),
( //Weapon smithy
PlanYX: ((0,0,0,0), (0,0,0,0), (1,1,1,1), (1,2,1,1));
NeedsPlayerOrder: True;
BuildIcon: 303;
TabletSpriteId: 253;
Input: (wtCoal, wtIron, wtNone, wtNone);
Output: (wtSword, wtPike, wtCrossbow, wtNone);
UnlockedByHouse: htIronSmithy;
SnowSpriteId: 2069;
GroundArea: ((0,0,0,0), (0,0,0,0), (3,0,0,3), (4,1,2,3));
),
( //Weapon workshop
PlanYX: ((0,0,0,0), (0,0,0,0), (1,1,1,1), (1,2,1,1));
NeedsPlayerOrder: True;
BuildIcon: 320;
TabletSpriteId: 270;
Input: (wtTimber, wtNone, wtNone, wtNone);
Output: (wtAxe, wtLance, wtBow, wtNone);
UnlockedByHouse: htSawmill;
SnowSpriteId: 2061;
GroundArea: ((0,0,0,0), (0,0,0,0), (3,0,0,3), (3,1,2,3));
),
( //Wineyard
PlanYX: ((0,0,0,0), (0,0,0,0), (0,1,1,1), (0,1,1,2));
NeedsPlayerOrder: False;
BuildIcon: 329;
TabletSpriteId: 279;
Input: (wtNone, wtNone, wtNone, wtNone);
Output: (wtWine, wtNone, wtNone, wtNone);
UnlockedByHouse: htSawmill;
SnowSpriteId: 2065;
GroundArea: ((0,0,0,0), (0,0,0,0), (0,2,0,0), (0,2,3,1));
),
( //Woodcutter
PlanYX: ((0,0,0,0), (0,0,0,0), (1,1,1,0), (1,1,2,0));
NeedsPlayerOrder: False;
BuildIcon: 310;
TabletSpriteId: 260;
Input: (wtNone, wtNone, wtNone, wtNone);
Output: (wtTrunk, wtNone, wtNone, wtNone);
UnlockedByHouse: htSchool;
SnowSpriteId: 2057;
GroundArea: ((0,0,0,0), (0,0,0,0), (3,0,1,0), (4,2,1,0));
)
);
//For some reason in KaM the piles of building supply are not aligned, each one has a different offset.
//These values were taking from the barracks offsets and are for use with new houses.
BUILD_SUPPLY_OFFSETS: TKMHouseBuildSupply = ( ((MoveX: 0; MoveY: 0), (MoveX: -7; MoveY: 0), (MoveX:-26; MoveY: 0), //Wood 1-3
(MoveX:-26; MoveY: 0), (MoveX:-26; MoveY:-1), (MoveX:-26; MoveY:-4)), //Wood 4-6
((MoveX: 0; MoveY: 0), (MoveX: 0; MoveY: 0), (MoveX: -7; MoveY: 0), //Stone 1-3
(MoveX: -7; MoveY:-4), (MoveX:-16; MoveY:-4), (MoveX:-16; MoveY:-4)));//Stone 4-6
//'This house is unoccupied' msg index
HOUSE_TYPE_2_UNOCCUPIED_MSG_INDEX: array[TKMHouseType] of ShortInt = (
-1, -1, //utNone, utAny
0,1,2,
-1, //htBarracks
3,4,5,6,7,
-1, //htInn
8,9,
-1, //htMarketplace
10,11,12,13,
-1, //htSchool
14,15,
-1, //htStore
16,17,
-1, //htTownHall
18,19,20,21,22);
{ TKMHouseDatClass }
constructor TKMHouseSpec.Create(aHouseType: TKMHouseType);
begin
inherited Create;
fHouseType := aHouseType;
fNameTextID := TX_HOUSES_NAMES__29 + HOUSE_TYPE_TO_ID[fHouseType] - 1; //May be overridden for new houses
end;
function TKMHouseSpec.AcceptsWares: boolean;
begin
Result := (WareInput[1] <> wtNone) //Exclude houses that do not receive wares
or (fHouseType = htMarket); //Marketplace also accepts wares
end;
function TKMHouseSpec.GetArea: TKMHouseArea;
begin
Result := HOUSE_DAT_X[fHouseType].PlanYX;
end;
function TKMHouseSpec.GetDoesOrders: Boolean;
begin
Result := HOUSE_DAT_X[fHouseType].NeedsPlayerOrder;
end;
function TKMHouseSpec.GetGroundVisibleArea: TKMHouseArea;
begin
Result := HOUSE_DAT_X[fHouseType].GroundArea;
end;
function TKMHouseSpec.GetGUIIcon: Word;
begin
Result := HOUSE_DAT_X[fHouseType].BuildIcon;
end;
function TKMHouseSpec.GetHouseName: UnicodeString;
begin
Result := gResTexts[fNameTextID];
end;
//MaxHealth is always a cost of construction * 50
function TKMHouseSpec.MaxHealth: Word;
begin
Result := (fHouseDat.WoodCost + fHouseDat.StoneCost) * 50;
end;
//Write houses outline into given list
procedure TKMHouseSpec.Outline(aList: TKMPointList);
var
I, K: Integer;
tmp: TKMByte2Array;
outlines: TKMShapesArray;
begin
aList.Clear;
SetLength(tmp, 6, 6);
for I := 0 to 3 do
for K := 0 to 3 do
tmp[I+1,K+1] := Byte(HOUSE_DAT_X[fHouseType].PlanYX[I+1,K+1] > 0);
GenerateOutline(tmp, 2, outlines);
Assert(outlines.Count = 1, 'Houses are expected to have single outline');
for I := 0 to outlines.Shape[0].Count - 1 do
aList.Add(outlines.Shape[0].Nodes[I]);
end;
function TKMHouseSpec.GetWorkerType: TKMUnitType;
begin
//fHouseDat.OwnerType is read from DAT file and is ShortInt, it can be out of range (i.e. -1)
if InRange(fHouseDat.WorkerType, Low(UNIT_ID_TO_TYPE), High(UNIT_ID_TO_TYPE)) then
Result := UNIT_ID_TO_TYPE[fHouseDat.WorkerType]
else
Result := utNone;
end;
function TKMHouseSpec.IsWorkshop: Boolean;
begin
Result := fHouseType in [htWeaponSmithy, htArmorSmithy, htWeaponWorkshop, htArmorWorkshop];
end;
function TKMHouseSpec.FlagPointTexId: Word;
begin
case fHouseType of
htBarracks: Result := 249;
htTownHall: Result := 249;
htWoodcutters: Result := 660;
else
Result := 0; // Has no such thing
end;
end;
// Returns True if this house could have a worker (or occupant)
function TKMHouseSpec.CanHasWorker: Boolean;
begin
Result := WorkerType <> utNone;
end;
function TKMHouseSpec.ProducesWares: Boolean;
begin
Result := not (WareOutput[1] in [wtNone, wtAll, wtWarfare]); //Exclude aggregate types
end;
//procedure TKMHouseSpec.SetSnowPicNoShadow(const Value: SmallInt);
//begin
// HouseDatX[fHouseType].SnowSpriteNoShadowId := Value;
//end;
function TKMHouseSpec.GetReleasedBy: TKMHouseType;
begin
Result := HOUSE_DAT_X[fHouseType].UnlockedByHouse;
end;
function TKMHouseSpec.GetWareInput: TKMWareType4;
begin
Result := HOUSE_DAT_X[fHouseType].Input;
end;
function TKMHouseSpec.GetWareOutput: TKMWareType4;
begin
Result := HOUSE_DAT_X[fHouseType].Output;
end;
function TKMHouseSpec.GetSnowPic: SmallInt;
begin
Result := HOUSE_DAT_X[fHouseType].SnowSpriteId;
end;
function TKMHouseSpec.GetTabletIcon: Word;
begin
Result := HOUSE_DAT_X[fHouseType].TabletSpriteId;
end;
function TKMHouseSpec.GetUnoccupiedMsgId: SmallInt;
var
houseUnnocupiedMsgIndex: ShortInt;
begin
Result := -1;
houseUnnocupiedMsgIndex := HOUSE_TYPE_2_UNOCCUPIED_MSG_INDEX[fHouseType];
if houseUnnocupiedMsgIndex <> -1 then
Result := TX_MSG_HOUSE_UNOCCUPIED__22 + houseUnnocupiedMsgIndex;
end;
procedure TKMHouseSpec.LoadFromStream(Stream: TMemoryStream);
begin
Stream.Read(fHouseDat, SizeOf(TKMHouseSpecLegacy));
end;
// Get doorway offset in tile fraction
function TKMHouseSpec.GetDoorwayOffset(aCheck: TKMCheckAxis): Single;
begin
if aCheck = axX then
Result := EntranceOffsetXpx - CELL_SIZE_PX div 2
else
Result := EntranceOffsetYpx;
Result := Result / CELL_SIZE_PX;
end;
{ TKMResHouses }
constructor TKMResHouses.Create;
procedure AddAnimation(aHouse: TKMHouseType; aAnim: TKMHouseActionType; aMoveX, aMoveY: Integer; const aSteps: array of SmallInt);
var
I: Integer;
begin
with fItems[aHouse].fHouseDat.Anim[aAnim] do
begin
MoveX := aMoveX;
MoveY := aMoveY;
Count := length(aSteps);
for I := 1 to Count do
Step[I] := aSteps[I-1];
end;
end;
procedure AddMarketBeastAnim(aBeast: Integer; const aStep: array of SmallInt);
var
I: Integer;
begin
// Beast anims are 0 indexed
for I := 1 to 30 do
fMarketBeastAnim[aBeast].Step[I] := aStep[I - 1] + MARKET_WARES_TEX_START - 1;
end;
var
H: TKMHouseType;
I: Integer;
begin
inherited;
for H := HOUSE_MIN to HOUSE_MAX do
fItems[H] := TKMHouseSpec.Create(H);
fCRC := LoadHouseDat(KaMResolvePackagedResource('data/defines/houses.dat'));
fItems[htFishermans].fHouseDat.ResProductionX := 1; //we cathc 1 fish per time
fItems[htFishermans].fHouseDat.WorkerRest := 5; //Set fisher's rest similar to what other houses have
fItems[htTannery].fHouseDat.Anim[haFlag3].Count := 0; //fix for tannery 2 flags at one place. Flag3 is unnecessary
fItems[htMarket].fHouseType := htMarket;
fItems[htMarket].fHouseDat.WorkerType := -1; //No unit works here (yet anyway)
fItems[htMarket].fHouseDat.StonePic := 150;
fItems[htMarket].fHouseDat.WoodPic := 151;
fItems[htMarket].fHouseDat.WoodPal := 152;
fItems[htMarket].fHouseDat.StonePal := 153;
fItems[htMarket].fHouseDat.SupplyIn[1,1] := 154;
fItems[htMarket].fHouseDat.SupplyIn[1,2] := 155;
fItems[htMarket].fHouseDat.SupplyIn[1,3] := 156;
fItems[htMarket].fHouseDat.SupplyIn[1,4] := 157;
fItems[htMarket].fHouseDat.SupplyIn[1,5] := 158;
fItems[htMarket].fHouseDat.WoodPicSteps := 23;
fItems[htMarket].fHouseDat.StonePicSteps := 140;
fItems[htMarket].fHouseDat.EntranceOffsetX := 1;
fItems[htMarket].fHouseDat.EntranceOffsetXpx := 4; //Enterance is slightly to the left
fItems[htMarket].fHouseDat.EntranceOffsetYpx := 10;
fItems[htMarket].fHouseDat.WoodCost := 5;
fItems[htMarket].fHouseDat.StoneCost := 6;
for I := 1 to 6 do begin
fItems[htMarket].fHouseDat.BuildSupply[1,I].MoveX := -55+ BUILD_SUPPLY_OFFSETS[1,I].MoveX;
fItems[htMarket].fHouseDat.BuildSupply[1,I].MoveY := 15 + BUILD_SUPPLY_OFFSETS[1,I].MoveY;
fItems[htMarket].fHouseDat.BuildSupply[2,I].MoveX := 28 + BUILD_SUPPLY_OFFSETS[2,I].MoveX;
fItems[htMarket].fHouseDat.BuildSupply[2,I].MoveY := 20 + BUILD_SUPPLY_OFFSETS[2,I].MoveY;
end;
fItems[htMarket].fHouseDat.Sight := 10;
fItems[htMarket].fHouseDat.SizeArea := 11;
fItems[htMarket].fHouseDat.SizeX := 4;
fItems[htMarket].fHouseDat.SizeY := 3;
fItems[htMarket].fHouseDat.MaxHealth := 550;
AddAnimation(htMarket, haFlag1, -80, -33, [1165,1166,1167,1163,1164]);
AddAnimation(htMarket, haFlag2, -73, -7, [1163,1164,1165,1166,1167]);
AddAnimation(htMarket, haFlag3, 73, -80, [1161,1162,1158,1159,1160]);
AddAnimation(htMarket, haFire1, 18, -83, [1623,1624,1625,1620,1621,1622]);
AddAnimation(htMarket, haFire2, 78, -67, [1637,1632,1633,1634,1635,1636]);
AddAnimation(htMarket, haFire3, -30, -103, [1620,1621,1622,1623,1624,1625]);
AddAnimation(htMarket, haFire4, -3, -54, [1617,1618,1619,1614,1615,1616]);
AddAnimation(htMarket, haFire5, -12, -38, [1632,1633,1634,1635,1636,1637]);
AddAnimation(htMarket, haFire6, 39, -47, [1629,1630,1631,1626,1627,1628]);
AddAnimation(htMarket, haFire7, 25, 13, [1635,1636,1637,1632,1633,1634]);
AddAnimation(htMarket, haFire8, -82, -40, [1621,1622,1623,1624,1625,1620]);
//Now add horse animations for the market
AddMarketBeastAnim(1,[278, 277, 276, 275, 274, 273, 272, 271, 271, 271, 271, 271, 272,
273, 274, 275, 276, 277, 277, 278, 279, 280, 281, 282, 282, 282,
282, 281, 280, 279]);
fMarketBeastAnim[1].Count := 30;
fMarketBeastAnim[1].MoveX := MARKET_WARES_OFF_X;
fMarketBeastAnim[1].MoveY := MARKET_WARES_OFF_Y;
AddMarketBeastAnim(2,[266, 265, 264, 263, 262, 261, 260, 259, 258, 257, 258, 259, 260,
261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 270, 270, 270,
270, 269, 268, 267]);
fMarketBeastAnim[2].Count := 30;
fMarketBeastAnim[2].MoveX := MARKET_WARES_OFF_X;
fMarketBeastAnim[2].MoveY := MARKET_WARES_OFF_Y;
//ExportCSV(ExeDir+'Houses.csv');
end;
destructor TKMResHouses.Destroy;
var
H: TKMHouseType;
begin
for H := HOUSE_MIN to HOUSE_MAX do
FreeAndNil(fItems[H]);
inherited;
end;
function TKMResHouses.GetHouse(aType: TKMHouseType): TKMHouseSpec;
begin
Result := fItems[aType];
end;
function TKMResHouses.IsValid(aType: TKMHouseType): Boolean;
begin
Result := aType in HOUSES_VALID;
end;
function TKMResHouses.GetBeastAnim(aType: TKMHouseType; aBeast, aAge: Integer): TKMAnimLoop;
begin
Assert(aType in [htSwine, htStables, htMarket]);
Assert(InRange(aBeast, 1, 5));
Assert(InRange(aAge, 1, 3));
case aType of
htSwine: Result := fBeastAnim[1, aBeast, aAge];
htStables: Result := fBeastAnim[2, aBeast, aAge];
htMarket: Result := fMarketBeastAnim[aBeast];
end;
end;
//Return CRC of loaded file
//CRC should be calculated right away, cos file may be swapped after loading
function TKMResHouses.LoadHouseDat(const aPath: string): Cardinal;
var
S: TKMemoryStream;
i: Integer;
begin
Assert(KaMResourceExists(aPath), Format('houses.dat can not be found at "%s"', [aPath]));
S := TKMemoryStreamBinary.Create;
try
S.LoadFromFile(aPath);
S.Read(fBeastAnim, SizeOf(fBeastAnim){30*70}); //Swine&Horses animations
//Read the records one by one because we need to reorder them and skip one in the middle
for i:=0 to 28 do //KaM has only 28 houses
if HOUSE_ID_TO_TYPE[i] <> htNone then
fItems[HOUSE_ID_TO_TYPE[i]].LoadFromStream(S)
else
S.Seek(SizeOf(TKMHouseSpecLegacy), soFromCurrent);
Result := Adler32CRC(S);
finally
S.Free;
end;
end;
procedure TKMResHouses.ExportCSV(const aPath: string);
var
HT: TKMHouseType;
S: string;
SL: TStringList;
I, K: Integer;
anim: TKMHouseActionType;
procedure AddField(const aField: string); overload;
begin S := S + aField + ';'; end;
procedure AddField(aField: Integer); overload;
begin S := S + IntToStr(aField) + ';'; end;
begin
SL := TStringList.Create;
S := 'House name;WoodCost;StoneCost;ResProductionX;WorkerRest;EntranceOffsetX';
SL.Append(S);
for HT := HOUSE_MIN to HOUSE_MAX do
begin
S := '';
AddField(fItems[HT].HouseName);
AddField(fItems[HT].WoodCost);
AddField(fItems[HT].StoneCost);
AddField(fItems[HT].ResProductionX);
AddField(fItems[HT].WorkerRest);
AddField(fItems[HT].EntranceOffsetX);
SL.Append(S);
for I := 1 to 4 do
begin
S := '';
for K := 1 to 4 do
AddField(fItems[HT].BuildArea[I, K]);
SL.Append(S);
end;
S := 'Animation;Count;OffsetX;OffsetY';
SL.Append(S);
for anim := Low(TKMHouseActionType) to High(TKMHouseActionType) do
begin
if fItems[HT].fHouseDat.Anim[anim].Count > 0 then
begin
S := '';
AddField(GetEnumName(TypeInfo(TKMHouseActionType), Integer(anim)));
AddField(fItems[HT].fHouseDat.Anim[anim].Count);
AddField(fItems[HT].fHouseDat.Anim[anim].MoveX);
AddField(fItems[HT].fHouseDat.Anim[anim].MoveY);
SL.Append(S);
end;
end;
S := '';
SL.Append(S);
end;
ForceDirectories(ExtractFilePath(aPath));
SL.SaveToFile(aPath);
SL.Free;
end;
end.
@@ -0,0 +1,725 @@
unit KM_ResUnits;
{$I KaM_Remake.inc}
interface
uses
Classes, SysUtils,
KM_CommonClasses, KM_CommonTypes, KM_Defaults, KM_Points,
KM_ResTypes;
type
// Used to separate close-combat units from archers (they use different fighting logic)
TKMFightType = (ftMelee, ftRanged);
TKMUnitMoveType = (umtWalk, umtWalkDiag, umtStorm, umtStormDiag);
TKMUnitSpecLegacy = packed record
HitPoints: SmallInt;
Attack: SmallInt;
AttackHorse: SmallInt;
x4: SmallInt;
Defence: SmallInt;
Speed: SmallInt;
x7: SmallInt;
Sight: SmallInt;
x9: ShortInt;
x10: ShortInt;
CanWalkOut: SmallInt;
x11: SmallInt;
end;
TKMUnitSprite = array [TKMUnitActionType, dirN..dirNW] of TKMAnimLoop;
TKMUnitSpecInfo = record
StepsPerTile: Byte;
StepsPerTileDiag: Byte;
StepsPerTileStorm: Byte;
StepsPerTileStormDiag: Byte;
end;
TKMUnitSprite2 = array [1..18] of SmallInt; //Sound indices vs sprite ID
// Common facade for unit specs that hides the legacy and modern complexities
TKMUnitSpec = class
private
fUnitType: TKMUnitType;
fUnitDat: TKMUnitSpecLegacy;
fUnitSpecInfo: TKMUnitSpecInfo;
fUnitSprite: TKMUnitSprite;
fUnitSprite2: TKMUnitSprite2;
function GetAllowedPassability: TKMTerrainPassability;
function GetDescription: UnicodeString;
function GetDesiredPassability: TKMTerrainPassability;
function GetFightType: TKMFightType;
function GetGUIIcon: Word;
function GetGUIScroll: Word;
function GetMinimapColor: Cardinal;
function GetMiningRange: Byte;
function GetSpeed: Single;
function GetUnitAnim(aAction: TKMUnitActionType; aDir: TKMDirection): TKMAnimLoop;
function GetUnitTextID: Integer;
function GetUnitName: UnicodeString;
public
constructor Create(aType: TKMUnitType);
function IsValid: Boolean;
function IsAnimal: Boolean;
function IsCitizen: Boolean;
function IsWarrior: Boolean;
function IsWarriorEquipable: Boolean;
function GetDefenceVsProjectiles(aIsBolt: Boolean): Single;
procedure LoadFromStream(Stream: TMemoryStream);
//Derived from KaM
property HitPoints: SmallInt read fUnitDat.HitPoints;
property Attack: SmallInt read fUnitDat.Attack;
property AttackHorse: SmallInt read fUnitDat.AttackHorse;
property Defence: SmallInt read fUnitDat.Defence;
property Description: UnicodeString read GetDescription;
property Sight: SmallInt read fUnitDat.Sight;
//Additional properties added by Remake
property AllowedPassability: TKMTerrainPassability read GetAllowedPassability;
property DesiredPassability: TKMTerrainPassability read GetDesiredPassability;
property FightType: TKMFightType read GetFightType;
property GUIIcon: Word read GetGUIIcon;
property GUIScroll: Word read GetGUIScroll;
property MinimapColor: Cardinal read GetMinimapColor;
property MiningRange: Byte read GetMiningRange;
property Speed: Single read GetSpeed;
function GetEffectiveSpeed(aMovementType: TKMUnitMoveType): Single;
function GetEffectiveWalkSpeed(aIsDiag: Boolean): Single;
function GetEffectiveStormSpeed(aIsDiag: Boolean): Single;
function SupportsAction(aAct: TKMUnitActionType): Boolean;
property UnitAnim[aAction: TKMUnitActionType; aDir: TKMDirection]: TKMAnimLoop read GetUnitAnim;
property GUIName: UnicodeString read GetUnitName;
property GUITextID: Integer read GetUnitTextID;
class function IsMelee(aUnitType: TKMUnitType): Boolean;
class function IsMounted(aUnitType: TKMUnitType): Boolean;
class function IsAntihorse(aUnitType: TKMUnitType): Boolean;
class function IsRanged(aUnitType: TKMUnitType): Boolean;
end;
TKMResUnits = class
private
fCRC: Cardinal;
fItems: array [TKMUnitType] of TKMUnitSpec;
fSerfCarry: array [WARE_MIN..WARE_MAX, dirN..dirNW] of TKMAnimLoop;
procedure LoadUnitsDat(const aPath: UnicodeString);
procedure PatchUnitSpec;
function GetItem(aType: TKMUnitType): TKMUnitSpec; inline;
function GetSerfCarry(aType: TKMWareType; aDir: TKMDirection): TKMAnimLoop;
public
constructor Create;
destructor Destroy; override;
procedure ResetToDefaults;
property Items[aType: TKMUnitType]: TKMUnitSpec read GetItem; default;
property SerfCarry[aType: TKMWareType; aDir: TKMDirection]: TKMAnimLoop read GetSerfCarry;
property CRC: Cardinal read fCRC; //Return hash of all values
procedure ExportCSV(const aPath: UnicodeString);
procedure SaveCustomData(aSaveStream: TKMemoryStream);
procedure LoadCustomData(aLoadStream: TKMemoryStream);
end;
const
//This is a map of the valid values for !SET_UNIT,
//TSK did not had place for new warriors that were inserted in the middle(!)
UNIT_OLD_ID_TO_TYPE: array[0..31] of TKMUnitType = (
utSerf,utWoodcutter,utMiner,utAnimalBreeder,utFarmer,
utCarpenter,utBaker,utButcher,utFisher,utBuilder,
utStonemason,utSmith,utMetallurgist,utRecruit, //Units
utMilitia,utAxeFighter,utSwordFighter,utBowman,utCrossbowman,
utLanceCarrier,utPikeman,utScout,utKnight,utBarbarian, //Troops
utWolf,utFish,utWatersnake,utSeastar,utCrab,
utWaterflower,utWaterleaf,utDuck); //Animals
//and the corresponing unit that will be created (matches KaM behavior)
UNIT_TYPE_TO_OLD_ID: array[TKMUnitType] of integer = (
-1, -1, //utNone, utAny
0,1,2,3,4,5,6,7,8,9,10,11,12,13, //Citizens
14,15,16,17,18,19,20,21,22,23, //Warriors
-1,-1,-1,-1, {-1,-1,} //TPR warriors (can't be placed with SET_UNIT)
24,25,26,27,28,29,30,31); //Animals
//This is a map of the valid values for !SET_GROUP, and the corresponing unit that will be created (matches KaM behavior)
UNIT_ID_TO_TYPE: array[0..40] of TKMUnitType = (
utSerf,utWoodcutter,utMiner,utAnimalBreeder,utFarmer,
utCarpenter,utBaker,utButcher,utFisher,utBuilder,
utStonemason,utSmith,utMetallurgist,utRecruit, //Units
utMilitia,utAxeFighter,utSwordFighter,utBowman,utCrossbowman,
utLanceCarrier,utPikeman,utScout,utKnight,utBarbarian, //TSK Troops
utRebel,utRogue,utWarrior,utVagabond,
{utCatapult,utBallista}utNone,utNone, //Placeholder for Seige weapons
utWolf, utFish, utWatersnake, utSeastar, utCrab,
utWaterflower, utWaterleaf, utDuck,
utNone, utNone, utNone
);
UNIT_TYPE_TO_ID: array[TKMUnitType] of ShortInt = (
-1, -1, // utNone, utAny
0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Citizens
14,15,16,17,18,19,20,21,22,23, // Warriors
24,25,26,27, {28,29,} // TPR warriors
30,31,32,33,34,35,36,37 // Animals
);
// Number means WareType as it is stored in Barracks, hence it's not wtSomething
TROOP_COST: array [utMilitia..utKnight, 1..4] of TKMWareType = (
(wtAxe, wtNone, wtNone, wtNone ), // Militia
(wtWoodenShield, wtLeatherArmor, wtAxe, wtNone ), // Axefighter
(wtIronShield, wtIronArmor, wtSword, wtNone ), // Swordfighter
(wtLeatherArmor, wtBow, wtNone, wtNone ), // Bowman
(wtIronArmor, wtCrossbow, wtNone, wtNone ), // Crossbowman
(wtLeatherArmor, wtLance, wtNone, wtNone ), // Lance Carrier
(wtIronArmor, wtPike, wtNone, wtNone ), // Pikeman
(wtWoodenShield, wtLeatherArmor, wtAxe, wtHorse), // Scout
(wtIronShield, wtIronArmor, wtSword, wtHorse) // Knight
);
// The frame shown when a unit is standing still in uaWalk. Same for all units!
UNIT_STILL_FRAMES: array [TKMDirection] of Byte = (0,3,2,2,1,6,7,6,6);
//Corresponding indices in units.rx
UNIT_THOUGHT_BOUNDS: array [TKMUnitThought, 1..2] of Word = (
(0,0), (6250,6257), (6258,6265), (6266,6273), (6274,6281), (6282,6289), (6290,6297), (6298,6305), (6314,6321)
);
UNIT_OFF_X = -0.5;
UNIT_OFF_Y = -0.4;
//Offsetting layers of units we control what goes above or below
//using smaller values to minimize impact on other objects and keeping within map bounds
FLAG_X_OFFSET = 0.01; //Flag is offset to be rendered above/below the flag carrier
THOUGHT_X_OFFSET = 0.02; //Thought is offset to be rendered always above the flag
// Fish counts
UNIT_FISH_CONUT_DEFAULT = 10;
UNIT_FISH_CONUT_MAX = 255;
var
// TownHall default units troops cost (number of gold chests needed)
// Could be modified by script functions
TH_TROOP_COST: array[0..4] of Byte;
implementation
uses
TypInfo,
KromUtils, KM_ResTexts, KaMResourceInput;
const
STORM_SPEEDUP = 1.5;
//TownHall default units troops cost (number of gold chests needed)
TH_DEFAULT_TROOP_COST: array[0..4] of Byte = (
2, 3, 5, 8, 8 //rebel / rogue / vagabond / barbarian / warrior
);
{ TKMUnitSpec }
constructor TKMUnitSpec.Create(aType: TKMUnitType);
begin
inherited Create;
fUnitType := aType;
end;
function TKMUnitSpec.IsValid: boolean;
begin
Result := not (fUnitType in [utNone, utAny]);
end;
function TKMUnitSpec.IsAnimal: boolean;
begin
Result := fUnitType in [ANIMAL_MIN..ANIMAL_MAX];
end;
function TKMUnitSpec.IsCitizen: boolean;
begin
Result := fUnitType in [CITIZEN_MIN..CITIZEN_MAX];
end;
function TKMUnitSpec.IsWarrior: boolean;
begin
Result := fUnitType in [WARRIOR_MIN..WARRIOR_MAX];
end;
function TKMUnitSpec.IsWarriorEquipable: boolean;
begin
Result := fUnitType in [WARRIOR_EQUIPABLE_BARRACKS_MIN..WARRIOR_EQUIPABLE_BARRACKS_MAX];
end;
function TKMUnitSpec.GetDefenceVsProjectiles(aIsBolt: Boolean): Single;
const
SHEILD_DEFENCE_BONUS_AGAINST_XBOW = 0.5;
SHEILD_DEFENCE_BONUS_AGAINST_BOW_N_SLING = 1;
begin
Result := Defence;
//Shielded units get a small bonus
if fUnitType in [utAxeFighter, utSwordFighter, utScout, utKnight] then
begin
if aIsBolt then
Result := Result + SHEILD_DEFENCE_BONUS_AGAINST_XBOW
else
Result := Result + SHEILD_DEFENCE_BONUS_AGAINST_BOW_N_SLING;
end;
end;
procedure TKMUnitSpec.LoadFromStream(Stream: TMemoryStream);
begin
Stream.Read(fUnitDat, SizeOf(TKMUnitSpecLegacy));
Stream.Read(fUnitSprite, SizeOf(TKMUnitSprite));
Stream.Read(fUnitSprite2, SizeOf(TKMUnitSprite2));
end;
function TKMUnitSpec.SupportsAction(aAct: TKMUnitActionType): Boolean;
const
UNIT_SUPPOSTED_ACTIONS: array [TKMUnitType] of TKMUnitActionTypeSet = (
[], [], //None, Any
[uaWalk, uaDie, uaEat, uaWalkArm], //Serf
[uaWalk, uaWork, uaDie, uaWork1, uaEat..uaWalkTool2],
[uaWalk, uaDie, uaEat],
[uaWalk, uaDie, uaEat],
[uaWalk, uaWork, uaDie..uaWalkBooty2],
[uaWalk, uaDie, uaEat],
[uaWalk, uaDie, uaEat],
[uaWalk, uaDie, uaEat],
[uaWalk, uaWork, uaDie, uaWork1..uaWalkBooty],
[uaWalk, uaWork, uaDie, uaEat, uaWork1, uaWork2],
[uaWalk, uaWork, uaDie, uaWork1, uaEat..uaWalkBooty],
[uaWalk, uaDie, uaEat],
[uaWalk, uaDie, uaEat],
[uaWalk, uaSpec, uaDie, uaEat], //Recruit
[uaWalk, uaWork, uaSpec, uaDie, uaEat], //Militia
[uaWalk, uaWork, uaSpec, uaDie, uaEat], //Axeman
[uaWalk, uaWork, uaSpec, uaDie, uaEat], //Swordsman
[uaWalk, uaWork, uaSpec, uaDie, uaEat], //Bowman
[uaWalk, uaWork, uaSpec, uaDie, uaEat], //Crossbowman
[uaWalk, uaWork, uaDie, uaEat],
[uaWalk, uaWork, uaDie, uaEat],
[uaWalk, uaWork, uaDie, uaEat],
[uaWalk, uaWork, uaDie, uaEat], //Cavalry
[uaWalk, uaWork, uaSpec, uaDie, uaEat], //Barbarian
[uaWalk, uaWork, uaDie, uaEat], //Rebel
[uaWalk, uaWork, uaSpec, uaDie, uaEat], //Slingshot
[uaWalk, uaWork, uaSpec, uaDie, uaEat], //Warrior
[uaWalk, uaWork, uaDie, uaEat],
[uaWalk], [uaWalk], [uaWalk], [uaWalk], [uaWalk], [uaWalk], [uaWalk], [uaWalk] //Animals
);
begin
Result := aAct in UNIT_SUPPOSTED_ACTIONS[fUnitType];
end;
function TKMUnitSpec.GetAllowedPassability: TKMTerrainPassability;
const
// Defines which animal prefers which terrain
ANIMAL_TERRAIN: array[ANIMAL_MIN .. ANIMAL_MAX] of TKMTerrainPassability = (
tpWolf, tpFish, tpFish, tpFish, tpCrab, tpFish, tpFish, tpFish);
begin
case fUnitType of
ANIMAL_MIN..ANIMAL_MAX: Result := ANIMAL_TERRAIN[fUnitType]; //Animals
else
Result := tpWalk; // Worker, Warriors
end;
end;
// Where unit would like to be
function TKMUnitSpec.GetDesiredPassability: TKMTerrainPassability;
begin
if fUnitType in [CITIZEN_MIN..CITIZEN_MAX] - [utBuilder] then
Result := tpWalkRoad //Citizens except Worker
else
Result := GetAllowedPassability; //Workers, warriors, animals
end;
function TKMUnitSpec.GetFightType: TKMFightType;
const
WARRIOR_FIGHT_TYPE: array[WARRIOR_MIN..WARRIOR_MAX] of TKMFightType = (
ftMelee,ftMelee,ftMelee, //Militia, AxeFighter, Swordsman
ftRanged,ftRanged, //Bowman, Arbaletman
ftMelee,ftMelee, //Pikeman, Hallebardman,
ftMelee,ftMelee, //HorseScout, Cavalry,
ftMelee, //Barbarian
ftMelee, //Peasant
ftRanged, //utSlingshot
ftMelee, //utMetalBarbarian
ftMelee //utHorseman
{ftRanged,ftRanged, //utCatapult, utBallista,}
);
begin
Assert(fUnitType in [Low(WARRIOR_FIGHT_TYPE)..High(WARRIOR_FIGHT_TYPE)]);
Result := WARRIOR_FIGHT_TYPE[fUnitType];
end;
function TKMUnitSpec.GetGUIIcon: Word;
begin
case fUnitType of
utNone, utAny: Result := 0;
utBarbarian: Result := 70;
else
if IsCitizen then
Result := 141 + UNIT_TYPE_TO_ID[fUnitType]
else if IsWarriorEquipable then
Result := 47 + UNIT_TYPE_TO_ID[fUnitType]
else if IsWarrior then
Result := 55 + UNIT_TYPE_TO_ID[fUnitType]
else
Result := 0;
end;
end;
function TKMUnitSpec.GetGUIScroll: Word;
begin
if IsValid then
Result := 521 + UNIT_TYPE_TO_ID[fUnitType]
else
Result := 0;
end;
//Units are rendered on minimap with their team color
//Animals don't have team and thus are rendered in their own prefered clors
function TKMUnitSpec.GetMinimapColor: Cardinal;
const
MINIMAP_COLOR: array [TKMUnitType] of Cardinal = (
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
$B0B0B0,$B08000,$B08000,$80B0B0,$00B0B0,$B080B0,$00B000,$80B0B0); // Exact colors can be tweaked
begin
Result := MINIMAP_COLOR[fUnitType] or $FF000000;
end;
// Unit mining ranges. (measured from KaM)
function TKMUnitSpec.GetMiningRange: Byte;
begin
case fUnitType of
utWoodcutter: Result := 10;
utFarmer: Result := 10;
utStonemason: Result := 16;
utFisher: Result := 14;
else
raise Exception.Create(GUIName + ' has no mining range');
end;
end;
function TKMUnitSpec.GetSpeed: Single;
begin
// Return speed in tiles per tick (usually 0.1)
Result := fUnitDat.Speed / 240;
end;
function TKMUnitSpec.GetEffectiveSpeed(aMovementType: TKMUnitMoveType): Single;
begin
case aMovementType of
umtWalk: Result := 1 / fUnitSpecInfo.StepsPerTile;
umtWalkDiag: Result := 1 / fUnitSpecInfo.StepsPerTileDiag;
umtStorm: Result := 1 / fUnitSpecInfo.StepsPerTileStorm;
umtStormDiag: Result := 1 / fUnitSpecInfo.StepsPerTileStormDiag;
else
raise Exception.Create('Unexpected type');
end;
end;
function TKMUnitSpec.GetEffectiveWalkSpeed(aIsDiag: Boolean): Single;
begin
if aIsDiag then
Result := GetEffectiveSpeed(umtWalkDiag)
else
Result := GetEffectiveSpeed(umtWalk);
end;
function TKMUnitSpec.GetEffectiveStormSpeed(aIsDiag: Boolean): Single;
begin
if aIsDiag then
Result := GetEffectiveSpeed(umtStormDiag)
else
Result := GetEffectiveSpeed(umtStorm);
end;
function TKMUnitSpec.GetUnitAnim(aAction: TKMUnitActionType; aDir: TKMDirection): TKMAnimLoop;
begin
Assert(aDir <> dirNA);
Assert(aAction in [Low(TKMUnitActionType)..High(TKMUnitActionType)]);
Result := fUnitSprite[aAction, aDir];
end;
function TKMUnitSpec.GetUnitTextID: Integer;
begin
if not IsValid then Exit(-1);
case fUnitType of
utWolf: Result := TX_UNITS_WOLF;
utFish: Result := TX_UNITS_FISH;
utWatersnake: Result := TX_UNITS_WATERSNAKE;
utSeastar: Result := TX_UNITS_SEASTAR;
utCrab: Result := TX_UNITS_CRAB;
utWaterflower: Result := TX_UNITS_WATERFLOWER;
utWaterleaf: Result := TX_UNITS_WATERLEAF;
utDuck: Result := TX_UNITS_DUCK;
else
Result := TX_UNITS_NAMES__29 + UNIT_TYPE_TO_ID[fUnitType];
end;
end;
function TKMUnitSpec.GetUnitName: UnicodeString;
begin
case fUnitType of
utAny: Result := gResTexts[TX_UNITS_ALL];
utNone: Result := 'N/A';
else
Result := gResTexts[GetUnitTextID];
end;
end;
function TKMUnitSpec.GetDescription: UnicodeString;
begin
if IsValid and not IsAnimal then
Result := gResTexts[TX_UNITS_DESCRIPTIONS__13 + UNIT_TYPE_TO_ID[fUnitType]]
else
Result := 'N/A';
end;
class function TKMUnitSpec.IsMelee(aUnitType: TKMUnitType): Boolean;
begin
Result := aUnitType in [utMilitia, utAxeFighter, utSwordFighter, utBarbarian, utWarrior];
end;
class function TKMUnitSpec.IsMounted(aUnitType: TKMUnitType): Boolean;
begin
Result := aUnitType in [utScout, utKnight, utVagabond];
end;
class function TKMUnitSpec.IsAntihorse(aUnitType: TKMUnitType): Boolean;
begin
Result := aUnitType in [utLanceCarrier, utPikeman, utRebel];
end;
class function TKMUnitSpec.IsRanged(aUnitType: TKMUnitType): Boolean;
begin
Result := aUnitType in [utBowman, utCrossbowman, utRogue];
end;
{ TKMUnitsDatCollection }
constructor TKMResUnits.Create;
var
UT: TKMUnitType;
begin
inherited;
for UT := Low(TKMUnitType) to High(TKMUnitType) do
fItems[UT] := TKMUnitSpec.Create(UT);
LoadUnitsDat(KaMResolvePackagedResource('data/defines/unit.dat'));
//ExportCSV(ExeDir + 'units.original.csv');
PatchUnitSpec;
//ExportCSV(ExeDir + 'units.remake.csv');
//Halt;
end;
destructor TKMResUnits.Destroy;
var
UT: TKMUnitType;
begin
for UT := Low(TKMUnitType) to High(TKMUnitType) do
fItems[UT].Free;
inherited;
end;
procedure TKMResUnits.SaveCustomData(aSaveStream: TKMemoryStream);
begin
aSaveStream.PlaceMarker('UnitsCustomData');
aSaveStream.Write(TH_TROOP_COST, SizeOF(TH_TROOP_COST));
end;
procedure TKMResUnits.LoadCustomData(aLoadStream: TKMemoryStream);
begin
aLoadStream.CheckMarker('UnitsCustomData');
aLoadStream.Read(TH_TROOP_COST, SizeOF(TH_TROOP_COST));
end;
procedure TKMResUnits.PatchUnitSpec;
const
DEF_SCOUT_SIGHT = 9;
DEF_HORSEMAN_ATTACK = 40;
DEF_PEASANT_ATTACK_HORSE = 60;
DEF_PIKEMAN_ATTACK_HORSE = 55;
DEF_MOUNTED_SPEED = 39;
var
UT: TKMUnitType;
begin
// Overwrite units stats only if they are set for default values from original game
// We don't want to update them, in case player manually edited unit.dat file
if fItems[utScout].fUnitDat.Sight = DEF_SCOUT_SIGHT then
fItems[utScout].fUnitDat.Sight := 13;
if fItems[utVagabond].fUnitDat.Attack = DEF_HORSEMAN_ATTACK then
fItems[utVagabond].fUnitDat.Attack := 35;
if fItems[utRebel].fUnitDat.AttackHorse = DEF_PEASANT_ATTACK_HORSE then
fItems[utRebel].fUnitDat.AttackHorse := 50;
if fItems[utLanceCarrier].fUnitDat.AttackHorse = DEF_PIKEMAN_ATTACK_HORSE then
fItems[utLanceCarrier].fUnitDat.AttackHorse := 60;
// .Dat mounted speed is 39, but it makes 9 steps per diagonal tile after rounding, while we used to 8 steps
if fItems[utScout].fUnitDat.Speed = DEF_MOUNTED_SPEED then
fItems[utScout].fUnitDat.Speed := 40;
if fItems[utKnight].fUnitDat.Speed = DEF_MOUNTED_SPEED then
fItems[utKnight].fUnitDat.Speed := 40;
if fItems[utVagabond].fUnitDat.Speed = DEF_MOUNTED_SPEED then
fItems[utVagabond].fUnitDat.Speed := 40;
for UT := UNIT_MIN to UNIT_MAX do
begin
fItems[UT].fUnitSpecInfo.StepsPerTile := Round(1 / fItems[UT].Speed);
fItems[UT].fUnitSpecInfo.StepsPerTileDiag := Round(1.41 / fItems[UT].Speed);
fItems[UT].fUnitSpecInfo.StepsPerTileStorm := Round(1 / (fItems[UT].Speed * STORM_SPEEDUP));
fItems[UT].fUnitSpecInfo.StepsPerTileStormDiag := Round(1.41 / (fItems[UT].Speed * STORM_SPEEDUP));
end;
end;
procedure TKMResUnits.ExportCSV(const aPath: UnicodeString);
var
SL: TStringList;
UT: TKMUnitType;
begin
SL := TStringList.Create;
try
SL.Add('Name,HitPoints,Attack,AttackHorse,x4,Defence,Speed,x7,Sight,x9,x10,CanWalkOut,x11');
for UT := Low(TKMUnitType) to High(TKMUnitType) do
if Items[UT].IsValid then
SL.Add(GetEnumName(TypeInfo(TKMUnitType), Ord(UT)) + ','
+ IntToStr(Items[UT].HitPoints) + ','
+ IntToStr(Items[UT].Attack) + ','
+ IntToStr(Items[UT].AttackHorse) + ','
+ IntToStr(Items[UT].fUnitDat.x4) + ','
+ IntToStr(Items[UT].Defence) + ','
+ IntToStr(Items[UT].fUnitDat.Speed) + ','
+ IntToStr(Items[UT].fUnitDat.x7) + ','
+ IntToStr(Items[UT].Sight) + ','
+ IntToStr(Items[UT].fUnitDat.x9) + ','
+ IntToStr(Items[UT].fUnitDat.x10) + ','
+ IntToStr(Items[UT].fUnitDat.CanWalkOut) + ','
+ IntToStr(Items[UT].fUnitDat.x11) + ',');
SL.SaveToFile(aPath);
finally
SL.Free;
end;
{AssignFile(ft,ExeDir+'Units.txt'); rewrite(ft);
for ii:=Low(TKMUnitType) to High(TKMUnitType) do
if UnitsDat[ii].IsValid then
begin
writeln(ft);
writeln(ft);
writeln(ft,'NewUnit'+inttostr(ii));
for kk:=1 to 14 do
for hh:=1 to 8 do
// if UnitSprite[ii].Act[kk].Dir[hh].Step[1]>0 then
begin
write(ft,inttostr(kk)+'.'+inttostr(hh)+#9);
for jj:=1 to 30 do
if UnitSprite[ii].Act[kk].Dir[hh].Step[jj]>0 then //write(ft,'#');
write(ft,inttostr(UnitSprite[ii].Act[kk].Dir[hh].Step[jj])+'. ');
write(ft,inttostr(UnitSprite[ii].Act[kk].Dir[hh].Count)+' ');
write(ft,inttostr(UnitSprite[ii].Act[kk].Dir[hh].MoveX)+' ');
write(ft,inttostr(UnitSprite[ii].Act[kk].Dir[hh].MoveY)+' ');
writeln(ft);
end;
end;
closefile(ft);}
end;
procedure TKMResUnits.ResetToDefaults;
var
I: Integer;
begin
for I := Low(TH_TROOP_COST) to High(TH_TROOP_COST) do
TH_TROOP_COST[I] := TH_DEFAULT_TROOP_COST[I];
end;
function TKMResUnits.GetSerfCarry(aType: TKMWareType; aDir: TKMDirection): TKMAnimLoop;
begin
Assert(aType in [WARE_MIN .. WARE_MAX]);
Result := fSerfCarry[aType, aDir];
end;
function TKMResUnits.GetItem(aType: TKMUnitType): TKMUnitSpec;
begin
Result := fItems[aType];
end;
procedure TKMResUnits.LoadUnitsDat(const aPath: UnicodeString);
const
UNIT_DAT_COUNT = 41;
var
S: TKMemoryStream;
I: Integer;
begin
Assert(KaMResourceExists(aPath), Format('unit.dat can not be found at "%s"', [aPath]));
S := TKMemoryStreamBinary.Create;
try
S.LoadFromFile(aPath);
S.Read(fSerfCarry, SizeOf(fSerfCarry){28*8*70});
for I := 0 to UNIT_DAT_COUNT - 1 do
if UNIT_ID_TO_TYPE[I] <> utNone then
fItems[UNIT_ID_TO_TYPE[I]].LoadFromStream(S)
else //Skip
S.Seek(SizeOf(TKMUnitSpecLegacy) + SizeOf(TKMUnitSprite) + SizeOf(TKMUnitSprite2), soFromCurrent);
fCRC := Adler32CRC(S);
finally
S.Free;
end;
end;
end.