Add tests to the Foundation component + fix some minor issues found by implementing the tests. Fixes #4121

This was SVN commit r18567.
This commit is contained in:
sanderd17 2016-07-29 11:48:07 +00:00
parent ab2c73e93a
commit 09ab4fe9d3
3 changed files with 242 additions and 46 deletions

View File

@ -16,6 +16,9 @@ Foundation.prototype.Init = function()
this.buildMultiplier = 1; // Multiplier for the amount of work builders do.
this.previewEntity = INVALID_ENTITY;
// Penalty for multiple builders
this.buildTimePenalty = 0.7;
};
Foundation.prototype.InitialiseConstruction = function(owner, template)
@ -29,6 +32,9 @@ Foundation.prototype.InitialiseConstruction = function(owner, template)
// Remember the cost here, so if it changes after construction begins (from auras or technologies)
// we will use the correct values to refund partial construction costs
let cmpCost = Engine.QueryInterface(this.entity, IID_Cost);
if (!cmpCost)
error("A foundation must have a cost component to know the build time");
this.costs = cmpCost.GetResourceCosts(owner);
this.maxProgress = 0;
@ -62,7 +68,7 @@ Foundation.prototype.GetBuildProgress = function()
var hitpoints = cmpHealth.GetHitpoints();
var maxHitpoints = cmpHealth.GetMaxHitpoints();
return (hitpoints / maxHitpoints);
return hitpoints / maxHitpoints;
};
Foundation.prototype.GetBuildPercentage = function()
@ -116,24 +122,31 @@ Foundation.prototype.OnDestroy = function()
*/
Foundation.prototype.AddBuilder = function(builderEnt)
{
if (this.builders.indexOf(builderEnt) === -1)
{
this.builders.push(builderEnt);
Engine.QueryInterface(this.entity, IID_Visual).SetVariable("numbuilders", this.builders.length);
this.SetBuildMultiplier();
Engine.PostMessage(this.entity, MT_FoundationBuildersChanged, { "to": this.builders });
}
if (this.builders.indexOf(builderEnt) != -1)
return;
this.builders.push(builderEnt);
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
cmpVisual.SetVariable("numbuilders", this.builders.length);
this.SetBuildMultiplier();
Engine.PostMessage(this.entity, MT_FoundationBuildersChanged, { "to": this.builders });
};
Foundation.prototype.RemoveBuilder = function(builderEnt)
{
if (this.builders.indexOf(builderEnt) !== -1)
{
this.builders.splice(this.builders.indexOf(builderEnt),1);
Engine.QueryInterface(this.entity, IID_Visual).SetVariable("numbuilders", this.builders.length);
this.SetBuildMultiplier();
Engine.PostMessage(this.entity, MT_FoundationBuildersChanged, { "to": this.builders });
}
let index = this.builders.indexOf(builderEnt);
if (index == -1)
return;
this.builders.splice(index, 1);
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
cmpVisual.SetVariable("numbuilders", this.builders.length);
this.SetBuildMultiplier();
Engine.PostMessage(this.entity, MT_FoundationBuildersChanged, { "to": this.builders });
};
/**
@ -146,7 +159,7 @@ Foundation.prototype.SetBuildMultiplier = function()
if (numBuilders < 2)
this.buildMultiplier = 1;
else
this.buildMultiplier = Math.pow(numBuilders, 0.7) / numBuilders;
this.buildMultiplier = Math.pow(numBuilders, this.buildTimePenalty) / numBuilders;
};
/**
@ -199,18 +212,19 @@ Foundation.prototype.Build = function(builderEnt, work)
// Handle the initial 'committing' of the foundation
if (!this.committed)
{
// The obstruction always blocks new foundations/construction,
// but we've temporarily allowed units to walk all over it
// (via CCmpTemplateManager). Now we need to remove that temporary
// blocker-disabling, so that we'll perform standard unit blocking instead.
if (cmpObstruction && cmpObstruction.GetBlockMovementFlag())
{
// The obstruction always blocks new foundations/construction,
// but we've temporarily allowed units to walk all over it
// (via CCmpTemplateManager). Now we need to remove that temporary
// blocker-disabling, so that we'll perform standard unit blocking instead.
cmpObstruction.SetDisableBlockMovementPathfinding(false, false, -1);
// Call the related trigger event
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("ConstructionStarted", {"foundation": this.entity, "template": this.finalTemplateName});
}
// Call the related trigger event
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("ConstructionStarted", {
"foundation": this.entity,
"template": this.finalTemplateName
});
// Switch foundation to scaffold variant
var cmpFoundationVisual = Engine.QueryInterface(this.entity, IID_Visual);
@ -249,6 +263,11 @@ Foundation.prototype.Build = function(builderEnt, work)
// Add an appropriate proportion of hitpoints
var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
if (!cmpHealth)
{
error("Foundation " + this.entity + " does not have a health component.");
return;
}
var maxHealth = cmpHealth.GetMaxHitpoints();
var deltaHP = Math.max(work, Math.min(maxHealth, Math.floor(work * this.GetBuildRate() * this.buildMultiplier)));
if (deltaHP > 0)
@ -274,9 +293,21 @@ Foundation.prototype.Build = function(builderEnt, work)
cmpBuildingVisual.SetActorSeed(cmpVisual.GetActorSeed());
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
{
error("Foundation " + this.entity + " does not have a position in-world.");
Engine.DestroyEntity(building);
return;
}
var cmpBuildingPosition = Engine.QueryInterface(building, IID_Position);
var pos = cmpPosition.GetPosition();
cmpBuildingPosition.JumpTo(pos.x, pos.z);
if (!cmpBuildingPosition)
{
error("New building " + building + " has no position component.");
Engine.DestroyEntity(building);
return;
}
var pos = cmpPosition.GetPosition2D();
cmpBuildingPosition.JumpTo(pos.x, pos.y);
var rot = cmpPosition.GetRotation();
cmpBuildingPosition.SetYRotation(rot.y);
cmpBuildingPosition.SetXZRotation(rot.x, rot.z);
@ -307,38 +338,57 @@ Foundation.prototype.Build = function(builderEnt, work)
else
{
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
{
error("Foundation " + this.entity + " has no ownership.");
Engine.DestroyEntity(building);
return;
}
owner = cmpOwnership.GetOwner();
}
var cmpBuildingOwnership = Engine.QueryInterface(building, IID_Ownership);
if (!cmpBuildingOwnership)
{
error("New Building " + building + " has no ownership.");
Engine.DestroyEntity(building);
return;
}
cmpBuildingOwnership.SetOwner(owner);
// ----------------------------------------------------------------------
/*
Copy over the obstruction control group IDs from the foundation
entities. This is needed to ensure that when a foundation is completed
and replaced by a new entity, it remains in the same control group(s)
as any other foundation entities that may surround it. This is the
mechanism that is used to e.g. enable wall pieces to be built closely
together, ignoring their mutual obstruction shapes (since they would
otherwise be prevented from being built so closely together). If the
control groups are not copied over, the new entity will default to a
new control group containing only itself, and will hence block
construction of any surrounding foundations that it was previously in
the same control group with.
// Copy over the obstruction control group IDs from the foundation entities. This is needed to ensure that when a foundation
// is completed and replaced by a new entity, it remains in the same control group(s) as any other foundation entities that
// may surround it. This is the mechanism that is used to e.g. enable wall pieces to be built closely together, ignoring their
// mutual obstruction shapes (since they would otherwise be prevented from being built so closely together). If the control
// groups are not copied over, the new entity will default to a new control group containing only itself, and will hence block
// construction of any surrounding foundations that it was previously in the same control group with.
Note that this will result in the completed building entities having
control group IDs that equal entity IDs of old (and soon to be deleted)
foundation entities. This should not have any consequences, however,
since the control group IDs are only meant to be unique identifiers,
which is still true when reusing the old ones.
*/
// Note that this will result in the completed building entities having control group IDs that equal entity IDs of old (and soon
// to be deleted) foundation entities. This should not have any consequences, however, since the control group IDs are only meant
// to be unique identifiers, which is still true when reusing the old ones.
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
var cmpBuildingObstruction = Engine.QueryInterface(building, IID_Obstruction);
cmpBuildingObstruction.SetControlGroup(cmpObstruction.GetControlGroup());
cmpBuildingObstruction.SetControlGroup2(cmpObstruction.GetControlGroup2());
if (cmpObstruction && cmpBuildingObstruction)
{
cmpBuildingObstruction.SetControlGroup(cmpObstruction.GetControlGroup());
cmpBuildingObstruction.SetControlGroup2(cmpObstruction.GetControlGroup2());
}
// ----------------------------------------------------------------------
var cmpPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
cmpPlayerStatisticsTracker.IncreaseConstructedBuildingsCounter(building);
var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
var cmpBuildingHealth = Engine.QueryInterface(building, IID_Health);
cmpBuildingHealth.SetHitpoints(cmpHealth.GetHitpoints());
if (cmpBuildingHealth)
cmpBuildingHealth.SetHitpoints(cmpHealth.GetHitpoints());
PlaySound("constructed", building);

