Line data Source code
1 : function Foundation() {}
2 :
3 1 : Foundation.prototype.Schema =
4 : "<element name='BuildTimeModifier' a:help='Effect for having multiple builders.'>" +
5 : "<ref name='nonNegativeDecimal'/>" +
6 : "</element>";
7 :
8 1 : Foundation.prototype.Init = function()
9 : {
10 : // Foundations are initially 'uncommitted' and do not block unit movement at all
11 : // (to prevent players exploiting free foundations to confuse enemy units).
12 : // The first builder to reach the uncommitted foundation will tell friendly units
13 : // and animals to move out of the way, then will commit the foundation and enable
14 : // its obstruction once there's nothing in the way.
15 5 : this.committed = false;
16 :
17 5 : this.builders = new Map(); // Map of builder entities to their work per second
18 5 : this.totalBuilderRate = 0; // Total amount of work the builders do each second
19 5 : this.buildMultiplier = 1; // Multiplier for the amount of work builders do
20 :
21 5 : this.buildTimeModifier = +this.template.BuildTimeModifier;
22 :
23 5 : this.previewEntity = INVALID_ENTITY;
24 : };
25 :
26 1 : Foundation.prototype.Serialize = function()
27 : {
28 0 : let ret = Object.assign({}, this);
29 0 : ret.previewEntity = INVALID_ENTITY;
30 0 : return ret;
31 : };
32 :
33 1 : Foundation.prototype.Deserialize = function(data)
34 : {
35 0 : this.Init();
36 0 : Object.assign(this, data);
37 : };
38 :
39 1 : Foundation.prototype.OnDeserialized = function()
40 : {
41 0 : this.CreateConstructionPreview();
42 : };
43 :
44 1 : Foundation.prototype.InitialiseConstruction = function(template)
45 : {
46 5 : this.finalTemplateName = template;
47 :
48 : // Remember the cost here, so if it changes after construction begins (from auras or technologies)
49 : // we will use the correct values to refund partial construction costs.
50 5 : let cmpCost = Engine.QueryInterface(this.entity, IID_Cost);
51 5 : if (!cmpCost)
52 0 : error("A foundation, from " + template + ", must have a cost component to know the build time");
53 :
54 5 : this.costs = cmpCost.GetResourceCosts();
55 :
56 5 : this.maxProgress = 0;
57 :
58 5 : this.initialised = true;
59 : };
60 :
61 : /**
62 : * Moving the revelation logic from Build to here makes the building sink if
63 : * it is attacked.
64 : */
65 1 : Foundation.prototype.OnHealthChanged = function(msg)
66 : {
67 17 : let cmpPosition = Engine.QueryInterface(this.previewEntity, IID_Position);
68 17 : if (cmpPosition)
69 2 : cmpPosition.SetConstructionProgress(this.GetBuildProgress());
70 :
71 17 : Engine.PostMessage(this.entity, MT_FoundationProgressChanged, { "to": this.GetBuildPercentage() });
72 : };
73 :
74 : /**
75 : * Returns the current build progress in a [0,1] range.
76 : */
77 1 : Foundation.prototype.GetBuildProgress = function()
78 : {
79 82 : let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
80 82 : if (!cmpHealth)
81 0 : return 0;
82 :
83 82 : return cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints();
84 : };
85 :
86 1 : Foundation.prototype.GetBuildPercentage = function()
87 : {
88 37 : return Math.floor(this.GetBuildProgress() * 100);
89 : };
90 :
91 : /**
92 : * @return {number[]} - An array containing the entity IDs of assigned builders.
93 : */
94 1 : Foundation.prototype.GetBuilders = function()
95 : {
96 17 : return Array.from(this.builders.keys());
97 : };
98 :
99 1 : Foundation.prototype.GetNumBuilders = function()
100 : {
101 49 : return this.builders.size;
102 : };
103 :
104 1 : Foundation.prototype.IsFinished = function()
105 : {
106 17 : return (this.GetBuildProgress() == 1.0);
107 : };
108 :
109 1 : Foundation.prototype.OnOwnershipChanged = function(msg)
110 : {
111 0 : if (msg.to != INVALID_PLAYER && this.previewEntity != INVALID_ENTITY)
112 : {
113 0 : let cmpPreviewOwnership = Engine.QueryInterface(this.previewEntity, IID_Ownership);
114 0 : if (cmpPreviewOwnership)
115 0 : cmpPreviewOwnership.SetOwner(msg.to);
116 0 : return;
117 : }
118 :
119 0 : if (msg.to != INVALID_PLAYER || !this.initialised)
120 0 : return;
121 :
122 0 : if (this.previewEntity != INVALID_ENTITY)
123 : {
124 0 : Engine.DestroyEntity(this.previewEntity);
125 0 : this.previewEntity = INVALID_ENTITY;
126 : }
127 :
128 0 : if (this.IsFinished())
129 0 : return;
130 :
131 0 : let cmpPlayer = QueryPlayerIDInterface(msg.from);
132 0 : let cmpStatisticsTracker = QueryPlayerIDInterface(msg.from, IID_StatisticsTracker);
133 :
134 : // Refund a portion of the construction cost, proportional
135 : // to the amount of build progress remaining.
136 0 : for (let r in this.costs)
137 : {
138 0 : let scaled = Math.ceil(this.costs[r] * (1.0 - this.maxProgress));
139 0 : if (scaled)
140 : {
141 0 : if (cmpPlayer)
142 0 : cmpPlayer.AddResource(r, scaled);
143 0 : if (cmpStatisticsTracker)
144 0 : cmpStatisticsTracker.IncreaseResourceUsedCounter(r, -scaled);
145 : }
146 : }
147 : };
148 :
149 : /**
150 : * @param {number[]} builders - An array containing the entity IDs of builders to assign.
151 : */
152 1 : Foundation.prototype.AddBuilders = function(builders)
153 : {
154 0 : let changed = false;
155 0 : for (let builder of builders)
156 0 : changed = this.AddBuilderHelper(builder) || changed;
157 :
158 0 : if (changed)
159 0 : this.HandleBuildersChanged();
160 : };
161 :
162 : /**
163 : * @param {number} builderEnt - The entity to add.
164 : * @return {boolean} - Whether the addition was successful.
165 : */
166 1 : Foundation.prototype.AddBuilderHelper = function(builderEnt)
167 : {
168 13 : if (this.builders.has(builderEnt))
169 4 : return false;
170 :
171 9 : let cmpBuilder = Engine.QueryInterface(builderEnt, IID_Builder) ||
172 : Engine.QueryInterface(this.entity, IID_AutoBuildable);
173 9 : if (!cmpBuilder)
174 1 : return false;
175 :
176 8 : let buildRate = cmpBuilder.GetRate();
177 8 : this.builders.set(builderEnt, buildRate);
178 8 : this.totalBuilderRate += buildRate;
179 :
180 8 : return true;
181 : };
182 :
183 : /**
184 : * @param {number} builderEnt - The entity to add.
185 : */
186 1 : Foundation.prototype.AddBuilder = function(builderEnt)
187 : {
188 13 : if (this.AddBuilderHelper(builderEnt))
189 8 : this.HandleBuildersChanged();
190 : };
191 :
192 : /**
193 : * @param {number} builderEnt - The entity to remove.
194 : */
195 1 : Foundation.prototype.RemoveBuilder = function(builderEnt)
196 : {
197 9 : if (!this.builders.has(builderEnt))
198 4 : return;
199 :
200 5 : this.totalBuilderRate -= this.builders.get(builderEnt);
201 5 : this.builders.delete(builderEnt);
202 5 : this.HandleBuildersChanged();
203 : };
204 :
205 : /**
206 : * This has to be called whenever the number of builders change.
207 : */
208 1 : Foundation.prototype.HandleBuildersChanged = function()
209 : {
210 13 : this.SetBuildMultiplier();
211 :
212 13 : let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
213 13 : if (cmpVisual)
214 3 : cmpVisual.SetVariable("numbuilders", this.GetNumBuilders());
215 :
216 13 : Engine.PostMessage(this.entity, MT_FoundationBuildersChanged, { "to": this.GetBuilders() });
217 : };
218 :
219 : /**
220 : * The build multiplier is a penalty that is applied to each builder.
221 : * For example, ten women build at a combined rate of 10^0.7 = 5.01 instead of 10.
222 : */
223 1 : Foundation.prototype.CalculateBuildMultiplier = function(num)
224 : {
225 : // Avoid division by zero, in particular 0/0 = NaN which isn't reliably serialized
226 33 : return num < 2 ? 1 : Math.pow(num, this.buildTimeModifier) / num;
227 : };
228 :
229 1 : Foundation.prototype.SetBuildMultiplier = function()
230 : {
231 13 : this.buildMultiplier = this.CalculateBuildMultiplier(this.GetNumBuilders());
232 : };
233 :
234 1 : Foundation.prototype.GetBuildTime = function()
235 : {
236 8 : let timeLeft = (1 - this.GetBuildProgress()) * Engine.QueryInterface(this.entity, IID_Cost).GetBuildTime();
237 8 : let rate = this.totalBuilderRate * this.buildMultiplier;
238 8 : let rateNew = (this.totalBuilderRate + 1) * this.CalculateBuildMultiplier(this.GetNumBuilders() + 1);
239 8 : return {
240 : // Avoid division by zero, in particular 0/0 = NaN which isn't reliably serialized
241 : "timeRemaining": rate ? timeLeft / rate : 0,
242 : "timeRemainingNew": timeLeft / rateNew
243 : };
244 : };
245 :
246 : /**
247 : * @return {boolean} - Whether the foundation has been committed sucessfully.
248 : */
249 1 : Foundation.prototype.Commit = function()
250 : {
251 5 : if (this.committed)
252 0 : return false;
253 :
254 5 : let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
255 5 : if (cmpObstruction && cmpObstruction.GetBlockMovementFlag(true))
256 : {
257 4 : for (let ent of cmpObstruction.GetEntitiesDeletedUponConstruction())
258 0 : Engine.DestroyEntity(ent);
259 :
260 4 : let collisions = cmpObstruction.GetEntitiesBlockingConstruction();
261 4 : if (collisions.length)
262 : {
263 0 : for (let ent of collisions)
264 : {
265 0 : let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
266 0 : if (cmpUnitAI)
267 0 : cmpUnitAI.LeaveFoundation(this.entity);
268 :
269 : // TODO: What if an obstruction has no UnitAI?
270 : }
271 :
272 : // TODO: maybe we should tell the builder to use a special
273 : // animation to indicate they're waiting for people to get
274 : // out the way
275 :
276 0 : return false;
277 : }
278 : }
279 :
280 : // The obstruction always blocks new foundations/construction,
281 : // but we've temporarily allowed units to walk all over it
282 : // (via CCmpTemplateManager). Now we need to remove that temporary
283 : // blocker-disabling, so that we'll perform standard unit blocking instead.
284 5 : if (cmpObstruction)
285 4 : cmpObstruction.SetDisableBlockMovementPathfinding(false, false, -1);
286 :
287 5 : let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
288 5 : cmpTrigger.CallEvent("OnConstructionStarted", {
289 : "foundation": this.entity,
290 : "template": this.finalTemplateName
291 : });
292 :
293 5 : let cmpFoundationVisual = Engine.QueryInterface(this.entity, IID_Visual);
294 5 : if (cmpFoundationVisual)
295 1 : cmpFoundationVisual.SelectAnimation("scaffold", false, 1.0);
296 :
297 5 : this.committed = true;
298 5 : this.CreateConstructionPreview();
299 5 : return true;
300 : };
301 :
302 : /**
303 : * Perform some number of seconds of construction work.
304 : * Returns true if the construction is completed.
305 : */
306 1 : Foundation.prototype.Build = function(builderEnt, work)
307 : {
308 : // Do nothing if we've already finished building
309 : // (The entity will be destroyed soon after completion so
310 : // this won't happen much.)
311 17 : if (this.IsFinished())
312 0 : return;
313 :
314 17 : if (!this.committed && !this.Commit())
315 0 : return;
316 :
317 17 : let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
318 17 : if (!cmpHealth)
319 : {
320 0 : error("Foundation " + this.entity + " does not have a health component.");
321 0 : return;
322 : }
323 17 : let deltaHP = work * this.GetBuildRate() * this.buildMultiplier;
324 17 : if (deltaHP > 0)
325 17 : cmpHealth.Increase(deltaHP);
326 :
327 : // Update the total builder rate.
328 17 : this.totalBuilderRate += work - this.builders.get(builderEnt);
329 17 : this.builders.set(builderEnt, work);
330 :
331 : // Remember our max progress for partial refund in case of destruction.
332 17 : this.maxProgress = Math.max(this.maxProgress, this.GetBuildProgress());
333 :
334 17 : if (this.maxProgress >= 1.0)
335 : {
336 4 : let cmpPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
337 :
338 4 : let building = ChangeEntityTemplate(this.entity, this.finalTemplateName);
339 :
340 4 : const cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
341 4 : const cmpBuildingIdentity = Engine.QueryInterface(building, IID_Identity);
342 4 : if (cmpIdentity && cmpBuildingIdentity)
343 : {
344 0 : const oldPhenotype = cmpIdentity.GetPhenotype();
345 0 : if (cmpBuildingIdentity.GetPhenotype() !== oldPhenotype)
346 : {
347 0 : cmpBuildingIdentity.SetPhenotype(oldPhenotype);
348 0 : Engine.QueryInterface(building, IID_Visual)?.RecomputeActorName();
349 : }
350 : }
351 :
352 4 : if (cmpPlayerStatisticsTracker)
353 1 : cmpPlayerStatisticsTracker.IncreaseConstructedBuildingsCounter(building);
354 :
355 4 : PlaySound("constructed", building);
356 :
357 4 : Engine.PostMessage(this.entity, MT_ConstructionFinished,
358 : { "entity": this.entity, "newentity": building });
359 :
360 4 : for (let builder of this.GetBuilders())
361 : {
362 4 : let cmpUnitAIBuilder = Engine.QueryInterface(builder, IID_UnitAI);
363 4 : if (cmpUnitAIBuilder)
364 0 : cmpUnitAIBuilder.ConstructionFinished({ "entity": this.entity, "newentity": building });
365 : }
366 : }
367 : };
368 :
369 1 : Foundation.prototype.GetBuildRate = function()
370 : {
371 25 : let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
372 25 : let cmpCost = Engine.QueryInterface(this.entity, IID_Cost);
373 : // Return infinity for instant structure conversion
374 25 : return cmpHealth.GetMaxHitpoints() / cmpCost.GetBuildTime();
375 : };
376 :
377 : /**
378 : * Create preview entity and copy various parameters from the foundation.
379 : */
380 1 : Foundation.prototype.CreateConstructionPreview = function()
381 : {
382 5 : if (this.previewEntity)
383 : {
384 5 : Engine.DestroyEntity(this.previewEntity);
385 5 : this.previewEntity = INVALID_ENTITY;
386 : }
387 :
388 5 : if (!this.committed)
389 0 : return;
390 :
391 5 : let cmpFoundationVisual = Engine.QueryInterface(this.entity, IID_Visual);
392 5 : if (!cmpFoundationVisual || !cmpFoundationVisual.HasConstructionPreview())
393 4 : return;
394 :
395 1 : this.previewEntity = Engine.AddLocalEntity("construction|"+this.finalTemplateName);
396 1 : let cmpFoundationOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
397 1 : let cmpPreviewOwnership = Engine.QueryInterface(this.previewEntity, IID_Ownership);
398 1 : if (cmpFoundationOwnership && cmpPreviewOwnership)
399 1 : cmpPreviewOwnership.SetOwner(cmpFoundationOwnership.GetOwner());
400 :
401 : // TODO: the 'preview' would be invisible if it doesn't have the below component,
402 : // Maybe it makes more sense to simply delete it then?
403 :
404 : // Initially hide the preview underground
405 1 : let cmpPreviewPosition = Engine.QueryInterface(this.previewEntity, IID_Position);
406 1 : let cmpFoundationPosition = Engine.QueryInterface(this.entity, IID_Position);
407 1 : if (cmpPreviewPosition && cmpFoundationPosition)
408 : {
409 1 : let rot = cmpFoundationPosition.GetRotation();
410 1 : cmpPreviewPosition.SetYRotation(rot.y);
411 1 : cmpPreviewPosition.SetXZRotation(rot.x, rot.z);
412 :
413 1 : let pos = cmpFoundationPosition.GetPosition2D();
414 1 : cmpPreviewPosition.JumpTo(pos.x, pos.y);
415 :
416 1 : cmpPreviewPosition.SetConstructionProgress(this.GetBuildProgress());
417 : }
418 :
419 1 : let cmpPreviewVisual = Engine.QueryInterface(this.previewEntity, IID_Visual);
420 1 : if (cmpPreviewVisual && cmpFoundationVisual)
421 : {
422 0 : cmpPreviewVisual.SetActorSeed(cmpFoundationVisual.GetActorSeed());
423 0 : cmpPreviewVisual.SelectAnimation("scaffold", false, 1.0);
424 : }
425 : };
426 :
427 1 : Foundation.prototype.OnEntityRenamed = function(msg)
428 : {
429 0 : let cmpFoundationNew = Engine.QueryInterface(msg.newentity, IID_Foundation);
430 0 : if (cmpFoundationNew)
431 0 : cmpFoundationNew.AddBuilders(this.GetBuilders());
432 : };
433 :
434 : function FoundationMirage() {}
435 1 : FoundationMirage.prototype.Init = function(cmpFoundation)
436 : {
437 0 : this.numBuilders = cmpFoundation.GetNumBuilders();
438 0 : this.buildTime = cmpFoundation.GetBuildTime();
439 : };
440 :
441 1 : FoundationMirage.prototype.GetNumBuilders = function() { return this.numBuilders; };
442 1 : FoundationMirage.prototype.GetBuildTime = function() { return this.buildTime; };
443 :
444 1 : Engine.RegisterGlobal("FoundationMirage", FoundationMirage);
445 :
446 1 : Foundation.prototype.Mirage = function()
447 : {
448 0 : let mirage = new FoundationMirage();
449 0 : mirage.Init(this);
450 0 : return mirage;
451 : };
452 :
453 1 : Engine.RegisterComponentType(IID_Foundation, "Foundation", Foundation);
|