mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-08-15 14:43:32 -07:00
This patch renames the `unitAI.formation` field in entity states to `unitAI.formationController` for clarity (there's `unitAI.formations` as well, which was confusing)
734 lines
28 KiB
JavaScript
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 }
|
|
});
|