Index: ps/trunk/binaries/data/mods/public/globalscripts/Technologies.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/Technologies.js (revision 25156) +++ ps/trunk/binaries/data/mods/public/globalscripts/Technologies.js (revision 25157) @@ -1,390 +1,393 @@ /** * This file contains shared logic for applying tech modifications in GUI, AI, * and simulation scripts. As such it must be fully deterministic and not store * any global state, but each context should do its own caching as needed. * Also it cannot directly access the simulation and requires data passed to it. */ /** * Returns modified property value modified by the applicable tech * modifications. * * @param modifications array of modificiations * @param classes Array containing the class list of the template. * @param originalValue Number storing the original value. Can also be * non-numeric, but then only "replace" and "tokens" techs can be supported. */ function GetTechModifiedProperty(modifications, classes, originalValue) { if (!modifications.length) return originalValue; // From indicative profiling, splitting in two sub-functions or checking directly // is about as efficient, but splitting makes it easier to report errors. if (typeof originalValue === "string") return GetTechModifiedProperty_string(modifications, classes, originalValue); if (typeof originalValue === "number") return GetTechModifiedProperty_numeric(modifications, classes, originalValue); return GetTechModifiedProperty_generic(modifications, classes, originalValue); } function GetTechModifiedProperty_generic(modifications, classes, originalValue) { for (let modification of modifications) { if (!DoesModificationApply(modification, classes)) continue; if (!modification.replace) warn("GetTechModifiedProperty: modification format not recognised : " + uneval(modification)); return modification.replace; } return originalValue; } function GetTechModifiedProperty_numeric(modifications, classes, originalValue) { let multiply = 1; let add = 0; for (let modification of modifications) { if (!DoesModificationApply(modification, classes)) continue; if (modification.replace !== undefined) return modification.replace; if (modification.multiply) multiply *= modification.multiply; else if (modification.add) add += modification.add; else warn("GetTechModifiedProperty: numeric modification format not recognised : " + uneval(modification)); } return originalValue * multiply + add; } function GetTechModifiedProperty_string(modifications, classes, originalValue) { let value = originalValue; for (let modification of modifications) { if (!DoesModificationApply(modification, classes)) continue; if (modification.replace !== undefined) return modification.replace; // Multiple token replacement works, though ordering is not technically guaranteed. // In practice, the order will be that of 'research', which ought to be fine, // and operations like adding tokens are order-independent anyways, // but modders beware if replacement or deletions are implemented. if (modification.tokens !== undefined) value = HandleTokens(value, modification.tokens); else warn("GetTechModifiedProperty: string modification format not recognised : " + uneval(modification)); } return value; } /** * Returns whether the given modification applies to the entity containing the given class list + * NB: returns true if modifications.affects is empty, to allow "affects anything" modifiers. */ function DoesModificationApply(modification, classes) { + if (!modification.affects || !modification.affects.length) + return true; return MatchesClassList(classes, modification.affects); } /** * Returns a modified list of tokens. * Supports "A>B" to replace A by B, "-A" to remove A, and the rest will add tokens. */ function HandleTokens(originalValue, modification) { let tokens = originalValue === "" ? [] : originalValue.split(/\s+/); let newTokens = modification === "" ? [] : modification.split(/\s+/); for (let token of newTokens) { if (token.indexOf(">") !== -1) { let [oldToken, newToken] = token.split(">"); let index = tokens.indexOf(oldToken); if (index !== -1) tokens[index] = newToken; } else if (token[0] == "-") { let index = tokens.indexOf(token.substr(1)); if (index !== -1) tokens.splice(index, 1); } else tokens.push(token); } return tokens.join(" "); } /** * Derives the technology requirements from a given technology template. * Takes into account the `supersedes` attribute. * * @param {Object} template - The template object. Loading of the template must have already occured. * * @return Derived technology requirements. See `InterpretTechRequirements` for object's syntax. */ function DeriveTechnologyRequirements(template, civ) { let requirements = []; if (template.requirements) { let op = Object.keys(template.requirements)[0]; let val = template.requirements[op]; requirements = InterpretTechRequirements(civ, op, val); } if (template.supersedes && requirements) { if (!requirements.length) requirements.push({}); for (let req of requirements) { if (!req.techs) req.techs = []; req.techs.push(template.supersedes); } } return requirements; } /** * Interprets the prerequisite requirements of a technology. * * Takes the initial { key: value } from the short-form requirements object in entity templates, * and parses it into an object that can be more easily checked by simulation and gui. * * Works recursively if needed. * * The returned object is in the form: * ``` * { "techs": ["tech1", "tech2"] }, * { "techs": ["tech3"] } * ``` * or * ``` * { "entities": [[{ * "class": "human", * "number": 2, * "check": "count" * } * or * ``` * false; * ``` * (Or, to translate: * 1. need either both `tech1` and `tech2`, or `tech3` * 2. need 2 entities with the `human` class * 3. cannot research this tech at all) * * @param {string} civ - The civ code * @param {string} operator - The base operation. Can be "civ", "notciv", "tech", "entity", "all" or "any". * @param {mixed} value - The value associated with the above operation. * * @return Object containing the requirements for the given civ, or false if the civ cannot research the tech. */ function InterpretTechRequirements(civ, operator, value) { let requirements = []; switch (operator) { case "civ": return !civ || civ == value ? [] : false; case "notciv": return civ == value ? false : []; case "entity": { let number = value.number || value.numberOfTypes || 0; if (number > 0) requirements.push({ "entities": [{ "class": value.class, "number": number, "check": value.number ? "count" : "variants" }] }); break; } case "tech": requirements.push({ "techs": [value] }); break; case "all": { let civPermitted = undefined; // tri-state (undefined, false, or true) for (let subvalue of value) { let newOper = Object.keys(subvalue)[0]; let newValue = subvalue[newOper]; let result = InterpretTechRequirements(civ, newOper, newValue); switch (newOper) { case "civ": if (result) civPermitted = true; else if (civPermitted !== true) civPermitted = false; break; case "notciv": if (!result) return false; break; case "any": if (!result) return false; // else, fall through case "all": if (!result) { let nullcivreqs = InterpretTechRequirements(null, newOper, newValue); if (!nullcivreqs || !nullcivreqs.length) civPermitted = false; continue; } // else, fall through case "tech": case "entity": { if (result.length) { if (!requirements.length) requirements.push({}); let newRequirements = []; for (let currReq of requirements) for (let res of result) { let newReq = {}; for (let subtype in currReq) newReq[subtype] = currReq[subtype]; for (let subtype in res) { if (!newReq[subtype]) newReq[subtype] = []; newReq[subtype] = newReq[subtype].concat(res[subtype]); } newRequirements.push(newReq); } requirements = newRequirements; } break; } } } if (civPermitted === false) // if and only if false return false; break; } case "any": { let civPermitted = false; for (let subvalue of value) { let newOper = Object.keys(subvalue)[0]; let newValue = subvalue[newOper]; let result = InterpretTechRequirements(civ, newOper, newValue); switch (newOper) { case "civ": if (result) return []; break; case "notciv": if (!result) return false; civPermitted = true; break; case "any": if (!result) { let nullcivreqs = InterpretTechRequirements(null, newOper, newValue); if (!nullcivreqs || !nullcivreqs.length) continue; return false; } // else, fall through case "all": if (!result) continue; civPermitted = true; // else, fall through case "tech": case "entity": for (let res of result) requirements.push(res); break; } } if (!civPermitted && !requirements.length) return false; break; } default: warn("Unknown requirement operator: "+operator); } return requirements; } /** * Determine order of phases. * * @param {Object} phases - The current available store of phases. * @return {array} List of phases */ function UnravelPhases(phases) { let phaseMap = {}; for (let phaseName in phases) { let phaseData = phases[phaseName]; if (!phaseData.reqs.length || !phaseData.reqs[0].techs || !phaseData.replaces) continue; let myPhase = phaseData.replaces[0]; let reqPhase = phaseData.reqs[0].techs[0]; if (phases[reqPhase] && phases[reqPhase].replaces) reqPhase = phases[reqPhase].replaces[0]; phaseMap[myPhase] = reqPhase; if (!phaseMap[reqPhase]) phaseMap[reqPhase] = undefined; } let phaseList = Object.keys(phaseMap); phaseList.sort((a, b) => phaseList.indexOf(a) - phaseList.indexOf(phaseMap[b])); return phaseList; } Index: ps/trunk/binaries/data/mods/public/globalscripts/tests/test_Technologies_effects.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/tests/test_Technologies_effects.js (revision 25156) +++ ps/trunk/binaries/data/mods/public/globalscripts/tests/test_Technologies_effects.js (revision 25157) @@ -1,61 +1,62 @@ // This tests the GetTechModifiedProperty function. function test_numeric() { - let add = [{ "add": 10, "affects": "Unit" }]; + // Also test "no affects" + let add = [{ "add": 10 }]; let add_add = [{ "add": 10, "affects": "Unit" }, { "add": 5, "affects": "Unit" }]; let add_mul_add = [{ "add": 10, "affects": "Unit" }, { "multiply": 2, "affects": "Unit" }, { "add": 5, "affects": "Unit" }]; let add_replace = [{ "add": 10, "affects": "Unit" }, { "replace": 10, "affects": "Unit" }]; let replace_add = [{ "replace": 10, "affects": "Unit" }, { "add": 10, "affects": "Unit" }]; let replace_replace = [{ "replace": 10, "affects": "Unit" }, { "replace": 30, "affects": "Unit" }]; TS_ASSERT_EQUALS(GetTechModifiedProperty(add, "Unit", 5), 15); TS_ASSERT_EQUALS(GetTechModifiedProperty(add_add, "Unit", 5), 20); TS_ASSERT_EQUALS(GetTechModifiedProperty(add_add, "Other", 5), 5); // Technologies work by multiplying then adding all. TS_ASSERT_EQUALS(GetTechModifiedProperty(add_mul_add, "Unit", 5), 25); TS_ASSERT_EQUALS(GetTechModifiedProperty(add_replace, "Unit", 5), 10); // Only the first replace is taken into account TS_ASSERT_EQUALS(GetTechModifiedProperty(replace_replace, "Unit", 5), 10); } test_numeric(); function test_non_numeric() { let replace_nonnum = [{ "replace": "alpha", "affects": "Unit" }]; TS_ASSERT_EQUALS(GetTechModifiedProperty(replace_nonnum, "Unit", "beta"), "alpha"); TS_ASSERT_EQUALS(GetTechModifiedProperty(replace_nonnum, "Structure", "beta"), "beta"); let replace_tokens = [{ "tokens": "-beta alpha gamma -delta", "affects": "Unit" }]; TS_ASSERT_EQUALS(GetTechModifiedProperty(replace_tokens, "Unit", "beta"), "alpha gamma"); TS_ASSERT_EQUALS(GetTechModifiedProperty(replace_tokens, "Structure", "beta"), "beta"); let replace_tokens_2 = [{ "tokens": "beta>gamma -delta", "affects": "Unit" }]; TS_ASSERT_EQUALS(GetTechModifiedProperty(replace_tokens_2, "Unit", "beta"), "gamma"); TS_ASSERT_EQUALS(GetTechModifiedProperty(replace_tokens_2, "Structure", "beta"), "beta"); let replace_tokens_3 = [ { "tokens": "beta>alpha gamma", "affects": "Unit" }, { "tokens": "alpha>zeta -gamma delta", "affects": "Unit" } ]; TS_ASSERT_EQUALS(GetTechModifiedProperty(replace_tokens_3, "Unit", "beta"), "zeta delta"); TS_ASSERT_EQUALS(GetTechModifiedProperty(replace_tokens_3, "Structure", "beta"), "beta"); // Ordering matters. let replace_tokens_4 = [ { "tokens": "alpha>zeta -gamma delta", "affects": "Unit" }, { "tokens": "beta>alpha gamma", "affects": "Unit" } ]; TS_ASSERT_EQUALS(GetTechModifiedProperty(replace_tokens_4, "Unit", "beta"), "alpha delta gamma"); } test_non_numeric();