Line data Source code
1 : /**
2 : * This file contains shared logic for applying tech modifications in GUI, AI,
3 : * and simulation scripts. As such it must be fully deterministic and not store
4 : * any global state, but each context should do its own caching as needed.
5 : * Also it cannot directly access the simulation and requires data passed to it.
6 : */
7 :
8 : /**
9 : * Returns modified property value modified by the applicable tech
10 : * modifications.
11 : *
12 : * @param modifications array of modificiations
13 : * @param classes Array containing the class list of the template.
14 : * @param originalValue Number storing the original value. Can also be
15 : * non-numeric, but then only "replace" and "tokens" techs can be supported.
16 : */
17 : function GetTechModifiedProperty(modifications, classes, originalValue)
18 : {
19 52 : if (!modifications.length)
20 0 : return originalValue;
21 :
22 : // From indicative profiling, splitting in two sub-functions or checking directly
23 : // is about as efficient, but splitting makes it easier to report errors.
24 52 : if (typeof originalValue === "string")
25 9 : return GetTechModifiedProperty_string(modifications, classes, originalValue);
26 43 : if (typeof originalValue === "number")
27 43 : return GetTechModifiedProperty_numeric(modifications, classes, originalValue);
28 0 : return GetTechModifiedProperty_generic(modifications, classes, originalValue);
29 : }
30 :
31 : function GetTechModifiedProperty_generic(modifications, classes, originalValue)
32 : {
33 0 : for (let modification of modifications)
34 : {
35 0 : if (!DoesModificationApply(modification, classes))
36 0 : continue;
37 0 : if (!modification.replace)
38 0 : warn("GetTechModifiedProperty: modification format not recognised : " + uneval(modification));
39 :
40 0 : return modification.replace;
41 : }
42 :
43 0 : return originalValue;
44 : }
45 :
46 : function GetTechModifiedProperty_numeric(modifications, classes, originalValue)
47 : {
48 43 : let multiply = 1;
49 43 : let add = 0;
50 :
51 43 : for (let modification of modifications)
52 : {
53 82 : if (!DoesModificationApply(modification, classes))
54 35 : continue;
55 47 : if (modification.replace !== undefined)
56 6 : return modification.replace;
57 41 : if (modification.multiply)
58 3 : multiply *= modification.multiply;
59 38 : else if (modification.add)
60 38 : add += modification.add;
61 : else
62 0 : warn("GetTechModifiedProperty: numeric modification format not recognised : " + uneval(modification));
63 : }
64 37 : return originalValue * multiply + add;
65 : }
66 :
67 : function GetTechModifiedProperty_string(modifications, classes, originalValue)
68 : {
69 9 : let value = originalValue;
70 9 : for (let modification of modifications)
71 : {
72 12 : if (!DoesModificationApply(modification, classes))
73 5 : continue;
74 7 : if (modification.replace !== undefined)
75 1 : return modification.replace;
76 : // Multiple token replacement works, though ordering is not technically guaranteed.
77 : // In practice, the order will be that of 'research', which ought to be fine,
78 : // and operations like adding tokens are order-independent anyways,
79 : // but modders beware if replacement or deletions are implemented.
80 6 : if (modification.tokens !== undefined)
81 6 : value = HandleTokens(value, modification.tokens);
82 : else
83 0 : warn("GetTechModifiedProperty: string modification format not recognised : " + uneval(modification));
84 : }
85 8 : return value;
86 : }
87 :
88 :
89 : /**
90 : * Returns whether the given modification applies to the entity containing the given class list
91 : * NB: returns true if modifications.affects is empty, to allow "affects anything" modifiers.
92 : */
93 : function DoesModificationApply(modification, classes)
94 : {
95 95 : if (!modification.affects || !modification.affects.length)
96 1 : return true;
97 94 : return MatchesClassList(classes, modification.affects);
98 : }
99 :
100 : /**
101 : * Returns a modified list of tokens.
102 : * Supports "A>B" to replace A by B, "-A" to remove A, and the rest will add tokens.
103 : */
104 : function HandleTokens(originalValue, modification)
105 : {
106 13 : let tokens = originalValue === "" ? [] : originalValue.split(/\s+/);
107 13 : let newTokens = modification === "" ? [] : modification.split(/\s+/);
108 13 : for (let token of newTokens)
109 : {
110 36 : if (token.indexOf(">") !== -1)
111 : {
112 11 : let [oldToken, newToken] = token.split(">");
113 11 : let index = tokens.indexOf(oldToken);
114 11 : if (index !== -1)
115 5 : tokens[index] = newToken;
116 : }
117 25 : else if (token[0] == "-")
118 : {
119 12 : let index = tokens.indexOf(token.substr(1));
120 12 : if (index !== -1)
121 4 : tokens.splice(index, 1);
122 : }
123 : else
124 13 : tokens.push(token);
125 : }
126 13 : return tokens.join(" ");
127 : }
128 :
129 : /**
130 : * Derives the technology requirements from a given technology template.
131 : * Takes into account the `supersedes` attribute.
132 : *
133 : * @param {Object} template - The template object. Loading of the template must have already occured.
134 : *
135 : * @return Derived technology requirements. See `InterpretTechRequirements` for object's syntax.
136 : */
137 : function DeriveTechnologyRequirements(template, civ)
138 : {
139 144 : let requirements = [];
140 :
141 144 : if (template.requirements)
142 : {
143 114 : let op = Object.keys(template.requirements)[0];
144 114 : let val = template.requirements[op];
145 114 : requirements = InterpretTechRequirements(civ, op, val);
146 : }
147 :
148 144 : if (template.supersedes && requirements)
149 : {
150 0 : if (!requirements.length)
151 0 : requirements.push({});
152 :
153 0 : for (let req of requirements)
154 : {
155 0 : if (!req.techs)
156 0 : req.techs = [];
157 0 : req.techs.push(template.supersedes);
158 : }
159 : }
160 :
161 144 : return requirements;
162 : }
163 :
164 : /**
165 : * Interprets the prerequisite requirements of a technology.
166 : *
167 : * Takes the initial { key: value } from the short-form requirements object in entity templates,
168 : * and parses it into an object that can be more easily checked by simulation and gui.
169 : *
170 : * Works recursively if needed.
171 : *
172 : * The returned object is in the form:
173 : * ```
174 : * { "techs": ["tech1", "tech2"] },
175 : * { "techs": ["tech3"] }
176 : * ```
177 : * or
178 : * ```
179 : * { "entities": [[{
180 : * "class": "human",
181 : * "number": 2,
182 : * "check": "count"
183 : * }
184 : * or
185 : * ```
186 : * false;
187 : * ```
188 : * (Or, to translate:
189 : * 1. need either both `tech1` and `tech2`, or `tech3`
190 : * 2. need 2 entities with the `human` class
191 : * 3. cannot research this tech at all)
192 : *
193 : * @param {string} civ - The civ code
194 : * @param {string} operator - The base operation. Can be "civ", "notciv", "tech", "entity", "all" or "any".
195 : * @param {mixed} value - The value associated with the above operation.
196 : *
197 : * @return Object containing the requirements for the given civ, or false if the civ cannot research the tech.
198 : */
199 : function InterpretTechRequirements(civ, operator, value)
200 : {
201 473 : let requirements = [];
202 :
203 473 : switch (operator)
204 : {
205 : case "civ":
206 97 : return !civ || civ == value ? [] : false;
207 :
208 : case "notciv":
209 70 : return civ == value ? false : [];
210 :
211 : case "entity":
212 : {
213 33 : let number = value.number || value.numberOfTypes || 0;
214 33 : if (number > 0)
215 33 : requirements.push({
216 : "entities": [{
217 : "class": value.class,
218 : "number": number,
219 : "check": value.number ? "count" : "variants"
220 : }]
221 : });
222 33 : break;
223 : }
224 :
225 : case "tech":
226 83 : requirements.push({
227 : "techs": [value]
228 : });
229 83 : break;
230 :
231 : case "all":
232 : {
233 104 : let civPermitted = undefined; // tri-state (undefined, false, or true)
234 104 : for (let subvalue of value)
235 : {
236 198 : let newOper = Object.keys(subvalue)[0];
237 198 : let newValue = subvalue[newOper];
238 198 : let result = InterpretTechRequirements(civ, newOper, newValue);
239 :
240 198 : switch (newOper)
241 : {
242 : case "civ":
243 57 : if (result)
244 23 : civPermitted = true;
245 34 : else if (civPermitted !== true)
246 27 : civPermitted = false;
247 57 : break;
248 :
249 : case "notciv":
250 37 : if (!result)
251 12 : return false;
252 25 : break;
253 :
254 : case "any":
255 17 : if (!result)
256 7 : return false;
257 : // else, fall through
258 :
259 : case "all":
260 26 : if (!result)
261 : {
262 6 : let nullcivreqs = InterpretTechRequirements(null, newOper, newValue);
263 6 : if (!nullcivreqs || !nullcivreqs.length)
264 4 : civPermitted = false;
265 6 : continue;
266 : }
267 : // else, fall through
268 :
269 : case "tech":
270 : case "entity":
271 : {
272 91 : if (result.length)
273 : {
274 81 : if (!requirements.length)
275 62 : requirements.push({});
276 :
277 81 : let newRequirements = [];
278 81 : for (let currReq of requirements)
279 83 : for (let res of result)
280 : {
281 89 : let newReq = {};
282 89 : for (let subtype in currReq)
283 26 : newReq[subtype] = currReq[subtype];
284 :
285 89 : for (let subtype in res)
286 : {
287 91 : if (!newReq[subtype])
288 72 : newReq[subtype] = [];
289 91 : newReq[subtype] = newReq[subtype].concat(res[subtype]);
290 : }
291 89 : newRequirements.push(newReq);
292 : }
293 81 : requirements = newRequirements;
294 : }
295 91 : break;
296 : }
297 :
298 : }
299 : }
300 85 : if (civPermitted === false) // if and only if false
301 22 : return false;
302 63 : break;
303 : }
304 :
305 : case "any":
306 : {
307 86 : let civPermitted = false;
308 86 : for (let subvalue of value)
309 : {
310 151 : let newOper = Object.keys(subvalue)[0];
311 151 : let newValue = subvalue[newOper];
312 151 : let result = InterpretTechRequirements(civ, newOper, newValue);
313 :
314 151 : switch (newOper)
315 : {
316 :
317 : case "civ":
318 38 : if (result)
319 14 : return [];
320 24 : break;
321 :
322 : case "notciv":
323 31 : if (!result)
324 10 : return false;
325 21 : civPermitted = true;
326 21 : break;
327 :
328 : case "any":
329 12 : if (!result)
330 : {
331 4 : let nullcivreqs = InterpretTechRequirements(null, newOper, newValue);
332 4 : if (!nullcivreqs || !nullcivreqs.length)
333 4 : continue;
334 0 : return false;
335 : }
336 : // else, fall through
337 :
338 : case "all":
339 36 : if (!result)
340 14 : continue;
341 22 : civPermitted = true;
342 : // else, fall through
343 :
344 : case "tech":
345 : case "entity":
346 64 : for (let res of result)
347 60 : requirements.push(res);
348 64 : break;
349 :
350 : }
351 : }
352 62 : if (!civPermitted && !requirements.length)
353 14 : return false;
354 48 : break;
355 : }
356 :
357 : default:
358 0 : warn("Unknown requirement operator: "+operator);
359 : }
360 :
361 227 : return requirements;
362 : }
363 :
364 : /**
365 : * Determine order of phases.
366 : *
367 : * @param {Object} phases - The current available store of phases.
368 : * @return {array} List of phases
369 : */
370 : function UnravelPhases(phases)
371 : {
372 0 : let phaseMap = {};
373 0 : for (let phaseName in phases)
374 : {
375 0 : let phaseData = phases[phaseName];
376 0 : if (!phaseData.reqs.length || !phaseData.reqs[0].techs || !phaseData.replaces)
377 0 : continue;
378 :
379 0 : let myPhase = phaseData.replaces[0];
380 0 : let reqPhase = phaseData.reqs[0].techs[0];
381 0 : if (phases[reqPhase] && phases[reqPhase].replaces)
382 0 : reqPhase = phases[reqPhase].replaces[0];
383 :
384 0 : phaseMap[myPhase] = reqPhase;
385 0 : if (!phaseMap[reqPhase])
386 0 : phaseMap[reqPhase] = undefined;
387 : }
388 :
389 0 : let phaseList = Object.keys(phaseMap);
390 0 : phaseList.sort((a, b) => phaseList.indexOf(a) - phaseList.indexOf(phaseMap[b]));
391 :
392 0 : return phaseList;
393 : }
|