1
1
forked from 0ad/0ad

Build using Builder instead of UnitAI.

Moves the building logic from UnitAI to Builder.
Makes it easier for modders to change building behaviour, e.g. letting
structures build.

Differential revision: D3812
Comment by: @Angen
This was SVN commit r25208.
This commit is contained in:
Freagarach 2021-04-08 05:50:18 +00:00
parent f2d5603422
commit e3695abe59
4 changed files with 175 additions and 55 deletions

View File

@ -18,12 +18,15 @@ Builder.prototype.Schema =
"<text/>" +
"</element>";
/*
* Build interval and repeat time, in ms.
*/
Builder.prototype.BUILD_INTERVAL = 1000;
Builder.prototype.Init = function()
{
};
Builder.prototype.Serialize = null; // we have no dynamic state to save
Builder.prototype.GetEntitiesList = function()
{
let string = this.template.Entities._string;
@ -80,28 +83,120 @@ Builder.prototype.CanRepair = function(target)
};
/**
* Build/repair the target entity. This should only be called after a successful range check.
* It should be called at a rate of once per second.
* @param {number} target - The target to repair.
* @param {number} callerIID - The IID to notify on specific events.
* @return {boolean} - Whether we started repairing.
*/
Builder.prototype.PerformBuilding = function(target)
Builder.prototype.StartRepairing = function(target, callerIID)
{
let rate = this.GetRate();
if (this.target)
this.StopRepairing();
let cmpFoundation = Engine.QueryInterface(target, IID_Foundation);
if (!this.CanRepair(target))
return false;
let cmpBuilderList = QueryBuilderListInterface(target);
if (cmpBuilderList)
cmpBuilderList.AddBuilder(this.entity);
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
cmpVisual.SelectAnimation("build", false, 1.0);
this.target = target;
this.callerIID = callerIID;
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetInterval(this.entity, IID_Builder, "PerformBuilding", this.BUILD_INTERVAL, this.BUILD_INTERVAL, null);
return true;
};
/**
* @param {string} reason - The reason why we stopped repairing.
*/
Builder.prototype.StopRepairing = function(reason)
{
if (this.timer)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
delete this.timer;
}
if (this.target)
{
let cmpBuilderList = QueryBuilderListInterface(this.target);
if (cmpBuilderList)
cmpBuilderList.RemoveBuilder(this.entity);
delete this.target;
}
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
cmpVisual.SelectAnimation("idle", false, 1.0);
// The callerIID component may start repairing again,
// replacing the callerIID, hence save that.
let callerIID = this.callerIID;
delete this.callerIID;
if (reason && callerIID)
{
let component = Engine.QueryInterface(this.entity, callerIID);
if (component)
component.ProcessMessage(reason, null);
}
};
/**
* Repair our target entity.
* @params - data and lateness are unused.
*/
Builder.prototype.PerformBuilding = function(data, lateness)
{
if (!this.CanRepair(this.target))
{
this.StopRepairing("TargetInvalidated");
return;
}
if (!this.IsTargetInRange(this.target))
{
this.StopRepairing("OutOfRange");
return;
}
// ToDo: Enable entities to keep facing a target.
Engine.QueryInterface(this.entity, IID_UnitAI)?.FaceTowardsTarget(this.target);
let cmpFoundation = Engine.QueryInterface(this.target, IID_Foundation);
if (cmpFoundation)
{
cmpFoundation.Build(this.entity, rate);
cmpFoundation.Build(this.entity, this.GetRate());
return;
}
let cmpRepairable = Engine.QueryInterface(target, IID_Repairable);
let cmpRepairable = Engine.QueryInterface(this.target, IID_Repairable);
if (cmpRepairable)
{
cmpRepairable.Repair(this.entity, rate);
cmpRepairable.Repair(this.entity, this.GetRate());
return;
}
};
/**
* @param {number} - The entity ID of the target to check.
* @return {boolean} - Whether this entity is in range of its target.
*/
Builder.prototype.IsTargetInRange = function(target)
{
let range = this.GetRange();
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false);
};
Builder.prototype.OnValueModification = function(msg)
{
if (msg.component != "Builder" || !msg.valueNames.some(name => name.endsWith('_string')))

View File

@ -2961,6 +2961,13 @@ UnitAI.prototype.UnitFsmSpec = {
"REPAIRING": {
"enter": function() {
let cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
if (!cmpBuilder)
{
this.FinishOrder();
return true;
}
// If this order was forced, the player probably gave it, but now we've reached the target
// switch to an unforced order (can be interrupted by attacks)
if (this.order.data.force)
@ -2968,68 +2975,43 @@ UnitAI.prototype.UnitFsmSpec = {
this.order.data.force = false;
// Needed to remove the entity from the builder list when leaving this state.
this.repairTarget = this.order.data.target;
if (!this.CanRepair(this.repairTarget))
if (!this.CheckTargetRange(this.order.data.target, IID_Builder))
{
this.FinishOrder();
this.ProcessMessage("OutOfRange");
return true;
}
if (!this.CheckTargetRange(this.repairTarget, IID_Builder))
{
this.SetNextState("APPROACHING");
return true;
}
let cmpHealth = Engine.QueryInterface(this.repairTarget, IID_Health);
let cmpHealth = Engine.QueryInterface(this.order.data.target, IID_Health);
if (cmpHealth && cmpHealth.GetHitpoints() >= cmpHealth.GetMaxHitpoints())
{
// The building was already finished/fully repaired before we arrived;
// let the ConstructionFinished handler handle this.
this.ConstructionFinished({ "entity": this.repairTarget, "newentity": this.repairTarget });
this.ConstructionFinished({ "entity": this.order.data.target, "newentity": this.order.data.target });
return true;
}
let cmpBuilderList = QueryBuilderListInterface(this.repairTarget);
if (cmpBuilderList)
cmpBuilderList.AddBuilder(this.entity);
if (!cmpBuilder.StartRepairing(this.order.data.target, IID_UnitAI))
{
this.ProcessMessage("TargetInvalidated");
return true;
}
this.FaceTowardsTarget(this.repairTarget);
this.SelectAnimation("build");
this.StartTimer(1000, 1000);
this.FaceTowardsTarget(this.order.data.target);
return false;
},
"leave": function() {
let cmpBuilderList = QueryBuilderListInterface(this.repairTarget);
if (cmpBuilderList)
cmpBuilderList.RemoveBuilder(this.entity);
delete this.repairTarget;
this.StopTimer();
this.ResetAnimation();
let cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
if (cmpBuilder)
cmpBuilder.StopRepairing();
},
"Timer": function(msg) {
if (!this.CanRepair(this.repairTarget))
{
this.FinishOrder();
return;
}
"OutOfRange": function(msg) {
this.SetNextState("APPROACHING");
},
this.FaceTowardsTarget(this.repairTarget);
let cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
cmpBuilder.PerformBuilding(this.repairTarget);
// If the building is completed, the leave() function will be called
// by the ConstructionFinished message.
// In that case, the repairTarget is deleted, and we can just return.
if (!this.repairTarget)
return;
if (!this.CheckTargetRange(this.repairTarget, IID_Builder))
this.SetNextState("APPROACHING");
"TargetInvalidated": function(msg) {
this.FinishOrder();
},
},

View File

@ -1,11 +1,21 @@
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/Builder.js");
Engine.LoadComponentScript("interfaces/Foundation.js");
Engine.LoadComponentScript("interfaces/Repairable.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("Builder.js");
Engine.LoadComponentScript("Timer.js");
const builderId = 6;
const target = 7;
const playerId = 1;
const playerEntityID = 2;
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
"IsInTargetRange": () => true
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"TemplateExists": () => true
});
@ -14,7 +24,7 @@ Engine.RegisterGlobal("ApplyValueModificationsToEntity", (prop, oVal, ent) => oV
let cmpBuilder = ConstructComponent(builderId, "Builder", {
"Rate": 1.0,
"Rate": "1.0",
"Entities": { "_string": "structures/{civ}/barracks structures/{civ}/civil_centre structures/{native}/house" }
});
@ -81,3 +91,30 @@ AddMock(builderId, IID_Obstruction, {
});
TS_ASSERT_UNEVAL_EQUALS(cmpBuilder.GetRange(), { "max": 3, "min": 0 });
// Test repairing.
AddMock(playerEntityID, IID_Player, {
"IsAlly": (p) => p == playerId
});
AddMock(target, IID_Ownership, {
"GetOwner": () => playerId
});
let increased = false;
AddMock(target, IID_Foundation, {
"Build": (entity, amount) => {
increased = true;
TS_ASSERT_EQUALS(amount, 1);
},
"AddBuilder": () => {}
});
let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer");
TS_ASSERT(cmpBuilder.StartRepairing(target));
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT(increased);
increased = false;
cmpTimer.OnUpdate({ "turnLength": 2 });
TS_ASSERT(increased);

View File

@ -68,8 +68,14 @@ TestTargetEntityRenaming(
);
TestTargetEntityRenaming(
"INDIVIDUAL.REPAIR.REPAIRING", "INDIVIDUAL.IDLE",
"INDIVIDUAL.REPAIR.REPAIRING", "INDIVIDUAL.REPAIR.REPAIRING",
(unitAI, player_ent, target_ent) => {
AddMock(player_ent, IID_Builder, {
"StartRepairing": () => true,
"StopRepairing": () => {}
});
QueryBuilderListInterface = () => {};
unitAI.CheckTargetRange = () => true;
unitAI.CanRepair = (target) => target == target_ent;