Define technology pairs in an array

This patch renames the "pair" property of paired technologies to
"partOfPair" for clarity and in the pair parent packs the two techs
(previously "top" and "bottom") into an array called "pair".
This makes the hierarchy more clear in the code and pairs might not be
always placed vertically in the future.
This commit is contained in:
Vantha 2026-05-23 23:27:31 +02:00 committed by wowgetoffyourcellphone
parent 8491b7f084
commit 280a19587f
23 changed files with 51 additions and 83 deletions

View file

@ -132,7 +132,7 @@ class TemplateLoader
{ {
const template = this.loadTechnologyTemplate(templateName); const template = this.loadTechnologyTemplate(templateName);
return { return {
"techs": [template.top, template.bottom], "techs": template.pair,
"reqs": DeriveTechnologyRequirements(template, civCode) "reqs": DeriveTechnologyRequirements(template, civCode)
}; };
} }
@ -308,7 +308,7 @@ class TemplateLoader
isPairTech(technologyCode) isPairTech(technologyCode)
{ {
return !!this.loadTechnologyTemplate(technologyCode).top; return !!this.loadTechnologyTemplate(technologyCode).pair;
} }
isPhaseTech(technologyCode) isPhaseTech(technologyCode)

View file

@ -196,10 +196,10 @@ class TemplateParser
const tech = GetTechnologyDataHelper(template, civCode, g_ResourceData, this.modifiers[civCode] || {}); const tech = GetTechnologyDataHelper(template, civCode, g_ResourceData, this.modifiers[civCode] || {});
tech.name.internal = technologyName; tech.name.internal = technologyName;
if (template.pair !== undefined) if (template.partOfPair !== undefined)
{ {
tech.pair = template.pair; tech.partOfPair = template.partOfPair;
tech.reqs = this.mergeRequirements(tech.reqs, this.TemplateLoader.loadTechnologyPairTemplate(template.pair).reqs); tech.reqs = this.mergeRequirements(tech.reqs, this.TemplateLoader.loadTechnologyPairTemplate(template.partOfPair).reqs);
} }
if (this.TemplateLoader.isPhaseTech(technologyName)) if (this.TemplateLoader.isPhaseTech(technologyName))

View file