View File

@ -44,7 +44,7 @@ Engine.DestroyEntity = function(ent)
for (var cid in g_Components[ent])
{
var cmp = g_Components[ent][cid];
if (cmp.Deinit)
if (cmp && cmp.Deinit)
cmp.Deinit();
}
@ -76,6 +76,13 @@ global.AddMock = function(ent, iid, mock)
g_Components[ent][iid] = mock;
};
global.DeleteMock = function(ent, iid)
{
if (!g_Components[ent])
g_Components[ent] = {};
delete g_Components[ent][iid];
};
global.ConstructComponent = function(ent, name, template)
{
var cmp = new g_ComponentTypes[name].ctor();

View File

@ -0,0 +1,139 @@
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/Cost.js");
Engine.LoadComponentScript("interfaces/Foundation.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/RallyPoint.js");
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
Engine.LoadComponentScript("interfaces/TerritoryDecay.js");
Engine.LoadComponentScript("interfaces/Trigger.js");
Engine.LoadComponentScript("Foundation.js");
AddMock(SYSTEM_ENTITY, IID_Trigger, {
"CallEvent": () => {},
});
let player = 1;
let playerEnt = 3;
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": () => playerEnt,
});
AddMock(playerEnt, IID_StatisticsTracker, {
"IncreaseConstructedBuildingsCounter": ent => {
TS_ASSERT_EQUALS(ent, newEnt);
},
});
let foundationEnt = 20;
let newEnt = 21;
let finalTemplate = "structures/athen_civil_centre.xml";
let foundationHP = 1;
let maxHP = 100;
AddMock(foundationEnt, IID_Cost, {
"GetBuildTime": () => 50,
"GetResourceCosts": () => ({ "wood": 100 }),
});
AddMock(foundationEnt, IID_Health, {
"GetHitpoints": () => foundationHP,
"GetMaxHitpoints": () => maxHP,
"Increase": hp => {
foundationHP = Math.min(foundationHP + hp, maxHP);
cmpFoundation.OnHealthChanged();
},
});
AddMock(foundationEnt, IID_Obstruction, {
"GetBlockMovementFlag": () => true,
"GetUnitCollisions": () => [],
"SetDisableBlockMovementPathfinding": () => {},
});
AddMock(foundationEnt, IID_Ownership, {
"GetOwner": () => player,
});
AddMock(foundationEnt, IID_Position, {
"GetPosition2D": () => new Vector2D(1, 2),
"GetRotation": () => new Vector3D(1, 2, 3),
"IsInWorld": () => true,
});
Engine.AddEntity = function(template)
{
AddMock(newEnt, IID_Position, {
"JumpTo": (x, y) => { TS_ASSERT_EQUALS(x, 1); TS_ASSERT_EQUALS(y, 2); },
"SetYRotation": r => { TS_ASSERT_EQUALS(r, 2); },
"SetXZRotation": (rx, rz) => {
TS_ASSERT_EQUALS(rx, 1);
TS_ASSERT_EQUALS(rz, 3);
},
});
AddMock(newEnt, IID_Ownership, {
"SetOwner": owner => { TS_ASSERT_EQUALS(owner, player); },
});
return newEnt;
};
function PlaySound(name, source)
{
TS_ASSERT_EQUALS(name, "constructed");
TS_ASSERT_EQUALS(source, newEnt);
};
Engine.RegisterGlobal("PlaySound", PlaySound);
Engine.RegisterGlobal("MT_EntityRenamed", "entityRenamed");
let cmpFoundation = ConstructComponent(foundationEnt, "Foundation", {});
// INITIALISE
cmpFoundation.InitialiseConstruction(player, finalTemplate);
TS_ASSERT_EQUALS(cmpFoundation.owner, player);
TS_ASSERT_EQUALS(cmpFoundation.finalTemplateName, finalTemplate);
TS_ASSERT_EQUALS(cmpFoundation.maxProgress, 0);
TS_ASSERT_EQUALS(cmpFoundation.initialised, true);
// BUILDER COUNT
let twoBuilderMultiplier = Math.pow(2, cmpFoundation.buildTimePenalty) / 2;
TS_ASSERT_EQUALS(cmpFoundation.GetNumBuilders(), 0);
cmpFoundation.AddBuilder(10);
TS_ASSERT_EQUALS(cmpFoundation.GetNumBuilders(), 1);
TS_ASSERT_EQUALS(cmpFoundation.buildMultiplier, 1);
cmpFoundation.AddBuilder(11);
TS_ASSERT_EQUALS(cmpFoundation.GetNumBuilders(), 2);
TS_ASSERT_EQUALS(cmpFoundation.buildMultiplier, twoBuilderMultiplier);
cmpFoundation.AddBuilder(11);
TS_ASSERT_EQUALS(cmpFoundation.GetNumBuilders(), 2);
TS_ASSERT_EQUALS(cmpFoundation.buildMultiplier, twoBuilderMultiplier);
cmpFoundation.RemoveBuilder(11);
TS_ASSERT_EQUALS(cmpFoundation.GetNumBuilders(), 1);
TS_ASSERT_EQUALS(cmpFoundation.buildMultiplier, 1);
cmpFoundation.RemoveBuilder(11);
TS_ASSERT_EQUALS(cmpFoundation.GetNumBuilders(), 1);
TS_ASSERT_EQUALS(cmpFoundation.buildMultiplier, 1);
// with cmpVisual
AddMock(foundationEnt, IID_Visual, {
"SetVariable": (key, num) => {
TS_ASSERT_EQUALS(key, "numbuilders");
TS_ASSERT_EQUALS(num, 2);
},
});
cmpFoundation.AddBuilder(11);
DeleteMock(foundationEnt, IID_Visual);
cmpFoundation.RemoveBuilder(11);
// COMMIT FOUNDATION
TS_ASSERT_EQUALS(cmpFoundation.committed, false);
let work = 5;
cmpFoundation.Build(10, work);
TS_ASSERT_EQUALS(cmpFoundation.committed, true);
TS_ASSERT_EQUALS(foundationHP, 1 + work * cmpFoundation.GetBuildRate() * cmpFoundation.buildMultiplier);
TS_ASSERT_EQUALS(cmpFoundation.maxProgress, foundationHP / maxHP);
// FINISH CONSTRUCTION
cmpFoundation.Build(10, 1000);
TS_ASSERT_EQUALS(cmpFoundation.maxProgress, 1);
TS_ASSERT_EQUALS(foundationHP, maxHP);