Line data Source code
1 : function BuildRestrictions() {}
2 :
3 0 : BuildRestrictions.prototype.Schema =
4 : "<a:help>Specifies building placement restrictions as they relate to terrain, territories, and distance.</a:help>" +
5 : "<a:example>" +
6 : "<BuildRestrictions>" +
7 : "<PlacementType>land</PlacementType>" +
8 : "<Territory>own</Territory>" +
9 : "<Category>Structure</Category>" +
10 : "<Distance>" +
11 : "<FromClass>CivilCentre</FromClass>" +
12 : "<MaxDistance>40</MaxDistance>" +
13 : "</Distance>" +
14 : "</BuildRestrictions>" +
15 : "</a:example>" +
16 : "<element name='PlacementType' a:help='Specifies the terrain type restriction for this building.'>" +
17 : "<choice>" +
18 : "<value>land</value>" +
19 : "<value>shore</value>" +
20 : "<value>land-shore</value>"+
21 : "</choice>" +
22 : "</element>" +
23 : "<element name='Territory' a:help='Specifies territory type restrictions for this building.'>" +
24 : "<list>" +
25 : "<oneOrMore>" +
26 : "<choice>" +
27 : "<value>own</value>" +
28 : "<value>ally</value>" +
29 : "<value>neutral</value>" +
30 : "<value>enemy</value>" +
31 : "</choice>" +
32 : "</oneOrMore>" +
33 : "</list>" +
34 : "</element>" +
35 : "<element name='Category' a:help='Specifies the category of this building, for satisfying special constraints. Choices include: Apadana, CivilCentre, Council, Embassy, Fortress, Gladiator, Hall, Hero, Juggernaut, Library, Lighthouse, Monument, Pillar, PyramidLarge, PyramidSmall, Stoa, TempleOfAmun, Theater, Tower, UniqueBuilding, WarDog, Wonder'>" +
36 : "<text/>" +
37 : "</element>" +
38 : "<optional>" +
39 : "<element name='MatchLimit' a:help='Specifies how many times this entity can be created during a match.'>" +
40 : "<data type='positiveInteger'/>" +
41 : "</element>" +
42 : "</optional>" +
43 : "<optional>" +
44 : "<element name='Distance' a:help='Specifies distance restrictions on this building, relative to buildings from the given category.'>" +
45 : "<interleave>" +
46 : "<element name='FromClass'>" +
47 : "<text/>" +
48 : "</element>" +
49 : "<optional><element name='MinDistance'><data type='positiveInteger'/></element></optional>" +
50 : "<optional><element name='MaxDistance'><data type='positiveInteger'/></element></optional>" +
51 : "</interleave>" +
52 : "</element>" +
53 : "</optional>";
54 :
55 0 : BuildRestrictions.prototype.Init = function()
56 : {
57 : };
58 :
59 : /**
60 : * Checks whether building placement is valid
61 : * 1. Visibility is not hidden (may be fogged or visible)
62 : * 2. Check foundation
63 : * a. Doesn't obstruct foundation-blocking entities
64 : * b. On valid terrain, based on passability class
65 : * 3. Territory type is allowed (see note below)
66 : * 4. Dock is on shoreline and facing into water
67 : * 5. Distance constraints satisfied
68 : *
69 : * Returns result object:
70 : * {
71 : * "success": true iff the placement is valid, else false
72 : * "message": message to display in UI for invalid placement, else ""
73 : * "parameters": parameters to use in the GUI message
74 : * "translateMessage": always true
75 : * "translateParameters": list of parameters to translate
76 : * "pluralMessage": we might return a plural translation instead (optional)
77 : * "pluralCount": plural translation argument (optional)
78 : * }
79 : *
80 : * Note: The entity which is used to check this should be a preview entity
81 : * (template name should be "preview|"+templateName), as otherwise territory
82 : * checks for buildings with territory influence will not work as expected.
83 : */
84 0 : BuildRestrictions.prototype.CheckPlacement = function()
85 : {
86 0 : var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
87 0 : var name = cmpIdentity ? cmpIdentity.GetGenericName() : "Building";
88 :
89 0 : var result = {
90 : "success": false,
91 : "message": markForTranslation("%(name)s cannot be built due to unknown error"),
92 : "parameters": {
93 : "name": name,
94 : },
95 : "translateMessage": true,
96 : "translateParameters": ["name"],
97 : };
98 :
99 0 : var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
100 0 : if (!cmpPlayer)
101 0 : return result; // Fail
102 :
103 : // TODO: AI has no visibility info
104 0 : if (!cmpPlayer.IsAI())
105 : {
106 : // Check whether it's in a visible or fogged region
107 0 : var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
108 0 : var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
109 0 : if (!cmpRangeManager || !cmpOwnership)
110 0 : return result; // Fail
111 :
112 0 : var explored = (cmpRangeManager.GetLosVisibility(this.entity, cmpOwnership.GetOwner()) != "hidden");
113 0 : if (!explored)
114 : {
115 0 : result.message = markForTranslation("%(name)s cannot be built in unexplored area");
116 0 : return result; // Fail
117 : }
118 : }
119 :
120 : // Check obstructions and terrain passability
121 0 : var passClassName = "";
122 0 : switch (this.template.PlacementType)
123 : {
124 : case "shore":
125 0 : passClassName = "building-shore";
126 0 : break;
127 :
128 : case "land-shore":
129 : // 'default-terrain-only' is everywhere a normal unit can go, ignoring
130 : // obstructions (i.e. on passable land, and not too deep in the water)
131 0 : passClassName = "default-terrain-only";
132 0 : break;
133 :
134 : case "land":
135 : default:
136 0 : passClassName = "building-land";
137 : }
138 :
139 0 : var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
140 0 : if (!cmpObstruction)
141 0 : return result; // Fail
142 :
143 :
144 0 : if (this.template.Category == "Wall")
145 : {
146 : // for walls, only test the center point
147 0 : var ret = cmpObstruction.CheckFoundation(passClassName, true);
148 : }
149 : else
150 : {
151 0 : var ret = cmpObstruction.CheckFoundation(passClassName, false);
152 : }
153 :
154 0 : if (ret != "success")
155 : {
156 0 : switch (ret)
157 : {
158 : case "fail_error":
159 : case "fail_no_obstruction":
160 0 : error("CheckPlacement: Error returned from CheckFoundation");
161 0 : break;
162 : case "fail_obstructs_foundation":
163 0 : result.message = markForTranslation("%(name)s cannot be built on another building or resource");
164 0 : break;
165 : case "fail_terrain_class":
166 : // TODO: be more specific and/or list valid terrain?
167 0 : result.message = markForTranslation("%(name)s cannot be built on invalid terrain");
168 : }
169 0 : return result; // Fail
170 : }
171 :
172 : // Check territory restrictions
173 0 : var cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager);
174 0 : var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
175 0 : if (!cmpTerritoryManager || !cmpPosition || !cmpPosition.IsInWorld())
176 0 : return result; // Fail
177 :
178 0 : var pos = cmpPosition.GetPosition2D();
179 0 : var tileOwner = cmpTerritoryManager.GetOwner(pos.x, pos.y);
180 0 : var isConnected = !cmpTerritoryManager.IsTerritoryBlinking(pos.x, pos.y);
181 0 : var isOwn = tileOwner == cmpPlayer.GetPlayerID();
182 0 : var isMutualAlly = cmpPlayer.IsExclusiveMutualAlly(tileOwner);
183 0 : var isNeutral = tileOwner == 0;
184 :
185 0 : var invalidTerritory = "";
186 0 : if (isOwn)
187 : {
188 0 : if (!this.HasTerritory("own"))
189 : // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
190 0 : invalidTerritory = markForTranslationWithContext("Territory type", "own");
191 0 : else if (!isConnected && !this.HasTerritory("neutral"))
192 : // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
193 0 : invalidTerritory = markForTranslationWithContext("Territory type", "unconnected own");
194 : }
195 0 : else if (isMutualAlly)
196 : {
197 0 : if (!this.HasTerritory("ally"))
198 : // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
199 0 : invalidTerritory = markForTranslationWithContext("Territory type", "allied");
200 0 : else if (!isConnected && !this.HasTerritory("neutral"))
201 : // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
202 0 : invalidTerritory = markForTranslationWithContext("Territory type", "unconnected allied");
203 : }
204 0 : else if (isNeutral)
205 : {
206 0 : if (!this.HasTerritory("neutral"))
207 : // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
208 0 : invalidTerritory = markForTranslationWithContext("Territory type", "neutral");
209 : }
210 : else
211 : {
212 : // consider everything else enemy territory
213 0 : if (!this.HasTerritory("enemy"))
214 : // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.".
215 0 : invalidTerritory = markForTranslationWithContext("Territory type", "enemy");
216 : }
217 :
218 0 : if (invalidTerritory)
219 : {
220 0 : result.message = markForTranslation("%(name)s cannot be built in %(territoryType)s territory. Valid territories: %(validTerritories)s");
221 0 : result.translateParameters.push("territoryType");
222 0 : result.translateParameters.push("validTerritories");
223 0 : result.parameters.territoryType = { "context": "Territory type", "_string": invalidTerritory };
224 : // gui code will join this array to a string
225 0 : result.parameters.validTerritories = { "context": "Territory type list", "list": this.GetTerritories() };
226 0 : return result; // Fail
227 : }
228 :
229 : // Check special requirements
230 0 : if (this.template.PlacementType == "shore")
231 : {
232 0 : if (!cmpObstruction.CheckShorePlacement())
233 : {
234 0 : result.message = markForTranslation("%(name)s must be built on a valid shoreline");
235 0 : return result; // Fail
236 : }
237 : }
238 :
239 0 : let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
240 :
241 0 : let templateName = cmpTemplateManager.GetCurrentTemplateName(this.entity);
242 0 : let template = cmpTemplateManager.GetTemplate(removeFiltersFromTemplateName(templateName));
243 :
244 : // Check distance restriction
245 0 : if (this.template.Distance)
246 : {
247 0 : var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
248 0 : var cat = this.template.Distance.FromClass;
249 :
250 0 : var filter = function(id)
251 : {
252 0 : var cmpIdentity = Engine.QueryInterface(id, IID_Identity);
253 0 : return cmpIdentity.GetClassesList().indexOf(cat) > -1;
254 : };
255 :
256 0 : if (this.template.Distance.MinDistance !== undefined)
257 : {
258 0 : let minDistance = ApplyValueModificationsToTemplate("BuildRestrictions/Distance/MinDistance", +this.template.Distance.MinDistance, cmpPlayer.GetPlayerID(), template);
259 0 : if (cmpRangeManager.ExecuteQuery(this.entity, 0, minDistance, [cmpPlayer.GetPlayerID()], IID_BuildRestrictions, false).some(filter))
260 : {
261 0 : let result = markForPluralTranslation(
262 : "%(name)s too close to a %(category)s, must be at least %(distance)s meter away",
263 : "%(name)s too close to a %(category)s, must be at least %(distance)s meters away",
264 : minDistance);
265 :
266 0 : result.success = false;
267 0 : result.translateMessage = true;
268 0 : result.parameters = {
269 : "name": name,
270 : "category": cat,
271 : "distance": minDistance
272 : };
273 0 : result.translateParameters = ["name", "category"];
274 0 : return result; // Fail
275 : }
276 : }
277 0 : if (this.template.Distance.MaxDistance !== undefined)
278 : {
279 0 : let maxDistance = ApplyValueModificationsToTemplate("BuildRestrictions/Distance/MaxDistance", +this.template.Distance.MaxDistance, cmpPlayer.GetPlayerID(), template);
280 0 : if (!cmpRangeManager.ExecuteQuery(this.entity, 0, maxDistance, [cmpPlayer.GetPlayerID()], IID_BuildRestrictions, false).some(filter))
281 : {
282 0 : let result = markForPluralTranslation(
283 : "%(name)s too far from a %(category)s, must be within %(distance)s meter",
284 : "%(name)s too far from a %(category)s, must be within %(distance)s meters",
285 : maxDistance);
286 :
287 0 : result.success = false;
288 0 : result.translateMessage = true;
289 0 : result.parameters = {
290 : "name": name,
291 : "category": cat,
292 : "distance": maxDistance
293 : };
294 0 : result.translateParameters = ["name", "category"];
295 0 : return result; // Fail
296 : }
297 : }
298 : }
299 :
300 : // Success
301 0 : result.success = true;
302 0 : result.message = "";
303 0 : return result;
304 : };
305 :
306 0 : BuildRestrictions.prototype.GetCategory = function()
307 : {
308 0 : return this.template.Category;
309 : };
310 :
311 0 : BuildRestrictions.prototype.GetTerritories = function()
312 : {
313 0 : return ApplyValueModificationsToEntity("BuildRestrictions/Territory", this.template.Territory, this.entity).split(/\s+/);
314 : };
315 :
316 0 : BuildRestrictions.prototype.HasTerritory = function(territory)
317 : {
318 0 : return (this.GetTerritories().indexOf(territory) != -1);
319 : };
320 :
321 : // Translation: Territory types being displayed as part of a list like "Valid territories: own, ally".
322 0 : markForTranslationWithContext("Territory type list", "own");
323 : // Translation: Territory types being displayed as part of a list like "Valid territories: own, ally".
324 0 : markForTranslationWithContext("Territory type list", "ally");
325 : // Translation: Territory types being displayed as part of a list like "Valid territories: own, ally".
326 0 : markForTranslationWithContext("Territory type list", "neutral");
327 : // Translation: Territory types being displayed as part of a list like "Valid territories: own, ally".
328 0 : markForTranslationWithContext("Territory type list", "enemy");
329 :
330 0 : Engine.RegisterComponentType(IID_BuildRestrictions, "BuildRestrictions", BuildRestrictions);
|