forked from mirrors/0ad
e5b971c2bf
This allows modifiers without "affects", or with empty "affects" lists, to do something. This makes it possible to force a modifier on an entity without classes (e.g. trees) or with unknown classes. Accepted By: asterix Differential Revision: https://code.wildfiregames.com/D3761 This was SVN commit r25157.
394 lines
9.7 KiB
JavaScript
394 lines
9.7 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|