mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-18 22:33:56 -07:00
Previously (only) setting `multiply` and/or `add` to zero in a tech modification caused warnings saying the format wasn't recognised. With this patch, those cases are now handled as one would expect.
396 lines
9.9 KiB
JavaScript
396 lines
9.9 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 (const modification of modifications)
|
|
{
|
|
if (!DoesModificationApply(modification, classes))
|
|
continue;
|
|
if (!modification.replace)
|
|
warn("GetTechModifiedProperty: modification format not recognized : " + uneval(modification));
|
|
|
|
return modification.replace;
|
|
}
|
|
|
|
return originalValue;
|
|
}
|
|
|
|
function GetTechModifiedProperty_numeric(modifications, classes, originalValue)
|
|
{
|
|
let multiply = 1;
|
|
let add = 0;
|
|
|
|
for (const modification of modifications)
|
|
{
|
|
if (!DoesModificationApply(modification, classes))
|
|
continue;
|
|
if (modification.replace !== undefined)
|
|
return modification.replace;
|
|
if (modification.multiply !== undefined)
|
|
multiply *= modification.multiply;
|
|
else if (modification.add !== undefined)
|
|
add += modification.add;
|
|
else
|
|
warn("GetTechModifiedProperty: numeric modification format not recognized : " + uneval(modification));
|
|
}
|
|
return originalValue * multiply + add;
|
|
}
|
|
|
|
function GetTechModifiedProperty_string(modifications, classes, originalValue)
|
|
{
|
|
let value = originalValue;
|
|
for (const 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 recognized : " + 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)
|
|
{
|
|
const tokens = originalValue === "" ? [] : originalValue.split(/\s+/);
|
|
const newTokens = modification === "" ? [] : modification.split(/\s+/);
|
|
for (const token of newTokens)
|
|
{
|
|
if (token.indexOf(">") !== -1)
|
|
{
|
|
const [oldToken, newToken] = token.split(">");
|
|
const index = tokens.indexOf(oldToken);
|
|
if (index !== -1)
|
|
tokens[index] = newToken;
|
|
}
|
|
else if (token[0] == "-")
|
|
{
|
|
const 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)
|
|
{
|
|
const op = Object.keys(template.requirements)[0];
|
|
const val = template.requirements[op];
|
|
requirements = InterpretTechRequirements(civ, op, val);
|
|
}
|
|
|
|
if (template.supersedes && requirements)
|
|
{
|
|
if (!requirements.length)
|
|
requirements.push({});
|
|
|
|
for (const 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":
|
|
{
|
|
const 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; // tri-state (undefined, false, or true)
|
|
for (const subvalue of value)
|
|
{
|
|
const newOper = Object.keys(subvalue)[0];
|
|
const newValue = subvalue[newOper];
|
|
const 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)
|
|
{
|
|
const 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({});
|
|
|
|
const newRequirements = [];
|
|
for (const currReq of requirements)
|
|
for (const res of result)
|
|
{
|
|
const newReq = {};
|
|
for (const subtype in currReq)
|
|
newReq[subtype] = currReq[subtype];
|
|
|
|
for (const subtype in res)
|
|
{
|
|
if (!newReq[subtype])
|
|
newReq[subtype] = [];
|
|
newReq[subtype] = newReq[subtype].concat(res[subtype]);
|
|
}
|
|
newRequirements.push(newReq);
|
|
}
|
|
requirements = newRequirements;
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
warn("Unknown requirement operator in 'all': " + newOper);
|
|
}
|
|
}
|
|
if (civPermitted === false) // if and only if false
|
|
return false;
|
|
break;
|
|
}
|
|
|
|
case "any":
|
|
{
|
|
let civPermitted = false;
|
|
for (const subvalue of value)
|
|
{
|
|
const newOper = Object.keys(subvalue)[0];
|
|
const newValue = subvalue[newOper];
|
|
const 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)
|
|
{
|
|
const 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 (const res of result)
|
|
requirements.push(res);
|
|
break;
|
|
|
|
default:
|
|
warn("Unknown requirement operator in 'any': " + newOper);
|
|
}
|
|
}
|
|
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)
|
|
{
|
|
const phaseMap = {};
|
|
for (const phaseName in phases)
|
|
{
|
|
const phaseData = phases[phaseName];
|
|
if (!phaseData.reqs.length || !phaseData.reqs[0].techs || !phaseData.replaces)
|
|
continue;
|
|
|
|
const 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;
|
|
}
|
|
|
|
const phaseList = Object.keys(phaseMap);
|
|
phaseList.sort((a, b) => phaseList.indexOf(a) - phaseList.indexOf(phaseMap[b]));
|
|
|
|
return phaseList;
|
|
}
|