0ad/binaries/data/mods/public/simulation/components/tests/test_EntityStates.js
Vantha 35bf79fdf3
Rename the formation field to formationController
This patch renames the `unitAI.formation` field in entity states to
`unitAI.formationController` for clarity (there's `unitAI.formations`
as well, which was confusing)
2026-08-12 11:55:14 +02:00

734 lines
28 KiB
JavaScript

Engine.LoadHelperScript("EntityStateRetriever.js");
Engine.LoadHelperScript("ObstructionSnap.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("ValueModification.js");
Engine.LoadComponentScript("interfaces/BuildingAI.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
Engine.LoadComponentScript("interfaces/Formation.js");
Engine.LoadComponentScript("interfaces/Foundation.js");
Engine.LoadComponentScript("interfaces/Garrisonable.js");
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
Engine.LoadComponentScript("interfaces/Gate.js");
Engine.LoadComponentScript("interfaces/Guard.js");
Engine.LoadComponentScript("interfaces/Heal.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Loot.js");
Engine.LoadComponentScript("interfaces/Market.js");
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
Engine.LoadComponentScript("interfaces/Pack.js");
Engine.LoadComponentScript("interfaces/Population.js");
Engine.LoadComponentScript("interfaces/ProductionQueue.js");
Engine.LoadComponentScript("interfaces/Promotion.js");
Engine.LoadComponentScript("interfaces/Repairable.js");
Engine.LoadComponentScript("interfaces/Researcher.js");
Engine.LoadComponentScript("interfaces/Resistance.js");
Engine.LoadComponentScript("interfaces/ResourceDropsite.js");
Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
Engine.LoadComponentScript("interfaces/ResourceTrickle.js");
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
Engine.LoadComponentScript("interfaces/Trader.js");
Engine.LoadComponentScript("interfaces/Trainer.js");
Engine.LoadComponentScript("interfaces/TurretHolder.js");
Engine.LoadComponentScript("interfaces/Turretable.js");
Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("interfaces/Upgrade.js");
Engine.LoadComponentScript("interfaces/Upkeep.js");
Engine.LoadComponentScript("GuiInterface.js");
Engine.LoadComponentScript("ModifiersManager.js");
Engine.LoadComponentScript("RallyPoint.js");
// This script tests the GetEntityState and GetTemplateData methods of the GuiInterface component and the underlying
// logic of the EntityStateRetriever, ModifiersManager, and globalscripts/Templates.js.
// This data is not actually serialised, but passed to the GUI. However, it is essential that it is complete and
// crucial for performance that each call returns as little data as necessary, that nothing is computed twice.
// See helpers/EntityStateRetriever.js for details.
const cmpGuiInterface = ConstructComponent(SYSTEM_ENTITY, "GuiInterface");
const cmpModifiersManager = ConstructComponent(SYSTEM_ENTITY, "ModifiersManager");
const templateNames = {};
const templates = {};
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"GetCurrentTemplateName": function(ent) { return templateNames[ent]; },
"GetTemplate": function(name) { return templates[name]; }
});
templates.example_structure_template = {
"@parent": "parent_structure_emplate",
"AlertRaiser": {
"List": { "@datatype": "tokens", "_string": "UnitClass1 UnitClass2" },
"RaiseAlertRange": 120, "EndOfAlertRange": 180, "SearchRange": 100
},
"Attack": {
"Ranged": {
"AttackName": "Bow", "Damage": { "Pierce": "8" },
"MaxRange": "60", "PrepareTime": "400",
"PreferredClasses": { "@datatype": "tokens", "_string": "Human" },
"Projectile": {
"FriendlyFire": "false", "Gravity": "50",
"LaunchPoint": { "@y": "0" },
"Speed": "100", "Spread": "2"
},
"RepeatTime": "4000"
}
},
"Auras": { "@datatype": "tokens", "_string": "aura1" },
"BuildingAI": {
"DefaultArrowCount": "6", "GarrisonArrowClasses": "UnitClass1",
"GarrisonArrowMultiplier": "1"
},
"BuildRestrictions": {
"Territory": "own ally", "PlacementType": "shore",
"Category": "Structure1",
"Distance": { "FromClass": "StructureClass0", "MinDistance": 100 }
},
"Capturable": {
"CapturePoints": "2500", "GarrisonRegenRate": "1.0", "RegenRate": "30"
},
"Cost": {
"BuildTime": "500", "Population": "0",
"Resources": { "food": "0", "metal": "250", "stone": "300", "wood": "300" }
},
"DeathDamage": {
"Shape": "Circular", "Range": 20, "FriendlyFire": "true", "Damage": { "Fire": 500 }
},
"Decay": {
"Active": "false", "DelayTime": "0.0", "SinkAccel": "9.8",
"SinkProb": "1.0", "SinkRate": "3.0", "SinkingAnim": "false"
},
"GarrisonHolder": {
"BuffHeal": "1", "EjectHealth": "0.1",
"EjectClassesOnDestroy": { "@datatype": "tokens", "_string": "Unit" },
"List": { "@datatype": "tokens", "_string": "Support Infantry Cavalry" },
"LoadingRange": "1", "Max": "20"
},
"Health": {
"DamageVariants": {
"heavydamage": "0.35", "lightdamage": "0.85", "mediumdamage": "0.65"
},
"DeathType": "corpse", "IdleRegenRate": "0",
"Max": "3000", "RegenRate": "0",
"SpawnEntityOnDeath": "decay|rubble/rubble_stone_6x6",
"Unhealable": "true"
},
"Identity": {
"Civ": "civ1",
"Classes": { "@datatype": "tokens", "_string": "StructureClass1 StructureClass2 StructureClass3" },
"GenericName": "Example Structure",
"Icon": "example_structure.png",
"SelectionGroupName": "example_structure",
"Tooltip": "This is an example structure.",
"Undeletable": "false",
"VisibleClasses": { "@datatype": "tokens", "_string": "StructureVisibleClass1 StructureVisibleClass2" }
},
"Loot": { "food": "10", "metal": "50", "stone": "60", "wood": "60", "xp": "30" },
"Population": { "Bonus": 5 },
"Position": {
"Altitude": "0", "Anchor": "upright", "FloatDepth": "0",
"Floating": "false", "TurnRate": "6"
},
"ProductionQueue": {},
"Repairable": { "RepairTimeRatio": "2.0" },
"Researcher": {
"Technologies": { "@datatype": "tokens", "_string": "tech1 tech2 tech3" }
},
"Resistance": {
"Entity": {
"ApplyStatus": { "Poisoned": { "BlockChance": "1", "Duration": "0.0" } },
"Damage": { "Crush": "3", "Hack": "29", "Pierce": "35" }
},
"Foundation": {
"ApplyStatus": { "Poisoned": { "BlockChance": "1", "Duration": "0.0" } },
"Damage": { "Crush": "1", "Hack": "1", "Pierce": "10" }
}
},
"ResourceDropsite": { "Sharable": "true", "Types": "food stone metal" },
"ResourceTrickle": { "Rates": { "food": 1.3 }, "Interval": 1500 },
"TerritoryDecay": { "DecayRate": "20", "Territory": "neutral enemy" },
"TerritoryInfluence": { "Radius": "140", "Root": "true", "Weight": "10000" },
"Trainer": {
"BatchTimeModifier": "0.8",
"Entities": { "@datatype": "tokens", "_string": "template1 template2 template3" }
},
"Visibility": {
"AlwaysVisible": "false", "Corpse": "false",
"Preview": "false", "RetainInFog": "true"
},
"Vision": { "Range": "90" },
"VisionSharing": { "Bribable": "false" }
};
templates.example_unit_template = {
"@parent": "parent_unit_template",
"Attack": {
"Capture": {
"AttackName": "Capture", "Capture": "2.5",
"MaxRange": "4", "RepeatTime": "1000",
"RestrictedClasses": { "@datatype": "tokens", "_string": "RestrictedClass1" }
},
"Melee": {
"AttackName": "Spear", "MaxRange": "4",
"Bonuses": { "BonusCavMelee": { "Classes": "Cavalry", "Multiplier": "2.5" } },
"Damage": { "Hack": "4.5", "Pierce": "4" },
"PreferredClasses": { "@datatype": "tokens", "_string": "PreferredClass1" },
"PrepareTime": "500", "RepeatTime": "1000"
},
"Slaughter": {
"AttackName": "Slaughter", "Damage": { "Hack": "1000" },
"MaxRange": "2", "PrepareTime": "900", "RepeatTime": "1000",
}
},
"Builder": {
"Entities": {
"@datatype": "tokens",
"_string": "buildable1 buildable2 buildable3 buildable4"
},
"Rate": "1.0"
},
"Cost": {
"BuildTime": "10", "Population": "1",
"Resources": { "food": "50", "metal": "0", "stone": "0", "wood": "50" }
},
"Garrisonable": { "Size": "1" },
"Heal": {
"Range": 20, "Health": 10, "Interval": 2500,
"UnhealableClasses": { "@datatype": "tokens", "_string": "" },
"HealableClasses": { "@datatype": "tokens", "_string": "HealableClass1" },
"LineTexture": "heal_line_texture.png",
"LineTextureMask": "heal_line_mask.png",
"LineThickness": 0.36
},
"Health": {
"DeathType": "corpse", "IdleRegenRate": "0",
"Max": "100", "RegenRate": "0", "Unhealable": "false"
},
"Identity": {
"Civ": "civ2", "Lang": "greek", "Rank": "Basic",
"Classes": { "@datatype": "tokens", "_string": "UnitClass1 UnitClass2 UnitClass3" },
"GenericName": "Example Unit",
"Icon": "example_unit.png",
"Phenotype": { "@datatype": "tokens", "_string": "male" },
"SelectionGroupName": "example_unit",
"Tooltip": "This is an example unit.",
"Undeletable": "false",
"VisibleClasses": {
"@datatype": "tokens",
"_string": "VisibleUnitClass1 VisibleUnitClass2"
}
},
"Loot": { "food": "5", "metal": "4", "stone": "3", "wood": "5", "xp": "100" },
"Position": {
"Altitude": "0", "Anchor": "upright",
"FloatDepth": "0.0", "Floating": "false",
"TurnRate": "14"
},
"Promotion": { "Entity": "promoted_unit_template", "RequiredXp": "100" },
"Resistance": {
"Entity": { "Damage": { "Crush": "15", "Hack": "3", "Pierce": "3" } }
},
"ResourceGatherer": {
"BaseSpeed": "1.0",
"Capacities": { "food": "10", "metal": "11", "stone": "12", "wood": "13" },
"MaxDistance": "2.0",
"Rates": {
"food.fruit": "0.5", "food.grain": "0.6", "food.meat": "0.7",
"metal.ore": "0.8", "metal.ruins": "0.9", "stone.rock": "1.0",
"stone.ruins": "1.1", "wood.ruins": "1.2", "wood.tree": "1.3"
}
},
"TreasureCollector": { "MaxDistance": "2" },
"UnitAI": {
"CanGuard": "true", "CanPatrol": "true",
"CheeringTime": "2800",
"DefaultStance": "aggressive",
"FleeDistance": "12.0",
"FormationController": "false",
"Formations": { "@datatype": "tokens", "_string": "formation1 formation2 formation3" },
"PatrolWaitTime": "1"
},
"UnitMotion": {
"Acceleration": "35", "FormationController": "false",
"InstantTurnAngle": "1.5",
"PassabilityClass": "default",
"RunMultiplier": "2",
"WalkSpeed": "9.5",
"Weight": "10"
},
"Visibility": {
"AlwaysVisible": "false", "Corpse": "false",
"Preview": "false", "RetainInFog": "false"
},
"Vision": { "Range": "80" },
"VisionSharing": { "Bribable": "false" }
};
const expectedTemplateData = {
"example_structure_template": {
"alertRaiser": { "classes": "UnitClass1 UnitClass2" },
"attack": {
"Ranged": {
"Damage": { "Pierce": 8 }, "attackName": { "name": "Bow" },
"minRange": 0, "maxRange": 60, "yOrigin": 0, "elevationAdaptedRange": 60,
"repeatTime": 4000, "projectileCount": 1, "friendlyFire": false
}
},
"auras": {
"aura1": {
"name": { "generic": "Aura 1" }, "description": "Aura Description",
"modifications": { "value": "Resistance/Damage/Crush", "add": 1 }, "radius": 30 }
},
"buildingAI": {
"defaultArrowCount": 6, "maxArrowCount": 0,
"garrisonArrowMultiplier": 1, "garrisonArrowClasses": ["UnitClass1"]
},
"buildRestrictions": {
"placementType": "shore", "territory": "own ally", "category": "Structure1",
"distance": { "fromClass": "StructureClass0", "min": 100 }
},
"maxCapturePoints": 2500,
"cost": { "food": 0, "metal": 250, "stone": 300, "wood": 300, "population": 0, "time": 500 },
"deathDamage": { "Damage": { "Fire": 500 }, "friendlyFire": true },
"garrisonHolder": { "allowedClasses": "Support Infantry Cavalry", "buffHeal": 1, "capacity": 20 },
"maxHitpoints": 3000,
"selectionGroupName": "example_structure",
"name": { "specific": "Example Structure", "generic": "Example Structure" },
"icon": "example_structure.png",
"tooltip": "This is an example structure.",
"identityClasses": ["StructureClass1", "StructureClass2", "StructureClass3",
"StructureVisibleClass1", "StructureVisibleClass2"],
"visibleIdentityClasses": ["StructureVisibleClass1", "StructureVisibleClass2"],
"nativeCiv": "civ1",
"requirements": undefined,
"rank": undefined,
"undeletable": "false",
"enablesBartering": false,
"loot": { "food": 10, "metal": 50, "stone": 60, "wood": 60, "xp": 30 },
"population": { "bonus": 5 },
"researcher": {
"techCostMultiplier": { "food": 1, "metal": 1, "stone": 1, "wood": 1, "time": 1 }
},
"resistance": {
"Damage": { "Crush": 3, "Hack": 29, "Pierce": 35 },
"ApplyStatus": { "Poisoned": { "blockChance": 1, "duration": 0 } }
},
"resourceDropsite": { "types": ["food", "stone", "metal"] },
"resourceTrickle": { "interval": 1500, "rates": { "food": 1.3 } }
},
"example_unit_template": {
"attack": {
"Capture": {
"Capture": 2.5, "attackName": { "name": "Capture" },
"minRange": 0, "maxRange": 4, "yOrigin": 0,
"elevationAdaptedRange": 4, "repeatTime": 1000
},
"Melee": {
"Damage": { "Hack": 4.5, "Pierce": 4 }, "attackName": { "name": "Spear" },
"minRange": 0, "maxRange": 4, "yOrigin": 0,
"elevationAdaptedRange": 4, "repeatTime": 1000
},
"Slaughter": {
"Damage": { "Hack": 1000 }, "attackName": { "name": "Slaughter" },
"minRange": 0, "maxRange": 2, "yOrigin": 0,
"elevationAdaptedRange": 2, "repeatTime": 1000
}
},
"builder": true,
"cost": { "food": 50, "metal": 0, "stone": 0, "wood": 50, "population": 1, "time": 10 },
"garrisonable": { "size": 1 },
"heal": {
"health": 10, "range": 20, "interval": 2500, "unhealableClasses": "", "healableClasses": "HealableClass1"
},
"maxHitpoints": 100,
"selectionGroupName": "example_unit",
"name": { "specific": "Example Unit", "generic": "Example Unit" },
"icon": "example_unit.png",
"tooltip": "This is an example unit.",
"identityClasses": [
"UnitClass1", "UnitClass2", "UnitClass3",
"VisibleUnitClass1", "VisibleUnitClass2", "Basic"
],
"visibleIdentityClasses": ["VisibleUnitClass1", "VisibleUnitClass2"],
"nativeCiv": "civ2",
"requirements": undefined,
"rank": "Basic",
"undeletable": "false",
"enablesBartering": false,
"loot": { "food": 5, "metal": 4, "stone": 3, "wood": 5, "xp": 100 },
"promotion": { "req": 100 },
"resistance": { "Damage": { "Crush": 15, "Hack": 3, "Pierce": 3 } },
"resourceGatherRates": {
"food.fruit": 0.5, "food.grain": 0.6, "food.meat": 0.7,
"metal.ore": 0.8, "metal.ruins": 0.9,
"stone.rock": 1, "stone.ruins": 1.1,
"wood.ruins": 1.2, "wood.tree": 1.3
},
"treasureCollector": true,
"unitAI": { "formations": ["formation1", "formation2", "formation3"] },
"unitMotion": { "walk": 9.5, "run": 19, "acceleration": 35 }
}
};
global.AuraTemplates = { "GetAll": () => ({
"aura1": {
"auraName": "Aura 1",
"auraDescription": "Aura Description",
"modifications": { "value": "Resistance/Damage/Crush", "add": 1 },
"radius": 30
}
}) };
Resources = {
"GetCodes": () => ["food", "metal", "stone", "wood"],
"GetNames": () => ({
"food": "Food",
"metal": "Metal",
"stone": "Stone",
"wood": "Wood"
}),
"GetResource": resource => ({
"aiAnalysisInfluenceGroup":
resource == "food" ? "ignore" :
resource == "wood" ? "abundant" : "sparse"
})
};
const PLAYER_1_ENT_ID = 101;
const PLAYER_2_ENT_ID = 102;
cmpModifiersManager.OnGlobalPlayerEntityChanged({ "player": 1, "from": INVALID_PLAYER, "to": PLAYER_1_ENT_ID });
cmpModifiersManager.OnGlobalPlayerEntityChanged({ "player": 2, "from": INVALID_PLAYER, "to": PLAYER_2_ENT_ID });
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": (id) => 100 + id
});
AddMock(PLAYER_1_ENT_ID, IID_Identity, {
"GetCiv": () => "civ1"
});
AddMock(PLAYER_1_ENT_ID, IID_Player, {
"GetPlayerID": () => 1
});
AddMock(PLAYER_2_ENT_ID, IID_Identity, {
"GetCiv": () => "civ1"
});
AddMock(PLAYER_2_ENT_ID, IID_Player, {
"GetPlayerID": () => 2
});
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetTemplateData(1, { "templateName": "example_structure_template" }), expectedTemplateData.example_structure_template);
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetTemplateData(1, { "templateName": "example_unit_template" }), expectedTemplateData.example_unit_template);
const TEST_ENTITY_STRUCTURE = 11;
const TEST_ENTITY_UNIT_1 = 21;
const TEST_ENTITY_UNIT_2 = 22;
templateNames[TEST_ENTITY_STRUCTURE] = "example_structure_template";
templateNames[TEST_ENTITY_UNIT_1] = "example_unit_template";
templateNames[TEST_ENTITY_UNIT_2] = "example_unit_template";
const AddMockToEnts = (ents, iid, mock) =>
{
for (const ent of ents)
AddMock(ent, iid, mock);
};
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"GetLosVisibility": () => "visible",
"GetElevationAdaptedRange": () => 80,
"GetEntitiesByPlayer": (player) => player == 1 ? [TEST_ENTITY_STRUCTURE, TEST_ENTITY_UNIT_1, TEST_ENTITY_UNIT_2] : []
});
AddMockToEnts([TEST_ENTITY_STRUCTURE, TEST_ENTITY_UNIT_1, TEST_ENTITY_UNIT_2], IID_Ownership, {
"GetOwner": () => 1
});
AddMock(TEST_ENTITY_STRUCTURE, IID_Position, {
"GetTurretParent": () => INVALID_ENTITY,
"GetPosition": () => new Vector3D(1, 2, 3),
"GetRotation": () => new Vector3D(0, 1, 0),
"IsInWorld": () => true
});
AddMock(TEST_ENTITY_UNIT_1, IID_Position, {
"GetTurretParent": () => INVALID_ENTITY,
"GetPosition": () => new Vector3D(10, 12, 19),
"GetRotation": () => new Vector3D(0, 1, 0),
"IsInWorld": () => true
});
AddMock(TEST_ENTITY_UNIT_2, IID_Position, {
"GetTurretParent": () => INVALID_ENTITY,
"GetPosition": () => new Vector3D(20, 22, 24),
"GetRotation": () => new Vector3D(0, 1, 1),
"IsInWorld": () => true
});
const structureAttack = expectedTemplateData.example_structure_template.attack;
AddMock(TEST_ENTITY_STRUCTURE, IID_Attack, {
"GetAttackTypes": () => Object.keys(structureAttack),
"GetRange": (type) => ({ "min": structureAttack[type].minRange, "max": structureAttack[type].maxRange }),
"GetAttackYOrigin": (type) => structureAttack[type].yOrigin
});
AddMockToEnts([TEST_ENTITY_UNIT_1, TEST_ENTITY_UNIT_2], IID_Attack, {
"GetAttackTypes": () => Object.keys(expectedTemplateData.example_unit_template.attack)
});
AddMock(TEST_ENTITY_STRUCTURE, IID_BuildingAI, {
"GetArrowCount": () => 10
});
AddMock(TEST_ENTITY_STRUCTURE, IID_Capturable, {
"GetCapturePoints": () => 1567
});
AddMockToEnts([TEST_ENTITY_UNIT_1, TEST_ENTITY_UNIT_2], IID_Garrisonable, {
"HolderID": () => INVALID_ENTITY
});
AddMock(TEST_ENTITY_STRUCTURE, IID_GarrisonHolder, {
"GetEntities": () => [51, 52, 53],
"OccupiedSlots": () => 3
});
AddMock(TEST_ENTITY_UNIT_1, IID_Guard, {
"GetEntities": () => [TEST_ENTITY_STRUCTURE]
});
AddMock(TEST_ENTITY_UNIT_2, IID_Guard, {
"GetEntities": () => []
});
AddMock(TEST_ENTITY_STRUCTURE, IID_Health, {
"GetHitpoints": () => 2139,
"IsRepairable": () => true,
"IsInjured": () => true,
"IsUnhealable": () => true
});
AddMock(TEST_ENTITY_UNIT_1, IID_Health, {
"GetHitpoints": () => 39,
"IsRepairable": () => false,
"IsInjured": () => true,
"IsUnhealable": () => false
});
AddMock(TEST_ENTITY_UNIT_2, IID_Health, {
"GetHitpoints": () => 100,
"IsRepairable": () => false,
"IsInjured": () => false,
"IsUnhealable": () => false
});
AddMockToEnts([TEST_ENTITY_UNIT_1, TEST_ENTITY_UNIT_2], IID_Identity, {
"GetClassesList": () => expectedTemplateData.example_unit_template.identityClasses,
"IsControllable": () => true
});
AddMock(TEST_ENTITY_STRUCTURE, IID_Identity, {
"GetClassesList": () => expectedTemplateData.example_structure_template.identityClasses,
"IsControllable": () => true
});
AddMock(TEST_ENTITY_STRUCTURE, IID_ProductionQueue, {
"GetQueue": () => [{
"unitTemplate": "trainable1", "count": 2, "neededSlots": 0, "progress": 0.375,
"timeRemaining": 5000, "paused": undefined, "metadata": undefined, "id": 12
}],
"IsAutoQueueing": () => true
});
AddMock(TEST_ENTITY_UNIT_1, IID_Promotion, {
"GetCurrentXp": () => 89
});
AddMock(TEST_ENTITY_UNIT_2, IID_Promotion, {
"GetCurrentXp": () => 56
});
AddMock(TEST_ENTITY_STRUCTURE, IID_RallyPoint, {
"GetPositions": () => [{ "x": 10, "y": 50 }, { "x": 20, "y": 40 }]
});
AddMock(TEST_ENTITY_STRUCTURE, IID_Repairable, {
"GetNumBuilders": () => 2,
"GetBuildTime": () => 234
});
AddMock(TEST_ENTITY_UNIT_1, IID_ResourceGatherer, {
"GetCarryingStatus": () => [ { "type": "wood", "amount": 7, "max": 13 } ]
});
AddMock(TEST_ENTITY_UNIT_2, IID_ResourceGatherer, {
"GetCarryingStatus": () => [ { "type": "wood", "amount": 2, "max": 13 } ]
});
AddMock(TEST_ENTITY_STRUCTURE, IID_Researcher, {
"GetTechnologiesList": () => ["tech1", { "pair": ["tech2_1", "tech2_2"] }]
});
AddMock(TEST_ENTITY_STRUCTURE, IID_Trainer, {
"GetEntitiesList": () => ["trainable1", "trainable2", "trainable3"]
});
const FORMATION_CONTROLLER_ENT_ID = 43;
AddMockToEnts([TEST_ENTITY_UNIT_1, TEST_ENTITY_UNIT_2], IID_UnitAI, {
"GetCurrentState": () => "INDIVIDUAL.IDLE",
"GetOrders": () => [{ "type": "Walk", "data": { "x": 11, "z": 22, "force": true, "relaxed": true } }],
"HasWorkOrders": () => true,
"IsGuardOf": () => false,
"IsIdle": () => true,
"GetFormationController": () => FORMATION_CONTROLLER_ENT_ID
});
const expectedDynamicStates = {
[TEST_ENTITY_STRUCTURE]: {
"id": TEST_ENTITY_STRUCTURE,
"player": 1,
"templateName": "example_structure_template",
"attack": { "Ranged": { "elevationAdaptedRange": 80 } },
"buildingAI": { "arrowCount": 10 },
"capturePoints": 1567,
"controllable": true,
"garrisonHolder": { "entities": [51, 52, 53], "occupiedSlots": 3 },
"hitpoints": 2139,
"needsRepair": true,
"needsHeal": false,
"position": { "x": 1, "y": 2, "z": 3 },
"production": {
"queue": [{
"unitTemplate": "trainable1", "count": 2, "neededSlots": 0, "progress": 0.375,
"timeRemaining": 5000, "paused": undefined, "metadata": undefined, "id": 12
}],
"autoqueue": true
},
"rallyPoint": { "position": { "x": 10, "y": 50 } },
"repairable": { "numBuilders": 2, "buildTime": 234 },
"researcher": { "technologies": ["tech1", { "pair": ["tech2_1", "tech2_2"] }] },
"trainer": { "entities": ["trainable1", "trainable2", "trainable3"] },
"visibility": "visible"
},
[TEST_ENTITY_UNIT_1]: {
"id": TEST_ENTITY_UNIT_1,
"player": 1,
"templateName": "example_unit_template",
"controllable": true,
"garrisonable": { "holder": INVALID_ENTITY },
"guard": { "entities": [TEST_ENTITY_STRUCTURE] },
"hitpoints": 39,
"needsRepair": false,
"needsHeal": true,
"position": { "x": 10, "y": 12, "z": 19 },
"promotion": { "curr": 89 },
"resourceCarrying": [{ "type": "wood", "amount": 7, "max": 13 }],
"unitAI": {
"state": "INDIVIDUAL.IDLE",
"orders": [{ "type": "Walk", "data": { "x": 11, "z": 22, "force": true, "relaxed": true } }],
"hasWorkOrders": true,
"isGuarding": false,
"isIdle": true,
"formationController": FORMATION_CONTROLLER_ENT_ID
},
"visibility": "visible"
},
[TEST_ENTITY_UNIT_2]: {
"id": TEST_ENTITY_UNIT_2,
"player": 1,
"templateName": "example_unit_template",
"controllable": true,
"garrisonable": { "holder": INVALID_ENTITY },
"guard": { "entities": [] },
"hitpoints": 100,
"needsRepair": false,
"needsHeal": true,
"position": { "x": 20, "y": 22, "z": 24 },
"promotion": { "curr": 56 },
"resourceCarrying": [{ "type": "wood", "amount": 2, "max": 13 }],
"unitAI": {
"state": "INDIVIDUAL.IDLE",
"orders": [{ "type": "Walk", "data": { "x": 11, "z": 22, "force": true, "relaxed": true } }],
"hasWorkOrders": true,
"isGuarding": false,
"isIdle": true,
"formationController": FORMATION_CONTROLLER_ENT_ID
},
"visibility": "visible"
}
};
// The template data of both templates has been retrieved already, so it shouldn't be computed again now.
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_STRUCTURE), {
"dynamicState": expectedDynamicStates[TEST_ENTITY_STRUCTURE]
});
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_UNIT_1), {
"dynamicState": expectedDynamicStates[TEST_ENTITY_UNIT_1]
});
Engine.PostMessage = (ent, mtid, msg) =>
{
if (mtid == MT_TemplateModification && ent == SYSTEM_ENTITY)
cmpGuiInterface.OnTemplateModification(msg);
};
// This makes the previously returned templateData out-of-date, so it should now be recomputed (once) the next time
// queried for it.
cmpModifiersManager.AddModifier(
"Attack/Ranged/Damage/Pierce", "DamageBuff", [{ "affects": ["StructureClass2"], "add": 1 }], PLAYER_1_ENT_ID
);
expectedTemplateData.example_structure_template.attack.Ranged.Damage.Pierce += 1;
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_STRUCTURE), {
"dynamicState": expectedDynamicStates[TEST_ENTITY_STRUCTURE],
"templateData": expectedTemplateData.example_structure_template
});
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_UNIT_1), {
"dynamicState": expectedDynamicStates[TEST_ENTITY_UNIT_1],
"templateData": expectedTemplateData.example_unit_template
});
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_UNIT_2), {
"dynamicState": expectedDynamicStates[TEST_ENTITY_UNIT_2]
});
// On the other hand, changes of other playes' modifiers should be ignored.
cmpGuiInterface.OnTemplateModification({ "player": 2 });
TS_ASSERT(!cmpGuiInterface.GetEntityState(1, TEST_ENTITY_STRUCTURE).templateData);
const increasedMaxHPModifiers = [{ "affects": ["UnitClass1", "StructureClass1"], "add": 50 }];
cmpModifiersManager.AddModifier("Health/Max", "IncreasedMaxHP", increasedMaxHPModifiers, TEST_ENTITY_UNIT_1);
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_UNIT_1), {
"dynamicState": expectedDynamicStates[TEST_ENTITY_UNIT_1],
"modificationsID": "Health/Max:IncreasedMaxHP(1) ",
// New modificationsID, so modifiedTemplateData should be computed. Only for the affected components, though.
"modifiedTemplateData": { "maxHitpoints": 150 }
});
cmpModifiersManager.AddModifier("Health/Max", "IncreasedMaxHP", increasedMaxHPModifiers, TEST_ENTITY_UNIT_2);
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_UNIT_2), {
"dynamicState": expectedDynamicStates[TEST_ENTITY_UNIT_2],
"modificationsID": "Health/Max:IncreasedMaxHP(1) "
// modifiedTemplateData for owner 1, example_unit_template and the above modificationsID has been returned in the
// previous call already, so it shouldn't be computed again now.
});
cmpModifiersManager.AddModifier("Promotion/RequiredXp", "FasterPromotion", [{ "affects": ["UnitClass2"], "add": -10 }], TEST_ENTITY_UNIT_2, true);
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_UNIT_2), {
"dynamicState": expectedDynamicStates[TEST_ENTITY_UNIT_2],
"modificationsID": "Health/Max:IncreasedMaxHP(1) Promotion/RequiredXp:FasterPromotion(1) ",
// New modificationsID, so modifiedTemplateData should be computed again.
"modifiedTemplateData": { "maxHitpoints": 150, "promotion": { "req": 90 } }
});
cmpModifiersManager.AddModifier("Health/Max", "IncreasedMaxHP", increasedMaxHPModifiers, TEST_ENTITY_STRUCTURE);
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_STRUCTURE), {
"dynamicState": expectedDynamicStates[TEST_ENTITY_STRUCTURE],
"modificationsID": "Health/Max:IncreasedMaxHP(1) ",
// modifiedTemplateData for owner 1 and the above modificationsID has been returned in a previous call but only for
// different template (example_unit_template and not example_structure_template), so it should be computed again now.
"modifiedTemplateData": { "maxHitpoints": 3050 }
});
// This makes any previously returned templateData and modifiedTemplateData out-of-date, so they should now be recomputed
// (once) the next time queried for them.
cmpModifiersManager.AddModifier("Health/Max", "DecreasedMaxHP", [{ "affects": ["UnitClass1"], "add": -1 }], PLAYER_1_ENT_ID);
expectedTemplateData.example_unit_template.maxHitpoints -= 1;
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_UNIT_1), {
"dynamicState": expectedDynamicStates[TEST_ENTITY_UNIT_1],
"templateData": expectedTemplateData.example_unit_template,
"modificationsID": "Health/Max:IncreasedMaxHP(1) ",
"modifiedTemplateData": { "maxHitpoints": 149 }
});
cmpModifiersManager.RemoveModifier("Promotion/RequiredXp", "FasterPromotion", TEST_ENTITY_UNIT_2);
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_UNIT_2), {
"dynamicState": expectedDynamicStates[TEST_ENTITY_UNIT_2],
"modificationsID": "Health/Max:IncreasedMaxHP(1) ",
// modifiedTemplateData for owner 1, example_unit_template and the above modificationsID has been returned in the
// previous call already, so it shouldn't be computed again now.
});
AddMock(TEST_ENTITY_UNIT_2, IID_Ownership, {
"GetOwner": () => 2
});
expectedDynamicStates[TEST_ENTITY_UNIT_2].player = 2;
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_UNIT_2), {
"dynamicState": expectedDynamicStates[TEST_ENTITY_UNIT_2],
"templateData": { ...expectedTemplateData.example_unit_template, "maxHitpoints": 100 },
"modificationsID": "Health/Max:IncreasedMaxHP(1) ",
// modifiedTemplateData for example_unit_template and the above modificationsID has been returned in the previous
// call, but for a different owner (player 1, not player 2), so it should be computed again now.
"modifiedTemplateData": { "maxHitpoints": 150 }
});