@ -684,8 +684,8 @@ g_SelectionPanels.Research = {
(item.tech == tech || (item.tech == tech ||
item.tech.pair && item.tech.pair &&
tech.pair && tech.pair &&
item.tech.bottom == tech.bottom && item.tech.pair?.[0] == tech.pair?.[0] &&
item.tech.top == tech.top) && item.tech.pair?.[1] == tech.pair?.[1]) &&
Object.keys(item.techCostMultiplier).every( Object.keys(item.techCostMultiplier).every(
k => item.techCostMultiplier[k] == state.researcher.techCostMultiplier[k]) k => item.techCostMultiplier[k] == state.researcher.techCostMultiplier[k])
)); ));
@ -731,7 +731,7 @@ g_SelectionPanels.Research = {
// Handle one or two techs (tech pair) // Handle one or two techs (tech pair)
const player = data.player; const player = data.player;
const playerState = GetSimState().players[player]; const playerState = GetSimState().players[player];
for (const tech of data.item.tech.pair ? [data.item.tech.bottom, data.item.tech.top] : [data.item.tech]) for (const tech of (data.item.tech.pair || [data.item.tech]))
{ {
// Don't change the object returned by GetTechnologyData // Don't change the object returned by GetTechnologyData
const template = clone(GetTechnologyData(tech, playerState.civ)); const template = clone(GetTechnologyData(tech, playerState.civ));

View file

@ -48,8 +48,8 @@ GameState.prototype.init = function(SharedScript, state, player)
{ {
// Cannot call pickrandom because this function is called on rejoin and that causes oos. // Cannot call pickrandom because this function is called on rejoin and that causes oos.
// (reverting rP20750) // (reverting rP20750)
techName = this.playerData.disabledTechnologies[techData._template.bottom] ? techName = this.playerData.disabledTechnologies[techData._template.pair[0]] ?
techData._template.top : techData._template.bottom; techData._template.pair[1] : techData._template.pair[0];
const supersedes = techData._template.supersedes; const supersedes = techData._template.supersedes;
techData = clone(this.getTemplate(techName)); techData = clone(this.getTemplate(techName));

View file

@ -7,15 +7,14 @@ export function Technology(templateName)
const template = TechnologyTemplates.Get(templateName); const template = TechnologyTemplates.Get(templateName);
// check if this is one of two paired technologies. // check if this is one of two paired technologies.
this._isPair = template.pair !== undefined; if (template.partOfPair)
if (this._isPair)
{ {
const pairTech = TechnologyTemplates.Get(template.pair); const parentTech = TechnologyTemplates.Get(template.partOfPair);
this._pairedWith = pairTech.top == templateName ? pairTech.bottom : pairTech.top; this._pairedWith = parentTech.pair[0] == templateName ? parentTech.pair[1] : parentTech.pair[0];
} }
// check if it only defines a pair: // check if it only defines a pair:
this._definesPair = template.top !== undefined; this._definesPair = !!template.pair;
this._template = template; this._template = template;
} }
@ -41,22 +40,17 @@ Technology.prototype.getPairedTechs = function()
if (!this._definesPair) if (!this._definesPair)
return undefined; return undefined;
return [ return this._template.pair.map(name => new Technology(name));
new Technology(this._template.top),
new Technology(this._template.bottom)
];
}; };
Technology.prototype.pair = function() Technology.prototype.pair = function()
{ {
if (!this._isPair) return this._template.partOfPair;
return undefined;
return this._template.pair;
}; };
Technology.prototype.pairedWith = function() Technology.prototype.pairedWith = function()
{ {
if (!this._isPair) if (!this._template.partOfPair)
return undefined; return undefined;
return this._pairedWith; return this._pairedWith;
}; };

View file

@ -234,36 +234,22 @@ Researcher.prototype.GetTechnologiesList = function()
superseded[template.supersedes] = tech; superseded[template.supersedes] = tech;
} }
// Now make researched/in progress techs invisible. return techList.map(tech =>
for (const i in techList)
{ {
let tech = techList[i]; // Make researched/in progress techs invisible.
while (this.IsTechnologyResearchedOrInProgress(tech)) while (tech && this.IsTechnologyResearchedOrInProgress(tech))
tech = superseded[tech]; tech = superseded[tech]; // might be undefined
techList[i] = tech;
}
const ret = [];
// This inserts the techs into the correct positions to line up the technology pairs.
for (let i = 0; i < techList.length; ++i)
{
const tech = techList[i];
if (!tech) if (!tech)
{ return undefined;
ret[i] = undefined;
continue;
}
// "Unwrap" tech pairs
const template = TechnologyTemplates.Get(tech); const template = TechnologyTemplates.Get(tech);
if (template.top) if (template?.pair)
ret[i] = { "pair": true, "top": template.top, "bottom": template.bottom }; tech = { "pair": template.pair };
else
ret[i] = tech;
}
return ret; return tech;
});
}; };
/** /**
@ -294,11 +280,9 @@ Researcher.prototype.IsTechnologyResearchedOrInProgress = function(tech)
return false; return false;
const template = TechnologyTemplates.Get(tech); const template = TechnologyTemplates.Get(tech);
if (template.top) if (template.pair)
return cmpTechnologyManager.IsTechnologyResearched(template.top) || return template.pair.some(t => cmpTechnologyManager.IsTechnologyResearched(t) ||
cmpTechnologyManager.IsInProgress(template.top) || cmpTechnologyManager.IsInProgress(t));
cmpTechnologyManager.IsTechnologyResearched(template.bottom) ||
cmpTechnologyManager.IsInProgress(template.bottom);
return cmpTechnologyManager.IsTechnologyResearched(tech) || cmpTechnologyManager.IsInProgress(tech); return cmpTechnologyManager.IsTechnologyResearched(tech) || cmpTechnologyManager.IsInProgress(tech);
}; };
@ -310,9 +294,7 @@ Researcher.prototype.IsTechnologyResearchedOrInProgress = function(tech)
*/ */
Researcher.prototype.QueueTechnology = function(templateName, metadata) Researcher.prototype.QueueTechnology = function(templateName, metadata)
{ {
if (!this.GetTechnologiesList().some(tech => if (!this.GetTechnologiesList().some(tech => tech === templateName || tech?.pair?.includes(templateName)))
tech && (tech == templateName ||
tech.pair && (tech.top == templateName || tech.bottom == templateName))))
{ {
error("This entity cannot research " + templateName + "."); error("This entity cannot research " + templateName + ".");
return -1; return -1;

View file

@ -212,7 +212,7 @@ TechnologyManager.prototype.Init = function()
this.unresearchedAutoResearchTechs = new Set(); this.unresearchedAutoResearchTechs = new Set();
const allTechs = TechnologyTemplates.GetAll(); const allTechs = TechnologyTemplates.GetAll();
for (const key in allTechs) for (const key in allTechs)
if (allTechs[key].autoResearch || allTechs[key].top) if (allTechs[key].autoResearch || allTechs[key].pair)
this.unresearchedAutoResearchTechs.add(key); this.unresearchedAutoResearchTechs.add(key);
}; };
@ -263,8 +263,7 @@ TechnologyManager.prototype.UpdateAutoResearch = function()
for (const key of this.unresearchedAutoResearchTechs) for (const key of this.unresearchedAutoResearchTechs)
{ {
const tech = TechnologyTemplates.Get(key); const tech = TechnologyTemplates.Get(key);
if ((tech.autoResearch && this.CanResearch(key)) || if ((tech.autoResearch && this.CanResearch(key)) || (tech.pair?.some(t => this.IsTechnologyResearched(t))))
(tech.top && (this.IsTechnologyResearched(tech.top) || this.IsTechnologyResearched(tech.bottom))))
{ {
this.unresearchedAutoResearchTechs.delete(key); this.unresearchedAutoResearchTechs.delete(key);
this.ResearchTechnology(key); this.ResearchTechnology(key);
@ -306,11 +305,10 @@ TechnologyManager.prototype.CanResearch = function(tech)
return false; return false;
} }
if (template.top && this.IsInProgress(template.top) || if (template.pair?.some(t => this.IsInProgress(t)))
template.bottom && this.IsInProgress(template.bottom))
return false; return false;
if (template.pair && !this.CanResearch(template.pair)) if (template.partOfPair && !this.CanResearch(template.partOfPair))
return false; return false;
if (this.IsInProgress(tech)) if (this.IsInProgress(tech))

View file

@ -1,5 +1,5 @@
{ {
"pair": "pair_unlock_civil_engineering_han", "partOfPair": "pair_unlock_civil_engineering_han",
"genericName": "Improved Construction", "genericName": "Improved Construction",
"specificName": { "han": "Gōngchéng" }, "specificName": { "han": "Gōngchéng" },
"description": "", "description": "",

View file

@ -1,5 +1,5 @@
{ {
"pair": "pair_unlock_civil_engineering_han", "partOfPair": "pair_unlock_civil_engineering_han",
"genericName": "Robust Architecture", "genericName": "Robust Architecture",
"specificName": { "han": "Gōngchéng" }, "specificName": { "han": "Gōngchéng" },
"description": "", "description": "",

View file

@ -1,5 +1,5 @@
{ {
"pair": "pair_unlock_civil_service_han", "partOfPair": "pair_unlock_civil_service_han",
"genericName": "Efficient Bureaucracy", "genericName": "Efficient Bureaucracy",
"specificName": { "han": "Guānliáo" }, "specificName": { "han": "Guānliáo" },
"description": "", "description": "",

View file

@ -1,5 +1,5 @@
{ {
"pair": "pair_unlock_civil_service_han", "partOfPair": "pair_unlock_civil_service_han",
"genericName": "Intensive Training", "genericName": "Intensive Training",
"specificName": { "han": "Guānliáo" }, "specificName": { "han": "Guānliáo" },
"description": "", "description": "",

View file

@ -1,5 +1,5 @@
{ {
"pair": "pair_gather_food_maur", "partOfPair": "pair_gather_food_maur",
"genericName": "Ahimsa", "genericName": "Ahimsa",
"description": "Ahimsa is the ancient Indian principle of nonviolence which applies to actions towards all living beings. It is a key virtue in Indian religions like Jainism, Buddhism, Hinduism, and Sikhism.", "description": "Ahimsa is the ancient Indian principle of nonviolence which applies to actions towards all living beings. It is a key virtue in Indian religions like Jainism, Buddhism, Hinduism, and Sikhism.",
"cost": { "cost": {

View file

@ -1,5 +1,5 @@
{ {
"pair": "pair_gather_food_maur", "partOfPair": "pair_gather_food_maur",
"genericName": "Wicker Baskets", "genericName": "Wicker Baskets",
"description": "Equip your foragers with wicker baskets for foraging.", "description": "Equip your foragers with wicker baskets for foraging.",
"cost": { "cost": {

View file

@ -1,6 +1,5 @@
{ {
"genericName": "Wicker Basket vs Ahimsa", "genericName": "Wicker Basket vs Ahimsa",
"top": "gather_wicker_baskets_maur", "pair": ["gather_wicker_baskets_maur", "gather_ahimsa"],
"bottom": "gather_ahimsa",
"requirements": { "civ": "maur" } "requirements": { "civ": "maur" }
} }

View file

@ -1,6 +1,5 @@
{ {
"genericName": "Traditional Army vs Reform Army", "genericName": "Traditional Army vs Reform Army",
"top": "traditional_army_sele", "pair": ["traditional_army_sele", "reformed_army_sele"],
"bottom": "reformed_army_sele",
"requirements": { "civ": "sele" } "requirements": { "civ": "sele" }
} }

View file

@ -1,6 +1,5 @@
{ {
"genericName": "Civil Engineering", "genericName": "Civil Engineering",
"top": "civil_engineering_01", "pair": ["civil_engineering_01", "civil_engineering_02"],
"bottom": "civil_engineering_02",
"requirements": { "civ": "han" } "requirements": { "civ": "han" }
} }

View file

@ -1,6 +1,5 @@
{ {
"genericName": "Civil Service", "genericName": "Civil Service",
"top": "civil_service_01", "pair": ["civil_service_01", "civil_service_02"],
"bottom": "civil_service_02",
"requirements": { "civ": "han" } "requirements": { "civ": "han" }
} }

View file

@ -1,6 +1,5 @@
{ {
"genericName": "Cult", "genericName": "Cult",
"top": "pharaonic_cult", "pair": ["pharaonic_cult", "serapis_cult"],
"bottom": "serapis_cult",
"requirements": { "civ": "ptol" } "requirements": { "civ": "ptol" }
} }

View file

@ -1,5 +1,5 @@
{ {
"pair": "pair_unlock_cult_ptol", "partOfPair": "pair_unlock_cult_ptol",
"genericName": "Pharaonic Cult", "genericName": "Pharaonic Cult",
"description": "The Pharaohs were worshipped as living gods. Their word was sacrosanct and beyond reproach, at least among the common people. The Ptolemaic dynasts certainly took advantage of this ancient custom to the fullest, to varying degrees of success.", "description": "The Pharaohs were worshipped as living gods. Their word was sacrosanct and beyond reproach, at least among the common people. The Ptolemaic dynasts certainly took advantage of this ancient custom to the fullest, to varying degrees of success.",
"cost": { "cost": {

View file

@ -1,5 +1,5 @@
{ {
"pair": "pair_unlock_champions_sele", "partOfPair": "pair_unlock_champions_sele",
"genericName": "Reform Army", "genericName": "Reform Army",
"description": "The Roman-style core of the Seleucid army.", "description": "The Roman-style core of the Seleucid army.",
"requirements": { "requirements": {

View file

@ -1,5 +1,5 @@
{ {
"pair": "pair_unlock_cult_ptol", "partOfPair": "pair_unlock_cult_ptol",
"genericName": "Serapis Cult", "genericName": "Serapis Cult",
"description": "The cult of Serapis was introduced during the 3rd century BC on the orders of Ptolemy I of Egypt as a means to unify the Greeks and Egyptians in his realm. The god was depicted as Greek in appearance, but with Egyptian trappings, and combined iconography from a great many cults, signifying both abundance and resurrection. A serapeion was any temple or religious precinct devoted to Serapis. The cult of Serapis was spread as a matter of deliberate policy by the Ptolemaic kings, who also built an immense serapeum in Alexandria.", "description": "The cult of Serapis was introduced during the 3rd century BC on the orders of Ptolemy I of Egypt as a means to unify the Greeks and Egyptians in his realm. The god was depicted as Greek in appearance, but with Egyptian trappings, and combined iconography from a great many cults, signifying both abundance and resurrection. A serapeion was any temple or religious precinct devoted to Serapis. The cult of Serapis was spread as a matter of deliberate policy by the Ptolemaic kings, who also built an immense serapeum in Alexandria.",
"cost": { "cost": {

View file

@ -1,5 +1,5 @@
{ {
"pair": "pair_unlock_champions_sele", "partOfPair": "pair_unlock_champions_sele",
"genericName": "Traditional Army", "genericName": "Traditional Army",
"description": "The Macedonian-style core of the Seleucid army.", "description": "The Macedonian-style core of the Seleucid army.",
"requirements": { "requirements": {

View file

@ -151,7 +151,6 @@ function Cheat(input)
// try to spilt the input // try to spilt the input
const tmp = input.parameter.split(/\s+/); const tmp = input.parameter.split(/\s+/);
const number = +tmp[0]; const number = +tmp[0];
const pair = tmp.length > 1 && (tmp[1] == "top" || tmp[1] == "bottom") ? tmp[1] : "top"; // use top as default value
// check, if valid number was parsed. // check, if valid number was parsed.
if (!isNaN(number)) if (!isNaN(number))
@ -166,7 +165,7 @@ function Cheat(input)
// get name of tech // get name of tech
if (tech.pair) if (tech.pair)
techname = tech[pair]; techname = tech.pair[tmp[1] === "1" ? 1 : 0];
else else
techname = tech; techname = tech;
} }