historic_bruno
31be9cd0de
Adds scaffold support for foundations, includes two examples for 3x3 and 4x4, fixes #1581. Extends CmpVisualActor and CUnit to support random variant seeds. Fixes bug in actor hotloading. Fixes serialization failure caused by destroying entities in OnDestroy handlers. This was SVN commit r13143.
320 lines
12 KiB
JavaScript
320 lines
12 KiB
JavaScript
function Foundation() {}
|
|
|
|
Foundation.prototype.Schema =
|
|
"<a:component type='internal'/><empty/>";
|
|
|
|
Foundation.prototype.Init = function()
|
|
{
|
|
// Foundations are initially 'uncommitted' and do not block unit movement at all
|
|
// (to prevent players exploiting free foundations to confuse enemy units).
|
|
// The first builder to reach the uncommitted foundation will tell friendly units
|
|
// and animals to move out of the way, then will commit the foundation and enable
|
|
// its obstruction once there's nothing in the way.
|
|
this.committed = false;
|
|
|
|
this.buildProgress = 0.0; // 0 <= progress <= 1
|
|
|
|
// Set up a timer so we can count the number of builders in a 1-second period.
|
|
// (We assume each builder only builds once per second, which is what UnitAI
|
|
// implements.)
|
|
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
|
this.timer = cmpTimer.SetInterval(this.entity, IID_Foundation, "UpdateTimeout", 1000, 1000, {});
|
|
this.recentBuilders = []; // builder entities since the last timeout
|
|
this.numRecentBuilders = 0; // number of builder entities as of the last timeout
|
|
this.buildMultiplier = 1; // Multiplier for the amount of work builders do.
|
|
|
|
this.previewEntity = INVALID_ENTITY;
|
|
};
|
|
|
|
Foundation.prototype.UpdateTimeout = function()
|
|
{
|
|
this.numRecentBuilders = this.recentBuilders.length;
|
|
this.recentBuilders = [];
|
|
this.SetBuildMultiplier();
|
|
|
|
Engine.QueryInterface(this.entity, IID_Visual).SetVariable("numbuilders", this.numRecentBuilders);
|
|
};
|
|
|
|
Foundation.prototype.InitialiseConstruction = function(owner, template)
|
|
{
|
|
var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
|
|
this.addedHitpoints = cmpHealth.GetHitpoints();
|
|
|
|
this.finalTemplateName = template;
|
|
|
|
// We need to know the owner in OnDestroy, but at that point the entity has already been
|
|
// decoupled from its owner, so we need to remember it in here (and assume it won't change)
|
|
this.owner = owner;
|
|
|
|
// Remember our cost here, so if it changes after construction begins (from technologies)
|
|
// we will use the correct values to refund partial construction costs
|
|
var cmpCost = Engine.QueryInterface(this.entity, IID_Cost);
|
|
this.costs = cmpCost.GetResourceCosts();
|
|
|
|
this.initialised = true;
|
|
};
|
|
|
|
Foundation.prototype.GetBuildPercentage = function()
|
|
{
|
|
return Math.floor(this.buildProgress * 100);
|
|
};
|
|
|
|
Foundation.prototype.IsFinished = function()
|
|
{
|
|
return (this.buildProgress >= 1.0);
|
|
};
|
|
|
|
Foundation.prototype.OnDestroy = function()
|
|
{
|
|
// Refund a portion of the construction cost, proportional to the amount of build progress remaining
|
|
|
|
if (!this.initialised) // this happens if the foundation was destroyed because the player had insufficient resources
|
|
return;
|
|
|
|
if (this.previewEntity != INVALID_ENTITY)
|
|
{
|
|
Engine.DestroyEntity(this.previewEntity);
|
|
this.previewEntity = INVALID_ENTITY;
|
|
}
|
|
|
|
if (this.buildProgress == 1.0)
|
|
return;
|
|
|
|
var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
|
|
var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(this.owner), IID_Player);
|
|
|
|
for (var r in this.costs)
|
|
{
|
|
var scaled = Math.floor(this.costs[r] * (1.0 - this.buildProgress));
|
|
if (scaled)
|
|
{
|
|
cmpPlayer.AddResource(r, scaled);
|
|
var cmpStatisticsTracker = QueryPlayerIDInterface(this.owner, IID_StatisticsTracker);
|
|
if (cmpStatisticsTracker)
|
|
cmpStatisticsTracker.IncreaseResourceUsedCounter(r, -scaled);
|
|
}
|
|
}
|
|
|
|
// Reset the timer
|
|
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
|
cmpTimer.CancelTimer(this.timer);
|
|
};
|
|
|
|
/**
|
|
* Adds a builder to the counter. Used indirectly by UnitAI to signal that
|
|
* a unit has started the construction timer and will actually build soon.
|
|
*/
|
|
Foundation.prototype.AddBuilder = function(builderEnt)
|
|
{
|
|
if (this.recentBuilders.indexOf(builderEnt) != -1)
|
|
return;
|
|
|
|
this.recentBuilders.push(builderEnt);
|
|
if (this.recentBuilders.length > this.numRecentBuilders)
|
|
{
|
|
this.numRecentBuilders = this.recentBuilders.length;
|
|
this.SetBuildMultiplier();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the build rate multiplier, which is applied to all builders.
|
|
*/
|
|
Foundation.prototype.SetBuildMultiplier = function()
|
|
{
|
|
// Yields a total rate of construction equal to numRecentBuilders^0.7
|
|
this.buildMultiplier = Math.pow(this.numRecentBuilders, 0.7) / this.numRecentBuilders;
|
|
}
|
|
|
|
/**
|
|
* Perform some number of seconds of construction work.
|
|
* Returns true if the construction is completed.
|
|
*/
|
|
Foundation.prototype.Build = function(builderEnt, work)
|
|
{
|
|
// Do nothing if we've already finished building
|
|
// (The entity will be destroyed soon after completion so
|
|
// this won't happen much)
|
|
if (this.buildProgress == 1.0)
|
|
return;
|
|
|
|
// Handle the initial 'committing' of the foundation
|
|
if (!this.committed)
|
|
{
|
|
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
|
|
if (cmpObstruction && cmpObstruction.GetBlockMovementFlag())
|
|
{
|
|
// If there's any units in the way, ask them to move away
|
|
// and return early from this method.
|
|
// Otherwise enable this obstruction so it blocks any further
|
|
// units, and continue building.
|
|
|
|
var collisions = cmpObstruction.GetEntityCollisions(true, true);
|
|
if (collisions.length)
|
|
{
|
|
for each (var ent in collisions)
|
|
{
|
|
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
|
|
if (cmpUnitAI)
|
|
cmpUnitAI.LeaveFoundation(this.entity);
|
|
else
|
|
{
|
|
// If obstructing fauna is gaia but doesn't have UnitAI, just destroy it
|
|
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
|
|
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
|
|
if (cmpOwnership && cmpIdentity && cmpOwnership.GetOwner() == 0 && cmpIdentity.HasClass("Animal"))
|
|
Engine.DestroyEntity(ent);
|
|
}
|
|
|
|
// TODO: What if an obstruction has no UnitAI?
|
|
}
|
|
|
|
// TODO: maybe we should tell the builder to use a special
|
|
// animation to indicate they're waiting for people to get
|
|
// out the way
|
|
|
|
return;
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// Switch foundation to scaffold variant
|
|
var cmpFoundationVisual = Engine.QueryInterface(this.entity, IID_Visual);
|
|
if (cmpFoundationVisual)
|
|
cmpFoundationVisual.SelectAnimation("scaffold", false, 1.0, "");
|
|
|
|
// Create preview entity and copy various parameters from the foundation
|
|
if (cmpFoundationVisual && cmpFoundationVisual.HasConstructionPreview())
|
|
{
|
|
this.previewEntity = Engine.AddEntity("construction|"+this.finalTemplateName);
|
|
var cmpFoundationOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
|
|
var cmpPreviewOwnership = Engine.QueryInterface(this.previewEntity, IID_Ownership);
|
|
cmpPreviewOwnership.SetOwner(cmpFoundationOwnership.GetOwner());
|
|
|
|
// Initially hide the preview underground
|
|
var cmpPreviewVisual = Engine.QueryInterface(this.previewEntity, IID_Visual);
|
|
if (cmpPreviewVisual)
|
|
{
|
|
cmpPreviewVisual.SetConstructionProgress(0.0);
|
|
cmpPreviewVisual.SetActorSeed(cmpFoundationVisual.GetActorSeed());
|
|
}
|
|
|
|
var cmpFoundationPosition = Engine.QueryInterface(this.entity, IID_Position);
|
|
var pos = cmpFoundationPosition.GetPosition();
|
|
var rot = cmpFoundationPosition.GetRotation();
|
|
var cmpPreviewPosition = Engine.QueryInterface(this.previewEntity, IID_Position);
|
|
cmpPreviewPosition.SetYRotation(rot.y);
|
|
cmpPreviewPosition.SetXZRotation(rot.x, rot.z);
|
|
cmpPreviewPosition.JumpTo(pos.x, pos.z);
|
|
}
|
|
|
|
this.committed = true;
|
|
}
|
|
|
|
// Calculate the amount of progress that will be added (where 1.0 = completion)
|
|
var cmpCost = Engine.QueryInterface(this.entity, IID_Cost);
|
|
var amount = work / cmpCost.GetBuildTime();
|
|
|
|
// Record this builder so we can count the total number
|
|
this.AddBuilder(builderEnt);
|
|
|
|
this.buildProgress += amount * this.buildMultiplier;
|
|
if (this.buildProgress > 1.0)
|
|
this.buildProgress = 1.0;
|
|
|
|
Engine.PostMessage(this.entity, MT_FoundationProgressChanged, { "to": this.GetBuildPercentage() });
|
|
|
|
// Gradually reveal the final building preview
|
|
var cmpPreviewVisual = Engine.QueryInterface(this.previewEntity, IID_Visual);
|
|
if (cmpPreviewVisual)
|
|
cmpPreviewVisual.SetConstructionProgress(this.buildProgress);
|
|
|
|
// Add an appropriate proportion of hitpoints
|
|
var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
|
|
var maxHealth = cmpHealth.GetMaxHitpoints();
|
|
var targetHP = Math.max(0, Math.min(maxHealth, Math.floor(maxHealth * this.buildProgress)));
|
|
var deltaHP = targetHP - this.addedHitpoints;
|
|
if (deltaHP > 0)
|
|
{
|
|
cmpHealth.Increase(deltaHP);
|
|
this.addedHitpoints += deltaHP;
|
|
}
|
|
|
|
if (this.buildProgress >= 1.0)
|
|
{
|
|
// Finished construction
|
|
|
|
// Create the real entity
|
|
var building = Engine.AddEntity(this.finalTemplateName);
|
|
|
|
// Copy various parameters from the foundation
|
|
|
|
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
|
|
var cmpBuildingVisual = Engine.QueryInterface(building, IID_Visual)
|
|
if (cmpVisual && cmpBuildingVisual)
|
|
cmpBuildingVisual.SetActorSeed(cmpVisual.GetActorSeed());
|
|
|
|
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
|
|
var cmpBuildingPosition = Engine.QueryInterface(building, IID_Position);
|
|
var pos = cmpPosition.GetPosition();
|
|
cmpBuildingPosition.JumpTo(pos.x, pos.z);
|
|
var rot = cmpPosition.GetRotation();
|
|
cmpBuildingPosition.SetYRotation(rot.y);
|
|
cmpBuildingPosition.SetXZRotation(rot.x, rot.z);
|
|
// TODO: should add a ICmpPosition::CopyFrom() instead of all this
|
|
|
|
// ----------------------------------------------------------------------
|
|
|
|
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
|
|
var cmpBuildingOwnership = Engine.QueryInterface(building, IID_Ownership);
|
|
cmpBuildingOwnership.SetOwner(cmpOwnership.GetOwner());
|
|
|
|
// ----------------------------------------------------------------------
|
|
|
|
// 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.
|
|
|
|
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
|
|
var cmpBuildingObstruction = Engine.QueryInterface(building, IID_Obstruction);
|
|
cmpBuildingObstruction.SetControlGroup(cmpObstruction.GetControlGroup());
|
|
cmpBuildingObstruction.SetControlGroup2(cmpObstruction.GetControlGroup2());
|
|
|
|
// ----------------------------------------------------------------------
|
|
|
|
var cmpPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
|
|
cmpPlayerStatisticsTracker.IncreaseConstructedBuildingsCounter();
|
|
|
|
var cmpIdentity = Engine.QueryInterface(building, IID_Identity);
|
|
if (cmpIdentity.GetClassesList().indexOf("CivCentre") != -1)
|
|
cmpPlayerStatisticsTracker.IncreaseBuiltCivCentresCounter();
|
|
|
|
var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
|
|
var cmpBuildingHealth = Engine.QueryInterface(building, IID_Health);
|
|
cmpBuildingHealth.SetHitpoints(cmpHealth.GetHitpoints());
|
|
|
|
PlaySound("constructed", building);
|
|
|
|
Engine.PostMessage(this.entity, MT_ConstructionFinished,
|
|
{ "entity": this.entity, "newentity": building });
|
|
Engine.BroadcastMessage(MT_EntityRenamed, { entity: this.entity, newentity: building });
|
|
|
|
Engine.DestroyEntity(this.entity);
|
|
}
|
|
};
|
|
|
|
Engine.RegisterComponentType(IID_Foundation, "Foundation", Foundation);
|
|
|